rnxsim 0.1.343 → 0.1.345

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 (209) hide show
  1. package/README.md +59 -31
  2. package/cli/app-fonts.ts +8 -28
  3. package/cli/app-project.ts +12 -184
  4. package/cli/app-splash.ts +3 -17
  5. package/cli/bin.ts +15 -28
  6. package/cli/commands/control.ts +20 -30
  7. package/cli/commands/daemon.ts +19 -44
  8. package/cli/commands/flow.ts +0 -27
  9. package/cli/commands/install-desktop.ts +2 -16
  10. package/cli/commands/login.ts +3 -43
  11. package/cli/commands/logout.ts +0 -7
  12. package/cli/commands/serve.ts +2 -32
  13. package/cli/commands/setup.ts +20 -194
  14. package/cli/commands/telemetry.ts +7 -5
  15. package/cli/commands/upload.ts +1 -48
  16. package/cli/commands/welcome.ts +64 -0
  17. package/cli/outbound-endpoints.ts +586 -0
  18. package/cli/privacy.ts +53 -0
  19. package/cli/runtime-notes.ts +2 -2
  20. package/cli/telemetry.ts +46 -149
  21. package/dist-cli/bin.js +6 -6
  22. package/dist-cli/chunks/{agent-RFD32ACU.js → agent-52G55XTO.js} +2 -2
  23. package/dist-cli/chunks/{agent-wrapper-2YLW2HQK.js → agent-wrapper-BOZM43ON.js} +2 -2
  24. package/dist-cli/chunks/{app-fonts-6OATDF3Y.js → app-fonts-A2L6V5EZ.js} +2 -2
  25. package/dist-cli/chunks/{assert-PSQICFOY.js → assert-CJKIP4LN.js} +2 -2
  26. package/dist-cli/chunks/{auth-XWLFKZT7.js → auth-VHHN3TOB.js} +2 -2
  27. package/dist-cli/chunks/{beta-I6WTVM6C.js → beta-EBDIGV3E.js} +2 -2
  28. package/dist-cli/chunks/{camera-45HKTLWG.js → camera-5F4KKCQ3.js} +2 -2
  29. package/dist-cli/chunks/chunk-3DBTS4MC.js +15 -0
  30. package/dist-cli/chunks/chunk-3N7M5HMJ.js +6 -0
  31. package/dist-cli/chunks/{chunk-RWYA2USN.js → chunk-4BIPA7MD.js} +1 -1
  32. package/dist-cli/chunks/chunk-4OKV7T5D.js +4 -0
  33. package/dist-cli/chunks/{chunk-T4T6DDKM.js → chunk-4ZILDTZX.js} +2 -2
  34. package/dist-cli/chunks/{chunk-R7X74I3I.js → chunk-5GXRRUOA.js} +26 -26
  35. package/dist-cli/chunks/chunk-63ASUHOV.js +4 -0
  36. package/dist-cli/chunks/{chunk-DASP2EKF.js → chunk-7GO2DSYQ.js} +1 -1
  37. package/dist-cli/chunks/{chunk-HJXIBZ35.js → chunk-7LGINKM6.js} +2 -2
  38. package/dist-cli/chunks/{chunk-2IPMH24A.js → chunk-7UIIAHVU.js} +2 -2
  39. package/dist-cli/chunks/{chunk-ES5BHPTS.js → chunk-7V4KPZ7O.js} +2 -2
  40. package/dist-cli/chunks/{chunk-FCHRGEHA.js → chunk-7YMQRA6L.js} +2 -2
  41. package/dist-cli/chunks/{chunk-DC7TS573.js → chunk-A2Q76LJV.js} +2 -2
  42. package/dist-cli/chunks/{chunk-SAUCA7SN.js → chunk-BCQV63BW.js} +45 -45
  43. package/dist-cli/chunks/{chunk-3FYJ7U3C.js → chunk-BFTDL2GX.js} +17 -17
  44. package/dist-cli/chunks/chunk-BHKSCNDC.js +4 -0
  45. package/dist-cli/chunks/{chunk-QPTJNLPT.js → chunk-DGTV3KOW.js} +1 -1
  46. package/dist-cli/chunks/chunk-DHM3JDCK.js +4 -0
  47. package/dist-cli/chunks/{chunk-TQRVLK64.js → chunk-DPP2M4AJ.js} +1 -1
  48. package/dist-cli/chunks/chunk-E2RGSCHT.js +4 -0
  49. package/dist-cli/chunks/{chunk-TQ5OPG33.js → chunk-EPLVIDPC.js} +1 -1
  50. package/dist-cli/chunks/{chunk-P3IDK7FR.js → chunk-GKGXULJK.js} +2 -2
  51. package/dist-cli/chunks/{chunk-34KDMQ6U.js → chunk-HK7YKH6N.js} +2 -2
  52. package/dist-cli/chunks/{chunk-BJUNXCPO.js → chunk-HXE344V3.js} +1 -1
  53. package/dist-cli/chunks/{chunk-HJHFC5ZZ.js → chunk-JC33TOAF.js} +1 -1
  54. package/dist-cli/chunks/{chunk-G3B2RZ47.js → chunk-KA4PJNTE.js} +2 -2
  55. package/dist-cli/chunks/{chunk-VC4TDPJQ.js → chunk-KH777UAO.js} +3 -3
  56. package/dist-cli/chunks/{chunk-4PFMDBLB.js → chunk-KRZPFXVN.js} +1 -1
  57. package/dist-cli/chunks/{chunk-6SLM6URN.js → chunk-KZFJNDH3.js} +1 -1
  58. package/dist-cli/chunks/{chunk-KLMWLQAO.js → chunk-LAQGSSTT.js} +1 -1
  59. package/dist-cli/chunks/chunk-LRM2XCEQ.js +11 -0
  60. package/dist-cli/chunks/{chunk-YKKVDKLE.js → chunk-NUPRHZQW.js} +2 -2
  61. package/dist-cli/chunks/{chunk-AKV2DGKM.js → chunk-OPHT4DUO.js} +1 -1
  62. package/dist-cli/chunks/{chunk-KON6XG4Y.js → chunk-P2CGGMU2.js} +2 -2
  63. package/dist-cli/chunks/{chunk-5FB4EA33.js → chunk-PLB6UVVX.js} +2 -2
  64. package/dist-cli/chunks/{chunk-FRRHCLG2.js → chunk-PLUWN2V5.js} +1 -1
  65. package/dist-cli/chunks/{chunk-WDA4QR6U.js → chunk-QNBBKRCA.js} +2 -2
  66. package/dist-cli/chunks/{chunk-G7S6T5OX.js → chunk-QNRWPAOA.js} +2 -2
  67. package/dist-cli/chunks/{chunk-2EVBZ37G.js → chunk-R7ZNRGQN.js} +1 -1
  68. package/dist-cli/chunks/{chunk-TMJGPUWJ.js → chunk-RB4VFUNM.js} +3 -3
  69. package/dist-cli/chunks/chunk-RT3DXTDP.js +25 -0
  70. package/dist-cli/chunks/{chunk-OYLIWHDB.js → chunk-RTDHX6CW.js} +2 -2
  71. package/dist-cli/chunks/{chunk-YLFDA4WW.js → chunk-T7SBFG5I.js} +2 -2
  72. package/dist-cli/chunks/{chunk-W7EXE22C.js → chunk-TEU5NGUA.js} +2 -2
  73. package/dist-cli/chunks/chunk-TMRD5MHL.js +99 -0
  74. package/dist-cli/chunks/{chunk-SU7RE3PV.js → chunk-TOF5TA5X.js} +11 -11
  75. package/dist-cli/chunks/{chunk-6IVEGYWV.js → chunk-U5YHXQHH.js} +2 -2
  76. package/dist-cli/chunks/{chunk-BA3HRFJ5.js → chunk-UVOEHIGP.js} +1 -1
  77. package/dist-cli/chunks/{chunk-SW4DV2TY.js → chunk-VLBN3KRC.js} +1 -1
  78. package/dist-cli/chunks/{chunk-7WKV6EKX.js → chunk-WPPGU5IK.js} +2 -2
  79. package/dist-cli/chunks/{chunk-JOE3J46E.js → chunk-WU72KL6Z.js} +2 -2
  80. package/dist-cli/chunks/chunk-XJBFJGNT.js +4 -0
  81. package/dist-cli/chunks/{chunk-3CMKNWRL.js → chunk-XXZLK4BY.js} +1 -1
  82. package/dist-cli/chunks/{chunk-BKMPV6N4.js → chunk-YVSUFVVB.js} +1 -1
  83. package/dist-cli/chunks/{chunk-RO3WVQPB.js → chunk-ZMTZ5G5H.js} +2 -2
  84. package/dist-cli/chunks/{cleanup-OU4VKFC3.js → cleanup-MAVZJXVW.js} +2 -2
  85. package/dist-cli/chunks/cli-version-2B4KRNML.js +4 -0
  86. package/dist-cli/chunks/{compat-GAEKP67T.js → compat-7AURLGCS.js} +2 -2
  87. package/dist-cli/chunks/config-TN3Y4J5N.js +13 -0
  88. package/dist-cli/chunks/{control-VQJURMFD.js → control-EV2ELU5R.js} +2 -2
  89. package/dist-cli/chunks/daemon-KKDG5MYR.js +4 -0
  90. package/dist-cli/chunks/{debug-F5N2LO4T.js → debug-JICITUB6.js} +3 -3
  91. package/dist-cli/chunks/{desktop-I4ILT4BS.js → desktop-4F5HGLLX.js} +3 -3
  92. package/dist-cli/chunks/{detox-2HMPYSP4.js → detox-ZC7NHLJI.js} +2 -2
  93. package/dist-cli/chunks/{device-5LWLOB5R.js → device-V57GHRXX.js} +2 -2
  94. package/dist-cli/chunks/{diagnose-5S4IQPAP.js → diagnose-FF4IWYXC.js} +2 -2
  95. package/dist-cli/chunks/{disk-cleanup-CJSZVJQX.js → disk-cleanup-T277L6ZO.js} +2 -2
  96. package/dist-cli/chunks/drivers-LCX7S76X.js +4 -0
  97. package/dist-cli/chunks/{film-HCPU2TDB.js → film-G7ZAPWB7.js} +3 -3
  98. package/dist-cli/chunks/flow-ONZGGFGD.js +4 -0
  99. package/dist-cli/chunks/help-DROVDXJM.js +4 -0
  100. package/dist-cli/chunks/{hidden-runtime-alias-YYUEZQPR.js → hidden-runtime-alias-VSJNNSNI.js} +2 -2
  101. package/dist-cli/chunks/{home-paths-3FXISM5I.js → home-paths-75HP3HKA.js} +2 -2
  102. package/dist-cli/chunks/inspect-MDA7ESGQ.js +4 -0
  103. package/dist-cli/chunks/install-desktop-EIVZHIFA.js +4 -0
  104. package/dist-cli/chunks/login-ZSIHGHUK.js +26 -0
  105. package/dist-cli/chunks/logout-Q4DXMI2C.js +4 -0
  106. package/dist-cli/chunks/{maestro-F5KK4RC4.js → maestro-LE5LCRBL.js} +3 -3
  107. package/dist-cli/chunks/{maestro-generate-6SUU3Y5I.js → maestro-generate-D3ZIJCRL.js} +3 -3
  108. package/dist-cli/chunks/{mode-CSWGOJLI.js → mode-BWX6MDPJ.js} +2 -2
  109. package/dist-cli/chunks/{optional-demo-registry-Y6VJ4SVV.js → optional-demo-registry-6CVASL3S.js} +2 -2
  110. package/dist-cli/chunks/{perf-NRWWAJL4.js → perf-NXQZISN6.js} +2 -2
  111. package/dist-cli/chunks/{permissions-OD62A3DJ.js → permissions-5PD67RRO.js} +2 -2
  112. package/dist-cli/chunks/privacy-H5KNRNIZ.js +4 -0
  113. package/dist-cli/chunks/{record-JZT2GM3G.js → record-YJF4KXMR.js} +3 -3
  114. package/dist-cli/chunks/{report-issue-ZWSNHOPB.js → report-issue-XOL5FQBG.js} +2 -2
  115. package/dist-cli/chunks/reset-5LZN5P5B.js +4 -0
  116. package/dist-cli/chunks/runtime-U36VWBH3.js +4 -0
  117. package/dist-cli/chunks/{screenshot-command-ZGJR7ICY.js → screenshot-command-VRPJXWFS.js} +3 -3
  118. package/dist-cli/chunks/{screenshot-layers-5AQKOJZK.js → screenshot-layers-QP4WA2F5.js} +2 -2
  119. package/dist-cli/chunks/{screenshots-capture-CPTWK5Y3.js → screenshots-capture-O2IBAVSP.js} +2 -2
  120. package/dist-cli/chunks/serve-WC6RZNJL.js +118 -0
  121. package/dist-cli/chunks/setup-A6BIG3KL.js +30 -0
  122. package/dist-cli/chunks/{skills-NVWRBMEO.js → skills-A7YC6RL5.js} +2 -2
  123. package/dist-cli/chunks/{state-QGLTKN6K.js → state-IJI5YROJ.js} +2 -2
  124. package/dist-cli/chunks/{storage-OZMEZ4AV.js → storage-Z2DBO2PQ.js} +4 -4
  125. package/dist-cli/chunks/store-BYTCKNIC.js +4 -0
  126. package/dist-cli/chunks/telemetry-JLHFMD7N.js +4 -0
  127. package/dist-cli/chunks/{timeline-EXLDREY3.js → timeline-N7V2M6QA.js} +2 -2
  128. package/dist-cli/chunks/{upgrade-4VWXQO6I.js → upgrade-P2NH3BOR.js} +2 -2
  129. package/dist-cli/chunks/upload-Z2WX5DCD.js +4 -0
  130. package/dist-cli/chunks/{version-WZM6XCD2.js → version-OUHCSCUS.js} +2 -2
  131. package/dist-cli/chunks/{web-ZYPEUVWL.js → web-6NRTF3UV.js} +2 -2
  132. package/dist-cli/chunks/welcome-6RS7PEJW.js +5 -0
  133. package/dist-cli/chunks/{what-happened-KF2HLWKW.js → what-happened-FUEL3BEF.js} +2 -2
  134. package/dist-lib/agent-daemon-client.cjs +1 -1
  135. package/dist-lib/agent-events.cjs +1 -1
  136. package/dist-lib/agent-identity.cjs +1 -1
  137. package/dist-lib/agent-sessions.cjs +1 -1
  138. package/dist-lib/attached-projects.cjs +1 -1
  139. package/dist-lib/auth/shared-session.cjs +1 -1
  140. package/dist-lib/backend-origin.cjs +1 -1
  141. package/dist-lib/beta.cjs +1 -1
  142. package/dist-lib/beta.mjs +1 -1
  143. package/dist-lib/bridge-constants.cjs +1 -1
  144. package/dist-lib/bridge-contract.cjs +1 -1
  145. package/dist-lib/cli-constants.cjs +1 -1
  146. package/dist-lib/config.cjs +1 -1
  147. package/dist-lib/detox/index.cjs +1 -1
  148. package/dist-lib/dev-bundle-resolution.cjs +18 -4
  149. package/dist-lib/home-paths.cjs +145 -11
  150. package/dist-lib/host/bridge-host.cjs +236 -31
  151. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  152. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  153. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  154. package/dist-lib/host/replacement-module-handler.cjs +71 -4
  155. package/dist-lib/host/websocket-proxy.cjs +1 -1
  156. package/dist-lib/index.cjs +337 -37
  157. package/dist-lib/jump-to-source-babel.cjs +1 -1
  158. package/dist-lib/menu.cjs +1 -1
  159. package/dist-lib/menu.mjs +1 -1
  160. package/dist-lib/metro-production-bundle.cjs +1 -1
  161. package/dist-lib/metro-production-bundle.mjs +1 -1
  162. package/dist-lib/metro.cjs +484 -14
  163. package/dist-lib/profiles.cjs +1 -1
  164. package/dist-lib/public-brand.cjs +1 -1
  165. package/dist-lib/react-native-host-modules.cjs +1 -1
  166. package/dist-lib/react-native-host-modules.mjs +1 -1
  167. package/dist-lib/render-mode.cjs +1 -1
  168. package/dist-lib/scripts/dev-server-scanner.cjs +25 -2
  169. package/dist-lib/sdk.cjs +1 -1
  170. package/dist-lib/sdk.mjs +1 -1
  171. package/dist-lib/skills.cjs +384 -385
  172. package/dist-lib/vite.cjs +193 -37
  173. package/package.json +2 -1
  174. package/scripts/dev-server-scanner.ts +12 -5
  175. package/skills/rnx-test/SKILL.md +4 -0
  176. package/src/dev-bundle-resolution.ts +13 -6
  177. package/src/dev-server-open.ts +197 -0
  178. package/src/home-paths.ts +168 -15
  179. package/src/host/bridge-host.ts +2 -2
  180. package/src/host/replacement-module-handler.ts +68 -2
  181. package/src/metro-plugin.ts +83 -5
  182. package/src/native-dev-bundle-url.ts +22 -0
  183. package/src/vite-plugin-one.ts +42 -28
  184. package/dist-cli/chunks/chunk-44LRQEU3.js +0 -4
  185. package/dist-cli/chunks/chunk-4RKTGL63.js +0 -9
  186. package/dist-cli/chunks/chunk-5SCIDXJD.js +0 -25
  187. package/dist-cli/chunks/chunk-CUVY4A2A.js +0 -4
  188. package/dist-cli/chunks/chunk-DPJGB7MP.js +0 -101
  189. package/dist-cli/chunks/chunk-HD6P3W5R.js +0 -4
  190. package/dist-cli/chunks/chunk-HFNESSHG.js +0 -15
  191. package/dist-cli/chunks/chunk-I7HHBFSD.js +0 -4
  192. package/dist-cli/chunks/chunk-LV2G34IX.js +0 -4
  193. package/dist-cli/chunks/cli-version-REQOM22G.js +0 -4
  194. package/dist-cli/chunks/config-QTFXJKD6.js +0 -13
  195. package/dist-cli/chunks/daemon-ETA375WE.js +0 -4
  196. package/dist-cli/chunks/drivers-R4YB2XAF.js +0 -4
  197. package/dist-cli/chunks/flow-3VQL6XZT.js +0 -4
  198. package/dist-cli/chunks/help-2RYCKZ2G.js +0 -4
  199. package/dist-cli/chunks/inspect-XDSX5D6D.js +0 -4
  200. package/dist-cli/chunks/install-desktop-OKVRLL35.js +0 -4
  201. package/dist-cli/chunks/login-4VCBPAY2.js +0 -28
  202. package/dist-cli/chunks/logout-WSL5JS5R.js +0 -4
  203. package/dist-cli/chunks/reset-3C52YQCV.js +0 -4
  204. package/dist-cli/chunks/runtime-4V2RBUOR.js +0 -4
  205. package/dist-cli/chunks/serve-ERAZQGCJ.js +0 -118
  206. package/dist-cli/chunks/setup-UG6PLYIL.js +0 -32
  207. package/dist-cli/chunks/store-VPBQKKWA.js +0 -4
  208. package/dist-cli/chunks/telemetry-FRW7OCYZ.js +0 -4
  209. package/dist-cli/chunks/upload-C6676NST.js +0 -4
