()
| 1197 | }; |
| 1198 | |
| 1199 | const boot = async () => { |
| 1200 | installDockIcon(); |
| 1201 | installApplicationMenu(); |
| 1202 | await showStartupWindow(); |
| 1203 | setupAutoUpdater(); |
| 1204 | applyFakeUpdateFromEnv(); |
| 1205 | registerIpcHandlers(); |
| 1206 | // A sidecar that dies under a live window would leave the web UI failing |
| 1207 | // every request with no explanation. Swap in the crash screen — its |
| 1208 | // buttons drive the regular preload bridge (restart / export diagnostics). |
| 1209 | onUnexpectedSidecarExit((notice) => { |
| 1210 | // An expected shutdown (an interrupt aimed at the app) still leaves the web |
| 1211 | // UI dead, so the screen is the same — it just must not claim a crash |
| 1212 | // report was sent when none was. |
| 1213 | showCrashScreen(liveMainWindow(), { reported: notice.reported }); |
| 1214 | // A crashing sidecar may be a broken release — quietly stage any |
| 1215 | // available update so the install prompt appears on its own (same |
| 1216 | // self-heal as the fatal startup path). |
| 1217 | void runUpdateCheck({ alertOnFail: false, trigger: "boot" }); |
| 1218 | }); |
| 1219 | // Prefer an OS-supervised daemon: attach to one that's running, kick one |
| 1220 | // that's installed, or offer to install on first run. Quitting the app then |
| 1221 | // leaves MCP serving. This is also the clean handoff that replaces the old |
| 1222 | // "another server owns the data dir → fatal error" path. Packaged builds only; |
| 1223 | // dev and unsupported platforms keep managed-spawn. |
| 1224 | if (app.isPackaged) { |
| 1225 | const supervised = await ensureSupervisedConnection(); |
| 1226 | if (supervised) { |
| 1227 | connection = supervised; |
| 1228 | // createWindow installs the bearer-auth header itself. |
| 1229 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: supervised attach can race with daemon shutdown; fall back to managed spawn |
| 1230 | try { |
| 1231 | await createWindow(supervised); |
| 1232 | armSupervisedMonitor(); |
| 1233 | void runUpdateCheck({ alertOnFail: false, trigger: "boot" }); |
| 1234 | return; |
| 1235 | } catch (error) { |
| 1236 | log.warn("Failed to load supervised daemon; falling back to managed sidecar", error); |
| 1237 | stopSupervisedMonitor(); |
| 1238 | connection = null; |
| 1239 | } |
| 1240 | } |
| 1241 | } |
| 1242 | connection = await startWithCurrentSettings(); |
| 1243 | if (!connection && lastSidecarStartError != null) { |
| 1244 | // Port conflicts already showed their dialog inside |
| 1245 | // startWithCurrentSettings; every other failure surfaces here so the app |
| 1246 | // never silently bounces-and-vanishes. The dialog offers a data reset |
| 1247 | // (move-aside, for damaged state) — when taken, retry the boot once |
| 1248 | // against the fresh dir. |
| 1249 | const retryAfterReset = await handleFatalSidecarFailure(lastSidecarStartError); |
| 1250 | if (retryAfterReset) { |
| 1251 | lastSidecarStartError = null; |
| 1252 | connection = await startWithCurrentSettings(); |
| 1253 | if (!connection && lastSidecarStartError != null) { |
| 1254 | await handleFatalSidecarFailure(lastSidecarStartError); |
| 1255 | } |
| 1256 | } |
no test coverage detected