From 89d92d8ae07f748c1719f64773fb034ad3b69ebe Mon Sep 17 00:00:00 2001 From: pftom <1043269994@qq.com> Date: Tue, 28 Apr 2026 22:19:42 +0800 Subject: [PATCH] feat(dev): auto-switch ports on dev:all when defaults are busy Adds a small launcher (scripts/dev-all.mjs) that probes free ports for the daemon (OD_PORT, default 7456) and Vite (VITE_PORT, default 5173) before invoking concurrently, so a stray process holding either port no longer breaks the boot. The resolved ports are exported into the child env; vite.config.ts now reads VITE_PORT to keep its dev server and /api proxy aligned with the daemon's actual port. Made-with: Cursor --- package.json | 2 +- scripts/dev-all.mjs | 84 +++++++++++++++++++++++++++++++++++++++++++++ vite.config.ts | 3 +- 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100755 scripts/dev-all.mjs diff --git a/package.json b/package.json index 9febc36..82846f0 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "scripts": { "daemon": "node daemon/cli.js --no-open", "dev": "vite", - "dev:all": "concurrently -k -n daemon,web -c cyan,magenta \"npm:daemon\" \"npm:dev\"", + "dev:all": "node scripts/dev-all.mjs", "build": "tsc -b && vite build", "preview": "vite preview", "typecheck": "tsc -b --noEmit", diff --git a/scripts/dev-all.mjs b/scripts/dev-all.mjs new file mode 100755 index 0000000..32b4076 --- /dev/null +++ b/scripts/dev-all.mjs @@ -0,0 +1,84 @@ +#!/usr/bin/env node +// Launcher for `npm run dev:all`. +// +// Probes for free ports for the daemon (OD_PORT, default 7456) and the Vite +// dev server (VITE_PORT, default 5173) before spawning `concurrently`, so a +// stray process holding either port doesn't kill the whole boot. The +// resolved ports are exported into the child env, which means: +// * the daemon's cli.js sees the new OD_PORT and binds to it +// * vite.config.ts reads the same OD_PORT and points its /api proxy at +// the daemon's actual port +// * Vite itself binds to VITE_PORT +// +// If a port is busy we walk forward up to PORT_SEARCH_RANGE steps and log +// the switch so the user notices. + +import { spawn } from 'node:child_process'; +import net from 'node:net'; + +const HOST = '127.0.0.1'; +const PORT_SEARCH_RANGE = 50; + +function isPortFree(port, host = HOST) { + return new Promise((resolve) => { + const server = net.createServer(); + server.unref(); + server.once('error', () => resolve(false)); + server.listen({ port, host, exclusive: true }, () => { + server.close(() => resolve(true)); + }); + }); +} + +async function findFreePort(start, label) { + for (let port = start; port < start + PORT_SEARCH_RANGE; port++) { + if (await isPortFree(port)) return port; + } + throw new Error( + `[dev:all] could not find a free ${label} port near ${start} (tried ${PORT_SEARCH_RANGE})`, + ); +} + +const desiredDaemon = Number(process.env.OD_PORT) || 7456; +const desiredVite = Number(process.env.VITE_PORT) || 5173; + +const daemonPort = await findFreePort(desiredDaemon, 'daemon'); +const vitePort = await findFreePort(desiredVite, 'vite'); + +if (daemonPort !== desiredDaemon) { + console.log( + `[dev:all] daemon port ${desiredDaemon} is busy, switching to ${daemonPort}`, + ); +} +if (vitePort !== desiredVite) { + console.log( + `[dev:all] vite port ${desiredVite} is busy, switching to ${vitePort}`, + ); +} + +const env = { + ...process.env, + OD_PORT: String(daemonPort), + VITE_PORT: String(vitePort), +}; + +// We spawn the local `concurrently` bin via shell so Windows .cmd shims +// resolve correctly. The `npm:daemon` / `npm:dev` shorthand runs the +// matching package.json scripts, so any future tweak to those scripts is +// picked up automatically. +const child = spawn( + 'concurrently', + ['-k', '-n', 'daemon,web', '-c', 'cyan,magenta', 'npm:daemon', 'npm:dev'], + { env, stdio: 'inherit', shell: true }, +); + +child.on('exit', (code, signal) => { + if (signal) process.kill(process.pid, signal); + else process.exit(code ?? 0); +}); + +for (const sig of ['SIGINT', 'SIGTERM']) { + process.on(sig, () => { + if (!child.killed) child.kill(sig); + }); +} diff --git a/vite.config.ts b/vite.config.ts index 00a3fa1..8547202 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,11 +2,12 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; const DAEMON_PORT = Number(process.env.OD_PORT) || 7456; +const VITE_PORT = Number(process.env.VITE_PORT) || 5173; export default defineConfig({ plugins: [react()], server: { - port: 5173, + port: VITE_PORT, proxy: { '/api': { target: `http://127.0.0.1:${DAEMON_PORT}`,