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,502 @@
1
+ import { SIM_LONG_PRESS_MAX_MS } from './bridge-contract'
2
+ // untrusted JSON -> bridge-contract argument. one owner for that conversion, so
3
+ // every transport in front of a sim (the RNX Cloud sim service's HTTP and
4
+ // WebSocket routes) validates its request bodies here rather than growing a
5
+ // second reader for the same shapes.
6
+ import type {
7
+ ResetOptions,
8
+ SemanticWaitOptions,
9
+ SimSemanticQuery,
10
+ SimSemanticResolveSelector,
11
+ SimSemanticSelector,
12
+ SimStateOptions,
13
+ } from './bridge-contract'
14
+ import type { SimPerformStep, SimScreenshotOptions } from './sim-client'
15
+ import type { SootSimReplayTarget } from '@rnx/globals'
16
+
17
+ /** the caller sent a value this bridge argument cannot be read from. */
18
+ export class BridgeInputError extends Error {
19
+ constructor(message: string) {
20
+ super(message)
21
+ this.name = 'BridgeInputError'
22
+ }
23
+ }
24
+
25
+ export const SEMANTIC_WAIT_MAX_TIMEOUT_MS = 30_000
26
+
27
+ function validationFailure(path: string, expectation: string): never {
28
+ throw new BridgeInputError(`${path} ${expectation}`)
29
+ }
30
+
31
+ function isRecordValue(value: unknown): value is Record<string, unknown> {
32
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
33
+ }
34
+
35
+ function recordValue(value: unknown, path: string): Record<string, unknown> {
36
+ if (!isRecordValue(value)) {
37
+ validationFailure(path, 'must be an object')
38
+ }
39
+ return value
40
+ }
41
+
42
+ function stringValue(value: unknown, path: string): string {
43
+ if (typeof value !== 'string') validationFailure(path, 'must be a string')
44
+ return value
45
+ }
46
+
47
+ function numberValue(value: unknown, path: string): number {
48
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
49
+ validationFailure(path, 'must be a finite number')
50
+ }
51
+ return value
52
+ }
53
+
54
+ function booleanValue(value: unknown, path: string): boolean {
55
+ if (typeof value !== 'boolean') validationFailure(path, 'must be a boolean')
56
+ return value
57
+ }
58
+
59
+ function optionalNumber(
60
+ record: Record<string, unknown>,
61
+ key: string,
62
+ path: string,
63
+ ): number | undefined {
64
+ return record[key] === undefined
65
+ ? undefined
66
+ : numberValue(record[key], `${path}.${key}`)
67
+ }
68
+
69
+ function optionalBoolean(
70
+ record: Record<string, unknown>,
71
+ key: string,
72
+ path: string,
73
+ ): boolean | undefined {
74
+ return record[key] === undefined
75
+ ? undefined
76
+ : booleanValue(record[key], `${path}.${key}`)
77
+ }
78
+
79
+ function semanticWaitTimeout(
80
+ record: Record<string, unknown>,
81
+ path: string,
82
+ ): number | undefined {
83
+ const timeoutMs = optionalNumber(record, 'timeoutMs', path)
84
+ if (timeoutMs !== undefined && timeoutMs > SEMANTIC_WAIT_MAX_TIMEOUT_MS) {
85
+ validationFailure(
86
+ `${path}.timeoutMs`,
87
+ `must be at most ${SEMANTIC_WAIT_MAX_TIMEOUT_MS}`,
88
+ )
89
+ }
90
+ return timeoutMs
91
+ }
92
+
93
+ export function parsePerformStep(value: unknown, index: number): SimPerformStep {
94
+ const path = `perform.steps[${index}]`
95
+ const step = recordValue(value, path)
96
+ const type = stringValue(step.type, `${path}.type`)
97
+ switch (type) {
98
+ case 'touchDown':
99
+ case 'touchMove':
100
+ case 'touchUp': {
101
+ const pointerId = optionalNumber(step, 'pointerId', path)
102
+ return {
103
+ type,
104
+ x: numberValue(step.x, `${path}.x`),
105
+ y: numberValue(step.y, `${path}.y`),
106
+ ...(pointerId === undefined ? {} : { pointerId }),
107
+ }
108
+ }
109
+ case 'touchCancel': {
110
+ const pointerId = optionalNumber(step, 'pointerId', path)
111
+ return { type, ...(pointerId === undefined ? {} : { pointerId }) }
112
+ }
113
+ case 'tap':
114
+ return {
115
+ type,
116
+ target: parseTapTarget(step.target),
117
+ x: numberValue(step.x, `${path}.x`),
118
+ y: numberValue(step.y, `${path}.y`),
119
+ }
120
+ case 'tapId':
121
+ return { type, id: stringValue(step.id, `${path}.id`) }
122
+ case 'tapText':
123
+ return { type, text: stringValue(step.text, `${path}.text`) }
124
+ case 'doubleTap': {
125
+ const gapMs = optionalNumber(step, 'gapMs', path)
126
+ return {
127
+ type,
128
+ x: numberValue(step.x, `${path}.x`),
129
+ y: numberValue(step.y, `${path}.y`),
130
+ ...(gapMs === undefined ? {} : { gapMs }),
131
+ }
132
+ }
133
+ case 'longPress': {
134
+ const durationMs = optionalNumber(step, 'durationMs', path)
135
+ if (
136
+ durationMs !== undefined &&
137
+ (!Number.isInteger(durationMs) ||
138
+ durationMs < 1 ||
139
+ durationMs > SIM_LONG_PRESS_MAX_MS)
140
+ ) {
141
+ validationFailure(
142
+ `${path}.durationMs`,
143
+ `must be an integer between 1 and ${SIM_LONG_PRESS_MAX_MS}`,
144
+ )
145
+ }
146
+ return {
147
+ type,
148
+ x: numberValue(step.x, `${path}.x`),
149
+ y: numberValue(step.y, `${path}.y`),
150
+ ...(durationMs === undefined ? {} : { durationMs }),
151
+ }
152
+ }
153
+ case 'drag': {
154
+ const steps = optionalNumber(step, 'steps', path)
155
+ const stepMs = optionalNumber(step, 'stepMs', path)
156
+ return {
157
+ type,
158
+ fromX: numberValue(step.fromX, `${path}.fromX`),
159
+ fromY: numberValue(step.fromY, `${path}.fromY`),
160
+ toX: numberValue(step.toX, `${path}.toX`),
161
+ toY: numberValue(step.toY, `${path}.toY`),
162
+ ...(steps === undefined ? {} : { steps }),
163
+ ...(stepMs === undefined ? {} : { stepMs }),
164
+ }
165
+ }
166
+ case 'pinch': {
167
+ const steps = optionalNumber(step, 'steps', path)
168
+ const stepMs = optionalNumber(step, 'stepMs', path)
169
+ return {
170
+ type,
171
+ fromX1: numberValue(step.fromX1, `${path}.fromX1`),
172
+ fromY1: numberValue(step.fromY1, `${path}.fromY1`),
173
+ fromX2: numberValue(step.fromX2, `${path}.fromX2`),
174
+ fromY2: numberValue(step.fromY2, `${path}.fromY2`),
175
+ toX1: numberValue(step.toX1, `${path}.toX1`),
176
+ toY1: numberValue(step.toY1, `${path}.toY1`),
177
+ toX2: numberValue(step.toX2, `${path}.toX2`),
178
+ toY2: numberValue(step.toY2, `${path}.toY2`),
179
+ ...(steps === undefined ? {} : { steps }),
180
+ ...(stepMs === undefined ? {} : { stepMs }),
181
+ }
182
+ }
183
+ case 'scroll': {
184
+ const x = optionalNumber(step, 'x', path)
185
+ const y = optionalNumber(step, 'y', path)
186
+ const animated = optionalBoolean(step, 'animated', path)
187
+ return {
188
+ type,
189
+ id: stringValue(step.id, `${path}.id`),
190
+ ...(x === undefined ? {} : { x }),
191
+ ...(y === undefined ? {} : { y }),
192
+ ...(animated === undefined ? {} : { animated }),
193
+ }
194
+ }
195
+ case 'type':
196
+ return { type, text: stringValue(step.text, `${path}.text`) }
197
+ case 'key':
198
+ return { type, key: stringValue(step.key, `${path}.key`) }
199
+ case 'dismissKeyboard':
200
+ return { type }
201
+ case 'setElementValue': {
202
+ const submit = optionalBoolean(step, 'submit', path)
203
+ return {
204
+ type,
205
+ id: stringValue(step.id, `${path}.id`),
206
+ text: stringValue(step.text, `${path}.text`),
207
+ ...(submit === undefined ? {} : { submit }),
208
+ }
209
+ }
210
+ case 'toggleKeyboard':
211
+ return { type }
212
+ case 'openUrl': {
213
+ const url = stringValue(step.url, `${path}.url`)
214
+ if (url.length === 0) validationFailure(`${path}.url`, 'must be non-empty')
215
+ return { type, url }
216
+ }
217
+ case 'keyDown':
218
+ case 'keyUp':
219
+ return { type, key: stringValue(step.key, `${path}.key`) }
220
+ case 'setOrientation': {
221
+ const orientation = stringValue(step.orientation, `${path}.orientation`)
222
+ if (orientation !== 'portrait' && orientation !== 'landscape') {
223
+ validationFailure(`${path}.orientation`, 'must be portrait or landscape')
224
+ }
225
+ return { type, orientation }
226
+ }
227
+ case 'deviceInfo':
228
+ return { type }
229
+ case 'buttonDown':
230
+ case 'buttonUp': {
231
+ const button =
232
+ step.button === undefined ? undefined : stringValue(step.button, `${path}.button`)
233
+ return { type, ...(button === undefined ? {} : { button }) }
234
+ }
235
+ case 'incrementElement':
236
+ case 'decrementElement':
237
+ return { type, id: stringValue(step.id, `${path}.id`) }
238
+ case 'discoverStoreKitConfig':
239
+ case 'clearStoreKitConfig':
240
+ return { type }
241
+ case 'wait':
242
+ return { type, ms: numberValue(step.ms, `${path}.ms`) }
243
+ case 'waitFor': {
244
+ const timeoutMs = semanticWaitTimeout(step, path)
245
+ const condition =
246
+ step.condition === undefined
247
+ ? undefined
248
+ : stringValue(step.condition, `${path}.condition`)
249
+ if (
250
+ condition !== undefined &&
251
+ condition !== 'present' &&
252
+ condition !== 'mounted' &&
253
+ condition !== 'absent'
254
+ ) {
255
+ return validationFailure(
256
+ `${path}.condition`,
257
+ 'must be present, mounted, or absent',
258
+ )
259
+ }
260
+ const selector =
261
+ step.testID === undefined
262
+ ? { id: stringValue(step.id, `${path}.id`) }
263
+ : { testID: stringValue(step.testID, `${path}.testID`) }
264
+ return {
265
+ type,
266
+ ...selector,
267
+ ...(condition === undefined ? {} : { condition }),
268
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
269
+ }
270
+ }
271
+ default:
272
+ return validationFailure(`${path}.type`, 'is not a supported perform step')
273
+ }
274
+ }
275
+
276
+ // an unclosed touchDown leaves a pointer stuck down, and the batch abort path
277
+ // would cancel it after the caller was told the batch failed. every batch is
278
+ // self-contained (pointers never survive it), so a touchDown without its
279
+ // touchUp or touchCancel in the same batch is rejected before anything runs.
280
+ export function findUnbalancedTouchDown(
281
+ steps: readonly SimPerformStep[],
282
+ ): { index: number; pointerId: number } | null {
283
+ const open = new Map<number, number>()
284
+ const pointerIdOf = (step: SimPerformStep): number | null => {
285
+ if (
286
+ step.type !== 'touchDown' &&
287
+ step.type !== 'touchMove' &&
288
+ step.type !== 'touchUp' &&
289
+ step.type !== 'touchCancel'
290
+ ) {
291
+ return null
292
+ }
293
+ const pointerId = step.pointerId ?? 999
294
+ return Number.isSafeInteger(pointerId) ? pointerId : null
295
+ }
296
+ for (const [index, step] of steps.entries()) {
297
+ if (step.type === 'touchDown') {
298
+ const pointerId = pointerIdOf(step) ?? 999
299
+ if (!open.has(pointerId)) open.set(pointerId, index)
300
+ } else if (step.type === 'touchUp' || step.type === 'touchCancel') {
301
+ const pointerId = pointerIdOf(step)
302
+ if (pointerId !== null) open.delete(pointerId)
303
+ }
304
+ }
305
+ const next = open.entries().next()
306
+ if (next.done) return null
307
+ return { index: next.value[1], pointerId: next.value[0] }
308
+ }
309
+
310
+ export function parseStateOptions(value: unknown): SimStateOptions {
311
+ const input = recordValue(value, 'state')
312
+ const route = optionalBoolean(input, 'route', 'state')
313
+ const errors = optionalBoolean(input, 'errors', 'state')
314
+ const logs = optionalBoolean(input, 'logs', 'state')
315
+ const depth = optionalNumber(input, 'depth', 'state')
316
+ const errorLimit = optionalNumber(input, 'errorLimit', 'state')
317
+ const logLimit = optionalNumber(input, 'logLimit', 'state')
318
+ const layers =
319
+ input.layers === undefined ? undefined : stringValue(input.layers, 'state.layers')
320
+ if (
321
+ layers !== undefined &&
322
+ layers !== 'full' &&
323
+ layers !== 'tenant' &&
324
+ layers !== 'shell'
325
+ ) {
326
+ return validationFailure('state.layers', 'must be full, tenant, or shell')
327
+ }
328
+ return {
329
+ screenshot: optionalBoolean(input, 'screenshot', 'state') ?? true,
330
+ tree: optionalBoolean(input, 'tree', 'state') ?? true,
331
+ ...(route === undefined ? {} : { route }),
332
+ ...(errors === undefined ? {} : { errors }),
333
+ ...(logs === undefined ? {} : { logs }),
334
+ ...(depth === undefined ? {} : { depth }),
335
+ ...(layers === undefined ? {} : { layers }),
336
+ ...(errorLimit === undefined ? {} : { errorLimit }),
337
+ ...(logLimit === undefined ? {} : { logLimit }),
338
+ }
339
+ }
340
+
341
+ function parseSemanticSelector(value: unknown, path: string): SimSemanticSelector {
342
+ const selector = recordValue(value, path)
343
+ if (selector.testID !== undefined) {
344
+ return { testID: stringValue(selector.testID, `${path}.testID`) }
345
+ }
346
+ if (selector.text !== undefined) {
347
+ return { text: stringValue(selector.text, `${path}.text`) }
348
+ }
349
+ if (selector.role !== undefined) {
350
+ return { role: stringValue(selector.role, `${path}.role`) }
351
+ }
352
+ if (selector.type !== undefined) {
353
+ return { type: stringValue(selector.type, `${path}.type`) }
354
+ }
355
+ if (selector.pressable !== undefined) {
356
+ if (booleanValue(selector.pressable, `${path}.pressable`) !== true) {
357
+ return validationFailure(`${path}.pressable`, 'must be true')
358
+ }
359
+ return { pressable: true }
360
+ }
361
+ if (selector.visible !== undefined) {
362
+ if (booleanValue(selector.visible, `${path}.visible`) !== true) {
363
+ return validationFailure(`${path}.visible`, 'must be true')
364
+ }
365
+ return { visible: true }
366
+ }
367
+ return validationFailure(
368
+ path,
369
+ 'must select testID, text, role, type, pressable, or visible',
370
+ )
371
+ }
372
+
373
+ export function parseSemanticQuery(value: unknown): SimSemanticQuery {
374
+ const query = recordValue(value, 'query')
375
+ const kind = stringValue(query.kind, 'query.kind')
376
+ if (kind === 'count') return { kind }
377
+ if (kind === 'tree') {
378
+ const depth = optionalNumber(query, 'depth', 'query')
379
+ return { kind, ...(depth === undefined ? {} : { depth }) }
380
+ }
381
+ if (kind === 'find') {
382
+ return { kind, selector: parseSemanticSelector(query.selector, 'query.selector') }
383
+ }
384
+ return validationFailure('query.kind', 'must be tree, find, or count')
385
+ }
386
+
387
+ function semanticResolveSelector(
388
+ value: unknown,
389
+ path: string,
390
+ ): SimSemanticResolveSelector {
391
+ const selector = recordValue(value, path)
392
+ if (selector.testID !== undefined) {
393
+ return { testID: stringValue(selector.testID, `${path}.testID`) }
394
+ }
395
+ if (selector.text !== undefined) {
396
+ return { text: stringValue(selector.text, `${path}.text`) }
397
+ }
398
+ return validationFailure(path, 'must select testID or text')
399
+ }
400
+
401
+ export function parseSemanticResolveSelector(value: unknown): SimSemanticResolveSelector {
402
+ return semanticResolveSelector(value, 'selector')
403
+ }
404
+
405
+ export function parseSemanticWaitOptions(value: unknown): SemanticWaitOptions {
406
+ const input = recordValue(value, 'waitFor')
407
+ const condition = recordValue(input.condition, 'waitFor.condition')
408
+ const type = stringValue(condition.type, 'waitFor.condition.type')
409
+ const timeoutMs = semanticWaitTimeout(input, 'waitFor')
410
+ if (type === 'ready') {
411
+ return {
412
+ condition: { type },
413
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
414
+ }
415
+ }
416
+ if (type !== 'present' && type !== 'mounted' && type !== 'absent') {
417
+ return validationFailure(
418
+ 'waitFor.condition.type',
419
+ 'must be ready, present, mounted, or absent',
420
+ )
421
+ }
422
+ return {
423
+ condition: {
424
+ type,
425
+ selector: semanticResolveSelector(condition.selector, 'waitFor.condition.selector'),
426
+ },
427
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
428
+ }
429
+ }
430
+
431
+ export function parseResetOptions(value: unknown): ResetOptions {
432
+ const input = recordValue(value, 'reset')
433
+ const strategy =
434
+ input.strategy === undefined ? 'data' : stringValue(input.strategy, 'reset.strategy')
435
+ if (strategy !== 'data' && strategy !== 'full') {
436
+ return validationFailure('reset.strategy', 'must be data or full')
437
+ }
438
+ // the shape check only; the simulator service re-validates the id against
439
+ // its own strict pattern and the caller's account before touching storage.
440
+ const from = input.from
441
+ if (
442
+ from !== undefined &&
443
+ (typeof from !== 'string' || from.length === 0 || from.length > 64)
444
+ ) {
445
+ return validationFailure('reset.from', 'must name a simulator')
446
+ }
447
+ return {
448
+ strategy,
449
+ relaunch: optionalBoolean(input, 'relaunch', 'reset') ?? true,
450
+ ...(from === undefined ? {} : { from }),
451
+ }
452
+ }
453
+
454
+ export function parseScreenshotOptions(value: unknown): SimScreenshotOptions {
455
+ const input = recordValue(value, 'screenshot')
456
+ const layers =
457
+ input.layers === undefined
458
+ ? undefined
459
+ : stringValue(input.layers, 'screenshot.layers')
460
+ if (
461
+ layers !== undefined &&
462
+ layers !== 'full' &&
463
+ layers !== 'tenant' &&
464
+ layers !== 'shell'
465
+ ) {
466
+ return validationFailure('screenshot.layers', 'must be full, tenant, or shell')
467
+ }
468
+ const crop =
469
+ input.crop === undefined ? undefined : recordValue(input.crop, 'screenshot.crop')
470
+ return {
471
+ ...(layers === undefined ? {} : { layers }),
472
+ ...(crop === undefined
473
+ ? {}
474
+ : {
475
+ crop: {
476
+ x: numberValue(crop.x, 'screenshot.crop.x'),
477
+ y: numberValue(crop.y, 'screenshot.crop.y'),
478
+ w: numberValue(crop.w, 'screenshot.crop.w'),
479
+ h: numberValue(crop.h, 'screenshot.crop.h'),
480
+ },
481
+ }),
482
+ }
483
+ }
484
+
485
+ export function parseTapTarget(value: unknown): SootSimReplayTarget | undefined {
486
+ if (value === undefined) return undefined
487
+ const input = recordValue(value, 'tap.target')
488
+ const target: SootSimReplayTarget = {}
489
+ if (input.nodeId !== undefined && input.nodeId !== null) {
490
+ const nodeId = numberValue(input.nodeId, 'tap.target.nodeId')
491
+ if (!Number.isSafeInteger(nodeId))
492
+ validationFailure('tap.target.nodeId', 'must be an integer')
493
+ target.nodeId = nodeId
494
+ }
495
+ for (const key of ['id', 'testID', 'text', 'type'] as const) {
496
+ if (input[key] !== undefined && input[key] !== null)
497
+ target[key] = stringValue(input[key], `tap.target.${key}`)
498
+ }
499
+ if (target.nodeId === undefined && !target.testID && !target.id)
500
+ validationFailure('tap.target', 'requires nodeId, testID or id')
501
+ return target
502
+ }