From c44b773a86ecfe5617baeb9bbb7bb2b50e87388e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=A2=A8?= Date: Thu, 7 May 2026 14:35:53 +0000 Subject: [PATCH] refactor(cli): auto-generate skill doc from command registry (#71) --- packages/cli-workflow/src/cli-dispatch.ts | 367 ++++++++++++++-------- packages/cli-workflow/src/cmd-help.ts | 58 +--- 2 files changed, 258 insertions(+), 167 deletions(-) diff --git a/packages/cli-workflow/src/cli-dispatch.ts b/packages/cli-workflow/src/cli-dispatch.ts index ea186e2..7a0c9fe 100644 --- a/packages/cli-workflow/src/cli-dispatch.ts +++ b/packages/cli-workflow/src/cli-dispatch.ts @@ -21,84 +21,49 @@ import { cmdThreads } from "./cmd-threads.js"; import { parseLiveArgv } from "./live-argv.js"; import { parseRunArgv } from "./run-argv.js"; -export function formatCliUsage(): string { - return [ - "Usage:", - " uncaged-workflow workflow add [--types ]", - " uncaged-workflow workflow list", - " uncaged-workflow workflow show ", - " uncaged-workflow workflow rm ", - " uncaged-workflow workflow history ", - " uncaged-workflow workflow rollback [hash]", - "", - " uncaged-workflow thread run [--prompt ] [--max-rounds N]", - " uncaged-workflow thread list [name]", - " uncaged-workflow thread show ", - " uncaged-workflow thread rm ", - " uncaged-workflow thread ps", - " uncaged-workflow thread kill ", - " uncaged-workflow thread live [--debug] [--role ]", - " uncaged-workflow thread live --latest [--debug] [--role ]", - " uncaged-workflow thread pause ", - " uncaged-workflow thread resume ", - " uncaged-workflow thread fork [--from-role ]", - "", - " uncaged-workflow cas get ", - " uncaged-workflow cas put ", - " uncaged-workflow cas list ", - " uncaged-workflow cas rm ", - " uncaged-workflow cas gc", - "", - " uncaged-workflow init workspace ", - " uncaged-workflow init template ", - "", - " uncaged-workflow run [...] (shortcut for thread run)", - " uncaged-workflow live [...] (shortcut for thread live)", - "", - "Environment variables:", - " WORKFLOW_STORAGE_ROOT Override storage directory (default: ~/.uncaged/workflow)", - " UNCAGED_WORKFLOW_STORAGE_ROOT Internal override (takes priority over WORKFLOW_STORAGE_ROOT)", - ].join("\n"); -} - -function printDeprecation(oldCmd: string, newCmd: string): void { - printCliWarn(`⚠ "${oldCmd}" is deprecated, use "${newCmd}" instead`); -} - type DispatchFn = (storageRoot: string, argv: string[]) => Promise; +type CommandEntry = { + handler: DispatchFn; + args: string; + description: string; +}; + +type CommandGroup = { + name: string; + commands: ReadonlyArray<{ name: string; args: string; description: string }>; +}; + // ── Individual dispatch functions ────────────────────────────────────── -async function dispatchInit(_storageRoot: string, argv: string[]): Promise { - const sub = argv[0]; - const name = argv[1]; - if (sub === undefined || name === undefined || argv.length > 2) { - printCliError(`${formatCliUsage()}\n\nerror: init requires workspace|template `); +async function dispatchInitWorkspace(_storageRoot: string, argv: string[]): Promise { + const name = argv[0]; + if (name === undefined || argv.length > 1) { + printCliError(`${formatCliUsage()}\n\nerror: init workspace requires `); return 1; } - - if (sub === "workspace") { - const result = await cmdInitWorkspace(process.cwd(), name); - if (!result.ok) { - printCliError(result.error); - return 1; - } - printCliLine(`initialized workflow workspace at ${result.value.rootPath}`); - return 0; + const result = await cmdInitWorkspace(process.cwd(), name); + if (!result.ok) { + printCliError(result.error); + return 1; } + printCliLine(`initialized workflow workspace at ${result.value.rootPath}`); + return 0; +} - if (sub === "template") { - const result = await cmdInitTemplate(process.cwd(), name); - if (!result.ok) { - printCliError(result.error); - return 1; - } - printCliLine(`initialized template at ${result.value.templatePath}`); - return 0; +async function dispatchInitTemplate(_storageRoot: string, argv: string[]): Promise { + const name = argv[0]; + if (name === undefined || argv.length > 1) { + printCliError(`${formatCliUsage()}\n\nerror: init template requires `); + return 1; } - - printCliError(`${formatCliUsage()}\n\nerror: unknown init subcommand: ${sub}`); - return 1; + const result = await cmdInitTemplate(process.cwd(), name); + if (!result.ok) { + printCliError(result.error); + return 1; + } + printCliLine(`initialized template at ${result.value.templatePath}`); + return 0; } async function dispatchAdd(storageRoot: string, argv: string[]): Promise { @@ -426,49 +391,210 @@ async function dispatchCasRm(storageRoot: string, rest: string[]): Promise = { - get: dispatchCasGet, - put: dispatchCasPut, - list: dispatchCasList, - rm: dispatchCasRm, - gc: dispatchGc, +// ── Subcommand tables with metadata ──────────────────────────────────── + +const WORKFLOW_SUBCOMMAND_TABLE: Record = { + add: { + handler: dispatchAdd, + args: " [--types ]", + description: "Register a workflow bundle in the registry", + }, + list: { handler: dispatchList, args: "", description: "List all registered workflows" }, + show: { + handler: dispatchShow, + args: "", + description: "Show details of a registered workflow", + }, + rm: { + handler: dispatchRemove, + args: "", + description: "Remove a workflow from the registry", + }, + history: { + handler: dispatchHistory, + args: "", + description: "Show version history of a workflow", + }, + rollback: { + handler: dispatchRollback, + args: " [hash]", + description: "Rollback a workflow to a previous version", + }, }; -async function dispatchCas(storageRoot: string, argv: string[]): Promise { - const sub = argv[0]; - if (sub === undefined) { - printCliError(`${formatCliUsage()}\n\nerror: unknown cas subcommand: (none)`); - return 1; - } - const handler = CAS_SUBCOMMAND_TABLE[sub]; - if (handler === undefined) { - printCliError(`${formatCliUsage()}\n\nerror: unknown cas subcommand: ${sub}`); - return 1; - } - return handler(storageRoot, argv.slice(1)); +const THREAD_SUBCOMMAND_TABLE: Record = { + run: { + handler: dispatchRun, + args: " [--prompt ] [--max-rounds N]", + description: "Start a new thread executing a workflow", + }, + list: { + handler: dispatchThreadList, + args: "[name]", + description: "List threads, optionally filtered by workflow name", + }, + show: { handler: dispatchThreadShow, args: "", description: "Show thread details and state" }, + rm: { handler: dispatchThreadRm, args: "", description: "Remove a thread" }, + fork: { + handler: dispatchFork, + args: " [--from-role ]", + description: "Fork a thread, optionally from a specific role", + }, + ps: { handler: dispatchPs, args: "", description: "List running threads" }, + kill: { handler: dispatchKill, args: "", description: "Kill a running thread" }, + live: { + handler: dispatchLive, + args: " [--debug] [--role ]", + description: "Attach to a thread and stream output live", + }, + pause: { handler: dispatchPause, args: "", description: "Pause a running thread" }, + resume: { handler: dispatchResume, args: "", description: "Resume a paused thread" }, +}; + +const CAS_SUBCOMMAND_TABLE: Record = { + get: { + handler: dispatchCasGet, + args: " ", + description: "Retrieve content by hash from a thread's CAS", + }, + put: { + handler: dispatchCasPut, + args: " ", + description: "Store content in a thread's CAS, returns hash", + }, + list: { + handler: dispatchCasList, + args: "", + description: "List all CAS entries for a thread", + }, + rm: { handler: dispatchCasRm, args: " ", description: "Remove a CAS entry" }, + gc: { handler: dispatchGc, args: "", description: "Garbage-collect unreferenced CAS entries" }, +}; + +const INIT_SUBCOMMAND_TABLE: Record = { + workspace: { + handler: dispatchInitWorkspace, + args: "", + description: "Initialize a new workflow workspace", + }, + template: { + handler: dispatchInitTemplate, + args: "", + description: "Initialize a new workflow template", + }, +}; + +// ── Command registry ─────────────────────────────────────────────────── + +export function getCommandRegistry(): ReadonlyArray { + return [ + { + name: "workflow", + commands: Object.entries(WORKFLOW_SUBCOMMAND_TABLE).map(([name, e]) => ({ + name, + args: e.args, + description: e.description, + })), + }, + { + name: "thread", + commands: Object.entries(THREAD_SUBCOMMAND_TABLE).map(([name, e]) => ({ + name, + args: e.args, + description: e.description, + })), + }, + { + name: "cas", + commands: Object.entries(CAS_SUBCOMMAND_TABLE).map(([name, e]) => ({ + name, + args: e.args, + description: e.description, + })), + }, + { + name: "init", + commands: Object.entries(INIT_SUBCOMMAND_TABLE).map(([name, e]) => ({ + name, + args: e.args, + description: e.description, + })), + }, + ]; } -// ── Workflow subcommand table (Phase 1) ──────────────────────────────── +// ── Auto-generated CLI usage ─────────────────────────────────────────── -const WORKFLOW_SUBCOMMAND_TABLE: Record = { - add: dispatchAdd, - list: dispatchList, - show: dispatchShow, - rm: dispatchRemove, - history: dispatchHistory, - rollback: dispatchRollback, -}; +export function formatCliUsage(): string { + const groups = getCommandRegistry(); + const lines: string[] = ["Usage:"]; + for (const group of groups) { + for (const cmd of group.commands) { + const args = cmd.args ? ` ${cmd.args}` : ""; + lines.push(` uncaged-workflow ${group.name} ${cmd.name}${args}`); + } + lines.push(""); + } + lines.push(" uncaged-workflow run [...] (shortcut for thread run)"); + lines.push(" uncaged-workflow live [...] (shortcut for thread live)"); + lines.push(""); + lines.push("Environment variables:"); + lines.push( + " WORKFLOW_STORAGE_ROOT Override storage directory (default: ~/.uncaged/workflow)", + ); + lines.push( + " UNCAGED_WORKFLOW_STORAGE_ROOT Internal override (takes priority over WORKFLOW_STORAGE_ROOT)", + ); + return lines.join("\n"); +} -async function dispatchWorkflow(storageRoot: string, argv: string[]): Promise { +function printDeprecation(oldCmd: string, newCmd: string): void { + printCliWarn(`⚠ "${oldCmd}" is deprecated, use "${newCmd}" instead`); +} + +// ── Group dispatchers ────────────────────────────────────────────────── + +function dispatchGroup( + tableName: string, + table: Record, + storageRoot: string, + argv: string[], +): Promise | null { const sub = argv[0]; if (sub === undefined) { - printCliError(`${formatCliUsage()}\n\nerror: unknown workflow subcommand: (none)`); + printCliError(`${formatCliUsage()}\n\nerror: unknown ${tableName} subcommand: (none)`); + return Promise.resolve(1); + } + const entry = table[sub]; + if (entry === undefined) { + return null; + } + return entry.handler(storageRoot, argv.slice(1)); +} + +async function dispatchInit(storageRoot: string, argv: string[]): Promise { + const sub = argv[0]; + const name = argv[1]; + if (sub === undefined || name === undefined || argv.length > 2) { + printCliError(`${formatCliUsage()}\n\nerror: init requires workspace|template `); return 1; } - const handler = WORKFLOW_SUBCOMMAND_TABLE[sub]; - if (handler !== undefined) { - return handler(storageRoot, argv.slice(1)); + + const entry = INIT_SUBCOMMAND_TABLE[sub]; + if (entry !== undefined) { + return entry.handler(storageRoot, argv.slice(1)); } + + printCliError(`${formatCliUsage()}\n\nerror: unknown init subcommand: ${sub}`); + return 1; +} + +async function dispatchWorkflow(storageRoot: string, argv: string[]): Promise { + const result = dispatchGroup("workflow", WORKFLOW_SUBCOMMAND_TABLE, storageRoot, argv); + if (result !== null) { + return result; + } + const sub = argv[0]; if (sub === "remove") { printDeprecation("workflow remove", "workflow rm"); return dispatchRemove(storageRoot, argv.slice(1)); @@ -477,33 +603,24 @@ async function dispatchWorkflow(storageRoot: string, argv: string[]): Promise = { - run: dispatchRun, - list: dispatchThreadList, - show: dispatchThreadShow, - rm: dispatchThreadRm, - fork: dispatchFork, - ps: dispatchPs, - kill: dispatchKill, - live: dispatchLive, - pause: dispatchPause, - resume: dispatchResume, -}; - async function dispatchThread(storageRoot: string, argv: string[]): Promise { + const result = dispatchGroup("thread", THREAD_SUBCOMMAND_TABLE, storageRoot, argv); + if (result !== null) { + return result; + } const sub = argv[0]; - if (sub === undefined) { - printCliError(`${formatCliUsage()}\n\nerror: unknown thread subcommand: (none)`); - return 1; + printCliError(`${formatCliUsage()}\n\nerror: unknown thread subcommand: ${sub}`); + return 1; +} + +async function dispatchCas(storageRoot: string, argv: string[]): Promise { + const result = dispatchGroup("cas", CAS_SUBCOMMAND_TABLE, storageRoot, argv); + if (result !== null) { + return result; } - const handler = THREAD_SUBCOMMAND_TABLE[sub]; - if (handler === undefined) { - printCliError(`${formatCliUsage()}\n\nerror: unknown thread subcommand: ${sub}`); - return 1; - } - return handler(storageRoot, argv.slice(1)); + const sub = argv[0]; + printCliError(`${formatCliUsage()}\n\nerror: unknown cas subcommand: ${sub}`); + return 1; } // ── Help ──────────────────────────────────────────────────────────────── diff --git a/packages/cli-workflow/src/cmd-help.ts b/packages/cli-workflow/src/cmd-help.ts index db85e4f..00a0806 100644 --- a/packages/cli-workflow/src/cmd-help.ts +++ b/packages/cli-workflow/src/cmd-help.ts @@ -1,4 +1,19 @@ +import { getCommandRegistry } from "./cli-dispatch.js"; + export function formatSkillDoc(): string { + const groups = getCommandRegistry(); + + const commandSections: string[] = []; + for (const group of groups) { + const rows = group.commands.map((cmd) => { + const args = cmd.args ? `\`${cmd.args}\`` : "(none)"; + return `| \`${group.name} ${cmd.name}\` | ${args} | ${cmd.description} |`; + }); + commandSections.push( + `### ${group.name}\n\n| Command | Args | Description |\n|---------|------|-------------|\n${rows.join("\n")}`, + ); + } + return `# uncaged-workflow CLI Reference ## Core Concepts @@ -13,48 +28,7 @@ export function formatSkillDoc(): string { ## Commands -### workflow - -| Command | Args | Description | -|---------|------|-------------| -| \`workflow add\` | \` [--types ]\` | Register a workflow bundle in the registry | -| \`workflow list\` | (none) | List all registered workflows | -| \`workflow show\` | \`\` | Show details of a registered workflow | -| \`workflow rm\` | \`\` | Remove a workflow from the registry | -| \`workflow history\` | \`\` | Show version history of a workflow | -| \`workflow rollback\` | \` [hash]\` | Rollback a workflow to a previous version | - -### thread - -| Command | Args | Description | -|---------|------|-------------| -| \`thread run\` | \` [--prompt ] [--max-rounds N]\` | Start a new thread executing a workflow | -| \`thread list\` | \`[name]\` | List threads, optionally filtered by workflow name | -| \`thread show\` | \`\` | Show thread details and state | -| \`thread rm\` | \`\` | Remove a thread | -| \`thread fork\` | \` [--from-role ]\` | Fork a thread, optionally from a specific role | -| \`thread ps\` | (none) | List running threads | -| \`thread kill\` | \`\` | Kill a running thread | -| \`thread live\` | \` [--debug] [--role ]\` or \`--latest [--debug] [--role ]\` | Attach to a thread and stream output live | -| \`thread pause\` | \`\` | Pause a running thread | -| \`thread resume\` | \`\` | Resume a paused thread | - -### cas - -| Command | Args | Description | -|---------|------|-------------| -| \`cas get\` | \` \` | Retrieve content by hash from a thread's CAS | -| \`cas put\` | \` \` | Store content in a thread's CAS, returns hash | -| \`cas list\` | \`\` | List all CAS entries for a thread | -| \`cas rm\` | \` \` | Remove a CAS entry | -| \`cas gc\` | (none) | Garbage-collect unreferenced CAS entries | - -### init - -| Command | Args | Description | -|---------|------|-------------| -| \`init workspace\` | \`\` | Initialize a new workflow workspace | -| \`init template\` | \`\` | Initialize a new workflow template | +${commandSections.join("\n\n")} ### Top-level shortcuts