rnxsim 0.1.369 → 0.1.371

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 (265) hide show
  1. package/cli/auth.ts +2 -1
  2. package/cli/bin.ts +283 -273
  3. package/cli/commands/app-fonts.ts +3 -2
  4. package/cli/commands/assert.ts +8 -7
  5. package/cli/commands/auth.ts +2 -1
  6. package/cli/commands/box/checkout-plane.ts +109 -29
  7. package/cli/commands/box/cloud-box.ts +294 -0
  8. package/cli/commands/box/plane-share.ts +208 -0
  9. package/cli/commands/box/rnx-command.ts +2 -2
  10. package/cli/commands/box/search-command.ts +157 -0
  11. package/cli/commands/box.ts +269 -13
  12. package/cli/commands/config.ts +2 -1
  13. package/cli/commands/control.ts +43 -34
  14. package/cli/commands/daemon.ts +22 -21
  15. package/cli/commands/debug.ts +17 -15
  16. package/cli/commands/detox.ts +5 -4
  17. package/cli/commands/device.ts +6 -5
  18. package/cli/commands/electron.ts +6 -5
  19. package/cli/commands/flow.ts +12 -11
  20. package/cli/commands/inspect/find.ts +2 -1
  21. package/cli/commands/inspect/keyboard.ts +2 -1
  22. package/cli/commands/inspect/sleep.ts +2 -1
  23. package/cli/commands/inspect/wait-event.ts +4 -3
  24. package/cli/commands/inspect/wait-idle.ts +2 -1
  25. package/cli/commands/inspect/wait-ready.ts +3 -2
  26. package/cli/commands/inspect/wait-selector.ts +4 -3
  27. package/cli/commands/inspect/wait.ts +2 -1
  28. package/cli/commands/inspect.ts +82 -80
  29. package/cli/commands/install-desktop.ts +5 -4
  30. package/cli/commands/login.ts +3 -2
  31. package/cli/commands/mode.ts +2 -1
  32. package/cli/commands/record.ts +38 -37
  33. package/cli/commands/runtime.ts +9 -8
  34. package/cli/commands/screenshot-layers.ts +4 -3
  35. package/cli/commands/screenshot-mode.ts +2 -1
  36. package/cli/commands/screenshot.ts +8 -7
  37. package/cli/commands/screenshots-capture.ts +2 -1
  38. package/cli/commands/serve.ts +5 -4
  39. package/cli/commands/shell-boolean-mode.ts +2 -1
  40. package/cli/commands/skills.ts +11 -10
  41. package/cli/commands/telemetry.ts +2 -1
  42. package/cli/commands/three-mode.ts +2 -1
  43. package/cli/commands/timeline.ts +4 -3
  44. package/cli/commands/upload.ts +14 -13
  45. package/cli/commands/what-happened.ts +2 -1
  46. package/cli/outbound-endpoints.ts +37 -1
  47. package/cli/recording-access.ts +6 -5
  48. package/cli/run-rnx.ts +140 -0
  49. package/dist-cli/bin.js +6 -6
  50. package/dist-cli/chunks/{agent-OUUFWDLF.js → agent-W2D2V7WF.js} +2 -2
  51. package/dist-cli/chunks/{agent-wrapper-7BE7DR4R.js → agent-wrapper-GVNM2Z4S.js} +2 -2
  52. package/dist-cli/chunks/app-fonts-J6R4TTXD.js +4 -0
  53. package/dist-cli/chunks/{assert-I3CMRNS7.js → assert-YN6U4ZTB.js} +11 -11
  54. package/dist-cli/chunks/auth-XGYZAK2P.js +12 -0
  55. package/dist-cli/chunks/{beta-BHSKY4K4.js → beta-WRDMHHWJ.js} +2 -2
  56. package/dist-cli/chunks/{box-3CVWXVTT.js → box-ML5TFUQW.js} +617 -596
  57. package/dist-cli/chunks/{camera-MNK73TYV.js → camera-OBEI4FZA.js} +2 -2
  58. package/dist-cli/chunks/{chunk-JXARZBWS.js → chunk-2FIT64GR.js} +1 -1
  59. package/dist-cli/chunks/{chunk-RYGQ6LZN.js → chunk-2LH4DYPM.js} +2 -2
  60. package/dist-cli/chunks/chunk-2QW3JH6T.js +99 -0
  61. package/dist-cli/chunks/{chunk-2APXX4RN.js → chunk-2W4YBCPX.js} +2 -2
  62. package/dist-cli/chunks/{chunk-X3PEDNLV.js → chunk-42YOIDLR.js} +2 -2
  63. package/dist-cli/chunks/{chunk-6BMM6OCW.js → chunk-47MSXLWK.js} +2 -2
  64. package/dist-cli/chunks/chunk-4OLUFHEZ.js +74 -0
  65. package/dist-cli/chunks/{chunk-WUR4FXWT.js → chunk-5PPVVGLH.js} +25 -25
  66. package/dist-cli/chunks/{chunk-AKJYEX54.js → chunk-6AZKZ5AJ.js} +2 -2
  67. package/dist-cli/chunks/{chunk-2PVVIPQS.js → chunk-6FPXLSBJ.js} +1 -1
  68. package/dist-cli/chunks/{chunk-CCI3DZKE.js → chunk-763GBI7G.js} +2 -2
  69. package/dist-cli/chunks/{chunk-CZ2HCNRK.js → chunk-7ASCDKA3.js} +1 -1
  70. package/dist-cli/chunks/{chunk-ZNNE5MAV.js → chunk-7Q4WHJDH.js} +1 -1
  71. package/dist-cli/chunks/{chunk-IYPMULI7.js → chunk-7TWD4P6J.js} +2 -2
  72. package/dist-cli/chunks/{chunk-45X3D6CI.js → chunk-7YB46LGI.js} +1 -1
  73. package/dist-cli/chunks/{chunk-3454VMHD.js → chunk-ADJSGO55.js} +2 -2
  74. package/dist-cli/chunks/{chunk-F4OTMARL.js → chunk-AGZZWLEL.js} +2 -2
  75. package/dist-cli/chunks/{chunk-UDXQCEJU.js → chunk-AVIH3TAZ.js} +11 -11
  76. package/dist-cli/chunks/{chunk-DCPDMF27.js → chunk-B26ZJUQA.js} +2 -2
  77. package/dist-cli/chunks/{chunk-D6MGELKZ.js → chunk-BNS5LNEU.js} +1 -1
  78. package/dist-cli/chunks/{chunk-FHRG772G.js → chunk-CJQXP4XM.js} +1 -1
  79. package/dist-cli/chunks/{chunk-OROUEG22.js → chunk-DP6RGTNN.js} +2 -2
  80. package/dist-cli/chunks/{chunk-V7I4CTAX.js → chunk-DYFOLOYL.js} +2 -2
  81. package/dist-cli/chunks/chunk-ERBGQEJ5.js +4 -0
  82. package/dist-cli/chunks/{chunk-H2GB2PLY.js → chunk-ESWSXF55.js} +2 -2
  83. package/dist-cli/chunks/{chunk-FGGESBWD.js → chunk-FLNGSIBL.js} +2 -2
  84. package/dist-cli/chunks/{chunk-4SCVGBOX.js → chunk-GQGS7TRK.js} +1 -1
  85. package/dist-cli/chunks/{chunk-WGE2P25F.js → chunk-H4DU457S.js} +1 -1
  86. package/dist-cli/chunks/{chunk-TBK5UXW2.js → chunk-HFY35VAG.js} +2 -2
  87. package/dist-cli/chunks/{chunk-JLOOLA47.js → chunk-HV3WOWQM.js} +1 -1
  88. package/dist-cli/chunks/{chunk-RV76U3EI.js → chunk-HZ365EMS.js} +10 -10
  89. package/dist-cli/chunks/chunk-IPNDXGVA.js +11 -0
  90. package/dist-cli/chunks/{chunk-TAC4CEIX.js → chunk-IX7Z6ZVJ.js} +2 -2
  91. package/dist-cli/chunks/{chunk-UQJY6O72.js → chunk-IYU2EOIU.js} +1 -1
  92. package/dist-cli/chunks/chunk-J6A5W2DI.js +30 -0
  93. package/dist-cli/chunks/chunk-JKRMBNPO.js +28 -0
  94. package/dist-cli/chunks/{chunk-VGJLCFSC.js → chunk-JUTLQ3GX.js} +1 -1
  95. package/dist-cli/chunks/{chunk-5QFJFASW.js → chunk-KHGXEU4L.js} +1 -1
  96. package/dist-cli/chunks/{chunk-A56GQVNF.js → chunk-KL34TRNW.js} +1 -1
  97. package/dist-cli/chunks/chunk-LFANRLWK.js +7 -0
  98. package/dist-cli/chunks/{chunk-NPVEBXU3.js → chunk-MOIQZVRU.js} +2 -2
  99. package/dist-cli/chunks/chunk-MW4E5EED.js +497 -0
  100. package/dist-cli/chunks/{chunk-AJJCYLBC.js → chunk-N3LAMTGS.js} +2 -2
  101. package/dist-cli/chunks/{chunk-QE5MC4V3.js → chunk-NQ4CI2VI.js} +1 -1
  102. package/dist-cli/chunks/{chunk-6F3WOWFC.js → chunk-OF3U4SZO.js} +2 -2
  103. package/dist-cli/chunks/{chunk-DKSZZABN.js → chunk-OF5OBB67.js} +1 -1
  104. package/dist-cli/chunks/{chunk-MXWME7ET.js → chunk-OQPZOEEP.js} +1 -1
  105. package/dist-cli/chunks/{chunk-SSBZA5VZ.js → chunk-OWDJQWJ7.js} +1 -1
  106. package/dist-cli/chunks/{chunk-YLNSLMQM.js → chunk-PTWMX4AA.js} +2 -2
  107. package/dist-cli/chunks/{chunk-TTUZ5MCK.js → chunk-RYJTPZCX.js} +1 -1
  108. package/dist-cli/chunks/{chunk-A72MIIJI.js → chunk-S3AH2O67.js} +1 -1
  109. package/dist-cli/chunks/{chunk-ZW6JXEVX.js → chunk-SH2HXHR2.js} +2 -2
  110. package/dist-cli/chunks/{chunk-EKAEEWC2.js → chunk-SHLJYXIN.js} +1 -1
  111. package/dist-cli/chunks/{chunk-4G4VCT2M.js → chunk-TL22EBF5.js} +2 -2
  112. package/dist-cli/chunks/{chunk-LU3AHE6T.js → chunk-TWNLRDIH.js} +2 -2
  113. package/dist-cli/chunks/{chunk-B5NW4TT3.js → chunk-UHYAY3RU.js} +2 -2
  114. package/dist-cli/chunks/{chunk-N5KDDGV6.js → chunk-UPMR6Y5P.js} +2 -2
  115. package/dist-cli/chunks/chunk-VEFSHPSU.js +24 -0
  116. package/dist-cli/chunks/{chunk-YPBDPVLR.js → chunk-WCB5XPCH.js} +2 -2
  117. package/dist-cli/chunks/{chunk-USHV3QFZ.js → chunk-WHZE63R2.js} +2 -2
  118. package/dist-cli/chunks/{chunk-ZEF57HQO.js → chunk-WLRIPAGJ.js} +1 -1
  119. package/dist-cli/chunks/{chunk-FBMNYSSW.js → chunk-Y7SKGRAV.js} +1 -1
  120. package/dist-cli/chunks/chunk-Z6CQDU74.js +4 -0
  121. package/dist-cli/chunks/{cleanup-H4XFRMZV.js → cleanup-PTJBMC34.js} +2 -2
  122. package/dist-cli/chunks/cli-version-6ZBD6GKP.js +4 -0
  123. package/dist-cli/chunks/{compat-NKGYERLN.js → compat-FJ2HWRAG.js} +2 -2
  124. package/dist-cli/chunks/config-PYGH7JPN.js +13 -0
  125. package/dist-cli/chunks/{connect-WWWAVVCF.js → connect-AJV4PZA4.js} +2 -2
  126. package/dist-cli/chunks/control-667PAYX4.js +4 -0
  127. package/dist-cli/chunks/daemon-EIBOHZYQ.js +4 -0
  128. package/dist-cli/chunks/{debug-RO6GLDIM.js → debug-XF2XUDHN.js} +20 -20
  129. package/dist-cli/chunks/desktop-HEZYNFYW.js +31 -0
  130. package/dist-cli/chunks/detox-JCFZJRNE.js +52 -0
  131. package/dist-cli/chunks/device-KGUVZHFW.js +18 -0
  132. package/dist-cli/chunks/{diagnose-HDRTUPDO.js → diagnose-KVJQ3TOI.js} +2 -2
  133. package/dist-cli/chunks/{disk-cleanup-QP2NYN43.js → disk-cleanup-7HQXJFUX.js} +2 -2
  134. package/dist-cli/chunks/{do-chain-6LP6SV5J.js → do-chain-3FYJNA2N.js} +2 -2
  135. package/dist-cli/chunks/drivers-5S6M5TJB.js +4 -0
  136. package/dist-cli/chunks/{env-4XD5D2G6.js → env-BX6S2SOM.js} +2 -2
  137. package/dist-cli/chunks/{film-I5SIXLS5.js → film-2WZUVYFA.js} +3 -3
  138. package/dist-cli/chunks/flow-6BZSWNHG.js +4 -0
  139. package/dist-cli/chunks/help-OGJBD4NP.js +4 -0
  140. package/dist-cli/chunks/{hidden-runtime-alias-LJV2S4M2.js → hidden-runtime-alias-PGF55OFH.js} +2 -2
  141. package/dist-cli/chunks/{home-paths-RFI2QRC3.js → home-paths-EGRMTSGE.js} +2 -2
  142. package/dist-cli/chunks/inspect-MZVDGAN4.js +4 -0
  143. package/dist-cli/chunks/install-desktop-QB76DNZF.js +4 -0
  144. package/dist-cli/chunks/login-HRFPYPZ3.js +26 -0
  145. package/dist-cli/chunks/{logout-DKXZZ6HU.js → logout-LRH5HZGP.js} +2 -2
  146. package/dist-cli/chunks/{maestro-WKGVVWRA.js → maestro-S53T4PPE.js} +3 -3
  147. package/dist-cli/chunks/{maestro-generate-KCK6RK2C.js → maestro-generate-YKIVKOB3.js} +3 -3
  148. package/dist-cli/chunks/mode-47JKNUNS.js +26 -0
  149. package/dist-cli/chunks/{optional-demo-registry-EUSKBIPP.js → optional-demo-registry-ARZSPS5Y.js} +2 -2
  150. package/dist-cli/chunks/{perf-IMZMA5MT.js → perf-J73OKB4T.js} +2 -2
  151. package/dist-cli/chunks/{permissions-SLWXTLH6.js → permissions-L3FPI56H.js} +2 -2
  152. package/dist-cli/chunks/{platform-HQ735LAE.js → platform-YAROAHRM.js} +3 -3
  153. package/dist-cli/chunks/privacy-JZQCXHKN.js +4 -0
  154. package/dist-cli/chunks/record-QAQMGYOO.js +72 -0
  155. package/dist-cli/chunks/report-issue-4FVZCAFQ.js +29 -0
  156. package/dist-cli/chunks/reset-NYFUFZCG.js +4 -0
  157. package/dist-cli/chunks/resolve-target-SP22ZGFZ.js +4 -0
  158. package/dist-cli/chunks/runtime-WTS2QMXZ.js +4 -0
  159. package/dist-cli/chunks/{screenshot-command-HAJB5FHK.js → screenshot-command-Q5MGGEWY.js} +11 -11
  160. package/dist-cli/chunks/screenshot-layers-4JW7DRL7.js +20 -0
  161. package/dist-cli/chunks/screenshots-capture-X5PMO44E.js +42 -0
  162. package/dist-cli/chunks/{serve-TM46TKFW.js → serve-3JWJ45J3.js} +20 -20
  163. package/dist-cli/chunks/setup-USPDJESI.js +4 -0
  164. package/dist-cli/chunks/{sim-client-LQUZSO45.js → sim-client-RL6DGOJE.js} +2 -2
  165. package/dist-cli/chunks/skills-QNO6GDJB.js +50 -0
  166. package/dist-cli/chunks/{state-RUVM4Z4S.js → state-OGUCSUC3.js} +2 -2
  167. package/dist-cli/chunks/{storage-GQ33UTYN.js → storage-TG7KFQZ3.js} +2 -2
  168. package/dist-cli/chunks/store-ZYMQW2LN.js +4 -0
  169. package/dist-cli/chunks/telemetry-7CSLUUIW.js +4 -0
  170. package/dist-cli/chunks/timeline-R4G5P335.js +24 -0
  171. package/dist-cli/chunks/{upgrade-AZDZLUIT.js → upgrade-RDTXJ6O3.js} +2 -2
  172. package/dist-cli/chunks/upload-PLF2DPMS.js +4 -0
  173. package/dist-cli/chunks/{version-N3ALB75G.js → version-GU2NUZDS.js} +2 -2
  174. package/dist-cli/chunks/{web-XPHFZATA.js → web-7PUVG3GE.js} +2 -2
  175. package/dist-cli/chunks/welcome-4GGUV7FY.js +5 -0
  176. package/dist-cli/chunks/what-happened-OHAAX6F6.js +25 -0
  177. package/dist-cli/chunks/{ws-bridge-RUXOYOU6.js → ws-bridge-Z3O35MNF.js} +2 -2
  178. package/dist-lib/agent-daemon-client.cjs +1 -1
  179. package/dist-lib/agent-events.cjs +1 -1
  180. package/dist-lib/agent-identity.cjs +1 -1
  181. package/dist-lib/agent-sessions.cjs +1 -1
  182. package/dist-lib/attached-projects.cjs +1 -1
  183. package/dist-lib/auth/shared-session.cjs +1 -1
  184. package/dist-lib/backend-origin.cjs +1 -1
  185. package/dist-lib/beta.cjs +1 -1
  186. package/dist-lib/beta.mjs +1 -1
  187. package/dist-lib/bridge-constants.cjs +1 -1
  188. package/dist-lib/bridge-contract-input.cjs +1 -1
  189. package/dist-lib/bridge-contract-input.mjs +1 -1
  190. package/dist-lib/bridge-contract.cjs +1 -1
  191. package/dist-lib/bridge-contract.mjs +1 -1
  192. package/dist-lib/cli-constants.cjs +1 -1
  193. package/dist-lib/config.cjs +1 -1
  194. package/dist-lib/detox/index.cjs +1 -1
  195. package/dist-lib/dev-bundle-resolution.cjs +69 -34
  196. package/dist-lib/home-paths.cjs +1 -1
  197. package/dist-lib/host/bridge-host.cjs +430 -106
  198. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  199. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  200. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  201. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  202. package/dist-lib/host/websocket-proxy.cjs +1 -1
  203. package/dist-lib/index.cjs +1 -1
  204. package/dist-lib/jump-to-source-babel.cjs +1 -1
  205. package/dist-lib/menu.cjs +1 -1
  206. package/dist-lib/menu.mjs +1 -1
  207. package/dist-lib/metro-production-bundle.cjs +1 -1
  208. package/dist-lib/metro-production-bundle.mjs +1 -1
  209. package/dist-lib/metro.cjs +1 -1
  210. package/dist-lib/profiles.cjs +1 -1
  211. package/dist-lib/public-brand.cjs +1 -1
  212. package/dist-lib/react-native-host-modules.cjs +2 -1
  213. package/dist-lib/react-native-host-modules.mjs +2 -1
  214. package/dist-lib/render-mode.cjs +1 -1
  215. package/dist-lib/scripts/dev-server-scanner.cjs +149 -93
  216. package/dist-lib/sdk.cjs +1 -1
  217. package/dist-lib/sdk.mjs +1 -1
  218. package/dist-lib/skills.cjs +757 -353
  219. package/dist-lib/vite.cjs +1 -1
  220. package/package.json +1 -1
  221. package/scripts/dev-server-scanner.ts +275 -92
  222. package/src/dev-bundle-resolution.ts +123 -40
  223. package/src/host/bridge-host.ts +11 -0
  224. package/src/host/plane-host.ts +203 -0
  225. package/src/react-native-host-modules.ts +1 -0
  226. package/dist-cli/chunks/app-fonts-66V7TL26.js +0 -4
  227. package/dist-cli/chunks/auth-3FMWUVJH.js +0 -12
  228. package/dist-cli/chunks/chunk-3NLB5LCN.js +0 -28
  229. package/dist-cli/chunks/chunk-5RWUFIZW.js +0 -24
  230. package/dist-cli/chunks/chunk-COXCR6UU.js +0 -7
  231. package/dist-cli/chunks/chunk-JH5KE2OI.js +0 -4
  232. package/dist-cli/chunks/chunk-ROISJKH3.js +0 -99
  233. package/dist-cli/chunks/chunk-SUHAD6N2.js +0 -30
  234. package/dist-cli/chunks/chunk-TIB3CTRX.js +0 -497
  235. package/dist-cli/chunks/chunk-YFZLDFZK.js +0 -74
  236. package/dist-cli/chunks/cli-version-MJWLDXBW.js +0 -4
  237. package/dist-cli/chunks/config-WK7467KI.js +0 -13
  238. package/dist-cli/chunks/control-ESI7IZNN.js +0 -4
  239. package/dist-cli/chunks/daemon-UCLO27YP.js +0 -4
  240. package/dist-cli/chunks/desktop-FF34Z5X4.js +0 -31
  241. package/dist-cli/chunks/detox-Q3CWY5UX.js +0 -52
  242. package/dist-cli/chunks/device-WN4ZWLES.js +0 -18
  243. package/dist-cli/chunks/drivers-OR2JPAXD.js +0 -4
  244. package/dist-cli/chunks/flow-4GWW5DYT.js +0 -4
  245. package/dist-cli/chunks/help-MIIWOT3C.js +0 -4
  246. package/dist-cli/chunks/inspect-3ZGNJENJ.js +0 -4
  247. package/dist-cli/chunks/install-desktop-BYOJVTLS.js +0 -4
  248. package/dist-cli/chunks/login-GNIFSAFE.js +0 -26
  249. package/dist-cli/chunks/mode-YVMX7OJY.js +0 -26
  250. package/dist-cli/chunks/privacy-EDC7PF7G.js +0 -4
  251. package/dist-cli/chunks/record-N4KTTVDI.js +0 -72
  252. package/dist-cli/chunks/report-issue-PP752HAD.js +0 -29
  253. package/dist-cli/chunks/reset-DRMKGVP4.js +0 -4
  254. package/dist-cli/chunks/resolve-target-7WWI32R3.js +0 -4
  255. package/dist-cli/chunks/runtime-EKJV2ZVI.js +0 -4
  256. package/dist-cli/chunks/screenshot-layers-GOCJVM2G.js +0 -20
  257. package/dist-cli/chunks/screenshots-capture-YYJLYA3V.js +0 -42
  258. package/dist-cli/chunks/setup-FVPVIYE4.js +0 -4
  259. package/dist-cli/chunks/skills-V5TE57FN.js +0 -50
  260. package/dist-cli/chunks/store-IKNBDDKM.js +0 -4
  261. package/dist-cli/chunks/telemetry-2ZVU6WLK.js +0 -4
  262. package/dist-cli/chunks/timeline-LP7DMJYT.js +0 -24
  263. package/dist-cli/chunks/upload-DFQK2Q7X.js +0 -4
  264. package/dist-cli/chunks/welcome-2NFYLWLJ.js +0 -5
  265. package/dist-cli/chunks/what-happened-EJ5PHKT7.js +0 -25
