diff --git a/LICENSE b/LICENSE
index 41400bc9954374f2d1e66516a87524cc555359cf..ca4fe13006a1fcb8fa078da3d6ebbe34213cb20a 100644
--- a/LICENSE
+++ b/LICENSE
@@ -629,7 +629,7 @@ to attach them to the start of each source file to most effectively
 state the exclusion of warranty; and each file should have at least
 the "copyright" line and a pointer to where the full notice is found.
 
-    NodeClientSharedCode
+    NodeSharedCode
     Copyright (C) 2023  ISC / projects / Dojo / Projects / Shared
 
     This program is free software: you can redistribute it and/or modify
diff --git a/README.md b/README.md
index a0170e24c2a493cf92fd40f70341be88f95ef289..e0c65cf50afb02dc946e602fd08b2e5102f96e04 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,21 @@
 # NodeSharedCode
 
-This repo contains some code that can be shared across clients projects of Dojo.
+This repo contains some code that can be shared across node projects of Dojo.
 
 ## Prerequisites
 
-These shared modules are needed :
-
-- `NodeSharedCode` imported in parent directory with directory name `shared`
-
 These packages are needed :
 
-- `boxen@5.1`
-- `chalk@4.1`
-- `fs-extra`
-- `yaml`
+- `json5`
+- `tar-stream`
+- `winston`
+- `zod`
+- `zod-validation-error`
 
 ## How to use it
 
 By adding this repo as submodule
 
 ```bash
-git submodule add ../../shared/nodeclientsharedcode.git sharedByClients
+git submodule add ../../shared/nodesharedcode.git shared
 ```
