vibelet 0.1.37 → 1.0.0
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/README.md +80 -0
- package/bin/cloudflared-quick-tunnel.mjs +11 -0
- package/bin/cloudflared-resolver.mjs +171 -0
- package/bin/vibelet-runtime-policy.mjs +36 -0
- package/bin/vibelet.cjs +12 -0
- package/bin/vibelet.mjs +1062 -0
- package/dist/index.cjs +126 -0
- package/package.json +25 -24
- package/app.json +0 -5
- package/dist/advertised-hosts.d.ts +0 -34
- package/dist/advertised-hosts.d.ts.map +0 -1
- package/dist/advertised-hosts.js +0 -176
- package/dist/advertised-hosts.js.map +0 -1
- package/dist/advertised-hosts.test.d.ts +0 -2
- package/dist/advertised-hosts.test.d.ts.map +0 -1
- package/dist/advertised-hosts.test.js +0 -96
- package/dist/advertised-hosts.test.js.map +0 -1
- package/dist/audit.d.ts +0 -30
- package/dist/audit.d.ts.map +0 -1
- package/dist/audit.js +0 -73
- package/dist/audit.js.map +0 -1
- package/dist/audit.test.d.ts +0 -2
- package/dist/audit.test.d.ts.map +0 -1
- package/dist/audit.test.js +0 -33
- package/dist/audit.test.js.map +0 -1
- package/dist/auth.d.ts +0 -6
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js +0 -27
- package/dist/auth.js.map +0 -1
- package/dist/claude-hooks.d.ts +0 -58
- package/dist/claude-hooks.d.ts.map +0 -1
- package/dist/claude-hooks.js +0 -129
- package/dist/claude-hooks.js.map +0 -1
- package/dist/cli-version.d.ts +0 -3
- package/dist/cli-version.d.ts.map +0 -1
- package/dist/cli-version.js +0 -35
- package/dist/cli-version.js.map +0 -1
- package/dist/cli-version.test.d.ts +0 -2
- package/dist/cli-version.test.d.ts.map +0 -1
- package/dist/cli-version.test.js +0 -38
- package/dist/cli-version.test.js.map +0 -1
- package/dist/config.d.ts +0 -30
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -327
- package/dist/config.js.map +0 -1
- package/dist/config.test.d.ts +0 -2
- package/dist/config.test.d.ts.map +0 -1
- package/dist/config.test.js +0 -184
- package/dist/config.test.js.map +0 -1
- package/dist/dev-auth.test.d.ts +0 -2
- package/dist/dev-auth.test.d.ts.map +0 -1
- package/dist/dev-auth.test.js +0 -154
- package/dist/dev-auth.test.js.map +0 -1
- package/dist/dev-script.test.d.ts +0 -2
- package/dist/dev-script.test.d.ts.map +0 -1
- package/dist/dev-script.test.js +0 -412
- package/dist/dev-script.test.js.map +0 -1
- package/dist/drivers/claude.d.ts +0 -34
- package/dist/drivers/claude.d.ts.map +0 -1
- package/dist/drivers/claude.js +0 -413
- package/dist/drivers/claude.js.map +0 -1
- package/dist/drivers/claude.test.d.ts +0 -2
- package/dist/drivers/claude.test.d.ts.map +0 -1
- package/dist/drivers/claude.test.js +0 -951
- package/dist/drivers/claude.test.js.map +0 -1
- package/dist/drivers/codex.d.ts +0 -38
- package/dist/drivers/codex.d.ts.map +0 -1
- package/dist/drivers/codex.js +0 -771
- package/dist/drivers/codex.js.map +0 -1
- package/dist/drivers/codex.test.d.ts +0 -2
- package/dist/drivers/codex.test.d.ts.map +0 -1
- package/dist/drivers/codex.test.js +0 -939
- package/dist/drivers/codex.test.js.map +0 -1
- package/dist/drivers/types.d.ts +0 -14
- package/dist/drivers/types.d.ts.map +0 -1
- package/dist/drivers/types.js +0 -2
- package/dist/drivers/types.js.map +0 -1
- package/dist/e2e.test.d.ts +0 -2
- package/dist/e2e.test.d.ts.map +0 -1
- package/dist/e2e.test.js +0 -111
- package/dist/e2e.test.js.map +0 -1
- package/dist/identity.d.ts +0 -10
- package/dist/identity.d.ts.map +0 -1
- package/dist/identity.js +0 -66
- package/dist/identity.js.map +0 -1
- package/dist/identity.test.d.ts +0 -2
- package/dist/identity.test.d.ts.map +0 -1
- package/dist/identity.test.js +0 -25
- package/dist/identity.test.js.map +0 -1
- package/dist/index-entry.test.d.ts +0 -2
- package/dist/index-entry.test.d.ts.map +0 -1
- package/dist/index-entry.test.js +0 -272
- package/dist/index-entry.test.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -707
- package/dist/index.js.map +0 -1
- package/dist/logger.d.ts +0 -31
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -75
- package/dist/logger.js.map +0 -1
- package/dist/metrics.d.ts +0 -52
- package/dist/metrics.d.ts.map +0 -1
- package/dist/metrics.js +0 -89
- package/dist/metrics.js.map +0 -1
- package/dist/pairing-store.d.ts +0 -29
- package/dist/pairing-store.d.ts.map +0 -1
- package/dist/pairing-store.js +0 -131
- package/dist/pairing-store.js.map +0 -1
- package/dist/pairing-store.test.d.ts +0 -2
- package/dist/pairing-store.test.d.ts.map +0 -1
- package/dist/pairing-store.test.js +0 -47
- package/dist/pairing-store.test.js.map +0 -1
- package/dist/paths.d.ts +0 -16
- package/dist/paths.d.ts.map +0 -1
- package/dist/paths.js +0 -18
- package/dist/paths.js.map +0 -1
- package/dist/perf-compare.d.ts +0 -13
- package/dist/perf-compare.d.ts.map +0 -1
- package/dist/perf-compare.js +0 -125
- package/dist/perf-compare.js.map +0 -1
- package/dist/port-conflict.d.ts +0 -9
- package/dist/port-conflict.d.ts.map +0 -1
- package/dist/port-conflict.js +0 -33
- package/dist/port-conflict.js.map +0 -1
- package/dist/port-conflict.test.d.ts +0 -2
- package/dist/port-conflict.test.d.ts.map +0 -1
- package/dist/port-conflict.test.js +0 -38
- package/dist/port-conflict.test.js.map +0 -1
- package/dist/process-scanner.d.ts +0 -43
- package/dist/process-scanner.d.ts.map +0 -1
- package/dist/process-scanner.js +0 -453
- package/dist/process-scanner.js.map +0 -1
- package/dist/process-scanner.perf.test.d.ts +0 -2
- package/dist/process-scanner.perf.test.d.ts.map +0 -1
- package/dist/process-scanner.perf.test.js +0 -186
- package/dist/process-scanner.perf.test.js.map +0 -1
- package/dist/process-scanner.test.d.ts +0 -2
- package/dist/process-scanner.test.d.ts.map +0 -1
- package/dist/process-scanner.test.js +0 -399
- package/dist/process-scanner.test.js.map +0 -1
- package/dist/push-protocol.d.ts +0 -15
- package/dist/push-protocol.d.ts.map +0 -1
- package/dist/push-protocol.js +0 -23
- package/dist/push-protocol.js.map +0 -1
- package/dist/push-protocol.test.d.ts +0 -2
- package/dist/push-protocol.test.d.ts.map +0 -1
- package/dist/push-protocol.test.js +0 -57
- package/dist/push-protocol.test.js.map +0 -1
- package/dist/push-store.d.ts +0 -22
- package/dist/push-store.d.ts.map +0 -1
- package/dist/push-store.js +0 -103
- package/dist/push-store.js.map +0 -1
- package/dist/push-store.test.d.ts +0 -2
- package/dist/push-store.test.d.ts.map +0 -1
- package/dist/push-store.test.js +0 -79
- package/dist/push-store.test.js.map +0 -1
- package/dist/push.d.ts +0 -65
- package/dist/push.d.ts.map +0 -1
- package/dist/push.js +0 -202
- package/dist/push.js.map +0 -1
- package/dist/push.test.d.ts +0 -2
- package/dist/push.test.d.ts.map +0 -1
- package/dist/push.test.js +0 -199
- package/dist/push.test.js.map +0 -1
- package/dist/safe-stdio.d.ts +0 -3
- package/dist/safe-stdio.d.ts.map +0 -1
- package/dist/safe-stdio.js +0 -46
- package/dist/safe-stdio.js.map +0 -1
- package/dist/scanner.d.ts +0 -30
- package/dist/scanner.d.ts.map +0 -1
- package/dist/scanner.js +0 -859
- package/dist/scanner.js.map +0 -1
- package/dist/scanner.perf.test.d.ts +0 -2
- package/dist/scanner.perf.test.d.ts.map +0 -1
- package/dist/scanner.perf.test.js +0 -320
- package/dist/scanner.perf.test.js.map +0 -1
- package/dist/scanner.test.d.ts +0 -2
- package/dist/scanner.test.d.ts.map +0 -1
- package/dist/scanner.test.js +0 -948
- package/dist/scanner.test.js.map +0 -1
- package/dist/session-inventory.d.ts +0 -63
- package/dist/session-inventory.d.ts.map +0 -1
- package/dist/session-inventory.js +0 -525
- package/dist/session-inventory.js.map +0 -1
- package/dist/session-inventory.perf.test.d.ts +0 -2
- package/dist/session-inventory.perf.test.d.ts.map +0 -1
- package/dist/session-inventory.perf.test.js +0 -220
- package/dist/session-inventory.perf.test.js.map +0 -1
- package/dist/session-inventory.test.d.ts +0 -2
- package/dist/session-inventory.test.d.ts.map +0 -1
- package/dist/session-inventory.test.js +0 -712
- package/dist/session-inventory.test.js.map +0 -1
- package/dist/session-manager.d.ts +0 -75
- package/dist/session-manager.d.ts.map +0 -1
- package/dist/session-manager.js +0 -1515
- package/dist/session-manager.js.map +0 -1
- package/dist/session-manager.test.d.ts +0 -2
- package/dist/session-manager.test.d.ts.map +0 -1
- package/dist/session-manager.test.js +0 -2861
- package/dist/session-manager.test.js.map +0 -1
- package/dist/session-store.d.ts +0 -42
- package/dist/session-store.d.ts.map +0 -1
- package/dist/session-store.js +0 -163
- package/dist/session-store.js.map +0 -1
- package/dist/session-store.test.d.ts +0 -2
- package/dist/session-store.test.d.ts.map +0 -1
- package/dist/session-store.test.js +0 -236
- package/dist/session-store.test.js.map +0 -1
- package/dist/session-title.d.ts +0 -6
- package/dist/session-title.d.ts.map +0 -1
- package/dist/session-title.js +0 -105
- package/dist/session-title.js.map +0 -1
- package/dist/session-title.perf.test.d.ts +0 -2
- package/dist/session-title.perf.test.d.ts.map +0 -1
- package/dist/session-title.perf.test.js +0 -99
- package/dist/session-title.perf.test.js.map +0 -1
- package/dist/session-title.test.d.ts +0 -2
- package/dist/session-title.test.d.ts.map +0 -1
- package/dist/session-title.test.js +0 -199
- package/dist/session-title.test.js.map +0 -1
- package/dist/shutdown-endpoint.test.d.ts +0 -2
- package/dist/shutdown-endpoint.test.d.ts.map +0 -1
- package/dist/shutdown-endpoint.test.js +0 -93
- package/dist/shutdown-endpoint.test.js.map +0 -1
- package/dist/storage-housekeeping.d.ts +0 -28
- package/dist/storage-housekeeping.d.ts.map +0 -1
- package/dist/storage-housekeeping.js +0 -76
- package/dist/storage-housekeeping.js.map +0 -1
- package/dist/storage-housekeeping.test.d.ts +0 -2
- package/dist/storage-housekeeping.test.d.ts.map +0 -1
- package/dist/storage-housekeeping.test.js +0 -65
- package/dist/storage-housekeeping.test.js.map +0 -1
- package/dist/test-daemon-harness.d.ts +0 -31
- package/dist/test-daemon-harness.d.ts.map +0 -1
- package/dist/test-daemon-harness.js +0 -337
- package/dist/test-daemon-harness.js.map +0 -1
- package/dist/token-auth.test.d.ts +0 -2
- package/dist/token-auth.test.d.ts.map +0 -1
- package/dist/token-auth.test.js +0 -52
- package/dist/token-auth.test.js.map +0 -1
- package/dist/utils.d.ts +0 -4
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -40
- package/dist/utils.js.map +0 -1
- package/dist/utils.test.d.ts +0 -2
- package/dist/utils.test.d.ts.map +0 -1
- package/dist/utils.test.js +0 -54
- package/dist/utils.test.js.map +0 -1
- package/dist/ws-data.d.ts +0 -4
- package/dist/ws-data.d.ts.map +0 -1
- package/dist/ws-data.js +0 -20
- package/dist/ws-data.js.map +0 -1
- package/dist/ws-data.test.d.ts +0 -2
- package/dist/ws-data.test.d.ts.map +0 -1
- package/dist/ws-data.test.js +0 -17
- package/dist/ws-data.test.js.map +0 -1
- package/perf-reporter.mjs +0 -138
- package/scripts/build-release.mjs +0 -41
- package/scripts/dev.mjs +0 -537
- package/src/advertised-hosts.test.ts +0 -125
- package/src/advertised-hosts.ts +0 -225
- package/src/audit.test.ts +0 -38
- package/src/audit.ts +0 -117
- package/src/auth.ts +0 -31
- package/src/claude-hooks.ts +0 -195
- package/src/cli-version.test.ts +0 -36
- package/src/cli-version.ts +0 -46
- package/src/config.test.ts +0 -254
- package/src/config.ts +0 -324
- package/src/dev-auth.test.ts +0 -183
- package/src/dev-script.test.ts +0 -511
- package/src/drivers/claude.test.ts +0 -1186
- package/src/drivers/claude.ts +0 -443
- package/src/drivers/codex.test.ts +0 -1096
- package/src/drivers/codex.ts +0 -879
- package/src/drivers/types.ts +0 -15
- package/src/e2e.test.ts +0 -139
- package/src/identity.test.ts +0 -26
- package/src/identity.ts +0 -82
- package/src/index-entry.test.ts +0 -336
- package/src/index.ts +0 -781
- package/src/logger.ts +0 -112
- package/src/metrics.ts +0 -117
- package/src/pairing-store.test.ts +0 -53
- package/src/pairing-store.ts +0 -154
- package/src/paths.ts +0 -19
- package/src/perf-compare.ts +0 -164
- package/src/port-conflict.test.ts +0 -45
- package/src/port-conflict.ts +0 -44
- package/src/process-scanner.perf.test.ts +0 -222
- package/src/process-scanner.test.ts +0 -575
- package/src/process-scanner.ts +0 -514
- package/src/push-protocol.test.ts +0 -74
- package/src/push-protocol.ts +0 -36
- package/src/push-store.test.ts +0 -89
- package/src/push-store.ts +0 -126
- package/src/push.test.ts +0 -234
- package/src/push.ts +0 -318
- package/src/safe-stdio.ts +0 -51
- package/src/scanner.perf.test.ts +0 -359
- package/src/scanner.test.ts +0 -1045
- package/src/scanner.ts +0 -924
- package/src/session-inventory.perf.test.ts +0 -250
- package/src/session-inventory.test.ts +0 -1002
- package/src/session-inventory.ts +0 -721
- package/src/session-manager.test.ts +0 -3430
- package/src/session-manager.ts +0 -1775
- package/src/session-store.test.ts +0 -276
- package/src/session-store.ts +0 -202
- package/src/session-title.perf.test.ts +0 -118
- package/src/session-title.test.ts +0 -286
- package/src/session-title.ts +0 -108
- package/src/shutdown-endpoint.test.ts +0 -95
- package/src/storage-housekeeping.test.ts +0 -78
- package/src/storage-housekeeping.ts +0 -111
- package/src/test-daemon-harness.ts +0 -410
- package/src/token-auth.test.ts +0 -67
- package/src/utils.test.ts +0 -65
- package/src/utils.ts +0 -47
- package/src/ws-data.test.ts +0 -20
- package/src/ws-data.ts +0 -26
- package/tsconfig.json +0 -12
package/bin/vibelet.mjs
ADDED
|
@@ -0,0 +1,1062 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, openSync, writeSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import QRCode from 'qrcode';
|
|
9
|
+
import { extractQuickTunnelUrl } from './cloudflared-quick-tunnel.mjs';
|
|
10
|
+
import { formatCloudflaredFailureMessage, resolveCloudflaredLaunchSpec } from './cloudflared-resolver.mjs';
|
|
11
|
+
import { doesHealthMatchRequestedConnectionConfig, shouldReuseHealthyDaemon } from './vibelet-runtime-policy.mjs';
|
|
12
|
+
|
|
13
|
+
// ─── Paths & constants ─────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
+
const packageJson = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf8'));
|
|
17
|
+
const daemonDistDir = resolve(rootDir, 'dist');
|
|
18
|
+
const daemonEntryPath = resolve(daemonDistDir, 'index.cjs');
|
|
19
|
+
const port = Number(process.env.VIBE_PORT) || 9876;
|
|
20
|
+
const vibeletDir = join(homedir(), '.vibelet');
|
|
21
|
+
const pairingQrPngPath = join(vibeletDir, 'pairing-qr.png');
|
|
22
|
+
const logDir = join(vibeletDir, 'logs');
|
|
23
|
+
const runtimeDir = join(vibeletDir, 'runtime');
|
|
24
|
+
const runtimeCurrentDir = join(runtimeDir, 'current');
|
|
25
|
+
const runtimeMetadataPath = join(runtimeCurrentDir, 'runtime.json');
|
|
26
|
+
const runtimeDaemonEntryPath = join(runtimeCurrentDir, 'dist', 'index.cjs');
|
|
27
|
+
const stdoutLogPath = join(logDir, 'daemon.stdout.log');
|
|
28
|
+
const stderrLogPath = join(logDir, 'daemon.stderr.log');
|
|
29
|
+
const pidFilePath = join(vibeletDir, 'daemon.pid');
|
|
30
|
+
const relayConfigPath = join(vibeletDir, 'relay.json');
|
|
31
|
+
const tunnelStatePath = join(vibeletDir, 'tunnel.json');
|
|
32
|
+
const updateCheckPath = join(vibeletDir, 'update-check.json');
|
|
33
|
+
const OFFICIAL_SITE_URL = 'https://vibelet.icu';
|
|
34
|
+
const UPDATE_CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
35
|
+
|
|
36
|
+
let officialSitePrinted = false;
|
|
37
|
+
let updateMessage = '';
|
|
38
|
+
|
|
39
|
+
function printOfficialSite() {
|
|
40
|
+
if (officialSitePrinted) return;
|
|
41
|
+
officialSitePrinted = true;
|
|
42
|
+
try {
|
|
43
|
+
if (updateMessage) writeSync(2, `\n${updateMessage}\n`);
|
|
44
|
+
writeSync(1, `\nOfficial site: ${OFFICIAL_SITE_URL}\n`);
|
|
45
|
+
} catch {
|
|
46
|
+
// Best-effort branding footer; ignore broken pipes and closed stdio.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Update check ──────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
function readUpdateCheck() {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(readFileSync(updateCheckPath, 'utf8'));
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writeUpdateCheck(data) {
|
|
61
|
+
try {
|
|
62
|
+
mkdirSync(vibeletDir, { recursive: true });
|
|
63
|
+
writeFileSync(updateCheckPath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
64
|
+
} catch {
|
|
65
|
+
// Best-effort; failing to persist is fine.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function compareVersions(a, b) {
|
|
70
|
+
const pa = a.split('.').map(Number);
|
|
71
|
+
const pb = b.split('.').map(Number);
|
|
72
|
+
for (let i = 0; i < 3; i++) {
|
|
73
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
74
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
75
|
+
}
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function checkForUpdateFromCache() {
|
|
80
|
+
const cached = readUpdateCheck();
|
|
81
|
+
if (!cached?.latestVersion) return;
|
|
82
|
+
if (compareVersions(packageJson.version, cached.latestVersion) < 0) {
|
|
83
|
+
updateMessage =
|
|
84
|
+
`\x1b[33m╭───────────────────────────────────────────╮\x1b[0m\n` +
|
|
85
|
+
`\x1b[33m│\x1b[0m Update available: \x1b[90m${packageJson.version}\x1b[0m → \x1b[32m${cached.latestVersion}\x1b[0m${' '.repeat(Math.max(0, 14 - packageJson.version.length - cached.latestVersion.length))}\x1b[33m│\x1b[0m\n` +
|
|
86
|
+
`\x1b[33m│\x1b[0m Run \x1b[36mnpx @vibelet/cli@latest\x1b[0m to upgrade \x1b[33m│\x1b[0m\n` +
|
|
87
|
+
`\x1b[33m╰───────────────────────────────────────────╯\x1b[0m`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function fetchLatestVersionInBackground() {
|
|
92
|
+
const cached = readUpdateCheck();
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
if (cached?.checkedAt && now - cached.checkedAt < UPDATE_CHECK_INTERVAL_MS) {
|
|
95
|
+
return; // Checked recently; skip.
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Fire-and-forget: spawn a detached process to query the registry so we
|
|
99
|
+
// never block the CLI. Results are read on the *next* invocation.
|
|
100
|
+
const script = `
|
|
101
|
+
const https = await import('node:https');
|
|
102
|
+
const fs = await import('node:fs');
|
|
103
|
+
const url = 'https://registry.npmjs.org/@vibelet/cli/latest';
|
|
104
|
+
https.get(url, { headers: { 'Accept': 'application/json' }, timeout: 8000 }, (res) => {
|
|
105
|
+
let data = '';
|
|
106
|
+
res.on('data', (c) => data += c);
|
|
107
|
+
res.on('end', () => {
|
|
108
|
+
try {
|
|
109
|
+
const version = JSON.parse(data).version;
|
|
110
|
+
if (version) {
|
|
111
|
+
fs.writeFileSync(${JSON.stringify(updateCheckPath)}, JSON.stringify({
|
|
112
|
+
latestVersion: version,
|
|
113
|
+
checkedAt: Date.now(),
|
|
114
|
+
}, null, 2) + '\\n');
|
|
115
|
+
}
|
|
116
|
+
} catch {}
|
|
117
|
+
});
|
|
118
|
+
}).on('error', () => {});
|
|
119
|
+
`;
|
|
120
|
+
|
|
121
|
+
const child = spawn(process.execPath, ['--input-type=module', '-e', script], {
|
|
122
|
+
detached: true,
|
|
123
|
+
stdio: 'ignore',
|
|
124
|
+
windowsHide: true,
|
|
125
|
+
});
|
|
126
|
+
child.unref();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
function fail(message, details) {
|
|
132
|
+
process.stderr.write(`${message}\n`);
|
|
133
|
+
if (details) {
|
|
134
|
+
process.stderr.write(`${details}\n`);
|
|
135
|
+
}
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readRuntimeMetadata() {
|
|
140
|
+
if (!existsSync(runtimeMetadataPath)) return null;
|
|
141
|
+
try {
|
|
142
|
+
return JSON.parse(readFileSync(runtimeMetadataPath, 'utf8'));
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function ensureRuntimeInstalled() {
|
|
149
|
+
if (!existsSync(daemonEntryPath)) {
|
|
150
|
+
fail('The compiled daemon runtime is missing.', 'Run `pnpm build` before invoking `npx @vibelet/cli` from a source checkout.');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const sourceDaemonStat = statSync(daemonEntryPath);
|
|
154
|
+
const runtimeMetadata = readRuntimeMetadata();
|
|
155
|
+
const runtimeLooksFresh =
|
|
156
|
+
existsSync(runtimeDaemonEntryPath) &&
|
|
157
|
+
runtimeMetadata?.version === packageJson.version &&
|
|
158
|
+
runtimeMetadata?.daemonEntryMtimeMs === sourceDaemonStat.mtimeMs;
|
|
159
|
+
|
|
160
|
+
if (runtimeLooksFresh) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
165
|
+
const nextRuntimeDir = join(runtimeDir, `current.${Date.now()}.${process.pid}`);
|
|
166
|
+
rmSync(nextRuntimeDir, { recursive: true, force: true });
|
|
167
|
+
mkdirSync(nextRuntimeDir, { recursive: true });
|
|
168
|
+
mkdirSync(logDir, { recursive: true });
|
|
169
|
+
|
|
170
|
+
writeFileSync(join(nextRuntimeDir, 'package.json'), JSON.stringify({
|
|
171
|
+
name: 'vibelet-runtime',
|
|
172
|
+
private: true,
|
|
173
|
+
type: 'module',
|
|
174
|
+
}, null, 2) + '\n', 'utf8');
|
|
175
|
+
|
|
176
|
+
cpSync(daemonDistDir, join(nextRuntimeDir, 'dist'), {
|
|
177
|
+
recursive: true,
|
|
178
|
+
dereference: true,
|
|
179
|
+
force: true,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
writeFileSync(runtimeMetadataPath.replace(runtimeCurrentDir, nextRuntimeDir), JSON.stringify({
|
|
183
|
+
version: packageJson.version,
|
|
184
|
+
daemonEntryMtimeMs: sourceDaemonStat.mtimeMs,
|
|
185
|
+
installedAt: new Date().toISOString(),
|
|
186
|
+
}, null, 2) + '\n', 'utf8');
|
|
187
|
+
|
|
188
|
+
rmSync(runtimeCurrentDir, { recursive: true, force: true });
|
|
189
|
+
renameSync(nextRuntimeDir, runtimeCurrentDir);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── PID file helpers ───────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
function writePidFile(pid) {
|
|
195
|
+
mkdirSync(dirname(pidFilePath), { recursive: true });
|
|
196
|
+
writeFileSync(pidFilePath, String(pid), 'utf8');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function readPidFile() {
|
|
200
|
+
try {
|
|
201
|
+
const pid = Number(readFileSync(pidFilePath, 'utf8').trim());
|
|
202
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function removePidFile() {
|
|
209
|
+
rmSync(pidFilePath, { force: true });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isProcessAlive(pid) {
|
|
213
|
+
try {
|
|
214
|
+
process.kill(pid, 0);
|
|
215
|
+
return true;
|
|
216
|
+
} catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ─── Platform service backends ──────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
function createDarwinBackend() {
|
|
224
|
+
const label = 'dev.vibelet.daemon';
|
|
225
|
+
const uid = process.getuid?.();
|
|
226
|
+
const launchDomain = `gui/${uid ?? 0}`;
|
|
227
|
+
const launchAgentsDir = join(homedir(), 'Library', 'LaunchAgents');
|
|
228
|
+
const plistPath = join(launchAgentsDir, `${label}.plist`);
|
|
229
|
+
|
|
230
|
+
function launchctl(args) {
|
|
231
|
+
return spawnSync('launchctl', args, { encoding: 'utf8' });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function plistContents() {
|
|
235
|
+
const programArgs = [
|
|
236
|
+
process.execPath,
|
|
237
|
+
runtimeDaemonEntryPath,
|
|
238
|
+
].map((value) => ` <string>${value}</string>`).join('\n');
|
|
239
|
+
|
|
240
|
+
const envVars = { VIBE_PORT: String(port) };
|
|
241
|
+
if (process.env.VIBELET_RELAY_URL) envVars.VIBELET_RELAY_URL = process.env.VIBELET_RELAY_URL;
|
|
242
|
+
if (process.env.VIBELET_CANONICAL_HOST) envVars.VIBELET_CANONICAL_HOST = process.env.VIBELET_CANONICAL_HOST;
|
|
243
|
+
if (process.env.VIBELET_FALLBACK_HOSTS) envVars.VIBELET_FALLBACK_HOSTS = process.env.VIBELET_FALLBACK_HOSTS;
|
|
244
|
+
const envSection = Object.keys(envVars).length > 0
|
|
245
|
+
? ` <key>EnvironmentVariables</key>
|
|
246
|
+
<dict>
|
|
247
|
+
${Object.entries(envVars).map(([k, v]) => ` <key>${k}</key>\n <string>${v}</string>`).join('\n')}
|
|
248
|
+
</dict>`
|
|
249
|
+
: '';
|
|
250
|
+
|
|
251
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
252
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
253
|
+
<plist version="1.0">
|
|
254
|
+
<dict>
|
|
255
|
+
<key>Label</key>
|
|
256
|
+
<string>${label}</string>
|
|
257
|
+
<key>ProgramArguments</key>
|
|
258
|
+
<array>
|
|
259
|
+
${programArgs}
|
|
260
|
+
</array>
|
|
261
|
+
<key>WorkingDirectory</key>
|
|
262
|
+
<string>${runtimeCurrentDir}</string>
|
|
263
|
+
<key>StandardOutPath</key>
|
|
264
|
+
<string>${stdoutLogPath}</string>
|
|
265
|
+
<key>StandardErrorPath</key>
|
|
266
|
+
<string>${stderrLogPath}</string>
|
|
267
|
+
${envSection}
|
|
268
|
+
<key>RunAtLoad</key>
|
|
269
|
+
<true/>
|
|
270
|
+
<key>KeepAlive</key>
|
|
271
|
+
<true/>
|
|
272
|
+
</dict>
|
|
273
|
+
</plist>
|
|
274
|
+
`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
name: 'launchd',
|
|
279
|
+
handlesProcessLifecycle: true,
|
|
280
|
+
|
|
281
|
+
isServiceInstalled() {
|
|
282
|
+
return launchctl(['print', `${launchDomain}/${label}`]).status === 0;
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
install() {
|
|
286
|
+
mkdirSync(launchAgentsDir, { recursive: true });
|
|
287
|
+
mkdirSync(logDir, { recursive: true });
|
|
288
|
+
const nextContents = plistContents();
|
|
289
|
+
const currentContents = existsSync(plistPath) ? readFileSync(plistPath, 'utf8') : null;
|
|
290
|
+
const serviceLoaded = this.isServiceInstalled();
|
|
291
|
+
const changed = currentContents !== nextContents;
|
|
292
|
+
if (changed) {
|
|
293
|
+
writeFileSync(plistPath, nextContents, 'utf8');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (changed && serviceLoaded) {
|
|
297
|
+
launchctl(['bootout', `${launchDomain}/${label}`]);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (changed || !serviceLoaded) {
|
|
301
|
+
const result = launchctl(['bootstrap', launchDomain, plistPath]);
|
|
302
|
+
if (result.status !== 0) {
|
|
303
|
+
fail('Failed to bootstrap vibelet launch agent.', result.stderr || result.stdout);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
start() {
|
|
309
|
+
const result = launchctl(['kickstart', `${launchDomain}/${label}`]);
|
|
310
|
+
if (result.status !== 0 && !this.isServiceInstalled()) {
|
|
311
|
+
fail('Failed to start vibelet daemon.', result.stderr || result.stdout);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
stop() {
|
|
316
|
+
if (this.isServiceInstalled()) {
|
|
317
|
+
launchctl(['bootout', `${launchDomain}/${label}`]);
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
statusLabel() {
|
|
322
|
+
return this.isServiceInstalled() ? 'loaded' : 'not loaded';
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function createLinuxBackend() {
|
|
328
|
+
const unitName = 'vibelet-daemon.service';
|
|
329
|
+
const unitDir = join(homedir(), '.config', 'systemd', 'user');
|
|
330
|
+
const unitPath = join(unitDir, unitName);
|
|
331
|
+
|
|
332
|
+
function systemctl(args) {
|
|
333
|
+
return spawnSync('systemctl', ['--user', ...args], { encoding: 'utf8' });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function hasSystemd() {
|
|
337
|
+
return spawnSync('systemctl', ['--user', '--version'], { encoding: 'utf8' }).status === 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function unitContents() {
|
|
341
|
+
return `[Unit]
|
|
342
|
+
Description=Vibelet Daemon
|
|
343
|
+
After=network.target
|
|
344
|
+
|
|
345
|
+
[Service]
|
|
346
|
+
ExecStart=${process.execPath} ${runtimeDaemonEntryPath}
|
|
347
|
+
WorkingDirectory=${runtimeCurrentDir}
|
|
348
|
+
Restart=always
|
|
349
|
+
RestartSec=3
|
|
350
|
+
StandardOutput=append:${stdoutLogPath}
|
|
351
|
+
StandardError=append:${stderrLogPath}
|
|
352
|
+
Environment=VIBE_PORT=${port}${process.env.VIBELET_RELAY_URL ? `\nEnvironment=VIBELET_RELAY_URL=${process.env.VIBELET_RELAY_URL}` : ''}${process.env.VIBELET_CANONICAL_HOST ? `\nEnvironment=VIBELET_CANONICAL_HOST=${process.env.VIBELET_CANONICAL_HOST}` : ''}${process.env.VIBELET_FALLBACK_HOSTS ? `\nEnvironment=VIBELET_FALLBACK_HOSTS=${process.env.VIBELET_FALLBACK_HOSTS}` : ''}
|
|
353
|
+
|
|
354
|
+
[Install]
|
|
355
|
+
WantedBy=default.target
|
|
356
|
+
`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Fallback: detached process with PID file (no systemd)
|
|
360
|
+
const fallback = createDetachedBackend();
|
|
361
|
+
|
|
362
|
+
const useSystemd = hasSystemd();
|
|
363
|
+
return {
|
|
364
|
+
name: useSystemd ? 'systemd' : 'detached',
|
|
365
|
+
handlesProcessLifecycle: useSystemd,
|
|
366
|
+
|
|
367
|
+
isServiceInstalled() {
|
|
368
|
+
if (!hasSystemd()) return fallback.isServiceInstalled();
|
|
369
|
+
return systemctl(['is-enabled', unitName]).status === 0;
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
install() {
|
|
373
|
+
if (!hasSystemd()) {
|
|
374
|
+
fallback.install();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
mkdirSync(unitDir, { recursive: true });
|
|
378
|
+
mkdirSync(logDir, { recursive: true });
|
|
379
|
+
const nextContents = unitContents();
|
|
380
|
+
const currentContents = existsSync(unitPath) ? readFileSync(unitPath, 'utf8') : null;
|
|
381
|
+
if (currentContents !== nextContents) {
|
|
382
|
+
writeFileSync(unitPath, nextContents, 'utf8');
|
|
383
|
+
systemctl(['daemon-reload']);
|
|
384
|
+
}
|
|
385
|
+
systemctl(['enable', unitName]);
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
start() {
|
|
389
|
+
if (!hasSystemd()) {
|
|
390
|
+
fallback.start();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const result = systemctl(['start', unitName]);
|
|
394
|
+
if (result.status !== 0) {
|
|
395
|
+
fail('Failed to start vibelet daemon.', result.stderr || result.stdout);
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
stop() {
|
|
400
|
+
if (!hasSystemd()) {
|
|
401
|
+
fallback.stop();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
systemctl(['stop', unitName]);
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
statusLabel() {
|
|
408
|
+
if (!hasSystemd()) return fallback.statusLabel();
|
|
409
|
+
const result = systemctl(['is-active', unitName]);
|
|
410
|
+
return result.stdout?.trim() || 'unknown';
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function createDetachedBackend() {
|
|
416
|
+
return {
|
|
417
|
+
name: 'detached',
|
|
418
|
+
handlesProcessLifecycle: false,
|
|
419
|
+
|
|
420
|
+
isServiceInstalled() {
|
|
421
|
+
const pid = readPidFile();
|
|
422
|
+
return pid !== null && isProcessAlive(pid);
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
install() {
|
|
426
|
+
mkdirSync(logDir, { recursive: true });
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
start() {
|
|
430
|
+
if (this.isServiceInstalled()) return;
|
|
431
|
+
const stdoutFd = openSync(stdoutLogPath, 'a');
|
|
432
|
+
const stderrFd = openSync(stderrLogPath, 'a');
|
|
433
|
+
const child = spawn(process.execPath, [runtimeDaemonEntryPath], {
|
|
434
|
+
detached: true,
|
|
435
|
+
stdio: ['ignore', stdoutFd, stderrFd],
|
|
436
|
+
cwd: runtimeCurrentDir,
|
|
437
|
+
env: { ...process.env, VIBE_PORT: String(port) },
|
|
438
|
+
});
|
|
439
|
+
child.unref();
|
|
440
|
+
writePidFile(child.pid);
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
stop() {
|
|
444
|
+
const pid = readPidFile();
|
|
445
|
+
if (pid && isProcessAlive(pid)) {
|
|
446
|
+
try {
|
|
447
|
+
process.kill(pid, 'SIGTERM');
|
|
448
|
+
} catch { /* already dead */ }
|
|
449
|
+
}
|
|
450
|
+
removePidFile();
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
statusLabel() {
|
|
454
|
+
const pid = readPidFile();
|
|
455
|
+
if (!pid) return 'not running';
|
|
456
|
+
return isProcessAlive(pid) ? `running (pid ${pid})` : 'not running (stale pid)';
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function createWindowsBackend() {
|
|
462
|
+
// Windows: detached process with PID file
|
|
463
|
+
// Node.js detached on Windows creates a new console window — use windowsHide
|
|
464
|
+
return {
|
|
465
|
+
name: 'detached',
|
|
466
|
+
handlesProcessLifecycle: false,
|
|
467
|
+
|
|
468
|
+
isServiceInstalled() {
|
|
469
|
+
const pid = readPidFile();
|
|
470
|
+
return pid !== null && isProcessAlive(pid);
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
install() {
|
|
474
|
+
mkdirSync(logDir, { recursive: true });
|
|
475
|
+
},
|
|
476
|
+
|
|
477
|
+
start() {
|
|
478
|
+
if (this.isServiceInstalled()) return;
|
|
479
|
+
const stdoutFd = openSync(stdoutLogPath, 'a');
|
|
480
|
+
const stderrFd = openSync(stderrLogPath, 'a');
|
|
481
|
+
const child = spawn(process.execPath, [runtimeDaemonEntryPath], {
|
|
482
|
+
detached: true,
|
|
483
|
+
stdio: ['ignore', stdoutFd, stderrFd],
|
|
484
|
+
cwd: runtimeCurrentDir,
|
|
485
|
+
env: { ...process.env, VIBE_PORT: String(port) },
|
|
486
|
+
windowsHide: true,
|
|
487
|
+
});
|
|
488
|
+
child.unref();
|
|
489
|
+
writePidFile(child.pid);
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
stop() {
|
|
493
|
+
const pid = readPidFile();
|
|
494
|
+
if (pid && isProcessAlive(pid)) {
|
|
495
|
+
try {
|
|
496
|
+
process.kill(pid, 'SIGTERM');
|
|
497
|
+
} catch { /* already dead */ }
|
|
498
|
+
}
|
|
499
|
+
removePidFile();
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
statusLabel() {
|
|
503
|
+
const pid = readPidFile();
|
|
504
|
+
if (!pid) return 'not running';
|
|
505
|
+
return isProcessAlive(pid) ? `running (pid ${pid})` : 'not running (stale pid)';
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function resolveBackend() {
|
|
511
|
+
switch (process.platform) {
|
|
512
|
+
case 'darwin': return createDarwinBackend();
|
|
513
|
+
case 'linux': return createLinuxBackend();
|
|
514
|
+
case 'win32': return createWindowsBackend();
|
|
515
|
+
default: return createDetachedBackend();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ─── HTTP helpers ───────────────────────────────────────────────────────────────
|
|
520
|
+
|
|
521
|
+
async function probeHealth(timeoutMs = 0) {
|
|
522
|
+
const deadline = Date.now() + timeoutMs;
|
|
523
|
+
do {
|
|
524
|
+
try {
|
|
525
|
+
const response = await fetch(`http://127.0.0.1:${port}/health`);
|
|
526
|
+
if (response.ok) {
|
|
527
|
+
return await response.json();
|
|
528
|
+
}
|
|
529
|
+
} catch {
|
|
530
|
+
// Retry until timeout.
|
|
531
|
+
}
|
|
532
|
+
if (timeoutMs <= 0) break;
|
|
533
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 250));
|
|
534
|
+
} while (Date.now() < deadline);
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function waitForHealth(timeoutMs = 10_000) {
|
|
539
|
+
const health = await probeHealth(timeoutMs);
|
|
540
|
+
if (health) {
|
|
541
|
+
return health;
|
|
542
|
+
}
|
|
543
|
+
fail(`Timed out waiting for vibelet daemon on port ${port}.`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function postJson(pathname, body = undefined) {
|
|
547
|
+
const response = await fetch(`http://127.0.0.1:${port}${pathname}`, {
|
|
548
|
+
method: 'POST',
|
|
549
|
+
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
|
550
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
551
|
+
});
|
|
552
|
+
const payload = await response.json().catch(() => ({}));
|
|
553
|
+
if (!response.ok) {
|
|
554
|
+
fail(`Request to ${pathname} failed.`, JSON.stringify(payload, null, 2));
|
|
555
|
+
}
|
|
556
|
+
return payload;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function requestShutdown() {
|
|
560
|
+
try {
|
|
561
|
+
await fetch(`http://127.0.0.1:${port}/shutdown`, { method: 'POST' });
|
|
562
|
+
// Wait for daemon to actually stop
|
|
563
|
+
const deadline = Date.now() + 5_000;
|
|
564
|
+
while (Date.now() < deadline) {
|
|
565
|
+
const health = await probeHealth(0);
|
|
566
|
+
if (!health) return true;
|
|
567
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
568
|
+
}
|
|
569
|
+
return false;
|
|
570
|
+
} catch {
|
|
571
|
+
return true; // Already dead
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function isDaemonStartCommand(command) {
|
|
576
|
+
return command === 'default' || command === 'start' || command === 'restart' || command === 'reset';
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async function stopRunningDaemon(backend) {
|
|
580
|
+
await requestShutdown();
|
|
581
|
+
backend.stop();
|
|
582
|
+
const stillAlive = await probeHealth(5_000);
|
|
583
|
+
if (stillAlive) {
|
|
584
|
+
fail('Daemon did not stop in time.');
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function createCompactPairingPayload(pairingPayload) {
|
|
589
|
+
const compactPayload = {
|
|
590
|
+
t: 'vp',
|
|
591
|
+
d: pairingPayload.daemonId,
|
|
592
|
+
n: pairingPayload.displayName,
|
|
593
|
+
h: pairingPayload.canonicalHost,
|
|
594
|
+
p: pairingPayload.port,
|
|
595
|
+
c: pairingPayload.pairNonce,
|
|
596
|
+
e: pairingPayload.expiresAt,
|
|
597
|
+
};
|
|
598
|
+
if (pairingPayload.fallbackHosts) compactPayload.f = pairingPayload.fallbackHosts;
|
|
599
|
+
return compactPayload;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async function printPairingQr(pairingPayload) {
|
|
603
|
+
const payload = JSON.stringify(createCompactPairingPayload(pairingPayload));
|
|
604
|
+
mkdirSync(vibeletDir, { recursive: true });
|
|
605
|
+
await QRCode.toFile(pairingQrPngPath, payload, {
|
|
606
|
+
type: 'png',
|
|
607
|
+
errorCorrectionLevel: 'M',
|
|
608
|
+
margin: 1,
|
|
609
|
+
scale: 8,
|
|
610
|
+
});
|
|
611
|
+
const qr = await QRCode.toString(payload, {
|
|
612
|
+
type: 'terminal',
|
|
613
|
+
small: true,
|
|
614
|
+
errorCorrectionLevel: 'M',
|
|
615
|
+
});
|
|
616
|
+
process.stdout.write(`\nScan this QR code with the Vibelet app:\n\n${qr}\n`);
|
|
617
|
+
process.stdout.write(`If Vibelet app can't scan the terminal QR, open this PNG instead:\n${pairingQrPngPath}\n`);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ─── Commands ───────────────────────────────────────────────────────────────────
|
|
621
|
+
|
|
622
|
+
async function printPairingSummary(existingHealth = null) {
|
|
623
|
+
const health = existingHealth ?? await waitForHealth();
|
|
624
|
+
const pairingPayload = await postJson('/pair/open');
|
|
625
|
+
|
|
626
|
+
process.stdout.write(`Vibelet daemon is ready.\n\n`);
|
|
627
|
+
process.stdout.write(`Device: ${health.displayName}\n`);
|
|
628
|
+
process.stdout.write(`Daemon ID: ${health.daemonId}\n`);
|
|
629
|
+
process.stdout.write(`Host: ${pairingPayload.canonicalHost}\n`);
|
|
630
|
+
process.stdout.write(`Port: ${pairingPayload.port}\n`);
|
|
631
|
+
process.stdout.write(`Paired devices: ${health.pairedDevices}\n`);
|
|
632
|
+
await printPairingQr(pairingPayload);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function printHelp() {
|
|
636
|
+
process.stdout.write(`Vibelet ${packageJson.version}\n\n`);
|
|
637
|
+
process.stdout.write(`Package names:\n`);
|
|
638
|
+
process.stdout.write(` @vibelet/cli\n`);
|
|
639
|
+
process.stdout.write(` vibelet\n\n`);
|
|
640
|
+
process.stdout.write(`Usage:\n`);
|
|
641
|
+
process.stdout.write(` npx ${packageJson.name} Install/start the daemon, auto-enable remote access, and print a pairing QR code\n`);
|
|
642
|
+
process.stdout.write(` npx ${packageJson.name} start Same as above\n`);
|
|
643
|
+
process.stdout.write(` npx ${packageJson.name} --local Skip the default Cloudflare Tunnel for this run\n`);
|
|
644
|
+
process.stdout.write(` npx ${packageJson.name} --remote --force Force a new Cloudflare Tunnel URL\n`);
|
|
645
|
+
process.stdout.write(` npx ${packageJson.name} --relay <url> Use a custom tunnel URL for remote access\n`);
|
|
646
|
+
process.stdout.write(` npx ${packageJson.name} --host <ip> Set the primary host/IP address\n`);
|
|
647
|
+
process.stdout.write(` npx ${packageJson.name} --fallback-hosts <ips> Comma-separated fallback IPs\n`);
|
|
648
|
+
process.stdout.write(` npx ${packageJson.name} stop Stop the daemon\n`);
|
|
649
|
+
process.stdout.write(` npx ${packageJson.name} restart Restart the daemon\n`);
|
|
650
|
+
process.stdout.write(` npx ${packageJson.name} status Show service and daemon status\n`);
|
|
651
|
+
process.stdout.write(` npx ${packageJson.name} logs Print recent daemon logs\n`);
|
|
652
|
+
process.stdout.write(` npx ${packageJson.name} reset Reset pairings and print a fresh QR code\n`);
|
|
653
|
+
process.stdout.write(` npx ${packageJson.name} --help Show this help text\n`);
|
|
654
|
+
process.stdout.write(` npx ${packageJson.name} --version Show the installed CLI version\n`);
|
|
655
|
+
process.stdout.write(`\n`);
|
|
656
|
+
process.stdout.write(`You can also invoke the published alias with:\n`);
|
|
657
|
+
process.stdout.write(` npx ${packageJson.name === 'vibelet' ? '@vibelet/cli' : 'vibelet'}\n`);
|
|
658
|
+
process.stdout.write(` vibelet\n\n`);
|
|
659
|
+
process.stdout.write(`Remote access:\n`);
|
|
660
|
+
process.stdout.write(` # Remote access is on by default for start/reset/restart\n`);
|
|
661
|
+
process.stdout.write(` npx ${packageJson.name}\n\n`);
|
|
662
|
+
process.stdout.write(` # Want LAN-only pairing for this run?\n`);
|
|
663
|
+
process.stdout.write(` npx ${packageJson.name} --local\n\n`);
|
|
664
|
+
process.stdout.write(` # Need a fresh Cloudflare Tunnel URL?\n`);
|
|
665
|
+
process.stdout.write(` npx ${packageJson.name} --remote --force\n\n`);
|
|
666
|
+
process.stdout.write(` # Or bring your own tunnel and pass the URL manually:\n`);
|
|
667
|
+
process.stdout.write(` npx cloudflared tunnel --protocol http2 --url http://localhost:${port}\n`);
|
|
668
|
+
process.stdout.write(` ngrok http ${port}\n`);
|
|
669
|
+
process.stdout.write(` npx ${packageJson.name} --relay=https://<your-tunnel-url>\n\n`);
|
|
670
|
+
process.stdout.write(` # Tailscale (P2P VPN, no tunnel needed)\n`);
|
|
671
|
+
process.stdout.write(` npx ${packageJson.name} --host=<tailscale-ip>\n`);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function consumeFlag(name) {
|
|
675
|
+
const idx = process.argv.indexOf(`--${name}`);
|
|
676
|
+
if (idx === -1) return false;
|
|
677
|
+
process.argv.splice(idx, 1);
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function readNpmConfigFlag(name) {
|
|
682
|
+
const value = process.env[`npm_config_${name.replace(/-/g, '_')}`];
|
|
683
|
+
return value === '' || value === 'true';
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function parseNamedArg(name, errorHint) {
|
|
687
|
+
const inlinePrefix = `--${name}=`;
|
|
688
|
+
const inlineArg = process.argv.find((arg) => arg.startsWith(inlinePrefix));
|
|
689
|
+
if (inlineArg) {
|
|
690
|
+
const idx = process.argv.indexOf(inlineArg);
|
|
691
|
+
process.argv.splice(idx, 1);
|
|
692
|
+
return inlineArg.slice(inlinePrefix.length);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const idx = process.argv.indexOf(`--${name}`);
|
|
696
|
+
if (idx === -1) return null;
|
|
697
|
+
const value = process.argv[idx + 1];
|
|
698
|
+
if (!value || value.startsWith('-')) {
|
|
699
|
+
fail(`--${name} requires an argument${errorHint ? ` (e.g. --${name} ${errorHint})` : ''}`);
|
|
700
|
+
}
|
|
701
|
+
process.argv.splice(idx, 2);
|
|
702
|
+
return value;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function parseRelayArg() {
|
|
706
|
+
return parseNamedArg('relay', 'https://abc.trycloudflare.com');
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function loadRelayConfig() {
|
|
710
|
+
try {
|
|
711
|
+
const data = JSON.parse(readFileSync(relayConfigPath, 'utf8'));
|
|
712
|
+
return data.relayUrl || '';
|
|
713
|
+
} catch {
|
|
714
|
+
return '';
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function saveRelayConfig(relayUrl) {
|
|
719
|
+
mkdirSync(vibeletDir, { recursive: true });
|
|
720
|
+
writeFileSync(relayConfigPath, JSON.stringify({ relayUrl }, null, 2) + '\n', 'utf8');
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function clearRelayConfig() {
|
|
724
|
+
rmSync(relayConfigPath, { force: true });
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// ─── Tunnel management ──────────────────────────────────────────────────────────
|
|
728
|
+
|
|
729
|
+
function loadTunnelState() {
|
|
730
|
+
try {
|
|
731
|
+
return JSON.parse(readFileSync(tunnelStatePath, 'utf8'));
|
|
732
|
+
} catch {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function saveTunnelState(pid, url) {
|
|
738
|
+
mkdirSync(vibeletDir, { recursive: true });
|
|
739
|
+
writeFileSync(tunnelStatePath, JSON.stringify({ pid, url }, null, 2) + '\n', 'utf8');
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function clearTunnelState() {
|
|
743
|
+
rmSync(tunnelStatePath, { force: true });
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function stopTunnel() {
|
|
747
|
+
const state = loadTunnelState();
|
|
748
|
+
if (state?.pid && isProcessAlive(state.pid)) {
|
|
749
|
+
try { process.kill(state.pid, 'SIGTERM'); } catch { /* already dead */ }
|
|
750
|
+
}
|
|
751
|
+
clearTunnelState();
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function getAliveTunnel() {
|
|
755
|
+
const state = loadTunnelState();
|
|
756
|
+
if (state?.pid && state?.url && isProcessAlive(state.pid)) {
|
|
757
|
+
return state;
|
|
758
|
+
}
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function readCloudflaredLog(logPath) {
|
|
763
|
+
try {
|
|
764
|
+
return readFileSync(logPath, 'utf8');
|
|
765
|
+
} catch {
|
|
766
|
+
return '';
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function startTunnel() {
|
|
771
|
+
return new Promise((resolve, reject) => {
|
|
772
|
+
const logPath = join(logDir, 'tunnel.stderr.log');
|
|
773
|
+
mkdirSync(logDir, { recursive: true });
|
|
774
|
+
|
|
775
|
+
const launchSpec = resolveCloudflaredLaunchSpec();
|
|
776
|
+
|
|
777
|
+
// Strategy: start cloudflared with output to log files (so it survives detach),
|
|
778
|
+
// then tail the log to capture the URL.
|
|
779
|
+
// Truncate log so we don't match a stale URL from a previous run.
|
|
780
|
+
writeFileSync(logPath, '', 'utf8');
|
|
781
|
+
const logFd = openSync(logPath, 'a');
|
|
782
|
+
const child = spawn(launchSpec.command, [
|
|
783
|
+
...launchSpec.args,
|
|
784
|
+
'tunnel',
|
|
785
|
+
'--protocol',
|
|
786
|
+
'http2',
|
|
787
|
+
'--url',
|
|
788
|
+
`http://localhost:${port}`,
|
|
789
|
+
], {
|
|
790
|
+
detached: true,
|
|
791
|
+
stdio: ['ignore', logFd, logFd],
|
|
792
|
+
});
|
|
793
|
+
child.unref();
|
|
794
|
+
|
|
795
|
+
const pid = child.pid;
|
|
796
|
+
let url = null;
|
|
797
|
+
|
|
798
|
+
const timeout = setTimeout(() => {
|
|
799
|
+
if (!url) {
|
|
800
|
+
try { process.kill(pid, 'SIGTERM'); } catch { /* */ }
|
|
801
|
+
reject(new Error(formatCloudflaredFailureMessage({
|
|
802
|
+
launchSpec,
|
|
803
|
+
logContent: readCloudflaredLog(logPath),
|
|
804
|
+
logPath,
|
|
805
|
+
phase: 'timeout',
|
|
806
|
+
})));
|
|
807
|
+
}
|
|
808
|
+
}, launchSpec.urlTimeoutMs);
|
|
809
|
+
|
|
810
|
+
// Poll the log file for the tunnel URL
|
|
811
|
+
const poll = setInterval(() => {
|
|
812
|
+
try {
|
|
813
|
+
const content = readFileSync(logPath, 'utf8');
|
|
814
|
+
const tunnelUrl = extractQuickTunnelUrl(content);
|
|
815
|
+
if (tunnelUrl) {
|
|
816
|
+
url = tunnelUrl;
|
|
817
|
+
clearInterval(poll);
|
|
818
|
+
clearTimeout(timeout);
|
|
819
|
+
saveTunnelState(pid, url);
|
|
820
|
+
resolve({ pid, url });
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
// Check if process died before producing URL
|
|
824
|
+
if (!isProcessAlive(pid)) {
|
|
825
|
+
clearInterval(poll);
|
|
826
|
+
clearTimeout(timeout);
|
|
827
|
+
reject(new Error(formatCloudflaredFailureMessage({
|
|
828
|
+
launchSpec,
|
|
829
|
+
logContent: content,
|
|
830
|
+
logPath,
|
|
831
|
+
phase: 'exit',
|
|
832
|
+
})));
|
|
833
|
+
}
|
|
834
|
+
} catch { /* file not ready yet */ }
|
|
835
|
+
}, 300);
|
|
836
|
+
|
|
837
|
+
child.on('error', (err) => {
|
|
838
|
+
clearInterval(poll);
|
|
839
|
+
clearTimeout(timeout);
|
|
840
|
+
reject(new Error(formatCloudflaredFailureMessage({
|
|
841
|
+
launchSpec,
|
|
842
|
+
logContent: readCloudflaredLog(logPath),
|
|
843
|
+
logPath,
|
|
844
|
+
err,
|
|
845
|
+
phase: 'spawn',
|
|
846
|
+
})));
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async function main() {
|
|
852
|
+
// Update check: read cached result (sync, instant) and spawn background fetch.
|
|
853
|
+
checkForUpdateFromCache();
|
|
854
|
+
fetchLatestVersionInBackground();
|
|
855
|
+
|
|
856
|
+
consumeFlag('remote') || consumeFlag('tunnel') || readNpmConfigFlag('remote') || readNpmConfigFlag('tunnel');
|
|
857
|
+
const localFlag = consumeFlag('local') || readNpmConfigFlag('local');
|
|
858
|
+
const forceFlag = consumeFlag('force') || readNpmConfigFlag('force');
|
|
859
|
+
const relayArg = parseRelayArg();
|
|
860
|
+
const hostArg = parseNamedArg('host', '100.x.x.x');
|
|
861
|
+
const fallbackHostsArg = parseNamedArg('fallback-hosts', '100.x.x.x,192.168.1.x');
|
|
862
|
+
const command = process.argv[2] ?? 'default';
|
|
863
|
+
const startCommand = isDaemonStartCommand(command);
|
|
864
|
+
// --remote/--tunnel remain accepted for compatibility, but startup commands
|
|
865
|
+
// now default to managed remote access unless another connection target wins.
|
|
866
|
+
const shouldManageTunnel = startCommand
|
|
867
|
+
&& !localFlag
|
|
868
|
+
&& relayArg === null
|
|
869
|
+
&& !hostArg
|
|
870
|
+
&& !fallbackHostsArg;
|
|
871
|
+
|
|
872
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
873
|
+
printHelp();
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (command === '--version' || command === '-v' || command === 'version') {
|
|
878
|
+
process.stdout.write(`${packageJson.version}\n`);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (shouldManageTunnel) {
|
|
883
|
+
const existing = forceFlag ? null : getAliveTunnel();
|
|
884
|
+
if (existing) {
|
|
885
|
+
process.stdout.write(`Reusing tunnel: ${existing.url} (pid ${existing.pid})\n`);
|
|
886
|
+
saveRelayConfig(existing.url);
|
|
887
|
+
} else {
|
|
888
|
+
if (forceFlag) stopTunnel();
|
|
889
|
+
process.stdout.write('Starting Cloudflare Tunnel...\n');
|
|
890
|
+
try {
|
|
891
|
+
const tunnel = await startTunnel();
|
|
892
|
+
process.stdout.write(`Tunnel ready: ${tunnel.url}\n`);
|
|
893
|
+
saveRelayConfig(tunnel.url);
|
|
894
|
+
} catch (err) {
|
|
895
|
+
fail(err.message);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// --relay "" clears saved relay; --relay <url> saves it; omitted uses saved value
|
|
901
|
+
if (relayArg !== null) {
|
|
902
|
+
if (relayArg) {
|
|
903
|
+
saveRelayConfig(relayArg);
|
|
904
|
+
} else {
|
|
905
|
+
clearRelayConfig();
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
const shouldIgnoreSavedRelay = relayArg === null && (localFlag || Boolean(hostArg) || Boolean(fallbackHostsArg));
|
|
909
|
+
const relayUrl = relayArg !== null
|
|
910
|
+
? relayArg
|
|
911
|
+
: (shouldIgnoreSavedRelay ? '' : loadRelayConfig());
|
|
912
|
+
if (relayUrl) {
|
|
913
|
+
process.env.VIBELET_RELAY_URL = relayUrl;
|
|
914
|
+
} else {
|
|
915
|
+
delete process.env.VIBELET_RELAY_URL;
|
|
916
|
+
}
|
|
917
|
+
if (hostArg) {
|
|
918
|
+
process.env.VIBELET_CANONICAL_HOST = hostArg;
|
|
919
|
+
} else {
|
|
920
|
+
delete process.env.VIBELET_CANONICAL_HOST;
|
|
921
|
+
}
|
|
922
|
+
if (fallbackHostsArg) {
|
|
923
|
+
process.env.VIBELET_FALLBACK_HOSTS = fallbackHostsArg;
|
|
924
|
+
} else {
|
|
925
|
+
delete process.env.VIBELET_FALLBACK_HOSTS;
|
|
926
|
+
}
|
|
927
|
+
const backend = resolveBackend();
|
|
928
|
+
|
|
929
|
+
if (command === 'stop') {
|
|
930
|
+
process.stdout.write('Stopping vibelet daemon...\n');
|
|
931
|
+
// Always try graceful HTTP shutdown first — gives the daemon time to
|
|
932
|
+
// close sessions and flush logs before the service manager kills it.
|
|
933
|
+
await stopRunningDaemon(backend);
|
|
934
|
+
// Also stop tunnel if running
|
|
935
|
+
const tunnelState = getAliveTunnel();
|
|
936
|
+
if (tunnelState) {
|
|
937
|
+
stopTunnel();
|
|
938
|
+
process.stdout.write('Tunnel stopped.\n');
|
|
939
|
+
}
|
|
940
|
+
process.stdout.write('Daemon stopped.\n');
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (command === 'status') {
|
|
945
|
+
process.stdout.write(`Service (${backend.name}): ${backend.statusLabel()}\n`);
|
|
946
|
+
process.stdout.write(`Runtime: ${existsSync(runtimeDaemonEntryPath) ? runtimeDaemonEntryPath : 'not installed'}\n`);
|
|
947
|
+
const tunnelState = getAliveTunnel();
|
|
948
|
+
if (tunnelState) {
|
|
949
|
+
process.stdout.write(`Tunnel: ${tunnelState.url} (pid ${tunnelState.pid})\n`);
|
|
950
|
+
}
|
|
951
|
+
const savedRelay = loadRelayConfig();
|
|
952
|
+
if (savedRelay) {
|
|
953
|
+
process.stdout.write(`Relay: ${savedRelay}\n`);
|
|
954
|
+
}
|
|
955
|
+
const health = await probeHealth(1_500);
|
|
956
|
+
if (health) {
|
|
957
|
+
process.stdout.write(JSON.stringify(health, null, 2) + '\n');
|
|
958
|
+
} else {
|
|
959
|
+
process.stdout.write('Daemon is not responding.\n');
|
|
960
|
+
}
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (command === 'logs') {
|
|
965
|
+
process.stdout.write(`Stdout: ${stdoutLogPath}\n`);
|
|
966
|
+
process.stdout.write(`Stderr: ${stderrLogPath}\n\n`);
|
|
967
|
+
const logFiles = [stdoutLogPath, stderrLogPath].filter((filePath) => existsSync(filePath));
|
|
968
|
+
if (logFiles.length === 0) {
|
|
969
|
+
process.stdout.write('No daemon logs have been written yet.\n');
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (process.platform === 'win32') {
|
|
973
|
+
for (const logFile of logFiles) {
|
|
974
|
+
const content = readFileSync(logFile, 'utf8');
|
|
975
|
+
const lines = content.split('\n').slice(-80).join('\n');
|
|
976
|
+
process.stdout.write(`==> ${logFile} <==\n${lines}\n`);
|
|
977
|
+
}
|
|
978
|
+
} else {
|
|
979
|
+
const result = spawnSync('tail', ['-n', '80', ...logFiles], { encoding: 'utf8' });
|
|
980
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
981
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
982
|
+
}
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (command === 'restart') {
|
|
987
|
+
process.stdout.write('Restarting vibelet daemon...\n');
|
|
988
|
+
await stopRunningDaemon(backend);
|
|
989
|
+
process.stdout.write('Daemon stopped. Starting...\n');
|
|
990
|
+
ensureRuntimeInstalled();
|
|
991
|
+
backend.install();
|
|
992
|
+
backend.start();
|
|
993
|
+
await printPairingSummary();
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (command === 'reset') {
|
|
998
|
+
const healthyDaemon = await probeHealth(1_500);
|
|
999
|
+
const hasExplicitConfigOverrides = !doesHealthMatchRequestedConnectionConfig({
|
|
1000
|
+
health: healthyDaemon,
|
|
1001
|
+
relayUrl,
|
|
1002
|
+
canonicalHost: hostArg || '',
|
|
1003
|
+
fallbackHosts: fallbackHostsArg || '',
|
|
1004
|
+
localMode: localFlag,
|
|
1005
|
+
}) && (localFlag || Boolean(relayUrl) || Boolean(hostArg) || Boolean(fallbackHostsArg));
|
|
1006
|
+
if (healthyDaemon && hasExplicitConfigOverrides) {
|
|
1007
|
+
await stopRunningDaemon(backend);
|
|
1008
|
+
}
|
|
1009
|
+
ensureRuntimeInstalled();
|
|
1010
|
+
backend.install();
|
|
1011
|
+
backend.start();
|
|
1012
|
+
await waitForHealth();
|
|
1013
|
+
await postJson('/pair/reset');
|
|
1014
|
+
await printPairingSummary();
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (command !== 'default' && command !== 'start') {
|
|
1019
|
+
printHelp();
|
|
1020
|
+
fail(`Unknown command: ${command}`);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const healthyDaemon = await probeHealth(1_500);
|
|
1024
|
+
const hasExplicitConfigOverrides = !doesHealthMatchRequestedConnectionConfig({
|
|
1025
|
+
health: healthyDaemon,
|
|
1026
|
+
relayUrl,
|
|
1027
|
+
canonicalHost: hostArg || '',
|
|
1028
|
+
fallbackHosts: fallbackHostsArg || '',
|
|
1029
|
+
localMode: localFlag,
|
|
1030
|
+
}) && (localFlag || Boolean(relayUrl) || Boolean(hostArg) || Boolean(fallbackHostsArg));
|
|
1031
|
+
const existingHealth = shouldReuseHealthyDaemon({
|
|
1032
|
+
command,
|
|
1033
|
+
daemonHealthy: Boolean(healthyDaemon),
|
|
1034
|
+
hasExplicitConfigOverrides,
|
|
1035
|
+
}) ? healthyDaemon : null;
|
|
1036
|
+
|
|
1037
|
+
if (existingHealth) {
|
|
1038
|
+
process.stdout.write('Vibelet daemon is already running.\n');
|
|
1039
|
+
process.stdout.write('Reusing the current runtime so active sessions stay alive.\n');
|
|
1040
|
+
process.stdout.write('Run `npx vibelet restart` to apply freshly built daemon code.\n\n');
|
|
1041
|
+
await printPairingSummary(existingHealth);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (healthyDaemon && hasExplicitConfigOverrides) {
|
|
1046
|
+
await stopRunningDaemon(backend);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
ensureRuntimeInstalled();
|
|
1050
|
+
backend.install();
|
|
1051
|
+
backend.start();
|
|
1052
|
+
await printPairingSummary();
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
process.on('SIGINT', () => process.exit(0));
|
|
1056
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
1057
|
+
process.on('exit', printOfficialSite);
|
|
1058
|
+
await main();
|
|
1059
|
+
// On Windows + Node 24, process.exit(0) can trigger a libuv assertion
|
|
1060
|
+
// (UV_HANDLE_CLOSING) when detached child handles haven't fully released.
|
|
1061
|
+
// Letting the event loop drain naturally avoids this.
|
|
1062
|
+
if (process.platform !== 'win32') process.exit(0);
|