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,351 @@
1
+ // sootsim desktop install — download and install the optional desktop GUI
2
+ //
3
+ // the CLI + daemon are the canonical sootsim surface; this command is only
4
+ // for users who want the Electron-based GUI companion on top of that.
5
+ //
6
+ // fetches the right platform artifact from https://contrast.dev/api/electron-release/
7
+ // and installs it into the OS-native location that findDesktopCompanion() looks for:
8
+ // macOS: ~/Applications/sootsim.app (or /Applications if writable)
9
+ // linux: ~/Applications/sootsim.AppImage
10
+ // win: spawns the NSIS installer
11
+ //
12
+ // post-install the electron-updater feed (same URL) handles upgrades inside the
13
+ // running app, so this command is a first-install bootstrap, not an update path.
14
+
15
+ import { spawn, spawnSync } from 'child_process'
16
+ import {
17
+ chmodSync,
18
+ copyFileSync,
19
+ createWriteStream,
20
+ existsSync,
21
+ mkdirSync,
22
+ rmSync,
23
+ statSync,
24
+ unlinkSync,
25
+ } from 'fs'
26
+ import { tmpdir } from 'os'
27
+ import { dirname, join, resolve } from 'path'
28
+ import { findDesktopCompanion } from '../desktop-companion'
29
+ import { confirm } from '../prompt'
30
+
31
+ const DOWNLOAD_BASE = 'https://contrast.dev/api/electron-release'
32
+
33
+ type TargetPlatform = 'mac-arm64' | 'mac-x64' | 'win-x64' | 'linux-x64' | 'linux-arm64'
34
+
35
+ interface Target {
36
+ platform: TargetPlatform
37
+ filename: string
38
+ url: string
39
+ install: (downloaded: string) => Promise<string>
40
+ }
41
+
42
+ function detectTarget(): Target | null {
43
+ const arch = process.arch
44
+ if (process.platform === 'darwin') {
45
+ const platform: TargetPlatform = arch === 'arm64' ? 'mac-arm64' : 'mac-x64'
46
+ const filename = `sootsim-latest-${platform}.dmg`
47
+ return {
48
+ platform,
49
+ filename,
50
+ url: `${DOWNLOAD_BASE}/${filename}`,
51
+ install: installMacDmg,
52
+ }
53
+ }
54
+ if (process.platform === 'linux') {
55
+ // ship arch-appropriate AppImages. on Apple Silicon Macs (the default
56
+ // host for orb/multipass VMs) the linux VM is arm64, so the x64
57
+ // AppImage fails at runtime with "Dynamic loader not found:
58
+ // /lib64/ld-linux-x86-64.so.2". detect the running arch and pick the
59
+ // matching artifact.
60
+ const platform: TargetPlatform = arch === 'arm64' ? 'linux-arm64' : 'linux-x64'
61
+ const filename = `sootsim-latest-${platform}.AppImage`
62
+ return {
63
+ platform,
64
+ filename,
65
+ url: `${DOWNLOAD_BASE}/${filename}`,
66
+ install: installLinuxAppImage,
67
+ }
68
+ }
69
+ if (process.platform === 'win32') {
70
+ const filename = `sootsim-latest-win-x64.exe`
71
+ return {
72
+ platform: 'win-x64',
73
+ filename,
74
+ url: `${DOWNLOAD_BASE}/${filename}`,
75
+ install: installWindowsExe,
76
+ }
77
+ }
78
+ return null
79
+ }
80
+
81
+ interface DownloadOptions {
82
+ url: string
83
+ dest: string
84
+ onProgress?: (received: number, total: number | null) => void
85
+ }
86
+
87
+ async function download({ url, dest, onProgress }: DownloadOptions): Promise<void> {
88
+ const response = await fetch(url, { redirect: 'follow' })
89
+ if (!response.ok) {
90
+ throw new Error(`${response.status} ${response.statusText} (${url})`)
91
+ }
92
+ const contentLength = response.headers.get('content-length')
93
+ const total = contentLength ? Number(contentLength) : null
94
+ if (!response.body) {
95
+ throw new Error(`empty response body (${url})`)
96
+ }
97
+
98
+ mkdirSync(dirname(dest), { recursive: true })
99
+ const file = createWriteStream(dest)
100
+ const reader = response.body.getReader()
101
+ let received = 0
102
+
103
+ try {
104
+ while (true) {
105
+ const { done, value } = await reader.read()
106
+ if (done) break
107
+ file.write(Buffer.from(value))
108
+ received += value.byteLength
109
+ onProgress?.(received, total)
110
+ }
111
+ } finally {
112
+ await new Promise<void>((resolve, reject) => {
113
+ file.end((err?: Error | null) => (err ? reject(err) : resolve()))
114
+ })
115
+ }
116
+ }
117
+
118
+ function renderProgress(received: number, total: number | null): string {
119
+ const mb = (received / (1024 * 1024)).toFixed(1)
120
+ if (!total) return ` ${mb} MB`
121
+ const pct = Math.min(100, Math.round((received / total) * 100))
122
+ const totalMb = (total / (1024 * 1024)).toFixed(1)
123
+ const barWidth = 24
124
+ const filled = Math.round((pct / 100) * barWidth)
125
+ const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled)
126
+ return ` ${bar} ${pct}% ${mb} / ${totalMb} MB`
127
+ }
128
+
129
+ async function installMacDmg(dmgPath: string): Promise<string> {
130
+ const mountOutput = spawnSync(
131
+ 'hdiutil',
132
+ ['attach', '-nobrowse', '-readonly', dmgPath],
133
+ {
134
+ encoding: 'utf8',
135
+ },
136
+ )
137
+ if (mountOutput.status !== 0) {
138
+ throw new Error(
139
+ `hdiutil attach failed: ${mountOutput.stderr || mountOutput.stdout}`.trim(),
140
+ )
141
+ }
142
+ const mountLines = mountOutput.stdout.trim().split('\n')
143
+ const mountLine = mountLines[mountLines.length - 1]
144
+ const mountPoint = mountLine.split('\t').pop()?.trim()
145
+ if (!mountPoint || !existsSync(mountPoint)) {
146
+ throw new Error(`could not determine dmg mount point (output: ${mountOutput.stdout})`)
147
+ }
148
+
149
+ const appSource = join(mountPoint, 'sootsim.app')
150
+ if (!existsSync(appSource)) {
151
+ spawnSync('hdiutil', ['detach', '-force', mountPoint])
152
+ throw new Error(`sootsim.app not found inside ${mountPoint}`)
153
+ }
154
+
155
+ // prefer /Applications if writable, otherwise ~/Applications (same paths
156
+ // findDesktopCompanion checks, so no extra config needed post-install).
157
+ const systemApps = '/Applications'
158
+ const userApps = resolve(process.env.HOME || '', 'Applications')
159
+ let destRoot = systemApps
160
+ try {
161
+ const probe = join(systemApps, `.sootsim-write-probe-${process.pid}`)
162
+ spawnSync('touch', [probe])
163
+ if (existsSync(probe)) rmSync(probe, { force: true })
164
+ else destRoot = userApps
165
+ } catch {
166
+ destRoot = userApps
167
+ }
168
+ if (destRoot === userApps) {
169
+ mkdirSync(destRoot, { recursive: true })
170
+ }
171
+
172
+ const appDest = join(destRoot, 'sootsim.app')
173
+ if (existsSync(appDest)) rmSync(appDest, { recursive: true, force: true })
174
+
175
+ const copy = spawnSync('cp', ['-R', appSource, appDest])
176
+ const detach = spawnSync('hdiutil', ['detach', '-force', mountPoint])
177
+ if (copy.status !== 0) {
178
+ throw new Error(`copy to ${appDest} failed: ${copy.stderr?.toString() || ''}`.trim())
179
+ }
180
+ if (detach.status !== 0) {
181
+ console.warn(
182
+ ` warning: failed to unmount ${mountPoint} (${detach.stderr?.toString().trim() || 'unknown error'})`,
183
+ )
184
+ }
185
+
186
+ // clear the quarantine attribute so gatekeeper doesn't nag on first launch;
187
+ // the app is already notarized so this is safe.
188
+ spawnSync('xattr', ['-dr', 'com.apple.quarantine', appDest])
189
+
190
+ return appDest
191
+ }
192
+
193
+ async function installLinuxAppImage(appImagePath: string): Promise<string> {
194
+ const destDir = resolve(process.env.HOME || '', 'Applications')
195
+ mkdirSync(destDir, { recursive: true })
196
+ const dest = join(destDir, 'sootsim.AppImage')
197
+ if (existsSync(dest)) rmSync(dest, { force: true })
198
+ // copyFile + unlink instead of rename: /tmp and ~/Applications are
199
+ // routinely on separate mounts in containers / VMs, where renameSync
200
+ // fails EXDEV. copy is portable across mounts and the unlink at the
201
+ // end keeps the staging dir clean.
202
+ copyFileSync(appImagePath, dest)
203
+ unlinkSync(appImagePath)
204
+ chmodSync(dest, 0o755)
205
+ return dest
206
+ }
207
+
208
+ async function installWindowsExe(exePath: string): Promise<string> {
209
+ // nsis installer prompts and self-elevates; run it and let it take over.
210
+ await new Promise<void>((resolvePromise, rejectPromise) => {
211
+ const child = spawn('cmd', ['/c', 'start', '""', '/wait', exePath], {
212
+ stdio: 'inherit',
213
+ })
214
+ child.once('error', rejectPromise)
215
+ child.once('exit', (code) => {
216
+ if (code === 0) resolvePromise()
217
+ else rejectPromise(new Error(`installer exited with code ${code}`))
218
+ })
219
+ })
220
+ return exePath
221
+ }
222
+
223
+ export async function runInstallDesktop(args: string[]) {
224
+ if (args.includes('--help') || args.includes('-h')) {
225
+ console.log(`
226
+ rnxsim desktop install — download and install the optional desktop GUI
227
+
228
+ the CLI + daemon are the canonical rnx surface; the GUI is optional.
229
+ if you just want to drive a running metro/expo dev server from the
230
+ terminal, you don't need this.
231
+
232
+ usage:
233
+ rnxsim desktop install [options]
234
+
235
+ options:
236
+ -y, --yes skip confirmation and install immediately
237
+ --force reinstall even if the companion is already present
238
+
239
+ examples:
240
+ rnxsim desktop install
241
+ rnxsim desktop install --yes
242
+ `)
243
+ process.exit(0)
244
+ }
245
+
246
+ const skipConfirm =
247
+ args.includes('--yes') ||
248
+ args.includes('-y') ||
249
+ process.env.SOOTSIM_NO_PROMPT === '1' ||
250
+ process.env.CI === '1' ||
251
+ !process.stdin.isTTY
252
+ const force = args.includes('--force')
253
+
254
+ const existing = findDesktopCompanion()
255
+ if (existing && !force) {
256
+ console.log(` rnxsim desktop already installed at: ${existing.path}`)
257
+ console.log(' pass --force to reinstall.')
258
+ return
259
+ }
260
+
261
+ const target = detectTarget()
262
+ if (!target) {
263
+ console.error(` no desktop build available for ${process.platform}/${process.arch}.`)
264
+ console.error(
265
+ ' supported: darwin-arm64, darwin-x64, linux-x64, linux-arm64, win32-x64',
266
+ )
267
+ process.exit(1)
268
+ }
269
+
270
+ console.log(` platform: ${target.platform}`)
271
+ console.log(` download: ${target.url}`)
272
+ console.log()
273
+
274
+ if (!skipConfirm) {
275
+ const ok = await confirm('download and install now?', true)
276
+ if (!ok) {
277
+ console.log(' cancelled.')
278
+ return
279
+ }
280
+ console.log()
281
+ }
282
+
283
+ const workDir = join(tmpdir(), `sootsim-install-${Date.now()}`)
284
+ const downloadPath = join(workDir, target.filename)
285
+
286
+ let lastDraw = 0
287
+ process.stdout.write(` downloading ${target.filename}...\n`)
288
+
289
+ try {
290
+ await download({
291
+ url: target.url,
292
+ dest: downloadPath,
293
+ onProgress: (received, total) => {
294
+ const now = Date.now()
295
+ if (now - lastDraw < 100 && received < (total ?? Infinity)) return
296
+ lastDraw = now
297
+ if (process.stdout.isTTY) {
298
+ process.stdout.write(`\r\x1b[2K${renderProgress(received, total)}`)
299
+ }
300
+ },
301
+ })
302
+ } catch (err) {
303
+ rmSync(workDir, { recursive: true, force: true })
304
+ const message = err instanceof Error ? err.message : String(err)
305
+ console.error(`\n download failed: ${message}`)
306
+ console.error(
307
+ ` the ${target.platform} desktop build may not be published yet, or your`,
308
+ )
309
+ console.error(
310
+ ' network blocked the request. check your connection and try again later;',
311
+ )
312
+ console.error(' the CLI + daemon work without the desktop app (run: rnxsim --help).')
313
+ process.exit(1)
314
+ }
315
+
316
+ if (process.stdout.isTTY) process.stdout.write('\n')
317
+ const size = statSync(downloadPath).size
318
+ console.log(` downloaded ${(size / (1024 * 1024)).toFixed(1)} MB`)
319
+ console.log()
320
+ console.log(` installing...`)
321
+
322
+ const { trackCliEvent, flushCliTelemetry } = await import('../telemetry')
323
+
324
+ try {
325
+ const installedAt = await target.install(downloadPath)
326
+ trackCliEvent({
327
+ event: 'cli_install_desktop_succeeded',
328
+ properties: { platform: target.platform, size_bytes: size },
329
+ })
330
+ console.log(` installed: ${installedAt}`)
331
+ } catch (err) {
332
+ trackCliEvent({
333
+ event: 'cli_install_desktop_failed',
334
+ properties: {
335
+ platform: target.platform,
336
+ error: err instanceof Error ? err.message : String(err),
337
+ },
338
+ })
339
+ await flushCliTelemetry()
340
+ console.error(` install failed: ${err instanceof Error ? err.message : String(err)}`)
341
+ console.error(` keeping download at ${downloadPath} for manual install.`)
342
+ process.exit(1)
343
+ } finally {
344
+ rmSync(workDir, { recursive: true, force: true })
345
+ }
346
+
347
+ console.log()
348
+ console.log(' next steps:')
349
+ console.log(' rnxsim desktop launch the desktop app')
350
+ console.log(' rnxsim open <target> open a demo or bundle in it')
351
+ }
@@ -0,0 +1,331 @@
1
+ import { exec } from 'node:child_process'
2
+ import { randomBytes } from 'node:crypto'
3
+ import http from 'node:http'
4
+ import {
5
+ refreshSharedDesktopAuthSession,
6
+ writeSharedDesktopAuthSession,
7
+ } from '../../src/auth/shared-session'
8
+ import { rnxPublicBrand } from '../../src/public-brand'
9
+
10
+ const REMOTE_DEFAULT_ORIGIN = process.env.SOOTSIM_AUTH_ORIGIN || 'https://contrast.dev'
11
+ const LOCAL_DEFAULT_ORIGIN = 'https://contrast.localhost:3000'
12
+
13
+ type LoginResult =
14
+ | { ok: true; token: string; userId?: string; email?: string }
15
+ | { ok: false; error: string }
16
+
17
+ async function resolveDefaultAuthOrigin(explicitOrigin?: string) {
18
+ if (explicitOrigin) return explicitOrigin
19
+ if (process.env.SOOTSIM_AUTH_ORIGIN) return process.env.SOOTSIM_AUTH_ORIGIN
20
+ if (process.env.SOOTSIM_UPLOAD_ORIGIN) return process.env.SOOTSIM_UPLOAD_ORIGIN
21
+
22
+ try {
23
+ const res = await fetch(`${LOCAL_DEFAULT_ORIGIN}/api/auth/me`)
24
+ if (res.ok) return LOCAL_DEFAULT_ORIGIN
25
+ } catch {}
26
+
27
+ return REMOTE_DEFAULT_ORIGIN
28
+ }
29
+
30
+ function printHelp() {
31
+ console.log(`
32
+ rnxsim login — sign in so uploads can attach to your account
33
+
34
+ usage:
35
+ rnxsim login [--origin <url>]
36
+
37
+ options:
38
+ --origin <url> auth host (default: auto)
39
+ prefers ${LOCAL_DEFAULT_ORIGIN} when available, otherwise
40
+ falls back to ${REMOTE_DEFAULT_ORIGIN}
41
+ override with SOOTSIM_AUTH_ORIGIN env var
42
+ -h, --help
43
+ `)
44
+ }
45
+
46
+ function extractStringFlag(args: string[], name: string): string | undefined {
47
+ const idx = args.findIndex((a) => a === name)
48
+ if (idx < 0) return undefined
49
+ const value = args[idx + 1]
50
+ args.splice(idx, 2)
51
+ return value
52
+ }
53
+
54
+ function openBrowser(url: string) {
55
+ const command =
56
+ process.platform === 'darwin'
57
+ ? `open "${url}"`
58
+ : process.platform === 'win32'
59
+ ? `start "" "${url}"`
60
+ : `xdg-open "${url}"`
61
+
62
+ exec(command, (err) => {
63
+ if (!err) return
64
+ console.log(' could not open browser automatically.')
65
+ console.log(` open this URL manually:\n ${url}\n`)
66
+ })
67
+ }
68
+
69
+ function renderCallbackPage(body: string) {
70
+ return `<!doctype html>
71
+ <html>
72
+ <body style="font-family:system-ui;background:#000;color:#fff;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
73
+ <div style="max-width:480px;text-align:center">
74
+ <h1 style="font-size:28px;margin:0 0 12px">${rnxPublicBrand.name}</h1>
75
+ <p style="color:#ccc">${body}</p>
76
+ </div>
77
+ </body>
78
+ </html>`
79
+ }
80
+
81
+ async function runDevLogin(origin: string): Promise<LoginResult> {
82
+ try {
83
+ const res = await fetch(`${origin.replace(/\/$/, '')}/api/dev-login`, {
84
+ method: 'POST',
85
+ })
86
+ if (res.status === 403) {
87
+ return { ok: false, error: 'local dev-login unavailable' }
88
+ }
89
+ if (!res.ok) {
90
+ const text = await res.text().catch(() => '')
91
+ return { ok: false, error: `dev-login ${res.status}: ${text}` }
92
+ }
93
+ const body = (await res.json()) as {
94
+ ok?: boolean
95
+ token?: string
96
+ email?: string
97
+ user?: { id?: string }
98
+ }
99
+ if (!body.token) {
100
+ return { ok: false, error: 'dev-login response missing token' }
101
+ }
102
+ return {
103
+ ok: true,
104
+ token: body.token,
105
+ userId: body.user?.id,
106
+ email: body.email,
107
+ }
108
+ } catch (err) {
109
+ return {
110
+ ok: false,
111
+ error: err instanceof Error ? err.message : 'dev-login fetch failed',
112
+ }
113
+ }
114
+ }
115
+
116
+ function isLocalOrigin(origin: string): boolean {
117
+ try {
118
+ const hostname = new URL(origin).hostname.replace(/^\[|\]$/g, '').toLowerCase()
119
+ if (hostname === 'localhost') return true
120
+ if (/^127(?:\.\d{1,3}){3}$/.test(hostname)) return true
121
+ if (hostname === '::1' || hostname === '0.0.0.0') return true
122
+ if (hostname.endsWith('.local')) return true
123
+ if (hostname.endsWith('.localhost')) return true
124
+ // VM↔host bridge aliases — orb, docker, lima, multipass all forward the
125
+ // host on a well-known name (IPv4 or IPv6) so a VM-internal `sootsim login`
126
+ // can still take the dev-login fast path against the host's Contrast dev
127
+ // server. without this the CLI falls through to a browser callback that
128
+ // is unreachable from a headless VM.
129
+ if (hostname === 'host.containers.internal') return true
130
+ if (hostname === 'host.docker.internal') return true
131
+ if (hostname === 'host.orb.internal') return true
132
+ if (hostname === 'host.lima.internal') return true
133
+ // orb's IPv6 form of host.orb.internal: fd07:b51a:cc66:f0::fe (always
134
+ // this fixed address; it's a stable orb ULA assignment, see orb docs).
135
+ if (hostname === 'fd07:b51a:cc66:f0::fe') return true
136
+ // lima's qemu (slirp) backend does NOT resolve host.lima.internal — the
137
+ // host is the fixed slirp gateway 192.168.5.2. a lima/qemu user therefore
138
+ // targets the raw gateway IP, which would otherwise miss the dev-login fast
139
+ // path and fall through to the headless-broken browser callback. recognize
140
+ // it (and multipass's default bridge .1 gateways) so dev-login still fires.
141
+ // safe: no-device dev-login requests are accepted only in local dev.
142
+ if (hostname === '192.168.5.2') return true
143
+ if (/^192\.168\.(?:64|139)\.1$/.test(hostname)) return true
144
+ return false
145
+ } catch {
146
+ return false
147
+ }
148
+ }
149
+
150
+ export async function runLogin(args: string[]) {
151
+ if (args.includes('--help') || args.includes('-h')) {
152
+ printHelp()
153
+ process.exit(0)
154
+ }
155
+
156
+ const mutableArgs = [...args]
157
+ const origin = await resolveDefaultAuthOrigin(
158
+ extractStringFlag(mutableArgs, '--origin'),
159
+ )
160
+
161
+ // in dev (localhost), skip the browser and sign in as the shared dev
162
+ // user via POST /api/dev-login. the no-device contract is local-only, so this
163
+ // path is safe to take unconditionally against local hosts.
164
+ if (isLocalOrigin(origin)) {
165
+ console.log(` detected local origin (${origin}) — signing in as dev user`)
166
+ const devResult = await runDevLogin(origin)
167
+ if (devResult.ok) {
168
+ writeSharedDesktopAuthSession({
169
+ token: devResult.token,
170
+ user: devResult.userId ? { id: devResult.userId, email: devResult.email } : null,
171
+ origin,
172
+ source: 'cli',
173
+ })
174
+ const refreshed = await refreshSharedDesktopAuthSession(origin)
175
+ const label =
176
+ refreshed?.user?.email ||
177
+ devResult.email ||
178
+ refreshed?.user?.id ||
179
+ devResult.userId
180
+ const { trackCliEvent, flushCliTelemetry } = await import('../telemetry')
181
+ trackCliEvent({
182
+ event: 'cli_login_succeeded',
183
+ identity: { userId: devResult.userId ?? refreshed?.user?.id ?? null },
184
+ properties: { origin, mode: 'dev' },
185
+ })
186
+ await flushCliTelemetry()
187
+ console.log(` signed in${label ? ` as ${label}` : ''} (dev)`)
188
+ return
189
+ }
190
+ console.log(` dev-login not available (${devResult.error}); falling back to browser`)
191
+ }
192
+
193
+ const state = randomBytes(16).toString('hex')
194
+
195
+ // flush a start event BEFORE the browser wait: queued telemetry only drains
196
+ // on process exit, so a login that hangs (or gets ctrl+c'd) would otherwise
197
+ // leave zero trace. first-time-user hangs were invisible in posthog.
198
+ {
199
+ const { trackCliEvent, flushCliTelemetry } = await import('../telemetry')
200
+ trackCliEvent({ event: 'cli_login_started', properties: { origin } })
201
+ await flushCliTelemetry()
202
+ }
203
+
204
+ // the browser round-trip needs human action (github's authorize screen on
205
+ // first sign-in), so the wait is generous, but it must not be infinite: a
206
+ // closed tab, denied consent, or blocked popup used to hang the CLI forever.
207
+ const LOGIN_TIMEOUT_MS = 5 * 60 * 1000
208
+
209
+ const result = await new Promise<LoginResult>((resolve) => {
210
+ let timer: ReturnType<typeof setTimeout>
211
+ let settled = false
212
+ const finish = (r: LoginResult) => {
213
+ if (settled) return
214
+ settled = true
215
+ clearTimeout(timer)
216
+ resolve(r)
217
+ server.close()
218
+ }
219
+
220
+ const server = http.createServer((req, res) => {
221
+ try {
222
+ const url = new URL(req.url || '/', 'http://127.0.0.1')
223
+ const returnedState = url.searchParams.get('state')
224
+ const token = url.searchParams.get('token')
225
+ const email = url.searchParams.get('email') || undefined
226
+ const userId = url.searchParams.get('userId') || undefined
227
+ const error = url.searchParams.get('error')
228
+
229
+ // stray localhost traffic (port scanners, favicon fetches, browser
230
+ // prefetch) must not abort the login. only a request carrying our
231
+ // state is the real callback; rfc 8252 loopback servers ignore the
232
+ // rest. aborting here used to kill the login mid-wait.
233
+ if (returnedState !== state) {
234
+ res.statusCode = 404
235
+ res.end('not found')
236
+ return
237
+ }
238
+
239
+ res.setHeader('content-type', 'text/html; charset=utf-8')
240
+
241
+ if (error) {
242
+ res.end(renderCallbackPage(`sign-in failed: ${error}`))
243
+ finish({ ok: false, error })
244
+ return
245
+ }
246
+
247
+ if (!token) {
248
+ res.end(
249
+ renderCallbackPage('missing token. close this tab and retry `rnxsim login`.'),
250
+ )
251
+ finish({ ok: false, error: 'missing token' })
252
+ return
253
+ }
254
+
255
+ res.end(renderCallbackPage('sign-in complete. you can return to the terminal.'))
256
+ finish({ ok: true, token, email, userId })
257
+ } catch (err) {
258
+ res.statusCode = 500
259
+ res.end(renderCallbackPage('sign-in callback failed. retry `rnxsim login`.'))
260
+ finish({
261
+ ok: false,
262
+ error: err instanceof Error ? err.message : 'callback failed',
263
+ })
264
+ }
265
+ })
266
+
267
+ timer = setTimeout(() => {
268
+ finish({
269
+ ok: false,
270
+ error:
271
+ 'timed out waiting for the browser sign-in (5 minutes). rerun `rnxsim login` and complete the github authorize screen if it appears.',
272
+ })
273
+ }, LOGIN_TIMEOUT_MS)
274
+
275
+ server.listen(0, '127.0.0.1', () => {
276
+ const address = server.address()
277
+ if (!address || typeof address === 'string') {
278
+ finish({ ok: false, error: 'failed to bind callback server' })
279
+ return
280
+ }
281
+
282
+ const url = new URL(`${origin.replace(/\/$/, '')}/auth/sootsim-cli`)
283
+ url.searchParams.set('state', state)
284
+ url.searchParams.set('port', String(address.port))
285
+ url.searchParams.set('contrast', origin)
286
+
287
+ console.log(' opening browser for rnxsim login...')
288
+ console.log(
289
+ ' first time here? github will show an authorize screen. approve it to continue.',
290
+ )
291
+ console.log(` waiting up to 5 minutes for the browser (ctrl+c to cancel)`)
292
+ console.log(` if nothing opens, visit:\n ${url.toString()}\n`)
293
+ openBrowser(url.toString())
294
+ })
295
+ })
296
+
297
+ if (!result.ok) {
298
+ const { trackCliEvent, flushCliTelemetry } = await import('../telemetry')
299
+ trackCliEvent({
300
+ event: 'cli_login_failed',
301
+ properties: { origin, error: result.error },
302
+ })
303
+ await flushCliTelemetry()
304
+ console.error(` login failed: ${result.error}`)
305
+ process.exit(1)
306
+ }
307
+
308
+ writeSharedDesktopAuthSession({
309
+ token: result.token,
310
+ user: result.userId
311
+ ? {
312
+ id: result.userId,
313
+ email: result.email,
314
+ }
315
+ : null,
316
+ origin,
317
+ source: 'cli',
318
+ })
319
+
320
+ const refreshed = await refreshSharedDesktopAuthSession(origin)
321
+ const label =
322
+ refreshed?.user?.email || result.email || refreshed?.user?.id || result.userId
323
+ const { trackCliEvent, flushCliTelemetry } = await import('../telemetry')
324
+ trackCliEvent({
325
+ event: 'cli_login_succeeded',
326
+ identity: { userId: result.userId ?? refreshed?.user?.id ?? null },
327
+ properties: { origin },
328
+ })
329
+ await flushCliTelemetry()
330
+ console.log(` signed in${label ? ` as ${label}` : ''}`)
331
+ }