peachsim 0.1.1619 → 0.1.1620

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 (85) hide show
  1. package/cli/bridge-flow-runner.ts +39 -63
  2. package/cli/cloud-client.ts +26 -4
  3. package/cli/commands/platform.ts +29 -7
  4. package/cli/commands/record.ts +20 -15
  5. package/cli/drivers/playwright-sim-host.ts +12 -1
  6. package/cli/outbound-endpoints.ts +5 -3
  7. package/dist-lib/agent-daemon-client.cjs +1 -1
  8. package/dist-lib/agent-events.cjs +1 -1
  9. package/dist-lib/attached-projects.cjs +1 -1
  10. package/dist-lib/auth/shared-session.cjs +1 -1
  11. package/dist-lib/backend-origin.cjs +1 -1
  12. package/dist-lib/beta.cjs +1 -1
  13. package/dist-lib/beta.mjs +1 -1
  14. package/dist-lib/bridge-constants.cjs +1 -1
  15. package/dist-lib/bridge-contract-input.cjs +1 -1
  16. package/dist-lib/bridge-contract-input.mjs +1 -1
  17. package/dist-lib/bridge-contract.cjs +1 -1
  18. package/dist-lib/bridge-contract.mjs +1 -1
  19. package/dist-lib/bundle-channel.cjs +10 -5
  20. package/dist-lib/capture-contract.cjs +1 -1
  21. package/dist-lib/capture-contract.mjs +1 -1
  22. package/dist-lib/cloud-contract.cjs +1 -1
  23. package/dist-lib/cloud-contract.mjs +1 -1
  24. package/dist-lib/cloud-test-args.cjs +1 -1
  25. package/dist-lib/cloud-test-args.mjs +1 -1
  26. package/dist-lib/cloud.cjs +2 -2
  27. package/dist-lib/cloud.mjs +2 -2
  28. package/dist-lib/config.cjs +1 -1
  29. package/dist-lib/config.mjs +1 -1
  30. package/dist-lib/detox/index.cjs +1 -1
  31. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  32. package/dist-lib/expo-swift-bundle.cjs +10 -5
  33. package/dist-lib/expo-swift-metadata.cjs +1 -1
  34. package/dist-lib/expo-swift-metadata.mjs +1 -1
  35. package/dist-lib/home-paths.cjs +1 -1
  36. package/dist-lib/host/bridge-host.cjs +10 -5
  37. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  38. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  39. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  40. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  41. package/dist-lib/host/websocket-proxy.cjs +1 -1
  42. package/dist-lib/index.cjs +8 -1
  43. package/dist-lib/jump-to-source-babel.cjs +1 -1
  44. package/dist-lib/jump-to-source-native.cjs +1 -1
  45. package/dist-lib/kotlin-host.cjs +1 -1
  46. package/dist-lib/menu.cjs +1 -1
  47. package/dist-lib/menu.mjs +1 -1
  48. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  49. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  50. package/dist-lib/metro-production-bundle.cjs +1 -1
  51. package/dist-lib/metro-production-bundle.mjs +1 -1
  52. package/dist-lib/metro.cjs +8 -1
  53. package/dist-lib/native-app-identity-project.cjs +3 -1
  54. package/dist-lib/native-app-identity-project.mjs +3 -1
  55. package/dist-lib/native-app-identity.cjs +9 -1
  56. package/dist-lib/native-app-identity.mjs +9 -1
  57. package/dist-lib/native-firebase-metadata.cjs +1 -1
  58. package/dist-lib/native-firebase-metadata.mjs +1 -1
  59. package/dist-lib/plist.cjs +1 -1
  60. package/dist-lib/plist.mjs +1 -1
  61. package/dist-lib/profiles.cjs +1 -1
  62. package/dist-lib/public-brand.cjs +1 -1
  63. package/dist-lib/react-native-host-modules.cjs +1 -1
  64. package/dist-lib/react-native-host-modules.mjs +1 -1
  65. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  66. package/dist-lib/sdk.cjs +1 -1
  67. package/dist-lib/sdk.mjs +1 -1
  68. package/dist-lib/skills.cjs +85 -49
  69. package/dist-lib/swift-compile.cjs +10 -5
  70. package/dist-lib/swift-compile.mjs +10 -5
  71. package/dist-lib/swift-package.cjs +1 -1
  72. package/dist-lib/swift-package.mjs +1 -1
  73. package/dist-lib/swift-submit.cjs +1 -1
  74. package/dist-lib/swift-submit.mjs +1 -1
  75. package/dist-lib/swift-toolchain.cjs +1 -1
  76. package/dist-lib/swift.cjs +1 -1
  77. package/dist-lib/vite-swift.cjs +10 -5
  78. package/dist-lib/vite.cjs +1 -1
  79. package/dist-lib/worklet-executable.cjs +1 -1
  80. package/dist-lib/worklet-executable.mjs +1 -1
  81. package/package.json +1 -1
  82. package/src/cloud.ts +2 -2
  83. package/src/native-app-identity-project.ts +2 -0
  84. package/src/native-app-identity.ts +17 -0
  85. package/src/swift-compile.ts +10 -4
