responses-proxy 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -1,76 +1,307 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * responses-proxy CLI — starts the proxy server and opens the dashboard.
3
+ * responses-proxy CLI — full command interface for managing the proxy.
4
4
  *
5
5
  * Usage:
6
- * npx responses-proxy
7
- * responses-proxy --port 8318
8
- * responses-proxy --no-browser
6
+ * responses-proxy # Start foreground, open browser
7
+ * responses-proxy --background # Start as background daemon
8
+ * responses-proxy stop # Stop daemon
9
+ * responses-proxy status # Check running state
10
+ * responses-proxy restart # Restart daemon
11
+ * responses-proxy logs # Tail server logs
12
+ * responses-proxy config # Show current config
13
+ * responses-proxy config set KEY VAL # Set env config
14
+ * responses-proxy setup # Interactive first-time setup
15
+ * responses-proxy password <pass> # Set dashboard password
16
+ * responses-proxy info # Show connection details (endpoint + key)
9
17
  */
10
18
 
11
- const { spawn, exec } = require("child_process");
19
+ const { spawn, exec, execSync } = require("child_process");
12
20
  const path = require("path");
13
21
  const fs = require("fs");
14
22
  const os = require("os");
23
+ const readline = require("readline");
15
24
 
16
25
  const pkg = require("./package.json");
17
26
  const args = process.argv.slice(2);
18
27
 
19
- // Defaults
28
+ // ─── Paths ───────────────────────────────────────────────────────────────────
29
+
30
+ const dataDir = path.join(os.homedir(), ".responses-proxy");
31
+ fs.mkdirSync(dataDir, { recursive: true });
32
+ const PID_FILE = path.join(dataDir, "server.pid");
33
+ const LOG_FILE = path.join(dataDir, "server.log");
34
+ const ENV_FILE = path.join(dataDir, "config.env");
35
+ const serverPath = path.join(__dirname, "dist", "server.js");
36
+ const nodeModulesPath = path.join(__dirname, "dist", "node_modules");
37
+
38
+ // ─── Config ──────────────────────────────────────────────────────────────────
39
+
40
+ function loadEnvConfig() {
41
+ if (!fs.existsSync(ENV_FILE)) return {};
42
+ const lines = fs.readFileSync(ENV_FILE, "utf8").split("\n");
43
+ const env = {};
44
+ for (const line of lines) {
45
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
46
+ if (match) env[match[1]] = match[2];
47
+ }
48
+ return env;
49
+ }
50
+
51
+ function saveEnvConfig(env) {
52
+ const lines = Object.entries(env).map(([k, v]) => `${k}=${v}`);
53
+ fs.writeFileSync(ENV_FILE, lines.join("\n") + "\n");
54
+ }
55
+
56
+ function getConfig(key) {
57
+ const env = loadEnvConfig();
58
+ return env[key] || process.env[key] || "";
59
+ }
60
+
61
+ function setConfig(key, value) {
62
+ const env = loadEnvConfig();
63
+ env[key] = value;
64
+ saveEnvConfig(env);
65
+ }
66
+
67
+ // ─── Parse args ──────────────────────────────────────────────────────────────
68
+
20
69
  const DEFAULT_PORT = 8318;
21
- let port = DEFAULT_PORT;
22
- let host = "0.0.0.0";
70
+ let port = parseInt(getConfig("PORT")) || DEFAULT_PORT;
71
+ let host = getConfig("HOST") || "0.0.0.0";
23
72
  let noBrowser = false;
73
+ let background = false;
74
+ let subcommand = null;
75
+ let subArgs = [];
24
76
 
