scripts/summarize-review-state.mjs
scripts/summarize-review-state.mjsBrowse 11 files
1,575 tokens
6,158 bytes
Token encoding: o200k_base
Snapshot fac8604
← Back to SKILL.md
1#!/usr/bin/env node2 3import { realpathSync } from 'node:fs';4import { mkdir, readFile, writeFile } from 'node:fs/promises';5import { dirname, resolve } from 'node:path';6import { fileURLToPath } from 'node:url';7 8import { assertReviewStateV2, formatCanonicalJson } from './review-artifacts.mjs';9 10const EXIT_SUCCESS = 0;11const EXIT_OPERATIONAL = 1;12const EXIT_CLI = 2;13 14function getHelpText() {15 return [16 'Usage:',17 ' summarize-review-state.mjs --in <review-state.json> [--format text|json] [--out <path>|-] [--help]',18 '',19 'Purpose:',20 ' Render deterministic summaries from review-state.json with no network access.',21 '',22 'Flags:',23 ' --in <path.json> Input path to review-state.json.',24 ' --format text|json Summary output format. Defaults to text.',25 ' --out <path>|- Output path. Use "-" to write to stdout. Defaults to stdout.',26 ' --help Show this help text and exit.',27 ].join('\n');28}29 30function parseCliArgs(argv) {31 const args = argv.slice(2);32 const result = { inPath: null, format: 'text', outPath: null, help: false };33 if (args.includes('--help')) {34 result.help = true;35 return result;36 }37 38 const knownFlags = new Set(['--in', '--format', '--out']);39 let index = 0;40 while (index < args.length) {41 const arg = args[index];42 if (!arg.startsWith('--') || !knownFlags.has(arg)) {43 throw { code: EXIT_CLI, message: `error: unknown flag "${arg}"` };44 }45 46 index += 1;47 if (index >= args.length) {48 throw { code: EXIT_CLI, message: `error: ${arg} requires a value` };49 }50 51 const value = args[index];52 if (arg === '--in') {53 result.inPath = value;54 } else if (arg === '--format') {55 result.format = value;56 } else if (arg === '--out') {57 result.outPath = value;58 }59 index += 1;60 }61 62 if (!result.inPath) {63 throw { code: EXIT_CLI, message: 'error: --in is required' };64 }65 if (result.inPath === '-') {66 throw { code: EXIT_CLI, message: 'error: --in - is not supported' };67 }68 if (result.inPath !== '-' && !result.inPath.endsWith('.json')) {69 throw { code: EXIT_CLI, message: 'error: --in file path must end with .json' };70 }71 if (result.format !== 'text' && result.format !== 'json') {72 throw { code: EXIT_CLI, message: 'error: --format must be text or json' };73 }74 if (result.outPath !== null && result.outPath !== '-') {75 if (result.format === 'json' && !result.outPath.endsWith('.json')) {76 throw {77 code: EXIT_CLI,78 message: 'error: --out file path must end with .json for --format json',79 };80 }81 if (result.format === 'text' && !result.outPath.endsWith('.txt')) {82 throw {83 code: EXIT_CLI,84 message: 'error: --out file path must end with .txt for --format text',85 };86 }87 }88 89 return result;90}91 92export function buildReviewStateSummary(payload) {93 assertReviewStateV2(payload);94 95 return {96 version: 1,97 pr: {98 url: payload.pr.url,99 nodeId: payload.pr.nodeId,100 number: payload.pr.number,101 title: payload.pr.title,102 state: payload.pr.state,103 },104 fetchedAt: payload.fetchedAt,105 sourceBranch: payload.sourceBranch,106 counts: {107 unresolvedThreads: payload.reviewThreads.length,108 reviewsWithBody: payload.reviews.length,109 issueComments: payload.issueComments.length,110 },111 unresolvedThreadNodeIds: payload.reviewThreads.map((thread) => thread.nodeId),112 reviewNodeIds: payload.reviews.map((review) => review.nodeId),113 issueCommentNodeIds: payload.issueComments.map((comment) => comment.nodeId),114 };115}116 117export function renderReviewStateSummaryText(summary) {118 const lines = [];119 lines.push('Review State Summary');120 lines.push(`PR: ${summary.pr.url ?? ''}`);121 lines.push(`FetchedAt: ${summary.fetchedAt}`);122 lines.push(`SourceBranch: ${summary.sourceBranch ?? ''}`);123 lines.push('');124 lines.push(`Unresolved threads: ${summary.counts.unresolvedThreads}`);125 lines.push(`Reviews with body: ${summary.counts.reviewsWithBody}`);126 lines.push(`Issue comments: ${summary.counts.issueComments}`);127 lines.push('');128 lines.push('Unresolved thread nodeIds:');129 for (const nodeId of summary.unresolvedThreadNodeIds) {130 lines.push(`- ${nodeId}`);131 }132 lines.push('');133 lines.push('Review nodeIds:');134 for (const nodeId of summary.reviewNodeIds) {135 lines.push(`- ${nodeId}`);136 }137 lines.push('');138 lines.push('Issue comment nodeIds:');139 for (const nodeId of summary.issueCommentNodeIds) {140 lines.push(`- ${nodeId}`);141 }142 return `${lines.join('\n')}\n`;143}144 145export function renderReviewStateSummaryJson(summary) {146 return formatCanonicalJson(summary);147}148 149async function readJson(path) {150 const raw = await readFile(path, 'utf8');151 return JSON.parse(raw);152}153 154async function writeOutput(outPath, text) {155 if (!outPath || outPath === '-') {156 process.stdout.write(text);157 return;158 }159 await mkdir(dirname(outPath), { recursive: true });160 await writeFile(outPath, text, 'utf8');161}162 163async function main() {164 const args = parseCliArgs(process.argv);165 if (args.help) {166 process.stdout.write(`${getHelpText()}\n`);167 process.exit(EXIT_SUCCESS);168 }169 170 const payload = await readJson(args.inPath);171 const summary = buildReviewStateSummary(payload);172 const output =173 args.format === 'json'174 ? renderReviewStateSummaryJson(summary)175 : renderReviewStateSummaryText(summary);176 177 await writeOutput(args.outPath, output);178}179 180function safeRealpath(path) {181 try {182 return realpathSync(path);183 } catch {184 return null;185 }186}187 188const invokedScriptPath = process.argv[1] ? safeRealpath(resolve(process.argv[1])) : null;189const currentModulePath = safeRealpath(fileURLToPath(import.meta.url));190const isMain =191 invokedScriptPath !== null &&192 currentModulePath !== null &&193 invokedScriptPath === currentModulePath;194 195if (isMain) {196 main().catch((error) => {197 const code = typeof error?.code === 'number' ? error.code : EXIT_OPERATIONAL;198 const message = error?.message ? String(error.message) : String(error);199 process.stderr.write(`${message}\n`);200 process.exit(code);201 });202}203 204export { parseCliArgs };205