peachsim 0.1.1625 → 0.1.1640

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 (169) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +15 -2
  3. package/cli/app-config.ts +18 -6
  4. package/cli/app-fonts.ts +37 -17
  5. package/cli/app-stubs.ts +284 -20
  6. package/cli/bridge-flow-runner.ts +66 -45
  7. package/cli/cloud-client.ts +60 -40
  8. package/cli/command-registry.ts +1 -0
  9. package/cli/commands/control.ts +283 -49
  10. package/cli/commands/doctor.ts +30 -189
  11. package/cli/commands/flow.ts +79 -11
  12. package/cli/commands/inspect/core.ts +10 -5
  13. package/cli/commands/inspect/settling.ts +14 -2
  14. package/cli/commands/inspect/shared.ts +9 -4
  15. package/cli/commands/inspect/wait-ready.ts +12 -0
  16. package/cli/commands/inspect.ts +91 -50
  17. package/cli/commands/install-desktop.ts +1 -0
  18. package/cli/commands/maestro-compare.ts +97 -38
  19. package/cli/commands/maestro.ts +9 -1
  20. package/cli/commands/preview.ts +1 -1
  21. package/cli/commands/record.ts +118 -48
  22. package/cli/commands/screenshot.ts +23 -1
  23. package/cli/commands/serve.ts +7 -2
  24. package/cli/commands/setup.ts +19 -42
  25. package/cli/commands/swift.ts +14 -3
  26. package/cli/commands/upload.ts +93 -48
  27. package/cli/desktop-companion.ts +102 -17
  28. package/cli/drivers/desktop.ts +14 -1
  29. package/cli/drivers/playwright-sim-host.ts +103 -45
  30. package/cli/drivers/playwright.ts +9 -8
  31. package/cli/drivers/types.ts +5 -0
  32. package/cli/maestro-accessibility.ts +1 -1
  33. package/cli/maestro-compare.ts +84 -12
  34. package/cli/main.ts +3 -2
  35. package/cli/outbound-endpoints.ts +20 -6
  36. package/cli/pause-aware-timeout.ts +106 -0
  37. package/cli/scene-video.ts +89 -0
  38. package/cli/ws-bridge.ts +37 -19
  39. package/detox/attempt-events.cjs +66 -5
  40. package/detox/attempt-events.d.cts +7 -0
  41. package/detox/expectations.ts +7 -3
  42. package/detox/index.ts +269 -170
  43. package/detox/motion-change.cjs +206 -21
  44. package/detox/motion-change.d.cts +39 -2
  45. package/detox/navigation-lifecycle.ts +35 -3
  46. package/dist-lib/agent-daemon-client.cjs +1 -1
  47. package/dist-lib/agent-events.cjs +1 -1
  48. package/dist-lib/attached-projects.cjs +1 -1
  49. package/dist-lib/auth/shared-session.cjs +1 -1
  50. package/dist-lib/backend-origin.cjs +8 -7
  51. package/dist-lib/beta.cjs +1 -1
  52. package/dist-lib/beta.mjs +1 -1
  53. package/dist-lib/bridge-constants.cjs +13 -1
  54. package/dist-lib/bridge-contract-input.cjs +1 -1
  55. package/dist-lib/bridge-contract-input.mjs +1 -1
  56. package/dist-lib/bridge-contract.cjs +14 -8
  57. package/dist-lib/bridge-contract.mjs +13 -7
  58. package/dist-lib/bundle-channel.cjs +11887 -7900
  59. package/dist-lib/capture-contract.cjs +1 -1
  60. package/dist-lib/capture-contract.mjs +1 -1
  61. package/dist-lib/cloud-contract.cjs +1 -1
  62. package/dist-lib/cloud-contract.mjs +1 -1
  63. package/dist-lib/cloud-test-args.cjs +1 -1
  64. package/dist-lib/cloud-test-args.mjs +1 -1
  65. package/dist-lib/cloud.cjs +1 -1
  66. package/dist-lib/cloud.mjs +1 -1
  67. package/dist-lib/config.cjs +6 -3
  68. package/dist-lib/config.mjs +6 -3
  69. package/dist-lib/detox/index.cjs +1379 -725
  70. package/dist-lib/dev-bundle-resolution.cjs +14 -2
  71. package/dist-lib/expo-swift-bundle.cjs +9341 -5462
  72. package/dist-lib/expo-swift-metadata.cjs +11 -7
  73. package/dist-lib/expo-swift-metadata.mjs +11 -7
  74. package/dist-lib/home-paths.cjs +32 -3
  75. package/dist-lib/host/bridge-host.cjs +50204 -7258
  76. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  77. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  78. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  79. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  80. package/dist-lib/host/websocket-proxy.cjs +1 -1
  81. package/dist-lib/index.cjs +3541 -448
  82. package/dist-lib/jump-to-source-babel.cjs +1 -1
  83. package/dist-lib/jump-to-source-native.cjs +1 -1
  84. package/dist-lib/kotlin-host.cjs +1 -1
  85. package/dist-lib/menu.cjs +1 -1
  86. package/dist-lib/menu.mjs +1 -1
  87. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  88. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  89. package/dist-lib/metro-production-bundle.cjs +1 -1
  90. package/dist-lib/metro-production-bundle.mjs +1 -1
  91. package/dist-lib/metro.cjs +2931 -104
  92. package/dist-lib/native-app-identity-project.cjs +289 -49
  93. package/dist-lib/native-app-identity-project.mjs +289 -49
  94. package/dist-lib/native-app-identity.cjs +77 -26
  95. package/dist-lib/native-app-identity.mjs +76 -26
  96. package/dist-lib/native-firebase-metadata.cjs +1 -1
  97. package/dist-lib/native-firebase-metadata.mjs +1 -1
  98. package/dist-lib/plist.cjs +1 -1
  99. package/dist-lib/plist.mjs +1 -1
  100. package/dist-lib/profiles.cjs +1 -1
  101. package/dist-lib/public-brand.cjs +1 -1
  102. package/dist-lib/react-native-host-modules.cjs +6 -2
  103. package/dist-lib/react-native-host-modules.mjs +6 -2
  104. package/dist-lib/scripts/dev-server-scanner.cjs +22 -8
  105. package/dist-lib/sdk.cjs +27 -11
  106. package/dist-lib/sdk.mjs +27 -10
  107. package/dist-lib/skills.cjs +72201 -26208
  108. package/dist-lib/swift-compile.cjs +1007 -140
  109. package/dist-lib/swift-compile.mjs +1049 -174
  110. package/dist-lib/swift-package.cjs +12 -3
  111. package/dist-lib/swift-package.mjs +12 -3
  112. package/dist-lib/swift-submit.cjs +13 -5
  113. package/dist-lib/swift-submit.mjs +11 -5
  114. package/dist-lib/swift-toolchain.cjs +423 -33
  115. package/dist-lib/swift.cjs +1 -1
  116. package/dist-lib/vite-swift.cjs +8628 -5268
  117. package/dist-lib/vite.cjs +2859 -12
  118. package/dist-lib/worklet-executable.cjs +14 -2
  119. package/dist-lib/worklet-executable.mjs +14 -2
  120. package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
  121. package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
  122. package/package.json +1 -1
  123. package/scripts/dev-server-scanner.ts +2 -1
  124. package/skills/peach-perf/SKILL.md +11 -5
  125. package/skills/peach-setup/SKILL.md +16 -19
  126. package/src/backend-origin.ts +13 -6
  127. package/src/bridge-constants.ts +11 -0
  128. package/src/bridge-contract.ts +22 -34
  129. package/src/config.ts +15 -0
  130. package/src/desktop-host-delivery.ts +5 -1
  131. package/src/dev-bundle-resolution.ts +2 -1
  132. package/src/expo-manifest-body.ts +13 -0
  133. package/src/expo-swift-host.ts +196 -49
  134. package/src/expo-swift-ready.ts +78 -0
  135. package/src/expoKotlinContract.ts +4 -7
  136. package/src/expoSwiftContract.ts +17 -6
  137. package/src/home-paths.ts +48 -2
  138. package/src/host/bridge-host.ts +5 -2
  139. package/src/host/staged-host-files.ts +9 -0
  140. package/src/native-app-identity-project.ts +27 -6
  141. package/src/native-app-identity.ts +91 -4
  142. package/src/native-firebase-config.ts +209 -13
  143. package/src/nativeAppIconContract.ts +1 -4
  144. package/src/output-lock-stdio.ts +36 -0
  145. package/src/process-group.ts +25 -2
  146. package/src/react-native-bridge-modules.ts +431 -0
  147. package/src/react-native-host-modules.ts +8 -1
  148. package/src/settings-bundle.ts +1 -4
  149. package/src/swift-binary-modules.ts +2 -2
  150. package/src/swift-compile.ts +918 -109
  151. package/src/swift-compiler-digest.ts +14 -0
  152. package/src/swift-distribution.lock.json +24 -12
  153. package/src/swift-distribution.ts +103 -4
  154. package/src/swift-host.ts +180 -55
  155. package/src/swift-icu.ts +27 -13
  156. package/src/swift-objc-compile.ts +355 -0
  157. package/src/swift-objc-sdk.ts +821 -27
  158. package/src/swift-objc-standin.ts +148 -14
  159. package/src/swift-package-manifest.ts +60 -7
  160. package/src/swift-package.ts +15 -1
  161. package/src/swift-packages.ts +154 -81
  162. package/src/swift-standin-emit.ts +96 -15
  163. package/src/swift-standins.ts +530 -116
  164. package/src/swift-submit.ts +13 -5
  165. package/src/swift-toolchain.ts +500 -42
  166. package/src/vite-plugin.ts +12 -1
  167. package/src/worker-system-chunks.ts +11 -12
  168. package/src/worklet-executable.ts +19 -1
  169. package/src/xcode-project.ts +270 -30
