rnxsim 0.1.366 → 0.1.367

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-EKKIAKP5.js} +2 -2
  31. package/dist-cli/chunks/{agent-wrapper-PMYCB2KO.js → agent-wrapper-MHLQUGA6.js} +2 -2
  32. package/dist-cli/chunks/{app-fonts-LULQGFY7.js → app-fonts-HN6ADRTQ.js} +2 -2
  33. package/dist-cli/chunks/{assert-WGNCD4ZU.js → assert-IMR6QCN6.js} +3 -3
  34. package/dist-cli/chunks/{auth-G7CCHH3M.js → auth-27LPMQNH.js} +2 -2
  35. package/dist-cli/chunks/{beta-N4LXL5TO.js → beta-6RXBB5AI.js} +2 -2
  36. package/dist-cli/chunks/box-DORLF3GP.js +1709 -0
  37. package/dist-cli/chunks/{camera-4JHCYBQS.js → camera-2G7JZ5DL.js} +2 -2
  38. package/dist-cli/chunks/{chunk-2UXU53MG.js → chunk-3QZJLNYY.js} +1 -1
  39. package/dist-cli/chunks/{chunk-YXXKSL6R.js → chunk-3VBVEDDA.js} +1 -1
  40. package/dist-cli/chunks/{chunk-ZJIS3CTW.js → chunk-4BWGLTKU.js} +2 -2
  41. package/dist-cli/chunks/chunk-4XSNO374.js +423 -0
  42. package/dist-cli/chunks/{chunk-RYIMSKDO.js → chunk-5DGMFUZG.js} +2 -2
  43. package/dist-cli/chunks/{chunk-M3BNCF7E.js → chunk-6DBS5EGX.js} +2 -2
  44. package/dist-cli/chunks/{chunk-ONORMKWX.js → chunk-6SO6I3D5.js} +2 -2
  45. package/dist-cli/chunks/chunk-75EFOPXE.js +32 -0
  46. package/dist-cli/chunks/{chunk-5NMCF5VF.js → chunk-A5AJFVOF.js} +1 -1
  47. package/dist-cli/chunks/{chunk-N3UC7HIX.js → chunk-AM2ODYAN.js} +1 -1
  48. package/dist-cli/chunks/{chunk-I6KIM6PN.js → chunk-AVOROGA3.js} +1 -1
  49. package/dist-cli/chunks/chunk-AXMMLO7D.js +130 -0
  50. package/dist-cli/chunks/{chunk-ORWP2E7R.js → chunk-BSEXEYNX.js} +2 -2
  51. package/dist-cli/chunks/{chunk-NQFJOCSF.js → chunk-CM6ZKZW7.js} +2 -2
  52. package/dist-cli/chunks/{chunk-ZMJ75EDG.js → chunk-DNVABCEC.js} +2 -2
  53. package/dist-cli/chunks/{chunk-2S7BKUP2.js → chunk-E33A4AIR.js} +2 -2
  54. package/dist-cli/chunks/{chunk-GXAB52K7.js → chunk-EFMOXYDD.js} +2 -2
  55. package/dist-cli/chunks/{chunk-TIAEYO6I.js → chunk-EV4P5UBP.js} +2 -2
  56. package/dist-cli/chunks/{chunk-JHXETDVC.js → chunk-G5IUIGGC.js} +1 -1
  57. package/dist-cli/chunks/chunk-GMLOCV3E.js +312 -0
  58. package/dist-cli/chunks/{chunk-ZLVRYXZ7.js → chunk-GRN2FHVW.js} +1 -1
  59. package/dist-cli/chunks/{chunk-A5FXX5PJ.js → chunk-HFFXPUEP.js} +1 -1
  60. package/dist-cli/chunks/chunk-HG6T4OOR.js +4 -0
  61. package/dist-cli/chunks/{chunk-OW5XHAA4.js → chunk-HNT2OXPZ.js} +1 -1
  62. package/dist-cli/chunks/chunk-IPEURNLI.js +24 -0
  63. package/dist-cli/chunks/chunk-IR44SFBA.js +5 -0
  64. package/dist-cli/chunks/chunk-JMSLXZFW.js +4 -0
  65. package/dist-cli/chunks/{chunk-EFR5DVNP.js → chunk-JPXQKLNH.js} +1 -1
  66. package/dist-cli/chunks/chunk-K3EC6JDE.js +4 -0
  67. package/dist-cli/chunks/{chunk-YGMEDJKJ.js → chunk-KHX65HAU.js} +2 -2
  68. package/dist-cli/chunks/{chunk-EQK5MJCU.js → chunk-LWOVKHRC.js} +2 -2
  69. package/dist-cli/chunks/{chunk-5OK3LCEJ.js → chunk-MQN5DWTI.js} +2 -2
  70. package/dist-cli/chunks/{chunk-TTASWLKM.js → chunk-N3BXQEUF.js} +1 -1
  71. package/dist-cli/chunks/{chunk-UCHW7ZHR.js → chunk-N5OSV26B.js} +2 -2
  72. package/dist-cli/chunks/chunk-PHNURIH3.js +4 -0
  73. package/dist-cli/chunks/{chunk-AFV32WZT.js → chunk-PP2KNVXP.js} +3 -3
  74. package/dist-cli/chunks/{chunk-XJU2QIRY.js → chunk-PROKNJVY.js} +2 -2
  75. package/dist-cli/chunks/chunk-QXLLY4IM.js +7 -0
  76. package/dist-cli/chunks/{chunk-HT6GFZOZ.js → chunk-RNQTOGM7.js} +1 -1
  77. package/dist-cli/chunks/{chunk-CN6XYYEA.js → chunk-RPNKKVBN.js} +27 -27
  78. package/dist-cli/chunks/{chunk-LN3RNW6X.js → chunk-RSNLI2NH.js} +2 -2
  79. package/dist-cli/chunks/{chunk-YCCCPFWT.js → chunk-RZGHV2WI.js} +2 -2
  80. package/dist-cli/chunks/chunk-SGGL3AFV.js +497 -0
  81. package/dist-cli/chunks/chunk-SH5ETJKG.js +4 -0
  82. package/dist-cli/chunks/{chunk-77ZHQJZR.js → chunk-T6J4YP56.js} +2 -2
  83. package/dist-cli/chunks/{chunk-MGDSJZ37.js → chunk-TWUE7WF5.js} +2 -2
  84. package/dist-cli/chunks/{chunk-SA6XVI55.js → chunk-U44VEQRN.js} +1 -1
  85. package/dist-cli/chunks/{chunk-IZKHOM37.js → chunk-URLVQHC3.js} +1 -1
  86. package/dist-cli/chunks/{chunk-GAJQSITE.js → chunk-VANZFTUE.js} +1 -1
  87. package/dist-cli/chunks/{chunk-EGAGZHO6.js → chunk-VB2YBH46.js} +1 -1
  88. package/dist-cli/chunks/{chunk-H23KWIKA.js → chunk-VFU2SJ7M.js} +2 -2
  89. package/dist-cli/chunks/{chunk-34IQM22N.js → chunk-VUBJ6FXU.js} +15 -15
  90. package/dist-cli/chunks/{chunk-NM2L2QV4.js → chunk-W5PATILQ.js} +2 -2
  91. package/dist-cli/chunks/chunk-WI5V4TYG.js +38 -0
  92. package/dist-cli/chunks/{chunk-6PL2CCGP.js → chunk-XTRHQAOV.js} +2 -2
  93. package/dist-cli/chunks/chunk-XWUTJLBM.js +27 -0
  94. package/dist-cli/chunks/{chunk-5DAIA77C.js → chunk-XYRUQQUR.js} +2 -2
  95. package/dist-cli/chunks/chunk-YBFB3RM7.js +4 -0
  96. package/dist-cli/chunks/chunk-YGQVQ2VG.js +4 -0
  97. package/dist-cli/chunks/{chunk-CTG7HBCA.js → chunk-YINACV32.js} +1 -1
  98. package/dist-cli/chunks/chunk-YV7ERO2V.js +5 -0
  99. package/dist-cli/chunks/{cleanup-KRIATKFH.js → cleanup-GVFHQ3KH.js} +2 -2
  100. package/dist-cli/chunks/cli-version-DPX5MA2M.js +4 -0
  101. package/dist-cli/chunks/{compat-6PTQ3QEA.js → compat-AYRTXYAM.js} +2 -2
  102. package/dist-cli/chunks/{config-ETGDUANO.js → config-K7LOOV62.js} +2 -2
  103. package/dist-cli/chunks/connect-H3CNY4IO.js +4 -0
  104. package/dist-cli/chunks/control-SMHZ2CF5.js +4 -0
  105. package/dist-cli/chunks/daemon-TEZUNI73.js +4 -0
  106. package/dist-cli/chunks/{debug-WIIB6ZKC.js → debug-ELGHG57T.js} +3 -3
  107. package/dist-cli/chunks/{desktop-TD7Q5LNB.js → desktop-4G7FK3MP.js} +3 -3
  108. package/dist-cli/chunks/{detox-7GNR33VM.js → detox-JKXGEJIZ.js} +2 -2
  109. package/dist-cli/chunks/{device-QOR3PGUF.js → device-IV3AR6KG.js} +2 -2
  110. package/dist-cli/chunks/{diagnose-RRCBLP6E.js → diagnose-3ERGF7EW.js} +2 -2
  111. package/dist-cli/chunks/{disk-cleanup-MJUGJPXT.js → disk-cleanup-ALFEVZ4B.js} +2 -2
  112. package/dist-cli/chunks/do-chain-K2B4OHM7.js +5 -0
  113. package/dist-cli/chunks/drivers-KJYXA3M3.js +4 -0
  114. package/dist-cli/chunks/env-2KF6IYZE.js +4 -0
  115. package/dist-cli/chunks/{film-D7Y64LID.js → film-UQXJFAD5.js} +3 -3
  116. package/dist-cli/chunks/flow-ASTUH56E.js +4 -0
  117. package/dist-cli/chunks/help-SNNYFELA.js +4 -0
  118. package/dist-cli/chunks/{hidden-runtime-alias-JGURVR2Z.js → hidden-runtime-alias-VJCWCFVO.js} +2 -2
  119. package/dist-cli/chunks/{home-paths-4ICFIGXO.js → home-paths-EGUZZ6J6.js} +2 -2
  120. package/dist-cli/chunks/inspect-XANAADQT.js +4 -0
  121. package/dist-cli/chunks/install-desktop-JKQA7UY6.js +4 -0
  122. package/dist-cli/chunks/{login-RU33QR2G.js → login-6KQGD7HK.js} +2 -2
  123. package/dist-cli/chunks/{logout-2IUG3Y5J.js → logout-2FCOVLBC.js} +2 -2
  124. package/dist-cli/chunks/{maestro-2MXTGG7I.js → maestro-HPSSEVJD.js} +3 -3
  125. package/dist-cli/chunks/{maestro-generate-2PYIFGLR.js → maestro-generate-KQQ6JHMR.js} +5 -5
  126. package/dist-cli/chunks/{mode-3DAPG3HF.js → mode-SH7KRZET.js} +2 -2
  127. package/dist-cli/chunks/{optional-demo-registry-BWSDJF2Z.js → optional-demo-registry-NAMCAVFX.js} +2 -2
  128. package/dist-cli/chunks/{perf-2TKPWB4N.js → perf-LWKAHXUL.js} +2 -2
  129. package/dist-cli/chunks/{permissions-5CSMHYNA.js → permissions-EZTKWTXV.js} +2 -2
  130. package/dist-cli/chunks/platform-KSOJIVN5.js +6 -0
  131. package/dist-cli/chunks/privacy-EJULWP7L.js +4 -0
  132. package/dist-cli/chunks/{record-I2HJCFP3.js → record-2IC7BAQN.js} +3 -3
  133. package/dist-cli/chunks/{report-issue-A4FZWBOC.js → report-issue-MMX6HOQO.js} +2 -2
  134. package/dist-cli/chunks/reset-PKFZA4NC.js +4 -0
  135. package/dist-cli/chunks/resolve-target-AFKU3BRE.js +4 -0
  136. package/dist-cli/chunks/runtime-XHWWPOIN.js +4 -0
  137. package/dist-cli/chunks/{screenshot-command-UFW5BFHP.js → screenshot-command-JYUXDVSA.js} +3 -3
  138. package/dist-cli/chunks/{screenshot-layers-5BIAL4YN.js → screenshot-layers-OGRY6AA3.js} +2 -2
  139. package/dist-cli/chunks/{screenshots-capture-EHYS3BV2.js → screenshots-capture-QHDN65XS.js} +2 -2
  140. package/dist-cli/chunks/{serve-O4FDM3AC.js → serve-W4BKAG4J.js} +3 -3
  141. package/dist-cli/chunks/setup-WJPMPSAC.js +4 -0
  142. package/dist-cli/chunks/sim-client-UOQGFBPO.js +4 -0
  143. package/dist-cli/chunks/{skills-ZK55SY32.js → skills-OPPXOD63.js} +2 -2
  144. package/dist-cli/chunks/state-TDAN7BOM.js +14 -0
  145. package/dist-cli/chunks/{storage-WP33ISEM.js → storage-CFB2YGLY.js} +2 -2
  146. package/dist-cli/chunks/store-4TEED4ZU.js +4 -0
  147. package/dist-cli/chunks/telemetry-S6N7CV6G.js +4 -0
  148. package/dist-cli/chunks/{timeline-DKGW5S4I.js → timeline-ASOOVP4C.js} +2 -2
  149. package/dist-cli/chunks/{upgrade-LL7G4B7G.js → upgrade-6MVJISHG.js} +2 -2
  150. package/dist-cli/chunks/upload-CBNIRFEW.js +4 -0
  151. package/dist-cli/chunks/{version-YLWR5G4F.js → version-5GS7CWRL.js} +2 -2
  152. package/dist-cli/chunks/{web-QELZ4CP6.js → web-QRRMYMV4.js} +2 -2
  153. package/dist-cli/chunks/welcome-33FXMAMB.js +5 -0
  154. package/dist-cli/chunks/{what-happened-MIOMZWM5.js → what-happened-GAZJGKXX.js} +2 -2
  155. package/dist-cli/chunks/ws-bridge-DIXUAPLQ.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
