22// before any import (e.g. `@executor-js/local` → libSQL) eagerly loads them.
33import "./native-bindings" ;
44
5- import { randomUUID } from "node:crypto" ;
5+ import { randomBytes , randomUUID } from "node:crypto" ;
66import { existsSync , realpathSync } from "node:fs" ;
77import { dirname , join , resolve } from "node:path" ;
88// Make sibling binaries (if any are added later) discoverable on $PATH so
@@ -93,11 +93,15 @@ import {
9393import {
9494 acquireLocalServerStartLock ,
9595 readLocalServerManifest ,
96+ readServiceKey ,
9697 releaseLocalServerStartLock ,
9798 removeLocalServerManifestIfOwnedBy ,
99+ removeServiceKey ,
98100 resolveExecutorDataDir ,
99101 writeLocalServerManifest ,
102+ writeServiceKey ,
100103} from "./local-server-manifest" ;
104+ import { DEFAULT_SERVICE_PORT , getServiceBackend , SERVICE_LABEL } from "./service" ;
101105import {
102106 defaultCliServerConnectionProfile ,
103107 findCliServerConnectionProfile ,
@@ -1993,12 +1997,24 @@ const daemonRunCommand = Command.make(
19931997 Effect . gen ( function * ( ) {
19941998 applyScope ( scope ) ;
19951999 if ( foreground ) {
2000+ // Auth resolution for the foreground daemon (the form OS service
2001+ // managers run). Flag wins; then EXECUTOR_AUTH_PASSWORD/TOKEN env (how
2002+ // a dynamically-written launchd/systemd unit injects the secret); then,
2003+ // only when running supervised, the durable service.key (how the
2004+ // desktop's static SMAppService plist — which can't carry a per-user
2005+ // secret — supplies it).
2006+ const flagPassword = Option . getOrUndefined ( authPassword ) ;
2007+ const envPassword = process . env . EXECUTOR_AUTH_PASSWORD ;
2008+ const keyPassword =
2009+ process . env . EXECUTOR_SUPERVISED && ! flagPassword && ! envPassword
2010+ ? ( ( yield * readServiceKey ( ) ) ?. password ?? undefined )
2011+ : undefined ;
19962012 yield * runDaemonSession ( {
19972013 port,
19982014 hostname,
19992015 allowedHosts : allowedHost ,
2000- authToken : Option . getOrUndefined ( authToken ) ,
2001- authPassword : Option . getOrUndefined ( authPassword ) ,
2016+ authToken : Option . getOrUndefined ( authToken ) ?? process . env . EXECUTOR_AUTH_TOKEN ,
2017+ authPassword : flagPassword ?? envPassword ?? keyPassword ,
20022018 } ) ;
20032019 } else {
20042020 yield * runBackgroundDaemonStart ( { port, hostname, allowedHosts : allowedHost } ) ;
@@ -2110,6 +2126,185 @@ const mcpCommand = Command.make(
21102126 } ) ,
21112127) . pipe ( Command . withDescription ( "Start an MCP server over stdio" ) ) ;
21122128
2129+ // ---------------------------------------------------------------------------
2130+ // Service — register the daemon with the OS so it survives app-quit + restart
2131+ // ---------------------------------------------------------------------------
2132+
2133+ const generateServicePassword = ( ) : string => randomBytes ( 24 ) . toString ( "base64url" ) ;
2134+
2135+ const supervisedServiceOrigin = ( port : number ) : string => `http://127.0.0.1:${ port } ` ;
2136+
2137+ const supervisedServiceAuthorization = (
2138+ password : string | null ,
2139+ port : number ,
2140+ ) : string | undefined =>
2141+ getExecutorServerAuthorizationHeader (
2142+ normalizeExecutorServerConnection ( {
2143+ origin : supervisedServiceOrigin ( port ) ,
2144+ ...( password
2145+ ? {
2146+ auth : {
2147+ kind : "basic" as const ,
2148+ username : DEFAULT_EXECUTOR_SERVER_USERNAME ,
2149+ password,
2150+ } ,
2151+ }
2152+ : { } ) ,
2153+ } ) ,
2154+ ) ?? undefined ;
2155+
2156+ const serviceInstallCommand = Command . make (
2157+ "install" ,
2158+ {
2159+ port : Options . integer ( "port" )
2160+ . pipe ( Options . withDefault ( DEFAULT_SERVICE_PORT ) )
2161+ . pipe ( Options . withDescription ( "Port the supervised daemon binds (loopback only)." ) ) ,
2162+ password : Options . string ( "password" )
2163+ . pipe ( Options . optional )
2164+ . pipe (
2165+ Options . withDescription (
2166+ "Basic auth password. Reused from a prior install or generated if omitted." ,
2167+ ) ,
2168+ ) ,
2169+ noAuth : Options . boolean ( "no-auth" )
2170+ . pipe ( Options . withDefault ( false ) )
2171+ . pipe ( Options . withDescription ( "Run without Basic auth (loopback trust only)." ) ) ,
2172+ } ,
2173+ ( { port, password, noAuth } ) =>
2174+ Effect . gen ( function * ( ) {
2175+ if ( isDevMode ) {
2176+ return yield * Effect . fail (
2177+ new Error (
2178+ [
2179+ "`service install` requires the compiled `executor` binary so the OS can run it directly." ,
2180+ `In a dev checkout, run \`${ cliPrefix } daemon run --foreground\` instead.` ,
2181+ ] . join ( "\n" ) ,
2182+ ) ,
2183+ ) ;
2184+ }
2185+
2186+ const backend = getServiceBackend ( ) ;
2187+ if ( ! backend . automated ) {
2188+ // win32 / unsupported: the backend surfaces manual steps via its error.
2189+ yield * backend . install ( { executablePath : process . execPath , port, version : CLI_VERSION } ) ;
2190+ return ;
2191+ }
2192+
2193+ // Don't fight an already-running local server against the same data dir
2194+ // (a desktop sidecar, a foreground `executor web`, or an existing daemon).
2195+ const active = yield * readActiveLocalServerManifest ( ) ;
2196+ if ( active ) {
2197+ const status = yield * backend . status ( ) ;
2198+ if ( status . registered && status . running && active . kind === "cli-daemon" ) {
2199+ console . log (
2200+ `Executor service already running at ${ active . connection . origin } (pid ${ active . pid } ).` ,
2201+ ) ;
2202+ return ;
2203+ }
2204+ return yield * Effect . fail (
2205+ new Error (
2206+ [
2207+ `A local Executor ${ active . kind } is already running at ${ active . connection . origin } (pid ${ active . pid } ).` ,
2208+ `Stop it first (quit the desktop app, or \`${ cliPrefix } daemon stop\`), then re-run install.` ,
2209+ ] . join ( "\n" ) ,
2210+ ) ,
2211+ ) ;
2212+ }
2213+
2214+ let servicePassword : string | null = null ;
2215+ if ( ! noAuth ) {
2216+ const explicit = Option . getOrUndefined ( password ) ;
2217+ const existing = yield * readServiceKey ( ) ;
2218+ servicePassword = explicit ?? existing ?. password ?? generateServicePassword ( ) ;
2219+ yield * writeServiceKey ( { password : servicePassword , createdAt : new Date ( ) . toISOString ( ) } ) ;
2220+ }
2221+
2222+ yield * backend . install ( { executablePath : process . execPath , port, version : CLI_VERSION } ) ;
2223+
2224+ const origin = supervisedServiceOrigin ( port ) ;
2225+ const reachable = yield * waitForReachable ( {
2226+ check : isServerReachable ( origin , supervisedServiceAuthorization ( servicePassword , port ) ) ,
2227+ timeoutMs : DAEMON_BOOT_TIMEOUT_MS ,
2228+ intervalMs : DAEMON_BOOT_POLL_MS ,
2229+ } ) ;
2230+ if ( ! reachable ) {
2231+ return yield * Effect . fail (
2232+ new Error (
2233+ [
2234+ `Installed ${ SERVICE_LABEL } but it did not become reachable at ${ origin } within ${ DAEMON_BOOT_TIMEOUT_MS / 1000 } s.` ,
2235+ `Check ~/.executor/logs/daemon.error.log and \`${ cliPrefix } service status\`.` ,
2236+ ] . join ( "\n" ) ,
2237+ ) ,
2238+ ) ;
2239+ }
2240+
2241+ console . log ( `Executor is now running as a background service at ${ origin } .` ) ;
2242+ console . log ( "It keeps serving after you quit the app and restarts on login." ) ;
2243+ if ( servicePassword ) {
2244+ console . log (
2245+ `Basic auth user "${ DEFAULT_EXECUTOR_SERVER_USERNAME } "; password saved to ~/.executor/server-control/service.key.` ,
2246+ ) ;
2247+ }
2248+ } ) ,
2249+ ) . pipe (
2250+ Command . withDescription ( "Install and start Executor as an OS-supervised background service" ) ,
2251+ ) ;
2252+
2253+ const serviceUninstallCommand = Command . make ( "uninstall" , { } , ( ) =>
2254+ Effect . gen ( function * ( ) {
2255+ const backend = getServiceBackend ( ) ;
2256+ yield * backend . uninstall ( ) ;
2257+ yield * removeServiceKey ( ) . pipe ( Effect . ignore ) ;
2258+ console . log ( "Executor background service uninstalled." ) ;
2259+ } ) ,
2260+ ) . pipe ( Command . withDescription ( "Stop and remove the OS-supervised background service" ) ) ;
2261+
2262+ const serviceStatusCommand = Command . make ( "status" , { } , ( ) =>
2263+ Effect . gen ( function * ( ) {
2264+ const backend = getServiceBackend ( ) ;
2265+ const status = yield * backend . status ( ) ;
2266+ // Tolerate a registered-but-unreachable manifest here — status shouldn't throw.
2267+ const active = yield * readActiveLocalServerManifest ( ) . pipe (
2268+ Effect . catchCause ( ( ) => Effect . succeed ( null ) ) ,
2269+ ) ;
2270+ console . log ( `Platform: ${ status . platform } ` ) ;
2271+ console . log ( `Registered: ${ status . registered ? "yes" : "no" } ` ) ;
2272+ console . log (
2273+ `Running: ${ status . running ? "yes" : "no" } ${ status . pid ? ` (pid ${ status . pid } )` : "" } ` ,
2274+ ) ;
2275+ if ( active ) {
2276+ console . log ( `Serving: ${ active . connection . origin } (${ active . kind } , pid ${ active . pid } )` ) ;
2277+ // Version drift: the running daemon was launched by the binary the unit
2278+ // points at. If that differs from this CLI, an upgrade left the unit
2279+ // pointing at an older binary — reinstall to repoint + restart.
2280+ if ( active . owner . version && active . owner . version !== CLI_VERSION ) {
2281+ console . log (
2282+ `Drift: running ${ active . owner . version } , current ${ CLI_VERSION } — run \`${ cliPrefix } service install\` to upgrade.` ,
2283+ ) ;
2284+ }
2285+ }
2286+ for ( const line of status . detail ) console . log ( line ) ;
2287+ } ) ,
2288+ ) . pipe ( Command . withDescription ( "Show the OS-supervised service status" ) ) ;
2289+
2290+ const serviceRestartCommand = Command . make ( "restart" , { } , ( ) =>
2291+ Effect . gen ( function * ( ) {
2292+ const backend = getServiceBackend ( ) ;
2293+ yield * backend . restart ( ) ;
2294+ console . log ( "Executor background service restarted." ) ;
2295+ } ) ,
2296+ ) . pipe ( Command . withDescription ( "Restart the OS-supervised background service" ) ) ;
2297+
2298+ const serviceCommand = Command . make ( "service" ) . pipe (
2299+ Command . withSubcommands ( [
2300+ serviceInstallCommand ,
2301+ serviceUninstallCommand ,
2302+ serviceStatusCommand ,
2303+ serviceRestartCommand ,
2304+ ] as const ) ,
2305+ Command . withDescription ( "Manage the OS-supervised background service" ) ,
2306+ ) ;
2307+
21132308// ---------------------------------------------------------------------------
21142309// Root command
21152310// ---------------------------------------------------------------------------
@@ -2122,6 +2317,7 @@ const root = Command.make("executor").pipe(
21222317 serverCommand ,
21232318 webCommand ,
21242319 daemonCommand ,
2320+ serviceCommand ,
21252321 mcpCommand ,
21262322 ] as const ) ,
21272323 Command . withDescription ( "Executor local CLI" ) ,
0 commit comments