conductor-remote 0.0.0-development

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.
@@ -0,0 +1,2 @@
1
+ try{self["workbox:window:7.4.0"]&&_()}catch{}function b(t,r){return new Promise((function(n){var u=new MessageChannel;u.port1.onmessage=function(s){n(s.data)},t.postMessage(r,[u.port2])}))}function P(t,r){(r==null||r>t.length)&&(r=t.length);for(var n=0,u=Array(r);n<r;n++)u[n]=t[n];return u}function j(t,r,n){return r&&(function(u,s){for(var c=0;c<s.length;c++){var o=s[c];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(u,W(o.key),o)}})(t.prototype,r),Object.defineProperty(t,"prototype",{writable:!1}),t}function S(t,r){var n=typeof Symbol<"u"&&t[Symbol.iterator]||t["@@iterator"];if(n)return(n=n.call(t)).next.bind(n);if(Array.isArray(t)||(n=(function(s,c){if(s){if(typeof s=="string")return P(s,c);var o={}.toString.call(s).slice(8,-1);return o==="Object"&&s.constructor&&(o=s.constructor.name),o==="Map"||o==="Set"?Array.from(s):o==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(o)?P(s,c):void 0}})(t))||r){n&&(t=n);var u=0;return function(){return u>=t.length?{done:!0}:{done:!1,value:t[u++]}}}throw new TypeError(`Invalid attempt to iterate non-iterable instance.
2
+ In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function w(t,r){return w=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(n,u){return n.__proto__=u,n},w(t,r)}function W(t){var r=(function(n,u){if(typeof n!="object"||!n)return n;var s=n[Symbol.toPrimitive];if(s!==void 0){var c=s.call(n,u);if(typeof c!="object")return c;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(n)})(t,"string");return typeof r=="symbol"?r:r+""}try{self["workbox:core:7.4.0"]&&_()}catch{}var m=function(){var t=this;this.promise=new Promise((function(r,n){t.resolve=r,t.reject=n}))};function y(t,r){var n=location.href;return new URL(t,n).href===new URL(r,n).href}var d=function(t,r){this.type=t,Object.assign(this,r)};function l(t,r,n){return t&&t.then||(t=Promise.resolve(t)),r?t.then(r):t}function k(){}var L={type:"SKIP_WAITING"};function E(t,r){return t&&t.then?t.then(k):Promise.resolve()}var O=(function(t){function r(c,o){var e,i;return o===void 0&&(o={}),(e=t.call(this)||this).nn={},e.tn=0,e.rn=new m,e.en=new m,e.on=new m,e.un=0,e.an=new Set,e.cn=function(){var f=e.fn,a=f.installing;e.tn>0||!y(a.scriptURL,e.sn.toString())||performance.now()>e.un+6e4?(e.vn=a,f.removeEventListener("updatefound",e.cn)):(e.hn=a,e.an.add(a),e.rn.resolve(a)),++e.tn,a.addEventListener("statechange",e.ln)},e.ln=function(f){var a=e.fn,v=f.target,h=v.state,p=v===e.vn,g={sw:v,isExternal:p,originalEvent:f};!p&&e.mn&&(g.isUpdate=!0),e.dispatchEvent(new d(h,g)),h==="installed"?e.wn=self.setTimeout((function(){h==="installed"&&a.waiting===v&&e.dispatchEvent(new d("waiting",g))}),200):h==="activating"&&(clearTimeout(e.wn),p||e.en.resolve(v))},e.yn=function(f){var a=e.hn,v=a!==navigator.serviceWorker.controller;e.dispatchEvent(new d("controlling",{isExternal:v,originalEvent:f,sw:a,isUpdate:e.mn})),v||e.on.resolve(a)},e.gn=(i=function(f){var a=f.data,v=f.ports,h=f.source;return l(e.getSW(),(function(){e.an.has(h)&&e.dispatchEvent(new d("message",{data:a,originalEvent:f,ports:v,sw:h}))}))},function(){for(var f=[],a=0;a<arguments.length;a++)f[a]=arguments[a];try{return Promise.resolve(i.apply(this,f))}catch(v){return Promise.reject(v)}}),e.sn=c,e.nn=o,navigator.serviceWorker.addEventListener("message",e.gn),e}var n,u;u=t,(n=r).prototype=Object.create(u.prototype),n.prototype.constructor=n,w(n,u);var s=r.prototype;return s.register=function(c){var o=(c===void 0?{}:c).immediate,e=o!==void 0&&o;try{var i=this;return l((function(f,a){var v=f();return v&&v.then?v.then(a):a(v)})((function(){if(!e&&document.readyState!=="complete")return E(new Promise((function(f){return window.addEventListener("load",f)})))}),(function(){return i.mn=!!navigator.serviceWorker.controller,i.dn=i.pn(),l(i.bn(),(function(f){i.fn=f,i.dn&&(i.hn=i.dn,i.en.resolve(i.dn),i.on.resolve(i.dn),i.dn.addEventListener("statechange",i.ln,{once:!0}));var a=i.fn.waiting;return a&&y(a.scriptURL,i.sn.toString())&&(i.hn=a,Promise.resolve().then((function(){i.dispatchEvent(new d("waiting",{sw:a,wasWaitingBeforeRegister:!0}))})).then((function(){}))),i.hn&&(i.rn.resolve(i.hn),i.an.add(i.hn)),i.fn.addEventListener("updatefound",i.cn),navigator.serviceWorker.addEventListener("controllerchange",i.yn),i.fn}))})))}catch(f){return Promise.reject(f)}},s.update=function(){try{return this.fn?l(E(this.fn.update())):l()}catch(c){return Promise.reject(c)}},s.getSW=function(){return this.hn!==void 0?Promise.resolve(this.hn):this.rn.promise},s.messageSW=function(c){try{return l(this.getSW(),(function(o){return b(o,c)}))}catch(o){return Promise.reject(o)}},s.messageSkipWaiting=function(){this.fn&&this.fn.waiting&&b(this.fn.waiting,L)},s.pn=function(){var c=navigator.serviceWorker.controller;return c&&y(c.scriptURL,this.sn.toString())?c:void 0},s.bn=function(){try{var c=this;return l((function(o,e){try{var i=o()}catch(f){return e(f)}return i&&i.then?i.then(void 0,e):i})((function(){return l(navigator.serviceWorker.register(c.sn,c.nn),(function(o){return c.un=performance.now(),o}))}),(function(o){throw o})))}catch(o){return Promise.reject(o)}},j(r,[{key:"active",get:function(){return this.en.promise}},{key:"controlling",get:function(){return this.on.promise}}])})((function(){function t(){this.Pn=new Map}var r=t.prototype;return r.addEventListener=function(n,u){this.jn(n).add(u)},r.removeEventListener=function(n,u){this.jn(n).delete(u)},r.dispatchEvent=function(n){n.target=this;for(var u,s=S(this.jn(n.type));!(u=s()).done;)(0,u.value)(n)},r.jn=function(n){return this.Pn.has(n)||this.Pn.set(n,new Set),this.Pn.get(n)},t})());export{O as Workbox,d as WorkboxEvent,b as messageSW};
Binary file
Binary file
Binary file
package/dist/icon.svg ADDED
@@ -0,0 +1,28 @@
1
+ <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" y1="0" x2="0" y2="512" gradientUnits="userSpaceOnUse">
4
+ <stop stop-color="#1b1e26" />
5
+ <stop offset="1" stop-color="#0a0b0e" />
6
+ </linearGradient>
7
+ <linearGradient id="fg" x1="140" y1="146" x2="372" y2="376" gradientUnits="userSpaceOnUse">
8
+ <stop stop-color="#a99bff" />
9
+ <stop offset="1" stop-color="#5b9dff" />
10
+ </linearGradient>
11
+ </defs>
12
+ <rect width="512" height="512" rx="112" fill="url(#bg)" />
13
+ <g fill="url(#fg)">
14
+ <rect x="140" y="196" width="44" height="180" rx="22" />
15
+ <rect x="234" y="146" width="44" height="230" rx="22" />
16
+ <rect x="328" y="236" width="44" height="140" rx="22" />
17
+ </g>
18
+ <g fill="#0a0b0e">
19
+ <circle cx="162" cy="196" r="27" />
20
+ <circle cx="256" cy="146" r="27" />
21
+ <circle cx="350" cy="236" r="27" />
22
+ </g>
23
+ <g fill="url(#fg)">
24
+ <circle cx="162" cy="196" r="14" />
25
+ <circle cx="256" cy="146" r="14" />
26
+ <circle cx="350" cy="236" r="14" />
27
+ </g>
28
+ </svg>
@@ -0,0 +1,20 @@
1
+ <!doctype html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, maximum-scale=1" />
6
+ <meta name="theme-color" content="#0a0b0e" />
7
+ <meta name="apple-mobile-web-app-capable" content="yes" />
8
+ <meta name="mobile-web-app-capable" content="yes" />
9
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
10
+ <meta name="apple-mobile-web-app-title" content="Conductor" />
11
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
12
+ <link rel="icon" type="image/svg+xml" href="/icon.svg" />
13
+ <title>Conductor Remote</title>
14
+ <script type="module" crossorigin src="/assets/index-DtY88CkY.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-DiiwvzTm.css">
16
+ <link rel="manifest" href="/manifest.webmanifest"></head>
17
+ <body>
18
+ <div id="root"></div>
19
+ </body>
20
+ </html>
@@ -0,0 +1 @@
1
+ {"name":"Conductor Remote","short_name":"Conductor","description":"Monitor and drive your local Conductor agents from your phone.","start_url":"/","display":"standalone","background_color":"#0a0b0e","theme_color":"#0a0b0e","lang":"en","scope":"/","orientation":"portrait","icons":[{"src":"/icon-192.png","sizes":"192x192","type":"image/png"},{"src":"/icon-512.png","sizes":"512x512","type":"image/png"},{"src":"/icon-maskable-512.png","sizes":"512x512","type":"image/png","purpose":"maskable"},{"src":"/icon.svg","sizes":"any","type":"image/svg+xml"}]}
package/dist/sw.js ADDED
@@ -0,0 +1 @@
1
+ if(!self.define){let e,i={};const n=(n,s)=>(n=new URL(n+".js",s).href,i[n]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=i,document.head.appendChild(e)}else e=n,importScripts(n),i()}).then(()=>{let e=i[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(s,o)=>{const r=e||("document"in self?document.currentScript.src:"")||location.href;if(i[r])return;let c={};const t=e=>n(e,r),l={module:{uri:r},exports:c,require:t};i[r]=Promise.all(s.map(e=>l[e]||t(e))).then(e=>(o(...e),c))}}define(["./workbox-9c191d2f"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"index.html",revision:"6e8372abc9c23734781047fec16901ca"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-DtY88CkY.js",revision:null},{url:"assets/index-DiiwvzTm.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\//]}))});
@@ -0,0 +1 @@
1
+ define(["exports"],function(t){"use strict";try{self["workbox:core:7.4.0"]&&_()}catch(t){}const e=(t,...e)=>{let s=t;return e.length>0&&(s+=` :: ${JSON.stringify(e)}`),s};class s extends Error{constructor(t,s){super(e(t,s)),this.name=t,this.details=s}}try{self["workbox:routing:7.4.0"]&&_()}catch(t){}const n=t=>t&&"object"==typeof t?t:{handle:t};class i{constructor(t,e,s="GET"){this.handler=n(e),this.match=t,this.method=s}setCatchHandler(t){this.catchHandler=n(t)}}class r extends i{constructor(t,e,s){super(({url:e})=>{const s=t.exec(e.href);if(s&&(e.origin===location.origin||0===s.index))return s.slice(1)},e,s)}}class o{constructor(){this.t=new Map,this.i=new Map}get routes(){return this.t}addFetchListener(){self.addEventListener("fetch",t=>{const{request:e}=t,s=this.handleRequest({request:e,event:t});s&&t.respondWith(s)})}addCacheListener(){self.addEventListener("message",t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,s=Promise.all(e.urlsToCache.map(e=>{"string"==typeof e&&(e=[e]);const s=new Request(...e);return this.handleRequest({request:s,event:t})}));t.waitUntil(s),t.ports&&t.ports[0]&&s.then(()=>t.ports[0].postMessage(!0))}})}handleRequest({request:t,event:e}){const s=new URL(t.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:i,route:r}=this.findMatchingRoute({event:e,request:t,sameOrigin:n,url:s});let o=r&&r.handler;const c=t.method;if(!o&&this.i.has(c)&&(o=this.i.get(c)),!o)return;let a;try{a=o.handle({url:s,request:t,event:e,params:i})}catch(t){a=Promise.reject(t)}const h=r&&r.catchHandler;return a instanceof Promise&&(this.o||h)&&(a=a.catch(async n=>{if(h)try{return await h.handle({url:s,request:t,event:e,params:i})}catch(t){t instanceof Error&&(n=t)}if(this.o)return this.o.handle({url:s,request:t,event:e});throw n})),a}findMatchingRoute({url:t,sameOrigin:e,request:s,event:n}){const i=this.t.get(s.method)||[];for(const r of i){let i;const o=r.match({url:t,sameOrigin:e,request:s,event:n});if(o)return i=o,(Array.isArray(i)&&0===i.length||o.constructor===Object&&0===Object.keys(o).length||"boolean"==typeof o)&&(i=void 0),{route:r,params:i}}return{}}setDefaultHandler(t,e="GET"){this.i.set(e,n(t))}setCatchHandler(t){this.o=n(t)}registerRoute(t){this.t.has(t.method)||this.t.set(t.method,[]),this.t.get(t.method).push(t)}unregisterRoute(t){if(!this.t.has(t.method))throw new s("unregister-route-but-not-found-with-method",{method:t.method});const e=this.t.get(t.method).indexOf(t);if(!(e>-1))throw new s("unregister-route-route-not-registered");this.t.get(t.method).splice(e,1)}}let c;const a=()=>(c||(c=new o,c.addFetchListener(),c.addCacheListener()),c);function h(t,e,n){let o;if("string"==typeof t){const s=new URL(t,location.href);o=new i(({url:t})=>t.href===s.href,e,n)}else if(t instanceof RegExp)o=new r(t,e,n);else if("function"==typeof t)o=new i(t,e,n);else{if(!(t instanceof i))throw new s("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});o=t}return a().registerRoute(o),o}const u={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},l=t=>[u.prefix,t,u.suffix].filter(t=>t&&t.length>0).join("-"),f=t=>t||l(u.precache),w=t=>t||l(u.runtime);function d(t,e){const s=e();return t.waitUntil(s),s}try{self["workbox:precaching:7.4.0"]&&_()}catch(t){}function p(t){if(!t)throw new s("add-to-cache-list-unexpected-type",{entry:t});if("string"==typeof t){const e=new URL(t,location.href);return{cacheKey:e.href,url:e.href}}const{revision:e,url:n}=t;if(!n)throw new s("add-to-cache-list-unexpected-type",{entry:t});if(!e){const t=new URL(n,location.href);return{cacheKey:t.href,url:t.href}}const i=new URL(n,location.href),r=new URL(n,location.href);return i.searchParams.set("__WB_REVISION__",e),{cacheKey:i.href,url:r.href}}class y{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:t,state:e})=>{e&&(e.originalRequest=t)},this.cachedResponseWillBeUsed=async({event:t,state:e,cachedResponse:s})=>{if("install"===t.type&&e&&e.originalRequest&&e.originalRequest instanceof Request){const t=e.originalRequest.url;s?this.notUpdatedURLs.push(t):this.updatedURLs.push(t)}return s}}}class g{constructor({precacheController:t}){this.cacheKeyWillBeUsed=async({request:t,params:e})=>{const s=(null==e?void 0:e.cacheKey)||this.h.getCacheKeyForURL(t.url);return s?new Request(s,{headers:t.headers}):t},this.h=t}}let R;async function m(t,e){let n=null;if(t.url){n=new URL(t.url).origin}if(n!==self.location.origin)throw new s("cross-origin-copy-response",{origin:n});const i=t.clone(),r={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=function(){if(void 0===R){const t=new Response("");if("body"in t)try{new Response(t.body),R=!0}catch(t){R=!1}R=!1}return R}()?i.body:await i.blob();return new Response(o,r)}function v(t,e){const s=new URL(t);for(const t of e)s.searchParams.delete(t);return s.href}class q{constructor(){this.promise=new Promise((t,e)=>{this.resolve=t,this.reject=e})}}const U=new Set;try{self["workbox:strategies:7.4.0"]&&_()}catch(t){}function L(t){return"string"==typeof t?new Request(t):t}class b{constructor(t,e){this.u={},Object.assign(this,e),this.event=e.event,this.l=t,this.p=new q,this.R=[],this.m=[...t.plugins],this.v=new Map;for(const t of this.m)this.v.set(t,{});this.event.waitUntil(this.p.promise)}async fetch(t){const{event:e}=this;let n=L(t);if("navigate"===n.mode&&e instanceof FetchEvent&&e.preloadResponse){const t=await e.preloadResponse;if(t)return t}const i=this.hasCallback("fetchDidFail")?n.clone():null;try{for(const t of this.iterateCallbacks("requestWillFetch"))n=await t({request:n.clone(),event:e})}catch(t){if(t instanceof Error)throw new s("plugin-error-request-will-fetch",{thrownErrorMessage:t.message})}const r=n.clone();try{let t;t=await fetch(n,"navigate"===n.mode?void 0:this.l.fetchOptions);for(const s of this.iterateCallbacks("fetchDidSucceed"))t=await s({event:e,request:r,response:t});return t}catch(t){throw i&&await this.runCallbacks("fetchDidFail",{error:t,event:e,originalRequest:i.clone(),request:r.clone()}),t}}async fetchAndCachePut(t){const e=await this.fetch(t),s=e.clone();return this.waitUntil(this.cachePut(t,s)),e}async cacheMatch(t){const e=L(t);let s;const{cacheName:n,matchOptions:i}=this.l,r=await this.getCacheKey(e,"read"),o=Object.assign(Object.assign({},i),{cacheName:n});s=await caches.match(r,o);for(const t of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await t({cacheName:n,matchOptions:i,cachedResponse:s,request:r,event:this.event})||void 0;return s}async cachePut(t,e){const n=L(t);var i;await(i=0,new Promise(t=>setTimeout(t,i)));const r=await this.getCacheKey(n,"write");if(!e)throw new s("cache-put-with-no-response",{url:(o=r.url,new URL(String(o),location.href).href.replace(new RegExp(`^${location.origin}`),""))});var o;const c=await this.q(e);if(!c)return!1;const{cacheName:a,matchOptions:h}=this.l,u=await self.caches.open(a),l=this.hasCallback("cacheDidUpdate"),f=l?await async function(t,e,s,n){const i=v(e.url,s);if(e.url===i)return t.match(e,n);const r=Object.assign(Object.assign({},n),{ignoreSearch:!0}),o=await t.keys(e,r);for(const e of o)if(i===v(e.url,s))return t.match(e,n)}(u,r.clone(),["__WB_REVISION__"],h):null;try{await u.put(r,l?c.clone():c)}catch(t){if(t instanceof Error)throw"QuotaExceededError"===t.name&&await async function(){for(const t of U)await t()}(),t}for(const t of this.iterateCallbacks("cacheDidUpdate"))await t({cacheName:a,oldResponse:f,newResponse:c.clone(),request:r,event:this.event});return!0}async getCacheKey(t,e){const s=`${t.url} | ${e}`;if(!this.u[s]){let n=t;for(const t of this.iterateCallbacks("cacheKeyWillBeUsed"))n=L(await t({mode:e,request:n,event:this.event,params:this.params}));this.u[s]=n}return this.u[s]}hasCallback(t){for(const e of this.l.plugins)if(t in e)return!0;return!1}async runCallbacks(t,e){for(const s of this.iterateCallbacks(t))await s(e)}*iterateCallbacks(t){for(const e of this.l.plugins)if("function"==typeof e[t]){const s=this.v.get(e),n=n=>{const i=Object.assign(Object.assign({},n),{state:s});return e[t](i)};yield n}}waitUntil(t){return this.R.push(t),t}async doneWaiting(){for(;this.R.length;){const t=this.R.splice(0),e=(await Promise.allSettled(t)).find(t=>"rejected"===t.status);if(e)throw e.reason}}destroy(){this.p.resolve(null)}async q(t){let e=t,s=!1;for(const t of this.iterateCallbacks("cacheWillUpdate"))if(e=await t({request:this.request,response:e,event:this.event})||void 0,s=!0,!e)break;return s||e&&200!==e.status&&(e=void 0),e}}class C{constructor(t={}){this.cacheName=w(t.cacheName),this.plugins=t.plugins||[],this.fetchOptions=t.fetchOptions,this.matchOptions=t.matchOptions}handle(t){const[e]=this.handleAll(t);return e}handleAll(t){t instanceof FetchEvent&&(t={event:t,request:t.request});const e=t.event,s="string"==typeof t.request?new Request(t.request):t.request,n="params"in t?t.params:void 0,i=new b(this,{event:e,request:s,params:n}),r=this.U(i,s,e);return[r,this.L(r,i,s,e)]}async U(t,e,n){let i;await t.runCallbacks("handlerWillStart",{event:n,request:e});try{if(i=await this._(e,t),!i||"error"===i.type)throw new s("no-response",{url:e.url})}catch(s){if(s instanceof Error)for(const r of t.iterateCallbacks("handlerDidError"))if(i=await r({error:s,event:n,request:e}),i)break;if(!i)throw s}for(const s of t.iterateCallbacks("handlerWillRespond"))i=await s({event:n,request:e,response:i});return i}async L(t,e,s,n){let i,r;try{i=await t}catch(r){}try{await e.runCallbacks("handlerDidRespond",{event:n,request:s,response:i}),await e.doneWaiting()}catch(t){t instanceof Error&&(r=t)}if(await e.runCallbacks("handlerDidComplete",{event:n,request:s,response:i,error:r}),e.destroy(),r)throw r}}class E extends C{constructor(t={}){t.cacheName=f(t.cacheName),super(t),this.C=!1!==t.fallbackToNetwork,this.plugins.push(E.copyRedirectedCacheableResponsesPlugin)}async _(t,e){const s=await e.cacheMatch(t);return s||(e.event&&"install"===e.event.type?await this.O(t,e):await this.N(t,e))}async N(t,e){let n;const i=e.params||{};if(!this.C)throw new s("missing-precache-entry",{cacheName:this.cacheName,url:t.url});{const s=i.integrity,r=t.integrity,o=!r||r===s;n=await e.fetch(new Request(t,{integrity:"no-cors"!==t.mode?r||s:void 0})),s&&o&&"no-cors"!==t.mode&&(this.P(),await e.cachePut(t,n.clone()))}return n}async O(t,e){this.P();const n=await e.fetch(t);if(!await e.cachePut(t,n.clone()))throw new s("bad-precaching-response",{url:t.url,status:n.status});return n}P(){let t=null,e=0;for(const[s,n]of this.plugins.entries())n!==E.copyRedirectedCacheableResponsesPlugin&&(n===E.defaultPrecacheCacheabilityPlugin&&(t=s),n.cacheWillUpdate&&e++);0===e?this.plugins.push(E.defaultPrecacheCacheabilityPlugin):e>1&&null!==t&&this.plugins.splice(t,1)}}E.defaultPrecacheCacheabilityPlugin={cacheWillUpdate:async({response:t})=>!t||t.status>=400?null:t},E.copyRedirectedCacheableResponsesPlugin={cacheWillUpdate:async({response:t})=>t.redirected?await m(t):t};class O{constructor({cacheName:t,plugins:e=[],fallbackToNetwork:s=!0}={}){this.j=new Map,this.k=new Map,this.K=new Map,this.l=new E({cacheName:f(t),plugins:[...e,new g({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this.l}precache(t){this.addToCacheList(t),this.T||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this.T=!0)}addToCacheList(t){const e=[];for(const n of t){"string"==typeof n?e.push(n):n&&void 0===n.revision&&e.push(n.url);const{cacheKey:t,url:i}=p(n),r="string"!=typeof n&&n.revision?"reload":"default";if(this.j.has(i)&&this.j.get(i)!==t)throw new s("add-to-cache-list-conflicting-entries",{firstEntry:this.j.get(i),secondEntry:t});if("string"!=typeof n&&n.integrity){if(this.K.has(t)&&this.K.get(t)!==n.integrity)throw new s("add-to-cache-list-conflicting-integrities",{url:i});this.K.set(t,n.integrity)}if(this.j.set(i,t),this.k.set(i,r),e.length>0){const t=`Workbox is precaching URLs without revision info: ${e.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(t)}}}install(t){return d(t,async()=>{const e=new y;this.strategy.plugins.push(e);for(const[e,s]of this.j){const n=this.K.get(s),i=this.k.get(e),r=new Request(e,{integrity:n,cache:i,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:s},request:r,event:t}))}const{updatedURLs:s,notUpdatedURLs:n}=e;return{updatedURLs:s,notUpdatedURLs:n}})}activate(t){return d(t,async()=>{const t=await self.caches.open(this.strategy.cacheName),e=await t.keys(),s=new Set(this.j.values()),n=[];for(const i of e)s.has(i.url)||(await t.delete(i),n.push(i.url));return{deletedURLs:n}})}getURLsToCacheKeys(){return this.j}getCachedURLs(){return[...this.j.keys()]}getCacheKeyForURL(t){const e=new URL(t,location.href);return this.j.get(e.href)}getIntegrityForCacheKey(t){return this.K.get(t)}async matchPrecache(t){const e=t instanceof Request?t.url:t,s=this.getCacheKeyForURL(e);if(s){return(await self.caches.open(this.strategy.cacheName)).match(s)}}createHandlerBoundToURL(t){const e=this.getCacheKeyForURL(t);if(!e)throw new s("non-precached-url",{url:t});return s=>(s.request=new Request(t),s.params=Object.assign({cacheKey:e},s.params),this.strategy.handle(s))}}let x;const N=()=>(x||(x=new O),x);class P extends i{constructor(t,e){super(({request:s})=>{const n=t.getURLsToCacheKeys();for(const i of function*(t,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:s="index.html",cleanURLs:n=!0,urlManipulation:i}={}){const r=new URL(t,location.href);r.hash="",yield r.href;const o=function(t,e=[]){for(const s of[...t.searchParams.keys()])e.some(t=>t.test(s))&&t.searchParams.delete(s);return t}(r,e);if(yield o.href,s&&o.pathname.endsWith("/")){const t=new URL(o.href);t.pathname+=s,yield t.href}if(n){const t=new URL(o.href);t.pathname+=".html",yield t.href}if(i){const t=i({url:r});for(const e of t)yield e.href}}(s.url,e)){const e=n.get(i);if(e){return{cacheKey:e,integrity:t.getIntegrityForCacheKey(e)}}}},t.strategy)}}t.NavigationRoute=class extends i{constructor(t,{allowlist:e=[/./],denylist:s=[]}={}){super(t=>this.W(t),t),this.M=e,this.S=s}W({url:t,request:e}){if(e&&"navigate"!==e.mode)return!1;const s=t.pathname+t.search;for(const t of this.S)if(t.test(s))return!1;return!!this.M.some(t=>t.test(s))}},t.cleanupOutdatedCaches=function(){self.addEventListener("activate",t=>{const e=f();t.waitUntil((async(t,e="-precache-")=>{const s=(await self.caches.keys()).filter(s=>s.includes(e)&&s.includes(self.registration.scope)&&s!==t);return await Promise.all(s.map(t=>self.caches.delete(t))),s})(e).then(t=>{}))})},t.clientsClaim=function(){self.addEventListener("activate",()=>self.clients.claim())},t.createHandlerBoundToURL=function(t){return N().createHandlerBoundToURL(t)},t.precacheAndRoute=function(t,e){!function(t){N().precache(t)}(t),function(t){const e=N();h(new P(e,t))}(e)},t.registerRoute=h});
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "conductor-remote",
3
+ "version": "0.0.0-development",
4
+ "type": "module",
5
+ "packageManager": "yarn@4.15.0",
6
+ "description": "Phone control panel for local Conductor agents. Reads ride SQLite + git; prompts ride Conductor's own dispatch path.",
7
+ "license": "MIT",
8
+ "author": "Eivind Hyldmo",
9
+ "homepage": "https://github.com/hyldmo/conductor-remote#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/hyldmo/conductor-remote.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/hyldmo/conductor-remote/issues"
16
+ },
17
+ "keywords": [
18
+ "conductor",
19
+ "claude-code",
20
+ "coding-agents",
21
+ "remote-control",
22
+ "pwa",
23
+ "tailscale",
24
+ "sqlite"
25
+ ],
26
+ "engines": {
27
+ "node": ">=24"
28
+ },
29
+ "bin": "bin/cli.js",
30
+ "files": [
31
+ "bin",
32
+ "src",
33
+ "scripts",
34
+ "dist"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "provenance": true
39
+ },
40
+ "scripts": {
41
+ "start": "node bin/cli.js",
42
+ "relay": "node --watch bin/cli.js",
43
+ "dev": "node scripts/dev.ts",
44
+ "dev:web": "vite",
45
+ "build": "vite build",
46
+ "preview": "yarn build && yarn start",
47
+ "gen:icons": "node scripts/gen-icons.ts",
48
+ "deploy": "yarn build && node bin/cli.js service install",
49
+ "service": "node bin/cli.js service",
50
+ "typecheck": "tsc -p tsconfig.json",
51
+ "lint": "biome check .",
52
+ "fix": "biome check . --fix",
53
+ "verify": "yarn typecheck && yarn lint",
54
+ "release": "semantic-release",
55
+ "prepack": "yarn build",
56
+ "postinstall": "husky || true"
57
+ },
58
+ "devDependencies": {
59
+ "@biomejs/biome": "^2.3.8",
60
+ "@commitlint/cli": "^20.4.2",
61
+ "@commitlint/config-conventional": "^20.4.2",
62
+ "@tailwindcss/vite": "^4.1.18",
63
+ "@tanstack/react-query": "^5.90.20",
64
+ "@types/node": "^24.7.0",
65
+ "@types/react": "^19.2.10",
66
+ "@types/react-dom": "^19.2.3",
67
+ "@vitejs/plugin-react": "^5.0.4",
68
+ "clsx": "^2.1.1",
69
+ "husky": "^9.1.7",
70
+ "lucide-react": "^0.563.0",
71
+ "react": "^19.2.4",
72
+ "react-dom": "^19.2.4",
73
+ "react-markdown": "^10.1.0",
74
+ "react-router": "^7.15.0",
75
+ "remark-breaks": "^4.0.0",
76
+ "remark-gfm": "^4.0.1",
77
+ "semantic-release": "^25.0.3",
78
+ "sharp": "^0.34.5",
79
+ "tailwind-merge": "^3.4.0",
80
+ "tailwindcss": "^4.1.18",
81
+ "typescript": "^5.9.3",
82
+ "vite": "^7.3.1",
83
+ "vite-plugin-pwa": "^1.2.0",
84
+ "zustand": "^5.0.12"
85
+ }
86
+ }
package/scripts/dev.ts ADDED
@@ -0,0 +1,75 @@
1
+ import { spawn } from 'node:child_process'
2
+ import net from 'node:net'
3
+
4
+ // Run the Vite dev server (HMR PWA) and the relay (API + reads) together.
5
+ // Vite proxies /api → the relay so the phone hits a single origin.
6
+ //
7
+ // Dev-only per-workspace ports: inside a Conductor workspace, $CONDUCTOR_PORT is
8
+ // unique per workspace, so several `yarn dev`s can run concurrently. Vite (the
9
+ // origin a browser/phone opens) takes that port; the relay gets a free ephemeral
10
+ // port and Vite proxies /api to it on loopback. Outside Conductor, fall back to
11
+ // the classic 5173 (web) / 8787 (relay) pair. Prod (`yarn start`/`deploy`/the
12
+ // LaunchAgent) never runs through here, so its Tailscale bind and RELAY_PORT
13
+ // default stay untouched — this clutters neither the prod path nor the build.
14
+
15
+ /** Ask the OS for an unused TCP port so a Conductor workspace's relay never collides. */
16
+ function freePort(): Promise<number> {
17
+ return new Promise((resolve, reject) => {
18
+ const srv = net.createServer()
19
+ srv.once('error', reject)
20
+ srv.listen(0, '127.0.0.1', () => {
21
+ const { port } = srv.address() as net.AddressInfo
22
+ srv.close(() => resolve(port))
23
+ })
24
+ })
25
+ }
26
+
27
+ const conductorPort = Number(process.env.CONDUCTOR_PORT) || 0
28
+ const webPort = conductorPort || 5173
29
+ // Loopback bind + Vite proxy target must match; only prod auto-binds the Tailscale NIC.
30
+ const relayPort = conductorPort ? await freePort() : 8787
31
+
32
+ const procs = [
33
+ {
34
+ name: 'web ',
35
+ cmd: 'yarn',
36
+ args: ['dev:web'],
37
+ color: '\x1b[35m',
38
+ env: { ...process.env, WEB_PORT: String(webPort), RELAY_PORT: String(relayPort) }
39
+ },
40
+ {
41
+ name: 'relay',
42
+ cmd: 'node',
43
+ args: ['--watch', 'bin/cli.js'],
44
+ color: '\x1b[36m',
45
+ env: { ...process.env, RELAY_PORT: String(relayPort), RELAY_HOST: process.env.RELAY_HOST ?? '127.0.0.1' }
46
+ }
47
+ ]
48
+
49
+ const children = procs.map(p => {
50
+ const child = spawn(p.cmd, p.args, { stdio: ['inherit', 'pipe', 'pipe'], env: p.env })
51
+ const tag = `${p.color}[${p.name}]\x1b[0m `
52
+ const pipe = (stream: NodeJS.ReadableStream) => {
53
+ let buf = ''
54
+ stream.on('data', (d: Buffer) => {
55
+ buf += d.toString()
56
+ let nl = buf.indexOf('\n')
57
+ while (nl >= 0) {
58
+ console.log(tag + buf.slice(0, nl))
59
+ buf = buf.slice(nl + 1)
60
+ nl = buf.indexOf('\n')
61
+ }
62
+ })
63
+ }
64
+ pipe(child.stdout)
65
+ pipe(child.stderr)
66
+ return child
67
+ })
68
+
69
+ const shutdown = () => {
70
+ for (const c of children) c.kill('SIGTERM')
71
+ process.exit(0)
72
+ }
73
+ process.on('SIGINT', shutdown)
74
+ process.on('SIGTERM', shutdown)
75
+ for (const c of children) c.on('exit', shutdown)
@@ -0,0 +1,42 @@
1
+ // ── Conductor IPC reconnaissance snippet ──────────────────────────────────
2
+ //
3
+ // PREREQUISITE (read FINDINGS.md first): this only works if Conductor's WKWeb
4
+ // view is inspectable. On the shipped build it is NOT — the app is hardened/
5
+ // notarized without `get-task-allow`, so Safari's Develop menu won't list it
6
+ // and there is no console to paste this into. Injection is therefore blocked;
7
+ // the relay drives writes via Accessibility instead. Keep this snippet for the
8
+ // case where a future build ships with devtools enabled.
9
+ //
10
+ // IF a console is available (Safari ▸ Develop ▸ [Conductor] ▸ [webview]):
11
+ // paste this, then perform the action you want to reverse-engineer (send a
12
+ // prompt, approve a tool, stop a session). Every Tauri IPC call is logged with
13
+ // its command name + payload — that is the undocumented write API.
14
+
15
+ ;(() => {
16
+ const internals = window.__TAURI_INTERNALS__
17
+ if (!internals || typeof internals.invoke !== 'function') {
18
+ console.warn('[recon] __TAURI_INTERNALS__.invoke not found — not a Tauri webview or not exposed.')
19
+ return
20
+ }
21
+ if (internals.__reconWrapped) {
22
+ console.info('[recon] already wrapped.')
23
+ return
24
+ }
25
+ const original = internals.invoke.bind(internals)
26
+ const log = []
27
+ window.__reconLog = log
28
+ internals.invoke = (cmd, payload, options) => {
29
+ // Stock Tauri plugin traffic is noise; Conductor's own commands are the signal.
30
+ const interesting = !String(cmd).startsWith('plugin:')
31
+ const record = { t: new Date().toISOString(), cmd, payload }
32
+ if (interesting) {
33
+ log.push(record)
34
+ console.info('%c[INVOKE]', 'color:#4a9eff', cmd, payload)
35
+ }
36
+ return original(cmd, payload, options)
37
+ }
38
+ internals.__reconWrapped = true
39
+ console.info(
40
+ '[recon] invoke wrapped. Reproduce an action, then run: copy(JSON.stringify(window.__reconLog, null, 2))'
41
+ )
42
+ })()
@@ -0,0 +1,22 @@
1
+ import { readFile, writeFile } from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import sharp from 'sharp'
4
+
5
+ // Rasterize web/public/icon.svg into the PNG sizes the PWA manifest + iOS need.
6
+ const pub = path.join(import.meta.dirname, '..', 'web', 'public')
7
+ const svg = await readFile(path.join(pub, 'icon.svg'))
8
+
9
+ const targets: { name: string; size: number; background?: string }[] = [
10
+ { name: 'icon-192.png', size: 192 },
11
+ { name: 'icon-512.png', size: 512 },
12
+ { name: 'icon-maskable-512.png', size: 512 },
13
+ { name: 'apple-touch-icon.png', size: 180, background: '#0a0b0e' }
14
+ ]
15
+
16
+ for (const t of targets) {
17
+ let pipe = sharp(svg, { density: 384 }).resize(t.size, t.size, { fit: 'contain' })
18
+ if (t.background) pipe = pipe.flatten({ background: t.background })
19
+ const out = await pipe.png().toBuffer()
20
+ await writeFile(path.join(pub, t.name), out)
21
+ console.info(`wrote ${t.name} (${t.size}px)`)
22
+ }
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Deploy the relay as a macOS LaunchAgent — the only "deployment" this app has, since it must run
3
+ * on the Mac that runs Conductor (local SQLite DB, git worktrees, and the sidecar unix socket all
4
+ * live there). Installs a per-user agent that starts the relay on login and keeps it alive.
5
+ *
6
+ * node scripts/service.ts <install|uninstall|status|restart>
7
+ * (or, once installed globally: `conductor-remote service <...>`)
8
+ *
9
+ * `yarn deploy` builds dist/ first, then runs `install`.
10
+ */
11
+ import { execFileSync } from 'node:child_process'
12
+ import fs from 'node:fs'
13
+ import os from 'node:os'
14
+ import path from 'node:path'
15
+
16
+ const LABEL = 'no.adluna.conductor-remote'
17
+ const projectDir = path.resolve(import.meta.dirname, '..')
18
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`)
19
+ const logDir = path.join(os.homedir(), 'Library', 'Logs', 'conductor-remote')
20
+ const uid = process.getuid?.() ?? 0
21
+ const domain = `gui/${uid}`
22
+
23
+ function xml(s: string): string {
24
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
25
+ }
26
+
27
+ /** Run launchctl, swallowing the exit code so "already-loaded"/"not-loaded" states aren't fatal. */
28
+ function launchctl(...args: string[]): void {
29
+ try {
30
+ execFileSync('launchctl', args, { stdio: 'pipe' })
31
+ } catch {
32
+ // non-zero is expected for bootout-when-absent etc.; state is asserted by the caller's sequence
33
+ }
34
+ }
35
+
36
+ /** Block the main thread briefly — used to let launchd settle between bootout and bootstrap. */
37
+ function sleepSync(ms: number): void {
38
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
39
+ }
40
+
41
+ /** Is the agent currently bootstrapped into the user domain? */
42
+ function serviceLoaded(): boolean {
43
+ try {
44
+ execFileSync('launchctl', ['print', `${domain}/${LABEL}`], { stdio: 'pipe' })
45
+ return true
46
+ } catch {
47
+ return false
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Reload the agent from the freshly written plist. `bootout` of a *running* instance is asynchronous,
53
+ * so we wait for it to fully unload before `bootstrap` — otherwise bootstrap races the teardown and
54
+ * fails silently, leaving the relay down after a re-deploy. Bootstrap is retried and its failure is fatal.
55
+ */
56
+ function reloadAgent(): void {
57
+ launchctl('bootout', `${domain}/${LABEL}`)
58
+ for (let i = 0; i < 30 && serviceLoaded(); i++) sleepSync(100)
59
+ let bootstrapped = false
60
+ for (let i = 0; i < 10 && !bootstrapped; i++) {
61
+ try {
62
+ execFileSync('launchctl', ['bootstrap', domain, plistPath], { stdio: 'pipe' })
63
+ bootstrapped = true
64
+ } catch {
65
+ sleepSync(150)
66
+ }
67
+ }
68
+ if (!bootstrapped) {
69
+ console.error(`✗ launchctl bootstrap failed for ${plistPath}`)
70
+ console.error(` Inspect with: launchctl print ${domain}/${LABEL}`)
71
+ process.exit(1)
72
+ }
73
+ launchctl('enable', `${domain}/${LABEL}`)
74
+ launchctl('kickstart', '-k', `${domain}/${LABEL}`)
75
+ }
76
+
77
+ /** Node runs the relay via the flag-free CLI shim; the absolute execPath is baked at install time. */
78
+ function buildPlist(): string {
79
+ const node = xml(process.execPath)
80
+ const proj = xml(projectDir)
81
+ const out = xml(path.join(logDir, 'relay.log'))
82
+ const err = xml(path.join(logDir, 'relay.err.log'))
83
+ // Optional overrides carried into the daemon's environment.
84
+ const envEntries: Array<[string, string]> = [['PATH', '/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin']]
85
+ if (process.env.WRITE_STRATEGY) envEntries.push(['WRITE_STRATEGY', process.env.WRITE_STRATEGY])
86
+ if (process.env.RELAY_HOST) envEntries.push(['RELAY_HOST', process.env.RELAY_HOST])
87
+ if (process.env.RELAY_PORT) envEntries.push(['RELAY_PORT', process.env.RELAY_PORT])
88
+ const envXml = envEntries.map(([k, v]) => `\t\t<key>${xml(k)}</key>\n\t\t<string>${xml(v)}</string>`).join('\n')
89
+ return `<?xml version="1.0" encoding="UTF-8"?>
90
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
91
+ <plist version="1.0">
92
+ <dict>
93
+ <key>Label</key>
94
+ <string>${LABEL}</string>
95
+ <key>ProgramArguments</key>
96
+ <array>
97
+ <string>${node}</string>
98
+ <string>${proj}/bin/cli.js</string>
99
+ </array>
100
+ <key>WorkingDirectory</key>
101
+ <string>${proj}</string>
102
+ <key>EnvironmentVariables</key>
103
+ <dict>
104
+ ${envXml}
105
+ </dict>
106
+ <key>RunAtLoad</key>
107
+ <true/>
108
+ <key>KeepAlive</key>
109
+ <true/>
110
+ <key>ProcessType</key>
111
+ <string>Background</string>
112
+ <key>StandardOutPath</key>
113
+ <string>${out}</string>
114
+ <key>StandardErrorPath</key>
115
+ <string>${err}</string>
116
+ </dict>
117
+ </plist>
118
+ `
119
+ }
120
+
121
+ function distBuilt(): boolean {
122
+ return fs.existsSync(path.join(projectDir, 'dist', 'index.html'))
123
+ }
124
+
125
+ /** Read the persisted token (or env override) purely to print the phone URL — never mints one. */
126
+ function currentToken(): string | null {
127
+ if (process.env.RELAY_TOKEN) return process.env.RELAY_TOKEN
128
+ try {
129
+ return (
130
+ fs
131
+ .readFileSync(path.join(os.homedir(), 'Library', 'Application Support', 'conductor-remote', 'token'), 'utf8')
132
+ .trim() || null
133
+ )
134
+ } catch {
135
+ return null
136
+ }
137
+ }
138
+
139
+ const RELAY_PORT = process.env.RELAY_PORT ?? '8787'
140
+
141
+ /** Locate the tailscale CLI: PATH first, then the common macOS install locations. Null if absent. */
142
+ function tailscaleBin(): string | null {
143
+ for (const bin of [
144
+ 'tailscale',
145
+ '/opt/homebrew/bin/tailscale',
146
+ '/usr/local/bin/tailscale',
147
+ '/Applications/Tailscale.app/Contents/MacOS/Tailscale'
148
+ ]) {
149
+ try {
150
+ execFileSync(bin, ['version'], { stdio: 'pipe' })
151
+ return bin
152
+ } catch {
153
+ // try the next candidate
154
+ }
155
+ }
156
+ return null
157
+ }
158
+
159
+ /** This node's MagicDNS name without the trailing dot, e.g. `mac.taila6dcd6.ts.net`. */
160
+ function magicDnsName(bin: string): string | null {
161
+ try {
162
+ const out = execFileSync(bin, ['status', '--json'], { encoding: 'utf8', stdio: 'pipe' })
163
+ return (JSON.parse(out)?.Self?.DNSName ?? '').replace(/\.$/, '') || null
164
+ } catch {
165
+ return null
166
+ }
167
+ }
168
+
169
+ /** Is `tailscale serve` already proxying https://<dns>/ to the loopback relay? Keeps the wiring idempotent. */
170
+ function serveConfigured(bin: string, dns: string): boolean {
171
+ try {
172
+ const out = execFileSync(bin, ['serve', 'status', '--json'], { encoding: 'utf8', stdio: 'pipe' })
173
+ const proxy = JSON.parse(out)?.Web?.[`${dns}:443`]?.Handlers?.['/']?.Proxy
174
+ return proxy === `http://127.0.0.1:${RELAY_PORT}`
175
+ } catch {
176
+ return false
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Wire the relay to a stable HTTPS tailnet URL via `tailscale serve` (tailnet-only — NOT Funnel).
182
+ * Idempotent and non-fatal: the relay binds loopback regardless, so a failure here just means the phone
183
+ * URL isn't set up yet, and we print how to do it by hand. Real TLS also satisfies the PWA's secure-context
184
+ * requirement (a service worker won't register over plain http on a 100.x IP).
185
+ */
186
+ function ensureServe(): void {
187
+ const bin = tailscaleBin()
188
+ if (!bin) {
189
+ console.info('\n ⚠ tailscale CLI not found — skipped serve setup. Once Tailscale is installed, run:')
190
+ console.info(` tailscale serve --bg ${RELAY_PORT}`)
191
+ return
192
+ }
193
+ const dns = magicDnsName(bin)
194
+ if (dns && serveConfigured(bin, dns)) {
195
+ console.info(`✓ tailscale serve already fronts https://${dns}/ → 127.0.0.1:${RELAY_PORT}`)
196
+ return
197
+ }
198
+ try {
199
+ execFileSync(bin, ['serve', '--bg', RELAY_PORT], { stdio: 'pipe' })
200
+ console.info(`✓ tailscale serve → https://${dns ?? '<node>'}/ now proxies 127.0.0.1:${RELAY_PORT} (tailnet-only)`)
201
+ } catch (err) {
202
+ console.info(
203
+ `\n ⚠ could not configure tailscale serve (${err instanceof Error ? err.message : err}). Run by hand:`
204
+ )
205
+ console.info(` tailscale serve --bg ${RELAY_PORT}`)
206
+ }
207
+ }
208
+
209
+ function printUrl(): void {
210
+ const frag = `#token=${currentToken() ?? '<starts on first run>'}`
211
+ const bin = tailscaleBin()
212
+ const dns = bin ? magicDnsName(bin) : null
213
+ if (bin && dns && serveConfigured(bin, dns)) {
214
+ console.info(`\n Phone URL (same Tailnet, HTTPS):\n https://${dns}/${frag}`)
215
+ return
216
+ }
217
+ // Serve not wired yet — the relay is only on loopback.
218
+ console.info(`\n Local URL:\n http://127.0.0.1:${RELAY_PORT}/${frag}`)
219
+ console.info(
220
+ `\n ⚠ Not reachable from your phone yet. Run \`tailscale serve --bg ${RELAY_PORT}\`${dns ? ` → https://${dns}/` : ''}, then \`yarn service status\`.`
221
+ )
222
+ }
223
+
224
+ /** npx unpacks into a throwaway cache that gets purged; a LaunchAgent baked against it would rot. */
225
+ function isEphemeralInstall(dir: string): boolean {
226
+ return /[\\/]_npx[\\/]|[\\/]\.npm[\\/]_npx[\\/]/.test(dir)
227
+ }
228
+
229
+ function install(): void {
230
+ if (isEphemeralInstall(projectDir)) {
231
+ console.error(
232
+ `✗ refusing to install from an npx cache path:\n ${projectDir}\n` +
233
+ ' That directory is temporary and gets purged, which would break the LaunchAgent.\n' +
234
+ ' Install globally first: `npm i -g conductor-remote`, then `conductor-remote service install`.'
235
+ )
236
+ process.exit(1)
237
+ }
238
+ if (!distBuilt()) {
239
+ console.error('✗ dist/ not built. Run `yarn build` first (or use `yarn deploy`, which builds).')
240
+ process.exit(1)
241
+ }
242
+ fs.mkdirSync(path.dirname(plistPath), { recursive: true })
243
+ fs.mkdirSync(logDir, { recursive: true })
244
+ fs.writeFileSync(plistPath, buildPlist())
245
+ reloadAgent()
246
+ console.info(`✓ installed LaunchAgent ${LABEL}`)
247
+ console.info(` plist: ${plistPath}`)
248
+ console.info(` logs: ${logDir}/relay.log`)
249
+ console.info(` node: ${process.execPath}`)
250
+ ensureServe()
251
+ printUrl()
252
+ console.info(
253
+ '\n Note: a node version change (nvm) invalidates the baked path — re-run `yarn deploy` after upgrading node.'
254
+ )
255
+ console.info(
256
+ ' Note: the AppleScript write path needs Accessibility permission granted to this node binary (System Settings ▸ Privacy).'
257
+ )
258
+ }
259
+
260
+ function uninstall(): void {
261
+ launchctl('bootout', `${domain}/${LABEL}`)
262
+ try {
263
+ fs.rmSync(plistPath)
264
+ } catch {
265
+ // already gone
266
+ }
267
+ console.info(`✓ removed LaunchAgent ${LABEL}`)
268
+ }
269
+
270
+ function restart(): void {
271
+ launchctl('kickstart', '-k', `${domain}/${LABEL}`)
272
+ console.info(`✓ restarted ${LABEL}`)
273
+ printUrl()
274
+ }
275
+
276
+ function status(): void {
277
+ const installed = fs.existsSync(plistPath)
278
+ console.info(`plist: ${installed ? plistPath : '(not installed)'}`)
279
+ if (!installed) return
280
+ try {
281
+ const out = execFileSync('launchctl', ['print', `${domain}/${LABEL}`], { encoding: 'utf8', stdio: 'pipe' })
282
+ const state = out.match(/state = (\S+)/)?.[1] ?? 'unknown'
283
+ const pid = out.match(/pid = (\d+)/)?.[1] ?? '—'
284
+ console.info(`state: ${state} (pid ${pid})`)
285
+ } catch {
286
+ console.info('state: loaded but not running (check logs)')
287
+ }
288
+ printUrl()
289
+ }
290
+
291
+ const cmd = process.argv[2] ?? 'status'
292
+ switch (cmd) {
293
+ case 'install':
294
+ install()
295
+ break
296
+ case 'uninstall':
297
+ uninstall()
298
+ break
299
+ case 'restart':
300
+ restart()
301
+ break
302
+ case 'status':
303
+ status()
304
+ break
305
+ default:
306
+ console.error(`unknown command: ${cmd}\nusage: service.ts <install|uninstall|restart|status>`)
307
+ process.exit(1)
308
+ }