diff --git a/.gitignore b/.gitignore
index 5d889f542ad1b6de7093a00bd316a0e421d049f5..7a4b14b17f6c684d9bb8d343f5523e66c3157453 100644
--- a/.gitignore
+++ b/.gitignore
@@ -350,6 +350,6 @@ Sessionx.vim
 .netrwhist
 *~
 # Auto-generated tag files
-tags
+# tags
 # Persistent undo
 [._]*.un~
diff --git a/.gitmodules b/.gitmodules
index f612bc05fef311547b546e68da614a7aec463793..139e244614dce96d58e362ede9d99239d56c2271 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -3,7 +3,7 @@
 	url = ../../shared/jetbrains_configuration.git
 [submodule "NodeApp/src/shared"]
 	path = NodeApp/src/shared
-	url = ../../shared/nodesharedcode.git
+	url = https://gitedu.hesge.ch/dojo_project/projects/shared/nodesharedcode
 [submodule "NodeApp/src/sharedByClients"]
 	path = NodeApp/src/sharedByClients
-	url = ../../shared/nodeclientsharedcode.git
+	url = https://gitedu.hesge.ch/dojo_project/projects/shared/nodeclientsharedcode
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..b58b603fea78041071d125a30db58d79b3d49217
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,5 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/dojocli.iml b/.idea/dojocli.iml
new file mode 100644
index 0000000000000000000000000000000000000000..24643cc37449b4bde54411a80b8ed61258225e34
--- /dev/null
+++ b/.idea/dojocli.iml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="WEB_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/.tmp" />
+      <excludeFolder url="file://$MODULE_DIR$/temp" />
+      <excludeFolder url="file://$MODULE_DIR$/tmp" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4ab30517f783babc3ee1b319137f7aef281d02af
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/dojocli.iml" filepath="$PROJECT_DIR$/.idea/dojocli.iml" />
+    </modules>
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000000000000000000000000000000000000..35eb1ddfbbc029bcab630581847471d7f238ec53
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d2d28342a81587602e6f84a7bab5f2c4ef00680..ff0f2b11db8d270ede26780bb75dcf919040ea13 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,11 @@
 - No modifications / Keep major and minors versions in sync with all parts of the project
 -->
 
+## 4.2.0 (Upcoming)
+
+### ✨ Feature
+- Add support for tags on assignments and exercises
+
 
 ## 4.1.1 (2024-05-28)
 
@@ -27,7 +32,7 @@
 ## 4.1.0 (2024-05-28)
 
 ### ✨ Feature
-- Add features related to corrige (commentary, commit specific link / update, delete link)
+- Add features related to corrige (commentary, commit a specific link / update, delete link)
 
 ### 🎨 Interface
 - Ask for confirmation before creating an exercise that already exists
diff --git a/NodeApp/.gitlab-ci/01_functions.yml b/NodeApp/.gitlab-ci/01_functions.yml
index 83e624592f6847ffaca7aa0d5bc06a71493d35f5..f3ca0ffab9b95592774020d65bfa43e2aa69304b 100644
--- a/NodeApp/.gitlab-ci/01_functions.yml
+++ b/NodeApp/.gitlab-ci/01_functions.yml
@@ -92,7 +92,7 @@
                 sed -i -r "s/,[\ \n]*\}/\}/g" src/init.ts
             
                 echo "DOTENV_KEY_PRODUCTION=\"${DOTENV_PROD_KEY}\"" > .env.keys
-                npx @dotenvx/dotenvx decrypt
+                npx @dotenvx/dotenvx@0.45.0 decrypt
                 mv .env.production .env
                 rm .env.keys
             fi
diff --git a/NodeApp/.idea/material_theme_project_new.xml b/NodeApp/.idea/material_theme_project_new.xml
new file mode 100644
index 0000000000000000000000000000000000000000..16e830f2d917c0815e814dcbf57e219c3b85c2e2
--- /dev/null
+++ b/NodeApp/.idea/material_theme_project_new.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="MaterialThemeProjectNewConfig">
+    <option name="metadata">
+      <MTProjectMetadataState>
+        <option name="migrated" value="true" />
+        <option name="pristineConfig" value="false" />
+        <option name="userId" value="104e8585:19002424fea:-7ffe" />
+      </MTProjectMetadataState>
+    </option>
+  </component>
+</project>
\ No newline at end of file
diff --git a/NodeApp/package-lock.json b/NodeApp/package-lock.json
index 6bb6f37914629570747b79e26795d2aa055cc242..e52fbce9fe43a25f2bc90f42b46f5d6ada4cbb44 100644
--- a/NodeApp/package-lock.json
+++ b/NodeApp/package-lock.json
@@ -1,16 +1,16 @@
 {
     "name": "dojo_cli",
-    "version": "4.1.1",
+    "version": "4.2.0",
     "lockfileVersion": 3,
     "requires": true,
     "packages": {
         "": {
             "name": "dojo_cli",
-            "version": "4.1.1",
+            "version": "4.2.0",
             "license": "AGPLv3",
             "dependencies": {
-                "@dotenvx/dotenvx": "^0.44.1",
-                "@eslint/js": "^9.3.0",
+                "@dotenvx/dotenvx": "^0.45.0",
+                "@eslint/js": "^9.6.0",
                 "@gitbeaker/core": "^40.0.3",
                 "@gitbeaker/requester-utils": "^40.0.3",
                 "@gitbeaker/rest": "^40.0.3",
@@ -18,6 +18,7 @@
                 "axios": "^1.7.2",
                 "boxen": "^5.1.2",
                 "chalk": "^4.1.2",
+                "cli-table3": "^0.6.5",
                 "commander": "^12.1.0",
                 "form-data": "^4.0.0",
                 "fs-extra": "^11.2.0",
@@ -31,7 +32,7 @@
                 "tar-stream": "^3.1.7",
                 "winston": "^3.13.0",
                 "winston-transport": "^4.7.0",
-                "yaml": "^2.4.2",
+                "yaml": "^2.4.5",
                 "zod": "^3.23.8",
                 "zod-validation-error": "^3.3.0"
             },
@@ -42,19 +43,20 @@
                 "@types/fs-extra": "^11.0.4",
                 "@types/inquirer": "^8.2.10",
                 "@types/jsonwebtoken": "^8.5.9",
-                "@types/node": "^18.19.33",
+                "@types/node": "^18.19.39",
                 "@types/semver": "^7.5.8",
                 "@types/tar-stream": "^3.1.3",
-                "@typescript-eslint/eslint-plugin": "^7.11.0",
-                "@typescript-eslint/parser": "^7.11.0",
-                "dotenv-vault": "^1.26.1",
+                "@typescript-eslint/eslint-plugin": "^7.15.0",
+                "@typescript-eslint/parser": "^7.15.0",
+                "dotenv-cli": "^7.4.2",
+                "dotenv-vault": "^1.26.2",
                 "eslint": "^8.57.0",
                 "genversion": "^3.2.0",
                 "pkg": "^5.8.1",
                 "tiny-typed-emitter": "^2.1.0",
-                "tsx": "^4.11.0",
-                "typescript": "^5.4.5",
-                "typescript-eslint": "^7.11.0"
+                "tsx": "^4.16.2",
+                "typescript": "^5.5.3",
+                "typescript-eslint": "^7.15.0"
             }
         },
         "node_modules/@aashutoshrathi/word-wrap": {
@@ -165,9 +167,9 @@
             }
         },
         "node_modules/@dotenvx/dotenvx": {
-            "version": "0.44.1",
-            "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-0.44.1.tgz",
-            "integrity": "sha512-OmOU7CRwhXydZUHeTP46GNZsGpwQ3mwrr3cUAWod+FmrKW3ib4GYe1jU++ZFyEEUNvg532QvvM7hQ44YyJrgfw==",
+            "version": "0.45.0",
+            "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-0.45.0.tgz",
+            "integrity": "sha512-qjW6PDX3mONGQHSrI6LPzOCHn/RZPZuHThPrRZOvyo90pTMGQaCqRbcastl1Y3ZQlw0igdcKnk9pJSh5uYNLbg==",
             "dependencies": {
                 "@inquirer/confirm": "^2.0.17",
                 "arch": "^2.1.1",
@@ -216,9 +218,9 @@
             }
         },
         "node_modules/@esbuild/aix-ppc64": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
