rnxsim 0.1.312 → 0.1.314

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/LICENSE +11 -10
  2. package/README.md +5 -0
  3. package/cli/app-config.ts +65 -0
  4. package/cli/app-fonts.ts +408 -0
  5. package/cli/app-project.ts +231 -0
  6. package/cli/app-splash.ts +185 -0
  7. package/cli/app-state-reset.ts +24 -0
  8. package/cli/auth.ts +155 -0
  9. package/cli/bin.ts +594 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2830 -0
  12. package/cli/browser-evals.ts +96 -0
  13. package/cli/commands/agent-wrapper.ts +986 -0
  14. package/cli/commands/agent.ts +423 -0
  15. package/cli/commands/app-fonts.ts +98 -0
  16. package/cli/commands/assert.ts +541 -0
  17. package/cli/commands/auth.ts +59 -0
  18. package/cli/commands/camera.ts +266 -0
  19. package/cli/commands/cleanup.ts +169 -0
  20. package/cli/commands/compat.ts +87 -0
  21. package/cli/commands/config.ts +32 -0
  22. package/cli/commands/control.ts +2142 -0
  23. package/cli/commands/cpu-profile.ts +269 -0
  24. package/cli/commands/daemon-mac-app.ts +169 -0
  25. package/cli/commands/daemon.ts +874 -0
  26. package/cli/commands/debug.ts +719 -0
  27. package/cli/commands/desktop.ts +39 -0
  28. package/cli/commands/detect.ts +197 -0
  29. package/cli/commands/detox.ts +385 -0
  30. package/cli/commands/device.ts +133 -0
  31. package/cli/commands/diagnose.ts +589 -0
  32. package/cli/commands/electron.ts +95 -0
  33. package/cli/commands/film.ts +379 -0
  34. package/cli/commands/flow.ts +1124 -0
  35. package/cli/commands/inspect/actions.ts +622 -0
  36. package/cli/commands/inspect/core.ts +2405 -0
  37. package/cli/commands/inspect/count.ts +17 -0
  38. package/cli/commands/inspect/describe.ts +192 -0
  39. package/cli/commands/inspect/env.ts +23 -0
  40. package/cli/commands/inspect/find.ts +171 -0
  41. package/cli/commands/inspect/get-layout.ts +39 -0
  42. package/cli/commands/inspect/keyboard.ts +52 -0
  43. package/cli/commands/inspect/list.ts +58 -0
  44. package/cli/commands/inspect/memory.ts +215 -0
  45. package/cli/commands/inspect/redaction.ts +39 -0
  46. package/cli/commands/inspect/resolve-target.ts +82 -0
  47. package/cli/commands/inspect/screens.ts +78 -0
  48. package/cli/commands/inspect/settle.ts +22 -0
  49. package/cli/commands/inspect/settling.ts +158 -0
  50. package/cli/commands/inspect/shared.ts +353 -0
  51. package/cli/commands/inspect/sleep.ts +14 -0
  52. package/cli/commands/inspect/tree.ts +32 -0
  53. package/cli/commands/inspect/url.ts +17 -0
  54. package/cli/commands/inspect/wait-event.ts +210 -0
  55. package/cli/commands/inspect/wait-idle.ts +24 -0
  56. package/cli/commands/inspect/wait-ready.ts +74 -0
  57. package/cli/commands/inspect/wait-selector.ts +54 -0
  58. package/cli/commands/inspect/wait.ts +31 -0
  59. package/cli/commands/inspect.ts +4519 -0
  60. package/cli/commands/install-desktop.ts +351 -0
  61. package/cli/commands/login.ts +331 -0
  62. package/cli/commands/logout.ts +31 -0
  63. package/cli/commands/maestro-generate.ts +361 -0
  64. package/cli/commands/maestro.ts +453 -0
  65. package/cli/commands/mode.ts +57 -0
  66. package/cli/commands/no-bridge-hint.ts +80 -0
  67. package/cli/commands/perf.ts +66 -0
  68. package/cli/commands/permissions.ts +203 -0
  69. package/cli/commands/profile.ts +108 -0
  70. package/cli/commands/react.ts +353 -0
  71. package/cli/commands/record.ts +1434 -0
  72. package/cli/commands/report-issue.ts +305 -0
  73. package/cli/commands/reset.ts +85 -0
  74. package/cli/commands/runtime.ts +351 -0
  75. package/cli/commands/screenshot-command.ts +106 -0
  76. package/cli/commands/screenshot-layers.ts +143 -0
  77. package/cli/commands/screenshot-mode.ts +37 -0
  78. package/cli/commands/screenshot.ts +488 -0
  79. package/cli/commands/screenshots-capture.ts +607 -0
  80. package/cli/commands/screenshots.ts +127 -0
  81. package/cli/commands/serve.ts +168 -0
  82. package/cli/commands/setup.ts +545 -0
  83. package/cli/commands/shell-boolean-mode.ts +81 -0
  84. package/cli/commands/skills.ts +467 -0
  85. package/cli/commands/slides.ts +361 -0
  86. package/cli/commands/state.ts +87 -0
  87. package/cli/commands/storage.ts +58 -0
  88. package/cli/commands/telemetry.ts +54 -0
  89. package/cli/commands/three-mode.ts +763 -0
  90. package/cli/commands/timeline.ts +122 -0
  91. package/cli/commands/upgrade.ts +208 -0
  92. package/cli/commands/upload.ts +1225 -0
  93. package/cli/commands/version.ts +54 -0
  94. package/cli/commands/what-happened.ts +327 -0
  95. package/cli/current-sim.ts +204 -0
  96. package/cli/desktop-companion.ts +300 -0
  97. package/cli/drivers/electron.ts +70 -0
  98. package/cli/drivers/index.ts +20 -0
  99. package/cli/drivers/playwright-provisioning.ts +180 -0
  100. package/cli/drivers/playwright.ts +698 -0
  101. package/cli/drivers/registry.ts +65 -0
  102. package/cli/drivers/types.ts +102 -0
  103. package/cli/flow-file.ts +142 -0
  104. package/cli/flow-live-status.ts +120 -0
  105. package/cli/flow-session.ts +187 -0
  106. package/cli/help.ts +80 -0
  107. package/cli/hidden-runtime-alias.ts +19 -0
  108. package/cli/hints.ts +216 -0
  109. package/cli/inspect-notice-state.ts +114 -0
  110. package/cli/maestro-js.ts +334 -0
  111. package/cli/open-url.ts +8 -0
  112. package/cli/parent-pid.ts +204 -0
  113. package/cli/parse-args.ts +211 -0
  114. package/cli/prompt.ts +51 -0
  115. package/cli/recording-access.ts +107 -0
  116. package/cli/registry.ts +1 -0
  117. package/cli/resolve-assets.ts +63 -0
  118. package/cli/run-registry.ts +226 -0
  119. package/cli/runtime-notes.ts +66 -0
  120. package/cli/runtime-summary.ts +25 -0
  121. package/cli/setup-repository.ts +187 -0
  122. package/cli/telemetry.ts +187 -0
  123. package/cli/ws-bridge.ts +798 -0
  124. package/dist-cli/bin.js +5 -5
  125. package/dist-cli/chunks/{agent-NXVDDCFG.js → agent-7YBDCYMA.js} +2 -2
  126. package/dist-cli/chunks/{agent-wrapper-DXJCFKL3.js → agent-wrapper-2GHFBHCR.js} +2 -2
  127. package/dist-cli/chunks/{app-fonts-W5AJZQJF.js → app-fonts-RSNVPQSU.js} +2 -2
  128. package/dist-cli/chunks/{assert-3LOJEUDT.js → assert-XCMX3XJX.js} +2 -2
  129. package/dist-cli/chunks/{auth-MFTYYPNX.js → auth-B442HRAX.js} +2 -2
  130. package/dist-cli/chunks/{beta-GT3NN67B.js → beta-XJ55JK3M.js} +2 -2
  131. package/dist-cli/chunks/camera-UGYSSLIK.js +33 -0
  132. package/dist-cli/chunks/{chunk-EUAVBTKU.js → chunk-277AEQZX.js} +2 -2
  133. package/dist-cli/chunks/{chunk-5OX7ANRA.js → chunk-2JNSK774.js} +2 -2
  134. package/dist-cli/chunks/{chunk-L24Z3CRS.js → chunk-32WOTSTR.js} +3 -3
  135. package/dist-cli/chunks/{chunk-BKNOLP3J.js → chunk-3S753SNQ.js} +2 -2
  136. package/dist-cli/chunks/{chunk-N6A3SXMR.js → chunk-46ZOLOYA.js} +2 -2
  137. package/dist-cli/chunks/{chunk-GZDHEONW.js → chunk-5L7ELDQL.js} +8 -9
  138. package/dist-cli/chunks/{chunk-YZRFF4YA.js → chunk-7OOPFSQS.js} +2 -2
  139. package/dist-cli/chunks/{chunk-TBMVBI26.js → chunk-7SV3RPRW.js} +2 -2
  140. package/dist-cli/chunks/{chunk-JG6S4RSH.js → chunk-7ZC35MOU.js} +1 -1
  141. package/dist-cli/chunks/chunk-AMG5E6CC.js +9 -0
  142. package/dist-cli/chunks/{chunk-MENV7VLD.js → chunk-APWNH3A4.js} +1 -1
  143. package/dist-cli/chunks/chunk-B57XUKY3.js +4 -0
  144. package/dist-cli/chunks/{chunk-3D57IBPC.js → chunk-C2NL26TD.js} +1 -1
  145. package/dist-cli/chunks/chunk-D2FNUWAB.js +15 -0
  146. package/dist-cli/chunks/{chunk-4BXCZHAU.js → chunk-E5T4XSJ3.js} +2 -2
  147. package/dist-cli/chunks/{chunk-FTC4M26Q.js → chunk-EAC34EQS.js} +1 -1
  148. package/dist-cli/chunks/{chunk-NPVGMH2N.js → chunk-EG32ML36.js} +2 -2
  149. package/dist-cli/chunks/{chunk-DM6KUAOZ.js → chunk-FXUAC6D5.js} +1 -1
  150. package/dist-cli/chunks/chunk-GQSL4USA.js +6 -0
  151. package/dist-cli/chunks/{chunk-U4PN6JOT.js → chunk-HAXW27SS.js} +2 -2
  152. package/dist-cli/chunks/{chunk-ZMZTHU7V.js → chunk-IZAHPAN6.js} +1 -1
  153. package/dist-cli/chunks/{chunk-SI3BUTQR.js → chunk-J62KM5TB.js} +2 -2
  154. package/dist-cli/chunks/{chunk-Y2FSZSO7.js → chunk-JEMCD5E4.js} +1 -1
  155. package/dist-cli/chunks/{chunk-T4FT6CM7.js → chunk-JZS3Q37N.js} +2 -2
  156. package/dist-cli/chunks/{chunk-2SA2VTGP.js → chunk-KR4JON7D.js} +1 -1
  157. package/dist-cli/chunks/chunk-KWCYKQIQ.js +4 -0
  158. package/dist-cli/chunks/{chunk-HG7ORJMT.js → chunk-MCWPL644.js} +2 -2
  159. package/dist-cli/chunks/chunk-MJYK3N2I.js +4 -0
  160. package/dist-cli/chunks/{chunk-YRV7HR3P.js → chunk-O6TRIZNS.js} +2 -2
  161. package/dist-cli/chunks/{chunk-WBT32R6N.js → chunk-P7XL2E73.js} +3 -3
  162. package/dist-cli/chunks/{chunk-MNGQ42QE.js → chunk-PFQTUKQ4.js} +62 -87
  163. package/dist-cli/chunks/{chunk-Q3RGLERJ.js → chunk-PG5RZCTN.js} +2 -2
  164. package/dist-cli/chunks/{chunk-OR3XTXEB.js → chunk-QGRI2Z4M.js} +2 -2
  165. package/dist-cli/chunks/chunk-QLXXE7GE.js +125 -0
  166. package/dist-cli/chunks/{chunk-KZW5WBGU.js → chunk-QOJJJWJE.js} +89 -133
  167. package/dist-cli/chunks/{chunk-BRIZOR4K.js → chunk-RWZY5427.js} +2 -2
  168. package/dist-cli/chunks/{chunk-TYE3WBSR.js → chunk-RZKU2K3J.js} +2 -2
  169. package/dist-cli/chunks/{chunk-7VWOSTD5.js → chunk-SBV4IK4H.js} +1 -1
  170. package/dist-cli/chunks/{chunk-FWYYCUAZ.js → chunk-SGMVFFMK.js} +1 -1
  171. package/dist-cli/chunks/{chunk-LDQ7HVCW.js → chunk-TAX4UT2N.js} +1 -1
  172. package/dist-cli/chunks/{chunk-G74YIEVS.js → chunk-TUOFAWXT.js} +1 -1
  173. package/dist-cli/chunks/{chunk-274UD5FD.js → chunk-UHZLOHGP.js} +1 -1
  174. package/dist-cli/chunks/{chunk-37I5AIS7.js → chunk-VC7V76U3.js} +1 -1
  175. package/dist-cli/chunks/{chunk-B7KMNHX7.js → chunk-VQVMLW4U.js} +1 -1
  176. package/dist-cli/chunks/{chunk-VZBLFMYC.js → chunk-VUKKYPZN.js} +2 -2
  177. package/dist-cli/chunks/{chunk-VUFRB347.js → chunk-WGGRJDRE.js} +1 -1
  178. package/dist-cli/chunks/chunk-X6H76EKP.js +15 -0
  179. package/dist-cli/chunks/chunk-XLZ5FNRT.js +27 -0
  180. package/dist-cli/chunks/{chunk-TCWQJILS.js → chunk-XULEACM4.js} +2 -2
  181. package/dist-cli/chunks/{chunk-XRGEEPJ5.js → chunk-YFSDM7AX.js} +4 -4
  182. package/dist-cli/chunks/chunk-YWI3UEVX.js +5 -0
  183. package/dist-cli/chunks/{chunk-TGQNQ6XT.js → chunk-ZBSJO4NB.js} +10 -9
  184. package/dist-cli/chunks/{cleanup-H3GWQB63.js → cleanup-EYLGCA6Z.js} +2 -2
  185. package/dist-cli/chunks/cli-version-LL2UGIHE.js +4 -0
  186. package/dist-cli/chunks/{compat-5WG2XJV7.js → compat-TLJYHB4E.js} +2 -2
  187. package/dist-cli/chunks/{config-ANMBVVLL.js → config-4JWUOEXK.js} +2 -2
  188. package/dist-cli/chunks/{control-NSGNQFJG.js → control-HPAOYF4N.js} +2 -2
  189. package/dist-cli/chunks/daemon-OYLASXLE.js +4 -0
  190. package/dist-cli/chunks/{debug-CHV2O2KF.js → debug-4BKXF6KI.js} +5 -5
  191. package/dist-cli/chunks/{desktop-FIPIK4CH.js → desktop-ZQ6ZD2S6.js} +3 -3
  192. package/dist-cli/chunks/{detox-CVUJP7K6.js → detox-WNPASSS3.js} +2 -2
  193. package/dist-cli/chunks/{device-6WCUM4W6.js → device-BXLXVG3V.js} +2 -2
  194. package/dist-cli/chunks/{diagnose-I6EMWEII.js → diagnose-3QX7W5RW.js} +2 -2
  195. package/dist-cli/chunks/{disk-cleanup-P27CYFTN.js → disk-cleanup-KSWKI7WB.js} +2 -2
  196. package/dist-cli/chunks/drivers-EPIEFF7P.js +4 -0
  197. package/dist-cli/chunks/{film-UOQFFYNF.js → film-GDMR33VO.js} +3 -3
  198. package/dist-cli/chunks/flow-NKMPBYCJ.js +4 -0
  199. package/dist-cli/chunks/help-OGCBHOKA.js +4 -0
  200. package/dist-cli/chunks/{hidden-runtime-alias-NJYFSJKZ.js → hidden-runtime-alias-S2GTDX3T.js} +2 -2
  201. package/dist-cli/chunks/home-paths-QRCDLTTV.js +4 -0
  202. package/dist-cli/chunks/inspect-FUYMOZPY.js +4 -0
  203. package/dist-cli/chunks/install-desktop-FR6YKW7Y.js +4 -0
  204. package/dist-cli/chunks/{login-BPFQENCX.js → login-IJAPUZHI.js} +4 -4
  205. package/dist-cli/chunks/{logout-5A45ZGY7.js → logout-QMXDFU2X.js} +2 -2
  206. package/dist-cli/chunks/{maestro-E4CSDMLM.js → maestro-ZXU3YCVX.js} +3 -3
  207. package/dist-cli/chunks/{maestro-generate-CKMOJQX2.js → maestro-generate-PYB5QY7K.js} +3 -3
  208. package/dist-cli/chunks/{mode-SZYNMIQF.js → mode-WGUCL5FZ.js} +2 -2
  209. package/dist-cli/chunks/{optional-demo-registry-FRH4SFVG.js → optional-demo-registry-WH2O6H36.js} +2 -2
  210. package/dist-cli/chunks/{perf-VXU2XJAX.js → perf-TOD3UFAH.js} +2 -2
  211. package/dist-cli/chunks/{permissions-IRIAOVND.js → permissions-I5BRJGTB.js} +2 -2
  212. package/dist-cli/chunks/{record-RY7FPLLH.js → record-ZYL2FYSK.js} +3 -3
  213. package/dist-cli/chunks/{report-issue-KNWXOE53.js → report-issue-TAI6DYZO.js} +2 -2
  214. package/dist-cli/chunks/reset-7YGYKQPQ.js +4 -0
  215. package/dist-cli/chunks/runtime-B4JO6QRI.js +4 -0
  216. package/dist-cli/chunks/{screenshot-command-QLALR6GY.js → screenshot-command-7ANLODZY.js} +7 -7
  217. package/dist-cli/chunks/{screenshot-layers-OQFMFVRL.js → screenshot-layers-A7FYXSVU.js} +3 -3
  218. package/dist-cli/chunks/{screenshots-capture-6TSRJFGB.js → screenshots-capture-WT2ZY6CB.js} +2 -2
  219. package/dist-cli/chunks/serve-TG5WKA4V.js +44 -0
  220. package/dist-cli/chunks/{setup-7V6UKX4U.js → setup-PCWLD22V.js} +2 -2
  221. package/dist-cli/chunks/{skills-QQBOSETH.js → skills-27ZHWCQS.js} +2 -2
  222. package/dist-cli/chunks/state-ZQVY46ZO.js +14 -0
  223. package/dist-cli/chunks/{storage-TAPMKK5O.js → storage-OYC57C4X.js} +6 -6
  224. package/dist-cli/chunks/store-32HSPZHI.js +4 -0
  225. package/dist-cli/chunks/telemetry-PBJR7XHR.js +4 -0
  226. package/dist-cli/chunks/{timeline-IAM564NA.js → timeline-RAJJFQT6.js} +2 -2
  227. package/dist-cli/chunks/{upgrade-CD55QXIX.js → upgrade-OEFNZRIP.js} +2 -2
  228. package/dist-cli/chunks/upload-LQJITLKK.js +4 -0
  229. package/dist-cli/chunks/version-NIXTY6PL.js +6 -0
  230. package/dist-cli/chunks/{web-6NW6YLPI.js → web-XBUTBVGR.js} +2 -2
  231. package/dist-cli/chunks/{what-happened-CZZDOOVG.js → what-happened-YNHRFUDX.js} +3 -3
  232. package/dist-lib/agent-daemon-client.cjs +1 -1
  233. package/dist-lib/agent-events.cjs +1 -1
  234. package/dist-lib/agent-identity.cjs +1 -1
  235. package/dist-lib/agent-sessions.cjs +1 -1
  236. package/dist-lib/attached-projects.cjs +1 -1
  237. package/dist-lib/auth/shared-session.cjs +1 -1
  238. package/dist-lib/backend-origin.cjs +1 -1
  239. package/dist-lib/beta.cjs +1 -1
  240. package/dist-lib/beta.mjs +1 -1
  241. package/dist-lib/bridge-constants.cjs +1 -1
  242. package/dist-lib/bridge-contract.cjs +20 -0
  243. package/dist-lib/cli-constants.cjs +1 -1
  244. package/dist-lib/config.cjs +1 -1
  245. package/dist-lib/detox/index.cjs +1 -1
  246. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  247. package/dist-lib/home-paths.cjs +67 -28
  248. package/dist-lib/host/bridge-host.cjs +140 -12
  249. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  250. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  251. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  252. package/dist-lib/host/websocket-proxy.cjs +1 -1
  253. package/dist-lib/index.cjs +2815 -40
  254. package/dist-lib/jump-to-source-babel.cjs +1 -1
  255. package/dist-lib/menu.cjs +1 -1
  256. package/dist-lib/menu.mjs +1 -1
  257. package/dist-lib/metro.cjs +1 -1
  258. package/dist-lib/profiles.cjs +1 -1
  259. package/dist-lib/public-brand.cjs +1 -1
  260. package/dist-lib/render-mode.cjs +1 -1
  261. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  262. package/dist-lib/sdk.cjs +2549 -2061
  263. package/dist-lib/sdk.mjs +2543 -2061
  264. package/dist-lib/skills.cjs +493 -282
  265. package/dist-lib/vite.cjs +1 -1
  266. package/package.json +8 -2
  267. package/src/bridge-constants.ts +3 -4
  268. package/src/bridge-contract.ts +251 -0
  269. package/src/connect.ts +83 -0
  270. package/src/disk-cleanup.ts +30 -0
  271. package/src/home-paths.ts +81 -38
  272. package/src/host/bridge-host.ts +134 -6
  273. package/src/index.ts +27 -1
  274. package/src/sdk.ts +8 -0
  275. package/src/sim-client.ts +660 -0
  276. package/dist-cli/chunks/camera-6EAK7CUL.js +0 -22
  277. package/dist-cli/chunks/chunk-33FKQH54.js +0 -5
  278. package/dist-cli/chunks/chunk-66NF2KNG.js +0 -9
  279. package/dist-cli/chunks/chunk-6UKJB2VC.js +0 -125
  280. package/dist-cli/chunks/chunk-7INBNA3U.js +0 -26
  281. package/dist-cli/chunks/chunk-AY47WKS5.js +0 -23
  282. package/dist-cli/chunks/chunk-HMDOLCKG.js +0 -15
  283. package/dist-cli/chunks/chunk-KR5BGMBZ.js +0 -4
  284. package/dist-cli/chunks/chunk-NLEMQ4PY.js +0 -6
  285. package/dist-cli/chunks/chunk-PEPUGJVI.js +0 -4
  286. package/dist-cli/chunks/cli-version-3QPAJ3LC.js +0 -4
  287. package/dist-cli/chunks/daemon-JHF4BJ3U.js +0 -4
  288. package/dist-cli/chunks/drivers-HQ6AKZT6.js +0 -4
  289. package/dist-cli/chunks/flow-N27MRRNL.js +0 -4
  290. package/dist-cli/chunks/help-SF6FEGEE.js +0 -4
  291. package/dist-cli/chunks/home-paths-XOV44JEF.js +0 -4
  292. package/dist-cli/chunks/inspect-CWFRFQTY.js +0 -4
  293. package/dist-cli/chunks/install-desktop-KLQDNXZQ.js +0 -4
  294. package/dist-cli/chunks/runtime-AR3GJTGK.js +0 -4
  295. package/dist-cli/chunks/serve-F54J2NQO.js +0 -44
  296. package/dist-cli/chunks/store-7NX3S43H.js +0 -4
  297. package/dist-cli/chunks/telemetry-E7BUK6RA.js +0 -4
  298. package/dist-cli/chunks/upload-GZ6LIMYM.js +0 -4
  299. package/dist-cli/chunks/version-3DYJ5AS3.js +0 -6
