Files
pikasTech-unidesk/scripts/src/gh/issue-summary.ts
T
2026-06-26 00:16:53 +08:00

202 lines
7.1 KiB
TypeScript

// 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<string, unknown> {
const body = issue.body ?? "";
const includeBody = options.includeBody ?? true;
const includePreview = options.includePreview ?? true;
const summary: Record<string, unknown> = {
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<GitHubCommandResult> {
const { owner, name } = repoParts(repo);
const result = await githubRequest<GitHubRepository>(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<GitHubCommandResult> {
const { owner, name } = repoParts(repo);
const visibility = options.repoVisibility ?? "private";
const viewer = options.dryRun ? null : await githubRequest<GitHubAuthenticatedUser>(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<string, unknown> = {
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<GitHubRepository>(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<GitHubRepository>(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<string, unknown> {
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<string, unknown> {
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<string, unknown> {
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<string, unknown> {
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<string, unknown> {
const summary: Record<IssueListJsonField, unknown> = {
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<string, unknown> = {};
for (const field of fields) selected[field] = summary[field];
return selected;
}