From bec7d56b7368f683ff69441d77e7df4419691065 Mon Sep 17 00:00:00 2001 From: YBoy <231405196+YB0y@users.noreply.github.com> Date: Tue, 26 May 2026 05:38:43 +0700 Subject: [PATCH] fix(cli): reject unknown command help roots (#81083) (thanks @YB0y) Behavior addressed: Unknown CLI command roots now error consistently even when --help or --version is appended, while legitimate built-in help fast paths still render normally. Real environment tested: Local OpenClaw source checkout plus GitHub workflow run-level status. Exact steps or command run after this patch: pnpm test src/cli/run-main.exit.test.ts src/cli/argv.test.ts src/cli/argv-invocation.test.ts; pnpm exec oxfmt --check --threads=1 src/cli/run-main.ts src/cli/run-main.exit.test.ts; autoreview --mode branch --base origin/main --no-web-search. Evidence after fix: Focused CLI test shards passed 178 tests; formatter clean; autoreview reported no accepted/actionable findings; GitHub CI run 26422344121 and CodeQL Critical Quality run 26422344090 completed successfully. Observed result after fix: `openclaw foo --help` and `openclaw foo --version` reject before proxy/program startup, while known help fast paths remain ahead of the unknown-root guard. What was not tested: Full local build; contributor PR body already supplied build/CLI command proof before rebase. Co-authored-by: YB0y --- src/cli/run-main.exit.test.ts | 20 ++++++++++++++++++++ src/cli/run-main.ts | 14 +++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/cli/run-main.exit.test.ts b/src/cli/run-main.exit.test.ts index fec33456a47..4c2a4928cd1 100644 --- a/src/cli/run-main.exit.test.ts +++ b/src/cli/run-main.exit.test.ts @@ -672,6 +672,26 @@ describe("runCli exit behavior", () => { expect(registerPluginCliCommandsFromValidatedConfigMock).not.toHaveBeenCalled(); }); + it("rejects unowned command roots even when --help is appended (regression for #81077)", async () => { + await expect(runCli(["node", "openclaw", "foo", "--help"])).rejects.toThrow( + 'No built-in command or plugin CLI metadata owns "foo"', + ); + + expect(startProxyMock).not.toHaveBeenCalled(); + expect(tryRouteCliMock).not.toHaveBeenCalled(); + expect(buildProgramMock).not.toHaveBeenCalled(); + expect(registerPluginCliCommandsFromValidatedConfigMock).not.toHaveBeenCalled(); + }); + + it("rejects unowned command roots even when --version is appended", async () => { + await expect(runCli(["node", "openclaw", "foo", "--version"])).rejects.toThrow( + 'No built-in command or plugin CLI metadata owns "foo"', + ); + + expect(startProxyMock).not.toHaveBeenCalled(); + expect(tryRouteCliMock).not.toHaveBeenCalled(); + }); + it("does not suggest plugins.allow for unknown command roots before proxy startup", async () => { loadConfigMock.mockReturnValueOnce({ plugins: { diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index 8e1c9e8c240..5840f7f9fd4 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -397,7 +397,6 @@ async function resolveUnownedCliPrimary(params: { const invocation = resolveCliArgvInvocation(rewriteUpdateFlagArgv(params.argv)); const { primary } = invocation; if ( - invocation.hasHelpOrVersion || !primary || primary === "help" || isReservedNonPluginCommandRoot(primary) || @@ -616,6 +615,19 @@ export async function runCli(argv: string[] = process.argv) { } } + // Reject unowned command roots before help/version routing, so that + // `openclaw --help` surfaces the same Unknown command error as + // `openclaw ` instead of silently showing generic top-level help. + // Runs after legitimate precomputed help fast paths so known help commands + // still dispatch normally. See #81077. + { + const config = await readBestEffortCliConfig(); + const unownedPrimary = await resolveUnownedCliPrimary({ argv: normalizedArgv, config }); + if (unownedPrimary) { + throw new Error(await resolveUnownedCliPrimaryMessage({ primary: unownedPrimary, config })); + } + } + const shouldRunBareRootCrestodian = shouldStartCrestodianForBareRoot(normalizedArgv); const shouldRunModernOnboardCrestodian = shouldStartCrestodianForModernOnboard(normalizedArgv); if (shouldRunBareRootCrestodian || shouldRunModernOnboardCrestodian) {