vibelet 1.0.10 → 1.0.12
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/vibelet.mjs +185 -25
- package/dist/index.cjs +52 -48
- package/package.json +1 -19
package/bin/vibelet.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, openSync, writeSync } from 'node:fs';
|
|
4
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, openSync, writeSync } from 'node:fs';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
|
-
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { basename, delimiter, dirname, join, resolve } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import QRCode from 'qrcode';
|
|
9
9
|
import { extractQuickTunnelUrl } from './cloudflared-quick-tunnel.mjs';
|
|
10
10
|
import { formatCloudflaredFailureMessage, resolveCloudflaredLaunchSpec } from './cloudflared-resolver.mjs';
|
|
11
|
+
import { canUseSystemdUserManager, isSystemdUserManagerUnavailable } from './linux-systemd.mjs';
|
|
11
12
|
import { doesHealthMatchRequestedConnectionConfig, shouldReuseHealthyDaemon } from './vibelet-runtime-policy.mjs';
|
|
12
13
|
|
|
13
14
|
// ─── Paths & constants ─────────────────────────────────────────────────────────
|
|
@@ -16,7 +17,6 @@ const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
16
17
|
const packageJson = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf8'));
|
|
17
18
|
const daemonDistDir = resolve(rootDir, 'dist');
|
|
18
19
|
const daemonEntryPath = resolve(daemonDistDir, 'index.cjs');
|
|
19
|
-
const port = Number(process.env.VIBE_PORT) || 9876;
|
|
20
20
|
const vibeletDir = join(homedir(), '.vibelet');
|
|
21
21
|
const pairingQrPngPath = join(vibeletDir, 'pairing-qr.png');
|
|
22
22
|
const logDir = join(vibeletDir, 'logs');
|
|
@@ -35,6 +35,22 @@ const UPDATE_CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
|
35
35
|
|
|
36
36
|
let officialSitePrinted = false;
|
|
37
37
|
let updateMessage = '';
|
|
38
|
+
let port = normalizePortValue(process.env.VIBE_PORT) ?? 9876;
|
|
39
|
+
|
|
40
|
+
function normalizePortValue(rawValue) {
|
|
41
|
+
if (typeof rawValue !== 'string' && typeof rawValue !== 'number') {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const normalized = String(rawValue).trim();
|
|
45
|
+
if (!/^\d+$/.test(normalized)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const parsed = Number(normalized);
|
|
49
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return parsed;
|
|
53
|
+
}
|
|
38
54
|
|
|
39
55
|
function printOfficialSite() {
|
|
40
56
|
if (officialSitePrinted) return;
|
|
@@ -147,7 +163,7 @@ function readRuntimeMetadata() {
|
|
|
147
163
|
|
|
148
164
|
function ensureRuntimeInstalled() {
|
|
149
165
|
if (!existsSync(daemonEntryPath)) {
|
|
150
|
-
fail('The compiled daemon runtime is missing.', 'Run `pnpm build` before invoking `npx @vibelet/cli` from a source checkout.');
|
|
166
|
+
fail('The compiled daemon runtime is missing.', 'Run `pnpm build:release` before invoking `npx @vibelet/cli` from a source checkout.');
|
|
151
167
|
}
|
|
152
168
|
|
|
153
169
|
const sourceDaemonStat = statSync(daemonEntryPath);
|
|
@@ -218,6 +234,75 @@ function isProcessAlive(pid) {
|
|
|
218
234
|
}
|
|
219
235
|
}
|
|
220
236
|
|
|
237
|
+
// ─── Service PATH helpers ──────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
// Transient shims like fnm_multishells/<pid>/bin disappear or get GC'd after the
|
|
240
|
+
// shell that created them exits. Long-lived services (launchd, systemd) that
|
|
241
|
+
// inherit these paths end up spawning wrapper scripts whose `exec node` fails
|
|
242
|
+
// with "node: not found". Stabilize to the underlying stable dir/file.
|
|
243
|
+
function isTransientNodePath(p) {
|
|
244
|
+
return typeof p === 'string' && p.includes('fnm_multishells');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function stabilizeServiceFile(p) {
|
|
248
|
+
if (!p || !isTransientNodePath(p)) return p;
|
|
249
|
+
try {
|
|
250
|
+
const stable = join(realpathSync(dirname(p)), basename(p));
|
|
251
|
+
if (existsSync(stable) && !isTransientNodePath(stable)) return stable;
|
|
252
|
+
} catch { /* broken symlink */ }
|
|
253
|
+
return p;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function stabilizeServiceDir(dir) {
|
|
257
|
+
if (!dir || !isTransientNodePath(dir)) return dir;
|
|
258
|
+
try {
|
|
259
|
+
const real = realpathSync(dir);
|
|
260
|
+
if (real && !isTransientNodePath(real)) return real;
|
|
261
|
+
} catch { /* broken symlink */ }
|
|
262
|
+
return dir;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Build a PATH the daemon can rely on under launchd/systemd, where no login
|
|
266
|
+
// shell ran and no rc files loaded. Puts the node bin dir first so wrapper
|
|
267
|
+
// scripts spawned by the daemon (codex, claude) can resolve `node` via PATH,
|
|
268
|
+
// then falls back to common system and user-tool locations.
|
|
269
|
+
function buildServicePath(nodeBinDir) {
|
|
270
|
+
const home = homedir();
|
|
271
|
+
const pnpmHome = process.env.PNPM_HOME
|
|
272
|
+
|| (process.platform === 'win32' ? join(home, 'pnpm') : join(home, 'Library', 'pnpm'));
|
|
273
|
+
const baseline = process.platform === 'win32'
|
|
274
|
+
? [
|
|
275
|
+
nodeBinDir,
|
|
276
|
+
join(home, '.npm-global', 'bin'),
|
|
277
|
+
join(home, '.local', 'bin'),
|
|
278
|
+
join(home, '.claude', 'bin'),
|
|
279
|
+
pnpmHome,
|
|
280
|
+
]
|
|
281
|
+
: [
|
|
282
|
+
nodeBinDir,
|
|
283
|
+
'/usr/local/bin',
|
|
284
|
+
'/opt/homebrew/bin',
|
|
285
|
+
'/usr/bin',
|
|
286
|
+
'/bin',
|
|
287
|
+
'/usr/sbin',
|
|
288
|
+
'/sbin',
|
|
289
|
+
join(home, '.npm-global', 'bin'),
|
|
290
|
+
join(home, '.local', 'bin'),
|
|
291
|
+
join(home, '.claude', 'bin'),
|
|
292
|
+
pnpmHome,
|
|
293
|
+
];
|
|
294
|
+
const current = (process.env.PATH || '')
|
|
295
|
+
.split(delimiter)
|
|
296
|
+
.filter(Boolean)
|
|
297
|
+
.map(stabilizeServiceDir);
|
|
298
|
+
const out = [];
|
|
299
|
+
const seen = new Set();
|
|
300
|
+
for (const p of [...baseline, ...current]) {
|
|
301
|
+
if (p && !seen.has(p)) { out.push(p); seen.add(p); }
|
|
302
|
+
}
|
|
303
|
+
return out.join(delimiter);
|
|
304
|
+
}
|
|
305
|
+
|
|
221
306
|
// ─── Platform service backends ──────────────────────────────────────────────────
|
|
222
307
|
|
|
223
308
|
function createDarwinBackend() {
|
|
@@ -232,12 +317,20 @@ function createDarwinBackend() {
|
|
|
232
317
|
}
|
|
233
318
|
|
|
234
319
|
function plistContents() {
|
|
320
|
+
// Prefer a stable node path — fnm_multishells shims disappear when the
|
|
321
|
+
// installing shell exits, leaving launchd unable to respawn the daemon.
|
|
322
|
+
const nodeBin = stabilizeServiceFile(process.execPath);
|
|
235
323
|
const programArgs = [
|
|
236
|
-
|
|
324
|
+
nodeBin,
|
|
237
325
|
runtimeDaemonEntryPath,
|
|
238
326
|
].map((value) => ` <string>${value}</string>`).join('\n');
|
|
239
327
|
|
|
240
|
-
|
|
328
|
+
// launchd starts the daemon with a bare PATH; inject a rich one so child
|
|
329
|
+
// CLIs like codex (a sh wrapper that does `exec node`) can find node.
|
|
330
|
+
const envVars = {
|
|
331
|
+
PATH: buildServicePath(dirname(nodeBin)),
|
|
332
|
+
VIBE_PORT: String(port),
|
|
333
|
+
};
|
|
241
334
|
if (process.env.VIBELET_RELAY_URL) envVars.VIBELET_RELAY_URL = process.env.VIBELET_RELAY_URL;
|
|
242
335
|
if (process.env.VIBELET_CANONICAL_HOST) envVars.VIBELET_CANONICAL_HOST = process.env.VIBELET_CANONICAL_HOST;
|
|
243
336
|
if (process.env.VIBELET_FALLBACK_HOSTS) envVars.VIBELET_FALLBACK_HOSTS = process.env.VIBELET_FALLBACK_HOSTS;
|
|
@@ -342,22 +435,35 @@ function createLinuxBackend() {
|
|
|
342
435
|
return spawnSync('systemctl', ['--user', ...args], { encoding: 'utf8' });
|
|
343
436
|
}
|
|
344
437
|
|
|
345
|
-
function
|
|
346
|
-
return
|
|
438
|
+
function resultOutput(result) {
|
|
439
|
+
return [result?.stderr, result?.stdout].filter(Boolean).join('\n').trim();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
let useSystemd = canUseSystemdUserManager();
|
|
443
|
+
|
|
444
|
+
function demoteToDetachedIfSystemdUnavailable(result) {
|
|
445
|
+
if (!isSystemdUserManagerUnavailable(resultOutput(result))) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
useSystemd = false;
|
|
449
|
+
return true;
|
|
347
450
|
}
|
|
348
451
|
|
|
349
452
|
function unitContents() {
|
|
453
|
+
const nodeBin = stabilizeServiceFile(process.execPath);
|
|
454
|
+
const servicePath = buildServicePath(dirname(nodeBin));
|
|
350
455
|
return `[Unit]
|
|
351
456
|
Description=Vibelet Daemon
|
|
352
457
|
After=network.target
|
|
353
458
|
|
|
354
459
|
[Service]
|
|
355
|
-
ExecStart=${
|
|
460
|
+
ExecStart=${nodeBin} ${runtimeDaemonEntryPath}
|
|
356
461
|
WorkingDirectory=${runtimeCurrentDir}
|
|
357
462
|
Restart=always
|
|
358
463
|
RestartSec=3
|
|
359
464
|
StandardOutput=append:${stdoutLogPath}
|
|
360
465
|
StandardError=append:${stderrLogPath}
|
|
466
|
+
Environment=PATH=${servicePath}
|
|
361
467
|
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}` : ''}
|
|
362
468
|
|
|
363
469
|
[Install]
|
|
@@ -368,18 +474,26 @@ WantedBy=default.target
|
|
|
368
474
|
// Fallback: detached process with PID file (no systemd)
|
|
369
475
|
const fallback = createDetachedBackend();
|
|
370
476
|
|
|
371
|
-
const useSystemd = hasSystemd();
|
|
372
477
|
return {
|
|
373
|
-
name
|
|
374
|
-
|
|
478
|
+
get name() {
|
|
479
|
+
return useSystemd ? 'systemd' : 'detached';
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
get handlesProcessLifecycle() {
|
|
483
|
+
return useSystemd;
|
|
484
|
+
},
|
|
375
485
|
|
|
376
486
|
isServiceInstalled() {
|
|
377
|
-
if (!
|
|
378
|
-
|
|
487
|
+
if (!useSystemd) return fallback.isServiceInstalled();
|
|
488
|
+
const result = systemctl(['is-enabled', unitName]);
|
|
489
|
+
if (result.status !== 0 && demoteToDetachedIfSystemdUnavailable(result)) {
|
|
490
|
+
return fallback.isServiceInstalled();
|
|
491
|
+
}
|
|
492
|
+
return result.status === 0;
|
|
379
493
|
},
|
|
380
494
|
|
|
381
495
|
install() {
|
|
382
|
-
if (!
|
|
496
|
+
if (!useSystemd) {
|
|
383
497
|
fallback.install();
|
|
384
498
|
return;
|
|
385
499
|
}
|
|
@@ -389,33 +503,58 @@ WantedBy=default.target
|
|
|
389
503
|
const currentContents = existsSync(unitPath) ? readFileSync(unitPath, 'utf8') : null;
|
|
390
504
|
if (currentContents !== nextContents) {
|
|
391
505
|
writeFileSync(unitPath, nextContents, 'utf8');
|
|
392
|
-
systemctl(['daemon-reload']);
|
|
506
|
+
const reloadResult = systemctl(['daemon-reload']);
|
|
507
|
+
if (reloadResult.status !== 0) {
|
|
508
|
+
if (demoteToDetachedIfSystemdUnavailable(reloadResult)) {
|
|
509
|
+
fallback.install();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
fail('Failed to reload vibelet systemd user units.', resultOutput(reloadResult));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const enableResult = systemctl(['enable', unitName]);
|
|
516
|
+
if (enableResult.status !== 0) {
|
|
517
|
+
if (demoteToDetachedIfSystemdUnavailable(enableResult)) {
|
|
518
|
+
fallback.install();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
fail('Failed to enable vibelet daemon.', resultOutput(enableResult));
|
|
393
522
|
}
|
|
394
|
-
systemctl(['enable', unitName]);
|
|
395
523
|
},
|
|
396
524
|
|
|
397
525
|
start() {
|
|
398
|
-
if (!
|
|
526
|
+
if (!useSystemd) {
|
|
399
527
|
fallback.start();
|
|
400
528
|
return;
|
|
401
529
|
}
|
|
402
530
|
const result = systemctl(['start', unitName]);
|
|
403
531
|
if (result.status !== 0) {
|
|
532
|
+
if (demoteToDetachedIfSystemdUnavailable(result)) {
|
|
533
|
+
fallback.install();
|
|
534
|
+
fallback.start();
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
404
537
|
fail('Failed to start vibelet daemon.', result.stderr || result.stdout);
|
|
405
538
|
}
|
|
406
539
|
},
|
|
407
540
|
|
|
408
541
|
stop() {
|
|
409
|
-
if (!
|
|
542
|
+
if (!useSystemd) {
|
|
410
543
|
fallback.stop();
|
|
411
544
|
return;
|
|
412
545
|
}
|
|
413
|
-
systemctl(['stop', unitName]);
|
|
546
|
+
const result = systemctl(['stop', unitName]);
|
|
547
|
+
if (result.status !== 0 && demoteToDetachedIfSystemdUnavailable(result)) {
|
|
548
|
+
fallback.stop();
|
|
549
|
+
}
|
|
414
550
|
},
|
|
415
551
|
|
|
416
552
|
statusLabel() {
|
|
417
|
-
if (!
|
|
553
|
+
if (!useSystemd) return fallback.statusLabel();
|
|
418
554
|
const result = systemctl(['is-active', unitName]);
|
|
555
|
+
if (result.status !== 0 && demoteToDetachedIfSystemdUnavailable(result)) {
|
|
556
|
+
return fallback.statusLabel();
|
|
557
|
+
}
|
|
419
558
|
return result.stdout?.trim() || 'unknown';
|
|
420
559
|
},
|
|
421
560
|
};
|
|
@@ -556,7 +695,7 @@ async function waitForDaemonExit(timeoutMs) {
|
|
|
556
695
|
return false;
|
|
557
696
|
}
|
|
558
697
|
|
|
559
|
-
async function waitForHealth(timeoutMs =
|
|
698
|
+
async function waitForHealth(timeoutMs = 30_000) {
|
|
560
699
|
const health = await probeHealth(timeoutMs);
|
|
561
700
|
if (health) {
|
|
562
701
|
return health;
|
|
@@ -623,6 +762,7 @@ function createCompactPairingPayload(pairingPayload) {
|
|
|
623
762
|
target.port,
|
|
624
763
|
target.source,
|
|
625
764
|
target.stability,
|
|
765
|
+
target.secure === true,
|
|
626
766
|
]);
|
|
627
767
|
}
|
|
628
768
|
return compactPayload;
|
|
@@ -719,6 +859,7 @@ function normalizePairingConnections(pairingPayload) {
|
|
|
719
859
|
kind: target.kind === 'relay' ? 'relay' : 'direct',
|
|
720
860
|
host: normalizeHostValue(target.host),
|
|
721
861
|
port: Math.floor(Number(target.port)),
|
|
862
|
+
...(target.secure === true ? { secure: true } : {}),
|
|
722
863
|
source: target.source,
|
|
723
864
|
stability: target.stability,
|
|
724
865
|
}))
|
|
@@ -728,7 +869,7 @@ function normalizePairingConnections(pairingPayload) {
|
|
|
728
869
|
const dedupe = (targets) => {
|
|
729
870
|
const seen = new Set();
|
|
730
871
|
return targets.filter((target) => {
|
|
731
|
-
const key = `${target.host}:${target.port}`;
|
|
872
|
+
const key = `${target.host}:${target.port}:${target.secure === true ? 'secure' : 'plain'}`;
|
|
732
873
|
if (seen.has(key)) {
|
|
733
874
|
return false;
|
|
734
875
|
}
|
|
@@ -836,6 +977,7 @@ function printHelp() {
|
|
|
836
977
|
process.stdout.write(` npx ${packageJson.name} --force Force a new Cloudflare Tunnel URL\n`);
|
|
837
978
|
process.stdout.write(` npx ${packageJson.name} --relay <url> Use a custom tunnel URL for remote access\n`);
|
|
838
979
|
process.stdout.write(` npx ${packageJson.name} --host <ip> Set the primary host/IP address\n`);
|
|
980
|
+
process.stdout.write(` npx ${packageJson.name} --port <port> Start or query the daemon on a custom port\n`);
|
|
839
981
|
process.stdout.write(` npx ${packageJson.name} --fallback-hosts <ips> Comma-separated fallback IPs\n`);
|
|
840
982
|
process.stdout.write(` npx ${packageJson.name} stop Stop the daemon\n`);
|
|
841
983
|
process.stdout.write(` npx ${packageJson.name} restart Restart the daemon\n`);
|
|
@@ -898,6 +1040,16 @@ function parseRelayArg() {
|
|
|
898
1040
|
return parseNamedArg('relay', 'https://abc.trycloudflare.com');
|
|
899
1041
|
}
|
|
900
1042
|
|
|
1043
|
+
function parsePortArg() {
|
|
1044
|
+
const rawValue = parseNamedArg('port', '9876');
|
|
1045
|
+
if (rawValue === null) return null;
|
|
1046
|
+
const parsed = normalizePortValue(rawValue);
|
|
1047
|
+
if (parsed === null) {
|
|
1048
|
+
fail('--port must be an integer between 1 and 65535');
|
|
1049
|
+
}
|
|
1050
|
+
return parsed;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
901
1053
|
function loadRelayConfig() {
|
|
902
1054
|
try {
|
|
903
1055
|
const data = JSON.parse(readFileSync(relayConfigPath, 'utf8'));
|
|
@@ -1045,6 +1197,12 @@ async function main() {
|
|
|
1045
1197
|
checkForUpdateFromCache();
|
|
1046
1198
|
fetchLatestVersionInBackground();
|
|
1047
1199
|
|
|
1200
|
+
const portArg = parsePortArg();
|
|
1201
|
+
if (portArg !== null) {
|
|
1202
|
+
port = portArg;
|
|
1203
|
+
process.env.VIBE_PORT = String(portArg);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1048
1206
|
consumeFlag('remote') || consumeFlag('tunnel') || readNpmConfigFlag('remote') || readNpmConfigFlag('tunnel');
|
|
1049
1207
|
const localFlag = consumeFlag('local') || readNpmConfigFlag('local');
|
|
1050
1208
|
const forceFlag = consumeFlag('force') || readNpmConfigFlag('force');
|
|
@@ -1195,7 +1353,8 @@ async function main() {
|
|
|
1195
1353
|
fallbackHosts: fallbackHostsArg || '',
|
|
1196
1354
|
localMode: localFlag,
|
|
1197
1355
|
}) && (localFlag || Boolean(relayUrl) || Boolean(hostArg) || Boolean(fallbackHostsArg));
|
|
1198
|
-
|
|
1356
|
+
const shouldReplaceDetachedDaemon = !healthyDaemon && !backend.handlesProcessLifecycle && backend.isServiceInstalled();
|
|
1357
|
+
if (shouldReplaceDetachedDaemon || (healthyDaemon && hasExplicitConfigOverrides)) {
|
|
1199
1358
|
await stopRunningDaemon(backend);
|
|
1200
1359
|
}
|
|
1201
1360
|
ensureRuntimeInstalled();
|
|
@@ -1234,7 +1393,8 @@ async function main() {
|
|
|
1234
1393
|
return;
|
|
1235
1394
|
}
|
|
1236
1395
|
|
|
1237
|
-
|
|
1396
|
+
const shouldReplaceDetachedDaemon = !healthyDaemon && !backend.handlesProcessLifecycle && backend.isServiceInstalled();
|
|
1397
|
+
if (shouldReplaceDetachedDaemon || (healthyDaemon && hasExplicitConfigOverrides)) {
|
|
1238
1398
|
await stopRunningDaemon(backend);
|
|
1239
1399
|
}
|
|
1240
1400
|
|