@@ -0,0 +1,231 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ export const EXPO_APP_CONFIG_NAMES = [
5
+ 'app.config.js',
6
+ 'app.config.ts',
7
+ 'app.config.cjs',
8
+ 'app.config.mjs',
9
+ 'app.json',
10
+ ] as const
11
+
12
+ export const SOOTSIM_APP_CONFIG_NAMES = [
13
+ 'sootsim.config.ts',
14
+ 'sootsim.config.js',
15
+ 'sootsim.config.mjs',
16
+ 'sootsim.config.cjs',
17
+ ] as const
18
+
19
+ const SKIP_DIR_NAMES = new Set([
20
+ 'node_modules',
21
+ 'artifacts',
22
+ 'dist',
23
+ 'build',
24
+ 'out',
25
+ 'coverage',
26
+ 'tmp',
27
+ 'public',
28
+ 'ios',
29
+ 'android',
30
+ 'macos',
31
+ 'windows',
32
+ '__fixtures__',
33
+ '__tests__',
34
+ 'fixtures',
35
+ 'fixture',
36
+ 'examples',
37
+ 'example',
38
+ ])
39
+
40
+ const MAX_SCANNED_DIRECTORIES = 256
41
+ const MAX_SCAN_DEPTH = 4
42
+
43
+ export interface ResolvedAppProject {
44
+ appDir: string
45
+ configPath: string | null
46
+ }
47
+
48
+ function firstNamedFile(dir: string, names: readonly string[]): string | null {
49
+ for (const name of names) {
50
+ const candidate = path.join(dir, name)
51
+ try {
52
+ if (fs.statSync(candidate).isFile()) return candidate
53
+ } catch {}
54
+ }
55
+ return null
56
+ }
57
+
58
+ export function findExpoAppConfig(dir: string): string | null {
59
+ return firstNamedFile(dir, EXPO_APP_CONFIG_NAMES)
60
+ }
61
+
62
+ export function findSootSimAppConfig(dir: string): string | null {
63
+ return firstNamedFile(dir, SOOTSIM_APP_CONFIG_NAMES)
64
+ }
65
+
66
+ function readWorkspacePatterns(rootDir: string): string[] {
67
+ try {
68
+ const value = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'))
69
+ if (!value || typeof value !== 'object') return []
70
+ const workspaces = Reflect.get(value, 'workspaces')
71
+ if (Array.isArray(workspaces)) {
72
+ return workspaces.filter((entry): entry is string => typeof entry === 'string')
73
+ }
74
+ if (!workspaces || typeof workspaces !== 'object') return []
75
+ const packages = Reflect.get(workspaces, 'packages')
76
+ return Array.isArray(packages)
77
+ ? packages.filter((entry): entry is string => typeof entry === 'string')
78
+ : []
79
+ } catch {
80
+ return []
81
+ }
82
+ }
83
+
84
+ interface DirectoryScan {
85
+ directories: string[]
86
+ complete: boolean
87
+ }
88
+
89
+ function workspaceDirectories(rootDir: string): DirectoryScan {
90
+ const directories = new Set<string>()
91
+ let expanded = 0
92
+ let complete = true
93
+ for (const pattern of readWorkspacePatterns(rootDir)) {
94
+ if (expanded >= MAX_SCANNED_DIRECTORIES) {
95
+ complete = false
96
+ break
97
+ }
98
+ if (!pattern || path.isAbsolute(pattern)) continue
99
+ const segments = pattern.split(/[\\/]+/).filter(Boolean)
100
+ if (segments.length === 0 || segments.length > MAX_SCAN_DEPTH) continue
101
+ if (segments.some((segment) => segment === '..' || segment === '**')) continue
102
+
103
+ let current = [rootDir]
104
+ for (const segment of segments) {
105
+ const next: string[] = []
106
+ for (const parent of current) {
107
+ if (expanded >= MAX_SCANNED_DIRECTORIES) {
108
+ complete = false
109
+ break
110
+ }
111
+ expanded++
112
+ if (segment === '*') {
113
+ let entries: fs.Dirent[]
114
+ try {
115
+ entries = fs.readdirSync(parent, { withFileTypes: true })
116
+ } catch {
117
+ continue
118
+ }
119
+ for (const entry of entries) {
120
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue
121
+ if (entry.name.startsWith('.') || SKIP_DIR_NAMES.has(entry.name)) continue
122
+ next.push(path.join(parent, entry.name))
123
+ }
124
+ } else if (!segment.includes('*')) {
125
+ const candidate = path.join(parent, segment)
126
+ try {
127
+ const stat = fs.lstatSync(candidate)
128
+ if (stat.isDirectory() && !stat.isSymbolicLink()) next.push(candidate)
129
+ } catch {}
130
+ }
131
+ }
132
+ current = next
133
+ if (current.length > MAX_SCANNED_DIRECTORIES) {
134
+ current.length = MAX_SCANNED_DIRECTORIES
135
+ complete = false
136
+ }
137
+ }
138
+ for (const directory of current) directories.add(directory)
139
+ }
140
+ return { directories: [...directories], complete }
141
+ }
142
+
143
+ function boundedConfigDirectories(rootDir: string): DirectoryScan {
144
+ const queue: Array<{ dir: string; depth: number }> = [{ dir: rootDir, depth: 0 }]
145
+ const matches: string[] = []
146
+ let visited = 0
147
+ let nextIndex = 0
148
+ let complete = true
149
+
150
+ while (nextIndex < queue.length && visited < MAX_SCANNED_DIRECTORIES) {
151
+ const current = queue[nextIndex++]
152
+ if (!current) break
153
+ visited++
154
+ if (current.depth > 0 && findExpoAppConfig(current.dir)) {
155
+ matches.push(current.dir)
156
+ if (matches.length > 1) return { directories: matches, complete: true }
157
+ }
158
+ if (current.depth >= MAX_SCAN_DEPTH) continue
159
+
160
+ let entries: fs.Dirent[]
161
+ try {
162
+ entries = fs.readdirSync(current.dir, { withFileTypes: true })
163
+ } catch {
164
+ continue
165
+ }
166
+ for (const entry of entries) {
167
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue
168
+ if (entry.name.startsWith('.') || SKIP_DIR_NAMES.has(entry.name)) continue
169
+ if (queue.length >= MAX_SCANNED_DIRECTORIES) {
170
+ complete = false
171
+ break
172
+ }
173
+ queue.push({ dir: path.join(current.dir, entry.name), depth: current.depth + 1 })
174
+ }
175
+ }
176
+
177
+ return { directories: matches, complete: complete && nextIndex === queue.length }
178
+ }
179
+
180
+ export function resolveAppProject(
181
+ startDir: string,
182
+ opts: { trustStart?: boolean } = {},
183
+ ): ResolvedAppProject | null {
184
+ const rootDir = path.resolve(startDir)
185
+ const rootConfigPath = findSootSimAppConfig(rootDir)
186
+ if (findExpoAppConfig(rootDir) || (opts.trustStart && fs.existsSync(rootDir))) {
187
+ let configPath = rootConfigPath
188
+ if (!configPath && opts.trustStart) {
189
+ let parent = path.dirname(rootDir)
190
+ for (
191
+ let depth = 0;
192
+ depth < MAX_SCAN_DEPTH && parent !== path.dirname(parent);
193
+ depth++
194
+ ) {
195
+ configPath = findSootSimAppConfig(parent)
196
+ if (configPath) break
197
+ parent = path.dirname(parent)
198
+ }
199
+ }
200
+ return { appDir: rootDir, configPath }
201
+ }
202
+
203
+ const workspaceScan = workspaceDirectories(rootDir)
204
+ const workspaceApps = workspaceScan.directories.filter((directory) =>
205
+ findExpoAppConfig(directory),
206
+ )
207
+ if (workspaceApps.length > 1) return null
208
+ if (!workspaceScan.complete) return null
209
+ if (workspaceApps.length === 1) {
210
+ const workspaceAppDir = workspaceApps[0]
211
+ return {
212
+ appDir: workspaceAppDir,
213
+ configPath: rootConfigPath ?? findSootSimAppConfig(workspaceAppDir),
214
+ }
215
+ }
216
+
217
+ const fallbackScan = boundedConfigDirectories(rootDir)
218
+ const scannedApps = fallbackScan.directories
219
+ if (scannedApps.length > 1) return null
220
+ if (!fallbackScan.complete) return null
221
+ if (scannedApps.length === 1) {
222
+ const scannedAppDir = scannedApps[0]
223
+ return {
224
+ appDir: scannedAppDir,
225
+ configPath: rootConfigPath ?? findSootSimAppConfig(scannedAppDir),
226
+ }
227
+ }
228
+
229
+ if (rootConfigPath) return { appDir: rootDir, configPath: rootConfigPath }
230
+ return null
231
+ }
@@ -0,0 +1,185 @@
1
+ import { createHash } from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import { createRequire } from 'node:module'
4
+ import path from 'node:path'
5
+ import {
6
+ configPluginSplashFromExpoConfig,
7
+ type ConfigPluginSplashSpec,
8
+ } from 'sootsim-engine/engine/config-plugin-splash'
9
+ import { resolveAppProject } from './app-project'
10
+
11
+ const require = createRequire(import.meta.url)
12
+
13
+ // the filesystem scan and the engine's metro-manifest scan share the same
14
+ // parser (config-plugin-splash.ts) and produce the same resolved spec.
15
+ export type DiscoveredAppSplashSpec = ConfigPluginSplashSpec
16
+
17
+ type ExpoConfigGetter = (dir: string, opts: Record<string, unknown>) => { exp?: unknown }
18
+
19
+ function localOriginOf(value: string): string | null {
20
+ try {
21
+ const url = new URL(value)
22
+ const host = url.hostname.replace(/^\[|\]$/g, '').toLowerCase()
23
+ if (
24
+ host === 'localhost' ||
25
+ host.endsWith('.localhost') ||
26
+ host === '0.0.0.0' ||
27
+ host === '::1' ||
28
+ /^127(?:\.\d{1,3}){3}$/.test(host)
29
+ ) {
30
+ return url.origin
31
+ }
32
+ } catch {}
33
+ return null
34
+ }
35
+
36
+ function hasPackageJson(dir: string): boolean {
37
+ try {
38
+ return fs.statSync(path.join(dir, 'package.json')).isFile()
39
+ } catch {
40
+ return false
41
+ }
42
+ }
43
+
44
+ // discover the app's configured splash by evaluating its expo config on the
45
+ // filesystem (mirrors discoverNativeLinkedAppFonts exactly). returns null when
46
+ // no splash is configured, no app config is found, or the bundle isn't local.
47
+ export function discoverConfigPluginAppSplash(opts: {
48
+ bundleUrl: string
49
+ repoDir?: string
50
+ exactProjectDir?: boolean
51
+ platform?: string
52
+ }): DiscoveredAppSplashSpec | null {
53
+ const metroOrigin = localOriginOf(opts.bundleUrl)
54
+ if (!metroOrigin) return null
55
+
56
+ const explicit = opts.repoDir || process.env.CONTRAST_REPO_DIR
57
+ const startDir = explicit ? path.resolve(explicit) : process.cwd()
58
+
59
+ if (!explicit && !hasPackageJson(startDir)) return null
60
+
61
+ const project = resolveAppProject(startDir, { trustStart: opts.exactProjectDir })
62
+ const selectedAppDir = project?.appDir ?? null
63
+ if (!selectedAppDir) return null
64
+
65
+ const platform = (opts.platform || process.env.CONTRAST_PLATFORM || 'ios').toLowerCase()
66
+
67
+ let getConfig: ExpoConfigGetter | null = null
68
+ try {
69
+ const modulePath = require.resolve('@expo/config', {
70
+ paths: [selectedAppDir, startDir],
71
+ })
72
+ const mod = require(modulePath) as { getConfig?: unknown }
73
+ if (typeof mod.getConfig === 'function') {
74
+ getConfig = mod.getConfig as ExpoConfigGetter
75
+ }
76
+ } catch {}
77
+ if (!getConfig) return null
78
+
79
+ let exp: unknown
80
+ try {
81
+ const result = getConfig(selectedAppDir, { skipSDKVersionRequirement: true })
82
+ if (result?.exp && typeof result.exp === 'object') exp = result.exp
83
+ } catch {}
84
+ if (!exp || typeof exp !== 'object') return null
85
+
86
+ return configPluginSplashFromExpoConfig(
87
+ exp,
88
+ platform === 'android' ? 'android' : 'ios',
89
+ metroOrigin,
90
+ )
91
+ }
92
+
93
+ // encode a resolved splash spec into the `?appSplash=` engine-URL wire value
94
+ // (URL-safe JSON). the engine decodes it with parseAppSplashSpec.
95
+ export function encodeAppSplashWire(spec: DiscoveredAppSplashSpec): string {
96
+ return encodeURIComponent(JSON.stringify(spec))
97
+ }
98
+
99
+ // parse a stamped `?appSplash=` wire value back into a spec (or null). used by
100
+ // the preview upload path to learn the splash image url it must stage.
101
+ export function parseAppSplashWire(wire: string): DiscoveredAppSplashSpec | null {
102
+ if (!wire) return null
103
+ try {
104
+ const decoded = JSON.parse(decodeURIComponent(wire)) as unknown
105
+ if (!decoded || typeof decoded !== 'object') return null
106
+ const spec = decoded as Record<string, unknown>
107
+ if (
108
+ spec.source !== 'expo-splash-screen' &&
109
+ spec.source !== 'expo-legacy' &&
110
+ spec.source !== 'react-native-bootsplash'
111
+ )
112
+ return null
113
+ if (typeof spec.backgroundColor !== 'string') return null
114
+ return decoded as DiscoveredAppSplashSpec
115
+ } catch {
116
+ return null
117
+ }
118
+ }
119
+
120
+ // every distinct image url a splash spec references (base + dark variant). used
121
+ // by the preview upload path so both the light and dark splash images get
122
+ // staged as share assets.
123
+ export function splashImageUrls(spec: DiscoveredAppSplashSpec): string[] {
124
+ const urls = new Set<string>()
125
+ if (spec.imageUrl) urls.add(spec.imageUrl)
126
+ if (spec.dark?.imageUrl) urls.add(spec.dark.imageUrl)
127
+ return [...urls]
128
+ }
129
+
130
+ // content type for a splash image url, by extension. config splash images ship
131
+ // as png/jpg/webp/gif/svg; fall back to octet-stream so the share store still
132
+ // serves bytes the engine's <img> can load.
133
+ export function splashImageContentType(url: string): string {
134
+ const ext = (url.split('?')[0].split('.').pop() || '').toLowerCase()
135
+ if (ext === 'png') return 'image/png'
136
+ if (ext === 'jpg' || ext === 'jpeg') return 'image/jpeg'
137
+ if (ext === 'webp') return 'image/webp'
138
+ if (ext === 'gif') return 'image/gif'
139
+ if (ext === 'svg') return 'image/svg+xml'
140
+ return 'application/octet-stream'
141
+ }
142
+
143
+ export type FetchedAppSplashImage = {
144
+ url: string
145
+ // sha256(url) — the share-store key, matching the font/asset convention.
146
+ urlhash: string
147
+ contentType: string
148
+ bytes: Uint8Array
149
+ }
150
+
151
+ // fetch every splash image declared on an `?appSplash=` wire. like the config-
152
+ // plugin fonts, these images live in the native binary / launch storyboard,
153
+ // never in the metro bundle graph, so the preview upload path backfills them by
154
+ // re-fetching the declared urls off the still-live metro — the same staging
155
+ // pattern fetchAppFontFiles uses, keyed by sha256(url).
156
+ export async function fetchAppSplashImages(
157
+ wire: string,
158
+ hooks?: {
159
+ onStaged?: (info: { url: string; byteLength: number }) => void
160
+ onError?: (url: string, err: unknown) => void
161
+ },
162
+ ): Promise<FetchedAppSplashImage[]> {
163
+ const spec = parseAppSplashWire(wire)
164
+ if (!spec) return []
165
+ const out: FetchedAppSplashImage[] = []
166
+ const seen = new Set<string>()
167
+ for (const url of splashImageUrls(spec)) {
168
+ const urlhash = createHash('sha256').update(url).digest('hex')
169
+ if (seen.has(urlhash)) continue
170
+ seen.add(urlhash)
171
+ try {
172
+ const res = await fetch(url)
173
+ if (!res.ok) {
174
+ hooks?.onError?.(url, new Error(`${res.status} ${res.statusText}`))
175
+ continue
176
+ }
177
+ const bytes = new Uint8Array(await res.arrayBuffer())
178
+ out.push({ url, urlhash, contentType: splashImageContentType(url), bytes })
179
+ hooks?.onStaged?.({ url, byteLength: bytes.byteLength })
180
+ } catch (err) {
181
+ hooks?.onError?.(url, err)
182
+ }
183
+ }
184
+ return out
185
+ }
@@ -0,0 +1,24 @@
1
+ // resets the GUEST APP to a fresh launch through the engine's own soft-reload
2
+ // bridge (`SootSim.bridges.hotRemount.reloadExternalApp`). that re-runs the
3
+ // guest app entry while keeping the engine bundle + worker alive — no heavy
4
+ // metro re-fetch of every lazy chunk, and the sim id does NOT rotate. when
5
+ // `resetStorage` is set it clears the guest app's tenant-scoped storage via the
6
+ // engine's own clear path (scoped localStorage + open-and-clear of the known
7
+ // tenant IndexedDBs / secure-store) — the maestro `clearState` contract.
8
+ //
9
+ // this replaces a host-realm `localStorage.clear()` + `indexedDB.deleteDatabase`
10
+ // loop that was wrong twice: (1) `localStorage.clear()` wiped engine settings
11
+ // and every co-tenant sim's tenant storage, not just this app's; (2) the delete
12
+ // loop ran in the host realm where the only database is the engine's own
13
+ // `preview-share-state`, which the engine holds open — `deleteDatabase` then
14
+ // hung forever (no success/error/blocked event fired), so the whole eval timed
15
+ // out at 60s and never cleared the guest's secure-store session either. that
16
+ // was the bluesky e2e suite's blanket setupApp failure.
17
+ export function resetGuestAppStateEval(resetStorage: boolean): string {
18
+ return `(async () => {
19
+ const bridge = globalThis.SootSim && globalThis.SootSim.bridges && globalThis.SootSim.bridges.hotRemount
20
+ if (!bridge || typeof bridge.reloadExternalApp !== 'function') return false
21
+ await bridge.reloadExternalApp({ resetStorage: ${resetStorage ? 'true' : 'false'} })
22
+ return true
23
+ })()`
24
+ }
package/cli/auth.ts ADDED
@@ -0,0 +1,155 @@
1
+ // sootsim cli auth resolution for cloud-hitting commands.
2
+ //
3
+ // priority order (first match wins):
4
+ // 1. RNX_API_KEY, or SOOTSIM_API_KEY if that is unset — ci path
5
+ // 2. CONTRAST_INSTALLATION_TOKEN/GITHUB_TOKEN + repo env — github action
6
+ // preview uploads from the Contrast runner, no extra secret
7
+ // 3. desktop session token from shared desktop auth (`rnxsim login`)
8
+ //
9
+ // callers wanting an auth header: `authHeaderOrExit('upload')`.
10
+
11
+ import { readSharedDesktopAuthSession } from '../src/auth/shared-session'
12
+
13
+ export type CliApiKeyEnvName = 'RNX_API_KEY' | 'SOOTSIM_API_KEY'
14
+
15
+ export type CliAuth =
16
+ | { kind: 'api-key'; secret: string; source: 'env'; envName: CliApiKeyEnvName }
17
+ | {
18
+ kind: 'github'
19
+ token: string
20
+ // historical name: this is the github owner/repo slug, not the
21
+ // numeric repository id.
22
+ repoId: string
23
+ repositoryId: string | null
24
+ owner: string | null
25
+ repo: string | null
26
+ installationId: string | null
27
+ source: 'contrast-runner' | 'github-actions'
28
+ }
29
+ | { kind: 'session'; token: string; origin: string }
30
+
31
+ export type GitHubUploadIdentity = {
32
+ repoId: string
33
+ owner?: string
34
+ repo?: string
35
+ installationId?: string | null
36
+ }
37
+
38
+ // ─── resolution ────────────────────────────────────────────────────────────
39
+
40
+ const GITHUB_TOKEN_PREFIXES = ['ghs_', 'ghp_', 'gho_', 'ghu_', 'github_pat_']
41
+
42
+ function looksLikeGitHubToken(token: string | undefined): boolean {
43
+ // github's 2026 stateless installation tokens (`secrets.GITHUB_TOKEN` /
44
+ // github.token in actions) are ~520 chars and vary by payload — keep them
45
+ // opaque with NO upper length cap, matching the canonical server-side check
46
+ // in src/github/token.ts. the length floor + prefix is only a cheap typo
47
+ // guard before the cloud endpoints verify real repo access. an upper cap
48
+ // here silently rejects the long actions token, so resolveCliAuth() returns
49
+ // null and every CI `record start` dies with "set RNX_API_KEY".
50
+ return Boolean(
51
+ token &&
52
+ token.length >= 20 &&
53
+ GITHUB_TOKEN_PREFIXES.some((prefix) => token.startsWith(prefix)),
54
+ )
55
+ }
56
+
57
+ function resolveGitHubActionsAuth(): CliAuth | null {
58
+ const contrastRunnerToken = process.env.CONTRAST_INSTALLATION_TOKEN?.trim()
59
+ const githubActionsToken = process.env.GITHUB_TOKEN?.trim()
60
+ const token = contrastRunnerToken || githubActionsToken
61
+ if (!looksLikeGitHubToken(token)) return null
62
+
63
+ const repoSlug = (
64
+ process.env.CONTRAST_REPO ||
65
+ process.env.GITHUB_REPOSITORY ||
66
+ ''
67
+ ).trim()
68
+ if (!repoSlug) return null
69
+ const [owner, repo] = repoSlug.includes('/') ? repoSlug.split('/', 2) : [null, null]
70
+ const repositoryId = (
71
+ process.env.CONTRAST_REPO_ID ||
72
+ process.env.GITHUB_REPOSITORY_ID ||
73
+ ''
74
+ ).trim()
75
+
76
+ const installationId = (
77
+ process.env.CONTRAST_INSTALLATION_ID ||
78
+ process.env.GITHUB_APP_INSTALLATION_ID ||
79
+ ''
80
+ ).trim()
81
+
82
+ return {
83
+ kind: 'github',
84
+ token: token!,
85
+ repoId: repoSlug,
86
+ repositoryId: repositoryId || null,
87
+ owner: owner || null,
88
+ repo: repo || null,
89
+ installationId: installationId || null,
90
+ source: contrastRunnerToken ? 'contrast-runner' : 'github-actions',
91
+ }
92
+ }
93
+
94
+ function isCliApiKeySecret(secret: string): boolean {
95
+ return secret.startsWith('sk_rnx_') || secret.startsWith('sk_sootsim_')
96
+ }
97
+
98
+ function readApiKeyFromEnv(): { secret: string; envName: CliApiKeyEnvName } | null {
99
+ for (const envName of ['RNX_API_KEY', 'SOOTSIM_API_KEY'] as const) {
100
+ const secret = process.env[envName]?.trim()
101
+ if (secret && isCliApiKeySecret(secret)) return { secret, envName }
102
+ }
103
+ return null
104
+ }
105
+
106
+ export function resolveCliAuth(): CliAuth | null {
107
+ const envKey = readApiKeyFromEnv()
108
+ if (envKey) {
109
+ return {
110
+ kind: 'api-key',
111
+ secret: envKey.secret,
112
+ source: 'env',
113
+ envName: envKey.envName,
114
+ }
115
+ }
116
+ const githubAuth = resolveGitHubActionsAuth()
117
+ if (githubAuth) return githubAuth
118
+ const session = readSharedDesktopAuthSession()
119
+ if (session?.token) {
120
+ return { kind: 'session', token: session.token, origin: session.origin }
121
+ }
122
+ return null
123
+ }
124
+
125
+ export function authHeaderValue(auth: CliAuth): string {
126
+ return auth.kind === 'api-key' ? `Bearer ${auth.secret}` : `Bearer ${auth.token}`
127
+ }
128
+
129
+ export function githubUploadIdentity(auth: CliAuth | null): GitHubUploadIdentity | null {
130
+ if (!auth || auth.kind !== 'github') return null
131
+ return {
132
+ repoId: auth.repoId,
133
+ owner: auth.owner ?? undefined,
134
+ repo: auth.repo ?? undefined,
135
+ installationId: auth.installationId,
136
+ }
137
+ }
138
+
139
+ // fail fast with a friendly message — use at the top of cloud-hitting
140
+ // commands that can't proceed without auth.
141
+ export function authHeaderOrExit(commandLabel: string): {
142
+ auth: CliAuth
143
+ header: string
144
+ } {
145
+ const auth = resolveCliAuth()
146
+ if (auth) return { auth, header: authHeaderValue(auth) }
147
+
148
+ process.stderr.write(
149
+ `\n rnx ${commandLabel} needs to be authenticated.\n\n` +
150
+ ` pick one:\n` +
151
+ ` • run \`rnxsim login\` to sign in with your GH account\n` +
152
+ ` • set RNX_API_KEY=sk_rnx_... (recommended for CI)\n\n`,
153
+ )
154
+ process.exit(1)
155
+ }