peachsim 0.1.1620 → 0.1.1635

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 (185) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +45 -6
  3. package/cli/app-config.ts +4 -1
  4. package/cli/app-fonts.ts +39 -18
  5. package/cli/app-stubs.ts +224 -9
  6. package/cli/bridge-flow-runner.ts +248 -156
  7. package/cli/cloud-client.ts +68 -57
  8. package/cli/command-registry.ts +1 -0
  9. package/cli/commands/box/search-command.ts +3 -2
  10. package/cli/commands/box.ts +10 -29
  11. package/cli/commands/control.ts +391 -92
  12. package/cli/commands/doctor.ts +17 -184
  13. package/cli/commands/flow.ts +79 -11
  14. package/cli/commands/inspect/core.ts +10 -5
  15. package/cli/commands/inspect/settling.ts +71 -22
  16. package/cli/commands/inspect/wait-ready.ts +12 -0
  17. package/cli/commands/inspect.ts +26 -3
  18. package/cli/commands/install-desktop.ts +1 -0
  19. package/cli/commands/maestro-compare.ts +105 -67
  20. package/cli/commands/maestro.ts +9 -1
  21. package/cli/commands/platform.ts +71 -36
  22. package/cli/commands/record.ts +21 -1
  23. package/cli/commands/screenshot.ts +23 -1
  24. package/cli/commands/serve.ts +7 -2
  25. package/cli/commands/setup.ts +19 -42
  26. package/cli/commands/swift.ts +5 -0
  27. package/cli/commands/upload.ts +51 -8
  28. package/cli/desktop-companion.ts +102 -17
  29. package/cli/dev-build-freshness.ts +37 -2
  30. package/cli/drivers/desktop.ts +14 -1
  31. package/cli/drivers/playwright-sim-host.ts +44 -8
  32. package/cli/drivers/playwright.ts +9 -8
  33. package/cli/drivers/types.ts +5 -0
  34. package/cli/maestro-accessibility.ts +53 -0
  35. package/cli/maestro-compare.ts +108 -12
  36. package/cli/main.ts +2 -1
  37. package/cli/outbound-endpoints.ts +35 -6
  38. package/cli/parent-pid.ts +0 -9
  39. package/cli/pause-aware-timeout.ts +102 -0
  40. package/cli/remote-simulators.ts +112 -0
  41. package/cli/remoteSimulatorContract.ts +21 -0
  42. package/cli/scene-video.ts +89 -0
  43. package/cli/ws-bridge.ts +23 -14
  44. package/detox/attempt-events.cjs +4 -0
  45. package/detox/expectations.ts +5 -1
  46. package/detox/index.ts +241 -181
  47. package/detox/motion-change.cjs +73 -0
  48. package/detox/motion-change.d.cts +21 -0
  49. package/detox/navigation-lifecycle.ts +22 -4
  50. package/dist-lib/agent-daemon-client.cjs +3 -2
  51. package/dist-lib/agent-events.cjs +1 -1
  52. package/dist-lib/attached-projects.cjs +1 -1
  53. package/dist-lib/auth/shared-session.cjs +3 -2
  54. package/dist-lib/backend-origin.cjs +1 -1
  55. package/dist-lib/beta.cjs +1 -1
  56. package/dist-lib/beta.mjs +1 -1
  57. package/dist-lib/bridge-constants.cjs +13 -1
  58. package/dist-lib/bridge-contract-input.cjs +1 -1
  59. package/dist-lib/bridge-contract-input.mjs +1 -1
  60. package/dist-lib/bridge-contract.cjs +14 -8
  61. package/dist-lib/bridge-contract.mjs +13 -7
  62. package/dist-lib/bundle-channel.cjs +10480 -7962
  63. package/dist-lib/capture-contract.cjs +1 -1
  64. package/dist-lib/capture-contract.mjs +1 -1
  65. package/dist-lib/cloud-contract.cjs +1 -1
  66. package/dist-lib/cloud-contract.mjs +1 -1
  67. package/dist-lib/cloud-test-args.cjs +1 -1
  68. package/dist-lib/cloud-test-args.mjs +1 -1
  69. package/dist-lib/cloud.cjs +3 -2
  70. package/dist-lib/cloud.mjs +3 -2
  71. package/dist-lib/config.cjs +10 -6
  72. package/dist-lib/config.mjs +10 -6
  73. package/dist-lib/detox/index.cjs +1854 -966
  74. package/dist-lib/dev-bundle-resolution.cjs +64 -24
  75. package/dist-lib/expo-swift-bundle.cjs +8715 -6271
  76. package/dist-lib/expo-swift-metadata.cjs +7 -4
  77. package/dist-lib/expo-swift-metadata.mjs +7 -4
  78. package/dist-lib/home-paths.cjs +32 -3
  79. package/dist-lib/host/bridge-host.cjs +10135 -7060
  80. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  81. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  82. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  83. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  84. package/dist-lib/host/websocket-proxy.cjs +1 -1
  85. package/dist-lib/index.cjs +1843 -408
  86. package/dist-lib/jump-to-source-babel.cjs +1 -1
  87. package/dist-lib/jump-to-source-native.cjs +1 -1
  88. package/dist-lib/kotlin-host.cjs +1 -1
  89. package/dist-lib/menu.cjs +3 -2
  90. package/dist-lib/menu.mjs +3 -2
  91. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  92. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  93. package/dist-lib/metro-production-bundle.cjs +5 -4
  94. package/dist-lib/metro-production-bundle.mjs +5 -4
  95. package/dist-lib/metro.cjs +1182 -418
  96. package/dist-lib/native-app-identity-project.cjs +782 -23
  97. package/dist-lib/native-app-identity-project.mjs +781 -23
  98. package/dist-lib/native-app-identity.cjs +276 -9
  99. package/dist-lib/native-app-identity.mjs +273 -8
  100. package/dist-lib/native-firebase-metadata.cjs +5 -4
  101. package/dist-lib/native-firebase-metadata.mjs +5 -4
  102. package/dist-lib/plist.cjs +1 -1
  103. package/dist-lib/plist.mjs +1 -1
  104. package/dist-lib/profiles.cjs +1 -1
  105. package/dist-lib/public-brand.cjs +3 -2
  106. package/dist-lib/react-native-host-modules.cjs +8 -1
  107. package/dist-lib/react-native-host-modules.mjs +8 -1
  108. package/dist-lib/scripts/dev-server-scanner.cjs +106 -28
  109. package/dist-lib/sdk.cjs +115 -34
  110. package/dist-lib/sdk.mjs +115 -33
  111. package/dist-lib/skills.cjs +15274 -9904
  112. package/dist-lib/swift-compile.cjs +1165 -173
  113. package/dist-lib/swift-compile.mjs +1212 -203
  114. package/dist-lib/swift-package.cjs +12 -3
  115. package/dist-lib/swift-package.mjs +12 -3
  116. package/dist-lib/swift-submit.cjs +7 -1
  117. package/dist-lib/swift-submit.mjs +5 -1
  118. package/dist-lib/swift-toolchain.cjs +133 -18
  119. package/dist-lib/swift.cjs +1 -1
  120. package/dist-lib/vite-swift.cjs +2648 -862
  121. package/dist-lib/vite.cjs +70 -14
  122. package/dist-lib/worklet-executable.cjs +25 -2
  123. package/dist-lib/worklet-executable.mjs +25 -2
  124. package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
  125. package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
  126. package/package.json +2 -2
  127. package/scripts/dev-server-scanner.ts +35 -14
  128. package/skills/peach-perf/SKILL.md +11 -5
  129. package/skills/peach-setup/SKILL.md +16 -19
  130. package/src/admitted-mac-build.ts +34 -8
  131. package/src/android-app-identity.ts +146 -0
  132. package/src/android-autolinking.ts +75 -0
  133. package/src/android-string-value.ts +36 -0
  134. package/src/android-strings.ts +5 -36
  135. package/src/androidAutolinkingContract.ts +40 -0
  136. package/src/bridge-constants.ts +11 -0
  137. package/src/bridge-contract.ts +22 -34
  138. package/src/bundleGlobalFooter.ts +26 -7
  139. package/src/codex-client.ts +3 -2
  140. package/src/config.ts +15 -0
  141. package/src/desktop-host-delivery.ts +5 -1
  142. package/src/dev-bundle-resolution.ts +60 -26
  143. package/src/expo-kotlin-host.ts +6 -36
  144. package/src/expo-manifest-body.ts +13 -0
  145. package/src/expo-swift-bundle.ts +17 -12
  146. package/src/expo-swift-host.ts +640 -192
  147. package/src/expo-swift-ready.ts +78 -0
  148. package/src/expoSwiftContract.ts +19 -0
  149. package/src/home-paths.ts +48 -2
  150. package/src/host/bridge-host.ts +12 -57
  151. package/src/host/staged-host-files.ts +29 -2
  152. package/src/installed-package-versions.ts +54 -14
  153. package/src/launchMetadataContract.ts +24 -0
  154. package/src/metro-plugin.ts +37 -42
  155. package/src/native-app-identity-project.ts +142 -5
  156. package/src/native-app-identity.ts +176 -11
  157. package/src/native-firebase-config.ts +209 -13
  158. package/src/nitro-compile.ts +2 -5
  159. package/src/nitro-source.ts +147 -63
  160. package/src/process-group.ts +114 -61
  161. package/src/process-output.ts +0 -9
  162. package/src/public-brand.ts +1 -0
  163. package/src/react-native-bridge-modules.ts +416 -0
  164. package/src/react-native-host-modules.ts +7 -0
  165. package/src/sim-client.ts +6 -4
  166. package/src/swift-binary-modules.ts +2 -2
  167. package/src/swift-compile.ts +1007 -108
  168. package/src/swift-compiler-digest.ts +14 -0
  169. package/src/swift-distribution.lock.json +40 -12
  170. package/src/swift-distribution.ts +61 -9
  171. package/src/swift-host.ts +149 -56
  172. package/src/swift-icu.ts +14 -6
  173. package/src/swift-objc-sdk.ts +133 -10
  174. package/src/swift-objc-standin.ts +38 -3
  175. package/src/swift-package-manifest.ts +21 -6
  176. package/src/swift-package.ts +15 -1
  177. package/src/swift-packages.ts +119 -60
  178. package/src/swift-standin-emit.ts +57 -9
  179. package/src/swift-standins.ts +185 -25
  180. package/src/swift-submit.ts +10 -0
  181. package/src/swift-toolchain.ts +149 -23
  182. package/src/vite-plugin.ts +20 -20
  183. package/src/worklet-executable.ts +30 -3
  184. package/src/worklets-babel.ts +35 -14
  185. package/src/xcode-project.ts +10 -0
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1635 | (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;
@@ -319,7 +319,7 @@ function hasOwnKeys(value) {
319
319
  }
320
320
  function hasPeachConfig(config) {
321
321
  if (!config) return false;
322
- return config.nativeRenderer !== void 0 || typeof config.runtimeVersion === "string" && config.runtimeVersion.length > 0 || hasOwnKeys(config.modules) || hasOwnKeys(config.turboModules) || hasOwnKeys(config.nativeModules) || hasOwnKeys(config.nativeViews) || hasOwnKeys(config.env) || hasOwnKeys(config.network?.remap) || hasOwnKeys(config.settings) || hasOwnKeys(config.initialState);
322
+ return config.nativeRenderer !== void 0 || config.mapKit !== void 0 || typeof config.runtimeVersion === "string" && config.runtimeVersion.length > 0 || hasOwnKeys(config.modules) || hasOwnKeys(config.turboModules) || hasOwnKeys(config.nativeModules) || hasOwnKeys(config.nativeViews) || hasOwnKeys(config.env) || hasOwnKeys(config.network?.remap) || hasOwnKeys(config.settings) || hasOwnKeys(config.initialProps) || hasOwnKeys(config.initialState);
323
323
  }
324
324
  function applyPeachConfigToUrl(url, config) {
325
325
  const parsed = new URL(url);
@@ -331,18 +331,16 @@ function applyPeachConfigToUrl(url, config) {
331
331
  return parsed.toString();
332
332
  }
333
333
 
334
- // src/kotlinDiscoveryContract.ts
335
- var KOTLIN_DISCOVERY_PATH = "/__peach/kotlin-host";
336
- var KOTLIN_GUEST_PATH = "/__peach/kotlin-guest.js";
337
- function parseKotlinHostDiscovery(value) {
338
- if (!value || typeof value !== "object") return null;
339
- if (!("framework" in value) || value.framework !== "kotlin" || !("projectName" in value) || typeof value.projectName !== "string" || !("status" in value) || value.status !== "compiling" && value.status !== "ready" && value.status !== "error")
340
- return null;
341
- return {
342
- framework: value.framework,
343
- projectName: value.projectName,
344
- status: value.status
345
- };
334
+ // src/expo-manifest-body.ts
335
+ function parseExpoManifestBody(body) {
336
+ const opening = /^(--[^\r\n]+)\r?\n/.exec(body);
337
+ if (!opening) return JSON.parse(body);
338
+ for (const part of body.split(opening[1])) {
339
+ const headerEnd = part.search(/\r?\n\r?\n/);
340
+ if (headerEnd < 0 || !/\bname="manifest"/.test(part.slice(0, headerEnd))) continue;
341
+ return JSON.parse(part.slice(headerEnd).trim());
342
+ }
343
+ throw new Error("Expo manifest response has no manifest part");
346
344
  }
347
345
 
348
346
  // src/native-dev-bundle-url.ts
@@ -389,6 +387,75 @@ function normalizeNativeDevBundleUrl(bundleUrl) {
389
387
  }
390
388
  }
391
389
 
390
+ // src/dev-bundle-resolution.ts
391
+ function shouldProxyLoopbackProbe(targetUrl) {
392
+ if (typeof window === "undefined") return false;
393
+ try {
394
+ const parsed = new URL(targetUrl);
395
+ return isLoopbackHost(parsed.hostname) && isLoopbackHost(window.location.hostname) && parsed.origin !== window.location.origin;
396
+ } catch {
397
+ return false;
398
+ }
399
+ }
400
+ async function fetchDevProbe(targetUrl, init) {
401
+ const requestInit = {
402
+ ...init,
403
+ cache: init?.cache ?? "no-store"
404
+ };
405
+ if (!shouldProxyLoopbackProbe(targetUrl)) {
406
+ return fetch(targetUrl, requestInit);
407
+ }
408
+ return fetch(`/__fetch-proxy?url=${encodeURIComponent(targetUrl)}`, requestInit);
409
+ }
410
+ var MANIFEST_PROBE_TIMEOUT_MS = 3e4;
411
+ var manifestRequests = /* @__PURE__ */ new Map();
412
+ async function fetchDevManifest(baseUrl) {
413
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
414
+ const existing = manifestRequests.get(normalizedBaseUrl);
415
+ if (existing) return (await existing)?.clone();
416
+ const pending = fetchDevProbe(`${normalizedBaseUrl}/`, {
417
+ headers: { "expo-platform": "ios" }
418
+ }).then(async (res) => {
419
+ const body = await res.arrayBuffer();
420
+ return new Response(body.byteLength ? body : null, {
421
+ status: res.status,
422
+ headers: res.headers
423
+ });
424
+ }).catch(() => void 0);
425
+ let probeDeadline;
426
+ const request = Promise.race([
427
+ pending,
428
+ new Promise((resolve2) => {
429
+ probeDeadline = setTimeout(() => resolve2("timeout"), MANIFEST_PROBE_TIMEOUT_MS);
430
+ })
431
+ ]).then((answered) => {
432
+ if (answered === "timeout")
433
+ throw new Error(
434
+ `Metro manifest request timed out after ${MANIFEST_PROBE_TIMEOUT_MS / 1e3}s: ${normalizedBaseUrl}/. Wait for the dev server to finish starting or check its manifest endpoint.`
435
+ );
436
+ return answered;
437
+ }).finally(() => {
438
+ clearTimeout(probeDeadline);
439
+ manifestRequests.delete(normalizedBaseUrl);
440
+ });
441
+ manifestRequests.set(normalizedBaseUrl, request);
442
+ return (await request)?.clone();
443
+ }
444
+
445
+ // src/kotlinDiscoveryContract.ts
446
+ var KOTLIN_DISCOVERY_PATH = "/__peach/kotlin-host";
447
+ var KOTLIN_GUEST_PATH = "/__peach/kotlin-guest.js";
448
+ function parseKotlinHostDiscovery(value) {
449
+ if (!value || typeof value !== "object") return null;
450
+ if (!("framework" in value) || value.framework !== "kotlin" || !("projectName" in value) || typeof value.projectName !== "string" || !("status" in value) || value.status !== "compiling" && value.status !== "ready" && value.status !== "error")
451
+ return null;
452
+ return {
453
+ framework: value.framework,
454
+ projectName: value.projectName,
455
+ status: value.status
456
+ };
457
+ }
458
+
392
459
  // src/oneWidgetContract.ts
393
460
  var HOME_WIDGET_FAMILIES = [
394
461
  "systemSmall",
@@ -1138,6 +1205,28 @@ function httpGet(hostname, port, path, method = "GET", timeout = TIMEOUT_MS, hea
1138
1205
  });
1139
1206
  });
