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,874 @@
1
+ // sootsim daemon — manage the persistent sootsim bridge daemon
2
+ //
3
+ // wraps `sootsim serve` so it starts automatically on login and restarts if
4
+ // it crashes. the agent is tied to whichever `sootsim` binary resolves at
5
+ // registration time, so `npm update -g sootsim` + `sootsim daemon restart`
6
+ // picks up the upgrade without touching the plist/unit file.
7
+ //
8
+ // `daemonInstall` is shared by `sootsim setup` and the explicit
9
+ // `sootsim daemon install` machine-bootstrap path.
10
+ //
11
+ // macOS: ~/Library/LaunchAgents/dev.sootsim.daemon.plist (launchd)
12
+ // linux: ~/.config/systemd/user/sootsim-daemon.service (systemd --user)
13
+ // win: not yet supported — use `sootsim serve` in a persistent shell.
14
+ //
15
+ // the macOS launchd Label is `dev.sootsim.daemon` (matching the .app
16
+ // CFBundleIdentifier).
17
+
18
+ import { execFileSync, spawnSync } from 'child_process'
19
+ import {
20
+ existsSync,
21
+ mkdirSync,
22
+ readFileSync,
23
+ realpathSync,
24
+ rmSync,
25
+ writeFileSync,
26
+ } from 'fs'
27
+ import { homedir } from 'os'
28
+ import { basename, dirname, resolve, sep } from 'path'
29
+ import { DEFAULT_SOOTSIM_BRIDGE_PORT } from '../../src/bridge-constants'
30
+ import { getCliVersion } from '../../src/cli-version'
31
+ import {
32
+ daemonAppBundlePath,
33
+ isDaemonLockfileFresh,
34
+ readDaemonLockfile,
35
+ shouldSkipPersistentDaemon,
36
+ sootsimHomeDir,
37
+ } from '../../src/home-paths'
38
+ import { rnxPublicBrand } from '../../src/public-brand'
39
+ import { printCommandHelp } from '../help'
40
+ import { flushCliTelemetry, trackCliEvent } from '../telemetry'
41
+ import { ensureDaemonApp } from './daemon-mac-app'
42
+
43
+ // minimum seconds between launchd / systemd respawns. without this, a daemon
44
+ // that crashes on startup respawns in a tight loop — launchd's default is 10s,
45
+ // systemd's previous setting was 3s. bumping to 60s caps the damage of any
46
+ // misconfig that still slips past the bridge-host's port-fallback search.
47
+ const RESPAWN_THROTTLE_SECONDS = 60
48
+
49
+ const SERVICE_LABEL_MAC = 'dev.sootsim.daemon'
50
+ const LEGACY_SERVICE_LABEL_MAC = 'dev.sootsim.server'
51
+ const SERVICE_NAME_LINUX = 'sootsim-daemon'
52
+ const LOG_DIR = resolve(homedir(), 'Library/Logs/sootsim')
53
+ const LOG_DIR_LINUX = resolve(homedir(), '.local/state/sootsim')
54
+
55
+ interface DaemonOptions {
56
+ port?: number
57
+ }
58
+
59
+ export async function runDaemon(args: string[], opts: DaemonOptions = {}) {
60
+ const [sub, ...rest] = args
61
+ const port = opts.port ?? DEFAULT_SOOTSIM_BRIDGE_PORT
62
+
63
+ if (!sub || sub === '--help' || sub === '-h') {
64
+ printHelp()
65
+ return
66
+ }
67
+
68
+ // emit a single observable event per subcommand. validate against the
69
+ // known set so an unknown / malformed `sub` (e.g. accidental path or
70
+ // shell-expanded garbage) doesn't pollute the posthog event taxonomy.
71
+ // tagged with subsource so daemon traffic is filterable from regular
72
+ // cli traffic.
73
+ const KNOWN_SUBCOMMANDS = new Set([
74
+ 'install',
75
+ 'uninstall',
76
+ 'restart',
77
+ 'start',
78
+ 'stop',
79
+ 'logs',
80
+ ])
81
+ if (KNOWN_SUBCOMMANDS.has(sub)) {
82
+ trackCliEvent({
83
+ event: `daemon_${sub}`,
84
+ properties: { platform: process.platform, subsource: 'daemon' },
85
+ })
86
+ }
87
+
88
+ try {
89
+ switch (sub) {
90
+ case 'install':
91
+ return await daemonInstall({ port, force: rest.includes('--force') })
92
+ case 'uninstall':
93
+ return await daemonUninstall()
94
+ case 'status':
95
+ return await daemonStatus()
96
+ case 'restart':
97
+ return await daemonRestart()
98
+ case 'start':
99
+ return await daemonStart()
100
+ case 'stop':
101
+ return await daemonStop()
102
+ case 'logs':
103
+ return await daemonLogs(rest)
104
+ default:
105
+ console.error(` unknown daemon subcommand: ${sub}`)
106
+ printHelp()
107
+ process.exit(1)
108
+ }
109
+ } finally {
110
+ await flushCliTelemetry()
111
+ }
112
+ }
113
+
114
+ function printHelp() {
115
+ printCommandHelp('daemon')
116
+ }
117
+
118
+ // --- resolve sootsim binary ----------------------------------------------
119
+
120
+ interface SootsimInvocation {
121
+ /** absolute path to the executable launchd / systemd should spawn directly. */
122
+ executable: string
123
+ /** args to pass before `serve --quiet --port <port>`. */
124
+ prefixArgs: string[]
125
+ }
126
+
127
+ function resolveSootsimInvocation(): SootsimInvocation {
128
+ // launchd / systemd run agents with a stripped PATH (basically just
129
+ // /usr/bin:/bin:/usr/sbin:/sbin), so a `#!/usr/bin/env node` shebang
130
+ // resolves nothing and the daemon never starts. spawn the absolute node
131
+ // binary directly with the sootsim entry script as an argument.
132
+ //
133
+ // process.execPath is whichever node ran the install command, which is
134
+ // also the node the user expects sootsim to run on.
135
+ const npxInvocation = resolveNpxDaemonInvocation()
136
+ if (npxInvocation) return npxInvocation
137
+ const entry = resolveSootsimEntryScript()
138
+ return { executable: process.execPath, prefixArgs: [entry] }
139
+ }
140
+
141
+ function resolveNpxDaemonInvocation(): SootsimInvocation | null {
142
+ if (!isNpxSootsimInvocation()) return null
143
+
144
+ const npmCli = resolveNpmCliPath()
145
+ if (!npmCli) return null
146
+
147
+ const version = getCliVersion()
148
+ const packageSpec =
149
+ version === '0.0.0'
150
+ ? rnxPublicBrand.packageName
151
+ : `${rnxPublicBrand.packageName}@${version}`
152
+ return {
153
+ executable: process.execPath,
154
+ prefixArgs: [
155
+ npmCli,
156
+ 'exec',
157
+ '--yes',
158
+ '--package',
159
+ packageSpec,
160
+ '--',
161
+ rnxPublicBrand.commandName,
162
+ ],
163
+ }
164
+ }
165
+
166
+ function isNpxSootsimInvocation(): boolean {
167
+ const argv1 = process.argv[1]
168
+ if (process.env.npm_command === 'exec') return true
169
+ if (!argv1) return false
170
+ const real = realPathOrSelf(argv1)
171
+ return real.includes(`${sep}_npx${sep}`)
172
+ }
173
+
174
+ function resolveNpmCliPath(): string | null {
175
+ try {
176
+ const out = execFileSync('which', ['npm'], { encoding: 'utf8' }).trim()
177
+ if (out && existsSync(out)) return realNpmCliPath(out)
178
+ } catch {}
179
+
180
+ const envPath = process.env.npm_execpath
181
+ if (envPath && existsSync(envPath)) return realNpmCliPath(envPath)
182
+ return null
183
+ }
184
+
185
+ function realNpmCliPath(path: string): string {
186
+ const real = realPathOrSelf(path)
187
+ if (basename(real) === 'npx-cli.js') {
188
+ const npmCli = resolve(dirname(real), 'npm-cli.js')
189
+ if (existsSync(npmCli)) return npmCli
190
+ }
191
+ return real
192
+ }
193
+
194
+ function resolveSootsimEntryScript(): string {
195
+ // prefer which(1) → handles the user's $PATH (npm/bun/pnpm global bin dirs).
196
+ // fall back to process.argv[1] for dev checkouts. resolve any symlink so
197
+ // launchd / systemd spawn the real script, not a /node_modules/.bin shim
198
+ // that may itself depend on PATH.
199
+ try {
200
+ const out = execFileSync('which', [rnxPublicBrand.commandName], {
201
+ encoding: 'utf8',
202
+ }).trim()
203
+ if (out && existsSync(out)) return realPathOrSelf(out)
204
+ } catch {}
205
+ const argv1 = process.argv[1]
206
+ if (argv1 && existsSync(argv1)) return realPathOrSelf(argv1)
207
+ throw new Error(`could not locate the ${rnxPublicBrand.commandName} binary`)
208
+ }
209
+
210
+ function realPathOrSelf(p: string): string {
211
+ try {
212
+ return realpathSync(p)
213
+ } catch {
214
+ return p
215
+ }
216
+ }
217
+
218
+ // --- plist / unit generation ---------------------------------------------
219
+
220
+ function renderPlist(invocation: SootsimInvocation, port: number): string {
221
+ mkdirSync(LOG_DIR, { recursive: true })
222
+ const stdout = resolve(LOG_DIR, 'bridge.out.log')
223
+ const stderr = resolve(LOG_DIR, 'bridge.err.log')
224
+ // launchd points at the .app launcher (which exec's the real invocation
225
+ // in place) rather than at bun/node directly, so Login Items reads the
226
+ // bundle's CFBundleDisplayName instead of bun's signer identity.
227
+ const { launcherPath } = ensureDaemonApp(invocation, port, { stdout, stderr })
228
+ const programArgs = ` <string>${escapeXml(launcherPath)}</string>`
229
+ return `<?xml version="1.0" encoding="UTF-8"?>
230
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
231
+ <plist version="1.0">
232
+ <dict>
233
+ <key>Label</key><string>${SERVICE_LABEL_MAC}</string>
234
+ <key>ProgramArguments</key>
235
+ <array>
236
+ ${programArgs}
237
+ </array>
238
+ <key>RunAtLoad</key><true/>
239
+ <key>KeepAlive</key><true/>
240
+ <key>ThrottleInterval</key><integer>${RESPAWN_THROTTLE_SECONDS}</integer>
241
+ <key>ProcessType</key><string>Background</string>
242
+ <key>StandardOutPath</key><string>${stdout}</string>
243
+ <key>StandardErrorPath</key><string>${stderr}</string>
244
+ </dict>
245
+ </plist>
246
+ `
247
+ }
248
+
249
+ function renderSystemdUnit(invocation: SootsimInvocation, port: number): string {
250
+ mkdirSync(LOG_DIR_LINUX, { recursive: true })
251
+ const exec = [invocation.executable, ...invocation.prefixArgs].map(shellQuote).join(' ')
252
+ return `[Unit]
253
+ Description=rnx bridge daemon
254
+ After=default.target
255
+
256
+ [Service]
257
+ Type=simple
258
+ ExecStart=${exec} serve --quiet --port ${port}
259
+ Restart=always
260
+ RestartSec=${RESPAWN_THROTTLE_SECONDS}
261
+
262
+ [Install]
263
+ WantedBy=default.target
264
+ `
265
+ }
266
+
267
+ function escapeXml(value: string): string {
268
+ return value
269
+ .replace(/&/g, '&amp;')
270
+ .replace(/</g, '&lt;')
271
+ .replace(/>/g, '&gt;')
272
+ .replace(/"/g, '&quot;')
273
+ .replace(/'/g, '&apos;')
274
+ }
275
+
276
+ function shellQuote(value: string): string {
277
+ return /[^\w@%+=:,./-]/.test(value) ? `'${value.replace(/'/g, `'\\''`)}'` : value
278
+ }
279
+
280
+ // --- platform paths ------------------------------------------------------
281
+
282
+ function macPlistPath(): string {
283
+ return resolve(homedir(), 'Library/LaunchAgents', `${SERVICE_LABEL_MAC}.plist`)
284
+ }
285
+
286
+ function macLegacyPlistPath(userHome = homedir()): string {
287
+ return resolve(userHome, 'Library/LaunchAgents', `${LEGACY_SERVICE_LABEL_MAC}.plist`)
288
+ }
289
+
290
+ function linuxUnitPath(): string {
291
+ return resolve(homedir(), '.config/systemd/user', `${SERVICE_NAME_LINUX}.service`)
292
+ }
293
+
294
+ // --- subcommands ---------------------------------------------------------
295
+
296
+ export interface DaemonServiceStatus {
297
+ supported: boolean
298
+ installed: boolean
299
+ path?: string
300
+ }
301
+
302
+ export function getDaemonServiceStatus(): DaemonServiceStatus {
303
+ if (shouldSkipPersistentDaemon()) return { supported: false, installed: false }
304
+ if (process.platform === 'darwin') {
305
+ const path = macPlistPath()
306
+ return { supported: true, installed: existsSync(path), path }
307
+ }
308
+ if (process.platform === 'linux') {
309
+ const path = linuxUnitPath()
310
+ return { supported: true, installed: existsSync(path), path }
311
+ }
312
+ return { supported: false, installed: false }
313
+ }
314
+
315
+ export async function daemonInstall({ port, force }: { port: number; force: boolean }) {
316
+ if (shouldSkipPersistentDaemon()) {
317
+ throw new Error('background daemon setup is unavailable in this environment')
318
+ }
319
+
320
+ const invocation = resolveSootsimInvocation()
321
+ console.log(` binary: ${[invocation.executable, ...invocation.prefixArgs].join(' ')}`)
322
+ console.log(` port: ${port}`)
323
+
324
+ if (process.platform === 'darwin') {
325
+ const legacy = teardownLegacyMacDaemonArtifacts()
326
+ if (!legacy.missing) {
327
+ console.log(` removed legacy ${LEGACY_SERVICE_LABEL_MAC} service`)
328
+ }
329
+ const path = macPlistPath()
330
+ if (existsSync(path) && !force) {
331
+ console.log(` already installed at ${path}`)
332
+ console.log(` pass --force to overwrite, or use 'rnxsim daemon restart' to reload`)
333
+ return
334
+ }
335
+ mkdirSync(dirname(path), { recursive: true })
336
+ writeFileSync(path, renderPlist(invocation, port))
337
+ console.log(` wrote ${path}`)
338
+
339
+ // bootstrap into the gui session. launchctl is famous for returning exit
340
+ // 0 from `bootstrap` while leaving the service unregistered (a stale
341
+ // bootout reference, an in-flight unload, etc.) — kickstart then fails
342
+ // with a confusing "service not found" 113. so: bootstrap, immediately
343
+ // verify with `launchctl print`, and if the verify fails do a clean
344
+ // bootout-then-bootstrap retry before giving up. we surface real stderr
345
+ // text from any of those steps so users see what launchd actually said.
346
+ const target = `gui/${process.getuid!()}`
347
+ const serviceTarget = `${target}/${SERVICE_LABEL_MAC}`
348
+ const tryBootstrap = (): { ok: true } | { ok: false; reason: string } => {
349
+ const bs = spawnSync('launchctl', ['bootstrap', target, path], {
350
+ stdio: 'pipe',
351
+ encoding: 'utf8',
352
+ })
353
+ const bsErr = (bs.stderr || bs.stdout || '').trim()
354
+ if (bs.status !== 0) {
355
+ return {
356
+ ok: false,
357
+ reason: `bootstrap exit ${bs.status}${bsErr ? `: ${bsErr}` : ''}`,
358
+ }
359
+ }
360
+ const verify = spawnSync('launchctl', ['print', serviceTarget], {
361
+ encoding: 'utf8',
362
+ })
363
+ if (verify.status !== 0) {
364
+ return {
365
+ ok: false,
366
+ reason: `bootstrap returned 0 but service not registered (print exit ${verify.status})`,
367
+ }
368
+ }
369
+ return { ok: true }
370
+ }
371
+
372
+ let attempt = tryBootstrap()
373
+ if (!attempt.ok) {
374
+ const first = attempt.reason
375
+ // clean reload — bootout any stale reference then bootstrap again.
376
+ spawnSync('launchctl', ['bootout', serviceTarget], { stdio: 'ignore' })
377
+ attempt = tryBootstrap()
378
+ if (!attempt.ok) {
379
+ throw new Error(
380
+ `launchctl bootstrap failed: ${attempt.reason}` +
381
+ (first !== attempt.reason ? ` (initial: ${first})` : '') +
382
+ ` — try \`rnxsim daemon uninstall\` then retry, or reboot if launchd state is stuck.`,
383
+ )
384
+ }
385
+ }
386
+
387
+ // RunAtLoad=true on the plist means bootstrap already started the
388
+ // agent. an explicit `launchctl kickstart` is redundant here and on
389
+ // some machines fails with 113 ("service not found") when the agent
390
+ // crashes immediately and lands in launchd's throttle state — even
391
+ // though the service IS registered. instead, poll `launchctl print`
392
+ // for a few seconds until we see the service is running, then we're
393
+ // done; if it never reaches running, surface the err log path.
394
+ const deadline = Date.now() + 4000
395
+ let lastState: string | undefined
396
+ let lastPid: string | undefined
397
+ while (Date.now() < deadline) {
398
+ const p = spawnSync('launchctl', ['print', serviceTarget], { encoding: 'utf8' })
399
+ if (p.status === 0) {
400
+ lastState = p.stdout?.match(/state\s*=\s*(\w+)/)?.[1]
401
+ lastPid = p.stdout?.match(/pid\s*=\s*(\d+)/)?.[1]
402
+ if (lastState === 'running' || lastPid) break
403
+ }
404
+ await new Promise((r) => setTimeout(r, 150))
405
+ }
406
+ if (lastState !== 'running' && !lastPid) {
407
+ throw new Error(
408
+ `daemon registered but did not reach running state within 4s ` +
409
+ `(last state: ${lastState || 'unknown'}). check ${resolve(LOG_DIR, 'bridge.err.log')} ` +
410
+ `— typically a stale runtime or a port already bound by another process.`,
411
+ )
412
+ }
413
+ console.log(
414
+ ` registered (state: ${lastState || 'unknown'}${lastPid ? `, pid ${lastPid}` : ''}, log: ${resolve(LOG_DIR, 'bridge.err.log')})`,
415
+ )
416
+ return
417
+ }
418
+
419
+ if (process.platform === 'linux') {
420
+ const path = linuxUnitPath()
421
+ if (existsSync(path) && !force) {
422
+ console.log(` already installed at ${path}`)
423
+ console.log(` pass --force to overwrite, or use 'rnxsim daemon restart' to reload`)
424
+ return
425
+ }
426
+ mkdirSync(dirname(path), { recursive: true })
427
+ writeFileSync(path, renderSystemdUnit(invocation, port))
428
+ console.log(` wrote ${path}`)
429
+
430
+ const reload = spawnSync('systemctl', ['--user', 'daemon-reload'], {
431
+ stdio: 'pipe',
432
+ encoding: 'utf8',
433
+ })
434
+ if (reload.status !== 0) {
435
+ const msg = (reload.stderr || reload.stdout || '').trim()
436
+ throw new Error(`systemctl daemon-reload failed${msg ? `: ${msg}` : ''}`)
437
+ }
438
+ const enable = spawnSync(
439
+ 'systemctl',
440
+ ['--user', 'enable', '--now', SERVICE_NAME_LINUX],
441
+ { stdio: 'pipe', encoding: 'utf8' },
442
+ )
443
+ if (enable.status !== 0) {
444
+ const msg = (enable.stderr || enable.stdout || '').trim()
445
+ throw new Error(
446
+ `systemctl enable --now failed${msg ? `: ${msg}` : ''}` +
447
+ ` — try \`rnxsim daemon uninstall\` then retry.`,
448
+ )
449
+ }
450
+ const isActive = spawnSync('systemctl', ['--user', 'is-active', SERVICE_NAME_LINUX], {
451
+ encoding: 'utf8',
452
+ })
453
+ const state = (isActive.stdout || '').trim() || 'unknown'
454
+ console.log(
455
+ ` registered (state: ${state}, journalctl --user -u ${SERVICE_NAME_LINUX} to tail logs)`,
456
+ )
457
+
458
+ // user-scope systemd units are killed when the user's last session ends.
459
+ // on most distros the daemon then disappears at logout — the unit is
460
+ // "active" until you log off, then silently dead, and `sootsim` looks
461
+ // broken when you ssh back in. `loginctl enable-linger` keeps the user
462
+ // bus alive across sessions, which is what every other long-running
463
+ // user agent (gpg-agent, ssh-agent --systemd) also relies on.
464
+ //
465
+ // orb's default user is already lingered (orb seeds it); multipass and
466
+ // lima fresh users are not. probing `loginctl show-user --property=Linger`
467
+ // tells us whether to nudge the user — running enable-linger ourselves
468
+ // requires root (`loginctl enable-linger` invokes a polkit action that
469
+ // typically needs sudo from a non-root user), so we just print the
470
+ // one-liner instead of trying to escalate ourselves.
471
+ try {
472
+ const username = process.env.USER || process.env.LOGNAME || ''
473
+ if (username) {
474
+ const linger = spawnSync(
475
+ 'loginctl',
476
+ ['show-user', username, '--property=Linger', '--value'],
477
+ { encoding: 'utf8' },
478
+ )
479
+ const lingerEnabled = (linger.stdout || '').trim().toLowerCase() === 'yes'
480
+ if (!lingerEnabled && linger.status === 0) {
481
+ console.log(
482
+ ` note: linger is not enabled for "${username}". the daemon will exit at logout.\n` +
483
+ ` enable persistence with: sudo loginctl enable-linger ${username}`,
484
+ )
485
+ }
486
+ }
487
+ } catch {
488
+ // loginctl missing or non-systemd init — silent; the daemon still works
489
+ // for the current session even without lingering.
490
+ }
491
+ return
492
+ }
493
+
494
+ console.error(` background daemon is not supported on ${process.platform}`)
495
+ console.error(` run 'rnxsim serve' in a persistent shell instead`)
496
+ process.exit(1)
497
+ }
498
+
499
+ // removed-path log (idempotent helpers track what they actually touched so the
500
+ // caller can report a unified "not installed" / "removed N entries" line).
501
+ type TeardownReport = { removed: string[]; missing: boolean }
502
+
503
+ export function teardownLegacyMacDaemonArtifacts(
504
+ options: {
505
+ userHome?: string
506
+ launchTarget?: string
507
+ bootout?: (target: string) => void
508
+ } = {},
509
+ ): TeardownReport {
510
+ const userHome = options.userHome ?? homedir()
511
+ const plist = macLegacyPlistPath(userHome)
512
+ const logDir = resolve(userHome, 'Library/Logs/sootsim')
513
+ const removed: string[] = []
514
+ if (existsSync(plist)) {
515
+ const target = `${
516
+ options.launchTarget ?? `gui/${process.getuid?.() ?? 0}`
517
+ }/${LEGACY_SERVICE_LABEL_MAC}`
518
+ const bootout =
519
+ options.bootout ??
520
+ ((serviceTarget: string) => {
521
+ spawnSync('launchctl', ['bootout', serviceTarget], { stdio: 'ignore' })
522
+ })
523
+ bootout(target)
524
+ rmSync(plist, { force: true })
525
+ removed.push(plist)
526
+ }
527
+ for (const name of ['server.err.log', 'server.out.log']) {
528
+ const log = resolve(logDir, name)
529
+ if (!existsSync(log)) continue
530
+ rmSync(log, { force: true })
531
+ removed.push(log)
532
+ }
533
+ return { removed, missing: removed.length === 0 }
534
+ }
535
+
536
+ /** stop + disable + remove the persistent launchd / systemd registration
537
+ * and its support files (mac .app bundle, log dir). does NOT touch user
538
+ * data under ~/.sootsim. idempotent: each entry is checked before remove.
539
+ * exported for reuse by the npm preuninstall hook (scripts/preuninstall.cjs)
540
+ * via the bundled CLI so a `npm rm -g sootsim` tears down the daemon before
541
+ * the bin disappears. */
542
+ export function teardownDaemonService(): TeardownReport {
543
+ const removed: string[] = []
544
+ if (process.platform === 'darwin') {
545
+ const legacy = teardownLegacyMacDaemonArtifacts()
546
+ removed.push(...legacy.removed)
547
+ const plist = macPlistPath()
548
+ if (existsSync(plist)) {
549
+ spawnSync(
550
+ 'launchctl',
551
+ ['bootout', `gui/${process.getuid!()}/${SERVICE_LABEL_MAC}`],
552
+ { stdio: 'ignore' },
553
+ )
554
+ rmSync(plist, { force: true })
555
+ removed.push(plist)
556
+ }
557
+ const bundle = daemonAppBundlePath()
558
+ if (existsSync(bundle)) {
559
+ rmSync(bundle, { recursive: true, force: true })
560
+ removed.push(bundle)
561
+ }
562
+ if (existsSync(LOG_DIR)) {
563
+ rmSync(LOG_DIR, { recursive: true, force: true })
564
+ removed.push(LOG_DIR)
565
+ }
566
+ return { removed, missing: removed.length === 0 }
567
+ }
568
+ if (process.platform === 'linux') {
569
+ const unit = linuxUnitPath()
570
+ if (existsSync(unit)) {
571
+ // `disable --now` stops + disables in one call. ignore exit status: a
572
+ // unit pointing at a now-stale ExecStart (the npm preuninstall path)
573
+ // can return non-zero while still successfully disabling the unit.
574
+ spawnSync('systemctl', ['--user', 'disable', '--now', SERVICE_NAME_LINUX], {
575
+ stdio: 'ignore',
576
+ })
577
+ rmSync(unit, { force: true })
578
+ spawnSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore' })
579
+ // reset-failed clears the "failed" state that lingers in systemd when
580
+ // the unit exited with a missing ExecStart, otherwise `systemctl --user
581
+ // status sootsim-daemon` keeps showing failed even after the unit file
582
+ // is gone.
583
+ spawnSync('systemctl', ['--user', 'reset-failed', SERVICE_NAME_LINUX], {
584
+ stdio: 'ignore',
585
+ })
586
+ removed.push(unit)
587
+ }
588
+ if (existsSync(LOG_DIR_LINUX)) {
589
+ rmSync(LOG_DIR_LINUX, { recursive: true, force: true })
590
+ removed.push(LOG_DIR_LINUX)
591
+ }
592
+ return { removed, missing: removed.length === 0 }
593
+ }
594
+ // win + others: no persistent daemon mechanism yet; nothing to tear down.
595
+ return { removed, missing: true }
596
+ }
597
+
598
+ /** remove the user-data cache under ~/.sootsim (runtimes, electron support,
599
+ * profiles, lockfile, config). idempotent. honors $SOOTSIM_HOME. exported
600
+ * for reuse by the npm preuninstall hook. */
601
+ export function teardownSootsimHome(): TeardownReport {
602
+ const removed: string[] = []
603
+ const home = sootsimHomeDir()
604
+ if (existsSync(home)) {
605
+ rmSync(home, { recursive: true, force: true })
606
+ removed.push(home)
607
+ }
608
+ return { removed, missing: removed.length === 0 }
609
+ }
610
+
611
+ async function daemonUninstall() {
612
+ const service = teardownDaemonService()
613
+ for (const path of service.removed) console.log(` removed ${path}`)
614
+ const home = teardownSootsimHome()
615
+ for (const path of home.removed) console.log(` removed ${path}`)
616
+ if (service.missing && home.missing) console.log(' not installed')
617
+ }
618
+
619
+ // the service manager's own word is not proof the bridge works: a daemon that
620
+ // crashes on start reads as `active` all through systemd's restart backoff,
621
+ // which is how a fully broken daemon reported healthy on the 2026-08-19 drive.
622
+ // the lockfile carries a pid and a heartbeat, so it answers the question the
623
+ // caller actually asked.
624
+ function printBridgeLiveness() {
625
+ const lock = readDaemonLockfile()
626
+ if (isDaemonLockfileFresh(lock)) {
627
+ console.log(` bridge: responding on port ${lock.bridgePort} (pid ${lock.pid})`)
628
+ return
629
+ }
630
+ console.log(
631
+ lock
632
+ ? ' bridge: NOT responding — stale lockfile; check `rnxsim daemon logs`'
633
+ : ' bridge: NOT responding — no lockfile; check `rnxsim daemon logs`',
634
+ )
635
+ }
636
+
637
+ async function daemonStatus() {
638
+ if (process.platform === 'darwin') {
639
+ const path = macPlistPath()
640
+ if (!existsSync(path)) {
641
+ console.log(` installed: no`)
642
+ return
643
+ }
644
+ console.log(` installed: yes (${path})`)
645
+ const binary = readPlistBinary(path)
646
+ if (binary) console.log(` binary: ${binary}`)
647
+ const res = spawnSync(
648
+ 'launchctl',
649
+ ['print', `gui/${process.getuid!()}/${SERVICE_LABEL_MAC}`],
650
+ { encoding: 'utf8' },
651
+ )
652
+ if (res.status === 0) {
653
+ const state = res.stdout.match(/state\s*=\s*(\w+)/)?.[1]
654
+ const pid = res.stdout.match(/pid\s*=\s*(\d+)/)?.[1]
655
+ console.log(` state: ${state || 'unknown'}${pid ? ` (pid ${pid})` : ''}`)
656
+ } else {
657
+ console.log(' state: not running')
658
+ }
659
+ printBridgeLiveness()
660
+ return
661
+ }
662
+ if (process.platform === 'linux') {
663
+ const path = linuxUnitPath()
664
+ if (!existsSync(path)) {
665
+ console.log(` installed: no`)
666
+ return
667
+ }
668
+ console.log(` installed: yes (${path})`)
669
+ const res = spawnSync('systemctl', ['--user', 'is-active', SERVICE_NAME_LINUX], {
670
+ encoding: 'utf8',
671
+ })
672
+ console.log(` state: ${res.stdout.trim() || 'unknown'}`)
673
+ printBridgeLiveness()
674
+ return
675
+ }
676
+ console.error(` unsupported platform: ${process.platform}`)
677
+ process.exit(1)
678
+ }
679
+
680
+ async function daemonRestart() {
681
+ if (process.platform === 'darwin') {
682
+ const path = macPlistPath()
683
+ if (!existsSync(path)) {
684
+ console.error(' daemon not registered — run `rnxsim setup`')
685
+ process.exit(1)
686
+ }
687
+ const res = spawnSync(
688
+ 'launchctl',
689
+ ['kickstart', '-k', `gui/${process.getuid!()}/${SERVICE_LABEL_MAC}`],
690
+ { stdio: 'pipe', encoding: 'utf8' },
691
+ )
692
+ if (res.status !== 0) {
693
+ console.error(` kickstart failed: ${res.stderr?.trim()}`)
694
+ process.exit(1)
695
+ }
696
+ console.log(' restarted')
697
+ return
698
+ }
699
+ if (process.platform === 'linux') {
700
+ const path = linuxUnitPath()
701
+ if (!existsSync(path)) {
702
+ console.error(' daemon not registered — run `rnxsim setup`')
703
+ process.exit(1)
704
+ }
705
+ const res = spawnSync('systemctl', ['--user', 'restart', SERVICE_NAME_LINUX], {
706
+ stdio: 'pipe',
707
+ encoding: 'utf8',
708
+ })
709
+ if (res.status !== 0) {
710
+ console.error(` restart failed: ${res.stderr?.trim()}`)
711
+ process.exit(1)
712
+ }
713
+ console.log(' restarted')
714
+ return
715
+ }
716
+ console.error(` unsupported platform: ${process.platform}`)
717
+ process.exit(1)
718
+ }
719
+
720
+ async function daemonStart() {
721
+ if (process.platform === 'darwin') {
722
+ const path = macPlistPath()
723
+ if (!existsSync(path)) {
724
+ console.error(' daemon not registered — run `rnxsim setup`')
725
+ process.exit(1)
726
+ }
727
+ const res = spawnSync(
728
+ 'launchctl',
729
+ ['kickstart', `gui/${process.getuid!()}/${SERVICE_LABEL_MAC}`],
730
+ { stdio: 'pipe', encoding: 'utf8' },
731
+ )
732
+ if (res.status !== 0) {
733
+ console.error(` start failed: ${res.stderr?.trim()}`)
734
+ process.exit(1)
735
+ }
736
+ console.log(' started')
737
+ return
738
+ }
739
+ if (process.platform === 'linux') {
740
+ const path = linuxUnitPath()
741
+ if (!existsSync(path)) {
742
+ console.error(' daemon not registered — run `rnxsim setup`')
743
+ process.exit(1)
744
+ }
745
+ const res = spawnSync('systemctl', ['--user', 'start', SERVICE_NAME_LINUX], {
746
+ stdio: 'pipe',
747
+ encoding: 'utf8',
748
+ })
749
+ if (res.status !== 0) {
750
+ console.error(` start failed: ${res.stderr?.trim()}`)
751
+ process.exit(1)
752
+ }
753
+ console.log(' started')
754
+ return
755
+ }
756
+ console.error(` unsupported platform: ${process.platform}`)
757
+ process.exit(1)
758
+ }
759
+
760
+ async function daemonStop() {
761
+ if (process.platform === 'darwin') {
762
+ const path = macPlistPath()
763
+ if (!existsSync(path)) {
764
+ console.error(' daemon not registered — nothing to stop')
765
+ process.exit(1)
766
+ }
767
+ const res = spawnSync(
768
+ 'launchctl',
769
+ ['bootout', `gui/${process.getuid!()}/${SERVICE_LABEL_MAC}`],
770
+ { stdio: 'pipe', encoding: 'utf8' },
771
+ )
772
+ if (res.status !== 0) {
773
+ console.error(` stop failed: ${res.stderr?.trim()}`)
774
+ process.exit(1)
775
+ }
776
+ console.log(' stopped')
777
+ return
778
+ }
779
+ if (process.platform === 'linux') {
780
+ const path = linuxUnitPath()
781
+ if (!existsSync(path)) {
782
+ console.error(' daemon not registered — nothing to stop')
783
+ process.exit(1)
784
+ }
785
+ const res = spawnSync('systemctl', ['--user', 'stop', SERVICE_NAME_LINUX], {
786
+ stdio: 'pipe',
787
+ encoding: 'utf8',
788
+ })
789
+ if (res.status !== 0) {
790
+ console.error(` stop failed: ${res.stderr?.trim()}`)
791
+ process.exit(1)
792
+ }
793
+ console.log(' stopped')
794
+ return
795
+ }
796
+ console.error(` unsupported platform: ${process.platform}`)
797
+ process.exit(1)
798
+ }
799
+
800
+ // tail the daemon's logs. macOS reads the launchd-redirected files directly
801
+ // (works for any user); linux reads the systemd journal. journalctl --user
802
+ // fails for a freshly-created user not yet in the systemd-journal/adm group
803
+ // ("No journal files were opened due to insufficient permissions"), so on that
804
+ // error we retry the system-scoped query and, failing that, point the user at
805
+ // the sudo form rather than printing a cryptic permission error.
806
+ async function daemonLogs(rest: string[]) {
807
+ const linesArg = rest.find((a) => /^-n\d+$|^--lines=\d+$|^\d+$/.test(a))
808
+ const lines = linesArg ? Number(linesArg.replace(/\D/g, '')) : 200
809
+ const follow = rest.includes('-f') || rest.includes('--follow')
810
+
811
+ if (process.platform === 'darwin') {
812
+ const out = resolve(LOG_DIR, 'bridge.out.log')
813
+ const err = resolve(LOG_DIR, 'bridge.err.log')
814
+ const present = [out, err].filter((p) => existsSync(p))
815
+ if (present.length === 0) {
816
+ console.error(
817
+ ` no daemon logs yet at ${LOG_DIR} — run \`rnxsim setup\` to install the daemon`,
818
+ )
819
+ process.exit(1)
820
+ }
821
+ const args = follow
822
+ ? ['-n', String(lines), '-F', ...present]
823
+ : ['-n', String(lines), ...present]
824
+ spawnSync('tail', args, { stdio: 'inherit' })
825
+ return
826
+ }
827
+
828
+ if (process.platform === 'linux') {
829
+ const unit = `${SERVICE_NAME_LINUX}.service`
830
+ const base = ['--user', '-u', unit, '-n', String(lines), '--no-pager']
831
+ const res = spawnSync('journalctl', follow ? [...base, '-f'] : base, {
832
+ encoding: 'utf8',
833
+ stdio: follow ? 'inherit' : 'pipe',
834
+ })
835
+ if (follow) return
836
+ const combined = `${res.stdout || ''}${res.stderr || ''}`
837
+ const blocked = /insufficient permissions|No journal files were opened/i.test(
838
+ combined,
839
+ )
840
+ if (res.status === 0 && !blocked) {
841
+ process.stdout.write(res.stdout || '')
842
+ return
843
+ }
844
+ // fresh-user fallback: the system journal holds the user-unit logs even when
845
+ // the user can't read --user scope. try it, then fall back to guidance.
846
+ const sys = spawnSync(
847
+ 'journalctl',
848
+ [`_SYSTEMD_USER_UNIT=${unit}`, '-n', String(lines), '--no-pager'],
849
+ { encoding: 'utf8' },
850
+ )
851
+ if (sys.status === 0 && (sys.stdout || '').trim()) {
852
+ process.stdout.write(sys.stdout)
853
+ return
854
+ }
855
+ console.error(
856
+ ` could not read --user journal for ${unit} (this user isn't in the systemd-journal group yet).\n` +
857
+ ` run: sudo journalctl _SYSTEMD_USER_UNIT=${unit} -n ${lines} --no-pager`,
858
+ )
859
+ process.exit(1)
860
+ }
861
+
862
+ console.error(` unsupported platform: ${process.platform}`)
863
+ process.exit(1)
864
+ }
865
+
866
+ function readPlistBinary(path: string): string | null {
867
+ try {
868
+ const body = readFileSync(path, 'utf8')
869
+ const match = body.match(/<array>\s*<string>([^<]+)<\/string>/)
870
+ return match?.[1] || null
871
+ } catch {
872
+ return null
873
+ }
874
+ }