diff --git a/NodeApp/src/commander/assignment/AssignmentCommand.ts b/NodeApp/src/commander/assignment/AssignmentCommand.ts
index 70f09e2ff18e17774067e71a3d6e773dbf201f18..90ae6167deaece3339766cc3f8a7b090263d2e79 100644
--- a/NodeApp/src/commander/assignment/AssignmentCommand.ts
+++ b/NodeApp/src/commander/assignment/AssignmentCommand.ts
@@ -3,6 +3,7 @@ import AssignmentCreateCommand    from './subcommands/AssignmentCreateCommand';
 import AssignmentPublishCommand   from './subcommands/AssignmentPublishCommand';
 import AssignmentUnpublishCommand from './subcommands/AssignmentUnpublishCommand';
 import AssignmentCheckCommand     from './subcommands/AssignmentCheckCommand';
+import AssignmentRunCommand       from './subcommands/AssignmentRunCommand';
 
 
 class AssignmentCommand extends CommanderCommand {
@@ -16,6 +17,7 @@ class AssignmentCommand extends CommanderCommand {
     protected defineSubCommands() {
         AssignmentCreateCommand.registerOnCommand(this.command);
         AssignmentCheckCommand.registerOnCommand(this.command);
+        AssignmentRunCommand.registerOnCommand(this.command);
         AssignmentPublishCommand.registerOnCommand(this.command);
         AssignmentUnpublishCommand.registerOnCommand(this.command);
     }
diff --git a/NodeApp/src/commander/assignment/subcommands/AssignmentRunCommand.ts b/NodeApp/src/commander/assignment/subcommands/AssignmentRunCommand.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e33c755cf8416eec60414a582e68bcc436899fd7
--- /dev/null
+++ b/NodeApp/src/commander/assignment/subcommands/AssignmentRunCommand.ts
@@ -0,0 +1,24 @@
+import CommanderCommand  from '../../CommanderCommand';
+import Config            from '../../../config/Config';
+import ExerciseRunHelper from '../../../helpers/Dojo/ExerciseRunHelper';
+
+
+class AssignmentRunCommand extends CommanderCommand {
+    protected commandName: string = 'run';
+
+    protected defineCommand() {
+        // This command is synced with the "exercise run" command
+        this.command
+        .description('locally run the assignment as an exercise')
+        .option('-p, --path <value>', 'exercise path', Config.folders.defaultLocalExercise)
+        .option('-v, --verbose', 'verbose mode (display docker compose logs in live)')
+        .action(this.commandAction.bind(this));
+    }
+
+    protected async commandAction(options: { path: string, verbose: boolean }): Promise<void> {
+        await ExerciseRunHelper.run(options);
+    }
+}
+
+
+export default new AssignmentRunCommand();
\ No newline at end of file
diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseRunCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseRunCommand.ts
index cc139fe0a399909a51bee3a976d2d8debf2d5fce..90282b7955b1dccfdb20925e4f20fd8b3337c9a4 100644
--- a/NodeApp/src/commander/exercise/subcommands/ExerciseRunCommand.ts
+++ b/NodeApp/src/commander/exercise/subcommands/ExerciseRunCommand.ts
@@ -1,38 +1,13 @@
-import CommanderCommand                     from '../../CommanderCommand';
-import Config                               from '../../../config/Config';
-import fs                                   from 'node:fs';
-import ora                                  from 'ora';
-import util                                 from 'util';
-import { exec }                             from 'child_process';
-import chalk                                from 'chalk';
-import * as os                              from 'os';
-import path                                 from 'path';
-import ClientsSharedConfig                  from '../../../sharedByClients/config/ClientsSharedConfig';
-import AssignmentFile                       from '../../../shared/types/Dojo/AssignmentFile';
-import ExerciseDockerCompose                from '../../../sharedByClients/helpers/Dojo/ExerciseDockerCompose';
-import SharedAssignmentHelper               from '../../../shared/helpers/Dojo/SharedAssignmentHelper';
-import ExerciseCheckerError                 from '../../../shared/types/Dojo/ExerciseCheckerError';
-import ClientsSharedExerciseHelper          from '../../../sharedByClients/helpers/Dojo/ClientsSharedExerciseHelper';
-import ExerciseResultsSanitizerAndValidator from '../../../sharedByClients/helpers/Dojo/ExerciseResultsSanitizerAndValidator';
-
-
-const execAsync = util.promisify(exec);
+import CommanderCommand  from '../../CommanderCommand';
+import Config            from '../../../config/Config';
+import ExerciseRunHelper from '../../../helpers/Dojo/ExerciseRunHelper';
 
 
 class ExerciseRunCommand extends CommanderCommand {
     protected commandName: string = 'run';
 
-    private readonly dateISOString: string = (new Date()).toISOString().replace(/:/g, '_').replace(/\./g, '_');
-
-    private readonly folderResultsVolume: string = path.join(os.homedir(), 'DojoExecutions', `dojo_execLogs_${ this.dateISOString }`);
-    private readonly folderResultsDojo: string = path.join(this.folderResultsVolume, `Dojo/`);
-    private readonly folderResultsExercise: string = path.join(this.folderResultsVolume, `Exercise/`);
-
-    private readonly projectName: string = `${ ClientsSharedConfig.dockerCompose.projectName }_${ this.dateISOString.toLowerCase() }`;
-
-    private readonly fileComposeLogs: string = path.join(this.folderResultsDojo, `dockerComposeLogs.txt`);
-
     protected defineCommand() {
+        // This command is synced with the "assignment run" command
         this.command
         .description('locally run an exercise')
         .option('-p, --path <value>', 'exercise path', Config.folders.defaultLocalExercise)
@@ -40,222 +15,8 @@ class ExerciseRunCommand extends CommanderCommand {
         .action(this.commandAction.bind(this));
     }
 
-    private displayExecutionLogs() {
-        ora({
-                text  : `${ chalk.magenta('Execution logs folder:') } ${ this.folderResultsVolume }`,
-                indent: 0
-            }).start().info();
-    }
-
     protected async commandAction(options: { path: string, verbose: boolean }): Promise<void> {
-        const localExercisePath: string = options.path ?? Config.folders.defaultLocalExercise;
-
-        let assignmentFile: AssignmentFile;
-        let exerciseDockerCompose: ExerciseDockerCompose;
-        let exerciseResultsValidation: ExerciseResultsSanitizerAndValidator;
-
-        let haveResultsVolume: boolean;
-
-        // Step 1: Check requirements (if it's an exercise folder and if Docker daemon is running)
-        {
-            console.log(chalk.cyan('Please wait while we are checking and creating dependencies...'));
-
-            // Create result temp folder
-            fs.mkdirSync(this.folderResultsVolume, { recursive: true });
-            fs.mkdirSync(this.folderResultsDojo, { recursive: true });
-            fs.mkdirSync(this.folderResultsExercise, { recursive: true });
-
-
-            ora({
-                    text  : `Checking exercise content:`,
-                    indent: 4
-                }).start().info();
-
-            // Exercise folder
-            {
-                const spinner: ora.Ora = ora({
-                                                 text  : `Checking exercise folder`,
-                                                 indent: 8
-                                             }).start();
-
-                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]);
-
-                if ( missingFiles.length > 0 ) {
-                    spinner.fail(`The exercise folder is missing the following files: ${ missingFiles.map((file: [ string, boolean ]) => file[0]).join(', ') }`);
-                    return;
-                }
-
-                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(options.path, ClientsSharedConfig.assignment.filename));
-                if ( !validationResults.isValid ) {
-                    spinner.fail(`The ${ ClientsSharedConfig.assignment.filename } file is invalid: ${ JSON.stringify(validationResults.errors) }`);
-                    return;
-                } else {
-                    assignmentFile = validationResults.results!;
-                }
-
-                haveResultsVolume = assignmentFile.result.volume !== undefined;
-
-                spinner.succeed(`The ${ ClientsSharedConfig.assignment.filename } file is valid`);
-            }
-
-            // Docker daemon
-            {
-                const spinner: ora.Ora = ora({
-                                                 text  : `Checking Docker daemon`,
-                                                 indent: 4
-                                             }).start();
-
-                try {
-                    await execAsync(`cd "${ Config.folders.defaultLocalExercise }";docker ps`);
-                } catch ( error ) {
-                    spinner.fail(`The Docker daemon is not running`);
-                    return;
-                }
-
-                spinner.succeed(`The Docker daemon is running`);
-            }
-        }
-
-
-        // Step 2: Run docker-compose file
-        {
-            console.log(chalk.cyan('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);
-
-                composeFileOverride = [ composeOverridePath ];
-            }
-
-            exerciseDockerCompose = new ExerciseDockerCompose(this.projectName, assignmentFile, localExercisePath, composeFileOverride);
-
-            try {
-                await new Promise<void>((resolve, reject) => {
-                    let spinner: ora.Ora;
-
-                    if ( options.verbose ) {
-                        exerciseDockerCompose.events.on('logs', (log: string, _error: boolean, displayable: boolean) => {
-                            if ( displayable ) {
-                                console.log(log);
-                            }
-                        });
-                    }
-
-                    exerciseDockerCompose.events.on('step', (name: string, message: string) => {
-                        spinner = ora({
-                                          text  : message,
-                                          indent: 4
-                                      }).start();
-
-                        if ( options.verbose && name == 'COMPOSE_RUN' ) {
-                            spinner.info();
-                        }
-                    });
-
-                    exerciseDockerCompose.events.on('endStep', (stepName: string, message: string, error: boolean) => {
-                        if ( error ) {
-                            if ( options.verbose && stepName == 'COMPOSE_RUN' ) {
-                                ora({
-                                        text  : message,
-                                        indent: 4
-                                    }).start().fail();
-                            } else {
-                                spinner.fail(message);
-                            }
-                        } else {
-                            if ( options.verbose && stepName == 'COMPOSE_RUN' ) {
-                                ora({
-                                        text  : message,
-                                        indent: 4
-                                    }).start().succeed();
-                            } else {
-                                spinner.succeed(message);
-                            }
-                        }
-                    });
-
-                    exerciseDockerCompose.events.on('finished', (success: boolean) => {
-                        success ? resolve() : reject();
-                    });
-
-                    exerciseDockerCompose.run(true);
-                });
-            } catch ( error ) { /* empty */ }
-
-            fs.rmSync(composeOverridePath, { force: true });
-            fs.writeFileSync(this.fileComposeLogs, exerciseDockerCompose.allLogs);
-
-            if ( !exerciseDockerCompose.success ) {
-                this.displayExecutionLogs();
-                return;
-            }
-        }
-
-
-        // Step 3: Get results
-        {
-            console.log(chalk.cyan('Please wait while we are checking the results...'));
-
-            exerciseResultsValidation = new ExerciseResultsSanitizerAndValidator(this.folderResultsDojo, this.folderResultsExercise, exerciseDockerCompose.exitCode);
-
-            try {
-                await new Promise<void>((resolve, reject) => {
-                    let spinner: ora.Ora;
-
-                    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);
-                            }
-                        } else {
-                            spinner.succeed(message);
-                        }
-                    });
-
-                    exerciseResultsValidation.events.on('finished', (success: boolean, exitCode: number) => {
-                        success || exitCode == ExerciseCheckerError.EXERCISE_RESULTS_FOLDER_TOO_BIG ? resolve() : reject();
-                    });
-
-                    exerciseResultsValidation.run();
-                });
-            } catch ( error ) {
-                this.displayExecutionLogs();
-                return;
-            }
-        }
-
-
-        // Step 4: Display results + Volume location
-        {
-            ClientsSharedExerciseHelper.displayExecutionResults(exerciseResultsValidation.exerciseResults!, exerciseDockerCompose.exitCode, {
-                INFO   : chalk.bold,
-                SUCCESS: chalk.green,
-                FAILURE: chalk.red
-            }, `\n\n${ chalk.bold('Execution results folder') } : ${ this.folderResultsVolume }`);
-        }
+        await ExerciseRunHelper.run(options);
     }
 }
 