@@ -45,6 +45,23 @@ const DEFAULT_TIMEOUT = 10000
45
45
  const ASSERTION_LOOKUP_MS = 17_000
46
46
  const FLOW_TIMEOUT_SCALE = flowTimeoutScale()
47
47
  const scaledTimeout = (ms: number) => Math.round(ms * FLOW_TIMEOUT_SCALE)
48
+
49
+ // the ceiling backing a bounded step must never fire before the step's own
50
+ // deadline. waitFor and extendedWaitUntil scale their declared timeout by
51
+ // SOOTSIM_FLOW_TIMEOUT_SCALE, so the ceiling scales it too.
52
+ function stepCeilingMs(step: MaestroStep): number {
53
+ const waitForTimeout =
54
+ typeof step.waitFor?.timeout === 'number' ? step.waitFor.timeout : 0
55
+ const extendedWaitTimeout =
56
+ typeof step.extendedWaitUntil?.timeout === 'number'
57
+ ? step.extendedWaitUntil.timeout
58
+ : 0
59
+ return Math.max(
60
+ 60_000,
61
+ scaledTimeout(waitForTimeout) + 15_000,
62
+ scaledTimeout(extendedWaitTimeout) + 15_000,
63
+ )
64
+ }
48
65
  type FlowScreenshotPathMode = 'dir' | 'flow'
49
66
  type ReplayTapTarget = SootSimReplayTarget
50
67
 
@@ -3322,6 +3339,10 @@ export class SootSimBridgeFlowRunner {
3322
3339
  }
3323
3340
  }
3324
3341
 
