vibelet 1.0.9 → 1.0.11

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 CHANGED
@@ -1,9 +1,9 @@
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';
@@ -218,6 +218,75 @@ function isProcessAlive(pid) {
218
218
  }
219
219
  }
220
220
 
221
+ // ─── Service PATH helpers ──────────────────────────────────────────────────────
222
+
223
+ // Transient shims like fnm_multishells/<pid>/bin disappear or get GC'd after the
224
+ // shell that created them exits. Long-lived services (launchd, systemd) that
225
+ // inherit these paths end up spawning wrapper scripts whose `exec node` fails
226
+ // with "node: not found". Stabilize to the underlying stable dir/file.
227
+ function isTransientNodePath(p) {
228
+ return typeof p === 'string' && p.includes('fnm_multishells');
229
+ }
230
+
231
+ function stabilizeServiceFile(p) {
232
+ if (!p || !isTransientNodePath(p)) return p;
233
+ try {
234
+ const stable = join(realpathSync(dirname(p)), basename(p));
235
+ if (existsSync(stable) && !isTransientNodePath(stable)) return stable;
236
+ } catch { /* broken symlink */ }
237
+ return p;
238
+ }
239
+
240
+ function stabilizeServiceDir(dir) {
241
+ if (!dir || !isTransientNodePath(dir)) return dir;
242
+ try {
243
+ const real = realpathSync(dir);
244
+ if (real && !isTransientNodePath(real)) return real;
245
+ } catch { /* broken symlink */ }
246
+ return dir;
247
+ }
248
+
249
+ // Build a PATH the daemon can rely on under launchd/systemd, where no login
250
+ // shell ran and no rc files loaded. Puts the node bin dir first so wrapper
251
+ // scripts spawned by the daemon (codex, claude) can resolve `node` via PATH,
252
+ // then falls back to common system and user-tool locations.
253
+ function buildServicePath(nodeBinDir) {
254
+ const home = homedir();
255
+ const pnpmHome = process.env.PNPM_HOME
256
+ || (process.platform === 'win32' ? join(home, 'pnpm') : join(home, 'Library', 'pnpm'));
257
+ const baseline = process.platform === 'win32'
258
+ ? [
259
+ nodeBinDir,
260
+ join(home, '.npm-global', 'bin'),
261
+ join(home, '.local', 'bin'),
262
+ join(home, '.claude', 'bin'),
263
+ pnpmHome,
264
+ ]
265
+ : [
266
+ nodeBinDir,
267
+ '/usr/local/bin',
268
+ '/opt/homebrew/bin',
269
+ '/usr/bin',
270
+ '/bin',
271
+ '/usr/sbin',
272
+ '/sbin',
273
+ join(home, '.npm-global', 'bin'),
274
+ join(home, '.local', 'bin'),
275
+ join(home, '.claude', 'bin'),
276
+ pnpmHome,
277
+ ];
278
+ const current = (process.env.PATH || '')
279
+ .split(delimiter)
280
+ .filter(Boolean)
281
+ .map(stabilizeServiceDir);
282
+ const out = [];
283
+ const seen = new Set();
284
+ for (const p of [...baseline, ...current]) {
285
+ if (p && !seen.has(p)) { out.push(p); seen.add(p); }
286
+ }
287
+ return out.join(delimiter);
288
+ }
289
+
221
290
  // ─── Platform service backends ──────────────────────────────────────────────────
222
291
 
223
292
  function createDarwinBackend() {
@@ -232,15 +301,25 @@ function createDarwinBackend() {
232
301
  }
233
302
 
234
303
  function plistContents() {
304
+ // Prefer a stable node path — fnm_multishells shims disappear when the
305
+ // installing shell exits, leaving launchd unable to respawn the daemon.
306
+ const nodeBin = stabilizeServiceFile(process.execPath);
235
307
  const programArgs = [
236
- process.execPath,
308
+ nodeBin,
237
309
  runtimeDaemonEntryPath,
238
310
  ].map((value) => ` <string>${value}</string>`).join('\n');
239
311
 
240
- const envVars = { VIBE_PORT: String(port) };
312
+ // launchd starts the daemon with a bare PATH; inject a rich one so child
313
+ // CLIs like codex (a sh wrapper that does `exec node`) can find node.
314
+ const envVars = {
315
+ PATH: buildServicePath(dirname(nodeBin)),
316
+ VIBE_PORT: String(port),
317
+ };
241
318
  if (process.env.VIBELET_RELAY_URL) envVars.VIBELET_RELAY_URL = process.env.VIBELET_RELAY_URL;
242
319
  if (process.env.VIBELET_CANONICAL_HOST) envVars.VIBELET_CANONICAL_HOST = process.env.VIBELET_CANONICAL_HOST;
243
320
  if (process.env.VIBELET_FALLBACK_HOSTS) envVars.VIBELET_FALLBACK_HOSTS = process.env.VIBELET_FALLBACK_HOSTS;
321
+ if (process.env.CODEX_PATH) envVars.CODEX_PATH = process.env.CODEX_PATH;
322
+ if (process.env.CLAUDE_PATH) envVars.CLAUDE_PATH = process.env.CLAUDE_PATH;
244
323
  const envSection = Object.keys(envVars).length > 0
245
324
  ? ` <key>EnvironmentVariables</key>
246
325
  <dict>
@@ -313,8 +392,15 @@ ${envSection}
313
392
  },
314
393
 
315
394
  stop() {
316
- if (this.isServiceInstalled()) {
317
- launchctl(['bootout', `${launchDomain}/${label}`]);
395
+ if (!this.isServiceInstalled()) return;
396
+ launchctl(['bootout', `${launchDomain}/${label}`]);
397
+ // bootout returns before launchd actually finishes unloading. Poll
398
+ // until the service is gone so a follow-up install() doesn't see a
399
+ // stale registration, skip bootstrap, and leave the daemon down.
400
+ const deadline = Date.now() + 5000;
401
+ const wait = new Int32Array(new SharedArrayBuffer(4));
402
+ while (Date.now() < deadline && this.isServiceInstalled()) {
403
+ Atomics.wait(wait, 0, 0, 100);
318
404
  }
319
405
  },
320
406
 
@@ -338,17 +424,20 @@ function createLinuxBackend() {
338
424
  }
339
425
 
340
426
  function unitContents() {
427
+ const nodeBin = stabilizeServiceFile(process.execPath);
428
+ const servicePath = buildServicePath(dirname(nodeBin));
341
429
  return `[Unit]
342
430
  Description=Vibelet Daemon
343
431
  After=network.target
344
432
 
345
433
  [Service]
346
- ExecStart=${process.execPath} ${runtimeDaemonEntryPath}
434
+ ExecStart=${nodeBin} ${runtimeDaemonEntryPath}
347
435
  WorkingDirectory=${runtimeCurrentDir}
348
436
  Restart=always
349
437
  RestartSec=3
350
438
  StandardOutput=append:${stdoutLogPath}
351
439
  StandardError=append:${stderrLogPath}
440
+ Environment=PATH=${servicePath}
352
441
  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
442
 
354
443
  [Install]
@@ -547,7 +636,7 @@ async function waitForDaemonExit(timeoutMs) {
547
636
  return false;
548
637
  }
549
638
 
550
- async function waitForHealth(timeoutMs = 10_000) {
639
+ async function waitForHealth(timeoutMs = 30_000) {
551
640
  const health = await probeHealth(timeoutMs);
552
641
  if (health) {
553
642
  return health;