rnxsim 0.1.366 → 0.1.368

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 (239) hide show
  1. package/README.md +14 -2
  2. package/cli/bin.ts +18 -0
  3. package/cli/bridge-flow-runner.ts +14 -151
  4. package/cli/commands/assert.ts +1 -1
  5. package/cli/commands/box/checkout-plane.ts +126 -0
  6. package/cli/commands/box/rnx-command.ts +68 -0
  7. package/cli/commands/box.ts +200 -0
  8. package/cli/commands/control.ts +12 -32
  9. package/cli/commands/do-chain.ts +412 -0
  10. package/cli/commands/flow.ts +0 -10
  11. package/cli/commands/inspect/core.ts +302 -725
  12. package/cli/commands/inspect/describe.ts +3 -76
  13. package/cli/commands/inspect/find.ts +3 -6
  14. package/cli/commands/inspect/get-layout.ts +14 -1
  15. package/cli/commands/inspect/resolve-target.ts +41 -47
  16. package/cli/commands/inspect/shared.ts +28 -123
  17. package/cli/commands/inspect/tree.ts +6 -15
  18. package/cli/commands/inspect/wait-ready.ts +2 -2
  19. package/cli/commands/inspect/wait-selector.ts +4 -8
  20. package/cli/commands/inspect.ts +23 -17
  21. package/cli/commands/install-desktop.ts +44 -7
  22. package/cli/commands/maestro-generate.ts +0 -2
  23. package/cli/commands/platform.ts +68 -0
  24. package/cli/commands/state.ts +5 -2
  25. package/cli/outbound-endpoints.ts +6 -1
  26. package/cli/parse-args.ts +13 -2
  27. package/cli/ws-bridge.ts +30 -2
  28. package/detox/index.ts +34 -17
  29. package/dist-cli/bin.js +5 -5
  30. package/dist-cli/chunks/{agent-SO6IOPRK.js → agent-GXCQMYV4.js} +2 -2
  31. package/dist-cli/chunks/{agent-wrapper-PMYCB2KO.js → agent-wrapper-Y3PQAL6I.js} +2 -2
  32. package/dist-cli/chunks/{app-fonts-LULQGFY7.js → app-fonts-MDD3W4VN.js} +2 -2
  33. package/dist-cli/chunks/{assert-WGNCD4ZU.js → assert-BSRQBRH7.js} +3 -3
  34. package/dist-cli/chunks/{auth-G7CCHH3M.js → auth-SLPHDPCF.js} +2 -2
  35. package/dist-cli/chunks/{beta-N4LXL5TO.js → beta-R4IZ5SWO.js} +2 -2
  36. package/dist-cli/chunks/box-2NQYYY3B.js +1709 -0
  37. package/dist-cli/chunks/{camera-4JHCYBQS.js → camera-ER66WWAX.js} +2 -2
  38. package/dist-cli/chunks/chunk-2YK5J2D2.js +24 -0
  39. package/dist-cli/chunks/{chunk-I6KIM6PN.js → chunk-3MPFC3DS.js} +1 -1
  40. package/dist-cli/chunks/{chunk-77ZHQJZR.js → chunk-3S2J6IVC.js} +2 -2
  41. package/dist-cli/chunks/chunk-3XMEIZEV.js +4 -0
  42. package/dist-cli/chunks/chunk-42TLK75A.js +497 -0
  43. package/dist-cli/chunks/{chunk-OW5XHAA4.js → chunk-4FLQYRGN.js} +1 -1
  44. package/dist-cli/chunks/{chunk-EFR5DVNP.js → chunk-4VA2ISWX.js} +1 -1
  45. package/dist-cli/chunks/chunk-57LITN73.js +7 -0
  46. package/dist-cli/chunks/{chunk-EQK5MJCU.js → chunk-7B22IPSC.js} +2 -2
  47. package/dist-cli/chunks/{chunk-EGAGZHO6.js → chunk-7V3S2DY2.js} +1 -1
  48. package/dist-cli/chunks/{chunk-IZKHOM37.js → chunk-AUPJX4KI.js} +1 -1
  49. package/dist-cli/chunks/{chunk-2S7BKUP2.js → chunk-BWJOLJOK.js} +2 -2
  50. package/dist-cli/chunks/{chunk-CN6XYYEA.js → chunk-CKNNO6ZG.js} +27 -27
  51. package/dist-cli/chunks/{chunk-MGDSJZ37.js → chunk-DJSPNOF6.js} +2 -2
  52. package/dist-cli/chunks/{chunk-YXXKSL6R.js → chunk-E6XYIIJK.js} +1 -1
  53. package/dist-cli/chunks/{chunk-ONORMKWX.js → chunk-ECXDCED5.js} +2 -2
  54. package/dist-cli/chunks/chunk-EHA65UVJ.js +5 -0
  55. package/dist-cli/chunks/chunk-ESVUVW3R.js +32 -0
  56. package/dist-cli/chunks/{chunk-XJU2QIRY.js → chunk-FDAFSRQE.js} +2 -2
  57. package/dist-cli/chunks/{chunk-NM2L2QV4.js → chunk-FSPZY4AI.js} +2 -2
  58. package/dist-cli/chunks/chunk-FVODUDPS.js +4 -0
  59. package/dist-cli/chunks/{chunk-H23KWIKA.js → chunk-GFWUIGGY.js} +2 -2
  60. package/dist-cli/chunks/{chunk-CTG7HBCA.js → chunk-GS5VUDCF.js} +1 -1
  61. package/dist-cli/chunks/{chunk-ORWP2E7R.js → chunk-GXHCJRKE.js} +2 -2
  62. package/dist-cli/chunks/{chunk-TTASWLKM.js → chunk-HKQXGCQT.js} +1 -1
  63. package/dist-cli/chunks/{chunk-5OK3LCEJ.js → chunk-IXRVFQMT.js} +2 -2
  64. package/dist-cli/chunks/{chunk-TIAEYO6I.js → chunk-JFGW75UL.js} +2 -2
  65. package/dist-cli/chunks/{chunk-RYIMSKDO.js → chunk-JHYXRORU.js} +2 -2
  66. package/dist-cli/chunks/chunk-LAKWOJ5Y.js +5 -0
  67. package/dist-cli/chunks/{chunk-GAJQSITE.js → chunk-LPAOOJIW.js} +1 -1
  68. package/dist-cli/chunks/chunk-M7COA4BE.js +312 -0
  69. package/dist-cli/chunks/{chunk-6PL2CCGP.js → chunk-MCO7V4VN.js} +2 -2
  70. package/dist-cli/chunks/{chunk-YGMEDJKJ.js → chunk-MGOAQHMO.js} +2 -2
  71. package/dist-cli/chunks/chunk-NERY6HGZ.js +4 -0
  72. package/dist-cli/chunks/chunk-NGWUGNPV.js +4 -0
  73. package/dist-cli/chunks/{chunk-ZMJ75EDG.js → chunk-NPSPWMBO.js} +2 -2
  74. package/dist-cli/chunks/{chunk-2UXU53MG.js → chunk-OXCDE3DD.js} +1 -1
  75. package/dist-cli/chunks/{chunk-ZLVRYXZ7.js → chunk-OZJMWTGA.js} +1 -1
  76. package/dist-cli/chunks/{chunk-HT6GFZOZ.js → chunk-PNBQBBG5.js} +1 -1
  77. package/dist-cli/chunks/{chunk-JHXETDVC.js → chunk-Q65KH25G.js} +1 -1
  78. package/dist-cli/chunks/{chunk-NQFJOCSF.js → chunk-QKOY7UW5.js} +2 -2
  79. package/dist-cli/chunks/{chunk-5DAIA77C.js → chunk-QMVBWZH2.js} +2 -2
  80. package/dist-cli/chunks/chunk-RRALQ4WM.js +38 -0
  81. package/dist-cli/chunks/{chunk-GXAB52K7.js → chunk-SHMLYTNH.js} +2 -2
  82. package/dist-cli/chunks/chunk-SSUPPM6K.js +130 -0
  83. package/dist-cli/chunks/chunk-T2MGLXY5.js +27 -0
  84. package/dist-cli/chunks/{chunk-ZJIS3CTW.js → chunk-T4ZZ26A2.js} +2 -2
  85. package/dist-cli/chunks/{chunk-YCCCPFWT.js → chunk-TC6DNQL6.js} +2 -2
  86. package/dist-cli/chunks/chunk-TE645QCO.js +423 -0
  87. package/dist-cli/chunks/{chunk-UCHW7ZHR.js → chunk-TOE5O6JE.js} +2 -2
  88. package/dist-cli/chunks/{chunk-N3UC7HIX.js → chunk-TPIPKXJL.js} +1 -1
  89. package/dist-cli/chunks/chunk-UBH2BWCY.js +4 -0
  90. package/dist-cli/chunks/{chunk-AFV32WZT.js → chunk-UXROQPIN.js} +3 -3
  91. package/dist-cli/chunks/{chunk-SA6XVI55.js → chunk-WF2CRNYF.js} +1 -1
  92. package/dist-cli/chunks/chunk-WFAFAGMG.js +4 -0
  93. package/dist-cli/chunks/{chunk-A5FXX5PJ.js → chunk-WJQB6AXZ.js} +1 -1
  94. package/dist-cli/chunks/{chunk-5NMCF5VF.js → chunk-WVVAYLWZ.js} +1 -1
  95. package/dist-cli/chunks/chunk-WXRDFUR7.js +4 -0
  96. package/dist-cli/chunks/{chunk-M3BNCF7E.js → chunk-X5MXQB3L.js} +2 -2
  97. package/dist-cli/chunks/{chunk-LN3RNW6X.js → chunk-YSHJ3LMQ.js} +2 -2
  98. package/dist-cli/chunks/{chunk-34IQM22N.js → chunk-YYPTSF6W.js} +15 -15
  99. package/dist-cli/chunks/{cleanup-KRIATKFH.js → cleanup-WHGLBAJ2.js} +2 -2
  100. package/dist-cli/chunks/cli-version-DMXIXBIM.js +4 -0
  101. package/dist-cli/chunks/{compat-6PTQ3QEA.js → compat-MGBKGYGQ.js} +2 -2
  102. package/dist-cli/chunks/{config-ETGDUANO.js → config-TRDOMP4T.js} +2 -2
  103. package/dist-cli/chunks/connect-N4FV3DDV.js +4 -0
  104. package/dist-cli/chunks/control-5GRBZVYK.js +4 -0
  105. package/dist-cli/chunks/daemon-MBT5E3ZC.js +4 -0
  106. package/dist-cli/chunks/{debug-WIIB6ZKC.js → debug-AHEK43AJ.js} +3 -3
  107. package/dist-cli/chunks/{desktop-TD7Q5LNB.js → desktop-LDRIXOVV.js} +3 -3
  108. package/dist-cli/chunks/{detox-7GNR33VM.js → detox-6HAKYISO.js} +2 -2
  109. package/dist-cli/chunks/{device-QOR3PGUF.js → device-QM3S7OVU.js} +2 -2
  110. package/dist-cli/chunks/{diagnose-RRCBLP6E.js → diagnose-WDP3CVIX.js} +2 -2
  111. package/dist-cli/chunks/{disk-cleanup-MJUGJPXT.js → disk-cleanup-JRT5MKFW.js} +2 -2
  112. package/dist-cli/chunks/do-chain-7FDMO56C.js +5 -0
  113. package/dist-cli/chunks/drivers-AJL2EVXH.js +4 -0
  114. package/dist-cli/chunks/env-YIBOMB24.js +4 -0
  115. package/dist-cli/chunks/{film-D7Y64LID.js → film-R3EQADMG.js} +3 -3
  116. package/dist-cli/chunks/flow-BDAA6BWL.js +4 -0
  117. package/dist-cli/chunks/help-5EJD6LJF.js +4 -0
  118. package/dist-cli/chunks/{hidden-runtime-alias-JGURVR2Z.js → hidden-runtime-alias-6M4UERRQ.js} +2 -2
  119. package/dist-cli/chunks/{home-paths-4ICFIGXO.js → home-paths-QJNOHZJO.js} +2 -2
  120. package/dist-cli/chunks/inspect-S3LR3R2A.js +4 -0
  121. package/dist-cli/chunks/install-desktop-GXE6K7BJ.js +4 -0
  122. package/dist-cli/chunks/{login-RU33QR2G.js → login-ISWDW4WU.js} +2 -2
  123. package/dist-cli/chunks/{logout-2IUG3Y5J.js → logout-VNJX2IEW.js} +2 -2
  124. package/dist-cli/chunks/{maestro-2MXTGG7I.js → maestro-SQS2OS2D.js} +3 -3
  125. package/dist-cli/chunks/{maestro-generate-2PYIFGLR.js → maestro-generate-GWVPISG5.js} +5 -5
  126. package/dist-cli/chunks/{mode-3DAPG3HF.js → mode-3URDGBZB.js} +2 -2
  127. package/dist-cli/chunks/{optional-demo-registry-BWSDJF2Z.js → optional-demo-registry-L3LRBQHG.js} +2 -2
  128. package/dist-cli/chunks/{perf-2TKPWB4N.js → perf-XLPC4UKR.js} +2 -2
  129. package/dist-cli/chunks/{permissions-5CSMHYNA.js → permissions-QRHWVMDP.js} +2 -2
  130. package/dist-cli/chunks/platform-F47URZNB.js +6 -0
  131. package/dist-cli/chunks/privacy-BF23T67T.js +4 -0
  132. package/dist-cli/chunks/{record-I2HJCFP3.js → record-2DN3OUE5.js} +3 -3
  133. package/dist-cli/chunks/{report-issue-A4FZWBOC.js → report-issue-D3BQPSLU.js} +2 -2
  134. package/dist-cli/chunks/reset-T5CNZ7D3.js +4 -0
  135. package/dist-cli/chunks/resolve-target-KAAS274S.js +4 -0
  136. package/dist-cli/chunks/runtime-AQJDBR3X.js +4 -0
  137. package/dist-cli/chunks/{screenshot-command-UFW5BFHP.js → screenshot-command-RK7IJHVK.js} +3 -3
  138. package/dist-cli/chunks/{screenshot-layers-5BIAL4YN.js → screenshot-layers-DP2QKSMN.js} +2 -2
  139. package/dist-cli/chunks/{screenshots-capture-EHYS3BV2.js → screenshots-capture-MA7P3FUT.js} +2 -2
  140. package/dist-cli/chunks/{serve-O4FDM3AC.js → serve-UBXNGLNP.js} +3 -3
  141. package/dist-cli/chunks/setup-GH7OW4GC.js +4 -0
  142. package/dist-cli/chunks/sim-client-VTKFEDJX.js +4 -0
  143. package/dist-cli/chunks/{skills-ZK55SY32.js → skills-HPK25R4B.js} +2 -2
  144. package/dist-cli/chunks/state-273XJPUE.js +14 -0
  145. package/dist-cli/chunks/{storage-WP33ISEM.js → storage-RTDFWIY7.js} +2 -2
  146. package/dist-cli/chunks/store-DLKNNM4L.js +4 -0
  147. package/dist-cli/chunks/telemetry-7CWDXEOO.js +4 -0
  148. package/dist-cli/chunks/{timeline-DKGW5S4I.js → timeline-IF2PGK7C.js} +2 -2
  149. package/dist-cli/chunks/{upgrade-LL7G4B7G.js → upgrade-R74NVHKG.js} +2 -2
  150. package/dist-cli/chunks/upload-3JFFVCZ4.js +4 -0
  151. package/dist-cli/chunks/{version-YLWR5G4F.js → version-RTFCYQZ7.js} +2 -2
  152. package/dist-cli/chunks/{web-QELZ4CP6.js → web-35SSKBRU.js} +2 -2
  153. package/dist-cli/chunks/welcome-N5V352SE.js +5 -0
  154. package/dist-cli/chunks/{what-happened-MIOMZWM5.js → what-happened-WXTP6EPR.js} +2 -2
  155. package/dist-cli/chunks/ws-bridge-JXQUAWFA.js +4 -0
  156. package/dist-lib/agent-daemon-client.cjs +1 -1
  157. package/dist-lib/agent-events.cjs +1 -1
  158. package/dist-lib/agent-identity.cjs +1 -1
  159. package/dist-lib/agent-sessions.cjs +1 -1
  160. package/dist-lib/attached-projects.cjs +1 -1
  161. package/dist-lib/auth/shared-session.cjs +1 -1
  162. package/dist-lib/backend-origin.cjs +1 -1
  163. package/dist-lib/beta.cjs +1 -1
  164. package/dist-lib/beta.mjs +1 -1
  165. package/dist-lib/bridge-constants.cjs +1 -1
  166. package/dist-lib/bridge-contract-input.cjs +336 -0
  167. package/dist-lib/bridge-contract-input.mjs +303 -0
  168. package/dist-lib/bridge-contract.cjs +29 -1
  169. package/dist-lib/bridge-contract.mjs +19 -0
  170. package/dist-lib/cli-constants.cjs +1 -1
  171. package/dist-lib/config.cjs +1 -1
  172. package/dist-lib/detox/index.cjs +32 -18
  173. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  174. package/dist-lib/home-paths.cjs +1 -1
  175. package/dist-lib/host/bridge-host.cjs +5 -1
  176. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  177. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  178. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  179. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  180. package/dist-lib/host/websocket-proxy.cjs +1 -1
  181. package/dist-lib/index.cjs +214 -594
  182. package/dist-lib/jump-to-source-babel.cjs +1 -1
  183. package/dist-lib/menu.cjs +1 -1
  184. package/dist-lib/menu.mjs +1 -1
  185. package/dist-lib/metro-production-bundle.cjs +1 -1
  186. package/dist-lib/metro-production-bundle.mjs +1 -1
  187. package/dist-lib/metro.cjs +1 -1
  188. package/dist-lib/profiles.cjs +1 -1
  189. package/dist-lib/public-brand.cjs +1 -1
  190. package/dist-lib/react-native-host-modules.cjs +1 -1
  191. package/dist-lib/react-native-host-modules.mjs +1 -1
  192. package/dist-lib/render-mode.cjs +1 -1
  193. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  194. package/dist-lib/sdk.cjs +273 -657
  195. package/dist-lib/sdk.mjs +271 -652
  196. package/dist-lib/skills.cjs +190 -287
  197. package/dist-lib/vite.cjs +1 -1
  198. package/package.json +9 -6
  199. package/skills/contrast/SKILL.md +3 -0
  200. package/skills/rnx-debug/SKILL.md +8 -0
  201. package/skills/rnx-test/SKILL.md +21 -0
  202. package/src/bridge-contract-input.ts +359 -0
  203. package/src/bridge-contract.ts +121 -1
  204. package/src/host/bridge-host.ts +7 -12
  205. package/src/index.ts +7 -0
  206. package/src/sim-client.ts +12 -3
  207. package/dist-cli/chunks/chunk-3X6IBUBX.js +0 -38
  208. package/dist-cli/chunks/chunk-7B37IZBN.js +0 -15
  209. package/dist-cli/chunks/chunk-DMPF2D24.js +0 -4
  210. package/dist-cli/chunks/chunk-DW37EVX5.js +0 -767
  211. package/dist-cli/chunks/chunk-DWFIH7CB.js +0 -32
  212. package/dist-cli/chunks/chunk-GUI3PDH2.js +0 -27
  213. package/dist-cli/chunks/chunk-KLF3K6ZZ.js +0 -4
  214. package/dist-cli/chunks/chunk-M5QCGLXR.js +0 -944
  215. package/dist-cli/chunks/chunk-M6SKQGAN.js +0 -5
  216. package/dist-cli/chunks/chunk-NAYLPOC5.js +0 -24
  217. package/dist-cli/chunks/chunk-VIND4SP3.js +0 -124
  218. package/dist-cli/chunks/chunk-XXNLYR64.js +0 -5
  219. package/dist-cli/chunks/chunk-ZFM24DOX.js +0 -4
  220. package/dist-cli/chunks/cli-version-SYGNHGFR.js +0 -4
  221. package/dist-cli/chunks/control-RBTNOLQM.js +0 -4
  222. package/dist-cli/chunks/daemon-N3SC5DK6.js +0 -4
  223. package/dist-cli/chunks/drivers-W6E3BZH4.js +0 -4
  224. package/dist-cli/chunks/flow-7QAE2V5Y.js +0 -4
  225. package/dist-cli/chunks/help-NLD6KWVG.js +0 -4
  226. package/dist-cli/chunks/inspect-C347BG52.js +0 -4
  227. package/dist-cli/chunks/install-desktop-YR6TWZYK.js +0 -4
  228. package/dist-cli/chunks/privacy-PWWC2BXO.js +0 -4
  229. package/dist-cli/chunks/reset-Y3PBPFYA.js +0 -4
  230. package/dist-cli/chunks/runtime-IC6NVVGA.js +0 -4
  231. package/dist-cli/chunks/setup-Y4QGSJBM.js +0 -4
  232. package/dist-cli/chunks/state-KFLAZNEY.js +0 -14
  233. package/dist-cli/chunks/store-VEE22CAY.js +0 -4
  234. package/dist-cli/chunks/telemetry-PZV4BISJ.js +0 -4
  235. package/dist-cli/chunks/upload-Y4C3YZDK.js +0 -4
  236. package/dist-cli/chunks/welcome-ZPKA5PZJ.js +0 -5
  237. package/dist-lib/mcp.cjs +0 -673
  238. package/dist-lib/mcp.mjs +0 -641
  239. package/src/mcp.ts +0 -791
