diff --git a/bin/run b/bin/run index fcaebe51129..330b6c39987 100755 --- a/bin/run +++ b/bin/run @@ -6,7 +6,6 @@ oclif.run() .then(require('@oclif/core/flush')) .catch((err) => { const oclifHandler = require('@oclif/core/handle'); - console.error(err.message); return oclifHandler(err.message); }); diff --git a/package-lock.json b/package-lock.json index fefcce035b8..de198f48b7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "ajv": "^8.12.0", "chalk": "^4.1.0", "chokidar": "^3.5.2", + "fast-levenshtein": "^3.0.0", "fs-extra": "^11.1.0", "indent-string": "^4.0.0", "inquirer": "^8.2.0", @@ -54,6 +55,7 @@ "@oclif/test": "^2", "@swc/core": "^1.3.2", "@types/chai": "^4.3.6", + "@types/fast-levenshtein": "^0.0.2", "@types/fs-extra": "^11.0.1", "@types/inquirer": "^8.1.3", "@types/js-yaml": "^4.0.5", @@ -6289,6 +6291,12 @@ "version": "1.20.4", "license": "MIT" }, + "node_modules/@types/fast-levenshtein": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@types/fast-levenshtein/-/fast-levenshtein-0.0.2.tgz", + "integrity": "sha512-h9AGeNlFimLtFUlEZgk+hb3LUT4tNHu8y0jzCUeTdi1BM4e86sBQs/nQYgHk70ksNyNbuLwpymFAXkb0GAehmw==", + "dev": true + }, "node_modules/@types/fs-extra": { "version": "11.0.1", "dev": true, @@ -10883,7 +10891,8 @@ }, "node_modules/fast-levenshtein": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", "dependencies": { "fastest-levenshtein": "^1.0.7" } diff --git a/package.json b/package.json index 92fba468dba..261188d0abe 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "ajv": "^8.12.0", "chalk": "^4.1.0", "chokidar": "^3.5.2", + "fast-levenshtein": "^3.0.0", "fs-extra": "^11.1.0", "indent-string": "^4.0.0", "inquirer": "^8.2.0", @@ -50,6 +51,7 @@ "@oclif/test": "^2", "@swc/core": "^1.3.2", "@types/chai": "^4.3.6", + "@types/fast-levenshtein": "^0.0.2", "@types/fs-extra": "^11.0.1", "@types/inquirer": "^8.1.3", "@types/js-yaml": "^4.0.5", @@ -103,9 +105,12 @@ "oclif": { "commands": "./lib/commands", "bin": "asyncapi", - "plugins": [ - "@oclif/plugin-not-found" - ], + "plugins": [], + "hooks": { + "command_not_found": [ + "./lib/hooks/command_not_found/myhook" + ] + }, "macos": { "identifier": "com.asyncapi.cli" }, @@ -151,7 +156,9 @@ "release": "semantic-release", "test": "npm run test:unit", "test:unit": "cross-env NODE_ENV=development TEST=1 CUSTOM_CONTEXT_FILENAME=\"test.asyncapi-cli\" CUSTOM_CONTEXT_FILE_LOCATION=\"\" nyc --extension .ts mocha --require ts-node/register --require test/helpers/init.js --reporter spec --timeout 100000 \"test/**/*.test.ts\"", - "get-version": "echo $npm_package_version" + "get-version": "echo $npm_package_version", + "createhook": "oclif generate hook myhook --event=command_not_found", + "createhookinit": "oclif generate hook inithook --event=init" }, "types": "lib/index.d.ts" } diff --git a/src/hooks/command_not_found/myhook.ts b/src/hooks/command_not_found/myhook.ts new file mode 100644 index 00000000000..ef2698c94b9 --- /dev/null +++ b/src/hooks/command_not_found/myhook.ts @@ -0,0 +1,75 @@ +import {Hook, toConfiguredId, CliUx} from '@oclif/core'; +import chalk from 'chalk'; +import {default as levenshtein} from 'fast-levenshtein'; +import { Help } from '@oclif/core'; + +export const closest = (target: string, possibilities: string[]): string => + possibilities + .map((id) => ({distance: levenshtein.get(target, id, {useCollator: true}), id})) + .sort((a, b) => a.distance - b.distance)[0]?.id ?? ''; + +const hook: Hook.CommandNotFound = async function (opts) { + if (opts.id === '--help') { + const help = new Help(this.config); + help.showHelp(['--help']); + return; + } + const hiddenCommandIds = new Set(opts.config.commands.filter((c) => c.hidden).map((c) => c.id)); + const commandIDs = [...opts.config.commandIDs, ...opts.config.commands.flatMap((c) => c.aliases)].filter( + (c) => !hiddenCommandIds.has(c), + ); + + if (commandIDs.length === 0) {return;} + + // now we we return if the command id are not there. + + let binHelp = `${opts.config.bin} help`; + + const idSplit = opts.id.split(':'); + if (opts.config.findTopic(idSplit[0])) { + // if valid topic, update binHelp with topic + binHelp = `${binHelp} ${idSplit[0]}`; + } + + //if there is a topic in the opts we just upgrade the our commnad like + + // alter the suggestion in the help scenario so that help is the first command + // otherwise the user will be presented 'did you mean 'help'?' instead of 'did you mean "help "?' + let suggestion = (/:?help:?/).test(opts.id) + ? ['help', ...opts.id.split(':').filter((cmd) => cmd !== 'help')].join(':') + : closest(opts.id, commandIDs); + + let readableSuggestion = toConfiguredId(suggestion, this.config); + const originalCmd = toConfiguredId(opts.id, this.config); + this.warn(`${chalk.yellow(originalCmd)} is not a ${opts.config.bin} command.`); + + let response = ''; + try { + if (opts.id === 'help') {readableSuggestion = '--help';} + response = await CliUx.ux.prompt(`Did you mean ${chalk.blueBright(readableSuggestion)}? [y/n]`, {timeout: 10_000}); + } catch (error) { + this.log(''); + this.debug(error); + } + + if (response === 'y') { + // this will split the original command from the suggested replacement, and gather the remaining args as varargs to help with situations like: + // confit set foo-bar -> confit:set:foo-bar -> config:set:foo-bar -> config:set foo-bar + let argv = opts.argv?.length ? opts.argv : opts.id.split(':').slice(suggestion.split(':').length); + if (suggestion.startsWith('help:')) { + // the args are the command/partial command you need help for (package:version) + // we created the suggestion variable to start with "help" so slice the first entry + argv = suggestion.split(':').slice(1); + // the command is just the word "help" + suggestion = 'help'; + } + if (opts.id === 'help') { + return this.config.runCommand('--help'); + } + return this.config.runCommand(suggestion, argv); + } + + this.error(`Run ${chalk.bold.cyan(binHelp)} for a list of available commands.`, {exit: 127}); +}; + +export default hook; diff --git a/test/hooks/command_not_found/myhook.spec.ts b/test/hooks/command_not_found/myhook.spec.ts new file mode 100644 index 00000000000..bab80936b07 --- /dev/null +++ b/test/hooks/command_not_found/myhook.spec.ts @@ -0,0 +1,10 @@ +import {expect, test} from '@oclif/test'; + +describe('hooks', () => { + test + .stdout() + .hook('command_not_found', {id: 'help'}) + .do(output => expect(output.stdout).to.contain('help command not found.')) + .it('shows a message'); +}); + diff --git a/test/tsconfig.json b/test/tsconfig.json index 8c82b0a0459..fa355fb9797 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -3,9 +3,5 @@ "compilerOptions": { "noEmit": true, }, - "references": [ - { - "path": "../" - } - ] + }