refactor(scripts): make docs index generator and checkers recurse subfolders

Update tooling for the new docs/<subfolder>/ layout:

- scripts/generate-docs-index.mjs walks the 8 subfolders in defined order and
  emits fileName values relative to docs/ (e.g. "architecture/ARCHITECTURE.md").
- scripts/check-docs-sync.mjs reads docs/reference/openapi.yaml.
- scripts/check-docs-counts-sync.mjs targets new doc paths.
- scripts/check-env-doc-sync.mjs reads docs/reference/ENVIRONMENT.md.
- scripts/gen-provider-reference.ts writes to docs/reference/PROVIDER_REFERENCE.md.
- scripts/pack-artifact-policy.ts allowlists docs/reference/openapi.yaml.
- New scripts/docs/{fix-internal-links,move-i18n-mirrors}.mjs are one-shot
  FASE 3 helpers, safe to delete after merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
diegosouzapw
2026-05-13 13:12:56 -03:00
parent eb02fda327
commit ac39badc2e
8 changed files with 508 additions and 109 deletions

View File

@@ -0,0 +1,274 @@
#!/usr/bin/env node
// One-shot: FASE 3 helper, safe to delete after merge.
//
// Rewrites doc-file references after the docs/ flat -> subfolder restructure.
//
// Modes:
// --internal Rewrite relative links inside docs/<subfolder>/*.md to point at
// the new subfolder paths (e.g. ./AUTO-COMBO.md -> ../routing/AUTO-COMBO.md).
// --external Rewrite absolute-style `docs/<DOC>.md` references in files
// outside docs/ (README, CLAUDE.md, .agents, .claude, scripts, src,
// tests, etc.) to `docs/<subfolder>/<DOC>.md`.
//
// Usage:
// node scripts/docs/fix-internal-links.mjs --internal
// node scripts/docs/fix-internal-links.mjs --external
// node scripts/docs/fix-internal-links.mjs --internal --external --dry
//
// Notes:
// - Idempotent: rerunning does not double-rewrite (it only matches the old shape).
// - openapi.yaml lives under docs/reference/ — also handled.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, "..", "..");
const DOCS = path.join(ROOT, "docs");
const args = new Set(process.argv.slice(2));
const RUN_INTERNAL = args.has("--internal");
const RUN_EXTERNAL = args.has("--external");
const DRY = args.has("--dry");
if (!RUN_INTERNAL && !RUN_EXTERNAL) {
console.error("Pass --internal and/or --external (and optionally --dry).");
process.exit(2);
}
// ----------------------------------------------------------------------
// Mapping: stem -> subfolder
// ----------------------------------------------------------------------
const DOC_TO_SUBFOLDER = {
// architecture
"ARCHITECTURE.md": "architecture",
"CODEBASE_DOCUMENTATION.md": "architecture",
"REPOSITORY_MAP.md": "architecture",
"AUTHZ_GUIDE.md": "architecture",
"RESILIENCE_GUIDE.md": "architecture",
// guides
"SETUP_GUIDE.md": "guides",
"USER_GUIDE.md": "guides",
"DOCKER_GUIDE.md": "guides",
"ELECTRON_GUIDE.md": "guides",
"TERMUX_GUIDE.md": "guides",
"PWA_GUIDE.md": "guides",
"TROUBLESHOOTING.md": "guides",
"UNINSTALL.md": "guides",
"I18N.md": "guides",
"FEATURES.md": "guides",
// reference
"API_REFERENCE.md": "reference",
"PROVIDER_REFERENCE.md": "reference",
"openapi.yaml": "reference",
"ENVIRONMENT.md": "reference",
"CLI-TOOLS.md": "reference",
"FREE_TIERS.md": "reference",
// frameworks
"MCP-SERVER.md": "frameworks",
"A2A-SERVER.md": "frameworks",
"AGENT_PROTOCOLS_GUIDE.md": "frameworks",
"CLOUD_AGENT.md": "frameworks",
"SKILLS.md": "frameworks",
"MEMORY.md": "frameworks",
"WEBHOOKS.md": "frameworks",
"EVALS.md": "frameworks",
// routing
"AUTO-COMBO.md": "routing",
"REASONING_REPLAY.md": "routing",
// security
"GUARDRAILS.md": "security",
"COMPLIANCE.md": "security",
"STEALTH_GUIDE.md": "security",
// compression
"COMPRESSION_GUIDE.md": "compression",
"COMPRESSION_ENGINES.md": "compression",
"COMPRESSION_RULES_FORMAT.md": "compression",
"COMPRESSION_LANGUAGE_PACKS.md": "compression",
"RTK_COMPRESSION.md": "compression",
// ops
"RELEASE_CHECKLIST.md": "ops",
"COVERAGE_PLAN.md": "ops",
"FLY_IO_DEPLOYMENT_GUIDE.md": "ops",
"VM_DEPLOYMENT_GUIDE.md": "ops",
"PROXY_GUIDE.md": "ops",
"TUNNELS_GUIDE.md": "ops",
};
// Build alternation regex (longest-first) of file basenames we know about.
const FILES_ALT = Object.keys(DOC_TO_SUBFOLDER)
.sort((a, b) => b.length - a.length)
.map((s) => s.replace(/\./g, "\\."))
.join("|");
// ----------------------------------------------------------------------
// Internal rewriter (within docs/**)
// ----------------------------------------------------------------------
function listDocFiles() {
const out = [];
// walk subfolders we created
for (const sub of new Set(Object.values(DOC_TO_SUBFOLDER))) {
const dir = path.join(DOCS, sub);
if (!fs.existsSync(dir)) continue;
for (const f of fs.readdirSync(dir)) {
if (!f.endsWith(".md") && !f.endsWith(".yaml")) continue;
out.push(path.join(dir, f));
}
}
// also rewrite the new README and diagrams README
out.push(path.join(DOCS, "README.md"));
return out;
}
function relativeFromTo(fromAbsFile, targetSub, targetBasename) {
const fromDir = path.dirname(fromAbsFile);
const toAbs = path.join(DOCS, targetSub, targetBasename);
let rel = path.relative(fromDir, toAbs);
// posix-style
rel = rel.split(path.sep).join("/");
if (!rel.startsWith(".")) rel = "./" + rel;
return rel;
}
function rewriteInternal(filePath) {
const src = fs.readFileSync(filePath, "utf8");
let out = src;
// Pattern A: relative refs like ./FOO.md, ../FOO.md, or bare FOO.md inside (... ).
// We match `]( <optional ./ or ../+> <basename> )` and `]( <basename> )`.
// Captures: prefix (=./ or ../ chains, may be empty), basename.
const reA = new RegExp(`\\]\\(\\s*((?:\\.{1,2}/)*)(${FILES_ALT})((?:#[^\\s)]+)?)\\s*\\)`, "g");
out = out.replace(reA, (full, prefix, basename, anchor) => {
const subFolder = DOC_TO_SUBFOLDER[basename];
if (!subFolder) return full;
const newRel = relativeFromTo(filePath, subFolder, basename);
return `](${newRel}${anchor || ""})`;
});
// Pattern B: absolute-style `docs/FOO.md` inside markdown links — convert
// to `docs/<sub>/FOO.md`. We avoid double-rewriting if a subfolder is
// already present.
const reB = new RegExp(`docs/(${FILES_ALT})((?:#[^\\s)\"']+)?)`, "g");
out = out.replace(reB, (full, basename, anchor) => {
const subFolder = DOC_TO_SUBFOLDER[basename];
if (!subFolder) return full;
// If preceded by `<sub>/` already, skip — but the regex won't capture that
// because it only matches `docs/<basename>`. So this is safe.
return `docs/${subFolder}/${basename}${anchor || ""}`;
});
if (out !== src) {
if (!DRY) fs.writeFileSync(filePath, out, "utf8");
return true;
}
return false;
}
// ----------------------------------------------------------------------
// External rewriter (files outside docs/)
// ----------------------------------------------------------------------
function listExternalFiles() {
// We rewrite a curated set of paths to avoid wandering into vendor / build dirs.
const candidates = [];
// 1) Root-level documentation files
for (const f of fs.readdirSync(ROOT)) {
const full = path.join(ROOT, f);
if (!fs.statSync(full).isFile()) continue;
if (/\.(md|txt)$/i.test(f)) candidates.push(full);
}
// 2) Specific directories we know contain doc references
const dirs = [
".agents",
".claude",
".github",
"bin",
"electron",
"open-sse",
"scripts",
"src",
"tests",
"vscode-extension",
// i18n mirrors — root-level locale files (llm.txt, CHANGELOG.md, etc.) reference
// the root /docs/ paths and must stay in sync after restructure.
"docs/i18n",
];
for (const d of dirs) {
const full = path.join(ROOT, d);
if (!fs.existsSync(full)) continue;
walk(full, candidates);
}
return candidates;
}
function walk(dir, out) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (entry.name === "node_modules" || entry.name === ".next") continue;
if (entry.name.startsWith(".git")) continue;
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(full, out);
} else if (entry.isFile()) {
// accept text-y file types
if (/\.(md|txt|ts|tsx|mjs|cjs|js|json|yaml|yml|sh)$/i.test(entry.name)) {
out.push(full);
}
}
}
}
function rewriteExternal(filePath) {
const src = fs.readFileSync(filePath, "utf8");
let out = src;
// Rewrite any `docs/FOO.md` (only the bare-basename form, not already
// pointing into a subfolder) to `docs/<sub>/FOO.md`.
// Word-boundary lookbehind/lookahead-ish: use (?<![\w/-]) so we don't touch
// already-prefixed `docs/architecture/FOO.md`.
const re = new RegExp(`(?<![A-Za-z0-9_./-])docs/(${FILES_ALT})((?:#[^\\s)\"'\`]+)?)`, "g");
out = out.replace(re, (full, basename, anchor) => {
const subFolder = DOC_TO_SUBFOLDER[basename];
if (!subFolder) return full;
return `docs/${subFolder}/${basename}${anchor || ""}`;
});
if (out !== src) {
if (!DRY) fs.writeFileSync(filePath, out, "utf8");
return true;
}
return false;
}
// ----------------------------------------------------------------------
// Run
// ----------------------------------------------------------------------
let internalChanged = 0;
let externalChanged = 0;
let internalScanned = 0;
let externalScanned = 0;
if (RUN_INTERNAL) {
const files = listDocFiles();
for (const f of files) {
internalScanned++;
if (rewriteInternal(f)) internalChanged++;
}
console.log(
`[internal] scanned=${internalScanned} changed=${internalChanged}${DRY ? " (dry-run)" : ""}`
);
}
if (RUN_EXTERNAL) {
const files = listExternalFiles();
for (const f of files) {
externalScanned++;
if (rewriteExternal(f)) externalChanged++;
}
console.log(
`[external] scanned=${externalScanned} changed=${externalChanged}${DRY ? " (dry-run)" : ""}`
);
}

