diff --git a/NodeApp/package-lock.json b/NodeApp/package-lock.json
index 1337801ee848bc94b5964f1db8653f732a687c17..ba7bd845261fe22f359c5c422f92c34a7d1d9420 100644
--- a/NodeApp/package-lock.json
+++ b/NodeApp/package-lock.json
@@ -17,6 +17,7 @@
                 "commander": "^11.1.0",
                 "dotenv": "^16.3.1",
                 "dotenv-expand": "^10.0.0",
+                "form-data": "^4.0.0",
                 "fs-extra": "^11.2.0",
                 "http-status-codes": "^2.3.0",
                 "inquirer": "^8.2.6",
diff --git a/NodeApp/package.json b/NodeApp/package.json
index 06adaf3e94bcefedc33f493eb1999fafbd25d851..87b5a5fcb2eda88e01efea2f3a8e4cdcd0b61c1b 100644
--- a/NodeApp/package.json
+++ b/NodeApp/package.json
@@ -41,6 +41,7 @@
         "commander"           : "^11.1.0",
         "dotenv"              : "^16.3.1",
         "dotenv-expand"       : "^10.0.0",
+        "form-data"           : "^4.0.0",
         "fs-extra"            : "^11.2.0",
         "http-status-codes"   : "^2.3.0",
         "inquirer"            : "^8.2.6",
@@ -51,6 +52,7 @@
         "semver"              : "^7.5.4",
         "tar-stream"          : "^3.1.6",
         "winston"             : "^3.11.0",
+        "winston-transport"   : "^4.7.0",
         "yaml"                : "^2.3.4",
         "zod"                 : "^3.22.4",
         "zod-validation-error": "^3.0.0"
diff --git a/NodeApp/src/app.ts b/NodeApp/src/app.ts
index be01e40ef06b23244940be8b6e228a38344a74d3..2f9948806c16dcbecd7fce1b7b94e9317f4383a0 100644
--- a/NodeApp/src/app.ts
+++ b/NodeApp/src/app.ts
@@ -20,4 +20,4 @@ import HttpManager  from './managers/HttpManager';
 HttpManager.registerAxiosInterceptor();
 
 