package/README.md CHANGED
@@ -53,7 +53,7 @@ internally splits into shell, compositor, and tenant workers.
53
53
  ```mermaid
54
54
  graph TD
55
55
  subgraph cli["rnx CLI process (short-lived, cli/)"]
56
- BIN["bin.ts (dispatcher + telemetry)"]
56
+ BIN["bin.ts (dispatcher + privacy choice)"]
57
57
  SETUP["commands/setup.ts (guided daemon setup)"]
58
58
  RUNTIMECMD["commands/runtime.ts (runtime install/use)"]
59
59
  INSPECT["commands/inspect/* (describe/find/do/get/wait)"]
@@ -131,12 +131,10 @@ graph TD
131
131
  SKILLS --> INSPECT
132
132
  ```
133
133
 
134
- **The CLI process** (`cli/bin.ts`) parses argv, emits invocation telemetry, and
135
- lazy-imports exactly one command module per run. Bare `rnx` and
136
- `rnx setup` own guided one-time machine work: ask which package manager you
137
- use for app commands, explain repo-pinned Node versions, recommend a global
138
- `rnxsim` install, and install the faster background daemon when it is
139
- missing. `rnx open`
134
+ **The CLI process** (`cli/bin.ts`) parses argv and lazy-imports exactly one
135
+ command module per run. Bare `rnx` discovers a running React Native development
136
+ server and opens it directly. `rnx setup` remains the explicit path for
137
+ repository preparation and the optional background daemon. `rnx open`
140
138
  ensures the engine runtime exists and starts a local foreground bridge when no