25
- // Parse args
26
77
  for (let i = 0; i < args.length; i++) {
27
- if (args[i] === "--port" || args[i] === "-p") {
28
- port = parseInt(args[i + 1], 10) || DEFAULT_PORT;
29
- i++;
30
- } else if (args[i] === "--host" || args[i] === "-H") {
31
- host = args[i + 1] || "0.0.0.0";
32
- i++;
33
- } else if (args[i] === "--no-browser" || args[i] === "-n") {
34
- noBrowser = true;
35
- } else if (args[i] === "--help" || args[i] === "-h") {
36
- console.log(`
37
- responses-proxy v${pkg.version}
38
-
39
- Usage: responses-proxy [options]
78
+ const arg = args[i];
79
+ if (arg === "--port" || arg === "-p") { port = parseInt(args[++i], 10) || DEFAULT_PORT; }
80
+ else if (arg === "--host" || arg === "-H") { host = args[++i] || "0.0.0.0"; }
81
+ else if (arg === "--no-browser" || arg === "-n") { noBrowser = true; }
82
+ else if (arg === "--background" || arg === "--bg" || arg === "-b" || arg === "--daemon") { background = true; }
83
+ else if (arg === "--help" || arg === "-h") { printHelp(); process.exit(0); }
84
+ else if (arg === "--version" || arg === "-v") { console.log(pkg.version); process.exit(0); }
85
+ else if (!arg.startsWith("-")) {
86
+ subcommand = arg;
87
+ subArgs = args.slice(i + 1);
88
+ break;
89
+ }
90
+ }
91
+
92
+ function printHelp() {
93
+ console.log(`
94
+ responses-proxy v${pkg.version} — AI Routing Proxy
95
+
96
+ Usage: responses-proxy [command] [options]
97
+
98
+ Commands:
99
+ (none) Start the server (foreground)
100
+ stop Stop the background daemon
101
+ status Check if running
102
+ restart Restart background daemon
103
+ logs Show recent server logs
104
+ info Show endpoint URL + API key
105
+ config Show all config
106
+ config set KEY VALUE Set a config value
107
+ config get KEY Get a config value
108
+ setup Interactive first-time setup wizard
109
+ password <pass> Set dashboard admin password
110
+ open Open dashboard in browser
40
111
 
41
112
  Options:
42
- -p, --port <port> Port to run the server (default: ${DEFAULT_PORT})
43
- -H, --host <host> Host to bind (default: 0.0.0.0)
44
- -n, --no-browser Don't open browser automatically
45
- -h, --help Show this help message
46
- -v, --version Show version
113
+ -p, --port <port> Port (default: ${DEFAULT_PORT})
114
+ -H, --host <host> Host (default: 0.0.0.0)
115
+ -n, --no-browser Don't auto-open browser
116
+ -b, --background Run as background daemon
117
+ -h, --help Show help
118
+ -v, --version Show version
119
+
120
+ Data directory: ${dataDir}
121
+ Config file: ${ENV_FILE}
47
122
  `);
48
- process.exit(0);
49
- } else if (args[i] === "--version" || args[i] === "-v") {
50
- console.log(pkg.version);
51
- process.exit(0);
123
+ }
124
+
125
+ // ─── Subcommand dispatch ─────────────────────────────────────────────────────
126
+
127
+ if (subcommand === "stop") { stopDaemon(); process.exit(0); }
128
+ if (subcommand === "status") { checkStatus(); process.exit(0); }
129
+ if (subcommand === "restart") { stopDaemon(); background = true; }
130
+ if (subcommand === "logs") { showLogs(); process.exit(0); }
131
+ if (subcommand === "info") { showInfo(); process.exit(0); }
132
+ if (subcommand === "open") { openDashboard(); process.exit(0); }
133
+ if (subcommand === "config") { handleConfig(); process.exit(0); }
134
+ if (subcommand === "password") { handlePassword(); process.exit(0); }
135
+ if (subcommand === "setup") { runSetup().then(() => process.exit(0)); }
136
+ else if (subcommand && !["restart"].includes(subcommand)) {
137
+ console.error(`Unknown command: ${subcommand}\nRun 'responses-proxy --help' for usage.`);
138
+ process.exit(1);
139
+ }
140
+
141
+ // ─── Subcommand implementations ──────────────────────────────────────────────
142
+
143
+ function getDaemonPid() {
144
+ try {
145
+ if (!fs.existsSync(PID_FILE)) return null;
146
+ const pid = parseInt(fs.readFileSync(PID_FILE, "utf8").trim(), 10);
147
+ if (!pid) return null;
148
+ process.kill(pid, 0);
149
+ return pid;
150
+ } catch {
151
+ try { fs.unlinkSync(PID_FILE); } catch {}
152
+ return null;
52
153
  }
53
154
  }
54
155
 
55
- // Resolve paths
56
- const serverPath = path.join(__dirname, "dist", "server.js");
156
+ function stopDaemon() {
157
+ const pid = getDaemonPid();
158
+ if (!pid) { console.log("⏹ Not running."); return; }
159
+ try {
160
+ process.kill(pid, "SIGTERM");
161
+ console.log(`⏹ Stopped (PID: ${pid})`);
162
+ try { fs.unlinkSync(PID_FILE); } catch {}
163
+ } catch (e) { console.error(`Failed: ${e.message}`); }
164
+ }
165
+
166
+ function checkStatus() {
167
+ const pid = getDaemonPid();
168
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
169
+ if (pid) {
170
+ console.log(`✅ Running (PID: ${pid})`);
171
+ console.log(` Endpoint: http://${displayHost}:${port}/v1`);
172
+ console.log(` Dashboard: http://${displayHost}:${port}/`);
173
+ console.log(` Data: ${dataDir}`);
174
+ } else {
175
+ console.log("⏹ Not running.");
176
+ }
177
+ }
178
+
179
+ function showLogs() {
180
+ if (!fs.existsSync(LOG_FILE)) { console.log("No logs yet."); return; }
181
+ const lines = fs.readFileSync(LOG_FILE, "utf8").split("\n");
182
+ console.log(lines.slice(-80).join("\n"));
183
+ }
184
+
185
+ function showInfo() {
186
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
187
+ const password = getConfig("DASHBOARD_PASSWORD") || "admin";
188
+ console.log(`
189
+ ┌─────────────────────────────────────────┐
190
+ │ responses-proxy v${pkg.version.padEnd(25)}│
191
+ ├─────────────────────────────────────────┤
192
+ │ Endpoint: http://${displayHost}:${port}/v1${" ".repeat(Math.max(0, 18 - String(port).length))}│
193
+ │ Dashboard: http://${displayHost}:${port}/${" ".repeat(Math.max(0, 20 - String(port).length))}│
194
+ │ Password: ${password.padEnd(27)}│
195
+ ├─────────────────────────────────────────┤
196
+ │ For Claude Code: │
197
+ │ export ANTHROPIC_BASE_URL=http://${displayHost}:${port}/v1
198
+ │ export ANTHROPIC_API_KEY=sk_anything │
199
+ ├─────────────────────────────────────────┤
200
+ │ For Codex / OpenAI SDK: │
201
+ │ base_url = http://${displayHost}:${port}/v1${" ".repeat(Math.max(0, 18 - String(port).length))}│
202
+ │ api_key = sk_anything │
203
+ └─────────────────────────────────────────┘
204
+ `);
205
+ }
206
+
207
+ function openDashboard() {
208
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
209
+ const url = `http://${displayHost}:${port}`;
210
+ const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
211
+ exec(cmd, { windowsHide: true }, () => {});
212
+ console.log(`Opening ${url}`);
213
+ }
214
+
215
+ function handleConfig() {
216
+ if (subArgs[0] === "set" && subArgs[1]) {
217
+ setConfig(subArgs[1], subArgs.slice(2).join(" "));
218
+ console.log(`✅ ${subArgs[1]} = ${subArgs.slice(2).join(" ")}`);
219
+ } else if (subArgs[0] === "get" && subArgs[1]) {
220
+ console.log(getConfig(subArgs[1]) || "(not set)");
221
+ } else {
222
+ const env = loadEnvConfig();
223
+ console.log(`Config file: ${ENV_FILE}\n`);
224
+ if (Object.keys(env).length === 0) {
225
+ console.log(" (empty — using defaults)");
226
+ console.log(`\n Default port: ${DEFAULT_PORT}`);
227
+ console.log(` Default password: admin`);
228
+ console.log(`\n Set values with: responses-proxy config set KEY VALUE`);
229
+ } else {
230
+ for (const [k, v] of Object.entries(env)) {
231
+ console.log(` ${k} = ${v}`);
232
+ }
233
+ }
234
+ console.log(`\nAvailable keys:`);
235
+ console.log(` PORT Server port (default: 8318)`);
236
+ console.log(` HOST Bind host (default: 0.0.0.0)`);
237
+ console.log(` DASHBOARD_PASSWORD Admin password (default: admin)`);
238
+ console.log(` UPSTREAM_BASE_URL Default upstream provider URL`);
239
+ console.log(` UPSTREAM_API_KEY Default upstream API key`);
240
+ console.log(` KIRO_ENABLED Enable Kiro provider (true/false)`);
241
+ console.log(` KIRO_DB_PATH Path to 9router kiro.sqlite`);
242
+ }
243
+ }
244
+
245
+ function handlePassword() {
246
+ const pass = subArgs[0];
247
+ if (!pass) {
248
+ console.log(`Current password: ${getConfig("DASHBOARD_PASSWORD") || "admin"}`);
249
+ console.log(`\nUsage: responses-proxy password <new-password>`);
250
+ return;
251
+ }
252
+ setConfig("DASHBOARD_PASSWORD", pass);
253
+ console.log(`✅ Dashboard password set to: ${pass}`);
254
+ console.log(` Restart the server for changes to take effect.`);
255
+ }
256
+
257
+ async function runSetup() {
258
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
259
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
260
+
261
+ console.log(`\n🚀 responses-proxy Setup Wizard\n`);
262
+
263
+ const portAnswer = await ask(`Port [${port}]: `);
264
+ if (portAnswer.trim()) setConfig("PORT", portAnswer.trim());
265
+
266
+ const passAnswer = await ask(`Dashboard password [admin]: `);
267
+ if (passAnswer.trim()) setConfig("DASHBOARD_PASSWORD", passAnswer.trim());
268
+
269
+ const kiroAnswer = await ask(`Enable Kiro provider? (y/n) [y]: `);
270
+ if (kiroAnswer.trim().toLowerCase() === "n") setConfig("KIRO_ENABLED", "false");
271
+ else setConfig("KIRO_ENABLED", "true");
272
+
273
+ const upstreamAnswer = await ask(`Default upstream URL (leave empty to skip): `);
274
+ if (upstreamAnswer.trim()) setConfig("UPSTREAM_BASE_URL", upstreamAnswer.trim());
275
+
276
+ const keyAnswer = await ask(`Default upstream API key (leave empty to skip): `);
277
+ if (keyAnswer.trim()) setConfig("UPSTREAM_API_KEY", keyAnswer.trim());
278
+
279
+ rl.close();
280
+
281
+ console.log(`\n✅ Config saved to ${ENV_FILE}`);
282
+ console.log(`\nStart the server with:`);
283
+ console.log(` responses-proxy`);
284
+ console.log(` responses-proxy --background`);
285
+ console.log(`\nView connection info:`);
286
+ console.log(` responses-proxy info\n`);
287
+ }
288
+
289
+ // ─── If subcommand was handled above (setup is async), bail ──────────────────
290
+ if (subcommand === "setup") { /* runSetup handles exit */ return; }
291
+ if (subcommand && subcommand !== "restart") process.exit(0);
292
+
293
+ // ─── Ensure deps ─────────────────────────────────────────────────────────────
57
294
 
58
295
  if (!fs.existsSync(serverPath)) {
59
- console.error("Error: Built server not found at", serverPath);
60
- console.error("Run 'npm run build' first, or reinstall the package.");
296
+ console.error("Error: Built server not found. Reinstall the package.");
61
297
  process.exit(1);
62
298
  }
63
299
 
64
- // Check if deps are installed
65
- const nodeModulesPath = path.join(__dirname, "dist", "node_modules");
66
300
  if (!fs.existsSync(path.join(nodeModulesPath, "fastify"))) {
67
- console.log("📦 Runtime deps missing. Installing...");
68
- const { execSync } = require("child_process");
301
+ console.log("📦 Installing runtime dependencies...");
69
302
  try {
70
303
  execSync("npm install --omit=dev --no-audit --no-fund --loglevel=error", {
71
- cwd: path.join(__dirname, "dist"),
72
- stdio: "inherit",
73
- timeout: 120000,
304
+ cwd: path.join(__dirname, "dist"), stdio: "inherit", timeout: 120000,
74
305
  });
75
306
  } catch {
76
307
  console.error("Failed to install deps. Run: cd " + path.join(__dirname, "dist") + " && npm install --omit=dev");
@@ -78,69 +309,112 @@ if (!fs.existsSync(path.join(nodeModulesPath, "fastify"))) {
78
309
  }
79
310
  }
80
311
 
81
- // Data directory
82
- const dataDir = path.join(os.homedir(), ".responses-proxy");
312
+ // ─── Kill existing ───────────────────────────────────────────────────────────
313
+
314
+ const existingPid = getDaemonPid();
315
+ if (existingPid) {
316
+ try { process.kill(existingPid, "SIGTERM"); } catch {}
317
+ try { fs.unlinkSync(PID_FILE); } catch {}
318
+ try { execSync("sleep 1", { stdio: "ignore" }); } catch {}
319
+ }
320
+
321
+ // ─── Build env ───────────────────────────────────────────────────────────────
322
+
83
323
  fs.mkdirSync(path.join(dataDir, "sessions"), { recursive: true });
324
+ const userConfig = loadEnvConfig();
325
+
326
+ const serverEnv = {
327
+ ...process.env,
328
+ ...userConfig,
329
+ NODE_PATH: nodeModulesPath,
330
+ PORT: String(port),
331
+ HOST: host,
332
+ UPSTREAM_BASE_URL: userConfig.UPSTREAM_BASE_URL || process.env.UPSTREAM_BASE_URL || "https://placeholder.invalid",
333
+ UPSTREAM_API_KEY: userConfig.UPSTREAM_API_KEY || process.env.UPSTREAM_API_KEY || "",
334
+ APP_DB_PATH: path.join(dataDir, "app.sqlite"),
335
+ SESSION_LOG_DIR: path.join(dataDir, "sessions"),
336
+ CUSTOMER_KEY_DB_PATH: path.join(dataDir, "telegram-bot.sqlite"),
337
+ KIRO_DB_PATH: userConfig.KIRO_DB_PATH || path.join(dataDir, "kiro.sqlite"),
338
+ KIRO_ENABLED: userConfig.KIRO_ENABLED || process.env.KIRO_ENABLED || "true",
339
+ QUICK_APPLY_HERMES_CONFIG_PATH: path.join(os.homedir(), ".hermes", "config.yaml"),
340
+ QUICK_APPLY_CODEX_CONFIG_PATH: path.join(os.homedir(), ".codex", "config.toml"),
341
+ QUICK_APPLY_CODEX_AUTH_PATH: path.join(os.homedir(), ".codex", "auth.json"),
342
+ TELEGRAM_BOT_TOKEN: userConfig.TELEGRAM_BOT_TOKEN || "",
343
+ TELEGRAM_OWNER_USER_IDS: userConfig.TELEGRAM_OWNER_USER_IDS || "",
344
+ TELEGRAM_ADMIN_USER_IDS: userConfig.TELEGRAM_ADMIN_USER_IDS || "",
345
+ DASHBOARD_PASSWORD: userConfig.DASHBOARD_PASSWORD || process.env.DASHBOARD_PASSWORD || "admin",
346
+ };
84
347
 
85
348
  const displayHost = host === "0.0.0.0" ? "localhost" : host;
86
349
  const url = `http://${displayHost}:${port}`;
87
350
 
88
- console.log(`
351
+ // ─── Background mode ─────────────────────────────────────────────────────────
352
+
353
+ if (background) {
354
+ const logFd = fs.openSync(LOG_FILE, "a");
355
+ const child = spawn(process.execPath, [serverPath], {
356
+ cwd: __dirname, env: serverEnv,
357
+ stdio: ["ignore", logFd, logFd],
358
+ detached: true,
359
+ });
360
+ child.unref();
361
+ fs.writeFileSync(PID_FILE, String(child.pid));
362
+ fs.closeSync(logFd);
363
+
364
+ console.log(`
89
365
  ┌─────────────────────────────────────────┐
90
366
  │ responses-proxy v${pkg.version.padEnd(25)}│
367
+ │ Running in background (PID: ${String(child.pid).padEnd(10)}│
91
368
  │ ${url.padEnd(39)}│
92
369
  │ Dashboard: ${(url + "/").padEnd(27)}│
93
370
  │ API: ${(url + "/v1").padEnd(33)}│
371
+ ├─────────────────────────────────────────┤
372
+ │ Stop: responses-proxy stop │
373
+ │ Status: responses-proxy status │
374
+ │ Logs: responses-proxy logs │
375
+ │ Config: responses-proxy config │
376
+ │ Info: responses-proxy info │
94
377
  └─────────────────────────────────────────┘
95
378
  `);
96
379
 
97
- // Spawn server
98
- const server = spawn(process.execPath, [serverPath], {
99
- cwd: __dirname,
100
- stdio: "inherit",
101
- env: {
102
- ...process.env,
103
- NODE_PATH: nodeModulesPath,
104
- PORT: String(port),
105
- HOST: host,
106
- UPSTREAM_BASE_URL: process.env.UPSTREAM_BASE_URL || "https://placeholder.invalid",
107
- UPSTREAM_API_KEY: process.env.UPSTREAM_API_KEY || "",
108
- APP_DB_PATH: path.join(dataDir, "app.sqlite"),
109
- SESSION_LOG_DIR: path.join(dataDir, "sessions"),
110
- CUSTOMER_KEY_DB_PATH: path.join(dataDir, "telegram-bot.sqlite"),
111
- KIRO_DB_PATH: path.join(dataDir, "kiro.sqlite"),
112
- KIRO_ENABLED: process.env.KIRO_ENABLED || "true",
113
- QUICK_APPLY_HERMES_CONFIG_PATH: path.join(os.homedir(), ".hermes", "config.yaml"),
114
- QUICK_APPLY_CODEX_CONFIG_PATH: path.join(os.homedir(), ".codex", "config.toml"),
115
- QUICK_APPLY_CODEX_AUTH_PATH: path.join(os.homedir(), ".codex", "auth.json"),
116
- TELEGRAM_BOT_TOKEN: "",
117
- TELEGRAM_OWNER_USER_IDS: "",
118
- TELEGRAM_ADMIN_USER_IDS: "",
119
- DASHBOARD_PASSWORD: process.env.DASHBOARD_PASSWORD || "admin",
120
- },
121
- });
122
-
123
- // Open browser after short delay
124
- if (!noBrowser) {
125
- setTimeout(() => {
126
- const openCmd =
127
- process.platform === "darwin" ? `open "${url}"` :
128
- process.platform === "win32" ? `start "" "${url}"` :
129
- `xdg-open "${url}"`;
130
- exec(openCmd, { windowsHide: true }, () => {});
131
- }, 2000);
132
- }
133
-
134
- // Handle exit
135
- function cleanup() {
136
- if (server.pid) {
137
- try { process.kill(server.pid, "SIGTERM"); } catch {}
380
+ if (!noBrowser) {
381
+ setTimeout(() => {
382
+ const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
383
+ exec(cmd, { windowsHide: true }, () => {});
384
+ }, 2000);
138
385
  }
139
- }
386
+ setTimeout(() => process.exit(0), 2500);
387
+
388
+ } else {
389
+ // ─── Foreground mode ───────────────────────────────────────────────────────
390
+
391
+ console.log(`
392
+ ┌─────────────────────────────────────────┐
393
+ │ responses-proxy v${pkg.version.padEnd(25)}│
394
+ │ ${url.padEnd(39)}│
395
+ │ Dashboard: ${(url + "/").padEnd(27)}│
396
+ │ API: ${(url + "/v1").padEnd(33)}│
397
+ │ Press Ctrl+C to stop │
398
+ └─────────────────────────────────────────┘
399
+ `);
400
+
401
+ const server = spawn(process.execPath, [serverPath], {
402
+ cwd: __dirname, stdio: "inherit", env: serverEnv,
403
+ });
404
+ fs.writeFileSync(PID_FILE, String(server.pid));
140
405
 
141
- process.on("SIGINT", () => { cleanup(); process.exit(0); });
142
- process.on("SIGTERM", () => { cleanup(); process.exit(0); });
406
+ if (!noBrowser) {
407
+ setTimeout(() => {
408
+ const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
409
+ exec(cmd, { windowsHide: true }, () => {});
410
+ }, 2000);
411
+ }
143
412
 
144
- server.on("close", (code) => {
145
- process.exit(code || 0);
146
- });
413
+ function cleanup() {
414
+ try { fs.unlinkSync(PID_FILE); } catch {}
415
+ if (server.pid) { try { process.kill(server.pid, "SIGTERM"); } catch {} }
416
+ }
417
+ process.on("SIGINT", () => { cleanup(); process.exit(0); });
418
+ process.on("SIGTERM", () => { cleanup(); process.exit(0); });
419
+ server.on("close", (code) => { try { fs.unlinkSync(PID_FILE); } catch {} process.exit(code || 0); });
420
+ }