@@ -0,0 +1,200 @@
1
+ // `rnx box` — the box environment.
2
+ //
3
+ // bare `rnx box` is LOCAL and mounts the current checkout at /project. `rnx box
4
+ // create` provisions a durable cloud box. everything else in the namespace is
5
+ // cloud lifecycle. the cloud side is not built yet; those verbs fail by name
6
+ // and say what works instead, so nothing advertises a capability that always
7
+ // fails.
8
+
9
+ import path from 'node:path'
10
+ import { createInterface } from 'node:readline'
11
+ import { createHarnessShell, HarnessProjectFs, PROJECT_MOUNT } from '@contrast/harness'
12
+ import { rnxPublicBrand } from '../../src/public-brand'
13
+ import { findRepositoryPackageRoot } from '../setup-repository'
14
+ import { resolvePlacement } from '../ws-bridge'
15
+ import { CheckoutFilePlane, CheckoutRequiresGitError } from './box/checkout-plane'
16
+ import { createRnxCommand } from './box/rnx-command'
17
+
18
+ interface BoxCommandOptions {
19
+ port?: number
20
+ verbose?: boolean
21
+ }
22
+
23
+ const CLOUD_SUBCOMMANDS = new Set(['create', 'ls', 'connect', 'stop', 'delete'])
24
+
25
+ function usage(): void {
26
+ const rnx = rnxPublicBrand.commandName
27
+ console.log(`
28
+ ${rnx} box — start a local box and enter its shell
29
+
30
+ usage:
31
+ ${rnx} box start a local box over the current checkout
32
+ ${rnx} box create create a cloud box and enter its shell
33
+ ${rnx} box create --name=<n> create a named cloud box
34
+
35
+ a local box mounts this checkout at ${PROJECT_MOUNT}, gives you a shell with
36
+ ls, cat, find, rg, sed, pipes and redirects, and registers ${rnx} itself as a
37
+ shell command. writes go straight to the checkout — there is no second copy.
38
+ `)
39
+ }
40
+
41
+ export async function runBox(
42
+ args: string[],
43
+ opts: BoxCommandOptions = {},
44
+ ): Promise<number> {
45
+ if (args.includes('--help') || args.includes('-h')) {
46
+ usage()
47
+ return 0
48
+ }
49
+
50
+ // a box inside a box would mount the same checkout twice and give the
51
+ // user two shells writing through one filesystem. refuse at the top rather
52
+ // than letting the second one start and quietly race the first.
53
+ const placement = resolvePlacement()
54
+ if (placement === 'local-box') {
55
+ console.error(
56
+ ` ${rnxPublicBrand.commandName} box: you are already in a box.\n` +
57
+ ` this shell is the box — run ${rnxPublicBrand.commandName} commands directly, or \`exit\` to leave.`,
58
+ )
59
+ return 1
60
+ }
61
+
62
+ const subcommand = args.find((arg) => !arg.startsWith('-'))
63
+ if (subcommand !== undefined) {
64
+ if (CLOUD_SUBCOMMANDS.has(subcommand)) {
65
+ console.error(
66
+ ` ${rnxPublicBrand.commandName} box ${subcommand} needs a cloud box, which is not built yet.\n` +
67
+ ` \`${rnxPublicBrand.commandName} box\` runs the same shell locally over this checkout.`,
68
+ )
69
+ return 1
70
+ }
71
+ console.error(
72
+ ` unknown command: ${rnxPublicBrand.commandName} box ${subcommand}\n` +
73
+ ` run \`${rnxPublicBrand.commandName} box --help\` to see the box surface.`,
74
+ )
75
+ return 1
76
+ }
77
+
78
+ return runLocalBox(opts)
79
+ }
80
+
81
+ async function runLocalBox(opts: BoxCommandOptions): Promise<number> {
82
+ // findRepositoryPackageRoot answers both halves of what a box needs: the
83
+ // nearest package.json, and only when it sits inside a git repository. git
84
+ // is what decides which files belong to the project, so a box without it
85
+ // would have to walk the whole directory tree — measured at 230k files on
86
+ // this repo against 15k from git.
87
+ const root = findRepositoryPackageRoot(process.cwd())
88
+ if (!root) {
89
+ console.error(
90
+ ` ${rnxPublicBrand.commandName} box: no project here.\n` +
91
+ ` the box needs a directory inside a git repository that has a\n` +
92
+ ` package.json. it asks git which files belong to the project, so it\n` +
93
+ ` never walks node_modules or build output. run \`git init\` if this is\n` +
94
+ ` a new project.`,
95
+ )
96
+ return 1
97
+ }
98
+
99
+ const plane = new CheckoutFilePlane(root)
100
+ try {
101
+ await plane.refresh()
102
+ } catch (error) {
103
+ if (error instanceof CheckoutRequiresGitError) {
104
+ console.error(` ${rnxPublicBrand.commandName} box: ${error.message}`)
105
+ return 1
106
+ }
107
+ throw error
108
+ }
109
+
110
+ const shell = createHarnessShell({
111
+ projectFs: new HarnessProjectFs(plane),
112
+ commands: [createRnxCommand({ root, shellCwd: () => shell.cwd })],
113
+ })
114
+
115
+ const relative = path.relative(root, process.cwd())
116
+ console.log(` ...starting local box`)
117
+ console.log(` ...mounting ${root} at ${PROJECT_MOUNT}`)
118
+ console.log(` ...accessing shell\n`)
119
+ if (opts.verbose) {
120
+ console.log(` ${plane.index().size} project files indexed\n`)
121
+ }
122
+
123
+ return repl(shell, plane, relative)
124
+ }
125
+
126
+ async function repl(
127
+ shell: ReturnType<typeof createHarnessShell>,
128
+ plane: CheckoutFilePlane,
129
+ startRelative: string,
130
+ ): Promise<number> {
131
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
132
+
133
+ // enter where the user was standing, so `rnx box` from a subdirectory does
134
+ // not silently teleport them to the project root.
135
+ if (startRelative && !startRelative.startsWith('..')) {
136
+ await shell.exec(`cd ${JSON.stringify(startRelative)}`)
137
+ }
138
+
139
+ const prompt = () => {
140
+ rl.setPrompt(`${shell.cwd} > `)
141
+ rl.prompt()
142
+ }
143
+
144
+ // readline delivers a pasted block, or a piped script, as a burst of `line`
145
+ // events. run them one at a time in order — dropping the ones that arrive
146
+ // while a command is running would silently execute only the first line of
147
+ // a paste.
148
+ let queue: Promise<void> = Promise.resolve()
149
+ // stdin ended: stop printing prompts, but still finish what is already
150
+ // queued — piping a script in must run every line, not just the first.
151
+ let inputEnded = false
152
+ // the user typed `exit`: abandon anything still queued behind it.
153
+ let stopped = false
154
+
155
+ const run = async (command: string) => {
156
+ if (stopped) return
157
+ try {
158
+ // the checkout is shared with editors, agents, and the user's own
159
+ // terminal, so re-read the file set before every command rather than
160
+ // trusting a snapshot taken when the box started.
161
+ await plane.refresh()
162
+ const result = await shell.exec(command)
163
+ if (result.text && result.text !== '(no output)') console.log(result.text)
164
+ } catch (error) {
165
+ console.error(error instanceof Error ? error.message : String(error))
166
+ }
167
+ if (!inputEnded && !stopped) prompt()
168
+ }
169
+
170
+ return new Promise<number>((resolve) => {
171
+ prompt()
172
+
173
+ rl.on('line', (line) => {
174
+ const command = line.trim()
175
+ if (!command) {
176
+ queue = queue.then(() => {
177
+ if (!inputEnded && !stopped) prompt()
178
+ })
179
+ return
180
+ }
181
+ if (command === 'exit' || command === 'quit') {
182
+ queue = queue.then(() => {
183
+ stopped = true
184
+ rl.close()
185
+ })
186
+ return
187
+ }
188
+ queue = queue.then(() => run(command))
189
+ })
190
+
191
+ rl.on('close', () => {
192
+ inputEnded = true
193
+ // let any already-queued command finish before the process exits.
194
+ queue.then(() => {
195
+ console.log()
196
+ resolve(0)
197
+ })
198
+ })
199
+ })
200
+ }
@@ -66,7 +66,7 @@ import {
66
66
  worldForBridgePort,
67
67
  type BridgeSimInfo,
68
68
  } from '../ws-bridge'
