// SPEC: PJ2026-010606 GitHub入口 draft-2026-06-25-gh-split // Moved mechanically from scripts/src/gh.ts:8348-8845. import { issueAttachmentDownload, issueAttachmentList } from "./attachments"; import { resolveToken } from "./auth-and-safety"; import { authStatus, prFiles, prList, prRead, prView } from "./auth-pr-read"; import { issueBoardAudit } from "./board-audit"; import { issueBoardRowAdd, issueBoardRowDelete, issueBoardRowGet, issueBoardRowList, issueBoardRowMove, issueBoardRowUpdate, issueBoardRowUpsert } from "./board-commands"; import { authRequired, unsupportedCommand, validationError } from "./client"; import { commentDelete, commentPatch, commentUpdate, commentView, issueComment, issueState } from "./comments-and-state"; import { issueScanEscape } from "./escape-scan"; import { ghHelp, ghScopedHelp } from "./help"; import { issueList } from "./issue-list"; import { issueRead, issueView } from "./issue-read"; import { repoCreate, repoView } from "./issue-summary"; import { issueCreate, issueEdit, issuePatch } from "./issue-write"; import { allowsNumberTargetAlias, isIssueReadCommand, isPrReadCommand, optionValue, optionWasProvided, parseOptions } from "./options"; import { prComment, prCreate, prState, prUpdate } from "./pr-commands"; import { prMerge, prPreflight } from "./pr-merge"; import { prDiffFile, prReviewPlan } from "./pr-review"; import { isGitHubCommandResult, issueReadJsonFields, prReadJsonFields, readDisclosureOptions, readViewSupportedJsonFields, resolvePositionalIssueReference, resolvePositionalNumberReference, resolvePositionalPrReference, resolveReadViewNumberReference, unknownGhOptionDetails, withNumberOptionHint } from "./refs"; import { issueStaleClose } from "./stale-close"; import { DEFAULT_REPO } from "./types"; import type { GitHubCommandResult, GitHubOptions } from "./types"; export async function runGhCommand(args: string[]): Promise { const scopedHelp = ghScopedHelp(args); if (scopedHelp !== null) return scopedHelp; const [top, sub, third] = args; if (top === undefined || top === "help" || top === "--help" || top === "-h") return ghHelp(); let options: GitHubOptions; try { options = parseOptions(args); } catch (error) { const message = error instanceof Error ? error.message : String(error); const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; const repo = optionValue(args, "--repo") ?? DEFAULT_REPO; const unknownOption = /^unknown gh option:\s+(.+)$/u.exec(message)?.[1]; return unknownOption !== undefined ? unsupportedCommand(command, repo, message, unknownGhOptionDetails(args, unknownOption)) : validationError(command, repo, message); } if (options.notifyClaudeQqBriefDiff && !(top === "issue" && sub === "edit")) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--notify-claudeqq-brief-diff is only supported by gh issue edit 24"); } if (optionWasProvided(args, "--state") && !(top === "issue" && (sub === "list" || sub === "board-row" && third === "list") || top === "pr" && sub === "list")) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--state is only supported by gh issue list, gh issue board-row list, and gh pr list"); } if (optionWasProvided(args, "--json") && !(top === "issue" && (isIssueReadCommand(sub) || sub === "list"))) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; if (!(top === "pr" && (isPrReadCommand(sub) || sub === "list"))) { return validationError(command, options.repo, "--json field selection is only supported by gh issue read/view/list and gh pr read/view/list"); } } const isIssueCommentReadCommand = top === "issue" && sub === "comment" && (third === "view" || third === "read"); const isPrCommentReadCommand = top === "pr" && sub === "comment" && (third === "view" || third === "read"); const isPrDiffFileCommand = top === "pr" && sub === "diff" && optionWasProvided(args, "--file"); if ((optionWasProvided(args, "--raw") || optionWasProvided(args, "--full")) && !((top === "issue" && (isIssueReadCommand(sub) || sub === "list" || sub === "update" || sub === "edit" || sub === "patch")) || isIssueCommentReadCommand || top === "preflight" || (top === "pr" && (isPrReadCommand(sub) || sub === "list" || sub === "preflight" || sub === "closeout")) || isPrCommentReadCommand || isPrDiffFileCommand)) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--raw and --full are explicit full-disclosure aliases only for gh issue list/read/view/update/edit/patch/comment view, gh pr list/read/view/comment view, gh pr preflight/closeout, and gh pr diff --file.", { supportedCommands: [ "bun scripts/cli.ts gh issue list --repo owner/name --limit 200 --full", "bun scripts/cli.ts gh issue view owner/name# --raw", "bun scripts/cli.ts gh issue view --repo owner/name --json body,title,state,comments", "bun scripts/cli.ts gh issue comment view --repo owner/name --full", "bun scripts/cli.ts gh issue update --repo owner/name --body-stdin --full <<'EOF'\n\nEOF", "bun scripts/cli.ts gh issue patch --repo owner/name --body-patch-stdin --full <<'PATCH'\n*** Begin Patch\n*** Update File: issue.md\n@@\n-old\n+new\n*** End Patch\nPATCH", "bun scripts/cli.ts gh pr list --repo owner/name --limit 100 --full", "bun scripts/cli.ts gh pr view owner/name# --raw", `bun scripts/cli.ts gh pr view --repo owner/name --json ${readViewSupportedJsonFields("pr")}`, "bun scripts/cli.ts gh pr preflight --repo owner/name --full", "bun scripts/cli.ts gh pr diff --repo owner/name --file path/to/file --full", ], }); } if (optionWasProvided(args, "--attachment") && !(top === "issue" && sub === "attachment" && third === "download")) { const command = [top, sub, third].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--attachment is only supported by gh issue attachment download"); } if (optionWasProvided(args, "--output") && !(top === "issue" && sub === "attachment" && third === "download")) { const command = [top, sub, third].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--output is only supported by gh issue attachment download"); } if (optionWasProvided(args, "--stat") && !(top === "pr" && (sub === "diff" || sub === "files"))) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--stat is only supported by gh pr diff --stat.", { supportedCommands: [ "bun scripts/cli.ts gh pr files --repo owner/name --limit 30", "bun scripts/cli.ts gh pr diff --stat --repo owner/name --limit 30", ], }); } if (optionWasProvided(args, "--file") && !(top === "pr" && sub === "diff")) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--file is only supported by gh pr diff --file ", { supportedCommands: [ "bun scripts/cli.ts gh pr review-plan --repo owner/name", "bun scripts/cli.ts gh pr diff --repo owner/name --file path/to/file", ], }); } if (optionWasProvided(args, "--hunk") && !(top === "pr" && sub === "diff" && optionWasProvided(args, "--file"))) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--hunk is only supported by gh pr diff --file --hunk ", { supportedCommands: [ "bun scripts/cli.ts gh pr diff --repo owner/name --file path/to/file --hunk 1", ], }); } if (optionWasProvided(args, "--number") && !allowsNumberTargetAlias(top, sub, third)) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; 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 issue view --repo owner/name --json body,title,state", "bun scripts/cli.ts gh issue view https://github.com/owner/name/issues/ --raw", "bun scripts/cli.ts gh pr view --repo owner/name --json body,title,state,head,base", "bun scripts/cli.ts gh pr view https://github.com/owner/name/pull/ --raw", ], rejectedOption: "--number", }); } if (optionWasProvided(args, "--mode") && !((top === "issue" && (sub === "update" || sub === "edit")) || (top === "pr" && (sub === "update" || sub === "edit")))) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--mode is only supported by gh issue update/edit and gh pr update/edit"); } const issueBodyGuardCommand = top === "issue" && (sub === "edit" || sub === "update" || sub === "patch" || sub === "board-row" && ["update", "add", "move", "delete", "upsert"].includes(third ?? "")); const issueCommentPatchCommand = top === "issue" && sub === "comment" && third === "patch"; if ((options.allowShortBody || optionWasProvided(args, "--body-profile")) && !issueBodyGuardCommand) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--allow-short-body and --body-profile are only supported by gh issue update/edit/patch and gh issue board-row update/add/move/delete/upsert"); } if ((options.expectUpdatedAt !== undefined || options.expectBodySha !== undefined) && !(issueBodyGuardCommand || issueCommentPatchCommand)) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--expect-updated-at and --expect-body-sha are only supported by gh issue update/edit/patch, gh issue comment patch, and gh issue board-row update/add/move/delete/upsert"); } if ((optionWasProvided(args, "--body-patch-file") || optionWasProvided(args, "--body-patch-stdin")) && !(top === "issue" && (sub === "patch" || sub === "comment" && third === "patch"))) { const command = [top, sub, third].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--body-patch-file and --body-patch-stdin are only supported by gh issue patch and gh issue comment patch", { supportedCommands: [ "bun scripts/cli.ts gh issue patch --repo owner/name --body-patch-stdin", "bun scripts/cli.ts gh issue comment patch --repo owner/name --body-patch-stdin", ], }); } if ((optionWasProvided(args, "--board-issue") || optionWasProvided(args, "--known-meta-issue") || optionWasProvided(args, "--ignore-issue")) && !(top === "issue" && (sub === "board-audit" || sub === "board-row"))) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--board-issue, --known-meta-issue, and --ignore-issue are only supported by gh issue board-audit and --board-issue by gh issue board-row"); } if ((optionWasProvided(args, "--known-meta-issue") || optionWasProvided(args, "--ignore-issue")) && !(top === "issue" && sub === "board-audit")) { const command = [top, sub, third].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--known-meta-issue and --ignore-issue are only supported by gh issue board-audit"); } if ((optionWasProvided(args, "--field") || optionWasProvided(args, "--value")) && !(top === "issue" && sub === "board-row" && third === "update")) { const command = [top, sub, third].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--field and --value are only supported by gh issue board-row update"); } if (optionWasProvided(args, "--row-file") && !(top === "issue" && sub === "board-row" && third === "add")) { const command = [top, sub, third].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--row-file is only supported by gh issue board-row add"); } if (optionWasProvided(args, "--section") && !(top === "issue" && sub === "board-row" && (third === "add" || third === "upsert"))) { const command = [top, sub, third].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--section is only supported by gh issue board-row add/upsert"); } if (optionWasProvided(args, "--to") && !(top === "issue" && sub === "board-row" && third === "move")) { const command = [top, sub, third].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--to is only supported by gh issue board-row move"); } if (optionWasProvided(args, "--status") && !(top === "issue" && sub === "board-row" && (third === "move" || third === "upsert"))) { const command = [top, sub, third].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--status is only supported by gh issue board-row move/upsert"); } if ((optionWasProvided(args, "--category") || optionWasProvided(args, "--branch") || optionWasProvided(args, "--tasks") || optionWasProvided(args, "--summary") || optionWasProvided(args, "--focus") || optionWasProvided(args, "--validation") || optionWasProvided(args, "--progress")) && !(top === "issue" && sub === "board-row" && third === "upsert")) { const command = [top, sub, third].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--category, --branch, --tasks, --summary, --focus, --validation, and --progress are only supported by gh issue board-row upsert"); } if (optionWasProvided(args, "--label") && !(top === "issue" && (sub === "create" || sub === "list" || sub === "stale-close"))) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--label is only supported by gh issue create, gh issue list, and gh issue stale-close"); } if (optionWasProvided(args, "--search") && !(top === "issue" && sub === "list")) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--search is only supported by gh issue list"); } if (optionWasProvided(args, "--title-prefix") && !(top === "issue" && sub === "list")) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--title-prefix is only supported by gh issue list"); } if (optionWasProvided(args, "--inactive-hours") && !(top === "issue" && sub === "stale-close")) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--inactive-hours is only supported by gh issue stale-close"); } if (optionWasProvided(args, "--comment") && !(top === "issue" && (sub === "close" || sub === "reopen"))) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--comment/--comment-file is only supported by gh issue close/reopen; use gh issue comment create for standalone comments"); } if ((optionWasProvided(args, "--description") || optionWasProvided(args, "--private") || optionWasProvided(args, "--public") || optionWasProvided(args, "--auto-init")) && !(top === "repo" && sub === "create")) { const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh"; return validationError(command, options.repo, "--description, --private, --public, and --auto-init are only supported by gh repo create"); } if (top === "auth" && sub === "status") return authStatus(options.repo); if (top === "repo") { if (sub !== "view" && sub !== "create") { return unsupportedCommand(`repo ${sub ?? ""}`.trim(), options.repo, "repo supported commands are view and create.", { supportedCommands: [ `bun scripts/cli.ts gh repo view ${options.repo}`, `bun scripts/cli.ts gh repo create ${options.repo} --private --dry-run`, ], }); } if (sub === "create" && options.dryRun) return repoCreate(options.repo, "", options); const { token, probe } = resolveToken(true); const commandName = `repo ${sub}`; const missing = authRequired(options.repo, commandName, probe); if (missing !== null || token === null) return missing ?? authRequired(options.repo, commandName, { present: false, source: null, ghFallbackAttempted: true }); if (sub === "view") return repoView(options.repo, token); return repoCreate(options.repo, token, options); } if (top === "preflight") { const resolved = resolvePositionalPrReference(args, 1, "preflight", options); if (isGitHubCommandResult(resolved)) return resolved; 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 === "attachment") { const action = third; const commandName = `issue attachment ${action ?? ""}`.trim(); if (action !== "list" && action !== "download") { return unsupportedCommand(commandName, options.repo, "issue attachment supported commands are list and download.", { supportedCommands: [ "bun scripts/cli.ts gh issue attachment list --repo owner/name", "bun scripts/cli.ts gh issue attachment download --repo owner/name --attachment 1 --output /tmp/issue-attachment.png", ], }); } const resolved = resolvePositionalIssueReference(args, 3, commandName, options); if (isGitHubCommandResult(resolved)) return resolved; const { token, probe } = resolveToken(true); const missing = authRequired(resolved.repo, commandName, probe); if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, commandName, { present: false, source: null, ghFallbackAttempted: true }); if (action === "list") return withNumberOptionHint(issueAttachmentList(resolved.repo, token, resolved.number), resolved); return withNumberOptionHint(issueAttachmentDownload(resolved.repo, token, resolved.number, { ...options, repo: resolved.repo }), resolved); } if (sub === "comment" && (third === "view" || third === "read")) { const commandName = `issue comment ${third}`; const resolved = resolvePositionalNumberReference("issue", args, 3, commandName, options); if (isGitHubCommandResult(resolved)) return resolved; const commentId = resolved.number; if (typeof commentId !== "number") return commentId; const { token, probe } = resolveToken(true); const missing = authRequired(resolved.repo, commandName, probe); if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, commandName, { present: false, source: null, ghFallbackAttempted: true }); return withNumberOptionHint(commentView(resolved.repo, token, "issue", commentId, options.raw || options.full, commandName), resolved); } if (sub === "comment" && third === "delete") { 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 withNumberOptionHint(commentDelete(resolved.repo, "", "issue", commentId, true), resolved); const { token, probe } = resolveToken(true); 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 === "patch") { const commandName = "issue comment patch"; const resolved = resolvePositionalNumberReference("issue", args, 3, commandName, options); if (isGitHubCommandResult(resolved)) return resolved; const commentId = resolved.number; if (typeof commentId !== "number") return commentId; const { token, probe } = resolveToken(true); const missing = authRequired(resolved.repo, commandName, probe); if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, commandName, { present: false, source: null, ghFallbackAttempted: true }); return withNumberOptionHint(commentPatch(resolved.repo, token, "issue", commentId, { ...options, repo: resolved.repo }, commandName), resolved); } if (sub === "comment" && (third === "update" || third === "edit")) { const commandName = `issue comment ${third}`; const resolved = resolvePositionalNumberReference("issue", args, 3, commandName, options); if (isGitHubCommandResult(resolved)) return resolved; const commentId = resolved.number; if (typeof commentId !== "number") return commentId; const updateOptions = { ...options, repo: resolved.repo }; if (options.dryRun) return withNumberOptionHint(commentUpdate(resolved.repo, "", "issue", commentId, updateOptions, commandName), resolved); const { token, probe } = resolveToken(true); const missing = authRequired(resolved.repo, commandName, probe); if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, commandName, { present: false, source: null, ghFallbackAttempted: true }); return withNumberOptionHint(commentUpdate(resolved.repo, token, "issue", commentId, updateOptions, commandName), 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 withNumberOptionHint(issueComment(resolved.repo, "", issueNumber, { ...options, repo: resolved.repo }), resolved); const { token, probe } = resolveToken(true); 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 withNumberOptionHint(issueComment(resolved.repo, token, issueNumber, { ...options, repo: resolved.repo }), resolved); } if (sub === "scan-escape" || sub === "cleanup-plan") { const { token, probe } = resolveToken(true); const commandName = sub === "cleanup-plan" ? "issue cleanup-plan" : "issue scan-escape"; const missing = authRequired(options.repo, commandName, probe); if (missing !== null || token === null) return missing ?? authRequired(options.repo, commandName, { present: false, source: null, ghFallbackAttempted: true }); return issueScanEscape(options.repo, token, options.limit, options.dryRun || sub === "cleanup-plan", commandName); } if (sub === "board-audit") { const { token, probe } = resolveToken(true); const missing = authRequired(options.repo, "issue board-audit", probe); if (missing !== null || token === null) return missing ?? authRequired(options.repo, "issue board-audit", { present: false, source: null, ghFallbackAttempted: true }); return issueBoardAudit(options.repo, token, options); } if (sub === "board-row") { const action = third; const commandName = `issue board-row ${action ?? ""}`.trim(); if (action !== "list" && action !== "get" && action !== "update" && action !== "add" && action !== "move" && action !== "delete" && action !== "upsert") { return unsupportedCommand(commandName, options.repo, "board-row supported commands are list, get, update, add, move, delete, and upsert."); } const { token, probe } = resolveToken(true); const missing = authRequired(options.repo, commandName, probe); if (missing !== null || token === null) return missing ?? authRequired(options.repo, commandName, { present: false, source: null, ghFallbackAttempted: true }); if (action === "list") return issueBoardRowList(options.repo, token, options); const resolvedBoardRow = resolvePositionalIssueReference(args, 3, commandName, options); if (isGitHubCommandResult(resolvedBoardRow)) return resolvedBoardRow; const issueNumber = resolvedBoardRow.number; if (typeof issueNumber !== "number") return issueNumber; 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 (sub === "patch") { const resolved = resolvePositionalIssueReference(args, 2, "issue patch", options); if (isGitHubCommandResult(resolved)) return resolved; const { token, probe } = resolveToken(true); const missing = authRequired(resolved.repo, "issue patch", probe); if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "issue patch", { present: false, source: null, ghFallbackAttempted: true }); return withNumberOptionHint(issuePatch(resolved.repo, token, resolved.number, { ...options, repo: resolved.repo }), resolved); } if (options.dryRun) { if (sub === "create") return issueCreate(options.repo, "", options); if (sub === "edit") { const issueEditRef = resolvePositionalIssueReference(args, 2, "issue edit", options); if (isGitHubCommandResult(issueEditRef)) return issueEditRef; const issueNumber = issueEditRef.number; const { token } = resolveToken(false); 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 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 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 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 withNumberOptionHint(issueState(r.repo, "", r.number, "open", true, { ...options, repo: r.repo }), r); } } if (sub === "read" || sub === "view") { const resolved = resolveReadViewNumberReference("issue", sub, third, options, args); if (isGitHubCommandResult(resolved)) return resolved; const { token, probe } = resolveToken(true); 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 withNumberOptionHint(issueRead(resolved.repo, token, resolved.number, issueReadJsonFields(options), "issue read", disclosure, { includeFullCommentBodies: options.raw }), resolved); return withNumberOptionHint(issueView(resolved.repo, token, resolved.number, issueReadJsonFields(options), disclosure, { includeFullCommentBodies: options.raw }), resolved); } const { token, probe } = resolveToken(true); const missing = authRequired(options.repo, `issue ${sub ?? ""}`.trim(), probe); if (missing !== null || token === null) return missing ?? authRequired(options.repo, `issue ${sub ?? ""}`.trim(), { present: false, source: null, ghFallbackAttempted: true }); if (sub === "list") return issueList(options.repo, token, options.listState, options.limit, options.issueListJsonFields, options.search, options.labels, options.raw || options.full, options.titlePrefix); if (sub === "stale-close") return issueStaleClose(options.repo, token, options); if (sub === "create") return issueCreate(options.repo, token, options); if (sub === "edit") { const r = resolvePositionalIssueReference(args, 2, "issue edit", options); if (isGitHubCommandResult(r)) return r; 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 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 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 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 withNumberOptionHint(issueState(r.repo, token, r.number, "open", options.dryRun, { ...options, repo: r.repo }), r); } } if (top === "pr") { if (sub === "diff") { const resolved = resolvePositionalPrReference(args, 2, "pr diff", options); if (isGitHubCommandResult(resolved)) return resolved; if (optionWasProvided(args, "--stat") && optionWasProvided(args, "--file")) { return validationError("pr diff", resolved.repo, "choose either --stat for file/stat summary or --file for bounded patch drill-down, not both", { supportedCommands: [ `bun scripts/cli.ts gh pr diff ${resolved.number} --stat --repo ${resolved.repo} --limit 30`, `bun scripts/cli.ts gh pr diff ${resolved.number} --repo ${resolved.repo} --file ${options.filePath ?? "path/to/file"}`, ], }); } if (optionWasProvided(args, "--file")) { const { token, probe } = resolveToken(true); const missing = authRequired(resolved.repo, "pr diff", probe); if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "pr diff", { present: false, source: null, ghFallbackAttempted: true }); return withNumberOptionHint(prDiffFile(resolved.repo, token, resolved.number, { ...options, repo: resolved.repo }), resolved); } if (!optionWasProvided(args, "--stat")) { return unsupportedCommand("pr diff", options.repo, "Raw PR diff output is intentionally unsupported by default; use gh pr review-plan for an index, gh pr diff --file for bounded patch drill-down, or gh pr diff --stat for a file/stat summary.", { rawDiffIncluded: false, supportedCommands: [ `bun scripts/cli.ts gh pr review-plan ${resolved.number} --repo ${resolved.repo}`, `bun scripts/cli.ts gh pr diff ${resolved.number} --repo ${resolved.repo} --file path/to/file`, `bun scripts/cli.ts gh pr files ${resolved.number} --repo ${resolved.repo} --limit 30`, `bun scripts/cli.ts gh pr diff ${resolved.number} --stat --repo ${resolved.repo} --limit 30`, ], }); } 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 withNumberOptionHint(prFiles(resolved.repo, token, resolved.number, options.limit, "pr diff --stat"), resolved); } if (sub === "review-plan") { const resolved = resolvePositionalPrReference(args, 2, "pr review-plan", options); if (isGitHubCommandResult(resolved)) return resolved; const { token, probe } = resolveToken(true); const missing = authRequired(resolved.repo, "pr review-plan", probe); if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "pr review-plan", { present: false, source: null, ghFallbackAttempted: true }); return withNumberOptionHint(prReviewPlan(resolved.repo, token, resolved.number, options.limit), resolved); } if (sub === "files") { const resolved = resolvePositionalPrReference(args, 2, "pr files", options); if (isGitHubCommandResult(resolved)) return resolved; 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 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 withNumberOptionHint(prPreflight(resolved.repo, resolved.number, sub === "closeout" ? "pr closeout" : "pr preflight", options.full || options.raw), resolved); } if (sub === "comment" && third === "delete") { 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(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 === "view" || third === "read")) { const commandName = `pr comment ${third}`; const resolved = resolvePositionalNumberReference("pr", args, 3, commandName, options); if (isGitHubCommandResult(resolved)) return resolved; const { token, probe } = resolveToken(true); const missing = authRequired(resolved.repo, commandName, probe); if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, commandName, { present: false, source: null, ghFallbackAttempted: true }); return withNumberOptionHint(commentView(resolved.repo, token, "pr", resolved.number, options.raw || options.full, commandName), resolved); } if (sub === "comment" && (third === "update" || third === "edit")) { const commandName = `pr comment ${third}`; const resolved = resolvePositionalNumberReference("pr", args, 3, commandName, options); if (isGitHubCommandResult(resolved)) return resolved; const updateOptions = { ...options, repo: resolved.repo }; if (options.dryRun) return withNumberOptionHint(commentUpdate(resolved.repo, "", "pr", resolved.number, updateOptions, commandName), resolved); const { token, probe } = resolveToken(true); const missing = authRequired(resolved.repo, commandName, probe); if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, commandName, { present: false, source: null, ghFallbackAttempted: true }); return withNumberOptionHint(commentUpdate(resolved.repo, token, "pr", resolved.number, updateOptions, commandName), resolved); } if (sub === "comment" && third === "create") { 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(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 withNumberOptionHint(prComment(resolved.repo, token, resolved.number, { ...options, repo: resolved.repo }), resolved); } if (sub === "create") { if (options.dryRun) return prCreate(options.repo, "", options); const { token, probe } = resolveToken(true); const missing = authRequired(options.repo, "pr create", probe); if (missing !== null || token === null) return missing ?? authRequired(options.repo, "pr create", { present: false, source: null, ghFallbackAttempted: true }); return prCreate(options.repo, token, options); } if (sub === "comment") { if (options.dryRun) { 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 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 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(resolved.repo, commandName, probe); if (missing !== null || token === null) return missing ?? authRequired(options.repo, commandName, { present: false, source: null, ghFallbackAttempted: true }); return withNumberOptionHint(prUpdate(resolved.repo, token, resolved.number, { ...options, repo: resolved.repo }, commandName), resolved); } if (sub === "close" || sub === "reopen") { 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(resolved.repo, `pr ${sub}`, probe); if (missing !== null || token === null) return missing ?? authRequired(options.repo, `pr ${sub}`, { present: false, source: null, ghFallbackAttempted: true }); 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); if (isGitHubCommandResult(resolved)) return resolved; 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 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/update/edit/delete, and unsupported delete."); } if (sub === "read" || sub === "view") { const resolved = resolveReadViewNumberReference("pr", sub, third, options, args); if (isGitHubCommandResult(resolved)) return resolved; const { token, probe } = resolveToken(true); 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 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); if (missing !== null || token === null) return missing ?? authRequired(options.repo, `pr ${sub}`, { present: false, source: null, ghFallbackAttempted: true }); if (sub === "list") return prList(options.repo, token, options.prListState, options.limit, options.prListJsonFields, options.raw || options.full); } return unsupportedCommand(args.join(" ") || "gh", options.repo, "Unsupported gh command", { help: ghHelp() }); }