141
139
  daemon is reachable, then opens a `WsBridge` (`cli/ws-bridge.ts`) on **port
142
140
  7668** and sends JSON commands. Inspection/interaction verbs live in
@@ -172,8 +170,8 @@ bridge always addresses the **shell worker**, never an undifferentiated sim.
172
170
 
173
171
  - The published **`rnxsim/vite`** (`src/vite-plugin-one.ts`, `rnxPlugin`)
174
172
  and **`rnxsim/metro`** plugins do one thing: serve the installed runtime at
175
- `/__soot/` so an app's own dev server can host the sim shell. The vite
176
- plugin's `open: true` option also launches the simulator when the dev server
173
+ `/__soot/` so an app's own dev server can host the sim shell. Both accept an
174
+ opt-in `open` option that launches a named Electron window when the dev server
177
175
  starts (skipped when `CI` or `SOOTSIM_NO_OPEN` is set).
178
176
  - The internal **`sootsim()`** plugin (`src/vite-plugin.ts`) is a separate, much
179
177
  larger RN-resolution / native-stub plugin: it aliases `react-native` to the
@@ -186,7 +184,7 @@ bridge always addresses the **shell worker**, never an undifferentiated sim.
186
184
 
187
185
  | component | role | key files |
188
186
  | --- | --- | --- |
