diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e56405b1..841bae53f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.clawd.bot - Compaction: include tool failure summaries in safeguard compaction to prevent retry loops. (#1084) - TUI: show generic empty-state text for searchable pickers. (#1201) — thanks @vignesh07. - Doctor: canonicalize legacy session keys in session stores to prevent stale metadata. (#1169) +- CLI: centralize CLI command registration to keep fast-path routing and program wiring in sync. (#1207) — thanks @gumadeiras. ## 2026.1.18-5 diff --git a/src/cli/program.smoke.test.ts b/src/cli/program.smoke.test.ts index 107678bb9..10ebc9188 100644 --- a/src/cli/program.smoke.test.ts +++ b/src/cli/program.smoke.test.ts @@ -73,6 +73,12 @@ describe("cli program (smoke)", () => { expect(statusCommand).toHaveBeenCalled(); }); + it("registers memory command", () => { + const program = buildProgram(); + const names = program.commands.map((command) => command.name()); + expect(names).toContain("memory"); + }); + it("runs tui without overriding timeout", async () => { const program = buildProgram(); await program.parseAsync(["tui"], { from: "user" }); diff --git a/src/cli/program/build-program.ts b/src/cli/program/build-program.ts index ed40cafbd..917ee53f3 100644 --- a/src/cli/program/build-program.ts +++ b/src/cli/program/build-program.ts @@ -1,17 +1,8 @@ import { Command } from "commander"; -import { registerBrowserCli } from "../browser-cli.js"; -import { registerConfigCli } from "../config-cli.js"; import { createProgramContext } from "./context.js"; +import { registerProgramCommands } from "./command-registry.js"; import { configureProgramHelp } from "./help.js"; import { registerPreActionHooks } from "./preaction.js"; -import { registerAgentCommands } from "./register.agent.js"; -import { registerConfigureCommand } from "./register.configure.js"; -import { registerMaintenanceCommands } from "./register.maintenance.js"; -import { registerMessageCommands } from "./register.message.js"; -import { registerOnboardCommand } from "./register.onboard.js"; -import { registerSetupCommand } from "./register.setup.js"; -import { registerStatusHealthSessionsCommands } from "./register.status-health-sessions.js"; -import { registerSubCliCommands } from "./register.subclis.js"; export function buildProgram() { const program = new Command(); @@ -21,18 +12,7 @@ export function buildProgram() { configureProgramHelp(program, ctx); registerPreActionHooks(program, ctx.programVersion); - registerSetupCommand(program); - registerOnboardCommand(program); - registerConfigureCommand(program); - registerConfigCli(program); - registerMaintenanceCommands(program); - registerMessageCommands(program, ctx); - registerAgentCommands(program, { - agentChannelOptions: ctx.agentChannelOptions, - }); - registerSubCliCommands(program, argv); - registerStatusHealthSessionsCommands(program); - registerBrowserCli(program); + registerProgramCommands(program, ctx, argv); return program; } diff --git a/src/cli/program/command-registry.ts b/src/cli/program/command-registry.ts new file mode 100644 index 000000000..861411b64 --- /dev/null +++ b/src/cli/program/command-registry.ts @@ -0,0 +1,185 @@ +import type { Command } from "commander"; + +import { agentsListCommand } from "../../commands/agents.js"; +import { healthCommand } from "../../commands/health.js"; +import { sessionsCommand } from "../../commands/sessions.js"; +import { statusCommand } from "../../commands/status.js"; +import { setVerbose } from "../../globals.js"; +import { defaultRuntime } from "../../runtime.js"; +import { + getFlagValue, + getPositiveIntFlagValue, + getVerboseFlag, + hasFlag, +} from "../argv.js"; +import { registerBrowserCli } from "../browser-cli.js"; +import { registerConfigCli } from "../config-cli.js"; +import { registerMemoryCli, runMemoryStatus } from "../memory-cli.js"; +import { registerAgentCommands } from "./register.agent.js"; +import { registerConfigureCommand } from "./register.configure.js"; +import { registerMaintenanceCommands } from "./register.maintenance.js"; +import { registerMessageCommands } from "./register.message.js"; +import { registerOnboardCommand } from "./register.onboard.js"; +import { registerSetupCommand } from "./register.setup.js"; +import { registerStatusHealthSessionsCommands } from "./register.status-health-sessions.js"; +import { registerSubCliCommands } from "./register.subclis.js"; +import type { ProgramContext } from "./context.js"; + +type CommandRegisterParams = { + program: Command; + ctx: ProgramContext; + argv: string[]; +}; + +type RouteSpec = { + match: (path: string[]) => boolean; + loadPlugins?: boolean; + run: (argv: string[]) => Promise; +}; + +export type CommandRegistration = { + id: string; + register: (params: CommandRegisterParams) => void; + routes?: RouteSpec[]; +}; + +const routeHealth: RouteSpec = { + match: (path) => path[0] === "health", + loadPlugins: true, + run: async (argv) => { + const json = hasFlag(argv, "--json"); + const verbose = getVerboseFlag(argv, { includeDebug: true }); + const timeoutMs = getPositiveIntFlagValue(argv, "--timeout"); + if (timeoutMs === null) return false; + setVerbose(verbose); + await healthCommand({ json, timeoutMs, verbose }, defaultRuntime); + return true; + }, +}; + +const routeStatus: RouteSpec = { + match: (path) => path[0] === "status", + loadPlugins: true, + run: async (argv) => { + const json = hasFlag(argv, "--json"); + const deep = hasFlag(argv, "--deep"); + const all = hasFlag(argv, "--all"); + const usage = hasFlag(argv, "--usage"); + const verbose = getVerboseFlag(argv, { includeDebug: true }); + const timeoutMs = getPositiveIntFlagValue(argv, "--timeout"); + if (timeoutMs === null) return false; + setVerbose(verbose); + await statusCommand({ json, deep, all, usage, timeoutMs, verbose }, defaultRuntime); + return true; + }, +}; + +const routeSessions: RouteSpec = { + match: (path) => path[0] === "sessions", + run: async (argv) => { + const json = hasFlag(argv, "--json"); + const verbose = getVerboseFlag(argv); + const store = getFlagValue(argv, "--store"); + if (store === null) return false; + const active = getFlagValue(argv, "--active"); + if (active === null) return false; + setVerbose(verbose); + await sessionsCommand({ json, store, active }, defaultRuntime); + return true; + }, +}; + +const routeAgentsList: RouteSpec = { + match: (path) => path[0] === "agents" && path[1] === "list", + run: async (argv) => { + const json = hasFlag(argv, "--json"); + const bindings = hasFlag(argv, "--bindings"); + await agentsListCommand({ json, bindings }, defaultRuntime); + return true; + }, +}; + +const routeMemoryStatus: RouteSpec = { + match: (path) => path[0] === "memory" && path[1] === "status", + run: async (argv) => { + const agent = getFlagValue(argv, "--agent"); + if (agent === null) return false; + const json = hasFlag(argv, "--json"); + const deep = hasFlag(argv, "--deep"); + const index = hasFlag(argv, "--index"); + const verbose = hasFlag(argv, "--verbose"); + await runMemoryStatus({ agent, json, deep, index, verbose }); + return true; + }, +}; + +export const commandRegistry: CommandRegistration[] = [ + { + id: "setup", + register: ({ program }) => registerSetupCommand(program), + }, + { + id: "onboard", + register: ({ program }) => registerOnboardCommand(program), + }, + { + id: "configure", + register: ({ program }) => registerConfigureCommand(program), + }, + { + id: "config", + register: ({ program }) => registerConfigCli(program), + }, + { + id: "maintenance", + register: ({ program }) => registerMaintenanceCommands(program), + }, + { + id: "message", + register: ({ program, ctx }) => registerMessageCommands(program, ctx), + }, + { + id: "memory", + register: ({ program }) => registerMemoryCli(program), + routes: [routeMemoryStatus], + }, + { + id: "agent", + register: ({ program, ctx }) => + registerAgentCommands(program, { agentChannelOptions: ctx.agentChannelOptions }), + routes: [routeAgentsList], + }, + { + id: "subclis", + register: ({ program, argv }) => registerSubCliCommands(program, argv), + }, + { + id: "status-health-sessions", + register: ({ program }) => registerStatusHealthSessionsCommands(program), + routes: [routeHealth, routeStatus, routeSessions], + }, + { + id: "browser", + register: ({ program }) => registerBrowserCli(program), + }, +]; + +export function registerProgramCommands( + program: Command, + ctx: ProgramContext, + argv: string[] = process.argv, +) { + for (const entry of commandRegistry) { + entry.register({ program, ctx, argv }); + } +} + +export function findRoutedCommand(path: string[]): RouteSpec | null { + for (const entry of commandRegistry) { + if (!entry.routes) continue; + for (const route of entry.routes) { + if (route.match(path)) return route; + } + } + return null; +} diff --git a/src/cli/route.ts b/src/cli/route.ts index 34dcbbb54..885904964 100644 --- a/src/cli/route.ts +++ b/src/cli/route.ts @@ -1,23 +1,11 @@ import { defaultRuntime } from "../runtime.js"; -import { setVerbose } from "../globals.js"; -import { healthCommand } from "../commands/health.js"; -import { statusCommand } from "../commands/status.js"; -import { sessionsCommand } from "../commands/sessions.js"; -import { agentsListCommand } from "../commands/agents.js"; import { ensurePluginRegistryLoaded } from "./plugin-registry.js"; import { isTruthyEnvValue } from "../infra/env.js"; import { emitCliBanner } from "./banner.js"; import { VERSION } from "../version.js"; -import { - getCommandPath, - getFlagValue, - getPositiveIntFlagValue, - getVerboseFlag, - hasFlag, - hasHelpOrVersion, -} from "./argv.js"; +import { getCommandPath, hasHelpOrVersion } from "./argv.js"; import { ensureConfigReady } from "./program/config-guard.js"; -import { runMemoryStatus } from "./memory-cli.js"; +import { findRoutedCommand } from "./program/command-registry.js"; async function prepareRoutedCommand(params: { argv: string[]; @@ -36,65 +24,9 @@ export async function tryRouteCli(argv: string[]): Promise { if (hasHelpOrVersion(argv)) return false; const path = getCommandPath(argv, 2); - const [primary, secondary] = path; - if (!primary) return false; - if (primary === "health") { - await prepareRoutedCommand({ argv, commandPath: path, loadPlugins: true }); - const json = hasFlag(argv, "--json"); - const verbose = getVerboseFlag(argv, { includeDebug: true }); - const timeoutMs = getPositiveIntFlagValue(argv, "--timeout"); - if (timeoutMs === null) return false; - setVerbose(verbose); - await healthCommand({ json, timeoutMs, verbose }, defaultRuntime); - return true; - } - - if (primary === "status") { - await prepareRoutedCommand({ argv, commandPath: path, loadPlugins: true }); - const json = hasFlag(argv, "--json"); - const deep = hasFlag(argv, "--deep"); - const all = hasFlag(argv, "--all"); - const usage = hasFlag(argv, "--usage"); - const verbose = getVerboseFlag(argv, { includeDebug: true }); - const timeoutMs = getPositiveIntFlagValue(argv, "--timeout"); - if (timeoutMs === null) return false; - setVerbose(verbose); - await statusCommand({ json, deep, all, usage, timeoutMs, verbose }, defaultRuntime); - return true; - } - - if (primary === "sessions") { - await prepareRoutedCommand({ argv, commandPath: path }); - const json = hasFlag(argv, "--json"); - const verbose = getVerboseFlag(argv); - const store = getFlagValue(argv, "--store"); - if (store === null) return false; - const active = getFlagValue(argv, "--active"); - if (active === null) return false; - setVerbose(verbose); - await sessionsCommand({ json, store, active }, defaultRuntime); - return true; - } - - if (primary === "agents" && secondary === "list") { - await prepareRoutedCommand({ argv, commandPath: path }); - const json = hasFlag(argv, "--json"); - const bindings = hasFlag(argv, "--bindings"); - await agentsListCommand({ json, bindings }, defaultRuntime); - return true; - } - - if (primary === "memory" && secondary === "status") { - await prepareRoutedCommand({ argv, commandPath: path }); - const agent = getFlagValue(argv, "--agent"); - if (agent === null) return false; - const json = hasFlag(argv, "--json"); - const deep = hasFlag(argv, "--deep"); - const index = hasFlag(argv, "--index"); - const verbose = hasFlag(argv, "--verbose"); - await runMemoryStatus({ agent, json, deep, index, verbose }); - return true; - } - - return false; + if (!path[0]) return false; + const route = findRoutedCommand(path); + if (!route) return false; + await prepareRoutedCommand({ argv, commandPath: path, loadPlugins: route.loadPlugins }); + return route.run(argv); }