rnxsim 0.1.368 → 0.1.370

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 (256) 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 +49 -16
  7. package/cli/commands/compat.ts +93 -4
  8. package/cli/commands/config.ts +2 -1
  9. package/cli/commands/control.ts +37 -33
  10. package/cli/commands/daemon.ts +22 -21
  11. package/cli/commands/debug.ts +17 -15
  12. package/cli/commands/detect.ts +11 -7
  13. package/cli/commands/detox.ts +5 -4
  14. package/cli/commands/device.ts +6 -5
  15. package/cli/commands/electron.ts +6 -5
  16. package/cli/commands/flow.ts +12 -11
  17. package/cli/commands/inspect/find.ts +2 -1
  18. package/cli/commands/inspect/keyboard.ts +2 -1
  19. package/cli/commands/inspect/sleep.ts +2 -1
  20. package/cli/commands/inspect/wait-event.ts +4 -3
  21. package/cli/commands/inspect/wait-idle.ts +2 -1
  22. package/cli/commands/inspect/wait-ready.ts +3 -2
  23. package/cli/commands/inspect/wait-selector.ts +4 -3
  24. package/cli/commands/inspect/wait.ts +2 -1
  25. package/cli/commands/inspect.ts +82 -80
  26. package/cli/commands/install-desktop.ts +5 -4
  27. package/cli/commands/login.ts +3 -2
  28. package/cli/commands/mode.ts +2 -1
  29. package/cli/commands/record.ts +38 -37
  30. package/cli/commands/runtime.ts +9 -8
  31. package/cli/commands/screenshot-layers.ts +4 -3
  32. package/cli/commands/screenshot-mode.ts +2 -1
  33. package/cli/commands/screenshot.ts +8 -7
  34. package/cli/commands/screenshots-capture.ts +2 -1
  35. package/cli/commands/serve.ts +5 -4
  36. package/cli/commands/shell-boolean-mode.ts +2 -1
  37. package/cli/commands/skills.ts +11 -10
  38. package/cli/commands/telemetry.ts +2 -1
  39. package/cli/commands/three-mode.ts +2 -1
  40. package/cli/commands/timeline.ts +4 -3
  41. package/cli/commands/upload.ts +14 -13
  42. package/cli/commands/what-happened.ts +2 -1
  43. package/cli/recording-access.ts +6 -5
  44. package/cli/run-rnx.ts +140 -0
  45. package/dist-cli/bin.js +6 -6
  46. package/dist-cli/chunks/{agent-GXCQMYV4.js → agent-3R4424LW.js} +2 -2
  47. package/dist-cli/chunks/{agent-wrapper-Y3PQAL6I.js → agent-wrapper-4PPWZWSY.js} +2 -2
  48. package/dist-cli/chunks/app-fonts-KYXB6CXQ.js +4 -0
  49. package/dist-cli/chunks/{assert-BSRQBRH7.js → assert-7EZOWGYQ.js} +11 -11
  50. package/dist-cli/chunks/auth-L7M36H6C.js +12 -0
  51. package/dist-cli/chunks/{beta-R4IZ5SWO.js → beta-FKAF6YWJ.js} +2 -2
  52. package/dist-cli/chunks/{box-2NQYYY3B.js → box-5Y5VBMHV.js} +62 -62
  53. package/dist-cli/chunks/{camera-ER66WWAX.js → camera-XHF2RPFG.js} +2 -2
  54. package/dist-cli/chunks/{chunk-UBH2BWCY.js → chunk-2FOGXVOK.js} +2 -2
  55. package/dist-cli/chunks/{chunk-7B22IPSC.js → chunk-3BG6UCQY.js} +2 -2
  56. package/dist-cli/chunks/{chunk-IXRVFQMT.js → chunk-3SOPYG7S.js} +2 -2
  57. package/dist-cli/chunks/{chunk-7V3S2DY2.js → chunk-3UXXYJVD.js} +1 -1
  58. package/dist-cli/chunks/{chunk-3MPFC3DS.js → chunk-5BR33NAZ.js} +1 -1
  59. package/dist-cli/chunks/{chunk-BWJOLJOK.js → chunk-5RKRVS5A.js} +2 -2
  60. package/dist-cli/chunks/{chunk-WVVAYLWZ.js → chunk-64JAEJ6N.js} +1 -1
  61. package/dist-cli/chunks/{chunk-GXHCJRKE.js → chunk-6A6L3VJR.js} +2 -2
  62. package/dist-cli/chunks/chunk-6GPS7U4X.js +74 -0
  63. package/dist-cli/chunks/{chunk-JHYXRORU.js → chunk-6S5LJV2O.js} +2 -2
  64. package/dist-cli/chunks/{chunk-TOE5O6JE.js → chunk-6W4HIWC7.js} +2 -2
  65. package/dist-cli/chunks/{chunk-ESVUVW3R.js → chunk-72S5FCBY.js} +2 -2
  66. package/dist-cli/chunks/{chunk-TC6DNQL6.js → chunk-7C7TJYZ3.js} +2 -2
  67. package/dist-cli/chunks/{chunk-M7COA4BE.js → chunk-AALKIAUZ.js} +1 -1
  68. package/dist-cli/chunks/{chunk-CKNNO6ZG.js → chunk-ARMXC3C3.js} +25 -25
  69. package/dist-cli/chunks/chunk-AVJBT5LF.js +4 -0
  70. package/dist-cli/chunks/chunk-BMIJZNKD.js +28 -0
  71. package/dist-cli/chunks/{chunk-FSPZY4AI.js → chunk-C7BA5CCM.js} +2 -2
  72. package/dist-cli/chunks/{chunk-EHA65UVJ.js → chunk-CMVRTZ5G.js} +2 -2
  73. package/dist-cli/chunks/chunk-D24EOJ4C.js +24 -0
  74. package/dist-cli/chunks/{chunk-3XMEIZEV.js → chunk-D2GZASRV.js} +1 -1
  75. package/dist-cli/chunks/{chunk-3S2J6IVC.js → chunk-DHYTRCIP.js} +2 -2
  76. package/dist-cli/chunks/{chunk-QMVBWZH2.js → chunk-DMHSD6YJ.js} +2 -2
  77. package/dist-cli/chunks/{chunk-NGWUGNPV.js → chunk-DUXHYPBP.js} +2 -2
  78. package/dist-cli/chunks/{chunk-4FLQYRGN.js → chunk-DXV37CJN.js} +1 -1
  79. package/dist-cli/chunks/{chunk-T2MGLXY5.js → chunk-EOIBLKZO.js} +2 -2
  80. package/dist-cli/chunks/{chunk-PNBQBBG5.js → chunk-EQ3OE2AV.js} +1 -1
  81. package/dist-cli/chunks/{chunk-WFAFAGMG.js → chunk-EWSWOWXK.js} +2 -2
  82. package/dist-cli/chunks/{chunk-HKQXGCQT.js → chunk-FA75F34Z.js} +1 -1
  83. package/dist-cli/chunks/{chunk-NERY6HGZ.js → chunk-FK4JSTZ3.js} +2 -2
  84. package/dist-cli/chunks/{chunk-NPSPWMBO.js → chunk-GJRLPJQL.js} +2 -2
  85. package/dist-cli/chunks/{chunk-TPIPKXJL.js → chunk-GRO74J2X.js} +1 -1
  86. package/dist-cli/chunks/chunk-H5X5FJ63.js +99 -0
  87. package/dist-cli/chunks/chunk-HEKHLQ3B.js +497 -0
  88. package/dist-cli/chunks/chunk-HQQTS2CB.js +30 -0
  89. package/dist-cli/chunks/{chunk-Q65KH25G.js → chunk-I52NT6VG.js} +1 -1
  90. package/dist-cli/chunks/{chunk-AUPJX4KI.js → chunk-IDLFVRPN.js} +1 -1
  91. package/dist-cli/chunks/{chunk-LAKWOJ5Y.js → chunk-JZLDHPB7.js} +2 -2
  92. package/dist-cli/chunks/{chunk-WJQB6AXZ.js → chunk-LIMWG54N.js} +1 -1
  93. package/dist-cli/chunks/{chunk-TE645QCO.js → chunk-LUOHRBJG.js} +1 -1
  94. package/dist-cli/chunks/{chunk-DJSPNOF6.js → chunk-LXPHL6FO.js} +2 -2
  95. package/dist-cli/chunks/{chunk-ECXDCED5.js → chunk-MDCY532E.js} +2 -2
  96. package/dist-cli/chunks/{chunk-4VA2ISWX.js → chunk-NJMU5BN6.js} +1 -1
  97. package/dist-cli/chunks/{chunk-RRALQ4WM.js → chunk-NR2V2ONN.js} +2 -2
  98. package/dist-cli/chunks/{chunk-SHMLYTNH.js → chunk-NTC46LSA.js} +2 -2
  99. package/dist-cli/chunks/{chunk-X5MXQB3L.js → chunk-O6QLKSAM.js} +10 -10
  100. package/dist-cli/chunks/{chunk-LPAOOJIW.js → chunk-OJZHECGH.js} +1 -1
  101. package/dist-cli/chunks/chunk-OTFTMGAL.js +7 -0
  102. package/dist-cli/chunks/{chunk-QKOY7UW5.js → chunk-PAUEZO6F.js} +2 -2
  103. package/dist-cli/chunks/{chunk-WXRDFUR7.js → chunk-PIZXOXBL.js} +2 -2
  104. package/dist-cli/chunks/{chunk-WF2CRNYF.js → chunk-PNOENVMJ.js} +1 -1
  105. package/dist-cli/chunks/{chunk-FDAFSRQE.js → chunk-Q4MRJ6PO.js} +2 -2
  106. package/dist-cli/chunks/{chunk-SSUPPM6K.js → chunk-RRX52O37.js} +1 -1
  107. package/dist-cli/chunks/{chunk-E6XYIIJK.js → chunk-U63T6LIQ.js} +1 -1
  108. package/dist-cli/chunks/{chunk-OZJMWTGA.js → chunk-UFVAAWGZ.js} +1 -1
  109. package/dist-cli/chunks/{chunk-GS5VUDCF.js → chunk-VVQCZ42C.js} +1 -1
  110. package/dist-cli/chunks/{chunk-MCO7V4VN.js → chunk-WFQHT4QD.js} +2 -2
  111. package/dist-cli/chunks/{chunk-JFGW75UL.js → chunk-WPV4H5S4.js} +2 -2
  112. package/dist-cli/chunks/{chunk-OXCDE3DD.js → chunk-X6WQ4YWE.js} +1 -1
  113. package/dist-cli/chunks/{chunk-FVODUDPS.js → chunk-YIZGNT7A.js} +1 -1
  114. package/dist-cli/chunks/{chunk-MGOAQHMO.js → chunk-Z2VBSNFI.js} +2 -2
  115. package/dist-cli/chunks/{chunk-GFWUIGGY.js → chunk-Z3DENGXW.js} +2 -2
  116. package/dist-cli/chunks/{cleanup-WHGLBAJ2.js → cleanup-MXMMJ7JW.js} +2 -2
  117. package/dist-cli/chunks/cli-version-MLS55UTY.js +4 -0
  118. package/dist-cli/chunks/compat-E6KE5KDI.js +23 -0
  119. package/dist-cli/chunks/config-QSLLVZDS.js +13 -0
  120. package/dist-cli/chunks/{connect-N4FV3DDV.js → connect-QE4VU6GB.js} +2 -2
  121. package/dist-cli/chunks/control-XD5F6K5T.js +4 -0
  122. package/dist-cli/chunks/daemon-UN5SE2JO.js +4 -0
  123. package/dist-cli/chunks/{debug-AHEK43AJ.js → debug-5FG3MBJS.js} +20 -20
  124. package/dist-cli/chunks/desktop-5EZX7NX5.js +31 -0
  125. package/dist-cli/chunks/detox-WJGQP5TM.js +52 -0
  126. package/dist-cli/chunks/device-D7LKCAMS.js +18 -0
  127. package/dist-cli/chunks/{diagnose-WDP3CVIX.js → diagnose-A3W6ZTGA.js} +2 -2
  128. package/dist-cli/chunks/{disk-cleanup-JRT5MKFW.js → disk-cleanup-BDRHX5ZG.js} +2 -2
  129. package/dist-cli/chunks/{do-chain-7FDMO56C.js → do-chain-RUJNRPPF.js} +2 -2
  130. package/dist-cli/chunks/drivers-3NJ5RL6X.js +4 -0
  131. package/dist-cli/chunks/{env-YIBOMB24.js → env-U67SQVTV.js} +2 -2
  132. package/dist-cli/chunks/{film-R3EQADMG.js → film-XC6UAQ3V.js} +3 -3
  133. package/dist-cli/chunks/flow-FHZZD3P6.js +4 -0
  134. package/dist-cli/chunks/help-BYYDUSXF.js +4 -0
  135. package/dist-cli/chunks/{hidden-runtime-alias-6M4UERRQ.js → hidden-runtime-alias-7S2TNZXG.js} +2 -2
  136. package/dist-cli/chunks/{home-paths-QJNOHZJO.js → home-paths-LMO5LB4F.js} +2 -2
  137. package/dist-cli/chunks/inspect-RQCTWDCC.js +4 -0
  138. package/dist-cli/chunks/install-desktop-GOWXCQUE.js +4 -0
  139. package/dist-cli/chunks/login-JIU42S4C.js +26 -0
  140. package/dist-cli/chunks/{logout-VNJX2IEW.js → logout-JTAWQXZO.js} +2 -2
  141. package/dist-cli/chunks/{maestro-SQS2OS2D.js → maestro-TYBQYOGB.js} +3 -3
  142. package/dist-cli/chunks/{maestro-generate-GWVPISG5.js → maestro-generate-YLAGWVKE.js} +3 -3
  143. package/dist-cli/chunks/mode-DV24ZRZ6.js +26 -0
  144. package/dist-cli/chunks/{optional-demo-registry-L3LRBQHG.js → optional-demo-registry-JZH66VIM.js} +2 -2
  145. package/dist-cli/chunks/{perf-XLPC4UKR.js → perf-LXXJ4EAX.js} +2 -2
  146. package/dist-cli/chunks/{permissions-QRHWVMDP.js → permissions-KAOVZUPW.js} +2 -2
  147. package/dist-cli/chunks/{platform-F47URZNB.js → platform-KW35446W.js} +3 -3
  148. package/dist-cli/chunks/privacy-MIFE72CG.js +4 -0
  149. package/dist-cli/chunks/record-EK3ETFHZ.js +72 -0
  150. package/dist-cli/chunks/report-issue-NW5PCAZF.js +29 -0
  151. package/dist-cli/chunks/reset-RZ4O5AFJ.js +4 -0
  152. package/dist-cli/chunks/resolve-target-7R5BNYMM.js +4 -0
  153. package/dist-cli/chunks/runtime-MZHLSRS6.js +4 -0
  154. package/dist-cli/chunks/{screenshot-command-RK7IJHVK.js → screenshot-command-TQSPTFT2.js} +11 -11
  155. package/dist-cli/chunks/screenshot-layers-IZP3L3AQ.js +20 -0
  156. package/dist-cli/chunks/screenshots-capture-K7LJOVAE.js +42 -0
  157. package/dist-cli/chunks/{serve-UBXNGLNP.js → serve-MMX4WXXG.js} +18 -18
  158. package/dist-cli/chunks/setup-YFP3KEQZ.js +4 -0
  159. package/dist-cli/chunks/{sim-client-VTKFEDJX.js → sim-client-KLAYIFOC.js} +2 -2
  160. package/dist-cli/chunks/skills-N4D3POIS.js +50 -0
  161. package/dist-cli/chunks/{state-273XJPUE.js → state-ES6TFQZF.js} +2 -2
  162. package/dist-cli/chunks/{storage-RTDFWIY7.js → storage-52DR5V2C.js} +2 -2
  163. package/dist-cli/chunks/store-HOL45WCI.js +4 -0
  164. package/dist-cli/chunks/telemetry-BV6OTYPF.js +4 -0
  165. package/dist-cli/chunks/timeline-NRVNB5JR.js +24 -0
  166. package/dist-cli/chunks/{upgrade-R74NVHKG.js → upgrade-LNED5YQ2.js} +2 -2
  167. package/dist-cli/chunks/upload-RXKHFIBU.js +4 -0
  168. package/dist-cli/chunks/{version-RTFCYQZ7.js → version-VEF2DYGG.js} +2 -2
  169. package/dist-cli/chunks/{web-35SSKBRU.js → web-CGDE6S73.js} +2 -2
  170. package/dist-cli/chunks/welcome-FT2AMUWW.js +5 -0
  171. package/dist-cli/chunks/what-happened-FSIDWL32.js +25 -0
  172. package/dist-cli/chunks/{ws-bridge-JXQUAWFA.js → ws-bridge-QXFQLRE7.js} +2 -2
  173. package/dist-lib/agent-daemon-client.cjs +1 -1
  174. package/dist-lib/agent-events.cjs +1 -1
  175. package/dist-lib/agent-identity.cjs +1 -1
  176. package/dist-lib/agent-sessions.cjs +1 -1
  177. package/dist-lib/attached-projects.cjs +1 -1
  178. package/dist-lib/auth/shared-session.cjs +1 -1
  179. package/dist-lib/backend-origin.cjs +1 -1
  180. package/dist-lib/beta.cjs +1 -1
  181. package/dist-lib/beta.mjs +1 -1
  182. package/dist-lib/bridge-constants.cjs +1 -1
  183. package/dist-lib/bridge-contract-input.cjs +1 -1
  184. package/dist-lib/bridge-contract-input.mjs +1 -1
  185. package/dist-lib/bridge-contract.cjs +1 -1
  186. package/dist-lib/bridge-contract.mjs +1 -1
  187. package/dist-lib/cli-constants.cjs +1 -1
  188. package/dist-lib/config.cjs +1 -1
  189. package/dist-lib/detox/index.cjs +1 -1
  190. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  191. package/dist-lib/home-paths.cjs +1 -1
  192. package/dist-lib/host/bridge-host.cjs +1 -1
  193. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  194. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  195. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  196. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  197. package/dist-lib/host/websocket-proxy.cjs +1 -1
  198. package/dist-lib/index.cjs +1 -1
  199. package/dist-lib/jump-to-source-babel.cjs +1 -1
  200. package/dist-lib/menu.cjs +1 -1
  201. package/dist-lib/menu.mjs +1 -1
  202. package/dist-lib/metro-production-bundle.cjs +1 -1
  203. package/dist-lib/metro-production-bundle.mjs +1 -1
  204. package/dist-lib/metro.cjs +1 -1
  205. package/dist-lib/profiles.cjs +1 -1
  206. package/dist-lib/public-brand.cjs +1 -1
  207. package/dist-lib/react-native-host-modules.cjs +2 -1
  208. package/dist-lib/react-native-host-modules.mjs +2 -1
  209. package/dist-lib/render-mode.cjs +1 -1
  210. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  211. package/dist-lib/sdk.cjs +1 -1
  212. package/dist-lib/sdk.mjs +1 -1
  213. package/dist-lib/skills.cjs +560 -48
  214. package/dist-lib/vite.cjs +1 -1
  215. package/package.json +1 -1
  216. package/src/react-native-host-modules.ts +1 -0
  217. package/dist-cli/chunks/app-fonts-MDD3W4VN.js +0 -4
  218. package/dist-cli/chunks/auth-SLPHDPCF.js +0 -12
  219. package/dist-cli/chunks/chunk-2YK5J2D2.js +0 -24
  220. package/dist-cli/chunks/chunk-42TLK75A.js +0 -497
  221. package/dist-cli/chunks/chunk-57LITN73.js +0 -7
  222. package/dist-cli/chunks/chunk-T4ZZ26A2.js +0 -28
  223. package/dist-cli/chunks/chunk-UXROQPIN.js +0 -30
  224. package/dist-cli/chunks/chunk-YSHJ3LMQ.js +0 -74
  225. package/dist-cli/chunks/chunk-YYPTSF6W.js +0 -99
  226. package/dist-cli/chunks/cli-version-DMXIXBIM.js +0 -4
  227. package/dist-cli/chunks/compat-MGBKGYGQ.js +0 -20
  228. package/dist-cli/chunks/config-TRDOMP4T.js +0 -13
  229. package/dist-cli/chunks/control-5GRBZVYK.js +0 -4
  230. package/dist-cli/chunks/daemon-MBT5E3ZC.js +0 -4
  231. package/dist-cli/chunks/desktop-LDRIXOVV.js +0 -31
  232. package/dist-cli/chunks/detox-6HAKYISO.js +0 -52
  233. package/dist-cli/chunks/device-QM3S7OVU.js +0 -18
  234. package/dist-cli/chunks/drivers-AJL2EVXH.js +0 -4
  235. package/dist-cli/chunks/flow-BDAA6BWL.js +0 -4
  236. package/dist-cli/chunks/help-5EJD6LJF.js +0 -4
  237. package/dist-cli/chunks/inspect-S3LR3R2A.js +0 -4
  238. package/dist-cli/chunks/install-desktop-GXE6K7BJ.js +0 -4
  239. package/dist-cli/chunks/login-ISWDW4WU.js +0 -26
  240. package/dist-cli/chunks/mode-3URDGBZB.js +0 -26
  241. package/dist-cli/chunks/privacy-BF23T67T.js +0 -4
  242. package/dist-cli/chunks/record-2DN3OUE5.js +0 -72
  243. package/dist-cli/chunks/report-issue-D3BQPSLU.js +0 -29
  244. package/dist-cli/chunks/reset-T5CNZ7D3.js +0 -4
  245. package/dist-cli/chunks/resolve-target-KAAS274S.js +0 -4
  246. package/dist-cli/chunks/runtime-AQJDBR3X.js +0 -4
  247. package/dist-cli/chunks/screenshot-layers-DP2QKSMN.js +0 -20
  248. package/dist-cli/chunks/screenshots-capture-MA7P3FUT.js +0 -42
  249. package/dist-cli/chunks/setup-GH7OW4GC.js +0 -4
  250. package/dist-cli/chunks/skills-HPK25R4B.js +0 -50
  251. package/dist-cli/chunks/store-DLKNNM4L.js +0 -4
  252. package/dist-cli/chunks/telemetry-7CWDXEOO.js +0 -4
  253. package/dist-cli/chunks/timeline-IF2PGK7C.js +0 -24
  254. package/dist-cli/chunks/upload-3JFFVCZ4.js +0 -4
  255. package/dist-cli/chunks/welcome-N5V352SE.js +0 -5
  256. package/dist-cli/chunks/what-happened-WXTP6EPR.js +0 -25
