fable-agent/scripts/check-command-parity.js

102 lines
2.9 KiB
JavaScript

#!/usr/bin/env node
import { readFileSync } from "node:fs";
const lines = readFileSync("src/index.ts", "utf8").split(/\r?\n/);
const docs = readFileSync("COMMANDS.md", "utf8");
function hasDocRef(commandText) {
return (
docs.includes(`\`${commandText}\``) ||
docs.includes(` ${commandText} \``) ||
docs.includes(`\`${commandText} `)
);
}
const varToCommand = new Map();
const commandSet = new Set();
let pendingParentVar = null;
let currentChainBase = null;
for (const line of lines) {
const varAssign = line.match(/^\s*const\s+(\w+)\s*=\s*program\s*$/);
if (varAssign) {
pendingParentVar = varAssign[1];
continue;
}
const lineRef = line.match(/^\s*(\w+)\s*$/);
if (lineRef) {
const mapped = varToCommand.get(lineRef[1]);
if (mapped) currentChainBase = mapped;
continue;
}
const dotCommand = line.match(/^\s*\.command\(\s*(?:"([^"]+)"|'([^']+)')/);
if (dotCommand) {
const raw = (dotCommand[1] ?? dotCommand[2]).trim();
if (pendingParentVar) {
varToCommand.set(pendingParentVar, raw);
commandSet.add(raw);
currentChainBase = raw;
pendingParentVar = null;
} else if (currentChainBase) {
commandSet.add(`${currentChainBase} ${raw}`);
} else {
commandSet.add(raw);
}
continue;
}
const childCommand = line.match(/^\s*(\w+)\.command\(\s*(?:"([^"]+)"|'([^']+)')/);
if (childCommand) {
const varName = childCommand[1];
const raw = (childCommand[2] ?? childCommand[3]).trim();
const base = varToCommand.get(varName);
if (base) commandSet.add(`${base} ${raw}`);
else if (currentChainBase) commandSet.add(`${currentChainBase} ${raw}`);
else commandSet.add(raw);
currentChainBase = base ?? currentChainBase ?? raw;
pendingParentVar = null;
continue;
}
// keep context across chained calls
if (/^\s*\./.test(line)) {
continue;
}
pendingParentVar = null;
currentChainBase = null;
}
const fullText = lines.join("\n");
const optionMatches = [...fullText.matchAll(/\.option\(\s*(?:"([^"]+)"|'([^']+)')/g)];
const options = [...new Set(
optionMatches
.map((m) => m[1] ?? m[2])
.filter((opt) => opt.includes("--") && !opt.includes("<"))
.map((opt) => opt.match(/--[^\s,>]+/)?.[0])
.filter(Boolean)
)];
const commandList = [...commandSet];
const missingCommands = commandList.filter((cmd) => !hasDocRef(cmd));
const missingOptions = options.filter((opt) => !docs.includes(opt));
if (missingCommands.length || missingOptions.length) {
console.error("DOCS PARITY CHECK FAILED");
if (missingCommands.length) {
console.error("Missing command entries:");
for (const cmd of missingCommands) console.error(` - ${cmd}`);
}
if (missingOptions.length) {
console.error("Missing long options:");
for (const opt of missingOptions) console.error(` - ${opt}`);
}
process.exit(1);
}
console.log(`DOCS PARITY CHECK OK: ${commandSet.size} commands, ${options.length} options`);