Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • Jw_sonar_backup
  • add_route_assignments
  • add_route_user
  • assignment_filter
  • bedran_exercise-list
  • exercise_list_filter
  • interactive-mode-preference
  • jw_sonar
  • main
  • move-to-esm-only
  • v6.0.0
  • 2.0.0
  • 2.1.0
  • 2.2.0
  • 3.0.0
  • 3.0.1
  • 3.1.0
  • 3.1.1
  • 3.1.2
  • 3.2.0
  • 3.2.2
  • 3.2.3
  • 3.3.0
  • 3.4.1
  • 3.4.2
  • 3.5.0
  • 4.0.0
  • 4.0.1
  • 4.1.0
  • 4.1.1
  • 4.2.0
  • 5.0.0
  • 6.0.0-dev
  • Latest
  • Pre-alpha
  • v1.0.1
36 results

Target

Select target project
  • Dojo_Project_Nguyen/ui/dojocli
  • dojo_project/projects/ui/dojocli
  • tom.andrivet/dojocli
  • orestis.malaspin/dojocli
4 results
Select Git revision
Show changes
Showing
with 1454 additions and 1685 deletions
import { Command } from 'commander';
import { existsSync, renameSync } from 'fs';
import ora from 'ora';
import TextStyle from '../types/TextStyle.js';
import inquirer from 'inquirer';
import fs from 'fs-extra';
function renameFile(filename: string, showWarning: boolean) {
const oldFilename = `${ filename }.old`;
const spinner: ora.Ora = ora(`Renaming ${ TextStyle.CODE(filename) } in ${ TextStyle.CODE(oldFilename) } ...`).start();
try {
renameSync(filename, oldFilename);
spinner.succeed(`Renaming success: ${ TextStyle.CODE(filename) } in ${ TextStyle.CODE(oldFilename) }`);
if ( showWarning ) {
console.log(`${ TextStyle.WARNING('Warning:') } Your ${ TextStyle.CODE(filename) } was renamed ${ TextStyle.CODE(oldFilename) }. If this was not intended please revert this change.`);
}
} catch ( error ) {
spinner.fail(`Renaming failed: ${ error }.`);
}
}
async function askConfirmation(msg: string): Promise<boolean> {
return (await inquirer.prompt({
name : 'confirm',
message: msg,
type : 'confirm',
default: false
})).confirm as boolean;
}
// Returns false, when the renaming is interrupted
export async function tryRenameFile(path: string, force: boolean): Promise<boolean> {
const fileExists = existsSync(path);
if ( fileExists && force ) {
renameFile(path, false);
} else if ( fileExists ) {
const confirm = (await askConfirmation(`${ TextStyle.CODE(path) } in ${ TextStyle.CODE(path + '.old') }. Are you sure?`));
if ( confirm ) {
renameFile(path, true);
} else {
console.log(`${ TextStyle.BLOCK('Completion generation interrupted.') }`);
return false;
}
}
return true;
}
const fishFunction = `
function __fish_dojo_using_commands
set cmd (commandline -opc)
set num_cmd (count $cmd)
if [ $num_cmd -eq $argv[1] ]
for i in (seq 1 (math $num_cmd))
if [ $argv[(math $i+1)] != $cmd[$i] ]
return 1
end
end
return 0
end
return 1
end
complete -f -c dojo
`;
function isHidden(cmd: Command): boolean {
return (cmd as Command & { _hidden: boolean })._hidden;
}
function isLeaf(cmd: Command): boolean {
return cmd.commands.length === 0;
}
function flatten(cmd: Command): Array<Command> {
if ( isLeaf(cmd) ) {
return [ cmd ];
} else {
return cmd.commands
.filter(c => !isHidden(c))
.map(child => flatten(child))
.reduce((acc, subCmd) => acc.concat(subCmd), [ cmd ]);
}
}
// Computes the maximum number of commands until a leaf is reached
function computeDepth(cmd: Command | undefined): number {
if ( cmd === undefined ) {
return 0;
} else {
return 1 + cmd.commands.filter(c => !isHidden(c)).map(subCmd => computeDepth(subCmd)).reduce((acc, depth) => depth > acc ? depth : acc, 0);
}
}
// Computes the maximum number of commands until the root is reached
function computeHeight(cmd: Command | null): number {
let height = 0;
let tmp = cmd;
while ( tmp !== null ) {
tmp = tmp.parent;
height += 1;
}
return height;
}
// Computes the maximum number of commands until the root is reached
export function getRoot(cmd: Command): Command {
if ( cmd.parent == null ) {
return cmd;
} else {
return getRoot(cmd.parent);
}
}
function getOptions(cmd: Command): string {
// we remove <args>, [command], and , from option lines
return cmd.options.filter(opt => !opt.hidden).map(opt => opt.flags.replace(/<.*?>/, '').replace(/\[.*?]/, '').replace(',', '').trimEnd()).join(' ');
}
function commandsAndOptionsToString(cmd: Command): string {
return cmd.commands.filter(c => !isHidden(c)).map(c => c.name()).join(' ').concat(' ' + getOptions(cmd)).trim().concat(' --help -h').trim();
}
function addLine(identLevel: number, pattern: string): string {
return `${ ' '.repeat(identLevel) }${ pattern }\n`;
}
export function getCommandFromChain(currentCmd: Command, chain: Array<string>): Command | null {
if ( chain.length === 0 ) {
return currentCmd;
} else {
const subCmd = currentCmd.commands.find(c => c.name() === chain[0]);
if ( subCmd === undefined ) {
return currentCmd;
} else {
return getCommandFromChain(subCmd, chain.slice(1));
}
}
}
function generateBashSubCommands(cmd: Command, current: number, maxDepth: number, ident: number): string {
if ( current === maxDepth ) {
return addLine(ident, `case "\${COMP_WORDS[$COMP_CWORD - ${ maxDepth - current + 1 }]}" in`) + addLine(ident + 1, `${ cmd.name() })`) + addLine(ident + 2, `words="${ commandsAndOptionsToString(cmd) }"`) + addLine(ident + 1, ';;') + addLine(ident + 1, '*)') + addLine(ident + 1, ';;') + addLine(ident, 'esac');
} else {
let data = addLine(ident, `case "\${COMP_WORDS[$COMP_CWORD - ${ maxDepth - current + 1 }]}" in`) + addLine(ident + 1, `${ cmd.name() })`);
cmd.commands.filter(c => !isHidden(c)).forEach(subCmd => {
data += generateBashSubCommands(subCmd, current + 1, maxDepth, ident + 2);
});
data += addLine(ident + 1, ';;') + addLine(ident + 1, '*)') + addLine(ident + 1, ';;') + addLine(ident, 'esac');
return data;
}
}
export function generateBashCompletion(root: Command): string {
const depth = computeDepth(root);
let data = addLine(0, '#/usr/bin/env bash\nfunction _dojo_completions()') + addLine(0, '{') + addLine(1, 'latest="${COMP_WORDS[$COMP_CWORD]}"');
for ( let i = 1 ; i <= depth ; i++ ) {
data += addLine(1, `${ i === 1 ? 'if' : 'elif' } [ $COMP_CWORD -eq ${ depth - i + 1 } ]`) + addLine(1, 'then');
data += generateBashSubCommands(root, i, depth, 2);
}
data += addLine(1, 'fi') + addLine(1, 'COMPREPLY=($(compgen -W "$words" -- $latest))') + addLine(1, 'return 0') + addLine(0, '}') + addLine(0, 'complete -F _dojo_completions dojo');
return data;
}
const prefix = 'complete -f -c dojo -n \'__fish_dojo_using_commands';
function generateCommandChain(cmd: Command | null): string {
let data = '';
while ( cmd !== null ) {
data = cmd.name().concat(` ${ data }`);
cmd = cmd.parent;
}
return data.trimEnd();
}
function hasOptions(cmd: Command): boolean {
return cmd.options.length > 0;
}
function optionsToString(cmd: Command): string {
return cmd.options.filter(opt => !opt.hidden).map(opt => `${ prefix } ${ computeHeight(cmd) } ${ generateCommandChain(cmd) }' -a "${ opt.short ?? '' } ${ opt.long ?? '' }" -d "${ opt.description }"`).join('\n').concat('\n');
}
export function generateFishCompletion(root: Command): string {
const commands = flatten(root);
return fishFunction.concat(// add completions for options
commands.filter(c => !isHidden(c)).filter(cmd => hasOptions(cmd)).map(cmd => optionsToString(cmd)).filter(str => str !== '').join('')).concat(// add completions for commands and subcommands
commands.filter(c => !isHidden(c)).filter(cmd => !isLeaf(cmd)).map(cmd => cmd.commands.filter(c => !isHidden(c)).map(subCmd => `${ prefix } ${ computeHeight(cmd) } ${ generateCommandChain(cmd) }' -a ${ subCmd.name() } -d "${ subCmd.description() }"`).join('\n').concat('\n')).join(''));
}
export function updateRcFile(shellType: 'bash' | 'zsh', filePath: string, completionCommand: string) {
const spinner: ora.Ora = ora(`Modifying ${ filePath } ...`).start();
if ( fs.existsSync(filePath) ) {
const data = fs.readFileSync(filePath);
let updated = false;
try {
if ( !data.includes(completionCommand) ) {
fs.appendFileSync(filePath, completionCommand);
updated = true;
}
} catch {
spinner.fail(`Error appending in ${ filePath }`);
return;
}
spinner.succeed(updated ? `${ shellType } updated. Please restart your shell session.` : `${ shellType } already up to date.`);
} else {
try {
fs.writeFileSync(filePath, completionCommand);
spinner.succeed(`${ shellType } written. Please restart your shell session.`);
} catch ( error ) {
spinner.fail(`Error writing in ${ filePath }`);
}
}
}
// The following code should create a bash completion automatically from the Commander
// CLI library. The file should look something like that (it looks at the time
// this comment is written).
// #/usr/bin/env bash
// function _dojo_completions()
// {
// latest="${COMP_WORDS[$COMP_CWORD]}"
// if [ $COMP_CWORD -eq 3 ]
// then
// case "${COMP_WORDS[$COMP_CWORD - 3]}" in
// dojo)
// case "${COMP_WORDS[$COMP_CWORD - 2]}" in
// session)
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// login)
// words="-c --cli --help -h"
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// logout)
// words="-f --force --help -h"
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// test)
// words="--help -h"
// ;;
// *)
// ;;
// esac
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 2]}" in
// assignment)
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// create)
// words="-n --name -i --members_id -u --members_username -t --template -c --clone --help -h"
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// check)
// words="-p --path -v --verbose -w --super-verbose --help -h"
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// run)
// words="-p --path -v --verbose -w --super-verbose --help -h"
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// publish)
// words="-f --force --help -h"
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// unpublish)
// words="-f --force --help -h"
// ;;
// *)
// ;;
// esac
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 2]}" in
// exercise)
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// create)
// words="-a --assignment -i --members_id -u --members_username -c --clone --help -h"
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// run)
// words="-p --path -v --verbose -w --super-verbose --help -h"
// ;;
// *)
// ;;
// esac
// ;;
// *)
// ;;
// esac
// ;;
// *)
// ;;
// esac
// elif [ $COMP_CWORD -eq 2 ]
// then
// case "${COMP_WORDS[$COMP_CWORD - 2]}" in
// dojo)
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// session)
// words="login logout test --help -h"
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// assignment)
// words="create check run publish unpublish --help -h"
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// exercise)
// words="create run --help -h"
// ;;
// *)
// ;;
// esac
// ;;
// *)
// ;;
// esac
// elif [ $COMP_CWORD -eq 1 ]
// then
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// dojo)
// words="session assignment exercise -V --version -H --host --help -h"
// ;;
// *)
// ;;
// esac
// fi
// COMPREPLY=($(compgen -W "$words" -- $latest))
// return 0
// }
// complete -F _dojo_completions dojo
import Exercise from '../../sharedByClients/models/Exercise';
import ora from 'ora';
import TextStyle from '../../types/TextStyle';
import inquirer from 'inquirer';
import Config from '../../config/Config';
import DojoBackendManager from '../../managers/DojoBackendManager';
import SharedConfig from '../../shared/config/SharedConfig';
class ExerciseHelper {
/**
* Clone the exercise repository
* @param exercise
* @param providedPath If a string is provided, the repository will be cloned in the specified directory. If true, the repository will be cloned in the current directory. If false, the repository will not be cloned, and if undefined, the user will be prompted for the path.
*/
async clone(exercise: Exercise, providedPath: string | boolean | undefined) {
if ( providedPath === false ) {
return;
}
let path: string | boolean = './';
if ( providedPath === undefined ) {
path = (await inquirer.prompt([ {
type : 'input',
name : 'path',
message: `Please enter the path (blank, '.' or './' for current directory):`
} ])).path;
} else {
path = providedPath;
}
console.log(TextStyle.BLOCK('Please wait while we are cloning the repository...'));
await Config.gitlabManager.cloneRepository(path === '' ? true : path, exercise.gitlabCreationInfo.ssh_url_to_repo, `DojoExercise_${ exercise.assignmentName }`, true, 0);
}
async delete(exerciseIdOrUrl: string) {
console.log(TextStyle.BLOCK('Please wait while we are deleting the exercise...'));
await DojoBackendManager.deleteExercise(exerciseIdOrUrl);
}
async actionMenu(exercise: Exercise): Promise<void> {
// eslint-disable-next-line no-constant-condition
while ( true ) {
const action: string = (await inquirer.prompt([ {
type : 'list',
name : 'action',
message: 'What action do you want to do on the exercise ?',
choices: [ {
name : 'Display details of the exercise',
value: 'info'
}, new inquirer.Separator(), {
name : 'Clone (SSH required) in current directory (will create a subdirectory)',
value: 'cloneInCurrentDirectory'
}, {
name : 'Clone (SSH required) in the specified directory (will create a subdirectory)',
value: 'clone'
}, new inquirer.Separator(), {
name : 'Delete the exercise',
value: 'delete'
}, new inquirer.Separator(), {
name : 'Exit',
value: 'exit'
}, new inquirer.Separator() ]
} ])).action;
switch ( action ) {
case 'info':
await this.displayDetails(exercise, false);
break;
case 'cloneInCurrentDirectory':
await this.clone(exercise, true);
break;
case 'clone':
await this.clone(exercise, undefined);
break;
case 'delete':
await this.delete(exercise.id);
return;
case 'exit':
return;
default:
ora().fail('Invalid option.');
return;
}
}
}
async displayDetails(exercise: Exercise, showActionMenu: boolean = false): Promise<void> {
ora().info(`Details of the exercise:`);
const oraInfo = (message: string, indent: number = 4) => {
ora({
text : message,
indent: indent
}).start().info();
};
oraInfo(`${ TextStyle.LIST_ITEM_NAME('Id:') } ${ exercise.id }`);
oraInfo(`${ TextStyle.LIST_ITEM_NAME('Name:') } ${ exercise.name }`);
oraInfo(`${ TextStyle.LIST_ITEM_NAME('Assignment:') } ${ exercise.assignmentName }`);
// Display exercise teachers
if ( exercise.assignment?.staff && exercise.assignment?.staff.length > 0 ) {
oraInfo(`${ TextStyle.LIST_ITEM_NAME('Teachers:') }`);
exercise.assignment?.staff.forEach(staff => {
console.log(` - ${ staff.gitlabUsername }`);
});
} else {
ora({
text : `${ TextStyle.LIST_ITEM_NAME('Teachers:') } No teachers found for this exercise.`,
indent: 4
}).start().warn();
}
// Display exercise members
if ( exercise.members && exercise.members.length > 0 ) {
oraInfo(`${ TextStyle.LIST_ITEM_NAME('Members:') }`);
exercise.members.forEach(member => {
console.log(` - ${ member.gitlabUsername }`);
});
} else {
ora({
text : `${ TextStyle.LIST_ITEM_NAME('Members:') } No members found for this exercise.`,
indent: 4
}).start().warn();
}
if ( exercise.assignment?.useSonar ) {
oraInfo(`${ TextStyle.LIST_ITEM_NAME('Sonar project:') } ${ SharedConfig.sonar.url }/dashboard?id=${ exercise.sonarKey }`);
}
oraInfo(`${ TextStyle.LIST_ITEM_NAME('Gitlab URL:') } ${ exercise.gitlabCreationInfo.web_url }`);
oraInfo(`${ TextStyle.LIST_ITEM_NAME('HTTP Repo:') } ${ exercise.gitlabCreationInfo.http_url_to_repo }`);
oraInfo(`${ TextStyle.LIST_ITEM_NAME('SSH Repo:') } ${ exercise.gitlabCreationInfo.ssh_url_to_repo }`);
if ( showActionMenu ) {
await this.actionMenu(exercise);
}
}
}
export default new ExerciseHelper();
\ No newline at end of file
import { Command, Option } from 'commander';
import SessionManager from '../managers/SessionManager.js';
import TextStyle from '../types/TextStyle.js';
import ora from 'ora';
import DojoBackendManager from '../managers/DojoBackendManager.js';
import Assignment from '../sharedByClients/models/Assignment.js';
import Config from '../config/Config';
class GlobalHelper {
public runCommandDefinition(command: Command, isAssignment: boolean = true): Command {
command
.option('-p, --path <value>', `${ isAssignment ? 'assignment' : 'exercise' } path`, Config.folders.defaultLocalExercise)
.option('-v, --verbose', 'verbose mode - display principal container output in live')
.addOption(new Option('-w, --super-verbose', 'verbose mode - display all docker compose logs (build included) in live').conflicts('verbose'))
.addOption(new Option('--verbose-ssj2').hideHelp().implies({ superVerbose: true }));
return command;
}
public readonly refreshGitlabTokenFunction = async () => {
await SessionManager.refreshTokens();
return SessionManager.gitlabCredentials.accessToken ?? '';
};
public async checkAssignmentCorrectionAccess(assignmentName: string): Promise<Assignment | undefined> {
console.log(TextStyle.BLOCK('Please wait while we check access...'));
const assignmentGetSpinner: ora.Ora = ora('Checking if assignment exists').start();
const assignment = await DojoBackendManager.getAssignment(assignmentName);
if ( !assignment ) {
assignmentGetSpinner.fail(`The assignment doesn't exists`);
return undefined;
}
assignmentGetSpinner.succeed(`The assignment exists`);
const assignmentAccessSpinner: ora.Ora = ora('Checking assignment access').start();
if ( assignment.staff.find(staff => staff.id === SessionManager.profile?.id) === undefined ) {
assignmentAccessSpinner.fail(`You are not in the staff of the assignment`);
return undefined;
}
assignmentAccessSpinner.succeed(`You are in the staff of the assignment`);
return assignment;
}
}
export default new GlobalHelper();
\ No newline at end of file
import path from 'node:path';
import dotenv from 'dotenv';
import dotenvExpand from 'dotenv-expand';
import './shared/helpers/TypeScriptExtensions.js';
dotenvExpand.expand(dotenv.config({
path : path.join(__dirname, '../.env'),
DOTENV_KEY: 'dotenv://:key_fc323d8e0a02349342f1c6a119bb38495958ce3a43a87d19a3f674b7e2896dcb@dotenv.local/vault/.env.vault?environment=development'
}));
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Subproject commit 9e3f29d2f313ef96944a199da0db39f1827c496a Subproject commit 937081e68f6127b669daca30e57c43e73b9c96c9
Subproject commit 4efff1c5127c6f84104016d7041d0cf281d981f8 Subproject commit 59308a719fdee1a2025e90a18774d56fc6d11d9b
import chalk from 'chalk';
class TextStyle {
public readonly BLOCK = chalk.cyan;
public readonly CODE = chalk.bgHex('F7F7F7').grey.italic;
public readonly LIST_ITEM_NAME = chalk.magenta;
public readonly LIST_SUBITEM_NAME = chalk.green;
public readonly QUESTION = chalk.greenBright;
public readonly TIPS = chalk.blueBright;
public readonly URL = chalk.blue.underline;
public readonly WARNING = chalk.red;
}
export default new TextStyle();
\ No newline at end of file
{ {
"compilerOptions": { "compilerOptions": {
"rootDir" : "src", "rootDir" : "src",
"outDir" : "dist", "outDir" : "dist",
"strict" : true, "strict" : true,
"target" : "ES2022", "target" : "ES2022",
"module" : "commonjs", "module" : "commonjs",
"sourceMap" : true, "sourceMap" : true,
"esModuleInterop" : true, "noImplicitAny" : true,
"moduleResolution": "node", "esModuleInterop": true,
"noImplicitAny" : true "lib" : [
"ES2022",
"DOM"
],
"types" : [
"node"
]
}, },
"exclude" : [ "exclude" : [
"node_modules" "node_modules"
],
"include" : [
"src/**/*.ts",
"eslint.config.mjs"
] ]
} }
\ No newline at end of file
# DojoCLI # Documentation of `The Dojo CLI` utility
More informations about the DojoCLI can be found in the [wiki](https://gitedu.hesge.ch/dojo_project/projects/ui/dojocli/-/wikis/home). All documentations are available on the [Dojo documentation website](https://www.hepiapp.ch/) : https://www.hepiapp.ch/.
\ No newline at end of file \ No newline at end of file
# Dojo: a platform to practice programming
The dojo platform is an online tool built to help practice programming by
allowing users to propose assignments and perform them as exercises.
The tool is very flexible and allows for proposing exercises for any language
and does not impose any limitation on a framework to be heavily relying
on Docker and Gitlab. These tools used in combination allow for automatic
correction of assignments in order to give immediate feedback to users
performing exercises. Solved exercises can then be shared among the community
of users such that they can inspire other users or give hints on ways to solve
a given exercise.
The two major concepts of the platform are the **assignments** and the **exercises**.
The principal way to interact with the Dojo platform is currently the `dojo` CLI.
## The assignment
An assignment is written by a user that wants to propose an exercise. It is typically composed of a written description of the work to be performed,
and tests that must be passed once the exercise is successfully performed (and some configuration files for the infrastructure of the tests
such as docker files). At its core, an assignment is
nothing else than a git repository that can be forked in the form of an exercise and modified using standard git commands.
For a more detailed description please see the [CLI documentation](home).
An assignment can be proposed by any user.
In the future a dependency tree of assignments can be created, as well as tagging for filtering purposes.
## The exercise
An exercise is an instance of an assignment which the learner will modify in order to make it pass the automatic tests.
It can be run locally on any user's machine using the dojo CLI. When the exercise is completed
it is pushed on the dojo where the CI/CD tools of Gitlab can evaluate it automatically and
notify the dojo platform of the result. The exercises can then be shared with other users
in order to propose a wide variety of solutions and can be a base for discussion among users
and with teachers.
For a more detailed description please see the [CLI documentation](home).
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
# Installation of the Dojo CLI
1. Download the latest stable version (without "-dev" suffix) from the releases: <https://gitedu.hesge.ch/dojo_project/projects/ui/dojocli/-/releases>
![releases](../figures/releases.png)
1. Download the executable corresponding to your OS and architecture.
- ℹ️ For these OS you can use specific packaged release that install the binary at the right place (so you can ignore the next point).
- **Debian / Ubuntu** : You can use the `deb` package named `Debian / Ubuntu (YOUR_ARCH) package`.
- **macOS**: You can use the `pkg` version named `macOS (YOUR_ARCH) package`.
2. Put it in your path. For
- For exemple:
- ℹ️ **Linux**: `$HOME/.local/bin`
- ℹ️ **macOS**: `/usr/local/bin`
3. Verify your installation is working correctly by calling the `dojo` CLI.
```bash
dojo
```
```console
Usage: dojo [options] [command]
CLI of the Dojo application
Options:
-h, --help display help for command
-H, --host <string> override the Dojo API endpoint (default: "https://rdps.hesge.ch/dojo/api")
-V, --version output the version number
Commands:
assignment manage an assignment
exercise manage an exercise
help [command] display help for command
session manage Dojo and Gitlab sessions
```
As you can see calling the `dojo` command shows the help menu.
\ No newline at end of file