69
- import { WAIT_READY_PROBE, readyProbeHasContent, type ReadyProbe } from './inspect/core'
69
+ import { inspectDescribe, inspectWaitReady } from './inspect/core'
70
70
  import { isAgentEnv } from './inspect/env'
71
71
  import type { DriverId, DriverLaunchResult } from '../drivers/types'
72
72
 
@@ -1248,19 +1248,12 @@ async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: strin
1248
1248
  simIdSource: 'flag',
1249
1249
  })
1250
1250
  try {
1251
- let readyProbe: ReadyProbe | null = null
1251
+ let readyProbe: Awaited<ReturnType<typeof inspectWaitReady>> | null = null
1252
1252
  try {
1253
- readyProbe = (await bridge.send({
1254
- type: 'evaluate',
1255
- code: WAIT_READY_PROBE,
1256
- })) as ReadyProbe | null
1253
+ readyProbe = await inspectWaitReady(bridge, 1)
1257
1254
  } catch {}
1258
1255
  if (readyProbe) {
1259
- const readyEnough =
1260
- readyProbe.flag === true &&
1261
- !readyProbe.loadingText &&
1262
- readyProbeHasContent(readyProbe)
1263
- if (!readyEnough) {
1256
+ if (!readyProbe.ready) {
1264
1257
  const reason = readyProbe.loadingText
1265
1258
  ? `still showing "${readyProbe.loadingText}"`
1266
1259
  : readyProbe.flag !== true
@@ -1274,29 +1267,16 @@ async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: strin
1274
1267
  console.log(' before interacting, run: rnx wait ready --max-ms 120000')
1275
1268
  }
1276
1269
  }
1277
- const dumpOpts = { describe: true, verbose: false, filter: '' }
1278
- const code = `(async () => {
1279
- const t = window.__sootsimTest
1280
- const ms = window.SootSim?.bridges?.mainShell
1281
- if (!t) return null
1282
- let shell = null
1283
- try { shell = ms?.getState ? await ms.getState() : null } catch {}
1284
- const tree = await t.dumpTree(12, ${JSON.stringify(dumpOpts)})
1285
- return { tree, shell }
1286
- })()`
1287
- const result = (await bridge.send({ type: 'evaluate', code })) as {
1288
- tree: string | null
1289
- shell: { state?: string; activeApp?: string } | null
1290
- } | null
1270
+ const result = await inspectDescribe(bridge, {
1271
+ describe: true,
1272
+ verbose: false,
1273
+ filter: '',
1274
+ compact: true,
1275
+ hideXy: false,
1276
+ maxDepth: 12,
1277
+ })
1291
1278
  if (!result?.tree) return
1292
1279
  console.log('')
1293
- if (result.shell?.state) {
1294
- const parts = [
1295
- `state=${result.shell.state}`,
1296
- result.shell.activeApp ? `app=${result.shell.activeApp}` : null,
1297
- ].filter(Boolean)
1298
- console.log(` shell: ${parts.join(' ')}`)
1299
- }
1300
1280
  console.log(result.tree)
1301
1281
  } finally {
1302
1282
  bridge.close()
@@ -0,0 +1,412 @@
1
+ import { BridgeInputError } from '../../src/bridge-contract-input'
2
+ import type { SimPerformStep } from '../../src/sim-client'
3
+
4
+ interface TargetedDoubleTapAction {
5
+ type: 'targetedDoubleTap'
6
+ id: string
7
+ gapMs: number
8
+ }
9
+
10
+ interface TargetedLongPressAction {
11
+ type: 'targetedLongPress'
12
+ id: string
13
+ durationMs: number
14
+ }
15
+
16
+ export type DoChainAction =
17
+ | SimPerformStep
18
+ | TargetedDoubleTapAction
19
+ | TargetedLongPressAction
20
+
21
+ const DO_CHAIN_ACTIONS = new Set([
22
+ 'tap',
23
+ 'tap-id',
24
+ 'tap-text',
25
+ 'double-tap',
26
+ 'long-press',
27
+ 'type',
28
+ 'type-into',
29
+ 'key',
30
+ 'key-sequence',
31
+ 'dismiss',
32
+ 'scroll',
33
+ 'drag',
34
+ 'swipe',
35
+ 'pinch',
36
+ 'touch',
37
+ 'sleep',
38
+ ])
39
+
40
+ export function hasDoChain(args: string[]): boolean {
41
+ return args.includes('--then')
42
+ }
43
+
44
+ function invalidAction(index: number, usage: string): never {
45
+ throw new BridgeInputError(
46
+ `do chain action ${index + 1} has invalid arguments; usage: rnx do ${usage}`,
47
+ )
48
+ }
49
+
50
+ function finiteNumber(value: string | undefined, index: number, usage: string): number {
51
+ const parsed = Number(value)
52
+ if (value === undefined || !Number.isFinite(parsed)) invalidAction(index, usage)
53
+ return parsed
54
+ }
55
+
56
+ export function parseDoChainActions(args: string[]): DoChainAction[] {
57
+ const segments: string[][] = [[]]
58
+ for (const argument of args) {
59
+ if (argument !== '--then') {
60
+ segments[segments.length - 1].push(argument)
61
+ continue
62
+ }
63
+ if (segments[segments.length - 1].length === 0) {
64
+ throw new BridgeInputError('do chain contains an empty action')
65
+ }
66
+ segments.push([])
67
+ }
68
+ if (segments.length < 2) {
69
+ throw new BridgeInputError('do chain needs at least two actions separated by --then')
70
+ }
71
+ if (segments[segments.length - 1].length === 0) {
72
+ throw new BridgeInputError('do chain cannot end with --then')
73
+ }
74
+
75
+ return segments.flatMap((segment, index): DoChainAction[] => {
76
+ const [action, ...values] = segment
77
+ if (!action) throw new BridgeInputError('do chain contains an empty action')
78
+ if (!DO_CHAIN_ACTIONS.has(action)) {
79
+ throw new BridgeInputError(
80
+ `do chain action ${index + 1} cannot batch ${JSON.stringify(action)}; run it as a separate rnx do command`,
81
+ )
82
+ }
83
+
84
+ switch (action) {
85
+ case 'tap': {
86
+ if (values.length === 1 && !Number.isFinite(Number(values[0]))) {
87
+ return [{ type: 'tapId', id: values[0] }]
88
+ }
89
+ if (values.length !== 2) invalidAction(index, 'tap <testid> | <x> <y>')
90
+ return [
91
+ {
92
+ type: 'tap',
93
+ x: finiteNumber(values[0], index, 'tap <testid> | <x> <y>'),
94
+ y: finiteNumber(values[1], index, 'tap <testid> | <x> <y>'),
95
+ },
96
+ ]
97
+ }
98
+
99
+ case 'tap-id':
100
+ if (values.length !== 1) invalidAction(index, 'tap-id <id>')
101
+ return [{ type: 'tapId', id: values[0] }]
102
+
103
+ case 'tap-text': {
104
+ const text = values.join(' ')
105
+ if (!text) invalidAction(index, 'tap-text <text>')
106
+ return [{ type: 'tapText', text }]
107
+ }
108
+
109
+ case 'double-tap': {
110
+ const usage = 'double-tap <testid> [gapMs] | <x> <y> [gapMs]'
111
+ if (values.length > 0 && !Number.isFinite(Number(values[0]))) {
112
+ if (values.length > 2) invalidAction(index, usage)
113
+ return [
114
+ {
115
+ type: 'targetedDoubleTap',
116
+ id: values[0],
117
+ gapMs:
118
+ values.length === 2
119
+ ? Math.max(0, Math.round(finiteNumber(values[1], index, usage)))
120
+ : 80,
121
+ },
122
+ ]
123
+ }
124
+ if (values.length < 2 || values.length > 3) invalidAction(index, usage)
125
+ return [
126
+ {
127
+ type: 'doubleTap',
128
+ x: finiteNumber(values[0], index, usage),
129
+ y: finiteNumber(values[1], index, usage),
130
+ gapMs:
131
+ values.length === 3
132
+ ? Math.max(0, Math.round(finiteNumber(values[2], index, usage)))
133
+ : 80,
134
+ },
135
+ ]
136
+ }
137
+
138
+ case 'long-press': {
139
+ const usage = 'long-press <testid> [durationMs] | <x> <y> [durationMs]'
140
+ if (values.length > 0 && !Number.isFinite(Number(values[0]))) {
141
+ if (values.length > 2) invalidAction(index, usage)
142
+ return [
143
+ {
144
+ type: 'targetedLongPress',
145
+ id: values[0],
146
+ durationMs:
147
+ values.length === 2
148
+ ? Math.max(0, Math.round(finiteNumber(values[1], index, usage)))
149
+ : 600,
150
+ },
151
+ ]
152
+ }
153
+ if (values.length < 2 || values.length > 3) invalidAction(index, usage)
154
+ return [
155
+ {
156
+ type: 'longPress',
157
+ x: finiteNumber(values[0], index, usage),
158
+ y: finiteNumber(values[1], index, usage),
159
+ durationMs:
160
+ values.length === 3
161
+ ? Math.max(0, Math.round(finiteNumber(values[2], index, usage)))
162
+ : 600,
163
+ },
164
+ ]
165
+ }
166
+
167
+ case 'type': {
168
+ const text = values.join(' ')
169
+ if (!text) invalidAction(index, 'type <text>')
170
+ return [{ type: 'type', text }]
171
+ }
172
+
173
+ case 'type-into': {
174
+ const text = values.slice(1).join(' ')
175
+ if (!values[0] || !text) invalidAction(index, 'type-into <id> <text>')
176
+ return [
177
+ { type: 'tapId', id: values[0] },
178
+ { type: 'type', text },
179
+ ]
180
+ }
181
+
182
+ case 'key':
183
+ if (values.length !== 1) invalidAction(index, 'key <name>')
184
+ return [{ type: 'key', key: values[0] }]
185
+
186
+ case 'key-sequence':
187
+ if (values.length === 0) invalidAction(index, 'key-sequence <key> [<key> ...]')
188
+ return values.map((key) => ({ type: 'key', key }))
189
+
190
+ case 'dismiss':
191
+ if (values.length !== 0) invalidAction(index, 'dismiss')
192
+ return [{ type: 'dismissKeyboard' }]
193
+
194
+ case 'scroll': {
195
+ const usage = 'scroll <id> <x> <y>'
196
+ if (values.length !== 3) invalidAction(index, usage)
197
+ return [
198
+ {
199
+ type: 'scroll',
200
+ id: values[0],
201
+ x: finiteNumber(values[1], index, usage),
202
+ y: finiteNumber(values[2], index, usage),
203
+ animated: false,
204
+ },
205
+ ]
206
+ }
207
+
208
+ case 'drag':
209
+ case 'swipe': {
210
+ const usage = `${action} <x1> <y1> <x2> <y2> [steps] [stepMs]`
211
+ if (values.length < 4 || values.length > 6) invalidAction(index, usage)
212
+ const defaultSteps = action === 'swipe' ? 10 : 12
213
+ const defaultStepMs = action === 'swipe' ? 8 : 16
214
+ return [
215
+ {
216
+ type: 'drag',
217
+ fromX: finiteNumber(values[0], index, usage),
218
+ fromY: finiteNumber(values[1], index, usage),
219
+ toX: finiteNumber(values[2], index, usage),
220
+ toY: finiteNumber(values[3], index, usage),
221
+ steps:
222
+ values.length >= 5
223
+ ? Math.max(1, Math.round(finiteNumber(values[4], index, usage)))
224
+ : defaultSteps,
225
+ stepMs:
226
+ values.length === 6
227
+ ? Math.max(0, Math.round(finiteNumber(values[5], index, usage)))
228
+ : defaultStepMs,
229
+ },
230
+ ]
231
+ }
232
+
233
+ case 'pinch': {
234
+ const usage = "pinch <x1> <y1> <x2> <y2> <x1'> <y1'> <x2'> <y2'> [steps] [stepMs]"
235
+ if (values.length < 8 || values.length > 10) invalidAction(index, usage)
236
+ return [
237
+ {
238
+ type: 'pinch',
239
+ fromX1: finiteNumber(values[0], index, usage),
240
+ fromY1: finiteNumber(values[1], index, usage),
241
+ fromX2: finiteNumber(values[2], index, usage),
242
+ fromY2: finiteNumber(values[3], index, usage),
243
+ toX1: finiteNumber(values[4], index, usage),
244
+ toY1: finiteNumber(values[5], index, usage),
245
+ toX2: finiteNumber(values[6], index, usage),
246
+ toY2: finiteNumber(values[7], index, usage),
247
+ steps:
248
+ values.length >= 9
249
+ ? Math.max(1, Math.round(finiteNumber(values[8], index, usage)))
250
+ : 12,
251
+ stepMs:
252
+ values.length === 10
253
+ ? Math.max(0, Math.round(finiteNumber(values[9], index, usage)))
254
+ : 16,
255
+ },
256
+ ]
257
+ }
258
+
259
+ case 'touch': {
260
+ const usage = 'touch <down|move|up|cancel> <x> <y> [pointerId]'
261
+ if (values.length < 3 || values.length > 4) invalidAction(index, usage)
262
+ const phase = values[0]
263
+ if (
264
+ phase !== 'down' &&
265
+ phase !== 'move' &&
266
+ phase !== 'up' &&
267
+ phase !== 'cancel'
268
+ ) {
269
+ invalidAction(index, usage)
270
+ }
271
+ const pointerId =
272
+ values.length === 4
273
+ ? Math.max(1, Math.round(finiteNumber(values[3], index, usage)))
274
+ : 999
275
+ if (phase === 'cancel') return [{ type: 'touchCancel', pointerId }]
276
+ return [
277
+ {
278
+ type:
279
+ phase === 'down' ? 'touchDown' : phase === 'move' ? 'touchMove' : 'touchUp',
280
+ x: finiteNumber(values[1], index, usage),
281
+ y: finiteNumber(values[2], index, usage),
282
+ pointerId,
283
+ },
284
+ ]
285
+ }
286
+
287
+ case 'sleep': {
288
+ const usage = 'sleep [seconds]'
289
+ if (values.length > 1) invalidAction(index, usage)
290
+ const seconds = values.length === 1 ? finiteNumber(values[0], index, usage) : 0.5
291
+ if (seconds < 0) invalidAction(index, usage)
292
+ return [{ type: 'wait', ms: Math.round(seconds * 1000) }]
293
+ }
294
+
295
+ default:
296
+ throw new BridgeInputError(
297
+ `do chain action ${index + 1} cannot batch ${JSON.stringify(action)}`,
298
+ )
299
+ }
300
+ })
301
+ }
302
+
303
+ export async function runDoChain(
304
+ args: string[],
305
+ opts: { port?: number } = {},
306
+ ): Promise<number> {
307
+ const [connectModule, simClientModule, bridgeModule, envModule, targetModule] =
308
+ await Promise.all([
309
+ import('../../src/connect'),
310
+ import('../../src/sim-client'),
311
+ import('../ws-bridge'),
312
+ import('./inspect/env'),
313
+ import('./inspect/resolve-target'),
314
+ ])
315
+ const { connect } = connectModule
316
+ const { SimPerformError } = simClientModule
317
+ const { checkSimHealth, parseBridgeCliArgs } = bridgeModule
318
+ const { isAgentEnv } = envModule
319
+ const { resolveTargetCoords } = targetModule
320
+ const wantsJson = args.includes('--json')
321
+ const skipSettle =
322
+ args.includes('--no-wait') || process.env.SOOTSIM_NO_AUTO_WAIT === '1'
323
+ const parsed = parseBridgeCliArgs(args, {
324
+ port: opts.port,
325
+ commandTimeoutMs: 30_000,
326
+ stripBooleanFlags: ['--json', '--no-wait'],
327
+ })
328
+ if (!Number.isFinite(parsed.commandTimeoutMs) || parsed.commandTimeoutMs <= 0) {
329
+ console.error(' do chain: --timeout must be a positive number')
330
+ return 1
331
+ }
332
+
333
+ let actions: DoChainAction[]
334
+ try {
335
+ actions = parseDoChainActions(parsed.positional)
336
+ } catch (error) {
337
+ console.error(` do chain: ${error instanceof Error ? error.message : error}`)
338
+ return 1
339
+ }
340
+
341
+ let sim: Awaited<ReturnType<typeof connect>> | null = null
342
+ try {
343
+ sim = await connect({
344
+ port: parsed.wsPort,
345
+ sim: parsed.simId,
346
+ timeoutMs: parsed.commandTimeoutMs,
347
+ })
348
+ await checkSimHealth(sim)
349
+
350
+ const steps: SimPerformStep[] = []
351
+ for (const action of actions) {
352
+ if (action.type !== 'targetedDoubleTap' && action.type !== 'targetedLongPress') {
353
+ steps.push(action)
354
+ continue
355
+ }
356
+ const target = await resolveTargetCoords(sim, { mode: 'testid', value: action.id })
357
+ if (!target) {
358
+ throw new BridgeInputError(
359
+ `do chain could not find testID ${JSON.stringify(action.id)}`,
360
+ )
361
+ }
362
+ steps.push(
363
+ action.type === 'targetedDoubleTap'
364
+ ? {
365
+ type: 'doubleTap',
366
+ x: target.x,
367
+ y: target.y,
368
+ gapMs: action.gapMs,
369
+ }
370
+ : {
371
+ type: 'longPress',
372
+ x: target.x,
373
+ y: target.y,
374
+ durationMs: action.durationMs,
375
+ },
376
+ )
377
+ }
378
+
379
+ const result = await sim.perform(steps, { timeoutMs: parsed.commandTimeoutMs })
380
+ if (!skipSettle) {
381
+ try {
382
+ const budgetMs = isAgentEnv() ? 400 : 200
383
+ const settled = await sim.settle({ maxMs: budgetMs })
384
+ if (!settled.settled) {
385
+ process.stderr.write(
386
+ ` ⚠ auto-wait timed out after ${settled.elapsed ?? budgetMs}ms. next command may see mid-animation state. use \`rnx do settle\` for a longer wait.\n`,
387
+ )
388
+ }
389
+ } catch {}
390
+ }
391
+
392
+ if (wantsJson) {
393
+ console.log(JSON.stringify(result))
394
+ } else {
395
+ console.log(
396
+ ` completed ${result.completed} batched ${result.completed === 1 ? 'step' : 'steps'} in ${result.durationMs}ms`,
397
+ )
398
+ }
399
+ return 0
400
+ } catch (error) {
401
+ if (error instanceof SimPerformError && wantsJson) {
402
+ console.log(JSON.stringify(error.result))
403
+ } else {
404
+ console.error(
405
+ ` do chain failed: ${error instanceof Error ? error.message : error}`,
406
+ )
407
+ }
408
+ return 1
409
+ } finally {
410
+ sim?.close()
411
+ }
412
+ }
@@ -456,16 +456,6 @@ flow extension:
456
456
  })
457
457
  }
458
458
  if (!targetDevice) return false
459
- const currentUrl = await bridge.send({
460
- type: 'evaluate',
461
- simId: driver.simId,
462
- code: 'window.location.href',
463
- })
464
- if (typeof currentUrl === 'string' && currentUrl.length > 0) {
465
- const nextUrl = new URL(currentUrl)
466
- nextUrl.searchParams.set('device', targetDevice)
467
- driver.setSimRouteHint(nextUrl.toString())
468
- }
469
459
  return Boolean(
470
460
  await bridge.send({
471
461
  type: 'call',