-new CommanderApp();
\ No newline at end of file
+(new CommanderApp()).parse();
\ No newline at end of file
diff --git a/NodeApp/src/commander/CommanderApp.ts b/NodeApp/src/commander/CommanderApp.ts
index b944b9e4f902307c3d8d3d469fa8ba7cb9edecd6..1b117859ab168b2d16bab98a778e94ca54b14579 100644
--- a/NodeApp/src/commander/CommanderApp.ts
+++ b/NodeApp/src/commander/CommanderApp.ts
@@ -18,21 +18,21 @@ class CommanderApp {
 
     constructor() {
         this.program
-        .name('dojo')
-        .description('CLI of the Dojo application')
-        .version('{{VERSION}}')
-        .showHelpAfterError()
-        .configureHelp({
-                           showGlobalOptions: true,
-                           sortOptions      : true,
-                           sortSubcommands  : true
-                       })
-        .option('-H, --host <string>', 'override the Dojo API endpoint', ClientsSharedConfig.apiURL)
-        .option('-I, --interactive', 'show interactive interface when available', Config.interactiveMode)
-        .addOption(new Option('--debug').hideHelp())
-        .hook('preAction', () => {
-            this.warnDevelopmentVersion();
-        }).hook('postAction', () => {
+            .name('dojo')
+            .description('CLI of the Dojo application')
+            .version('{{VERSION}}')
+            .showHelpAfterError()
+            .configureHelp({
+                               showGlobalOptions: true,
+                               sortOptions      : true,
+                               sortSubcommands  : true
+                           })
+            .option('-H, --host <string>', 'override the Dojo API endpoint', ClientsSharedConfig.apiURL)
+            .option('-I, --interactive', 'show interactive interface when available', Config.interactiveMode)
+            .addOption(new Option('--debug').hideHelp())
+            .hook('preAction', () => {
+                this.warnDevelopmentVersion();
+            }).hook('postAction', () => {
             this.informNewVersion();
         });
 
@@ -53,6 +53,10 @@ class CommanderApp {
         this.program.parse();
     }
 
+    public parse() {
+        this.program.parse();
+    }
+
     private warnDevelopmentVersion() {
         if ( !SharedConfig.production ) {
             console.log(boxen(`This is a development (unstable) version of the DojoCLI.
@@ -73,20 +77,18 @@ https://gitedu.hesge.ch/dojo_project/projects/ui/dojocli/-/releases/Latest`, {
         if ( SharedConfig.production ) {
             const latestDojoCliVersion = stateConfigFile.getParam('latestDojoCliVersion') as string | null || '0.0.0';
             const latestDojoCliVersionNotification = stateConfigFile.getParam('latestDojoCliVersionNotification') as number | null || 0;
-            if ( semver.lt(version, latestDojoCliVersion) ) {
-                if ( (new Date()).getTime() - latestDojoCliVersionNotification >= Config.versionUpdateInformationPeriodHours * 60 * 60 * 1000 ) {
-                    console.log(boxen(`The ${ latestDojoCliVersion } version of the DojoCLI is available: 
+            if ( semver.lt(version, latestDojoCliVersion) && (new Date()).getTime() - latestDojoCliVersionNotification >= Config.versionUpdateInformationPeriodHours * 60 * 60 * 1000 ) {
+                console.log(boxen(`The ${ latestDojoCliVersion } version of the DojoCLI is available: 
 https://gitedu.hesge.ch/dojo_project/projects/ui/dojocli/-/releases/Latest`, {
-                        title         : 'Information',
-                        titleAlignment: 'center',
-                        borderColor   : 'blue',
-                        borderStyle   : 'bold',
-                        margin        : 1,
-                        padding       : 1,
-                        textAlignment : 'left'
-                    }));
-                    stateConfigFile.setParam('latestDojoCliVersionNotification', (new Date()).getTime());
-                }
+                    title         : 'Information',
+                    titleAlignment: 'center',
+                    borderColor   : 'blue',
+                    borderStyle   : 'bold',
+                    margin        : 1,
+                    padding       : 1,
+                    textAlignment : 'left'
+                }));
+                stateConfigFile.setParam('latestDojoCliVersionNotification', (new Date()).getTime());
             }
         }
     }
diff --git a/NodeApp/src/commander/CommanderCommand.ts b/NodeApp/src/commander/CommanderCommand.ts
index 258606926813aed8e554614cfb5011f767f3accd..4a34ad5c3e5ecc33c26d5fb245ad42e18c911421 100644
--- a/NodeApp/src/commander/CommanderCommand.ts
+++ b/NodeApp/src/commander/CommanderCommand.ts
@@ -18,7 +18,12 @@ abstract class CommanderCommand {
 
     protected abstract defineCommand(): void;
 
-    protected defineSubCommands() {}
+    protected defineSubCommands() {
+        /*
+        * No action
+        * Override this method only if you need to define subcommands
+        * */
+    }
 
     protected abstract commandAction(...args: Array<unknown>): Promise<void>;
 }
diff --git a/NodeApp/src/commander/assignment/AssignmentCommand.ts b/NodeApp/src/commander/assignment/AssignmentCommand.ts
index b54c2cb00fcd0622f818ed9c621b823d9b7699e4..d306aca8c891a2958298d476df4c20bf9ab38e55 100644
--- a/NodeApp/src/commander/assignment/AssignmentCommand.ts
+++ b/NodeApp/src/commander/assignment/AssignmentCommand.ts
@@ -12,7 +12,7 @@ class AssignmentCommand extends CommanderCommand {
 
     protected defineCommand() {
         this.command
-        .description('manage an assignment');
+            .description('manage an assignment');
     }
 
     protected defineSubCommands() {
@@ -24,7 +24,9 @@ class AssignmentCommand extends CommanderCommand {
         AssignmentCorrectionCommand.registerOnCommand(this.command);
     }
 
-    protected async commandAction(): Promise<void> { }
+    protected async commandAction(): Promise<void> {
+        // No action
+    }
 }
 
 
diff --git a/NodeApp/src/commander/assignment/subcommands/AssignmentCheckCommand.ts b/NodeApp/src/commander/assignment/subcommands/AssignmentCheckCommand.ts
index c83a3d16aa5900ecd5e1f2a04648f3bdef1abc1f..0f5a726119df1220275af13dcb706abe7c44a0de 100644
--- a/NodeApp/src/commander/assignment/subcommands/AssignmentCheckCommand.ts
+++ b/NodeApp/src/commander/assignment/subcommands/AssignmentCheckCommand.ts
@@ -10,15 +10,69 @@ import GlobalHelper                  from '../../../helpers/GlobalHelper';
 
 class AssignmentCheckCommand extends CommanderCommand {
     protected commandName: string = 'check';
+    protected currentSpinner: ora.Ora = ora();
+    private verbose: boolean = false;
+    private superVerbose: boolean = false;
+    private buildPhase: boolean | undefined = undefined;
 
     protected defineCommand() {
         GlobalHelper.runCommandDefinition(this.command)
-        .description('locally run a check of an assignment')
-        .action(this.commandAction.bind(this));
+            .description('locally run a check of an assignment')
+            .action(this.commandAction.bind(this));
+    }
+
+    private logsEvent(log: string, _error: boolean, displayable: boolean, _currentStep: string, currentSubStep: string) {
+        for ( const line of log.split('\n') ) {
+            if ( displayable && this.buildPhase === undefined && line.startsWith('#') ) {
+                this.buildPhase = true;
+            }
+
+            if ( currentSubStep === 'COMPOSE_RUN' && this.buildPhase === true && line !== '' && !line.startsWith('#') ) {
+                this.buildPhase = false;
+            }
+
+            if ( SharedConfig.debug || (displayable && (this.superVerbose || this.buildPhase === false)) ) {
+                console.log(line);
+            }
+        }
+    }
+
+    private subStepEvent(name: string, message: string) {
+        this.currentSpinner = ora({
+                                      text  : message,
+                                      indent: 4
+                                  }).start();
+
+        if ( this.verbose && name === 'COMPOSE_RUN' ) {
+            this.currentSpinner.info();
+        }
+    }
+
+    private endSubStepEvent(stepName: string, message: string, error: boolean) {
+        if ( error ) {
+            if ( this.verbose && stepName === 'COMPOSE_RUN' ) {
+                ora({
+                        text  : message,
+                        indent: 4
+                    }).start().fail();
+            } else {
+                this.currentSpinner.fail(message);
+            }
+        } else {
+            if ( this.verbose && stepName === 'COMPOSE_RUN' ) {
+                ora({
+                        text  : message,
+                        indent: 4
+                    }).start().succeed();
+            } else {
+                this.currentSpinner.succeed(message);
+            }
+        }
     }
 
     protected async commandAction(options: { path: string, verbose: boolean, superVerbose: boolean }): Promise<void> {
-        const verbose: boolean = options.verbose || options.superVerbose || SharedConfig.debug;
+        this.superVerbose = options.superVerbose;
+        this.verbose = options.verbose || options.superVerbose || SharedConfig.debug;
 
         const localExercisePath: string = options.path ?? Config.folders.defaultLocalExercise;
 
@@ -26,69 +80,19 @@ class AssignmentCheckCommand extends CommanderCommand {
 
         try {
             await new Promise<void>((resolve, reject) => {
-                let spinner: ora.Ora;
-
-                if ( verbose ) {
-                    let buildPhase: boolean | undefined = undefined;
-                    assignmentValidator.events.on('logs', (log: string, error: boolean, displayable: boolean, _currentStep: string, currentSubStep: string) => {
-                        for ( const line of log.split('\n') ) {
-                            if ( displayable && buildPhase == undefined && line.startsWith('#') ) {
-                                buildPhase = true;
-                            }
-
-                            if ( currentSubStep == 'COMPOSE_RUN' && buildPhase === true && line != '' && !line.startsWith('#') ) {
-                                buildPhase = false;
-                            }
-
-                            if ( SharedConfig.debug || (displayable && (options.superVerbose || buildPhase === false)) ) {
-                                console.log(line);
-                            }
-                        }
-                    });
+                if ( this.verbose ) {
+                    assignmentValidator.events.on('logs', this.logsEvent.bind(this));
                 }
 
-                assignmentValidator.events.on('step', (name: string, message: string) => {
-                    console.log(chalk.cyan(message));
-                });
+                assignmentValidator.events.on('step', (_name: string, message: string) => console.log(chalk.cyan(message)));
 
-                assignmentValidator.events.on('subStep', (name: string, message: string) => {
-                    spinner = ora({
-                                      text  : message,
-                                      indent: 4
-                                  }).start();
+                assignmentValidator.events.on('subStep', this.subStepEvent.bind(this));
+
+                assignmentValidator.events.on('endSubStep', this.endSubStepEvent.bind(this));
+
+                assignmentValidator.events.on('finished', (success: boolean) => success ? resolve() : reject());
 
-                    if ( verbose && name == 'COMPOSE_RUN' ) {
-                        spinner.info();
-                    }
-                });
-
-                assignmentValidator.events.on('endSubStep', (stepName: string, message: string, error: boolean) => {
-                    if ( error ) {
-                        if ( verbose && stepName == 'COMPOSE_RUN' ) {
-                            ora({
-                                    text  : message,
-                                    indent: 4
-                                }).start().fail();
-                        } else {
-                            spinner.fail(message);
-                        }
-                    } else {
-                        if ( verbose && stepName == 'COMPOSE_RUN' ) {
-                            ora({
-                                    text  : message,
-                                    indent: 4
-                                }).start().succeed();
-                        } else {
-                            spinner.succeed(message);
-                        }
-                    }
-                });
-
-                assignmentValidator.events.on('finished', (success: boolean) => {
-                    success ? resolve() : reject();
-                });
-
-                assignmentValidator.run(true);
+                assignmentValidator.run();
             });
         } catch ( error ) { /* empty */ }
 
diff --git a/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts b/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts
index 7844d44021863c0cf3a8c67e1404635778d08ae9..fed344e1177221865e279869473aabd492d28215 100644
--- a/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts
+++ b/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts
@@ -14,13 +14,13 @@ class AssignmentCreateCommand extends CommanderCommand {
 
     protected defineCommand() {
         this.command
-        .description('create a new repository for an assignment')
-        .requiredOption('-n, --name <name>', 'name of the assignment')
-        .option('-i, --members_id <ids...>', 'list of gitlab members ids (teaching staff) to add to the repository')
-        .option('-u, --members_username <usernames...>', 'list of gitlab members username (teaching staff) to add to the repository')
-        .option('-t, --template <string>', 'id or url of the template (http/s and ssh urls are possible)')
-        .option('-c, --clone [string]', 'automatically clone the repository (SSH required) in the specified directory (this will create a subdirectory with the assignment name)')
-        .action(this.commandAction.bind(this));
+            .description('create a new repository for an assignment')
+            .requiredOption('-n, --name <name>', 'name of the assignment')
+            .option('-i, --members_id <ids...>', 'list of gitlab members ids (teaching staff) to add to the repository')
+            .option('-u, --members_username <usernames...>', 'list of gitlab members username (teaching staff) to add to the repository')
+            .option('-t, --template <string>', 'id or url of the template (http/s and ssh urls are possible)')
+            .option('-c, --clone [string]', 'automatically clone the repository (SSH required) in the specified directory (this will create a subdirectory with the assignment name)')
+            .action(this.commandAction.bind(this));
     }
 
     protected async commandAction(options: { name: string, template?: string, members_id?: Array<number>, members_username?: Array<string>, clone?: string | boolean }): Promise<void> {
@@ -52,10 +52,10 @@ class AssignmentCreateCommand extends CommanderCommand {
                 templateIdOrNamespace = options.template;
 
                 if ( Number.isNaN(Number(templateIdOrNamespace)) ) {
-                    templateIdOrNamespace = Toolbox.urlToPath(templateIdOrNamespace as string);
+                    templateIdOrNamespace = Toolbox.urlToPath(templateIdOrNamespace);
                 }
 
-                if ( !await DojoBackendManager.checkTemplateAccess(encodeURIComponent(templateIdOrNamespace as string)) ) {
+                if ( !await DojoBackendManager.checkTemplateAccess(encodeURIComponent(templateIdOrNamespace)) ) {
                     return;
                 }
             }
diff --git a/NodeApp/src/commander/assignment/subcommands/AssignmentRunCommand.ts b/NodeApp/src/commander/assignment/subcommands/AssignmentRunCommand.ts
index 3b44f98a6370b7bd95cd7888897329e8a3021b33..81636fd36ca1ca49c528b234090ada9a9e69c638 100644
--- a/NodeApp/src/commander/assignment/subcommands/AssignmentRunCommand.ts
+++ b/NodeApp/src/commander/assignment/subcommands/AssignmentRunCommand.ts
@@ -8,12 +8,12 @@ class AssignmentRunCommand extends CommanderCommand {
 
     protected defineCommand() {
         GlobalHelper.runCommandDefinition(this.command)
-        .description('locally run the assignment as an exercise')
-        .action(this.commandAction.bind(this));
+            .description('locally run the assignment as an exercise')
+            .action(this.commandAction.bind(this));
     }
 
     protected async commandAction(options: { path: string, verbose: boolean, superVerbose: boolean }): Promise<void> {
-        await ExerciseRunHelper.run(options);
+        await (new ExerciseRunHelper(options)).run();
     }
 }
 
diff --git a/NodeApp/src/commander/assignment/subcommands/correction/AssignmentCorrectionCommand.ts b/NodeApp/src/commander/assignment/subcommands/correction/AssignmentCorrectionCommand.ts
index 3f30a994e7fdcd0a773401db5daa36abba861579..bd7ebce061a2f99b8b1c62d94178e92ce37621b9 100644
--- a/NodeApp/src/commander/assignment/subcommands/correction/AssignmentCorrectionCommand.ts
+++ b/NodeApp/src/commander/assignment/subcommands/correction/AssignmentCorrectionCommand.ts
@@ -8,7 +8,7 @@ class AssignmentCorrectionCommand extends CommanderCommand {
 
     protected defineCommand() {
         this.command
-        .description('manage corrections of an assignment');
+            .description('manage corrections of an assignment');
     }
 
     protected defineSubCommands() {
@@ -16,7 +16,9 @@ class AssignmentCorrectionCommand extends CommanderCommand {
         AssignmentCorrectionUpdateCommand.registerOnCommand(this.command);
     }
 
-    protected async commandAction(): Promise<void> { }
+    protected async commandAction(): Promise<void> {
+        // No action
+    }
 }
 
 
diff --git a/NodeApp/src/commander/auth/AuthCommand.ts b/NodeApp/src/commander/auth/AuthCommand.ts
index c5f4e00afdfa037f1f732c940fee25b34b401822..85fac503754f6994f0f44224088850cea0de64a9 100644
--- a/NodeApp/src/commander/auth/AuthCommand.ts
+++ b/NodeApp/src/commander/auth/AuthCommand.ts
@@ -9,7 +9,7 @@ class AuthCommand extends CommanderCommand {
 
     protected defineCommand() {
         this.command
-        .description('manage Dojo and Gitlab sessions');
+            .description('manage Dojo and Gitlab sessions');
     }
 
     protected defineSubCommands() {
@@ -18,7 +18,9 @@ class AuthCommand extends CommanderCommand {
         SessionTestCommand.registerOnCommand(this.command);
     }
 
-    protected async commandAction(): Promise<void> { }
+    protected async commandAction(): Promise<void> {
+        // No action
+    }
 }
 
 
diff --git a/NodeApp/src/commander/completion/CompletionCommand.ts b/NodeApp/src/commander/completion/CompletionCommand.ts
index c24a7d6f4876f8a20812f6a07f8b5d8c82e9eacb..1ee0668b994796b04d303357de071c7a3b4f75ff 100644
--- a/NodeApp/src/commander/completion/CompletionCommand.ts
+++ b/NodeApp/src/commander/completion/CompletionCommand.ts
@@ -9,7 +9,7 @@ class CompletionCommand extends CommanderCommand {
 
     protected defineCommand() {
         this.command
-        .description('generate completions for bash, fish, or zsh');
+            .description('generate completions for bash, fish, or zsh');
     }
 
     protected defineSubCommands() {
@@ -18,7 +18,9 @@ class CompletionCommand extends CommanderCommand {
         CompletionZshCommand.registerOnCommand(this.command);
     }
 
-    protected async commandAction(): Promise<void> { }
+    protected async commandAction(): Promise<void> {
+        // No action
+    }
 }
 
 
diff --git a/NodeApp/src/commander/completion/subcommands/CompletionFishCommand.ts b/NodeApp/src/commander/completion/subcommands/CompletionFishCommand.ts
index 1b29eab1a9daa083f89a174f32b1be7e5ea44297..2be5716ee17ae7347d146533f48c8594801b73e5 100644
--- a/NodeApp/src/commander/completion/subcommands/CompletionFishCommand.ts
+++ b/NodeApp/src/commander/completion/subcommands/CompletionFishCommand.ts
@@ -11,7 +11,7 @@ import GlobalHelper                                       from '../../../helpers
 class CompletionFishCommand extends CommanderCommand {
     protected commandName: string = 'fish';
 
-    private installPath = path.join(os.homedir(), '.config/fish/completions/dojo.fish');
+    private readonly installPath = path.join(os.homedir(), '.config/fish/completions/dojo.fish');
 
     protected defineCommand() {
         GlobalHelper.completionCommandDefinition(this.command)
@@ -29,10 +29,11 @@ class CompletionFishCommand extends CommanderCommand {
 
             spinner.succeed(`Fish completion successfully written in ${ filename }.`);
             if ( showInstructions ) {
+                const cpCommand = ` cp -i ${ filename } ~/.config/fish/completions  # interactive cp to avoid accidents `;
                 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 `) }`);
+${ TextStyle.CODE(cpCommand) }`);
             }
         } catch ( error ) {
             spinner.fail(`Fish completion error: ${ error }.`);
diff --git a/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts b/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts
index f7f4b8721846f94ce6f303c030069e9c37e998b7..f9d279eb2e86b29f567db78797f98333b70c4ec2 100644
--- a/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts
+++ b/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts
@@ -37,6 +37,11 @@ source ${ this.bashCompletion }
 
             spinner.succeed(`Bash completion successfully written in ${ TextStyle.CODE(filename) }`);
             if ( showInstructions ) {
+                const zprofileContent = TextStyle.CODE(`
+autoload -U +X compinit && compinit
+autoload -U +X bashcompinit && bashcompinit
+source ${ filename }
+`);
                 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.
 
@@ -45,54 +50,40 @@ 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 }
-`) } `);
+Next add the following lines to your ${ TextStyle.CODE('~/.zprofile') } file: ${ zprofileContent } `);
             }
         } 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);
+    protected addToZprofile(zprofilePath: string, bashPath: string) {
+        const spinner: ora.Ora = ora(`Modifying ${ zprofilePath } ...`).start();
+        if ( fs.existsSync(zprofilePath) ) {
+            const data = fs.readFileSync(zprofilePath);
             let updated = false;
-            if ( !data.includes('autoload -U +X compinit && compinit') ) {
-                try {
-                    fs.appendFileSync(path, '\nautoload -U +X compinit && compinit');
+            try {
+                if ( !data.includes('autoload -U +X compinit && compinit') ) {
+                    fs.appendFileSync(zprofilePath, '\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');
+                if ( !data.includes('autoload -U +X bashcompinit && bashcompinit') ) {
+                    fs.appendFileSync(zprofilePath, '\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 }`);
+                if ( !data.includes(`source ${ bashPath }`) ) {
+                    fs.appendFileSync(zprofilePath, `\nsource ${ bashPath }`);
                     updated = true;
-                } catch {
-                    spinner.fail(`Error appending in ${ this.zprofile }`);
                 }
+            } catch {
+                spinner.fail(`Error appending in ${ this.zprofile }`);
+                return;
             }
-            if ( updated ) {
-                spinner.succeed(`Zsh profile updated.`);
-            } else {
-                spinner.succeed(`Zsh profile already up to date.`);
-            }
+
+            spinner.succeed(updated ? `Zsh profile updated.` : `Zsh profile already up to date.`);
         } else {
             try {
-                fs.writeFileSync(path, this.loadBashCompletion);
+                fs.writeFileSync(zprofilePath, this.loadBashCompletion);
                 spinner.succeed(`Zsh profile written.`);
             } catch ( error ) {
                 spinner.fail(`Error writing in ${ this.zprofile }`);
diff --git a/NodeApp/src/commander/exercise/ExerciseCommand.ts b/NodeApp/src/commander/exercise/ExerciseCommand.ts
index 2a93b7a40c66845444f75d2453f731ab8fa6925f..13bd0b3992b9983fc210513203bb6ede5909b13a 100644
--- a/NodeApp/src/commander/exercise/ExerciseCommand.ts
+++ b/NodeApp/src/commander/exercise/ExerciseCommand.ts
@@ -9,7 +9,7 @@ class ExerciseCommand extends CommanderCommand {
 
     protected defineCommand() {
         this.command
-        .description('manage an exercise');
+            .description('manage an exercise');
     }
 
     protected defineSubCommands() {
@@ -18,7 +18,9 @@ class ExerciseCommand extends CommanderCommand {
         ExerciseCorrectionCommand.registerOnCommand(this.command);
     }
 
-    protected async commandAction(): Promise<void> { }
+    protected async commandAction(): Promise<void> {
+        // No action
+    }
 }
 
 
diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseCorrectionCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseCorrectionCommand.ts
index 03641466226d60d598814addff6ad1e1963a505e..0e9882660eb9cb19e555cc56d59db1f8bebeeba5 100644
--- a/NodeApp/src/commander/exercise/subcommands/ExerciseCorrectionCommand.ts
+++ b/NodeApp/src/commander/exercise/subcommands/ExerciseCorrectionCommand.ts
@@ -17,9 +17,9 @@ class ExerciseCorrectionCommand extends CommanderCommand {
 
     protected defineCommand() {
         this.command
-        .description('link an exercise repo as a correction for an assignment')
-        .requiredOption('-a, --assignment <string>', 'id or url of the assignment of the correction')
-        .action(this.commandAction.bind(this));
+            .description('link an exercise repo as a correction for an assignment')
+            .requiredOption('-a, --assignment <string>', 'id or url of the assignment of the correction')
+            .action(this.commandAction.bind(this));
     }
 
     protected async commandAction(options: { assignment: string }): Promise<void> {
@@ -34,7 +34,6 @@ class ExerciseCorrectionCommand extends CommanderCommand {
             Config.interactiveMode ? await this.showCorrectionsInteractive(assignment, assignmentGetSpinner) : this.showCorrections(assignment, assignmentGetSpinner);
         } else {
             assignmentGetSpinner.fail(`The assignment doesn't have any corrections yet`);
-            return;
         }
     }
 
diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts
index 8f7673d0049654d3b9b394cbc9cf986ef07442c0..2fc6bf7bb8da17746bf56cfceed927aed91fb65e 100644
--- a/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts
+++ b/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts
@@ -14,12 +14,12 @@ class ExerciseCreateCommand extends CommanderCommand {
 
     protected defineCommand() {
         this.command
-        .description('create a new exercise from an assignment')
-        .requiredOption('-a, --assignment <value>', 'assignment source (Dojo assignment ID, Dojo assignment name or Gitlab assignment URL)')
-        .option('-i, --members_id <ids...>', 'list of gitlab members ids (group\'s student) to add to the repository')
-        .option('-u, --members_username <usernames...>', 'list of gitlab members username (group\'s student) to add to the repository')
-        .option('-c, --clone [string]', 'automatically clone the repository (SSH required) in the specified directory (this will create a subdirectory with the assignment name)')
-        .action(this.commandAction.bind(this));
+            .description('create a new exercise from an assignment')
+            .requiredOption('-a, --assignment <value>', 'assignment source (Dojo assignment ID, Dojo assignment name or Gitlab assignment URL)')
+            .option('-i, --members_id <ids...>', 'list of gitlab members ids (group\'s student) to add to the repository')
+            .option('-u, --members_username <usernames...>', 'list of gitlab members username (group\'s student) to add to the repository')
+            .option('-c, --clone [string]', 'automatically clone the repository (SSH required) in the specified directory (this will create a subdirectory with the assignment name)')
+            .action(this.commandAction.bind(this));
     }
 
     protected async commandAction(options: { assignment: string, members_id?: Array<number>, members_username?: Array<string>, clone?: string | boolean }): Promise<void> {
@@ -68,7 +68,7 @@ class ExerciseCreateCommand extends CommanderCommand {
             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);
+                exercise = await DojoBackendManager.createExercise(assignment.name, members);
 
                 const oraInfo = (message: string) => {
                     ora({
diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseRunCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseRunCommand.ts
index 75af9463ee742490d62857b8d97f686367516fc6..db7be9be2bc62e0fdcc5b2f12526bc405a2b101d 100644
--- a/NodeApp/src/commander/exercise/subcommands/ExerciseRunCommand.ts
+++ b/NodeApp/src/commander/exercise/subcommands/ExerciseRunCommand.ts
@@ -8,12 +8,12 @@ class ExerciseRunCommand extends CommanderCommand {
 
     protected defineCommand() {
         GlobalHelper.runCommandDefinition(this.command)
-        .description('locally run an exercise')
-        .action(this.commandAction.bind(this));
+            .description('locally run an exercise')
+            .action(this.commandAction.bind(this));
     }
 
     protected async commandAction(options: { path: string, verbose: boolean, superVerbose: boolean }): Promise<void> {
-        await ExerciseRunHelper.run(options);
+        await (new ExerciseRunHelper(options)).run();
     }
 }
 
diff --git a/NodeApp/src/config/LocalConfigFile.ts b/NodeApp/src/config/LocalConfigFile.ts
index ecc0530b3ce4144a99a10f1d9ec87fcb11ab99c3..390e95b955cdd52c787793dc802a4e5dc21686ff 100644
--- a/NodeApp/src/config/LocalConfigFile.ts
+++ b/NodeApp/src/config/LocalConfigFile.ts
@@ -4,7 +4,11 @@ import JSON5   from 'json5';
 
 
 class LocalConfigFile {
-    constructor(private filename: string) {
+    private readonly filename: string;
+
+    constructor(filename: string) {
+        this.filename = filename;
+        
         this.loadConfig();
     }
 
diff --git a/NodeApp/src/helpers/AutoCompletionHelper.ts b/NodeApp/src/helpers/AutoCompletionHelper.ts
index f0c480a82d97af2be721b88ecc7285f9c242abf8..56988ebdceb9f25b1ba53fff4a6021059ca138c9 100644
--- a/NodeApp/src/helpers/AutoCompletionHelper.ts
+++ b/NodeApp/src/helpers/AutoCompletionHelper.ts
@@ -1,48 +1,49 @@
-import { Command } from 'commander';
+import { Command }                from 'commander';
 import { existsSync, renameSync } from 'fs';
-import ora from 'ora';
-import TextStyle from '../types/TextStyle';
-import inquirer from 'inquirer';
+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();
+    const oldFilename = `${ filename }.old`;
+    const spinner: ora.Ora = ora(`Renaming ${ TextStyle.CODE(filename) } in ${ TextStyle.CODE(oldFilename) } ...`).start();
     try {
-        renameSync(filename, old_filename);
-        spinner.succeed(`Renaming success: ${TextStyle.CODE(filename)} in ${TextStyle.CODE(old_filename)}`);
+        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(old_filename)}. If this was not intended please revert this change.`);
+        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}.`);
+    } 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
+                                      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)
+    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
+            console.log(`${ TextStyle.BLOCK('Completion generation interrupted.') }`);
+            return false;
         }
     }
-    return true
+    return true;
 }
 
 const fishFunction = `
@@ -64,28 +65,28 @@ complete -f -c dojo
 `;
 
 function isHidden(cmd: Command): boolean {
-    return (cmd as Command & { _hidden: boolean })._hidden
+    return (cmd as Command & { _hidden: boolean })._hidden;
 
 }
 
 function isLeaf(cmd: Command): boolean {
-    return cmd.commands.length == 0;
+    return cmd.commands.length === 0;
 }
 
 function flatten(cmd: Command): Array<Command> {
-    if (isLeaf(cmd)) {
-        return [cmd];
+    if ( isLeaf(cmd) ) {
+        return [ cmd ];
     } else {
         return cmd.commands
             .filter(c => !isHidden(c))
             .map(child => flatten(child))
-            .reduce((acc, cmd) => acc.concat(cmd), [cmd]);
+            .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) {
+    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);
@@ -96,7 +97,7 @@ function computeDepth(cmd: Command | undefined): number {
 function computeHeight(cmd: Command | null): number {
     let height = 0;
     let tmp = cmd;
-    while (tmp !== null) {
+    while ( tmp !== null ) {
         tmp = tmp.parent;
         height += 1;
     }
@@ -105,7 +106,7 @@ function computeHeight(cmd: Command | null): number {
 
 // Computes the maximum number of commands until the root is reached
 export function getRoot(cmd: Command): Command {
-    if (cmd.parent == null) {
+    if ( cmd.parent == null ) {
         return cmd;
     } else {
         return getRoot(cmd.parent);
@@ -114,7 +115,7 @@ export function getRoot(cmd: Command): Command {
 
 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(' ');
+    return cmd.options.filter(opt => !opt.hidden).map(opt => opt.flags.replace(/<.*?>/, '').replace(/\[.*?]/, '').replace(',', '').trimEnd()).join(' ');
 }
 
 function commandsAndOptionsToString(cmd: Command): string {
@@ -122,15 +123,14 @@ function commandsAndOptionsToString(cmd: Command): string {
 }
 
 function addLine(identLevel: number, pattern: string): string {
-    return `${'    '.repeat(identLevel)}${pattern}\n`;
+    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');
-
+    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()})`);
+        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);
         });
@@ -142,8 +142,8 @@ function generateBashSubCommands(cmd: Command, current: number, maxDepth: number
 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');
+    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');
@@ -155,8 +155,8 @@ 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}`);
+    while ( cmd !== null ) {
+        data = cmd.name().concat(` ${ data }`);
         cmd = cmd.parent;
     }
     return data.trimEnd();
@@ -167,21 +167,15 @@ function hasOptions(cmd: Command): boolean {
 }
 
 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');
+    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);
 
-    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;
+    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(''));
 }
 
 
diff --git a/NodeApp/src/helpers/Dojo/ExerciseRunHelper.ts b/NodeApp/src/helpers/Dojo/ExerciseRunHelper.ts
index 07fa2f5b4de4d383c4cfd77765efe02ac8349886..da2ff6f6de73e73a257cc29d1f127b692663c5f9 100644
--- a/NodeApp/src/helpers/Dojo/ExerciseRunHelper.ts
+++ b/NodeApp/src/helpers/Dojo/ExerciseRunHelper.ts
@@ -31,6 +31,25 @@ class ExerciseRunHelper {
 
     private readonly fileComposeLogs: string = path.join(this.folderResultsDojo, `dockerComposeLogs.txt`);
 
+    private readonly options: { path: string, verbose: boolean, superVerbose: boolean };
+    private readonly verbose: boolean;
+    private readonly localExercisePath: string;
+
+    private assignmentFile!: AssignmentFile;
+    private exerciseDockerCompose!: ExerciseDockerCompose;
+    private exerciseResultsValidation!: ExerciseResultsSanitizerAndValidator;
+
+    private haveResultsVolume!: boolean;
+
+    private exerciseRunSpinner!: ora.Ora;
+    private buildPhase: boolean | undefined = undefined;
+
+    constructor(options: { path: string, verbose: boolean, superVerbose: boolean }) {
+        this.options = options;
+        this.verbose = options.verbose || options.superVerbose || SharedConfig.debug;
+        this.localExercisePath = options.path ?? Config.folders.defaultLocalExercise;
+    }
+
     private displayExecutionLogs() {
         ora({
                 text  : `${ chalk.magenta('Execution logs folder:') } ${ this.folderResultsVolume }`,
@@ -38,231 +57,239 @@ class ExerciseRunHelper {
             }).start().info();
     }
 
-    async run(options: { path: string, verbose: boolean, superVerbose: boolean }): Promise<void> {
-        const verbose: boolean = options.verbose || options.superVerbose || SharedConfig.debug;
+    /**
+     * Step 1: Check requirements (if it's an exercise folder and if Docker daemon is running)
+     * @private
+     */
+    private async checkRequirements() {
+        console.log(TextStyle.BLOCK('Please wait while we are checking and creating dependencies...'));
 
-        const localExercisePath: string = options.path ?? Config.folders.defaultLocalExercise;
+        // Create result temp folder
+        fs.mkdirSync(this.folderResultsVolume, { recursive: true });
+        fs.mkdirSync(this.folderResultsDojo, { recursive: true });
+        fs.mkdirSync(this.folderResultsExercise, { recursive: true });
 
-        let assignmentFile: AssignmentFile;
-        let exerciseDockerCompose: ExerciseDockerCompose;
-        let exerciseResultsValidation: ExerciseResultsSanitizerAndValidator;
 
-        let haveResultsVolume: boolean;
+        ora({
+                text  : `Checking exercise content:`,
+                indent: 4
+            }).start().info();
 
-        // Step 1: Check requirements (if it's an exercise folder and if Docker daemon is running)
+        // Exercise folder
         {
-            console.log(TextStyle.BLOCK('Please wait while we are checking and creating dependencies...'));
+            const spinner: ora.Ora = ora({
+                                             text  : `Checking exercise folder`,
+                                             indent: 8
+                                         }).start();
 
-            // Create result temp folder
-            fs.mkdirSync(this.folderResultsVolume, { recursive: true });
-            fs.mkdirSync(this.folderResultsDojo, { recursive: true });
-            fs.mkdirSync(this.folderResultsExercise, { recursive: true });
+            const files = fs.readdirSync(this.options.path);
+            const missingFiles = Config.exercise.neededFiles.map((file: string): [ string, boolean ] => [ file, files.includes(file) ]).filter((file: [ string, boolean ]) => !file[1]);
 
+            if ( missingFiles.length > 0 ) {
+                spinner.fail(`The exercise folder is missing the following files: ${ missingFiles.map((file: [ string, boolean ]) => file[0]).join(', ') }`);
+                throw new Error();
+            }
 
-            ora({
-                    text  : `Checking exercise content:`,
-                    indent: 4
-                }).start().info();
+            spinner.succeed(`The exercise folder contains all the needed files`);
+        }
+
+        // dojo_assignment.json validity
+        {
+            const spinner: ora.Ora = ora({
+                                             text  : `Checking ${ ClientsSharedConfig.assignment.filename } file`,
+                                             indent: 8
+                                         }).start();
+
+            const validationResults = SharedAssignmentHelper.validateDescriptionFile(path.join(this.options.path, ClientsSharedConfig.assignment.filename));
+            if ( !validationResults.isValid ) {
+                spinner.fail(`The ${ ClientsSharedConfig.assignment.filename } file is invalid: ${ validationResults.error }`);
+                throw new Error();
+            } else {
+                this.assignmentFile = validationResults.content!;
+            }
 
-            // Exercise folder
-            {
-                const spinner: ora.Ora = ora({
-                                                 text  : `Checking exercise folder`,
-                                                 indent: 8
-                                             }).start();
+            this.haveResultsVolume = this.assignmentFile.result.volume !== undefined;
 
-                const files = fs.readdirSync(options.path);
-                const missingFiles = Config.exercise.neededFiles.map((file: string): [ string, boolean ] => [ file, files.includes(file) ]).filter((file: [ string, boolean ]) => !file[1]);
+            spinner.succeed(`The ${ ClientsSharedConfig.assignment.filename } file is valid`);
+        }
 
-                if ( missingFiles.length > 0 ) {
-                    spinner.fail(`The exercise folder is missing the following files: ${ missingFiles.map((file: [ string, boolean ]) => file[0]).join(', ') }`);
-                    return;
-                }
+        // Docker daemon
+        {
+            const spinner: ora.Ora = ora({
+                                             text  : `Checking Docker daemon`,
+                                             indent: 4
+                                         }).start();
 
-                spinner.succeed(`The exercise folder contains all the needed files`);
+            try {
+                await execAsync(`docker ps`);
+            } catch ( error ) {
+                spinner.fail(`The Docker daemon is not running`);
+                throw new Error();
             }
 
-            // dojo_assignment.json validity
-            {
-                const spinner: ora.Ora = ora({
-                                                 text  : `Checking ${ ClientsSharedConfig.assignment.filename } file`,
-                                                 indent: 8
-                                             }).start();
-
-                const validationResults = SharedAssignmentHelper.validateDescriptionFile(path.join(options.path, ClientsSharedConfig.assignment.filename));
-                if ( !validationResults.isValid ) {
-                    spinner.fail(`The ${ ClientsSharedConfig.assignment.filename } file is invalid: ${ validationResults.error }`);
-                    return;
-                } else {
-                    assignmentFile = validationResults.content!;
-                }
+            spinner.succeed(`The Docker daemon is running`);
+        }
+    }
 
-                haveResultsVolume = assignmentFile.result.volume !== undefined;
+    private logsEvent(log: string, _error: boolean, displayable: boolean, currentStep: string) {
+        for ( const line of log.split('\n') ) {
+            if ( displayable && this.buildPhase === undefined && line.startsWith('#') ) {
+                this.buildPhase = true;
+            }
 
-                spinner.succeed(`The ${ ClientsSharedConfig.assignment.filename } file is valid`);
+            if ( currentStep === 'COMPOSE_RUN' && this.buildPhase === true && line !== '' && !line.startsWith('#') ) {
+                this.buildPhase = false;
             }
 
-            // Docker daemon
-            {
-                const spinner: ora.Ora = ora({
-                                                 text  : `Checking Docker daemon`,
-                                                 indent: 4
-                                             }).start();
-
-                try {
-                    await execAsync(`docker ps`);
-                } catch ( error ) {
-                    spinner.fail(`The Docker daemon is not running`);
-                    return;
-                }
+            if ( SharedConfig.debug || (displayable && (this.options.superVerbose || this.buildPhase === false)) ) {
+                console.log(line);
+            }
+        }
+    }
+
+    private stepEvent(name: string, message: string) {
+        this.exerciseRunSpinner = ora({
+                                          text  : message,
+                                          indent: 4
+                                      }).start();
+
+        if ( this.verbose && name === 'COMPOSE_RUN' ) {
+            this.exerciseRunSpinner.info();
+        }
+    }
 
-                spinner.succeed(`The Docker daemon is running`);
+    private endStepEvent(stepName: string, message: string, error: boolean) {
+        if ( error ) {
+            if ( this.verbose && stepName === 'COMPOSE_RUN' ) {
+                ora({
+                        text  : message,
+                        indent: 4
+                    }).start().fail();
+            } else {
+                this.exerciseRunSpinner.fail(message);
             }
+        } else if ( this.verbose && stepName === 'COMPOSE_RUN' ) {
+            ora({
+                    text  : message,
+                    indent: 4
+                }).start().succeed();
+        } else {
+            this.exerciseRunSpinner.succeed(message);
         }
 
+    }
 
-        // Step 2: Run docker-compose file
-        {
-            console.log(TextStyle.BLOCK('Please wait while we are running the exercise...'));
+    /**
+     * Step 2: Run docker-compose file
+     * @private
+     */
+    private async runDockerCompose() {
+        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');
-            if ( haveResultsVolume ) {
-                const composeOverride = fs.readFileSync(path.join(__dirname, '../../../assets/docker-compose-override.yml'), 'utf8').replace('{{VOLUME_NAME}}', assignmentFile.result.volume!).replace('{{MOUNT_PATH}}', this.folderResultsExercise);
-                fs.writeFileSync(composeOverridePath, composeOverride);
+        let composeFileOverride: string[] = [];
+        const composeOverridePath: string = path.join(this.localExercisePath, 'docker-compose-override.yml');
+        if ( this.haveResultsVolume ) {
+            const composeOverride = fs.readFileSync(path.join(__dirname, '../../../assets/docker-compose-override.yml'), 'utf8').replace('{{VOLUME_NAME}}', this.assignmentFile.result.volume!).replace('{{MOUNT_PATH}}', this.folderResultsExercise);
+            fs.writeFileSync(composeOverridePath, composeOverride);
 
-                composeFileOverride = [ composeOverridePath ];
-            }
+            composeFileOverride = [ composeOverridePath ];
+        }
 
-            exerciseDockerCompose = new ExerciseDockerCompose(this.projectName, assignmentFile, localExercisePath, composeFileOverride);
+        this.exerciseDockerCompose = new ExerciseDockerCompose(this.projectName, this.assignmentFile, this.localExercisePath, composeFileOverride);
 
-            try {
-                await new Promise<void>((resolve, reject) => {
-                    let spinner: ora.Ora;
-
-                    if ( verbose ) {
-                        let buildPhase: boolean | undefined = undefined;
-                        exerciseDockerCompose.events.on('logs', (log: string, _error: boolean, displayable: boolean, currentStep: string) => {
-                            for ( const line of log.split('\n') ) {
-                                if ( displayable && buildPhase == undefined && line.startsWith('#') ) {
-                                    buildPhase = true;
-                                }
-
-                                if ( currentStep == 'COMPOSE_RUN' && buildPhase === true && line != '' && !line.startsWith('#') ) {
-                                    buildPhase = false;
-                                }
-
-                                if ( SharedConfig.debug || (displayable && (options.superVerbose || buildPhase === false)) ) {
-                                    console.log(line);
-                                }
-                            }
-                        });
-                    }
+        try {
+            await new Promise<void>((resolve, reject) => {
+                if ( this.verbose ) {
+                    this.exerciseDockerCompose.events.on('logs', this.logsEvent.bind(this));
+                }
 
-                    exerciseDockerCompose.events.on('step', (name: string, message: string) => {
-                        spinner = ora({
-                                          text  : message,
-                                          indent: 4
-                                      }).start();
+                this.exerciseDockerCompose.events.on('step', this.stepEvent.bind(this));
 
-                        if ( verbose && name == 'COMPOSE_RUN' ) {
-                            spinner.info();
-                        }
-                    });
-
-                    exerciseDockerCompose.events.on('endStep', (stepName: string, message: string, error: boolean) => {
-                        if ( error ) {
-                            if ( verbose && stepName == 'COMPOSE_RUN' ) {
-                                ora({
-                                        text  : message,
-                                        indent: 4
-                                    }).start().fail();
-                            } else {
-                                spinner.fail(message);
-                            }
-                        } else {
-                            if ( verbose && stepName == 'COMPOSE_RUN' ) {
-                                ora({
-                                        text  : message,
-                                        indent: 4
-                                    }).start().succeed();
-                            } else {
-                                spinner.succeed(message);
-                            }
-                        }
-                    });
+                this.exerciseDockerCompose.events.on('endStep', this.endStepEvent.bind(this));
 
-                    exerciseDockerCompose.events.on('finished', (success: boolean) => {
-                        success ? resolve() : reject();
-                    });
+                this.exerciseDockerCompose.events.on('finished', (success: boolean) => success ? resolve() : reject());
 
-                    exerciseDockerCompose.run(true);
-                });
-            } catch ( error ) { /* empty */ }
+                this.exerciseDockerCompose.run(true);
+            });
+        } catch ( error ) { /* empty */ }
 
-            fs.rmSync(composeOverridePath, { force: true });
-            fs.writeFileSync(this.fileComposeLogs, exerciseDockerCompose.allLogs);
+        fs.rmSync(composeOverridePath, { force: true });
+        fs.writeFileSync(this.fileComposeLogs, this.exerciseDockerCompose.allLogs);
 
-            if ( !exerciseDockerCompose.success ) {
-                this.displayExecutionLogs();
-                return;
-            }
+        if ( !this.exerciseDockerCompose.success ) {
+            this.displayExecutionLogs();
+            throw new Error();
         }
+    }
 
+    /**
+     * Step 3: Get results
+     * @private
+     */
+    private async getResults() {
+        console.log(TextStyle.BLOCK('Please wait while we are checking the results...'));
 
-        // Step 3: Get results
-        {
-            console.log(TextStyle.BLOCK('Please wait while we are checking the results...'));
+        this.exerciseResultsValidation = new ExerciseResultsSanitizerAndValidator(this.folderResultsDojo, this.folderResultsExercise, this.exerciseDockerCompose.exitCode);
 
-            exerciseResultsValidation = new ExerciseResultsSanitizerAndValidator(this.folderResultsDojo, this.folderResultsExercise, exerciseDockerCompose.exitCode);
+        try {
+            await new Promise<void>((resolve, reject) => {
+                let spinner: ora.Ora;
 
-            try {
-                await new Promise<void>((resolve, reject) => {
-                    let spinner: ora.Ora;
+                this.exerciseResultsValidation.events.on('step', (_name: string, message: string) => {
+                    spinner = ora({
+                                      text  : message,
+                                      indent: 4
+                                  }).start();
+                });
 
-                    exerciseResultsValidation.events.on('step', (_name: string, message: string) => {
-                        spinner = ora({
-                                          text  : message,
-                                          indent: 4
-                                      }).start();
-                    });
-
-                    exerciseResultsValidation.events.on('endStep', (stepName: string, message: string, error: boolean) => {
-                        if ( error ) {
-                            if ( stepName == 'CHECK_SIZE' ) {
-                                spinner.warn(message);
-                            } else {
-                                spinner.fail(message);
-                            }
+                this.exerciseResultsValidation.events.on('endStep', (stepName: string, message: string, error: boolean) => {
+                    if ( error ) {
+                        if ( stepName === 'CHECK_SIZE' ) {
+                            spinner.warn(message);
                         } else {
-                            spinner.succeed(message);
+                            spinner.fail(message);
                         }
-                    });
-
-                    exerciseResultsValidation.events.on('finished', (success: boolean, exitCode: number) => {
-                        success || exitCode == ExerciseCheckerError.EXERCISE_RESULTS_FOLDER_TOO_BIG ? resolve() : reject();
-                    });
+                    } else {
+                        spinner.succeed(message);
+                    }
+                });
 
-                    exerciseResultsValidation.run();
+                this.exerciseResultsValidation.events.on('finished', (success: boolean, exitCode: number) => {
+                    success || exitCode === ExerciseCheckerError.EXERCISE_RESULTS_FOLDER_TOO_BIG ? resolve() : reject();
                 });
-            } catch ( error ) {
-                this.displayExecutionLogs();
-                return;
-            }
+
+                this.exerciseResultsValidation.run();
+            });
+        } catch ( error ) {
+            this.displayExecutionLogs();
+            throw new Error();
         }
+    }
 
+    /**
+     * Step 4: Display results + Volume location
+     * @private
+     */
+    private async displayResults() {
+        const info = chalk.magenta.bold.italic;
+        ClientsSharedExerciseHelper.displayExecutionResults(this.exerciseResultsValidation.exerciseResults, this.exerciseDockerCompose.exitCode, {
+            INFO   : info,
+            SUCCESS: chalk.green,
+            FAILURE: chalk.red
+        }, `\n\n${ info('Execution results folder: ') }${ this.folderResultsVolume }`);
+    }
 
-        // Step 4: Display results + Volume location
-        {
-            const info = chalk.magenta.bold.italic;
-            ClientsSharedExerciseHelper.displayExecutionResults(exerciseResultsValidation.exerciseResults!, exerciseDockerCompose.exitCode, {
-                INFO   : info,
-                SUCCESS: chalk.green,
-                FAILURE: chalk.red
-            }, `\n\n${ info('Execution results folder: ') }${ this.folderResultsVolume }`);
+    async run(): Promise<void> {
+        try {
+            await this.checkRequirements();
+            await this.runDockerCompose();
+            await this.getResults();
+            await this.displayResults();
+        } catch ( error ) {
+            return;
         }
     }
 }
 
 
-export default new ExerciseRunHelper();
+export default ExerciseRunHelper;
diff --git a/NodeApp/src/managers/DojoBackendManager.ts b/NodeApp/src/managers/DojoBackendManager.ts
index fcd749df9a44d151c202498cb8d5a8dfe5f2df2a..b2ef6295a811b254a32f333d7f4fd2800bd91420 100644
--- a/NodeApp/src/managers/DojoBackendManager.ts
+++ b/NodeApp/src/managers/DojoBackendManager.ts
@@ -1,7 +1,6 @@
 import axios, { AxiosError } from 'axios';
 import ora                   from 'ora';
 import ApiRoute              from '../sharedByClients/types/Dojo/ApiRoute';
-import { StatusCodes }       from 'http-status-codes';
 import GitlabUser            from '../shared/types/Gitlab/GitlabUser';
 import ClientsSharedConfig   from '../sharedByClients/config/ClientsSharedConfig';
 import Assignment            from '../sharedByClients/models/Assignment';
@@ -13,6 +12,49 @@ import DojoStatusCode        from '../shared/types/Dojo/DojoStatusCode';
 
 
 class DojoBackendManager {
+    private handleApiError(error: unknown, spinner: ora.Ora, verbose: boolean, defaultErrorMessage?: string, otherErrorHandler?: (error: AxiosError, spinner: ora.Ora, verbose: boolean) => void) {
+        if ( verbose ) {
+            if ( error instanceof AxiosError ) {
+                switch ( error.response?.data?.code ) {
+                    case DojoStatusCode.ASSIGNMENT_EXERCISE_NOT_RELATED:
+                        spinner.fail(`The exercise does not belong to the assignment.`);
+                        break;
+                    case DojoStatusCode.EXERCISE_CORRECTION_ALREADY_EXIST:
+                        spinner.fail(`This exercise is already labelled as a correction. If you want to update it, please use the update command.`);
+                        break;
+                    case DojoStatusCode.EXERCISE_CORRECTION_NOT_EXIST:
+                        spinner.fail(`The exercise is not labelled as a correction so it's not possible to update it.`);
+                        break;
+                    case DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR:
+                        spinner.fail(`Assignment creation error: An unknown error occurred while creating the assignment on Gitlab. Please try again later or contact an administrator.`);
+                        break;
+                    case DojoStatusCode.MAX_EXERCISE_PER_ASSIGNMENT_REACHED:
+                        spinner.fail(`The following users have reached the maximum number of exercise of this assignment : ${ ((error.response.data as DojoBackendResponse<Array<GitlabUser>>).data).map(user => user.name).join(', ') }.`);
+                        break;
+                    case DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR:
+                        spinner.fail(`Exercise creation error: An unknown error occurred while creating the exercise on Gitlab. Please try again later or contact an administrator.`);
+                        break;
+                    case DojoStatusCode.GITLAB_TEMPLATE_NOT_FOUND:
+                        spinner.fail(`Template not found or access denied. Please check the template ID or url. Also, please check that the template have public/internal visibility or that your and Dojo account (${ ClientsSharedConfig.gitlab.dojoAccount.username }) have at least reporter role to the template (if private).`);
+                        break;
+                    case DojoStatusCode.GITLAB_TEMPLATE_ACCESS_UNAUTHORIZED:
+                        spinner.fail(`Please check that the template have public/internal visibility or that your and Dojo account (${ ClientsSharedConfig.gitlab.dojoAccount.username }) have at least reporter role to the template (if private).`);
+                        break;
+                    default:
+                        if ( otherErrorHandler ) {
+                            otherErrorHandler(error, spinner, verbose);
+                        } else {
+                            spinner.fail(defaultErrorMessage ?? 'Unknown error');
+                        }
+                        break;
+                }
+            } else {
+                spinner.fail(defaultErrorMessage ?? 'Unknown error');
+            }
+        }
+    }
+
+
     public getApiUrl(route: ApiRoute): string {
         return `${ ClientsSharedConfig.apiURL }${ route }`;
     }
@@ -62,21 +104,7 @@ class DojoBackendManager {
 
             return true;
         } catch ( error ) {
-            if ( verbose ) {
-                if ( error instanceof AxiosError ) {
-                    if ( error.response ) {
-                        if ( error.response.status === StatusCodes.NOT_FOUND ) {
-                            spinner.fail(`Template not found or access denied. Please check the template ID or url. Also, please check that the template have public/internal visibility or that your and Dojo account (${ ClientsSharedConfig.gitlab.dojoAccount.username }) have at least reporter role to the template (if private).`);
-                        } else if ( error.response.status === StatusCodes.UNAUTHORIZED ) {
-                            spinner.fail(`Please check that the template have public/internal visibility or that your and Dojo account (${ ClientsSharedConfig.gitlab.dojoAccount.username }) have at least reporter role to the template (if private).`);
-                        } else {
-                            spinner.fail(`Template error: ${ error.response.statusText }`);
-                        }
-                    }
-                } else {
-                    spinner.fail(`Template error: ${ error }`);
-                }
-            }
+            this.handleApiError(error, spinner, verbose, `Template error: ${ error }`);
 
             return false;
         }
@@ -101,23 +129,7 @@ class DojoBackendManager {
 
             return response.data.data;
         } catch ( error ) {
-            if ( verbose ) {
-                if ( error instanceof AxiosError ) {
-                    if ( error.response ) {
-                        if ( error.response.status === StatusCodes.CONFLICT ) {
-                            spinner.fail(`The assignment name is already used. Please choose another one.`);
-                        } else {
-                            if ( (error.response.data as DojoBackendResponse<unknown>).code === DojoStatusCode.ASSIGNMENT_CREATION_GITLAB_ERROR ) {
-                                spinner.fail(`Assignment creation error: An unknown error occurred while creating the assignment on Gitlab. Please try again later or contact an administrator.`);
-                            } else {
-                                spinner.fail(`Assignment creation error: An unknown error occurred while creating the assignment on Dojo server. Please try again later or contact an administrator.`);
-                            }
-                        }
-                    }
-                } else {
-                    spinner.fail(`Assignment creation error: unknown error`);
-                }
-            }
+            this.handleApiError(error, spinner, verbose, `Assignment creation error: unknown error`);
 
             throw error;
         }
@@ -139,27 +151,7 @@ class DojoBackendManager {
 
             return response.data.data;
         } catch ( error ) {
-            if ( verbose ) {
-                if ( error instanceof AxiosError ) {
-                    if ( error.response ) {
-                        if ( error.response.status === StatusCodes.INSUFFICIENT_SPACE_ON_RESOURCE ) {
-                            if ( error.response.data && (error.response.data as DojoBackendResponse<Array<GitlabUser>>).code === DojoStatusCode.MAX_EXERCISE_PER_ASSIGNMENT_REACHED ) {
-                                spinner.fail(`The following users have reached the maximum number of exercise of this assignment : ${ ((error.response.data as DojoBackendResponse<Array<GitlabUser>>).data as Array<GitlabUser>).map(user => user.name).join(', ') }.`);
-                            } else {
-                                spinner.fail(`You've already reached the max number of exercise of this assignment.`);
-                            }
-                        } else {
-                            if ( (error.response.data as DojoBackendResponse<unknown>).code === DojoStatusCode.EXERCISE_CREATION_GITLAB_ERROR ) {
-                                spinner.fail(`Exercise creation error: An unknown error occurred while creating the exercise on Gitlab. Please try again later or contact an administrator.`);
-                            } else {
-                                spinner.fail(`Exercise creation error: An unknown error occurred while creating the exercise on Dojo server. Please try again later or contact an administrator.`);
-                            }
-                        }
-                    }
-                } else {
-                    spinner.fail(`Exercise creation error: unknown error`);
-                }
-            }
+            this.handleApiError(error, spinner, verbose, `Exercise creation error: unknown error`);
 
             throw error;
         }
@@ -214,23 +206,7 @@ class DojoBackendManager {
 
             return true;
         } catch ( error ) {
-            if ( verbose ) {
-                if ( error instanceof AxiosError ) {
-                    if ( error.response?.data ) {
-                        if ( error.response.data.code === DojoStatusCode.ASSIGNMENT_EXERCISE_NOT_RELATED ) {
-                            spinner.fail(`The exercise does not belong to the assignment.`);
-                        } else if ( error.response.data.code === DojoStatusCode.EXERCISE_CORRECTION_ALREADY_EXIST ) {
-                            spinner.fail(`This exercise is already labelled as a correction. If you want to update it, please use the update command.`);
-                        } else if ( error.response.data.code === DojoStatusCode.EXERCISE_CORRECTION_NOT_EXIST ) {
-                            spinner.fail(`The exercise is not labelled as a correction so it's not possible to update it.`);
-                        }
-                    } else {
-                        spinner.fail(`Correction ${ isUpdate ? 'update' : 'link' } error: ${ error.response?.statusText }`);
-                    }
-                } else {
-                    spinner.fail(`Correction ${ isUpdate ? 'update' : 'link' } error: ${ error }`);
-                }
-            }
+            this.handleApiError(error, spinner, verbose, `Correction ${ isUpdate ? 'update' : 'link' } error: ${ error }`);
 
             return false;
         }
diff --git a/NodeApp/src/managers/GitlabManager.ts b/NodeApp/src/managers/GitlabManager.ts
index 356d9e26d61f1e55a591be9e6c811271c9ef8323..f8ee3f16dd36dfc68418dd6eab1baf2384fc64ad 100644
--- a/NodeApp/src/managers/GitlabManager.ts
+++ b/NodeApp/src/managers/GitlabManager.ts
@@ -63,7 +63,7 @@ class GitlabManager {
 
             try {
                 const oldSettings = notificationSettings;
-                const newSettings = { level: someLevelTypes[someLevelTypes[0] == oldSettings.level ? 1 : 0] };
+                const newSettings = { level: someLevelTypes[someLevelTypes[0] === oldSettings.level ? 1 : 0] };
 
                 await this.setNotificationSettings(newSettings);
                 await this.setNotificationSettings(oldSettings);
@@ -111,11 +111,14 @@ class GitlabManager {
                     if ( verbose ) {
                         spinner.succeed(`${ gitlabUser.username } (${ gitlabUser.id })`);
                     }
+
                     return gitlabUser;
                 } else {
                     if ( verbose ) {
                         spinner.fail(`${ param }`);
                     }
+
+                    return undefined;
                 }
             }));
         } catch ( e ) {
@@ -124,15 +127,15 @@ class GitlabManager {
     }
 
     public async getUsersById(ids: Array<number>, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<GitlabUser | undefined>> {
-        return await this.getGitlabUsers(ids, 'id', verbose, verboseIndent);
+        return this.getGitlabUsers(ids, 'id', verbose, verboseIndent);
     }
 
     public async getUsersByUsername(usernames: Array<string>, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<GitlabUser | undefined>> {
-        return await this.getGitlabUsers(usernames, 'search', verbose, verboseIndent);
+        return this.getGitlabUsers(usernames, 'search', verbose, verboseIndent);
     }
 
     public async getRepository(repoId: number): Promise<GitlabRepository> {
-        return await axios.get(this.getApiUrl(GitlabRoute.REPOSITORY_GET).replace('{{id}}', repoId.toString()));
+        return axios.get(this.getApiUrl(GitlabRoute.REPOSITORY_GET).replace('{{id}}', repoId.toString()));
     }
 
     public async fetchMembers(options: { members_id?: Array<number>, members_username?: Array<string> }): Promise<Array<GitlabUser> | false> {
@@ -143,10 +146,10 @@ class GitlabManager {
         let members: Array<GitlabUser> = [];
 
         async function getMembers<T>(context: unknown, functionName: string, paramsToSearch: Array<T>): Promise<boolean> {
-            const result = await ((context as { [functionName: string]: (arg: Array<T>, verbose: boolean, verboseIndent: number) => Promise<Array<GitlabUser | undefined>> })[functionName])(paramsToSearch, true, 8);
+            const gitlabUsers = await ((context as { [functionName: string]: (arg: Array<T>, verbose: boolean, verboseIndent: number) => Promise<Array<GitlabUser | undefined>> })[functionName])(paramsToSearch, true, 8);
 
-            if ( result.every(user => user) ) {
-                members = members.concat(result as Array<GitlabUser>);
+            if ( gitlabUsers.every(user => user) ) {
+                members = members.concat(gitlabUsers as Array<GitlabUser>);
                 return true;
             } else {
                 return false;
@@ -205,8 +208,8 @@ class GitlabManager {
                     shell: true
                 });
 
-                gitClone.on('exit', (code) => {
-                    code !== null && code == 0 ? resolve() : reject();
+                gitClone.on('exit', code => {
+                    code !== null && code === 0 ? resolve() : reject();
                 });
             });
 
diff --git a/NodeApp/src/managers/HttpManager.ts b/NodeApp/src/managers/HttpManager.ts
index 6c75099ca1b1d5bc86f33336502b7d29cad4be93..38462759e0596492c2231f2ad291810e2af8a1e1 100644
--- a/NodeApp/src/managers/HttpManager.ts
+++ b/NodeApp/src/managers/HttpManager.ts
@@ -1,15 +1,15 @@
-import axios, { AxiosError, AxiosRequestHeaders } from 'axios';
-import SessionManager                             from './SessionManager';
-import FormData                                   from 'form-data';
-import { StatusCodes }                            from 'http-status-codes';
-import ClientsSharedConfig                        from '../sharedByClients/config/ClientsSharedConfig';
-import { version }                                from '../config/Version';
-import DojoBackendResponse                        from '../shared/types/Dojo/DojoBackendResponse';
-import DojoStatusCode                             from '../shared/types/Dojo/DojoStatusCode';
-import boxen                                      from 'boxen';
-import Config                                     from '../config/Config';
-import SharedConfig                               from '../shared/config/SharedConfig';
-import { stateConfigFile }                        from '../config/ConfigFiles';
+import axios, { AxiosError, AxiosRequestConfig, AxiosRequestHeaders } from 'axios';
+import SessionManager                                                 from './SessionManager';
+import FormData                                                       from 'form-data';
+import { StatusCodes }                                                from 'http-status-codes';
+import ClientsSharedConfig                                            from '../sharedByClients/config/ClientsSharedConfig';
+import { version }                                                    from '../config/Version';
+import DojoBackendResponse                                            from '../shared/types/Dojo/DojoBackendResponse';
+import DojoStatusCode                                                 from '../shared/types/Dojo/DojoStatusCode';
+import boxen                                                          from 'boxen';
+import Config                                                         from '../config/Config';
+import SharedConfig                                                   from '../shared/config/SharedConfig';
+import { stateConfigFile }                                            from '../config/ConfigFiles';
 
 
 class HttpManager {
@@ -21,10 +21,10 @@ class HttpManager {
     }
 
     private registerRequestInterceptor() {
-        axios.interceptors.request.use((config) => {
+        axios.interceptors.request.use(config => {
 
             if ( config.data instanceof FormData ) {
-                config.headers = { ...config.headers, ...(config.data as FormData).getHeaders() } as AxiosRequestHeaders;
+                config.headers = { ...config.headers, ...config.data.getHeaders() } as AxiosRequestHeaders;
             }
 
             if ( config.url && (config.url.indexOf(ClientsSharedConfig.apiURL) !== -1) ) {
@@ -63,8 +63,66 @@ class HttpManager {
         process.exit(1);
     }
 
+    private apiMethodNotAllowed(error: AxiosError, isFromApi: boolean) {
+        if ( error.response?.status === StatusCodes.METHOD_NOT_ALLOWED && isFromApi && error.response.data ) {
+            const data: DojoBackendResponse<void> = error.response.data as DojoBackendResponse<void>;
+
+            switch ( data.code ) {
+                case DojoStatusCode.CLIENT_NOT_SUPPORTED:
+                    this.requestError('Client not recognized by the server. Please contact the administrator.');
+                    break;
+                case DojoStatusCode.CLIENT_VERSION_NOT_SUPPORTED:
+                    this.requestError(`CLI version not anymore supported by the server. Please update the CLI.\nYou can download the latest stable version on this page:\n${ Config.gitlab.cliReleasePage }`);
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    private apiAuthorizationError(error: AxiosError, isFromApi: boolean) {
+        if ( this.handleAuthorizationCommandErrors && isFromApi && error.response ) {
+            switch ( error.response.status ) {
+                case StatusCodes.UNAUTHORIZED:
+                    this.requestError('Session expired or does not exist. Please login again.');
+                    break;
+                case StatusCodes.FORBIDDEN:
+                    this.requestError('Forbidden access.');
+                    break;
+                default:
+                    this.requestError('Unknown error.');
+                    break;
+            }
+        } else {
+            this.handleAuthorizationCommandErrors = true;
+        }
+    }
+
+    private async gitlabAuthorizationError(error: AxiosError, isFromGitlab: boolean): Promise<Promise<unknown> | undefined> {
+        const originalConfig = Object.assign({}, error.config, { _retry: false });
+
+        // Try to refresh the Gitlab tokens if the request have failed with a 401 error
+        if ( error.response && error.response.status === StatusCodes.UNAUTHORIZED && isFromGitlab && !originalConfig?._retry ) {
+            originalConfig._retry = true;
+
+            try {
+                await SessionManager.refreshTokens();
+
+                return axios(error.config as AxiosRequestConfig);
+            } catch ( subError ) {
+                if ( subError instanceof AxiosError && subError.response && subError.response.data ) {
+                    return Promise.reject(subError.response.data);
+                }
+
+                return Promise.reject(subError);
+            }
+        }
+
+        return undefined;
+    }
+
     private registerResponseInterceptor() {
-        axios.interceptors.response.use((response) => {
+        axios.interceptors.response.use(response => {
             if ( response.data && response.data.sessionToken ) {
                 SessionManager.apiToken = response.data.sessionToken;
             }
@@ -80,61 +138,15 @@ class HttpManager {
             }
 
             return response;
-        }, async (error) => {
+        }, async error => {
             if ( error.response ) {
-                const originalConfig = error.config;
 
                 const isFromApi = error.response.config.url && error.response.config.url.indexOf(ClientsSharedConfig.apiURL) !== -1;
                 const isFromGitlab = error.response.config.url && error.response.config.url.indexOf(SharedConfig.gitlab.URL) !== -1;
 
-                // Try to refresh the Gitlab tokens if the request have failed with a 401 error
-                if ( error.response.status === StatusCodes.UNAUTHORIZED && isFromGitlab && !originalConfig._retry ) {
-                    originalConfig._retry = true;
-
-                    try {
-                        await SessionManager.refreshTokens();
-
-                        return axios(originalConfig);
-                    } catch ( error: unknown ) {
-                        if ( error instanceof AxiosError ) {
-                            if ( error.response && error.response.data ) {
-                                return Promise.reject(error.response.data);
-                            }
-                        }
-
-                        return Promise.reject(error);
-                    }
-                }
-
-                if ( error.response.status === StatusCodes.METHOD_NOT_ALLOWED && isFromApi && error.response.data ) {
-                    const data: DojoBackendResponse<void> = error.response.data;
-
-                    switch ( data.code ) {
-                        case DojoStatusCode.CLIENT_NOT_SUPPORTED:
-                            this.requestError('Client not recognized by the server. Please contact the administrator.');
-                            break;
-                        case DojoStatusCode.CLIENT_VERSION_NOT_SUPPORTED:
-                            this.requestError(`CLI version not anymore supported by the server. Please update the CLI.\nYou can download the latest stable version on this page:\n${ Config.gitlab.cliReleasePage }`);
-                            break;
-                        default:
-                            break;
-                    }
-                }
-
-                if ( this.handleAuthorizationCommandErrors ) {
-                    if ( isFromApi ) {
-                        switch ( error.response.status ) {
-                            case StatusCodes.UNAUTHORIZED:
-                                this.requestError('Session expired or does not exist. Please login again.');
-                                break;
-                            case StatusCodes.FORBIDDEN:
-                                this.requestError('Forbidden access.');
-                                break;
-                        }
-                    }
-                } else {
-                    this.handleAuthorizationCommandErrors = true;
-                }
+                await this.gitlabAuthorizationError(error, isFromGitlab);
+                this.apiMethodNotAllowed(error, isFromApi);
+                this.apiAuthorizationError(error, isFromApi);
             } else {
                 this.requestError('Error connecting to the server. Please check your internet connection. If the problem persists, please contact the administrator.');
             }
diff --git a/NodeApp/src/managers/SessionManager.ts b/NodeApp/src/managers/SessionManager.ts
index d06fef9e44ec22ec7e89d045f066995b67606fad..8b1ed3299e3ec76f86873389938776d43cb34797 100644
--- a/NodeApp/src/managers/SessionManager.ts
+++ b/NodeApp/src/managers/SessionManager.ts
@@ -25,7 +25,7 @@ import TextStyle                 from '../types/TextStyle';
 
 class LoginServer {
     readonly events: EventEmitter = new EventEmitter();
-    private server: http.Server;
+    private readonly server: http.Server;
 
     constructor() {
         this.server = http.createServer((req, res) => {
@@ -48,10 +48,7 @@ class LoginServer {
                 }
 
                 sendError(`Incorrect call => ${ req.url }`);
-                return;
             }
-
-            //sendError(`Unknown route call => ${ req.url }`);
         });
     }
 
@@ -120,14 +117,12 @@ class SessionManager {
         sessionConfigFile.setParam(LocalConfigKeys.GITLAB, credentials);
     }
 
-    constructor() { }
-
     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(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): ${ TextStyle.URL(`${ 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',
@@ -220,7 +215,7 @@ class SessionManager {
             throw error;
         }
 
-        const isGitlabTokensValid = (await GitlabManager.testToken()).every((value) => value);
+        const isGitlabTokensValid = (await GitlabManager.testToken()).every(value => value);
         if ( !isGitlabTokensValid ) {
             throw new Error('Gitlab tokens are invalid');
         }
@@ -259,25 +254,25 @@ class SessionManager {
         };
     }
 
-    checkPermissions(verbose: boolean = true, indent: number = 8, checkPermissions: Array<string> | null = []): Permissions {
-        const hasPermission = (permissionPredicate: () => boolean, verboseText: string): boolean => {
-            const isAllowed: boolean = this.profile !== undefined && permissionPredicate();
+    private hasPermission(permissionPredicate: () => boolean, verbose: boolean, verboseText: string, indent: number): boolean {
+        const isAllowed: boolean = this.profile !== undefined && permissionPredicate();
 
-            if ( verbose ) {
-                const spinner: ora.Ora = ora({
-                                                 text  : verboseText,
-                                                 indent: indent
-                                             }).start();
-                isAllowed ? spinner.succeed() : spinner.fail();
-            }
+        if ( verbose ) {
+            const spinner: ora.Ora = ora({
+                                             text  : verboseText,
+                                             indent: indent
+                                         }).start();
+            isAllowed ? spinner.succeed() : spinner.fail();
+        }
 
-            return isAllowed;
-        };
+        return isAllowed;
+    }
 
+    checkPermissions(verbose: boolean = true, indent: number = 8, checkPermissions: Array<string> | null = []): Permissions {
         return {
-            student      : checkPermissions && (checkPermissions.length == 0 || checkPermissions.includes('student')) ? hasPermission(() => true, 'Student permissions') : false,
-            teachingStaff: checkPermissions && (checkPermissions.length == 0 || checkPermissions.includes('teachingStaff')) ? hasPermission(() => this.profile?.isTeachingStaff ?? false, 'Teaching staff permissions') : false,
-            admin        : checkPermissions && (checkPermissions.length == 0 || checkPermissions.includes('admin')) ? hasPermission(() => this.profile?.isAdmin ?? false, 'Admin permissions') : false
+            student      : checkPermissions && (checkPermissions.length === 0 || checkPermissions.includes('student')) ? this.hasPermission(() => true, verbose, 'Student permissions', indent) : false,
+            teachingStaff: checkPermissions && (checkPermissions.length === 0 || checkPermissions.includes('teachingStaff')) ? this.hasPermission(() => this.profile?.isTeachingStaff ?? false, verbose, 'Teaching staff permissions', indent) : false,
+            admin        : checkPermissions && (checkPermissions.length === 0 || checkPermissions.includes('admin')) ? this.hasPermission(() => this.profile?.isAdmin ?? false, verbose, 'Admin permissions', indent) : false
         };
     }
 
diff --git a/NodeApp/src/shared b/NodeApp/src/shared
index 6214acbd799d9eed3f5b6840858f8d5ecda82c86..fca59c4d155603b53d48a30401aabab82d91fc59 160000
--- a/NodeApp/src/shared
+++ b/NodeApp/src/shared
@@ -1 +1 @@
-Subproject commit 6214acbd799d9eed3f5b6840858f8d5ecda82c86
+Subproject commit fca59c4d155603b53d48a30401aabab82d91fc59
diff --git a/NodeApp/src/sharedByClients b/NodeApp/src/sharedByClients
index 4efff1c5127c6f84104016d7041d0cf281d981f8..2b2b2376b0389a39283327bba5bfaf7d1ce136ac 160000
--- a/NodeApp/src/sharedByClients
+++ b/NodeApp/src/sharedByClients
@@ -1 +1 @@
-Subproject commit 4efff1c5127c6f84104016d7041d0cf281d981f8
+Subproject commit 2b2b2376b0389a39283327bba5bfaf7d1ce136ac