refactor: share cli command descriptor helpers

This commit is contained in:
Peter Steinberger
2026-04-06 14:51:57 +01:00
parent 154a7edb7c
commit 41905d9fd7
10 changed files with 555 additions and 239 deletions

View File

@@ -0,0 +1,74 @@
import { Command } from "commander";
import { describe, expect, it } from "vitest";
import {
addCommandDescriptorsToProgram,
collectUniqueCommandDescriptors,
defineCommandDescriptorCatalog,
getCommandDescriptorNames,
getCommandsWithSubcommands,
} from "./command-descriptor-utils.js";
describe("command-descriptor-utils", () => {
const descriptors = [
{ name: "alpha", description: "Alpha", hasSubcommands: false },
{ name: "beta", description: "Beta", hasSubcommands: true },
{ name: "gamma", description: "Gamma", hasSubcommands: true },
] as const;
it("returns descriptor names in order", () => {
expect(getCommandDescriptorNames(descriptors)).toEqual(["alpha", "beta", "gamma"]);
});
it("returns commands with subcommands", () => {
expect(getCommandsWithSubcommands(descriptors)).toEqual(["beta", "gamma"]);
});
it("collects unique descriptors across groups in order", () => {
expect(
collectUniqueCommandDescriptors([
[
{ name: "alpha", description: "Alpha" },
{ name: "beta", description: "Beta" },
],
[
{ name: "beta", description: "Ignored duplicate" },
{ name: "gamma", description: "Gamma" },
],
]),
).toEqual([
{ name: "alpha", description: "Alpha" },
{ name: "beta", description: "Beta" },
{ name: "gamma", description: "Gamma" },
]);
});
it("defines a reusable descriptor catalog", () => {
const catalog = defineCommandDescriptorCatalog(descriptors);
expect(catalog.descriptors).toBe(descriptors);
expect(catalog.getDescriptors()).toBe(descriptors);
expect(catalog.getNames()).toEqual(["alpha", "beta", "gamma"]);
expect(catalog.getCommandsWithSubcommands()).toEqual(["beta", "gamma"]);
});
it("adds descriptors without duplicating existing commands", () => {
const program = new Command();
const existingCommands = addCommandDescriptorsToProgram(program, descriptors);
addCommandDescriptorsToProgram(
program,
[
{ name: "beta", description: "Ignored duplicate" },
{ name: "delta", description: "Delta" },
],
existingCommands,
);
expect(program.commands.map((command) => command.name())).toEqual([
"alpha",
"beta",
"gamma",
"delta",
]);
});
});

View File

@@ -0,0 +1,68 @@
import type { Command } from "commander";
import type { NamedCommandDescriptor } from "./command-group-descriptors.js";
export type CommandDescriptorLike = Pick<NamedCommandDescriptor, "name" | "description">;
export type CommandDescriptorCatalog<TDescriptor extends NamedCommandDescriptor> = {
descriptors: readonly TDescriptor[];
getDescriptors: () => readonly TDescriptor[];
getNames: () => string[];
getCommandsWithSubcommands: () => string[];
};
export function getCommandDescriptorNames<TDescriptor extends CommandDescriptorLike>(
descriptors: readonly TDescriptor[],
): string[] {
return descriptors.map((descriptor) => descriptor.name);
}
export function getCommandsWithSubcommands<TDescriptor extends NamedCommandDescriptor>(
descriptors: readonly TDescriptor[],
): string[] {
return descriptors
.filter((descriptor) => descriptor.hasSubcommands)
.map((descriptor) => descriptor.name);
}
export function collectUniqueCommandDescriptors<TDescriptor extends CommandDescriptorLike>(
descriptorGroups: readonly (readonly TDescriptor[])[],
): TDescriptor[] {
const seen = new Set<string>();
const descriptors: TDescriptor[] = [];
for (const group of descriptorGroups) {
for (const descriptor of group) {
if (seen.has(descriptor.name)) {
continue;
}
seen.add(descriptor.name);
descriptors.push(descriptor);
}
}
return descriptors;
}
export function defineCommandDescriptorCatalog<TDescriptor extends NamedCommandDescriptor>(
descriptors: readonly TDescriptor[],
): CommandDescriptorCatalog<TDescriptor> {
return {
descriptors,
getDescriptors: () => descriptors,
getNames: () => getCommandDescriptorNames(descriptors),
getCommandsWithSubcommands: () => getCommandsWithSubcommands(descriptors),
};
}
export function addCommandDescriptorsToProgram<TDescriptor extends CommandDescriptorLike>(
program: Command,
descriptors: readonly TDescriptor[],
existingCommands: Set<string> = new Set(),
): Set<string> {
for (const descriptor of descriptors) {
if (existingCommands.has(descriptor.name)) {
continue;
}
program.command(descriptor.name).description(descriptor.description);
existingCommands.add(descriptor.name);
}
return existingCommands;
}

