peachsim 0.1.527

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (324) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +640 -0
  3. package/cli/app-config.ts +82 -0
  4. package/cli/app-fonts.ts +388 -0
  5. package/cli/app-project.ts +59 -0
  6. package/cli/app-splash.ts +171 -0
  7. package/cli/app-state-reset.ts +26 -0
  8. package/cli/auth.ts +184 -0
  9. package/cli/bin.ts +59 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2806 -0
  12. package/cli/browser-backend.ts +44 -0
  13. package/cli/browser-evals.ts +96 -0
  14. package/cli/cli-update.ts +261 -0
  15. package/cli/cloud-client.ts +1530 -0
  16. package/cli/cloud-dispatch.ts +206 -0
  17. package/cli/cloud-session.ts +227 -0
  18. package/cli/command-contract.ts +221 -0
  19. package/cli/command-registry.ts +304 -0
  20. package/cli/commands/agent-wrapper.ts +1048 -0
  21. package/cli/commands/agent.ts +439 -0
  22. package/cli/commands/app-fonts.ts +99 -0
  23. package/cli/commands/assert.ts +542 -0
  24. package/cli/commands/auth.ts +71 -0
  25. package/cli/commands/box/bundle-plane.ts +71 -0
  26. package/cli/commands/box/checkout-plane.ts +256 -0
  27. package/cli/commands/box/plane-share.ts +208 -0
  28. package/cli/commands/box/rnx-command.ts +60 -0
  29. package/cli/commands/box/search-command.ts +157 -0
  30. package/cli/commands/box.ts +925 -0
  31. package/cli/commands/camera.ts +266 -0
  32. package/cli/commands/cleanup.ts +169 -0
  33. package/cli/commands/compat.ts +176 -0
  34. package/cli/commands/config.ts +34 -0
  35. package/cli/commands/control.ts +2154 -0
  36. package/cli/commands/cpu-profile.ts +941 -0
  37. package/cli/commands/daemon-mac-app.ts +165 -0
  38. package/cli/commands/daemon.ts +800 -0
  39. package/cli/commands/debug.ts +751 -0
  40. package/cli/commands/desktop.ts +39 -0
  41. package/cli/commands/detect.ts +201 -0
  42. package/cli/commands/detox.ts +481 -0
  43. package/cli/commands/device.ts +134 -0
  44. package/cli/commands/diagnose.ts +589 -0
  45. package/cli/commands/do-chain.ts +408 -0
  46. package/cli/commands/electron.ts +94 -0
  47. package/cli/commands/film.ts +379 -0
  48. package/cli/commands/flow.ts +1240 -0
  49. package/cli/commands/inspect/actions.ts +516 -0
  50. package/cli/commands/inspect/core.ts +2086 -0
  51. package/cli/commands/inspect/count.ts +17 -0
  52. package/cli/commands/inspect/describe.ts +119 -0
  53. package/cli/commands/inspect/env.ts +25 -0
  54. package/cli/commands/inspect/find.ts +169 -0
  55. package/cli/commands/inspect/get-layout.ts +53 -0
  56. package/cli/commands/inspect/keyboard.ts +54 -0
  57. package/cli/commands/inspect/list.ts +175 -0
  58. package/cli/commands/inspect/memory.ts +215 -0
  59. package/cli/commands/inspect/redaction.ts +39 -0
  60. package/cli/commands/inspect/resolve-target.ts +77 -0
  61. package/cli/commands/inspect/screens.ts +78 -0
  62. package/cli/commands/inspect/settle.ts +32 -0
  63. package/cli/commands/inspect/settling.ts +197 -0
  64. package/cli/commands/inspect/shared.ts +259 -0
  65. package/cli/commands/inspect/sleep.ts +15 -0
  66. package/cli/commands/inspect/tree.ts +23 -0
  67. package/cli/commands/inspect/url.ts +17 -0
  68. package/cli/commands/inspect/wait-event.ts +211 -0
  69. package/cli/commands/inspect/wait-idle.ts +25 -0
  70. package/cli/commands/inspect/wait-ready.ts +87 -0
  71. package/cli/commands/inspect/wait-selector.ts +59 -0
  72. package/cli/commands/inspect/wait.ts +32 -0
  73. package/cli/commands/inspect.ts +4738 -0
  74. package/cli/commands/install-cli.ts +275 -0
  75. package/cli/commands/install-desktop.ts +385 -0
  76. package/cli/commands/login.ts +357 -0
  77. package/cli/commands/logout.ts +24 -0
  78. package/cli/commands/maestro-generate.ts +375 -0
  79. package/cli/commands/maestro.ts +536 -0
  80. package/cli/commands/mode.ts +58 -0
  81. package/cli/commands/no-bridge-hint.ts +80 -0
  82. package/cli/commands/perf.ts +67 -0
  83. package/cli/commands/permissions.ts +203 -0
  84. package/cli/commands/platform.ts +410 -0
  85. package/cli/commands/preview.ts +103 -0
  86. package/cli/commands/profile.ts +106 -0
  87. package/cli/commands/react.ts +353 -0
  88. package/cli/commands/record.ts +1435 -0
  89. package/cli/commands/report-issue.ts +305 -0
  90. package/cli/commands/reset.ts +89 -0
  91. package/cli/commands/runtime.ts +348 -0
  92. package/cli/commands/screenshot-command.ts +106 -0
  93. package/cli/commands/screenshot-layers.ts +144 -0
  94. package/cli/commands/screenshot-mode.ts +38 -0
  95. package/cli/commands/screenshot.ts +492 -0
  96. package/cli/commands/screenshots-capture.ts +608 -0
  97. package/cli/commands/screenshots.ts +127 -0
  98. package/cli/commands/serve.ts +139 -0
  99. package/cli/commands/setup.ts +416 -0
  100. package/cli/commands/shell-boolean-mode.ts +82 -0
  101. package/cli/commands/skills.ts +468 -0
  102. package/cli/commands/slides.ts +357 -0
  103. package/cli/commands/state.ts +94 -0
  104. package/cli/commands/storage.ts +58 -0
  105. package/cli/commands/telemetry.ts +55 -0
  106. package/cli/commands/test.ts +535 -0
  107. package/cli/commands/three-mode.ts +771 -0
  108. package/cli/commands/timeline.ts +121 -0
  109. package/cli/commands/upgrade.ts +167 -0
  110. package/cli/commands/upload.ts +1203 -0
  111. package/cli/commands/version.ts +63 -0
  112. package/cli/commands/welcome.ts +17 -0
  113. package/cli/commands/what-happened.ts +328 -0
  114. package/cli/current-sim.ts +217 -0
  115. package/cli/desktop-companion.ts +315 -0
  116. package/cli/drivers/electron.ts +70 -0
  117. package/cli/drivers/index.ts +20 -0
  118. package/cli/drivers/playwright-provisioning.ts +184 -0
  119. package/cli/drivers/playwright-sim-host.ts +537 -0
  120. package/cli/drivers/playwright.ts +209 -0
  121. package/cli/drivers/registry.ts +65 -0
  122. package/cli/drivers/types.ts +102 -0
  123. package/cli/flow-export.ts +55 -0
  124. package/cli/flow-file.ts +142 -0
  125. package/cli/flow-live-status.ts +126 -0
  126. package/cli/flow-session.ts +140 -0
  127. package/cli/help-core.ts +143 -0
  128. package/cli/help.ts +44 -0
  129. package/cli/hidden-runtime-alias.ts +19 -0
  130. package/cli/hints.ts +214 -0
  131. package/cli/inspect-notice-state.ts +114 -0
  132. package/cli/internal-child.ts +176 -0
  133. package/cli/maestro-js.ts +323 -0
  134. package/cli/main.ts +704 -0
  135. package/cli/open-url.ts +8 -0
  136. package/cli/outbound-endpoints.ts +830 -0
  137. package/cli/parent-pid.ts +204 -0
  138. package/cli/parse-args.ts +189 -0
  139. package/cli/privacy.ts +53 -0
  140. package/cli/prompt.ts +56 -0
  141. package/cli/recording-access.ts +108 -0
  142. package/cli/registry.ts +2 -0
  143. package/cli/resolve-assets.ts +63 -0
  144. package/cli/rnx-in-process.ts +98 -0
  145. package/cli/run-registry.ts +226 -0
  146. package/cli/run-rnx-core.ts +148 -0
  147. package/cli/run-rnx.ts +137 -0
  148. package/cli/runtime-notes.ts +66 -0
  149. package/cli/runtime-summary.ts +25 -0
  150. package/cli/secure-file.ts +31 -0
  151. package/cli/self-invocation.ts +58 -0
  152. package/cli/send-to-box.ts +109 -0
  153. package/cli/setup-repository.ts +191 -0
  154. package/cli/shell-init.ts +98 -0
  155. package/cli/standalone.ts +4 -0
  156. package/cli/telemetry.ts +84 -0
  157. package/cli/ui.ts +68 -0
  158. package/cli/ws-bridge.ts +1020 -0
  159. package/detox/attempt-events.cjs +23 -0
  160. package/detox/colors.ts +54 -0
  161. package/detox/config-loader.ts +135 -0
  162. package/detox/element-types.ts +48 -0
  163. package/detox/expectations.ts +534 -0
  164. package/detox/gestures.ts +501 -0
  165. package/detox/index.ts +2694 -0
  166. package/detox/jest-preset.cjs +29 -0
  167. package/detox/jest-setup-after-env.cjs +22 -0
  168. package/detox/matchers.ts +30 -0
  169. package/detox/motion-change.cjs +109 -0
  170. package/detox/motion-change.d.cts +35 -0
  171. package/detox/navigation-lifecycle.ts +290 -0
  172. package/detox/navigation.ts +43 -0
  173. package/detox/proof-frame.cjs +114 -0
  174. package/detox/record-block-execution.cjs +78 -0
  175. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  176. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  177. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  178. package/detox/screenshots/theme-blue-toggle.png +0 -0
  179. package/detox/screenshots/theme-blue.png +0 -0
  180. package/detox/screenshots/theme-red-toggle.png +0 -0
  181. package/detox/screenshots/theme-red.png +0 -0
  182. package/dist-lib/agent-daemon-client.cjs +502 -0
  183. package/dist-lib/agent-events.cjs +48 -0
  184. package/dist-lib/agent-identity.cjs +53 -0
  185. package/dist-lib/agent-sessions.cjs +759 -0
  186. package/dist-lib/attached-projects.cjs +526 -0
  187. package/dist-lib/auth/shared-session.cjs +208 -0
  188. package/dist-lib/backend-origin.cjs +88 -0
  189. package/dist-lib/beta.cjs +41 -0
  190. package/dist-lib/beta.mjs +13 -0
  191. package/dist-lib/bridge-constants.cjs +161 -0
  192. package/dist-lib/bridge-contract-input.cjs +451 -0
  193. package/dist-lib/bridge-contract-input.mjs +414 -0
  194. package/dist-lib/bridge-contract.cjs +130 -0
  195. package/dist-lib/bridge-contract.mjs +94 -0
  196. package/dist-lib/capture-contract.cjs +220 -0
  197. package/dist-lib/capture-contract.mjs +190 -0
  198. package/dist-lib/cli-constants.cjs +32 -0
  199. package/dist-lib/cloud-contract.cjs +194 -0
  200. package/dist-lib/cloud-contract.mjs +145 -0
  201. package/dist-lib/cloud.cjs +670 -0
  202. package/dist-lib/cloud.mjs +639 -0
  203. package/dist-lib/config.cjs +96 -0
  204. package/dist-lib/detox/index.cjs +3789 -0
  205. package/dist-lib/detox/jest-preset.cjs +29 -0
  206. package/dist-lib/dev-bundle-resolution.cjs +290 -0
  207. package/dist-lib/home-paths.cjs +638 -0
  208. package/dist-lib/host/bridge-host.cjs +10903 -0
  209. package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
  210. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  211. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  212. package/dist-lib/host/replacement-module-handler.cjs +469 -0
  213. package/dist-lib/host/websocket-proxy.cjs +298 -0
  214. package/dist-lib/index.cjs +4268 -0
  215. package/dist-lib/jump-to-source-babel.cjs +93 -0
  216. package/dist-lib/jump-to-source-native.cjs +112 -0
  217. package/dist-lib/menu.cjs +646 -0
  218. package/dist-lib/menu.mjs +604 -0
  219. package/dist-lib/metro-fingerprint-registry.cjs +186 -0
  220. package/dist-lib/metro-fingerprint-registry.mjs +156 -0
  221. package/dist-lib/metro-production-bundle.cjs +329 -0
  222. package/dist-lib/metro-production-bundle.mjs +295 -0
  223. package/dist-lib/metro.cjs +904 -0
  224. package/dist-lib/profiles.cjs +246 -0
  225. package/dist-lib/public-brand.cjs +39 -0
  226. package/dist-lib/react-native-host-modules.cjs +51 -0
  227. package/dist-lib/react-native-host-modules.mjs +25 -0
  228. package/dist-lib/render-mode.cjs +54 -0
  229. package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
  230. package/dist-lib/sdk.cjs +2433 -0
  231. package/dist-lib/sdk.mjs +2336 -0
  232. package/dist-lib/skills.cjs +39132 -0
  233. package/dist-lib/swift-submit.cjs +92 -0
  234. package/dist-lib/swift-submit.mjs +65 -0
  235. package/dist-lib/swift.cjs +1646 -0
  236. package/dist-lib/vite.cjs +1591 -0
  237. package/package.json +290 -0
  238. package/scripts/dev-server-scanner.ts +1291 -0
  239. package/scripts/optional-demo-registry.ts +113 -0
  240. package/skills/contrast/SKILL.md +165 -0
  241. package/skills/rnx-debug/SKILL.md +433 -0
  242. package/skills/rnx-perf/SKILL.md +399 -0
  243. package/skills/rnx-setup/SKILL.md +172 -0
  244. package/skills/rnx-test/SKILL.md +321 -0
  245. package/skills/rnx-visual/SKILL.md +126 -0
  246. package/src/agent-daemon-client.ts +410 -0
  247. package/src/agent-events.ts +71 -0
  248. package/src/agent-identity.ts +23 -0
  249. package/src/agent-prompt.ts +71 -0
  250. package/src/agent-sessions.ts +580 -0
  251. package/src/app-url.ts +39 -0
  252. package/src/attached-projects.ts +539 -0
  253. package/src/auth/shared-session.ts +235 -0
  254. package/src/backend-origin.ts +70 -0
  255. package/src/beta.ts +19 -0
  256. package/src/bridge-constants.ts +175 -0
  257. package/src/bridge-contract-input.ts +502 -0
  258. package/src/bridge-contract.ts +558 -0
  259. package/src/browser-cache.ts +16 -0
  260. package/src/capture-contract.ts +587 -0
  261. package/src/cli-constants.ts +1 -0
  262. package/src/cli-version.ts +40 -0
  263. package/src/cloud-contract.ts +274 -0
  264. package/src/cloud.ts +762 -0
  265. package/src/codex-client.ts +234 -0
  266. package/src/config.ts +132 -0
  267. package/src/connect.ts +101 -0
  268. package/src/dev-bundle-resolution.ts +346 -0
  269. package/src/dev-server-open.ts +198 -0
  270. package/src/disk-cleanup.ts +540 -0
  271. package/src/flow-timeout-scale.ts +15 -0
  272. package/src/home-paths.ts +806 -0
  273. package/src/host/agent-host.ts +666 -0
  274. package/src/host/bridge-host.ts +2788 -0
  275. package/src/host/fetch-proxy-handler.ts +449 -0
  276. package/src/host/fetch-proxy-overrides.ts +51 -0
  277. package/src/host/open-url.ts +89 -0
  278. package/src/host/plane-host.ts +203 -0
  279. package/src/host/replacement-module-handler.ts +158 -0
  280. package/src/host/websocket-proxy.ts +315 -0
  281. package/src/index.ts +71 -0
  282. package/src/jump-to-source-babel.ts +135 -0
  283. package/src/jump-to-source-native.ts +102 -0
  284. package/src/menu.ts +777 -0
  285. package/src/metro-fingerprint-registry.ts +302 -0
  286. package/src/metro-plugin.ts +341 -0
  287. package/src/metro-production-bundle.ts +471 -0
  288. package/src/native-dev-bundle-url.ts +118 -0
  289. package/src/native-seam-manifest.ts +341 -0
  290. package/src/playwright-host-log.ts +16 -0
  291. package/src/plist.ts +27 -0
  292. package/src/poll-until.ts +123 -0
  293. package/src/profiles.ts +179 -0
  294. package/src/public-brand.ts +9 -0
  295. package/src/react-native-host-modules.ts +26 -0
  296. package/src/render-mode.ts +27 -0
  297. package/src/rolldown-plugin-swift.ts +240 -0
  298. package/src/runtime-assets.ts +233 -0
  299. package/src/runtime-delivery.ts +54 -0
  300. package/src/screenshots/compose.ts +422 -0
  301. package/src/screenshots/frame-compose.ts +514 -0
  302. package/src/screenshots/orchestrate.ts +256 -0
  303. package/src/screenshots/org-deck.ts +91 -0
  304. package/src/screenshots/registry.ts +58 -0
  305. package/src/screenshots/schema.ts +364 -0
  306. package/src/sdk.ts +15 -0
  307. package/src/settings-bundle.ts +418 -0
  308. package/src/shell-bridge-identity.ts +15 -0
  309. package/src/sim-client.ts +697 -0
  310. package/src/skills/builtin/a11y-review.ts +132 -0
  311. package/src/skills/builtin/compat-check.ts +71 -0
  312. package/src/skills/builtin/maestro-test.ts +121 -0
  313. package/src/skills/builtin/perf-profile.ts +90 -0
  314. package/src/skills/builtin/screenshot-all.ts +52 -0
  315. package/src/skills/builtin/visual-diff.ts +100 -0
  316. package/src/skills/registry.ts +107 -0
  317. package/src/skills/types.ts +41 -0
  318. package/src/swift-submit.ts +185 -0
  319. package/src/swift.ts +7 -0
  320. package/src/team-machine-prompt.ts +73 -0
  321. package/src/vite-plugin-one.ts +264 -0
  322. package/src/vite-plugin-swift.ts +620 -0
  323. package/src/vite-plugin.ts +1946 -0
  324. package/src/worklets-babel.ts +250 -0
