Struttura: il sito esistente si sposta sotto apps/sito
Primo passo della separazione sito / piattaforme decisa da Adriano il 03/09: due applicazioni autosufficienti, senza workspace di root, cosi' che ognuna resti un progetto Astro completo ed estraibile. Qui non cambia niente del codice: solo la radice. I pattern del .gitignore che avevano una barra erano ancorati alla radice e dopo lo spostamento non avrebbero piu' coperto niente (data/*.db*, uploads/*): resi validi a qualunque profondita'. Suite: 45 file, 347 test verdi prima e dopo lo spostamento. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EndnceRA5WnA6rvA5iV9WL
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user