#!/usr/bin/env node // pack_nedb_to_leveldb.mjs // Przepakowuje kompendia z legacy NeDB (.db, jeden JSON/linia) do formatu LevelDB // (katalog), którego wymaga Foundry VTT v11+ (i v13/v14). Czyta listę paczek // wprost z module.json danego modułu, więc nie trzeba niczego wpisywać ręcznie. // // Wymaga jednorazowo: npm i -D @foundryvtt/foundryvtt-cli (lub -g) // // Użycie: // node help_scripts/pack_nedb_to_leveldb.mjs [--write] [--replace] [ścieżki/do/modułów...] // // (bez argumentów ścieżek) -> domyślnie 3 własne moduły w tym repo // --write faktycznie buduje katalogi LevelDB (bez tego: tylko dry-run/plan) // --replace po udanym zbudowaniu usuwa stary plik .db ORAZ podmienia ścieżkę // w module.json (packs[].path: "packs/foo.db" -> "packs/foo") // // Bezpiecznie: domyślnie nic nie nadpisuje. LevelDB trafia obok, do packs/. // Stare .db zostają, dopóki nie podasz --replace. import { readFile, writeFile, rm, mkdtemp, mkdir } from "node:fs/promises"; import { existsSync, statSync } from "node:fs"; import { tmpdir } from "node:os"; import { fileURLToPath } from "node:url"; import path from "node:path"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const REPO = path.resolve(HERE, ".."); const args = process.argv.slice(2); const WRITE = args.includes("--write"); const REPLACE = args.includes("--replace"); const moduleDirs = args.filter((a) => !a.startsWith("--")); const DEFAULT_MODULES = [ "wg-greyknights", "wg-voidships-builder", "wg-voidships-journal", ].map((m) => path.join(REPO, m)); const targets = (moduleDirs.length ? moduleDirs : DEFAULT_MODULES).map((p) => path.resolve(p) ); // Typ paczki z module.json -> documentType wymagany przez CLI dla źródła NeDB. const TYPE_OK = new Set([ "Actor", "Item", "JournalEntry", "RollTable", "Macro", "Scene", "Playlist", "Cards", "Adventure", ]); async function loadCli() { try { return await import("@foundryvtt/foundryvtt-cli"); } catch { console.error( "\n[!] Brak @foundryvtt/foundryvtt-cli. Zainstaluj jednorazowo:\n" + " npm i -D @foundryvtt/foundryvtt-cli\n" ); process.exit(2); } } async function processModule(cli, modDir) { const manifestPath = path.join(modDir, "module.json"); if (!existsSync(manifestPath)) { console.warn(`- pomijam ${modDir} (brak module.json)`); return; } const manifest = JSON.parse(await readFile(manifestPath, "utf8")); const packs = manifest.packs ?? []; if (!packs.length) { console.log(`- ${manifest.id ?? path.basename(modDir)}: brak paczek`); return; } let manifestDirty = false; console.log(`\n=== ${manifest.id ?? path.basename(modDir)} ===`); for (const pack of packs) { const rel = pack.path; // np. "packs/wg-voidship-hulls.db" LUB "packs/gk-items" const newRel = rel.replace(/\.db$/, ""); // docelowa ścieżka LevelDB (bez .db) const outDir = path.join(modDir, newRel); // Plik NeDB może być tam, gdzie wskazuje manifest (…​.db) albo obok, gdy // manifest podaje ścieżkę bez rozszerzenia, a na dysku i tak leży .db. const nedbAbs = rel.endsWith(".db") ? path.join(modDir, rel) : path.join(modDir, rel + ".db"); if (existsSync(outDir) && statSync(outDir).isDirectory() && !existsSync(nedbAbs)) { console.log(` • ${pack.name}: już LevelDB (${newRel}/) — pomijam`); continue; } if (!existsSync(nedbAbs) || !statSync(nedbAbs).isFile()) { console.warn(` • ${pack.name}: nie znaleziono źródła NeDB (${path.relative(modDir, nedbAbs)}) — pomijam`); continue; } if (rel === newRel) { // manifest już wskazuje bez .db, ale plik to NeDB -> rozbieżność do naprawy console.log(` (uwaga: module.json wskazuje "${rel}", a na dysku jest NeDB "${path.basename(nedbAbs)}")`); } const type = pack.type; if (!TYPE_OK.has(type)) { console.warn(` • ${pack.name}: nieznany type="${type}" — pomijam`); continue; } const abs = nedbAbs; console.log( ` • ${pack.name} [${type}] ${rel} -> ${newRel}/ ${WRITE ? "" : "(dry-run)"}` ); if (!WRITE) continue; // 1) NeDB -> katalog źródeł (.json z poprawnymi _key) const srcTmp = await mkdtemp(path.join(tmpdir(), "fvtt-src-")); await cli.extractPack(abs, srcTmp, { nedb: true, documentType: type, yaml: false, log: false, }); // 2) źródła -> LevelDB await mkdir(outDir, { recursive: true }); await cli.compilePack(srcTmp, outDir, { yaml: false, log: false }); await rm(srcTmp, { recursive: true, force: true }); console.log(` ✓ zbudowano LevelDB: ${newRel}/`); if (REPLACE) { await rm(abs, { force: true }); if (pack.path !== newRel) { pack.path = newRel; manifestDirty = true; } console.log( ` ✓ usunięto ${path.basename(abs)}` + (manifestDirty ? ", zaktualizowano ścieżkę w module.json" : "") ); } } if (manifestDirty && WRITE && REPLACE) { await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n"); } } const cli = WRITE ? await loadCli() : null; // CLI potrzebny tylko do budowania console.log( WRITE ? `Przepakowuję (${REPLACE ? "z podmianą .db i module.json" : "obok, .db zostają"})` : "DRY-RUN — plan (dodaj --write aby zbudować, --replace aby podmienić):" ); for (const t of targets) await processModule(cli, t); console.log("\nGotowe.");