View File

@@ -0,0 +1,134 @@
import { describe, expect, it, vi } from "vitest";
import {
buildCommandGroupEntries,
defineImportedCommandGroupSpec,
defineImportedCommandGroupSpecs,
defineImportedProgramCommandGroupSpec,
defineImportedProgramCommandGroupSpecs,
resolveCommandGroupEntries,
} from "./command-group-descriptors.js";
const descriptors = [
{
name: "alpha",
description: "Alpha command",
hasSubcommands: false,
},
{
name: "beta",
description: "Beta command",
hasSubcommands: true,
},
] as const;
describe("command-group-descriptors", () => {
it("resolves placeholders by descriptor name", () => {
const register = vi.fn();
expect(
resolveCommandGroupEntries(descriptors, [{ commandNames: ["alpha"], register }]),
).toEqual([
{
placeholders: [descriptors[0]],
register,
},
]);
});
it("builds command-group entries with a register mapper", () => {
const register = vi.fn();
const mappedRegister = vi.fn();
const entries = buildCommandGroupEntries(
descriptors,
[{ commandNames: ["beta"], register }],
() => mappedRegister,
);
expect(entries).toEqual([
{
placeholders: [descriptors[1]],
register: mappedRegister,
},
]);
expect(register).not.toHaveBeenCalled();
});
it("builds imported specs that lazy-load and register once", async () => {
const module = { register: vi.fn() };
const loadModule = vi.fn(async () => module);
const spec = defineImportedCommandGroupSpec(["alpha"], loadModule, (loaded, args: string) => {
loaded.register(args);
});
await spec.register("ok");
expect(loadModule).toHaveBeenCalledTimes(1);
expect(module.register).toHaveBeenCalledWith("ok");
});
it("builds imported specs from definition arrays", async () => {
const alpha = { registerAlpha: vi.fn() };
const beta = { registerBeta: vi.fn() };
const specs = defineImportedCommandGroupSpecs<string, typeof alpha | typeof beta>([
{
commandNames: ["alpha"],
loadModule: async () => alpha,
register: (loaded, value) => {
if ("registerAlpha" in loaded) {
loaded.registerAlpha(value);
}
},
},
{
commandNames: ["beta"],
loadModule: async () => beta,
register: (loaded, value) => {
if ("registerBeta" in loaded) {
loaded.registerBeta(value);
}
},
},
]);
await specs[0].register("one");
await specs[1].register("two");
expect(alpha.registerAlpha).toHaveBeenCalledWith("one");
expect(beta.registerBeta).toHaveBeenCalledWith("two");
});
it("builds program-only imported specs from exported registrar names", async () => {
const module = { registerAlpha: vi.fn() };
const spec = defineImportedProgramCommandGroupSpec({
commandNames: ["alpha"],
loadModule: async () => module,
exportName: "registerAlpha",
});
await spec.register("program" as never);
expect(module.registerAlpha).toHaveBeenCalledWith("program");
});
it("builds multiple program-only imported specs from definition arrays", async () => {
const alpha = { registerAlpha: vi.fn() };
const beta = { registerBeta: vi.fn() };
const specs = defineImportedProgramCommandGroupSpecs([
{
commandNames: ["alpha"],
loadModule: async () => alpha,
exportName: "registerAlpha",
},
{
commandNames: ["beta"],
loadModule: async () => beta,
exportName: "registerBeta",
},
]);
await specs[0].register("program-one" as never);
await specs[1].register("program-two" as never);
expect(alpha.registerAlpha).toHaveBeenCalledWith("program-one");
expect(beta.registerBeta).toHaveBeenCalledWith("program-two");
});
});

