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