peachsim 0.1.1406 → 0.1.1465

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 (150) hide show
  1. package/LICENSE +8 -4
  2. package/README.md +22 -2
  3. package/cli/app-fonts.ts +50 -44
  4. package/cli/bin.ts +5 -4
  5. package/cli/bridge-diagnostics.ts +5 -0
  6. package/cli/bridge-flow-runner.ts +21 -13
  7. package/cli/cloud-client.ts +4 -3
  8. package/cli/commands/control.ts +155 -123
  9. package/cli/commands/daemon-mac-app.ts +3 -3
  10. package/cli/commands/daemon.ts +2 -2
  11. package/cli/commands/do-chain.ts +9 -2
  12. package/cli/commands/electron.ts +4 -1
  13. package/cli/commands/inspect/core.ts +1 -1
  14. package/cli/commands/inspect/settle.ts +19 -7
  15. package/cli/commands/inspect/settling.ts +92 -56
  16. package/cli/commands/inspect/wait-idle.ts +21 -9
  17. package/cli/commands/inspect/wait-ready.ts +22 -8
  18. package/cli/commands/inspect.ts +101 -134
  19. package/cli/commands/setup.ts +26 -16
  20. package/cli/commands/upgrade.ts +53 -9
  21. package/cli/desktop-companion.ts +1 -1
  22. package/cli/drivers/playwright-sim-host.ts +32 -13
  23. package/cli/drivers/playwright.ts +3 -3
  24. package/cli/main.ts +14 -5
  25. package/cli/outbound-endpoints.ts +1 -9
  26. package/detox/index.ts +6 -1
  27. package/dist-lib/agent-daemon-client.cjs +1 -1
  28. package/dist-lib/agent-events.cjs +1 -1
  29. package/dist-lib/attached-projects.cjs +1 -1
  30. package/dist-lib/auth/shared-session.cjs +1 -1
  31. package/dist-lib/backend-origin.cjs +1 -1
  32. package/dist-lib/beta.cjs +1 -1
  33. package/dist-lib/beta.mjs +1 -1
  34. package/dist-lib/bridge-constants.cjs +1 -1
  35. package/dist-lib/bridge-contract-input.cjs +1 -1
  36. package/dist-lib/bridge-contract-input.mjs +1 -1
  37. package/dist-lib/bridge-contract.cjs +3 -3
  38. package/dist-lib/bridge-contract.mjs +3 -3
  39. package/dist-lib/bundle-channel.cjs +322 -274
  40. package/dist-lib/capture-contract.cjs +1 -1
  41. package/dist-lib/capture-contract.mjs +1 -1
  42. package/dist-lib/cloud-contract.cjs +1 -1
  43. package/dist-lib/cloud-contract.mjs +1 -1
  44. package/dist-lib/cloud-test-args.cjs +1 -1
  45. package/dist-lib/cloud-test-args.mjs +1 -1
  46. package/dist-lib/cloud.cjs +1 -1
  47. package/dist-lib/cloud.mjs +1 -1
  48. package/dist-lib/config.cjs +1 -1
  49. package/dist-lib/config.mjs +1 -1
  50. package/dist-lib/detox/index.cjs +512 -122
  51. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  52. package/dist-lib/expo-swift-bundle.cjs +69 -33
  53. package/dist-lib/expo-swift-metadata.cjs +1 -1
  54. package/dist-lib/expo-swift-metadata.mjs +1 -1
  55. package/dist-lib/home-paths.cjs +1 -1
  56. package/dist-lib/host/bridge-host.cjs +3650 -3528
  57. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  58. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  59. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  60. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  61. package/dist-lib/host/websocket-proxy.cjs +1 -1
  62. package/dist-lib/index.cjs +327 -123
  63. package/dist-lib/jump-to-source-babel.cjs +1 -1
  64. package/dist-lib/jump-to-source-native.cjs +1 -1
  65. package/dist-lib/kotlin-host.cjs +10 -5
  66. package/dist-lib/menu.cjs +1 -1
  67. package/dist-lib/menu.mjs +1 -1
  68. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  69. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  70. package/dist-lib/metro-production-bundle.cjs +1 -1
  71. package/dist-lib/metro-production-bundle.mjs +1 -1
  72. package/dist-lib/metro.cjs +49 -47
  73. package/dist-lib/native-app-identity-project.cjs +1 -1
  74. package/dist-lib/native-app-identity-project.mjs +1 -1
  75. package/dist-lib/native-app-identity.cjs +1 -1
  76. package/dist-lib/native-app-identity.mjs +1 -1
  77. package/dist-lib/native-firebase-metadata.cjs +1 -1
  78. package/dist-lib/native-firebase-metadata.mjs +1 -1
  79. package/dist-lib/plist.cjs +1 -1
  80. package/dist-lib/plist.mjs +1 -1
  81. package/dist-lib/profiles.cjs +1 -1
  82. package/dist-lib/public-brand.cjs +1 -1
  83. package/dist-lib/react-native-host-modules.cjs +1 -1
  84. package/dist-lib/react-native-host-modules.mjs +1 -1
  85. package/dist-lib/scripts/dev-server-scanner.cjs +23 -17
  86. package/dist-lib/sdk.cjs +99 -61
  87. package/dist-lib/sdk.mjs +97 -61
  88. package/dist-lib/skills.cjs +3347 -3051
  89. package/dist-lib/swift-compile.cjs +26 -12
  90. package/dist-lib/swift-compile.mjs +26 -12
  91. package/dist-lib/swift-package.cjs +1 -1
  92. package/dist-lib/swift-package.mjs +1 -1
  93. package/dist-lib/swift-submit.cjs +2 -2
  94. package/dist-lib/swift-submit.mjs +2 -2
  95. package/dist-lib/swift-toolchain.cjs +1 -1
  96. package/dist-lib/swift.cjs +1 -1
  97. package/dist-lib/vite-swift.cjs +66 -31
  98. package/dist-lib/vite.cjs +1 -1
  99. package/dist-lib/worklet-executable.cjs +5 -1
  100. package/dist-lib/worklet-executable.mjs +5 -1
  101. package/kotlin/AppSources/android/view/Window.kt +2 -0
  102. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +1 -1
  103. package/kotlin/ComposeSources/PeachComposeHost.kt +35 -9
  104. package/kotlin/ExpoComposeSources/dev/peach/expo/PeachExpoCompose.kt +2 -2
  105. package/kotlin/ExpoSources/dev/peach/expo/PeachExpoRuntime.kt +9 -6
  106. package/kotlin/ExpoSources/dev/peach/expo/PeachExpoTypes.kt +4 -4
  107. package/kotlin/ExpoSources/expo/modules/kotlin/modules/Module.kt +2 -2
  108. package/kotlin/ExpoSources/expo/modules/kotlin/viewevent/ViewEvent.kt +22 -8
  109. package/kotlin/ExpoSources/expo/modules/kotlin/views/ExpoView.kt +0 -3
  110. package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewDefinitionBuilder.kt +10 -9
  111. package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewProps.kt +6 -5
  112. package/kotlin/fixtures/import-drawing/Drawing.d.kt.ts +7 -0
  113. package/kotlin/fixtures/import-drawing/Drawing.kt +101 -0
  114. package/kotlin/fixtures/import-drawing/ImportDrawing.native.tsx +18 -0
  115. package/kotlin/fixtures/import-drawing/README.md +34 -0
  116. package/kotlin/fixtures/import-drawing/app/_layout.tsx +4 -0
  117. package/kotlin/fixtures/import-drawing/app/index+spa.tsx +1 -0
  118. package/kotlin/fixtures/import-drawing/package.json +23 -0
  119. package/kotlin/fixtures/import-drawing/tsconfig.json +20 -0
  120. package/kotlin/fixtures/import-drawing/vite.config.ts +20 -0
  121. package/kotlin/fixtures/import-scroll/ImportNested.native.tsx +10 -0
  122. package/kotlin/fixtures/import-scroll/Nested.d.kt.ts +4 -0
  123. package/kotlin/fixtures/import-scroll/README.md +15 -0
  124. package/kotlin/fixtures/import-scroll/app/nested+spa.tsx +1 -0
  125. package/kotlin/fixtures/import-values/ImportValues.native.tsx +26 -0
  126. package/kotlin/fixtures/import-values/README.md +6 -1
  127. package/kotlin/fixtures/import-values/Values.d.kt.ts +4 -0
  128. package/kotlin/fixtures/import-values/Values.kt +18 -0
  129. package/kotlin/tools/expo-types.main.kts +17 -5
  130. package/package.json +2 -1
  131. package/scripts/dev-server-scanner.ts +33 -23
  132. package/src/assetCatalogContract.ts +7 -0
  133. package/src/bridge-contract.ts +21 -3
  134. package/src/connect.ts +18 -5
  135. package/src/desktop-host-delivery.ts +33 -3
  136. package/src/expo-swift-host.ts +6 -3
  137. package/src/host/bridge-host.ts +2 -0
  138. package/src/host/fetch-proxy-transport.ts +8 -0
  139. package/src/kotlin-host.ts +11 -5
  140. package/src/native-firebase-config.ts +95 -54
  141. package/src/process-output.ts +16 -0
  142. package/src/sdk.ts +6 -1
  143. package/src/sim-client.ts +6 -1
  144. package/src/swift-compile.ts +36 -15
  145. package/src/swift-distribution.lock.json +8 -8
  146. package/src/swift-standin-emit.ts +10 -1
  147. package/src/swift-standins.ts +21 -7
  148. package/src/swift-submit.ts +6 -1
  149. package/src/worklet-executable.ts +7 -2
  150. /package/kotlin/fixtures/{kotlin-app-variants → import-scroll}/Nested.kt +0 -0