View File

@@ -0,0 +1,139 @@
#!/usr/bin/env node
// One-shot: FASE 3 helper, safe to delete after merge.
//
// Moves existing i18n mirror docs from `docs/i18n/<lang>/docs/X.md` into the
// matching subfolder `docs/i18n/<lang>/docs/<sub>/X.md`, mirroring the new
// docs/ layout. Uses `git mv` to preserve history.
//
// Usage:
// node scripts/docs/move-i18n-mirrors.mjs [--dry]
//
// Notes:
// - Skips files that don't appear in DOC_TO_SUBFOLDER (e.g., the legacy
// `cloudflare-zero-trust-guide.md` or `features/` subfolder — those will be
// handled in FASE 5 when translations are regenerated).
// - Idempotent: if the target already lives under a subfolder, the entry is
// skipped.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { execSync } from "node:child_process";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, "..", "..");
const I18N_DIR = path.join(ROOT, "docs", "i18n");
const DRY = process.argv.includes("--dry");
const DOC_TO_SUBFOLDER = {
// architecture
"ARCHITECTURE.md": "architecture",
"CODEBASE_DOCUMENTATION.md": "architecture",
"REPOSITORY_MAP.md": "architecture",
"AUTHZ_GUIDE.md": "architecture",
"RESILIENCE_GUIDE.md": "architecture",
// guides
"SETUP_GUIDE.md": "guides",
"USER_GUIDE.md": "guides",
"DOCKER_GUIDE.md": "guides",
"ELECTRON_GUIDE.md": "guides",
"TERMUX_GUIDE.md": "guides",
"PWA_GUIDE.md": "guides",
"TROUBLESHOOTING.md": "guides",
"UNINSTALL.md": "guides",
"I18N.md": "guides",
"FEATURES.md": "guides",
// reference
"API_REFERENCE.md": "reference",
"PROVIDER_REFERENCE.md": "reference",
"openapi.yaml": "reference",
"ENVIRONMENT.md": "reference",
"CLI-TOOLS.md": "reference",
"FREE_TIERS.md": "reference",
// frameworks
"MCP-SERVER.md": "frameworks",
"A2A-SERVER.md": "frameworks",
"AGENT_PROTOCOLS_GUIDE.md": "frameworks",
"CLOUD_AGENT.md": "frameworks",
"SKILLS.md": "frameworks",
"MEMORY.md": "frameworks",
"WEBHOOKS.md": "frameworks",
"EVALS.md": "frameworks",
// routing
"AUTO-COMBO.md": "routing",
"REASONING_REPLAY.md": "routing",
// security
"GUARDRAILS.md": "security",
"COMPLIANCE.md": "security",
"STEALTH_GUIDE.md": "security",
// compression
"COMPRESSION_GUIDE.md": "compression",
"COMPRESSION_ENGINES.md": "compression",
"COMPRESSION_RULES_FORMAT.md": "compression",
"COMPRESSION_LANGUAGE_PACKS.md": "compression",
"RTK_COMPRESSION.md": "compression",
// ops
"RELEASE_CHECKLIST.md": "ops",
"COVERAGE_PLAN.md": "ops",
"FLY_IO_DEPLOYMENT_GUIDE.md": "ops",
"VM_DEPLOYMENT_GUIDE.md": "ops",
"PROXY_GUIDE.md": "ops",
"TUNNELS_GUIDE.md": "ops",
};
let moved = 0;
let skipped = 0;
const seenLocales = [];
for (const locale of fs.readdirSync(I18N_DIR)) {
const localeDir = path.join(I18N_DIR, locale);
const stat = fs.statSync(localeDir);
if (!stat.isDirectory()) continue;
const docsDir = path.join(localeDir, "docs");
if (!fs.existsSync(docsDir)) continue;
seenLocales.push(locale);
for (const fname of fs.readdirSync(docsDir)) {
const sub = DOC_TO_SUBFOLDER[fname];
if (!sub) continue; // not in our mapping (e.g. features/, cloudflare-zero-trust-guide.md)
const src = path.join(docsDir, fname);
if (!fs.statSync(src).isFile()) continue;
const subDir = path.join(docsDir, sub);
const dst = path.join(subDir, fname);
if (fs.existsSync(dst)) {
skipped++;
continue;
}
if (DRY) {
console.log(`would move: ${path.relative(ROOT, src)} -> ${path.relative(ROOT, dst)}`);
moved++;
continue;
}
if (!fs.existsSync(subDir)) fs.mkdirSync(subDir, { recursive: true });
try {
execSync(`git mv -k -- "${path.relative(ROOT, src)}" "${path.relative(ROOT, dst)}"`, {
cwd: ROOT,
stdio: "pipe",
});
moved++;
} catch (e) {
// fallback: copy + delete
fs.renameSync(src, dst);
execSync(
`git rm --cached -- "${path.relative(ROOT, src)}" 2>/dev/null || true; git add -- "${path.relative(ROOT, dst)}"`,
{ cwd: ROOT, stdio: "pipe", shell: "/bin/bash" }
);
moved++;
}
}
}
console.log(
`[i18n-mirrors] locales=${seenLocales.length} moved=${moved} skipped=${skipped}${DRY ? " (dry-run)" : ""}`
);