import { spawn } from "node:child_process"; import net from "node:net"; import { fileURLToPath } from "node:url"; import path from "node:path"; const scriptsDir = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.resolve(scriptsDir, ".."); const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm"; function canListen(port) { return new Promise((resolve) => { const connection = net.createConnection({ port, host: "127.0.0.1" }); connection.once("connect", () => { connection.destroy(); resolve(false); }); connection.once("error", () => { connection.destroy(); const probe = net.createServer(); probe.once("error", () => resolve(false)); probe.listen(port, "0.0.0.0", () => probe.close(() => resolve(true))); }); }); } const reservedPorts = new Set(); async function freePort(start) { for (let port = start; port < start + 100; port += 1) { if (reservedPorts.has(port)) continue; if (await canListen(port)) { reservedPorts.add(port); return port; } } throw new Error(`No free port near ${start}`); } const serverPort = await freePort(Number(process.env.MIRAGENFLOW_SERVER_PORT || 3100)); const webPort = await freePort(Number(process.env.MIRAGENFLOW_WEB_PORT || 3000)); const adminPort = await freePort(Number(process.env.MIRAGENFLOW_ADMIN_PORT || 3003)); // Keep the local preview account/session state across server restarts. Without // an explicit persistence setting the server defaults to an in-memory store, // which makes a user appear to have the wrong password after Vite/tsx reloads // the process. Production deployments can override this with PostgreSQL (or // an explicit MIRAGENFLOW_STORE_FILE). const previewStoreFile = process.env.MIRAGENFLOW_STORE_FILE?.trim() || path.join("/tmp", "miragenflow-dev", "store.json"); const commonEnv = { ...process.env, MIRAGENFLOW_SERVER_PORT: String(serverPort), MIRAGENFLOW_ADMIN_PORT: String(adminPort), MIRAGENFLOW_STORE_FILE: previewStoreFile, MIRAGENFLOW_ADMIN_EMAIL: process.env.MIRAGENFLOW_ADMIN_EMAIL?.trim() || "admin@admin.com", MIRAGENFLOW_ADMIN_PASSWORD: process.env.MIRAGENFLOW_ADMIN_PASSWORD || "admin", MIRAGENFLOW_ADMIN_MFA_REQUIRED: process.env.MIRAGENFLOW_ADMIN_MFA_REQUIRED ?? "false", MIRAGENFLOW_ADMIN_CAPTCHA_REQUIRED: process.env.MIRAGENFLOW_ADMIN_CAPTCHA_REQUIRED ?? "false", MIRAGENFLOW_ADMIN_SELF_APPROVAL: process.env.NODE_ENV === "production" ? "false" : process.env.MIRAGENFLOW_ADMIN_SELF_APPROVAL ?? "true", // Keep the local preview clean by default; opt into showcase records explicitly. MIRAGENFLOW_DEMO_DATA: process.env.MIRAGENFLOW_DEMO_DATA ?? "false", MIRAGENFLOW_USER_MFA_REQUIRED: process.env.MIRAGENFLOW_USER_MFA_REQUIRED ?? "false", MIRAGENFLOW_USER_VERIFICATION_REQUIRED: process.env.MIRAGENFLOW_USER_VERIFICATION_REQUIRED ?? "false", MIRAGENFLOW_CAPTCHA_REQUIRED: process.env.MIRAGENFLOW_CAPTCHA_REQUIRED ?? "false", // Local provider emulators commonly run on localhost or a private LAN. // Production is forced closed; non-production can opt out explicitly. MIRAGENFLOW_ALLOW_LOCAL_PROVIDER_URLS: process.env.NODE_ENV === "production" ? "false" : process.env.MIRAGENFLOW_ALLOW_LOCAL_PROVIDER_URLS ?? "true", }; const processes = [ spawn(npmCommand, ["run", "dev"], { cwd: path.join(projectRoot, "server"), env: { ...commonEnv, MIRAGENFLOW_PORT: String(serverPort) }, stdio: "inherit" }), spawn(npmCommand, ["run", "dev", "--", "--port", String(webPort)], { cwd: path.join(projectRoot, "web"), env: commonEnv, stdio: "inherit" }), spawn(npmCommand, ["run", "dev:linux", "--", "--force", "--port", String(adminPort)], { cwd: path.join(projectRoot, "admin"), env: commonEnv, stdio: "inherit" }), ]; console.log(`MiragenFlow dev origin: http://localhost:${webPort}/`); console.log(`Admin path: http://localhost:${webPort}/admin/ (admin Vite ${adminPort})`); console.log(`API gateway: http://localhost:${webPort}/api/ (server ${serverPort})`); let stopping = false; function stopAll(exitCode = 0) { if (stopping) return; stopping = true; for (const child of processes) if (!child.killed) child.kill("SIGTERM"); setTimeout(() => process.exit(exitCode), 100); } for (const child of processes) child.on("exit", (code, signal) => { if (stopping) return; stopAll(typeof code === "number" && code !== 0 ? code : signal ? 1 : 0); }); process.on("SIGINT", () => stopAll(0)); process.on("SIGTERM", () => stopAll(0));