Merge pull request #1388 from pikasTech/issue-1386-gh-output
修复 gh 默认输出渐进披露
This commit is contained in:
+14
-13
@@ -21,6 +21,7 @@ import { runDevEnvCommand } from "./src/dev-env";
|
||||
import { runArtifactRegistryCommand } from "./src/artifact-registry";
|
||||
import { runAuthBrokerCommand } from "./src/auth-broker";
|
||||
import { runGhCommand } from "./src/gh";
|
||||
import { withGhDefaultRendered } from "./src/gh/default-render";
|
||||
import { isGhContentRoute, runGhContentRoute } from "./src/gh-route";
|
||||
import { runGitToolsCommand } from "./src/git-tools";
|
||||
import { runCommanderCommand } from "./src/commander";
|
||||
@@ -282,6 +283,19 @@ async function main(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (top === "gh") {
|
||||
const result = withGhDefaultRendered(args.slice(1), await runGhCommand(args.slice(1)));
|
||||
const ok = (result as { ok?: unknown }).ok !== false;
|
||||
if (isRenderedCliResult(result)) {
|
||||
emitText(result.renderedText, result.command || commandName);
|
||||
if (!ok) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
emitJson(commandName, result, ok);
|
||||
if (!ok) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const namespaceHelp = await staticNamespaceHelp(args);
|
||||
if (namespaceHelp !== null) {
|
||||
emitJson(commandName, namespaceHelp);
|
||||
@@ -318,19 +332,6 @@ async function main(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (top === "gh") {
|
||||
const result = await runGhCommand(args.slice(1));
|
||||
const ok = (result as { ok?: unknown }).ok !== false;
|
||||
if (isRenderedCliResult(result)) {
|
||||
emitText(result.renderedText, result.command || commandName);
|
||||
if (!ok) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
emitJson(commandName, result, ok);
|
||||
if (!ok) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (top === "git") {
|
||||
const result = runGitToolsCommand(args.slice(1));
|
||||
const ok = (result as { ok?: unknown }).ok !== false;
|
||||
|
||||
@@ -0,0 +1,559 @@
|
||||
// SPEC: PJ2026-010606 GitHub入口 draft-2026-06-25-gh-default-render
|
||||
|
||||
import { isRecord } from "./notify-claudeqq";
|
||||
import { ghShort, ghTable, ghText } from "./render";
|
||||
import type { GitHubCommandResult } from "./types";
|
||||
|
||||
const STRUCTURED_FLAGS = new Set(["--json", "--full", "--raw"]);
|
||||
|
||||
export function withGhDefaultRendered(args: string[], value: unknown): unknown {
|
||||
if (wantsStructuredGhOutput(args)) return value;
|
||||
if (isGhHelpPayload(value)) {
|
||||
return {
|
||||
...value,
|
||||
ok: true,
|
||||
command: typeof value.command === "string" ? value.command : "gh",
|
||||
contentType: "text/plain",
|
||||
renderedText: renderGhHelp(value),
|
||||
};
|
||||
}
|
||||
if (!isGitHubCommandResult(value)) return value;
|
||||
if (typeof value.renderedText === "string") return value;
|
||||
return {
|
||||
...value,
|
||||
contentType: "text/plain",
|
||||
renderedText: renderGhDefaultText(value),
|
||||
};
|
||||
}
|
||||
|
||||
function wantsStructuredGhOutput(args: string[]): boolean {
|
||||
return args.some((arg) => STRUCTURED_FLAGS.has(arg));
|
||||
}
|
||||
|
||||
function isGitHubCommandResult(value: unknown): value is GitHubCommandResult {
|
||||
return isRecord(value)
|
||||
&& typeof value.ok === "boolean"
|
||||
&& typeof value.command === "string"
|
||||
&& typeof value.repo === "string";
|
||||
}
|
||||
|
||||
function isGhHelpPayload(value: unknown): value is Record<string, unknown> {
|
||||
return isRecord(value)
|
||||
&& typeof value.command === "string"
|
||||
&& (Array.isArray(value.usage) || Array.isArray(value.notes) || typeof value.fullHelpCommand === "string");
|
||||
}
|
||||
|
||||
function renderGhDefaultText(result: GitHubCommandResult): string {
|
||||
if (result.ok === false) return renderGhError(result);
|
||||
const command = result.command;
|
||||
if (isIssueReadResult(result)) return renderIssueView(result);
|
||||
if (isPrReadResult(result)) return renderPrView(result);
|
||||
if (command === "pr list") return renderPrList(result);
|
||||
if (command === "pr files" || command === "pr diff --stat") return renderPrFiles(result);
|
||||
if (command.includes("comment")) return renderCommentResult(result);
|
||||
if (command === "auth status") return renderAuthStatus(result);
|
||||
if (command.startsWith("repo ")) return renderRepoResult(result);
|
||||
if (command.startsWith("issue board-row")) return renderBoardRowResult(result);
|
||||
if (command === "issue board-audit") return renderBoardAudit(result);
|
||||
if (command === "issue attachment list" || command === "issue attachment download") return renderAttachmentResult(result);
|
||||
if (command === "issue stale-close") return renderStaleClose(result);
|
||||
if (command === "issue scan-escape" || command === "issue cleanup-plan") return renderScanEscape(result);
|
||||
if (command === "issue close" || command === "issue reopen") return renderIssueLifecycle(result);
|
||||
if (command === "issue update" || command === "issue edit" || command === "issue patch") return renderIssueWrite(result);
|
||||
if (command === "pr update" || command === "pr edit" || command === "pr close" || command === "pr reopen") return renderPrWrite(result);
|
||||
return renderGenericResult(result);
|
||||
}
|
||||
|
||||
function renderGhHelp(help: Record<string, unknown>): string {
|
||||
const usage = Array.isArray(help.usage) ? help.usage.filter((item): item is string => typeof item === "string") : [];
|
||||
const notes = Array.isArray(help.notes) ? help.notes.filter((item): item is string => typeof item === "string") : [];
|
||||
const usageRows = usage.slice(0, 24).map((line, index) => [String(index + 1), ghShort(line, 132)]);
|
||||
const lines = [
|
||||
`${ghText(help.command)} help`,
|
||||
"",
|
||||
usageRows.length > 0 ? ghTable(["#", "USAGE"], usageRows) : ghText(help.message),
|
||||
"",
|
||||
"Summary:",
|
||||
` usage=${usage.length} shown=${usageRows.length} notes=${notes.length} output=text`,
|
||||
];
|
||||
if (notes.length > 0) lines.push("", "Notes:", ...notes.slice(0, 8).map((note) => ` ${ghShort(note, 160)}`));
|
||||
const fullHelpCommand = typeof help.fullHelpCommand === "string" ? help.fullHelpCommand : "bun scripts/cli.ts gh --help";
|
||||
lines.push("", "Next:", ` ${fullHelpCommand}`, " bun scripts/cli.ts gh <command> --help");
|
||||
lines.push("", "Disclosure:", " help output is bounded; use scoped help for a specific command or --raw where supported for structured metadata.");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function renderGhError(result: GitHubCommandResult): string {
|
||||
const details = record(result.details);
|
||||
const message = ghText(result.message ?? details.message);
|
||||
const reason = ghText(result.degradedReason ?? details.degradedReason ?? "failed");
|
||||
const lines = [
|
||||
`gh ${result.command} (failed)`,
|
||||
"",
|
||||
ghTable(["COMMAND", "STATUS", "REASON", "MESSAGE"], [[
|
||||
result.command,
|
||||
"failed",
|
||||
ghShort(reason, 36),
|
||||
ghShort(message, 120),
|
||||
]]),
|
||||
"",
|
||||
"Summary:",
|
||||
` repo=${ghText(result.repo)} runnerDisposition=${ghText(result.runnerDisposition ?? details.runnerDisposition)}`,
|
||||
];
|
||||
const supportedCommands = Array.isArray(result.supportedCommands) ? result.supportedCommands.filter((item): item is string => typeof item === "string") : [];
|
||||
if (supportedCommands.length > 0) lines.push("", "Next:", ...supportedCommands.slice(0, 4).map((command) => ` ${command}`));
|
||||
lines.push("", "Disclosure:", " default error output is compact; rerun with explicit --json/--full/--raw where supported for structured diagnostics.");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function isIssueReadResult(result: GitHubCommandResult): boolean {
|
||||
return (result.command === "issue view" || result.command === "issue read") && isRecord(result.issue);
|
||||
}
|
||||
|
||||
function isPrReadResult(result: GitHubCommandResult): boolean {
|
||||
return (result.command === "pr view" || result.command === "pr read") && isRecord(result.pullRequest);
|
||||
}
|
||||
|
||||
function renderIssueView(result: GitHubCommandResult): string {
|
||||
const issue = record(result.issue);
|
||||
const number = ghText(issue.number);
|
||||
const body = bodyInfo(issue);
|
||||
const readCommands = record(record(result.compatibility).readCommands);
|
||||
const lines = [
|
||||
`gh ${result.command} (observed)`,
|
||||
"",
|
||||
ghTable(["ISSUE", "STATE", "UPDATED", "COMMENTS", "BODY", "TITLE"], [[
|
||||
number === "-" ? "-" : `#${number}`,
|
||||
ghText(issue.state),
|
||||
shortDate(issue.updatedAt),
|
||||
ghText(issue.commentCount),
|
||||
body.label,
|
||||
ghShort(ghText(issue.title), 96),
|
||||
]]),
|
||||
"",
|
||||
"Summary:",
|
||||
` repo=${ghText(result.repo)} url=${ghText(issue.url)}`,
|
||||
` author=${ghText(issue.author)} bodySha=${ghText(issue.bodySha)} bodyPreview=${body.preview}`,
|
||||
"",
|
||||
"Next:",
|
||||
` ${ghText(readCommands.body ?? `bun scripts/cli.ts gh issue view ${number} --repo ${result.repo} --json body`)}`,
|
||||
` ${ghText(readCommands.full ?? `bun scripts/cli.ts gh issue view ${number} --repo ${result.repo} --full`)}`,
|
||||
"",
|
||||
"Disclosure:",
|
||||
" default view omits full issue.body and comment bodies; use --json body/comments, --full, or --raw for explicit structured disclosure.",
|
||||
];
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function renderPrView(result: GitHubCommandResult): string {
|
||||
const pr = record(result.pullRequest);
|
||||
const number = ghText(pr.number);
|
||||
const body = bodyInfo(pr);
|
||||
const head = record(pr.head);
|
||||
const base = record(pr.base);
|
||||
return [
|
||||
`gh ${result.command} (observed)`,
|
||||
"",
|
||||
ghTable(["PR", "STATE", "BASE", "HEAD", "DRAFT", "BODY", "TITLE"], [[
|
||||
number === "-" ? "-" : `#${number}`,
|
||||
ghText(pr.stateDetail ?? pr.state),
|
||||
ghText(base.ref ?? pr.baseRefName),
|
||||
ghText(head.ref ?? pr.headRefName),
|
||||
ghText(pr.draft),
|
||||
body.label,
|
||||
ghShort(ghText(pr.title), 96),
|
||||
]]),
|
||||
"",
|
||||
"Summary:",
|
||||
` repo=${ghText(result.repo)} url=${ghText(pr.url)}`,
|
||||
` author=${ghText(pr.author)} merged=${ghText(pr.merged)} bodySha=${ghText(pr.bodySha)} bodyPreview=${body.preview}`,
|
||||
"",
|
||||
"Next:",
|
||||
` bun scripts/cli.ts gh pr files ${number} --repo ${result.repo}`,
|
||||
` bun scripts/cli.ts gh pr view ${number} --repo ${result.repo} --json body,title,state,head,base`,
|
||||
` bun scripts/cli.ts gh pr view ${number} --repo ${result.repo} --full`,
|
||||
"",
|
||||
"Disclosure:",
|
||||
" default view omits full pullRequest.body; use --json body, --full, or --raw for explicit structured disclosure.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderPrList(result: GitHubCommandResult): string {
|
||||
const prs = arrayOfRecords(result.pullRequests);
|
||||
const rows = prs.slice(0, 40).map((pr) => {
|
||||
const head = record(pr.head);
|
||||
const base = record(pr.base);
|
||||
return [
|
||||
`#${ghText(pr.number)}`,
|
||||
ghText(pr.state),
|
||||
shortDate(pr.updatedAt),
|
||||
ghText(base.ref),
|
||||
ghText(head.ref),
|
||||
ghText(pr.draft),
|
||||
ghShort(ghText(pr.title), 96),
|
||||
];
|
||||
});
|
||||
return [
|
||||
"gh pr list (observed)",
|
||||
"",
|
||||
ghTable(["PR", "STATE", "UPDATED", "BASE", "HEAD", "DRAFT", "TITLE"], rows),
|
||||
"",
|
||||
"Summary:",
|
||||
` repo=${ghText(result.repo)} state=${ghText(result.state)} count=${ghText(result.count)} limit=${ghText(result.limit)}`,
|
||||
"",
|
||||
"Next:",
|
||||
" bun scripts/cli.ts gh pr view <number> --repo " + ghText(result.repo),
|
||||
"",
|
||||
"Disclosure:",
|
||||
" default list omits PR bodies and closeout metadata; use --json fields, --full, or --raw for structured output.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderPrFiles(result: GitHubCommandResult): string {
|
||||
const files = arrayOfRecords(result.files);
|
||||
const rows = files.slice(0, 40).map((file) => [
|
||||
ghShort(ghText(file.filename), 84),
|
||||
ghText(file.status),
|
||||
ghText(file.additions),
|
||||
ghText(file.deletions),
|
||||
ghText(file.changes),
|
||||
]);
|
||||
const summary = record(result.summary);
|
||||
const truncation = record(result.truncation);
|
||||
const next = record(result.next);
|
||||
const lines = [
|
||||
`gh ${result.command} (observed)`,
|
||||
"",
|
||||
ghTable(["FILE", "STATUS", "ADD", "DEL", "CHANGES"], rows),
|
||||
"",
|
||||
"Summary:",
|
||||
` repo=${ghText(result.repo)} pr=#${ghText(record(result.pullRequest).number)} files=${ghText(summary.files)} returned=${ghText(result.filesReturned)} truncated=${ghText(truncation.truncated)}`,
|
||||
` additions=${ghText(summary.additions)} deletions=${ghText(summary.deletions)} changes=${ghText(summary.changes)}`,
|
||||
];
|
||||
if (typeof next.command === "string") lines.push("", "Next:", ` ${next.command}`);
|
||||
lines.push("", "Disclosure:", " default view is a bounded file/stat table and never emits raw diffs.");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function renderCommentResult(result: GitHubCommandResult): string {
|
||||
const comment = record(result.comment);
|
||||
const status = result.dryRun === true ? "dry-run" : result.deleted === true ? "deleted" : result.ok ? "ok" : "failed";
|
||||
const id = ghText(result.commentId ?? comment.id);
|
||||
const readCommands = record(result.readCommands);
|
||||
const lines = [
|
||||
`gh ${result.command} (${status})`,
|
||||
"",
|
||||
ghTable(["COMMENT", "STATUS", "BODY", "UPDATED", "URL"], [[
|
||||
id,
|
||||
status,
|
||||
`${ghText(result.bodyChars ?? comment.bodyChars)} chars`,
|
||||
shortDate(comment.updatedAt),
|
||||
ghShort(ghText(comment.url), 96),
|
||||
]]),
|
||||
"",
|
||||
"Summary:",
|
||||
` repo=${ghText(result.repo)} bodySha=${ghText(result.bodySha ?? comment.bodySha)} preview=${previewFrom(comment)}`,
|
||||
];
|
||||
if (typeof readCommands.full === "string" || typeof readCommands.comments === "string") lines.push("", "Next:", ` ${ghText(readCommands.comments ?? readCommands.full)}`);
|
||||
lines.push("", "Disclosure:", " default comment output omits full body; use comment view <id> --full or --raw for explicit disclosure.");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function renderAuthStatus(result: GitHubCommandResult): string {
|
||||
const token = record(result.token);
|
||||
const gh = record(result.gh);
|
||||
const probes = record(result.probes);
|
||||
const rows = [
|
||||
["token", token.present === true ? "ok" : "missing", `source=${ghText(token.source)} ghFallback=${ghText(token.ghFallbackAttempted)}`],
|
||||
["gh-binary", gh.binaryFound === true ? "ok" : "missing", `path=${ghText(gh.path)}`],
|
||||
["rest-api", probeStatus(probes.restApi), ghText(probes.restApi)],
|
||||
["repo", probeStatus(probes.repo), probeDetail(probes.repo)],
|
||||
["issue-read", probeStatus(probes.issueRead), probeDetail(probes.issueRead)],
|
||||
];
|
||||
return [
|
||||
`gh auth status (${result.ok ? "ok" : "failed"})`,
|
||||
"",
|
||||
ghTable(["CHECK", "STATUS", "DETAIL"], rows),
|
||||
"",
|
||||
"Summary:",
|
||||
` repo=${ghText(result.repo)} restFallback=${ghText(result.restFallback)} valuesPrinted=false`,
|
||||
"",
|
||||
"Disclosure:",
|
||||
" token values are never printed; use --raw only where supported for structured diagnostics.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderRepoResult(result: GitHubCommandResult): string {
|
||||
const repo = record(result.repository);
|
||||
const planned = record(result.planned);
|
||||
const status = result.dryRun === true ? "dry-run" : result.command === "repo create" ? "created" : "observed";
|
||||
return [
|
||||
`gh ${result.command} (${status})`,
|
||||
"",
|
||||
ghTable(["REPO", "STATUS", "PRIVATE", "URL", "DESCRIPTION"], [[
|
||||
ghText(repo.fullName ?? result.repo),
|
||||
status,
|
||||
ghText(repo.private ?? planned.visibility),
|
||||
ghShort(ghText(repo.url ?? repo.htmlUrl), 96),
|
||||
ghShort(ghText(repo.description ?? planned.description), 80),
|
||||
]]),
|
||||
"",
|
||||
"Summary:",
|
||||
` repo=${ghText(result.repo)} dryRun=${ghText(result.dryRun)}`,
|
||||
"",
|
||||
"Disclosure:",
|
||||
" default repo output is compact; use --raw where supported for request metadata.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderBoardRowResult(result: GitHubCommandResult): string {
|
||||
const rows = arrayOfRecords(result.rows);
|
||||
const row = isRecord(result.row) ? [result.row] : [];
|
||||
const shown = rows.length > 0 ? rows : row;
|
||||
const tableRows = shown.slice(0, 30).map((item) => [
|
||||
`#${ghText(item.issueNumber ?? item.number)}`,
|
||||
ghText(item.section ?? item.sectionKind ?? result.state),
|
||||
ghShort(ghText(item.status ?? item.githubStatus ?? item.summary), 28),
|
||||
ghShort(ghText(item.progress ?? item.focus ?? item.raw), 96),
|
||||
]);
|
||||
const boardIssue = record(result.boardIssue);
|
||||
const lines = [
|
||||
`gh ${result.command} (${result.dryRun === true ? "dry-run" : "ok"})`,
|
||||
"",
|
||||
ghTable(["ISSUE", "SECTION", "STATUS", "SUMMARY"], tableRows),
|
||||
"",
|
||||
"Summary:",
|
||||
` repo=${ghText(result.repo)} board=#${ghText(boardIssue.number)} issue=#${ghText(result.issueNumber)} count=${ghText(result.count)} operation=${ghText(result.operation)}`,
|
||||
];
|
||||
lines.push("", "Disclosure:", " default board-row output is a bounded row summary; use --full/--raw where supported for mutation plans.");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function renderBoardAudit(result: GitHubCommandResult): string {
|
||||
const sections = arrayOfRecords(result.sections);
|
||||
const rows = sections.map((section) => [
|
||||
ghText(section.kind),
|
||||
ghShort(ghText(section.heading), 80),
|
||||
ghText(section.rows),
|
||||
ghText(section.headerLine),
|
||||
]);
|
||||
const summary = record(result.summary);
|
||||
return [
|
||||
"gh issue board-audit (observed)",
|
||||
"",
|
||||
ghTable(["KIND", "HEADING", "ROWS", "LINE"], rows),
|
||||
"",
|
||||
"Summary:",
|
||||
` repo=${ghText(result.repo)} board=#${ghText(record(result.boardIssue).number)} sections=${ghText(summary.parsedSections)} rows=${ghText(summary.parsedRows)} warnings=${ghText(summary.parserWarnings)}`,
|
||||
"",
|
||||
"Disclosure:",
|
||||
" default board audit prints parsed section counts only; use --raw for full parser details.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderAttachmentResult(result: GitHubCommandResult): string {
|
||||
const attachments = arrayOfRecords(result.attachments);
|
||||
const rows = attachments.map((attachment) => [
|
||||
ghText(attachment.index),
|
||||
ghText(attachment.source),
|
||||
ghText(attachment.assetId),
|
||||
ghShort(ghText(attachment.url), 96),
|
||||
]);
|
||||
const output = record(result.output);
|
||||
const next = record(result.next);
|
||||
const table = rows.length > 0
|
||||
? ghTable(["INDEX", "SOURCE", "ASSET", "URL"], rows)
|
||||
: result.command === "issue attachment download"
|
||||
? ghTable(["OUTPUT", "BYTES", "SHA256", "TYPE"], [[ghText(output.path), ghText(output.bytes), ghShort(ghText(output.sha256), 16), ghText(output.contentType)]])
|
||||
: "No attachments found.";
|
||||
const lines = [
|
||||
`gh ${result.command} (${result.dryRun === true ? "dry-run" : "ok"})`,
|
||||
"",
|
||||
table,
|
||||
"",
|
||||
"Summary:",
|
||||
` repo=${ghText(result.repo)} issue=#${ghText(record(result.issue).number)} count=${ghText(result.count)} commentsScanned=${ghText(result.commentsScanned)}`,
|
||||
];
|
||||
if (typeof next.command === "string") lines.push("", "Next:", ` ${next.command}`);
|
||||
lines.push("", "Disclosure:", " default attachment output prints metadata/path only and never prints binary bytes.");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function renderStaleClose(result: GitHubCommandResult): string {
|
||||
const stale = record(result.stale);
|
||||
const closed = record(result.closed);
|
||||
return [
|
||||
`gh issue stale-close (${result.dryRun === true ? "dry-run" : "applied"})`,
|
||||
"",
|
||||
ghTable(["SCOPE", "COUNT", "RETURNED", "OMITTED"], [
|
||||
["stale", ghText(stale.count), ghText(stale.returned), ghText(stale.omitted)],
|
||||
["closed", ghText(closed.count), ghText(closed.returned), ghText(closed.omitted)],
|
||||
]),
|
||||
"",
|
||||
"Summary:",
|
||||
` repo=${ghText(result.repo)} scanned=${ghText(result.scannedCount)} stale=${ghText(result.staleCount)} closed=${ghText(result.closedCount)} failed=${ghText(result.failedCount)}`,
|
||||
"",
|
||||
"Disclosure:",
|
||||
" default stale-close output is a lifecycle summary; use --raw for candidate arrays and failure details.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderScanEscape(result: GitHubCommandResult): string {
|
||||
const summary = record(result.summary);
|
||||
return [
|
||||
`gh ${result.command} (observed)`,
|
||||
"",
|
||||
ghTable(["FINDINGS", "POLLUTION", "EXPLANATORY", "RISK", "BODY_RISK", "COMMENT_ERRORS"], [[
|
||||
ghText(summary.findings),
|
||||
ghText(summary.suspectedPollution),
|
||||
ghText(summary.explanatoryMention),
|
||||
ghText(summary.risk),
|
||||
ghText(summary.bodyRisks),
|
||||
ghText(summary.commentScanErrors),
|
||||
]]),
|
||||
"",
|
||||
"Summary:",
|
||||
` repo=${ghText(result.repo)} scannedIssues=${ghText(result.scannedIssues)} scannedComments=${ghText(result.scannedComments)} suggestions=${ghText(summary.suggestions)}`,
|
||||
"",
|
||||
"Disclosure:",
|
||||
" default scan output is a bounded summary; use --raw for full findings.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderIssueLifecycle(result: GitHubCommandResult): string {
|
||||
const issue = record(result.issue);
|
||||
const comment = record(result.comment);
|
||||
return [
|
||||
`gh ${result.command} (${result.dryRun === true ? "dry-run" : "updated"})`,
|
||||
"",
|
||||
ghTable(["ISSUE", "STATE", "COMMENT", "UPDATED", "TITLE"], [[
|
||||
`#${ghText(result.issueNumber ?? issue.number)}`,
|
||||
ghText(issue.state ?? record(result.wouldPatch).state),
|
||||
ghText(comment.id ?? "-"),
|
||||
shortDate(issue.updatedAt),
|
||||
ghShort(ghText(issue.title), 96),
|
||||
]]),
|
||||
"",
|
||||
"Summary:",
|
||||
` repo=${ghText(result.repo)} bodySha=${ghText(issue.bodySha)} fullBodyIncluded=${ghText(record(result.disclosure).fullBodyIncluded)}`,
|
||||
"",
|
||||
"Disclosure:",
|
||||
" default lifecycle output omits full issue.body; use readCommands.full/raw for explicit disclosure.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderIssueWrite(result: GitHubCommandResult): string {
|
||||
const issue = record(result.issue);
|
||||
const bodyChange = record(result.bodyChange);
|
||||
return [
|
||||
`gh ${result.command} (${result.dryRun === true ? "dry-run" : "updated"})`,
|
||||
"",
|
||||
ghTable(["ISSUE", "MODE", "BODY", "DELTA", "UPDATED", "TITLE"], [[
|
||||
`#${ghText(result.issueNumber ?? issue.number)}`,
|
||||
ghText(result.mode ?? record(result.wouldPatch).mode),
|
||||
`${ghText(result.bodyChars ?? issue.bodyChars ?? bodyChange.newBodyChars)} chars`,
|
||||
ghText(bodyChange.deltaChars),
|
||||
shortDate(issue.updatedAt ?? bodyChange.newUpdatedAt),
|
||||
ghShort(ghText(issue.title ?? result.title), 96),
|
||||
]]),
|
||||
"",
|
||||
"Summary:",
|
||||
` repo=${ghText(result.repo)} bodySha=${ghText(result.bodySha ?? issue.bodySha ?? bodyChange.newBodySha)} fullBodyIncluded=${ghText(record(result.disclosure).fullBodyIncluded)}`,
|
||||
"",
|
||||
"Disclosure:",
|
||||
" default write output omits full issue.body and prints body SHA/chars plus drill-down commands.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderPrWrite(result: GitHubCommandResult): string {
|
||||
const pr = record(result.pullRequest);
|
||||
return [
|
||||
`gh ${result.command} (${result.dryRun === true ? "dry-run" : "updated"})`,
|
||||
"",
|
||||
ghTable(["PR", "STATE", "CHANGED", "URL", "TITLE"], [[
|
||||
`#${ghText(result.number ?? pr.number)}`,
|
||||
ghText(pr.stateDetail ?? pr.state ?? record(result.wouldPatch).state),
|
||||
arrayText(result.changedFields),
|
||||
ghShort(ghText(result.url ?? pr.url), 96),
|
||||
ghShort(ghText(pr.title), 96),
|
||||
]]),
|
||||
"",
|
||||
"Summary:",
|
||||
` repo=${ghText(result.repo)} rest=${ghText(result.rest)} graphQl=${ghText(result.graphQl)}`,
|
||||
"",
|
||||
"Disclosure:",
|
||||
" default PR write output is compact; use pr view --json body or --full for explicit body disclosure.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function renderGenericResult(result: GitHubCommandResult): string {
|
||||
const rows = Object.entries(result)
|
||||
.filter(([key]) => !["ok", "command", "repo", "renderedText", "contentType"].includes(key))
|
||||
.slice(0, 16)
|
||||
.map(([key, value]) => [key, valueSummary(value)]);
|
||||
return [
|
||||
`gh ${result.command} (${result.ok ? "ok" : "failed"})`,
|
||||
"",
|
||||
ghTable(["FIELD", "VALUE"], rows),
|
||||
"",
|
||||
"Summary:",
|
||||
` repo=${ghText(result.repo)}`,
|
||||
"",
|
||||
"Disclosure:",
|
||||
" default output is compact; use explicit --json/--full/--raw where supported for structured details.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function record(value: unknown): Record<string, unknown> {
|
||||
return isRecord(value) ? value : {};
|
||||
}
|
||||
|
||||
function arrayOfRecords(value: unknown): Record<string, unknown>[] {
|
||||
return Array.isArray(value) ? value.filter(isRecord) : [];
|
||||
}
|
||||
|
||||
function bodyInfo(source: Record<string, unknown>): { label: string; preview: string } {
|
||||
const chars = ghText(source.bodyChars);
|
||||
const preview = previewFrom(source);
|
||||
return { label: chars === "-" ? "-" : `${chars} chars`, preview };
|
||||
}
|
||||
|
||||
function previewFrom(source: Record<string, unknown>): string {
|
||||
if (typeof source.bodyPreview === "string") return ghShort(source.bodyPreview.replace(/\s+/gu, " "), 160);
|
||||
if (typeof source.body === "string") return ghShort(source.body.replace(/\s+/gu, " "), 160);
|
||||
if (typeof source.preview === "string") return ghShort(source.preview.replace(/\s+/gu, " "), 160);
|
||||
return "-";
|
||||
}
|
||||
|
||||
function shortDate(value: unknown): string {
|
||||
if (typeof value !== "string" || value.length === 0) return "-";
|
||||
return ghShort(value, 19);
|
||||
}
|
||||
|
||||
function probeStatus(value: unknown): string {
|
||||
if (value === "ok") return "ok";
|
||||
if (value === "skipped") return "skipped";
|
||||
if (isRecord(value) && value.ok === true) return "ok";
|
||||
if (isRecord(value) && value.ok === false) return "failed";
|
||||
return "-";
|
||||
}
|
||||
|
||||
function probeDetail(value: unknown): string {
|
||||
if (isRecord(value)) return valueSummary(value);
|
||||
return ghText(value);
|
||||
}
|
||||
|
||||
function arrayText(value: unknown): string {
|
||||
return Array.isArray(value) ? value.map(ghText).join(",") || "-" : ghText(value);
|
||||
}
|
||||
|
||||
function valueSummary(value: unknown): string {
|
||||
if (value === null || value === undefined || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return ghShort(ghText(value), 120);
|
||||
if (Array.isArray(value)) return `array(${value.length})`;
|
||||
if (isRecord(value)) {
|
||||
const keys = Object.keys(value);
|
||||
const title = ghText(value.title ?? value.name ?? value.number ?? value.id ?? value.path ?? value.command);
|
||||
return title === "-" ? `object(${keys.slice(0, 5).join(",")})` : ghShort(title, 120);
|
||||
}
|
||||
return ghShort(String(value), 120);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { DEFAULT_REPO } from "./types";
|
||||
export function ghHelp(): unknown {
|
||||
return {
|
||||
command: "gh",
|
||||
output: "json",
|
||||
output: "text",
|
||||
usage: [
|
||||
"bun scripts/cli.ts gh auth status [--repo owner/name]",
|
||||
"bun scripts/cli.ts gh repo view <owner/repo>|--repo owner/name",
|
||||
@@ -66,6 +66,7 @@ export function ghHelp(): unknown {
|
||||
"Issue and PR create/read/update/comment/close/reopen use GitHub REST and do not require the gh binary when GH_TOKEN or GITHUB_TOKEN is present.",
|
||||
"repo view/create use GitHub REST through the same token path. repo create defaults to private repositories, preflights existing repos, supports --dry-run, and refuses duplicate creation.",
|
||||
"Token values are never printed; auth status reports only token source and presence.",
|
||||
"Default gh output is k8s-style text/table/summary with Next drill-down commands. Machine-readable or full-disclosure output must be requested explicitly with --json, --full, or --raw where supported.",
|
||||
"issue list and pr list accept a single positional owner/repo as a compatibility alias for --repo owner/name. The positional repo and --repo must match if both are supplied; non-repo positionals fail structurally instead of falling back to the default repo.",
|
||||
"issue list defaults to --state open and bounded --limit 30; it paginates GitHub REST/Search pages internally when --limit exceeds GitHub's per-page cap and discloses pagination/rawCount/hasMore so operators do not mistake a single page for the full repository. --search uses GitHub Search Issues API with repo/type/state qualifiers for low-friction dedupe lookup before creating a new issue. --title-prefix filters the bounded listed issues locally by exact title startsWith, useful for [FEEDBACK] dedupe, and reports titleFilter input/output counts. Supported --json fields are number,title,state,closed,closedAt,url,updatedAt,createdAt,author,labels and unknown fields fail structurally.",
|
||||
"PR list defaults to --state all for compatibility with earlier UniDesk CLI behavior; supported states are open, closed, and all.",
|
||||
@@ -178,7 +179,7 @@ export function ghScopedHelp(args: string[]): unknown | null {
|
||||
if (usage.length === 0) continue;
|
||||
return {
|
||||
command: `gh ${scopedTokens.join(" ")}`,
|
||||
output: "json",
|
||||
output: "text",
|
||||
scoped: true,
|
||||
requestedCommand: requestedTokens.join(" "),
|
||||
matchedCommand: scopedTokens.join(" "),
|
||||
@@ -189,7 +190,7 @@ export function ghScopedHelp(args: string[]): unknown | null {
|
||||
}
|
||||
return {
|
||||
command: `gh ${requestedTokens.join(" ")}`,
|
||||
output: "json",
|
||||
output: "text",
|
||||
scoped: true,
|
||||
requestedCommand: requestedTokens.join(" "),
|
||||
usage: [],
|
||||
|
||||
Reference in New Issue
Block a user