package/README.md CHANGED
@@ -23,7 +23,8 @@ What this package gives you:
23
23
  editor
24
24
  - `rnxsim/detox` — a drop-in Detox driver + jest preset
25
25
  - `rnxsim/sdk` — the inspect/interact verbs as a programmatic API
26
- - `rnxsim/mcp` — a transport-neutral, injected-sim MCP tool registry
26
+ - `rnxsim/bridge-contract-input` — parsers turning untrusted JSON into
27
+ bridge-contract arguments
27
28
  - `rnxsim/skills` — a registry of Contrast-flavored automation skills
28
29
 
29
30
  Platform status: iOS is the parity baseline. Android is bootstrapped and usable
@@ -189,7 +190,7 @@ bridge always addresses the **shell worker**, never an undifferentiated sim.
189
190
  | rnx CLI (bin + dispatcher) | Short-lived terminal entry point. Parses argv, owns the private-by-default one-time choice, lazy-imports one command per run, and routes direct open/runtime/inspection flows. | `cli/bin.ts`, `cli/privacy.ts`, `cli/parse-args.ts`, `cli/help.ts`, `cli/commands/setup.ts`, `cli/commands/control.ts` |
190
191
  | WsBridge client | Client side of the CLI→daemon link. Resolves the daemon port from the lockfile, opens a WS to `:7668`, sends JSON commands and awaits `{id,result}`/`{id,error}`. Used by every interactive command. | `cli/ws-bridge.ts`, `src/bridge-constants.ts`, `cli/current-sim.ts` |
