diff --git a/CHANGELOG.md b/CHANGELOG.md
index da9d85a63776ef536cc6549f76c3551db2cda4bb..e685b34a4e788bb34634a5dd33f4f40217ef9aff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,7 +18,16 @@
 -->
 
 
-## 3.4.0 (???)
+## 3.4.2 (2024-01-23)
+
+### πŸ› Bugfix
+- Fix description of the unpublish command in the CLI help
+
+### πŸ“š Documentation
+- Wiki: add tutorial about adding a new command to the CLI
+
+
+## 3.4.1 (2024-01-23)
 
 ### ✨ Feature
 - Limit of 2 exercises by user
diff --git a/NodeApp/package-lock.json b/NodeApp/package-lock.json
index 5fa193f14e3e973eddca8cf7af8fcac0e7b1c0ea..e65258129e6b3233b33eea6eb42fb84f12d39f5e 100644
--- a/NodeApp/package-lock.json
+++ b/NodeApp/package-lock.json
@@ -1,12 +1,12 @@
 {
     "name": "dojo_cli",
-    "version": "3.4.1",
+    "version": "3.4.2",
     "lockfileVersion": 3,
     "requires": true,
     "packages": {
         "": {
             "name": "dojo_cli",
-            "version": "3.4.1",
+            "version": "3.4.2",
             "license": "AGPLv3",
             "dependencies": {
                 "appdata-path": "^1.0.0",
diff --git a/NodeApp/package.json b/NodeApp/package.json
index fe5466e2f44a52dcf09f8bd37b9ee203d6b3cb1f..3ae9cf17b1c7c2932e3ed62e9ead6f9a7e6f0339 100644
--- a/NodeApp/package.json
+++ b/NodeApp/package.json
@@ -1,7 +1,7 @@
 {
     "name"           : "dojo_cli",
     "description"    : "CLI of the Dojo project",
-    "version"        : "3.4.1",
+    "version"        : "3.4.2",
     "license"        : "AGPLv3",
     "author"         : "MichaΓ«l Minelli <dojo@minelli.me>",
     "main"           : "dist/app.js",
diff --git a/NodeApp/src/commander/assignment/subcommands/AssignmentPublishUnpublishCommandBase.ts b/NodeApp/src/commander/assignment/subcommands/AssignmentPublishUnpublishCommandBase.ts
index acc361c3ffd9e01d2adf65a55bc187c7180720c5..3c7c1d86e5c365d9b12e23d28487f8f5a3edcf67 100644
--- a/NodeApp/src/commander/assignment/subcommands/AssignmentPublishUnpublishCommandBase.ts
+++ b/NodeApp/src/commander/assignment/subcommands/AssignmentPublishUnpublishCommandBase.ts
@@ -13,7 +13,7 @@ abstract class AssignmentPublishUnpublishCommandBase extends CommanderCommand {
 
     protected defineCommand() {
         this.command
-        .description('publish an assignment')
+        .description(`${ this.publish ? 'publish' : 'unpublish' } an assignment`)
         .argument('<name or url>', 'name or url (http/s or ssh) of the assignment')
         .option('-f, --force', 'don\'t ask for confirmation')
         .action(this.commandAction.bind(this));
diff --git a/Wiki/Development/1-How-to-setup-your-development-environment.md b/Wiki/Development/1-How-to-setup-your-development-environment.md
new file mode 100644
index 0000000000000000000000000000000000000000..47a876164e87e7f6a17827ed854a0c1484f5e40a
--- /dev/null
+++ b/Wiki/Development/1-How-to-setup-your-development-environment.md
@@ -0,0 +1,94 @@
+# How to setup your development environment
+
+## Introduction
+
+This tutorial describes how to setup your development environment for building the Dojo CLI by detailing the prerequisites and dependencies needed.
+
+## Technologies
+
+The cli is built using [NodeJS](https://nodejs.org/en/) and [NPM](https://www.npmjs.com/).
+
+The programming language used is [Typescript](https://www.typescriptlang.org/) v5.
+
+
+## Prerequisites
+
+In order to build the cli you will need the following tools:
+- [NodeJS](https://nodejs.org/en/) (version 18 or higher)
+- [NPM](https://www.npmjs.com/) (version 10 or higher)
+
+Install NodeJS and NPM by following the instructions on the [official website](https://nodejs.org/en/download/package-manager).
+Or via Node Version Manager (NVM) by following the instructions on the [official website](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
+
+
+## Dependencies
+
+The CLI is packaged using [pkg](https://www.npmjs.com/package/pkg). 
+This means that all the dependencies are bundled in the final binary.
+
+Here are the main dependencies used by the cli (you don't need to install them manually or globally on your system):
+- [Axios](https://www.npmjs.com/package/axios): a promise-based HTTP client for the browser and Node.js. It is
+used to make HTTP(S) requests to the Dojo backend and Gitlab.
+- [Boxen](https://www.npmjs.com/package/boxen): used to display messages in a box in the terminal.
+- [Chalk](https://www.npmjs.com/package/chalk): used to add colors to the messages in the terminal.
+- [Commander.js](https://www.npmjs.com/package/commander): a library to write command-line interfaces.
+- [Dotenv](https://www.npmjs.com/package/dotenv): used to load environment variables from a .env file.
+- [Dotenv-vault](https://www.npmjs.com/package/dotenv-vault): a CLI to sync .env files across machines, 
+environments, and team members.
+- [Inquirer](https://www.npmjs.com/package/inquirer): used to ask perform to the interactive command line user interfaces.
+- [JsonWebToken](https://www.npmjs.com/package/jsonwebtoken): used to generate and validate [JSON Web Tokens](https://jwt.io/).
+- [ora](https://www.npmjs.com/package/ora): used to display elegantly in the terminal.
+- [zod](https://www.npmjs.com/package/zod): a TypeScript-first schema validation with static type inference. Used
+in the projet to validate json files created by the user.
+
+
+## Installation
+
+First of all, you need to clone the repository:
+```bash
+$ git clone --recurse-submodule ssh://git@ssh.hesge.ch:10572/dojo_project/projects/ui/dojocli.git
+```
+
+Then, you need to move to the project's directory:
+```bash
+$ cd NodeApp
+```
+
+To install the dependencies listed above you can use the following command in the base directory of the project:
+```bash
+$ npm install
+```
+
+
+## Environment variables
+
+Environment variables are used to store sensitive information such as API keys, passwords, etc.
+They are also used to store configuration information that can be changed without modifying the code.
+
+You can decrypt env var stored in the `.env.vault` file with the following commands in the project's main folder:
+```bash
+> npx dotenv-vault local keys
+ environment DOTENV_KEY
+ ─────────── ─────────────────────────────────────────────────────────────────────────
+ development dotenv://:key_1234@dotenv.local/vault/.env.vault?environment=development
+
+Set DOTENV_KEY on your server
+
+> npx dotenv-vault local decrypt dotenv://:key_1234@dotenv.local/vault/.env.vault?environment=development > .env.development
+```
+
+**The `.env.keys` file have to be requested to the project maintainer: [MichaΓ«l Minelli](mailto:dojo@minelli.me).**
+
+
+## Run the cli
+
+To run the cli (in dev mode) you can use the following command in the base directory of the project:
+```bash
+$ npm run start:dev -- COMMAND
+```
+Where `COMMAND` is the command you want to run.
+
+For example, if you want to test the exercise creation command you can use the following command:
+```bash
+$ npm run start:dev -- exercise create -a "Technique de compilation - TP" --members_username michael.minelli
+```
\ No newline at end of file
diff --git a/Wiki/Development/2-How-to-add-a-new-command.md b/Wiki/Development/2-How-to-add-a-new-command.md
new file mode 100644
index 0000000000000000000000000000000000000000..8548351bc0d7d45d5f0f8dcb4d8ad48fa7d6dabb
--- /dev/null
+++ b/Wiki/Development/2-How-to-add-a-new-command.md
@@ -0,0 +1,402 @@
+# How to add a new command to the Dojo cli
+
+## Introduction
+
+This tutorial describes how to add a new command to the Dojo cli. For that we take an existing command and describe
+his implementation. This command allow a member of the teaching staff of an assignment to publish it.
+
+The command is named `publish` and is a subcommand of the `assignment` command. Here is the command structure:
+```bash
+dojo assignment publish [options] <assignment_name_or_url>
+```
+
+This tutorial is linked to another one which explains how to add a new route to the Dojo backend.
+The documentation for this is available on the [Dojo backend wiki](https://gitedu.hesge.ch/dojo/backend/-/wikis/Development/1-How-to-add-a-new-route).
+
+For the rest of this tutorial we will assume that we are in the `NodeApp` folder :
+```bash
+cd NodeApp
+```
+
+
+## Prerequisites
+
+All the prerequisites are described in
+[How to setup your development environment](1-How-to-setup-your-development-environment) tutorial.
+
+
+## Commands files structure
+
+The commands files are located in the `src/commander` folder. All command files are named with the following pattern:
+`commandPathCommand.ts` where `commandPath` has to be replaced by the path of the command (command, subcommand,
+subsubcommand, etc.).
+
+For each command there are two choices:
+1. If it's a command that will contains subcommands, you will need to create a folder with the name of the command.
+   - In this folder you will need to create a file with the name of the command.
+   - A subfolder named `subcommands` will be needed to store the subcommands files or folders.
+2. If it's a command that will not contains subcommands, you will need to create a file with the name of the command.
+
+### Apply to our use case
+In our case we will create a command that will be a subcommand of the `assignment` command, so its file path will be :
+`src/commander/assignment/subcommands/AssignmentPublishCommand.ts`.
+
+
+## Command class inheritance
+
+All commands must inherit from the `CommanderCommand` abstract class. This class is located in the
+`src/commander/CommanderCommand.ts` file.
+
+When you inherit from this class you will need to implement the following methods and properties :
+- `commandName: string`: This property will be used to define the name of the (sub)command.
+- `defineCommand(): void`: This method will be used to define the command itself.
+- `commandAction(...args: Array<unknown>): Promise<void>`: This method will be used to define the action of the command.
+  You will need to link the command to the action by writing this code in the defineCommand method:
+```typescript
+this.command.action(this.commandAction.bind(this));
+```
+
+Optionally, you can implement the `defineSubCommands(): void` method if you want to declare some subcommands.
+Here is an exemple of implementation:
+```typescript
+protected defineSubCommands() {
+  SessionLoginCommand.registerOnCommand(this.command);
+  SessionLogoutCommand.registerOnCommand(this.command);
+  SessionTestCommand.registerOnCommand(this.command);
+}
+```
+
+### Apply to our use case
+
+Now, the `src/commander/assignment/subcommands/AssignmentPublishCommand.ts` file will look like this:
+```typescript
+import CommanderCommand from '../../CommanderCommand';
+
+class AssignmentPublishCommand extends CommanderCommand {
+  protected commandName: string = 'publish';
+
+  protected defineCommand() {
+  ...
+  }
+
+  protected async commandAction(assignmentNameOrUrl: string, options: { force: boolean }): Promise<void> {
+  ...
+  }
+}
+
+export default new AssignmentPublishCommand();
+```
+
+
+## Define the command
+
+In the `defineCommand()` method you will need to define the command itself. To do this you will need to use the
+Commander.js library. The documentation is available [here](https://github.com/tj/commander.js).
+
+### Apply to our use case
+
+We want to define the `publish` command that take the name of the assignment (or his url) as an argument and have a
+`--force` option that will allow the user to publish the assignment without having to confirm the action.
+
+Here is the code to add in the `defineCommand()` method:
+```typescript
+protected defineCommand() {
+  this.command
+    .description('publish an assignment')
+    .argument('<name or url>', 'name or url (http/s or ssh) of the assignment')
+    .option('-f, --force', 'don\'t ask for confirmation')
+    .action(this.commandAction.bind(this));
+}
+```
+
+
+## Define the action
+
+To define the action we must adapt the `commandAction()` method by adding arguments and options defined in the command
+definition.
+
+And then we will need to implement the action of the command.
+
+### Apply to our use case
+
+In our case we will need to adapt the `commandAction()` method like this:
+```typescript
+protected async commandAction(assignmentNameOrUrl: string, options: { force: boolean }): Promise<void> {
+  ...
+}
+```
+
+For the implementation we will split our code into several steps:
+1. Options parsing and confirmation asking
+2. Checking / Retrieving data
+   1. Test the session validity
+   2. Retrieve the assignment and check if it exists
+   3. Check if the user is in the teaching staff of the assignment
+   4. Check if the assignment is publishable by getting the last pipeline status
+3. Publishing the assignment
+
+#### 1. Options parsing and confirmation asking
+
+First we need to parse the options and check if the `--force` option is present. If it's not present we ask
+the user to confirm the action.
+
+```typescript
+if ( !options.force ) {
+  options.force = (await inquirer.prompt({
+    type   : 'confirm',
+    name   : 'force',
+    message: 'Are you sure you want to publish this assignment?'
+  })).force;
+}
+
+if ( !options.force ) {
+  return;
+}
+```
+
+#### 2. Checking / Retrieving data
+
+##### 2.1. Test the session validity
+
+We call the SessionManager singleton that contain a function to test the session validity.
+It checks only the Dojo backend session validity and not the Gitlab one.
+
+```typescript
+if ( !await SessionManager.testSession(true, null) ) {
+  return;
+}
+```
+
+##### 2.2. Retrieve the assignment and check if it exists
+
+We call the DojoBackendManager singleton that contains a function to retrieve an assignment by his name or his url.
+This function make a request to the Dojo backend to retrieve the assignment.
+
+```typescript
+assignment = await DojoBackendManager.getAssignment(assignmentNameOrUrl);
+if ( !assignment ) {
+  return;
+}
+```
+
+##### 2.3. Check if the user is in the teaching staff of the assignment
+
+We check if the user is in the teaching staff of the assignment by verifying if his id is in the staff array.
+
+```typescript
+if ( !assignment.staff.some(staff => staff.id === SessionManager.profile?.id) ) {
+  return;
+}
+```
+
+##### 2.4. Check if the assignment is publishable by getting the last pipeline status
+
+We call the SharedAssignmentHelper singleton that contains a function to check if the assignment is publishable
+(an assignment is publishable only if the last pipeline status is `success`).
+This function make a request to the Gitlab API to retrieve the last pipeline of the assignment and return an object
+with the informations of it including a `isPublishable` state that is true only if the status of the pipeline is
+`success`.
+
+```typescript
+const isPublishable = await SharedAssignmentHelper.isPublishable(assignment.gitlabId);
+if ( !isPublishable.isPublishable ) {
+  return;
+}
+```
+
+#### 3. Publishing the assignment
+
+Finally, we will call the DojoBackendManager singleton that contain a function to publish an assignment as will be described hereafter.
+
+First of all, we need to implement the route on the backend. For that, please refer to the linked tutorial on the
+[Dojo backend API wiki](https://gitedu.hesge.ch/dojo/backend/-/wikis/Development/1-How-to-add-a-new-route).
+
+Then, we need to create a function that will call the newly created route. This function needs to know the route
+that we have previously created. We will store this route in the `src/sharedByClients/types/Dojo/ApiRoutes.ts` file.
+
+```typescript
+export enum ApiRoute {
+  ...
+  ASSIGNMENT_PUBLISH = '/assignment/{{nameOrUrl}}/publish',
+  ASSIGNMENT_UNPUBLISH = '/assignment/{{nameOrUrl}}/unpublish',
+  ...
+}
+```
+
+Then, we can create the function that will be located in the backend manager singleton stored in the
+`src/managers/DojoBackendManager.ts` file.
+
+_Note:_ We use the `axios` library to make the request to the backend. We do not need to fill authorization headers
+because the `axios interceptors` defined in the `src/managers/HttpManager.ts` file will do it for us.
+
+```typescript
+public async changeAssignmentPublishedStatus(assignment: Assignment, publish: boolean, verbose: boolean = true) {
+  try {
+     await axios.patch<DojoBackendResponse<null>>(this.getApiUrl(publish ? ApiRoute.ASSIGNMENT_PUBLISH : ApiRoute.ASSIGNMENT_UNPUBLISH).replace('{{nameOrUrl}}', encodeURIComponent(assignment.name)), {});
+  } catch ( error ) {
+     ...
+     throw error;
+  }
+}
+```
+
+Finally, we go back to the command file and we call the function to publish the assignment.
+
+```typescript
+try {
+  await DojoBackendManager.changeAssignmentPublishedStatus(assignment, true);
+} catch ( error ) {
+  return;
+}
+```
+
+
+## Use case: final code
+
+In the examples above we have seen all the different steps to implement our `publish` subcommand.
+For a better readability all the UI code (done with ora library) have been removed from the snippets.
+
+Here is the final code of the `src/commander/assignment/subcommands/AssignmentPublishCommand.ts` file with integration
+of ora calls for the interface.
+
+### AssignmentPublishCommand.ts
+```typescript
+import CommanderCommand     from '../../CommanderCommand';
+import inquirer          from 'inquirer';
+import Assignment         from '../../../sharedByClients/models/Assignment';
+import chalk            from 'chalk';
+import SessionManager      from '../../../managers/SessionManager';
+import ora              from 'ora';
+import DojoBackendManager    from '../../../managers/DojoBackendManager';
+import SharedAssignmentHelper from '../../../shared/helpers/Dojo/SharedAssignmentHelper';
+
+
+class AssignmentPublishCommand extends CommanderCommand {
+  protected commandName: string = 'publish';
+
+  protected defineCommand() {
+    this.command
+      .description('publish an assignment')
+      .argument('<name or url>', 'name or url (http/s or ssh) of the assignment')
+      .option('-f, --force', 'don\'t ask for confirmation')
+      .action(this.commandAction.bind(this));
+  }
+
+  protected async commandAction(assignmentNameOrUrl: string, options: { force: boolean }): Promise<void> {
+    if ( !options.force ) {
+      options.force = (await inquirer.prompt({
+        type  : 'confirm',
+        name  : 'force',
+        message: 'Are you sure you want to publish this assignment?'
+      })).force;
+    }
+
+    if ( !options.force ) {
+      return;
+    }
+
+    let assignment!: Assignment | undefined;
+
+    {
+      console.log(chalk.cyan('Please wait while we verify and retrieve data...'));
+
+      if ( !await SessionManager.testSession(true, null) ) {
+        return;
+      }
+
+      ora('Checking assignment:').start().info();
+      ora({
+        text  : assignmentNameOrUrl,
+        indent: 4
+      }).start().info();
+      const assignmentGetSpinner: ora.Ora = ora({
+        text  : 'Checking if assignment exists',
+        indent: 8
+      }).start();
+      assignment = await DojoBackendManager.getAssignment(assignmentNameOrUrl);
+      if ( !assignment ) {
+        assignmentGetSpinner.fail(`The assignment doesn't exists`);
+        return;
+      }
+      assignmentGetSpinner.succeed(`The assignment exists`);
+
+
+      const assignmentCheckAccessSpinner: ora.Ora = ora({
+        text  : 'Checking accesses',
+        indent: 8
+      }).start();
+      if ( !assignment.staff.some(staff => staff.id === SessionManager.profile?.id) ) {
+        assignmentCheckAccessSpinner.fail(`You are not in the staff of this assignment`);
+        return;
+      }
+      assignmentCheckAccessSpinner.succeed(`You are in the staff of this assignment`);
+
+
+      const assignmentIsPublishable: ora.Ora = ora({
+        text  : 'Checking if the assignment is publishable',
+        indent: 8
+      }).start();
+      const isPublishable = await SharedAssignmentHelper.isPublishable(assignment.gitlabId);
+      if ( !isPublishable.isPublishable ) {
+        assignmentIsPublishable.fail(`The assignment is not publishable: ${ isPublishable.status?.message }`);
+        return;
+      }
+      assignmentIsPublishable.succeed(`The assignment is publishable`);
+    }
+
+    {
+      console.log(chalk.cyan(`Please wait while we publish the assignment...`));
+
+      try {
+        await DojoBackendManager.changeAssignmentPublishedStatus(assignment, true);
+      } catch ( error ) {
+        return;
+      }
+    }
+  }
+}
+
+
+export default new AssignmentPublishCommand();
+```
+
+### ApiRoutes.ts
+```typescript
+export enum ApiRoute {
+  ...
+  ASSIGNMENT_PUBLISH = '/assignment/{{nameOrUrl}}/publish',
+  ASSIGNMENT_UNPUBLISH = '/assignment/{{nameOrUrl}}/unpublish',
+  ...
+}
+```
+
+### DojoBackendManager.ts
+```typescript
+public async changeAssignmentPublishedStatus(assignment: Assignment, publish: boolean, verbose: boolean = true) {
+  const spinner: ora.Ora = ora('Changing published status...');
+
+  if ( verbose ) {
+    spinner.start();
+  }
+
+  try {
+    await axios.patch<DojoBackendResponse<null>>(this.getApiUrl(publish ? ApiRoute.ASSIGNMENT_PUBLISH : ApiRoute.ASSIGNMENT_UNPUBLISH).replace('{{nameOrUrl}}', encodeURIComponent(assignment.name)), {});
+
+    if ( verbose ) {
+      spinner.succeed(`Assignment ${ assignment.name } successfully ${ publish ? 'published' : 'unpublished' }`);
+    }
+
+    return;
+  } catch ( error ) {
+    if ( verbose ) {
+      if ( error instanceof AxiosError && error.response ) {
+        spinner.fail(`Assignment visibility change error: ${ error.response.statusText }`);
+      } else {
+        spinner.fail(`Assignment visibility change error: unknown error`);
+      }
+    }
+
+    throw error;
+  }
+}
+```
\ No newline at end of file
diff --git a/Wiki/home.md b/Wiki/home.md
index 90b298e88fc361293cfa24c148149689e3cd9fa6..df06c272b3e13cb87c280810470168ba50f5cba9 100644
--- a/Wiki/home.md
+++ b/Wiki/home.md
@@ -23,3 +23,10 @@ More details here : [Dojo detailed presentation](0-Dojo-presentation)
 
 ### Teaching staff
 * [How to create and publish an assignment](Tutorials/1-Assignment-creation)
+
+
+## Development / Contribution
+
+* [How to contribute]() - Available soon
+* [How to setup your development environment](Development/1-How-to-setup-your-development-environment)
+* [How to add a new command](Development/2-How-to-add-a-new-command)
\ No newline at end of file