feat: integrate platform backend and application interfaces
This commit is contained in:
+76
-21
@@ -1,4 +1,5 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import net from "node:net";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -6,29 +7,83 @@ const scriptsDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = path.resolve(scriptsDir, "..");
|
||||
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
||||
|
||||
const processes = [
|
||||
spawn(npmCommand, ["run", "dev"], { cwd: path.join(projectRoot, "web"), stdio: "inherit" }),
|
||||
spawn(npmCommand, ["run", "dev:linux", "--", "--force"], { cwd: path.join(projectRoot, "admin"), stdio: "inherit" }),
|
||||
];
|
||||
|
||||
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;
|
||||
const exitCode = typeof code === "number" && code !== 0 ? code : signal ? 1 : 0;
|
||||
stopAll(exitCode);
|
||||
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));
|
||||
|
||||
Reference in New Issue
Block a user