From 9d9aaa3407112ae76d7f546d91f232e8a3eba1be Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 10 Sep 2026 20:46:22 +0000 Subject: [PATCH] Append live leaf command paths to hierarchical help Root and group human --help still use Commander for immediate children, then list descendant leaves from the live command tree so agents and the plugin skill check can find two-word forms like `media generate`. Co-authored-by: Gersham Meharg --- src/help.test.ts | 40 +++++++++++++++++++++++++++++++++++++--- src/help.ts | 26 +++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/help.test.ts b/src/help.test.ts index 2fab0540..79d0b50e 100644 --- a/src/help.test.ts +++ b/src/help.test.ts @@ -3,7 +3,8 @@ import { execFileSync } from "node:child_process"; import { test } from "node:test"; import { fileURLToPath } from "node:url"; import { COMMAND_SPECS } from "./command-spec.js"; -import { commandHelp } from "./help.js"; +import { createCommandTree, findCommand } from "./command-tree.js"; +import { commandHelp, leafCommandPaths } from "./help.js"; import { CLI_VERSION } from "./version.js"; const bin = fileURLToPath(new URL("./bin.js", import.meta.url)); @@ -15,14 +16,16 @@ function invoke(args: string[]): { data: ReturnType & { vers test("root help is compact and every command is discoverable through immediate children", () => { const root = commandHelp(); - assert.ok(root.usage.split("\n").length < 35); - assert.doesNotMatch(root.usage, /localhost|log_socket|media upload|screen assign|--token/); + const commanderHelp = root.usage.split(/\n\nAll commands:\n/)[0]!; + assert.ok(commanderHelp.split("\n").length < 35); + assert.doesNotMatch(root.usage, /localhost|log_socket|--token/); const discovered: string[] = []; function visit(path: string[]): void { const help = commandHelp(path); if (help.kind === "command") { if (path.join(" ") !== "help") discovered.push(path.join(" ")); assert.ok(help.synopsis.every((line) => line.startsWith(`screenrig ${path.join(" ")}`))); + assert.equal(help.usage.includes("\nAll commands:\n"), false); return; } for (const child of help.commands) { @@ -32,6 +35,37 @@ test("root help is compact and every command is discoverable through immediate c } visit([]); assert.deepEqual(discovered.sort(), COMMAND_SPECS.map((spec) => spec.path.join(" ")).sort()); + const inventory = leafCommandPaths(createCommandTree().root); + assert.ok(inventory.includes("help")); + for (const path of inventory) { + assert.ok(root.usage.includes(path), path); + } + assert.ok(root.commands.every((child) => child.path.length === 1)); + assert.equal(root.commands.some((child) => child.name === "generate"), false); +}); + +test("group help stays focused and still names descendant leaf paths", () => { + const media = commandHelp(["media"]); + assert.match(media.usage, /Usage: screenrig media \[options\] \[command\]/); + assert.ok(media.commands.every((child) => child.path[0] === "media" && child.path.length === 2)); + assert.deepEqual(media.commands.map((child) => child.name), [ + "generate", "upload", "upload-batch", "show", "download", "list", "update", "delete", + ]); + for (const path of leafCommandPaths(findCommand(createCommandTree().root, ["media"])!)) { + assert.ok(media.usage.includes(path), path); + } + assert.doesNotMatch(media.usage, /screen assign|account show|agent enroll|playlist create/); +}); + +test("human root --help includes every taught leaf command path", () => { + const text = execFileSync(process.execPath, [bin, "--help"], { + encoding: "utf8", env: { ...process.env, SCREENRIG_CONFIG: "/nonexistent/screenrig-help-config" }, + }); + for (const spec of COMMAND_SPECS) { + const path = spec.path.join(" "); + assert.ok(text.includes(path), path); + for (const alias of spec.aliases ?? []) assert.ok(text.includes(alias.join(" ")), alias.join(" ")); + } }); test("help paths, --help, and bare groups work before configuration or authentication", () => { diff --git a/src/help.ts b/src/help.ts index cc02d102..94dd0ade 100644 --- a/src/help.ts +++ b/src/help.ts @@ -16,12 +16,36 @@ export interface HelpDocument { notes: string[]; } +/** Canonical and alias paths of every descendant leaf, in tree order. */ +export function leafCommandPaths(command: Command): string[] { + const paths: string[] = []; + const visit = (node: Command): void => { + if (node.commands.length) { + node.commands.forEach(visit); + return; + } + const path = commandPath(node); + if (!path.length) return; + paths.push(path.join(" ")); + for (const alias of node.aliases()) paths.push([...path.slice(0, -1), alias].join(" ")); + }; + command.commands.forEach(visit); + return paths; +} + +function commandInventory(command: Command): string | undefined { + const paths = leafCommandPaths(command); + if (!paths.length) return undefined; + return ["All commands:", ...paths.map((path) => ` ${path}`)].join("\n"); +} + /** JSON discovery and human help both read the actual Commander command tree. */ export function describeHelp(command: Command): HelpDocument { const helper = command.createHelp(); const path = commandPath(command); const note = NOTES[path.join(" ")]; const notes = note ? [note] : []; + const inventory = commandInventory(command); const describeOption = (option: Option) => ({ name: option.long!, type: (option.negate || option.isBoolean()) ? "boolean" as const : "value" as const, description: option.description, required: option.mandatory, }); @@ -29,7 +53,7 @@ export function describeHelp(command: Command): HelpDocument { path, aliases: command.aliases().map((alias) => [...path.slice(0, -1), alias]), kind: command.commands.length ? "group" : "command", - usage: [command.helpInformation().trimEnd(), ...notes].join("\n\n"), + usage: [command.helpInformation().trimEnd(), inventory, ...notes].filter((part): part is string => part !== undefined).join("\n\n"), synopsis: [helper.commandUsage(command)], commands: helper.visibleCommands(command).map((child) => ({ name: child.name(), path: commandPath(child), kind: child.commands.length ? "group" as const : "command" as const,