-            "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+            "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
             "cpu": [
                 "ppc64"
             ],
@@ -232,9 +234,9 @@
             }
         },
         "node_modules/@esbuild/android-arm": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
-            "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+            "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
             "cpu": [
                 "arm"
             ],
@@ -248,9 +250,9 @@
             }
         },
         "node_modules/@esbuild/android-arm64": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
-            "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+            "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
             "cpu": [
                 "arm64"
             ],
@@ -264,9 +266,9 @@
             }
         },
         "node_modules/@esbuild/android-x64": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
-            "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+            "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
             "cpu": [
                 "x64"
             ],
@@ -280,9 +282,9 @@
             }
         },
         "node_modules/@esbuild/darwin-arm64": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
-            "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+            "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
             "cpu": [
                 "arm64"
             ],
@@ -296,9 +298,9 @@
             }
         },
         "node_modules/@esbuild/darwin-x64": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
-            "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+            "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
             "cpu": [
                 "x64"
             ],
@@ -312,9 +314,9 @@
             }
         },
         "node_modules/@esbuild/freebsd-arm64": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
-            "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+            "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
             "cpu": [
                 "arm64"
             ],
@@ -328,9 +330,9 @@
             }
         },
         "node_modules/@esbuild/freebsd-x64": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
-            "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+            "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
             "cpu": [
                 "x64"
             ],
@@ -344,9 +346,9 @@
             }
         },
         "node_modules/@esbuild/linux-arm": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
-            "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+            "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
             "cpu": [
                 "arm"
             ],
@@ -360,9 +362,9 @@
             }
         },
         "node_modules/@esbuild/linux-arm64": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
-            "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+            "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
             "cpu": [
                 "arm64"
             ],
@@ -376,9 +378,9 @@
             }
         },
         "node_modules/@esbuild/linux-ia32": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
-            "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+            "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
             "cpu": [
                 "ia32"
             ],
@@ -392,9 +394,9 @@
             }
         },
         "node_modules/@esbuild/linux-loong64": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
-            "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+            "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
             "cpu": [
                 "loong64"
             ],
@@ -408,9 +410,9 @@
             }
         },
         "node_modules/@esbuild/linux-mips64el": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
-            "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+            "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
             "cpu": [
                 "mips64el"
             ],
@@ -424,9 +426,9 @@
             }
         },
         "node_modules/@esbuild/linux-ppc64": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
-            "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+            "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
             "cpu": [
                 "ppc64"
             ],
@@ -440,9 +442,9 @@
             }
         },
         "node_modules/@esbuild/linux-riscv64": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
-            "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+            "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
             "cpu": [
                 "riscv64"
             ],
@@ -456,9 +458,9 @@
             }
         },
         "node_modules/@esbuild/linux-s390x": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
-            "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+            "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
             "cpu": [
                 "s390x"
             ],
@@ -472,9 +474,9 @@
             }
         },
         "node_modules/@esbuild/linux-x64": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
-            "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+            "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
             "cpu": [
                 "x64"
             ],
@@ -488,9 +490,9 @@
             }
         },
         "node_modules/@esbuild/netbsd-x64": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
-            "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+            "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
             "cpu": [
                 "x64"
             ],
@@ -504,9 +506,9 @@
             }
         },
         "node_modules/@esbuild/openbsd-x64": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
-            "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+            "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
             "cpu": [
                 "x64"
             ],
@@ -520,9 +522,9 @@
             }
         },
         "node_modules/@esbuild/sunos-x64": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
-            "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+            "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
             "cpu": [
                 "x64"
             ],
@@ -536,9 +538,9 @@
             }
         },
         "node_modules/@esbuild/win32-arm64": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
-            "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+            "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
             "cpu": [
                 "arm64"
             ],
@@ -552,9 +554,9 @@
             }
         },
         "node_modules/@esbuild/win32-ia32": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
-            "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+            "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
             "cpu": [
                 "ia32"
             ],
@@ -568,9 +570,9 @@
             }
         },
         "node_modules/@esbuild/win32-x64": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
-            "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+            "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
             "cpu": [
                 "x64"
             ],