189
- | rnx CLI (bin + dispatcher) | Short-lived terminal entry point. Parses argv, emits telemetry, lazy-imports one command per run, and routes guided setup/open/runtime/inspection flows without a hidden bootstrap path. | `cli/bin.ts`, `cli/parse-args.ts`, `cli/help.ts`, `cli/commands/setup.ts`, `cli/commands/control.ts` |
187
+ | rnx CLI (bin + dispatcher) | Short-lived terminal entry point. Parses argv, owns the private-by-default one-time choice, lazy-imports one command per run, and routes direct open/runtime/inspection flows. | `cli/bin.ts`, `cli/privacy.ts`, `cli/parse-args.ts`, `cli/help.ts`, `cli/commands/setup.ts`, `cli/commands/control.ts` |
190
188
  | WsBridge client | Client side of the CLI→daemon link. Resolves the daemon port from the lockfile, opens a WS to `:7668`, sends JSON commands and awaits `{id,result}`/`{id,error}`. Used by every interactive command. | `cli/ws-bridge.ts`, `src/bridge-constants.ts`, `cli/current-sim.ts` |
191
189
  | inspect / SDK command surface | Runtime inspection + interaction verbs (describe, find, do tap, get errors, wait, layout, settle). Re-exported as `rnxsim/sdk` so programmatic callers drive a sim the same way the CLI does. | `cli/commands/inspect/core.ts`, `cli/commands/inspect/actions.ts`, `cli/commands/inspect.ts`, `src/sdk.ts` |
