From 00c956394d44cd0270eda93fba4498ea2e4f9881 Mon Sep 17 00:00:00 2001 From: Scott Lowe Date: Thu, 10 Sep 2020 14:44:32 +0200 Subject: [PATCH] Improve command suggestions, add topic help Change-type: minor Resolves: #2021 Signed-off-by: Scott Lowe --- lib/help.ts | 21 ++++++++++++++++ lib/hooks/command-not-found/suggest.ts | 34 +++++++++++++++++++++----- patches/all/@oclif+config+1.17.0.patch | 15 ++++++++++++ 3 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 patches/all/@oclif+config+1.17.0.patch diff --git a/lib/help.ts b/lib/help.ts index cad3bd63..b2f67200 100644 --- a/lib/help.ts +++ b/lib/help.ts @@ -40,6 +40,27 @@ export default class BalenaHelp extends Help { return; } + // If they've typed a topic (e.g. `balena os`) that isn't also a command (e.g. `balena device`) + // then list the associated commands. + const topicCommands = this.config.commands.filter((c) => { + return c.id.startsWith(`${subject}:`); + }); + if (topicCommands.length > 0) { + console.log(`${chalk.yellow(subject)} commands include:`); + console.log(this.formatCommands(topicCommands)); + console.log( + `\nRun ${chalk.cyan.bold( + 'balena help -v', + )} for a list of all available commands,`, + ); + console.log( + ` or ${chalk.cyan.bold( + 'balena help ', + )} for detailed help on a specific command.`, + ); + return; + } + throw new ExpectedError(`command ${chalk.cyan.bold(subject)} not found`); } diff --git a/lib/hooks/command-not-found/suggest.ts b/lib/hooks/command-not-found/suggest.ts index 292f6032..b1c3a735 100644 --- a/lib/hooks/command-not-found/suggest.ts +++ b/lib/hooks/command-not-found/suggest.ts @@ -22,10 +22,12 @@ import type { IConfig } from '@oclif/config'; A modified version of the command-not-found plugin logic, which deals with spaces separators stead of colons, and prints suggested commands instead of prompting interactively. + + Also see help.ts showHelp() for handling of topics. */ const hook: Hook<'command-not-found'> = async function ( - opts: object & { config: IConfig; id?: string }, + opts: object & { config: IConfig; id?: string; argv?: string[] }, ) { const Levenshtein = await import('fast-levenshtein'); const _ = await import('lodash'); @@ -44,16 +46,36 @@ const hook: Hook<'command-not-found'> = async function ( return _.minBy(commandIDs, (c) => Levenshtein.get(cmd, c))!; } + const suggestions: string[] = []; + suggestions.push(closest(commandId).replace(':', ' ') || ''); + + // opts.argv contains everything after the first command word + // if there's something there, also test if it might be a double + // word command spelt wrongly, rather than command args. + if (opts.argv?.[0]) { + suggestions.unshift( + closest(`${commandId}: + ${opts.argv[0]}`).replace(':', ' ') || '', + ); + } + + // Output suggestions console.error( `${color.yellow(command)} is not a recognized balena command.\n`, ); - - const suggestion = closest(commandId).replace(':', ' ') || ''; - console.log(`Did you mean: ${color.cmd(suggestion)} ? `); - console.log( - `Run ${color.cmd('balena help -v')} for a list of available commands.`, + console.error(`Did you mean: ? `); + suggestions.forEach((s) => { + console.error(` ${color.cmd(s)}`); + }); + console.error( + `\nRun ${color.cmd('balena help -v')} for a list of available commands,`, + ); + console.error( + ` or ${color.cmd( + 'balena help ', + )} for detailed help on a specific command.`, ); + // Exit const COMMAND_NOT_FOUND = 127; process.exit(COMMAND_NOT_FOUND); }; diff --git a/patches/all/@oclif+config+1.17.0.patch b/patches/all/@oclif+config+1.17.0.patch new file mode 100644 index 00000000..facffe54 --- /dev/null +++ b/patches/all/@oclif+config+1.17.0.patch @@ -0,0 +1,15 @@ +diff --git a/node_modules/@oclif/config/lib/config.js b/node_modules/@oclif/config/lib/config.js +index aba1da9..830f800 100644 +--- a/node_modules/@oclif/config/lib/config.js ++++ b/node_modules/@oclif/config/lib/config.js +@@ -165,7 +165,9 @@ class Config { + debug('runCommand %s %o', id, argv); + const c = this.findCommand(id); + if (!c) { +- await this.runHook('command_not_found', { id }); ++ // argv added to command_not_found hook ++ // We should try to upstream this change ++ await this.runHook('command_not_found', { id, argv }); + throw new errors_1.CLIError(`command ${id} not found`); + } + const command = c.load();