191
192
  | inspect / SDK command surface | Runtime inspection + interaction verbs (describe, find, do tap, get errors, wait, layout, settle). Re-exported as `rnxsim/sdk` so programmatic callers drive a sim the same way the CLI does. | `cli/commands/inspect/core.ts`, `cli/commands/inspect/actions.ts`, `cli/commands/inspect.ts`, `src/sdk.ts` |
192
- | per-sim MCP adapter | Strict JSON-schema tools over one injected `SimClient`. The schemas expose no sim selector, and the adapter owns no transport or server lifecycle. | `src/mcp.ts`, `src/sim-client.ts` |
193
+ | bridge-contract input parsers | Turn untrusted JSON into typed bridge-contract arguments, so a transport in front of a sim validates request bodies once instead of writing a second reader. | `src/bridge-contract-input.ts`, `src/bridge-contract.ts` |
193
194
  | Maestro / Detox runners | Drop-in test compatibility. `bridge-flow-runner.ts` runs Maestro YAML and recorded flows over the bridge; `detox/` provides a `by`/`element`/`expect`/`device` driver + jest preset so existing Detox suites run unchanged. | `cli/bridge-flow-runner.ts`, `cli/commands/maestro.ts`, `cli/commands/detox.ts`, `detox/index.ts`, `detox/jest-preset.cjs` |