1140
1207
  }
1208
+ async function probeManifest(hostname, port, timeout) {
1209
+ let deadline;
1210
+ try {
1211
+ return await Promise.race([
1212
+ fetchDevManifest(buildLoopbackProbeBaseUrl("http:", hostname, port)).then(
1213
+ async (res) => res ? {
1214
+ statusCode: res.status,
1215
+ body: await res.text(),
1216
+ contentType: res.headers.get("content-type") || "",
1217
+ hostname
1218
+ } : null
1219
+ ),
1220
+ new Promise((resolve2) => {
1221
+ deadline = setTimeout(() => resolve2(null), timeout);
1222
+ })
1223
+ ]);
1224
+ } catch {
1225
+ return null;
1226
+ } finally {
1227
+ clearTimeout(deadline);
1228
+ }
1229
+ }
1141
1230
  var FALLBACK_PORTS = [
1142
1231
  8081,
1143
1232
  8082,
@@ -1485,7 +1574,7 @@ async function resolveLocalIconPath(hostname, port, rawIcon) {
1485
1574
  function applyManifest(result, manifestRes, buildIconProxyUrl) {
1486
1575
  if (!manifestRes) return result;
1487
1576
  try {
1488
- const manifest = JSON.parse(manifestRes.body);
1577
+ const manifest = parseExpoManifestBody(manifestRes.body);
1489
1578
  const client = manifest?.extra?.expoClient || manifest?.extra || {};
1490
1579
  if (client.name) result.projectName = client.name;
1491
1580
  if (typeof client.primaryColor === "string") result.primaryColor = client.primaryColor;
@@ -1579,16 +1668,7 @@ function enrichWithManifest(result, buildIconProxyUrl) {
1579
1668
  }
1580
1669
  async function fetchManifestEnrichment(result, buildIconProxyUrl) {
1581
1670
  const hostname = normalizeHostname(new URL(result.bundleUrl).hostname);
1582
- const manifestRes = await httpGet(
1583
- hostname,
1584
- result.port,
1585
- "/",
1586
- "GET",
1587
- ENRICH_TIMEOUT_MS,
1588
- {
1589
- "expo-platform": "ios"
1590
- }
1591
- );
1671
+ const manifestRes = await probeManifest(hostname, result.port, ENRICH_TIMEOUT_MS);
1592
1672
  if (!manifestRes || manifestRes.statusCode !== 200) return;
1593
1673
  const parsed = applyManifest(
1594
1674
  {
@@ -1637,9 +1717,7 @@ async function probePort(port, buildIconProxyUrl, listeningProcess, verdict) {
1637
1717
  Promise.all([
1638
1718
  skipPatched ? Promise.resolve(null) : httpGet(probeHostname, port, "/__peachsim/"),
1639
1719
  httpGet(probeHostname, port, "/status"),
1640
- skipManifest ? Promise.resolve(null) : httpGet(probeHostname, port, "/", "GET", MANIFEST_TIMEOUT_MS, {
1641
- "expo-platform": "ios"
1642
- }),
1720
+ skipManifest ? Promise.resolve(null) : probeManifest(probeHostname, port, MANIFEST_TIMEOUT_MS),
1643
1721
  skipExpo ? Promise.resolve(null) : httpGet(probeHostname, port, "/_expo/status"),
1644
1722
  httpGet(probeHostname, port, SWIFT_DISCOVERY_PATH),
1645
1723
  httpGet(probeHostname, port, KOTLIN_DISCOVERY_PATH),
package/dist-lib/sdk.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1620 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1635 | (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;
@@ -87,7 +87,6 @@ __export(sdk_exports, {
87
87
  layoutGetNativeEval: () => layoutGetNativeEval,
88
88
  rankInteractive: () => rankInteractive,
89
89
  readyProbeHasContent: () => readyProbeHasContent,
90
- readyProbeHasTargetContent: () => readyProbeHasTargetContent,
91
90
  readyProbeIsReady: () => readyProbeIsReady,
92
91
  resolveFindMode: () => resolveFindMode,
93
92
  resolveMaxMsFlag: () => resolveMaxMsFlag,
@@ -112,14 +111,20 @@ module.exports = __toCommonJS(sdk_exports);
112
111
  // src/bridge-contract.ts
113
112
  var SEMANTIC_READY_CONTENT_NODE_FLOOR = 100;
114
113
  var HEADLESS_TENANT_FETCH_HUNG_MARKER = "headless tenant fetch hung";
115
- function readyProbeHasContent(probe) {
116
- return probe.paintedAppContent === true || (probe.browserSurfaceCount ?? 0) > 0 || probe.liveFrameActive || probe.targets > 0 || probe.nodes >= SEMANTIC_READY_CONTENT_NODE_FLOOR;
114
+ function readyProbeContentBasis(probe) {
115
+ if (probe.paintedAppContent === true) return "paintedAppContent";
116
+ if ((probe.browserSurfaceCount ?? 0) > 0) return "browserSurface";
117
+ if (probe.liveFrameActive) return "liveFrame";
118
+ if (probe.targets > 0) return "targets";
119
+ if (probe.flag === true && probe.nodes >= SEMANTIC_READY_CONTENT_NODE_FLOOR)
120
+ return "nodeFloor";
121
+ return "";
117
122
  }
118
- function readyProbeHasTargetContent(probe) {
119
- return probe.paintedAppContent === true || (probe.browserSurfaceCount ?? 0) > 0 || probe.liveFrameActive || probe.targets > 0;
123
+ function readyProbeHasContent(probe) {
124
+ return readyProbeContentBasis({ ...probe, flag: true }) !== "";
120
125
  }
121
126
  function readyProbeIsReady(probe) {
122
- return (probe.flag === true ? readyProbeHasContent(probe) : readyProbeHasTargetContent(probe)) && probe.externalReady !== false && !probe.externalError && !probe.loadingText;
127
+ return readyProbeContentBasis(probe) !== "" && probe.externalReady !== false && !probe.externalError && !probe.loadingText;
123
128
  }
124
129
 
125
130
  // src/bridge-contract-input.ts
@@ -969,7 +974,10 @@ function waitReadyStatus(result, ready) {
969
974
  externalError: probe?.externalError ?? "",
970
975
  suppressedEntryError: probe?.suppressedEntryError ?? "",
971
976
  errors: probe?.errors ?? 0,
972
- bridgeError: result.error ?? ""
977
+ bridgeError: result.error ?? "",
978
+ paintedAppContent: probe?.paintedAppContent ?? false,
979
+ treeQuietMs: probe?.treeQuietMs ?? 0,
980
+ readyBasis: probe ? readyProbeContentBasis(probe) : ""
973
981
  };
974
982
  }
975
983
  async function inspectWaitSelector(bridge, testId, timeoutMs = 5e3, opts = {}) {
@@ -1585,6 +1593,28 @@ async function resolveTargetCoords(bridge, target) {
1585
1593
  };
1586
1594
  }
1587
1595
 
1596
+ // cli/maestro-accessibility.ts
1597
+ function accessibilityBounds(node) {
1598
+ const left = Math.trunc(node.frame.x);
1599
+ const top = Math.trunc(node.frame.y);
1600
+ return {
1601
+ x: left,
1602
+ y: top,
1603
+ width: Math.trunc(node.frame.x + node.frame.width) - left,
1604
+ height: Math.trunc(node.frame.y + node.frame.height) - top
1605
+ };
1606
+ }
1607
+ function sameNativeAccessibilityShape(a, b, bounds) {
1608
+ if ((a.testID ?? "") !== (b.testID ?? "") || (a.label ?? "") !== (b.label ?? "") || (a.placeholder ?? "") !== (b.placeholder ?? "") || (a.value ?? "") !== (b.value ?? "") || a.enabled !== b.enabled || a.selected !== b.selected || a.checked !== b.checked || a.clickable !== b.clickable || (a.hasFocus ?? false) !== (b.hasFocus ?? false) || JSON.stringify(bounds(a)) !== JSON.stringify(bounds(b)) || a.children.length !== b.children.length) {
1609
+ return false;
1610
+ }
1611
+ for (const [index, child] of a.children.entries()) {
1612
+ const other = b.children[index];
1613
+ if (!other || !sameNativeAccessibilityShape(child, other, bounds)) return false;
1614
+ }
1615
+ return true;
1616
+ }
1617
+
1588
1618
  // cli/commands/inspect/settling.ts
1589
1619
  var RuntimeReadinessUnsupportedError = class extends Error {
1590
1620
  constructor(runtimeVersion, missingFields) {
@@ -1617,6 +1647,7 @@ async function waitForSootsimIdle({
1617
1647
  stablePolls = 3,
1618
1648
  strict = false,
1619
1649
  paintStill = false,
1650
+ maestro = false,
1620
1651
  motionOnly = false,
1621
1652
  transitionStartWindowMs = 80,
1622
1653
  transitionTimeoutMs = 4e3
@@ -1648,12 +1679,15 @@ async function waitForSootsimIdle({
1648
1679
  const pollMs = ${Math.max(1, Math.round(pollMs))}
1649
1680
  const strict = ${strict ? "true" : "false"}
1650
1681
  const paintStill = ${paintStill ? "true" : "false"}
1682
+ const maestro = ${maestro ? "true" : "false"}
1683
+ const accessibilityBounds = ${accessibilityBounds.toString()}
1684
+ const sameNativeAccessibilityShape = ${sameNativeAccessibilityShape.toString()}
1651
1685
  const motionOnly = ${motionOnly ? "true" : "false"}
1652
1686
  // full mounted-geometry observation needs at least two hierarchy reads
1653
1687
  // plus the stillness window, which a sub-second budget cannot afford:
1654
1688
  // tight budgets (auto-wait) settle on worker stillness plus root
1655
1689
  // membership, generous ones confirm the geometry too.
1656
- const fullGeometry = ${!motionOnly && maxMs >= 1e3 ? "true" : "false"}
1690
+ const fullGeometry = ${!maestro && !motionOnly && maxMs >= 1e3 ? "true" : "false"}
1657
1691
  const requiredStillMs = paintStill ? 0 : ${Math.max(1, Math.round(stablePolls)) * Math.max(1, Math.round(pollMs))}
1658
1692
  const minimumStablePolls = paintStill ? 1 : ${Math.min(2, Math.max(1, Math.round(stablePolls)))}
1659
1693
  const composite = paintStill ? window.SootSim?.bridges?.liveComposite : null
@@ -1775,6 +1809,9 @@ async function waitForSootsimIdle({
1775
1809
  // strict also waits out shell motion: a system alert, sheet or menu
1776
1810
  // animates in the shell worker while the app's own stats read still,
1777
1811
  // so its presentation is complete only once the shell's list empties.
1812
+ const accessibilityRead = maestro
1813
+ ? answer(() => window.__sootsimTest.captureAccessibilityTree()).catch(() => null)
1814
+ : null
1778
1815
  const motionRead = strict
1779
1816
  ? answer(() => window.__sootsimTest?.listAnimations?.()).catch(() => null)
1780
1817
  : null
@@ -1784,9 +1821,12 @@ async function waitForSootsimIdle({
1784
1821
  // the compositor rejects a stats request it is too busy to answer.
1785
1822
  const compositorRead = answer(() => window.__sootsimCompositor.getStats()).catch(() => BUSY)
1786
1823
  const countsRead = motionOnly ? null : answer(() => window.__sootsimTest?.getRequestCounts?.()).catch(() => null)
1787
- // mounted-root membership: the live node count moves while the tree
1788
- // mounts, unmounts or recycles, in either render mode.
1789
- const nodeCountRead = answer(() => window.__sootsimTest?.getNodeCount?.()).catch(() => null)
1824
+ // mounted-root membership: the count of nodes that can reach the
1825
+ // window moves while the screen mounts, unmounts or recycles, in
1826
+ // either render mode. a screen a pushed one covers or an inactive
1827
+ // tab stays mounted out of the window, so what it mounts is not
1828
+ // screen motion.
1829
+ const nodeCountRead = answer(() => window.__sootsimTest?.getWindowNodeCount?.()).catch(() => null)
1790
1830
  // the mounted hierarchy in render-worker mode comes from the test
1791
1831
  // bridge: window.__sootsimRoot is an empty placeholder there, so a
1792
1832
  // walk over it hashes every screen to the same empty string while
@@ -1816,7 +1856,7 @@ async function waitForSootsimIdle({
1816
1856
  const motion = await motionRead
1817
1857
  if (motion === BUSY) workerBusy = true
1818
1858
  else if (Array.isArray(motion)) {
1819
- const blocking = motion.filter((anim) => anim.realm === 'shell' && anim.priority !== 'low')
1859
+ const blocking = motion.filter((anim) => anim.realm === 'shell' && anim.priority !== 'low' && anim.visible !== false)
1820
1860
  shellAnimating = blocking.length > 0
1821
1861
  // name what holds the settle so a timeout points at its owner.
1822
1862
  shellBlockers = blocking
@@ -1848,6 +1888,8 @@ async function waitForSootsimIdle({
1848
1888
  stats.hasActiveAnims === true ||
1849
1889
  stats.hasActiveBlockingNativeAnimations === true ||
1850
1890
  (!paintStill && stats.hasPendingBlockingAnimationFrames === true)
1891
+ // the runtime reports only layout work that can reach the window:
1892
+ // a covered screen or inactive tab committing does not hold it.
1851
1893
  layoutDirty =
1852
1894
  stats.layoutDirty === true && stats.renderRequested === true
1853
1895
  // a freshly-pushed screen showing a skeleton is layout-stable
@@ -1880,6 +1922,11 @@ async function waitForSootsimIdle({
1880
1922
  const total = await nodeCountRead
1881
1923
  if (total === BUSY) workerBusy = true
1882
1924
  nodeCount = typeof total === 'number' ? total : null
1925
+ const accessibility = accessibilityRead ? await accessibilityRead : null
1926
+ const nativeHierarchy = Array.isArray(accessibility) && accessibility.length > 0
1927
+ ? accessibility
1928
+ : null
1929
+ if (accessibility === BUSY) workerBusy = true
1883
1930
  let hierarchy = hierarchyRead ? await hierarchyRead : null
1884
1931
  if (hierarchy === BUSY) {
1885
1932
  workerBusy = true
@@ -1939,9 +1986,15 @@ async function waitForSootsimIdle({
1939
1986
  webViewStarting = true
1940
1987
  webViewBlocker = 'preview progress element displayed'
1941
1988
  } else {
1942
- const unreadyWebView = document.querySelector(
1989
+ // a loading document with no area (an app's hidden worker or
1990
+ // executor frame) paints nothing, and a screenshot on device
1991
+ // does not wait for it.
1992
+ const unreadyWebView = [...document.querySelectorAll(
1943
1993
  '[data-sootsim-webview-loading="true"], [data-sootsim-webview-mode="placeholder"]'
1944
- )
1994
+ )].find((view) => {
1995
+ const frame = view.querySelector('iframe')
1996
+ return !frame || frame.clientWidth > 0 || frame.clientHeight > 0
1997
+ })
1945
1998
  if (unreadyWebView) {
1946
1999
  webViewStarting = true
1947
2000
  webViewBlocker = 'webView document still loading'
@@ -1960,7 +2013,7 @@ async function waitForSootsimIdle({
1960
2013
  pixelFrameToken = token
1961
2014
  }
1962
2015
  }
1963
- return { layout: nodes.join(';'), screenPixels, animating, shellAnimating, shellBlockers, layoutDirty, pendingFetches, workerBusy, webViewStarting, webViewBlocker, renderStatsAvailable, requestTotal, requestInFlight, appCommits, nodeCount }
2016
+ return { layout: nodes.join(';'), nativeHierarchy, screenPixels, animating, shellAnimating, shellBlockers, layoutDirty, pendingFetches, workerBusy, webViewStarting, webViewBlocker, renderStatsAvailable, requestTotal, requestInFlight, appCommits, nodeCount }
1964
2017
  }
1965
2018
 
1966
2019
  // how long generic background network is allowed to keep us waiting AFTER
@@ -1974,6 +2027,7 @@ async function waitForSootsimIdle({
1974
2027
  // so the generic request counters are a courtesy, not a gate: honor them
1975
2028
  // only within this grace once the screen is visually still.
1976
2029
  const networkQuietGraceMs = 1200
2030
+ let lastNativeHierarchy = null
1977
2031
  let lastLayout = ''
1978
2032
  let lastScreenPixels = null
1979
2033
  let screenChangedPolls = 0
@@ -2001,6 +2055,9 @@ async function waitForSootsimIdle({
2001
2055
  }
2002
2056
  return true
2003
2057
  }
2058
+ const hierarchyUnchanged = (current, previous) =>
2059
+ current !== null && previous !== null && current.length === previous.length &&
2060
+ current.every((node, index) => sameNativeAccessibilityShape(node, previous[index], accessibilityBounds))
2004
2061
  const IDLE_HELD = { idleHeld: true }
2005
2062
  while (true) {
2006
2063
  if (clockNow() >= deadline) {
@@ -2046,7 +2103,7 @@ async function waitForSootsimIdle({
2046
2103
  // the loop head drains a transition still in flight before it ends.
2047
2104
  if (snapshot.workerBusy && clockNow() >= deadline && report) continue
2048
2105
  polls++
2049
- report = { snapshot, layout: lastLayout, screenPixels: lastScreenPixels, requestTotal: lastRequestTotal, appCommits: lastAppCommits, nodeCount: lastNodeCount }
2106
+ report = { snapshot, nativeHierarchy: lastNativeHierarchy, layout: lastLayout, screenPixels: lastScreenPixels, requestTotal: lastRequestTotal, appCommits: lastAppCommits, nodeCount: lastNodeCount }
2050
2107
  if (snapshot.layoutDirty) dirtyPolls++
2051
2108
  if (snapshot.animating || snapshot.shellAnimating) animatingPolls++
2052
2109
  if (snapshot.webViewStarting) webViewStartingPolls++
@@ -2067,7 +2124,15 @@ async function waitForSootsimIdle({
2067
2124
  // strictOk and adds shell motion plus the blocking-stats floor. a
2068
2125
  // null count or commit reading means the runtime does not report
2069
2126
  // it, which reads as stable the way a missing stats block does.
2070
- const visuallyStill =
2127
+ // native screen checks observe output, not animation ownership. after
2128
+ // its 3 s pixel phase, iOS accepts two equal native TreeNodes even
2129
+ // while paint-only playback continues. overlap hierarchy reads so
2130
+ // neither the gesture nor screenshot deadline grows.
2131
+ const maestroStill = snapshot.screenPixels !== null && !snapshot.workerBusy && !snapshot.webViewStarting && (
2132
+ !screenChanged || (clockNow() - start >= 3000 &&
2133
+ hierarchyUnchanged(snapshot.nativeHierarchy, lastNativeHierarchy))
2134
+ )
2135
+ const visuallyStill = maestro ? maestroStill :
2071
2136
  strictOk &&
2072
2137
  !snapshot.animating &&
2073
2138
  !snapshot.layoutDirty &&
@@ -2095,7 +2160,7 @@ async function waitForSootsimIdle({
2095
2160
  // grace (continuous background traffic must not block forever).
2096
2161
  if (
2097
2162
  stillProven() &&
2098
- (motionOnly || networkQuiet || clockNow() - visuallyStillSince >= networkQuietGraceMs)
2163
+ (maestro || motionOnly || networkQuiet || clockNow() - visuallyStillSince >= networkQuietGraceMs)
2099
2164
  ) {
2100
2165
  if (transitionWait) {
2101
2166
  await transitionWait
@@ -2107,6 +2172,7 @@ async function waitForSootsimIdle({
2107
2172
  stillPolls = []
2108
2173
  visuallyStillSince = 0
2109
2174
  }
2175
+ lastNativeHierarchy = snapshot.nativeHierarchy
2110
2176
  lastLayout = snapshot.layout
2111
2177
  lastScreenPixels = snapshot.screenPixels
2112
2178
  lastRequestTotal = snapshot.requestTotal
@@ -2129,20 +2195,21 @@ async function waitForSootsimIdle({
2129
2195
  }
2130
2196
  const last = report.snapshot
2131
2197
  const blockers = []
2132
- if (strict && !last.renderStatsAvailable) blockers.push('render stats unavailable')
2133
- if (last.animating) blockers.push('animations still running')
2134
- if (last.shellAnimating) blockers.push('shell animations still running (' + last.shellBlockers + ')')
2135
- if (last.layoutDirty) blockers.push('layout still dirty')
2136
- if (last.appCommits !== null && report.appCommits !== null && last.appCommits !== report.appCommits) blockers.push('layout commits still landing')
2137
- if (last.nodeCount !== null && report.nodeCount !== null && last.nodeCount !== report.nodeCount) blockers.push('node count still changing')
2138
- if (!motionOnly && last.pendingFetches > 0) blockers.push(last.pendingFetches + ' image fetches pending')
2198
+ if (!maestro && strict && !last.renderStatsAvailable) blockers.push('render stats unavailable')
2199
+ if (!maestro && last.animating) blockers.push('animations still running')
2200
+ if (!maestro && last.shellAnimating) blockers.push('shell animations still running (' + last.shellBlockers + ')')
2201
+ if (!maestro && last.layoutDirty) blockers.push('layout still dirty')
2202
+ if (!maestro && last.appCommits !== null && report.appCommits !== null && last.appCommits !== report.appCommits) blockers.push('layout commits still landing')
2203
+ if (!maestro && last.nodeCount !== null && report.nodeCount !== null && last.nodeCount !== report.nodeCount) blockers.push('node count still changing')
2204
+ if (!maestro && !motionOnly && last.pendingFetches > 0) blockers.push(last.pendingFetches + ' image fetches pending')
2139
2205
  if (last.workerBusy) blockers.push('a render worker too busy to report')
2140
2206
  if (last.webViewStarting) blockers.push(last.webViewBlocker || 'webView still starting')
2141
- if (last.layout !== report.layout) blockers.push('layout still changing')
2207
+ if (!maestro && last.layout !== report.layout) blockers.push('layout still changing')
2208
+ if (maestro && !hierarchyUnchanged(last.nativeHierarchy, report.nativeHierarchy)) blockers.push(last.nativeHierarchy === null ? 'native accessibility hierarchy unavailable' : 'native accessibility hierarchy still changing')
2142
2209
  if (paintStill && !last.screenPixels) blockers.push('screen pixels unavailable')
2143
2210
  else if (paintStill && !screenUnchanged(last.screenPixels, report.screenPixels)) blockers.push('screen pixels still changing')
2144
- if (!motionOnly && last.requestInFlight > 0) blockers.push(last.requestInFlight + ' requests in flight')
2145
- else if (!motionOnly && last.requestTotal !== report.requestTotal) blockers.push('new requests issued')
2211
+ if (!maestro && !motionOnly && last.requestInFlight > 0) blockers.push(last.requestInFlight + ' requests in flight')
2212
+ else if (!maestro && !motionOnly && last.requestTotal !== report.requestTotal) blockers.push('new requests issued')
2146
2213
  if (polls < 2) blockers.push('sim too busy to poll twice')
2147
2214
  // the last read is still: motion ended too near the deadline for the
2148
2215
  // stillness window to hold before it.
@@ -2184,8 +2251,10 @@ async function waitForSootsimIdle({
2184
2251
  async function waitReadyAndSettle(bridge, timeoutMs, options = {}) {
2185
2252
  const start = Date.now();
2186
2253
  const remaining = () => Math.max(0, start + timeoutMs - Date.now());
2254
+ let accepted = null;
2187
2255
  const result = (status, idle2) => ({
2188
2256
  status,
2257
+ accepted,
2189
2258
  idle: idle2,
2190
2259
  elapsedMs: Date.now() - start
2191
2260
  });
@@ -2194,6 +2263,7 @@ async function waitReadyAndSettle(bridge, timeoutMs, options = {}) {
2194
2263
  idle = null;
2195
2264
  const ready = await inspectWaitReady(bridge, remaining(), options);
2196
2265
  if (!ready.ready) return result(ready, idle);
2266
+ accepted = ready;
2197
2267
  idle = await waitForSootsimIdle({
2198
2268
  bridge,
2199
2269
  simId: options.simId,
@@ -2617,6 +2687,20 @@ async function openAppUrl(bridge, target) {
2617
2687
  return normalized;
2618
2688
  }
2619
2689
 
2690
+ // src/launchMetadataContract.ts
2691
+ var LAUNCH_METADATA_QUERY_PARAM = "launchMetadata";
2692
+ var LAUNCH_METADATA_KEYS = [
2693
+ "appFonts",
2694
+ "appSplash",
2695
+ "appCatalog",
2696
+ "nativeAppIdentity",
2697
+ "nativeFirebase",
2698
+ "appManifest",
2699
+ "packageVersions",
2700
+ "dotEnv",
2701
+ "env"
2702
+ ];
2703
+
2620
2704
  // src/sleep.ts
2621
2705
  function sleep(ms) {
2622
2706
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -2685,10 +2769,8 @@ var TARGET_SEARCH_PARAMS = [
2685
2769
  "open",
2686
2770
  "port",
2687
2771
  "inspectOpen",
2688
- "appFonts",
2689
- "appManifest",
2690
- "appSplash",
2691
- "appCatalog"
2772
+ ...LAUNCH_METADATA_KEYS,
2773
+ LAUNCH_METADATA_QUERY_PARAM
2692
2774
  ];
2693
2775
  function buildSimOpenUrl(currentUrl, target) {
2694
2776
  const url = new URL(currentUrl);
@@ -3144,7 +3226,6 @@ var SimClient = class {
3144
3226
  layoutGetNativeEval,
3145
3227
  rankInteractive,
3146
3228
  readyProbeHasContent,
3147
- readyProbeHasTargetContent,
3148
3229
  readyProbeIsReady,
3149
3230
  resolveFindMode,
3150
3231
  resolveMaxMsFlag,