peachsim 0.1.527

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 (324) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +640 -0
  3. package/cli/app-config.ts +82 -0
  4. package/cli/app-fonts.ts +388 -0
  5. package/cli/app-project.ts +59 -0
  6. package/cli/app-splash.ts +171 -0
  7. package/cli/app-state-reset.ts +26 -0
  8. package/cli/auth.ts +184 -0
  9. package/cli/bin.ts +59 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2806 -0
  12. package/cli/browser-backend.ts +44 -0
  13. package/cli/browser-evals.ts +96 -0
  14. package/cli/cli-update.ts +261 -0
  15. package/cli/cloud-client.ts +1530 -0
  16. package/cli/cloud-dispatch.ts +206 -0
  17. package/cli/cloud-session.ts +227 -0
  18. package/cli/command-contract.ts +221 -0
  19. package/cli/command-registry.ts +304 -0
  20. package/cli/commands/agent-wrapper.ts +1048 -0
  21. package/cli/commands/agent.ts +439 -0
  22. package/cli/commands/app-fonts.ts +99 -0
  23. package/cli/commands/assert.ts +542 -0
  24. package/cli/commands/auth.ts +71 -0
  25. package/cli/commands/box/bundle-plane.ts +71 -0
  26. package/cli/commands/box/checkout-plane.ts +256 -0
  27. package/cli/commands/box/plane-share.ts +208 -0
  28. package/cli/commands/box/rnx-command.ts +60 -0
  29. package/cli/commands/box/search-command.ts +157 -0
  30. package/cli/commands/box.ts +925 -0
  31. package/cli/commands/camera.ts +266 -0
  32. package/cli/commands/cleanup.ts +169 -0
  33. package/cli/commands/compat.ts +176 -0
  34. package/cli/commands/config.ts +34 -0
  35. package/cli/commands/control.ts +2154 -0
  36. package/cli/commands/cpu-profile.ts +941 -0
  37. package/cli/commands/daemon-mac-app.ts +165 -0
  38. package/cli/commands/daemon.ts +800 -0
  39. package/cli/commands/debug.ts +751 -0
  40. package/cli/commands/desktop.ts +39 -0
  41. package/cli/commands/detect.ts +201 -0
  42. package/cli/commands/detox.ts +481 -0
  43. package/cli/commands/device.ts +134 -0
  44. package/cli/commands/diagnose.ts +589 -0
  45. package/cli/commands/do-chain.ts +408 -0
  46. package/cli/commands/electron.ts +94 -0
  47. package/cli/commands/film.ts +379 -0
  48. package/cli/commands/flow.ts +1240 -0
  49. package/cli/commands/inspect/actions.ts +516 -0
  50. package/cli/commands/inspect/core.ts +2086 -0
  51. package/cli/commands/inspect/count.ts +17 -0
  52. package/cli/commands/inspect/describe.ts +119 -0
  53. package/cli/commands/inspect/env.ts +25 -0
  54. package/cli/commands/inspect/find.ts +169 -0
  55. package/cli/commands/inspect/get-layout.ts +53 -0
  56. package/cli/commands/inspect/keyboard.ts +54 -0
  57. package/cli/commands/inspect/list.ts +175 -0
  58. package/cli/commands/inspect/memory.ts +215 -0
  59. package/cli/commands/inspect/redaction.ts +39 -0
  60. package/cli/commands/inspect/resolve-target.ts +77 -0
  61. package/cli/commands/inspect/screens.ts +78 -0
  62. package/cli/commands/inspect/settle.ts +32 -0
  63. package/cli/commands/inspect/settling.ts +197 -0
  64. package/cli/commands/inspect/shared.ts +259 -0
  65. package/cli/commands/inspect/sleep.ts +15 -0
  66. package/cli/commands/inspect/tree.ts +23 -0
  67. package/cli/commands/inspect/url.ts +17 -0
  68. package/cli/commands/inspect/wait-event.ts +211 -0
  69. package/cli/commands/inspect/wait-idle.ts +25 -0
  70. package/cli/commands/inspect/wait-ready.ts +87 -0
  71. package/cli/commands/inspect/wait-selector.ts +59 -0
  72. package/cli/commands/inspect/wait.ts +32 -0
  73. package/cli/commands/inspect.ts +4738 -0
  74. package/cli/commands/install-cli.ts +275 -0
  75. package/cli/commands/install-desktop.ts +385 -0
  76. package/cli/commands/login.ts +357 -0
  77. package/cli/commands/logout.ts +24 -0
  78. package/cli/commands/maestro-generate.ts +375 -0
  79. package/cli/commands/maestro.ts +536 -0
  80. package/cli/commands/mode.ts +58 -0
  81. package/cli/commands/no-bridge-hint.ts +80 -0
  82. package/cli/commands/perf.ts +67 -0
  83. package/cli/commands/permissions.ts +203 -0
  84. package/cli/commands/platform.ts +410 -0
  85. package/cli/commands/preview.ts +103 -0
  86. package/cli/commands/profile.ts +106 -0
  87. package/cli/commands/react.ts +353 -0
  88. package/cli/commands/record.ts +1435 -0
  89. package/cli/commands/report-issue.ts +305 -0
  90. package/cli/commands/reset.ts +89 -0
  91. package/cli/commands/runtime.ts +348 -0
  92. package/cli/commands/screenshot-command.ts +106 -0
  93. package/cli/commands/screenshot-layers.ts +144 -0
  94. package/cli/commands/screenshot-mode.ts +38 -0
  95. package/cli/commands/screenshot.ts +492 -0
  96. package/cli/commands/screenshots-capture.ts +608 -0
  97. package/cli/commands/screenshots.ts +127 -0
  98. package/cli/commands/serve.ts +139 -0
  99. package/cli/commands/setup.ts +416 -0
  100. package/cli/commands/shell-boolean-mode.ts +82 -0
  101. package/cli/commands/skills.ts +468 -0
  102. package/cli/commands/slides.ts +357 -0
  103. package/cli/commands/state.ts +94 -0
  104. package/cli/commands/storage.ts +58 -0
  105. package/cli/commands/telemetry.ts +55 -0
  106. package/cli/commands/test.ts +535 -0
  107. package/cli/commands/three-mode.ts +771 -0
  108. package/cli/commands/timeline.ts +121 -0
  109. package/cli/commands/upgrade.ts +167 -0
  110. package/cli/commands/upload.ts +1203 -0
  111. package/cli/commands/version.ts +63 -0
  112. package/cli/commands/welcome.ts +17 -0
  113. package/cli/commands/what-happened.ts +328 -0
  114. package/cli/current-sim.ts +217 -0
  115. package/cli/desktop-companion.ts +315 -0
  116. package/cli/drivers/electron.ts +70 -0
  117. package/cli/drivers/index.ts +20 -0
  118. package/cli/drivers/playwright-provisioning.ts +184 -0
  119. package/cli/drivers/playwright-sim-host.ts +537 -0
  120. package/cli/drivers/playwright.ts +209 -0
  121. package/cli/drivers/registry.ts +65 -0
  122. package/cli/drivers/types.ts +102 -0
  123. package/cli/flow-export.ts +55 -0
  124. package/cli/flow-file.ts +142 -0
  125. package/cli/flow-live-status.ts +126 -0
  126. package/cli/flow-session.ts +140 -0
  127. package/cli/help-core.ts +143 -0
  128. package/cli/help.ts +44 -0
  129. package/cli/hidden-runtime-alias.ts +19 -0
  130. package/cli/hints.ts +214 -0
  131. package/cli/inspect-notice-state.ts +114 -0
  132. package/cli/internal-child.ts +176 -0
  133. package/cli/maestro-js.ts +323 -0
  134. package/cli/main.ts +704 -0
  135. package/cli/open-url.ts +8 -0
  136. package/cli/outbound-endpoints.ts +830 -0
  137. package/cli/parent-pid.ts +204 -0
  138. package/cli/parse-args.ts +189 -0
  139. package/cli/privacy.ts +53 -0
  140. package/cli/prompt.ts +56 -0
  141. package/cli/recording-access.ts +108 -0
  142. package/cli/registry.ts +2 -0
  143. package/cli/resolve-assets.ts +63 -0
  144. package/cli/rnx-in-process.ts +98 -0
  145. package/cli/run-registry.ts +226 -0
  146. package/cli/run-rnx-core.ts +148 -0
  147. package/cli/run-rnx.ts +137 -0
  148. package/cli/runtime-notes.ts +66 -0
  149. package/cli/runtime-summary.ts +25 -0
  150. package/cli/secure-file.ts +31 -0
  151. package/cli/self-invocation.ts +58 -0
  152. package/cli/send-to-box.ts +109 -0
  153. package/cli/setup-repository.ts +191 -0
  154. package/cli/shell-init.ts +98 -0
  155. package/cli/standalone.ts +4 -0
  156. package/cli/telemetry.ts +84 -0
  157. package/cli/ui.ts +68 -0
  158. package/cli/ws-bridge.ts +1020 -0
  159. package/detox/attempt-events.cjs +23 -0
  160. package/detox/colors.ts +54 -0
  161. package/detox/config-loader.ts +135 -0
  162. package/detox/element-types.ts +48 -0
  163. package/detox/expectations.ts +534 -0
  164. package/detox/gestures.ts +501 -0
  165. package/detox/index.ts +2694 -0
  166. package/detox/jest-preset.cjs +29 -0
  167. package/detox/jest-setup-after-env.cjs +22 -0
  168. package/detox/matchers.ts +30 -0
  169. package/detox/motion-change.cjs +109 -0
  170. package/detox/motion-change.d.cts +35 -0
  171. package/detox/navigation-lifecycle.ts +290 -0
  172. package/detox/navigation.ts +43 -0
  173. package/detox/proof-frame.cjs +114 -0
  174. package/detox/record-block-execution.cjs +78 -0
  175. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  176. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  177. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  178. package/detox/screenshots/theme-blue-toggle.png +0 -0
  179. package/detox/screenshots/theme-blue.png +0 -0
  180. package/detox/screenshots/theme-red-toggle.png +0 -0
  181. package/detox/screenshots/theme-red.png +0 -0
  182. package/dist-lib/agent-daemon-client.cjs +502 -0
  183. package/dist-lib/agent-events.cjs +48 -0
  184. package/dist-lib/agent-identity.cjs +53 -0
  185. package/dist-lib/agent-sessions.cjs +759 -0
  186. package/dist-lib/attached-projects.cjs +526 -0
  187. package/dist-lib/auth/shared-session.cjs +208 -0
  188. package/dist-lib/backend-origin.cjs +88 -0
  189. package/dist-lib/beta.cjs +41 -0
  190. package/dist-lib/beta.mjs +13 -0
  191. package/dist-lib/bridge-constants.cjs +161 -0
  192. package/dist-lib/bridge-contract-input.cjs +451 -0
  193. package/dist-lib/bridge-contract-input.mjs +414 -0
  194. package/dist-lib/bridge-contract.cjs +130 -0
  195. package/dist-lib/bridge-contract.mjs +94 -0
  196. package/dist-lib/capture-contract.cjs +220 -0
  197. package/dist-lib/capture-contract.mjs +190 -0
  198. package/dist-lib/cli-constants.cjs +32 -0
  199. package/dist-lib/cloud-contract.cjs +194 -0
  200. package/dist-lib/cloud-contract.mjs +145 -0
  201. package/dist-lib/cloud.cjs +670 -0
  202. package/dist-lib/cloud.mjs +639 -0
  203. package/dist-lib/config.cjs +96 -0
  204. package/dist-lib/detox/index.cjs +3789 -0
  205. package/dist-lib/detox/jest-preset.cjs +29 -0
  206. package/dist-lib/dev-bundle-resolution.cjs +290 -0
  207. package/dist-lib/home-paths.cjs +638 -0
  208. package/dist-lib/host/bridge-host.cjs +10903 -0
  209. package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
  210. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  211. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  212. package/dist-lib/host/replacement-module-handler.cjs +469 -0
  213. package/dist-lib/host/websocket-proxy.cjs +298 -0
  214. package/dist-lib/index.cjs +4268 -0
  215. package/dist-lib/jump-to-source-babel.cjs +93 -0
  216. package/dist-lib/jump-to-source-native.cjs +112 -0
  217. package/dist-lib/menu.cjs +646 -0
  218. package/dist-lib/menu.mjs +604 -0
  219. package/dist-lib/metro-fingerprint-registry.cjs +186 -0
  220. package/dist-lib/metro-fingerprint-registry.mjs +156 -0
  221. package/dist-lib/metro-production-bundle.cjs +329 -0
  222. package/dist-lib/metro-production-bundle.mjs +295 -0
  223. package/dist-lib/metro.cjs +904 -0
  224. package/dist-lib/profiles.cjs +246 -0
  225. package/dist-lib/public-brand.cjs +39 -0
  226. package/dist-lib/react-native-host-modules.cjs +51 -0
  227. package/dist-lib/react-native-host-modules.mjs +25 -0
  228. package/dist-lib/render-mode.cjs +54 -0
  229. package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
  230. package/dist-lib/sdk.cjs +2433 -0
  231. package/dist-lib/sdk.mjs +2336 -0
  232. package/dist-lib/skills.cjs +39132 -0
  233. package/dist-lib/swift-submit.cjs +92 -0
  234. package/dist-lib/swift-submit.mjs +65 -0
  235. package/dist-lib/swift.cjs +1646 -0
  236. package/dist-lib/vite.cjs +1591 -0
  237. package/package.json +290 -0
  238. package/scripts/dev-server-scanner.ts +1291 -0
  239. package/scripts/optional-demo-registry.ts +113 -0
  240. package/skills/contrast/SKILL.md +165 -0
  241. package/skills/rnx-debug/SKILL.md +433 -0
  242. package/skills/rnx-perf/SKILL.md +399 -0
  243. package/skills/rnx-setup/SKILL.md +172 -0
  244. package/skills/rnx-test/SKILL.md +321 -0
  245. package/skills/rnx-visual/SKILL.md +126 -0
  246. package/src/agent-daemon-client.ts +410 -0
  247. package/src/agent-events.ts +71 -0
  248. package/src/agent-identity.ts +23 -0
  249. package/src/agent-prompt.ts +71 -0
  250. package/src/agent-sessions.ts +580 -0
  251. package/src/app-url.ts +39 -0
  252. package/src/attached-projects.ts +539 -0
  253. package/src/auth/shared-session.ts +235 -0
  254. package/src/backend-origin.ts +70 -0
  255. package/src/beta.ts +19 -0
  256. package/src/bridge-constants.ts +175 -0
  257. package/src/bridge-contract-input.ts +502 -0
  258. package/src/bridge-contract.ts +558 -0
  259. package/src/browser-cache.ts +16 -0
  260. package/src/capture-contract.ts +587 -0
  261. package/src/cli-constants.ts +1 -0
  262. package/src/cli-version.ts +40 -0
  263. package/src/cloud-contract.ts +274 -0
  264. package/src/cloud.ts +762 -0
  265. package/src/codex-client.ts +234 -0
  266. package/src/config.ts +132 -0
  267. package/src/connect.ts +101 -0
  268. package/src/dev-bundle-resolution.ts +346 -0
  269. package/src/dev-server-open.ts +198 -0
  270. package/src/disk-cleanup.ts +540 -0
  271. package/src/flow-timeout-scale.ts +15 -0
  272. package/src/home-paths.ts +806 -0
  273. package/src/host/agent-host.ts +666 -0
  274. package/src/host/bridge-host.ts +2788 -0
  275. package/src/host/fetch-proxy-handler.ts +449 -0
  276. package/src/host/fetch-proxy-overrides.ts +51 -0
  277. package/src/host/open-url.ts +89 -0
  278. package/src/host/plane-host.ts +203 -0
  279. package/src/host/replacement-module-handler.ts +158 -0
  280. package/src/host/websocket-proxy.ts +315 -0
  281. package/src/index.ts +71 -0
  282. package/src/jump-to-source-babel.ts +135 -0
  283. package/src/jump-to-source-native.ts +102 -0
  284. package/src/menu.ts +777 -0
  285. package/src/metro-fingerprint-registry.ts +302 -0
  286. package/src/metro-plugin.ts +341 -0
  287. package/src/metro-production-bundle.ts +471 -0
  288. package/src/native-dev-bundle-url.ts +118 -0
  289. package/src/native-seam-manifest.ts +341 -0
  290. package/src/playwright-host-log.ts +16 -0
  291. package/src/plist.ts +27 -0
  292. package/src/poll-until.ts +123 -0
  293. package/src/profiles.ts +179 -0
  294. package/src/public-brand.ts +9 -0
  295. package/src/react-native-host-modules.ts +26 -0
  296. package/src/render-mode.ts +27 -0
  297. package/src/rolldown-plugin-swift.ts +240 -0
  298. package/src/runtime-assets.ts +233 -0
  299. package/src/runtime-delivery.ts +54 -0
  300. package/src/screenshots/compose.ts +422 -0
  301. package/src/screenshots/frame-compose.ts +514 -0
  302. package/src/screenshots/orchestrate.ts +256 -0
  303. package/src/screenshots/org-deck.ts +91 -0
  304. package/src/screenshots/registry.ts +58 -0
  305. package/src/screenshots/schema.ts +364 -0
  306. package/src/sdk.ts +15 -0
  307. package/src/settings-bundle.ts +418 -0
  308. package/src/shell-bridge-identity.ts +15 -0
  309. package/src/sim-client.ts +697 -0
  310. package/src/skills/builtin/a11y-review.ts +132 -0
  311. package/src/skills/builtin/compat-check.ts +71 -0
  312. package/src/skills/builtin/maestro-test.ts +121 -0
  313. package/src/skills/builtin/perf-profile.ts +90 -0
  314. package/src/skills/builtin/screenshot-all.ts +52 -0
  315. package/src/skills/builtin/visual-diff.ts +100 -0
  316. package/src/skills/registry.ts +107 -0
  317. package/src/skills/types.ts +41 -0
  318. package/src/swift-submit.ts +185 -0
  319. package/src/swift.ts +7 -0
  320. package/src/team-machine-prompt.ts +73 -0
  321. package/src/vite-plugin-one.ts +264 -0
  322. package/src/vite-plugin-swift.ts +620 -0
  323. package/src/vite-plugin.ts +1946 -0
  324. package/src/worklets-babel.ts +250 -0
