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
@@ -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;
@@ -35,19 +35,309 @@ __export(src_exports, {
35
35
  DEFAULT_SOOTSIM_BRIDGE_PORT: () => DEFAULT_SOOTSIM_BRIDGE_PORT,
36
36
  DEFAULT_SOOTSIM_BRIDGE_URL: () => DEFAULT_SOOTSIM_BRIDGE_URL,
37
37
  DEFAULT_SOOTSIM_SHELL_URL: () => DEFAULT_SOOTSIM_SHELL_URL,
38
+ SimClient: () => SimClient,
39
+ SimClientError: () => SimClientError,
40
+ SimConnectError: () => SimConnectError,
41
+ SimKeyboardError: () => SimKeyboardError,
42
+ SimPerformError: () => SimPerformError,
43
+ SimReadyTimeoutError: () => SimReadyTimeoutError,
44
+ SimResetError: () => SimResetError,
45
+ SimTapError: () => SimTapError,
46
+ connect: () => connect,
38
47
  jumpToSourceBabelPlugin: () => sootsimJumpToSourceBabelPlugin,
39
48
  metroPlugin: () => metro_plugin_default,
40
49
  sootsimPlugin: () => sootsimPlugin
41
50
  });
42
51
  module.exports = __toCommonJS(src_exports);
43
52
 
