import { spawn } from "node:child_process"; import { mkdirSync } from "node:fs"; import { withLock } from "./lock.mjs"; function run(cmd, args, opts = {}) { return new Promise((resolve, reject) => { const child = spawn(cmd, args, { stdio: "inherit", ...opts }); child.on("error", reject); child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${cmd} ${args.join(" ")} exited ${code}`)) ); }); } /** Absolute path of the global build lock. Callers that build outside buildSite() * (e.g. console preview builds) must take THIS same lock to serialize. */ export function buildLockPath(projectRoot) { return `${projectRoot}/agents/state/build.lock`; } /** * Build the Astro app and fix ownership so Apache can serve the output. * Serialized behind the global build lock so it never races the console's * preview/publish builds (concurrent astro builds would OOM this box). */ export async function buildSite({ projectRoot, appDir }) { const lockPath = buildLockPath(projectRoot); mkdirSync(`${projectRoot}/agents/state`, { recursive: true }); await withLock(lockPath, async () => { await run("npm", ["run", "build"], { cwd: `${projectRoot}/${appDir}` }); // Best-effort ownership fix (ignore failure if not running as root). try { await run("chown", ["-R", "www:www", `${projectRoot}/${appDir}`, `${projectRoot}/public`]); } catch (e) { console.warn(`[build] chown skipped: ${e.message}`); } }); }