184 lines
8.0 KiB
TypeScript
184 lines
8.0 KiB
TypeScript
// SPEC: PJ2026-010606 GitHub入口 draft-2026-06-25-gh-split
|
|
// Moved mechanically from scripts/src/gh.ts:6534-6664.
|
|
|
|
import path from "node:path";
|
|
import { commandError, isGitHubError, validationError } from "./client";
|
|
import { githubSearchLabelQualifier, issueListPaginationSummary, listIssues } from "./issue-read";
|
|
import { issueListSummary } from "./issue-summary";
|
|
import { isRecord } from "./notify-claudeqq";
|
|
import { ghShort, ghTable, ghText } from "./render";
|
|
import { GITHUB_REST_PAGE_SIZE, ISSUE_LIST_JSON_FIELDS, MAX_ISSUE_LIST_LIMIT } from "./types";
|
|
import type { GitHubCommandResult, IssueListJsonField, IssueListState } from "./types";
|
|
|
|
const COMPACT_JSON_ISSUE_LIMIT = 40;
|
|
|
|
export function shellWord(value: string): string {
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
export function issueListNextCommand(repo: string, state: IssueListState, limit: number, search: string, labels: string[], titlePrefix: string): string {
|
|
const parts = [
|
|
"bun scripts/cli.ts gh issue list",
|
|
"--repo", shellWord(repo),
|
|
"--state", state,
|
|
"--limit", String(Math.min(limit * 2, MAX_ISSUE_LIST_LIMIT)),
|
|
];
|
|
if (search.length > 0) parts.push("--search", shellWord(search));
|
|
if (titlePrefix.length > 0) parts.push("--title-prefix", shellWord(titlePrefix));
|
|
for (const label of labels) parts.push("--label", shellWord(label));
|
|
return parts.join(" ");
|
|
}
|
|
|
|
export function issueListLabelText(value: unknown): string {
|
|
if (!Array.isArray(value)) return ghText(value);
|
|
const labels = value
|
|
.map((item) => typeof item === "string" ? item : isRecord(item) ? ghText(item.name) : ghText(item))
|
|
.filter((item) => item !== "-");
|
|
return labels.length === 0 ? "-" : labels.join(",");
|
|
}
|
|
|
|
export function withIssueListRendered(result: GitHubCommandResult): GitHubCommandResult {
|
|
return {
|
|
...result,
|
|
contentType: "text/plain",
|
|
renderedText: renderIssueListTable(result),
|
|
};
|
|
}
|
|
|
|
export function renderIssueListTable(result: GitHubCommandResult): string {
|
|
const issues = Array.isArray(result.issues) ? result.issues.map((item) => isRecord(item) ? item : {}) : [];
|
|
const rows = issues.map((issue) => [
|
|
issue.number === undefined ? "-" : `#${ghText(issue.number)}`,
|
|
ghText(issue.state),
|
|
ghShort(ghText(issue.updatedAt), 19),
|
|
ghShort(issueListLabelText(issue.labels), 32),
|
|
ghShort(ghText(issue.title), 96),
|
|
]);
|
|
const next = isRecord(result.next) ? result.next : {};
|
|
const lines = [
|
|
"gh issue list (observed)",
|
|
"",
|
|
ghTable(["ISSUE", "STATE", "UPDATED", "LABELS", "TITLE"], rows),
|
|
"",
|
|
"Summary:",
|
|
` repo=${ghText(result.repo)} state=${ghText(result.state)} count=${ghText(result.count)} raw=${ghText(result.rawCount)} hasMore=${ghText(result.hasMore)}`,
|
|
` search=${ghText(result.search)} labels=${issueListLabelText(result.labels)} titlePrefix=${ghText(result.titlePrefix)}`,
|
|
];
|
|
if (result.searchTotalCount !== undefined) lines.push(` searchTotal=${ghText(result.searchTotalCount)} incomplete=${ghText(result.searchIncomplete)}`);
|
|
if (typeof next.command === "string" && next.command.length > 0) {
|
|
lines.push("", "Next:", ` ${next.command}`);
|
|
}
|
|
lines.push("", "Disclosure:");
|
|
lines.push(" default view is a bounded table; use --json for selected fields or --full/--raw for structured pagination and request metadata.");
|
|
return lines.join("\n");
|
|
}
|
|
|
|
export async function issueList(repo: string, token: string, state: IssueListState, limit: number, jsonFields: IssueListJsonField[] | undefined, search?: string, labels: string[] = [], noDump = false, titlePrefix?: string): Promise<GitHubCommandResult> {
|
|
const normalizedSearch = String(search ?? "").trim();
|
|
const normalizedTitlePrefix = String(titlePrefix ?? "").trim();
|
|
if (titlePrefix !== undefined && normalizedTitlePrefix.length === 0) return validationError("issue list", repo, "--title-prefix requires a non-empty value");
|
|
const result = await listIssues(token, repo, state, limit, normalizedSearch, labels);
|
|
if (isGitHubError(result)) return commandError("issue list", repo, result, { state, limit, search: normalizedSearch || null, titlePrefix: normalizedTitlePrefix || null, labels });
|
|
const listedIssues = result.items;
|
|
const issues = normalizedTitlePrefix.length > 0
|
|
? listedIssues.filter((issue) => issue.title.startsWith(normalizedTitlePrefix))
|
|
: listedIssues;
|
|
const fields = jsonFields ?? ISSUE_LIST_JSON_FIELDS.slice();
|
|
const selectedIssues = issues.map((issue) => issueListSummary(issue, fields));
|
|
const compactJson = jsonFields !== undefined && !noDump;
|
|
const returnedIssues = compactJson ? selectedIssues.slice(0, COMPACT_JSON_ISSUE_LIMIT) : selectedIssues;
|
|
const omittedIssues = Math.max(0, selectedIssues.length - returnedIssues.length);
|
|
const fullCommand = [
|
|
"bun scripts/cli.ts gh issue list",
|
|
"--repo", shellWord(repo),
|
|
"--state", state,
|
|
"--limit", String(limit),
|
|
...(normalizedSearch.length > 0 ? ["--search", shellWord(normalizedSearch)] : []),
|
|
...(normalizedTitlePrefix.length > 0 ? ["--title-prefix", shellWord(normalizedTitlePrefix)] : []),
|
|
...labels.flatMap((label) => ["--label", shellWord(label)]),
|
|
"--json", shellWord(fields.join(",")),
|
|
"--full",
|
|
].join(" ");
|
|
const payload: GitHubCommandResult = {
|
|
ok: true,
|
|
command: "issue list",
|
|
repo,
|
|
...(noDump ? { noDump: true } : {}),
|
|
state,
|
|
limit,
|
|
search: normalizedSearch || null,
|
|
titlePrefix: normalizedTitlePrefix || null,
|
|
...(normalizedTitlePrefix.length > 0
|
|
? {
|
|
titleFilter: {
|
|
field: "title",
|
|
operator: "startsWith",
|
|
prefix: normalizedTitlePrefix,
|
|
scope: "bounded-listed-issues",
|
|
inputCount: listedIssues.length,
|
|
outputCount: issues.length,
|
|
filteredOut: listedIssues.length - issues.length,
|
|
},
|
|
}
|
|
: {}),
|
|
labels,
|
|
count: issues.length,
|
|
rawCount: result.rawCount,
|
|
searchTotalCount: result.searchTotalCount,
|
|
searchIncomplete: result.searchIncomplete,
|
|
...(compactJson ? {} : { pagination: issueListPaginationSummary(result) }),
|
|
hasMore: result.hasMore,
|
|
jsonFields: fields,
|
|
issues: returnedIssues,
|
|
...(compactJson
|
|
? {
|
|
issueListDisclosure: {
|
|
mode: "bounded-json",
|
|
returned: returnedIssues.length,
|
|
omitted: omittedIssues,
|
|
totalMatchedInBoundedScan: issues.length,
|
|
inlineLimit: COMPACT_JSON_ISSUE_LIMIT,
|
|
fullCommand,
|
|
note: "Default --json issue list output is bounded for interactive use; add --full for complete selected fields and pagination/request metadata.",
|
|
},
|
|
}
|
|
: {}),
|
|
...(omittedIssues > 0
|
|
? {
|
|
omittedIssues,
|
|
fullCommand,
|
|
}
|
|
: {}),
|
|
...(result.hasMore && limit < MAX_ISSUE_LIST_LIMIT
|
|
? {
|
|
next: {
|
|
command: issueListNextCommand(repo, state, limit, normalizedSearch, labels, normalizedTitlePrefix),
|
|
note: "Increase --limit to continue scanning beyond the current bounded result set.",
|
|
},
|
|
}
|
|
: result.hasMore
|
|
? {
|
|
scanLimit: {
|
|
maxLimitReached: true,
|
|
maxLimit: MAX_ISSUE_LIST_LIMIT,
|
|
note: "The command reached the maximum bounded issue scan; narrow with --state/--label/--search before treating the repository as exhausted.",
|
|
},
|
|
}
|
|
: {}),
|
|
...(compactJson
|
|
? {}
|
|
: {
|
|
request: {
|
|
method: "GET",
|
|
path: normalizedSearch ? "/search/issues" : "/repos/{owner}/{repo}/issues",
|
|
query: normalizedSearch
|
|
? { q: `${normalizedSearch} repo:${repo} type:issue${state === "all" ? "" : ` state:${state}`}${labels.map((label) => ` ${githubSearchLabelQualifier(label)}`).join("")}`, per_page: GITHUB_REST_PAGE_SIZE }
|
|
: { state, labels, per_page: GITHUB_REST_PAGE_SIZE },
|
|
localTitlePrefixFilter: normalizedTitlePrefix || null,
|
|
},
|
|
}),
|
|
note: "GitHub's issues endpoint may include pull requests; this command filters pull requests from .issues.",
|
|
};
|
|
return jsonFields === undefined && !noDump ? withIssueListRendered(payload) : payload;
|
|
}
|