Skip to content
Snippets Groups Projects
Commit 870f7371 authored by michael.minelli's avatar michael.minelli
Browse files

Merge branch 'completion-improvement' into v3.6.0

parents 45ba3540 bfa3332b
Branches
Tags
No related merge requests found
Pipeline #30172 passed
......@@ -6,3 +6,5 @@
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# GitHub Copilot persisted chat sessions
/copilot/chatSessions
......@@ -14,7 +14,21 @@ import SessionCommand from './auth/SessionCommand';
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;
}
return this.hasToExecuteHook(actionCommand.parent);
}
}
constructor() {
this.program
......@@ -30,11 +44,16 @@ class CommanderApp {
.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())
.hook('preAction', () => {
this.warnDevelopmentVersion();
}).hook('postAction', () => {
this.informNewVersion();
});
.hook('preAction', (_thisCommand: Command, actionCommand: Command) => {
if ( this.hasToExecuteHook(actionCommand) ) {
this.warnDevelopmentVersion();
}
})
.hook('postAction', (_thisCommand: Command, actionCommand: Command) => {
if ( this.hasToExecuteHook(actionCommand) ) {
this.informNewVersion();
}
});
this.program.on('option:host', () => {
ClientsSharedConfig.apiURL = this.program.opts().host;
......
import CommanderCommand from '../CommanderCommand';
import CompletionBashCommand from './subcommands/CompletionBashCommand';
import CompletionFishCommand from './subcommands/CompletionFishCommand';
import CompletionZshCommand from './subcommands/CompletionZshCommand';
import CommanderCommand from '../CommanderCommand';
import CompletionCreateUpdateCommand from './subcommands/CompletionCreateUpdateCommand';
import CompletionGetCommand from './subcommands/CompletionGetCommand';
import CompletionScriptCommand from './subcommands/CompletionScriptCommand';
class CompletionCommand extends CommanderCommand {
......@@ -13,9 +13,9 @@ class CompletionCommand extends CommanderCommand {
}
protected defineSubCommands() {
CompletionBashCommand.registerOnCommand(this.command);
CompletionFishCommand.registerOnCommand(this.command);
CompletionZshCommand.registerOnCommand(this.command);
CompletionCreateUpdateCommand.registerOnCommand(this.command);
CompletionGetCommand.registerOnCommand(this.command);
CompletionScriptCommand.registerOnCommand(this.command);
}
protected async commandAction(): Promise<void> {
......
import CommanderCommand from '../../CommanderCommand';
import { generateBashCompletion, getRoot, tryRenameFile } from '../../../helpers/AutoCompletionHelper';
import ora from 'ora';
import TextStyle from '../../../types/TextStyle';
import fs from 'fs-extra';
import path from 'path';
import os from 'os';
import GlobalHelper from '../../../helpers/GlobalHelper';
class CompletionBashCommand extends CommanderCommand {
protected commandName: string = 'bash';
private readonly installPath = path.join(os.homedir(), '.bash_completion');
protected defineCommand() {
GlobalHelper.completionCommandDefinition(this.command)
.description('generate bash completion')
.action(this.commandAction.bind(this));
}
private writeFile(filename: string, showInstructions: boolean) {
const spinner: ora.Ora = ora(`Writing Bash completion in ${ TextStyle.CODE(filename) } ...`).start();
try {
fs.mkdirsSync(path.dirname(filename));
fs.writeFileSync(filename, generateBashCompletion(getRoot(this.command)));
spinner.succeed(`Bash completion successfully written in ${ TextStyle.CODE(filename) }`);
if ( showInstructions ) {
console.log(`
The easiest way to install the completion is to append the content of the generated file to the end of the ${ TextStyle.CODE('~/.bash_completion') } file or to overwrite it, if it only contains the 'dojo' completion.
This can be performed by either
${ TextStyle.CODE(`
cat ${ filename } > ~/.bash_completion # overwrites .bash_completion
cat ${ filename } >> ~/.bash_completion # appends to .bash_completion
`) }
For more details: ${ TextStyle.URL('https://github.com/scop/bash-completion/blob/master/README.md') }
`);
}
} catch ( error ) {
spinner.fail(`Bash completion error: ${ error }`);
}
}
/* The completion command must do the following:
- if a file is provided:
- if force is not enabled:
- check if the bash completion file exists:
- if it exists, prompt the user that it will be overwritten
- if ok is given write the file and prompt that a backup has been created
- else create the file containing the completion
- else
- if force is not enabled:
- check if the default file exists:
- if it exists, prompt the user that it will be erased:
- if ok is given write the file and prompt that a backup has been created
- else
- create the file containing the completion
- create a .zprofile or append the appropriate commands into the .zprofile file
*/
protected async commandAction(options: { file: string, force: boolean }): Promise<void> {
const filePath = path.resolve(options.file ?? this.installPath); // change that if file is empty
const showInstructions = !!options.file;
if ( !(await tryRenameFile(filePath, options.force)) ) { // means renaming was interrupted
return;
}
this.writeFile(filePath, showInstructions);
}
}
export default new CompletionBashCommand();
\ No newline at end of file
import { generateFishCompletion, getRoot, tryRenameFile } from '../../../helpers/AutoCompletionHelper';
import CommanderCommand from '../../CommanderCommand';
import ora from 'ora';
import TextStyle from '../../../types/TextStyle';
import path from 'path';
import os from 'os';
import fs from 'fs-extra';
import GlobalHelper from '../../../helpers/GlobalHelper';
import CommanderCommand from '../../CommanderCommand';
import { Option } from 'commander';
import { generateFishCompletion, getRoot, tryRenameFile, updateRcFile } from '../../../helpers/AutoCompletionHelper';
import os, { homedir } from 'os';
import path from 'path';
import ora from 'ora';
import fs from 'fs-extra';
import TextStyle from '../../../types/TextStyle';
class CompletionFishCommand extends CommanderCommand {
protected commandName: string = 'fish';
class CompletionCreateUpdateCommand extends CommanderCommand {
protected commandName: string = 'create';
protected aliasNames: Array<string> = [ 'update' ];
private readonly installPath = path.join(os.homedir(), '.config/fish/completions/dojo.fish');
protected defineCommand() {
GlobalHelper.completionCommandDefinition(this.command)
.description('generate fish completion')
this.command.description('generate shell completion')
.addOption(new Option('-s, --shell <shell>', 'shell type').choices([ 'bash', 'zsh', 'fish' ]).makeOptionMandatory(true))
.addOption(new Option('-f, --file <filename>', '(only for fish shell)').implies({ shell: 'fish' }))
.addOption(new Option('-y, --force', 'don\'t ask for file overwrite confirmation (only for fish shell)').implies({ shell: 'fish' }))
.action(this.commandAction.bind(this));
}
private writeFile(filename: string, showInstructions: boolean) {
const spinner: ora.Ora = ora(`Writing fish completion in ${ filename }...`).start();
try {
fs.mkdirsSync(path.dirname(filename));
fs.writeFileSync(filename, generateFishCompletion(getRoot(this.command)));
spinner.succeed(`Fish completion successfully written in ${ filename }.`);
if ( showInstructions ) {
const cpCommand = ` cp -i ${ filename } ~/.config/fish/completions # interactive cp to avoid accidents `;
console.log(`
The easiest way to install the completion is to copy the ${ TextStyle.CODE(filename) } into the ${ TextStyle.CODE('~/.config/fish/completions') } directory.
${ TextStyle.CODE(cpCommand) }`);
}
} catch ( error ) {
spinner.fail(`Fish completion error: ${ error }.`);
}
private bash() {
const completionCommand = `
# Added by DojoCLI
eval "$(dojo completion script bash)"
`;
updateRcFile('bash', path.join(os.homedir(), '.bashrc'), completionCommand);
}
private zsh() {
const completionCommand = `
# Added by DojoCLI
source <(dojo completion script zsh)
`;
updateRcFile('zsh', path.join(homedir(), '.zshrc'), completionCommand);
}
/* The completion command must do the following:
- if a file is provided:
......@@ -56,15 +52,50 @@ ${ TextStyle.CODE(cpCommand) }`);
- else
- create the file containing the completion
*/
protected async commandAction(options: { file: string, force: boolean }): Promise<void> {
const filePath = path.resolve(options.file ?? this.installPath); // change that if file is empty
private async fish(options: { file: string, force: boolean }) {
const filePath = path.resolve(options.file ?? path.join(os.homedir(), '.config/fish/completions/dojo.fish'));
const showInstructions = !!options.file;
if ( !(await tryRenameFile(filePath, options.force)) ) { // means renaming was interrupted
return;
}
this.writeFile(filePath, showInstructions);
const spinner: ora.Ora = ora(`Writing fish completion in ${ filePath }...`).start();
try {
fs.mkdirsSync(path.dirname(filePath));
fs.writeFileSync(filePath, generateFishCompletion(getRoot(this.command)));
spinner.succeed(`Fish completion successfully written in ${ filePath }.`);
if ( showInstructions ) {
const cpCommand = ` cp -i ${ filePath } ~/.config/fish/completions # interactive cp to avoid accidents `;
console.log(`
The easiest way to install the completion is to copy the ${ TextStyle.CODE(filePath) } into the ${ TextStyle.CODE('~/.config/fish/completions') } directory.
${ TextStyle.CODE(cpCommand) }`);
}
} catch ( error ) {
spinner.fail(`Fish completion error: ${ error }.`);
}
}
protected async commandAction(options: { shell: 'bash' | 'zsh' | 'fish', file: string, force: boolean }): Promise<void> {
switch ( options.shell ) {
case 'bash':
this.bash();
break;
case 'zsh':
this.zsh();
break;
case 'fish':
await this.fish(options);
break;
default:
console.error('Unsupported shell.');
break;
}
}
}
export default new CompletionFishCommand();
\ No newline at end of file
export default new CompletionCreateUpdateCommand();
\ No newline at end of file
import CommanderCommand from '../../CommanderCommand';
import { Command, CommandOptions, Option } from 'commander';
import * as AutoCompletionHelper from '../../../helpers/AutoCompletionHelper';
type CompletionProposal = { name: string, description: string };
class CompletionGetCommand extends CommanderCommand {
protected commandName: string = 'get';
protected options: CommandOptions = {
hidden: true
};
protected defineCommand() {
this.command.description('generate completion options for a given command')
.addOption(new Option('-s, --shell <shell>', 'shell completion result format').choices([ 'bash', 'zsh' ]))
.argument('<commands...>', 'command chain to complete')
.action(this.commandAction.bind(this));
}
private completion(commandsChain: Array<string>, displayFunction: (completionProposals: Array<CompletionProposal>) => void) {
const command = AutoCompletionHelper.getCommandFromChain(AutoCompletionHelper.getRoot(this.command), commandsChain.slice(1));
if ( command ) {
const commands = command.commands.filter(cmd => !(cmd as Command & { _hidden: boolean })._hidden);
const options = command.options.filter(option => !option.hidden);
displayFunction([ ...commands.flatMap(cmd => [ {
name : cmd.name(),
description: cmd.description()
}, ...cmd.aliases().map(alias => ({
name : alias,
description: cmd.description()
})) ]), ...options.flatMap(option => [ {
name : option.long,
description: option.description
}, {
name : option.short,
description: option.description
} ]) ].filter(proposal => proposal.name) as Array<CompletionProposal>);
}
}
private bashCompletion(commandsChain: Array<string>) {
this.completion(commandsChain, (completionProposals: Array<CompletionProposal>) => console.log(completionProposals.map(proposal => proposal.name).join(' ')));
}
private zshCompletion(commandsChain: Array<string>) {
this.completion(commandsChain, (completionProposals: Array<CompletionProposal>) => completionProposals.forEach(proposal => console.log(`${ proposal.name }:${ proposal.description }`)));
}
protected async commandAction(commandsChain: Array<string>, options: { shell: 'bash' | 'zsh' }): Promise<void> {
switch ( options.shell ) {
case 'bash':
this.bashCompletion(commandsChain);
break;
case 'zsh':
this.zshCompletion(commandsChain);
break;
default:
console.error('Unsupported shell completion format');
break;
}
}
}
export default new CompletionGetCommand();
\ No newline at end of file
import CommanderCommand from '../../CommanderCommand';
import { Argument } from 'commander';
class CompletionScriptCommand extends CommanderCommand {
protected commandName: string = 'script';
protected defineCommand() {
this.command.description('generate script completion')
.addArgument(new Argument('<shell>', 'shell completion format').choices([ 'bash', 'zsh' ]))
.action(this.commandAction.bind(this));
}
private bashCompletionScript() {
console.log(`
#/usr/bin/env bash
###-begin-dojo-completions-###
#
# dojo command completion script for bash
#
# Installation: dojo completion bash
#
function _dojo_completions()
{
latest="\${COMP_WORDS[$COMP_CWORD]}"
words=$(dojo completion get --shell bash \${COMP_WORDS[@]})
COMPREPLY=($(compgen -W "$words" -- $latest))
return 0
}
complete -F _dojo_completions dojo
###-end-dojo-completions-###
`);
}
private zshCompletionScript() {
console.log(`
#compdef dojo
###-begin-dojo-completions-###
#
# dojo command completion script for zsh
#
# Installation: dojo completion zsh
#
_dojo_completions()
{
local reply
local si=$IFS
IFS=$'
' reply=($(dojo completion get --shell zsh \${words[@]}))
IFS=$si
_describe 'values' reply
}
compdef _dojo_completions dojo
###-end-dojo-completions-###
`);
}
protected async commandAction(shell: 'bash' | 'zsh'): Promise<void> {
switch ( shell ) {
case 'bash':
this.bashCompletionScript();
break;
case 'zsh':
this.zshCompletionScript();
break;
default:
console.error('Unsupported shell completion format');
break;
}
}
}
export default new CompletionScriptCommand();
\ No newline at end of file
import CommanderCommand from '../../CommanderCommand';
import { generateBashCompletion, getRoot, tryRenameFile } from '../../../helpers/AutoCompletionHelper';
import ora from 'ora';
import TextStyle from '../../../types/TextStyle';
import path from 'path';
import { homedir } from 'os';
import fs from 'fs-extra';
import GlobalHelper from '../../../helpers/GlobalHelper';
class CompletionZshCommand extends CommanderCommand {
protected commandName: string = 'zsh';
private readonly zprofile: string = path.join(homedir(), '.zprofile');
private readonly bashCompletion = path.join(homedir(), '.bash_completion');
private readonly loadBashCompletion = `
# Added by DojoCLI
autoload -U +X compinit && compinit
autoload -U +X bashcompinit && bashcompinit
source ${ this.bashCompletion }
`;
protected defineCommand() {
GlobalHelper.completionCommandDefinition(this.command)
.description('generate zsh completion, which is derived from the bash completion')
.action(this.commandAction.bind(this));
}
private writeFile(filename: string, showInstructions: boolean) {
const spinner: ora.Ora = ora(`Writing Bash completion in ${ TextStyle.CODE(filename) } ...`).start();
try {
fs.mkdirsSync(path.dirname(filename));
fs.writeFileSync(filename, generateBashCompletion(getRoot(this.command)));
spinner.succeed(`Bash completion successfully written in ${ TextStyle.CODE(filename) }`);
if ( showInstructions ) {
const zprofileContent = TextStyle.CODE(`
autoload -U +X compinit && compinit
autoload -U +X bashcompinit && bashcompinit
source ${ filename }
`);
console.log(`
The easiest way to install the completion is to append the content of the generated file to the end of the ${ TextStyle.CODE(filename) } file or to overwrite it, if it only contains the 'dojo' completion.
This can be performed by either ${ TextStyle.CODE(`
cat ${ filename } > ~/.bash_completion # overwrites .bash_completion
cat ${ filename } >> ~/.bash_completion # appends to .bash_completion`) }
For more details: ${ TextStyle.URL('https://github.com/scop/bash-completion/blob/master/README.md') }
Next add the following lines to your ${ TextStyle.CODE('~/.zprofile') } file: ${ zprofileContent } `);
}
} catch ( error ) {
spinner.fail(`Bash completion writing error: ${ error }`);
}
}
protected addToZprofile(zprofilePath: string, bashPath: string) {
const spinner: ora.Ora = ora(`Modifying ${ zprofilePath } ...`).start();
if ( fs.existsSync(zprofilePath) ) {
const data = fs.readFileSync(zprofilePath);
let updated = false;
try {
if ( !data.includes('autoload -U +X compinit && compinit') ) {
fs.appendFileSync(zprofilePath, '\nautoload -U +X compinit && compinit');
updated = true;
}
if ( !data.includes('autoload -U +X bashcompinit && bashcompinit') ) {
fs.appendFileSync(zprofilePath, '\nautoload -U +X bashcompinit && bashcompinit');
updated = true;
}
if ( !data.includes(`source ${ bashPath }`) ) {
fs.appendFileSync(zprofilePath, `\nsource ${ bashPath }`);
updated = true;
}
} catch {
spinner.fail(`Error appending in ${ this.zprofile }`);
return;
}
spinner.succeed(updated ? `Zsh profile updated.` : `Zsh profile already up to date.`);
} else {
try {
fs.writeFileSync(zprofilePath, this.loadBashCompletion);
spinner.succeed(`Zsh profile written.`);
} catch ( error ) {
spinner.fail(`Error writing in ${ this.zprofile }`);
}
}
}
/* The completion command must do the following:
- if a file is provided:
- if force is not enabled:
- check if the bash completion file exists:
- if it exists, prompt the user that it will be overwritten
- if ok is given write the file and prompt that a backup has been created
- else create the file containing the completion
- else
- if force is not enabled:
- check if the default file exists:
- if it exists, prompt the user that it will be erased:
- if ok is given write the file and prompt that a backup has been created
- else
- create the file containing the completion
- create a .zprofile or append the appropriate commands into the .zprofile file
*/
protected async commandAction(options: { file: string, force: boolean }): Promise<void> {
const filePath = path.resolve(options.file ?? this.bashCompletion); // change that if file is empty
const showInstructions = !!options.file;
if ( !(await tryRenameFile(filePath, options.force)) ) { // means renaming was interrupted
return;
}
this.writeFile(filePath, showInstructions);
// Do not modify if custom file was provided
if ( !options.file ) {
this.addToZprofile(this.zprofile, filePath);
}
}
}
export default new CompletionZshCommand();
\ No newline at end of file
......@@ -3,6 +3,7 @@ import { existsSync, renameSync } from 'fs';
import ora from 'ora';
import TextStyle from '../types/TextStyle';
import inquirer from 'inquirer';
import fs from 'fs-extra';
function renameFile(filename: string, showWarning: boolean) {
......@@ -126,6 +127,19 @@ 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');
......@@ -178,6 +192,32 @@ export function generateFishCompletion(root: Command): string {
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
......
......@@ -5,18 +5,10 @@ import Config from '../config/Config';
class GlobalHelper {
public runCommandDefinition(command: Command) {
command
.option('-p, --path <value>', 'assignment 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 completionCommandDefinition(command: Command) {
command
.option('-f, --file <filename>')
.option('-y, --force', 'don\'t ask for file overwrite confirmation');
.option('-p, --path <value>', 'assignment 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;
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment