scripts/render-review-actions.mjs
scripts/render-review-actions.mjsBrowse 8 files
1,953 tokens
7,439 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 { assertReviewActionsV1 } 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 ' render-review-actions.mjs --in <review-actions.json> [--out <review-actions.md>|-] [--view will-address|all] [--help]',18 '',19 'Purpose:',20 ' Render deterministic Markdown (review-actions.md) from review-actions.json.',21 '',22 'Flags:',23 ' --in <path.json> Input path to review-actions.json.',24 ' --out <path.md>|- Markdown output path. Use "-" to write to stdout. Defaults to stdout.',25 ' --view will-address|all',26 ' Render all actions (default) or only will-address actions.',27 ' --help Show this help text and exit.',28 ].join('\n');29}30 31function parseCliArgs(argv) {32 const args = argv.slice(2);33 const result = { inPath: null, outPath: null, view: 'all', help: false };34 if (args.includes('--help')) {35 result.help = true;36 return result;37 }38 const knownFlags = new Set(['--in', '--out', '--view']);39 let i = 0;40 while (i < args.length) {41 const arg = args[i];42 if (!arg.startsWith('--')) {43 throw { code: EXIT_CLI, message: `error: unknown flag "${arg}"` };44 }45 const flag = arg;46 if (!knownFlags.has(flag)) {47 throw { code: EXIT_CLI, message: `error: unknown flag "${flag}"` };48 }49 i++;50 if (i >= args.length) {51 throw {52 code: EXIT_CLI,53 message: `error: ${flag} requires a value`,54 };55 }56 const value = args[i];57 i++;58 if (flag === '--in') {59 result.inPath = value;60 } else if (flag === '--out') {61 result.outPath = value;62 } else if (flag === '--view') {63 result.view = value;64 }65 }66 if (!result.inPath) {67 throw { code: EXIT_CLI, message: 'error: --in is required' };68 }69 if (result.inPath === '-') {70 throw { code: EXIT_CLI, message: 'error: --in - is not supported' };71 }72 if (!result.inPath.endsWith('.json')) {73 throw { code: EXIT_CLI, message: 'error: --in file path must end with .json' };74 }75 if (result.outPath !== null && result.outPath !== '-' && !result.outPath.endsWith('.md')) {76 throw { code: EXIT_CLI, message: 'error: --out file path must end with .md' };77 }78 if (result.view !== 'will-address' && result.view !== 'all') {79 throw { code: EXIT_CLI, message: 'error: --view must be will-address or all' };80 }81 return result;82}83 84function escapeTableCell(value) {85 return String(value ?? '')86 .replace(/\r?\n/g, ' ')87 .replace(/\\/g, '\\\\')88 .replace(/\|/g, '\\|')89 .replace(/\s+/g, ' ')90 .trim();91}92 93function formatCodeSpan(value) {94 const text = String(value ?? '')95 .replace(/\r?\n/g, ' ')96 .replace(/\|/g, '\\|')97 .replace(/\s+/g, ' ')98 .trim();99 const longestBacktickRun = (text.match(/`+/g) ?? []).reduce(100 (max, run) => Math.max(max, run.length),101 0,102 );103 const fence = '`'.repeat(longestBacktickRun + 1);104 const pad = text.startsWith('`') || text.endsWith('`') ? ' ' : '';105 return `${fence}${pad}${text}${pad}${fence}`;106}107 108function formatCodePaths(paths) {109 if (!Array.isArray(paths) || paths.length === 0) return '';110 return paths.map((p) => formatCodeSpan(p)).join(', ');111}112 113function computeStatus(actions) {114 const statuses = new Set((actions ?? []).map((a) => a?.status).filter(Boolean));115 if (statuses.size === 0) return 'Triaged';116 if (statuses.has('in_progress')) return 'In progress';117 if (statuses.size === 1 && statuses.has('done')) return 'Complete';118 if (statuses.size === 1 && statuses.has('pending')) return 'Triaged';119 return 'In progress';120}121 122function formatTarget(target) {123 const kind = escapeTableCell(target?.kind);124 const nodeId = escapeTableCell(target?.nodeId);125 return `${kind} / ${nodeId}`;126}127 128export function renderReviewActionsMarkdown(payload, { sourcePath }) {129 assertReviewActionsV1(payload);130 131 const prUrl = payload?.pr?.url ?? '';132 const source = formatCodeSpan(sourcePath || 'review-actions.json');133 const actions = Array.isArray(payload?.actions) ? payload.actions : [];134 const view = payload?.meta?.renderView === 'will-address' ? 'will-address' : 'all';135 const includedActions =136 view === 'all' ? actions : actions.filter((action) => action?.decision === 'will_address');137 138 const lines = [];139 lines.push('# Review Actions');140 lines.push('');141 lines.push(`PR: ${escapeTableCell(prUrl)}`);142 lines.push(`Source: ${source}`);143 lines.push('');144 lines.push(`Status: ${computeStatus(includedActions)}`);145 lines.push('');146 lines.push(147 view === 'all'148 ? 'All actions are listed below.'149 : 'Only items triaged as **WILL ADDRESS** are listed below.',150 );151 lines.push('');152 lines.push(153 '| Action ID | Decision | Target | Link | Action | Target files | Acceptance check | Status |',154 );155 lines.push('| --- | --- | --- | --- | --- | --- | --- | --- |');156 157 for (const action of includedActions) {158 const actionId = action?.actionId ?? '';159 const decision = action?.decision ?? '';160 const target = formatTarget(action?.target);161 const link = action?.target?.url ?? '';162 const summary = action?.summary ?? '';163 const targetFiles = formatCodePaths(action?.targetFiles);164 const acceptance = action?.acceptance ?? '';165 const status = action?.status ?? '';166 167 lines.push(168 [169 escapeTableCell(actionId),170 escapeTableCell(decision),171 target,172 escapeTableCell(link),173 escapeTableCell(summary || '(pending triage)'),174 targetFiles,175 escapeTableCell(acceptance || ''),176 escapeTableCell(status),177 ]178 .join(' | ')179 .replace(/^/, '| ')180 .replace(/$/, ' |'),181 );182 }183 184 return `${lines.join('\n')}\n`;185}186 187async function readJson(path) {188 const raw = await readFile(path, 'utf8');189 return JSON.parse(raw);190}191 192async function writeOutput(outPath, text) {193 if (!outPath || outPath === '-') {194 process.stdout.write(text);195 if (!text.endsWith('\n')) process.stdout.write('\n');196 return;197 }198 await mkdir(dirname(outPath), { recursive: true });199 await writeFile(outPath, `${text.endsWith('\n') ? text : `${text}\n`}`, 'utf8');200}201 202async function main() {203 const args = parseCliArgs(process.argv);204 if (args.help) {205 process.stdout.write(`${getHelpText()}\n`);206 process.exit(EXIT_SUCCESS);207 }208 209 const payload = await readJson(args.inPath);210 const markdown = renderReviewActionsMarkdown(211 { ...payload, meta: { ...(payload.meta ?? {}), renderView: args.view } },212 { sourcePath: args.inPath },213 );214 await writeOutput(args.outPath, markdown);215}216 217const isMain = (() => {218 try {219 const invokedScriptPath = process.argv[1] ? realpathSync(resolve(process.argv[1])) : null;220 const currentModulePath = realpathSync(fileURLToPath(import.meta.url));221 return invokedScriptPath !== null && invokedScriptPath === currentModulePath;222 } catch {223 return false;224 }225})();226 227if (isMain) {228 main().catch((error) => {229 const code = typeof error?.code === 'number' ? error.code : EXIT_OPERATIONAL;230 const message = error?.message ? String(error.message) : String(error);231 process.stderr.write(`${message}\n`);232 process.exit(code);233 });234}235