rnxsim 0.1.313 → 0.1.314
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +11 -10
- package/README.md +5 -0
- package/cli/app-config.ts +65 -0
- package/cli/app-fonts.ts +408 -0
- package/cli/app-project.ts +231 -0
- package/cli/app-splash.ts +185 -0
- package/cli/app-state-reset.ts +24 -0
- package/cli/auth.ts +155 -0
- package/cli/bin.ts +594 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2830 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/commands/agent-wrapper.ts +986 -0
- package/cli/commands/agent.ts +423 -0
- package/cli/commands/app-fonts.ts +98 -0
- package/cli/commands/assert.ts +541 -0
- package/cli/commands/auth.ts +59 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +87 -0
- package/cli/commands/config.ts +32 -0
- package/cli/commands/control.ts +2142 -0
- package/cli/commands/cpu-profile.ts +269 -0
- package/cli/commands/daemon-mac-app.ts +169 -0
- package/cli/commands/daemon.ts +874 -0
- package/cli/commands/debug.ts +719 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +197 -0
- package/cli/commands/detox.ts +385 -0
- package/cli/commands/device.ts +133 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/electron.ts +95 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1124 -0
- package/cli/commands/inspect/actions.ts +622 -0
- package/cli/commands/inspect/core.ts +2405 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +192 -0
- package/cli/commands/inspect/env.ts +23 -0
- package/cli/commands/inspect/find.ts +171 -0
- package/cli/commands/inspect/get-layout.ts +39 -0
- package/cli/commands/inspect/keyboard.ts +52 -0
- package/cli/commands/inspect/list.ts +58 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +82 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +22 -0
- package/cli/commands/inspect/settling.ts +158 -0
- package/cli/commands/inspect/shared.ts +353 -0
- package/cli/commands/inspect/sleep.ts +14 -0
- package/cli/commands/inspect/tree.ts +32 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +210 -0
- package/cli/commands/inspect/wait-idle.ts +24 -0
- package/cli/commands/inspect/wait-ready.ts +74 -0
- package/cli/commands/inspect/wait-selector.ts +54 -0
- package/cli/commands/inspect/wait.ts +31 -0
- package/cli/commands/inspect.ts +4519 -0
- package/cli/commands/install-desktop.ts +351 -0
- package/cli/commands/login.ts +331 -0
- package/cli/commands/logout.ts +31 -0
- package/cli/commands/maestro-generate.ts +361 -0
- package/cli/commands/maestro.ts +453 -0
- package/cli/commands/mode.ts +57 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +66 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/profile.ts +108 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1434 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +85 -0
- package/cli/commands/runtime.ts +351 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +143 -0
- package/cli/commands/screenshot-mode.ts +37 -0
- package/cli/commands/screenshot.ts +488 -0
- package/cli/commands/screenshots-capture.ts +607 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +168 -0
- package/cli/commands/setup.ts +545 -0
- package/cli/commands/shell-boolean-mode.ts +81 -0
- package/cli/commands/skills.ts +467 -0
- package/cli/commands/slides.ts +361 -0
- package/cli/commands/state.ts +87 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +54 -0
- package/cli/commands/three-mode.ts +763 -0
- package/cli/commands/timeline.ts +122 -0
- package/cli/commands/upgrade.ts +208 -0
- package/cli/commands/upload.ts +1225 -0
- package/cli/commands/version.ts +54 -0
- package/cli/commands/what-happened.ts +327 -0
- package/cli/current-sim.ts +204 -0
- package/cli/desktop-companion.ts +300 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +180 -0
- package/cli/drivers/playwright.ts +698 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +120 -0
- package/cli/flow-session.ts +187 -0
- package/cli/help.ts +80 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +216 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/maestro-js.ts +334 -0
- package/cli/open-url.ts +8 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +211 -0
- package/cli/prompt.ts +51 -0
- package/cli/recording-access.ts +107 -0
- package/cli/registry.ts +1 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/run-registry.ts +226 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/setup-repository.ts +187 -0
- package/cli/telemetry.ts +187 -0
- package/cli/ws-bridge.ts +798 -0
- package/dist-cli/bin.js +5 -5
- package/dist-cli/chunks/{agent-XZ2KTPCU.js → agent-7YBDCYMA.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-JJYYW2WH.js → agent-wrapper-2GHFBHCR.js} +2 -2
- package/dist-cli/chunks/{app-fonts-IXRNQG6B.js → app-fonts-RSNVPQSU.js} +2 -2
- package/dist-cli/chunks/{assert-54T5SK5F.js → assert-XCMX3XJX.js} +2 -2
- package/dist-cli/chunks/{auth-FI5UDI45.js → auth-B442HRAX.js} +2 -2
- package/dist-cli/chunks/{beta-JV6UKADW.js → beta-XJ55JK3M.js} +2 -2
- package/dist-cli/chunks/camera-UGYSSLIK.js +33 -0
- package/dist-cli/chunks/{chunk-3NV2NCNX.js → chunk-277AEQZX.js} +2 -2
- package/dist-cli/chunks/{chunk-2YR5BGA5.js → chunk-2JNSK774.js} +2 -2
- package/dist-cli/chunks/{chunk-RSZWCKNT.js → chunk-32WOTSTR.js} +3 -3
- package/dist-cli/chunks/{chunk-WEXDAC74.js → chunk-3S753SNQ.js} +2 -2
- package/dist-cli/chunks/{chunk-IJO63TDP.js → chunk-46ZOLOYA.js} +2 -2
- package/dist-cli/chunks/{chunk-WUSWBCWA.js → chunk-5L7ELDQL.js} +8 -9
- package/dist-cli/chunks/{chunk-WF3T4SVI.js → chunk-7OOPFSQS.js} +2 -2
- package/dist-cli/chunks/{chunk-TZFFR3SD.js → chunk-7SV3RPRW.js} +2 -2
- package/dist-cli/chunks/{chunk-WWZIXIRD.js → chunk-7ZC35MOU.js} +1 -1
- package/dist-cli/chunks/chunk-AMG5E6CC.js +9 -0
- package/dist-cli/chunks/{chunk-WINYQ44O.js → chunk-APWNH3A4.js} +1 -1
- package/dist-cli/chunks/chunk-B57XUKY3.js +4 -0
- package/dist-cli/chunks/{chunk-YDGQTMQL.js → chunk-C2NL26TD.js} +1 -1
- package/dist-cli/chunks/{chunk-NMF2ZMZQ.js → chunk-D2FNUWAB.js} +4 -4
- package/dist-cli/chunks/{chunk-YIFT42WN.js → chunk-E5T4XSJ3.js} +2 -2
- package/dist-cli/chunks/{chunk-46EUUFJ5.js → chunk-EAC34EQS.js} +1 -1
- package/dist-cli/chunks/{chunk-OVFJFXUD.js → chunk-EG32ML36.js} +2 -2
- package/dist-cli/chunks/{chunk-5YJCOWCH.js → chunk-FXUAC6D5.js} +1 -1
- package/dist-cli/chunks/chunk-GQSL4USA.js +6 -0
- package/dist-cli/chunks/{chunk-VFCMSYZK.js → chunk-HAXW27SS.js} +2 -2
- package/dist-cli/chunks/{chunk-2D2UPBBR.js → chunk-IZAHPAN6.js} +1 -1
- package/dist-cli/chunks/{chunk-7GN3LVWB.js → chunk-J62KM5TB.js} +2 -2
- package/dist-cli/chunks/{chunk-BTWORNNG.js → chunk-JEMCD5E4.js} +1 -1
- package/dist-cli/chunks/{chunk-DCEMHR2Y.js → chunk-JZS3Q37N.js} +2 -2
- package/dist-cli/chunks/{chunk-D4FFVGI5.js → chunk-KR4JON7D.js} +1 -1
- package/dist-cli/chunks/chunk-KWCYKQIQ.js +4 -0
- package/dist-cli/chunks/{chunk-VNQEB4L7.js → chunk-MCWPL644.js} +2 -2
- package/dist-cli/chunks/chunk-MJYK3N2I.js +4 -0
- package/dist-cli/chunks/{chunk-5TEF3ET3.js → chunk-O6TRIZNS.js} +2 -2
- package/dist-cli/chunks/{chunk-QKDWYITG.js → chunk-P7XL2E73.js} +3 -3
- package/dist-cli/chunks/{chunk-BBULZ7CG.js → chunk-PFQTUKQ4.js} +62 -87
- package/dist-cli/chunks/{chunk-GGRX24GF.js → chunk-PG5RZCTN.js} +2 -2
- package/dist-cli/chunks/{chunk-W6K4EFPH.js → chunk-QGRI2Z4M.js} +2 -2
- package/dist-cli/chunks/{chunk-ZMJD5GEC.js → chunk-QLXXE7GE.js} +1 -1
- package/dist-cli/chunks/{chunk-VZXWHRUZ.js → chunk-QOJJJWJE.js} +89 -133
- package/dist-cli/chunks/{chunk-OZSSI4WN.js → chunk-RWZY5427.js} +2 -2
- package/dist-cli/chunks/{chunk-UC6U3MML.js → chunk-RZKU2K3J.js} +2 -2
- package/dist-cli/chunks/{chunk-IJ5CAZZC.js → chunk-SBV4IK4H.js} +1 -1
- package/dist-cli/chunks/{chunk-DZS6WPUI.js → chunk-SGMVFFMK.js} +1 -1
- package/dist-cli/chunks/{chunk-OHAZNXLK.js → chunk-TAX4UT2N.js} +1 -1
- package/dist-cli/chunks/{chunk-5DHC6KHQ.js → chunk-TUOFAWXT.js} +1 -1
- package/dist-cli/chunks/{chunk-GASE6UBA.js → chunk-UHZLOHGP.js} +1 -1
- package/dist-cli/chunks/{chunk-F5ZRSS3C.js → chunk-VC7V76U3.js} +1 -1
- package/dist-cli/chunks/{chunk-RTN5C5RL.js → chunk-VQVMLW4U.js} +1 -1
- package/dist-cli/chunks/{chunk-WMIIKMGK.js → chunk-VUKKYPZN.js} +2 -2
- package/dist-cli/chunks/{chunk-5TPRP5QT.js → chunk-WGGRJDRE.js} +1 -1
- package/dist-cli/chunks/chunk-X6H76EKP.js +15 -0
- package/dist-cli/chunks/chunk-XLZ5FNRT.js +27 -0
- package/dist-cli/chunks/{chunk-HI5TFJWN.js → chunk-XULEACM4.js} +2 -2
- package/dist-cli/chunks/{chunk-MJRLLB4R.js → chunk-YFSDM7AX.js} +4 -4
- package/dist-cli/chunks/chunk-YWI3UEVX.js +5 -0
- package/dist-cli/chunks/{chunk-XEVZYVIW.js → chunk-ZBSJO4NB.js} +10 -9
- package/dist-cli/chunks/{cleanup-P27PA6JI.js → cleanup-EYLGCA6Z.js} +2 -2
- package/dist-cli/chunks/cli-version-LL2UGIHE.js +4 -0
- package/dist-cli/chunks/{compat-ZD65FED3.js → compat-TLJYHB4E.js} +2 -2
- package/dist-cli/chunks/{config-XMJRNM2A.js → config-4JWUOEXK.js} +2 -2
- package/dist-cli/chunks/{control-KMIQT3QP.js → control-HPAOYF4N.js} +2 -2
- package/dist-cli/chunks/daemon-OYLASXLE.js +4 -0
- package/dist-cli/chunks/{debug-PT4HOP7N.js → debug-4BKXF6KI.js} +5 -5
- package/dist-cli/chunks/{desktop-S3FG72AK.js → desktop-ZQ6ZD2S6.js} +3 -3
- package/dist-cli/chunks/{detox-B3D4IFCN.js → detox-WNPASSS3.js} +2 -2
- package/dist-cli/chunks/{device-XBNDSB2R.js → device-BXLXVG3V.js} +2 -2
- package/dist-cli/chunks/{diagnose-HMQXJE5N.js → diagnose-3QX7W5RW.js} +2 -2
- package/dist-cli/chunks/{disk-cleanup-BLCZ5BSZ.js → disk-cleanup-KSWKI7WB.js} +2 -2
- package/dist-cli/chunks/drivers-EPIEFF7P.js +4 -0
- package/dist-cli/chunks/{film-BJGTBYZB.js → film-GDMR33VO.js} +3 -3
- package/dist-cli/chunks/flow-NKMPBYCJ.js +4 -0
- package/dist-cli/chunks/help-OGCBHOKA.js +4 -0
- package/dist-cli/chunks/{hidden-runtime-alias-ANOYADHM.js → hidden-runtime-alias-S2GTDX3T.js} +2 -2
- package/dist-cli/chunks/home-paths-QRCDLTTV.js +4 -0
- package/dist-cli/chunks/inspect-FUYMOZPY.js +4 -0
- package/dist-cli/chunks/install-desktop-FR6YKW7Y.js +4 -0
- package/dist-cli/chunks/{login-FJ737MWG.js → login-IJAPUZHI.js} +4 -4
- package/dist-cli/chunks/{logout-ZCNMMHMY.js → logout-QMXDFU2X.js} +2 -2
- package/dist-cli/chunks/{maestro-SZTNKLDF.js → maestro-ZXU3YCVX.js} +3 -3
- package/dist-cli/chunks/{maestro-generate-DCFAIZ4H.js → maestro-generate-PYB5QY7K.js} +3 -3
- package/dist-cli/chunks/{mode-GRMQCRXR.js → mode-WGUCL5FZ.js} +2 -2
- package/dist-cli/chunks/{optional-demo-registry-W36EWFFB.js → optional-demo-registry-WH2O6H36.js} +2 -2
- package/dist-cli/chunks/{perf-QYBAAUZG.js → perf-TOD3UFAH.js} +2 -2
- package/dist-cli/chunks/{permissions-3QCQ6VF4.js → permissions-I5BRJGTB.js} +2 -2
- package/dist-cli/chunks/{record-QPPC2S4E.js → record-ZYL2FYSK.js} +3 -3
- package/dist-cli/chunks/{report-issue-7NMFP4HK.js → report-issue-TAI6DYZO.js} +2 -2
- package/dist-cli/chunks/reset-7YGYKQPQ.js +4 -0
- package/dist-cli/chunks/runtime-B4JO6QRI.js +4 -0
- package/dist-cli/chunks/{screenshot-command-67AECJFB.js → screenshot-command-7ANLODZY.js} +7 -7
- package/dist-cli/chunks/{screenshot-layers-ASWBYPJL.js → screenshot-layers-A7FYXSVU.js} +3 -3
- package/dist-cli/chunks/{screenshots-capture-PXA3HFQK.js → screenshots-capture-WT2ZY6CB.js} +2 -2
- package/dist-cli/chunks/serve-TG5WKA4V.js +44 -0
- package/dist-cli/chunks/{setup-7DWPMRSB.js → setup-PCWLD22V.js} +2 -2
- package/dist-cli/chunks/{skills-S3Y22TUA.js → skills-27ZHWCQS.js} +2 -2
- package/dist-cli/chunks/state-ZQVY46ZO.js +14 -0
- package/dist-cli/chunks/{storage-XUIMJWAJ.js → storage-OYC57C4X.js} +6 -6
- package/dist-cli/chunks/store-32HSPZHI.js +4 -0
- package/dist-cli/chunks/telemetry-PBJR7XHR.js +4 -0
- package/dist-cli/chunks/{timeline-TMPLQPSP.js → timeline-RAJJFQT6.js} +2 -2
- package/dist-cli/chunks/{upgrade-7HDSIM7K.js → upgrade-OEFNZRIP.js} +2 -2
- package/dist-cli/chunks/upload-LQJITLKK.js +4 -0
- package/dist-cli/chunks/version-NIXTY6PL.js +6 -0
- package/dist-cli/chunks/{web-DG3WBYD3.js → web-XBUTBVGR.js} +2 -2
- package/dist-cli/chunks/{what-happened-XFVUTZR7.js → what-happened-YNHRFUDX.js} +3 -3
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract.cjs +20 -0
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +67 -28
- package/dist-lib/host/bridge-host.cjs +140 -12
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +2815 -40
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +2549 -2061
- package/dist-lib/sdk.mjs +2543 -2061
- package/dist-lib/skills.cjs +480 -280
- package/dist-lib/vite.cjs +1 -1
- package/package.json +8 -2
- package/src/bridge-constants.ts +3 -4
- package/src/bridge-contract.ts +251 -0
- package/src/connect.ts +83 -0
- package/src/disk-cleanup.ts +30 -0
- package/src/home-paths.ts +81 -38
- package/src/host/bridge-host.ts +134 -6
- package/src/index.ts +27 -1
- package/src/sdk.ts +8 -0
- package/src/sim-client.ts +660 -0
- package/dist-cli/chunks/camera-VL73YIKP.js +0 -22
- package/dist-cli/chunks/chunk-4NPPOV2N.js +0 -5
- package/dist-cli/chunks/chunk-FSUYIVJ6.js +0 -9
- package/dist-cli/chunks/chunk-G2WW6L2C.js +0 -23
- package/dist-cli/chunks/chunk-KTHV3RUS.js +0 -26
- package/dist-cli/chunks/chunk-LF2ZVT7O.js +0 -6
- package/dist-cli/chunks/chunk-NFK7T35W.js +0 -4
- package/dist-cli/chunks/chunk-TIVZIMMW.js +0 -4
- package/dist-cli/chunks/cli-version-WWLPBDQ7.js +0 -4
- package/dist-cli/chunks/daemon-G2ME7NLB.js +0 -4
- package/dist-cli/chunks/drivers-LDECZGP2.js +0 -4
- package/dist-cli/chunks/flow-UEQNVTU7.js +0 -4
- package/dist-cli/chunks/help-T5FYSVGB.js +0 -4
- package/dist-cli/chunks/home-paths-GT3LFNOR.js +0 -4
- package/dist-cli/chunks/inspect-ZA6XF5LD.js +0 -4
- package/dist-cli/chunks/install-desktop-TIMUDHPL.js +0 -4
- package/dist-cli/chunks/runtime-XOAXMSTU.js +0 -4
- package/dist-cli/chunks/serve-BI2NBAXG.js +0 -44
- package/dist-cli/chunks/store-JTHEJLAZ.js +0 -4
- package/dist-cli/chunks/telemetry-ZYJGD2DB.js +0 -4
- package/dist-cli/chunks/upload-GMSZPWM6.js +0 -4
- package/dist-cli/chunks/version-HOCHZ37L.js +0 -6
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// sootsim version — one overview of both halves you're running.
|
|
2
|
+
//
|
|
3
|
+
// the `sootsim` CLI (npm package) and the engine runtime (versioned tarball
|
|
4
|
+
// rendered in the browser/electron shell) are versioned independently, which
|
|
5
|
+
// is a frequent "I upgraded but the version didn't change" trap. the terse
|
|
6
|
+
// `sootsim --version` flag prints both for scripts; this command adds the
|
|
7
|
+
// channel, a "newer runtime available" hint, and a what's-new pointer.
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
import { getCliVersion } from '../../src/cli-version'
|
|
12
|
+
import { runtimeDir } from '../../src/home-paths'
|
|
13
|
+
import { SOOTSIM_CHANGELOG_URL, sootsimRuntime } from '../../src/runtime-delivery'
|
|
14
|
+
import { resolveSootSimAppConfig } from '../app-config'
|
|
15
|
+
import { getRuntimeSummary } from '../runtime-summary'
|
|
16
|
+
|
|
17
|
+
export async function runVersion(_args: string[]): Promise<void> {
|
|
18
|
+
const { IS_BETA, BETA_LABEL } = await import('../../src/beta')
|
|
19
|
+
const suffix = IS_BETA ? ` · ${BETA_LABEL}` : ''
|
|
20
|
+
console.log(`rnxsim v${getCliVersion()}${suffix}`)
|
|
21
|
+
|
|
22
|
+
const runtime = getRuntimeSummary()
|
|
23
|
+
const channel = sootsimRuntime.resolveChannel()
|
|
24
|
+
if (runtime.isDevBridge) {
|
|
25
|
+
console.log(runtime.primary)
|
|
26
|
+
if (runtime.installedRuntime) console.log(`${runtime.installedRuntime} · ${channel}`)
|
|
27
|
+
} else if (runtime.primary === 'runtime not installed') {
|
|
28
|
+
console.log(`runtime not installed — run \`rnxsim runtime install\``)
|
|
29
|
+
} else {
|
|
30
|
+
console.log(`${runtime.primary} · ${channel}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { config } = await resolveSootSimAppConfig()
|
|
34
|
+
if (config?.runtimeVersion) {
|
|
35
|
+
const installed = fs.existsSync(
|
|
36
|
+
path.join(runtimeDir(config.runtimeVersion), 'index.html'),
|
|
37
|
+
)
|
|
38
|
+
console.log(
|
|
39
|
+
`project runtime v${config.runtimeVersion} · configured · ${
|
|
40
|
+
installed ? 'installed' : 'installs on open'
|
|
41
|
+
}`,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// best-effort network check; never fail the command on a flaky network.
|
|
46
|
+
const check = await sootsimRuntime.checkUpToDate({ channel })
|
|
47
|
+
if (check.outdated && check.latest) {
|
|
48
|
+
console.log(
|
|
49
|
+
`\n ↑ default runtime v${check.latest} available; run \`rnxsim upgrade\``,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(`\n what's new: ${SOOTSIM_CHANGELOG_URL}`)
|
|
54
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// sootsim what-happened — show recent events from the semantic timeline
|
|
2
|
+
//
|
|
3
|
+
// the agent-facing read verb for SootSim.bridges.timeline. by default
|
|
4
|
+
// shows everything since the previous CLI call from the same agent
|
|
5
|
+
// identity (cursor model). after rendering, advances the cursor past
|
|
6
|
+
// the events it displayed so the next call shows only new events.
|
|
7
|
+
//
|
|
8
|
+
// usage:
|
|
9
|
+
// sootsim what-happened # since last call
|
|
10
|
+
// sootsim what-happened --summary # one-line counts
|
|
11
|
+
// sootsim what-happened --all # full ring (no cursor)
|
|
12
|
+
// sootsim what-happened --since 5s # absolute window
|
|
13
|
+
// sootsim what-happened --kinds toast,fetch # filter
|
|
14
|
+
// sootsim what-happened --json # structured
|
|
15
|
+
// sootsim what-happened --limit 50 # cap (default 200)
|
|
16
|
+
|
|
17
|
+
import { getCliIdentityKey } from '../current-sim'
|
|
18
|
+
import { createBridgeFromParsed, parseBridgeCliArgs } from '../ws-bridge'
|
|
19
|
+
import {
|
|
20
|
+
formatTimelineEvent,
|
|
21
|
+
formatTimelineSummary,
|
|
22
|
+
inspectTimelineAdvanceCursor,
|
|
23
|
+
inspectTimelineRecent,
|
|
24
|
+
inspectTimelineSummary,
|
|
25
|
+
NOISY_TIMELINE_KINDS,
|
|
26
|
+
} from './inspect/core'
|
|
27
|
+
import type { SootSimTimelineEvent, SootSimTimelineQuery } from '@sootsim/globals'
|
|
28
|
+
|
|
29
|
+
interface WhatHappenedOptions {
|
|
30
|
+
port?: number
|
|
31
|
+
verbose?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseSinceFlag(args: string[]): { since?: number; consumed: number[] } {
|
|
35
|
+
const consumed: number[] = []
|
|
36
|
+
for (let i = 0; i < args.length; i++) {
|
|
37
|
+
if (args[i] === '--since' && i + 1 < args.length) {
|
|
38
|
+
consumed.push(i, i + 1)
|
|
39
|
+
const v = args[i + 1].trim()
|
|
40
|
+
// accept '5s', '500ms', or absolute ms epoch
|
|
41
|
+
const m = /^(\d+(?:\.\d+)?)(ms|s|m)?$/.exec(v)
|
|
42
|
+
if (m) {
|
|
43
|
+
const n = Number(m[1])
|
|
44
|
+
const unit = m[2] ?? 'ms'
|
|
45
|
+
const ms = unit === 's' ? n * 1000 : unit === 'm' ? n * 60_000 : n
|
|
46
|
+
return { since: Date.now() - ms, consumed }
|
|
47
|
+
}
|
|
48
|
+
const asNum = Number(v)
|
|
49
|
+
if (Number.isFinite(asNum) && asNum > 1_000_000_000_000) {
|
|
50
|
+
return { since: asNum, consumed }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { consumed }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseKindsFlag(args: string[]): { kinds?: string[]; consumed: number[] } {
|
|
58
|
+
const consumed: number[] = []
|
|
59
|
+
for (let i = 0; i < args.length; i++) {
|
|
60
|
+
if (args[i] === '--kinds' && i + 1 < args.length) {
|
|
61
|
+
consumed.push(i, i + 1)
|
|
62
|
+
return {
|
|
63
|
+
kinds: args[i + 1]
|
|
64
|
+
.split(',')
|
|
65
|
+
.map((s) => s.trim())
|
|
66
|
+
.filter(Boolean),
|
|
67
|
+
consumed,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { consumed }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseLimitFlag(args: string[]): { limit?: number; consumed: number[] } {
|
|
75
|
+
const consumed: number[] = []
|
|
76
|
+
for (let i = 0; i < args.length; i++) {
|
|
77
|
+
if (args[i] === '--limit' && i + 1 < args.length) {
|
|
78
|
+
consumed.push(i, i + 1)
|
|
79
|
+
const n = Number(args[i + 1])
|
|
80
|
+
if (Number.isFinite(n) && n > 0) return { limit: n, consumed }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { consumed }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface FlowSection {
|
|
87
|
+
label: string
|
|
88
|
+
events: SootSimTimelineEvent[]
|
|
89
|
+
startedAt: number | null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// section the events by screen/route boundaries for `what-happened
|
|
93
|
+
// --flow`. each screen-name (or route-path) becomes a section
|
|
94
|
+
// header; every event that lands between two screen changes belongs
|
|
95
|
+
// to the first one's section. events before the first screen change
|
|
96
|
+
// land in an "initial state" section so nothing gets dropped.
|
|
97
|
+
//
|
|
98
|
+
// design note: this groups by *appearance* events only (phase ===
|
|
99
|
+
// 'enter' or no phase). disappearance events (`exit`) end the
|
|
100
|
+
// current section but don't start a new one — they're listed inside
|
|
101
|
+
// the section that contained them.
|
|
102
|
+
function groupByScreen(events: SootSimTimelineEvent[]): FlowSection[] {
|
|
103
|
+
const sections: FlowSection[] = []
|
|
104
|
+
let current: FlowSection = {
|
|
105
|
+
label: 'initial state',
|
|
106
|
+
events: [],
|
|
107
|
+
startedAt: events[0]?.t ?? null,
|
|
108
|
+
}
|
|
109
|
+
sections.push(current)
|
|
110
|
+
for (const ev of events) {
|
|
111
|
+
current.events.push(ev)
|
|
112
|
+
if (ev.kind === 'screen' || ev.kind === 'route') {
|
|
113
|
+
const data = ev.data as Record<string, unknown> | null
|
|
114
|
+
const phase = data?.phase as string | undefined
|
|
115
|
+
// only treat 'enter' (and the legacy no-phase form) as a
|
|
116
|
+
// section boundary. an 'exit' event is the tail of a screen,
|
|
117
|
+
// not the start of a new one.
|
|
118
|
+
if (!phase || phase === 'enter' || phase === 'appear' || phase === 'active') {
|
|
119
|
+
const name =
|
|
120
|
+
(data?.name as string) ||
|
|
121
|
+
(data?.activeName as string) ||
|
|
122
|
+
(data?.path as string) ||
|
|
123
|
+
(data?.pathname as string) ||
|
|
124
|
+
ev.kind
|
|
125
|
+
if (sections.length === 1 && current.events.length === 1) {
|
|
126
|
+
// first screen event — rename "initial state" inline so it
|
|
127
|
+
// doesn't show an empty initial-state section before it.
|
|
128
|
+
current.label = `${ev.kind}: ${name}`
|
|
129
|
+
} else {
|
|
130
|
+
current = {
|
|
131
|
+
label: `${ev.kind}: ${name}`,
|
|
132
|
+
events: [],
|
|
133
|
+
startedAt: ev.t,
|
|
134
|
+
}
|
|
135
|
+
sections.push(current)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return sections
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function runWhatHappened(args: string[], opts: WhatHappenedOptions) {
|
|
144
|
+
const parsed = parseBridgeCliArgs(args, {
|
|
145
|
+
port: opts.port,
|
|
146
|
+
stripBooleanFlags: [
|
|
147
|
+
'--summary',
|
|
148
|
+
'--all',
|
|
149
|
+
'--json',
|
|
150
|
+
'--no-advance',
|
|
151
|
+
'--help',
|
|
152
|
+
'-h',
|
|
153
|
+
// `--flow` groups events under screen/route boundaries instead of
|
|
154
|
+
// printing a strict chronological list. demo-friendly for the
|
|
155
|
+
// "what just happened in this sim" recap.
|
|
156
|
+
'--flow',
|
|
157
|
+
// by default we hide high-frequency render/layout/scroll noise so
|
|
158
|
+
// a single tap doesn't get buried under 1000+ rows. enable
|
|
159
|
+
// `react-commit` with `sootsim timeline start react-commit`, then pass
|
|
160
|
+
// `--noisy` (or an explicit `--kinds react-commit,…` filter) when you
|
|
161
|
+
// actually want to see them.
|
|
162
|
+
'--noisy',
|
|
163
|
+
],
|
|
164
|
+
stripValueFlags: ['--since', '--kinds', '--limit'],
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
168
|
+
console.log(`
|
|
169
|
+
rnxsim what-happened — show recent events from the semantic timeline
|
|
170
|
+
|
|
171
|
+
usage:
|
|
172
|
+
rnxsim what-happened # since last CLI call
|
|
173
|
+
rnxsim what-happened --summary # one-line counts
|
|
174
|
+
rnxsim what-happened --all # full ring (ignore cursor)
|
|
175
|
+
rnxsim what-happened --since 5s # absolute window
|
|
176
|
+
rnxsim what-happened --kinds toast,fetch
|
|
177
|
+
rnxsim what-happened --noisy # include react-commit/layout/scroll
|
|
178
|
+
rnxsim what-happened --limit 50
|
|
179
|
+
rnxsim what-happened --json
|
|
180
|
+
rnxsim what-happened --no-advance # don't advance cursor after read
|
|
181
|
+
|
|
182
|
+
note: react-commit, layout, and scroll are opt-in/noisy events. enable them
|
|
183
|
+
with "rnxsim timeline start <kind>", then pass --noisy or include them in
|
|
184
|
+
--kinds to see them.
|
|
185
|
+
`)
|
|
186
|
+
process.exit(0)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const summaryMode = args.includes('--summary')
|
|
190
|
+
const flowMode = args.includes('--flow')
|
|
191
|
+
const allMode = args.includes('--all')
|
|
192
|
+
const jsonMode = args.includes('--json')
|
|
193
|
+
const noAdvance = args.includes('--no-advance')
|
|
194
|
+
const noisyMode = args.includes('--noisy')
|
|
195
|
+
|
|
196
|
+
const { since } = parseSinceFlag(args)
|
|
197
|
+
const { kinds } = parseKindsFlag(args)
|
|
198
|
+
const { limit } = parseLimitFlag(args)
|
|
199
|
+
|
|
200
|
+
const cursorKey = getCliIdentityKey()
|
|
201
|
+
const query: SootSimTimelineQuery = {
|
|
202
|
+
limit: limit ?? 200,
|
|
203
|
+
...(kinds && kinds.length ? { kinds: kinds as SootSimTimelineQuery['kinds'] } : {}),
|
|
204
|
+
...(since !== undefined ? { since } : allMode ? {} : { sinceCursor: cursorKey }),
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const bridge = createBridgeFromParsed(parsed)
|
|
208
|
+
try {
|
|
209
|
+
if (summaryMode) {
|
|
210
|
+
const summary = await inspectTimelineSummary(bridge, query)
|
|
211
|
+
if (jsonMode) {
|
|
212
|
+
console.log(JSON.stringify(summary))
|
|
213
|
+
} else {
|
|
214
|
+
const since_label = allMode
|
|
215
|
+
? 'all time'
|
|
216
|
+
: since !== undefined
|
|
217
|
+
? `last ${((Date.now() - since) / 1000).toFixed(1)}s`
|
|
218
|
+
: 'since last call'
|
|
219
|
+
console.log(` ${since_label}: ${formatTimelineSummary(summary)}`)
|
|
220
|
+
}
|
|
221
|
+
// advance cursor past the summary window's end
|
|
222
|
+
if (!noAdvance && !allMode && summary.lastAt) {
|
|
223
|
+
await inspectTimelineAdvanceCursor(bridge, cursorKey, summary.lastAt)
|
|
224
|
+
}
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const result = await inspectTimelineRecent(bridge, query)
|
|
229
|
+
|
|
230
|
+
// hide high-frequency render/layout/scroll noise unless the caller
|
|
231
|
+
// explicitly asked for it (--noisy or --kinds containing one of
|
|
232
|
+
// these). this is the single most common reason `what-happened`
|
|
233
|
+
// output drowns out the meaningful events for the user/agent.
|
|
234
|
+
const userPickedNoisyKind =
|
|
235
|
+
Array.isArray(kinds) && kinds.some((k) => NOISY_TIMELINE_KINDS.has(k))
|
|
236
|
+
const suppressNoisy = !noisyMode && !userPickedNoisyKind
|
|
237
|
+
let suppressedNoisyCount = 0
|
|
238
|
+
if (suppressNoisy) {
|
|
239
|
+
const kept = result.events.filter((ev) => {
|
|
240
|
+
if (NOISY_TIMELINE_KINDS.has(ev.kind)) {
|
|
241
|
+
suppressedNoisyCount += 1
|
|
242
|
+
return false
|
|
243
|
+
}
|
|
244
|
+
return true
|
|
245
|
+
})
|
|
246
|
+
result.events = kept
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (jsonMode) {
|
|
250
|
+
if (flowMode) {
|
|
251
|
+
// grouped JSON shape — sections by screen/route. agents who want
|
|
252
|
+
// a chronological stream can still get it from the default
|
|
253
|
+
// shape (without --flow).
|
|
254
|
+
console.log(JSON.stringify(groupByScreen(result.events), null, 2))
|
|
255
|
+
} else {
|
|
256
|
+
console.log(JSON.stringify(result, null, 2))
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
if (result.events.length === 0) {
|
|
260
|
+
// distinguish "truly no events" from "events present but all
|
|
261
|
+
// filtered as noise". the second case is the much more common
|
|
262
|
+
// misdiagnosis — agents think a tap did nothing when actually
|
|
263
|
+
// it just produced react-commits and no shell-state change.
|
|
264
|
+
if (suppressedNoisyCount > 0) {
|
|
265
|
+
console.log(
|
|
266
|
+
` no non-noise events (${suppressedNoisyCount} react-commit/layout/scroll hidden)`,
|
|
267
|
+
)
|
|
268
|
+
} else if (allMode) {
|
|
269
|
+
console.log(' no events recorded')
|
|
270
|
+
} else if (since !== undefined) {
|
|
271
|
+
console.log(' no events in window')
|
|
272
|
+
} else {
|
|
273
|
+
console.log(' no new events since last call')
|
|
274
|
+
}
|
|
275
|
+
} else if (flowMode) {
|
|
276
|
+
// section-per-screen render. each section heads with the
|
|
277
|
+
// screen/route name and lists every event that landed in
|
|
278
|
+
// that section in chronological order. unattributed events
|
|
279
|
+
// (events that landed before the first screen change) go in
|
|
280
|
+
// an "initial state" section.
|
|
281
|
+
const anchor = result.events[0]?.t ?? null
|
|
282
|
+
const sections = groupByScreen(result.events)
|
|
283
|
+
const header = allMode
|
|
284
|
+
? `─── ${result.events.length} event(s) total — flow view ───`
|
|
285
|
+
: since !== undefined
|
|
286
|
+
? `─── ${result.events.length} event(s) in last ${((Date.now() - since) / 1000).toFixed(1)}s — flow view ───`
|
|
287
|
+
: `─── ${result.events.length} event(s) since last call — flow view ───`
|
|
288
|
+
console.log(` ${header}`)
|
|
289
|
+
for (const section of sections) {
|
|
290
|
+
console.log(
|
|
291
|
+
`\n ── ${section.label} (${section.events.length} event${section.events.length === 1 ? '' : 's'}) ──`,
|
|
292
|
+
)
|
|
293
|
+
for (const ev of section.events) {
|
|
294
|
+
console.log(formatTimelineEvent(ev, anchor))
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
const anchor = result.events[0]?.t ?? null
|
|
299
|
+
const header = allMode
|
|
300
|
+
? `─── ${result.events.length} event(s) total ───`
|
|
301
|
+
: since !== undefined
|
|
302
|
+
? `─── ${result.events.length} event(s) in last ${((Date.now() - since) / 1000).toFixed(1)}s ───`
|
|
303
|
+
: `─── ${result.events.length} event(s) since last call ───`
|
|
304
|
+
console.log(` ${header}`)
|
|
305
|
+
for (const ev of result.events) {
|
|
306
|
+
console.log(formatTimelineEvent(ev, anchor))
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// when at least one real event printed, append the suppression
|
|
310
|
+
// note so the user knows the picture isn't complete. when the
|
|
311
|
+
// window was entirely noise, the empty-state branch above already
|
|
312
|
+
// mentioned it — don't double up.
|
|
313
|
+
if (suppressedNoisyCount > 0 && result.events.length > 0) {
|
|
314
|
+
process.stderr.write(
|
|
315
|
+
`\n ${suppressedNoisyCount} high-frequency event(s) hidden (react-commit/layout/scroll)\n` +
|
|
316
|
+
` rerun with --noisy or --kinds react-commit,layout,scroll to include them\n`,
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!noAdvance && !allMode && result.watermark > 0) {
|
|
322
|
+
await inspectTimelineAdvanceCursor(bridge, cursorKey, result.watermark)
|
|
323
|
+
}
|
|
324
|
+
} finally {
|
|
325
|
+
bridge.close()
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'
|
|
2
|
+
import { tmpdir } from 'os'
|
|
3
|
+
import { dirname, join, resolve } from 'path'
|
|
4
|
+
import { getParentPid, getProcessComm } from './parent-pid'
|
|
5
|
+
|
|
6
|
+
const CURRENT_SIM_VERSION = 1 as const
|
|
7
|
+
const CURRENT_SIM_PATH_ENV = 'SOOTSIM_CLI_CURRENT_SIM_PATH'
|
|
8
|
+
|
|
9
|
+
interface CurrentSimState {
|
|
10
|
+
version: 1
|
|
11
|
+
simId: string
|
|
12
|
+
updatedAt: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// env vars that tend to identify a specific terminal, pane, or agent run.
|
|
16
|
+
// checked in order — the first one present wins. anything further down is a
|
|
17
|
+
// progressively weaker signal of "same caller across invocations".
|
|
18
|
+
const IDENTITY_ENV_VARS = [
|
|
19
|
+
'SOOTSIM_CLI_IDENTITY', // explicit override — fastest path, skips all pid lookups
|
|
20
|
+
'TM_SESSION', // Team Machine session id; routes prompt-bar followups back to this agent
|
|
21
|
+
'CLAUDE_CODE_SESSION_ID', // claude code, if ever set
|
|
22
|
+
'CODEX_THREAD_ID', // codex / openai agent thread, stable across tool invocations
|
|
23
|
+
'TERM_SESSION_ID', // macOS Terminal.app + iTerm2
|
|
24
|
+
'ITERM_SESSION_ID', // iTerm2 (older variant)
|
|
25
|
+
'TMUX_PANE', // tmux — stable per pane
|
|
26
|
+
'STY', // GNU screen session
|
|
27
|
+
'KITTY_WINDOW_ID', // kitty
|
|
28
|
+
'WEZTERM_PANE', // wezterm
|
|
29
|
+
'ALACRITTY_WINDOW_ID', // alacritty
|
|
30
|
+
'WINDOWID', // X11 terminals
|
|
31
|
+
'VSCODE_INJECTION', // vscode integrated terminal
|
|
32
|
+
] as const
|
|
33
|
+
|
|
34
|
+
export interface CliIdentity {
|
|
35
|
+
key: string
|
|
36
|
+
source: (typeof IDENTITY_ENV_VARS)[number] | 'grand-ppid' | 'ppid' | 'explicit-sim-id'
|
|
37
|
+
stable: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// derive a stable per-terminal identity key so different agents/terminals
|
|
41
|
+
// don't share state. priority:
|
|
42
|
+
// 1. explicit env vars (iTerm, tmux, etc.)
|
|
43
|
+
// 2. grand-ppid — the parent of our invoking shell, which in Claude Code's
|
|
44
|
+
// Bash tool is the per-agent claude process (stable across invocations,
|
|
45
|
+
// distinct between agents). generic — not claude-specific.
|
|
46
|
+
// 3. ppid — the invoking shell's pid. stable when the shell is reused
|
|
47
|
+
// across invocations (claude's bash tool, persistent tty sessions).
|
|
48
|
+
// unstable when each call spawns a fresh shell — in that case set an
|
|
49
|
+
// env var from IDENTITY_ENV_VARS to pin the identity.
|
|
50
|
+
// an earlier env-hash fallback was removed: sha'ing process.env looks stable
|
|
51
|
+
// but in practice any per-turn env drift (timestamps, cwd, agent vars)
|
|
52
|
+
// flipped the hash and made the same agent look like a new client.
|
|
53
|
+
// walk up to the topmost non-init ancestor. needed because intermediate
|
|
54
|
+
// pids drift across invocations:
|
|
55
|
+
// - claude code's bash tool spawns a fresh shell per call, so the
|
|
56
|
+
// immediate gppid (the shell's parent) can change
|
|
57
|
+
// - shell pipelines (`sootsim list | grep`) wrap the command in a
|
|
58
|
+
// subshell, adding an extra ppid layer that shifts gppid down by one
|
|
59
|
+
// the topmost user process (just below init/launchd) is stable across
|
|
60
|
+
// all of these — same agent / login shell / terminal across calls.
|
|
61
|
+
// terminal-multiplexer servers are effectively immortal (the tmux server
|
|
62
|
+
// lives while ANY session exists), so an owner-pid walk that climbs into one
|
|
63
|
+
// produces an owner that never dies — detached browser hosts then leak until
|
|
64
|
+
// an explicit close (real incident: a tmux-launched sim's chrome survived its
|
|
65
|
+
// pane at 129% cpu). the Team Machine daemon is the same class: it spawns worker
|
|
66
|
+
// harnesses directly, outlives them all, and additionally collapses every
|
|
67
|
+
// worker into one shared identity key (real incident 2026-07-14: three film
|
|
68
|
+
// render hosts leaked with owner = the daemon while load hit 224). stop
|
|
69
|
+
// BELOW these hosts: the child (pane shell / worker harness) dies with the
|
|
70
|
+
// session, which is the lifetime a sim owner should have.
|
|
71
|
+
// these are matched against libproc's pbi_comm, the executable's basename,
|
|
72
|
+
// truncated to 16 chars. Team Machine reports comm `tm`; matching its product
|
|
73
|
+
// name never fired and every agent-launched sim resolved its owner to the terminal
|
|
74
|
+
// tab's /usr/bin/login instead (real incident 2026-08-06: six chrome hosts
|
|
75
|
+
// survived their agents, 17GB rss, load 19 with the GPU saturated).
|
|
76
|
+
const IMMORTAL_HOST_COMM_RE = /^(?:tmux|screen)|^tm$/i
|
|
77
|
+
|
|
78
|
+
function findTopAncestor(start: number, maxDepth = 20): number | null {
|
|
79
|
+
// check each CANDIDATE's own comm and return the child below a multiplexer
|
|
80
|
+
// (prev). checking only the parent's comm looked equivalent but was not: on
|
|
81
|
+
// macOS the tmux server is launchd's direct child, so it arrives as the
|
|
82
|
+
// candidate via the initializer and the parent<=1 branch returned it before
|
|
83
|
+
// any comm check ran (runtime-caught on a real tmux box: owner resolved to
|
|
84
|
+
// the immortal server). prev starts at `start` so a pane shell directly
|
|
85
|
+
// under the server resolves to itself.
|
|
86
|
+
let prev = start
|
|
87
|
+
let pid = getParentPid(start)
|
|
88
|
+
if (!pid || pid <= 1) {
|
|
89
|
+
// a readable ordinary process directly below init is itself the stable
|
|
90
|
+
// session root. this is the shape produced by detached job launchers, and
|
|
91
|
+
// returning null here used to disable the browser owner's lifetime watch.
|
|
92
|
+
// an unreadable or known-immortal process has no defensible child bound at
|
|
93
|
+
// this point, so leave it unresolved and make the launcher fail loudly.
|
|
94
|
+
const comm = getProcessComm(start)
|
|
95
|
+
return start > 1 && comm && !IMMORTAL_HOST_COMM_RE.test(comm) ? start : null
|
|
96
|
+
}
|
|
97
|
+
for (let i = 0; i < maxDepth; i++) {
|
|
98
|
+
const comm = getProcessComm(pid)
|
|
99
|
+
if (comm && IMMORTAL_HOST_COMM_RE.test(comm)) return prev
|
|
100
|
+
const parent = getParentPid(pid)
|
|
101
|
+
if (!parent || parent <= 1) {
|
|
102
|
+
// top of the tree we can read. an unreadable comm means a root-owned
|
|
103
|
+
// session host — /usr/bin/login under a terminal tab — which outlives
|
|
104
|
+
// every agent and shell inside it, so stop below it. a readable top is
|
|
105
|
+
// an ordinary user process (a CI job root) and is a valid owner.
|
|
106
|
+
return comm ? pid : prev
|
|
107
|
+
}
|
|
108
|
+
prev = pid
|
|
109
|
+
pid = parent
|
|
110
|
+
}
|
|
111
|
+
return pid
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getCliIdentity(): CliIdentity {
|
|
115
|
+
for (const name of IDENTITY_ENV_VARS) {
|
|
116
|
+
const value = process.env[name]
|
|
117
|
+
if (value && value.trim()) {
|
|
118
|
+
return { key: `${name}:${value.trim()}`, source: name, stable: true }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const topPid = findTopAncestor(process.ppid)
|
|
122
|
+
if (topPid && topPid > 1) {
|
|
123
|
+
return { key: `gppid-${topPid}`, source: 'grand-ppid', stable: true }
|
|
124
|
+
}
|
|
125
|
+
return { key: `pid-${process.ppid}`, source: 'ppid', stable: false }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function getCliIdentityKey(): string {
|
|
129
|
+
return getCliIdentity().key
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// the stable owning-session pid: the topmost non-init ancestor of this CLI
|
|
133
|
+
// process — in Claude Code's Bash tool the per-agent process, otherwise the
|
|
134
|
+
// login shell / terminal / CI process-tree root. it lives exactly as long as
|
|
135
|
+
// the session that wants a sim, and dies when that session does. used to tie
|
|
136
|
+
// a detached browser host's lifetime to its owner so the host self-terminates
|
|
137
|
+
// (closing Chrome) when the owner exits, instead of leaking a headless
|
|
138
|
+
// browser forever. returns null on platforms where ancestry can't be read.
|
|
139
|
+
export function getStableOwnerPid(): number | null {
|
|
140
|
+
return findTopAncestor(process.ppid)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getCurrentSimPath() {
|
|
144
|
+
if (process.env[CURRENT_SIM_PATH_ENV]) {
|
|
145
|
+
return resolve(process.env[CURRENT_SIM_PATH_ENV])
|
|
146
|
+
}
|
|
147
|
+
const key = getCliIdentityKey()
|
|
148
|
+
return join(tmpdir(), `sootsim-current-sim-${key}.json`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function readCurrentSim(): CurrentSimState | null {
|
|
152
|
+
const filepath = getCurrentSimPath()
|
|
153
|
+
if (!existsSync(filepath)) return null
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const parsed = JSON.parse(readFileSync(filepath, 'utf8')) as Partial<CurrentSimState>
|
|
157
|
+
if (
|
|
158
|
+
parsed.version !== CURRENT_SIM_VERSION ||
|
|
159
|
+
typeof parsed.simId !== 'string' ||
|
|
160
|
+
!parsed.simId.trim() ||
|
|
161
|
+
typeof parsed.updatedAt !== 'string'
|
|
162
|
+
) {
|
|
163
|
+
rmSync(filepath, { force: true })
|
|
164
|
+
return null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
version: CURRENT_SIM_VERSION,
|
|
169
|
+
simId: parsed.simId.trim(),
|
|
170
|
+
updatedAt: parsed.updatedAt,
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
rmSync(filepath, { force: true })
|
|
174
|
+
return null
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function readCurrentSimId() {
|
|
179
|
+
return readCurrentSim()?.simId || null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function saveCurrentSimId(simId: string) {
|
|
183
|
+
const normalized = simId.trim()
|
|
184
|
+
if (!normalized) return
|
|
185
|
+
|
|
186
|
+
const filepath = getCurrentSimPath()
|
|
187
|
+
mkdirSync(dirname(filepath), { recursive: true })
|
|
188
|
+
writeFileSync(
|
|
189
|
+
filepath,
|
|
190
|
+
JSON.stringify(
|
|
191
|
+
{
|
|
192
|
+
version: CURRENT_SIM_VERSION,
|
|
193
|
+
simId: normalized,
|
|
194
|
+
updatedAt: new Date().toISOString(),
|
|
195
|
+
} satisfies CurrentSimState,
|
|
196
|
+
null,
|
|
197
|
+
2,
|
|
198
|
+
) + '\n',
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function clearCurrentSimId() {
|
|
203
|
+
rmSync(getCurrentSimPath(), { force: true })
|
|
204
|
+
}
|