@@ -14,6 +14,7 @@
14
14
  // 2 misuse — bad flags, unknown verb (treated as bad by bisect)
15
15
 
16
16
  import { spawn } from 'child_process'
17
+ import { rnxExit } from '../run-rnx'
17
18
 
18
19
  // READ_VERBS lists every command whose stdout we know is valid JSON under
19
20
  // --json. keeping this explicit means `sootsim assert tap 100 200` fails
@@ -403,13 +404,13 @@ function runJq(expr: string, payload: unknown): Promise<string> {
403
404
  export async function runAssert(rawArgs: string[]): Promise<void> {
404
405
  if (rawArgs[0] === '--help' || rawArgs[0] === '-h' || rawArgs.length === 0) {
405
406
  printHelp()
406
- process.exit(rawArgs.length === 0 ? 2 : 0)
407
+ rnxExit(rawArgs.length === 0 ? 2 : 0)
407
408
  }
408
409
 
409
410
  const parsed = parseAssertArgs(rawArgs)
410
411
  if ('error' in parsed) {
411
412
  process.stderr.write(` assert: ${parsed.error}\n`)
412
- process.exit(2)
413
+ rnxExit(2)
413
414
  }
414
415
 
415
416
  const { verbArgs, predicates, withinMs, negate, quiet, allowTimeout } = parsed
@@ -420,7 +421,7 @@ export async function runAssert(rawArgs: string[]): Promise<void> {
420
421
  ` assert: unknown or non-read verb: ${verbArgs.join(' ')}\n` +
421
422
  ` supported: ${Array.from(READ_VERBS).sort().join(', ')}\n`,
422
423
  )
423
- process.exit(2)
424
+ rnxExit(2)
424
425
  }
425
426
 
426
427
  // default assertion when none given is --exists. matches the common
@@ -452,7 +453,7 @@ export async function runAssert(rawArgs: string[]): Promise<void> {
452
453
  const label = exitCode === 1 ? 'fail' : 'skip'
453
454
  process.stderr.write(` assert: ${label} — ${lastVerbSkipReason}\n`)
454
455
  }
455
- process.exit(exitCode)
456
+ rnxExit(exitCode)
456
457
  }
