rnxsim 0.1.313 → 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 (297) 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-XZ2KTPCU.js → agent-7YBDCYMA.js} +2 -2
  126. package/dist-cli/chunks/{agent-wrapper-JJYYW2WH.js → agent-wrapper-2GHFBHCR.js} +2 -2
  127. package/dist-cli/chunks/{app-fonts-IXRNQG6B.js → app-fonts-RSNVPQSU.js} +2 -2
  128. package/dist-cli/chunks/{assert-54T5SK5F.js → assert-XCMX3XJX.js} +2 -2
  129. package/dist-cli/chunks/{auth-FI5UDI45.js → auth-B442HRAX.js} +2 -2
  130. package/dist-cli/chunks/{beta-JV6UKADW.js → beta-XJ55JK3M.js} +2 -2
  131. package/dist-cli/chunks/camera-UGYSSLIK.js +33 -0
  132. package/dist-cli/chunks/{chunk-3NV2NCNX.js → chunk-277AEQZX.js} +2 -2
  133. package/dist-cli/chunks/{chunk-2YR5BGA5.js → chunk-2JNSK774.js} +2 -2
  134. package/dist-cli/chunks/{chunk-RSZWCKNT.js → chunk-32WOTSTR.js} +3 -3
  135. package/dist-cli/chunks/{chunk-WEXDAC74.js → chunk-3S753SNQ.js} +2 -2
  136. package/dist-cli/chunks/{chunk-IJO63TDP.js → chunk-46ZOLOYA.js} +2 -2
  137. package/dist-cli/chunks/{chunk-WUSWBCWA.js → chunk-5L7ELDQL.js} +8 -9
  138. package/dist-cli/chunks/{chunk-WF3T4SVI.js → chunk-7OOPFSQS.js} +2 -2
  139. package/dist-cli/chunks/{chunk-TZFFR3SD.js → chunk-7SV3RPRW.js} +2 -2
  140. package/dist-cli/chunks/{chunk-WWZIXIRD.js → chunk-7ZC35MOU.js} +1 -1
  141. package/dist-cli/chunks/chunk-AMG5E6CC.js +9 -0
  142. package/dist-cli/chunks/{chunk-WINYQ44O.js → chunk-APWNH3A4.js} +1 -1
  143. package/dist-cli/chunks/chunk-B57XUKY3.js +4 -0
  144. package/dist-cli/chunks/{chunk-YDGQTMQL.js → chunk-C2NL26TD.js} +1 -1
  145. package/dist-cli/chunks/{chunk-NMF2ZMZQ.js → chunk-D2FNUWAB.js} +4 -4
  146. package/dist-cli/chunks/{chunk-YIFT42WN.js → chunk-E5T4XSJ3.js} +2 -2
  147. package/dist-cli/chunks/{chunk-46EUUFJ5.js → chunk-EAC34EQS.js} +1 -1
  148. package/dist-cli/chunks/{chunk-OVFJFXUD.js → chunk-EG32ML36.js} +2 -2
  149. package/dist-cli/chunks/{chunk-5YJCOWCH.js → chunk-FXUAC6D5.js} +1 -1
  150. package/dist-cli/chunks/chunk-GQSL4USA.js +6 -0
  151. package/dist-cli/chunks/{chunk-VFCMSYZK.js → chunk-HAXW27SS.js} +2 -2
  152. package/dist-cli/chunks/{chunk-2D2UPBBR.js → chunk-IZAHPAN6.js} +1 -1
  153. package/dist-cli/chunks/{chunk-7GN3LVWB.js → chunk-J62KM5TB.js} +2 -2
  154. package/dist-cli/chunks/{chunk-BTWORNNG.js → chunk-JEMCD5E4.js} +1 -1
  155. package/dist-cli/chunks/{chunk-DCEMHR2Y.js → chunk-JZS3Q37N.js} +2 -2
  156. package/dist-cli/chunks/{chunk-D4FFVGI5.js → chunk-KR4JON7D.js} +1 -1
  157. package/dist-cli/chunks/chunk-KWCYKQIQ.js +4 -0
  158. package/dist-cli/chunks/{chunk-VNQEB4L7.js → chunk-MCWPL644.js} +2 -2
  159. package/dist-cli/chunks/chunk-MJYK3N2I.js +4 -0
  160. package/dist-cli/chunks/{chunk-5TEF3ET3.js → chunk-O6TRIZNS.js} +2 -2
  161. package/dist-cli/chunks/{chunk-QKDWYITG.js → chunk-P7XL2E73.js} +3 -3
  162. package/dist-cli/chunks/{chunk-BBULZ7CG.js → chunk-PFQTUKQ4.js} +62 -87
  163. package/dist-cli/chunks/{chunk-GGRX24GF.js → chunk-PG5RZCTN.js} +2 -2
  164. package/dist-cli/chunks/{chunk-W6K4EFPH.js → chunk-QGRI2Z4M.js} +2 -2
  165. package/dist-cli/chunks/{chunk-ZMJD5GEC.js → chunk-QLXXE7GE.js} +1 -1
  166. package/dist-cli/chunks/{chunk-VZXWHRUZ.js → chunk-QOJJJWJE.js} +89 -133
  167. package/dist-cli/chunks/{chunk-OZSSI4WN.js → chunk-RWZY5427.js} +2 -2
  168. package/dist-cli/chunks/{chunk-UC6U3MML.js → chunk-RZKU2K3J.js} +2 -2
  169. package/dist-cli/chunks/{chunk-IJ5CAZZC.js → chunk-SBV4IK4H.js} +1 -1
  170. package/dist-cli/chunks/{chunk-DZS6WPUI.js → chunk-SGMVFFMK.js} +1 -1
  171. package/dist-cli/chunks/{chunk-OHAZNXLK.js → chunk-TAX4UT2N.js} +1 -1
  172. package/dist-cli/chunks/{chunk-5DHC6KHQ.js → chunk-TUOFAWXT.js} +1 -1
  173. package/dist-cli/chunks/{chunk-GASE6UBA.js → chunk-UHZLOHGP.js} +1 -1
  174. package/dist-cli/chunks/{chunk-F5ZRSS3C.js → chunk-VC7V76U3.js} +1 -1
  175. package/dist-cli/chunks/{chunk-RTN5C5RL.js → chunk-VQVMLW4U.js} +1 -1
  176. package/dist-cli/chunks/{chunk-WMIIKMGK.js → chunk-VUKKYPZN.js} +2 -2
  177. package/dist-cli/chunks/{chunk-5TPRP5QT.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-HI5TFJWN.js → chunk-XULEACM4.js} +2 -2
  181. package/dist-cli/chunks/{chunk-MJRLLB4R.js → chunk-YFSDM7AX.js} +4 -4
  182. package/dist-cli/chunks/chunk-YWI3UEVX.js +5 -0
  183. package/dist-cli/chunks/{chunk-XEVZYVIW.js → chunk-ZBSJO4NB.js} +10 -9
  184. package/dist-cli/chunks/{cleanup-P27PA6JI.js → cleanup-EYLGCA6Z.js} +2 -2
  185. package/dist-cli/chunks/cli-version-LL2UGIHE.js +4 -0
  186. package/dist-cli/chunks/{compat-ZD65FED3.js → compat-TLJYHB4E.js} +2 -2
  187. package/dist-cli/chunks/{config-XMJRNM2A.js → config-4JWUOEXK.js} +2 -2
  188. package/dist-cli/chunks/{control-KMIQT3QP.js → control-HPAOYF4N.js} +2 -2
  189. package/dist-cli/chunks/daemon-OYLASXLE.js +4 -0
  190. package/dist-cli/chunks/{debug-PT4HOP7N.js → debug-4BKXF6KI.js} +5 -5
  191. package/dist-cli/chunks/{desktop-S3FG72AK.js → desktop-ZQ6ZD2S6.js} +3 -3
  192. package/dist-cli/chunks/{detox-B3D4IFCN.js → detox-WNPASSS3.js} +2 -2
  193. package/dist-cli/chunks/{device-XBNDSB2R.js → device-BXLXVG3V.js} +2 -2
  194. package/dist-cli/chunks/{diagnose-HMQXJE5N.js → diagnose-3QX7W5RW.js} +2 -2
  195. package/dist-cli/chunks/{disk-cleanup-BLCZ5BSZ.js → disk-cleanup-KSWKI7WB.js} +2 -2
  196. package/dist-cli/chunks/drivers-EPIEFF7P.js +4 -0
  197. package/dist-cli/chunks/{film-BJGTBYZB.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-ANOYADHM.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-FJ737MWG.js → login-IJAPUZHI.js} +4 -4
  205. package/dist-cli/chunks/{logout-ZCNMMHMY.js → logout-QMXDFU2X.js} +2 -2
  206. package/dist-cli/chunks/{maestro-SZTNKLDF.js → maestro-ZXU3YCVX.js} +3 -3
  207. package/dist-cli/chunks/{maestro-generate-DCFAIZ4H.js → maestro-generate-PYB5QY7K.js} +3 -3
  208. package/dist-cli/chunks/{mode-GRMQCRXR.js → mode-WGUCL5FZ.js} +2 -2
  209. package/dist-cli/chunks/{optional-demo-registry-W36EWFFB.js → optional-demo-registry-WH2O6H36.js} +2 -2
  210. package/dist-cli/chunks/{perf-QYBAAUZG.js → perf-TOD3UFAH.js} +2 -2
  211. package/dist-cli/chunks/{permissions-3QCQ6VF4.js → permissions-I5BRJGTB.js} +2 -2
  212. package/dist-cli/chunks/{record-QPPC2S4E.js → record-ZYL2FYSK.js} +3 -3
  213. package/dist-cli/chunks/{report-issue-7NMFP4HK.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-67AECJFB.js → screenshot-command-7ANLODZY.js} +7 -7
  217. package/dist-cli/chunks/{screenshot-layers-ASWBYPJL.js → screenshot-layers-A7FYXSVU.js} +3 -3
  218. package/dist-cli/chunks/{screenshots-capture-PXA3HFQK.js → screenshots-capture-WT2ZY6CB.js} +2 -2
  219. package/dist-cli/chunks/serve-TG5WKA4V.js +44 -0
  220. package/dist-cli/chunks/{setup-7DWPMRSB.js → setup-PCWLD22V.js} +2 -2
  221. package/dist-cli/chunks/{skills-S3Y22TUA.js → skills-27ZHWCQS.js} +2 -2
  222. package/dist-cli/chunks/state-ZQVY46ZO.js +14 -0
  223. package/dist-cli/chunks/{storage-XUIMJWAJ.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-TMPLQPSP.js → timeline-RAJJFQT6.js} +2 -2
  227. package/dist-cli/chunks/{upgrade-7HDSIM7K.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-DG3WBYD3.js → web-XBUTBVGR.js} +2 -2
  231. package/dist-cli/chunks/{what-happened-XFVUTZR7.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 +480 -280
  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-VL73YIKP.js +0 -22
  277. package/dist-cli/chunks/chunk-4NPPOV2N.js +0 -5
  278. package/dist-cli/chunks/chunk-FSUYIVJ6.js +0 -9
  279. package/dist-cli/chunks/chunk-G2WW6L2C.js +0 -23
  280. package/dist-cli/chunks/chunk-KTHV3RUS.js +0 -26
  281. package/dist-cli/chunks/chunk-LF2ZVT7O.js +0 -6
  282. package/dist-cli/chunks/chunk-NFK7T35W.js +0 -4
  283. package/dist-cli/chunks/chunk-TIVZIMMW.js +0 -4
  284. package/dist-cli/chunks/cli-version-WWLPBDQ7.js +0 -4
  285. package/dist-cli/chunks/daemon-G2ME7NLB.js +0 -4
  286. package/dist-cli/chunks/drivers-LDECZGP2.js +0 -4
  287. package/dist-cli/chunks/flow-UEQNVTU7.js +0 -4
  288. package/dist-cli/chunks/help-T5FYSVGB.js +0 -4
  289. package/dist-cli/chunks/home-paths-GT3LFNOR.js +0 -4
  290. package/dist-cli/chunks/inspect-ZA6XF5LD.js +0 -4
  291. package/dist-cli/chunks/install-desktop-TIMUDHPL.js +0 -4
  292. package/dist-cli/chunks/runtime-XOAXMSTU.js +0 -4
  293. package/dist-cli/chunks/serve-BI2NBAXG.js +0 -44
  294. package/dist-cli/chunks/store-JTHEJLAZ.js +0 -4
  295. package/dist-cli/chunks/telemetry-ZYJGD2DB.js +0 -4
  296. package/dist-cli/chunks/upload-GMSZPWM6.js +0 -4
  297. package/dist-cli/chunks/version-HOCHZ37L.js +0 -6
@@ -0,0 +1,589 @@
1
+ // sootsim get diagnosis — composed read commands for "what just went wrong?"
2
+ //
3
+ // the demo-friendly compact diagnostic surface called out in
4
+ // plans/expensify-sootsim-agent-device-demo.md. one command, one round
5
+ // of bridge calls, one paste-able output that beats the agent-device
6
+ // sed/grep workflow in both speed and information density.
7
+ //
8
+ // usage:
9
+ // sootsim get diagnosis [--since 90s] [--include react,network,console,screen,fetch,alert]
10
+ // sootsim get diagnosis --json
11
+ //
12
+ // the output is grouped by source (timeline, network, react, console)
13
+ // rather than chronological. for chronological order use
14
+ // `sootsim what-happened` directly. `recent` is the only subcommand
15
+ // today; `diagnose <something-else>` may be added later (e.g. boot,
16
+ // scrolling-jank, idle-burn).
17
+ //
18
+ // design note: every dimension here is a single bridge call against an
19
+ // existing primitive. this command exists to remove the agent's tax of
20
+ // chaining commands and reformatting four output styles, not to add a
21
+ // new data source. when a new data source becomes worth surfacing in a
22
+ // "diagnose" pass, add it as one more parallel call here.
23
+
24
+ import { getCliIdentityKey } from '../current-sim'
25
+ import {
26
+ callInBridge,
27
+ createBridgeFromParsed,
28
+ parseBridgeCliArgs,
29
+ type WsBridge,
30
+ } from '../ws-bridge'
31
+ import type {
32
+ SootSimTimelineEvent,
33
+ SootSimTimelineQuery,
34
+ SootSimTimelineQueryResult,
35
+ } from '@sootsim/globals'
36
+
37
+ interface DiagnoseOptions {
38
+ port?: number
39
+ verbose?: boolean
40
+ }
41
+
42
+ interface NetworkEntry {
43
+ id: string
44
+ url: string
45
+ displayUrl?: string
46
+ method?: string
47
+ status?: number | null
48
+ durationMs?: number | null
49
+ error?: string | null
50
+ // observability store uses startTs (epoch ms) as the entry's start
51
+ // time. accept either name for forward-compat with any future
52
+ // rename.
53
+ startTs?: number
54
+ startedAt?: number
55
+ responseAt?: number | null
56
+ responseSize?: number | null
57
+ size?: number | null
58
+ }
59
+
60
+ interface SlowRow {
61
+ displayName: string
62
+ totalMs: number
63
+ avgMs: number
64
+ commitCount: number
65
+ slowestCommit: number
66
+ slowestMs: number
67
+ }
68
+
69
+ interface RerenderRow {
70
+ displayName: string
71
+ count?: number
72
+ renders?: number
73
+ causes?: Record<string, number>
74
+ }
75
+
76
+ interface LogEntry {
77
+ level: string
78
+ // observability ring uses `ts` (epoch ms) and `args` (string[]).
79
+ // older shapes had `t` + `message`. accept both.
80
+ ts?: number
81
+ t?: number
82
+ args?: unknown[]
83
+ message?: string
84
+ source?: string
85
+ context?: string
86
+ }
87
+
88
+ function logTs(e: LogEntry): number {
89
+ return e.ts ?? e.t ?? 0
90
+ }
91
+
92
+ function logMessage(e: LogEntry): string {
93
+ if (typeof e.message === 'string' && e.message.length > 0) return e.message
94
+ if (Array.isArray(e.args)) {
95
+ return e.args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ')
96
+ }
97
+ return ''
98
+ }
99
+
100
+ const DEFAULT_INCLUDES = new Set(['timeline', 'network', 'react', 'console', 'screen'])
101
+
102
+ function printHelp(): void {
103
+ console.log(`
104
+ rnxsim get diagnosis — compact "what just went wrong?" report
105
+
106
+ usage:
107
+ rnxsim get diagnosis [--since <duration>] [--include <csv>] [--json]
108
+
109
+ options:
110
+ --since <d> absolute window for timeline + network (e.g. 90s, 2m, 500ms; default 90s)
111
+ --include <csv> comma-separated dimensions: timeline, network, react, console, screen
112
+ (default: all)
113
+ --slow-threshold <n> network slow-request threshold in ms (default 1000)
114
+ --limit <n> rows to print per react table (default 8)
115
+ --json emit a single structured JSON document
116
+
117
+ the diagnose report runs every dimension in parallel against the same
118
+ sim and returns a grouped summary. use \`rnxsim what-happened\`
119
+ for chronological events, \`rnxsim network\` for full request inspection,
120
+ \`rnxsim perf react summary\` for the React tables on their own.
121
+
122
+ examples:
123
+ rnxsim get diagnosis
124
+ rnxsim get diagnosis --since 30s --include network,react
125
+ rnxsim get diagnosis --json > /tmp/diagnosis.json
126
+ `)
127
+ }
128
+
129
+ function parseSinceMs(value: string | undefined, fallback: number): number {
130
+ if (!value) return fallback
131
+ const m = /^(\d+(?:\.\d+)?)(ms|s|m)?$/.exec(value.trim())
132
+ if (!m) return fallback
133
+ const n = Number(m[1])
134
+ const unit = m[2] ?? 'ms'
135
+ return unit === 's' ? n * 1000 : unit === 'm' ? n * 60_000 : n
136
+ }
137
+
138
+ function parseCsv(value: string | undefined): Set<string> {
139
+ if (!value) return DEFAULT_INCLUDES
140
+ const out = new Set<string>()
141
+ for (const raw of value.split(',')) {
142
+ const trimmed = raw.trim().toLowerCase()
143
+ if (trimmed) out.add(trimmed)
144
+ }
145
+ return out
146
+ }
147
+
148
+ function valueOf(args: string[], flag: string): string | undefined {
149
+ const idx = args.indexOf(flag)
150
+ if (idx >= 0 && idx + 1 < args.length) return args[idx + 1]
151
+ const prefix = `${flag}=`
152
+ return args.find((a) => a.startsWith(prefix))?.slice(prefix.length)
153
+ }
154
+
155
+ function numberFlag(args: string[], flag: string, fallback: number): number {
156
+ const raw = valueOf(args, flag)
157
+ if (raw === undefined) return fallback
158
+ const n = Number(raw)
159
+ return Number.isFinite(n) && n > 0 ? Math.round(n) : fallback
160
+ }
161
+
162
+ interface TimelineSlice {
163
+ events: SootSimTimelineEvent[]
164
+ byKind: Record<string, number>
165
+ total: number
166
+ errors: SootSimTimelineEvent[]
167
+ alerts: SootSimTimelineEvent[]
168
+ screens: SootSimTimelineEvent[]
169
+ }
170
+
171
+ async function fetchTimeline(bridge: WsBridge, sinceMs: number): Promise<TimelineSlice> {
172
+ const query: SootSimTimelineQuery = {
173
+ limit: 500,
174
+ since: Date.now() - sinceMs,
175
+ }
176
+ const result = await callInBridge<SootSimTimelineQueryResult>(
177
+ bridge,
178
+ 'SootSim.bridges.timeline.recent',
179
+ query,
180
+ )
181
+ const events = result.events ?? []
182
+ const byKind: Record<string, number> = {}
183
+ const errors: SootSimTimelineEvent[] = []
184
+ const alerts: SootSimTimelineEvent[] = []
185
+ const screens: SootSimTimelineEvent[] = []
186
+ for (const ev of events) {
187
+ byKind[ev.kind] = (byKind[ev.kind] ?? 0) + 1
188
+ const data = ev.data as Record<string, unknown> | null
189
+ const level = data && typeof data === 'object' ? (data.level as string) : undefined
190
+ if (ev.kind === 'console' && (level === 'error' || level === 'warn')) {
191
+ errors.push(ev)
192
+ }
193
+ if (ev.kind === 'alert' || ev.kind === 'actionsheet' || ev.kind === 'picker') {
194
+ alerts.push(ev)
195
+ }
196
+ if (ev.kind === 'screen' || ev.kind === 'route') {
197
+ screens.push(ev)
198
+ }
199
+ }
200
+ return {
201
+ events,
202
+ byKind,
203
+ total: events.length,
204
+ errors,
205
+ alerts,
206
+ screens,
207
+ }
208
+ }
209
+
210
+ interface NetworkSlice {
211
+ total: number
212
+ failed: NetworkEntry[]
213
+ slow: NetworkEntry[]
214
+ inFlight: number
215
+ slowThresholdMs: number
216
+ }
217
+
218
+ async function fetchNetwork(
219
+ bridge: WsBridge,
220
+ sinceMs: number,
221
+ slowThresholdMs: number,
222
+ ): Promise<NetworkSlice | { error: string }> {
223
+ // re-uses the observability store the existing `network` command
224
+ // reads from. local filtering keeps this command independent of any
225
+ // new bridge surface and lets it run against today's engine.
226
+ const result = (await bridge.send({
227
+ type: 'evaluate',
228
+ code: `(() => {
229
+ const obs = window.__sootsimObservability;
230
+ if (!obs) return { ok: false };
231
+ return { ok: true, entries: obs.network.getSnapshot() };
232
+ })()`,
233
+ })) as { ok: boolean; entries?: NetworkEntry[] }
234
+ if (!result || !result.ok) {
235
+ return { error: 'observability bridge not installed' }
236
+ }
237
+ const all = result.entries ?? []
238
+ const cutoff = Date.now() - sinceMs
239
+ // observability ring uses startTs; older shapes used startedAt. accept
240
+ // both so this command works against both shapes without a feature
241
+ // detection round-trip.
242
+ const inWindow = all.filter((e) => (e.startTs ?? e.startedAt ?? 0) >= cutoff)
243
+ const failed = inWindow.filter(
244
+ (e) => !!e.error || (e.status != null && e.status >= 400),
245
+ )
246
+ const slow = inWindow
247
+ .filter((e) => e.durationMs != null && e.durationMs >= slowThresholdMs)
248
+ .sort((a, b) => (b.durationMs ?? 0) - (a.durationMs ?? 0))
249
+ const inFlight = inWindow.filter((e) => e.durationMs == null).length
250
+ return {
251
+ total: inWindow.length,
252
+ failed,
253
+ slow,
254
+ inFlight,
255
+ slowThresholdMs,
256
+ }
257
+ }
258
+
259
+ interface ReactSlice {
260
+ commits: number
261
+ slow: SlowRow[]
262
+ rerenders: RerenderRow[]
263
+ durationWarning?: string
264
+ error?: string
265
+ }
266
+
267
+ async function fetchReact(bridge: WsBridge, limit: number): Promise<ReactSlice> {
268
+ const [slowResult, rerendersResult] = (await Promise.all([
269
+ callInBridge(bridge, 'SootSim.bridges.reactProfile.slow', { limit }),
270
+ callInBridge(bridge, 'SootSim.bridges.reactProfile.rerenders', { limit }),
271
+ ])) as [
272
+ {
273
+ rows?: SlowRow[]
274
+ commits?: number
275
+ error?: string
276
+ durationWarning?: string
277
+ },
278
+ { rows?: RerenderRow[]; commits?: number; error?: string },
279
+ ]
280
+ return {
281
+ commits: slowResult.commits ?? rerendersResult.commits ?? 0,
282
+ slow: slowResult.rows ?? [],
283
+ rerenders: rerendersResult.rows ?? [],
284
+ durationWarning: slowResult.durationWarning,
285
+ error: slowResult.error ?? rerendersResult.error,
286
+ }
287
+ }
288
+
289
+ async function fetchConsoleErrors(
290
+ bridge: WsBridge,
291
+ sinceMs: number,
292
+ ): Promise<{ entries: LogEntry[]; total: number } | { error: string }> {
293
+ const result = (await bridge.send({
294
+ type: 'evaluate',
295
+ code: `(() => {
296
+ const obs = window.__sootsimObservability;
297
+ if (!obs) return { ok: false };
298
+ return { ok: true, entries: obs.logs.getSnapshot() };
299
+ })()`,
300
+ })) as { ok: boolean; entries?: LogEntry[] }
301
+ if (!result || !result.ok) {
302
+ return { error: 'observability bridge not installed' }
303
+ }
304
+ const cutoff = Date.now() - sinceMs
305
+ const all = result.entries ?? []
306
+ const errs = all.filter(
307
+ (e) => logTs(e) >= cutoff && (e.level === 'error' || e.level === 'warn'),
308
+ )
309
+ return { entries: errs.slice(-20), total: errs.length }
310
+ }
311
+
312
+ function formatNetworkRow(entry: NetworkEntry): string {
313
+ const status = entry.error ? 'err' : entry.status != null ? String(entry.status) : '...'
314
+ const method = (entry.method ?? 'GET').padEnd(6)
315
+ const dur = entry.durationMs != null ? `${entry.durationMs.toFixed(0)}ms` : '...'
316
+ const url = entry.displayUrl ?? entry.url
317
+ // shorten long urls to 96 chars in the diagnose report — full
318
+ // detail is one `sootsim network get <id>` away.
319
+ const shortUrl = url.length > 96 ? `${url.slice(0, 92)}…` : url
320
+ return ` ${status.padStart(3)} ${method} ${dur.padStart(7)} ${shortUrl} (${entry.id})`
321
+ }
322
+
323
+ function formatTimelineRow(ev: SootSimTimelineEvent, anchorMs: number): string {
324
+ const dt = ((ev.t - anchorMs) / 1000).toFixed(2)
325
+ const sign = ev.t >= anchorMs ? '-' : '+'
326
+ const data = ev.data as Record<string, unknown> | null
327
+ let payload = ''
328
+ if (data && typeof data === 'object') {
329
+ const message = (data.message ?? '').toString().slice(0, 120)
330
+ const title = (data.title ?? data.name ?? data.path ?? data.url ?? '').toString()
331
+ const phase = (data.phase ?? '').toString()
332
+ payload = [phase, title, message].filter(Boolean).join(' · ')
333
+ }
334
+ return ` -${dt.padStart(5)}s [${ev.kind}] ${payload}`
335
+ }
336
+
337
+ function formatTable(headers: string[], rows: string[][], indent: string): string {
338
+ if (rows.length === 0) return `${indent}(no rows)`
339
+ const widths = headers.map((h, i) =>
340
+ Math.max(h.length, ...rows.map((row) => row[i]?.length ?? 0)),
341
+ )
342
+ const line = (cells: string[]) =>
343
+ cells
344
+ .map((cell, i) => cell.padEnd(widths[i]))
345
+ .join(' ')
346
+ .trimEnd()
347
+ return [
348
+ line(headers),
349
+ line(headers.map((h) => '-'.repeat(h.length))),
350
+ ...rows.map(line),
351
+ ]
352
+ .map((row) => `${indent}${row}`)
353
+ .join('\n')
354
+ }
355
+
356
+ export async function runDiagnose(
357
+ args: string[],
358
+ opts: DiagnoseOptions,
359
+ ): Promise<number> {
360
+ if (args.includes('--help') || args.includes('-h')) {
361
+ printHelp()
362
+ return 0
363
+ }
364
+
365
+ const parsed = parseBridgeCliArgs(args, {
366
+ port: opts.port,
367
+ stripBooleanFlags: ['--json', '--help', '-h'],
368
+ stripValueFlags: ['--since', '--include', '--limit', '--slow-threshold'],
369
+ })
370
+
371
+ const sub = parsed.positional[0] ?? 'recent'
372
+ if (sub !== 'recent') {
373
+ console.error(` unknown subcommand: ${sub}`)
374
+ console.error(' did you mean: rnxsim get diagnosis ?')
375
+ return 1
376
+ }
377
+
378
+ const sinceMs = parseSinceMs(valueOf(args, '--since'), 90_000)
379
+ const include = parseCsv(valueOf(args, '--include'))
380
+ const reactLimit = numberFlag(args, '--limit', 8)
381
+ const slowThresholdMs = numberFlag(args, '--slow-threshold', 1000)
382
+ const wantsJson = args.includes('--json')
383
+
384
+ const bridge = createBridgeFromParsed(parsed)
385
+ try {
386
+ // run every requested dimension in parallel — one bridge round-trip
387
+ // per primitive, no chained CLI invocations. on a healthy sim
388
+ // this finishes in a single tick.
389
+ const tasks: Array<Promise<unknown>> = []
390
+ const ordered: string[] = []
391
+ if (include.has('timeline') || include.has('screen') || include.has('alert')) {
392
+ ordered.push('timeline')
393
+ tasks.push(fetchTimeline(bridge, sinceMs))
394
+ }
395
+ if (include.has('network') || include.has('fetch')) {
396
+ ordered.push('network')
397
+ tasks.push(fetchNetwork(bridge, sinceMs, slowThresholdMs))
398
+ }
399
+ if (include.has('react')) {
400
+ ordered.push('react')
401
+ tasks.push(fetchReact(bridge, reactLimit))
402
+ }
403
+ if (include.has('console')) {
404
+ ordered.push('console')
405
+ tasks.push(fetchConsoleErrors(bridge, sinceMs))
406
+ }
407
+
408
+ // always probe the engine build id (one cheap eval). a reused sim
409
+ // session on a stale engine silently produced false QA verdicts
410
+ // (QA F20-5); surfacing the build here makes the staleness checkable.
411
+ const engineBuild = await bridge
412
+ .send({
413
+ type: 'evaluate',
414
+ code: `globalThis.__sootsimEngineBuild ?? null`,
415
+ })
416
+ .then((v) => (typeof v === 'string' ? v : null))
417
+ .catch(() => null)
418
+
419
+ const results = await Promise.all(tasks)
420
+ const slices: Record<string, unknown> = {}
421
+ for (let i = 0; i < ordered.length; i++) slices[ordered[i]] = results[i]
422
+
423
+ if (wantsJson) {
424
+ console.log(
425
+ JSON.stringify(
426
+ {
427
+ generatedAt: Date.now(),
428
+ engineBuild,
429
+ sinceMs,
430
+ include: Array.from(include),
431
+ slices,
432
+ },
433
+ null,
434
+ 2,
435
+ ),
436
+ )
437
+ return 0
438
+ }
439
+
440
+ // human render — one section per slice. terse, scannable, demo-paste-friendly.
441
+ const now = Date.now()
442
+ console.log(
443
+ ` diagnose recent (last ${(sinceMs / 1000).toFixed(0)}s, include: ${Array.from(include).sort().join(',')}):`,
444
+ )
445
+ console.log(
446
+ ` engine build: ${engineBuild ?? 'unknown (no build stamp — likely a stale or pre-F20-5 engine)'}`,
447
+ )
448
+
449
+ if (slices.timeline) {
450
+ const t = slices.timeline as TimelineSlice
451
+ console.log(`\n timeline — ${t.total} event(s) in window`)
452
+ const summary = Object.entries(t.byKind)
453
+ .sort((a, b) => b[1] - a[1])
454
+ .map(([k, n]) => `${n} ${k}`)
455
+ .join(' · ')
456
+ if (summary) console.log(` summary: ${summary}`)
457
+ if (t.screens.length) {
458
+ const last = t.screens[t.screens.length - 1]
459
+ const data = last.data as Record<string, unknown> | null
460
+ const name = data ? (data.name ?? data.activeName ?? data.path ?? '?') : '?'
461
+ console.log(` last screen: ${String(name)}`)
462
+ }
463
+ if (t.alerts.length) {
464
+ console.log(` alerts (${t.alerts.length}):`)
465
+ for (const a of t.alerts.slice(-5)) {
466
+ console.log(formatTimelineRow(a, now))
467
+ }
468
+ }
469
+ if (t.errors.length) {
470
+ console.log(` console errors/warns from timeline (${t.errors.length}):`)
471
+ for (const e of t.errors.slice(-5)) {
472
+ console.log(formatTimelineRow(e, now))
473
+ }
474
+ }
475
+ }
476
+
477
+ if (slices.network) {
478
+ const n = slices.network as NetworkSlice | { error: string }
479
+ if ('error' in n) {
480
+ console.log(`\n network — ${n.error}`)
481
+ } else {
482
+ console.log(
483
+ `\n network — ${n.total} request(s), ${n.failed.length} failed, ${n.slow.length} slow (>${n.slowThresholdMs}ms), ${n.inFlight} in-flight`,
484
+ )
485
+ if (n.failed.length) {
486
+ console.log(` failed:`)
487
+ for (const e of n.failed.slice(-8)) console.log(formatNetworkRow(e))
488
+ }
489
+ if (n.slow.length) {
490
+ console.log(` slowest:`)
491
+ for (const e of n.slow.slice(0, 5)) console.log(formatNetworkRow(e))
492
+ }
493
+ }
494
+ }
495
+
496
+ if (slices.react) {
497
+ const r = slices.react as ReactSlice
498
+ console.log(`\n react — ${r.commits} commit(s)`)
499
+ if (r.error) console.log(` warning: ${r.error}`)
500
+ if (r.durationWarning) console.log(` warning: ${r.durationWarning}`)
501
+ const allZero =
502
+ r.slow.length > 0 && r.slow.every((row) => row.totalMs <= 0 && row.slowestMs <= 0)
503
+ if (allZero && !r.durationWarning) {
504
+ console.log(
505
+ ` warning: every commit reports actualDuration=0ms — bundle has React profiler timer disabled`,
506
+ )
507
+ }
508
+ if (r.slow.length) {
509
+ console.log(' slowest:')
510
+ console.log(
511
+ formatTable(
512
+ ['component', 'total', 'commits', 'slowest'],
513
+ r.slow
514
+ .slice(0, reactLimit)
515
+ .map((row) => [
516
+ row.displayName,
517
+ `${row.totalMs.toFixed(2)}ms`,
518
+ String(row.commitCount),
519
+ `${row.slowestMs.toFixed(2)}ms`,
520
+ ]),
521
+ ' ',
522
+ ),
523
+ )
524
+ }
525
+ if (r.rerenders.length) {
526
+ console.log(' most rerenders:')
527
+ console.log(
528
+ formatTable(
529
+ ['component', 'renders', 'top causes'],
530
+ r.rerenders.slice(0, reactLimit).map((row) => {
531
+ const causes = Object.entries(row.causes ?? {})
532
+ .filter(([, v]) => v > 0)
533
+ .sort((a, b) => b[1] - a[1])
534
+ .slice(0, 3)
535
+ .map(([k, v]) => `${k}:${v}`)
536
+ .join(', ')
537
+ return [
538
+ row.displayName,
539
+ String(row.renders ?? row.count ?? 0),
540
+ causes || '-',
541
+ ]
542
+ }),
543
+ ' ',
544
+ ),
545
+ )
546
+ }
547
+ }
548
+
549
+ if (slices.console) {
550
+ const c = slices.console as
551
+ | { entries: LogEntry[]; total: number }
552
+ | { error: string }
553
+ if ('error' in c) {
554
+ console.log(`\n console — ${c.error}`)
555
+ } else {
556
+ console.log(`\n console errors/warns — ${c.total} in window`)
557
+ for (const log of c.entries.slice(-8)) {
558
+ const dt = ((logTs(log) - now) / 1000).toFixed(2)
559
+ const msg = logMessage(log).slice(0, 160)
560
+ console.log(` -${dt.padStart(5)}s [${log.level}] ${msg}`)
561
+ }
562
+ }
563
+ }
564
+
565
+ // close with the per-identity cursor advance hint, mirroring
566
+ // what-happened's footer.
567
+ const identityKey = getCliIdentityKey()
568
+ if (identityKey) {
569
+ console.log(
570
+ `\n next: rnxsim what-happened (cursor advances) · rnxsim network get <id> · rnxsim perf react why <fiber-id>`,
571
+ )
572
+ }
573
+ return 0
574
+ } catch (err) {
575
+ // a bridge call rejecting (most commonly "no sim connected") must not
576
+ // escape as a raw stack trace with a 0 exit code — `get diagnosis` is the
577
+ // "what just went wrong" command; failing it should read cleanly and
578
+ // exit non-zero so `sootsim get diagnosis && …` chains behave.
579
+ const msg = err instanceof Error ? err.message : String(err)
580
+ if (/no sim connected/i.test(msg)) {
581
+ console.error(' no sim connected — open one first: rnxsim open <metroPort>')
582
+ } else {
583
+ console.error(` diagnose failed: ${msg}`)
584
+ }
585
+ return 1
586
+ } finally {
587
+ bridge.close()
588
+ }
589
+ }
@@ -0,0 +1,95 @@
1
+ // sootsim desktop — launch the installed desktop companion via the
2
+ // electron driver. the driver wraps findDesktopCompanion +
3
+ // launchDesktopCompanion, so this command is a thin CLI surface that
4
+ // parses options, prompts for install, and delegates everything else.
5
+
6
+ import { ensureProfile } from '../../src/profiles'
7
+ import { findDesktopCompanion } from '../desktop-companion'
8
+ import { electronDriver } from '../drivers'
9
+ import { confirm } from '../prompt'
10
+ import { buildShellUrl } from './control'
11
+ import { printMissingCompanionHint } from './no-bridge-hint'
12
+
13
+ interface ElectronOptions {
14
+ port?: number
15
+ device?: string
16
+ }
17
+
18
+ function parseInlinePort(args: string[]): number | undefined {
19
+ const portIdx = args.indexOf('--port')
20
+ if (portIdx < 0) return undefined
21
+ const raw = args[portIdx + 1]
22
+ const port = raw ? Number(raw) : Number.NaN
23
+ if (!Number.isInteger(port) || port <= 0) {
24
+ console.error(` invalid --port value: ${raw || '(missing)'}`)
25
+ process.exit(1)
26
+ }
27
+ return port
28
+ }
29
+
30
+ export async function resolveElectronLaunchUrl(
31
+ args: string[],
32
+ opts: ElectronOptions,
33
+ ): Promise<string | undefined> {
34
+ const targetPort = parseInlinePort(args) ?? opts.port
35
+ return targetPort ? buildShellUrl(String(targetPort)) : undefined
36
+ }
37
+
38
+ export async function runElectron(args: string[], opts: ElectronOptions) {
39
+ if (args.includes('--help') || args.includes('-h')) {
40
+ console.log(`
41
+ rnxsim desktop — launch the desktop companion
42
+
43
+ usage:
44
+ rnxsim desktop [options]
45
+
46
+ options:
47
+ --port <number> connect to running dev server on this port
48
+ --profile <id> launch with an isolated persistent storage profile
49
+ --ephemeral launch with a temporary isolated storage profile
50
+
51
+ examples:
52
+ rnxsim desktop
53
+ rnxsim desktop --port 5173
54
+ rnxsim desktop --profile qa
55
+ `)
56
+ process.exit(0)
57
+ }
58
+
59
+ const profileArg = args.find((_, i) => args[i - 1] === '--profile')
60
+ const ephemeralProfile = args.includes('--ephemeral')
61
+ if (profileArg && ephemeralProfile) {
62
+ console.error(' rnxsim desktop: --profile cannot be combined with --ephemeral')
63
+ process.exit(1)
64
+ }
65
+ const profileId = profileArg ? ensureProfile(profileArg).id : undefined
66
+ let install = findDesktopCompanion()
67
+ if (!install) {
68
+ printMissingCompanionHint()
69
+ const canPrompt =
70
+ process.stdin.isTTY &&
71
+ process.env.CI !== '1' &&
72
+ process.env.SOOTSIM_NO_PROMPT !== '1'
73
+ if (canPrompt && (await confirm('run rnxsim desktop install now?', true))) {
74
+ console.log()
75
+ const { runInstallDesktop } = await import('./install-desktop')
76
+ await runInstallDesktop(['--yes'])
77
+ install = findDesktopCompanion()
78
+ }
79
+ if (!install) process.exit(1)
80
+ }
81
+ console.log(` launching ${install.path}`)
82
+
83
+ const url = await resolveElectronLaunchUrl(args, opts)
84
+ const result = await electronDriver.launch({
85
+ url,
86
+ device: opts.device,
87
+ profileId,
88
+ ephemeralProfile,
89
+ })
90
+ if (!result.launched) {
91
+ console.error(` ${result.message}`)
92
+ process.exit(1)
93
+ }
94
+ console.log(` ${result.message}`)
95
+ }