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.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/bin/cli.js +64 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/index-DiiwvzTm.css +1 -0
- package/dist/assets/index-DtY88CkY.js +40 -0
- package/dist/assets/workbox-window.prod.es5-BBnX5xw4.js +2 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/icon-maskable-512.png +0 -0
- package/dist/icon.svg +28 -0
- package/dist/index.html +20 -0
- package/dist/manifest.webmanifest +1 -0
- package/dist/sw.js +1 -0
- package/dist/workbox-9c191d2f.js +1 -0
- package/package.json +86 -0
- package/scripts/dev.ts +75 -0
- package/scripts/devtools-recon.js +42 -0
- package/scripts/gen-icons.ts +22 -0
- package/scripts/service.ts +308 -0
- package/src/config.ts +81 -0
- package/src/db.ts +38 -0
- package/src/git.ts +116 -0
- package/src/reads.ts +164 -0
- package/src/server.ts +149 -0
- package/src/sidecar.ts +194 -0
- package/src/transcript.ts +111 -0
- package/src/writes.ts +133 -0
|
@@ -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>
|
package/dist/index.html
ADDED
|
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
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
|
+
}
|