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