diff --git a/src/cli/program/command-registry.test.ts b/src/cli/program/command-registry.test.ts index d7e7e92af..cc667fb51 100644 --- a/src/cli/program/command-registry.test.ts +++ b/src/cli/program/command-registry.test.ts @@ -1,5 +1,5 @@ import { Command } from "commander"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import type { ProgramContext } from "./context.js"; import { getCoreCliCommandNames, @@ -7,6 +7,14 @@ import { registerCoreCliCommands, } from "./command-registry.js"; +vi.mock("./register.status-health-sessions.js", () => ({ + registerStatusHealthSessionsCommands: (program: Command) => { + program.command("status"); + program.command("health"); + program.command("sessions"); + }, +})); + const testProgramContext: ProgramContext = { programVersion: "0.0.0-test", channelOptions: [], @@ -57,4 +65,23 @@ describe("command-registry", () => { expect(names).toContain("uninstall"); expect(names).not.toContain("maintenance"); }); + + it("registers grouped core entry placeholders without duplicate command errors", async () => { + const program = new Command(); + registerCoreCliCommands(program, testProgramContext, ["node", "openclaw", "vitest"]); + + const prevArgv = process.argv; + process.argv = ["node", "openclaw", "status"]; + try { + program.exitOverride(); + await program.parseAsync(["node", "openclaw", "status"]); + } finally { + process.argv = prevArgv; + } + + const names = program.commands.map((command) => command.name()); + expect(names).toContain("status"); + expect(names).toContain("health"); + expect(names).toContain("sessions"); + }); }); diff --git a/src/cli/program/command-registry.ts b/src/cli/program/command-registry.ts index 0ed726cfa..cbe61c67c 100644 --- a/src/cli/program/command-registry.ts +++ b/src/cli/program/command-registry.ts @@ -148,7 +148,14 @@ function registerLazyCoreCommand( placeholder.allowUnknownOption(true); placeholder.allowExcessArguments(true); placeholder.action(async (...actionArgs) => { - removeCommand(program, placeholder); + // Some registrars install multiple top-level commands (e.g. status/health/sessions). + // Remove placeholders/old registrations for all names in the entry before re-registering. + for (const cmd of entry.commands) { + const existing = program.commands.find((c) => c.name() === cmd.name); + if (existing) { + removeCommand(program, existing); + } + } await entry.register({ program, ctx, argv: process.argv }); const actionCommand = actionArgs.at(-1) as Command | undefined; const root = actionCommand?.parent ?? program;