View File

@@ -0,0 +1,117 @@
import type { Command } from "commander";
import type { CommandGroupEntry } from "./register-command-groups.js";
export type NamedCommandDescriptor = {
name: string;
description: string;
hasSubcommands: boolean;
};
export type CommandGroupDescriptorSpec<TRegister> = {
commandNames: readonly string[];
register: TRegister;
};
export type ImportedCommandGroupDefinition<TRegisterArgs, TModule> = {
commandNames: readonly string[];
loadModule: () => Promise<TModule>;
register: (module: TModule, args: TRegisterArgs) => Promise<void> | void;
};
export type ResolvedCommandGroupEntry<TDescriptor extends NamedCommandDescriptor, TRegister> = {
placeholders: TDescriptor[];
register: TRegister;
};
function buildDescriptorIndex<TDescriptor extends NamedCommandDescriptor>(
descriptors: readonly TDescriptor[],
): Map<string, TDescriptor> {
return new Map(descriptors.map((descriptor) => [descriptor.name, descriptor]));
}
export function resolveCommandGroupEntries<TDescriptor extends NamedCommandDescriptor, TRegister>(
descriptors: readonly TDescriptor[],
specs: readonly CommandGroupDescriptorSpec<TRegister>[],
): ResolvedCommandGroupEntry<TDescriptor, TRegister>[] {
const descriptorsByName = buildDescriptorIndex(descriptors);
return specs.map((spec) => ({
placeholders: spec.commandNames.map((name) => {
const descriptor = descriptorsByName.get(name);
if (!descriptor) {
throw new Error(`Unknown command descriptor: ${name}`);
}
return descriptor;
}),
register: spec.register,
}));
}
export function buildCommandGroupEntries<TDescriptor extends NamedCommandDescriptor, TRegister>(
descriptors: readonly TDescriptor[],
specs: readonly CommandGroupDescriptorSpec<TRegister>[],
mapRegister: (register: TRegister) => CommandGroupEntry["register"],
): CommandGroupEntry[] {
return resolveCommandGroupEntries(descriptors, specs).map((entry) => ({
placeholders: entry.placeholders,
register: mapRegister(entry.register),
}));
}
export function defineImportedCommandGroupSpec<TRegisterArgs, TModule>(
commandNames: readonly string[],
loadModule: () => Promise<TModule>,
register: (module: TModule, args: TRegisterArgs) => Promise<void> | void,
): CommandGroupDescriptorSpec<(args: TRegisterArgs) => Promise<void>> {
return {
commandNames,
register: async (args: TRegisterArgs) => {
const module = await loadModule();
await register(module, args);
},
};
}
export function defineImportedCommandGroupSpecs<TRegisterArgs, TModule>(
definitions: readonly ImportedCommandGroupDefinition<TRegisterArgs, TModule>[],
): CommandGroupDescriptorSpec<(args: TRegisterArgs) => Promise<void>>[] {
return definitions.map((definition) =>
defineImportedCommandGroupSpec(
definition.commandNames,
definition.loadModule,
definition.register,
),
);
}
type ProgramCommandRegistrar = (program: Command) => Promise<void> | void;
export type ImportedProgramCommandGroupDefinition<
TModule extends Record<TKey, ProgramCommandRegistrar>,
TKey extends keyof TModule & string,
> = {
commandNames: readonly string[];
loadModule: () => Promise<TModule>;
exportName: TKey;
};
export function defineImportedProgramCommandGroupSpec<
TModule extends Record<TKey, ProgramCommandRegistrar>,
TKey extends keyof TModule & string,
>(
definition: ImportedProgramCommandGroupDefinition<TModule, TKey>,
): CommandGroupDescriptorSpec<(program: Command) => Promise<void>> {
return defineImportedCommandGroupSpec(
definition.commandNames,
definition.loadModule,
(module, program: Command) => module[definition.exportName](program),
);
}
export function defineImportedProgramCommandGroupSpecs<
TModule extends Record<TKey, ProgramCommandRegistrar>,
TKey extends keyof TModule & string,
>(
definitions: readonly ImportedProgramCommandGroupDefinition<TModule, TKey>[],
): CommandGroupDescriptorSpec<(program: Command) => Promise<void>>[] {
return definitions.map((definition) => defineImportedProgramCommandGroupSpec(definition));
}