@@ -0,0 +1,63 @@
1
+ // rnx version — one overview of both halves you're running.
2
+ //
3
+ // the rnx CLI 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
+ // `rnx --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 { RNX_CHANGELOG_URL, rnxRuntime } from '../../src/runtime-delivery'
14
+ import { resolveRNXAppConfig } from '../app-config'
15
+ import { readCachedCliUpdate, refreshCliUpdateCache } from '../cli-update'
16
+ import { getRuntimeSummary } from '../runtime-summary'
17
+ import { IS_STANDALONE } from '../standalone'
18
+
19
+ export async function runVersion(_args: string[]): Promise<void> {
20
+ const cliUpdatePromise = IS_STANDALONE
21
+ ? refreshCliUpdateCache().catch(() => readCachedCliUpdate())
22
+ : Promise.resolve(null)
23
+ const { IS_BETA, BETA_LABEL } = await import('../../src/beta')
24
+ const suffix = IS_BETA ? ` · ${BETA_LABEL}` : ''
25
+ console.log(`rnx v${getCliVersion()}${suffix}`)
26
+
27
+ const runtime = getRuntimeSummary()
28
+ const channel = rnxRuntime.resolveChannel()
29
+ if (runtime.isDevBridge) {
30
+ console.log(runtime.primary)
31
+ if (runtime.installedRuntime) console.log(`${runtime.installedRuntime} · ${channel}`)
32
+ } else if (runtime.primary === 'runtime not installed') {
33
+ console.log(`runtime not installed — run \`rnx runtime install\``)
34
+ } else {
35
+ console.log(`${runtime.primary} · ${channel}`)
36
+ }
37
+
38
+ const { config } = await resolveRNXAppConfig()
39
+ if (config?.runtimeVersion) {
40
+ const installed = fs.existsSync(
41
+ path.join(runtimeDir(config.runtimeVersion), 'index.html'),
42
+ )
43
+ console.log(
44
+ `project runtime v${config.runtimeVersion} · configured · ${
45
+ installed ? 'installed' : 'installs on open'
46
+ }`,
47
+ )
48
+ }
49
+
50
+ // best-effort network check; never fail the command on a flaky network.
51
+ const [check, cliUpdate] = await Promise.all([
52
+ rnxRuntime.checkUpToDate({ channel }),
53
+ cliUpdatePromise,
54
+ ])
55
+ if (cliUpdate?.outdated) {
56
+ console.log(`\n ↑ CLI v${cliUpdate.latestVersion} available; run \`rnx upgrade\``)
57
+ }
58
+ if (check.outdated && check.latest) {
59
+ console.log(`\n ↑ default runtime v${check.latest} available; run \`rnx upgrade\``)
60
+ }
61
+
62
+ console.log(`\n what's new: ${RNX_CHANGELOG_URL}`)
63
+ }
@@ -0,0 +1,17 @@
1
+ import { isOnboardingComplete } from '../../src/home-paths'
2
+ import { canPromptForPrivacyChoice } from '../privacy'
3
+ import { printBrand } from '../ui'
4
+ import { runOpenCommand } from './control'
5
+ import { runSetup } from './setup'
6
+
7
+ export async function runWelcome(): Promise<number> {
8
+ if (!isOnboardingComplete() && canPromptForPrivacyChoice()) {
9
+ await runSetup([], { launchConnectRN: true })
10
+ return 0
11
+ }
12
+
13
+ printBrand('Run any React Native app in a local simulator.')
14
+ console.log(`\n Opening ConnectRN`)
15
+ await runOpenCommand(['ConnectRN', '--no-describe', '--quiet'])
16
+ return 0
17
+ }
@@ -0,0 +1,328 @@
1
+ // rnx what-happened — show recent events from the semantic timeline
2
+ //
3
+ // the agent-facing read verb for the SootSim.bridges.timeline API. 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
+ // rnx what-happened # since last call
10
+ // rnx what-happened --summary # one-line counts
11
+ // rnx what-happened --all # full ring (no cursor)
12
+ // rnx what-happened --since 5s # absolute window
13
+ // rnx what-happened --kinds toast,fetch # filter
14
+ // rnx what-happened --json # structured
15
+ // rnx what-happened --limit 50 # cap (default 200)
16
+
17
+ import { getCliIdentityKey } from '../current-sim'
18
+ import { rnxExit } from '../run-rnx'
19
+ import { createBridgeFromParsed, parseBridgeCliArgs } from '../ws-bridge'
20
+ import {
21
+ formatTimelineEvent,
22
+ formatTimelineSummary,
23
+ inspectTimelineAdvanceCursor,
24
+ inspectTimelineRecent,
25
+ inspectTimelineSummary,
26
+ NOISY_TIMELINE_KINDS,
27
+ } from './inspect/core'
28
+ import type { SootSimTimelineEvent, SootSimTimelineQuery } from '@rnx/globals'
29
+
30
+ interface WhatHappenedOptions {
31
+ port?: number
32
+ verbose?: boolean
33
+ }
34
+
35
+ function parseSinceFlag(args: string[]): { since?: number; consumed: number[] } {
36
+ const consumed: number[] = []
37
+ for (let i = 0; i < args.length; i++) {
38
+ if (args[i] === '--since' && i + 1 < args.length) {
39
+ consumed.push(i, i + 1)
40
+ const v = args[i + 1].trim()
41
+ // accept '5s', '500ms', or absolute ms epoch
42
+ const m = /^(\d+(?:\.\d+)?)(ms|s|m)?$/.exec(v)
43
+ if (m) {
44
+ const n = Number(m[1])
45
+ const unit = m[2] ?? 'ms'
46
+ const ms = unit === 's' ? n * 1000 : unit === 'm' ? n * 60_000 : n
47
+ return { since: Date.now() - ms, consumed }
48
+ }
49
+ const asNum = Number(v)
50
+ if (Number.isFinite(asNum) && asNum > 1_000_000_000_000) {
51
+ return { since: asNum, consumed }
52
+ }
53
+ }
54
+ }
55
+ return { consumed }
56
+ }
57
+
58
+ function parseKindsFlag(args: string[]): { kinds?: string[]; consumed: number[] } {
59
+ const consumed: number[] = []
60
+ for (let i = 0; i < args.length; i++) {
61
+ if (args[i] === '--kinds' && i + 1 < args.length) {
62
+ consumed.push(i, i + 1)
63
+ return {
64
+ kinds: args[i + 1]
65
+ .split(',')
66
+ .map((s) => s.trim())
67
+ .filter(Boolean),
68
+ consumed,
69
+ }
70
+ }
71
+ }
72
+ return { consumed }
73
+ }
74
+
75
+ function parseLimitFlag(args: string[]): { limit?: number; consumed: number[] } {
76
+ const consumed: number[] = []
77
+ for (let i = 0; i < args.length; i++) {
78
+ if (args[i] === '--limit' && i + 1 < args.length) {
79
+ consumed.push(i, i + 1)
80
+ const n = Number(args[i + 1])
81
+ if (Number.isFinite(n) && n > 0) return { limit: n, consumed }
82
+ }
83
+ }
84
+ return { consumed }
85
+ }
86
+
87
+ interface FlowSection {
88
+ label: string
89
+ events: SootSimTimelineEvent[]
90
+ startedAt: number | null
91
+ }
92
+
93
+ // section the events by screen/route boundaries for `what-happened
94
+ // --flow`. each screen-name (or route-path) becomes a section
95
+ // header; every event that lands between two screen changes belongs
96
+ // to the first one's section. events before the first screen change
97
+ // land in an "initial state" section so nothing gets dropped.
98
+ //
99
+ // design note: this groups by *appearance* events only (phase ===
100
+ // 'enter' or no phase). disappearance events (`exit`) end the
101
+ // current section but don't start a new one — they're listed inside
102
+ // the section that contained them.
103
+ function groupByScreen(events: SootSimTimelineEvent[]): FlowSection[] {
104
+ const sections: FlowSection[] = []
105
+ let current: FlowSection = {
106
+ label: 'initial state',
107
+ events: [],
108
+ startedAt: events[0]?.t ?? null,
109
+ }
110
+ sections.push(current)
111
+ for (const ev of events) {
112
+ current.events.push(ev)
113
+ if (ev.kind === 'screen' || ev.kind === 'route') {
114
+ const data = ev.data as Record<string, unknown> | null
115
+ const phase = data?.phase as string | undefined
116
+ // only treat 'enter' (and the legacy no-phase form) as a
117
+ // section boundary. an 'exit' event is the tail of a screen,
118
+ // not the start of a new one.
119
+ if (!phase || phase === 'enter' || phase === 'appear' || phase === 'active') {
120
+ const name =
121
+ (data?.name as string) ||
122
+ (data?.activeName as string) ||
123
+ (data?.path as string) ||
124
+ (data?.pathname as string) ||
125
+ ev.kind
126
+ if (sections.length === 1 && current.events.length === 1) {
127
+ // first screen event — rename "initial state" inline so it
128
+ // doesn't show an empty initial-state section before it.
129
+ current.label = `${ev.kind}: ${name}`
130
+ } else {
131
+ current = {
132
+ label: `${ev.kind}: ${name}`,
133
+ events: [],
134
+ startedAt: ev.t,
135
+ }
136
+ sections.push(current)
137
+ }
138
+ }
139
+ }
140
+ }
141
+ return sections
142
+ }
143
+
144
+ export async function runWhatHappened(args: string[], opts: WhatHappenedOptions) {
145
+ const parsed = parseBridgeCliArgs(args, {
146
+ port: opts.port,
147
+ stripBooleanFlags: [
148
+ '--summary',
149
+ '--all',
150
+ '--json',
151
+ '--no-advance',
152
+ '--help',
153
+ '-h',
154
+ // `--flow` groups events under screen/route boundaries instead of
155
+ // printing a strict chronological list. demo-friendly for the
156
+ // "what just happened in this sim" recap.
157
+ '--flow',
158
+ // by default we hide high-frequency render/layout/scroll noise so
159
+ // a single tap doesn't get buried under 1000+ rows. enable
160
+ // `react-commit` with `rnx timeline start react-commit`, then pass
161
+ // `--noisy` (or an explicit `--kinds react-commit,…` filter) when you
162
+ // actually want to see them.
163
+ '--noisy',
164
+ ],
165
+ stripValueFlags: ['--since', '--kinds', '--limit'],
166
+ })
167
+
168
+ if (args.includes('--help') || args.includes('-h')) {
169
+ console.log(`
170
+ rnx what-happened — show recent events from the semantic timeline
171
+
172
+ usage:
173
+ rnx what-happened # since last CLI call
174
+ rnx what-happened --summary # one-line counts
175
+ rnx what-happened --all # full ring (ignore cursor)
176
+ rnx what-happened --since 5s # absolute window
177
+ rnx what-happened --kinds toast,fetch
178
+ rnx what-happened --noisy # include react-commit/layout/scroll
179
+ rnx what-happened --limit 50
180
+ rnx what-happened --json
181
+ rnx what-happened --no-advance # don't advance cursor after read
182
+
183
+ note: react-commit, layout, and scroll are opt-in/noisy events. enable them
184
+ with "rnx timeline start <kind>", then pass --noisy or include them in
185
+ --kinds to see them.
186
+ `)
187
+ rnxExit(0)
188
+ }
189
+
190
+ const summaryMode = args.includes('--summary')
191
+ const flowMode = args.includes('--flow')
192
+ const allMode = args.includes('--all')
193
+ const jsonMode = args.includes('--json')
194
+ const noAdvance = args.includes('--no-advance')
195
+ const noisyMode = args.includes('--noisy')
196
+
197
+ const { since } = parseSinceFlag(args)
198
+ const { kinds } = parseKindsFlag(args)
199
+ const { limit } = parseLimitFlag(args)
200
+
201
+ const cursorKey = getCliIdentityKey()
202
+ const query: SootSimTimelineQuery = {
203
+ limit: limit ?? 200,
204
+ ...(kinds && kinds.length ? { kinds: kinds as SootSimTimelineQuery['kinds'] } : {}),
205
+ ...(since !== undefined ? { since } : allMode ? {} : { sinceCursor: cursorKey }),
206
+ }
207
+
208
+ const bridge = createBridgeFromParsed(parsed)
209
+ try {
210
+ if (summaryMode) {
211
+ const summary = await inspectTimelineSummary(bridge, query)
212
+ if (jsonMode) {
213
+ console.log(JSON.stringify(summary))
214
+ } else {
215
+ const since_label = allMode
216
+ ? 'all time'
217
+ : since !== undefined
218
+ ? `last ${((Date.now() - since) / 1000).toFixed(1)}s`
219
+ : 'since last call'
220
+ console.log(` ${since_label}: ${formatTimelineSummary(summary)}`)
221
+ }
222
+ // advance cursor past the summary window's end
223
+ if (!noAdvance && !allMode && summary.lastAt) {
224
+ await inspectTimelineAdvanceCursor(bridge, cursorKey, summary.lastAt)
225
+ }
226
+ return
227
+ }
228
+
229
+ const result = await inspectTimelineRecent(bridge, query)
230
+
231
+ // hide high-frequency render/layout/scroll noise unless the caller
232
+ // explicitly asked for it (--noisy or --kinds containing one of
233
+ // these). this is the single most common reason `what-happened`
234
+ // output drowns out the meaningful events for the user/agent.
235
+ const userPickedNoisyKind =
236
+ Array.isArray(kinds) && kinds.some((k) => NOISY_TIMELINE_KINDS.has(k))
237
+ const suppressNoisy = !noisyMode && !userPickedNoisyKind
238
+ let suppressedNoisyCount = 0
239
+ if (suppressNoisy) {
240
+ const kept = result.events.filter((ev) => {
241
+ if (NOISY_TIMELINE_KINDS.has(ev.kind)) {
242
+ suppressedNoisyCount += 1
243
+ return false
244
+ }
245
+ return true
246
+ })
247
+ result.events = kept
248
+ }
249
+
250
+ if (jsonMode) {
251
+ if (flowMode) {
252
+ // grouped JSON shape — sections by screen/route. agents who want
253
+ // a chronological stream can still get it from the default
254
+ // shape (without --flow).
255
+ console.log(JSON.stringify(groupByScreen(result.events), null, 2))
256
+ } else {
257
+ console.log(JSON.stringify(result, null, 2))
258
+ }
259
+ } else {
260
+ if (result.events.length === 0) {
261
+ // distinguish "truly no events" from "events present but all
262
+ // filtered as noise". the second case is the much more common
263
+ // misdiagnosis — agents think a tap did nothing when actually
264
+ // it just produced react-commits and no shell-state change.
265
+ if (suppressedNoisyCount > 0) {
266
+ console.log(
267
+ ` no non-noise events (${suppressedNoisyCount} react-commit/layout/scroll hidden)`,
268
+ )
269
+ } else if (allMode) {
270
+ console.log(' no events recorded')
271
+ } else if (since !== undefined) {
272
+ console.log(' no events in window')
273
+ } else {
274
+ console.log(' no new events since last call')
275
+ }
276
+ } else if (flowMode) {
277
+ // section-per-screen render. each section heads with the
278
+ // screen/route name and lists every event that landed in
279
+ // that section in chronological order. unattributed events
280
+ // (events that landed before the first screen change) go in
281
+ // an "initial state" section.
282
+ const anchor = result.events[0]?.t ?? null
283
+ const sections = groupByScreen(result.events)
284
+ const header = allMode
285
+ ? `─── ${result.events.length} event(s) total — flow view ───`
286
+ : since !== undefined
287
+ ? `─── ${result.events.length} event(s) in last ${((Date.now() - since) / 1000).toFixed(1)}s — flow view ───`
288
+ : `─── ${result.events.length} event(s) since last call — flow view ───`
289
+ console.log(` ${header}`)
290
+ for (const section of sections) {
291
+ console.log(
292
+ `\n ── ${section.label} (${section.events.length} event${section.events.length === 1 ? '' : 's'}) ──`,
293
+ )
294
+ for (const ev of section.events) {
295
+ console.log(formatTimelineEvent(ev, anchor))
296
+ }
297
+ }
298
+ } else {
299
+ const anchor = result.events[0]?.t ?? null
300
+ const header = allMode
301
+ ? `─── ${result.events.length} event(s) total ───`
302
+ : since !== undefined
303
+ ? `─── ${result.events.length} event(s) in last ${((Date.now() - since) / 1000).toFixed(1)}s ───`
304
+ : `─── ${result.events.length} event(s) since last call ───`
305
+ console.log(` ${header}`)
306
+ for (const ev of result.events) {
307
+ console.log(formatTimelineEvent(ev, anchor))
308
+ }
309
+ }
310
+ // when at least one real event printed, append the suppression
311
+ // note so the user knows the picture isn't complete. when the
312
+ // window was entirely noise, the empty-state branch above already
313
+ // mentioned it — don't double up.
314
+ if (suppressedNoisyCount > 0 && result.events.length > 0) {
315
+ process.stderr.write(
316
+ `\n ${suppressedNoisyCount} high-frequency event(s) hidden (react-commit/layout/scroll)\n` +
317
+ ` rerun with --noisy or --kinds react-commit,layout,scroll to include them\n`,
318
+ )
319
+ }
320
+ }
321
+
322
+ if (!noAdvance && !allMode && result.watermark > 0) {
323
+ await inspectTimelineAdvanceCursor(bridge, cursorKey, result.watermark)
324
+ }
325
+ } finally {
326
+ bridge.close()
327
+ }
328
+ }
@@ -0,0 +1,217 @@
1
+ import { existsSync, readFileSync, rmSync } from 'fs'
2
+ import { tmpdir } from 'os'
3
+ import { basename, dirname, join, resolve } from 'path'
4
+ import { getParentPid, getProcessComm } from './parent-pid'
5
+ import { writeSecureJsonAtomic } from './secure-file'
6
+
7
+ const CURRENT_SIM_VERSION = 2 as const
8
+ const CURRENT_SIM_PATH_ENV = 'SOOTSIM_CLI_CURRENT_SIM_PATH'
9
+
10
+ export interface LocalCurrentTarget {
11
+ version: 2
12
+ plane: 'local'
13
+ simId: string
14
+ updatedAt: string
15
+ }
16
+
17
+ // env vars that tend to identify a specific terminal, pane, or agent run.
18
+ // checked in order — the first one present wins. anything further down is a
19
+ // progressively weaker signal of "same caller across invocations".
20
+ const IDENTITY_ENV_VARS = [
21
+ 'RNX_CLI_IDENTITY', // explicit override — fastest path, skips all pid lookups
22
+ 'TM_SESSION', // Team Machine session id; routes prompt-bar followups back to this agent
23
+ 'CLAUDE_CODE_SESSION_ID', // claude code, if ever set
24
+ 'CODEX_THREAD_ID', // codex / openai agent thread, stable across tool invocations
25
+ 'TERM_SESSION_ID', // macOS Terminal.app + iTerm2
26
+ 'ITERM_SESSION_ID', // iTerm2 (older variant)
27
+ 'TMUX_PANE', // tmux — stable per pane
28
+ 'STY', // GNU screen session
29
+ 'KITTY_WINDOW_ID', // kitty
30
+ 'WEZTERM_PANE', // wezterm
31
+ 'ALACRITTY_WINDOW_ID', // alacritty
32
+ 'WINDOWID', // X11 terminals
33
+ 'VSCODE_INJECTION', // vscode integrated terminal
34
+ ] as const
35
+
36
+ export interface CliIdentity {
37
+ key: string
38
+ source: (typeof IDENTITY_ENV_VARS)[number] | 'grand-ppid' | 'ppid' | 'explicit-sim-id'
39
+ stable: boolean
40
+ }
41
+
42
+ // derive a stable per-terminal identity key so different agents/terminals
43
+ // don't share state. priority:
44
+ // 1. explicit env vars (iTerm, tmux, etc.)
45
+ // 2. grand-ppid — the parent of our invoking shell, which in Claude Code's
46
+ // Bash tool is the per-agent claude process (stable across invocations,
47
+ // distinct between agents). generic — not claude-specific.
48
+ // 3. ppid — the invoking shell's pid. stable when the shell is reused
49
+ // across invocations (claude's bash tool, persistent tty sessions).
50
+ // unstable when each call spawns a fresh shell — in that case set an
51
+ // env var from IDENTITY_ENV_VARS to pin the identity.
52
+ // an earlier env-hash fallback was removed: sha'ing process.env looks stable
53
+ // but in practice any per-turn env drift (timestamps, cwd, agent vars)
54
+ // flipped the hash and made the same agent look like a new client.
55
+ // walk up to the topmost non-init ancestor. needed because intermediate
56
+ // pids drift across invocations:
57
+ // - claude code's bash tool spawns a fresh shell per call, so the
58
+ // immediate gppid (the shell's parent) can change
59
+ // - shell pipelines (`rnx list | grep`) wrap the command in a
60
+ // subshell, adding an extra ppid layer that shifts gppid down by one
61
+ // the topmost user process (just below init/launchd) is stable across
62
+ // all of these — same agent / login shell / terminal across calls.
63
+ // terminal-multiplexer servers are effectively immortal (the tmux server
64
+ // lives while ANY session exists), so an owner-pid walk that climbs into one
65
+ // produces an owner that never dies — detached browser hosts then leak until
66
+ // an explicit close (real incident: a tmux-launched sim's chrome survived its
67
+ // pane at 129% cpu). the Team Machine daemon is the same class: it spawns worker
68
+ // harnesses directly, outlives them all, and additionally collapses every
69
+ // worker into one shared identity key (real incident 2026-07-14: three film
70
+ // render hosts leaked with owner = the daemon while load hit 224). stop
71
+ // BELOW these hosts: the child (pane shell / worker harness) dies with the
72
+ // session, which is the lifetime a sim owner should have.
73
+ // these are matched against libproc's pbi_comm, the executable's basename,
74
+ // truncated to 16 chars. Team Machine reports comm `tm`; matching its product
75
+ // name never fired and every agent-launched sim resolved its owner to the terminal
76
+ // tab's /usr/bin/login instead (real incident 2026-08-06: six chrome hosts
77
+ // survived their agents, 17GB rss, load 19 with the GPU saturated).
78
+ const IMMORTAL_HOST_COMM_RE = /^(?:tmux|screen)|^tm$/i
79
+
80
+ function findTopAncestor(start: number, maxDepth = 20): number | null {
81
+ // check each CANDIDATE's own comm and return the child below a multiplexer
82
+ // (prev). checking only the parent's comm looked equivalent but was not: on
83
+ // macOS the tmux server is launchd's direct child, so it arrives as the
84
+ // candidate via the initializer and the parent<=1 branch returned it before
85
+ // any comm check ran (runtime-caught on a real tmux box: owner resolved to
86
+ // the immortal server). prev starts at `start` so a pane shell directly
87
+ // under the server resolves to itself.
88
+ let prev = start
89
+ let pid = getParentPid(start)
90
+ if (!pid || pid <= 1) {
91
+ // a readable ordinary process directly below init is itself the stable
92
+ // session root. this is the shape produced by detached job launchers, and
93
+ // returning null here used to disable the browser owner's lifetime watch.
94
+ // an unreadable or known-immortal process has no defensible child bound at
95
+ // this point, so leave it unresolved and make the launcher fail loudly.
96
+ const comm = getProcessComm(start)
97
+ return start > 1 && comm && !IMMORTAL_HOST_COMM_RE.test(comm) ? start : null
98
+ }
99
+ for (let i = 0; i < maxDepth; i++) {
100
+ const comm = getProcessComm(pid)
101
+ if (comm && IMMORTAL_HOST_COMM_RE.test(comm)) return prev
102
+ const parent = getParentPid(pid)
103
+ if (!parent || parent <= 1) {
104
+ // top of the tree we can read. an unreadable comm means a root-owned
105
+ // session host — /usr/bin/login under a terminal tab — which outlives
106
+ // every agent and shell inside it, so stop below it. a readable top is
107
+ // an ordinary user process (a CI job root) and is a valid owner.
108
+ return comm ? pid : prev
109
+ }
110
+ prev = pid
111
+ pid = parent
112
+ }
113
+ return pid
114
+ }
115
+
116
+ export function getCliIdentity(): CliIdentity {
117
+ for (const name of IDENTITY_ENV_VARS) {
118
+ const value = process.env[name]
119
+ if (value && value.trim()) {
120
+ return { key: `${name}:${value.trim()}`, source: name, stable: true }
121
+ }
122
+ }
123
+ const topPid = findTopAncestor(process.ppid)
124
+ if (topPid && topPid > 1) {
125
+ return { key: `gppid-${topPid}`, source: 'grand-ppid', stable: true }
126
+ }
127
+ return { key: `pid-${process.ppid}`, source: 'ppid', stable: false }
128
+ }
129
+
130
+ export function getCliIdentityKey(): string {
131
+ return getCliIdentity().key
132
+ }
133
+
134
+ // the stable owning-session pid: the topmost non-init ancestor of this CLI
135
+ // process — in Claude Code's Bash tool the per-agent process, otherwise the
136
+ // login shell / terminal / CI process-tree root. it lives exactly as long as
137
+ // the session that wants a sim, and dies when that session does. used to tie
138
+ // a detached browser host's lifetime to its owner so the host self-terminates
139
+ // (closing Chrome) when the owner exits, instead of leaking a headless
140
+ // browser forever. returns null on platforms where ancestry can't be read.
141
+ export function getStableOwnerPid(): number | null {
142
+ return findTopAncestor(process.ppid)
143
+ }
144
+
145
+ export function getCurrentSimPath() {
146
+ if (process.env[CURRENT_SIM_PATH_ENV]) {
147
+ return resolve(process.env[CURRENT_SIM_PATH_ENV])
148
+ }
149
+ const key = getCliIdentityKey()
150
+ return join(tmpdir(), `rnx-current-sim-${key}.json`)
151
+ }
152
+
153
+ function legacyCloudCredentialDirectory(filepath: string): string {
154
+ return join(dirname(filepath), `${basename(filepath)}.credentials`)
155
+ }
156
+
157
+ export function readCurrentTarget(): LocalCurrentTarget | null {
158
+ const filepath = getCurrentSimPath()
159
+ rmSync(legacyCloudCredentialDirectory(filepath), { recursive: true, force: true })
160
+ if (!existsSync(filepath)) return null
161
+
162
+ try {
163
+ const parsed: unknown = JSON.parse(readFileSync(filepath, 'utf8'))
164
+ if (parsed === null || typeof parsed !== 'object') {
165
+ rmSync(filepath, { force: true })
166
+ return null
167
+ }
168
+ const version = Reflect.get(parsed, 'version')
169
+ const plane = Reflect.get(parsed, 'plane')
170
+ const simId = Reflect.get(parsed, 'simId')
171
+ const updatedAt = Reflect.get(parsed, 'updatedAt')
172
+ if (
173
+ (version !== 1 && version !== CURRENT_SIM_VERSION) ||
174
+ (version === CURRENT_SIM_VERSION && plane !== 'local') ||
175
+ typeof simId !== 'string' ||
176
+ !simId.trim() ||
177
+ typeof updatedAt !== 'string'
178
+ ) {
179
+ rmSync(filepath, { force: true })
180
+ return null
181
+ }
182
+
183
+ const target = {
184
+ version: CURRENT_SIM_VERSION,
185
+ plane: 'local',
186
+ simId: simId.trim(),
187
+ updatedAt,
188
+ } satisfies LocalCurrentTarget
189
+ if (version === 1) writeSecureJsonAtomic(filepath, target)
190
+ return target
191
+ } catch {
192
+ rmSync(filepath, { force: true })
193
+ return null
194
+ }
195
+ }
196
+
197
+ export const readCurrentSim = readCurrentTarget
198
+
199
+ export function readCurrentSimId() {
200
+ return readCurrentTarget()?.simId ?? null
201
+ }
202
+
203
+ export function saveCurrentSimId(simId: string) {
204
+ const normalized = simId.trim()
205
+ if (!normalized) return
206
+
207
+ writeSecureJsonAtomic(getCurrentSimPath(), {
208
+ version: CURRENT_SIM_VERSION,
209
+ plane: 'local',
210
+ simId: normalized,
211
+ updatedAt: new Date().toISOString(),
212
+ } satisfies LocalCurrentTarget)
213
+ }
214
+
215
+ export function clearCurrentSimId() {
216
+ rmSync(getCurrentSimPath(), { force: true })
217
+ }