rnxsim 0.0.0 → 0.1.313
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 +21 -0
- package/README.md +512 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +39 -0
- package/detox/expectations.ts +537 -0
- package/detox/gestures.ts +480 -0
- package/detox/index.ts +2118 -0
- package/detox/jest-preset.cjs +51 -0
- package/detox/jest-setup-after-env.cjs +18 -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.ts +43 -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-cli/bin.js +15 -0
- package/dist-cli/chunks/agent-XZ2KTPCU.js +48 -0
- package/dist-cli/chunks/agent-wrapper-JJYYW2WH.js +17 -0
- package/dist-cli/chunks/app-fonts-IXRNQG6B.js +4 -0
- package/dist-cli/chunks/assert-54T5SK5F.js +49 -0
- package/dist-cli/chunks/auth-FI5UDI45.js +12 -0
- package/dist-cli/chunks/beta-JV6UKADW.js +4 -0
- package/dist-cli/chunks/camera-VL73YIKP.js +22 -0
- package/dist-cli/chunks/chunk-2D2UPBBR.js +4 -0
- package/dist-cli/chunks/chunk-2YR5BGA5.js +5 -0
- package/dist-cli/chunks/chunk-3NV2NCNX.js +4 -0
- package/dist-cli/chunks/chunk-46EUUFJ5.js +4 -0
- package/dist-cli/chunks/chunk-4NPPOV2N.js +5 -0
- package/dist-cli/chunks/chunk-5DHC6KHQ.js +7 -0
- package/dist-cli/chunks/chunk-5TEF3ET3.js +74 -0
- package/dist-cli/chunks/chunk-5TPRP5QT.js +4 -0
- package/dist-cli/chunks/chunk-5YJCOWCH.js +4 -0
- package/dist-cli/chunks/chunk-7GN3LVWB.js +4 -0
- package/dist-cli/chunks/chunk-BBULZ7CG.js +568 -0
- package/dist-cli/chunks/chunk-BTWORNNG.js +71 -0
- package/dist-cli/chunks/chunk-D4FFVGI5.js +5 -0
- package/dist-cli/chunks/chunk-DCEMHR2Y.js +4 -0
- package/dist-cli/chunks/chunk-DZS6WPUI.js +4 -0
- package/dist-cli/chunks/chunk-F5ZRSS3C.js +3 -0
- package/dist-cli/chunks/chunk-FSUYIVJ6.js +9 -0
- package/dist-cli/chunks/chunk-G2WW6L2C.js +23 -0
- package/dist-cli/chunks/chunk-GASE6UBA.js +4 -0
- package/dist-cli/chunks/chunk-GGRX24GF.js +142 -0
- package/dist-cli/chunks/chunk-HI5TFJWN.js +5 -0
- package/dist-cli/chunks/chunk-IJ5CAZZC.js +5 -0
- package/dist-cli/chunks/chunk-IJO63TDP.js +4 -0
- package/dist-cli/chunks/chunk-KTHV3RUS.js +26 -0
- package/dist-cli/chunks/chunk-LF2ZVT7O.js +6 -0
- package/dist-cli/chunks/chunk-MJRLLB4R.js +38 -0
- package/dist-cli/chunks/chunk-NFK7T35W.js +4 -0
- package/dist-cli/chunks/chunk-NMF2ZMZQ.js +15 -0
- package/dist-cli/chunks/chunk-OHAZNXLK.js +4 -0
- package/dist-cli/chunks/chunk-OVFJFXUD.js +101 -0
- package/dist-cli/chunks/chunk-OZSSI4WN.js +28 -0
- package/dist-cli/chunks/chunk-QKDWYITG.js +25 -0
- package/dist-cli/chunks/chunk-RSZWCKNT.js +30 -0
- package/dist-cli/chunks/chunk-RTN5C5RL.js +4 -0
- package/dist-cli/chunks/chunk-TIVZIMMW.js +4 -0
- package/dist-cli/chunks/chunk-TZFFR3SD.js +7 -0
- package/dist-cli/chunks/chunk-UC6U3MML.js +5 -0
- package/dist-cli/chunks/chunk-VFCMSYZK.js +6 -0
- package/dist-cli/chunks/chunk-VNQEB4L7.js +5 -0
- package/dist-cli/chunks/chunk-VZXWHRUZ.js +983 -0
- package/dist-cli/chunks/chunk-W6K4EFPH.js +107 -0
- package/dist-cli/chunks/chunk-WEXDAC74.js +4 -0
- package/dist-cli/chunks/chunk-WF3T4SVI.js +14 -0
- package/dist-cli/chunks/chunk-WINYQ44O.js +4 -0
- package/dist-cli/chunks/chunk-WMIIKMGK.js +10 -0
- package/dist-cli/chunks/chunk-WUSWBCWA.js +507 -0
- package/dist-cli/chunks/chunk-WWZIXIRD.js +4 -0
- package/dist-cli/chunks/chunk-XEVZYVIW.js +766 -0
- package/dist-cli/chunks/chunk-YDGQTMQL.js +4 -0
- package/dist-cli/chunks/chunk-YIFT42WN.js +4 -0
- package/dist-cli/chunks/chunk-ZMJD5GEC.js +125 -0
- package/dist-cli/chunks/cleanup-P27PA6JI.js +25 -0
- package/dist-cli/chunks/cli-version-WWLPBDQ7.js +4 -0
- package/dist-cli/chunks/compat-ZD65FED3.js +20 -0
- package/dist-cli/chunks/config-XMJRNM2A.js +13 -0
- package/dist-cli/chunks/control-KMIQT3QP.js +4 -0
- package/dist-cli/chunks/daemon-G2ME7NLB.js +4 -0
- package/dist-cli/chunks/debug-PT4HOP7N.js +200 -0
- package/dist-cli/chunks/desktop-S3FG72AK.js +31 -0
- package/dist-cli/chunks/detox-B3D4IFCN.js +52 -0
- package/dist-cli/chunks/device-XBNDSB2R.js +18 -0
- package/dist-cli/chunks/diagnose-HMQXJE5N.js +43 -0
- package/dist-cli/chunks/disk-cleanup-BLCZ5BSZ.js +4 -0
- package/dist-cli/chunks/drivers-LDECZGP2.js +4 -0
- package/dist-cli/chunks/film-BJGTBYZB.js +74 -0
- package/dist-cli/chunks/flow-UEQNVTU7.js +4 -0
- package/dist-cli/chunks/help-T5FYSVGB.js +4 -0
- package/dist-cli/chunks/hidden-runtime-alias-ANOYADHM.js +4 -0
- package/dist-cli/chunks/home-paths-GT3LFNOR.js +4 -0
- package/dist-cli/chunks/inspect-ZA6XF5LD.js +4 -0
- package/dist-cli/chunks/install-desktop-TIMUDHPL.js +4 -0
- package/dist-cli/chunks/login-FJ737MWG.js +28 -0
- package/dist-cli/chunks/logout-ZCNMMHMY.js +4 -0
- package/dist-cli/chunks/maestro-SZTNKLDF.js +104 -0
- package/dist-cli/chunks/maestro-generate-DCFAIZ4H.js +36 -0
- package/dist-cli/chunks/mode-GRMQCRXR.js +26 -0
- package/dist-cli/chunks/optional-demo-registry-W36EWFFB.js +4 -0
- package/dist-cli/chunks/perf-QYBAAUZG.js +48 -0
- package/dist-cli/chunks/permissions-3QCQ6VF4.js +13 -0
- package/dist-cli/chunks/record-QPPC2S4E.js +72 -0
- package/dist-cli/chunks/report-issue-7NMFP4HK.js +29 -0
- package/dist-cli/chunks/runtime-XOAXMSTU.js +4 -0
- package/dist-cli/chunks/screenshot-command-67AECJFB.js +159 -0
- package/dist-cli/chunks/screenshot-layers-ASWBYPJL.js +20 -0
- package/dist-cli/chunks/screenshots-capture-PXA3HFQK.js +42 -0
- package/dist-cli/chunks/serve-BI2NBAXG.js +44 -0
- package/dist-cli/chunks/setup-7DWPMRSB.js +32 -0
- package/dist-cli/chunks/skills-S3Y22TUA.js +50 -0
- package/dist-cli/chunks/storage-XUIMJWAJ.js +37 -0
- package/dist-cli/chunks/store-JTHEJLAZ.js +4 -0
- package/dist-cli/chunks/telemetry-ZYJGD2DB.js +4 -0
- package/dist-cli/chunks/timeline-TMPLQPSP.js +24 -0
- package/dist-cli/chunks/upgrade-7HDSIM7K.js +7 -0
- package/dist-cli/chunks/upload-GMSZPWM6.js +4 -0
- package/dist-cli/chunks/version-HOCHZ37L.js +6 -0
- package/dist-cli/chunks/web-DG3WBYD3.js +4 -0
- package/dist-cli/chunks/what-happened-XFVUTZR7.js +25 -0
- package/dist-lib/agent-daemon-client.cjs +468 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +725 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +195 -0
- package/dist-lib/backend-origin.cjs +70 -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/cli-constants.cjs +32 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +2863 -0
- package/dist-lib/detox/jest-preset.cjs +51 -0
- package/dist-lib/dev-bundle-resolution.cjs +223 -0
- package/dist-lib/home-paths.cjs +421 -0
- package/dist-lib/host/bridge-host.cjs +5600 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +373 -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/websocket-proxy.cjs +214 -0
- package/dist-lib/index.cjs +513 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/menu.cjs +620 -0
- package/dist-lib/menu.mjs +581 -0
- package/dist-lib/metro.cjs +297 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1034 -0
- package/dist-lib/sdk.cjs +2333 -0
- package/dist-lib/sdk.mjs +2240 -0
- package/dist-lib/skills.cjs +23922 -0
- package/dist-lib/vite.cjs +358 -0
- package/package.json +189 -18
- package/scripts/dev-server-scanner.ts +941 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/scripts/postinstall.cjs +3 -0
- package/scripts/preuninstall.cjs +118 -0
- package/skills/contrast/SKILL.md +171 -0
- package/skills/rnx-debug/SKILL.md +425 -0
- package/skills/rnx-perf/SKILL.md +295 -0
- package/skills/rnx-setup/SKILL.md +160 -0
- package/skills/rnx-test/SKILL.md +292 -0
- package/skills/rnx-visual/SKILL.md +122 -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 +573 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +204 -0
- package/src/backend-origin.ts +49 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +171 -0
- package/src/browser-cache.ts +16 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +32 -0
- package/src/codex-client.ts +215 -0
- package/src/config.ts +134 -0
- package/src/dev-bundle-resolution.ts +236 -0
- package/src/disk-cleanup.ts +466 -0
- package/src/home-paths.ts +552 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2626 -0
- package/src/host/fetch-proxy-handler.ts +397 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/websocket-proxy.ts +210 -0
- package/src/index.ts +12 -705
- package/src/jump-to-source-babel.ts +135 -0
- package/src/menu.ts +734 -0
- package/src/metro-plugin.ts +139 -0
- package/src/native-dev-bundle-url.ts +96 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +92 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/render-mode.ts +27 -0
- package/src/runtime-assets.ts +228 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +454 -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 +7 -0
- package/src/settings-bundle.ts +418 -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/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +190 -0
- package/src/vite-plugin.ts +1750 -0
- package/src/worklets-babel.ts +134 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
// canonical filesystem layout under ~/.sootsim/. shared by the CLI and the
|
|
2
|
+
// electron main process so every surface agrees where runtimes, the daemon
|
|
3
|
+
// lockfile, and caches live.
|
|
4
|
+
//
|
|
5
|
+
// ~/.sootsim/
|
|
6
|
+
// ├── runtimes/
|
|
7
|
+
// │ ├── <version>/ unpacked dist/ of sootsim-engine
|
|
8
|
+
// │ └── active text file with the active version string (win-safe
|
|
9
|
+
// │ alternative to a symlink)
|
|
10
|
+
// ├── electron/
|
|
11
|
+
// │ └── <version>/ pinned electron binary (future: playwright-style)
|
|
12
|
+
// ├── profiles/
|
|
13
|
+
// │ └── profiles.json storage profile metadata
|
|
14
|
+
// ├── cache/
|
|
15
|
+
// │ └── sootsim-runtime-<version>.tar.gz
|
|
16
|
+
// ├── daemon.json lockfile: pid, ports, active runtime, heartbeat
|
|
17
|
+
// ├── dev-bridge.json live Vite dev bridge pointer for local CLI commands
|
|
18
|
+
// ├── runtime-upgraded.json one-shot notice written by the daemon's
|
|
19
|
+
// │ background updater, consumed (printed + deleted)
|
|
20
|
+
// │ by the next interactive CLI run
|
|
21
|
+
// ├── automatic-cleanup-v1.json completion marker for the one-time safe
|
|
22
|
+
// │ cleanup introduced for legacy unbounded data
|
|
23
|
+
// └── config.json user prefs: update channel, cdn origin override,
|
|
24
|
+
// telemetry, engine settings
|
|
25
|
+
|
|
26
|
+
import { randomUUID } from 'node:crypto'
|
|
27
|
+
import fs from 'node:fs'
|
|
28
|
+
import { homedir } from 'node:os'
|
|
29
|
+
import path from 'node:path'
|
|
30
|
+
|
|
31
|
+
export const SOOTSIM_HOME_ENV = 'SOOTSIM_HOME'
|
|
32
|
+
export const ACTIVE_RUNTIME_FILE = 'active'
|
|
33
|
+
export const DAEMON_LOCKFILE = 'daemon.json'
|
|
34
|
+
export const DEV_BRIDGE_LOCKFILE = 'dev-bridge.json'
|
|
35
|
+
export const CONFIG_FILE = 'config.json'
|
|
36
|
+
export const DAEMON_HEARTBEAT_STALE_MS = 30_000
|
|
37
|
+
|
|
38
|
+
export function sootsimHomeDir(): string {
|
|
39
|
+
const override = process.env[SOOTSIM_HOME_ENV]
|
|
40
|
+
if (override && override.length > 0) return path.resolve(override)
|
|
41
|
+
return path.join(homedir(), '.sootsim')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// detect when sootsim is running from a source checkout (the Contrast monorepo)
|
|
45
|
+
// rather than a published npm install. used to skip auto-install of the
|
|
46
|
+
// persistent launchd / systemd agent: dev shells shouldn't register an agent
|
|
47
|
+
// whose Program path points at workspace artifacts that change between
|
|
48
|
+
// sessions, and whose served engine assets are the stale prod build instead
|
|
49
|
+
// of the live `bun dev:sootsim` output.
|
|
50
|
+
//
|
|
51
|
+
// overrides:
|
|
52
|
+
// SOOTSIM_DEV=1 / SOOTSIM_DEV=0 force the answer
|
|
53
|
+
// SOOTSIM_FORCE_DAEMON_INSTALL=1 pretend prod even from a dev
|
|
54
|
+
// checkout, for exercising the
|
|
55
|
+
// install path from this repo
|
|
56
|
+
//
|
|
57
|
+
// signal: realpath of process.argv[1] lands inside a `packages/sootsim/`
|
|
58
|
+
// directory. workspace bin symlinks (`node_modules/.bin/sootsim` →
|
|
59
|
+
// `packages/sootsim/dist-cli/bin.js`) and bun-direct invocations
|
|
60
|
+
// (`bun packages/sootsim/cli/bin.ts ...`) both match; published installs
|
|
61
|
+
// resolve under `node_modules/sootsim/` instead.
|
|
62
|
+
export function isSootsimDevCheckout(): boolean {
|
|
63
|
+
if (process.env.SOOTSIM_FORCE_DAEMON_INSTALL === '1') return false
|
|
64
|
+
const env = process.env.SOOTSIM_DEV
|
|
65
|
+
if (env === '1' || env === 'true') return true
|
|
66
|
+
if (env === '0' || env === 'false') return false
|
|
67
|
+
const argv1 = process.argv[1]
|
|
68
|
+
if (!argv1) return false
|
|
69
|
+
try {
|
|
70
|
+
const real = fs.realpathSync(argv1)
|
|
71
|
+
return real.includes(`${path.sep}packages${path.sep}sootsim${path.sep}`)
|
|
72
|
+
} catch {
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// detect the human's dev workstation by the env markers the developer sets in
|
|
78
|
+
// their shell profile (`IS_TAMAGUI_DEV=1` in ~/.zshrc). distinct from
|
|
79
|
+
// isSootsimDevCheckout, which keys on the *binary path*: a globally-installed
|
|
80
|
+
// `sootsim` binary on the dev machine would NOT match the checkout path, yet a
|
|
81
|
+
// persistent daemon registered there is just as dangerous. it runs in the
|
|
82
|
+
// background carrying the workstation's dev env (dev auth tokens, prod-pointing
|
|
83
|
+
// vars), and when the live `bun dev` stack already owns the bridge port it
|
|
84
|
+
// silently lands on a fallback port and shadows the real one with stale assets
|
|
85
|
+
// — the "I was on the daemon on port 7-something" footgun.
|
|
86
|
+
//
|
|
87
|
+
// override: SOOTSIM_FORCE_DAEMON_INSTALL=1 to run the daemon here anyway.
|
|
88
|
+
export function isDevWorkstation(): boolean {
|
|
89
|
+
if (process.env.SOOTSIM_FORCE_DAEMON_INSTALL === '1') return false
|
|
90
|
+
return process.env.IS_TAMAGUI_DEV === '1'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// the persistent launchd / systemd daemon must never auto-install or run on a
|
|
94
|
+
// dev checkout or a dev workstation. either makes the background agent point at
|
|
95
|
+
// moving workspace artifacts / dev env and serve stale assets that shadow the
|
|
96
|
+
// live dev stack. setup and the launchd-spawned `sootsim serve` boot consult
|
|
97
|
+
// this; a foreground `sootsim serve` the dev starts
|
|
98
|
+
// by hand is intentional and stays allowed.
|
|
99
|
+
export function shouldSkipPersistentDaemon(): boolean {
|
|
100
|
+
return isSootsimDevCheckout() || isDevWorkstation()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function runtimesDir(): string {
|
|
104
|
+
return path.join(sootsimHomeDir(), 'runtimes')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function runtimeDir(version: string): string {
|
|
108
|
+
return path.join(runtimesDir(), version)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function activeRuntimeFile(): string {
|
|
112
|
+
return path.join(runtimesDir(), ACTIVE_RUNTIME_FILE)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function electronDir(): string {
|
|
116
|
+
return path.join(sootsimHomeDir(), 'electron')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function electronUserDataDir(): string {
|
|
120
|
+
return path.join(electronDir(), 'userData')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function electronVersionDir(version: string): string {
|
|
124
|
+
return path.join(electronDir(), version)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function profilesDir(): string {
|
|
128
|
+
return path.join(sootsimHomeDir(), 'profiles')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// the launchd-managed daemon spawns ProgramArguments[0] directly, and macOS
|
|
132
|
+
// Background Task Management attributes the entry to whoever code-signed
|
|
133
|
+
// that binary. pointing launchd at bun directly makes Login Items say
|
|
134
|
+
// "software from Jarred Sumner" (bun's signer); wrapping the invocation in
|
|
135
|
+
// an ad-hoc-signed .app bundle here gives BTM a CFBundleDisplayName to
|
|
136
|
+
// read instead.
|
|
137
|
+
export function daemonAppDir(): string {
|
|
138
|
+
return path.join(sootsimHomeDir(), 'daemon-app')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function daemonAppBundlePath(): string {
|
|
142
|
+
return path.join(daemonAppDir(), 'SootSim Daemon.app')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function daemonAppLauncherPath(): string {
|
|
146
|
+
return path.join(daemonAppBundlePath(), 'Contents', 'MacOS', 'sootsim-daemon')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function cacheDir(): string {
|
|
150
|
+
return path.join(sootsimHomeDir(), 'cache')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function daemonLockfilePath(): string {
|
|
154
|
+
return path.join(sootsimHomeDir(), DAEMON_LOCKFILE)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function devBridgeLockfilePath(): string {
|
|
158
|
+
return path.join(sootsimHomeDir(), DEV_BRIDGE_LOCKFILE)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function configFilePath(): string {
|
|
162
|
+
return path.join(sootsimHomeDir(), CONFIG_FILE)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- shared sootsim config -----------------------------------------------
|
|
166
|
+
//
|
|
167
|
+
// ~/.sootsim/config.json is the single user-level config file every sootsim
|
|
168
|
+
// surface (cli, electron renderer, dev browser via electron ipc) agrees on.
|
|
169
|
+
//
|
|
170
|
+
// privacy preferences live in the nested settings object so the cli, desktop
|
|
171
|
+
// main process, renderer, daemon-served browser, and settings UI all read and
|
|
172
|
+
// write one persisted value per choice.
|
|
173
|
+
|
|
174
|
+
export interface SharedConfig {
|
|
175
|
+
telemetry?: boolean
|
|
176
|
+
settings?: Record<string, unknown>
|
|
177
|
+
[key: string]: unknown
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function readSharedConfig(): SharedConfig {
|
|
181
|
+
try {
|
|
182
|
+
const raw = fs.readFileSync(configFilePath(), 'utf8')
|
|
183
|
+
const parsed = JSON.parse(raw) as SharedConfig
|
|
184
|
+
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
185
|
+
} catch {
|
|
186
|
+
return {}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** merge `patch` into the shared config and atomically write to disk. nested
|
|
191
|
+
* objects (currently just `settings`) are shallow-merged so partial writes
|
|
192
|
+
* don't clobber unrelated fields. returns the new full snapshot. */
|
|
193
|
+
export function writeSharedConfig(patch: Partial<SharedConfig>): SharedConfig {
|
|
194
|
+
ensureSootsimHome()
|
|
195
|
+
const current = readSharedConfig()
|
|
196
|
+
const next: SharedConfig = { ...current, ...patch }
|
|
197
|
+
if (patch.settings && typeof patch.settings === 'object') {
|
|
198
|
+
next.settings = {
|
|
199
|
+
...(current.settings && typeof current.settings === 'object'
|
|
200
|
+
? current.settings
|
|
201
|
+
: {}),
|
|
202
|
+
...patch.settings,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const tmp = `${configFilePath()}.tmp`
|
|
206
|
+
fs.writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, 'utf8')
|
|
207
|
+
fs.renameSync(tmp, configFilePath())
|
|
208
|
+
return next
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface PrivacyPreferences {
|
|
212
|
+
productAnalytics: boolean
|
|
213
|
+
crashReports: boolean
|
|
214
|
+
configured: boolean
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function readPrivacyPreferences(): PrivacyPreferences {
|
|
218
|
+
const settings = readSharedConfig().settings
|
|
219
|
+
const productAnalytics = settings?.productAnalytics
|
|
220
|
+
const crashReports = settings?.crashReports
|
|
221
|
+
return {
|
|
222
|
+
productAnalytics: typeof productAnalytics === 'boolean' ? productAnalytics : true,
|
|
223
|
+
crashReports: typeof crashReports === 'boolean' ? crashReports : true,
|
|
224
|
+
configured:
|
|
225
|
+
typeof productAnalytics === 'boolean' && typeof crashReports === 'boolean',
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function writePrivacyPreferences(
|
|
230
|
+
preferences: Pick<PrivacyPreferences, 'productAnalytics' | 'crashReports'>,
|
|
231
|
+
): void {
|
|
232
|
+
writeSharedConfig({
|
|
233
|
+
settings: {
|
|
234
|
+
productAnalytics: preferences.productAnalytics,
|
|
235
|
+
crashReports: preferences.crashReports,
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// stable per-machine anonymous telemetry id. every signed-out cli used to
|
|
241
|
+
// report as the single literal distinct_id 'anonymous-cli', which merged all
|
|
242
|
+
// first-run users into one posthog person and made "a new user's login hung"
|
|
243
|
+
// impossible to see. created lazily on first use, persisted in config.json.
|
|
244
|
+
export function readOrCreateAnonymousId(): string {
|
|
245
|
+
const existing = readSharedConfig().anonymousId
|
|
246
|
+
if (typeof existing === 'string' && existing.trim()) return existing
|
|
247
|
+
const created = `anon-cli-${randomUUID()}`
|
|
248
|
+
writeSharedConfig({ anonymousId: created })
|
|
249
|
+
return created
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// --- background-upgrade notice --------------------------------------------
|
|
253
|
+
//
|
|
254
|
+
// the daemon auto-updates the engine runtime in the background (hourly), so
|
|
255
|
+
// the only place its "updated to vX" line lands is the daemon's own stderr,
|
|
256
|
+
// which nobody reads. it also drops a one-shot notice file here; the next
|
|
257
|
+
// interactive CLI invocation prints it once and deletes it, so users learn
|
|
258
|
+
// their engine moved without having to run `sootsim version`.
|
|
259
|
+
|
|
260
|
+
export interface RuntimeUpgradeNotice {
|
|
261
|
+
from: string | null
|
|
262
|
+
to: string
|
|
263
|
+
at: number
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function runtimeUpgradeNoticePath(): string {
|
|
267
|
+
return path.join(sootsimHomeDir(), 'runtime-upgraded.json')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function writeRuntimeUpgradeNotice(notice: RuntimeUpgradeNotice): void {
|
|
271
|
+
ensureSootsimHome()
|
|
272
|
+
const tmp = `${runtimeUpgradeNoticePath()}.tmp`
|
|
273
|
+
fs.writeFileSync(tmp, `${JSON.stringify(notice)}\n`, 'utf8')
|
|
274
|
+
fs.renameSync(tmp, runtimeUpgradeNoticePath())
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** read + delete the pending upgrade notice, or null when none is pending.
|
|
278
|
+
* deleting first-read keeps the banner one-shot even across racing CLIs. */
|
|
279
|
+
export function consumeRuntimeUpgradeNotice(): RuntimeUpgradeNotice | null {
|
|
280
|
+
try {
|
|
281
|
+
const raw = fs.readFileSync(runtimeUpgradeNoticePath(), 'utf8')
|
|
282
|
+
fs.unlinkSync(runtimeUpgradeNoticePath())
|
|
283
|
+
const parsed = JSON.parse(raw) as Partial<RuntimeUpgradeNotice>
|
|
284
|
+
if (typeof parsed?.to !== 'string' || !parsed.to) return null
|
|
285
|
+
return {
|
|
286
|
+
from: typeof parsed.from === 'string' ? parsed.from : null,
|
|
287
|
+
to: parsed.to,
|
|
288
|
+
at: typeof parsed.at === 'number' ? parsed.at : 0,
|
|
289
|
+
}
|
|
290
|
+
} catch {
|
|
291
|
+
return null
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function ensureSootsimHome(): void {
|
|
296
|
+
fs.mkdirSync(sootsimHomeDir(), { recursive: true })
|
|
297
|
+
fs.mkdirSync(runtimesDir(), { recursive: true })
|
|
298
|
+
fs.mkdirSync(electronDir(), { recursive: true })
|
|
299
|
+
fs.mkdirSync(profilesDir(), { recursive: true })
|
|
300
|
+
fs.mkdirSync(cacheDir(), { recursive: true })
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** read the active runtime version string, or null if none is selected. */
|
|
304
|
+
export function readActiveRuntime(): string | null {
|
|
305
|
+
try {
|
|
306
|
+
const value = fs.readFileSync(activeRuntimeFile(), 'utf8').trim()
|
|
307
|
+
return value.length > 0 ? value : null
|
|
308
|
+
} catch {
|
|
309
|
+
return null
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** set the active runtime version. caller is responsible for verifying the
|
|
314
|
+
* version actually exists on disk before calling. */
|
|
315
|
+
export function writeActiveRuntime(version: string): void {
|
|
316
|
+
fs.mkdirSync(runtimesDir(), { recursive: true })
|
|
317
|
+
fs.writeFileSync(activeRuntimeFile(), `${version}\n`, 'utf8')
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// listInstalledRuntimes + compareSemver live in @contrast/runtime-delivery:
|
|
321
|
+
// they are delivery machinery (sorting the hosted catalog, deciding whether
|
|
322
|
+
// channel latest is newer than active), shared with contrast, and this module
|
|
323
|
+
// stays free of that dependency so every published `sootsim/*` export that
|
|
324
|
+
// resolves through it keeps working with no bundling requirement.
|
|
325
|
+
|
|
326
|
+
/** absolute path to the active runtime's directory, or null if none active
|
|
327
|
+
* or the active version is no longer installed. */
|
|
328
|
+
export function activeRuntimeDir(): string | null {
|
|
329
|
+
const version = readActiveRuntime()
|
|
330
|
+
if (!version) return null
|
|
331
|
+
const dir = runtimeDir(version)
|
|
332
|
+
try {
|
|
333
|
+
if (fs.statSync(dir).isDirectory()) return dir
|
|
334
|
+
} catch {}
|
|
335
|
+
return null
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// --- daemon lockfile ----------------------------------------------------
|
|
339
|
+
|
|
340
|
+
export interface DaemonLockfile {
|
|
341
|
+
/** sootsim cli/daemon version that wrote the lockfile. bumped whenever
|
|
342
|
+
* the lockfile shape changes so readers can gate on it. */
|
|
343
|
+
schema: 1
|
|
344
|
+
pid: number
|
|
345
|
+
/** platform — useful when one home dir is shared across platforms via NFS. */
|
|
346
|
+
platform: NodeJS.Platform
|
|
347
|
+
/** ws bridge port (where cli + electron open control connections). */
|
|
348
|
+
bridgePort: number
|
|
349
|
+
/** http runtime server port (where electron loads the renderer from). */
|
|
350
|
+
runtimePort: number
|
|
351
|
+
/** active runtime version at boot, or null if the daemon booted with no
|
|
352
|
+
* runtime installed. updated live when the user runs `sootsim runtime use`. */
|
|
353
|
+
activeRuntime: string | null
|
|
354
|
+
/** absolute path to the active runtime's dist directory, or null. */
|
|
355
|
+
activeRuntimeDir: string | null
|
|
356
|
+
/** repo-selected runtime versions currently served to connected sims. */
|
|
357
|
+
servedRuntimes?: string[]
|
|
358
|
+
/** epoch-ms of daemon start. */
|
|
359
|
+
startedAt: number
|
|
360
|
+
/** epoch-ms of last heartbeat. daemons update this every ~5s; readers
|
|
361
|
+
* treat the lockfile as stale if now - heartbeatAt > DAEMON_HEARTBEAT_STALE_MS. */
|
|
362
|
+
heartbeatAt: number
|
|
363
|
+
/** true while the daemon is still fetching/activating its runtime on first
|
|
364
|
+
* boot. clients (electron splash, cli) should wait for this to become
|
|
365
|
+
* false before treating the daemon as ready to serve. */
|
|
366
|
+
bootstrapping?: boolean
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export interface DevBridgeLockfile {
|
|
370
|
+
schema: 1
|
|
371
|
+
pid: number
|
|
372
|
+
platform: NodeJS.Platform
|
|
373
|
+
bridgePort: number
|
|
374
|
+
runtimePort: number
|
|
375
|
+
/** http port of the vite shell dev server that owns this bridge (e.g. 5173).
|
|
376
|
+
* optional: lockfiles written before this field existed still parse, and
|
|
377
|
+
* readers fall back to the default shell URL. this is what lets the CLI
|
|
378
|
+
* resolve the shell base URL and the bridge port from the SAME world
|
|
379
|
+
* instead of mixing dev bridge + daemon runtime (the split-brain bug). */
|
|
380
|
+
shellPort?: number
|
|
381
|
+
cwd: string
|
|
382
|
+
startedAt: number
|
|
383
|
+
heartbeatAt: number
|
|
384
|
+
source: 'vite-dev'
|
|
385
|
+
/** repo-selected runtime versions currently served to connected sims. */
|
|
386
|
+
servedRuntimes?: string[]
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const DAEMON_LOCKFILE_MAX_BYTES = 16 * 1024
|
|
390
|
+
|
|
391
|
+
export function readDaemonLockfile(): DaemonLockfile | null {
|
|
392
|
+
try {
|
|
393
|
+
// cap the read so a junk/large file on the lockfile path can't OOM
|
|
394
|
+
// the CLI when someone has been messing with ~/.sootsim/.
|
|
395
|
+
const fd = fs.openSync(daemonLockfilePath(), 'r')
|
|
396
|
+
try {
|
|
397
|
+
const buf = Buffer.alloc(DAEMON_LOCKFILE_MAX_BYTES)
|
|
398
|
+
const bytesRead = fs.readSync(fd, buf, 0, DAEMON_LOCKFILE_MAX_BYTES, 0)
|
|
399
|
+
const raw = buf.subarray(0, bytesRead).toString('utf8')
|
|
400
|
+
const parsed = JSON.parse(raw) as Partial<DaemonLockfile>
|
|
401
|
+
if (
|
|
402
|
+
parsed &&
|
|
403
|
+
parsed.schema === 1 &&
|
|
404
|
+
typeof parsed.pid === 'number' &&
|
|
405
|
+
typeof parsed.bridgePort === 'number' &&
|
|
406
|
+
typeof parsed.runtimePort === 'number' &&
|
|
407
|
+
typeof parsed.startedAt === 'number' &&
|
|
408
|
+
typeof parsed.heartbeatAt === 'number' &&
|
|
409
|
+
(parsed.servedRuntimes === undefined ||
|
|
410
|
+
(Array.isArray(parsed.servedRuntimes) &&
|
|
411
|
+
parsed.servedRuntimes.every((version) => typeof version === 'string')))
|
|
412
|
+
) {
|
|
413
|
+
return parsed as DaemonLockfile
|
|
414
|
+
}
|
|
415
|
+
return null
|
|
416
|
+
} finally {
|
|
417
|
+
fs.closeSync(fd)
|
|
418
|
+
}
|
|
419
|
+
} catch {
|
|
420
|
+
return null
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function readDevBridgeLockfile(): DevBridgeLockfile | null {
|
|
425
|
+
try {
|
|
426
|
+
const fd = fs.openSync(devBridgeLockfilePath(), 'r')
|
|
427
|
+
try {
|
|
428
|
+
const buf = Buffer.alloc(DAEMON_LOCKFILE_MAX_BYTES)
|
|
429
|
+
const bytesRead = fs.readSync(fd, buf, 0, DAEMON_LOCKFILE_MAX_BYTES, 0)
|
|
430
|
+
const raw = buf.subarray(0, bytesRead).toString('utf8')
|
|
431
|
+
const parsed = JSON.parse(raw) as Partial<DevBridgeLockfile>
|
|
432
|
+
if (
|
|
433
|
+
parsed &&
|
|
434
|
+
parsed.schema === 1 &&
|
|
435
|
+
parsed.source === 'vite-dev' &&
|
|
436
|
+
typeof parsed.pid === 'number' &&
|
|
437
|
+
typeof parsed.bridgePort === 'number' &&
|
|
438
|
+
typeof parsed.runtimePort === 'number' &&
|
|
439
|
+
typeof parsed.cwd === 'string' &&
|
|
440
|
+
typeof parsed.startedAt === 'number' &&
|
|
441
|
+
typeof parsed.heartbeatAt === 'number' &&
|
|
442
|
+
(parsed.shellPort === undefined || typeof parsed.shellPort === 'number') &&
|
|
443
|
+
(parsed.servedRuntimes === undefined ||
|
|
444
|
+
(Array.isArray(parsed.servedRuntimes) &&
|
|
445
|
+
parsed.servedRuntimes.every((version) => typeof version === 'string')))
|
|
446
|
+
) {
|
|
447
|
+
return parsed as DevBridgeLockfile
|
|
448
|
+
}
|
|
449
|
+
return null
|
|
450
|
+
} finally {
|
|
451
|
+
fs.closeSync(fd)
|
|
452
|
+
}
|
|
453
|
+
} catch {
|
|
454
|
+
return null
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** true when the lockfile exists, the named pid is alive, and the heartbeat
|
|
459
|
+
* is recent. callers should reach for this before trusting any of the
|
|
460
|
+
* ports inside.
|
|
461
|
+
*
|
|
462
|
+
* pid-reuse note: `process.kill(pid, 0)` only checks that *some* process
|
|
463
|
+
* with that pid exists. after a reboot or heavy fork churn the OS can
|
|
464
|
+
* recycle the pid onto an unrelated process owned by the same user. the
|
|
465
|
+
* heartbeat freshness check catches most of that (30s stale ⇒ reject),
|
|
466
|
+
* but a stale lockfile where pid happens to be reused < 30s ago could
|
|
467
|
+
* still slip through. the consumer paths that actually connect (electron
|
|
468
|
+
* + ws-bridge client) time out quickly, so a stale lockfile degrades to
|
|
469
|
+
* "connect attempt fails" rather than corrupt state. */
|
|
470
|
+
export function isDaemonLockfileFresh(
|
|
471
|
+
lock: DaemonLockfile | null,
|
|
472
|
+
now = Date.now(),
|
|
473
|
+
): lock is DaemonLockfile {
|
|
474
|
+
if (!lock) return false
|
|
475
|
+
if (now - lock.heartbeatAt > DAEMON_HEARTBEAT_STALE_MS) return false
|
|
476
|
+
try {
|
|
477
|
+
// signal 0 just tests whether the pid exists + we have perm to signal.
|
|
478
|
+
process.kill(lock.pid, 0)
|
|
479
|
+
return true
|
|
480
|
+
} catch {
|
|
481
|
+
return false
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function isDevBridgeLockfileFresh(
|
|
486
|
+
lock: DevBridgeLockfile | null,
|
|
487
|
+
now = Date.now(),
|
|
488
|
+
): lock is DevBridgeLockfile {
|
|
489
|
+
if (!lock) return false
|
|
490
|
+
if (now - lock.heartbeatAt > DAEMON_HEARTBEAT_STALE_MS) return false
|
|
491
|
+
try {
|
|
492
|
+
process.kill(lock.pid, 0)
|
|
493
|
+
return true
|
|
494
|
+
} catch {
|
|
495
|
+
return false
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export function readLiveRuntimeVersions(): string[] {
|
|
500
|
+
const versions = new Set<string>()
|
|
501
|
+
const daemon = readDaemonLockfile()
|
|
502
|
+
if (isDaemonLockfileFresh(daemon)) {
|
|
503
|
+
if (daemon.activeRuntime) versions.add(daemon.activeRuntime)
|
|
504
|
+
for (const version of daemon.servedRuntimes ?? []) versions.add(version)
|
|
505
|
+
}
|
|
506
|
+
const devBridge = readDevBridgeLockfile()
|
|
507
|
+
if (isDevBridgeLockfileFresh(devBridge)) {
|
|
508
|
+
for (const version of devBridge.servedRuntimes ?? []) versions.add(version)
|
|
509
|
+
}
|
|
510
|
+
return [...versions].sort()
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export function writeDaemonLockfile(data: DaemonLockfile): void {
|
|
514
|
+
ensureSootsimHome()
|
|
515
|
+
const tmp = `${daemonLockfilePath()}.tmp`
|
|
516
|
+
fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
|
|
517
|
+
fs.renameSync(tmp, daemonLockfilePath())
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function writeDevBridgeLockfile(data: DevBridgeLockfile): void {
|
|
521
|
+
ensureSootsimHome()
|
|
522
|
+
const tmp = `${devBridgeLockfilePath()}.tmp`
|
|
523
|
+
fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
|
|
524
|
+
fs.renameSync(tmp, devBridgeLockfilePath())
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** try to claim the lockfile atomically on daemon boot. returns true if
|
|
528
|
+
* we now own it. returns false when another fresh daemon beat us to the
|
|
529
|
+
* punch — callers should refuse to start. a stale lockfile (dead pid or
|
|
530
|
+
* old heartbeat) is overwritten. race-safe: the final fs.renameSync is
|
|
531
|
+
* atomic on POSIX and on NTFS. */
|
|
532
|
+
export function claimDaemonLockfile(data: DaemonLockfile): boolean {
|
|
533
|
+
ensureSootsimHome()
|
|
534
|
+
const existing = readDaemonLockfile()
|
|
535
|
+
if (existing && isDaemonLockfileFresh(existing) && existing.pid !== data.pid) {
|
|
536
|
+
return false
|
|
537
|
+
}
|
|
538
|
+
writeDaemonLockfile(data)
|
|
539
|
+
return true
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function removeDaemonLockfile(): void {
|
|
543
|
+
try {
|
|
544
|
+
fs.unlinkSync(daemonLockfilePath())
|
|
545
|
+
} catch {}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function removeDevBridgeLockfile(): void {
|
|
549
|
+
try {
|
|
550
|
+
fs.unlinkSync(devBridgeLockfilePath())
|
|
551
|
+
} catch {}
|
|
552
|
+
}
|