160 lines
4.9 KiB
JavaScript
160 lines
4.9 KiB
JavaScript
#!/usr/bin/env node
|
|
import { spawnSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
const root = process.cwd();
|
|
const commandDocsDir = path.join(root, "docs", "commands");
|
|
const failures = [];
|
|
|
|
const commands = readHelpCommands();
|
|
const commandNames = commands.map((command) => command.name);
|
|
const commandSet = new Set(commandNames);
|
|
const indexEntries = readCommandIndex();
|
|
const cliPath = buildCLIForHelp();
|
|
|
|
for (const command of commands) {
|
|
const file = path.join(commandDocsDir, `${command.name}.md`);
|
|
if (!fs.existsSync(file)) {
|
|
failures.push(`docs/commands/README.md is missing ${command.name}.md for CLI command ${command.name}`);
|
|
continue;
|
|
}
|
|
const firstLine = fs.readFileSync(file, "utf8").split(/\r?\n/, 1)[0]?.trim();
|
|
if (firstLine !== `# ${command.name}`) {
|
|
failures.push(`${rel(file)} should start with "# ${command.name}"`);
|
|
}
|
|
const help = runCommandHelp(command.name);
|
|
if (help.status !== 0) {
|
|
failures.push(`crabbox ${command.name} --help should exit 0, got ${help.status}: ${help.output.trim()}`);
|
|
} else if (!help.output.includes("Usage:") && !help.output.includes("Usage of ")) {
|
|
failures.push(`crabbox ${command.name} --help should print usage text`);
|
|
}
|
|
}
|
|
|
|
const seenIndexEntries = new Set();
|
|
for (const entry of indexEntries) {
|
|
if (seenIndexEntries.has(entry.name)) {
|
|
failures.push(`docs/commands/README.md lists ${entry.name} more than once`);
|
|
}
|
|
seenIndexEntries.add(entry.name);
|
|
|
|
if (!commandSet.has(entry.name)) {
|
|
failures.push(`docs/commands/README.md lists ${entry.name}, but CLI help does not expose that command`);
|
|
}
|
|
if (entry.href !== `${entry.name}.md`) {
|
|
failures.push(`docs/commands/README.md should link ${entry.name} to ${entry.name}.md, not ${entry.href}`);
|
|
}
|
|
}
|
|
|
|
for (const command of commandNames) {
|
|
if (!seenIndexEntries.has(command)) {
|
|
failures.push(`docs/commands/README.md does not link CLI command ${command}`);
|
|
}
|
|
}
|
|
|
|
const documentedFiles = fs
|
|
.readdirSync(commandDocsDir, { withFileTypes: true })
|
|
.filter((entry) => entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md")
|
|
.map((entry) => entry.name.replace(/\.md$/, ""))
|
|
.sort();
|
|
|
|
for (const fileCommand of documentedFiles) {
|
|
if (!commandSet.has(fileCommand)) {
|
|
failures.push(`docs/commands/${fileCommand}.md exists, but CLI help does not expose ${fileCommand}`);
|
|
}
|
|
}
|
|
|
|
if (indexEntries.map((entry) => entry.name).join("\n") !== commandNames.join("\n")) {
|
|
failures.push(
|
|
`docs/commands/README.md order should match internal/cli/app.go help order:\n${commandNames
|
|
.map((name) => `- [${name}](${name}.md)`)
|
|
.join("\n")}`,
|
|
);
|
|
}
|
|
|
|
if (failures.length) {
|
|
console.error(failures.join("\n"));
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`checked ${commands.length} command docs: command surface ok`);
|
|
|
|
function readHelpCommands() {
|
|
const appPath = path.join(root, "internal", "cli", "app.go");
|
|
const app = fs.readFileSync(appPath, "utf8");
|
|
const match = app.match(/Commands:\n([\s\S]*?)\n\nCommon Flows:/);
|
|
if (!match) {
|
|
fail(`could not find Commands block in ${rel(appPath)}`);
|
|
}
|
|
|
|
const commands = [];
|
|
for (const line of match[1].split(/\r?\n/)) {
|
|
const command = line.match(/^ ([a-z][a-z0-9-]*)\s{2,}\S/);
|
|
if (command) {
|
|
commands.push({ name: command[1] });
|
|
}
|
|
}
|
|
|
|
if (commands.length === 0) {
|
|
fail(`could not parse any commands from ${rel(appPath)}`);
|
|
}
|
|
return commands;
|
|
}
|
|
|
|
function runCommandHelp(command) {
|
|
if (!cliPath) {
|
|
return {
|
|
status: 1,
|
|
output: "failed to build crabbox CLI for help checks",
|
|
};
|
|
}
|
|
const result = spawnSync(cliPath, [command, "--help"], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
});
|
|
return {
|
|
status: result.status ?? 1,
|
|
output: `${result.stdout ?? ""}${result.stderr ?? ""}`,
|
|
};
|
|
}
|
|
|
|
function buildCLIForHelp() {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "crabbox-docs-"));
|
|
const binary = path.join(dir, process.platform === "win32" ? "crabbox.exe" : "crabbox");
|
|
process.on("exit", () => {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
const result = spawnSync("go", ["build", "-o", binary, "./cmd/crabbox"], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
});
|
|
if (result.status !== 0) {
|
|
failures.push(`go build ./cmd/crabbox failed: ${`${result.stdout ?? ""}${result.stderr ?? ""}`.trim()}`);
|
|
return "";
|
|
}
|
|
return binary;
|
|
}
|
|
|
|
function readCommandIndex() {
|
|
const readmePath = path.join(commandDocsDir, "README.md");
|
|
const readme = fs.readFileSync(readmePath, "utf8");
|
|
const entries = [];
|
|
for (const match of readme.matchAll(/^- \[([a-z][a-z0-9-]*)\]\(([^)]+)\)$/gm)) {
|
|
entries.push({ name: match[1], href: match[2] });
|
|
}
|
|
if (entries.length === 0) {
|
|
fail(`could not parse command links from ${rel(readmePath)}`);
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
function fail(message) {
|
|
console.error(message);
|
|
process.exit(1);
|
|
}
|
|
|
|
function rel(file) {
|
|
return path.relative(root, file).replaceAll(path.sep, "/");
|
|
}
|