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,697 @@
1
+ // SimClient — the typed, promise-based client every rnx frontend drives a
2
+ // sim through. it wraps a bridge transport (the CLI's ws bridge, or any
3
+ // InspectBridge-shaped channel) and reuses the existing inspect verbs as its
4
+ // implementation; it never re-implements tap resolution, find, settling, or
5
+ // readiness.
6
+ //
7
+ // MUST stay browser-safe: no node builtins, no `process`, no `ws`. the ws
8
+ // transport and `connect()` live in ./connect (node-only) so this class can
9
+ // also back in-page transports later. runtime imports here may only reach the
10
+ // browser-safe inspect kernels.
11
+
12
+ import {
13
+ isTapSuccess,
14
+ tapBest,
15
+ tapById,
16
+ tapByText,
17
+ type TapOutcome,
18
+ type TapTextOptions,
19
+ } from '../cli/commands/inspect/actions'
20
+ import {
21
+ filterLogEntries,
22
+ getShellState,
23
+ inspectAccessibilityTree,
24
+ inspectDescribe,
25
+ inspectErrors,
26
+ inspectFind,
27
+ inspectKeyboard,
28
+ inspectLogs,
29
+ inspectTree,
30
+ inspectUrl,
31
+ inspectWaitReady,
32
+ inspectWarnings,
33
+ waitReadyReason,
34
+ type AccessibilityTreeNode,
35
+ type ConsoleEntry,
36
+ type DescribeResult,
37
+ type FindQuery,
38
+ type InspectBridge,
39
+ type InteractiveNode,
40
+ type KeyboardState,
41
+ type LogEntry,
42
+ type LogFilterOptions,
43
+ type WaitReadyStatus,
44
+ } from '../cli/commands/inspect/core'
45
+ import { resolveTargetCoords } from '../cli/commands/inspect/resolve-target'
46
+ import {
47
+ waitForSootsimIdle,
48
+ type WaitForSootsimIdleResult,
49
+ } from '../cli/commands/inspect/settling'
50
+ import { openAppUrl } from './app-url'
51
+ import type { BridgeClaimResult, BridgeSimInfo, WsBridge } from '../cli/ws-bridge'
52
+ import type {
53
+ PerformOptions,
54
+ PerformResult,
55
+ PerformStep,
56
+ ResetOptions,
57
+ ResetResult,
58
+ SimSemanticNode,
59
+ SimSemanticResolveResult,
60
+ SimSemanticResolveSelector,
61
+ SimStateOptions,
62
+ SimStateResult,
63
+ } from './bridge-contract'
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // typed errors — extend the BridgeSimLockedError precedent so scripts can
67
+ // branch with instanceof instead of parsing message strings.
68
+ // ---------------------------------------------------------------------------
69
+
70
+ export class SimClientError extends Error {
71
+ constructor(message: string) {
72
+ super(message)
73
+ this.name = new.target.name
74
+ }
75
+ }
76
+
77
+ /** no daemon/bridge reachable, no sim connected, or the target sim is gone. */
78
+ export class SimConnectError extends SimClientError {}
79
+
80
+ /** `waitReady` ran out of budget. carries the last probe status. */
81
+ export class SimReadyTimeoutError extends SimClientError {
82
+ status: WaitReadyStatus
83
+ constructor(status: WaitReadyStatus) {
84
+ super(`sim not ready: ${waitReadyReason(status)}`)
85
+ this.status = status
86
+ }
87
+ }
88
+
89
+ /** a tap could not resolve its target or the dispatched press missed. */
90
+ export class SimTapError extends SimClientError {
91
+ outcome: TapOutcome | null
92
+ constructor(message: string, outcome: TapOutcome | null = null) {
93
+ super(message)
94
+ this.outcome = outcome
95
+ }
96
+ }
97
+
98
+ /** `type`/`press` with no visible keyboard (nothing focused to receive it). */
99
+ export class SimKeyboardError extends SimClientError {}
100
+
101
+ /** a `perform` batch reported failure. carries the full step-by-step result. */
102
+ export class SimPerformError extends SimClientError {
103
+ result: PerformResult
104
+ constructor(result: PerformResult) {
105
+ const failed = result.steps.find((step) => !step.ok)
106
+ const at = failed ? `${failed.index} (${failed.type})` : String(result.completed)
107
+ const cause = failed?.error ?? result.error
108
+ super(`perform failed at step ${at}${cause ? `: ${cause}` : ''}`)
109
+ this.result = result
110
+ }
111
+ }
112
+
113
+ /** a `reset` reported failure. carries the engine's result. */
114
+ export class SimResetError extends SimClientError {
115
+ result: ResetResult
116
+ constructor(result: ResetResult) {
117
+ super(`reset (${result.strategy}) failed${result.error ? `: ${result.error}` : ''}`)
118
+ this.result = result
119
+ }
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // public option/target shapes
124
+ // ---------------------------------------------------------------------------
125
+
126
+ export interface SimFindOptions {
127
+ testId?: string
128
+ text?: string
129
+ role?: string
130
+ type?: string
131
+ pressable?: boolean
132
+ }
133
+
134
+ export type SimTapTarget =
135
+ | string
136
+ | { x: number; y: number }
137
+ | { testId: string }
138
+ | ({ text: string } & TapTextOptions)
139
+ | InteractiveNode
140
+
141
+ // the contract deliberately has no text-targeted step (text matching is fuzzy
142
+ // and lives once in the CLI's tap resolution). the SDK accepts one as sugar and
143
+ // resolves it to a coordinate `tap` step client-side before sending ONE batch.
144
+ export type SimPerformStep = PerformStep | { type: 'tapText'; text: string }
145
+
146
+ export interface SimScreenshot {
147
+ dataUrl: string
148
+ bytes: Uint8Array
149
+ }
150
+
151
+ export interface SimScreenshotOptions {
152
+ layers?: 'full' | 'tenant' | 'shell'
153
+ crop?: { x: number; y: number; w: number; h: number }
154
+ }
155
+
156
+ export interface SimClientOptions {
157
+ /** default per-command bridge timeout. */
158
+ commandTimeoutMs?: number
159
+ }
160
+
161
+ function sleep(ms: number): Promise<void> {
162
+ return new Promise((resolve) => setTimeout(resolve, ms))
163
+ }
164
+
165
+ function decodePngDataUrl(dataUrl: unknown): SimScreenshot {
166
+ if (typeof dataUrl !== 'string' || !dataUrl.startsWith('data:image/png;base64,')) {
167
+ throw new SimClientError(
168
+ `screenshot bridge returned a non-png payload: ${JSON.stringify(
169
+ typeof dataUrl === 'string' ? dataUrl.slice(0, 80) : dataUrl,
170
+ )}`,
171
+ )
172
+ }
173
+ const base64 = dataUrl.slice('data:image/png;base64,'.length)
174
+ const binary = atob(base64)
175
+ const bytes = new Uint8Array(binary.length)
176
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
177
+ return { dataUrl, bytes }
178
+ }
179
+
180
+ // derive the shell base for navigating this sim to a new target, from the
181
+ // sim's current URL: keep the origin and every non-target param (device,
182
+ // renderMode, debug…), drop the params and `/rn` path segment that name the
183
+ // current target. mirrors the CLI open command's base derivation; the shared
184
+ // module extraction is tracked with the driver.
185
+ const TARGET_SEARCH_PARAMS = [
186
+ 'bundle',
187
+ 'demo',
188
+ 'app',
189
+ 'open',
190
+ 'port',
191
+ 'inspectOpen',
192
+ 'appFonts',
193
+ 'appSplash',
194
+ ]
195
+
196
+ export function buildSimOpenUrl(currentUrl: string, target: number | string): string {
197
+ const url = new URL(currentUrl)
198
+ for (const param of TARGET_SEARCH_PARAMS) url.searchParams.delete(param)
199
+ // shell route grammar: an optional /__soot prefix, then /rn, /rn/<port>, or
200
+ // /app/<name>. the current route is replaced wholesale, keeping the prefix.
201
+ const pathname = url.pathname.replace(/\/+$/, '') || '/'
202
+ const prefix =
203
+ pathname === '/__soot' || pathname.startsWith('/__soot/') ? '/__soot' : ''
204
+ const normalized = String(target).trim()
205
+ if (/^\d+$/.test(normalized)) {
206
+ // `/rn/<port>` — the shell's own connect route resolves the metro bundle
207
+ // in-page, so the SDK never grows a second bundle-resolution path.
208
+ url.pathname = `${prefix}/rn/${normalized}`
209
+ } else {
210
+ url.pathname = `${prefix}/rn`
211
+ url.searchParams.set('open', normalized)
212
+ }
213
+ return url.toString()
214
+ }
215
+
216
+ // a `{ text, ...TapTextOptions }` target, as distinct from a node returned by
217
+ // find() (which carries layout/absolutePosition alongside its text).
218
+ function isTextTapTarget(
219
+ target: SimTapTarget,
220
+ ): target is { text: string } & TapTextOptions {
221
+ return (
222
+ typeof target === 'object' &&
223
+ 'text' in target &&
224
+ typeof target.text === 'string' &&
225
+ !('layout' in target) &&
226
+ !('absolutePosition' in target)
227
+ )
228
+ }
229
+
230
+ // ---------------------------------------------------------------------------
231
+ // SimClient
232
+ // ---------------------------------------------------------------------------
233
+
234
+ // implements the full WsBridge surface (not just InspectBridge) so it can be
235
+ // handed to every existing verb and CLI helper unchanged, with each command
236
+ // pinned to the sim this client was connected to.
237
+ export class SimClient implements WsBridge, InspectBridge {
238
+ readonly simId: string
239
+ private readonly bridge: WsBridge
240
+ private closed = false
241
+
242
+ constructor(bridge: WsBridge, simId: string, _options: SimClientOptions = {}) {
243
+ this.bridge = bridge
244
+ this.simId = simId
245
+ }
246
+
247
+ get plane(): WsBridge['plane'] {
248
+ return this.bridge.plane
249
+ }
250
+
251
+ // --- transport (WsBridge/InspectBridge) ---
252
+
253
+ send(
254
+ cmd: { type: string; [k: string]: any },
255
+ opts?: { timeoutMs?: number },
256
+ ): Promise<any> {
257
+ if (this.closed) {
258
+ return Promise.reject(new SimConnectError('sim client is closed'))
259
+ }
260
+ const payload = cmd.simId === undefined ? { ...cmd, simId: this.simId } : cmd
261
+ return this.bridge.send(payload, opts)
262
+ }
263
+
264
+ listSims(): Promise<BridgeSimInfo[]> {
265
+ return this.bridge.listSims()
266
+ }
267
+
268
+ resolveReloadedSim(retiredSimId: string): Promise<string | null> {
269
+ return this.bridge.resolveReloadedSim(retiredSimId)
270
+ }
271
+
272
+ focusSim(simId: string = this.simId): Promise<any> {
273
+ return this.bridge.focusSim(simId)
274
+ }
275
+
276
+ closeSim(simId: string = this.simId): Promise<any> {
277
+ return this.bridge.closeSim(simId)
278
+ }
279
+
280
+ claim(
281
+ simId: string = this.simId,
282
+ opts?: { force?: boolean },
283
+ ): Promise<BridgeClaimResult> {
284
+ return this.bridge.claim(simId, opts)
285
+ }
286
+
287
+ /** release the socket. a script that connected must close to exit cleanly. */
288
+ close(): void {
289
+ if (this.closed) return
290
+ this.closed = true
291
+ this.bridge.close()
292
+ }
293
+
294
+ // --- navigation + readiness ---
295
+
296
+ /**
297
+ * point this sim at a dev target: a metro port (`8081`), or anything the
298
+ * shell's connect flow accepts (`localhost:8081`, an app slug…). resolves
299
+ * once the reloaded page answers on the new target route.
300
+ */
301
+ async open(target: number | string, opts: { timeoutMs?: number } = {}): Promise<void> {
302
+ const sims = await this.listSims()
303
+ const sim = sims.find((entry) => entry.id === this.simId)
304
+ if (!sim) {
305
+ throw new SimConnectError(`sim ${this.simId} is no longer connected`)
306
+ }
307
+ const currentUrl = sim.url ?? sim.origin
308
+ if (!currentUrl) {
309
+ throw new SimConnectError(
310
+ `sim ${this.simId} reported no URL to derive the shell from`,
311
+ )
312
+ }
313
+ const nextUrl = new URL(buildSimOpenUrl(currentUrl, target))
314
+ // the page being replaced keeps answering until the browser commits the
315
+ // navigation, and it may already sit on this target. only a one-off token
316
+ // in the new URL proves the reloaded page is the one answering.
317
+ const token = `sim-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
318
+ nextUrl.searchParams.set('inspectOpen', token)
319
+ await this.send({
320
+ type: 'evaluate',
321
+ code: `window.location.href = ${JSON.stringify(nextUrl.toString())}`,
322
+ })
323
+ const deadline = Date.now() + (opts.timeoutMs ?? 30_000)
324
+ while (Date.now() < deadline) {
325
+ await sleep(250)
326
+ try {
327
+ const { url } = await inspectUrl(this)
328
+ if (new URL(url).searchParams.get('inspectOpen') === token) return
329
+ } catch {
330
+ // page is mid-reload; keep polling until the deadline.
331
+ }
332
+ }
333
+ throw new SimConnectError(
334
+ `timed out waiting for sim ${this.simId} to load ${nextUrl}`,
335
+ )
336
+ }
337
+
338
+ /** dispatch a route or app URL through React Native Linking in this guest. */
339
+ openUrl(target: string): Promise<string> {
340
+ return openAppUrl(this, target)
341
+ }
342
+
343
+ async setAppearance(appearance: 'light' | 'dark'): Promise<void> {
344
+ await this.send({ type: 'setAppearance', appearance })
345
+ }
346
+
347
+ /**
348
+ * block until the guest app has mounted and painted real content.
349
+ * throws SimReadyTimeoutError (with the last probe status) on timeout.
350
+ */
351
+ async waitReady(
352
+ opts: {
353
+ timeoutMs?: number
354
+ onProgress?: (status: WaitReadyStatus) => void
355
+ } = {},
356
+ ): Promise<WaitReadyStatus> {
357
+ const status = await inspectWaitReady(this, opts.timeoutMs ?? 20_000, {
358
+ onProgress: opts.onProgress,
359
+ })
360
+ if (!status.ready) throw new SimReadyTimeoutError(status)
361
+ return status
362
+ }
363
+
364
+ /** wait for layout/animation/transition settle after an interaction. */
365
+ settle(
366
+ opts: { maxMs?: number; strict?: boolean } = {},
367
+ ): Promise<WaitForSootsimIdleResult> {
368
+ return waitForSootsimIdle({
369
+ bridge: this,
370
+ maxMs: opts.maxMs ?? 3_000,
371
+ strict: opts.strict,
372
+ })
373
+ }
374
+
375
+ // --- read verbs ---
376
+
377
+ /** first matching node, or null. query precedence matches the CLI's find. */
378
+ async find(query: SimFindOptions): Promise<InteractiveNode | null> {
379
+ const nodes = await this.findAll(query)
380
+ return nodes[0] ?? null
381
+ }
382
+
383
+ async findAll(query: SimFindOptions): Promise<InteractiveNode[]> {
384
+ const mapped: FindQuery = {
385
+ testId: query.testId ?? null,
386
+ role: query.role ?? null,
387
+ type: query.type ?? null,
388
+ text: query.text ?? null,
389
+ pressable: query.pressable,
390
+ }
391
+ const found = await inspectFind(this, mapped)
392
+ if (!found) {
393
+ throw new SimClientError(
394
+ 'find() needs at least one of testId, text, role, type, or pressable',
395
+ )
396
+ }
397
+ const { result } = found
398
+ if (Array.isArray(result)) return result
399
+ return result ? [result as InteractiveNode] : []
400
+ }
401
+
402
+ async tree(depth = 5): Promise<SimSemanticNode[]> {
403
+ const { tree } = await inspectTree(this, depth)
404
+ return Array.isArray(tree) ? tree : []
405
+ }
406
+
407
+ resolve(
408
+ selector: SimSemanticResolveSelector,
409
+ ): Promise<SimSemanticResolveResult | null> {
410
+ return this.send({ type: 'resolve', selector })
411
+ }
412
+
413
+ /** compact visible semantic tree, formatted as the CLI prints it. */
414
+ describe(opts: { filter?: string; verbose?: boolean } = {}): Promise<DescribeResult> {
415
+ return inspectDescribe(this, {
416
+ describe: true,
417
+ verbose: opts.verbose ?? false,
418
+ filter: opts.filter ?? '',
419
+ compact: !opts.verbose,
420
+ hideXy: false,
421
+ fullText: true,
422
+ })
423
+ }
424
+
425
+ accessibilityTree(): Promise<AccessibilityTreeNode[]> {
426
+ return inspectAccessibilityTree(this)
427
+ }
428
+
429
+ async logs(opts: LogFilterOptions = {}): Promise<LogEntry[]> {
430
+ return filterLogEntries(await inspectLogs(this), opts)
431
+ }
432
+
433
+ errors(limit = 20): Promise<ConsoleEntry[]> {
434
+ return inspectErrors(this, limit)
435
+ }
436
+
437
+ warnings(limit = 20): Promise<ConsoleEntry[]> {
438
+ return inspectWarnings(this, limit)
439
+ }
440
+
441
+ keyboard(): Promise<KeyboardState | { error: string }> {
442
+ return inspectKeyboard(this)
443
+ }
444
+
445
+ shellState(readyTimeoutMs = 0): Promise<Record<string, any> | null> {
446
+ return getShellState(this, readyTimeoutMs)
447
+ }
448
+
449
+ async url(): Promise<string> {
450
+ const { url } = await inspectUrl(this)
451
+ return url
452
+ }
453
+
454
+ async screenshot(opts: SimScreenshotOptions = {}): Promise<SimScreenshot> {
455
+ const result = await this.send({
456
+ type: 'screenshot',
457
+ ...(opts.layers ? { layers: opts.layers } : {}),
458
+ ...(opts.crop ? { crop: opts.crop } : {}),
459
+ })
460
+ return decodePngDataUrl(result)
461
+ }
462
+
463
+ // --- input verbs ---
464
+
465
+ /**
466
+ * tap by testID, visible text, node, or coordinates. resolution, retry, and
467
+ * hit verification are the CLI's shared tap kernels. throws SimTapError when
468
+ * the target is missing, covered, offscreen, or the press missed.
469
+ */
470
+ async tap(target: SimTapTarget): Promise<void> {
471
+ if (typeof target === 'string') {
472
+ return this.throwUnlessTapped(await tapBest(this, target), target)
473
+ }
474
+ if ('testId' in target && typeof target.testId === 'string') {
475
+ return this.throwUnlessTapped(await tapById(this, target.testId), target.testId)
476
+ }
477
+ if (isTextTapTarget(target)) {
478
+ const { text, ...textOpts } = target
479
+ return this.throwUnlessTapped(await tapByText(this, text, textOpts), text)
480
+ }
481
+ if (
482
+ 'testID' in target &&
483
+ typeof target.testID === 'string' &&
484
+ typeof target.nodeId !== 'number'
485
+ ) {
486
+ return this.throwUnlessTapped(await tapById(this, target.testID), target.testID)
487
+ }
488
+ const coords = this.coordsForTapTarget(target)
489
+ const result = await this.send({
490
+ type: 'tap',
491
+ x: coords.x,
492
+ y: coords.y,
493
+ ...('nodeId' in target && typeof target.nodeId === 'number'
494
+ ? { target: { nodeId: target.nodeId } }
495
+ : {}),
496
+ })
497
+ if (!isTapSuccess(result)) {
498
+ throw new SimTapError(
499
+ `tap at ${coords.x},${coords.y} missed: ${JSON.stringify(result)}`,
500
+ )
501
+ }
502
+ }
503
+
504
+ async longPress(
505
+ target: SimTapTarget,
506
+ opts: { durationMs?: number } = {},
507
+ ): Promise<void> {
508
+ const coords = await this.resolveCoords(target)
509
+ const result = await this.send(
510
+ {
511
+ type: 'longPress',
512
+ x: coords.x,
513
+ y: coords.y,
514
+ durationMs: opts.durationMs ?? 600,
515
+ },
516
+ { timeoutMs: (opts.durationMs ?? 600) + 15_000 },
517
+ )
518
+ if (!result?.ok) {
519
+ throw new SimTapError(`longPress at ${coords.x},${coords.y} missed`)
520
+ }
521
+ }
522
+
523
+ /** type through the visual iOS keyboard. requires a focused input. */
524
+ async type(text: string): Promise<void> {
525
+ await this.requireVisibleKeyboard('type')
526
+ await this.send({ type: 'keyboard', action: 'type', text })
527
+ }
528
+
529
+ /** press a keyboard key ('return', 'delete', 'space', …). */
530
+ async press(key: string): Promise<void> {
531
+ await this.requireVisibleKeyboard('press')
532
+ await this.send({ type: 'keyboard', action: 'press', text: key })
533
+ }
534
+
535
+ async dismissKeyboard(): Promise<void> {
536
+ await this.send({ type: 'keyboard', action: 'dismiss' })
537
+ }
538
+
539
+ // --- batched input, compound state, reset ---
540
+
541
+ /**
542
+ * execute an ordered step batch inside the page in ONE round-trip, so waits
543
+ * and gesture physics run on real wall-clock timing. `tapText` steps are
544
+ * resolved to coordinates client-side before the batch is sent. throws
545
+ * SimPerformError (carrying the per-step result) when the batch fails.
546
+ */
547
+ async perform(
548
+ steps: SimPerformStep[],
549
+ options?: PerformOptions,
550
+ ): Promise<PerformResult> {
551
+ const resolved: PerformStep[] = []
552
+ for (const step of steps) {
553
+ if (step.type !== 'tapText') {
554
+ resolved.push(step)
555
+ continue
556
+ }
557
+ const target = await resolveTargetCoords(this, {
558
+ mode: 'text',
559
+ value: step.text,
560
+ })
561
+ if (!target) {
562
+ throw new SimTapError(
563
+ `perform tapText: no node with text ${JSON.stringify(step.text)}`,
564
+ )
565
+ }
566
+ resolved.push({
567
+ type: 'tap',
568
+ x: target.x,
569
+ y: target.y,
570
+ target: {
571
+ nodeId: target.nodeId,
572
+ testID: target.testID ?? undefined,
573
+ text: target.text ?? undefined,
574
+ type: target.type ?? undefined,
575
+ },
576
+ })
577
+ }
578
+ const batchTimeoutMs = options?.timeoutMs ?? 30_000
579
+ const result = (await this.send(
580
+ {
581
+ type: 'perform',
582
+ steps: resolved,
583
+ ...(options ? { performOptions: options } : {}),
584
+ },
585
+ { timeoutMs: batchTimeoutMs + 5_000 },
586
+ )) as PerformResult
587
+ if (!result?.ok) throw new SimPerformError(result)
588
+ return result
589
+ }
590
+
591
+ /** screenshot + tree + route + recent errors in one round-trip. */
592
+ state(options?: SimStateOptions): Promise<SimStateResult> {
593
+ return this.send({
594
+ type: 'state',
595
+ ...(options ? { stateOptions: options } : {}),
596
+ }) as Promise<SimStateResult>
597
+ }
598
+
599
+ /**
600
+ * two-tier app state wipe ('data' clears storage/caches, 'full' also clears
601
+ * keychain/permissions/native-module state). throws SimResetError on failure.
602
+ */
603
+ async reset(options?: ResetOptions): Promise<ResetResult> {
604
+ const result = (await this.send(
605
+ { type: 'reset', ...(options ? { resetOptions: options } : {}) },
606
+ { timeoutMs: 60_000 },
607
+ )) as ResetResult
608
+ if (!result?.ok) throw new SimResetError(result)
609
+ return result
610
+ }
611
+
612
+ // --- internals ---
613
+
614
+ private throwUnlessTapped(outcome: TapOutcome, label: string): void {
615
+ if (!outcome.failure && isTapSuccess(outcome.result)) return
616
+ const reason =
617
+ outcome.failure === 'not-found'
618
+ ? 'no matching node'
619
+ : outcome.failure === 'missed'
620
+ ? (outcome.result?.reason ?? 'press missed')
621
+ : outcome.payload?.ambiguous
622
+ ? `ambiguous: ${outcome.payload.total} matches`
623
+ : (outcome.result?.reason ?? outcome.payload?.error ?? 'tap failed')
624
+ throw new SimTapError(`tap ${JSON.stringify(label)} failed: ${reason}`, outcome)
625
+ }
626
+
627
+ private coordsForTapTarget(target: SimTapTarget): { x: number; y: number } {
628
+ if (
629
+ typeof target === 'object' &&
630
+ 'x' in target &&
631
+ 'y' in target &&
632
+ typeof target.x === 'number' &&
633
+ typeof target.y === 'number'
634
+ ) {
635
+ return { x: target.x, y: target.y }
636
+ }
637
+ if (
638
+ typeof target === 'object' &&
639
+ 'absolutePosition' in target &&
640
+ target.absolutePosition &&
641
+ target.layout
642
+ ) {
643
+ return {
644
+ x: target.absolutePosition.x + (target.layout.width ?? 0) / 2,
645
+ y: target.absolutePosition.y + (target.layout.height ?? 0) / 2,
646
+ }
647
+ }
648
+ throw new SimClientError(
649
+ 'tap target needs coordinates, a testId, text, or a node from find()',
650
+ )
651
+ }
652
+
653
+ private async resolveCoords(target: SimTapTarget): Promise<{ x: number; y: number }> {
654
+ if (typeof target === 'string') {
655
+ const resolved =
656
+ (await resolveTargetCoords(this, { mode: 'testid', value: target })) ??
657
+ (await resolveTargetCoords(this, { mode: 'text', value: target }))
658
+ if (!resolved) throw new SimTapError(`no node matching ${JSON.stringify(target)}`)
659
+ return resolved
660
+ }
661
+ if ('testId' in target && typeof target.testId === 'string') {
662
+ const resolved = await resolveTargetCoords(this, {
663
+ mode: 'testid',
664
+ value: target.testId,
665
+ })
666
+ if (!resolved) {
667
+ throw new SimTapError(`no node with testID ${JSON.stringify(target.testId)}`)
668
+ }
669
+ return resolved
670
+ }
671
+ if ('text' in target && typeof target.text === 'string' && !('layout' in target)) {
672
+ const resolved = await resolveTargetCoords(this, {
673
+ mode: 'text',
674
+ value: target.text,
675
+ })
676
+ if (!resolved) {
677
+ throw new SimTapError(`no node with text ${JSON.stringify(target.text)}`)
678
+ }
679
+ return resolved
680
+ }
681
+ return this.coordsForTapTarget(target)
682
+ }
683
+
684
+ private async requireVisibleKeyboard(action: string): Promise<void> {
685
+ const deadline = Date.now() + 1_500
686
+ for (;;) {
687
+ const state = await inspectKeyboard(this)
688
+ if (!('error' in state) && state.visible) return
689
+ if (Date.now() >= deadline) {
690
+ throw new SimKeyboardError(
691
+ `${action} requires the iOS keyboard to be visible — focus an input first (tap a TextInput)`,
692
+ )
693
+ }
694
+ await sleep(150)
695
+ }
696
+ }
697
+ }