|
| 1 | +#!/usr/bin/env node |
| 2 | +/** |
| 3 | + * sync-sidebar.mjs |
| 4 | + * |
| 5 | + * Detects docs/*.md files that are NOT listed in the manually‑curated sidebar |
| 6 | + * of astro.config.mjs and auto‑adds them to a "New & Uncategorized" section. |
| 7 | + * |
| 8 | + * Workflow: |
| 9 | + * 1. Scan docs/ for .md files → derive slugs (lowercase, no extension) |
| 10 | + * 2. Read astro.config.mjs, strip any existing auto‑generated section, |
| 11 | + * and extract all *manually* placed sidebar slugs |
| 12 | + * 3. Identify docs that have no manual sidebar entry |
| 13 | + * 4. If any are missing → inject (or update) a "New & Uncategorized" section |
| 14 | + * between marker comments so it can be re‑generated on the next run |
| 15 | + * 5. If none are missing → remove the auto section (user moved them all) |
| 16 | + * |
| 17 | + * Marker comments used in astro.config.mjs: |
| 18 | + * // AUTO-SIDEBAR-START |
| 19 | + * // AUTO-SIDEBAR-END |
| 20 | + * |
| 21 | + * Run: node docs-site/sync-sidebar.mjs (standalone) |
| 22 | + * make docs-build (integrated) |
| 23 | + */ |
| 24 | + |
| 25 | +import { readdirSync, readFileSync, writeFileSync } from "fs"; |
| 26 | +import { join, dirname } from "path"; |
| 27 | +import { fileURLToPath } from "url"; |
| 28 | + |
| 29 | +const __dirname = dirname(fileURLToPath(import.meta.url)); |
| 30 | +const projectRoot = join(__dirname, ".."); |
| 31 | +const configPath = join(__dirname, "astro.config.mjs"); |
| 32 | + |
| 33 | +const MARKER_START = "// AUTO-SIDEBAR-START"; |
| 34 | +const MARKER_END = "// AUTO-SIDEBAR-END"; |
| 35 | + |
| 36 | +// --------------------------------------------------------------------------- |
| 37 | +// Helpers |
| 38 | +// --------------------------------------------------------------------------- |
| 39 | + |
| 40 | +function escapeRegex(s) { |
| 41 | + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); |
| 42 | +} |
| 43 | + |
| 44 | +/** Convert a slug like "agent-analysis" → "Agent Analysis" */ |
| 45 | +function slugToLabel(slug) { |
| 46 | + return slug |
| 47 | + .split("-") |
| 48 | + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) |
| 49 | + .join(" "); |
| 50 | +} |
| 51 | + |
| 52 | +// --------------------------------------------------------------------------- |
| 53 | +// 1. Collect doc slugs from docs/ |
| 54 | +// --------------------------------------------------------------------------- |
| 55 | + |
| 56 | +const docFiles = readdirSync(join(projectRoot, "docs")) |
| 57 | + .filter((f) => f.endsWith(".md") && f !== "README.md") |
| 58 | + .map((f) => f.replace(/\.md$/, "").toLowerCase()) |
| 59 | + .sort(); |
| 60 | + |
| 61 | +// --------------------------------------------------------------------------- |
| 62 | +// 2. Read config — strip auto section to get manual slugs only |
| 63 | +// --------------------------------------------------------------------------- |
| 64 | + |
| 65 | +const config = readFileSync(configPath, "utf-8"); |
| 66 | + |
| 67 | +const autoSectionRegex = new RegExp( |
| 68 | + `[ \\t]*${escapeRegex(MARKER_START)}[\\s\\S]*?${escapeRegex(MARKER_END)}[ \\t]*\\n?`, |
| 69 | + "m", |
| 70 | +); |
| 71 | +const manualConfig = config.replace(autoSectionRegex, ""); |
| 72 | + |
| 73 | +const manualSlugs = [...manualConfig.matchAll(/slug:\s*"([^"]+)"/g)].map( |
| 74 | + (m) => m[1], |
| 75 | +); |
| 76 | + |
| 77 | +// --------------------------------------------------------------------------- |
| 78 | +// 3. Find missing docs |
| 79 | +// --------------------------------------------------------------------------- |
| 80 | + |
| 81 | +const missing = docFiles.filter((slug) => !manualSlugs.includes(slug)); |
| 82 | + |
| 83 | +// --------------------------------------------------------------------------- |
| 84 | +// 4. Nothing missing — make sure auto section is removed and exit |
| 85 | +// --------------------------------------------------------------------------- |
| 86 | + |
| 87 | +if (missing.length === 0) { |
| 88 | + if (config !== manualConfig) { |
| 89 | + // Auto section existed but is no longer needed — clean it up |
| 90 | + writeFileSync(configPath, manualConfig); |
| 91 | + console.log( |
| 92 | + "🧹 Removed empty auto‑generated sidebar section (all docs are manually placed).", |
| 93 | + ); |
| 94 | + } |
| 95 | + console.log("✅ All docs are included in the sidebar."); |
| 96 | + process.exit(0); |
| 97 | +} |
| 98 | + |
| 99 | +// --------------------------------------------------------------------------- |
| 100 | +// 5. Build the new auto section |
| 101 | +// --------------------------------------------------------------------------- |
| 102 | + |
| 103 | +console.log(`\n📄 Found ${missing.length} new doc(s) not yet in sidebar:`); |
| 104 | +missing.forEach((slug) => console.log(` • docs/${slug}.md`)); |
| 105 | + |
| 106 | +const items = missing |
| 107 | + .map( |
| 108 | + (slug) => |
| 109 | + ` { label: "${slugToLabel(slug)}", slug: "${slug}" },`, |
| 110 | + ) |
| 111 | + .join("\n"); |
| 112 | + |
| 113 | +const newSection = [ |
| 114 | + ` ${MARKER_START}`, |
| 115 | + ` {`, |
| 116 | + ` label: "New & Uncategorized",`, |
| 117 | + ` items: [`, |
| 118 | + items, |
| 119 | + ` ],`, |
| 120 | + ` },`, |
| 121 | + ` ${MARKER_END}`, |
| 122 | +].join("\n"); |
| 123 | + |
| 124 | +// --------------------------------------------------------------------------- |
| 125 | +// 6. Patch config |
| 126 | +// --------------------------------------------------------------------------- |
| 127 | + |
| 128 | +let updatedConfig; |
| 129 | + |
| 130 | +if (config.includes(MARKER_START)) { |
| 131 | + // Replace existing auto section in‑place |
| 132 | + updatedConfig = config.replace(autoSectionRegex, newSection + "\n"); |
| 133 | +} else { |
| 134 | + // First time — insert just before the sidebar array's closing "]," |
| 135 | + // That bracket is at 6‑space indent and is the LAST such occurrence. |
| 136 | + const lines = config.split("\n"); |
| 137 | + let insertAt = -1; |
| 138 | + for (let i = lines.length - 1; i >= 0; i--) { |
| 139 | + if (/^\s{6}\],/.test(lines[i])) { |
| 140 | + insertAt = i; |
| 141 | + break; |
| 142 | + } |
| 143 | + } |
| 144 | + if (insertAt === -1) { |
| 145 | + console.error( |
| 146 | + "❌ Could not locate the sidebar closing bracket in astro.config.mjs", |
| 147 | + ); |
| 148 | + process.exit(1); |
| 149 | + } |
| 150 | + lines.splice(insertAt, 0, newSection); |
| 151 | + updatedConfig = lines.join("\n"); |
| 152 | +} |
| 153 | + |
| 154 | +writeFileSync(configPath, updatedConfig); |
| 155 | + |
| 156 | +console.log( |
| 157 | + `\n✅ Auto‑added ${missing.length} doc(s) to "New & Uncategorized" sidebar section.`, |
| 158 | +); |
| 159 | +console.log( |
| 160 | + " 💡 Move them to the appropriate section in astro.config.mjs when ready.", |
| 161 | +); |
0 commit comments