mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(cli): scope unknown-subcommand help to its command tree (#124544)
This commit is contained in:
committed by
GitHub
parent
bba57301d9
commit
23a034d584
@@ -16,25 +16,31 @@ function uniqueSortedCommandNames(commands: Iterable<string>): string[] {
|
||||
);
|
||||
}
|
||||
|
||||
export function formatCliCommandSuggestions(input: string): string | undefined {
|
||||
export function formatCliCommandSuggestions(
|
||||
input: string,
|
||||
commandPath: readonly string[] = [],
|
||||
candidates?: Iterable<string>,
|
||||
): string | undefined {
|
||||
const normalizedInput = input.trim().toLowerCase();
|
||||
if (!normalizedInput) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const knownCommands = uniqueSortedCommandNames([
|
||||
...getCoreCliCommandNamesCore(),
|
||||
...getSubCliEntriesCore().map((entry) => entry.name),
|
||||
]);
|
||||
const knownCommands = uniqueSortedCommandNames(
|
||||
candidates ??
|
||||
(commandPath.length === 0
|
||||
? [...getCoreCliCommandNamesCore(), ...getSubCliEntriesCore().map((entry) => entry.name)]
|
||||
: []),
|
||||
);
|
||||
const explicitAlias = EXPLICIT_COMMAND_ALIASES.get(normalizedInput);
|
||||
if (explicitAlias && knownCommands.includes(explicitAlias)) {
|
||||
return formatCliSuggestionLines([explicitAlias]);
|
||||
return formatCliSuggestionLines([explicitAlias], commandPath);
|
||||
}
|
||||
const suggestions = findCliCommandSuggestions(normalizedInput, knownCommands);
|
||||
if (suggestions.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return formatCliSuggestionLines(suggestions);
|
||||
return formatCliSuggestionLines(suggestions, commandPath);
|
||||
}
|
||||
|
||||
function findCliCommandSuggestions(input: string, candidates: readonly string[]): string[] {
|
||||
@@ -49,9 +55,13 @@ function findCliCommandSuggestions(input: string, candidates: readonly string[])
|
||||
.map(({ command }) => command);
|
||||
}
|
||||
|
||||
function formatCliSuggestionLines(suggestions: readonly string[]): string {
|
||||
function formatCliSuggestionLines(
|
||||
suggestions: readonly string[],
|
||||
commandPath: readonly string[],
|
||||
): string {
|
||||
const commandPrefix = ["openclaw", ...commandPath].join(" ");
|
||||
const commandLines = suggestions
|
||||
.map((command) => ` ${formatCliCommand(`openclaw ${command}`)}`)
|
||||
.map((command) => ` ${formatCliCommand(`${commandPrefix} ${command}`)}`)
|
||||
.join("\n");
|
||||
return `Did you mean this?\n${commandLines}`;
|
||||
}
|
||||
|
||||
@@ -103,3 +103,14 @@ export function getCommanderErrorCommandPath(program: Command): string[] | undef
|
||||
const command = activeErrorCommandByRoot.get(getRootCommand(program));
|
||||
return command ? getCommanderCommandPath(command) : undefined;
|
||||
}
|
||||
|
||||
/** Return visible children of the non-root command synchronously emitting a parse error. */
|
||||
export function getCommanderErrorCommandNames(program: Command): string[] | undefined {
|
||||
const command = activeErrorCommandByRoot.get(getRootCommand(program));
|
||||
return command && getCommanderCommandPath(command).length
|
||||
? command
|
||||
.createHelp()
|
||||
.visibleCommands(command)
|
||||
.map((child) => child.name())
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,29 @@ describe("formatCliParseErrorOutput", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("explains unknown subcommands within the active command tree", () => {
|
||||
const output = formatCliParseErrorOutput("error: unknown command 'list'\n", {
|
||||
argv: ["node", "openclaw", "webhooks", "list"],
|
||||
commandPath: ["webhooks"],
|
||||
});
|
||||
|
||||
expect(output).toBe(
|
||||
'OpenClaw webhooks has no command "list".\nTry: openclaw webhooks --help\nDocs: https://docs.openclaw.ai/cli\n',
|
||||
);
|
||||
});
|
||||
|
||||
it("suggests sibling subcommands within the active command tree", () => {
|
||||
const output = formatCliParseErrorOutput("error: unknown command 'gmial'\n", {
|
||||
argv: ["node", "openclaw", "webhooks", "gmial"],
|
||||
commandPath: ["webhooks"],
|
||||
commandNames: ["gmail"],
|
||||
});
|
||||
|
||||
expect(output).toBe(
|
||||
'OpenClaw webhooks has no command "gmial".\nDid you mean this?\n openclaw webhooks gmail\nTry: openclaw webhooks --help\nDocs: https://docs.openclaw.ai/cli\n',
|
||||
);
|
||||
});
|
||||
|
||||
it("suggests close known commands for unknown commands", () => {
|
||||
const output = formatCliParseErrorOutput("error: unknown command 'upate'\n", {
|
||||
argv: ["node", "openclaw", "upate"],
|
||||
|
||||
@@ -8,6 +8,7 @@ import { formatCliCommandSuggestions } from "./command-suggestions.js";
|
||||
type FormatCliParseErrorOptions = {
|
||||
argv?: string[];
|
||||
commandPath?: string[];
|
||||
commandNames?: readonly string[];
|
||||
};
|
||||
|
||||
function stripCommanderErrorPrefix(raw: string): string {
|
||||
@@ -23,11 +24,8 @@ function quote(value: string): string {
|
||||
|
||||
function resolveHelpCommand(
|
||||
argv: string[] | undefined,
|
||||
options?: { commandPath?: string[]; root?: boolean },
|
||||
options?: { commandPath?: string[] },
|
||||
): string {
|
||||
if (options?.root) {
|
||||
return formatCliCommand("openclaw --help");
|
||||
}
|
||||
const commandPath = options?.commandPath ?? (argv ? getCommandPathWithRootOptions(argv, 2) : []);
|
||||
if (commandPath.length === 0) {
|
||||
return formatCliCommand("openclaw --help");
|
||||
@@ -39,10 +37,7 @@ function lines(...items: Array<string | undefined>): string {
|
||||
return `${items.filter((item): item is string => Boolean(item)).join("\n")}\n`;
|
||||
}
|
||||
|
||||
function formatHelpHint(
|
||||
argv: string[] | undefined,
|
||||
options?: { commandPath?: string[]; root?: boolean },
|
||||
): string {
|
||||
function formatHelpHint(argv: string[] | undefined, options?: { commandPath?: string[] }): string {
|
||||
return `${theme.muted("Try:")} ${theme.command(resolveHelpCommand(argv, options))}`;
|
||||
}
|
||||
|
||||
@@ -59,11 +54,19 @@ export function formatCliParseErrorOutput(
|
||||
const unknownCommand = message.match(/^unknown command ['"`](.+?)['"`]/i);
|
||||
if (unknownCommand) {
|
||||
const command = unknownCommand[1] ?? "";
|
||||
const commandPath = options.commandPath ?? [];
|
||||
const hasParentCommand = commandPath.length > 0;
|
||||
return lines(
|
||||
theme.error(`OpenClaw does not know the command ${quote(command)}.`),
|
||||
formatCliCommandSuggestions(command),
|
||||
formatHelpHint(options.argv, { root: true }),
|
||||
`${theme.muted("Plugin command?")} ${theme.command(formatCliCommand("openclaw plugins list"))}`,
|
||||
theme.error(
|
||||
hasParentCommand
|
||||
? `OpenClaw ${commandPath.join(" ")} has no command ${quote(command)}.`
|
||||
: `OpenClaw does not know the command ${quote(command)}.`,
|
||||
),
|
||||
formatCliCommandSuggestions(command, commandPath, options.commandNames),
|
||||
formatHelpHint(options.argv, { commandPath }),
|
||||
hasParentCommand
|
||||
? undefined
|
||||
: `${theme.muted("Plugin command?")} ${theme.command(formatCliCommand("openclaw plugins list"))}`,
|
||||
formatDocsHint(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,9 +167,13 @@ describe("configureProgramHelp", () => {
|
||||
process.argv = ["node", "openclaw", "plugins", "list", "--still-wat"];
|
||||
const secondError = await program.parseAsync(process.argv).catch((error: unknown) => error);
|
||||
expect(secondError).toBeInstanceOf(CommanderError);
|
||||
process.argv = ["node", "openclaw", "plugins", "lis"];
|
||||
const thirdError = await program.parseAsync(process.argv).catch((error: unknown) => error);
|
||||
expect(thirdError).toBeInstanceOf(CommanderError);
|
||||
|
||||
expect(stderr.match(/Try: openclaw plugins list --help/g)).toHaveLength(2);
|
||||
expect(stderr).not.toContain("openclaw plugins list list --help");
|
||||
expect(stderr).toContain("Did you mean this?\n openclaw plugins list\n");
|
||||
});
|
||||
|
||||
it("suppresses banner formatting when parent default help requests it", () => {
|
||||
|
||||
@@ -9,7 +9,10 @@ import { isRootVersionInvocation } from "../argv.js";
|
||||
import { formatCliBannerLine, hasEmittedCliBanner } from "../banner.js";
|
||||
import { replaceCliName, resolveCliName } from "../cli-name.js";
|
||||
import { CLI_LOG_LEVEL_VALUES, parseCliLogLevelOption } from "../log-level-option.js";
|
||||
import { getCommanderErrorCommandPath } from "./commander-parse-facts.js";
|
||||
import {
|
||||
getCommanderErrorCommandNames,
|
||||
getCommanderErrorCommandPath,
|
||||
} from "./commander-parse-facts.js";
|
||||
import type { ProgramContext } from "./context.js";
|
||||
import { getCoreCliCommandsWithSubcommands } from "./core-command-descriptors.js";
|
||||
import { formatCliParseErrorOutput } from "./error-output.js";
|
||||
@@ -119,13 +122,15 @@ export function configureProgramHelp(
|
||||
const message = formatProgramHelpOutput(str);
|
||||
process.stderr.write(formatConsoleDiagnosticBlock({ level: "error", message }));
|
||||
},
|
||||
outputError: (str, write) =>
|
||||
outputError: (str, write) => {
|
||||
write(
|
||||
formatCliParseErrorOutput(str, {
|
||||
argv: process.argv,
|
||||
commandPath: getCommanderErrorCommandPath(program),
|
||||
commandNames: getCommanderErrorCommandNames(program),
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
if (isRootVersionInvocation(process.argv)) {
|
||||
|
||||
Reference in New Issue
Block a user