Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • Dojo_Project_Nguyen/ui/dojocli
  • dojo_project/projects/ui/dojocli
  • tom.andrivet/dojocli
  • orestis.malaspin/dojocli
4 results
Select Git revision
Show changes
Showing
with 3614 additions and 990 deletions
release:wiki: release:doc:wiki:
stage: release stage: release
tags: tags:
- release - release
...@@ -44,7 +44,7 @@ release:wiki: ...@@ -44,7 +44,7 @@ release:wiki:
# Push the change back to the master branch of the wiki # Push the change back to the master branch of the wiki
- git push origin "HEAD:main" - git push origin "HEAD:main"
rules: rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' - when: never
release:gitlab: release:gitlab:
...@@ -58,10 +58,10 @@ release:gitlab: ...@@ -58,10 +58,10 @@ release:gitlab:
- RELEASE_NAME=$VERSION - RELEASE_NAME=$VERSION
- !reference [ .release_gitlab, script ] - !reference [ .release_gitlab, script ]
rules: rules:
- if: '$CI_COMMIT_REF_PROTECTED == "true"' - if: $CI_COMMIT_REF_PROTECTED == "true"
release:latest:gitlab: release:gitlab:latest:
stage: release stage: release
tags: tags:
- release - release
...@@ -72,10 +72,10 @@ release:latest:gitlab: ...@@ -72,10 +72,10 @@ release:latest:gitlab:
- RELEASE_NAME="Latest" - RELEASE_NAME="Latest"
- !reference [ .release_gitlab, script ] - !reference [ .release_gitlab, script ]
rules: rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
release:pre-alpha:gitlab: release:gitlab:pre-alpha:
stage: release stage: release
tags: tags:
- release - release
...@@ -86,4 +86,6 @@ release:pre-alpha:gitlab: ...@@ -86,4 +86,6 @@ release:pre-alpha:gitlab:
- RELEASE_NAME="Pre-alpha" - RELEASE_NAME="Pre-alpha"
- !reference [ .release_gitlab, script ] - !reference [ .release_gitlab, script ]
rules: rules:
- if: '$CI_COMMIT_REF_PROTECTED == "true" && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH' - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
\ No newline at end of file when: never
- if: $CI_COMMIT_REF_PROTECTED == "true"
\ No newline at end of file
...@@ -6,3 +6,5 @@ ...@@ -6,3 +6,5 @@
# Datasource local storage ignored files # Datasource local storage ignored files
/dataSources/ /dataSources/
/dataSources.local.xml /dataSources.local.xml
# GitHub Copilot persisted chat sessions
/copilot/chatSessions
...@@ -12,4 +12,7 @@ ...@@ -12,4 +12,7 @@
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
<component name="SonarLintModuleSettings">
<option name="uniqueId" value="7e31b6fd-1f86-4d26-8c50-c9ab6530f54e" />
</component>
</module> </module>
\ No newline at end of file
Subproject commit ffc5d65f9f0f0e825688177425e526131aa84631 Subproject commit ef5c7bd49a57bc28db77bad797de4980133d6523
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="EslintConfiguration"> <component name="EslintConfiguration">
<custom-configuration-file used="false" path="./eslint.config.js" />
<option name="fix-on-save" value="true" /> <option name="fix-on-save" value="true" />
</component> </component>
</project> </project>
\ No newline at end of file
<?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
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" /> <mapping directory="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$/.idea/jetbrainsConfiguration" vcs="Git" />
<mapping directory="$PROJECT_DIR$/src/shared" vcs="Git" /> <mapping directory="$PROJECT_DIR$/src/shared" vcs="Git" />
<mapping directory="$PROJECT_DIR$/src/sharedByClients" vcs="Git" /> <mapping directory="$PROJECT_DIR$/src/sharedByClients" vcs="Git" />
</component> </component>
......
// @ts-check
// @formatter:off
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config({
ignores: [ 'dist/*', 'node_modules/*', '.gitlab-ci.yml', 'eslint.config.mjs' ]
}, eslint.configs.recommended, ...tseslint.configs.recommendedTypeChecked, {
languageOptions: {
parserOptions: {
project: true, tsconfigRootDir: import.meta.dirname
}
}
}, {
plugins: {
'@typescript-eslint': tseslint.plugin
}, rules: {
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/require-await': 'off',
'@typescript-eslint/restrict-template-expressions': 'off',
'@typescript-eslint/no-floating-promises': 'off',
}
});
\ No newline at end of file
This diff is collapsed.
{ {
"name" : "dojo_cli", "name" : "dojo_cli",
"description" : "CLI of the Dojo project", "description" : "CLI of the Dojo project",
"version" : "3.5.0", "version" : "6.0.0",
"license" : "AGPLv3", "license" : "AGPLv3",
"author" : "Michaël Minelli <dojo@minelli.me>", "author" : "Michaël Minelli <dojo@mail.minelli.swiss>",
"main" : "dist/app.js", "main" : "dist/app.js",
"bin" : { "bin" : {
"dojo": "./dist/app.js" "dojo": "./dist/app.js"
...@@ -25,50 +25,58 @@ ...@@ -25,50 +25,58 @@
] ]
}, },
"scripts" : { "scripts" : {
"dotenv:build": "npx dotenv-vault local build", "dotenv:build": "npx dotenvx encrypt",
"lint" : "npx eslint .", "lint" : "npx eslint .",
"genversion" : "npx genversion -s -e src/config/Version.ts", "genversion" : "npx genversion -s -e src/config/Version.ts",
"build" : "npm run genversion; npx tsc", "build" : "npm run genversion; npx tsc",
"start:dev" : "npm run genversion; npm run lint; npx ts-node 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" "test" : "echo \"Error: no test specified\" && exit 1"
}, },
"dependencies" : { "dependencies" : {
"@gitbeaker/rest" : "^39.34.3", "@dotenvx/dotenvx" : "^0.45.0",
"appdata-path" : "^1.0.0", "@eslint/js" : "^9.21.0",
"axios" : "^1.6.5", "@gitbeaker/core" : "^42.1.0",
"boxen" : "^5.1.2", "@gitbeaker/requester-utils": "^42.1.0",
"chalk" : "^4.1.2", "@gitbeaker/rest" : "^42.1.0",
"commander" : "^11.1.0", "appdata-path" : "^1.0.0",
"dotenv" : "^16.3.1", "axios" : "1.7.8",
"dotenv-expand" : "^10.0.0", "boxen" : "^5.1.2",
"fs-extra" : "^11.2.0", "chalk" : "^4.1.2",
"http-status-codes" : "^2.3.0", "cli-table3" : "^0.6.5",
"inquirer" : "^8.2.6", "commander" : "^13.1.0",
"json5" : "^2.2.3", "form-data" : "^4.0.2",
"jsonwebtoken" : "^8.5.1", "fs-extra" : "^11.3.0",
"open" : "^8.4.2", "fuse.js" : "^7.1.0",
"ora" : "^5.4.1", "http-status-codes" : "^2.3.0",
"semver" : "^7.5.4", "inquirer" : "^8.2.6",
"tar-stream" : "^3.1.6", "json5" : "^2.2.3",
"winston" : "^3.11.0", "jsonwebtoken" : "^8.5.1",
"yaml" : "^2.3.4", "open" : "^8.4.2",
"zod" : "^3.22.4", "ora" : "^5.4.1",
"zod-validation-error": "^3.0.0" "semver" : "^7.7.1",
"tar-stream" : "^3.1.7",
"winston" : "^3.17.0",
"winston-transport" : "^4.9.0",
"yaml" : "^2.7.0",
"zod" : "^3.24.2",
"zod-validation-error" : "^3.4.0"
}, },
"devDependencies": { "devDependencies": {
"@types/fs-extra" : "^11.0.4", "@types/fs-extra" : "^11.0.4",
"@types/inquirer" : "^8.2.10", "@types/inquirer" : "^8.2.10",
"@types/jsonwebtoken" : "^8.5.9", "@types/jsonwebtoken" : "^9.0.9",
"@types/node" : "^18.19.2", "@types/node" : "^18.19.76",
"@types/semver" : "^7.5.6", "@types/semver" : "^7.5.8",
"@types/tar-stream" : "^3.1.3", "@types/tar-stream" : "^3.1.3",
"@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/eslint-plugin": "^8.25.0",
"@typescript-eslint/parser" : "^6.18.1", "@typescript-eslint/parser" : "^8.25.0",
"dotenv-vault" : "^1.25.0", "dotenv-cli" : "^8.0.0",
"dotenv-vault" : "^1.26.2",
"genversion" : "^3.2.0", "genversion" : "^3.2.0",
"pkg" : "^5.8.1", "pkg" : "^5.8.1",
"tiny-typed-emitter" : "^2.1.0", "tiny-typed-emitter" : "^2.1.0",
"ts-node" : "^10.9.2", "tsx" : "^4.19.3",
"typescript" : "^5.3.3" "typescript" : "~5.5.4",
"typescript-eslint" : "^7.18.0"
} }
} }
// Read from the .env file // ATTENTION : This line MUST be the first of this file
// ATTENTION : These lines MUST be the first of this file (except for the path import) import './init.js';
import path = require('node:path'); import CommanderApp from './commander/CommanderApp.js';
import myEnv = require('dotenv'); import HttpManager from './managers/HttpManager.js';
import dotenvExpand = require('dotenv-expand'); import Config from './config/Config';
import ConfigFiles from './config/ConfigFiles';
import DojoBackendManager from './managers/DojoBackendManager';
import SharedAssignmentHelper from './shared/helpers/Dojo/SharedAssignmentHelper';
dotenvExpand.expand(myEnv.config({ (async () => {
path : path.join(__dirname, '../.env'), ConfigFiles.init();
DOTENV_KEY: 'dotenv://:key_fc323d8e0a02349342f1c6a119bb38495958ce3a43a87d19a3f674b7e2896dcb@dotenv.local/vault/.env.vault?environment=development'
}));
require('./shared/helpers/TypeScriptExtensions'); // ATTENTION : This line MUST be the second of this file await Config.init(await DojoBackendManager.getApiUrl());
SharedAssignmentHelper.init(Config.gitlabManager);
import CommanderApp from './commander/CommanderApp'; HttpManager.registerAxiosInterceptor();
import HttpManager from './managers/HttpManager';
const commanderApp = new CommanderApp();
HttpManager.registerAxiosInterceptor(); await commanderApp.init();
commanderApp.parse();
})();
new CommanderApp(); \ No newline at end of file
\ No newline at end of file
import { Command, Option } from 'commander'; import { Command, Option } from 'commander';
import SessionCommand from './session/SessionCommand'; import AssignmentCommand from './assignment/AssignmentCommand.js';
import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig'; import ExerciseCommand from './exercise/ExerciseCommand.js';
import AssignmentCommand from './assignment/AssignmentCommand'; import SharedConfig from '../shared/config/SharedConfig.js';
import ExerciseCommand from './exercise/ExerciseCommand';
import SharedConfig from '../shared/config/SharedConfig';
import boxen from 'boxen'; import boxen from 'boxen';
import { stateConfigFile } from '../config/ConfigFiles'; import semver from 'semver/preload.js';
import semver from 'semver/preload'; import { version } from '../config/Version.js';
import { version } from '../config/Version'; import CompletionCommand from './completion/CompletionCommand.js';
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';
import ConfigFiles from '../config/ConfigFiles';
import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig';
import Config from '../config/Config'; import Config from '../config/Config';
import SettingsCommand from './settings/SettingsCommand';
class CommanderApp { class CommanderApp {
program: Command = new Command(); public program: Command = new Command();
private readonly commandHookDisabled: Array<string> = [ 'completion' ];
private hasToExecuteHook(actionCommand: Command): boolean {
if ( actionCommand.parent == null ) {
return true;
} else {
if ( this.commandHookDisabled.includes(actionCommand.name()) ) {
return false;
}
constructor() { return this.hasToExecuteHook(actionCommand.parent);
}
}
public async init() {
this.program this.program
.name('dojo') .name('dojo')
.description('CLI of the Dojo application') .description('CLI of the Dojo application')
.version('{{VERSION}}') .version('{{VERSION}}')
.showHelpAfterError() .showHelpAfterError()
.configureHelp({ .configureHelp({
showGlobalOptions: true, showGlobalOptions: true,
sortOptions : true, sortOptions : true,
sortSubcommands : true sortSubcommands : true
}) })
.option('-H, --host <string>', 'override the Dojo API endpoint', ClientsSharedConfig.apiURL) .option('-H, --host <string>', 'override the Dojo API endpoint', ClientsSharedConfig.apiURL)
.option('-I, --interactive', 'show interactive interface when available', Config.interactiveMode) .addOption(new Option('--debug').hideHelp())
.addOption(new Option('--debug').hideHelp()) .hook('preAction', (_thisCommand: Command, actionCommand: Command) => {
.hook('preAction', () => { if ( this.hasToExecuteHook(actionCommand) ) {
this.warnDevelopmentVersion(); this.warnDevelopmentVersion();
}).hook('postAction', () => { }
this.informNewVersion(); })
}); .hook('postAction', (_thisCommand: Command, actionCommand: Command) => {
if ( this.hasToExecuteHook(actionCommand) ) {
this.informNewVersion();
}
});
const interactiveModeOption = new Option('-I, --interactive', 'show interactive interface when available');
const noInteractiveModeOption = new Option('-N, --no_interactive', 'hide interactive interface when available').conflicts('interactive'); // WARNING: Due to a bug in commander.js, the conflicts method doesn't work as expected if the command is named no-interactive
if ( Config.interactiveMode ) {
interactiveModeOption.hideHelp();
} else {
noInteractiveModeOption.hideHelp();
}
this.program.addOption(interactiveModeOption).addOption(noInteractiveModeOption);
this.program.on('option:host', () => { this.program.on('option:host', () => {
ClientsSharedConfig.apiURL = this.program.opts().host; ClientsSharedConfig.apiURL = this.program.opts().host;
}); });
this.program.on('option:no_interactive', () => {
Config.interactiveMode = false;
});
this.program.on('option:interactive', () => { this.program.on('option:interactive', () => {
Config.interactiveMode = this.program.opts().interactive; Config.interactiveMode = true;
}); });
this.program.on('option:debug', () => { this.program.on('option:debug', () => {
...@@ -47,15 +84,17 @@ class CommanderApp { ...@@ -47,15 +84,17 @@ class CommanderApp {
}); });
this.registerCommands(); this.registerCommands();
}
public parse() {
this.program.parse(); this.program.parse();
} }
private warnDevelopmentVersion() { private warnDevelopmentVersion() {
if ( !SharedConfig.production ) { if ( !SharedConfig.production ) {
console.log(boxen(`This is a development (unstable) version of the DojoCLI. console.log(boxen(`This is a development (unstable) version of the DojoCLI.
If you want to use the stable version, please install the package from the following url: If you want to use the stable version, please execute this command:
https://gitedu.hesge.ch/dojo_project/projects/ui/dojocli/-/releases/Latest`, { ${ TextStyle.CODE(' dojo upgrade ') }`, {
title : 'Warning', title : 'Warning',
titleAlignment: 'center', titleAlignment: 'center',
borderColor : 'red', borderColor : 'red',
...@@ -69,30 +108,34 @@ https://gitedu.hesge.ch/dojo_project/projects/ui/dojocli/-/releases/Latest`, { ...@@ -69,30 +108,34 @@ https://gitedu.hesge.ch/dojo_project/projects/ui/dojocli/-/releases/Latest`, {
private informNewVersion() { private informNewVersion() {
if ( SharedConfig.production ) { if ( SharedConfig.production ) {
const latestDojoCliVersion = stateConfigFile.getParam('latestDojoCliVersion') as string | null || '0.0.0'; const latestDojoCliVersion = ConfigFiles.stateConfigFile.getParam('latestDojoCliVersion') as string | null || '0.0.0';
const latestDojoCliVersionNotification = stateConfigFile.getParam('latestDojoCliVersionNotification') as number | null || 0; const latestDojoCliVersionNotification = ConfigFiles.stateConfigFile.getParam('latestDojoCliVersionNotification') as number | null || 0;
if ( semver.lt(version, latestDojoCliVersion) ) { if ( semver.lt(version as string, latestDojoCliVersion) && (new Date()).getTime() - latestDojoCliVersionNotification >= Config.versionUpdateInformationPeriodHours * 60 * 60 * 1000 ) {
if ( (new Date()).getTime() - latestDojoCliVersionNotification >= Config.versionUpdateInformationPeriodHours * 60 * 60 * 1000 ) { console.log(boxen(`The ${ latestDojoCliVersion } version of the DojoCLI is available.
console.log(boxen(`The ${ latestDojoCliVersion } version of the DojoCLI is available: You can upgrade the DojoCLI by executing this command:
https://gitedu.hesge.ch/dojo_project/projects/ui/dojocli/-/releases/Latest`, { ${ TextStyle.CODE(' dojo upgrade ') }`, {
title : 'Information', title : 'Information',
titleAlignment: 'center', titleAlignment: 'center',
borderColor : 'blue', borderColor : 'blue',
borderStyle : 'bold', borderStyle : 'bold',
margin : 1, margin : 1,
padding : 1, padding : 1,
textAlignment : 'left' textAlignment : 'left'
})); }));
stateConfigFile.setParam('latestDojoCliVersionNotification', (new Date()).getTime()); ConfigFiles.stateConfigFile.setParam('latestDojoCliVersionNotification', (new Date()).getTime());
}
} }
} }
} }
private registerCommands() { private registerCommands() {
new AuthCommand().registerOnCommand(this.program);
SessionCommand.registerOnCommand(this.program); SessionCommand.registerOnCommand(this.program);
AssignmentCommand.registerOnCommand(this.program); AssignmentCommand.registerOnCommand(this.program);
ExerciseCommand.registerOnCommand(this.program); ExerciseCommand.registerOnCommand(this.program);
TagCommand.registerOnCommand(this.program);
CompletionCommand.registerOnCommand(this.program);
UpgradeCommand.registerOnCommand(this.program);
SettingsCommand.registerOnCommand(this.program);
} }
} }
......
import { Command } from 'commander'; import { Command, CommandOptions } from 'commander';
abstract class CommanderCommand { abstract class CommanderCommand {
protected abstract commandName: string; protected abstract commandName: string;
protected aliasNames: Array<string> = [];
protected options: CommandOptions = {};
command: Command = new Command(); command: Command = new Command();
registerOnCommand(parent: Command) { registerOnCommand(parent: Command) {
this.command = parent.command(this.commandName); this.command = parent.command(this.commandName, this.options).aliases(this.aliasNames);
this.defineCommand(); this.defineCommand();
this.defineSubCommands(); this.defineSubCommands();
...@@ -14,7 +18,12 @@ abstract class CommanderCommand { ...@@ -14,7 +18,12 @@ abstract class CommanderCommand {
protected abstract defineCommand(): void; protected abstract defineCommand(): void;
protected defineSubCommands() {} protected defineSubCommands() {
/*
* No action
* Override this method only if you need to define subcommands
* */
}
protected abstract commandAction(...args: Array<unknown>): Promise<void>; protected abstract commandAction(...args: Array<unknown>): Promise<void>;
} }
......
import CommanderCommand from './CommanderCommand.js';
import ora from 'ora';
import TextStyle from '../types/TextStyle.js';
import os from 'os';
import { spawn } from 'child_process';
import Config from '../config/Config';
class UpgradeCommand extends CommanderCommand {
protected commandName: string = 'upgrade';
protected defineCommand() {
this.command.description('upgrade the DojoCLI.')
.option('--pre-alpha', 'upgrade to the latest pre-alpha (dev) version. Attention it\'s an unstable version without support.')
.option('-o, --instructions', 'only display upgrade instructions.')
.action(this.commandAction.bind(this));
}
private displayInstructions(upgradeCommand: string, preAlpha: boolean) {
upgradeCommand = TextStyle.CODE(` ${ upgradeCommand } `);
console.log(TextStyle.BLOCK(`You can upgrade DojoCLI on ${ os.platform() === 'darwin' ? 'macOS' : 'Linux' }:`));
ora().start().info(`By executing this command: ${ upgradeCommand }`);
console.log('or');
ora().start().info(`By downloading the release from: ${ preAlpha ? Config.gitlab.cliPreAlphaReleasePage : Config.gitlab.cliReleasePage }`);
}
private upgrade(upgradeCommand: string) {
const upgradeSpinner = ora().start('DojoCLI upgrade is in progress. Please wait...');
const upgradeProcess = spawn(upgradeCommand, {
shell: true
});
upgradeProcess.on('exit', code => {
code === null || code !== 0 ? upgradeSpinner.fail('DojoCLI upgrade failed... Please try again manually.') : upgradeSpinner.succeed('DojoCLI upgrade is successful.');
});
}
protected async commandAction(options: { preAlpha: boolean, instructions: boolean }): Promise<void> {
if ( os.platform() === 'win32' ) {
if ( !options.instructions ) {
ora().start().warn('Automatic upgrade is not supported on Windows. \n');
}
console.log(TextStyle.BLOCK('You can upgrade DojoCLI on Windows:'));
ora().start().info(`By downloading the release from: ${ options.preAlpha ? Config.gitlab.cliPreAlphaReleasePage : Config.gitlab.cliReleasePage }`);
return;
}
const upgradeCommand = `curl -L "https://dojo.isc-hesge.ch/installer.sh" | sh /dev/stdin${ options.preAlpha ? ' installer=pre-alpha' : '' }`;
if ( options.instructions ) {
this.displayInstructions(upgradeCommand, options.preAlpha);
} else {
this.upgrade(upgradeCommand);
}
}
}
export default new UpgradeCommand();
\ No newline at end of file
import CommanderCommand from '../CommanderCommand'; import CommanderCommand from '../CommanderCommand.js';
import AssignmentCreateCommand from './subcommands/AssignmentCreateCommand'; import AssignmentCreateCommand from './subcommands/AssignmentCreateCommand.js';
import AssignmentPublishCommand from './subcommands/AssignmentPublishCommand'; import AssignmentPublishCommand from './subcommands/AssignmentPublishCommand.js';
import AssignmentUnpublishCommand from './subcommands/AssignmentUnpublishCommand'; import AssignmentUnpublishCommand from './subcommands/AssignmentUnpublishCommand.js';
import AssignmentCheckCommand from './subcommands/AssignmentCheckCommand'; import AssignmentCheckCommand from './subcommands/AssignmentCheckCommand.js';
import AssignmentRunCommand from './subcommands/AssignmentRunCommand'; import AssignmentRunCommand from './subcommands/AssignmentRunCommand.js';
import AssignmentCorrectionCommand from './subcommands/correction/AssignmentCorrectionCommand'; import AssignmentCorrectionCommand from './subcommands/correction/AssignmentCorrectionCommand.js';
class AssignmentCommand extends CommanderCommand { class AssignmentCommand extends CommanderCommand {
...@@ -12,7 +12,7 @@ class AssignmentCommand extends CommanderCommand { ...@@ -12,7 +12,7 @@ class AssignmentCommand extends CommanderCommand {
protected defineCommand() { protected defineCommand() {
this.command this.command
.description('manage an assignment'); .description('manage an assignment');
} }
protected defineSubCommands() { protected defineSubCommands() {
...@@ -24,7 +24,9 @@ class AssignmentCommand extends CommanderCommand { ...@@ -24,7 +24,9 @@ class AssignmentCommand extends CommanderCommand {
AssignmentCorrectionCommand.registerOnCommand(this.command); AssignmentCorrectionCommand.registerOnCommand(this.command);
} }
protected async commandAction(): Promise<void> { } protected async commandAction(): Promise<void> {
// No action
}
} }
......
import CommanderCommand from '../../CommanderCommand'; import CommanderCommand from '../../CommanderCommand.js';
import Config from '../../../config/Config'; import Config from '../../../config/Config.js';
import ora from 'ora'; import ora from 'ora';
import chalk from 'chalk'; import chalk from 'chalk';
import AssignmentValidator from '../../../sharedByClients/helpers/Dojo/AssignmentValidator'; import AssignmentValidator from '../../../sharedByClients/helpers/Dojo/AssignmentValidator.js';
import ClientsSharedAssignmentHelper from '../../../sharedByClients/helpers/Dojo/ClientsSharedAssignmentHelper'; import ClientsSharedAssignmentHelper from '../../../sharedByClients/helpers/Dojo/ClientsSharedAssignmentHelper.js';
import { Option } from 'commander'; import SharedConfig from '../../../shared/config/SharedConfig.js';
import SharedConfig from '../../../shared/config/SharedConfig'; import GlobalHelper from '../../../helpers/GlobalHelper.js';
class AssignmentCheckCommand extends CommanderCommand { class AssignmentCheckCommand extends CommanderCommand {
protected commandName: string = 'check'; protected commandName: string = 'check';
protected currentSpinner: ora.Ora = ora();
private verbose: boolean = false;
private superVerbose: boolean = false;
private buildPhase: boolean | undefined = undefined;
protected defineCommand() { protected defineCommand() {
this.command GlobalHelper.runCommandDefinition(this.command)
.description('locally run a check of an assignment') .description('locally run a check of an assignment')
.option('-p, --path <value>', 'assignment path', Config.folders.defaultLocalExercise) .action(this.commandAction.bind(this));
.option('-v, --verbose', 'verbose mode - display principal container output in live') }
.addOption(new Option('-w, --super-verbose', 'verbose mode - display all docker compose logs (build included) in live').conflicts('verbose'))
.addOption(new Option('--verbose-ssj2').hideHelp().implies({ superVerbose: true })) private logsEvent(log: string, _error: boolean, displayable: boolean, _currentStep: string, currentSubStep: string) {
.action(this.commandAction.bind(this)); for ( const line of log.split('\n') ) {
if ( displayable && this.buildPhase === undefined && line.startsWith('#') ) {
this.buildPhase = true;
}
if ( currentSubStep === 'COMPOSE_RUN' && this.buildPhase === true && line !== '' && !line.startsWith('#') ) {
this.buildPhase = false;
}
if ( SharedConfig.debug || (displayable && (this.superVerbose || this.buildPhase === false)) ) {
console.log(line);
}
}
}
private subStepEvent(name: string, message: string) {
this.currentSpinner = ora({
text : message,
indent: 4
}).start();
if ( this.verbose && name === 'COMPOSE_RUN' ) {
this.currentSpinner.info();
}
}
private endSubStepEvent(stepName: string, message: string, error: boolean) {
if ( error ) {
if ( this.verbose && stepName === 'COMPOSE_RUN' ) {
ora({
text : message,
indent: 4
}).start().fail();
} else {
this.currentSpinner.fail(message);
}
} else {
if ( this.verbose && stepName === 'COMPOSE_RUN' ) {
ora({
text : message,
indent: 4
}).start().succeed();
} else {
this.currentSpinner.succeed(message);
}
}
} }
protected async commandAction(options: { path: string, verbose: boolean, superVerbose: boolean }): Promise<void> { protected async commandAction(options: { path: string, verbose: boolean, superVerbose: boolean }): Promise<void> {
const verbose: boolean = options.verbose || options.superVerbose || SharedConfig.debug; this.superVerbose = options.superVerbose;
this.verbose = options.verbose || options.superVerbose || SharedConfig.debug;
const localExercisePath: string = options.path ?? Config.folders.defaultLocalExercise; const localExercisePath: string = options.path ?? Config.folders.defaultLocalExercise;
...@@ -30,69 +80,19 @@ class AssignmentCheckCommand extends CommanderCommand { ...@@ -30,69 +80,19 @@ class AssignmentCheckCommand extends CommanderCommand {
try { try {
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
let spinner: ora.Ora; if ( this.verbose ) {
assignmentValidator.events.on('logs', this.logsEvent.bind(this));
if ( verbose ) {
let buildPhase: boolean | undefined = undefined;
assignmentValidator.events.on('logs', (log: string, error: boolean, displayable: boolean, _currentStep: string, currentSubStep: string) => {
for ( const line of log.split('\n') ) {
if ( displayable && buildPhase == undefined && line.startsWith('#') ) {
buildPhase = true;
}
if ( currentSubStep == 'COMPOSE_RUN' && buildPhase === true && line != '' && !line.startsWith('#') ) {
buildPhase = false;
}
if ( SharedConfig.debug || (displayable && (options.superVerbose || buildPhase === false)) ) {
console.log(line);
}
}
});
} }
assignmentValidator.events.on('step', (name: string, message: string) => { assignmentValidator.events.on('step', (_name: string, message: string) => console.log(chalk.cyan(message)));
console.log(chalk.cyan(message));
});
assignmentValidator.events.on('subStep', (name: string, message: string) => { assignmentValidator.events.on('subStep', this.subStepEvent.bind(this));
spinner = ora({
text : message, assignmentValidator.events.on('endSubStep', this.endSubStepEvent.bind(this));
indent: 4
}).start(); assignmentValidator.events.on('finished', (success: boolean) => success ? resolve() : reject());
if ( verbose && name == 'COMPOSE_RUN' ) { assignmentValidator.run();
spinner.info();
}
});
assignmentValidator.events.on('endSubStep', (stepName: string, message: string, error: boolean) => {
if ( error ) {
if ( verbose && stepName == 'COMPOSE_RUN' ) {
ora({
text : message,
indent: 4
}).start().fail();
} else {
spinner.fail(message);
}
} else {
if ( verbose && stepName == 'COMPOSE_RUN' ) {
ora({
text : message,
indent: 4
}).start().succeed();
} else {
spinner.succeed(message);
}
}
});
assignmentValidator.events.on('finished', (success: boolean) => {
success ? resolve() : reject();
});
assignmentValidator.run(true);
}); });
} catch ( error ) { /* empty */ } } catch ( error ) { /* empty */ }
......
import CommanderCommand from '../../CommanderCommand'; import CommanderCommand from '../../CommanderCommand.js';
import chalk from 'chalk';
import ora from 'ora'; import ora from 'ora';
import AccessesHelper from '../../../helpers/AccessesHelper'; import SharedConfig from '../../../shared/config/SharedConfig';
import Assignment from '../../../sharedByClients/models/Assignment'; import AccessesHelper from '../../../helpers/AccessesHelper.js';
import GitlabUser from '../../../shared/types/Gitlab/GitlabUser'; import Assignment from '../../../sharedByClients/models/Assignment.js';
import GitlabManager from '../../../managers/GitlabManager'; import DojoBackendManager from '../../../managers/DojoBackendManager.js';
import DojoBackendManager from '../../../managers/DojoBackendManager'; import Toolbox from '../../../shared/helpers/Toolbox.js';
import Toolbox from '../../../shared/helpers/Toolbox'; import * as Gitlab from '@gitbeaker/rest';
import TextStyle from '../../../types/TextStyle.js';
import Config from '../../../config/Config';
import SharedSonarManager from '../../../shared/managers/SharedSonarManager';
import { Option } from 'commander';
type CommandOptions = { name: string, language: string, template?: string, members_id?: Array<number>, members_username?: Array<string>, clone?: string | boolean, sonar?: boolean, sonarStrict: boolean, gate?: string, profile?: string[] }
class AssignmentCreateCommand extends CommanderCommand { class AssignmentCreateCommand extends CommanderCommand {
protected commandName: string = 'create'; protected commandName: string = 'create';
private members!: Array<Gitlab.UserSchema> | undefined;
private templateIdOrNamespace: string | null = null;
private assignment!: Assignment;
private sonar: boolean = false;
private sonarGate: string | undefined = undefined;
private sonarProfiles: string[] = [];
protected defineCommand() { protected defineCommand() {
this.command this.command
.description('create a new repository for an assignment') .description('create a new repository for an assignment')
.requiredOption('-n, --name <name>', 'name of the assignment') .requiredOption('-n, --name <name>', 'name of the assignment')
.option('-i, --members_id <ids...>', 'list of gitlab members ids (teaching staff) to add to the repository') .requiredOption('-l, --language <string>', 'main programming language of the assignment')
.option('-u, --members_username <usernames...>', 'list of gitlab members username (teaching staff) to add to the repository') .option('-i, --members_id <ids...>', 'list of gitlab members ids (teaching staff) to add to the repository')
.option('-t, --template <string>', 'id or url of the template (http/s and ssh urls are possible)') .option('-u, --members_username <usernames...>', 'list of gitlab members username (teaching staff) to add to the repository')
.option('-c, --clone [string]', 'automatically clone the repository (SSH required) in the specified directory (this will create a subdirectory with the assignment name)') .option('-t, --template <string>', 'id or url of the template (http/s and ssh urls are possible)')
.action(this.commandAction.bind(this)); .option('-c, --clone [string]', 'automatically clone the repository (SSH required) in the specified directory (this will create a subdirectory with the assignment name)')
.action(this.commandAction.bind(this));
if ( SharedConfig.sonar.enabled ) {
this.command.requiredOption('-s, --sonar', 'add sonar to the code checking process for assignment and exercises')
.requiredOption('-d, --no-sonar', 'disable sonar for the code checking process for assignment and exercises')
.addOption(new Option('--sonar-strict', 'force the sonar gate to pass to validate the exercise results').default(false).implies({ sonar: true }))
.addOption(new Option('-g, --gate <gate>', 'quality gate for sonar').implies({ sonar: true }))
.addOption(new Option('-p, --profile <profile...>', 'quality profiles for sonar').default([]).implies({ sonar: true }));
}
} }
protected async commandAction(options: { name: string, template?: string, members_id?: Array<number>, members_username?: Array<string>, clone?: string | boolean }): Promise<void> { private async dataRetrieval(options: CommandOptions) {
let members!: Array<GitlabUser> | false; console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...'));
let templateIdOrNamespace: string | null = null;
let assignment!: Assignment;
// Check access and retrieve data await AccessesHelper.checkTeachingStaff();
{
console.log(chalk.cyan('Please wait while we verify and retrieve data...'));
if ( !await AccessesHelper.checkTeachingStaff() ) { this.members = await Config.gitlabManager.fetchMembers(options);
return; if ( !this.members ) {
} throw new Error();
}
members = await GitlabManager.fetchMembers(options); if ( options.sonar ) {
if ( !members ) { const assignmentGetSonarSpinner: ora.Ora = ora('Checking server sonar status').start();
return; this.sonar = (SharedConfig.sonar.enabled ? options.sonar ?? false : false);
if ( this.sonar && !(await DojoBackendManager.isSonarEnabled()) ) {
assignmentGetSonarSpinner.fail(`Sonar is currently not supported by the server. Disable sonar integration or try again later.`);
throw new Error();
} }
assignmentGetSonarSpinner.succeed(`Sonar is supported by the server`);
const assignmentGetSpinner: ora.Ora = ora('Checking assignment name availability').start(); this.sonarGate = options.gate;
if ( await DojoBackendManager.getAssignment(options.name) ) { this.sonarProfiles = options.profile ?? [];
assignmentGetSpinner.fail(`Assignment name "${ options.name }" is already taken. Please choose another one.`); }
return;
}
assignmentGetSpinner.succeed(`Assignment name "${ options.name }" is available`);
if ( options.template ) { const assignmentGetSpinner: ora.Ora = ora('Checking assignment name availability').start();
templateIdOrNamespace = options.template; if ( await DojoBackendManager.getAssignment(options.name) ) {
assignmentGetSpinner.fail(`Assignment name "${ options.name }" is already taken. Please choose another one.`);
throw new Error();
}
assignmentGetSpinner.succeed(`Assignment name "${ options.name }" is available`);
// Dojo languages
const languagesSpinner: ora.Ora = ora('Checking language support').start();
const languages = await DojoBackendManager.getLanguages();
if ( !languages.includes(options.language) ) {
languagesSpinner.fail(`Language "${ options.language }" is not supported. Choose a supported language or "other"`);
console.log('List of supported languages:');
for ( const l of languages ) {
console.log(` - ${ l }`);
}
throw new Error();
}
languagesSpinner.succeed(`Language "${ options.language }" is supported`);
if ( Number.isNaN(Number(templateIdOrNamespace)) ) { if ( (this.sonarGate ?? '') !== '' || this.sonarProfiles.length > 0 ) {
templateIdOrNamespace = Toolbox.urlToPath(templateIdOrNamespace as string); const qualitiesSpinner: ora.Ora = ora('Checking sonar qualities').start();
}
if ( !await DojoBackendManager.checkTemplateAccess(encodeURIComponent(templateIdOrNamespace as string)) ) { // SonarQube quality gate and profiles
return; const result = await DojoBackendManager.testSonarQualities(this.sonarGate ?? '', this.sonarProfiles);
} if ( !result.valid ) {
const invalid = (result.badGate == undefined ? result.badProfiles : [ result.badGate, ...result.badProfiles ]);
qualitiesSpinner.fail(`Invalid quality gate or profiles : ${ invalid }`);
throw new Error();
} }
qualitiesSpinner.succeed(`Quality gate and profiles are valid`);
} }
// Create the assignment // SonarQube languages
{ if ( this.sonar ) {
console.log(chalk.cyan('Please wait while we are creating the assignment (approximately 10 seconds)...')); const sonarLang = await DojoBackendManager.getSonarLanguages();
if ( !sonarLang.includes(SharedSonarManager.mapLanguage(options.language)) ) {
try { languagesSpinner.fail(`Language "${ options.language }" is not supported with Sonar. Choose a supported language or disable sonar`);
assignment = await DojoBackendManager.createAssignment(options.name, members, templateIdOrNamespace); throw new Error();
const oraInfo = (message: string) => {
ora({
text : message,
indent: 4
}).start().info();
};
oraInfo(`${ chalk.magenta('Name:') } ${ assignment.name }`);
oraInfo(`${ chalk.magenta('Web URL:') } ${ assignment.gitlabCreationInfo.web_url }`);
oraInfo(`${ chalk.magenta('HTTP Repo:') } ${ assignment.gitlabCreationInfo.http_url_to_repo }`);
oraInfo(`${ chalk.magenta('SSH Repo:') } ${ assignment.gitlabCreationInfo.ssh_url_to_repo }`);
} catch ( error ) {
return;
} }
} }
// Clone the repository if ( options.template ) {
{ this.templateIdOrNamespace = options.template;
if ( options.clone ) {
console.log(chalk.cyan('Please wait while we are cloning the repository...')); if ( Number.isNaN(Number(this.templateIdOrNamespace)) ) {
this.templateIdOrNamespace = Toolbox.urlToPath(this.templateIdOrNamespace);
}
await GitlabManager.cloneRepository(options.clone, assignment.gitlabCreationInfo.ssh_url_to_repo, undefined, true, 0); if ( !await DojoBackendManager.checkTemplateAccess(this.templateIdOrNamespace) ) {
throw new Error();
} }
} }
} }
private async createAssignment(options: CommandOptions) {
console.log(TextStyle.BLOCK('Please wait while we are creating the assignment (approximately 10 seconds)...'));
this.assignment = await DojoBackendManager.createAssignment(options.name, options.language, this.members!, this.templateIdOrNamespace, this.sonar, !options.sonarStrict, this.sonarGate, this.sonarProfiles);
const oraInfo = (message: string) => {
ora({
text : message,
indent: 4
}).start().info();
};
oraInfo(`${ TextStyle.LIST_ITEM_NAME('Name:') } ${ this.assignment.name }`);
oraInfo(`${ TextStyle.LIST_ITEM_NAME('Web URL:') } ${ this.assignment.gitlabCreationInfo.web_url }`);
oraInfo(`${ TextStyle.LIST_ITEM_NAME('HTTP Repo:') } ${ this.assignment.gitlabCreationInfo.http_url_to_repo }`);
oraInfo(`${ TextStyle.LIST_ITEM_NAME('SSH Repo:') } ${ this.assignment.gitlabCreationInfo.ssh_url_to_repo }`);
if ( this.assignment.useSonar ) {
oraInfo(`${ TextStyle.LIST_ITEM_NAME('Sonar project:') } ${ SharedConfig.sonar.url }/dashboard?id=${ this.assignment.sonarKey }`);
}
}
private async cloneRepository(options: CommandOptions) {
if ( options.clone ) {
console.log(TextStyle.BLOCK('Please wait while we are cloning the repository...'));
await Config.gitlabManager.cloneRepository(options.clone, this.assignment.gitlabCreationInfo.ssh_url_to_repo, undefined, true, 0);
}
}
protected async commandAction(options: CommandOptions): Promise<void> {
try {
await this.dataRetrieval(options);
await this.createAssignment(options);
await this.cloneRepository(options);
} catch ( e ) { /* Do nothing */ }
}
} }
......
import AssignmentPublishUnpublishCommandBase from './AssignmentPublishUnpublishCommandBase'; import AssignmentPublishUnpublishCommandBase from './AssignmentPublishUnpublishCommandBase.js';
class AssignmentPublishCommand extends AssignmentPublishUnpublishCommandBase { class AssignmentPublishCommand extends AssignmentPublishUnpublishCommandBase {
......
import CommanderCommand from '../../CommanderCommand'; import CommanderCommand from '../../CommanderCommand.js';
import inquirer from 'inquirer'; import inquirer from 'inquirer';
import chalk from 'chalk'; import SessionManager from '../../../managers/SessionManager.js';
import SessionManager from '../../../managers/SessionManager';
import ora from 'ora'; import ora from 'ora';
import DojoBackendManager from '../../../managers/DojoBackendManager'; import DojoBackendManager from '../../../managers/DojoBackendManager.js';
import Assignment from '../../../sharedByClients/models/Assignment'; import Assignment from '../../../sharedByClients/models/Assignment.js';
import SharedAssignmentHelper from '../../../shared/helpers/Dojo/SharedAssignmentHelper'; import SharedAssignmentHelper from '../../../shared/helpers/Dojo/SharedAssignmentHelper.js';
import TextStyle from '../../../types/TextStyle.js';
abstract class AssignmentPublishUnpublishCommandBase extends CommanderCommand { abstract class AssignmentPublishUnpublishCommandBase extends CommanderCommand {
...@@ -13,10 +13,10 @@ abstract class AssignmentPublishUnpublishCommandBase extends CommanderCommand { ...@@ -13,10 +13,10 @@ abstract class AssignmentPublishUnpublishCommandBase extends CommanderCommand {
protected defineCommand() { protected defineCommand() {
this.command this.command
.description(`${ this.publish ? 'publish' : 'unpublish' } an assignment`) .description(`${ this.publish ? 'publish' : 'unpublish' } an assignment`)
.argument('<name or url>', 'name or url (http/s or ssh) of the assignment') .argument('<name or url>', 'name or url (http/s or ssh) of the assignment')
.option('-f, --force', 'don\'t ask for confirmation') .option('-f, --force', 'don\'t ask for confirmation')
.action(this.commandAction.bind(this)); .action(this.commandAction.bind(this));
} }
protected async commandAction(assignmentNameOrUrl: string, options: { force: boolean }): Promise<void> { protected async commandAction(assignmentNameOrUrl: string, options: { force: boolean }): Promise<void> {
...@@ -35,7 +35,7 @@ abstract class AssignmentPublishUnpublishCommandBase extends CommanderCommand { ...@@ -35,7 +35,7 @@ abstract class AssignmentPublishUnpublishCommandBase extends CommanderCommand {
let assignment!: Assignment | undefined; let assignment!: Assignment | undefined;
{ {
console.log(chalk.cyan('Please wait while we verify and retrieve data...')); console.log(TextStyle.BLOCK('Please wait while we verify and retrieve data...'));
if ( !await SessionManager.testSession(true, null) ) { if ( !await SessionManager.testSession(true, null) ) {
return; return;
...@@ -82,7 +82,7 @@ abstract class AssignmentPublishUnpublishCommandBase extends CommanderCommand { ...@@ -82,7 +82,7 @@ abstract class AssignmentPublishUnpublishCommandBase extends CommanderCommand {
} }
{ {
console.log(chalk.cyan(`Please wait while we ${ this.publish ? 'publish' : 'unpublish' } the assignment...`)); console.log(TextStyle.BLOCK(`Please wait while we ${ this.publish ? 'publish' : 'unpublish' } the assignment...`));
try { try {
await DojoBackendManager.changeAssignmentPublishedStatus(assignment, this.publish); await DojoBackendManager.changeAssignmentPublishedStatus(assignment, this.publish);
......
import CommanderCommand from '../../CommanderCommand'; import CommanderCommand from '../../CommanderCommand.js';
import Config from '../../../config/Config'; import ExerciseRunHelper from '../../../helpers/Dojo/ExerciseRunHelper.js';
import { Option } from 'commander'; import GlobalHelper from '../../../helpers/GlobalHelper.js';
import ExerciseRunHelper from '../../../helpers/Dojo/ExerciseRunHelper';
class AssignmentRunCommand extends CommanderCommand { class AssignmentRunCommand extends CommanderCommand {
protected commandName: string = 'run'; protected commandName: string = 'run';
protected defineCommand() { protected defineCommand() {
// This command is synced with the "exercise run" command GlobalHelper.runCommandDefinition(this.command)
this.command .description('locally run the assignment as an exercise')
.description('locally run the assignment as an exercise') .action(this.commandAction.bind(this));
.option('-p, --path <value>', 'exercise path', Config.folders.defaultLocalExercise)
.option('-v, --verbose', 'verbose mode - display principal container output in live')
.addOption(new Option('-w, --super-verbose', 'verbose mode - display all docker compose logs (build included) in live').conflicts('verbose'))
.addOption(new Option('--verbose-ssj2').hideHelp().implies({ superVerbose: true }))
.action(this.commandAction.bind(this));
} }
protected async commandAction(options: { path: string, verbose: boolean, superVerbose: boolean }): Promise<void> { protected async commandAction(options: { path: string, verbose: boolean, superVerbose: boolean }): Promise<void> {
await ExerciseRunHelper.run(options); await (new ExerciseRunHelper(options)).run();
} }
} }
......