fix: scope gh subcommand help output

This commit is contained in:
Codex
2026-06-16 01:34:38 +00:00
parent 7eadf39529
commit 028679cfcb
3 changed files with 106 additions and 2 deletions
+12
View File
@@ -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 <subcommand> --help``gh <subcommand> -h``gh <subcommand> help` 只输出匹配命令或命令组的 bounded JSON,包括相关 usage、短 notes 和完整 help 入口;`gh --help` / `gh help` 才输出完整顶层命令索引。
---
## Issue 命令
+92
View File
@@ -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 <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 <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 <number>` 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<GitHubCommandResult | unknown> {
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;
+2 -2
View File
@@ -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<unknown | nul
if (top === "dev-env") return devEnvHelp();
if (top === "artifact-registry") return artifactRegistryHelp();
if (top === "auth-broker") return authBrokerHelp();
if (top === "gh") return ghHelp();
if (top === "gh") return ghScopedHelp(args.slice(1)) ?? ghHelp();
if (top === "agentrun") return loadHelp(async () => (await import("./agentrun")).agentRunHelp(), agentRunHelpSummary());
if (top === "platform-infra") return loadHelp(async () => (await import("./platform-infra")).platformInfraHelp(), platformInfraHelpSummary());
if (top === "platform-db") return platformDbHelp();