diff --git a/.gitignore b/.gitignore
index bbcf40c9751719662c0912bd3f5b52dcc6e3e6ac..b30f2e1d0b10ca45c92e87f88f9766903d8ff732 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,9 @@ Wiki/.idea
 
 NodeApp/src/config/Version.ts
 
+dojo_bash_completion.sh
+dojo.fish
+
 ############################ MacOS
 # General
 .DS_Store
diff --git a/NodeApp/src/commander/CommanderApp.ts b/NodeApp/src/commander/CommanderApp.ts
index 9da832294078c3c474435505bc835414f2828ed4..b944b9e4f902307c3d8d3d469fa8ba7cb9edecd6 100644
--- a/NodeApp/src/commander/CommanderApp.ts
+++ b/NodeApp/src/commander/CommanderApp.ts
@@ -1,5 +1,4 @@
 import { Command, Option } from 'commander';
-import AuthCommand         from './auth/AuthCommand';
 import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig';
 import AssignmentCommand   from './assignment/AssignmentCommand';
 import ExerciseCommand     from './exercise/ExerciseCommand';
@@ -9,6 +8,8 @@ import { stateConfigFile } from '../config/ConfigFiles';
 import semver              from 'semver/preload';
 import { version }         from '../config/Version';
 import Config              from '../config/Config';
+import CompletionCommand   from './completion/CompletionCommand';
+import AuthCommand         from './auth/AuthCommand';
 import SessionCommand      from './auth/SessionCommand';
 
 
@@ -95,6 +96,7 @@ https://gitedu.hesge.ch/dojo_project/projects/ui/dojocli/-/releases/Latest`, {
         SessionCommand.registerOnCommand(this.program);
         AssignmentCommand.registerOnCommand(this.program);
         ExerciseCommand.registerOnCommand(this.program);
+        CompletionCommand.registerOnCommand(this.program);
     }
 }
 
diff --git a/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts b/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts
index 3429b7518bb8694b892f2037f1ed138b163b28e9..7844d44021863c0cf3a8c67e1404635778d08ae9 100644
--- a/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts
+++ b/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts
@@ -1,5 +1,4 @@
 import CommanderCommand   from '../../CommanderCommand';
-import chalk              from 'chalk';
 import ora                from 'ora';
 import AccessesHelper     from '../../../helpers/AccessesHelper';
 import Assignment         from '../../../sharedByClients/models/Assignment';
@@ -7,6 +6,7 @@ import GitlabUser         from '../../../shared/types/Gitlab/GitlabUser';
 import GitlabManager      from '../../../managers/GitlabManager';
 import DojoBackendManager from '../../../managers/DojoBackendManager';
 import Toolbox            from '../../../shared/helpers/Toolbox';
