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,226 @@
1
+ // shared test-run registration for both runners (Maestro / Detox). posts a
2
+ // `sootRun` row to /api/sootsim/test-runs so the org
3
+ // dashboard and PR comments get a pass/fail list with git provenance and a
4
+ // link into the hosted replay (the uploaded preview share).
5
+ //
6
+ // registration policy: a run registers when it produced a hosted share
7
+ // (--preview) or when it runs in CI (github-token / actions env) — local
8
+ // iteration without --preview stays local. registration failures warn and
9
+ // never change the run's exit code; the test result is the playback result.
10
+
11
+ import { execSync } from 'child_process'
12
+ import { authHeaderValue, resolveCliAuth, type CliAuth } from './auth'
13
+ import type { SootSimFlowTraceStep } from './bridge-flow-runner'
14
+
15
+ export type RunKind = 'maestro' | 'detox'
16
+
17
+ export type RunProvenance = {
18
+ owner: string | null
19
+ repo: string | null
20
+ branch: string | null
21
+ commitSha: string | null
22
+ githubUsername: string | null
23
+ pullRequestNumber: number | null
24
+ pullRequestTitle: string | null
25
+ githubRunId: string | null
26
+ source: 'cli' | 'ci'
27
+ }
28
+
29
+ function gitOutput(command: string): string | null {
30
+ try {
31
+ const out = execSync(command, {
32
+ stdio: ['ignore', 'pipe', 'ignore'],
33
+ timeout: 3000,
34
+ encoding: 'utf8',
35
+ }).trim()
36
+ return out || null
37
+ } catch {
38
+ return null
39
+ }
40
+ }
41
+
42
+ function parsePositiveInt(value: string | undefined): number | null {
43
+ const trimmed = value?.trim()
44
+ if (!trimmed || !/^\d+$/.test(trimmed)) return null
45
+ return Number(trimmed)
46
+ }
47
+
48
+ // owner/repo from the checkout's github remote, for local runs with no
49
+ // CONTRAST_REPO/GITHUB_REPOSITORY env. runs are repo-scoped on the dashboard
50
+ // (/org/<owner>/<repo>/runs), so a repo-less registration has no home — the
51
+ // server 400s it. handles ssh (git@github.com:o/r.git) and https
52
+ // (https://github.com/o/r(.git)) remote shapes.
53
+ function ownerRepoFromGitRemote(): { owner: string; repo: string } | null {
54
+ const url = gitOutput('git remote get-url origin')
55
+ if (!url) return null
56
+ const match = url.match(/github\.com[/:]([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/i)
57
+ if (!match) return null
58
+ return { owner: match[1], repo: match[2] }
59
+ }
60
+
61
+ // git provenance for the current run. in GitHub Actions the CONTRAST_*/GITHUB_*
62
+ // env is authoritative (the checkout can be a detached merge ref); locally
63
+ // we ask git itself.
64
+ export function resolveRunProvenance(): RunProvenance {
65
+ const isActions = process.env.GITHUB_ACTIONS === 'true'
66
+ const contrastBranch = process.env.CONTRAST_BRANCH?.trim() || null
67
+ const contrastSha = process.env.CONTRAST_SHA?.trim() || null
68
+ const pullRequestTitle = process.env.CONTRAST_PR_TITLE?.trim() || null
69
+ const repoSlug = (
70
+ process.env.CONTRAST_REPO ||
71
+ process.env.GITHUB_REPOSITORY ||
72
+ ''
73
+ ).trim()
74
+ const [envOwner, envRepo] = repoSlug.includes('/')
75
+ ? repoSlug.split('/', 2)
76
+ : [null, null]
77
+
78
+ if (isActions) {
79
+ // pull_request events: GITHUB_HEAD_REF is the PR head branch and
80
+ // GITHUB_REF looks like refs/pull/<n>/merge. push events: GITHUB_REF_NAME.
81
+ const prFromRef = process.env.GITHUB_REF?.match(/^refs\/pull\/(\d+)\//)?.[1]
82
+ return {
83
+ owner: envOwner,
84
+ repo: envRepo,
85
+ branch:
86
+ contrastBranch ||
87
+ process.env.GITHUB_HEAD_REF?.trim() ||
88
+ process.env.GITHUB_REF_NAME?.trim() ||
89
+ null,
90
+ commitSha: contrastSha || process.env.GITHUB_SHA?.trim() || null,
91
+ githubUsername: process.env.GITHUB_ACTOR?.trim() || null,
92
+ pullRequestNumber:
93
+ parsePositiveInt(process.env.CONTRAST_PR_NUMBER) ?? parsePositiveInt(prFromRef),
94
+ pullRequestTitle,
95
+ githubRunId: process.env.GITHUB_RUN_ID?.trim() || null,
96
+ source: 'ci',
97
+ }
98
+ }
99
+
100
+ const branch = contrastBranch ?? gitOutput('git rev-parse --abbrev-ref HEAD')
101
+ // env slug wins when present; otherwise infer from the github remote so a
102
+ // a plain local `sootsim maestro test --preview` still lands on its repo's dashboard.
103
+ const remote = envOwner ? null : ownerRepoFromGitRemote()
104
+ return {
105
+ owner: envOwner ?? remote?.owner ?? null,
106
+ repo: envRepo ?? remote?.repo ?? null,
107
+ branch: branch === 'HEAD' ? null : branch,
108
+ commitSha: contrastSha ?? gitOutput('git rev-parse HEAD'),
109
+ githubUsername: null,
110
+ pullRequestNumber: parsePositiveInt(process.env.CONTRAST_PR_NUMBER),
111
+ pullRequestTitle,
112
+ githubRunId: null,
113
+ source: 'cli',
114
+ }
115
+ }
116
+
117
+ export function stepSummaryFromTrace(steps: SootSimFlowTraceStep[]): {
118
+ stepCount: number | null
119
+ failedStepIndex: number | null
120
+ } {
121
+ if (steps.length === 0) return { stepCount: null, failedStepIndex: null }
122
+ const failed = steps.find((step) => step.status === 'failure')
123
+ return {
124
+ stepCount: steps.length,
125
+ failedStepIndex: failed ? failed.stepIndex : null,
126
+ }
127
+ }
128
+
129
+ // whether this run should register at all — see the policy note at the top.
130
+ export function shouldRegisterRun(args: {
131
+ uploadedShare: boolean
132
+ auth: CliAuth | null
133
+ }): boolean {
134
+ if (!args.auth) return false
135
+ if (args.uploadedShare) return true
136
+ return args.auth.kind === 'github' || process.env.GITHUB_ACTIONS === 'true'
137
+ }
138
+
139
+ export type RegisterRunArgs = {
140
+ origin: string
141
+ kind: RunKind
142
+ status: 'passed' | 'failed'
143
+ name?: string | null
144
+ prompt?: string | null
145
+ summary?: string | null
146
+ failureMessage?: string | null
147
+ previewShareId?: string | null
148
+ // explicit --owner/--repo flags override env/git detection
149
+ owner?: string | null
150
+ repo?: string | null
151
+ durationMs?: number | null
152
+ flowYamlSizeBytes?: number | null
153
+ stepCount?: number | null
154
+ failedStepIndex?: number | null
155
+ auth?: CliAuth | null
156
+ }
157
+
158
+ export type RegisteredRun = {
159
+ id: string
160
+ previewUrl: string | null
161
+ traceUrl: string | null
162
+ }
163
+
164
+ export async function registerRun(args: RegisterRunArgs): Promise<RegisteredRun | null> {
165
+ const auth = args.auth ?? resolveCliAuth()
166
+ if (!auth) return null
167
+ const provenance = resolveRunProvenance()
168
+ const owner =
169
+ args.owner ?? (auth.kind === 'github' ? auth.owner : null) ?? provenance.owner
170
+ const repo = args.repo ?? (auth.kind === 'github' ? auth.repo : null) ?? provenance.repo
171
+
172
+ try {
173
+ const res = await fetch(`${args.origin.replace(/\/$/, '')}/api/sootsim/test-runs`, {
174
+ method: 'POST',
175
+ headers: {
176
+ 'content-type': 'application/json',
177
+ authorization: authHeaderValue(auth),
178
+ },
179
+ body: JSON.stringify({
180
+ kind: args.kind,
181
+ source: provenance.source,
182
+ name: args.name ?? undefined,
183
+ prompt: args.prompt ?? undefined,
184
+ summary: args.summary ?? undefined,
185
+ status: args.status,
186
+ failureMessage: args.failureMessage ?? undefined,
187
+ previewShareId: args.previewShareId ?? undefined,
188
+ owner: owner ?? undefined,
189
+ repo: repo ?? undefined,
190
+ branch: provenance.branch ?? undefined,
191
+ commitSha: provenance.commitSha ?? undefined,
192
+ githubUsername: provenance.githubUsername ?? undefined,
193
+ pullRequestNumber: provenance.pullRequestNumber ?? undefined,
194
+ githubRunId: provenance.githubRunId ?? undefined,
195
+ stepCount: args.stepCount ?? undefined,
196
+ failedStepIndex: args.failedStepIndex ?? undefined,
197
+ durationMs: args.durationMs ?? undefined,
198
+ flowYamlSizeBytes: args.flowYamlSizeBytes ?? undefined,
199
+ }),
200
+ })
201
+ if (!res.ok) {
202
+ const text = await res.text().catch(() => '')
203
+ console.warn(
204
+ ` warn: run registration failed: /api/sootsim/test-runs ${res.status}${text ? `: ${text.slice(0, 200)}` : ''}`,
205
+ )
206
+ return null
207
+ }
208
+ const body = (await res.json()) as {
209
+ run?: { id?: string; traceUrl?: string | null; previewUrl?: string | null }
210
+ }
211
+ if (!body.run?.id) {
212
+ console.warn(' warn: run registration returned no id')
213
+ return null
214
+ }
215
+ return {
216
+ id: body.run.id,
217
+ previewUrl: body.run.previewUrl ?? null,
218
+ traceUrl: body.run.traceUrl ?? null,
219
+ }
220
+ } catch (err) {
221
+ console.warn(
222
+ ` warn: run registration failed: ${err instanceof Error ? err.message : String(err)}`,
223
+ )
224
+ return null
225
+ }
226
+ }
@@ -0,0 +1,66 @@
1
+ import { SOOTSIM_CHANGELOG_API_URL, SOOTSIM_CHANGELOG_URL } from '../src/runtime-delivery'
2
+ import type { ChangelogEntry } from '../../../scripts/changelog/types'
3
+
4
+ function isChangelogEntry(value: unknown): value is ChangelogEntry {
5
+ if (!value || typeof value !== 'object') return false
6
+ const sections = Reflect.get(value, 'sections')
7
+ return (
8
+ typeof Reflect.get(value, 'title') === 'string' &&
9
+ Array.isArray(sections) &&
10
+ sections.every(
11
+ (section) =>
12
+ section &&
13
+ typeof section === 'object' &&
14
+ typeof Reflect.get(section, 'title') === 'string' &&
15
+ Array.isArray(Reflect.get(section, 'items')) &&
16
+ Reflect.get(section, 'items').every(
17
+ (item: unknown) =>
18
+ item &&
19
+ typeof item === 'object' &&
20
+ typeof Reflect.get(item, 'summary') === 'string' &&
21
+ typeof Reflect.get(item, 'hash') === 'string',
22
+ ),
23
+ )
24
+ )
25
+ }
26
+
27
+ export async function fetchRuntimeReleaseNotes(
28
+ version: string,
29
+ request: typeof fetch = fetch,
30
+ ): Promise<ChangelogEntry | null> {
31
+ try {
32
+ const url = new URL(SOOTSIM_CHANGELOG_API_URL)
33
+ url.searchParams.set('version', version)
34
+ const response = await request(url, {
35
+ headers: { accept: 'application/json' },
36
+ signal: AbortSignal.timeout(5000),
37
+ })
38
+ if (!response.ok) return null
39
+ const payload: unknown = await response.json()
40
+ if (!payload || typeof payload !== 'object') return null
41
+ const entries = Reflect.get(payload, 'entries')
42
+ if (!Array.isArray(entries) || !isChangelogEntry(entries[0])) return null
43
+ return entries[0]
44
+ } catch {
45
+ return null
46
+ }
47
+ }
48
+
49
+ export function formatRuntimeReleaseNotes(
50
+ version: string,
51
+ entry: ChangelogEntry | null,
52
+ ): string {
53
+ const lines = [`what's new in rnx engine v${version}:`]
54
+ if (!entry || entry.sections.length === 0) {
55
+ lines.push(` release notes are not available yet`)
56
+ } else {
57
+ for (const section of entry.sections) {
58
+ lines.push(` ${section.title}:`)
59
+ for (const item of section.items) {
60
+ lines.push(` • ${item.summary}`)
61
+ }
62
+ }
63
+ }
64
+ lines.push(` full changelog: ${SOOTSIM_CHANGELOG_URL}`)
65
+ return lines.join('\n')
66
+ }
@@ -0,0 +1,25 @@
1
+ import { readActiveRuntime, shouldSkipPersistentDaemon } from '../src/home-paths'
2
+ import { resolveBridgeWorld } from './ws-bridge'
3
+
4
+ export type RuntimeSummary = {
5
+ primary: string
6
+ installedRuntime: string | null
7
+ isDevBridge: boolean
8
+ }
9
+
10
+ export function getRuntimeSummary(): RuntimeSummary {
11
+ const active = readActiveRuntime()
12
+ const dev = shouldSkipPersistentDaemon() ? resolveBridgeWorld() : null
13
+ if (dev?.source === 'dev-bridge') {
14
+ return {
15
+ primary: `engine dev source (:${dev.bridgePort})`,
16
+ installedRuntime: active ? `installed runtime v${active}` : null,
17
+ isDevBridge: true,
18
+ }
19
+ }
20
+ return {
21
+ primary: active ? `runtime v${active}` : 'runtime not installed',
22
+ installedRuntime: null,
23
+ isDevBridge: false,
24
+ }
25
+ }
@@ -0,0 +1,187 @@
1
+ import { spawn } from 'node:child_process'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { rnxPublicBrand } from '../src/public-brand'
5
+
6
+ export type SetupPackageManager = 'bun' | 'pnpm' | 'npm'
7
+
8
+ export interface RepositoryOperation {
9
+ command: string
10
+ args: string[]
11
+ description: string
12
+ }
13
+
14
+ export interface RepositorySetupPlan {
15
+ appDir: string
16
+ packageManager: SetupPackageManager
17
+ operations: RepositoryOperation[]
18
+ }
19
+
20
+ interface PackageManifest {
21
+ scripts?: Record<string, string>
22
+ devDependencies?: Record<string, string>
23
+ }
24
+
25
+ export function findPackageRoot(startDir: string): string | null {
26
+ let current = path.resolve(startDir)
27
+ while (true) {
28
+ if (fs.existsSync(path.join(current, 'package.json'))) return current
29
+ const parent = path.dirname(current)
30
+ if (parent === current) return null
31
+ current = parent
32
+ }
33
+ }
34
+
35
+ export function detectPreferredPackageManager(startDir: string): SetupPackageManager {
36
+ let current = path.resolve(startDir)
37
+ while (true) {
38
+ if (fs.existsSync(path.join(current, 'bun.lock'))) return 'bun'
39
+ if (fs.existsSync(path.join(current, 'bun.lockb'))) return 'bun'
40
+ if (fs.existsSync(path.join(current, 'pnpm-lock.yaml'))) return 'pnpm'
41
+ if (fs.existsSync(path.join(current, 'package-lock.json'))) return 'npm'
42
+ const parent = path.dirname(current)
43
+ if (parent === current) return 'npm'
44
+ current = parent
45
+ }
46
+ }
47
+
48
+ export function planRepositorySetup(input: {
49
+ appDir: string
50
+ cliVersion: string
51
+ }): RepositorySetupPlan {
52
+ const appDir = path.resolve(input.appDir)
53
+ const manifest = readManifest(path.join(appDir, 'package.json'))
54
+ const expectedScript = `${rnxPublicBrand.commandName} open`
55
+ const existingScript = manifest.scripts?.[rnxPublicBrand.commandName]
56
+ if (existingScript && existingScript !== expectedScript) {
57
+ throw new Error(
58
+ `package.json already defines scripts.${rnxPublicBrand.commandName} as ${JSON.stringify(existingScript)}; resolve that conflict before setup`,
59
+ )
60
+ }
61
+
62
+ const packageManager = detectPreferredPackageManager(appDir)
63
+ const operations: RepositoryOperation[] = []
64
+ if (manifest.devDependencies?.[rnxPublicBrand.packageName] !== input.cliVersion) {
65
+ operations.push(addDependencyOperation(packageManager, input.cliVersion))
66
+ }
67
+ if (existingScript !== expectedScript) {
68
+ operations.push(scriptOperation(packageManager))
69
+ }
70
+ return {
71
+ appDir,
72
+ packageManager,
73
+ operations,
74
+ }
75
+ }
76
+
77
+ export async function applyRepositorySetup(
78
+ plan: RepositorySetupPlan,
79
+ options: { dryRun: boolean; json: boolean },
80
+ ): Promise<void> {
81
+ for (const operation of plan.operations) {
82
+ if (options.dryRun) continue
83
+ await runOperation(operation, plan.appDir, options.json)
84
+ }
85
+ }
86
+
87
+ export function formatRepositoryOperations(plan: RepositorySetupPlan): string[] {
88
+ return plan.operations.map(
89
+ (operation) => `${operation.command} ${operation.args.map(quoteArg).join(' ')}`,
90
+ )
91
+ }
92
+
93
+ function readManifest(packagePath: string): PackageManifest {
94
+ let parsed: unknown
95
+ try {
96
+ parsed = JSON.parse(fs.readFileSync(packagePath, 'utf8'))
97
+ } catch (error) {
98
+ const message = error instanceof Error ? error.message : String(error)
99
+ throw new Error(`could not read ${packagePath}: ${message}`)
100
+ }
101
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
102
+ throw new Error(`${packagePath} must contain a JSON object`)
103
+ }
104
+ const scripts = readStringRecord(Reflect.get(parsed, 'scripts'))
105
+ const devDependencies = readStringRecord(Reflect.get(parsed, 'devDependencies'))
106
+ return {
107
+ ...(scripts ? { scripts } : {}),
108
+ ...(devDependencies ? { devDependencies } : {}),
109
+ }
110
+ }
111
+
112
+ function readStringRecord(value: unknown): Record<string, string> | null {
113
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null
114
+ const entries = Object.entries(value).filter(
115
+ (entry): entry is [string, string] => typeof entry[1] === 'string',
116
+ )
117
+ return Object.fromEntries(entries)
118
+ }
119
+
120
+ function addDependencyOperation(
121
+ packageManager: SetupPackageManager,
122
+ version: string,
123
+ ): RepositoryOperation {
124
+ const spec = `${rnxPublicBrand.packageName}@${version}`
125
+ switch (packageManager) {
126
+ case 'bun':
127
+ return {
128
+ command: 'bun',
129
+ args: ['add', '--dev', '--exact', spec],
130
+ description: `install ${spec} as an exact dev dependency`,
131
+ }
132
+ case 'pnpm':
133
+ return {
134
+ command: 'pnpm',
135
+ args: ['add', '--save-dev', '--save-exact', spec],
136
+ description: `install ${spec} as an exact dev dependency`,
137
+ }
138
+ case 'npm':
139
+ return {
140
+ command: 'npm',
141
+ args: ['install', '--save-dev', '--save-exact', spec],
142
+ description: `install ${spec} as an exact dev dependency`,
143
+ }
144
+ }
145
+ }
146
+
147
+ function quoteArg(value: string): string {
148
+ return /^[a-zA-Z0-9_./@=-]+$/.test(value) ? value : JSON.stringify(value)
149
+ }
150
+
151
+ function runOperation(
152
+ operation: RepositoryOperation,
153
+ cwd: string,
154
+ json: boolean,
155
+ ): Promise<void> {
156
+ return new Promise((resolve, reject) => {
157
+ const child = spawn(operation.command, operation.args, {
158
+ cwd,
159
+ stdio: json ? ['ignore', 2, 'inherit'] : 'inherit',
160
+ })
161
+ child.once('error', reject)
162
+ child.once('exit', (code) => {
163
+ if (code === 0) resolve()
164
+ else reject(new Error(`${operation.command} exited with code ${code ?? 'unknown'}`))
165
+ })
166
+ })
167
+ }
168
+
169
+ function scriptOperation(packageManager: SetupPackageManager): RepositoryOperation {
170
+ return {
171
+ command: packageManager,
172
+ args:
173
+ packageManager === 'bun'
174
+ ? [
175
+ 'pm',
176
+ 'pkg',
177
+ 'set',
178
+ `scripts.${rnxPublicBrand.commandName}=${rnxPublicBrand.commandName} open`,
179
+ ]
180
+ : [
181
+ 'pkg',
182
+ 'set',
183
+ `scripts.${rnxPublicBrand.commandName}=${rnxPublicBrand.commandName} open`,
184
+ ],
185
+ description: `add the \`${rnxPublicBrand.commandName}\` package script`,
186
+ }
187
+ }
@@ -0,0 +1,187 @@
1
+ // cli-side telemetry. fire-and-forget posthog events tagged with
2
+ // source='cli' plus whatever identity is available from the local shared
3
+ // desktop auth session.
4
+ //
5
+ // this file is intentionally self-contained — no imports from Contrast `src/`
6
+ // or other in-monorepo packages — so the sootsim npm package stays small
7
+ // and doesn't drag app code into the distributed cli.
8
+ //
9
+ // scope: only emit at cli boundaries that matter for product metering
10
+ // (e.g. upload command start/completion). authoritative metering for
11
+ // uploads lives on the server; the cli event is there so we can observe
12
+ // cli invocation volume even when the request never reaches the server.
13
+
14
+ import { isValidTeamMachineSessionId } from '../src/agent-identity'
15
+ import { readSharedDesktopAuthSession } from '../src/auth/shared-session'
16
+ import { readOrCreateAnonymousId, readPrivacyPreferences } from '../src/home-paths'
17
+
18
+ type Plan = 'free' | 'pro' | 'enterprise'
19
+ type Source = 'cli' | 'browser' | 'electron' | 'github-app' | 'server'
20
+
21
+ type Identity = {
22
+ userId?: string | null
23
+ repoId?: string | null
24
+ installationId?: string | number | null
25
+ shareId?: string | null
26
+ plan?: Plan | null
27
+ source?: Source
28
+ }
29
+
30
+ export type CliTelemetryEvent = {
31
+ event: string
32
+ identity?: Identity
33
+ properties?: Record<string, unknown>
34
+ }
35
+
36
+ type Config = { apiKey: string; host: string }
37
+
38
+ function isOptedOut(): boolean {
39
+ // standard cross-tool opt-out signal. honored by npm, homebrew, supabase,
40
+ // turbo, etc. — set DO_NOT_TRACK=1 to silence telemetry across the board.
41
+ const v = process.env.DO_NOT_TRACK
42
+ if (v === '1' || v === 'true') return true
43
+ // shared user preference written from the electron splash screen and any
44
+ // future cli `sootsim config telemetry off` command. false in config.json wins
45
+ // over an absent env var; missing/default means opted in.
46
+ if (!readPrivacyPreferences().productAnalytics) return true
47
+ return false
48
+ }
49
+
50
+ function loadConfig(): Config | null {
51
+ if (isOptedOut()) return null
52
+ // env var precedence: explicit SootSim CLI override → vite-mirrored value.
53
+ // the published cli bakes CONTRAST_POSTHOG_API_KEY in at build time (see
54
+ // scripts/build-cli.ts); a keyless build silently no-ops, keeping the
55
+ // beta plan's "cli remains ungated" rule.
56
+ const apiKey =
57
+ process.env.CONTRAST_POSTHOG_API_KEY || process.env.VITE_POSTHOG_API_KEY || ''
58
+ if (!apiKey) return null
59
+ const host =
60
+ process.env.CONTRAST_POSTHOG_HOST ||
61
+ process.env.VITE_POSTHOG_HOST ||
62
+ 'https://us.i.posthog.com'
63
+ return { apiKey, host: host.replace(/\/+$/, '') }
64
+ }
65
+
66
+ function anonymousDistinctId(): string {
67
+ // per-machine persisted id. the old literal 'anonymous-cli' merged every
68
+ // signed-out user into one posthog person, hiding individual first-run
69
+ // journeys (like a login that hung). never let telemetry throw.
70
+ try {
71
+ return readOrCreateAnonymousId()
72
+ } catch {
73
+ return 'anonymous-cli'
74
+ }
75
+ }
76
+
77
+ function resolveDistinctId(identity: Identity | undefined): string {
78
+ if (!identity) return anonymousDistinctId()
79
+ if (identity.userId) return identity.userId
80
+ if (identity.installationId != null) return `install:${identity.installationId}`
81
+ if (identity.repoId) return `repo:${identity.repoId}`
82
+ if (identity.shareId) return `share:${identity.shareId}`
83
+ return anonymousDistinctId()
84
+ }
85
+
86
+ function toPosthog(event: CliTelemetryEvent) {
87
+ const identity = event.identity ?? {}
88
+ const agentSession = process.env.TM_SESSION
89
+ const automationSource =
90
+ agentSession && isValidTeamMachineSessionId(agentSession)
91
+ ? 'team-machine'
92
+ : process.env.CI === '1' ||
93
+ process.env.CI === 'true' ||
94
+ process.env.GITHUB_ACTIONS === 'true'
95
+ ? 'ci'
96
+ : null
97
+ const properties: Record<string, unknown> = {
98
+ $lib: 'sootsim-cli',
99
+ source: identity.source ?? 'cli',
100
+ ...event.properties,
101
+ traffic_type: automationSource ? 'automated' : 'human',
102
+ is_automated: automationSource !== null,
103
+ ...(automationSource ? { automation_source: automationSource } : {}),
104
+ }
105
+ if (identity.userId) properties.userId = identity.userId
106
+ if (identity.repoId) properties.repoId = identity.repoId
107
+ if (identity.installationId != null) {
108
+ properties.installationId = String(identity.installationId)
109
+ }
110
+ if (identity.shareId) properties.shareId = identity.shareId
111
+ if (identity.plan) properties.plan = identity.plan
112
+
113
+ return {
114
+ event: event.event,
115
+ distinct_id: resolveDistinctId(identity),
116
+ properties,
117
+ timestamp: new Date().toISOString(),
118
+ }
119
+ }
120
+
121
+ const queue: CliTelemetryEvent[] = []
122
+ let exitHooksInstalled = false
123
+
124
+ async function drain(): Promise<void> {
125
+ if (queue.length === 0) return
126
+ const cfg = loadConfig()
127
+ if (!cfg) {
128
+ // silently drop — the cli shouldn't die because telemetry isn't
129
+ // configured on this machine.
130
+ queue.length = 0
131
+ return
132
+ }
133
+ const batch = queue.splice(0, queue.length).map(toPosthog)
134
+ try {
135
+ // hard timeout: a hung posthog endpoint must never block a cli
136
+ // command. 2s is well under any user's patience threshold and gives
137
+ // a healthy posthog plenty of time to ack the batch.
138
+ await fetch(`${cfg.host}/batch/`, {
139
+ method: 'POST',
140
+ headers: { 'content-type': 'application/json' },
141
+ body: JSON.stringify({ api_key: cfg.apiKey, batch }),
142
+ signal: AbortSignal.timeout(2000),
143
+ })
144
+ } catch {
145
+ // swallow — never fail a cli command because of telemetry.
146
+ }
147
+ }
148
+
149
+ function installExitHooks() {
150
+ if (exitHooksInstalled) return
151
+ exitHooksInstalled = true
152
+ // best-effort: beforeExit/exit handlers can't await async work, so
153
+ // process.exit() paths drop in-flight events. call flushCliTelemetry()
154
+ // explicitly before `process.exit()` at sites that enqueued events.
155
+ process.on('beforeExit', () => void drain())
156
+ process.on('exit', () => void drain())
157
+ }
158
+
159
+ // enrich + enqueue a cli event. reads the shared desktop session once per
160
+ // call so logout between events is reflected. source defaults to 'cli'.
161
+ export function trackCliEvent(event: CliTelemetryEvent): void {
162
+ if (isOptedOut()) return
163
+ installExitHooks()
164
+
165
+ let userId: string | null = null
166
+ try {
167
+ userId = readSharedDesktopAuthSession()?.user?.id ?? null
168
+ } catch {
169
+ userId = null
170
+ }
171
+
172
+ queue.push({
173
+ ...event,
174
+ identity: {
175
+ source: 'cli',
176
+ userId,
177
+ ...event.identity,
178
+ },
179
+ })
180
+ }
181
+
182
+ // explicit flush — use before a process.exit() call that won't give the
183
+ // beforeExit hook time to complete its fetch. idempotent: safe to call
184
+ // multiple times; drains whatever is currently queued.
185
+ export async function flushCliTelemetry(): Promise<void> {
186
+ await drain()
187
+ }