194
195
  | sim drivers | Launch or attach the actual sim page. Playwright owns isolated browser profiles; Electron owns the native desktop surface. The current sim is reused unless `--new`, `--profile`, or `--ephemeral` requests a separate one. | `cli/drivers/index.ts`, `cli/drivers/registry.ts`, `cli/drivers/electron.ts`, `cli/drivers/playwright.ts` |
195
196
  | SootSimBridgeHost (serve/daemon) | The bridge process (`rnx serve`, or autostarted as the daemon via launchd/systemd). HTTP+WS hub on `:7668`: relays commands between CLI clients and registered sims; also serves runtime HTTP, `/__server-scan`, and fetch/WS proxies. | `src/host/bridge-host.ts`, `cli/commands/serve.ts`, `cli/commands/daemon.ts`, `src/home-paths.ts` |
@@ -306,6 +307,7 @@ rnx find --testid cta # inspect one node deeply
306
307
  rnx debug enable animated,layout
307
308
  rnx debug trace shell on 240
308
309
  rnx do tap-text "..." # interact
310
+ rnx do tap email --then type user@example.com --then tap submit
309
311
  rnx debug snapshot before
310
312
  rnx debug snapshot after
311
313
  rnx debug diff before after
@@ -323,6 +325,16 @@ connected. With multiple driveable sims, the bridge lists their ids and refuses
323
325
  to choose. Run `rnx use <id>` to pin later commands, or pass `--sim <id>` to
