fix: render gh defaults as concise tables

This commit is contained in:
Codex
2026-07-01 09:15:02 +00:00
parent b44bbc3a93
commit feeafc1420
3 changed files with 577 additions and 16 deletions
+14 -13
View File
@@ -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;
+559
View File
@@ -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);
}
+4 -3
View File
@@ -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: [],