@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,13 +1,26 @@
1
1
  /**
2
- * Install the lease's uncaught-error listener. Call BEFORE the first navigation — the listener
3
- * timing is the whole fix: an import-time throw lands during `goto`, and one attached after it
4
- * reads a broken page as healthy. Forwarded with the lease id, because the session for this lease
5
- * usually does not exist yet, so the sink buffers.
2
+ * The page-error side of a lease: listen for the throws the SDK will never see, and tell the
3
+ * wiring when the lease is gone.
4
+ *
5
+ * An app whose module graph throws during the first navigation fires its `error` event while the
6
+ * page still has no Reticle SDK in it — the zero-install reader is only evaluated after `goto`
7
+ * resolves — so nothing on the page records it, and a blank page reported as a bare console-error
8
+ * count (#1966). The server's Playwright handle is attached before the navigation, so it is the
9
+ * one witness that cannot miss the throw. This module owns that wiring for `BrowserPool`; the
10
+ * journal that buffers and journals what arrives lives in `page-error-journal.ts`.
6
11
  */
7
- export function listenForPageErrors(page, leaseId, hooks) {
12
+ import { RETICLE_EARLY_CAPTURE } from '@reticlehq/core';
13
+ /**
14
+ * Install the lease's uncaught-error listener and its early console queue. Call BEFORE the first
15
+ * navigation — the timing is the whole fix: an import-time throw lands during `goto`, and one
16
+ * attached after it reads a broken page as healthy. Forwarded with the lease id, because the
17
+ * session for this lease usually does not exist yet, so the sink buffers.
18
+ */
19
+ export async function listenForPageErrors(page, leaseId, hooks) {
8
20
  page.onPageError?.((error) => {
9
21
  hooks?.onPageError?.(leaseId, error);
10
22
  });
23
+ await page.addInitScript?.(queueEarlyConsole, EARLY_CAPTURE_ARG);
11
24
  }
12
25
  /**
13
26
  * Report a closed lease — manual release, reaper and crash reclaim all funnel through the pool's
@@ -16,3 +29,132 @@ export function listenForPageErrors(page, leaseId, hooks) {
16
29
  export function reportLeaseClosed(hooks, leaseId) {
17
30
  hooks?.onLeaseClosed?.(leaseId);
18
31
  }
32
+ /** How many console calls and error events a page keeps for an SDK that has not installed yet. */
33
+ export const EARLY_CAPTURE_LIMIT = 200;
34
+ /**
35
+ * The argument for `queueEarlyConsole`. Every driven page gets the same one: a pool lease, and
36
+ * `verify`'s own page. It looks for the SDK every `pollMs`, for at most `maxPolls` looks (30 s).
37
+ */
38
+ export const EARLY_CAPTURE_ARG = {
39
+ key: RETICLE_EARLY_CAPTURE,
40
+ limit: EARLY_CAPTURE_LIMIT,
41
+ pollMs: 50,
42
+ maxPolls: 600,
43
+ };
44
+ /**
45
+ * Init script: keep the console errors and warnings a page logs before the SDK installs.
46
+ *
47
+ * An app connects the SDK from a module script or a bundle, and both run after the page's own
48
+ * classic scripts. An error such a script logs while the page loads was lost, so "no console
49
+ * errors" passed on a page that logged one on every load. Playwright's own console event is not a
50
+ * substitute: it also reports messages the browser writes itself (a failed WebSocket, which is how
51
+ * the SDK's own first dial can fail), and the SDK's console channel has never counted those.
52
+ *
53
+ * So this keeps the RAW calls and events. Once the SDK has wrapped the console, this hands them to
54
+ * it: each call goes through the console again and each event is dispatched again, so the SDK's own
55
+ * handlers report them and the meaning of "console error" stays the SDK's. The SDK needs no code for
56
+ * this, so a page that Reticle does not drive downloads nothing for it. Entries the bound did not
57
+ * keep are declared with one console warning. Must stay self-contained: Playwright serialises the
58
+ * function source into the page.
59
+ */
60
+ export function queueEarlyConsole(arg) {
61
+ const win = globalThis.window;
62
+ if (win === undefined || win[arg.key] !== undefined) {
63
+ return;
64
+ }
65
+ const capture = { queue: [], dropped: 0 };
66
+ win[arg.key] = capture;
67
+ const keep = (item) => {
68
+ if (win[arg.key] !== capture) {
69
+ return;
70
+ }
71
+ if (capture.queue.length < arg.limit) {
72
+ capture.queue.push(item);
73
+ }
74
+ else {
75
+ capture.dropped += 1;
76
+ }
77
+ };
78
+ const mine = {};
79
+ for (const method of ['error', 'warn']) {
80
+ const original = win.console[method];
81
+ const wrapper = (...args) => {
82
+ keep({ method, args });
83
+ original.apply(win.console, args);
84
+ };
85
+ mine[method] = wrapper;
86
+ win.console[method] = wrapper;
87
+ }
88
+ win.addEventListener('error', (event) => keep({ event }), true);
89
+ win.addEventListener('unhandledrejection', (event) => keep({ event }), true);
90
+ // A copy of a kept event: an event that has been dispatched once is copied, not sent again.
91
+ const copy = (event) => {
92
+ if ('reason' in event) {
93
+ return new win.PromiseRejectionEvent('unhandledrejection', {
94
+ promise: event['promise'],
95
+ reason: event['reason'],
96
+ });
97
+ }
98
+ const init = {};
99
+ for (const field of ['message', 'filename', 'lineno', 'colno', 'error']) {
100
+ if (event[field] !== undefined) {
101
+ init[field] = event[field];
102
+ }
103
+ }
104
+ return new win.ErrorEvent('error', init);
105
+ };
106
+ // A `<script>`, `<img>` or `<link>` that failed to load dispatches a plain `Event` on its element,
107
+ // and only the element names the URL. A copy on `window` loses it, and the SDK reported an error
108
+ // with no message, no source and line 0 (#2220). So such an event is sent again from the same
109
+ // element, and the SDK's capture listener on `window` reads the tag and the URL. A listener added
110
+ // after the SDK's stops it on `window`, so the page's own handlers on the element do not run twice.
111
+ // An element that left the page cannot reach `window`, so it gets the copy.
112
+ const resend = (event) => {
113
+ const target = event['target'];
114
+ if ('message' in event || 'reason' in event || true !== target?.isConnected) {
115
+ return false;
116
+ }
117
+ const again = new win.Event('error');
118
+ const stop = (seen) => {
119
+ if (seen === again) {
120
+ again.stopPropagation();
121
+ }
122
+ };
123
+ win.addEventListener('error', stop, true);
124
+ try {
125
+ target.dispatchEvent?.(again);
126
+ }
127
+ finally {
128
+ win.removeEventListener('error', stop, true);
129
+ }
130
+ return true;
131
+ };
132
+ let polls = 0;
133
+ const timer = win.setInterval(() => {
134
+ polls += 1;
135
+ // The SDK has installed when the console is no longer this script's own wrapper.
136
+ const installed = win.console.error !== mine.error;
137
+ if (!installed && polls < arg.maxPolls) {
138
+ return;
139
+ }
140
+ win.clearInterval(timer);
141
+ win[arg.key] = null;
142
+ if (!installed) {
143
+ return;
144
+ }
145
+ for (const item of capture.queue) {
146
+ if ('method' in item) {
147
+ win.console[item['method']](...item['args']);
148
+ }
149
+ else {
150
+ const event = item['event'];
151
+ if (!resend(event)) {
152
+ win.dispatchEvent(copy(event));
153
+ }
154
+ }
155
+ }
156
+ if (capture.dropped > 0) {
157
+ win.console.warn(`reticle: ${String(capture.dropped)} early console entries were not kept`);
158
+ }
159
+ }, arg.pollMs);
160
+ }
@@ -25,7 +25,7 @@
25
25
  * only the redundant second copy is cost, not wrong evidence.
26
26
  */
27
27
  import { type ReticleEvent } from '@reticlehq/core';
28
- import type { PooledPageError } from './pool-contract.js';
28
+ import type { PooledDialog, PooledPageError } from './pool-contract.js';
29
29
  /** The session surface the journal needs. The real `Session` satisfies this structurally. */
30
30
  export interface JournalableSession {
31
31
  readonly id: string;
@@ -43,6 +43,12 @@ export interface PageErrorJournal {
43
43
  onLeaseClosed(leaseId: string): void;
44
44
  /** The bridge hook — register with `bridge.attachSessionEnd`. */
45
45
  onSessionEnd(sessionId: string): void;
46
+ /**
47
+ * The pool's dialog sink — hand it to `BrowserPoolOptions.onDialog`. Journals the dismissal as
48
+ * `dialog.opened` on the lease's session, so a verdict reads it as the SDK's own answer. A dialog
49
+ * before the session connects is dropped: no action window can hold it.
50
+ */
51
+ onPoolDialog(leaseId: string, dialog: Pick<PooledDialog, 'kind' | 'message'>): void;
46
52
  }
47
53
  /**
48
54
  * Fold the two spellings of one throw into one identity. Chromium prefixes a window-reported
@@ -25,6 +25,16 @@
25
25
  * only the redundant second copy is cost, not wrong evidence.
26
26
  */
27
27
  import { CONSOLE_EVENT, EventType, RETICLE_URL_PARAM } from '@reticlehq/core';
28
+ /**
29
+ * What a dismissal answers, per dialog kind the wire knows. The same replies the SDK gives
30
+ * (`observers/dialogs.ts`): `confirm` is "no", `prompt` is no value, `alert` has no answer.
31
+ * A kind not in this map (`beforeunload`) is not a `dialog.opened` and is not reported.
32
+ */
33
+ const DISMISSED_ANSWER = {
34
+ alert: {},
35
+ confirm: { answered: false },
36
+ prompt: { answered: null },
37
+ };
28
38
  /** How many errors wait per lease for a session that may never connect. Older ones give way. */
29
39
  const MAX_BUFFERED_PER_LEASE = 8;
30
40
  /** How many recent error identities a session tracks — the dedupe memory for both sources. */
@@ -177,5 +187,22 @@ export function makePageErrorJournal(deps) {
177
187
  onSessionEnd(sessionId) {
178
188
  seen.delete(sessionId);
179
189
  },
190
+ onPoolDialog(leaseId, dialog) {
191
+ // An own key only: an inherited name such as `toString` is not a dialog kind.
192
+ const kind = dialog.kind;
193
+ const answer = kind !== undefined && Object.hasOwn(DISMISSED_ANSWER, kind)
194
+ ? DISMISSED_ANSWER[kind]
195
+ : undefined;
196
+ const session = deps.resolveSession(leaseId);
197
+ if (answer === undefined || session === undefined) {
198
+ return;
199
+ }
200
+ session.pushEvent({
201
+ t: 0, // pushEvent re-stamps with the session's own clock
202
+ type: EventType.DIALOG_OPENED,
203
+ sessionId: session.id,
204
+ data: { kind: dialog.kind, message: dialog.message, ...answer },
205
+ });
206
+ },
180
207
  };
181
208
  }
@@ -5,6 +5,7 @@
5
5
  *
6
6
  * Headless by default — the pool exists for fault-tolerant headless multi-agent testing.
7
7
  */
8
+ import { startJsCoverage, takeJsCoverage } from '../input/js-coverage.js';
8
9
  import { BrowserLaunchKind } from '@reticlehq/core/telemetry';
9
10
  import { ActionType, HIDE_RETICLE_CHROME_CSS, RETICLE_OVERLAY_SELECTOR } from '@reticlehq/core';
10
11
  import { launchChromium } from '../../launch-chromium.js';
@@ -15,6 +16,8 @@ import { chromiumInstallCommand, chromiumInstallDepsCommand, bundledPlaywrightVe
15
16
  import { installNetworkMocks } from '../input/network-mock.js';
16
17
  import { performGesture, withChromePassThrough, } from '../input/real-input.js';
17
18
  import { widenDocumentCsp } from './csp-widen.js';
19
+ import { contextEmulationOf } from './lease-emulation.js';
20
+ import { determinismConfigOf, installDeterminism } from './determinism.js';
18
21
  /** A key addresses focus, not a point; the gesture driver takes a box for every action. */
19
22
  const NO_BOX = { x: 0, y: 0, width: 0, height: 0 };
20
23
  const sleepFor = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -93,20 +96,48 @@ export function gotoOptions(timeoutMs) {
93
96
  ...(timeoutMs === undefined ? {} : { timeout: timeoutMs }),
94
97
  };
95
98
  }
99
+ /**
100
+ * Reload with the cache ignored, as Cmd+Shift+R does (#1785).
101
+ *
102
+ * Playwright's `page.reload()` is a plain reload: a module with a fresh cache entry is not fetched
103
+ * again. The DevTools protocol's `Page.reload { ignoreCache: true }` is the browser's own hard
104
+ * reload. The pool launches Chromium, so the protocol is always there.
105
+ */
106
+ async function reloadIgnoringCache(page) {
107
+ const cdp = await page.context().newCDPSession(page);
108
+ try {
109
+ await cdp.send('Page.reload', { ignoreCache: true });
110
+ }
111
+ finally {
112
+ await cdp.detach();
113
+ }
114
+ }
96
115
  function wrapBrowser(browser, headless) {
97
116
  return {
98
117
  isConnected: () => browser.isConnected(),
99
118
  newContext: async (opts) => {
100
- // Media and viewport ride on the context, so the first paint of the first page has them.
101
- const { ignoreHTTPSErrors, ...emulate } = opts ?? {};
119
+ // Media, viewport and pointer ride on the context, so the first paint of the first page has
120
+ // them. `pointer` becomes `hasTouch` here — see contextEmulationOf.
121
+ const { ignoreHTTPSErrors, jsCoverage, ...emulate } = opts ?? {};
122
+ const covering = new WeakSet();
123
+ // A deterministic context needs a fixed viewport: a density is not set on a window's own size.
124
+ const deterministic = emulate.deterministic;
102
125
  const context = await browser.newContext({
103
126
  ...(true === ignoreHTTPSErrors ? { ignoreHTTPSErrors: true } : {}),
104
- ...emulate,
105
- ...pageViewportOptions(headless, emulate.viewport),
127
+ ...contextEmulationOf(emulate),
128
+ ...pageViewportOptions(headless || deterministic !== undefined, emulate.viewport),
106
129
  });
130
+ // On the context, so every page and frame of the lease gets it before its first script.
131
+ if (deterministic !== undefined) {
132
+ await context.addInitScript(installDeterminism, determinismConfigOf(deterministic));
133
+ }
107
134
  return {
108
135
  newPage: async () => {
109
136
  const page = await context.newPage();
137
+ // Before the first navigation, so the code that runs while the page loads is counted.
138
+ if (true === jsCoverage) {
139
+ await startJsCoverage(page, covering);
140
+ }
110
141
  return {
111
142
  goto: (url, opts) => page.goto(url, gotoOptions(opts?.timeoutMs)),
112
143
  close: () => page.close(),
@@ -129,6 +160,7 @@ function wrapBrowser(browser, headless) {
129
160
  await page.mouse.move(x + 1, y);
130
161
  await page.mouse.move(x, y);
131
162
  }),
163
+ reloadIgnoringCache: () => reloadIgnoringCache(page),
132
164
  // The same driver `reticle drive` presses with, so a held key and a chord read alike.
133
165
  emulateMedia: (media) => page.emulateMedia(media),
134
166
  press: (args) => performGesture(page, ActionType.PRESS, NO_BOX, args, sleepFor),
@@ -148,6 +180,10 @@ function wrapBrowser(browser, headless) {
148
180
  });
149
181
  },
150
182
  setViewport: (size) => page.setViewportSize(size),
183
+ jsCoverage: {
184
+ start: () => startJsCoverage(page, covering),
185
+ take: () => covering.has(page) ? takeJsCoverage(page, covering) : Promise.resolve(undefined),
186
+ },
151
187
  bringToFront: () => page.bringToFront(),
152
188
  onCrash: (handler) => page.on('crash', handler),
153
189
  onConsole: (handler) => page.on('console', (msg) => handler(msg.text())),
@@ -157,7 +193,11 @@ function wrapBrowser(browser, headless) {
157
193
  message: error.message,
158
194
  ...(error.stack === undefined ? {} : { stack: error.stack }),
159
195
  })),
160
- onDialog: (handler) => page.on('dialog', (dialog) => handler({ message: dialog.message(), dismiss: () => dialog.dismiss() })),
196
+ onDialog: (handler) => page.on('dialog', (dialog) => handler({
197
+ message: dialog.message(),
198
+ kind: dialog.type(),
199
+ dismiss: () => dialog.dismiss(),
200
+ })),
161
201
  addInitScript: async (script, arg) => {
162
202
  const handle = (await page.addInitScript(script, arg));
163
203
  const dispose = handle?.dispose;
@@ -216,11 +256,13 @@ function pathFromPlaywrightError(msg) {
216
256
  // Playwright puts the path at the end of the line, and a Windows path can hold a space
217
257
  // (for example C:\Users\Jo Doe). Read to the end of the line and trim the result.
218
258
  const found = /executable doesn.?t exist at (.+)$/im.exec(msg);
219
- if (null === found)
259
+ if (null === found) {
220
260
  return undefined;
261
+ }
221
262
  const captured = found[1];
222
- if (undefined === captured)
263
+ if (undefined === captured) {
223
264
  return undefined;
265
+ }
224
266
  const path = captured.trim();
225
267
  return '' === path ? undefined : path;
226
268
  }
@@ -267,8 +309,9 @@ export function chromiumLaunchHint(msg) {
267
309
  // Checked FIRST: Playwright's host-requirement validation error also contains
268
310
  // "browserType.launch", so the generic check below would otherwise misdiagnose it as a
269
311
  // missing binary and send the user through a fix that changes nothing.
270
- if (/host system is missing dependencies/i.test(msg))
312
+ if (/host system is missing dependencies/i.test(msg)) {
271
313
  return CHROMIUM_MISSING_DEPS_HINT;
314
+ }
272
315
  if (/executable doesn.?t exist|playwright install|browsertype\.launch/i.test(msg)) {
273
316
  return chromiumMissingHint(pathFromPlaywrightError(msg));
274
317
  }
@@ -7,6 +7,14 @@
7
7
  */
8
8
  import type { RealInputArgs, RealInputResult } from '../input/real-input.js';
9
9
  import type { LeaseEmulation, LeaseMedia } from './lease-emulation.js';
10
+ import type { ScriptCoverage } from '../input/js-coverage.js';
11
+ /** JS coverage on one page: start it, and take what ran since the previous take. */
12
+ export interface PooledCoverage {
13
+ /** False when the page cannot measure coverage. */
14
+ start(): Promise<boolean>;
15
+ /** Undefined when coverage was not running; the first take does not start it. */
16
+ take(): Promise<ScriptCoverage[] | undefined>;
17
+ }
10
18
  /** The minimal page surface the pool drives. Real Playwright `Page` satisfies this. */
11
19
  export interface PooledPage {
12
20
  goto(url: string, opts?: {
@@ -47,6 +55,11 @@ export interface PooledPage {
47
55
  * page without it keeps the in-page dispatcher, whose synthetic Tab moves no focus.
48
56
  */
49
57
  press?(args: RealInputArgs): Promise<RealInputResult>;
58
+ /**
59
+ * Reload the page with the browser cache ignored for the document and every subresource, as
60
+ * Cmd+Shift+R does. OPTIONAL: without it a hard reload can only re-fetch the document (#1785).
61
+ */
62
+ reloadIgnoringCache?(): Promise<void>;
50
63
  /** Set prefers-color-scheme and prefers-reduced-motion. OPTIONAL: without it the media stay. */
51
64
  emulateMedia?(media: LeaseMedia): Promise<void>;
52
65
  /** Evaluate a script in the page's current document. OPTIONAL: without it no reader is injected. */
@@ -92,11 +105,18 @@ export interface PooledPage {
92
105
  * hidden, and a hidden tab throttles timers and rAF so nothing on it verifies (#1351).
93
106
  */
94
107
  bringToFront?(): Promise<void>;
108
+ /**
109
+ * JS coverage of this page. OPTIONAL, like `installMocks`: a page without it makes the
110
+ * coverage tool refuse rather than report that no code ran.
111
+ */
112
+ jsCoverage?: PooledCoverage;
95
113
  }
96
114
  /** A native dialog the page opened, handed to the pool so it can be dismissed instead of left blocking. */
97
115
  export interface PooledDialog {
98
116
  /** The dialog's message text — kept for diagnostics (`BrowserPool#lastDialogMessage`). */
99
117
  readonly message: string;
118
+ /** `alert`, `confirm`, `prompt` or `beforeunload`, as the engine names it. Absent: not known. */
119
+ readonly kind?: string;
100
120
  dismiss(): Promise<void>;
101
121
  }
102
122
  /**
@@ -133,6 +153,8 @@ export interface PooledMockRule {
133
153
  contentType?: string;
134
154
  delayMs?: number;
135
155
  abort?: boolean;
156
+ /** Send the request on to this origin; the path and query stay (see network-mock.ts). */
157
+ forwardTo?: string;
136
158
  }
137
159
  /** An isolated browsing context (cookies/storage). Real Playwright `BrowserContext` satisfies this. */
138
160
  export interface PooledContext {
@@ -159,9 +181,13 @@ export interface PooledContext {
159
181
  /** The launched browser. Real Playwright `Browser` satisfies this. */
160
182
  export interface PooledBrowser {
161
183
  isConnected(): boolean;
162
- /** `ignoreHTTPSErrors` accepts a self-signed or local-CA certificate for this context only. */
184
+ /**
185
+ * `ignoreHTTPSErrors` accepts a self-signed or local-CA certificate for this context only.
186
+ * `jsCoverage` starts JS coverage on each page before its first script runs.
187
+ */
163
188
  newContext(opts?: {
164
189
  ignoreHTTPSErrors?: boolean;
190
+ jsCoverage?: boolean;
165
191
  } & LeaseEmulation): Promise<PooledContext>;
166
192
  close(): Promise<void>;
167
193
  /** Fires when the browser process dies/crashes so the pool can relaunch on the next acquire. */
@@ -2,7 +2,37 @@
2
2
  * The pool's lease handle and lease record, its options, and its limits: the types and constants
3
3
  * `BrowserPool` is built from.
4
4
  */
5
- import type { InitScriptHandle, Launcher, PooledBrowser, PooledContext, PooledPage, PooledPageError } from './pool-contract.js';
5
+ import type { InitScriptHandle, Launcher, PooledBrowser, PooledContext, PooledDialog, PooledPage, PooledPageError } from './pool-contract.js';
6
+ import type { SeedStorage } from '@reticlehq/core';
7
+ import type { LeaseEmulation } from './lease-emulation.js';
8
+ /** What one `BrowserPool#acquire` asks for. Every field is optional. */
9
+ export interface AcquireOptions {
10
+ signal?: AbortSignal;
11
+ /**
12
+ * `'wait'` (the default) queues behind the cap. `'refuse'` is for a caller that has a person
13
+ * or an agent waiting on the answer and no way to see a queue: it is told the pool is full.
14
+ */
15
+ whenFull?: 'wait' | 'refuse';
16
+ sessionId?: string;
17
+ seedStorage?: SeedStorage;
18
+ owner?: string;
19
+ /** In a browser with a window the person can watch. */
20
+ headed?: boolean;
21
+ /**
22
+ * JavaScript installed before any page script runs, for this lease only. A view built for a
23
+ * desktop shell calls bindings the shell injects (`window.go`, `window.runtime`, a preload
24
+ * object) on mount; a stub here is what lets it reach its data path in a leased tab.
25
+ */
26
+ initScript?: string;
27
+ /** Granted on the lease's origin before the first navigation, so the first render sees them. */
28
+ permissions?: readonly string[];
29
+ /** Accept a self-signed or local-CA dev certificate for this lease's context only (#1255). */
30
+ ignoreHTTPSErrors?: boolean;
31
+ /** Media and viewport set on the context, so the first paint already has them. */
32
+ emulate?: LeaseEmulation;
33
+ /** Start JS coverage before the page's first script, so load-time code is counted. */
34
+ jsCoverage?: boolean;
35
+ }
6
36
  /** A leased context+page for one flow. `release` frees the slot and closes the context. */
7
37
  export interface Lease {
8
38
  readonly sessionId: string;
@@ -67,6 +97,11 @@ export interface BrowserPoolOptions {
67
97
  * holding it for the life of the daemon. Optional: no sink, nothing to clean up.
68
98
  */
69
99
  onLeaseClosed?: (leaseId: string) => void;
100
+ /**
101
+ * Sink for each native dialog the pool dismissed, with the lease id. The dismissal is an answer
102
+ * of "no" on the app's behalf, so the wiring records it for the verdict (#1350). Optional.
103
+ */
104
+ onDialog?: (leaseId: string, dialog: Pick<PooledDialog, 'kind' | 'message'>) => void;
70
105
  /**
71
106
  * Launches a browser with a window, for a lease somebody wants to watch. Launched only when the
72
107
  * first headed lease is asked for. Absent: a headed lease is refused.
@@ -25,6 +25,14 @@ export declare function zeroInstallScript(bundle: string, opts: InjectedConnect)
25
25
  export declare class NotHtmlDocumentError extends Error {
26
26
  constructor();
27
27
  }
28
+ /**
29
+ * Run the reader in a leased page now, and again before the page's own scripts on each later load.
30
+ * False when there is no reader or the page cannot run one; throws when the page is not HTML.
31
+ */
32
+ export declare function injectReaderInto(page: {
33
+ evaluate?(script: string): Promise<unknown>;
34
+ addInitScript?(script: string): unknown;
35
+ }, script: string | undefined): Promise<boolean>;
28
36
  /**
29
37
  * The single-file build, or undefined when this install has none.
30
38
  *
@@ -35,6 +35,14 @@ const HTML_CONTENT_TYPES = ['text/html', 'application/xhtml+xml'];
35
35
  * reads `document.contentType`, because an init script runs before the parser makes the root
36
36
  * element. The check binds no name, so it cannot collide with a name that the bundle declares.
37
37
  */
38
+ /*
39
+ * Run in the top frame only. Playwright runs an init script in every frame, and a rich-text editor's
40
+ * editing area is an iframe whose <body> IS the document being edited: the SDK mounted its HUD there,
41
+ * and saving the form stored the HUD markup in the app's data (Dolibarr product and project
42
+ * descriptions). The top frame's SDK already reads same-origin iframes, so a child frame needs nothing.
43
+ * A context with no `top` (a test sandbox) counts as the top frame.
44
+ */
45
+ const TOP_FRAME_ONLY = 'if (globalThis.top !== undefined && globalThis.self !== undefined && globalThis.top !== globalThis.self) return;';
38
46
  const NOT_HTML_GUARD = `if (globalThis.document !== undefined && !${JSON.stringify(HTML_CONTENT_TYPES)}.includes(globalThis.document.contentType)) ` +
39
47
  `return ${JSON.stringify(READER_NOT_HTML)};`;
40
48
  /*
@@ -115,7 +123,7 @@ const ZOD_JITLESS = `(${ZOD_CONFIG} ??= {}).jitless ??= true;`;
115
123
  */
116
124
  export function zeroInstallScript(bundle, opts) {
117
125
  const args = JSON.stringify(injectedConnectArgs(opts));
118
- return `(() => {\n${NOT_HTML_GUARD}\nif (!globalThis.__reticleInstance) {\n${EARLY_KEEP}\n${ZOD_JITLESS}\n${bundle}\n}\n${CONNECT_WHEN_DOM_READY(args)}\n})();`;
126
+ return `(() => {\n${TOP_FRAME_ONLY}\n${NOT_HTML_GUARD}\nif (!globalThis.__reticleInstance) {\n${EARLY_KEEP}\n${ZOD_JITLESS}\n${bundle}\n}\n${CONNECT_WHEN_DOM_READY(args)}\n})();`;
119
127
  }
120
128
  /** The page is not an HTML document, so the lease did not give it the zero-install reader. */
121
129
  export class NotHtmlDocumentError extends Error {
@@ -124,6 +132,20 @@ export class NotHtmlDocumentError extends Error {
124
132
  this.name = 'NotHtmlDocumentError';
125
133
  }
126
134
  }
135
+ /**
136
+ * Run the reader in a leased page now, and again before the page's own scripts on each later load.
137
+ * False when there is no reader or the page cannot run one; throws when the page is not HTML.
138
+ */
139
+ export async function injectReaderInto(page, script) {
140
+ if (script === undefined || page.evaluate === undefined) {
141
+ return false;
142
+ }
143
+ await page.addInitScript?.(script);
144
+ if (READER_NOT_HTML === (await page.evaluate(script))) {
145
+ throw new NotHtmlDocumentError();
146
+ }
147
+ return true;
148
+ }
127
149
  let cached;
128
150
  /**
129
151
  * The single-file build, or undefined when this install has none.
@@ -20,13 +20,14 @@ export interface DevCommand {
20
20
  directory?: string;
21
21
  }
22
22
  /**
23
- * The direct child folders of `directory` that are apps with a dev command, sorted by name.
23
+ * The direct child folders of `directory` that are apps with a dev command, sorted by name: the web
24
+ * apps when there are any, every dev-script folder when there are none (see `preferWebApps`).
24
25
  *
25
26
  * Only for a folder with no package.json of its own. A task folder holds the app one level down, and
26
- * no workspace file lists it, so `workspacePackageDirs` never finds it. Folders that are not apps
27
- * (no package.json, or no dev script), `node_modules` and hidden folders are skipped.
27
+ * no workspace file lists it, so `workspacePackageDirs` never finds it. `node_modules`, hidden
28
+ * folders and folders with no dev script are never candidates.
28
29
  */
29
- export declare function childDevApps(directory: string, read?: (path: string) => string | undefined, listDirs?: (path: string) => readonly string[]): readonly string[];
30
+ export declare function childDevApps(directory: string, read?: (path: string) => string | undefined, listDirs?: (path: string) => readonly string[], writable?: (path: string) => boolean): readonly string[];
30
31
  /**
31
32
  * The dev command for the project in `directory`, or undefined when there is not one to be sure of.
32
33
  *
@@ -43,7 +44,7 @@ export declare function detectDevCommand(directory: string, read?: (path: string
43
44
  * init's own workspace listing answers which directories to consider; the root's lockfile picks the
44
45
  * package manager, because a workspace package usually has no lockfile of its own.
45
46
  */
46
- export declare function detectDevCommandInProject(directory: string, read?: (path: string) => string | undefined, listDirs?: (path: string) => readonly string[]): DevCommand | undefined;
47
+ export declare function detectDevCommandInProject(directory: string, read?: (path: string) => string | undefined, listDirs?: (path: string) => readonly string[], writable?: (path: string) => boolean): DevCommand | undefined;
47
48
  /**
48
49
  * The command that wires THIS project for good: `init`, in the app's own folder, through the
49
50
  * one-off runner the project's package manager ships.
@@ -52,4 +53,4 @@ export declare function detectDevCommandInProject(directory: string, read?: (pat
52
53
  * time: `init` run in a folder that is not the app stops on a missing package.json, and a runner the
53
54
  * machine does not have is "command not found". npx whenever there is doubt — it ships with Node.
54
55
  */
55
- export declare function initCommandIn(directory: string, read?: (path: string) => string | undefined, listDirs?: (path: string) => readonly string[]): string;
56
+ export declare function initCommandIn(directory: string, read?: (path: string) => string | undefined, listDirs?: (path: string) => readonly string[], writable?: (path: string) => boolean): string;