@reticlehq/server 3.9.0 → 3.10.0

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 (350) hide show
  1. package/SKILL.md +13 -12
  2. package/dist/command/cli/adhoc-suite.js +2 -0
  3. package/dist/command/cli/adhoc-verdict.d.ts +2 -43
  4. package/dist/command/cli/adhoc-verdict.js +50 -187
  5. package/dist/command/cli/answers/mcp-connection-hint.d.ts +2 -0
  6. package/dist/command/cli/answers/mcp-connection-hint.js +6 -0
  7. package/dist/command/cli/cli-doctor.js +1 -1
  8. package/dist/command/cli/cli-flow-commands.js +2 -9
  9. package/dist/command/cli/cli-kill.js +1 -1
  10. package/dist/command/cli/cli-parse-compare.d.ts +22 -0
  11. package/dist/command/cli/cli-parse-compare.js +75 -0
  12. package/dist/command/cli/cli-parse-grade.d.ts +36 -0
  13. package/dist/command/cli/cli-parse-grade.js +75 -0
  14. package/dist/command/cli/cli-parse-grammar.d.ts +2 -0
  15. package/dist/command/cli/cli-parse-grammar.js +18 -20
  16. package/dist/command/cli/cli-parse-verify.d.ts +10 -0
  17. package/dist/command/cli/cli-parse-verify.js +63 -14
  18. package/dist/command/cli/cli-parse.d.ts +17 -15
  19. package/dist/command/cli/cli-parse.js +44 -27
  20. package/dist/command/cli/cli-update-commands.d.ts +1 -1
  21. package/dist/command/cli/cli-update-commands.js +29 -20
  22. package/dist/command/cli/cli-usage.js +58 -29
  23. package/dist/command/cli/cli-verify.d.ts +11 -2
  24. package/dist/command/cli/cli-verify.js +27 -8
  25. package/dist/command/cli/cloud-cli.js +2 -119
  26. package/dist/command/cli/daemon-link/daemon-client.d.ts +45 -0
  27. package/dist/command/cli/daemon-link/daemon-client.js +205 -0
  28. package/dist/command/cli/doctor/doctor-daemon-line.js +6 -3
  29. package/dist/command/cli/doctor-repairs.js +1 -1
  30. package/dist/command/cli/ensure-daemon.d.ts +26 -0
  31. package/dist/command/cli/ensure-daemon.js +57 -0
  32. package/dist/command/cli/finished-run/grade-command.d.ts +23 -0
  33. package/dist/command/cli/finished-run/grade-command.js +180 -0
  34. package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +1 -1
  35. package/dist/command/cli/lifecycle/daemon-lifecycle.js +1 -1
  36. package/dist/command/cli/ports/port-holder.d.ts +9 -0
  37. package/dist/command/cli/ports/port-holder.js +27 -3
  38. package/dist/command/cli/status/status-lines.d.ts +1 -0
  39. package/dist/command/cli/status/status-lines.js +22 -0
  40. package/dist/command/cli/status-fields.d.ts +5 -1
  41. package/dist/command/cli/status-fields.js +7 -3
  42. package/dist/command/cli/two-builds/compare-command.d.ts +48 -0
  43. package/dist/command/cli/two-builds/compare-command.js +314 -0
  44. package/dist/command/cli/verify-against.d.ts +61 -0
  45. package/dist/command/cli/verify-against.js +187 -0
  46. package/dist/command/cli/verify-no-session.d.ts +2 -0
  47. package/dist/command/cli/verify-no-session.js +13 -0
  48. package/dist/command/cli/verify-results.d.ts +35 -2
  49. package/dist/command/cli/verify-results.js +76 -10
  50. package/dist/command/cli.js +46 -41
  51. package/dist/command/daemon/daemon-resolve.d.ts +8 -1
  52. package/dist/command/daemon/daemon-resolve.js +27 -1
  53. package/dist/command/daemon/daemon.d.ts +12 -1
  54. package/dist/command/daemon/daemon.js +35 -4
  55. package/dist/command/daemon/lifetime/death-cause.js +21 -2
  56. package/dist/command/daemon/lifetime/heartbeat.js +7 -2
  57. package/dist/command/daemon/lifetime/startup-failure.d.ts +29 -0
  58. package/dist/command/daemon/lifetime/startup-failure.js +62 -0
  59. package/dist/command/setup/bringup/dev-server-files.d.ts +9 -0
  60. package/dist/command/setup/bringup/dev-server-files.js +17 -0
  61. package/dist/command/setup/bringup/static-page-server.d.ts +14 -2
  62. package/dist/command/setup/bringup/static-page-server.js +43 -9
  63. package/dist/command/setup/init/init-runtime.js +6 -0
  64. package/dist/command/setup/node-effects.d.ts +3 -2
  65. package/dist/command/setup/node-effects.js +9 -3
  66. package/dist/command/setup/remaining-steps.d.ts +8 -0
  67. package/dist/command/setup/remaining-steps.js +10 -0
  68. package/dist/command/setup/run-setup.d.ts +2 -0
  69. package/dist/command/setup/run-setup.js +9 -1
  70. package/dist/command/setup/setup-command.js +6 -3
  71. package/dist/command/update/updater.js +6 -0
  72. package/dist/command/version/version-skew.js +1 -1
  73. package/dist/features/compare/action-plan.d.ts +260 -0
  74. package/dist/features/compare/action-plan.js +543 -0
  75. package/dist/features/compare/behaviour-compare.d.ts +24 -0
  76. package/dist/features/compare/behaviour-compare.js +85 -0
  77. package/dist/features/compare/behaviour-diff.d.ts +97 -0
  78. package/dist/features/compare/behaviour-diff.js +202 -0
  79. package/dist/features/compare/behaviour-report.d.ts +40 -0
  80. package/dist/features/compare/behaviour-report.js +128 -0
  81. package/dist/features/compare/build-readings.d.ts +106 -0
  82. package/dist/features/compare/build-readings.js +250 -0
  83. package/dist/features/compare/compare-diff.d.ts +62 -0
  84. package/dist/features/compare/compare-diff.js +177 -0
  85. package/dist/features/compare/compare-report-html.d.ts +6 -0
  86. package/dist/features/compare/compare-report-html.js +102 -0
  87. package/dist/features/compare/compare-report.d.ts +15 -0
  88. package/dist/features/compare/compare-report.js +73 -0
  89. package/dist/features/compare/compare-run.d.ts +60 -0
  90. package/dist/features/compare/compare-run.js +123 -0
  91. package/dist/features/compare/outside/effect-lines.d.ts +26 -0
  92. package/dist/features/compare/outside/effect-lines.js +103 -0
  93. package/dist/features/compare/outside/state-sources.d.ts +76 -0
  94. package/dist/features/compare/outside/state-sources.js +164 -0
  95. package/dist/features/compare/outside/value-dictionary.d.ts +51 -0
  96. package/dist/features/compare/outside/value-dictionary.js +227 -0
  97. package/dist/features/compare/visible-content.d.ts +20 -0
  98. package/dist/features/compare/visible-content.js +125 -0
  99. package/dist/features/crawl/crawl-empty.js +2 -1
  100. package/dist/features/crawl/crawl-exhaustive.d.ts +55 -0
  101. package/dist/features/crawl/crawl-exhaustive.js +138 -0
  102. package/dist/features/crawl/crawl-tools.d.ts +9 -2
  103. package/dist/features/crawl/crawl-tools.js +28 -12
  104. package/dist/features/crawl/crawl.d.ts +8 -9
  105. package/dist/features/crawl/crawl.js +61 -65
  106. package/dist/features/exhaust/app-coverage.d.ts +5 -1
  107. package/dist/features/exhaust/app-coverage.js +3 -1
  108. package/dist/features/exhaust/code-coverage.js +8 -4
  109. package/dist/features/exhaust/edge-cases.d.ts +133 -0
  110. package/dist/features/exhaust/edge-cases.js +310 -0
  111. package/dist/features/exhaust/edges/edge-drive.d.ts +42 -0
  112. package/dist/features/exhaust/edges/edge-drive.js +364 -0
  113. package/dist/features/exhaust/edges/edge-run.d.ts +59 -0
  114. package/dist/features/exhaust/edges/edge-run.js +169 -0
  115. package/dist/features/exhaust/explorer.d.ts +87 -14
  116. package/dist/features/exhaust/explorer.js +159 -25
  117. package/dist/features/exhaust/failure-branches.d.ts +5 -0
  118. package/dist/features/exhaust/failure-branches.js +29 -11
  119. package/dist/features/exhaust/invariants.d.ts +112 -0
  120. package/dist/features/exhaust/invariants.js +188 -0
  121. package/dist/features/exhaust/ledger.d.ts +68 -0
  122. package/dist/features/exhaust/ledger.js +121 -13
  123. package/dist/features/exhaust/session-fold.d.ts +10 -0
  124. package/dist/features/exhaust/session-fold.js +46 -3
  125. package/dist/features/exhaust/session-port.d.ts +3 -1
  126. package/dist/features/exhaust/session-port.js +97 -28
  127. package/dist/features/grade/grade-answer.d.ts +99 -0
  128. package/dist/features/grade/grade-answer.js +112 -0
  129. package/dist/features/grade/grade-noise.d.ts +27 -0
  130. package/dist/features/grade/grade-noise.js +55 -0
  131. package/dist/features/grade/grade-renumber.d.ts +12 -0
  132. package/dist/features/grade/grade-renumber.js +86 -0
  133. package/dist/features/grade/grade-state.d.ts +50 -0
  134. package/dist/features/grade/grade-state.js +93 -0
  135. package/dist/features/grade/grade-suggest.d.ts +33 -0
  136. package/dist/features/grade/grade-suggest.js +98 -0
  137. package/dist/features/visual/visual-tools.js +66 -19
  138. package/dist/index.js +7 -4
  139. package/dist/judgement/capsule/causal-summary.js +9 -2
  140. package/dist/judgement/runs/artifact/replay-mapping.d.ts +13 -1
  141. package/dist/judgement/runs/artifact/replay-mapping.js +1 -1
  142. package/dist/judgement/runs/reticle-runner.js +16 -2
  143. package/dist/judgement/runs/runner-port.d.ts +2 -0
  144. package/dist/judgement/runs/runner-port.js +23 -5
  145. package/dist/language/flows/affected-tools.d.ts +1 -1
  146. package/dist/language/flows/affected-tools.js +49 -30
  147. package/dist/language/flows/change/git-changed.d.ts +2 -1
  148. package/dist/language/flows/change/git-changed.js +19 -2
  149. package/dist/language/flows/fields/flow-secret-field.d.ts +1 -13
  150. package/dist/language/flows/fields/flow-secret-field.js +77 -21
  151. package/dist/language/flows/flow-anchor.d.ts +60 -2
  152. package/dist/language/flows/flow-anchor.js +125 -20
  153. package/dist/language/flows/flow-classify.d.ts +20 -0
  154. package/dist/language/flows/flow-classify.js +90 -12
  155. package/dist/language/flows/flow-cross-step.d.ts +7 -1
  156. package/dist/language/flows/flow-cross-step.js +25 -1
  157. package/dist/language/flows/flow-journey.d.ts +38 -4
  158. package/dist/language/flows/flow-journey.js +138 -8
  159. package/dist/language/flows/flow-replay-run.d.ts +0 -16
  160. package/dist/language/flows/flow-replay-run.js +76 -14
  161. package/dist/language/flows/flow-replay-types.d.ts +4 -2
  162. package/dist/language/flows/flow-replay.d.ts +19 -1
  163. package/dist/language/flows/flow-replay.js +169 -52
  164. package/dist/language/flows/flow-replayable.js +7 -1
  165. package/dist/language/flows/flow-start-path.d.ts +34 -5
  166. package/dist/language/flows/flow-start-path.js +202 -15
  167. package/dist/language/flows/flow-step-runners.d.ts +14 -13
  168. package/dist/language/flows/flow-step-runners.js +184 -109
  169. package/dist/language/flows/flow-store-for-session.d.ts +26 -25
  170. package/dist/language/flows/flow-store-for-session.js +44 -0
  171. package/dist/language/flows/flow-tools.js +46 -33
  172. package/dist/language/flows/flows.js +176 -76
  173. package/dist/language/flows/recording/tape/recordings.d.ts +19 -3
  174. package/dist/language/flows/recording/tape/recordings.js +71 -5
  175. package/dist/language/flows/replay.d.ts +19 -5
  176. package/dist/language/flows/replay.js +99 -7
  177. package/dist/language/flows/steps/page-steps.d.ts +68 -0
  178. package/dist/language/flows/steps/page-steps.js +198 -0
  179. package/dist/language/flows/suite/sequential-run.d.ts +68 -0
  180. package/dist/language/flows/suite/sequential-run.js +96 -0
  181. package/dist/language/flows/verify-change-tools.js +26 -15
  182. package/dist/memory/cloud/background-sync.d.ts +13 -0
  183. package/dist/memory/cloud/background-sync.js +24 -3
  184. package/dist/memory/cloud/sync-daemon.js +26 -27
  185. package/dist/memory/journal/attach-journal.js +2 -1
  186. package/dist/memory/journal/drive-flow.d.ts +7 -0
  187. package/dist/memory/journal/drive-flow.js +44 -13
  188. package/dist/memory/journal/journal-recorder.d.ts +4 -2
  189. package/dist/memory/journal/session-journal.d.ts +7 -0
  190. package/dist/memory/journal/session-journal.js +58 -10
  191. package/dist/memory/project/root-claims.d.ts +9 -0
  192. package/dist/memory/project/root-claims.js +9 -4
  193. package/dist/portal/input/download-capture.d.ts +7 -0
  194. package/dist/portal/input/download-capture.js +92 -0
  195. package/dist/portal/input/network-detail.d.ts +6 -0
  196. package/dist/portal/input/network-detail.js +12 -1
  197. package/dist/portal/input/network-mock-tools.js +49 -13
  198. package/dist/portal/input/network-mock.d.ts +8 -0
  199. package/dist/portal/input/network-mock.js +28 -9
  200. package/dist/portal/input/page-capture.d.ts +24 -0
  201. package/dist/portal/input/page-capture.js +33 -0
  202. package/dist/portal/input/real-input.d.ts +22 -20
  203. package/dist/portal/input/real-input.js +41 -31
  204. package/dist/portal/input/viewport-tools.d.ts +8 -0
  205. package/dist/portal/input/viewport-tools.js +75 -18
  206. package/dist/portal/pool/browser-pool.d.ts +11 -29
  207. package/dist/portal/pool/browser-pool.js +35 -17
  208. package/dist/portal/pool/context-permissions.d.ts +17 -3
  209. package/dist/portal/pool/context-permissions.js +36 -5
  210. package/dist/portal/pool/determinism.d.ts +52 -0
  211. package/dist/portal/pool/determinism.js +259 -0
  212. package/dist/portal/pool/lease-emulation.d.ts +34 -1
  213. package/dist/portal/pool/lease-emulation.js +18 -0
  214. package/dist/portal/pool/lease-marker-stamp.d.ts +6 -0
  215. package/dist/portal/pool/lease-marker-stamp.js +15 -1
  216. package/dist/portal/pool/lease-page-error.d.ts +44 -17
  217. package/dist/portal/pool/lease-page-error.js +147 -5
  218. package/dist/portal/pool/page-error-journal.d.ts +7 -1
  219. package/dist/portal/pool/page-error-journal.js +27 -0
  220. package/dist/portal/pool/playwright-launcher.js +51 -8
  221. package/dist/portal/pool/pool-contract.d.ts +27 -1
  222. package/dist/portal/pool/pool-lease.d.ts +36 -1
  223. package/dist/portal/pool/zero-install.d.ts +8 -0
  224. package/dist/portal/pool/zero-install.js +23 -1
  225. package/dist/portal/session/dev-server/dev-command.d.ts +7 -6
  226. package/dist/portal/session/dev-server/dev-command.js +61 -15
  227. package/dist/portal/session/dev-server/dev-server-probe.d.ts +7 -0
  228. package/dist/portal/session/dev-server/dev-server-probe.js +9 -0
  229. package/dist/portal/session/dev-server/project-ports.d.ts +21 -0
  230. package/dist/portal/session/dev-server/project-ports.js +62 -0
  231. package/dist/portal/session/facts/departed-network.d.ts +66 -0
  232. package/dist/portal/session/facts/departed-network.js +142 -0
  233. package/dist/portal/session/no-session-diagnosis.d.ts +9 -0
  234. package/dist/portal/session/no-session-diagnosis.js +67 -25
  235. package/dist/portal/session/no-session-next-action.d.ts +2 -0
  236. package/dist/portal/session/no-session-next-action.js +12 -3
  237. package/dist/portal/session/no-session-watch.d.ts +6 -0
  238. package/dist/portal/session/no-session-watch.js +38 -3
  239. package/dist/portal/session/serving-directory.d.ts +10 -0
  240. package/dist/portal/session/serving-directory.js +39 -8
  241. package/dist/portal/session/session-manager.d.ts +10 -0
  242. package/dist/portal/session/session-manager.js +174 -63
  243. package/dist/start-wiring.d.ts +2 -1
  244. package/dist/start-wiring.js +2 -1
  245. package/dist/surface/mcp/mcp.js +3 -2
  246. package/dist/surface/mcp/shared-params.d.ts +2 -0
  247. package/dist/surface/mcp/shared-params.js +31 -0
  248. package/dist/surface/tools/act/act-observation.d.ts +40 -2
  249. package/dist/surface/tools/act/act-observation.js +118 -12
  250. package/dist/surface/tools/act/act-preflight.js +1 -1
  251. package/dist/surface/tools/act/act-sequence-retry.d.ts +23 -2
  252. package/dist/surface/tools/act/act-sequence-retry.js +61 -5
  253. package/dist/surface/tools/act/after-match.d.ts +4 -1
  254. package/dist/surface/tools/act/after-match.js +8 -1
  255. package/dist/surface/tools/act/already-true.js +1 -1
  256. package/dist/surface/tools/act/navigation/reload-result.d.ts +12 -1
  257. package/dist/surface/tools/act/navigation/reload-result.js +25 -2
  258. package/dist/surface/tools/act/resolve-target.d.ts +15 -1
  259. package/dist/surface/tools/act/resolve-target.js +24 -1
  260. package/dist/surface/tools/act/sequence-grade.d.ts +12 -0
  261. package/dist/surface/tools/act/sequence-grade.js +37 -26
  262. package/dist/surface/tools/act/settle-in-flight.js +27 -15
  263. package/dist/surface/tools/act-command.d.ts +23 -0
  264. package/dist/surface/tools/act-command.js +50 -0
  265. package/dist/surface/tools/act-sequence-tool.js +78 -50
  266. package/dist/surface/tools/act-tools.d.ts +1 -13
  267. package/dist/surface/tools/act-tools.js +74 -86
  268. package/dist/surface/tools/agent/build-staleness.d.ts +1 -1
  269. package/dist/surface/tools/agent/build-staleness.js +1 -1
  270. package/dist/surface/tools/assert/assert-verdict.js +16 -37
  271. package/dist/surface/tools/browser-tools.js +12 -6
  272. package/dist/surface/tools/compare-tools.d.ts +11 -0
  273. package/dist/surface/tools/compare-tools.js +94 -0
  274. package/dist/surface/tools/coverage-tools.js +1 -1
  275. package/dist/surface/tools/dynamic-tools.js +9 -2
  276. package/dist/surface/tools/error-recovery.d.ts +12 -1
  277. package/dist/surface/tools/error-recovery.js +91 -21
  278. package/dist/surface/tools/explore-tools.js +1 -1
  279. package/dist/surface/tools/invoke-tool.js +3 -2
  280. package/dist/surface/tools/lease-coverage-tools.d.ts +49 -0
  281. package/dist/surface/tools/lease-coverage-tools.js +128 -0
  282. package/dist/surface/tools/lease-hint.d.ts +35 -1
  283. package/dist/surface/tools/lease-hint.js +42 -0
  284. package/dist/surface/tools/lease-readiness.d.ts +26 -0
  285. package/dist/surface/tools/lease-readiness.js +79 -0
  286. package/dist/surface/tools/lease-seed.d.ts +14 -1
  287. package/dist/surface/tools/lease-seed.js +35 -3
  288. package/dist/surface/tools/lease-tools.d.ts +3 -41
  289. package/dist/surface/tools/lease-tools.js +62 -110
  290. package/dist/surface/tools/lease-url-identity.d.ts +46 -0
  291. package/dist/surface/tools/lease-url-identity.js +97 -0
  292. package/dist/surface/tools/merge-tools.js +99 -2
  293. package/dist/surface/tools/merged-name-redirect.js +5 -1
  294. package/dist/surface/tools/observe-tools.js +15 -9
  295. package/dist/surface/tools/read/console-pre-attach.d.ts +19 -0
  296. package/dist/surface/tools/read/console-pre-attach.js +23 -0
  297. package/dist/surface/tools/read-tools.js +33 -30
  298. package/dist/surface/tools/real-input-attempt.d.ts +0 -8
  299. package/dist/surface/tools/real-input-attempt.js +143 -28
  300. package/dist/surface/tools/session-ports.d.ts +14 -0
  301. package/dist/surface/tools/session-ports.js +53 -0
  302. package/dist/surface/tools/tool-kit.d.ts +6 -0
  303. package/dist/surface/tools/tool-kit.js +2 -3
  304. package/dist/surface/tools/tool-surface.js +0 -1
  305. package/dist/surface/tools/tools.js +28 -5
  306. package/dist/surface/tools/type-spelling.d.ts +13 -0
  307. package/dist/surface/tools/type-spelling.js +36 -0
  308. package/dist/telemetry/agent-client.d.ts +6 -0
  309. package/dist/telemetry/agent-client.js +10 -0
  310. package/dist/telemetry/argument-shape.js +1 -0
  311. package/dist/telemetry/tool-refused.d.ts +6 -3
  312. package/dist/telemetry/tool-refused.js +8 -3
  313. package/docs/actions.mdx +3 -3
  314. package/docs/agent-cheatsheet.md +1 -1
  315. package/docs/capabilities.mdx +61 -1
  316. package/docs/cli/affected.mdx +9 -7
  317. package/docs/cli/cloud.mdx +3 -3
  318. package/docs/cli/compare.mdx +94 -0
  319. package/docs/cli/doctor.mdx +1 -1
  320. package/docs/cli/grade.mdx +97 -0
  321. package/docs/cli/license.mdx +9 -7
  322. package/docs/cli/restart.mdx +7 -5
  323. package/docs/cli/rollback.mdx +5 -5
  324. package/docs/cli/stop.mdx +3 -3
  325. package/docs/cli/update.mdx +2 -2
  326. package/docs/cli/verify.mdx +23 -1
  327. package/docs/cli.mdx +33 -23
  328. package/docs/deploy-checks.md +1 -1
  329. package/docs/desktop.mdx +1 -1
  330. package/docs/enterprise.md +2 -2
  331. package/docs/environment.mdx +1 -1
  332. package/docs/flows.md +11 -1
  333. package/docs/frameworks.mdx +9 -9
  334. package/docs/getting-started.md +1 -1
  335. package/docs/install-manual.mdx +1 -1
  336. package/docs/license-activation.mdx +3 -3
  337. package/docs/packages/tauri.mdx +1 -1
  338. package/docs/packages.mdx +1 -1
  339. package/docs/predicates.mdx +1 -1
  340. package/docs/quickstart.mdx +68 -49
  341. package/docs/skill-setup.md +1 -1
  342. package/docs/skill.md +14 -13
  343. package/docs/telemetry.md +2 -2
  344. package/docs/tools/navigate.mdx +1 -1
  345. package/docs/tools/verify.mdx +4 -1
  346. package/docs/troubleshooting.mdx +1 -1
  347. package/docs/usage.md +17 -14
  348. package/package.json +13 -13
  349. package/dist/features/exhaust/exhaust-run.d.ts +0 -33
  350. package/dist/features/exhaust/exhaust-run.js +0 -82
