import { cp, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises"; import { dirname, join, relative, resolve } from "node:path"; const root = resolve(new URL("..", import.meta.url).pathname); const runtimeDir = process.env.FOUNDRY_RUNTIME_DIR ? resolve(root, process.env.FOUNDRY_RUNTIME_DIR) : join(root, "runtime-data"); const seedDir = resolve(process.env.FOUNDRY_RUNTIME_SEED_DIR || join(root, "runtime-seed")); const manifestPath = join(seedDir, "seed-manifest.json"); function isInside(parent, target) { const path = relative(parent, target); return path && !path.startsWith("..") && !path.includes("../"); } async function exists(path) { try { await stat(path); return true; } catch (error) { if (error?.code === "ENOENT") return false; throw error; } } async function copySeedFile(file) { const source = resolve(seedDir, file); const target = resolve(runtimeDir, file); if (!isInside(seedDir, source) || !isInside(runtimeDir, target)) throw new Error("runtime_seed_path_invalid"); if (!await exists(source)) throw new Error(`runtime_seed_file_missing:${file}`); await mkdir(dirname(target), { recursive: true }); await cp(source, target, { force: true, preserveTimestamps: true }); } async function isGeneratedFallback() { const profilePath = join(runtimeDir, "design-profiles", "default.json"); if (!await exists(profilePath)) return true; try { const profile = JSON.parse(await readFile(profilePath, "utf8")); // The server fallback has the same default media paths as the canonical // profile, but it has never been persisted as a real Design Profile // layout. `savedAt` is written by the baseline/profile save path. A // subsequently edited user profile must never be replaced implicitly. return profile?.id === "default" && profile?.name === "NODE.DC Default" && profile?.status === "draft" && !String(profile?.layout?.savedAt || "").trim() && String(profile?.timestamps?.createdAt || "") === String(profile?.timestamps?.updatedAt || ""); } catch { return false; } } const manifest = JSON.parse(await readFile(manifestPath, "utf8")); if (!/^\d+\.\d+\.\d+$/.test(String(manifest.version || ""))) throw new Error("runtime_seed_version_invalid"); if (!Array.isArray(manifest.files) || !manifest.files.every((file) => typeof file === "string" && file.length > 0)) { throw new Error("runtime_seed_manifest_invalid"); } const marker = join(runtimeDir, "runtime-seed", `${manifest.version}.json`); if (!await exists(marker)) { const shouldApply = await isGeneratedFallback(); if (shouldApply) for (const file of manifest.files) await copySeedFile(file); await mkdir(dirname(marker), { recursive: true }); const temporaryMarker = `${marker}.tmp`; await writeFile(temporaryMarker, `${JSON.stringify({ version: manifest.version, appliedAt: new Date().toISOString(), applied: shouldApply }, null, 2)}\n`, "utf8"); await rename(temporaryMarker, marker); console.log(`Foundry runtime seed ${manifest.version}: ${shouldApply ? "applied" : "preserved existing runtime"}`); }