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,103 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { brotliBundle } from 'sootsim-engine/preview/compress-node'
3
+ import {
4
+ runPresignedUpload,
5
+ type InitRequestBody,
6
+ } from 'sootsim-engine/preview/presigned-upload'
7
+ import { authHeaderValue, cloudAccountIdOrExit, resolveCliAuth } from '../auth'
8
+ import { producePreviewArtifact } from '../cloud-client'
9
+ import { openUrl } from '../open-url'
10
+ import { resolveDefaultUploadOrigin, resolvePublicPreviewOrigin } from './upload'
11
+
12
+ function takeValue(args: string[], flag: string): string | null {
13
+ const index = args.indexOf(flag)
14
+ if (index < 0) return null
15
+ const value = args[index + 1]
16
+ args.splice(index, 2)
17
+ if (!value || value.startsWith('-')) throw new Error(`${flag} requires a value`)
18
+ return value
19
+ }
20
+
21
+ export async function runPreview(args: string[]): Promise<number> {
22
+ if (args.includes('--help') || args.includes('-h')) {
23
+ console.log(`
24
+ rnx preview — publish a prebuilt React Native bundle as a standalone share
25
+
26
+ usage:
27
+ rnx preview <bundle.js|https://bundle-url> [--platform ios|android] [--open]
28
+
29
+ The share contains the immutable build and its automatically discovered Metro assets. It is not a recorded replay.
30
+ `)
31
+ return 0
32
+ }
33
+ try {
34
+ const input = [...args]
35
+ const origin = await resolveDefaultUploadOrigin(
36
+ takeValue(input, '--origin') ?? undefined,
37
+ )
38
+ const publicOrigin = resolvePublicPreviewOrigin(
39
+ origin,
40
+ takeValue(input, '--public-origin') ?? undefined,
41
+ )
42
+ const platform = takeValue(input, '--platform') ?? 'ios'
43
+ const openIndex = input.indexOf('--open')
44
+ const open = openIndex >= 0
45
+ if (open) input.splice(openIndex, 1)
46
+ if (platform !== 'ios' && platform !== 'android') {
47
+ throw new Error('--platform must be ios or android')
48
+ }
49
+ if (input.length !== 1 || input[0]?.startsWith('-')) {
50
+ throw new Error('usage: rnx preview <bundle.js|https://bundle-url>')
51
+ }
52
+ const auth = resolveCliAuth()
53
+ if (!auth) throw new Error('rnx preview needs `rnx login` or RNX_API_KEY')
54
+ const accountId = cloudAccountIdOrExit(auth)
55
+
56
+ const artifact = await producePreviewArtifact(input[0])
57
+ const bundle = brotliBundle(artifact.bytes)
58
+ const initBody: InitRequestBody = {
59
+ kind: 'build',
60
+ buildPlatform: platform,
61
+ contentHash: createHash('sha256').update(artifact.bytes).digest('hex'),
62
+ bundleSizeBytes: artifact.bytes.byteLength,
63
+ bundleContentType: 'application/javascript',
64
+ bundleEncoding: 'br',
65
+ bundleMinified: true,
66
+ bundleOrigin: null,
67
+ entry: null,
68
+ isTransformed: false,
69
+ deviceSpec:
70
+ platform === 'android'
71
+ ? { model: 'pixel-8', width: 412, height: 915 }
72
+ : { model: 'iphone-16', width: 402, height: 874 },
73
+ files: [],
74
+ }
75
+ const { finalize } = await runPresignedUpload({
76
+ originBase: origin,
77
+ endpoints: {
78
+ init: '/api/v1/previews',
79
+ finalize: '/api/v1/previews/finalize',
80
+ },
81
+ initBody,
82
+ bundleBytes: bundle,
83
+ filesByHash: new Map(),
84
+ authHeader: authHeaderValue(auth),
85
+ accountId,
86
+ })
87
+ const url = finalize.url.startsWith('http')
88
+ ? finalize.url
89
+ : `${publicOrigin}${finalize.url}`
90
+ console.log(` preview: ${url}`)
91
+ console.log(
92
+ ` artifact: sha256:${artifact.sha256} (${artifact.bytes.byteLength} bytes)`,
93
+ )
94
+ for (const warning of artifact.warnings) console.warn(` warning: ${warning}`)
95
+ if (open) await openUrl(url)
96
+ return 0
97
+ } catch (error) {
98
+ console.error(
99
+ ` rnx preview failed: ${error instanceof Error ? error.message : String(error)}`,
100
+ )
101
+ return 1
102
+ }
103
+ }
@@ -0,0 +1,106 @@
1
+ // rnx storage profile — manage isolated app storage profiles.
2
+
3
+ import {
4
+ clearProfileStorage,
5
+ createProfile,
6
+ deleteProfile,
7
+ ensureProfile,
8
+ listProfiles,
9
+ } from '../../src/profiles'
10
+
11
+ interface ProfileOptions {
12
+ port?: number
13
+ verbose?: boolean
14
+ }
15
+
16
+ function printHelp() {
17
+ console.log(`
18
+ rnx storage profile — manage isolated storage profiles
19
+
20
+ usage:
21
+ rnx storage profile list
22
+ rnx storage profile create <id>
23
+ rnx storage profile clear <id>
24
+ rnx storage profile delete <id>
25
+
26
+ profiles isolate cookies, localStorage, IndexedDB, cache, permissions, and
27
+ service workers in Electron and Playwright launches. Plain browser-tab launches
28
+ cannot honor profiles and will fail clearly if a profile is requested.
29
+
30
+ examples:
31
+ rnx storage profile list
32
+ rnx storage profile create qa
33
+ rnx open 8081 --profile qa
34
+ rnx open 8081 --profile qa --driver playwright
35
+ rnx storage profile clear qa
36
+ rnx storage profile delete qa
37
+ `)
38
+ }
39
+
40
+ export async function runProfile(args: string[], _opts: ProfileOptions): Promise<number> {
41
+ if (args.includes('--help') || args.includes('-h')) {
42
+ printHelp()
43
+ return 0
44
+ }
45
+
46
+ const subcommand = args[0] ?? 'list'
47
+ try {
48
+ switch (subcommand) {
49
+ case 'list': {
50
+ const profiles = listProfiles()
51
+ for (const profile of profiles) {
52
+ console.log(` ${profile.id}`)
53
+ }
54
+ return 0
55
+ }
56
+ case 'create': {
57
+ const id = args[1]
58
+ if (!id) {
59
+ console.error(' profile create expects <id>')
60
+ return 1
61
+ }
62
+ const profile = createProfile(id)
63
+ console.log(` created profile: ${profile.id}`)
64
+ return 0
65
+ }
66
+ case 'ensure': {
67
+ const id = args[1]
68
+ if (!id) {
69
+ console.error(' profile ensure expects <id>')
70
+ return 1
71
+ }
72
+ const profile = ensureProfile(id)
73
+ console.log(` profile: ${profile.id}`)
74
+ return 0
75
+ }
76
+ case 'clear': {
77
+ const id = args[1]
78
+ if (!id) {
79
+ console.error(' profile clear expects <id>')
80
+ return 1
81
+ }
82
+ ensureProfile(id)
83
+ clearProfileStorage(id)
84
+ console.log(` cleared profile: ${id}`)
85
+ return 0
86
+ }
87
+ case 'delete': {
88
+ const id = args[1]
89
+ if (!id) {
90
+ console.error(' profile delete expects <id>')
91
+ return 1
92
+ }
93
+ const profile = deleteProfile(id)
94
+ console.log(` deleted profile: ${profile.id}`)
95
+ return 0
96
+ }
97
+ default:
98
+ console.error(` unknown profile command: ${subcommand}`)
99
+ printHelp()
100
+ return 1
101
+ }
102
+ } catch (err) {
103
+ console.error(` profile failed: ${err instanceof Error ? err.message : String(err)}`)
104
+ return 1
105
+ }
106
+ }
@@ -0,0 +1,353 @@
1
+ import {
2
+ callInBridge,
3
+ callInBridgeWrite,
4
+ createBridgeFromParsed,
5
+ parseBridgeCliArgs,
6
+ } from '../ws-bridge'
7
+
8
+ interface ReactOptions {
9
+ port?: number
10
+ verbose?: boolean
11
+ }
12
+
13
+ interface SlowRow {
14
+ displayName: string
15
+ totalMs: number
16
+ avgMs: number
17
+ commitCount: number
18
+ slowestCommit: number
19
+ slowestMs: number
20
+ }
21
+
22
+ interface RerenderRow {
23
+ displayName: string
24
+ count: number
25
+ renders?: number
26
+ causes: Record<string, number>
27
+ }
28
+
29
+ interface TreeRow {
30
+ id: number
31
+ displayName: string
32
+ kind: string
33
+ depth: number
34
+ childCount: number
35
+ }
36
+
37
+ function printHelp() {
38
+ console.log(`
39
+ rnx perf react — inspect React component render cost in the tenant worker
40
+
41
+ usage:
42
+ rnx perf react profile start [--include-props] [--max-commits N]
43
+ rnx perf react profile stop
44
+ rnx perf react summary [--limit 10] [--json]
45
+ rnx perf react slow [--limit 5] [--json]
46
+ rnx perf react rerenders [--limit 5] [--json]
47
+ rnx perf react tree [--depth 3] [--find <name>] [--json]
48
+ rnx perf react why <fiber-id> [--json]
49
+
50
+ \`react summary\` is the demo-friendly shortcut: it runs both slow and
51
+ rerenders in one round-trip, prints them as a single table per side,
52
+ and surfaces the duration-zero warning prominently when the bundle is
53
+ running without React profiler timers.
54
+
55
+ examples:
56
+ rnx perf react profile start
57
+ rnx do tap-text Inbox
58
+ rnx perf react profile stop
59
+ rnx perf react summary --limit 10
60
+ rnx perf react slow --limit 10
61
+ rnx perf react rerenders --json
62
+ `)
63
+ }
64
+
65
+ function valueOf(args: string[], flag: string): string | undefined {
66
+ const idx = args.indexOf(flag)
67
+ if (idx >= 0 && idx + 1 < args.length) return args[idx + 1]
68
+ const prefix = `${flag}=`
69
+ const found = args.find((arg) => arg.startsWith(prefix))
70
+ return found ? found.slice(prefix.length) : undefined
71
+ }
72
+
73
+ function numberFlag(args: string[], flag: string, fallback: number): number {
74
+ const raw = valueOf(args, flag)
75
+ if (raw === undefined) return fallback
76
+ const value = Number(raw)
77
+ return Number.isFinite(value) && value > 0 ? Math.round(value) : fallback
78
+ }
79
+
80
+ function formatTable(headers: string[], rows: string[][]): string {
81
+ const widths = headers.map((h, i) =>
82
+ Math.max(h.length, ...rows.map((row) => row[i]?.length ?? 0)),
83
+ )
84
+ const line = (cells: string[]) =>
85
+ cells
86
+ .map((cell, i) => cell.padEnd(widths[i]))
87
+ .join(' ')
88
+ .trimEnd()
89
+ return [
90
+ line(headers),
91
+ line(headers.map((h) => '-'.repeat(h.length))),
92
+ ...rows.map(line),
93
+ ]
94
+ .map((row) => ` ${row}`)
95
+ .join('\n')
96
+ }
97
+
98
+ function printJson(value: unknown) {
99
+ console.log(JSON.stringify(value, null, 2))
100
+ }
101
+
102
+ function printSlow(result: {
103
+ rows?: SlowRow[]
104
+ commits?: number
105
+ error?: string
106
+ durationWarning?: string
107
+ }) {
108
+ if (result.error) console.error(` warning: ${result.error}`)
109
+ if (result.durationWarning) console.error(` warning: ${result.durationWarning}`)
110
+ const rows = result.rows ?? []
111
+ if (rows.length === 0) {
112
+ console.log(` no React render samples yet (${result.commits ?? 0} commit(s))`)
113
+ return
114
+ }
115
+ console.log(
116
+ formatTable(
117
+ ['component', 'total', 'avg', 'commits', 'slowest'],
118
+ rows.map((row) => [
119
+ row.displayName,
120
+ `${row.totalMs.toFixed(2)}ms`,
121
+ `${row.avgMs.toFixed(2)}ms`,
122
+ String(row.commitCount),
123
+ `#${row.slowestCommit} ${row.slowestMs.toFixed(2)}ms`,
124
+ ]),
125
+ ),
126
+ )
127
+ }
128
+
129
+ function printRerenders(result: {
130
+ rows?: RerenderRow[]
131
+ commits?: number
132
+ error?: string
133
+ }) {
134
+ if (result.error) console.error(` warning: ${result.error}`)
135
+ const rows = result.rows ?? []
136
+ if (rows.length === 0) {
137
+ console.log(` no React render samples yet (${result.commits ?? 0} commit(s))`)
138
+ return
139
+ }
140
+ console.log(
141
+ formatTable(
142
+ ['component', 'renders', 'causes'],
143
+ rows.map((row) => {
144
+ const causes = Object.entries(row.causes ?? {})
145
+ .filter(([, count]) => count > 0)
146
+ .sort((a, b) => b[1] - a[1])
147
+ .slice(0, 4)
148
+ .map(([cause, count]) => `${cause}:${count}`)
149
+ .join(', ')
150
+ return [row.displayName, String(row.renders ?? row.count), causes || '-']
151
+ }),
152
+ ),
153
+ )
154
+ }
155
+
156
+ function printTree(result: { rows?: TreeRow[]; roots?: number; error?: string }) {
157
+ if (result.error) console.error(` warning: ${result.error}`)
158
+ const rows = result.rows ?? []
159
+ if (rows.length === 0) {
160
+ console.log(` no React tree data yet (${result.roots ?? 0} root(s))`)
161
+ return
162
+ }
163
+ for (const row of rows) {
164
+ const indent = ' '.repeat(row.depth)
165
+ console.log(
166
+ ` ${indent}@${row.id} ${row.displayName} [${row.kind}] children=${row.childCount}`,
167
+ )
168
+ }
169
+ }
170
+
171
+ function printWhy(result: { sample?: any; error?: string }) {
172
+ if (result.error) {
173
+ console.error(` ${result.error}`)
174
+ return
175
+ }
176
+ const sample = result.sample
177
+ if (!sample) {
178
+ console.log(' no sample')
179
+ return
180
+ }
181
+ console.log(` @${sample.fiberId} ${sample.displayName}`)
182
+ console.log(` duration: ${Number(sample.actualDuration ?? 0).toFixed(2)}ms`)
183
+ console.log(` causes: ${(sample.causes ?? []).join(', ') || '-'}`)
184
+ if (sample.propDiffs?.length) {
185
+ console.log(' props:')
186
+ for (const diff of sample.propDiffs) {
187
+ const values =
188
+ 'old' in diff || 'new' in diff
189
+ ? ` ${JSON.stringify(diff.old)} → ${JSON.stringify(diff.new)}`
190
+ : ''
191
+ console.log(` ${diff.key}${values}`)
192
+ }
193
+ }
194
+ if (sample.parentChain?.length) {
195
+ console.log(` parents: ${sample.parentChain.join(' ← ')}`)
196
+ }
197
+ }
198
+
199
+ export async function runReact(args: string[], opts: ReactOptions): Promise<number> {
200
+ const parsed = parseBridgeCliArgs(args, {
201
+ port: opts.port,
202
+ stripBooleanFlags: ['--json', '--include-props', '--help', '-h'],
203
+ stripValueFlags: ['--limit', '--depth', '--find', '--max-commits'],
204
+ })
205
+
206
+ if (args.includes('--help') || args.includes('-h')) {
207
+ printHelp()
208
+ return 0
209
+ }
210
+
211
+ const [first, second] = parsed.positional
212
+ const json = args.includes('--json')
213
+ const bridge = createBridgeFromParsed(parsed)
214
+
215
+ try {
216
+ if (first === 'profile' && second === 'start') {
217
+ const result = await callInBridgeWrite(
218
+ bridge,
219
+ 'SootSim.bridges.reactProfile.start',
220
+ {
221
+ includeProps: args.includes('--include-props'),
222
+ maxCommits: numberFlag(args, '--max-commits', 2000),
223
+ },
224
+ )
225
+ if (json) printJson(result)
226
+ else console.log(' React profile started')
227
+ return 0
228
+ }
229
+
230
+ if (first === 'profile' && second === 'stop') {
231
+ const result = (await callInBridgeWrite(
232
+ bridge,
233
+ 'SootSim.bridges.reactProfile.stop',
234
+ )) as { commits?: number; error?: string }
235
+ if (json) printJson(result)
236
+ else {
237
+ console.log(` React profile stopped (${result.commits ?? 0} commit(s))`)
238
+ if (result.error) console.error(` warning: ${result.error}`)
239
+ }
240
+ return result.error ? 1 : 0
241
+ }
242
+
243
+ if (first === 'summary') {
244
+ // run slow + rerenders in parallel against the existing bridge.
245
+ // demo-friendly: one command, one table per dimension, plus a
246
+ // single duration-zero warning that's easy to call out on stage.
247
+ const limit = numberFlag(args, '--limit', 10)
248
+ const [slowResult, rerendersResult] = (await Promise.all([
249
+ callInBridge(bridge, 'SootSim.bridges.reactProfile.slow', { limit }),
250
+ callInBridge(bridge, 'SootSim.bridges.reactProfile.rerenders', { limit }),
251
+ ])) as [
252
+ {
253
+ rows?: SlowRow[]
254
+ commits?: number
255
+ error?: string
256
+ durationWarning?: string
257
+ },
258
+ { rows?: RerenderRow[]; commits?: number; error?: string },
259
+ ]
260
+ if (json) {
261
+ printJson({
262
+ commits: slowResult.commits ?? rerendersResult.commits ?? 0,
263
+ slow: slowResult,
264
+ rerenders: rerendersResult,
265
+ })
266
+ return 0
267
+ }
268
+ const commits = slowResult.commits ?? rerendersResult.commits ?? 0
269
+ console.log(` React profile summary (${commits} commit(s)):`)
270
+ if (slowResult.error) console.error(` warning: ${slowResult.error}`)
271
+ if (slowResult.durationWarning) {
272
+ console.error(` warning: ${slowResult.durationWarning}`)
273
+ }
274
+ if (rerendersResult.error) {
275
+ console.error(` warning: ${rerendersResult.error}`)
276
+ }
277
+ console.log('\n slowest renders:')
278
+ printSlow(slowResult)
279
+ console.log('\n most rerenders:')
280
+ printRerenders(rerendersResult)
281
+ // hint about the actualDuration-zero limitation when the slow
282
+ // table is empty/zero — the demo-stage signal is "no timer
283
+ // data, expect rerenders rows only".
284
+ const slowestRow = slowResult.rows?.[0]
285
+ const allZero =
286
+ slowResult.rows && slowResult.rows.length > 0
287
+ ? slowResult.rows.every((r) => r.totalMs <= 0 && r.slowestMs <= 0)
288
+ : false
289
+ if (allZero && !slowResult.durationWarning) {
290
+ console.error(
291
+ `\n warning: every commit reports actualDuration=0ms — the React profiler timer is disabled in this bundle (rerender counts are still accurate; ${slowestRow?.displayName ? `top-listed: ${slowestRow.displayName}` : 'no slow signal'}).`,
292
+ )
293
+ }
294
+ return 0
295
+ }
296
+
297
+ if (first === 'slow') {
298
+ const result = (await callInBridge(bridge, 'SootSim.bridges.reactProfile.slow', {
299
+ limit: numberFlag(args, '--limit', 10),
300
+ })) as {
301
+ rows?: SlowRow[]
302
+ commits?: number
303
+ error?: string
304
+ durationWarning?: string
305
+ }
306
+ if (json) printJson(result)
307
+ else printSlow(result)
308
+ return 0
309
+ }
310
+
311
+ if (first === 'rerenders') {
312
+ const result = (await callInBridge(
313
+ bridge,
314
+ 'SootSim.bridges.reactProfile.rerenders',
315
+ { limit: numberFlag(args, '--limit', 10) },
316
+ )) as { rows?: RerenderRow[]; commits?: number; error?: string }
317
+ if (json) printJson(result)
318
+ else printRerenders(result)
319
+ return 0
320
+ }
321
+
322
+ if (first === 'tree') {
323
+ const result = (await callInBridge(bridge, 'SootSim.bridges.reactProfile.tree', {
324
+ depth: numberFlag(args, '--depth', 3),
325
+ find: valueOf(args, '--find'),
326
+ })) as { rows?: TreeRow[]; roots?: number; error?: string }
327
+ if (json) printJson(result)
328
+ else printTree(result)
329
+ return 0
330
+ }
331
+
332
+ if (first === 'why') {
333
+ const fiberId = Number(second)
334
+ if (!Number.isFinite(fiberId) || fiberId <= 0) {
335
+ console.error(' usage: rnx perf react why <fiber-id>')
336
+ return 1
337
+ }
338
+ const result = (await callInBridge(
339
+ bridge,
340
+ 'SootSim.bridges.reactProfile.why',
341
+ Math.round(fiberId),
342
+ )) as { sample?: unknown; error?: string }
343
+ if (json) printJson(result)
344
+ else printWhy(result)
345
+ return result.error ? 1 : 0
346
+ }
347
+
348
+ printHelp()
349
+ return first ? 1 : 0
350
+ } finally {
351
+ bridge.close()
352
+ }
353
+ }