// SPEC: PJ2026-010606 GitHub入口 draft-2026-06-25-gh-split // Moved mechanically from scripts/src/gh.ts:2463-2672. import path from "node:path"; import { bodySha, preview, previewLines, repoParts } from "./auth-and-safety"; import { commandError, githubRequest, isGitHubError, issueBodyReadCommands, validationError } from "./client"; import { repoSummary } from "./repo-compare"; import { ISSUE_LIFECYCLE_PREVIEW_LIMIT } from "./types"; import type { GitHubAuthenticatedUser, GitHubCommandResult, GitHubIssue, GitHubOptions, GitHubRepository, IssueListJsonField } from "./types"; export function issueSummary(issue: GitHubIssue, options: { includeBody?: boolean; includePreview?: boolean; previewLineCount?: number } = {}): Record { const body = issue.body ?? ""; const includeBody = options.includeBody ?? true; const includePreview = options.includePreview ?? true; const summary: Record = { id: issue.id, number: issue.number, title: issue.title, state: issue.state, closed: issue.state === "closed", closedAt: issue.closed_at ?? null, url: issue.html_url, author: issue.user?.login ?? null, createdAt: issue.created_at ?? null, updatedAt: issue.updated_at ?? null, commentCount: issue.comments ?? null, bodyChars: body.length, bodySha: bodySha(body), }; if (includeBody) { summary.body = body; } else { summary.bodyOmitted = true; summary.fullBodyIncluded = false; if (includePreview) { summary.bodyPreview = preview(body); summary.bodyPreviewLines = previewLines(body, options.previewLineCount ?? 8); } } return summary; } export async function repoView(repo: string, token: string): Promise { const { owner, name } = repoParts(repo); const result = await githubRequest(token, "GET", `/repos/${owner}/${name}`); if (isGitHubError(result)) return commandError("repo view", repo, result); return { ok: true, command: "repo view", repo, repository: repoSummary(result), request: { method: "GET", path: `/repos/${owner}/${name}`, }, rest: true, }; } export async function repoCreate(repo: string, token: string, options: GitHubOptions): Promise { const { owner, name } = repoParts(repo); const visibility = options.repoVisibility ?? "private"; const viewer = options.dryRun ? null : await githubRequest(token, "GET", "/user"); if (isGitHubError(viewer)) return commandError("repo create", repo, viewer, { phase: "viewer" }); const viewerLogin = viewer?.login ?? null; const createPath = viewerLogin !== null && viewerLogin.toLowerCase() === owner.toLowerCase() ? "/user/repos" : `/orgs/${owner}/repos`; const payload: Record = { name, private: visibility === "private", auto_init: options.repoAutoInit, }; if (options.repoDescription !== undefined) payload.description = options.repoDescription; const planned = { repo, owner, name, visibility, description: options.repoDescription ?? null, autoInit: options.repoAutoInit, request: { method: "POST", path: createPath, body: { name, private: visibility === "private", descriptionChars: options.repoDescription?.length ?? 0, auto_init: options.repoAutoInit, }, }, }; if (options.dryRun) { return { ok: true, command: "repo create", repo, dryRun: true, planned, note: "Dry-run only; no GitHub repository was created.", }; } const existing = await githubRequest(token, "GET", `/repos/${owner}/${name}`); if (!isGitHubError(existing)) { return validationError("repo create", repo, "repository already exists; refusing duplicate create", { repository: repoSummary(existing), planned, next: { command: `bun scripts/cli.ts gh repo view ${repo}`, }, }); } if (existing.degradedReason !== "repo-not-found") { return commandError("repo create", repo, existing, { phase: "preflight-existing-repo", planned }); } const created = await githubRequest(token, "POST", createPath, payload); if (isGitHubError(created)) return commandError("repo create", repo, created, { planned }); return { ok: true, command: "repo create", repo, repository: repoSummary(created), planned, rest: true, }; } export function issueLifecycleSummary(issue: GitHubIssue): Record { return { id: issue.id, number: issue.number, title: issue.title, state: issue.state, closed: issue.state === "closed", closedAt: issue.closed_at ?? null, url: issue.html_url, author: issue.user?.login ?? null, createdAt: issue.created_at ?? null, updatedAt: issue.updated_at ?? null, commentCount: issue.comments ?? null, labels: issueLabelNames(issue), bodyChars: (issue.body ?? "").length, bodySha: bodySha(issue.body ?? ""), bodyOmitted: true, fullBodyIncluded: false, }; } export function issueLifecycleDisclosure(repo: string, issueNumber: number, dryRun: boolean): Record { return { defaultCompact: true, explicitFullDisclosure: false, fullBodyIncluded: false, bodyOmitted: true, dryRunBoundedPreview: dryRun, 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), }; } export function issueLifecycleBatchSummary(issues: GitHubIssue[]): Record { const visible = issues.slice(0, ISSUE_LIFECYCLE_PREVIEW_LIMIT); return { count: issues.length, returned: visible.length, omitted: Math.max(0, issues.length - visible.length), numbers: visible.map((issue) => issue.number), issues: visible.map(issueLifecycleSummary), }; } export function labelSummary(label: string | { name?: string; color?: string; description?: string | null }): Record { if (typeof label === "string") return { name: label, color: null, description: null }; return { name: label.name ?? "", color: label.color ?? null, description: label.description ?? null, }; } export function issueLabelNames(issue: GitHubIssue): string[] { return (issue.labels ?? []).map((label) => typeof label === "string" ? label : label.name ?? "").filter((label) => label.length > 0); } export function issueListSummary(issue: GitHubIssue, fields: IssueListJsonField[]): Record { const summary: Record = { number: issue.number, title: issue.title, state: issue.state, closed: issue.state === "closed", closedAt: issue.closed_at ?? null, url: issue.html_url, updatedAt: issue.updated_at ?? null, createdAt: issue.created_at ?? null, author: issue.user?.login ?? null, labels: (issue.labels ?? []).map(labelSummary), }; const selected: Record = {}; for (const field of fields) selected[field] = summary[field]; return selected; }