44
- // src/vite-plugin-one.ts
45
- var import_fs2 = __toESM(require("fs"), 1);
46
- var import_path2 = __toESM(require("path"), 1);
53
+ // cli/current-sim.ts
54
+ var import_fs2 = require("fs");
55
+ var import_os = require("os");
56
+ var import_path = require("path");
57
+
58
+ // cli/parent-pid.ts
59
+ var import_child_process = require("child_process");
60
+ var import_fs = require("fs");
61
+ var import_module = require("module");
62
+ var cached = null;
63
+ var cachedComm = null;
64
+ function getParentPid(pid) {
65
+ if (!cached) cached = build();
66
+ try {
67
+ return cached(pid);
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+ function getProcessComm(pid) {
73
+ if (!cachedComm) cachedComm = buildComm();
74
+ try {
75
+ return cachedComm(pid);
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+ function buildComm() {
81
+ if (process.platform === "linux") {
82
+ return (pid) => {
83
+ try {
84
+ return (0, import_fs.readFileSync)(`/proc/${pid}/comm`, "utf8").trim() || null;
85
+ } catch {
86
+ return null;
87
+ }
88
+ };
89
+ }
90
+ if (process.platform === "darwin") {
91
+ const ffi = tryBuildDarwinCommFfi();
92
+ if (ffi) return ffi;
93
+ return (pid) => readProcessTable().get(pid)?.comm || null;
94
+ }
95
+ return () => null;
96
+ }
97
+ function build() {
98
+ const envPpid = Number(process.env.PPID);
99
+ if (Number.isFinite(envPpid) && envPpid > 1 && envPpid !== process.ppid) {
100
+ return () => envPpid;
101
+ }
102
+ if (process.platform === "linux") return buildLinux();
103
+ if (process.platform === "darwin") {
104
+ const ffi = tryBuildDarwinFfi();
105
+ if (ffi) return ffi;
106
+ return (pid) => {
107
+ const ppid = readProcessTable().get(pid)?.ppid;
108
+ return ppid !== void 0 && ppid > 0 ? ppid : null;
109
+ };
110
+ }
111
+ return () => null;
112
+ }
113
+ var PROCESS_TABLE_TTL_MS = 2e3;
114
+ var processTable = { readAt: 0, rows: /* @__PURE__ */ new Map() };
115
+ function readProcessTable() {
116
+ const now = Date.now();
117
+ if (processTable.readAt && now - processTable.readAt < PROCESS_TABLE_TTL_MS) {
118
+ return processTable.rows;
119
+ }
120
+ const rows = /* @__PURE__ */ new Map();
121
+ try {
122
+ const out = (0, import_child_process.execFileSync)("ps", ["-eo", "pid=,ppid=,comm="], {
123
+ encoding: "utf8",
124
+ timeout: 5e3,
125
+ maxBuffer: 8 * 1024 * 1024,
126
+ stdio: ["ignore", "pipe", "ignore"]
127
+ });
128
+ for (const line of out.split("\n")) {
129
+ const parsed = line.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/);
130
+ if (!parsed) continue;
131
+ rows.set(Number(parsed[1]), {
132
+ ppid: Number(parsed[2]),
133
+ comm: (parsed[3].split("/").at(-1) ?? "").slice(0, PBI_COMM_LEN)
134
+ });
135
+ }
136
+ } catch {
137
+ }
138
+ processTable = { readAt: now, rows };
139
+ return rows;
140
+ }
141
+ function buildLinux() {
142
+ return (pid) => {
143
+ try {
144
+ const raw = (0, import_fs.readFileSync)(`/proc/${pid}/stat`, "utf8");
145
+ const end = raw.lastIndexOf(")");
146
+ if (end < 0) return null;
147
+ const rest = raw.slice(end + 1).trim().split(/\s+/);
148
+ const ppid = Number(rest[1]);
149
+ return Number.isFinite(ppid) && ppid > 0 ? ppid : null;
150
+ } catch {
151
+ return null;
152
+ }
153
+ };
154
+ }
155
+ var PROC_PIDTBSDINFO = 3;
156
+ var PROC_BSDINFO_SIZE = 216;
157
+ var PBI_PPID_OFFSET = 16;
158
+ var PBI_COMM_OFFSET = 48;
159
+ var PBI_COMM_LEN = 16;
160
+ function tryBuildDarwinFfi() {
161
+ if (!process.versions?.bun) return null;
162
+ try {
163
+ const r = (0, import_module.createRequire)(__sootsim_import_meta_url);
164
+ const { dlopen, FFIType } = r("bun:ffi");
165
+ const lib = dlopen("/usr/lib/libproc.dylib", {
166
+ proc_pidinfo: {
167
+ args: [FFIType.i32, FFIType.i32, FFIType.u64, FFIType.ptr, FFIType.i32],
168
+ returns: FFIType.i32
169
+ }
170
+ });
171
+ const buf = Buffer.alloc(PROC_BSDINFO_SIZE);
172
+ return (pid) => {
173
+ const ret = Number(
174
+ lib.symbols.proc_pidinfo(pid, PROC_PIDTBSDINFO, 0n, buf, PROC_BSDINFO_SIZE)
175
+ );
176
+ if (ret <= 0) return null;
177
+ const ppid = buf.readUInt32LE(PBI_PPID_OFFSET);
178
+ return ppid > 0 ? ppid : null;
179
+ };
180
+ } catch {
181
+ return null;
182
+ }
183
+ }
184
+ function tryBuildDarwinCommFfi() {
185
+ if (!process.versions?.bun) return null;
186
+ try {
187
+ const r = (0, import_module.createRequire)(__sootsim_import_meta_url);
188
+ const { dlopen, FFIType } = r("bun:ffi");
189
+ const lib = dlopen("/usr/lib/libproc.dylib", {
190
+ proc_pidinfo: {
191
+ args: [FFIType.i32, FFIType.i32, FFIType.u64, FFIType.ptr, FFIType.i32],
192
+ returns: FFIType.i32
193
+ }
194
+ });
195
+ const buf = Buffer.alloc(PROC_BSDINFO_SIZE);
196
+ return (pid) => {
197
+ const ret = Number(
198
+ lib.symbols.proc_pidinfo(pid, PROC_PIDTBSDINFO, 0n, buf, PROC_BSDINFO_SIZE)
199
+ );
200
+ if (ret <= 0) return null;
201
+ const raw = buf.subarray(PBI_COMM_OFFSET, PBI_COMM_OFFSET + PBI_COMM_LEN);
202
+ const end = raw.indexOf(0);
203
+ const comm = raw.subarray(0, end < 0 ? raw.length : end).toString("utf8");
204
+ return comm || null;
205
+ };
206
+ } catch {
207
+ return null;
208
+ }
209
+ }
210
+
211
+ // cli/current-sim.ts
212
+ var CURRENT_SIM_VERSION = 1;
213
+ var CURRENT_SIM_PATH_ENV = "SOOTSIM_CLI_CURRENT_SIM_PATH";
214
+ var IDENTITY_ENV_VARS = [
215
+ "SOOTSIM_CLI_IDENTITY",
216
+ // explicit override — fastest path, skips all pid lookups
217
+ "TM_SESSION",
218
+ // Team Machine session id; routes prompt-bar followups back to this agent
219
+ "CLAUDE_CODE_SESSION_ID",
220
+ // claude code, if ever set
221
+ "CODEX_THREAD_ID",
222
+ // codex / openai agent thread, stable across tool invocations
223
+ "TERM_SESSION_ID",
224
+ // macOS Terminal.app + iTerm2
225
+ "ITERM_SESSION_ID",
226
+ // iTerm2 (older variant)
227
+ "TMUX_PANE",
228
+ // tmux — stable per pane
229
+ "STY",
230
+ // GNU screen session
231
+ "KITTY_WINDOW_ID",
232
+ // kitty
233
+ "WEZTERM_PANE",
234
+ // wezterm
235
+ "ALACRITTY_WINDOW_ID",
236
+ // alacritty
237
+ "WINDOWID",
238
+ // X11 terminals
239
+ "VSCODE_INJECTION"
240
+ // vscode integrated terminal
241
+ ];
242
+ var IMMORTAL_HOST_COMM_RE = /^(?:tmux|screen)|^tm$/i;
243
+ function findTopAncestor(start, maxDepth = 20) {
244
+ let prev = start;
245
+ let pid = getParentPid(start);
246
+ if (!pid || pid <= 1) {
247
+ const comm = getProcessComm(start);
248
+ return start > 1 && comm && !IMMORTAL_HOST_COMM_RE.test(comm) ? start : null;
249
+ }
250
+ for (let i = 0; i < maxDepth; i++) {
251
+ const comm = getProcessComm(pid);
252
+ if (comm && IMMORTAL_HOST_COMM_RE.test(comm)) return prev;
253
+ const parent = getParentPid(pid);
254
+ if (!parent || parent <= 1) {
255
+ return comm ? pid : prev;
256
+ }
257
+ prev = pid;
258
+ pid = parent;
259
+ }
260
+ return pid;
261
+ }
262
+ function getCliIdentity() {
263
+ for (const name of IDENTITY_ENV_VARS) {
264
+ const value = process.env[name];
265
+ if (value && value.trim()) {
266
+ return { key: `${name}:${value.trim()}`, source: name, stable: true };
267
+ }
268
+ }
269
+ const topPid = findTopAncestor(process.ppid);
270
+ if (topPid && topPid > 1) {
271
+ return { key: `gppid-${topPid}`, source: "grand-ppid", stable: true };
272
+ }
273
+ return { key: `pid-${process.ppid}`, source: "ppid", stable: false };
274
+ }
275
+ function getCliIdentityKey() {
276
+ return getCliIdentity().key;
277
+ }
278
+ function getCurrentSimPath() {
279
+ if (process.env[CURRENT_SIM_PATH_ENV]) {
280
+ return (0, import_path.resolve)(process.env[CURRENT_SIM_PATH_ENV]);
281
+ }
282
+ const key = getCliIdentityKey();
283
+ return (0, import_path.join)((0, import_os.tmpdir)(), `sootsim-current-sim-${key}.json`);
284
+ }
285
+ function readCurrentSim() {
286
+ const filepath = getCurrentSimPath();
287
+ if (!(0, import_fs2.existsSync)(filepath)) return null;
288
+ try {
289
+ const parsed = JSON.parse((0, import_fs2.readFileSync)(filepath, "utf8"));
290
+ if (parsed.version !== CURRENT_SIM_VERSION || typeof parsed.simId !== "string" || !parsed.simId.trim() || typeof parsed.updatedAt !== "string") {
291
+ (0, import_fs2.rmSync)(filepath, { force: true });
292
+ return null;
293
+ }
294
+ return {
295
+ version: CURRENT_SIM_VERSION,
296
+ simId: parsed.simId.trim(),
297
+ updatedAt: parsed.updatedAt
298
+ };
299
+ } catch {
300
+ (0, import_fs2.rmSync)(filepath, { force: true });
301
+ return null;
302
+ }
303
+ }
304
+ function readCurrentSimId() {
305
+ return readCurrentSim()?.simId || null;
306
+ }
307
+ function clearCurrentSimId() {
308
+ (0, import_fs2.rmSync)(getCurrentSimPath(), { force: true });
309
+ }
310
+
311
+ // cli/ws-bridge.ts
312
+ var import_node_fs2 = __toESM(require("node:fs"), 1);
313
+ var import_node_path2 = __toESM(require("node:path"), 1);
314
+ var import_ws = require("ws");
47
315
 
48
316
  // src/bridge-constants.ts
49
317
  var DEFAULT_SOOTSIM_BRIDGE_PORT = 7668;
50
318
  var DEFAULT_SOOTSIM_BRIDGE_URL = `http://localhost:${DEFAULT_SOOTSIM_BRIDGE_PORT}/`;
319
+ function parsePort(value) {
320
+ if (typeof value === "number") {
321
+ return Number.isInteger(value) && value > 0 ? value : null;
322
+ }
323
+ if (typeof value !== "string") return null;
324
+ const trimmed = value.trim();
325
+ if (!trimmed) return null;
326
+ const parsed = Number(trimmed);
327
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
328
+ }
329
+ function parsePortOffset(value) {
330
+ const parsed = parsePort(value);
331
+ return parsed === null ? 0 : parsed;
332
+ }
333
+ function resolveSootsimBridgePort(input = {}) {
334
+ const explicitPort = parsePort(input.explicitPort);
335
+ if (explicitPort !== null) return explicitPort;
336
+ return DEFAULT_SOOTSIM_BRIDGE_PORT + parsePortOffset(input.portOffset);
337
+ }
338
+
339
+ // src/cli-constants.ts
340
+ var DEFAULT_SOOTSIM_SHELL_URL = "http://localhost:5173/";
51
341
 
52
342
  // src/home-paths.ts
53
343
  var import_node_fs = __toESM(require("node:fs"), 1);
@@ -56,6 +346,7 @@ var import_node_path = __toESM(require("node:path"), 1);
56
346
  var SOOTSIM_HOME_ENV = "SOOTSIM_HOME";
57
347
  var ACTIVE_RUNTIME_FILE = "active";
58
348
  var DAEMON_LOCKFILE = "daemon.json";
349
+ var DEV_BRIDGES_DIR = "dev-bridges";
59
350
  var DAEMON_HEARTBEAT_STALE_MS = 3e4;
60
351
  function sootsimHomeDir() {
61
352
  const override = process.env[SOOTSIM_HOME_ENV];
@@ -74,6 +365,9 @@ function activeRuntimeFile() {
74
365
  function daemonLockfilePath() {
75
366
  return import_node_path.default.join(sootsimHomeDir(), DAEMON_LOCKFILE);
76
367
  }
368
+ function devBridgesDir() {
369
+ return import_node_path.default.join(sootsimHomeDir(), DEV_BRIDGES_DIR);
370
+ }
77
371
  function readActiveRuntime() {
78
372
  try {
79
373
  const value = import_node_fs.default.readFileSync(activeRuntimeFile(), "utf8").trim();
@@ -102,6 +396,47 @@ function readDaemonLockfile() {
102
396
  return null;
103
397
  }
104
398
  }
399
+ function parseDevBridgeLockfile(raw) {
400
+ try {
401
+ const parsed = JSON.parse(raw);
402
+ if (parsed && parsed.schema === 1 && parsed.source === "vite-dev" && typeof parsed.pid === "number" && typeof parsed.bridgePort === "number" && typeof parsed.runtimePort === "number" && typeof parsed.cwd === "string" && typeof parsed.startedAt === "number" && typeof parsed.heartbeatAt === "number" && (parsed.shellPort === void 0 || typeof parsed.shellPort === "number") && (parsed.servedRuntimes === void 0 || Array.isArray(parsed.servedRuntimes) && parsed.servedRuntimes.every((version) => typeof version === "string"))) {
403
+ return parsed;
404
+ }
405
+ return null;
406
+ } catch {
407
+ return null;
408
+ }
409
+ }
410
+ function readDevBridgeLockfiles() {
411
+ let entries;
412
+ try {
413
+ entries = import_node_fs.default.readdirSync(devBridgesDir(), { withFileTypes: true });
414
+ } catch {
415
+ return [];
416
+ }
417
+ const lockfiles = [];
418
+ for (const entry of entries) {
419
+ if (!entry.isFile() || !/^\d+\.json$/.test(entry.name)) continue;
420
+ try {
421
+ const filePath = import_node_path.default.join(devBridgesDir(), entry.name);
422
+ const fd = import_node_fs.default.openSync(filePath, "r");
423
+ try {
424
+ const buf = Buffer.alloc(DAEMON_LOCKFILE_MAX_BYTES);
425
+ const bytesRead = import_node_fs.default.readSync(fd, buf, 0, DAEMON_LOCKFILE_MAX_BYTES, 0);
426
+ const lockfile = parseDevBridgeLockfile(
427
+ buf.subarray(0, bytesRead).toString("utf8")
428
+ );
429
+ if (lockfile && entry.name === `${lockfile.bridgePort}.json`) {
430
+ lockfiles.push(lockfile);
431
+ }
432
+ } finally {
433
+ import_node_fs.default.closeSync(fd);
434
+ }
435
+ } catch {
436
+ }
437
+ }
438
+ return lockfiles.sort((left, right) => left.bridgePort - right.bridgePort);
439
+ }
105
440
  function isDaemonLockfileFresh(lock, now = Date.now()) {
106
441
  if (!lock) return false;
107
442
  if (now - lock.heartbeatAt > DAEMON_HEARTBEAT_STALE_MS) return false;
@@ -112,10 +447,2444 @@ function isDaemonLockfileFresh(lock, now = Date.now()) {
112
447
  return false;
113
448
  }
114
449
  }
450
+ function isDevBridgeLockfileFresh(lock, now = Date.now()) {
451
+ if (!lock) return false;
452
+ if (now - lock.heartbeatAt > DAEMON_HEARTBEAT_STALE_MS) return false;
453
+ try {
454
+ process.kill(lock.pid, 0);
455
+ return true;
456
+ } catch {
457
+ return false;
458
+ }
459
+ }
460
+
461
+ // cli/ws-bridge.ts
462
+ var resolvedWorld = null;
463
+ function liveDevBridgeLockfiles() {
464
+ return readDevBridgeLockfiles().filter((lockfile) => isDevBridgeLockfileFresh(lockfile));
465
+ }
466
+ function checkoutRoot(cwd) {
467
+ let current = import_node_path2.default.resolve(cwd);
468
+ while (true) {
469
+ if (import_node_fs2.default.existsSync(import_node_path2.default.join(current, ".git"))) return current;
470
+ const parent = import_node_path2.default.dirname(current);
471
+ if (parent === current) return null;
472
+ current = parent;
473
+ }
474
+ }
475
+ function pathContains(parent, child) {
476
+ const relative = import_node_path2.default.relative(parent, child);
477
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${import_node_path2.default.sep}`) && !import_node_path2.default.isAbsolute(relative);
478
+ }
479
+ function resolveDefaultDevBridgeLockfile() {
480
+ const lockfiles = liveDevBridgeLockfiles();
481
+ if (lockfiles.length === 0) return null;
482
+ if (process.env.SOOTSIM_BRIDGE_PORT !== void 0 || process.env.PORT_OFFSET !== void 0) {
483
+ const requestedPort = resolveSootsimBridgePort({
484
+ explicitPort: process.env.SOOTSIM_BRIDGE_PORT,
485
+ portOffset: process.env.PORT_OFFSET
486
+ });
487
+ const requested = lockfiles.find((lockfile) => lockfile.bridgePort === requestedPort);
488
+ if (requested) return requested;
489
+ }
490
+ let cwd = null;
491
+ try {
492
+ cwd = process.cwd();
493
+ } catch {
494
+ }
495
+ if (cwd) {
496
+ const currentCheckout = checkoutRoot(cwd);
497
+ const sameCheckout = lockfiles.filter((lockfile) => {
498
+ const bridgeCheckout = checkoutRoot(lockfile.cwd);
499
+ if (currentCheckout && bridgeCheckout) return currentCheckout === bridgeCheckout;
500
+ return pathContains(cwd, lockfile.cwd) || pathContains(lockfile.cwd, cwd);
501
+ });
502
+ if (sameCheckout.length > 0) {
503
+ return sameCheckout.find(
504
+ (lockfile) => lockfile.bridgePort === DEFAULT_SOOTSIM_BRIDGE_PORT
505
+ ) ?? sameCheckout[0];
506
+ }
507
+ }
508
+ return lockfiles.find((lockfile) => lockfile.bridgePort === DEFAULT_SOOTSIM_BRIDGE_PORT) ?? lockfiles[0];
509
+ }
510
+ function resolveBridgeWorld() {
511
+ const homeDir = sootsimHomeDir();
512
+ if (resolvedWorld?.homeDir === homeDir) return resolvedWorld.world;
513
+ const world = resolveBridgeWorldFresh();
514
+ resolvedWorld = { homeDir, world };
515
+ return world;
516
+ }
517
+ function resolveBridgeWorldFresh() {
518
+ const dev = resolveDefaultDevBridgeLockfile();
519
+ if (dev) {
520
+ return {
521
+ source: "dev-bridge",
522
+ bridgePort: dev.bridgePort,
523
+ // older lockfile writers don't record the vite http port; the default
524
+ // shell URL is the dev shell anyway, so the fallback stays coherent.
525
+ shellBaseUrl: dev.shellPort ? `http://localhost:${dev.shellPort}/` : DEFAULT_SOOTSIM_SHELL_URL
526
+ };
527
+ }
528
+ const lock = readDaemonLockfile();
529
+ if (lock && isDaemonLockfileFresh(lock)) {
530
+ return {
531
+ source: "daemon",
532
+ bridgePort: lock.bridgePort,
533
+ shellBaseUrl: lock.runtimePort > 0 ? `http://localhost:${lock.runtimePort}/` : DEFAULT_SOOTSIM_SHELL_URL
534
+ };
535
+ }
536
+ return {
537
+ source: "default",
538
+ bridgePort: DEFAULT_SOOTSIM_BRIDGE_PORT,
539
+ shellBaseUrl: DEFAULT_SOOTSIM_SHELL_URL
540
+ };
541
+ }
542
+ var BridgeSimLockedError = class extends Error {
543
+ lock;
544
+ constructor(message, lock) {
545
+ super(message);
546
+ this.name = "BridgeSimLockedError";
547
+ this.lock = lock;
548
+ }
549
+ };
550
+ function describeSimIdSource(source) {
551
+ if (source === "flag") return "via --sim";
552
+ if (source === "saved") return "saved via `rnxsim use`";
553
+ return "primary fallback \u2014 no sim pinned";
554
+ }
555
+ function createBridge(wsPort, opts = {}) {
556
+ let nextId = 1;
557
+ const commandTimeoutMs = opts.commandTimeoutMs ?? 15e3;
558
+ const pending = /* @__PURE__ */ new Map();
559
+ const ws = new import_ws.WebSocket(`ws://localhost:${wsPort}`);
560
+ const identity = getCliIdentity();
561
+ const ready = new Promise((resolve2, reject) => {
562
+ ws.on("open", () => {
563
+ try {
564
+ ws.send(
565
+ JSON.stringify({
566
+ type: "bridge:hello",
567
+ id: 0,
568
+ cliIdentityKey: identity.key,
569
+ cliIdentitySource: identity.source,
570
+ cliLabel: opts.cliLabel
571
+ })
572
+ );
573
+ } catch {
574
+ }
575
+ resolve2();
576
+ });
577
+ ws.on("error", (err) => {
578
+ const cause = err.message ? `: ${err.message}` : "";
579
+ reject(new Error(`could not connect to ws://localhost:${wsPort}${cause}`));
580
+ });
581
+ });
582
+ ready.catch(() => {
583
+ });
584
+ let warnedAboutContention = false;
585
+ let noticedTarget = false;
586
+ function maybeNoticeTarget(cmdType, simId) {
587
+ if (noticedTarget) return;
588
+ if (process.env.SOOTSIM_QUIET_TARGET_NOTICE === "1") return;
589
+ if (cmdType.startsWith("bridge:") || cmdType === "focus" || cmdType === "close")
590
+ return;
591
+ noticedTarget = true;
592
+ const label = simId ?? "primary";
593
+ const source = describeSimIdSource(simId ? opts.simIdSource : "none");
594
+ process.stderr.write(` \u2192 ${label} (${source})
595
+ `);
596
+ }
597
+ ws.on("message", (data) => {
598
+ let msg;
599
+ try {
600
+ msg = JSON.parse(data.toString());
601
+ } catch {
602
+ return;
603
+ }
604
+ if (msg.id === 0) return;
605
+ const p = pending.get(msg.id);
606
+ if (!p) return;
607
+ pending.delete(msg.id);
608
+ if (msg._otherCliCount > 0 && !warnedAboutContention) {
609
+ warnedAboutContention = true;
610
+ process.stderr.write(
611
+ `
612
+ \u26A0 ${msg._otherCliCount} other CLI identity/identities are driving this sim
613
+ taps, scrolls, and keyboard input from multiple agents will collide on
614
+ the same screen state (this is not a bridge throughput limit).
615
+ use \`rnxsim open --new\` for an isolated sim per agent.
616
+
617
+ `
618
+ );
619
+ }
620
+ if (msg.error) {
621
+ if (msg._locked) {
622
+ p.reject(new BridgeSimLockedError(msg.error, msg._locked));
623
+ } else {
624
+ p.reject(new Error(msg.error));
625
+ }
626
+ } else {
627
+ p.resolve(msg.result);
628
+ }
629
+ });
630
+ ws.on("close", (code, reason) => {
631
+ const msg = reason?.toString() || "connection closed";
632
+ for (const [id, p] of pending) {
633
+ pending.delete(id);
634
+ p.reject(new Error(`sim disconnected: ${msg} (code ${code})`));
635
+ }
636
+ });
637
+ return {
638
+ async send(cmd, callOpts) {
639
+ const effectiveTimeoutMs = callOpts?.timeoutMs ?? commandTimeoutMs;
640
+ const sendOnce = async (defaultSimId) => {
641
+ await ready;
642
+ const id = nextId++;
643
+ return new Promise((resolve2, reject) => {
644
+ const timeout = setTimeout(() => {
645
+ pending.delete(id);
646
+ reject(
647
+ new Error(
648
+ `command timed out after ${Math.round(effectiveTimeoutMs / 1e3)}s`
649
+ )
650
+ );
651
+ }, effectiveTimeoutMs);
652
+ pending.set(id, {
653
+ resolve: (v) => {
654
+ clearTimeout(timeout);
655
+ resolve2(v);
656
+ },
657
+ reject: (e) => {
658
+ clearTimeout(timeout);
659
+ reject(e);
660
+ }
661
+ });
662
+ const payload = { ...cmd, id };
663
+ if (payload.simId === void 0 && defaultSimId) {
664
+ payload.simId = defaultSimId;
665
+ }
666
+ maybeNoticeTarget(cmd.type ?? "", payload.simId);
667
+ ws.send(JSON.stringify(payload));
668
+ });
669
+ };
670
+ const sentSimId = opts.simIdSource === "flag" ? opts.simId : readCurrentSimId() ?? opts.simId;
671
+ try {
672
+ return await sendOnce(sentSimId);
673
+ } catch (error) {
674
+ const message = error instanceof Error ? error.message : String(error);
675
+ const missingSimMessage = `no sim connected with id ${sentSimId}`;
676
+ if (opts.simIdSource !== "flag" && sentSimId && cmd.simId === void 0 && (message === missingSimMessage || message.startsWith(`${missingSimMessage};`))) {
677
+ clearCurrentSimId();
678
+ throw new Error(
679
+ `saved sim ${sentSimId} is gone${message.slice(missingSimMessage.length)}; run \`rnxsim list\` and \`rnxsim use <sim>\`, or open a fresh sim`
680
+ );
681
+ }
682
+ throw error;
683
+ }
684
+ },
685
+ async listSims() {
686
+ const sims = await this.send({ type: "bridge:list-sims" });
687
+ return Array.isArray(sims) ? sims : [];
688
+ },
689
+ async focusSim(simId) {
690
+ return this.send({ type: "focus", simId });
691
+ },
692
+ async closeSim(simId) {
693
+ return this.send({ type: "close", simId });
694
+ },
695
+ async claim(simId, claimOpts = {}) {
696
+ const result = await this.send({
697
+ type: "bridge:claim",
698
+ simId,
699
+ force: claimOpts.force === true
700
+ });
701
+ return result;
702
+ },
703
+ close() {
704
+ try {
705
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
706
+ ws.send(JSON.stringify({ type: "bridge:bye", id: 0 }));
707
+ }
708
+ } catch {
709
+ }
710
+ ws.close();
711
+ const terminateTimer = setTimeout(() => {
712
+ if (ws.readyState !== import_ws.WebSocket.CLOSED) {
713
+ ws.terminate();
714
+ }
715
+ }, 250);
716
+ }
717
+ };
718
+ }
719
+
720
+ // cli/commands/inspect/settling.ts
721
+ async function waitForSootsimIdle({
722
+ bridge,
723
+ simId,
724
+ maxMs,
725
+ pollMs = 50,
726
+ stablePolls = 3,
727
+ strict = false
728
+ }) {
729
+ const result = await bridge.send(
730
+ {
731
+ type: "evaluate",
732
+ simId,
733
+ code: `(async () => {
734
+ const start = Date.now()
735
+ const deadline = start + ${Math.max(0, Math.round(maxMs))}
736
+ const pollMs = ${Math.max(1, Math.round(pollMs))}
737
+ const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
738
+ const strict = ${strict ? "true" : "false"}
739
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
740
+
741
+ // route-aware settle: a tap that pushes/pops a screen is async \u2014 the
742
+ // old screen still renders for a moment, then the new one mounts and
743
+ // its content loads. a pure layout-hash check reports "idle" on the
744
+ // outgoing screen or on the incoming skeleton, so a driver re-taps
745
+ // and stacks duplicate navigations. drain any in-flight screen
746
+ // transition FIRST, bounded by the overall budget. when no
747
+ // transition is happening this returns in ~80ms (startWindowMs), so
748
+ // a plain button tap barely pays for it.
749
+ try {
750
+ const wfst = window.__sootsimTest?.waitForScreenTransitions
751
+ if (typeof wfst === 'function') {
752
+ const remaining = deadline - Date.now()
753
+ if (remaining > 120) {
754
+ await wfst({
755
+ timeoutMs: Math.min(remaining - 80, 4000),
756
+ settleMs: 64,
757
+ startWindowMs: 80,
758
+ })
759
+ }
760
+ }
761
+ } catch {}
762
+
763
+ const readSnapshot = async () => {
764
+ let animating = false
765
+ let pendingFetches = 0
766
+ let requestTotal = 0
767
+ let requestInFlight = 0
768
+ try {
769
+ const stats = await window.__sootsimRenderHost?.queryStats?.()
770
+ if (stats) {
771
+ animating =
772
+ stats.hasActiveAnims === true ||
773
+ stats.hasActiveNativeAnimations === true ||
774
+ stats.hasPendingAnimationFrames === true
775
+ // a freshly-pushed screen showing a skeleton is layout-stable
776
+ // but not actually settled \u2014 its data/images are still in
777
+ // flight. treat bounded image-loader fetches as not-idle so
778
+ // settle waits for real content, capped by the budget.
779
+ const pf = stats.memory && stats.memory.imageLoader
780
+ ? stats.memory.imageLoader.pendingFetches
781
+ : 0
782
+ pendingFetches = typeof pf === 'number' ? pf : 0
783
+ }
784
+ } catch {}
785
+ try {
786
+ const counts = await window.__sootsimTest?.getRequestCounts?.()
787
+ requestTotal = typeof counts?.total === 'number' ? counts.total : 0
788
+ requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
789
+ } catch {}
790
+ const root = window.__sootsimRoot
791
+ const nodes = []
792
+ if (root) {
793
+ const walk = (n) => {
794
+ if (n.layout && n.layout.width > 0) {
795
+ nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
796
+ }
797
+ for (const c of n.children || []) walk(c)
798
+ }
799
+ walk(root)
800
+ }
801
+ return { layout: nodes.join(';'), animating, pendingFetches, requestTotal, requestInFlight }
802
+ }
803
+
804
+ // how long generic background network is allowed to keep us waiting AFTER
805
+ // the screen is otherwise visually settled. apps with continuous traffic
806
+ // (polling, realtime sockets, a tap that fires 20-50 fetches) never reach
807
+ // requestInFlight===0 / a stable requestTotal, so requiring full network
808
+ // quiet burns the ENTIRE budget on every command \u2014 the leading idle that
809
+ // failed PR-preview recordings for network-heavy apps (3pc fight-pulse).
810
+ // layout-stability + the image-loader already prove visible content
811
+ // loaded (a data load changes layout; an image load shows in the loader),
812
+ // so the generic request counters are a courtesy, not a gate: honor them
813
+ // only within this grace once the screen is visually still.
814
+ const networkQuietGraceMs = 1200
815
+ let lastLayout = ''
816
+ let lastRequestTotal = -1
817
+ let stable = 0
818
+ let visuallyStillSince = 0
819
+ while (Date.now() < deadline) {
820
+ const snapshot = await readSnapshot()
821
+ const strictOk = !strict || !snapshot.animating
822
+ // visually settled: no animations, no image content loading, layout
823
+ // unchanged since the last poll.
824
+ const visuallyStill =
825
+ strictOk && snapshot.pendingFetches === 0 && snapshot.layout === lastLayout
826
+ // generic network quiet: nothing in flight and no new requests issued.
827
+ const networkQuiet =
828
+ snapshot.requestInFlight === 0 && snapshot.requestTotal === lastRequestTotal
829
+ if (visuallyStill) {
830
+ stable++
831
+ if (visuallyStillSince === 0) visuallyStillSince = Date.now()
832
+ // settle once the screen has held still long enough AND either the
833
+ // network is genuinely quiet OR it has refused to quiet within the
834
+ // grace (continuous background traffic must not block forever).
835
+ if (
836
+ stable >= requiredStablePolls &&
837
+ (networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
838
+ ) {
839
+ return { settled: true, elapsed: Date.now() - start }
840
+ }
841
+ } else {
842
+ stable = 0
843
+ visuallyStillSince = 0
844
+ }
845
+ lastLayout = snapshot.layout
846
+ lastRequestTotal = snapshot.requestTotal
847
+ await sleep(pollMs)
848
+ }
849
+ return { settled: false, elapsed: Date.now() - start }
850
+ })()`
851
+ },
852
+ {
853
+ timeoutMs: maxMs + 1e3
854
+ }
855
+ );
856
+ const { elapsed, settled } = result ?? {};
857
+ return {
858
+ elapsed: typeof elapsed === "number" ? elapsed : maxMs,
859
+ settled: settled === true
860
+ };
861
+ }
862
+
863
+ // cli/commands/inspect/actions.ts
864
+ var SCREEN_EVAL = `(() => {
865
+ const spec = window.SootSim?.state?.engineSnapshot?.windowState?.deviceSpec
866
+ return spec && spec.width > 0 && spec.height > 0
867
+ ? { width: spec.width, height: spec.height }
868
+ : null
869
+ })()`;
870
+ var DEFAULT_AGENT_TIMING = {
871
+ initialWaitMs: 3e3,
872
+ deadlineMs: 5e3,
873
+ retryWaitMs: 700
874
+ };
875
+ var DEFAULT_INTERACTIVE_TIMING = {
876
+ initialWaitMs: 1200,
877
+ deadlineMs: 2500,
878
+ retryWaitMs: 700
879
+ };
880
+ function tapTiming(agent, opts = {}) {
881
+ return {
882
+ ...agent ? DEFAULT_AGENT_TIMING : DEFAULT_INTERACTIVE_TIMING,
883
+ ...opts
884
+ };
885
+ }
886
+ function isTapSuccess(result) {
887
+ if (!result || result.hit === false || result.ok === false) return false;
888
+ if (result.requestedTargetMatched === false) return false;
889
+ if (result.pointerTapHandled === false && !result.keyboardOpened && !result.isTextInput) {
890
+ return false;
891
+ }
892
+ return true;
893
+ }
894
+ function tapTargetFromPayload(payload, textFallback) {
895
+ return {
896
+ nodeId: payload.target?.nodeId ?? payload.match?.nodeId ?? payload.node?.nodeId ?? null,
897
+ id: payload.target?.id ?? payload.match?.id ?? payload.node?.id ?? null,
898
+ testID: payload.target?.testID ?? payload.match?.testID ?? payload.node?.testID ?? null,
899
+ text: payload.target?.text ?? payload.target?.accessibilityLabel ?? payload.match?.text ?? payload.match?.accessibilityLabel ?? payload.node?.text ?? textFallback ?? null,
900
+ type: payload.target?.type ?? payload.match?.type ?? payload.node?.type ?? null
901
+ };
902
+ }
903
+ async function tapResolvedTarget(bridge, args) {
904
+ const timing = tapTiming(!!args.agent, args.timing);
905
+ let attempts = 0;
906
+ let lastPayload = null;
907
+ let lastResult = null;
908
+ try {
909
+ await waitForSootsimIdle({
910
+ bridge,
911
+ maxMs: timing.initialWaitMs,
912
+ pollMs: 32,
913
+ stablePolls: 2
914
+ });
915
+ } catch {
916
+ }
917
+ const deadline = Date.now() + timing.deadlineMs;
918
+ let lastOffscreenAt = null;
919
+ while (Date.now() <= deadline || attempts === 0) {
920
+ attempts++;
921
+ const payload = await args.resolve();
922
+ lastPayload = payload;
923
+ if (payload?.error === "bridge-not-ready" || payload?.ambiguous || payload?.nthOutOfRange) {
924
+ return { payload, result: null, attempts, failure: "special" };
925
+ }
926
+ if (payload && typeof payload.cx === "number" && typeof payload.cy === "number") {
927
+ if (payload.offscreen) {
928
+ lastResult = {
929
+ hit: false,
930
+ reason: "offscreen",
931
+ x: payload.cx,
932
+ y: payload.cy,
933
+ screen: payload.screen
934
+ };
935
+ const at = `${Math.round(payload.cx)},${Math.round(payload.cy)}`;
936
+ if (lastOffscreenAt === at) {
937
+ return { payload, result: lastResult, attempts, failure: "missed" };
938
+ }
939
+ lastOffscreenAt = at;
940
+ } else {
941
+ lastOffscreenAt = null;
942
+ const requestedTarget = tapTargetFromPayload(payload, args.textFallback);
943
+ const verification = await bridge.send({
944
+ type: "evaluate",
945
+ code: `(async () => {
946
+ const t = window.__sootsimTest
947
+ if (
948
+ !t ||
949
+ typeof t.inspectAt !== 'function' ||
950
+ typeof t.resolveTapTarget !== 'function'
951
+ ) {
952
+ return { error: 'tap-target-inspection-unavailable' }
953
+ }
954
+ const requestedTarget = ${JSON.stringify(requestedTarget)}
955
+ const inspected = await t.inspectAt(${payload.cx}, ${payload.cy})
956
+ const resolved =
957
+ inspected && typeof inspected.nodeId === 'number'
958
+ ? await t.resolveTapTarget(inspected.nodeId)
959
+ : null
960
+ const node = (resolved && resolved.target) || inspected
961
+ const coordinateTarget = node
962
+ ? {
963
+ nodeId: node.nodeId ?? null,
964
+ id: node.id ?? null,
965
+ testID: node.testID ?? null,
966
+ text: node.text ?? node.accessibilityLabel ?? null,
967
+ type: node.type ?? null,
968
+ }
969
+ : null
970
+ const coordinateAncestors = Array.isArray(inspected?.ancestors)
971
+ ? inspected.ancestors
972
+ : []
973
+ const ancestorMatchesRequested = coordinateAncestors.some(
974
+ (ancestor) =>
975
+ (typeof requestedTarget.nodeId === 'number' &&
976
+ ancestor?.nodeId === requestedTarget.nodeId) ||
977
+ (requestedTarget.testID &&
978
+ ancestor?.testID === requestedTarget.testID) ||
979
+ (requestedTarget.id && ancestor?.id === requestedTarget.id),
980
+ )
981
+ const requestedTargetMatched =
982
+ coordinateTarget === null
983
+ ? false
984
+ : typeof requestedTarget.nodeId === 'number' &&
985
+ typeof coordinateTarget.nodeId === 'number'
986
+ ? requestedTarget.nodeId === coordinateTarget.nodeId ||
987
+ ancestorMatchesRequested
988
+ : requestedTarget.testID && coordinateTarget.testID
989
+ ? requestedTarget.testID === coordinateTarget.testID
990
+ || ancestorMatchesRequested
991
+ : requestedTarget.id && coordinateTarget.id
992
+ ? requestedTarget.id === coordinateTarget.id
993
+ || ancestorMatchesRequested
994
+ : ancestorMatchesRequested
995
+ if (!requestedTargetMatched) {
996
+ return {
997
+ requestedTargetMatched,
998
+ requestedTarget,
999
+ coordinateTarget,
1000
+ }
1001
+ }
1002
+ if (typeof t.activatePressAt !== 'function') {
1003
+ return { error: 'tap-target-activation-unavailable' }
1004
+ }
1005
+ const activation = await t.activatePressAt(
1006
+ ${payload.cx},
1007
+ ${payload.cy},
1008
+ requestedTarget.nodeId,
1009
+ )
1010
+ if (activation?.ok) {
1011
+ window.dispatchEvent(
1012
+ new CustomEvent('sootsim:agentAction', {
1013
+ detail: {
1014
+ type: 'tap',
1015
+ x: ${payload.cx},
1016
+ y: ${payload.cy},
1017
+ target: requestedTarget,
1018
+ },
1019
+ }),
1020
+ )
1021
+ }
1022
+ return {
1023
+ activation,
1024
+ requestedTargetMatched,
1025
+ requestedTarget,
1026
+ coordinateTarget,
1027
+ }
1028
+ })()`
1029
+ });
1030
+ if (verification?.error) {
1031
+ return {
1032
+ payload,
1033
+ result: {
1034
+ hit: false,
1035
+ reason: verification.error,
1036
+ requestedTarget
1037
+ },
1038
+ attempts,
1039
+ failure: "special"
1040
+ };
1041
+ }
1042
+ if (verification?.requestedTargetMatched === false) {
1043
+ return {
1044
+ payload,
1045
+ result: {
1046
+ hit: false,
1047
+ reason: "target-covered",
1048
+ ...verification
1049
+ },
1050
+ attempts,
1051
+ failure: "missed"
1052
+ };
1053
+ }
1054
+ const dispatched = verification?.activation?.tap ?? verification?.activation;
1055
+ const result = dispatched && typeof dispatched === "object" ? { ...dispatched, ...verification?.activation, ...verification } : { hit: !!dispatched, ...verification };
1056
+ lastResult = result;
1057
+ if (isTapSuccess(result)) return { payload, result, attempts };
1058
+ }
1059
+ }
1060
+ const remaining = deadline - Date.now();
1061
+ if (remaining <= 0) break;
1062
+ try {
1063
+ await waitForSootsimIdle({
1064
+ bridge,
1065
+ maxMs: Math.min(remaining, timing.retryWaitMs),
1066
+ pollMs: 32,
1067
+ stablePolls: 2
1068
+ });
1069
+ } catch {
1070
+ await new Promise((resolve2) => setTimeout(resolve2, Math.min(120, remaining)));
1071
+ }
1072
+ }
1073
+ return {
1074
+ payload: lastPayload,
1075
+ result: lastResult,
1076
+ attempts,
1077
+ failure: lastPayload && typeof lastPayload.cx === "number" ? "missed" : "not-found"
1078
+ };
1079
+ }
1080
+ async function tapById(bridge, query, opts = {}) {
1081
+ const arg = JSON.stringify(query);
1082
+ return tapResolvedTarget(bridge, {
1083
+ agent: opts.agent,
1084
+ timing: opts.timing,
1085
+ resolve: () => bridge.send({
1086
+ type: "evaluate",
1087
+ code: `(async () => {
1088
+ const t = window.__sootsimTest
1089
+ if (!t) return null
1090
+ const n = (await t.findByTestId(${arg})) || (await t.findById(${arg}))
1091
+ if (!n || !n.absolutePosition || !n.layout) return { cx: null }
1092
+ const resolved =
1093
+ typeof n.nodeId === 'number' && typeof t.resolveTapTarget === 'function'
1094
+ ? await t.resolveTapTarget(n.nodeId)
1095
+ : null
1096
+ const target = (resolved && resolved.target) || n
1097
+ const cx =
1098
+ resolved && typeof resolved.cx === 'number'
1099
+ ? resolved.cx
1100
+ : n.absolutePosition.x + (n.layout.width || 0) / 2
1101
+ const cy =
1102
+ resolved && typeof resolved.cy === 'number'
1103
+ ? resolved.cy
1104
+ : n.absolutePosition.y + (n.layout.height || 0) / 2
1105
+ const scr = ${SCREEN_EVAL}
1106
+ const offscreen =
1107
+ !!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
1108
+ return {
1109
+ cx,
1110
+ cy,
1111
+ ...(offscreen ? { offscreen: true, screen: scr } : {}),
1112
+ match: {
1113
+ nodeId: n.nodeId ?? null,
1114
+ id: n.id,
1115
+ testID: n.testID,
1116
+ text: n.text ?? n.accessibilityLabel ?? null,
1117
+ type: n.type,
1118
+ },
1119
+ target: {
1120
+ nodeId: target.nodeId ?? null,
1121
+ id: target.id,
1122
+ testID: target.testID,
1123
+ text:
1124
+ target.text ??
1125
+ target.accessibilityLabel ??
1126
+ n.text ??
1127
+ n.accessibilityLabel ??
1128
+ null,
1129
+ type: target.type,
1130
+ },
1131
+ strategy: (resolved && resolved.strategy) || 'matched-node',
1132
+ }
1133
+ })()`
1134
+ })
1135
+ });
1136
+ }
1137
+ async function tapByText(bridge, query, options = {}, opts = {}) {
1138
+ const filterArg = JSON.stringify({
1139
+ query,
1140
+ exact: !!options.exact,
1141
+ role: options.role ?? null,
1142
+ within: options.within ?? null,
1143
+ minX: options.minX ?? null,
1144
+ maxX: options.maxX ?? null,
1145
+ minY: options.minY ?? null,
1146
+ maxY: options.maxY ?? null,
1147
+ near: options.near ?? null,
1148
+ nth: options.nth ?? null,
1149
+ first: !!options.first
1150
+ });
1151
+ return tapResolvedTarget(bridge, {
1152
+ agent: opts.agent,
1153
+ timing: opts.timing,
1154
+ textFallback: query,
1155
+ resolve: () => bridge.send({
1156
+ type: "evaluate",
1157
+ code: `(async () => {
1158
+ const t = window.__sootsimTest
1159
+ if (!t) return { error: 'bridge-not-ready' }
1160
+ const F = ${filterArg}
1161
+
1162
+ const res = await t.queryTextCandidates({
1163
+ query: F.query,
1164
+ exact: !!F.exact,
1165
+ ...(F.role ? { role: F.role } : {}),
1166
+ })
1167
+
1168
+ let candidates = res.candidates || []
1169
+
1170
+ candidates = candidates.filter((c) => {
1171
+ const ax = c.info.absolutePosition && c.info.absolutePosition.x
1172
+ const ay = c.info.absolutePosition && c.info.absolutePosition.y
1173
+ if (F.minX !== null && !(ax >= F.minX)) return false
1174
+ if (F.maxX !== null && !(ax <= F.maxX)) return false
1175
+ if (F.minY !== null && !(ay >= F.minY)) return false
1176
+ if (F.maxY !== null && !(ay <= F.maxY)) return false
1177
+ if (F.within && !c.ancestorTestIDs.includes(F.within)) return false
1178
+ return true
1179
+ })
1180
+
1181
+ if (F.near) {
1182
+ candidates.sort((a, b) => {
1183
+ const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
1184
+ const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
1185
+ const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
1186
+ const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
1187
+ return (
1188
+ Math.hypot(ax - F.near.x, ay - F.near.y) -
1189
+ Math.hypot(bx - F.near.x, by - F.near.y)
1190
+ )
1191
+ })
1192
+ } else {
1193
+ candidates.sort((a, b) => {
1194
+ const ay = (a.info.absolutePosition && a.info.absolutePosition.y) || 0
1195
+ const by = (b.info.absolutePosition && b.info.absolutePosition.y) || 0
1196
+ if (Math.abs(ay - by) > 2) return ay - by
1197
+ const ax = (a.info.absolutePosition && a.info.absolutePosition.x) || 0
1198
+ const bx = (b.info.absolutePosition && b.info.absolutePosition.x) || 0
1199
+ return ax - bx
1200
+ })
1201
+ }
1202
+
1203
+ const total = candidates.length
1204
+ if (total === 0) return { matched: 0, total: 0 }
1205
+
1206
+ let idx = 0
1207
+ if (F.nth !== null) {
1208
+ idx = F.nth < 0 ? total + F.nth : F.nth
1209
+ if (idx < 0 || idx >= total) {
1210
+ return { matched: 0, total, nthOutOfRange: true, nth: F.nth }
1211
+ }
1212
+ } else if (total > 1 && !F.first && !F.near) {
1213
+ return {
1214
+ ambiguous: true,
1215
+ total,
1216
+ candidates: candidates.slice(0, 10).map((c, i) => ({
1217
+ idx: i,
1218
+ nodeId: c.info.nodeId,
1219
+ type: c.info.type,
1220
+ testID: c.info.testID,
1221
+ text: (c.info.text || '').slice(0, 60),
1222
+ abs: c.info.absolutePosition,
1223
+ layout: c.info.layout
1224
+ ? {
1225
+ width: Math.round(c.info.layout.width || 0),
1226
+ height: Math.round(c.info.layout.height || 0),
1227
+ }
1228
+ : null,
1229
+ ancestorTestIDs: (c.ancestorTestIDs || []).slice(0, 5),
1230
+ })),
1231
+ }
1232
+ }
1233
+
1234
+ const picked = candidates[idx]
1235
+ const n = picked.info
1236
+ if (!n.absolutePosition || !n.layout) return { matched: 0, total }
1237
+
1238
+ const resolved =
1239
+ typeof n.nodeId === 'number' &&
1240
+ typeof t.resolveTapTarget === 'function'
1241
+ ? await t.resolveTapTarget(n.nodeId)
1242
+ : null
1243
+ const target = (resolved && resolved.target) || n
1244
+ const cx =
1245
+ resolved && typeof resolved.cx === 'number'
1246
+ ? resolved.cx
1247
+ : n.absolutePosition.x + (n.layout.width || 0) / 2
1248
+ const cy =
1249
+ resolved && typeof resolved.cy === 'number'
1250
+ ? resolved.cy
1251
+ : n.absolutePosition.y + (n.layout.height || 0) / 2
1252
+ const scr = ${SCREEN_EVAL}
1253
+ const offscreen =
1254
+ !!scr && (cx < 0 || cy < 0 || cx > scr.width || cy > scr.height)
1255
+ return {
1256
+ cx,
1257
+ cy,
1258
+ ...(offscreen ? { offscreen: true, screen: scr } : {}),
1259
+ match: {
1260
+ nodeId: n.nodeId ?? null,
1261
+ id: n.id,
1262
+ testID: n.testID,
1263
+ type: n.type,
1264
+ },
1265
+ target: {
1266
+ nodeId: target.nodeId ?? null,
1267
+ id: target.id,
1268
+ testID: target.testID,
1269
+ text:
1270
+ target.text ??
1271
+ target.accessibilityLabel ??
1272
+ n.text ??
1273
+ n.accessibilityLabel ??
1274
+ null,
1275
+ type: target.type,
1276
+ },
1277
+ strategy: (resolved && resolved.strategy) || 'matched-node',
1278
+ total,
1279
+ idx,
1280
+ }
1281
+ })()`
1282
+ })
1283
+ });
1284
+ }
1285
+ async function tapBest(bridge, query, opts = {}) {
1286
+ const arg = JSON.stringify(query);
1287
+ return tapResolvedTarget(bridge, {
1288
+ agent: opts.agent,
1289
+ timing: opts.timing,
1290
+ textFallback: query,
1291
+ resolve: async () => {
1292
+ const payload = await bridge.send({
1293
+ type: "evaluate",
1294
+ code: `(async () => {
1295
+ const t = window.__sootsimTest
1296
+ if (!t) return { error: 'bridge-not-ready' }
1297
+ const byTestId =
1298
+ (await t.findByTestId(${arg})) || (await t.findById(${arg}))
1299
+ if (byTestId && byTestId.absolutePosition && byTestId.layout) {
1300
+ return {
1301
+ strategy: 'testid',
1302
+ node: {
1303
+ nodeId: byTestId.nodeId ?? null,
1304
+ id: byTestId.id,
1305
+ testID: byTestId.testID,
1306
+ type: byTestId.type,
1307
+ text: byTestId.text,
1308
+ absolutePosition: byTestId.absolutePosition,
1309
+ layout: byTestId.layout,
1310
+ },
1311
+ screen: ${SCREEN_EVAL},
1312
+ }
1313
+ }
1314
+ const byText = await t.findByText(${arg})
1315
+ if (byText && byText.absolutePosition && byText.layout) {
1316
+ return {
1317
+ strategy: 'text',
1318
+ node: {
1319
+ nodeId: byText.nodeId ?? null,
1320
+ id: byText.id,
1321
+ testID: byText.testID,
1322
+ type: byText.type,
1323
+ text: byText.text,
1324
+ absolutePosition: byText.absolutePosition,
1325
+ layout: byText.layout,
1326
+ },
1327
+ screen: ${SCREEN_EVAL},
1328
+ }
1329
+ }
1330
+ return { strategy: 'none' }
1331
+ })()`
1332
+ });
1333
+ if (!payload || !("node" in payload)) return payload;
1334
+ const node = payload.node;
1335
+ const cx = node.absolutePosition.x + node.layout.width / 2;
1336
+ const cy = node.absolutePosition.y + node.layout.height / 2;
1337
+ const screen = payload.screen ?? null;
1338
+ const offscreen = !!screen && (cx < 0 || cy < 0 || cx > screen.width || cy > screen.height);
1339
+ return {
1340
+ ...payload,
1341
+ cx,
1342
+ cy,
1343
+ ...offscreen ? { offscreen: true, screen } : {},
1344
+ target: {
1345
+ id: node.id,
1346
+ testID: node.testID,
1347
+ text: payload.strategy === "text" ? query : node.text,
1348
+ type: node.type
1349
+ }
1350
+ };
1351
+ }
1352
+ });
1353
+ }
1354
+
1355
+ // cli/commands/inspect/core.ts
1356
+ async function inspectTree(bridge, depth = 5, opts) {
1357
+ const optsArg = opts?.format ? `, ${JSON.stringify(opts)}` : "";
1358
+ const tree = await bridge.send({
1359
+ type: "evaluate",
1360
+ code: `(async () => await window.__sootsimTest.dumpTree(${depth}${optsArg}))()`
1361
+ });
1362
+ if (opts?.format === "boxes" && typeof tree === "string") {
1363
+ try {
1364
+ return { depth, tree: JSON.parse(tree) };
1365
+ } catch {
1366
+ return { depth, tree };
1367
+ }
1368
+ }
1369
+ return { depth, tree };
1370
+ }
1371
+ async function inspectUrl(bridge) {
1372
+ const url = await bridge.send({ type: "evaluate", code: "window.location.href" });
1373
+ return { url: typeof url === "string" ? url : "" };
1374
+ }
1375
+ async function inspectDescribe(bridge, dumpOpts) {
1376
+ const dumpDepth = dumpOpts.maxDepth ?? 12;
1377
+ const code = `(async () => {
1378
+ const t = window.__sootsimTest
1379
+ const mainShell = window.SootSim?.bridges?.mainShell
1380
+ const kb = window.__sootsimKeyboard
1381
+ if (!t) return { error: 'no test bridge' }
1382
+
1383
+ let shell = null
1384
+ try {
1385
+ shell = typeof mainShell?.getState === 'function' ? await mainShell.getState() : null
1386
+ } catch {}
1387
+
1388
+ const opts = ${JSON.stringify(dumpOpts)}
1389
+ const needsRecursiveDump =
1390
+ opts.verbose || opts.subtreeRoot || opts.includeOccluded || opts.maxDepth != null
1391
+ const globToRegex = (glob) => {
1392
+ const escaped = String(glob).replace(/[.+^$(){}|[\\]\\\\]/g, '\\\\$&')
1393
+ const body = escaped.replace(/\\*/g, '.*').replace(/\\?/g, '.')
1394
+ return new RegExp('^' + body + '$', 'i')
1395
+ }
1396
+ // clipping a describe line is a DISPLAY decision. opts.fullText (set by
1397
+ // \`describe --json\`) keeps the real string, so a bundle-eval stack or a
1398
+ // red-box message reaches the caller whole instead of ending in an
1399
+ // ellipsis nothing downstream can undo.
1400
+ const clipForDisplay = (value, max) => {
1401
+ const text = typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''
1402
+ if (opts.fullText || text.length <= max) return text
1403
+ return text.slice(0, max - 1).trimEnd() + '\u2026'
1404
+ }
1405
+ const formatInspectableTree = async () => {
1406
+ if (typeof t.listInspectable !== 'function') return null
1407
+ const list = await t.listInspectable({})
1408
+ if (!Array.isArray(list)) return null
1409
+ const filter = typeof opts.filter === 'string' ? opts.filter.toLowerCase() : ''
1410
+ const testIdRegex = opts.testIdLike ? globToRegex(opts.testIdLike) : null
1411
+ const onlyRegex = opts.onlyGlob ? globToRegex(opts.onlyGlob) : null
1412
+ const lines = []
1413
+ let matched = 0
1414
+ for (const node of list) {
1415
+ const testID = node?.testID || node?.testId || ''
1416
+ const role = node?.role || ''
1417
+ const label = node?.accessibilityLabel || node?.label || ''
1418
+ const text = node?.text || ''
1419
+ if (testIdRegex && !testIdRegex.test(testID)) continue
1420
+ const joined = [role, label, text, testID].filter(Boolean).join(' ')
1421
+ if (onlyRegex && !onlyRegex.test(joined)) continue
1422
+ if (filter && !joined.toLowerCase().includes(filter)) continue
1423
+ matched++
1424
+
1425
+ const type = String(node?.type || 'node').toLowerCase()
1426
+ const handlers = Array.isArray(node?.handlers) ? node.handlers : []
1427
+ const parts = []
1428
+ if (role) parts.push('[' + role + ']')
1429
+ else if (handlers.length > 0) parts.push('[tap]')
1430
+ else parts.push('<' + type + '>')
1431
+ const visibleText = clipForDisplay(text, 50)
1432
+ if (visibleText) parts.push(JSON.stringify(visibleText))
1433
+ if (testID) parts.push('#' + testID)
1434
+ if (label && label !== text) {
1435
+ parts.push('label=' + JSON.stringify(clipForDisplay(label, 50)))
1436
+ }
1437
+ if (!opts.hideXy) {
1438
+ const abs = node?.absolute || {}
1439
+ const layout = node?.layout || {}
1440
+ const x = Math.round(Number(abs.x) || 0)
1441
+ const y = Math.round(Number(abs.y) || 0)
1442
+ const w = Math.round(Number(layout.width) || 0)
1443
+ const h = Math.round(Number(layout.height) || 0)
1444
+ parts.push('@(' + x + ',' + y + ') ' + w + 'x' + h)
1445
+ }
1446
+ if (handlers.length > 0) parts.push('(tap)')
1447
+ lines.push(' ' + parts.join(' '))
1448
+ }
1449
+ return { tree: lines.join('\\n'), matched }
1450
+ }
1451
+
1452
+ let tree = ''
1453
+ const compactResult = needsRecursiveDump ? null : await formatInspectableTree()
1454
+ if (compactResult) {
1455
+ tree = compactResult.tree
1456
+ } else {
1457
+ tree = await t.dumpTree(${dumpDepth}, opts)
1458
+ }
1459
+ const nodeCount = (await t.getNodeCount?.()) || 0
1460
+ const keyboard = kb && typeof kb.getLayout === 'function' ? kb.getLayout() : null
1461
+ return { tree, shell, nodeCount, keyboard }
1462
+ })()`;
1463
+ const result = await bridge.send({ type: "evaluate", code });
1464
+ return result ?? {};
1465
+ }
1466
+ function scoreLayoutTextContrast(input) {
1467
+ const required = input.fontSize >= 18 || input.fontSize >= 14 && (input.fontWeight ?? 400) >= 700 ? 3 : 4.5;
1468
+ const { normal, background } = input;
1469
+ if (normal.width <= 0 || normal.height <= 0 || normal.width !== background.width || normal.height !== background.height) {
1470
+ return { ratio: null, required, reason: "capture dimensions do not match" };
1471
+ }
1472
+ const left = Math.max(0, Math.floor(input.box.x));
1473
+ const top = Math.max(0, Math.floor(input.box.y));
1474
+ const right = Math.min(normal.width, Math.ceil(input.box.x + input.box.w));
1475
+ const bottom = Math.min(normal.height, Math.ceil(input.box.y + input.box.h));
1476
+ if (right <= left || bottom <= top) {
1477
+ return { ratio: null, required, reason: "text is outside the captured frame" };
1478
+ }
1479
+ const linear = (value) => {
1480
+ const channel = value / 255;
1481
+ return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
1482
+ };
1483
+ const ratioOf = (foreground, behind) => {
1484
+ const foregroundLuminance = 0.2126 * linear(foreground[0]) + 0.7152 * linear(foreground[1]) + 0.0722 * linear(foreground[2]);
1485
+ const backgroundLuminance = 0.2126 * linear(behind[0]) + 0.7152 * linear(behind[1]) + 0.0722 * linear(behind[2]);
1486
+ return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05) / (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
1487
+ };
1488
+ const hex = (color) => `#${color.slice(0, 3).map((value) => Math.round(value).toString(16).padStart(2, "0")).join("")}`;
1489
+ const area = (right - left) * (bottom - top);
1490
+ const step = Math.max(1, Math.floor(Math.sqrt(area / 12e3)));
1491
+ const offsetAt = (x, y) => (y * normal.width + x) * 4;
1492
+ const deltaAt = (offset) => Math.max(
1493
+ Math.abs(normal.data[offset] - background.data[offset]),
1494
+ Math.abs(normal.data[offset + 1] - background.data[offset + 1]),
1495
+ Math.abs(normal.data[offset + 2] - background.data[offset + 2])
1496
+ );
1497
+ let strongestDelta = 0;
1498
+ let strongestForeground = null;
1499
+ let opaqueBackground = null;
1500
+ for (let y = top; y < bottom; y += step) {
1501
+ for (let x = left; x < right; x += step) {
1502
+ const offset = offsetAt(x, y);
1503
+ if (background.data[offset + 3] < 250) continue;
1504
+ const behind = [
1505
+ background.data[offset],
1506
+ background.data[offset + 1],
1507
+ background.data[offset + 2]
1508
+ ];
1509
+ opaqueBackground ??= behind;
1510
+ const delta = deltaAt(offset);
1511
+ if (delta <= strongestDelta) continue;
1512
+ strongestDelta = delta;
1513
+ strongestForeground = [
1514
+ normal.data[offset],
1515
+ normal.data[offset + 1],
1516
+ normal.data[offset + 2]
1517
+ ];
1518
+ }
1519
+ }
1520
+ let bestRatio = Infinity;
1521
+ let bestForeground = null;
1522
+ let bestBackground = null;
1523
+ const consider = (foreground, behind) => {
1524
+ const ratio = ratioOf(foreground, behind);
1525
+ if (ratio >= bestRatio) return;
1526
+ bestRatio = ratio;
1527
+ bestForeground = foreground;
1528
+ bestBackground = behind;
1529
+ };
1530
+ if (strongestDelta >= 2 && strongestForeground) {
1531
+ for (let y = top; y < bottom; y += step) {
1532
+ for (let x = left; x < right; x += step) {
1533
+ const offset = offsetAt(x, y);
1534
+ if (background.data[offset + 3] < 250 || deltaAt(offset) < 2) {
1535
+ continue;
1536
+ }
1537
+ consider(strongestForeground, [
1538
+ background.data[offset],
1539
+ background.data[offset + 1],
1540
+ background.data[offset + 2]
1541
+ ]);
1542
+ }
1543
+ }
1544
+ } else if (opaqueBackground) {
1545
+ consider(opaqueBackground, opaqueBackground);
1546
+ }
1547
+ if (!bestForeground || !bestBackground || !Number.isFinite(bestRatio)) {
1548
+ return {
1549
+ ratio: null,
1550
+ required,
1551
+ reason: "could not isolate opaque rendered text pixels"
1552
+ };
1553
+ }
1554
+ return {
1555
+ ratio: Math.round(bestRatio * 100) / 100,
1556
+ required,
1557
+ foreground: hex(bestForeground),
1558
+ background: hex(bestBackground)
1559
+ };
1560
+ }
1561
+ function layoutGetNativeEval(opts = {}) {
1562
+ const includeStyle = opts.styling === true;
1563
+ const filter = opts.filter?.trim().toLowerCase() ?? "";
1564
+ return `(async () => {
1565
+ const t = window.__sootsimTest
1566
+ if (!t || typeof t.listInspectable !== 'function') return []
1567
+ const list = await t.listInspectable({})
1568
+ if (!Array.isArray(list)) return []
1569
+ const includeStyle = ${JSON.stringify(includeStyle)}
1570
+ const scoreContrast = ${scoreLayoutTextContrast.toString()}
1571
+ // narrows by ELEMENT, not by style field: fewer rows, full detail on each.
1572
+ const filter = ${JSON.stringify(filter)}
1573
+ const matchesFilter = (n, text) => {
1574
+ if (!filter) return true
1575
+ const fields = [text, n.testID || n.testId, n.accessibilityRole || n.role, n.type]
1576
+ return fields.some((f) => typeof f === 'string' && f.toLowerCase().includes(filter))
1577
+ }
1578
+
1579
+ const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0)
1580
+ const round = (v) => Math.round(num(v))
1581
+
1582
+ // borderRadius can be authored as a string ("12px") or split per-corner;
1583
+ // prefer the uniform borderRadius, else the largest corner radius present.
1584
+ const radiusOf = (style) => {
1585
+ if (!style) return 0
1586
+ const keys = ['borderRadius','borderTopLeftRadius','borderTopRightRadius','borderBottomLeftRadius','borderBottomRightRadius']
1587
+ let max = 0
1588
+ for (const k of keys) {
1589
+ const raw = style[k]
1590
+ const n = typeof raw === 'number' ? raw : (typeof raw === 'string' ? parseFloat(raw) : NaN)
1591
+ if (Number.isFinite(n)) max = Math.max(max, n)
1592
+ }
1593
+ return Math.round(max)
1594
+ }
1595
+ const fontSizeOf = (style) => {
1596
+ if (!style) return 0
1597
+ const raw = style.fontSize
1598
+ const n = typeof raw === 'number' ? raw : (typeof raw === 'string' ? parseFloat(raw) : NaN)
1599
+ return Number.isFinite(n) ? Math.round(n) : 0
1600
+ }
1601
+ // styling extractors \u2014 omit boring defaults so the agent only sees notable
1602
+ // styling (low opacity, struck text, clipping, a real border/color).
1603
+ const colorStr = (v) => {
1604
+ if (v == null) return undefined
1605
+ const s = String(v).split(' ').join('')
1606
+ return s && s !== 'transparent' && s !== 'rgba(0,0,0,0)' ? s : undefined
1607
+ }
1608
+ const opacityOf = (style) => {
1609
+ const o = style && style.opacity
1610
+ const n = typeof o === 'number' ? o : (typeof o === 'string' ? parseFloat(o) : NaN)
1611
+ return Number.isFinite(n) && n < 1 ? Math.round(n * 100) / 100 : undefined
1612
+ }
1613
+ const weightOf = (style) => {
1614
+ const w = style && style.fontWeight
1615
+ const n = w === 'bold' ? 700 : (typeof w === 'number' ? w : (typeof w === 'string' ? parseInt(w, 10) : NaN))
1616
+ return Number.isFinite(n) && n >= 600 ? n : undefined
1617
+ }
1618
+ const decoOf = (style) => {
1619
+ const d = style && (style.textDecorationLine || style.textDecoration)
1620
+ return d && d !== 'none' && d !== 'normal' ? String(d).split(' ')[0] : undefined
1621
+ }
1622
+ const overflowOf = (style) => (style && style.overflow === 'hidden' ? 'hidden' : undefined)
1623
+ const borderWOf = (style) => {
1624
+ if (!style) return undefined
1625
+ const keys = ['borderWidth','borderTopWidth','borderBottomWidth','borderLeftWidth','borderRightWidth']
1626
+ let max = 0
1627
+ for (const k of keys) {
1628
+ const r = style[k]
1629
+ const n = typeof r === 'number' ? r : (typeof r === 'string' ? parseFloat(r) : NaN)
1630
+ if (Number.isFinite(n)) max = Math.max(max, n)
1631
+ }
1632
+ return max > 0 ? Math.round(max) : undefined
1633
+ }
1634
+ // only meaningful alongside a real border width, so it is resolved by the
1635
+ // caller below rather than on its own.
1636
+ const borderColorOf = (style) => {
1637
+ if (!style) return undefined
1638
+ const keys = ['borderColor','borderTopColor','borderBottomColor','borderLeftColor','borderRightColor']
1639
+ for (const k of keys) {
1640
+ const c = colorStr(style[k])
1641
+ if (c) return c
1642
+ }
1643
+ return undefined
1644
+ }
1645
+
1646
+ const isVisible = (n) => {
1647
+ const layout = n && n.layout
1648
+ if (!layout || layout.width <= 0 || layout.height <= 0) return false
1649
+ const abs = (n && (n.absolute || n.absolutePosition)) || null
1650
+ if (!abs) return true
1651
+ const device = (n && n.device) || {}
1652
+ const screenW = Number(device.width) || window.innerWidth || 0
1653
+ const screenH = Number(device.height) || window.innerHeight || 0
1654
+ if (!screenW || !screenH) return true
1655
+ return abs.x + layout.width > 0 && abs.y + layout.height > 0 && abs.x < screenW && abs.y < screenH
1656
+ }
1657
+
1658
+ const elements = list.filter(isVisible).filter((n) => {
1659
+ const t = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
1660
+ return matchesFilter(n, t)
1661
+ }).map((n) => {
1662
+ const layout = n.layout || { x: 0, y: 0, width: 0, height: 0 }
1663
+ const abs = n.absolute || n.absolutePosition || { x: layout.x, y: layout.y }
1664
+ const text = typeof n.text === 'string' && n.text.trim() ? n.text.trim() : undefined
1665
+ const base = {
1666
+ ...(typeof n.nodeId === 'number'
1667
+ ? {
1668
+ nodeId: n.nodeId,
1669
+ ancestorIds: Array.isArray(n.ancestors)
1670
+ ? n.ancestors.map((ancestor) => ancestor && ancestor.nodeId).filter((id) => typeof id === 'number')
1671
+ : [],
1672
+ }
1673
+ : {}),
1674
+ testID: n.testID || n.testId || undefined,
1675
+ role: n.accessibilityRole || n.role || undefined,
1676
+ type: typeof n.type === 'string' ? n.type : 'node',
1677
+ text,
1678
+ box: { x: round(abs.x), y: round(abs.y), w: round(layout.width), h: round(layout.height) },
1679
+ borderRadius: 0,
1680
+ padding: { t: 0, r: 0, b: 0, l: 0 },
1681
+ fontSize: 0,
1682
+ }
1683
+ if (!includeStyle) return base
1684
+ // computedStyle has the resolved values; style is the curated subset \u2014
1685
+ // fall back across both so radius/fontSize survive either shape.
1686
+ const style = Object.assign({}, n.style || {}, n.computedStyle || {})
1687
+ const pad = (n.boxModel && n.boxModel.padding) || {}
1688
+ const borderWidth = borderWOf(style)
1689
+ return {
1690
+ ...base,
1691
+ borderRadius: radiusOf(style),
1692
+ padding: { t: round(pad.top), r: round(pad.right), b: round(pad.bottom), l: round(pad.left) },
1693
+ fontSize: text ? (fontSizeOf(style) || 14) : 0,
1694
+ color: text ? colorStr(style.color) : undefined,
1695
+ bg: colorStr(style.backgroundColor),
1696
+ opacity: opacityOf(style),
1697
+ fontWeight: weightOf(style),
1698
+ textDecoration: decoOf(style),
1699
+ overflow: overflowOf(style),
1700
+ borderWidth,
1701
+ borderColor: borderWidth ? borderColorOf(style) : undefined,
1702
+ }
1703
+ })
1704
+ const textElements = elements.filter((element) => element.text && element.fontSize > 0)
1705
+ if (!includeStyle || textElements.length === 0) return elements
1706
+
1707
+ const screenshot = window.SootSim && window.SootSim.bridges && window.SootSim.bridges.screenshot
1708
+ if (typeof screenshot !== 'function') {
1709
+ throw new Error('styled native layout requires the rnx capture bridge')
1710
+ }
1711
+ const device = list.find((node) => node && node.device)?.device || {}
1712
+ const width = Math.round(Number(device.width) || window.innerWidth || 0)
1713
+ const height = Math.round(Number(device.height) || window.innerHeight || 0)
1714
+ if (!width || !height) throw new Error('native layout has no capture dimensions')
1715
+ const capture = async (suppressText) => {
1716
+ const dataUrl = await screenshot({
1717
+ format: 'png',
1718
+ outputWidth: width,
1719
+ outputHeight: height,
1720
+ ...(suppressText ? { suppressText: true } : {}),
1721
+ })
1722
+ const image = await new Promise((resolve, reject) => {
1723
+ const next = new Image()
1724
+ next.onload = () => resolve(next)
1725
+ next.onerror = () => reject(new Error('could not decode native layout pixels'))
1726
+ next.src = dataUrl
1727
+ })
1728
+ const canvas = document.createElement('canvas')
1729
+ canvas.width = image.naturalWidth
1730
+ canvas.height = image.naturalHeight
1731
+ const context = canvas.getContext('2d', { willReadFrequently: true })
1732
+ if (!context) throw new Error('could not read native layout pixels')
1733
+ context.drawImage(image, 0, 0)
1734
+ return context.getImageData(0, 0, canvas.width, canvas.height)
1735
+ }
1736
+ const normal = await capture(false)
1737
+ const background = await capture(true)
1738
+ for (const element of textElements) {
1739
+ element.contrast = scoreContrast({
1740
+ normal,
1741
+ background,
1742
+ box: element.box,
1743
+ fontSize: element.fontSize,
1744
+ fontWeight: element.fontWeight,
1745
+ })
1746
+ }
1747
+ return elements
1748
+ })()`;
1749
+ }
1750
+ var LAYOUT_GET_NATIVE_EVAL = layoutGetNativeEval();
1751
+ var INSPECT_A11Y_CODE = `(async () => {
1752
+ const t = window.__sootsimTest
1753
+ if (!t) return []
1754
+
1755
+ const readLayout = (n) => n?.layout || null
1756
+ const readAbs = (n) => n?.absolutePosition || n?.absolute || null
1757
+ const readRole = (n) => n?.accessibilityRole || n?.role || null
1758
+ const readLabel = (n) => n?.accessibilityLabel || n?.label || n?.text || null
1759
+ const readHint = (n) => n?.accessibilityHint || n?.hint || null
1760
+ const readState = (n) => n?.accessibilityState || n?.state || null
1761
+ const readTestId = (n) => n?.testID || n?.testId || null
1762
+ const readType = (n) => typeof n?.type === 'string' ? n.type.toLowerCase() : n?.type
1763
+ const readPressable = (n) => {
1764
+ if (n?.pressable) return true
1765
+ const handlers = Array.isArray(n?.handlers) ? n.handlers : []
1766
+ const role = readRole(n)
1767
+ return handlers.length > 0 || role === 'button' || role === 'link' || role === 'tab'
1768
+ }
1769
+ const isTextInput = (n) => n?.isTextInput === true
1770
+ const isTextNode = (n) => readType(n) === 'text'
1771
+ const isVisibleTarget = (n) => {
1772
+ const layout = readLayout(n)
1773
+ if (!layout || layout.width <= 0 || layout.height <= 0) return false
1774
+ const abs = readAbs(n)
1775
+ if (!abs) return true
1776
+ const device = n?.device || {}
1777
+ const screenW = Number(device.width) || window.innerWidth || 0
1778
+ const screenH = Number(device.height) || window.innerHeight || 0
1779
+ return (
1780
+ abs.x + layout.width > 0 &&
1781
+ abs.y + layout.height > 0 &&
1782
+ abs.x < screenW &&
1783
+ abs.y < screenH
1784
+ )
1785
+ }
1786
+ const hasAccessibleSignal = (n) => {
1787
+ const role = readRole(n)
1788
+ const label = readLabel(n)
1789
+ const hint = readHint(n)
1790
+ if (role) return true
1791
+ if (hint) return true
1792
+ if (isTextInput(n)) return true
1793
+ if (readPressable(n)) return true
1794
+ if (isTextNode(n) && n?.text) return true
1795
+ if (typeof label === 'string' && label.length <= 30 && label !== n?.text) return true
1796
+ return false
1797
+ }
1798
+ const normalize = (n) => {
1799
+ const layout = readLayout(n)
1800
+ const abs = readAbs(n)
1801
+ const role = readRole(n) || (readPressable(n) ? 'button' : isTextInput(n) ? 'textfield' : isTextNode(n) ? 'statictext' : 'none')
1802
+ return {
1803
+ role,
1804
+ label: readLabel(n),
1805
+ hint: readHint(n),
1806
+ state: readState(n),
1807
+ testID: readTestId(n),
1808
+ position: abs ? { x: Math.round(abs.x), y: Math.round(abs.y) } : null,
1809
+ size: layout ? { w: Math.round(layout.width), h: Math.round(layout.height) } : null,
1810
+ }
1811
+ }
1812
+
1813
+ if (typeof t.listInspectable === 'function') {
1814
+ const list = await t.listInspectable({})
1815
+ if (Array.isArray(list)) {
1816
+ return list.filter(n => isVisibleTarget(n) && hasAccessibleSignal(n)).map(normalize)
1817
+ }
1818
+ }
1819
+
1820
+ const all = await t.queryAll({ pruneHidden: true })
1821
+ return all
1822
+ .filter(n => isVisibleTarget(n) && hasAccessibleSignal(n))
1823
+ .map(normalize)
1824
+ })()`;
1825
+ async function inspectAccessibilityTree(bridge) {
1826
+ const nodes = await bridge.send({ type: "evaluate", code: INSPECT_A11Y_CODE });
1827
+ return Array.isArray(nodes) ? nodes : [];
1828
+ }
1829
+ var FIND_INSPECTABLE_TARGETS = `
1830
+ const fromInspectable = async () => {
1831
+ if (typeof t.listInspectable !== 'function') return null
1832
+ const list = await t.listInspectable({})
1833
+ if (!Array.isArray(list)) return null
1834
+ return list.map((n) => {
1835
+ const role = n.accessibilityRole ?? n.role
1836
+ const label = n.accessibilityLabel ?? n.label
1837
+ const layout = n.layout
1838
+ const absolutePosition = n.absolutePosition ?? n.absolute
1839
+ const screenW = Number(n.device?.width) || window.innerWidth || 0
1840
+ const screenH = Number(n.device?.height) || window.innerHeight || 0
1841
+ const visibleFrame = layout && absolutePosition
1842
+ ? {
1843
+ x: Math.max(0, absolutePosition.x),
1844
+ y: Math.max(0, absolutePosition.y),
1845
+ width: Math.max(
1846
+ 0,
1847
+ Math.min(screenW, absolutePosition.x + layout.width) -
1848
+ Math.max(0, absolutePosition.x),
1849
+ ),
1850
+ height: Math.max(
1851
+ 0,
1852
+ Math.min(screenH, absolutePosition.y + layout.height) -
1853
+ Math.max(0, absolutePosition.y),
1854
+ ),
1855
+ }
1856
+ : null
1857
+ const handlers = Array.isArray(n.handlers) ? n.handlers : []
1858
+ const pressable =
1859
+ handlers.length > 0 ||
1860
+ role === 'button' ||
1861
+ role === 'link' ||
1862
+ role === 'tab'
1863
+ return {
1864
+ type: typeof n.type === 'string' ? n.type.toLowerCase() : n.type,
1865
+ id: n.id ?? n.nodeId,
1866
+ nodeId: n.nodeId,
1867
+ testID: n.testID ?? n.testId,
1868
+ text: n.text,
1869
+ layout,
1870
+ absolutePosition,
1871
+ visibleFrame,
1872
+ style: n.computedStyle ?? n.style ?? {},
1873
+ childCount: n.childCount ?? 0,
1874
+ pressed: n.pressed ?? false,
1875
+ pressable,
1876
+ isTextInput: n.isTextInput ?? false,
1877
+ accessible: n.accessible ?? true,
1878
+ accessibilityLabel: label,
1879
+ accessibilityRole: role,
1880
+ accessibilityHint: n.accessibilityHint,
1881
+ accessibilityState: n.accessibilityState,
1882
+ accessibilityValue: n.accessibilityValue,
1883
+ }
1884
+ })
1885
+ }
1886
+ const isVisibleTarget = (n) => {
1887
+ if (!n?.layout) return false
1888
+ const frame = n.visibleFrame
1889
+ if (frame) return frame.width > 0 && frame.height > 0
1890
+ const abs = n.absolutePosition
1891
+ if (!abs) return n.layout.width > 0 && n.layout.height > 0
1892
+ const screenW = window.innerWidth || 0
1893
+ const screenH = window.innerHeight || 0
1894
+ return (
1895
+ abs.x + n.layout.width > 0 &&
1896
+ abs.y + n.layout.height > 0 &&
1897
+ abs.x < screenW &&
1898
+ abs.y < screenH
1899
+ )
1900
+ }
1901
+ `;
1902
+ function resolveFindMode(q) {
1903
+ if (q.testId) {
1904
+ return {
1905
+ mode: "testid",
1906
+ code: `(async () => {
1907
+ const t = window.__sootsimTest
1908
+ if (!t) return null
1909
+ return (await t.findByTestId(${JSON.stringify(q.testId)})) || (await t.findById(${JSON.stringify(q.testId)}))
1910
+ })()`
1911
+ };
1912
+ }
1913
+ if (q.role) {
1914
+ return {
1915
+ mode: "role",
1916
+ code: `(async () => {
1917
+ const t = window.__sootsimTest
1918
+ if (!t) return []
1919
+ return await t.queryAll({ hasRole: ${JSON.stringify(q.role)}, pruneHidden: true })
1920
+ })()`
1921
+ };
1922
+ }
1923
+ if (q.type) {
1924
+ return {
1925
+ mode: "type",
1926
+ code: `(async () => {
1927
+ const t = window.__sootsimTest
1928
+ if (!t) return []
1929
+ return await t.queryAll({ type: ${JSON.stringify(q.type)}, pruneHidden: true })
1930
+ })()`
1931
+ };
1932
+ }
1933
+ if (q.pressable) {
1934
+ return {
1935
+ mode: "pressable",
1936
+ code: `(async () => {
1937
+ const t = window.__sootsimTest
1938
+ if (!t) return []
1939
+ ${FIND_INSPECTABLE_TARGETS}
1940
+ const inspectable = await fromInspectable()
1941
+ if (inspectable) return inspectable.filter(n => n.pressable && isVisibleTarget(n))
1942
+ const all = await t.queryAll({ pruneHidden: true })
1943
+ return all.filter(n => n.pressable && isVisibleTarget(n))
1944
+ })()`
1945
+ };
1946
+ }
1947
+ if (q.interactive) {
1948
+ return {
1949
+ mode: "interactive-targets",
1950
+ code: `(async () => {
1951
+ const t = window.__sootsimTest
1952
+ if (!t) return []
1953
+ ${FIND_INSPECTABLE_TARGETS}
1954
+ const inspectable = await fromInspectable()
1955
+ if (inspectable) {
1956
+ return inspectable.filter(n => n.pressable && isVisibleTarget(n))
1957
+ }
1958
+ const all = await t.queryAll({ pruneHidden: true })
1959
+ return all.filter(n => n.pressable && isVisibleTarget(n))
1960
+ })()`
1961
+ };
1962
+ }
1963
+ if (q.visible) {
1964
+ return {
1965
+ mode: "visible",
1966
+ code: `(async () => {
1967
+ const t = window.__sootsimTest
1968
+ if (!t) return []
1969
+ const all = await t.queryAll({ pruneHidden: true })
1970
+ return all.filter(n => n.layout && n.layout.width > 0 && n.layout.height > 0)
1971
+ })()`
1972
+ };
1973
+ }
1974
+ if (q.text) {
1975
+ return {
1976
+ mode: "text",
1977
+ code: `(async () => {
1978
+ const t = window.__sootsimTest
1979
+ if (!t) return null
1980
+ return await t.findByText(${JSON.stringify(q.text)})
1981
+ })()`
1982
+ };
1983
+ }
1984
+ return null;
1985
+ }
1986
+ async function inspectFind(bridge, q) {
1987
+ const resolved = resolveFindMode(q);
1988
+ if (!resolved) return null;
1989
+ const result = await bridge.send({ type: "evaluate", code: resolved.code });
1990
+ return { mode: resolved.mode, result };
1991
+ }
1992
+ var WAIT_READY_PROBE = `(async () => {
1993
+ const t = window.__sootsimTest
1994
+ let nodes = 0
1995
+ try { nodes = (await t?.getNodeCount?.()) || 0 } catch {}
1996
+ let targets = 0
1997
+ let liveFrameActive = false
1998
+ let liveFrameChannels = 0
1999
+ let liveFramePublishes = 0
2000
+ let loadingText = ''
2001
+ const cleanText = (value) =>
2002
+ typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''
2003
+ const loadingSignal = (value) => {
2004
+ const text = cleanText(value)
2005
+ if (!text) return ''
2006
+ return /^(opening app\\b|looking for your app\\b|building bundle\\b|preparing rnxsim runtime\\b|loading native bundle\\b|loading rnx stubs\\b|loading bundle loader\\b|loading native stubs\\b|loading compat bridge\\b|loading react-native shim\\b|loading react bridge\\b|loading reconciler\\b|loading replacement modules\\b|injecting rnx modules\\b|warming sqlite engine\\b|capturing root component\\b|reloading app\\b|loading(?:[.!\u2026]*)?$|connecting(?:[.!\u2026]*)?$)/i.test(text)
2007
+ ? text
2008
+ : ''
2009
+ }
2010
+ try {
2011
+ const loading = window.__sootsimConnectRNLoading
2012
+ if (loading && loading.active === true) {
2013
+ loadingText = [loading.title, loading.detail].map(cleanText).filter(Boolean).join(' ')
2014
+ }
2015
+ } catch {}
2016
+ try {
2017
+ if (typeof t?.listInspectable === 'function') {
2018
+ const list = await t.listInspectable({})
2019
+ if (Array.isArray(list)) {
2020
+ const screenW = window.innerWidth || 0
2021
+ const screenH = window.innerHeight || 0
2022
+ const isVisible = (n) => {
2023
+ const layout = n?.layout
2024
+ if (!layout || layout.width <= 0 || layout.height <= 0) return false
2025
+ const abs = n?.absolutePosition || n?.absolute
2026
+ if (!abs) return true
2027
+ const device = n?.device || {}
2028
+ const w = Number(device.width) || screenW
2029
+ const h = Number(device.height) || screenH
2030
+ return (
2031
+ abs.x + layout.width > 0 &&
2032
+ abs.y + layout.height > 0 &&
2033
+ abs.x < w &&
2034
+ abs.y < h
2035
+ )
2036
+ }
2037
+ const hasContentSignal = (n) => {
2038
+ const role = n?.accessibilityRole || n?.role
2039
+ const label = cleanText(n?.accessibilityLabel || n?.label)
2040
+ const handlers = Array.isArray(n?.handlers) ? n.handlers : []
2041
+ const text = cleanText(n?.text)
2042
+ const testID = cleanText(n?.testID || n?.testId)
2043
+ const loading = loadingSignal(text) || loadingSignal(label) || loadingSignal(testID)
2044
+ if (!loadingText && loading) {
2045
+ loadingText = loading
2046
+ }
2047
+ if (handlers.length > 0) return true
2048
+ if (role) return true
2049
+ if (n?.isTextInput) return true
2050
+ if (text) return true
2051
+ if (label) return true
2052
+ if (testID && !/^splash|loading/i.test(testID)) return true
2053
+ return false
2054
+ }
2055
+ targets = list.filter(n => isVisible(n) && hasContentSignal(n)).length
2056
+ }
2057
+ }
2058
+ } catch {}
2059
+ try {
2060
+ const stats = await t?.getImageLoaderStats?.()
2061
+ const presentation = stats?.liveFramePresentation
2062
+ liveFrameChannels = Number(presentation?.activeChannels) || 0
2063
+ liveFramePublishes = Number(presentation?.publishes) || 0
2064
+ const previous = window.__sootsimWaitReadyLiveFramePublishes
2065
+ liveFrameActive =
2066
+ liveFrameChannels > 0 &&
2067
+ typeof previous === 'number' &&
2068
+ liveFramePublishes > previous
2069
+ window.__sootsimWaitReadyLiveFramePublishes = liveFramePublishes
2070
+ } catch {}
2071
+ let externalReady = null
2072
+ let externalStatus = ''
2073
+ let externalError = ''
2074
+ let suppressedEntryError = ''
2075
+ try {
2076
+ const getExternalAppState = t?.getExternalAppState
2077
+ const external = typeof getExternalAppState === 'function'
2078
+ ? await Promise.race([
2079
+ getExternalAppState(),
2080
+ new Promise((resolve) => setTimeout(
2081
+ () => resolve({
2082
+ state: {
2083
+ ready: false,
2084
+ loading: true,
2085
+ status: 'waiting for tenant worker...',
2086
+ },
2087
+ }),
2088
+ 1000,
2089
+ )),
2090
+ ])
2091
+ : null
2092
+ const state = external?.state
2093
+ if (state && typeof state === 'object') {
2094
+ externalReady = state.ready === true
2095
+ if (typeof state.status === 'string') externalStatus = state.status
2096
+ if (typeof state.error === 'string') externalError = state.error
2097
+ }
2098
+ if (!externalError && Array.isArray(external?.entryErrors)) {
2099
+ // suppressed-and-continued entry errors (bundle-loader's patched __r
2100
+ // swallows entry throws and moves on) must NOT gate readiness \u2014 the
2101
+ // app frequently runs fine after one (e.g. metro's "Unknown named
2102
+ // module" probing noise). they surface separately as a diagnostic
2103
+ // for the timeout path.
2104
+ const isReal = (item) => item && typeof item.message === 'string' && item.message.trim()
2105
+ const fatal = external.entryErrors.find((item) => isReal(item) && item.suppressed !== true)
2106
+ if (fatal) externalError = fatal.message
2107
+ const suppressed = external.entryErrors.find((item) => isReal(item) && item.suppressed === true)
2108
+ if (suppressed) suppressedEntryError = suppressed.message
2109
+ }
2110
+ // once the app has errored its last status ("injecting sootsim
2111
+ // modules...") is stale \u2014 reporting it makes a dead app read as one that
2112
+ // is still booting.
2113
+ if (externalError) loadingText = ''
2114
+ else if (externalReady === false && externalStatus) loadingText = externalStatus
2115
+ } catch {}
2116
+ let errors = 0
2117
+ try { errors = window.__sootsimConsole?.count?.()?.errors ?? 0 } catch {}
2118
+ return {
2119
+ flag: (window).__sootsimExternalAppReady,
2120
+ at: (window).__sootsimExternalAppReadyAt || 0,
2121
+ nodes,
2122
+ targets,
2123
+ liveFrameActive,
2124
+ liveFrameChannels,
2125
+ liveFramePublishes,
2126
+ errors,
2127
+ loadingText,
2128
+ externalReady,
2129
+ externalStatus,
2130
+ externalError,
2131
+ suppressedEntryError,
2132
+ }
2133
+ })()`;
2134
+ var READY_CONTENT_NODE_FLOOR = 100;
2135
+ var READY_NODE_STABLE_MS = 750;
2136
+ var WAIT_READY_PROGRESS_INTERVAL_MS = 2e3;
2137
+ var READY_PROBE_POLL_MS = 150;
2138
+ function readyProbeHasContent(probe) {
2139
+ return probe.liveFrameActive || probe.targets > 0 || probe.nodes >= READY_CONTENT_NODE_FLOOR;
2140
+ }
2141
+ function readyProbeHasTargetContent(probe) {
2142
+ return probe.liveFrameActive || probe.targets > 0;
2143
+ }
2144
+ function waitReadyReason(status) {
2145
+ if (status.bridgeError) {
2146
+ return `sim unreachable: ${status.bridgeError} \u2014 the target tab is closed or no sim is connected (check \`rnxsim list\`, reopen with \`rnxsim open\`)`;
2147
+ }
2148
+ const base = status.externalError ? `guest app errored: ${status.externalError}` : status.loadingText ? `still showing "${status.loadingText}"` : status.externalReady === false ? "guest app is still loading" : status.flag !== true && status.targets > 0 ? "native content is rendered but the ready signal has not settled" : status.flag !== true ? "guest app has not emitted sootsim:externalAppReady" : status.targets <= 0 ? "ready flag emitted but no visible app content is inspectable yet" : "node tree is still changing";
2149
+ return !status.externalError && status.suppressedEntryError ? `${base} (suppressed entry error: ${status.suppressedEntryError})` : base;
2150
+ }
2151
+ async function inspectWaitReady(bridge, timeoutMs = 2e4, options = {}) {
2152
+ const start = Date.now();
2153
+ const deadline = start + timeoutMs;
2154
+ const progressIntervalMs = options.progressIntervalMs ?? WAIT_READY_PROGRESS_INTERVAL_MS;
2155
+ let nextProgressAt = start + progressIntervalMs;
2156
+ let lastNodes = -1;
2157
+ let nodeStableSince = start;
2158
+ let bridgeError = "";
2159
+ let last = {
2160
+ flag: void 0,
2161
+ at: 0,
2162
+ nodes: 0,
2163
+ targets: 0,
2164
+ liveFrameActive: false,
2165
+ liveFrameChannels: 0,
2166
+ liveFramePublishes: 0,
2167
+ errors: 0,
2168
+ loadingText: "",
2169
+ externalReady: null,
2170
+ externalStatus: "",
2171
+ externalError: "",
2172
+ suppressedEntryError: ""
2173
+ };
2174
+ while (Date.now() < deadline) {
2175
+ try {
2176
+ last = await bridge.send({ type: "evaluate", code: WAIT_READY_PROBE }) ?? last;
2177
+ bridgeError = "";
2178
+ } catch (error) {
2179
+ bridgeError = error instanceof Error ? error.message : String(error);
2180
+ if (bridgeError.startsWith("multiple sims are connected:") || bridgeError.startsWith("saved sim ")) {
2181
+ throw error;
2182
+ }
2183
+ }
2184
+ const now = Date.now();
2185
+ const status2 = {
2186
+ ready: false,
2187
+ elapsedMs: now - start,
2188
+ nodes: last.nodes,
2189
+ targets: last.targets,
2190
+ liveFrameActive: last.liveFrameActive,
2191
+ liveFrameChannels: last.liveFrameChannels,
2192
+ liveFramePublishes: last.liveFramePublishes,
2193
+ flag: last.flag,
2194
+ loadingText: last.loadingText,
2195
+ externalReady: last.externalReady,
2196
+ externalStatus: last.externalStatus,
2197
+ externalError: last.externalError,
2198
+ suppressedEntryError: last.suppressedEntryError,
2199
+ errors: last.errors,
2200
+ bridgeError
2201
+ };
2202
+ if (last.nodes !== lastNodes) {
2203
+ lastNodes = last.nodes;
2204
+ nodeStableSince = now;
2205
+ }
2206
+ const hasReadySignal = last.flag === true && readyProbeHasContent(last);
2207
+ const hasRenderedContentWithoutReadySignal = last.flag !== true && readyProbeHasTargetContent(last);
2208
+ if ((hasReadySignal || hasRenderedContentWithoutReadySignal) && last.externalReady !== false && !last.externalError && !last.loadingText) {
2209
+ if (now - nodeStableSince >= READY_NODE_STABLE_MS) {
2210
+ return {
2211
+ ...status2,
2212
+ ready: true
2213
+ };
2214
+ }
2215
+ }
2216
+ if (last.externalError) return status2;
2217
+ if (options.onProgress && progressIntervalMs > 0 && now >= nextProgressAt) {
2218
+ options.onProgress(status2);
2219
+ do {
2220
+ nextProgressAt += progressIntervalMs;
2221
+ } while (now >= nextProgressAt);
2222
+ }
2223
+ await new Promise((r) => setTimeout(r, READY_PROBE_POLL_MS));
2224
+ }
2225
+ const status = {
2226
+ ready: false,
2227
+ elapsedMs: Date.now() - start,
2228
+ nodes: last.nodes,
2229
+ targets: last.targets,
2230
+ liveFrameActive: last.liveFrameActive,
2231
+ liveFrameChannels: last.liveFrameChannels,
2232
+ liveFramePublishes: last.liveFramePublishes,
2233
+ flag: last.flag,
2234
+ loadingText: last.loadingText,
2235
+ externalReady: last.externalReady,
2236
+ externalStatus: last.externalStatus,
2237
+ externalError: last.externalError,
2238
+ suppressedEntryError: last.suppressedEntryError,
2239
+ errors: last.errors,
2240
+ bridgeError
2241
+ };
2242
+ if (options.onProgress && progressIntervalMs > 0 && Date.now() >= nextProgressAt) {
2243
+ options.onProgress(status);
2244
+ }
2245
+ return status;
2246
+ }
2247
+ function consoleEntriesEval(level, limit) {
2248
+ const getter = level === "error" ? "getErrors" : "getWarnings";
2249
+ return `(() => {
2250
+ const out = []
2251
+ const seen = new Set()
2252
+ const norm = (s) => (typeof s === 'string' ? s : (() => { try { return JSON.stringify(s) } catch { return String(s) } })())
2253
+ const key = (ts, args) => Math.round((ts || 0) / 250) + '|' + (Array.isArray(args) ? args.map(norm).join(' ') : norm(args))
2254
+ const push = (e) => {
2255
+ if (!e) return
2256
+ const ts = typeof e.timestamp === 'number' ? e.timestamp : (typeof e.ts === 'number' ? e.ts : 0)
2257
+ const k = key(ts, e.args)
2258
+ if (seen.has(k)) return
2259
+ seen.add(k)
2260
+ out.push({ timestamp: ts, args: Array.isArray(e.args) ? e.args : [e.args], stack: e.stack || undefined, source: e.source || undefined })
2261
+ }
2262
+ // page-realm ws-bridge buffer first (richest stacks for page-origin errors)
2263
+ try { for (const e of (window.__sootsimConsole?.${getter}(${limit}) || [])) push(e) } catch {}
2264
+ // engine observability store \u2014 the always-on render-worker / forwarded path
2265
+ try {
2266
+ const obs = window.__sootsimObservability
2267
+ const snap = obs && obs.logs && typeof obs.logs.getSnapshot === 'function' ? obs.logs.getSnapshot() : []
2268
+ for (const e of snap) if (e && e.level === ${JSON.stringify(level)}) push(e)
2269
+ } catch {}
2270
+ return out.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)).slice(-${limit})
2271
+ })()`;
2272
+ }
2273
+ async function inspectErrors(bridge, limit = 20) {
2274
+ const result = await bridge.send({
2275
+ type: "evaluate",
2276
+ code: consoleEntriesEval("error", limit)
2277
+ });
2278
+ return Array.isArray(result) ? result : [];
2279
+ }
2280
+ async function inspectWarnings(bridge, limit = 20) {
2281
+ const result = await bridge.send({
2282
+ type: "evaluate",
2283
+ code: consoleEntriesEval("warn", limit)
2284
+ });
2285
+ return Array.isArray(result) ? result : [];
2286
+ }
2287
+ async function inspectLogs(bridge) {
2288
+ const res = await bridge.send({
2289
+ type: "evaluate",
2290
+ code: `(() => {
2291
+ const obs = window.__sootsimObservability;
2292
+ if (!obs) return { ok: false };
2293
+ return { ok: true, entries: obs.logs.getSnapshot() };
2294
+ })()`
2295
+ });
2296
+ if (!res || !res.ok) return [];
2297
+ return res.entries ?? [];
2298
+ }
2299
+ function isForwardedWorkerLogTwin(a, b) {
2300
+ if (a.source === b.source) return false;
2301
+ if (a.level !== b.level) return false;
2302
+ if (Math.abs(a.ts - b.ts) > 1e3) return false;
2303
+ if (a.args.length !== b.args.length) return false;
2304
+ const sources = /* @__PURE__ */ new Set([a.source, b.source]);
2305
+ if (!sources.has("sootsim-worker")) return false;
2306
+ if (!sources.has("render-worker") && !sources.has("forwarded-render-worker")) {
2307
+ return false;
2308
+ }
2309
+ return a.args.every((arg, index) => arg === b.args[index]);
2310
+ }
2311
+ function filterLogEntries(entries, opts = {}) {
2312
+ let out = [];
2313
+ for (const entry of entries) {
2314
+ if (out.some((candidate) => isForwardedWorkerLogTwin(candidate, entry))) {
2315
+ continue;
2316
+ }
2317
+ out.push(entry);
2318
+ }
2319
+ if (!opts.showInternal) {
2320
+ out = out.filter((e) => {
2321
+ const first = e.args[0];
2322
+ return !(typeof first === "string" && first.startsWith("[rnx]"));
2323
+ });
2324
+ }
2325
+ if (opts.level) out = out.filter((e) => opts.level.has(e.level));
2326
+ if (opts.filter) {
2327
+ const lf = opts.filter.toLowerCase();
2328
+ out = out.filter((e) => e.args.join(" ").toLowerCase().includes(lf));
2329
+ }
2330
+ return out;
2331
+ }
2332
+ async function inspectKeyboard(bridge) {
2333
+ const result = await bridge.send({
2334
+ type: "evaluate",
2335
+ code: `(() => {
2336
+ const kb = window.__sootsimKeyboard
2337
+ if (!kb || typeof kb.getLayout !== 'function') {
2338
+ return { error: 'keyboard bridge getLayout() not available' }
2339
+ }
2340
+ return kb.getLayout()
2341
+ })()`
2342
+ });
2343
+ return result ?? { error: "keyboard bridge returned no result" };
2344
+ }
2345
+ function isShellCommandUnavailable(error) {
2346
+ const message = error instanceof Error ? error.message : String(error);
2347
+ return message.includes("call target not found: SootSim.bridges.mainShell") || message.includes("test bridge unavailable before app-in-worker boot");
2348
+ }
2349
+ async function getShellState(bridge, readyTimeoutMs = 0) {
2350
+ const deadline = Date.now() + Math.max(0, readyTimeoutMs);
2351
+ while (true) {
2352
+ try {
2353
+ return await bridge.send({
2354
+ type: "call",
2355
+ path: "SootSim.bridges.mainShell.getState",
2356
+ args: []
2357
+ });
2358
+ } catch (error) {
2359
+ if (!isShellCommandUnavailable(error) || Date.now() >= deadline) throw error;
2360
+ await new Promise((r) => setTimeout(r, 50));
2361
+ }
2362
+ }
2363
+ }
2364
+
2365
+ // cli/commands/inspect/resolve-target.ts
2366
+ async function resolveTargetCoords(bridge, target) {
2367
+ const arg = JSON.stringify(target.value);
2368
+ const findExpr = target.mode === "testid" ? `(await t.findByTestId(${arg})) || (await t.findById(${arg}))` : `await t.findByText(${arg})`;
2369
+ const result = await bridge.send({
2370
+ type: "evaluate",
2371
+ code: `(async () => {
2372
+ const t = window.__sootsimTest
2373
+ if (!t) return null
2374
+ const n = ${findExpr}
2375
+ if (!n || !n.absolutePosition || !n.layout) return null
2376
+ const resolved =
2377
+ typeof n.nodeId === 'number' && typeof t.resolveTapTarget === 'function'
2378
+ ? await t.resolveTapTarget(n.nodeId)
2379
+ : null
2380
+ const cx =
2381
+ typeof resolved?.cx === 'number'
2382
+ ? resolved.cx
2383
+ : n.absolutePosition.x + (n.layout.width || 0) / 2
2384
+ const cy =
2385
+ typeof resolved?.cy === 'number'
2386
+ ? resolved.cy
2387
+ : n.absolutePosition.y + (n.layout.height || 0) / 2
2388
+ return {
2389
+ x: cx,
2390
+ y: cy,
2391
+ id: n.id ?? null,
2392
+ testID: n.testID ?? null,
2393
+ text: ${JSON.stringify(target.mode === "text")} ? ${arg} : (n.text ?? n.accessibilityLabel ?? null),
2394
+ type: n.type ?? null,
2395
+ }
2396
+ })()`
2397
+ });
2398
+ return result ?? null;
2399
+ }
2400
+
2401
+ // src/sim-client.ts
2402
+ var SimClientError = class extends Error {
2403
+ constructor(message) {
2404
+ super(message);
2405
+ this.name = new.target.name;
2406
+ }
2407
+ };
2408
+ var SimConnectError = class extends SimClientError {
2409
+ };
2410
+ var SimReadyTimeoutError = class extends SimClientError {
2411
+ status;
2412
+ constructor(status) {
2413
+ super(`sim not ready: ${waitReadyReason(status)}`);
2414
+ this.status = status;
2415
+ }
2416
+ };
2417
+ var SimTapError = class extends SimClientError {
2418
+ outcome;
2419
+ constructor(message, outcome = null) {
2420
+ super(message);
2421
+ this.outcome = outcome;
2422
+ }
2423
+ };
2424
+ var SimKeyboardError = class extends SimClientError {
2425
+ };
2426
+ var SimPerformError = class extends SimClientError {
2427
+ result;
2428
+ constructor(result) {
2429
+ const failed = result.steps.find((step) => !step.ok);
2430
+ const at = failed ? `${failed.index} (${failed.type})` : String(result.completed);
2431
+ const cause = failed?.error ?? result.error;
2432
+ super(`perform failed at step ${at}${cause ? `: ${cause}` : ""}`);
2433
+ this.result = result;
2434
+ }
2435
+ };
2436
+ var SimResetError = class extends SimClientError {
2437
+ result;
2438
+ constructor(result) {
2439
+ super(`reset (${result.strategy}) failed${result.error ? `: ${result.error}` : ""}`);
2440
+ this.result = result;
2441
+ }
2442
+ };
2443
+ function sleep(ms) {
2444
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
2445
+ }
2446
+ function decodePngDataUrl(dataUrl) {
2447
+ if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:image/png;base64,")) {
2448
+ throw new SimClientError(
2449
+ `screenshot bridge returned a non-png payload: ${JSON.stringify(
2450
+ typeof dataUrl === "string" ? dataUrl.slice(0, 80) : dataUrl
2451
+ )}`
2452
+ );
2453
+ }
2454
+ const base64 = dataUrl.slice("data:image/png;base64,".length);
2455
+ const binary = atob(base64);
2456
+ const bytes = new Uint8Array(binary.length);
2457
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
2458
+ return { dataUrl, bytes };
2459
+ }
2460
+ var TARGET_SEARCH_PARAMS = [
2461
+ "bundle",
2462
+ "demo",
2463
+ "app",
2464
+ "open",
2465
+ "port",
2466
+ "inspectOpen",
2467
+ "appFonts",
2468
+ "appSplash"
2469
+ ];
2470
+ function buildSimOpenUrl(currentUrl, target) {
2471
+ const url = new URL(currentUrl);
2472
+ for (const param of TARGET_SEARCH_PARAMS) url.searchParams.delete(param);
2473
+ const pathname = url.pathname.replace(/\/+$/, "") || "/";
2474
+ const prefix2 = pathname === "/__soot" || pathname.startsWith("/__soot/") ? "/__soot" : "";
2475
+ const normalized = String(target).trim();
2476
+ if (/^\d+$/.test(normalized)) {
2477
+ url.pathname = `${prefix2}/rn/${normalized}`;
2478
+ } else {
2479
+ url.pathname = `${prefix2}/rn`;
2480
+ url.searchParams.set("open", normalized);
2481
+ }
2482
+ return url.toString();
2483
+ }
2484
+ function isTextTapTarget(target) {
2485
+ return typeof target === "object" && "text" in target && typeof target.text === "string" && !("layout" in target) && !("absolutePosition" in target);
2486
+ }
2487
+ var SimClient = class {
2488
+ simId;
2489
+ bridge;
2490
+ closed = false;
2491
+ constructor(bridge, simId, _options = {}) {
2492
+ this.bridge = bridge;
2493
+ this.simId = simId;
2494
+ }
2495
+ // --- transport (WsBridge/InspectBridge) ---
2496
+ send(cmd, opts) {
2497
+ if (this.closed) {
2498
+ return Promise.reject(new SimConnectError("sim client is closed"));
2499
+ }
2500
+ const payload = cmd.simId === void 0 ? { ...cmd, simId: this.simId } : cmd;
2501
+ return this.bridge.send(payload, opts);
2502
+ }
2503
+ listSims() {
2504
+ return this.bridge.listSims();
2505
+ }
2506
+ focusSim(simId = this.simId) {
2507
+ return this.bridge.focusSim(simId);
2508
+ }
2509
+ closeSim(simId = this.simId) {
2510
+ return this.bridge.closeSim(simId);
2511
+ }
2512
+ claim(simId = this.simId, opts) {
2513
+ return this.bridge.claim(simId, opts);
2514
+ }
2515
+ /** release the socket. a script that connected must close to exit cleanly. */
2516
+ close() {
2517
+ if (this.closed) return;
2518
+ this.closed = true;
2519
+ this.bridge.close();
2520
+ }
2521
+ // --- navigation + readiness ---
2522
+ /**
2523
+ * point this sim at a dev target: a metro port (`8081`), or anything the
2524
+ * shell's connect flow accepts (`localhost:8081`, an app slug…). resolves
2525
+ * once the reloaded page answers on the new target route.
2526
+ */
2527
+ async open(target, opts = {}) {
2528
+ const sims = await this.listSims();
2529
+ const sim = sims.find((entry) => entry.id === this.simId);
2530
+ if (!sim) {
2531
+ throw new SimConnectError(`sim ${this.simId} is no longer connected`);
2532
+ }
2533
+ const currentUrl = sim.url ?? sim.origin;
2534
+ if (!currentUrl) {
2535
+ throw new SimConnectError(
2536
+ `sim ${this.simId} reported no URL to derive the shell from`
2537
+ );
2538
+ }
2539
+ const nextUrl = buildSimOpenUrl(currentUrl, target);
2540
+ const expected = new URL(nextUrl);
2541
+ await this.send({
2542
+ type: "evaluate",
2543
+ code: `window.location.href = ${JSON.stringify(nextUrl)}`
2544
+ });
2545
+ const deadline = Date.now() + (opts.timeoutMs ?? 3e4);
2546
+ while (Date.now() < deadline) {
2547
+ await sleep(250);
2548
+ try {
2549
+ const { url } = await inspectUrl(this);
2550
+ const current = new URL(url);
2551
+ if (current.pathname === expected.pathname && current.searchParams.get("open") === expected.searchParams.get("open")) {
2552
+ return;
2553
+ }
2554
+ } catch {
2555
+ }
2556
+ }
2557
+ throw new SimConnectError(
2558
+ `timed out waiting for sim ${this.simId} to load ${nextUrl}`
2559
+ );
2560
+ }
2561
+ /**
2562
+ * block until the guest app has mounted and painted real content.
2563
+ * throws SimReadyTimeoutError (with the last probe status) on timeout.
2564
+ */
2565
+ async waitReady(opts = {}) {
2566
+ const status = await inspectWaitReady(this, opts.timeoutMs ?? 2e4, {
2567
+ onProgress: opts.onProgress
2568
+ });
2569
+ if (!status.ready) throw new SimReadyTimeoutError(status);
2570
+ return status;
2571
+ }
2572
+ /** wait for layout/animation/transition settle after an interaction. */
2573
+ settle(opts = {}) {
2574
+ return waitForSootsimIdle({
2575
+ bridge: this,
2576
+ maxMs: opts.maxMs ?? 3e3,
2577
+ strict: opts.strict
2578
+ });
2579
+ }
2580
+ // --- read verbs ---
2581
+ /** first matching node, or null. query precedence matches the CLI's find. */
2582
+ async find(query) {
2583
+ const nodes = await this.findAll(query);
2584
+ return nodes[0] ?? null;
2585
+ }
2586
+ async findAll(query) {
2587
+ const mapped = {
2588
+ testId: query.testId ?? null,
2589
+ role: query.role ?? null,
2590
+ type: query.type ?? null,
2591
+ text: query.text ?? null,
2592
+ pressable: query.pressable
2593
+ };
2594
+ const found = await inspectFind(this, mapped);
2595
+ if (!found) {
2596
+ throw new SimClientError(
2597
+ "find() needs at least one of testId, text, role, type, or pressable"
2598
+ );
2599
+ }
2600
+ const { result } = found;
2601
+ if (Array.isArray(result)) return result;
2602
+ return result ? [result] : [];
2603
+ }
2604
+ async tree(depth = 5) {
2605
+ const { tree } = await inspectTree(this, depth);
2606
+ return tree;
2607
+ }
2608
+ /** compact visible-UI dump with shell + keyboard state, as the CLI prints. */
2609
+ describe(opts = {}) {
2610
+ return inspectDescribe(this, {
2611
+ describe: true,
2612
+ verbose: opts.verbose ?? false,
2613
+ filter: opts.filter ?? "",
2614
+ compact: !opts.verbose,
2615
+ hideXy: false,
2616
+ fullText: true
2617
+ });
2618
+ }
2619
+ accessibilityTree() {
2620
+ return inspectAccessibilityTree(this);
2621
+ }
2622
+ async logs(opts = {}) {
2623
+ return filterLogEntries(await inspectLogs(this), opts);
2624
+ }
2625
+ errors(limit = 20) {
2626
+ return inspectErrors(this, limit);
2627
+ }
2628
+ warnings(limit = 20) {
2629
+ return inspectWarnings(this, limit);
2630
+ }
2631
+ keyboard() {
2632
+ return inspectKeyboard(this);
2633
+ }
2634
+ shellState(readyTimeoutMs = 0) {
2635
+ return getShellState(this, readyTimeoutMs);
2636
+ }
2637
+ async url() {
2638
+ const { url } = await inspectUrl(this);
2639
+ return url;
2640
+ }
2641
+ async screenshot(opts = {}) {
2642
+ const result = await this.send({
2643
+ type: "screenshot",
2644
+ ...opts.layers ? { layers: opts.layers } : {},
2645
+ ...opts.crop ? { crop: opts.crop } : {}
2646
+ });
2647
+ return decodePngDataUrl(result);
2648
+ }
2649
+ // --- input verbs ---
2650
+ /**
2651
+ * tap by testID, visible text, node, or coordinates. resolution, retry, and
2652
+ * hit verification are the CLI's shared tap kernels. throws SimTapError when
2653
+ * the target is missing, covered, offscreen, or the press missed.
2654
+ */
2655
+ async tap(target) {
2656
+ if (typeof target === "string") {
2657
+ return this.throwUnlessTapped(await tapBest(this, target), target);
2658
+ }
2659
+ if ("testId" in target && typeof target.testId === "string") {
2660
+ return this.throwUnlessTapped(await tapById(this, target.testId), target.testId);
2661
+ }
2662
+ if (isTextTapTarget(target)) {
2663
+ const { text, ...textOpts } = target;
2664
+ return this.throwUnlessTapped(await tapByText(this, text, textOpts), text);
2665
+ }
2666
+ if ("testID" in target && typeof target.testID === "string") {
2667
+ return this.throwUnlessTapped(await tapById(this, target.testID), target.testID);
2668
+ }
2669
+ const coords = this.coordsForTapTarget(target);
2670
+ const result = await this.send({ type: "tap", x: coords.x, y: coords.y });
2671
+ if (!isTapSuccess(result)) {
2672
+ throw new SimTapError(
2673
+ `tap at ${coords.x},${coords.y} missed: ${JSON.stringify(result)}`
2674
+ );
2675
+ }
2676
+ }
2677
+ async longPress(target, opts = {}) {
2678
+ const coords = await this.resolveCoords(target);
2679
+ const result = await this.send(
2680
+ {
2681
+ type: "longPress",
2682
+ x: coords.x,
2683
+ y: coords.y,
2684
+ durationMs: opts.durationMs ?? 600
2685
+ },
2686
+ { timeoutMs: (opts.durationMs ?? 600) + 15e3 }
2687
+ );
2688
+ if (!result?.ok) {
2689
+ throw new SimTapError(`longPress at ${coords.x},${coords.y} missed`);
2690
+ }
2691
+ }
2692
+ /** type through the visual iOS keyboard. requires a focused input. */
2693
+ async type(text) {
2694
+ await this.requireVisibleKeyboard("type");
2695
+ await this.send({ type: "keyboard", action: "type", text });
2696
+ }
2697
+ /** press a keyboard key ('return', 'delete', 'space', …). */
2698
+ async press(key) {
2699
+ await this.requireVisibleKeyboard("press");
2700
+ await this.send({ type: "keyboard", action: "press", text: key });
2701
+ }
2702
+ async dismissKeyboard() {
2703
+ await this.send({ type: "keyboard", action: "dismiss" });
2704
+ }
2705
+ // --- batched input, compound state, reset ---
2706
+ /**
2707
+ * execute an ordered step batch inside the page in ONE round-trip, so waits
2708
+ * and gesture physics run on real wall-clock timing. `tapText` steps are
2709
+ * resolved to coordinates client-side before the batch is sent. throws
2710
+ * SimPerformError (carrying the per-step result) when the batch fails.
2711
+ */
2712
+ async perform(steps, options) {
2713
+ const resolved = [];
2714
+ for (const step of steps) {
2715
+ if (step.type !== "tapText") {
2716
+ resolved.push(step);
2717
+ continue;
2718
+ }
2719
+ const target = await resolveTargetCoords(this, {
2720
+ mode: "text",
2721
+ value: step.text
2722
+ });
2723
+ if (!target) {
2724
+ throw new SimTapError(
2725
+ `perform tapText: no node with text ${JSON.stringify(step.text)}`
2726
+ );
2727
+ }
2728
+ resolved.push({
2729
+ type: "tap",
2730
+ x: target.x,
2731
+ y: target.y,
2732
+ target: {
2733
+ testID: target.testID ?? void 0,
2734
+ text: target.text ?? void 0,
2735
+ type: target.type ?? void 0
2736
+ }
2737
+ });
2738
+ }
2739
+ const batchTimeoutMs = options?.timeoutMs ?? 3e4;
2740
+ const result = await this.send(
2741
+ {
2742
+ type: "perform",
2743
+ steps: resolved,
2744
+ ...options ? { performOptions: options } : {}
2745
+ },
2746
+ { timeoutMs: batchTimeoutMs + 5e3 }
2747
+ );
2748
+ if (!result?.ok) throw new SimPerformError(result);
2749
+ return result;
2750
+ }
2751
+ /** screenshot + tree + route + recent errors in one round-trip. */
2752
+ state(options) {
2753
+ return this.send({
2754
+ type: "state",
2755
+ ...options ? { stateOptions: options } : {}
2756
+ });
2757
+ }
2758
+ /**
2759
+ * two-tier app state wipe ('data' clears storage/caches, 'full' also clears
2760
+ * keychain/permissions/native-module state). throws SimResetError on failure.
2761
+ */
2762
+ async reset(options) {
2763
+ const result = await this.send(
2764
+ { type: "reset", ...options ? { resetOptions: options } : {} },
2765
+ { timeoutMs: 6e4 }
2766
+ );
2767
+ if (!result?.ok) throw new SimResetError(result);
2768
+ return result;
2769
+ }
2770
+ // --- internals ---
2771
+ throwUnlessTapped(outcome, label) {
2772
+ if (!outcome.failure && isTapSuccess(outcome.result)) return;
2773
+ const reason = outcome.failure === "not-found" ? "no matching node" : outcome.failure === "missed" ? outcome.result?.reason ?? "press missed" : outcome.payload?.ambiguous ? `ambiguous: ${outcome.payload.total} matches` : outcome.result?.reason ?? outcome.payload?.error ?? "tap failed";
2774
+ throw new SimTapError(`tap ${JSON.stringify(label)} failed: ${reason}`, outcome);
2775
+ }
2776
+ coordsForTapTarget(target) {
2777
+ if (typeof target === "object" && "x" in target && "y" in target && typeof target.x === "number" && typeof target.y === "number") {
2778
+ return { x: target.x, y: target.y };
2779
+ }
2780
+ if (typeof target === "object" && "absolutePosition" in target && target.absolutePosition && target.layout) {
2781
+ return {
2782
+ x: target.absolutePosition.x + (target.layout.width ?? 0) / 2,
2783
+ y: target.absolutePosition.y + (target.layout.height ?? 0) / 2
2784
+ };
2785
+ }
2786
+ throw new SimClientError(
2787
+ "tap target needs coordinates, a testId, text, or a node from find()"
2788
+ );
2789
+ }
2790
+ async resolveCoords(target) {
2791
+ if (typeof target === "string") {
2792
+ const resolved = await resolveTargetCoords(this, { mode: "testid", value: target }) ?? await resolveTargetCoords(this, { mode: "text", value: target });
2793
+ if (!resolved) throw new SimTapError(`no node matching ${JSON.stringify(target)}`);
2794
+ return resolved;
2795
+ }
2796
+ if ("testId" in target && typeof target.testId === "string") {
2797
+ const resolved = await resolveTargetCoords(this, {
2798
+ mode: "testid",
2799
+ value: target.testId
2800
+ });
2801
+ if (!resolved) {
2802
+ throw new SimTapError(`no node with testID ${JSON.stringify(target.testId)}`);
2803
+ }
2804
+ return resolved;
2805
+ }
2806
+ if ("text" in target && typeof target.text === "string" && !("layout" in target)) {
2807
+ const resolved = await resolveTargetCoords(this, {
2808
+ mode: "text",
2809
+ value: target.text
2810
+ });
2811
+ if (!resolved) {
2812
+ throw new SimTapError(`no node with text ${JSON.stringify(target.text)}`);
2813
+ }
2814
+ return resolved;
2815
+ }
2816
+ return this.coordsForTapTarget(target);
2817
+ }
2818
+ async requireVisibleKeyboard(action) {
2819
+ const deadline = Date.now() + 1500;
2820
+ for (; ; ) {
2821
+ const state = await inspectKeyboard(this);
2822
+ if (!("error" in state) && state.visible) return;
2823
+ if (Date.now() >= deadline) {
2824
+ throw new SimKeyboardError(
2825
+ `${action} requires the iOS keyboard to be visible \u2014 focus an input first (tap a TextInput)`
2826
+ );
2827
+ }
2828
+ await sleep(150);
2829
+ }
2830
+ }
2831
+ };
2832
+
2833
+ // src/connect.ts
2834
+ function pickSim(sims, wanted, port) {
2835
+ const open = sims.filter((sim) => sim.readyState === "open");
2836
+ if (wanted) {
2837
+ const match = open.find((sim) => sim.id === wanted);
2838
+ if (!match) {
2839
+ throw new SimConnectError(
2840
+ `no sim connected with id ${wanted} on bridge :${port}` + (open.length ? ` \u2014 connected: ${open.map((sim) => sim.id).join(", ")}` : " \u2014 no sims connected; open one with `rnxsim open <port>`")
2841
+ );
2842
+ }
2843
+ return match;
2844
+ }
2845
+ const pinned = readCurrentSimId();
2846
+ if (pinned) {
2847
+ const match = open.find((sim) => sim.id === pinned);
2848
+ if (match) return match;
2849
+ }
2850
+ const primary = open.find((sim) => sim.isPrimary) ?? open[0];
2851
+ if (!primary) {
2852
+ throw new SimConnectError(
2853
+ `no sims connected on bridge :${port} \u2014 open one with \`rnxsim open <port>\``
2854
+ );
2855
+ }
2856
+ return primary;
2857
+ }
2858
+ async function connect(options = {}) {
2859
+ const port = options.port ?? resolveBridgeWorld().bridgePort;
2860
+ const commandTimeoutMs = options.timeoutMs ?? 15e3;
2861
+ const discovery = createBridge(port, { commandTimeoutMs, cliLabel: "sdk" });
2862
+ let picked;
2863
+ try {
2864
+ picked = pickSim(await discovery.listSims(), options.sim, port);
2865
+ } catch (error) {
2866
+ discovery.close();
2867
+ if (error instanceof SimConnectError) throw error;
2868
+ const message = error instanceof Error ? error.message : String(error);
2869
+ throw new SimConnectError(`${message} \u2014 is the rnxsim daemon or dev shell running?`);
2870
+ }
2871
+ discovery.close();
2872
+ const bridge = createBridge(port, {
2873
+ commandTimeoutMs,
2874
+ cliLabel: "sdk",
2875
+ simId: picked.id,
2876
+ simIdSource: "flag"
2877
+ });
2878
+ return new SimClient(bridge, picked.id, { commandTimeoutMs });
2879
+ }
2880
+
2881
+ // src/vite-plugin-one.ts
2882
+ var import_fs4 = __toESM(require("fs"), 1);
2883
+ var import_path3 = __toESM(require("path"), 1);
115
2884
 
116
2885
  // src/runtime-assets.ts
117
- var import_fs = __toESM(require("fs"), 1);
118
- var import_path = __toESM(require("path"), 1);
2886
+ var import_fs3 = __toESM(require("fs"), 1);
2887
+ var import_path2 = __toESM(require("path"), 1);
119
2888
  var import_url = require("url");
120
2889
 
121
2890
  // ../contrast-runtime-delivery/src/product.ts
@@ -128,13 +2897,13 @@ var SOOTSIM_RUNTIME_MISSING_MESSAGE = "[rnx] no engine runtime installed \u2014
128
2897
  var _monorepoRootCache;
129
2898
  function findMonorepoRoot() {
130
2899
  if (_monorepoRootCache !== void 0) return _monorepoRootCache;
131
- let dir = import_path.default.dirname((0, import_url.fileURLToPath)(__sootsim_import_meta_url));
132
- for (let i = 0; i < 12 && dir !== import_path.default.dirname(dir); i++) {
133
- if (import_fs.default.existsSync(import_path.default.join(dir, "packages/sootsim-engine/package.json")) && import_fs.default.existsSync(import_path.default.join(dir, "public/sootsim"))) {
2900
+ let dir = import_path2.default.dirname((0, import_url.fileURLToPath)(__sootsim_import_meta_url));
2901
+ for (let i = 0; i < 12 && dir !== import_path2.default.dirname(dir); i++) {
2902
+ if (import_fs3.default.existsSync(import_path2.default.join(dir, "packages/sootsim-engine/package.json")) && import_fs3.default.existsSync(import_path2.default.join(dir, "public/sootsim"))) {
134
2903
  _monorepoRootCache = dir;
135
2904
  return dir;
136
2905
  }
137
- dir = import_path.default.dirname(dir);
2906
+ dir = import_path2.default.dirname(dir);
138
2907
  }
139
2908
  _monorepoRootCache = null;
140
2909
  return null;
@@ -143,8 +2912,8 @@ function resolveDevCheckoutRuntimeRoot() {
143
2912
  if (process.env.SOOTSIM_HOME && readActiveRuntime()) return null;
144
2913
  const root = findMonorepoRoot();
145
2914
  if (!root) return null;
146
- const devBuild = import_path.default.join(root, "public/sootsim");
147
- if (import_fs.default.existsSync(import_path.default.join(devBuild, "index.html")) && import_fs.default.existsSync(import_path.default.join(devBuild, "engine/index.html")) && import_fs.default.existsSync(import_path.default.join(devBuild, "engine-tenant/worker.js"))) {
2915
+ const devBuild = import_path2.default.join(root, "public/sootsim");
2916
+ if (import_fs3.default.existsSync(import_path2.default.join(devBuild, "index.html")) && import_fs3.default.existsSync(import_path2.default.join(devBuild, "engine/index.html")) && import_fs3.default.existsSync(import_path2.default.join(devBuild, "engine-tenant/worker.js"))) {
148
2917
  return devBuild;
149
2918
  }
150
2919
  return null;
@@ -189,7 +2958,7 @@ function resolveActiveRuntimeRoot() {
189
2958
  const active = readActiveRuntime();
190
2959
  if (!active) return null;
191
2960
  const dir = runtimeDir(active);
192
- if (!import_fs.default.existsSync(import_path.default.join(dir, "index.html"))) return null;
2961
+ if (!import_fs3.default.existsSync(import_path2.default.join(dir, "index.html"))) return null;
193
2962
  return dir;
194
2963
  }
195
2964
  var RUNTIME_VERSION_HOSTNAME_SUFFIX = ".runtime.localhost";
@@ -213,7 +2982,7 @@ function runtimeVersionFromHostname(hostname) {
213
2982
  }
214
2983
  function resolveInstalledRuntimeRoot(version) {
215
2984
  const dir = runtimeDir(version);
216
- if (!import_fs.default.existsSync(import_path.default.join(dir, "index.html"))) return null;
2985
+ if (!import_fs3.default.existsSync(import_path2.default.join(dir, "index.html"))) return null;
217
2986
  return dir;
218
2987
  }
219
2988
  function resolveRuntimeRootForHostname(hostname) {
@@ -238,15 +3007,15 @@ function resolveRuntimeFilePath(runtimeRoot, pathname) {
238
3007
  for (const segment of pathname.split("/")) {
239
3008
  if (segment === "..") return null;
240
3009
  }
241
- const fullPath = import_path.default.resolve(runtimeRoot, pathname.replace(/^\/+/, ""));
242
- const rootWithSep = runtimeRoot.endsWith(import_path.default.sep) ? runtimeRoot : runtimeRoot + import_path.default.sep;
3010
+ const fullPath = import_path2.default.resolve(runtimeRoot, pathname.replace(/^\/+/, ""));
3011
+ const rootWithSep = runtimeRoot.endsWith(import_path2.default.sep) ? runtimeRoot : runtimeRoot + import_path2.default.sep;
243
3012
  if (!fullPath.startsWith(rootWithSep) && fullPath !== runtimeRoot) return null;
244
- if (!import_fs.default.existsSync(fullPath) || !import_fs.default.statSync(fullPath).isFile()) return null;
3013
+ if (!import_fs3.default.existsSync(fullPath) || !import_fs3.default.statSync(fullPath).isFile()) return null;
245
3014
  return fullPath;
246
3015
  }
247
3016
  function serveRuntimeFile(req, res, fullPath) {
248
- const ext = import_path.default.extname(fullPath);
249
- const stat = import_fs.default.statSync(fullPath);
3017
+ const ext = import_path2.default.extname(fullPath);
3018
+ const stat = import_fs3.default.statSync(fullPath);
250
3019
  const lastModified = stat.mtime.toUTCString();
251
3020
  res.setHeader("content-type", MIME_TYPES[ext] || "application/octet-stream");
252
3021
  res.setHeader("cache-control", "no-cache");
@@ -256,7 +3025,7 @@ function serveRuntimeFile(req, res, fullPath) {
256
3025
  res.end();
257
3026
  return;
258
3027
  }
259
- import_fs.default.createReadStream(fullPath).pipe(res);
3028
+ import_fs3.default.createReadStream(fullPath).pipe(res);
260
3029
  }
261
3030
 
262
3031
  // src/vite-plugin-one.ts
@@ -269,7 +3038,7 @@ function sootsimPlugin(options = {}) {
269
3038
  configureServer(server) {
270
3039
  const bundleUrl = options.bundleUrl || "/node_modules/one/metro-entry.bundle?platform=ios&dev=true&minify=false";
271
3040
  const buildShellHtml = (runtimeRoot) => {
272
- const html = import_fs2.default.readFileSync(import_path2.default.join(runtimeRoot, "index.html"), "utf8");
3041
+ const html = import_fs4.default.readFileSync(import_path3.default.join(runtimeRoot, "index.html"), "utf8");
273
3042
  const lock = readDaemonLockfile();
274
3043
  const bridgePort = lock && isDaemonLockfileFresh(lock) && lock.bridgePort > 0 ? lock.bridgePort : DEFAULT_SOOTSIM_BRIDGE_PORT;
275
3044
  const inject = `<script>(function(){var u=new URL(location.href);if(!u.searchParams.has('bundle'))u.searchParams.set('bundle',${JSON.stringify(bundleUrl)});window.__sootsimBridgePort=${bridgePort};history.replaceState(null,'','${prefix2}/'+u.search)})()</script>`;
@@ -341,7 +3110,7 @@ function sootsimPlugin(options = {}) {
341
3110
  serveRuntimeFile(req, res, fullPath);
342
3111
  return;
343
3112
  }
344
- const ext = import_path2.default.extname(pathname);
3113
+ const ext = import_path3.default.extname(pathname);
345
3114
  if (!ext) {
346
3115
  res.setHeader("content-type", "text/html");
347
3116
  res.end(buildShellHtml(runtimeRoot));
@@ -359,8 +3128,8 @@ function sootsimPlugin(options = {}) {
359
3128
  }
360
3129
 
361
3130
  // src/metro-plugin.ts
362
- var import_fs3 = __toESM(require("fs"), 1);
363
- var import_path3 = __toESM(require("path"), 1);
3131
+ var import_fs5 = __toESM(require("fs"), 1);
3132
+ var import_path4 = __toESM(require("path"), 1);
364
3133
  var prefix = "/__soot";
365
3134
  function withSootsim(config, options = {}) {
366
3135
  if (options.enabled === false) return config;
@@ -374,14 +3143,14 @@ function withSootsim(config, options = {}) {
374
3143
  if (existingEnhance) {
375
3144
  middleware = existingEnhance(metroMiddleware, metroServer);
376
3145
  }
377
- let connect;
3146
+ let connect2;
378
3147
  try {
379
- connect = require("connect");
3148
+ connect2 = require("connect");
380
3149
  } catch {
381
3150
  console.warn("[rnx] connect not available");
382
3151
  return middleware;
383
3152
  }
384
- const server = connect();
3153
+ const server = connect2();
385
3154
  server.use((req, res, next) => {
386
3155
  const url = req.url || "";
387
3156
  const pathname = url.split("?")[0];
@@ -413,10 +3182,10 @@ function withSootsim(config, options = {}) {
413
3182
  }
414
3183
  }
415
3184
  if (pathname === prefix || pathname === prefix + "/" || pathname.startsWith(prefix + "/")) {
416
- const ext = import_path3.default.extname(pathname);
3185
+ const ext = import_path4.default.extname(pathname);
417
3186
  if (!ext) {
418
- const htmlPath = import_path3.default.join(runtimeRoot, "index.html");
419
- let html = import_fs3.default.readFileSync(htmlPath, "utf8");
3187
+ const htmlPath = import_path4.default.join(runtimeRoot, "index.html");
3188
+ let html = import_fs5.default.readFileSync(htmlPath, "utf8");
420
3189
  html = html.replace(
421
3190
  "</head>",
422
3191
  `<script>history.replaceState(null,'','${prefix}/?bundle=${encodeURIComponent(bundleUrl)}')</script></head>`
@@ -438,13 +3207,13 @@ function withSootsim(config, options = {}) {
438
3207
  var metro_plugin_default = withSootsim;
439
3208
 
440
3209
  // src/jump-to-source-babel.ts
441
- function normalizeSourcePath(path5) {
442
- const queryStart = path5.indexOf("?");
443
- const clean = queryStart >= 0 ? path5.slice(0, queryStart) : path5;
3210
+ function normalizeSourcePath(path6) {
3211
+ const queryStart = path6.indexOf("?");
3212
+ const clean = queryStart >= 0 ? path6.slice(0, queryStart) : path6;
444
3213
  return clean.replace(/\\/g, "/");
445
3214
  }
446
- function matchesPath(path5, matcher) {
447
- return typeof matcher === "string" ? path5.includes(matcher) : matcher.test(path5);
3215
+ function matchesPath(path6, matcher) {
3216
+ return typeof matcher === "string" ? path6.includes(matcher) : matcher.test(path6);
448
3217
  }
449
3218
  function shouldStampFile(filename, options) {
450
3219
  const normalized = normalizeSourcePath(filename);
@@ -477,11 +3246,11 @@ function sootsimJumpToSourceBabelPlugin(api, options = {}) {
477
3246
  return {
478
3247
  name: "sootsim-jump-to-source",
479
3248
  visitor: {
480
- JSXOpeningElement(path5, state) {
3249
+ JSXOpeningElement(path6, state) {
481
3250
  if (options.enabled === false) return;
482
3251
  const filename = state.file?.opts?.filename;
483
3252
  if (!filename || !shouldStampFile(filename, options)) return;
484
- const node = path5.node;
3253
+ const node = path6.node;
485
3254
  const tagName = getJSXName(node.name);
486
3255
  if (!tagName || tagName === "Fragment" || tagName.endsWith(".Fragment")) return;
487
3256
  if (hasAttribute(node, attrName)) return;
@@ -499,14 +3268,20 @@ function sootsimJumpToSourceBabelPlugin(api, options = {}) {
499
3268
  }
500
3269
  };
501
3270
  }
502
-
503
- // src/cli-constants.ts
504
- var DEFAULT_SOOTSIM_SHELL_URL = "http://localhost:5173/";
505
3271
  // Annotate the CommonJS export names for ESM import in node:
506
3272
  0 && (module.exports = {
507
3273
  DEFAULT_SOOTSIM_BRIDGE_PORT,
508
3274
  DEFAULT_SOOTSIM_BRIDGE_URL,
509
3275
  DEFAULT_SOOTSIM_SHELL_URL,
3276
+ SimClient,
3277
+ SimClientError,
3278
+ SimConnectError,
3279
+ SimKeyboardError,
3280
+ SimPerformError,
3281
+ SimReadyTimeoutError,
3282
+ SimResetError,
3283
+ SimTapError,
3284
+ connect,
510
3285
  jumpToSourceBabelPlugin,
511
3286
  metroPlugin,
512
3287
  sootsimPlugin