diff --git a/scripts/cli.ts b/scripts/cli.ts index 242d3a56..7611e9d0 100644 --- a/scripts/cli.ts +++ b/scripts/cli.ts @@ -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 { 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 { 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; diff --git a/scripts/src/gh/default-render.ts b/scripts/src/gh/default-render.ts new file mode 100644 index 00000000..72ea7b06 --- /dev/null +++ b/scripts/src/gh/default-render.ts @@ -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 { + 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 { + 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 --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 --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 --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 { + return isRecord(value) ? value : {}; +} + +function arrayOfRecords(value: unknown): Record[] { + return Array.isArray(value) ? value.filter(isRecord) : []; +} + +function bodyInfo(source: Record): { label: string; preview: string } { + const chars = ghText(source.bodyChars); + const preview = previewFrom(source); + return { label: chars === "-" ? "-" : `${chars} chars`, preview }; +} + +function previewFrom(source: Record): 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); +} diff --git a/scripts/src/gh/help.ts b/scripts/src/gh/help.ts index 1c9663d7..33c02b7c 100644 --- a/scripts/src/gh/help.ts +++ b/scripts/src/gh/help.ts @@ -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 |--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: [],