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,215 @@
1
+ // `rnx get memory` / `rnx debug memory` — per-worker live object
2
+ // counts (nodes registered, pictures, raster images, paragraphs, yoga wasm
3
+ // nodes), image-loader cache, and JS heaps. one-shot by default; --watch
4
+ // samples repeatedly and reports per-counter slope, which is the leak-RCA
5
+ // workflow: run the repro, watch which counter's slope tracks RSS.
6
+
7
+ import { inspectMemory, type MemoryReport, type WorkerMemorySample } from './core'
8
+ import { printJson, wantsJson } from './shared'
9
+ import type { WsBridge } from '../../ws-bridge'
10
+
11
+ function formatBytes(n: number): string {
12
+ if (n < 1024) return `${n}B`
13
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`
14
+ return `${(n / 1024 / 1024).toFixed(1)}MB`
15
+ }
16
+
17
+ function pct(num: number, denom: number): string {
18
+ if (denom <= 0) return '?'
19
+ return `${((num / denom) * 100).toFixed(0)}%`
20
+ }
21
+
22
+ // flatten one report into named numeric counters so watch mode can diff and
23
+ // slope every field uniformly.
24
+ function flattenCounters(report: MemoryReport): Record<string, number> {
25
+ const out: Record<string, number> = {}
26
+ const worker = (prefix: string, w: WorkerMemorySample | null) => {
27
+ if (!w) return
28
+ if (w.nodesRegistered != null) out[`${prefix}.nodes`] = w.nodesRegistered
29
+ if (w.nodesDetached != null) out[`${prefix}.detached`] = w.nodesDetached
30
+ if (w.objects) {
31
+ out[`${prefix}.paragraphs`] = w.objects.paragraphs
32
+ out[`${prefix}.yogaNodes`] = w.objects.yogaNodes
33
+ out[`${prefix}.pictures`] = w.objects.pictures
34
+ out[`${prefix}.rasterImages`] = w.objects.rasterImages
35
+ }
36
+ if (w.imageLoader) {
37
+ out[`${prefix}.imageCacheEntries`] = w.imageLoader.cacheEntries
38
+ out[`${prefix}.imagePixelBytes`] = w.imageLoader.cachePixelBytes
39
+ }
40
+ if (w.workerHeap) out[`${prefix}.heapBytes`] = w.workerHeap.usedJSHeapSize
41
+ if (w.wasmHeapBytes != null) out[`${prefix}.wasmHeapBytes`] = w.wasmHeapBytes
42
+ }
43
+ worker('tenant', report.tenant)
44
+ worker('shell', report.shell)
45
+ worker('compositor', report.compositor)
46
+ if (report.hostHeap) out['host.heapBytes'] = report.hostHeap.usedJSHeapSize
47
+ return out
48
+ }
49
+
50
+ function formatCounter(key: string, value: number): string {
51
+ return key.endsWith('Bytes') ? formatBytes(value) : String(value)
52
+ }
53
+
54
+ function printWorkerBlock(name: string, w: WorkerMemorySample | null): void {
55
+ if (!w) {
56
+ console.log(` ${name}: not available`)
57
+ return
58
+ }
59
+ const objects = w.objects
60
+ console.log(` ${name}`)
61
+ if (w.nodesRegistered != null) {
62
+ console.log(` nodes registered: ${w.nodesRegistered}`)
63
+ }
64
+ if (w.nodesDetached != null) {
65
+ const types = Object.entries(w.detachedTypes ?? {})
66
+ .map(([type, count]) => `${type}:${count}`)
67
+ .join(' ')
68
+ console.log(
69
+ ` nodes detached: ${w.nodesDetached}${types ? ` (${types})` : ''}`,
70
+ )
71
+ }
72
+ if (objects) {
73
+ console.log(` paragraphs: ${objects.paragraphs}`)
74
+ console.log(` yoga nodes: ${objects.yogaNodes}`)
75
+ console.log(` pictures: ${objects.pictures}`)
76
+ console.log(` raster images: ${objects.rasterImages}`)
77
+ }
78
+ if (w.activeNativeAnimations != null) {
79
+ console.log(` native anims: ${w.activeNativeAnimations}`)
80
+ }
81
+ if (w.imageLoader) {
82
+ const im = w.imageLoader
83
+ console.log(
84
+ ` image cache: ${im.cacheEntries}/${im.cacheMaxEntries} entries, ${formatBytes(im.cachePixelBytes)}/${formatBytes(im.cachePixelBudget)} (${pct(im.cachePixelBytes, im.cachePixelBudget)})`,
85
+ )
86
+ }
87
+ if (w.workerHeap) {
88
+ console.log(
89
+ ` js heap: ${formatBytes(w.workerHeap.usedJSHeapSize)} used / ${formatBytes(w.workerHeap.totalJSHeapSize)} total`,
90
+ )
91
+ }
92
+ if (w.wasmHeapBytes != null) {
93
+ console.log(` canvaskit wasm: ${formatBytes(w.wasmHeapBytes)}`)
94
+ }
95
+ }
96
+
97
+ function printReport(report: MemoryReport): void {
98
+ console.log(' memory:')
99
+ printWorkerBlock('tenant worker', report.tenant)
100
+ printWorkerBlock('shell worker', report.shell)
101
+ printWorkerBlock('compositor worker', report.compositor)
102
+ if (report.hostHeap) {
103
+ console.log(' host')
104
+ console.log(
105
+ ` js heap: ${formatBytes(report.hostHeap.usedJSHeapSize)} used / ${formatBytes(report.hostHeap.totalJSHeapSize)} total`,
106
+ )
107
+ } else {
108
+ console.log(' host js heap: not available (chrome only)')
109
+ }
110
+ }
111
+
112
+ function readNumberFlag(args: string[], flag: string, fallback: number): number {
113
+ const idx = args.indexOf(flag)
114
+ if (idx === -1) return fallback
115
+ const value = Number(args[idx + 1])
116
+ return Number.isFinite(value) && value > 0 ? value : fallback
117
+ }
118
+
119
+ export async function runMemorySubcommand(
120
+ bridge: WsBridge,
121
+ opts: { args: string[] } = { args: [] },
122
+ ): Promise<void> {
123
+ const json = wantsJson(opts.args)
124
+ const watchIdx = opts.args.indexOf('--watch')
125
+
126
+ if (watchIdx === -1) {
127
+ const report = await inspectMemory(bridge)
128
+ if (json) {
129
+ printJson(report)
130
+ return
131
+ }
132
+ printReport(report)
133
+ return
134
+ }
135
+
136
+ // --watch [seconds] — sample every --interval seconds (default 5) for the
137
+ // given duration (default 60s), then report per-counter slope.
138
+ const durationValue = Number(opts.args[watchIdx + 1])
139
+ const durationSec =
140
+ Number.isFinite(durationValue) && durationValue > 0 ? durationValue : 60
141
+ const intervalSec = readNumberFlag(opts.args, '--interval', 5)
142
+
143
+ const samples: Array<{ tSec: number; counters: Record<string, number> }> = []
144
+ const start = Date.now()
145
+ let prev: Record<string, number> | null = null
146
+
147
+ if (!json) {
148
+ console.log(` sampling every ${intervalSec}s for ${durationSec}s — deltas per tick:`)
149
+ }
150
+
151
+ while (true) {
152
+ const report = await inspectMemory(bridge)
153
+ const tSec = (Date.now() - start) / 1000
154
+ const counters = flattenCounters(report)
155
+ samples.push({ tSec, counters })
156
+
157
+ if (!json) {
158
+ const parts: string[] = []
159
+ for (const [key, value] of Object.entries(counters)) {
160
+ const delta = prev && key in prev ? value - prev[key] : null
161
+ if (delta === null || delta === 0) continue
162
+ const sign = delta > 0 ? '+' : '-'
163
+ const deltaStr = key.endsWith('Bytes')
164
+ ? `${sign}${formatBytes(Math.abs(delta))}`
165
+ : `${sign}${Math.abs(delta)}`
166
+ parts.push(`${key} ${formatCounter(key, value)} (${deltaStr})`)
167
+ }
168
+ console.log(
169
+ ` t+${tSec.toFixed(0)}s ${
170
+ parts.length
171
+ ? parts.join(' ')
172
+ : prev
173
+ ? 'no change'
174
+ : Object.entries(counters)
175
+ .map(([k, v]) => `${k} ${formatCounter(k, v)}`)
176
+ .join(' ')
177
+ }`,
178
+ )
179
+ }
180
+ prev = counters
181
+
182
+ if (tSec >= durationSec) break
183
+ await new Promise((resolve) => setTimeout(resolve, intervalSec * 1000))
184
+ }
185
+
186
+ // per-counter slope over the whole window
187
+ const first = samples[0]
188
+ const last = samples[samples.length - 1]
189
+ const elapsed = Math.max(last.tSec - first.tSec, 0.001)
190
+ const perSecond: Record<string, number> = {}
191
+ for (const key of Object.keys(last.counters)) {
192
+ if (!(key in first.counters)) continue
193
+ perSecond[key] = (last.counters[key] - first.counters[key]) / elapsed
194
+ }
195
+
196
+ if (json) {
197
+ printJson({ samples, perSecond, elapsedSec: elapsed })
198
+ return
199
+ }
200
+
201
+ console.log(`\n slope over ${elapsed.toFixed(0)}s:`)
202
+ const moving = Object.entries(perSecond)
203
+ .filter(([, v]) => v !== 0)
204
+ .sort((a, b) => Math.abs(b[1]) - Math.abs(a[1]))
205
+ if (moving.length === 0) {
206
+ console.log(' all counters flat')
207
+ return
208
+ }
209
+ for (const [key, rate] of moving) {
210
+ const label = key.endsWith('Bytes')
211
+ ? `${formatBytes(Math.abs(rate))}/s${rate < 0 ? ' (shrinking)' : ''}`
212
+ : `${rate > 0 ? '+' : ''}${rate.toFixed(2)}/s`
213
+ console.log(` ${key.padEnd(28)}${label}`)
214
+ }
215
+ }
@@ -0,0 +1,39 @@
1
+ export const SECURE_TEXT_REDACTION = '<redacted secure text>'
2
+
3
+ type KeyboardSpecLike = {
4
+ secureTextEntry?: boolean | null
5
+ keyboardType?: string | null
6
+ } | null
7
+
8
+ type KeyboardStateLike = {
9
+ layout?: { spec?: KeyboardSpecLike } | null
10
+ spec?: KeyboardSpecLike
11
+ focusedInput?: Record<string, unknown> | null
12
+ } | null
13
+
14
+ export function isSecureKeyboardState(state: KeyboardStateLike): boolean {
15
+ const spec = state?.layout?.spec ?? state?.spec ?? null
16
+ return !!spec?.secureTextEntry && spec.keyboardType !== 'visible-password'
17
+ }
18
+
19
+ export function redactTextForKeyboardState(
20
+ text: string,
21
+ state: KeyboardStateLike,
22
+ ): string {
23
+ return isSecureKeyboardState(state) ? SECURE_TEXT_REDACTION : text
24
+ }
25
+
26
+ export function redactKeyboardStateForOutput<T extends KeyboardStateLike>(state: T): T {
27
+ if (!state || !isSecureKeyboardState(state)) return state
28
+ const focusedInput = state.focusedInput
29
+ if (!focusedInput || typeof focusedInput !== 'object') return state
30
+ return {
31
+ ...state,
32
+ focusedInput: {
33
+ ...focusedInput,
34
+ secureTextEntry: true,
35
+ text:
36
+ typeof focusedInput.text === 'string' ? SECURE_TEXT_REDACTION : focusedInput.text,
37
+ },
38
+ }
39
+ }
@@ -0,0 +1,77 @@
1
+ import { isSimSemanticTree, type InspectBridge } from './core'
2
+
3
+ // read a --testid / --text flag from args. returns null when neither is
4
+ // present so coord-based do verbs can fall through to their positional path.
5
+ //
6
+ // pass `positional` from a verb whose positionals are x/y coordinates (tap,
7
+ // double-tap, long-press) and a bare non-numeric first positional reads as a
8
+ // testID: `rnx do tap my-button` works like `do tap-id my-button` and like
9
+ // `do scroll <id>`, instead of failing with a usage line for the one verb an
10
+ // agent is most likely to reach for. a coordinate is always numeric, so there
11
+ // is nothing to disambiguate.
12
+ export function readTargetFlag(
13
+ args: string[],
14
+ positional?: string[],
15
+ ): { mode: 'testid' | 'text'; value: string } | null {
16
+ const tid = args.indexOf('--testid')
17
+ if (tid >= 0 && args[tid + 1]) return { mode: 'testid', value: args[tid + 1] }
18
+ const alt = args.indexOf('--test-id')
19
+ if (alt >= 0 && args[alt + 1]) return { mode: 'testid', value: args[alt + 1] }
20
+ const text = args.indexOf('--text')
21
+ if (text >= 0 && args[text + 1]) return { mode: 'text', value: args[text + 1] }
22
+ const bare = positional?.[1]
23
+ if (bare && !bare.startsWith('-') && !Number.isFinite(Number(bare))) {
24
+ return { mode: 'testid', value: bare }
25
+ }
26
+ return null
27
+ }
28
+
29
+ export interface ResolvedTargetCoords {
30
+ nodeId?: number
31
+ x: number
32
+ y: number
33
+ testID?: string | null
34
+ text?: string | null
35
+ type?: string | null
36
+ }
37
+
38
+ export async function resolveTargetCoords(
39
+ bridge: InspectBridge,
40
+ target: { mode: 'testid' | 'text'; value: string },
41
+ ): Promise<ResolvedTargetCoords | null> {
42
+ const result: unknown = await bridge.send({
43
+ type: 'resolve',
44
+ selector:
45
+ target.mode === 'testid' ? { testID: target.value } : { text: target.value },
46
+ })
47
+ if (result === null) return null
48
+ if (typeof result !== 'object') throw new Error('resolve returned an invalid result')
49
+ const match = Reflect.get(result, 'match')
50
+ const resolvedTarget = Reflect.get(result, 'target')
51
+ const semanticNodes = [match, resolvedTarget]
52
+ const point = Reflect.get(result, 'point')
53
+ const x =
54
+ point !== null && typeof point === 'object' ? Reflect.get(point, 'x') : undefined
55
+ const y =
56
+ point !== null && typeof point === 'object' ? Reflect.get(point, 'y') : undefined
57
+ if (
58
+ !isSimSemanticTree(semanticNodes) ||
59
+ typeof x !== 'number' ||
60
+ !Number.isFinite(x) ||
61
+ typeof y !== 'number' ||
62
+ !Number.isFinite(y)
63
+ ) {
64
+ throw new Error('resolve returned an invalid result')
65
+ }
66
+ const semanticMatch = semanticNodes[1]
67
+ if (!semanticMatch) throw new Error('resolve returned an invalid result')
68
+ return {
69
+ x,
70
+ y,
71
+ nodeId: semanticMatch.nodeId,
72
+ testID: semanticMatch.testID ?? null,
73
+ text:
74
+ semanticMatch.text ?? (target.mode === 'text' ? target.value : semanticMatch.label),
75
+ type: semanticMatch.type,
76
+ }
77
+ }
@@ -0,0 +1,78 @@
1
+ // `rnx get screens` — print the current navigation stack + shell state +
2
+ // any presented modal + keyboard. answers "what screen am I on?" without
3
+ // having to dump the whole tree or scrape screen-specific testIDs.
4
+ //
5
+ // sources:
6
+ // - navigation-runtime snapshot (test bridge) for screen stack + phase
7
+ // - mainShell.getState() for active app + switcher / launch progress
8
+ // - __sootsimKeyboard.getLayout() for keyboard visibility + spec
9
+
10
+ import { inspectScreens } from './core'
11
+ import type { WsBridge } from '../../ws-bridge'
12
+
13
+ export async function runScreensSubcommand(
14
+ bridge: WsBridge,
15
+ opts: { json?: boolean } = {},
16
+ ): Promise<void> {
17
+ const { nav, route, keyboard, shell } = await inspectScreens(bridge)
18
+
19
+ if (opts.json) {
20
+ console.log(JSON.stringify({ shell: shell ?? null, nav, route, keyboard }, null, 2))
21
+ return
22
+ }
23
+
24
+ const lines: string[] = []
25
+
26
+ // shell — what app, what switcher state
27
+ if (shell) {
28
+ const activeApp = shell.activeApp ?? shell.state ?? '<none>'
29
+ const switcher = shell.showSwitcher ? ' (app switcher open)' : ''
30
+ const launchProgress =
31
+ typeof shell.launchProgress === 'number' && shell.launchProgress < 0.98
32
+ ? ` launching (${Math.round(shell.launchProgress * 100)}%)`
33
+ : ''
34
+ lines.push(`shell: ${activeApp}${switcher}${launchProgress}`)
35
+ } else {
36
+ lines.push(`shell: <unavailable>`)
37
+ }
38
+
39
+ // navigation stack
40
+ if (nav) {
41
+ const phase =
42
+ nav.transitionPhase !== 'idle'
43
+ ? ` (${nav.transitionPhase}, ${nav.activeTransitionCount} active)`
44
+ : ''
45
+ lines.push(`nav: phase=${nav.transitionPhase}${phase}`)
46
+ if (nav.screens.length === 0) {
47
+ lines.push(` <no registered screens — app may not use react-native-screens>`)
48
+ } else {
49
+ for (const s of nav.screens) {
50
+ const marker = s.isActive ? '▶' : ' '
51
+ const route = s.routeName ? ` ${s.routeName}` : ''
52
+ const header = s.headerHeight > 0 ? ` header=${s.headerHeight}` : ''
53
+ const lt =
54
+ s.largeTitleState && s.largeTitleState !== 'expanded'
55
+ ? ` large-title=${s.largeTitleState}`
56
+ : ''
57
+ lines.push(` ${marker} #${s.id}${route}${header}${lt}`)
58
+ }
59
+ }
60
+ } else {
61
+ lines.push(`nav: <runtime not available>`)
62
+ }
63
+ lines.push(`route: ${route?.currentPath ?? '<unavailable>'}`)
64
+ if (route?.coveringPath) {
65
+ lines.push(`covered: a presented screen (${route.coveringPath}) is above this route`)
66
+ }
67
+
68
+ // keyboard
69
+ if (keyboard && keyboard.visible) {
70
+ const type = keyboard.spec?.keyboardType ?? 'default'
71
+ const ret = keyboard.spec?.returnKeyType ?? 'default'
72
+ lines.push(`keyboard: visible (${type}, return=${ret}, mode=${keyboard.mode ?? '?'})`)
73
+ } else {
74
+ lines.push(`keyboard: hidden`)
75
+ }
76
+
77
+ console.log(lines.join('\n'))
78
+ }
@@ -0,0 +1,32 @@
1
+ import { waitForSootsimIdle } from './settling'
2
+ import type { WsBridge } from '../../ws-bridge'
3
+
4
+ // default: layout hash stable for 3 consecutive polls. --strict additionally
5
+ // requires worker queryStats animation flags to be false — use when the
6
+ // target app has no perpetual background animation.
7
+ export async function runSettleSubcommand(opts: {
8
+ bridge: WsBridge
9
+ args: string[]
10
+ positional: string[]
11
+ }): Promise<void> {
12
+ const { bridge, args, positional } = opts
13
+ const maxMs = positional[1] ? Number(positional[1]) * 1000 : 3000
14
+ if (bridge.plane === 'cloud') {
15
+ const maxFrames = Math.max(1, Math.min(600, Math.ceil(maxMs / 16)))
16
+ const result: unknown = await bridge.send({ type: 'settle', maxFrames })
17
+ const frames =
18
+ result !== null && typeof result === 'object' ? Reflect.get(result, 'frames') : null
19
+ console.log(
20
+ typeof frames === 'number' ? ` settled in ${frames} frames` : ' settled',
21
+ )
22
+ return
23
+ }
24
+ const strict = args.includes('--strict')
25
+ const { elapsed, settled } = await waitForSootsimIdle({ bridge, maxMs, strict })
26
+
27
+ if (settled) {
28
+ console.log(` settled in ${elapsed}ms`)
29
+ } else {
30
+ console.log(` timed out after ${elapsed}ms (may still be animating)`)
31
+ }
32
+ }
@@ -0,0 +1,197 @@
1
+ import type { InspectBridge } from './core'
2
+
3
+ export type WaitForSootsimIdleOptions = {
4
+ bridge: InspectBridge
5
+ simId?: string
6
+ maxMs: number
7
+ pollMs?: number
8
+ stablePolls?: number
9
+ strict?: boolean
10
+ }
11
+
12
+ export type WaitForSootsimIdleResult = {
13
+ elapsed: number
14
+ settled: boolean
15
+ // what the last poll still saw moving. a settle that only reports "did not
16
+ // settle" cannot tell a slow screen from one that animates forever, and the
17
+ // two need opposite fixes.
18
+ blockedBy: string
19
+ }
20
+
21
+ export async function waitForSootsimIdle({
22
+ bridge,
23
+ simId,
24
+ maxMs,
25
+ pollMs = 50,
26
+ stablePolls = 3,
27
+ strict = false,
28
+ }: WaitForSootsimIdleOptions): Promise<WaitForSootsimIdleResult> {
29
+ const result = await bridge.send(
30
+ {
31
+ type: 'evaluate',
32
+ simId,
33
+ code: `(async () => {
34
+ const start = Date.now()
35
+ const deadline = start + ${Math.max(0, Math.round(maxMs))}
36
+ const pollMs = ${Math.max(1, Math.round(pollMs))}
37
+ const requiredStablePolls = ${Math.max(1, Math.round(stablePolls))}
38
+ const strict = ${strict ? 'true' : 'false'}
39
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
40
+
41
+ // route-aware settle: a tap that pushes/pops a screen is async — the
42
+ // old screen still renders for a moment, then the new one mounts and
43
+ // its content loads. a pure layout-hash check reports "idle" on the
44
+ // outgoing screen or on the incoming skeleton, so a driver re-taps
45
+ // and stacks duplicate navigations. drain any in-flight screen
46
+ // transition FIRST, bounded by the overall budget. when no
47
+ // transition is happening this returns in ~80ms (startWindowMs), so
48
+ // a plain button tap barely pays for it.
49
+ try {
50
+ const wfst = window.__sootsimTest?.waitForScreenTransitions
51
+ if (typeof wfst === 'function') {
52
+ const remaining = deadline - Date.now()
53
+ if (remaining > 120) {
54
+ await wfst({
55
+ timeoutMs: Math.min(remaining - 80, 4000),
56
+ settleMs: 64,
57
+ startWindowMs: 80,
58
+ })
59
+ }
60
+ }
61
+ } catch {}
62
+
63
+ const readSnapshot = async () => {
64
+ let animating = false
65
+ let layoutDirty = false
66
+ let pendingFetches = 0
67
+ let requestTotal = 0
68
+ let requestInFlight = 0
69
+ let renderStatsAvailable = false
70
+ try {
71
+ const stats = await window.__sootsimRenderHost?.queryStats?.()
72
+ if (stats) {
73
+ renderStatsAvailable = true
74
+ animating =
75
+ stats.hasActiveAnims === true ||
76
+ stats.hasActiveNativeAnimations === true ||
77
+ stats.hasPendingAnimationFrames === true
78
+ layoutDirty =
79
+ stats.layoutDirty === true && stats.renderRequested === true
80
+ // a freshly-pushed screen showing a skeleton is layout-stable
81
+ // but not actually settled — its data/images are still in
82
+ // flight. treat bounded image-loader fetches as not-idle so
83
+ // settle waits for real content, capped by the budget.
84
+ const pf = stats.memory && stats.memory.imageLoader
85
+ ? stats.memory.imageLoader.pendingFetches
86
+ : 0
87
+ pendingFetches = typeof pf === 'number' ? pf : 0
88
+ }
89
+ } catch {}
90
+ try {
91
+ const counts = await window.__sootsimTest?.getRequestCounts?.()
92
+ requestTotal = typeof counts?.total === 'number' ? counts.total : 0
93
+ requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
94
+ } catch {}
95
+ const root = window.__sootsimRoot
96
+ const nodes = []
97
+ if (root) {
98
+ const walk = (n) => {
99
+ if (n.layout && n.layout.width > 0) {
100
+ nodes.push(Math.round(n.layout.x) + ',' + Math.round(n.layout.y) + ',' + Math.round(n.layout.width))
101
+ }
102
+ for (const c of n.children || []) walk(c)
103
+ }
104
+ walk(root)
105
+ }
106
+ return { layout: nodes.join(';'), animating, layoutDirty, pendingFetches, renderStatsAvailable, requestTotal, requestInFlight }
107
+ }
108
+
109
+ // how long generic background network is allowed to keep us waiting AFTER
110
+ // the screen is otherwise visually settled. apps with continuous traffic
111
+ // (polling, realtime sockets, a tap that fires 20-50 fetches) never reach
112
+ // requestInFlight===0 / a stable requestTotal, so requiring full network
113
+ // quiet burns the ENTIRE budget on every command — the leading idle that
114
+ // failed PR-preview recordings for network-heavy apps (3pc fight-pulse).
115
+ // layout-stability + the image-loader already prove visible content
116
+ // loaded (a data load changes layout; an image load shows in the loader),
117
+ // so the generic request counters are a courtesy, not a gate: honor them
118
+ // only within this grace once the screen is visually still.
119
+ const networkQuietGraceMs = 1200
120
+ let lastLayout = ''
121
+ let lastRequestTotal = -1
122
+ let stable = 0
123
+ let visuallyStillSince = 0
124
+ // counts across the whole budget: a flag that is true on every poll is a
125
+ // different bug from one that is true intermittently, and the two need
126
+ // opposite fixes (engine vs budget).
127
+ let polls = 0
128
+ let dirtyPolls = 0
129
+ let animatingPolls = 0
130
+ let layoutChangedPolls = 0
131
+ while (Date.now() < deadline) {
132
+ const snapshot = await readSnapshot()
133
+ polls++
134
+ if (snapshot.layoutDirty) dirtyPolls++
135
+ if (snapshot.animating) animatingPolls++
136
+ if (polls > 1 && snapshot.layout !== lastLayout) layoutChangedPolls++
137
+ const strictOk =
138
+ !strict ||
139
+ (snapshot.renderStatsAvailable && !snapshot.animating && !snapshot.layoutDirty)
140
+ // visually settled: no animations, no image content loading, layout
141
+ // unchanged since the last poll.
142
+ const visuallyStill =
143
+ strictOk && snapshot.pendingFetches === 0 && snapshot.layout === lastLayout
144
+ // generic network quiet: nothing in flight and no new requests issued.
145
+ const networkQuiet =
146
+ snapshot.requestInFlight === 0 && snapshot.requestTotal === lastRequestTotal
147
+ if (visuallyStill) {
148
+ stable++
149
+ if (visuallyStillSince === 0) visuallyStillSince = Date.now()
150
+ // settle once the screen has held still long enough AND either the
151
+ // network is genuinely quiet OR it has refused to quiet within the
152
+ // grace (continuous background traffic must not block forever).
153
+ if (
154
+ stable >= requiredStablePolls &&
155
+ (networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
156
+ ) {
157
+ return { settled: true, elapsed: Date.now() - start, blockedBy: '' }
158
+ }
159
+ } else {
160
+ stable = 0
161
+ visuallyStillSince = 0
162
+ }
163
+ lastLayout = snapshot.layout
164
+ lastRequestTotal = snapshot.requestTotal
165
+ await sleep(pollMs)
166
+ }
167
+ const last = await readSnapshot()
168
+ const blockers = []
169
+ if (strict && !last.renderStatsAvailable) blockers.push('render stats unavailable')
170
+ if (last.animating) blockers.push('animations still running')
171
+ if (last.layoutDirty) blockers.push('layout still dirty')
172
+ if (last.pendingFetches > 0) blockers.push(last.pendingFetches + ' image fetches pending')
173
+ if (last.layout !== lastLayout) blockers.push('layout still changing')
174
+ if (last.requestInFlight > 0) blockers.push(last.requestInFlight + ' requests in flight')
175
+ return {
176
+ settled: false,
177
+ elapsed: Date.now() - start,
178
+ blockedBy:
179
+ (blockers.length ? blockers.join(', ') : 'unknown') +
180
+ ' over ' + polls + ' polls (layout changed ' + layoutChangedPolls +
181
+ 'x, layoutDirty ' + dirtyPolls + 'x, animating ' + animatingPolls + 'x)',
182
+ }
183
+ })()`,
184
+ },
185
+ {
186
+ timeoutMs: maxMs + 1_000,
187
+ },
188
+ )
189
+
190
+ const { elapsed, settled, blockedBy } = (result ??
191
+ {}) as Partial<WaitForSootsimIdleResult>
192
+ return {
193
+ elapsed: typeof elapsed === 'number' ? elapsed : maxMs,
194
+ settled: settled === true,
195
+ blockedBy: typeof blockedBy === 'string' ? blockedBy : 'unknown',
196
+ }
197
+ }