192
190
  | Maestro / Detox runners | Drop-in test compatibility. `bridge-flow-runner.ts` runs Maestro YAML and recorded flows over the bridge; `detox/` provides a `by`/`element`/`expect`/`device` driver + jest preset so existing Detox suites run unchanged. | `cli/bridge-flow-runner.ts`, `cli/commands/maestro.ts`, `cli/commands/detox.ts`, `detox/index.ts`, `detox/jest-preset.cjs` |
@@ -195,19 +193,22 @@ bridge always addresses the **shell worker**, never an undifferentiated sim.
195
193
  | AgentHost + sessions | Agent-routing extension of the host: owns the single FIFO reader per agent session and fans `agent:event`/`session-status` pushes to every WS subscriber. Backed by agent-sessions + attached-projects stores. | `src/host/agent-host.ts`, `src/agent-sessions.ts`, `src/attached-projects.ts`, `src/agent-daemon-client.ts` |
196
194
  | host proxies + dev-server scan | Host-side helpers so a cross-origin tenant worker can reach local dev servers and same-origin APIs: `/__server-scan` discovers running metro/expo/vxrn/one servers; fetch + websocket proxies relay guest-app network through the daemon. | `src/host/fetch-proxy-handler.ts`, `src/host/websocket-proxy.ts`, `scripts/dev-server-scanner.ts`, `src/dev-bundle-resolution.ts` |
197
195
  | runtime delivery + rnx home | Versioned engine runtime management. The shared machinery owns manifest fetch, sha256 verification, channels, and auto-update. rnx binds it to `https://contrast.dev` and `~/.sootsim/runtimes/<version>`. A repo can select `runtimeVersion` without changing the machine default. | `src/runtime-delivery.ts`, `packages/contrast-runtime-delivery/`, `src/home-paths.ts`, `src/runtime-assets.ts`, `scripts/postinstall.cjs` |
198
- | `rnxsim/vite` + `rnxsim/metro` (runtime serving) | Published bundler plugins. Both serve the installed engine runtime at `/__soot/` from `~/.sootsim/runtimes/<version>` so an app's own dev server can host the sim shell; the vite plugin can also launch the Electron app. | `src/vite-plugin-one.ts`, `src/metro-plugin.ts`, `src/runtime-assets.ts` |
196
+ | `rnxsim/vite` + `rnxsim/metro` (runtime serving) | Published bundler plugins. Both serve the installed engine runtime at `/__soot/` from `~/.sootsim/runtimes/<version>` so an app's own dev server can host the sim shell; either plugin can opt into a named Electron window. | `src/vite-plugin-one.ts`, `src/metro-plugin.ts`, `src/runtime-assets.ts` |
199
197
  | `sootsim()` vite resolver plugin (internal) | The large RN-resolution / native-stub vite plugin: aliases `react-native` to the engine shim, native packages to `@sootsim/compat` stubs, applies worklets/babel transforms, and instantiates `SootSimBridgeHost` in its dev server. Distinct from the published `rnxsim/vite` export. | `src/vite-plugin.ts`, `src/worklets-babel.ts`, `packages/compat/src/stub-manifest.ts` |
