Add suggested CLI command names
MODIFY
packages/@romejs/cli-flags/Parser.test.ts
packages/@romejs/cli-flags/Parser.test.ts
*/
import {TestHelper, test} from "rome";
-import Parser from "./Parser";
+import Parser, {ParserOptions} from "./Parser";
import {Reporter} from "@romejs/cli-reporter";
import {Consumer} from "@romejs/consume";
import {catchDiagnostics} from "@romejs/diagnostics";
@@ -18,10 +18,12 @@ async function testParser<T>(
defineFlags,
args,
callback,
+ options: opts,
}: {
defineFlags: (consumer: Consumer) => T;
args: Array<string>;
callback?: (parser: Parser<T>, flags: T) => void;
+ options?: Partial<ParserOptions<T>>;
},
) {
const reporter = new Reporter();
@@ -30,6 +32,7 @@ async function testParser<T>(
const parser = new Parser(
reporter,
{
+ ...opts,
programName: "test",
defineFlags,
},
@@ -278,3 +281,69 @@ test(
);
},
);
+
+test(
+ "command required with missing command",
+ async (t) => {
+ await testParser(
+ t,
+ {
+ options: {
+ commandRequired: true,
+ },
+ defineFlags: () => {
+ return {};
+ },
+ args: [],
+ },
+ );
+ },
+);
+
+test(
+ "command required with wrong command",
+ async (t) => {
+ await testParser(
+ t,
+ {
+ options: {
+ commandRequired: true,
+ },
+ defineFlags: () => {
+ return {};
+ },
+ args: ["foo"],
+ callback(p) {
+ p.addCommand({
+ name: "foobar",
+ callback() {},
+ });
+ },
+ },
+ );
+ },
+);
+
+test(
+ "command required with wrong command and suggestion",
+ async (t) => {
+ await testParser(
+ t,
+ {
+ options: {
+ commandRequired: true,
+ commandSuggestions: {
+ foo: {
+ commandName: "foobar",
+ description: "A much cooler command",
+ },
+ },
+ },
+ defineFlags: () => {
+ return {};
+ },
+ args: ["foo"],
+ },
+ );
+ },
+);

MODIFY
packages/@romejs/cli-flags/Parser.ts
packages/@romejs/cli-flags/Parser.ts
@@ -69,6 +69,10 @@ export type ParserOptions<T> = {
version?: string;
ignoreFlags?: Array<string>;
commandRequired?: boolean;
+ commandSuggestions?: Dict<{
+ commandName: string;
+ description: string;
+ }>;
defineFlags: (consumer: Consumer) => T;
};
@@ -873,19 +877,54 @@ export default class Parser<T> {
return;
}
- const {programName} = this.opts;
- const {args} = this;
+ const {programName, commandSuggestions} = this.opts;
+ let {args} = this;
+ let commandName = args.join(" ");
+ let description = descriptions.FLAGS.COMMAND_REQUIRED(
+ programName,
+ commandName,
+ Array.from(this.commands.keys()),
+ );
- const diag: Diagnostic = {
- description: descriptions.FLAGS.COMMAND_REQUIRED(
+ // If we were provided with a list of command suggestions, try and find one
+ if (commandSuggestions !== undefined) {
+ for (let i = 0; i < args.length; i++) {
+ const possibleCommandName = args.slice(0, i + 1).join(" ");
+ const suggestion = commandSuggestions[possibleCommandName];
+ if (suggestion !== undefined) {
+ commandName = possibleCommandName;
+ args = args.slice(i + 1);
+ description = descriptions.FLAGS.UNKNOWN_COMMAND_SUGGESTED(
+ commandName,
+ suggestion.commandName,
+ suggestion.description,
+ serializeCLIFlags(
+ {
programName,
- args.length === 0,
- ),
+ commandName: suggestion.commandName,
+ args,
+ defaultFlags,
+ flags: rawFlags,
+ incorrectCaseFlags: this.incorrectCaseFlags,
+ shorthandFlags: this.shorthandFlags,
+ },
+ {
+ type: "none",
+ },
+ ).sourceText,
+ );
+ break;
+ }
+ }
+ }
+
+ const diag: Diagnostic = {
+ description,
location: serializeCLIFlags(
{
programName,
- commandName: this.args.join(" "),
- args: [],
+ commandName,
+ args,
defaultFlags,
flags: rawFlags,
incorrectCaseFlags: this.incorrectCaseFlags,

MODIFY
packages/@romejs/cli-flags/serializeCLIFlags.ts
packages/@romejs/cli-flags/serializeCLIFlags.ts
@@ -13,8 +13,8 @@ import {Dict, RequiredProps} from "@romejs/typescript-helpers";
import {FlagValue} from "./Parser";
type SerializeCLIData = {
- programName: undefined | string;
- commandName: undefined | string;
+ programName?: string;
+ commandName?: string;
args?: Array<string>;
defaultFlags?: Dict<FlagValue>;
flags?: Dict<FlagValue>;
@@ -188,20 +188,29 @@ export function serializeCLIFlags(
endColumn = startColumn;
}
- return {
- language: "shell",
- mtime: undefined,
- sourceText: code,
- filename: "argv",
- start: {
+ let start: DiagnosticLocation["start"] = {
line: ob1Number1,
column: startColumn,
index: startColumn,
- },
- end: {
+ };
+
+ let end: DiagnosticLocation["end"] = {
line: ob1Number1,
column: endColumn,
index: endColumn,
- },
+ };
+
+ if (target.type === "none") {
+ start = undefined;
+ end = undefined;
+ }
+
+ return {
+ language: "shell",
+ mtime: undefined,
+ sourceText: code,
+ filename: "argv",
+ start,
+ end,
};
}

MODIFY
packages/@romejs/cli/cli.ts
packages/@romejs/cli/cli.ts
@@ -53,6 +53,12 @@ export default async function cli() {
usage: "[command] [flags]",
version: VERSION,
commandRequired: true,
+ commandSuggestions: {
+ lint: {
+ commandName: "check",
+ description: "The <emphasis>check</emphasis> command covers linting, formatting, and more",
+ },
+ },
defineFlags(
c: Consumer,
): {

MODIFY
packages/@romejs/diagnostics/descriptions/flags.ts
packages/@romejs/diagnostics/descriptions/flags.ts
import {createDiagnosticsCategory} from "./index";
import {markup} from "@romejs/string-markup";
import {toKebabCase} from "@romejs/string-utils";
+import {buildSuggestionAdvice} from "../helpers";
export const flags = createDiagnosticsCategory({
UNSUPPORTED_SHORTHANDS: `Shorthand flags are not supported`,
@@ -36,10 +37,39 @@ export const flags = createDiagnosticsCategory({
NO_FILES_FOUND: (noun: undefined | string) => ({
message: noun === undefined ? "No files found" : `No files to ${noun} found`,
}),
- COMMAND_REQUIRED: (programName: string, missing: boolean) => ({
+ UNKNOWN_COMMAND_SUGGESTED: (
+ unknownCommandName: string,
+ commandName: string,
+ description: undefined | string,
+ command: string,
+ ) => ({
category: "flags/invalid",
- message: missing ? "No command specified" : "Unknown command",
+ message: markup`Unknown command <emphasis>${unknownCommandName}</emphasis>`,
advice: [
+ {
+ type: "log",
+ category: "info",
+ text: markup`Did you mean <emphasis>${commandName}</emphasis> instead?` +
+ (description === undefined ? "" : ` ${description}`),
+ },
+ {
+ type: "code",
+ language: "shell",
+ code: command,
+ },
+ ],
+ }),
+ COMMAND_REQUIRED: (
+ programName: string,
+ commandName: string,
+ knownCommands: Array<string>,
+ ) => ({
+ category: "flags/invalid",
+ message: commandName === ""
+ ? "No command specified"
+ : markup`Unknown command <emphasis>${commandName}</emphasis>`,
+ advice: [
+ ...buildSuggestionAdvice(commandName, knownCommands),
{
type: "log",
category: "info",

Add suggested CLI command names (#671)