@@ -1,5 +1,5 @@
1
1
  import { join } from 'node:path';
2
- import { homedir } from 'node:os';
2
+ import { homedir, uptime } from 'node:os';
3
3
  import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync, openSync, closeSync, readdirSync, statSync, renameSync, writeSync, } from 'node:fs';
4
4
  import { spawn } from 'node:child_process';
5
5
  import { daemonRegistryFileName, DaemonSpawnMethod, ReticleEnv, } from '@reticlehq/core';
@@ -155,13 +155,42 @@ export function discoverDaemonPort() {
155
155
  }
156
156
  return found;
157
157
  }
158
+ /**
159
+ * When the machine last booted, in epoch ms. A pidfile written before it names no daemon: every
160
+ * process from before the boot is gone, and its pid may now belong to anything.
161
+ */
162
+ function lastBootMs() {
163
+ // The clock is read at this I/O boundary, as the other timestamps in this file are.
164
+ return Date.now() - uptime() * 1000;
165
+ }
166
+ /**
167
+ * Does this pidfile name a daemon that is still running?
168
+ *
169
+ * The pid must be alive AND the file must be newer than the last boot. A pid alone is not enough:
170
+ * the OS reuses pids, and after a reboot a pidfile named an unrelated Bluetooth service on Windows.
171
+ * The port was free, but serve skipped the start because that pid was alive, and the daemon never
172
+ * came up. A file whose age cannot be read is judged by its pid alone, as before.
173
+ */
174
+ export function pidFileIsLive(path, pid, pidAlive, bootMs = lastBootMs) {
175
+ if (!pidAlive(pid)) {
176
+ return false;
177
+ }
178
+ let writtenMs;
179
+ try {
180
+ writtenMs = statSync(path).mtimeMs;
181
+ }
182
+ catch {
183
+ return true;
184
+ }
185
+ return writtenMs >= bootMs();
186
+ }
158
187
  /**
159
188
  * Sweep ~/.reticle for daemon-<port>.pid files whose process is no longer alive and delete them, so a
160
189
  * crashed daemon never leaves a stale pidfile that confuses discovery or makes a port look "taken".
161
190
  * Returns the ports reclaimed. `home` and `pidAlive` are injectable for testing (default to the real
162
191
  * ~/.reticle and the process.kill(pid,0) liveness probe).
163
192
  */
