diff --git a/NodeApp/src/commander/completion/CompletionCommand.ts b/NodeApp/src/commander/completion/CompletionCommand.ts index 1ee0668b994796b04d303357de071c7a3b4f75ff..f0b71909c73ce320e1e71d564582d76e1d7903b9 100644 --- a/NodeApp/src/commander/completion/CompletionCommand.ts +++ b/NodeApp/src/commander/completion/CompletionCommand.ts @@ -1,7 +1,9 @@ -import CommanderCommand from '../CommanderCommand'; -import CompletionBashCommand from './subcommands/CompletionBashCommand'; -import CompletionFishCommand from './subcommands/CompletionFishCommand'; -import CompletionZshCommand from './subcommands/CompletionZshCommand'; +import CommanderCommand from '../CommanderCommand'; +import CompletionBashCommand from './subcommands/CompletionBashCommand'; +import CompletionFishCommand from './subcommands/CompletionFishCommand'; +import CompletionZshCommand from './subcommands/CompletionZshCommand'; +import CompletionGetCommand from './subcommands/CompletionGetCommand'; +import CompletionScriptCommand from './subcommands/CompletionScriptCommand'; class CompletionCommand extends CommanderCommand { @@ -16,6 +18,9 @@ class CompletionCommand extends CommanderCommand { CompletionBashCommand.registerOnCommand(this.command); CompletionFishCommand.registerOnCommand(this.command); CompletionZshCommand.registerOnCommand(this.command); + + CompletionGetCommand.registerOnCommand(this.command); + CompletionScriptCommand.registerOnCommand(this.command); } protected async commandAction(): Promise<void> { diff --git a/NodeApp/src/commander/completion/subcommands/CompletionBashCommand.ts b/NodeApp/src/commander/completion/subcommands/CompletionBashCommand.ts index d135440dccf7310afbb1304c15bbbd507f3a6eaa..54b0a1e97ab88f4abec2ed74150f54ec36faedb5 100644 --- a/NodeApp/src/commander/completion/subcommands/CompletionBashCommand.ts +++ b/NodeApp/src/commander/completion/subcommands/CompletionBashCommand.ts @@ -1,75 +1,26 @@ -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'; +import CommanderCommand from '../../CommanderCommand'; +import path from 'path'; +import os from 'os'; +import { updateRcFile } from '../../../helpers/AutoCompletionHelper'; class CompletionBashCommand extends CommanderCommand { protected commandName: string = 'bash'; - private readonly installPath = path.join(os.homedir(), '.bash_completion'); + private readonly bashrcPath = path.join(os.homedir(), '.bashrc'); + private readonly completionCommand = ` +# Added by DojoCLI +source <(dojo completion script bash) +`; protected defineCommand() { - GlobalHelper.completionCommandDefinition(this.command) - .description('generate bash completion') + 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 }`); - } + protected async commandAction(): Promise<void> { + updateRcFile('bash', this.bashrcPath, this.completionCommand); } - - /* 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); - } - } diff --git a/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts b/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts index f9d279eb2e86b29f567db78797f98333b70c4ec2..94fe5b44e46293594529c73c38a1459d7ac4d544 100644 --- a/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts +++ b/NodeApp/src/commander/completion/subcommands/CompletionZshCommand.ts @@ -1,123 +1,26 @@ -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'; +import CommanderCommand from '../../CommanderCommand'; +import path from 'path'; +import { homedir } from 'os'; +import { updateRcFile } from '../../../helpers/AutoCompletionHelper'; 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 = ` + private readonly zshrcPath: string = path.join(homedir(), '.zshrc'); + private readonly completionCommand = ` # Added by DojoCLI -autoload -U +X compinit && compinit -autoload -U +X bashcompinit && bashcompinit -source ${ this.bashCompletion } +source <(dojo completion script zsh) `; protected defineCommand() { - GlobalHelper.completionCommandDefinition(this.command) - .description('generate zsh completion, which is derived from the bash completion') + this.command.description('generate zsh 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); - } + protected async commandAction(): Promise<void> { + updateRcFile('zsh', this.zshrcPath, this.completionCommand); } } diff --git a/NodeApp/src/helpers/AutoCompletionHelper.ts b/NodeApp/src/helpers/AutoCompletionHelper.ts index 56988ebdceb9f25b1ba53fff4a6021059ca138c9..2e14837560ce574be8f039560f92ffa6695dfb3e 100644 --- a/NodeApp/src/helpers/AutoCompletionHelper.ts +++ b/NodeApp/src/helpers/AutoCompletionHelper.ts @@ -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.` : `${ shellType } already up to date.`); + } else { + try { + fs.writeFileSync(filePath, completionCommand); + spinner.succeed(`${ shellType } written.`); + } 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