@@ -693,9 +695,9 @@
             }
         },
         "node_modules/@eslint/js": {
-            "version": "9.3.0",
-            "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.3.0.tgz",
-            "integrity": "sha512-niBqk8iwv96+yuTwjM6bWg8ovzAPF9qkICsGtcoa5/dmqcEMfdwNAX7+/OHcJHc7wj7XqPxH98oAHytFYlw6Sw==",
+            "version": "9.6.0",
+            "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.6.0.tgz",
+            "integrity": "sha512-D9B0/3vNg44ZeWbYMpBoXqNP4j6eQD5vNwIlGAuFRRzK/WtT/jvDQW3Bi9kkf3PMDMlM7Yi+73VLUsn5bJcl8A==",
             "engines": {
                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
             }
@@ -1613,9 +1615,9 @@
             }
         },
         "node_modules/@types/node": {
-            "version": "18.19.33",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.33.tgz",
-            "integrity": "sha512-NR9+KrpSajr2qBVp/Yt5TU/rp+b5Mayi3+OlMlcg2cVCfRmcG5PWZ7S4+MG9PZ5gWBoc9Pd0BKSRViuBCRPu0A==",
+            "version": "18.19.39",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.39.tgz",
+            "integrity": "sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==",
             "dependencies": {
                 "undici-types": "~5.26.4"
             }
@@ -1655,16 +1657,16 @@
             "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g=="
         },
         "node_modules/@typescript-eslint/eslint-plugin": {
-            "version": "7.11.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz",
-            "integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==",
+            "version": "7.15.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz",
+            "integrity": "sha512-uiNHpyjZtFrLwLDpHnzaDlP3Tt6sGMqTCiqmxaN4n4RP0EfYZDODJyddiFDF44Hjwxr5xAcaYxVKm9QKQFJFLA==",
             "dev": true,
             "dependencies": {
                 "@eslint-community/regexpp": "^4.10.0",
-                "@typescript-eslint/scope-manager": "7.11.0",
-                "@typescript-eslint/type-utils": "7.11.0",
-                "@typescript-eslint/utils": "7.11.0",
-                "@typescript-eslint/visitor-keys": "7.11.0",
+                "@typescript-eslint/scope-manager": "7.15.0",
+                "@typescript-eslint/type-utils": "7.15.0",
+                "@typescript-eslint/utils": "7.15.0",
+                "@typescript-eslint/visitor-keys": "7.15.0",
                 "graphemer": "^1.4.0",
                 "ignore": "^5.3.1",
                 "natural-compare": "^1.4.0",
@@ -1688,15 +1690,15 @@
             }
         },
         "node_modules/@typescript-eslint/parser": {
-            "version": "7.11.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz",
-            "integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==",
+            "version": "7.15.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.15.0.tgz",
+            "integrity": "sha512-k9fYuQNnypLFcqORNClRykkGOMOj+pV6V91R4GO/l1FDGwpqmSwoOQrOHo3cGaH63e+D3ZiCAOsuS/D2c99j/A==",
             "dev": true,
             "dependencies": {
-                "@typescript-eslint/scope-manager": "7.11.0",
-                "@typescript-eslint/types": "7.11.0",
-                "@typescript-eslint/typescript-estree": "7.11.0",
-                "@typescript-eslint/visitor-keys": "7.11.0",
+                "@typescript-eslint/scope-manager": "7.15.0",
+                "@typescript-eslint/types": "7.15.0",
+                "@typescript-eslint/typescript-estree": "7.15.0",
+                "@typescript-eslint/visitor-keys": "7.15.0",
                 "debug": "^4.3.4"
             },
             "engines": {
@@ -1716,13 +1718,13 @@
             }
         },
         "node_modules/@typescript-eslint/scope-manager": {
-            "version": "7.11.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz",
-            "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==",
+            "version": "7.15.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.15.0.tgz",
+            "integrity": "sha512-Q/1yrF/XbxOTvttNVPihxh1b9fxamjEoz2Os/Pe38OHwxC24CyCqXxGTOdpb4lt6HYtqw9HetA/Rf6gDGaMPlw==",
             "dev": true,
             "dependencies": {
-                "@typescript-eslint/types": "7.11.0",
-                "@typescript-eslint/visitor-keys": "7.11.0"
+                "@typescript-eslint/types": "7.15.0",
+                "@typescript-eslint/visitor-keys": "7.15.0"
             },
             "engines": {
                 "node": "^18.18.0 || >=20.0.0"
@@ -1733,13 +1735,13 @@
             }
         },
         "node_modules/@typescript-eslint/type-utils": {
-            "version": "7.11.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz",
-            "integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==",
+            "version": "7.15.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.15.0.tgz",
+            "integrity": "sha512-SkgriaeV6PDvpA6253PDVep0qCqgbO1IOBiycjnXsszNTVQe5flN5wR5jiczoEoDEnAqYFSFFc9al9BSGVltkg==",
             "dev": true,
             "dependencies": {
-                "@typescript-eslint/typescript-estree": "7.11.0",
-                "@typescript-eslint/utils": "7.11.0",
+                "@typescript-eslint/typescript-estree": "7.15.0",
+                "@typescript-eslint/utils": "7.15.0",
                 "debug": "^4.3.4",
                 "ts-api-utils": "^1.3.0"
             },
@@ -1760,9 +1762,9 @@
             }
         },
         "node_modules/@typescript-eslint/types": {
-            "version": "7.11.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz",
-            "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==",
+            "version": "7.15.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.15.0.tgz",
+            "integrity": "sha512-aV1+B1+ySXbQH0pLK0rx66I3IkiZNidYobyfn0WFsdGhSXw+P3YOqeTq5GED458SfB24tg+ux3S+9g118hjlTw==",
             "dev": true,
             "engines": {
                 "node": "^18.18.0 || >=20.0.0"
@@ -1773,13 +1775,13 @@
             }
         },
         "node_modules/@typescript-eslint/typescript-estree": {
-            "version": "7.11.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz",
-            "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==",
+            "version": "7.15.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.15.0.tgz",
+            "integrity": "sha512-gjyB/rHAopL/XxfmYThQbXbzRMGhZzGw6KpcMbfe8Q3nNQKStpxnUKeXb0KiN/fFDR42Z43szs6rY7eHk0zdGQ==",
             "dev": true,
             "dependencies": {
-                "@typescript-eslint/types": "7.11.0",
-                "@typescript-eslint/visitor-keys": "7.11.0",
+                "@typescript-eslint/types": "7.15.0",
+                "@typescript-eslint/visitor-keys": "7.15.0",
                 "debug": "^4.3.4",
                 "globby": "^11.1.0",
                 "is-glob": "^4.0.3",
@@ -1801,15 +1803,15 @@
             }
         },
         "node_modules/@typescript-eslint/utils": {
-            "version": "7.11.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz",
-            "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==",
+            "version": "7.15.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.15.0.tgz",
+            "integrity": "sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==",
             "dev": true,
             "dependencies": {
                 "@eslint-community/eslint-utils": "^4.4.0",
-                "@typescript-eslint/scope-manager": "7.11.0",
-                "@typescript-eslint/types": "7.11.0",
-                "@typescript-eslint/typescript-estree": "7.11.0"
+                "@typescript-eslint/scope-manager": "7.15.0",
+                "@typescript-eslint/types": "7.15.0",
+                "@typescript-eslint/typescript-estree": "7.15.0"
             },
             "engines": {
                 "node": "^18.18.0 || >=20.0.0"
@@ -1823,12 +1825,12 @@
             }
         },
         "node_modules/@typescript-eslint/visitor-keys": {
-            "version": "7.11.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz",
-            "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==",
+            "version": "7.15.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.15.0.tgz",
+            "integrity": "sha512-Hqgy/ETgpt2L5xueA/zHHIl4fJI2O4XUE9l4+OIfbJIRSnTJb/QscncdqqZzofQegIJugRIF57OJea1khw2SDw==",
             "dev": true,
             "dependencies": {
-                "@typescript-eslint/types": "7.11.0",
+                "@typescript-eslint/types": "7.15.0",
                 "eslint-visitor-keys": "^3.4.3"
             },
             "engines": {
@@ -2349,6 +2351,29 @@
                 "url": "https://github.com/sponsors/sindresorhus"
             }
         },
+        "node_modules/cli-table3": {
+            "version": "0.6.5",
+            "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz",
+            "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==",
+            "dependencies": {
+                "string-width": "^4.2.0"
+            },
+            "engines": {
+                "node": "10.* || >= 12.*"
+            },
+            "optionalDependencies": {
+                "@colors/colors": "1.5.0"
+            }
+        },
+        "node_modules/cli-table3/node_modules/@colors/colors": {
+            "version": "1.5.0",
+            "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
+            "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
+            "optional": true,
+            "engines": {
+                "node": ">=0.1.90"
+            }
+        },
         "node_modules/cli-width": {
             "version": "4.1.0",
             "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
@@ -2726,6 +2751,30 @@
                 "url": "https://dotenvx.com"
             }
         },
+        "node_modules/dotenv-cli": {
+            "version": "7.4.2",
+            "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-7.4.2.tgz",
+            "integrity": "sha512-SbUj8l61zIbzyhIbg0FwPJq6+wjbzdn9oEtozQpZ6kW2ihCcapKVZj49oCT3oPM+mgQm+itgvUQcG5szxVrZTA==",
+            "dev": true,
+            "dependencies": {
+                "cross-spawn": "^7.0.3",
+                "dotenv": "^16.3.0",
+                "dotenv-expand": "^10.0.0",
+                "minimist": "^1.2.6"
+            },
+            "bin": {
+                "dotenv": "cli.js"
+            }
+        },
+        "node_modules/dotenv-cli/node_modules/dotenv-expand": {
+            "version": "10.0.0",
+            "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
+            "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==",
+            "dev": true,
+            "engines": {
+                "node": ">=12"
+            }
+        },
         "node_modules/dotenv-expand": {
             "version": "11.0.6",
             "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.6.tgz",
@@ -2741,9 +2790,9 @@
             }
         },
         "node_modules/dotenv-vault": {
-            "version": "1.26.1",
-            "resolved": "https://registry.npmjs.org/dotenv-vault/-/dotenv-vault-1.26.1.tgz",
-            "integrity": "sha512-v+RK6LXpJQWhaelTT2s0b5FQB0qziRBuGCrAgAeDHtgkDEA0NqF7OXYXsrnKTuCPnwBg0FmNJr4lZebCpJnrFA==",
+            "version": "1.26.2",
+            "resolved": "https://registry.npmjs.org/dotenv-vault/-/dotenv-vault-1.26.2.tgz",
+            "integrity": "sha512-nURqmc3kii3kqiZXcBYdt0QrjpXBjXWtzevCwC9FRbIjwKenGoN/bZHC9l9ueYI3gGoKjgt/1Cmno6HvzgMlDA==",
             "dev": true,
             "dependencies": {
                 "@oclif/core": "^1",
@@ -2859,9 +2908,9 @@
             }
         },
         "node_modules/esbuild": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
-            "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
+            "version": "0.21.5",
+            "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+            "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
             "dev": true,
             "hasInstallScript": true,
             "bin": {
@@ -2871,29 +2920,29 @@
                 "node": ">=12"
             },
             "optionalDependencies": {
-                "@esbuild/aix-ppc64": "0.20.2",
-                "@esbuild/android-arm": "0.20.2",
-                "@esbuild/android-arm64": "0.20.2",
-                "@esbuild/android-x64": "0.20.2",
-                "@esbuild/darwin-arm64": "0.20.2",
-                "@esbuild/darwin-x64": "0.20.2",
-                "@esbuild/freebsd-arm64": "0.20.2",
-                "@esbuild/freebsd-x64": "0.20.2",
-                "@esbuild/linux-arm": "0.20.2",
-                "@esbuild/linux-arm64": "0.20.2",
-                "@esbuild/linux-ia32": "0.20.2",
-                "@esbuild/linux-loong64": "0.20.2",
-                "@esbuild/linux-mips64el": "0.20.2",
-                "@esbuild/linux-ppc64": "0.20.2",
-                "@esbuild/linux-riscv64": "0.20.2",
-                "@esbuild/linux-s390x": "0.20.2",
-                "@esbuild/linux-x64": "0.20.2",
-                "@esbuild/netbsd-x64": "0.20.2",
-                "@esbuild/openbsd-x64": "0.20.2",
-                "@esbuild/sunos-x64": "0.20.2",
-                "@esbuild/win32-arm64": "0.20.2",
-                "@esbuild/win32-ia32": "0.20.2",
-                "@esbuild/win32-x64": "0.20.2"
+                "@esbuild/aix-ppc64": "0.21.5",
+                "@esbuild/android-arm": "0.21.5",
+                "@esbuild/android-arm64": "0.21.5",
+                "@esbuild/android-x64": "0.21.5",
+                "@esbuild/darwin-arm64": "0.21.5",
+                "@esbuild/darwin-x64": "0.21.5",
+                "@esbuild/freebsd-arm64": "0.21.5",
+                "@esbuild/freebsd-x64": "0.21.5",
+                "@esbuild/linux-arm": "0.21.5",
+                "@esbuild/linux-arm64": "0.21.5",
+                "@esbuild/linux-ia32": "0.21.5",
+                "@esbuild/linux-loong64": "0.21.5",
+                "@esbuild/linux-mips64el": "0.21.5",
+                "@esbuild/linux-ppc64": "0.21.5",
+                "@esbuild/linux-riscv64": "0.21.5",
+                "@esbuild/linux-s390x": "0.21.5",
+                "@esbuild/linux-x64": "0.21.5",
+                "@esbuild/netbsd-x64": "0.21.5",
+                "@esbuild/openbsd-x64": "0.21.5",
+                "@esbuild/sunos-x64": "0.21.5",
+                "@esbuild/win32-arm64": "0.21.5",
+                "@esbuild/win32-ia32": "0.21.5",
+                "@esbuild/win32-x64": "0.21.5"
             }
         },
         "node_modules/escalade": {
@@ -6038,12 +6087,12 @@
             "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
         },
         "node_modules/tsx": {
-            "version": "4.11.0",
-            "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.11.0.tgz",
-            "integrity": "sha512-vzGGELOgAupsNVssAmZjbUDfdm/pWP4R+Kg8TVdsonxbXk0bEpE1qh0yV6/QxUVXaVlNemgcPajGdJJ82n3stg==",
+            "version": "4.16.2",
+            "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.16.2.tgz",
+            "integrity": "sha512-C1uWweJDgdtX2x600HjaFaucXTilT7tgUZHbOE4+ypskZ1OP8CRCSDkCxG6Vya9EwaFIVagWwpaVAn5wzypaqQ==",
             "dev": true,
             "dependencies": {
-                "esbuild": "~0.20.2",
+                "esbuild": "~0.21.5",
                 "get-tsconfig": "^4.7.5"
             },
             "bin": {
@@ -6092,9 +6141,9 @@
             }
         },
         "node_modules/typescript": {
-            "version": "5.4.5",
-            "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
-            "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
+            "version": "5.5.3",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
+            "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
             "dev": true,
             "bin": {
                 "tsc": "bin/tsc",
@@ -6105,14 +6154,14 @@
             }
         },
         "node_modules/typescript-eslint": {
-            "version": "7.11.0",
-            "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.11.0.tgz",
-            "integrity": "sha512-ZKe3yHF/IS/kCUE4CGE3UgtK+Q7yRk1e9kwEI0rqm9XxMTd9P1eHe0LVVtrZ3oFuIQ2unJ9Xn0vTsLApzJ3aPw==",
+            "version": "7.15.0",
+            "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.15.0.tgz",
+            "integrity": "sha512-Ta40FhMXBCwHura4X4fncaCVkVcnJ9jnOq5+Lp4lN8F4DzHZtOwZdRvVBiNUGznUDHPwdGnrnwxmUOU2fFQqFA==",
             "dev": true,
             "dependencies": {
-                "@typescript-eslint/eslint-plugin": "7.11.0",
-                "@typescript-eslint/parser": "7.11.0",
-                "@typescript-eslint/utils": "7.11.0"
+                "@typescript-eslint/eslint-plugin": "7.15.0",
+                "@typescript-eslint/parser": "7.15.0",
+                "@typescript-eslint/utils": "7.15.0"
             },
             "engines": {
                 "node": "^18.18.0 || >=20.0.0"
@@ -6321,9 +6370,9 @@
             }
         },
         "node_modules/yaml": {
-            "version": "2.4.2",
-            "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
-            "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
+            "version": "2.4.5",
+            "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz",
+            "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==",
             "bin": {
                 "yaml": "bin.mjs"
             },
diff --git a/NodeApp/package.json b/NodeApp/package.json
index b6155d914200399fedf4c430cd13d65757a80cb9..52d1b78367af51a475b6666b85283bbfdec1ad09 100644
--- a/NodeApp/package.json
+++ b/NodeApp/package.json
@@ -1,9 +1,9 @@
 {
     "name"           : "dojo_cli",
     "description"    : "CLI of the Dojo project",
-    "version"        : "4.1.1",
+    "version"        : "4.2.0",
     "license"        : "AGPLv3",
-    "author"         : "Michaël Minelli <dojo@minelli.me>",
+    "author"         : "Michaël Minelli <dojo@mail.minelli.swiss>",
     "main"           : "dist/app.js",
     "bin"            : {
         "dojo": "./dist/app.js"
@@ -29,12 +29,12 @@
         "lint"        : "npx eslint .",
         "genversion"  : "npx genversion -s -e src/config/Version.ts",
         "build"       : "npm run genversion; npx tsc",
-        "start:dev"   : "npm run genversion; npm run lint; tsc --noEmit && npx tsx src/app.ts",
+        "start:dev"   : "npm run genversion; npm run lint; tsc --noEmit && npx tsx --no-warnings src/app.ts",
         "test"        : "echo \"Error: no test specified\" && exit 1"
     },
     "dependencies"   : {
-        "@dotenvx/dotenvx"          : "^0.44.1",
-        "@eslint/js"                : "^9.3.0",
+        "@dotenvx/dotenvx"          : "^0.45.0",
+        "@eslint/js"                : "^9.6.0",
         "@gitbeaker/core"           : "^40.0.3",
         "@gitbeaker/requester-utils": "^40.0.3",
         "@gitbeaker/rest"           : "^40.0.3",
@@ -42,6 +42,7 @@
         "axios"                     : "^1.7.2",
         "boxen"                     : "^5.1.2",
         "chalk"                     : "^4.1.2",
+        "cli-table3"                : "^0.6.5",
         "commander"                 : "^12.1.0",
         "form-data"                 : "^4.0.0",
         "fs-extra"                  : "^11.2.0",
@@ -55,7 +56,7 @@
         "tar-stream"                : "^3.1.7",
         "winston"                   : "^3.13.0",
         "winston-transport"         : "^4.7.0",
-        "yaml"                      : "^2.4.2",
+        "yaml"                      : "^2.4.5",
         "zod"                       : "^3.23.8",
         "zod-validation-error"      : "^3.3.0"
     },
@@ -63,18 +64,19 @@
         "@types/fs-extra"                 : "^11.0.4",
         "@types/inquirer"                 : "^8.2.10",
         "@types/jsonwebtoken"             : "^8.5.9",
-        "@types/node"                     : "^18.19.33",
+        "@types/node"                     : "^18.19.39",
         "@types/semver"                   : "^7.5.8",
         "@types/tar-stream"               : "^3.1.3",
-        "@typescript-eslint/eslint-plugin": "^7.11.0",
-        "@typescript-eslint/parser"       : "^7.11.0",
-        "dotenv-vault"                    : "^1.26.1",
+        "@typescript-eslint/eslint-plugin": "^7.15.0",
+        "@typescript-eslint/parser"       : "^7.15.0",
+        "dotenv-cli"                      : "^7.4.2",
+        "dotenv-vault"                    : "^1.26.2",
         "eslint"                          : "^8.57.0",
         "genversion"                      : "^3.2.0",
         "pkg"                             : "^5.8.1",
         "tiny-typed-emitter"              : "^2.1.0",
-        "tsx"                             : "^4.11.0",
-        "typescript"                      : "^5.4.5",
-        "typescript-eslint"               : "^7.11.0"
+        "tsx"                             : "^4.16.2",
+        "typescript"                      : "^5.5.3",
+        "typescript-eslint"               : "^7.15.0"
     }
 }
