rnxsim 0.1.313 → 0.1.314

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 (297) hide show
  1. package/LICENSE +11 -10
  2. package/README.md +5 -0
  3. package/cli/app-config.ts +65 -0
  4. package/cli/app-fonts.ts +408 -0
  5. package/cli/app-project.ts +231 -0
  6. package/cli/app-splash.ts +185 -0
  7. package/cli/app-state-reset.ts +24 -0
  8. package/cli/auth.ts +155 -0
  9. package/cli/bin.ts +594 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2830 -0
  12. package/cli/browser-evals.ts +96 -0
  13. package/cli/commands/agent-wrapper.ts +986 -0
  14. package/cli/commands/agent.ts +423 -0
  15. package/cli/commands/app-fonts.ts +98 -0
  16. package/cli/commands/assert.ts +541 -0
  17. package/cli/commands/auth.ts +59 -0
  18. package/cli/commands/camera.ts +266 -0
  19. package/cli/commands/cleanup.ts +169 -0
  20. package/cli/commands/compat.ts +87 -0
  21. package/cli/commands/config.ts +32 -0
  22. package/cli/commands/control.ts +2142 -0
  23. package/cli/commands/cpu-profile.ts +269 -0
  24. package/cli/commands/daemon-mac-app.ts +169 -0
  25. package/cli/commands/daemon.ts +874 -0
  26. package/cli/commands/debug.ts +719 -0
  27. package/cli/commands/desktop.ts +39 -0
  28. package/cli/commands/detect.ts +197 -0
  29. package/cli/commands/detox.ts +385 -0
  30. package/cli/commands/device.ts +133 -0
  31. package/cli/commands/diagnose.ts +589 -0
  32. package/cli/commands/electron.ts +95 -0
  33. package/cli/commands/film.ts +379 -0
  34. package/cli/commands/flow.ts +1124 -0
  35. package/cli/commands/inspect/actions.ts +622 -0
  36. package/cli/commands/inspect/core.ts +2405 -0
  37. package/cli/commands/inspect/count.ts +17 -0
  38. package/cli/commands/inspect/describe.ts +192 -0
  39. package/cli/commands/inspect/env.ts +23 -0
  40. package/cli/commands/inspect/find.ts +171 -0
  41. package/cli/commands/inspect/get-layout.ts +39 -0
  42. package/cli/commands/inspect/keyboard.ts +52 -0
  43. package/cli/commands/inspect/list.ts +58 -0
  44. package/cli/commands/inspect/memory.ts +215 -0
  45. package/cli/commands/inspect/redaction.ts +39 -0
  46. package/cli/commands/inspect/resolve-target.ts +82 -0
  47. package/cli/commands/inspect/screens.ts +78 -0
  48. package/cli/commands/inspect/settle.ts +22 -0
  49. package/cli/commands/inspect/settling.ts +158 -0
  50. package/cli/commands/inspect/shared.ts +353 -0
  51. package/cli/commands/inspect/sleep.ts +14 -0
  52. package/cli/commands/inspect/tree.ts +32 -0
  53. package/cli/commands/inspect/url.ts +17 -0
  54. package/cli/commands/inspect/wait-event.ts +210 -0
  55. package/cli/commands/inspect/wait-idle.ts +24 -0
  56. package/cli/commands/inspect/wait-ready.ts +74 -0
  57. package/cli/commands/inspect/wait-selector.ts +54 -0
  58. package/cli/commands/inspect/wait.ts +31 -0
  59. package/cli/commands/inspect.ts +4519 -0
  60. package/cli/commands/install-desktop.ts +351 -0
  61. package/cli/commands/login.ts +331 -0
  62. package/cli/commands/logout.ts +31 -0
  63. package/cli/commands/maestro-generate.ts +361 -0
  64. package/cli/commands/maestro.ts +453 -0
  65. package/cli/commands/mode.ts +57 -0
  66. package/cli/commands/no-bridge-hint.ts +80 -0
  67. package/cli/commands/perf.ts +66 -0
  68. package/cli/commands/permissions.ts +203 -0
  69. package/cli/commands/profile.ts +108 -0
  70. package/cli/commands/react.ts +353 -0
  71. package/cli/commands/record.ts +1434 -0
  72. package/cli/commands/report-issue.ts +305 -0
  73. package/cli/commands/reset.ts +85 -0
  74. package/cli/commands/runtime.ts +351 -0
  75. package/cli/commands/screenshot-command.ts +106 -0
  76. package/cli/commands/screenshot-layers.ts +143 -0
  77. package/cli/commands/screenshot-mode.ts +37 -0
  78. package/cli/commands/screenshot.ts +488 -0
  79. package/cli/commands/screenshots-capture.ts +607 -0
  80. package/cli/commands/screenshots.ts +127 -0
  81. package/cli/commands/serve.ts +168 -0
  82. package/cli/commands/setup.ts +545 -0
  83. package/cli/commands/shell-boolean-mode.ts +81 -0
  84. package/cli/commands/skills.ts +467 -0
  85. package/cli/commands/slides.ts +361 -0
  86. package/cli/commands/state.ts +87 -0
  87. package/cli/commands/storage.ts +58 -0
  88. package/cli/commands/telemetry.ts +54 -0
  89. package/cli/commands/three-mode.ts +763 -0
  90. package/cli/commands/timeline.ts +122 -0
  91. package/cli/commands/upgrade.ts +208 -0
  92. package/cli/commands/upload.ts +1225 -0
  93. package/cli/commands/version.ts +54 -0
  94. package/cli/commands/what-happened.ts +327 -0
  95. package/cli/current-sim.ts +204 -0
  96. package/cli/desktop-companion.ts +300 -0
  97. package/cli/drivers/electron.ts +70 -0
  98. package/cli/drivers/index.ts +20 -0
  99. package/cli/drivers/playwright-provisioning.ts +180 -0
  100. package/cli/drivers/playwright.ts +698 -0
  101. package/cli/drivers/registry.ts +65 -0
  102. package/cli/drivers/types.ts +102 -0
  103. package/cli/flow-file.ts +142 -0
  104. package/cli/flow-live-status.ts +120 -0
  105. package/cli/flow-session.ts +187 -0
  106. package/cli/help.ts +80 -0
  107. package/cli/hidden-runtime-alias.ts +19 -0
  108. package/cli/hints.ts +216 -0
  109. package/cli/inspect-notice-state.ts +114 -0
  110. package/cli/maestro-js.ts +334 -0
  111. package/cli/open-url.ts +8 -0
  112. package/cli/parent-pid.ts +204 -0
  113. package/cli/parse-args.ts +211 -0
  114. package/cli/prompt.ts +51 -0
  115. package/cli/recording-access.ts +107 -0
  116. package/cli/registry.ts +1 -0
  117. package/cli/resolve-assets.ts +63 -0
  118. package/cli/run-registry.ts +226 -0
  119. package/cli/runtime-notes.ts +66 -0
  120. package/cli/runtime-summary.ts +25 -0
  121. package/cli/setup-repository.ts +187 -0
  122. package/cli/telemetry.ts +187 -0
  123. package/cli/ws-bridge.ts +798 -0
  124. package/dist-cli/bin.js +5 -5
  125. package/dist-cli/chunks/{agent-XZ2KTPCU.js → agent-7YBDCYMA.js} +2 -2
  126. package/dist-cli/chunks/{agent-wrapper-JJYYW2WH.js → agent-wrapper-2GHFBHCR.js} +2 -2
  127. package/dist-cli/chunks/{app-fonts-IXRNQG6B.js → app-fonts-RSNVPQSU.js} +2 -2
  128. package/dist-cli/chunks/{assert-54T5SK5F.js → assert-XCMX3XJX.js} +2 -2
  129. package/dist-cli/chunks/{auth-FI5UDI45.js → auth-B442HRAX.js} +2 -2
  130. package/dist-cli/chunks/{beta-JV6UKADW.js → beta-XJ55JK3M.js} +2 -2
  131. package/dist-cli/chunks/camera-UGYSSLIK.js +33 -0
  132. package/dist-cli/chunks/{chunk-3NV2NCNX.js → chunk-277AEQZX.js} +2 -2
  133. package/dist-cli/chunks/{chunk-2YR5BGA5.js → chunk-2JNSK774.js} +2 -2
  134. package/dist-cli/chunks/{chunk-RSZWCKNT.js → chunk-32WOTSTR.js} +3 -3
  135. package/dist-cli/chunks/{chunk-WEXDAC74.js → chunk-3S753SNQ.js} +2 -2
  136. package/dist-cli/chunks/{chunk-IJO63TDP.js → chunk-46ZOLOYA.js} +2 -2
  137. package/dist-cli/chunks/{chunk-WUSWBCWA.js → chunk-5L7ELDQL.js} +8 -9
  138. package/dist-cli/chunks/{chunk-WF3T4SVI.js → chunk-7OOPFSQS.js} +2 -2
  139. package/dist-cli/chunks/{chunk-TZFFR3SD.js → chunk-7SV3RPRW.js} +2 -2
  140. package/dist-cli/chunks/{chunk-WWZIXIRD.js → chunk-7ZC35MOU.js} +1 -1
  141. package/dist-cli/chunks/chunk-AMG5E6CC.js +9 -0
  142. package/dist-cli/chunks/{chunk-WINYQ44O.js → chunk-APWNH3A4.js} +1 -1
  143. package/dist-cli/chunks/chunk-B57XUKY3.js +4 -0
  144. package/dist-cli/chunks/{chunk-YDGQTMQL.js → chunk-C2NL26TD.js} +1 -1
  145. package/dist-cli/chunks/{chunk-NMF2ZMZQ.js → chunk-D2FNUWAB.js} +4 -4
  146. package/dist-cli/chunks/{chunk-YIFT42WN.js → chunk-E5T4XSJ3.js} +2 -2
  147. package/dist-cli/chunks/{chunk-46EUUFJ5.js → chunk-EAC34EQS.js} +1 -1
  148. package/dist-cli/chunks/{chunk-OVFJFXUD.js → chunk-EG32ML36.js} +2 -2
  149. package/dist-cli/chunks/{chunk-5YJCOWCH.js → chunk-FXUAC6D5.js} +1 -1
  150. package/dist-cli/chunks/chunk-GQSL4USA.js +6 -0
  151. package/dist-cli/chunks/{chunk-VFCMSYZK.js → chunk-HAXW27SS.js} +2 -2
  152. package/dist-cli/chunks/{chunk-2D2UPBBR.js → chunk-IZAHPAN6.js} +1 -1
  153. package/dist-cli/chunks/{chunk-7GN3LVWB.js → chunk-J62KM5TB.js} +2 -2
  154. package/dist-cli/chunks/{chunk-BTWORNNG.js → chunk-JEMCD5E4.js} +1 -1
  155. package/dist-cli/chunks/{chunk-DCEMHR2Y.js → chunk-JZS3Q37N.js} +2 -2
  156. package/dist-cli/chunks/{chunk-D4FFVGI5.js → chunk-KR4JON7D.js} +1 -1
  157. package/dist-cli/chunks/chunk-KWCYKQIQ.js +4 -0
  158. package/dist-cli/chunks/{chunk-VNQEB4L7.js → chunk-MCWPL644.js} +2 -2
  159. package/dist-cli/chunks/chunk-MJYK3N2I.js +4 -0
  160. package/dist-cli/chunks/{chunk-5TEF3ET3.js → chunk-O6TRIZNS.js} +2 -2
  161. package/dist-cli/chunks/{chunk-QKDWYITG.js → chunk-P7XL2E73.js} +3 -3
  162. package/dist-cli/chunks/{chunk-BBULZ7CG.js → chunk-PFQTUKQ4.js} +62 -87
  163. package/dist-cli/chunks/{chunk-GGRX24GF.js → chunk-PG5RZCTN.js} +2 -2
  164. package/dist-cli/chunks/{chunk-W6K4EFPH.js → chunk-QGRI2Z4M.js} +2 -2
  165. package/dist-cli/chunks/{chunk-ZMJD5GEC.js → chunk-QLXXE7GE.js} +1 -1
  166. package/dist-cli/chunks/{chunk-VZXWHRUZ.js → chunk-QOJJJWJE.js} +89 -133
  167. package/dist-cli/chunks/{chunk-OZSSI4WN.js → chunk-RWZY5427.js} +2 -2
  168. package/dist-cli/chunks/{chunk-UC6U3MML.js → chunk-RZKU2K3J.js} +2 -2
  169. package/dist-cli/chunks/{chunk-IJ5CAZZC.js → chunk-SBV4IK4H.js} +1 -1
  170. package/dist-cli/chunks/{chunk-DZS6WPUI.js → chunk-SGMVFFMK.js} +1 -1
  171. package/dist-cli/chunks/{chunk-OHAZNXLK.js → chunk-TAX4UT2N.js} +1 -1
  172. package/dist-cli/chunks/{chunk-5DHC6KHQ.js → chunk-TUOFAWXT.js} +1 -1
  173. package/dist-cli/chunks/{chunk-GASE6UBA.js → chunk-UHZLOHGP.js} +1 -1
  174. package/dist-cli/chunks/{chunk-F5ZRSS3C.js → chunk-VC7V76U3.js} +1 -1
  175. package/dist-cli/chunks/{chunk-RTN5C5RL.js → chunk-VQVMLW4U.js} +1 -1
  176. package/dist-cli/chunks/{chunk-WMIIKMGK.js → chunk-VUKKYPZN.js} +2 -2
  177. package/dist-cli/chunks/{chunk-5TPRP5QT.js → chunk-WGGRJDRE.js} +1 -1
  178. package/dist-cli/chunks/chunk-X6H76EKP.js +15 -0
  179. package/dist-cli/chunks/chunk-XLZ5FNRT.js +27 -0
  180. package/dist-cli/chunks/{chunk-HI5TFJWN.js → chunk-XULEACM4.js} +2 -2
  181. package/dist-cli/chunks/{chunk-MJRLLB4R.js → chunk-YFSDM7AX.js} +4 -4
  182. package/dist-cli/chunks/chunk-YWI3UEVX.js +5 -0
  183. package/dist-cli/chunks/{chunk-XEVZYVIW.js → chunk-ZBSJO4NB.js} +10 -9
  184. package/dist-cli/chunks/{cleanup-P27PA6JI.js → cleanup-EYLGCA6Z.js} +2 -2
  185. package/dist-cli/chunks/cli-version-LL2UGIHE.js +4 -0
  186. package/dist-cli/chunks/{compat-ZD65FED3.js → compat-TLJYHB4E.js} +2 -2
  187. package/dist-cli/chunks/{config-XMJRNM2A.js → config-4JWUOEXK.js} +2 -2
  188. package/dist-cli/chunks/{control-KMIQT3QP.js → control-HPAOYF4N.js} +2 -2
  189. package/dist-cli/chunks/daemon-OYLASXLE.js +4 -0
  190. package/dist-cli/chunks/{debug-PT4HOP7N.js → debug-4BKXF6KI.js} +5 -5
  191. package/dist-cli/chunks/{desktop-S3FG72AK.js → desktop-ZQ6ZD2S6.js} +3 -3
  192. package/dist-cli/chunks/{detox-B3D4IFCN.js → detox-WNPASSS3.js} +2 -2
  193. package/dist-cli/chunks/{device-XBNDSB2R.js → device-BXLXVG3V.js} +2 -2
  194. package/dist-cli/chunks/{diagnose-HMQXJE5N.js → diagnose-3QX7W5RW.js} +2 -2
  195. package/dist-cli/chunks/{disk-cleanup-BLCZ5BSZ.js → disk-cleanup-KSWKI7WB.js} +2 -2
  196. package/dist-cli/chunks/drivers-EPIEFF7P.js +4 -0
  197. package/dist-cli/chunks/{film-BJGTBYZB.js → film-GDMR33VO.js} +3 -3
  198. package/dist-cli/chunks/flow-NKMPBYCJ.js +4 -0
  199. package/dist-cli/chunks/help-OGCBHOKA.js +4 -0
  200. package/dist-cli/chunks/{hidden-runtime-alias-ANOYADHM.js → hidden-runtime-alias-S2GTDX3T.js} +2 -2
  201. package/dist-cli/chunks/home-paths-QRCDLTTV.js +4 -0
  202. package/dist-cli/chunks/inspect-FUYMOZPY.js +4 -0
  203. package/dist-cli/chunks/install-desktop-FR6YKW7Y.js +4 -0
  204. package/dist-cli/chunks/{login-FJ737MWG.js → login-IJAPUZHI.js} +4 -4
  205. package/dist-cli/chunks/{logout-ZCNMMHMY.js → logout-QMXDFU2X.js} +2 -2
  206. package/dist-cli/chunks/{maestro-SZTNKLDF.js → maestro-ZXU3YCVX.js} +3 -3
  207. package/dist-cli/chunks/{maestro-generate-DCFAIZ4H.js → maestro-generate-PYB5QY7K.js} +3 -3
  208. package/dist-cli/chunks/{mode-GRMQCRXR.js → mode-WGUCL5FZ.js} +2 -2
  209. package/dist-cli/chunks/{optional-demo-registry-W36EWFFB.js → optional-demo-registry-WH2O6H36.js} +2 -2
  210. package/dist-cli/chunks/{perf-QYBAAUZG.js → perf-TOD3UFAH.js} +2 -2
  211. package/dist-cli/chunks/{permissions-3QCQ6VF4.js → permissions-I5BRJGTB.js} +2 -2
  212. package/dist-cli/chunks/{record-QPPC2S4E.js → record-ZYL2FYSK.js} +3 -3
  213. package/dist-cli/chunks/{report-issue-7NMFP4HK.js → report-issue-TAI6DYZO.js} +2 -2
  214. package/dist-cli/chunks/reset-7YGYKQPQ.js +4 -0
  215. package/dist-cli/chunks/runtime-B4JO6QRI.js +4 -0
  216. package/dist-cli/chunks/{screenshot-command-67AECJFB.js → screenshot-command-7ANLODZY.js} +7 -7
  217. package/dist-cli/chunks/{screenshot-layers-ASWBYPJL.js → screenshot-layers-A7FYXSVU.js} +3 -3
  218. package/dist-cli/chunks/{screenshots-capture-PXA3HFQK.js → screenshots-capture-WT2ZY6CB.js} +2 -2
  219. package/dist-cli/chunks/serve-TG5WKA4V.js +44 -0
  220. package/dist-cli/chunks/{setup-7DWPMRSB.js → setup-PCWLD22V.js} +2 -2
  221. package/dist-cli/chunks/{skills-S3Y22TUA.js → skills-27ZHWCQS.js} +2 -2
  222. package/dist-cli/chunks/state-ZQVY46ZO.js +14 -0
  223. package/dist-cli/chunks/{storage-XUIMJWAJ.js → storage-OYC57C4X.js} +6 -6
  224. package/dist-cli/chunks/store-32HSPZHI.js +4 -0
  225. package/dist-cli/chunks/telemetry-PBJR7XHR.js +4 -0
  226. package/dist-cli/chunks/{timeline-TMPLQPSP.js → timeline-RAJJFQT6.js} +2 -2
  227. package/dist-cli/chunks/{upgrade-7HDSIM7K.js → upgrade-OEFNZRIP.js} +2 -2
  228. package/dist-cli/chunks/upload-LQJITLKK.js +4 -0
  229. package/dist-cli/chunks/version-NIXTY6PL.js +6 -0
  230. package/dist-cli/chunks/{web-DG3WBYD3.js → web-XBUTBVGR.js} +2 -2
  231. package/dist-cli/chunks/{what-happened-XFVUTZR7.js → what-happened-YNHRFUDX.js} +3 -3
  232. package/dist-lib/agent-daemon-client.cjs +1 -1
  233. package/dist-lib/agent-events.cjs +1 -1
  234. package/dist-lib/agent-identity.cjs +1 -1
  235. package/dist-lib/agent-sessions.cjs +1 -1
  236. package/dist-lib/attached-projects.cjs +1 -1
  237. package/dist-lib/auth/shared-session.cjs +1 -1
  238. package/dist-lib/backend-origin.cjs +1 -1
  239. package/dist-lib/beta.cjs +1 -1
  240. package/dist-lib/beta.mjs +1 -1
  241. package/dist-lib/bridge-constants.cjs +1 -1
  242. package/dist-lib/bridge-contract.cjs +20 -0
  243. package/dist-lib/cli-constants.cjs +1 -1
  244. package/dist-lib/config.cjs +1 -1
  245. package/dist-lib/detox/index.cjs +1 -1
  246. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  247. package/dist-lib/home-paths.cjs +67 -28
  248. package/dist-lib/host/bridge-host.cjs +140 -12
  249. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  250. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  251. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  252. package/dist-lib/host/websocket-proxy.cjs +1 -1
  253. package/dist-lib/index.cjs +2815 -40
  254. package/dist-lib/jump-to-source-babel.cjs +1 -1
  255. package/dist-lib/menu.cjs +1 -1
  256. package/dist-lib/menu.mjs +1 -1
  257. package/dist-lib/metro.cjs +1 -1
  258. package/dist-lib/profiles.cjs +1 -1
  259. package/dist-lib/public-brand.cjs +1 -1
  260. package/dist-lib/render-mode.cjs +1 -1
  261. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  262. package/dist-lib/sdk.cjs +2549 -2061
  263. package/dist-lib/sdk.mjs +2543 -2061
  264. package/dist-lib/skills.cjs +480 -280
  265. package/dist-lib/vite.cjs +1 -1
  266. package/package.json +8 -2
  267. package/src/bridge-constants.ts +3 -4
  268. package/src/bridge-contract.ts +251 -0
  269. package/src/connect.ts +83 -0
  270. package/src/disk-cleanup.ts +30 -0
  271. package/src/home-paths.ts +81 -38
  272. package/src/host/bridge-host.ts +134 -6
  273. package/src/index.ts +27 -1
  274. package/src/sdk.ts +8 -0
  275. package/src/sim-client.ts +660 -0
  276. package/dist-cli/chunks/camera-VL73YIKP.js +0 -22
  277. package/dist-cli/chunks/chunk-4NPPOV2N.js +0 -5
  278. package/dist-cli/chunks/chunk-FSUYIVJ6.js +0 -9
  279. package/dist-cli/chunks/chunk-G2WW6L2C.js +0 -23
  280. package/dist-cli/chunks/chunk-KTHV3RUS.js +0 -26
  281. package/dist-cli/chunks/chunk-LF2ZVT7O.js +0 -6
  282. package/dist-cli/chunks/chunk-NFK7T35W.js +0 -4
  283. package/dist-cli/chunks/chunk-TIVZIMMW.js +0 -4
  284. package/dist-cli/chunks/cli-version-WWLPBDQ7.js +0 -4
  285. package/dist-cli/chunks/daemon-G2ME7NLB.js +0 -4
  286. package/dist-cli/chunks/drivers-LDECZGP2.js +0 -4
  287. package/dist-cli/chunks/flow-UEQNVTU7.js +0 -4
  288. package/dist-cli/chunks/help-T5FYSVGB.js +0 -4
  289. package/dist-cli/chunks/home-paths-GT3LFNOR.js +0 -4
  290. package/dist-cli/chunks/inspect-ZA6XF5LD.js +0 -4
  291. package/dist-cli/chunks/install-desktop-TIMUDHPL.js +0 -4
  292. package/dist-cli/chunks/runtime-XOAXMSTU.js +0 -4
  293. package/dist-cli/chunks/serve-BI2NBAXG.js +0 -44
  294. package/dist-cli/chunks/store-JTHEJLAZ.js +0 -4
  295. package/dist-cli/chunks/telemetry-ZYJGD2DB.js +0 -4
  296. package/dist-cli/chunks/upload-GMSZPWM6.js +0 -4
  297. package/dist-cli/chunks/version-HOCHZ37L.js +0 -6
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.313 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.314 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "rnxsim",
3
- "version": "0.1.313",
3
+ "version": "0.1.314",
4
4
  "description": "rnxsim CLI, Vite and Metro plugins, and agent skills for driving React Native apps in the browser.",
