#!/usr/bin/env node // scripts/launch.mjs — cross-platform "prepare + start + open the browser", used by scripts/start-windows.bat, // scripts/start-windows.ps1 and scripts/start.sh (docs/DEPLOY.md). // // node scripts/launch.mjs [--port 3000] [--host 0.0.0.0] [--no-open] [--no-setup] [setup options…] // // 1. If our server already answers on the port, just open the browser (double-clicking twice is harmless). // 2. node tools/setup.mjs --quiet (dependencies, vendor libs, art download / resume, optional local extraction); // setup options such as --no-assets, --no-local, --local, --game , -y are passed through. // 3. node server/index.js (PORT / HOST from the options or the environment; SP_COMBAT / SP_VERIFY / TRUST_PROXY / // DEBUG are inherited), then — once /healthz answers — prints the addresses to share and opens // http://localhost: (not with --no-open, SP_NO_BROWSER=1, or on a Linux box without a display). // Ctrl+C stops the server (it gets the signal from the terminal itself); the exit code is the server's. import { spawn, spawnSync } from 'node:child_process'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import fs from 'node:fs'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const IS_WIN = process.platform === 'win32'; if (Number(process.versions.node.split('.')[0]) < 18) { console.error(`Node.js ${process.versions.node} 太旧,需要 18 或更高(推荐 22 LTS):https://nodejs.org/zh-cn/download`); process.exit(1); } const { c, mark } = await import('../tools/setup.mjs'); const { probePort, classifyAddresses, KIND_LABEL } = await import('../tools/doctor.mjs'); function parseArgs(argv) { const o = { port: Number(process.env.PORT) || 3000, host: process.env.HOST || '0.0.0.0', open: !/^(1|true|yes)$/i.test(process.env.SP_NO_BROWSER || ''), setup: true, setupArgs: [], help: false }; for (let i = 0; i < argv.length; i++) { const a = argv[i]; const [k, v] = a.split('='); const val = () => (v !== undefined ? v : argv[++i]); if (k === '--port') o.port = Number(val()) || o.port; else if (k === '--host') o.host = val() || o.host; else if (a === '--no-open') o.open = false; else if (a === '--no-setup') o.setup = false; else if (a === '-h' || a === '--help') o.help = true; else if (a === '--game') o.setupArgs.push(a, argv[++i] ?? ''); else o.setupArgs.push(a); } return o; } function openBrowser(url) { try { let cmd; let args; if (IS_WIN) { cmd = 'rundll32'; args = ['url.dll,FileProtocolHandler', url]; } else if (process.platform === 'darwin') { cmd = 'open'; args = [url]; } else { if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false; cmd = 'xdg-open'; args = [url]; } const child = spawn(cmd, args, { stdio: 'ignore', detached: true, windowsHide: true }); child.on('error', () => {}); child.unref(); return true; } catch { return false; } } function printShare(port) { const addrs = classifyAddresses().filter((a) => a.kind === 'lan' || a.kind === 'vpn' || a.kind === 'public'); const line = c.dim('─'.repeat(56)); console.log(`\n${line}`); console.log(`${mark.ok} ${c.bold('服务器已启动')} 本机打开:${c.cyan(`http://localhost:${port}`)}`); if (addrs.length) { console.log(' 发给朋友(需要能访问这台电脑的网络):'); for (const a of addrs.slice(0, 4)) console.log(` ${c.cyan(`http://${a.address}:${port}`)} ${c.dim(KIND_LABEL[a.kind])}`); } else { console.log(c.warn(' 没有检测到局域网地址:朋友暂时无法连接(检查网线/Wi-Fi)。')); } console.log(c.dim(' 建房后把 4 位「同盟密钥」或「复制链接」(…/?room=密钥)发给朋友。')); console.log(c.dim(' 朋友打不开?运行 node tools/doctor.mjs 检查防火墙。按 Ctrl+C 停止服务器。')); console.log(`${line}\n`); } async function waitHealthy(port, child, timeoutMs = 30000) { const until = Date.now() + timeoutMs; while (Date.now() < until && child.exitCode === null) { const p = await probePort(port); if (p.state === 'ours') return true; await new Promise((r) => setTimeout(r, 300)); } return false; } async function main() { const o = parseArgs(process.argv.slice(2)); if (o.help) { const src = fs.readFileSync(fileURLToPath(import.meta.url), 'utf8').split('\n'); console.log(src.slice(1, 13).map((l) => l.replace(/^\/\/ ?/, '')).join('\n')); return 0; } const localUrl = `http://localhost:${o.port}`; const before = await probePort(o.port, o.host); if (before.state === 'ours') { console.log(`${mark.ok} 服务器已经在运行(端口 ${o.port}),直接打开浏览器。`); printShare(o.port); if (o.open) openBrowser(localUrl); return 0; } if (before.state !== 'free') { console.error(`${mark.err} 端口 ${o.port} 被其他程序占用或无权限(${before.code || before.state})。`); console.error(` 换一个端口:${IS_WIN ? 'scripts\\start-windows.bat --port 3001' : 'scripts/start.sh --port 3001'}`); return 1; } if (o.setup) { const r = spawnSync(process.execPath, [path.join(ROOT, 'tools', 'setup.mjs'), '--quiet', ...o.setupArgs], { cwd: ROOT, stdio: 'inherit' }); if (r.status !== 0) { console.error(`${mark.err} 准备步骤失败(见上方)。`); return r.status || 1; } } const env = { ...process.env, PORT: String(o.port), HOST: o.host }; const child = spawn(process.execPath, [path.join(ROOT, 'server', 'index.js')], { cwd: ROOT, env, stdio: 'inherit' }); const forward = (sig) => { if (child.exitCode === null) { try { child.kill(sig); } catch { /* gone */ } } }; // SIGINT reaches the server straight from the terminal (same process group / console); forwarding it too would make // the server's second-signal path force-exit. Other signals (service managers, `kill`) are forwarded. process.on('SIGINT', () => {}); for (const sig of ['SIGTERM', 'SIGHUP', 'SIGBREAK']) { try { process.on(sig, () => forward(sig === 'SIGBREAK' ? 'SIGTERM' : sig)); } catch { /* unsupported here */ } } const exited = new Promise((resolve) => child.on('exit', (code, signal) => resolve(code ?? (signal ? 0 : 1)))); if (await waitHealthy(o.port, child)) { printShare(o.port); if (o.open && !openBrowser(localUrl)) console.log(c.dim(`(未能自动打开浏览器,请手动访问 ${localUrl})`)); } return exited; } main().then((code) => { process.exitCode = code; }, (e) => { console.error(e?.stack || e); process.exitCode = 1; });