457
458
 
458
459
  let allPass = true
@@ -468,7 +469,7 @@ export async function runAssert(rawArgs: string[]): Promise<void> {
468
469
  }
469
470
  } catch (err: any) {
470
471
  if (!quiet) process.stderr.write(` assert: skip — ${err.message}\n`)
471
- process.exit(125)
472
+ rnxExit(125)
472
473
  }
473
474
 
474
475
  const pass = negate ? !allPass : allPass
@@ -478,7 +479,7 @@ export async function runAssert(rawArgs: string[]): Promise<void> {
478
479
  ` assert: pass${attempts > 1 ? ` (attempt ${attempts})` : ''} — ${reasons.join('; ')}\n`,
479
480
  )
480
481
  }
481
- process.exit(0)
482
+ rnxExit(0)
482
483
  }
483
484
 
484
485
  lastFailReason = reasons.join('; ')
@@ -491,7 +492,7 @@ export async function runAssert(rawArgs: string[]): Promise<void> {
491
492
  ` assert: fail${attempts > 1 ? ` after ${attempts} attempt(s)` : ''} — ${lastFailReason}\n`,
492
493
  )
493
494
  }
494
- process.exit(1)
495
+ rnxExit(1)
495
496
  }
496
497
  }
497
498
 
@@ -1,5 +1,6 @@
1
1
  import { refreshSharedDesktopAuthSession } from '../../src/auth/shared-session'
