peachsim 0.1.1635 → 0.1.1641

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 (142) hide show
  1. package/README.md +10 -1
  2. package/cli/app-config.ts +14 -5
  3. package/cli/app-stubs.ts +96 -22
  4. package/cli/bridge-flow-runner.ts +2 -2
  5. package/cli/cloud-client.ts +60 -40
  6. package/cli/cloud-session.ts +16 -0
  7. package/cli/commands/assert.ts +17 -4
  8. package/cli/commands/box.ts +33 -19
  9. package/cli/commands/doctor.ts +37 -20
  10. package/cli/commands/flow.ts +1 -1
  11. package/cli/commands/inspect/actions.ts +3 -2
  12. package/cli/commands/inspect/core.ts +0 -8
  13. package/cli/commands/inspect/shared.ts +9 -4
  14. package/cli/commands/inspect.ts +65 -61
  15. package/cli/commands/platform.ts +2 -1
  16. package/cli/commands/preview.ts +1 -1
  17. package/cli/commands/record.ts +154 -68
  18. package/cli/commands/swift.ts +18 -4
  19. package/cli/commands/three-mode.ts +4 -38
  20. package/cli/commands/upload.ts +55 -49
  21. package/cli/drivers/playwright-sim-host.ts +65 -42
  22. package/cli/flow-live-status.ts +0 -2
  23. package/cli/main.ts +2 -2
  24. package/cli/outbound-endpoints.ts +7 -5
  25. package/cli/pause-aware-timeout.ts +4 -0
  26. package/cli/ws-bridge.ts +19 -16
  27. package/detox/attempt-events.cjs +99 -6
  28. package/detox/attempt-events.d.cts +7 -0
  29. package/detox/expectations.ts +7 -3
  30. package/detox/index.ts +259 -139
  31. package/detox/jest-environment.cjs +5 -1
  32. package/detox/motion-change.cjs +170 -21
  33. package/detox/motion-change.d.cts +32 -0
  34. package/detox/navigation-lifecycle.ts +35 -3
  35. package/dist-lib/agent-daemon-client.cjs +1 -1
  36. package/dist-lib/agent-events.cjs +1 -1
  37. package/dist-lib/attached-projects.cjs +1 -1
  38. package/dist-lib/auth/shared-session.cjs +1 -1
  39. package/dist-lib/backend-origin.cjs +17 -7
  40. package/dist-lib/beta.cjs +1 -1
  41. package/dist-lib/beta.mjs +1 -1
  42. package/dist-lib/bridge-constants.cjs +1 -1
  43. package/dist-lib/bridge-contract-input.cjs +1 -1
  44. package/dist-lib/bridge-contract-input.mjs +1 -1
  45. package/dist-lib/bridge-contract.cjs +1 -1
  46. package/dist-lib/bridge-contract.mjs +1 -1
  47. package/dist-lib/bundle-channel.cjs +5014 -2268
  48. package/dist-lib/capture-contract.cjs +1 -1
  49. package/dist-lib/capture-contract.mjs +1 -1
  50. package/dist-lib/cloud-contract.cjs +1 -1
  51. package/dist-lib/cloud-contract.mjs +1 -1
  52. package/dist-lib/cloud-test-args.cjs +1 -1
  53. package/dist-lib/cloud-test-args.mjs +1 -1
  54. package/dist-lib/cloud.cjs +1 -1
  55. package/dist-lib/cloud.mjs +1 -1
  56. package/dist-lib/config.cjs +1 -1
  57. package/dist-lib/config.mjs +1 -1
  58. package/dist-lib/detox/index.cjs +1036 -166
  59. package/dist-lib/detox/jest-environment.cjs +5 -1
  60. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  61. package/dist-lib/expo-swift-bundle.cjs +4613 -1963
  62. package/dist-lib/expo-swift-metadata.cjs +9 -7
  63. package/dist-lib/expo-swift-metadata.mjs +9 -7
  64. package/dist-lib/home-paths.cjs +1 -1
  65. package/dist-lib/host/bridge-host.cjs +58072 -16400
  66. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  67. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  68. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  69. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  70. package/dist-lib/host/websocket-proxy.cjs +1 -1
  71. package/dist-lib/index.cjs +2790 -143
  72. package/dist-lib/jump-to-source-babel.cjs +1 -1
  73. package/dist-lib/jump-to-source-native.cjs +1 -1
  74. package/dist-lib/kotlin-host.cjs +1 -1
  75. package/dist-lib/menu.cjs +1 -1
  76. package/dist-lib/menu.mjs +1 -1
  77. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  78. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  79. package/dist-lib/metro-production-bundle.cjs +1 -1
  80. package/dist-lib/metro-production-bundle.mjs +1 -1
  81. package/dist-lib/metro.cjs +2579 -95
  82. package/dist-lib/native-app-identity-project.cjs +243 -53
  83. package/dist-lib/native-app-identity-project.mjs +243 -53
  84. package/dist-lib/native-app-identity.cjs +29 -30
  85. package/dist-lib/native-app-identity.mjs +29 -30
  86. package/dist-lib/native-firebase-metadata.cjs +1 -1
  87. package/dist-lib/native-firebase-metadata.mjs +1 -1
  88. package/dist-lib/plist.cjs +1 -1
  89. package/dist-lib/plist.mjs +1 -1
  90. package/dist-lib/profiles.cjs +1 -1
  91. package/dist-lib/public-brand.cjs +1 -1
  92. package/dist-lib/react-native-host-modules.cjs +2 -2
  93. package/dist-lib/react-native-host-modules.mjs +2 -2
  94. package/dist-lib/scripts/dev-server-scanner.cjs +8 -6
  95. package/dist-lib/sdk.cjs +4 -3
  96. package/dist-lib/sdk.mjs +4 -3
  97. package/dist-lib/skills.cjs +66475 -23097
  98. package/dist-lib/swift-compile.cjs +452 -260
  99. package/dist-lib/swift-compile.mjs +451 -252
  100. package/dist-lib/swift-package.cjs +1 -1
  101. package/dist-lib/swift-package.mjs +1 -1
  102. package/dist-lib/swift-submit.cjs +14 -7
  103. package/dist-lib/swift-submit.mjs +14 -7
  104. package/dist-lib/swift-toolchain.cjs +316 -41
  105. package/dist-lib/swift.cjs +1 -1
  106. package/dist-lib/vite-swift.cjs +8188 -5636
  107. package/dist-lib/vite.cjs +2857 -12
  108. package/dist-lib/worklet-executable.cjs +14 -2
  109. package/dist-lib/worklet-executable.mjs +14 -2
  110. package/package.json +1 -1
  111. package/src/backend-origin.ts +21 -6
  112. package/src/expo-swift-host.ts +5 -2
  113. package/src/expoKotlinContract.ts +4 -7
  114. package/src/expoSwiftContract.ts +4 -6
  115. package/src/host/staged-host-files.ts +9 -0
  116. package/src/native-app-identity-project.ts +1 -4
  117. package/src/native-app-identity.ts +1 -4
  118. package/src/nativeAppIconContract.ts +1 -4
  119. package/src/output-lock-stdio.ts +36 -0
  120. package/src/react-native-bridge-modules.ts +106 -78
  121. package/src/react-native-host-modules.ts +4 -1
  122. package/src/settings-bundle.ts +1 -4
  123. package/src/swift-compile.ts +224 -139
  124. package/src/swift-distribution.lock.json +23 -12
  125. package/src/swift-distribution.ts +103 -4
  126. package/src/swift-host.ts +95 -30
  127. package/src/swift-icu.ts +16 -10
  128. package/src/swift-objc-compile.ts +547 -0
  129. package/src/swift-objc-metadata.ts +204 -0
  130. package/src/swift-objc-sdk.ts +883 -19
  131. package/src/swift-objc-standin.ts +126 -14
  132. package/src/swift-package-manifest.ts +42 -1
  133. package/src/swift-packages.ts +35 -21
  134. package/src/swift-root-lock.ts +153 -0
  135. package/src/swift-standin-emit.ts +40 -7
  136. package/src/swift-standins.ts +404 -138
  137. package/src/swift-submit.ts +18 -7
  138. package/src/swift-toolchain.ts +378 -46
  139. package/src/swiftContract.ts +12 -0
  140. package/src/worker-system-chunks.ts +11 -12
  141. package/src/worklet-executable.ts +19 -1
  142. package/src/xcode-project.ts +280 -33
