fix: relax gh number target compatibility
This commit is contained in:
@@ -141,11 +141,11 @@
|
||||
|
||||
## T26 GitHub CLI PR 安全写入口
|
||||
|
||||
阅读 `AGENTS.md` 和 `docs/reference/cli.md`,然后用 cli 手动测试以下内容:准备一份包含真实换行、反引号和 Markdown 表格的临时正文文件,运行 `bun scripts/cli.ts gh help`,确认 help 中包含 `gh pr create`、`gh pr edit`、`gh pr comment`、`gh pr read <number|owner/repo#number>`、`gh pr preflight <number>`、`gh preflight <prNumber>`、`--raw|--full`、`gh pr files <number>` 和 `gh pr diff <number> --stat`。运行 `bun scripts/gh-cli-pr-contract-test.ts`,确认 mock GitHub 覆盖 PR read/view 的 `owner/repo#number` shorthand、`--raw` 完整披露、冲突 `--repo` 结构化失败、PR closeout GraphQL 字段、低噪声 `gh pr preflight`/`gh preflight`、PR edit/update REST PATCH payload、stdin `--body-file -` 和不回显完整正文。运行 `bun scripts/gh-cli-pr-files-contract-test.ts`,确认 mock GitHub 覆盖 `gh pr files` 的 REST changed-file/stat JSON、bounded file list、truncation metadata、next command、无 raw patch,以及 `gh pr diff --stat` 兼容别名和无 `--stat` raw diff 的结构化拒绝。对真实仓库只读观察可运行 `bun scripts/cli.ts gh pr files <number> --repo pikasTech/unidesk --limit 30`、`bun scripts/cli.ts gh pr diff <number> --repo pikasTech/unidesk --stat --limit 30` 或 `bun scripts/cli.ts gh pr preflight <number> --repo pikasTech/unidesk`,确认输出固定 JSON 且默认不含 raw diff 或完整 status contexts;需要完整 status contexts 时显式加 `--full`。运行 `bun scripts/cli.ts gh pr create --repo pikasTech/unidesk --title <title> --body-file <file> --base master --head <branch> --draft --dry-run`,确认命令不访问 GitHub、不创建 PR,JSON 中包含 `dryRun=true`、`planned=true`、repo、title、base、head、draft、bodyChars、bodyPreviewLines、request plan,并且正文预览保留真实换行和反引号。运行 `bun scripts/cli.ts gh pr edit <number> --repo pikasTech/unidesk --title <title> --body-file <file> --dry-run`,确认命令使用 REST PATCH 计划、不访问 GitHub Projects Classic GraphQL/projectCards,JSON 只包含 repo、PR number、changedFields、url、body 长度/SHA/source 和 request plan,不默认回显完整正文;再运行 `cat <file> | bun scripts/cli.ts gh pr edit <number> --repo pikasTech/unidesk --body-file - --dry-run`,确认 stdin source 标记为 `kind=stdin` 且同样低噪声。运行 `bun scripts/cli.ts gh pr comment <number> --repo pikasTech/unidesk --body-file <file> --dry-run`,确认命令不写评论,JSON 中包含 PR number、bodyChars、bodySource 和 request plan,且没有把换行污染成字面量 `\n`。运行 `bun scripts/cli.ts gh pr merge <number> --repo pikasTech/unidesk`,确认返回非零状态和结构化 JSON,`degradedReason=unsupported-command`、`runnerDisposition=business-failed`,且不会真实 merge。需要测试真实创建、编辑或评论时,只允许使用明确的 throwaway 源分支和 PR,并在记录中写明 PR URL、number、源/目标分支和清理动作;默认验收只做 dry-run,不创建或修改真实 PR。
|
||||
阅读 `AGENTS.md` 和 `docs/reference/cli.md`,然后用 cli 手动测试以下内容:准备一份包含真实换行、反引号和 Markdown 表格的临时正文文件,运行 `bun scripts/cli.ts gh help`,确认 help 中包含 `gh pr create`、`gh pr edit`、`gh pr comment`、`gh pr view <number|url|owner/repo#number>`、`gh pr read ... [compatibility alias for pr view]`、`--number N compat`、`gh pr preflight <number>`、`gh preflight <prNumber>`、`--raw|--full`、`gh pr files <number>` 和 `gh pr diff <number> --stat`。运行 `bun scripts/gh-cli-pr-contract-test.ts`,确认 mock GitHub 覆盖 PR view/read 的 GitHub URL、`owner/repo#number` shorthand、`--number` 兼容提示、`--raw` 完整披露、冲突 `--repo` 结构化失败、PR closeout GraphQL 字段、低噪声 `gh pr preflight`/`gh preflight`、PR edit/update REST PATCH payload、stdin `--body-file -` 和不回显完整正文。运行 `bun scripts/gh-cli-pr-files-contract-test.ts`,确认 mock GitHub 覆盖 `gh pr files` 的 REST changed-file/stat JSON、bounded file list、truncation metadata、next command、无 raw patch,以及 `gh pr diff --stat` 兼容别名和无 `--stat` raw diff 的结构化拒绝。对真实仓库只读观察可运行 `bun scripts/cli.ts gh pr files <number> --repo pikasTech/unidesk --limit 30`、`bun scripts/cli.ts gh pr diff <number> --repo pikasTech/unidesk --stat --limit 30` 或 `bun scripts/cli.ts gh pr preflight <number> --repo pikasTech/unidesk`,确认输出固定 JSON 且默认不含 raw diff 或完整 status contexts;需要完整 status contexts 时显式加 `--full`。运行 `bun scripts/cli.ts gh pr create --repo pikasTech/unidesk --title <title> --body-file <file> --base master --head <branch> --draft --dry-run`,确认命令不访问 GitHub、不创建 PR,JSON 中包含 `dryRun=true`、`planned=true`、repo、title、base、head、draft、bodyChars、bodyPreviewLines、request plan,并且正文预览保留真实换行和反引号。运行 `bun scripts/cli.ts gh pr edit <number> --repo pikasTech/unidesk --title <title> --body-file <file> --dry-run` 和 `bun scripts/cli.ts gh pr edit --number <number> --repo pikasTech/unidesk --title <title> --body-file <file> --dry-run`,确认命令使用 REST PATCH 计划、不访问 GitHub Projects Classic GraphQL/projectCards,`--number` 兼容路径返回 `standardSyntaxHint`,JSON 只包含 repo、PR number、changedFields、url、body 长度/SHA/source 和 request plan,不默认回显完整正文;再运行 `cat <file> | bun scripts/cli.ts gh pr edit <number> --repo pikasTech/unidesk --body-file - --dry-run`,确认 stdin source 标记为 `kind=stdin` 且同样低噪声。运行 `bun scripts/cli.ts gh pr comment <number> --repo pikasTech/unidesk --body-file <file> --dry-run`,确认命令不写评论,JSON 中包含 PR number、bodyChars、bodySource 和 request plan,且没有把换行污染成字面量 `\n`;运行 `bun scripts/cli.ts gh pr comment delete --number <commentId> --repo pikasTech/unidesk --dry-run`,确认 `--number` 按 commentId 兼容并返回 `standardSyntaxHint`。运行 `bun scripts/cli.ts gh pr merge <number> --repo pikasTech/unidesk`,确认 guarded merge plan 或结构化失败不会绕过预检。需要测试真实创建、编辑、评论或 merge 时,只允许使用明确的 throwaway 源分支和 PR,并在记录中写明 PR URL、number、源/目标分支和清理动作;默认验收只做 dry-run,不创建或修改真实 PR。
|
||||
|
||||
## T27 GitHub Issue/Comment 换行转义卫生扫描
|
||||
|
||||
阅读 `AGENTS.md` 和 `docs/reference/cli.md`,然后用 cli 手动测试以下内容:运行 `bun scripts/cli.ts gh help`,确认 help 中包含 `gh issue create --title <title> --body-file <file> [--label label[,label...]]...`、`gh issue read <number|owner/repo#number>`、`--raw|--full`、`gh issue scan-escape`、`gh issue cleanup-plan`、`gh issue board-row list` 和 `gh issue board-row update`,notes 中明确推荐 `--body-file`、quoted heredoc、只读 cleanup-plan、read/view shorthand、raw/full 显式完整披露、board-row update 默认 dry-run 和 `--expect-body-sha`/`--expect-updated-at` 并发保护。运行 `bun scripts/gh-cli-issue-guard-contract-test.ts`,确认 mock GitHub 覆盖 issue read/view 的 `owner/repo#number` shorthand、`--raw` 完整披露、冲突 `--repo` 结构化失败、污染命中、说明性 `\n` 命中不误报、短 body/null body guard、body-file dry-run 写入路径、`issue create --label cli,infra --label ops --dry-run` labels 解析和 request plan、真实 create REST payload labels、missing label 的结构化 `validation-failed`、comment-id/body-id 定位和 cleanupSuggestions、board-row list/get 复用 #20 表格解析、board-row update 给出 old/new row、body SHA、guard 结果、表格管道转义、默认 dry-run 不写入、带 `--expect-body-sha` 时只对 mock server PATCH、以及 board-row move 迁移支持。对真实仓库只允许运行 `bun scripts/cli.ts gh issue scan-escape --repo pikasTech/unidesk --limit <N> --dry-run`、`bun scripts/cli.ts gh issue cleanup-plan --repo pikasTech/unidesk --limit <N>`、`bun scripts/cli.ts gh issue board-row list --repo pikasTech/unidesk --board-issue 20 --state open --dry-run` 或 `bun scripts/cli.ts gh issue board-row get <issueNumber> --repo pikasTech/unidesk --board-issue 20` 这类只读命令;不得运行真实历史评论清理、不得真实改写 #20/#24 正文,除非另有明确人工指令并先审阅 dry-run 输出和 body SHA。
|
||||
阅读 `AGENTS.md` 和 `docs/reference/cli.md`,然后用 cli 手动测试以下内容:运行 `bun scripts/cli.ts gh help`,确认 help 中包含 `gh issue create --title <title> --body-file <file> [--label label[,label...]]...`、`gh issue view <number|url|owner/repo#number>`、`gh issue read ... [compatibility alias for issue view]`、`--number N compat`、`--raw|--full`、`gh issue scan-escape`、`gh issue cleanup-plan`、`gh issue board-row list` 和 `gh issue board-row update`,notes 中明确推荐 `--body-file`、quoted heredoc、只读 cleanup-plan、view/read shorthand、GitHub URL、`--number` 兼容提示、raw/full 显式完整披露、board-row update 默认 dry-run 和 `--expect-body-sha`/`--expect-updated-at` 并发保护。运行 `bun scripts/gh-cli-issue-guard-contract-test.ts`,确认 mock GitHub 覆盖 issue view/read 的 GitHub URL、`owner/repo#number` shorthand、`--number` 兼容提示、`--raw` 完整披露、冲突 `--repo` 结构化失败、污染命中、说明性 `\n` 命中不误报、短 body/null body guard、body-file dry-run 写入路径、`issue create --label cli,infra --label ops --dry-run` labels 解析和 request plan、真实 create REST payload labels、missing label 的结构化 `validation-failed`、comment-id/body-id 定位、comment delete --number commentId dry-run 和 cleanupSuggestions、board-row list/get 复用 #20 表格解析、board-row update 给出 old/new row、body SHA、guard 结果、表格管道转义、默认 dry-run 不写入、带 `--expect-body-sha` 时只对 mock server PATCH、以及 board-row move 迁移支持。对真实仓库只允许运行 `bun scripts/cli.ts gh issue scan-escape --repo pikasTech/unidesk --limit <N> --dry-run`、`bun scripts/cli.ts gh issue cleanup-plan --repo pikasTech/unidesk --limit <N>`、`bun scripts/cli.ts gh issue board-row list --repo pikasTech/unidesk --board-issue 20 --state open --dry-run` 或 `bun scripts/cli.ts gh issue board-row get <issueNumber> --repo pikasTech/unidesk --board-issue 20` 这类只读命令;不得运行真实历史评论清理、不得真实改写 #20/#24 正文,除非另有明确人工指令并先审阅 dry-run 输出和 body SHA。
|
||||
|
||||
## T28 Host Codex Commander Skeleton Contract
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -649,8 +649,8 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const usage = Array.isArray(helpData.usage) ? helpData.usage.map((value) => String(value)) : [];
|
||||
const notes = Array.isArray(helpData.notes) ? helpData.notes.map((value) => String(value)) : [];
|
||||
assertCondition(usage.some((line) => line.includes("gh issue list")), "gh help should list issue list", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh issue read")), "gh help should list issue read", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh issue view")), "gh help should list issue view", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh issue view") && line.includes("number|url|owner/repo#number")), "gh help should list standard issue view target forms", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh issue read") && line.includes("compatibility alias for issue view")), "gh help should list issue read compatibility alias", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh issue comment create") && line.includes("--body <short-text>")), "gh help should list short inline issue comment body", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("owner/repo#number") && line.includes("--raw|--full")), "gh help should document issue shorthand and raw/full disclosure", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh issue board-row list")), "gh help should list board-row list", { usage });
|
||||
@@ -660,9 +660,10 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
assertCondition(usage.some((line) => line.includes("gh issue board-row move")), "gh help should list board-row move", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh issue board-row delete")), "gh help should list board-row delete", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh issue list") && line.includes("--search text")), "gh help should list issue list search", { usage });
|
||||
assertCondition(notes.some((line) => line.includes("canonical read path")), "gh help should state issue read is canonical", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("compatibility alias")), "gh help should state issue view is alias", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("owner/repo#number shorthand")), "gh help should explain read/view shorthand", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("issue view is the canonical")), "gh help should state issue view is canonical", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("read remains") && line.includes("compatibility alias")), "gh help should state issue read is alias", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("GitHub issue URLs") && line.includes("owner/repo#number shorthand")), "gh help should explain issue view/read URL and shorthand targets", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("--number is accepted on single issue/comment numeric target commands") && line.includes("Comment delete treats --number as commentId")), "gh help should document issue --number compatibility scope", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("--raw and --full are explicit full-disclosure aliases")), "gh help should explain raw/full read disclosure", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("issue comment create accepts --body-file <file|->") && line.includes("--body only for short single-line text")), "gh help should document issue comment stdin and inline safety limits", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("board-row update changes one table cell")), "gh help should describe board-row update safety", { notes });
|
||||
@@ -1256,12 +1257,33 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
assertCondition(!("comments" in selectedJson), "--json body should not imply comments field", selectedJson);
|
||||
|
||||
const viewBody = await runCli(["gh", "issue", "view", "20", "--repo", "pikasTech/unidesk", "--json", "body"], env);
|
||||
assertCondition(viewBody.status === 0, "issue view alias should succeed", viewBody.json ?? { stdout: viewBody.stdout });
|
||||
assertCondition(viewBody.status === 0, "issue view should succeed as canonical read path", viewBody.json ?? { stdout: viewBody.stdout });
|
||||
const viewBodyData = dataOf(viewBody.json ?? {});
|
||||
const viewIssue = viewBodyData.issue as JsonRecord;
|
||||
assertCondition(typeof viewIssue.body === "string" && viewIssue.body.includes("## 看板(OPEN)"), "issue view alias should keep .data.issue.body readable", viewBodyData);
|
||||
assertCondition(typeof viewIssue.body === "string" && viewIssue.body.includes("## 看板(OPEN)"), "issue view should keep .data.issue.body readable", viewBodyData);
|
||||
const viewSelectedJson = viewBodyData.json as JsonRecord;
|
||||
assertCondition(typeof viewSelectedJson.body === "string" && viewSelectedJson.body === readIssue.body, "issue view alias should preserve selected json body", viewBodyData);
|
||||
assertCondition(typeof viewSelectedJson.body === "string" && viewSelectedJson.body === readIssue.body, "issue view should preserve selected json body", viewBodyData);
|
||||
|
||||
const issueUrlView = await runCli(["gh", "issue", "view", "https://github.com/pikasTech/HWLAB/issues/7", "--json", "body,title,state"], env);
|
||||
assertCondition(issueUrlView.status === 0, "issue view should accept GitHub issue URL target", issueUrlView.json ?? { stdout: issueUrlView.stdout });
|
||||
const issueUrlViewData = dataOf(issueUrlView.json ?? {});
|
||||
assertCondition(issueUrlViewData.repo === "pikasTech/HWLAB", "issue URL target should derive repo", issueUrlViewData);
|
||||
assertCondition((issueUrlViewData.issue as JsonRecord).number === 7, "issue URL target should derive issue number", issueUrlViewData);
|
||||
const issueUrlDisclosure = issueUrlViewData.disclosure as JsonRecord;
|
||||
assertCondition(issueUrlDisclosure.shorthand && (issueUrlDisclosure.shorthand as JsonRecord).source === "github-url", "issue URL target should be disclosed", issueUrlDisclosure);
|
||||
|
||||
const issuePrUrlMismatch = await runCli(["gh", "issue", "view", "https://github.com/pikasTech/HWLAB/pull/7", "--json", "body"], env);
|
||||
assertCondition(issuePrUrlMismatch.status !== 0, "issue view should reject PR URLs", issuePrUrlMismatch.json ?? { stdout: issuePrUrlMismatch.stdout });
|
||||
const issuePrUrlMismatchData = failedDataOf(issuePrUrlMismatch.json ?? {});
|
||||
assertCondition(failureMessageOf(issuePrUrlMismatchData).includes("GitHub pr URL"), "issue view PR URL mismatch should be explicit", issuePrUrlMismatchData);
|
||||
|
||||
const issueNumberOption = await runCli(["gh", "issue", "view", "--repo", "pikasTech/HWLAB", "--number", "7", "--json", "body"], env);
|
||||
assertCondition(issueNumberOption.status === 0, "issue view should accept --number compatibility alias", issueNumberOption.json ?? { stdout: issueNumberOption.stdout });
|
||||
const issueNumberOptionData = dataOf(issueNumberOption.json ?? {});
|
||||
assertCondition(issueNumberOptionData.repo === "pikasTech/HWLAB", "issue view --number should preserve explicit repo", issueNumberOptionData);
|
||||
assertCondition((issueNumberOptionData.issue as JsonRecord).number === 7, "issue view --number should read the requested issue", issueNumberOptionData);
|
||||
const issueNumberOptionHint = issueNumberOptionData.standardSyntaxHint as JsonRecord;
|
||||
assertCondition(issueNumberOptionHint.compatibility === true && String(issueNumberOptionHint.standardCommand ?? "").includes("gh issue view 7 --repo pikasTech/HWLAB"), "issue view --number should return standard syntax hint", issueNumberOptionHint);
|
||||
|
||||
const shorthandRaw = await runCli(["gh", "issue", "view", "pikasTech/HWLAB#7", "--raw"], env);
|
||||
assertCondition(shorthandRaw.status === 0, "issue view should accept owner/repo#number shorthand with --raw", shorthandRaw.json ?? { stdout: shorthandRaw.stdout });
|
||||
@@ -1281,7 +1303,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
assertCondition(shorthandConflictData.degradedReason === "validation-failed", "conflicting --repo should be validation-failed", shorthandConflictData);
|
||||
assertCondition(String(shorthandConflictData.message ?? "").includes("resolves to repo pikasTech/HWLAB"), "conflict message should name the derived repo", shorthandConflictData);
|
||||
const issueConflictCommands = shorthandConflictData.supportedCommands as string[];
|
||||
assertCondition(Array.isArray(issueConflictCommands) && issueConflictCommands.some((command) => command === "bun scripts/cli.ts gh issue read 7 --repo pikasTech/HWLAB --json body,title,state,closed,closedAt,comments,number,url,author,createdAt,updatedAt"), "conflict should include the exact supported issue read command", shorthandConflictData);
|
||||
assertCondition(Array.isArray(issueConflictCommands) && issueConflictCommands.some((command) => command === "bun scripts/cli.ts gh issue view 7 --repo pikasTech/HWLAB --json body,title,state,closed,closedAt,comments,number,url,author,createdAt,updatedAt"), "conflict should include the exact supported issue view command", shorthandConflictData);
|
||||
|
||||
const rawIssueList = await runCli(["gh", "issue", "list", "--raw"], env);
|
||||
assertCondition(rawIssueList.status === 0, "issue list --raw should be a supported explicit list disclosure path", rawIssueList.json ?? { stdout: rawIssueList.stdout });
|
||||
@@ -1576,11 +1598,17 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const replaceDisclosure = replaceData.disclosure as JsonRecord;
|
||||
const replaceReadCommands = replaceData.readCommands as JsonRecord;
|
||||
assertCondition(replaceDisclosure.bodyOmitted === true && replaceDisclosure.dryRunBoundedPreview === true, "issue update dry-run should disclose compact body policy", replaceDisclosure);
|
||||
assertCondition(typeof replaceReadCommands.full === "string" && String(replaceReadCommands.full).includes("gh issue read 20"), "issue update dry-run should expose full body drill-down", replaceReadCommands);
|
||||
assertCondition(typeof replaceReadCommands.full === "string" && String(replaceReadCommands.full).includes("gh issue view 20"), "issue update dry-run should expose full body drill-down", replaceReadCommands);
|
||||
const replaceWouldPatch = replaceData.wouldPatch as JsonRecord;
|
||||
assertCondition(typeof replaceWouldPatch.bodySha === "string" && String(replaceWouldPatch.bodySha).length === 64, "issue update dry-run should include wouldPatch body sha", replaceWouldPatch);
|
||||
assertCondition(Number(replaceWouldPatch.bodyChars ?? 0) === Number(replaceData.bodyChars ?? 0), "issue update dry-run wouldPatch should include final body chars", replaceWouldPatch);
|
||||
|
||||
const replaceNumberDryRun = await runCli(["gh", "issue", "update", "--number", "20", "--repo", "pikasTech/unidesk", "--mode", "replace", "--body-file", safeFile, "--dry-run"], env);
|
||||
assertCondition(replaceNumberDryRun.status === 0, "issue update should accept --number compatibility alias", replaceNumberDryRun.json ?? { stdout: replaceNumberDryRun.stdout });
|
||||
const replaceNumberData = dataOf(replaceNumberDryRun.json ?? {});
|
||||
const replaceNumberHint = replaceNumberData.standardSyntaxHint as JsonRecord;
|
||||
assertCondition(String(replaceNumberHint.standardCommand ?? "").includes("gh issue update 20 --repo pikasTech/unidesk"), "issue update --number should return standard syntax hint", replaceNumberHint);
|
||||
|
||||
const compactLongBody = Array.from({ length: 260 }, (_, index) => `compact-success-line-${String(index + 1).padStart(4, "0")} ${"x".repeat(80)}`).join("\n");
|
||||
const compactLongFile = join(tmp, "compact-long-body.md");
|
||||
writeFileSync(compactLongFile, compactLongBody, "utf8");
|
||||
@@ -1604,7 +1632,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const compactDisclosure = compactUpdateData.disclosure as JsonRecord;
|
||||
assertCondition(compactDisclosure.bodyOmitted === true && compactDisclosure.fullBodyIncluded === false && compactDisclosure.defaultCompact === true, "compact update disclosure should be explicit", compactDisclosure);
|
||||
const compactCommands = compactUpdateData.readCommands as JsonRecord;
|
||||
assertCondition(String(compactCommands.body ?? "").includes("gh issue read 7 --repo pikasTech/HWLAB --json body"), "compact update should expose body read command", compactCommands);
|
||||
assertCondition(String(compactCommands.body ?? "").includes("gh issue view 7 --repo pikasTech/HWLAB --json body"), "compact update should expose body view command", compactCommands);
|
||||
assertCondition(String(compactCommands.full ?? "").includes("--full") && String(compactCommands.raw ?? "").includes("--raw"), "compact update should expose full/raw drill-down", compactCommands);
|
||||
const compactUpdatePatchCount = mock.requests.slice(compactUpdateRequestCountBefore).filter((request) => request.method === "PATCH" && request.url === "/repos/pikasTech/HWLAB/issues/7").length;
|
||||
assertCondition(compactUpdatePatchCount === 1, "compact update should PATCH GitHub exactly once", { requests: mock.requests.slice(compactUpdateRequestCountBefore) });
|
||||
@@ -1655,7 +1683,7 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
assertCondition(Number(inlineDryRunData.bodyChars ?? 0) === inlineBody.length && typeof inlineDryRunData.bodySha === "string", "inline issue comment dry-run should expose bodyChars/bodySha", inlineDryRunData);
|
||||
assertCondition(String(inlineDryRunData.bodyPreview ?? "") === inlineBody, "inline issue comment dry-run should provide bounded preview for short text", inlineDryRunData);
|
||||
const inlineDryRunReadCommands = inlineDryRunData.readCommands as JsonRecord;
|
||||
assertCondition(String(inlineDryRunReadCommands.comments ?? "").includes("gh issue read 36") && String(inlineDryRunReadCommands.comments ?? "").includes("--json comments"), "inline issue comment dry-run should expose comment read command", inlineDryRunReadCommands);
|
||||
assertCondition(String(inlineDryRunReadCommands.comments ?? "").includes("gh issue view 36") && String(inlineDryRunReadCommands.comments ?? "").includes("--json comments"), "inline issue comment dry-run should expose comment view command", inlineDryRunReadCommands);
|
||||
const inlineDryRunWriteCount = mock.requests.slice(inlineDryRunRequestCountBefore).filter((request) => request.method === "POST" && request.url.includes("/comments")).length;
|
||||
assertCondition(inlineDryRunWriteCount === 0, "inline issue comment dry-run must not POST GitHub", { requests: mock.requests.slice(inlineDryRunRequestCountBefore) });
|
||||
|
||||
@@ -1751,6 +1779,13 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
const commentDeleteDryRunData = dataOf(commentDeleteDryRun.json ?? {});
|
||||
assertCondition(commentDeleteDryRunData.command === "issue comment delete" && commentDeleteDryRunData.planned === true, "comment delete dry-run should plan DELETE", commentDeleteDryRunData);
|
||||
|
||||
const commentDeleteNumberDryRun = await runCli(["gh", "issue", "comment", "delete", "--number", "9001", "--repo", "pikasTech/unidesk", "--dry-run"], env);
|
||||
assertCondition(commentDeleteNumberDryRun.status === 0, "issue comment delete should accept --number commentId compatibility alias", commentDeleteNumberDryRun.json ?? { stdout: commentDeleteNumberDryRun.stdout });
|
||||
const commentDeleteNumberData = dataOf(commentDeleteNumberDryRun.json ?? {});
|
||||
assertCondition(commentDeleteNumberData.commentId === 9001 && commentDeleteNumberData.standardSyntaxHint, "issue comment delete --number should return commentId and standard syntax hint", commentDeleteNumberData);
|
||||
const commentDeleteNumberHint = commentDeleteNumberData.standardSyntaxHint as JsonRecord;
|
||||
assertCondition(String(commentDeleteNumberHint.standardCommand ?? "").includes("gh issue comment delete 9001 --repo pikasTech/unidesk"), "issue comment delete --number should point to positional commentId syntax", commentDeleteNumberHint);
|
||||
|
||||
const commentDelete = await runCli(["gh", "issue", "comment", "delete", "9001", "--repo", "pikasTech/unidesk"], env);
|
||||
assertCondition(commentDelete.status === 0, "issue comment delete should succeed", commentDelete.json ?? { stdout: commentDelete.stdout });
|
||||
const commentDeleteData = dataOf(commentDelete.json ?? {});
|
||||
@@ -1764,15 +1799,16 @@ export async function runGhCliIssueGuardContract(): Promise<JsonRecord> {
|
||||
return {
|
||||
ok: true,
|
||||
checks: [
|
||||
"issue read --json body preserves .data.issue.body",
|
||||
"issue view remains a compatibility alias",
|
||||
"issue read/view accept owner/repo#number shorthand and reject conflicting --repo",
|
||||
"issue read/view --raw is explicit full disclosure",
|
||||
"issue view --json body preserves .data.issue.body",
|
||||
"issue read remains a compatibility alias",
|
||||
"issue view/read accept GitHub URL and owner/repo#number targets and reject conflicting --repo",
|
||||
"issue single numeric target commands accept --number compatibility with a standard syntax hint",
|
||||
"issue view/read --raw is explicit full disclosure",
|
||||
"issue list supports state/limit/json with stable selected fields",
|
||||
"issue list positional owner/repo targets the requested repo and conflicting --repo fails",
|
||||
"acceptance issue list command succeeds under mock GitHub",
|
||||
"issue list default fields include labels and filter pull requests",
|
||||
"large gh issue read output is dumped to a temp file with bounded stdout and head/tail metadata",
|
||||
"large gh issue view/read output is dumped to a temp file with bounded stdout and head/tail metadata",
|
||||
"issue scan-escape classifies pollution, explanatory mentions, and body risks",
|
||||
"issue cleanup-plan remains dry-run with body/comment cleanup suggestions",
|
||||
"issue board-audit returns read-only board structure, disables OPEN/CLOSED coverage validation, and keeps compatibility fields empty without writes",
|
||||
|
||||
@@ -288,6 +288,10 @@ function failedDataOf(response: JsonRecord): JsonRecord {
|
||||
return response.data as JsonRecord;
|
||||
}
|
||||
|
||||
function failureMessageOf(data: JsonRecord): string {
|
||||
return String((data.details as JsonRecord | undefined)?.message ?? data.message ?? "");
|
||||
}
|
||||
|
||||
export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
const help = await runCli(["gh", "help"]);
|
||||
assertCondition(help.status === 0, "gh help should succeed", help.json ?? { stdout: help.stdout });
|
||||
@@ -295,9 +299,8 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
const usage = Array.isArray(helpData.usage) ? helpData.usage.map((value) => String(value)) : [];
|
||||
const notes = Array.isArray(helpData.notes) ? helpData.notes.map((value) => String(value)) : [];
|
||||
assertCondition(usage.some((line) => line.includes("gh pr list")), "gh help should list pr list", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr read")), "gh help should list pr read", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr view")), "gh help should list pr view", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr read") && line.includes("owner/repo#number") && line.includes("--raw|--full")), "gh help should document pr shorthand and raw/full disclosure", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr view") && line.includes("number|url|owner/repo#number") && line.includes("--raw|--full")), "gh help should document standard pr view targets and raw/full disclosure", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr read") && line.includes("compatibility alias for pr view")), "gh help should list pr read compatibility alias", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh preflight")), "gh help should list top-level preflight alias", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr preflight")), "gh help should list pr preflight", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr create")), "gh help should list pr create", { usage });
|
||||
@@ -305,9 +308,10 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
assertCondition(usage.some((line) => line.includes("gh pr comment")), "gh help should list pr comment", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("gh pr list") && line.includes("--state open|closed|all")), "gh help should document pr list state filtering", { usage });
|
||||
assertCondition(usage.some((line) => line.includes("mergedAt") && line.includes("mergeCommit")), "gh help should document merged PR closeout fields", { usage });
|
||||
assertCondition(notes.some((line) => line.includes("canonical read path")), "gh help should state pr read is canonical", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("compatibility alias")), "gh help should state pr view is alias", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("PR read/view accept owner/repo#number shorthand")), "gh help should explain pr read/view shorthand", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("PR view is the canonical")), "gh help should state pr view is canonical", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("read remains") && line.includes("compatibility alias")), "gh help should state pr read is alias", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("GitHub PR URLs") && line.includes("owner/repo#number shorthand")), "gh help should explain pr view/read URL and shorthand targets", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("--number is accepted on single PR/comment numeric target commands") && line.includes("PR comment delete treats --number as commentId")), "gh help should document --number compatibility hint", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("--raw and --full are explicit full-disclosure aliases")), "gh help should explain raw/full read disclosure", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("PR list defaults to --state all")), "gh help should document pr list default state", { notes });
|
||||
assertCondition(notes.some((line) => line.includes("stateDetail") && line.includes("mergedAt")), "gh help should describe closeout field normalization", { notes });
|
||||
@@ -383,15 +387,17 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
assertCondition(selected.body === "PR body" && selected.title === "contract PR", "pr read --json should select fields", readData);
|
||||
|
||||
const readNumberAlias = await runCli(["gh", "pr", "read", "--repo", "pikasTech/HWLAB", "--number", "7", "--json", "body,title,state,head,base"], env);
|
||||
assertCondition(readNumberAlias.status === 0, "pr read should accept --number alias", readNumberAlias.json ?? { stdout: readNumberAlias.stdout });
|
||||
assertCondition(readNumberAlias.status === 0, "pr read should accept --number compatibility alias", readNumberAlias.json ?? { stdout: readNumberAlias.stdout });
|
||||
const readNumberAliasData = dataOf(readNumberAlias.json ?? {});
|
||||
assertCondition(readNumberAliasData.repo === "pikasTech/HWLAB", "pr read --number should preserve explicit repo", readNumberAliasData);
|
||||
const readNumberAliasPr = readNumberAliasData.pullRequest as JsonRecord;
|
||||
assertCondition(readNumberAliasPr.number === 7 && readNumberAliasPr.url === "https://github.com/pikasTech/HWLAB/pull/7", "pr read --number should read the requested PR", readNumberAliasData);
|
||||
const readNumberAliasDisclosure = readNumberAliasData.disclosure as JsonRecord;
|
||||
assertCondition(String(readNumberAliasDisclosure.compatibilityHint ?? "").includes("standard gh syntax") && String(readNumberAliasDisclosure.standardCommand ?? "").includes("gh pr view 7 --repo pikasTech/HWLAB"), "pr read --number should return standard syntax hint", readNumberAliasDisclosure);
|
||||
assertCondition(mock.requests.some((request) => request.method === "GET" && request.url === "/repos/pikasTech/HWLAB/pulls/7"), "pr read --number should call explicit repo REST path", mock.requests);
|
||||
|
||||
const numberAliasUnsupported = await runCli(["gh", "pr", "list", "--repo", "pikasTech/unidesk", "--number", "7"], env);
|
||||
assertCondition(numberAliasUnsupported.status !== 0, "--number should not be silently ignored outside pr read/view", numberAliasUnsupported.json ?? { stdout: numberAliasUnsupported.stdout });
|
||||
assertCondition(numberAliasUnsupported.status !== 0, "--number should not be silently ignored outside standard view/read", numberAliasUnsupported.json ?? { stdout: numberAliasUnsupported.stdout });
|
||||
const numberAliasUnsupportedData = failedDataOf(numberAliasUnsupported.json ?? {});
|
||||
assertCondition(numberAliasUnsupportedData.degradedReason === "validation-failed", "unsupported --number should be validation-failed", numberAliasUnsupportedData);
|
||||
|
||||
@@ -404,11 +410,24 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
assertCondition(openLifecycleJson.merged === false && openLifecycleJson.mergedAt === null && openLifecycleJson.mergeCommit === null, "open pr should expose merged=false", openLifecycleData);
|
||||
|
||||
const view = await runCli(["gh", "pr", "view", "42", "--repo", "pikasTech/unidesk", "--json", "body,title,state,head,base"], env);
|
||||
assertCondition(view.status === 0, "pr view alias should succeed through REST", view.json ?? { stdout: view.stdout });
|
||||
assertCondition(view.status === 0, "pr view should succeed as canonical read path", view.json ?? { stdout: view.stdout });
|
||||
const viewData = dataOf(view.json ?? {});
|
||||
assertCondition((viewData.pullRequest as JsonRecord).number === 42, "pr view alias should expose PR details", viewData);
|
||||
assertCondition((viewData.pullRequest as JsonRecord).number === 42, "pr view should expose PR details", viewData);
|
||||
const viewSelected = viewData.json as JsonRecord;
|
||||
assertCondition(viewSelected.body === "PR body" && viewSelected.title === "contract PR", "pr view alias should preserve selected fields", viewData);
|
||||
assertCondition(viewSelected.body === "PR body" && viewSelected.title === "contract PR", "pr view should preserve selected fields", viewData);
|
||||
|
||||
const prUrlView = await runCli(["gh", "pr", "view", "https://github.com/pikasTech/HWLAB/pull/7", "--json", "body,title,state,head,base"], env);
|
||||
assertCondition(prUrlView.status === 0, "pr view should accept GitHub PR URL target", prUrlView.json ?? { stdout: prUrlView.stdout });
|
||||
const prUrlViewData = dataOf(prUrlView.json ?? {});
|
||||
assertCondition(prUrlViewData.repo === "pikasTech/HWLAB", "PR URL target should derive repo", prUrlViewData);
|
||||
assertCondition((prUrlViewData.pullRequest as JsonRecord).number === 7, "PR URL target should derive PR number", prUrlViewData);
|
||||
const prUrlDisclosure = prUrlViewData.disclosure as JsonRecord;
|
||||
assertCondition(prUrlDisclosure.shorthand && (prUrlDisclosure.shorthand as JsonRecord).source === "github-url", "PR URL target should be disclosed", prUrlDisclosure);
|
||||
|
||||
const prIssueUrlMismatch = await runCli(["gh", "pr", "view", "https://github.com/pikasTech/HWLAB/issues/7", "--json", "body"], env);
|
||||
assertCondition(prIssueUrlMismatch.status !== 0, "pr view should reject issue URLs", prIssueUrlMismatch.json ?? { stdout: prIssueUrlMismatch.stdout });
|
||||
const prIssueUrlMismatchData = failedDataOf(prIssueUrlMismatch.json ?? {});
|
||||
assertCondition(failureMessageOf(prIssueUrlMismatchData).includes("GitHub issue URL"), "pr view issue URL mismatch should be explicit", prIssueUrlMismatchData);
|
||||
|
||||
const shorthandRaw = await runCli(["gh", "pr", "view", "pikasTech/HWLAB#7", "--raw"], env);
|
||||
assertCondition(shorthandRaw.status === 0, "pr view should accept owner/repo#number shorthand with --raw", shorthandRaw.json ?? { stdout: shorthandRaw.stdout });
|
||||
@@ -428,7 +447,7 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
assertCondition(shorthandConflictData.degradedReason === "validation-failed", "pr conflicting --repo should be validation-failed", shorthandConflictData);
|
||||
assertCondition(String(shorthandConflictData.message ?? "").includes("resolves to repo pikasTech/HWLAB"), "pr conflict message should name the derived repo", shorthandConflictData);
|
||||
const prConflictCommands = shorthandConflictData.supportedCommands as string[];
|
||||
assertCondition(Array.isArray(prConflictCommands) && prConflictCommands.some((command) => command === "bun scripts/cli.ts gh pr read 7 --repo pikasTech/HWLAB --json body,title,state,stateDetail,closed,closedAt,merged,mergedAt,mergeCommit,head,base,draft,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup"), "pr conflict should include exact supported read command", shorthandConflictData);
|
||||
assertCondition(Array.isArray(prConflictCommands) && prConflictCommands.some((command) => command === "bun scripts/cli.ts gh pr view 7 --repo pikasTech/HWLAB --json body,title,state,stateDetail,closed,closedAt,merged,mergedAt,mergeCommit,head,base,draft,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup"), "pr conflict should include exact supported view command", shorthandConflictData);
|
||||
|
||||
const closeout = await runCli(["gh", "pr", "view", "42", "--repo", "pikasTech/unidesk", "--json", "mergeable,mergeStateStatus,statusCheckRollup,headRefName,baseRefName"], env);
|
||||
assertCondition(closeout.status === 0, "pr view closeout metadata fields should not be rejected", closeout.json ?? { stdout: closeout.stdout });
|
||||
@@ -648,6 +667,12 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
assertCondition(appendBody.mode === "append", "pr append mode should be explicit", updateAppendData);
|
||||
assertCondition(finalBody.containsBackticks === true && finalBody.containsMarkdownTable === true, "pr append should preserve markdown signals", updateAppendData);
|
||||
|
||||
const updateNumberDryRun = await runCli(["gh", "pr", "update", "--number", "42", "--repo", "pikasTech/unidesk", "--mode", "replace", "--body-file", bodyFile, "--dry-run"], env2);
|
||||
assertCondition(updateNumberDryRun.status === 0, "pr update should accept --number compatibility alias", updateNumberDryRun.json ?? { stdout: updateNumberDryRun.stdout });
|
||||
const updateNumberData = dataOf(updateNumberDryRun.json ?? {});
|
||||
const updateNumberHint = updateNumberData.standardSyntaxHint as JsonRecord;
|
||||
assertCondition(String(updateNumberHint.standardCommand ?? "").includes("gh pr update 42 --repo pikasTech/unidesk"), "pr update --number should return standard syntax hint", updateNumberHint);
|
||||
|
||||
const editStdinBody = "stdin line\n`stdin code`\n| c | d |\n";
|
||||
const beforeEditRequests = mock2.requests.length;
|
||||
const editStdin = await runCli(["gh", "pr", "edit", "42", "--repo", "pikasTech/unidesk", "--title", "stdin title", "--body-file", "-"], env2, editStdinBody);
|
||||
@@ -701,6 +726,12 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
assertCondition(commentDelete.status === 0, "pr comment delete should succeed", commentDelete.json ?? { stdout: commentDelete.stdout });
|
||||
const commentDeleteData = dataOf(commentDelete.json ?? {});
|
||||
assertCondition(commentDeleteData.deleted === true, "pr comment delete should report deleted", commentDeleteData);
|
||||
|
||||
const commentDeleteNumber = await runCli(["gh", "pr", "comment", "delete", "--number", "9101", "--repo", "pikasTech/unidesk", "--dry-run"], env2);
|
||||
assertCondition(commentDeleteNumber.status === 0, "pr comment delete should accept --number commentId compatibility alias", commentDeleteNumber.json ?? { stdout: commentDeleteNumber.stdout });
|
||||
const commentDeleteNumberData = dataOf(commentDeleteNumber.json ?? {});
|
||||
const commentDeleteNumberHint = commentDeleteNumberData.standardSyntaxHint as JsonRecord;
|
||||
assertCondition(commentDeleteNumberData.commentId === 9101 && String(commentDeleteNumberHint.standardCommand ?? "").includes("gh pr comment delete 9101 --repo pikasTech/unidesk"), "pr comment delete --number should point to positional commentId syntax", commentDeleteNumberData);
|
||||
} finally {
|
||||
await mock2.close();
|
||||
}
|
||||
@@ -731,9 +762,9 @@ export async function runGhCliPrContract(): Promise<JsonRecord> {
|
||||
"gh help lists pr create/comment",
|
||||
"pr list/read/view work through REST with token and no gh binary dependency",
|
||||
"pr list positional owner/repo targets the requested repo and conflicting --repo fails",
|
||||
"pr read supports --number alias without silently ignoring it elsewhere",
|
||||
"pr read/view accept owner/repo#number shorthand and reject conflicting --repo",
|
||||
"pr read/view --raw is explicit full disclosure",
|
||||
"pr single numeric target commands accept --number compatibility with a standard syntax hint",
|
||||
"pr view/read accept GitHub URL and owner/repo#number targets and reject conflicting --repo",
|
||||
"pr view/read --raw is explicit full disclosure",
|
||||
"pr list rejects closeout fields and points to pr view",
|
||||
"pr read normalizes open and merged lifecycle fields from REST",
|
||||
"GitHub DNS/API transients are retryable and distinct from auth or PR semantic failures",
|
||||
|
||||
+313
-158
@@ -382,6 +382,9 @@ interface GitHubShorthandReference {
|
||||
input: string;
|
||||
repo: string;
|
||||
number: number;
|
||||
source?: "owner-repo-number" | "github-url" | "number-option";
|
||||
urlKind?: "issue" | "pr";
|
||||
standardCommand?: string;
|
||||
}
|
||||
|
||||
interface GitHubResolvedNumberReference {
|
||||
@@ -695,6 +698,21 @@ function isPrReadCommand(sub: string | undefined): boolean {
|
||||
return sub === "read" || sub === "view";
|
||||
}
|
||||
|
||||
function allowsNumberTargetAlias(top: string | undefined, sub: string | undefined, third: string | undefined): boolean {
|
||||
if (top === "preflight") return true;
|
||||
if (top === "issue") {
|
||||
if (sub === "read" || sub === "view" || sub === "edit" || sub === "update" || sub === "close" || sub === "reopen" || sub === "delete") return true;
|
||||
if (sub === "comment") return true;
|
||||
if (sub === "board-row" && ["get", "update", "add", "move", "delete", "upsert"].includes(third ?? "")) return true;
|
||||
return false;
|
||||
}
|
||||
if (top === "pr") {
|
||||
if (sub === "read" || sub === "view" || sub === "files" || sub === "diff" || sub === "preflight" || sub === "closeout" || sub === "edit" || sub === "update" || sub === "close" || sub === "reopen" || sub === "merge" || sub === "delete") return true;
|
||||
if (sub === "comment") return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function parseIssueListState(args: string[]): IssueListState {
|
||||
const raw = optionValue(args, "--state") ?? "open";
|
||||
if ((ISSUE_LIST_STATES as readonly string[]).includes(raw)) return raw as IssueListState;
|
||||
@@ -889,24 +907,92 @@ function parsePositionalNumberForCommand(repo: string, args: string[], startInde
|
||||
return parseNumberForCommand(repo, targets[0], label);
|
||||
}
|
||||
|
||||
function resolvePositionalPrReference(args: string[], startIndex: number, label: string, options: GitHubOptions): GitHubResolvedNumberReference | GitHubCommandResult {
|
||||
function resolvedNumberOptionReference(kind: "issue" | "pr", command: string, repo: string, raw: string): GitHubResolvedNumberReference | GitHubCommandResult {
|
||||
const number = parseNumberForCommand(repo, raw, command);
|
||||
if (typeof number !== "number") return number;
|
||||
return {
|
||||
repo,
|
||||
number,
|
||||
shorthand: {
|
||||
input: `--number ${number}`,
|
||||
repo,
|
||||
number,
|
||||
source: "number-option",
|
||||
standardCommand: `bun scripts/cli.ts gh ${command} ${number} --repo ${repo}`,
|
||||
urlKind: kind,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePositionalNumberReference(kind: "issue" | "pr", args: string[], startIndex: number, label: string, options: GitHubOptions): GitHubResolvedNumberReference | GitHubCommandResult {
|
||||
const targets = positionalArgs(args.slice(startIndex));
|
||||
const prOption = optionValue(args, "--pr");
|
||||
if (targets.length > 0 && prOption !== undefined) {
|
||||
return validationError(label, options.repo, `${label} accepts either a positional PR target or --pr, not both`, {
|
||||
const numberOption = optionValue(args, "--number");
|
||||
if (targets.length > 0 && numberOption !== undefined) {
|
||||
return validationError(label, options.repo, `${label} accepts either a positional numeric target or --number, not both`, {
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh ${label} <number> --repo ${options.repo}`,
|
||||
`bun scripts/cli.ts gh ${label} --pr <number> --repo ${options.repo}`,
|
||||
`bun scripts/cli.ts gh ${label} --number <number> --repo ${options.repo}`,
|
||||
`bun scripts/cli.ts gh ${label} ${options.repo}#<number>`,
|
||||
],
|
||||
});
|
||||
}
|
||||
if (numberOption !== undefined) return resolvedNumberOptionReference(kind, label, options.repo, numberOption);
|
||||
if (targets.length !== 1) {
|
||||
return validationError(label, options.repo, `${label} requires exactly one positive integer or owner/repo#number positional argument`, {
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh ${label} <number> --repo ${options.repo}`,
|
||||
`bun scripts/cli.ts gh ${label} --number <number> --repo ${options.repo}`,
|
||||
`bun scripts/cli.ts gh ${label} ${options.repo}#<number>`,
|
||||
],
|
||||
});
|
||||
}
|
||||
const shorthand = parseOwnerRepoNumberShorthand(targets[0]);
|
||||
if (shorthand !== null) {
|
||||
const explicitRepo = optionValue(args, "--repo");
|
||||
if (explicitRepo !== undefined && explicitRepo !== shorthand.repo) {
|
||||
return validationError(label, explicitRepo, `${label} target ${shorthand.input} resolves to repo ${shorthand.repo}, but --repo ${explicitRepo} was also provided.`, {
|
||||
shorthand,
|
||||
explicitRepo,
|
||||
});
|
||||
}
|
||||
return { repo: shorthand.repo, number: shorthand.number, shorthand };
|
||||
}
|
||||
const number = parseNumberForCommand(options.repo, targets[0], label);
|
||||
if (typeof number !== "number") return number;
|
||||
return { repo: options.repo, number };
|
||||
}
|
||||
|
||||
function resolvePositionalPrReference(args: string[], startIndex: number, label: string, options: GitHubOptions): GitHubResolvedNumberReference | GitHubCommandResult {
|
||||
const targets = positionalArgs(args.slice(startIndex));
|
||||
const prOption = optionValue(args, "--pr");
|
||||
const numberOption = optionValue(args, "--number");
|
||||
if (prOption !== undefined && numberOption !== undefined) {
|
||||
return validationError(label, options.repo, `${label} accepts either --pr or --number as a compatibility alias, not both`, {
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh ${label} <number> --repo ${options.repo}`,
|
||||
`bun scripts/cli.ts gh ${label} --number <number> --repo ${options.repo}`,
|
||||
`bun scripts/cli.ts gh ${label} ${options.repo}#<number>`,
|
||||
],
|
||||
});
|
||||
}
|
||||
if (targets.length > 0 && (prOption !== undefined || numberOption !== undefined)) {
|
||||
return validationError(label, options.repo, `${label} accepts either a positional PR target or a compatibility number option, not both`, {
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh ${label} <number> --repo ${options.repo}`,
|
||||
`bun scripts/cli.ts gh ${label} --pr <number> --repo ${options.repo}`,
|
||||
`bun scripts/cli.ts gh ${label} --number <number> --repo ${options.repo}`,
|
||||
`bun scripts/cli.ts gh ${label} ${options.repo}#<number>`,
|
||||
],
|
||||
});
|
||||
}
|
||||
if (numberOption !== undefined) return resolvedNumberOptionReference("pr", label, options.repo, numberOption);
|
||||
const effectiveTargets = prOption !== undefined ? [prOption] : targets;
|
||||
if (effectiveTargets.length !== 1) {
|
||||
return validationError(label, options.repo, `${label} requires exactly one positive integer or owner/repo#number positional argument`, {
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh ${label} <number> --repo ${options.repo}`,
|
||||
`bun scripts/cli.ts gh ${label} --pr <number> --repo ${options.repo}`,
|
||||
`bun scripts/cli.ts gh ${label} --number <number> --repo ${options.repo}`,
|
||||
`bun scripts/cli.ts gh ${label} ${options.repo}#<number>`,
|
||||
],
|
||||
});
|
||||
@@ -927,12 +1013,24 @@ function resolvePositionalPrReference(args: string[], startIndex: number, label:
|
||||
return { repo: options.repo, number };
|
||||
}
|
||||
|
||||
function resolvePositionalIssueReference(args: string[], startIndex: number, label: string, options: GitHubOptions): GitHubResolvedNumberReference | GitHubCommandResult {
|
||||
function resolvePositionalIssueReference(args: string[], startIndex: number, label: string, options: GitHubOptions, allowNumberOption = true): GitHubResolvedNumberReference | GitHubCommandResult {
|
||||
const targets = positionalArgs(args.slice(startIndex));
|
||||
const numberOption = allowNumberOption ? optionValue(args, "--number") : undefined;
|
||||
if (targets.length > 0 && numberOption !== undefined) {
|
||||
return validationError(label, options.repo, `${label} accepts either a positional issue target or --number, not both`, {
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh ${label} <number> --repo ${options.repo}`,
|
||||
`bun scripts/cli.ts gh ${label} --number <number> --repo ${options.repo}`,
|
||||
`bun scripts/cli.ts gh ${label} ${options.repo}#<number>`,
|
||||
],
|
||||
});
|
||||
}
|
||||
if (numberOption !== undefined) return resolvedNumberOptionReference("issue", label, options.repo, numberOption);
|
||||
if (targets.length !== 1) {
|
||||
return validationError(label, options.repo, `${label} requires exactly one positive integer or owner/repo#number positional argument`, {
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh ${label} <number> --repo ${options.repo}`,
|
||||
...(allowNumberOption ? [`bun scripts/cli.ts gh ${label} --number <number> --repo ${options.repo}`] : []),
|
||||
`bun scripts/cli.ts gh ${label} ${options.repo}#<number>`,
|
||||
],
|
||||
});
|
||||
@@ -961,13 +1059,43 @@ function parseOwnerRepoNumberShorthand(raw: string | undefined): GitHubShorthand
|
||||
input: raw,
|
||||
repo: `${match[1]}/${match[2]}`,
|
||||
number: Number(match[3]),
|
||||
source: "owner-repo-number",
|
||||
};
|
||||
}
|
||||
|
||||
function parseGitHubIssueOrPrUrl(raw: string | undefined): GitHubShorthandReference | null {
|
||||
if (raw === undefined) return null;
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
|
||||
if (parsed.hostname.toLowerCase() !== "github.com" && parsed.hostname.toLowerCase() !== "www.github.com") return null;
|
||||
const parts = parsed.pathname.split("/").filter((part) => part.length > 0);
|
||||
if (parts.length < 4) return null;
|
||||
const [owner, repoName, kind, numberRaw] = parts;
|
||||
if (!owner || !repoName || (kind !== "issues" && kind !== "pull")) return null;
|
||||
if (!/^[1-9]\d*$/u.test(numberRaw ?? "")) return null;
|
||||
return {
|
||||
input: raw,
|
||||
repo: `${owner}/${repoName}`,
|
||||
number: Number(numberRaw),
|
||||
source: "github-url",
|
||||
urlKind: kind === "issues" ? "issue" : "pr",
|
||||
};
|
||||
}
|
||||
|
||||
function parseReadViewTarget(raw: string | undefined): GitHubShorthandReference | null {
|
||||
return parseOwnerRepoNumberShorthand(raw) ?? parseGitHubIssueOrPrUrl(raw);
|
||||
}
|
||||
|
||||
function readViewSupportedCommands(kind: "issue" | "pr", repo: string, number: number): string[] {
|
||||
return [
|
||||
`bun scripts/cli.ts gh ${kind} read ${number} --repo ${repo} --json ${readViewSupportedJsonFields(kind)}`,
|
||||
`bun scripts/cli.ts gh ${kind} view ${number} --repo ${repo} --json ${readViewSupportedJsonFields(kind)}`,
|
||||
`bun scripts/cli.ts gh ${kind} view ${repo}#${number} --raw`,
|
||||
`bun scripts/cli.ts gh ${kind} read ${number} --repo ${repo} --json ${readViewSupportedJsonFields(kind)} [compatibility alias]`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -981,28 +1109,50 @@ function resolveReadViewNumberReference(kind: "issue" | "pr", sub: "read" | "vie
|
||||
const command = `${kind} ${sub}`;
|
||||
const targets = positionalArgs(args.slice(2));
|
||||
if (targets.length > 1) {
|
||||
return validationError(command, options.repo, `${command} accepts one number or owner/repo#number target; use --repo owner/name and --number N when passing options first`, {
|
||||
return validationError(command, options.repo, `${command} accepts one positional target: number, GitHub ${kind} URL, or owner/repo#number`, {
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh ${kind} read <number> --repo owner/name --json ${readViewSupportedJsonFields(kind)}`,
|
||||
`bun scripts/cli.ts gh ${kind} read owner/name#<number> --raw`,
|
||||
`bun scripts/cli.ts gh ${kind} view <number> --repo owner/name --json ${readViewSupportedJsonFields(kind)}`,
|
||||
`bun scripts/cli.ts gh ${kind} view https://github.com/owner/name/${kind === "issue" ? "issues" : "pull"}/<number> --raw`,
|
||||
`bun scripts/cli.ts gh ${kind} view owner/name#<number> --raw`,
|
||||
],
|
||||
});
|
||||
}
|
||||
const raw = targets[0];
|
||||
const numberAliasRaw = kind === "pr" ? optionValue(args, "--number") : undefined;
|
||||
const shorthand = parseOwnerRepoNumberShorthand(raw);
|
||||
if (shorthand !== null) {
|
||||
if (numberAliasRaw !== undefined) {
|
||||
const aliasNumber = parseNumberForCommand(shorthand.repo, numberAliasRaw, command);
|
||||
if (typeof aliasNumber !== "number") return aliasNumber;
|
||||
if (aliasNumber !== shorthand.number) {
|
||||
return validationError(command, shorthand.repo, `${command} target ${shorthand.input} conflicts with --number ${aliasNumber}; use one PR number target.`, {
|
||||
shorthand,
|
||||
if (optionWasProvided(args, "--number")) {
|
||||
const numberAliasRaw = optionValue(args, "--number");
|
||||
const aliasNumber = parseNumberForCommand(options.repo, numberAliasRaw, command);
|
||||
if (typeof aliasNumber !== "number") return aliasNumber;
|
||||
if (raw !== undefined) {
|
||||
const parsedRaw = parseNumberForCommand(options.repo, raw, command);
|
||||
if (typeof parsedRaw !== "number") return parsedRaw;
|
||||
if (parsedRaw !== aliasNumber) {
|
||||
return validationError(command, options.repo, `${command} positional number ${parsedRaw} conflicts with --number ${aliasNumber}; use one target number.`, {
|
||||
positionalNumber: parsedRaw,
|
||||
numberAlias: aliasNumber,
|
||||
supportedCommands: readViewSupportedCommands(kind, shorthand.repo, shorthand.number),
|
||||
supportedCommands: readViewSupportedCommands(kind, options.repo, aliasNumber),
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
repo: options.repo,
|
||||
number: aliasNumber,
|
||||
shorthand: {
|
||||
input: `--number ${aliasNumber}`,
|
||||
repo: options.repo,
|
||||
number: aliasNumber,
|
||||
source: "number-option",
|
||||
standardCommand: `bun scripts/cli.ts gh ${kind} view ${aliasNumber} --repo ${options.repo}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
const shorthand = parseReadViewTarget(raw);
|
||||
if (shorthand !== null) {
|
||||
if (shorthand.urlKind !== undefined && shorthand.urlKind !== kind) {
|
||||
return validationError(command, shorthand.repo, `${command} target ${shorthand.input} is a GitHub ${shorthand.urlKind} URL, not a ${kind} URL.`, {
|
||||
shorthand,
|
||||
supportedCommands: readViewSupportedCommands(shorthand.urlKind, shorthand.repo, shorthand.number),
|
||||
});
|
||||
}
|
||||
const explicitRepo = optionValue(args, "--repo");
|
||||
if (explicitRepo !== undefined && explicitRepo !== shorthand.repo) {
|
||||
const message = `${command} target ${shorthand.input} resolves to repo ${shorthand.repo}, but --repo ${explicitRepo} was also provided. Use either the shorthand or a matching --repo, not both.`;
|
||||
@@ -1015,38 +1165,14 @@ function resolveReadViewNumberReference(kind: "issue" | "pr", sub: "read" | "vie
|
||||
}
|
||||
return { repo: shorthand.repo, number: shorthand.number, shorthand };
|
||||
}
|
||||
if (numberAliasRaw !== undefined) {
|
||||
const aliasNumber = parseNumberForCommand(options.repo, numberAliasRaw, command);
|
||||
if (typeof aliasNumber !== "number") return {
|
||||
...aliasNumber,
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh pr read --repo owner/name --number <number> --json ${readViewSupportedJsonFields("pr")}`,
|
||||
"bun scripts/cli.ts gh pr read owner/name#<number> --raw",
|
||||
],
|
||||
};
|
||||
if (raw !== undefined) {
|
||||
const parsedRaw = parseNumberForCommand(options.repo, raw, command);
|
||||
if (typeof parsedRaw !== "number") return parsedRaw;
|
||||
if (parsedRaw !== aliasNumber) {
|
||||
return validationError(command, options.repo, `${command} positional number ${parsedRaw} conflicts with --number ${aliasNumber}; use one PR number target.`, {
|
||||
positionalNumber: parsedRaw,
|
||||
numberAlias: aliasNumber,
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh pr read ${aliasNumber} --repo ${options.repo} --json ${readViewSupportedJsonFields("pr")}`,
|
||||
`bun scripts/cli.ts gh pr read --repo ${options.repo} --number ${aliasNumber} --full`,
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
return { repo: options.repo, number: aliasNumber };
|
||||
}
|
||||
const parsed = parseNumberForCommand(options.repo, raw, command);
|
||||
if (typeof parsed !== "number") {
|
||||
return {
|
||||
...parsed,
|
||||
supportedCommands: [
|
||||
`bun scripts/cli.ts gh ${kind} read <number> --repo owner/name --json ${readViewSupportedJsonFields(kind)}`,
|
||||
`bun scripts/cli.ts gh ${kind} read owner/name#<number> --raw`,
|
||||
`bun scripts/cli.ts gh ${kind} view <number> --repo owner/name --json ${readViewSupportedJsonFields(kind)}`,
|
||||
`bun scripts/cli.ts gh ${kind} view https://github.com/owner/name/${kind === "issue" ? "issues" : "pull"}/<number> --raw`,
|
||||
`bun scripts/cli.ts gh ${kind} view owner/name#<number> --raw`,
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -1068,6 +1194,30 @@ function readDisclosureOptions(options: GitHubOptions, shorthand: GitHubShorthan
|
||||
...(options.full ? { full: true } : {}),
|
||||
fullDisclosure: options.raw || options.full,
|
||||
shorthand: shorthand ?? null,
|
||||
...(shorthand?.source === "number-option" ? {
|
||||
compatibilityHint: "--number is accepted for low-friction compatibility; standard gh syntax uses a positional number or URL.",
|
||||
standardCommand: shorthand.standardCommand,
|
||||
} : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function numberOptionCompatibilityHint(resolved: GitHubResolvedNumberReference): Record<string, unknown> | null {
|
||||
if (resolved.shorthand?.source !== "number-option") return null;
|
||||
return {
|
||||
acceptedOption: "--number",
|
||||
compatibility: true,
|
||||
message: "--number is accepted for low-friction compatibility; prefer the standard positional number target shown in standardCommand.",
|
||||
standardCommand: resolved.shorthand.standardCommand ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async function withNumberOptionHint(result: GitHubCommandResult | Promise<GitHubCommandResult>, resolved: GitHubResolvedNumberReference): Promise<GitHubCommandResult> {
|
||||
const output = await result;
|
||||
const hint = numberOptionCompatibilityHint(resolved);
|
||||
if (hint === null) return output;
|
||||
return {
|
||||
...output,
|
||||
standardSyntaxHint: hint,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1082,14 +1232,14 @@ function unknownGhOptionDetails(args: string[], option: string): Record<string,
|
||||
helpCommand: "bun scripts/cli.ts gh help",
|
||||
};
|
||||
if ((top === "issue" || top === "pr") && (sub === "read" || sub === "view")) {
|
||||
const shorthand = parseOwnerRepoNumberShorthand(third);
|
||||
const shorthand = parseReadViewTarget(third);
|
||||
const repo = shorthand?.repo ?? optionValue(args, "--repo") ?? "owner/name";
|
||||
const number = shorthand?.number ?? (third !== undefined && /^\d+$/u.test(third) ? Number(third) : 0);
|
||||
details.supportedCommands = number > 0
|
||||
? readViewSupportedCommands(top, repo, number)
|
||||
: [
|
||||
`bun scripts/cli.ts gh ${top} read <number> --repo owner/name --json ${readViewSupportedJsonFields(top)}`,
|
||||
`bun scripts/cli.ts gh ${top} read owner/name#<number> --raw`,
|
||||
`bun scripts/cli.ts gh ${top} view <number> --repo owner/name --json ${readViewSupportedJsonFields(top)}`,
|
||||
`bun scripts/cli.ts gh ${top} view owner/name#<number> --raw`,
|
||||
];
|
||||
}
|
||||
return details;
|
||||
@@ -2036,17 +2186,17 @@ function isGitHubError(value: unknown): value is GitHubErrorPayload {
|
||||
|
||||
function issueBodyReadCommands(repo: string, issueNumber: number): Record<string, string> {
|
||||
return {
|
||||
body: `bun scripts/cli.ts gh issue read ${issueNumber} --repo ${repo} --json body`,
|
||||
full: `bun scripts/cli.ts gh issue read ${issueNumber} --repo ${repo} --full`,
|
||||
raw: `bun scripts/cli.ts gh issue read ${issueNumber} --repo ${repo} --raw`,
|
||||
body: `bun scripts/cli.ts gh issue view ${issueNumber} --repo ${repo} --json body`,
|
||||
full: `bun scripts/cli.ts gh issue view ${issueNumber} --repo ${repo} --full`,
|
||||
raw: `bun scripts/cli.ts gh issue view ${issueNumber} --repo ${repo} --raw`,
|
||||
};
|
||||
}
|
||||
|
||||
function issueCommentReadCommands(repo: string, issueNumber: number): Record<string, string> {
|
||||
return {
|
||||
comments: `bun scripts/cli.ts gh issue read ${issueNumber} --repo ${repo} --json comments`,
|
||||
full: `bun scripts/cli.ts gh issue read ${issueNumber} --repo ${repo} --full`,
|
||||
raw: `bun scripts/cli.ts gh issue read ${issueNumber} --repo ${repo} --raw`,
|
||||
comments: `bun scripts/cli.ts gh issue view ${issueNumber} --repo ${repo} --json comments`,
|
||||
full: `bun scripts/cli.ts gh issue view ${issueNumber} --repo ${repo} --full`,
|
||||
raw: `bun scripts/cli.ts gh issue view ${issueNumber} --repo ${repo} --raw`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2060,7 +2210,7 @@ function issueWriteDisclosure(options: GitHubOptions, repo: string, issueNumber:
|
||||
dryRunBoundedPreview: dryRun,
|
||||
note: explicitFullDisclosure && !dryRun
|
||||
? "The returned issue object includes the full body because --full or --raw was explicitly requested."
|
||||
: "Default issue write output omits full issue.body; use readCommands.full/raw or gh issue read --json body when full text is needed.",
|
||||
: "Default issue write output omits full issue.body; use readCommands.full/raw or gh issue view --json body when full text is needed.",
|
||||
readCommands: issueBodyReadCommands(repo, issueNumber),
|
||||
};
|
||||
}
|
||||
@@ -2122,7 +2272,7 @@ function issueLifecycleDisclosure(repo: string, issueNumber: number, dryRun: boo
|
||||
fullBodyIncluded: false,
|
||||
bodyOmitted: true,
|
||||
dryRunBoundedPreview: dryRun,
|
||||
note: "Issue lifecycle write output omits full issue.body; use readCommands.full/raw or gh issue read --json body when full text is needed.",
|
||||
note: "Issue lifecycle write output omits full issue.body; use readCommands.full/raw or gh issue view --json body when full text is needed.",
|
||||
readCommands: issueBodyReadCommands(repo, issueNumber),
|
||||
};
|
||||
}
|
||||
@@ -6313,7 +6463,7 @@ async function prFiles(repo: string, token: string, number: number, limit: numbe
|
||||
const nextLimit = totalFiles === null ? MAX_PR_FILES_LIMIT : Math.min(totalFiles, MAX_PR_FILES_LIMIT);
|
||||
const nextCommand = truncated
|
||||
? `bun scripts/cli.ts gh pr files ${number} --repo ${repo} --limit ${nextLimit}`
|
||||
: `bun scripts/cli.ts gh pr read ${number} --repo ${repo} --json body,title,state,head,base`;
|
||||
: `bun scripts/cli.ts gh pr view ${number} --repo ${repo} --json body,title,state,head,base`;
|
||||
return {
|
||||
ok: true,
|
||||
command: commandName,
|
||||
@@ -6384,42 +6534,42 @@ export function ghHelp(): unknown {
|
||||
usage: [
|
||||
"bun scripts/cli.ts gh auth status [--repo owner/name]",
|
||||
"bun scripts/cli.ts gh issue list [owner/repo] [--state open|closed|all] [--limit N] [--search text] [--label label[,label...]]... [--repo owner/name] [--json number,title,state,closed,closedAt,url,updatedAt,createdAt,author,labels] [--raw|--full]",
|
||||
"bun scripts/cli.ts gh issue read <number|owner/repo#number> [--repo owner/name] [--json body,title,state,closed,closedAt,comments] [--raw|--full]",
|
||||
"bun scripts/cli.ts gh issue view <number|owner/repo#number> [--repo owner/name] [--raw|--full] [compatibility alias for issue read]",
|
||||
"bun scripts/cli.ts gh issue view <number|url|owner/repo#number> [--repo owner/name] [--number N compat] [--json body,title,state,closed,closedAt,comments] [--raw|--full]",
|
||||
"bun scripts/cli.ts gh issue read <number|url|owner/repo#number> [--repo owner/name] [--number N compat] [--raw|--full] [compatibility alias for issue view]",
|
||||
"bun scripts/cli.ts gh issue create --title <title> --body-file <file|-> [--label label[,label...]]... [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue update <number> --mode replace|append --body-file <file|-> [--title title] [--repo owner/name] [--dry-run] [--expect-updated-at ts|--expect-body-sha sha256] [--body-profile auto|code-queue-board|commander-brief] [--allow-short-body] [--full|--raw]",
|
||||
"bun scripts/cli.ts gh issue edit <number> --body-file <file|-> [--full|--raw] [compat alias for issue update --mode replace]",
|
||||
"bun scripts/cli.ts gh issue update <number> --mode replace|append --body-file <file|-> [--title title] [--repo owner/name] [--number N compat] [--dry-run] [--expect-updated-at ts|--expect-body-sha sha256] [--body-profile auto|code-queue-board|commander-brief] [--allow-short-body] [--full|--raw]",
|
||||
"bun scripts/cli.ts gh issue edit <number> --body-file <file|-> [--repo owner/name] [--number N compat] [--full|--raw] [compat alias for issue update --mode replace]",
|
||||
"bun scripts/cli.ts gh issue edit 24 --body-file <file> --notify-claudeqq-brief-diff [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue comment create <number> --body-file <file|->|--body <short-text> [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue comment delete <commentId> [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue close|reopen <number> [--repo owner/name] [--comment <short-text>|--comment-file <file|->] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue comment create <number> --body-file <file|->|--body <short-text> [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue comment delete <commentId> [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue close|reopen <number> [--repo owner/name] [--number N compat] [--comment <short-text>|--comment-file <file|->] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue stale-close [--repo owner/name] [--inactive-hours N] [--limit N] [--label label[,label...]]... [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue delete <number> [unsupported: use close]",
|
||||
"bun scripts/cli.ts gh issue scan-escape [--repo owner/name] [--limit N] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue cleanup-plan [--repo owner/name] [--limit N] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue board-audit [--repo owner/name] [--board-issue 20] [--limit N] [--known-meta-issue N[,N...]] [--ignore-issue N[,N...]] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue board-row list [--repo owner/name] --board-issue 20 [--state open|closed|all] [--dry-run]",
|
||||
"bun scripts/cli.ts gh issue board-row get <issueNumber> [--repo owner/name] --board-issue 20",
|
||||
"bun scripts/cli.ts gh issue board-row update <issueNumber> [--repo owner/name] --board-issue 20 --field progress|status|validation|branch|tasks|focus --value <text> [--dry-run] [--expect-updated-at ts|--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh issue board-row add <issueNumber> [--repo owner/name] --board-issue 20 --section open|closed --row-file <markdown-row-file> [--dry-run] [--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh issue board-row upsert <issueNumber> [--repo owner/name] --board-issue 20 --section open|closed [--category text] --branch <branch> --tasks <task> --summary <text> --focus <text> --validation <text> --progress <text> [--status OPEN|CLOSED] [--dry-run] [--expect-updated-at ts|--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh issue board-row move <issueNumber> [--repo owner/name] --board-issue 20 --to open|closed [--status OPEN|CLOSED] [--dry-run] [--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh issue board-row delete <issueNumber> [--repo owner/name] --board-issue 20 [--dry-run] [--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh preflight <prNumber|owner/repo#number> [--repo owner/name] [--full|--raw] [compatibility alias for gh pr preflight]",
|
||||
"bun scripts/cli.ts gh issue board-row get <issueNumber> [--repo owner/name] [--number N compat] --board-issue 20",
|
||||
"bun scripts/cli.ts gh issue board-row update <issueNumber> [--repo owner/name] [--number N compat] --board-issue 20 --field progress|status|validation|branch|tasks|focus --value <text> [--dry-run] [--expect-updated-at ts|--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh issue board-row add <issueNumber> [--repo owner/name] [--number N compat] --board-issue 20 --section open|closed --row-file <markdown-row-file> [--dry-run] [--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh issue board-row upsert <issueNumber> [--repo owner/name] [--number N compat] --board-issue 20 --section open|closed [--category text] --branch <branch> --tasks <task> --summary <text> --focus <text> --validation <text> --progress <text> [--status OPEN|CLOSED] [--dry-run] [--expect-updated-at ts|--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh issue board-row move <issueNumber> [--repo owner/name] [--number N compat] --board-issue 20 --to open|closed [--status OPEN|CLOSED] [--dry-run] [--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh issue board-row delete <issueNumber> [--repo owner/name] [--number N compat] --board-issue 20 [--dry-run] [--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh preflight <prNumber|owner/repo#number> [--repo owner/name] [--number N compat] [--full|--raw] [compatibility alias for gh pr preflight]",
|
||||
"bun scripts/cli.ts gh pr list [owner/repo] [--repo owner/name] [--state open|closed|all] [--limit N] [--json number,title,state,url,updatedAt,createdAt,author,head,base,draft]",
|
||||
"bun scripts/cli.ts gh pr files <number> [--repo owner/name] [--limit N] [number may appear before or after options]",
|
||||
"bun scripts/cli.ts gh pr diff <number> --stat [--repo owner/name] [--limit N] [number may appear before or after options; compatibility alias for pr files; no raw diff]",
|
||||
"bun scripts/cli.ts gh pr read <number|owner/repo#number> [--repo owner/name] [--number N] [--json body,title,state,stateDetail,closed,closedAt,merged,mergedAt,mergeCommit,head,base,draft,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup] [--raw|--full]",
|
||||
"bun scripts/cli.ts gh pr view <number|owner/repo#number> [--repo owner/name] [--raw|--full] [compatibility alias for pr read]",
|
||||
"bun scripts/cli.ts gh pr preflight <number|owner/repo#number> [--repo owner/name] [--full|--raw] [number may appear before or after options]",
|
||||
"bun scripts/cli.ts gh pr closeout <number|owner/repo#number> [--repo owner/name] [--full|--raw] [number may appear before or after options; compatibility alias for pr preflight]",
|
||||
"bun scripts/cli.ts gh pr files <number> [--repo owner/name] [--number N compat] [--limit N] [number may appear before or after options]",
|
||||
"bun scripts/cli.ts gh pr diff <number> --stat [--repo owner/name] [--number N compat] [--limit N] [number may appear before or after options; compatibility alias for pr files; no raw diff]",
|
||||
"bun scripts/cli.ts gh pr view <number|url|owner/repo#number> [--repo owner/name] [--number N compat] [--json body,title,state,stateDetail,closed,closedAt,merged,mergedAt,mergeCommit,head,base,draft,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup] [--raw|--full]",
|
||||
"bun scripts/cli.ts gh pr read <number|url|owner/repo#number> [--repo owner/name] [--number N compat] [--raw|--full] [compatibility alias for pr view]",
|
||||
"bun scripts/cli.ts gh pr preflight <number|owner/repo#number> [--repo owner/name] [--number N compat] [--full|--raw] [number may appear before or after options]",
|
||||
"bun scripts/cli.ts gh pr closeout <number|owner/repo#number> [--repo owner/name] [--number N compat] [--full|--raw] [number may appear before or after options; compatibility alias for pr preflight]",
|
||||
"bun scripts/cli.ts gh pr create --title <title> --body-file <file>|--body <text> --base <branch> --head <branch> [--repo owner/name] [--draft] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr edit <number> [--title title] [--body-file <file>|--body-file -|--body <text>] [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr update <number> --mode replace|append [--body-file <file>|--body-file -|--body <text>] [--title title] [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr comment create <number> --body-file <file>|--body <text> [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr comment delete <commentId> [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr close|reopen <number> [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr merge <number> [--repo owner/name] [--merge|--squash|--rebase] [--delete-branch] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr edit <number> [--title title] [--body-file <file>|--body-file -|--body <text>] [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr update <number> --mode replace|append [--body-file <file>|--body-file -|--body <text>] [--title title] [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr comment create <number> --body-file <file>|--body <text> [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr comment delete <commentId> [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr close|reopen <number> [--repo owner/name] [--number N compat] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr merge <number> [--repo owner/name] [--number N compat] [--merge|--squash|--rebase] [--delete-branch] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr delete <number> [unsupported: use close]",
|
||||
],
|
||||
defaults: { repo: DEFAULT_REPO },
|
||||
@@ -6429,7 +6579,7 @@ export function ghHelp(): unknown {
|
||||
"issue list and pr list accept a single positional owner/repo as a compatibility alias for --repo owner/name. The positional repo and --repo must match if both are supplied; non-repo positionals fail structurally instead of falling back to the default repo.",
|
||||
"issue list defaults to --state open and bounded --limit 30; it paginates GitHub REST/Search pages internally when --limit exceeds GitHub's per-page cap and discloses pagination/rawCount/hasMore so operators do not mistake a single page for the full repository. --search uses GitHub Search Issues API with repo/type/state qualifiers for low-friction dedupe lookup before creating a new issue. Supported --json fields are number,title,state,closed,closedAt,url,updatedAt,createdAt,author,labels and unknown fields fail structurally.",
|
||||
"PR list defaults to --state all for compatibility with earlier UniDesk CLI behavior; supported states are open, closed, and all.",
|
||||
"issue read is the canonical read path; view remains a compatibility alias. Read/view accept owner/repo#number shorthand and derive --repo unless an explicit conflicting --repo is supplied, which fails structurally with suggested commands. Read supports lifecycle fields closed/closedAt plus legacy --json field selection such as --json body and still exposes .data.issue.body for compatibility; unsupported fields fail structurally.",
|
||||
"issue view is the canonical GitHub CLI-compatible read path; read remains a UniDesk compatibility alias. View/read accept positional numbers, GitHub issue URLs, and owner/repo#number shorthand, deriving --repo unless an explicit conflicting --repo is supplied. --number is accepted on single issue/comment numeric target commands for low-friction compatibility and returns a standard syntax hint; list/create/scan-escape/cleanup-plan/board-audit/board-row list do not accept it. Comment delete treats --number as commentId, not an issue number. View supports lifecycle fields closed/closedAt plus legacy --json field selection such as --json body and still exposes .data.issue.body for compatibility; unsupported fields fail structurally.",
|
||||
"--raw and --full are explicit full-disclosure aliases for gh issue list/read/view/update/edit and gh pr list/read/view. For issue writes, default success output omits full issue.body and returns bodyChars/bodySha/bodyPreview plus readCommands; --full|--raw includes the full returned issue body only on commands that explicitly support full disclosure.",
|
||||
"GitHub CLI output larger than 20 KiB is automatically written to /tmp/unidesk-cli-output/*.json; stdout stays bounded JSON with outputTruncated=true, the dump path, total bytes/lines, and head/tail previews.",
|
||||
"issue create accepts --body-file <file|-> plus repeatable --label values and comma-separated labels; inline --body is intentionally unsupported for issue creation. Dry-run prints the parsed labels and non-dry-run sends them in the GitHub REST create-issue payload.",
|
||||
@@ -6439,7 +6589,7 @@ export function ghHelp(): unknown {
|
||||
"issue update --body-file accepts files or - for stdin, refuses literal null, blank, and too-short bodies by default. Use --allow-short-body only for intentional short writes; #20 requires its board heading, warns when HWLAB product/user issue routing appears in favor of pikasTech/HWLAB, and still rejects commander brief update sections; commander-brief requires its stable heading on legacy #24 plus daily rolling brief issues titled YYYY-MM-DD 指挥简报(北京时间).",
|
||||
"issue update dry-run reports bounded bodyPreview/bodyPreviewLines, old/new body length slots, body SHA, required heading checks, literal \\n detection, shell-pollution signals, guard/concurrency summary, wouldPatch, and readCommands without printing an unbounded full body. Non-dry-run automatically reads current issue metadata before PATCH and returns oldBodySha/updatedAt; --expect-updated-at or --expect-body-sha remain available for explicit stale-cache protection.",
|
||||
"issue comment create accepts --body-file <file|-> for Markdown/generated content and --body only for short single-line text. Blank, multiline, shell-polluted, secret-like, and overlong inline bodies fail structurally.",
|
||||
"issue close/reopen default success output is compact and omits full issue.body. Optional --comment <short-text> or --comment-file <file|-> posts a bounded lifecycle comment before the state change and aborts the state change if the comment POST fails. --comment-file is the recommended path for generated Markdown closeout evidence; --comment remains the short inline form. Use gh issue read <number> --json body or --full/--raw on read when full text is needed.",
|
||||
"issue close/reopen default success output is compact and omits full issue.body. Optional --comment <short-text> or --comment-file <file|-> posts a bounded lifecycle comment before the state change and aborts the state change if the comment POST fails. --comment-file is the recommended path for generated Markdown closeout evidence; --comment remains the short inline form. Use gh issue view <number> --json body or --full/--raw on view when full text is needed.",
|
||||
"issue stale-close is the reusable lifecycle cleanup path for policies such as closing open issues inactive for more than 48 hours. It selects open issues by GitHub updatedAt older than observedAt - --inactive-hours, treats comments and state changes as activity, filters pull requests, supports --dry-run, and returns bounded candidate/closed/failure summaries without echoing full bodies.",
|
||||
"For one-shot issue writes, pipe reviewed Markdown through stdin: cat body.md | bun scripts/cli.ts gh issue update <number> --repo owner/name --body-file - or gh issue comment create <number> --body-file -. When staging a body file from a shell, use a quoted heredoc such as cat <<'EOF' > /tmp/body.md so backticks and backslashes are not expanded before --body-file reads the file.",
|
||||
"For JSON request bodies in other CLI namespaces, prefer --body-file or --body-stdin over long inline shell arguments. GitHub issue/PR Markdown writes use --body-file <file|-> for long or multiline content.",
|
||||
@@ -6453,10 +6603,10 @@ export function ghHelp(): unknown {
|
||||
"comment delete is supported because GitHub supports deleting issue comments; issue/pr hard delete is unsupported and close is the lifecycle alternative.",
|
||||
"PR files is the canonical compact changed-file/stat summary. It uses GitHub REST, returns bounded file rows, additions/deletions/changes when available, truncation metadata, and a next command for full details. Raw diff patches are not emitted by default; gh pr diff <number> --stat is a compatibility alias for the same JSON summary.",
|
||||
"PR edit/update PATCHes /repos/{owner}/{repo}/pulls/{number} through REST only, never GitHub Projects Classic GraphQL/projectCards, and returns low-noise JSON with repo, PR number, changedFields, url, and body size/SHA metadata instead of echoing the full body.",
|
||||
"PR read is the canonical read path; view remains a compatibility alias. PR read/view accept owner/repo#number shorthand and --number N as a compatibility alias for the positional PR number; shorthand derives --repo unless an explicit conflicting --repo is supplied, which fails structurally with suggested commands. PR read/view supports REST closeout fields stateDetail, closed, closedAt, merged, mergedAt, mergeCommit, headRefName, and baseRefName; mergeable, mergeStateStatus, and statusCheckRollup are fetched through GitHub GraphQL only when requested or when --raw/--full requests full disclosure, and closeoutMetadata makes GraphQL errors plus UNKNOWN/null metadata explicit.",
|
||||
"PR preflight/closeout accept the same owner/repo#number shorthand as PR read/view so merge readiness checks do not require repeating --repo after a PR URL has already been normalized.",
|
||||
"PR view is the canonical GitHub CLI-compatible read path; read remains a UniDesk compatibility alias. PR view/read accept positional numbers, GitHub PR URLs, and owner/repo#number shorthand, deriving --repo unless an explicit conflicting --repo is supplied. --number is accepted on single PR/comment numeric target commands for low-friction compatibility and returns a standard syntax hint; list/create do not accept it. PR comment delete treats --number as commentId, not a PR number. PR view/read supports REST closeout fields stateDetail, closed, closedAt, merged, mergedAt, mergeCommit, headRefName, and baseRefName; mergeable, mergeStateStatus, and statusCheckRollup are fetched through GitHub GraphQL only when requested or when --raw/--full requests full disclosure, and closeoutMetadata makes GraphQL errors plus UNKNOWN/null metadata explicit.",
|
||||
"PR preflight/closeout accept the same owner/repo#number shorthand as PR view/read so merge readiness checks do not require repeating --repo after a PR URL has already been normalized.",
|
||||
"PR list does not fetch mergeability or statusCheckRollup; request those closeout fields with gh pr view <number> --json headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup.",
|
||||
"PR preflight is a low-noise read-only closeout helper. It combines redacted auth capability, PR branch/state metadata, mergeability, mergeStateStatus, compact status check counts, and the explicit UniDesk REST CLI no-merge policy. Use --full or --raw to include all fetched status contexts.",
|
||||
"PR preflight is a low-noise read-only closeout helper. It combines redacted auth capability, PR branch/state metadata, mergeability, mergeStateStatus, compact status check counts, and an explicit read-only policy. Use --full or --raw to include all fetched status contexts; gh pr merge is the separate guarded write path.",
|
||||
"PR merge is a guarded write operation: it first reads closeout metadata, refuses non-open/draft/conflicting/non-clean/failed/pending PRs, then uses GitHub REST merge. Use --dry-run to see the exact merge plan without writing.",
|
||||
],
|
||||
};
|
||||
@@ -6496,12 +6646,12 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
return validationError(command, options.repo, "--raw and --full are explicit full-disclosure aliases only for gh issue list/read/view/update/edit, gh pr list/read/view, and gh pr preflight/closeout.", {
|
||||
supportedCommands: [
|
||||
"bun scripts/cli.ts gh issue list --repo owner/name --limit 200 --full",
|
||||
"bun scripts/cli.ts gh issue read owner/name#<number> --raw",
|
||||
"bun scripts/cli.ts gh issue read <number> --repo owner/name --json body,title,state,comments",
|
||||
"bun scripts/cli.ts gh issue view owner/name#<number> --raw",
|
||||
"bun scripts/cli.ts gh issue view <number> --repo owner/name --json body,title,state,comments",
|
||||
"cat body.md | bun scripts/cli.ts gh issue update <number> --repo owner/name --body-file - --full",
|
||||
"bun scripts/cli.ts gh pr list --repo owner/name --limit 100 --full",
|
||||
"bun scripts/cli.ts gh pr read owner/name#<number> --raw",
|
||||
`bun scripts/cli.ts gh pr read <number> --repo owner/name --json ${readViewSupportedJsonFields("pr")}`,
|
||||
"bun scripts/cli.ts gh pr view owner/name#<number> --raw",
|
||||
`bun scripts/cli.ts gh pr view <number> --repo owner/name --json ${readViewSupportedJsonFields("pr")}`,
|
||||
"bun scripts/cli.ts gh pr preflight <number> --repo owner/name --full",
|
||||
],
|
||||
});
|
||||
@@ -6515,13 +6665,17 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
],
|
||||
});
|
||||
}
|
||||
if (optionWasProvided(args, "--number") && !(top === "pr" && isPrReadCommand(sub))) {
|
||||
if (optionWasProvided(args, "--number") && !allowsNumberTargetAlias(top, sub, third)) {
|
||||
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
return validationError(command, options.repo, "--number is only supported by gh pr read/view; use gh pr read --repo owner/name --number N.", {
|
||||
const standardViewCommand = top === "issue" || top === "pr" ? `gh ${top} view` : "gh issue/pr view";
|
||||
return validationError(command, options.repo, `--number is only a compatibility alias for single numeric target commands; standard ${standardViewCommand} uses a positional number or URL target.`, {
|
||||
supportedCommands: [
|
||||
"bun scripts/cli.ts gh pr read --repo owner/name --number <number> --full",
|
||||
"bun scripts/cli.ts gh pr read <number> --repo owner/name --json body,title,state,head,base",
|
||||
"bun scripts/cli.ts gh issue view <number> --repo owner/name --json body,title,state",
|
||||
"bun scripts/cli.ts gh issue view https://github.com/owner/name/issues/<number> --raw",
|
||||
"bun scripts/cli.ts gh pr view <number> --repo owner/name --json body,title,state,head,base",
|
||||
"bun scripts/cli.ts gh pr view https://github.com/owner/name/pull/<number> --raw",
|
||||
],
|
||||
rejectedOption: "--number",
|
||||
});
|
||||
}
|
||||
if (optionWasProvided(args, "--mode") && !((top === "issue" && (sub === "update" || sub === "edit")) || (top === "pr" && (sub === "update" || sub === "edit")))) {
|
||||
@@ -6586,32 +6740,32 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
if (top === "preflight") {
|
||||
const resolved = resolvePositionalPrReference(args, 1, "preflight", options);
|
||||
if (isGitHubCommandResult(resolved)) return resolved;
|
||||
return prPreflight(resolved.repo, resolved.number, "preflight", options.full || options.raw);
|
||||
return withNumberOptionHint(prPreflight(resolved.repo, resolved.number, "preflight", options.full || options.raw), resolved);
|
||||
}
|
||||
|
||||
if (top === "issue") {
|
||||
if (sub === "delete") return unsupportedCommand("issue delete", options.repo, "GitHub REST does not support hard-deleting issues; use gh issue close for lifecycle deletion semantics.");
|
||||
if (sub === "comment" && third === "delete") {
|
||||
const resolved = resolvePositionalIssueReference(args, 3, "issue comment delete", options);
|
||||
const resolved = resolvePositionalNumberReference("issue", args, 3, "issue comment delete", options);
|
||||
if (isGitHubCommandResult(resolved)) return resolved;
|
||||
const commentId = resolved.number;
|
||||
if (typeof commentId !== "number") return commentId;
|
||||
if (options.dryRun) return commentDelete(options.repo, "", "issue", commentId, true);
|
||||
if (options.dryRun) return withNumberOptionHint(commentDelete(resolved.repo, "", "issue", commentId, true), resolved);
|
||||
const { token, probe } = resolveToken(true);
|
||||
const missing = authRequired(options.repo, "issue comment delete", probe);
|
||||
if (missing !== null || token === null) return missing ?? authRequired(options.repo, "issue comment delete", { present: false, source: null, ghFallbackAttempted: true });
|
||||
return commentDelete(options.repo, token, "issue", commentId, false);
|
||||
const missing = authRequired(resolved.repo, "issue comment delete", probe);
|
||||
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "issue comment delete", { present: false, source: null, ghFallbackAttempted: true });
|
||||
return withNumberOptionHint(commentDelete(resolved.repo, token, "issue", commentId, false), resolved);
|
||||
}
|
||||
if (sub === "comment" && third === "create") {
|
||||
const resolved = resolvePositionalIssueReference(args, 3, "issue comment create", options);
|
||||
if (isGitHubCommandResult(resolved)) return resolved;
|
||||
const issueNumber = resolved.number;
|
||||
if (typeof issueNumber !== "number") return issueNumber;
|
||||
if (options.dryRun) return issueComment(options.repo, "", issueNumber, options);
|
||||
if (options.dryRun) return withNumberOptionHint(issueComment(resolved.repo, "", issueNumber, { ...options, repo: resolved.repo }), resolved);
|
||||
const { token, probe } = resolveToken(true);
|
||||
const missing = authRequired(options.repo, "issue comment create", probe);
|
||||
const missing = authRequired(resolved.repo, "issue comment create", probe);
|
||||
if (missing !== null || token === null) return missing ?? authRequired(options.repo, "issue comment create", { present: false, source: null, ghFallbackAttempted: true });
|
||||
return issueComment(options.repo, token, issueNumber, options);
|
||||
return withNumberOptionHint(issueComment(resolved.repo, token, issueNumber, { ...options, repo: resolved.repo }), resolved);
|
||||
}
|
||||
if (sub === "scan-escape" || sub === "cleanup-plan") {
|
||||
const { token, probe } = resolveToken(true);
|
||||
@@ -6640,12 +6794,13 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
if (isGitHubCommandResult(resolvedBoardRow)) return resolvedBoardRow;
|
||||
const issueNumber = resolvedBoardRow.number;
|
||||
if (typeof issueNumber !== "number") return issueNumber;
|
||||
if (action === "get") return issueBoardRowGet(options.repo, token, issueNumber, options);
|
||||
if (action === "update") return issueBoardRowUpdate(options.repo, token, issueNumber, options);
|
||||
if (action === "add") return issueBoardRowAdd(options.repo, token, issueNumber, options);
|
||||
if (action === "upsert") return issueBoardRowUpsert(options.repo, token, issueNumber, options);
|
||||
if (action === "move") return issueBoardRowMove(options.repo, token, issueNumber, options);
|
||||
return issueBoardRowDelete(options.repo, token, issueNumber, options);
|
||||
const boardRowOptions = { ...options, repo: resolvedBoardRow.repo };
|
||||
if (action === "get") return withNumberOptionHint(issueBoardRowGet(resolvedBoardRow.repo, token, issueNumber, boardRowOptions), resolvedBoardRow);
|
||||
if (action === "update") return withNumberOptionHint(issueBoardRowUpdate(resolvedBoardRow.repo, token, issueNumber, boardRowOptions), resolvedBoardRow);
|
||||
if (action === "add") return withNumberOptionHint(issueBoardRowAdd(resolvedBoardRow.repo, token, issueNumber, boardRowOptions), resolvedBoardRow);
|
||||
if (action === "upsert") return withNumberOptionHint(issueBoardRowUpsert(resolvedBoardRow.repo, token, issueNumber, boardRowOptions), resolvedBoardRow);
|
||||
if (action === "move") return withNumberOptionHint(issueBoardRowMove(resolvedBoardRow.repo, token, issueNumber, boardRowOptions), resolvedBoardRow);
|
||||
return withNumberOptionHint(issueBoardRowDelete(resolvedBoardRow.repo, token, issueNumber, boardRowOptions), resolvedBoardRow);
|
||||
}
|
||||
if (options.dryRun) {
|
||||
if (sub === "create") return issueCreate(options.repo, "", options);
|
||||
@@ -6654,29 +6809,29 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
if (isGitHubCommandResult(issueEditRef)) return issueEditRef;
|
||||
const issueNumber = issueEditRef.number;
|
||||
const { token } = resolveToken(false);
|
||||
return issueEdit(options.repo, token ?? "", issueNumber, options);
|
||||
return withNumberOptionHint(issueEdit(issueEditRef.repo, token ?? "", issueNumber, { ...options, repo: issueEditRef.repo }), issueEditRef);
|
||||
}
|
||||
if (sub === "update") {
|
||||
const issueUpdateRef = resolvePositionalIssueReference(args, 2, "issue update", options);
|
||||
if (isGitHubCommandResult(issueUpdateRef)) return issueUpdateRef;
|
||||
const issueNumber = issueUpdateRef.number;
|
||||
const { token } = resolveToken(false);
|
||||
return issueEdit(options.repo, token ?? "", issueNumber, options, "issue update");
|
||||
return withNumberOptionHint(issueEdit(issueUpdateRef.repo, token ?? "", issueNumber, { ...options, repo: issueUpdateRef.repo }, "issue update"), issueUpdateRef);
|
||||
}
|
||||
if (sub === "comment") {
|
||||
const r = resolvePositionalIssueReference(args, 2, "issue comment", options);
|
||||
if (isGitHubCommandResult(r)) return r;
|
||||
return issueComment(r.repo, "", r.number, { ...options, repo: r.repo });
|
||||
return withNumberOptionHint(issueComment(r.repo, "", r.number, { ...options, repo: r.repo }), r);
|
||||
}
|
||||
if (sub === "close") {
|
||||
const r = resolvePositionalIssueReference(args, 2, "issue close", options);
|
||||
if (isGitHubCommandResult(r)) return r;
|
||||
return issueState(r.repo, "", r.number, "closed", true, { ...options, repo: r.repo });
|
||||
return withNumberOptionHint(issueState(r.repo, "", r.number, "closed", true, { ...options, repo: r.repo }), r);
|
||||
}
|
||||
if (sub === "reopen") {
|
||||
const r = resolvePositionalIssueReference(args, 2, "issue reopen", options);
|
||||
if (isGitHubCommandResult(r)) return r;
|
||||
return issueState(r.repo, "", r.number, "open", true, { ...options, repo: r.repo });
|
||||
return withNumberOptionHint(issueState(r.repo, "", r.number, "open", true, { ...options, repo: r.repo }), r);
|
||||
}
|
||||
}
|
||||
if (sub === "read" || sub === "view") {
|
||||
@@ -6686,8 +6841,8 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
const missing = authRequired(resolved.repo, `issue ${sub}`, probe);
|
||||
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, `issue ${sub}`, { present: false, source: null, ghFallbackAttempted: true });
|
||||
const disclosure = readDisclosureOptions(options, resolved.shorthand);
|
||||
if (sub === "read") return issueRead(resolved.repo, token, resolved.number, issueReadJsonFields(options), "issue read", disclosure);
|
||||
return issueView(resolved.repo, token, resolved.number, issueReadJsonFields(options), disclosure);
|
||||
if (sub === "read") return withNumberOptionHint(issueRead(resolved.repo, token, resolved.number, issueReadJsonFields(options), "issue read", disclosure), resolved);
|
||||
return withNumberOptionHint(issueView(resolved.repo, token, resolved.number, issueReadJsonFields(options), disclosure), resolved);
|
||||
}
|
||||
const { token, probe } = resolveToken(true);
|
||||
const missing = authRequired(options.repo, `issue ${sub ?? ""}`.trim(), probe);
|
||||
@@ -6699,27 +6854,27 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
if (sub === "edit") {
|
||||
const r = resolvePositionalIssueReference(args, 2, "issue edit", options);
|
||||
if (isGitHubCommandResult(r)) return r;
|
||||
return issueEdit(r.repo, token, r.number, { ...options, repo: r.repo });
|
||||
return withNumberOptionHint(issueEdit(r.repo, token, r.number, { ...options, repo: r.repo }), r);
|
||||
}
|
||||
if (sub === "update") {
|
||||
const r = resolvePositionalIssueReference(args, 2, "issue update", options);
|
||||
if (isGitHubCommandResult(r)) return r;
|
||||
return issueEdit(r.repo, token, r.number, { ...options, repo: r.repo }, "issue update");
|
||||
return withNumberOptionHint(issueEdit(r.repo, token, r.number, { ...options, repo: r.repo }, "issue update"), r);
|
||||
}
|
||||
if (sub === "comment") {
|
||||
const r = resolvePositionalIssueReference(args, 2, "issue comment", options);
|
||||
if (isGitHubCommandResult(r)) return r;
|
||||
return issueComment(r.repo, token, r.number, { ...options, repo: r.repo });
|
||||
return withNumberOptionHint(issueComment(r.repo, token, r.number, { ...options, repo: r.repo }), r);
|
||||
}
|
||||
if (sub === "close") {
|
||||
const r = resolvePositionalIssueReference(args, 2, "issue close", options);
|
||||
if (isGitHubCommandResult(r)) return r;
|
||||
return issueState(r.repo, token, r.number, "closed", options.dryRun, { ...options, repo: r.repo });
|
||||
return withNumberOptionHint(issueState(r.repo, token, r.number, "closed", options.dryRun, { ...options, repo: r.repo }), r);
|
||||
}
|
||||
if (sub === "reopen") {
|
||||
const r = resolvePositionalIssueReference(args, 2, "issue reopen", options);
|
||||
if (isGitHubCommandResult(r)) return r;
|
||||
return issueState(r.repo, token, r.number, "open", options.dryRun, { ...options, repo: r.repo });
|
||||
return withNumberOptionHint(issueState(r.repo, token, r.number, "open", options.dryRun, { ...options, repo: r.repo }), r);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6739,7 +6894,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
const { token, probe } = resolveToken(true);
|
||||
const missing = authRequired(resolved.repo, "pr diff --stat", probe);
|
||||
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "pr diff --stat", { present: false, source: null, ghFallbackAttempted: true });
|
||||
return prFiles(resolved.repo, token, resolved.number, options.limit, "pr diff --stat");
|
||||
return withNumberOptionHint(prFiles(resolved.repo, token, resolved.number, options.limit, "pr diff --stat"), resolved);
|
||||
}
|
||||
if (sub === "files") {
|
||||
const resolved = resolvePositionalPrReference(args, 2, "pr files", options);
|
||||
@@ -6747,31 +6902,31 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
const { token, probe } = resolveToken(true);
|
||||
const missing = authRequired(resolved.repo, "pr files", probe);
|
||||
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "pr files", { present: false, source: null, ghFallbackAttempted: true });
|
||||
return prFiles(resolved.repo, token, resolved.number, options.limit, "pr files");
|
||||
return withNumberOptionHint(prFiles(resolved.repo, token, resolved.number, options.limit, "pr files"), resolved);
|
||||
}
|
||||
if (sub === "delete") return unsupportedCommand("pr delete", options.repo, "GitHub REST does not support hard-deleting pull requests; use gh pr close for lifecycle deletion semantics.");
|
||||
if (sub === "preflight" || sub === "closeout") {
|
||||
const resolved = resolvePositionalPrReference(args, 2, `pr ${sub}`, options);
|
||||
if (isGitHubCommandResult(resolved)) return resolved;
|
||||
return prPreflight(resolved.repo, resolved.number, sub === "closeout" ? "pr closeout" : "pr preflight", options.full || options.raw);
|
||||
return withNumberOptionHint(prPreflight(resolved.repo, resolved.number, sub === "closeout" ? "pr closeout" : "pr preflight", options.full || options.raw), resolved);
|
||||
}
|
||||
if (sub === "comment" && third === "delete") {
|
||||
const commentId = parseNumberForCommand(options.repo, args[3], "pr comment delete");
|
||||
if (typeof commentId !== "number") return commentId;
|
||||
if (options.dryRun) return commentDelete(options.repo, "", "pr", commentId, true);
|
||||
const resolved = resolvePositionalNumberReference("pr", args, 3, "pr comment delete", options);
|
||||
if (isGitHubCommandResult(resolved)) return resolved;
|
||||
if (options.dryRun) return withNumberOptionHint(commentDelete(resolved.repo, "", "pr", resolved.number, true), resolved);
|
||||
const { token, probe } = resolveToken(true);
|
||||
const missing = authRequired(options.repo, "pr comment delete", probe);
|
||||
if (missing !== null || token === null) return missing ?? authRequired(options.repo, "pr comment delete", { present: false, source: null, ghFallbackAttempted: true });
|
||||
return commentDelete(options.repo, token, "pr", commentId, false);
|
||||
const missing = authRequired(resolved.repo, "pr comment delete", probe);
|
||||
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "pr comment delete", { present: false, source: null, ghFallbackAttempted: true });
|
||||
return withNumberOptionHint(commentDelete(resolved.repo, token, "pr", resolved.number, false), resolved);
|
||||
}
|
||||
if (sub === "comment" && third === "create") {
|
||||
const number = parseNumberForCommand(options.repo, args[3], "pr comment create");
|
||||
if (typeof number !== "number") return number;
|
||||
if (options.dryRun) return prComment(options.repo, "", number, options);
|
||||
const resolved = resolvePositionalPrReference(args, 3, "pr comment create", options);
|
||||
if (isGitHubCommandResult(resolved)) return resolved;
|
||||
if (options.dryRun) return withNumberOptionHint(prComment(resolved.repo, "", resolved.number, { ...options, repo: resolved.repo }), resolved);
|
||||
const { token, probe } = resolveToken(true);
|
||||
const missing = authRequired(options.repo, "pr comment create", probe);
|
||||
const missing = authRequired(resolved.repo, "pr comment create", probe);
|
||||
if (missing !== null || token === null) return missing ?? authRequired(options.repo, "pr comment create", { present: false, source: null, ghFallbackAttempted: true });
|
||||
return prComment(options.repo, token, number, options);
|
||||
return withNumberOptionHint(prComment(resolved.repo, token, resolved.number, { ...options, repo: resolved.repo }), resolved);
|
||||
}
|
||||
if (sub === "create") {
|
||||
if (options.dryRun) return prCreate(options.repo, "", options);
|
||||
@@ -6782,35 +6937,35 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
}
|
||||
if (sub === "comment") {
|
||||
if (options.dryRun) {
|
||||
const number = parseNumberForCommand(options.repo, third, "pr comment");
|
||||
if (typeof number !== "number") return number;
|
||||
return prComment(options.repo, "", number, options);
|
||||
const resolved = resolvePositionalPrReference(args, 2, "pr comment", options);
|
||||
if (isGitHubCommandResult(resolved)) return resolved;
|
||||
return withNumberOptionHint(prComment(resolved.repo, "", resolved.number, { ...options, repo: resolved.repo }), resolved);
|
||||
}
|
||||
const { token, probe } = resolveToken(true);
|
||||
const missing = authRequired(options.repo, "pr comment", probe);
|
||||
if (missing !== null || token === null) return missing ?? authRequired(options.repo, "pr comment", { present: false, source: null, ghFallbackAttempted: true });
|
||||
const number = parseNumberForCommand(options.repo, third, "pr comment");
|
||||
if (typeof number !== "number") return number;
|
||||
return prComment(options.repo, token, number, options);
|
||||
const resolved = resolvePositionalPrReference(args, 2, "pr comment", options);
|
||||
if (isGitHubCommandResult(resolved)) return resolved;
|
||||
return withNumberOptionHint(prComment(resolved.repo, token, resolved.number, { ...options, repo: resolved.repo }), resolved);
|
||||
}
|
||||
if (sub === "update" || sub === "edit") {
|
||||
const commandName = `pr ${sub}`;
|
||||
const number = parseNumberForCommand(options.repo, third, commandName);
|
||||
if (typeof number !== "number") return number;
|
||||
if (options.dryRun && options.mode === "replace") return prUpdate(options.repo, "", number, options, commandName);
|
||||
const resolved = resolvePositionalPrReference(args, 2, commandName, options);
|
||||
if (isGitHubCommandResult(resolved)) return resolved;
|
||||
if (options.dryRun && options.mode === "replace") return withNumberOptionHint(prUpdate(resolved.repo, "", resolved.number, { ...options, repo: resolved.repo }, commandName), resolved);
|
||||
const { token, probe } = resolveToken(true);
|
||||
const missing = authRequired(options.repo, commandName, probe);
|
||||
const missing = authRequired(resolved.repo, commandName, probe);
|
||||
if (missing !== null || token === null) return missing ?? authRequired(options.repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
|
||||
return prUpdate(options.repo, token, number, options, commandName);
|
||||
return withNumberOptionHint(prUpdate(resolved.repo, token, resolved.number, { ...options, repo: resolved.repo }, commandName), resolved);
|
||||
}
|
||||
if (sub === "close" || sub === "reopen") {
|
||||
const number = parseNumberForCommand(options.repo, third, `pr ${sub}`);
|
||||
if (typeof number !== "number") return number;
|
||||
if (options.dryRun) return prState(options.repo, "", number, sub === "close" ? "closed" : "open", true);
|
||||
const resolved = resolvePositionalPrReference(args, 2, `pr ${sub}`, options);
|
||||
if (isGitHubCommandResult(resolved)) return resolved;
|
||||
if (options.dryRun) return withNumberOptionHint(prState(resolved.repo, "", resolved.number, sub === "close" ? "closed" : "open", true), resolved);
|
||||
const { token, probe } = resolveToken(true);
|
||||
const missing = authRequired(options.repo, `pr ${sub}`, probe);
|
||||
const missing = authRequired(resolved.repo, `pr ${sub}`, probe);
|
||||
if (missing !== null || token === null) return missing ?? authRequired(options.repo, `pr ${sub}`, { present: false, source: null, ghFallbackAttempted: true });
|
||||
return prState(options.repo, token, number, sub === "close" ? "closed" : "open", false);
|
||||
return withNumberOptionHint(prState(resolved.repo, token, resolved.number, sub === "close" ? "closed" : "open", false), resolved);
|
||||
}
|
||||
if (sub === "merge") {
|
||||
const resolved = resolvePositionalPrReference(args, 2, "pr merge", options);
|
||||
@@ -6818,7 +6973,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
const { token, probe } = resolveToken(true);
|
||||
const missing = authRequired(options.repo, "pr merge", probe);
|
||||
if (missing !== null || token === null) return missing ?? authRequired(options.repo, "pr merge", { present: false, source: null, ghFallbackAttempted: true });
|
||||
return prMerge(resolved.repo, token, resolved.number, options);
|
||||
return withNumberOptionHint(prMerge(resolved.repo, token, resolved.number, options), resolved);
|
||||
}
|
||||
if (sub !== "list" && !isPrReadCommand(sub)) {
|
||||
return unsupportedCommand(`pr ${sub ?? ""}`.trim(), options.repo, "PR supported commands are list, files, diff --stat, read/view, preflight/closeout, create, update/edit, close, reopen, merge, comment create/delete, and unsupported delete.");
|
||||
@@ -6830,8 +6985,8 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
const missing = authRequired(resolved.repo, `pr ${sub}`, probe);
|
||||
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, `pr ${sub}`, { present: false, source: null, ghFallbackAttempted: true });
|
||||
const disclosure = readDisclosureOptions(options, resolved.shorthand);
|
||||
if (sub === "read") return prRead(resolved.repo, token, resolved.number, prReadJsonFields(options), "pr read", disclosure);
|
||||
return prView(resolved.repo, token, resolved.number, prReadJsonFields(options), disclosure);
|
||||
if (sub === "read") return withNumberOptionHint(prRead(resolved.repo, token, resolved.number, prReadJsonFields(options), "pr read", disclosure), resolved);
|
||||
return withNumberOptionHint(prView(resolved.repo, token, resolved.number, prReadJsonFields(options), disclosure), resolved);
|
||||
}
|
||||
const { token, probe } = resolveToken(true);
|
||||
const missing = authRequired(options.repo, `pr ${sub}`, probe);
|
||||
|
||||
Reference in New Issue
Block a user