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.
- package/LICENSE +11 -10
- package/README.md +5 -0
- package/cli/app-config.ts +65 -0
- package/cli/app-fonts.ts +408 -0
- package/cli/app-project.ts +231 -0
- package/cli/app-splash.ts +185 -0
- package/cli/app-state-reset.ts +24 -0
- package/cli/auth.ts +155 -0
- package/cli/bin.ts +594 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2830 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/commands/agent-wrapper.ts +986 -0
- package/cli/commands/agent.ts +423 -0
- package/cli/commands/app-fonts.ts +98 -0
- package/cli/commands/assert.ts +541 -0
- package/cli/commands/auth.ts +59 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +87 -0
- package/cli/commands/config.ts +32 -0
- package/cli/commands/control.ts +2142 -0
- package/cli/commands/cpu-profile.ts +269 -0
- package/cli/commands/daemon-mac-app.ts +169 -0
- package/cli/commands/daemon.ts +874 -0
- package/cli/commands/debug.ts +719 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +197 -0
- package/cli/commands/detox.ts +385 -0
- package/cli/commands/device.ts +133 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/electron.ts +95 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1124 -0
- package/cli/commands/inspect/actions.ts +622 -0
- package/cli/commands/inspect/core.ts +2405 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +192 -0
- package/cli/commands/inspect/env.ts +23 -0
- package/cli/commands/inspect/find.ts +171 -0
- package/cli/commands/inspect/get-layout.ts +39 -0
- package/cli/commands/inspect/keyboard.ts +52 -0
- package/cli/commands/inspect/list.ts +58 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +82 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +22 -0
- package/cli/commands/inspect/settling.ts +158 -0
- package/cli/commands/inspect/shared.ts +353 -0
- package/cli/commands/inspect/sleep.ts +14 -0
- package/cli/commands/inspect/tree.ts +32 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +210 -0
- package/cli/commands/inspect/wait-idle.ts +24 -0
- package/cli/commands/inspect/wait-ready.ts +74 -0
- package/cli/commands/inspect/wait-selector.ts +54 -0
- package/cli/commands/inspect/wait.ts +31 -0
- package/cli/commands/inspect.ts +4519 -0
- package/cli/commands/install-desktop.ts +351 -0
- package/cli/commands/login.ts +331 -0
- package/cli/commands/logout.ts +31 -0
- package/cli/commands/maestro-generate.ts +361 -0
- package/cli/commands/maestro.ts +453 -0
- package/cli/commands/mode.ts +57 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +66 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/profile.ts +108 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1434 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +85 -0
- package/cli/commands/runtime.ts +351 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +143 -0
- package/cli/commands/screenshot-mode.ts +37 -0
- package/cli/commands/screenshot.ts +488 -0
- package/cli/commands/screenshots-capture.ts +607 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +168 -0
- package/cli/commands/setup.ts +545 -0
- package/cli/commands/shell-boolean-mode.ts +81 -0
- package/cli/commands/skills.ts +467 -0
- package/cli/commands/slides.ts +361 -0
- package/cli/commands/state.ts +87 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +54 -0
- package/cli/commands/three-mode.ts +763 -0
- package/cli/commands/timeline.ts +122 -0
- package/cli/commands/upgrade.ts +208 -0
- package/cli/commands/upload.ts +1225 -0
- package/cli/commands/version.ts +54 -0
- package/cli/commands/what-happened.ts +327 -0
- package/cli/current-sim.ts +204 -0
- package/cli/desktop-companion.ts +300 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +180 -0
- package/cli/drivers/playwright.ts +698 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +120 -0
- package/cli/flow-session.ts +187 -0
- package/cli/help.ts +80 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +216 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/maestro-js.ts +334 -0
- package/cli/open-url.ts +8 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +211 -0
- package/cli/prompt.ts +51 -0
- package/cli/recording-access.ts +107 -0
- package/cli/registry.ts +1 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/run-registry.ts +226 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/setup-repository.ts +187 -0
- package/cli/telemetry.ts +187 -0
- package/cli/ws-bridge.ts +798 -0
- package/dist-cli/bin.js +5 -5
- package/dist-cli/chunks/{agent-XZ2KTPCU.js → agent-7YBDCYMA.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-JJYYW2WH.js → agent-wrapper-2GHFBHCR.js} +2 -2
- package/dist-cli/chunks/{app-fonts-IXRNQG6B.js → app-fonts-RSNVPQSU.js} +2 -2
- package/dist-cli/chunks/{assert-54T5SK5F.js → assert-XCMX3XJX.js} +2 -2
- package/dist-cli/chunks/{auth-FI5UDI45.js → auth-B442HRAX.js} +2 -2
- package/dist-cli/chunks/{beta-JV6UKADW.js → beta-XJ55JK3M.js} +2 -2
- package/dist-cli/chunks/camera-UGYSSLIK.js +33 -0
- package/dist-cli/chunks/{chunk-3NV2NCNX.js → chunk-277AEQZX.js} +2 -2
- package/dist-cli/chunks/{chunk-2YR5BGA5.js → chunk-2JNSK774.js} +2 -2
- package/dist-cli/chunks/{chunk-RSZWCKNT.js → chunk-32WOTSTR.js} +3 -3
- package/dist-cli/chunks/{chunk-WEXDAC74.js → chunk-3S753SNQ.js} +2 -2
- package/dist-cli/chunks/{chunk-IJO63TDP.js → chunk-46ZOLOYA.js} +2 -2
- package/dist-cli/chunks/{chunk-WUSWBCWA.js → chunk-5L7ELDQL.js} +8 -9
- package/dist-cli/chunks/{chunk-WF3T4SVI.js → chunk-7OOPFSQS.js} +2 -2
- package/dist-cli/chunks/{chunk-TZFFR3SD.js → chunk-7SV3RPRW.js} +2 -2
- package/dist-cli/chunks/{chunk-WWZIXIRD.js → chunk-7ZC35MOU.js} +1 -1
- package/dist-cli/chunks/chunk-AMG5E6CC.js +9 -0
- package/dist-cli/chunks/{chunk-WINYQ44O.js → chunk-APWNH3A4.js} +1 -1
- package/dist-cli/chunks/chunk-B57XUKY3.js +4 -0
- package/dist-cli/chunks/{chunk-YDGQTMQL.js → chunk-C2NL26TD.js} +1 -1
- package/dist-cli/chunks/{chunk-NMF2ZMZQ.js → chunk-D2FNUWAB.js} +4 -4
- package/dist-cli/chunks/{chunk-YIFT42WN.js → chunk-E5T4XSJ3.js} +2 -2
- package/dist-cli/chunks/{chunk-46EUUFJ5.js → chunk-EAC34EQS.js} +1 -1
- package/dist-cli/chunks/{chunk-OVFJFXUD.js → chunk-EG32ML36.js} +2 -2
- package/dist-cli/chunks/{chunk-5YJCOWCH.js → chunk-FXUAC6D5.js} +1 -1
- package/dist-cli/chunks/chunk-GQSL4USA.js +6 -0
- package/dist-cli/chunks/{chunk-VFCMSYZK.js → chunk-HAXW27SS.js} +2 -2
- package/dist-cli/chunks/{chunk-2D2UPBBR.js → chunk-IZAHPAN6.js} +1 -1
- package/dist-cli/chunks/{chunk-7GN3LVWB.js → chunk-J62KM5TB.js} +2 -2
- package/dist-cli/chunks/{chunk-BTWORNNG.js → chunk-JEMCD5E4.js} +1 -1
- package/dist-cli/chunks/{chunk-DCEMHR2Y.js → chunk-JZS3Q37N.js} +2 -2
- package/dist-cli/chunks/{chunk-D4FFVGI5.js → chunk-KR4JON7D.js} +1 -1
- package/dist-cli/chunks/chunk-KWCYKQIQ.js +4 -0
- package/dist-cli/chunks/{chunk-VNQEB4L7.js → chunk-MCWPL644.js} +2 -2
- package/dist-cli/chunks/chunk-MJYK3N2I.js +4 -0
- package/dist-cli/chunks/{chunk-5TEF3ET3.js → chunk-O6TRIZNS.js} +2 -2
- package/dist-cli/chunks/{chunk-QKDWYITG.js → chunk-P7XL2E73.js} +3 -3
- package/dist-cli/chunks/{chunk-BBULZ7CG.js → chunk-PFQTUKQ4.js} +62 -87
- package/dist-cli/chunks/{chunk-GGRX24GF.js → chunk-PG5RZCTN.js} +2 -2
- package/dist-cli/chunks/{chunk-W6K4EFPH.js → chunk-QGRI2Z4M.js} +2 -2
- package/dist-cli/chunks/{chunk-ZMJD5GEC.js → chunk-QLXXE7GE.js} +1 -1
- package/dist-cli/chunks/{chunk-VZXWHRUZ.js → chunk-QOJJJWJE.js} +89 -133
- package/dist-cli/chunks/{chunk-OZSSI4WN.js → chunk-RWZY5427.js} +2 -2
- package/dist-cli/chunks/{chunk-UC6U3MML.js → chunk-RZKU2K3J.js} +2 -2
- package/dist-cli/chunks/{chunk-IJ5CAZZC.js → chunk-SBV4IK4H.js} +1 -1
- package/dist-cli/chunks/{chunk-DZS6WPUI.js → chunk-SGMVFFMK.js} +1 -1
- package/dist-cli/chunks/{chunk-OHAZNXLK.js → chunk-TAX4UT2N.js} +1 -1
- package/dist-cli/chunks/{chunk-5DHC6KHQ.js → chunk-TUOFAWXT.js} +1 -1
- package/dist-cli/chunks/{chunk-GASE6UBA.js → chunk-UHZLOHGP.js} +1 -1
- package/dist-cli/chunks/{chunk-F5ZRSS3C.js → chunk-VC7V76U3.js} +1 -1
- package/dist-cli/chunks/{chunk-RTN5C5RL.js → chunk-VQVMLW4U.js} +1 -1
- package/dist-cli/chunks/{chunk-WMIIKMGK.js → chunk-VUKKYPZN.js} +2 -2
- package/dist-cli/chunks/{chunk-5TPRP5QT.js → chunk-WGGRJDRE.js} +1 -1
- package/dist-cli/chunks/chunk-X6H76EKP.js +15 -0
- package/dist-cli/chunks/chunk-XLZ5FNRT.js +27 -0
- package/dist-cli/chunks/{chunk-HI5TFJWN.js → chunk-XULEACM4.js} +2 -2
- package/dist-cli/chunks/{chunk-MJRLLB4R.js → chunk-YFSDM7AX.js} +4 -4
- package/dist-cli/chunks/chunk-YWI3UEVX.js +5 -0
- package/dist-cli/chunks/{chunk-XEVZYVIW.js → chunk-ZBSJO4NB.js} +10 -9
- package/dist-cli/chunks/{cleanup-P27PA6JI.js → cleanup-EYLGCA6Z.js} +2 -2
- package/dist-cli/chunks/cli-version-LL2UGIHE.js +4 -0
- package/dist-cli/chunks/{compat-ZD65FED3.js → compat-TLJYHB4E.js} +2 -2
- package/dist-cli/chunks/{config-XMJRNM2A.js → config-4JWUOEXK.js} +2 -2
- package/dist-cli/chunks/{control-KMIQT3QP.js → control-HPAOYF4N.js} +2 -2
- package/dist-cli/chunks/daemon-OYLASXLE.js +4 -0
- package/dist-cli/chunks/{debug-PT4HOP7N.js → debug-4BKXF6KI.js} +5 -5
- package/dist-cli/chunks/{desktop-S3FG72AK.js → desktop-ZQ6ZD2S6.js} +3 -3
- package/dist-cli/chunks/{detox-B3D4IFCN.js → detox-WNPASSS3.js} +2 -2
- package/dist-cli/chunks/{device-XBNDSB2R.js → device-BXLXVG3V.js} +2 -2
- package/dist-cli/chunks/{diagnose-HMQXJE5N.js → diagnose-3QX7W5RW.js} +2 -2
- package/dist-cli/chunks/{disk-cleanup-BLCZ5BSZ.js → disk-cleanup-KSWKI7WB.js} +2 -2
- package/dist-cli/chunks/drivers-EPIEFF7P.js +4 -0
- package/dist-cli/chunks/{film-BJGTBYZB.js → film-GDMR33VO.js} +3 -3
- package/dist-cli/chunks/flow-NKMPBYCJ.js +4 -0
- package/dist-cli/chunks/help-OGCBHOKA.js +4 -0
- package/dist-cli/chunks/{hidden-runtime-alias-ANOYADHM.js → hidden-runtime-alias-S2GTDX3T.js} +2 -2
- package/dist-cli/chunks/home-paths-QRCDLTTV.js +4 -0
- package/dist-cli/chunks/inspect-FUYMOZPY.js +4 -0
- package/dist-cli/chunks/install-desktop-FR6YKW7Y.js +4 -0
- package/dist-cli/chunks/{login-FJ737MWG.js → login-IJAPUZHI.js} +4 -4
- package/dist-cli/chunks/{logout-ZCNMMHMY.js → logout-QMXDFU2X.js} +2 -2
- package/dist-cli/chunks/{maestro-SZTNKLDF.js → maestro-ZXU3YCVX.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-DCFAIZ4H.js → maestro-generate-PYB5QY7K.js} +3 -3
- package/dist-cli/chunks/{mode-GRMQCRXR.js → mode-WGUCL5FZ.js} +2 -2
- package/dist-cli/chunks/{optional-demo-registry-W36EWFFB.js → optional-demo-registry-WH2O6H36.js} +2 -2
- package/dist-cli/chunks/{perf-QYBAAUZG.js → perf-TOD3UFAH.js} +2 -2
- package/dist-cli/chunks/{permissions-3QCQ6VF4.js → permissions-I5BRJGTB.js} +2 -2
- package/dist-cli/chunks/{record-QPPC2S4E.js → record-ZYL2FYSK.js} +3 -3
- package/dist-cli/chunks/{report-issue-7NMFP4HK.js → report-issue-TAI6DYZO.js} +2 -2
- package/dist-cli/chunks/reset-7YGYKQPQ.js +4 -0
- package/dist-cli/chunks/runtime-B4JO6QRI.js +4 -0
- package/dist-cli/chunks/{screenshot-command-67AECJFB.js → screenshot-command-7ANLODZY.js} +7 -7
- package/dist-cli/chunks/{screenshot-layers-ASWBYPJL.js → screenshot-layers-A7FYXSVU.js} +3 -3
- package/dist-cli/chunks/{screenshots-capture-PXA3HFQK.js → screenshots-capture-WT2ZY6CB.js} +2 -2
- package/dist-cli/chunks/serve-TG5WKA4V.js +44 -0
- package/dist-cli/chunks/{setup-7DWPMRSB.js → setup-PCWLD22V.js} +2 -2
- package/dist-cli/chunks/{skills-S3Y22TUA.js → skills-27ZHWCQS.js} +2 -2
- package/dist-cli/chunks/state-ZQVY46ZO.js +14 -0
- package/dist-cli/chunks/{storage-XUIMJWAJ.js → storage-OYC57C4X.js} +6 -6
- package/dist-cli/chunks/store-32HSPZHI.js +4 -0
- package/dist-cli/chunks/telemetry-PBJR7XHR.js +4 -0
- package/dist-cli/chunks/{timeline-TMPLQPSP.js → timeline-RAJJFQT6.js} +2 -2
- package/dist-cli/chunks/{upgrade-7HDSIM7K.js → upgrade-OEFNZRIP.js} +2 -2
- package/dist-cli/chunks/upload-LQJITLKK.js +4 -0
- package/dist-cli/chunks/version-NIXTY6PL.js +6 -0
- package/dist-cli/chunks/{web-DG3WBYD3.js → web-XBUTBVGR.js} +2 -2
- package/dist-cli/chunks/{what-happened-XFVUTZR7.js → what-happened-YNHRFUDX.js} +3 -3
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract.cjs +20 -0
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +67 -28
- package/dist-lib/host/bridge-host.cjs +140 -12
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +2815 -40
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +2549 -2061
- package/dist-lib/sdk.mjs +2543 -2061
- package/dist-lib/skills.cjs +480 -280
- package/dist-lib/vite.cjs +1 -1
- package/package.json +8 -2
- package/src/bridge-constants.ts +3 -4
- package/src/bridge-contract.ts +251 -0
- package/src/connect.ts +83 -0
- package/src/disk-cleanup.ts +30 -0
- package/src/home-paths.ts +81 -38
- package/src/host/bridge-host.ts +134 -6
- package/src/index.ts +27 -1
- package/src/sdk.ts +8 -0
- package/src/sim-client.ts +660 -0
- package/dist-cli/chunks/camera-VL73YIKP.js +0 -22
- package/dist-cli/chunks/chunk-4NPPOV2N.js +0 -5
- package/dist-cli/chunks/chunk-FSUYIVJ6.js +0 -9
- package/dist-cli/chunks/chunk-G2WW6L2C.js +0 -23
- package/dist-cli/chunks/chunk-KTHV3RUS.js +0 -26
- package/dist-cli/chunks/chunk-LF2ZVT7O.js +0 -6
- package/dist-cli/chunks/chunk-NFK7T35W.js +0 -4
- package/dist-cli/chunks/chunk-TIVZIMMW.js +0 -4
- package/dist-cli/chunks/cli-version-WWLPBDQ7.js +0 -4
- package/dist-cli/chunks/daemon-G2ME7NLB.js +0 -4
- package/dist-cli/chunks/drivers-LDECZGP2.js +0 -4
- package/dist-cli/chunks/flow-UEQNVTU7.js +0 -4
- package/dist-cli/chunks/help-T5FYSVGB.js +0 -4
- package/dist-cli/chunks/home-paths-GT3LFNOR.js +0 -4
- package/dist-cli/chunks/inspect-ZA6XF5LD.js +0 -4
- package/dist-cli/chunks/install-desktop-TIMUDHPL.js +0 -4
- package/dist-cli/chunks/runtime-XOAXMSTU.js +0 -4
- package/dist-cli/chunks/serve-BI2NBAXG.js +0 -44
- package/dist-cli/chunks/store-JTHEJLAZ.js +0 -4
- package/dist-cli/chunks/telemetry-ZYJGD2DB.js +0 -4
- package/dist-cli/chunks/upload-GMSZPWM6.js +0 -4
- 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, '&')
|
|
270
|
+
.replace(/</g, '<')
|
|
271
|
+
.replace(/>/g, '>')
|
|
272
|
+
.replace(/"/g, '"')
|
|
273
|
+
.replace(/'/g, ''')
|
|
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
|
+
}
|