From 6a99f84025a92301682e2cafb84496133b1d9365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E6=9C=88?= Date: Fri, 8 May 2026 09:04:27 +0800 Subject: [PATCH] refactor(cli): split cli-dispatch.ts into group dispatchers + usage module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cli-dispatch.ts: 775 → 149 lines (top-level routing only) - cli-usage.ts: usage formatting (formatCliUsage, formatUsageCommandLines) - cli-command-types.ts: shared types (DispatchFn, CommandEntry, CommandGroup) - cli-registry.ts: getCommandRegistry() assembling all group tables - cli-usage-context.ts: decouple usage from registry (avoids circular deps) - commands/{workflow,thread,cas,init}/dispatch.ts: group-specific dispatch functions + subcommand tables - 242 tests pass, CLI output identical, biome clean Refs #96 --- .../cli-workflow/src/cli-command-types.ts | 19 + packages/cli-workflow/src/cli-dispatch.ts | 708 +----------------- packages/cli-workflow/src/cli-registry.ts | 45 ++ .../cli-workflow/src/cli-usage-context.ts | 14 + packages/cli-workflow/src/cli-usage.ts | 81 ++ .../cli-workflow/src/commands/cas/dispatch.ts | 128 ++++ .../src/commands/init/dispatch.ts | 66 ++ .../src/commands/thread/dispatch.ts | 204 +++++ .../src/commands/workflow/dispatch.ts | 162 ++++ packages/cli-workflow/src/skill.ts | 2 +- 10 files changed, 761 insertions(+), 668 deletions(-) create mode 100644 packages/cli-workflow/src/cli-command-types.ts create mode 100644 packages/cli-workflow/src/cli-registry.ts create mode 100644 packages/cli-workflow/src/cli-usage-context.ts create mode 100644 packages/cli-workflow/src/cli-usage.ts create mode 100644 packages/cli-workflow/src/commands/cas/dispatch.ts create mode 100644 packages/cli-workflow/src/commands/init/dispatch.ts create mode 100644 packages/cli-workflow/src/commands/thread/dispatch.ts create mode 100644 packages/cli-workflow/src/commands/workflow/dispatch.ts diff --git a/packages/cli-workflow/src/cli-command-types.ts b/packages/cli-workflow/src/cli-command-types.ts new file mode 100644 index 0000000..f69bd61 --- /dev/null +++ b/packages/cli-workflow/src/cli-command-types.ts @@ -0,0 +1,19 @@ +export type DispatchFn = (storageRoot: string, argv: string[]) => Promise; + +export type CommandEntry = { + handler: DispatchFn; + args: string; + description: string; +}; + +export type CommandGroup = { + name: string; + commands: ReadonlyArray<{ name: string; args: string; description: string }>; +}; + +export type DispatchGroupFn = ( + tableName: string, + table: Record, + storageRoot: string, + argv: string[], +) => Promise | null; diff --git a/packages/cli-workflow/src/cli-dispatch.ts b/packages/cli-workflow/src/cli-dispatch.ts index 17e5a3f..eae2aa5 100644 --- a/packages/cli-workflow/src/cli-dispatch.ts +++ b/packages/cli-workflow/src/cli-dispatch.ts @@ -1,611 +1,33 @@ +import type { CommandEntry, DispatchFn } from "./cli-command-types.js"; import { printCliError, printCliLine, printCliWarn } from "./cli-output.js"; -import { cmdGc } from "./commands/cas/gc.js"; -import { cmdCasGet } from "./commands/cas/get.js"; -import { cmdCasList } from "./commands/cas/list.js"; -import { cmdCasPut } from "./commands/cas/put.js"; -import { cmdCasRm } from "./commands/cas/rm.js"; -import { cmdInitTemplate } from "./commands/init/template.js"; -import { cmdInitWorkspace } from "./commands/init/workspace.js"; -import { cmdKill, cmdPause, cmdResume } from "./commands/thread/control.js"; -import { cmdFork, parseForkArgv } from "./commands/thread/fork.js"; -import { cmdThreads } from "./commands/thread/list.js"; -import { cmdLive } from "./commands/thread/live.js"; -import { cmdPs } from "./commands/thread/ps.js"; -import { cmdThreadRemove } from "./commands/thread/rm.js"; -import { cmdRun } from "./commands/thread/run.js"; -import { cmdThreadShow } from "./commands/thread/show.js"; -import { cmdAdd, formatAddSuccess, parseAddArgv } from "./commands/workflow/add.js"; -import { cmdHistory } from "./commands/workflow/history.js"; -import { cmdList, formatListLines } from "./commands/workflow/list.js"; -import { cmdRemove } from "./commands/workflow/rm.js"; -import { cmdRollback } from "./commands/workflow/rollback.js"; -import { cmdShow, formatShowYaml } from "./commands/workflow/show.js"; -import { parseLiveArgv } from "./live-argv.js"; -import { parseRunArgv } from "./run-argv.js"; +import { getCommandRegistry } from "./cli-registry.js"; +import { formatCliUsage as formatCliUsageWithGroups } from "./cli-usage.js"; +import { createCasDispatcher, dispatchGc } from "./commands/cas/dispatch.js"; +import { createInitDispatcher } from "./commands/init/dispatch.js"; +import { + createThreadDispatcher, + dispatchFork, + dispatchKill, + dispatchLive, + dispatchPause, + dispatchPs, + dispatchResume, + dispatchRun, + dispatchThreadList, +} from "./commands/thread/dispatch.js"; +import { + createWorkflowDispatcher, + dispatchAdd, + dispatchHistory, + dispatchList, + dispatchRemove, + dispatchRollback, + dispatchShow, +} from "./commands/workflow/dispatch.js"; import { formatSkillIndex, formatSkillTopic, getSkillTopics } from "./skill.js"; -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 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; - } - 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; -} - -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; - } - 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 { - const parsed = parseAddArgv(argv); - if (!parsed.ok) { - printCliError(`${formatCliUsage()}\n\nerror: ${parsed.error}`); - return 1; - } - const result = await cmdAdd(storageRoot, parsed.value); - if (!result.ok) { - printCliError(result.error); - return 1; - } - for (const w of result.value.warnings) { - printCliWarn(w); - } - printCliLine(formatAddSuccess(parsed.value.name, parsed.value.filePath, result.value.hash)); - return 0; -} - -async function dispatchList(storageRoot: string, argv: string[]): Promise { - if (argv.length > 0) { - printCliError(`${formatCliUsage()}\n\nerror: list takes no arguments`); - return 1; - } - const result = await cmdList(storageRoot); - if (!result.ok) { - printCliError(result.error); - return 1; - } - for (const line of formatListLines(result.value)) { - printCliLine(line); - } - return 0; -} - -async function dispatchShow(storageRoot: string, argv: string[]): Promise { - const name = argv[0]; - if (name === undefined || argv.length > 1) { - printCliError(`${formatCliUsage()}\n\nerror: show requires `); - return 1; - } - const result = await cmdShow(storageRoot, name); - if (!result.ok) { - printCliError(result.error); - return 1; - } - printCliLine(formatShowYaml(name, result.value)); - return 0; -} - -async function dispatchRemove(storageRoot: string, argv: string[]): Promise { - const name = argv[0]; - if (name === undefined || argv.length > 1) { - printCliError(`${formatCliUsage()}\n\nerror: remove requires `); - return 1; - } - const result = await cmdRemove(storageRoot, name); - if (!result.ok) { - printCliError(result.error); - return 1; - } - printCliLine(`removed workflow "${name}" from registry`); - return 0; -} - -async function dispatchRun(storageRoot: string, argv: string[]): Promise { - const parsed = parseRunArgv(argv); - if (!parsed.ok) { - printCliError(`${formatCliUsage()}\n\nerror: ${parsed.error}`); - return 1; - } - - const result = await cmdRun( - storageRoot, - parsed.value.name, - parsed.value.prompt, - parsed.value.maxRounds, - ); - if (!result.ok) { - printCliError(result.error); - return 1; - } - - printCliLine(result.value.threadId); - return 0; -} - -async function dispatchPs(storageRoot: string, argv: string[]): Promise { - if (argv.length > 0) { - printCliError(`${formatCliUsage()}\n\nerror: ps takes no arguments`); - return 1; - } - for (const line of await cmdPs(storageRoot)) { - printCliLine(line); - } - return 0; -} - -async function dispatchKill(storageRoot: string, argv: string[]): Promise { - const threadId = argv[0]; - if (threadId === undefined || argv.length > 1) { - printCliError(`${formatCliUsage()}\n\nerror: kill requires `); - return 1; - } - const result = await cmdKill(storageRoot, threadId); - if (!result.ok) { - printCliError(result.error); - return 1; - } - printCliLine(`kill sent for thread ${threadId}`); - return 0; -} - -async function dispatchLive(storageRoot: string, argv: string[]): Promise { - const parsed = parseLiveArgv(argv); - if (!parsed.ok) { - printCliError(`${formatCliUsage()}\n\nerror: ${parsed.error}`); - return 1; - } - return cmdLive(storageRoot, parsed.value); -} - -async function dispatchHistory(storageRoot: string, argv: string[]): Promise { - const name = argv[0]; - if (name === undefined || argv.length > 1) { - printCliError(`${formatCliUsage()}\n\nerror: history requires `); - return 1; - } - const result = await cmdHistory(storageRoot, name); - if (!result.ok) { - printCliError(result.error); - return 1; - } - for (const line of result.value) { - printCliLine(line); - } - return 0; -} - -async function dispatchRollback(storageRoot: string, argv: string[]): Promise { - const name = argv[0]; - if (name === undefined || argv.length > 2) { - printCliError(`${formatCliUsage()}\n\nerror: rollback requires [hash]`); - return 1; - } - const hashArg = argv[1]; - const result = await cmdRollback(storageRoot, name, hashArg === undefined ? null : hashArg); - if (!result.ok) { - printCliError(result.error); - return 1; - } - printCliLine(`rolled back workflow "${name}"`); - return 0; -} - -async function dispatchPause(storageRoot: string, argv: string[]): Promise { - const threadId = argv[0]; - if (threadId === undefined || argv.length > 1) { - printCliError(`${formatCliUsage()}\n\nerror: pause requires `); - return 1; - } - const result = await cmdPause(storageRoot, threadId); - if (!result.ok) { - printCliError(result.error); - return 1; - } - printCliLine(`pause sent for thread ${threadId}`); - return 0; -} - -async function dispatchResume(storageRoot: string, argv: string[]): Promise { - const threadId = argv[0]; - if (threadId === undefined || argv.length > 1) { - printCliError(`${formatCliUsage()}\n\nerror: resume requires `); - return 1; - } - const result = await cmdResume(storageRoot, threadId); - if (!result.ok) { - printCliError(result.error); - return 1; - } - printCliLine(`resume sent for thread ${threadId}`); - return 0; -} - -async function dispatchThreadList(storageRoot: string, argv: string[]): Promise { - const result = await cmdThreads(storageRoot, argv); - if (!result.ok) { - printCliError(result.error); - return 1; - } - for (const line of result.value) { - printCliLine(line); - } - return 0; -} - -async function dispatchThreadShow(storageRoot: string, argv: string[]): Promise { - const id = argv[0]; - if (id === undefined || argv.length > 1) { - printCliError(`${formatCliUsage()}\n\nerror: thread show requires `); - return 1; - } - const result = await cmdThreadShow(storageRoot, id); - if (!result.ok) { - printCliError(result.error); - return 1; - } - printCliLine(result.value); - return 0; -} - -async function dispatchThreadRm(storageRoot: string, argv: string[]): Promise { - const id = argv[0]; - if (id === undefined || argv.length > 1) { - printCliError(`${formatCliUsage()}\n\nerror: thread rm requires `); - return 1; - } - const result = await cmdThreadRemove(storageRoot, id); - if (!result.ok) { - printCliError(result.error); - return 1; - } - printCliLine(`removed thread ${id}`); - return 0; -} - -async function dispatchGc(storageRoot: string, argv: string[]): Promise { - if (argv.length > 0) { - printCliError(`${formatCliUsage()}\n\nerror: gc takes no arguments`); - return 1; - } - const result = await cmdGc(storageRoot); - if (!result.ok) { - printCliError(result.error); - return 1; - } - const stats = result.value; - printCliLine( - `scanned ${stats.scannedThreads} threads, ${stats.activeRefs} active refs, deleted ${stats.deletedEntries} entries`, - ); - return 0; -} - -async function dispatchFork(storageRoot: string, argv: string[]): Promise { - const parsed = parseForkArgv(argv); - if (!parsed.ok) { - printCliError(`${formatCliUsage()}\n\nerror: ${parsed.error}`); - return 1; - } - const result = await cmdFork(storageRoot, parsed.value.threadId, parsed.value.fromRole); - if (!result.ok) { - printCliError(result.error); - return 1; - } - printCliLine(result.value.threadId); - return 0; -} - -// ── CAS subcommand table ─────────────────────────────────────────────── - -async function dispatchCasGet(storageRoot: string, rest: string[]): Promise { - const threadId = rest[0]; - const hash = rest[1]; - if (threadId === undefined || hash === undefined || rest.length > 2) { - printCliError(`${formatCliUsage()}\n\nerror: cas get requires `); - return 1; - } - const result = await cmdCasGet(storageRoot, threadId, hash); - if (!result.ok) { - printCliError(result.error); - return 1; - } - printCliLine(result.value); - return 0; -} - -async function dispatchCasPut(storageRoot: string, rest: string[]): Promise { - const threadId = rest[0]; - const content = rest[1]; - if (threadId === undefined || content === undefined || rest.length > 2) { - printCliError(`${formatCliUsage()}\n\nerror: cas put requires `); - return 1; - } - const result = await cmdCasPut(storageRoot, threadId, content); - if (!result.ok) { - printCliError(result.error); - return 1; - } - printCliLine(result.value); - return 0; -} - -async function dispatchCasList(storageRoot: string, rest: string[]): Promise { - const threadId = rest[0]; - if (threadId === undefined || rest.length > 1) { - printCliError(`${formatCliUsage()}\n\nerror: cas list requires `); - return 1; - } - const result = await cmdCasList(storageRoot, threadId); - if (!result.ok) { - printCliError(result.error); - return 1; - } - for (const hash of result.value) { - printCliLine(hash); - } - return 0; -} - -async function dispatchCasRm(storageRoot: string, rest: string[]): Promise { - const threadId = rest[0]; - const hash = rest[1]; - if (threadId === undefined || hash === undefined || rest.length > 2) { - printCliError(`${formatCliUsage()}\n\nerror: cas rm requires `); - return 1; - } - const result = await cmdCasRm(storageRoot, threadId, hash); - if (!result.ok) { - printCliError(result.error); - return 1; - } - printCliLine(`removed cas entry ${hash}`); - return 0; -} - -// ── 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", - }, -}; - -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: " | --latest [--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, - })), - }, - ]; -} - -// ── Auto-generated CLI usage ─────────────────────────────────────────── - -const USAGE_SECTION_BY_GROUP: Record = { - workflow: "Workflow registry:", - thread: "Thread execution:", - cas: "Content-addressable storage:", - init: "Development:", -}; - -function formatUsageCommandLines( - rows: ReadonlyArray<{ prefix: string; description: string }>, -): string[] { - const maxPrefix = rows.reduce((max, row) => Math.max(max, row.prefix.length), 0); - const gap = 2; - return rows.map((row) => { - const pad = " ".repeat(maxPrefix - row.prefix.length + gap); - return ` ${row.prefix}${pad}${row.description}`; - }); -} - -export function formatCliUsage(): string { - const groups = getCommandRegistry(); - const lines: string[] = ["uncaged-workflow — workflow engine CLI", ""]; - - for (const group of groups) { - const sectionTitle = USAGE_SECTION_BY_GROUP[group.name]; - if (sectionTitle === undefined) { - throw new Error(`BUG: missing usage section title for group "${group.name}"`); - } - lines.push(sectionTitle); - const rows = group.commands.map((cmd) => { - const args = cmd.args ? ` ${cmd.args}` : ""; - return { - prefix: `${group.name} ${cmd.name}${args}`, - description: cmd.description, - }; - }); - lines.push(...formatUsageCommandLines(rows)); - lines.push(""); - } - - lines.push("Shortcuts:"); - lines.push( - ...formatUsageCommandLines([ - { prefix: "run [...]", description: "→ thread run" }, - { prefix: "live [...]", description: "→ thread live" }, - ]), - ); - lines.push(""); - - lines.push("Reference:"); - const skillTopicNames = getSkillTopics() - .map((t) => t.name) - .join(", "); - lines.push( - ...formatUsageCommandLines([ - { - prefix: "skill [topic]", - description: `Agent-consumable docs (${skillTopicNames})`, - }, - ]), - ); - lines.push(""); - lines.push("Use --help for subcommand details."); - 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"); -} - -function printDeprecation(oldCmd: string, newCmd: string): void { - printCliWarn(`⚠ "${oldCmd}" is deprecated, use "${newCmd}" instead`); -} - -// ── Group dispatchers ────────────────────────────────────────────────── +export type { CommandEntry, CommandGroup, DispatchFn } from "./cli-command-types.js"; +export { getCommandRegistry } from "./cli-registry.js"; function dispatchGroup( tableName: string, @@ -632,54 +54,20 @@ function dispatchGroup( return entry.handler(storageRoot, argv.slice(1)); } -async function dispatchInit(storageRoot: string, argv: string[]): Promise { - const result = dispatchGroup("init", INIT_SUBCOMMAND_TABLE, storageRoot, argv); - if (result !== null) { - return result; - } - const sub = argv[0]; - printCliError(`${formatCliUsage()}\n\nerror: unknown init subcommand: ${sub}`); - return 1; +function printDeprecation(oldCmd: string, newCmd: string): void { + printCliWarn(`⚠ "${oldCmd}" is deprecated, use "${newCmd}" instead`); } -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)); - } - printCliError(`${formatCliUsage()}\n\nerror: unknown workflow subcommand: ${sub}`); - return 1; +export function formatCliUsage(): string { + return formatCliUsageWithGroups(getCommandRegistry(), getSkillTopics()); } -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]; - printCliError(`${formatCliUsage()}\n\nerror: unknown thread subcommand: ${sub}`); - return 1; -} +const dispatchWorkflow = createWorkflowDispatcher({ dispatchGroup, printDeprecation }); +const dispatchThread = createThreadDispatcher({ dispatchGroup }); +const dispatchCas = createCasDispatcher({ dispatchGroup }); +const dispatchInit = createInitDispatcher({ dispatchGroup }); -async function dispatchCas(storageRoot: string, argv: string[]): Promise { - const result = dispatchGroup("cas", CAS_SUBCOMMAND_TABLE, storageRoot, argv); - if (result !== null) { - return result; - } - const sub = argv[0]; - printCliError(`${formatCliUsage()}\n\nerror: unknown cas subcommand: ${sub}`); - return 1; -} - -// ── Help ──────────────────────────────────────────────────────────────── - -async function dispatchSkill(_storageRoot: string, argv: string[]): Promise { - const topic = argv[0]; +async function showSkillDocOrIndex(topic: string | undefined): Promise { if (topic === undefined) { printCliLine(formatSkillIndex()); return 0; @@ -693,44 +81,30 @@ async function dispatchSkill(_storageRoot: string, argv: string[]): Promise { + return showSkillDocOrIndex(argv[0]); +} + async function dispatchHelp(_storageRoot: string, argv: string[]): Promise { - // Legacy compat: help --skill [topic] → skill [topic] const skillIdx = argv.indexOf("--skill"); if (skillIdx !== -1) { - const topic = argv[skillIdx + 1]; - if (topic === undefined) { - printCliLine(formatSkillIndex()); - return 0; - } - const doc = formatSkillTopic(topic); - if (doc === null) { - printCliError(`unknown skill topic: ${topic}\n\n${formatSkillIndex()}`); - return 1; - } - printCliLine(doc); - return 0; + return showSkillDocOrIndex(argv[skillIdx + 1]); } printCliLine(formatCliUsage()); return 0; } -// ── Top-level command table (Phase 3) ────────────────────────────────── - const COMMAND_TABLE: Record = { - // Grouped commands (primary) workflow: dispatchWorkflow, thread: dispatchThread, cas: dispatchCas, init: dispatchInit, help: dispatchHelp, skill: dispatchSkill, - - // Top-level shortcuts (no deprecation) run: dispatchRun, live: dispatchLive, }; -// Deprecated flat commands that delegate to grouped commands const DEPRECATED_ALIASES: Record = { add: { newCmd: "workflow add", handler: dispatchAdd }, list: { newCmd: "workflow list", handler: dispatchList }, diff --git a/packages/cli-workflow/src/cli-registry.ts b/packages/cli-workflow/src/cli-registry.ts new file mode 100644 index 0000000..c16ead5 --- /dev/null +++ b/packages/cli-workflow/src/cli-registry.ts @@ -0,0 +1,45 @@ +import type { CommandGroup } from "./cli-command-types.js"; +import { setCommandGroupsForUsage } from "./cli-usage-context.js"; +import { CAS_SUBCOMMAND_TABLE } from "./commands/cas/dispatch.js"; +import { INIT_SUBCOMMAND_TABLE } from "./commands/init/dispatch.js"; +import { THREAD_SUBCOMMAND_TABLE } from "./commands/thread/dispatch.js"; +import { WORKFLOW_SUBCOMMAND_TABLE } from "./commands/workflow/dispatch.js"; + +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, + })), + }, + ]; +} + +setCommandGroupsForUsage(getCommandRegistry()); diff --git a/packages/cli-workflow/src/cli-usage-context.ts b/packages/cli-workflow/src/cli-usage-context.ts new file mode 100644 index 0000000..e748e27 --- /dev/null +++ b/packages/cli-workflow/src/cli-usage-context.ts @@ -0,0 +1,14 @@ +import type { CommandGroup } from "./cli-command-types.js"; + +let commandGroupsForUsage: ReadonlyArray | null = null; + +export function setCommandGroupsForUsage(groups: ReadonlyArray): void { + commandGroupsForUsage = groups; +} + +export function getCommandGroupsForUsage(): ReadonlyArray { + if (commandGroupsForUsage === null) { + throw new Error("BUG: command groups for usage not initialized"); + } + return commandGroupsForUsage; +} diff --git a/packages/cli-workflow/src/cli-usage.ts b/packages/cli-workflow/src/cli-usage.ts new file mode 100644 index 0000000..0834800 --- /dev/null +++ b/packages/cli-workflow/src/cli-usage.ts @@ -0,0 +1,81 @@ +import type { CommandGroup } from "./cli-command-types.js"; + +/** Keep aligned with `getSkillTopics()` in skill.ts (names only, for error usage lines). */ +export const USAGE_SKILL_TOPIC_ROWS: ReadonlyArray<{ name: string }> = [ + { name: "cli" }, + { name: "develop" }, + { name: "author" }, +]; + +const USAGE_SECTION_BY_GROUP: Record = { + workflow: "Workflow registry:", + thread: "Thread execution:", + cas: "Content-addressable storage:", + init: "Development:", +}; + +export function formatUsageCommandLines( + rows: ReadonlyArray<{ prefix: string; description: string }>, +): string[] { + const maxPrefix = rows.reduce((max, row) => Math.max(max, row.prefix.length), 0); + const gap = 2; + return rows.map((row) => { + const pad = " ".repeat(maxPrefix - row.prefix.length + gap); + return ` ${row.prefix}${pad}${row.description}`; + }); +} + +export function formatCliUsage( + groups: ReadonlyArray, + skillTopics: ReadonlyArray<{ name: string }>, +): string { + const lines: string[] = ["uncaged-workflow — workflow engine CLI", ""]; + + for (const group of groups) { + const sectionTitle = USAGE_SECTION_BY_GROUP[group.name]; + if (sectionTitle === undefined) { + throw new Error(`BUG: missing usage section title for group "${group.name}"`); + } + lines.push(sectionTitle); + const rows = group.commands.map((cmd) => { + const args = cmd.args ? ` ${cmd.args}` : ""; + return { + prefix: `${group.name} ${cmd.name}${args}`, + description: cmd.description, + }; + }); + lines.push(...formatUsageCommandLines(rows)); + lines.push(""); + } + + lines.push("Shortcuts:"); + lines.push( + ...formatUsageCommandLines([ + { prefix: "run [...]", description: "→ thread run" }, + { prefix: "live [...]", description: "→ thread live" }, + ]), + ); + lines.push(""); + + lines.push("Reference:"); + const skillTopicNames = skillTopics.map((t) => t.name).join(", "); + lines.push( + ...formatUsageCommandLines([ + { + prefix: "skill [topic]", + description: `Agent-consumable docs (${skillTopicNames})`, + }, + ]), + ); + lines.push(""); + lines.push("Use --help for subcommand details."); + 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"); +} diff --git a/packages/cli-workflow/src/commands/cas/dispatch.ts b/packages/cli-workflow/src/commands/cas/dispatch.ts new file mode 100644 index 0000000..fd5ff6c --- /dev/null +++ b/packages/cli-workflow/src/commands/cas/dispatch.ts @@ -0,0 +1,128 @@ +import type { CommandEntry, DispatchGroupFn } from "../../cli-command-types.js"; +import { printCliError, printCliLine } from "../../cli-output.js"; +import { formatCliUsage, USAGE_SKILL_TOPIC_ROWS } from "../../cli-usage.js"; +import { getCommandGroupsForUsage } from "../../cli-usage-context.js"; +import { cmdGc } from "./gc.js"; +import { cmdCasGet } from "./get.js"; +import { cmdCasList } from "./list.js"; +import { cmdCasPut } from "./put.js"; +import { cmdCasRm } from "./rm.js"; + +function usageText(): string { + return formatCliUsage(getCommandGroupsForUsage(), USAGE_SKILL_TOPIC_ROWS); +} + +export async function dispatchGc(storageRoot: string, argv: string[]): Promise { + if (argv.length > 0) { + printCliError(`${usageText()}\n\nerror: gc takes no arguments`); + return 1; + } + const result = await cmdGc(storageRoot); + if (!result.ok) { + printCliError(result.error); + return 1; + } + const stats = result.value; + printCliLine( + `scanned ${stats.scannedThreads} threads, ${stats.activeRefs} active refs, deleted ${stats.deletedEntries} entries`, + ); + return 0; +} + +export async function dispatchCasGet(storageRoot: string, rest: string[]): Promise { + const threadId = rest[0]; + const hash = rest[1]; + if (threadId === undefined || hash === undefined || rest.length > 2) { + printCliError(`${usageText()}\n\nerror: cas get requires `); + return 1; + } + const result = await cmdCasGet(storageRoot, threadId, hash); + if (!result.ok) { + printCliError(result.error); + return 1; + } + printCliLine(result.value); + return 0; +} + +export async function dispatchCasPut(storageRoot: string, rest: string[]): Promise { + const threadId = rest[0]; + const content = rest[1]; + if (threadId === undefined || content === undefined || rest.length > 2) { + printCliError(`${usageText()}\n\nerror: cas put requires `); + return 1; + } + const result = await cmdCasPut(storageRoot, threadId, content); + if (!result.ok) { + printCliError(result.error); + return 1; + } + printCliLine(result.value); + return 0; +} + +export async function dispatchCasList(storageRoot: string, rest: string[]): Promise { + const threadId = rest[0]; + if (threadId === undefined || rest.length > 1) { + printCliError(`${usageText()}\n\nerror: cas list requires `); + return 1; + } + const result = await cmdCasList(storageRoot, threadId); + if (!result.ok) { + printCliError(result.error); + return 1; + } + for (const hash of result.value) { + printCliLine(hash); + } + return 0; +} + +export async function dispatchCasRm(storageRoot: string, rest: string[]): Promise { + const threadId = rest[0]; + const hash = rest[1]; + if (threadId === undefined || hash === undefined || rest.length > 2) { + printCliError(`${usageText()}\n\nerror: cas rm requires `); + return 1; + } + const result = await cmdCasRm(storageRoot, threadId, hash); + if (!result.ok) { + printCliError(result.error); + return 1; + } + printCliLine(`removed cas entry ${hash}`); + return 0; +} + +export 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" }, +}; + +export function createCasDispatcher(deps: { dispatchGroup: DispatchGroupFn }) { + const { dispatchGroup } = deps; + return async function dispatchCas(storageRoot: string, argv: string[]): Promise { + const result = dispatchGroup("cas", CAS_SUBCOMMAND_TABLE, storageRoot, argv); + if (result !== null) { + return result; + } + const sub = argv[0]; + printCliError(`${usageText()}\n\nerror: unknown cas subcommand: ${sub}`); + return 1; + }; +} diff --git a/packages/cli-workflow/src/commands/init/dispatch.ts b/packages/cli-workflow/src/commands/init/dispatch.ts new file mode 100644 index 0000000..1bb0567 --- /dev/null +++ b/packages/cli-workflow/src/commands/init/dispatch.ts @@ -0,0 +1,66 @@ +import type { CommandEntry, DispatchGroupFn } from "../../cli-command-types.js"; +import { printCliError, printCliLine } from "../../cli-output.js"; +import { formatCliUsage, USAGE_SKILL_TOPIC_ROWS } from "../../cli-usage.js"; +import { getCommandGroupsForUsage } from "../../cli-usage-context.js"; +import { cmdInitTemplate } from "./template.js"; +import { cmdInitWorkspace } from "./workspace.js"; + +function usageText(): string { + return formatCliUsage(getCommandGroupsForUsage(), USAGE_SKILL_TOPIC_ROWS); +} + +export async function dispatchInitWorkspace(_storageRoot: string, argv: string[]): Promise { + const name = argv[0]; + if (name === undefined || argv.length > 1) { + printCliError(`${usageText()}\n\nerror: init workspace requires `); + return 1; + } + 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; +} + +export async function dispatchInitTemplate(_storageRoot: string, argv: string[]): Promise { + const name = argv[0]; + if (name === undefined || argv.length > 1) { + printCliError(`${usageText()}\n\nerror: init template requires `); + 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; +} + +export 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", + }, +}; + +export function createInitDispatcher(deps: { dispatchGroup: DispatchGroupFn }) { + const { dispatchGroup } = deps; + return async function dispatchInit(storageRoot: string, argv: string[]): Promise { + const result = dispatchGroup("init", INIT_SUBCOMMAND_TABLE, storageRoot, argv); + if (result !== null) { + return result; + } + const sub = argv[0]; + printCliError(`${usageText()}\n\nerror: unknown init subcommand: ${sub}`); + return 1; + }; +} diff --git a/packages/cli-workflow/src/commands/thread/dispatch.ts b/packages/cli-workflow/src/commands/thread/dispatch.ts new file mode 100644 index 0000000..9d2fb9a --- /dev/null +++ b/packages/cli-workflow/src/commands/thread/dispatch.ts @@ -0,0 +1,204 @@ +import type { CommandEntry, DispatchGroupFn } from "../../cli-command-types.js"; +import { printCliError, printCliLine } from "../../cli-output.js"; +import { formatCliUsage, USAGE_SKILL_TOPIC_ROWS } from "../../cli-usage.js"; +import { getCommandGroupsForUsage } from "../../cli-usage-context.js"; +import { parseLiveArgv } from "../../live-argv.js"; +import { parseRunArgv } from "../../run-argv.js"; +import { cmdKill, cmdPause, cmdResume } from "./control.js"; +import { cmdFork, parseForkArgv } from "./fork.js"; +import { cmdThreads } from "./list.js"; +import { cmdLive } from "./live.js"; +import { cmdPs } from "./ps.js"; +import { cmdThreadRemove } from "./rm.js"; +import { cmdRun } from "./run.js"; +import { cmdThreadShow } from "./show.js"; + +function usageText(): string { + return formatCliUsage(getCommandGroupsForUsage(), USAGE_SKILL_TOPIC_ROWS); +} + +export async function dispatchRun(storageRoot: string, argv: string[]): Promise { + const parsed = parseRunArgv(argv); + if (!parsed.ok) { + printCliError(`${usageText()}\n\nerror: ${parsed.error}`); + return 1; + } + + const result = await cmdRun( + storageRoot, + parsed.value.name, + parsed.value.prompt, + parsed.value.maxRounds, + ); + if (!result.ok) { + printCliError(result.error); + return 1; + } + + printCliLine(result.value.threadId); + return 0; +} + +export async function dispatchPs(storageRoot: string, argv: string[]): Promise { + if (argv.length > 0) { + printCliError(`${usageText()}\n\nerror: ps takes no arguments`); + return 1; + } + for (const line of await cmdPs(storageRoot)) { + printCliLine(line); + } + return 0; +} + +export async function dispatchKill(storageRoot: string, argv: string[]): Promise { + const threadId = argv[0]; + if (threadId === undefined || argv.length > 1) { + printCliError(`${usageText()}\n\nerror: kill requires `); + return 1; + } + const result = await cmdKill(storageRoot, threadId); + if (!result.ok) { + printCliError(result.error); + return 1; + } + printCliLine(`kill sent for thread ${threadId}`); + return 0; +} + +export async function dispatchLive(storageRoot: string, argv: string[]): Promise { + const parsed = parseLiveArgv(argv); + if (!parsed.ok) { + printCliError(`${usageText()}\n\nerror: ${parsed.error}`); + return 1; + } + return cmdLive(storageRoot, parsed.value); +} + +export async function dispatchPause(storageRoot: string, argv: string[]): Promise { + const threadId = argv[0]; + if (threadId === undefined || argv.length > 1) { + printCliError(`${usageText()}\n\nerror: pause requires `); + return 1; + } + const result = await cmdPause(storageRoot, threadId); + if (!result.ok) { + printCliError(result.error); + return 1; + } + printCliLine(`pause sent for thread ${threadId}`); + return 0; +} + +export async function dispatchResume(storageRoot: string, argv: string[]): Promise { + const threadId = argv[0]; + if (threadId === undefined || argv.length > 1) { + printCliError(`${usageText()}\n\nerror: resume requires `); + return 1; + } + const result = await cmdResume(storageRoot, threadId); + if (!result.ok) { + printCliError(result.error); + return 1; + } + printCliLine(`resume sent for thread ${threadId}`); + return 0; +} + +export async function dispatchThreadList(storageRoot: string, argv: string[]): Promise { + const result = await cmdThreads(storageRoot, argv); + if (!result.ok) { + printCliError(result.error); + return 1; + } + for (const line of result.value) { + printCliLine(line); + } + return 0; +} + +export async function dispatchThreadShow(storageRoot: string, argv: string[]): Promise { + const id = argv[0]; + if (id === undefined || argv.length > 1) { + printCliError(`${usageText()}\n\nerror: thread show requires `); + return 1; + } + const result = await cmdThreadShow(storageRoot, id); + if (!result.ok) { + printCliError(result.error); + return 1; + } + printCliLine(result.value); + return 0; +} + +export async function dispatchThreadRm(storageRoot: string, argv: string[]): Promise { + const id = argv[0]; + if (id === undefined || argv.length > 1) { + printCliError(`${usageText()}\n\nerror: thread rm requires `); + return 1; + } + const result = await cmdThreadRemove(storageRoot, id); + if (!result.ok) { + printCliError(result.error); + return 1; + } + printCliLine(`removed thread ${id}`); + return 0; +} + +export async function dispatchFork(storageRoot: string, argv: string[]): Promise { + const parsed = parseForkArgv(argv); + if (!parsed.ok) { + printCliError(`${usageText()}\n\nerror: ${parsed.error}`); + return 1; + } + const result = await cmdFork(storageRoot, parsed.value.threadId, parsed.value.fromRole); + if (!result.ok) { + printCliError(result.error); + return 1; + } + printCliLine(result.value.threadId); + return 0; +} + +export 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: " | --latest [--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" }, +}; + +export function createThreadDispatcher(deps: { dispatchGroup: DispatchGroupFn }) { + const { dispatchGroup } = deps; + return 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]; + printCliError(`${usageText()}\n\nerror: unknown thread subcommand: ${sub}`); + return 1; + }; +} diff --git a/packages/cli-workflow/src/commands/workflow/dispatch.ts b/packages/cli-workflow/src/commands/workflow/dispatch.ts new file mode 100644 index 0000000..560a742 --- /dev/null +++ b/packages/cli-workflow/src/commands/workflow/dispatch.ts @@ -0,0 +1,162 @@ +import type { CommandEntry, DispatchGroupFn } from "../../cli-command-types.js"; +import { printCliError, printCliLine, printCliWarn } from "../../cli-output.js"; +import { formatCliUsage, USAGE_SKILL_TOPIC_ROWS } from "../../cli-usage.js"; +import { getCommandGroupsForUsage } from "../../cli-usage-context.js"; +import { cmdAdd, formatAddSuccess, parseAddArgv } from "./add.js"; +import { cmdHistory } from "./history.js"; +import { cmdList, formatListLines } from "./list.js"; +import { cmdRemove } from "./rm.js"; +import { cmdRollback } from "./rollback.js"; +import { cmdShow, formatShowYaml } from "./show.js"; + +function usageText(): string { + return formatCliUsage(getCommandGroupsForUsage(), USAGE_SKILL_TOPIC_ROWS); +} + +export async function dispatchAdd(storageRoot: string, argv: string[]): Promise { + const parsed = parseAddArgv(argv); + if (!parsed.ok) { + printCliError(`${usageText()}\n\nerror: ${parsed.error}`); + return 1; + } + const result = await cmdAdd(storageRoot, parsed.value); + if (!result.ok) { + printCliError(result.error); + return 1; + } + for (const w of result.value.warnings) { + printCliWarn(w); + } + printCliLine(formatAddSuccess(parsed.value.name, parsed.value.filePath, result.value.hash)); + return 0; +} + +export async function dispatchList(storageRoot: string, argv: string[]): Promise { + if (argv.length > 0) { + printCliError(`${usageText()}\n\nerror: list takes no arguments`); + return 1; + } + const result = await cmdList(storageRoot); + if (!result.ok) { + printCliError(result.error); + return 1; + } + for (const line of formatListLines(result.value)) { + printCliLine(line); + } + return 0; +} + +export async function dispatchShow(storageRoot: string, argv: string[]): Promise { + const name = argv[0]; + if (name === undefined || argv.length > 1) { + printCliError(`${usageText()}\n\nerror: show requires `); + return 1; + } + const result = await cmdShow(storageRoot, name); + if (!result.ok) { + printCliError(result.error); + return 1; + } + printCliLine(formatShowYaml(name, result.value)); + return 0; +} + +export async function dispatchRemove(storageRoot: string, argv: string[]): Promise { + const name = argv[0]; + if (name === undefined || argv.length > 1) { + printCliError(`${usageText()}\n\nerror: remove requires `); + return 1; + } + const result = await cmdRemove(storageRoot, name); + if (!result.ok) { + printCliError(result.error); + return 1; + } + printCliLine(`removed workflow "${name}" from registry`); + return 0; +} + +export async function dispatchHistory(storageRoot: string, argv: string[]): Promise { + const name = argv[0]; + if (name === undefined || argv.length > 1) { + printCliError(`${usageText()}\n\nerror: history requires `); + return 1; + } + const result = await cmdHistory(storageRoot, name); + if (!result.ok) { + printCliError(result.error); + return 1; + } + for (const line of result.value) { + printCliLine(line); + } + return 0; +} + +export async function dispatchRollback(storageRoot: string, argv: string[]): Promise { + const name = argv[0]; + if (name === undefined || argv.length > 2) { + printCliError(`${usageText()}\n\nerror: rollback requires [hash]`); + return 1; + } + const hashArg = argv[1]; + const result = await cmdRollback(storageRoot, name, hashArg === undefined ? null : hashArg); + if (!result.ok) { + printCliError(result.error); + return 1; + } + printCliLine(`rolled back workflow "${name}"`); + return 0; +} + +export 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", + }, +}; + +type WorkflowDispatchDeps = { + dispatchGroup: DispatchGroupFn; + printDeprecation: (oldCmd: string, newCmd: string) => void; +}; + +export function createWorkflowDispatcher(deps: WorkflowDispatchDeps) { + const { dispatchGroup, printDeprecation } = deps; + return 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)); + } + printCliError(`${usageText()}\n\nerror: unknown workflow subcommand: ${sub}`); + return 1; + }; +} diff --git a/packages/cli-workflow/src/skill.ts b/packages/cli-workflow/src/skill.ts index 91fed64..a1501c1 100644 --- a/packages/cli-workflow/src/skill.ts +++ b/packages/cli-workflow/src/skill.ts @@ -1,4 +1,4 @@ -import { getCommandRegistry } from "./cli-dispatch.js"; +import { getCommandRegistry } from "./cli-registry.js"; type SkillTopic = { name: string; -- 2.43.0