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
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1406 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1465 | (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;
@@ -2273,7 +2273,7 @@ var init_configured_bundle_producer = __esm({
2273
2273
  // src/index.ts
2274
2274
  var src_exports = {};
2275
2275
  __export(src_exports, {
2276
- DEFAULT_SOOTSIM_BRIDGE_PORT: () => DEFAULT_SOOTSIM_BRIDGE_PORT,
2276
+ DEFAULT_SOOTSIM_BRIDGE_PORT: () => DEFAULT_SOOTSIM_BRIDGE_PORT2,
2277
2277
  DEFAULT_SOOTSIM_BRIDGE_URL: () => DEFAULT_SOOTSIM_BRIDGE_URL,
2278
2278
  DEFAULT_SOOTSIM_SHELL_URL: () => DEFAULT_SOOTSIM_SHELL_URL,
2279
2279
  RNX_SCREEN_CAPTURE_VERSION: () => RNX_SCREEN_CAPTURE_VERSION,
@@ -2302,11 +2302,31 @@ module.exports = __toCommonJS(src_exports);
2302
2302
  // cli/cloud-client.ts
2303
2303
  var import_node_crypto = require("node:crypto");
2304
2304
 
2305
+ // ../peach-engine/src/tenant-bundle-mode.ts
2306
+ var import_metro_production_bundle = require("peachsim/metro-production-bundle");
2307
+
2308
+ // ../peach-engine/src/development-mode.ts
2309
+ var bridgeConstants = __toESM(require("peachsim/bridge-constants"), 1);
2310
+
2311
+ // ../peach-engine/src/tenant-runtime-environment.ts
2312
+ function readBrowserLocation() {
2313
+ try {
2314
+ return typeof location === "undefined" ? null : location;
2315
+ } catch {
2316
+ return null;
2317
+ }
2318
+ }
2319
+ var nativeRuntimeLocation = readBrowserLocation();
2320
+
2321
+ // ../peach-engine/src/development-mode.ts
2322
+ var { DEFAULT_SOOTSIM_BRIDGE_PORT } = bridgeConstants;
2323
+ var DAEMON_RUNTIME_PORT_MAX = DEFAULT_SOOTSIM_BRIDGE_PORT + 10;
2324
+
2305
2325
  // ../peach-engine/src/metro-fingerprint-registry-client.ts
2306
2326
  var import_metro_fingerprint_registry = require("peachsim/metro-fingerprint-registry");
2307
2327
 
2308
2328
  // ../peach-engine/src/metro-fingerprint.ts
2309
- var import_metro_production_bundle = require("peachsim/metro-production-bundle");
2329
+ var import_metro_production_bundle2 = require("peachsim/metro-production-bundle");
2310
2330
  var import_react_native_host_modules = require("peachsim/react-native-host-modules");
2311
2331
 
2312
2332
  // cli/cloud-client.ts
@@ -5528,8 +5548,8 @@ var import_node_path3 = __toESM(require("node:path"), 1);
5528
5548
  var import_ws = require("ws");
5529
5549
 
5530
5550
  // src/bridge-constants.ts
5531
- var DEFAULT_SOOTSIM_BRIDGE_PORT = 7668;
5532
- var DEFAULT_SOOTSIM_BRIDGE_URL = `http://localhost:${DEFAULT_SOOTSIM_BRIDGE_PORT}/`;
5551
+ var DEFAULT_SOOTSIM_BRIDGE_PORT2 = 7668;
5552
+ var DEFAULT_SOOTSIM_BRIDGE_URL = `http://localhost:${DEFAULT_SOOTSIM_BRIDGE_PORT2}/`;
5533
5553
  function parsePort(value) {
5534
5554
  if (typeof value === "number") {
5535
5555
  return Number.isInteger(value) && value > 0 ? value : null;
@@ -5547,7 +5567,7 @@ function parsePortOffset(value) {
5547
5567
  function resolveSootsimBridgePort(input = {}) {
5548
5568
  const explicitPort = parsePort(input.explicitPort);
5549
5569
  if (explicitPort !== null) return explicitPort;
5550
- return DEFAULT_SOOTSIM_BRIDGE_PORT + parsePortOffset(input.portOffset);
5570
+ return DEFAULT_SOOTSIM_BRIDGE_PORT2 + parsePortOffset(input.portOffset);
5551
5571
  }
5552
5572
  var HOSTED_SHELL_PATH = new RegExp(`^(?:${PEACH_SIMULATOR_PATH}|/sootsim)(?:/|$)`);
5553
5573
  var AUTOMATION_SIM_IDLE_REAP_TTL_MS = 5 * 6e4;
@@ -5600,11 +5620,11 @@ function resolveDefaultDevBridgeLockfile() {
5600
5620
  });
5601
5621
  if (sameCheckout.length > 0) {
5602
5622
  return sameCheckout.find(
5603
- (lockfile) => lockfile.bridgePort === DEFAULT_SOOTSIM_BRIDGE_PORT
5623
+ (lockfile) => lockfile.bridgePort === DEFAULT_SOOTSIM_BRIDGE_PORT2
5604
5624
  ) ?? sameCheckout[0];
5605
5625
  }
5606
5626
  }
5607
- return lockfiles.find((lockfile) => lockfile.bridgePort === DEFAULT_SOOTSIM_BRIDGE_PORT) ?? null;
5627
+ return lockfiles.find((lockfile) => lockfile.bridgePort === DEFAULT_SOOTSIM_BRIDGE_PORT2) ?? null;
5608
5628
  }
5609
5629
  function resolveBridgeWorld() {
5610
5630
  const homeDir = rnxHomeDir();
@@ -5643,6 +5663,33 @@ function resolveBridgeWorldFresh() {
5643
5663
  shellBaseUrl: DEFAULT_SOOTSIM_SHELL_URL
5644
5664
  };
5645
5665
  }
5666
+ function worldForBridgePort(port) {
5667
+ const dev = liveDevBridgeLockfiles().find((lockfile) => lockfile.bridgePort === port);
5668
+ if (dev) {
5669
+ return {
5670
+ source: "dev-bridge",
5671
+ shellBaseUrl: dev.shellPort ? `http://localhost:${dev.shellPort}/` : DEFAULT_SOOTSIM_SHELL_URL
5672
+ };
5673
+ }
5674
+ const lock = readDaemonLockfile();
5675
+ if (lock && isDaemonLockfileFresh(lock) && lock.bridgePort === port) {
5676
+ return {
5677
+ source: "daemon",
5678
+ shellBaseUrl: lock.runtimePort > 0 ? `http://localhost:${lock.runtimePort}/` : DEFAULT_SOOTSIM_SHELL_URL
5679
+ };
5680
+ }
5681
+ return null;
5682
+ }
5683
+ function printBridgeWorldNotice(bridgePort, shellBaseUrl) {
5684
+ const world = worldForBridgePort(bridgePort);
5685
+ const kind = world?.source === "dev-bridge" ? "dev bridge" : world?.source === "daemon" ? "daemon bridge" : "bridge";
5686
+ const origin = world?.source === "dev-bridge" ? `dev-bridges/${bridgePort}.json` : world?.source === "daemon" ? "daemon.json" : "no lockfile";
5687
+ const shell = shellBaseUrl ?? world?.shellBaseUrl;
5688
+ process.stderr.write(
5689
+ ` \u2192 ${kind} :${bridgePort} (${shell ? `shell ${shell}, ` : ""}${origin})
5690
+ `
5691
+ );
5692
+ }
5646
5693
  var BridgeSimLockedError = class extends Error {
5647
5694
  lock;
5648
5695
  constructor(message, lock) {
@@ -5936,6 +5983,120 @@ function createBridge(wsPort, opts = {}) {
5936
5983
  }
5937
5984
  };
5938
5985
  }
5986
+ function createBridgeFromParsed(parsed) {
5987
+ const cloudBridge = createCloudBridgeForParsed(parsed);
5988
+ if (cloudBridge) return cloudBridge;
5989
+ const options = {
5990
+ commandTimeoutMs: parsed.commandTimeoutMs,
5991
+ simId: parsed.simId,
5992
+ simIdSource: parsed.simIdSource
5993
+ };
5994
+ if (!parsed.simId || parsed.simIdSource !== "flag") {
5995
+ return createBridge(parsed.wsPort, options);
5996
+ }
5997
+ let selected;
5998
+ let closed = false;
5999
+ const getBridge = () => {
6000
+ selected ??= resolveBridgePortForSim(parsed).then((port) => {
6001
+ if (closed) throw new Error("bridge closed during sim world resolution");
6002
+ parsed.wsPort = port;
6003
+ printBridgeWorldNotice(port);
6004
+ return createBridge(port, options);
6005
+ });
6006
+ return selected;
6007
+ };
6008
+ return {
6009
+ plane: "local",
6010
+ async send(cmd, opts) {
6011
+ return (await getBridge()).send(cmd, opts);
6012
+ },
6013
+ async listSims() {
6014
+ return (await getBridge()).listSims();
6015
+ },
6016
+ async resolveReloadedSim(id) {
6017
+ return (await getBridge()).resolveReloadedSim(id);
6018
+ },
6019
+ async focusSim(id) {
6020
+ return (await getBridge()).focusSim(id);
6021
+ },
6022
+ async closeSim(id) {
6023
+ return (await getBridge()).closeSim(id);
6024
+ },
6025
+ async claim(id, opts) {
6026
+ return (await getBridge()).claim(id, opts);
6027
+ },
6028
+ close() {
6029
+ closed = true;
6030
+ void selected?.then(
6031
+ (bridge) => bridge.close(),
6032
+ () => {
6033
+ }
6034
+ );
6035
+ }
6036
+ };
6037
+ }
6038
+ async function resolveBridgePortForSim(parsed) {
6039
+ const worlds = liveDevBridgeLockfiles().map((lock) => ({
6040
+ port: lock.bridgePort,
6041
+ record: `dev-bridges/${lock.bridgePort}.json`
6042
+ }));
6043
+ const daemon = readDaemonLockfile();
6044
+ if (daemon && isDaemonLockfileFresh(daemon)) {
6045
+ worlds.push({ port: daemon.bridgePort, record: "daemon.json" });
6046
+ }
6047
+ if (parsed.explicitPort && !worlds.some((world) => world.port === parsed.wsPort)) {
6048
+ worlds.push({ port: parsed.wsPort, record: "--port" });
6049
+ }
6050
+ const checked = await Promise.all(
6051
+ worlds.map(async (world) => {
6052
+ const probe = createBridge(world.port, {
6053
+ connectTimeoutMs: 2e3,
6054
+ commandTimeoutMs: 2e3
6055
+ });
6056
+ try {
6057
+ const sims = await probe.listSims();
6058
+ return {
6059
+ ...world,
6060
+ ownsSim: sims.some(
6061
+ (sim) => sim.id === parsed.simId && sim.readyState === "open"
6062
+ )
6063
+ };
6064
+ } catch (error) {
6065
+ return {
6066
+ ...world,
6067
+ ownsSim: false,
6068
+ error: error instanceof Error ? error.message : String(error)
6069
+ };
6070
+ } finally {
6071
+ probe.close();
6072
+ }
6073
+ })
6074
+ );
6075
+ const owners = checked.filter((world) => world.ownsSim);
6076
+ if (parsed.explicitPort) {
6077
+ if (owners.some((world) => world.port === parsed.wsPort)) return parsed.wsPort;
6078
+ if (owners.length > 0) {
6079
+ throw new Error(
6080
+ `--sim ${parsed.simId} belongs to bridge port ${owners[0].port}, but --port ${parsed.wsPort} names another world`
6081
+ );
6082
+ }
6083
+ }
6084
+ if (owners.length === 0) {
6085
+ const names = checked.map(
6086
+ (world) => `${world.record} (bridge :${world.port}${"error" in world ? `, ${world.error}` : ""})`
6087
+ );
6088
+ throw new Error(
6089
+ `no live peach world holds sim ${parsed.simId}; worlds checked: ${names.join(", ") || "none (no fresh dev-bridge or daemon lockfiles)"}`
6090
+ );
6091
+ }
6092
+ if (owners.length > 1) {
6093
+ const candidates = owners.map((w) => `${w.record} (:${w.port})`).join(", ");
6094
+ throw new Error(
6095
+ `sim ${parsed.simId} exists in multiple worlds (${candidates}); pin one with --port`
6096
+ );
6097
+ }
6098
+ return owners[0].port;
6099
+ }
5939
6100
 
5940
6101
  // src/bridge-contract.ts
5941
6102
  var HEADLESS_TENANT_FETCH_HUNG_MARKER = "headless tenant fetch hung";
@@ -6765,6 +6926,18 @@ var RuntimeReadinessUnsupportedError = class extends Error {
6765
6926
  }
6766
6927
  code = "runtime_readiness_unsupported";
6767
6928
  };
6929
+ var IdleObservationError = class extends Error {
6930
+ constructor(elapsedMs, failure) {
6931
+ const detail = failure instanceof Error ? failure.message : String(failure);
6932
+ super(`idle observation failed after ${elapsedMs}ms: ${detail}`, {
6933
+ cause: failure
6934
+ });
6935
+ this.elapsedMs = elapsedMs;
6936
+ this.failure = failure;
6937
+ this.name = "IdleObservationError";
6938
+ }
6939
+ code = "idle_observation_failed";
6940
+ };
6768
6941
  async function waitForSootsimIdle({
6769
6942
  bridge,
6770
6943
  simId,
@@ -6775,6 +6948,7 @@ async function waitForSootsimIdle({
6775
6948
  paintStill = false
6776
6949
  }) {
6777
6950
  let result;
6951
+ const observationStartedAt = Date.now();
6778
6952
  try {
6779
6953
  result = await bridge.send(
6780
6954
  {
@@ -6805,27 +6979,23 @@ async function waitForSootsimIdle({
6805
6979
  let screenPixels = null
6806
6980
  try {
6807
6981
 
6808
- // route-aware settle: a tap that pushes/pops a screen is async \u2014 the
6809
- // old screen still renders for a moment, then the new one mounts and
6810
- // its content loads. a pure layout-hash check reports "idle" on the
6811
- // outgoing screen or on the incoming skeleton, so a driver re-taps
6812
- // and stacks duplicate navigations. drain any in-flight screen
6813
- // transition FIRST, bounded by the overall budget. when no
6814
- // transition is happening this returns in ~80ms (startWindowMs), so
6815
- // a plain button tap barely pays for it.
6816
- try {
6817
- const wfst = window.__sootsimTest?.waitForScreenTransitions
6818
- if (typeof wfst === 'function') {
6982
+ // discover transitions while observing layout. when motion starts,
6983
+ // observations from before its completion cannot prove the new screen.
6984
+ let transitionWait = (async () => {
6985
+ try {
6986
+ const wfst = window.__sootsimTest?.waitForScreenTransitions
6819
6987
  const remaining = deadline - Date.now()
6820
- if (remaining > 120) {
6821
- await wfst({
6988
+ if (typeof wfst === 'function' && remaining > 120) {
6989
+ const transition = await wfst({
6822
6990
  timeoutMs: Math.min(remaining - 80, 4000),
6823
6991
  settleMs: 64,
6824
6992
  startWindowMs: 80,
6825
6993
  })
6994
+ return transition?.started === true
6826
6995
  }
6827
- }
6828
- } catch {}
6996
+ } catch {}
6997
+ return false
6998
+ })()
6829
6999
 
6830
7000
  // a worker that cannot answer within the remaining budget is busy, not
6831
7001
  // idle: its poll reads as unsettled. bounding each read by the budget
@@ -6850,17 +7020,51 @@ async function waitForSootsimIdle({
6850
7020
  let nodeCount = null
6851
7021
  let renderStatsAvailable = false
6852
7022
  let shellAnimating = false
7023
+ let shellBlockers = ''
6853
7024
  let webViewStarting = false
6854
7025
  let webViewBlocker = ''
7026
+ // the reads are independent, so they run concurrently: a loaded sim
7027
+ // answers each one late, and a poll that waits for them in turn
7028
+ // costs their sum, which fits too few polls in the budget to settle.
6855
7029
  // strict also waits out shell motion: a system alert, sheet or menu
6856
7030
  // animates in the shell worker while the app's own stats read still,
6857
7031
  // so its presentation is complete only once the shell's list empties.
6858
- if (strict) {
6859
- const motion = await answer(() => window.__sootsimTest?.listAnimations?.()).catch(() => null)
7032
+ const motionRead = strict
7033
+ ? answer(() => window.__sootsimTest?.listAnimations?.()).catch(() => null)
7034
+ : null
7035
+ const statsRead = answer(() => window.__sootsimRenderHost?.queryStats?.()).catch(() => null)
7036
+ // image preparation ends in the tenant before the compositor decodes
7037
+ // and realizes its pixels. both owners must finish before settling.
7038
+ // the compositor rejects a stats request it is too busy to answer.
7039
+ const compositorRead = answer(() => window.__sootsimCompositor.getStats()).catch(() => BUSY)
7040
+ const countsRead = Promise.resolve().then(() => window.__sootsimTest?.getRequestCounts?.()).catch(() => null)
7041
+ // mounted-root membership: the live node count moves while the tree
7042
+ // mounts, unmounts or recycles, in either render mode.
7043
+ const nodeCountRead = Promise.resolve().then(() => window.__sootsimTest?.getNodeCount?.()).catch(() => null)
7044
+ // the mounted hierarchy in render-worker mode comes from the test
7045
+ // bridge: window.__sootsimRoot is an empty placeholder there, so a
7046
+ // walk over it hashes every screen to the same empty string while
7047
+ // it moves. listInspectable is the worker-backed read describe
7048
+ // uses; a worker too busy to answer it reads as unsettled. tight
7049
+ // budgets skip this read and settle on the cheaper signals above.
7050
+ const hierarchyRead =
7051
+ fullGeometry && typeof window.__sootsimTest?.listInspectable === 'function'
7052
+ ? answer(() => window.__sootsimTest.listInspectable({})).catch(() => null)
7053
+ : null
7054
+ if (motionRead) {
7055
+ const motion = await motionRead
6860
7056
  if (motion === BUSY) workerBusy = true
6861
- else shellAnimating = Array.isArray(motion) && motion.some((anim) => anim.realm === 'shell' && anim.priority !== 'low')
7057
+ else if (Array.isArray(motion)) {
7058
+ const blocking = motion.filter((anim) => anim.realm === 'shell' && anim.priority !== 'low')
7059
+ shellAnimating = blocking.length > 0
7060
+ // name what holds the settle so a timeout points at its owner.
7061
+ shellBlockers = blocking
7062
+ .slice(0, 4)
7063
+ .map((anim) => anim.source + '/' + anim.kind + '#' + anim.id + ' at ' + anim.current + (anim.loop ? ' loop' : ''))
7064
+ .join(', ')
7065
+ }
6862
7066
  }
6863
- const stats = await answer(() => window.__sootsimRenderHost?.queryStats?.()).catch(() => null)
7067
+ const stats = await statsRead
6864
7068
  if (stats === BUSY) {
6865
7069
  workerBusy = true
6866
7070
  } else if (stats) {
@@ -6897,10 +7101,7 @@ async function waitForSootsimIdle({
6897
7101
  // mounting or remounting, even with no animation flag raised.
6898
7102
  appCommits = typeof stats.appCommitCount === 'number' ? stats.appCommitCount : null
6899
7103
  }
6900
- // image preparation ends in the tenant before the compositor decodes
6901
- // and realizes its pixels. both owners must finish before settling.
6902
- // the compositor rejects a stats request it is too busy to answer.
6903
- const compositor = await answer(() => window.__sootsimCompositor.getStats()).catch(() => BUSY)
7104
+ const compositor = await compositorRead
6904
7105
  if (compositor === BUSY) {
6905
7106
  workerBusy = true
6906
7107
  } else {
@@ -6911,31 +7112,12 @@ async function waitForSootsimIdle({
6911
7112
  webViewBlocker = 'compositor webView capture not settled'
6912
7113
  }
6913
7114
  }
6914
- try {
6915
- const counts = await window.__sootsimTest?.getRequestCounts?.()
6916
- requestTotal = typeof counts?.total === 'number' ? counts.total : 0
6917
- requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
6918
- } catch {}
6919
- // mounted-root membership: the live node count moves while the tree
6920
- // mounts, unmounts or recycles, in either render mode.
6921
- try {
6922
- const total = await window.__sootsimTest?.getNodeCount?.()
6923
- nodeCount = typeof total === 'number' ? total : null
6924
- } catch {}
6925
- // the mounted hierarchy in render-worker mode comes from the test
6926
- // bridge: window.__sootsimRoot is an empty placeholder there, so a
6927
- // walk over it hashes every screen to the same empty string while
6928
- // it moves. listInspectable is the worker-backed read describe
6929
- // uses; a worker too busy to answer it reads as unsettled. tight
6930
- // budgets skip this read and settle on the cheaper signals above.
6931
- let hierarchy = null
6932
- if (fullGeometry && typeof window.__sootsimTest?.listInspectable === 'function') {
6933
- try {
6934
- hierarchy = await answer(() => window.__sootsimTest.listInspectable({}))
6935
- } catch {
6936
- hierarchy = null
6937
- }
6938
- }
7115
+ const counts = await countsRead
7116
+ requestTotal = typeof counts?.total === 'number' ? counts.total : 0
7117
+ requestInFlight = typeof counts?.inFlight === 'number' ? counts.inFlight : 0
7118
+ const total = await nodeCountRead
7119
+ nodeCount = typeof total === 'number' ? total : null
7120
+ let hierarchy = hierarchyRead ? await hierarchyRead : null
6939
7121
  if (hierarchy === BUSY) {
6940
7122
  workerBusy = true
6941
7123
  hierarchy = null
@@ -7011,7 +7193,7 @@ async function waitForSootsimIdle({
7011
7193
  pixelFrameToken = token
7012
7194
  }
7013
7195
  }
7014
- return { layout: nodes.join(';'), screenPixels, animating, shellAnimating, layoutDirty, pendingFetches, workerBusy, webViewStarting, webViewBlocker, renderStatsAvailable, requestTotal, requestInFlight, appCommits, nodeCount }
7196
+ return { layout: nodes.join(';'), screenPixels, animating, shellAnimating, shellBlockers, layoutDirty, pendingFetches, workerBusy, webViewStarting, webViewBlocker, renderStatsAvailable, requestTotal, requestInFlight, appCommits, nodeCount }
7015
7197
  }
7016
7198
 
7017
7199
  // how long generic background network is allowed to keep us waiting AFTER
@@ -7098,6 +7280,19 @@ async function waitForSootsimIdle({
7098
7280
  Date.now() - visuallyStillSince >= requiredStillMs &&
7099
7281
  (networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
7100
7282
  ) {
7283
+ if (transitionWait) {
7284
+ const started = await transitionWait
7285
+ transitionWait = null
7286
+ if (started) {
7287
+ lastLayout = ''
7288
+ lastRequestTotal = -1
7289
+ lastAppCommits = null
7290
+ lastNodeCount = null
7291
+ stable = 0
7292
+ visuallyStillSince = 0
7293
+ continue
7294
+ }
7295
+ }
7101
7296
  return { settled: true, elapsed: Date.now() - start, blockedBy: '' }
7102
7297
  }
7103
7298
  } else {
@@ -7116,7 +7311,7 @@ async function waitForSootsimIdle({
7116
7311
  const blockers = []
7117
7312
  if (strict && !last.renderStatsAvailable) blockers.push('render stats unavailable')
7118
7313
  if (last.animating) blockers.push('animations still running')
7119
- if (last.shellAnimating) blockers.push('shell animations still running')
7314
+ if (last.shellAnimating) blockers.push('shell animations still running (' + last.shellBlockers + ')')
7120
7315
  if (last.layoutDirty) blockers.push('layout still dirty')
7121
7316
  if (last.appCommits !== null && lastAppCommits !== null && last.appCommits !== lastAppCommits) blockers.push('layout commits still landing')
7122
7317
  if (last.nodeCount !== null && lastNodeCount !== null && last.nodeCount !== lastNodeCount) blockers.push('node count still changing')
@@ -7147,12 +7342,7 @@ async function waitForSootsimIdle({
7147
7342
  }
7148
7343
  );
7149
7344
  } catch (error) {
7150
- const message = error instanceof Error ? error.message : String(error);
7151
- return {
7152
- settled: false,
7153
- elapsed: maxMs,
7154
- blockedBy: message
7155
- };
7345
+ throw new IdleObservationError(Math.max(0, Date.now() - observationStartedAt), error);
7156
7346
  }
7157
7347
  if (result?.error === "runtime_readiness_unsupported" && typeof result.runtimeVersion === "string" && Array.isArray(result.missingFields) && result.missingFields.every((field) => typeof field === "string")) {
7158
7348
  throw new RuntimeReadinessUnsupportedError(
@@ -7719,7 +7909,12 @@ var SimClient = class {
7719
7909
  if (!status.ready) throw new SimReadyTimeoutError(status);
7720
7910
  return status;
7721
7911
  }
7722
- /** wait for layout/animation/transition settle after an interaction. */
7912
+ /**
7913
+ * wait for layout/animation/transition settle after an interaction.
7914
+ * a still-busy guest returns settled false with its own elapsed time.
7915
+ * a failed idle read throws IdleObservationError with the host elapsed
7916
+ * time and the original failure.
7917
+ */
7723
7918
  settle(opts = {}) {
7724
7919
  return waitForSootsimIdle({
7725
7920
  bridge: this,
@@ -8028,12 +8223,19 @@ async function connect(options = {}) {
8028
8223
  commandTimeoutMs: options.timeoutMs ?? 15e3
8029
8224
  });
8030
8225
  }
8031
- const port = options.port ?? resolveBridgeWorld().bridgePort;
8032
8226
  const commandTimeoutMs = options.timeoutMs ?? 15e3;
8033
- const discovery = createBridge(port, { commandTimeoutMs, cliLabel: "sdk" });
8227
+ const parsed = {
8228
+ ...target,
8229
+ wsPort: options.port ?? resolveBridgeWorld().bridgePort,
8230
+ explicitPort: options.port !== void 0,
8231
+ commandTimeoutMs,
8232
+ positional: []
8233
+ };
8234
+ const discovery = createBridgeFromParsed(parsed);
8034
8235
  let picked;
8035
8236
  try {
8036
- picked = pickSim(await discovery.listSims(), options.sim, port);
8237
+ const sims = await discovery.listSims();
8238
+ picked = pickSim(sims, options.sim, parsed.wsPort);
8037
8239
  } catch (error) {
8038
8240
  discovery.close();
8039
8241
  if (error instanceof SimConnectError) throw error;
@@ -8041,7 +8243,7 @@ async function connect(options = {}) {
8041
8243
  throw new SimConnectError(`${message} \u2014 is the Peach daemon or dev shell running?`);
8042
8244
  }
8043
8245
  discovery.close();
8044
- const bridge = createBridge(port, {
8246
+ const bridge = createBridge(parsed.wsPort, {
8045
8247
  commandTimeoutMs,
8046
8248
  cliLabel: "sdk",
8047
8249
  simId: picked.id,
@@ -9095,7 +9297,7 @@ function peachPlugin(options = {}) {
9095
9297
  const buildShellHtml = (runtimeRoot) => {
9096
9298
  const html = import_fs4.default.readFileSync(import_path3.default.join(runtimeRoot, "index.html"), "utf8");
9097
9299
  const lock = readDaemonLockfile();
9098
- const bridgePort = lock && isDaemonLockfileFresh(lock) && lock.bridgePort > 0 ? lock.bridgePort : DEFAULT_SOOTSIM_BRIDGE_PORT;
9300
+ const bridgePort = lock && isDaemonLockfileFresh(lock) && lock.bridgePort > 0 ? lock.bridgePort : DEFAULT_SOOTSIM_BRIDGE_PORT2;
9099
9301
  const inject = `<script>(function(){var u=new URL(location.href);if(!u.searchParams.has('bundle'))u.searchParams.set('bundle',${JSON.stringify(bundleForPort(listeningPort()))});window.__sootsimBridgePort=${bridgePort};history.replaceState(null,'','${prefix2}/'+u.search)})()</script>`;
9100
9302
  return html.replace("</head>", inject + "</head>");
9101
9303
  };
@@ -10123,14 +10325,14 @@ function workspaceReferences(workspace) {
10123
10325
  if (!import_node_fs9.default.existsSync(data))
10124
10326
  throw new Error(`${workspace} has no contents.xcworkspacedata`);
10125
10327
  const container = import_node_path10.default.dirname(workspace);
10126
- const locate = (location, base) => {
10127
- const colon = location.indexOf(":");
10128
- const kind = location.slice(0, colon);
10129
- const rest = location.slice(colon + 1);
10328
+ const locate = (location2, base) => {
10329
+ const colon = location2.indexOf(":");
10330
+ const kind = location2.slice(0, colon);
10331
+ const rest = location2.slice(colon + 1);
10130
10332
  if (kind === "group") return import_node_path10.default.resolve(base, rest);
10131
10333
  if (kind === "container" || kind === "self") return import_node_path10.default.resolve(container, rest);
10132
10334
  if (kind === "absolute") return import_node_path10.default.resolve(rest);
10133
- throw new Error(`${import_node_path10.default.basename(workspace)} has an unsupported location ${location}`);
10335
+ throw new Error(`${import_node_path10.default.basename(workspace)} has an unsupported location ${location2}`);
10134
10336
  };
10135
10337
  const groups = [container];
10136
10338
  const references = [];
@@ -10140,8 +10342,8 @@ function workspaceReferences(workspace) {
10140
10342
  continue;
10141
10343
  }
10142
10344
  const base = groups.at(-1);
10143
- const location = /\blocation\s*=\s*"([^"]*)"/.exec(attributes)?.[1];
10144
- const resolved = location ? locate(decodeXml(location), base) : base;
10345
+ const location2 = /\blocation\s*=\s*"([^"]*)"/.exec(attributes)?.[1];
10346
+ const resolved = location2 ? locate(decodeXml(location2), base) : base;
10145
10347
  if (tag === "FileRef") references.push(resolved);
10146
10348
  else if (!selfClosing) groups.push(resolved);
10147
10349
  }
@@ -10842,59 +11044,61 @@ var import_node_fs11 = __toESM(require("node:fs"), 1);
10842
11044
  var import_node_os2 = __toESM(require("node:os"), 1);
10843
11045
  var import_node_path12 = __toESM(require("node:path"), 1);
10844
11046
  init_is_record();
10845
- function readNativeFirebaseConfig(projectRoot, platforms = ["ios", "android"], iosTarget) {
10846
- const result = {};
10847
- let config = {};
10848
- const appJsonPath = import_node_path12.default.join(projectRoot, "app.json");
10849
- const appJson = import_node_fs11.default.existsSync(appJsonPath) ? JSON.parse(import_node_fs11.default.readFileSync(appJsonPath, "utf8")) : null;
10850
- const staticExpo = isRecord(appJson) && (isRecord(appJson.expo) || [appJson.ios, appJson.android].some(
10851
- (value) => isRecord(value) && typeof value.googleServicesFile === "string"
10852
- ));
10853
- const dynamicExpo = [
10854
- "app.config.js",
10855
- "app.config.ts",
10856
- "app.config.cjs",
10857
- "app.config.mjs"
10858
- ].some((name2) => import_node_fs11.default.existsSync(import_node_path12.default.join(projectRoot, name2)));
10859
- if (staticExpo || dynamicExpo) {
10860
- const directory = import_node_fs11.default.mkdtempSync(import_node_path12.default.join(import_node_os2.default.tmpdir(), "peach-expo-config-"));
10861
- const resultPath = import_node_path12.default.join(directory, "config.json");
10862
- try {
10863
- const child = (0, import_node_child_process3.spawnSync)(
10864
- "node",
10865
- [
10866
- "--input-type=commonjs",
10867
- "-e",
10868
- `const { createRequire } = require('node:module');
11047
+ var EXPO_CONFIG_SCRIPT = `const { createRequire } = require('node:module');
10869
11048
  const { writeFileSync } = require('node:fs');
10870
11049
  const { join } = require('node:path');
10871
11050
  const root = process.argv[1];
10872
11051
  const { getConfig } = createRequire(join(root, 'package.json'))('@expo/config');
10873
11052
  const result = JSON.stringify(getConfig(root, { skipSDKVersionRequirement: true }).exp);
10874
11053
  if (Buffer.byteLength(result) > 1024 * 1024) throw new Error('Expo native configuration exceeds 1 MiB.');
10875
- writeFileSync(process.argv[2], result, { mode: 0o600 });`,
10876
- import_node_path12.default.resolve(projectRoot),
10877
- resultPath
10878
- ],
10879
- {
10880
- cwd: projectRoot,
10881
- encoding: "utf8",
10882
- stdio: ["ignore", "ignore", "pipe"],
10883
- timeout: 3e4,
10884
- maxBuffer: 1024 * 1024
10885
- }
10886
- );
10887
- if (child.error) throw child.error;
10888
- if (child.status !== 0)
10889
- throw new Error(child.stderr || "Expo native configuration evaluation failed.");
10890
- const resolved = JSON.parse(import_node_fs11.default.readFileSync(resultPath, "utf8"));
10891
- if (!isRecord(resolved))
10892
- throw new Error("Expo native configuration is unavailable.");
10893
- config = resolved;
10894
- } finally {
10895
- import_node_fs11.default.rmSync(directory, { recursive: true, force: true });
10896
- }
11054
+ writeFileSync(process.argv[2], result, { mode: 0o600 });`;
11055
+ function hasExpoConfig(projectRoot) {
11056
+ const appJsonPath = import_node_path12.default.join(projectRoot, "app.json");
11057
+ const appJson = import_node_fs11.default.existsSync(appJsonPath) ? JSON.parse(import_node_fs11.default.readFileSync(appJsonPath, "utf8")) : null;
11058
+ const staticExpo = isRecord(appJson) && (isRecord(appJson.expo) || [appJson.ios, appJson.android].some(
11059
+ (value) => isRecord(value) && typeof value.googleServicesFile === "string"
11060
+ ));
11061
+ return staticExpo || ["app.config.js", "app.config.ts", "app.config.cjs", "app.config.mjs"].some(
11062
+ (name2) => import_node_fs11.default.existsSync(import_node_path12.default.join(projectRoot, name2))
11063
+ );
11064
+ }
11065
+ function expoConfigArgs(projectRoot, resultPath) {
11066
+ return [
11067
+ "--input-type=commonjs",
11068
+ "-e",
11069
+ EXPO_CONFIG_SCRIPT,
11070
+ import_node_path12.default.resolve(projectRoot),
11071
+ resultPath
11072
+ ];
11073
+ }
11074
+ function readExpoConfigResult(resultPath, status, stderr) {
11075
+ if (status !== 0)
11076
+ throw new Error(stderr || "Expo native configuration evaluation failed.");
11077
+ const resolved = JSON.parse(import_node_fs11.default.readFileSync(resultPath, "utf8"));
11078
+ if (!isRecord(resolved)) throw new Error("Expo native configuration is unavailable.");
11079
+ return resolved;
11080
+ }
11081
+ var resultDirectory = () => import_node_fs11.default.mkdtempSync(import_node_path12.default.join(import_node_os2.default.tmpdir(), "peach-expo-config-"));
11082
+ function readExpoConfig(projectRoot) {
11083
+ if (!hasExpoConfig(projectRoot)) return {};
11084
+ const directory = resultDirectory();
11085
+ const resultPath = import_node_path12.default.join(directory, "config.json");
11086
+ try {
11087
+ const child = (0, import_node_child_process3.spawnSync)("node", expoConfigArgs(projectRoot, resultPath), {
11088
+ cwd: projectRoot,
11089
+ encoding: "utf8",
11090
+ stdio: ["ignore", "ignore", "pipe"],
11091
+ timeout: 3e4,
11092
+ maxBuffer: 1024 * 1024
11093
+ });
11094
+ if (child.error) throw child.error;
11095
+ return readExpoConfigResult(resultPath, child.status, child.stderr);
11096
+ } finally {
11097
+ import_node_fs11.default.rmSync(directory, { recursive: true, force: true });
10897
11098
  }
11099
+ }
11100
+ function readNativeFirebaseConfig(projectRoot, platforms = ["ios", "android"], iosTarget, config = readExpoConfig(projectRoot)) {
11101
+ const result = {};
10898
11102
  const ios = config.ios;
10899
11103
  if (platforms.includes("ios") && (!isRecord(ios) || typeof ios.googleServicesFile !== "string")) {
10900
11104
  const iosRoot = import_node_path12.default.join(projectRoot, "ios");
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1406 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1465 | (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.1406 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1465 | (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;