diff --git a/scripts/check-docs-counts-sync.mjs b/scripts/check-docs-counts-sync.mjs index 833d52c5a5..b6e9859ae9 100644 --- a/scripts/check-docs-counts-sync.mjs +++ b/scripts/check-docs-counts-sync.mjs @@ -59,31 +59,31 @@ const checks = [ label: "Executors count", actual: countFiles("open-sse/executors"), docKey: "executors", - docs: ["ARCHITECTURE.md", "CODEBASE_DOCUMENTATION.md"], + docs: ["architecture/ARCHITECTURE.md", "architecture/CODEBASE_DOCUMENTATION.md"], }, { label: "Routing strategies count", actual: countRoutingStrategies(), docKey: "strategies", - docs: ["AUTO-COMBO.md", "RESILIENCE_GUIDE.md"], + docs: ["routing/AUTO-COMBO.md", "architecture/RESILIENCE_GUIDE.md"], }, { label: "OAuth providers count", actual: countFiles("src/lib/oauth/providers"), docKey: "OAuth providers", - docs: ["ARCHITECTURE.md"], + docs: ["architecture/ARCHITECTURE.md"], }, { label: "A2A skills count", actual: countFiles("src/lib/a2a/skills"), docKey: "A2A skills", - docs: ["A2A-SERVER.md"], + docs: ["frameworks/A2A-SERVER.md"], }, { label: "Cloud agents count", actual: countFiles("src/lib/cloudAgent/agents"), docKey: "cloud agents", - docs: ["CLOUD_AGENT.md", "AGENT_PROTOCOLS_GUIDE.md"], + docs: ["frameworks/CLOUD_AGENT.md", "frameworks/AGENT_PROTOCOLS_GUIDE.md"], }, ]; diff --git a/scripts/check-docs-sync.mjs b/scripts/check-docs-sync.mjs index c8434fa185..05c1bbbb09 100644 --- a/scripts/check-docs-sync.mjs +++ b/scripts/check-docs-sync.mjs @@ -5,7 +5,7 @@ import path from "node:path"; const cwd = process.cwd(); const packageJsonPath = path.resolve(cwd, "package.json"); -const openApiPath = path.resolve(cwd, "docs/openapi.yaml"); +const openApiPath = path.resolve(cwd, "docs/reference/openapi.yaml"); const changelogPath = path.resolve(cwd, "CHANGELOG.md"); const llmPath = path.resolve(cwd, "llm.txt"); const i18nDocsPath = path.resolve(cwd, "docs/i18n"); @@ -209,7 +209,7 @@ try { const openApiVersion = extractOpenApiVersion(readText(openApiPath)); if (!openApiVersion) { - fail("could not extract docs/openapi.yaml info.version"); + fail("could not extract docs/reference/openapi.yaml info.version"); } else if (openApiVersion !== packageVersion) { fail(`OpenAPI version (${openApiVersion}) differs from package.json (${packageVersion})`); } else { diff --git a/scripts/check-env-doc-sync.mjs b/scripts/check-env-doc-sync.mjs index 8f03665604..7e1111991d 100644 --- a/scripts/check-env-doc-sync.mjs +++ b/scripts/check-env-doc-sync.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -// Validates that env vars referenced in code appear in .env.example AND in docs/ENVIRONMENT.md. +// Validates that env vars referenced in code appear in .env.example AND in docs/reference/ENVIRONMENT.md. // Exits 0 on success, 1 on missing entries. Designed for use in pre-commit / CI. // // Run: node scripts/check-env-doc-sync.mjs @@ -14,7 +14,7 @@ import { execSync } from "node:child_process"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, ".."); const ENV_EXAMPLE = path.join(ROOT, ".env.example"); -const ENV_DOC = path.join(ROOT, "docs", "ENVIRONMENT.md"); +const ENV_DOC = path.join(ROOT, "docs", "reference", "ENVIRONMENT.md"); const STRICT = process.argv.includes("--strict"); @@ -102,7 +102,7 @@ function main() { console.log("==================="); console.log(`Code references: ${codeVars.size} unique vars`); console.log(`In .env.example: ${exampleVars.size} unique vars`); - console.log(`In docs/ENVIRONMENT.md: ${docVars.size} unique vars (heuristic)`); + console.log(`In docs/reference/ENVIRONMENT.md: ${docVars.size} unique vars (heuristic)`); console.log(); function printList(label, list) { diff --git a/scripts/docs/fix-internal-links.mjs b/scripts/docs/fix-internal-links.mjs new file mode 100644 index 0000000000..abe933f94b --- /dev/null +++ b/scripts/docs/fix-internal-links.mjs @@ -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//*.md to point at +// the new subfolder paths (e.g. ./AUTO-COMBO.md -> ../routing/AUTO-COMBO.md). +// --external Rewrite absolute-style `docs/.md` references in files +// outside docs/ (README, CLAUDE.md, .agents, .claude, scripts, src, +// tests, etc.) to `docs//.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 `]( )` and `]( )`. + // 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//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 `/` already, skip — but the regex won't capture that + // because it only matches `docs/`. 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//FOO.md`. + // Word-boundary lookbehind/lookahead-ish: use (? { + 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)" : ""}` + ); +} diff --git a/scripts/docs/move-i18n-mirrors.mjs b/scripts/docs/move-i18n-mirrors.mjs new file mode 100644 index 0000000000..110f3dfda2 --- /dev/null +++ b/scripts/docs/move-i18n-mirrors.mjs @@ -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//docs/X.md` into the +// matching subfolder `docs/i18n//docs//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)" : ""}` +); diff --git a/scripts/gen-provider-reference.ts b/scripts/gen-provider-reference.ts index c8a62be86a..fcad51d1d8 100644 --- a/scripts/gen-provider-reference.ts +++ b/scripts/gen-provider-reference.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -// Generates docs/PROVIDER_REFERENCE.md from src/shared/constants/providers.ts. +// Generates docs/reference/PROVIDER_REFERENCE.md from src/shared/constants/providers.ts. // Run: node --import tsx/esm scripts/gen-provider-reference.ts import fs from "node:fs"; @@ -26,7 +26,7 @@ import { const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, ".."); -const OUT_FILE = path.join(ROOT, "docs", "PROVIDER_REFERENCE.md"); +const OUT_FILE = path.join(ROOT, "docs", "reference", "PROVIDER_REFERENCE.md"); type ProviderRecord = { id: string; diff --git a/scripts/generate-docs-index.mjs b/scripts/generate-docs-index.mjs index 69c6babd88..f4d9f861b9 100644 --- a/scripts/generate-docs-index.mjs +++ b/scripts/generate-docs-index.mjs @@ -1,12 +1,15 @@ #!/usr/bin/env node /** - * Build-time script: scans docs/*.md and generates + * Build-time script: scans docs//*.md and generates * src/app/docs/lib/docs-auto-generated.ts with static navigation + search data. * * This file is imported by both client and server components — NO fs/path imports. * Run via: node scripts/generate-docs-index.mjs * Automatically runs as prebuild step. + * + * Sections come from the on-disk subfolder layout introduced in FASE 3 of the + * platform overhaul. Order is fixed by SECTION_ORDER below. */ import fs from "node:fs"; @@ -19,64 +22,19 @@ const ROOT = path.resolve(__dirname, ".."); const DOCS_DIR = path.join(ROOT, "docs"); const OUT_FILE = path.join(ROOT, "src", "app", "docs", "lib", "docs-auto-generated.ts"); -const SECTION_CATEGORIES = { - "Getting Started": [ - "SETUP_GUIDE", - "USER_GUIDE", - "CLI_TOOLS", - "ARCHITECTURE", - "QUICK_START", - "GETTING_STARTED", - ], - Features: [ - "FEATURES", - "AUTO_COMBO", - "COMPRESSION_GUIDE", - "RTK_COMPRESSION", - "COMPRESSION_ENGINES", - "COMPRESSION_RULES_FORMAT", - "COMPRESSION_LANGUAGE_PACKS", - "FREE_TIERS", - ], - "API & Protocols": ["API_REFERENCE", "MCP_SERVER", "A2A_SERVER"], - Deployment: [ - "DOCKER_GUIDE", - "VM_DEPLOYMENT_GUIDE", - "FLY_IO_DEPLOYMENT_GUIDE", - "TERMUX_GUIDE", - "PWA_GUIDE", - ], - Operations: ["PROXY_GUIDE", "RESILIENCE_GUIDE", "ENVIRONMENT", "TROUBLESHOOTING"], - Development: [ - "CODEBASE_DOCUMENTATION", - "COVERAGE_PLAN", - "I18N", - "RELEASE_CHECKLIST", - "UNINSTALL", - "CONTRIBUTING", - "CHANGELOG", - "CODE_OF_CONDUCT", - ], -}; +// Subfolder -> nav section title, in the order they should appear in the sidebar. +const SECTION_ORDER = [ + { dir: "architecture", title: "Architecture" }, + { dir: "guides", title: "Guides" }, + { dir: "reference", title: "Reference" }, + { dir: "frameworks", title: "Frameworks" }, + { dir: "routing", title: "Routing" }, + { dir: "security", title: "Security" }, + { dir: "compression", title: "Compression" }, + { dir: "ops", title: "Ops" }, +]; -const SECTION_ORDER = { - "Getting Started": 1, - Features: 2, - "API & Protocols": 3, - Deployment: 4, - Operations: 5, - Development: 6, -}; - -function categorizeFile(fileName) { - const stem = fileName.replace(/\.md$/i, "").toUpperCase().replace(/-/g, "_"); - for (const [section, patterns] of Object.entries(SECTION_CATEGORIES)) { - if (patterns.some((p) => stem === p)) { - return section; - } - } - return "Other"; -} +const SECTION_INDEX = Object.fromEntries(SECTION_ORDER.map((s, i) => [s.title, i + 1])); function extractTitleFromContent(content) { const match = content.match(/^#\s+(.+)$/m); @@ -114,16 +72,7 @@ function extractContentPreview(content) { return stripped.slice(0, 300); } -// ---------- Main ---------- - -if (!fs.existsSync(DOCS_DIR)) { - if (fs.existsSync(OUT_FILE)) { - console.warn( - `[generate-docs-index] ${DOCS_DIR} not found; keeping existing generated docs index.` - ); - process.exit(0); - } - +function emitEmpty(reason) { fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true }); fs.writeFileSync( OUT_FILE, @@ -158,51 +107,88 @@ export const autoAllSlugs: string[] = []; `, "utf8" ); - console.warn(`[generate-docs-index] ${DOCS_DIR} not found; generated empty docs index.`); + console.warn(`[generate-docs-index] ${reason}; generated empty docs index.`); +} + +// ---------- Main ---------- + +if (!fs.existsSync(DOCS_DIR)) { + if (fs.existsSync(OUT_FILE)) { + console.warn( + `[generate-docs-index] ${DOCS_DIR} not found; keeping existing generated docs index.` + ); + process.exit(0); + } + emitEmpty(`${DOCS_DIR} not found`); process.exit(0); } -const files = fs.readdirSync(DOCS_DIR).filter((f) => f.endsWith(".md") || f.endsWith(".mdx")); - const docs = []; -for (const fileName of files) { - const filePath = path.join(DOCS_DIR, fileName); - const fileContent = fs.readFileSync(filePath, "utf8"); - const { data: frontmatter, content } = matter(fileContent); - const slug = - frontmatter.slug || - fileName - .replace(/\.mdx?$/i, "") - .toLowerCase() - .replace(/_/g, "-"); - const title = frontmatter.title || extractTitleFromContent(content) || slug.replace(/-/g, " "); - const section = frontmatter.section || categorizeFile(fileName); - const order = frontmatter.order ?? 999; - const headings = extractHeadings(content); - const contentPreview = frontmatter.description || extractContentPreview(content); +for (const { dir, title } of SECTION_ORDER) { + const fullDir = path.join(DOCS_DIR, dir); + if (!fs.existsSync(fullDir)) continue; + const entries = fs.readdirSync(fullDir, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isFile()) continue; + if (!entry.name.endsWith(".md") && !entry.name.endsWith(".mdx")) continue; + if (entry.name === "README.md") continue; // section index, not a doc page - docs.push({ slug, title, fileName, section, order, content: contentPreview, headings }); + const absPath = path.join(fullDir, entry.name); + const fileContent = fs.readFileSync(absPath, "utf8"); + const { data: frontmatter, content } = matter(fileContent); + + const slug = + frontmatter.slug || + entry.name + .replace(/\.mdx?$/i, "") + .toLowerCase() + .replace(/_/g, "-"); + + // fileName is relative to docs/ — used by the slug page to read content. + const fileName = path.posix.join(dir, entry.name); + + const titleStr = + frontmatter.title || extractTitleFromContent(content) || slug.replace(/-/g, " "); + const section = frontmatter.section || title; + const order = frontmatter.order ?? 999; + const headings = extractHeadings(content); + const contentPreview = frontmatter.description || extractContentPreview(content); + + docs.push({ + slug, + title: titleStr, + fileName, + section, + order, + content: contentPreview, + headings, + }); + } +} + +if (docs.length === 0) { + emitEmpty("no docs discovered in subfolders"); + process.exit(0); } docs.sort((a, b) => { - const sectionA = SECTION_ORDER[a.section] ?? 99; - const sectionB = SECTION_ORDER[b.section] ?? 99; + const sectionA = SECTION_INDEX[a.section] ?? 99; + const sectionB = SECTION_INDEX[b.section] ?? 99; if (sectionA !== sectionB) return sectionA - sectionB; return a.order - b.order; }); -// Build navigation sections +// Build navigation sections in the configured order. const sectionMap = new Map(); for (const doc of docs) { const items = sectionMap.get(doc.section) || []; items.push(doc); sectionMap.set(doc.section, items); } -const orderedSections = [...new Set([...Object.keys(SECTION_ORDER), ...sectionMap.keys()])]; -const navSections = orderedSections - .filter((s) => sectionMap.has(s)) - .sort((a, b) => (SECTION_ORDER[a] ?? 99) - (SECTION_ORDER[b] ?? 99)) + +const navSections = SECTION_ORDER.map(({ title }) => title) + .filter((t) => sectionMap.has(t)) .map((title) => ({ title, items: sectionMap.get(title).map((doc) => ({ @@ -228,8 +214,8 @@ if (searchIndex.some((item) => item.slug === "api-reference")) { searchIndex.push({ slug: "api-explorer", title: "API Explorer", - fileName: "API_REFERENCE.md", - section: "API & Protocols", + fileName: "reference/API_REFERENCE.md", + section: "Reference", content: "interactive try it live api explorer endpoint test request response curl example", headings: ["Try It", "Endpoints"], }); diff --git a/scripts/pack-artifact-policy.ts b/scripts/pack-artifact-policy.ts index 5417abecce..6882894916 100644 --- a/scripts/pack-artifact-policy.ts +++ b/scripts/pack-artifact-policy.ts @@ -29,7 +29,7 @@ export const APP_STAGING_REMOVAL_PATHS: string[] = [ export const APP_STAGING_ALLOWED_EXACT_PATHS: string[] = [ ".env.example", - "docs/openapi.yaml", + "docs/reference/openapi.yaml", "open-sse/mcp-server/server.js", "package.json", "responses-ws-proxy.mjs",