#!/usr/bin/env node 'use strict'; const fs = require('fs'); const path = require('path'); const TOPIC_BEGIN = //; const ANY_END = //g; const CANONICAL_BEGIN = ''; const CANONICAL_END = ''; function loadRegistry(nodeDir) { const registryPath = path.join(nodeDir, 'src/commands/index.js'); if (!fs.existsSync(registryPath)) return null; delete require.cache[require.resolve(registryPath)]; const descriptors = require(registryPath); if (!Array.isArray(descriptors)) { throw new Error('commands/index.js must export an array of descriptors'); } return descriptors; } function renderTopicTable(descriptors) { const lines = []; lines.push('| Canonical topic | Aliases | Payload | Unit | Effect |'); lines.push('|---|---|---|---|---|'); for (const d of descriptors) { if (!d || typeof d.topic !== 'string') continue; const canonical = '`' + d.topic + '`'; const aliases = (Array.isArray(d.aliases) && d.aliases.length) ? d.aliases.map((a) => '`' + a + '`').join(', ') : '—'; const payload = renderPayload(d.payloadSchema); const unit = renderUnit(d.units); const effect = (d.description || '').replace(/\|/g, '\\|').trim() || '—'; lines.push(`| ${canonical} | ${aliases} | ${payload} | ${unit} | ${effect} |`); } return lines.join('\n'); } function renderPayload(schema) { if (!schema) return '—'; if (typeof schema === 'string') return '`' + schema + '`'; if (typeof schema !== 'object') return '—'; if (schema.type === 'any' || schema.type === undefined) return 'any'; if (typeof schema.type === 'string') return '`' + schema.type + '`'; return '—'; } function renderUnit(units) { if (!units || typeof units !== 'object') return '—'; const m = units.measure; const d = units.default; if (!m && !d) return '—'; if (m && d) return '`' + m + '` (default `' + d + '`)'; return '`' + (m || d) + '`'; } function regenerateFile(filePath, replacement) { if (!fs.existsSync(filePath)) return { changed: false, reason: 'file not found' }; const src = fs.readFileSync(filePath, 'utf8'); const beginMatch = src.match(TOPIC_BEGIN); if (!beginMatch) return { changed: false, reason: 'no AUTOGEN markers' }; const afterBegin = beginMatch.index + beginMatch[0].length; ANY_END.lastIndex = afterBegin; const endMatch = ANY_END.exec(src); if (!endMatch) return { changed: false, reason: 'BEGIN marker has no matching END' }; const endStart = endMatch.index; const endStop = endStart + endMatch[0].length; const block = `${CANONICAL_BEGIN}\n\n${replacement}\n\n${CANONICAL_END}`; const next = src.slice(0, beginMatch.index) + block + src.slice(endStop); if (next === src) return { changed: false, reason: 'already up to date' }; fs.writeFileSync(filePath, next, 'utf8'); return { changed: true }; } function regenerateNode(nodeDir, opts) { const nodeName = path.basename(nodeDir); const descriptors = loadRegistry(nodeDir); if (!descriptors) return { node: nodeName, skipped: true, reason: 'no commands/index.js' }; const table = renderTopicTable(descriptors); const targets = [ path.join(nodeDir, 'wiki/Reference-Contracts.md'), path.join(nodeDir, 'wiki/Home.md'), ]; const results = []; for (const target of targets) { const out = regenerateFile(target, table); results.push({ file: path.relative(nodeDir, target), ...out }); } return { node: nodeName, results }; } function findNodes(repoRoot) { const nodesDir = path.join(repoRoot, 'nodes'); return fs.readdirSync(nodesDir) .filter((n) => fs.existsSync(path.join(nodesDir, n, 'src/commands/index.js'))) .map((n) => path.join(nodesDir, n)); } function report(result, json) { if (json) { process.stdout.write(JSON.stringify(result) + '\n'); return; } if (result.skipped) { process.stdout.write(`SKIP ${result.node} (${result.reason})\n`); return; } for (const r of result.results) { const tag = r.changed ? 'UPDATE' : (r.reason === 'no AUTOGEN markers' ? 'NO-MARK' : 'OK'); process.stdout.write(`${tag.padEnd(7)} ${result.node}/${r.file}${r.reason ? ' — ' + r.reason : ''}\n`); } } function main() { const args = process.argv.slice(2); const json = args.includes('--json'); const check = args.includes('--check'); const positional = args.filter((a) => !a.startsWith('--')); const repoRoot = path.resolve(__dirname, '../../..'); const targets = positional.length === 0 ? findNodes(repoRoot) : positional.map((p) => path.resolve(p)); let drift = false; for (const nodeDir of targets) { let res; try { res = regenerateNode(nodeDir, { check }); } catch (err) { process.stderr.write(`[${path.basename(nodeDir)}] ERROR: ${err.message}\n`); drift = true; continue; } if (!res.skipped && check) { for (const r of res.results) { if (r.changed) drift = true; } } report(res, json); } if (check && drift) { process.stderr.write('\nAUTOGEN blocks are out of date. Run without --check to regenerate.\n'); process.exit(1); } process.exit(0); } if (require.main === module) main(); module.exports = { renderTopicTable, regenerateFile, loadRegistry };