@hellcoder/companion 0.124.0 → 0.125.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.ts +9 -1
- package/dist/assets/{AgentsPage-Byq8wvdn.js → AgentsPage-0Fi6tbQN.js} +1 -1
- package/dist/assets/{CronManager-Bq2lOMbZ.js → CronManager-DNN709nr.js} +1 -1
- package/dist/assets/{DashboardPage-V77HIt8S.js → DashboardPage-6cNRrqGQ.js} +1 -1
- package/dist/assets/{IntegrationsPage-C3aW9cDI.js → IntegrationsPage-DrE-8KPE.js} +1 -1
- package/dist/assets/{LinearOAuthSettingsPage-_eAbciJN.js → LinearOAuthSettingsPage-Czi_NsX1.js} +1 -1
- package/dist/assets/{LinearSettingsPage-DDHFHt7j.js → LinearSettingsPage-C1RHlPwX.js} +1 -1
- package/dist/assets/{MagicUIDashboard-Cl18AKjV.js → MagicUIDashboard-BOZc4rKn.js} +1 -1
- package/dist/assets/{MagicUIView-C0aiMTet.js → MagicUIView-BVM8CqGg.js} +1 -1
- package/dist/assets/{Playground-CvoHRMBd.js → Playground-DWAal5bL.js} +1 -1
- package/dist/assets/{PromptsPage-CzTs1_Se.js → PromptsPage-D2YI_-95.js} +1 -1
- package/dist/assets/{RunsPage-B7xru-Xy.js → RunsPage-dn5rSkPu.js} +1 -1
- package/dist/assets/{SandboxManager-C3tFkiWz.js → SandboxManager-vWbQKeLT.js} +1 -1
- package/dist/assets/{SettingsPage-BZ2fBrHD.js → SettingsPage-CrV1Tb8F.js} +1 -1
- package/dist/assets/{TailscalePage-DFRiFKcb.js → TailscalePage-ba0g8QT1.js} +1 -1
- package/dist/assets/{index-C7nK12tB.js → index-e4RnFbra.js} +4 -4
- package/dist/assets/{sw-register-Dc72vh_Y.js → sw-register-BBun0drK.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/sw.js +1 -1
- package/package.json +1 -1
- package/server/index.ts +2 -2
- package/server/routes/system-routes.test.ts +57 -0
- package/server/routes/system-routes.ts +20 -24
- package/server/service.test.ts +232 -0
- package/server/service.ts +286 -23
- package/server/settings-manager.ts +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import{_ as u}from"./index-
|
|
1
|
+
import{_ as u}from"./index-e4RnFbra.js";function w(r={}){const{immediate:i=!1,onNeedReload:d,onNeedRefresh:_,onOfflineReady:s,onRegistered:n,onRegisteredSW:o,onRegisterError:t}=r;let a,c;const l=async(e=!0)=>{await c};async function f(){if("serviceWorker"in navigator){if(a=await u(async()=>{const{Workbox:e}=await import("./workbox-window.prod.es5-BBnX5xw4.js");return{Workbox:e}},[]).then(({Workbox:e})=>new e("/sw.js",{scope:"/",type:"classic"})).catch(e=>{t==null||t(e)}),!a)return;a.addEventListener("activated",e=>{(e.isUpdate||e.isExternal)&&(d?d():window.location.reload())}),a.addEventListener("installed",e=>{e.isUpdate||s==null||s()}),a.register({immediate:i}).then(e=>{o?o("/sw.js",e):n==null||n(e)}).catch(e=>{t==null||t(e)})}}return c=f(),l}w({onRegisteredSW(r,i){i&&setInterval(()=>{i.update()},3600*1e3)},onOfflineReady(){console.log("[SW] Offline-ready: all assets precached")}});
|
package/dist/index.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="Companion ME" />
|
|
12
12
|
<meta name="theme-color" content="#d97757" />
|
|
13
13
|
<title>The Companion — Moritz Edition</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-e4RnFbra.js"></script>
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/index-OAVDFR0O.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body class="bg-cc-bg text-cc-fg">
|
package/dist/sw.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
try{self["workbox:core:7.4.0"]&&_()}catch{}const x=(a,...e)=>{let t=a;return e.length>0&&(t+=` :: ${JSON.stringify(e)}`),t},N=x;class l extends Error{constructor(e,t){const s=N(e,t);super(s),this.name=e,this.details=t}}const E=new Set,f={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:typeof registration<"u"?registration.scope:""},b=a=>[f.prefix,a,f.suffix].filter(e=>e&&e.length>0).join("-"),O=a=>{for(const e of Object.keys(f))a(e)},C={updateDetails:a=>{O(e=>{typeof a[e]=="string"&&(f[e]=a[e])})},getGoogleAnalyticsName:a=>a||b(f.googleAnalytics),getPrecacheName:a=>a||b(f.precache),getPrefix:()=>f.prefix,getRuntimeName:a=>a||b(f.runtime),getSuffix:()=>f.suffix};function P(a,e){const t=new URL(a);for(const s of e)t.searchParams.delete(s);return t.href}async function I(a,e,t,s){const n=P(e.url,t);if(e.url===n)return a.match(e,s);const r=Object.assign(Object.assign({},s),{ignoreSearch:!0}),c=await a.keys(e,r);for(const i of c){const o=P(i.url,t);if(n===o)return a.match(i,s)}}let y;function M(){if(y===void 0){const a=new Response("");if("body"in a)try{new Response(a.body),y=!0}catch{y=!1}y=!1}return y}class D{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}}async function S(){for(const a of E)await a()}const W=a=>new URL(String(a),location.href).href.replace(new RegExp(`^${location.origin}`),"");function A(a){return new Promise(e=>setTimeout(e,a))}function K(a,e){const t=e();return a.waitUntil(t),t}async function q(a,e){let t=null;if(a.url&&(t=new URL(a.url).origin),t!==self.location.origin)throw new l("cross-origin-copy-response",{origin:t});const s=a.clone(),r={headers:new Headers(s.headers),status:s.status,statusText:s.statusText},c=M()?s.body:await s.blob();return new Response(c,r)}function j(){self.addEventListener("activate",()=>self.clients.claim())}try{self["workbox:precaching:7.4.0"]&&_()}catch{}const H="__WB_REVISION__";function F(a){if(!a)throw new l("add-to-cache-list-unexpected-type",{entry:a});if(typeof a=="string"){const r=new URL(a,location.href);return{cacheKey:r.href,url:r.href}}const{revision:e,url:t}=a;if(!t)throw new l("add-to-cache-list-unexpected-type",{entry:a});if(!e){const r=new URL(t,location.href);return{cacheKey:r.href,url:r.href}}const s=new URL(t,location.href),n=new URL(t,location.href);return s.searchParams.set(H,e),{cacheKey:s.href,url:n.href}}class B{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:s})=>{if(e.type==="install"&&t&&t.originalRequest&&t.originalRequest instanceof Request){const n=t.originalRequest.url;s?this.notUpdatedURLs.push(n):this.updatedURLs.push(n)}return s}}}class ${constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:t,params:s})=>{const n=(s==null?void 0:s.cacheKey)||this._precacheController.getCacheKeyForURL(t.url);return n?new Request(n,{headers:t.headers}):t},this._precacheController=e}}try{self["workbox:strategies:7.4.0"]&&_()}catch{}function m(a){return typeof a=="string"?new Request(a):a}class G{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new D,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(const s of this._plugins)this._pluginStateMap.set(s,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){const{event:t}=this;let s=m(e);if(s.mode==="navigate"&&t instanceof FetchEvent&&t.preloadResponse){const c=await t.preloadResponse;if(c)return c}const n=this.hasCallback("fetchDidFail")?s.clone():null;try{for(const c of this.iterateCallbacks("requestWillFetch"))s=await c({request:s.clone(),event:t})}catch(c){if(c instanceof Error)throw new l("plugin-error-request-will-fetch",{thrownErrorMessage:c.message})}const r=s.clone();try{let c;c=await fetch(s,s.mode==="navigate"?void 0:this._strategy.fetchOptions);for(const i of this.iterateCallbacks("fetchDidSucceed"))c=await i({event:t,request:r,response:c});return c}catch(c){throw n&&await this.runCallbacks("fetchDidFail",{error:c,event:t,originalRequest:n.clone(),request:r.clone()}),c}}async fetchAndCachePut(e){const t=await this.fetch(e),s=t.clone();return this.waitUntil(this.cachePut(e,s)),t}async cacheMatch(e){const t=m(e);let s;const{cacheName:n,matchOptions:r}=this._strategy,c=await this.getCacheKey(t,"read"),i=Object.assign(Object.assign({},r),{cacheName:n});s=await caches.match(c,i);for(const o of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await o({cacheName:n,matchOptions:r,cachedResponse:s,request:c,event:this.event})||void 0;return s}async cachePut(e,t){const s=m(e);await A(0);const n=await this.getCacheKey(s,"write");if(!t)throw new l("cache-put-with-no-response",{url:W(n.url)});const r=await this._ensureResponseSafeToCache(t);if(!r)return!1;const{cacheName:c,matchOptions:i}=this._strategy,o=await self.caches.open(c),h=this.hasCallback("cacheDidUpdate"),p=h?await I(o,n.clone(),["__WB_REVISION__"],i):null;try{await o.put(n,h?r.clone():r)}catch(u){if(u instanceof Error)throw u.name==="QuotaExceededError"&&await S(),u}for(const u of this.iterateCallbacks("cacheDidUpdate"))await u({cacheName:c,oldResponse:p,newResponse:r.clone(),request:n,event:this.event});return!0}async getCacheKey(e,t){const s=`${e.url} | ${t}`;if(!this._cacheKeys[s]){let n=e;for(const r of this.iterateCallbacks("cacheKeyWillBeUsed"))n=m(await r({mode:t,request:n,event:this.event,params:this.params}));this._cacheKeys[s]=n}return this._cacheKeys[s]}hasCallback(e){for(const t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(const s of this.iterateCallbacks(e))await s(t)}*iterateCallbacks(e){for(const t of this._strategy.plugins)if(typeof t[e]=="function"){const s=this._pluginStateMap.get(t);yield r=>{const c=Object.assign(Object.assign({},r),{state:s});return t[e](c)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){const e=this._extendLifetimePromises.splice(0),s=(await Promise.allSettled(e)).find(n=>n.status==="rejected");if(s)throw s.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,s=!1;for(const n of this.iterateCallbacks("cacheWillUpdate"))if(t=await n({request:this.request,response:t,event:this.event})||void 0,s=!0,!t)break;return s||t&&t.status!==200&&(t=void 0),t}}class V{constructor(e={}){this.cacheName=C.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){const[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});const t=e.event,s=typeof e.request=="string"?new Request(e.request):e.request,n="params"in e?e.params:void 0,r=new G(this,{event:t,request:s,params:n}),c=this._getResponse(r,s,t),i=this._awaitComplete(c,r,s,t);return[c,i]}async _getResponse(e,t,s){await e.runCallbacks("handlerWillStart",{event:s,request:t});let n;try{if(n=await this._handle(t,e),!n||n.type==="error")throw new l("no-response",{url:t.url})}catch(r){if(r instanceof Error){for(const c of e.iterateCallbacks("handlerDidError"))if(n=await c({error:r,event:s,request:t}),n)break}if(!n)throw r}for(const r of e.iterateCallbacks("handlerWillRespond"))n=await r({event:s,request:t,response:n});return n}async _awaitComplete(e,t,s,n){let r,c;try{r=await e}catch{}try{await t.runCallbacks("handlerDidRespond",{event:n,request:s,response:r}),await t.doneWaiting()}catch(i){i instanceof Error&&(c=i)}if(await t.runCallbacks("handlerDidComplete",{event:n,request:s,response:r,error:c}),t.destroy(),c)throw c}}class d extends V{constructor(e={}){e.cacheName=C.getPrecacheName(e.cacheName),super(e),this._fallbackToNetwork=e.fallbackToNetwork!==!1,this.plugins.push(d.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){const s=await t.cacheMatch(e);return s||(t.event&&t.event.type==="install"?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,t){let s;const n=t.params||{};if(this._fallbackToNetwork){const r=n.integrity,c=e.integrity,i=!c||c===r;s=await t.fetch(new Request(e,{integrity:e.mode!=="no-cors"?c||r:void 0})),r&&i&&e.mode!=="no-cors"&&(this._useDefaultCacheabilityPluginIfNeeded(),await t.cachePut(e,s.clone()))}else throw new l("missing-precache-entry",{cacheName:this.cacheName,url:e.url});return s}async _handleInstall(e,t){this._useDefaultCacheabilityPluginIfNeeded();const s=await t.fetch(e);if(!await t.cachePut(e,s.clone()))throw new l("bad-precaching-response",{url:e.url,status:s.status});return s}_useDefaultCacheabilityPluginIfNeeded(){let e=null,t=0;for(const[s,n]of this.plugins.entries())n!==d.copyRedirectedCacheableResponsesPlugin&&(n===d.defaultPrecacheCacheabilityPlugin&&(e=s),n.cacheWillUpdate&&t++);t===0?this.plugins.push(d.defaultPrecacheCacheabilityPlugin):t>1&&e!==null&&this.plugins.splice(e,1)}}d.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:a}){return!a||a.status>=400?null:a}};d.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:a}){return a.redirected?await q(a):a}};class Q{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:s=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new d({cacheName:C.getPrecacheName(e),plugins:[...t,new $({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this._installAndActiveListenersAdded=!0)}addToCacheList(e){const t=[];for(const s of e){typeof s=="string"?t.push(s):s&&s.revision===void 0&&t.push(s.url);const{cacheKey:n,url:r}=F(s),c=typeof s!="string"&&s.revision?"reload":"default";if(this._urlsToCacheKeys.has(r)&&this._urlsToCacheKeys.get(r)!==n)throw new l("add-to-cache-list-conflicting-entries",{firstEntry:this._urlsToCacheKeys.get(r),secondEntry:n});if(typeof s!="string"&&s.integrity){if(this._cacheKeysToIntegrities.has(n)&&this._cacheKeysToIntegrities.get(n)!==s.integrity)throw new l("add-to-cache-list-conflicting-integrities",{url:r});this._cacheKeysToIntegrities.set(n,s.integrity)}if(this._urlsToCacheKeys.set(r,n),this._urlsToCacheModes.set(r,c),t.length>0){const i=`Workbox is precaching URLs without revision info: ${t.join(", ")}
|
|
2
|
-
This is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(i)}}}install(e){return K(e,async()=>{const t=new B;this.strategy.plugins.push(t);for(const[r,c]of this._urlsToCacheKeys){const i=this._cacheKeysToIntegrities.get(c),o=this._urlsToCacheModes.get(r),h=new Request(r,{integrity:i,cache:o,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:c},request:h,event:e}))}const{updatedURLs:s,notUpdatedURLs:n}=t;return{updatedURLs:s,notUpdatedURLs:n}})}activate(e){return K(e,async()=>{const t=await self.caches.open(this.strategy.cacheName),s=await t.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(const c of s)n.has(c.url)||(await t.delete(c),r.push(c.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){const t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s)return(await self.caches.open(this.strategy.cacheName)).match(s)}createHandlerBoundToURL(e){const t=this.getCacheKeyForURL(e);if(!t)throw new l("non-precached-url",{url:e});return s=>(s.request=new Request(e),s.params=Object.assign({cacheKey:t},s.params),this.strategy.handle(s))}}let U;const L=()=>(U||(U=new Q),U);try{self["workbox:routing:7.4.0"]&&_()}catch{}const v="GET",R=a=>a&&typeof a=="object"?a:{handle:a};class g{constructor(e,t,s=v){this.handler=R(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=R(e)}}class z extends g{constructor(e,t,s){const n=({url:r})=>{const c=e.exec(r.href);if(c&&!(r.origin!==location.origin&&c.index!==0))return c.slice(1)};super(n,t,s)}}class J{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",(e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(e=>{if(e.data&&e.data.type==="CACHE_URLS"){const{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(n=>{typeof n=="string"&&(n=[n]);const r=new Request(...n);return this.handleRequest({request:r,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:r,route:c}=this.findMatchingRoute({event:t,request:e,sameOrigin:n,url:s});let i=c&&c.handler;const o=e.method;if(!i&&this._defaultHandlerMap.has(o)&&(i=this._defaultHandlerMap.get(o)),!i)return;let h;try{h=i.handle({url:s,request:e,event:t,params:r})}catch(u){h=Promise.reject(u)}const p=c&&c.catchHandler;return h instanceof Promise&&(this._catchHandler||p)&&(h=h.catch(async u=>{if(p)try{return await p.handle({url:s,request:e,event:t,params:r})}catch(k){k instanceof Error&&(u=k)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw u})),h}findMatchingRoute({url:e,sameOrigin:t,request:s,event:n}){const r=this._routes.get(s.method)||[];for(const c of r){let i;const o=c.match({url:e,sameOrigin:t,request:s,event:n});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o=="boolean")&&(i=void 0),{route:c,params:i}}return{}}setDefaultHandler(e,t=v){this._defaultHandlerMap.set(t,R(e))}setCatchHandler(e){this._catchHandler=R(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new l("unregister-route-but-not-found-with-method",{method:e.method});const t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new l("unregister-route-route-not-registered")}}let w;const X=()=>(w||(w=new J,w.addFetchListener(),w.addCacheListener()),w);function T(a,e,t){let s;if(typeof a=="string"){const r=new URL(a,location.href),c=({url:i})=>i.href===r.href;s=new g(c,e,t)}else if(a instanceof RegExp)s=new z(a,e,t);else if(typeof a=="function")s=new g(a,e,t);else if(a instanceof g)s=a;else throw new l("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return X().registerRoute(s),s}function Y(a,e=[]){for(const t of[...a.searchParams.keys()])e.some(s=>s.test(t))&&a.searchParams.delete(t);return a}function*Z(a,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:t="index.html",cleanURLs:s=!0,urlManipulation:n}={}){const r=new URL(a,location.href);r.hash="",yield r.href;const c=Y(r,e);if(yield c.href,t&&c.pathname.endsWith("/")){const i=new URL(c.href);i.pathname+=t,yield i.href}if(s){const i=new URL(c.href);i.pathname+=".html",yield i.href}if(n){const i=n({url:r});for(const o of i)yield o.href}}class ee extends g{constructor(e,t){const s=({request:n})=>{const r=e.getURLsToCacheKeys();for(const c of Z(n.url,t)){const i=r.get(c);if(i){const o=e.getIntegrityForCacheKey(i);return{cacheKey:i,integrity:o}}}};super(s,e.strategy)}}function te(a){const e=L(),t=new ee(e,a);T(t)}const se="-precache-",ae=async(a,e=se)=>{const s=(await self.caches.keys()).filter(n=>n.includes(e)&&n.includes(self.registration.scope)&&n!==a);return await Promise.all(s.map(n=>self.caches.delete(n))),s};function ne(){self.addEventListener("activate",(a=>{const e=C.getPrecacheName();a.waitUntil(ae(e).then(t=>{}))}))}function re(a){return L().createHandlerBoundToURL(a)}function ce(a){L().precache(a)}function ie(a,e){ce(a),te(e)}class oe extends g{constructor(e,{allowlist:t=[/./],denylist:s=[]}={}){super(n=>this._match(n),e),this._allowlist=t,this._denylist=s}_match({url:e,request:t}){if(t&&t.mode!=="navigate")return!1;const s=e.pathname+e.search;for(const n of this._denylist)if(n.test(s))return!1;return!!this._allowlist.some(n=>n.test(s))}}self.skipWaiting();j();ie([{"revision":"fab346dacb0b9b9af12554e9a59f004d","url":"logo.svg"},{"revision":"623840baec64a3438663496bb1050d30","url":"logo-docker.svg"},{"revision":"c5c8611b432190ec28522f1a2dfcbb7f","url":"logo-codex.svg"},{"revision":"
|
|
2
|
+
This is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(i)}}}install(e){return K(e,async()=>{const t=new B;this.strategy.plugins.push(t);for(const[r,c]of this._urlsToCacheKeys){const i=this._cacheKeysToIntegrities.get(c),o=this._urlsToCacheModes.get(r),h=new Request(r,{integrity:i,cache:o,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:c},request:h,event:e}))}const{updatedURLs:s,notUpdatedURLs:n}=t;return{updatedURLs:s,notUpdatedURLs:n}})}activate(e){return K(e,async()=>{const t=await self.caches.open(this.strategy.cacheName),s=await t.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(const c of s)n.has(c.url)||(await t.delete(c),r.push(c.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){const t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){const t=e instanceof Request?e.url:e,s=this.getCacheKeyForURL(t);if(s)return(await self.caches.open(this.strategy.cacheName)).match(s)}createHandlerBoundToURL(e){const t=this.getCacheKeyForURL(e);if(!t)throw new l("non-precached-url",{url:e});return s=>(s.request=new Request(e),s.params=Object.assign({cacheKey:t},s.params),this.strategy.handle(s))}}let U;const L=()=>(U||(U=new Q),U);try{self["workbox:routing:7.4.0"]&&_()}catch{}const v="GET",R=a=>a&&typeof a=="object"?a:{handle:a};class g{constructor(e,t,s=v){this.handler=R(t),this.match=e,this.method=s}setCatchHandler(e){this.catchHandler=R(e)}}class z extends g{constructor(e,t,s){const n=({url:r})=>{const c=e.exec(r.href);if(c&&!(r.origin!==location.origin&&c.index!==0))return c.slice(1)};super(n,t,s)}}class J{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener("fetch",(e=>{const{request:t}=e,s=this.handleRequest({request:t,event:e});s&&e.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(e=>{if(e.data&&e.data.type==="CACHE_URLS"){const{payload:t}=e.data,s=Promise.all(t.urlsToCache.map(n=>{typeof n=="string"&&(n=[n]);const r=new Request(...n);return this.handleRequest({request:r,event:e})}));e.waitUntil(s),e.ports&&e.ports[0]&&s.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){const s=new URL(e.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:r,route:c}=this.findMatchingRoute({event:t,request:e,sameOrigin:n,url:s});let i=c&&c.handler;const o=e.method;if(!i&&this._defaultHandlerMap.has(o)&&(i=this._defaultHandlerMap.get(o)),!i)return;let h;try{h=i.handle({url:s,request:e,event:t,params:r})}catch(u){h=Promise.reject(u)}const p=c&&c.catchHandler;return h instanceof Promise&&(this._catchHandler||p)&&(h=h.catch(async u=>{if(p)try{return await p.handle({url:s,request:e,event:t,params:r})}catch(k){k instanceof Error&&(u=k)}if(this._catchHandler)return this._catchHandler.handle({url:s,request:e,event:t});throw u})),h}findMatchingRoute({url:e,sameOrigin:t,request:s,event:n}){const r=this._routes.get(s.method)||[];for(const c of r){let i;const o=c.match({url:e,sameOrigin:t,request:s,event:n});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o=="boolean")&&(i=void 0),{route:c,params:i}}return{}}setDefaultHandler(e,t=v){this._defaultHandlerMap.set(t,R(e))}setCatchHandler(e){this._catchHandler=R(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new l("unregister-route-but-not-found-with-method",{method:e.method});const t=this._routes.get(e.method).indexOf(e);if(t>-1)this._routes.get(e.method).splice(t,1);else throw new l("unregister-route-route-not-registered")}}let w;const X=()=>(w||(w=new J,w.addFetchListener(),w.addCacheListener()),w);function T(a,e,t){let s;if(typeof a=="string"){const r=new URL(a,location.href),c=({url:i})=>i.href===r.href;s=new g(c,e,t)}else if(a instanceof RegExp)s=new z(a,e,t);else if(typeof a=="function")s=new g(a,e,t);else if(a instanceof g)s=a;else throw new l("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});return X().registerRoute(s),s}function Y(a,e=[]){for(const t of[...a.searchParams.keys()])e.some(s=>s.test(t))&&a.searchParams.delete(t);return a}function*Z(a,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:t="index.html",cleanURLs:s=!0,urlManipulation:n}={}){const r=new URL(a,location.href);r.hash="",yield r.href;const c=Y(r,e);if(yield c.href,t&&c.pathname.endsWith("/")){const i=new URL(c.href);i.pathname+=t,yield i.href}if(s){const i=new URL(c.href);i.pathname+=".html",yield i.href}if(n){const i=n({url:r});for(const o of i)yield o.href}}class ee extends g{constructor(e,t){const s=({request:n})=>{const r=e.getURLsToCacheKeys();for(const c of Z(n.url,t)){const i=r.get(c);if(i){const o=e.getIntegrityForCacheKey(i);return{cacheKey:i,integrity:o}}}};super(s,e.strategy)}}function te(a){const e=L(),t=new ee(e,a);T(t)}const se="-precache-",ae=async(a,e=se)=>{const s=(await self.caches.keys()).filter(n=>n.includes(e)&&n.includes(self.registration.scope)&&n!==a);return await Promise.all(s.map(n=>self.caches.delete(n))),s};function ne(){self.addEventListener("activate",(a=>{const e=C.getPrecacheName();a.waitUntil(ae(e).then(t=>{}))}))}function re(a){return L().createHandlerBoundToURL(a)}function ce(a){L().precache(a)}function ie(a,e){ce(a),te(e)}class oe extends g{constructor(e,{allowlist:t=[/./],denylist:s=[]}={}){super(n=>this._match(n),e),this._allowlist=t,this._denylist=s}_match({url:e,request:t}){if(t&&t.mode!=="navigate")return!1;const s=e.pathname+e.search;for(const n of this._denylist)if(n.test(s))return!1;return!!this._allowlist.some(n=>n.test(s))}}self.skipWaiting();j();ie([{"revision":"fab346dacb0b9b9af12554e9a59f004d","url":"logo.svg"},{"revision":"623840baec64a3438663496bb1050d30","url":"logo-docker.svg"},{"revision":"c5c8611b432190ec28522f1a2dfcbb7f","url":"logo-codex.svg"},{"revision":"38904c45f039fa501fcc7a9ad2ba6ac0","url":"index.html"},{"revision":"1421e9c6cad8218ef5689b85034d3a95","url":"icon-512.png"},{"revision":"b96da5a0b6636012517f54b0e69c304d","url":"icon-192.png"},{"revision":"c64ca8ae8596d4ffe13d0f591beb2274","url":"favicon.svg"},{"revision":"6481face04bd08e6f0bfc2a633840b57","url":"apple-touch-icon.png"},{"revision":"83304194a3c8be2b5b61242eeb1c1046","url":"fonts/MesloLGSNerdFontMono-Regular.woff2"},{"revision":"60fafe18cbcb717c51cdabf87f9490f0","url":"fonts/MesloLGSNerdFontMono-Bold.woff2"},{"revision":null,"url":"assets/workbox-window.prod.es5-BBnX5xw4.js"},{"revision":null,"url":"assets/sw-register-BBun0drK.js"},{"revision":null,"url":"assets/index-e4RnFbra.js"},{"revision":null,"url":"assets/index-OAVDFR0O.css"},{"revision":null,"url":"assets/TailscalePage-ba0g8QT1.js"},{"revision":null,"url":"assets/SettingsPage-CrV1Tb8F.js"},{"revision":null,"url":"assets/SandboxManager-vWbQKeLT.js"},{"revision":null,"url":"assets/RunsPage-dn5rSkPu.js"},{"revision":null,"url":"assets/PromptsPage-D2YI_-95.js"},{"revision":null,"url":"assets/Playground-DWAal5bL.js"},{"revision":null,"url":"assets/MagicUIView-BVM8CqGg.js"},{"revision":null,"url":"assets/MagicUIDashboard-BOZc4rKn.js"},{"revision":null,"url":"assets/LinearSettingsPage-C1RHlPwX.js"},{"revision":null,"url":"assets/LinearOAuthSettingsPage-Czi_NsX1.js"},{"revision":null,"url":"assets/IntegrationsPage-DrE-8KPE.js"},{"revision":null,"url":"assets/DashboardPage-6cNRrqGQ.js"},{"revision":null,"url":"assets/CronManager-DNN709nr.js"},{"revision":null,"url":"assets/AgentsPage-0Fi6tbQN.js"}]);ne();T(new oe(re("index.html"),{denylist:[/^\/api/,/^\/ws/]}));
|
package/package.json
CHANGED
package/server/index.ts
CHANGED
|
@@ -45,7 +45,7 @@ import { getSettings, updateSettings } from "./settings-manager.js";
|
|
|
45
45
|
import { setCliIngressServer } from "./routes/system-routes.js";
|
|
46
46
|
import { imagePullManager } from "./image-pull-manager.js";
|
|
47
47
|
import { restoreIfNeeded as restoreTailscaleFunnel, cleanup as cleanupTailscaleFunnel } from "./tailscale-manager.js";
|
|
48
|
-
import { isRunningAsService } from "./service.js";
|
|
48
|
+
import { getDetectedServiceKind, isRunningAsService } from "./service.js";
|
|
49
49
|
import { getToken, verifyToken } from "./auth-manager.js";
|
|
50
50
|
import { getCookie } from "hono/cookie";
|
|
51
51
|
import type { SocketData } from "./ws-bridge.js";
|
|
@@ -421,7 +421,7 @@ if (getSettings().claudeBridgeMode === "patched") {
|
|
|
421
421
|
}
|
|
422
422
|
if (isRunningAsService()) {
|
|
423
423
|
setServiceMode(true);
|
|
424
|
-
console.log(
|
|
424
|
+
console.log(`[server] Running as background service (${getDetectedServiceKind()}, auto-update available)`);
|
|
425
425
|
}
|
|
426
426
|
|
|
427
427
|
// ── Runtime diagnostics ──────────────────────────────────────────────────────
|
|
@@ -28,6 +28,15 @@ vi.mock("../update-checker.js", () => ({
|
|
|
28
28
|
// ─── Mock service ──────────────────────────────────────────────────────────
|
|
29
29
|
vi.mock("../service.js", () => ({
|
|
30
30
|
refreshServiceDefinition: vi.fn(),
|
|
31
|
+
getDetectedServiceKind: vi.fn(() => "systemd-user"),
|
|
32
|
+
getUpdateRestartPlan: vi.fn((kind: string | null) =>
|
|
33
|
+
kind === "systemd-system" || kind === "external"
|
|
34
|
+
? { exitCode: 75 }
|
|
35
|
+
: {
|
|
36
|
+
command: ["systemctl", "--user", "restart", "the-companion.service"],
|
|
37
|
+
env: { XDG_RUNTIME_DIR: "/run/user/1000" },
|
|
38
|
+
exitCode: 0,
|
|
39
|
+
}),
|
|
31
40
|
}));
|
|
32
41
|
|
|
33
42
|
// ─── Mock claude-compat checker ────────────────────────────────────────────
|
|
@@ -88,6 +97,7 @@ import {
|
|
|
88
97
|
setUpdateInProgress,
|
|
89
98
|
} from "../update-checker.js";
|
|
90
99
|
import { registerSystemRoutes } from "./system-routes.js";
|
|
100
|
+
import { getDetectedServiceKind } from "../service.js";
|
|
91
101
|
import { checkCompat, getCompatState } from "../claude-compat-checker.js";
|
|
92
102
|
import { pinToVersion, patchBinary, unpatch } from "../claude-patcher.js";
|
|
93
103
|
import { getSettings, updateSettings } from "../settings-manager.js";
|
|
@@ -496,6 +506,53 @@ describe("POST /api/update", () => {
|
|
|
496
506
|
// Advance past the 500ms exit timeout
|
|
497
507
|
await vi.advanceTimersByTimeAsync(600);
|
|
498
508
|
|
|
509
|
+
// The user unit is restarted through systemctl --user, then we exit 0.
|
|
510
|
+
expect(mockSpawn).toHaveBeenCalledWith(
|
|
511
|
+
["systemctl", "--user", "restart", "the-companion.service"],
|
|
512
|
+
expect.objectContaining({ env: expect.objectContaining({ XDG_RUNTIME_DIR: "/run/user/1000" }) }),
|
|
513
|
+
);
|
|
514
|
+
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
515
|
+
|
|
516
|
+
vi.useRealTimers();
|
|
517
|
+
exitSpy.mockRestore();
|
|
518
|
+
// @ts-expect-error -- cleanup Bun global mock
|
|
519
|
+
delete globalThis.Bun;
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// Under a system-wide unit (issue #139) the server can't restart itself via
|
|
523
|
+
// systemctl; it must exit non-zero so Restart=on-failure respawns it.
|
|
524
|
+
it("exits with the restart code and spawns nothing under a system unit", async () => {
|
|
525
|
+
vi.useFakeTimers();
|
|
526
|
+
vi.mocked(getDetectedServiceKind).mockReturnValueOnce("systemd-system");
|
|
527
|
+
vi.mocked(getUpdateState).mockReturnValue({
|
|
528
|
+
currentVersion: "1.0.0",
|
|
529
|
+
latestVersion: "2.0.0",
|
|
530
|
+
lastChecked: Date.now(),
|
|
531
|
+
isServiceMode: true,
|
|
532
|
+
checking: false,
|
|
533
|
+
updateInProgress: false,
|
|
534
|
+
channel: "stable",
|
|
535
|
+
});
|
|
536
|
+
vi.mocked(isUpdateAvailable).mockReturnValue(true);
|
|
537
|
+
|
|
538
|
+
const mockSpawn = vi.fn().mockReturnValueOnce({
|
|
539
|
+
exited: Promise.resolve(0),
|
|
540
|
+
stdout: new ReadableStream(),
|
|
541
|
+
stderr: new ReadableStream(),
|
|
542
|
+
});
|
|
543
|
+
// @ts-expect-error -- Bun global mock
|
|
544
|
+
globalThis.Bun = { spawn: mockSpawn };
|
|
545
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
|
|
546
|
+
|
|
547
|
+
const res = await app.request("/api/update", { method: "POST" });
|
|
548
|
+
expect(res.status).toBe(200);
|
|
549
|
+
await vi.advanceTimersByTimeAsync(150);
|
|
550
|
+
await vi.advanceTimersByTimeAsync(600);
|
|
551
|
+
|
|
552
|
+
// Only the install was spawned — no systemctl --user restart.
|
|
553
|
+
expect(mockSpawn).toHaveBeenCalledTimes(1);
|
|
554
|
+
expect(exitSpy).toHaveBeenCalledWith(75);
|
|
555
|
+
|
|
499
556
|
vi.useRealTimers();
|
|
500
557
|
exitSpy.mockRestore();
|
|
501
558
|
// @ts-expect-error -- cleanup Bun global mock
|
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
isUpdateAvailable,
|
|
12
12
|
setUpdateInProgress,
|
|
13
13
|
} from "../update-checker.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getDetectedServiceKind,
|
|
16
|
+
getUpdateRestartPlan,
|
|
17
|
+
refreshServiceDefinition,
|
|
18
|
+
} from "../service.js";
|
|
15
19
|
import { getSettings, updateSettings } from "../settings-manager.js";
|
|
16
20
|
import { imagePullManager } from "../image-pull-manager.js";
|
|
17
21
|
import { isSandboxEnabled } from "../feature-flags.js";
|
|
@@ -187,29 +191,21 @@ export function registerSystemRoutes(
|
|
|
187
191
|
|
|
188
192
|
console.log("[update] Update successful, restarting service...");
|
|
189
193
|
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
XDG_RUNTIME_DIR:
|
|
206
|
-
process.env.XDG_RUNTIME_DIR ||
|
|
207
|
-
`/run/user/${uid ?? 1000}`,
|
|
208
|
-
}
|
|
209
|
-
: undefined,
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
setTimeout(() => process.exit(0), 500);
|
|
194
|
+
const plan = getUpdateRestartPlan(getDetectedServiceKind());
|
|
195
|
+
if (plan.command) {
|
|
196
|
+
Bun.spawn(plan.command, {
|
|
197
|
+
stdout: "ignore",
|
|
198
|
+
stderr: "ignore",
|
|
199
|
+
stdin: "ignore",
|
|
200
|
+
env: plan.env ? { ...process.env, ...plan.env } : undefined,
|
|
201
|
+
});
|
|
202
|
+
} else {
|
|
203
|
+
console.log(
|
|
204
|
+
`[update] Exiting with code ${plan.exitCode} so the supervisor restarts the server.`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
setTimeout(() => process.exit(plan.exitCode), 500);
|
|
213
209
|
} catch (err) {
|
|
214
210
|
console.error("[update] Update failed:", err);
|
|
215
211
|
setUpdateInProgress(false);
|
package/server/service.test.ts
CHANGED
|
@@ -50,6 +50,38 @@ vi.mock("node:child_process", async (importOriginal) => {
|
|
|
50
50
|
};
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
// Mock node:net so isPortInUse() never binds a real port (the dev box may
|
|
54
|
+
// well be running a Companion on the default port).
|
|
55
|
+
const mockPortInUse = vi.hoisted(() => {
|
|
56
|
+
let inUse = false;
|
|
57
|
+
return { get: () => inUse, set: (v: boolean) => { inUse = v; } };
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
vi.mock("node:net", async (importOriginal) => {
|
|
61
|
+
const actual = await importOriginal<typeof import("node:net")>();
|
|
62
|
+
const { EventEmitter } = await import("node:events");
|
|
63
|
+
return {
|
|
64
|
+
...actual,
|
|
65
|
+
createServer: () => {
|
|
66
|
+
const server = new EventEmitter() as InstanceType<typeof EventEmitter> & {
|
|
67
|
+
listen: (port: number) => void;
|
|
68
|
+
close: (cb?: () => void) => void;
|
|
69
|
+
};
|
|
70
|
+
server.listen = () => {
|
|
71
|
+
queueMicrotask(() => {
|
|
72
|
+
if (mockPortInUse.get()) {
|
|
73
|
+
server.emit("error", Object.assign(new Error("in use"), { code: "EADDRINUSE" }));
|
|
74
|
+
} else {
|
|
75
|
+
server.emit("listening");
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
server.close = (cb) => cb?.();
|
|
80
|
+
return server;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
|
|
53
85
|
// Mock path-resolver to return a deterministic enriched PATH
|
|
54
86
|
const MOCK_SERVICE_PATH = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/mock/.bun/bin:/mock/.local/bin";
|
|
55
87
|
vi.mock("./path-resolver.js", () => ({
|
|
@@ -74,6 +106,9 @@ beforeEach(async () => {
|
|
|
74
106
|
mockHomedir.set(tempDir);
|
|
75
107
|
mockPlatform.set("darwin");
|
|
76
108
|
mockExecSync.mockReset();
|
|
109
|
+
mockPortInUse.set(false);
|
|
110
|
+
delete process.env.COMPANION_SERVICE_MODE;
|
|
111
|
+
delete process.env.INVOCATION_ID;
|
|
77
112
|
|
|
78
113
|
// Set process.platform AFTER resetting mockPlatform to "darwin"
|
|
79
114
|
Object.defineProperty(process, "platform", {
|
|
@@ -1417,3 +1452,200 @@ describe("platform check", () => {
|
|
|
1417
1452
|
expect(existsSync(unitPath())).toBe(true);
|
|
1418
1453
|
});
|
|
1419
1454
|
});
|
|
1455
|
+
|
|
1456
|
+
// ===========================================================================
|
|
1457
|
+
// System-wide systemd unit support (issue #139)
|
|
1458
|
+
// ===========================================================================
|
|
1459
|
+
|
|
1460
|
+
const SYSTEM_UNIT_SHOW = [
|
|
1461
|
+
"LoadState=loaded",
|
|
1462
|
+
"ActiveState=active",
|
|
1463
|
+
"MainPID=4321",
|
|
1464
|
+
"InvocationID=0123456789abcdef0123456789abcdef",
|
|
1465
|
+
"FragmentPath=/etc/systemd/system/the-companion.service",
|
|
1466
|
+
"Environment=NODE_ENV=production PORT=3999 HOST=0.0.0.0",
|
|
1467
|
+
].join("\n");
|
|
1468
|
+
|
|
1469
|
+
/** True for system-scope `systemctl show the-companion.service` queries. */
|
|
1470
|
+
function isSystemShow(cmd: string): boolean {
|
|
1471
|
+
return cmd.startsWith("systemctl show the-companion.service");
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
describe("system-wide systemd unit (linux)", () => {
|
|
1475
|
+
beforeEach(async () => {
|
|
1476
|
+
mockPlatform.set("linux");
|
|
1477
|
+
Object.defineProperty(process, "platform", { value: "linux" });
|
|
1478
|
+
vi.resetModules();
|
|
1479
|
+
service = await import("./service.js");
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
describe("getSystemUnitInfo", () => {
|
|
1483
|
+
it("parses state, pid, invocation id, path and port", () => {
|
|
1484
|
+
mockExecSync.mockImplementation((cmd: string) => (isSystemShow(cmd) ? SYSTEM_UNIT_SHOW : ""));
|
|
1485
|
+
expect(service.getSystemUnitInfo()).toEqual({
|
|
1486
|
+
activeState: "active",
|
|
1487
|
+
mainPid: 4321,
|
|
1488
|
+
invocationId: "0123456789abcdef0123456789abcdef",
|
|
1489
|
+
fragmentPath: "/etc/systemd/system/the-companion.service",
|
|
1490
|
+
port: 3999,
|
|
1491
|
+
});
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
it("returns undefined for LoadState=not-found", () => {
|
|
1495
|
+
mockExecSync.mockImplementation(() => "LoadState=not-found\nActiveState=inactive\n");
|
|
1496
|
+
expect(service.getSystemUnitInfo()).toBeUndefined();
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
it("returns undefined when systemctl fails", () => {
|
|
1500
|
+
mockExecSync.mockImplementation(() => { throw new Error("no systemd"); });
|
|
1501
|
+
expect(service.getSystemUnitInfo()).toBeUndefined();
|
|
1502
|
+
});
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
describe("detectServiceKind / isRunningAsService", () => {
|
|
1506
|
+
it("detects the system unit when INVOCATION_ID matches", () => {
|
|
1507
|
+
process.env.INVOCATION_ID = "0123456789abcdef0123456789abcdef";
|
|
1508
|
+
mockExecSync.mockImplementation((cmd: string) => (isSystemShow(cmd) ? SYSTEM_UNIT_SHOW : ""));
|
|
1509
|
+
expect(service.isRunningAsService()).toBe(true);
|
|
1510
|
+
expect(service.getDetectedServiceKind()).toBe("systemd-system");
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
it("ignores the system unit when this process is not its invocation", () => {
|
|
1514
|
+
process.env.INVOCATION_ID = "ffffffffffffffffffffffffffffffff";
|
|
1515
|
+
mockExecSync.mockImplementation((cmd: string) => (isSystemShow(cmd) ? SYSTEM_UNIT_SHOW : ""));
|
|
1516
|
+
expect(service.detectServiceKind()).toBeNull();
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
it("ignores the system unit when not started by systemd (no INVOCATION_ID)", () => {
|
|
1520
|
+
mockExecSync.mockImplementation((cmd: string) => (isSystemShow(cmd) ? SYSTEM_UNIT_SHOW : ""));
|
|
1521
|
+
expect(service.detectServiceKind()).toBeNull();
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
it("ignores an inactive system unit", () => {
|
|
1525
|
+
process.env.INVOCATION_ID = "0123456789abcdef0123456789abcdef";
|
|
1526
|
+
mockExecSync.mockImplementation((cmd: string) =>
|
|
1527
|
+
isSystemShow(cmd) ? SYSTEM_UNIT_SHOW.replace("ActiveState=active", "ActiveState=failed") : "");
|
|
1528
|
+
expect(service.detectServiceKind()).toBeNull();
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
it("COMPANION_SERVICE_MODE=1 opts in without any unit", () => {
|
|
1532
|
+
process.env.COMPANION_SERVICE_MODE = "1";
|
|
1533
|
+
expect(service.isRunningAsService()).toBe(true);
|
|
1534
|
+
expect(service.getDetectedServiceKind()).toBe("external");
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
it("COMPANION_SERVICE_MODE=0 forces service mode off", () => {
|
|
1538
|
+
process.env.COMPANION_SERVICE_MODE = "0";
|
|
1539
|
+
process.env.INVOCATION_ID = "0123456789abcdef0123456789abcdef";
|
|
1540
|
+
mockExecSync.mockImplementation((cmd: string) => (isSystemShow(cmd) ? SYSTEM_UNIT_SHOW : ""));
|
|
1541
|
+
expect(service.isRunningAsService()).toBe(false);
|
|
1542
|
+
expect(service.getDetectedServiceKind()).toBeNull();
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
it("still reports systemd-user for an active per-user unit", async () => {
|
|
1546
|
+
mockExecSync.mockImplementation((cmd: string) =>
|
|
1547
|
+
cmd.startsWith("which") ? "/usr/local/bin/the-companion\n" : "");
|
|
1548
|
+
await service.install();
|
|
1549
|
+
mockExecSync.mockReset();
|
|
1550
|
+
mockExecSync.mockImplementation((cmd: string) => (cmd.includes("is-active") ? "active\n" : ""));
|
|
1551
|
+
expect(service.detectServiceKind()).toBe("systemd-user");
|
|
1552
|
+
});
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
describe("install", () => {
|
|
1556
|
+
it("refuses when a system-wide unit exists", async () => {
|
|
1557
|
+
mockExecSync.mockImplementation((cmd: string) => {
|
|
1558
|
+
if (cmd.startsWith("which")) return "/usr/local/bin/the-companion\n";
|
|
1559
|
+
if (isSystemShow(cmd)) return SYSTEM_UNIT_SHOW;
|
|
1560
|
+
return "";
|
|
1561
|
+
});
|
|
1562
|
+
await expect(service.install()).rejects.toThrow("process.exit(1)");
|
|
1563
|
+
expect(existsSync(unitPath())).toBe(false);
|
|
1564
|
+
expect(mockExecSync).not.toHaveBeenCalledWith(
|
|
1565
|
+
expect.stringContaining("enable --now"), expect.anything());
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
it("refuses when the port is already in use", async () => {
|
|
1569
|
+
mockPortInUse.set(true);
|
|
1570
|
+
mockExecSync.mockImplementation((cmd: string) =>
|
|
1571
|
+
cmd.startsWith("which") ? "/usr/local/bin/the-companion\n" : "");
|
|
1572
|
+
await expect(service.install()).rejects.toThrow("process.exit(1)");
|
|
1573
|
+
expect(existsSync(unitPath())).toBe(false);
|
|
1574
|
+
});
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
describe("start/stop/restart/status/uninstall with only a system unit", () => {
|
|
1578
|
+
beforeEach(() => {
|
|
1579
|
+
mockExecSync.mockImplementation((cmd: string) => (isSystemShow(cmd) ? SYSTEM_UNIT_SHOW : ""));
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
it("start does not install a user unit and prints the sudo hint when unprivileged", async () => {
|
|
1583
|
+
vi.spyOn(process, "getuid").mockReturnValue(1000);
|
|
1584
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
1585
|
+
await expect(service.start()).rejects.toThrow("process.exit(1)");
|
|
1586
|
+
expect(existsSync(unitPath())).toBe(false);
|
|
1587
|
+
expect(log.mock.calls.flat().join("\n")).toContain("sudo systemctl start the-companion.service");
|
|
1588
|
+
});
|
|
1589
|
+
|
|
1590
|
+
it("restart drives the system unit directly as root", async () => {
|
|
1591
|
+
vi.spyOn(process, "getuid").mockReturnValue(0);
|
|
1592
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
1593
|
+
await service.restart();
|
|
1594
|
+
expect(mockExecSync).toHaveBeenCalledWith(
|
|
1595
|
+
"systemctl restart the-companion.service", expect.anything());
|
|
1596
|
+
expect(mockExecSync).not.toHaveBeenCalledWith(
|
|
1597
|
+
expect.stringContaining("--user restart"), expect.anything());
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
it("stop prints the sudo hint when unprivileged", async () => {
|
|
1601
|
+
vi.spyOn(process, "getuid").mockReturnValue(1000);
|
|
1602
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
1603
|
+
await expect(service.stop()).rejects.toThrow("process.exit(1)");
|
|
1604
|
+
expect(log.mock.calls.flat().join("\n")).toContain("sudo systemctl stop the-companion.service");
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
it("status reports the system unit", async () => {
|
|
1608
|
+
expect(await service.status()).toEqual({
|
|
1609
|
+
installed: true, running: true, pid: 4321, port: 3999, scope: "system",
|
|
1610
|
+
});
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
it("uninstall leaves the system unit alone", async () => {
|
|
1614
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
1615
|
+
await service.uninstall();
|
|
1616
|
+
expect(mockExecSync).not.toHaveBeenCalledWith(
|
|
1617
|
+
expect.stringContaining("disable"), expect.anything());
|
|
1618
|
+
expect(log.mock.calls.flat().join("\n")).toContain("sudo systemctl disable --now");
|
|
1619
|
+
});
|
|
1620
|
+
});
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
describe("getUpdateRestartPlan", () => {
|
|
1624
|
+
it("exits non-zero without spawning for a system unit", () => {
|
|
1625
|
+
expect(service.getUpdateRestartPlan("systemd-system")).toEqual({
|
|
1626
|
+
exitCode: service.UPDATE_RESTART_EXIT_CODE,
|
|
1627
|
+
});
|
|
1628
|
+
expect(service.UPDATE_RESTART_EXIT_CODE).not.toBe(0);
|
|
1629
|
+
// 42 is SuccessExitStatus in our own template; a "please restart" code must differ.
|
|
1630
|
+
expect(service.UPDATE_RESTART_EXIT_CODE).not.toBe(42);
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
it("exits non-zero without spawning for COMPANION_SERVICE_MODE=1", () => {
|
|
1634
|
+
expect(service.getUpdateRestartPlan("external").command).toBeUndefined();
|
|
1635
|
+
expect(service.getUpdateRestartPlan("external").exitCode).toBe(service.UPDATE_RESTART_EXIT_CODE);
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
it("uses systemctl --user restart for the user unit", () => {
|
|
1639
|
+
const plan = service.getUpdateRestartPlan("systemd-user");
|
|
1640
|
+
expect(plan.command).toEqual(["systemctl", "--user", "restart", "the-companion.service"]);
|
|
1641
|
+
expect(plan.env?.XDG_RUNTIME_DIR).toBeTruthy();
|
|
1642
|
+
expect(plan.exitCode).toBe(0);
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
it("uses launchctl kickstart for launchd", () => {
|
|
1646
|
+
const plan = service.getUpdateRestartPlan("launchd");
|
|
1647
|
+
expect(plan.command?.slice(0, 3)).toEqual(["launchctl", "kickstart", "-k"]);
|
|
1648
|
+
expect(plan.command?.[3]).toMatch(/sh\.thecompanion\.app$/);
|
|
1649
|
+
expect(plan.exitCode).toBe(0);
|
|
1650
|
+
});
|
|
1651
|
+
});
|