refactor: share cli command registration policy

This commit is contained in:
Peter Steinberger
2026-04-06 13:51:37 +01:00
parent a21709d041
commit 5edabf4776
6 changed files with 102 additions and 127 deletions

View File

@@ -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);
});
});

View File

@@ -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);
}

View File

@@ -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> | 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),
);

View File

@@ -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<OpenClawConfig | null> => {
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);

View File

@@ -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);

View File

@@ -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) {