From 5edabf4776c9063f63b7f30367add54552a6be07 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 6 Apr 2026 13:51:37 +0100 Subject: [PATCH] refactor: share cli command registration policy --- src/cli/command-registration-policy.test.ts | 54 +++++++++++++++++ src/cli/command-registration-policy.ts | 31 ++++++++++ src/cli/program/command-registry.ts | 12 +--- src/cli/program/register.subclis.ts | 25 +++----- src/cli/run-main.test.ts | 67 --------------------- src/cli/run-main.ts | 40 +++--------- 6 files changed, 102 insertions(+), 127 deletions(-) create mode 100644 src/cli/command-registration-policy.test.ts create mode 100644 src/cli/command-registration-policy.ts diff --git a/src/cli/command-registration-policy.test.ts b/src/cli/command-registration-policy.test.ts new file mode 100644 index 00000000000..5fbb951aea2 --- /dev/null +++ b/src/cli/command-registration-policy.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import { + shouldEagerRegisterSubcommands, + shouldRegisterPrimaryCommandOnly, + shouldRegisterPrimarySubcommandOnly, + shouldSkipPluginCommandRegistration, +} from "./command-registration-policy.js"; + +describe("command-registration-policy", () => { + it("matches primary command registration policy", () => { + expect(shouldRegisterPrimaryCommandOnly(["node", "openclaw", "status"])).toBe(true); + expect(shouldRegisterPrimaryCommandOnly(["node", "openclaw", "status", "--help"])).toBe(false); + expect(shouldRegisterPrimaryCommandOnly(["node", "openclaw", "-V"])).toBe(false); + expect(shouldRegisterPrimaryCommandOnly(["node", "openclaw", "acp", "-v"])).toBe(true); + }); + + it("matches plugin registration skip policy", () => { + expect( + shouldSkipPluginCommandRegistration({ + argv: ["node", "openclaw", "--help"], + primary: null, + hasBuiltinPrimary: false, + }), + ).toBe(true); + expect( + shouldSkipPluginCommandRegistration({ + argv: ["node", "openclaw", "config", "--help"], + primary: "config", + hasBuiltinPrimary: true, + }), + ).toBe(true); + expect( + shouldSkipPluginCommandRegistration({ + argv: ["node", "openclaw", "voicecall", "--help"], + primary: "voicecall", + hasBuiltinPrimary: false, + }), + ).toBe(false); + }); + + it("matches lazy subcommand registration policy", () => { + expect(shouldEagerRegisterSubcommands({ OPENCLAW_DISABLE_LAZY_SUBCOMMANDS: "1" })).toBe(true); + expect(shouldEagerRegisterSubcommands({ OPENCLAW_DISABLE_LAZY_SUBCOMMANDS: "0" })).toBe(false); + expect(shouldRegisterPrimarySubcommandOnly(["node", "openclaw", "acp"], {})).toBe(true); + expect(shouldRegisterPrimarySubcommandOnly(["node", "openclaw", "acp", "--help"], {})).toBe( + false, + ); + expect( + shouldRegisterPrimarySubcommandOnly(["node", "openclaw", "acp"], { + OPENCLAW_DISABLE_LAZY_SUBCOMMANDS: "1", + }), + ).toBe(false); + }); +}); diff --git a/src/cli/command-registration-policy.ts b/src/cli/command-registration-policy.ts new file mode 100644 index 00000000000..ffa7ac71789 --- /dev/null +++ b/src/cli/command-registration-policy.ts @@ -0,0 +1,31 @@ +import { isTruthyEnvValue } from "../infra/env.js"; +import { hasHelpOrVersion } from "./argv.js"; + +export function shouldRegisterPrimaryCommandOnly(argv: string[]): boolean { + return !hasHelpOrVersion(argv); +} + +export function shouldSkipPluginCommandRegistration(params: { + argv: string[]; + primary: string | null; + hasBuiltinPrimary: boolean; +}): boolean { + if (params.hasBuiltinPrimary) { + return true; + } + if (!params.primary) { + return hasHelpOrVersion(params.argv); + } + return false; +} + +export function shouldEagerRegisterSubcommands(env: NodeJS.ProcessEnv = process.env): boolean { + return isTruthyEnvValue(env.OPENCLAW_DISABLE_LAZY_SUBCOMMANDS); +} + +export function shouldRegisterPrimarySubcommandOnly( + argv: string[], + env: NodeJS.ProcessEnv = process.env, +): boolean { + return !shouldEagerRegisterSubcommands(env) && shouldRegisterPrimaryCommandOnly(argv); +} diff --git a/src/cli/program/command-registry.ts b/src/cli/program/command-registry.ts index 95321560301..257cf73d070 100644 --- a/src/cli/program/command-registry.ts +++ b/src/cli/program/command-registry.ts @@ -1,5 +1,6 @@ import type { Command } from "commander"; -import { getPrimaryCommand, hasHelpOrVersion } from "../argv.js"; +import { getPrimaryCommand } from "../argv.js"; +import { shouldRegisterPrimaryCommandOnly } from "../command-registration-policy.js"; import { removeCommandByName } from "./command-tree.js"; import type { ProgramContext } from "./context.js"; import { @@ -28,13 +29,6 @@ type CoreCliEntry = { register: (params: CommandRegisterParams) => Promise | void; }; -const shouldRegisterCorePrimaryOnly = (argv: string[]) => { - if (hasHelpOrVersion(argv)) { - return false; - } - return true; -}; - // Note for humans and agents: // If you update the list of commands, also check whether they have subcommands // and set the flag accordingly. @@ -259,7 +253,7 @@ export async function registerCoreCliByName( export function registerCoreCliCommands(program: Command, ctx: ProgramContext, argv: string[]) { const primary = getPrimaryCommand(argv); - if (primary && shouldRegisterCorePrimaryOnly(argv)) { + if (primary && shouldRegisterPrimaryCommandOnly(argv)) { const entry = coreEntries.find((candidate) => candidate.commands.some((cmd) => cmd.name === primary), ); diff --git a/src/cli/program/register.subclis.ts b/src/cli/program/register.subclis.ts index 24810755337..8036e9d2c70 100644 --- a/src/cli/program/register.subclis.ts +++ b/src/cli/program/register.subclis.ts @@ -1,7 +1,10 @@ import type { Command } from "commander"; import type { OpenClawConfig } from "../../config/config.js"; -import { isTruthyEnvValue } from "../../infra/env.js"; -import { getPrimaryCommand, hasHelpOrVersion } from "../argv.js"; +import { getPrimaryCommand } from "../argv.js"; +import { + shouldEagerRegisterSubcommands, + shouldRegisterPrimarySubcommandOnly, +} from "../command-registration-policy.js"; import { removeCommandByName } from "./command-tree.js"; import { registerLazyCommand as registerLazyCommandPlaceholder } from "./register-lazy-command.js"; import { @@ -18,20 +21,6 @@ type SubCliEntry = SubCliDescriptor & { register: SubCliRegistrar; }; -const shouldRegisterPrimaryOnly = (argv: string[]) => { - if (isTruthyEnvValue(process.env.OPENCLAW_DISABLE_LAZY_SUBCOMMANDS)) { - return false; - } - if (hasHelpOrVersion(argv)) { - return false; - } - return true; -}; - -const shouldEagerRegisterSubcommands = (_argv: string[]) => { - return isTruthyEnvValue(process.env.OPENCLAW_DISABLE_LAZY_SUBCOMMANDS); -}; - export const loadValidatedConfigForPluginRegistration = async (): Promise => { const mod = await import("../../config/config.js"); @@ -348,14 +337,14 @@ function registerLazyCommand(program: Command, entry: SubCliEntry) { } export function registerSubCliCommands(program: Command, argv: string[] = process.argv) { - if (shouldEagerRegisterSubcommands(argv)) { + if (shouldEagerRegisterSubcommands()) { for (const entry of entries) { void entry.register(program); } return; } const primary = getPrimaryCommand(argv); - if (primary && shouldRegisterPrimaryOnly(argv)) { + if (primary && shouldRegisterPrimarySubcommandOnly(argv)) { const entry = entries.find((candidate) => candidate.name === primary); if (entry) { registerLazyCommand(program, entry); diff --git a/src/cli/run-main.test.ts b/src/cli/run-main.test.ts index a9e1da193a6..e3d5671d553 100644 --- a/src/cli/run-main.test.ts +++ b/src/cli/run-main.test.ts @@ -3,8 +3,6 @@ import { rewriteUpdateFlagArgv, resolveMissingPluginCommandMessage, shouldEnsureCliPath, - shouldRegisterPrimarySubcommand, - shouldSkipPluginCommandRegistration, shouldUseRootHelpFastPath, } from "./run-main.js"; @@ -42,71 +40,6 @@ describe("rewriteUpdateFlagArgv", () => { }); }); -describe("shouldRegisterPrimarySubcommand", () => { - it("skips eager primary registration for help/version invocations", () => { - expect(shouldRegisterPrimarySubcommand(["node", "openclaw", "status", "--help"])).toBe(false); - expect(shouldRegisterPrimarySubcommand(["node", "openclaw", "-V"])).toBe(false); - expect(shouldRegisterPrimarySubcommand(["node", "openclaw", "-v"])).toBe(false); - }); - - it("keeps eager primary registration for regular command runs", () => { - expect(shouldRegisterPrimarySubcommand(["node", "openclaw", "status"])).toBe(true); - expect(shouldRegisterPrimarySubcommand(["node", "openclaw", "acp", "-v"])).toBe(true); - }); -}); - -describe("shouldSkipPluginCommandRegistration", () => { - it("skips plugin registration for root help/version", () => { - expect( - shouldSkipPluginCommandRegistration({ - argv: ["node", "openclaw", "--help"], - primary: null, - hasBuiltinPrimary: false, - }), - ).toBe(true); - }); - - it("skips plugin registration for builtin subcommand help", () => { - expect( - shouldSkipPluginCommandRegistration({ - argv: ["node", "openclaw", "config", "--help"], - primary: "config", - hasBuiltinPrimary: true, - }), - ).toBe(true); - }); - - it("skips plugin registration for builtin command runs", () => { - expect( - shouldSkipPluginCommandRegistration({ - argv: ["node", "openclaw", "sessions", "--json"], - primary: "sessions", - hasBuiltinPrimary: true, - }), - ).toBe(true); - }); - - it("keeps plugin registration for non-builtin help", () => { - expect( - shouldSkipPluginCommandRegistration({ - argv: ["node", "openclaw", "voicecall", "--help"], - primary: "voicecall", - hasBuiltinPrimary: false, - }), - ).toBe(false); - }); - - it("keeps plugin registration for non-builtin command runs", () => { - expect( - shouldSkipPluginCommandRegistration({ - argv: ["node", "openclaw", "voicecall", "status"], - primary: "voicecall", - hasBuiltinPrimary: false, - }), - ).toBe(false); - }); -}); - describe("shouldEnsureCliPath", () => { it("skips path bootstrap for help/version invocations", () => { expect(shouldEnsureCliPath(["node", "openclaw", "--help"])).toBe(false); diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index fa30d732e35..7359ea4379b 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -18,6 +18,11 @@ import { hasHelpOrVersion, isRootHelpInvocation, } from "./argv.js"; +import { + shouldRegisterPrimaryCommandOnly, + shouldSkipPluginCommandRegistration, +} from "./command-registration-policy.js"; +import { shouldEnsureCliPathForCommandPath } from "./command-startup-policy.js"; import { maybeRunCliInContainer, parseCliContainerArgs } from "./container-target.js"; import { applyCliProfileEnv, parseCliProfileArgs } from "./profile.js"; import { tryRouteCli } from "./route.js"; @@ -46,42 +51,11 @@ export function rewriteUpdateFlagArgv(argv: string[]): string[] { return next; } -export function shouldRegisterPrimarySubcommand(argv: string[]): boolean { - return !hasHelpOrVersion(argv); -} - -export function shouldSkipPluginCommandRegistration(params: { - argv: string[]; - primary: string | null; - hasBuiltinPrimary: boolean; -}): boolean { - if (params.hasBuiltinPrimary) { - return true; - } - if (!params.primary) { - return hasHelpOrVersion(params.argv); - } - return false; -} - export function shouldEnsureCliPath(argv: string[]): boolean { if (hasHelpOrVersion(argv)) { return false; } - const [primary, secondary] = getCommandPathWithRootOptions(argv, 2); - if (!primary) { - return true; - } - if (primary === "status" || primary === "health" || primary === "sessions") { - return false; - } - if (primary === "config" && (secondary === "get" || secondary === "unset")) { - return false; - } - if (primary === "models" && (secondary === "list" || secondary === "status")) { - return false; - } - return true; + return shouldEnsureCliPathForCommandPath(getCommandPathWithRootOptions(argv, 2)); } export function shouldUseRootHelpFastPath(argv: string[]): boolean { @@ -200,7 +174,7 @@ export async function runCli(argv: string[] = process.argv) { // Register the primary command (builtin or subcli) so help and command parsing // are correct even with lazy command registration. const primary = getPrimaryCommand(parseArgv); - if (primary) { + if (primary && shouldRegisterPrimaryCommandOnly(parseArgv)) { const { getProgramContext } = await import("./program/program-context.js"); const ctx = getProgramContext(program); if (ctx) {