@@ -8,19 +8,41 @@
8
8
 
9
9
  import path from 'node:path'
10
10
  import { createInterface } from 'node:readline'
11
- import { createHarnessShell, HarnessProjectFs, PROJECT_MOUNT } from '@contrast/harness'
11
+ import {
12
+ createHarnessShell,
13
+ HarnessProjectFs,
14
+ PROJECT_MOUNT,
15
+ } from '@contrast/harness/shell'
12
16
  import { rnxPublicBrand } from '../../src/public-brand'
17
+ import { authHeaderOrExit } from '../auth'
13
18
  import { findRepositoryPackageRoot } from '../setup-repository'
14
19
  import { resolvePlacement } from '../ws-bridge'
15
20
  import { CheckoutFilePlane, CheckoutRequiresGitError } from './box/checkout-plane'
21
+ import {
22
+ CloudBoxError,
23
+ connectCloudBox,
24
+ createCloudBox,
25
+ heartbeatSession,
26
+ listCloudBoxes,
27
+ stopCloudBox,
28
+ } from './box/cloud-box'
29
+ import { sharePlane } from './box/plane-share'
16
30
  import { createRnxCommand } from './box/rnx-command'
31
+ import { createSearchCommand } from './box/search-command'
32
+ import type { HarnessShellResult } from '@contrast/harness/shell'
17
33
 
