refactor: share cli command registrar engine

This commit is contained in:
Peter Steinberger
2026-04-06 14:15:15 +01:00
parent f43aba40a2
commit 73a5504708
3 changed files with 141 additions and 106 deletions

View File

@@ -1,14 +1,17 @@
import type { Command } from "commander";
import { getPrimaryCommand } from "../argv.js";
import { resolveCliArgvInvocation } from "../argv-invocation.js";
import { shouldRegisterPrimaryCommandOnly } from "../command-registration-policy.js";
import { removeCommandByName } from "./command-tree.js";
import type { ProgramContext } from "./context.js";
import {
type CoreCliCommandDescriptor,
getCoreCliCommandDescriptors,
getCoreCliCommandsWithSubcommands,
} from "./core-command-descriptors.js";
import { registerLazyCommand } from "./register-lazy-command.js";
import {
registerCommandGroupByName,
registerCommandGroups,
type CommandGroupEntry,
} from "./register-command-groups.js";
import { registerSubCliCommands } from "./register.subclis.js";
export { getCoreCliCommandDescriptors, getCoreCliCommandsWithSubcommands };
@@ -26,7 +29,7 @@ export type CommandRegistration = {
type CoreCliEntry = {
commands: CoreCliCommandDescriptor[];
register: (params: CommandRegisterParams) => Promise<void> | void;
registerWithParams: (params: CommandRegisterParams) => Promise<void> | void;
};
// Note for humans and agents:
@@ -41,7 +44,7 @@ const coreEntries: CoreCliEntry[] = [
hasSubcommands: false,
},
],
register: async ({ program }) => {
registerWithParams: async ({ program }) => {
const mod = await import("./register.setup.js");
mod.registerSetupCommand(program);
},
@@ -54,7 +57,7 @@ const coreEntries: CoreCliEntry[] = [
hasSubcommands: false,
},
],
register: async ({ program }) => {
registerWithParams: async ({ program }) => {
const mod = await import("./register.onboard.js");
mod.registerOnboardCommand(program);
},
@@ -68,7 +71,7 @@ const coreEntries: CoreCliEntry[] = [
hasSubcommands: false,
},
],
register: async ({ program }) => {
registerWithParams: async ({ program }) => {
const mod = await import("./register.configure.js");
mod.registerConfigureCommand(program);
},
@@ -82,7 +85,7 @@ const coreEntries: CoreCliEntry[] = [
hasSubcommands: true,
},
],
register: async ({ program }) => {
registerWithParams: async ({ program }) => {
const mod = await import("../config-cli.js");
mod.registerConfigCli(program);
},
@@ -95,7 +98,7 @@ const coreEntries: CoreCliEntry[] = [
hasSubcommands: true,
},
],
register: async ({ program }) => {
registerWithParams: async ({ program }) => {
const mod = await import("./register.backup.js");
mod.registerBackupCommand(program);
},
@@ -123,7 +126,7 @@ const coreEntries: CoreCliEntry[] = [
hasSubcommands: false,
},
],
register: async ({ program }) => {
registerWithParams: async ({ program }) => {
const mod = await import("./register.maintenance.js");
mod.registerMaintenanceCommands(program);
},
@@ -136,7 +139,7 @@ const coreEntries: CoreCliEntry[] = [
hasSubcommands: true,
},
],
register: async ({ program, ctx }) => {
registerWithParams: async ({ program, ctx }) => {
const mod = await import("./register.message.js");
mod.registerMessageCommands(program, ctx);
},
@@ -149,7 +152,7 @@ const coreEntries: CoreCliEntry[] = [
hasSubcommands: true,
},
],
register: async ({ program }) => {
registerWithParams: async ({ program }) => {
const mod = await import("../mcp-cli.js");
mod.registerMcpCli(program);
},
@@ -167,7 +170,7 @@ const coreEntries: CoreCliEntry[] = [
hasSubcommands: true,
},
],
register: async ({ program, ctx }) => {
registerWithParams: async ({ program, ctx }) => {
const mod = await import("./register.agent.js");
mod.registerAgentCommands(program, {
agentChannelOptions: ctx.agentChannelOptions,
@@ -197,80 +200,42 @@ const coreEntries: CoreCliEntry[] = [
hasSubcommands: true,
},
],
register: async ({ program }) => {
registerWithParams: async ({ program }) => {
const mod = await import("./register.status-health-sessions.js");
mod.registerStatusHealthSessionsCommands(program);
},
},
];
function resolveCoreCommandGroups(ctx: ProgramContext, argv: string[]): CommandGroupEntry[] {
return coreEntries.map((entry) => ({
placeholders: entry.commands,
register: async (program) => {
await entry.registerWithParams({ program, ctx, argv });
},
}));
}
export function getCoreCliCommandNames(): string[] {
return getCoreCliCommandDescriptors().map((command) => command.name);
}
function removeEntryCommands(program: Command, entry: CoreCliEntry) {
// 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) {
removeCommandByName(program, cmd.name);
}
}
function registerLazyCoreCommand(
program: Command,
ctx: ProgramContext,
entry: CoreCliEntry,
command: CoreCliCommandDescriptor,
) {
registerLazyCommand({
program,
name: command.name,
description: command.description,
removeNames: entry.commands.map((cmd) => cmd.name),
register: async () => {
await entry.register({ program, ctx, argv: process.argv });
},
});
}
export async function registerCoreCliByName(
program: Command,
ctx: ProgramContext,
name: string,
argv: string[] = process.argv,
): Promise<boolean> {
const entry = coreEntries.find((candidate) =>
candidate.commands.some((cmd) => cmd.name === name),
);
if (!entry) {
return false;
}
removeEntryCommands(program, entry);
await entry.register({ program, ctx, argv });
return true;
return registerCommandGroupByName(program, resolveCoreCommandGroups(ctx, argv), name);
}
export function registerCoreCliCommands(program: Command, ctx: ProgramContext, argv: string[]) {
const primary = getPrimaryCommand(argv);
if (primary && shouldRegisterPrimaryCommandOnly(argv)) {
const entry = coreEntries.find((candidate) =>
candidate.commands.some((cmd) => cmd.name === primary),
);
if (entry) {
const cmd = entry.commands.find((c) => c.name === primary);
if (cmd) {
registerLazyCoreCommand(program, ctx, entry, cmd);
}
return;
}
}
for (const entry of coreEntries) {
for (const cmd of entry.commands) {
registerLazyCoreCommand(program, ctx, entry, cmd);
}
}
const { primary } = resolveCliArgvInvocation(argv);
registerCommandGroups(program, resolveCoreCommandGroups(ctx, argv), {
eager: false,
primary,
registerPrimaryOnly: Boolean(primary && shouldRegisterPrimaryCommandOnly(argv)),
});
}
export function registerProgramCommands(

View File

@@ -0,0 +1,88 @@
import type { Command } from "commander";
import { removeCommandByName } from "./command-tree.js";
import { registerLazyCommand } from "./register-lazy-command.js";
export type CommandGroupPlaceholder = {
name: string;
description: string;
};
export type CommandGroupEntry = {
placeholders: readonly CommandGroupPlaceholder[];
register: (program: Command) => Promise<void> | void;
};
function findCommandGroupEntry(
entries: readonly CommandGroupEntry[],
name: string,
): CommandGroupEntry | undefined {
return entries.find((entry) =>
entry.placeholders.some((placeholder) => placeholder.name === name),
);
}
export async function registerCommandGroupByName(
program: Command,
entries: readonly CommandGroupEntry[],
name: string,
): Promise<boolean> {
const entry = findCommandGroupEntry(entries, name);
if (!entry) {
return false;
}
for (const placeholder of entry.placeholders) {
removeCommandByName(program, placeholder.name);
}
await entry.register(program);
return true;
}
function registerLazyCommandGroup(
program: Command,
entry: CommandGroupEntry,
placeholder: CommandGroupPlaceholder,
) {
registerLazyCommand({
program,
name: placeholder.name,
description: placeholder.description,
removeNames: entry.placeholders.map((candidate) => candidate.name),
register: async () => {
await entry.register(program);
},
});
}
export function registerCommandGroups(
program: Command,
entries: readonly CommandGroupEntry[],
params: {
eager: boolean;
primary: string | null;
registerPrimaryOnly: boolean;
},
) {
if (params.eager) {
for (const entry of entries) {
void entry.register(program);
}
return;
}
if (params.primary && params.registerPrimaryOnly) {
const entry = findCommandGroupEntry(entries, params.primary);
if (entry) {
const placeholder = entry.placeholders.find((candidate) => candidate.name === params.primary);
if (placeholder) {
registerLazyCommandGroup(program, entry, placeholder);
}
return;
}
}
for (const entry of entries) {
for (const placeholder of entry.placeholders) {
registerLazyCommandGroup(program, entry, placeholder);
}
}
}

View File

@@ -1,12 +1,15 @@
import type { Command } from "commander";
import type { OpenClawConfig } from "../../config/config.js";
import { getPrimaryCommand } from "../argv.js";
import { resolveCliArgvInvocation } from "../argv-invocation.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 {
registerCommandGroupByName,
registerCommandGroups,
type CommandGroupEntry,
} from "./register-command-groups.js";
import {
getSubCliCommandsWithSubcommands,
getSubCliEntries as getSubCliEntryDescriptors,
@@ -311,47 +314,26 @@ const entries: SubCliEntry[] = [
},
];
function resolveSubCliCommandGroups(): CommandGroupEntry[] {
return entries.map((entry) => ({
placeholders: [entry],
register: entry.register,
}));
}
export function getSubCliEntries(): ReadonlyArray<SubCliDescriptor> {
return getSubCliEntryDescriptors();
}
export async function registerSubCliByName(program: Command, name: string): Promise<boolean> {
const entry = entries.find((candidate) => candidate.name === name);
if (!entry) {
return false;
}
removeCommandByName(program, entry.name);
await entry.register(program);
return true;
}
function registerLazyCommand(program: Command, entry: SubCliEntry) {
registerLazyCommandPlaceholder({
program,
name: entry.name,
description: entry.description,
register: async () => {
await entry.register(program);
},
});
return registerCommandGroupByName(program, resolveSubCliCommandGroups(), name);
}
export function registerSubCliCommands(program: Command, argv: string[] = process.argv) {
if (shouldEagerRegisterSubcommands()) {
for (const entry of entries) {
void entry.register(program);
}
return;
}
const primary = getPrimaryCommand(argv);
if (primary && shouldRegisterPrimarySubcommandOnly(argv)) {
const entry = entries.find((candidate) => candidate.name === primary);
if (entry) {
registerLazyCommand(program, entry);
return;
}
}
for (const candidate of entries) {
registerLazyCommand(program, candidate);
}
const { primary } = resolveCliArgvInvocation(argv);
registerCommandGroups(program, resolveSubCliCommandGroups(), {
eager: shouldEagerRegisterSubcommands(),
primary,
registerPrimaryOnly: Boolean(primary && shouldRegisterPrimarySubcommandOnly(argv)),
});
}