\ No newline at end of file
diff --git a/config/SharedConfig.ts b/config/SharedConfig.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0f8f8b0397b4a855f9a761934e7a37f5a9e6b9a3
--- /dev/null
+++ b/config/SharedConfig.ts
@@ -0,0 +1,47 @@
+class SharedConfig {
+    public readonly production: boolean;
+    public debug: boolean = false;
+
+    public readonly logsFolder: string;
+
+    public gitlab: {
+        URL: string, apiURL: string
+    };
+
+    public readonly login: {
+        gitlab: {
+            client: {
+                id: string
+            }, url: {
+                redirect: string, token: string
+            }
+        }
+    };
+
+
+    constructor() {
+        this.production = process.env.NODE_ENV === 'production';
+
+        this.logsFolder = process.env.LOGS_FOLDER || '';
+
+        this.gitlab = {
+            URL   : process.env.GITLAB_URL || '',
+            apiURL: process.env.GITLAB_API_URL || ''
+        };
+
+        this.login = {
+            gitlab: {
+                client: {
+                    id: process.env.LOGIN_GITLAB_CLIENT_ID || ''
+                },
+                url   : {
+                    redirect: process.env.LOGIN_GITLAB_URL_REDIRECT || '',
+                    token   : process.env.LOGIN_GITLAB_URL_TOKEN || ''
+                }
+            }
+        };
+    }
+}
+
+
+export default new SharedConfig();
diff --git a/helpers/ArchiveHelper.ts b/helpers/ArchiveHelper.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f4449d6e8f4953227e0b422bb0a647a85432351c
--- /dev/null
+++ b/helpers/ArchiveHelper.ts
@@ -0,0 +1,69 @@
+import fs           from 'node:fs';
+import path         from 'node:path';
+import tar          from 'tar-stream';
+import stream       from 'node:stream';
+import { Writable } from 'stream';
+import zlib         from 'zlib';
+
+
+class ArchiveHelper {
+    private async explore(absoluteBasePath: string, rootPath: string, pack: tar.Pack) {
+        for ( let file of await fs.promises.readdir(rootPath) ) {
+            if ( file === 'output.tar' ) {
+                continue;
+            }
+            file = path.join(rootPath, file);
+            const stat = await fs.promises.stat(file);
+            if ( stat.isDirectory() ) {
+                await this.explore(absoluteBasePath, file, pack);
+                continue;
+            }
+            const entry = pack.entry({
+                                         name: file.replace(absoluteBasePath, ''),
+                                         size: stat.size
+                                     }, (err) => {
+                if ( err ) {
+                    throw err;
+                }
+            });
+            const stream = fs.createReadStream(file);
+            stream.pipe(entry);
+        }
+    }
+
+    private async compress(folderPath: string, tarDataStream: stream.Writable) {
+        const pack = tar.pack();
+
+        await this.explore(folderPath, folderPath, pack);
+
+        pack.pipe(zlib.createGzip()).pipe(tarDataStream);
+        pack.finalize();
+    }
+
+    public async getBase64(folderPath: string): Promise<string> {
+        let data: string;
+        const tarDataStream = new stream.Writable({
+                                                      write(this: Writable, chunk: Buffer, _encoding: BufferEncoding, next: (error?: Error | null) => void) {
+                                                          if ( data ) {
+                                                              data += chunk.toString('hex');
+                                                          } else {
+                                                              data = chunk.toString('hex');
+                                                          }
+                                                          next();
+                                                      }
+                                                  });
+
+        await this.compress(folderPath, tarDataStream);
+
+        data = await (new Promise((resolve) => {
+            tarDataStream.on('close', () => {
+                resolve(data);
+            });
+        }));
+
+        return Buffer.from(data, 'hex').toString('base64');
+    }
+}
+
+
+export default new ArchiveHelper();
\ No newline at end of file
diff --git a/helpers/Dojo/SharedAssignmentHelper.ts b/helpers/Dojo/SharedAssignmentHelper.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b103f8cc526ed60a9aa6b7c5276557bcf1ab6635
--- /dev/null
+++ b/helpers/Dojo/SharedAssignmentHelper.ts
@@ -0,0 +1,56 @@
+import AssignmentFile       from '../../types/Dojo/AssignmentFile';
+import GitlabPipelineStatus from '../../types/Gitlab/GitlabPipelineStatus';
+import DojoStatusCode       from '../../types/Dojo/DojoStatusCode';
+import GitlabPipeline       from '../../types/Gitlab/GitlabPipeline';
+import SharedGitlabManager  from '../../managers/SharedGitlabManager';
+import Json5FileValidator   from '../Json5FileValidator';
+
+
+class SharedAssignmentHelper {
+    validateDescriptionFile(filePathOrStr: string, isFile: boolean = true, version: number = 1): { content: AssignmentFile | undefined, isValid: boolean, error: string | null } {
+        switch ( version ) {
+            case 1:
+                return Json5FileValidator.validateFile(AssignmentFile, filePathOrStr, isFile);
+            default:
+                return {
+                    content: undefined,
+                    isValid: false,
+                    error  : `Version ${ version } not supported`
+                };
+        }
+    }
+
+    async isPublishable(repositoryId: number): Promise<{ isPublishable: boolean, lastPipeline: GitlabPipeline | null, status?: { code: DojoStatusCode, message: string } }> {
+        const pipelines = await SharedGitlabManager.getRepositoryPipelines(repositoryId, 'main');
+        if ( pipelines.length > 0 ) {
+            const lastPipeline = pipelines[0];
+            if ( lastPipeline.status != GitlabPipelineStatus.SUCCESS ) {
+                return {
+                    isPublishable: false,
+                    lastPipeline : lastPipeline,
+                    status       : {
+                        code   : DojoStatusCode.ASSIGNMENT_PUBLISH_PIPELINE_FAILED,
+                        message: `Last pipeline status is not "${ GitlabPipelineStatus.SUCCESS }" but "${ lastPipeline.status }".`
+                    }
+                };
+            } else {
+                return {
+                    isPublishable: true,
+                    lastPipeline : lastPipeline
+                };
+            }
+        } else {
+            return {
+                isPublishable: false,
+                lastPipeline : null,
+                status       : {
+                    code   : DojoStatusCode.ASSIGNMENT_PUBLISH_NO_PIPELINE,
+                    message: 'No pipeline found for this assignment.'
+                }
+            };
+        }
+    }
+}
+
+
+export default new SharedAssignmentHelper();
\ No newline at end of file
diff --git a/helpers/Dojo/SharedExerciseHelper.ts b/helpers/Dojo/SharedExerciseHelper.ts
new file mode 100644
index 0000000000000000000000000000000000000000..97ccf7c9a39682ba8b14fa65b54a89d9898ebf3c
--- /dev/null
+++ b/helpers/Dojo/SharedExerciseHelper.ts
@@ -0,0 +1,4 @@
+class SharedExerciseHelper {}
+
+
+export default new SharedExerciseHelper();
\ No newline at end of file
diff --git a/helpers/Json5FileValidator.ts b/helpers/Json5FileValidator.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c9be717bbf249bdb6230b187deb1e1fc948a2274
--- /dev/null
+++ b/helpers/Json5FileValidator.ts
@@ -0,0 +1,46 @@
+import JSON5            from 'json5';
+import fs               from 'fs';
+import { z, ZodError }  from 'zod';
+import { fromZodError } from 'zod-validation-error';
+
+
+class Json5FileValidator {
+    validateFile<T>(schema: z.ZodType<T>, filePathOrStr: string, isFile: boolean = true, resultSanitizer: (value: T) => T = value => value): { content: T | undefined, isValid: boolean, error: string | null } {
+        let parsedInput: T;
+
+        try {
+            parsedInput = JSON5.parse(isFile ? fs.readFileSync(filePathOrStr, 'utf8') : filePathOrStr);
+        } catch ( error ) {
+            return {
+                content: undefined,
+                isValid: false,
+                error  : `JSON5 invalid : ${ JSON.stringify(error) }`
+            };
+        }
+
+        try {
+            return {
+                content: resultSanitizer(schema.parse(parsedInput) as unknown as T),
+                isValid: true,
+                error  : null
+            };
+        } catch ( error ) {
+            if ( error instanceof ZodError ) {
+                return {
+                    content: parsedInput,
+                    isValid: false,
+                    error  : fromZodError(error).toString()
+                };
+            }
+
+            return {
+                content: parsedInput,
+                isValid: false,
+                error  : `Unknown error : ${ JSON.stringify(error) }`
+            };
+        }
+    }
+}
+
+
+export default new Json5FileValidator();
\ No newline at end of file
diff --git a/helpers/LazyVal.ts b/helpers/LazyVal.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4613c4b535706f5853b060279ce46a957e3c3030
--- /dev/null
+++ b/helpers/LazyVal.ts
@@ -0,0 +1,29 @@
+class LazyVal<T> {
+    private val: T | undefined = undefined;
+
+    constructor(private valLoader: () => Promise<T> | T) {}
+
+    get value(): Promise<T> {
+        return new Promise<T>((resolve) => {
+            if ( this.val === undefined ) {
+                Promise.resolve(this.valLoader()).then((value: T) => {
+                    this.val = value;
+                    resolve(value);
+                });
+            } else {
+                resolve(this.val);
+            }
+        });
+    }
+
+    reset() {
+        this.val = undefined;
+    }
+
+    get isValueLoaded(): boolean {
+        return this.val !== undefined;
+    }
+}
+
+
+export default LazyVal;
diff --git a/helpers/Toolbox.ts b/helpers/Toolbox.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8a76bebb202e6bcbe46944e562924effdeb26afe
--- /dev/null
+++ b/helpers/Toolbox.ts
@@ -0,0 +1,56 @@
+import fs   from 'fs/promises';
+import path from 'path';
+
+
+class Toolbox {
+    public urlToPath(url: string): string {
+        return url.replace(/^([a-z]{3,5}:\/{2})?[a-z.@]+(:[0-9]{1,5})?.(.*)/, '$3').replace('.git', '');
+    }
+
+    /*
+     Source of getAllFiles and getTotalSize (modified for this project): https://coderrocketfuel.com/article/get-the-total-size-of-all-files-in-a-directory-using-node-js
+     */
+    private async getAllFiles(dirPath: string, arrayOfFiles: Array<string> = []): Promise<Array<string>> {
+        const files = await fs.readdir(dirPath);
+
+        await Promise.all(files.map(async file => {
+            if ( (await fs.stat(dirPath + '/' + file)).isDirectory() ) {
+                arrayOfFiles = await this.getAllFiles(dirPath + '/' + file, arrayOfFiles);
+            } else {
+                arrayOfFiles.push(path.join(dirPath, file));
+            }
+        }));
+
+        return arrayOfFiles;
+    }
+
+    private async getTotalSize(directoryPath: string): Promise<number> {
+        const arrayOfFiles = await this.getAllFiles(directoryPath);
+
+        let totalSize = 0;
+
+        for ( const filePath of arrayOfFiles ) {
+            totalSize += (await fs.stat(filePath)).size;
+        }
+
+        return totalSize;
+    }
+
+    get fs() {
+        return {
+            getAllFiles : this.getAllFiles.bind(this),
+            getTotalSize: this.getTotalSize.bind(this)
+        };
+    }
+
+    public snakeToCamel(str: string): string {
+        return str.toLowerCase().replace(/([-_][a-z])/g, (group: string) => group.toUpperCase().replace('-', '').replace('_', ''));
+    }
+
+    public getKeysWithPrefix(obj: object, prefix: string): Array<string> {
+        return Object.keys(obj).filter(key => key.startsWith(prefix));
+    }
+}
+
+
+export default new Toolbox();
diff --git a/helpers/TypeScriptExtensions.ts b/helpers/TypeScriptExtensions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fd730ad37daa50a1b6e855efaf3c48ce739faa05
--- /dev/null
+++ b/helpers/TypeScriptExtensions.ts
@@ -0,0 +1,76 @@
+declare global {
+    interface BigInt {
+        toJSON: () => string;
+    }
+
+
+    interface Array<T> {
+        removeObjectDuplicates: (getProperty: (item: T) => unknown) => Array<T>;
+    }
+
+
+    interface String {
+        toBoolean: () => boolean;
+        capitalizingFirstLetter: () => string;
+        capitalizeName: () => string;
+        convertWithEnvVars: () => string;
+    }
+}
+
+function registerAll() {
+    registerBigIntJson();
+    registerArrayRemoveObjectDuplicates();
+    registerStringToBoolean();
+    registerStringCapitalizingFirstLetter();
+    registerStringCapitalizeName();
+    registerStringConvertWithEnvVars();
+}
+
+function registerBigIntJson() {
+    BigInt.prototype.toJSON = function () {
+        return this.toString();
+    };
+}
+
+function registerArrayRemoveObjectDuplicates() {
+    Array.prototype.removeObjectDuplicates = function <T>(this: Array<T>, getProperty: (item: T) => unknown): Array<T> {
+        return this.reduce((accumulator: Array<T>, current: T) => {
+            if ( !accumulator.find((item: T) => getProperty(item) === getProperty(current)) ) {
+                accumulator.push(current);
+            }
+            return accumulator;
+        }, Array<T>());
+    };
+}
+
+function registerStringToBoolean() {
+    String.prototype.toBoolean = function (this: string): boolean {
+        const tmp = this.toLowerCase().trim();
+        return tmp === 'true' || tmp === '1';
+    };
+}
+
+function registerStringCapitalizingFirstLetter() {
+    String.prototype.capitalizingFirstLetter = function (this: string): string {
+        return this.charAt(0).toUpperCase() + this.slice(1);
+    };
+}
+
+function registerStringCapitalizeName() {
+    String.prototype.capitalizeName = function (this: string): string {
+        return this.trim().replace(/(?:^|\s|-)\S/g, s => s.toUpperCase());
+    };
+}
+
+function registerStringConvertWithEnvVars() {
+    String.prototype.convertWithEnvVars = function (this: string): string {
+        return this.replace(/\${?([a-zA-Z0-9_]+)}?/g, (_match: string, p1: string) => {
+            return process.env[p1] || '';
+        });
+    };
+}
+
+registerAll();
+
+
+export default null;
\ No newline at end of file
diff --git a/helpers/recursiveFilesStats/README.md b/helpers/recursiveFilesStats/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..f2e98d4319eb3ce841bd1b2bb57c1b393aa8f483
--- /dev/null
+++ b/helpers/recursiveFilesStats/README.md
@@ -0,0 +1,148 @@
+Source: recursive-readdir-files
+===
+Modified for Dojo
+===
+
+## Usage
+
+```js
+import recursiveReaddirFiles from 'recursive-readdir-files';
+
+
+const files = await recursiveReaddirFiles(process.cwd(), {
+    ignored: /\/(node_modules|\.git)/
+});
+
+// `files` is an array
+console.log(files);
+// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
+// [
+//   {
+//     dev: 16777233,
+//     mode: 33188,
+//     nlink: 1,
+//     uid: 501,
+//     gid: 20,
+//     rdev: 0,
+//     blksize: 4096,
+//     ino: 145023089,
+//     size: 89,
+//     blocks: 8,
+//     atimeMs: 1649303678077.934,
+//     mtimeMs: 1649303676847.1777,
+//     ctimeMs: 1649303676847.1777,
+//     birthtimeMs: 1649301118132.6782,
+//     atime: 2022-04-07T03:54:38.078Z,
+//     mtime: 2022-04-07T03:54:36.847Z,
+//     ctime: 2022-04-07T03:54:36.847Z,
+//     birthtime: 2022-04-07T03:11:58.133Z,
+//     name: 'watch.ts',
+//     path: '/Users/xxx/watch.ts',
+//     ext: 'ts'
+//   },
+//   // ...
+// ]
+```
+
+Or
+
+```js
+recursiveReaddirFiles(process.cwd(), {
+    ignored: /\/(node_modules|\.git)/
+}, (filepath, state) => {
+    console.log(filepath);
+    // 👉 /Users/xxx/watch.ts
+    console.log(state.isFile());      // 👉 true
+    console.log(state.isDirectory()); // 👉 false
+    console.log(state);
+    // ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
+    // {
+    //   dev: 16777233,
+    //   mode: 33188,
+    //   nlink: 1,
+    //   uid: 501,
+    //   gid: 20,
+    //   rdev: 0,
+    //   blksize: 4096,
+    //   ino: 145023089,
+    //   size: 89,
+    //   blocks: 8,
+    //   atimeMs: 1649303678077.934,
+    //   mtimeMs: 1649303676847.1777,
+    //   ctimeMs: 1649303676847.1777,
+    //   birthtimeMs: 1649301118132.6782,
+    //   atime: 2022-04-07T03:54:38.078Z,
+    //   mtime: 2022-04-07T03:54:36.847Z,
+    //   ctime: 2022-04-07T03:54:36.847Z,
+    //   birthtime: 2022-04-07T03:11:58.133Z,
+    //   name: 'watch.ts',
+    //   path: '/Users/xxx/watch.ts',
+    //   ext: 'ts'
+    // }
+})
+```
+
+## Options
+
+```ts
+export interface RecursiveReaddirFilesOptions {
+    /**
+     * Ignore files
+     * @example `/\/(node_modules|\.git)/`
+     */
+    ignored?: RegExp;
+    /**
+     * Specifies a list of `glob` patterns that match files to be included in compilation.
+     * @example `/(\.json)$/`
+     */
+    include?: RegExp;
+    /**
+     * Specifies a list of files to be excluded from compilation.
+     * @example `/(package\.json)$/`
+     */
+    exclude?: RegExp;
+    /** Provide filtering methods to filter data. */
+    filter?: (item: IFileDirStat) => boolean;
+    /** Do not give the absolute path but the relative one from the root folder */
+    replacePathByRelativeOne?: boolean;
+    /** Remove stats that are not necessary for transfert */
+    liteStats?: boolean;
+}
+```
+
+## Result
+
+```ts
+import fs from 'node:fs';
+
+
+export interface IFileDirStat extends Partial<fs.Stats> {
+    /**
+     * @example `/a/sum.jpg` => `sum.jpg`
+     */
+    name: string;
+    /**
+     * @example `/basic/src/utils/sum.ts`
+     */
+    path: string;
+    /**
+     * @example `/a/b.jpg` => `jpg`
+     */
+    ext?: string;
+}
+
+
+declare type Callback = (filepath: string, stat: IFileDirStat) => void;
+export default function recursiveReaddirFiles(rootPath: string, options?: RecursiveReaddirFilesOptions, callback?: Callback): Promise<IFileDirStat[]>;
+export { recursiveReaddirFiles };
+export declare const getStat: (filepath: string) => Promise<IFileDirStat>;
+/**
+ * Get ext
+ * @param {String} filePath `/a/b.jpg` => `jpg`
+ */
+export declare const getExt: (filePath: string) => string;
+```
+
+## License
+
+Licensed under the MIT License.
diff --git a/helpers/recursiveFilesStats/RecursiveFilesStats.ts b/helpers/recursiveFilesStats/RecursiveFilesStats.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a571aaac797de782ee6bc95d33ad9e75ee03b647
--- /dev/null
+++ b/helpers/recursiveFilesStats/RecursiveFilesStats.ts
@@ -0,0 +1,146 @@
+import fs   from 'node:fs';
+import path from 'node:path';
+
+
+export interface RecursiveReaddirFilesOptions {
+    /**
+     * Ignore files
+     * @example `/\/(node_modules|\.git)/`
+     */
+    ignored?: RegExp;
+    /**
+     * Specifies a list of `glob` patterns that match files to be included in compilation.
+     * @example `/(\.json)$/`
+     */
+    include?: RegExp;
+    /**
+     * Specifies a list of files to be excluded from compilation.
+     * @example `/(package\.json)$/`
+     */
+    exclude?: RegExp;
+    /** Provide filtering methods to filter data. */
+    filter?: (item: IFileDirStat) => boolean;
+    /** Do not give the absolute path but the relative one from the root folder */
+    replacePathByRelativeOne?: boolean;
+    /** Remove stats that are not necessary for transfert */
+    liteStats?: boolean;
+}
+
+
+export interface IFileDirStat extends Partial<fs.Stats> {
+    /**
+     * @example `/a/sum.jpg` => `sum.jpg`
+     */
+    name: string;
+    /**
+     * @example `/basic/src/utils/sum.ts`
+     */
+    path: string;
+    /**
+     * @example `/a/b.jpg` => `jpg`
+     */
+    ext?: string;
+}
+
+
+type Callback = (filepath: string, stat: IFileDirStat) => void;
+
+
+class RecursiveFilesStats {
+    async explore(rootPath: string, options: RecursiveReaddirFilesOptions = {}, callback?: Callback): Promise<IFileDirStat[]> {
+        return this.getFiles(`${ path.resolve(rootPath) }/`, rootPath, options, [], callback);
+    }
+
+    private async getFiles(absoluteBasePath: string, rootPath: string, options: RecursiveReaddirFilesOptions = {}, files: IFileDirStat[] = [], callback?: Callback): Promise<IFileDirStat[]> {
+        const {
+                  ignored, include, exclude, filter
+              } = options;
+        const filesData = await fs.promises.readdir(rootPath);
+        const fileDir: IFileDirStat[] = filesData.map((file) => ({
+            name: file, path: path.join(rootPath, file)
+        })).filter((item) => {
+            if ( include && include.test(item.path) ) {
+                return true;
+            }
+            if ( exclude && exclude.test(item.path) ) {
+                return false;
+            }
+            if ( ignored ) {
+                return !ignored.test(item.path);
+            }
+            return true;
+        });
+        if ( callback ) {
+            fileDir.map(async (item: IFileDirStat) => {
+                const stat = await this.getStat(item.path, absoluteBasePath, options);
+                if ( stat.isDirectory!() ) {
+                    await this.getFiles(absoluteBasePath, item.path, options, [], callback);
+                }
+                callback(item.path, stat);
+            });
+        } else {
+            await Promise.all(fileDir.map(async (item: IFileDirStat) => {
+                const stat = await this.getStat(item.path, absoluteBasePath, options);
+                if ( stat.isDirectory!() ) {
+                    const arr = await this.getFiles(absoluteBasePath, item.path, options, []);
+                    files = files.concat(arr);
+                } else if ( stat.isFile!() ) {
+                    files.push(stat);
+                }
+            }));
+        }
+        return files.filter((item) => {
+            if ( filter && typeof filter === 'function' ) {
+                return filter(item);
+            }
+            return true;
+        });
+    }
+
+    private async getStat(filepath: string, absoluteRootPath: string, options: RecursiveReaddirFilesOptions): Promise<IFileDirStat> {
+        const stat = (await fs.promises.stat(filepath)) as IFileDirStat;
+        stat.ext = '';
+        if ( stat.isFile!() ) {
+            stat.ext = this.getExt(filepath);
+            stat.name = path.basename(filepath);
+            stat.path = path.resolve(filepath);
+        }
+
+        if ( options.replacePathByRelativeOne && stat.path ) {
+            stat.path = stat.path.replace(absoluteRootPath, '');
+        }
+
+        if ( options.liteStats ) {
+            delete stat.dev;
+            delete stat.nlink;
+            delete stat.uid;
+            delete stat.gid;
+            delete stat.rdev;
+            delete stat.blksize;
+            delete stat.ino;
+            delete stat.blocks;
+            delete stat.atimeMs;
+            delete stat.mtimeMs;
+            delete stat.ctimeMs;
+            delete stat.birthtimeMs;
+            delete stat.atime;
+            //delete stat.mtime;
+            delete stat.ctime;
+            //delete stat.birthtime;
+            //delete stat.mode;
+        }
+
+        return stat;
+    }
+
+    /**
+     * Get ext
+     * @param {String} filePath `/a/b.jpg` => `jpg`
+     */
+    private getExt(filePath: string): string {
+        return path.extname(filePath).replace(/^\./, '').toLowerCase();
+    }
+}
+
+
+export default new RecursiveFilesStats();
diff --git a/logging/WinstonLogger.ts b/logging/WinstonLogger.ts
new file mode 100644
index 0000000000000000000000000000000000000000..941e5388ed95b1f281c21f5d2bf2c0c9217ae10a
--- /dev/null
+++ b/logging/WinstonLogger.ts
@@ -0,0 +1,64 @@
+import winston        from 'winston';
+import SharedConfig   from '../config/SharedConfig';
+import * as Transport from 'winston-transport';
+
+
+const levels = {
+    error: 0,
+    warn : 1,
+    info : 2,
+    http : 3,
+    debug: 4
+};
+
+const colors = {
+    error: 'red',
+    warn : 'orange',
+    info : 'green',
+    http : 'magenta',
+    debug: 'blue'
+};
+winston.addColors(colors);
+
+const format = winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format(info => ({
+    ...info,
+    level: info.level.toUpperCase()
+}))(), SharedConfig.production ? winston.format.uncolorize() : winston.format.colorize({ all: true }), winston.format.prettyPrint(), winston.format.errors({ stack: true }), winston.format.align(), winston.format.printf((info) => `[${ info.timestamp }] (${ process.pid }) ${ info.level } ${ info.message } ${ info.metadata ? `\n${ JSON.stringify(info.metadata) }` : '' } ${ info.stack ? `\n${ info.stack }` : '' } `));
+
+const commonTransportOptions = {
+    handleRejections: true,
+    handleExceptions: true
+};
+
+let transports: Array<Transport> = [ new winston.transports.Console({
+                                                                        ...commonTransportOptions,
+                                                                        level: 'debug'
+                                                                    }) ];
+
+if ( SharedConfig.production ) {
+    const commonFileOptions = {
+        ...commonTransportOptions,
+        maxsize : 5242880, // 5MB
+        maxFiles: 100,
+        tailable: true
+    };
+
+    transports = transports.concat([ new winston.transports.File({
+                                                                     ...commonFileOptions,
+                                                                     filename: `${ SharedConfig.logsFolder }/error.log`,
+                                                                     level   : 'error'
+                                                                 }), new winston.transports.File({
+                                                                                                     ...commonFileOptions,
+                                                                                                     filename: `${ SharedConfig.logsFolder }/all.log`,
+                                                                                                     level   : 'debug'
+                                                                                                 }) ]);
+}
+
+const logger = winston.createLogger({
+                                        levels,
+                                        format,
+                                        transports,
+                                        exitOnError: false
+                                    });
+
+export default logger;
diff --git a/managers/SharedGitlabManager.ts b/managers/SharedGitlabManager.ts
new file mode 100644
index 0000000000000000000000000000000000000000..68ff699a036044872cb7f33ead08782afbbdb830
--- /dev/null
+++ b/managers/SharedGitlabManager.ts
@@ -0,0 +1,38 @@
+import axios          from 'axios';
+import GitlabPipeline from '../types/Gitlab/GitlabPipeline';
+import GitlabRoute    from '../types/Gitlab/GitlabRoute';
+import SharedConfig   from '../config/SharedConfig';
+import GitlabToken    from '../types/Gitlab/GitlabToken';
+
+
+class GitlabManager {
+    private getApiUrl(route: GitlabRoute): string {
+        return `${ SharedConfig.gitlab.apiURL }${ route }`;
+    }
+
+    async getTokens(codeOrRefresh: string, isRefresh: boolean = false, clientSecret: string = ''): Promise<GitlabToken> {
+        const response = await axios.post<GitlabToken>(SharedConfig.login.gitlab.url.token, {
+            client_id    : SharedConfig.login.gitlab.client.id,
+            client_secret: clientSecret,
+            grant_type   : isRefresh ? 'refresh_token' : 'authorization_code',
+            refresh_token: codeOrRefresh,
+            code         : codeOrRefresh,
+            redirect_uri : SharedConfig.login.gitlab.url.redirect
+        });
+
+        return response.data;
+    }
+
+    async getRepositoryPipelines(repoId: number, branch: string = 'main'): Promise<Array<GitlabPipeline>> {
+        const response = await axios.get<Array<GitlabPipeline>>(this.getApiUrl(GitlabRoute.REPOSITORY_PIPELINES).replace('{{id}}', String(repoId)), {
+            params: {
+                ref: branch
+            }
+        });
+
+        return response.data;
+    }
+}
+
+
+export default new GitlabManager();
diff --git a/types/Dojo/AssignmentCheckerError.ts b/types/Dojo/AssignmentCheckerError.ts
new file mode 100644
index 0000000000000000000000000000000000000000..34df2a8d771aad43750bb78ce490bdc3202f1022
--- /dev/null
+++ b/types/Dojo/AssignmentCheckerError.ts
@@ -0,0 +1,23 @@
+enum ExerciseCheckerError {
+    DOCKER_DAEMON_NOT_RUNNING       = 200,
+    REQUIRED_FILES_MISSING          = 201,
+    ASSIGNMENT_FILE_SCHEMA_ERROR    = 202,
+    IMMUTABLE_PATH_NOT_FOUND        = 203,
+    IMMUTABLE_PATH_IS_DIRECTORY     = 204,
+    IMMUTABLE_PATH_IS_NOT_DIRECTORY = 205,
+    COMPOSE_FILE_YAML_ERROR         = 206,
+    COMPOSE_FILE_SCHEMA_ERROR       = 207,
+    COMPOSE_FILE_CONTAINER_MISSING  = 208,
+    COMPOSE_FILE_VOLUME_MISSING     = 209,
+    DOCKERFILE_NOT_FOUND            = 210,
+    COMPOSE_RUN_SUCCESSFULLY        = 211, // Yes, this is an error
+}
+
+
+/**
+ * Codes that are unusable for historic reasons:
+ * None
+ */
+
+
+export default ExerciseCheckerError;
\ No newline at end of file
diff --git a/types/Dojo/AssignmentFile.ts b/types/Dojo/AssignmentFile.ts
new file mode 100644
index 0000000000000000000000000000000000000000..df09d53ae249bf3614debd251630eb10e96a5c0b
--- /dev/null
+++ b/types/Dojo/AssignmentFile.ts
@@ -0,0 +1,19 @@
+import ImmutableFileDescriptor from './ImmutableFileDescriptor';
+import { z }                   from 'zod';
+
+
+const AssignmentFile = z.object({
+                                    dojoAssignmentVersion: z.number(),
+                                    version              : z.number(),
+                                    immutable            : z.array(ImmutableFileDescriptor.transform(value => value as ImmutableFileDescriptor)),
+                                    result               : z.object({
+                                                                        container: z.string(),
+                                                                        volume   : z.string().optional()
+                                                                    })
+                                }).strict();
+
+
+type AssignmentFile = z.infer<typeof AssignmentFile>;
+
+
+export default AssignmentFile;
\ No newline at end of file
diff --git a/types/Dojo/DojoBackendResponse.ts b/types/Dojo/DojoBackendResponse.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6c6eed91a45e42e0ff784baedeed2c76833236d3
--- /dev/null
+++ b/types/Dojo/DojoBackendResponse.ts
@@ -0,0 +1,10 @@
+interface DojoBackendResponse<T> {
+    timestamp: string;
+    code: number;
+    description: string;
+    sessionToken: string | null;
+    data: T;
+}
+
+
+export default DojoBackendResponse;
\ No newline at end of file
diff --git a/types/Dojo/DojoStatusCode.ts b/types/Dojo/DojoStatusCode.ts
new file mode 100644
index 0000000000000000000000000000000000000000..16f152daedf7a9f3b8154ea632dce404b8ccc8cf
--- /dev/null
+++ b/types/Dojo/DojoStatusCode.ts
@@ -0,0 +1,20 @@
+enum DojoStatusCode {
+    LOGIN_FAILED                        = 1,
+    REFRESH_TOKENS_FAILED               = 2,
+    CLIENT_NOT_SUPPORTED                = 100,
+    CLIENT_VERSION_NOT_SUPPORTED        = 110,
+    ASSIGNMENT_PUBLISH_NO_PIPELINE      = 200,
+    ASSIGNMENT_PUBLISH_PIPELINE_FAILED  = 201,
+    ASSIGNMENT_CREATION_GITLAB_ERROR    = 202,
+    ASSIGNMENT_CREATION_INTERNAL_ERROR  = 203,
+    ASSIGNMENT_EXERCISE_NOT_RELATED     = 204,
+    ASSIGNMENT_NOT_PUBLISHED            = 205,
+    EXERCISE_CORRECTION_NOT_EXIST       = 206,
+    EXERCISE_CORRECTION_ALREADY_EXIST   = 207,
+    EXERCISE_CREATION_GITLAB_ERROR      = 302,
+    EXERCISE_CREATION_INTERNAL_ERROR    = 303,
+    MAX_EXERCISE_PER_ASSIGNMENT_REACHED = 304
+}
+
+
+export default DojoStatusCode;
\ No newline at end of file
diff --git a/types/Dojo/ExerciseCheckerError.ts b/types/Dojo/ExerciseCheckerError.ts
new file mode 100644
index 0000000000000000000000000000000000000000..45810d0c9e34dcdb0c37fcf64525bcad29edbbcb
--- /dev/null
+++ b/types/Dojo/ExerciseCheckerError.ts
@@ -0,0 +1,19 @@
+enum ExerciseCheckerError {
+    EXERCISE_ASSIGNMENT_GET_ERROR          = 200,
+    DOCKER_COMPOSE_RUN_ERROR               = 201,
+    DOCKER_COMPOSE_LOGS_ERROR              = 202,
+    DOCKER_COMPOSE_DOWN_ERROR              = 203,
+    EXERCISE_RESULTS_FOLDER_TOO_BIG        = 204,
+    EXERCISE_RESULTS_FILE_SCHEMA_NOT_VALID = 206,
+    UPLOAD                                 = 207,
+    DOCKER_COMPOSE_REMOVE_DANGLING_ERROR   = 208
+}
+
+
+/**
+ * Codes that are unusable for historic reasons:
+ * - 205: EXERCISE_RESULTS_FILE_NOT_FOUND => From the version 2.2.0 this error is not possible anymore because the results file is now optional
+ */
+
+
+export default ExerciseCheckerError;
\ No newline at end of file
diff --git a/types/Dojo/ExerciseResultsFile.ts b/types/Dojo/ExerciseResultsFile.ts
new file mode 100644
index 0000000000000000000000000000000000000000..135a532d11623c74389f049ec83da16a2c5988f2
--- /dev/null
+++ b/types/Dojo/ExerciseResultsFile.ts
@@ -0,0 +1,39 @@
+import Icon  from '../Icon';
+import { z } from 'zod';
+
+
+const ExerciseResultsFile = z.object({
+                                         success: z.boolean().optional(),
+
+                                         containerExitCode: z.number().optional(),
+
+                                         successfulTests: z.number().optional(),
+                                         failedTests    : z.number().optional(),
+
+                                         successfulTestsList: z.array(z.string()).optional(),
+                                         failedTestsList    : z.array(z.string()).optional(),
+
+                                         otherInformations: z.array(z.object({
+                                                                                 name               : z.string(),
+                                                                                 description        : z.string().optional(),
+                                                                                 icon               : z.enum(Object.keys(Icon) as [ firstKey: string, ...otherKeys: Array<string> ]).optional(),
+                                                                                 itemsOrInformations: z.union([ z.array(z.string()), z.string() ])
+                                                                             }))
+                                         .optional()
+                                     }).strict().transform(value => {
+    if ( value.successfulTests === undefined && value.successfulTestsList !== undefined ) {
+        value.successfulTests = value.successfulTestsList.length;
+    }
+
+    if ( value.failedTests === undefined && value.failedTestsList !== undefined ) {
+        value.failedTests = value.failedTestsList.length;
+    }
+
+    return value;
+});
+
+
+type ExerciseResultsFile = z.infer<typeof ExerciseResultsFile>;
+
+
+export default ExerciseResultsFile;
\ No newline at end of file
diff --git a/types/Dojo/ImmutableFileDescriptor.ts b/types/Dojo/ImmutableFileDescriptor.ts
new file mode 100644
index 0000000000000000000000000000000000000000..27f66a1104340bc404b6788a88b06b776df8f5e4
--- /dev/null
+++ b/types/Dojo/ImmutableFileDescriptor.ts
@@ -0,0 +1,14 @@
+import { z } from 'zod';
+
+
+const ImmutableFileDescriptor = z.object({
+                                             description: z.string().optional(),
+                                             path       : z.string(),
+                                             isDirectory: z.boolean().optional()
+                                         });
+
+
+type ImmutableFileDescriptor = z.infer<typeof ImmutableFileDescriptor>;
+
+
+export default ImmutableFileDescriptor;
\ No newline at end of file
diff --git a/types/Gitlab/GitlabAccessLevel.ts b/types/Gitlab/GitlabAccessLevel.ts
new file mode 100644
index 0000000000000000000000000000000000000000..be06ffd009103feb9f30a4e18250c2fcf978527b
--- /dev/null
+++ b/types/Gitlab/GitlabAccessLevel.ts
@@ -0,0 +1,11 @@
+enum GitlabAccessLevel {
+    GUEST      = 10,
+    REPORTER   = 20,
+    DEVELOPER  = 30,
+    MAINTAINER = 40,
+    OWNER      = 50,
+    ADMIN      = 60
+}
+
+
+export default GitlabAccessLevel;
diff --git a/types/Gitlab/GitlabCommit.ts b/types/Gitlab/GitlabCommit.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5f94c2e431b220b4247e0dc3bf0222fba14f2a1c
--- /dev/null
+++ b/types/Gitlab/GitlabCommit.ts
@@ -0,0 +1,17 @@
+interface GitlabCommit {
+    id: string;
+    short_id: string;
+    created_at: string;
+    parent_ids: Array<string>;
+    title: string;
+    message: string;
+    author_name: string;
+    author_email: string;
+    authored_date: string;
+    committer_name: string;
+    committer_email: string;
+    committed_date: string;
+}
+
+
+export default GitlabCommit;
\ No newline at end of file
diff --git a/types/Gitlab/GitlabFile.ts b/types/Gitlab/GitlabFile.ts
new file mode 100644
index 0000000000000000000000000000000000000000..05205d4f1e28d5d04e60ae304acb02f3ddcb0d02
--- /dev/null
+++ b/types/Gitlab/GitlabFile.ts
@@ -0,0 +1,16 @@
+interface GitlabFile {
+    file_name: string,
+    file_path: string,
+    size: number,
+    encoding: string,
+    content_sha256: string,
+    ref: string,
+    blob_id: string,
+    commit_id: string,
+    last_commit_id: string,
+    execute_filemode: boolean,
+    content: string,
+}
+
+
+export default GitlabFile;
\ No newline at end of file
diff --git a/types/Gitlab/GitlabGroup.ts b/types/Gitlab/GitlabGroup.ts
new file mode 100644
index 0000000000000000000000000000000000000000..812f8838b5e00eb50cea0a282e3e26dc20344fef
--- /dev/null
+++ b/types/Gitlab/GitlabGroup.ts
@@ -0,0 +1,10 @@
+interface GitlabGroup {
+    group_id: number,
+    group_name: string,
+    group_full_path: string,
+    group_access_level: number,
+    expires_at: string
+}
+
+
+export default GitlabGroup;
\ No newline at end of file
diff --git a/types/Gitlab/GitlabMember.ts b/types/Gitlab/GitlabMember.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6c7c7163f73104221de918af4ceb09d4a55c98e2
--- /dev/null
+++ b/types/Gitlab/GitlabMember.ts
@@ -0,0 +1,12 @@
+import GitlabUser from './GitlabUser';
+
+
+interface GitlabMember extends GitlabUser {
+    access_level: number,
+    created_at: string,
+    created_by: GitlabUser,
+    expires_at: string | null
+}
+
+
+export default GitlabMember;
\ No newline at end of file
diff --git a/types/Gitlab/GitlabMilestone.ts b/types/Gitlab/GitlabMilestone.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a7285cd9ee3683aeaf4ebc1ea85ab778af1e970c
--- /dev/null
+++ b/types/Gitlab/GitlabMilestone.ts
@@ -0,0 +1,19 @@
+interface GitlabMilestone {
+    id: number;
+    iid: number;
+    project_id: number;
+    title: string;
+    description: string;
+    state: string;
+    created_at: string;
+    updated_at: string;
+    due_date: string;
+    start_date: string;
+    web_url: string;
+    issue_stats: {
+        total: number; closed: number;
+    };
+}
+
+
+export default GitlabMilestone;
\ No newline at end of file
diff --git a/types/Gitlab/GitlabNamespace.ts b/types/Gitlab/GitlabNamespace.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4d892afb58d81138a6b204124fc5b2ed6526eeca
--- /dev/null
+++ b/types/Gitlab/GitlabNamespace.ts
@@ -0,0 +1,13 @@
+interface GitlabNamespace {
+    id: number,
+    name: string,
+    path: string,
+    kind: string,
+    full_path: string,
+    parent_id: number,
+    avatar_url: string,
+    web_url: string
+}
+
+
+export default GitlabNamespace;
\ No newline at end of file
diff --git a/types/Gitlab/GitlabPipeline.ts b/types/Gitlab/GitlabPipeline.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1ee75b643b458b1f0c924f5d47f7ae0f0f88ddb0
--- /dev/null
+++ b/types/Gitlab/GitlabPipeline.ts
@@ -0,0 +1,31 @@
+import GitlabPipelineStatus from './GitlabPipelineStatus';
+import GitlabPipelineSource from './GitlabPipelineSource';
+import GitlabUser           from './GitlabUser';
+
+
+interface GitlabPipeline {
+    id: number,
+    iid: number,
+    project_id: number,
+    status: GitlabPipelineStatus,
+    source: GitlabPipelineSource,
+    ref: string,
+    sha: string,
+    before_sha: string,
+    tag: boolean,
+    name: string,
+    yaml_errors: string | null,
+    user: GitlabUser,
+    web_url: string,
+    created_at: string,
+    updated_at: string,
+    started_at: string | null,
+    finished_at: string | null,
+    committed_at: string | null,
+    duration: number | null,
+    queued_duration: number | null,
+    coverage: string | null,
+}
+
+
+export default GitlabPipeline;
\ No newline at end of file
diff --git a/types/Gitlab/GitlabPipelineSource.ts b/types/Gitlab/GitlabPipelineSource.ts
new file mode 100644
index 0000000000000000000000000000000000000000..33253b07ded1850dfcce309971e84836e417a615
--- /dev/null
+++ b/types/Gitlab/GitlabPipelineSource.ts
@@ -0,0 +1,19 @@
+enum GitlabPipelineSource {
+    PUSH                      = 'push',
+    WEB                       = 'web',
+    TRIGGER                   = 'trigger',
+    SCHEDULE                  = 'schedule',
+    API                       = 'api',
+    EXTERNAL                  = 'external',
+    PIPELINE                  = 'pipeline',
+    CHAT                      = 'chat',
+    WEBIDE                    = 'webide',
+    MERGE_REQUEST             = 'merge_request_event',
+    EXTERNAL_PULL_REQUEST     = 'external_pull_request_event',
+    PARENT_PIPELINE           = 'parent_pipeline',
+    ON_DEMAND_DAST_SCAN       = 'ondemand_dast_scan',
+    ON_DEMAND_DAST_VALIDATION = 'ondemand_dast_validation',
+}
+
+
+export default GitlabPipelineSource;
diff --git a/types/Gitlab/GitlabPipelineStatus.ts b/types/Gitlab/GitlabPipelineStatus.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cd6d7b268b951785fccfe11b6a82f3b6376b566a
--- /dev/null
+++ b/types/Gitlab/GitlabPipelineStatus.ts
@@ -0,0 +1,16 @@
+enum GitlabPipelineStatus {
+    CREATED              = 'created',
+    WAITING_FOR_RESOURCE = 'waiting_for_resource',
+    PREPARING            = 'preparing',
+    PENDING              = 'pending',
+    RUNNING              = 'running',
+    SUCCESS              = 'success',
+    FAILED               = 'failed',
+    CANCELED             = 'canceled',
+    SKIPPED              = 'skipped',
+    MANUAL               = 'manual',
+    SCHEDULED            = 'scheduled'
+}
+
+
+export default GitlabPipelineStatus;
diff --git a/types/Gitlab/GitlabProfile.ts b/types/Gitlab/GitlabProfile.ts
new file mode 100644
index 0000000000000000000000000000000000000000..aa7506dbddcf7051a29c118d69fc26d1f7fb55bb
--- /dev/null
+++ b/types/Gitlab/GitlabProfile.ts
@@ -0,0 +1,40 @@
+import GitlabUser from './GitlabUser';
+
+
+interface GitlabProfile extends GitlabUser {
+    created_at: string,
+    bio: string,
+    location: string,
+    public_email: string,
+    skype: string,
+    linkedin: string,
+    twitter: string,
+    discord: string,
+    website_url: string,
+    organization: string,
+    job_title: string,
+    pronouns: string,
+    bot: boolean,
+    work_information: string,
+    local_time: string,
+    last_sign_in_at: string,
+    confirmed_at: string,
+    last_activity_on: string,
+    email: string,
+    theme_id: number,
+    color_scheme_id: number,
+    projects_limit: number,
+    current_sign_in_at: string,
+    identities: Array<{
+        provider: string, extern_uid: string
+    }>,
+    can_create_group: boolean,
+    can_create_project: boolean,
+    two_factor_enabled: boolean,
+    external: boolean,
+    private_profile: boolean,
+    commit_email: string
+}
+
+
+export default GitlabProfile;
\ No newline at end of file
diff --git a/types/Gitlab/GitlabRelease.ts b/types/Gitlab/GitlabRelease.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a7c68d770e4e88c7acd2f6f6eb1305329c42f61d
--- /dev/null
+++ b/types/Gitlab/GitlabRelease.ts
@@ -0,0 +1,29 @@
+import GitlabUser      from './GitlabUser';
+import GitlabCommit    from './GitlabCommit';
+import GitlabMilestone from './GitlabMilestone';
+
+
+interface GitlabRelease {
+    tag_name: string;
+    description: string;
+    created_at: string;
+    released_at: string;
+    author: GitlabUser;
+    commit: GitlabCommit;
+    milestones: Array<GitlabMilestone>;
+    commit_path: string;
+    tag_path: string;
+    assets: {
+        count: number; sources: Array<{
+            format: string; url: string;
+        }>; links: Array<{
+            id: number; name: string; url: string; link_type: string;
+        }>; evidence_file_path: string;
+    };
+    evidences: Array<{
+        sha: string; filepath: string; collected_at: string;
+    }>;
+}
+
+
+export default GitlabRelease; 
\ No newline at end of file
diff --git a/types/Gitlab/GitlabRepository.ts b/types/Gitlab/GitlabRepository.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0aa260287cc95030231761b2781a31a1b203f22b
--- /dev/null
+++ b/types/Gitlab/GitlabRepository.ts
@@ -0,0 +1,106 @@
+import GitlabGroup     from './GitlabGroup';
+import GitlabNamespace from './GitlabNamespace';
+
+
+interface GitlabRepository {
+    id: number,
+    description: string,
+    name: string,
+    name_with_namespace: string,
+    path: string,
+    path_with_namespace: string,
+    created_at: string,
+    default_branch: string,
+    tag_list: Array<string>,
+    topics: Array<string>,
+    ssh_url_to_repo: string,
+    http_url_to_repo: string,
+    web_url: string,
+    readme_url: string,
+    forks_count: number,
+    avatar_url: string,
+    star_count: number,
+    last_activity_at: string,
+    namespace: GitlabNamespace,
+    _links: {
+        self: string, issues: string, merge_requests: string, repo_branches: string, labels: string, events: string, members: string, cluster_agents: string
+    },
+    packages_enabled: boolean,
+    empty_repo: boolean,
+    archived: boolean,
+    visibility: string,
+    resolve_outdated_diff_discussions: boolean,
+    container_expiration_policy: {
+        cadence: string, enabled: boolean, keep_n: number, older_than: string, name_regex: string, name_regex_keep: string, next_run_at: string
+    },
+    issues_enabled: boolean,
+    merge_requests_enabled: boolean,
+    wiki_enabled: boolean,
+    jobs_enabled: boolean,
+    snippets_enabled: boolean,
+    container_registry_enabled: boolean,
+    service_desk_enabled: boolean,
+    service_desk_address: string,
+    can_create_merge_request_in: boolean,
+    issues_access_level: string,
+    repository_access_level: string,
+    merge_requests_access_level: string,
+    forking_access_level: string,
+    wiki_access_level: string,
+    builds_access_level: string,
+    snippets_access_level: string,
+    pages_access_level: string,
+    operations_access_level: string,
+    analytics_access_level: string,
+    container_registry_access_level: string,
+    security_and_compliance_access_level: string,
+    releases_access_level: string,
+    environments_access_level: string,
+    feature_flags_access_level: string,
+    infrastructure_access_level: string,
+    monitor_access_level: string,
+    emails_disabled: boolean,
+    shared_runners_enabled: boolean,
+    lfs_enabled: boolean,
+    creator_id: number,
+    import_url: string,
+    import_type: string,
+    import_status: string,
+    import_error: string,
+    open_issues_count: number,
+    runners_token: string,
+    ci_default_git_depth: number,
+    ci_forward_deployment_enabled: boolean,
+    ci_job_token_scope_enabled: boolean,
+    ci_separated_caches: boolean,
+    ci_opt_in_jwt: boolean,
+    ci_allow_fork_pipelines_to_run_in_parent_project: boolean,
+    public_jobs: boolean,
+    build_git_strategy: string,
+    build_timeout: number,
+    auto_cancel_pending_pipelines: string,
+    ci_config_path: string,
+    shared_with_groups: Array<GitlabGroup>,
+    only_allow_merge_if_pipeline_succeeds: boolean,
+    allow_merge_on_skipped_pipeline: boolean,
+    restrict_user_defined_variables: boolean,
+    request_access_enabled: boolean,
+    only_allow_merge_if_all_discussions_are_resolved: boolean,
+    remove_source_branch_after_merge: boolean,
+    printing_merge_request_link_enabled: boolean,
+    merge_method: string,
+    squash_option: string,
+    enforce_auth_checks_on_uploads: boolean,
+    suggestion_commit_message: string,
+    merge_commit_template: string,
+    squash_commit_template: string,
+    issue_branch_template: string,
+    auto_devops_enabled: boolean,
+    auto_devops_deploy_strategy: string,
+    autoclose_referenced_issues: boolean,
+    keep_latest_artifact: boolean,
+    runner_token_expiration_interval: number,
+}
+
+
+export default GitlabRepository;
\ No newline at end of file
diff --git a/types/Gitlab/GitlabRoute.ts b/types/Gitlab/GitlabRoute.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bdeccc68f582f5f477bd6fb9d8f3fe665680ca5d
--- /dev/null
+++ b/types/Gitlab/GitlabRoute.ts
@@ -0,0 +1,22 @@
+enum GitlabRoute {
+    NOTIFICATION_SETTINGS       = '/notification_settings',
+    PROFILE_GET                 = '/user',
+    USERS_GET                   = '/users',
+    REPOSITORY_GET              = '/projects/{{id}}',
+    REPOSITORY_CREATE           = '/projects', // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
+    REPOSITORY_DELETE           = '/projects/{{id}}', // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
+    REPOSITORY_EDIT             = '/projects/{{id}}',
+    REPOSITORY_FORK             = '/projects/{{id}}/fork',
+    REPOSITORY_MEMBER_ADD       = '/projects/{{id}}/members',
+    REPOSITORY_MEMBERS_GET      = '/projects/{{id}}/members/all',
+    REPOSITORY_RELEASES_GET     = '/projects/{{id}}/releases',
+    REPOSITORY_BADGES_ADD       = '/projects/{{id}}/badges',
+    REPOSITORY_VARIABLES_ADD    = '/projects/{{id}}/variables',
+    REPOSITORY_BRANCHES_PROTECT = '/projects/{{id}}/protected_branches',
+    REPOSITORY_TREE             = '/projects/{{id}}/repository/tree',
+    REPOSITORY_FILE             = '/projects/{{id}}/repository/files/{{filePath}}',
+    REPOSITORY_PIPELINES        = '/projects/{{id}}/pipelines',
+}
+
+
+export default GitlabRoute;
\ No newline at end of file
diff --git a/types/Gitlab/GitlabToken.ts b/types/Gitlab/GitlabToken.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c8c647e6285cf626b155d7ff93beb0dd2c7f9d52
--- /dev/null
+++ b/types/Gitlab/GitlabToken.ts
@@ -0,0 +1,11 @@
+interface GitlabToken {
+    access_token: string;
+    token_type: string;
+    expires_in: number;
+    refresh_token: string;
+    scope: string;
+    created_at: number;
+}
+
+
+export default GitlabToken;
\ No newline at end of file
diff --git a/types/Gitlab/GitlabTreeFile.ts b/types/Gitlab/GitlabTreeFile.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b2cf67ecb1d636838ff2e48cbc6ce62e11248476
--- /dev/null
+++ b/types/Gitlab/GitlabTreeFile.ts
@@ -0,0 +1,13 @@
+import GitlabTreeFileType from './GitlabTreeFileType';
+
+
+interface GitlabTreeFile {
+    id: number,
+    name: string,
+    type: GitlabTreeFileType,
+    path: string,
+    mode: string
+}
+
+
+export default GitlabTreeFile;
\ No newline at end of file
diff --git a/types/Gitlab/GitlabTreeFileType.ts b/types/Gitlab/GitlabTreeFileType.ts
new file mode 100644
index 0000000000000000000000000000000000000000..eead9b931715ec73fa158513775419dfb14ee538
--- /dev/null
+++ b/types/Gitlab/GitlabTreeFileType.ts
@@ -0,0 +1,8 @@
+enum GitlabTreeFileType {
+    TREE   = 'tree',
+    BLOB   = 'blob',
+    COMMIT = 'commit'
+}
+
+
+export default GitlabTreeFileType;
diff --git a/types/Gitlab/GitlabUser.ts b/types/Gitlab/GitlabUser.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bbb759264764020b1dd80cca050392371b8e87b7
--- /dev/null
+++ b/types/Gitlab/GitlabUser.ts
@@ -0,0 +1,11 @@
+interface GitlabUser {
+    id: number,
+    username: string,
+    name: string,
+    state: string,
+    avatar_url: string,
+    web_url: string,
+}
+
+
+export default GitlabUser;
\ No newline at end of file
diff --git a/types/Gitlab/GitlabVisibility.ts b/types/Gitlab/GitlabVisibility.ts
new file mode 100644
index 0000000000000000000000000000000000000000..842ff168e274cb6cfb2a39ef7a952cdb5248c1b9
--- /dev/null
+++ b/types/Gitlab/GitlabVisibility.ts
@@ -0,0 +1,8 @@
+enum GitlabVisibility {
+    PUBLIC   = 'public',
+    INTERNAL = 'internal',
+    PRIVATE  = 'private'
+}
+
+
+export default GitlabVisibility;
diff --git a/types/Icon.ts b/types/Icon.ts
new file mode 100644
index 0000000000000000000000000000000000000000..33ee07279853d172f2c975a01f6299b438449175
--- /dev/null
+++ b/types/Icon.ts
@@ -0,0 +1,16 @@
+const Icon = {
+    CAT_INFO : '▶️',
+    INFO     : 'ℹ️',
+    ERROR    : '⛔️',
+    SUCCESS  : '✅',
+    FAILURE  : '❌',
+    VOMIT    : '🤮',
+    YUCK     : '🤢',
+    WELL_DONE: '👍',
+    NULL     : '',
+    NONE     : '',
+    BADMINTON: '🏸'
+} as { [index: string]: string };
+
+
+export default Icon;
\ No newline at end of file