@@ -0,0 +1,800 @@
1
+ // rnx daemon — manage the persistent rnx bridge daemon
2
+ //
3
+ // wraps `rnx serve` so it starts automatically on login and restarts if
4
+ // it crashes. the agent points at the executable that ran `daemon install`,
5
+ // so `rnx upgrade` (which replaces that file in place) is picked up by
6
+ // `rnx daemon restart` without touching the plist/unit file.
7
+ //
8
+ // `daemonInstall` is shared by `rnx setup` and the explicit
9
+ // `rnx 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 `rnx serve` in a persistent shell.
14
+ //
15
+ // the macOS launchd Label is `dev.sootsim.daemon` (matching the .app
16
+ // CFBundleIdentifier).
17
+
18
+ import { 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 { dirname, resolve } from 'path'
29
+ import { DEFAULT_SOOTSIM_BRIDGE_PORT } from '../../src/bridge-constants'
30
+ import {
31
+ daemonAppDir,
32
+ isDaemonLockfileFresh,
33
+ readDaemonLockfile,
34
+ shouldSkipPersistentDaemon,
35
+ rnxHomeDir,
36
+ } from '../../src/home-paths'
37
+ import { rnxPublicBrand } from '../../src/public-brand'
38
+ import { printCommandHelp } from '../help'
39
+ import { rnxExit } from '../run-rnx'
40
+ import { rnxSelfInvocation, type RnxSelfInvocation } from '../self-invocation'
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
+ switch (sub) {
69
+ case 'install':
70
+ return daemonInstall({ port, force: rest.includes('--force') })
71
+ case 'uninstall':
72
+ return daemonUninstall(rest)
73
+ case 'status':
74
+ return daemonStatus()
75
+ case 'restart':
76
+ return daemonRestart()
77
+ case 'start':
78
+ return daemonStart()
79
+ case 'stop':
80
+ return daemonStop()
81
+ case 'logs':
82
+ return daemonLogs(rest)
83
+ default:
84
+ console.error(` unknown daemon subcommand: ${sub}`)
85
+ printHelp()
86
+ rnxExit(1)
87
+ }
88
+ }
89
+
90
+ function printHelp() {
91
+ printCommandHelp('daemon')
92
+ }
93
+
94
+ // --- resolve sootsim binary ----------------------------------------------
95
+
96
+ // launchd and systemd run agents with a stripped PATH (basically just
97
+ // /usr/bin:/bin:/usr/sbin:/sbin), so anything that depends on PATH resolution
98
+ // never starts. rnxSelfInvocation() answers with absolute paths for both CLI
99
+ // shapes; resolve symlinks on the entry script so the service spawns the real
100
+ // file rather than a bin shim that would itself need PATH.
101
+ function resolveDaemonInvocation(): RnxSelfInvocation {
102
+ const invocation = rnxSelfInvocation()
103
+ return {
104
+ executable: invocation.executable,
105
+ prefixArgs: invocation.prefixArgs.map(realPathOrSelf),
106
+ }
107
+ }
108
+
109
+ function realPathOrSelf(p: string): string {
110
+ try {
111
+ return realpathSync(p)
112
+ } catch {
113
+ return p
114
+ }
115
+ }
116
+
117
+ // --- plist / unit generation ---------------------------------------------
118
+
119
+ function renderPlist(invocation: RnxSelfInvocation, port: number): string {
120
+ mkdirSync(LOG_DIR, { recursive: true })
121
+ const stdout = resolve(LOG_DIR, 'bridge.out.log')
122
+ const stderr = resolve(LOG_DIR, 'bridge.err.log')
123
+ // launchd points at the .app launcher (which exec's the real invocation
124
+ // in place) rather than at bun/node directly, so Login Items reads the
125
+ // bundle's CFBundleDisplayName instead of bun's signer identity.
126
+ const { launcherPath } = ensureDaemonApp(invocation, port, { stdout, stderr })
127
+ const programArgs = ` <string>${escapeXml(launcherPath)}</string>`
128
+ return `<?xml version="1.0" encoding="UTF-8"?>
129
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
130
+ <plist version="1.0">
131
+ <dict>
132
+ <key>Label</key><string>${SERVICE_LABEL_MAC}</string>
133
+ <key>ProgramArguments</key>
134
+ <array>
135
+ ${programArgs}
136
+ </array>
137
+ <key>RunAtLoad</key><true/>
138
+ <key>KeepAlive</key><true/>
139
+ <key>ThrottleInterval</key><integer>${RESPAWN_THROTTLE_SECONDS}</integer>
140
+ <key>ProcessType</key><string>Background</string>
141
+ <key>StandardOutPath</key><string>${stdout}</string>
142
+ <key>StandardErrorPath</key><string>${stderr}</string>
143
+ </dict>
144
+ </plist>
145
+ `
146
+ }
147
+
148
+ function renderSystemdUnit(invocation: RnxSelfInvocation, port: number): string {
149
+ mkdirSync(LOG_DIR_LINUX, { recursive: true })
150
+ const exec = [invocation.executable, ...invocation.prefixArgs].map(shellQuote).join(' ')
151
+ const rnxHome = process.env.RNX_HOME
152
+ const environment = rnxHome ? `Environment=${shellQuote(`RNX_HOME=${rnxHome}`)}\n` : ''
153
+ return `[Unit]
154
+ Description=rnx bridge daemon
155
+ After=default.target
156
+
157
+ [Service]
158
+ Type=simple
159
+ ${environment}ExecStart=${exec} serve --quiet --port ${port}
160
+ Restart=always
161
+ RestartSec=${RESPAWN_THROTTLE_SECONDS}
162
+
163
+ [Install]
164
+ WantedBy=default.target
165
+ `
166
+ }
167
+
168
+ function escapeXml(value: string): string {
169
+ return value
170
+ .replace(/&/g, '&amp;')
171
+ .replace(/</g, '&lt;')
172
+ .replace(/>/g, '&gt;')
173
+ .replace(/"/g, '&quot;')
174
+ .replace(/'/g, '&apos;')
175
+ }
176
+
177
+ function shellQuote(value: string): string {
178
+ return /[^\w@%+=:,./-]/.test(value) ? `'${value.replace(/'/g, `'\\''`)}'` : value
179
+ }
180
+
181
+ // --- platform paths ------------------------------------------------------
182
+
183
+ function macPlistPath(): string {
184
+ return resolve(homedir(), 'Library/LaunchAgents', `${SERVICE_LABEL_MAC}.plist`)
185
+ }
186
+
187
+ function macLegacyPlistPath(userHome = homedir()): string {
188
+ return resolve(userHome, 'Library/LaunchAgents', `${LEGACY_SERVICE_LABEL_MAC}.plist`)
189
+ }
190
+
191
+ function linuxUnitPath(): string {
192
+ return resolve(homedir(), '.config/systemd/user', `${SERVICE_NAME_LINUX}.service`)
193
+ }
194
+
195
+ // --- subcommands ---------------------------------------------------------
196
+
197
+ export interface DaemonServiceStatus {
198
+ supported: boolean
199
+ installed: boolean
200
+ path?: string
201
+ }
202
+
203
+ export function getDaemonServiceStatus(): DaemonServiceStatus {
204
+ if (shouldSkipPersistentDaemon()) return { supported: false, installed: false }
205
+ if (process.platform === 'darwin') {
206
+ const path = macPlistPath()
207
+ return { supported: true, installed: existsSync(path), path }
208
+ }
209
+ if (process.platform === 'linux') {
210
+ const path = linuxUnitPath()
211
+ return { supported: true, installed: existsSync(path), path }
212
+ }
213
+ return { supported: false, installed: false }
214
+ }
215
+
216
+ export async function daemonInstall({ port, force }: { port: number; force: boolean }) {
217
+ if (shouldSkipPersistentDaemon()) {
218
+ throw new Error('background daemon setup is unavailable in this environment')
219
+ }
220
+
221
+ const invocation = resolveDaemonInvocation()
222
+ console.log(` binary: ${[invocation.executable, ...invocation.prefixArgs].join(' ')}`)
223
+ console.log(` port: ${port}`)
224
+
225
+ if (process.platform === 'darwin') {
226
+ const legacy = teardownLegacyMacDaemonArtifacts()
227
+ if (!legacy.missing) {
228
+ console.log(` removed legacy ${LEGACY_SERVICE_LABEL_MAC} service`)
229
+ }
230
+ const path = macPlistPath()
231
+ if (existsSync(path) && !force) {
232
+ console.log(` already installed at ${path}`)
233
+ console.log(` pass --force to overwrite, or use 'rnx daemon restart' to reload`)
234
+ return
235
+ }
236
+ mkdirSync(dirname(path), { recursive: true })
237
+ writeFileSync(path, renderPlist(invocation, port))
238
+ console.log(` wrote ${path}`)
239
+
240
+ // bootstrap into the gui session. launchctl is famous for returning exit
241
+ // 0 from `bootstrap` while leaving the service unregistered (a stale
242
+ // bootout reference, an in-flight unload, etc.) — kickstart then fails
243
+ // with a confusing "service not found" 113. so: bootstrap, immediately
244
+ // verify with `launchctl print`, and if the verify fails do a clean
245
+ // bootout-then-bootstrap retry before giving up. we surface real stderr
246
+ // text from any of those steps so users see what launchd actually said.
247
+ const target = `gui/${process.getuid!()}`
248
+ const serviceTarget = `${target}/${SERVICE_LABEL_MAC}`
249
+ const tryBootstrap = (): { ok: true } | { ok: false; reason: string } => {
250
+ const bs = spawnSync('launchctl', ['bootstrap', target, path], {
251
+ stdio: 'pipe',
252
+ encoding: 'utf8',
253
+ })
254
+ const bsErr = (bs.stderr || bs.stdout || '').trim()
255
+ if (bs.status !== 0) {
256
+ return {
257
+ ok: false,
258
+ reason: `bootstrap exit ${bs.status}${bsErr ? `: ${bsErr}` : ''}`,
259
+ }
260
+ }
261
+ const verify = spawnSync('launchctl', ['print', serviceTarget], {
262
+ encoding: 'utf8',
263
+ })
264
+ if (verify.status !== 0) {
265
+ return {
266
+ ok: false,
267
+ reason: `bootstrap returned 0 but service not registered (print exit ${verify.status})`,
268
+ }
269
+ }
270
+ return { ok: true }
271
+ }
272
+
273
+ let attempt = tryBootstrap()
274
+ if (!attempt.ok) {
275
+ const first = attempt.reason
276
+ // clean reload — bootout any stale reference then bootstrap again.
277
+ spawnSync('launchctl', ['bootout', serviceTarget], { stdio: 'ignore' })
278
+ attempt = tryBootstrap()
279
+ if (!attempt.ok) {
280
+ throw new Error(
281
+ `launchctl bootstrap failed: ${attempt.reason}` +
282
+ (first !== attempt.reason ? ` (initial: ${first})` : '') +
283
+ ` — try \`rnx daemon uninstall\` then retry, or reboot if launchd state is stuck.`,
284
+ )
285
+ }
286
+ }
287
+
288
+ // RunAtLoad=true on the plist means bootstrap already started the
289
+ // agent. an explicit `launchctl kickstart` is redundant here and on
290
+ // some machines fails with 113 ("service not found") when the agent
291
+ // crashes immediately and lands in launchd's throttle state — even
292
+ // though the service IS registered. instead, poll `launchctl print`
293
+ // for a few seconds until we see the service is running, then we're
294
+ // done; if it never reaches running, surface the err log path.
295
+ const deadline = Date.now() + 4000
296
+ let lastState: string | undefined
297
+ let lastPid: string | undefined
298
+ while (Date.now() < deadline) {
299
+ const p = spawnSync('launchctl', ['print', serviceTarget], { encoding: 'utf8' })
300
+ if (p.status === 0) {
301
+ lastState = p.stdout?.match(/state\s*=\s*(\w+)/)?.[1]
302
+ lastPid = p.stdout?.match(/pid\s*=\s*(\d+)/)?.[1]
303
+ if (lastState === 'running' || lastPid) break
304
+ }
305
+ await new Promise((r) => setTimeout(r, 150))
306
+ }
307
+ if (lastState !== 'running' && !lastPid) {
308
+ throw new Error(
309
+ `daemon registered but did not reach running state within 4s ` +
310
+ `(last state: ${lastState || 'unknown'}). check ${resolve(LOG_DIR, 'bridge.err.log')} ` +
311
+ `— typically a stale runtime or a port already bound by another process.`,
312
+ )
313
+ }
314
+ console.log(
315
+ ` registered (state: ${lastState || 'unknown'}${lastPid ? `, pid ${lastPid}` : ''}, log: ${resolve(LOG_DIR, 'bridge.err.log')})`,
316
+ )
317
+ return
318
+ }
319
+
320
+ if (process.platform === 'linux') {
321
+ const path = linuxUnitPath()
322
+ if (existsSync(path) && !force) {
323
+ console.log(` already installed at ${path}`)
324
+ console.log(` pass --force to overwrite, or use 'rnx daemon restart' to reload`)
325
+ return
326
+ }
327
+ mkdirSync(dirname(path), { recursive: true })
328
+ writeFileSync(path, renderSystemdUnit(invocation, port))
329
+ console.log(` wrote ${path}`)
330
+
331
+ const reload = spawnSync('systemctl', ['--user', 'daemon-reload'], {
332
+ stdio: 'pipe',
333
+ encoding: 'utf8',
334
+ })
335
+ if (reload.status !== 0) {
336
+ const msg = (reload.stderr || reload.stdout || '').trim()
337
+ throw new Error(`systemctl daemon-reload failed${msg ? `: ${msg}` : ''}`)
338
+ }
339
+ const enable = spawnSync(
340
+ 'systemctl',
341
+ ['--user', 'enable', '--now', SERVICE_NAME_LINUX],
342
+ { stdio: 'pipe', encoding: 'utf8' },
343
+ )
344
+ if (enable.status !== 0) {
345
+ const msg = (enable.stderr || enable.stdout || '').trim()
346
+ throw new Error(
347
+ `systemctl enable --now failed${msg ? `: ${msg}` : ''}` +
348
+ ` — try \`rnx daemon uninstall\` then retry.`,
349
+ )
350
+ }
351
+ const isActive = spawnSync('systemctl', ['--user', 'is-active', SERVICE_NAME_LINUX], {
352
+ encoding: 'utf8',
353
+ })
354
+ const state = (isActive.stdout || '').trim() || 'unknown'
355
+ console.log(
356
+ ` registered (state: ${state}, journalctl --user -u ${SERVICE_NAME_LINUX} to tail logs)`,
357
+ )
358
+
359
+ // user-scope systemd units are killed when the user's last session ends.
360
+ // on most distros the daemon then disappears at logout — the unit is
361
+ // "active" until you log off, then silently dead, and `rnx` looks
362
+ // broken when you ssh back in. `loginctl enable-linger` keeps the user
363
+ // bus alive across sessions, which is what every other long-running
364
+ // user agent (gpg-agent, ssh-agent --systemd) also relies on.
365
+ //
366
+ // orb's default user is already lingered (orb seeds it); multipass and
367
+ // lima fresh users are not. probing `loginctl show-user --property=Linger`
368
+ // tells us whether to nudge the user — running enable-linger ourselves
369
+ // requires root (`loginctl enable-linger` invokes a polkit action that
370
+ // typically needs sudo from a non-root user), so we just print the
371
+ // one-liner instead of trying to escalate ourselves.
372
+ try {
373
+ const username = process.env.USER || process.env.LOGNAME || ''
374
+ if (username) {
375
+ const linger = spawnSync(
376
+ 'loginctl',
377
+ ['show-user', username, '--property=Linger', '--value'],
378
+ { encoding: 'utf8' },
379
+ )
380
+ const lingerEnabled = (linger.stdout || '').trim().toLowerCase() === 'yes'
381
+ if (!lingerEnabled && linger.status === 0) {
382
+ console.log(
383
+ ` note: linger is not enabled for "${username}". the daemon will exit at logout.\n` +
384
+ ` enable persistence with: sudo loginctl enable-linger ${username}`,
385
+ )
386
+ }
387
+ }
388
+ } catch {
389
+ // loginctl missing or non-systemd init — silent; the daemon still works
390
+ // for the current session even without lingering.
391
+ }
392
+ return
393
+ }
394
+
395
+ console.error(` background daemon is not supported on ${process.platform}`)
396
+ console.error(` run 'rnx serve' in a persistent shell instead`)
397
+ rnxExit(1)
398
+ }
399
+
400
+ // removed-path log (idempotent helpers track what they actually touched so the
401
+ // caller can report a unified "not installed" / "removed N entries" line).
402
+ type TeardownReport = { removed: string[]; missing: boolean }
403
+
404
+ export function teardownLegacyMacDaemonArtifacts(
405
+ options: {
406
+ userHome?: string
407
+ launchTarget?: string
408
+ bootout?: (target: string) => void
409
+ } = {},
410
+ ): TeardownReport {
411
+ const userHome = options.userHome ?? homedir()
412
+ const plist = macLegacyPlistPath(userHome)
413
+ const logDir = resolve(userHome, 'Library/Logs/sootsim')
414
+ const removed: string[] = []
415
+ if (existsSync(plist)) {
416
+ const target = `${
417
+ options.launchTarget ?? `gui/${process.getuid?.() ?? 0}`
418
+ }/${LEGACY_SERVICE_LABEL_MAC}`
419
+ const bootout =
420
+ options.bootout ??
421
+ ((serviceTarget: string) => {
422
+ spawnSync('launchctl', ['bootout', serviceTarget], { stdio: 'ignore' })
423
+ })
424
+ bootout(target)
425
+ rmSync(plist, { force: true })
426
+ removed.push(plist)
427
+ }
428
+ for (const name of ['server.err.log', 'server.out.log']) {
429
+ const log = resolve(logDir, name)
430
+ if (!existsSync(log)) continue
431
+ rmSync(log, { force: true })
432
+ removed.push(log)
433
+ }
434
+ return { removed, missing: removed.length === 0 }
435
+ }
436
+
437
+ /** stop + disable + remove the persistent launchd / systemd registration
438
+ * and its support files (mac .app bundle, log dir). does NOT touch user
439
+ * data under ~/.rnx. idempotent: each entry is checked before remove. */
440
+ export function teardownDaemonService(): TeardownReport {
441
+ const removed: string[] = []
442
+ if (process.platform === 'darwin') {
443
+ const legacy = teardownLegacyMacDaemonArtifacts()
444
+ removed.push(...legacy.removed)
445
+ const plist = macPlistPath()
446
+ if (existsSync(plist)) {
447
+ spawnSync(
448
+ 'launchctl',
449
+ ['bootout', `gui/${process.getuid!()}/${SERVICE_LABEL_MAC}`],
450
+ { stdio: 'ignore' },
451
+ )
452
+ rmSync(plist, { force: true })
453
+ removed.push(plist)
454
+ }
455
+ // daemon-app holds only the generated .app launcher, so the whole
456
+ // directory goes with the service.
457
+ const appDir = daemonAppDir()
458
+ if (existsSync(appDir)) {
459
+ rmSync(appDir, { recursive: true, force: true })
460
+ removed.push(appDir)
461
+ }
462
+ if (existsSync(LOG_DIR)) {
463
+ rmSync(LOG_DIR, { recursive: true, force: true })
464
+ removed.push(LOG_DIR)
465
+ }
466
+ return { removed, missing: removed.length === 0 }
467
+ }
468
+ if (process.platform === 'linux') {
469
+ const unit = linuxUnitPath()
470
+ if (existsSync(unit)) {
471
+ // `disable --now` stops + disables in one call. ignore exit status: a
472
+ // unit whose ExecStart no longer exists can return non-zero while
473
+ // still successfully disabling the unit.
474
+ spawnSync('systemctl', ['--user', 'disable', '--now', SERVICE_NAME_LINUX], {
475
+ stdio: 'ignore',
476
+ })
477
+ rmSync(unit, { force: true })
478
+ spawnSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore' })
479
+ // reset-failed clears the "failed" state that lingers in systemd when
480
+ // the unit exited with a missing ExecStart, otherwise `systemctl --user
481
+ // status sootsim-daemon` keeps showing failed even after the unit file
482
+ // is gone.
483
+ spawnSync('systemctl', ['--user', 'reset-failed', SERVICE_NAME_LINUX], {
484
+ stdio: 'ignore',
485
+ })
486
+ removed.push(unit)
487
+ }
488
+ if (existsSync(LOG_DIR_LINUX)) {
489
+ rmSync(LOG_DIR_LINUX, { recursive: true, force: true })
490
+ removed.push(LOG_DIR_LINUX)
491
+ }
492
+ return { removed, missing: removed.length === 0 }
493
+ }
494
+ // win + others: no persistent daemon mechanism yet; nothing to tear down.
495
+ return { removed, missing: true }
496
+ }
497
+
498
+ /** remove EVERYTHING under ~/.rnx: installed runtimes, electron support,
499
+ * device profiles and their app storage, live dev-bridge state, lockfile,
500
+ * and config. only `rnx daemon uninstall --purge` calls this, because a
501
+ * user who asked to remove a background service did not ask to lose the
502
+ * simulators they have been working in. idempotent. honors $RNX_HOME. */
503
+ export function teardownRnxHome(): TeardownReport {
504
+ const removed: string[] = []
505
+ const home = rnxHomeDir()
506
+ if (existsSync(home)) {
507
+ rmSync(home, { recursive: true, force: true })
508
+ removed.push(home)
509
+ }
510
+ return { removed, missing: removed.length === 0 }
511
+ }
512
+
513
+ // `uninstall` removes the background service and nothing else. device
514
+ // profiles, their app storage, and live dev-bridge state belong to simulators
515
+ // the user may still be working in, so deleting them is something you ask for
516
+ // by name with `--purge`.
517
+ async function daemonUninstall(rest: string[]) {
518
+ const unknown = rest.filter((arg) => arg !== '--purge')
519
+ if (unknown.length > 0) {
520
+ console.error(` unknown daemon uninstall option: ${unknown[0]}`)
521
+ printHelp()
522
+ rnxExit(1)
523
+ return
524
+ }
525
+ const purge = rest.includes('--purge')
526
+
527
+ const service = teardownDaemonService()
528
+ for (const path of service.removed) console.log(` removed ${path}`)
529
+
530
+ if (!purge) {
531
+ if (service.missing) console.log(' not installed')
532
+ const home = rnxHomeDir()
533
+ if (existsSync(home)) {
534
+ console.log(` kept ${home} (runtimes, device profiles, open simulators)`)
535
+ console.log(
536
+ ` remove that too with \`${rnxPublicBrand.commandName} daemon uninstall --purge\``,
537
+ )
538
+ }
539
+ return
540
+ }
541
+
542
+ const home = teardownRnxHome()
543
+ for (const path of home.removed) console.log(` removed ${path}`)
544
+ if (service.missing && home.missing) console.log(' not installed')
545
+ }
546
+
547
+ // the service manager's own word is not proof the bridge works: a daemon that
548
+ // crashes on start reads as `active` all through systemd's restart backoff,
549
+ // which is how a fully broken daemon reported healthy on the 2026-08-19 drive.
550
+ // the lockfile carries a pid and a heartbeat, so it answers the question the
551
+ // caller actually asked.
552
+ function printBridgeLiveness() {
553
+ const lock = readDaemonLockfile()
554
+ if (isDaemonLockfileFresh(lock)) {
555
+ console.log(` bridge: responding on port ${lock.bridgePort} (pid ${lock.pid})`)
556
+ return
557
+ }
558
+ console.log(
559
+ lock
560
+ ? ' bridge: NOT responding — stale lockfile; check `rnx daemon logs`'
561
+ : ' bridge: NOT responding — no lockfile; check `rnx daemon logs`',
562
+ )
563
+ }
564
+
565
+ async function daemonStatus() {
566
+ if (process.platform === 'darwin') {
567
+ const path = macPlistPath()
568
+ if (!existsSync(path)) {
569
+ console.log(` installed: no`)
570
+ return
571
+ }
572
+ console.log(` installed: yes (${path})`)
573
+ const binary = readPlistBinary(path)
574
+ if (binary) console.log(` binary: ${binary}`)
575
+ const res = spawnSync(
576
+ 'launchctl',
577
+ ['print', `gui/${process.getuid!()}/${SERVICE_LABEL_MAC}`],
578
+ { encoding: 'utf8' },
579
+ )
580
+ if (res.status === 0) {
581
+ const state = res.stdout.match(/state\s*=\s*(\w+)/)?.[1]
582
+ const pid = res.stdout.match(/pid\s*=\s*(\d+)/)?.[1]
583
+ console.log(` state: ${state || 'unknown'}${pid ? ` (pid ${pid})` : ''}`)
584
+ } else {
585
+ console.log(' state: not running')
586
+ }
587
+ printBridgeLiveness()
588
+ return
589
+ }
590
+ if (process.platform === 'linux') {
591
+ const path = linuxUnitPath()
592
+ if (!existsSync(path)) {
593
+ console.log(` installed: no`)
594
+ return
595
+ }
596
+ console.log(` installed: yes (${path})`)
597
+ const res = spawnSync('systemctl', ['--user', 'is-active', SERVICE_NAME_LINUX], {
598
+ encoding: 'utf8',
599
+ })
600
+ console.log(` state: ${res.stdout.trim() || 'unknown'}`)
601
+ printBridgeLiveness()
602
+ return
603
+ }
604
+ console.error(` unsupported platform: ${process.platform}`)
605
+ rnxExit(1)
606
+ }
607
+
608
+ async function daemonRestart() {
609
+ if (process.platform === 'darwin') {
610
+ const path = macPlistPath()
611
+ if (!existsSync(path)) {
612
+ console.error(' daemon not registered. run `rnx daemon install`')
613
+ rnxExit(1)
614
+ }
615
+ const res = spawnSync(
616
+ 'launchctl',
617
+ ['kickstart', '-k', `gui/${process.getuid!()}/${SERVICE_LABEL_MAC}`],
618
+ { stdio: 'pipe', encoding: 'utf8' },
619
+ )
620
+ if (res.status !== 0) {
621
+ console.error(` kickstart failed: ${res.stderr?.trim()}`)
622
+ rnxExit(1)
623
+ }
624
+ console.log(' restarted')
625
+ return
626
+ }
627
+ if (process.platform === 'linux') {
628
+ const path = linuxUnitPath()
629
+ if (!existsSync(path)) {
630
+ console.error(' daemon not registered. run `rnx daemon install`')
631
+ rnxExit(1)
632
+ }
633
+ const res = spawnSync('systemctl', ['--user', 'restart', SERVICE_NAME_LINUX], {
634
+ stdio: 'pipe',
635
+ encoding: 'utf8',
636
+ })
637
+ if (res.status !== 0) {
638
+ console.error(` restart failed: ${res.stderr?.trim()}`)
639
+ rnxExit(1)
640
+ }
641
+ console.log(' restarted')
642
+ return
643
+ }
644
+ console.error(` unsupported platform: ${process.platform}`)
645
+ rnxExit(1)
646
+ }
647
+
648
+ async function daemonStart() {
649
+ if (process.platform === 'darwin') {
650
+ const path = macPlistPath()
651
+ if (!existsSync(path)) {
652
+ console.error(' daemon not registered. run `rnx daemon install`')
653
+ rnxExit(1)
654
+ }
655
+ const res = spawnSync(
656
+ 'launchctl',
657
+ ['kickstart', `gui/${process.getuid!()}/${SERVICE_LABEL_MAC}`],
658
+ { stdio: 'pipe', encoding: 'utf8' },
659
+ )
660
+ if (res.status !== 0) {
661
+ console.error(` start failed: ${res.stderr?.trim()}`)
662
+ rnxExit(1)
663
+ }
664
+ console.log(' started')
665
+ return
666
+ }
667
+ if (process.platform === 'linux') {
668
+ const path = linuxUnitPath()
669
+ if (!existsSync(path)) {
670
+ console.error(' daemon not registered. run `rnx daemon install`')
671
+ rnxExit(1)
672
+ }
673
+ const res = spawnSync('systemctl', ['--user', 'start', SERVICE_NAME_LINUX], {
674
+ stdio: 'pipe',
675
+ encoding: 'utf8',
676
+ })
677
+ if (res.status !== 0) {
678
+ console.error(` start failed: ${res.stderr?.trim()}`)
679
+ rnxExit(1)
680
+ }
681
+ console.log(' started')
682
+ return
683
+ }
684
+ console.error(` unsupported platform: ${process.platform}`)
685
+ rnxExit(1)
686
+ }
687
+
688
+ async function daemonStop() {
689
+ if (process.platform === 'darwin') {
690
+ const path = macPlistPath()
691
+ if (!existsSync(path)) {
692
+ console.error(' daemon not registered — nothing to stop')
693
+ rnxExit(1)
694
+ }
695
+ const res = spawnSync(
696
+ 'launchctl',
697
+ ['bootout', `gui/${process.getuid!()}/${SERVICE_LABEL_MAC}`],
698
+ { stdio: 'pipe', encoding: 'utf8' },
699
+ )
700
+ if (res.status !== 0) {
701
+ console.error(` stop failed: ${res.stderr?.trim()}`)
702
+ rnxExit(1)
703
+ }
704
+ console.log(' stopped')
705
+ return
706
+ }
707
+ if (process.platform === 'linux') {
708
+ const path = linuxUnitPath()
709
+ if (!existsSync(path)) {
710
+ console.error(' daemon not registered — nothing to stop')
711
+ rnxExit(1)
712
+ }
713
+ const res = spawnSync('systemctl', ['--user', 'stop', SERVICE_NAME_LINUX], {
714
+ stdio: 'pipe',
715
+ encoding: 'utf8',
716
+ })
717
+ if (res.status !== 0) {
718
+ console.error(` stop failed: ${res.stderr?.trim()}`)
719
+ rnxExit(1)
720
+ }
721
+ console.log(' stopped')
722
+ return
723
+ }
724
+ console.error(` unsupported platform: ${process.platform}`)
725
+ rnxExit(1)
726
+ }
727
+
728
+ // tail the daemon's logs. macOS reads the launchd-redirected files directly
729
+ // (works for any user); linux reads the systemd journal. journalctl --user
730
+ // fails for a freshly-created user not yet in the systemd-journal/adm group
731
+ // ("No journal files were opened due to insufficient permissions"), so on that
732
+ // error we retry the system-scoped query and, failing that, point the user at
733
+ // the sudo form rather than printing a cryptic permission error.
734
+ async function daemonLogs(rest: string[]) {
735
+ const linesArg = rest.find((a) => /^-n\d+$|^--lines=\d+$|^\d+$/.test(a))
736
+ const lines = linesArg ? Number(linesArg.replace(/\D/g, '')) : 200
737
+ const follow = rest.includes('-f') || rest.includes('--follow')
738
+
739
+ if (process.platform === 'darwin') {
740
+ const out = resolve(LOG_DIR, 'bridge.out.log')
741
+ const err = resolve(LOG_DIR, 'bridge.err.log')
742
+ const present = [out, err].filter((p) => existsSync(p))
743
+ if (present.length === 0) {
744
+ console.error(` no daemon logs yet at ${LOG_DIR}. run \`rnx daemon install\``)
745
+ rnxExit(1)
746
+ }
747
+ const args = follow
748
+ ? ['-n', String(lines), '-F', ...present]
749
+ : ['-n', String(lines), ...present]
750
+ spawnSync('tail', args, { stdio: 'inherit' })
751
+ return
752
+ }
753
+
754
+ if (process.platform === 'linux') {
755
+ const unit = `${SERVICE_NAME_LINUX}.service`
756
+ const base = ['--user', '-u', unit, '-n', String(lines), '--no-pager']
757
+ const res = spawnSync('journalctl', follow ? [...base, '-f'] : base, {
758
+ encoding: 'utf8',
759
+ stdio: follow ? 'inherit' : 'pipe',
760
+ })
761
+ if (follow) return
762
+ const combined = `${res.stdout || ''}${res.stderr || ''}`
763
+ const blocked = /insufficient permissions|No journal files were opened/i.test(
764
+ combined,
765
+ )
766
+ if (res.status === 0 && !blocked) {
767
+ process.stdout.write(res.stdout || '')
768
+ return
769
+ }
770
+ // fresh-user fallback: the system journal holds the user-unit logs even when
771
+ // the user can't read --user scope. try it, then fall back to guidance.
772
+ const sys = spawnSync(
773
+ 'journalctl',
774
+ [`_SYSTEMD_USER_UNIT=${unit}`, '-n', String(lines), '--no-pager'],
775
+ { encoding: 'utf8' },
776
+ )
777
+ if (sys.status === 0 && (sys.stdout || '').trim()) {
778
+ process.stdout.write(sys.stdout)
779
+ return
780
+ }
781
+ console.error(
782
+ ` could not read --user journal for ${unit} (this user isn't in the systemd-journal group yet).\n` +
783
+ ` run: sudo journalctl _SYSTEMD_USER_UNIT=${unit} -n ${lines} --no-pager`,
784
+ )
785
+ rnxExit(1)
786
+ }
787
+
788
+ console.error(` unsupported platform: ${process.platform}`)
789
+ rnxExit(1)
790
+ }
791
+
792
+ function readPlistBinary(path: string): string | null {
793
+ try {
794
+ const body = readFileSync(path, 'utf8')
795
+ const match = body.match(/<array>\s*<string>([^<]+)<\/string>/)
796
+ return match?.[1] || null
797
+ } catch {
798
+ return null
799
+ }
800
+ }