#!/usr/bin/env node /** * sync-campus.mjs — importa le guide di studio del corso Biohacking Campus * nella sezione riservata /campus del sito. * * COSA FA * Legge i file `.studio.md` dal progetto sorgente, li converte in * frammenti HTML e produce in `campus-content/`: * - `nav.json` → aree, capitoli, guide (ordine, titoli, prev/next) * - `pages/.html` → corpo della guida * - `assets/` → immagini delle slide, copiate dal sorgente * Le pagine sono renderizzate a runtime da src/pages/campus/ con lo stile * del sito InsanityLab; qui non si genera alcun layout. * * USO * npm run sync-campus [-- ] * Default: /home/adriano/Wasabi-Adp-Work/AI-OS/projects/personale/biohacking-campus * * IDEMPOTENTE * Rigenera nav.json e le pagine, rimuovendo i frammenti orfani. Gli assets * sono copiati solo se mancanti o più vecchi del sorgente. */ import { readFile, writeFile, mkdir, readdir, rm, copyFile, stat } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { join, dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { CHAPTERS, CH_COLORS, AREAS, sourceDirFor } from './campus/structure.mjs'; import { parseGuide } from './campus/parse.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); const OUT_DIR = resolve(HERE, '..', 'campus-content'); const DEFAULT_SRC = '/home/adriano/Wasabi-Adp-Work/AI-OS/projects/personale/biohacking-campus'; async function copyTree(src, dest) { await mkdir(dest, { recursive: true }); let copied = 0; for (const entry of await readdir(src, { withFileTypes: true })) { const from = join(src, entry.name); const to = join(dest, entry.name); if (entry.isDirectory()) { copied += await copyTree(from, to); continue; } const [s, d] = [await stat(from), existsSync(to) ? await stat(to) : null]; if (!d || d.mtimeMs < s.mtimeMs || d.size !== s.size) { await copyFile(from, to); copied++; } } return copied; } async function main() { const srcRoot = resolve(process.argv[2] ?? DEFAULT_SRC); if (!existsSync(srcRoot)) { console.error(`Progetto sorgente non trovato: ${srcRoot}`); process.exit(1); } const pagesDir = join(OUT_DIR, 'pages'); await mkdir(pagesDir, { recursive: true }); // 1. parsing di tutte le guide, capitolo per capitolo const chapters = []; const written = new Set(); for (const [num, title, sub, slugs] of CHAPTERS) { const lessons = []; for (const [i, slug] of slugs.entries()) { const file = join(srcRoot, sourceDirFor(slug), `${slug}.studio.md`); if (!existsSync(file)) { console.warn(` guida mancante, salto: ${slug}`); continue; } const g = parseGuide(await readFile(file, 'utf8'), slug, i + 1); await writeFile(join(pagesDir, `${slug}.html`), g.html); written.add(`${slug}.html`); const entry = { slug: g.slug, kind: g.kind, label: g.label, titleFull: g.titleFull, titleShort: g.titleShort, relatore: g.relatore, concetti: g.concetti.slice(0, 6), chapter: num, }; lessons.push(entry); } chapters.push({ num, title, sub, color: CH_COLORS[num - 1], lessons }); } const byNum = new Map(chapters.map((c) => [c.num, c])); const areas = AREAS.map(([slug, title, sub, color, chapterNums]) => ({ slug, title, sub, color, chapters: chapterNums.map((n) => byNum.get(n)).filter(Boolean), })); // 2. prev/next nell'ordine in cui l'utente naviga: aree → capitoli → guide, // che non coincide con l'ordine dei capitoli (le aree li raggruppano). const flat = []; for (const area of areas) for (const ch of area.chapters) for (const l of ch.lessons) { l.area = area.slug; flat.push(l); } const linkTo = (l) => (l ? { slug: l.slug, area: l.area, title: l.titleShort } : null); flat.forEach((l, i) => { l.prev = linkTo(flat[i - 1]); l.next = linkTo(flat[i + 1]); }); await writeFile(join(OUT_DIR, 'nav.json'), JSON.stringify({ areas, count: flat.length }, null, 2)); // 3. rimozione dei frammenti orfani let removed = 0; for (const f of await readdir(pagesDir)) { if (!written.has(f)) { await rm(join(pagesDir, f)); removed++; } } // 4. assets delle slide const assetsSrc = join(srcRoot, 'sito', 'assets'); const copied = existsSync(assetsSrc) ? await copyTree(assetsSrc, join(OUT_DIR, 'assets')) : 0; console.log(`Campus: ${flat.length} guide, ${areas.length} aree, ${copied} asset copiati, ${removed} pagine orfane rimosse.`); } await main();