synabun 2.0.0-beta → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (374) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/NOTICE +5 -0
  3. package/README.md +1 -1
  4. package/THIRD-PARTY-LICENSES.md +51 -0
  5. package/benchmarks/fixtures/browser/README.md +1 -1
  6. package/benchmarks/fixtures/desktop/README.md +1 -1
  7. package/docs/whatsapp.md +2 -2
  8. package/hooks/claude-code/pre-compact.mjs +11 -0
  9. package/hooks/claude-code/prompt-submit.mjs +60 -184
  10. package/hooks/claude-code/session-start.mjs +73 -16
  11. package/hooks/claude-code/shared.mjs +20 -0
  12. package/hooks/claude-code/stop.mjs +5 -5
  13. package/launcher.mjs +40 -0
  14. package/lib/claude-hooks.js +61 -8
  15. package/lib/client-config-repair.js +4 -3
  16. package/lib/data-home-migration.js +4 -0
  17. package/lib/desktop-pwa.js +206 -0
  18. package/lib/external-tools.js +221 -0
  19. package/lib/first-launch-protection.js +5 -0
  20. package/lib/packaged-runtime.js +110 -0
  21. package/lib/start-launcher.js +1396 -0
  22. package/mcp-server/package-lock.json +2 -2
  23. package/mcp-server/package.json +1 -3
  24. package/neural-interface/i18n/en.json +1540 -154
  25. package/neural-interface/i18n/pt-BR.json +1538 -153
  26. package/neural-interface/lib/assistant-api.js +62 -6
  27. package/neural-interface/lib/assistant-brains/claude.js +4 -0
  28. package/neural-interface/lib/assistant-catalog.js +179 -27
  29. package/neural-interface/lib/assistant-config.js +31 -2
  30. package/neural-interface/lib/assistant-dispatch.js +62 -13
  31. package/neural-interface/lib/assistant-envelope.js +9 -6
  32. package/neural-interface/lib/assistant-hidden-models.js +393 -66
  33. package/neural-interface/lib/assistant-memory.js +24 -10
  34. package/neural-interface/lib/assistant-persona.js +6 -2
  35. package/neural-interface/lib/assistant-pricing.js +21 -6
  36. package/neural-interface/lib/assistant-runtime.js +181 -12
  37. package/neural-interface/lib/assistant-task-prompt.js +13 -3
  38. package/neural-interface/lib/assistant-test-density.js +142 -0
  39. package/neural-interface/lib/assistant-user-context-budgets.js +123 -0
  40. package/neural-interface/lib/assistant-user-context.js +908 -0
  41. package/neural-interface/lib/assistant-user-knowledge.js +328 -0
  42. package/neural-interface/lib/assistant-user-learn.js +404 -0
  43. package/neural-interface/lib/assistant-user-threads.js +226 -0
  44. package/neural-interface/lib/assistant-user-writer.js +181 -0
  45. package/neural-interface/lib/augmented-path.js +23 -3
  46. package/neural-interface/lib/browser-background-tab.js +134 -0
  47. package/neural-interface/lib/browser-language.js +83 -0
  48. package/neural-interface/lib/browser-screenshot.js +678 -61
  49. package/neural-interface/lib/claude-agent-bridge.js +87 -153
  50. package/neural-interface/lib/claude-executable.js +241 -0
  51. package/neural-interface/lib/claude-model-catalog.js +82 -93
  52. package/neural-interface/lib/claude-temporary.js +46 -7
  53. package/neural-interface/lib/cloudflared/install.js +533 -0
  54. package/neural-interface/lib/cloudflared/manager.js +545 -0
  55. package/neural-interface/lib/cloudflared/pins.js +115 -0
  56. package/neural-interface/lib/cloudflared/pins.json +63 -0
  57. package/neural-interface/lib/cloudflared/tunnel.js +265 -0
  58. package/neural-interface/lib/editor-completion-endpoint.js +264 -0
  59. package/neural-interface/lib/editor-completion-opencode.js +436 -0
  60. package/neural-interface/lib/editor-completion.js +714 -0
  61. package/neural-interface/lib/editor-config.js +149 -0
  62. package/neural-interface/lib/editor-files-api.js +268 -0
  63. package/neural-interface/lib/editor-git-api.js +134 -0
  64. package/neural-interface/lib/file-explorer-api.js +1039 -0
  65. package/neural-interface/lib/file-explorer-system.js +355 -0
  66. package/neural-interface/lib/file-explorer-trash.ps1 +85 -0
  67. package/neural-interface/lib/file-explorer-watch.js +239 -0
  68. package/neural-interface/lib/git-diff-api.js +103 -0
  69. package/neural-interface/lib/git-read.js +202 -0
  70. package/neural-interface/lib/greeting/api.js +89 -0
  71. package/neural-interface/lib/greeting/budgets.js +90 -0
  72. package/neural-interface/lib/greeting/engine.js +461 -0
  73. package/neural-interface/lib/greeting/facts.js +320 -0
  74. package/neural-interface/lib/greeting/sources.js +252 -0
  75. package/neural-interface/lib/greeting/writer.js +139 -0
  76. package/neural-interface/lib/http-guards.js +189 -13
  77. package/neural-interface/lib/machine-names.js +88 -0
  78. package/neural-interface/lib/memory-api.js +31 -7
  79. package/neural-interface/lib/native-binary-runtime.js +69 -337
  80. package/neural-interface/lib/native-loop-providers.js +40 -49
  81. package/neural-interface/lib/offline-page.js +54 -0
  82. package/neural-interface/lib/plugin-installer.js +4 -2
  83. package/neural-interface/lib/project-storage.js +5 -7
  84. package/neural-interface/lib/request-gate.js +589 -0
  85. package/neural-interface/lib/session-title-generator.js +5 -0
  86. package/neural-interface/lib/synabun-update-plan.js +19 -0
  87. package/neural-interface/lib/ui-blob-api.js +108 -0
  88. package/neural-interface/lib/ui-blob-store.js +68 -0
  89. package/neural-interface/package-lock.json +701 -253
  90. package/neural-interface/package.json +13 -2
  91. package/neural-interface/public/install-app.html +34 -0
  92. package/neural-interface/public/manifest.json +2 -0
  93. package/neural-interface/public/offline.html +402 -66
  94. package/neural-interface/public/onboarding.html +264 -147
  95. package/neural-interface/public/shared/agent-runtime-options.js +22 -6
  96. package/neural-interface/public/shared/assistant/asst-automation.js +286 -0
  97. package/neural-interface/public/shared/assistant/asst-brain-picker.js +312 -62
  98. package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
  99. package/neural-interface/public/shared/assistant/asst-mascot.js +26 -64
  100. package/neural-interface/public/shared/assistant/asst-menu.js +12 -8
  101. package/neural-interface/public/shared/assistant/asst-panel.js +150 -28
  102. package/neural-interface/public/shared/assistant/asst-rack-styles.js +0 -3
  103. package/neural-interface/public/shared/assistant/asst-render.js +219 -67
  104. package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
  105. package/neural-interface/public/shared/assistant/asst-route.js +23 -329
  106. package/neural-interface/public/shared/assistant/asst-routes-editor.js +495 -0
  107. package/neural-interface/public/shared/assistant/asst-sanitize.js +8 -1
  108. package/neural-interface/public/shared/assistant/asst-sidepanel.js +31 -9
  109. package/neural-interface/public/shared/assistant/asst-state.js +185 -1
  110. package/neural-interface/public/shared/assistant/asst-styles.js +162 -6
  111. package/neural-interface/public/shared/blob-store.js +111 -22
  112. package/neural-interface/public/shared/cdx/cdx-panel.js +32 -11
  113. package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
  114. package/neural-interface/public/shared/cdx/cdx-styles.js +36 -0
  115. package/neural-interface/public/shared/cdx/cdx-tabs.js +87 -4
  116. package/neural-interface/public/shared/command-runner-model.js +30 -0
  117. package/neural-interface/public/shared/constants.js +6 -1
  118. package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
  119. package/neural-interface/public/shared/cp/cp-styles.js +15 -0
  120. package/neural-interface/public/shared/editor/editor-ai-logic.js +409 -0
  121. package/neural-interface/public/shared/editor/editor-ai.js +619 -0
  122. package/neural-interface/public/shared/editor/editor-commands.js +212 -0
  123. package/neural-interface/public/shared/editor/editor-compare.js +103 -0
  124. package/neural-interface/public/shared/editor/editor-completion-list.js +172 -0
  125. package/neural-interface/public/shared/editor/editor-doc.js +257 -0
  126. package/neural-interface/public/shared/editor/editor-git-logic.js +131 -0
  127. package/neural-interface/public/shared/editor/editor-git.js +95 -0
  128. package/neural-interface/public/shared/editor/editor-languages.js +156 -0
  129. package/neural-interface/public/shared/editor/editor-lint.js +229 -0
  130. package/neural-interface/public/shared/editor/editor-loader.js +51 -0
  131. package/neural-interface/public/shared/editor/editor-markdown.js +90 -0
  132. package/neural-interface/public/shared/editor/editor-menu.js +233 -0
  133. package/neural-interface/public/shared/editor/editor-minimap.js +371 -0
  134. package/neural-interface/public/shared/editor/editor-palette-logic.js +192 -0
  135. package/neural-interface/public/shared/editor/editor-palette.js +125 -0
  136. package/neural-interface/public/shared/editor/editor-panel-logic.js +288 -0
  137. package/neural-interface/public/shared/editor/editor-panel.js +2099 -0
  138. package/neural-interface/public/shared/editor/editor-prefs.js +224 -0
  139. package/neural-interface/public/shared/editor/editor-safety.js +500 -0
  140. package/neural-interface/public/shared/editor/editor-search.js +352 -0
  141. package/neural-interface/public/shared/editor/editor-statusbar.js +144 -0
  142. package/neural-interface/public/shared/editor/editor-tabs-logic.js +331 -0
  143. package/neural-interface/public/shared/editor/editor-tabs.js +233 -0
  144. package/neural-interface/public/shared/editor/editor-theme.js +244 -0
  145. package/neural-interface/public/shared/editor/editor-view.js +332 -0
  146. package/neural-interface/public/shared/explorer/fx-api.js +216 -0
  147. package/neural-interface/public/shared/explorer/fx-links.js +204 -0
  148. package/neural-interface/public/shared/explorer/fx-logic.js +537 -0
  149. package/neural-interface/public/shared/explorer/fx-main.js +865 -0
  150. package/neural-interface/public/shared/explorer/fx-names.js +87 -0
  151. package/neural-interface/public/shared/explorer/fx-ops.js +370 -0
  152. package/neural-interface/public/shared/explorer/fx-popups.js +490 -0
  153. package/neural-interface/public/shared/explorer/fx-tree.js +790 -0
  154. package/neural-interface/public/shared/file-links.js +339 -0
  155. package/neural-interface/public/shared/greeting/greeting-card.js +178 -0
  156. package/neural-interface/public/shared/greeting/greeting-view.js +148 -0
  157. package/neural-interface/public/shared/html-shell.js +169 -170
  158. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -0
  159. package/neural-interface/public/shared/ocp-v2/ocp-v2-greeting.js +47 -0
  160. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +99 -17
  161. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
  162. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +5 -0
  163. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +40 -0
  164. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
  165. package/neural-interface/public/shared/pwa-install-page.js +62 -0
  166. package/neural-interface/public/shared/pwa-install.css +12 -0
  167. package/neural-interface/public/shared/pwa-install.js +127 -0
  168. package/neural-interface/public/shared/settings/settings-code-editor-pane.js +412 -0
  169. package/neural-interface/public/shared/settings/settings-code-editor.js +328 -0
  170. package/neural-interface/public/shared/settings/settings-greeting.js +133 -0
  171. package/neural-interface/public/shared/settings/settings-ia.js +18 -4
  172. package/neural-interface/public/shared/settings/settings-rules.js +160 -0
  173. package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
  174. package/neural-interface/public/shared/start-bridge.js +802 -0
  175. package/neural-interface/public/shared/styleguide/sg-colors.js +5 -2
  176. package/neural-interface/public/shared/styleguide/sg-import-export.js +4 -3
  177. package/neural-interface/public/shared/styleguide/sg-kit.js +4 -1
  178. package/neural-interface/public/shared/styleguide/sg-panel.js +9 -1
  179. package/neural-interface/public/shared/styleguide/sg-typography.js +1 -0
  180. package/neural-interface/public/shared/styles.css +2219 -1117
  181. package/neural-interface/public/shared/synabun-mascot.js +20 -56
  182. package/neural-interface/public/shared/term-ready.js +307 -0
  183. package/neural-interface/public/shared/tunnel-view.js +167 -0
  184. package/neural-interface/public/shared/ui-claude-panel.js +160 -19
  185. package/neural-interface/public/shared/ui-command-runner.js +251 -468
  186. package/neural-interface/public/shared/ui-detail.js +15 -0
  187. package/neural-interface/public/shared/ui-explorer.js +22 -0
  188. package/neural-interface/public/shared/ui-file-explorer.js +481 -2496
  189. package/neural-interface/public/shared/ui-file-links.js +103 -0
  190. package/neural-interface/public/shared/ui-help.js +26 -0
  191. package/neural-interface/public/shared/ui-invite.js +126 -116
  192. package/neural-interface/public/shared/ui-judgments.js +1 -1
  193. package/neural-interface/public/shared/ui-keybinds.js +4 -0
  194. package/neural-interface/public/shared/ui-loading.js +367 -12
  195. package/neural-interface/public/shared/ui-menubar.js +21 -55
  196. package/neural-interface/public/shared/ui-native-loop-router-state.js +11 -0
  197. package/neural-interface/public/shared/ui-native-loop-router.js +15 -2
  198. package/neural-interface/public/shared/ui-navbar.js +45 -21
  199. package/neural-interface/public/shared/ui-settings.js +491 -547
  200. package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
  201. package/neural-interface/public/shared/ui-sync.js +16 -0
  202. package/neural-interface/public/shared/ui-terminal.js +153 -149
  203. package/neural-interface/public/shared/ui-tunnel.js +130 -0
  204. package/neural-interface/public/shared/ui-tutorial-steps.js +6 -2
  205. package/neural-interface/public/shared/ui-whiteboard.js +12 -0
  206. package/neural-interface/public/shared/ui-worker-end.js +245 -0
  207. package/neural-interface/public/shared/ui-workspaces.js +41 -27
  208. package/neural-interface/public/shared/worker-end.js +251 -0
  209. package/neural-interface/public/sw.js +3 -1
  210. package/neural-interface/public/variant/2d/main.js +2 -1
  211. package/neural-interface/public/variant/3d/main.js +2 -1
  212. package/neural-interface/public/vendor/codemirror/chunks/apl-CSDKT72Z.js +1 -0
  213. package/neural-interface/public/vendor/codemirror/chunks/asciiarmor-JYYPU5BM.js +1 -0
  214. package/neural-interface/public/vendor/codemirror/chunks/asn1-NRWCZDA6.js +1 -0
  215. package/neural-interface/public/vendor/codemirror/chunks/asterisk-5OU6D4ZQ.js +1 -0
  216. package/neural-interface/public/vendor/codemirror/chunks/brainfuck-ID4N4SYC.js +1 -0
  217. package/neural-interface/public/vendor/codemirror/chunks/chunk-4RZWGCS2.js +1 -0
  218. package/neural-interface/public/vendor/codemirror/chunks/chunk-ACTJM3JR.js +11 -0
  219. package/neural-interface/public/vendor/codemirror/chunks/chunk-S6BMAXLK.js +1 -0
  220. package/neural-interface/public/vendor/codemirror/chunks/chunk-TO2RDOHQ.js +3 -0
  221. package/neural-interface/public/vendor/codemirror/chunks/chunk-US2GMZZQ.js +13 -0
  222. package/neural-interface/public/vendor/codemirror/chunks/chunk-WYYO5ZAJ.js +1 -0
  223. package/neural-interface/public/vendor/codemirror/chunks/chunk-XALZTDJC.js +1 -0
  224. package/neural-interface/public/vendor/codemirror/chunks/chunk-YRNUTRDV.js +1 -0
  225. package/neural-interface/public/vendor/codemirror/chunks/chunk-Z3QOKH62.js +1 -0
  226. package/neural-interface/public/vendor/codemirror/chunks/clike-OWQBSOGL.js +1 -0
  227. package/neural-interface/public/vendor/codemirror/chunks/clojure-KV75UQWC.js +1 -0
  228. package/neural-interface/public/vendor/codemirror/chunks/cmake-GTEP5Y3C.js +1 -0
  229. package/neural-interface/public/vendor/codemirror/chunks/cobol-WCZ7QEGJ.js +1 -0
  230. package/neural-interface/public/vendor/codemirror/chunks/coffeescript-TGLBOT6B.js +1 -0
  231. package/neural-interface/public/vendor/codemirror/chunks/commonlisp-WVVI432V.js +1 -0
  232. package/neural-interface/public/vendor/codemirror/chunks/crystal-S6R6HLKQ.js +1 -0
  233. package/neural-interface/public/vendor/codemirror/chunks/css-PDA2RINP.js +1 -0
  234. package/neural-interface/public/vendor/codemirror/chunks/cypher-HL5QPL4C.js +1 -0
  235. package/neural-interface/public/vendor/codemirror/chunks/d-HHJH2HAJ.js +1 -0
  236. package/neural-interface/public/vendor/codemirror/chunks/diff-MN3A35BY.js +1 -0
  237. package/neural-interface/public/vendor/codemirror/chunks/dist-25SQLBVS.js +3 -0
  238. package/neural-interface/public/vendor/codemirror/chunks/dist-27ZGGJJS.js +1 -0
  239. package/neural-interface/public/vendor/codemirror/chunks/dist-4E52OJIF.js +1 -0
  240. package/neural-interface/public/vendor/codemirror/chunks/dist-6FT73XDC.js +2 -0
  241. package/neural-interface/public/vendor/codemirror/chunks/dist-BE6YGRMW.js +1 -0
  242. package/neural-interface/public/vendor/codemirror/chunks/dist-CJSGKW2O.js +1 -0
  243. package/neural-interface/public/vendor/codemirror/chunks/dist-CRPH2WLI.js +1 -0
  244. package/neural-interface/public/vendor/codemirror/chunks/dist-CTG7JEQN.js +1 -0
  245. package/neural-interface/public/vendor/codemirror/chunks/dist-I3FEVELX.js +1 -0
  246. package/neural-interface/public/vendor/codemirror/chunks/dist-IM5BBSWZ.js +1 -0
  247. package/neural-interface/public/vendor/codemirror/chunks/dist-KERUKTIF.js +1 -0
  248. package/neural-interface/public/vendor/codemirror/chunks/dist-LXTMWZAM.js +1 -0
  249. package/neural-interface/public/vendor/codemirror/chunks/dist-MABASSET.js +1 -0
  250. package/neural-interface/public/vendor/codemirror/chunks/dist-MJVBUZLL.js +3 -0
  251. package/neural-interface/public/vendor/codemirror/chunks/dist-MNB3WZVU.js +1 -0
  252. package/neural-interface/public/vendor/codemirror/chunks/dist-QYWXOE3U.js +7 -0
  253. package/neural-interface/public/vendor/codemirror/chunks/dist-TEBOFWLZ.js +1 -0
  254. package/neural-interface/public/vendor/codemirror/chunks/dist-UNXFROM3.js +1 -0
  255. package/neural-interface/public/vendor/codemirror/chunks/dist-XL2SLQO6.js +1 -0
  256. package/neural-interface/public/vendor/codemirror/chunks/dist-XO42Y3AP.js +6 -0
  257. package/neural-interface/public/vendor/codemirror/chunks/dist-Y2WWDWJK.js +1 -0
  258. package/neural-interface/public/vendor/codemirror/chunks/dist-YFEZKWMU.js +1 -0
  259. package/neural-interface/public/vendor/codemirror/chunks/dist-ZY7SWQO6.js +1 -0
  260. package/neural-interface/public/vendor/codemirror/chunks/dockerfile-3Y4R4NCH.js +1 -0
  261. package/neural-interface/public/vendor/codemirror/chunks/dtd-5343GJ4P.js +1 -0
  262. package/neural-interface/public/vendor/codemirror/chunks/dylan-FI2CXSW2.js +1 -0
  263. package/neural-interface/public/vendor/codemirror/chunks/ebnf-GNQ77YWA.js +1 -0
  264. package/neural-interface/public/vendor/codemirror/chunks/ecl-MRPUAE2L.js +1 -0
  265. package/neural-interface/public/vendor/codemirror/chunks/eiffel-DZROES27.js +1 -0
  266. package/neural-interface/public/vendor/codemirror/chunks/elm-4KICRFKQ.js +1 -0
  267. package/neural-interface/public/vendor/codemirror/chunks/erlang-3IHYC263.js +1 -0
  268. package/neural-interface/public/vendor/codemirror/chunks/factor-C6D3E5UK.js +1 -0
  269. package/neural-interface/public/vendor/codemirror/chunks/fcl-VYTWUJMI.js +1 -0
  270. package/neural-interface/public/vendor/codemirror/chunks/forth-ZBO2EH5M.js +1 -0
  271. package/neural-interface/public/vendor/codemirror/chunks/fortran-5NDCRWIK.js +1 -0
  272. package/neural-interface/public/vendor/codemirror/chunks/gas-SKRMOXFP.js +1 -0
  273. package/neural-interface/public/vendor/codemirror/chunks/gherkin-L45YXZLW.js +1 -0
  274. package/neural-interface/public/vendor/codemirror/chunks/groovy-GLPTLN44.js +1 -0
  275. package/neural-interface/public/vendor/codemirror/chunks/haskell-E7NF7P4Y.js +1 -0
  276. package/neural-interface/public/vendor/codemirror/chunks/haxe-K2NVDOB6.js +1 -0
  277. package/neural-interface/public/vendor/codemirror/chunks/http-Q6DRQIJB.js +1 -0
  278. package/neural-interface/public/vendor/codemirror/chunks/idl-MZYNJVLN.js +1 -0
  279. package/neural-interface/public/vendor/codemirror/chunks/javascript-6YRYK2HR.js +1 -0
  280. package/neural-interface/public/vendor/codemirror/chunks/julia-HXEL2T5G.js +1 -0
  281. package/neural-interface/public/vendor/codemirror/chunks/livescript-MV7CI2JZ.js +1 -0
  282. package/neural-interface/public/vendor/codemirror/chunks/lua-E36NHOOD.js +1 -0
  283. package/neural-interface/public/vendor/codemirror/chunks/mathematica-3XOZZCMC.js +1 -0
  284. package/neural-interface/public/vendor/codemirror/chunks/mbox-ODYGFKS7.js +1 -0
  285. package/neural-interface/public/vendor/codemirror/chunks/mirc-WB43AWF5.js +1 -0
  286. package/neural-interface/public/vendor/codemirror/chunks/mllike-ZT6ET7ZH.js +1 -0
  287. package/neural-interface/public/vendor/codemirror/chunks/modelica-LLMZPFGA.js +1 -0
  288. package/neural-interface/public/vendor/codemirror/chunks/mscgen-2OPNNYFH.js +1 -0
  289. package/neural-interface/public/vendor/codemirror/chunks/mumps-6P3HWJH6.js +1 -0
  290. package/neural-interface/public/vendor/codemirror/chunks/nginx-TCGKQTRW.js +1 -0
  291. package/neural-interface/public/vendor/codemirror/chunks/nsis-ATIGIXTJ.js +1 -0
  292. package/neural-interface/public/vendor/codemirror/chunks/ntriples-5PLBI5OV.js +1 -0
  293. package/neural-interface/public/vendor/codemirror/chunks/octave-QF2S3NL6.js +1 -0
  294. package/neural-interface/public/vendor/codemirror/chunks/oz-JFIVFFH2.js +1 -0
  295. package/neural-interface/public/vendor/codemirror/chunks/pascal-JG6SW76V.js +1 -0
  296. package/neural-interface/public/vendor/codemirror/chunks/perl-4B3IWVVO.js +1 -0
  297. package/neural-interface/public/vendor/codemirror/chunks/pig-ABEBH4MI.js +1 -0
  298. package/neural-interface/public/vendor/codemirror/chunks/powershell-H32XE7TL.js +1 -0
  299. package/neural-interface/public/vendor/codemirror/chunks/properties-NN644MMA.js +1 -0
  300. package/neural-interface/public/vendor/codemirror/chunks/protobuf-YKWQK2J3.js +1 -0
  301. package/neural-interface/public/vendor/codemirror/chunks/pug-RIYXEKFW.js +1 -0
  302. package/neural-interface/public/vendor/codemirror/chunks/puppet-JZQ5WPEV.js +1 -0
  303. package/neural-interface/public/vendor/codemirror/chunks/python-EBCGGHAC.js +1 -0
  304. package/neural-interface/public/vendor/codemirror/chunks/q-5UMH2YQY.js +1 -0
  305. package/neural-interface/public/vendor/codemirror/chunks/r-7XMGG453.js +1 -0
  306. package/neural-interface/public/vendor/codemirror/chunks/rpm-IEV7TSLR.js +1 -0
  307. package/neural-interface/public/vendor/codemirror/chunks/ruby-T4SPFPNM.js +1 -0
  308. package/neural-interface/public/vendor/codemirror/chunks/sas-GEDX67ET.js +1 -0
  309. package/neural-interface/public/vendor/codemirror/chunks/scheme-FTLOWKPC.js +1 -0
  310. package/neural-interface/public/vendor/codemirror/chunks/shell-HUFRKFSJ.js +1 -0
  311. package/neural-interface/public/vendor/codemirror/chunks/sieve-P5QUDVYO.js +1 -0
  312. package/neural-interface/public/vendor/codemirror/chunks/smalltalk-2SORJNQI.js +1 -0
  313. package/neural-interface/public/vendor/codemirror/chunks/solr-M22TGAR5.js +1 -0
  314. package/neural-interface/public/vendor/codemirror/chunks/sparql-4M3KL752.js +1 -0
  315. package/neural-interface/public/vendor/codemirror/chunks/spreadsheet-3K23LA4N.js +1 -0
  316. package/neural-interface/public/vendor/codemirror/chunks/sql-VHRFMUBI.js +1 -0
  317. package/neural-interface/public/vendor/codemirror/chunks/stex-QZDAQUY2.js +1 -0
  318. package/neural-interface/public/vendor/codemirror/chunks/stylus-7ZDKAKBD.js +1 -0
  319. package/neural-interface/public/vendor/codemirror/chunks/swift-WCLCLGMR.js +1 -0
  320. package/neural-interface/public/vendor/codemirror/chunks/tcl-JLMU6WOK.js +1 -0
  321. package/neural-interface/public/vendor/codemirror/chunks/textile-4CHDKZ4T.js +1 -0
  322. package/neural-interface/public/vendor/codemirror/chunks/tiddlywiki-DKMCZZQ4.js +1 -0
  323. package/neural-interface/public/vendor/codemirror/chunks/tiki-F4JIG36Q.js +1 -0
  324. package/neural-interface/public/vendor/codemirror/chunks/toml-BHMHTIM2.js +1 -0
  325. package/neural-interface/public/vendor/codemirror/chunks/troff-YQ2KV5NT.js +1 -0
  326. package/neural-interface/public/vendor/codemirror/chunks/ttcn-ZHRCVFJY.js +1 -0
  327. package/neural-interface/public/vendor/codemirror/chunks/ttcn-cfg-KJOVTY4X.js +1 -0
  328. package/neural-interface/public/vendor/codemirror/chunks/turtle-535P5N6I.js +1 -0
  329. package/neural-interface/public/vendor/codemirror/chunks/vb-76UMQHOT.js +1 -0
  330. package/neural-interface/public/vendor/codemirror/chunks/vbscript-CGNDTK6Z.js +1 -0
  331. package/neural-interface/public/vendor/codemirror/chunks/velocity-VSJDO5T5.js +1 -0
  332. package/neural-interface/public/vendor/codemirror/chunks/verilog-4T6ETXIP.js +1 -0
  333. package/neural-interface/public/vendor/codemirror/chunks/vhdl-T7FNAXUN.js +1 -0
  334. package/neural-interface/public/vendor/codemirror/chunks/webidl-P6MJSJ6J.js +1 -0
  335. package/neural-interface/public/vendor/codemirror/chunks/xquery-UOVILRGN.js +1 -0
  336. package/neural-interface/public/vendor/codemirror/chunks/yacas-CV2APROL.js +1 -0
  337. package/neural-interface/public/vendor/codemirror/chunks/z80-3KFNVM4M.js +1 -0
  338. package/neural-interface/public/vendor/codemirror/codemirror-6.43.14-75f05e67.min.js +2 -0
  339. package/neural-interface/public/vendor/codemirror/manifest.json +60 -0
  340. package/neural-interface/scripts/build-vendor.mjs +78 -2
  341. package/neural-interface/scripts/rebuild-pty.js +15 -12
  342. package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
  343. package/neural-interface/server.js +1092 -1212
  344. package/neural-interface/templates/rulesets/README.md +1 -1
  345. package/package.json +13 -7
  346. package/postinstall.js +11 -81
  347. package/setup.js +111 -6
  348. package/benchmarks/browser-live-results.json +0 -79
  349. package/benchmarks/browser-v2-results.json +0 -258
  350. package/docs/browser-v2.md +0 -109
  351. package/docs/hook-integrity.md +0 -89
  352. package/docs/judgments.md +0 -182
  353. package/docs/style-guide.md +0 -246
  354. package/neural-interface/lib/codex-capabilities.d.ts +0 -41
  355. package/neural-interface/public/category-logos/claude.svg +0 -1
  356. package/neural-interface/public/category-logos/gemini-color.svg +0 -1
  357. package/neural-interface/public/category-logos/openai.svg +0 -1
  358. package/neural-interface/public/category-logos/synabun.png +0 -0
  359. package/neural-interface/public/claude.svg +0 -1
  360. package/neural-interface/public/gallery.svg +0 -6
  361. package/neural-interface/public/leonardoai.svg +0 -1
  362. package/neural-interface/public/mcp.svg +0 -4
  363. package/neural-interface/public/opencode-logo-dark-square.svg +0 -1
  364. package/neural-interface/public/pin-svgrepo-com.svg +0 -25
  365. package/neural-interface/public/sessionicon.svg +0 -8
  366. package/neural-interface/public/toolbar-album.svg +0 -8
  367. package/neural-interface/public/toolbar-question.svg +0 -6
  368. package/neural-interface/public/toolbar-sessions.svg +0 -6
  369. package/neural-interface/scripts/codex-protocol-fixture.mjs +0 -83
  370. package/scripts/audit-claude-hook-usage.mjs +0 -12
  371. package/scripts/audit-memory-duplicates.mjs +0 -44
  372. package/scripts/measure-claude-hook-overhead.mjs +0 -77
  373. package/scripts/recover-user-config.mjs +0 -254
  374. package/skills/synabun/synabunicon.png +0 -0