324
326
  target one command.
325
327
 
328
+ Use an inline `rnx do` chain for a short action sequence that should execute as
329
+ one engine batch. Repeat `--then` before each additional action. Each phrase
330
+ uses the same positional grammar as its standalone `do` command. Touch timing
331
+ stays inside the sim, the batch stops at the first failure, and `--json` prints
332
+ the complete per-step result. Reusable file-based flows stay in Maestro YAML.
333
+
334
+ ```sh
335
+ rnx do tap email --then type user@example.com --then tap submit
336
+ ```
337
+
326
338
  ## Bundler plugins (`rnxsim/vite` and `rnxsim/metro`)
327
339
 
328
340
  The Vite plugin serves the installed RNX runtime from your existing dev server.
package/cli/bin.ts CHANGED
@@ -318,6 +318,12 @@ if (
318
318
  exitIfStandaloneBlocked(command)
319
319
 
320
320
  if (TOP_LEVEL_RUNTIME_COMMANDS.has(command)) {
321
+ if (command === 'do') {
322
+ const { hasDoChain, runDoChain } = await import('./commands/do-chain')
323
+ if (hasDoChain(commandArgsWithSim)) {
324
+ await exitWithFlush(await runDoChain(commandArgsWithSim, { port }))
325
+ }
326
+ }
321
327
  const { runInspect } = await import('./commands/inspect')
322
328
  await runInspect([command, ...commandArgsWithSim], { port, verbose })
323
329
  } else {
@@ -434,6 +440,18 @@ if (TOP_LEVEL_RUNTIME_COMMANDS.has(command)) {
434
440
  break
435
441
  }
436
442
 
443
+ case 'ios':
444
+ case 'android': {
445
+ const { runPlatformCommand } = await import('./commands/platform')
446
+ const code = await runPlatformCommand(command, commandArgsWithSim, { port })
447
+ await exitWithFlush(typeof code === 'number' ? code : 0)
448
+ }
449
+
450
+ case 'box': {
451
+ const { runBox } = await import('./commands/box')
452
+ await exitWithFlush(await runBox(commandArgsWithSim, { port, verbose }))
453
+ }
454
+
437
455
  case 'use': {
438
456
  const { runUseCommand } = await import('./commands/control')
439
457
  await runUseCommand(commandArgsWithSim, { port })
@@ -7,14 +7,7 @@ import {
7
7
  framesIdentical,
8
8
  } from '../detox/proof-frame.cjs'
9
9
  import { composeFramedScreenshot } from '../src/screenshots/frame-compose'
10
- import {
11
- READY_NODE_STABLE_MS,
12
- READY_PROBE_POLL_MS,
13
- WAIT_READY_PROBE,
14
- readyProbeHasContent,
15
- readyProbeHasTargetContent,
16
- type ReadyProbe,
17
- } from './commands/inspect/core'
10
+ import { inspectWaitReady, waitReadyReason } from './commands/inspect/core'
18
11
  import { waitForSootsimIdle } from './commands/inspect/settling'
19
12
  import { callShellCommandWhenReady, getShellState } from './commands/inspect/shared'
20
13
  import { FlowLiveStatusReporter } from './flow-live-status'
@@ -424,7 +417,6 @@ export class SootSimBridgeFlowRunner {
424
417
  private lastRecordingDurationMs: number | null = null
425
418
  private lastRecordingFrameStats: unknown = null
426
419
  private flowTraceSteps: SootSimFlowTraceStep[] = []
427
- private simRouteHint: string | null = null
428
420
  // set by runStep when a step throws — lets the outer flow command
429
421
  // dump a rich per-step failure bundle (screenshot + describe.json +
430
422
  // console + network) keyed by the failing step.
@@ -432,8 +424,8 @@ export class SootSimBridgeFlowRunner {
432
424
 
433
425
  constructor(
434
426
  readonly bridge: WsBridge,
435
- // simId rotates when the bridge picks a different open sim mid-flow;
436
- // the rest of opts is stable.
427
+ // simId identifies the browser tab for the whole flow, including reloads;
428
+ // the rest of opts is stable too.
437
429
  private readonly opts: {
438
430
  screenshotDir: string
439
431
  flowDir: string
@@ -473,58 +465,6 @@ export class SootSimBridgeFlowRunner {
473
465
  return this.opts.simId
474
466
  }
475
467
 
476
- setSimRouteHint(urlLike: string | null | undefined) {
477
- this.simRouteHint = this.normalizeSimRoute(urlLike)
478
- }
479
-
480
- private normalizeSimRoute(urlLike: string | null | undefined): string | null {
481
- if (!urlLike) return null
482
- try {
483
- const url = new URL(urlLike)
484
- url.searchParams.delete('inspectOpen')
485
- const search = url.searchParams.toString()
486
- return `${url.origin}${url.pathname}${search ? `?${search}` : ''}`
487
- } catch {
488
- return urlLike
489
- }
490
- }
491
-
492
- // sim ids rotate on `window.location.reload()` when the shell reconnects.
493
- // after launchApp we re-resolve the primary sim so the rest of the flow
494
- // doesn't send commands to a dead id.
495
- private async refreshSimId(): Promise<void> {
496
- if (!this.opts.simId) return
497
- try {
498
- const sims = await this.bridge.listSims()
499
- const current = sims.find((b) => b.id === this.opts.simId)
500
- if (current) {
501
- this.simRouteHint = this.normalizeSimRoute(current.url || current.origin)
502
- }
503
- const stillThere = sims.find(
504
- (b) => b.id === this.opts.simId && b.readyState === 'open',
505
- )
506
- if (stillThere) return
507
- if (!this.simRouteHint) return
508
- const hinted =
509
- this.simRouteHint &&
510
- sims.find(
511
- (b) =>
512
- b.readyState === 'open' &&
513
- this.normalizeSimRoute(b.url || b.origin) === this.simRouteHint,
514
- )
515
- if (!hinted) return
516
- const primary = sims.find((b) => b.isPrimary && b.readyState === 'open')
517
- const firstOpen = sims.find((b) => b.readyState === 'open')
518
- const next = hinted ?? primary ?? firstOpen
519
- if (next) {
520
- this.opts.simId = next.id
521
- console.log(` [flow] sim rotated to ${next.id}`)
522
- }
523
- } catch {
524
- // bridge unreachable — let the next command surface the real error
525
- }
526
- }
527
-
528
468
  private async evaluate<T = unknown>(code: string, timeoutMs?: number): Promise<T> {
529
469
  return this.bridge.send(
530
470
  {
@@ -556,90 +496,19 @@ export class SootSimBridgeFlowRunner {
556
496
  }
557
497
 
558
498
  async waitForTree(timeout: number = DEFAULT_TIMEOUT) {
559
- const deadline = Date.now() + scaledTimeout(timeout)
560
- const startedAt = Date.now()
561
- let lastError: Error | null = null
562
- // a not-ready app still answers `evaluate` (the probe body returns
563
- // `{count, loading}`); a sim that can't even run the probe — bridge
564
- // disconnect, command timeout — is a stale orphan or a dead tab. count
565
- // those distinctly so we can fail fast instead of polling the full
566
- // (up to 120s) timeout with no output, which reads as a dead hang (B1).
567
- let consecutiveBridgeFailures = 0
568
- let lastProgressAt = 0
569
- // readiness mirrors `sootsim wait ready` (shared WAIT_READY_PROBE + the
570
- // readyProbeHasContent decision). the previous inline probe used
571
- // findByText('loading'/'capturing'/'opening app'), which substring-matched
572
- // ordinary forum copy ("...still loading the card...") and reported a
573
- // fully-painted screen as perpetually loading. WAIT_READY_PROBE only treats
574
- // an exact, isolated loading label as "loading" and prefers the guest app's
575
- // own ready signal, so a content-rich screen no longer false-positives.
576
- let lastNodes = -1
577
- let nodeStableSince = startedAt
578
- while (Date.now() < deadline) {
579
- try {
580
- const probe = await this.evaluate<ReadyProbe | null>(
581
- WAIT_READY_PROBE,
582
- // the readiness probe is cheap — cap it well under the bridge-wide
583
- // command timeout so a non-responsive sim is detected in seconds,
584
- // not after a full 15s-per-poll stall.
585
- 8000,
586
- )
587
- consecutiveBridgeFailures = 0
588
- if (probe?.externalError) {
589
- throw new Error(`app failed to load: ${probe.externalError}`)
590
- }
591
- if (probe) {
592
- const now = Date.now()
593
- if (probe.nodes !== lastNodes) {
594
- lastNodes = probe.nodes
595
- nodeStableSince = now
596
- }
597
- const hasReadySignal = probe.flag === true && readyProbeHasContent(probe)
598
- const hasRenderedContentWithoutReadySignal =
599
- probe.flag !== true && readyProbeHasTargetContent(probe)
600
- const ready =
601
- (hasReadySignal || hasRenderedContentWithoutReadySignal) &&
602
- probe.externalReady !== false &&
603
- !probe.externalError &&
604
- !probe.loadingText &&
605
- now - nodeStableSince >= READY_NODE_STABLE_MS
606
- if (ready) return
607
- }
608
- } catch (err) {
609
- lastError = err instanceof Error ? err : new Error(String(err))
610
- // a real load error reported by the probe body — not a bridge
611
- // failure. surface it immediately rather than retrying.
612
- if (lastError.message.startsWith('app failed to load:')) {
613
- throw lastError
614
- }
615
- consecutiveBridgeFailures++
616
- if (consecutiveBridgeFailures >= 4) {
617
- throw new Error(
618
- `target sim is not responding to the bridge (${lastError.message}). ` +
619
- `it is likely a stale orphan — run \`rnx list\`, then close it ` +
620
- `with \`rnx close <id>\` or start a fresh one with \`rnx open --new <port>\`.`,
621
- )
622
- }
623
- await this.refreshSimId()
624
- }
625
- // surface progress every ~5s so a slow-loading app or a sim mid-boot
626
- // does not look like a dead hang while waitForTree polls (B1).
627
- const elapsed = Date.now() - startedAt
628
- if (elapsed - lastProgressAt >= 5000) {
629
- lastProgressAt = elapsed
630
- console.log(
631
- ` [flow] waiting for app to be ready… (${Math.round(elapsed / 1000)}s)`,
632
- )
633
- }
634
- // same cadence as inspectWaitReady, which runs this identical readiness
635
- // decision for `sootsim wait ready`. polling slower here only delayed
636
- // every launchApp by up to a poll interval.
637
- await sleep(READY_PROBE_POLL_MS)
499
+ const status = await inspectWaitReady(this.bridge, scaledTimeout(timeout))
500
+ if (status.ready) return
501
+ if (status.externalError) {
502
+ throw new Error(`app failed to load: ${status.externalError}`)
638
503
  }
639
- if (lastError?.message.startsWith('app failed to load:')) {
640
- throw lastError
504
+ if (status.bridgeError) {
505
+ throw new Error(
506
+ `target sim is not responding to the bridge (${status.bridgeError}). ` +
507
+ `it is likely a stale orphan; run \`rnx list\`, then close it ` +
508
+ `with \`rnx close <id>\` or start a fresh one with \`rnx open --new <port>\`.`,
509
+ )
641
510
  }
642
- throw new Error(`app not ready after ${timeout}ms`)
511
+ throw new Error(`app not ready after ${timeout}ms: ${waitReadyReason(status)}`)
643
512
  }
644
513
 
645
514
  private async ensureRecordingStarted() {
@@ -1966,10 +1835,6 @@ export class SootSimBridgeFlowRunner {
1966
1835
  private async reloadGuestApp() {
1967
1836
  // hard page reload resets the tenant worker.
1968
1837
  await this.evaluate('window.location.reload()').catch(() => {})
1969
- // the sim id rotates on a page reload; pick up the new primary before any
1970
- // further command, or waitForTree loops against a dead id.
1971
- await sleep(500)
1972
- await this.refreshSimId()
1973
1838
  }
1974
1839
 
1975
1840
  private async resetGuestAppData(
@@ -2014,7 +1879,6 @@ export class SootSimBridgeFlowRunner {
2014
1879
  const deadline = Date.now() + 10000
2015
1880
  let lastError: unknown = null
2016
1881
  while (Date.now() < deadline) {
2017
- await this.refreshSimId()
2018
1882
  try {
2019
1883
  if (this.recordingEnabled) await this.ensureRecordingStarted()
2020
1884
  if (this.profilingEnabled) await this.startProfile()
@@ -2032,7 +1896,6 @@ export class SootSimBridgeFlowRunner {
2032
1896
  }
2033
1897
 
2034
1898
  async launchApp(opts: any) {
2035
- await this.refreshSimId()
2036
1899
  let hasLaunchArguments = false
2037
1900
  let launchArguments: SootSimLaunchArguments | undefined
2038
1901
  if (isObjectRecord(opts) && Object.prototype.hasOwnProperty.call(opts, 'arguments')) {
@@ -536,6 +536,6 @@ examples:
536
536
  rnx assert describe --contains "swap-form-header"
537
537
  rnx assert get errors --count 0
538
538
  rnx assert find --testid submit --exists --within 3000
539
- rnx assert describe --jq '.shell.state == "app"'
539
+ rnx assert describe --jq '.tree | contains("swap-form-header")'
540
540
  `)
541
541
  }
@@ -0,0 +1,126 @@
1
+ // ProjectFilePlane over a real local checkout — the third provider behind the
2
+ // seam Contrast's browser host and its server host already implement. writes
3
+ // land on the user's actual disk, so this plane refuses anything it cannot
4
+ // represent faithfully rather than round-tripping it through a string.
5
+ //
6
+ // the file set comes from `git ls-files -co --exclude-standard`, which is the
7
+ // project's own definition of what belongs to it. a hardcoded skip list was
8
+ // measured against this repo and is not viable: it walked 230,229 files
9
+ // (16.5 GB) in 4.9 s, where git returns the same project as 14,984 paths in
10
+ // 67 ms. `index()` is a synchronous whole-project map, so the size of that set
11
+ // is the whole cost model.
12
+
13
+ import { execFile } from 'node:child_process'
14
+ import fs from 'node:fs'
15
+ import path from 'node:path'
16
+ import { promisify } from 'node:util'
17
+ import type { ProjectFileMeta, ProjectFilePlane } from '@contrast/harness'
18
+
19
+ const execFileAsync = promisify(execFile)
20
+
21
+ export class CheckoutRequiresGitError extends Error {}
22
+
23
+ // a NUL byte is the usual "this is not text" test. the plane serves strings,
24
+ // so a binary file cannot survive a read/write round trip. reading one gives
25
+ // the same garbage real `cat` gives, which is fine; WRITING one back would
26
+ // corrupt a file on the user's actual disk, so binary files are published as
27
+ // read-only and the write path refuses them by name. detecting this at read
28
+ // time is not an option: just-bash's `cat` discards every readFile error and
29
+ // reports "No such file or directory" whatever the error says, so the refusal
30
+ // has to live in the index where the filesystem layer can act on it.
31
+ const BINARY_SNIFF_BYTES = 8192
32
+
33
+ function looksBinary(filePath: string, size: number): boolean {
34
+ if (size === 0) return false
35
+ let fd: number | undefined
36
+ try {
37
+ fd = fs.openSync(filePath, 'r')
38
+ const buffer = Buffer.alloc(Math.min(BINARY_SNIFF_BYTES, size))
39
+ const read = fs.readSync(fd, buffer, 0, buffer.length, 0)
40
+ return buffer.subarray(0, read).includes(0)
41
+ } catch {
42
+ return false
43
+ } finally {
44
+ if (fd !== undefined) fs.closeSync(fd)
45
+ }
46
+ }
47
+
48
+ export class CheckoutFilePlane implements ProjectFilePlane {
49
+ readonly root: string
50
+ private entries = new Map<string, ProjectFileMeta>()
51
+
52
+ constructor(root: string) {
53
+ this.root = path.resolve(root)
54
+ }
55
+
56
+ index(): ReadonlyMap<string, ProjectFileMeta> {
57
+ return this.entries
58
+ }
59
+
60
+ async refresh(): Promise<void> {
61
+ let stdout: string
62
+ try {
63
+ // -z so paths containing spaces or newlines survive; -co --exclude-standard
64
+ // is tracked plus untracked-not-ignored, which is what a person means by
65
+ // "the files in my project".
66
+ const result = await execFileAsync(
67
+ 'git',
68
+ ['ls-files', '-co', '--exclude-standard', '-z'],
69
+ { cwd: this.root, maxBuffer: 64 * 1024 * 1024 },
70
+ )
71
+ stdout = result.stdout
72
+ } catch (error) {
73
+ const message = error instanceof Error ? error.message : String(error)
74
+ throw new CheckoutRequiresGitError(
75
+ `could not list the project's files with git in ${this.root}: ${message}`,
76
+ )
77
+ }
78
+
79
+ const next = new Map<string, ProjectFileMeta>()
80
+ for (const relative of stdout.split('\0')) {
81
+ if (!relative) continue
82
+ let stat: fs.Stats
83
+ try {
84
+ stat = fs.statSync(path.join(this.root, relative))
85
+ } catch {
86
+ // listed but gone (a deleted-but-tracked file); it is simply not there
87
+ continue
88
+ }
89
+ if (!stat.isFile()) continue
90
+ next.set(relative, {
91
+ size: stat.size,
92
+ readOnly: looksBinary(path.join(this.root, relative), stat.size),
93
+ })
94
+ }
95
+ this.entries = next
96
+ }
97
+
98
+ private absolute(planePath: string): string {
99
+ const resolved = path.resolve(this.root, planePath)
100
+ // the shell resolves "." and ".." before calling us, but the plane owns the
101
+ // checkout boundary and must not depend on that.
102
+ if (resolved !== this.root && !resolved.startsWith(`${this.root}${path.sep}`)) {
103
+ throw new Error(`path escapes the project: ${planePath}`)
104
+ }
105
+ return resolved
106
+ }
107
+
108
+ async read(planePath: string): Promise<string> {
109
+ return fs.promises.readFile(this.absolute(planePath), 'utf8')
110
+ }
111
+
112
+ async write(planePath: string, content: string): Promise<void> {
113
+ const absolute = this.absolute(planePath)
114
+ await fs.promises.mkdir(path.dirname(absolute), { recursive: true })
115
+ await fs.promises.writeFile(absolute, content, 'utf8')
116
+ this.entries.set(planePath, {
117
+ size: Buffer.byteLength(content, 'utf8'),
118
+ readOnly: false,
119
+ })
120
+ }
121
+
122
+ async delete(planePath: string): Promise<void> {
123
+ await fs.promises.rm(this.absolute(planePath), { force: true })
124
+ this.entries.delete(planePath)
125
+ }
126
+ }
@@ -0,0 +1,68 @@
1
+ // step 4: RNX operations registered as just-bash commands.
2
+ //
3
+ // `rnx` inside a box shell is the same CLI, re-invoked as a child process with
4
+ // the shell's own cwd. that is deliberate — spelling the verbs a second time
5
+ // in shell-command form would be a second implementation of every command, and
6
+ // the spec is explicit that the CLI and the in-shell commands are clients of
7
+ // one implementation, not two.
8
+
9
+ import { spawn } from 'node:child_process'
10
+ import path from 'node:path'
11
+ import { defineCommand, type CustomCommand } from '@contrast/harness'
12
+ import { PROJECT_MOUNT } from '@contrast/harness'
13
+ import { rnxPublicBrand } from '../../../src/public-brand'
14
+ import { RNX_PLACEMENT_ENV } from '../../ws-bridge'
15
+
16
+ // the standalone binary is its own entry; the npm package runs through node
17
+ // with the CLI as argv[1]. this is the same shape bin.ts uses to respawn
18
+ // itself for automatic cleanup.
19
+ function selfInvocation(): { command: string; prefix: string[] } {
20
+ const entry = process.argv[1]
21
+ if (!entry) return { command: process.execPath, prefix: [] }
22
+ return { command: process.execPath, prefix: [entry] }
23
+ }
24
+
25
+ // map a shell path under /project back onto the real checkout, so a child
26
+ // process started from `cd src && rnx describe` sees the directory the user
27
+ // is actually standing in.
28
+ function hostCwd(root: string, shellCwd: string): string {
29
+ if (shellCwd === PROJECT_MOUNT) return root
30
+ if (!shellCwd.startsWith(`${PROJECT_MOUNT}/`)) return root
31
+ return path.join(root, shellCwd.slice(PROJECT_MOUNT.length + 1))
32
+ }
33
+
34
+ export function createRnxCommand(options: {
35
+ root: string
36
+ shellCwd: () => string
37
+ }): CustomCommand {
38
+ return defineCommand(rnxPublicBrand.commandName, async (args) => {
39
+ const { command, prefix } = selfInvocation()
40
+ const child = spawn(command, [...prefix, ...args], {
41
+ cwd: hostCwd(options.root, options.shellCwd()),
42
+ // mark the child as running inside a local box. this is the only thing
43
+ // that sets a placement — nothing infers one from cwd — and it is what
44
+ // lets `rnx box` refuse to nest inside itself.
45
+ env: { ...process.env, [RNX_PLACEMENT_ENV]: 'local-box' },
46
+ stdio: ['ignore', 'pipe', 'pipe'],
47
+ })
48
+
49
+ let stdout = ''
50
+ let stderr = ''
51
+ child.stdout.on('data', (chunk) => {
52
+ stdout += String(chunk)
53
+ })
54
+ child.stderr.on('data', (chunk) => {
55
+ stderr += String(chunk)
56
+ })
57
+
58
+ const exitCode = await new Promise<number>((resolve) => {
59
+ child.once('error', (error) => {
60
+ stderr += `${rnxPublicBrand.commandName}: ${error.message}\n`
61
+ resolve(1)
62
+ })
63
+ child.once('close', (code) => resolve(code ?? 0))
64
+ })
65
+
66
+ return { stdout, stderr, exitCode }
67
+ })
68
+ }