Something went wrong. Try again.
A video game where you play as a misaligned AI, deceiving and building power. An experiment in spec-driven development.
Something went wrong. Try again.
9.5 kB · 335 lines
JavaScript
at commit e957ce7b
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336#!/usr/bin/env node/** * Sync repo wiki/ (+ DESIGN.md) into Starlight's src/content/docs/, and * emit a sidebar module from wiki/SUMMARY.md. * * Generated paths are gitignored; never hand-edit them. */import fs from 'node:fs';import path from 'node:path';import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));const SITE_ROOT = path.resolve(__dirname, '..');const REPO_ROOT = path.resolve(SITE_ROOT, '..');const WIKI_ROOT = path.join(REPO_ROOT, 'wiki');const DOCS_OUT = path.join(SITE_ROOT, 'src', 'content', 'docs');const GENERATED_OUT = path.join(SITE_ROOT, 'src', 'generated');const SIDEBAR_OUT = path.join(GENERATED_OUT, 'sidebar.ts');
function rmrf(dir) { fs.rmSync(dir, { recursive: true, force: true });}
function ensureDir(dir) { fs.mkdirSync(dir, { recursive: true });}
const ASSET_EXT = new Set([ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.avif',]);
function walkFiles(dir, predicate, out = []) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { walkFiles(full, predicate, out); } else if ( (entry.isFile() || entry.isSymbolicLink()) && predicate(entry.name, full) ) { out.push(full); } } return out;}
function extractTitle(body, fallback) { for (const line of body.split(/\r?\n/)) { const m = line.match(/^#\s+(.+?)\s*$/); if (m) { return m[1] .replace(/[*_`]/g, '') .replace(/\s+/g, ' ') .trim(); } } return fallback;}
/** Strip the first ATX H1 — Starlight already renders `title` as the page H1. */function stripLeadingH1(body) { const lines = body.split(/\r?\n/); let i = 0; while (i < lines.length && lines[i].trim() === '') i += 1; if (i < lines.length && /^#\s+/.test(lines[i])) { i += 1; while (i < lines.length && lines[i].trim() === '') i += 1; return lines.slice(i).join('\n'); } return body;}
function yamlEscape(s) { if (/[:#{}[\],&*!|>'"%@`]|^[-?]|\n/.test(s) || s !== s.trim()) { return JSON.stringify(s); } return s;}
/** wiki-relative path → Starlight slug (no leading/trailing slash). */function wikiRelToSlug(relPosix) { let slug = relPosix.replace(/\.md$/i, ''); // Constitution is a docs page; the splash homepage is a custom Astro route. if (slug === 'DESIGN') return 'constitution'; if (slug.endsWith('/README')) { slug = slug.slice(0, -'/README'.length); } else if (slug === 'README') { slug = ''; } // Starlight / Astro content collection IDs are lowercased. return slug.toLowerCase();}
function destFileForWikiRel(relPosix) { if (relPosix === 'DESIGN.md') return path.join(DOCS_OUT, 'constitution.md'); if (relPosix === 'SUMMARY.md') return null; if (/(^|\/)README\.md$/i.test(relPosix)) { const dir = relPosix.replace(/README\.md$/i, ''); return path.join(DOCS_OUT, dir, 'index.md'); } // Keep original filename casing on disk. Starlight content IDs are // lowercased regardless; sidebar slugs go through wikiRelToSlug(). return path.join(DOCS_OUT, relPosix);}
function rewriteMarkdownLinks(body, fromWikiRel) { const fromDir = path.posix.dirname(fromWikiRel); return body.replace(/\[([^\]]*)\]\(([^)]+)\)/g, (full, text, href) => { const trimmed = href.trim(); if ( !trimmed || trimmed.startsWith('#') || trimmed.startsWith('http://') || trimmed.startsWith('https://') || trimmed.startsWith('mailto:') || trimmed.startsWith('tel:') ) { return full; }
const [pathPartRaw, hash = ''] = trimmed.split('#'); let pathPart = pathPartRaw; if (!pathPart) return full;
pathPart = pathPart.replace(/^<|>$/g, '').split(/\s+/)[0];
let targetWikiRel; if (pathPart.startsWith('/')) { const abs = pathPart.replace(/^\//, ''); if (abs === 'DESIGN.md' || abs === 'DESIGN') { targetWikiRel = 'DESIGN.md'; } else if (abs.startsWith('wiki/')) { targetWikiRel = abs.slice('wiki/'.length); } else { return full; } } else { const joined = path.posix.normalize(path.posix.join(fromDir, pathPart)); if (joined === '../DESIGN.md' || joined === 'DESIGN.md') { targetWikiRel = 'DESIGN.md'; } else if (joined.startsWith('../')) { return full; } else { targetWikiRel = joined; } }
if (!/\.md$/i.test(targetWikiRel)) return full;
const slug = wikiRelToSlug(targetWikiRel); const url = `/${slug}/`; const withHash = hash ? `${url}#${hash}` : url; return `[${text}](${withHash})`; });}
function writeDoc(wikiRel, sourceAbs) { const dest = destFileForWikiRel(wikiRel); if (!dest) return;
let body = fs.readFileSync(sourceAbs, 'utf8'); if (body.startsWith('---\n')) { const end = body.indexOf('\n---\n', 4); if (end !== -1) body = body.slice(end + 5); }
const fallbackTitle = path.basename(wikiRel, '.md'); const title = extractTitle(body, fallbackTitle); body = stripLeadingH1(body); body = rewriteMarkdownLinks(body, wikiRel);
const out = `---\ntitle: ${yamlEscape(title)}\n---\n\n${body.trimStart()}`; ensureDir(path.dirname(dest)); fs.writeFileSync(dest, out, 'utf8');}
function nestInto(items, item, depth) { if (depth <= 0 || items.length === 0) { items.push(item); return; } const prev = items[items.length - 1]; if (!prev.items) { items[items.length - 1] = { label: prev.label, items: [{ label: prev.label, slug: prev.slug }], }; } const parent = items[items.length - 1]; if (depth === 1) { parent.items.push(item); } else { nestInto(parent.items, item, depth - 1); }}
function mapItem(item) { if (item.items) { return { label: item.label, collapsed: true, items: item.items.map(mapItem), }; } const slug = item.slug === '' ? 'index' : item.slug; return { label: item.label, slug };}
/** Parse mdBook SUMMARY.md into Starlight sidebar items. */function parseSummary(summaryText) { const lines = summaryText.split(/\r?\n/); const top = []; let currentSection = null;
const linkRe = /^(\s*)-\s+\[([^\]]+)\]\(([^)]+)\)\s*$/; const bareLinkRe = /^\[([^\]]+)\]\(([^)]+)\)\s*$/; const sectionRe = /^#\s+(.+?)\s*$/;
function pushLink(label, href, indent) { const cleanHref = href.split('#')[0]; if (!cleanHref.endsWith('.md')) return; const slug = wikiRelToSlug(cleanHref); const item = { label, slug };
if (!currentSection) { top.push({ type: 'link', ...item }); return; }
const depth = Math.floor(indent / 2); if (depth <= 1) { currentSection.items.push(item); } else { nestInto(currentSection.items, item, depth - 1); } }
for (const raw of lines) { const line = raw.replace(/\t/g, ' '); if (!line.trim()) continue; if (line.trim().startsWith('Dated session')) break;
const sec = line.match(sectionRe); if (sec) { const label = sec[1].trim(); if (label.toLowerCase() === 'summary') continue; currentSection = { label, items: [] }; top.push({ type: 'section', label, items: currentSection.items }); continue; }
const bare = line.match(bareLinkRe); if (bare && !line.trimStart().startsWith('-')) { pushLink(bare[1], bare[2], 0); continue; }
const m = line.match(linkRe); if (m) pushLink(m[2], m[3], m[1].length); }
const mapped = top.map((entry) => { if (entry.type === 'link') { return { label: entry.label, slug: entry.slug === '' ? 'index' : entry.slug, }; } return { label: entry.label, collapsed: false, items: entry.items.map(mapItem), }; }); // Splash homepage is a custom Astro route, not a docs slug. return [{ label: 'Home', link: '/' }, ...mapped];}
function writeSidebar(sidebar) { ensureDir(GENERATED_OUT); const json = JSON.stringify(sidebar, null, 2); const src = `/** Auto-generated by scripts/sync-wiki.mjs — do not edit. */\nexport const sidebar = ${json} as const;\n`; fs.writeFileSync(SIDEBAR_OUT, src, 'utf8');}
function main() { if (!fs.existsSync(WIKI_ROOT)) { console.error(`wiki root not found: ${WIKI_ROOT}`); process.exit(1); }
rmrf(DOCS_OUT); ensureDir(DOCS_OUT); ensureDir(GENERATED_OUT);
const mdFiles = walkFiles(WIKI_ROOT, (name) => name.endsWith('.md')); let pageCount = 0; for (const abs of mdFiles) { const rel = path.relative(WIKI_ROOT, abs).split(path.sep).join('/'); const real = fs.realpathSync(abs); writeDoc(rel, real); pageCount += 1; }
const assets = walkFiles(WIKI_ROOT, (name) => ASSET_EXT.has(path.extname(name).toLowerCase()), ); let assetCount = 0; for (const abs of assets) { const rel = path.relative(WIKI_ROOT, abs).split(path.sep).join('/'); const dest = path.join(DOCS_OUT, rel); ensureDir(path.dirname(dest)); fs.copyFileSync(fs.realpathSync(abs), dest); assetCount += 1; }
const summary = fs.readFileSync(path.join(WIKI_ROOT, 'SUMMARY.md'), 'utf8'); const sidebar = parseSummary(summary); writeSidebar(sidebar);
console.log( `sync-wiki: wrote ${pageCount} pages + ${assetCount} assets → ${path.relative(REPO_ROOT, DOCS_OUT)}`, ); console.log( `sync-wiki: sidebar → ${path.relative(REPO_ROOT, SIDEBAR_OUT)} (${sidebar.length} top entries)`, );}
main();