@@ -0,0 +1,1396 @@
1
+ /**
2
+ * SynaBun — the Start Server bridge
3
+ *
4
+ * The offline page cannot ask a server that is down to start itself, so it
5
+ * hands the operating system a synabun://start link. This module owns both
6
+ * ends of that hand-off:
7
+ *
8
+ * registration — what each OS needs to route synabun:// to launcher.mjs
9
+ * (HKCU registry on Windows, ~/.synabun/SynaBun.app on macOS,
10
+ * a .desktop entry on Linux). Written by postinstall.js, by
11
+ * setup.js on every start and by the server at boot, so npm
12
+ * and GitHub installs, new or old, end up the same.
13
+ * launch — what launcher.mjs does when the OS runs it: nothing if the
14
+ * server already answers or a start is under way, otherwise
15
+ * the normal supervisor (setup.js), once.
16
+ *
17
+ * progress — a launch says what it has actually seen happen (itself
18
+ * running, the Terminal asked to open, the supervisor's own
19
+ * phases, the server answering) on a loopback beacon the
20
+ * offline page reads, because that page has no server to ask.
21
+ * A restart is reported the same way, by the supervisor that
22
+ * brings the server back (`openRestartReport`).
23
+ *
24
+ * Nothing here stays running: the OS starts the launcher per click and it
25
+ * exits once the server answers.
26
+ */
27
+
28
+ import { execFileSync, spawn, spawnSync } from 'node:child_process';
29
+ import {
30
+ chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync,
31
+ } from 'node:fs';
32
+ import { createServer } from 'node:http';
33
+ import { createRequire } from 'node:module';
34
+ import { connect } from 'node:net';
35
+ import { homedir, tmpdir } from 'node:os';
36
+ import { basename, dirname, join, posix, win32 } from 'node:path';
37
+ import { randomBytes } from 'node:crypto';
38
+ import { getDataHome, getPlatformDataHome, pathIsInside } from './paths.js';
39
+ import { readPackagedRuntime } from './packaged-runtime.js';
40
+
41
+ export const START_PROTOCOL = 'synabun';
42
+ /** 1 was the handler that ran setup.js directly (no lock, no health check). */
43
+ export const LAUNCHER_REVISION = 2;
44
+ export const DEFAULT_PORT = 3344;
45
+
46
+ const BUNDLE_ID = 'ai.synabun.launcher';
47
+ const LSREGISTER = '/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister';
48
+ const WIN_KEY = 'HKCU\\Software\\Classes\\synabun';
49
+
50
+ /** A start that has not answered after this long no longer blocks a new one. */
51
+ export const LOCK_TTL_MS = 10 * 60 * 1000;
52
+ export const SUPERVISOR_TTL_MS = 15 * 60 * 1000;
53
+ const HEALTH_WAIT_MS = 5 * 60 * 1000;
54
+ /** How often a waiting launch looks: at the port, and at what the supervisor says it is doing. */
55
+ const WAIT_POLL_MS = 200;
56
+
57
+ // ═══════════════════════════════════════════
58
+ // Quoting
59
+ // ═══════════════════════════════════════════
60
+
61
+ /** One POSIX shell word. */
62
+ export function shQuote(value) {
63
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
64
+ }
65
+
66
+ /** One argument of a Windows command line (CommandLineToArgvW rules). */
67
+ export function winQuote(value) {
68
+ let out = '"';
69
+ let slashes = 0;
70
+ for (const ch of String(value)) {
71
+ if (ch === '\\') { slashes++; continue; }
72
+ if (ch === '"') { out += '\\'.repeat(slashes * 2 + 1) + '"'; slashes = 0; continue; }
73
+ out += '\\'.repeat(slashes) + ch;
74
+ slashes = 0;
75
+ }
76
+ return `${out}${'\\'.repeat(slashes * 2)}"`;
77
+ }
78
+
79
+ /**
80
+ * One argument of a .desktop Exec line: quoted per the Exec rules (" ` $ \
81
+ * escaped), then escaped again as a desktop-entry string, with % doubled so
82
+ * it is not read as a field code.
83
+ */
84
+ export function desktopExecArg(value) {
85
+ const quoted = `"${String(value).replace(/(["`$\\])/g, '\\$1')}"`;
86
+ return quoted.replace(/\\/g, '\\\\').replace(/%/g, '%%');
87
+ }
88
+
89
+ function xmlEscape(value) {
90
+ return String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
91
+ }
92
+
93
+ // ═══════════════════════════════════════════
94
+ // Configuration the launcher must agree with the server on
95
+ // ═══════════════════════════════════════════
96
+
97
+ export function normalizePort(value) {
98
+ const n = Number(value);
99
+ return Number.isInteger(n) && n >= 1 && n <= 65535 ? n : null;
100
+ }
101
+
102
+ function readDotEnvValue(dataHome, key) {
103
+ try {
104
+ for (const line of readFileSync(join(dataHome, '.env'), 'utf8').split(/\r?\n/)) {
105
+ const trimmed = line.trim();
106
+ if (!trimmed.startsWith(`${key}=`)) continue;
107
+ return trimmed.slice(key.length + 1).trim().replace(/^(['"])(.*)\1$/, '$2');
108
+ }
109
+ } catch {}
110
+ return null;
111
+ }
112
+
113
+ /** The port the server listens on: the environment, then DATA_HOME/.env, then 3344 — as server.js reads it. */
114
+ export function resolvePort({ explicit = null, env = process.env, dataHome } = {}) {
115
+ return normalizePort(explicit)
116
+ || normalizePort(env.NEURAL_PORT)
117
+ || (dataHome ? normalizePort(readDotEnvValue(dataHome, 'NEURAL_PORT')) : null)
118
+ || DEFAULT_PORT;
119
+ }
120
+
121
+ /**
122
+ * What a registration has to pin because the OS starts the launcher with a
123
+ * bare environment: a data home that is not the platform default, and a port
124
+ * the launcher would not arrive at as 3344 by itself.
125
+ */
126
+ export function launcherOverrides({ env = process.env, home = homedir(), platform = process.platform } = {}) {
127
+ const dataHome = getDataHome({ env, home, os: platform });
128
+ const standard = getPlatformDataHome({ env, home, os: platform });
129
+ // Read the port the way the launcher will, so setup.js (bare environment)
130
+ // and the server (DATA_HOME/.env loaded) write the same registration.
131
+ const port = resolvePort({ env, dataHome });
132
+ // Where a handler with no --port lands: DATA_HOME/.env, then 3344. A server
133
+ // run with NEURAL_PORT=3344 over a .env that names another port has to pin
134
+ // its 3344, or the next click starts (and waits for) the other one.
135
+ const unpinned = resolvePort({ env: {}, dataHome });
136
+ return {
137
+ dataHome: dataHome !== standard ? dataHome : null,
138
+ port: port !== DEFAULT_PORT || unpinned !== DEFAULT_PORT ? port : null,
139
+ };
140
+ }
141
+
142
+ // ═══════════════════════════════════════════
143
+ // Registration
144
+ // ═══════════════════════════════════════════
145
+
146
+ export function launcherLocations({ platform = process.platform, home = homedir() } = {}) {
147
+ if (platform === 'win32') return { kind: 'registry', key: WIN_KEY };
148
+ if (platform === 'darwin') {
149
+ const app = posix.join(home, '.synabun', 'SynaBun.app');
150
+ return {
151
+ kind: 'app',
152
+ app,
153
+ plist: posix.join(app, 'Contents', 'Info.plist'),
154
+ executable: posix.join(app, 'Contents', 'MacOS', 'synabun-launcher'),
155
+ terminalScript: posix.join(app, 'Contents', 'Resources', 'SynaBun Server.command'),
156
+ };
157
+ }
158
+ return { kind: 'desktop', desktopFile: posix.join(home, '.local', 'share', 'applications', 'synabun.desktop') };
159
+ }
160
+
161
+ function launcherArgs({ dataHome, port, terminalScript }) {
162
+ const args = ['--via=protocol'];
163
+ if (dataHome) args.push('--data-home', dataHome);
164
+ if (port) args.push('--port', String(port));
165
+ if (terminalScript) args.push('--terminal-script', terminalScript);
166
+ return args;
167
+ }
168
+
169
+ /**
170
+ * Everything a registration consists of, as data: files to write, registry
171
+ * values to set and the commands that tell the OS about them. No side effects.
172
+ */
173
+ export function buildLauncherPlan({
174
+ platform = process.platform,
175
+ nodePath = process.execPath,
176
+ packageRoot,
177
+ home = homedir(),
178
+ dataHome = null,
179
+ port = null,
180
+ entry = null,
181
+ } = {}) {
182
+ if (!packageRoot) throw new Error('packageRoot is required');
183
+ const where = launcherLocations({ platform, home });
184
+ const plan = { platform, revision: LAUNCHER_REVISION, where, files: [], registry: [], refresh: [] };
185
+ // A packaged application is reached through its entry executable, which
186
+ // brings its own Node: `<entry> launcher ...` and `<entry> start`.
187
+ const handler = (args, launcherPath) => (entry ? [entry, 'launcher', ...args] : [nodePath, launcherPath, ...args]);
188
+
189
+ if (platform === 'win32') {
190
+ const launcher = win32.join(packageRoot, 'launcher.mjs');
191
+ const command = [...handler(launcherArgs({ dataHome, port }), launcher), '--'].map(winQuote).join(' ') + ' "%1"';
192
+ plan.registry.push(
193
+ { key: WIN_KEY, name: null, value: 'URL:SynaBun Protocol' },
194
+ { key: WIN_KEY, name: 'URL Protocol', value: '' },
195
+ { key: `${WIN_KEY}\\shell\\open\\command`, name: null, value: command },
196
+ );
197
+ return plan;
198
+ }
199
+
200
+ const launcher = posix.join(packageRoot, 'launcher.mjs');
201
+ const setup = posix.join(packageRoot, 'setup.js');
202
+
203
+ if (platform === 'darwin') {
204
+ const args = launcherArgs({ dataHome, port, terminalScript: where.terminalScript });
205
+ plan.files.push({
206
+ path: where.plist,
207
+ mode: 0o644,
208
+ content: [
209
+ '<?xml version="1.0" encoding="UTF-8"?>',
210
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
211
+ '<plist version="1.0">',
212
+ '<dict>',
213
+ ` <key>CFBundleIdentifier</key><string>${BUNDLE_ID}</string>`,
214
+ ' <key>CFBundleName</key><string>SynaBun</string>',
215
+ ' <key>CFBundleExecutable</key><string>synabun-launcher</string>',
216
+ ' <key>CFBundlePackageType</key><string>APPL</string>',
217
+ ` <key>CFBundleVersion</key><string>${LAUNCHER_REVISION}.0</string>`,
218
+ // No Dock icon: the launcher is gone again within seconds.
219
+ ' <key>LSUIElement</key><true/>',
220
+ ' <key>CFBundleURLTypes</key>',
221
+ ' <array><dict>',
222
+ ' <key>CFBundleURLName</key><string>SynaBun Protocol</string>',
223
+ ` <key>CFBundleURLSchemes</key><array><string>${xmlEscape(START_PROTOCOL)}</string></array>`,
224
+ ' </dict></array>',
225
+ '</dict>',
226
+ '</plist>',
227
+ '',
228
+ ].join('\n'),
229
+ }, {
230
+ // macOS delivers the link as an Apple Event, which a script never sees:
231
+ // being launched at all is the request to start.
232
+ path: where.executable,
233
+ mode: 0o755,
234
+ content: [
235
+ '#!/bin/sh',
236
+ `# SynaBun start launcher (revision ${LAUNCHER_REVISION}). macOS runs this for ${START_PROTOCOL}:// links.`,
237
+ '# Generated by SynaBun and rewritten when the install moves. Do not edit.',
238
+ ...(entry ? [
239
+ `ENTRY=${shQuote(entry)}`,
240
+ '[ -x "$ENTRY" ] || exit 127',
241
+ `exec "$ENTRY" ${['launcher', ...args].map(shQuote).join(' ')} >/dev/null 2>&1`,
242
+ ] : [
243
+ `NODE=${shQuote(nodePath)}`,
244
+ 'if [ ! -x "$NODE" ]; then',
245
+ ' NODE="$(PATH="$PATH:/opt/homebrew/bin:/usr/local/bin" command -v node 2>/dev/null)"',
246
+ 'fi',
247
+ '[ -n "$NODE" ] || exit 127',
248
+ `exec "$NODE" ${[launcher, ...args].map(shQuote).join(' ')} >/dev/null 2>&1`,
249
+ ]),
250
+ '',
251
+ ].join('\n'),
252
+ }, {
253
+ // Terminal runs this in the user's login shell: the same environment and
254
+ // the same privacy grants as a server they started by hand.
255
+ path: where.terminalScript,
256
+ mode: 0o755,
257
+ content: [
258
+ '#!/bin/sh',
259
+ '# Runs the SynaBun server in this window (opened by the Start Server button).',
260
+ '# Generated by SynaBun and rewritten when the install moves. Do not edit.',
261
+ `printf '\\033]0;SynaBun Server\\007'`,
262
+ ...(entry ? [] : [
263
+ `NODE=${shQuote(nodePath)}`,
264
+ '[ -x "$NODE" ] || NODE=node',
265
+ ]),
266
+ ...(dataHome ? [`export SYNABUN_DATA_HOME=${shQuote(dataHome)}`] : []),
267
+ ...(port ? [`export NEURAL_PORT=${shQuote(String(port))}`] : []),
268
+ 'export SYNABUN_OPEN_BROWSER=0',
269
+ `cd ${shQuote(packageRoot)} || exit 1`,
270
+ entry
271
+ ? `exec ${shQuote(entry)} start`
272
+ : `exec "$NODE" --disable-warning=ExperimentalWarning ${shQuote(setup)}`,
273
+ '',
274
+ ].join('\n'),
275
+ });
276
+ plan.refresh.push({ file: LSREGISTER, args: ['-f', where.app] });
277
+ return plan;
278
+ }
279
+
280
+ const exec = [...handler(launcherArgs({ dataHome, port }), launcher), '--'].map(desktopExecArg).join(' ');
281
+ plan.files.push({
282
+ path: where.desktopFile,
283
+ mode: 0o644,
284
+ content: [
285
+ '[Desktop Entry]',
286
+ 'Type=Application',
287
+ 'Name=SynaBun',
288
+ `Comment=Starts the SynaBun server (${START_PROTOCOL}:// links)`,
289
+ `Exec=${exec} %u`,
290
+ `MimeType=x-scheme-handler/${START_PROTOCOL};`,
291
+ 'NoDisplay=true',
292
+ // The launcher starts the server in the background and exits: no window to keep.
293
+ 'Terminal=false',
294
+ `X-SynaBun-Launcher=${LAUNCHER_REVISION}`,
295
+ '',
296
+ ].join('\n'),
297
+ });
298
+ plan.refresh.push(
299
+ { file: 'xdg-mime', args: ['default', basename(where.desktopFile), `x-scheme-handler/${START_PROTOCOL}`] },
300
+ { file: 'update-desktop-database', args: [dirname(where.desktopFile)] },
301
+ );
302
+ return plan;
303
+ }
304
+
305
+ function regExe(env = process.env) {
306
+ const root = env.SystemRoot || env.SYSTEMROOT || env.windir;
307
+ return root ? win32.join(root, 'System32', 'reg.exe') : 'reg';
308
+ }
309
+
310
+ function defaultRun(file, args, { timeout = 15000 } = {}) {
311
+ return execFileSync(file, args, { encoding: 'utf8', timeout, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
312
+ }
313
+
314
+ /** The current value of one registry entry, or null when it is not there. */
315
+ function readRegistryValue(entry, { run = defaultRun, env = process.env } = {}) {
316
+ try {
317
+ const out = run(regExe(env), ['query', entry.key, ...(entry.name === null ? ['/ve'] : ['/v', entry.name])]);
318
+ for (const line of String(out).split(/\r?\n/)) {
319
+ const at = line.indexOf('REG_SZ');
320
+ if (at < 0) continue;
321
+ return line.slice(at + 'REG_SZ'.length).trim();
322
+ }
323
+ return null;
324
+ } catch {
325
+ return null;
326
+ }
327
+ }
328
+
329
+ function fileMatches(file) {
330
+ try {
331
+ if (readFileSync(file.path, 'utf8') !== file.content) return false;
332
+ // A launcher that lost its executable bit is as good as missing.
333
+ return !(file.mode & 0o111) || (statSync(file.path).mode & 0o111) !== 0;
334
+ } catch {
335
+ return false;
336
+ }
337
+ }
338
+
339
+ /** 'registered' | 'stale' | 'missing', without changing anything. */
340
+ export function inspectLauncher(plan, { run = defaultRun, env = process.env } = {}) {
341
+ let present = 0;
342
+ let current = 0;
343
+ for (const file of plan.files) {
344
+ if (existsSync(file.path)) present++;
345
+ if (fileMatches(file)) current++;
346
+ }
347
+ for (const entry of plan.registry) {
348
+ const value = readRegistryValue(entry, { run, env });
349
+ if (value !== null) present++;
350
+ if (value === entry.value) current++;
351
+ }
352
+ const total = plan.files.length + plan.registry.length;
353
+ if (current === total) return 'registered';
354
+ return present ? 'stale' : 'missing';
355
+ }
356
+
357
+ /**
358
+ * Bring the OS in line with a plan. Writes only what differs, so calling it on
359
+ * every start costs a few file reads. Returns { ok, changed, state, errors }.
360
+ */
361
+ export function applyLauncherPlan(plan, { run = defaultRun, env = process.env } = {}) {
362
+ const errors = [];
363
+ let changed = 0;
364
+
365
+ for (const file of plan.files) {
366
+ if (fileMatches(file)) continue;
367
+ try {
368
+ mkdirSync(dirname(file.path), { recursive: true });
369
+ const temp = `${file.path}.${process.pid}.tmp`;
370
+ writeFileSync(temp, file.content, { encoding: 'utf8', mode: file.mode });
371
+ chmodSync(temp, file.mode);
372
+ renameSync(temp, file.path);
373
+ changed++;
374
+ } catch (error) {
375
+ errors.push(`${file.path}: ${error.message}`);
376
+ }
377
+ }
378
+
379
+ for (const entry of plan.registry) {
380
+ if (readRegistryValue(entry, { run, env }) === entry.value) continue;
381
+ try {
382
+ run(regExe(env), ['add', entry.key, ...(entry.name === null ? ['/ve'] : ['/v', entry.name]), '/t', 'REG_SZ', '/d', entry.value, '/f']);
383
+ changed++;
384
+ } catch (error) {
385
+ errors.push(`${entry.key}: ${error.message}`);
386
+ }
387
+ }
388
+
389
+ // The OS caches its handler table: tell it only when something moved. These
390
+ // are best effort (a minimal Linux has neither tool and still resolves the
391
+ // entry through mimeinfo on most desktops).
392
+ if (changed) {
393
+ for (const command of plan.refresh) {
394
+ try { run(command.file, command.args); } catch {}
395
+ }
396
+ }
397
+
398
+ const state = errors.length ? inspectLauncher(plan, { run, env }) : 'registered';
399
+ return { ok: errors.length === 0, changed, state, errors };
400
+ }
401
+
402
+ /**
403
+ * Whether this process may touch the user's registration. A test run or a
404
+ * throwaway data home must never repoint the real handler at itself.
405
+ */
406
+ export function launcherRegistrationAllowed({ env = process.env, dataHome = null } = {}) {
407
+ if (env.SYNABUN_LAUNCHER_REGISTER === '0') return false;
408
+ if (env.NODE_TEST_CONTEXT || env.VITEST) return false;
409
+ if (dataHome) {
410
+ try { if (pathIsInside(dataHome, tmpdir())) return false; } catch {}
411
+ }
412
+ return true;
413
+ }
414
+
415
+ /** The entry executable of a packaged application, or null for an npm or Git install. */
416
+ function packagedEntry({ packageRoot, env }) {
417
+ return packageRoot ? readPackagedRuntime({ packageRoot, env })?.entry ?? null : null;
418
+ }
419
+
420
+ /** Register (or repair) the synabun:// handler for this install. Never throws. */
421
+ export function registerStartLauncher({
422
+ platform = process.platform,
423
+ nodePath = process.execPath,
424
+ packageRoot,
425
+ home = homedir(),
426
+ env = process.env,
427
+ run = defaultRun,
428
+ } = {}) {
429
+ try {
430
+ const overrides = launcherOverrides({ env, home, platform });
431
+ if (!launcherRegistrationAllowed({ env, dataHome: overrides.dataHome })) {
432
+ return { ok: true, changed: 0, state: 'skipped', errors: [] };
433
+ }
434
+ const plan = buildLauncherPlan({ platform, nodePath, packageRoot, home, entry: packagedEntry({ packageRoot, env }), ...overrides });
435
+ return applyLauncherPlan(plan, { run, env });
436
+ } catch (error) {
437
+ return { ok: false, changed: 0, state: 'missing', errors: [error.message] };
438
+ }
439
+ }
440
+
441
+ /** Read-only: is this install's handler in place? */
442
+ export function startLauncherState({
443
+ platform = process.platform,
444
+ nodePath = process.execPath,
445
+ packageRoot,
446
+ home = homedir(),
447
+ env = process.env,
448
+ run = defaultRun,
449
+ } = {}) {
450
+ try {
451
+ const plan = buildLauncherPlan({ platform, nodePath, packageRoot, home, entry: packagedEntry({ packageRoot, env }), ...launcherOverrides({ env, home, platform }) });
452
+ return inspectLauncher(plan, { run, env });
453
+ } catch {
454
+ return 'missing';
455
+ }
456
+ }
457
+
458
+ // ═══════════════════════════════════════════
459
+ // Launch
460
+ // ═══════════════════════════════════════════
461
+
462
+ /**
463
+ * Read the launcher's arguments. Only `start` is an action: a link that asks
464
+ * for anything else is ignored, and nothing from the link is ever passed on.
465
+ */
466
+ export function parseLauncherArgs(argv = []) {
467
+ const out = { action: 'start', via: 'manual', dataHome: null, port: null, terminalScript: null, link: null };
468
+ let afterDashes = false;
469
+ for (let i = 0; i < argv.length; i++) {
470
+ const arg = String(argv[i] ?? '');
471
+ if (new RegExp(`^${START_PROTOCOL}:`, 'i').test(arg)) {
472
+ out.link = arg;
473
+ out.action = parseStartLink(arg);
474
+ continue;
475
+ }
476
+ if (afterDashes) continue; // whatever else follows `--` came from the link
477
+ if (arg === '--') { afterDashes = true; continue; }
478
+ if (arg === '--via=protocol') out.via = 'protocol';
479
+ else if (arg === '--data-home') out.dataHome = argv[++i] || null;
480
+ else if (arg === '--port') out.port = normalizePort(argv[++i]);
481
+ else if (arg === '--terminal-script') out.terminalScript = argv[++i] || null;
482
+ }
483
+ return out;
484
+ }
485
+
486
+ /** 'start' for synabun://start (any query), otherwise 'ignore'. */
487
+ export function parseStartLink(link) {
488
+ const match = new RegExp(`^${START_PROTOCOL}:(?://)?([a-z-]*)/?(?:[?#].*)?$`, 'i').exec(String(link || '').trim());
489
+ return match && match[1].toLowerCase() === 'start' ? 'start' : 'ignore';
490
+ }
491
+
492
+ export function pidAlive(pid) {
493
+ if (!Number.isInteger(pid) || pid <= 0) return false;
494
+ try { process.kill(pid, 0); return true; } catch (error) { return error?.code === 'EPERM'; }
495
+ }
496
+
497
+ function readJson(path) {
498
+ try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
499
+ }
500
+
501
+ /** What a click should do, given what is already there. */
502
+ export function decideLaunch({ healthy, lock = null, supervisor = null, now = Date.now(), alive = pidAlive } = {}) {
503
+ if (healthy) return 'already-running';
504
+ if (lock && alive(lock.pid) && now - Number(lock.startedAt || 0) < LOCK_TTL_MS) return 'already-starting';
505
+ // A supervisor only lives while its server does. One that has been alive
506
+ // this long without a server answering is a reused pid, not a start; one
507
+ // that wrote down its own end is over, whoever has its pid now.
508
+ if (supervisor && !supervisor.endedAt && alive(supervisor.pid) && now - Number(supervisor.startedAt || 0) < SUPERVISOR_TTL_MS) return 'already-starting';
509
+ return 'start';
510
+ }
511
+
512
+ export function launcherStatePaths(dataHome) {
513
+ const dir = join(dataHome, 'data');
514
+ const lock = join(dir, 'start-launcher.lock');
515
+ return {
516
+ dir,
517
+ lock,
518
+ mutex: launchMutexPath(lock),
519
+ supervisor: join(dir, 'supervisor.json'),
520
+ log: join(dir, 'launcher.log'),
521
+ };
522
+ }
523
+
524
+ /**
525
+ * The mutex that goes with a lock file: DATA_HOME/runtime/start-launcher.mutex
526
+ * for DATA_HOME/data/start-launcher.lock. Derived from the lock's own path, so
527
+ * everyone after one lock is behind one mutex, and kept out of data/: an
528
+ * operating-system lock is not data, and a backup that copied it or a restore
529
+ * that replaced it would split it in two.
530
+ */
531
+ export function launchMutexPath(lockPath) {
532
+ return join(dirname(dirname(lockPath)), 'runtime', `${basename(lockPath, '.lock')}.mutex`);
533
+ }
534
+
535
+ /** What a supervisor says it is doing. Each one is written when it begins, never ahead of it. */
536
+ export const SUPERVISOR_PHASES = Object.freeze([
537
+ 'checking', // looking at the install: nothing slow has begun
538
+ 'dependencies', // npm install is running (a first start, or the first after an update)
539
+ 'browser', // downloading the automation browser
540
+ 'build', // compiling the MCP server
541
+ 'snapshot', // backing the data home up before the first start of a new version
542
+ 'server', // the server process was spawned
543
+ 'listening', // the server printed its address
544
+ 'failed', // it ended without a server; `error` says why
545
+ ]);
546
+
547
+ function supervisorError(error) {
548
+ const exitCode = Number(error?.exitCode);
549
+ return {
550
+ code: String(error?.code || 'SETUP_FAILED').slice(0, 40),
551
+ message: String(error?.message || error || 'the start failed').slice(0, 300),
552
+ ...(Number.isInteger(exitCode) ? { exitCode } : {}),
553
+ };
554
+ }
555
+
556
+ function writeRecord(path, record, pid) {
557
+ const text = JSON.stringify(record) + '\n';
558
+ const temp = `${path}.${pid}.tmp`;
559
+ try {
560
+ // Replaced whole: a launcher reading it mid-start never sees half a record.
561
+ writeFileSync(temp, text, 'utf8');
562
+ renameSync(temp, path);
563
+ } catch {
564
+ try { writeFileSync(path, text, 'utf8'); } catch {}
565
+ try { rmSync(temp, { force: true }); } catch {}
566
+ }
567
+ }
568
+
569
+ /**
570
+ * Called by setup.js: say that a supervisor owns the server from now on, and
571
+ * what it is doing. A second click reads the record and starts nothing; the
572
+ * launcher that started this supervisor passes its phases on to the page.
573
+ * Returns { phase(name, detail), fail(error), release() }: `release` (on
574
+ * exit) removes the record unless it says why the start failed.
575
+ */
576
+ export function openSupervisorRecord(dataHome, { pid = process.pid, now = () => Date.now() } = {}) {
577
+ const { dir, supervisor } = launcherStatePaths(dataHome);
578
+ const startedAt = now();
579
+ const record = { pid, startedAt, phase: 'checking', phaseAt: startedAt };
580
+ let ended = false;
581
+ const write = () => {
582
+ try { mkdirSync(dir, { recursive: true }); } catch {}
583
+ writeRecord(supervisor, record, pid);
584
+ };
585
+ write();
586
+ return {
587
+ phase(name, detail = null) {
588
+ if (ended || !SUPERVISOR_PHASES.includes(name) || name === 'failed') return;
589
+ record.phase = name;
590
+ record.phaseAt = now();
591
+ if (detail && typeof detail === 'object') record.detail = detail; else delete record.detail;
592
+ write();
593
+ },
594
+ fail(error) {
595
+ if (ended) return;
596
+ ended = true;
597
+ record.phase = 'failed';
598
+ record.phaseAt = record.endedAt = now();
599
+ record.error = supervisorError(error);
600
+ delete record.detail;
601
+ write();
602
+ },
603
+ release() {
604
+ // A record of why it failed stays for the launcher and the next click to
605
+ // read; `endedAt` keeps it from ever counting as a start under way.
606
+ if (ended) return;
607
+ try { if (readJson(supervisor)?.pid === pid) rmSync(supervisor, { force: true }); } catch {}
608
+ },
609
+ };
610
+ }
611
+
612
+ /**
613
+ * A start that ended before it could open its record (two data homes that
614
+ * disagree, say). Written only into a data directory that is already there:
615
+ * a failed start creates nothing.
616
+ */
617
+ export function noteSupervisorFailure(dataHome, error, { pid = process.pid, now = Date.now() } = {}) {
618
+ const { dir, supervisor } = launcherStatePaths(dataHome);
619
+ if (!existsSync(dir)) return false;
620
+ writeRecord(supervisor, { pid, startedAt: now, phase: 'failed', phaseAt: now, endedAt: now, error: supervisorError(error) }, pid);
621
+ return true;
622
+ }
623
+
624
+ const MUTEX_TIMEOUT_MS = 2000;
625
+ const SQLITE_BUSY = 5;
626
+
627
+ /**
628
+ * node:sqlite, loaded on first use: postinstall.js and setup.js import this
629
+ * module on any Node, and only a launch needs it. The server runs with
630
+ * --disable-warning=ExperimentalWarning; the OS starts the launcher without
631
+ * it, so the one line node prints for this module is kept out of its console.
632
+ */
633
+ function loadSqlite() {
634
+ const emitWarning = process.emitWarning;
635
+ process.emitWarning = function (warning, ...rest) {
636
+ if (/SQLite is an experimental feature/i.test(String(warning?.message ?? warning))) return undefined;
637
+ return emitWarning.call(this, warning, ...rest);
638
+ };
639
+ try { return createRequire(import.meta.url)('node:sqlite'); } finally { process.emitWarning = emitWarning; }
640
+ }
641
+
642
+ /**
643
+ * Run `fn` as the only process inside, among everyone who uses `path`. The
644
+ * mutex is a write transaction (BEGIN IMMEDIATE) on an empty SQLite database,
645
+ * like the rules installer's lock (neural-interface/lib/rulesets/lock.js):
646
+ * the operating system holds it and gives it back when the process ends,
647
+ * however it ends. Nothing is ever written to the file, there is no stale
648
+ * state to clean up, and so nothing here has to judge or remove someone
649
+ * else's claim. Returns { ran, value }; `ran` is false when another process
650
+ * stayed inside for `timeoutMs`. Throws when the mutex cannot be used at all.
651
+ */
652
+ function withLaunchMutex(path, fn, { timeoutMs = MUTEX_TIMEOUT_MS } = {}) {
653
+ const { DatabaseSync } = loadSqlite();
654
+ mkdirSync(dirname(path), { recursive: true });
655
+ // Which file the path leads to, by stat alone (never a second descriptor on it).
656
+ const identity = () => {
657
+ try { const info = statSync(path, { bigint: true }); return `${info.dev}:${info.ino}`; } catch { return null; }
658
+ };
659
+ for (let attempt = 0; attempt < 2; attempt++) {
660
+ const before = identity();
661
+ const db = new DatabaseSync(path);
662
+ try {
663
+ db.exec(`PRAGMA busy_timeout=${Math.max(0, Math.floor(Number(timeoutMs) || 0))}`);
664
+ const opened = identity();
665
+ try {
666
+ db.exec('BEGIN IMMEDIATE');
667
+ } catch (error) {
668
+ const busy = (Number(error?.errcode) & 0xff) === SQLITE_BUSY || /database is locked/i.test(String(error?.errstr || error?.message || ''));
669
+ if (busy) return { ran: false };
670
+ throw error;
671
+ }
672
+ try {
673
+ // Deleted or replaced while it was being taken: this transaction is on
674
+ // a file nobody else looks at. Give it back and take the one there now.
675
+ if (opened === null || (before !== null && before !== opened) || identity() !== opened) continue;
676
+ return { ran: true, value: fn() };
677
+ } finally {
678
+ try { db.exec('ROLLBACK'); } catch {}
679
+ }
680
+ } finally {
681
+ try { db.close(); } catch {}
682
+ }
683
+ }
684
+ throw new Error(`its mutex (${path}) was replaced while it was being taken`);
685
+ }
686
+
687
+ /**
688
+ * Take the launch lock, or return null when a live launch holds it. Reading
689
+ * the record, judging it dead and replacing it all happen inside the launch
690
+ * mutex, and so does the release: two launchers that found the same dead lock
691
+ * cannot remove each other's fresh one, and a late release cannot remove a
692
+ * lock taken after it. Throws (code LAUNCH_LOCK_UNAVAILABLE) when the lock
693
+ * cannot be taken at all; the caller then starts nothing.
694
+ */
695
+ export function acquireLaunchLock(lockPath, {
696
+ pid = process.pid, now = Date.now(), alive = pidAlive, port = null, mutexTimeoutMs = MUTEX_TIMEOUT_MS,
697
+ } = {}) {
698
+ const mutexPath = launchMutexPath(lockPath);
699
+ const id = randomBytes(8).toString('hex');
700
+ const record = JSON.stringify({ pid, startedAt: now, port, id }) + '\n';
701
+ let taken;
702
+ try {
703
+ taken = withLaunchMutex(mutexPath, () => {
704
+ const held = readJson(lockPath);
705
+ if (held && alive(held.pid) && now - Number(held.startedAt || 0) < LOCK_TTL_MS) return false;
706
+ // There and unreadable, a moment old: nobody else is writing it now (this
707
+ // is the mutex), so it is what a launcher that died mid-write left, or
708
+ // not ours at all. It gets a few seconds before it counts as debris.
709
+ if (!held) {
710
+ try { if (Date.now() - statSync(lockPath).mtimeMs < 5000) return false; } catch {}
711
+ }
712
+ // Written in place: a reader outside the mutex (decideLaunch) may catch
713
+ // it half written, reads nothing, and then waits here like everyone else.
714
+ mkdirSync(dirname(lockPath), { recursive: true });
715
+ writeFileSync(lockPath, record, 'utf8');
716
+ return true;
717
+ }, { timeoutMs: mutexTimeoutMs });
718
+ } catch (cause) {
719
+ throw Object.assign(new Error(`the launch lock could not be taken (${lockPath}): ${cause?.message || cause}`), { code: 'LAUNCH_LOCK_UNAVAILABLE', cause });
720
+ }
721
+ // Someone else inside the mutex for that long is a launch at work, too.
722
+ if (!taken.ran || !taken.value) return null;
723
+ return {
724
+ release() {
725
+ try {
726
+ withLaunchMutex(mutexPath, () => {
727
+ if (readJson(lockPath)?.id === id) rmSync(lockPath, { force: true });
728
+ }, { timeoutMs: mutexTimeoutMs });
729
+ } catch {}
730
+ },
731
+ };
732
+ }
733
+
734
+ /** True when something accepts a TCP connection on the port. */
735
+ export function portAccepts(port, { timeoutMs = 400, host = '127.0.0.1', connectImpl = connect } = {}) {
736
+ return new Promise((done) => {
737
+ let settled = false;
738
+ let socket = null;
739
+ const finish = (value) => {
740
+ if (settled) return;
741
+ settled = true;
742
+ try { socket?.destroy(); } catch {}
743
+ done(value);
744
+ };
745
+ try {
746
+ socket = connectImpl({ port, host });
747
+ socket.setTimeout?.(timeoutMs, () => finish(false));
748
+ socket.once('connect', () => finish(true));
749
+ socket.once('error', () => finish(false));
750
+ } catch {
751
+ finish(false);
752
+ }
753
+ });
754
+ }
755
+
756
+ /**
757
+ * True when a server holds the port: something answers HTTP there (a starting
758
+ * server answers before it is ready), or, when no answer came, something still
759
+ * accepts connections. A server too busy to answer in time is a server all the
760
+ * same, and a second one could not take its port.
761
+ */
762
+ export async function probeServer(port, { timeoutMs = 1500, fetchImpl = globalThis.fetch, accepts = portAccepts } = {}) {
763
+ try {
764
+ const response = await fetchImpl(`http://127.0.0.1:${port}/api/health`, {
765
+ signal: AbortSignal.timeout(timeoutMs),
766
+ headers: { accept: 'application/json' },
767
+ });
768
+ try { await response.body?.cancel(); } catch {}
769
+ return true;
770
+ } catch {
771
+ try { return (await accepts(port)) === true; } catch { return false; }
772
+ }
773
+ }
774
+
775
+ // ═══════════════════════════════════════════
776
+ // Progress: the start beacon
777
+ // ═══════════════════════════════════════════
778
+
779
+ export const BEACON_VERSION = 1;
780
+ export const BEACON_PATH = '/synabun-start-status';
781
+ /** How long a launch that failed keeps saying so, for a page that has not read it yet. */
782
+ export const BEACON_LINGER_MS = 20 * 1000;
783
+
784
+ /**
785
+ * Where a launch reports while the server is down: the server's port + 10000
786
+ * on the loopback address (3344 → 13344), − 10000 when that is past the last
787
+ * port. The page derives it the same way (`startBeaconPort` in
788
+ * neural-interface/public/shared/start-bridge.js): the two must agree.
789
+ */
790
+ export function startBeaconPort(port) {
791
+ const p = normalizePort(port) || DEFAULT_PORT;
792
+ return p + 10000 <= 65535 ? p + 10000 : p - 10000;
793
+ }
794
+
795
+ const DETAIL_NUMBERS = ['files', 'bytes', 'skippedFiles', 'port', 'pid', 'exitCode'];
796
+ const DETAIL_STRINGS = ['name', 'step', 'version'];
797
+
798
+ /** What of a supervisor record goes out on the beacon: known fields, bounded, nothing else from the file. */
799
+ export function publicSupervisorState(record) {
800
+ if (!record || typeof record !== 'object' || !SUPERVISOR_PHASES.includes(record.phase)) return null;
801
+ const out = {
802
+ phase: record.phase,
803
+ at: Number(record.phaseAt) || Number(record.startedAt) || 0,
804
+ startedAt: Number(record.startedAt) || 0,
805
+ };
806
+ if (record.detail && typeof record.detail === 'object') {
807
+ const detail = {};
808
+ for (const key of DETAIL_NUMBERS) if (Number.isFinite(Number(record.detail[key])) && record.detail[key] !== null) detail[key] = Number(record.detail[key]);
809
+ for (const key of DETAIL_STRINGS) if (typeof record.detail[key] === 'string') detail[key] = record.detail[key].slice(0, 80);
810
+ if (Object.keys(detail).length) out.detail = detail;
811
+ }
812
+ if (record.phase === 'failed') out.error = supervisorError(record.error);
813
+ return out;
814
+ }
815
+
816
+ /**
817
+ * The beacon of one launch: a loopback-only HTTP listener that answers one
818
+ * read-only question, "what has this launch seen happen so far?".
819
+ *
820
+ * step(id) — something the launcher itself did or saw, with its time
821
+ * supervisor(rec) — the supervisor's own record, as it stands
822
+ * finish(state, e) — 'started' | 'failed' | 'gave-up'
823
+ *
824
+ * It answers only its own host name (a page elsewhere cannot reach it by
825
+ * rebinding a name), gives the answer only to a page of this server's own
826
+ * loopback origin, and changes nothing. A port it cannot have is not an
827
+ * error: the launch goes on unreported and the page says less.
828
+ *
829
+ * `reason: 'restart'` is a supervisor bringing its server back, not a launcher
830
+ * starting one: the answer then says so, and its first step is 'restart'.
831
+ */
832
+ export function createStartBeacon({
833
+ port,
834
+ startedAt = Date.now(),
835
+ now = () => Date.now(),
836
+ launchId = randomBytes(8).toString('hex'),
837
+ logPath = null,
838
+ reason = 'start',
839
+ createServerImpl = createServer,
840
+ } = {}) {
841
+ const restart = reason === 'restart';
842
+ const beaconPort = startBeaconPort(port);
843
+ const hosts = new Set([`127.0.0.1:${beaconPort}`, `localhost:${beaconPort}`]);
844
+ const origins = new Set([`http://localhost:${port}`, `http://127.0.0.1:${port}`, `http://[::1]:${port}`]);
845
+ const status = { state: 'starting', steps: [{ id: restart ? 'restart' : 'received', at: startedAt }], supervisor: null, error: null };
846
+ let server = null;
847
+ let listening = false;
848
+ let closed = false;
849
+ let retryTimer = null;
850
+ let finalReads = 0;
851
+ let onFinalRead = null;
852
+ // Each phase the supervisor was seen in, with the time it gave for it. One
853
+ // that came and went between two looks is not here: this is what was seen.
854
+ const phases = [];
855
+
856
+ const snapshot = () => ({
857
+ synabun: 'start-beacon',
858
+ v: BEACON_VERSION,
859
+ port,
860
+ launchId,
861
+ startedAt,
862
+ now: now(),
863
+ state: status.state,
864
+ steps: status.steps,
865
+ supervisor: status.supervisor,
866
+ error: status.error,
867
+ log: logPath,
868
+ ...(restart ? { reason: 'restart' } : {}),
869
+ });
870
+
871
+ function handle(req, res) {
872
+ const deny = (code) => { res.writeHead(code, { 'Content-Length': '0', 'Cache-Control': 'no-store' }); res.end(); };
873
+ try {
874
+ if (!hosts.has(String(req.headers.host || '').toLowerCase())) return deny(403);
875
+ const origin = req.headers.origin;
876
+ if (origin !== undefined && !origins.has(origin)) return deny(403);
877
+ if (String(req.url || '').split('?')[0] !== BEACON_PATH) return deny(404);
878
+ const cors = origin ? { 'Access-Control-Allow-Origin': origin, Vary: 'Origin' } : {};
879
+ if (req.method === 'OPTIONS') {
880
+ res.writeHead(204, {
881
+ ...cors,
882
+ 'Access-Control-Allow-Methods': 'GET',
883
+ 'Access-Control-Allow-Private-Network': 'true',
884
+ 'Access-Control-Max-Age': '600',
885
+ 'Content-Length': '0',
886
+ });
887
+ return res.end();
888
+ }
889
+ if (req.method !== 'GET') return deny(405);
890
+ const body = JSON.stringify(snapshot());
891
+ res.writeHead(200, {
892
+ ...cors,
893
+ 'Content-Type': 'application/json; charset=utf-8',
894
+ 'Content-Length': String(Buffer.byteLength(body)),
895
+ 'Cache-Control': 'no-store',
896
+ 'X-Content-Type-Options': 'nosniff',
897
+ });
898
+ res.end(body);
899
+ if (status.state !== 'starting' && origin) {
900
+ finalReads++;
901
+ onFinalRead?.();
902
+ }
903
+ } catch {
904
+ try { deny(500); } catch {}
905
+ }
906
+ }
907
+
908
+ /** Try to take the port; `retryMs` keeps trying while an earlier launch still holds it. Resolves true | false. */
909
+ function listen({ retryMs = 0, retryEveryMs = 400 } = {}) {
910
+ const deadline = now() + retryMs;
911
+ return new Promise((done) => {
912
+ const attempt = () => {
913
+ retryTimer = null;
914
+ if (closed) return done(false);
915
+ let candidate;
916
+ try {
917
+ candidate = createServerImpl(handle);
918
+ } catch {
919
+ return done(false);
920
+ }
921
+ let bound = false;
922
+ // Kept for the listener's whole life: an error with nobody listening for
923
+ // it would end the launcher, and the beacon is never worth that.
924
+ candidate.on('error', () => {
925
+ if (bound) return;
926
+ try { candidate.close(); } catch {}
927
+ if (closed || now() >= deadline) return done(false);
928
+ retryTimer = setTimeout(attempt, retryEveryMs);
929
+ retryTimer.unref?.();
930
+ });
931
+ candidate.once('listening', () => {
932
+ bound = true;
933
+ if (closed) { try { candidate.close(); } catch {} return done(false); }
934
+ server = candidate;
935
+ listening = true;
936
+ // A slow or silent client cannot hold the launcher open.
937
+ server.headersTimeout = 5000;
938
+ server.requestTimeout = 5000;
939
+ server.keepAliveTimeout = 1000;
940
+ done(true);
941
+ });
942
+ candidate.unref?.();
943
+ candidate.listen({ port: beaconPort, host: '127.0.0.1', exclusive: true });
944
+ };
945
+ attempt();
946
+ });
947
+ }
948
+
949
+ return {
950
+ port: beaconPort,
951
+ launchId,
952
+ get listening() { return listening; },
953
+ get state() { return status.state; },
954
+ snapshot,
955
+ listen,
956
+ step(id, extra = {}) {
957
+ if (status.state !== 'starting' || status.steps.some(s => s.id === id)) return;
958
+ status.steps.push({ id: String(id), at: now(), ...extra });
959
+ },
960
+ supervisor(record) {
961
+ if (status.state !== 'starting') return;
962
+ const next = publicSupervisorState(record);
963
+ if (!next) return;
964
+ if (phases[phases.length - 1]?.phase !== next.phase) phases.push({ phase: next.phase, at: next.at });
965
+ status.supervisor = { ...next, phases: phases.slice(-12) };
966
+ },
967
+ finish(state, error = null) {
968
+ if (status.state !== 'starting') return;
969
+ status.state = state;
970
+ status.error = error ? supervisorError(error) : null;
971
+ status.steps.push({ id: state, at: now() });
972
+ },
973
+ /**
974
+ * Keep answering until a page has read how it ended (and a moment more,
975
+ * for a second tab), `ms` at the most. A beacon nobody can reach is over at once.
976
+ */
977
+ linger(ms, { afterReadMs = 1500 } = {}) {
978
+ if (!listening || closed || !(ms > 0)) return Promise.resolve();
979
+ return new Promise((done) => {
980
+ let timer = setTimeout(done, ms);
981
+ const soon = () => {
982
+ onFinalRead = null;
983
+ clearTimeout(timer);
984
+ timer = setTimeout(done, afterReadMs);
985
+ };
986
+ if (finalReads > 0) soon(); else onFinalRead = soon;
987
+ });
988
+ },
989
+ close() {
990
+ closed = true;
991
+ listening = false;
992
+ if (retryTimer) clearTimeout(retryTimer);
993
+ retryTimer = null;
994
+ onFinalRead = null;
995
+ try { server?.close(); } catch {}
996
+ try { server?.closeAllConnections?.(); } catch {}
997
+ server = null;
998
+ },
999
+ };
1000
+ }
1001
+
1002
+ // ═══════════════════════════════════════════
1003
+ // Progress: a restart
1004
+ // ═══════════════════════════════════════════
1005
+
1006
+ /** Set by a server that had no supervisor for the one it starts to restart it: that start is a restart's. */
1007
+ export const RESTART_REPORT_ENV = 'SYNABUN_RESTART_REPORT';
1008
+ /** How long a restart's report stays once the server said it is listening: a page polls every 300 ms. */
1009
+ export const RESTART_REPORT_SETTLE_MS = 3000;
1010
+
1011
+ /**
1012
+ * A restart reports itself the way a launch does, on the start beacon, for as
1013
+ * long as the server is away: the page that asked has no server to ask. Opened
1014
+ * by the supervisor (setup.js) when the server it runs exits to be restarted
1015
+ * (`exited: true`: it saw that process end), and by a supervisor that a server
1016
+ * without one started in its place.
1017
+ *
1018
+ * phase() — the supervisor's record changed: pass on what it says now
1019
+ * failed(error) — it ended without a server; resolves once a page has read
1020
+ * why (`lingerMs` at the most), so the caller can exit then
1021
+ * close() — over
1022
+ *
1023
+ * Only what the supervisor writes after the report began goes out: a record
1024
+ * from before it (the server that just stopped was 'listening') is not news.
1025
+ * The report ends by itself a moment after the server says it is listening.
1026
+ * Never throws; a beacon it cannot have leaves the restart unreported.
1027
+ */
1028
+ export function openRestartReport({
1029
+ port,
1030
+ dataHome,
1031
+ exited = false,
1032
+ now = () => Date.now(),
1033
+ settleMs = RESTART_REPORT_SETTLE_MS,
1034
+ lingerMs = BEACON_LINGER_MS,
1035
+ beacon = createStartBeacon,
1036
+ } = {}) {
1037
+ const startedAt = now();
1038
+ let report = null;
1039
+ try { report = beacon({ port, startedAt, now, reason: 'restart' }); } catch {}
1040
+ let over = !report;
1041
+ let settle = null;
1042
+ const close = () => {
1043
+ if (over) return;
1044
+ over = true;
1045
+ if (settle) clearTimeout(settle);
1046
+ settle = null;
1047
+ try { report.close(); } catch {}
1048
+ };
1049
+ // An earlier report (a restart right before this one) may still be letting go.
1050
+ let listening = Promise.resolve(false);
1051
+ if (report) {
1052
+ try { listening = Promise.resolve(report.listen({ retryMs: 5000 })).catch(() => false); } catch {}
1053
+ if (exited) report.step('exited');
1054
+ }
1055
+ return {
1056
+ get open() { return !over; },
1057
+ phase() {
1058
+ if (over) return;
1059
+ let record = null;
1060
+ try { record = readJson(launcherStatePaths(dataHome).supervisor); } catch {}
1061
+ if (!record || !(Number(record.phaseAt) >= startedAt)) return;
1062
+ report.supervisor(record);
1063
+ if (record.phase === 'listening' && !settle) {
1064
+ settle = setTimeout(close, settleMs);
1065
+ settle.unref?.();
1066
+ }
1067
+ },
1068
+ async failed(error) {
1069
+ if (over) return;
1070
+ if (settle) clearTimeout(settle);
1071
+ settle = null;
1072
+ report.finish('failed', error);
1073
+ await listening; // a failure right after it opened: let it be reachable first
1074
+ try { await report.linger(lingerMs); } catch {}
1075
+ close();
1076
+ },
1077
+ close,
1078
+ };
1079
+ }
1080
+
1081
+ const LOGIN_ENV_SHELLS = new Set(['zsh', 'bash', 'sh', 'dash', 'ksh', 'mksh', 'yash', 'fish']);
1082
+ /** What a launch decides for itself. A profile may export its own; the launch's values stand. */
1083
+ const LAUNCH_SETTINGS = ['SYNABUN_DATA_HOME', 'NEURAL_PORT', 'SYNABUN_OPEN_BROWSER'];
1084
+
1085
+ /**
1086
+ * The environment a terminal would give the server. A desktop starts the
1087
+ * launcher with a bare one (no nvm, no Homebrew, no exported keys), so ask the
1088
+ * user's own shell, bounded, and fall back to a PATH that at least finds node.
1089
+ * The shell's answer goes over `env`, except for the launch's own settings:
1090
+ * the server has to listen where the launcher probes and keep its state where
1091
+ * the launcher's lock is.
1092
+ */
1093
+ export function resolveLaunchEnv({
1094
+ env = process.env,
1095
+ nodePath = process.execPath,
1096
+ platform = process.platform,
1097
+ home = homedir(),
1098
+ timeoutMs = 5000,
1099
+ runShell = spawnSync,
1100
+ } = {}) {
1101
+ // Windows hands a protocol handler the user's full environment already.
1102
+ if (platform === 'win32') return { ...env };
1103
+ const nodeDir = dirname(nodePath);
1104
+ // node first, so npm / npx (and CLIs installed next to it) are the same install.
1105
+ const withNode = (base) => {
1106
+ const parts = String(base.PATH || '').split(':').filter(Boolean);
1107
+ const extra = ['/usr/local/bin', '/opt/homebrew/bin', posix.join(home, '.local', 'bin'), '/usr/bin', '/bin'];
1108
+ const path = [nodeDir, ...parts, ...extra].filter((p, i, all) => all.indexOf(p) === i).join(':');
1109
+ return { ...base, PATH: path };
1110
+ };
1111
+
1112
+ const shell = env.SHELL || '';
1113
+ if (!LOGIN_ENV_SHELLS.has(basename(shell))) return withNode(env);
1114
+ try {
1115
+ const marker = `__SYNABUN_ENV_${randomBytes(8).toString('hex')}__`;
1116
+ const script = `process.stdout.write(${JSON.stringify(marker)}+JSON.stringify(process.env)+${JSON.stringify(marker)})`;
1117
+ const result = runShell(shell, ['-ilc', `${shQuote(nodePath)} -e ${shQuote(script)}`], {
1118
+ encoding: 'utf8',
1119
+ timeout: timeoutMs,
1120
+ stdio: ['ignore', 'pipe', 'ignore'],
1121
+ env: { ...env, HOME: env.HOME || home },
1122
+ });
1123
+ const out = String(result?.stdout || '');
1124
+ const start = out.indexOf(marker);
1125
+ const end = out.lastIndexOf(marker);
1126
+ if (start < 0 || end <= start) return withNode(env);
1127
+ const resolved = JSON.parse(out.slice(start + marker.length, end));
1128
+ if (!resolved || typeof resolved !== 'object') return withNode(env);
1129
+ const decided = Object.fromEntries(LAUNCH_SETTINGS.filter(key => env[key]).map(key => [key, env[key]]));
1130
+ return withNode({ ...env, ...resolved, ...decided });
1131
+ } catch {
1132
+ return withNode(env);
1133
+ }
1134
+ }
1135
+
1136
+ function appendLog(logPath, line) {
1137
+ try {
1138
+ mkdirSync(dirname(logPath), { recursive: true });
1139
+ try { if (statSync(logPath).size > 1024 * 1024) renameSync(logPath, `${logPath}.1`); } catch {}
1140
+ writeFileSync(logPath, `${new Date().toISOString()} ${line}\n`, { encoding: 'utf8', flag: 'a' });
1141
+ } catch {}
1142
+ }
1143
+
1144
+ export function noteDesktopLaunch(dataHome, line) {
1145
+ appendLog(launcherStatePaths(dataHome).log, `desktop: ${line}`);
1146
+ }
1147
+
1148
+ async function waitForServer({ port, probe, sleep, now, timeoutMs, pollMs = WAIT_POLL_MS, stillGoing = () => true }) {
1149
+ const deadline = now() + timeoutMs;
1150
+ while (now() < deadline) {
1151
+ if (await probe(port)) return true;
1152
+ if (!stillGoing()) return false;
1153
+ await sleep(pollMs);
1154
+ }
1155
+ return false;
1156
+ }
1157
+
1158
+ /**
1159
+ * Run one launch. Returns { outcome, code, port }:
1160
+ * 'ignored' — the link asked for something other than start
1161
+ * 'already-running' — a server answers
1162
+ * 'already-starting' — another launch (or a supervisor) is on it
1163
+ * 'started' — the server answered after this launch
1164
+ * 'spawned' — started, but it had not answered when the wait ended
1165
+ * 'failed' — it could not be started
1166
+ *
1167
+ * `beacon: true` (launcher.mjs) reports the launch on its beacon while it
1168
+ * lasts, and `lingerMs` keeps a failure readable there for a moment. Every
1169
+ * step is also written to DATA_HOME/data/launcher.log with how long it took.
1170
+ */
1171
+ export async function runLauncher({
1172
+ argv = [],
1173
+ env = process.env,
1174
+ platform = process.platform,
1175
+ packageRoot,
1176
+ nodePath = process.execPath,
1177
+ home = homedir(),
1178
+ probe = probeServer,
1179
+ spawnImpl = spawn,
1180
+ runShell = spawnSync,
1181
+ sleep = (ms) => new Promise(r => setTimeout(r, ms)),
1182
+ now = () => Date.now(),
1183
+ waitMs = HEALTH_WAIT_MS,
1184
+ pollMs = WAIT_POLL_MS,
1185
+ beacon = false,
1186
+ lingerMs = 0,
1187
+ alive = pidAlive,
1188
+ background = false,
1189
+ } = {}) {
1190
+ const args = parseLauncherArgs(argv);
1191
+ if (args.action !== 'start') return { outcome: 'ignored', code: 0, port: null };
1192
+
1193
+ const launchedAt = now();
1194
+ const since = () => `${Math.max(0, now() - launchedAt)} ms`;
1195
+ const launchEnv = { ...env };
1196
+ if (args.dataHome) launchEnv.SYNABUN_DATA_HOME = args.dataHome;
1197
+ const dataHome = getDataHome({ env: launchEnv, home, os: platform });
1198
+ const port = resolvePort({ explicit: args.port, env: launchEnv, dataHome });
1199
+ // From here on the environment says what was decided, and resolveLaunchEnv
1200
+ // keeps it over anything a profile exports: the port probed below, the data
1201
+ // home the lock is in and the server that gets started are the same ones.
1202
+ Object.assign(launchEnv, { SYNABUN_DATA_HOME: dataHome, NEURAL_PORT: String(port), SYNABUN_OPEN_BROWSER: '0' });
1203
+ const paths = launcherStatePaths(dataHome);
1204
+ const log = (line) => appendLog(paths.log, `[${process.pid}] ${line}`);
1205
+ const makeBeacon = () => {
1206
+ if (!beacon) return null;
1207
+ try {
1208
+ const options = { port, startedAt: launchedAt, now, logPath: paths.log };
1209
+ return typeof beacon === 'function' ? beacon(options) : createStartBeacon(options);
1210
+ } catch {
1211
+ return null;
1212
+ }
1213
+ };
1214
+
1215
+ const healthy = await probe(port);
1216
+ const decision = decideLaunch({ healthy, lock: readJson(paths.lock), supervisor: readJson(paths.supervisor), now: now(), alive });
1217
+ if (decision !== 'start') {
1218
+ log(`${decision} (port ${port}, via ${args.via})`);
1219
+ // A start somebody else began (by hand in a terminal, say) has nobody
1220
+ // reporting it when no launcher is with it: this click does, for as long
1221
+ // as that start lasts. Windows keeps a console open for a launcher that
1222
+ // stays, so there it stands down at once.
1223
+ if (decision === 'already-starting' && (platform !== 'win32' || background)) {
1224
+ const standIn = makeBeacon();
1225
+ if (standIn && await standIn.listen()) {
1226
+ const supervisorOf = () => {
1227
+ const record = readJson(paths.supervisor);
1228
+ if (record) standIn.supervisor(record);
1229
+ return record;
1230
+ };
1231
+ const up = await waitForServer({
1232
+ port, probe, sleep, now, timeoutMs: waitMs, pollMs,
1233
+ stillGoing: () => {
1234
+ const record = supervisorOf();
1235
+ const lock = readJson(paths.lock);
1236
+ return !!(record && !record.endedAt && alive(record.pid)) || !!(lock && alive(lock.pid));
1237
+ },
1238
+ });
1239
+ const record = readJson(paths.supervisor);
1240
+ if (up) standIn.finish('started');
1241
+ else if (record?.phase === 'failed') standIn.finish('failed', record.error);
1242
+ else standIn.finish('gave-up');
1243
+ if (!up) await standIn.linger(lingerMs);
1244
+ }
1245
+ standIn?.close();
1246
+ }
1247
+ return { outcome: decision, code: 0, port };
1248
+ }
1249
+
1250
+ const report = makeBeacon();
1251
+ // An earlier launch that failed may still be saying so: take over when it lets go.
1252
+ report?.listen({ retryMs: 5000 });
1253
+ const fail = (error, line = error.message) => {
1254
+ log(`failed: ${line}`);
1255
+ report?.finish('failed', error);
1256
+ return { outcome: 'failed', code: 1, port };
1257
+ };
1258
+ const conclude = async (result) => {
1259
+ if (report && result.outcome !== 'started') await report.linger(lingerMs);
1260
+ report?.close();
1261
+ return result;
1262
+ };
1263
+
1264
+ let lock;
1265
+ try {
1266
+ lock = acquireLaunchLock(paths.lock, { now: now(), port });
1267
+ } catch (error) {
1268
+ return conclude(fail({ code: 'LOCK_UNAVAILABLE', message: error.message }));
1269
+ }
1270
+ if (!lock) {
1271
+ log(`already-starting (lost the lock, port ${port})`);
1272
+ report?.close();
1273
+ return { outcome: 'already-starting', code: 0, port };
1274
+ }
1275
+ log(`starting (port ${port}, via ${args.via})`);
1276
+ report?.step('locked');
1277
+
1278
+ // What the supervisor of this launch says about itself. One of an earlier
1279
+ // launch (a failure left for the next click to read) is not ours.
1280
+ let supervisor = null;
1281
+ const readSupervisor = () => {
1282
+ const record = readJson(paths.supervisor);
1283
+ if (!record || Number(record.startedAt) < launchedAt - 2000) return;
1284
+ if (!supervisor) log(`the supervisor is running after ${since()}`);
1285
+ if (record.phase && record.phase !== supervisor?.phase) log(`supervisor: ${record.phase}${record.phase === 'failed' ? ` (${record.error?.message || 'no reason given'})` : ''} at ${since()}`);
1286
+ supervisor = record;
1287
+ report?.supervisor(record);
1288
+ };
1289
+ const supervisorEnded = () => !!supervisor && (!!supervisor.endedAt || !alive(supervisor.pid));
1290
+ const supervisorFailure = (fallback) => (supervisor?.phase === 'failed' && supervisor.error
1291
+ ? { ...supervisor.error }
1292
+ : fallback);
1293
+ const answered = () => {
1294
+ log(`started: answering after ${since()}`);
1295
+ report?.finish('started');
1296
+ return { outcome: 'started', code: 0, port };
1297
+ };
1298
+ const gaveUp = () => {
1299
+ log(`not answering yet (gave up waiting after ${since()})`);
1300
+ report?.finish('gave-up', { code: 'NO_ANSWER', message: `no answer after ${Math.round(waitMs / 1000)} s` });
1301
+ return { outcome: 'spawned', code: 0, port };
1302
+ };
1303
+
1304
+ const setup = join(packageRoot, 'setup.js');
1305
+ const nodeArgs = ['--disable-warning=ExperimentalWarning', setup];
1306
+ let result;
1307
+ try {
1308
+ result = await (async () => {
1309
+ if (!existsSync(setup)) return fail({ code: 'SETUP_MISSING', message: `${setup} is missing` });
1310
+
1311
+ // ── Windows: the OS opened a console for this process; the server lives in it ──
1312
+ if (platform === 'win32' && !background) {
1313
+ log(`starting in this console (port ${port})`);
1314
+ const child = spawnImpl(nodePath, nodeArgs, {
1315
+ cwd: packageRoot,
1316
+ stdio: 'inherit',
1317
+ env: resolveLaunchEnv({ env: launchEnv, nodePath, platform, home, runShell }),
1318
+ });
1319
+ report?.step('spawned');
1320
+ let exited = null;
1321
+ const done = new Promise((res) => {
1322
+ child.once('error', (error) => { exited = { code: 1, error }; res(); });
1323
+ child.once('exit', (code) => { exited = { code: code ?? 1 }; res(); });
1324
+ });
1325
+ const up = await waitForServer({ port, probe, sleep, now, timeoutMs: waitMs, pollMs, stillGoing: () => { readSupervisor(); return !exited; } });
1326
+ lock.release();
1327
+ if (up) {
1328
+ answered();
1329
+ report?.close(); // the console stays for as long as the server does; the beacon does not
1330
+ } else {
1331
+ readSupervisor();
1332
+ fail(supervisorFailure({
1333
+ code: exited ? 'SUPERVISOR_EXITED' : 'NO_ANSWER',
1334
+ message: exited ? `the supervisor exited (${exited.error?.message || `code ${exited.code}`})` : `no answer after ${Math.round(waitMs / 1000)} s`,
1335
+ exitCode: exited?.code,
1336
+ }), `not answering${exited ? ` (exit ${exited.code})` : ''}`);
1337
+ // Still running, just silent: the console stays, the beacon says so and goes.
1338
+ if (!exited && report) { await report.linger(lingerMs); report.close(); }
1339
+ }
1340
+ await done; // keep the console (and the supervisor in it) until the server stops
1341
+ return { outcome: up ? 'started' : 'failed', code: exited?.code ?? 0, port };
1342
+ }
1343
+
1344
+ // ── macOS: a Terminal window, exactly like a server started by hand ──
1345
+ if (!background && platform === 'darwin' && args.terminalScript && existsSync(args.terminalScript)) {
1346
+ const asked = now();
1347
+ const opened = runShell('/usr/bin/open', ['-a', 'Terminal', args.terminalScript], { timeout: 15000, stdio: 'ignore' });
1348
+ if (!opened?.error && opened?.status === 0) {
1349
+ log(`opened Terminal (port ${port}) in ${Math.max(0, now() - asked)} ms`);
1350
+ report?.step('terminal');
1351
+ const up = await waitForServer({ port, probe, sleep, now, timeoutMs: waitMs, pollMs, stillGoing: () => { readSupervisor(); return !supervisorEnded(); } });
1352
+ if (up) return answered();
1353
+ // The supervisor this launch saw start is gone and nothing answers:
1354
+ // it is over, and holding the lock would only block the next click.
1355
+ if (supervisorEnded()) {
1356
+ return fail(supervisorFailure({ code: 'SUPERVISOR_EXITED', message: 'the server stopped before it answered (the Terminal window says why)' }));
1357
+ }
1358
+ return gaveUp();
1359
+ }
1360
+ log(`Terminal did not open (${opened?.error?.message || `status ${opened?.status}`}); starting in the background`);
1361
+ }
1362
+
1363
+ // ── Linux (and the macOS fallback): detached, with the login shell's environment ──
1364
+ let out = 'ignore';
1365
+ try { out = openSync(paths.log, 'a'); } catch {}
1366
+ const child = spawnImpl(nodePath, nodeArgs, {
1367
+ cwd: packageRoot,
1368
+ detached: true,
1369
+ windowsHide: background,
1370
+ stdio: ['ignore', out, out],
1371
+ // Packaged re-entry and isolated tests can supply an already resolved
1372
+ // environment. Do not read a login profile or add host CLI paths twice.
1373
+ env: background && launchEnv.SYNABUN_LAUNCH_ENV === 'resolved'
1374
+ ? launchEnv : resolveLaunchEnv({ env: launchEnv, nodePath, platform, home, runShell }),
1375
+ });
1376
+ let exited = null;
1377
+ child.once('error', (error) => { exited = { code: 1, error }; });
1378
+ child.once('exit', (code) => { exited = { code: code ?? 1 }; });
1379
+ child.unref();
1380
+ if (typeof out === 'number') { try { closeSync(out); } catch {} }
1381
+ log(`started in the background (pid ${child.pid}, port ${port}) after ${since()}`);
1382
+ report?.step('spawned');
1383
+ const up = await waitForServer({ port, probe, sleep, now, timeoutMs: waitMs, pollMs, stillGoing: () => { readSupervisor(); return !exited; } });
1384
+ if (up) return answered();
1385
+ if (exited) {
1386
+ readSupervisor();
1387
+ const reason = `the supervisor exited (${exited.error?.message || `code ${exited.code}`})`;
1388
+ return fail(supervisorFailure({ code: 'SUPERVISOR_EXITED', message: reason, exitCode: exited.code }), reason);
1389
+ }
1390
+ return gaveUp();
1391
+ })();
1392
+ } finally {
1393
+ lock.release();
1394
+ }
1395
+ return conclude(result);
1396
+ }