145 lines
4.7 KiB
JavaScript
145 lines
4.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* run-link-check.mjs — markdown link-check runner (M1, S-104)
|
|
*
|
|
* Runs markdown-link-check across README.md, docs/, and every SKILL.md
|
|
* (excluding node_modules and generated agent-variant directories).
|
|
*
|
|
* Modes:
|
|
* --offline (default) — checks only repo-relative links and #anchor links.
|
|
* All http/https links are ignored. Safe for CI and local dev
|
|
* without network access.
|
|
* --online — checks all links, including external URLs, with timeouts
|
|
* and retries as configured in markdown-link-check.online.json.
|
|
*
|
|
* Exit codes:
|
|
* 0 — all checked links are alive (or ignored in offline mode)
|
|
* 1 — one or more broken links found
|
|
*/
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import { readdirSync, existsSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const REPO_ROOT = path.resolve(__dirname, "../..");
|
|
|
|
// ── CLI arguments ──────────────────────────────────────────────────────────
|
|
|
|
const online = process.argv.includes("--online");
|
|
const configFile = online
|
|
? path.join(REPO_ROOT, "markdown-link-check.online.json")
|
|
: path.join(REPO_ROOT, "markdown-link-check.json");
|
|
|
|
// ── File discovery ─────────────────────────────────────────────────────────
|
|
|
|
const SKIP_PATHS = new Set([
|
|
"skills/atlassian/codex",
|
|
"skills/atlassian/claude-code",
|
|
"skills/atlassian/cursor",
|
|
"skills/atlassian/opencode",
|
|
"skills/atlassian/pi",
|
|
"skills/web-automation/claude-code",
|
|
"skills/web-automation/cursor",
|
|
"skills/web-automation/opencode",
|
|
"skills/web-automation/pi",
|
|
"pi-package",
|
|
]);
|
|
|
|
// Also skip any _source/ subdirectory within skills — canonical source files
|
|
// use relative paths calibrated to the generated location (one level shallower).
|
|
const SKIP_SEGMENT = "_source";
|
|
|
|
function shouldSkip(absPath) {
|
|
const rel = path.relative(REPO_ROOT, absPath);
|
|
for (const skip of SKIP_PATHS) {
|
|
if (rel === skip || rel.startsWith(skip + path.sep)) return true;
|
|
}
|
|
const parts = rel.split(path.sep);
|
|
if (parts.includes("node_modules")) return true;
|
|
// Skip canonical _source/ directories (links calibrated to generated location)
|
|
if (parts.includes(SKIP_SEGMENT)) return true;
|
|
return false;
|
|
}
|
|
|
|
function collectMarkdownFiles(dir) {
|
|
const found = [];
|
|
let entries;
|
|
try {
|
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
} catch {
|
|
return found;
|
|
}
|
|
for (const entry of entries) {
|
|
const full = path.join(dir, entry.name);
|
|
if (shouldSkip(full)) continue;
|
|
if (entry.isDirectory()) {
|
|
found.push(...collectMarkdownFiles(full));
|
|
} else if (
|
|
entry.isFile() &&
|
|
(entry.name.endsWith(".md") || entry.name === "SKILL.md")
|
|
) {
|
|
found.push(full);
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
// ── Collect target files ───────────────────────────────────────────────────
|
|
|
|
const files = [
|
|
path.join(REPO_ROOT, "README.md"),
|
|
...collectMarkdownFiles(path.join(REPO_ROOT, "docs")),
|
|
...collectMarkdownFiles(path.join(REPO_ROOT, "skills")),
|
|
].filter(existsSync);
|
|
|
|
// De-duplicate (README.md could appear twice)
|
|
const uniqueFiles = [...new Set(files)];
|
|
|
|
if (uniqueFiles.length === 0) {
|
|
console.log("link-check: no markdown files found — nothing to check.");
|
|
process.exit(0);
|
|
}
|
|
|
|
console.log(
|
|
`link-check: checking ${uniqueFiles.length} file(s) ` +
|
|
`[mode: ${online ? "online" : "offline"}]…`
|
|
);
|
|
|
|
// ── Run markdown-link-check ────────────────────────────────────────────────
|
|
|
|
const mlcBin = path.join(
|
|
REPO_ROOT,
|
|
"node_modules",
|
|
".bin",
|
|
"markdown-link-check"
|
|
);
|
|
|
|
let failures = 0;
|
|
for (const file of uniqueFiles.sort()) {
|
|
const result = spawnSync(
|
|
mlcBin,
|
|
["--config", configFile, "--quiet", file],
|
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
|
|
);
|
|
const output = (result.stdout + result.stderr).trim();
|
|
const rel = path.relative(REPO_ROOT, file);
|
|
if (result.status !== 0) {
|
|
failures += 1;
|
|
console.error(`\n--- ${rel} ---`);
|
|
if (output) console.error(output);
|
|
}
|
|
}
|
|
|
|
if (failures > 0) {
|
|
console.error(
|
|
`\nlink-check: ${failures} file(s) have broken links. ` +
|
|
`See docs/CLEANUP-BASELINE.md for the as-is baseline.`
|
|
);
|
|
process.exit(1);
|
|
} else {
|
|
console.log("link-check: all links OK.");
|
|
process.exit(0);
|
|
}
|