From f06b77c0d4c1902e097646b1cb68786ae3afee14 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 9 May 2026 07:45:45 +0800 Subject: [PATCH] fix(cli): explain parser errors --- src/cli/program/error-output.test.ts | 32 ++++++++++ src/cli/program/error-output.ts | 95 ++++++++++++++++++++++++++++ src/cli/program/help.ts | 3 +- 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 src/cli/program/error-output.test.ts create mode 100644 src/cli/program/error-output.ts diff --git a/src/cli/program/error-output.test.ts b/src/cli/program/error-output.test.ts new file mode 100644 index 00000000000..6cb1ac179a3 --- /dev/null +++ b/src/cli/program/error-output.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; +import { formatCliParseErrorOutput } from "./error-output.js"; + +describe("formatCliParseErrorOutput", () => { + it("explains unknown commands with root help and plugin hints", () => { + const output = formatCliParseErrorOutput("error: unknown command 'wat'\n", { + argv: ["node", "openclaw", "wat"], + }); + + expect(output).toContain('OpenClaw does not know the command "wat".'); + expect(output).toContain("openclaw --help"); + expect(output).toContain("openclaw plugins list"); + }); + + it("points unknown options at the active command help", () => { + const output = formatCliParseErrorOutput("error: unknown option '--wat'\n", { + argv: ["node", "openclaw", "channels", "status", "--wat"], + }); + + expect(output).toContain('OpenClaw does not recognize option "--wat".'); + expect(output).toContain("openclaw channels status --help"); + }); + + it("points missing required arguments at command help", () => { + const output = formatCliParseErrorOutput("error: missing required argument 'name'\n", { + argv: ["node", "openclaw", "plugins", "install"], + }); + + expect(output).toContain('Missing required argument "name".'); + expect(output).toContain("openclaw plugins install --help"); + }); +}); diff --git a/src/cli/program/error-output.ts b/src/cli/program/error-output.ts new file mode 100644 index 00000000000..f4e96ab564b --- /dev/null +++ b/src/cli/program/error-output.ts @@ -0,0 +1,95 @@ +import { formatDocsLink } from "../../terminal/links.js"; +import { theme } from "../../terminal/theme.js"; +import { getCommandPathWithRootOptions } from "../argv.js"; +import { formatCliCommand } from "../command-format.js"; + +type FormatCliParseErrorOptions = { + argv?: string[]; +}; + +function stripCommanderErrorPrefix(raw: string): string { + return raw + .trim() + .replace(/^error:\s*/i, "") + .trim(); +} + +function quote(value: string): string { + return `"${value}"`; +} + +function resolveHelpCommand(argv: string[] | undefined, options?: { root?: boolean }): string { + if (options?.root || !argv) { + return formatCliCommand("openclaw --help"); + } + const commandPath = getCommandPathWithRootOptions(argv, 2); + if (commandPath.length === 0) { + return formatCliCommand("openclaw --help"); + } + return formatCliCommand(`openclaw ${commandPath.join(" ")} --help`); +} + +function lines(...items: Array): string { + return `${items.filter((item): item is string => Boolean(item)).join("\n")}\n`; +} + +function formatHelpHint(argv: string[] | undefined, options?: { root?: boolean }): string { + return `${theme.muted("Try:")} ${theme.command(resolveHelpCommand(argv, options))}`; +} + +function formatDocsHint(): string { + return `${theme.muted("Docs:")} ${formatDocsLink("/cli", "docs.openclaw.ai/cli")}`; +} + +export function formatCliParseErrorOutput( + raw: string, + options: FormatCliParseErrorOptions = {}, +): string { + const message = stripCommanderErrorPrefix(raw); + const unknownCommand = message.match(/^unknown command ['"`](.+?)['"`]/i); + if (unknownCommand) { + const command = unknownCommand[1] ?? ""; + return lines( + theme.error(`OpenClaw does not know the command ${quote(command)}.`), + formatHelpHint(options.argv, { root: true }), + `${theme.muted("Plugin command?")} ${theme.command(formatCliCommand("openclaw plugins list"))}`, + formatDocsHint(), + ); + } + + const unknownOption = message.match(/^unknown option ['"`](.+?)['"`]/i); + if (unknownOption) { + const option = unknownOption[1] ?? ""; + return lines( + theme.error(`OpenClaw does not recognize option ${quote(option)}.`), + formatHelpHint(options.argv), + ); + } + + const missingArgument = message.match(/^missing required argument ['"`](.+?)['"`]/i); + if (missingArgument) { + const argument = missingArgument[1] ?? ""; + return lines( + theme.error(`Missing required argument ${quote(argument)}.`), + formatHelpHint(options.argv), + ); + } + + const missingOption = message.match(/^required option ['"`](.+?)['"`] not specified/i); + if (missingOption) { + const option = missingOption[1] ?? ""; + return lines( + theme.error(`Missing required option ${quote(option)}.`), + formatHelpHint(options.argv), + ); + } + + if (/^too many arguments\b/i.test(message)) { + return lines(theme.error("Too many arguments for this command."), formatHelpHint(options.argv)); + } + + return lines( + theme.error(`OpenClaw could not parse this command: ${message}`), + formatHelpHint(options.argv), + ); +} diff --git a/src/cli/program/help.ts b/src/cli/program/help.ts index 0d40458f335..0f428ca77c9 100644 --- a/src/cli/program/help.ts +++ b/src/cli/program/help.ts @@ -9,6 +9,7 @@ import { replaceCliName, resolveCliName } from "../cli-name.js"; import { CLI_LOG_LEVEL_VALUES, parseCliLogLevelOption } from "../log-level-option.js"; import type { ProgramContext } from "./context.js"; import { getCoreCliCommandsWithSubcommands } from "./core-command-descriptors.js"; +import { formatCliParseErrorOutput } from "./error-output.js"; import { getSubCliCommandsWithSubcommands } from "./subcli-descriptors.js"; const CLI_NAME = resolveCliName(); @@ -106,7 +107,7 @@ export function configureProgramHelp(program: Command, ctx: ProgramContext) { writeErr: (str) => { process.stderr.write(formatHelpOutput(str)); }, - outputError: (str, write) => write(theme.error(str)), + outputError: (str, write) => write(formatCliParseErrorOutput(str, { argv: process.argv })), }); if (