@@ -373,9 +373,8 @@ function printVersions(findings: DoctorFindings): void {
373
373
  }
374
374
  }
375
375
 
376
- // one line per package, worst first, with the published implementation
377
- // percent. packages that run from the bundle with no native code sit last on
378
- // one line: there is nothing to check in them.
376
+ // packages appear worst first with the published implementation percent and
377
+ // complete capability details. javascript-only packages sit last on one line.
379
378
  function printPackages(
380
379
  packages: readonly PackageResult[],
381
380
  verbose: boolean,
@@ -411,10 +410,26 @@ function printPackages(
411
410
  const columns = process.stdout.columns || 120
412
411
  for (const row of rows) {
413
412
  const percent = percentLabel(row.item)
414
- const detail = fit(row.detail, Math.max(40, columns - nameWidth - versionWidth - 13))
413
+ const detail = row.detail
415
414
  console.log(
416
415
  ` ${row.glyph} ${row.item.name.padEnd(nameWidth)}${muted(row.item.version.padEnd(versionWidth))}${percent.padStart(5)}${detail ? ` ${row.tone(detail)}` : ''}`,
417
416
  )
417
+ if (!answering.has(row.item.name)) {
418
+ for (const [label, text] of [
419
+ ['working', row.item.rnx.working],
420
+ ['missing', row.item.rnx.missing],
421
+ ]) {
422
+ if (!text) continue
423
+ let line = `${label}:`
424
+ for (const word of text.split(/\s+/)) {
425
+ if (line.length + word.length + 1 > columns - 6) {
426
+ console.log(` ${muted(line)}`)
427
+ line = word
428
+ } else line += ` ${word}`
429
+ }
430
+ console.log(` ${muted(line)}`)
431
+ }
432
+ }
418
433
  if (verbose && row.item.note) console.log(` ${muted(row.item.note)}`)
419
434
  }
420
435
  if (javascriptOnly.length > 0) {
@@ -476,7 +491,10 @@ function packageRow(
476
491
  return {
477
492
  rank: RANK.unknown,
478
493
  glyph: accent('?'),
479
- detail: 'native code Peach does not know; stub it below',
494
+ // fonts and assets, or names built at runtime, leave nothing to stub by name
495
+ detail: unknownNative.requests.length
496
+ ? 'native code Peach does not know; stub it below'
497
+ : 'native code; names no native module',
480
498
  tone: muted,
481
499
  }
482
500
  // a stub answers ahead of Peach's own support for the package
@@ -514,22 +532,12 @@ function packageRow(
514
532
  return {
515
533
  rank: RANK.partial,
516
534
  glyph: warning('◐'),
517
- detail: pkg.rnx.missing ? `missing: ${firstClause(pkg.rnx.missing)}` : '',
535
+ detail: '',
518
536
  tone: muted,
519
537
  }
520
538
  return { rank: RANK.supported, glyph: success('✓'), detail: '', tone: muted }
521
539
  }
522
540
 
523
- function firstClause(text: string): string {
524
- const end = text.search(/[.;,](\s|$)/)
525
- return end > 0 ? text.slice(0, end) : text
526
- }
527
-
528
- // a detail ends at the terminal edge rather than wrapping under the columns
529
- function fit(text: string, width: number): string {
530
- return text.length > width ? `${text.slice(0, Math.max(0, width - 1))}…` : text
531
- }
532
-
533
541
  function printNativeGuidance(
534
542
  unknownNative: readonly UnknownNativePackage[],
535
543
  local: readonly LocalNative[],
@@ -560,19 +568,28 @@ function printNativeGuidance(
560
568
  )
561
569
  }
562
570
  // the open names grouped by the package they belong to, the key `packages` takes
563
- const open = new Map<string, NativeRequest[]>()
571
+ const open = new Map<string, Map<string, NativeRequest>>()
564
572
  for (const [owner, requests] of [
565
573
  ...unknownNative.map((item) => [item.name, item.requests] as const),
566
574
  ...local.map((item) => [item.package, [item]] as const),
567
575
  ]) {
568
- const unanswered = requests.filter((request) => !request.answered)
569
- if (unanswered.length) open.set(owner, [...(open.get(owner) ?? []), ...unanswered])
576
+ for (const request of requests) {
577
+ if (request.answered) continue
578
+ const names = open.get(owner) ?? new Map<string, NativeRequest>()
579
+ names.set(`${request.kind}:${request.name}`, request)
580
+ open.set(owner, names)
581
+ }
570
582
  }
571
583
  if (open.size) {
572
584
  console.log(`\n stub them in peach.config.ts, by package:\n`)
585
+ console.log(
586
+ muted(` import { defineConfig } from '${rnxPublicBrand.packageName}/config'`),
587
+ )
588
+ console.log()
573
589
  console.log(muted(` export default defineConfig({`))
574
590
  console.log(muted(` packages: {`))
575
- for (const [owner, requests] of open) {
591
+ for (const [owner, requestsByName] of open) {
592
+ const requests = [...requestsByName.values()]
576
593
  console.log(muted(` '${owner}': {`))
577
594
  for (const kind of ['module', 'view'] as const) {
578
595
  const names = requests.filter((request) => request.kind === kind)
@@ -973,7 +973,7 @@ flow extension:
973
973
  // canvas during scrub for smooth motion while the real app
974
974
  // catches up.
975
975
  if (videoPath) uploadArgs.push('--video', videoPath)
976
- if (scenePath) uploadArgs.push('--scene', scenePath)
976
+ if (scenePath) uploadArgs.push('--scene', scenePath, '--live-bundle')
977
977
  if (previewScreenshotPath) {
978
978
  uploadArgs.push('--screenshot', previewScreenshotPath)
979
979
  }
@@ -380,9 +380,10 @@ async function resolveTextTapTarget(
380
380
  query: string,
381
381
  options: TapTextOptions = {},
382
382
  ) {
383
+ const needle = options.exact ? query : query.trim().toLowerCase()
384
+ if (!needle) return { matched: 0, total: 0 }
383
385
  const { tree } = await inspectTree(bridge, 50)
384
386
  if (!Array.isArray(tree)) return { matched: 0, total: 0 }
385
- const needle = options.exact ? query : query.toLowerCase()
386
387
  let candidates = collectTextCandidates(tree).filter(
387
388
  ({ node, ancestorTestIDs, role }) => {
388
389
  const text = nodeText(node)
@@ -411,7 +412,7 @@ async function resolveTextTapTarget(
411
412
  if (actionable.length > 0) candidates = actionable
412
413
  if (!options.exact) {
413
414
  const whole = candidates.filter(
414
- ({ node }) => nodeText(node).trim().toLowerCase() === needle.trim(),
415
+ ({ node }) => nodeText(node).trim().toLowerCase() === needle,
415
416
  )
416
417
  if (whole.length > 0) candidates = whole
417
418
  }
@@ -556,12 +556,6 @@ export interface LayoutElement {
556
556
  ellipsisActive?: boolean
557
557
  }
558
558
 
559
- export interface LayoutResult {
560
- preview: 'web' | 'native'
561
- count: number
562
- elements: LayoutElement[]
563
- }
564
-
565
559
  // in-page extractor for the native preview. mirrors the visibility + device
566
560
  // sizing the a11y / wait-ready kernels use so "what's on screen" agrees across
567
561
  // the inspect surface. returns LayoutElement[] (or [] when the bridge is cold).
@@ -2013,8 +2007,6 @@ export const DEBUG_CHANNELS = [
2013
2007
  'yoga',
2014
2008
  ] as const
2015
2009
 
2016
- export type DebugChannel = (typeof DEBUG_CHANNELS)[number]
2017
-
2018
2010
  // `debug status` — which debug channels are currently enabled + their counts.
2019
2011
  export async function inspectDebugStatus(bridge: InspectBridge): Promise<unknown> {
2020
2012
  return bridge.send({ type: 'evaluate', code: 'window.__sootsimDebug.status()' })
@@ -92,10 +92,15 @@ export async function maybeWaitForStartedScreenTransitions(
92
92
  ) {
93
93
  if (bridge.plane === 'cloud') return
94
94
  try {
95
- const result = await callTestBridge<SootSimScreenTransitionWaitResult>(
96
- bridge,
97
- 'waitForScreenTransitions',
98
- SCREEN_TRANSITION_PREFLIGHT,
95
+ // the sim bounds this wait itself, so the round trip gets a short budget of
96
+ // its own; under the command budget a wedged sim cost every read twice
97
+ const result: SootSimScreenTransitionWaitResult | undefined = await bridge.send(
98
+ {
99
+ type: 'call',
100
+ path: '__sootsimTest.waitForScreenTransitions',
101
+ args: [SCREEN_TRANSITION_PREFLIGHT],
102
+ },
103
+ { timeoutMs: SCREEN_TRANSITION_PREFLIGHT.timeoutMs + 1200 },
99
104
  )
100
105
  if (!opts.verbose || !result?.started) return
101
106
  if (result.timedOut) {
@@ -1,6 +1,7 @@
1
1
  // peach inspect — dump node tree, accessibility info, query nodes
2
2
  // connects to the peach WS bridge (no Playwright)
3
3
 
4
+ import { Script } from 'node:vm'
4
5
  import { getCliInspectVerbMeta } from '@peach/skills/cli'
5
6
  import { renderCliCommandHelp, renderCliGroupHelp } from '@peach/skills/cli/renderers'
6
7
  import { DEFAULT_SOOTSIM_BRIDGE_PORT } from '../../src/bridge-constants'
@@ -194,6 +195,8 @@ async function performSingleStep(
194
195
  }
195
196
  }
196
197
 
198
+ // runs after a command already spent its whole budget, so an explicit --timeout
199
+ // must not make the liveness check wait that long a second time
197
200
  async function probeSimResponsive(bridge: WsBridge): Promise<boolean> {
198
201
  try {
199
202
  await bridge.send({ type: 'evaluate', code: '1' }, { timeoutMs: 3000 })
@@ -210,6 +213,18 @@ async function reportMissingTarget(
210
213
  verb: 'tap' | 'double-tap' | 'long-press',
211
214
  target: { mode: 'testid' | 'text'; value: string },
212
215
  ): Promise<void> {
216
+ if (target.mode === 'testid') {
217
+ // the tree can hold the id while the engine has no point to press: the node
218
+ // is not painted yet, or was replaced since the query. say so, so the
219
+ // suggestion list never names the id that was just reported missing.
220
+ const exact = await inspectFind(bridge, { testId: target.value }).catch(() => null)
221
+ if (exact?.result) {
222
+ console.error(
223
+ ` found ${target.value} in the tree but it has no tap target yet; it may be unpainted, hidden or covered: wait for it with peach wait selector ${target.value}`,
224
+ )
225
+ return
226
+ }
227
+ }
213
228
  console.error(` not found: ${target.value}`)
214
229
  if (target.mode !== 'testid') return
215
230
  const byText = await inspectFind(bridge, { text: target.value }).catch(() => null)
@@ -858,11 +873,10 @@ async function waitForKeyboardVisible(
858
873
  }
859
874
 
860
875
  /**
861
- * keys for a CPU simulator. it has no keyboard bridge to probe and refuses the
862
- * `keyboard` command, so its `perform` presses each character against
863
- * whatever holds focus and fails the batch when nothing does.
876
+ * dispatch through the shared input executor, which checks the requested
877
+ * node identity before targeted typing and carries refusals out of the CLI.
864
878
  */
865
- async function performCloudKeys(
879
+ async function performKeys(
866
880
  bridge: WsBridge,
867
881
  steps: Array<Extract<PerformStep, { type: 'type' | 'typeInto' | 'key' }>>,
868
882
  actionLabel: string,
@@ -957,20 +971,6 @@ function keyCodeToVisualKey(code: string) {
957
971
  return null
958
972
  }
959
973
 
960
- function getInspectCommandName(subcommand: string, directSubcommand: string | null) {
961
- if (directSubcommand === subcommand) return `peach ${subcommand}`
962
- const group = getCliInspectVerbMeta(subcommand)?.group
963
- return group ? `peach ${group} ${subcommand}` : `peach ${subcommand}`
964
- }
965
-
966
- function getInspectUsage(
967
- subcommand: string,
968
- tail: string,
969
- directSubcommand: string | null,
970
- ) {
971
- return ` usage: ${getInspectCommandName(subcommand, directSubcommand)}${tail ? ` ${tail}` : ''}`
972
- }
973
-
974
974
  function normalizeRecordedText(value: unknown) {
975
975
  if (typeof value !== 'string') return null
976
976
  const text = value.replace(/\s+/g, ' ').trim()
@@ -2462,7 +2462,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2462
2462
  console.error(' type-into requires a resolved text input node identity')
2463
2463
  peachExit(1)
2464
2464
  }
2465
- const typed = await performCloudKeys(
2465
+ const typed = await performKeys(
2466
2466
  bridge,
2467
2467
  [{ type: 'typeInto', text, nodeId }],
2468
2468
  'type-into',
@@ -2500,6 +2500,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2500
2500
  return {
2501
2501
  cx: n.absolutePosition.x + (n.layout.width || 0) / 2,
2502
2502
  cy: n.absolutePosition.y + (n.layout.height || 0) / 2,
2503
+ nodeId: n.nodeId,
2503
2504
  id: n.id,
2504
2505
  testID: n.testID,
2505
2506
  type: n.type,
@@ -2514,7 +2515,13 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2514
2515
  peachExit(1)
2515
2516
  }
2516
2517
  if (!tiPayload.isTextInput) {
2517
- console.error(` warning: ${targetId} is not a text input (isTextInput: false)`)
2518
+ console.error(` ${targetId} is not a text input (isTextInput: false)`)
2519
+ peachExit(1)
2520
+ }
2521
+ const nodeId: unknown = tiPayload.nodeId
2522
+ if (typeof nodeId !== 'number' || !Number.isSafeInteger(nodeId) || nodeId === 0) {
2523
+ console.error(' type-into requires a resolved text input node identity')
2524
+ peachExit(1)
2518
2525
  }
2519
2526
  // step 2: tap to focus
2520
2527
  const tapResult = await bridge.send({
@@ -2522,12 +2529,17 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2522
2529
  x: tiPayload.cx,
2523
2530
  y: tiPayload.cy,
2524
2531
  target: {
2532
+ nodeId,
2525
2533
  id: tiPayload.id ?? targetId,
2526
2534
  testID: tiPayload.testID ?? targetId,
2527
2535
  text: null,
2528
2536
  type: tiPayload.type ?? null,
2529
2537
  },
2530
2538
  })
2539
+ if (!isTapSuccess(tapResult)) {
2540
+ console.error(` type-into could not tap ${targetId}`)
2541
+ peachExit(1)
2542
+ }
2531
2543
  const targetSecureTextEntry = tiPayload.secureTextEntry === true
2532
2544
  const keyboardState = await waitForKeyboardVisible(bridge, 1000, {
2533
2545
  targetId,
@@ -2558,38 +2570,26 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2558
2570
  peachExit(1)
2559
2571
  }
2560
2572
  }
2561
- // step 3: clear existing text then type through the visible keyboard.
2562
- // count the field's value, masked while secure. the node's described
2563
- // text is its placeholder while empty, and the keyboard spec's text
2564
- // follows only predictive candidates.
2565
- const drawnCharacters = async () => {
2566
- const count: unknown = await bridge.send({
2567
- type: 'evaluate',
2568
- code: `(async () => {
2569
- const text = await window.__sootsimTest?.getTextInputValue(${tiArg})
2570
- return text ? [...new Intl.Segmenter().segment(text)].length : 0
2571
- })()`,
2572
- })
2573
- if (typeof count !== 'number') {
2574
- console.error(` could not read the current text of ${targetId}`)
2575
- peachExit(1)
2576
- }
2577
- return count
2578
- }
2579
- const existingCharacters = await drawnCharacters()
2580
- for (let i = 0; i < existingCharacters; i++) {
2581
- await bridge.send({ type: 'keyboard', action: 'press', text: 'delete' })
2582
- }
2583
- // backspace clears only what precedes the caret the tap placed
2584
- if (existingCharacters > 0 && (await drawnCharacters()) > 0) {
2585
- console.error(
2586
- ` type-into could not clear ${targetId}: text remains after the caret the tap placed`,
2587
- )
2573
+ // type at the live selection through the same guarded operation as
2574
+ // chained and cloud typing; clearing first changes controlled values.
2575
+ const typed = await performKeys(
2576
+ bridge,
2577
+ [{ type: 'typeInto', text, nodeId }],
2578
+ 'type-into',
2579
+ )
2580
+ const receipt: unknown = typed.steps[0]?.value
2581
+ if (
2582
+ !receipt ||
2583
+ typeof receipt !== 'object' ||
2584
+ typeof Reflect.get(receipt, 'secureTextEntry') !== 'boolean'
2585
+ ) {
2586
+ console.error(' type-into did not return its matched input secure state')
2588
2587
  peachExit(1)
2589
2588
  }
2590
- await bridge.send({ type: 'keyboard', action: 'type', text })
2591
2589
  const secureTextEntry =
2592
- targetSecureTextEntry || isSecureKeyboardState(keyboardState as any)
2590
+ targetSecureTextEntry ||
2591
+ Reflect.get(receipt, 'secureTextEntry') === true ||
2592
+ isSecureKeyboardState(keyboardState as any)
2593
2593
  const outputText = secureTextEntry
2594
2594
  ? SECURE_TEXT_REDACTION
2595
2595
  : redactTextForKeyboardState(text, keyboardState as any)
@@ -2629,7 +2629,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2629
2629
  peachExit(1)
2630
2630
  }
2631
2631
  if (bridge.plane === 'cloud') {
2632
- await performCloudKeys(bridge, [{ type: 'type', text }], 'type')
2632
+ await performKeys(bridge, [{ type: 'type', text }], 'type')
2633
2633
  await recordInspectAction(
2634
2634
  'inspect type',
2635
2635
  { inputText: text },
@@ -2663,7 +2663,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2663
2663
  peachExit(1)
2664
2664
  }
2665
2665
  if (bridge.plane === 'cloud') {
2666
- await performCloudKeys(bridge, [{ type: 'key', key: name }], 'key')
2666
+ await performKeys(bridge, [{ type: 'key', key: name }], 'key')
2667
2667
  } else {
2668
2668
  await requireVisualKeyboard(bridge, 'key')
2669
2669
  await bridge.send({ type: 'keyboard', action: 'press', text: name })
@@ -2680,7 +2680,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2680
2680
  peachExit(1)
2681
2681
  }
2682
2682
  if (bridge.plane === 'cloud') {
2683
- await performCloudKeys(
2683
+ await performKeys(
2684
2684
  bridge,
2685
2685
  keys.map((key) => ({ type: 'key' as const, key })),
2686
2686
  'key-sequence',
@@ -3717,13 +3717,17 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3717
3717
  // wrap in async to support await in worker mode. no identifier
3718
3718
  // rewriting — the snippet runs verbatim so tokens like `root`,
3719
3719
  // `debug`, `test` refer to whatever the user actually typed.
3720
- // multi-statement snippets need a block body: the bare expression
3721
- // form terminates at the first `;` (Unexpected token).
3720
+ // compile without executing to distinguish expressions from statements;
3721
+ // punctuation inside callbacks, literals and comments is not a separator.
3722
3722
  let expandedCode = code
3723
3723
  if (!expandedCode.startsWith('(async')) {
3724
- expandedCode = /[;\n]/.test(expandedCode)
3725
- ? `(async () => { ${expandedCode} })()`
3726
- : `(async () => ${expandedCode})()`
3724
+ expandedCode = `(async () => (\n${code}\n))()`
3725
+ try {
3726
+ new Script(expandedCode)
3727
+ } catch (error) {
3728
+ if (!(error instanceof SyntaxError)) throw error
3729
+ expandedCode = `(async () => {\n${code}\n})()`
3730
+ }
3727
3731
  }
3728
3732
  const result = await bridge.send({ type: 'evaluate', code: expandedCode })
3729
3733
  printJson(result)
@@ -3959,11 +3963,11 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3959
3963
  case 'start': {
3960
3964
  const result = await bridge.send({
3961
3965
  type: 'evaluate',
3962
- code: `(() => {
3966
+ code: `(async () => {
3963
3967
  if (!window.__sootsimShellPerf) {
3964
3968
  return { error: 'shell frame profile unavailable (__sootsimShellPerf missing on the page)' }
3965
3969
  }
3966
- window.__sootsimShellPerf.start()
3970
+ await window.__sootsimShellPerf.start()
3967
3971
  return { started: true }
3968
3972
  })()`,
3969
3973
  })
@@ -4038,7 +4042,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
4038
4042
  if (!window.__sootsimShellPerf) {
4039
4043
  return { error: 'shell frame profile unavailable (__sootsimShellPerf missing on the page)' }
4040
4044
  }
4041
- window.__sootsimShellPerf.start()
4045
+ await window.__sootsimShellPerf.start()
4042
4046
  // give a frame for profiling to engage before the event
4043
4047
  await new Promise(r => requestAnimationFrame(() => r(undefined)))
4044
4048
  window.dispatchEvent(new Event('${eventName}'))
@@ -4626,7 +4630,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
4626
4630
  // re-incurs the full per-command timeout. three sequential probes turn a
4627
4631
  // single 15s timeout into ~60s of apparent hang (QA F19-1). there is
4628
4632
  // nothing useful to fetch from a sim that just timed out, so skip them.
4629
- const timedOut = /^command timed out after (\d+)s$/.exec(message)
4633
+ const timedOut = /^command timed out after (\d+(?:\.\d+)?)s$/.exec(message)
4630
4634
  // a disconnected bridge is genuinely gone; recover is right and probing
4631
4635
  // it would just re-incur a full timeout. no bridge at all means there is
4632
4636
  // no sim to recover, only one to open.
@@ -528,7 +528,7 @@ async function runRemoteBox(
528
528
  } else if (existingSession) {
529
529
  await closeCloudSession(existingSession)
530
530
  }
531
- const created = await box.createPrebuiltBundleBox({
531
+ const { box: created, appFontsWireSha256 } = await box.createPrebuiltBundleBox({
532
532
  bundle: new Uint8Array(artifact.bytes),
533
533
  nativeProjectDir: input.nativeProjectDir,
534
534
  device: selectedDevice,
@@ -546,6 +546,7 @@ async function runRemoteBox(
546
546
  apiOrigin: endpoint,
547
547
  platform,
548
548
  device: selectedDevice,
549
+ appFontsWireSha256,
549
550
  artifact: {
550
551
  sha256: artifactSha256,
551
552
  bytes: artifact.bytes.length,
@@ -88,7 +88,7 @@ export async function runPreview(args: string[]): Promise<number> {
88
88
  bundleSizeBytes: artifact.bytes.byteLength,
89
89
  bundleContentType: 'application/javascript',
90
90
  bundleEncoding: 'br',
91
- bundleMinified: true,
91
+ bundleMinified: artifact.minified,
92
92
  bundleOrigin: null,
93
93
  entry: null,
94
94
  isTransformed: false,