import { readdirSync, readFileSync, writeFileSync } from "node:fs"; import { relative, dirname, join, sep } from "node:path"; import { randomBytes } from "node:crypto"; const arg = (k, d) => { const i = process.argv.indexOf(k); return i > -1 ? process.argv[i + 1] : d; }; const SRC = arg("--src", "content"); const TARGET = arg("--target", "_manifest.js"); // --- uuid v7: 48-bit ms timestamp (time-ordered) + version/variant + random --- function uuidv7(){ const ts = Date.now(); const b = randomBytes(16); b[0]=(ts/2**40)&0xff; b[1]=(ts/2**32)&0xff; b[2]=(ts/2**24)&0xff; b[3]=(ts/2**16)&0xff; b[4]=(ts/2**8)&0xff; b[5]=ts&0xff; b[6]=(b[6]&0x0f)|0x70; // version 7 b[8]=(b[8]&0x3f)|0x80; // variant 10 const h = b.toString("hex"); return `${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20)}`; } const ID_RE = //i; const idLine = id => ``; const files = readdirSync(SRC).filter(f => f.endsWith(".md")).sort(); // --- discovery: every card gets a stable v7 id. mint if absent (incl. user-removed), // remint on collision (bootstrap audit). writes the id into the card at EOF. --- const seen = new Set(); const idOf = {}; let minted = 0, reminted = 0; for (const f of files) { const p = join(SRC, f); let md = readFileSync(p, "utf8"); const m = md.match(ID_RE); let id = m ? m[1].toLowerCase() : null; if (!id) { // absent → mint, append at bottom id = uuidv7(); md = md.replace(/\s*$/, "") + `\n\n${idLine(id)}\n`; writeFileSync(p, md); minted++; } else if (seen.has(id)) { // collision → remint in place id = uuidv7(); md = md.replace(ID_RE, idLine(id)); writeFileSync(p, md); reminted++; } seen.add(id); idOf[f] = id; } // --- manifest --- const parseTags = md => ((md.match(/^---\n([\s\S]*?)\n---/)?.[1] .match(/tags:\s*\[([^\]]*)\]/)?.[1]) || "") .split(",").map(s => s.trim()).filter(Boolean); const allTags = new Set(); for (const f of files) parseTags(readFileSync(join(SRC, f), "utf8")).forEach(t => allTags.add(t)); const spec = f => { let r = relative(dirname(TARGET) || ".", join(SRC, f)).split(sep).join("/"); return r.startsWith(".") ? r : "./" + r; }; const imports = files.map((f, i) => `import c${i} from ${JSON.stringify(spec(f))};`).join("\n"); const items = files.map((f, i) => ` { slug: ${JSON.stringify(f.replace(/\.md$/,""))}, id: ${JSON.stringify(idOf[f])}, md: c${i} }`).join(",\n"); const tags = [...allTags].sort(); writeFileSync(TARGET, `// AUTO-GENERATED by gen.mjs — do not edit ${imports} export const CARDS = [ ${items} ]; // tag vocabulary, derived at build (not boot) export const TAGS = ${JSON.stringify(tags)}; `); console.log(`gen: ${files.length} card(s), ${tags.length} tag(s), +${minted} id(s), ${reminted} reminted -> ${TARGET}`);