2
2
  import { resolveCliAuth } from '../auth'
3
+ import { rnxExit } from '../run-rnx'
3
4
 
4
5
  function printHelp(): void {
5
6
  console.log(`
@@ -55,5 +56,5 @@ export async function runAuth(args: string[]): Promise<void> {
55
56
 
56
57
  console.error(` unknown auth command: ${sub}`)
57
58
  printHelp()
58
- process.exit(1)
59
+ rnxExit(1)
59
60
  }
@@ -1,7 +1,6 @@
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.
1
+ // ProjectFilePlane over a real local checkout — the second host behind the
2
+ // seam Contrast's browser already implements. writes land on the user's actual
3
+ // disk, so nothing here converts content: the plane reads and writes bytes.
5
4
  //
6
5
  // the file set comes from `git ls-files -co --exclude-standard`, which is the
7
6
  // project's own definition of what belongs to it. a hardcoded skip list was
@@ -20,14 +19,14 @@ const execFileAsync = promisify(execFile)
20
19
 
21
20
  export class CheckoutRequiresGitError extends Error {}
22
21
 
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.
22
+ // a NUL byte is the usual "this is not text" test. it is published as metadata
23
+ // rather than acted on here: moving a binary file is byte-exact and perfectly
24
+ // fine, and so is deleting one, but a shell command that produced TEXT cannot
25
+ // be allowed to overwrite it, because that text is a lossy decode of the
26
+ // file's own bytes. HarnessProjectFs owns that refusal. detecting it at write
27
+ // time instead is not an option: just-bash's `cat` discards every readFile
28
+ // error and reports "No such file or directory" whatever the error says, so
29
+ // the fact has to be in the index where the filesystem layer can read it.
31
30
  const BINARY_SNIFF_BYTES = 8192
32
31
 
33
32
  function looksBinary(filePath: string, size: number): boolean {
@@ -45,9 +44,24 @@ function looksBinary(filePath: string, size: number): boolean {
45
44
  }
46
45
  }
47
46
 
47
+ // the box re-indexes before every command, so the sniff above runs against the
48
+ // whole project every time a user presses return. on a 14,889-file checkout
49
+ // that is 277-644 ms of a 430-840 ms refresh, and nearly all of it re-reads
50
+ // files that did not change. a file whose size and mtime both match the last
51
+ // look has the same first 8 KB, so remember the answer against those two.
52
+ // `statSync` already returns mtimeMs, and it carries sub-millisecond
53
+ // resolution here, so the window in which a same-size rewrite could hide is
54
+ // narrower than a millisecond rather than git's one-second racy-clean case.
55
+ interface SniffedFile {
56
+ size: number
57
+ mtimeMs: number
58
+ binary: boolean
59
+ }
60
+
48
61
  export class CheckoutFilePlane implements ProjectFilePlane {
49
62
  readonly root: string
50
63
  private entries = new Map<string, ProjectFileMeta>()
64
+ private sniffed = new Map<string, SniffedFile>()
51
65
 
52
66
  constructor(root: string) {
53
67
  this.root = path.resolve(root)
@@ -77,6 +91,9 @@ export class CheckoutFilePlane implements ProjectFilePlane {
77
91
  }
78
92
 
79
93
  const next = new Map<string, ProjectFileMeta>()
94
+ // rebuilt alongside the index rather than pruned, so a deleted file's
95
+ // record cannot outlive it in a box that stays open all day
96
+ const nextSniffed = new Map<string, SniffedFile>()
80
97
  for (const relative of stdout.split('\0')) {
81
98
  if (!relative) continue
82
99
  let stat: fs.Stats
@@ -87,12 +104,22 @@ export class CheckoutFilePlane implements ProjectFilePlane {
87
104
  continue
88
105
  }
89
106
  if (!stat.isFile()) continue
107
+ const seen = this.sniffed.get(relative)
108
+ const binary =
109
+ seen && seen.size === stat.size && seen.mtimeMs === stat.mtimeMs
110
+ ? seen.binary
111
+ : looksBinary(path.join(this.root, relative), stat.size)
112
+ nextSniffed.set(relative, { size: stat.size, mtimeMs: stat.mtimeMs, binary })
90
113
  next.set(relative, {
91
114
  size: stat.size,
92
- readOnly: looksBinary(path.join(this.root, relative), stat.size),
115
+ // a checkout carries no write policy of its own; if the user can write
116
+ // the file, so can the shell
117
+ readOnly: false,
118
+ binary,
93
119
  })
94
120
  }
95
121
  this.entries = next
122
+ this.sniffed = nextSniffed
96
123
  }
97
124
 
98
125
  private absolute(planePath: string): string {
@@ -109,13 +136,19 @@ export class CheckoutFilePlane implements ProjectFilePlane {
109
136
  return fs.promises.readFile(this.absolute(planePath), 'utf8')
110
137
  }
111
138
 
112
- async write(planePath: string, content: string): Promise<void> {
139
+ async readBytes(planePath: string): Promise<Uint8Array> {
140
+ return fs.promises.readFile(this.absolute(planePath))
141
+ }
142
+
143
+ async write(planePath: string, content: Uint8Array): Promise<void> {
113
144
  const absolute = this.absolute(planePath)
114
145
  await fs.promises.mkdir(path.dirname(absolute), { recursive: true })
115
- await fs.promises.writeFile(absolute, content, 'utf8')
146
+ await fs.promises.writeFile(absolute, content)
116
147
  this.entries.set(planePath, {
117
- size: Buffer.byteLength(content, 'utf8'),
148
+ size: content.byteLength,
118
149
  readOnly: false,
150
+ // the bytes are in hand, so the same NUL test costs nothing here
151
+ binary: content.subarray(0, BINARY_SNIFF_BYTES).includes(0),
119
152
  })
120
153
  }
121
154
 
@@ -1,7 +1,7 @@
1
1
  // sootsim compat — scan dependencies with the shared compatibility engine.
2
2
 
3
3
  import { existsSync, readFileSync } from 'node:fs'
4
- import { resolve } from 'node:path'
4
+ import { dirname, join, relative, resolve } from 'node:path'
5
5
  import {
6
6
  COMPAT_RESULT_CAVEAT,
7
7
  formatDetailedScanLines,
@@ -9,6 +9,7 @@ import {
9
9
  scanDeps,
10
10
  type ScanResult,
11
11
  } from '@sootsim/compat/web'
12
+ import { findProjectRoot, scanWorkspace } from './detect'
12
13
 
13
14
  interface PackageManifest {
14
15
  name?: string
@@ -47,7 +48,47 @@ flags:
47
48
  return
48
49
  }
49
50
 
50
- const result = packageName ? scanOnePackage(pkg, packageName) : scanDeps(pkg)
51
+ if (packageName) {
52
+ report(args, scanOnePackage(appDir, pkg, packageName))
53
+ return
54
+ }
55
+
56
+ const here = scanDeps(installedManifest(appDir, pkg))
57
+ if (here.packages.length > 0 || appIndex >= 0) {
58
+ report(args, here)
59
+ return
60
+ }
61
+
62
+ // a monorepo root manifest is not the app. it carries react-native and expo
63
+ // so the workspace resolves and little else, so scanning it reports a
64
+ // project with nothing to check while the real app sits in a subdirectory.
65
+ const workspaceRoot = findProjectRoot(appDir)
66
+ const elsewhere = scanWorkspace(workspaceRoot).apps.flatMap((app) => {
67
+ if (resolve(app.dir) === appDir) return []
68
+ const manifest = readPackageJson(app.dir)
69
+ if (!manifest) return []
70
+ const result = scanDeps(installedManifest(app.dir, manifest))
71
+ return result.packages.length > 0 ? [{ dir: app.dir, result }] : []
72
+ })
73
+
74
+ if (elsewhere.length === 1) {
75
+ const only = elsewhere[0]
76
+ console.log(`\n scanning ${relative(appDir, only.dir) || '.'}`)
77
+ report(args, only.result)
78
+ return
79
+ }
80
+ if (elsewhere.length > 1) {
81
+ console.log(`\n this workspace has more than one app. scan one with --app:`)
82
+ for (const entry of elsewhere) {
83
+ console.log(` rnx compat --app ${relative(appDir, entry.dir)}`)
84
+ }
85
+ console.log()
86
+ return
87
+ }
88
+ report(args, here)
89
+ }
90
+
91
+ function report(args: string[], result: ScanResult): void {
51
92
  if (args.includes('--json')) {
52
93
  console.log(JSON.stringify(result, null, 2))
53
94
  return
@@ -73,6 +114,47 @@ flags:
73
114
  console.log(`\n ${COMPAT_RESULT_CAVEAT}\n`)
74
115
  }
75
116
 
117
+ // the declared range is often not a version: pnpm and bun catalogs write
118
+ // `catalog:`, a workspace dependency writes `workspace:*`, and a pinned tarball
119
+ // writes a URL. none of those match a verified version range, so a whole app
120
+ // reports as unverified. the installed tree holds the version that will
121
+ // actually run, so ask it, and keep the declared range for anything the project
122
+ // has not installed.
123
+ function installedManifest(appDir: string, pkg: PackageManifest): PackageManifest {
124
+ const resolveVersion = (name: string, declared: string): string => {
125
+ let dir = appDir
126
+ while (true) {
127
+ const manifest = join(dir, 'node_modules', name, 'package.json')
128
+ if (existsSync(manifest)) {
129
+ try {
130
+ const parsed: unknown = JSON.parse(readFileSync(manifest, 'utf8'))
131
+ if (parsed && typeof parsed === 'object' && 'version' in parsed) {
132
+ const version = parsed.version
133
+ if (typeof version === 'string') return version
134
+ }
135
+ } catch {}
136
+ return declared
137
+ }
138
+ const parent = dirname(dir)
139
+ if (parent === dir) return declared
140
+ dir = parent
141
+ }
142
+ }
143
+ const mapped = (deps: Record<string, string> | undefined) =>
144
+ deps &&
145
+ Object.fromEntries(
146
+ Object.entries(deps).map(([name, declared]) => [
147
+ name,
148
+ resolveVersion(name, declared),
149
+ ]),
150
+ )
151
+ return {
152
+ name: pkg.name,
153
+ dependencies: mapped(pkg.dependencies),
154
+ devDependencies: mapped(pkg.devDependencies),
155
+ }
156
+ }
157
+
76
158
  function readPackageJson(appDir: string): PackageManifest | null {
77
159
  const packagePath = resolve(appDir, 'package.json')
78
160
  if (!existsSync(packagePath)) return null
@@ -80,8 +162,15 @@ function readPackageJson(appDir: string): PackageManifest | null {
80
162
  return parsed && typeof parsed === 'object' ? parsed : null
81
163
  }
82
164
 
83
- function scanOnePackage(pkg: PackageManifest, packageName: string): ScanResult {
165
+ function scanOnePackage(
166
+ appDir: string,
167
+ pkg: PackageManifest,
168
+ packageName: string,
169
+ ): ScanResult {
170
+ const installed = installedManifest(appDir, pkg)
84
171
  const version =
85
- pkg.dependencies?.[packageName] ?? pkg.devDependencies?.[packageName] ?? '*'
172
+ installed.dependencies?.[packageName] ??
173
+ installed.devDependencies?.[packageName] ??
174
+ '*'
86
175
  return scanDeps({ dependencies: { [packageName]: version } })
87
176
  }
@@ -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,
@@ -477,18 +478,18 @@ function parseOpenRuntimeConfig(args: string[]): SootSimConfig | undefined {
477
478
  const raw = args[i + 1]
478
479
  if (!raw) {
479
480
  console.error(' rnx open: --replace expects <module>=<file>')
480
- process.exit(1)
481
+ rnxExit(1)
481
482
  }
482
483
  const eqIndex = raw.indexOf('=')
483
484
  if (eqIndex <= 0 || eqIndex === raw.length - 1) {
484
485
  console.error(' rnx open: --replace expects <module>=<file>')
485
- process.exit(1)
486
+ rnxExit(1)
486
487
  }
487
488
  const moduleName = raw.slice(0, eqIndex).trim()
488
489
  const filePath = normalizeReplacementPath(raw.slice(eqIndex + 1).trim())
489
490
  if (!fs.existsSync(filePath)) {
490
491
  console.error(` rnx open: replacement file not found: ${filePath}`)
491
- process.exit(1)
492
+ rnxExit(1)
492
493
  }
493
494
  modules[moduleName] = { file: filePath }
494
495
  i++
@@ -502,7 +503,7 @@ function parseOpenRuntimeConfig(args: string[]): SootSimConfig | undefined {
502
503
  const eqIndex = raw ? raw.indexOf('=') : -1
503
504
  if (!raw || eqIndex <= 0 || eqIndex === raw.length - 1) {
504
505
  console.error(' rnx open: --remap expects <host:port>=<host:port>')
505
- process.exit(1)
506
+ rnxExit(1)
506
507
  }
507
508
  remap[raw.slice(0, eqIndex).trim()] = raw.slice(eqIndex + 1).trim()
508
509
  i++
@@ -1338,7 +1339,7 @@ export async function runLinkCommand(args: string[], opts: ControlCommandOptions
1338
1339
  if (parsed.positional.length !== 1) {
1339
1340
  console.error(' rnx open: expected exactly one <url-or-route>')
1340
1341
  console.error(' examples: rnx open /settings | rnx open myapp://settings')
1341
- process.exit(1)
1342
+ rnxExit(1)
1342
1343
  }
1343
1344
 
1344
1345
  const bridge = createBridgeFromParsed(parsed)
@@ -1353,7 +1354,7 @@ export async function runLinkCommand(args: string[], opts: ControlCommandOptions
1353
1354
  warningsCommand: `rnx get warnings 5${simHint}`,
1354
1355
  requestsCommand: `rnx get requests 5${simHint}`,
1355
1356
  })
1356
- process.exit(1)
1357
+ rnxExit(1)
1357
1358
  } finally {
1358
1359
  bridge.close()
1359
1360
  }
@@ -1397,11 +1398,11 @@ export async function runOpenCommand(
1397
1398
  console.error(
1398
1399
  ` rnx open: --cdp-port must be a positive port number, got "${cdpPortArg}"`,
1399
1400
  )
1400
- process.exit(1)
1401
+ rnxExit(1)
1401
1402
  }
1402
1403
  if (requestedProfileId && ephemeralProfile) {
1403
1404
  console.error(' rnx open: --profile cannot be combined with --ephemeral')
1404
- process.exit(1)
1405
+ rnxExit(1)
1405
1406
  }
1406
1407
  const profileId = requestedProfileId ? ensureProfile(requestedProfileId).id : undefined
1407
1408
  const openInNewSim = args.includes('--new') || !!profileId || ephemeralProfile
@@ -1410,7 +1411,7 @@ export async function runOpenCommand(
1410
1411
  console.error(
1411
1412
  ' rnx open: --new, --profile, and --ephemeral cannot be combined with --sim',
1412
1413
  )
1413
- process.exit(1)
1414
+ rnxExit(1)
1414
1415
  }
1415
1416
  const target = parsed.positional[0] || ''
1416
1417
 
@@ -1419,7 +1420,7 @@ export async function runOpenCommand(
1419
1420
  console.error(
1420
1421
  ' rnx open: deep links target the current app; remove --new, --profile, or --ephemeral',
1421
1422
  )
1422
- process.exit(1)
1423
+ rnxExit(1)
1423
1424
  }
1424
1425
  await runLinkCommand(args, opts)
1425
1426
  return
@@ -1447,7 +1448,7 @@ export async function runOpenCommand(
1447
1448
  ` could not launch a browser: ${err instanceof Error ? err.message : String(err)}`,
1448
1449
  )
1449
1450
  console.error(` open it yourself: ${target}`)
1450
- process.exit(1)
1451
+ rnxExit(1)
1451
1452
  }
1452
1453
  console.log(
1453
1454
  isLocal
@@ -1492,7 +1493,7 @@ export async function runOpenCommand(
1492
1493
  const requestedDriverId = args.find((_, i) => args[i - 1] === '--driver') || ''
1493
1494
  if (requestedDriverId && !isDriverId(requestedDriverId)) {
1494
1495
  console.error(` unknown driver "${requestedDriverId}" — run \`rnx list --drivers\``)
1495
- process.exit(1)
1496
+ rnxExit(1)
1496
1497
  }
