// canonical filesystem layout under ~/.rnx/. shared by the CLI and the // electron main process so every surface agrees where runtimes, the daemon // lockfile, and caches live. // // ~/.rnx/ // ├── runtimes/ // │ ├── / unpacked dist/ of sootsim-engine // │ └── active text file with the active version string (win-safe // │ alternative to a symlink) // ├── electron/ // │ └── / pinned electron binary (future: playwright-style) // ├── profiles/ // │ └── profiles.json storage profile metadata // ├── cache/ // │ └── rnx-runtime-.tar.gz // ├── daemon.json lockfile: pid, ports, active runtime, heartbeat // ├── dev-bridges/ live Vite dev bridge records, keyed by bound port // ├── runtime-upgraded.json one-shot notice written by the daemon's // │ background updater, consumed (printed + deleted) // │ by the next interactive CLI run // ├── cli-update.json cached standalone CLI release check + notice state // ├── automatic-cleanup-v1.json completion marker for the one-time safe // │ cleanup introduced for legacy unbounded data // └── config.json user prefs: update channel, cdn origin override, // telemetry, engine settings, default box import { randomUUID } from 'node:crypto' import fs from 'node:fs' import { homedir } from 'node:os' import path from 'node:path' export const RNX_HOME_ENV = 'RNX_HOME' export const ACTIVE_RUNTIME_FILE = 'active' export const DAEMON_LOCKFILE = 'daemon.json' export const DEV_BRIDGES_DIR = 'dev-bridges' export const CONFIG_FILE = 'config.json' export const CLI_UPDATE_FILE = 'cli-update.json' export const DAEMON_HEARTBEAT_STALE_MS = 30_000 export function rnxHomeDir(): string { const override = process.env[RNX_HOME_ENV] if (override && override.length > 0) return path.resolve(override) return path.join(homedir(), '.rnx') } // detect when sootsim is running from a source checkout (the Contrast monorepo) // rather than a published npm install. used to skip auto-install of the // persistent launchd / systemd agent: dev shells shouldn't register an agent // whose Program path points at workspace artifacts that change between // sessions, and whose served engine assets are the stale prod build instead // of the live `bun dev:sootsim` output. // // overrides: // SOOTSIM_DEV=1 / SOOTSIM_DEV=0 force the answer // SOOTSIM_FORCE_DAEMON_INSTALL=1 pretend prod even from a dev // checkout, for exercising the // install path from this repo // // signal: realpath of process.argv[1] lands inside a `packages/sootsim/` // directory. workspace bin symlinks (`node_modules/.bin/sootsim` → // `packages/sootsim/dist-cli/bin.js`) and bun-direct invocations // (`bun packages/sootsim/cli/bin.ts ...`) both match; published installs // resolve under `node_modules/sootsim/` instead. export function isSootsimDevCheckout(): boolean { if (process.env.SOOTSIM_FORCE_DAEMON_INSTALL === '1') return false const env = process.env.SOOTSIM_DEV if (env === '1' || env === 'true') return true if (env === '0' || env === 'false') return false const argv1 = process.argv[1] if (!argv1) return false try { const real = fs.realpathSync(argv1) return real.includes(`${path.sep}packages${path.sep}sootsim${path.sep}`) } catch { return false } } // detect the human's dev workstation by the env markers the developer sets in // their shell profile (`IS_TAMAGUI_DEV=1` in ~/.zshrc). distinct from // isSootsimDevCheckout, which keys on the *binary path*: a globally-installed // `sootsim` binary on the dev machine would NOT match the checkout path, yet a // persistent daemon registered there is just as dangerous. it runs in the // background carrying the workstation's dev env (dev auth tokens, prod-pointing // vars), and when the live `bun dev` stack already owns the bridge port it // silently lands on a fallback port and shadows the real one with stale assets // — the "I was on the daemon on port 7-something" footgun. // // override: SOOTSIM_FORCE_DAEMON_INSTALL=1 to run the daemon here anyway. export function isDevWorkstation(): boolean { if (process.env.SOOTSIM_FORCE_DAEMON_INSTALL === '1') return false return process.env.IS_TAMAGUI_DEV === '1' } // the persistent launchd / systemd daemon must never auto-install or run on a // dev checkout or a dev workstation. either makes the background agent point at // moving workspace artifacts / dev env and serve stale assets that shadow the // live dev stack. setup and the launchd-spawned `rnx serve` boot consult // this; a foreground `rnx serve` the dev starts // by hand is intentional and stays allowed. export function shouldSkipPersistentDaemon(): boolean { return isSootsimDevCheckout() || isDevWorkstation() } export function runtimesDir(): string { return path.join(rnxHomeDir(), 'runtimes') } export function runtimeDir(version: string): string { return path.join(runtimesDir(), version) } export function activeRuntimeFile(): string { return path.join(runtimesDir(), ACTIVE_RUNTIME_FILE) } export function electronDir(): string { return path.join(rnxHomeDir(), 'electron') } export function electronUserDataDir(): string { return path.join(electronDir(), 'userData') } export function electronVersionDir(version: string): string { return path.join(electronDir(), version) } export function profilesDir(): string { return path.join(rnxHomeDir(), 'profiles') } // video fixtures staged for `rnx camera play`. the CLI copies the user's // file in here and the daemon serves it by basename from this one directory, // so the route can never be talked into reading anywhere else on disk. export function cameraFixturesDir(): string { return path.join(rnxHomeDir(), 'camera-fixtures') } // the launchd-managed daemon spawns ProgramArguments[0] directly, and macOS // Background Task Management attributes the entry to whoever code-signed // that binary. pointing launchd at bun directly makes Login Items say // "software from Jarred Sumner" (bun's signer); wrapping the invocation in // an ad-hoc-signed .app bundle here gives BTM a CFBundleDisplayName to // read instead. export function daemonAppDir(): string { return path.join(rnxHomeDir(), 'daemon-app') } export function daemonAppBundlePath(): string { return path.join(daemonAppDir(), 'RNX Daemon.app') } export function daemonAppLauncherPath(): string { return path.join(daemonAppBundlePath(), 'Contents', 'MacOS', 'rnx-daemon') } export function cacheDir(): string { return path.join(rnxHomeDir(), 'cache') } export function daemonLockfilePath(): string { return path.join(rnxHomeDir(), DAEMON_LOCKFILE) } export function devBridgesDir(): string { return path.join(rnxHomeDir(), DEV_BRIDGES_DIR) } export function devBridgeLockfilePath(bridgePort: number): string { if (!Number.isInteger(bridgePort) || bridgePort <= 0) { throw new Error(`invalid development bridge port: ${bridgePort}`) } return path.join(devBridgesDir(), `${bridgePort}.json`) } export function configFilePath(): string { return path.join(rnxHomeDir(), CONFIG_FILE) } export function cliUpdateFilePath(): string { return path.join(rnxHomeDir(), CLI_UPDATE_FILE) } // --- shared rnx config --------------------------------------------------- // // ~/.rnx/config.json is the single user-level config file every rnx // surface (cli, electron renderer, dev browser via electron ipc) agrees on. // // privacy preferences live in the nested settings object so the cli, desktop // main process, renderer, daemon-served browser, and settings UI all read and // write one persisted value per choice. export interface SharedConfig { telemetry?: boolean onboardingVersion?: number simulatorDriver?: 'electron' | 'playwright' settings?: Record [key: string]: unknown } export function readSharedConfig(): SharedConfig { try { const raw = fs.readFileSync(configFilePath(), 'utf8') const parsed = JSON.parse(raw) as SharedConfig return parsed && typeof parsed === 'object' ? parsed : {} } catch { return {} } } /** merge `patch` into the shared config and atomically write to disk. nested * objects (currently just `settings`) are shallow-merged so partial writes * don't clobber unrelated fields. returns the new full snapshot. */ export function writeSharedConfig(patch: Partial): SharedConfig { ensureRnxHome() const current = readSharedConfig() const next: SharedConfig = { ...current, ...patch } if (patch.settings && typeof patch.settings === 'object') { next.settings = { ...(current.settings && typeof current.settings === 'object' ? current.settings : {}), ...patch.settings, } } const tmp = `${configFilePath()}.tmp` fs.writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, 'utf8') fs.renameSync(tmp, configFilePath()) return next } const CURRENT_ONBOARDING_VERSION = 1 export type SimulatorDriverPreference = 'electron' | 'playwright' export function isOnboardingComplete(): boolean { return readSharedConfig().onboardingVersion === CURRENT_ONBOARDING_VERSION } export function readSimulatorDriverPreference(): SimulatorDriverPreference | null { const driver = readSharedConfig().simulatorDriver return driver === 'electron' || driver === 'playwright' ? driver : null } export function writeOnboardingPreferences(driver: SimulatorDriverPreference): void { writeSharedConfig({ onboardingVersion: CURRENT_ONBOARDING_VERSION, simulatorDriver: driver, }) } export interface PrivacyPreferences { productAnalytics: boolean crashReports: boolean configured: boolean } export function readPrivacyPreferences(): PrivacyPreferences { const settings = readSharedConfig().settings const productAnalytics = settings?.productAnalytics const crashReports = settings?.crashReports return { productAnalytics: typeof productAnalytics === 'boolean' ? productAnalytics : false, crashReports: typeof crashReports === 'boolean' ? crashReports : false, configured: typeof productAnalytics === 'boolean' && typeof crashReports === 'boolean', } } export function writePrivacyPreferences( preferences: Pick, ): void { writeSharedConfig({ settings: { productAnalytics: preferences.productAnalytics, crashReports: preferences.crashReports, }, }) } // stable per-machine anonymous telemetry id. every signed-out cli used to // report as the single literal distinct_id 'anonymous-cli', which merged all // first-run users into one posthog person and made "a new user's login hung" // impossible to see. created lazily on first use, persisted in config.json. export function readOrCreateAnonymousId(): string { const existing = readSharedConfig().anonymousId if (typeof existing === 'string' && existing.trim()) return existing const created = `anon-cli-${randomUUID()}` writeSharedConfig({ anonymousId: created }) return created } // --- default box ---------------------------------------------------------- // // the box every box verb acts on when the command line names none. `rnx box // use` writes it and `rnx box stop` clears it when it stops that box. // // the id and the display name, and nothing else. a Box is reached with a // bearer token the account service hands out per attach, so writing one here // would make this file a credential store; every verb resolves the token // again from the id it reads back. export interface DefaultBox { boxId: string name: string | null } export function readDefaultBox(): DefaultBox | null { const stored = readSharedConfig().defaultBox if (typeof stored !== 'object' || stored === null) return null if (!('boxId' in stored) || typeof stored.boxId !== 'string' || !stored.boxId) { return null } const name = 'name' in stored && typeof stored.name === 'string' ? stored.name : null return { boxId: stored.boxId, name } } /** set the default box, or clear it with null. */ export function writeDefaultBox(box: DefaultBox | null): void { writeSharedConfig({ defaultBox: box }) } // --- background-upgrade notice -------------------------------------------- // // the daemon auto-updates the engine runtime in the background (hourly), so // the only place its "updated to vX" line lands is the daemon's own stderr, // which nobody reads. it also drops a one-shot notice file here; the next // interactive CLI invocation prints it once and deletes it, so users learn // their engine moved without having to run `rnx version`. export interface RuntimeUpgradeNotice { from: string | null to: string at: number } export function runtimeUpgradeNoticePath(): string { return path.join(rnxHomeDir(), 'runtime-upgraded.json') } export function writeRuntimeUpgradeNotice(notice: RuntimeUpgradeNotice): void { ensureRnxHome() const tmp = `${runtimeUpgradeNoticePath()}.tmp` fs.writeFileSync(tmp, `${JSON.stringify(notice)}\n`, 'utf8') fs.renameSync(tmp, runtimeUpgradeNoticePath()) } /** read + delete the pending upgrade notice, or null when none is pending. * deleting first-read keeps the banner one-shot even across racing CLIs. */ export function consumeRuntimeUpgradeNotice(): RuntimeUpgradeNotice | null { try { const raw = fs.readFileSync(runtimeUpgradeNoticePath(), 'utf8') fs.unlinkSync(runtimeUpgradeNoticePath()) const parsed = JSON.parse(raw) as Partial if (typeof parsed?.to !== 'string' || !parsed.to) return null return { from: typeof parsed.from === 'string' ? parsed.from : null, to: parsed.to, at: typeof parsed.at === 'number' ? parsed.at : 0, } } catch { return null } } export function ensureRnxHome(): void { fs.mkdirSync(rnxHomeDir(), { recursive: true }) fs.mkdirSync(runtimesDir(), { recursive: true }) fs.mkdirSync(electronDir(), { recursive: true }) fs.mkdirSync(profilesDir(), { recursive: true }) fs.mkdirSync(cacheDir(), { recursive: true }) } /** read the active runtime version string, or null if none is selected. */ export function readActiveRuntime(): string | null { try { const value = fs.readFileSync(activeRuntimeFile(), 'utf8').trim() return value.length > 0 ? value : null } catch { return null } } /** set the active runtime version. caller is responsible for verifying the * version actually exists on disk before calling. */ export function writeActiveRuntime(version: string): void { fs.mkdirSync(runtimesDir(), { recursive: true }) fs.writeFileSync(activeRuntimeFile(), `${version}\n`, 'utf8') } // listInstalledRuntimes + compareSemver live in @contrast/runtime-delivery: // they are delivery machinery (sorting the hosted catalog, deciding whether // channel latest is newer than active), shared with contrast, and this module // stays free of that dependency so every published `sootsim/*` export that // resolves through it keeps working with no bundling requirement. /** absolute path to the active runtime's directory, or null if none active * or the active version is no longer installed. */ export function activeRuntimeDir(): string | null { const version = readActiveRuntime() if (!version) return null const dir = runtimeDir(version) try { if (fs.statSync(dir).isDirectory()) return dir } catch {} return null } // --- daemon lockfile ---------------------------------------------------- export interface DaemonLockfile { /** sootsim cli/daemon version that wrote the lockfile. bumped whenever * the lockfile shape changes so readers can gate on it. */ schema: 1 pid: number /** platform — useful when one home dir is shared across platforms via NFS. */ platform: NodeJS.Platform /** ws bridge port (where cli + electron open control connections). */ bridgePort: number /** http runtime server port (where electron loads the renderer from). */ runtimePort: number /** active runtime version at boot, or null if the daemon booted with no * runtime installed. updated live when the user runs `sootsim runtime use`. */ activeRuntime: string | null /** absolute path to the active runtime's dist directory, or null. */ activeRuntimeDir: string | null /** repo-selected runtime versions currently served to connected sims. */ servedRuntimes?: string[] /** epoch-ms of daemon start. */ startedAt: number /** epoch-ms of last heartbeat. daemons update this every ~5s; readers * treat the lockfile as stale if now - heartbeatAt > DAEMON_HEARTBEAT_STALE_MS. */ heartbeatAt: number /** true while the daemon is still fetching/activating its runtime on first * boot. clients (electron splash, cli) should wait for this to become * false before treating the daemon as ready to serve. */ bootstrapping?: boolean } export interface DevBridgeLockfile { schema: 1 pid: number platform: NodeJS.Platform bridgePort: number runtimePort: number /** http port of the vite shell dev server that owns this bridge (e.g. 5173). * optional: lockfiles written before this field existed still parse, and * readers fall back to the default shell URL. this is what lets the CLI * resolve the shell base URL and the bridge port from the SAME world * instead of mixing dev bridge + daemon runtime (the split-brain bug). */ shellPort?: number cwd: string startedAt: number heartbeatAt: number source: 'vite-dev' /** repo-selected runtime versions currently served to connected sims. */ servedRuntimes?: string[] } const DAEMON_LOCKFILE_MAX_BYTES = 16 * 1024 const DAEMON_CLAIM_STALE_MS = 30_000 interface DaemonClaim { pid: number startedAt: number createdAt: number } export function readDaemonLockfile(): DaemonLockfile | null { try { // cap the read so a junk/large file on the lockfile path can't OOM // the CLI when someone has been messing with ~/.rnx/. const fd = fs.openSync(daemonLockfilePath(), 'r') try { const buf = Buffer.alloc(DAEMON_LOCKFILE_MAX_BYTES) const bytesRead = fs.readSync(fd, buf, 0, DAEMON_LOCKFILE_MAX_BYTES, 0) const raw = buf.subarray(0, bytesRead).toString('utf8') const parsed = JSON.parse(raw) as Partial if ( parsed && parsed.schema === 1 && typeof parsed.pid === 'number' && typeof parsed.bridgePort === 'number' && typeof parsed.runtimePort === 'number' && typeof parsed.startedAt === 'number' && typeof parsed.heartbeatAt === 'number' && (parsed.servedRuntimes === undefined || (Array.isArray(parsed.servedRuntimes) && parsed.servedRuntimes.every((version) => typeof version === 'string'))) ) { return parsed as DaemonLockfile } return null } finally { fs.closeSync(fd) } } catch { return null } } function parseDevBridgeLockfile(raw: string): DevBridgeLockfile | null { try { const parsed = JSON.parse(raw) if ( parsed && parsed.schema === 1 && parsed.source === 'vite-dev' && typeof parsed.pid === 'number' && typeof parsed.bridgePort === 'number' && typeof parsed.runtimePort === 'number' && typeof parsed.cwd === 'string' && typeof parsed.startedAt === 'number' && typeof parsed.heartbeatAt === 'number' && (parsed.shellPort === undefined || typeof parsed.shellPort === 'number') && (parsed.servedRuntimes === undefined || (Array.isArray(parsed.servedRuntimes) && parsed.servedRuntimes.every((version: unknown) => typeof version === 'string'))) ) { return parsed } return null } catch { return null } } export function readDevBridgeLockfiles(): DevBridgeLockfile[] { let entries: fs.Dirent[] try { entries = fs.readdirSync(devBridgesDir(), { withFileTypes: true }) } catch { return [] } const lockfiles: DevBridgeLockfile[] = [] for (const entry of entries) { if (!entry.isFile() || !/^\d+\.json$/.test(entry.name)) continue try { const filePath = path.join(devBridgesDir(), entry.name) const fd = fs.openSync(filePath, 'r') try { const buf = Buffer.alloc(DAEMON_LOCKFILE_MAX_BYTES) const bytesRead = fs.readSync(fd, buf, 0, DAEMON_LOCKFILE_MAX_BYTES, 0) const lockfile = parseDevBridgeLockfile( buf.subarray(0, bytesRead).toString('utf8'), ) if (lockfile && entry.name === `${lockfile.bridgePort}.json`) { lockfiles.push(lockfile) } } finally { fs.closeSync(fd) } } catch {} } return lockfiles.sort((left, right) => left.bridgePort - right.bridgePort) } /** true when the lockfile exists, the named pid is alive, and the heartbeat * is recent. callers should reach for this before trusting any of the * ports inside. * * pid-reuse note: `process.kill(pid, 0)` only checks that *some* process * with that pid exists. after a reboot or heavy fork churn the OS can * recycle the pid onto an unrelated process owned by the same user. the * heartbeat freshness check catches most of that (30s stale ⇒ reject), * but a stale lockfile where pid happens to be reused < 30s ago could * still slip through. the consumer paths that actually connect (electron * + ws-bridge client) time out quickly, so a stale lockfile degrades to * "connect attempt fails" rather than corrupt state. */ export function isDaemonLockfileFresh( lock: DaemonLockfile | null, now = Date.now(), ): lock is DaemonLockfile { if (!lock) return false if (now - lock.heartbeatAt > DAEMON_HEARTBEAT_STALE_MS) return false try { // signal 0 just tests whether the pid exists + we have perm to signal. process.kill(lock.pid, 0) return true } catch { return false } } export function isDevBridgeLockfileFresh( lock: DevBridgeLockfile | null, now = Date.now(), ): lock is DevBridgeLockfile { if (!lock) return false if (now - lock.heartbeatAt > DAEMON_HEARTBEAT_STALE_MS) return false try { process.kill(lock.pid, 0) return true } catch { return false } } export function readLiveRuntimeVersions(): string[] { const versions = new Set() const daemon = readDaemonLockfile() if (isDaemonLockfileFresh(daemon)) { if (daemon.activeRuntime) versions.add(daemon.activeRuntime) for (const version of daemon.servedRuntimes ?? []) versions.add(version) } for (const devBridge of readDevBridgeLockfiles()) { if (!isDevBridgeLockfileFresh(devBridge)) continue for (const version of devBridge.servedRuntimes ?? []) versions.add(version) } return [...versions].sort() } export function writeDaemonLockfile(data: DaemonLockfile): void { ensureRnxHome() const lockfilePath = daemonLockfilePath() const existing = readDaemonLockfile() if (!existing) { if (createDaemonLockfileExclusive(data)) return throw new Error('another rnx daemon claimed the lockfile before this write') } if (!sameDaemonOwner(existing, data)) { throw new Error('refusing to overwrite another rnx daemon lockfile') } const tmp = `${lockfilePath}.${data.pid}.${randomUUID()}.tmp` fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8') try { const current = readDaemonLockfile() if (!current || !sameDaemonOwner(current, data)) { throw new Error('rnx daemon lock ownership changed during heartbeat') } fs.renameSync(tmp, lockfilePath) } finally { try { fs.unlinkSync(tmp) } catch {} } } export function writeDevBridgeLockfile(data: DevBridgeLockfile): void { fs.mkdirSync(devBridgesDir(), { recursive: true }) const lockfilePath = devBridgeLockfilePath(data.bridgePort) const tmp = `${lockfilePath}.${data.pid}.tmp` fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8') fs.renameSync(tmp, lockfilePath) } function sameDaemonOwner( left: Pick, right: Pick, ): boolean { return left.pid === right.pid && left.startedAt === right.startedAt } function isAlreadyExists(error: unknown): boolean { return ( error instanceof Error && 'code' in error && Reflect.get(error, 'code') === 'EEXIST' ) } function createDaemonLockfileExclusive(data: DaemonLockfile): boolean { const lockfilePath = daemonLockfilePath() const tmp = `${lockfilePath}.${data.pid}.${randomUUID()}.claim` fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8') try { fs.linkSync(tmp, lockfilePath) return true } catch (error) { if (isAlreadyExists(error)) return false throw error } finally { try { fs.unlinkSync(tmp) } catch {} } } function daemonClaimPath(): string { return `${daemonLockfilePath()}.claim` } function parseDaemonClaim(raw: string): DaemonClaim | null { try { const value: unknown = JSON.parse(raw) if ( value !== null && typeof value === 'object' && typeof Reflect.get(value, 'pid') === 'number' && typeof Reflect.get(value, 'startedAt') === 'number' && typeof Reflect.get(value, 'createdAt') === 'number' ) { return { pid: Reflect.get(value, 'pid'), startedAt: Reflect.get(value, 'startedAt'), createdAt: Reflect.get(value, 'createdAt'), } } } catch {} return null } function readDaemonClaim(): DaemonClaim | null { try { return parseDaemonClaim(fs.readFileSync(daemonClaimPath(), 'utf8')) } catch { return null } } function isDaemonClaimFresh(claim: DaemonClaim, now = Date.now()): boolean { if (now - claim.createdAt > DAEMON_CLAIM_STALE_MS) return false try { process.kill(claim.pid, 0) return true } catch { return false } } function acquireDaemonClaim(data: DaemonLockfile): boolean { const claim: DaemonClaim = { pid: data.pid, startedAt: data.startedAt, createdAt: Date.now(), } const serialized = `${JSON.stringify(claim)}\n` for (let attempt = 0; attempt < 8; attempt++) { try { fs.writeFileSync(daemonClaimPath(), serialized, { encoding: 'utf8', flag: 'wx' }) return true } catch (error) { if (!isAlreadyExists(error)) throw error } const existing = readDaemonClaim() if (existing && sameDaemonOwner(existing, claim)) return true if (existing && isDaemonClaimFresh(existing)) return false let before: string try { before = fs.readFileSync(daemonClaimPath(), 'utf8') const latest = parseDaemonClaim(before) if (latest && isDaemonClaimFresh(latest)) return false if (fs.readFileSync(daemonClaimPath(), 'utf8') !== before) continue fs.unlinkSync(daemonClaimPath()) } catch {} } return false } function releaseDaemonClaim(owner: Pick): void { try { const claim = readDaemonClaim() if (!claim || !sameDaemonOwner(claim, owner)) return fs.unlinkSync(daemonClaimPath()) } catch {} } /** try to claim the lockfile atomically on daemon boot. a short-lived, * process-owned election file serializes concurrent fallback-port binds; * the complete lock snapshot is then installed with an exclusive hard link. */ export function claimDaemonLockfile(data: DaemonLockfile): boolean { ensureRnxHome() if (!acquireDaemonClaim(data)) return false try { const existing = readDaemonLockfile() if (existing && sameDaemonOwner(existing, data)) { writeDaemonLockfile(data) return true } if (existing && isDaemonLockfileFresh(existing)) return false if (existing) { const latest = readDaemonLockfile() if (!latest || !sameDaemonOwner(latest, existing)) return false if (isDaemonLockfileFresh(latest)) return false try { fs.unlinkSync(daemonLockfilePath()) } catch { return false } } return createDaemonLockfileExclusive(data) } finally { releaseDaemonClaim(data) } } export function removeDaemonLockfile( owner: Pick, ): void { try { const lockfile = readDaemonLockfile() if (!lockfile || !sameDaemonOwner(lockfile, owner)) return fs.unlinkSync(daemonLockfilePath()) } catch {} } export function removeDevBridgeLockfile( owner: Pick, ): void { try { const lockfilePath = devBridgeLockfilePath(owner.bridgePort) const lockfile = parseDevBridgeLockfile(fs.readFileSync(lockfilePath, 'utf8')) if (lockfile?.pid !== owner.pid || lockfile.startedAt !== owner.startedAt) return fs.unlinkSync(lockfilePath) } catch {} }