3342
+ // containers run each child through runStep, so every child keeps its own
3343
+ // deadline and failure. the container has no clock of its own: a parent
3344
+ // clock fails the parent while its children are still making progress.
3345
+ const isContainer = !!(step.runFlow || step.repeat || step.retry)
3325
3346
  try {
3326
3347
  // containers defer to their executed children: a nested launch must finish
3327
3348
  // resetting the guest before the first playable frame is recorded.
@@ -3329,9 +3350,7 @@ export class SootSimBridgeFlowRunner {
3329
3350
  this.recordingEnabled &&
3330
3351
  this.recordingStartedAtMs == null &&
3331
3352
  !step.launchApp &&
3332
- !step.runFlow &&
3333
- !step.repeat &&
3334
- !step.retry
3353
+ !isContainer
3335
3354
  ) {
3336
3355
  await this.ensureRecordingStarted()
3337
3356
  }
@@ -3341,7 +3360,8 @@ export class SootSimBridgeFlowRunner {
3341
3360
  startedAtMs = Date.now()
3342
3361
  // simple commands have a wall-clock watchdog. bounded operations keep
3343
3362
  // their own lookup, settle and bridge deadlines, so the watchdog cannot
3344
- // report a completed step while its action continues in the background.
3363
+ // report a completed step while its action continues in the background;
3364
+ // the step ceiling only backs those deadlines up.
3345
3365
  const hasBoundedWait = !!(
3346
3366
  // taps combine bounded lookup, settling and delivery. a wall-clock
3347
3367
  // watchdog can finish the step while that action is still running.
@@ -3353,14 +3373,8 @@ export class SootSimBridgeFlowRunner {
3353
3373
  step.extendedWaitUntil ||
3354
3374
  step.waitFor ||
3355
3375
  step.scrollUntilVisible ||
3356
- step.launchApp ||
3357
3376
  // deep links own the same bounded guest launch and tree waits.
3358
3377
  step.openLink ||
3359
- step.runFlow ||
3360
- // retry reruns its body on failure, so its wall time is the body's
3361
- // times the retry count — the 10s no-progress watchdog is the wrong
3362
- // budget and would report "bridge probably hung" for a slow retry.
3363
- step.retry ||
3364
3378
  // runScript http calls are synchronous with a 5-minute upstream
3365
3379
  // timeout — the 10s watchdog would kill legitimate setup scripts.
3366
3380
  step.runScript ||
@@ -3382,22 +3396,28 @@ export class SootSimBridgeFlowRunner {
3382
3396
  const STEP_WATCHDOG_MS = 10_000
3383
3397
  const body = this.runStepInner(step)
3384
3398
  let screenshotPath: string | undefined
3385
- if (hasBoundedWait) {
3399
+ // launchApp self-governs with an internal waitForTree (120s on a
3400
+ // clearState reload): a cache-cleared reload of a heavy bundle re-fetches
3401
+ // every chunk from metro and routinely needs more than the step ceiling.
3402
+ if (isContainer || step.launchApp) {
3386
3403
  const result = await body
3387
3404
  if (typeof result === 'string') screenshotPath = result
3388
3405
  } else {
3406
+ const deadlineMs = hasBoundedWait ? stepCeilingMs(step) : STEP_WATCHDOG_MS
3389
3407
  let timer: ReturnType<typeof setTimeout> | null = null
3390
- const watchdog = new Promise<never>((_, reject) => {
3408
+ const deadline = new Promise<never>((_, reject) => {
3391
3409
  timer = setTimeout(() => {
3392
3410
  reject(
3393
3411
  new Error(
3394
- `step watchdog: ${stepName} made no progress in ${STEP_WATCHDOG_MS}ms — bridge or bundle probably hung`,
3412
+ hasBoundedWait
3413
+ ? `step ${stepIndex + 1} (${stepName}) exceeded ${deadlineMs}ms deadline`
3414
+ : `step watchdog: ${stepName} made no progress in ${STEP_WATCHDOG_MS}ms — bridge or bundle probably hung`,
3395
3415
  ),
3396
3416
  )
3397
- }, STEP_WATCHDOG_MS)
3417
+ }, deadlineMs)
3398
3418
  })
3399
3419
  try {
3400
- const result = await Promise.race([body, watchdog])
3420
+ const result = await Promise.race([body, deadline])
3401
3421
  if (typeof result === 'string') screenshotPath = result
3402
3422
  } finally {
3403
3423
  if (timer) clearTimeout(timer)
@@ -3786,27 +3806,6 @@ export class SootSimBridgeFlowRunner {
3786
3806
  private async runFlowBody(steps: MaestroStep[]) {
3787
3807
  for (let i = 0; i < steps.length; i++) {
3788
3808
  const step = steps[i]
3789
- // the ceiling must never fire before the step's own deadline. waitFor and
3790
- // extendedWaitUntil both scale their declared timeout by
3791
- // SOOTSIM_FLOW_TIMEOUT_SCALE, so the ceiling scales it too — and it has to
3792
- // count extendedWaitUntil at all. Missing that, a flow declaring
3793
- // `extendedWaitUntil: {timeout: 90000}` was killed by the race at 60s while
3794
- // runStep was still waiting, so runStep never reached its own catch and the
3795
- // step was never recorded: a failed run whose every recorded step is green.
3796
- const explicitWait = typeof step.wait === 'number' ? step.wait : 0
3797
- const waitForTimeout =
3798
- typeof step.waitFor?.timeout === 'number' ? step.waitFor.timeout : 0
3799
- const extendedWaitTimeout =
3800
- typeof step.extendedWaitUntil?.timeout === 'number'
3801
- ? step.extendedWaitUntil.timeout
3802
- : 0
3803
- const stepCeiling = Math.max(
3804
- 60_000,
3805
- explicitWait + 15_000,
3806
- scaledTimeout(waitForTimeout) + 15_000,
3807
- scaledTimeout(extendedWaitTimeout) + 15_000,
3808
- )
3809
-
3810
3809
  // pause gate + live rail: checked between top-level steps only, so a
3811
3810
  // devtools pause never interrupts a step mid-gesture.
3812
3811
  if (this.liveStatus) {
@@ -3843,34 +3842,11 @@ export class SootSimBridgeFlowRunner {
3843
3842
  }
3844
3843
 
3845
3844
  try {
3846
- // runFlow runs its own sub-steps (each separately deadlined) and
3847
- // launchApp self-governs with an internal waitForTree (120s on a
3848
- // clearState reload) — a cache-cleared reload of a heavy bundle (bluesky)
3849
- // re-fetches every chunk from metro and routinely needs >60s, so the
3850
- // outer per-step race would kill a launch that's legitimately still
3851
- // booting. let both govern their own timing.
3852
- if (step.runFlow || step.launchApp) {
3853
- await this.runStep(step, i, onStepStart)
3854
- } else {
3855
- await Promise.race([
3856
- this.runStep(step, i, onStepStart),
3857
- new Promise<never>((_, reject) =>
3858
- setTimeout(
3859
- () =>
3860
- reject(
3861
- new Error(
3862
- `step ${i + 1} (${Object.keys(step)[0]}) exceeded ${stepCeiling}ms deadline`,
3863
- ),
3864
- ),
3865
- stepCeiling,
3866
- ),
3867
- ),
3868
- ])
3869
- }
3845
+ // runStep owns every step's deadline, nested or top level.
3846
+ await this.runStep(step, i, onStepStart)
3870
3847
  } catch (error) {
3871
- // a ceiling hit rejects the race while runStep is still awaiting, so
3872
- // runStep never records the step itself. record it here so the trace
3873
- // always names the step that failed.
3848
+ // a when predicate that throws fails before runStep records the step.
3849
+ // record it here so the trace always names the step that failed.
3874
3850
  const last = this.flowTraceSteps[this.flowTraceSteps.length - 1]
3875
3851
  if (!(last && last.stepIndex === i && last.startedAtMs >= liveStartedAtMs)) {
3876
3852
  const stepName = Object.keys(step)[0]
@@ -1603,9 +1603,11 @@ export function createCloudBridgeForParsed(
1603
1603
  : new CloudBridge(session)
1604
1604
  }
1605
1605
 
1606
- // stops the box, which ends the simulator it holds. an account api key stops
1607
- // with its own authority; a session login stops with the login, falling back
1608
- // to the box token the session already holds.
1606
+ // deletes the box, which ends the simulator it holds. a cloud session box is
1607
+ // the prebuilt bundle `peach ios --remote` created, so it holds nothing but that
1608
+ // bundle: stopping it would only keep its name taken and its storage held. an
1609
+ // account api key deletes with its own authority; a session login deletes with
1610
+ // the login, falling back to the box token the session already holds.
1609
1611
  export async function closeCloudBox(session: CloudSession): Promise<void> {
1610
1612
  if (session.service !== 'box') {
1611
1613
  if (!session.boxId || !session.boxToken) return
@@ -1632,7 +1634,7 @@ export async function closeCloudBox(session: CloudSession): Promise<void> {
1632
1634
  let response: Response
1633
1635
  try {
1634
1636
  response = await fetch(
1635
- `${session.apiOrigin}/v1/boxes/${encodeURIComponent(session.boxId)}/stop`,
1637
+ `${session.apiOrigin}/v1/boxes/${encodeURIComponent(session.boxId)}/delete`,
1636
1638
  {
1637
1639
  method: 'POST',
1638
1640
  headers: { authorization: authHeader },
@@ -1651,6 +1653,15 @@ export async function closeCloudBox(session: CloudSession): Promise<void> {
1651
1653
  }
1652
1654
  }
1653
1655
 
1656
+ // the box's page reported that the app threw while booting, so no simulator is
1657
+ // coming and the box should end rather than bill.
1658
+ export class BoxBootFailedError extends Error {
1659
+ constructor(boxId: string, detail: string) {
1660
+ super(`box ${boxId} could not boot the app: ${detail}`)
1661
+ this.name = 'BoxBootFailedError'
1662
+ }
1663
+ }
1664
+
1654
1665
  export interface BoxDisplayGrant {
1655
1666
  url: string
1656
1667
  authToken: string
@@ -1711,6 +1722,17 @@ export async function requestBoxWatchUrl(options: {
1711
1722
  }`,
1712
1723
  )
1713
1724
  }
1725
+ if (response.status === 422) {
1726
+ const text = await response.text()
1727
+ let failure: unknown = null
1728
+ try {
1729
+ failure = JSON.parse(text)
1730
+ } catch {}
1731
+ if (isRecord(failure) && failure.code === 'boot-failed') {
1732
+ throw new BoxBootFailedError(options.boxId, String(failure.error))
1733
+ }
1734
+ throw responseError(response.status, text)
1735
+ }
1714
1736
  if (response.status === 409) {
1715
1737
  // the page has not attached its simulator yet. this is the normal state
1716
1738
  // right after a create, so wait rather than refusing.
@@ -412,7 +412,13 @@ async function runRemoteBox(
412
412
  const command = `${platform} --remote`
413
413
  const input = remoteInputFromArgs(args, platform)
414
414
  const [
415
- { closeCloudBox, closeCloudSession, produceCloudBoxArtifact, requestBoxWatchUrl },
415
+ {
416
+ BoxBootFailedError,
417
+ closeCloudBox,
418
+ closeCloudSession,
419
+ produceCloudBoxArtifact,
420
+ requestBoxWatchUrl,
421
+ },
416
422
  box,
417
423
  { cloudBoxConnectUrl, ensureCloudBoxBrowser, readCloudBoxSession },
418
424
  { authHeaderOrExit, cloudAccountIdOrExit },
@@ -551,16 +557,32 @@ async function runRemoteBox(
551
557
  // `peach remote close` cannot end it; name the command that does.
552
558
  if (!attachedToShell) {
553
559
  console.log(
554
- ` this shell has no Peach integration; the Box bills until: ${rnxPublicBrand.commandName} box stop ${created.id}`,
560
+ ` this shell has no Peach integration; the Box bills until: ${rnxPublicBrand.commandName} box delete ${created.id}`,
555
561
  )
556
562
  }
557
563
  // the session is published before the grant is minted, so a grant that never
558
564
  // arrives still leaves the box addressable: re-running this command mints it.
559
- const watchUrl = await requestBoxWatchUrl({
560
- apiOrigin: endpoint,
561
- boxId: created.id,
562
- boxToken: created.accessToken ?? '',
563
- })
565
+ let watchUrl: string
566
+ try {
567
+ watchUrl = await requestBoxWatchUrl({
568
+ apiOrigin: endpoint,
569
+ boxId: created.id,
570
+ boxToken: created.accessToken ?? '',
571
+ })
572
+ } catch (error) {
573
+ // an app that cannot boot never gets a simulator, so the box ends with the
574
+ // error instead of billing until someone finds it.
575
+ if (error instanceof BoxBootFailedError) {
576
+ await closeCloudBox(session)
577
+ try {
578
+ publishCloudSessionToShell(null)
579
+ } catch {
580
+ // no shell update channel is required outside an interactive peach shell
581
+ }
582
+ delete process.env[PEACH_CLOUD_SESSION_ENV]
583
+ }
584
+ throw error
585
+ }
564
586
  console.log(` open ${watchUrl}`)
565
587
  return openInBrowser(watchUrl)
566
588
  }
@@ -416,40 +416,45 @@ export async function runRecord(args: string[], opts: RecordOptions) {
416
416
  }
417
417
  await new Promise((r) => setTimeout(r, durationMs))
418
418
  const deadline = Date.now() + Math.max(5_000, durationMs)
419
- let frames: Array<{ data: string; size: number }> | null = null
419
+ // frames come back a few at a time: one message holding every base64 png
420
+ // exceeds the bridge socket's message cap and closes it.
421
+ const FRAMES_PER_PAGE = 2
422
+ let saved = 0
420
423
  for (;;) {
421
424
  const result: {
422
425
  ok: boolean
423
426
  done: boolean
427
+ totalFrames?: number
424
428
  frames?: Array<{ data: string; size: number }>
425
429
  error?: string
426
430
  } | null = await bridge.send({
427
431
  type: 'evaluate',
428
- code: `window.__sootsimRecorder.getFrameCaptureResult(${start.requestId})`,
432
+ code: `window.__sootsimRecorder.getFrameCaptureResult(${start.requestId}, { offset: ${saved}, limit: ${FRAMES_PER_PAGE} })`,
429
433
  })
430
434
  if (!result) {
431
435
  console.error(' frame capture result missing')
432
436
  peachExit(1)
433
437
  }
434
- if (result.done) {
435
- if (!result.ok) {
436
- console.error(` frame capture failed: ${result.error ?? 'unknown error'}`)
438
+ if (!result.done) {
439
+ if (Date.now() >= deadline) {
440
+ console.error(' frame capture timed out')
437
441
  peachExit(1)
438
442
  }
439
- frames = result.frames ?? []
440
- break
443
+ await new Promise((r) => setTimeout(r, 100))
444
+ continue
441
445
  }
442
- if (Date.now() >= deadline) {
443
- console.error(' frame capture timed out')
446
+ if (!result.ok) {
447
+ console.error(` frame capture failed: ${result.error ?? 'unknown error'}`)
444
448
  peachExit(1)
445
449
  }
446
- await new Promise((r) => setTimeout(r, 100))
450
+ for (const f of result.frames ?? []) {
451
+ saved++
452
+ const p = `${outDir}/frame-${String(saved).padStart(3, '0')}.png`
453
+ writeFileSync(p, Buffer.from(f.data, 'base64'))
454
+ }
455
+ if (saved >= (result.totalFrames ?? 0)) break
447
456
  }
448
- frames.forEach((f, i) => {
449
- const p = `${outDir}/frame-${String(i + 1).padStart(3, '0')}.png`
450
- writeFileSync(p, Buffer.from(f.data, 'base64'))
451
- })
452
- console.log(` saved ${frames.length} frames`)
457
+ console.log(` saved ${saved} frames`)
453
458
  return
454
459
  }
455
460
 
@@ -1,4 +1,3 @@
1
- import { createBrowserPageTargetReader } from '../../../peach-engine/src/browser-page-target'
2
1
  import {
3
2
  acquireChromiumAdmission,
4
3
  readChromiumQueue,
@@ -21,6 +20,7 @@ import {
21
20
  chromeDisabledFeaturesSwitch,
22
21
  softwareChromeLaunchOptions,
23
22
  } from '../../../../scripts/lib/reap-browser'
23
+ import { createBrowserPageTargetReader } from '../../../peach-engine/src/browser-page-target'
24
24
  import {
25
25
  readBrowserCompositeCaptureRequest,
26
26
  resolveBrowserCompositeCdpClip,
@@ -213,6 +213,16 @@ const removeDisposableCaches = (root) => {
213
213
  removeDisposableCaches(childPath);
214
214
  }
215
215
  };
216
+ // chrome treats a SingletonLock whose recorded hostname differs from the
217
+ // current one as held by another machine, and macOS renames the host with the
218
+ // network, so a lock left by a killed host blocks every relaunch of a named
219
+ // profile. no process runs this profile here, so its lock files are stale.
220
+ const clearStaleSingletonLock = () => {
221
+ if (chromePidsByProfile().length > 0) return;
222
+ for (const name of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
223
+ try { rmSync(join(profileDir, name), { force: true }); } catch {}
224
+ }
225
+ };
216
226
  const cleanupProfile = () => {
217
227
  try { rmSync(diskCacheDir, { recursive: true, force: true }); } catch {}
218
228
  if (ephemeralProfile) {
@@ -538,6 +548,7 @@ const cleanupProfile = () => {
538
548
  label: 'peach sim host ' + profileDir,
539
549
  readQueue: readChromiumQueue,
540
550
  });
551
+ clearStaleSingletonLock();
541
552
  let launchErr = null;
542
553
  let launchedChannel = null;
543
554
  for (const channel of channelsToTry) {
@@ -275,8 +275,8 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
275
275
  // driving a box sends each command to its own box.
276
276
  'packages/peachsim/cli/cloud-client.ts :: fetch :: `${this.session.apiOrigin}/v1/boxes/${encodeURIComponent()}/commands`':
277
277
  { category: 'peach_cloud_sim', count: 1 },
278
- // stopping a box leaves it allocated, unlike the delete above.
279
- 'packages/peachsim/cli/cloud-client.ts :: fetch :: `${session.apiOrigin}/v1/boxes/${encodeURIComponent()}/stop`':
278
+ // closing a remote simulator deletes its box with the account's authority.
279
+ 'packages/peachsim/cli/cloud-client.ts :: fetch :: `${session.apiOrigin}/v1/boxes/${encodeURIComponent()}/delete`':
280
280
  { category: 'peach_cloud_sim', count: 1 },
281
281
  // the display grant is polled until the box hands one back; the body is
282
282
  // empty, so the request carries no project or app data.
@@ -1079,7 +1079,9 @@ function callKind(
1079
1079
  source: string,
1080
1080
  ): string | null {
1081
1081
  if (node.type === 'NewExpression' && node.callee.type === 'Identifier') {
1082
- if (node.callee.name === 'WebSocket') return 'websocket'
1082
+ if (node.callee.name === 'WebSocket' || node.callee.name === 'NativeWebSocket') {
1083
+ return 'websocket'
1084
+ }
1083
1085
  if (node.callee.name === 'EventSource') return 'event-source'
1084
1086
  if (node.callee.name === 'XMLHttpRequest') return 'xml-http-request'
1085
1087
  return null
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
package/dist-lib/beta.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
package/dist-lib/beta.mjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
 
3
3
  // src/beta.ts
4
4
  var IS_BETA = true;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
 
3
3
  // src/bridge-contract.ts
4
4
  var SIM_LONG_PRESS_MAX_MS = 5e3;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
 
3
3
  // src/bridge-contract.ts
4
4
  var REFUSED_PERFORM_STEP_TYPES = [
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1618,7 +1618,7 @@ function swiftTargetDependencies(files2, targets) {
1618
1618
  }
1619
1619
  function swiftArtifactHash(input, release, moduleRoot) {
1620
1620
  const hash = (0, import_node_crypto2.createHash)("sha256");
1621
- hash.update(`swift-compile/v46
1621
+ hash.update(`swift-compile/v47
1622
1622
  ${release}
1623
1623
  `);
1624
1624
  if (input.oneModule) hash.update("one-module\n");
@@ -2143,9 +2143,14 @@ function compiledSwiftSources(files2, targets) {
2143
2143
  const orderedTargets = [...targets].sort((a, b) => b.path.length - a.path.length);
2144
2144
  for (const [file, contents] of Object.entries(files2)) {
2145
2145
  if (!file.endsWith(".swift")) continue;
2146
- const owner = orderedTargets.find(
2147
- (candidate) => candidate.path === "." || file.startsWith(`${candidate.path}/`)
2148
- );
2146
+ const owner = orderedTargets.find((candidate) => {
2147
+ const prefix = candidate.path === "." ? "" : `${candidate.path}/`;
2148
+ if (!file.startsWith(prefix)) return false;
2149
+ const relative = file.slice(prefix.length);
2150
+ return !candidate.exclude?.some(
2151
+ (excluded) => relative === excluded || relative.startsWith(`${excluded}/`)
2152
+ );
2153
+ });
2149
2154
  if (/(?:^|\/)Package(?:@swift-[\d.]+)?\.swift$/.test(file) && (!owner || /^Package(?:@swift-[\d.]+)?\.swift$/.test(file))) {
2150
2155
  compiled.set(file, contents);
2151
2156
  continue;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
 
3
3
  // src/capture-contract.ts
4
4
  var RNX_SCREEN_CAPTURE_VERSION = 1;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
 
3
3
  // src/cloud-contract.ts
4
4
  var RNX_CLOUD_MAX_ARTIFACT_BYTES = 40761776;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
 
3
3
  // src/cloud-test-args.ts
4
4
  var CLOUD_TEST_ARGS_MAX_COUNT = 32;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -195,7 +195,7 @@ function sleep(ms) {
195
195
  var DEFAULT_DEVICE = "iphone-16";
196
196
  var DEFAULT_WAIT_READY_MS = 2e4;
197
197
  var CLOUD_MAX_RETRIES = 2;
198
- var CLOUD_NEVER_RETRY_CODES = /* @__PURE__ */ new Set(["concurrency_limit", "early_access"]);
198
+ var CLOUD_NEVER_RETRY_CODES = /* @__PURE__ */ new Set(["concurrency_limit"]);
199
199
  function normalizeCloudOrigin(source) {
200
200
  let parsed;
201
201
  try {
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
4
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -165,7 +165,7 @@ function sleep(ms) {
165
165
  var DEFAULT_DEVICE = "iphone-16";
166
166
  var DEFAULT_WAIT_READY_MS = 2e4;
167
167
  var CLOUD_MAX_RETRIES = 2;
168
- var CLOUD_NEVER_RETRY_CODES = /* @__PURE__ */ new Set(["concurrency_limit", "early_access"]);
168
+ var CLOUD_NEVER_RETRY_CODES = /* @__PURE__ */ new Set(["concurrency_limit"]);
169
169
  function normalizeCloudOrigin(source) {
170
170
  let parsed;
171
171
  try {
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
 
3
3
  // src/config.ts
4
4
  import { isValidRuntimeVersion } from "@contrast/runtime-delivery/version";
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;