diff --git a/NodeApp/src/commander/CommanderApp.ts b/NodeApp/src/commander/CommanderApp.ts
index 7cb5952408f98e02b73517ca53746c41dc13c3ab..30e9cfe3ba8195af46113d33add1ecff63aa2550 100644
--- a/NodeApp/src/commander/CommanderApp.ts
+++ b/NodeApp/src/commander/CommanderApp.ts
@@ -13,6 +13,7 @@ import AuthCommand         from './auth/AuthCommand.js';
 import SessionCommand      from './auth/SessionCommand.js';
 import UpgradeCommand      from './UpgradeCommand.js';
 import TextStyle           from '../types/TextStyle.js';
+import TagCommand          from './tag/TagCommand';
 
 
 class CommanderApp {
@@ -118,6 +119,7 @@ ${ TextStyle.CODE(' dojo upgrade ') }`, {
         SessionCommand.registerOnCommand(this.program);
         AssignmentCommand.registerOnCommand(this.program);
         ExerciseCommand.registerOnCommand(this.program);
+        TagCommand.registerOnCommand(this.program);
         CompletionCommand.registerOnCommand(this.program);
         UpgradeCommand.registerOnCommand(this.program);
     }
diff --git a/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts b/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts
index 922c126c5d7205de5de12136b80fcc980686ddcc..86a5bc585c2f68c833fb8b3618792da99fa9b439 100644
--- a/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts
+++ b/NodeApp/src/commander/assignment/subcommands/AssignmentCreateCommand.ts
@@ -33,9 +33,7 @@ class AssignmentCreateCommand extends CommanderCommand {
     private async dataRetrieval(options: CommandOptions) {
         console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...'));
 
-        if ( !await AccessesHelper.checkTeachingStaff() ) {
-            throw new Error();
-        }
+        await AccessesHelper.checkTeachingStaff();
 
         this.members = await GitlabManager.fetchMembers(options);
         if ( !this.members ) {
diff --git a/NodeApp/src/commander/exercise/ExerciseCommand.ts b/NodeApp/src/commander/exercise/ExerciseCommand.ts
index 8089092754831b94bf8b9fba3e07e2fcd92a657e..02e40e36a7bd369b056c2cf5a8303e796d8af5f5 100644
--- a/NodeApp/src/commander/exercise/ExerciseCommand.ts
+++ b/NodeApp/src/commander/exercise/ExerciseCommand.ts
@@ -2,6 +2,7 @@ import CommanderCommand          from '../CommanderCommand.js';
 import ExerciseCreateCommand     from './subcommands/ExerciseCreateCommand.js';
 import ExerciseRunCommand        from './subcommands/ExerciseRunCommand.js';
 import ExerciseCorrectionCommand from './subcommands/ExerciseCorrectionCommand.js';
+import ExerciseDeleteCommand     from './subcommands/ExerciseDeleteCommand';
 
 
 class ExerciseCommand extends CommanderCommand {
@@ -15,6 +16,7 @@ class ExerciseCommand extends CommanderCommand {
     protected defineSubCommands() {
         ExerciseCreateCommand.registerOnCommand(this.command);
         ExerciseRunCommand.registerOnCommand(this.command);
+        ExerciseDeleteCommand.registerOnCommand(this.command);
         ExerciseCorrectionCommand.registerOnCommand(this.command);
     }
 
diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseCorrectionCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseCorrectionCommand.ts
index 0a7aac7bb2302c1b1ec538803f4e87c1fb4d567e..650d1504f725c7db60d9cb5b45140838d21d5300 100644
--- a/NodeApp/src/commander/exercise/subcommands/ExerciseCorrectionCommand.ts
+++ b/NodeApp/src/commander/exercise/subcommands/ExerciseCorrectionCommand.ts
@@ -17,8 +17,8 @@ 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')
+            .description('list corrections of an assignment')
+            .requiredOption('-a, --assignment <string>', 'id or url of the assignment')
             .action(this.commandAction.bind(this));
     }
 
diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts
index 73a67c5387e448c4e904808e9c4341cbde33c971..3db1510582c5927fcf506ac6acc360f04548aa8c 100644
--- a/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts
+++ b/NodeApp/src/commander/exercise/subcommands/ExerciseCreateCommand.ts
@@ -33,9 +33,7 @@ class ExerciseCreateCommand extends CommanderCommand {
     private async dataRetrieval(options: CommandOptions) {
         console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...'));
 
-        if ( !await AccessesHelper.checkStudent() ) {
-            throw new Error();
-        }
+        await AccessesHelper.checkStudent();
 
         this.members = await GitlabManager.fetchMembers(options);
         if ( !this.members ) {
diff --git a/NodeApp/src/commander/exercise/subcommands/ExerciseDeleteCommand.ts b/NodeApp/src/commander/exercise/subcommands/ExerciseDeleteCommand.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2ec20cdfd13b93306de1bf9095adc9c6d40abb5c
--- /dev/null
+++ b/NodeApp/src/commander/exercise/subcommands/ExerciseDeleteCommand.ts
@@ -0,0 +1,38 @@
+import CommanderCommand   from '../../CommanderCommand';
+import DojoBackendManager from '../../../managers/DojoBackendManager';
+import AccessesHelper     from '../../../helpers/AccessesHelper';
+import TextStyle          from '../../../types/TextStyle';
+
+
+class ExerciseDeleteCommand extends CommanderCommand {
+    protected commandName: string = 'delete';
+
+    protected defineCommand(): void {
+        this.command
+            .description('delete an exercise')
+            .argument('id or url', 'id or url of the exercise')
+            .action(this.commandAction.bind(this));
+    }
+
+    private async dataRetrieval() {
+        console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...'));
+
+        await AccessesHelper.checkStudent();
+    }
+
+    private async deleteExercise(exerciseIdOrUrl: string) {
+        console.log(TextStyle.BLOCK('Please wait while we are deleting the exercise...'));
+
+        await DojoBackendManager.deleteExercise(exerciseIdOrUrl);
+    }
+
+    protected async commandAction(exerciseIdOrUrl: string): Promise<void> {
+        try {
+            await this.dataRetrieval();
+            await this.deleteExercise(exerciseIdOrUrl);
+        } catch ( e ) { /* Do nothing */ }
+    }
+}
+
+
+export default new ExerciseDeleteCommand();
diff --git a/NodeApp/src/commander/tag/TagCommand.ts b/NodeApp/src/commander/tag/TagCommand.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7d0698aa10076825a6ba0548203cc315aaa1c4bc
--- /dev/null
+++ b/NodeApp/src/commander/tag/TagCommand.ts
@@ -0,0 +1,27 @@
+import CommanderCommand   from '../CommanderCommand';
+import TagCreateCommand   from './subcommands/TagCreateCommand';
+import TagDelete          from './subcommands/TagDeleteCommand';
+import TagProposalCommand from './subcommands/proposal/TagProposalCommand';
+
+
+class TagCommand extends CommanderCommand {
+    protected commandName: string = 'tag';
+
+    protected defineCommand() {
+        this.command
+            .description('manage tags');
+    }
+
+    protected defineSubCommands() {
+        TagCreateCommand.registerOnCommand(this.command);
+        TagDelete.registerOnCommand(this.command);
+        TagProposalCommand.registerOnCommand(this.command);
+    }
+
+    protected async commandAction(): Promise<void> {
+        // No action
+    }
+}
+
+
+export default new TagCommand();
\ No newline at end of file
diff --git a/NodeApp/src/commander/tag/subcommands/TagCreateCommand.ts b/NodeApp/src/commander/tag/subcommands/TagCreateCommand.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fcbf63e76e94c741e2f08c1f0f2603d9cdd29990
--- /dev/null
+++ b/NodeApp/src/commander/tag/subcommands/TagCreateCommand.ts
@@ -0,0 +1,60 @@
+import CommanderCommand   from '../../CommanderCommand';
+import DojoBackendManager from '../../../managers/DojoBackendManager';
+import { Option }         from 'commander';
+import TextStyle          from '../../../types/TextStyle';
+import SessionManager     from '../../../managers/SessionManager';
+import ora                from 'ora';
+
+
+type CommandOptions = { name: string, type: 'Language' | 'Framework' | 'Theme' | 'UserDefined' }
+
+
+class TagCreateCommand extends CommanderCommand {
+    protected commandName: string = 'create';
+
+    protected defineCommand() {
+        this.command
+            .description('create a new tag')
+            .requiredOption('-n, --name <name>', 'name of the tag')
+            .addOption(new Option('-t, --type <type>', 'type of the tag').choices([ 'Language', 'Framework', 'Theme', 'UserDefined' ]).makeOptionMandatory(true))
+            .action(this.commandAction.bind(this));
+    }
+
+    private async dataRetrieval(options: CommandOptions) {
+        console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...'));
+
+        const sessionResult = await SessionManager.testSession(true, [ 'admin' ]);
+
+        if ( !sessionResult ) {
+            throw new Error();
+        }
+
+        if ( options.type !== 'UserDefined' && !sessionResult.admin ) {
+            ora({
+                    text  : `Only admins can create non UserDefined tags`,
+                    indent: 4
+                }).start().fail();
+            throw new Error();
+        }
+    }
+
+    private async createTag(options: CommandOptions) {
+        console.log(TextStyle.BLOCK('Please wait while we are creating the tag...'));
+
+        const tag = await DojoBackendManager.createTag(options.name, options.type);
+        if ( !tag ) {
+            throw new Error();
+        }
+    }
+
+    protected async commandAction(options: CommandOptions): Promise<void> {
+        try {
+            await this.dataRetrieval(options);
+            await this.createTag(options);
+        } catch ( e ) { /* Do nothing */ }
+    }
+
+}
+
+
+export default new TagCreateCommand();
\ No newline at end of file
diff --git a/NodeApp/src/commander/tag/subcommands/TagDeleteCommand.ts b/NodeApp/src/commander/tag/subcommands/TagDeleteCommand.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dae2d18a27f8dc3d060e94678781d4b6b44bb370
--- /dev/null
+++ b/NodeApp/src/commander/tag/subcommands/TagDeleteCommand.ts
@@ -0,0 +1,40 @@
+import CommanderCommand   from '../../CommanderCommand';
+import DojoBackendManager from '../../../managers/DojoBackendManager';
+import TextStyle          from '../../../types/TextStyle';
+import AccessesHelper     from '../../../helpers/AccessesHelper';
+
+
+class TagDeleteCommand extends CommanderCommand {
+    protected commandName: string = 'delete';
+
+    protected defineCommand() {
+        this.command
+            .description('Delete a tag')
+            .argument('<name>', 'name of the tag')
+            .action(this.commandAction.bind(this));
+    }
+
+    private async dataRetrieval() {
+        console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...'));
+
+        await AccessesHelper.checkAdmin();
+    }
+
+    private async deleteTag(name: string) {
+        console.log(TextStyle.BLOCK('Please wait while we are deleting the tag...'));
+
+        if ( !await DojoBackendManager.deleteTag(name) ) {
+            throw new Error();
+        }
+    }
+
+    protected async commandAction(name: string): Promise<void> {
+        try {
+            await this.dataRetrieval();
+            await this.deleteTag(name);
+        } catch ( e ) { /* Do nothing */ }
+    }
+}
+
+
+export default new TagDeleteCommand();
\ No newline at end of file
diff --git a/NodeApp/src/commander/tag/subcommands/proposal/TagProposalCommand.ts b/NodeApp/src/commander/tag/subcommands/proposal/TagProposalCommand.ts
new file mode 100644
index 0000000000000000000000000000000000000000..304a64733e7cd63947c7e9dc6cd2d4861d1dd28d
--- /dev/null
+++ b/NodeApp/src/commander/tag/subcommands/proposal/TagProposalCommand.ts
@@ -0,0 +1,28 @@
+import CommanderCommand          from '../../../CommanderCommand';
+import TagProposalListCommand    from './subcommands/TagProposalListCommand';
+import TagProposalCreateCommand  from './subcommands/TagProposalCreateCommand';
+import TagProposalApproveCommand from './subcommands/TagProposalApproveCommand';
+import TagProposalDeclineCommand from './subcommands/TagProposalDeclineCommand';
+
+
+class TagProposalCommand extends CommanderCommand {
+    protected commandName: string = 'proposal';
+
+    protected defineCommand() {
+        this.command.description('manage tag proposals');
+    }
+
+    protected defineSubCommands() {
+        TagProposalListCommand.registerOnCommand(this.command);
+        TagProposalCreateCommand.registerOnCommand(this.command);
+        TagProposalApproveCommand.registerOnCommand(this.command);
+        TagProposalDeclineCommand.registerOnCommand(this.command);
+    }
+
+    protected async commandAction(): Promise<void> {
+        // No action
+    }
+}
+
+
+export default new TagProposalCommand();
\ No newline at end of file
diff --git a/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalAnswerCommand.ts b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalAnswerCommand.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b0b1523d754264c62179398b94070974ab8e94bf
--- /dev/null
+++ b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalAnswerCommand.ts
@@ -0,0 +1,44 @@
+import CommanderCommand   from '../../../../CommanderCommand';
+import DojoBackendManager from '../../../../../managers/DojoBackendManager';
+import TextStyle          from '../../../../../types/TextStyle';
+import AccessesHelper     from '../../../../../helpers/AccessesHelper';
+
+
+type CommandOptions = { commentary?: string }
+
+
+abstract class TagProposalAnswerCommand extends CommanderCommand {
+    protected abstract state: 'Approved' | 'Declined';
+
+    protected defineCommand() {
+        this.command
+            .description(`${ this.state === 'Approved' ? 'Approve' : 'Decline' } a tag proposition`)
+            .argument('<name>', 'name of the tag proposition')
+            .option('-c, --commentary <comment>', 'add a commentary to the answer')
+            .action(this.commandAction.bind(this));
+    }
+
+    private async dataRetrieval() {
+        console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...'));
+
+        await AccessesHelper.checkAdmin();
+    }
+
+    private async answerTag(name: string, options: CommandOptions) {
+        console.log(TextStyle.BLOCK('Please wait while we are answering to the tag proposal...'));
+
+        if ( !await DojoBackendManager.answerTagProposal(name, this.state, options.commentary ?? '') ) {
+            throw new Error();
+        }
+    }
+
+    protected async commandAction(name: string, options: CommandOptions): Promise<void> {
+        try {
+            await this.dataRetrieval();
+            await this.answerTag(name, options);
+        } catch ( e ) { /* Do nothing */ }
+    }
+}
+
+
+export default TagProposalAnswerCommand;
\ No newline at end of file
diff --git a/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalApproveCommand.ts b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalApproveCommand.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e97b32310dfc84065f54680c383ee693b55f08d9
--- /dev/null
+++ b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalApproveCommand.ts
@@ -0,0 +1,10 @@
+import TagProposalAnswerCommand from './TagProposalAnswerCommand';
+
+
+class TagProposalApproveCommand extends TagProposalAnswerCommand {
+    protected commandName: string = 'approve';
+    protected state: 'Approved' | 'Declined' = 'Approved';
+}
+
+
+export default new TagProposalApproveCommand();
\ No newline at end of file
diff --git a/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalCreateCommand.ts b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalCreateCommand.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d34a4bc9d20ffffcc1962b976bb5bf5c5983d8d7
--- /dev/null
+++ b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalCreateCommand.ts
@@ -0,0 +1,46 @@
+import CommanderCommand   from '../../../../CommanderCommand';
+import DojoBackendManager from '../../../../../managers/DojoBackendManager';
+import { Option }         from 'commander';
+import TextStyle          from '../../../../../types/TextStyle';
+import AccessesHelper     from '../../../../../helpers/AccessesHelper';
+
+
+type CommandOptions = { name: string, type: 'Language' | 'Framework' | 'Theme' | 'UserDefined' }
+
+
+class TagProposalCreateCommand extends CommanderCommand {
+    protected commandName: string = 'create';
+
+    protected defineCommand() {
+        this.command
+            .description('Propose a new tag')
+            .requiredOption('-n, --name <name>', 'name of the tag')
+            .addOption(new Option('-t, --type <type>', 'type of the tag').choices([ 'Language', 'Framework', 'Theme', 'UserDefined' ]).makeOptionMandatory(true))
+            .action(this.commandAction.bind(this));
+    }
+
+    private async dataRetrieval() {
+        console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...'));
+
+        await AccessesHelper.checkTeachingStaff();
+    }
+
+    private async createTagProposal(options: CommandOptions) {
+        console.log(TextStyle.BLOCK('Please wait while we are creating the tag proposal...'));
+
+        const tag = await DojoBackendManager.createTagProposal(options.name, options.type);
+        if ( !tag ) {
+            throw new Error();
+        }
+    }
+
+    protected async commandAction(options: CommandOptions): Promise<void> {
+        try {
+            await this.dataRetrieval();
+            await this.createTagProposal(options);
+        } catch ( e ) { /* Do nothing */ }
+    }
+}
+
+
+export default new TagProposalCreateCommand();
\ No newline at end of file
diff --git a/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalDeclineCommand.ts b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalDeclineCommand.ts
new file mode 100644
index 0000000000000000000000000000000000000000..aae1ccc120b1011cc96a0f0429b806574d6508de
--- /dev/null
+++ b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalDeclineCommand.ts
@@ -0,0 +1,10 @@
+import TagProposalAnswerCommand from './TagProposalAnswerCommand';
+
+
+class TagProposalDeclineCommand extends TagProposalAnswerCommand {
+    protected commandName: string = 'decline';
+    protected state: 'Approved' | 'Declined' = 'Declined';
+}
+
+
+export default new TagProposalDeclineCommand();
\ No newline at end of file
diff --git a/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalListCommand.ts b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalListCommand.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f73bbd0caaf0d7ed01e35d02f419df7b9b90d934
--- /dev/null
+++ b/NodeApp/src/commander/tag/subcommands/proposal/subcommands/TagProposalListCommand.ts
@@ -0,0 +1,68 @@
+import CommanderCommand   from '../../../../CommanderCommand';
+import TagProposal        from '../../../../../sharedByClients/models/TagProposal';
+import DojoBackendManager from '../../../../../managers/DojoBackendManager';
+import TextStyle          from '../../../../../types/TextStyle';
+import AccessesHelper     from '../../../../../helpers/AccessesHelper';
+import { Option }         from 'commander';
+import ora                from 'ora';
+import Table              from 'cli-table3';
+
+
+type CommandOptions = { state: 'PendingApproval' | 'Approved' | 'Declined' }
+
+
+class TagProposalListCommand extends CommanderCommand {
+    protected commandName: string = 'list';
+
+    protected defineCommand() {
+        this.command
+            .description('Get a tag proposition')
+            .addOption(new Option('-s, --state <state>', 'state of the tag proposal').choices([ 'PendingApproval', 'Approved', 'Declined' ]).default('PendingApproval'))
+            .action(this.commandAction.bind(this));
+    }
+
+    private async dataRetrieval() {
+        console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...'));
+
+        await AccessesHelper.checkAdmin();
+    }
+
+    private async listTagProposals(options: CommandOptions) {
+        console.log(TextStyle.BLOCK('Please wait while we are creating the tag...'));
+
+        const spinner: ora.Ora = ora('Retrieving tag proposals...');
+
+        const tags = await DojoBackendManager.getTagProposals(options.state);
+
+        if ( tags && tags.length > 0 ) {
+            spinner.succeed(`Tag proposals retrieved. Here is the list of tag proposal with '${ options.state }' state:`);
+
+            const table = new Table({
+                                        head: [ 'Name', 'Type', 'Details' ]
+                                    });
+
+            tags.forEach((tag: TagProposal) => {
+                table.push([ tag.name, tag.type, tag.details ]);
+            });
+
+            console.log(table.toString());
+        } else if ( tags ) {
+            spinner.fail(`There is no tag proposal with '${ options.state }' state.`);
+            throw new Error();
+        } else {
+            spinner.fail('Failed to retrieve tag proposals.');
+            throw new Error();
+        }
+    }
+
+    protected async commandAction(options: CommandOptions): Promise<void> {
+        try {
+            await this.dataRetrieval();
+            await this.listTagProposals(options);
+        } catch ( e ) { /* Do nothing */ }
+    }
+
+}
+
+
+export default new TagProposalListCommand();
\ No newline at end of file
diff --git a/NodeApp/src/helpers/AccessesHelper.ts b/NodeApp/src/helpers/AccessesHelper.ts
index 1be0777e85227aae8e950cb6974e5df2866e72ba..a07b7aa24a40229be5460f1126e0e99eb4455138 100644
--- a/NodeApp/src/helpers/AccessesHelper.ts
+++ b/NodeApp/src/helpers/AccessesHelper.ts
@@ -3,32 +3,28 @@ import GitlabManager  from '../managers/GitlabManager.js';
 
 
 class AccessesHelper {
-    async checkStudent(testGitlab: boolean = false): Promise<boolean> {
-        const sessionResult = await SessionManager.testSession(true, [ 'student' ]);
+    private async checkAccess(accessName: string, testGitlab: boolean = false) {
+        const sessionResult = await SessionManager.testSession(true, [ accessName ]);
 
-        if ( !sessionResult ) {
-            return false;
+        if ( !sessionResult || !(sessionResult as unknown as { [key: string]: boolean })[accessName] ) {
+            throw new Error();
         }
 
-        if ( testGitlab ) {
-            return (await GitlabManager.testToken(true)).every(result => result);
-        } else {
-            return true;
+        if ( testGitlab && !(await GitlabManager.testToken(true)).every(result => result) ) {
+            throw new Error();
         }
     }
 
-    async checkTeachingStaff(testGitlab: boolean = false): Promise<boolean> {
-        const sessionResult = await SessionManager.testSession(true, [ 'teachingStaff' ]);
+    async checkStudent(testGitlab: boolean = false) {
+        await this.checkAccess('student', testGitlab);
+    }
 
-        if ( !sessionResult || !sessionResult.teachingStaff ) {
-            return false;
-        }
+    async checkTeachingStaff(testGitlab: boolean = false) {
+        await this.checkAccess('teachingStaff', testGitlab);
+    }
 
-        if ( testGitlab ) {
-            return (await GitlabManager.testToken(true)).every(result => result);
-        } else {
-            return true;
-        }
+    async checkAdmin(testGitlab: boolean = false) {
+        await this.checkAccess('admin', testGitlab);
     }
 }
 
diff --git a/NodeApp/src/managers/DojoBackendManager.ts b/NodeApp/src/managers/DojoBackendManager.ts
index 8c8a5a9685c1ab1fb2cabbfc4740f2914b54beae..0a878367581dbd7022567590557cf88222c05d00 100644
--- a/NodeApp/src/managers/DojoBackendManager.ts
+++ b/NodeApp/src/managers/DojoBackendManager.ts
@@ -11,6 +11,9 @@ import DojoStatusCode        from '../shared/types/Dojo/DojoStatusCode.js';
 import * as Gitlab           from '@gitbeaker/rest';
 import DojoBackendHelper     from '../sharedByClients/helpers/Dojo/DojoBackendHelper.js';
 import GitlabPipelineStatus  from '../shared/types/Gitlab/GitlabPipelineStatus.js';
+import Tag                   from '../sharedByClients/models/Tag';
+import TagProposal           from '../sharedByClients/models/TagProposal';
+import Result                from '../sharedByClients/models/Result';
 
 
 class DojoBackendManager {
@@ -59,6 +62,15 @@ class DojoBackendManager {
                     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;
+                    case DojoStatusCode.TAG_ONLY_ADMIN_CREATION:
+                        spinner.fail(`Only admins can create non UserDefined tags.`);
+                        break;
+                    case DojoStatusCode.TAG_WITH_ACTIVE_LINK_DELETION:
+                        spinner.fail(`This tag is used in resources (e.g. assignments). Please remove this tag from these resources before deleting it.`);
+                        break;
+                    case DojoStatusCode.TAG_PROPOSAL_ANSWER_NOT_PENDING:
+                        spinner.fail(`This tag proposal have already been answered.`);
+                        break;
                     default:
                         if ( otherErrorHandler ) {
                             otherErrorHandler(error, spinner, verbose);
@@ -73,7 +85,6 @@ class DojoBackendManager {
         }
     }
 
-
     public async login(gitlabTokens: GitlabToken): Promise<User | undefined> {
         try {
             return (await axios.post<DojoBackendResponse<User>>(DojoBackendHelper.getApiUrl(ApiRoute.LOGIN), {
@@ -249,6 +260,190 @@ class DojoBackendManager {
             return false;
         }
     }
+
+    public async createTag(name: string, type: string, verbose: boolean = true): Promise<Tag | undefined> {
+        const spinner: ora.Ora = ora('Creating tag...');
+
+        if ( verbose ) {
+            spinner.start();
+        }
+
+        try {
+            const response = await axios.post<DojoBackendResponse<Tag>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_CREATE), {
+                name: name,
+                type: type
+            });
+
+            if ( verbose ) {
+                spinner.succeed(`Tag successfully created`);
+            }
+
+            return response.data.data;
+        } catch ( error ) {
+            this.handleApiError(error, spinner, verbose, `Tag creation error: ${ error }`);
+
+            return undefined;
+        }
+    }
+
+    public async deleteTag(name: string, verbose: boolean = true): Promise<boolean> {
+        const spinner: ora.Ora = ora('Deleting tag...');
+
+        if ( verbose ) {
+            spinner.start();
+        }
+
+        try {
+            await axios.delete<DojoBackendResponse<Tag>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_DELETE, { tagName: name }));
+
+            if ( verbose ) {
+                spinner.succeed(`Tag successfully deleted`);
+            }
+
+            return true;
+        } catch ( error ) {
+            this.handleApiError(error, spinner, verbose, `Tag deletion error: ${ error }`);
+
+            return false;
+        }
+    }
+
+    public async getTagProposals(state: string | undefined): Promise<Array<TagProposal> | undefined> {
+        try {
+            return (await axios.get<DojoBackendResponse<Array<TagProposal>>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_PROPOSAL_GET_CREATE), { params: { stateFilter: state } })).data.data;
+        } catch ( error ) {
+            return undefined;
+        }
+    }
+
+    public async createTagProposal(name: string, type: string, verbose: boolean = true): Promise<TagProposal | undefined> {
+        const spinner: ora.Ora = ora('Creating tag...');
+
+        if ( verbose ) {
+            spinner.start();
+        }
+
+        try {
+            const response = await axios.post<DojoBackendResponse<TagProposal>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_PROPOSAL_GET_CREATE), {
+                name: name,
+                type: type
+            });
+
+            if ( verbose ) {
+                spinner.succeed(`Tag proposal successfully created`);
+            }
+
+            return response.data.data;
+        } catch ( error ) {
+            this.handleApiError(error, spinner, verbose, `Tag proposal creation error: ${ error }`);
+
+            return undefined;
+        }
+    }
+
+    public async answerTagProposal(tagProposalName: string, state: 'Approved' | 'Declined', details: string, verbose: boolean = true): Promise<boolean> {
+        const spinner: ora.Ora = ora('Answering tag proposal...');
+
+        if ( verbose ) {
+            spinner.start();
+        }
+
+        try {
+            await axios.patch<DojoBackendResponse<TagProposal>>(DojoBackendHelper.getApiUrl(ApiRoute.TAG_PROPOSAL_UPDATE, { tagName: tagProposalName }), {
+                state  : state,
+                details: details
+            });
+
+            if ( verbose ) {
+                spinner.succeed(`Tag proposal ${ state.toLowerCase() } with success`);
+            }
+
+            return true;
+        } catch ( error ) {
+            this.handleApiError(error, spinner, verbose, `Tag proposal answer error: ${ error }`);
+
+            return false;
+        }
+    }
+
+    public async getUserExercises(): Promise<Array<Exercise> | undefined> {
+        try {
+            const response = await axios.get<DojoBackendResponse<Array<Exercise>>>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_LIST));
+            return response.data.data;
+        } catch ( error ) {
+            console.error('Error fetching user exercises:', error);
+            return undefined;
+        }
+    }
+
+    public async getExerciseDetails(exerciseIdOrUrl: string): Promise<Exercise | undefined> {
+        try {
+
+            const response = await axios.get<Exercise>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_DETAILS_GET, {
+                exerciseIdOrUrl: exerciseIdOrUrl
+            }));
+            return response.data;
+        } catch ( error ) {
+            console.error('Error fetching exercise details:', error);
+            return undefined;
+        }
+    }
+
+    public async deleteExercise(exerciseIdOrUrl: string, verbose: boolean = true): Promise<void> {
+        const spinner: ora.Ora = ora('Deleting exercise...');
+
+        if ( verbose ) {
+            spinner.start();
+        }
+
+        try {
+            await axios.delete<DojoBackendResponse<Exercise>>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_GET_DELETE, {
+                exerciseIdOrUrl: exerciseIdOrUrl
+            }));
+
+            if ( verbose ) {
+                spinner.succeed(`Exercise deleted with success`);
+            }
+        } catch ( error ) {
+            this.handleApiError(error, spinner, verbose, `Exercise deleting error: ${ error }`);
+
+            throw error;
+        }
+    }
+
+    public async getExerciseMembers(exerciseIdOrUrl: string): Promise<Array<User>> {
+        return (await axios.get<DojoBackendResponse<Array<User>>>(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_MEMBERS_GET, {
+            exerciseIdOrUrl: exerciseIdOrUrl
+        }))).data.data;
+    }
+
+    public async getExerciseResults(exerciseIdOrUrl: string): Promise<Array<Result>> {
+        try {
+            const response = await axios.get(DojoBackendHelper.getApiUrl(ApiRoute.EXERCISE_RESULTS, {
+                exerciseIdOrUrl: exerciseIdOrUrl
+            }));
+
+            return response.data as Array<Result>;
+        } catch ( error ) {
+            console.error('Error fetching exercise results:', error);
+            return [];
+        }
+    }
+
+    public async getUsers(roleFilter?: string): Promise<Array<User> | undefined> {
+        try {
+            const response = await axios.get<DojoBackendResponse<Array<User>>>(DojoBackendHelper.getApiUrl(ApiRoute.USER_LIST), { params: roleFilter ? { roleFilter: roleFilter } : {} });
+
+            return response.data.data;
+        } catch ( error ) {
+            console.error('Error fetching professors:', error);
+            return undefined;
+        }
+    }
+
+    public async getTeachers(): Promise<Array<User> | undefined> {
+        return this.getUsers('teacher');
+    }
 }
 
 
diff --git a/NodeApp/src/shared b/NodeApp/src/shared
index c2afa861bf6306ddec79ffd465a4c7b0edcd3453..bf75a99ba472386daa111c2fefbe69a4272ef48c 160000
--- a/NodeApp/src/shared
+++ b/NodeApp/src/shared
@@ -1 +1 @@
-Subproject commit c2afa861bf6306ddec79ffd465a4c7b0edcd3453
+Subproject commit bf75a99ba472386daa111c2fefbe69a4272ef48c
diff --git a/NodeApp/src/sharedByClients b/NodeApp/src/sharedByClients
index 55a94e77db69635e1ca837a52de29cb04d0b4138..4e33e70f6035898f119369ae5db784d51d8298a0 160000
--- a/NodeApp/src/sharedByClients
+++ b/NodeApp/src/sharedByClients
@@ -1 +1 @@
-Subproject commit 55a94e77db69635e1ca837a52de29cb04d0b4138
+Subproject commit 4e33e70f6035898f119369ae5db784d51d8298a0