+import TextStyle          from '../../../types/TextStyle';
 
 
 class AssignmentCreateCommand extends CommanderCommand {
@@ -30,7 +30,7 @@ class AssignmentCreateCommand extends CommanderCommand {
 
         // Check access and retrieve data
         {
-            console.log(chalk.cyan('Please wait while we verify and retrieve data...'));
+            console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...'));
 
             if ( !await AccessesHelper.checkTeachingStaff() ) {
                 return;
@@ -63,7 +63,7 @@ class AssignmentCreateCommand extends CommanderCommand {
 
         // Create the assignment
         {
-            console.log(chalk.cyan('Please wait while we are creating the assignment (approximately 10 seconds)...'));
+            console.log(TextStyle.BLOCK('Please wait while we are creating the assignment (approximately 10 seconds)...'));
 
             try {
                 assignment = await DojoBackendManager.createAssignment(options.name, members, templateIdOrNamespace);
@@ -75,10 +75,10 @@ class AssignmentCreateCommand extends CommanderCommand {
                         }).start().info();
                 };
 
-                oraInfo(`${ chalk.magenta('Name:') } ${ assignment.name }`);
-                oraInfo(`${ chalk.magenta('Web URL:') } ${ assignment.gitlabCreationInfo.web_url }`);
-                oraInfo(`${ chalk.magenta('HTTP Repo:') } ${ assignment.gitlabCreationInfo.http_url_to_repo }`);
-                oraInfo(`${ chalk.magenta('SSH Repo:') } ${ assignment.gitlabCreationInfo.ssh_url_to_repo }`);
+                oraInfo(`${ TextStyle.LIST_ITEM_NAME('Name:') } ${ assignment.name }`);
+                oraInfo(`${ TextStyle.LIST_ITEM_NAME('Web URL:') } ${ assignment.gitlabCreationInfo.web_url }`);
+                oraInfo(`${ TextStyle.LIST_ITEM_NAME('HTTP Repo:') } ${ assignment.gitlabCreationInfo.http_url_to_repo }`);
+                oraInfo(`${ TextStyle.LIST_ITEM_NAME('SSH Repo:') } ${ assignment.gitlabCreationInfo.ssh_url_to_repo }`);
             } catch ( error ) {
                 return;
             }
@@ -87,7 +87,7 @@ class AssignmentCreateCommand extends CommanderCommand {
         // Clone the repository
         {
             if ( options.clone ) {
-                console.log(chalk.cyan('Please wait while we are cloning the repository...'));
+                console.log(TextStyle.BLOCK('Please wait while we are cloning the repository...'));
 
                 await GitlabManager.cloneRepository(options.clone, assignment.gitlabCreationInfo.ssh_url_to_repo, undefined, true, 0);
             }
diff --git a/NodeApp/src/commander/assignment/subcommands/AssignmentPublishUnpublishCommandBase.ts b/NodeApp/src/commander/assignment/subcommands/AssignmentPublishUnpublishCommandBase.ts
index 3c7c1d86e5c365d9b12e23d28487f8f5a3edcf67..c5745c4ed928e5b98ad84013581cbff510379da9 100644
--- a/NodeApp/src/commander/assignment/subcommands/AssignmentPublishUnpublishCommandBase.ts
+++ b/NodeApp/src/commander/assignment/subcommands/AssignmentPublishUnpublishCommandBase.ts
@@ -1,11 +1,11 @@
 import CommanderCommand       from '../../CommanderCommand';
 import inquirer               from 'inquirer';
-import chalk                  from 'chalk';
 import SessionManager         from '../../../managers/SessionManager';
 import ora                    from 'ora';
 import DojoBackendManager     from '../../../managers/DojoBackendManager';
 import Assignment             from '../../../sharedByClients/models/Assignment';
 import SharedAssignmentHelper from '../../../shared/helpers/Dojo/SharedAssignmentHelper';
+import TextStyle              from '../../../types/TextStyle';
 
 
 abstract class AssignmentPublishUnpublishCommandBase extends CommanderCommand {
@@ -35,7 +35,7 @@ abstract class AssignmentPublishUnpublishCommandBase extends CommanderCommand {
         let assignment!: Assignment | undefined;
 
         {
-            console.log(chalk.cyan('Please wait while we verify and retrieve data...'));
+            console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...'));
 
             if ( !await SessionManager.testSession(true, null) ) {
                 return;
@@ -82,7 +82,7 @@ abstract class AssignmentPublishUnpublishCommandBase extends CommanderCommand {
         }
 
         {
-            console.log(chalk.cyan(`Please wait while we ${ this.publish ? 'publish' : 'unpublish' } the assignment...`));
+            console.log(TextStyle.BLOCK(`Please wait while we ${ this.publish ? 'publish' : 'unpublish' } the assignment...`));
 
             try {
                 await DojoBackendManager.changeAssignmentPublishedStatus(assignment, this.publish);
diff --git a/NodeApp/src/commander/assignment/subcommands/correction/subcommands/AssignmentCorrectionLinkUpdateCommand.ts b/NodeApp/src/commander/assignment/subcommands/correction/subcommands/AssignmentCorrectionLinkUpdateCommand.ts
index cd7b4f8897678e3e43fe2bd6973a76f0ac61db94..6e5c6d3fd911e427dfa89eddb05d2f4278117202 100644
--- a/NodeApp/src/commander/assignment/subcommands/correction/subcommands/AssignmentCorrectionLinkUpdateCommand.ts
+++ b/NodeApp/src/commander/assignment/subcommands/correction/subcommands/AssignmentCorrectionLinkUpdateCommand.ts
@@ -1,9 +1,9 @@
 import CommanderCommand   from '../../../../CommanderCommand';
-import chalk              from 'chalk';
 import ora                from 'ora';
 import DojoBackendManager from '../../../../../managers/DojoBackendManager';
 import SessionManager     from '../../../../../managers/SessionManager';
 import Assignment         from '../../../../../sharedByClients/models/Assignment';
+import TextStyle          from '../../../../../types/TextStyle';
 
 
 abstract class AssignmentCorrectionLinkUpdateCommand extends CommanderCommand {
@@ -22,7 +22,7 @@ abstract class AssignmentCorrectionLinkUpdateCommand extends CommanderCommand {
 
         // Check access
         {
-            console.log(chalk.cyan('Please wait while we check access...'));
+            console.log(TextStyle.BLOCK('Please wait while we check access...'));
 
             const assignmentGetSpinner: ora.Ora = ora('Checking if assignment exists').start();
             assignment = await DojoBackendManager.getAssignment(options.assignment);
@@ -51,7 +51,7 @@ abstract class AssignmentCorrectionLinkUpdateCommand extends CommanderCommand {
 
         // Link the exercise
         {
-            console.log(chalk.cyan('Please wait while we link the exercise...'));
+            console.log(TextStyle.BLOCK('Please wait while we link the exercise...'));
 
             await DojoBackendManager.linkUpdateCorrection(exerciseIdOrUrl, assignment, this.isUpdate);
         }
diff --git a/NodeApp/src/commander/auth/subcommands/AuthLoginCommand.ts b/NodeApp/src/commander/auth/subcommands/AuthLoginCommand.ts
index 3bc4586c766e63e8af321047cff7215b2bf692e2..615fc20ecbbb38a3f3fce7918d5af9e7a77a61ff 100644
--- a/NodeApp/src/commander/auth/subcommands/AuthLoginCommand.ts
+++ b/NodeApp/src/commander/auth/subcommands/AuthLoginCommand.ts
@@ -1,6 +1,6 @@
-import chalk            from 'chalk';
 import CommanderCommand from '../../CommanderCommand';
 import SessionManager   from '../../../managers/SessionManager';
+import TextStyle        from '../../../types/TextStyle';
 
 
 class AuthLoginCommand extends CommanderCommand {
@@ -15,7 +15,7 @@ class AuthLoginCommand extends CommanderCommand {
 
     protected async commandAction(options: { cli: boolean }): Promise<void> {
         try {
-            console.log(chalk.cyan('Please wait while we login you into Dojo...'));
+            console.log(TextStyle.BLOCK('Please wait while we login you into Dojo...'));
             await SessionManager.login(options.cli);
         } catch ( error ) { /* empty */ }
     }
diff --git a/NodeApp/src/commander/completion/CompletionCommand.ts b/NodeApp/src/commander/completion/CompletionCommand.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c24a7d6f4876f8a20812f6a07f8b5d8c82e9eacb
--- /dev/null
+++ b/NodeApp/src/commander/completion/CompletionCommand.ts
@@ -0,0 +1,25 @@
+import CommanderCommand      from '../CommanderCommand';
+import CompletionBashCommand from './subcommands/CompletionBashCommand';
+import CompletionFishCommand from './subcommands/CompletionFishCommand';
+import CompletionZshCommand  from './subcommands/CompletionZshCommand';
+
+
+class CompletionCommand extends CommanderCommand {
+    protected commandName: string = 'completion';
+
+    protected defineCommand() {
+        this.command
+        .description('generate completions for bash, fish, or zsh');
+    }
+
+    protected defineSubCommands() {
+        CompletionBashCommand.registerOnCommand(this.command);
+        CompletionFishCommand.registerOnCommand(this.command);
+        CompletionZshCommand.registerOnCommand(this.command);
+    }
+
+    protected async commandAction(): Promise<void> { }
+}
+
+
+export default new CompletionCommand();
\ No newline at end of file
diff --git a/NodeApp/src/commander/completion/subcommands/CompletionBashCommand.ts b/NodeApp/src/commander/completion/subcommands/CompletionBashCommand.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a10dc0eb4ee8a805154893bbf7f762edf71279e5
--- /dev/null
+++ b/NodeApp/src/commander/completion/subcommands/CompletionBashCommand.ts
@@ -0,0 +1,76 @@
+import CommanderCommand                                   from '../../CommanderCommand';
+import { generateBashCompletion, getRoot, tryRenameFile } from '../../../helpers/AutoCompletionHelper';
+import ora                                                from 'ora';
+import TextStyle                                          from '../../../types/TextStyle';
+import fs                                                 from 'fs-extra';
+import path                                               from 'path';
+import os                                                 from 'os';
+
+
+class CompletionBashCommand extends CommanderCommand {
+    protected commandName: string = 'bash';
+
+    private installPath = path.join(os.homedir(), '.bash_completion');
+
+    protected defineCommand() {
+        this.command
+        .description('generate bash completion')
+        .option('-f, --file <filename>')
+        .option('-y, --force', 'don\'t ask for file overwrite confirmation')
+        .action(this.commandAction.bind(this));
+    }
+
+    private writeFile(filename: string, showInstructions: boolean) {
+        const spinner: ora.Ora = ora(`Writing Bash completion in ${ TextStyle.CODE(filename) } ...`).start();
+
+        try {
+            fs.mkdirsSync(path.dirname(filename));
+
+            fs.writeFileSync(filename, generateBashCompletion(getRoot(this.command)));
+
+            spinner.succeed(`Bash completion successfully written in ${ TextStyle.CODE(filename) }`);
+            if ( showInstructions ) {
+                console.log(`
+The easiest way to install the completion is to append the content of the generated file to the end of the ${ TextStyle.CODE('~/.bash_completion') } file or to overwrite it, if it only contains the 'dojo' completion.
+
+This can be performed by either
+${ TextStyle.CODE(`
+cat ${ filename } > ~/.bash_completion  # overwrites .bash_completion
+cat ${ filename } >> ~/.bash_completion # appends to .bash_completion
+`) }
+For more details: ${ TextStyle.URL('https://github.com/scop/bash-completion/blob/master/README.md') }
+`);
+            }
+        } catch ( error ) {
+            spinner.fail(`Bash completion error: ${ error }`);
+        }
+    }
+
+    /* The completion command must do the following:
+       - if a file is provided: 
+            - if force is not enabled:
+                - check if the file exists:
+                    - if it exists, prompt the user that it will be erased
+                        - if ok is given write the file and prompt that a backup has been created
+            - else create the file containing the completion
+        - else
+            - if force is not enabled:
+                - check if the default file exists:
+                    - if it exists, prompt the user that it will be erased:
+                        - if ok is given write the file and prompt that a backup has been created
+            - else
+                - create the file containing the completion
+    */
+    protected async commandAction(options: { file: string, force: boolean }): Promise<void> {
+        const filePath = path.resolve(options.file ?? this.installPath); // change that if file is empty
+        const showInstructions = !!options.file;
+        if ( !(await tryRenameFile(filePath, options.force)) ) { // means renaming was interrupted
+            return;
+        }
+        this.writeFile(filePath, showInstructions);
+    }
+
+}
+
+
+export default new CompletionBashCommand();
\ No newline at end of file
diff --git a/NodeApp/src/commander/completion/subcommands/CompletionFishCommand.ts b/NodeApp/src/commander/completion/subcommands/CompletionFishCommand.ts
new file mode 100644
index 0000000000000000000000000000000000000000..51431a58022023c1566f5092996c0c1cbf2ef219
--- /dev/null
+++ b/NodeApp/src/commander/completion/subcommands/CompletionFishCommand.ts
@@ -0,0 +1,70 @@
+import { generateFishCompletion, getRoot, tryRenameFile } from '../../../helpers/AutoCompletionHelper';
+import CommanderCommand                                   from '../../CommanderCommand';
+import ora                                                from 'ora';
+import TextStyle                                          from '../../../types/TextStyle';
+import path                                               from 'path';
+import os                                                 from 'os';
+import fs                                                 from 'fs-extra';
+
+
+class CompletionFishCommand extends CommanderCommand {
+    protected commandName: string = 'fish';
+
+    private installPath = path.join(os.homedir(), '.config/fish/completions/dojo.fish');
+
+    protected defineCommand() {
+        this.command
+        .description('generate fish completion')
+        .option('-f, --file <filename>', `filename where the bash completion will be stored`)
+        .option('-y, --force', 'don\'t ask for file overwrite confirmation')
+        .action(this.commandAction.bind(this));
+    }
+
+    private writeFile(filename: string, showInstructions: boolean) {
+        const spinner: ora.Ora = ora(`Writing fish completion in ${ filename }...`).start();
+        try {
+            fs.mkdirsSync(path.dirname(filename));
+
+            fs.writeFileSync(filename, generateFishCompletion(getRoot(this.command)));
+
+            spinner.succeed(`Fish completion successfully written in ${ filename }.`);
+            if ( showInstructions ) {
+
+                console.log(`
+The easiest way to install the completion is to copy the ${ TextStyle.CODE(filename) } into the ${ TextStyle.CODE('~/.config/fish/completions') } directory.
+
+${ TextStyle.CODE(` cp -i ${ filename } ~/.config/fish/completions  # interactive cp to avoid accidents `) }`);
+            }
+        } catch ( error ) {
+            spinner.fail(`Fish completion error: ${ error }.`);
+        }
+    }
+
+
+    /* The completion command must do the following:
+       - if a file is provided:
+            - if force is not enabled:
+                - check if the file exists:
+                    - if it exists, prompt the user that it will be erased
+                        - if ok is given write the file and prompt that a backup has been created
+            - else create the file containing the completion
+        - else
+            - if force is not enabled:
+                - check if the default file exists:
+                    - if it exists, prompt the user that it will be erased:
+                        - if ok is given write the file and prompt that a backup has been created
+            - else
+                - create the file containing the completion
+    */
+    protected async commandAction(options: { file: string, force: boolean }): Promise<void> {
+        const filePath = path.resolve(options.file ?? this.installPath); // change that if file is empty
+        const showInstructions = !!options.file;
+        if ( !(await tryRenameFile(filePath, options.force)) ) { // means renaming was interrupted
+            return;
+        }
+        this.writeFile(filePath, showInstructions);
+    }
+}
+
+
+export default new CompletionFishCommand();
\ No newline at end of file
diff --git a/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts b/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9e277aa6cb44d77eabf44521beb7786c452f7cc0
--- /dev/null
+++ b/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts
@@ -0,0 +1,135 @@
+import CommanderCommand                                   from '../../CommanderCommand';
+import { generateBashCompletion, getRoot, tryRenameFile } from '../../../helpers/AutoCompletionHelper';
+import ora                                                from 'ora';
+import TextStyle                                          from '../../../types/TextStyle';
+import path                                               from 'path';
+import { homedir }                                        from 'os';
+import fs                                                 from 'fs-extra';
+
+
+class CompletionZshCommand extends CommanderCommand {
+    protected commandName: string = 'zsh';
+
+    private zprofile: string = path.join(homedir(), '.zprofile');
+    private bash_completion = path.join(homedir(), '.bash_completion');
+    private load_bash_completion = `
+# Added by DojoCLI
+autoload -U +X compinit && compinit
+autoload -U +X bashcompinit && bashcompinit
+source ${ this.bash_completion }
+`;
+
+
+    protected defineCommand() {
+        this.command
+        .description('generate zsh completion, which is derived from the bash completion')
+        .option('-f, --file <filename>', 'bash completion filename')
+        .option('-y, --force', 'don\'t ask for file overwrite confirmation')
+        .action(this.commandAction.bind(this));
+    }
+
+    private writeFile(filename: string, showInstructions: boolean) {
+        const spinner: ora.Ora = ora(`Writing Bash completion in ${ TextStyle.CODE(filename) } ...`).start();
+
+        try {
+            fs.mkdirsSync(path.dirname(filename));
+
+            fs.writeFileSync(filename, generateBashCompletion(getRoot(this.command)));
+
+            spinner.succeed(`Bash completion successfully written in ${ TextStyle.CODE(filename) }`);
+            if ( showInstructions ) {
+                console.log(`
+The easiest way to install the completion is to append the content of the generated file to the end of the ${ TextStyle.CODE(filename) } file or to overwrite it, if it only contains the 'dojo' completion.
+
+This can be performed by either ${ TextStyle.CODE(`
+cat ${ filename } > ~/.bash_completion  # overwrites .bash_completion
+cat ${ filename } >> ~/.bash_completion # appends to .bash_completion`) }
+For more details: ${ TextStyle.URL('https://github.com/scop/bash-completion/blob/master/README.md') }
+
+Next add the following lines to your ${ TextStyle.CODE(`~/.zprofile`) } file: ${ TextStyle.CODE(`
+autoload -U +X compinit && compinit
+autoload -U +X bashcompinit && bashcompinit
+source ${ filename }
+`) } `);
+            }
+        } catch ( error ) {
+            spinner.fail(`Bash completion writing error: ${ error }`);
+        }
+    }
+
+    protected addToZprofile(path: string, bash_path: string) {
+        const spinner: ora.Ora = ora(`Modifying ${ path } ...`).start();
+        if ( fs.existsSync(path) ) {
+            const data = fs.readFileSync(path);
+            let updated = false;
+            if ( !data.includes('autoload -U +X compinit && compinit') ) {
+                try {
+                    fs.appendFileSync(path, '\nautoload -U +X compinit && compinit');
+                    updated = true;
+                } catch {
+                    spinner.fail(`Error appending in ${ this.zprofile }`);
+                }
+            }
+            if ( !data.includes('autoload -U +X bashcompinit && bashcompinit') ) {
+                try {
+                    fs.appendFileSync(path, '\nautoload -U +X bashcompinit && bashcompinit');
+                    updated = true;
+                } catch {
+                    spinner.fail(`Error appending in ${ this.zprofile }`);
+                }
+            }
+            if ( !data.includes(`source ${ bash_path }`) ) {
+                try {
+                    fs.appendFileSync(path, `\nsource ${ bash_path }`);
+                    updated = true;
+                } catch {
+                    spinner.fail(`Error appending in ${ this.zprofile }`);
+                }
+            }
+            if ( updated ) {
+                spinner.succeed(`Zsh profile updated.`);
+            } else {
+                spinner.succeed(`Zsh profile already up to date.`);
+            }
+        } else {
+            try {
+                fs.writeFileSync(path, this.load_bash_completion);
+                spinner.succeed(`Zsh profile written.`);
+            } catch ( error ) {
+                spinner.fail(`Error writing in ${ this.zprofile }`);
+            }
+        }
+    }
+
+    /* The completion command must do the following:
+    - if a file is provided: 
+        - if force is not enabled:
+            - check if the bash completion file exists:
+                - if it exists, prompt the user that it will be overwritten
+                    - if ok is given write the file and prompt that a backup has been created
+        - else create the file containing the completion
+    - else
+        - if force is not enabled:
+            - check if the default file exists:
+                - if it exists, prompt the user that it will be erased:
+                    - if ok is given write the file and prompt that a backup has been created
+        - else
+            - create the file containing the completion
+    - create a .zprofile or append the appropriate commands into the .zprofile file
+    */
+    protected async commandAction(options: { file: string, force: boolean }): Promise<void> {
+        const filePath = path.resolve(options.file ?? this.bash_completion); // change that if file is empty
+        const showInstructions = !!options.file;
+        if ( !(await tryRenameFile(filePath, options.force)) ) { // means renaming was interrupted
+            return;
+        }
+        this.writeFile(filePath, showInstructions);
+        // Do not modify if custom file was provided
+        if ( !options.file ) {
+            this.addToZprofile(this.zprofile, filePath);
+        }
+    }
+}
+
+
+export default new CompletionZshCommand();
\ No newline at end of file
diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseCorrectionCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseCorrectionCommand.ts
index 0704ebfa5598e601f2575f80aa7fa8e24c221f02..03641466226d60d598814addff6ad1e1963a505e 100644
--- a/NodeApp/src/commander/exercise/subcommands/ExerciseCorrectionCommand.ts
+++ b/NodeApp/src/commander/exercise/subcommands/ExerciseCorrectionCommand.ts
@@ -6,6 +6,7 @@ import Assignment         from '../../../sharedByClients/models/Assignment';
 import inquirer           from 'inquirer';
 import open               from 'open';
 import chalk              from 'chalk';
+import TextStyle          from '../../../types/TextStyle';
 
 
 type CorrectionResume = { name: string, value: string }
@@ -66,7 +67,7 @@ class ExerciseCorrectionCommand extends CommanderCommand {
                                                                  default: false
                                                              })).correctionUrl;
 
-        console.log(chalk.green(correctionUrl));
+        console.log(TextStyle.URL(correctionUrl));
 
         open(correctionUrl).then();
     }
diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts
index 00215301ae2cac6352adb41a152f64fcceeea91f..8f7673d0049654d3b9b394cbc9cf986ef07442c0 100644
--- a/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts
+++ b/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts
@@ -1,5 +1,4 @@
 import CommanderCommand   from '../../CommanderCommand';
-import chalk              from 'chalk';
 import GitlabManager      from '../../../managers/GitlabManager';
 import GitlabUser         from '../../../shared/types/Gitlab/GitlabUser';
 import ora                from 'ora';
@@ -7,6 +6,7 @@ import DojoBackendManager from '../../../managers/DojoBackendManager';
 import AccessesHelper     from '../../../helpers/AccessesHelper';
 import Assignment         from '../../../sharedByClients/models/Assignment';
 import Exercise           from '../../../sharedByClients/models/Exercise';
+import TextStyle          from '../../../types/TextStyle';
 
 
 class ExerciseCreateCommand extends CommanderCommand {
@@ -29,7 +29,7 @@ class ExerciseCreateCommand extends CommanderCommand {
 
         // Check access and retrieve data
         {
-            console.log(chalk.cyan('Please wait while we verify and retrieve data...'));
+            console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...'));
 
             if ( !await AccessesHelper.checkStudent() ) {
                 return;
@@ -65,7 +65,7 @@ class ExerciseCreateCommand extends CommanderCommand {
 
         //Create the exercise
         {
-            console.log(chalk.cyan('Please wait while we are creating the exercise (approximately 10 seconds)...'));
+            console.log(TextStyle.BLOCK('Please wait while we are creating the exercise (approximately 10 seconds)...'));
 
             try {
                 exercise = await DojoBackendManager.createExercise((assignment as Assignment).name, members);
@@ -77,11 +77,11 @@ class ExerciseCreateCommand extends CommanderCommand {
                         }).start().info();
                 };
 
-                oraInfo(`${ chalk.magenta('Id:') } ${ exercise.id }`);
-                oraInfo(`${ chalk.magenta('Name:') } ${ exercise.name }`);
-                oraInfo(`${ chalk.magenta('Web URL:') } ${ exercise.gitlabCreationInfo.web_url }`);
-                oraInfo(`${ chalk.magenta('HTTP Repo:') } ${ exercise.gitlabCreationInfo.http_url_to_repo }`);
-                oraInfo(`${ chalk.magenta('SSH Repo:') } ${ exercise.gitlabCreationInfo.ssh_url_to_repo }`);
+                oraInfo(`${ TextStyle.LIST_ITEM_NAME('Id:') } ${ exercise.id }`);
+                oraInfo(`${ TextStyle.LIST_ITEM_NAME('Name:') } ${ exercise.name }`);
+                oraInfo(`${ TextStyle.LIST_ITEM_NAME('Web 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 }`);
             } catch ( error ) {
                 return;
             }
@@ -90,7 +90,7 @@ class ExerciseCreateCommand extends CommanderCommand {
         // Clone the repository
         {
             if ( options.clone ) {
-                console.log(chalk.cyan('Please wait while we are cloning the repository...'));
+                console.log(TextStyle.BLOCK('Please wait while we are cloning the repository...'));
 
                 await GitlabManager.cloneRepository(options.clone, exercise.gitlabCreationInfo.ssh_url_to_repo, `DojoExercise - ${ exercise.assignmentName }`, true, 0);
             }
diff --git a/NodeApp/src/helpers/AutoCompletionHelper.ts b/NodeApp/src/helpers/AutoCompletionHelper.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f0c480a82d97af2be721b88ecc7285f9c242abf8
--- /dev/null
+++ b/NodeApp/src/helpers/AutoCompletionHelper.ts
@@ -0,0 +1,334 @@
+import { Command } from 'commander';
+import { existsSync, renameSync } from 'fs';
+import ora from 'ora';
+import TextStyle from '../types/TextStyle';
+import inquirer from 'inquirer';
+
+function renameFile(filename: string, showWarning: boolean) {
+    const old_filename = `${filename}.old`
+    const spinner: ora.Ora = ora(`Renaming ${TextStyle.CODE(filename)} in ${TextStyle.CODE(old_filename)} ...`).start();
+    try {
+        renameSync(filename, old_filename);
+        spinner.succeed(`Renaming success: ${TextStyle.CODE(filename)} in ${TextStyle.CODE(old_filename)}`);
+
+        if (showWarning) {
+            console.log(`${TextStyle.WARNING('Warning:')} Your ${TextStyle.CODE(filename)} was renamed ${TextStyle.CODE(old_filename)}. 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
+}
+
+// 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, cmd) => acc.concat(cmd), [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`;
+}
+
+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 => {
+        return `${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);
+
+    const data = 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 => {
+                return `${prefix} ${computeHeight(cmd)} ${generateCommandChain(cmd)}' -a ${subCmd.name()} -d "${subCmd.description()}"`;
+            }).join('\n').concat('\n')).join(''));
+
+    return data;
+}
+
+
+// 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
diff --git a/NodeApp/src/helpers/Dojo/ExerciseRunHelper.ts b/NodeApp/src/helpers/Dojo/ExerciseRunHelper.ts
index 2db4ba26242ff5fbea45c80b82bbfaff1b27dfa7..07fa2f5b4de4d383c4cfd77765efe02ac8349886 100644
--- a/NodeApp/src/helpers/Dojo/ExerciseRunHelper.ts
+++ b/NodeApp/src/helpers/Dojo/ExerciseRunHelper.ts
@@ -14,6 +14,7 @@ import os                                   from 'os';
 import util                                 from 'util';
 import { exec }                             from 'child_process';
 import SharedConfig                         from '../../shared/config/SharedConfig';
+import TextStyle                            from '../../types/TextStyle';
 
 
 const execAsync = util.promisify(exec);
@@ -50,7 +51,7 @@ class ExerciseRunHelper {
 
         // Step 1: Check requirements (if it's an exercise folder and if Docker daemon is running)
         {
-            console.log(chalk.cyan('Please wait while we are checking and creating dependencies...'));
+            console.log(TextStyle.BLOCK('Please wait while we are checking and creating dependencies...'));
 
             // Create result temp folder
             fs.mkdirSync(this.folderResultsVolume, { recursive: true });
@@ -122,7 +123,7 @@ class ExerciseRunHelper {
 
         // Step 2: Run docker-compose file
         {
-            console.log(chalk.cyan('Please wait while we are running the exercise...'));
+            console.log(TextStyle.BLOCK('Please wait while we are running the exercise...'));
 
             let composeFileOverride: string[] = [];
             const composeOverridePath: string = path.join(localExercisePath, 'docker-compose-override.yml');
@@ -211,7 +212,7 @@ class ExerciseRunHelper {
 
         // Step 3: Get results
         {
-            console.log(chalk.cyan('Please wait while we are checking the results...'));
+            console.log(TextStyle.BLOCK('Please wait while we are checking the results...'));
 
             exerciseResultsValidation = new ExerciseResultsSanitizerAndValidator(this.folderResultsDojo, this.folderResultsExercise, exerciseDockerCompose.exitCode);
 
diff --git a/NodeApp/src/managers/SessionManager.ts b/NodeApp/src/managers/SessionManager.ts
index b5175f8598955b32b9a1d2a4b0404e716e422926..d06fef9e44ec22ec7e89d045f066995b67606fad 100644
--- a/NodeApp/src/managers/SessionManager.ts
+++ b/NodeApp/src/managers/SessionManager.ts
@@ -20,6 +20,7 @@ import GitlabManager             from './GitlabManager';
 import GitlabToken               from '../shared/types/Gitlab/GitlabToken';
 import open                      from 'open';
 import { sessionConfigFile }     from '../config/ConfigFiles';
+import TextStyle                 from '../types/TextStyle';
 
 
 class LoginServer {
@@ -41,7 +42,7 @@ class LoginServer {
                     res.writeHead(HttpStatusCode.Ok, { 'Content-Type': 'text/html' });
                     res.write(`<html lang="en"><body><h1 style="color: green">DojoCLI login successful</h1><h3>You can close this window.</h3></body></html>`);
                     res.end();
-                    
+
                     this.events.emit('code', urlParts[1]);
                     return;
                 }
@@ -124,9 +125,9 @@ class SessionManager {
     private async getGitlabCodeFromHeadlessEnvironment(): Promise<string> {
         const indent: string = '    ';
         console.log(`${ indent }Please open the following URL in your web browser and accept to give the requested permissions to Dojo:`);
-        console.log(chalk.blue(`${ indent }${ Config.login.gitlab.url.code }`));
+        console.log(TextStyle.URL(`${ indent }${ Config.login.gitlab.url.code }`));
         console.log(`${ indent }Then, copy the code at the end of the redirected url and paste it bellow.`);
-        console.log(`${ indent }Example of url (here the code is 123456): ${ chalk.blue(`${ SharedConfig.login.gitlab.url.redirect }?code=`) }${ chalk.green('123456') }`);
+        console.log(`${ indent }Example of url (here the code is 123456): ${ TextStyle.URL(`${ SharedConfig.login.gitlab.url.redirect }?code=`) }${ chalk.green('123456') }`);
         return (await inquirer.prompt({
                                           type   : 'password',
                                           name   : 'code',
diff --git a/NodeApp/src/types/TextStyle.ts b/NodeApp/src/types/TextStyle.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d7fd0031614d80f943a61dcf3a6ab99a5f1bba6f
--- /dev/null
+++ b/NodeApp/src/types/TextStyle.ts
@@ -0,0 +1,13 @@
+import chalk from 'chalk';
+
+
+class TextStyle {
+    public readonly BLOCK = chalk.cyan;
+    public readonly CODE = chalk.bgHex('F7F7F7').grey;
+    public readonly LIST_ITEM_NAME = chalk.magenta;
+    public readonly URL = chalk.blue.underline;
+    public readonly WARNING = chalk.red;
+}
+
+
+export default new TextStyle();
\ No newline at end of file