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