/** * Dev environment startup script. * * Starts the local Postgres container, waits for it to be ready, runs * migrations + seed, then spawns `next dev` bound to 0.0.0.0 so the app * is reachable from other devices on the LAN. * * Usage: pnpm dev:local */ import { execSync, spawn } from "child_process"; import net from "net"; import os from "os"; import { fileURLToPath } from "url"; /** Return the first LAN IPv4 address (prefers 192.168.x / 10.x ranges). */ function getLanIp() { const all = Object.values(os.networkInterfaces()) .flat() .filter((n) => n.family === "IPv4" && !n.internal) .map((n) => n.address); return all.find((a) => a.startsWith("192.168.") || a.startsWith("10.")) ?? all[0] ?? "unknown"; } /** Poll TCP host:port until it accepts a connection or the timeout expires. */ function waitForTcp(host, port, timeoutMs = 30_000) { return new Promise((resolve, reject) => { const deadline = Date.now() + timeoutMs; function attempt() { const sock = net.createConnection({ host, port }); sock.on("connect", () => { sock.destroy(); resolve(); }); sock.on("error", () => { sock.destroy(); if (Date.now() >= deadline) { reject( new Error( `Postgres not ready on ${host}:${port} after ${timeoutMs / 1000}s. ` + "Is Docker running? Check: docker compose -f docker-compose.dev.yaml logs", ), ); } else { setTimeout(attempt, 500); } }); } attempt(); }); } /** Run a shell command synchronously, streaming output to the terminal. */ function run(cmd) { execSync(cmd, { stdio: "inherit", shell: true }); } export async function main() { // 1. Start DB container (idempotent — safe to call when already running) console.log("\n▸ Starting database container..."); run("docker compose -f docker-compose.dev.yaml up -d"); // 2. Wait for Postgres to accept connections process.stdout.write("▸ Waiting for Postgres"); const tick = setInterval(() => process.stdout.write("."), 500); try { await waitForTcp("127.0.0.1", 5432); } finally { clearInterval(tick); process.stdout.write(" ready\n"); } // 3. Apply any pending migrations (no-op if already current) console.log("▸ Running migrations..."); run("pnpm db:migrate"); // 4. Seed default data (idempotent) console.log("▸ Seeding..."); run("pnpm db:seed"); // 5. Print access URLs before the Next.js banner appears const lanIp = getLanIp(); console.log("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); console.log(` Local → http://localhost:3000`); console.log(` LAN → http://${lanIp}:3000 (phone on same WiFi)`); console.log(` HTTPS → https://dev.ginnoir.com (push/PWA — needs Caddy snippet)`); console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); // 6. Spawn Next.js dev server bound to all interfaces. // HMR handles most code changes without a restart. // Restart needed for: .env changes, next.config.ts changes. // Pass command as a single string with no args array — shell handles .cmd // resolution on Windows and the empty args avoids DEP0190. const next = spawn("pnpm run dev:network", [], { stdio: "inherit", shell: true }); // Forward Ctrl+C / SIGTERM to the child so it shuts down cleanly. const forward = (sig) => () => next.kill(sig); process.on("SIGINT", forward("SIGINT")); process.on("SIGTERM", forward("SIGTERM")); await new Promise((resolve) => next.on("exit", (code) => resolve(code))); process.exit(0); } // Run when invoked directly (not imported by dev-reset.mjs) const __filename = fileURLToPath(import.meta.url); if (process.argv[1] === __filename) { main().catch((err) => { console.error("\ndev startup failed:", err.message); process.exit(1); }); }