Display command suggestions, when command not recognized

Change-type: minor
Signed-off-by: Scott Lowe <scott@balena.io>
This commit is contained in:
Scott Lowe 2020-09-07 14:35:35 +02:00
parent d56fec6e36
commit 273ea5ce4d
3 changed files with 96 additions and 4 deletions

View File

@ -0,0 +1,61 @@
/**
* @license
* Copyright 2019-2020 Balena Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Hook } from '@oclif/config';
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.
*/
const hook: Hook<'command-not-found'> = async function (
opts: object & { config: IConfig; id?: string },
) {
const Levenshtein = await import('fast-levenshtein');
const _ = await import('lodash');
const { color } = await import('@oclif/color');
const commandId = opts.id || '';
const command = opts.id?.replace(':', ' ') || '';
const commandIDs = [
...opts.config.commandIDs,
..._.flatten(opts.config.commands.map((c) => c.aliases)),
'version',
];
function closest(cmd: string) {
return _.minBy(commandIDs, (c) => Levenshtein.get(cmd, c))!;
}
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.`,
);
const COMMAND_NOT_FOUND = 127;
process.exit(COMMAND_NOT_FOUND);
};
export default hook;

34
npm-shrinkwrap.json generated
View File

@ -957,6 +957,13 @@
"cli-ux": "^4.9.0",
"fast-levenshtein": "^2.0.6",
"lodash": "^4.17.13"
},
"dependencies": {
"fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
}
}
},
"@oclif/plugin-warn-if-update-available": {
@ -1564,6 +1571,11 @@
"@types/range-parser": "*"
}
},
"@types/fast-levenshtein": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@types/fast-levenshtein/-/fast-levenshtein-0.0.1.tgz",
"integrity": "sha1-OjYVzxc2Rcj8pY0FHk4ygk5L0oY="
},
"@types/fs-extra": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.1.tgz",
@ -6380,9 +6392,17 @@
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
},
"fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz",
"integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==",
"requires": {
"fastest-levenshtein": "^1.0.7"
}
},
"fastest-levenshtein": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz",
"integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow=="
},
"fastq": {
"version": "1.8.0",
@ -11751,6 +11771,14 @@
"prelude-ls": "~1.1.2",
"type-check": "~0.3.2",
"word-wrap": "~1.2.3"
},
"dependencies": {
"fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
"dev": true
}
}
},
"ordered-read-streams": {

View File

@ -103,7 +103,8 @@
"commands": "./build/actions-oclif",
"helpClass": "./build/help",
"hooks": {
"prerun": "./build/hooks/prerun/track"
"prerun": "./build/hooks/prerun/track",
"command_not_found": "./build/hooks/command-not-found/suggest"
},
"macos": {
"identifier": "io.balena.cli",
@ -197,6 +198,7 @@
"@oclif/command": "^1.8.0",
"@resin.io/valid-email": "^0.1.0",
"@sentry/node": "^5.21.1",
"@types/fast-levenshtein": "0.0.1",
"@types/update-notifier": "^4.1.1",
"@zeit/dockerignore": "0.0.3",
"JSONStream": "^1.0.3",
@ -230,6 +232,7 @@
"event-stream": "3.3.4",
"express": "^4.13.3",
"fast-boot2": "^1.1.0",
"fast-levenshtein": "^3.0.0",
"get-stdin": "^8.0.0",
"global-agent": "^2.1.12",
"global-tunnel-ng": "^2.1.1",