diff --git a/scripts/cli.ts b/scripts/cli.ts index dfaa46cf..273c403a 100644 --- a/scripts/cli.ts +++ b/scripts/cli.ts @@ -290,6 +290,11 @@ async function main(): Promise { 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; diff --git a/scripts/src/gh.ts b/scripts/src/gh.ts index ff6d3c53..02005876 100644 --- a/scripts/src/gh.ts +++ b/scripts/src/gh.ts @@ -5230,7 +5230,7 @@ async function prPreflight(repo: string, number: number, commandName: "preflight const metadataSummary = prMetadataSummary(metadata); const statusChecks = statusRollupSummary(repo, number, metadata.statusCheckRollup, includeRaw); const closeout = prCloseoutSummary(summary, metadataSummary, statusChecks); - return { + const result: GitHubCommandResult = { ok: true, command: commandName, canonicalCommand: `bun scripts/cli.ts gh pr preflight ${number} --repo ${repo}`, @@ -5251,6 +5251,73 @@ async function prPreflight(repo: string, number: number, commandName: "preflight policy, ...(includeRaw ? { raw: { authStatus: auth, pullRequest, closeoutMetadata: metadataSummary } } : {}), }; + if (includeRaw) return result; + return { + ...result, + contentType: "text/plain", + renderedText: renderPrPreflightTable(result), + }; +} + +function renderPrPreflightTable(result: GitHubCommandResult): string { + const pullRequest = isRecord(result.pullRequest) ? result.pullRequest : {}; + const mergeability = isRecord(result.mergeability) ? result.mergeability : {}; + const statusChecks = isRecord(result.statusChecks) ? result.statusChecks : {}; + const counts = isRecord(statusChecks.counts) ? statusChecks.counts : {}; + const policy = isRecord(result.policy) ? result.policy : {}; + const blockers = Array.isArray(mergeability.blockers) ? mergeability.blockers.map(String) : []; + const pending = Array.isArray(mergeability.pending) ? mergeability.pending.map(String) : []; + const conclusion = ghText(mergeability.conclusion); + const rows = [[ + `#${ghText(result.number)}`, + ghText(pullRequest.stateDetail ?? pullRequest.state), + ghText(mergeability.mergeable), + ghText(mergeability.mergeStateStatus), + `${ghText(statusChecks.totalContexts)} total ${ghText(counts.success)} ok ${ghText(counts.failure)} fail ${ghText(counts.pending)} pending`, + conclusion, + ]]; + const lines = [ + `gh pr preflight (${conclusion})`, + "", + ghTable(["PR", "STATE", "MERGEABLE", "MERGE_STATE", "CHECKS", "CONCLUSION"], rows), + "", + "Summary:", + ` repo=${ghText(result.repo)} title=${ghShort(ghText(pullRequest.title), 96)}`, + ` head=${ghText(isRecord(pullRequest.head) ? pullRequest.head.ref : null)} base=${ghText(isRecord(pullRequest.base) ? pullRequest.base.ref : null)}`, + ` blockers=${blockers.length === 0 ? "-" : blockers.join(",")} pending=${pending.length === 0 ? "-" : pending.join(",")}`, + "", + "Next:", + ]; + const mergeCommand = ghText(policy.mergeCommand); + if (mergeability.readyForCommanderMerge === true && mergeCommand !== "-") { + lines.push(` ${mergeCommand}`); + } + lines.push(` bun scripts/cli.ts gh pr preflight ${ghText(result.number)} --repo ${ghText(result.repo)} --full`); + lines.push("", "Disclosure:"); + lines.push(" default view is a bounded table; use --full or --raw for structured PR/check metadata."); + return lines.join("\n"); +} + +function ghTable(headers: string[], rows: string[][]): string { + const normalizedRows = rows.map((row) => headers.map((_, index) => row[index] ?? "")); + const widths = headers.map((header, index) => Math.max(header.length, ...normalizedRows.map((row) => row[index].length))); + return [ + headers.map((header, index) => header.padEnd(widths[index])).join(" ").trimEnd(), + ...normalizedRows.map((row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ").trimEnd()), + ].join("\n"); +} + +function ghText(value: unknown): string { + if (value === null || value === undefined || value === "") return "-"; + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean") return String(value); + return JSON.stringify(value); +} + +function ghShort(value: string, maxLength: number): string { + if (value.length <= maxLength) return value; + if (maxLength <= 1) return value.slice(0, maxLength); + return `${value.slice(0, maxLength - 1)}~`; } function repoSummary(repo: GitHubRepository): Record {