Files
pikasTech-unidesk/scripts/src/gh/index.ts
T
2026-07-02 09:29:00 +00:00

590 lines
45 KiB
TypeScript

// 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<GitHubCommandResult | unknown> {
const scopedHelp = ghScopedHelp(args);
if (scopedHelp !== null) return scopedHelp;
const [top, sub, third] = args;
if (top === undefined || top === "help" || top === "--help" || top === "-h") return ghHelp();
let options: GitHubOptions;
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#<number> --raw",
"bun scripts/cli.ts gh issue view <number> --repo owner/name --json body,title,state,comments",
"bun scripts/cli.ts gh issue comment view <commentId> --repo owner/name --full",
"bun scripts/cli.ts gh issue update <number> --repo owner/name --body-stdin --full <<'EOF'\n<reviewed body>\nEOF",
"bun scripts/cli.ts gh issue patch <number> --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#<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",
"bun scripts/cli.ts gh pr diff <number> --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 <number> --stat.", {
supportedCommands: [
"bun scripts/cli.ts gh pr files <number> --repo owner/name --limit 30",
"bun scripts/cli.ts gh pr diff <number> --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 <number> --file <path>", {
supportedCommands: [
"bun scripts/cli.ts gh pr review-plan <number> --repo owner/name",
"bun scripts/cli.ts gh pr diff <number> --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 <number> --file <path> --hunk <n>", {
supportedCommands: [
"bun scripts/cli.ts gh pr diff <number> --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 <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")))) {
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 <number> --repo owner/name --body-patch-stdin",
"bun scripts/cli.ts gh issue comment patch <commentId> --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 <number> --repo owner/name",
"bun scripts/cli.ts gh issue attachment download <number> --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 <number> --file <path> for bounded patch drill-down, or gh pr diff <number> --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() });
}