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.
- package/LICENSE +22 -0
- package/README.md +640 -0
- package/cli/app-config.ts +82 -0
- package/cli/app-fonts.ts +388 -0
- package/cli/app-project.ts +59 -0
- package/cli/app-splash.ts +171 -0
- package/cli/app-state-reset.ts +26 -0
- package/cli/auth.ts +184 -0
- package/cli/bin.ts +59 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2806 -0
- package/cli/browser-backend.ts +44 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/cli-update.ts +261 -0
- package/cli/cloud-client.ts +1530 -0
- package/cli/cloud-dispatch.ts +206 -0
- package/cli/cloud-session.ts +227 -0
- package/cli/command-contract.ts +221 -0
- package/cli/command-registry.ts +304 -0
- package/cli/commands/agent-wrapper.ts +1048 -0
- package/cli/commands/agent.ts +439 -0
- package/cli/commands/app-fonts.ts +99 -0
- package/cli/commands/assert.ts +542 -0
- package/cli/commands/auth.ts +71 -0
- package/cli/commands/box/bundle-plane.ts +71 -0
- package/cli/commands/box/checkout-plane.ts +256 -0
- package/cli/commands/box/plane-share.ts +208 -0
- package/cli/commands/box/rnx-command.ts +60 -0
- package/cli/commands/box/search-command.ts +157 -0
- package/cli/commands/box.ts +925 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +176 -0
- package/cli/commands/config.ts +34 -0
- package/cli/commands/control.ts +2154 -0
- package/cli/commands/cpu-profile.ts +941 -0
- package/cli/commands/daemon-mac-app.ts +165 -0
- package/cli/commands/daemon.ts +800 -0
- package/cli/commands/debug.ts +751 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +201 -0
- package/cli/commands/detox.ts +481 -0
- package/cli/commands/device.ts +134 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/do-chain.ts +408 -0
- package/cli/commands/electron.ts +94 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1240 -0
- package/cli/commands/inspect/actions.ts +516 -0
- package/cli/commands/inspect/core.ts +2086 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +119 -0
- package/cli/commands/inspect/env.ts +25 -0
- package/cli/commands/inspect/find.ts +169 -0
- package/cli/commands/inspect/get-layout.ts +53 -0
- package/cli/commands/inspect/keyboard.ts +54 -0
- package/cli/commands/inspect/list.ts +175 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +77 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +32 -0
- package/cli/commands/inspect/settling.ts +197 -0
- package/cli/commands/inspect/shared.ts +259 -0
- package/cli/commands/inspect/sleep.ts +15 -0
- package/cli/commands/inspect/tree.ts +23 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +211 -0
- package/cli/commands/inspect/wait-idle.ts +25 -0
- package/cli/commands/inspect/wait-ready.ts +87 -0
- package/cli/commands/inspect/wait-selector.ts +59 -0
- package/cli/commands/inspect/wait.ts +32 -0
- package/cli/commands/inspect.ts +4738 -0
- package/cli/commands/install-cli.ts +275 -0
- package/cli/commands/install-desktop.ts +385 -0
- package/cli/commands/login.ts +357 -0
- package/cli/commands/logout.ts +24 -0
- package/cli/commands/maestro-generate.ts +375 -0
- package/cli/commands/maestro.ts +536 -0
- package/cli/commands/mode.ts +58 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +67 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/platform.ts +410 -0
- package/cli/commands/preview.ts +103 -0
- package/cli/commands/profile.ts +106 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1435 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +89 -0
- package/cli/commands/runtime.ts +348 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +144 -0
- package/cli/commands/screenshot-mode.ts +38 -0
- package/cli/commands/screenshot.ts +492 -0
- package/cli/commands/screenshots-capture.ts +608 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +139 -0
- package/cli/commands/setup.ts +416 -0
- package/cli/commands/shell-boolean-mode.ts +82 -0
- package/cli/commands/skills.ts +468 -0
- package/cli/commands/slides.ts +357 -0
- package/cli/commands/state.ts +94 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +55 -0
- package/cli/commands/test.ts +535 -0
- package/cli/commands/three-mode.ts +771 -0
- package/cli/commands/timeline.ts +121 -0
- package/cli/commands/upgrade.ts +167 -0
- package/cli/commands/upload.ts +1203 -0
- package/cli/commands/version.ts +63 -0
- package/cli/commands/welcome.ts +17 -0
- package/cli/commands/what-happened.ts +328 -0
- package/cli/current-sim.ts +217 -0
- package/cli/desktop-companion.ts +315 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +184 -0
- package/cli/drivers/playwright-sim-host.ts +537 -0
- package/cli/drivers/playwright.ts +209 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-export.ts +55 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +126 -0
- package/cli/flow-session.ts +140 -0
- package/cli/help-core.ts +143 -0
- package/cli/help.ts +44 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +214 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/internal-child.ts +176 -0
- package/cli/maestro-js.ts +323 -0
- package/cli/main.ts +704 -0
- package/cli/open-url.ts +8 -0
- package/cli/outbound-endpoints.ts +830 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +189 -0
- package/cli/privacy.ts +53 -0
- package/cli/prompt.ts +56 -0
- package/cli/recording-access.ts +108 -0
- package/cli/registry.ts +2 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/rnx-in-process.ts +98 -0
- package/cli/run-registry.ts +226 -0
- package/cli/run-rnx-core.ts +148 -0
- package/cli/run-rnx.ts +137 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/secure-file.ts +31 -0
- package/cli/self-invocation.ts +58 -0
- package/cli/send-to-box.ts +109 -0
- package/cli/setup-repository.ts +191 -0
- package/cli/shell-init.ts +98 -0
- package/cli/standalone.ts +4 -0
- package/cli/telemetry.ts +84 -0
- package/cli/ui.ts +68 -0
- package/cli/ws-bridge.ts +1020 -0
- package/detox/attempt-events.cjs +23 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +48 -0
- package/detox/expectations.ts +534 -0
- package/detox/gestures.ts +501 -0
- package/detox/index.ts +2694 -0
- package/detox/jest-preset.cjs +29 -0
- package/detox/jest-setup-after-env.cjs +22 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation-lifecycle.ts +290 -0
- package/detox/navigation.ts +43 -0
- package/detox/proof-frame.cjs +114 -0
- package/detox/record-block-execution.cjs +78 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-lib/agent-daemon-client.cjs +502 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +759 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +208 -0
- package/dist-lib/backend-origin.cjs +88 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/bridge-contract-input.cjs +451 -0
- package/dist-lib/bridge-contract-input.mjs +414 -0
- package/dist-lib/bridge-contract.cjs +130 -0
- package/dist-lib/bridge-contract.mjs +94 -0
- package/dist-lib/capture-contract.cjs +220 -0
- package/dist-lib/capture-contract.mjs +190 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/cloud-contract.cjs +194 -0
- package/dist-lib/cloud-contract.mjs +145 -0
- package/dist-lib/cloud.cjs +670 -0
- package/dist-lib/cloud.mjs +639 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +3789 -0
- package/dist-lib/detox/jest-preset.cjs +29 -0
- package/dist-lib/dev-bundle-resolution.cjs +290 -0
- package/dist-lib/home-paths.cjs +638 -0
- package/dist-lib/host/bridge-host.cjs +10903 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/replacement-module-handler.cjs +469 -0
- package/dist-lib/host/websocket-proxy.cjs +298 -0
- package/dist-lib/index.cjs +4268 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/jump-to-source-native.cjs +112 -0
- package/dist-lib/menu.cjs +646 -0
- package/dist-lib/menu.mjs +604 -0
- package/dist-lib/metro-fingerprint-registry.cjs +186 -0
- package/dist-lib/metro-fingerprint-registry.mjs +156 -0
- package/dist-lib/metro-production-bundle.cjs +329 -0
- package/dist-lib/metro-production-bundle.mjs +295 -0
- package/dist-lib/metro.cjs +904 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/react-native-host-modules.cjs +51 -0
- package/dist-lib/react-native-host-modules.mjs +25 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
- package/dist-lib/sdk.cjs +2433 -0
- package/dist-lib/sdk.mjs +2336 -0
- package/dist-lib/skills.cjs +39132 -0
- package/dist-lib/swift-submit.cjs +92 -0
- package/dist-lib/swift-submit.mjs +65 -0
- package/dist-lib/swift.cjs +1646 -0
- package/dist-lib/vite.cjs +1591 -0
- package/package.json +290 -0
- package/scripts/dev-server-scanner.ts +1291 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/skills/contrast/SKILL.md +165 -0
- package/skills/rnx-debug/SKILL.md +433 -0
- package/skills/rnx-perf/SKILL.md +399 -0
- package/skills/rnx-setup/SKILL.md +172 -0
- package/skills/rnx-test/SKILL.md +321 -0
- package/skills/rnx-visual/SKILL.md +126 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +580 -0
- package/src/app-url.ts +39 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +235 -0
- package/src/backend-origin.ts +70 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +175 -0
- package/src/bridge-contract-input.ts +502 -0
- package/src/bridge-contract.ts +558 -0
- package/src/browser-cache.ts +16 -0
- package/src/capture-contract.ts +587 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +40 -0
- package/src/cloud-contract.ts +274 -0
- package/src/cloud.ts +762 -0
- package/src/codex-client.ts +234 -0
- package/src/config.ts +132 -0
- package/src/connect.ts +101 -0
- package/src/dev-bundle-resolution.ts +346 -0
- package/src/dev-server-open.ts +198 -0
- package/src/disk-cleanup.ts +540 -0
- package/src/flow-timeout-scale.ts +15 -0
- package/src/home-paths.ts +806 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2788 -0
- package/src/host/fetch-proxy-handler.ts +449 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/plane-host.ts +203 -0
- package/src/host/replacement-module-handler.ts +158 -0
- package/src/host/websocket-proxy.ts +315 -0
- package/src/index.ts +71 -0
- package/src/jump-to-source-babel.ts +135 -0
- package/src/jump-to-source-native.ts +102 -0
- package/src/menu.ts +777 -0
- package/src/metro-fingerprint-registry.ts +302 -0
- package/src/metro-plugin.ts +341 -0
- package/src/metro-production-bundle.ts +471 -0
- package/src/native-dev-bundle-url.ts +118 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/playwright-host-log.ts +16 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +123 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/react-native-host-modules.ts +26 -0
- package/src/render-mode.ts +27 -0
- package/src/rolldown-plugin-swift.ts +240 -0
- package/src/runtime-assets.ts +233 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +514 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +15 -0
- package/src/settings-bundle.ts +418 -0
- package/src/shell-bridge-identity.ts +15 -0
- package/src/sim-client.ts +697 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/swift-submit.ts +185 -0
- package/src/swift.ts +7 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +264 -0
- package/src/vite-plugin-swift.ts +620 -0
- package/src/vite-plugin.ts +1946 -0
- 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, '&')
|
|
171
|
+
.replace(/</g, '<')
|
|
172
|
+
.replace(/>/g, '>')
|
|
173
|
+
.replace(/"/g, '"')
|
|
174
|
+
.replace(/'/g, ''')
|
|
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
|
+
}
|