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,54 @@
1
+ // sootsim version — one overview of both halves you're running.
2
+ //
3
+ // the `sootsim` CLI (npm package) and the engine runtime (versioned tarball
4
+ // rendered in the browser/electron shell) are versioned independently, which
5
+ // is a frequent "I upgraded but the version didn't change" trap. the terse
6
+ // `sootsim --version` flag prints both for scripts; this command adds the
7
+ // channel, a "newer runtime available" hint, and a what's-new pointer.
8
+
9
+ import fs from 'node:fs'
10
+ import path from 'node:path'
11
+ import { getCliVersion } from '../../src/cli-version'
12
+ import { runtimeDir } from '../../src/home-paths'
13
+ import { SOOTSIM_CHANGELOG_URL, sootsimRuntime } from '../../src/runtime-delivery'
14
+ import { resolveSootSimAppConfig } from '../app-config'
15
+ import { getRuntimeSummary } from '../runtime-summary'
16
+
17
+ export async function runVersion(_args: string[]): Promise<void> {
18
+ const { IS_BETA, BETA_LABEL } = await import('../../src/beta')
19
+ const suffix = IS_BETA ? ` · ${BETA_LABEL}` : ''
20
+ console.log(`rnxsim v${getCliVersion()}${suffix}`)
21
+
22
+ const runtime = getRuntimeSummary()
23
+ const channel = sootsimRuntime.resolveChannel()
24
+ if (runtime.isDevBridge) {
25
+ console.log(runtime.primary)
26
+ if (runtime.installedRuntime) console.log(`${runtime.installedRuntime} · ${channel}`)
27
+ } else if (runtime.primary === 'runtime not installed') {
28
+ console.log(`runtime not installed — run \`rnxsim runtime install\``)
29
+ } else {
30
+ console.log(`${runtime.primary} · ${channel}`)
31
+ }
32
+
33
+ const { config } = await resolveSootSimAppConfig()
34
+ if (config?.runtimeVersion) {
35
+ const installed = fs.existsSync(
36
+ path.join(runtimeDir(config.runtimeVersion), 'index.html'),
37
+ )
38
+ console.log(
39
+ `project runtime v${config.runtimeVersion} · configured · ${
40
+ installed ? 'installed' : 'installs on open'
41
+ }`,
42
+ )
43
+ }
44
+
45
+ // best-effort network check; never fail the command on a flaky network.
46
+ const check = await sootsimRuntime.checkUpToDate({ channel })
47
+ if (check.outdated && check.latest) {
48
+ console.log(
49
+ `\n ↑ default runtime v${check.latest} available; run \`rnxsim upgrade\``,
50
+ )
51
+ }
52
+
53
+ console.log(`\n what's new: ${SOOTSIM_CHANGELOG_URL}`)
54
+ }
@@ -0,0 +1,327 @@
1
+ // sootsim what-happened — show recent events from the semantic timeline
2
+ //
3
+ // the agent-facing read verb for SootSim.bridges.timeline. by default
4
+ // shows everything since the previous CLI call from the same agent
5
+ // identity (cursor model). after rendering, advances the cursor past
6
+ // the events it displayed so the next call shows only new events.
7
+ //
8
+ // usage:
9
+ // sootsim what-happened # since last call
10
+ // sootsim what-happened --summary # one-line counts
11
+ // sootsim what-happened --all # full ring (no cursor)
12
+ // sootsim what-happened --since 5s # absolute window
13
+ // sootsim what-happened --kinds toast,fetch # filter
14
+ // sootsim what-happened --json # structured
15
+ // sootsim what-happened --limit 50 # cap (default 200)
16
+
17
+ import { getCliIdentityKey } from '../current-sim'
18
+ import { createBridgeFromParsed, parseBridgeCliArgs } from '../ws-bridge'
19
+ import {
20
+ formatTimelineEvent,
21
+ formatTimelineSummary,
22
+ inspectTimelineAdvanceCursor,
23
+ inspectTimelineRecent,
24
+ inspectTimelineSummary,
25
+ NOISY_TIMELINE_KINDS,
26
+ } from './inspect/core'
27
+ import type { SootSimTimelineEvent, SootSimTimelineQuery } from '@sootsim/globals'
28
+
29
+ interface WhatHappenedOptions {
30
+ port?: number
31
+ verbose?: boolean
32
+ }
33
+
34
+ function parseSinceFlag(args: string[]): { since?: number; consumed: number[] } {
35
+ const consumed: number[] = []
36
+ for (let i = 0; i < args.length; i++) {
37
+ if (args[i] === '--since' && i + 1 < args.length) {
38
+ consumed.push(i, i + 1)
39
+ const v = args[i + 1].trim()
40
+ // accept '5s', '500ms', or absolute ms epoch
41
+ const m = /^(\d+(?:\.\d+)?)(ms|s|m)?$/.exec(v)
42
+ if (m) {
43
+ const n = Number(m[1])
44
+ const unit = m[2] ?? 'ms'
45
+ const ms = unit === 's' ? n * 1000 : unit === 'm' ? n * 60_000 : n
46
+ return { since: Date.now() - ms, consumed }
47
+ }
48
+ const asNum = Number(v)
49
+ if (Number.isFinite(asNum) && asNum > 1_000_000_000_000) {
50
+ return { since: asNum, consumed }
51
+ }
52
+ }
53
+ }
54
+ return { consumed }
55
+ }
56
+
57
+ function parseKindsFlag(args: string[]): { kinds?: string[]; consumed: number[] } {
58
+ const consumed: number[] = []
59
+ for (let i = 0; i < args.length; i++) {
60
+ if (args[i] === '--kinds' && i + 1 < args.length) {
61
+ consumed.push(i, i + 1)
62
+ return {
63
+ kinds: args[i + 1]
64
+ .split(',')
65
+ .map((s) => s.trim())
66
+ .filter(Boolean),
67
+ consumed,
68
+ }
69
+ }
70
+ }
71
+ return { consumed }
72
+ }
73
+
74
+ function parseLimitFlag(args: string[]): { limit?: number; consumed: number[] } {
75
+ const consumed: number[] = []
76
+ for (let i = 0; i < args.length; i++) {
77
+ if (args[i] === '--limit' && i + 1 < args.length) {
78
+ consumed.push(i, i + 1)
79
+ const n = Number(args[i + 1])
80
+ if (Number.isFinite(n) && n > 0) return { limit: n, consumed }
81
+ }
82
+ }
83
+ return { consumed }
84
+ }
85
+
86
+ interface FlowSection {
87
+ label: string
88
+ events: SootSimTimelineEvent[]
89
+ startedAt: number | null
90
+ }
91
+
92
+ // section the events by screen/route boundaries for `what-happened
93
+ // --flow`. each screen-name (or route-path) becomes a section
94
+ // header; every event that lands between two screen changes belongs
95
+ // to the first one's section. events before the first screen change
96
+ // land in an "initial state" section so nothing gets dropped.
97
+ //
98
+ // design note: this groups by *appearance* events only (phase ===
99
+ // 'enter' or no phase). disappearance events (`exit`) end the
100
+ // current section but don't start a new one — they're listed inside
101
+ // the section that contained them.
102
+ function groupByScreen(events: SootSimTimelineEvent[]): FlowSection[] {
103
+ const sections: FlowSection[] = []
104
+ let current: FlowSection = {
105
+ label: 'initial state',
106
+ events: [],
107
+ startedAt: events[0]?.t ?? null,
108
+ }
109
+ sections.push(current)
110
+ for (const ev of events) {
111
+ current.events.push(ev)
112
+ if (ev.kind === 'screen' || ev.kind === 'route') {
113
+ const data = ev.data as Record<string, unknown> | null
114
+ const phase = data?.phase as string | undefined
115
+ // only treat 'enter' (and the legacy no-phase form) as a
116
+ // section boundary. an 'exit' event is the tail of a screen,
117
+ // not the start of a new one.
118
+ if (!phase || phase === 'enter' || phase === 'appear' || phase === 'active') {
119
+ const name =
120
+ (data?.name as string) ||
121
+ (data?.activeName as string) ||
122
+ (data?.path as string) ||
123
+ (data?.pathname as string) ||
124
+ ev.kind
125
+ if (sections.length === 1 && current.events.length === 1) {
126
+ // first screen event — rename "initial state" inline so it
127
+ // doesn't show an empty initial-state section before it.
128
+ current.label = `${ev.kind}: ${name}`
129
+ } else {
130
+ current = {
131
+ label: `${ev.kind}: ${name}`,
132
+ events: [],
133
+ startedAt: ev.t,
134
+ }
135
+ sections.push(current)
136
+ }
137
+ }
138
+ }
139
+ }
140
+ return sections
141
+ }
142
+
143
+ export async function runWhatHappened(args: string[], opts: WhatHappenedOptions) {
144
+ const parsed = parseBridgeCliArgs(args, {
145
+ port: opts.port,
146
+ stripBooleanFlags: [
147
+ '--summary',
148
+ '--all',
149
+ '--json',
150
+ '--no-advance',
151
+ '--help',
152
+ '-h',
153
+ // `--flow` groups events under screen/route boundaries instead of
154
+ // printing a strict chronological list. demo-friendly for the
155
+ // "what just happened in this sim" recap.
156
+ '--flow',
157
+ // by default we hide high-frequency render/layout/scroll noise so
158
+ // a single tap doesn't get buried under 1000+ rows. enable
159
+ // `react-commit` with `sootsim timeline start react-commit`, then pass
160
+ // `--noisy` (or an explicit `--kinds react-commit,…` filter) when you
161
+ // actually want to see them.
162
+ '--noisy',
163
+ ],
164
+ stripValueFlags: ['--since', '--kinds', '--limit'],
165
+ })
166
+
167
+ if (args.includes('--help') || args.includes('-h')) {
168
+ console.log(`
169
+ rnxsim what-happened — show recent events from the semantic timeline
170
+
171
+ usage:
172
+ rnxsim what-happened # since last CLI call
173
+ rnxsim what-happened --summary # one-line counts
174
+ rnxsim what-happened --all # full ring (ignore cursor)
175
+ rnxsim what-happened --since 5s # absolute window
176
+ rnxsim what-happened --kinds toast,fetch
177
+ rnxsim what-happened --noisy # include react-commit/layout/scroll
178
+ rnxsim what-happened --limit 50
179
+ rnxsim what-happened --json
180
+ rnxsim what-happened --no-advance # don't advance cursor after read
181
+
182
+ note: react-commit, layout, and scroll are opt-in/noisy events. enable them
183
+ with "rnxsim timeline start <kind>", then pass --noisy or include them in
184
+ --kinds to see them.
185
+ `)
186
+ process.exit(0)
187
+ }
188
+
189
+ const summaryMode = args.includes('--summary')
190
+ const flowMode = args.includes('--flow')
191
+ const allMode = args.includes('--all')
192
+ const jsonMode = args.includes('--json')
193
+ const noAdvance = args.includes('--no-advance')
194
+ const noisyMode = args.includes('--noisy')
195
+
196
+ const { since } = parseSinceFlag(args)
197
+ const { kinds } = parseKindsFlag(args)
198
+ const { limit } = parseLimitFlag(args)
199
+
200
+ const cursorKey = getCliIdentityKey()
201
+ const query: SootSimTimelineQuery = {
202
+ limit: limit ?? 200,
203
+ ...(kinds && kinds.length ? { kinds: kinds as SootSimTimelineQuery['kinds'] } : {}),
204
+ ...(since !== undefined ? { since } : allMode ? {} : { sinceCursor: cursorKey }),
205
+ }
206
+
207
+ const bridge = createBridgeFromParsed(parsed)
208
+ try {
209
+ if (summaryMode) {
210
+ const summary = await inspectTimelineSummary(bridge, query)
211
+ if (jsonMode) {
212
+ console.log(JSON.stringify(summary))
213
+ } else {
214
+ const since_label = allMode
215
+ ? 'all time'
216
+ : since !== undefined
217
+ ? `last ${((Date.now() - since) / 1000).toFixed(1)}s`
218
+ : 'since last call'
219
+ console.log(` ${since_label}: ${formatTimelineSummary(summary)}`)
220
+ }
221
+ // advance cursor past the summary window's end
222
+ if (!noAdvance && !allMode && summary.lastAt) {
223
+ await inspectTimelineAdvanceCursor(bridge, cursorKey, summary.lastAt)
224
+ }
225
+ return
226
+ }
227
+
228
+ const result = await inspectTimelineRecent(bridge, query)
229
+
230
+ // hide high-frequency render/layout/scroll noise unless the caller
231
+ // explicitly asked for it (--noisy or --kinds containing one of
232
+ // these). this is the single most common reason `what-happened`
233
+ // output drowns out the meaningful events for the user/agent.
234
+ const userPickedNoisyKind =
235
+ Array.isArray(kinds) && kinds.some((k) => NOISY_TIMELINE_KINDS.has(k))
236
+ const suppressNoisy = !noisyMode && !userPickedNoisyKind
237
+ let suppressedNoisyCount = 0
238
+ if (suppressNoisy) {
239
+ const kept = result.events.filter((ev) => {
240
+ if (NOISY_TIMELINE_KINDS.has(ev.kind)) {
241
+ suppressedNoisyCount += 1
242
+ return false
243
+ }
244
+ return true
245
+ })
246
+ result.events = kept
247
+ }
248
+
249
+ if (jsonMode) {
250
+ if (flowMode) {
251
+ // grouped JSON shape — sections by screen/route. agents who want
252
+ // a chronological stream can still get it from the default
253
+ // shape (without --flow).
254
+ console.log(JSON.stringify(groupByScreen(result.events), null, 2))
255
+ } else {
256
+ console.log(JSON.stringify(result, null, 2))
257
+ }
258
+ } else {
259
+ if (result.events.length === 0) {
260
+ // distinguish "truly no events" from "events present but all
261
+ // filtered as noise". the second case is the much more common
262
+ // misdiagnosis — agents think a tap did nothing when actually
263
+ // it just produced react-commits and no shell-state change.
264
+ if (suppressedNoisyCount > 0) {
265
+ console.log(
266
+ ` no non-noise events (${suppressedNoisyCount} react-commit/layout/scroll hidden)`,
267
+ )
268
+ } else if (allMode) {
269
+ console.log(' no events recorded')
270
+ } else if (since !== undefined) {
271
+ console.log(' no events in window')
272
+ } else {
273
+ console.log(' no new events since last call')
274
+ }
275
+ } else if (flowMode) {
276
+ // section-per-screen render. each section heads with the
277
+ // screen/route name and lists every event that landed in
278
+ // that section in chronological order. unattributed events
279
+ // (events that landed before the first screen change) go in
280
+ // an "initial state" section.
281
+ const anchor = result.events[0]?.t ?? null
282
+ const sections = groupByScreen(result.events)
283
+ const header = allMode
284
+ ? `─── ${result.events.length} event(s) total — flow view ───`
285
+ : since !== undefined
286
+ ? `─── ${result.events.length} event(s) in last ${((Date.now() - since) / 1000).toFixed(1)}s — flow view ───`
287
+ : `─── ${result.events.length} event(s) since last call — flow view ───`
288
+ console.log(` ${header}`)
289
+ for (const section of sections) {
290
+ console.log(
291
+ `\n ── ${section.label} (${section.events.length} event${section.events.length === 1 ? '' : 's'}) ──`,
292
+ )
293
+ for (const ev of section.events) {
294
+ console.log(formatTimelineEvent(ev, anchor))
295
+ }
296
+ }
297
+ } else {
298
+ const anchor = result.events[0]?.t ?? null
299
+ const header = allMode
300
+ ? `─── ${result.events.length} event(s) total ───`
301
+ : since !== undefined
302
+ ? `─── ${result.events.length} event(s) in last ${((Date.now() - since) / 1000).toFixed(1)}s ───`
303
+ : `─── ${result.events.length} event(s) since last call ───`
304
+ console.log(` ${header}`)
305
+ for (const ev of result.events) {
306
+ console.log(formatTimelineEvent(ev, anchor))
307
+ }
308
+ }
309
+ // when at least one real event printed, append the suppression
310
+ // note so the user knows the picture isn't complete. when the
311
+ // window was entirely noise, the empty-state branch above already
312
+ // mentioned it — don't double up.
313
+ if (suppressedNoisyCount > 0 && result.events.length > 0) {
314
+ process.stderr.write(
315
+ `\n ${suppressedNoisyCount} high-frequency event(s) hidden (react-commit/layout/scroll)\n` +
316
+ ` rerun with --noisy or --kinds react-commit,layout,scroll to include them\n`,
317
+ )
318
+ }
319
+ }
320
+
321
+ if (!noAdvance && !allMode && result.watermark > 0) {
322
+ await inspectTimelineAdvanceCursor(bridge, cursorKey, result.watermark)
323
+ }
324
+ } finally {
325
+ bridge.close()
326
+ }
327
+ }
@@ -0,0 +1,204 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'
2
+ import { tmpdir } from 'os'
3
+ import { dirname, join, resolve } from 'path'
4
+ import { getParentPid, getProcessComm } from './parent-pid'
5
+
6
+ const CURRENT_SIM_VERSION = 1 as const
7
+ const CURRENT_SIM_PATH_ENV = 'SOOTSIM_CLI_CURRENT_SIM_PATH'
8
+
9
+ interface CurrentSimState {
10
+ version: 1
11
+ simId: string
12
+ updatedAt: string
13
+ }
14
+
15
+ // env vars that tend to identify a specific terminal, pane, or agent run.
16
+ // checked in order — the first one present wins. anything further down is a
17
+ // progressively weaker signal of "same caller across invocations".
18
+ const IDENTITY_ENV_VARS = [
19
+ 'SOOTSIM_CLI_IDENTITY', // explicit override — fastest path, skips all pid lookups
20
+ 'TM_SESSION', // Team Machine session id; routes prompt-bar followups back to this agent
21
+ 'CLAUDE_CODE_SESSION_ID', // claude code, if ever set
22
+ 'CODEX_THREAD_ID', // codex / openai agent thread, stable across tool invocations
23
+ 'TERM_SESSION_ID', // macOS Terminal.app + iTerm2
24
+ 'ITERM_SESSION_ID', // iTerm2 (older variant)
25
+ 'TMUX_PANE', // tmux — stable per pane
26
+ 'STY', // GNU screen session
27
+ 'KITTY_WINDOW_ID', // kitty
28
+ 'WEZTERM_PANE', // wezterm
29
+ 'ALACRITTY_WINDOW_ID', // alacritty
30
+ 'WINDOWID', // X11 terminals
31
+ 'VSCODE_INJECTION', // vscode integrated terminal
32
+ ] as const
33
+
34
+ export interface CliIdentity {
35
+ key: string
36
+ source: (typeof IDENTITY_ENV_VARS)[number] | 'grand-ppid' | 'ppid' | 'explicit-sim-id'
37
+ stable: boolean
38
+ }
39
+
40
+ // derive a stable per-terminal identity key so different agents/terminals
41
+ // don't share state. priority:
42
+ // 1. explicit env vars (iTerm, tmux, etc.)
43
+ // 2. grand-ppid — the parent of our invoking shell, which in Claude Code's
44
+ // Bash tool is the per-agent claude process (stable across invocations,
45
+ // distinct between agents). generic — not claude-specific.
46
+ // 3. ppid — the invoking shell's pid. stable when the shell is reused
47
+ // across invocations (claude's bash tool, persistent tty sessions).
48
+ // unstable when each call spawns a fresh shell — in that case set an
49
+ // env var from IDENTITY_ENV_VARS to pin the identity.
50
+ // an earlier env-hash fallback was removed: sha'ing process.env looks stable
51
+ // but in practice any per-turn env drift (timestamps, cwd, agent vars)
52
+ // flipped the hash and made the same agent look like a new client.
53
+ // walk up to the topmost non-init ancestor. needed because intermediate
54
+ // pids drift across invocations:
55
+ // - claude code's bash tool spawns a fresh shell per call, so the
56
+ // immediate gppid (the shell's parent) can change
57
+ // - shell pipelines (`sootsim list | grep`) wrap the command in a
58
+ // subshell, adding an extra ppid layer that shifts gppid down by one
59
+ // the topmost user process (just below init/launchd) is stable across
60
+ // all of these — same agent / login shell / terminal across calls.
61
+ // terminal-multiplexer servers are effectively immortal (the tmux server
62
+ // lives while ANY session exists), so an owner-pid walk that climbs into one
63
+ // produces an owner that never dies — detached browser hosts then leak until
64
+ // an explicit close (real incident: a tmux-launched sim's chrome survived its
65
+ // pane at 129% cpu). the Team Machine daemon is the same class: it spawns worker
66
+ // harnesses directly, outlives them all, and additionally collapses every
67
+ // worker into one shared identity key (real incident 2026-07-14: three film
68
+ // render hosts leaked with owner = the daemon while load hit 224). stop
69
+ // BELOW these hosts: the child (pane shell / worker harness) dies with the
70
+ // session, which is the lifetime a sim owner should have.
71
+ // these are matched against libproc's pbi_comm, the executable's basename,
72
+ // truncated to 16 chars. Team Machine reports comm `tm`; matching its product
73
+ // name never fired and every agent-launched sim resolved its owner to the terminal
74
+ // tab's /usr/bin/login instead (real incident 2026-08-06: six chrome hosts
75
+ // survived their agents, 17GB rss, load 19 with the GPU saturated).
76
+ const IMMORTAL_HOST_COMM_RE = /^(?:tmux|screen)|^tm$/i
77
+
78
+ function findTopAncestor(start: number, maxDepth = 20): number | null {
79
+ // check each CANDIDATE's own comm and return the child below a multiplexer
80
+ // (prev). checking only the parent's comm looked equivalent but was not: on
81
+ // macOS the tmux server is launchd's direct child, so it arrives as the
82
+ // candidate via the initializer and the parent<=1 branch returned it before
83
+ // any comm check ran (runtime-caught on a real tmux box: owner resolved to
84
+ // the immortal server). prev starts at `start` so a pane shell directly
85
+ // under the server resolves to itself.
86
+ let prev = start
87
+ let pid = getParentPid(start)
88
+ if (!pid || pid <= 1) {
89
+ // a readable ordinary process directly below init is itself the stable
90
+ // session root. this is the shape produced by detached job launchers, and
91
+ // returning null here used to disable the browser owner's lifetime watch.
92
+ // an unreadable or known-immortal process has no defensible child bound at
93
+ // this point, so leave it unresolved and make the launcher fail loudly.
94
+ const comm = getProcessComm(start)
95
+ return start > 1 && comm && !IMMORTAL_HOST_COMM_RE.test(comm) ? start : null
96
+ }
97
+ for (let i = 0; i < maxDepth; i++) {
98
+ const comm = getProcessComm(pid)
99
+ if (comm && IMMORTAL_HOST_COMM_RE.test(comm)) return prev
100
+ const parent = getParentPid(pid)
101
+ if (!parent || parent <= 1) {
102
+ // top of the tree we can read. an unreadable comm means a root-owned
103
+ // session host — /usr/bin/login under a terminal tab — which outlives
104
+ // every agent and shell inside it, so stop below it. a readable top is
105
+ // an ordinary user process (a CI job root) and is a valid owner.
106
+ return comm ? pid : prev
107
+ }
108
+ prev = pid
109
+ pid = parent
110
+ }
111
+ return pid
112
+ }
113
+
114
+ export function getCliIdentity(): CliIdentity {
115
+ for (const name of IDENTITY_ENV_VARS) {
116
+ const value = process.env[name]
117
+ if (value && value.trim()) {
118
+ return { key: `${name}:${value.trim()}`, source: name, stable: true }
119
+ }
120
+ }
121
+ const topPid = findTopAncestor(process.ppid)
122
+ if (topPid && topPid > 1) {
123
+ return { key: `gppid-${topPid}`, source: 'grand-ppid', stable: true }
124
+ }
125
+ return { key: `pid-${process.ppid}`, source: 'ppid', stable: false }
126
+ }
127
+
128
+ export function getCliIdentityKey(): string {
129
+ return getCliIdentity().key
130
+ }
131
+
132
+ // the stable owning-session pid: the topmost non-init ancestor of this CLI
133
+ // process — in Claude Code's Bash tool the per-agent process, otherwise the
134
+ // login shell / terminal / CI process-tree root. it lives exactly as long as
135
+ // the session that wants a sim, and dies when that session does. used to tie
136
+ // a detached browser host's lifetime to its owner so the host self-terminates
137
+ // (closing Chrome) when the owner exits, instead of leaking a headless
138
+ // browser forever. returns null on platforms where ancestry can't be read.
139
+ export function getStableOwnerPid(): number | null {
140
+ return findTopAncestor(process.ppid)
141
+ }
142
+
143
+ export function getCurrentSimPath() {
144
+ if (process.env[CURRENT_SIM_PATH_ENV]) {
145
+ return resolve(process.env[CURRENT_SIM_PATH_ENV])
146
+ }
147
+ const key = getCliIdentityKey()
148
+ return join(tmpdir(), `sootsim-current-sim-${key}.json`)
149
+ }
150
+
151
+ export function readCurrentSim(): CurrentSimState | null {
152
+ const filepath = getCurrentSimPath()
153
+ if (!existsSync(filepath)) return null
154
+
155
+ try {
156
+ const parsed = JSON.parse(readFileSync(filepath, 'utf8')) as Partial<CurrentSimState>
157
+ if (
158
+ parsed.version !== CURRENT_SIM_VERSION ||
159
+ typeof parsed.simId !== 'string' ||
160
+ !parsed.simId.trim() ||
161
+ typeof parsed.updatedAt !== 'string'
162
+ ) {
163
+ rmSync(filepath, { force: true })
164
+ return null
165
+ }
166
+
167
+ return {
168
+ version: CURRENT_SIM_VERSION,
169
+ simId: parsed.simId.trim(),
170
+ updatedAt: parsed.updatedAt,
171
+ }
172
+ } catch {
173
+ rmSync(filepath, { force: true })
174
+ return null
175
+ }
176
+ }
177
+
178
+ export function readCurrentSimId() {
179
+ return readCurrentSim()?.simId || null
180
+ }
181
+
182
+ export function saveCurrentSimId(simId: string) {
183
+ const normalized = simId.trim()
184
+ if (!normalized) return
185
+
186
+ const filepath = getCurrentSimPath()
187
+ mkdirSync(dirname(filepath), { recursive: true })
188
+ writeFileSync(
189
+ filepath,
190
+ JSON.stringify(
191
+ {
192
+ version: CURRENT_SIM_VERSION,
193
+ simId: normalized,
194
+ updatedAt: new Date().toISOString(),
195
+ } satisfies CurrentSimState,
196
+ null,
197
+ 2,
198
+ ) + '\n',
199
+ )
200
+ }
201
+
202
+ export function clearCurrentSimId() {
203
+ rmSync(getCurrentSimPath(), { force: true })
204
+ }