@@ -44,6 +44,23 @@ export class RuntimeReadinessUnsupportedError extends Error {
44
44
  }
45
45
  }
46
46
 
47
+ // a thrown idle read carries the host elapsed time and the original failure.
48
+ // it stays an error so callers cannot treat it as motion observed for maxMs.
49
+ export class IdleObservationError extends Error {
50
+ readonly code = 'idle_observation_failed'
51
+
52
+ constructor(
53
+ readonly elapsedMs: number,
54
+ readonly failure: unknown,
55
+ ) {
56
+ const detail = failure instanceof Error ? failure.message : String(failure)
57
+ super(`idle observation failed after ${elapsedMs}ms: ${detail}`, {
58
+ cause: failure,
59
+ })
60
+ this.name = 'IdleObservationError'
61
+ }
62
+ }
63
+
47
64
  export async function waitForSootsimIdle({
48
65
  bridge,
49
66
  simId,
@@ -54,6 +71,9 @@ export async function waitForSootsimIdle({
54
71
  paintStill = false,
55
72
  }: WaitForSootsimIdleOptions): Promise<WaitForSootsimIdleResult> {
56
73
  let result: any
74
+ // guest elapsed covers a completed read. this clock is read only when the
75
+ // bridge throws, so a successful settle does not report host time.
76
+ const observationStartedAt = Date.now()
57
77
  try {
58
78
  result = await bridge.send(
59
79
  {
@@ -84,27 +104,23 @@ export async function waitForSootsimIdle({
84
104
  let screenPixels = null
85
105
  try {
86
106
 
87
- // route-aware settle: a tap that pushes/pops a screen is async — the
88
- // old screen still renders for a moment, then the new one mounts and
89
- // its content loads. a pure layout-hash check reports "idle" on the
90
- // outgoing screen or on the incoming skeleton, so a driver re-taps
91
- // and stacks duplicate navigations. drain any in-flight screen
92
- // transition FIRST, bounded by the overall budget. when no
93
- // transition is happening this returns in ~80ms (startWindowMs), so
94
- // a plain button tap barely pays for it.
95
- try {
96
- const wfst = window.__sootsimTest?.waitForScreenTransitions
97
- if (typeof wfst === 'function') {
107
+ // discover transitions while observing layout. when motion starts,
108
+ // observations from before its completion cannot prove the new screen.
109
+ let transitionWait = (async () => {
110
+ try {
111
+ const wfst = window.__sootsimTest?.waitForScreenTransitions
98
112
  const remaining = deadline - Date.now()
99
- if (remaining > 120) {
100
- await wfst({
113
+ if (typeof wfst === 'function' && remaining > 120) {
114
+ const transition = await wfst({
101
115
  timeoutMs: Math.min(remaining - 80, 4000),
102
116
  settleMs: 64,
103
117
  startWindowMs: 80,
104
118
  })
119
+ return transition?.started === true
105
120
  }
106
- }
107
- } catch {}
121
+ } catch {}
122
+ return false
123
+ })()
108
124
 
109
125
  // a worker that cannot answer within the remaining budget is busy, not
110
126
  // idle: its poll reads as unsettled. bounding each read by the budget
@@ -129,17 +145,51 @@ export async function waitForSootsimIdle({
129
145
  let nodeCount = null
130
146
  let renderStatsAvailable = false
131
147
  let shellAnimating = false
148
+ let shellBlockers = ''
132
149
  let webViewStarting = false
133
150
  let webViewBlocker = ''
151
+ // the reads are independent, so they run concurrently: a loaded sim
152
+ // answers each one late, and a poll that waits for them in turn
153
+ // costs their sum, which fits too few polls in the budget to settle.
134
154
  // strict also waits out shell motion: a system alert, sheet or menu
135
155
  // animates in the shell worker while the app's own stats read still,
136
156
  // so its presentation is complete only once the shell's list empties.
137
- if (strict) {
138
- const motion = await answer(() => window.__sootsimTest?.listAnimations?.()).catch(() => null)
157
+ const motionRead = strict
158
+ ? answer(() => window.__sootsimTest?.listAnimations?.()).catch(() => null)
159
+ : null
160
+ const statsRead = answer(() => window.__sootsimRenderHost?.queryStats?.()).catch(() => null)
161
+ // image preparation ends in the tenant before the compositor decodes
162
+ // and realizes its pixels. both owners must finish before settling.
163
+ // the compositor rejects a stats request it is too busy to answer.
164
+ const compositorRead = answer(() => window.__sootsimCompositor.getStats()).catch(() => BUSY)
165
+ const countsRead = Promise.resolve().then(() => window.__sootsimTest?.getRequestCounts?.()).catch(() => null)
166
+ // mounted-root membership: the live node count moves while the tree
167
+ // mounts, unmounts or recycles, in either render mode.
168
+ const nodeCountRead = Promise.resolve().then(() => window.__sootsimTest?.getNodeCount?.()).catch(() => null)
169
+ // the mounted hierarchy in render-worker mode comes from the test
170
+ // bridge: window.__sootsimRoot is an empty placeholder there, so a
171
+ // walk over it hashes every screen to the same empty string while
172
+ // it moves. listInspectable is the worker-backed read describe
173
+ // uses; a worker too busy to answer it reads as unsettled. tight
174
+ // budgets skip this read and settle on the cheaper signals above.
175
+ const hierarchyRead =
176
+ fullGeometry && typeof window.__sootsimTest?.listInspectable === 'function'
177
+ ? answer(() => window.__sootsimTest.listInspectable({})).catch(() => null)
178
+ : null
179
+ if (motionRead) {
180
+ const motion = await motionRead
139
181
  if (motion === BUSY) workerBusy = true
140
- else shellAnimating = Array.isArray(motion) && motion.some((anim) => anim.realm === 'shell' && anim.priority !== 'low')
182
+ else if (Array.isArray(motion)) {
183
+ const blocking = motion.filter((anim) => anim.realm === 'shell' && anim.priority !== 'low')
184
+ shellAnimating = blocking.length > 0
185
+ // name what holds the settle so a timeout points at its owner.
186
+ shellBlockers = blocking
187
+ .slice(0, 4)
188
+ .map((anim) => anim.source + '/' + anim.kind + '#' + anim.id + ' at ' + anim.current + (anim.loop ? ' loop' : ''))
189
+ .join(', ')
190
+ }
141
191
  }
142
- const stats = await answer(() => window.__sootsimRenderHost?.queryStats?.()).catch(() => null)
192
+ const stats = await statsRead
143
193
  if (stats === BUSY) {
144
194
  workerBusy = true
145
195
  } else if (stats) {
@@ -176,10 +226,7 @@ export async function waitForSootsimIdle({
176
226
  // mounting or remounting, even with no animation flag raised.
177
227
  appCommits = typeof stats.appCommitCount === 'number' ? stats.appCommitCount : null
178
228
  }
179
- // image preparation ends in the tenant before the compositor decodes
180
- // and realizes its pixels. both owners must finish before settling.
181
- // the compositor rejects a stats request it is too busy to answer.
182
- const compositor = await answer(() => window.__sootsimCompositor.getStats()).catch(() => BUSY)
229
+ const compositor = await compositorRead
183
230
  if (compositor === BUSY) {
184
231
  workerBusy = true
185
232
  } else {
@@ -190,31 +237,12 @@ export async function waitForSootsimIdle({
190
237
  webViewBlocker = 'compositor webView capture not settled'
191
238
  }
192
239
  }
193
- try {
194
- const counts = await window.__sootsimTest?.getRequestCounts?.()
195
- requestTotal = typeof counts?.total === 'number' ? counts.total : 0
196
- requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
197
- } catch {}
198
- // mounted-root membership: the live node count moves while the tree
199
- // mounts, unmounts or recycles, in either render mode.
200
- try {
201
- const total = await window.__sootsimTest?.getNodeCount?.()
202
- nodeCount = typeof total === 'number' ? total : null
203
- } catch {}
204
- // the mounted hierarchy in render-worker mode comes from the test
205
- // bridge: window.__sootsimRoot is an empty placeholder there, so a
206
- // walk over it hashes every screen to the same empty string while
207
- // it moves. listInspectable is the worker-backed read describe
208
- // uses; a worker too busy to answer it reads as unsettled. tight
209
- // budgets skip this read and settle on the cheaper signals above.
210
- let hierarchy = null
211
- if (fullGeometry && typeof window.__sootsimTest?.listInspectable === 'function') {
212
- try {
213
- hierarchy = await answer(() => window.__sootsimTest.listInspectable({}))
214
- } catch {
215
- hierarchy = null
216
- }
217
- }
240
+ const counts = await countsRead
241
+ requestTotal = typeof counts?.total === 'number' ? counts.total : 0
242
+ requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
243
+ const total = await nodeCountRead
244
+ nodeCount = typeof total === 'number' ? total : null
245
+ let hierarchy = hierarchyRead ? await hierarchyRead : null
218
246
  if (hierarchy === BUSY) {
219
247
  workerBusy = true
220
248
  hierarchy = null
@@ -290,7 +318,7 @@ export async function waitForSootsimIdle({
290
318
  pixelFrameToken = token
291
319
  }
292
320
  }
293
- return { layout: nodes.join(';'), screenPixels, animating, shellAnimating, layoutDirty, pendingFetches, workerBusy, webViewStarting, webViewBlocker, renderStatsAvailable, requestTotal, requestInFlight, appCommits, nodeCount }
321
+ return { layout: nodes.join(';'), screenPixels, animating, shellAnimating, shellBlockers, layoutDirty, pendingFetches, workerBusy, webViewStarting, webViewBlocker, renderStatsAvailable, requestTotal, requestInFlight, appCommits, nodeCount }
294
322
  }
295
323
 
296
324
  // how long generic background network is allowed to keep us waiting AFTER
@@ -377,6 +405,19 @@ export async function waitForSootsimIdle({
377
405
  Date.now() - visuallyStillSince >= requiredStillMs &&
378
406
  (networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
379
407
  ) {
408
+ if (transitionWait) {
409
+ const started = await transitionWait
410
+ transitionWait = null
411
+ if (started) {
412
+ lastLayout = ''
413
+ lastRequestTotal = -1
414
+ lastAppCommits = null
415
+ lastNodeCount = null
416
+ stable = 0
417
+ visuallyStillSince = 0
418
+ continue
419
+ }
420
+ }
380
421
  return { settled: true, elapsed: Date.now() - start, blockedBy: '' }
381
422
  }
382
423
  } else {
@@ -395,7 +436,7 @@ export async function waitForSootsimIdle({
395
436
  const blockers = []
396
437
  if (strict && !last.renderStatsAvailable) blockers.push('render stats unavailable')
397
438
  if (last.animating) blockers.push('animations still running')
398
- if (last.shellAnimating) blockers.push('shell animations still running')
439
+ if (last.shellAnimating) blockers.push('shell animations still running (' + last.shellBlockers + ')')
399
440
  if (last.layoutDirty) blockers.push('layout still dirty')
400
441
  if (last.appCommits !== null && lastAppCommits !== null && last.appCommits !== lastAppCommits) blockers.push('layout commits still landing')
401
442
  if (last.nodeCount !== null && lastNodeCount !== null && last.nodeCount !== lastNodeCount) blockers.push('node count still changing')
@@ -426,12 +467,7 @@ export async function waitForSootsimIdle({
426
467
  },
427
468
  )
428
469
  } catch (error) {
429
- const message = error instanceof Error ? error.message : String(error)
430
- return {
431
- settled: false,
432
- elapsed: maxMs,
433
- blockedBy: message,
434
- }
470
+ throw new IdleObservationError(Math.max(0, Date.now() - observationStartedAt), error)
435
471
  }
436
472
 
437
473
  if (
@@ -1,6 +1,6 @@
1
- import { rnxExit } from '../../run-rnx'
1
+ import { rethrowIfExit, rnxExit } from '../../run-rnx'
2
2
  import { resolveMaxMsFlag } from './core'
3
- import { waitForSootsimIdle } from './settling'
3
+ import { IdleObservationError, waitForSootsimIdle } from './settling'
4
4
  import type { WsBridge } from '../../ws-bridge'
5
5
 
6
6
  // alias for `do settle` under the discoverable `wait` verb grouping.
@@ -15,12 +15,24 @@ export async function runWaitIdleSubcommand(opts: {
15
15
  const { bridge, args } = opts
16
16
  const strict = args.includes('--strict')
17
17
  const maxMs = resolveMaxMsFlag(args, 3000)
18
- const { elapsed, settled } = await waitForSootsimIdle({ bridge, maxMs, strict })
19
-
20
- if (settled) {
21
- console.log(` idle in ${elapsed}ms`)
22
- } else {
23
- console.error(` ⚠ wait idle timed out after ${elapsed}ms (may still be animating)`)
24
- rnxExit(1)
18
+ try {
19
+ const { elapsed, settled, blockedBy } = await waitForSootsimIdle({
20
+ bridge,
21
+ maxMs,
22
+ strict,
23
+ })
24
+ if (settled) {
25
+ console.log(` idle in ${elapsed}ms`)
26
+ } else {
27
+ console.error(` ⚠ wait idle timed out after ${elapsed}ms (${blockedBy})`)
28
+ rnxExit(1)
29
+ }
30
+ } catch (error) {
31
+ rethrowIfExit(error)
32
+ if (error instanceof IdleObservationError) {
33
+ console.error(` ⚠ ${error.message}`)
34
+ rnxExit(1)
35
+ }
36
+ throw error
25
37
  }
26
38
  }
@@ -5,7 +5,7 @@ import {
5
5
  type WaitReadyStatus,
6
6
  waitReadyReason,
7
7
  } from './core'
8
- import { waitReadyAndSettle } from './settling'
8
+ import { IdleObservationError, waitReadyAndSettle } from './settling'
9
9
  import type { WsBridge } from '../../ws-bridge'
10
10
 
11
11
  // blocks until the guest app bundle has finished loading and painted real
@@ -29,13 +29,27 @@ export async function runWaitReadySubcommand(opts: {
29
29
  printReady(status, status.elapsedMs)
30
30
  return
31
31
  }
32
- const { status, idle, elapsedMs } = await waitReadyAndSettle(bridge, timeoutMs, {
33
- onProgress(progress) {
34
- console.error(
35
- ` still waiting after ${progress.elapsedMs}ms — ${waitReadyReason(progress)} (nodes: ${progress.nodes}, targets: ${progress.targets}, live surfaces: ${progress.liveFrameActive ? progress.liveFrameChannels : 0}, browser surfaces: ${progress.browserSurfaceCount}, errors: ${progress.errors})`,
36
- )
37
- },
38
- })
32
+ let status: Awaited<ReturnType<typeof waitReadyAndSettle>>['status']
33
+ let idle: Awaited<ReturnType<typeof waitReadyAndSettle>>['idle']
34
+ let elapsedMs: number
35
+ try {
36
+ const settledWait = await waitReadyAndSettle(bridge, timeoutMs, {
37
+ onProgress(progress) {
38
+ console.error(
39
+ ` still waiting after ${progress.elapsedMs}ms — ${waitReadyReason(progress)} (nodes: ${progress.nodes}, targets: ${progress.targets}, live surfaces: ${progress.liveFrameActive ? progress.liveFrameChannels : 0}, browser surfaces: ${progress.browserSurfaceCount}, errors: ${progress.errors})`,
40
+ )
41
+ },
42
+ })
43
+ status = settledWait.status
44
+ idle = settledWait.idle
45
+ elapsedMs = settledWait.elapsedMs
46
+ } catch (error) {
47
+ if (error instanceof IdleObservationError) {
48
+ console.error(` ⚠ ${error.message}`)
49
+ rnxExit(1)
50
+ }
51
+ throw error
52
+ }
39
53
  if (!idle) timedOut(status, elapsedMs)
40
54
  if (!idle.settled) {
41
55
  console.error(