200
198
  | skills registry | rnx agent skills (`rnx-setup`, `rnx-debug`, `rnx-test`, `rnx-visual`, and the generated `contrast` index) installable with `rnx skill`. | `skills/*.md`, `src/skills/registry.ts`, `src/skills/types.ts` |
201
199
 
202
200
  ## Getting started
203
201
 
204
202
  ```sh
205
- curl -fsSL https://rnxsim.com/install.sh | sh
206
- rnx setup # guided one-time machine setup
207
- rnx open 8081 # load a running metro/expo dev server
203
+ npx rnxsim # discover your running app and open it in the simulator
208
204
  rnx open /settings # dispatch a React Native deep link into that app
209
205
  ```
210
206
 
207
+ Start Metro, Expo, One, or another supported React Native development server
208
+ the way you normally do, then run `npx rnxsim` from any directory. If one app is
209
+ running, rnx opens it immediately. If several are running, a human terminal gets
210
+ one short choice; CI and agents choose deterministically and never prompt.
211
+
211
212
  The first `rnx open` launches an isolated Chrome for Testing sim. The CLI
212
213
  resolves Playwright from the app or CLI install, asks that exact package for
213
214
  its required browser executable, and runs that package's installer once when
@@ -217,21 +218,40 @@ requested target. Use `rnx do reload` when the target has not changed.
217
218
  Reserve `--new` for a genuinely concurrent sim; repeated new browser trees
218
219
  consume memory and CPU on shared machines.
219
220
 
220
- The public shell script is the recommended, inspectable bootstrap path;
221
- `rnx` is the normal path afterward. Manual installs can use `npm install
222
- --global rnxsim`, `bun add --global rnxsim`, or `pnpm add --global rnxsim`,
223
- followed by `rnx setup`. There is no per-app install:
221
+ There is no required global or per-app install. Optional global installs can
222
+ use `npm install --global rnxsim`, `bun add --global rnxsim`, or
223
+ `pnpm add --global rnxsim`. `rnx setup` is optional machine/repository tuning:
224
224
  start your Metro, Expo, or One dev server the way you already do, then open it
225
225
  from the CLI. There is no iOS native build in the inner loop; Metro alone is
226
- enough. `rnx open` starts a local bridge when no daemon is reachable; the
227
- guided setup installs the faster background daemon so later CLI commands reuse
228
- one server. A desktop GUI (Electron) is optional on top, via `rnx desktop
226
+ enough. The manifest bundle path and query remain authoritative. When both the
227
+ discovery request and manifest target are loopback addresses, RNX keeps the
228
+ loopback host that answered discovery so an IPv6-only listener is not reopened
229
+ through an unreachable IPv4 address. `rnx open` starts a local bridge when no
230
+ daemon is reachable; the guided setup installs the faster background daemon so
231
+ later CLI commands reuse one server. A desktop GUI (Electron) is optional on
232
+ top, via `rnx desktop
229
233
  install`. Native Electron windows created by `rnx open` belong to that CLI
230
234
  session and close when the session exits; `rnx desktop` keeps the persistent
231
235
  desktop-app lifecycle. In Electron, `File > New Window`
232
236
  duplicates the focused simulator, while `File > New Simulator >` opens a new
233
237
  window for a selected device (same device list as the `Window >` menu).
234
238
 
239
+ ### Privacy
240
+
241
+ rnx does not phone home by default. On an unsigned-in first run, its only
242
+ internet request is the runtime manifest/archive it is about to execute. The
243
+ first human run asks once, defaults to no, and never prompts in a non-TTY shell,
244
+ CI, or an agent. An automated first run is declined for that process without
245
+ consuming the later human choice. Legacy browser and Electron telemetry senders
246
+ are removed rather than being enabled by this consent.
247
+
248
+ Opt-in diagnostics are limited to package name/version/unsupported API or error
249
+ class. rnx never sends source, file paths, project names, app content, commands,
250
+ identity, screenshots, or recordings as diagnostics. Cloud commands such as
251
+ login, upload, billing, issue reporting, generated flows, and screenshot decks
252
+ contact their documented service only when explicitly invoked. The complete
253
+ enforced network contract is in [`specs/rnx-privacy.md`](../../specs/rnx-privacy.md).
254
+
235
255
  To uninstall the machine daemon and cached runtimes, run
236
256
  `rnx daemon uninstall`. It stops current and legacy launchd / systemd
237
257
  services, removes daemon logs, and removes `~/.sootsim/` (runtimes, profiles,
@@ -308,7 +328,7 @@ The Vite plugin serves the installed RNX runtime from your existing dev server.
308
328
  import { rnxPlugin } from 'rnxsim/vite'
309
329
 
310
330
  export default {
311
- plugins: [rnxPlugin()],
331
+ plugins: [rnxPlugin({ open: { appName: 'My App' } })],
312
332
  }
313
333
  ```
314
334
 
@@ -319,13 +339,18 @@ const { withRNX } = require('rnxsim/metro')
319
339
 
320
340
  const config = getDefaultConfig(__dirname)
321
341
 
322
- module.exports = withRNX(config, { devServer: true })
342
+ module.exports = withRNX(config, { open: { appName: 'My App' } })
323
343
  ```
324
344
 
325
- The Metro wrapper has two independent opt-ins. `devServer` serves the RNX shell
326
- from the Metro process. `productionBundles` lets explicitly marked production
327
- bundle requests use the RNX host implementations of React and React Native
328
- while the rest of the app receives production transforms.
345
+ The `open` option serves the RNX shell and opens the app in its own named
346
+ Electron window. `open: true` derives the name from `package.json`; use the
347
+ object form when the display name differs. Run `rnx desktop install` once before
348
+ enabling it. `SOOTSIM_NO_OPEN=1` keeps the server headless. The Metro wrapper
349
+ also has two independent non-window opt-ins.
350
+ `devServer` serves the RNX shell from the Metro process. `productionBundles`
351
+ lets explicitly marked production bundle requests use the RNX host
352
+ implementations of React and React Native while the rest of the app receives
353
+ production transforms.
329
354
 
330
355
  ```js
