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 1454 additions and 1685 deletions
import { Command } from 'commander';
import { existsSync, renameSync } from 'fs';
import ora from 'ora';
import TextStyle from '../types/TextStyle.js';
import inquirer from 'inquirer';
import fs from 'fs-extra';
function renameFile(filename: string, showWarning: boolean) {
const oldFilename = `${ filename }.old`;
const spinner: ora.Ora = ora(`Renaming ${ TextStyle.CODE(filename) } in ${ TextStyle.CODE(oldFilename) } ...`).start();
try {
renameSync(filename, oldFilename);
spinner.succeed(`Renaming success: ${ TextStyle.CODE(filename) } in ${ TextStyle.CODE(oldFilename) }`);
if ( showWarning ) {
console.log(`${ TextStyle.WARNING('Warning:') } Your ${ TextStyle.CODE(filename) } was renamed ${ TextStyle.CODE(oldFilename) }. If this was not intended please revert this change.`);
}
} catch ( error ) {
spinner.fail(`Renaming failed: ${ error }.`);
}
}
async function askConfirmation(msg: string): Promise<boolean> {
return (await inquirer.prompt({
name : 'confirm',
message: msg,
type : 'confirm',
default: false
})).confirm as boolean;
}
// Returns false, when the renaming is interrupted
export async function tryRenameFile(path: string, force: boolean): Promise<boolean> {
const fileExists = existsSync(path);
if ( fileExists && force ) {
renameFile(path, false);
} else if ( fileExists ) {
const confirm = (await askConfirmation(`${ TextStyle.CODE(path) } in ${ TextStyle.CODE(path + '.old') }. Are you sure?`));
if ( confirm ) {
renameFile(path, true);
} else {
console.log(`${ TextStyle.BLOCK('Completion generation interrupted.') }`);
return false;
}
}
return true;
}
const fishFunction = `
function __fish_dojo_using_commands
set cmd (commandline -opc)
set num_cmd (count $cmd)
if [ $num_cmd -eq $argv[1] ]
for i in (seq 1 (math $num_cmd))
if [ $argv[(math $i+1)] != $cmd[$i] ]
return 1
end
end
return 0
end
return 1
end
complete -f -c dojo
`;
function isHidden(cmd: Command): boolean {
return (cmd as Command & { _hidden: boolean })._hidden;
}
function isLeaf(cmd: Command): boolean {
return cmd.commands.length === 0;
}
function flatten(cmd: Command): Array<Command> {
if ( isLeaf(cmd) ) {
return [ cmd ];
} else {
return cmd.commands
.filter(c => !isHidden(c))
.map(child => flatten(child))
.reduce((acc, subCmd) => acc.concat(subCmd), [ cmd ]);
}
}
// Computes the maximum number of commands until a leaf is reached
function computeDepth(cmd: Command | undefined): number {
if ( cmd === undefined ) {
return 0;
} else {
return 1 + cmd.commands.filter(c => !isHidden(c)).map(subCmd => computeDepth(subCmd)).reduce((acc, depth) => depth > acc ? depth : acc, 0);
}
}
// Computes the maximum number of commands until the root is reached
function computeHeight(cmd: Command | null): number {
let height = 0;
let tmp = cmd;
while ( tmp !== null ) {
tmp = tmp.parent;
height += 1;
}
return height;
}
// Computes the maximum number of commands until the root is reached
export function getRoot(cmd: Command): Command {
if ( cmd.parent == null ) {
return cmd;
} else {
return getRoot(cmd.parent);
}
}
function getOptions(cmd: Command): string {
// we remove <args>, [command], and , from option lines
return cmd.options.filter(opt => !opt.hidden).map(opt => opt.flags.replace(/<.*?>/, '').replace(/\[.*?]/, '').replace(',', '').trimEnd()).join(' ');
}
function commandsAndOptionsToString(cmd: Command): string {
return cmd.commands.filter(c => !isHidden(c)).map(c => c.name()).join(' ').concat(' ' + getOptions(cmd)).trim().concat(' --help -h').trim();
}
function addLine(identLevel: number, pattern: string): string {
return `${ ' '.repeat(identLevel) }${ pattern }\n`;
}
export function getCommandFromChain(currentCmd: Command, chain: Array<string>): Command | null {
if ( chain.length === 0 ) {
return currentCmd;
} else {
const subCmd = currentCmd.commands.find(c => c.name() === chain[0]);
if ( subCmd === undefined ) {
return currentCmd;
} else {
return getCommandFromChain(subCmd, chain.slice(1));
}
}
}
function generateBashSubCommands(cmd: Command, current: number, maxDepth: number, ident: number): string {
if ( current === maxDepth ) {
return addLine(ident, `case "\${COMP_WORDS[$COMP_CWORD - ${ maxDepth - current + 1 }]}" in`) + addLine(ident + 1, `${ cmd.name() })`) + addLine(ident + 2, `words="${ commandsAndOptionsToString(cmd) }"`) + addLine(ident + 1, ';;') + addLine(ident + 1, '*)') + addLine(ident + 1, ';;') + addLine(ident, 'esac');
} else {
let data = addLine(ident, `case "\${COMP_WORDS[$COMP_CWORD - ${ maxDepth - current + 1 }]}" in`) + addLine(ident + 1, `${ cmd.name() })`);
cmd.commands.filter(c => !isHidden(c)).forEach(subCmd => {
data += generateBashSubCommands(subCmd, current + 1, maxDepth, ident + 2);
});
data += addLine(ident + 1, ';;') + addLine(ident + 1, '*)') + addLine(ident + 1, ';;') + addLine(ident, 'esac');
return data;
}
}
export function generateBashCompletion(root: Command): string {
const depth = computeDepth(root);
let data = addLine(0, '#/usr/bin/env bash\nfunction _dojo_completions()') + addLine(0, '{') + addLine(1, 'latest="${COMP_WORDS[$COMP_CWORD]}"');
for ( let i = 1 ; i <= depth ; i++ ) {
data += addLine(1, `${ i === 1 ? 'if' : 'elif' } [ $COMP_CWORD -eq ${ depth - i + 1 } ]`) + addLine(1, 'then');
data += generateBashSubCommands(root, i, depth, 2);
}
data += addLine(1, 'fi') + addLine(1, 'COMPREPLY=($(compgen -W "$words" -- $latest))') + addLine(1, 'return 0') + addLine(0, '}') + addLine(0, 'complete -F _dojo_completions dojo');
return data;
}
const prefix = 'complete -f -c dojo -n \'__fish_dojo_using_commands';
function generateCommandChain(cmd: Command | null): string {
let data = '';
while ( cmd !== null ) {
data = cmd.name().concat(` ${ data }`);
cmd = cmd.parent;
}
return data.trimEnd();
}
function hasOptions(cmd: Command): boolean {
return cmd.options.length > 0;
}
function optionsToString(cmd: Command): string {
return cmd.options.filter(opt => !opt.hidden).map(opt => `${ prefix } ${ computeHeight(cmd) } ${ generateCommandChain(cmd) }' -a "${ opt.short ?? '' } ${ opt.long ?? '' }" -d "${ opt.description }"`).join('\n').concat('\n');
}
export function generateFishCompletion(root: Command): string {
const commands = flatten(root);
return fishFunction.concat(// add completions for options
commands.filter(c => !isHidden(c)).filter(cmd => hasOptions(cmd)).map(cmd => optionsToString(cmd)).filter(str => str !== '').join('')).concat(// add completions for commands and subcommands
commands.filter(c => !isHidden(c)).filter(cmd => !isLeaf(cmd)).map(cmd => cmd.commands.filter(c => !isHidden(c)).map(subCmd => `${ prefix } ${ computeHeight(cmd) } ${ generateCommandChain(cmd) }' -a ${ subCmd.name() } -d "${ subCmd.description() }"`).join('\n').concat('\n')).join(''));
}
export function updateRcFile(shellType: 'bash' | 'zsh', filePath: string, completionCommand: string) {
const spinner: ora.Ora = ora(`Modifying ${ filePath } ...`).start();
if ( fs.existsSync(filePath) ) {
const data = fs.readFileSync(filePath);
let updated = false;
try {
if ( !data.includes(completionCommand) ) {
fs.appendFileSync(filePath, completionCommand);
updated = true;
}
} catch {
spinner.fail(`Error appending in ${ filePath }`);
return;
}
spinner.succeed(updated ? `${ shellType } updated. Please restart your shell session.` : `${ shellType } already up to date.`);
} else {
try {
fs.writeFileSync(filePath, completionCommand);
spinner.succeed(`${ shellType } written. Please restart your shell session.`);
} catch ( error ) {
spinner.fail(`Error writing in ${ filePath }`);
}
}
}
// The following code should create a bash completion automatically from the Commander
// CLI library. The file should look something like that (it looks at the time
// this comment is written).
// #/usr/bin/env bash
// function _dojo_completions()
// {
// latest="${COMP_WORDS[$COMP_CWORD]}"
// if [ $COMP_CWORD -eq 3 ]
// then
// case "${COMP_WORDS[$COMP_CWORD - 3]}" in
// dojo)
// case "${COMP_WORDS[$COMP_CWORD - 2]}" in
// session)
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// login)
// words="-c --cli --help -h"
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// logout)
// words="-f --force --help -h"
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// test)
// words="--help -h"
// ;;
// *)
// ;;
// esac
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 2]}" in
// assignment)
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// create)
// words="-n --name -i --members_id -u --members_username -t --template -c --clone --help -h"
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// check)
// words="-p --path -v --verbose -w --super-verbose --help -h"
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// run)
// words="-p --path -v --verbose -w --super-verbose --help -h"
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// publish)
// words="-f --force --help -h"
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// unpublish)
// words="-f --force --help -h"
// ;;
// *)
// ;;
// esac
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 2]}" in
// exercise)
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// create)
// words="-a --assignment -i --members_id -u --members_username -c --clone --help -h"
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// run)
// words="-p --path -v --verbose -w --super-verbose --help -h"
// ;;
// *)
// ;;
// esac
// ;;
// *)
// ;;
// esac
// ;;
// *)
// ;;
// esac
// elif [ $COMP_CWORD -eq 2 ]
// then
// case "${COMP_WORDS[$COMP_CWORD - 2]}" in
// dojo)
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// session)
// words="login logout test --help -h"
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// assignment)
// words="create check run publish unpublish --help -h"
// ;;
// *)
// ;;
// esac
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// exercise)
// words="create run --help -h"
// ;;
// *)
// ;;
// esac
// ;;
// *)
// ;;
// esac
// elif [ $COMP_CWORD -eq 1 ]
// then
// case "${COMP_WORDS[$COMP_CWORD - 1]}" in
// dojo)
// words="session assignment exercise -V --version -H --host --help -h"
// ;;
// *)
// ;;
// esac
// fi
// COMPREPLY=($(compgen -W "$words" -- $latest))
// return 0
// }
// complete -F _dojo_completions dojo
import Exercise from '../../sharedByClients/models/Exercise';
import ora from 'ora';
import TextStyle from '../../types/TextStyle';
import inquirer from 'inquirer';
import Config from '../../config/Config';
import DojoBackendManager from '../../managers/DojoBackendManager';
import SharedConfig from '../../shared/config/SharedConfig';
class ExerciseHelper {
/**
* Clone the exercise repository
* @param exercise
* @param providedPath If a string is provided, the repository will be cloned in the specified directory. If true, the repository will be cloned in the current directory. If false, the repository will not be cloned, and if undefined, the user will be prompted for the path.
*/
async clone(exercise: Exercise, providedPath: string | boolean | undefined) {
if ( providedPath === false ) {
return;
}
let path: string | boolean = './';
if ( providedPath === undefined ) {
path = (await inquirer.prompt([ {
type : 'input',
name : 'path',
message: `Please enter the path (blank, '.' or './' for current directory):`
} ])).path;
} else {
path = providedPath;
}
console.log(TextStyle.BLOCK('Please wait while we are cloning the repository...'));
await Config.gitlabManager.cloneRepository(path === '' ? true : path, exercise.gitlabCreationInfo.ssh_url_to_repo, `DojoExercise_${ exercise.assignmentName }`, true, 0);
}
async delete(exerciseIdOrUrl: string) {
console.log(TextStyle.BLOCK('Please wait while we are deleting the exercise...'));
await DojoBackendManager.deleteExercise(exerciseIdOrUrl);
}
async actionMenu(exercise: Exercise): Promise<void> {
// eslint-disable-next-line no-constant-condition
while ( true ) {
const action: string = (await inquirer.prompt([ {
type : 'list',
name : 'action',
message: 'What action do you want to do on the exercise ?',
choices: [ {
name : 'Display details of the exercise',
value: 'info'
}, new inquirer.Separator(), {
name : 'Clone (SSH required) in current directory (will create a subdirectory)',
value: 'cloneInCurrentDirectory'
}, {
name : 'Clone (SSH required) in the specified directory (will create a subdirectory)',
value: 'clone'
}, new inquirer.Separator(), {
name : 'Delete the exercise',
value: 'delete'
}, new inquirer.Separator(), {
name : 'Exit',
value: 'exit'
}, new inquirer.Separator() ]
} ])).action;
switch ( action ) {
case 'info':
await this.displayDetails(exercise, false);
break;
case 'cloneInCurrentDirectory':
await this.clone(exercise, true);
break;
case 'clone':
await this.clone(exercise, undefined);
break;
case 'delete':
await this.delete(exercise.id);
return;
case 'exit':
return;
default:
ora().fail('Invalid option.');
return;
}
}
}
async displayDetails(exercise: Exercise, showActionMenu: boolean = false): Promise<void> {
ora().info(`Details of the exercise:`);
const oraInfo = (message: string, indent: number = 4) => {
ora({
text : message,
indent: indent
}).start().info();
};
oraInfo(`${ TextStyle.LIST_ITEM_NAME('Id:') } ${ exercise.id }`);
oraInfo(`${ TextStyle.LIST_ITEM_NAME('Name:') } ${ exercise.name }`);
oraInfo(`${ TextStyle.LIST_ITEM_NAME('Assignment:') } ${ exercise.assignmentName }`);
// Display exercise teachers
if ( exercise.assignment?.staff && exercise.assignment?.staff.length > 0 ) {
oraInfo(`${ TextStyle.LIST_ITEM_NAME('Teachers:') }`);
exercise.assignment?.staff.forEach(staff => {
console.log(` - ${ staff.gitlabUsername }`);
});
} else {
ora({
text : `${ TextStyle.LIST_ITEM_NAME('Teachers:') } No teachers found for this exercise.`,
indent: 4
}).start().warn();
}
// Display exercise members
if ( exercise.members && exercise.members.length > 0 ) {
oraInfo(`${ TextStyle.LIST_ITEM_NAME('Members:') }`);
exercise.members.forEach(member => {
console.log(` - ${ member.gitlabUsername }`);
});
} else {
ora({
text : `${ TextStyle.LIST_ITEM_NAME('Members:') } No members found for this exercise.`,
indent: 4
}).start().warn();
}
if ( exercise.assignment?.useSonar ) {
oraInfo(`${ TextStyle.LIST_ITEM_NAME('Sonar project:') } ${ SharedConfig.sonar.url }/dashboard?id=${ exercise.sonarKey }`);
}
oraInfo(`${ TextStyle.LIST_ITEM_NAME('Gitlab URL:') } ${ exercise.gitlabCreationInfo.web_url }`);
oraInfo(`${ TextStyle.LIST_ITEM_NAME('HTTP Repo:') } ${ exercise.gitlabCreationInfo.http_url_to_repo }`);
oraInfo(`${ TextStyle.LIST_ITEM_NAME('SSH Repo:') } ${ exercise.gitlabCreationInfo.ssh_url_to_repo }`);
if ( showActionMenu ) {
await this.actionMenu(exercise);
}
}
}
export default new ExerciseHelper();
\ No newline at end of file
import ora from 'ora';
import chalk from 'chalk';
import Config from '../../config/Config';
import AssignmentFile from '../../shared/types/Dojo/AssignmentFile';
import ExerciseDockerCompose from '../../sharedByClients/helpers/Dojo/ExerciseDockerCompose';
import ExerciseResultsSanitizerAndValidator from '../../sharedByClients/helpers/Dojo/ExerciseResultsSanitizerAndValidator';
import Config from '../../config/Config.js';
import AssignmentFile from '../../shared/types/Dojo/AssignmentFile.js';
import ExerciseDockerCompose from '../../sharedByClients/helpers/Dojo/ExerciseDockerCompose.js';
import ExerciseResultsSanitizerAndValidator from '../../sharedByClients/helpers/Dojo/ExerciseResultsSanitizerAndValidator.js';
import fs from 'node:fs';
import ClientsSharedConfig from '../../sharedByClients/config/ClientsSharedConfig';
import SharedAssignmentHelper from '../../shared/helpers/Dojo/SharedAssignmentHelper';
import SharedAssignmentHelper from '../../shared/helpers/Dojo/SharedAssignmentHelper.js';
import path from 'path';
import ExerciseCheckerError from '../../shared/types/Dojo/ExerciseCheckerError';
import ClientsSharedExerciseHelper from '../../sharedByClients/helpers/Dojo/ClientsSharedExerciseHelper';
import ExerciseCheckerError from '../../shared/types/Dojo/ExerciseCheckerError.js';
import ClientsSharedExerciseHelper from '../../sharedByClients/helpers/Dojo/ClientsSharedExerciseHelper.js';
import os from 'os';
import util from 'util';
import { exec } from 'child_process';
import SharedConfig from '../../shared/config/SharedConfig';
import SharedConfig from '../../shared/config/SharedConfig.js';
import TextStyle from '../../types/TextStyle.js';
import ClientsSharedConfig from '../../sharedByClients/config/ClientsSharedConfig';
const execAsync = util.promisify(exec);
......@@ -30,6 +31,25 @@ class ExerciseRunHelper {
private readonly fileComposeLogs: string = path.join(this.folderResultsDojo, `dockerComposeLogs.txt`);
private readonly options: { path: string, verbose: boolean, superVerbose: boolean };
private readonly verbose: boolean;
private readonly localExercisePath: string;
private assignmentFile!: AssignmentFile;
private exerciseDockerCompose!: ExerciseDockerCompose;
private exerciseResultsValidation!: ExerciseResultsSanitizerAndValidator;
private haveResultsVolume!: boolean;
private exerciseRunSpinner!: ora.Ora;
private buildPhase: boolean | undefined = undefined;
constructor(options: { path: string, verbose: boolean, superVerbose: boolean }) {
this.options = options;
this.verbose = options.verbose || options.superVerbose || SharedConfig.debug;
this.localExercisePath = options.path ?? Config.folders.defaultLocalExercise;
}
private displayExecutionLogs() {
ora({
text : `${ chalk.magenta('Execution logs folder:') } ${ this.folderResultsVolume }`,
......@@ -37,20 +57,12 @@ class ExerciseRunHelper {
}).start().info();
}
async run(options: { path: string, verbose: boolean, superVerbose: boolean }): Promise<void> {
const verbose: boolean = options.verbose || options.superVerbose || SharedConfig.debug;
const localExercisePath: string = options.path ?? Config.folders.defaultLocalExercise;
let assignmentFile: AssignmentFile;
let exerciseDockerCompose: ExerciseDockerCompose;
let exerciseResultsValidation: ExerciseResultsSanitizerAndValidator;
let haveResultsVolume: boolean;
// Step 1: Check requirements (if it's an exercise folder and if Docker daemon is running)
{
console.log(chalk.cyan('Please wait while we are checking and creating dependencies...'));
/**
* Step 1: Check requirements (if it's an exercise folder and if Docker daemon is running)
* @private
*/
private async checkRequirements() {
console.log(TextStyle.BLOCK('Please wait while we are checking and creating dependencies...'));
// Create result temp folder
fs.mkdirSync(this.folderResultsVolume, { recursive: true });
......@@ -70,12 +82,12 @@ class ExerciseRunHelper {
indent: 8
}).start();
const files = fs.readdirSync(options.path);
const files = fs.readdirSync(this.options.path);
const missingFiles = Config.exercise.neededFiles.map((file: string): [ string, boolean ] => [ file, files.includes(file) ]).filter((file: [ string, boolean ]) => !file[1]);
if ( missingFiles.length > 0 ) {
spinner.fail(`The exercise folder is missing the following files: ${ missingFiles.map((file: [ string, boolean ]) => file[0]).join(', ') }`);
return;
throw new Error();
}
spinner.succeed(`The exercise folder contains all the needed files`);
......@@ -88,15 +100,15 @@ class ExerciseRunHelper {
indent: 8
}).start();
const validationResults = SharedAssignmentHelper.validateDescriptionFile(path.join(options.path, ClientsSharedConfig.assignment.filename));
const validationResults = SharedAssignmentHelper.validateDescriptionFile(path.join(this.options.path, ClientsSharedConfig.assignment.filename));
if ( !validationResults.isValid ) {
spinner.fail(`The ${ ClientsSharedConfig.assignment.filename } file is invalid: ${ validationResults.error }`);
return;
throw new Error();
} else {
assignmentFile = validationResults.content!;
this.assignmentFile = validationResults.content!;
}
haveResultsVolume = assignmentFile.result.volume !== undefined;
this.haveResultsVolume = this.assignmentFile.result.volume !== undefined;
spinner.succeed(`The ${ ClientsSharedConfig.assignment.filename } file is valid`);
}
......@@ -112,123 +124,127 @@ class ExerciseRunHelper {
await execAsync(`docker ps`);
} catch ( error ) {
spinner.fail(`The Docker daemon is not running`);
return;
throw new Error();
}
spinner.succeed(`The Docker daemon is running`);
}
}
// Step 2: Run docker-compose file
{
console.log(chalk.cyan('Please wait while we are running the exercise...'));
let composeFileOverride: string[] = [];
const composeOverridePath: string = path.join(localExercisePath, 'docker-compose-override.yml');
if ( haveResultsVolume ) {
const composeOverride = fs.readFileSync(path.join(__dirname, '../../../assets/docker-compose-override.yml'), 'utf8').replace('{{VOLUME_NAME}}', assignmentFile.result.volume!).replace('{{MOUNT_PATH}}', this.folderResultsExercise);
fs.writeFileSync(composeOverridePath, composeOverride);
composeFileOverride = [ composeOverridePath ];
}
exerciseDockerCompose = new ExerciseDockerCompose(this.projectName, assignmentFile, localExercisePath, composeFileOverride);
try {
await new Promise<void>((resolve, reject) => {
let spinner: ora.Ora;
if ( verbose ) {
let buildPhase: boolean | undefined = undefined;
exerciseDockerCompose.events.on('logs', (log: string, _error: boolean, displayable: boolean, currentStep: string) => {
private logsEvent(log: string, _error: boolean, displayable: boolean, currentStep: string) {
for ( const line of log.split('\n') ) {
if ( displayable && buildPhase == undefined && line.startsWith('#') ) {
buildPhase = true;
if ( displayable && this.buildPhase === undefined && line.startsWith('#') ) {
this.buildPhase = true;
}
if ( currentStep == 'COMPOSE_RUN' && buildPhase === true && line != '' && !line.startsWith('#') ) {
buildPhase = false;
if ( currentStep === 'COMPOSE_RUN' && this.buildPhase === true && line !== '' && !line.startsWith('#') ) {
this.buildPhase = false;
}
if ( SharedConfig.debug || (displayable && (options.superVerbose || buildPhase === false)) ) {
if ( SharedConfig.debug || (displayable && (this.options.superVerbose || this.buildPhase === false)) ) {
console.log(line);
}
}
});
}
exerciseDockerCompose.events.on('step', (name: string, message: string) => {
spinner = ora({
private stepEvent(name: string, message: string) {
this.exerciseRunSpinner = ora({
text : message,
indent: 4
}).start();
if ( verbose && name == 'COMPOSE_RUN' ) {
spinner.info();
if ( this.verbose && name === 'COMPOSE_RUN' ) {
this.exerciseRunSpinner.info();
}
}
});
exerciseDockerCompose.events.on('endStep', (stepName: string, message: string, error: boolean) => {
private endStepEvent(stepName: string, message: string, error: boolean) {
if ( error ) {
if ( verbose && stepName == 'COMPOSE_RUN' ) {
if ( this.verbose && stepName === 'COMPOSE_RUN' ) {
ora({
text : message,
indent: 4
}).start().fail();
} else {
spinner.fail(message);
this.exerciseRunSpinner.fail(message);
}
} else {
if ( verbose && stepName == 'COMPOSE_RUN' ) {
} else if ( this.verbose && stepName === 'COMPOSE_RUN' ) {
ora({
text : message,
indent: 4
}).start().succeed();
} else {
spinner.succeed(message);
this.exerciseRunSpinner.succeed(message);
}
}
});
exerciseDockerCompose.events.on('finished', (success: boolean) => {
success ? resolve() : reject();
});
/**
* Step 2: Run docker-compose file
* @private
*/
private async runDockerCompose() {
console.log(TextStyle.BLOCK('Please wait while we are running the exercise...'));
let composeFileOverride: string[] = [];
const composeOverridePath: string = path.join(this.localExercisePath, 'docker-compose-override.yml');
if ( this.haveResultsVolume ) {
const composeOverride = fs.readFileSync(path.join(__dirname, '../../../assets/docker-compose-override.yml'), 'utf8').replace('{{VOLUME_NAME}}', this.assignmentFile.result.volume!).replace('{{MOUNT_PATH}}', this.folderResultsExercise);
fs.writeFileSync(composeOverridePath, composeOverride);
composeFileOverride = [ composeOverridePath ];
}
this.exerciseDockerCompose = new ExerciseDockerCompose(this.projectName, this.assignmentFile, this.localExercisePath, composeFileOverride);
try {
await new Promise<void>((resolve, reject) => {
if ( this.verbose ) {
this.exerciseDockerCompose.events.on('logs', this.logsEvent.bind(this));
}
this.exerciseDockerCompose.events.on('step', this.stepEvent.bind(this));
this.exerciseDockerCompose.events.on('endStep', this.endStepEvent.bind(this));
exerciseDockerCompose.run(true);
this.exerciseDockerCompose.events.on('finished', (success: boolean) => success ? resolve() : reject());
this.exerciseDockerCompose.run(true);
});
} catch ( error ) { /* empty */ }
fs.rmSync(composeOverridePath, { force: true });
fs.writeFileSync(this.fileComposeLogs, exerciseDockerCompose.allLogs);
fs.writeFileSync(this.fileComposeLogs, this.exerciseDockerCompose.allLogs);
if ( !exerciseDockerCompose.success ) {
if ( !this.exerciseDockerCompose.success ) {
this.displayExecutionLogs();
return;
throw new Error();
}
}
/**
* Step 3: Get results
* @private
*/
private async getResults() {
console.log(TextStyle.BLOCK('Please wait while we are checking the results...'));
// Step 3: Get results
{
console.log(chalk.cyan('Please wait while we are checking the results...'));
exerciseResultsValidation = new ExerciseResultsSanitizerAndValidator(this.folderResultsDojo, this.folderResultsExercise, exerciseDockerCompose.exitCode);
this.exerciseResultsValidation = new ExerciseResultsSanitizerAndValidator(this.folderResultsDojo, this.folderResultsExercise, this.exerciseDockerCompose.exitCode);
try {
await new Promise<void>((resolve, reject) => {
let spinner: ora.Ora;
exerciseResultsValidation.events.on('step', (_name: string, message: string) => {
this.exerciseResultsValidation.events.on('step', (_name: string, message: string) => {
spinner = ora({
text : message,
indent: 4
}).start();
});
exerciseResultsValidation.events.on('endStep', (stepName: string, message: string, error: boolean) => {
this.exerciseResultsValidation.events.on('endStep', (stepName: string, message: string, error: boolean) => {
if ( error ) {
if ( stepName == 'CHECK_SIZE' ) {
if ( stepName === 'CHECK_SIZE' ) {
spinner.warn(message);
} else {
spinner.fail(message);
......@@ -238,30 +254,42 @@ class ExerciseRunHelper {
}
});
exerciseResultsValidation.events.on('finished', (success: boolean, exitCode: number) => {
success || exitCode == ExerciseCheckerError.EXERCISE_RESULTS_FOLDER_TOO_BIG ? resolve() : reject();
this.exerciseResultsValidation.events.on('finished', (success: boolean, exitCode: number) => {
success || exitCode === ExerciseCheckerError.EXERCISE_RESULTS_FOLDER_TOO_BIG.valueOf() ? resolve() : reject();
});
exerciseResultsValidation.run();
this.exerciseResultsValidation.run();
});
} catch ( error ) {
this.displayExecutionLogs();
return;
throw new Error();
}
}
// Step 4: Display results + Volume location
{
/**
* Step 4: Display results + Volume location
* @private
*/
private displayResults() {
const info = chalk.magenta.bold.italic;
ClientsSharedExerciseHelper.displayExecutionResults(exerciseResultsValidation.exerciseResults!, exerciseDockerCompose.exitCode, {
ClientsSharedExerciseHelper.displayExecutionResults(this.exerciseResultsValidation.exerciseResults, this.exerciseDockerCompose.exitCode, {
INFO : info,
SUCCESS: chalk.green,
FAILURE: chalk.red
}, `\n\n${ info('Execution results folder: ') }${ this.folderResultsVolume }`);
}
async run(): Promise<void> {
try {
await this.checkRequirements();
await this.runDockerCompose();
await this.getResults();
this.displayResults();
} catch ( error ) {
return;
}
}
}
export default new ExerciseRunHelper();
export default ExerciseRunHelper;
import { Command, Option } from 'commander';
import SessionManager from '../managers/SessionManager.js';
import TextStyle from '../types/TextStyle.js';
import ora from 'ora';
import DojoBackendManager from '../managers/DojoBackendManager.js';
import Assignment from '../sharedByClients/models/Assignment.js';
import Config from '../config/Config';
class GlobalHelper {
public runCommandDefinition(command: Command, isAssignment: boolean = true): Command {
command
.option('-p, --path <value>', `${ isAssignment ? 'assignment' : '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 }));
return command;
}
public readonly refreshGitlabTokenFunction = async () => {
await SessionManager.refreshTokens();
return SessionManager.gitlabCredentials.accessToken ?? '';
};
public async checkAssignmentCorrectionAccess(assignmentName: string): Promise<Assignment | undefined> {
console.log(TextStyle.BLOCK('Please wait while we check access...'));
const assignmentGetSpinner: ora.Ora = ora('Checking if assignment exists').start();
const assignment = await DojoBackendManager.getAssignment(assignmentName);
if ( !assignment ) {
assignmentGetSpinner.fail(`The assignment doesn't exists`);
return undefined;
}
assignmentGetSpinner.succeed(`The assignment exists`);
const assignmentAccessSpinner: ora.Ora = ora('Checking assignment access').start();
if ( assignment.staff.find(staff => staff.id === SessionManager.profile?.id) === undefined ) {
assignmentAccessSpinner.fail(`You are not in the staff of the assignment`);
return undefined;
}
assignmentAccessSpinner.succeed(`You are in the staff of the assignment`);
return assignment;
}
}
export default new GlobalHelper();
\ No newline at end of file
import path from 'node:path';
import dotenv from 'dotenv';
import dotenvExpand from 'dotenv-expand';
import './shared/helpers/TypeScriptExtensions.js';
dotenvExpand.expand(dotenv.config({
path : path.join(__dirname, '../.env'),
DOTENV_KEY: 'dotenv://:key_fc323d8e0a02349342f1c6a119bb38495958ce3a43a87d19a3f674b7e2896dcb@dotenv.local/vault/.env.vault?environment=development'
}));
import axios from 'axios';
import ora from 'ora';
import GitlabUser from '../shared/types/Gitlab/GitlabUser';
import GitlabRoute from '../shared/types/Gitlab/GitlabRoute';
import SharedConfig from '../shared/config/SharedConfig';
import GitlabRepository from '../shared/types/Gitlab/GitlabRepository';
import fs from 'fs-extra';
import { spawn } from 'child_process';
import { NotificationSettingSchema, UserSchema } from '@gitbeaker/rest';
import * as GitlabCore from '@gitbeaker/core';
import SharedGitlabManager from '../shared/managers/SharedGitlabManager.js';
import GlobalHelper from '../helpers/GlobalHelper.js';
class GitlabManager {
private getApiUrl(route: GitlabRoute): string {
return `${ SharedConfig.gitlab.apiURL }${ route }`;
type getGitlabUser = (param: number | string) => Promise<UserSchema | undefined>
class GitlabManager extends SharedGitlabManager {
constructor(public gitlabUrl: string, clientId?: string, urlRedirect?: string, urlToken?: string) {
super(gitlabUrl, '', clientId, urlRedirect, urlToken, GlobalHelper.refreshGitlabTokenFunction.bind(GlobalHelper));
}
public async testToken(verbose: boolean = true): Promise<[ boolean, boolean ]> {
......@@ -35,7 +37,7 @@ class GitlabManager {
}
try {
notificationSettings = (await this.getNotificationSettings()).data as NotificationSettings;
notificationSettings = await this.getNotificationSettings();
result[0] = true;
......@@ -63,7 +65,7 @@ class GitlabManager {
try {
const oldSettings = notificationSettings;
const newSettings = { level: someLevelTypes[someLevelTypes[0] == oldSettings.level ? 1 : 0] };
const newSettings = { level: someLevelTypes[someLevelTypes[0] === oldSettings.level ? 1 : 0] };
await this.setNotificationSettings(newSettings);
await this.setNotificationSettings(oldSettings);
......@@ -83,15 +85,15 @@ class GitlabManager {
return result;
}
public getNotificationSettings() {
return axios.get(this.getApiUrl(GitlabRoute.NOTIFICATION_SETTINGS));
public getNotificationSettings(): Promise<NotificationSettingSchema> {
return this.executeGitlabRequest(() => this.api.NotificationSettings.show());
}
public setNotificationSettings(newSettings: Record<string, string>) {
return axios.put(this.getApiUrl(GitlabRoute.NOTIFICATION_SETTINGS), { params: new URLSearchParams(newSettings) });
public setNotificationSettings(newSettings: GitlabCore.EditNotificationSettingsOptions) {
return this.executeGitlabRequest(() => this.api.NotificationSettings.edit(newSettings));
}
private async getGitlabUsers(paramsToSearch: Array<string | number>, paramName: string, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<GitlabUser | undefined>> {
private async getGitlabUsers(paramsToSearch: Array<string | number>, searchFunction: getGitlabUser, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<UserSchema | undefined>> {
try {
return await Promise.all(paramsToSearch.map(async param => {
const spinner: ora.Ora = ora({
......@@ -101,21 +103,20 @@ class GitlabManager {
if ( verbose ) {
spinner.start();
}
const params: { [key: string]: unknown } = {};
params[paramName] = param;
const user = await axios.get<Array<GitlabUser>>(this.getApiUrl(GitlabRoute.USERS_GET), { params: params });
if ( user.data[0] ) {
const gitlabUser = user.data[0];
const user = await searchFunction(param);
if ( user ) {
if ( verbose ) {
spinner.succeed(`${ gitlabUser.username } (${ gitlabUser.id })`);
spinner.succeed(`${ user.username } (${ user.id })`);
}
return gitlabUser;
return user;
} else {
if ( verbose ) {
spinner.fail(`${ param }`);
}
return undefined;
}
}));
} catch ( e ) {
......@@ -123,30 +124,26 @@ class GitlabManager {
}
}
public async getUsersById(ids: Array<number>, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<GitlabUser | undefined>> {
return await this.getGitlabUsers(ids, 'id', verbose, verboseIndent);
}
public async getUsersByUsername(usernames: Array<string>, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<GitlabUser | undefined>> {
return await this.getGitlabUsers(usernames, 'search', verbose, verboseIndent);
public async getUsersById(ids: Array<number>, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<UserSchema | undefined>> {
return this.getGitlabUsers(ids, this.getUserById.bind(this) as getGitlabUser, verbose, verboseIndent);
}
public async getRepository(repoId: number): Promise<GitlabRepository> {
return await axios.get(this.getApiUrl(GitlabRoute.REPOSITORY_GET).replace('{{id}}', repoId.toString()));
public async getUsersByUsername(usernames: Array<string>, verbose: boolean = false, verboseIndent: number = 0): Promise<Array<UserSchema | undefined>> {
return this.getGitlabUsers(usernames, this.getUserByUsername.bind(this) as getGitlabUser, verbose, verboseIndent);
}
public async fetchMembers(options: { members_id?: Array<number>, members_username?: Array<string> }): Promise<Array<GitlabUser> | false> {
public async fetchMembers(options: { members_id?: Array<number>, members_username?: Array<string> }): Promise<Array<UserSchema> | undefined> {
if ( options.members_id || options.members_username ) {
ora('Checking Gitlab members:').start().info();
}
let members: Array<GitlabUser> = [];
let members: Array<UserSchema> = [];
async function getMembers<T>(context: unknown, functionName: string, paramsToSearch: Array<T>): Promise<boolean> {
const result = await ((context as { [functionName: string]: (arg: Array<T>, verbose: boolean, verboseIndent: number) => Promise<Array<GitlabUser | undefined>> })[functionName])(paramsToSearch, true, 8);
async function isUsersExists<T>(context: unknown, functionName: string, paramsToSearch: Array<T>): Promise<boolean> {
const users = await ((context as { [functionName: string]: (arg: Array<T>, verbose: boolean, verboseIndent: number) => Promise<Array<UserSchema | undefined>> })[functionName])(paramsToSearch, true, 8);
if ( result.every(user => user) ) {
members = members.concat(result as Array<GitlabUser>);
if ( users.every(user => user) ) {
members = members.concat(users as Array<UserSchema>);
return true;
} else {
return false;
......@@ -161,7 +158,7 @@ class GitlabManager {
indent: 4
}).start().info();
result = await getMembers(this, 'getUsersById', options.members_id);
result = await isUsersExists(this, 'getUsersById', options.members_id);
}
if ( options.members_username ) {
......@@ -170,16 +167,10 @@ class GitlabManager {
indent: 4
}).start().info();
result = result && await getMembers(this, 'getUsersByUsername', options.members_username);
result = result && await isUsersExists(this, 'getUsersByUsername', options.members_username);
}
if ( !result ) {
return false;
}
members = members.removeObjectDuplicates(gitlabUser => gitlabUser.id);
return members;
return result ? members.removeObjectDuplicates(gitlabUser => gitlabUser.id) : undefined;
}
public async cloneRepository(clonePath: string | boolean, repositorySshUrl: string, folderName?: string, verbose: boolean = false, verboseIndent: number = 0) {
......@@ -200,13 +191,13 @@ class GitlabManager {
try {
await new Promise<void>((resolve, reject) => {
const gitClone = spawn(`git clone ${ repositorySshUrl } "${ folderName ?? '' }"`, {
const gitClone = spawn(`git clone ${ repositorySshUrl } "${ folderName?.replace(' ', '_') ?? '' }"`, {
cwd : path,
shell: true
});
gitClone.on('exit', (code) => {
code !== null && code == 0 ? resolve() : reject();
gitClone.on('exit', code => {
code !== null && code === 0 ? resolve() : reject();
});
});
......@@ -222,4 +213,4 @@ class GitlabManager {
}
export default new GitlabManager();
export default GitlabManager;
import axios, { AxiosError, AxiosRequestHeaders } from 'axios';
import SessionManager from './SessionManager';
import SessionManager from './SessionManager.js';
import FormData from 'form-data';
import { StatusCodes } from 'http-status-codes';
import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig';
import { version } from '../config/Version';
import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse';
import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode';
import { version } from '../config/Version.js';
import DojoBackendResponse from '../shared/types/Dojo/DojoBackendResponse.js';
import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode.js';
import boxen from 'boxen';
import Config from '../config/Config';
import SharedConfig from '../shared/config/SharedConfig';
import { stateConfigFile } from '../config/ConfigFiles';
import TextStyle from '../types/TextStyle.js';
import ConfigFiles from '../config/ConfigFiles';
import ClientsSharedConfig from '../sharedByClients/config/ClientsSharedConfig';
class HttpManager {
......@@ -21,16 +20,15 @@ class HttpManager {
}
private registerRequestInterceptor() {
axios.interceptors.request.use((config) => {
axios.interceptors.request.use(config => {
if ( config.data instanceof FormData ) {
config.headers = { ...config.headers, ...(config.data as FormData).getHeaders() } as AxiosRequestHeaders;
config.headers = { ...config.headers, ...config.data.getHeaders() } as AxiosRequestHeaders;
}
if ( config.url && (config.url.indexOf(ClientsSharedConfig.apiURL) !== -1) ) {
if ( config.url && config.url.indexOf(ClientsSharedConfig.apiURL) !== -1 ) {
config.headers['Accept-Encoding'] = 'gzip';
if ( config.data && Object.keys(config.data).length > 0 ) {
if ( config.data && Object.keys(config.data as { [key: string]: unknown }).length > 0 ) {
config.headers['Content-Type'] = 'multipart/form-data';
}
......@@ -42,10 +40,6 @@ class HttpManager {
config.headers['client-version'] = version;
}
if ( SessionManager.gitlabCredentials.accessToken && config.url && config.url.indexOf(SharedConfig.gitlab.apiURL) !== -1 ) {
config.headers.Authorization = `Bearer ${ SessionManager.gitlabCredentials.accessToken }`;
}
return config;
});
}
......@@ -63,78 +57,68 @@ class HttpManager {
process.exit(1);
}
private registerResponseInterceptor() {
axios.interceptors.response.use((response) => {
if ( response.data && response.data.sessionToken ) {
SessionManager.apiToken = response.data.sessionToken;
}
if ( response.headers['dojocli-latest-version'] ) {
const latestDojoCliVersion = response.headers['dojocli-latest-version'];
const storedLatestDojoCliVersion = stateConfigFile.getParam('latestDojoCliVersion') as string | null || '0.0.0';
if ( latestDojoCliVersion !== storedLatestDojoCliVersion ) {
stateConfigFile.setParam('latestDojoCliVersion', latestDojoCliVersion);
stateConfigFile.setParam('latestDojoCliVersionNotification', 0);
}
}
return response;
}, async (error) => {
if ( error.response ) {
const originalConfig = error.config;
const isFromApi = error.response.config.url && error.response.config.url.indexOf(ClientsSharedConfig.apiURL) !== -1;
const isFromGitlab = error.response.config.url && error.response.config.url.indexOf(SharedConfig.gitlab.URL) !== -1;
// Try to refresh the Gitlab tokens if the request have failed with a 401 error
if ( error.response.status === StatusCodes.UNAUTHORIZED && isFromGitlab && !originalConfig._retry ) {
originalConfig._retry = true;
try {
await SessionManager.refreshTokens();
return axios(originalConfig);
} catch ( error: unknown ) {
if ( error instanceof AxiosError ) {
if ( error.response && error.response.data ) {
return Promise.reject(error.response.data);
}
}
return Promise.reject(error);
}
}
if ( error.response.status === StatusCodes.METHOD_NOT_ALLOWED && isFromApi && error.response.data ) {
const data: DojoBackendResponse<void> = error.response.data;
private apiMethodNotAllowed(error: AxiosError, isFromApi: boolean) {
if ( error.response?.status === StatusCodes.METHOD_NOT_ALLOWED && isFromApi && error.response.data ) {
const data: DojoBackendResponse<void> = error.response.data as DojoBackendResponse<void>;
switch ( data.code ) {
case DojoStatusCode.CLIENT_NOT_SUPPORTED:
case DojoStatusCode.CLIENT_NOT_SUPPORTED.valueOf():
this.requestError('Client not recognized by the server. Please contact the administrator.');
break;
case DojoStatusCode.CLIENT_VERSION_NOT_SUPPORTED:
this.requestError(`CLI version not anymore supported by the server. Please update the CLI.\nYou can download the latest stable version on this page:\n${ Config.gitlab.cliReleasePage }`);
case DojoStatusCode.CLIENT_VERSION_NOT_SUPPORTED.valueOf():
this.requestError(`CLI version not anymore supported by the server. Please update the CLI by executing this command:\n${ TextStyle.CODE(' dojo upgrade ') }`);
break;
default:
break;
}
}
}
private apiAuthorizationError(error: AxiosError, isFromApi: boolean) {
if ( this.handleAuthorizationCommandErrors && isFromApi && error.response ) {
const errorCustomCode = (error.response.data as DojoBackendResponse<unknown> | undefined)?.code ?? error.response.status;
if ( this.handleAuthorizationCommandErrors ) {
if ( isFromApi ) {
if ( errorCustomCode === error.response.status ) {
switch ( error.response.status ) {
case StatusCodes.UNAUTHORIZED:
case StatusCodes.UNAUTHORIZED.valueOf():
this.requestError('Session expired or does not exist. Please login again.');
break;
case StatusCodes.FORBIDDEN:
case StatusCodes.FORBIDDEN.valueOf():
this.requestError('Forbidden access.');
break;
default:
this.requestError('Unknown error.');
break;
}
}
} else {
this.handleAuthorizationCommandErrors = true;
}
}
private registerResponseInterceptor() {
axios.interceptors.response.use(response => {
if ( response.data && response.data.sessionToken ) {
SessionManager.apiToken = response.data.sessionToken;
}
if ( response.headers['dojocli-latest-version'] ) {
const latestDojoCliVersion = response.headers['dojocli-latest-version'];
const storedLatestDojoCliVersion = ConfigFiles.stateConfigFile.getParam('latestDojoCliVersion') as string | null || '0.0.0';
if ( latestDojoCliVersion !== storedLatestDojoCliVersion ) {
ConfigFiles.stateConfigFile.setParam('latestDojoCliVersion', latestDojoCliVersion);
ConfigFiles.stateConfigFile.setParam('latestDojoCliVersionNotification', 0);
}
}
return response;
}, async error => {
if ( error instanceof AxiosError && error.response ) {
const isFromApi = error.response.config.url !== undefined && error.response.config.url.indexOf(ClientsSharedConfig.apiURL) !== -1;
this.apiMethodNotAllowed(error, isFromApi);
this.apiAuthorizationError(error, isFromApi);
} else {
this.requestError('Error connecting to the server. Please check your internet connection. If the problem persists, please contact the administrator.');
}
......
This diff is collapsed.
Subproject commit 9e3f29d2f313ef96944a199da0db39f1827c496a
Subproject commit 937081e68f6127b669daca30e57c43e73b9c96c9
Subproject commit 4efff1c5127c6f84104016d7041d0cf281d981f8
Subproject commit 59308a719fdee1a2025e90a18774d56fc6d11d9b
import chalk from 'chalk';
class TextStyle {
public readonly BLOCK = chalk.cyan;
public readonly CODE = chalk.bgHex('F7F7F7').grey.italic;
public readonly LIST_ITEM_NAME = chalk.magenta;
public readonly LIST_SUBITEM_NAME = chalk.green;
public readonly QUESTION = chalk.greenBright;
public readonly TIPS = chalk.blueBright;
public readonly URL = chalk.blue.underline;
public readonly WARNING = chalk.red;
}
export default new TextStyle();
\ No newline at end of file
......@@ -6,11 +6,21 @@
"target" : "ES2022",
"module" : "commonjs",
"sourceMap" : true,
"noImplicitAny" : true,
"esModuleInterop": true,
"moduleResolution": "node",
"noImplicitAny" : true
"lib" : [
"ES2022",
"DOM"
],
"types" : [
"node"
]
},
"exclude" : [
"node_modules"
],
"include" : [
"src/**/*.ts",
"eslint.config.mjs"
]
}
\ No newline at end of file
# DojoCLI
# Documentation of `The Dojo CLI` utility
More informations about the DojoCLI can be found in the [wiki](https://gitedu.hesge.ch/dojo_project/projects/ui/dojocli/-/wikis/home).
\ No newline at end of file
All documentations are available on the [Dojo documentation website](https://www.hepiapp.ch/) : https://www.hepiapp.ch/.
\ No newline at end of file
# Dojo: a platform to practice programming
The dojo platform is an online tool built to help practice programming by
allowing users to propose assignments and perform them as exercises.
The tool is very flexible and allows for proposing exercises for any language
and does not impose any limitation on a framework to be heavily relying
on Docker and Gitlab. These tools used in combination allow for automatic
correction of assignments in order to give immediate feedback to users
performing exercises. Solved exercises can then be shared among the community
of users such that they can inspire other users or give hints on ways to solve
a given exercise.
The two major concepts of the platform are the **assignments** and the **exercises**.
The principal way to interact with the Dojo platform is currently the `dojo` CLI.
## The assignment
An assignment is written by a user that wants to propose an exercise. It is typically composed of a written description of the work to be performed,
and tests that must be passed once the exercise is successfully performed (and some configuration files for the infrastructure of the tests
such as docker files). At its core, an assignment is
nothing else than a git repository that can be forked in the form of an exercise and modified using standard git commands.
For a more detailed description please see the [CLI documentation](home).
An assignment can be proposed by any user.
In the future a dependency tree of assignments can be created, as well as tagging for filtering purposes.
## The exercise
An exercise is an instance of an assignment which the learner will modify in order to make it pass the automatic tests.
It can be run locally on any user's machine using the dojo CLI. When the exercise is completed
it is pushed on the dojo where the CI/CD tools of Gitlab can evaluate it automatically and
notify the dojo platform of the result. The exercises can then be shared with other users
in order to propose a wide variety of solutions and can be a base for discussion among users
and with teachers.
For a more detailed description please see the [CLI documentation](home).
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.