View File

@@ -67,6 +67,7 @@ describe("command-registry", () => {
it("includes both agent and agents in core CLI command names", () => {
const names = getCoreCliCommandNames();
expect(names).toContain("mcp");
expect(names).toContain("agent");
expect(names).toContain("agents");
});
@@ -76,6 +77,7 @@ describe("command-registry", () => {
expect(names).toContain("config");
expect(names).toContain("agents");
expect(names).toContain("backup");
expect(names).toContain("mcp");
expect(names).toContain("sessions");
expect(names).toContain("tasks");
expect(names).not.toContain("agent");

View File

@@ -1,10 +1,16 @@
import type { Command } from "commander";
import { resolveCliArgvInvocation } from "../argv-invocation.js";
import { shouldRegisterPrimaryCommandOnly } from "../command-registration-policy.js";
import {
buildCommandGroupEntries,
defineImportedCommandGroupSpec,
defineImportedProgramCommandGroupSpecs,
type CommandGroupDescriptorSpec,
} from "./command-group-descriptors.js";
import type { ProgramContext } from "./context.js";
import {
type CoreCliCommandDescriptor,
getCoreCliCommandDescriptors,
getCoreCliCommandNames as getCoreDescriptorNames,
getCoreCliCommandsWithSubcommands,
} from "./core-command-descriptors.js";
import {
@@ -27,197 +33,105 @@ export type CommandRegistration = {
register: (params: CommandRegisterParams) => void;
};
type CoreCliEntry = {
commands: CoreCliCommandDescriptor[];
registerWithParams: (params: CommandRegisterParams) => Promise<void> | void;
};
// Note for humans and agents:
// If you update the list of commands, also check whether they have subcommands
// and set the flag accordingly.
const coreEntries: CoreCliEntry[] = [
{
commands: [
{
name: "setup",
description: "Initialize local config and agent workspace",
hasSubcommands: false,
},
],
registerWithParams: async ({ program }) => {
const mod = await import("./register.setup.js");
mod.registerSetupCommand(program);
},
},
{
commands: [
{
name: "onboard",
description: "Interactive onboarding for gateway, workspace, and skills",
hasSubcommands: false,
},
],
registerWithParams: async ({ program }) => {
const mod = await import("./register.onboard.js");
mod.registerOnboardCommand(program);
},
},
{
commands: [
{
name: "configure",
description:
"Interactive configuration for credentials, channels, gateway, and agent defaults",
hasSubcommands: false,
},
],
registerWithParams: async ({ program }) => {
const mod = await import("./register.configure.js");
mod.registerConfigureCommand(program);
},
},
{
commands: [
{
name: "config",
description:
"Non-interactive config helpers (get/set/unset/file/validate). Default: starts guided setup.",
hasSubcommands: true,
},
],
registerWithParams: async ({ program }) => {
const mod = await import("../config-cli.js");
mod.registerConfigCli(program);
},
},
{
commands: [
{
name: "backup",
description: "Create and verify local backup archives for OpenClaw state",
hasSubcommands: true,
},
],
registerWithParams: async ({ program }) => {
const mod = await import("./register.backup.js");
mod.registerBackupCommand(program);
},
},
{
commands: [
{
name: "doctor",
description: "Health checks + quick fixes for the gateway and channels",
hasSubcommands: false,
},
{
name: "dashboard",
description: "Open the Control UI with your current token",
hasSubcommands: false,
},
{
name: "reset",
description: "Reset local config/state (keeps the CLI installed)",
hasSubcommands: false,
},
{
name: "uninstall",
description: "Uninstall the gateway service + local data (CLI remains)",
hasSubcommands: false,
},
],
registerWithParams: async ({ program }) => {
const mod = await import("./register.maintenance.js");
mod.registerMaintenanceCommands(program);
},
},
{
commands: [
{
name: "message",
description: "Send, read, and manage messages",
hasSubcommands: true,
},
],
registerWithParams: async ({ program, ctx }) => {
const mod = await import("./register.message.js");
mod.registerMessageCommands(program, ctx);
},
},
{
commands: [
{
name: "mcp",
description: "Manage OpenClaw MCP config and channel bridge",
hasSubcommands: true,
},
],
registerWithParams: async ({ program }) => {
const mod = await import("../mcp-cli.js");
mod.registerMcpCli(program);
},
},
{
commands: [
{
name: "agent",
description: "Run one agent turn via the Gateway",
hasSubcommands: false,
},
{
name: "agents",
description: "Manage isolated agents (workspaces, auth, routing)",
hasSubcommands: true,
},
],
registerWithParams: async ({ program, ctx }) => {
const mod = await import("./register.agent.js");
mod.registerAgentCommands(program, {
agentChannelOptions: ctx.agentChannelOptions,
});
},
},
{
commands: [
{
name: "status",
description: "Show channel health and recent session recipients",
hasSubcommands: false,
},
{
name: "health",
description: "Fetch health from the running gateway",
hasSubcommands: false,
},
{
name: "sessions",
description: "List stored conversation sessions",
hasSubcommands: true,
},
{
name: "tasks",
description: "Inspect durable background task state",
hasSubcommands: true,
},
],
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 });
function withProgramOnlySpecs(
specs: readonly CommandGroupDescriptorSpec<(program: Command) => Promise<void> | void>[],
): CommandGroupDescriptorSpec<(params: CommandRegisterParams) => Promise<void>>[] {
return specs.map((spec) => ({
commandNames: spec.commandNames,
register: async ({ program }) => {
await spec.register(program);
},
}));
}
// Note for humans and agents:
// If you update the list of commands, also check whether they have subcommands
// and set the flag accordingly.
const coreEntrySpecs: readonly CommandGroupDescriptorSpec<
(params: CommandRegisterParams) => Promise<void> | void
>[] = [
...withProgramOnlySpecs(
defineImportedProgramCommandGroupSpecs([
{
commandNames: ["setup"],
loadModule: () => import("./register.setup.js"),
exportName: "registerSetupCommand",
},
{
commandNames: ["onboard"],
loadModule: () => import("./register.onboard.js"),
exportName: "registerOnboardCommand",
},
{
commandNames: ["configure"],
loadModule: () => import("./register.configure.js"),
exportName: "registerConfigureCommand",
},
{
commandNames: ["config"],
loadModule: () => import("../config-cli.js"),
exportName: "registerConfigCli",
},
{
commandNames: ["backup"],
loadModule: () => import("./register.backup.js"),
exportName: "registerBackupCommand",
},
{
commandNames: ["doctor", "dashboard", "reset", "uninstall"],
loadModule: () => import("./register.maintenance.js"),
exportName: "registerMaintenanceCommands",
},
]),
),
defineImportedCommandGroupSpec(
["message"],
() => import("./register.message.js"),
(mod, { program, ctx }) => {
mod.registerMessageCommands(program, ctx);
},
),
...withProgramOnlySpecs(
defineImportedProgramCommandGroupSpecs([
{
commandNames: ["mcp"],
loadModule: () => import("../mcp-cli.js"),
exportName: "registerMcpCli",
},
]),
),
defineImportedCommandGroupSpec(
["agent", "agents"],
() => import("./register.agent.js"),
(mod, { program, ctx }) => {
mod.registerAgentCommands(program, {
agentChannelOptions: ctx.agentChannelOptions,
});
},
),
...withProgramOnlySpecs(
defineImportedProgramCommandGroupSpecs([
{
commandNames: ["status", "health", "sessions", "tasks"],
loadModule: () => import("./register.status-health-sessions.js"),
exportName: "registerStatusHealthSessionsCommands",
},
]),
),
];
function resolveCoreCommandGroups(ctx: ProgramContext, argv: string[]): CommandGroupEntry[] {
return buildCommandGroupEntries(
getCoreCliCommandDescriptors(),
coreEntrySpecs,
(register) => async (program) => {
await register({ program, ctx, argv });
},
);
}
export function getCoreCliCommandNames(): string[] {
return getCoreCliCommandDescriptors().map((command) => command.name);
return getCoreDescriptorNames();
}
export async function registerCoreCliByName(

View File

@@ -1,10 +1,9 @@
export type CoreCliCommandDescriptor = {
name: string;
description: string;
hasSubcommands: boolean;
};
import { defineCommandDescriptorCatalog } from "./command-descriptor-utils.js";
import type { NamedCommandDescriptor } from "./command-group-descriptors.js";
export const CORE_CLI_COMMAND_DESCRIPTORS = [
export type CoreCliCommandDescriptor = NamedCommandDescriptor;
const coreCliCommandCatalog = defineCommandDescriptorCatalog([
{
name: "setup",
description: "Initialize local config and agent workspace",
@@ -56,6 +55,11 @@ export const CORE_CLI_COMMAND_DESCRIPTORS = [
description: "Send, read, and manage messages",
hasSubcommands: true,
},
{
name: "mcp",
description: "Manage OpenClaw MCP config and channel bridge",
hasSubcommands: true,
},
{
name: "agent",
description: "Run one agent turn via the Gateway",
@@ -86,14 +90,18 @@ export const CORE_CLI_COMMAND_DESCRIPTORS = [
description: "Inspect durable background task state",
hasSubcommands: true,
},
] as const satisfies ReadonlyArray<CoreCliCommandDescriptor>;
] as const satisfies ReadonlyArray<CoreCliCommandDescriptor>);
export const CORE_CLI_COMMAND_DESCRIPTORS = coreCliCommandCatalog.descriptors;
export function getCoreCliCommandDescriptors(): ReadonlyArray<CoreCliCommandDescriptor> {
return CORE_CLI_COMMAND_DESCRIPTORS;
return coreCliCommandCatalog.getDescriptors();
}
export function getCoreCliCommandNames(): string[] {
return coreCliCommandCatalog.getNames();
}
export function getCoreCliCommandsWithSubcommands(): string[] {
return CORE_CLI_COMMAND_DESCRIPTORS.filter((command) => command.hasSubcommands).map(
(command) => command.name,
);
return coreCliCommandCatalog.getCommandsWithSubcommands();
}

View File

@@ -9,16 +9,25 @@ export type CommandGroupPlaceholder = {
export type CommandGroupEntry = {
placeholders: readonly CommandGroupPlaceholder[];
names?: readonly string[];
register: (program: Command) => Promise<void> | void;
};
function findCommandGroupEntry(
export function getCommandGroupNames(entry: CommandGroupEntry): readonly string[] {
return entry.names ?? entry.placeholders.map((placeholder) => placeholder.name);
}
export function findCommandGroupEntry(
entries: readonly CommandGroupEntry[],
name: string,
): CommandGroupEntry | undefined {
return entries.find((entry) =>
entry.placeholders.some((placeholder) => placeholder.name === name),
);
return entries.find((entry) => getCommandGroupNames(entry).includes(name));
}
export function removeCommandGroupNames(program: Command, entry: CommandGroupEntry) {
for (const name of new Set(getCommandGroupNames(entry))) {
removeCommandByName(program, name);
}
}
export async function registerCommandGroupByName(
@@ -30,14 +39,12 @@ export async function registerCommandGroupByName(
if (!entry) {
return false;
}
for (const placeholder of entry.placeholders) {
removeCommandByName(program, placeholder.name);
}
removeCommandGroupNames(program, entry);
await entry.register(program);
return true;
}
function registerLazyCommandGroup(
export function registerLazyCommandGroup(
program: Command,
entry: CommandGroupEntry,
placeholder: CommandGroupPlaceholder,
@@ -46,7 +53,7 @@ function registerLazyCommandGroup(
program,
name: placeholder.name,
description: placeholder.description,
removeNames: entry.placeholders.map((candidate) => candidate.name),
removeNames: [...new Set(getCommandGroupNames(entry))],
register: async () => {
await entry.register(program);
},

View File

@@ -3,6 +3,10 @@ import type { OpenClawConfig } from "../../config/config.js";
import { getPluginCliCommandDescriptors } from "../../plugins/cli.js";
import type { PluginLoadOptions } from "../../plugins/loader.js";
import { VERSION } from "../../version.js";
import {
addCommandDescriptorsToProgram,
collectUniqueCommandDescriptors,
} from "./command-descriptor-utils.js";
import { getCoreCliCommandDescriptors } from "./core-command-descriptors.js";
import { configureProgramHelp } from "./help.js";
import { getSubCliEntries } from "./subcli-descriptors.js";
@@ -21,29 +25,16 @@ async function buildRootHelpProgram(renderOptions?: RootHelpRenderOptions): Prom
agentChannelOptions: "",
});
const existingCommands = new Set<string>();
for (const command of getCoreCliCommandDescriptors()) {
program.command(command.name).description(command.description);
existingCommands.add(command.name);
}
for (const command of getSubCliEntries()) {
if (existingCommands.has(command.name)) {
continue;
}
program.command(command.name).description(command.description);
existingCommands.add(command.name);
}
for (const command of await getPluginCliCommandDescriptors(
renderOptions?.config,
renderOptions?.env,
{ pluginSdkResolution: renderOptions?.pluginSdkResolution },
)) {
if (existingCommands.has(command.name)) {
continue;
}
program.command(command.name).description(command.description);
existingCommands.add(command.name);
}
addCommandDescriptorsToProgram(
program,
collectUniqueCommandDescriptors([
getCoreCliCommandDescriptors(),
getSubCliEntries(),
await getPluginCliCommandDescriptors(renderOptions?.config, renderOptions?.env, {
pluginSdkResolution: renderOptions?.pluginSdkResolution,
}),
]),
);
return program;
}

View File

@@ -1,10 +1,9 @@
export type SubCliDescriptor = {
name: string;
description: string;
hasSubcommands: boolean;
};
import { defineCommandDescriptorCatalog } from "./command-descriptor-utils.js";
import type { NamedCommandDescriptor } from "./command-group-descriptors.js";
export const SUB_CLI_DESCRIPTORS = [
export type SubCliDescriptor = NamedCommandDescriptor;
const subCliCommandCatalog = defineCommandDescriptorCatalog([
{ name: "acp", description: "Agent Control Protocol tools", hasSubcommands: true },
{
name: "gateway",
@@ -138,12 +137,14 @@ export const SUB_CLI_DESCRIPTORS = [
description: "Generate shell completion script",
hasSubcommands: false,
},
] as const satisfies ReadonlyArray<SubCliDescriptor>;
] as const satisfies ReadonlyArray<SubCliDescriptor>);
export const SUB_CLI_DESCRIPTORS = subCliCommandCatalog.descriptors;
export function getSubCliEntries(): ReadonlyArray<SubCliDescriptor> {
return SUB_CLI_DESCRIPTORS;
return subCliCommandCatalog.getDescriptors();
}
export function getSubCliCommandsWithSubcommands(): string[] {
return SUB_CLI_DESCRIPTORS.filter((entry) => entry.hasSubcommands).map((entry) => entry.name);
return subCliCommandCatalog.getCommandsWithSubcommands();
}