voicecc 1.1.7 → 1.1.8
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/bin/voicecc.js +338 -28
- package/package.json +1 -1
- package/server/index.ts +45 -3
package/bin/voicecc.js
CHANGED
|
@@ -3,30 +3,47 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* CLI entry point for the voicecc command.
|
|
5
5
|
*
|
|
6
|
+
* Responsibilities:
|
|
6
7
|
* - On first run (no .env), launches an interactive setup wizard
|
|
7
8
|
* - Copies CLAUDE.md template on first run
|
|
8
|
-
* -
|
|
9
|
+
* - Manages the server as a background daemon (start/stop/status)
|
|
10
|
+
* - Supports subcommands: stop, logs, autostart
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { spawn, execSync } from "node:child_process";
|
|
12
|
-
import { copyFileSync, existsSync,
|
|
13
|
-
import { writeFile
|
|
14
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, openSync, closeSync } from "node:fs";
|
|
15
|
+
import { writeFile } from "node:fs/promises";
|
|
14
16
|
import { createInterface } from "node:readline";
|
|
15
17
|
import { randomBytes } from "node:crypto";
|
|
16
18
|
import { dirname, join } from "node:path";
|
|
17
19
|
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { homedir, platform } from "node:os";
|
|
18
21
|
|
|
19
22
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
23
|
const PKG_ROOT = join(__dirname, "..");
|
|
21
24
|
const TSX_BIN = join(PKG_ROOT, "node_modules", ".bin", "tsx");
|
|
22
25
|
const ENV_PATH = join(PKG_ROOT, ".env");
|
|
23
26
|
|
|
27
|
+
const VOICECC_DIR = join(homedir(), ".voicecc");
|
|
28
|
+
const PID_FILE = join(VOICECC_DIR, "voicecc.pid");
|
|
29
|
+
const LOG_FILE = join(VOICECC_DIR, "voicecc.log");
|
|
30
|
+
const STATUS_FILE = join(VOICECC_DIR, "status.json");
|
|
31
|
+
|
|
24
32
|
process.chdir(PKG_ROOT);
|
|
25
33
|
|
|
26
34
|
// ============================================================================
|
|
27
|
-
//
|
|
35
|
+
// HELPER FUNCTIONS
|
|
28
36
|
// ============================================================================
|
|
29
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Ensure the ~/.voicecc directory exists.
|
|
40
|
+
*/
|
|
41
|
+
function ensureVoiceccDir() {
|
|
42
|
+
if (!existsSync(VOICECC_DIR)) {
|
|
43
|
+
mkdirSync(VOICECC_DIR, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
/**
|
|
31
48
|
* Prompt the user for a single line of input.
|
|
32
49
|
*
|
|
@@ -64,6 +81,75 @@ function generatePassword() {
|
|
|
64
81
|
return randomBytes(18).toString("base64url");
|
|
65
82
|
}
|
|
66
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Check if the daemon is currently running.
|
|
86
|
+
*
|
|
87
|
+
* @returns true if the PID file exists and the process is alive
|
|
88
|
+
*/
|
|
89
|
+
function isRunning() {
|
|
90
|
+
if (!existsSync(PID_FILE)) return false;
|
|
91
|
+
|
|
92
|
+
const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
93
|
+
if (isNaN(pid)) return false;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
process.kill(pid, 0);
|
|
97
|
+
return true;
|
|
98
|
+
} catch {
|
|
99
|
+
// Process not found, clean up stale PID file
|
|
100
|
+
unlinkSync(PID_FILE);
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Read the status.json written by the server.
|
|
107
|
+
*
|
|
108
|
+
* @returns parsed status object or null if unavailable
|
|
109
|
+
*/
|
|
110
|
+
function readStatus() {
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(readFileSync(STATUS_FILE, "utf-8"));
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Display server info banner.
|
|
120
|
+
*/
|
|
121
|
+
function showInfo() {
|
|
122
|
+
const status = readStatus();
|
|
123
|
+
|
|
124
|
+
console.log("");
|
|
125
|
+
console.log("========================================");
|
|
126
|
+
console.log(" VOICECC RUNNING ");
|
|
127
|
+
console.log("========================================");
|
|
128
|
+
console.log("");
|
|
129
|
+
|
|
130
|
+
if (status) {
|
|
131
|
+
console.log(` Dashboard: http://localhost:${status.dashboardPort}`);
|
|
132
|
+
console.log(` Tunnel: ${status.tunnelUrl ?? "disabled"}`);
|
|
133
|
+
} else {
|
|
134
|
+
console.log(" Server is starting up...");
|
|
135
|
+
console.log(" Run 'voicecc' again in a few seconds to see details.");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log("");
|
|
139
|
+
console.log(" Logs: voicecc logs");
|
|
140
|
+
console.log(" Stop: voicecc stop");
|
|
141
|
+
console.log("");
|
|
142
|
+
console.log(" TIP: Run 'voicecc autostart' to start VoiceCC");
|
|
143
|
+
console.log(" automatically on reboot.");
|
|
144
|
+
console.log("");
|
|
145
|
+
console.log("========================================");
|
|
146
|
+
console.log("");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ============================================================================
|
|
150
|
+
// SETUP WIZARD
|
|
151
|
+
// ============================================================================
|
|
152
|
+
|
|
67
153
|
/**
|
|
68
154
|
* Run the first-run setup wizard.
|
|
69
155
|
* Prompts for ElevenLabs API key and dashboard password configuration.
|
|
@@ -195,10 +281,248 @@ function chownPkgRoot() {
|
|
|
195
281
|
execSync(`chown -R ${VOICECC_USER}:${VOICECC_USER} ${PKG_ROOT}`, { stdio: "inherit" });
|
|
196
282
|
}
|
|
197
283
|
|
|
284
|
+
// ============================================================================
|
|
285
|
+
// SUBCOMMANDS
|
|
286
|
+
// ============================================================================
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Stop the running daemon.
|
|
290
|
+
*/
|
|
291
|
+
function stopDaemon() {
|
|
292
|
+
if (!isRunning()) {
|
|
293
|
+
console.log("VoiceCC is not running.");
|
|
294
|
+
process.exit(0);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
298
|
+
console.log(`Stopping VoiceCC (PID ${pid})...`);
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
process.kill(pid, "SIGTERM");
|
|
302
|
+
} catch {
|
|
303
|
+
// Already dead
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Clean up
|
|
307
|
+
try { unlinkSync(PID_FILE); } catch { /* ignore */ }
|
|
308
|
+
try { unlinkSync(STATUS_FILE); } catch { /* ignore */ }
|
|
309
|
+
|
|
310
|
+
console.log("VoiceCC stopped.");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Tail the log file.
|
|
315
|
+
*/
|
|
316
|
+
function showLogs() {
|
|
317
|
+
if (!existsSync(LOG_FILE)) {
|
|
318
|
+
console.log("No log file found. Has VoiceCC been started?");
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const child = spawn("tail", ["-f", LOG_FILE], { stdio: "inherit" });
|
|
323
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
324
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Set up auto-start on reboot using systemd (Linux) or launchd (macOS).
|
|
329
|
+
*/
|
|
330
|
+
function setupAutostart() {
|
|
331
|
+
const os = platform();
|
|
332
|
+
|
|
333
|
+
if (os === "linux") {
|
|
334
|
+
setupSystemdAutostart();
|
|
335
|
+
} else if (os === "darwin") {
|
|
336
|
+
setupLaunchdAutostart();
|
|
337
|
+
} else {
|
|
338
|
+
console.log(`Autostart is not supported on ${os}.`);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Install a systemd service for auto-start on Linux.
|
|
345
|
+
* Requires sudo for writing to /etc/systemd/system.
|
|
346
|
+
*/
|
|
347
|
+
function setupSystemdAutostart() {
|
|
348
|
+
const user = execSync("whoami", { encoding: "utf-8" }).trim();
|
|
349
|
+
|
|
350
|
+
const serviceContent = `[Unit]
|
|
351
|
+
Description=VoiceCC Voice Server
|
|
352
|
+
After=network.target
|
|
353
|
+
|
|
354
|
+
[Service]
|
|
355
|
+
Type=simple
|
|
356
|
+
User=${user}
|
|
357
|
+
ExecStart=${TSX_BIN} server/index.ts
|
|
358
|
+
Restart=on-failure
|
|
359
|
+
RestartSec=5
|
|
360
|
+
Environment=HOME=${homedir()}
|
|
361
|
+
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
362
|
+
WorkingDirectory=${PKG_ROOT}
|
|
363
|
+
|
|
364
|
+
[Install]
|
|
365
|
+
WantedBy=multi-user.target
|
|
366
|
+
`;
|
|
367
|
+
|
|
368
|
+
const tmpPath = join(VOICECC_DIR, "voicecc.service");
|
|
369
|
+
writeFileSync(tmpPath, serviceContent);
|
|
370
|
+
|
|
371
|
+
console.log("Installing systemd service (sudo required)...");
|
|
372
|
+
console.log("");
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
execSync(`sudo cp ${tmpPath} /etc/systemd/system/voicecc.service`, { stdio: "inherit" });
|
|
376
|
+
execSync("sudo systemctl daemon-reload", { stdio: "inherit" });
|
|
377
|
+
execSync("sudo systemctl enable voicecc", { stdio: "inherit" });
|
|
378
|
+
console.log("");
|
|
379
|
+
console.log("Autostart enabled! VoiceCC will start on reboot.");
|
|
380
|
+
console.log("The systemd service manages the daemon separately.");
|
|
381
|
+
console.log("");
|
|
382
|
+
console.log(" sudo systemctl start voicecc Start now via systemd");
|
|
383
|
+
console.log(" sudo systemctl stop voicecc Stop via systemd");
|
|
384
|
+
console.log(" sudo systemctl status voicecc Check status");
|
|
385
|
+
console.log("");
|
|
386
|
+
} catch {
|
|
387
|
+
console.log("Failed to install systemd service. Check sudo permissions.");
|
|
388
|
+
process.exit(1);
|
|
389
|
+
} finally {
|
|
390
|
+
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Install a launchd agent for auto-start on macOS.
|
|
396
|
+
* No sudo required (user-level agent).
|
|
397
|
+
*/
|
|
398
|
+
function setupLaunchdAutostart() {
|
|
399
|
+
const plistName = "com.voicecc.server";
|
|
400
|
+
const plistDir = join(homedir(), "Library", "LaunchAgents");
|
|
401
|
+
const plistPath = join(plistDir, `${plistName}.plist`);
|
|
402
|
+
|
|
403
|
+
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
404
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
405
|
+
<plist version="1.0">
|
|
406
|
+
<dict>
|
|
407
|
+
<key>Label</key>
|
|
408
|
+
<string>${plistName}</string>
|
|
409
|
+
<key>ProgramArguments</key>
|
|
410
|
+
<array>
|
|
411
|
+
<string>${TSX_BIN}</string>
|
|
412
|
+
<string>server/index.ts</string>
|
|
413
|
+
</array>
|
|
414
|
+
<key>RunAtLoad</key>
|
|
415
|
+
<true/>
|
|
416
|
+
<key>KeepAlive</key>
|
|
417
|
+
<true/>
|
|
418
|
+
<key>WorkingDirectory</key>
|
|
419
|
+
<string>${PKG_ROOT}</string>
|
|
420
|
+
<key>EnvironmentVariables</key>
|
|
421
|
+
<dict>
|
|
422
|
+
<key>HOME</key>
|
|
423
|
+
<string>${homedir()}</string>
|
|
424
|
+
<key>PATH</key>
|
|
425
|
+
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
|
|
426
|
+
</dict>
|
|
427
|
+
<key>StandardOutPath</key>
|
|
428
|
+
<string>${LOG_FILE}</string>
|
|
429
|
+
<key>StandardErrorPath</key>
|
|
430
|
+
<string>${LOG_FILE}</string>
|
|
431
|
+
</dict>
|
|
432
|
+
</plist>
|
|
433
|
+
`;
|
|
434
|
+
|
|
435
|
+
if (!existsSync(plistDir)) {
|
|
436
|
+
mkdirSync(plistDir, { recursive: true });
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
// Unload existing if present
|
|
441
|
+
if (existsSync(plistPath)) {
|
|
442
|
+
try {
|
|
443
|
+
execSync(`launchctl unload ${plistPath}`, { stdio: "ignore" });
|
|
444
|
+
} catch { /* ignore */ }
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
writeFileSync(plistPath, plistContent);
|
|
448
|
+
execSync(`launchctl load ${plistPath}`, { stdio: "inherit" });
|
|
449
|
+
|
|
450
|
+
console.log("");
|
|
451
|
+
console.log("Autostart enabled! VoiceCC will start on login.");
|
|
452
|
+
console.log("");
|
|
453
|
+
console.log(` Plist: ${plistPath}`);
|
|
454
|
+
console.log("");
|
|
455
|
+
console.log(" To disable autostart:");
|
|
456
|
+
console.log(` launchctl unload ${plistPath}`);
|
|
457
|
+
console.log("");
|
|
458
|
+
} catch (err) {
|
|
459
|
+
console.log(`Failed to install launchd agent: ${err.message}`);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Start the server as a detached background daemon.
|
|
466
|
+
*/
|
|
467
|
+
function startDaemon() {
|
|
468
|
+
ensureVoiceccDir();
|
|
469
|
+
|
|
470
|
+
// Clean up stale status file
|
|
471
|
+
try { unlinkSync(STATUS_FILE); } catch { /* ignore */ }
|
|
472
|
+
|
|
473
|
+
const logFd = openSync(LOG_FILE, "a");
|
|
474
|
+
|
|
475
|
+
const isRoot = process.getuid && process.getuid() === 0;
|
|
476
|
+
|
|
477
|
+
let child;
|
|
478
|
+
if (isRoot) {
|
|
479
|
+
ensureNonRootUser();
|
|
480
|
+
chownPkgRoot();
|
|
481
|
+
console.log(`Dropping root privileges, running as '${VOICECC_USER}'...`);
|
|
482
|
+
child = spawn("su", ["-", VOICECC_USER, "-c", `cd ${PKG_ROOT} && ${TSX_BIN} server/index.ts`], {
|
|
483
|
+
cwd: PKG_ROOT,
|
|
484
|
+
detached: true,
|
|
485
|
+
stdio: ["ignore", logFd, logFd],
|
|
486
|
+
});
|
|
487
|
+
} else {
|
|
488
|
+
child = spawn(TSX_BIN, ["server/index.ts"], {
|
|
489
|
+
cwd: PKG_ROOT,
|
|
490
|
+
detached: true,
|
|
491
|
+
stdio: ["ignore", logFd, logFd],
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Write PID file
|
|
496
|
+
writeFileSync(PID_FILE, String(child.pid));
|
|
497
|
+
|
|
498
|
+
// Detach parent from child
|
|
499
|
+
child.unref();
|
|
500
|
+
closeSync(logFd);
|
|
501
|
+
}
|
|
502
|
+
|
|
198
503
|
// ============================================================================
|
|
199
504
|
// MAIN ENTRYPOINT
|
|
200
505
|
// ============================================================================
|
|
201
506
|
|
|
507
|
+
const subcommand = process.argv[2];
|
|
508
|
+
|
|
509
|
+
// Handle subcommands
|
|
510
|
+
if (subcommand === "stop") {
|
|
511
|
+
stopDaemon();
|
|
512
|
+
process.exit(0);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (subcommand === "logs") {
|
|
516
|
+
showLogs();
|
|
517
|
+
// showLogs doesn't return (tail -f)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (subcommand === "autostart") {
|
|
521
|
+
ensureVoiceccDir();
|
|
522
|
+
setupAutostart();
|
|
523
|
+
process.exit(0);
|
|
524
|
+
}
|
|
525
|
+
|
|
202
526
|
// Copy CLAUDE.md template if available
|
|
203
527
|
const claudeMdSrc = join("init", "CLAUDE.md");
|
|
204
528
|
if (existsSync(claudeMdSrc)) {
|
|
@@ -210,29 +534,15 @@ if (!existsSync(ENV_PATH)) {
|
|
|
210
534
|
await runSetupWizard();
|
|
211
535
|
}
|
|
212
536
|
|
|
213
|
-
// If running
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
console.log(`Dropping root privileges, running as '${VOICECC_USER}'...`);
|
|
220
|
-
const child = spawn("su", ["-", VOICECC_USER, "-c", `cd ${PKG_ROOT} && ${TSX_BIN} server/index.ts`], {
|
|
221
|
-
cwd: PKG_ROOT,
|
|
222
|
-
stdio: "inherit",
|
|
223
|
-
});
|
|
537
|
+
// If already running, show info and exit
|
|
538
|
+
if (isRunning()) {
|
|
539
|
+
showInfo();
|
|
540
|
+
process.exit(0);
|
|
541
|
+
}
|
|
224
542
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
child.on("exit", (code) => process.exit(code ?? 1));
|
|
228
|
-
} else {
|
|
229
|
-
// Start the dashboard directly
|
|
230
|
-
const child = spawn(TSX_BIN, ["server/index.ts"], {
|
|
231
|
-
cwd: PKG_ROOT,
|
|
232
|
-
stdio: "inherit",
|
|
233
|
-
});
|
|
543
|
+
// Start the daemon
|
|
544
|
+
startDaemon();
|
|
234
545
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
546
|
+
// Wait briefly for server to write status.json, then show info
|
|
547
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
548
|
+
showInfo();
|
package/package.json
CHANGED
package/server/index.ts
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
import "dotenv/config";
|
|
12
12
|
|
|
13
|
+
import { writeFileSync, unlinkSync, mkdirSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
|
|
13
17
|
import { startDashboard } from "../dashboard/server.js";
|
|
14
18
|
import { readEnv } from "./services/env.js";
|
|
15
19
|
import { startTunnel, isTunnelRunning, getTunnelUrl } from "./services/tunnel.js";
|
|
@@ -18,6 +22,39 @@ import { startBrowserCallServer } from "./services/browser-call-manager.js";
|
|
|
18
22
|
import { startHeartbeat } from "./services/heartbeat.js";
|
|
19
23
|
import { startVoiceServer } from "./voice/voice-server.js";
|
|
20
24
|
|
|
25
|
+
const STATUS_FILE = join(homedir(), ".voicecc", "status.json");
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// HELPER FUNCTIONS
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Write server status to ~/.voicecc/status.json so the CLI can display info.
|
|
33
|
+
*
|
|
34
|
+
* @param dashboardPort - the port the dashboard is running on
|
|
35
|
+
* @param tunnelUrl - the tunnel URL, or null if disabled
|
|
36
|
+
*/
|
|
37
|
+
function writeStatusFile(dashboardPort: number, tunnelUrl: string | null): void {
|
|
38
|
+
const status = {
|
|
39
|
+
dashboardPort,
|
|
40
|
+
tunnelUrl,
|
|
41
|
+
startedAt: new Date().toISOString(),
|
|
42
|
+
};
|
|
43
|
+
try {
|
|
44
|
+
mkdirSync(join(homedir(), ".voicecc"), { recursive: true });
|
|
45
|
+
writeFileSync(STATUS_FILE, JSON.stringify(status, null, 2));
|
|
46
|
+
} catch {
|
|
47
|
+
console.error("Failed to write status file");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Remove the status file on shutdown.
|
|
53
|
+
*/
|
|
54
|
+
function cleanupStatusFile(): void {
|
|
55
|
+
try { unlinkSync(STATUS_FILE); } catch { /* ignore */ }
|
|
56
|
+
}
|
|
57
|
+
|
|
21
58
|
// ============================================================================
|
|
22
59
|
// MAIN ENTRYPOINT
|
|
23
60
|
// ============================================================================
|
|
@@ -67,8 +104,15 @@ async function main(): Promise<void> {
|
|
|
67
104
|
}
|
|
68
105
|
}
|
|
69
106
|
|
|
70
|
-
//
|
|
107
|
+
// Write status file so the CLI can display server info
|
|
71
108
|
const tunnelUrl = getTunnelUrl();
|
|
109
|
+
writeStatusFile(dashboardPort, tunnelUrl);
|
|
110
|
+
|
|
111
|
+
// Clean up status file on shutdown
|
|
112
|
+
process.on("SIGTERM", () => { cleanupStatusFile(); process.exit(0); });
|
|
113
|
+
process.on("SIGINT", () => { cleanupStatusFile(); process.exit(0); });
|
|
114
|
+
|
|
115
|
+
// Print startup banner
|
|
72
116
|
console.log("");
|
|
73
117
|
console.log("========================================");
|
|
74
118
|
console.log(" VOICECC RUNNING ");
|
|
@@ -77,8 +121,6 @@ async function main(): Promise<void> {
|
|
|
77
121
|
console.log(` Dashboard: http://localhost:${dashboardPort}`);
|
|
78
122
|
console.log(` Tunnel: ${tunnelUrl ?? "disabled"}`);
|
|
79
123
|
console.log("");
|
|
80
|
-
console.log(" Press Ctrl+C to stop.");
|
|
81
|
-
console.log("");
|
|
82
124
|
}
|
|
83
125
|
|
|
84
126
|
// ============================================================================
|