| #!/usr/bin/env node
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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);
|
| }
|
|
|
|
|
|
|
|
|
| const DOC_TO_SUBFOLDER = {
|
|
|
| "ARCHITECTURE.md": "architecture",
|
| "CODEBASE_DOCUMENTATION.md": "architecture",
|
| "REPOSITORY_MAP.md": "architecture",
|
| "AUTHZ_GUIDE.md": "architecture",
|
| "RESILIENCE_GUIDE.md": "architecture",
|
|
|
| "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",
|
|
|
| "API_REFERENCE.md": "reference",
|
| "PROVIDER_REFERENCE.md": "reference",
|
| "openapi.yaml": "reference",
|
| "ENVIRONMENT.md": "reference",
|
| "CLI-TOOLS.md": "reference",
|
| "FREE_TIERS.md": "reference",
|
|
|
| "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",
|
|
|
| "AUTO-COMBO.md": "routing",
|
| "REASONING_REPLAY.md": "routing",
|
|
|
| "GUARDRAILS.md": "security",
|
| "COMPLIANCE.md": "security",
|
| "STEALTH_GUIDE.md": "security",
|
|
|
| "COMPRESSION_GUIDE.md": "compression",
|
| "COMPRESSION_ENGINES.md": "compression",
|
| "COMPRESSION_RULES_FORMAT.md": "compression",
|
| "COMPRESSION_LANGUAGE_PACKS.md": "compression",
|
| "RTK_COMPRESSION.md": "compression",
|
|
|
| "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",
|
| };
|
|
|
|
|
|
|
|
|
|
|
| const RE_META = /[\\^$.*+?()[\]{}|]/g;
|
| const FILES_ALT = Object.keys(DOC_TO_SUBFOLDER)
|
| .sort((a, b) => b.length - a.length)
|
| .map((s) => s.replace(RE_META, "\\$&"))
|
| .join("|");
|
|
|
|
|
|
|
|
|
|
|
| function listDocFiles() {
|
| const out = [];
|
|
|
| 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));
|
| }
|
| }
|
|
|
| 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);
|
|
|
| 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;
|
|
|
|
|
|
|
|
|
| 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 || ""})`;
|
| });
|
|
|
|
|
|
|
|
|
| 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;
|
|
|
|
|
| return `docs/${subFolder}/${basename}${anchor || ""}`;
|
| });
|
|
|
| if (out !== src) {
|
| if (!DRY) fs.writeFileSync(filePath, out, "utf8");
|
| return true;
|
| }
|
| return false;
|
| }
|
|
|
|
|
|
|
|
|
|
|
| function listExternalFiles() {
|
|
|
| const candidates = [];
|
|
|
|
|
| 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);
|
| }
|
|
|
|
|
| const dirs = [
|
| ".agents",
|
| ".claude",
|
| ".github",
|
| "bin",
|
| "electron",
|
| "open-sse",
|
| "scripts",
|
| "src",
|
| "tests",
|
| "vscode-extension",
|
|
|
|
|
| "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()) {
|
|
|
| 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;
|
|
|
|
|
|
|
|
|
|
|
| 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;
|
| }
|
|
|
|
|
|
|
|
|
|
|
| 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)" : ""}`
|
| );
|
| }
|
|
|