| 228 | process.env.EXECUTOR_DESKTOP_SCOPE_DIR ?? join(homedir(), ".executor"); |
| 229 | |
| 230 | export async function startSidecar(options: StartOptions = {}): Promise<SidecarConnection> { |
| 231 | const hostname = options.hostname ?? "127.0.0.1"; |
| 232 | const settings = getServerSettings(); |
| 233 | // data.db and the optional executor.jsonc plugin manifest live under |
| 234 | // ~/.executor — the same path the CLI's `executor web` uses. Desktop and CLI |
| 235 | // share state on the same machine so sources/secrets/policies set up in one |
| 236 | // show up in the other, and user-facing commands like |
| 237 | // `executor mcp --scope ~/.executor` stay copy-paste-friendly. Electron's |
| 238 | // userData (set in main/index.ts) is still used for electron-store, |
| 239 | // electron-log, and window-state — those stay app-scoped to avoid colliding |
| 240 | // with anything else under HOME. |
| 241 | const scopeDir = executorScopeDir(); |
| 242 | const dataDir = scopeDir; |
| 243 | mkdirSync(dataDir, { recursive: true }); |
| 244 | |
| 245 | // The stable bearer token from auth.json (shared with the CLI). The main |
| 246 | // process holds it so it can inject the header into the webview; the child |
| 247 | // validates against the same value. Always present — auth is unconditional. |
| 248 | const authToken = loadOrMintLocalAuthToken(dataDir); |
| 249 | const { command, args, cwd, cliManagedManifest } = resolveSidecarCommand({ |
| 250 | port: settings.port, |
| 251 | hostname, |
| 252 | authToken, |
| 253 | }); |
| 254 | const clientDir = cliManagedManifest ? null : resolveClientDir(); |
| 255 | |
| 256 | if (!cliManagedManifest && clientDir && !existsSync(clientDir)) { |
| 257 | // oxlint-disable-next-line executor/no-error-constructor, executor/no-try-catch-or-throw -- boundary: startup failure is surfaced in the Electron main process |
| 258 | throw new Error( |
| 259 | `Executor client bundle not found at ${clientDir}. Run \`bun run --filter @executor-js/local build\` before launching desktop.`, |
| 260 | ); |
| 261 | } |
| 262 | |
| 263 | // No process-level startup lock: the dev sidecar child opens the DB through |
| 264 | // openOwnedLocalDatabase, whose ownership lock is the real gate. If the child |
| 265 | // loses the race, startup fails as before; only the packaged supervised boot |
| 266 | // path attaches to an existing daemon. |
| 267 | const webBaseUrl = `http://${hostname}:${settings.port}`; |
| 268 | const child = spawn(command, args, { |
| 269 | cwd, |
| 270 | stdio: ["ignore", "pipe", "pipe"], |
| 271 | env: { |
| 272 | ...process.env, |
| 273 | EXECUTOR_PORT: String(settings.port), |
| 274 | EXECUTOR_HOST: hostname, |
| 275 | EXECUTOR_WEB_BASE_URL: webBaseUrl, |
| 276 | PORT: String(settings.port), |
| 277 | // The bearer token the child validates and the main process injects into |
| 278 | // the webview. Always set — auth is unconditional. |
| 279 | EXECUTOR_AUTH_TOKEN: authToken, |
| 280 | ...(clientDir ? { EXECUTOR_CLIENT_DIR: clientDir } : {}), |
| 281 | EXECUTOR_SCOPE_DIR: scopeDir, |
| 282 | EXECUTOR_DATA_DIR: dataDir, |
| 283 | EXECUTOR_CLIENT: "desktop", |
| 284 | // Crash reporting (desktop builds with a baked-in DSN only). The |
| 285 | // CLI's `executor web` never sets these, so the shared server code |
| 286 | // stays telemetry-free outside the desktop app. |
| 287 | ...sidecarCrashReportingEnv(), |