diff --git a/packages/cli-workflow/__tests__/help.test.ts b/packages/cli-workflow/__tests__/help.test.ts index 8d829c0..bcb7548 100644 --- a/packages/cli-workflow/__tests__/help.test.ts +++ b/packages/cli-workflow/__tests__/help.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { runCli } from "../src/cli-dispatch.js"; +import { formatCliUsage, runCli } from "../src/cli-dispatch.js"; import { formatSkillDoc, formatSkillIndex, @@ -90,6 +90,8 @@ describe("getSkillTopics", () => { describe("formatSkillIndex", () => { test("lists all topics", () => { const idx = formatSkillIndex(); + expect(idx).toContain("# uncaged-workflow skill"); + expect(idx).not.toContain("# uncaged-workflow help --skill"); expect(idx).toContain("cli"); expect(idx).toContain("develop"); expect(idx).toContain("author"); @@ -97,6 +99,35 @@ describe("formatSkillIndex", () => { }); }); +describe("formatCliUsage", () => { + test("has tagline, grouped sections, help hint, and env vars", () => { + const u = formatCliUsage(); + expect(u.startsWith("uncaged-workflow — workflow engine CLI")).toBe(true); + expect(u).toContain("Workflow registry:"); + expect(u).toContain("Thread execution:"); + expect(u).toContain("Content-addressable storage:"); + expect(u).toContain("Development:"); + expect(u).toContain("Shortcuts:"); + expect(u).toContain("Reference:"); + expect(u).toContain("skill [topic]"); + expect(u).toContain("Agent-consumable docs"); + expect(u).toContain("Use --help for subcommand details."); + expect(u).toContain("Environment variables:"); + expect(u).toContain("WORKFLOW_STORAGE_ROOT"); + expect(u).toContain("UNCAGED_WORKFLOW_STORAGE_ROOT"); + }); + + test("lists commands from registry with descriptions", () => { + const u = formatCliUsage(); + expect(u).toContain("workflow add"); + expect(u).toContain("Register a workflow bundle in the registry"); + expect(u).toContain("thread run"); + expect(u).toContain("Start a new thread executing a workflow"); + expect(u).toContain("cas gc"); + expect(u).toContain("Garbage-collect unreferenced CAS entries"); + }); +}); + describe("formatSkillTopic('cli') — legacy formatSkillDoc", () => { const doc = formatSkillDoc(); diff --git a/packages/cli-workflow/__tests__/init-workspace.test.ts b/packages/cli-workflow/__tests__/init-workspace.test.ts index ba3e30e..4da8a39 100644 --- a/packages/cli-workflow/__tests__/init-workspace.test.ts +++ b/packages/cli-workflow/__tests__/init-workspace.test.ts @@ -129,8 +129,9 @@ describe("init workspace", () => { test("usage lists init subcommands", () => { const u = formatCliUsage(); - expect(u).toContain("uncaged-workflow init workspace "); - expect(u).toContain("uncaged-workflow init template "); + expect(u).toContain("init workspace "); + expect(u).toContain("init template "); + expect(u).toContain("Development:"); }); test("runCli rejects unknown init subcommand", async () => { diff --git a/packages/cli-workflow/src/cli-dispatch.ts b/packages/cli-workflow/src/cli-dispatch.ts index 84060d6..f6dd222 100644 --- a/packages/cli-workflow/src/cli-dispatch.ts +++ b/packages/cli-workflow/src/cli-dispatch.ts @@ -3,7 +3,12 @@ import { cmdAdd, formatAddSuccess, parseAddArgv } from "./cmd-add.js"; import { cmdCasGet, cmdCasList, cmdCasPut, cmdCasRm } from "./cmd-cas.js"; import { cmdFork, parseForkArgv } from "./cmd-fork.js"; import { cmdGc } from "./cmd-gc.js"; -import { formatSkillDoc, formatSkillIndex, formatSkillTopic } from "./cmd-help.js"; +import { + formatSkillDoc, + formatSkillIndex, + formatSkillTopic, + getSkillTopics, +} from "./cmd-help.js"; import { cmdHistory } from "./cmd-history.js"; import { cmdInitTemplate, cmdInitWorkspace } from "./cmd-init.js"; import { cmdKill } from "./cmd-kill.js"; @@ -525,21 +530,68 @@ export function getCommandRegistry(): ReadonlyArray { // ── 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[] = ["Usage:"]; + const lines: string[] = ["uncaged-workflow — workflow engine CLI", ""]; + 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}`); + 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(" uncaged-workflow run [...] (shortcut for thread run)"); - lines.push(" uncaged-workflow live [...] (shortcut for thread live)"); + + lines.push("Shortcuts:"); + lines.push( + ...formatUsageCommandLines([ + { prefix: "run [...]", description: "→ thread run" }, + { prefix: "live [...]", description: "→ thread live" }, + ]), + ); lines.push(""); - lines.push(" uncaged-workflow skill [topic] agent-consumable reference docs"); - lines.push(" uncaged-workflow help show this usage"); + + 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( diff --git a/packages/cli-workflow/src/cmd-help.ts b/packages/cli-workflow/src/cmd-help.ts index b435c43..91fed64 100644 --- a/packages/cli-workflow/src/cmd-help.ts +++ b/packages/cli-workflow/src/cmd-help.ts @@ -34,7 +34,7 @@ export function formatSkillTopic(topic: string): string | null { export function formatSkillIndex(): string { const rows = SKILL_TOPICS.map((t) => `| \`${t.name}\` | ${t.description} |`); - return `# uncaged-workflow help --skill + return `# uncaged-workflow skill Available topics: