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/README.md +25 -0
- package/bin/cli.js +8 -0
- package/dist/assets/{index-cYAMKygF.js → index-DFdIcbun.js} +2 -2
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/dist-node/scripts/service.js +156 -1
- package/package.json +1 -1
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-
|
|
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
|
|
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.
|
|
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.",
|