conductor-remote 1.42.1 → 1.42.2

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/dist/index.html CHANGED
@@ -13,7 +13,7 @@
13
13
  <title>Conductor Remote</title>
14
14
  <!-- Runs before the module bundle so it can catch a stale shell that fails to boot. -->
15
15
  <script src="/self-heal.js"></script>
16
- <script type="module" crossorigin src="/assets/index-cYAMKygF.js"></script>
16
+ <script type="module" crossorigin src="/assets/index-DFdIcbun.js"></script>
17
17
  <link rel="stylesheet" crossorigin href="/assets/index-95IdnA_r.css">
18
18
  <link rel="manifest" href="/manifest.webmanifest"></head>
19
19
  <body>
package/dist/sw.js CHANGED
@@ -1 +1 @@
1
- if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let l={};const t=e=>i(e,o),c={module:{uri:o},exports:l,require:t};s[o]=Promise.all(n.map(e=>c[e]||t(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"e1e682e2e5e88fa03b7808ae9db8e098"},{url:"index.html",revision:"260018ba85214e3a9de6a3eeca285216"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-cYAMKygF.js",revision:null},{url:"assets/index-95IdnA_r.css",revision:null},{url:"apple-touch-icon.png",revision:"2b9301416b880d45d4bb655f2600d1f2"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
1
+ if(!self.define){let e,s={};const i=(i,n)=>(i=new URL(i+".js",n).href,s[i]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=i,e.onload=s,document.head.appendChild(e)}else e=i,importScripts(i),s()}).then(()=>{let e=s[i];if(!e)throw new Error(`Module ${i} didn’t register its module`);return e}));self.define=(n,r)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let l={};const c=e=>i(e,o),t={module:{uri:o},exports:l,require:c};s[o]=Promise.all(n.map(e=>t[e]||c(e))).then(e=>(r(...e),l))}}define(["./workbox-9c191d2f"],function(e){"use strict";importScripts("/push-sw.js"),self.addEventListener("message",e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()}),e.clientsClaim(),e.precacheAndRoute([{url:"self-heal.js",revision:"49bd63adb25a09341f8d2610e8bd3c76"},{url:"push-sw.js",revision:"e1e682e2e5e88fa03b7808ae9db8e098"},{url:"index.html",revision:"94f9b873fb04ccb55680c1b74315be0f"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-DFdIcbun.js",revision:null},{url:"assets/index-95IdnA_r.css",revision:null},{url:"apple-touch-icon.png",revision:"2b9301416b880d45d4bb655f2600d1f2"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
@@ -133,6 +133,14 @@ function buildPlist() {
133
133
  ['PATH', daemonPath],
134
134
  ['CONDUCTOR_REMOTE_MANAGED', '1']
135
135
  ];
136
+ // EXPOSE belongs here with the other runtime knobs, and leaving it out was a real bug. The daemon
137
+ // reads its posture on every boot (the funnel watchdog only arms itself for `public`), but install()
138
+ // reloads launchd *before* ensureTailscale() persists the choice — so a fresh `--expose tailnet`
139
+ // daemon came up reading the old posture, armed the watchdog against a Funnel that was about to be
140
+ // switched off, and ~3 failed probes later healed it straight back to public. Baking the resolved
141
+ // value makes the daemon's own environment the answer, so nothing depends on write ordering.
142
+ if (process.env.EXPOSE)
143
+ envEntries.push(['EXPOSE', process.env.EXPOSE]);
136
144
  if (process.env.WRITE_STRATEGY)
137
145
  envEntries.push(['WRITE_STRATEGY', process.env.WRITE_STRATEGY]);
138
146
  if (process.env.RELAY_HOST)
@@ -419,6 +427,11 @@ function install() {
419
427
  process.exit(1);
420
428
  }
421
429
  persistPinnedToken();
430
+ // Resolve (and persist) the expose posture *before* the plist is written and launchd is reloaded, so
431
+ // the daemon starts with an explicit EXPOSE in its own environment rather than racing the file
432
+ // ensureTailscale() writes further down. Folding it back into process.env is how every other knob
433
+ // travels here (see applyFlags), and it keeps ensureTailscale()'s own resolve a no-op re-read.
434
+ process.env.EXPOSE = resolveExposeMode();
422
435
  fs.mkdirSync(path.dirname(plistPath), { recursive: true });
423
436
  fs.mkdirSync(logDir, { recursive: true });
424
437
  fs.writeFileSync(plistPath, buildPlist());
@@ -447,6 +460,145 @@ function restart() {
447
460
  console.info(`✓ restarted ${LABEL}`);
448
461
  printUrl();
449
462
  }
463
+ /**
464
+ * The daemon's environment, read back out of the plist it was installed with.
465
+ *
466
+ * This is the whole point of `config`: `process.env` here belongs to *your shell*, and the daemon
467
+ * runs under launchd with whatever `buildPlist()` baked in. Reading the shell's env to report the
468
+ * daemon's configuration is how a posture change can look applied while the running relay still
469
+ * believes something else — which is exactly the bug that made EXPOSE a plist entry.
470
+ */
471
+ function readPlistEnv() {
472
+ try {
473
+ const out = execFileSync('plutil', ['-convert', 'json', '-o', '-', plistPath], {
474
+ encoding: 'utf8',
475
+ stdio: 'pipe'
476
+ });
477
+ const env = JSON.parse(out)?.EnvironmentVariables;
478
+ return env && typeof env === 'object' ? env : {};
479
+ }
480
+ catch {
481
+ return {};
482
+ }
483
+ }
484
+ const HOME = os.homedir();
485
+ const tilde = (p) => (p.startsWith(HOME) ? `~${p.slice(HOME.length)}` : p);
486
+ const KNOBS = [
487
+ {
488
+ name: 'expose',
489
+ env: 'EXPOSE',
490
+ // The runtime's own precedence (src/tailscale.ts ▸ readExposeMode): env, then the persisted
491
+ // file, then public. Reported the same way so this can never describe a posture the relay
492
+ // isn't running.
493
+ fallback: () => {
494
+ try {
495
+ return normalizeExposeMode(fs.readFileSync(exposeStorePath(), 'utf8')) ?? 'public';
496
+ }
497
+ catch {
498
+ return 'public';
499
+ }
500
+ },
501
+ fallbackSource: () => (fs.existsSync(exposeStorePath()) ? 'expose file' : 'default')
502
+ },
503
+ { name: 'port', env: 'RELAY_PORT', fallback: () => '8787' },
504
+ { name: 'host', env: 'RELAY_HOST', fallback: () => '127.0.0.1' },
505
+ { name: 'write-strategy', env: 'WRITE_STRATEGY', fallback: () => 'applescript' },
506
+ { name: 'auto-update', env: 'AUTO_UPDATE', fallback: () => 'auto' },
507
+ { name: 'auto-update-interval', env: 'AUTO_UPDATE_INTERVAL_MINUTES', fallback: () => '5 (minutes)' },
508
+ {
509
+ name: 'funnel-watchdog',
510
+ env: 'FUNNEL_WATCHDOG',
511
+ // Two gates, not one: it defaults on for the managed daemon and then stands down unless the
512
+ // posture is public (src/funnel-watchdog.ts ▸ wantEnabled + startFunnelWatchdog).
513
+ fallback: env => (env.CONDUCTOR_REMOTE_MANAGED === '1' ? 'on (managed daemon)' : 'off (not managed)')
514
+ },
515
+ { name: 'funnel-watchdog-interval', env: 'FUNNEL_WATCHDOG_INTERVAL_SECONDS', fallback: () => '60 (seconds)' },
516
+ {
517
+ name: 'db',
518
+ env: 'CONDUCTOR_DB',
519
+ fallback: () => tilde(path.join(HOME, 'Library', 'Application Support', 'com.conductor.app', 'conductor.db'))
520
+ },
521
+ {
522
+ name: 'workspaces',
523
+ env: 'CONDUCTOR_WORKSPACES',
524
+ fallback: () => tilde(path.join(HOME, 'conductor', 'workspaces'))
525
+ }
526
+ ];
527
+ /**
528
+ * What is this daemon actually configured with, and where did each value come from.
529
+ *
530
+ * Read-only on purpose. Every knob is written by `service install`, and a second write path for the
531
+ * same state is how the plist and the `expose` file came to disagree in the first place. What was
532
+ * missing was the ability to *see* the disagreement, so that is what this adds — including a live
533
+ * cross-check of the Tailscale posture against the configured one.
534
+ */
535
+ function config() {
536
+ if (!fs.existsSync(plistPath)) {
537
+ console.info(`plist: (not installed)\n\nRun \`conductor-remote service install\` first.`);
538
+ return;
539
+ }
540
+ const env = readPlistEnv();
541
+ console.info(`plist: ${tilde(plistPath)}`);
542
+ console.info(`state: ${tilde(path.dirname(exposeStorePath()))}`);
543
+ try {
544
+ const out = execFileSync('launchctl', ['print', `${domain}/${LABEL}`], { encoding: 'utf8', stdio: 'pipe' });
545
+ console.info(`daemon: ${out.match(/state = (\S+)/)?.[1] ?? 'unknown'} (pid ${out.match(/pid = (\d+)/)?.[1] ?? '—'})`);
546
+ }
547
+ catch {
548
+ console.info('daemon: loaded but not running (check `conductor-remote logs`)');
549
+ }
550
+ const rows = KNOBS.map(k => {
551
+ const set = env[k.env];
552
+ return {
553
+ name: k.name,
554
+ value: set ?? k.fallback(env),
555
+ source: set ? 'plist' : (k.fallbackSource?.(env) ?? 'default')
556
+ };
557
+ });
558
+ // The token is deliberately not a row above: it never rides in the plist, and this output is the
559
+ // kind of thing that gets pasted into an issue. Shown truncated, like every other log surface here.
560
+ let token = '(none yet — minted on first start)';
561
+ try {
562
+ const raw = fs.readFileSync(path.join(path.dirname(exposeStorePath()), 'token'), 'utf8').trim();
563
+ if (raw)
564
+ token = `${raw.slice(0, 4)}…${raw.slice(-4)}`;
565
+ }
566
+ catch {
567
+ // no token file yet
568
+ }
569
+ rows.push({ name: 'token', value: token, source: 'token file' });
570
+ const width = Math.max(...rows.map(r => r.name.length));
571
+ const valueWidth = Math.max(...rows.map(r => r.value.length));
572
+ console.info('');
573
+ for (const r of rows) {
574
+ console.info(` ${r.name.padEnd(width)} ${r.value.padEnd(valueWidth)} ${r.source}`);
575
+ }
576
+ console.info('\n (values come from the daemon\u2019s own plist environment, not this shell\u2019s)');
577
+ console.info(' change one with: conductor-remote service install --<setting> <value>');
578
+ // The cross-check worth having. A posture the relay believes and a Tailscale that is doing something
579
+ // else is invisible in every other command, and it is self-healing in the wrong direction: the funnel
580
+ // watchdog only runs for `public`, so a relay that thinks it is public will re-register Funnel.
581
+ const bin = tailscaleBin();
582
+ const configured = rows.find(r => r.name === 'expose')?.value;
583
+ if (!bin || !configured)
584
+ return;
585
+ const dns = magicDnsName(bin);
586
+ const live = tailscaleState(bin, dns);
587
+ const liveMode = live.funnelOn ? 'public' : live.proxyOk ? 'tailnet' : null;
588
+ console.info('');
589
+ if (liveMode === null) {
590
+ console.info(` \u26a0 tailscale is not fronting 127.0.0.1:${RELAY_PORT} at all — the phone URL is not wired.`);
591
+ console.info(' Fix with: conductor-remote service install');
592
+ }
593
+ else if (liveMode === configured) {
594
+ console.info(` \u2713 tailscale agrees: ${liveMode === 'public' ? 'Funnel on (internet-facing)' : 'serve only (tailnet)'}`);
595
+ }
596
+ else {
597
+ console.info(` \u26a0 tailscale says ${liveMode}, the daemon is configured ${configured}. They must match:`);
598
+ console.info(` the funnel watchdog only runs for \`public\`, so the two can heal each other apart.`);
599
+ console.info(` Fix with: conductor-remote service install --expose ${configured}`);
600
+ }
601
+ }
450
602
  function status() {
451
603
  const installed = fs.existsSync(plistPath);
452
604
  console.info(`plist: ${installed ? plistPath : '(not installed)'}`);
@@ -497,12 +649,15 @@ switch (cmd) {
497
649
  case 'status':
498
650
  status();
499
651
  break;
652
+ case 'config':
653
+ config();
654
+ break;
500
655
  case 'logs':
501
656
  logs();
502
657
  break;
503
658
  default:
504
659
  console.error(`unknown command: ${cmd}\n` +
505
- 'usage: service.ts <install|uninstall|restart|status|logs> [flags]\n' +
660
+ 'usage: service.ts <install|uninstall|restart|status|config|logs> [flags]\n' +
506
661
  ` flags (install): ${Object.keys(FLAG_ENV).join(', ')}`);
507
662
  process.exit(1);
508
663
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conductor-remote",
3
- "version": "1.42.1",
3
+ "version": "1.42.2",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.15.0",
6
6
  "description": "Phone control panel for local Conductor agents. Reads ride SQLite + git; prompts ride Conductor's own dispatch path.",