@@ -2175,48 +2175,58 @@ export class SootSimBridgeFlowRunner {
2175
2175
  await sleep(120)
2176
2176
  }
2177
2177
 
2178
+ // maestro's android driver sends Back. its ios driver never presses a key:
2179
+ // while the keyboard shows it swipes from the screen center up 3%, then
2180
+ // left 3%, and the app's own handling of those touches dismisses it, or
2181
+ // presses whatever sits under the center
2178
2182
  async hideKeyboard() {
2179
- // CLI flows need "dismiss keyboard" to mean "get the focused TextInput
2180
- // out of the way before the next tap", not just "ask shell chrome to
2181
- // animate down eventually". If the TextInput stays focused, the next tap
2182
- // can still be swallowed by keyboard-dismiss policy on ScrollViews.
2183
- try {
2184
- await this.callTest<boolean>('blurFocusedTextInput')
2185
- } catch {
2186
- // fall through to the shell-level dismiss below; older bridges may not
2187
- // expose the text-input helper yet.
2183
+ if (this.js.maestro.platform.toLowerCase() === 'android') {
2184
+ await this.back()
2185
+ await sleep(300)
2186
+ await this.waitForAppToSettle()
2187
+ return
2188
2188
  }
2189
+ if (!(await this.isKeyboardVisible())) return
2190
+ const screen = await this.screenBounds()
2191
+ const center = parsePoint('50%,50%', screen)
2192
+ await this.xctestSwipe(center, parsePoint('50%,47%', screen), 50)
2193
+ if (!(await this.isKeyboardVisible())) return
2194
+ await this.xctestSwipe(center, parsePoint('47%,50%', screen), 50)
2195
+ await this.waitForAppToSettle()
2196
+ }
2189
2197
 
2190
- await this.bridge.send({
2191
- type: 'keyboard',
2192
- simId: this.opts.simId,
2193
- action: 'dismiss',
2194
- })
2198
+ // the xctest runner's swipe: press, move to the end over its 100ms tap
2199
+ // duration, rest there for the requested duration, then lift, so the
2200
+ // finger leaves at rest rather than flinging
2201
+ private async xctestSwipe(
2202
+ from: { x: number; y: number },
2203
+ to: { x: number; y: number },
2204
+ durationMs: number,
2205
+ ) {
2206
+ const moves = 6
2207
+ const steps: PerformStep[] = [{ type: 'touchDown', x: from.x, y: from.y }]
2208
+ for (let i = 1; i <= moves; i++) {
2209
+ steps.push({ type: 'wait', ms: 100 / moves })
2210
+ steps.push({
2211
+ type: 'touchMove',
2212
+ x: from.x + ((to.x - from.x) * i) / moves,
2213
+ y: from.y + ((to.y - from.y) * i) / moves,
2214
+ })
2215
+ }
2216
+ steps.push({ type: 'wait', ms: durationMs })
2217
+ steps.push({ type: 'touchUp', x: to.x, y: to.y })
2218
+ const result = await this.perform(steps)
2219
+ if (!result?.ok) throw new Error(`swipe failed: ${result?.error ?? 'swipe missed'}`)
2220
+ await this.waitForAppToSettle()
2221
+ }
2195
2222
 
2196
- const deadline = Date.now() + 3000
2197
- do {
2198
- // the host test bridge proxies the tenant, so isTextInputFocused
2199
- // resolves a promise; reading it unawaited is always truthy.
2200
- const state = await this.evaluate<{
2201
- visible: boolean
2202
- focused: boolean
2203
- }>(`(async () => {
2204
- const keyboard =
2205
- window.__sootsimKeyboard ??
2206
- window.SootSim?.bridges?.keyboard ??
2207
- null
2208
- const test = window.__sootsimTest
2209
- return {
2210
- visible: !!keyboard?.isVisible?.(),
2211
- focused: !!(await test?.isTextInputFocused?.()),
2212
- }
2213
- })()`)
2214
- if (!state?.visible && !state?.focused) {
2215
- await sleep(80)
2216
- return
2217
- }
2218
- await sleep(80)
2219
- } while (Date.now() < deadline)
2223
+ private async isKeyboardVisible(): Promise<boolean> {
2224
+ const visible = await this.evaluate<boolean>(`(() => {
2225
+ const keyboard =
2226
+ window.__sootsimKeyboard ?? window.SootSim?.bridges?.keyboard ?? null
2227
+ return !!keyboard?.isVisible?.()
2228
+ })()`)
2229
+ return visible === true
2220
2230
  }
2221
2231
 
2222
2232
  async eraseText(count: number) {
@@ -2765,8 +2775,15 @@ export class SootSimBridgeFlowRunner {
2765
2775
  : { x: centerX, y: screen.height * 0.1 }
2766
2776
  const element = await this.findElement(target)
2767
2777
  if (element) {
2768
- const { x, y } = element.absolutePosition
2769
- const { width, height } = element.layout
2778
+ // maestro computes visibility from integer edges, as it does selection.
2779
+ const { x, y, width, height } = accessibilityBounds({
2780
+ frame: {
2781
+ x: element.absolutePosition.x,
2782
+ y: element.absolutePosition.y,
2783
+ width: element.layout.width,
2784
+ height: element.layout.height,
2785
+ },
2786
+ })
2770
2787
  const shownX = Math.max(0, Math.min(x + width, screen.width) - Math.max(x, 0))
2771
2788
  const shownY = Math.max(0, Math.min(y + height, screen.height) - Math.max(y, 0))
2772
2789
  const share = width * height > 0 ? (shownX * shownY) / (width * height) : 0
@@ -2892,6 +2909,12 @@ export class SootSimBridgeFlowRunner {
2892
2909
  const wantsClear = !!(opts && typeof opts === 'object' && opts.clearState)
2893
2910
  const wantsResetRuntime = readBooleanKey(opts, 'resetRuntime')
2894
2911
  const wantsForeground = isObjectRecord(opts) && opts.stopApp === false
2912
+ // maestro's launchApp clears the keychain, then app state, then sets
2913
+ // permissions and launches
2914
+ if (readBooleanKey(opts, 'clearKeychain')) {
2915
+ if (!this.firstLaunchDone && !this.appStopped) await this.waitForTree(120000)
2916
+ await this.clearKeychain()
2917
+ }
2895
2918
  await this.setLaunchPermissions(isObjectRecord(opts) ? opts.permissions : undefined)
2896
2919
 
2897
2920
  if (!this.firstLaunchDone) {
@@ -2982,9 +3005,7 @@ export class SootSimBridgeFlowRunner {
2982
3005
 
2983
3006
  private async setLaunchPermissions(permissions: unknown) {
2984
3007
  const statuses = maestroLaunchPermissions(permissions)
2985
- const state = await getShellState(this.bridge)
2986
- const appId = readStringKey(state, 'activeApp') || readRecentShellAppId(state)
2987
- if (!appId) throw new Error('launchApp: no simulator app to set permissions for')
3008
+ const appId = await this.callTest<string>('permissionGuestAppId')
2988
3009
  for (const [kind, status] of statuses) {
2989
3010
  await this.callTest('permissionSetAppPermission', appId, kind, {
2990
3011
  status,
@@ -3082,11 +3103,11 @@ export class SootSimBridgeFlowRunner {
3082
3103
  }
3083
3104
 
3084
3105
  async startProfile() {
3085
- const result = await this.evaluate<any>(`(() => {
3106
+ const result = await this.evaluate<any>(`(async () => {
3086
3107
  if (!window.__sootsimShellPerf) {
3087
3108
  return { error: "shell frame profile unavailable (__sootsimShellPerf missing on the page)" }
3088
3109
  }
3089
- window.__sootsimShellPerf.start()
3110
+ await window.__sootsimShellPerf.start()
3090
3111
  return { started: true }
3091
3112
  })()`)
3092
3113
  if (result?.error) {
@@ -27,6 +27,10 @@ import {
27
27
  import { rewriteGuestImportMeta } from '../../peach-engine/src/guest-import-meta.ts'
28
28
  import { loadMetroFingerprintRegistry } from '../../peach-engine/src/metro-fingerprint-registry-client.ts'
29
29
  import { inferMetroModuleIdentity } from '../../peach-engine/src/metro-fingerprint.ts'
30
+ import {
31
+ isNativeJavaScriptBundle,
32
+ isRolldownBundle,
33
+ } from '../../peach-engine/src/preview/bundle-layout'
30
34
  import { isLoopbackHost } from '../src/backend-origin.ts'
31
35
  import {
32
36
  isRnxCloudCommandType,
@@ -172,7 +176,7 @@ function resolveCloudOrigin(value?: string): string {
172
176
  function readImmutableBundleSource(bundlePath: string, command: string): string {
173
177
  const filepath = resolve(bundlePath)
174
178
  if (extname(filepath) !== '.js') {
175
- throw new Error(`${command} requires one local Metro .js bundle`)
179
+ throw new Error(`${command} requires one local .js bundle`)
176
180
  }
177
181
  const descriptor = openSync(filepath, 'r')
178
182
  try {
@@ -200,9 +204,6 @@ function readImmutableBundleSource(bundlePath: string, command: string): string
200
204
  } catch {
201
205
  throw new Error(`${command} bundle must be valid UTF-8 JavaScript`)
202
206
  }
203
- if (source.includes('\0') || !source.includes('__d(') || !source.includes('__r(')) {
204
- throw new Error(`${command} requires a Metro JavaScript bundle`)
205
- }
206
207
  return source
207
208
  } finally {
208
209
  closeSync(descriptor)
@@ -231,9 +232,6 @@ async function fetchBundleSource(bundleUrl: string, command: string): Promise<st
231
232
  if (text.length === 0) {
232
233
  throw new Error(`${command} bundle is empty`)
233
234
  }
234
- if (text.includes('\0') || !text.includes('__d(') || !text.includes('__r(')) {
235
- throw new Error(`${command} requires a Metro JavaScript bundle`)
236
- }
237
235
  return text
238
236
  }
239
237
 
@@ -260,6 +258,9 @@ async function loadBundleSource(bundleInput: string, command: string): Promise<s
260
258
  const source = isRemoteBundleInput(bundleInput)
261
259
  ? await fetchBundleSource(bundleInput, command)
262
260
  : readImmutableBundleSource(bundleInput, command)
261
+ if (!isNativeJavaScriptBundle(source)) {
262
+ throw new Error(`${command} requires a supported React Native JavaScript bundle`)
263
+ }
263
264
  // validate the actual runtime script form without executing guest code or
264
265
  // allocating a host.
265
266
  try {
@@ -728,25 +729,44 @@ export async function produceCloudArtifact(
728
729
  maxBytes: number = MAX_RNX_CLOUD_BUNDLE_BYTES,
729
730
  assetsDirectory?: string,
730
731
  ): Promise<CloudArtifactProduction> {
731
- return produceArtifact(
732
+ const prepared = await embedBundleAssets(
732
733
  bundlePath,
733
- maxBytes,
734
734
  REMOTE_COMMAND,
735
735
  assetsDirectory ? 'require-assets' : 'allow-placeholders',
736
736
  assetsDirectory,
737
737
  )
738
+ return produceArtifact(prepared, maxBytes, REMOTE_COMMAND)
738
739
  }
739
740
 
740
741
  /** prepares a standalone preview whose bytes must represent every Metro asset. */
741
- export async function producePreviewArtifact(
742
- bundlePath: string,
743
- ): Promise<CloudArtifactProduction> {
744
- return produceArtifact(
745
- bundlePath,
746
- Number.POSITIVE_INFINITY,
747
- PREVIEW_COMMAND,
748
- 'require-assets',
749
- )
742
+ export async function producePreviewArtifact(bundlePath: string) {
743
+ const prepared = await embedBundleAssets(bundlePath, PREVIEW_COMMAND, 'require-assets')
744
+ if (!isRolldownBundle(prepared.source)) {
745
+ return {
746
+ ...(await produceArtifact(prepared, Number.POSITIVE_INFINITY, PREVIEW_COMMAND)),
747
+ minified: true,
748
+ }
749
+ }
750
+ const bytes = Buffer.from(prepared.code, 'utf8')
751
+ const sha256 = createHash('sha256').update(bytes).digest('hex')
752
+ return {
753
+ bytes,
754
+ sha256,
755
+ minified: false,
756
+ receipt: {
757
+ source: {
758
+ sha256: createHash('sha256').update(prepared.source).digest('hex'),
759
+ bytes: Buffer.byteLength(prepared.source, 'utf8'),
760
+ },
761
+ artifact: { sha256, bytes: bytes.byteLength },
762
+ },
763
+ warnings:
764
+ prepared.search.unconfirmed && prepared.dest
765
+ ? [
766
+ `peach preview could not authenticate asset hashes in ${prepared.dest.directory}; confirm these are the assets this bundle was built against.`,
767
+ ]
768
+ : [],
769
+ }
750
770
  }
751
771
 
752
772
  export interface CloudBoxArtifactProduction {
@@ -890,18 +910,10 @@ async function embedBundleAssets(
890
910
  }
891
911
 
892
912
  async function produceArtifact(
893
- bundlePath: string,
913
+ prepared: Awaited<ReturnType<typeof embedBundleAssets>>,
894
914
  maxBytes: number,
895
915
  command: string,
896
- assetPolicy: 'allow-placeholders' | 'require-assets',
897
- assetsDirectory?: string,
898
916
  ): Promise<CloudArtifactProduction> {
899
- const prepared = await embedBundleAssets(
900
- bundlePath,
901
- command,
902
- assetPolicy,
903
- assetsDirectory,
904
- )
905
917
  const artifact = await buildRnxCloudArtifact(prepared.code, {
906
918
  inferModuleIdentity: (source) => inferCloudModuleIdentity(source, command),
907
919
  })
@@ -1453,12 +1465,17 @@ function cloudCommand(command: {
1453
1465
  class CloudBridge implements WsBridge {
1454
1466
  readonly plane = 'cloud' as const
1455
1467
 
1456
- constructor(private readonly session: CloudSimSession) {}
1468
+ constructor(
1469
+ private readonly session: CloudSimSession,
1470
+ private readonly explicitTimeoutMs?: number,
1471
+ ) {}
1457
1472
 
1458
1473
  async send(
1459
1474
  command: { type: string; [key: string]: unknown },
1460
- options?: { timeoutMs?: number },
1475
+ options?: { timeoutMs?: number; defaultTimeoutMs?: number },
1461
1476
  ): Promise<unknown> {
1477
+ const timeoutMs =
1478
+ options?.timeoutMs ?? this.explicitTimeoutMs ?? options?.defaultTimeoutMs
1462
1479
  const body: CloudCommandEnvelope = {
1463
1480
  id: randomUUID(),
1464
1481
  claimId: this.session.claimId,
@@ -1475,9 +1492,7 @@ class CloudBridge implements WsBridge {
1475
1492
  'content-type': 'application/json',
1476
1493
  },
1477
1494
  body: JSON.stringify(body),
1478
- ...(options?.timeoutMs
1479
- ? { signal: AbortSignal.timeout(options.timeoutMs) }
1480
- : {}),
1495
+ ...(timeoutMs ? { signal: AbortSignal.timeout(timeoutMs) } : {}),
1481
1496
  },
1482
1497
  )
1483
1498
  } catch (error) {
@@ -1538,12 +1553,17 @@ class CloudBridge implements WsBridge {
1538
1553
  class CloudBoxBridge implements WsBridge {
1539
1554
  readonly plane = 'cloud' as const
1540
1555
 
1541
- constructor(private readonly session: CloudBoxSession) {}
1556
+ constructor(
1557
+ private readonly session: CloudBoxSession,
1558
+ private readonly explicitTimeoutMs?: number,
1559
+ ) {}
1542
1560
 
1543
1561
  async send(
1544
1562
  command: { type: string; [key: string]: unknown },
1545
- options?: { timeoutMs?: number },
1563
+ options?: { timeoutMs?: number; defaultTimeoutMs?: number },
1546
1564
  ): Promise<unknown> {
1565
+ const timeoutMs =
1566
+ options?.timeoutMs ?? this.explicitTimeoutMs ?? options?.defaultTimeoutMs
1547
1567
  const id = randomUUID()
1548
1568
  // the wire id is this request's, so the command carries none. `simId` names
1549
1569
  // a local sim among several and a box has exactly one.
@@ -1562,9 +1582,7 @@ class CloudBoxBridge implements WsBridge {
1562
1582
  'content-type': 'application/json',
1563
1583
  },
1564
1584
  body: JSON.stringify({ id, command: payload }),
1565
- ...(options?.timeoutMs
1566
- ? { signal: AbortSignal.timeout(options.timeoutMs) }
1567
- : {}),
1585
+ ...(timeoutMs ? { signal: AbortSignal.timeout(timeoutMs) } : {}),
1568
1586
  },
1569
1587
  )
1570
1588
  } catch (error) {
@@ -1619,13 +1637,15 @@ class CloudBoxBridge implements WsBridge {
1619
1637
  }
1620
1638
 
1621
1639
  export function createCloudBridgeForParsed(
1622
- parsed: Pick<ParsedBridgeCliArgs, 'simId' | 'simIdSource'>,
1640
+ parsed: Pick<ParsedBridgeCliArgs, 'simId' | 'simIdSource'> &
1641
+ Partial<Pick<ParsedBridgeCliArgs, 'explicitTimeout' | 'commandTimeoutMs'>>,
1623
1642
  ): WsBridge | null {
1624
1643
  const session = resolveCloudSessionForParsed(parsed)
1625
1644
  if (!session) return null
1645
+ const timeoutMs = parsed.explicitTimeout ? parsed.commandTimeoutMs : undefined
1626
1646
  return session.service === 'box'
1627
- ? new CloudBoxBridge(session)
1628
- : new CloudBridge(session)
1647
+ ? new CloudBoxBridge(session, timeoutMs)
1648
+ : new CloudBridge(session, timeoutMs)
1629
1649
  }
1630
1650
 
1631
1651
  // deletes the box, which ends the simulator it holds. a cloud session box is
@@ -128,6 +128,7 @@ const VALUE_FLAGS = new Set([
128
128
  '--timeout',
129
129
  '--device',
130
130
  '--posture',
131
+ '--render-resolution',
131
132
  '--os-version',
132
133
  '--ios-target',
133
134
  '--output',