#!/usr/bin/env node
* Build workspace packages and start API + web dev servers (Linux, Windows, macOS).
*
* Usage:
* npm run dev # start both
* npm run dev -- --api # API only
* npm run dev -- --web # web only
*/
import { spawn, execSync } from "node:child_process";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const args = process.argv.slice(2);
const apiOnly = args.includes("--api");
const webOnly = args.includes("--web");
const startApi = !webOnly || apiOnly;
const startWeb = !apiOnly || webOnly;
execSync("node scripts/ensure-dev-environment.mjs", {
cwd: root,
stdio: "inherit",
env: process.env,
shell: true,
});
for (const port of [8787, 3000]) {
try {
freePort(port);
} catch {
}
}
const children = [];
if (startApi) {
children.push(spawnNpm(["--workspace", "@datafoundry/api", "run", "dev"]));
}
if (startWeb) {
children.push(spawnNpm(["--workspace", "@datafoundry/web", "run", "dev"]));
}
if (children.length === 0) {
console.error("Nothing to start. Use --api and/or --web.");
process.exit(1);
}
console.log(
"\n[dev] " +
(startApi ? "API → http://127.0.0.1:8787 " : "") +
(startWeb ? "Web → http://localhost:3000/data-tasks" : "") +
"\n",
);
function shutdown(signal) {
for (const child of children) {
if (!child.killed) child.kill(signal);
}
}
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
for (const child of children) {
child.on("exit", (code, signal) => {
if (signal) return;
if (code && code !== 0) {
shutdown("SIGTERM");
process.exit(code);
}
});
}
function spawnNpm(args) {
return spawn("npm", args, {
cwd: root,
stdio: "inherit",
env: process.env,
shell: true,
});
}
function freePort(port) {
if (process.platform === "win32") {
let output = "";
try {
output = execSync(`netstat -ano | findstr :${port}`, {
encoding: "utf8",
shell: true,
stdio: ["ignore", "pipe", "ignore"],
});
} catch {
return;
}
const pids = new Set();
for (const line of output.split(/\r?\n/u)) {
if (!/\bLISTENING\b/u.test(line)) continue;
const pid = line.trim().split(/\s+/u).at(-1);
if (pid && /^\d+$/u.test(pid) && pid !== "0") {
pids.add(pid);
}
}
for (const pid of pids) {
execSync(`taskkill /F /PID ${pid}`, { stdio: "ignore", shell: true });
}
return;
}
execSync(`fuser -k ${port}/tcp 2>/dev/null || true`, {
cwd: root,
stdio: "ignore",
shell: true,
});
}