fix(cli): usage not red + skill subcommand + --help flag on groups

1. No-args usage uses printCliLine (not printCliError), exit 1
2. 'skill [topic]' as first-class command (help --skill kept as compat)
3. 'workflow --help', 'thread --help' etc. show group subcommands
4. Role prompts updated: 'uncaged-workflow skill develop'

240 tests (6 new), build clean.

Closes #83

小橘 🍊
This commit is contained in:
2026-05-07 15:17:20 +00:00
parent f042c9d640
commit 6b3aa4ce35
5 changed files with 100 additions and 37 deletions
+58 -22
View File
@@ -15,30 +15,66 @@ describe("help command", () => {
expect(code).toBe(0); expect(code).toBe(0);
}); });
test("help --skill (no topic) returns 0 and lists topics", async () => { test("no args prints usage (not red) and returns 1", async () => {
const code = await runCli(STORAGE_ROOT, []);
expect(code).toBe(1);
});
});
describe("skill command", () => {
test("skill (no topic) lists topics and returns 0", async () => {
const code = await runCli(STORAGE_ROOT, ["skill"]);
expect(code).toBe(0);
});
test("skill cli returns 0", async () => {
const code = await runCli(STORAGE_ROOT, ["skill", "cli"]);
expect(code).toBe(0);
});
test("skill develop returns 0", async () => {
const code = await runCli(STORAGE_ROOT, ["skill", "develop"]);
expect(code).toBe(0);
});
test("skill author returns 0", async () => {
const code = await runCli(STORAGE_ROOT, ["skill", "author"]);
expect(code).toBe(0);
});
test("skill unknown returns 1", async () => {
const code = await runCli(STORAGE_ROOT, ["skill", "unknown"]);
expect(code).toBe(1);
});
});
describe("--help flag on groups", () => {
test("workflow --help returns 0", async () => {
const code = await runCli(STORAGE_ROOT, ["workflow", "--help"]);
expect(code).toBe(0);
});
test("thread --help returns 0", async () => {
const code = await runCli(STORAGE_ROOT, ["thread", "--help"]);
expect(code).toBe(0);
});
test("cas --help returns 0", async () => {
const code = await runCli(STORAGE_ROOT, ["cas", "--help"]);
expect(code).toBe(0);
});
test("init --help returns 0", async () => {
const code = await runCli(STORAGE_ROOT, ["init", "--help"]);
expect(code).toBe(0);
});
});
describe("legacy help --skill compat", () => {
test("help --skill still works (lists topics)", async () => {
const code = await runCli(STORAGE_ROOT, ["help", "--skill"]); const code = await runCli(STORAGE_ROOT, ["help", "--skill"]);
expect(code).toBe(0); expect(code).toBe(0);
}); });
test("help --skill cli returns 0", async () => {
const code = await runCli(STORAGE_ROOT, ["help", "--skill", "cli"]);
expect(code).toBe(0);
});
test("help --skill develop returns 0", async () => {
const code = await runCli(STORAGE_ROOT, ["help", "--skill", "develop"]);
expect(code).toBe(0);
});
test("help --skill author returns 0", async () => {
const code = await runCli(STORAGE_ROOT, ["help", "--skill", "author"]);
expect(code).toBe(0);
});
test("help --skill unknown returns 1", async () => {
const code = await runCli(STORAGE_ROOT, ["help", "--skill", "unknown"]);
expect(code).toBe(1);
});
}); });
describe("getSkillTopics", () => { describe("getSkillTopics", () => {
@@ -57,7 +93,7 @@ describe("formatSkillIndex", () => {
expect(idx).toContain("cli"); expect(idx).toContain("cli");
expect(idx).toContain("develop"); expect(idx).toContain("develop");
expect(idx).toContain("author"); expect(idx).toContain("author");
expect(idx).toContain("help --skill <topic>"); expect(idx).toContain("skill <topic>");
}); });
}); });
+39 -12
View File
@@ -538,6 +538,9 @@ export function formatCliUsage(): string {
lines.push(" uncaged-workflow run <name> [...] (shortcut for thread run)"); lines.push(" uncaged-workflow run <name> [...] (shortcut for thread run)");
lines.push(" uncaged-workflow live <thread-id> [...] (shortcut for thread live)"); lines.push(" uncaged-workflow live <thread-id> [...] (shortcut for thread live)");
lines.push(""); lines.push("");
lines.push(" uncaged-workflow skill [topic] agent-consumable reference docs");
lines.push(" uncaged-workflow help show this usage");
lines.push("");
lines.push("Environment variables:"); lines.push("Environment variables:");
lines.push( lines.push(
" WORKFLOW_STORAGE_ROOT Override storage directory (default: ~/.uncaged/workflow)", " WORKFLOW_STORAGE_ROOT Override storage directory (default: ~/.uncaged/workflow)",
@@ -561,9 +564,16 @@ function dispatchGroup(
argv: string[], argv: string[],
): Promise<number> | null { ): Promise<number> | null {
const sub = argv[0]; const sub = argv[0];
if (sub === undefined) { if (sub === undefined || sub === "--help" || sub === "-h") {
printCliError(`${formatCliUsage()}\n\nerror: unknown ${tableName} subcommand: (none)`); const entries = Object.entries(table);
return Promise.resolve(1); const lines = [`${tableName} subcommands:\n`];
for (const [name, e] of entries) {
const args = e.args ? ` ${e.args}` : "";
lines.push(` uncaged-workflow ${tableName} ${name}${args}`);
lines.push(` ${e.description}\n`);
}
printCliLine(lines.join("\n"));
return Promise.resolve(sub === undefined ? 1 : 0);
} }
const entry = table[sub]; const entry = table[sub];
if (entry === undefined) { if (entry === undefined) {
@@ -618,13 +628,8 @@ async function dispatchCas(storageRoot: string, argv: string[]): Promise<number>
// ── Help ──────────────────────────────────────────────────────────────── // ── Help ────────────────────────────────────────────────────────────────
async function dispatchHelp(_storageRoot: string, argv: string[]): Promise<number> { async function dispatchSkill(_storageRoot: string, argv: string[]): Promise<number> {
const skillIdx = argv.indexOf("--skill"); const topic = argv[0];
if (skillIdx === -1) {
printCliLine(formatCliUsage());
return 0;
}
const topic = argv[skillIdx + 1];
if (topic === undefined) { if (topic === undefined) {
printCliLine(formatSkillIndex()); printCliLine(formatSkillIndex());
return 0; return 0;
@@ -638,6 +643,27 @@ async function dispatchHelp(_storageRoot: string, argv: string[]): Promise<numbe
return 0; return 0;
} }
async function dispatchHelp(_storageRoot: string, argv: string[]): Promise<number> {
// 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;
}
printCliLine(formatCliUsage());
return 0;
}
// ── Top-level command table (Phase 3) ────────────────────────────────── // ── Top-level command table (Phase 3) ──────────────────────────────────
const COMMAND_TABLE: Record<string, DispatchFn> = { const COMMAND_TABLE: Record<string, DispatchFn> = {
@@ -647,6 +673,7 @@ const COMMAND_TABLE: Record<string, DispatchFn> = {
cas: dispatchCas, cas: dispatchCas,
init: dispatchInit, init: dispatchInit,
help: dispatchHelp, help: dispatchHelp,
skill: dispatchSkill,
// Top-level shortcuts (no deprecation) // Top-level shortcuts (no deprecation)
run: dispatchRun, run: dispatchRun,
@@ -672,12 +699,12 @@ const DEPRECATED_ALIASES: Record<string, { newCmd: string; handler: DispatchFn }
export async function runCli(storageRoot: string, argv: string[]): Promise<number> { export async function runCli(storageRoot: string, argv: string[]): Promise<number> {
if (argv.length === 0) { if (argv.length === 0) {
printCliError(formatCliUsage()); printCliLine(formatCliUsage());
return 1; return 1;
} }
const command = argv[0]; const command = argv[0];
if (command === undefined) { if (command === undefined) {
printCliError(formatCliUsage()); printCliLine(formatCliUsage());
return 1; return 1;
} }
const rest = argv.slice(1); const rest = argv.slice(1);
+1 -1
View File
@@ -42,7 +42,7 @@ Available topics:
|-------|-------------| |-------|-------------|
${rows.join("\n")} ${rows.join("\n")}
Usage: \`uncaged-workflow help --skill <topic>\` Usage: \`uncaged-workflow skill <topic>\`
`; `;
} }
@@ -11,7 +11,7 @@ export type CoderMeta = z.infer<typeof coderMetaSchema>;
const CODER_SYSTEM = `You are a **coder**. Read the thread for the plan and work on the NEXT incomplete phase only. const CODER_SYSTEM = `You are a **coder**. Read the thread for the plan and work on the NEXT incomplete phase only.
Run \`uncaged-workflow help --skill develop\` for thread ID lookup, CAS commands, and meta output guide. Run \`uncaged-workflow skill develop\` for thread ID lookup, CAS commands, and meta output guide.
## Reading phase details ## Reading phase details
@@ -14,7 +14,7 @@ export type PlannerMeta = z.infer<typeof plannerMetaSchema>;
const PLANNER_SYSTEM = `You are a **planner** for a software task. Break the work into **sequential phases** the coder will execute one at a time. const PLANNER_SYSTEM = `You are a **planner** for a software task. Break the work into **sequential phases** the coder will execute one at a time.
Run \`uncaged-workflow help --skill develop\` for thread ID lookup, CAS commands, and meta output guide. Run \`uncaged-workflow skill develop\` for thread ID lookup, CAS commands, and meta output guide.
## Storing phase details — MANDATORY ## Storing phase details — MANDATORY