// SPEC: PJ2026-010606 GitHub入口 draft-2026-06-25-gh-split // Moved mechanically from scripts/src/gh.ts:5552-5680. import path from "node:path"; import { bodySafetySignals, bodySha, preview, previewLines, repoParts } from "./auth-and-safety"; import { githubRequest } from "./client"; import type { BodyUpdateMode, GitHubBranch, GitHubCompare, GitHubErrorPayload, GitHubOptions, GitHubRepository } from "./types"; export function repoSummary(repo: GitHubRepository): Record { return { id: repo.id ?? null, fullName: repo.full_name ?? null, private: repo.private ?? null, defaultBranch: repo.default_branch ?? null, permissions: repo.permissions ?? null, }; } export function branchSummary(branch: GitHubBranch): Record { return { name: branch.name ?? null, sha: branch.commit?.sha ?? null, }; } export function compareSummary(compare: GitHubCompare): Record { return { status: compare.status ?? null, aheadBy: compare.ahead_by ?? null, behindBy: compare.behind_by ?? null, totalCommits: compare.total_commits ?? null, htmlUrl: compare.html_url ?? null, baseSha: compare.base_commit?.sha ?? null, mergeBaseSha: compare.merge_base_commit?.sha ?? null, }; } export function prUrl(repo: string, number: number): string { return `https://github.com/${repo}/pull/${number}`; } export function encodePathPart(value: string): string { return encodeURIComponent(value); } export async function repoInfo(token: string, repo: string): Promise { const { owner, name } = repoParts(repo); return githubRequest(token, "GET", `/repos/${owner}/${name}`); } export async function branchInfo(token: string, repo: string, branch: string): Promise { const { owner, name } = repoParts(repo); return githubRequest(token, "GET", `/repos/${owner}/${name}/branches/${encodePathPart(branch)}`); } export async function compareBranches(token: string, repo: string, base: string, head: string): Promise { const { owner, name } = repoParts(repo); return githubRequest(token, "GET", `/repos/${owner}/${name}/compare/${encodePathPart(base)}...${encodePathPart(head)}`); } export function prCreatePlannedOperation(repo: string, options: GitHubOptions, body: string, bodySource: Record): Record { return { repo, title: options.title, base: options.base, head: options.head, draft: options.draft, bodyChars: body.length, bodyPreview: preview(body), bodyPreviewLines: body.split(/\r?\n/).slice(0, 12), bodySource, preservesRawNewlines: body.includes("\n"), containsLiteralBackslashN: body.includes("\\n"), containsBackticks: body.includes("`"), request: { method: "POST", path: "/repos/{owner}/{repo}/pulls", body: { title: options.title, base: options.base, head: options.head, draft: options.draft, bodyChars: body.length, }, }, validation: { repo: "required", base: "required", head: "required", title: "required", body: body.length > 0 ? "optional" : "empty", }, }; } export function prCommentPlannedOperation(repo: string, issueNumber: number, body: string, bodySource: Record): Record { return { repo, issueNumber, bodyChars: body.length, bodyPreview: preview(body), bodyPreviewLines: body.split(/\r?\n/).slice(0, 12), bodySource, preservesRawNewlines: body.includes("\n"), containsLiteralBackslashN: body.includes("\\n"), containsBackticks: body.includes("`"), request: { method: "POST", path: `/repos/{owner}/{repo}/issues/${issueNumber}/comments`, body: { bodyChars: body.length }, }, validation: { repo: "required", issueNumber: "required", body: "required", }, }; } export function bodyUpdatePlan(command: string, repo: string, number: number, mode: BodyUpdateMode, body: string, bodySource: Record, existingBody: string | null): Record { const finalBody = mode === "append" ? `${existingBody ?? ""}${body}` : body; return { command, repo, number, mode, bodySource, input: bodySafetySignals(body), existingBody: existingBody === null ? { fetched: false } : { fetched: true, bodyChars: existingBody.length, bodySha: bodySha(existingBody) }, finalBody: { bodyChars: finalBody.length, bodyPreview: preview(finalBody), bodyPreviewLines: previewLines(finalBody), ...bodySafetySignals(finalBody), }, }; }