164
- export function reclaimStaleDaemons(home = reticleStateHome(), pidAlive = isAlive) {
193
+ export function reclaimStaleDaemons(home = reticleStateHome(), pidAlive = isAlive, bootMs = lastBootMs) {
165
194
  const reclaimed = [];
166
195
  let files;
167
196
  try {
@@ -184,7 +213,7 @@ export function reclaimStaleDaemons(home = reticleStateHome(), pidAlive = isAliv
184
213
  catch {
185
214
  pid = null; // unreadable pidfile counts as stale
186
215
  }
187
- if (null === pid || !pidAlive(pid)) {
216
+ if (null === pid || !pidFileIsLive(path, pid, pidAlive, bootMs)) {
188
217
  try {
189
218
  unlinkSync(path);
190
219
  removeDaemonRegistry(Number(match[1])); // drop the sidecar discovery entry too
@@ -287,8 +316,10 @@ export function spawnDaemon(nodeExec, scriptPath, args, port, deps = defaultSpaw
287
316
  }
288
317
  catch {
289
318
  const existing = readPid(port, deps.home);
290
- if (existing !== null && deps.pidAlive(existing))
319
+ if (existing !== null &&
320
+ pidFileIsLive(pidFilePath, existing, (pid) => deps.pidAlive(pid), deps.bootTimeMs)) {
291
321
  return false;
322
+ }
292
323
  // This branch IS the crash detection — its own comment has always said so ("a stale pidfile from
293
324
  // a crashed daemon is reclaimed"), it just never said it out loud. Captured before the unlink,
294
325
  // because after it there is no trace left that the previous daemon died at all.
@@ -11,7 +11,7 @@
11
11
  * counts. Never a message, because a message can carry the user's paths.
12
12
  */
13
13
  import { closeSync, fstatSync, openSync, readSync } from 'node:fs';
14
- import { DaemonEnd, NODE_ERROR_CODE } from '@reticlehq/core';
14
+ import { DaemonEnd, DaemonStartupError, NODE_ERROR_CODE } from '@reticlehq/core';
15
15
  import { DAEMON_HEARTBEAT_EVENT, classifyDaemonLife } from './heartbeat.js';
16
16
  import { DAEMON_START_FAILED_EVENT } from './startup-failure.js';
17
17
  /**
@@ -45,6 +45,14 @@ function lastErrorCode(text) {
45
45
  const code = codes.filter((c) => c !== undefined && !NOT_A_CODE.has(c)).at(-1);
46
46
  return code !== undefined && NODE_ERROR_CODE.test(code) ? code : undefined;
47
47
  }
48
+ /** The stage of a failed start, only when it is a member of the closed list. */
49
+ function startupStage(v) {
50
+ return Object.values(DaemonStartupError).find((stage) => stage === v);
51
+ }
52
+ /** The code field of a failed start, only when it has the shape of a Node error code. */
53
+ function codeField(v) {
54
+ return 'string' === typeof v && NODE_ERROR_CODE.test(v) ? v : undefined;
55
+ }
48
56
  const count = (v) => 'number' === typeof v && Number.isInteger(v) && 0 <= v ? v : undefined;
49
57
  /**
50
58
  * Read the cause of death of the daemon that started at `startedMs`.
@@ -97,13 +105,24 @@ export function deathCauseFromLog(text, startedMs, nowMs) {
97
105
  }
98
106
  }
99
107
  const failed = ours.findLast((l) => l.event === DAEMON_START_FAILED_EVENT);
100
- const error = ('string' === typeof failed?.['error'] ? lastErrorCode(failed['error']) : undefined) ??
108
+ const stage = startupStage(failed?.['stage']);
109
+ if (stage !== undefined) {
110
+ cause.startupError = stage;
111
+ }
112
+ const error = codeField(failed?.['code']) ??
113
+ ('string' === typeof failed?.['error'] ? lastErrorCode(failed['error']) : undefined) ??
101
114
  // Raw lines before this daemon's first line belong to the daemon before it, unless this one
102
115
  // wrote no line at all, which is a crash before its first log line.
103
116
  lastErrorCode((0 < ours.length ? rawInOurs : rawAfterLastLine).join('\n'));
104
117
  if (error !== undefined) {
105
118
  cause.error = error;
106
119
  }
120
+ // No line of its own, and a Node error after the last line of the daemon before it: Node stopped
121
+ // this daemon before its code could write anything, for example on a module that did not load.
122
+ if (0 === ours.length && error !== undefined) {
123
+ cause.end = DaemonEnd.START_FAILED;
124
+ cause.startupError = DaemonStartupError.BEFORE_LOG;
125
+ }
107
126
  return cause;
108
127
  }
109
128
  /** How much of the end of a daemon log the next start reads. The last beats and a stack fit. */
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { DaemonEnd } from '@reticlehq/core';
21
21
  import { DAEMON_LOG_TRUNCATED_EVENT, guardDaemonLog } from './log-cap.js';
22
+ import { DAEMON_START_FAILED_EVENT } from './startup-failure.js';
22
23
  /** The event name a beat carries. One `grep` separates liveness from everything else in the file. */
23
24
  export const DAEMON_HEARTBEAT_EVENT = 'reticle_daemon_alive';
24
25
  /** Written by installExitTrace when the process exits through Node. */
@@ -132,8 +133,12 @@ export function classifyDaemonLife(lines, nowMs) {
132
133
  };
133
134
  }
134
135
  const last = beats.at(-1);
135
- if (last === undefined)
136
- return { end: DaemonEnd.UNKNOWN };
136
+ if (last === undefined) {
137
+ // A daemon that logged a failed start never beat. Without this branch its death was `unknown`,
138
+ // which said "no evidence" about a log that held the cause (#2043).
139
+ const failed = lines.some((l) => l.event === DAEMON_START_FAILED_EVENT);
140
+ return { end: failed ? DaemonEnd.START_FAILED : DaemonEnd.UNKNOWN };
141
+ }
137
142
  const everyMs = 'number' === typeof last.everyMs ? last.everyMs : DAEMON_HEARTBEAT_MS;
138
143
  const silentForMs = nowMs - last.t;
139
144
  return silentForMs > everyMs * MISSED_BEATS_TOLERATED
@@ -1,5 +1,33 @@
1
+ import { DaemonStartupError } from '@reticlehq/core';
1
2
  /** The structured log event emitted when the daemon child cannot start. */
2
3
  export declare const DAEMON_START_FAILED_EVENT = "reticle_daemon_start_failed";
4
+ type LogFn = (event: string, fields: Record<string, unknown>) => void;
5
+ /**
6
+ * Write a failed start to the daemon log, before the process exits (#2043).
7
+ *
8
+ * The line has three fields. `error` is the message, for the person who reads the log and for
9
+ * `readDaemonStartupCause`. `code` is the Node error code, read from the error and not from the
10
+ * message. `stage` is where the start failed: a failed bind is always `listen`, whatever the caller
11
+ * gave. The next start reads `code` and `stage` into `daemon_died_unclean`. The message stays here.
12
+ */
13
+ export declare function recordStartupFailure(log: LogFn, error: unknown, stage: DaemonStartupError): void;
14
+ /** The parts of `process` that the startup trap uses. */
15
+ export interface StartupTrapTarget {
16
+ prependListener(event: 'uncaughtException' | 'unhandledRejection', handler: (value: unknown) => void): void;
17
+ off(event: 'uncaughtException' | 'unhandledRejection', handler: (value: unknown) => void): void;
18
+ }
19
+ /**
20
+ * Write a throw or a rejection that stops the start to the daemon log.
21
+ *
22
+ * Before the daemon is ready, the process crash guard owns these two events. It writes one sentence
23
+ * to stderr and exits, and that sentence has no event name and no code. So a daemon that died in
24
+ * its first 100 ms left a log that no reader could classify. This trap runs FIRST (it is prepended)
25
+ * and writes one structured line. It does not exit: the crash guard still does that.
26
+ *
27
+ * Returns a disposer. Call it when the daemon is ready, because from then on the daemon's own
28
+ * resilience handlers own these events, and a crash is no longer a failed start.
29
+ */
30
+ export declare function installStartupTrap(proc: StartupTrapTarget, log: LogFn): () => void;
3
31
  /**
4
32
  * Return the most recent startup cause written by this launch attempt.
5
33
  *
@@ -9,3 +37,4 @@ export declare const DAEMON_START_FAILED_EVENT = "reticle_daemon_start_failed";
9
37
  * unavailable logs are best-effort misses; the caller still has its generic fallback diagnosis.
10
38
  */
11
39
  export declare function readDaemonStartupCause(path: string, sinceMs: number): string | undefined;
40
+ export {};
@@ -1,6 +1,68 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import { DaemonStartupError, NODE_ERROR_CODE } from '@reticlehq/core';
2
3
  /** The structured log event emitted when the daemon child cannot start. */
3
4
  export const DAEMON_START_FAILED_EVENT = 'reticle_daemon_start_failed';
5
+ /** The `syscall` that Node puts on the error of a failed bind. */
6
+ const LISTEN_SYSCALL = 'listen';
7
+ /** The Node error code on `error`, or undefined. Only a value that has the shape of a code. */
8
+ function nodeErrorCode(error) {
9
+ if ('object' !== typeof error || null === error) {
10
+ return undefined;
11
+ }
12
+ const code = error.code;
13
+ return 'string' === typeof code && NODE_ERROR_CODE.test(code) ? code : undefined;
14
+ }
15
+ function isListenError(error) {
16
+ if ('object' !== typeof error || null === error) {
17
+ return false;
18
+ }
19
+ return LISTEN_SYSCALL === error.syscall;
20
+ }
21
+ /**
22
+ * Write a failed start to the daemon log, before the process exits (#2043).
23
+ *
24
+ * The line has three fields. `error` is the message, for the person who reads the log and for
25
+ * `readDaemonStartupCause`. `code` is the Node error code, read from the error and not from the
26
+ * message. `stage` is where the start failed: a failed bind is always `listen`, whatever the caller
27
+ * gave. The next start reads `code` and `stage` into `daemon_died_unclean`. The message stays here.
28
+ */
29
+ export function recordStartupFailure(log, error, stage) {
30
+ const code = nodeErrorCode(error);
31
+ log(DAEMON_START_FAILED_EVENT, {
32
+ stage: isListenError(error) ? DaemonStartupError.LISTEN : stage,
33
+ ...(code === undefined ? {} : { code }),
34
+ error: error instanceof Error ? error.message : String(error),
35
+ });
36
+ }
37
+ /**
38
+ * Write a throw or a rejection that stops the start to the daemon log.
39
+ *
40
+ * Before the daemon is ready, the process crash guard owns these two events. It writes one sentence
41
+ * to stderr and exits, and that sentence has no event name and no code. So a daemon that died in
42
+ * its first 100 ms left a log that no reader could classify. This trap runs FIRST (it is prepended)
43
+ * and writes one structured line. It does not exit: the crash guard still does that.
44
+ *
45
+ * Returns a disposer. Call it when the daemon is ready, because from then on the daemon's own
46
+ * resilience handlers own these events, and a crash is no longer a failed start.
47
+ */
48
+ export function installStartupTrap(proc, log) {
49
+ let written = false;
50
+ const trap = (stage) => (value) => {
51
+ if (written) {
52
+ return;
53
+ }
54
+ written = true;
55
+ recordStartupFailure(log, value, stage);
56
+ };
57
+ const onException = trap(DaemonStartupError.UNCAUGHT_EXCEPTION);
58
+ const onRejection = trap(DaemonStartupError.UNHANDLED_REJECTION);
59
+ proc.prependListener('uncaughtException', onException);
60
+ proc.prependListener('unhandledRejection', onRejection);
61
+ return () => {
62
+ proc.off('uncaughtException', onException);
63
+ proc.off('unhandledRejection', onRejection);
64
+ };
65
+ }
4
66
  /**
5
67
  * Return the most recent startup cause written by this launch attempt.
6
68
  *
@@ -13,6 +13,7 @@ declare const HandedOverSchema: z.ZodObject<{
13
13
  pid: z.ZodNumber;
14
14
  url: z.ZodString;
15
15
  appDir: z.ZodString;
16
+ bridgePort: z.ZodOptional<z.ZodNumber>;
16
17
  }, z.core.$strip>;
17
18
  export type HandedOver = z.infer<typeof HandedOverSchema>;
18
19
  /**
@@ -32,6 +33,14 @@ export declare function handedOverUrl(dir: string, appDir: string, deps: {
32
33
  alive: (pid: number) => boolean;
33
34
  answers: (url: string) => Promise<boolean>;
34
35
  }): Promise<string | undefined>;
36
+ /**
37
+ * True when the live server `init` handed over for `appDir` was started for a daemon port other than
38
+ * `bridgePort`. Attaching to it leaves the page dialling the old port, even when no daemon on that
39
+ * port is left to retire. A record with no port gives false, as before.
40
+ */
41
+ export declare function handedOverToOtherPort(dir: string, appDir: string, bridgePort: number, deps: {
42
+ alive: (pid: number) => boolean;
43
+ }): boolean;
35
44
  /**
36
45
  * Stop the server `init` handed over for `appDir`, and forget it. True when one was stopped.
37
46
  *
@@ -34,6 +34,11 @@ const HandedOverSchema = z.object({
34
34
  pid: z.number().int().positive(),
35
35
  url: z.string().min(1),
36
36
  appDir: z.string().min(1),
37
+ /**
38
+ * The daemon port the server was started for. Absent in a record from an older build. Reticle's
39
+ * static server adds a snippet that dials this port, so it cannot follow a port move.
40
+ */
41
+ bridgePort: z.number().int().positive().optional(),
37
42
  });
38
43
  /**
39
44
  * Written when `init` leaves a server running, so the next `init` can attach to it.
@@ -70,6 +75,18 @@ export async function handedOverUrl(dir, appDir, deps) {
70
75
  return undefined;
71
76
  return (await deps.answers(record.url)) ? record.url : undefined;
72
77
  }
78
+ /**
79
+ * True when the live server `init` handed over for `appDir` was started for a daemon port other than
80
+ * `bridgePort`. Attaching to it leaves the page dialling the old port, even when no daemon on that
81
+ * port is left to retire. A record with no port gives false, as before.
82
+ */
83
+ export function handedOverToOtherPort(dir, appDir, bridgePort, deps) {
84
+ const record = readRecord(join(dir, recordName(appDir)));
85
+ if (undefined === record || record.appDir !== appDir || !deps.alive(record.pid)) {
86
+ return false;
87
+ }
88
+ return undefined !== record.bridgePort && record.bridgePort !== bridgePort;
89
+ }
73
90
  /**
74
91
  * Stop the server `init` handed over for `appDir`, and forget it. True when one was stopped.
75
92
  *
@@ -8,8 +8,11 @@
8
8
  * starts `npm run dev`, and the rest of setup (wait, open, prove the connect) runs unchanged.
9
9
  *
10
10
  * It is run as its own process (`node static-page-server.js --dir <app>`), not inside init, because it
11
- * has to outlive init exactly like any handed-over dev server. It imports nothing but Node, so the
12
- * built file runs on its own.
11
+ * has to outlive init exactly like any handed-over dev server. It imports only Node and
12
+ * `@reticlehq/init`, so the built file runs on its own.
13
+ *
14
+ * It adds the connect snippet to each HTML page it serves, so init does not edit the file the user
15
+ * hands over (#1544). The file on disk stays byte-identical.
13
16
  *
14
17
  * Loopback only, on both families: a page served to the LAN would publish the pairing token the
15
18
  * snippet carries.
@@ -21,9 +24,16 @@ export interface StaticPageServer {
21
24
  readonly url: string;
22
25
  close(): Promise<void>;
23
26
  }
27
+ /**
28
+ * The connect literal for the page in `root`: the bridge port, and the project id its `.reticle.json`
29
+ * names. The same literal init wrote into the page before #1544.
30
+ */
31
+ export declare function staticPageConnect(root: string, bridgePort: number): string;
24
32
  export declare function startStaticPageServer(opts: {
25
33
  root: string;
26
34
  port: number;
35
+ /** The connect literal to add to each HTML page. Absent: pages are served as written. */
36
+ connect?: string | undefined;
27
37
  }): Promise<StaticPageServer>;
28
38
  /**
29
39
  * The command setup runs for a page with no dev server of its own, or undefined when this is not
@@ -32,4 +42,6 @@ export declare function startStaticPageServer(opts: {
32
42
  export declare function staticPageDevCommand(opts: {
33
43
  appDir: string;
34
44
  isStaticPage: boolean;
45
+ /** The daemon port the added snippet dials. Absent: the page is served as written. */
46
+ bridgePort?: number | undefined;
35
47
  }): string | undefined;
@@ -8,22 +8,29 @@
8
8
  * starts `npm run dev`, and the rest of setup (wait, open, prove the connect) runs unchanged.
9
9
  *
10
10
  * It is run as its own process (`node static-page-server.js --dir <app>`), not inside init, because it
11
- * has to outlive init exactly like any handed-over dev server. It imports nothing but Node, so the
12
- * built file runs on its own.
11
+ * has to outlive init exactly like any handed-over dev server. It imports only Node and
12
+ * `@reticlehq/init`, so the built file runs on its own.
13
+ *
14
+ * It adds the connect snippet to each HTML page it serves, so init does not edit the file the user
15
+ * hands over (#1544). The file on disk stays byte-identical.
13
16
  *
14
17
  * Loopback only, on both families: a page served to the LAN would publish the pairing token the
15
18
  * snippet carries.
16
19
  */
17
- import { existsSync, readFile, statSync } from 'node:fs';
20
+ import { existsSync, readFile, readFileSync, statSync } from 'node:fs';
18
21
  import { createServer } from 'node:http';
19
22
  import { extname, join, resolve, sep } from 'node:path';
20
23
  import { fileURLToPath, pathToFileURL } from 'node:url';
24
+ import { connectArg, projectIdOf, withStaticSnippet } from '@reticlehq/init';
21
25
  export const STATIC_PAGE_INDEX = 'index.html';
22
26
  /** Where the search for a free port starts when none is given — clear of Vite's 5173 run. */
23
27
  export const STATIC_PAGE_FIRST_PORT = 5500;
24
28
  const PORT_SEARCH_SPAN = 50;
25
29
  const DIR_FLAG = '--dir';
26
30
  const PORT_FLAG = '--port';
31
+ const BRIDGE_PORT_FLAG = '--bridge-port';
32
+ const PROJECT_CONFIG = '.reticle.json';
33
+ const HTML_EXTENSION = '.html';
27
34
  const LOOPBACK_V4 = '127.0.0.1';
28
35
  const LOOPBACK_V6 = '::1';
29
36
  const ANNOUNCED_HOST = 'localhost';
@@ -54,8 +61,9 @@ function fileFor(root, rawUrl) {
54
61
  return null;
55
62
  }
56
63
  const target = resolve(root, `.${path}`);
57
- if (target !== root && !target.startsWith(`${root}${sep}`))
64
+ if (target !== root && !target.startsWith(`${root}${sep}`)) {
58
65
  return null;
66
+ }
59
67
  const file = path.endsWith('/') ? join(target, STATIC_PAGE_INDEX) : target;
60
68
  try {
61
69
  return statSync(file).isFile() ? file : null;
@@ -64,6 +72,27 @@ function fileFor(root, rawUrl) {
64
72
  return null;
65
73
  }
66
74
  }
75
+ /**
76
+ * The connect literal for the page in `root`: the bridge port, and the project id its `.reticle.json`
77
+ * names. The same literal init wrote into the page before #1544.
78
+ */
79
+ export function staticPageConnect(root, bridgePort) {
80
+ let config;
81
+ try {
82
+ config = readFileSync(join(root, PROJECT_CONFIG), 'utf8');
83
+ }
84
+ catch {
85
+ config = null;
86
+ }
87
+ return connectArg(bridgePort, projectIdOf(config));
88
+ }
89
+ /** The body to send: an HTML page gets the connect snippet, unless it already connects. */
90
+ function servedBody(file, body, connect) {
91
+ if (undefined === connect || HTML_EXTENSION !== extname(file).toLowerCase()) {
92
+ return body;
93
+ }
94
+ return withStaticSnippet(body.toString('utf8'), connect) ?? body;
95
+ }
67
96
  function listen(server, port, host) {
68
97
  return new Promise((ok, fail) => {
69
98
  server.once('error', fail);
@@ -93,7 +122,7 @@ export async function startStaticPageServer(opts) {
93
122
  'content-type': CONTENT_TYPES[extname(file).toLowerCase()] ?? FALLBACK_TYPE,
94
123
  'cache-control': 'no-store',
95
124
  });
96
- res.end(body);
125
+ res.end(servedBody(file, body, opts.connect));
97
126
  });
98
127
  };
99
128
  const v4 = createServer(handler);
@@ -121,8 +150,9 @@ export async function startStaticPageServer(opts) {
121
150
  url: `http://${ANNOUNCED_HOST}:${String(bound)}/`,
122
151
  close: async () => {
123
152
  await closeServer(v4);
124
- if (hasV6)
153
+ if (hasV6) {
125
154
  await closeServer(v6);
155
+ }
126
156
  },
127
157
  };
128
158
  }
@@ -131,10 +161,12 @@ export async function startStaticPageServer(opts) {
131
161
  * that page. Quoted so an app path with a space survives the shell.
132
162
  */
133
163
  export function staticPageDevCommand(opts) {
134
- if (!opts.isStaticPage || !existsSync(join(opts.appDir, STATIC_PAGE_INDEX)))
164
+ if (!opts.isStaticPage || !existsSync(join(opts.appDir, STATIC_PAGE_INDEX))) {
135
165
  return undefined;
166
+ }
136
167
  const script = fileURLToPath(import.meta.url).replace(/\.ts$/, '.js');
137
- return `"${process.execPath}" "${script}" ${DIR_FLAG} "${opts.appDir}"`;
168
+ const bridge = undefined === opts.bridgePort ? '' : ` ${BRIDGE_PORT_FLAG} ${String(opts.bridgePort)}`;
169
+ return `"${process.execPath}" "${script}" ${DIR_FLAG} "${opts.appDir}"${bridge}`;
138
170
  }
139
171
  function flag(argv, name) {
140
172
  const at = argv.indexOf(name);
@@ -144,7 +176,9 @@ function flag(argv, name) {
144
176
  if (undefined !== process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
145
177
  const dir = flag(process.argv, DIR_FLAG) ?? process.cwd();
146
178
  const port = Number(flag(process.argv, PORT_FLAG) ?? STATIC_PAGE_FIRST_PORT);
147
- startStaticPageServer({ root: dir, port }).then((server) => process.stdout.write(`serving ${dir}\n Local: ${server.url}\n`), (err) => {
179
+ const bridgePort = flag(process.argv, BRIDGE_PORT_FLAG);
180
+ const connect = undefined === bridgePort ? undefined : staticPageConnect(dir, Number(bridgePort));
181
+ startStaticPageServer({ root: dir, port, connect }).then((server) => process.stdout.write(`serving ${dir}\n Local: ${server.url}\n`), (err) => {
148
182
  process.stderr.write(`static page server: ${String(err)}\n`);
149
183
  process.exit(1);
150
184
  });
@@ -242,12 +242,18 @@ firstFlow, agentsBlock) {
242
242
  const devCommand = staticPageDevCommand({
243
243
  appDir: context.appDir,
244
244
  isStaticPage: Framework.HTML === context.framework,
245
+ // The server adds the connect snippet to the page, so init left the file alone (#1544).
246
+ bridgePort: port,
245
247
  });
246
248
  return undefined === devCommand ? {} : { devCommand, devCommandIsStaticPage: true };
247
249
  })(),
248
250
  ...(undefined === parsed.url ? {} : { suppliedUrl: parsed.url }),
249
251
  // So setup can tell which port the dev server will take, before it starts one (#1377).
250
252
  framework: context.framework,
253
+ // So a dev-server stop names the Vite step init could not do (#1263).
254
+ ...(undefined === context.pendingViteConfig
255
+ ? {}
256
+ : { pendingViteConfig: context.pendingViteConfig }),
251
257
  }, (line) => io.print(line))
252
258
  .then(async (outcome) => {
253
259
  if (result.outcome !== undefined) {
@@ -56,9 +56,10 @@ export declare class OwnedDevServer {
56
56
  *
57
57
  * Only `unref` is needed: the output is the supervisor's, so no pipe of ours holds the event loop
58
58
  * (`init` once printed "setup complete" and hung on one). `url` is recorded beside the log, so the
59
- * next `init` attaches to this server rather than starting a second one beside it.
59
+ * next `init` attaches to this server rather than starting a second one beside it. `bridgePort`
60
+ * is recorded too, so the next `init` on another port restarts this server instead.
60
61
  */
61
- handOver(url?: string): void;
62
+ handOver(url?: string, bridgePort?: number): void;
62
63
  stop(): void;
63
64
  }
64
65
  /** Run a command and return what it printed, or '' if it failed to finish within `timeoutMs`. */
@@ -175,14 +175,20 @@ export class OwnedDevServer {
175
175
  *
176
176
  * Only `unref` is needed: the output is the supervisor's, so no pipe of ours holds the event loop
177
177
  * (`init` once printed "setup complete" and hung on one). `url` is recorded beside the log, so the
178
- * next `init` attaches to this server rather than starting a second one beside it.
178
+ * next `init` attaches to this server rather than starting a second one beside it. `bridgePort`
179
+ * is recorded too, so the next `init` on another port restarts this server instead.
179
180
  */
180
- handOver(url) {
181
+ handOver(url, bridgePort) {
181
182
  this.handedOver = true;
182
183
  this.child?.unref();
183
184
  const pid = this.child?.pid;
184
185
  if (undefined !== url && undefined !== pid && undefined !== this.cwd) {
185
- recordHandOver(this.logDir, { pid, url, appDir: this.cwd });
186
+ recordHandOver(this.logDir, {
187
+ pid,
188
+ url,
189
+ appDir: this.cwd,
190
+ ...(undefined === bridgePort ? {} : { bridgePort }),
191
+ });
186
192
  }
187
193
  }
188
194
  stop() {
@@ -8,5 +8,13 @@ export interface Progress {
8
8
  readonly urlSuppliedByCaller: boolean;
9
9
  readonly url?: string | undefined;
10
10
  readonly devCommand?: string | undefined;
11
+ /** The Vite config that init could not patch and left as a manual step. */
12
+ readonly pendingViteConfig?: string | undefined;
11
13
  }
14
+ /**
15
+ * Said when the dev server stops after init left the Vite config unpatched (#1263).
16
+ *
17
+ * The dev server loads that config, so the step init already reported is the first cause to check.
18
+ */
19
+ export declare const pendingViteConfigCause: (config: string) => string;
12
20
  export declare function remainingSteps(p: Progress): string[];
@@ -6,6 +6,13 @@
6
6
  * worked — it should pick up at the step that did not. Ordered, and only ever the REMAINING steps.
7
7
  */
8
8
  import { FEEDBACK_KIND_CHOICES } from '@reticlehq/core';
9
+ /**
10
+ * Said when the dev server stops after init left the Vite config unpatched (#1263).
11
+ *
12
+ * The dev server loads that config, so the step init already reported is the first cause to check.
13
+ */
14
+ export const pendingViteConfigCause = (config) => `init could not patch ${config}, and its ⚠ Vite plugin step is not done. ` +
15
+ 'The dev server loads that config, so that step is the likely cause. Do it first.';
9
16
  const DOCS_INDEX = 'https://docs.reticle.sh/llms.txt';
10
17
  /**
11
18
  * The shell form on purpose, not `reticle_session { action: "feedback" }`.
@@ -21,6 +28,9 @@ export function remainingSteps(p) {
21
28
  if (!p.initDone) {
22
29
  steps.push('Run `npx @reticlehq/server@latest init` here, and fix every ⚠ it reports.');
23
30
  }
31
+ if (undefined !== p.pendingViteConfig) {
32
+ steps.push(`Do the ⚠ Vite plugin step that init printed: add \`reticle()\` to the plugins in ${p.pendingViteConfig}, or to the config that the dev server loads.`);
33
+ }
24
34
  if (!p.devServerUp && !p.urlSuppliedByCaller) {
25
35
  steps.push(`Start the dev server yourself: ${p.devCommand ?? 'the dev script in package.json'}, then open the app in a browser.`);
26
36
  }
@@ -158,6 +158,8 @@ export interface SetupInput {
158
158
  readonly suppliedUrl?: string | undefined;
159
159
  /** The port the dev command will take, when it is known. See probe/dev-port.ts. */
160
160
  readonly devPort?: number | undefined;
161
+ /** The Vite config init left for a manual step. A dev-server stop names it (#1263). */
162
+ readonly pendingViteConfig?: string | undefined;
161
163
  /**
162
164
  * The caller's own budget for the connect wait, when they named one.
163
165
  *
@@ -26,7 +26,7 @@ const WINDOWS_QUIET_MEANS_HUNG_MS = 3 * 60_000;
26
26
  const WINDOWS_QUIET_MEANS_HUNG_MS_APPLIES = 'win32' === process.platform;
27
27
  import { liveSessionOfProject, pickSession } from './session-pick.js';
28
28
  import { readPage, describePage, findingBeforeOpen, PageFinding, } from './probe/page-probe.js';
29
- import { remainingSteps } from './remaining-steps.js';
29
+ import { pendingViteConfigCause, remainingSteps } from './remaining-steps.js';
30
30
  import { splitDevPortNote } from './probe/dev-port.js';
31
31
  import { AppShape, isDesktop, policyFor } from './desktop-shape.js';
32
32
  import { InitStop } from '@reticlehq/core/telemetry';
@@ -82,6 +82,7 @@ const asProgress = (input, o) => ({
82
82
  urlSuppliedByCaller: undefined !== input.suppliedUrl,
83
83
  ...(undefined === o.url ? {} : { url: o.url }),
84
84
  ...(undefined === input.devCommand ? {} : { devCommand: input.devCommand }),
85
+ ...(undefined === input.pendingViteConfig ? {} : { pendingViteConfig: input.pendingViteConfig }),
85
86
  });
86
87
  /** The connect step a web page stopped at. A bundle-connect page's HTML never shows the SDK. */
87
88
  const connectStop = (finding, htmlCarriesSdk) => {
@@ -215,6 +216,10 @@ export async function runSetupPhases(input, fx) {
215
216
  if (WaitVerdict.DEAD === verdict) {
216
217
  const busy = portBusyMessage(fx.devServerOutput());
217
218
  note(busy ?? devServerDiedMessage(fx.devServerOutput()));
219
+ // A busy port is its own cause. Any other exit can come from the config init left alone.
220
+ if (undefined === busy && undefined !== input.pendingViteConfig) {
221
+ note(pendingViteConfigCause(input.pendingViteConfig));
222
+ }
218
223
  const step = undefined === busy ? InitStop.DEV_SERVER_EXITED : InitStop.PORT_BUSY;
219
224
  return stop(input, SetupPhase.DEV_SERVER, { stop: step }, notes);
220
225
  }
@@ -231,6 +236,9 @@ export async function runSetupPhases(input, fx) {
231
236
  ? `The dev server did not become ready${within}. `
232
237
  : `The dev server stopped making progress before ${watching} became ready. `) +
233
238
  'Check its log, increase --timeout for a slow build, or pass --url with the address it serves.');
239
+ if (undefined !== input.pendingViteConfig) {
240
+ note(pendingViteConfigCause(input.pendingViteConfig));
241
+ }
234
242
  const step = undefined === watching ? InitStop.NO_URL : InitStop.NOT_READY;
235
243
  return stop(input, SetupPhase.DEV_SERVER, { stop: step }, notes);
236
244
  }
@@ -26,7 +26,7 @@ import { spawnSync } from 'node:child_process';
26
26
  import { isAlive, reticleStateHome } from '../daemon/daemon.js';
27
27
  import { readDevServers } from '../daemon/dev-servers.js';
28
28
  import { connectedAppUrl, urlOfExistingApp } from './probe/existing-app.js';
29
- import { handedOverUrl, stopHandedOver } from './bringup/dev-server-files.js';
29
+ import { handedOverToOtherPort, handedOverUrl, stopHandedOver, } from './bringup/dev-server-files.js';
30
30
  import { listSessions, OwnedDevServer, probePage } from './node-effects.js';
31
31
  import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
32
32
  import { daemonSkew, summarizeStatus } from '../cli/launch/cli-launch.js';
@@ -214,7 +214,10 @@ export async function runSetupCommand(input, print) {
214
214
  }
215
215
  // And the dev server a previous init left running, which may have read the old port once at
216
216
  // startup (Next does) — so the attach below would keep a page dialling the daemon just stopped.
217
- if (0 < retired.length && (await restartHandedOverServer(input.appDir))) {
217
+ // Its record names the port it was started for, so this works when the old daemon is gone too.
218
+ const movedUnderServer = 0 < retired.length ||
219
+ handedOverToOtherPort(reticleStateHome(), input.appDir, input.bridgePort, { alive: isAlive });
220
+ if (movedUnderServer && (await restartHandedOverServer(input.appDir))) {
218
221
  print(`stopped the dev server a previous init started, so it restarts on port ${String(input.bridgePort)}.`);
219
222
  }
220
223
  // Before the app is booted, because the app's whole job from here is to dial this port. Without
@@ -364,7 +367,7 @@ export async function runSetupCommand(input, print) {
364
367
  }, effects);
365
368
  // The app stays up only when there is something worth watching.
366
369
  if (outcome.ok) {
367
- server.handOver(outcome.url);
370
+ server.handOver(outcome.url, input.bridgePort);
368
371
  // Its output is on a file now, not this terminal, so say where: a compile error that used to
369
372
  // scroll past here is the first thing somebody will look for.
370
373
  const log = server.logPath();