1497
1498
  const driverId: DriverId = isDriverId(requestedDriverId)
1498
1499
  ? requestedDriverId
@@ -1506,7 +1507,7 @@ export async function runOpenCommand(
1506
1507
  const driverHeadless = resolveDriverHeadless(args, driverId)
1507
1508
  if (cdpPort && driverId !== 'playwright') {
1508
1509
  console.error(' rnx open: --cdp-port is only honored by the playwright driver')
1509
- process.exit(1)
1510
+ rnxExit(1)
1510
1511
  }
1511
1512
 
1512
1513
  // metro HMR mode. interactive opens default to hot=true so edits hot-apply
@@ -1520,7 +1521,7 @@ export async function runOpenCommand(
1520
1521
  const noHmrFlag = args.includes('--no-hmr')
1521
1522
  if (hotFlag && noHmrFlag) {
1522
1523
  console.error(' rnx open: --hot and --no-hmr cannot be combined')
1523
- process.exit(1)
1524
+ rnxExit(1)
1524
1525
  }
1525
1526
  const hotMode = hotFlag ? true : noHmrFlag ? false : driverId !== 'playwright'
1526
1527
 
@@ -1532,7 +1533,7 @@ export async function runOpenCommand(
1532
1533
  if (deviceArg && !(deviceArg in devices)) {
1533
1534
  console.error(` rnx open: unknown device "${deviceArg}"`)
1534
1535
  console.error(` known devices: ${listSelectableDeviceModels().join(', ')}`)
1535
- process.exit(1)
1536
+ rnxExit(1)
1536
1537
  }
1537
1538
  const deviceSpec = deviceArg ? devices[deviceArg as DeviceModel] : undefined
1538
1539
 
@@ -1544,7 +1545,7 @@ export async function runOpenCommand(
1544
1545
  const viewportMatch = viewportArg ? /^(\d+)x(\d+)$/.exec(viewportArg) : null
1545
1546
  if (viewportArg && !viewportMatch) {
1546
1547
  console.error(` rnx open: --viewport must be <width>x<height>, got "${viewportArg}"`)
1547
- process.exit(1)
1548
+ rnxExit(1)
1548
1549
  }
1549
1550
  const explicitViewport = viewportMatch
1550
1551
  ? { width: Number(viewportMatch[1]), height: Number(viewportMatch[2]) }
@@ -1662,7 +1663,7 @@ export async function runOpenCommand(
1662
1663
  warningsCommand: `rnx get warnings 5${simHint}`,
1663
1664
  requestsCommand: `rnx get requests 5${simHint}`,
1664
1665
  })
1665
- process.exit(1)
1666
+ rnxExit(1)
1666
1667
  }
1667
1668
 
1668
1669
  if (!fallBackToNewSim && targetSim) {
@@ -1674,7 +1675,7 @@ export async function runOpenCommand(
1674
1675
  )
1675
1676
  if (!ready) {
1676
1677
  console.error(' timed out waiting for current sim to load target')
1677
- process.exit(1)
1678
+ rnxExit(1)
1678
1679
  }
1679
1680
  ready.bridge.close()
1680
1681
  saveCurrentSimId(targetSim.id)
@@ -1706,7 +1707,7 @@ export async function runOpenCommand(
1706
1707
  console.error(
1707
1708
  ' could not resolve a stable owning process; refusing unsupervised sim host',
1708
1709
  )
1709
- process.exit(1)
1710
+ rnxExit(1)
1710
1711
  }
1711
1712
  const simMatchesToken = (sim: BridgeSimInfo) =>
1712
1713
  sim.url?.includes(`inspectOpen=${token}`) ?? false
@@ -1714,7 +1715,7 @@ export async function runOpenCommand(
1714
1715
  const driver = getDriver(driverId)
1715
1716
  if (!driver) {
1716
1717
  console.error(` unknown driver "${driverId}" — run \`rnx list --drivers\``)
1717
- process.exit(1)
1718
+ rnxExit(1)
1718
1719
  }
1719
1720
  const connectWait = resolveDriverConnectWaitOptions()
1720
1721
  const result = await driver.launch({
@@ -1738,7 +1739,7 @@ export async function runOpenCommand(
1738
1739
  })
1739
1740
  if (!result.launched) {
1740
1741
  console.error(` ${driver.name} driver: ${result.message}`)
1741
- process.exit(1)
1742
+ rnxExit(1)
1742
1743
  }
1743
1744
  const outcome = await waitForSimMatchOrHostExit(
1744
1745
  parsed.wsPort,
@@ -1750,7 +1751,7 @@ export async function runOpenCommand(
1750
1751
  if (outcome.kind === 'host-exit') {
1751
1752
  printDriverDiagnosticLogTail(result)
1752
1753
  console.error(` ${driver.name} host crashed — sim is no longer attached`)
1753
- process.exit(1)
1754
+ rnxExit(1)
1754
1755
  }
1755
1756
  if (outcome.kind === 'timeout') {
1756
1757
  if (result.pid) {
@@ -1763,7 +1764,7 @@ export async function runOpenCommand(
1763
1764
  console.error(
1764
1765
  ` timed out after ${connectWait.timeoutMs}ms waiting for opened sim to connect`,
1765
1766
  )
1766
- process.exit(1)
1767
+ rnxExit(1)
1767
1768
  }
1768
1769
  const match = outcome.sim
1769
1770
  saveCurrentSimId(match.id)
@@ -1801,7 +1802,7 @@ async function runUseLikeCommand(args: string[], opts: ControlCommandOptions = {
1801
1802
  ` refused: ${target.id} is locked by the active user (${secs}s) — that's a live human tab`,
1802
1803
  )
1803
1804
  console.error(` run \`rnx open --new\` for a fresh investigation sim`)
1804
- process.exit(1)
1805
+ rnxExit(1)
1805
1806
  }
1806
1807
  if (!force) {
1807
1808
  // another CLI agent leases it. don't hijack its target silently;
@@ -1813,20 +1814,21 @@ async function runUseLikeCommand(args: string[], opts: ControlCommandOptions = {
1813
1814
  console.error(
1814
1815
  ` \`sootsim use ${target.id} --force\` to take it, or \`rnx open --new\` for a fresh sim`,
1815
1816
  )
1816
- process.exit(1)
1817
+ rnxExit(1)
1817
1818
  }
1818
1819
  }
1819
1820
  await bridge.focusSim(target.id)
1820
1821
  saveCurrentSimId(target.id)
1821
1822
  console.log(` using: ${target.id}`)
1822
1823
  } catch (err) {
1824
+ rethrowIfExit(err)
1823
1825
  console.error(` use failed: ${err instanceof Error ? err.message : String(err)}`)
1824
1826
  await printBridgeFailureDiagnostics(bridge, {
1825
1827
  errorsCommand: `rnx get errors 5${simHint}`,
1826
1828
  warningsCommand: `rnx get warnings 5${simHint}`,
1827
1829
  requestsCommand: `rnx get requests 5${simHint}`,
1828
1830
  })
1829
- process.exit(1)
1831
+ rnxExit(1)
1830
1832
  }
1831
1833
  } finally {
1832
1834
  bridge.close()
@@ -1871,10 +1873,10 @@ export async function runClaimCommand(args: string[], opts: ControlCommandOption
1871
1873
  const secs = Math.max(0, Math.round(err.lock.expiresInMs / 1000))
1872
1874
  console.error(` claim failed: locked by ${err.lock.by} for ${secs}s more`)
1873
1875
  console.error(` use --force to take it, or \`rnx open --new\` for a fresh sim`)
1874
- process.exit(1)
1876
+ rnxExit(1)
1875
1877
  }
1876
1878
  console.error(` claim failed: ${err instanceof Error ? err.message : String(err)}`)
1877
- process.exit(1)
1879
+ rnxExit(1)
1878
1880
  }
1879
1881
  } finally {
1880
1882
  bridge.close()
@@ -2016,7 +2018,7 @@ export async function runCloseCommand(args: string[], opts: ControlCommandOption
2016
2018
  console.error(
2017
2019
  ` close failed: keep sim ${plan.missingExplicitKeepId} is not connected; not closing other sims`,
2018
2020
  )
2019
- process.exit(1)
2021
+ rnxExit(1)
2020
2022
  }
2021
2023
  if (plan.targets.length === 0) {
2022
2024
  console.log(
@@ -2047,11 +2049,12 @@ export async function runCloseCommand(args: string[], opts: ControlCommandOption
2047
2049
  console.log(summary)
2048
2050
  if (result.remaining.length > 0) {
2049
2051
  console.error(` close failed: still connected: ${result.remaining.join(', ')}`)
2050
- process.exit(1)
2052
+ rnxExit(1)
2051
2053
  }
2052
2054
  } catch (err) {
2055
+ rethrowIfExit(err)
2053
2056
  console.error(` close failed: ${err instanceof Error ? err.message : String(err)}`)
2054
- process.exit(1)
2057
+ rnxExit(1)
2055
2058
  } finally {
2056
2059
  bridge.close()
2057
2060
  }
@@ -2074,7 +2077,7 @@ export async function runCloseCommand(args: string[], opts: ControlCommandOption
2074
2077
  if (!closed) {
2075
2078
  await terminatePlaywrightHostsForSims(sims, [target.id])
2076
2079
  console.error(` close failed: ${target.id} is still connected`)
2077
- process.exit(1)
2080
+ rnxExit(1)
2078
2081
  }
2079
2082
  await terminatePlaywrightHostsForSims(sims, [target.id])
2080
2083
  if (readCurrentSimId() === target.id) {
@@ -2083,13 +2086,14 @@ export async function runCloseCommand(args: string[], opts: ControlCommandOption
2083
2086
  }
2084
2087
  console.log(` closed: ${target.id}`)
2085
2088
  } catch (err) {
2089
+ rethrowIfExit(err)
2086
2090
  console.error(` close failed: ${err instanceof Error ? err.message : String(err)}`)
2087
2091
  await printBridgeFailureDiagnostics(bridge, {
2088
2092
  errorsCommand: `rnx get errors 5${simHint}`,
2089
2093
  warningsCommand: `rnx get warnings 5${simHint}`,
2090
2094
  requestsCommand: `rnx get requests 5${simHint}`,
2091
2095
  })
2092
- process.exit(1)
2096
+ rnxExit(1)
2093
2097
  }
2094
2098
  } finally {
2095
2099
  bridge.close()