Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7907e24ad0 | |||
| 21238f7825 | |||
| 6b3aa4ce35 | |||
| f042c9d640 |
@@ -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,
|
||||
@@ -15,30 +15,71 @@ describe("help command", () => {
|
||||
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 --help returns 0", async () => {
|
||||
const code = await runCli(STORAGE_ROOT, ["skill", "--help"]);
|
||||
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"]);
|
||||
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", () => {
|
||||
@@ -54,10 +95,41 @@ 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");
|
||||
expect(idx).toContain("help --skill <topic>");
|
||||
expect(idx).toContain("skill <topic>");
|
||||
});
|
||||
});
|
||||
|
||||
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 <command> --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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -129,8 +129,9 @@ describe("init workspace", () => {
|
||||
|
||||
test("usage lists init subcommands", () => {
|
||||
const u = formatCliUsage();
|
||||
expect(u).toContain("uncaged-workflow init workspace <name>");
|
||||
expect(u).toContain("uncaged-workflow init template <name>");
|
||||
expect(u).toContain("init workspace <name>");
|
||||
expect(u).toContain("init template <name>");
|
||||
expect(u).toContain("Development:");
|
||||
});
|
||||
|
||||
test("runCli rejects unknown init subcommand", async () => {
|
||||
|
||||
@@ -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,18 +530,68 @@ export function getCommandRegistry(): ReadonlyArray<CommandGroup> {
|
||||
|
||||
// ── Auto-generated CLI usage ───────────────────────────────────────────
|
||||
|
||||
const USAGE_SECTION_BY_GROUP: Record<string, string> = {
|
||||
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 <name> [...] (shortcut for thread run)");
|
||||
lines.push(" uncaged-workflow live <thread-id> [...] (shortcut for thread live)");
|
||||
|
||||
lines.push("Shortcuts:");
|
||||
lines.push(
|
||||
...formatUsageCommandLines([
|
||||
{ prefix: "run <name> [...]", description: "→ thread run" },
|
||||
{ prefix: "live <id> [...]", 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 <command> --help for subcommand details.");
|
||||
lines.push("");
|
||||
lines.push("Environment variables:");
|
||||
lines.push(
|
||||
@@ -561,9 +616,16 @@ function dispatchGroup(
|
||||
argv: string[],
|
||||
): Promise<number> | null {
|
||||
const sub = argv[0];
|
||||
if (sub === undefined) {
|
||||
printCliError(`${formatCliUsage()}\n\nerror: unknown ${tableName} subcommand: (none)`);
|
||||
return Promise.resolve(1);
|
||||
if (sub === undefined || sub === "--help" || sub === "-h") {
|
||||
const entries = Object.entries(table);
|
||||
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];
|
||||
if (entry === undefined) {
|
||||
@@ -618,14 +680,9 @@ async function dispatchCas(storageRoot: string, argv: string[]): Promise<number>
|
||||
|
||||
// ── Help ────────────────────────────────────────────────────────────────
|
||||
|
||||
async function dispatchHelp(_storageRoot: string, argv: string[]): Promise<number> {
|
||||
const skillIdx = argv.indexOf("--skill");
|
||||
if (skillIdx === -1) {
|
||||
printCliLine(formatCliUsage());
|
||||
return 0;
|
||||
}
|
||||
const topic = argv[skillIdx + 1];
|
||||
if (topic === undefined) {
|
||||
async function dispatchSkill(_storageRoot: string, argv: string[]): Promise<number> {
|
||||
const topic = argv[0];
|
||||
if (topic === undefined || topic === "--help" || topic === "-h") {
|
||||
printCliLine(formatSkillIndex());
|
||||
return 0;
|
||||
}
|
||||
@@ -638,6 +695,27 @@ async function dispatchHelp(_storageRoot: string, argv: string[]): Promise<numbe
|
||||
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) ──────────────────────────────────
|
||||
|
||||
const COMMAND_TABLE: Record<string, DispatchFn> = {
|
||||
@@ -647,6 +725,7 @@ const COMMAND_TABLE: Record<string, DispatchFn> = {
|
||||
cas: dispatchCas,
|
||||
init: dispatchInit,
|
||||
help: dispatchHelp,
|
||||
skill: dispatchSkill,
|
||||
|
||||
// Top-level shortcuts (no deprecation)
|
||||
run: dispatchRun,
|
||||
@@ -672,12 +751,12 @@ const DEPRECATED_ALIASES: Record<string, { newCmd: string; handler: DispatchFn }
|
||||
|
||||
export async function runCli(storageRoot: string, argv: string[]): Promise<number> {
|
||||
if (argv.length === 0) {
|
||||
printCliError(formatCliUsage());
|
||||
printCliLine(formatCliUsage());
|
||||
return 1;
|
||||
}
|
||||
const command = argv[0];
|
||||
if (command === undefined) {
|
||||
printCliError(formatCliUsage());
|
||||
printCliLine(formatCliUsage());
|
||||
return 1;
|
||||
}
|
||||
const rest = argv.slice(1);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -42,7 +42,7 @@ Available topics:
|
||||
|-------|-------------|
|
||||
${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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user