mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-14 02:31:24 +00:00
refactor: share cli command descriptor helpers
This commit is contained in:
74
src/cli/program/command-descriptor-utils.test.ts
Normal file
74
src/cli/program/command-descriptor-utils.test.ts
Normal 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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
68
src/cli/program/command-descriptor-utils.ts
Normal file
68
src/cli/program/command-descriptor-utils.ts
Normal 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;
|
||||
}
|
||||
134
src/cli/program/command-group-descriptors.test.ts
Normal file
134
src/cli/program/command-group-descriptors.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
117
src/cli/program/command-group-descriptors.ts
Normal file
117
src/cli/program/command-group-descriptors.ts
Normal 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));
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user