Files
InsanityLab-web/scripts/sync-campus.mjs
T
Adriano 5590a552cc Campus: allineate le 49 Q&A mancanti e corretto il percorso del sorgente
Il progetto Biohacking Campus è stato spostato in Wasabi-Adp-Work, mentre
sync-campus puntava ancora al vecchio percorso, ormai vuoto: da lì in poi
nessun aggiornamento è più arrivato. Il capitolo Q&A si era fermato alla
numero 35, mentre nel progetto ne esistono 84.

Ora la sezione conta 167 guide invece di 118. L'elenco dei Q&A è
rigenerato per intero dal filesystem, in ordine di numero, così il prossimo
allineamento è una sola riga di script.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 16:53:57 +02:00

121 lines
4.5 KiB
JavaScript

#!/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 `<slug>.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/<slug>.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 [-- <path-progetto-biohacking-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();