peachsim 0.1.626 → 0.1.628

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 (106) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +3 -2
  3. package/cli/bin.ts +6 -2
  4. package/cli/bridge-flow-runner.ts +79 -72
  5. package/cli/commands/box/bundle-plane.ts +7 -2
  6. package/cli/commands/box.ts +9 -2
  7. package/cli/commands/cleanup.ts +6 -6
  8. package/cli/commands/control.ts +36 -6
  9. package/cli/commands/detox.ts +25 -11
  10. package/cli/commands/flow.ts +20 -8
  11. package/cli/commands/inspect.ts +22 -37
  12. package/cli/commands/maestro-generate.ts +5 -12
  13. package/cli/commands/maestro.ts +16 -6
  14. package/cli/commands/mode.ts +11 -7
  15. package/cli/commands/platform.ts +1 -0
  16. package/cli/commands/report-issue.ts +1 -1
  17. package/cli/commands/setup.ts +16 -11
  18. package/cli/commands/show-touches-mode.ts +33 -0
  19. package/cli/commands/skills.ts +16 -0
  20. package/cli/commands/test.ts +22 -1
  21. package/cli/commands/upload.ts +18 -8
  22. package/cli/commands/welcome.ts +3 -3
  23. package/cli/drivers/playwright-sim-host.ts +16 -8
  24. package/cli/drivers/playwright.ts +8 -5
  25. package/cli/help-core.ts +9 -4
  26. package/cli/internal-child.ts +14 -2
  27. package/cli/levenshtein.ts +27 -0
  28. package/cli/main.ts +28 -18
  29. package/cli/run-rnx.ts +8 -4
  30. package/cli/ws-bridge.ts +3 -2
  31. package/detox/index.ts +234 -7
  32. package/dist-lib/agent-daemon-client.cjs +2 -1
  33. package/dist-lib/agent-events.cjs +1 -1
  34. package/dist-lib/agent-identity.cjs +1 -1
  35. package/dist-lib/agent-sessions.cjs +1 -1
  36. package/dist-lib/attached-projects.cjs +1 -1
  37. package/dist-lib/auth/shared-session.cjs +1 -1
  38. package/dist-lib/backend-origin.cjs +1 -1
  39. package/dist-lib/beta.cjs +1 -1
  40. package/dist-lib/beta.mjs +1 -1
  41. package/dist-lib/bridge-constants.cjs +4 -1
  42. package/dist-lib/bridge-contract-input.cjs +1 -1
  43. package/dist-lib/bridge-contract-input.mjs +1 -1
  44. package/dist-lib/bridge-contract.cjs +1 -1
  45. package/dist-lib/bridge-contract.mjs +1 -1
  46. package/dist-lib/capture-contract.cjs +18 -8
  47. package/dist-lib/capture-contract.mjs +18 -8
  48. package/dist-lib/cli-constants.cjs +1 -1
  49. package/dist-lib/cloud-contract.cjs +1 -1
  50. package/dist-lib/cloud-contract.mjs +1 -1
  51. package/dist-lib/cloud-test-args.cjs +1 -1
  52. package/dist-lib/cloud-test-args.mjs +1 -1
  53. package/dist-lib/cloud.cjs +1 -1
  54. package/dist-lib/cloud.mjs +1 -1
  55. package/dist-lib/config.cjs +1 -1
  56. package/dist-lib/detox/index.cjs +170 -13
  57. package/dist-lib/dev-bundle-resolution.cjs +2 -2
  58. package/dist-lib/home-paths.cjs +1 -1
  59. package/dist-lib/host/bridge-host.cjs +280 -138
  60. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  61. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  62. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  63. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  64. package/dist-lib/host/websocket-proxy.cjs +1 -1
  65. package/dist-lib/index.cjs +62 -12
  66. package/dist-lib/jump-to-source-babel.cjs +1 -1
  67. package/dist-lib/jump-to-source-native.cjs +1 -1
  68. package/dist-lib/menu.cjs +7 -1
  69. package/dist-lib/menu.mjs +7 -1
  70. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  71. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  72. package/dist-lib/metro-production-bundle.cjs +1 -1
  73. package/dist-lib/metro-production-bundle.mjs +1 -1
  74. package/dist-lib/metro.cjs +43 -4
  75. package/dist-lib/profiles.cjs +1 -1
  76. package/dist-lib/public-brand.cjs +1 -1
  77. package/dist-lib/react-native-host-modules.cjs +1 -1
  78. package/dist-lib/react-native-host-modules.mjs +1 -1
  79. package/dist-lib/render-mode.cjs +1 -1
  80. package/dist-lib/scripts/dev-server-scanner.cjs +215 -28
  81. package/dist-lib/sdk.cjs +1 -1
  82. package/dist-lib/sdk.mjs +1 -1
  83. package/dist-lib/skills.cjs +431 -276
  84. package/dist-lib/swift-compile.cjs +1 -1
  85. package/dist-lib/swift-compile.mjs +1 -1
  86. package/dist-lib/swift-package.cjs +1 -1
  87. package/dist-lib/swift-package.mjs +1 -1
  88. package/dist-lib/swift-submit.cjs +1 -1
  89. package/dist-lib/swift-submit.mjs +1 -1
  90. package/dist-lib/swift.cjs +14512 -14208
  91. package/dist-lib/vite-kotlin.cjs +1 -1
  92. package/dist-lib/vite-swift.cjs +1 -1
  93. package/dist-lib/vite.cjs +44 -4
  94. package/install.mjs +16 -0
  95. package/install.sh +199 -0
  96. package/package.json +6 -1
  97. package/scripts/dev-server-scanner.ts +77 -46
  98. package/skills/contrast/SKILL.md +1 -1
  99. package/src/bridge-constants.ts +11 -0
  100. package/src/capture-contract.ts +40 -7
  101. package/src/dev-bundle-resolution.ts +1 -1
  102. package/src/host/bridge-host.ts +26 -137
  103. package/src/menu.ts +7 -0
  104. package/src/native-seam-manifest.ts +4 -16
  105. package/src/runtime-assets.ts +59 -2
  106. package/src/vite-plugin.ts +723 -25
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.626 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.628 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -4707,7 +4707,7 @@ var init_supported_native_packages = __esm({
4707
4707
  "expo-sensors",
4708
4708
  // semantic-proof
4709
4709
  "expo-sharing",
4710
- // semantic-proof
4710
+ // semantic-proof+coverage
4711
4711
  "expo-sms",
4712
4712
  // semantic-proof
4713
4713
  "expo-speech",
@@ -5210,6 +5210,8 @@ var init_supported_native_packages = __esm({
5210
5210
  // preset+visual-proof+seam
5211
5211
  "react-native-widget-picker",
5212
5212
  // seam
5213
+ "react-native-widgetkit",
5214
+ // seam+coverage
5213
5215
  "react-native-wifi-p2p",
5214
5216
  // seam
5215
5217
  "react-native-worklets",
@@ -5801,8 +5803,8 @@ var init_registry = __esm({
5801
5803
  id: "navigation-controls",
5802
5804
  usage: 3,
5803
5805
  importance: 3,
5804
- estimate: 0.7,
5805
- rationale: "Reload and injection work. HTML fragment navigation now follows the observed iOS URL and callback sequence, and local link requests expose decisions with matching payloads. The remaining 30% judgment allowance retains initial-load interception, remote history, stop-loading, failed navigation and form requests."
5806
+ estimate: 0.8,
5807
+ rationale: "Reload and injection work. HTML fragment navigation now follows the observed iOS URL and callback sequence, and local link requests expose decisions with matching payloads. Initial http(s) loads and form submits are asked with matching payloads and resume through the lock callback, a stopped reload reports its start with no finish, and refused local connections and same-origin HTTP errors report onError/onHttpError with native shapes plus renderError. The remaining 20% judgment allowance retains remote history traversal and reload decisions, data:/about:/file initial-load decisions, and remote failure reporting."
5806
5808
  },
5807
5809
  {
5808
5810
  id: "layout-scroll-presentation",
@@ -5828,9 +5830,9 @@ var init_registry = __esm({
5828
5830
  ],
5829
5831
  capabilityCoverage: CAPABILITY_INVENTORY_PACKAGE_COVERAGE["react-native-webview"],
5830
5832
  strictCoverage: REACT_NATIVE_WEBVIEW_STRICT_COVERAGE,
5831
- note: "native-seam only: the published package runs unchanged and routes RNCWebView through the tenant-to-shell hosted iframe surface. the implementation estimate uses reviewed feature weights; the separate equal-surface strict diagnostic includes the shared, iOS, and Android public prop and ref surface. paired native and Peach runtime conformance covers local HTML, base URL, URI loading, load and navigation callbacks, both injection phases, two-way messaging, imperative URI reload and injection, local HTML fragment navigation and link-request decisions, content scrolling, parent translation, and onOpenWindow target delivery.",
5832
- working: "source.uri/source.html/source.baseUrl, Chromium cross-origin URI loading under the shell COEP, same-origin URI cookies, source.html persistent localStorage, document-start ReactNativeWebView bootstrap and injectedJavaScriptBeforeContentLoaded/injectedJavaScript on source.html, strict ReactNativeWebView.postMessage bridge envelope in both directions, onMessage synthetic event with navigation fields, onOpenWindow target delivery for source.html, successful onLoad/Start/End and onNavigationStateChange, ref.reload on URI pages, ref.injectJavaScript/postMessage, local HTML fragment URL/callback behavior, onShouldStartLoadWithRequest for local link clicks with native-shaped request and loading payloads, internal content scrolling, renderLoading, javaScriptEnabled sandbox, layout, inherited clipping and visibility, parked/live presentation",
5833
- missing: "Safari/WebKit cross-origin URI pages without COEP-compatible response headers remain permanently loading because WebKit ignores iframe credentialless; Chromium credentialless cross-origin URI frames send no credentials, including cookies set by their own origin, and use ephemeral storage; iframe pixels in canvas screenshots; script injection, ReactNativeWebView bootstrap, and onOpenWindow for remote pages, which get no bridge at all because a browser cannot script a cross-origin frame; cross-origin ref.goBack/goForward/stopLoading and truthful canGoBack/canGoForward values; failed HTTP/refused navigation callbacks (onError/onHttpError and renderError); initial-load and form onShouldStartLoadWithRequest callbacks, allowsBackForwardNavigationGestures, userAgent, dataDetectorTypes, cacheEnabled, incognito, scalesPageToFit, originWhitelist enforcement"
5833
+ note: "native-seam only: the published package runs unchanged and routes RNCWebView through the tenant-to-shell hosted iframe surface. the implementation estimate uses reviewed feature weights; the separate equal-surface strict diagnostic includes the shared, iOS, and Android public prop and ref surface. paired native and Peach runtime conformance covers local HTML, base URL, URI loading, load and navigation callbacks, both injection phases, two-way messaging, imperative URI reload and injection, local HTML fragment navigation and link-request decisions, initial-load and form request decisions, local stopLoading cancellation, refused-host and restricted-port onError with renderError, content scrolling, parent translation, and onOpenWindow target delivery. same-origin onHttpError follows RNCWebViewImpl but runs on Peach only, since no local HTTP error source is reachable from both targets.",
5834
+ working: "source.uri/source.html/source.baseUrl, Chromium cross-origin URI loading under the shell COEP, same-origin URI cookies, source.html persistent localStorage, document-start ReactNativeWebView bootstrap and injectedJavaScriptBeforeContentLoaded/injectedJavaScript on source.html, strict ReactNativeWebView.postMessage bridge envelope in both directions, onMessage synthetic event with navigation fields, onOpenWindow target delivery for source.html, successful onLoad/Start/End and onNavigationStateChange, ref.reload on URI pages, ref.injectJavaScript/postMessage, local HTML fragment URL/callback behavior, onShouldStartLoadWithRequest for local link clicks with native-shaped request and loading payloads, initial-load onShouldStartLoadWithRequest for http(s) documents with the native other/top-frame/lockIdentifier payload allowed or denied through the lock callback, form-submit onShouldStartLoadWithRequest with navigationType formsubmit and, for a get form, the form data as the url query, resumed in place, ref.stopLoading cancelling an in-flight reload with no following callbacks, onError with the native NSURLErrorDomain code/description for a refused local connection and WebKitErrorDomain 103 for a port on the WebKit blocked list, each plus onLoadEnd and renderError(domain, code, description), onHttpError with the native url/statusCode for a same-origin local HTTP error, internal content scrolling, renderLoading, javaScriptEnabled sandbox, layout, inherited clipping and visibility, parked/live presentation",
5835
+ missing: "Safari/WebKit cross-origin URI pages without COEP-compatible response headers remain permanently loading because WebKit ignores iframe credentialless; Chromium credentialless cross-origin URI frames send no credentials, including cookies set by their own origin, and use ephemeral storage; iframe pixels in canvas screenshots; script injection, ReactNativeWebView bootstrap, and onOpenWindow for remote pages, which get no bridge at all because a browser cannot script a cross-origin frame; cross-origin history truthfulness (goBack/goForward across documents, truthful canGoBack/canGoForward) plus reload and history-traversal policy decisions; initial-load decisions for data:/about:/file documents, which stay ungated with no paired native ask observed; remote HTTP error statuses and refused remote connections, which a cross-origin frame never exposes to the browser, and the HTTP status of a non-get form submit; allowsBackForwardNavigationGestures, userAgent, dataDetectorTypes, cacheEnabled, incognito, scalesPageToFit, originWhitelist enforcement"
5834
5836
  }
5835
5837
  ]
5836
5838
  },
@@ -5861,10 +5863,10 @@ var init_registry = __esm({
5861
5863
  },
5862
5864
  {
5863
5865
  range: ">=56.0.0 <58.0.0",
5864
- coverage: 0.35,
5865
- note: "native-seam only: Peach runtime conformance passes the published Expo 57.0.1 wrapper local-page bridge, script, object, message, scroll, update, and reload paths; native parity has no verdict, so these Peach-only observations do not raise the score.",
5866
- working: "DomWebView/WebView exports, source.uri rendering, onMessage data/url/title, ref.reload/injectJavaScript/scrollTo, document-start ReactNativeWebView bootstrap and injectedJavaScriptBeforeContentLoaded on percent-encoded data: URI pages, injectedJavaScriptObject, reload-on-update behavior for injectedJavaScriptObject/injectedJavaScript, layout and parked/live presentation",
5867
- missing: "Expo modules bridge, any bridge or script injection on a remote page because a browser cannot script a cross-origin frame, cross-origin scrolling, the re-encoded data: URI a page reports as its own location.href, exact native scroll physics and native-only media/keyboard props, iframe pixels in canvas screenshots"
5866
+ coverage: 0.5,
5867
+ note: 'native-seam only: the published Expo 57.0.1 wrapper and an Expo "use dom" component pass the same paired library case on a native iPhone 16 and in Peach (ExpoDomWebViewLibraryCase).',
5868
+ working: 'DomWebView/WebView exports, source.uri rendering, onMessage data/url/title, ref.reload/injectJavaScript/scrollTo, document-start ReactNativeWebView bootstrap and injectedJavaScriptBeforeContentLoaded, injectedJavaScriptObject, reload-on-update behavior for injectedJavaScriptObject/injectedJavaScript, "use dom" components (mount with initial props, prop updates, native actions that resolve or reject, useDOMImperativeHandle methods, reload), layout and parked/live presentation',
5869
+ missing: "Expo modules bridge (unstable_useExpoModulesBridge is accepted and ignored), any bridge or script injection on a remote page because a browser cannot script a cross-origin frame, cross-origin scrolling, the re-encoded data: URI a page reports as its own location.href, exact native scroll physics and native-only media/keyboard props, iframe pixels in canvas screenshots"
5868
5870
  }
5869
5871
  ]
5870
5872
  },
@@ -6145,9 +6147,9 @@ var init_registry = __esm({
6145
6147
  {
6146
6148
  range: ">=11.0.0",
6147
6149
  coverage: 0.95,
6148
- note: "runtime font registry backed by CSS FontFace where available",
6149
- working: "useFonts (URL/object/require sources), loadAsync, isLoaded, getLoadedFonts, isLoading, unloadAsync, unloadAllAsync, processFontFamily",
6150
- missing: "renderToImageAsync (native-only), exact Expo asset integration for numeric bundled font ids"
6150
+ note: "runtime font registry backed by the engine CanvasKit provider; renderToImageAsync rasterizes in the tenant worker on a cpu surface into the tmp file store",
6151
+ working: "useFonts (URL/object/require sources), loadAsync, isLoaded, getLoadedFonts, isLoading, processFontFamily, renderToImageAsync (tenant cpu raster, tmp file uri, dp width/height/scale); a file that is not a font rejects with ERR_FONT_REGISTRATION_FAILED; unloadAsync and unloadAllAsync throw UnavailabilityError, as the iOS and Android modules define neither",
6152
+ missing: "exact Expo asset integration for numeric bundled font ids"
6151
6153
  }
6152
6154
  ]
6153
6155
  },
@@ -6170,10 +6172,10 @@ var init_registry = __esm({
6170
6172
  versions: [
6171
6173
  {
6172
6174
  range: ">=1.0.0",
6173
- coverage: 0.9,
6174
- note: "the real expo-image wrapper renders through the shared sootsim-image host. Paired native and Peach runtime proof covers source-less placeholder rendering and right-edge contentPosition alignment; a negative control observed centered red/blue content before the renderer implemented the position.",
6175
- working: "Image component, ImageBackground, useImage hook, ImageRef class, contentFit, contentPosition right-edge alignment, source-less placeholder rendering and placeholderContentFit, tintColor, onLoad/onLoadStart/onLoadEnd/onError callbacks, source normalization (string/object/array), prefetch, clearMemoryCache, clearDiskCache, getCachePathAsync, configureCache, generateBlurhashAsync, generateThumbhashAsync, loadAsync",
6176
- missing: "transition/animation effects, blurRadius, contentPosition variants beyond the paired right-edge proof, sfEffect/SFSymbol, allowDownscaling, autoplay (GIF), responsivePolicy"
6175
+ coverage: 0.93,
6176
+ note: "the published Image, ImageBackground, and ExpoImage.tsx JS runs unchanged; the ExpoImage native view and native module follow the installed ImageView.swift and ImageModule.swift over the engine's sootsim-image host. paired native and Peach runtime proof covers cover, contain, fill, and scale-down contentFit, right-edge contentPosition, tint, opacity, uri, blurhash, and thumbhash placeholders, the cross-dissolve transition frame by frame, onLoad cache type and image facts, onDisplay, animated GIF frame playback with native durations, blurRadius, onProgress events, the placeholder generators, and a loaded image ref.",
6177
+ working: "Image, ImageBackground, useImage; contentFit cover, contain, fill, none, and scale-down; contentPosition; tintColor; blurRadius with the native halved radius; animated GIF playback with native frame durations and loop count (isAnimated, mediaType, autoplay, startAnimating, stopAnimating); source selection by view size; uri, blurhash, and thumbhash placeholders with placeholderContentFit; transition duration and timing curves with skipOnCacheHit; recyclingKey; onLoadStart, onLoad (cacheType by cachePolicy, url, size, mediaType, isAnimated), onProgress (loaded, total, progress) for data uris and remote sources the browser can read, onLoadEnd, onError, onDisplay; Image.prefetch with its cache policy, clearMemoryCache and clearDiskCache, loadAsync with maxWidth and maxHeight, readFromCacheAsync, writeToCacheAsync, generateBlurhashAsync and generateThumbhashAsync (the swift encoders), ImageRef rendering, a reload after every prop update, lockResourceAsync, unlockResourceAsync, and reloadAsync, the imageLoaded event",
6178
+ missing: "the flip, curl, and SF Symbol transition effects draw a cross-dissolve; SF Symbol sources; the disk cache lasts only as long as the page and has no files, so getCachePathAsync is null; prefetch ignores headers; source.cacheKey is ignored, so an image written with writeToCacheAsync does not render from its key; onProgress for a remote image served without CORS headers; animated WebP and APNG decode through the same player but have no paired proof"
6177
6179
  }
6178
6180
  ]
6179
6181
  },
@@ -6221,10 +6223,10 @@ var init_registry = __esm({
6221
6223
  versions: [
6222
6224
  {
6223
6225
  range: ">=15.0.0",
6224
- coverage: 0.9,
6225
- note: "both APIs implemented over one shared synchronous localStorage VFS: the SDK-18+ class-based File/Directory/Paths API (the bare `expo-file-system` export on SDK 18+) and the legacy async API (`expo-file-system/legacy`), with real network ops via fetch + navigator.storage. legacy and new interoperate on the same filesystem. paired native and Peach runtime conformance verifies the published DownloadResumable class identity, savable state, and cancel-before-start behavior; the score stays at 90% because active-transfer control remains unsupported.",
6226
- working: "SDK-18+ class API: Paths (document/cache/bundle, join/basename/extname/dirname), File (exists, size, md5, name, extension, parentDirectory, create, write text|bytes, text(Sync), base64(Sync), bytes(Sync), arrayBuffer, slice, stream, delete, info, copy, move/rename, open FileHandle, static downloadFileAsync/pickFileAsync), Directory (exists, create, createFile, createDirectory, list, info, delete, copy, move, static pickDirectoryAsync). Legacy API: documentDirectory, cacheDirectory, bundleDirectory, EncodingType, FileSystemSessionType, FileSystemUploadType, FileSystemAcceptedUploadHttpMethod, readAsStringAsync, writeAsStringAsync, deleteAsync, getInfoAsync, makeDirectoryAsync, readDirectoryAsync, copyAsync, moveAsync, downloadAsync, createDownloadResumable (published DownloadResumable class with fileUri/savable state and cancel-before-start), uploadAsync (real fetch with binary or multipart body), createUploadTask, UploadTask class (uploadAsync, cancelAsync, fires completion callback), getFreeDiskStorageAsync (navigator.storage.estimate quota - usage), getTotalDiskCapacityAsync (navigator.storage.estimate quota), getContentUriAsync (returns uri unchanged \u2014 Android-only behavior), StorageAccessFramework namespace stub (Android-only methods throw with platform error). All upstream legacy types exported: FileSystemUploadOptions, FileSystemUploadResult, UploadProgressData, DownloadProgressData, FileSystemNetworkTaskProgressCallback.",
6227
- missing: "in-flight DownloadResumable pause, resume, cancellation, and streamed download progress; chunked upload progress events (fetch does not expose upload progress)"
6226
+ coverage: 0.98,
6227
+ note: "upstream package JS runs against the ExpoFileSystem and ExponentFileSystem native-module seams over one shared VFS. both modules follow the installed Swift sources: member shapes, sync and async twins, error codes and messages including the legacy module's own codes, and events. transfers use fetch and the browser XMLHttpRequest, so download progress, pause and resume, cancellation, and upload progress are real. paired native and Peach runtime conformance covers the class API, write and copy semantics, file handles, digests, directory watching, and the legacy API.",
6228
+ working: "class API over the native FileSystemFile, FileSystemDirectory, and FileSystemFileHandle: write and writeSync (text, bytes, base64, append), text, base64, bytes and their Sync twins, info, md5 and digest (MD5, SHA-1, SHA-256, SHA-384, SHA-512), type from the extension UTType, lastModified, create with its native options, copy, move, and their Sync twins with native overwrite and existence errors, rename, delete, file handles in every FileMode with readBytes, writeBytes, offset, and size, Directory create, createFile, createDirectory, list, listAsRecords, size, copy, move; Paths (document, cache, bundle, disk space); downloadFileAsync with uuid progress events and cancelDownloadAsync; FileSystemDownloadTask and FileSystemUploadTask with start, pause, resume, cancel, and progress events; FileSystemWatcher change events for modify, delete, and rename; pickFileAsync through the document picker into the tmp Inbox. legacy API through ExponentFileSystem: read with position and length, write with append, delete, info, makeDirectory, readDirectory, recursive copy and move, downloadAsync, createDownloadResumable with pause, resume, savable resume data, and cancellation, uploadAsync and createUploadTask (binary and multipart) with progress events, disk capacity reads.",
6229
+ missing: "pickDirectoryAsync always reports a cancelled pick (native opens the chosen folder in place); background URLSession transfers do not continue while the app is suspended; Quick Look canPreview and preview are absent; legacy copyAsync cannot read ph:// photo library assets; Directory.size omits the directory entries' own sizes"
6228
6230
  }
6229
6231
  ]
6230
6232
  },
@@ -6262,9 +6264,9 @@ var init_registry = __esm({
6262
6264
  {
6263
6265
  range: ">=12.0.0",
6264
6266
  coverage: 0.8,
6265
- note: "the published package runs unchanged; ordinary browser opens hand off to the host, and host-owned auth sessions deliver successful callback URLs, explicit user cancellation, programmatic dismissal, and replacement. the 80% implementation estimate follows the mounted published-package test under cross-origin isolation and ordinary popup blocking; native browser presentation and unrestricted redirect schemes remain unsupported.",
6266
- working: "openBrowserAsync host handoff, dismissBrowser result, warmUpAsync, coolDownAsync, mayInitWithUrlAsync, published non-Android getCustomTabsSupportingBrowsersAsync shape, openAuthSessionAsync success with callback URL, explicit host Cancel Sign-In, dismissAuthSession and replacement dismissal, identity-matched callback and control cleanup, preservation of guest alerts during sign-in, and upstream enums and option processing",
6267
- missing: "embedded SFSafariViewController or Chrome Custom Tab, native styling and cookie isolation, closing an ordinary handed-off host tab from dismissBrowser, custom-scheme or foreign-origin auth callbacks, and observing abandonment after browser isolation severs the popup proxy. auth requires a same-origin HTTP(S) callback and user activation allowed by popup policy; an abandoned isolated provider requires the visible host cancel control"
6267
+ note: "the published package runs unchanged; host-owned browser sessions settle on user cancellation or programmatic dismissal, and auth sessions deliver successful callback URLs, explicit user cancellation, programmatic dismissal, and replacement. the 80% implementation estimate follows the mounted published-package test under cross-origin isolation and popup blocking; native browser presentation and unrestricted redirect schemes remain unsupported.",
6268
+ working: "openBrowserAsync pending result, user cancel and programmatic dismiss, locked result for a second browser, warmUpAsync, coolDownAsync, mayInitWithUrlAsync, published iOS getCustomTabsSupportingBrowsersAsync UnavailabilityError, openAuthSessionAsync success with callback URL, explicit host Cancel Sign-In, dismissAuthSession and replacement dismissal, identity-matched callback and control cleanup, preservation of guest alerts during sign-in, and upstream enums and option processing",
6269
+ missing: "embedded SFSafariViewController or Chrome Custom Tab, native styling and cookie isolation, custom-scheme or foreign-origin auth callbacks, and observing abandonment after browser isolation severs the popup proxy. auth requires a same-origin HTTP(S) callback and user activation allowed by popup policy; an abandoned isolated provider requires the visible host close control"
6268
6270
  }
6269
6271
  ]
6270
6272
  },
@@ -6560,10 +6562,10 @@ var init_registry = __esm({
6560
6562
  versions: [
6561
6563
  {
6562
6564
  range: ">=17.0.0",
6563
- coverage: 0.6,
6565
+ coverage: 0.8,
6564
6566
  note: "the published SDK 57 package runs unchanged against one ExpoLocation native seam backed by host browser geolocation and the shared app-scoped permission registry",
6565
- working: "upstream enums, validation, subscriber bookkeeping and permission hooks; getProviderStatusAsync, getCurrentPositionAsync, filtered getLastKnownPositionAsync, watchPositionAsync with native watch-id event routing and removal, foreground/background permission getters and requests, hasServicesEnabledAsync, unavailable motion-permission state, and isBackgroundLocationAvailableAsync=false",
6566
- missing: "device-orientation heading readings, an app-selected geocoder, motion activity readings, background location task registration and delivery, geofencing, and Android network-provider enablement"
6567
+ working: "upstream enums, validation, subscriber bookkeeping and permission hooks; native exception codes and reasons; two-key getProviderStatusAsync with backgroundModeEnabled always true like native; start* gated on the location UIBackgroundModes from the resolved expo config (infoPlist modes or the expo-location plugin flag), unavailable when undeclared; getCurrentPositionAsync, foreground-gated filtered getLastKnownPositionAsync, watchPositionAsync with native watch-id event routing and removal; foreground/background permission getters and requests; hasServicesEnabledAsync; isBackgroundLocationAvailableAsync=true; simulator-exact motion-activity unavailability; background location task registration and { locations } delivery through the task-manager seam with foreground no-deferral and distanceInterval filtering; startGeofencingAsync rejecting with ERR_GEOFENCING_UNAVAILABLE after the background permission check, as the reference iOS 27 simulator does (CLLocationManager.isMonitoringAvailable(for: CLCircularRegion.self) is false there)",
6568
+ missing: "device-orientation heading readings, an app-selected geocoder, Android network-provider enablement, deferred background delivery (deferredUpdatesInterval and deferredUpdatesDistance batching while the app is backgrounded), and task error delivery when the location manager fails"
6567
6569
  }
6568
6570
  ]
6569
6571
  },
@@ -6573,10 +6575,10 @@ var init_registry = __esm({
6573
6575
  versions: [
6574
6576
  {
6575
6577
  range: ">=0.20.0",
6576
- coverage: 0.93,
6577
- note: 'real local notification delivery via browser Notification + sootsim in-engine overlay; full upstream API surface present; matches iOS-simulator behavior where APNs registration silently fails. per docs/compat-scoring.md "score it high only when the full public API surface is present and returns the same unavailable/clean state an app should expect in development."',
6578
- working: "getPermissionsAsync/requestPermissionsAsync through the shared system permission registry, scheduleNotificationAsync (fires real browser Notification when host capability allows, plus the in-engine banner), cancelScheduledNotificationAsync, cancelAllScheduledNotificationsAsync, getAllScheduledNotificationsAsync, getPresentedNotificationsAsync, dismissNotificationAsync, dismissAllNotificationsAsync, setBadgeCountAsync (navigator.setAppBadge if available), getBadgeCountAsync, addNotificationReceivedListener/ResponseReceivedListener/DroppedListener/PushTokenListener, getLastNotificationResponseAsync + sync, clearLastNotificationResponseAsync + sync, useLastNotificationResponse hook, getNextTriggerDateAsync, setNotificationHandler, presentNotificationAsync, channel + channel-group CRUD, category CRUD, AndroidImportance/AndroidNotificationVisibility/IosAuthorizationStatus/PermissionStatus enums",
6579
- missing: "real push token registration (getDevicePushTokenAsync returns a placeholder UUID \u2014 there is no FCM/APNs provisioning, service-worker push subscription, or backend-uploadable token); background task delivery (registerTaskAsync/unregisterTaskAsync/isTaskRegisteredAsync are noops \u2014 service-worker push wiring is not implemented); scheduled-notification persistence across reload (setTimeout-only \u2014 page refresh drops the queue); subscribeToTopicAsync/unsubscribeFromTopicAsync FCM topic binding; setAutoServerRegistrationEnabledAsync"
6578
+ coverage: 0.95,
6579
+ note: 'real local notification delivery via browser Notification + sootsim in-engine overlay, plus push notifications from Detox sendUserNotification and launchApp({ userNotification }) dispatched the way DetoxUserNotificationDispatcher dispatches them (willPresent for an active app, the tap response otherwise and at launch, held until the emitter exists); full upstream API surface present; matches iOS-simulator behavior where APNs registration silently fails. per docs/compat-scoring.md "score it high only when the full public API surface is present and returns the same unavailable/clean state an app should expect in development."',
6580
+ working: "getPermissionsAsync/requestPermissionsAsync through the shared system permission registry, scheduleNotificationAsync (fires real browser Notification when host capability allows, plus the in-engine banner), cancelScheduledNotificationAsync, cancelAllScheduledNotificationsAsync, getAllScheduledNotificationsAsync, getPresentedNotificationsAsync, dismissNotificationAsync, dismissAllNotificationsAsync, setBadgeCountAsync (navigator.setAppBadge if available), getBadgeCountAsync, addNotificationReceivedListener/ResponseReceivedListener/DroppedListener/PushTokenListener, getLastNotificationResponseAsync + sync, clearLastNotificationResponseAsync + sync, useLastNotificationResponse hook, getNextTriggerDateAsync, setNotificationHandler, presentNotificationAsync, push notifications dispatched like Detox (onDidReceiveNotification with trigger { type: push, payload } and data from the user info body, tap responses with action identifier and user text, the launch tap as the last response), the native handler timeout presenting with the default options, channel + channel-group CRUD, category CRUD, AndroidImportance/AndroidNotificationVisibility/IosAuthorizationStatus/PermissionStatus enums",
6581
+ missing: "real push token registration (getDevicePushTokenAsync returns a placeholder UUID \u2014 there is no FCM/APNs provisioning, service-worker push subscription, or backend-uploadable token); background task delivery (registerTaskAsync/unregisterTaskAsync/isTaskRegisteredAsync are noops \u2014 service-worker push wiring is not implemented); scheduled-notification persistence across reload (setTimeout-only \u2014 page refresh drops the queue); subscribeToTopicAsync/unsubscribeFromTopicAsync FCM topic binding; setAutoServerRegistrationEnabledAsync; push notifications arriving from APNs or simctl push (only the Detox dispatch path exists, so there is no system banner for a push to a backgrounded app and no didReceiveRemoteNotification background delivery)"
6580
6582
  }
6581
6583
  ]
6582
6584
  },
@@ -7416,8 +7418,8 @@ var init_registry = __esm({
7416
7418
  range: ">=2.11.4 <3.0.0",
7417
7419
  coverage: 0.76,
7418
7420
  note: "the real upstream PickerIOS wrapper runs from the guest bundle and resolves codegenNativeComponent('RNCPicker') to the engine UIPickerView wheel. upstream owns Picker.Item mapping, controlled selection, and callback fan-out.",
7419
- working: "real upstream Picker, PickerIOS, Picker.Item label/value, selectedValue, onValueChange(value, index), itemStyle color/fontSize, MODE_DIALOG and MODE_DROPDOWN constants",
7420
- missing: "Picker.Item color/testID, themeVariant, textAlign, itemStyle fontFamily/fontWeight/fontStyle, multi-line numberOfLines behavior, controlled native selection restore proof, deprecated onChange proof, android dialog/dropdown presentation (mode/prompt/dropdownIconColor render as the iOS wheel), focus()/blur() imperative handle, onFocus/onBlur; selectionColor is ignored by the exact 2.11.4 Fabric iOS host and Peach matches that behavior"
7421
+ working: "real upstream Picker, PickerIOS, Picker.Item label/value, selectedValue, onValueChange(value, index), itemStyle color/fontSize/fontFamily/fontWeight/fontStyle (fontSize Int32-truncated, 0 keeps the 21pt system font), per-item textColor over the itemStyle color, Picker.Item testID row labels, themeVariant dark/light interface-style override, numberOfLines label and row height, MODE_DIALOG and MODE_DROPDOWN constants",
7422
+ missing: "textAlign (dead prop on the native host: row alignment reads themeVariant), multi-line numberOfLines proof; the dark/light default label color, a loaded fontFamily, themeVariant-driven label alignment, and fontSize truncation follow RNCPicker but have no paired proof, controlled native selection restore proof, deprecated onChange proof, android dialog/dropdown presentation (mode/prompt/dropdownIconColor render as the iOS wheel), focus()/blur() imperative handle, onFocus/onBlur; selectionColor is ignored by the exact 2.11.4 Fabric iOS host and Peach matches that behavior"
7421
7423
  }
7422
7424
  ]
7423
7425
  },
@@ -7572,10 +7574,10 @@ var init_registry = __esm({
7572
7574
  versions: [
7573
7575
  {
7574
7576
  range: ">=12.0.0",
7575
- coverage: 0.85,
7576
- note: "upstream expo-sharing JS runs unchanged; package-local wrapper only registers the ExpoSharing native module. an empty incoming-share inbox is the state a browser preview should report, so the share-into surface scores as present.",
7577
- working: "isAvailableAsync (real browser check), shareAsync calls navigator.share({url,title}) when available and safely noops otherwise, getSharedPayloads/getResolvedSharedPayloadsAsync/clearSharedPayloads report an empty incoming-share inbox",
7578
- missing: "native share sheet UI, native file readability validation, incoming share extension app-group storage. the share-into functions also diverge on a misconfigured app: iOS throws FailedToResolveAppGroupIdException from all three when the app declares no ExpoShareIntoAppGroupId (expo-sharing/ios/SharingModule.swift:4-11) where sootsim returns the empty inbox, so an app that redboxes on device previews cleanly"
7577
+ coverage: 0.9,
7578
+ note: "upstream expo-sharing JS runs unchanged; package-local wrapper only registers the ExpoSharing native module. shareAsync presents the same canvas share sheet core RN Share uses; a declared app group id reads the empty inbox a browser preview should report.",
7579
+ working: "isAvailableAsync through upstream js, shareAsync presenting the shared canvas sheet and resolving on dismissal whatever the outcome, dialogTitle accepted and not shown as on iOS, ERR_FILE_PERMISSION for unreadable files, ERR_MISSING_CURRENT_VIEW_CONTROLLER without a sheet host, ERR_FAILED_TO_RESOLVE_APP_GROUP_ID from all three share-into entries when the app declares no ExpoShareIntoAppGroupId, empty incoming-share inbox when it does",
7580
+ missing: "incoming share extension app-group storage (no extension writes into the group from a preview, so a declared key always reads empty); UTI/mimeType file restaging and iPad popover anchoring have no visible effect on the iPhone-idiom sheet"
7579
7581
  }
7580
7582
  ]
7581
7583
  },
@@ -7977,7 +7979,7 @@ var init_registry = __esm({
7977
7979
  {
7978
7980
  range: ">=14.0.0",
7979
7981
  coverage: 0.8,
7980
- note: "printAsync routes through real window.print() (browser print dialog). printToFileAsync now produces a REAL, structurally-valid PDF (verified by `file`/macOS as com.adobe.pdf) carrying the html's text content + a real page count, written to the same in-memory cache the expo-file-system stub serves \u2014 so the returned `file:///sootsim-cache/Print/*.pdf` URI resolves to genuine PDF bytes (readAsStringAsync / FormData upload / Sharing / Print preview all work), and base64 (when requested) decodes to those bytes. This replaces the old `mock://print.pdf` dangling URI that broke every consumer. Fidelity caveat: the worker has no WebView render engine, so the PDF reproduces the document's TEXT (paginated, escaped) but NOT CSS/image layout \u2014 a degraded-but-real simulation of iOS's WKWebView\u2192UIPrintPageRenderer output.",
7982
+ note: "printAsync routes through real window.print() (browser print dialog). printToFileAsync now produces a REAL, structurally-valid PDF (verified by `file`/macOS as com.adobe.pdf) carrying the html's text content + a real page count, written to the same in-memory cache the expo-file-system stub serves \u2014 so the returned `<cacheDirectory>Print/*.pdf` URI resolves to genuine PDF bytes (readAsStringAsync / FormData upload / Sharing / Print preview all work), and base64 (when requested) decodes to those bytes. This replaces the old `mock://print.pdf` dangling URI that broke every consumer. Fidelity caveat: the worker has no WebView render engine, so the PDF reproduces the document's TEXT (paginated, escaped) but NOT CSS/image layout \u2014 a degraded-but-real simulation of iOS's WKWebView\u2192UIPrintPageRenderer output.",
7981
7983
  working: "upstream validation and concurrency guard, printAsync (real window.print), printToFileAsync (real PDF bytes: %PDF header, catalog/pages tree, xref, paginated text content, real numberOfPages, real file:// uri + base64), selectPrinterAsync, Orientation enum",
7982
7984
  missing: "CSS/image/font layout fidelity in the generated PDF (text-only render \u2014 needs a worker WebView engine to match WKWebView pixel output), useMarkupFormatter (same text path), printerUrl/AirPrint physical printing, native printer selection sheet (selectPrinterAsync returns undefined)"
7983
7985
  }
@@ -8003,10 +8005,10 @@ var init_registry = __esm({
8003
8005
  versions: [
8004
8006
  {
8005
8007
  range: ">=4.0.0",
8006
- coverage: 0.9,
8008
+ coverage: 0.95,
8007
8009
  note: "the published two-function package runs unchanged; its native seam applies processed colors to the tenant-owned app root that Peach synchronizes to the CanvasKit compositor",
8008
- working: "setBackgroundColorAsync and getBackgroundColorAsync round-trip the visible app-surface background, including React Native processed-color conversion; null restores the neutral app surface",
8009
- missing: "the value does not persist across simulator restarts, and null restores Peach\u2019s neutral surface rather than the exact platform default color"
8010
+ working: "setBackgroundColorAsync and getBackgroundColorAsync round-trip the visible app-surface background, including React Native processed-color conversion; set colors persist across restarts through tenant storage and are reapplied at launch (falling back to the app-config background color the config plugin writes to Info.plist); null snapshots the current interface style (black in dark, white otherwise) and clears the stored color; a fresh launch with no stored or app-config color reads that snapshot; the getter returns uppercase #RRGGBB",
8011
+ missing: "persistence across restarts and the app-config fallback have no paired relaunch proof"
8010
8012
  }
8011
8013
  ]
8012
8014
  },
@@ -9986,9 +9988,10 @@ var init_registry = __esm({
9986
9988
  versions: [
9987
9989
  {
9988
9990
  range: ">=2.0.0",
9989
- coverage: 0,
9990
- note: "the reference iOS simulator performs this package's behavior. Peach currently provides only this unsupported shape: TrueSheet component passes children through; present/dismiss/resize/scrollToTop ref methods accepted as noops. imports and API shape receive no behavior credit",
9991
- missing: "no native bottom-sheet UI. recommended swap on web: a regular sootsim modal \u2014 sootsim already simulates iOS sheet semantics natively"
9991
+ coverage: 0.6,
9992
+ note: "the published TrueSheet JS runs unchanged; TrueSheetView presents through the engine's react-native-screens form sheet. paired native and Peach runtime proof covers present at a half detent, resize to the large detent, dismiss, and the mount, willPresent, didPresent, detentChange, willDismiss, and didDismiss events.",
9993
+ working: "present, resize, and dismiss, by ref or by name; fractional detents, initialDetentIndex, maxContentHeight, and maxContentWidth; cornerRadius, backgroundColor, grabber, dimmed, dimmedDetentIndex, and dismissible; header; onMount, onWillPresent, onDidPresent, onDetentChange, onWillDismiss, onDidDismiss, and the focus and blur events",
9994
+ missing: "onPositionChange fires only when a detent settles, and onDragBegin, onDragChange, and onDragEnd never fire; the footer is not pinned to the bottom edge; a sheet mounted with initialDetentIndex presents without onWillFocus and onWillPresent; the auto detent reports the large card position and index -1 while the sheet draws fitted to its content; the peek detent; insetAdjustment never also drops the bottom inset on iPad; scrollable, backgroundBlur, blurOptions, detached, anchor, draggable, and grabberOptions"
9992
9995
  }
9993
9996
  ]
9994
9997
  },
@@ -10046,9 +10049,10 @@ var init_registry = __esm({
10046
10049
  versions: [
10047
10050
  {
10048
10051
  range: ">=1.0.0",
10049
- coverage: 0,
10050
- note: "the reference iOS simulator performs this package's behavior. Peach currently provides only this unsupported shape: ContextMenuView passes children through; long-press shows nothing instead of a native UIContextMenu. apps still render their primary content correctly. imports and API shape receive no behavior credit",
10051
- missing: "no native context menu \u2014 recommended swap on web: a tap-on-icon menu via Pressable + Modal"
10052
+ coverage: 0.75,
10053
+ note: "the published ContextMenu JS runs unchanged; the ContextMenu native view follows the installed ContextMenuView.m over the engine's UIMenu presenter. paired native and Peach runtime proof covers a long-press menu with a title, subtitles, system icons, a submenu, and destructive, selected, and disabled actions, the onPress index, indexPath, and name, onCancel after a backdrop dismiss, and a dropdownMenuMode tap menu.",
10054
+ working: "long-press context menu with a title; actions with subtitle, systemIcon, iconColor, titleColor, destructive, selected, disabled, and nested actions as submenus; onPress with index, indexPath, and name; onCancel with an empty event only when the menu closes without an action; dropdownMenuMode; disabled",
10055
+ missing: "the custom preview and onPreviewPress (the lifted view is the content itself); asset `icon` images; inlineChildren; previewBackgroundColor, the border radius props that clip the lifted preview, and disableShadow; fontName"
10052
10056
  }
10053
10057
  ]
10054
10058
  },
@@ -10220,9 +10224,10 @@ var init_registry = __esm({
10220
10224
  versions: [
10221
10225
  {
10222
10226
  range: ">=1.0.0",
10223
- coverage: 0,
10224
- note: "the reference iOS simulator renders the LiquidGlassView effect. Peach passes children through without connecting the view to its blur primitives, so import safety receives no behavior credit",
10225
- missing: "no native iOS visual effect; consumers can substitute expo-blur which we already polyfill"
10227
+ coverage: 0.8,
10228
+ note: "the published LiquidGlassView and LiquidGlassContainerView JS runs unchanged; the native views draw through the engine's liquid glass material. paired native and Peach runtime proof covers clear, tinted regular, dark regular, and none effects over a striped backdrop, and isLiquidGlassSupported.",
10229
+ working: "LiquidGlassView effect clear, regular, and none, tintColor, colorScheme, interactive, and its border radius; LiquidGlassContainerView layout; isLiquidGlassSupported",
10230
+ missing: "LiquidGlassContainerView does not merge neighbouring glass, so spacing does nothing; an effect change does not animate; isLiquidGlassSupported is always true, including when the app sets UIDesignRequiresCompatibility"
10226
10231
  }
10227
10232
  ]
10228
10233
  },
@@ -11504,9 +11509,10 @@ var init_registry = __esm({
11504
11509
  versions: [
11505
11510
  {
11506
11511
  range: ">=1.0.0",
11507
- coverage: 0,
11508
- note: "the reference iOS simulator performs this package's behavior. Peach currently provides only this unsupported shape: getCurrentConfigurations={}; reload* accepted. imports and API shape receive no behavior credit",
11509
- missing: "no WidgetKit on iOS / web"
11512
+ coverage: 0.9,
11513
+ note: "Widgetkit native module: App Group UserDefaults getItem/setItem, sharing suites with react-native-shared-group-preferences; timeline reloads accepted",
11514
+ working: "getItem, setItem, reloadAllTimelines, reloadTimelines",
11515
+ missing: "no home screen widgets render in Peach, so reloads refresh nothing"
11510
11516
  }
11511
11517
  ]
11512
11518
  },
@@ -18717,7 +18723,7 @@ function formatDetailedScanLines(result) {
18717
18723
  );
18718
18724
  }
18719
18725
  lines.push(
18720
- "SootSim may still work. The simulator will report a concrete runtime error if it finds a gap."
18726
+ "Peach may still work. The simulator will report a concrete runtime error if it finds a gap."
18721
18727
  );
18722
18728
  }
18723
18729
  return lines;
@@ -18767,6 +18773,7 @@ var init_sleep = __esm({
18767
18773
  // src/bridge-constants.ts
18768
18774
  var bridge_constants_exports = {};
18769
18775
  __export(bridge_constants_exports, {
18776
+ AUTOMATION_SIM_IDLE_REAP_TTL_MS: () => AUTOMATION_SIM_IDLE_REAP_TTL_MS,
18770
18777
  DEFAULT_SOOTSIM_BRIDGE_PORT: () => DEFAULT_SOOTSIM_BRIDGE_PORT,
18771
18778
  DEFAULT_SOOTSIM_BRIDGE_URL: () => DEFAULT_SOOTSIM_BRIDGE_URL,
18772
18779
  SOOTSIM_BRIDGE_SIM_CLOSE_CODE: () => SOOTSIM_BRIDGE_SIM_CLOSE_CODE,
@@ -18877,7 +18884,7 @@ function resolveSootsimBridgeWebSocketUrl(input) {
18877
18884
  function isSootsimBridgeHealth(value, expectedPort) {
18878
18885
  return typeof value === "object" && value !== null && Reflect.get(value, "ok") === true && Reflect.get(value, "bridgePort") === expectedPort;
18879
18886
  }
18880
- var DEFAULT_SOOTSIM_BRIDGE_PORT, DEFAULT_SOOTSIM_BRIDGE_URL, CONTRAST_DEV_HTTP_BASE_PORT, SOOTSIM_SITE_DEV_HTTP_BASE_PORT, SOOTSIM_SHELL_DEV_HTTP_BASE_PORT, LOCAL_DEV_HTTP_BASE_PORTS, MAX_LOCAL_DEV_PORT_OFFSET, SOOTSIM_BRIDGE_SIM_CLOSE_CODE, SOOTSIM_BRIDGE_SIM_CLOSE_REASON;
18887
+ var DEFAULT_SOOTSIM_BRIDGE_PORT, DEFAULT_SOOTSIM_BRIDGE_URL, CONTRAST_DEV_HTTP_BASE_PORT, SOOTSIM_SITE_DEV_HTTP_BASE_PORT, SOOTSIM_SHELL_DEV_HTTP_BASE_PORT, LOCAL_DEV_HTTP_BASE_PORTS, MAX_LOCAL_DEV_PORT_OFFSET, SOOTSIM_BRIDGE_SIM_CLOSE_CODE, SOOTSIM_BRIDGE_SIM_CLOSE_REASON, AUTOMATION_SIM_IDLE_REAP_TTL_MS;
18881
18888
  var init_bridge_constants = __esm({
18882
18889
  "src/bridge-constants.ts"() {
18883
18890
  "use strict";
@@ -18893,6 +18900,170 @@ var init_bridge_constants = __esm({
18893
18900
  MAX_LOCAL_DEV_PORT_OFFSET = 2e3;
18894
18901
  SOOTSIM_BRIDGE_SIM_CLOSE_CODE = 4001;
18895
18902
  SOOTSIM_BRIDGE_SIM_CLOSE_REASON = "sootsim close";
18903
+ AUTOMATION_SIM_IDLE_REAP_TTL_MS = 5 * 6e4;
18904
+ }
18905
+ });
18906
+
18907
+ // ../contrast/src/sleep.ts
18908
+ function sleep2(ms) {
18909
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
18910
+ }
18911
+ var init_sleep2 = __esm({
18912
+ "../contrast/src/sleep.ts"() {
18913
+ "use strict";
18914
+ }
18915
+ });
18916
+
18917
+ // ../contrast/src/runtime/process-group.ts
18918
+ function signalProcessGroup(child, signal) {
18919
+ if (!child.pid) return;
18920
+ try {
18921
+ process.kill(-child.pid, signal);
18922
+ } catch {
18923
+ try {
18924
+ process.kill(child.pid, signal);
18925
+ } catch {
18926
+ }
18927
+ }
18928
+ }
18929
+ function runningPids(pids) {
18930
+ return pids.filter((pid) => {
18931
+ try {
18932
+ process.kill(pid, 0);
18933
+ return true;
18934
+ } catch (error) {
18935
+ return error instanceof Error && "code" in error && error.code === "EPERM";
18936
+ }
18937
+ });
18938
+ }
18939
+ function groupIsEmpty(pgid) {
18940
+ try {
18941
+ process.kill(-pgid, 0);
18942
+ } catch (error) {
18943
+ if (!(error instanceof Error && "code" in error && error.code === "EPERM"))
18944
+ return true;
18945
+ }
18946
+ const result = (0, import_node_child_process2.spawnSync)("ps", ["-A", "-o", "pgid=,stat="], {
18947
+ encoding: "utf8",
18948
+ stdio: ["ignore", "pipe", "pipe"]
18949
+ });
18950
+ if (result.error || result.status !== 0) return false;
18951
+ return !result.stdout.split("\n").some((line) => {
18952
+ const [group, state] = line.trim().split(/\s+/);
18953
+ return Number(group) === pgid && state !== void 0 && !state.startsWith("Z");
18954
+ });
18955
+ }
18956
+ async function waitForExit(pids, pgid, timeoutMs) {
18957
+ const deadline = Date.now() + timeoutMs;
18958
+ const gone = () => runningPids(pids).length === 0 && groupIsEmpty(pgid);
18959
+ while (Date.now() < deadline) {
18960
+ if (gone()) return true;
18961
+ await sleep2(50);
18962
+ }
18963
+ return gone();
18964
+ }
18965
+ async function terminateProcessGroup(child, signal = "SIGTERM", gracefulTimeoutMs = GRACEFUL_TIMEOUT_MS) {
18966
+ if (!child.pid) {
18967
+ try {
18968
+ child.kill(signal);
18969
+ } catch {
18970
+ }
18971
+ return;
18972
+ }
18973
+ signalProcessGroup(child, "SIGSTOP");
18974
+ const descendants = /* @__PURE__ */ new Set();
18975
+ let parents = [child.pid];
18976
+ let inspectionError = null;
18977
+ while (parents.length > 0) {
18978
+ const result = (0, import_node_child_process2.spawnSync)("pgrep", ["-P", parents.join(",")], {
18979
+ encoding: "utf8",
18980
+ stdio: ["ignore", "pipe", "pipe"]
18981
+ });
18982
+ if (result.error || result.status !== 0 && result.status !== 1) {
18983
+ inspectionError = new Error(
18984
+ `could not inspect descendants of processes ${parents.join(",")}: ${result.error?.message || result.stderr.trim() || `pgrep exited ${result.status}`}`
18985
+ );
18986
+ break;
18987
+ }
18988
+ const next = [];
18989
+ for (const line of result.stdout.split("\n")) {
18990
+ const descendantPid = Number(line.trim());
18991
+ if (Number.isInteger(descendantPid) && descendantPid > 1 && !descendants.has(descendantPid)) {
18992
+ descendants.add(descendantPid);
18993
+ next.push(descendantPid);
18994
+ try {
18995
+ process.kill(descendantPid, "SIGSTOP");
18996
+ } catch {
18997
+ }
18998
+ }
18999
+ }
19000
+ parents = next;
19001
+ }
19002
+ const pids = [child.pid, ...descendants];
19003
+ const gracefulDeadline = Date.now() + gracefulTimeoutMs;
19004
+ try {
19005
+ process.kill(child.pid, signal);
19006
+ } catch {
19007
+ }
19008
+ signalProcessGroup(child, "SIGCONT");
19009
+ for (const pid of descendants) {
19010
+ try {
19011
+ process.kill(pid, "SIGCONT");
19012
+ } catch {
19013
+ }
19014
+ }
19015
+ let rootExited = child.exitCode !== null || child.signalCode !== null;
19016
+ if (!rootExited) {
19017
+ rootExited = await new Promise((resolveExit) => {
19018
+ const onExit = () => {
19019
+ clearTimeout(deadlineTimer);
19020
+ resolveExit(true);
19021
+ };
19022
+ const deadlineTimer = setTimeout(
19023
+ () => {
19024
+ child.removeListener("exit", onExit);
19025
+ resolveExit(false);
19026
+ },
19027
+ Math.max(0, gracefulDeadline - Date.now())
19028
+ );
19029
+ child.once("exit", onExit);
19030
+ if (child.exitCode !== null || child.signalCode !== null) {
19031
+ child.removeListener("exit", onExit);
19032
+ onExit();
19033
+ }
19034
+ });
19035
+ }
19036
+ if (rootExited) {
19037
+ signalProcessGroup(child, signal);
19038
+ for (const pid of descendants) {
19039
+ try {
19040
+ process.kill(pid, signal);
19041
+ } catch {
19042
+ }
19043
+ }
19044
+ }
19045
+ if (await waitForExit(pids, child.pid, Math.max(0, gracefulDeadline - Date.now()))) {
19046
+ if (inspectionError) throw inspectionError;
19047
+ return;
19048
+ }
19049
+ for (const pid of pids) {
19050
+ try {
19051
+ process.kill(pid, "SIGKILL");
19052
+ } catch {
19053
+ }
19054
+ }
19055
+ signalProcessGroup(child, "SIGKILL");
19056
+ await waitForExit(pids, child.pid, FORCED_EXIT_TIMEOUT_MS);
19057
+ if (inspectionError) throw inspectionError;
19058
+ }
19059
+ var import_node_child_process2, GRACEFUL_TIMEOUT_MS, FORCED_EXIT_TIMEOUT_MS;
19060
+ var init_process_group = __esm({
19061
+ "../contrast/src/runtime/process-group.ts"() {
19062
+ "use strict";
19063
+ import_node_child_process2 = require("node:child_process");
19064
+ init_sleep2();
19065
+ GRACEFUL_TIMEOUT_MS = 3e3;
19066
+ FORCED_EXIT_TIMEOUT_MS = 1e3;
18896
19067
  }
18897
19068
  });
18898
19069
 
@@ -19456,28 +19627,51 @@ var init_optional_demo_registry = __esm({
19456
19627
  });
19457
19628
 
19458
19629
  // scripts/dev-server-scanner.ts
19459
- async function execGuarded(command, timeout, extra = {}) {
19460
- let child;
19461
- const ran = new Promise((resolve9, reject) => {
19462
- child = (0, import_child_process.exec)(command, { encoding: "utf8", timeout, ...extra }, (err, stdout) => {
19630
+ function execGuarded(command, timeout, extra = {}) {
19631
+ const maxBuffer = extra.maxBuffer ?? 1024 * 1024;
19632
+ return new Promise((resolve9, reject) => {
19633
+ const child = (0, import_child_process.spawn)("/bin/sh", ["-c", command], {
19634
+ detached: true,
19635
+ stdio: ["ignore", "pipe", "ignore"]
19636
+ });
19637
+ let stdout = "";
19638
+ let settled = false;
19639
+ let killing = false;
19640
+ const settle = (err) => {
19641
+ if (settled) return;
19642
+ settled = true;
19643
+ clearTimeout(deadline);
19463
19644
  if (err) reject(err);
19464
19645
  else resolve9({ stdout });
19646
+ };
19647
+ const killThenSettle = (err) => {
19648
+ if (killing) return;
19649
+ killing = true;
19650
+ clearTimeout(deadline);
19651
+ terminateProcessGroup(child, "SIGKILL", 0).finally(() => settle(err));
19652
+ };
19653
+ const deadline = setTimeout(() => {
19654
+ killThenSettle(
19655
+ Object.assign(new Error(`\`${command}\` did not exit within ${timeout}ms`), {
19656
+ killed: true
19657
+ })
19658
+ );
19659
+ }, timeout);
19660
+ child.stdout.setEncoding("utf8");
19661
+ child.stdout.on("data", (chunk) => {
19662
+ stdout += chunk;
19663
+ if (stdout.length > maxBuffer) {
19664
+ killThenSettle(new Error(`\`${command}\` exceeded maxBuffer ${maxBuffer}`));
19665
+ }
19666
+ });
19667
+ child.on("error", (err) => {
19668
+ if (!killing) settle(err);
19669
+ });
19670
+ child.on("close", (code) => {
19671
+ if (killing) return;
19672
+ settle(code === 0 ? null : new Error(`\`${command}\` exited with ${code}`));
19465
19673
  });
19466
19674
  });
19467
- let deadlineTimer;
19468
- const deadline = new Promise((_resolve, reject) => {
19469
- deadlineTimer = setTimeout(() => {
19470
- child?.kill("SIGKILL");
19471
- const err = new Error(`\`${command}\` did not exit within ${timeout}ms`);
19472
- Object.assign(err, { killed: true });
19473
- reject(err);
19474
- }, timeout + EXEC_EXIT_GRACE_MS);
19475
- });
19476
- try {
19477
- return await Promise.race([ran, deadline]);
19478
- } finally {
19479
- clearTimeout(deadlineTimer);
19480
- }
19481
19675
  }
19482
19676
  async function tcpPing(port, timeout = TCP_GATE_MS) {
19483
19677
  try {
@@ -19508,6 +19702,7 @@ async function tcpPing(port, timeout = TCP_GATE_MS) {
19508
19702
  function httpGet(hostname, port, path21, method = "GET", timeout = TIMEOUT_MS, headers = {}) {
19509
19703
  return new Promise((resolve9) => {
19510
19704
  let settled = false;
19705
+ let killing = false;
19511
19706
  const settle = (value) => {
19512
19707
  if (settled) return;
19513
19708
  settled = true;
@@ -19562,6 +19757,20 @@ function wasKilledByTimeout(err) {
19562
19757
  }
19563
19758
  async function scanListeningProcesses(excludePorts = []) {
19564
19759
  const excluded = new Set(excludePorts);
19760
+ const listing = await listListenersShared();
19761
+ if (listing.state === "could-not-look") return listing;
19762
+ return {
19763
+ state: "looked",
19764
+ processes: listing.processes.filter((p) => acceptPort(p.port, excluded))
19765
+ };
19766
+ }
19767
+ function listListenersShared() {
19768
+ inflightListing ??= listListeners().finally(() => {
19769
+ inflightListing = null;
19770
+ });
19771
+ return inflightListing;
19772
+ }
19773
+ async function listListeners() {
19565
19774
  let timedOut = false;
19566
19775
  try {
19567
19776
  const { stdout } = await execGuarded(
@@ -19573,11 +19782,9 @@ async function scanListeningProcesses(excludePorts = []) {
19573
19782
  const parts = line.trim().split(/\s+/);
19574
19783
  if (parts.length < 9) continue;
19575
19784
  const pid = Number(parts[1]);
19576
- const addr = parts[8];
19577
- const m = addr.match(/:(\d+)$/);
19785
+ const m = parts[8].match(/:(\d+)$/);
19578
19786
  if (!m) continue;
19579
19787
  const port = Number(m[1]);
19580
- if (!acceptPort(port, excluded)) continue;
19581
19788
  if (!seen.has(port)) seen.set(port, pid);
19582
19789
  }
19583
19790
  return {
@@ -19599,7 +19806,6 @@ async function scanListeningProcesses(excludePorts = []) {
19599
19806
  if (!portMatch) continue;
19600
19807
  const port = Number(portMatch[1]);
19601
19808
  const pid = pidMatch ? Number(pidMatch[1]) : 0;
19602
- if (!acceptPort(port, excluded)) continue;
19603
19809
  if (!seen.has(port)) seen.set(port, pid);
19604
19810
  }
19605
19811
  return {
@@ -19610,10 +19816,7 @@ async function scanListeningProcesses(excludePorts = []) {
19610
19816
  if (wasKilledByTimeout(err)) timedOut = true;
19611
19817
  }
19612
19818
  if (timedOut) return { state: "could-not-look" };
19613
- return {
19614
- state: "looked",
19615
- processes: FALLBACK_PORTS.filter((p) => acceptPort(p.port, excluded))
19616
- };
19819
+ return { state: "looked", processes: FALLBACK_PORTS };
19617
19820
  }
19618
19821
  function pidFromNetstat(stdout, port) {
19619
19822
  for (const line of stdout.split("\n")) {
@@ -20097,11 +20300,12 @@ async function scanDevServers(opts = {}) {
20097
20300
  }
20098
20301
  return results.filter((r) => !isSootSimSelfServer(r));
20099
20302
  }
20100
- var import_child_process, import_promises2, import_http, import_net, import_node_fs2, import_node_path3, EXEC_EXIT_GRACE_MS, TIMEOUT_MS, MANIFEST_TIMEOUT_MS, TCP_GATE_MS, BODY_TIMEOUT_MS, FALLBACK_PORTS, SELF_PORT_RANGE_START, SELF_PORT_RANGE_END, DevServerScanUnavailableError, LISTING_TIMEOUT_MS, cwdByPid, localIconPathCache, localIconProbeInFlight, ENRICH_TIMEOUT_MS, enrichmentCache, enrichmentInFlight, knownNonPatched, knownNonExpo, knownOne, portCache, portCacheInvalidatedAt, NEGATIVE_CACHE_TTL_MS, WEAK_RESULT_CACHE_TTL_MS;
20303
+ var import_child_process, import_promises2, import_http, import_net, import_node_fs2, import_node_path3, TIMEOUT_MS, MANIFEST_TIMEOUT_MS, TCP_GATE_MS, BODY_TIMEOUT_MS, FALLBACK_PORTS, SELF_PORT_RANGE_START, SELF_PORT_RANGE_END, DevServerScanUnavailableError, LISTING_TIMEOUT_MS, inflightListing, cwdByPid, localIconPathCache, localIconProbeInFlight, ENRICH_TIMEOUT_MS, enrichmentCache, enrichmentInFlight, knownNonPatched, knownNonExpo, knownOne, portCache, portCacheInvalidatedAt, NEGATIVE_CACHE_TTL_MS, WEAK_RESULT_CACHE_TTL_MS;
20101
20304
  var init_dev_server_scanner = __esm({
20102
20305
  "scripts/dev-server-scanner.ts"() {
20103
20306
  "use strict";
20104
20307
  import_child_process = require("child_process");
20308
+ init_process_group();
20105
20309
  import_promises2 = require("fs/promises");
20106
20310
  import_http = __toESM(require("http"), 1);
20107
20311
  import_net = __toESM(require("net"), 1);
@@ -20112,7 +20316,6 @@ var init_dev_server_scanner = __esm({
20112
20316
  init_native_dev_bundle_url();
20113
20317
  init_settings_bundle();
20114
20318
  init_optional_demo_registry();
20115
- EXEC_EXIT_GRACE_MS = 2e3;
20116
20319
  TIMEOUT_MS = 250;
20117
20320
  MANIFEST_TIMEOUT_MS = 1500;
20118
20321
  TCP_GATE_MS = 120;
@@ -20137,6 +20340,7 @@ var init_dev_server_scanner = __esm({
20137
20340
  }
20138
20341
  };
20139
20342
  LISTING_TIMEOUT_MS = 1e4;
20343
+ inflightListing = null;
20140
20344
  cwdByPid = /* @__PURE__ */ new Map();
20141
20345
  localIconPathCache = /* @__PURE__ */ new Map();
20142
20346
  localIconProbeInFlight = /* @__PURE__ */ new Set();
@@ -20910,10 +21114,12 @@ function createRuntimeDelivery(product) {
20910
21114
  );
20911
21115
  import_node_fs6.default.rmSync(tmpDir, { recursive: true, force: true });
20912
21116
  try {
20913
- process.stderr.write(`${product.id}: downloading runtime ${version}\u2026
21117
+ if (!opts.quiet)
21118
+ process.stderr.write(`${product.id}: downloading runtime ${version}\u2026
20914
21119
  `);
20915
21120
  await downloadToFile(tarUrl, runtimeArchivePath);
20916
- process.stderr.write(`${product.id}: extracting runtime ${version}\u2026
21121
+ if (!opts.quiet)
21122
+ process.stderr.write(`${product.id}: extracting runtime ${version}\u2026
20917
21123
  `);
20918
21124
  const actualSha = await sha256File(runtimeArchivePath);
20919
21125
  if (actualSha !== entry.sha256) {
@@ -21157,7 +21363,7 @@ function sha256File(filePath) {
21157
21363
  }
21158
21364
  function extractTarball(tarPath, destDir) {
21159
21365
  return new Promise((resolve9, reject) => {
21160
- const child = (0, import_node_child_process2.spawn)("tar", ["-xzf", tarPath, "-C", destDir], {
21366
+ const child = (0, import_node_child_process3.spawn)("tar", ["-xzf", tarPath, "-C", destDir], {
21161
21367
  stdio: ["ignore", "inherit", "inherit"]
21162
21368
  });
21163
21369
  child.on("error", reject);
@@ -21167,11 +21373,11 @@ function extractTarball(tarPath, destDir) {
21167
21373
  });
21168
21374
  });
21169
21375
  }
21170
- var import_node_child_process2, import_node_crypto2, import_node_fs6, import_node_os2, import_node_path6, import_node_stream, import_promises3, DEFAULT_LOCAL_RUNTIME_RETENTION_COUNT;
21376
+ var import_node_child_process3, import_node_crypto2, import_node_fs6, import_node_os2, import_node_path6, import_node_stream, import_promises3, DEFAULT_LOCAL_RUNTIME_RETENTION_COUNT;
21171
21377
  var init_src = __esm({
21172
21378
  "../contrast-runtime-delivery/src/index.ts"() {
21173
21379
  "use strict";
21174
- import_node_child_process2 = require("node:child_process");
21380
+ import_node_child_process3 = require("node:child_process");
21175
21381
  import_node_crypto2 = __toESM(require("node:crypto"), 1);
21176
21382
  import_node_fs6 = __toESM(require("node:fs"), 1);
21177
21383
  import_node_os2 = __toESM(require("node:os"), 1);
@@ -21255,6 +21461,41 @@ function resolveRuntimeRootForHostname(hostname) {
21255
21461
  const version = runtimeVersionFromHostname(hostname);
21256
21462
  return version ? resolveInstalledRuntimeRoot(version) : null;
21257
21463
  }
21464
+ function sendFileWithRanges(req, res, filePath, total, headers) {
21465
+ const head = (req?.method || "GET").toUpperCase() === "HEAD";
21466
+ const range = /^bytes=(\d*)-(\d*)$/.exec(req?.headers?.range || "");
21467
+ const invalid = range && range[1] && range[2] && Number(range[2]) < Number(range[1]);
21468
+ if (range && (range[1] || range[2]) && !invalid) {
21469
+ const [, startRaw, endRaw] = range;
21470
+ const start = startRaw ? Number(startRaw) : Math.max(0, total - Number(endRaw));
21471
+ const end = startRaw && endRaw ? Math.min(Number(endRaw), total - 1) : total - 1;
21472
+ if (start > end || start >= total) {
21473
+ res.writeHead(416, { ...headers, "Content-Range": `bytes */${total}` });
21474
+ res.end();
21475
+ return;
21476
+ }
21477
+ res.writeHead(206, {
21478
+ ...headers,
21479
+ "Content-Range": `bytes ${start}-${end}/${total}`,
21480
+ "Accept-Ranges": "bytes",
21481
+ "Content-Length": String(end - start + 1)
21482
+ });
21483
+ if (head) res.end();
21484
+ else streamFile(res, import_fs.default.createReadStream(filePath, { start, end }));
21485
+ return;
21486
+ }
21487
+ res.writeHead(200, {
21488
+ ...headers,
21489
+ "Accept-Ranges": "bytes",
21490
+ "Content-Length": String(total)
21491
+ });
21492
+ if (head || total === 0) res.end();
21493
+ else streamFile(res, import_fs.default.createReadStream(filePath, { start: 0, end: total - 1 }));
21494
+ }
21495
+ function streamFile(res, stream) {
21496
+ stream.on("error", (error) => res.destroy(error));
21497
+ stream.pipe(res);
21498
+ }
21258
21499
  var import_fs, import_path, import_url, _monorepoRootCache, RUNTIME_VERSION_HOSTNAME_SUFFIX;
21259
21500
  var init_runtime_assets = __esm({
21260
21501
  "src/runtime-assets.ts"() {
@@ -21879,7 +22120,7 @@ function mkfifoSync(p) {
21879
22120
  import_node_fs9.default.unlinkSync(p);
21880
22121
  }
21881
22122
  }
21882
- const result = (0, import_node_child_process3.spawnSync)("mkfifo", ["-m", "600", p]);
22123
+ const result = (0, import_node_child_process4.spawnSync)("mkfifo", ["-m", "600", p]);
21883
22124
  if (result.status !== 0) {
21884
22125
  throw new Error(
21885
22126
  `mkfifo(${p}) failed: ${result.stderr?.toString().trim() || "unknown error"}`
@@ -21947,7 +22188,7 @@ async function startSession(opts) {
21947
22188
  if (claudeSessionUuid) {
21948
22189
  wrapperArgs.push("--claude-session-uuid", claudeSessionUuid);
21949
22190
  }
21950
- const child = (0, import_node_child_process3.spawn)(executable, wrapperArgs, {
22191
+ const child = (0, import_node_child_process4.spawn)(executable, wrapperArgs, {
21951
22192
  detached: true,
21952
22193
  stdio: "ignore",
21953
22194
  env: {
@@ -22099,11 +22340,11 @@ async function waitForFirstEvent(fifo, predicate, timeoutMs) {
22099
22340
  import_node_fs9.default.closeSync(fd);
22100
22341
  }
22101
22342
  }
22102
- var import_node_child_process3, import_node_crypto4, import_node_fs9, import_node_path9, import_node_readline, AgentSessionError;
22343
+ var import_node_child_process4, import_node_crypto4, import_node_fs9, import_node_path9, import_node_readline, AgentSessionError;
22103
22344
  var init_agent_sessions = __esm({
22104
22345
  "src/agent-sessions.ts"() {
22105
22346
  "use strict";
22106
- import_node_child_process3 = require("node:child_process");
22347
+ import_node_child_process4 = require("node:child_process");
22107
22348
  import_node_crypto4 = require("node:crypto");
22108
22349
  import_node_fs9 = __toESM(require("node:fs"), 1);
22109
22350
  import_node_path9 = __toESM(require("node:path"), 1);
@@ -23271,11 +23512,11 @@ function looksBinary(filePath, size) {
23271
23512
  if (fd !== void 0) import_node_fs11.default.closeSync(fd);
23272
23513
  }
23273
23514
  }
23274
- var import_node_child_process4, import_node_fs11, import_node_path11, CheckoutRequiresGitError, BINARY_SNIFF_BYTES, LIST_FILES_TIMEOUT_MS, LIST_FILES_ATTEMPTS, LIST_FILES_RETRY_MS, CheckoutFilePlane;
23515
+ var import_node_child_process5, import_node_fs11, import_node_path11, CheckoutRequiresGitError, BINARY_SNIFF_BYTES, LIST_FILES_TIMEOUT_MS, LIST_FILES_ATTEMPTS, LIST_FILES_RETRY_MS, CheckoutFilePlane;
23275
23516
  var init_checkout_plane = __esm({
23276
23517
  "cli/commands/box/checkout-plane.ts"() {
23277
23518
  "use strict";
23278
- import_node_child_process4 = require("node:child_process");
23519
+ import_node_child_process5 = require("node:child_process");
23279
23520
  import_node_fs11 = __toESM(require("node:fs"), 1);
23280
23521
  import_node_path11 = __toESM(require("node:path"), 1);
23281
23522
  CheckoutRequiresGitError = class extends Error {
@@ -23303,7 +23544,7 @@ var init_checkout_plane = __esm({
23303
23544
  }
23304
23545
  listFiles() {
23305
23546
  const args = this.trackedOnly ? ["ls-files", "-z"] : ["ls-files", "-co", "--exclude-standard", "-z"];
23306
- const stdout = (0, import_node_child_process4.execFileSync)("git", args, {
23547
+ const stdout = (0, import_node_child_process5.execFileSync)("git", args, {
23307
23548
  cwd: this.root,
23308
23549
  encoding: "utf8",
23309
23550
  maxBuffer: 64 * 1024 * 1024,
@@ -24365,8 +24606,8 @@ var init_shared_session = __esm({
24365
24606
  function rethrowIfExit(error) {
24366
24607
  if (error instanceof PeachExit) throw error;
24367
24608
  }
24368
- function peachExit(code) {
24369
- throw new PeachExit(code);
24609
+ function peachExit(code, reason) {
24610
+ throw new PeachExit(code, reason);
24370
24611
  }
24371
24612
  var import_node_async_hooks, PeachExit, sinks, rnxExit;
24372
24613
  var init_run_rnx = __esm({
@@ -24375,10 +24616,12 @@ var init_run_rnx = __esm({
24375
24616
  import_node_async_hooks = require("node:async_hooks");
24376
24617
  PeachExit = class extends Error {
24377
24618
  code;
24378
- constructor(code) {
24619
+ reason;
24620
+ constructor(code, reason = null) {
24379
24621
  super(`peach exited with code ${code}`);
24380
24622
  this.name = "PeachExit";
24381
24623
  this.code = code;
24624
+ this.reason = reason;
24382
24625
  }
24383
24626
  };
24384
24627
  sinks = new import_node_async_hooks.AsyncLocalStorage();
@@ -27031,6 +27274,8 @@ var init_metro_asset_path = __esm({
27031
27274
  function matchesModuleEntry(modulePath, pattern) {
27032
27275
  if (!modulePath || !pattern) return false;
27033
27276
  if (modulePath === pattern) return true;
27277
+ if (!modulePath.includes(pattern)) return false;
27278
+ if (!modulePath.includes(`node_modules/${pattern}`)) return false;
27034
27279
  if (modulePath === `node_modules/${pattern}`) return true;
27035
27280
  if (modulePath === `node_modules/${pattern}.js`) return true;
27036
27281
  if (modulePath === `node_modules/${pattern}.ts`) return true;
@@ -28850,24 +29095,8 @@ var init_bridge_host = __esm({
28850
29095
  // terminal 4001 code, so the client closes the abandoned window instead
28851
29096
  // of reconnecting the zombie straight back in.
28852
29097
  static SIM_IDLE_REAP_TTL_MS = 30 * 6e4;
28853
- // automation sims (spawned by the playwright driver: `peach open --driver
28854
- // playwright`, the factory/screenshot capture path — `meta.sootsimHostDriver
28855
- // === 'playwright'`) are unattended one-shot browsers, never a human's tab. a
28856
- // capture run opens one per slide/app/retry and the 30-min human/dev TTL lets
28857
- // hundreds pile up inside the window — orphaned ms-playwright chromiums that
28858
- // drove the box to swap-thrash (load hit 157 in one incident). give them a far
28859
- // shorter idle TTL, and cap how many can sit concurrently: reap the most-idle
28860
- // overflow, but never a sim active within ACTIVE_GRACE (an agent composing
28861
- // between captures), nor primary / cli-attached / leased sims.
28862
- static AUTOMATION_SIM_IDLE_REAP_TTL_MS = 10 * 6e4;
28863
- static MAX_CONCURRENT_AUTOMATION_SIMS = 6;
28864
- static AUTOMATION_SIM_ACTIVE_GRACE_MS = 6e4;
28865
29098
  preferredPort;
28866
29099
  portFallbackCount;
28867
- simIdleReapTtlMs;
28868
- automationSimIdleReapTtlMs;
28869
- maxConcurrentAutomationSims;
28870
- automationSimActiveGraceMs;
28871
29100
  shouldWriteLockfile;
28872
29101
  shouldWriteDevLockfile;
28873
29102
  getShellPort;
@@ -28910,23 +29139,12 @@ var init_bridge_host = __esm({
28910
29139
  getExcludePorts: opts.agentScanExcludes
28911
29140
  });
28912
29141
  this.contrastOrigin = opts.contrastOrigin?.replace(/\/$/, "") || null;
28913
- this.simIdleReapTtlMs = opts.simIdleReapTtlMs ?? _SootSimBridgeHost.SIM_IDLE_REAP_TTL_MS;
28914
- this.automationSimIdleReapTtlMs = opts.automationSimIdleReapTtlMs ?? _SootSimBridgeHost.AUTOMATION_SIM_IDLE_REAP_TTL_MS;
28915
- this.maxConcurrentAutomationSims = opts.maxConcurrentAutomationSims ?? _SootSimBridgeHost.MAX_CONCURRENT_AUTOMATION_SIMS;
28916
- this.automationSimActiveGraceMs = opts.automationSimActiveGraceMs ?? _SootSimBridgeHost.AUTOMATION_SIM_ACTIVE_GRACE_MS;
28917
29142
  }
28918
29143
  /** expose the agent host so tests and embedders can inspect state or
28919
29144
  * inject behavior. not part of the public WS protocol. */
28920
29145
  getAgentHost() {
28921
29146
  return this.agentHost;
28922
29147
  }
28923
- /** run the abandoned-sim GC pass on demand. the reaper normally fires off
28924
- * the 30s idle-sweep timer; this lets the F21-3 integration test drive
28925
- * the real path without waiting for the timer. not part of the public
28926
- * WS protocol. */
28927
- reapIdleSimsForTest(now = Date.now()) {
28928
- this.reapIdleSims(now);
28929
- }
28930
29148
  /** synchronous wrapper around startAsync for callers that don't care
28931
29149
  * about port fallback outcomes. returns immediately; actual binding
28932
29150
  * happens on the event loop. callers that need to know the bound port
@@ -29172,6 +29390,10 @@ var init_bridge_host = __esm({
29172
29390
  this.updateUserActivity(sim);
29173
29391
  return;
29174
29392
  }
29393
+ if (msg.type === "bridge:capture-active" && sim) {
29394
+ sim.lastActiveAt = Date.now();
29395
+ return;
29396
+ }
29175
29397
  if (msg.type === "bridge:write-shared-config") {
29176
29398
  if (!msg.patch || typeof msg.patch !== "object" || Array.isArray(msg.patch)) {
29177
29399
  return;
@@ -29620,42 +29842,10 @@ var init_bridge_host = __esm({
29620
29842
  return;
29621
29843
  }
29622
29844
  const contentType = HTTP_MIME_TYPES[import_path3.default.extname(filePath).toLowerCase()] || "application/octet-stream";
29623
- const total = stats.size;
29624
- const range = /^bytes=(\d*)-(\d*)$/.exec(req.headers.range || "");
29625
- if (range) {
29626
- const [, startRaw, endRaw] = range;
29627
- const start = startRaw ? Number(startRaw) : 0;
29628
- const end = endRaw ? Math.min(Number(endRaw), total - 1) : total - 1;
29629
- if (!Number.isFinite(start) || start > end || start >= total) {
29630
- res.writeHead(416, { ...cors, "Content-Range": `bytes */${total}` });
29631
- res.end();
29632
- return;
29633
- }
29634
- res.writeHead(206, {
29635
- ...cors,
29636
- "Content-Type": contentType,
29637
- "Content-Range": `bytes ${start}-${end}/${total}`,
29638
- "Accept-Ranges": "bytes",
29639
- "Content-Length": String(end - start + 1)
29640
- });
29641
- if (method === "HEAD") {
29642
- res.end();
29643
- return;
29644
- }
29645
- import_fs3.default.createReadStream(filePath, { start, end }).pipe(res);
29646
- return;
29647
- }
29648
- res.writeHead(200, {
29845
+ sendFileWithRanges(req, res, filePath, stats.size, {
29649
29846
  ...cors,
29650
- "Content-Type": contentType,
29651
- "Accept-Ranges": "bytes",
29652
- "Content-Length": String(total)
29847
+ "Content-Type": contentType
29653
29848
  });
29654
- if (method === "HEAD") {
29655
- res.end();
29656
- return;
29657
- }
29658
- import_fs3.default.createReadStream(filePath).pipe(res);
29659
29849
  }
29660
29850
  handleServerScan(res) {
29661
29851
  const sendJson = (body) => {
@@ -30172,53 +30362,21 @@ var init_bridge_host = __esm({
30172
30362
  // automation sims are spawned by the playwright driver (`peach open
30173
30363
  // --driver playwright`, the factory/screenshot capture path), which tags them
30174
30364
  // with `meta.sootsimHostDriver === 'playwright'` via the host's addInitScript.
30175
- // they are unattended one-shot browsers, never a human's tab, so they get a
30176
- // shorter idle TTL and a concurrent cap.
30177
30365
  isAutomationSim(sim) {
30178
30366
  return sim.meta?.sootsimHostDriver === "playwright";
30179
30367
  }
30180
- // an automation sim whose spawning playwright host process is still alive is
30181
- // an ACTIVE run, not an abandoned tab. mirrors the cli's playwrightHostPidForSim
30182
- // (control.ts): extract the host pid from meta, then check liveness so the
30183
- // host-side idle GC never kills a live capture/factory sim mid-run.
30184
- automationSimOwnerAlive(sim) {
30185
- const pid = Number(sim.meta?.sootsimHostPid);
30186
- if (!Number.isInteger(pid) || pid <= 1 || pid === process.pid) return false;
30187
- try {
30188
- process.kill(pid, 0);
30189
- return true;
30190
- } catch (err) {
30191
- return err.code === "EPERM";
30192
- }
30193
- }
30194
30368
  reapIdleSims(now = Date.now()) {
30195
30369
  const attachedSimIds = new Set(this.cliSimBySocket.values());
30196
- const automationLive = [];
30197
30370
  for (const sim of this.sims.values()) {
30198
- if (sim.id === this.primarySimId) continue;
30199
30371
  if (attachedSimIds.has(sim.id)) continue;
30200
- if (this.getActiveLease(sim)) continue;
30201
- const lastTouched = Math.max(sim.lastActiveAt, sim.connectedAt);
30202
- const idleMs = now - lastTouched;
30203
30372
  const automation = this.isAutomationSim(sim);
30204
- if (automation && this.automationSimOwnerAlive(sim)) continue;
30205
- const ttl = automation ? this.automationSimIdleReapTtlMs : this.simIdleReapTtlMs;
30206
- if (idleMs >= ttl) {
30207
- this.closeSimSocketFromHost(sim.ws);
30373
+ const lease = this.getActiveLease(sim);
30374
+ if (automation ? lease?.kind === "user-active" : sim.id === this.primarySimId || lease) {
30208
30375
  continue;
30209
30376
  }
30210
- if (automation) automationLive.push({ sim, idleMs });
30211
- }
30212
- const cap = this.maxConcurrentAutomationSims;
30213
- if (automationLive.length > cap) {
30214
- automationLive.sort((a, b) => b.idleMs - a.idleMs);
30215
- for (const { sim, idleMs } of automationLive.slice(
30216
- 0,
30217
- automationLive.length - cap
30218
- )) {
30219
- if (idleMs < this.automationSimActiveGraceMs) continue;
30220
- this.closeSimSocketFromHost(sim.ws);
30221
- }
30377
+ const idleMs = now - Math.max(sim.lastActiveAt, sim.connectedAt);
30378
+ const ttl = automation ? AUTOMATION_SIM_IDLE_REAP_TTL_MS : _SootSimBridgeHost.SIM_IDLE_REAP_TTL_MS;
30379
+ if (idleMs >= ttl) this.closeSimSocketFromHost(sim.ws);
30222
30380
  }
30223
30381
  }
30224
30382
  // ping every connected ws; if the previous round's ping went unanswered,
@@ -31210,7 +31368,7 @@ function resolveDefaultDevBridgeLockfile() {
31210
31368
  ) ?? sameCheckout[0];
31211
31369
  }
31212
31370
  }
31213
- return lockfiles.find((lockfile) => lockfile.bridgePort === DEFAULT_SOOTSIM_BRIDGE_PORT) ?? lockfiles[0];
31371
+ return lockfiles.find((lockfile) => lockfile.bridgePort === DEFAULT_SOOTSIM_BRIDGE_PORT) ?? null;
31214
31372
  }
31215
31373
  function resolveBridgeWorld() {
31216
31374
  const homeDir = rnxHomeDir();
@@ -31776,7 +31934,7 @@ var init_app_url = __esm({
31776
31934
  // src/dev-bundle-resolution.ts
31777
31935
  function buildUnresolvedTargetError(targetLabel) {
31778
31936
  return new Error(
31779
- `could not resolve a native bundle for ${targetLabel}. pass an explicit bundle URL or open Connect and choose the app there.`
31937
+ `could not resolve a native bundle for ${targetLabel}. start the app's dev server (for example npx expo start), pass an explicit bundle URL, or open Connect and choose the app there.`
31780
31938
  );
31781
31939
  }
31782
31940
  function inferManifestFramework(launchUrl2, sdkVersion) {
@@ -33791,7 +33949,7 @@ function resolvePlaywright() {
33791
33949
  }
33792
33950
  function probeBrowser(resolved, env) {
33793
33951
  const self = rnxSelfInvocation();
33794
- const probe3 = (0, import_node_child_process5.spawnSync)(
33952
+ const probe3 = (0, import_node_child_process6.spawnSync)(
33795
33953
  self.executable,
33796
33954
  [...self.prefixArgs, RNX_INTERNAL_COMMAND, RNX_INTERNAL_CHILDREN.playwrightProbe],
33797
33955
  {
@@ -33834,7 +33992,7 @@ function ensurePlaywrightBrowser(resolved, env = process.env) {
33834
33992
  return { ok: true, executablePath: before.executablePath, installed: false };
33835
33993
  }
33836
33994
  const installer = rnxSelfInvocation();
33837
- const install = (0, import_node_child_process5.spawnSync)(
33995
+ const install = (0, import_node_child_process6.spawnSync)(
33838
33996
  installer.executable,
33839
33997
  [
33840
33998
  ...installer.prefixArgs,
@@ -33869,11 +34027,11 @@ ${detail}` : ""}`
33869
34027
  }
33870
34028
  return { ok: true, executablePath: after.executablePath, installed: true };
33871
34029
  }
33872
- var import_node_child_process5, import_node_fs20, import_node_module4, import_node_path22;
34030
+ var import_node_child_process6, import_node_fs20, import_node_module4, import_node_path22;
33873
34031
  var init_playwright_provisioning = __esm({
33874
34032
  "cli/drivers/playwright-provisioning.ts"() {
33875
34033
  "use strict";
33876
- import_node_child_process5 = require("node:child_process");
34034
+ import_node_child_process6 = require("node:child_process");
33877
34035
  import_node_fs20 = require("node:fs");
33878
34036
  import_node_module4 = require("node:module");
33879
34037
  import_node_path22 = require("node:path");
@@ -33990,7 +34148,8 @@ ${hint}` : ""}`
33990
34148
  };
33991
34149
  }
33992
34150
  const ownerComm = getProcessComm(ownerPid);
33993
- const ownerNote = ` \u2014 browser closes when pid ${ownerPid}${ownerComm ? ` (${ownerComm})` : ""} exits`;
34151
+ const idleMinutes = AUTOMATION_SIM_IDLE_REAP_TTL_MS / 6e4;
34152
+ const ownerNote = ` \u2014 browser closes after ${idleMinutes} min with no peach command, or when pid ${ownerPid}${ownerComm ? ` (${ownerComm})` : ""} exits`;
33994
34153
  return {
33995
34154
  launched: true,
33996
34155
  message: `playwright chrome launched \u2192 ${opts.url}${ownerNote}`,
@@ -34017,6 +34176,7 @@ var init_playwright = __esm({
34017
34176
  import_fs8 = require("fs");
34018
34177
  import_os2 = require("os");
34019
34178
  import_path7 = require("path");
34179
+ init_bridge_constants();
34020
34180
  init_playwright_host_log();
34021
34181
  init_profiles();
34022
34182
  init_internal_child();
@@ -34550,7 +34710,7 @@ async function ensureRuntimeForOpen() {
34550
34710
  }
34551
34711
  function spawnSootsim(args) {
34552
34712
  const { executable, prefixArgs } = rnxSelfInvocation();
34553
- return (0, import_node_child_process6.spawn)(executable, [...prefixArgs, ...args], {
34713
+ return (0, import_node_child_process7.spawn)(executable, [...prefixArgs, ...args], {
34554
34714
  detached: true,
34555
34715
  stdio: "ignore",
34556
34716
  env: process.env
@@ -34906,12 +35066,14 @@ async function buildShellUrl(target, baseUrl = resolveDefaultShellBaseUrl(), opt
34906
35066
  await stampAppSplash(await stampAppFonts(resolved, project), project),
34907
35067
  project
34908
35068
  );
34909
- if (!opts.device && !opts.proof && !opts.posture && !opts.osVersion) return stamped;
35069
+ if (!opts.device && !opts.proof && !opts.posture && !opts.osVersion && !opts.env?.length)
35070
+ return stamped;
34910
35071
  const url = new URL(stamped);
34911
35072
  if (opts.device) url.searchParams.set("device", opts.device);
34912
35073
  if (opts.posture) url.searchParams.set("posture", opts.posture);
34913
35074
  if (opts.osVersion) url.searchParams.set("osVersion", opts.osVersion);
34914
35075
  if (opts.proof) url.searchParams.set("proof", "1");
35076
+ for (const entry of opts.env ?? []) url.searchParams.append("env", entry);
34915
35077
  return url.toString();
34916
35078
  }
34917
35079
  function deriveCurrentSimBaseUrl(sim, fallback) {
@@ -35089,11 +35251,11 @@ async function waitForProcessExit(pid, timeoutMs = 2500) {
35089
35251
  return !isProcessAlive2(pid);
35090
35252
  }
35091
35253
  function isPlaywrightHostProcess(pid) {
35092
- const plain = (0, import_node_child_process6.spawnSync)("ps", ["-o", "command=", "-p", String(pid)], {
35254
+ const plain = (0, import_node_child_process7.spawnSync)("ps", ["-o", "command=", "-p", String(pid)], {
35093
35255
  encoding: "utf8",
35094
35256
  stdio: ["ignore", "pipe", "pipe"]
35095
35257
  });
35096
- const withEnv = (0, import_node_child_process6.spawnSync)("ps", ["eww", "-o", "command=", "-p", String(pid)], {
35258
+ const withEnv = (0, import_node_child_process7.spawnSync)("ps", ["eww", "-o", "command=", "-p", String(pid)], {
35097
35259
  encoding: "utf8",
35098
35260
  stdio: ["ignore", "pipe", "pipe"]
35099
35261
  });
@@ -35319,7 +35481,8 @@ async function runOpenCommand(args, opts = {}) {
35319
35481
  "--device",
35320
35482
  "--posture",
35321
35483
  "--os-version",
35322
- "--viewport"
35484
+ "--viewport",
35485
+ "--env"
35323
35486
  ]
35324
35487
  });
35325
35488
  const requestedProfileId = args.find((_, i) => args[i - 1] === "--profile");
@@ -35454,8 +35617,7 @@ async function runOpenCommand(args, opts = {}) {
35454
35617
  deviceArg,
35455
35618
  false,
35456
35619
  void 0,
35457
- postureArg === "open" ? "open" : "closed",
35458
- true
35620
+ postureArg === "open" ? "open" : "closed"
35459
35621
  ) : void 0;
35460
35622
  const osVersionArg = args.find((_, i) => args[i - 1] === "--os-version");
35461
35623
  if (osVersionArg && !/^\d+\.\d+$/.test(osVersionArg)) {
@@ -35464,6 +35626,17 @@ async function runOpenCommand(args, opts = {}) {
35464
35626
  );
35465
35627
  rnxExit(1);
35466
35628
  }
35629
+ const envArgs = [];
35630
+ for (let i = 0; i < args.length; i++) {
35631
+ if (args[i] !== "--env") continue;
35632
+ const raw = args[i + 1];
35633
+ const eqIndex = raw ? raw.indexOf("=") : -1;
35634
+ if (!raw || eqIndex <= 0) {
35635
+ console.error(` peach open: --env expects KEY=VALUE, got "${raw ?? ""}"`);
35636
+ rnxExit(1);
35637
+ }
35638
+ envArgs.push(raw);
35639
+ }
35467
35640
  const viewportArg = args.find((_, i) => args[i - 1] === "--viewport");
35468
35641
  const viewportMatch = viewportArg ? /^(\d+)x(\d+)$/.exec(viewportArg) : null;
35469
35642
  if (viewportArg && !viewportMatch) {
@@ -35480,7 +35653,8 @@ async function runOpenCommand(args, opts = {}) {
35480
35653
  osVersion: osVersionArg,
35481
35654
  resolvedBundle,
35482
35655
  project,
35483
- proof: args.includes("--proof")
35656
+ proof: args.includes("--proof"),
35657
+ env: envArgs.length ? envArgs : void 0
35484
35658
  };
35485
35659
  const selectedShellBaseUrl = configuredRuntimeVersion ? `http://localhost:${parsed.wsPort}/` : explicitBaseUrl || resolveShellBaseUrlForBridgePort(parsed.wsPort);
35486
35660
  const baseUrl = applyRuntimeGenerationOrigin(
@@ -35986,11 +36160,11 @@ async function runCloseCommand(args, opts = {}) {
35986
36160
  bridge.close();
35987
36161
  }
35988
36162
  }
35989
- var import_node_child_process6, import_node_fs21, import_node_os3, import_node_path23, import_app_catalog_wire, import_settings, import_ws4, DEFAULT_DRIVER_CONNECT_TIMEOUT_MS, DRIVER_CONNECT_INTERVAL_MS, DRIVER_CONNECT_HOST_GRACE_MS, OPEN_BRIDGE_SPAWN_TIMEOUT_MS, SHELL_APPS, DEV_BUNDLE_RESOLVE_ATTEMPTS, DEV_BUNDLE_RESOLVE_INTERVAL_MS;
36163
+ var import_node_child_process7, import_node_fs21, import_node_os3, import_node_path23, import_app_catalog_wire, import_settings, import_ws4, DEFAULT_DRIVER_CONNECT_TIMEOUT_MS, DRIVER_CONNECT_INTERVAL_MS, DRIVER_CONNECT_HOST_GRACE_MS, OPEN_BRIDGE_SPAWN_TIMEOUT_MS, SHELL_APPS, DEV_BUNDLE_RESOLVE_ATTEMPTS, DEV_BUNDLE_RESOLVE_INTERVAL_MS;
35990
36164
  var init_control = __esm({
35991
36165
  "cli/commands/control.ts"() {
35992
36166
  "use strict";
35993
- import_node_child_process6 = require("node:child_process");
36167
+ import_node_child_process7 = require("node:child_process");
35994
36168
  import_node_fs21 = __toESM(require("node:fs"), 1);
35995
36169
  import_node_os3 = __toESM(require("node:os"), 1);
35996
36170
  import_node_path23 = __toESM(require("node:path"), 1);
@@ -37141,11 +37315,9 @@ function normalizeFlowScreenshotSpec(step) {
37141
37315
  function parsePoint(value, bounds) {
37142
37316
  const [rawX, rawY] = value.split(",").map((part) => part.trim());
37143
37317
  const parseCoord = (coord, max) => coord.endsWith("%") ? Number.parseFloat(coord) / 100 * max : Number.parseFloat(coord);
37144
- const originX = bounds?.x ?? 0;
37145
- const originY = bounds?.y ?? 0;
37146
37318
  return {
37147
- x: originX + parseCoord(rawX, bounds?.width ?? SCREEN_W),
37148
- y: originY + parseCoord(rawY, bounds?.height ?? SCREEN_H)
37319
+ x: bounds.x + parseCoord(rawX, bounds.width),
37320
+ y: bounds.y + parseCoord(rawY, bounds.height)
37149
37321
  };
37150
37322
  }
37151
37323
  function normalizeBareStringStep(verb) {
@@ -37181,7 +37353,7 @@ function parseFlowSteps(content) {
37181
37353
  const parsed = import_yaml.default.parse(candidate);
37182
37354
  return Array.isArray(parsed) ? parsed : [];
37183
37355
  }
37184
- var fs19, path19, import_settings3, import_yaml, import_proof_frame, DEFAULT_TIMEOUT, ASSERTION_LOOKUP_MS, FLOW_TIMEOUT_SCALE, scaledTimeout, SCREEN_W, SCREEN_H, MAESTRO_SELECTOR_KEYS, SELECTOR_FILTER_KEYS, UNSUPPORTED_SELECTOR_KEYS, MAESTRO_PERMISSION_KINDS, MAESTRO_PERMISSION_STATUS, SootSimBridgeFlowRunner;
37356
+ var fs19, path19, import_settings3, import_yaml, import_proof_frame, DEFAULT_TIMEOUT, ASSERTION_LOOKUP_MS, FLOW_TIMEOUT_SCALE, scaledTimeout, MAESTRO_SELECTOR_KEYS, SELECTOR_FILTER_KEYS, UNSUPPORTED_SELECTOR_KEYS, MAESTRO_PERMISSION_KINDS, MAESTRO_PERMISSION_STATUS, SootSimBridgeFlowRunner;
37185
37357
  var init_bridge_flow_runner = __esm({
37186
37358
  "cli/bridge-flow-runner.ts"() {
37187
37359
  "use strict";
@@ -37204,8 +37376,6 @@ var init_bridge_flow_runner = __esm({
37204
37376
  ASSERTION_LOOKUP_MS = 17e3;
37205
37377
  FLOW_TIMEOUT_SCALE = flowTimeoutScale();
37206
37378
  scaledTimeout = (ms) => Math.round(ms * FLOW_TIMEOUT_SCALE);
37207
- SCREEN_W = 393;
37208
- SCREEN_H = 852;
37209
37379
  MAESTRO_SELECTOR_KEYS = /* @__PURE__ */ new Set([
37210
37380
  "text",
37211
37381
  "id",
@@ -37330,6 +37500,16 @@ var init_bridge_flow_runner = __esm({
37330
37500
  args
37331
37501
  });
37332
37502
  }
37503
+ async screenBounds() {
37504
+ const snapshot = await this.callTest(
37505
+ "getSurfaceMetricsSnapshot"
37506
+ );
37507
+ const window2 = snapshot?.window;
37508
+ if (!window2 || typeof window2 !== "object" || !("width" in window2) || !("height" in window2) || typeof window2.width !== "number" || typeof window2.height !== "number" || !Number.isFinite(window2.width) || !Number.isFinite(window2.height) || window2.width <= 0 || window2.height <= 0) {
37509
+ throw new Error("live screen dimensions are unavailable");
37510
+ }
37511
+ return { x: 0, y: 0, width: window2.width, height: window2.height };
37512
+ }
37333
37513
  // ordered input batch executed inside the page in one round-trip; the
37334
37514
  // engine emits agent-cursor actions for every step natively.
37335
37515
  async perform(steps) {
@@ -37566,6 +37746,7 @@ var init_bridge_flow_runner = __esm({
37566
37746
  // (match scoped to a container). a nested selector resolves the same way,
37567
37747
  // and a relative filter holds when ANY match of its anchor satisfies it.
37568
37748
  async findElementScoped(opts) {
37749
+ const screen = await this.screenBounds();
37569
37750
  const needsEveryNode = !opts.id || opts.childOf != null || SELECTOR_FILTER_KEYS.some((key) => opts[key] != null);
37570
37751
  return this.evaluate(`(async () => {
37571
37752
  const test = window.__sootsimTest
@@ -37669,7 +37850,7 @@ var init_bridge_flow_runner = __esm({
37669
37850
  // paints nothing, and maestro's hierarchy never holds it.
37670
37851
  const onScreen = candidates.filter((n) => {
37671
37852
  const cy = n.absolutePosition.y + n.layout.height / 2
37672
- return cy > 0 && cy < ${SCREEN_H}
37853
+ return cy > 0 && cy < ${screen.height}
37673
37854
  })
37674
37855
  if (candidates.some(painted)) candidates = candidates.filter(painted)
37675
37856
  else if (onScreen.length > 0) candidates = onScreen
@@ -37834,7 +38015,7 @@ var init_bridge_flow_runner = __esm({
37834
38015
  async resolveInteractionPoint(opts) {
37835
38016
  if (opts.point && !opts.id && !opts.text && opts.index === void 0 && !opts.childOf) {
37836
38017
  return {
37837
- ...parsePoint(opts.point),
38018
+ ...parsePoint(opts.point, await this.screenBounds()),
37838
38019
  element: null,
37839
38020
  target: void 0
37840
38021
  };
@@ -38295,32 +38476,14 @@ var init_bridge_flow_runner = __esm({
38295
38476
  }
38296
38477
  return outputDir;
38297
38478
  }
38298
- async swipe(direction = "UP", duration = 300) {
38299
- const centerX = SCREEN_W / 2;
38300
- const centerY = SCREEN_H / 2;
38301
- const distance = 200;
38302
- let fromX = centerX;
38303
- let fromY = centerY;
38304
- let toX = centerX;
38305
- let toY = centerY;
38306
- switch (direction.toUpperCase()) {
38307
- case "UP":
38308
- fromY += distance;
38309
- toY -= distance;
38310
- break;
38311
- case "DOWN":
38312
- fromY -= distance;
38313
- toY += distance;
38314
- break;
38315
- case "LEFT":
38316
- fromX += distance;
38317
- toX -= distance;
38318
- break;
38319
- case "RIGHT":
38320
- fromX -= distance;
38321
- toX += distance;
38322
- break;
38323
- }
38479
+ // maestro's ios driver: a directional swipe spans the screen, from 90% to
38480
+ // 10% for UP, 20% to 90% for DOWN, and 10% to 90% across for LEFT/RIGHT,
38481
+ // over the command's 400ms default.
38482
+ async swipe(direction = "UP", duration = 400) {
38483
+ const screen = await this.screenBounds();
38484
+ const midX = screen.width / 2;
38485
+ const midY = screen.height / 2;
38486
+ const [fromX, fromY, toX, toY] = direction.toUpperCase() === "DOWN" ? [midX, screen.height * 0.2, midX, screen.height * 0.9] : direction.toUpperCase() === "LEFT" ? [screen.width * 0.9, midY, screen.width * 0.1, midY] : direction.toUpperCase() === "RIGHT" ? [screen.width * 0.1, midY, screen.width * 0.9, midY] : [midX, screen.height * 0.9, midX, screen.height * 0.1];
38324
38487
  const steps = Math.max(10, Math.round(duration / 16));
38325
38488
  await this.drag(fromX, fromY, toX, toY, steps, 16);
38326
38489
  }
@@ -38341,34 +38504,22 @@ var init_bridge_flow_runner = __esm({
38341
38504
  }
38342
38505
  await this.settleAfterInput();
38343
38506
  }
38344
- async swipeCoords(start, end, duration = 300) {
38345
- const from = parsePoint(start);
38346
- const to = parsePoint(end);
38507
+ async swipeCoords(start, end, duration = 400) {
38508
+ const screen = await this.screenBounds();
38509
+ const from = parsePoint(start, screen);
38510
+ const to = parsePoint(end, screen);
38347
38511
  const steps = Math.max(10, Math.round(duration / 16));
38348
38512
  await this.drag(from.x, from.y, to.x, to.y, steps, 16);
38349
38513
  }
38350
- async swipeFrom(from, direction = "UP", duration = 300) {
38514
+ // maestro's ios driver swipes from the element's center to the 10% or 90%
38515
+ // edge in the swipe's direction.
38516
+ async swipeFrom(from, direction = "UP", duration = 400) {
38351
38517
  const element = await this.findElement(from);
38352
38518
  if (!element) throw new Error(`swipeFrom: element not found: ${JSON.stringify(from)}`);
38353
38519
  const startX = element.absolutePosition.x + element.layout.width / 2;
38354
38520
  const startY = element.absolutePosition.y + element.layout.height / 2;
38355
- const distance = 180;
38356
- let endX = startX;
38357
- let endY = startY;
38358
- switch (direction.toUpperCase()) {
38359
- case "UP":
38360
- endY -= distance;
38361
- break;
38362
- case "DOWN":
38363
- endY += distance;
38364
- break;
38365
- case "LEFT":
38366
- endX -= distance;
38367
- break;
38368
- case "RIGHT":
38369
- endX += distance;
38370
- break;
38371
- }
38521
+ const screen = await this.screenBounds();
38522
+ const [endX, endY] = direction.toUpperCase() === "DOWN" ? [startX, screen.height * 0.9] : direction.toUpperCase() === "LEFT" ? [screen.width * 0.1, startY] : direction.toUpperCase() === "RIGHT" ? [screen.width * 0.9, startY] : [startX, screen.height * 0.1];
38372
38523
  const steps = Math.max(10, Math.round(duration / 16));
38373
38524
  await this.drag(startX, startY, endX, endY, steps, 16);
38374
38525
  }
@@ -38453,16 +38604,17 @@ var init_bridge_flow_runner = __esm({
38453
38604
  );
38454
38605
  const deadline = Date.now() + (opts.timeout || 2e4);
38455
38606
  const direction = opts.direction?.toUpperCase() || "DOWN";
38456
- const centerX = SCREEN_W / 2;
38457
- const centerY = SCREEN_H / 2;
38458
- const swipeEnd = direction === "UP" ? { x: centerX, y: SCREEN_H * 0.9 } : direction === "LEFT" ? { x: SCREEN_W * 0.9, y: centerY } : direction === "RIGHT" ? { x: SCREEN_W * 0.1, y: centerY } : { x: centerX, y: SCREEN_H * 0.1 };
38459
38607
  while (Date.now() < deadline) {
38608
+ const screen = await this.screenBounds();
38609
+ const centerX = screen.width / 2;
38610
+ const centerY = screen.height / 2;
38611
+ const swipeEnd = direction === "UP" ? { x: centerX, y: screen.height * 0.9 } : direction === "LEFT" ? { x: screen.width * 0.9, y: centerY } : direction === "RIGHT" ? { x: screen.width * 0.1, y: centerY } : { x: centerX, y: screen.height * 0.1 };
38460
38612
  const element = await this.findElement(target);
38461
38613
  if (element) {
38462
38614
  const { x, y } = element.absolutePosition;
38463
38615
  const { width, height } = element.layout;
38464
- const shownX = Math.max(0, Math.min(x + width, SCREEN_W) - Math.max(x, 0));
38465
- const shownY = Math.max(0, Math.min(y + height, SCREEN_H) - Math.max(y, 0));
38616
+ const shownX = Math.max(0, Math.min(x + width, screen.width) - Math.max(x, 0));
38617
+ const shownY = Math.max(0, Math.min(y + height, screen.height) - Math.max(y, 0));
38466
38618
  const share = width * height > 0 ? shownX * shownY / (width * height) : 0;
38467
38619
  if (share >= required) {
38468
38620
  if (opts.centerElement) {
@@ -39046,7 +39198,10 @@ var init_bridge_flow_runner = __esm({
39046
39198
  await this.swipe(step.swipe.direction, step.swipe.duration);
39047
39199
  }
39048
39200
  } else if (step.scroll) {
39049
- await this.swipe(step.scroll.direction === "DOWN" ? "UP" : "DOWN");
39201
+ const screen = await this.screenBounds();
39202
+ const midX = screen.width / 2;
39203
+ const toY = step.scroll.direction === "UP" ? screen.height * 0.9 : screen.height * 0.1;
39204
+ await this.drag(midX, screen.height / 2, midX, toY, Math.round(333 / 16), 16);
39050
39205
  } else if (step.scrollTo) {
39051
39206
  const target = typeof step.scrollTo.nodeId === "number" ? { nodeId: step.scrollTo.nodeId } : step.scrollTo.id;
39052
39207
  if (!target) throw new Error("scrollTo requires id or nodeId");