refactor: split github cli implementation (#908)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user