331
356
  module.exports = withRNX(config, { productionBundles: true })
@@ -516,10 +541,13 @@ serves its selected version from a version-specific localhost origin, so a
516
541
  background activation cannot mix an already loaded shell with chunks from the
517
542
  next runtime. Without config, each new open selects the active stable version.
518
543
  When `sootsim.config.ts` sets `runtimeVersion`, `rnx open` installs that
519
- version without changing the machine default. Running from a monorepo root
520
- resolves the only React Native app from package workspaces, then shares that app
521
- root across config, font, and splash discovery. The fallback scan is bounded and
522
- prunes dependency, build, native, fixture, and example trees.
544
+ version without changing the machine default. App config, native-linked fonts,
545
+ and splash assets all resolve from the project root advertised by the selected
546
+ development server. Bare Metro, which has no Expo manifest project root, uses
547
+ the listening development process's cwd inside that same resolver. The invoking
548
+ CLI's cwd is never an asset/config fallback. Asset wires stay on each opened
549
+ project URL, so one daemon can serve several attached projects without sharing
550
+ font or splash state between them.
523
551
 
524
552
  The first interactive CLI command after this cleanup generation finishes its
525
553
  requested work, then launches the same safe default plan in a detached
package/cli/app-fonts.ts CHANGED
@@ -36,44 +36,26 @@ function localOriginOf(value: string): string | null {
36
36
  return null
37
37
  }
38
38
 