diff --git a/NodeApp/src/helpers/Dojo/ExerciseRunHelper.ts b/NodeApp/src/helpers/Dojo/ExerciseRunHelper.ts
new file mode 100644
index 0000000000000000000000000000000000000000..82c7651d57ea69245e024ba78c21656553a765c0
--- /dev/null
+++ b/NodeApp/src/helpers/Dojo/ExerciseRunHelper.ts
@@ -0,0 +1,252 @@
+import ora                                  from 'ora';
+import chalk                                from 'chalk';
+import Config                               from '../../config/Config';
+import AssignmentFile                       from '../../shared/types/Dojo/AssignmentFile';
+import ExerciseDockerCompose                from '../../sharedByClients/helpers/Dojo/ExerciseDockerCompose';
+import ExerciseResultsSanitizerAndValidator from '../../sharedByClients/helpers/Dojo/ExerciseResultsSanitizerAndValidator';
+import fs                                   from 'node:fs';
+import ClientsSharedConfig                  from '../../sharedByClients/config/ClientsSharedConfig';
+import SharedAssignmentHelper               from '../../shared/helpers/Dojo/SharedAssignmentHelper';
+import path                                 from 'path';
+import ExerciseCheckerError                 from '../../shared/types/Dojo/ExerciseCheckerError';
+import ClientsSharedExerciseHelper          from '../../sharedByClients/helpers/Dojo/ClientsSharedExerciseHelper';
+import os                                   from 'os';
+import util                                 from 'util';
+import { exec }                             from 'child_process';
+
+
+const execAsync = util.promisify(exec);
+
+
+class ExerciseRunHelper {
+    private readonly dateISOString: string = (new Date()).toISOString().replace(/:/g, '_').replace(/\./g, '_');
+
+    private readonly folderResultsVolume: string = path.join(os.homedir(), 'DojoExecutions', `dojo_execLogs_${ this.dateISOString }`);
+    private readonly folderResultsDojo: string = path.join(this.folderResultsVolume, `Dojo/`);
+    private readonly folderResultsExercise: string = path.join(this.folderResultsVolume, `Exercise/`);
+
+    private readonly projectName: string = `${ ClientsSharedConfig.dockerCompose.projectName }_${ this.dateISOString.toLowerCase() }`;
+
+    private readonly fileComposeLogs: string = path.join(this.folderResultsDojo, `dockerComposeLogs.txt`);
+
+    private displayExecutionLogs() {
+        ora({
+                text  : `${ chalk.magenta('Execution logs folder:') } ${ this.folderResultsVolume }`,
+                indent: 0
+            }).start().info();
+    }
+
+    async run(options: { path: string, verbose: boolean }): Promise<void> {
+        const localExercisePath: string = options.path ?? Config.folders.defaultLocalExercise;
+
+        let assignmentFile: AssignmentFile;
+        let exerciseDockerCompose: ExerciseDockerCompose;
+        let exerciseResultsValidation: ExerciseResultsSanitizerAndValidator;
+
+        let haveResultsVolume: boolean;
+
+        // Step 1: Check requirements (if it's an exercise folder and if Docker daemon is running)
+        {
+            console.log(chalk.cyan('Please wait while we are checking and creating dependencies...'));
+
+            // Create result temp folder
+            fs.mkdirSync(this.folderResultsVolume, { recursive: true });
+            fs.mkdirSync(this.folderResultsDojo, { recursive: true });
+            fs.mkdirSync(this.folderResultsExercise, { recursive: true });
+
+
+            ora({
+                    text  : `Checking exercise content:`,
+                    indent: 4
+                }).start().info();
+
+            // Exercise folder
+            {
+                const spinner: ora.Ora = ora({
+                                                 text  : `Checking exercise folder`,
+                                                 indent: 8
+                                             }).start();
+
+                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]);
+
+                if ( missingFiles.length > 0 ) {
+                    spinner.fail(`The exercise folder is missing the following files: ${ missingFiles.map((file: [ string, boolean ]) => file[0]).join(', ') }`);
+                    return;
+                }
+
+                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(options.path, ClientsSharedConfig.assignment.filename));
+                if ( !validationResults.isValid ) {
+                    spinner.fail(`The ${ ClientsSharedConfig.assignment.filename } file is invalid: ${ JSON.stringify(validationResults.errors) }`);
+                    return;
+                } else {
+                    assignmentFile = validationResults.results!;
+                }
+
+                haveResultsVolume = assignmentFile.result.volume !== undefined;
+
+                spinner.succeed(`The ${ ClientsSharedConfig.assignment.filename } file is valid`);
+            }
+
+            // Docker daemon
+            {
+                const spinner: ora.Ora = ora({
+                                                 text  : `Checking Docker daemon`,
+                                                 indent: 4
+                                             }).start();
+
+                try {
+                    await execAsync(`cd "${ Config.folders.defaultLocalExercise }";docker ps`);
+                } catch ( error ) {
+                    spinner.fail(`The Docker daemon is not running`);
+                    return;
+                }
+
+                spinner.succeed(`The Docker daemon is running`);
+            }
+        }
+
+
+        // Step 2: Run docker-compose file
+        {
+            console.log(chalk.cyan('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);
+
+                composeFileOverride = [ composeOverridePath ];
+            }
+
+            exerciseDockerCompose = new ExerciseDockerCompose(this.projectName, assignmentFile, localExercisePath, composeFileOverride);
+
+            try {
+                await new Promise<void>((resolve, reject) => {
+                    let spinner: ora.Ora;
+
+                    if ( options.verbose ) {
+                        exerciseDockerCompose.events.on('logs', (log: string, _error: boolean, displayable: boolean) => {
+                            if ( displayable ) {
+                                console.log(log);
+                            }
+                        });
+                    }
+
+                    exerciseDockerCompose.events.on('step', (name: string, message: string) => {
+                        spinner = ora({
+                                          text  : message,
+                                          indent: 4
+                                      }).start();
+
+                        if ( options.verbose && name == 'COMPOSE_RUN' ) {
+                            spinner.info();
+                        }
+                    });
+
+                    exerciseDockerCompose.events.on('endStep', (stepName: string, message: string, error: boolean) => {
+                        if ( error ) {
+                            if ( options.verbose && stepName == 'COMPOSE_RUN' ) {
+                                ora({
+                                        text  : message,
+                                        indent: 4
+                                    }).start().fail();
+                            } else {
+                                spinner.fail(message);
+                            }
+                        } else {
+                            if ( options.verbose && stepName == 'COMPOSE_RUN' ) {
+                                ora({
+                                        text  : message,
+                                        indent: 4
+                                    }).start().succeed();
+                            } else {
+                                spinner.succeed(message);
+                            }
+                        }
+                    });
+
+                    exerciseDockerCompose.events.on('finished', (success: boolean) => {
+                        success ? resolve() : reject();
+                    });
+
+                    exerciseDockerCompose.run(true);
+                });
+            } catch ( error ) { /* empty */ }
+
+            fs.rmSync(composeOverridePath, { force: true });
+            fs.writeFileSync(this.fileComposeLogs, exerciseDockerCompose.allLogs);
+
+            if ( !exerciseDockerCompose.success ) {
+                this.displayExecutionLogs();
+                return;
+            }
+        }
+
+
+        // Step 3: Get results
+        {
+            console.log(chalk.cyan('Please wait while we are checking the results...'));
+
+            exerciseResultsValidation = new ExerciseResultsSanitizerAndValidator(this.folderResultsDojo, this.folderResultsExercise, exerciseDockerCompose.exitCode);
+
+            try {
+                await new Promise<void>((resolve, reject) => {
+                    let spinner: ora.Ora;
+
+                    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);
+                            }
+                        } else {
+                            spinner.succeed(message);
+                        }
+                    });
+
+                    exerciseResultsValidation.events.on('finished', (success: boolean, exitCode: number) => {
+                        success || exitCode == ExerciseCheckerError.EXERCISE_RESULTS_FOLDER_TOO_BIG ? resolve() : reject();
+                    });
+
+                    exerciseResultsValidation.run();
+                });
+            } catch ( error ) {
+                this.displayExecutionLogs();
+                return;
+            }
+        }
+
+
+        // Step 4: Display results + Volume location
+        {
+            ClientsSharedExerciseHelper.displayExecutionResults(exerciseResultsValidation.exerciseResults!, exerciseDockerCompose.exitCode, {
+                INFO   : chalk.bold,
+                SUCCESS: chalk.green,
+                FAILURE: chalk.red
+            }, `\n\n${ chalk.bold('Execution results folder') } : ${ this.folderResultsVolume }`);
+        }
+    }
+}
+
+
+export default new ExerciseRunHelper();
\ No newline at end of file