18
34
  interface BoxCommandOptions {
19
35
  port?: number
20
36
  verbose?: boolean
21
37
  }
22
38
 
23
- const CLOUD_SUBCOMMANDS = new Set(['create', 'ls', 'connect', 'stop', 'delete'])
39
+ const CLOUD_SUBCOMMANDS = new Set(['connect'])
40
+
41
+ // no default. a cloud box service has to be deployed before one can exist, and
42
+ // defaulting to an origin that answers nothing would advertise a capability
43
+ // that always fails.
44
+ const ENDPOINT_ENV = 'RNX_BOX_ENDPOINT'
45
+ const API_ORIGIN_ENV = 'RNX_API_ORIGIN'
24
46
 
25
47
  function usage(): void {
26
48
  const rnx = rnxPublicBrand.commandName
@@ -31,6 +53,12 @@ function usage(): void {
31
53
  ${rnx} box start a local box over the current checkout
32
54
  ${rnx} box create create a cloud box and enter its shell
33
55
  ${rnx} box create --name=<n> create a named cloud box
56
+ ${rnx} box create --endpoint=<url>
57
+ the box service to create it in, or set
58
+ ${ENDPOINT_ENV}
59
+ ${rnx} box ls cloud boxes running on this account
60
+ ${rnx} box stop --name=<n> stop a cloud box and its billing
61
+ ${rnx} box delete --name=<n> stop it and wipe its files
34
62
 
35
63
  a local box mounts this checkout at ${PROJECT_MOUNT}, gives you a shell with
36
64
  ls, cat, find, rg, sed, pipes and redirects, and registers ${rnx} itself as a
@@ -60,6 +88,11 @@ export async function runBox(
60
88
  }
61
89
 
62
90
  const subcommand = args.find((arg) => !arg.startsWith('-'))
91
+ if (subcommand === 'create') return runCloudBoxCreate(args, opts)
92
+ if (subcommand === 'stop' || subcommand === 'delete') {
93
+ return runCloudBoxStop(args, subcommand === 'delete')
94
+ }
95
+ if (subcommand === 'ls') return runCloudBoxList(args)
63
96
  if (subcommand !== undefined) {
64
97
  if (CLOUD_SUBCOMMANDS.has(subcommand)) {
65
98
  console.error(
@@ -78,6 +111,177 @@ export async function runBox(
78
111
  return runLocalBox(opts)
79
112
  }
80
113
 
114
+ function flag(args: string[], name: string): string | undefined {
115
+ const inline = args.find((arg) => arg.startsWith(`--${name}=`))
116
+ if (inline) return inline.slice(name.length + 3)
117
+ const index = args.indexOf(`--${name}`)
118
+ if (index >= 0 && args[index + 1] && !args[index + 1].startsWith('-')) {
119
+ return args[index + 1]
120
+ }
121
+ return undefined
122
+ }
123
+
124
+ function apiOrigin(args: string[]): string {
125
+ return (
126
+ flag(args, 'api-origin') ??
127
+ process.env[API_ORIGIN_ENV]?.trim() ??
128
+ rnxPublicBrand.origin
129
+ )
130
+ }
131
+
132
+ function requireEndpoint(args: string[], label: string): string | null {
133
+ const endpoint = flag(args, 'endpoint') ?? process.env[ENDPOINT_ENV]?.trim()
134
+ if (endpoint) return endpoint
135
+ const rnx = rnxPublicBrand.commandName
136
+ console.error(
137
+ ` ${rnx} box ${label}: no cloud box service to talk to.\n` +
138
+ ` set ${ENDPOINT_ENV} or pass --endpoint=<url>.`,
139
+ )
140
+ return null
141
+ }
142
+
143
+ async function runCloudBoxStop(args: string[], purge: boolean): Promise<number> {
144
+ const rnx = rnxPublicBrand.commandName
145
+ const label = purge ? 'delete' : 'stop'
146
+ const endpoint = requireEndpoint(args, label)
147
+ if (!endpoint) return 1
148
+ const name =
149
+ flag(args, 'name') ?? path.basename(findRepositoryPackageRoot(process.cwd()) ?? '')
150
+ if (!name) {
151
+ console.error(` ${rnx} box ${label}: which box? pass --name=<n>.`)
152
+ return 1
153
+ }
154
+ const { header } = authHeaderOrExit(`box ${label}`)
155
+ try {
156
+ const result = await stopCloudBox({
157
+ endpoint,
158
+ name,
159
+ apiKey: header.replace(/^Bearer /, ''),
160
+ purge,
161
+ })
162
+ console.log(
163
+ purge
164
+ ? ` deleted ${name}, freeing ${Number(result.freedBytes ?? 0)} bytes`
165
+ : ` stopped ${name}`,
166
+ )
167
+ return 0
168
+ } catch (error) {
169
+ if (error instanceof CloudBoxError) {
170
+ console.error(` ${rnx} box ${label}: ${error.message}`)
171
+ return 1
172
+ }
173
+ throw error
174
+ }
175
+ }
176
+
177
+ async function runCloudBoxList(args: string[]): Promise<number> {
178
+ const rnx = rnxPublicBrand.commandName
179
+ const { header } = authHeaderOrExit('box ls')
180
+ try {
181
+ const boxes = await listCloudBoxes({
182
+ apiOrigin: apiOrigin(args),
183
+ apiKey: header.replace(/^Bearer /, ''),
184
+ })
185
+ if (boxes.length === 0) {
186
+ console.log(` no cloud boxes running on this account`)
187
+ return 0
188
+ }
189
+ for (const box of boxes) {
190
+ const minutes = Math.round(box.meteredMs / 60_000)
191
+ console.log(` ${box.name ?? box.id} running ${minutes}m since ${box.startedAt}`)
192
+ }
193
+ return 0
194
+ } catch (error) {
195
+ if (error instanceof CloudBoxError) {
196
+ console.error(` ${rnx} box ls: ${error.message}`)
197
+ return 1
198
+ }
199
+ throw error
200
+ }
201
+ }
202
+
203
+ async function runCloudBoxCreate(
204
+ args: string[],
205
+ opts: BoxCommandOptions,
206
+ ): Promise<number> {
207
+ const rnx = rnxPublicBrand.commandName
208
+ const endpoint = flag(args, 'endpoint') ?? process.env[ENDPOINT_ENV]?.trim()
209
+ if (!endpoint) {
210
+ console.error(
211
+ ` ${rnx} box create: no cloud box service to create a box in.\n` +
212
+ ` set ${ENDPOINT_ENV} or pass --endpoint=<url>. \`${rnx} box\` runs the\n` +
213
+ ` same shell locally over this checkout.`,
214
+ )
215
+ return 1
216
+ }
217
+
218
+ const root = findRepositoryPackageRoot(process.cwd())
219
+ if (!root) {
220
+ console.error(
221
+ ` ${rnx} box create: no project here. a box is created from a checkout,\n` +
222
+ ` so run this inside a git repository that has a package.json.`,
223
+ )
224
+ return 1
225
+ }
226
+
227
+ const plane = new CheckoutFilePlane(root)
228
+ try {
229
+ await plane.refresh()
230
+ } catch (error) {
231
+ if (error instanceof CheckoutRequiresGitError) {
232
+ console.error(` ${rnx} box create: ${error.message}`)
233
+ return 1
234
+ }
235
+ throw error
236
+ }
237
+
238
+ const name = flag(args, 'name') ?? path.basename(root)
239
+ // the ACCOUNT's key. the box service forwards it to /v1/rnx/sessions, which
240
+ // is what authenticates this caller, names the account, and starts the
241
+ // session the box is billed against.
242
+ const { header } = authHeaderOrExit('box create')
243
+ const apiKey = header.replace(/^Bearer /, '')
244
+
245
+ console.log(` ...creating cloud box ${name}`)
246
+ let session: Awaited<ReturnType<typeof connectCloudBox>> | null = null
247
+ let heartbeat: { stop: () => void } | null = null
248
+ try {
249
+ const created = await createCloudBox({
250
+ endpoint,
251
+ name,
252
+ apiKey,
253
+ plane,
254
+ onProgress: opts.verbose
255
+ ? (seeded, total) => console.log(` ...seeded ${seeded}/${total} files`)
256
+ : undefined,
257
+ })
258
+ console.log(` ...seeded ${created.files} project files`)
259
+ // shown once and never stored: this process does not own a credential
260
+ // store, and inventing one here would be a second place secrets live.
261
+ console.log(` ...box token (shown once): ${created.token}`)
262
+ console.log(` ...accessing shell\n`)
263
+ session = await connectCloudBox({ endpoint, name, token: created.token })
264
+ // bank the wall clock while the shell is open. the box holds no account
265
+ // credential, so the client that is using it is what keeps the session
266
+ // alive; when this process dies the sweep closes it at the last heartbeat.
267
+ heartbeat = heartbeatSession({
268
+ apiOrigin: apiOrigin(args),
269
+ apiKey,
270
+ sessionId: created.sessionId,
271
+ })
272
+ return await repl(session, '')
273
+ } catch (error) {
274
+ if (error instanceof CloudBoxError) {
275
+ console.error(` ${rnx} box create: ${error.message}`)
276
+ return 1
277
+ }
278
+ throw error
279
+ } finally {
280
+ heartbeat?.stop()
281
+ session?.close()
282
+ }
283
+ }
284
+
81
285
  async function runLocalBox(opts: BoxCommandOptions): Promise<number> {
82
286
  // findRepositoryPackageRoot answers both halves of what a box needs: the
83
287
  // nearest package.json, and only when it sits inside a git repository. git
@@ -109,35 +313,90 @@ async function runLocalBox(opts: BoxCommandOptions): Promise<number> {
109
313
 
110
314
  const shell = createHarnessShell({
111
315
  projectFs: new HarnessProjectFs(plane),
112
- commands: [createRnxCommand({ root, shellCwd: () => shell.cwd })],
316
+ commands: [
317
+ createRnxCommand({ root, shellCwd: () => shell.cwd }),
318
+ // overrides just-bash's JavaScript rg, which reads every file body
319
+ // through the plane. see search-command.ts for the numbers.
320
+ createSearchCommand({ root }),
321
+ ],
113
322
  })
114
323
 
115
324
  const relative = path.relative(root, process.cwd())
116
325
  console.log(` ...starting local box`)
117
326
  console.log(` ...mounting ${root} at ${PROJECT_MOUNT}`)
327
+
328
+ // authorize this checkout for the bridge daemon so a browser tab can reach
329
+ // this box's plane. deliberately NOT awaited: a box must not pay for a
330
+ // bridge that may not be there, and an older daemon never answers at all.
331
+ const shared = sharePlane(root, { port: opts.port })
332
+ // two boxes over one checkout is worth SAYING, not refusing. a person with
333
+ // two terminals in one project is doing something reasonable, and the plane
334
+ // re-reads the file list before every command, so neither box works from a
335
+ // stale one. what it does break is an agent mid-run, whose file set changes
336
+ // underneath it because something else is writing the tree. that is the wave
337
+ // case, and the wave's own isolation check is where a refusal belongs; this
338
+ // reports the fact so that check has something to refuse on.
339
+ void shared.settled.then((outcome) => {
340
+ if ('declined' in outcome) {
341
+ if (opts.verbose) console.log(` ...plane not shared: ${outcome.declined}`)
342
+ return
343
+ }
344
+ if (outcome.holders > 1) {
345
+ console.error(
346
+ `\n ${rnxPublicBrand.commandName} box: ${outcome.holders - 1} other box${
347
+ outcome.holders > 2 ? 'es are' : ' is'
348
+ } already open on this checkout.\n` +
349
+ ` writes from either land on the same files. that is fine for a person\n` +
350
+ ` with two terminals, and not fine for an agent, whose file set will\n` +
351
+ ` change underneath it mid-run.\n`,
352
+ )
353
+ return
354
+ }
355
+ if (opts.verbose) {
356
+ console.log(` ...plane shared on the bridge (port ${outcome.port})`)
357
+ }
358
+ })
359
+
118
360
  console.log(` ...accessing shell\n`)
119
361
  if (opts.verbose) {
120
362
  console.log(` ${plane.index().size} project files indexed\n`)
121
363
  }
122
364
 
123
- return repl(shell, plane, relative)
365
+ try {
366
+ // the checkout is shared with editors, agents, and the user's own terminal,
367
+ // so re-read the file set before every command rather than trusting a
368
+ // snapshot taken when the box started. a cloud box has no equivalent: its
369
+ // store is the box, and the box refreshes on its own side.
370
+ return await repl(shell, relative, () => plane.refresh())
371
+ } finally {
372
+ shared?.close()
373
+ }
374
+ }
375
+
376
+ // the REPL drives whatever holds the shell. locally that is an in-process
377
+ // HarnessShell over this checkout; for a cloud box it is a socket to the box,
378
+ // which holds both the files and the shell. same loop, because the only
379
+ // difference is where exec runs.
380
+ interface BoxSession {
381
+ readonly cwd: string
382
+ exec(command: string): Promise<HarnessShellResult>
124
383
  }
125
384
 
126
385
  async function repl(
127
- shell: ReturnType<typeof createHarnessShell>,
128
- plane: CheckoutFilePlane,
386
+ session: BoxSession,
129
387
  startRelative: string,
388
+ beforeCommand?: () => Promise<void>,
130
389
  ): Promise<number> {
131
390
  const rl = createInterface({ input: process.stdin, output: process.stdout })
132
391
 
133
392
  // enter where the user was standing, so `rnx box` from a subdirectory does
134
393
  // not silently teleport them to the project root.
135
394
  if (startRelative && !startRelative.startsWith('..')) {
136
- await shell.exec(`cd ${JSON.stringify(startRelative)}`)
395
+ await session.exec(`cd ${JSON.stringify(startRelative)}`)
137
396
  }
138
397
 
139
398
  const prompt = () => {
140
- rl.setPrompt(`${shell.cwd} > `)
399
+ rl.setPrompt(`${session.cwd} > `)
141
400
  rl.prompt()
142
401
  }
143
402
 
@@ -155,11 +414,8 @@ async function repl(
155
414
  const run = async (command: string) => {
156
415
  if (stopped) return
157
416
  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)
417
+ await beforeCommand?.()
418
+ const result = await session.exec(command)
163
419
  if (result.text && result.text !== '(no output)') console.log(result.text)
164
420
  } catch (error) {
165
421
  console.error(error instanceof Error ? error.message : String(error))
@@ -1,3 +1,4 @@
1
+ import { rnxExit } from '../run-rnx'
1
2
  import { runTelemetry } from './telemetry'
2
3
 
3
4
  function printHelp(): void {
@@ -28,5 +29,5 @@ export async function runConfig(args: string[]): Promise<void> {
28
29
 
29
30
  console.error(` unknown config command: ${sub ?? '(missing)'}`)
30
31
  printHelp()
31
- process.exit(1)
32
+ rnxExit(1)
32
33
  }
@@ -55,6 +55,7 @@ import {
55
55
  } from '../current-sim'
56
56
  import { getDriver } from '../drivers/registry'
57
57
  import { openUrl as openUrlInBrowser } from '../open-url'
58
+ import { rethrowIfExit, rnxExit } from '../run-rnx'
58
59
  import {
59
60
  BridgeSimLockedError,
60
61
  bridgePortForShellBaseUrl,
@@ -387,7 +388,12 @@ async function resolveConnectionInputForCli(target: string): Promise<ResolvedDev
387
388
 
388
389
  if (registeredPort && registeredPort > 0) {
389
390
  const discovered = await probePort(registeredPort)
390
- if (discovered) {
391
+ // a provisional bundle URL means the scanner's fast manifest probe got no
392
+ // answer and fell back to the generic `/index.bundle`. that is enough to
393
+ // LIST the server and not enough to open it, so hand the question to
394
+ // `resolveConnectionInput`, which asks the manifest again with a budget
395
+ // that fits a real dev server and returns the entry point the app names.
396
+ if (discovered && !discovered.bundleUrlProvisional) {
391
397
  return {
392
398
  bundleUrl: discovered.bundleUrl,
393
399
  port: discovered.port,
@@ -477,18 +483,18 @@ function parseOpenRuntimeConfig(args: string[]): SootSimConfig | undefined {
477
483
  const raw = args[i + 1]
478
484
  if (!raw) {
479
485
  console.error(' rnx open: --replace expects <module>=<file>')
480
- process.exit(1)
486
+ rnxExit(1)
481
487
  }
482
488
  const eqIndex = raw.indexOf('=')
483
489
  if (eqIndex <= 0 || eqIndex === raw.length - 1) {
484
490
  console.error(' rnx open: --replace expects <module>=<file>')
485
- process.exit(1)
491
+ rnxExit(1)
486
492
  }
487
493
  const moduleName = raw.slice(0, eqIndex).trim()
488
494
  const filePath = normalizeReplacementPath(raw.slice(eqIndex + 1).trim())
489
495
  if (!fs.existsSync(filePath)) {
490
496
  console.error(` rnx open: replacement file not found: ${filePath}`)
491
- process.exit(1)
497
+ rnxExit(1)
492
498
  }
493
499
  modules[moduleName] = { file: filePath }
494
500
  i++
@@ -502,7 +508,7 @@ function parseOpenRuntimeConfig(args: string[]): SootSimConfig | undefined {
502
508
  const eqIndex = raw ? raw.indexOf('=') : -1
503
509
  if (!raw || eqIndex <= 0 || eqIndex === raw.length - 1) {
504
510
  console.error(' rnx open: --remap expects <host:port>=<host:port>')
505
- process.exit(1)
511
+ rnxExit(1)
506
512
  }
507
513
  remap[raw.slice(0, eqIndex).trim()] = raw.slice(eqIndex + 1).trim()
508
514
  i++
@@ -1338,7 +1344,7 @@ export async function runLinkCommand(args: string[], opts: ControlCommandOptions
1338
1344
  if (parsed.positional.length !== 1) {
1339
1345
  console.error(' rnx open: expected exactly one <url-or-route>')
1340
1346
  console.error(' examples: rnx open /settings | rnx open myapp://settings')
1341
- process.exit(1)
1347
+ rnxExit(1)
1342
1348
  }
1343
1349
 
1344
1350
  const bridge = createBridgeFromParsed(parsed)
@@ -1353,7 +1359,7 @@ export async function runLinkCommand(args: string[], opts: ControlCommandOptions
1353
1359
  warningsCommand: `rnx get warnings 5${simHint}`,
1354
1360
  requestsCommand: `rnx get requests 5${simHint}`,
1355
1361
  })
1356
- process.exit(1)
1362
+ rnxExit(1)
1357
1363
  } finally {
1358
1364
  bridge.close()
1359
1365
  }
@@ -1397,11 +1403,11 @@ export async function runOpenCommand(
1397
1403
  console.error(
1398
1404
  ` rnx open: --cdp-port must be a positive port number, got "${cdpPortArg}"`,
1399
1405
  )
1400
- process.exit(1)
1406
+ rnxExit(1)
1401
1407
  }
1402
1408
  if (requestedProfileId && ephemeralProfile) {
1403
1409
  console.error(' rnx open: --profile cannot be combined with --ephemeral')
1404
- process.exit(1)
1410
+ rnxExit(1)
1405
1411
  }
1406
1412
  const profileId = requestedProfileId ? ensureProfile(requestedProfileId).id : undefined
1407
1413
  const openInNewSim = args.includes('--new') || !!profileId || ephemeralProfile
@@ -1410,7 +1416,7 @@ export async function runOpenCommand(
1410
1416
  console.error(
1411
1417
  ' rnx open: --new, --profile, and --ephemeral cannot be combined with --sim',
1412
1418
  )
1413
- process.exit(1)
1419
+ rnxExit(1)
1414
1420
  }
1415
1421
  const target = parsed.positional[0] || ''
1416
1422
 
@@ -1419,7 +1425,7 @@ export async function runOpenCommand(
1419
1425
  console.error(
1420
1426
  ' rnx open: deep links target the current app; remove --new, --profile, or --ephemeral',
1421
1427
  )
1422
- process.exit(1)
1428
+ rnxExit(1)
1423
1429
  }
1424
1430
  await runLinkCommand(args, opts)
1425
1431
  return
@@ -1447,7 +1453,7 @@ export async function runOpenCommand(
1447
1453
  ` could not launch a browser: ${err instanceof Error ? err.message : String(err)}`,
1448
1454
  )
1449
1455
  console.error(` open it yourself: ${target}`)
1450
- process.exit(1)
1456
+ rnxExit(1)
1451
1457
  }
1452
1458
  console.log(
1453
1459
  isLocal
@@ -1492,7 +1498,7 @@ export async function runOpenCommand(
1492
1498
  const requestedDriverId = args.find((_, i) => args[i - 1] === '--driver') || ''
1493
1499
  if (requestedDriverId && !isDriverId(requestedDriverId)) {
1494
1500
  console.error(` unknown driver "${requestedDriverId}" — run \`rnx list --drivers\``)
1495
- process.exit(1)
1501
+ rnxExit(1)
1496
1502
  }
1497
1503
  const driverId: DriverId = isDriverId(requestedDriverId)
1498
1504
  ? requestedDriverId
@@ -1506,7 +1512,7 @@ export async function runOpenCommand(
1506
1512
  const driverHeadless = resolveDriverHeadless(args, driverId)
1507
1513
  if (cdpPort && driverId !== 'playwright') {
1508
1514
  console.error(' rnx open: --cdp-port is only honored by the playwright driver')
1509
- process.exit(1)
1515
+ rnxExit(1)
1510
1516
  }
1511
1517
 
1512
1518
  // metro HMR mode. interactive opens default to hot=true so edits hot-apply
@@ -1520,7 +1526,7 @@ export async function runOpenCommand(
1520
1526
  const noHmrFlag = args.includes('--no-hmr')
1521
1527
  if (hotFlag && noHmrFlag) {
1522
1528
  console.error(' rnx open: --hot and --no-hmr cannot be combined')
1523
- process.exit(1)
1529
+ rnxExit(1)
1524
1530
  }
1525
1531
  const hotMode = hotFlag ? true : noHmrFlag ? false : driverId !== 'playwright'
1526
1532
 
@@ -1532,7 +1538,7 @@ export async function runOpenCommand(
1532
1538
  if (deviceArg && !(deviceArg in devices)) {
1533
1539
  console.error(` rnx open: unknown device "${deviceArg}"`)
1534
1540
  console.error(` known devices: ${listSelectableDeviceModels().join(', ')}`)
1535
- process.exit(1)
1541
+ rnxExit(1)
1536
1542
  }
1537
1543
  const deviceSpec = deviceArg ? devices[deviceArg as DeviceModel] : undefined
1538
1544
 
@@ -1544,7 +1550,7 @@ export async function runOpenCommand(
1544
1550
  const viewportMatch = viewportArg ? /^(\d+)x(\d+)$/.exec(viewportArg) : null
1545
1551
  if (viewportArg && !viewportMatch) {
1546
1552
  console.error(` rnx open: --viewport must be <width>x<height>, got "${viewportArg}"`)
1547
- process.exit(1)
1553
+ rnxExit(1)
1548
1554
  }
1549
1555
  const explicitViewport = viewportMatch
1550
1556
  ? { width: Number(viewportMatch[1]), height: Number(viewportMatch[2]) }
@@ -1662,7 +1668,7 @@ export async function runOpenCommand(
1662
1668
  warningsCommand: `rnx get warnings 5${simHint}`,
1663
1669
  requestsCommand: `rnx get requests 5${simHint}`,
1664
1670
  })
1665
- process.exit(1)
1671
+ rnxExit(1)
1666
1672
  }
1667
1673
 
1668
1674
  if (!fallBackToNewSim && targetSim) {
@@ -1674,7 +1680,7 @@ export async function runOpenCommand(
1674
1680
  )
1675
1681
  if (!ready) {
1676
1682
  console.error(' timed out waiting for current sim to load target')
1677
- process.exit(1)
1683
+ rnxExit(1)
1678
1684
  }
1679
1685
  ready.bridge.close()
1680
1686
  saveCurrentSimId(targetSim.id)
@@ -1706,7 +1712,7 @@ export async function runOpenCommand(
1706
1712
  console.error(
1707
1713
  ' could not resolve a stable owning process; refusing unsupervised sim host',
1708
1714
  )
1709
- process.exit(1)
1715
+ rnxExit(1)
1710
1716
  }
1711
1717
  const simMatchesToken = (sim: BridgeSimInfo) =>
1712
1718
  sim.url?.includes(`inspectOpen=${token}`) ?? false
@@ -1714,7 +1720,7 @@ export async function runOpenCommand(
1714
1720
  const driver = getDriver(driverId)
1715
1721
  if (!driver) {
1716
1722
  console.error(` unknown driver "${driverId}" — run \`rnx list --drivers\``)
1717
- process.exit(1)
1723
+ rnxExit(1)
1718
1724
  }
1719
1725
  const connectWait = resolveDriverConnectWaitOptions()
1720
1726
  const result = await driver.launch({
@@ -1738,7 +1744,7 @@ export async function runOpenCommand(
1738
1744
  })
1739
1745
  if (!result.launched) {
1740
1746
  console.error(` ${driver.name} driver: ${result.message}`)
1741
- process.exit(1)
1747
+ rnxExit(1)
1742
1748
  }
1743
1749
  const outcome = await waitForSimMatchOrHostExit(
1744
1750
  parsed.wsPort,
@@ -1750,7 +1756,7 @@ export async function runOpenCommand(
1750
1756
  if (outcome.kind === 'host-exit') {
1751
1757
  printDriverDiagnosticLogTail(result)
1752
1758
  console.error(` ${driver.name} host crashed — sim is no longer attached`)
1753
- process.exit(1)
1759
+ rnxExit(1)
1754
1760
  }
1755
1761
  if (outcome.kind === 'timeout') {
1756
1762
  if (result.pid) {
@@ -1763,7 +1769,7 @@ export async function runOpenCommand(
1763
1769
  console.error(
1764
1770
  ` timed out after ${connectWait.timeoutMs}ms waiting for opened sim to connect`,
1765
1771
  )
1766
- process.exit(1)
1772
+ rnxExit(1)
1767
1773
  }
1768
1774
  const match = outcome.sim
1769
1775
  saveCurrentSimId(match.id)
@@ -1801,7 +1807,7 @@ async function runUseLikeCommand(args: string[], opts: ControlCommandOptions = {
1801
1807
  ` refused: ${target.id} is locked by the active user (${secs}s) — that's a live human tab`,
1802
1808
  )
1803
1809
  console.error(` run \`rnx open --new\` for a fresh investigation sim`)
1804
- process.exit(1)
1810
+ rnxExit(1)
1805
1811
  }
1806
1812
  if (!force) {
1807
1813
  // another CLI agent leases it. don't hijack its target silently;
@@ -1813,20 +1819,21 @@ async function runUseLikeCommand(args: string[], opts: ControlCommandOptions = {
1813
1819
  console.error(
1814
1820
  ` \`sootsim use ${target.id} --force\` to take it, or \`rnx open --new\` for a fresh sim`,
1815
1821
  )
1816
- process.exit(1)
1822
+ rnxExit(1)
1817
1823
  }
1818
1824
  }
1819
1825
  await bridge.focusSim(target.id)
1820
1826
  saveCurrentSimId(target.id)
1821
1827
  console.log(` using: ${target.id}`)
1822
1828
  } catch (err) {
1829
+ rethrowIfExit(err)
1823
1830
  console.error(` use failed: ${err instanceof Error ? err.message : String(err)}`)
1824
1831
  await printBridgeFailureDiagnostics(bridge, {
1825
1832
  errorsCommand: `rnx get errors 5${simHint}`,
1826
1833
  warningsCommand: `rnx get warnings 5${simHint}`,
1827
1834
  requestsCommand: `rnx get requests 5${simHint}`,
1828
1835
  })
1829
- process.exit(1)
1836
+ rnxExit(1)
1830
1837
  }
1831
1838
  } finally {
1832
1839
  bridge.close()
@@ -1871,10 +1878,10 @@ export async function runClaimCommand(args: string[], opts: ControlCommandOption
1871
1878
  const secs = Math.max(0, Math.round(err.lock.expiresInMs / 1000))
1872
1879
  console.error(` claim failed: locked by ${err.lock.by} for ${secs}s more`)
1873
1880
  console.error(` use --force to take it, or \`rnx open --new\` for a fresh sim`)
1874
- process.exit(1)
1881
+ rnxExit(1)
1875
1882
  }
1876
1883
  console.error(` claim failed: ${err instanceof Error ? err.message : String(err)}`)
1877
- process.exit(1)
1884
+ rnxExit(1)
1878
1885
  }
1879
1886
  } finally {
1880
1887
  bridge.close()
@@ -2016,7 +2023,7 @@ export async function runCloseCommand(args: string[], opts: ControlCommandOption
2016
2023
  console.error(
2017
2024
  ` close failed: keep sim ${plan.missingExplicitKeepId} is not connected; not closing other sims`,
2018
2025
  )
2019
- process.exit(1)
2026
+ rnxExit(1)
2020
2027
  }
2021
2028
  if (plan.targets.length === 0) {
2022
2029
  console.log(
@@ -2047,11 +2054,12 @@ export async function runCloseCommand(args: string[], opts: ControlCommandOption
2047
2054
  console.log(summary)
2048
2055
  if (result.remaining.length > 0) {
2049
2056
  console.error(` close failed: still connected: ${result.remaining.join(', ')}`)
2050
- process.exit(1)
2057
+ rnxExit(1)
2051
2058
  }
2052
2059
  } catch (err) {
2060
+ rethrowIfExit(err)
2053
2061
  console.error(` close failed: ${err instanceof Error ? err.message : String(err)}`)
2054
- process.exit(1)
2062
+ rnxExit(1)
2055
2063
  } finally {
2056
2064
  bridge.close()
2057
2065
  }
@@ -2074,7 +2082,7 @@ export async function runCloseCommand(args: string[], opts: ControlCommandOption
2074
2082
  if (!closed) {
2075
2083
  await terminatePlaywrightHostsForSims(sims, [target.id])
2076
2084
  console.error(` close failed: ${target.id} is still connected`)
2077
- process.exit(1)
2085
+ rnxExit(1)
2078
2086
  }
2079
2087
  await terminatePlaywrightHostsForSims(sims, [target.id])
2080
2088
  if (readCurrentSimId() === target.id) {
@@ -2083,13 +2091,14 @@ export async function runCloseCommand(args: string[], opts: ControlCommandOption
2083
2091
  }
2084
2092
  console.log(` closed: ${target.id}`)
2085
2093
  } catch (err) {
2094
+ rethrowIfExit(err)
2086
2095
  console.error(` close failed: ${err instanceof Error ? err.message : String(err)}`)
2087
2096
  await printBridgeFailureDiagnostics(bridge, {
2088
2097
  errorsCommand: `rnx get errors 5${simHint}`,
2089
2098
  warningsCommand: `rnx get warnings 5${simHint}`,
2090
2099
  requestsCommand: `rnx get requests 5${simHint}`,
2091
2100
  })
2092
- process.exit(1)
2101
+ rnxExit(1)
2093
2102
  }
2094
2103
  } finally {
2095
2104
  bridge.close()