39
- function hasPackageJson(dir: string): boolean {
40
- try {
41
- return fs.statSync(path.join(dir, 'package.json')).isFile()
42
- } catch {
43
- return false
44
- }
45
- }
46
39
  export function discoverNativeLinkedAppFonts(opts: {
47
40
  bundleUrl: string
48
- repoDir?: string
49
- exactProjectDir?: boolean
41
+ projectDir: string
50
42
  platform?: string
51
43
  }): DiscoveredAppFontSpec[] {
52
44
  const metroOrigin = localOriginOf(opts.bundleUrl)
53
45
  if (!metroOrigin) return []
54
46
 
55
- const explicit = opts.repoDir || process.env.CONTRAST_REPO_DIR
56
- const startDir = explicit ? path.resolve(explicit) : process.cwd()
57
-
58
- // if we're using cwd() (no explicit repo) and there's no package.json,
59
- // assume we're not in a project tree — bail immediately instead of
60
- // recursing the filesystem.
61
- if (!explicit && !hasPackageJson(startDir)) return []
62
-
63
- const project = resolveAppProject(startDir, { trustStart: opts.exactProjectDir })
47
+ const project = resolveAppProject(opts.projectDir)
64
48
  const selectedAppDir = project?.appDir ?? null
65
49
  if (!selectedAppDir) return []
66
50
 
67
51
  const platform = (opts.platform || process.env.CONTRAST_PLATFORM || 'ios').toLowerCase()
68
52
 
69
53
  let getConfig: ExpoConfigGetter | null = null
70
- // use the app config dirs + startDir as resolve paths. real repos hoist
71
- // @expo/config to the root node_modules so startDir is sufficient; tests
72
- // that mock @expo/config can set CONTRAST_REPO_DIR to point where the mock lives.
73
- if (explicit || project) {
54
+ // resolve from the exact app root advertised by the development server.
55
+ if (project) {
74
56
  try {
75
57
  const modulePath = require.resolve('@expo/config', {
76
- paths: [selectedAppDir, startDir],
58
+ paths: [selectedAppDir],
77
59
  })
78
60
  const mod = require(modulePath) as { getConfig?: unknown }
79
61
  if (typeof mod.getConfig === 'function') {
@@ -283,11 +265,9 @@ export function discoverNativeLinkedAppFonts(opts: {
283
265
  // `sootsim open 8081` run from ~/soot against a 3pc metro), so scanning cwd for
284
266
  // font declarations finds nothing.
285
267
  //
286
- // a bare react-native packager serves no such manifest it answers `/` with an
287
- // HTML page so fall back to asking the OS which directory the listening
288
- // process runs from. bare apps are exactly the ones that link fonts through
289
- // react-native.config.js `assets`, so without this their fonts are never
290
- // discoverable and the appFonts wire comes out empty.
268
+ // a bare react-native packager serves no such manifest, so the same resolver
269
+ // asks the OS for the listening process's cwd. that is still the development
270
+ // server's project identity; the invoking CLI's cwd is never consulted.
291
271
  export async function resolveMetroProjectRoot(
292
272
  bundleUrl: string,
293
273
  opts?: { timeoutMs?: number },
@@ -16,30 +16,6 @@ export const SOOTSIM_APP_CONFIG_NAMES = [
16
16
  'sootsim.config.cjs',
17
17
  ] as const
18
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
19
  export interface ResolvedAppProject {
44
20
  appDir: string
45
21
  configPath: string | null
@@ -63,169 +39,21 @@ export function findSootSimAppConfig(dir: string): string | null {
63
39
  return firstNamedFile(dir, SOOTSIM_APP_CONFIG_NAMES)
64
40
  }
65
41
 
66
- function readWorkspacePatterns(rootDir: string): string[] {
42
+ export function resolveAppProject(projectRoot: string): ResolvedAppProject | null {
43
+ const appDir = path.resolve(projectRoot)
67
44
  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
- : []
45
+ if (!fs.statSync(appDir).isDirectory()) return null
79
46
  } 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
- }
47
+ return null
175
48
  }
176
49
 
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 }
50
+ const configPath = findSootSimAppConfig(appDir)
51
+ if (
52
+ !configPath &&
53
+ !findExpoAppConfig(appDir) &&
54
+ !fs.existsSync(path.join(appDir, 'package.json'))
55
+ ) {
56
+ return null
201
57
  }
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
58
+ return { appDir, configPath }
231
59
  }
package/cli/app-splash.ts CHANGED
@@ -33,32 +33,18 @@ function localOriginOf(value: string): string | null {
33
33
  return null
34
34
  }
35
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
36
  // discover the app's configured splash by evaluating its expo config on the
45
37
  // filesystem (mirrors discoverNativeLinkedAppFonts exactly). returns null when
46
38
  // no splash is configured, no app config is found, or the bundle isn't local.
47
39
  export function discoverConfigPluginAppSplash(opts: {
48
40
  bundleUrl: string
49
- repoDir?: string
50
- exactProjectDir?: boolean
41
+ projectDir: string
51
42
  platform?: string
52
43
  }): DiscoveredAppSplashSpec | null {
53
44
  const metroOrigin = localOriginOf(opts.bundleUrl)
54
45
  if (!metroOrigin) return null
55
46
 
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 })
47
+ const project = resolveAppProject(opts.projectDir)
62
48
  const selectedAppDir = project?.appDir ?? null
63
49
  if (!selectedAppDir) return null
64
50
 
@@ -67,7 +53,7 @@ export function discoverConfigPluginAppSplash(opts: {
67
53
  let getConfig: ExpoConfigGetter | null = null
68
54
  try {
69
55
  const modulePath = require.resolve('@expo/config', {
70
- paths: [selectedAppDir, startDir],
56
+ paths: [selectedAppDir],
71
57
  })
72
58
  const mod = require(modulePath) as { getConfig?: unknown }
73
59
  if (typeof mod.getConfig === 'function') {
package/cli/bin.ts CHANGED
@@ -89,22 +89,15 @@ const isAutomaticCleanupWorker =
89
89
  parsed.commandArgs.length === 1 &&
90
90
  parsed.commandArgs[0] === '--automatic-worker'
91
91
 
92
- // invocation telemetry. enqueued here, flushed by command-specific paths
93
- // (most commands run long enough that the beforeExit drain catches the
94
- // event; fast-exit paths like --version/--help explicitly flush below).
95
- if (!isAutomaticCleanupWorker) {
96
- const { trackCliEvent } = await import('./telemetry')
97
- trackCliEvent({
98
- event: 'cli_command_invoked',
99
- properties: {
100
- command:
101
- parsed.command || (parsed.version ? 'version' : parsed.help ? 'help' : 'none'),
102
- arg_count: parsed.commandArgs.length,
103
- platform: process.platform,
104
- arch: process.arch,
105
- node_version: process.versions.node,
106
- },
107
- })
92
+ if (
93
+ !isAutomaticCleanupWorker &&
94
+ !parsed.help &&
95
+ !parsed.version &&
96
+ parsed.command !== 'config' &&
97
+ parsed.command !== 'telemetry'
98
+ ) {
99
+ const { ensurePrivacyChoice } = await import('./privacy')
100
+ await ensurePrivacyChoice({ prompt: true })
108
101
  }
109
102
 
110
103
  // one-shot "engine upgraded in the background" banner. the daemon's hourly
@@ -176,9 +169,8 @@ async function scheduleAutomaticCleanup(): Promise<void> {
176
169
  }
177
170
  }
178
171
 
179
- // fast-exit helper: every path that calls process.exit before reaching a
180
- // command function would lose the cli_command_invoked event because node
181
- // doesn't await pending fetches on exit. flush first, then exit.
172
+ // fast-exit helper: diagnostic error shapes are best-effort, but explicit exit
173
+ // paths still give a permitted queued diagnostic a chance to drain.
182
174
  async function exitWithFlush(code: number): Promise<never> {
183
175
  const { flushCliTelemetry } = await import('./telemetry')
184
176
  await flushCliTelemetry()
@@ -236,16 +228,11 @@ if (device || theme) {
236
228
  settingsStore.apply(overrides)
237
229
  }
238
230
 
239
- // no command, no args: show the clean surface. setup/open own actual work.
231
+ // no command, no args: discover the running app and open it directly. explicit
232
+ // `rnx setup` remains available for repository and background-service choices.
240
233
  if (!parsed.command && parsed.commandArgs.length === 0) {
241
- const { runSetup, shouldRunSetupForBareCommand } = await import('./commands/setup')
242
- if (shouldRunSetupForBareCommand()) {
243
- await runSetup(['--from-welcome'])
244
- await exitWithFlush(0)
245
- }
246
- const { printHelp } = await import('./help')
247
- printHelp()
248
- await exitWithFlush(0)
234
+ const { runWelcome } = await import('./commands/welcome')
235
+ await exitWithFlush(await runWelcome())
249
236
  }
250
237
 
251
238
  // route to command. an empty command at this point only happens if the