5
5
  "author": "Tamagui LLC",
6
- "license": "MIT",
6
+ "license": "SEE LICENSE IN LICENSE",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/sootbean/soot.git",
@@ -88,6 +88,11 @@
88
88
  "source": "./src/backend-origin.ts",
89
89
  "default": "./dist-lib/backend-origin.cjs"
90
90
  },
91
+ "./bridge-contract": {
92
+ "types": "./src/bridge-contract.ts",
93
+ "source": "./src/bridge-contract.ts",
94
+ "default": "./dist-lib/bridge-contract.cjs"
95
+ },
91
96
  "./bridge-constants": {
92
97
  "source": "./src/bridge-constants.ts",
93
98
  "default": "./dist-lib/bridge-constants.cjs"
@@ -155,6 +160,7 @@
155
160
  "dist-cli/",
156
161
  "dist-lib/",
157
162
  "src/",
163
+ "cli/",
158
164
  "skills/",
159
165
  "scripts/dev-server-scanner.ts",
160
166
  "scripts/optional-demo-registry.ts",
@@ -74,10 +74,9 @@ function closestLocalDevBaseHttpPort(httpPort: number): number | null {
74
74
  /** the vite dev shell that shares a world with a bridge port. the two are one
75
75
  * offset apart by construction (bridge 7668 + N, shell 5173 + N), so a caller
76
76
  * that pinned an explicit bridge port can name its own shell directly. that
77
- * matters because the dev-bridge lockfile records only the stack that
78
- * heartbeated last: on a machine running several worktree stacks it routinely
79
- * names someone else's shell, and a page opened there while we poll our own
80
- * bridge never connects. null when the port is not a local dev bridge port. */
77
+ * keeps explicit ports independent from default-world selection and lets a
78
+ * page register on the exact bridge its caller will poll. null when the port
79
+ * is not a local dev bridge port. */
81
80
  export function resolveSootsimShellBaseUrlForBridgePort(
82
81
  bridgePort: number,
83
82
  ): string | null {
@@ -0,0 +1,251 @@
1
+ // the wire contract for the sootsim ws bridge. the engine implements it in
2
+ // sootsim-engine/src/mcp/ws-bridge.ts; the rnxsim cli client, the SimClient
3
+ // sdk, and the flow runner speak it. it is the single owner of these shapes,
4
+ // and neither side redeclares them.
5
+ //
6
+ // it lives in rnxsim rather than the engine because rnxsim is published and
7
+ // sootsim-engine is `private: true`. the engine already depends on rnxsim
8
+ // (see its `rnxsim` dependency and its `rnxsim/bridge-constants` import), so
9
+ // this is the direction that ships.
10
+
11
+ import type { SootSimAgentActionDetail } from '@sootsim/globals'
12
+
13
+ export type BridgeCommandType =
14
+ | 'evaluate'
15
+ | 'call'
16
+ | 'screenshot'
17
+ | 'tap'
18
+ | 'longPress'
19
+ | 'keyboard'
20
+ | 'tree'
21
+ | 'focus'
22
+ | 'close'
23
+ | 'flowStatus'
24
+ | 'perform'
25
+ | 'state'
26
+ | 'reset'
27
+ | 'camera'
28
+
29
+ export interface WsCommand {
30
+ id: string | number
31
+ type: BridgeCommandType
32
+ // flowStatus — live flow-runner progress payload (see flow-run-store)
33
+ status?: Record<string, unknown>
34
+ code?: string
35
+ path?: string
36
+ args?: unknown[]
37
+ x?: number
38
+ y?: number
39
+ durationMs?: number
40
+ target?: SootSimAgentActionDetail['target']
41
+ action?: string
42
+ text?: string
43
+ depth?: number
44
+ // screenshot — which canvas layers to composite. omit for full (today's
45
+ // default: everything including iOS chrome). 'tenant' excludes the shell
46
+ // overlay for a clean app capture.
47
+ layers?: 'full' | 'tenant' | 'shell'
48
+ // screenshot — engine-side layer isolation (hide subtrees / solo one
49
+ // subtree on transparency). see SootSimCaptureOptions.captureFilter.
50
+ captureFilter?: { hideTestIds?: string[]; soloTestId?: string }
51
+ // screenshot — optional crop rect in logical sootsim coords. the capture
52
+ // module scales x/y/w/h by device pixel ratio before cropping.
53
+ crop?: { x: number; y: number; w: number; h: number }
54
+ // perform — ordered batch executed inside the page, no round trips
55
+ steps?: PerformStep[]
56
+ performOptions?: PerformOptions
57
+ // state — compound screenshot + tree + route + errors
58
+ stateOptions?: SimStateOptions
59
+ // reset — two-tier app state wipe
60
+ resetOptions?: ResetOptions
61
+ // camera — fixture injection into the host camera pipeline
62
+ camera?: CameraCommand
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // perform — batched input
67
+ // ---------------------------------------------------------------------------
68
+
69
+ // every step maps onto a primitive that already exists on
70
+ // SootSim.bridges.interact / .keyboard / .test. the batch runs entirely inside
71
+ // the page so gesture timing is real wall-clock timing, not ws round-trips.
72
+ //
73
+ // deliberately absent: text-based tap resolution. matching by visible text is
74
+ // fuzzy, needs ranking and failure diagnostics, and lives once in the cli's
75
+ // tapByText. the sdk resolves text to coordinates before it builds a batch.
76
+ // testID lookup is exact, so tapId resolves in-engine.
77
+ export type PerformStep =
78
+ // raw pointer steps — full control over gesture physics
79
+ | { type: 'touchDown'; x: number; y: number; pointerId?: number }
80
+ | { type: 'touchMove'; x: number; y: number; pointerId?: number }
81
+ | { type: 'touchUp'; x: number; y: number; pointerId?: number }
82
+ | { type: 'touchCancel'; pointerId?: number }
83
+ // semantic steps
84
+ | { type: 'tap'; x: number; y: number; target?: SootSimAgentActionDetail['target'] }
85
+ | { type: 'tapId'; id: string }
86
+ | { type: 'doubleTap'; x: number; y: number; gapMs?: number }
87
+ | {
88
+ type: 'longPress'
89
+ x: number
90
+ y: number
91
+ durationMs?: number
92
+ target?: SootSimAgentActionDetail['target']
93
+ }
94
+ | {
95
+ type: 'drag'
96
+ fromX: number
97
+ fromY: number
98
+ toX: number
99
+ toY: number
100
+ steps?: number
101
+ stepMs?: number
102
+ }
103
+ | {
104
+ type: 'pinch'
105
+ fromX1: number
106
+ fromY1: number
107
+ fromX2: number
108
+ fromY2: number
109
+ toX1: number
110
+ toY1: number
111
+ toX2: number
112
+ toY2: number
113
+ steps?: number
114
+ stepMs?: number
115
+ }
116
+ | { type: 'scroll'; id: string; x?: number; y?: number; animated?: boolean }
117
+ | { type: 'type'; text: string }
118
+ | { type: 'key'; key: string }
119
+ | { type: 'dismissKeyboard' }
120
+ | { type: 'wait'; ms: number }
121
+ | { type: 'waitFor'; testID?: string; id?: string; timeoutMs?: number }
122
+
123
+ export interface PerformOptions {
124
+ // stop the batch at the first failing step. default true — a gesture whose
125
+ // touchDown missed should not keep dragging.
126
+ stopOnError?: boolean
127
+ // ceiling on batch PROGRESS, checked between steps: once it passes, the
128
+ // batch starts no further steps and reports which one it reached. a step
129
+ // already running is always awaited to completion, never abandoned, because
130
+ // a gesture that keeps dispatching pointer events after the caller has been
131
+ // told the batch failed is worse than a late reply. every step is bounded on
132
+ // its own (drag and pinch by steps * stepMs, wait by ms, waitFor by its own
133
+ // timeout), and a wedged worker is caught by the CLI client's per-command
134
+ // timeout one layer up. default 30000.
135
+ timeoutMs?: number
136
+ }
137
+
138
+ export interface PerformStepResult {
139
+ index: number
140
+ type: PerformStep['type']
141
+ ok: boolean
142
+ // step-specific payload: tap hit info, waitFor match, resolved coordinates
143
+ value?: unknown
144
+ error?: string
145
+ // wall-clock ms this step took inside the page
146
+ durationMs: number
147
+ }
148
+
149
+ export interface PerformResult {
150
+ ok: boolean
151
+ steps: PerformStepResult[]
152
+ // number of steps that ran. less than steps.length when the batch aborted.
153
+ completed: number
154
+ durationMs: number
155
+ error?: string
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // state — one round-trip situational awareness
160
+ // ---------------------------------------------------------------------------
161
+
162
+ export interface SimStateOptions {
163
+ // omit a section an agent does not need this turn
164
+ screenshot?: boolean
165
+ tree?: boolean
166
+ route?: boolean
167
+ errors?: boolean
168
+ // tree depth, matching the standalone `tree` command. default 5.
169
+ depth?: number
170
+ layers?: WsCommand['layers']
171
+ // cap on recentErrors entries. default 20.
172
+ errorLimit?: number
173
+ }
174
+
175
+ export interface SimStateResult {
176
+ // png data uri, same encoding the `screenshot` command returns
177
+ screenshot?: unknown
178
+ tree?: unknown
179
+ route?: { url?: string; screen?: string | null; stack?: string[] } | null
180
+ recentErrors?: Array<{ level: string; text: string; at?: number }>
181
+ keyboard?: { visible: boolean; mode?: string | null }
182
+ capturedAt: number
183
+ }
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // reset — two-tier app state wipe
187
+ // ---------------------------------------------------------------------------
188
+
189
+ // 'data' clears what a user could clear from inside the app: async storage,
190
+ // sqlite, cache dirs. 'full' also clears what only a fresh install would:
191
+ // simulated keychain, user defaults, granted permissions.
192
+ //
193
+ // an in-worker remount is preferred over restarting the tenant worker, because
194
+ // a restart is what makes reset too slow to run between tests. it is not
195
+ // guaranteed: the result reports which one actually happened rather than
196
+ // claiming the fast path.
197
+ export type ResetStrategy = 'data' | 'full'
198
+
199
+ export interface ResetOptions {
200
+ strategy?: ResetStrategy
201
+ // relaunch the app after wiping. default true.
202
+ relaunch?: boolean
203
+ }
204
+
205
+ export interface ResetResult {
206
+ ok: boolean
207
+ strategy: ResetStrategy
208
+ // stores verified empty after the wipe, read back rather than assumed
209
+ cleared: string[]
210
+ // stores this strategy deliberately did NOT clear, each with the reason.
211
+ // a named gap is not a failure, so this leaves `ok` true and stays out of
212
+ // `error`; a caller that cares can branch on it.
213
+ skipped?: Array<{ store: string; reason: string }>
214
+ relaunched: boolean
215
+ // true when reset had to restart the tenant worker instead of doing an
216
+ // in-worker cold remount. a restart is correct but slow, so a caller running
217
+ // reset between tests needs to see which path it got.
218
+ workerReloaded?: boolean
219
+ error?: string
220
+ }
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // camera — fixture injection
224
+ // ---------------------------------------------------------------------------
225
+
226
+ // the host thread owns the camera pipeline (hidden <video> -> <canvas> ->
227
+ // ImageBitmap -> worker). injection replaces the source of that <video>, so
228
+ // every downstream consumer — expo-camera's live frame texture and the
229
+ // BarcodeDetector scan loop — sees the fixture with no seam of its own.
230
+ export interface CameraPlayOptions {
231
+ // url the host can fetch. the cli serves the local file from the daemon.
232
+ url: string
233
+ loop?: boolean
234
+ // playback rate, for making a slow fixture scan faster in ci
235
+ rate?: number
236
+ }
237
+
238
+ export type CameraCommand =
239
+ | ({ action: 'play' } & CameraPlayOptions)
240
+ | { action: 'stop' }
241
+ | { action: 'status' }
242
+
243
+ export interface CameraResult {
244
+ ok: boolean
245
+ // 'live' = real getUserMedia, 'fixture' = injected video, 'idle' = no session
246
+ source: 'live' | 'fixture' | 'idle'
247
+ url?: string
248
+ loop?: boolean
249
+ rate?: number
250
+ error?: string
251
+ }
package/src/connect.ts ADDED
@@ -0,0 +1,83 @@
1
+ // `connect()` — the node entrypoint of the rnxsim SDK. resolves the running
2
+ // bridge world the same way the CLI does (development bridge registry, then
3
+ // daemon, then default port), picks a sim, and returns a connected SimClient
4
+ // over the shared ws transport. node-only by design: sim-client.ts stays
5
+ // browser-safe.
6
+
7
+ import { readCurrentSimId } from '../cli/current-sim'
8
+ import { createBridge, resolveBridgeWorld, type BridgeSimInfo } from '../cli/ws-bridge'
9
+ import { SimClient, SimConnectError } from './sim-client'
10
+
11
+ export interface ConnectOptions {
12
+ /** ws bridge port. default: the resolved bridge world (lockfiles). */
13
+ port?: number
14
+ /** sim id to attach to (as shown by `rnxsim list`). default: the primary sim. */
15
+ sim?: string
16
+ /** per-command bridge timeout in ms. default 15000. */
17
+ timeoutMs?: number
18
+ }
19
+
20
+ function pickSim(
21
+ sims: BridgeSimInfo[],
22
+ wanted: string | undefined,
23
+ port: number,
24
+ ): BridgeSimInfo {
25
+ const open = sims.filter((sim) => sim.readyState === 'open')
26
+ if (wanted) {
27
+ const match = open.find((sim) => sim.id === wanted)
28
+ if (!match) {
29
+ throw new SimConnectError(
30
+ `no sim connected with id ${wanted} on bridge :${port}` +
31
+ (open.length
32
+ ? ` — connected: ${open.map((sim) => sim.id).join(', ')}`
33
+ : ' — no sims connected; open one with `rnxsim open <port>`'),
34
+ )
35
+ }
36
+ return match
37
+ }
38
+ // same session affinity as the CLI: a sim pinned by `rnxsim open` /
39
+ // `rnxsim use` in this terminal wins over the primary election, so a script
40
+ // run next to CLI commands drives the sim the terminal is working with. a
41
+ // stale pin (sim gone) falls through to the primary.
42
+ const pinned = readCurrentSimId()
43
+ if (pinned) {
44
+ const match = open.find((sim) => sim.id === pinned)
45
+ if (match) return match
46
+ }
47
+ const primary = open.find((sim) => sim.isPrimary) ?? open[0]
48
+ if (!primary) {
49
+ throw new SimConnectError(
50
+ `no sims connected on bridge :${port} — open one with \`rnxsim open <port>\``,
51
+ )
52
+ }
53
+ return primary
54
+ }
55
+
56
+ export async function connect(options: ConnectOptions = {}): Promise<SimClient> {
57
+ const port = options.port ?? resolveBridgeWorld().bridgePort
58
+ const commandTimeoutMs = options.timeoutMs ?? 15_000
59
+
60
+ // discovery socket: list sims and pick the target. closed before the pinned
61
+ // client socket is opened so a failed connect never leaks a connection.
62
+ const discovery = createBridge(port, { commandTimeoutMs, cliLabel: 'sdk' })
63
+ let picked: BridgeSimInfo
64
+ try {
65
+ picked = pickSim(await discovery.listSims(), options.sim, port)
66
+ } catch (error) {
67
+ discovery.close()
68
+ if (error instanceof SimConnectError) throw error
69
+ const message = error instanceof Error ? error.message : String(error)
70
+ throw new SimConnectError(`${message} — is the rnxsim daemon or dev shell running?`)
71
+ }
72
+ discovery.close()
73
+
74
+ // pinned socket: every command targets exactly the picked sim, immune to the
75
+ // CLI's saved-pin re-resolution.
76
+ const bridge = createBridge(port, {
77
+ commandTimeoutMs,
78
+ cliLabel: 'sdk',
79
+ simId: picked.id,
80
+ simIdSource: 'flag',
81
+ })
82
+ return new SimClient(bridge, picked.id, { commandTimeoutMs })
83
+ }
@@ -11,6 +11,7 @@ const AUTOMATIC_CLEANUP_LOCK_STALE_MS = 6 * 60 * 60 * 1000
11
11
 
12
12
  export type SootsimCleanupKind =
13
13
  | 'browser-cache'
14
+ | 'camera-fixture'
14
15
  | 'recording'
15
16
  | 'runtime-archive'
16
17
  | 'runtime-version'
@@ -177,6 +178,34 @@ function collectRecordingEntries(
177
178
  })
178
179
  }
179
180
 
181
+ // videos staged by `rnxsim camera play`. a fixture is only a copy of a file
182
+ // the user still has, so it is cheap to reclaim, but the automatic sweep must
183
+ // not pull one out from under a sim that is playing it right now. aggressive
184
+ // only, matching recordings.
185
+ function collectCameraFixtureEntries(
186
+ home: string,
187
+ aggressive: boolean,
188
+ ): SootsimCleanupEntry[] {
189
+ const root = path.join(home, 'camera-fixtures')
190
+ let fixtures: fs.Dirent[]
191
+ try {
192
+ fixtures = fs.readdirSync(root, { withFileTypes: true })
193
+ } catch {
194
+ return []
195
+ }
196
+ return fixtures.map((fixture) => {
197
+ const fixturePath = path.join(root, fixture.name)
198
+ return {
199
+ path: fixturePath,
200
+ bytes: allocatedBytes(fixturePath),
201
+ kind: 'camera-fixture',
202
+ state: 'aggressive',
203
+ selected: aggressive,
204
+ detail: 'staged camera fixture video',
205
+ }
206
+ })
207
+ }
208
+
180
209
  function runtimeEntries(aggressive: boolean): {
181
210
  activeRuntime: string | null
182
211
  entries: SootsimCleanupEntry[]
@@ -255,6 +284,7 @@ export function planSootsimCleanup(
255
284
  ...collectPlaywrightCaches(home),
256
285
  ...collectElectronCaches(home),
257
286
  ...collectRecordingEntries(home, aggressive),
287
+ ...collectCameraFixtureEntries(home, aggressive),
258
288
  ].sort((left, right) => right.bytes - left.bytes || left.path.localeCompare(right.path))
259
289
  return {
260
290
  home,
package/src/home-paths.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  // ├── cache/
15
15
  // │ └── sootsim-runtime-<version>.tar.gz
16
16
  // ├── daemon.json lockfile: pid, ports, active runtime, heartbeat
17
- // ├── dev-bridge.json live Vite dev bridge pointer for local CLI commands
17
+ // ├── dev-bridges/ live Vite dev bridge records, keyed by bound port
18
18
  // ├── runtime-upgraded.json one-shot notice written by the daemon's
19
19
  // │ background updater, consumed (printed + deleted)
20
20
  // │ by the next interactive CLI run
@@ -31,7 +31,7 @@ import path from 'node:path'
31
31
  export const SOOTSIM_HOME_ENV = 'SOOTSIM_HOME'
32
32
  export const ACTIVE_RUNTIME_FILE = 'active'
33
33
  export const DAEMON_LOCKFILE = 'daemon.json'
34
- export const DEV_BRIDGE_LOCKFILE = 'dev-bridge.json'
34
+ export const DEV_BRIDGES_DIR = 'dev-bridges'
35
35
  export const CONFIG_FILE = 'config.json'
36
36
  export const DAEMON_HEARTBEAT_STALE_MS = 30_000
37
37
 
@@ -128,6 +128,13 @@ export function profilesDir(): string {
128
128
  return path.join(sootsimHomeDir(), 'profiles')
129
129
  }
130
130
 
131
+ // video fixtures staged for `rnxsim camera play`. the CLI copies the user's
132
+ // file in here and the daemon serves it by basename from this one directory,
133
+ // so the route can never be talked into reading anywhere else on disk.
134
+ export function cameraFixturesDir(): string {
135
+ return path.join(sootsimHomeDir(), 'camera-fixtures')
136
+ }
137
+
131
138
  // the launchd-managed daemon spawns ProgramArguments[0] directly, and macOS
132
139
  // Background Task Management attributes the entry to whoever code-signed
133
140
  // that binary. pointing launchd at bun directly makes Login Items say
@@ -154,8 +161,15 @@ export function daemonLockfilePath(): string {
154
161
  return path.join(sootsimHomeDir(), DAEMON_LOCKFILE)
155
162
  }
156
163
 
157
- export function devBridgeLockfilePath(): string {
158
- return path.join(sootsimHomeDir(), DEV_BRIDGE_LOCKFILE)
164
+ export function devBridgesDir(): string {
165
+ return path.join(sootsimHomeDir(), DEV_BRIDGES_DIR)
166
+ }
167
+
168
+ export function devBridgeLockfilePath(bridgePort: number): string {
169
+ if (!Number.isInteger(bridgePort) || bridgePort <= 0) {
170
+ throw new Error(`invalid development bridge port: ${bridgePort}`)
171
+ }
172
+ return path.join(devBridgesDir(), `${bridgePort}.json`)
159
173
  }
160
174
 
161
175
  export function configFilePath(): string {
@@ -421,40 +435,63 @@ export function readDaemonLockfile(): DaemonLockfile | null {
421
435
  }
422
436
  }
423
437
 
424
- export function readDevBridgeLockfile(): DevBridgeLockfile | null {
438
+ function parseDevBridgeLockfile(raw: string): DevBridgeLockfile | null {
425
439
  try {
426
- const fd = fs.openSync(devBridgeLockfilePath(), 'r')
427
- try {
428
- const buf = Buffer.alloc(DAEMON_LOCKFILE_MAX_BYTES)
429
- const bytesRead = fs.readSync(fd, buf, 0, DAEMON_LOCKFILE_MAX_BYTES, 0)
430
- const raw = buf.subarray(0, bytesRead).toString('utf8')
431
- const parsed = JSON.parse(raw) as Partial<DevBridgeLockfile>
432
- if (
433
- parsed &&
434
- parsed.schema === 1 &&
435
- parsed.source === 'vite-dev' &&
436
- typeof parsed.pid === 'number' &&
437
- typeof parsed.bridgePort === 'number' &&
438
- typeof parsed.runtimePort === 'number' &&
439
- typeof parsed.cwd === 'string' &&
440
- typeof parsed.startedAt === 'number' &&
441
- typeof parsed.heartbeatAt === 'number' &&
442
- (parsed.shellPort === undefined || typeof parsed.shellPort === 'number') &&
443
- (parsed.servedRuntimes === undefined ||
444
- (Array.isArray(parsed.servedRuntimes) &&
445
- parsed.servedRuntimes.every((version) => typeof version === 'string')))
446
- ) {
447
- return parsed as DevBridgeLockfile
448
- }
449
- return null
450
- } finally {
451
- fs.closeSync(fd)
440
+ const parsed = JSON.parse(raw)
441
+ if (
442
+ parsed &&
443
+ parsed.schema === 1 &&
444
+ parsed.source === 'vite-dev' &&
445
+ typeof parsed.pid === 'number' &&
446
+ typeof parsed.bridgePort === 'number' &&
447
+ typeof parsed.runtimePort === 'number' &&
448
+ typeof parsed.cwd === 'string' &&
449
+ typeof parsed.startedAt === 'number' &&
450
+ typeof parsed.heartbeatAt === 'number' &&
451
+ (parsed.shellPort === undefined || typeof parsed.shellPort === 'number') &&
452
+ (parsed.servedRuntimes === undefined ||
453
+ (Array.isArray(parsed.servedRuntimes) &&
454
+ parsed.servedRuntimes.every((version: unknown) => typeof version === 'string')))
455
+ ) {
456
+ return parsed
452
457
  }
458
+ return null
453
459
  } catch {
454
460
  return null
455
461
  }
456
462
  }
457
463
 
464
+ export function readDevBridgeLockfiles(): DevBridgeLockfile[] {
465
+ let entries: fs.Dirent[]
466
+ try {
467
+ entries = fs.readdirSync(devBridgesDir(), { withFileTypes: true })
468
+ } catch {
469
+ return []
470
+ }
471
+
472
+ const lockfiles: DevBridgeLockfile[] = []
473
+ for (const entry of entries) {
474
+ if (!entry.isFile() || !/^\d+\.json$/.test(entry.name)) continue
475
+ try {
476
+ const filePath = path.join(devBridgesDir(), entry.name)
477
+ const fd = fs.openSync(filePath, 'r')
478
+ try {
479
+ const buf = Buffer.alloc(DAEMON_LOCKFILE_MAX_BYTES)
480
+ const bytesRead = fs.readSync(fd, buf, 0, DAEMON_LOCKFILE_MAX_BYTES, 0)
481
+ const lockfile = parseDevBridgeLockfile(
482
+ buf.subarray(0, bytesRead).toString('utf8'),
483
+ )
484
+ if (lockfile && entry.name === `${lockfile.bridgePort}.json`) {
485
+ lockfiles.push(lockfile)
486
+ }
487
+ } finally {
488
+ fs.closeSync(fd)
489
+ }
490
+ } catch {}
491
+ }
492
+ return lockfiles.sort((left, right) => left.bridgePort - right.bridgePort)
493
+ }
494
+
458
495
  /** true when the lockfile exists, the named pid is alive, and the heartbeat
459
496
  * is recent. callers should reach for this before trusting any of the
460
497
  * ports inside.
@@ -503,8 +540,8 @@ export function readLiveRuntimeVersions(): string[] {
503
540
  if (daemon.activeRuntime) versions.add(daemon.activeRuntime)
504
541
  for (const version of daemon.servedRuntimes ?? []) versions.add(version)
505
542
  }
506
- const devBridge = readDevBridgeLockfile()
507
- if (isDevBridgeLockfileFresh(devBridge)) {
543
+ for (const devBridge of readDevBridgeLockfiles()) {
544
+ if (!isDevBridgeLockfileFresh(devBridge)) continue
508
545
  for (const version of devBridge.servedRuntimes ?? []) versions.add(version)
509
546
  }
510
547
  return [...versions].sort()
@@ -518,10 +555,11 @@ export function writeDaemonLockfile(data: DaemonLockfile): void {
518
555
  }
519
556
 
520
557
  export function writeDevBridgeLockfile(data: DevBridgeLockfile): void {
521
- ensureSootsimHome()
522
- const tmp = `${devBridgeLockfilePath()}.tmp`
558
+ fs.mkdirSync(devBridgesDir(), { recursive: true })
559
+ const lockfilePath = devBridgeLockfilePath(data.bridgePort)
560
+ const tmp = `${lockfilePath}.${data.pid}.tmp`
523
561
  fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
524
- fs.renameSync(tmp, devBridgeLockfilePath())
562
+ fs.renameSync(tmp, lockfilePath)
525
563
  }
526
564
 
527
565
  /** try to claim the lockfile atomically on daemon boot. returns true if
@@ -545,8 +583,13 @@ export function removeDaemonLockfile(): void {
545
583
  } catch {}
546
584
  }
547
585
 
548
- export function removeDevBridgeLockfile(): void {
586
+ export function removeDevBridgeLockfile(
587
+ owner: Pick<DevBridgeLockfile, 'bridgePort' | 'pid' | 'startedAt'>,
588
+ ): void {
549
589
  try {
550
- fs.unlinkSync(devBridgeLockfilePath())
590
+ const lockfilePath = devBridgeLockfilePath(owner.bridgePort)
591
+ const lockfile = parseDevBridgeLockfile(fs.readFileSync(lockfilePath, 'utf8'))
592
+ if (lockfile?.pid !== owner.pid || lockfile.startedAt !== owner.startedAt) return
593
+ fs.unlinkSync(lockfilePath)
551
594
  } catch {}
552
595
  }