diff --git a/src/cli.ts b/src/cli.ts index eab1991..286547f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -47,6 +47,16 @@ function capHelpText(cap: Capability): string { return lines.join("\n"); } +/** Group usage: `rome ` / `rome --help` — that group's commands only. */ +function groupHelpText(group: string): string { + const lines = [`Usage: rome ${group} [args]`, "", "Commands:"]; + for (const c of CAPABILITIES.filter((x) => !x.verb && x.group === group)) { + const a = c.args.map((x) => (x.required ? `<${x.name}>` : `[${x.name}]`)).join(" "); + lines.push(` rome ${c.cliPath}${a ? " " + a : ""}\n ${c.summary}`); + } + return lines.join("\n"); +} + function helpText(): string { const lines = [ "rome — Rome Protocol dev CLI + MCP server", @@ -86,6 +96,19 @@ export async function main(argv: string[]): Promise { const resolved = resolveCli(args); if (!resolved) { + // A bare group or ` --help` is a help request; an unknown subcommand + // gets the error scoped to that group's commands, not the full catalog. + const isGroup = CAPABILITIES.some((c) => !c.verb && c.group === first); + if (isGroup) { + const second = args[1]; + if (second === undefined || second === "--help" || second === "-h" || second === "help") { + console.log(groupHelpText(first)); + return 0; + } + console.error(`Unknown command: rome ${args.slice(0, 2).join(" ")}`); + console.error("\n" + groupHelpText(first)); + return 1; + } console.error(`Unknown command: rome ${args.slice(0, 2).join(" ")}`); console.error("\n" + helpText()); return 1; diff --git a/test/cli.test.ts b/test/cli.test.ts index 1af8a0e..eacf138 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -138,6 +138,37 @@ describe("CLI guardrails — extra positionals, per-command help, version", () = expect(c.out.join("\n")).toMatch(/rome facts chain /); }); + it("prints group usage on `rome cookbook --help` and exits 0 (not 'Unknown command')", async () => { + const c = capture(); + const code = await main(["node", "rome", "cookbook", "--help"]); + c.restore(); + expect(code).toBe(0); + expect(c.err.join("\n")).not.toMatch(/unknown command/i); + const out = c.out.join("\n"); + expect(out).toMatch(/rome cookbook cpi-recipe/); + expect(out).toMatch(/rome cookbook errors/); + }); + + it("treats a bare group (`rome facts`) as a help request, exit 0", async () => { + const c = capture(); + const code = await main(["node", "rome", "facts"]); + c.restore(); + expect(code).toBe(0); + expect(c.err.join("\n")).not.toMatch(/unknown command/i); + expect(c.out.join("\n")).toMatch(/rome facts chain /); + }); + + it("scopes the error help to the group on an unknown subcommand (`rome facts bogus`)", async () => { + const c = capture(); + const code = await main(["node", "rome", "facts", "bogus"]); + c.restore(); + expect(code).toBe(1); + const err = c.err.join("\n"); + expect(err).toMatch(/unknown command: rome facts bogus/i); + expect(err).toMatch(/rome facts chain/); + expect(err).not.toMatch(/rome deploy/); // scoped to the group, not the full catalog + }); + it("--version prints the real package version, not a hardcoded string", async () => { const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")); const c = capture();