From 028679cfcbf9429a03d701fb827d46ca175273d5 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 16 Jun 2026 01:34:38 +0000 Subject: [PATCH] fix: scope gh subcommand help output --- .agents/skills/unidesk-gh/SKILL.md | 12 ++++ scripts/src/gh.ts | 92 ++++++++++++++++++++++++++++++ scripts/src/help.ts | 4 +- 3 files changed, 106 insertions(+), 2 deletions(-) diff --git a/.agents/skills/unidesk-gh/SKILL.md b/.agents/skills/unidesk-gh/SKILL.md index 0f9ccdcb..1ff5f225 100644 --- a/.agents/skills/unidesk-gh/SKILL.md +++ b/.agents/skills/unidesk-gh/SKILL.md @@ -35,6 +35,18 @@ bun scripts/cli.ts gh auth status [--repo owner/name] 探测 token 来源(`GH_TOKEN`/`GITHUB_TOKEN`/`gh auth token`)、GitHub REST egress、repo 可见性、issue 可读性。不打印 token。 +## 聚焦帮助 + +具体子命令用法优先直接查 scoped help,不要先打开顶层长 help 再人工搜索: + +```bash +bun scripts/cli.ts gh issue close --help +bun scripts/cli.ts gh issue comment --help +bun scripts/cli.ts gh pr merge --help +``` + +`gh --help`、`gh -h` 和 `gh help` 只输出匹配命令或命令组的 bounded JSON,包括相关 usage、短 notes 和完整 help 入口;`gh --help` / `gh help` 才输出完整顶层命令索引。 + --- ## Issue 命令 diff --git a/scripts/src/gh.ts b/scripts/src/gh.ts index 34788f2e..a0a38943 100644 --- a/scripts/src/gh.ts +++ b/scripts/src/gh.ts @@ -7744,7 +7744,99 @@ export function ghHelp(): unknown { }; } +function isGhHelpRequest(args: string[]): boolean { + if (args.length === 0) return true; + if (args[0] === "help" || args[0] === "--help" || args[0] === "-h") return true; + if (args.includes("--help") || args.includes("-h")) return true; + const cleaned = args.filter((arg) => arg !== "--help" && arg !== "-h"); + return positionalArgs(cleaned).includes("help"); +} + +function ghHelpCommandTokens(args: string[]): string[] { + const cleaned = args.filter((arg) => arg !== "--help" && arg !== "-h"); + const positionals = positionalArgs(cleaned); + if (positionals[0] === "help") positionals.shift(); + if (positionals[positionals.length - 1] === "help") positionals.pop(); + return positionals; +} + +function ghHelpUsageLines(): string[] { + const help = ghHelp() as { usage?: unknown }; + if (!Array.isArray(help.usage)) return []; + return help.usage.filter((line): line is string => typeof line === "string"); +} + +function ghUsageCommandTokens(usageLine: string): string[] { + const prefix = "bun scripts/cli.ts gh "; + if (!usageLine.startsWith(prefix)) return []; + const tokens: string[] = []; + for (const token of usageLine.slice(prefix.length).split(/\s+/u)) { + if (token.length === 0) continue; + if (token.startsWith("[") || token.startsWith("<") || token.startsWith("--")) break; + tokens.push(token); + } + return tokens; +} + +function ghUsageMatchesCommandTokens(usageLine: string, requestedTokens: string[]): boolean { + const usageTokens = ghUsageCommandTokens(usageLine); + if (requestedTokens.length === 0 || requestedTokens.length > usageTokens.length) return false; + return requestedTokens.every((requested, index) => usageTokens[index]?.split("|").includes(requested) === true); +} + +function ghScopedHelpNotes(tokens: string[]): string[] { + const key = tokens.join(" "); + const notes = [ + "Scoped help is bounded to the requested command or command group; use `bun scripts/cli.ts gh --help` for the full top-level command index.", + ]; + if (key === "issue comment" || key.startsWith("issue comment ")) { + notes.push("Issue comments use `--body-stdin` or `--body-file ` for Markdown bodies; inline `--body` is only for short single-line comments."); + notes.push("Use `issue comment update/edit` for wording fixes, `issue comment patch` for apply_patch-style local edits, and `issue comment delete` only for intentional removal."); + } else if (key === "issue close" || key === "issue reopen") { + notes.push("Issue close/reopen can post a lifecycle comment with `--comment`, `--comment-stdin`, or `--comment-file ` before changing state."); + notes.push("For long closeout evidence, prefer `--comment-stdin` with a quoted heredoc."); + } else if (key === "pr comment" || key.startsWith("pr comment ")) { + notes.push("PR comments are GitHub issue comments under the hood; use comment id targets for update/edit/delete."); + } else if (key === "pr merge") { + notes.push("PR merge is guarded: run `gh pr preflight ` first when you need an explicit readiness summary."); + } + return notes; +} + +export function ghScopedHelp(args: string[]): unknown | null { + if (!isGhHelpRequest(args)) return null; + const requestedTokens = ghHelpCommandTokens(args); + if (requestedTokens.length === 0) return ghHelp(); + const usageLines = ghHelpUsageLines(); + for (let length = requestedTokens.length; length >= 1; length -= 1) { + const scopedTokens = requestedTokens.slice(0, length); + const usage = usageLines.filter((line) => ghUsageMatchesCommandTokens(line, scopedTokens)); + if (usage.length === 0) continue; + return { + command: `gh ${scopedTokens.join(" ")}`, + output: "json", + scoped: true, + requestedCommand: requestedTokens.join(" "), + matchedCommand: scopedTokens.join(" "), + usage, + notes: ghScopedHelpNotes(scopedTokens), + fullHelpCommand: "bun scripts/cli.ts gh --help", + }; + } + return { + command: `gh ${requestedTokens.join(" ")}`, + output: "json", + scoped: true, + requestedCommand: requestedTokens.join(" "), + usage: [], + message: "No scoped help entry matched this gh command; use the top-level help for the full command index.", + fullHelpCommand: "bun scripts/cli.ts gh --help", + }; +} + export async function runGhCommand(args: string[]): Promise { + const scopedHelp = ghScopedHelp(args); + if (scopedHelp !== null) return scopedHelp; const [top, sub, third] = args; if (top === undefined || top === "help" || top === "--help" || top === "-h") return ghHelp(); let options: GitHubOptions; diff --git a/scripts/src/help.ts b/scripts/src/help.ts index 04d921e7..ffa69ef4 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -1,4 +1,4 @@ -import { ghHelp } from "./gh"; +import { ghHelp, ghScopedHelp } from "./gh"; import { authBrokerHelp } from "./auth-broker"; import { platformDbHelp } from "./platform-db"; import { secretsHelp } from "./secrets"; @@ -724,7 +724,7 @@ export async function staticNamespaceHelp(args: string[]): Promise (await import("./agentrun")).agentRunHelp(), agentRunHelpSummary()); if (top === "platform-infra") return loadHelp(async () => (await import("./platform-infra")).platformInfraHelp(), platformInfraHelpSummary()); if (top === "platform-db") return platformDbHelp();