peachsim 0.1.939 → 0.1.1399

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 (396) hide show
  1. package/README.md +28 -22
  2. package/cli/app-catalog.ts +29 -14
  3. package/cli/app-fonts.ts +5 -3
  4. package/cli/bridge-flow-runner.ts +40 -29
  5. package/cli/cloud-client.ts +57 -21
  6. package/cli/cloud-test-tunnel.ts +12 -18
  7. package/cli/commands/agent.ts +1 -1
  8. package/cli/commands/app-fonts.ts +3 -3
  9. package/cli/commands/assert.ts +8 -8
  10. package/cli/commands/auth.ts +8 -3
  11. package/cli/commands/box/bundle-plane.ts +13 -1
  12. package/cli/commands/box.ts +16 -5
  13. package/cli/commands/compat.ts +1 -1
  14. package/cli/commands/config.ts +2 -2
  15. package/cli/commands/control.ts +125 -71
  16. package/cli/commands/cpu-profile.ts +45 -1
  17. package/cli/commands/daemon.ts +131 -33
  18. package/cli/commands/debug.ts +24 -17
  19. package/cli/commands/desktop.ts +7 -1
  20. package/cli/commands/detect.ts +101 -0
  21. package/cli/commands/detox.ts +6 -6
  22. package/cli/commands/device.ts +12 -12
  23. package/cli/commands/electron.ts +32 -14
  24. package/cli/commands/flow.ts +49 -41
  25. package/cli/commands/guest-host.ts +39 -1
  26. package/cli/commands/inspect/core.ts +14 -8
  27. package/cli/commands/inspect/list.ts +1 -1
  28. package/cli/commands/inspect/settle.ts +5 -3
  29. package/cli/commands/inspect/settling.ts +229 -44
  30. package/cli/commands/inspect/wait-idle.ts +3 -2
  31. package/cli/commands/inspect.ts +105 -93
  32. package/cli/commands/install-cli.ts +36 -15
  33. package/cli/commands/install-desktop.ts +68 -195
  34. package/cli/commands/kotlin.ts +6 -5
  35. package/cli/commands/login.ts +5 -5
  36. package/cli/commands/maestro-compare.ts +554 -0
  37. package/cli/commands/maestro-generate.ts +6 -3
  38. package/cli/commands/maestro.ts +22 -0
  39. package/cli/commands/mode.ts +2 -2
  40. package/cli/commands/no-bridge-hint.ts +6 -2
  41. package/cli/commands/platform.ts +35 -6
  42. package/cli/commands/preview.ts +6 -0
  43. package/cli/commands/project-report.ts +10 -12
  44. package/cli/commands/record.ts +37 -37
  45. package/cli/commands/runtime.ts +9 -9
  46. package/cli/commands/screenshot-layers.ts +4 -4
  47. package/cli/commands/screenshot-mode.ts +2 -2
  48. package/cli/commands/screenshot.ts +8 -8
  49. package/cli/commands/screenshots-capture.ts +2 -2
  50. package/cli/commands/serve.ts +9 -12
  51. package/cli/commands/setup.ts +29 -14
  52. package/cli/commands/shell-boolean-mode.ts +2 -2
  53. package/cli/commands/show-touches-mode.ts +2 -2
  54. package/cli/commands/skills.ts +11 -11
  55. package/cli/commands/swift.ts +29 -11
  56. package/cli/commands/telemetry.ts +2 -2
  57. package/cli/commands/test.ts +5 -0
  58. package/cli/commands/three-mode.ts +3 -3
  59. package/cli/commands/timeline.ts +4 -4
  60. package/cli/commands/upgrade.ts +2 -2
  61. package/cli/commands/upload.ts +15 -15
  62. package/cli/commands/what-happened.ts +2 -2
  63. package/cli/current-sim.ts +6 -3
  64. package/cli/desktop-companion.ts +55 -149
  65. package/cli/dev-build-freshness.ts +2 -2
  66. package/cli/drivers/desktop.ts +17 -9
  67. package/cli/drivers/electron.ts +1 -2
  68. package/cli/drivers/playwright-sim-host.ts +11 -2
  69. package/cli/drivers/playwright.ts +35 -2
  70. package/cli/drivers/types.ts +8 -0
  71. package/cli/maestro-compare.ts +670 -0
  72. package/cli/outbound-endpoints.ts +46 -8
  73. package/cli/recording-access.ts +6 -6
  74. package/cli/run-registry.ts +1 -1
  75. package/cli/runtime-compat.ts +5 -1
  76. package/cli/send-to-box.ts +3 -3
  77. package/cli/shell-init.ts +2 -1
  78. package/cli/ws-bridge.ts +9 -5
  79. package/detox/element-types.ts +1 -0
  80. package/detox/gestures.ts +3 -3
  81. package/detox/index.ts +67 -38
  82. package/detox/navigation-lifecycle.ts +9 -6
  83. package/detox/proof-frame.cjs +6 -4
  84. package/dist-lib/agent-daemon-client.cjs +1 -1
  85. package/dist-lib/agent-events.cjs +1 -1
  86. package/dist-lib/attached-projects.cjs +1 -1
  87. package/dist-lib/auth/shared-session.cjs +1 -1
  88. package/dist-lib/backend-origin.cjs +1 -1
  89. package/dist-lib/beta.cjs +1 -1
  90. package/dist-lib/beta.mjs +1 -1
  91. package/dist-lib/bridge-constants.cjs +1 -1
  92. package/dist-lib/bridge-contract-input.cjs +42 -17
  93. package/dist-lib/bridge-contract-input.mjs +41 -16
  94. package/dist-lib/bridge-contract.cjs +1 -4
  95. package/dist-lib/bridge-contract.mjs +1 -3
  96. package/dist-lib/bundle-channel.cjs +2918 -550
  97. package/dist-lib/capture-contract.cjs +65 -24
  98. package/dist-lib/capture-contract.mjs +63 -24
  99. package/dist-lib/cloud-contract.cjs +2 -2
  100. package/dist-lib/cloud-contract.mjs +2 -2
  101. package/dist-lib/cloud-test-args.cjs +1 -1
  102. package/dist-lib/cloud-test-args.mjs +1 -1
  103. package/dist-lib/cloud.cjs +3 -3
  104. package/dist-lib/cloud.mjs +3 -3
  105. package/dist-lib/config.cjs +15 -5
  106. package/dist-lib/config.mjs +154 -0
  107. package/dist-lib/detox/index.cjs +495 -139
  108. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  109. package/dist-lib/expo-swift-bundle.cjs +1168 -238
  110. package/dist-lib/expo-swift-metadata.cjs +34 -23
  111. package/dist-lib/expo-swift-metadata.mjs +32 -23
  112. package/dist-lib/home-paths.cjs +2 -2
  113. package/dist-lib/host/bridge-host.cjs +2938 -654
  114. package/dist-lib/host/fetch-proxy-handler.cjs +14 -1
  115. package/dist-lib/host/fetch-proxy-overrides.cjs +7 -1
  116. package/dist-lib/host/fetch-proxy-overrides.mjs +5 -1
  117. package/dist-lib/host/replacement-module-handler.cjs +2 -2
  118. package/dist-lib/host/websocket-proxy.cjs +1 -1
  119. package/dist-lib/index.cjs +999 -353
  120. package/dist-lib/jump-to-source-babel.cjs +1 -1
  121. package/dist-lib/jump-to-source-native.cjs +1 -1
  122. package/dist-lib/kotlin-host.cjs +332 -19
  123. package/dist-lib/menu.cjs +1 -1
  124. package/dist-lib/menu.mjs +1 -1
  125. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  126. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  127. package/dist-lib/metro-production-bundle.cjs +7 -7
  128. package/dist-lib/metro-production-bundle.mjs +5 -5
  129. package/dist-lib/metro.cjs +688 -250
  130. package/dist-lib/native-app-identity-project.cjs +2647 -0
  131. package/dist-lib/native-app-identity-project.mjs +1257 -0
  132. package/dist-lib/native-app-identity.cjs +325 -25
  133. package/dist-lib/native-app-identity.mjs +323 -25
  134. package/dist-lib/native-firebase-metadata.cjs +1 -1
  135. package/dist-lib/native-firebase-metadata.mjs +1 -1
  136. package/dist-lib/plist.cjs +1 -1
  137. package/dist-lib/plist.mjs +1 -1
  138. package/dist-lib/profiles.cjs +1 -1
  139. package/dist-lib/public-brand.cjs +1 -1
  140. package/dist-lib/react-native-host-modules.cjs +5 -1
  141. package/dist-lib/react-native-host-modules.mjs +5 -1
  142. package/dist-lib/scripts/dev-server-scanner.cjs +57 -32
  143. package/dist-lib/sdk.cjs +231 -51
  144. package/dist-lib/sdk.mjs +231 -51
  145. package/dist-lib/skills.cjs +4107 -1374
  146. package/dist-lib/swift-compile.cjs +290 -30
  147. package/dist-lib/swift-compile.mjs +290 -30
  148. package/dist-lib/swift-package.cjs +143 -12
  149. package/dist-lib/swift-package.mjs +137 -11
  150. package/dist-lib/swift-submit.cjs +47 -5
  151. package/dist-lib/swift-submit.mjs +42 -5
  152. package/dist-lib/swift-toolchain.cjs +7 -2
  153. package/dist-lib/swift.cjs +1 -1
  154. package/dist-lib/vite-swift.cjs +1392 -458
  155. package/dist-lib/vite.cjs +13 -17
  156. package/dist-lib/worklet-executable.cjs +248 -33
  157. package/dist-lib/worklet-executable.mjs +248 -33
  158. package/install.sh +10 -1
  159. package/kotlin/{AppSources → AndroidViewSources}/android/content/res/Resources.kt +9 -9
  160. package/kotlin/AndroidViewSources/android/graphics/Canvas.kt +20 -0
  161. package/kotlin/AndroidViewSources/android/view/View.kt +211 -0
  162. package/kotlin/AndroidViewSources/android/widget/FrameLayout.kt +61 -0
  163. package/kotlin/AndroidViewSources/android/widget/LinearLayout.kt +49 -0
  164. package/kotlin/AppSources/android/view/Window.kt +36 -0
  165. package/kotlin/AppSources/android/view/WindowInsets.kt +9 -0
  166. package/kotlin/AppSources/android/view/WindowInsetsController.kt +5 -0
  167. package/kotlin/AppSources/android/window/OnBackInvokedDispatcher.kt +48 -0
  168. package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +20 -5
  169. package/kotlin/AppSources/androidx/compose/foundation/layout/WindowInsets.kt +15 -0
  170. package/kotlin/AppSources/androidx/compose/ui/platform/ComposeView.kt +2 -1
  171. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +42 -50
  172. package/kotlin/ComposeSources/PeachComposeHost.kt +200 -84
  173. package/kotlin/ExpoComposeSources/PeachAndroidView.kt +87 -0
  174. package/kotlin/ExpoComposeSources/PeachExpoViewMount.kt +24 -0
  175. package/kotlin/ExpoComposeSources/dev/peach/expo/PeachExpoCompose.kt +28 -0
  176. package/kotlin/ExpoComposeSources/expo/modules/kotlin/views/ExpoComposeView.kt +70 -0
  177. package/kotlin/ExpoSources/dev/peach/expo/PeachExpoRuntime.kt +513 -0
  178. package/kotlin/ExpoSources/dev/peach/expo/PeachExpoTypes.kt +173 -0
  179. package/kotlin/ExpoSources/expo/modules/kotlin/AppContext.kt +20 -0
  180. package/kotlin/ExpoSources/expo/modules/kotlin/Promise.kt +19 -0
  181. package/kotlin/ExpoSources/expo/modules/kotlin/classcomponent/ClassComponentBuilder.kt +198 -0
  182. package/kotlin/ExpoSources/expo/modules/kotlin/exception/CodedException.kt +126 -0
  183. package/kotlin/ExpoSources/expo/modules/kotlin/functions/Functions.kt +132 -0
  184. package/kotlin/ExpoSources/expo/modules/kotlin/jni/JavaScriptTypedArray.kt +81 -0
  185. package/kotlin/ExpoSources/expo/modules/kotlin/modules/Module.kt +98 -0
  186. package/kotlin/ExpoSources/expo/modules/kotlin/objects/ObjectDefinitionBuilder.kt +236 -0
  187. package/kotlin/ExpoSources/expo/modules/kotlin/records/Record.kt +12 -0
  188. package/kotlin/ExpoSources/expo/modules/kotlin/runtime/Runtime.kt +9 -0
  189. package/kotlin/ExpoSources/expo/modules/kotlin/sharedobjects/SharedObject.kt +50 -0
  190. package/kotlin/ExpoSources/expo/modules/kotlin/sharedobjects/SharedObjectRegistry.kt +57 -0
  191. package/kotlin/ExpoSources/expo/modules/kotlin/sharedobjects/SharedRef.kt +19 -0
  192. package/kotlin/ExpoSources/expo/modules/kotlin/typedarray/ConcreteTypedArrays.kt +158 -0
  193. package/kotlin/ExpoSources/expo/modules/kotlin/typedarray/GenericTypedArray.kt +8 -0
  194. package/kotlin/ExpoSources/expo/modules/kotlin/typedarray/TypedArray.kt +29 -0
  195. package/kotlin/ExpoSources/expo/modules/kotlin/typedarray/TypedArrayIterator.kt +9 -0
  196. package/kotlin/ExpoSources/expo/modules/kotlin/types/AnyType.kt +155 -0
  197. package/kotlin/ExpoSources/expo/modules/kotlin/types/Either.kt +91 -0
  198. package/kotlin/ExpoSources/expo/modules/kotlin/types/Enumerable.kt +6 -0
  199. package/kotlin/ExpoSources/expo/modules/kotlin/viewevent/ViewEvent.kt +54 -0
  200. package/kotlin/ExpoSources/expo/modules/kotlin/views/ExpoView.kt +23 -0
  201. package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewDefinitionBuilder.kt +71 -0
  202. package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewProps.kt +35 -0
  203. package/kotlin/README.md +13 -3
  204. package/kotlin/Sources/android/os/Build.kt +17 -0
  205. package/kotlin/Sources/android/os/Handler.kt +35 -0
  206. package/kotlin/Sources/android/os/Looper.kt +17 -0
  207. package/kotlin/Sources/android/os/MessageQueue.kt +56 -0
  208. package/kotlin/Sources/android/os/SystemClock.kt +14 -0
  209. package/kotlin/Sources/android/util/Log.kt +64 -0
  210. package/kotlin/Sources/java/security/MessageDigest.kt +114 -0
  211. package/kotlin/Sources/kotlin/text/StringFormat.kt +9 -0
  212. package/kotlin/Sources/org/json/JSON.kt +44 -0
  213. package/kotlin/fixtures/counter/Counter.d.kt.ts +1 -0
  214. package/kotlin/fixtures/counter/Counter.kt +10 -3
  215. package/kotlin/fixtures/counter/CounterApp.native.tsx +1 -1
  216. package/kotlin/fixtures/counter/vite.config.ts +2 -2
  217. package/kotlin/fixtures/headless-counter/Headless.kt +7 -6
  218. package/kotlin/fixtures/import-animation/Animation.d.kt.ts +7 -0
  219. package/kotlin/fixtures/import-animation/Animation.kt +80 -0
  220. package/kotlin/fixtures/import-animation/ImportAnimation.native.tsx +13 -0
  221. package/kotlin/fixtures/import-animation/README.md +24 -0
  222. package/kotlin/fixtures/import-animation/app/_layout.tsx +4 -0
  223. package/kotlin/fixtures/import-animation/app/index+spa.tsx +1 -0
  224. package/kotlin/fixtures/import-animation/app/routes.d.ts +18 -0
  225. package/kotlin/fixtures/import-animation/package.json +23 -0
  226. package/kotlin/fixtures/import-animation/tsconfig.json +20 -0
  227. package/kotlin/fixtures/import-animation/vite.config.ts +20 -0
  228. package/kotlin/fixtures/import-editor/Editor.d.kt.ts +7 -0
  229. package/kotlin/fixtures/import-editor/Editor.kt +59 -0
  230. package/kotlin/fixtures/import-editor/ImportEditor.native.tsx +23 -0
  231. package/kotlin/fixtures/import-editor/README.md +27 -0
  232. package/kotlin/fixtures/import-editor/app/_layout.tsx +4 -0
  233. package/kotlin/fixtures/import-editor/app/index+spa.tsx +1 -0
  234. package/kotlin/fixtures/import-editor/package.json +23 -0
  235. package/kotlin/fixtures/import-editor/tsconfig.json +20 -0
  236. package/kotlin/fixtures/import-editor/vite.config.ts +20 -0
  237. package/kotlin/fixtures/import-effects/Effects.d.kt.ts +7 -0
  238. package/kotlin/fixtures/import-effects/Effects.kt +87 -0
  239. package/kotlin/fixtures/import-effects/ImportEffects.native.tsx +13 -0
  240. package/kotlin/fixtures/import-effects/README.md +19 -0
  241. package/kotlin/fixtures/import-effects/app/_layout.tsx +4 -0
  242. package/kotlin/fixtures/import-effects/app/index+spa.tsx +1 -0
  243. package/kotlin/fixtures/import-effects/package.json +23 -0
  244. package/kotlin/fixtures/import-effects/tsconfig.json +20 -0
  245. package/kotlin/fixtures/import-effects/vite.config.ts +20 -0
  246. package/kotlin/fixtures/import-layout/ImportLayout.native.tsx +13 -0
  247. package/kotlin/fixtures/import-layout/Layout.d.kt.ts +7 -0
  248. package/kotlin/fixtures/import-layout/Layout.kt +72 -0
  249. package/kotlin/fixtures/import-layout/README.md +24 -0
  250. package/kotlin/fixtures/import-layout/app/_layout.tsx +4 -0
  251. package/kotlin/fixtures/import-layout/app/index+spa.tsx +1 -0
  252. package/kotlin/fixtures/import-layout/package.json +23 -0
  253. package/kotlin/fixtures/import-layout/tsconfig.json +20 -0
  254. package/kotlin/fixtures/import-layout/vite.config.ts +20 -0
  255. package/kotlin/fixtures/import-navigation/ImportNavigation.native.tsx +18 -0
  256. package/kotlin/fixtures/import-navigation/Navigation.d.kt.ts +7 -0
  257. package/kotlin/fixtures/import-navigation/Navigation.kt +104 -0
  258. package/kotlin/fixtures/import-navigation/README.md +31 -0
  259. package/kotlin/fixtures/import-navigation/app/_layout.tsx +4 -0
  260. package/kotlin/fixtures/import-navigation/app/index+spa.tsx +1 -0
  261. package/kotlin/fixtures/import-navigation/package.json +23 -0
  262. package/kotlin/fixtures/import-navigation/tsconfig.json +20 -0
  263. package/kotlin/fixtures/import-navigation/vite.config.ts +20 -0
  264. package/kotlin/fixtures/import-overlays/ImportOverlays.native.tsx +29 -0
  265. package/kotlin/fixtures/import-overlays/Overlays.d.kt.ts +7 -0
  266. package/kotlin/fixtures/import-overlays/Overlays.kt +93 -0
  267. package/kotlin/fixtures/import-overlays/README.md +34 -0
  268. package/kotlin/fixtures/import-overlays/app/_layout.tsx +4 -0
  269. package/kotlin/fixtures/import-overlays/app/index+spa.tsx +1 -0
  270. package/kotlin/fixtures/import-overlays/package.json +23 -0
  271. package/kotlin/fixtures/import-overlays/tsconfig.json +20 -0
  272. package/kotlin/fixtures/import-overlays/vite.config.ts +20 -0
  273. package/kotlin/fixtures/import-scroll/ImportScroll.native.tsx +13 -0
  274. package/kotlin/fixtures/import-scroll/README.md +23 -0
  275. package/kotlin/fixtures/import-scroll/Scroll.d.kt.ts +7 -0
  276. package/kotlin/fixtures/import-scroll/Scroll.kt +91 -0
  277. package/kotlin/fixtures/import-scroll/app/_layout.tsx +4 -0
  278. package/kotlin/fixtures/import-scroll/app/index+spa.tsx +1 -0
  279. package/kotlin/fixtures/import-scroll/package.json +23 -0
  280. package/kotlin/fixtures/import-scroll/tsconfig.json +20 -0
  281. package/kotlin/fixtures/import-scroll/vite.config.ts +20 -0
  282. package/kotlin/fixtures/import-selection/ImportSelection.native.tsx +44 -0
  283. package/kotlin/fixtures/import-selection/README.md +32 -0
  284. package/kotlin/fixtures/import-selection/Selection.d.kt.ts +8 -0
  285. package/kotlin/fixtures/import-selection/Selection.kt +78 -0
  286. package/kotlin/fixtures/import-selection/app/_layout.tsx +4 -0
  287. package/kotlin/fixtures/import-selection/app/index+spa.tsx +1 -0
  288. package/kotlin/fixtures/import-selection/package.json +23 -0
  289. package/kotlin/fixtures/import-selection/tsconfig.json +20 -0
  290. package/kotlin/fixtures/import-selection/vite.config.ts +20 -0
  291. package/kotlin/fixtures/import-surfaces/ImportSurfaces.native.tsx +13 -0
  292. package/kotlin/fixtures/import-surfaces/README.md +22 -0
  293. package/kotlin/fixtures/import-surfaces/Surfaces.d.kt.ts +7 -0
  294. package/kotlin/fixtures/import-surfaces/Surfaces.kt +64 -0
  295. package/kotlin/fixtures/import-surfaces/app/_layout.tsx +4 -0
  296. package/kotlin/fixtures/import-surfaces/app/index+spa.tsx +1 -0
  297. package/kotlin/fixtures/import-surfaces/package.json +23 -0
  298. package/kotlin/fixtures/import-surfaces/tsconfig.json +20 -0
  299. package/kotlin/fixtures/import-surfaces/vite.config.ts +20 -0
  300. package/kotlin/fixtures/import-values/ImportValues.native.tsx +35 -0
  301. package/kotlin/fixtures/import-values/README.md +23 -0
  302. package/kotlin/fixtures/import-values/Values.d.kt.ts +11 -0
  303. package/kotlin/fixtures/import-values/Values.kt +66 -0
  304. package/kotlin/fixtures/import-values/app/_layout.tsx +4 -0
  305. package/kotlin/fixtures/import-values/app/index+spa.tsx +1 -0
  306. package/kotlin/fixtures/import-values/package.json +23 -0
  307. package/kotlin/fixtures/import-values/tsconfig.json +20 -0
  308. package/kotlin/fixtures/import-values/vite.config.ts +20 -0
  309. package/kotlin/fixtures/kotlin-app-variants/BarBehavior.kt +38 -0
  310. package/kotlin/fixtures/kotlin-app-variants/BarVisibility.kt +36 -0
  311. package/kotlin/fixtures/kotlin-app-variants/Ime.kt +3 -1
  312. package/kotlin/fixtures/kotlin-app-variants/TransientInsets.kt +50 -0
  313. package/kotlin/fork/NOTICE +6 -1
  314. package/kotlin/fork/README.md +15 -7
  315. package/kotlin/fork/build.sh +2 -0
  316. package/kotlin/fork/compose-worker-globals.patch +186 -4
  317. package/kotlin/fork/fork.json +2 -2
  318. package/kotlin/fork/skiko-worker-clock.patch +12 -0
  319. package/kotlin/toolchain.json +12 -12
  320. package/kotlin/tools/expo-types.main.kts +115 -0
  321. package/package.json +17 -23
  322. package/scripts/dev-server-scanner.ts +92 -66
  323. package/skills/contrast/SKILL.md +2 -2
  324. package/skills/peach-debug/SKILL.md +1 -1
  325. package/skills/peach-perf/SKILL.md +1 -1
  326. package/skills/peach-setup/SKILL.md +2 -1
  327. package/skills/peach-test/SKILL.md +8 -0
  328. package/src/assetCatalogContract.ts +43 -1
  329. package/src/bridge-contract-input.ts +46 -19
  330. package/src/bridge-contract.ts +0 -1
  331. package/src/bundle-channel.mjs +197 -84
  332. package/src/bundleGlobalFooter.ts +38 -0
  333. package/src/capture-contract.ts +107 -33
  334. package/src/cloud-contract.ts +5 -1
  335. package/src/cloud.ts +5 -2
  336. package/src/config.ts +22 -3
  337. package/src/configured-bundle-producer.ts +1 -1
  338. package/src/desktop-host-delivery.ts +95 -1
  339. package/src/expo-kotlin-bundle.ts +29 -0
  340. package/src/expo-kotlin-host.ts +201 -0
  341. package/src/expo-swift-host.ts +96 -76
  342. package/src/expoKotlinContract.ts +70 -0
  343. package/src/expoSwiftContract.ts +4 -25
  344. package/src/home-paths.ts +7 -4
  345. package/src/host/bridge-host.ts +23 -3
  346. package/src/host/fetch-proxy-handler.ts +13 -0
  347. package/src/host/fetch-proxy-overrides.ts +11 -0
  348. package/src/host/replacement-module-handler.ts +5 -2
  349. package/src/index.ts +3 -1
  350. package/src/kotlin-host.ts +463 -19
  351. package/src/metro-plugin.ts +56 -28
  352. package/src/metro-production-bundle.ts +2 -2
  353. package/src/native-app-identity-project.ts +190 -0
  354. package/src/native-app-identity.ts +398 -34
  355. package/src/native-firebase-config.ts +69 -15
  356. package/src/native-seam-manifest.ts +6 -0
  357. package/src/one-widget-discovery.ts +2 -1
  358. package/src/openstep-plist.ts +107 -0
  359. package/src/react-native-host-modules.ts +4 -0
  360. package/src/runtime-assets.ts +2 -2
  361. package/src/sim-client.ts +2 -2
  362. package/src/swift-binary-modules.ts +3 -1
  363. package/src/swift-compile.ts +286 -38
  364. package/src/swift-distribution-contract.ts +4 -0
  365. package/src/swift-distribution.lock.json +123 -12
  366. package/src/swift-distribution.ts +7 -3
  367. package/src/swift-host.ts +298 -83
  368. package/src/swift-package-manifest.ts +29 -2
  369. package/src/swift-package.ts +180 -10
  370. package/src/swift-packages.ts +4 -0
  371. package/src/swift-standin-emit.ts +4 -1
  372. package/src/swift-standins.ts +274 -15
  373. package/src/swift-submit.ts +62 -7
  374. package/src/swift-toolchain.ts +6 -1
  375. package/src/swiftContract.ts +126 -3
  376. package/src/vite-plugin-one.ts +13 -16
  377. package/src/vite-plugin-swift.ts +4 -5
  378. package/src/vite-plugin.ts +360 -144
  379. package/src/vite.d.ts +1 -1
  380. package/src/worker-system-chunks.ts +221 -0
  381. package/src/worklet-executable.ts +348 -38
  382. package/src/worklets-babel.ts +1 -1
  383. package/src/xcode-project.ts +243 -127
  384. package/dist-lib/agent-identity.cjs +0 -39
  385. package/dist-lib/agent-sessions.cjs +0 -760
  386. package/dist-lib/cli-constants.cjs +0 -32
  387. package/dist-lib/render-mode.cjs +0 -44
  388. package/kotlin/AppSources/android/view/View.kt +0 -74
  389. package/kotlin/AppSources/android/widget/FrameLayout.kt +0 -6
  390. package/src/render-mode.ts +0 -15
  391. /package/kotlin/{AppSources → AndroidViewSources}/android/content/Context.kt +0 -0
  392. /package/kotlin/{AppSources → AndroidViewSources}/android/content/res/Configuration.kt +0 -0
  393. /package/kotlin/{AppSources → AndroidViewSources}/android/graphics/Color.kt +0 -0
  394. /package/kotlin/{AppSources → AndroidViewSources}/android/util/DisplayMetrics.kt +0 -0
  395. /package/kotlin/{AppSources → AndroidViewSources}/dev/peach/app/JavaFormat.kt +0 -0
  396. /package/kotlin/{AppSources → Sources}/java/lang/Runnable.kt +0 -0
@@ -5,6 +5,7 @@ import { basename, dirname, join } from 'node:path'
5
5
  import JSON5 from 'json5'
6
6
  import { parse as parseYaml } from 'yaml'
7
7
  import { nodeRepoFileReader } from './nodeRepoFileReader'
8
+ import type { NativeRenderer } from '../../src/config'
8
9
  import type { DependencyVersion, ProjectManifest } from './projectReportContract'
9
10
  import type { RepoFileReader } from './repoFileReaderContract'
10
11
 
@@ -479,3 +480,103 @@ export function installedManifest(
479
480
  devDependencies: mapped(manifest.devDependencies),
480
481
  }
481
482
  }
483
+
484
+ // read only explicit static native build settings for the selected platform.
485
+ export function readNativeProjectRenderer(
486
+ dir: string,
487
+ platform: 'ios' | 'android',
488
+ files: RepoFileReader = nodeRepoFileReader,
489
+ ): NativeRenderer | undefined {
490
+ const declarations: Array<{ source: string; enabled: boolean }> = []
491
+ const add = (value: unknown, source: string, strings = false) => {
492
+ if (value === undefined) return
493
+ if (typeof value === 'boolean') declarations.push({ source, enabled: value })
494
+ else if (strings && (value === 'true' || value === 'false'))
495
+ declarations.push({ source, enabled: value === 'true' })
496
+ else throw new Error(`${source}: new architecture setting must be true or false`)
497
+ }
498
+ const json = (path: string): unknown => {
499
+ const text = readProjectMetadata(join(dir, path), files)
500
+ if (text === null) {
501
+ if (files.exists(join(dir, path)))
502
+ throw new Error(`${path}: unreadable native metadata`)
503
+ return undefined
504
+ }
505
+ try {
506
+ return JSON.parse(text)
507
+ } catch {
508
+ throw new Error(`${path}: malformed native metadata`)
509
+ }
510
+ }
511
+ const expo = metadataValue(json('app.json'), 'expo')
512
+ add(metadataValue(expo, 'newArchEnabled'), 'app.json expo.newArchEnabled')
513
+ add(
514
+ metadataValue(metadataValue(expo, platform), 'newArchEnabled'),
515
+ `app.json expo.${platform}.newArchEnabled`,
516
+ )
517
+ if (platform === 'ios') {
518
+ add(
519
+ metadataValue(json('ios/Podfile.properties.json'), 'newArchEnabled'),
520
+ 'ios/Podfile.properties.json newArchEnabled',
521
+ true,
522
+ )
523
+ const podfile = readProjectMetadata(join(dir, 'ios/Podfile'), files)
524
+ if (podfile === null && files.exists(join(dir, 'ios/Podfile')))
525
+ throw new Error('ios/Podfile: unreadable native metadata')
526
+ for (const line of (podfile ?? '').split(/\r?\n/)) {
527
+ // use_react_native!(new_arch_enabled: false) assigns RCT_NEW_ARCH_ENABLED
528
+ // itself; a non-literal argument defers to the declarations read here.
529
+ const argument =
530
+ /^\s*(?:use_react_native!\s*\(\s*)?:?new_arch_enabled\s*(?:=>|:)\s*(true|false)\b/.exec(
531
+ line,
532
+ )
533
+ if (argument) {
534
+ declarations.push({
535
+ source: 'ios/Podfile use_react_native! new_arch_enabled',
536
+ enabled: argument[1] === 'true',
537
+ })
538
+ continue
539
+ }
540
+ if (!/^\s*ENV\s*\[\s*['"]RCT_NEW_ARCH_ENABLED['"]\s*\]\s*(?:\|\|)?=/.test(line))
541
+ continue
542
+ // expo's generated conditional applies the parsed podfile property;
543
+ // it adds no independent renderer declaration.
544
+ if (
545
+ /^\s*ENV\s*\[\s*(['"])RCT_NEW_ARCH_ENABLED\1\s*\]\s*\|\|=\s*(['"])0\2\s+if\s+podfile_properties\s*\[\s*(['"])newArchEnabled\3\s*\]\s*==\s*(['"])false\4\s*;?\s*(?:#.*)?$/.test(
546
+ line,
547
+ )
548
+ )
549
+ continue
550
+ const match =
551
+ /^\s*ENV\s*\[\s*(['"])RCT_NEW_ARCH_ENABLED\1\s*\]\s*=\s*(['"])([01])\2\s*;?\s*(?:#.*)?$/.exec(
552
+ line,
553
+ )
554
+ if (!match)
555
+ throw new Error(
556
+ 'ios/Podfile: RCT_NEW_ARCH_ENABLED must be an unconditional literal 0 or 1 assignment',
557
+ )
558
+ declarations.push({
559
+ source: 'ios/Podfile RCT_NEW_ARCH_ENABLED',
560
+ enabled: match[3] === '1',
561
+ })
562
+ }
563
+ } else {
564
+ const gradle = readProjectMetadata(join(dir, 'android/gradle.properties'), files)
565
+ if (gradle === null && files.exists(join(dir, 'android/gradle.properties')))
566
+ throw new Error('android/gradle.properties: unreadable native metadata')
567
+ for (const line of (gradle ?? '').split(/\r?\n/)) {
568
+ if (!/^\s*newArchEnabled\s*(?:[=:]|\s)/.test(line)) continue
569
+ const match = /^\s*newArchEnabled\s*[=:]\s*(true|false)\s*$/.exec(line)
570
+ if (!match)
571
+ throw new Error('android/gradle.properties: newArchEnabled must be true or false')
572
+ add(match[1], 'android/gradle.properties newArchEnabled', true)
573
+ }
574
+ }
575
+ const first = declarations[0]
576
+ if (!first) return undefined
577
+ if (declarations.some((entry) => entry.enabled !== first.enabled))
578
+ throw new Error(
579
+ `contradictory ${platform} native renderer metadata: ${declarations.map((entry) => entry.source).join(', ')}`,
580
+ )
581
+ return first.enabled ? 'fabric' : 'paper'
582
+ }
@@ -18,7 +18,7 @@ import { dirname, resolve, join, relative } from 'path'
18
18
  import { rnxPublicBrand } from '../../src/public-brand'
19
19
  import { resolveCliAuth } from '../auth'
20
20
  import { registerRun, shouldRegisterRun } from '../run-registry'
21
- import { rnxExit } from '../run-rnx'
21
+ import { peachExit } from '../run-rnx'
22
22
  import { buildShellUrl } from './control'
23
23
  import { discoverSootsimUrl } from './flow'
24
24
  import { resolveDefaultUploadOrigin } from './upload'
@@ -54,7 +54,7 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
54
54
 
55
55
  if (args.includes('--help') || args.includes('-h')) {
56
56
  console.log(HELP)
57
- rnxExit(0)
57
+ peachExit(0)
58
58
  }
59
59
 
60
60
  if (args[0] === 'init') {
@@ -145,7 +145,7 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
145
145
  ` error: no detox tests found. expected one of: ${detoxDirs.join(', ')}\n` +
146
146
  ` run 'peach detox init' to scaffold a starter suite.`,
147
147
  )
148
- rnxExit(1)
148
+ peachExit(1)
149
149
  }
150
150
  testDir ??= process.cwd()
151
151
 
@@ -162,7 +162,7 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
162
162
  ` error: ${rnxPublicBrand.commandName} detox runs Jest from this project, which needs its dev dependencies:\n` +
163
163
  ` npm i -D ${rnxPublicBrand.packageName} jest`,
164
164
  )
165
- rnxExit(1)
165
+ peachExit(1)
166
166
  }
167
167
  }
168
168
 
@@ -378,7 +378,7 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
378
378
  ) {
379
379
  unlinkSync(resultsFile)
380
380
  }
381
- rnxExit(code)
381
+ peachExit(code)
382
382
  }
383
383
 
384
384
  export type JestRunSummary = {
@@ -515,5 +515,5 @@ describe('example', () => {
515
515
  console.log(`\n next:`)
516
516
  console.log(` peach detox --app <dev-server-port> # run the sample`)
517
517
  console.log(` peach detox --watch`)
518
- rnxExit(0)
518
+ peachExit(0)
519
519
  }
@@ -6,7 +6,7 @@ import {
6
6
  } from 'peach-engine/settings/devices'
7
7
  import { DUO_REST_PROGRESS, type DuoFoldRest } from 'peach-engine/settings/duo-fold-bus'
8
8
  import { SOOTSIM_SHELL_COMMAND_NAME } from '../../src/menu'
9
- import { rnxExit } from '../run-rnx'
9
+ import { peachExit } from '../run-rnx'
10
10
  import {
11
11
  callInBridge,
12
12
  createBridgeFromParsed,
@@ -121,7 +121,7 @@ async function readCurrentPosture(
121
121
  export async function runDeviceCommand(args: string[], opts: DeviceCommandOptions) {
122
122
  if (args.includes('--help') || args.includes('-h')) {
123
123
  usage()
124
- rnxExit(0)
124
+ peachExit(0)
125
125
  }
126
126
 
127
127
  const subcommand = args[0]
@@ -130,7 +130,7 @@ export async function runDeviceCommand(args: string[], opts: DeviceCommandOption
130
130
  const current = await readCurrentDevice(parsed)
131
131
  if (!current) {
132
132
  console.error(' error: could not read current device from the target sim')
133
- rnxExit(1)
133
+ peachExit(1)
134
134
  }
135
135
  console.log(
136
136
  ` ${current.model} ${current.width}x${current.height} pt ${current.width * current.scale}x${current.height * current.scale} px ${current.name}`,
@@ -148,12 +148,12 @@ export async function runDeviceCommand(args: string[], opts: DeviceCommandOption
148
148
  const model = args[1]
149
149
  if (!model) {
150
150
  console.error(' error: device set expects a model')
151
- rnxExit(1)
151
+ peachExit(1)
152
152
  }
153
153
  if (!(model in devices)) {
154
154
  console.error(` error: unknown device "${model}"`)
155
155
  console.error(' run `peach device list` to see valid models')
156
- rnxExit(1)
156
+ peachExit(1)
157
157
  }
158
158
  const parsed = parseBridgeCliArgs(args, { port: opts.port })
159
159
  const bridge = createBridgeFromParsed(parsed)
@@ -182,25 +182,25 @@ export async function runDeviceCommand(args: string[], opts: DeviceCommandOption
182
182
  const want = raw === undefined || raw === 'toggle' || isDuoRest(raw) ? raw : null
183
183
  if (want === null) {
184
184
  console.error(` error: device posture expects ${DUO_RESTS.join(', ')}, or toggle`)
185
- rnxExit(1)
185
+ peachExit(1)
186
186
  }
187
187
  const current = await readCurrentPosture(parsed)
188
188
  if (!current) {
189
189
  console.error(' error: could not read current posture from the target sim')
190
- rnxExit(1)
190
+ peachExit(1)
191
191
  }
192
192
  if (!devices[current.model].supportedPostures) {
193
193
  console.error(
194
194
  ` error: ${current.model} has a single posture — fold posture is only available on iphone-duo`,
195
195
  )
196
- rnxExit(1)
196
+ peachExit(1)
197
197
  }
198
198
  if (!want) {
199
199
  if (!current.rest) {
200
200
  console.error(
201
201
  ' error: the Duo fold has no current rest; choose a posture explicitly',
202
202
  )
203
- rnxExit(1)
203
+ peachExit(1)
204
204
  }
205
205
  console.log(` ${current.rest} (${current.model})`)
206
206
  return
@@ -209,7 +209,7 @@ export async function runDeviceCommand(args: string[], opts: DeviceCommandOption
209
209
  console.error(
210
210
  ' error: the Duo fold has no current rest; choose a posture explicitly',
211
211
  )
212
- rnxExit(1)
212
+ peachExit(1)
213
213
  }
214
214
  const target: DuoFoldRest =
215
215
  want === 'toggle' ? (current.rest === 'closed' ? 'open' : 'closed') : want
@@ -246,7 +246,7 @@ export async function runDeviceCommand(args: string[], opts: DeviceCommandOption
246
246
  console.error(
247
247
  ` error: the fold did not come to rest at ${target} within 10s (${current.model})`,
248
248
  )
249
- rnxExit(1)
249
+ peachExit(1)
250
250
  }
251
251
  console.log(` set posture: ${target} (${current.model})`)
252
252
  } finally {
@@ -257,5 +257,5 @@ export async function runDeviceCommand(args: string[], opts: DeviceCommandOption
257
257
 
258
258
  usage()
259
259
  console.error(` unknown subcommand: ${subcommand}`)
260
- rnxExit(1)
260
+ peachExit(1)
261
261
  }
@@ -1,14 +1,14 @@
1
- // peach desktop — launch the installed desktop companion via the
2
- // electron driver, or the native host via the desktop driver with --native
3
- // (or PEACH_DESKTOP_APP). the drivers wrap findDesktopCompanion +
4
- // launchDesktopCompanion, so this command is a thin CLI surface that
5
- // parses options, prompts for install, and delegates everything else.
1
+ // desktop launches the native host on Mac and Linux, and Electron on Windows.
6
2
 
7
3
  import { ensureProfile } from '../../src/profiles'
8
- import { findDesktopCompanion, wantsNativeDesktop } from '../desktop-companion'
4
+ import {
5
+ findDesktopCompanion,
6
+ wantsNativeDesktop,
7
+ notifyDesktopMigration,
8
+ } from '../desktop-companion'
9
9
  import { desktopDriver, electronDriver } from '../drivers'
10
10
  import { confirm } from '../prompt'
11
- import { rnxExit } from '../run-rnx'
11
+ import { peachExit } from '../run-rnx'
12
12
  import { buildShellUrl } from './control'
13
13
  import { printMissingCompanionHint } from './no-bridge-hint'
14
14
 
@@ -24,7 +24,7 @@ function parseInlinePort(args: string[]): number | undefined {
24
24
  const port = raw ? Number(raw) : Number.NaN
25
25
  if (!Number.isInteger(port) || port <= 0) {
26
26
  console.error(` invalid --port value: ${raw || '(missing)'}`)
27
- rnxExit(1)
27
+ peachExit(1)
28
28
  }
29
29
  return port
30
30
  }
@@ -33,6 +33,8 @@ export async function resolveElectronLaunchUrl(
33
33
  args: string[],
34
34
  opts: ElectronOptions,
35
35
  ): Promise<string | undefined> {
36
+ const deepLink = args.find((arg) => arg.startsWith('peach://'))
37
+ if (deepLink) return new URL(deepLink).href
36
38
  const targetPort = parseInlinePort(args) ?? opts.port
37
39
  return targetPort ? buildShellUrl(String(targetPort)) : undefined
38
40
  }
@@ -49,6 +51,7 @@ options:
49
51
  --port <number> connect to running dev server on this port
50
52
  --profile <id> launch with an isolated persistent storage profile
51
53
  --ephemeral launch with a temporary isolated storage profile
54
+ --electron launch Electron on Windows; native app on Mac and Linux
52
55
  --native launch the native desktop host (peach desktop install --native)
53
56
 
54
57
  examples:
@@ -57,17 +60,32 @@ examples:
57
60
  peach desktop --profile qa
58
61
  peach desktop --native
59
62
  `)
60
- rnxExit(0)
63
+ peachExit(0)
61
64
  }
62
65
 
63
66
  const profileArg = args.find((_, i) => args[i - 1] === '--profile')
64
67
  const ephemeralProfile = args.includes('--ephemeral')
65
68
  if (profileArg && ephemeralProfile) {
66
69
  console.error(' peach desktop: --profile cannot be combined with --ephemeral')
67
- rnxExit(1)
70
+ peachExit(1)
68
71
  }
69
72
  const profileId = profileArg ? ensureProfile(profileArg).id : undefined
70
- const native = wantsNativeDesktop({ native: args.includes('--native') })
73
+ if (args.includes('--native') && args.includes('--electron')) {
74
+ console.error(' --native cannot be combined with --electron')
75
+ peachExit(1)
76
+ }
77
+ const native = wantsNativeDesktop({
78
+ native: args.includes('--electron')
79
+ ? false
80
+ : args.includes('--native')
81
+ ? true
82
+ : undefined,
83
+ })
84
+ notifyDesktopMigration()
85
+ if (args.includes('--electron') && native)
86
+ console.error(
87
+ ' Electron is Windows-only; using the native desktop app (peach install-desktop).',
88
+ )
71
89
  let install = findDesktopCompanion({ native })
72
90
  if (!install) {
73
91
  printMissingCompanionHint(native)
@@ -76,10 +94,10 @@ examples:
76
94
  if (canPrompt && (await confirm('run peach desktop install now?', true))) {
77
95
  console.log()
78
96
  const { runInstallDesktop } = await import('./install-desktop')
79
- await runInstallDesktop(native ? ['--yes', '--native'] : ['--yes'])
97
+ await runInstallDesktop(['--yes'])
80
98
  install = findDesktopCompanion({ native })
81
99
  }
82
- if (!install) rnxExit(1)
100
+ if (!install) peachExit(1)
83
101
  }
84
102
  console.log(` launching ${install.path}`)
85
103
 
@@ -92,7 +110,7 @@ examples:
92
110
  })
93
111
  if (!result.launched) {
94
112
  console.error(` ${result.message}`)
95
- rnxExit(1)
113
+ peachExit(1)
96
114
  }
97
115
  console.log(` ${result.message}`)
98
116
  }
@@ -7,7 +7,6 @@ import { tmpdir } from 'os'
7
7
  import * as path from 'path'
8
8
  import { scanDevServers } from '../../scripts/dev-server-scanner'
9
9
  import { loadOptionalDemoApps } from '../../scripts/optional-demo-registry'
10
- import { applyPeachConfigToUrl, type PeachConfig } from '../../src/config'
11
10
  import { printBridgeFailureDiagnostics } from '../bridge-diagnostics'
12
11
  import {
13
12
  SootSimBridgeFlowRunner,
@@ -15,14 +14,14 @@ import {
15
14
  type SootSimFlowProfileResult,
16
15
  type SootSimFlowTraceStep,
17
16
  } from '../bridge-flow-runner'
18
- import { readCurrentSimId, saveCurrentSimId } from '../current-sim'
19
- import { findDesktopCompanion } from '../desktop-companion'
17
+ import { clearCurrentSimId, readCurrentSimId, saveCurrentSimId } from '../current-sim'
18
+ import { findDesktopCompanion, notifyDesktopMigration } from '../desktop-companion'
20
19
  import { exportFlowSession } from '../flow-export'
21
20
  import { parseFlowFile, validateFlowFile } from '../flow-file'
22
21
  import { FlowLiveStatusReporter } from '../flow-live-status'
23
22
  import { finalizeFlowSession, keepFlowCandidate, startFlowSession } from '../flow-session'
24
23
  import { GLOBAL_FLAG_TAKES_VALUE } from '../parse-args'
25
- import { PeachExit, rnxExit } from '../run-rnx'
24
+ import { PeachExit, peachExit } from '../run-rnx'
26
25
  import {
27
26
  BRIDGE_VALUE_FLAGS,
28
27
  createBridge,
@@ -32,15 +31,10 @@ import {
32
31
  parseBridgeCliArgs,
33
32
  resolveBridgePortForPin,
34
33
  } from '../ws-bridge'
35
- import {
36
- buildShellUrl,
37
- closeSimsBulk,
38
- resolveShellBaseUrlForBridgePort,
39
- runOpenCommand,
40
- terminatePlaywrightHostsForSims,
41
- } from './control'
34
+ import { closeSimsBulk, runOpenCommand, terminatePlaywrightHostsForSims } from './control'
42
35
  import { printShellPerfReport } from './inspect'
43
36
  import { resolveDefaultUploadOrigin } from './upload'
37
+ import type { PeachConfig } from '../../src/config'
44
38
  import type { UploadResult } from './upload'
45
39
 
46
40
  export { parseFlowFile, validateFlowFile } from '../flow-file'
@@ -59,6 +53,7 @@ const FLOW_BOOLEAN_FLAGS = [
59
53
  '--headless',
60
54
  '--headed',
61
55
  '--proof',
56
+ '--load-env',
62
57
  '--no-shell',
63
58
  '--shell-only',
64
59
  '--dry-run',
@@ -207,9 +202,9 @@ export async function resolveAppFrontmatterTarget(
207
202
  export async function discoverSootsimUrl(): Promise<string | null> {
208
203
  const servers = await scanDevServers()
209
204
  const patched = servers.find((s) => s.patched)
210
- if (patched) return `http://localhost:${patched.port}/__soot/`
205
+ if (patched) return `http://localhost:${patched.port}/__peachsim/`
211
206
  const any = servers[0]
212
- if (any) return `http://localhost:${any.port}/__soot/`
207
+ if (any) return `http://localhost:${any.port}/__peachsim/`
213
208
  return null
214
209
  }
215
210
 
@@ -221,6 +216,9 @@ export async function runFlowPlayback(
221
216
  hooks: {
222
217
  beforeSteps?: () => Promise<void>
223
218
  runSteps?: (driver: SootSimBridgeFlowRunner, steps: MaestroStep[]) => Promise<void>
219
+ // afterSteps runs once the steps end, passed or failed, while the app is
220
+ // still on screen (maestro compare reads the app console here).
221
+ afterSteps?: (driver: SootSimBridgeFlowRunner) => Promise<void>
224
222
  } = {},
225
223
  ): Promise<number> {
226
224
  lastPreviewUploadResult = null
@@ -281,6 +279,8 @@ options:
281
279
  frames without this, so its screenshot fails after 12
282
280
  tries. use it for any run whose screenshots get
283
281
  compared to anything
282
+ --load-env load local app environment through open for
283
+ react-native-config
284
284
  --electron prefer opening the target in the desktop companion
285
285
  --sim <id> target a specific current sim
286
286
  --validate replay the drafted flow before clearing it
@@ -325,7 +325,7 @@ flow extension:
325
325
  path: marketing/hero
326
326
  withFrame: true
327
327
  `)
328
- rnxExit(0)
328
+ peachExit(0)
329
329
  }
330
330
 
331
331
  const unexpected = findUnexpectedFlowArgs(args)
@@ -334,13 +334,13 @@ flow extension:
334
334
  console.error(` error: ${problem.message}`)
335
335
  }
336
336
  console.error('\n run `peach maestro --help` for the flags this command accepts')
337
- rnxExit(1)
337
+ peachExit(1)
338
338
  }
339
339
 
340
340
  const flowPath = args[0]
341
341
  if (!fs.existsSync(flowPath)) {
342
342
  console.error(` error: ${flowPath} not found`)
343
- rnxExit(1)
343
+ peachExit(1)
344
344
  }
345
345
 
346
346
  const getFlag = (name: string) => args.find((_, i) => args[i - 1] === name)
@@ -408,7 +408,7 @@ flow extension:
408
408
  : ''
409
409
  if (previewBillingKind && previewBillingKind !== 'test_run') {
410
410
  console.error(` error: invalid --billing-kind: ${previewBillingKind}`)
411
- rnxExit(1)
411
+ peachExit(1)
412
412
  }
413
413
  const replaceArgs = args.flatMap((arg, index) =>
414
414
  args[index - 1] === '--replace' ? ['--replace', arg] : [],
@@ -439,7 +439,7 @@ flow extension:
439
439
  const { frontmatter, steps } = parseFlowFile(flowContent)
440
440
  if (steps.length === 0) {
441
441
  console.error(' error: flow file must contain a YAML array of steps')
442
- rnxExit(1)
442
+ peachExit(1)
443
443
  }
444
444
  const targetDevice = deviceOverride || frontmatter.device || ''
445
445
  const targetTheme = themeOverride || frontmatter.theme || ''
@@ -474,16 +474,17 @@ flow extension:
474
474
  })
475
475
  if (pinned.error !== undefined) {
476
476
  console.error(` error: ${pinned.error}`)
477
- rnxExit(1)
477
+ peachExit(1)
478
478
  } else {
479
479
  parsedBridgeArgs.wsPort = pinned.port
480
480
  }
481
481
  }
482
- if (wantElectron && !findDesktopCompanion()) {
482
+ if (wantElectron) notifyDesktopMigration()
483
+ if (wantElectron && !findDesktopCompanion({ native: false })) {
483
484
  console.error(
484
- ' error: desktop companion not found. install or build it first with `bun run build:electron`',
485
+ ' error: desktop app not found. install it with `peach install-desktop`',
485
486
  )
486
- rnxExit(1)
487
+ peachExit(1)
487
488
  }
488
489
 
489
490
  const effectiveOpenInNewSim =
@@ -551,26 +552,15 @@ flow extension:
551
552
  const discovered = await discoverSootsimUrl()
552
553
  if (!discovered) {
553
554
  console.error(' error: no current sim and no peach target found')
554
- rnxExit(1)
555
+ peachExit(1)
555
556
  }
556
557
  openTarget = discovered
557
558
  }
558
559
 
559
- if (openTarget && targetDevice) {
560
- const shellBaseUrl = applyPeachConfigToUrl(
561
- pinnedShellBaseUrl || resolveShellBaseUrlForBridgePort(parsedBridgeArgs.wsPort),
562
- targetAppResolution.runtimeConfig,
563
- )
564
- const url = new URL(
565
- await buildShellUrl(openTarget, shellBaseUrl, { proof: proofMode }),
566
- )
567
- url.searchParams.set('device', targetDevice)
568
- openTarget = url.toString()
569
- }
570
-
571
560
  if (openTarget) {
572
561
  const openArgs = [openTarget]
573
562
  if (pinnedShellBaseUrl) openArgs.push('--base-url', pinnedShellBaseUrl)
563
+ if (targetDevice) openArgs.push('--device', targetDevice)
574
564
  openArgs.push(...replaceArgs)
575
565
  openArgs.push(...remapArgs)
576
566
  if (effectiveOpenInNewSim) {
@@ -587,6 +577,7 @@ flow extension:
587
577
  openArgs.push('--headless')
588
578
  }
589
579
  if (proofMode) openArgs.push('--proof')
580
+ if (args.includes('--load-env')) openArgs.push('--load-env')
590
581
  const opened = await runOpenCommand(openArgs, {
591
582
  port: parsedBridgeArgs.wsPort,
592
583
  timeoutMs: parsedBridgeArgs.commandTimeoutMs,
@@ -714,6 +705,16 @@ flow extension:
714
705
  let exitCode = 0
715
706
  const dryRun = args.includes('--dry-run') || args.includes('--no-run')
716
707
  const startsWithLaunchApp = !!steps[0]?.launchApp
708
+ let afterStepsRan = false
709
+ const runAfterSteps = async () => {
710
+ if (!hooks.afterSteps || afterStepsRan) return
711
+ afterStepsRan = true
712
+ try {
713
+ await hooks.afterSteps(driver)
714
+ } catch (hookErr: any) {
715
+ console.warn(` after-steps hook failed: ${hookErr?.message || String(hookErr)}`)
716
+ }
717
+ }
717
718
  try {
718
719
  if (!startsWithLaunchApp) {
719
720
  await driver.waitForTree(120000)
@@ -788,6 +789,7 @@ flow extension:
788
789
  await hooks.beforeSteps?.()
789
790
  if (hooks.runSteps) await hooks.runSteps(driver, steps)
790
791
  else await driver.runFlow(steps, { skipPlan: dryRun })
792
+ await runAfterSteps()
791
793
  if (profile) {
792
794
  profileResult = await driver.stopProfile()
793
795
  }
@@ -828,6 +830,7 @@ flow extension:
828
830
  }
829
831
  console.log()
830
832
  } catch (err: any) {
833
+ await runAfterSteps()
831
834
  if (profile && !profileResult) {
832
835
  try {
833
836
  profileResult = await driver.stopProfile()
@@ -1068,9 +1071,14 @@ async function closeFlowOwnedSims(
1068
1071
  const openIds = ids.filter((id) =>
1069
1072
  sims.some((sim) => sim.id === id && sim.readyState === 'open'),
1070
1073
  )
1071
- if (openIds.length === 0) return
1072
1074
  const result = await closeSimsBulk(bridge, wsPort, commandTimeoutMs, openIds)
1073
1075
  await terminatePlaywrightHostsForSims(sims, openIds)
1076
+ // retiring a flow-owned browser also retires its selection. a caller that
1077
+ // selected another sim during the flow keeps that selection.
1078
+ const selected = readCurrentSimId()
1079
+ if (selected && ids.includes(selected) && !result.remaining.includes(selected)) {
1080
+ clearCurrentSimId()
1081
+ }
1074
1082
  if (result.closed.length > 0) {
1075
1083
  console.log(` closed flow sim(s): ${result.closed.join(', ')}`)
1076
1084
  }
@@ -1185,7 +1193,7 @@ async function runFlowSession(args: string[]) {
1185
1193
  const result = keepFlowCandidate()
1186
1194
  if (!result.active) {
1187
1195
  console.error(' no active flow draft — run `peach maestro start` first')
1188
- rnxExit(1)
1196
+ peachExit(1)
1189
1197
  }
1190
1198
  if (!result.kept) {
1191
1199
  console.log(' no pending action to keep')
@@ -1214,7 +1222,7 @@ async function runFlowSession(args: string[]) {
1214
1222
  )
1215
1223
  if (!result.active) {
1216
1224
  console.error(' no active flow draft — run `peach maestro start` first')
1217
- rnxExit(1)
1225
+ peachExit(1)
1218
1226
  }
1219
1227
  if (!result.valid) {
1220
1228
  console.error(' flow draft is not valid:')
@@ -1222,7 +1230,7 @@ async function runFlowSession(args: string[]) {
1222
1230
  console.error(
1223
1231
  ' draft preserved — keep at least one real interaction or run `peach maestro start` to reset',
1224
1232
  )
1225
- rnxExit(1)
1233
+ peachExit(1)
1226
1234
  }
1227
1235
 
1228
1236
  if (validate) {
@@ -1267,13 +1275,13 @@ async function runFlowSession(args: string[]) {
1267
1275
  const flowPath = args[1]
1268
1276
  if (!flowPath || flowPath.startsWith('-')) {
1269
1277
  console.error(' usage: peach maestro validate <path>')
1270
- rnxExit(1)
1278
+ peachExit(1)
1271
1279
  }
1272
1280
  const issues = validateFlowFile(flowPath)
1273
1281
  if (issues.length > 0) {
1274
1282
  console.error(` x ${flowPath} failed validation:`)
1275
1283
  for (const issue of issues) console.error(` - ${issue}`)
1276
- rnxExit(1)
1284
+ peachExit(1)
1277
1285
  }
1278
1286
  console.log(` + ${flowPath} looks valid`)
1279
1287
  return
@@ -1,10 +1,48 @@
1
1
  // what `peach swift` and `peach kotlin` share: a loopback-only guest host
2
2
  // and the teardown of the simulator, driver and bridge their open started.
3
3
  import { isLoopbackHost } from '../../src/backend-origin'
4
+ import { clearCurrentSimId, readCurrentSimId } from '../current-sim'
4
5
  import { createBridge } from '../ws-bridge'
5
- import { runCloseCommand, type OpenCommandResources } from './control'
6
+ import {
7
+ printDriverDiagnosticLogTail,
8
+ runCloseCommand,
9
+ type OpenCommandResources,
10
+ } from './control'
6
11
  import type http from 'node:http'
7
12
 
13
+ export async function waitForGuestCompletion(
14
+ resources: OpenCommandResources,
15
+ signal: AbortSignal,
16
+ label: string,
17
+ ): Promise<number> {
18
+ if (signal.aborted) return 0
19
+ let onAbort: () => void = () => {}
20
+ const aborted = new Promise<null>((resolve) => {
21
+ onAbort = () => resolve(null)
22
+ signal.addEventListener('abort', onAbort, { once: true })
23
+ })
24
+ try {
25
+ const result = await Promise.race([
26
+ aborted,
27
+ ...(resources.driver?.completion ? [resources.driver.completion] : []),
28
+ ])
29
+ if (result === null) return 0
30
+ // an exited driver has already closed its page and simulator.
31
+ if (result.signal === null && result.code !== null) {
32
+ if (resources.simId && readCurrentSimId() === resources.simId) clearCurrentSimId()
33
+ delete resources.simId
34
+ }
35
+ const code = result.code === 0 && result.signal === null ? 0 : 1
36
+ if (code && resources.driver) printDriverDiagnosticLogTail(resources.driver)
37
+ console.log(
38
+ ` ${label} completed: ${result.signal ? `signal ${result.signal}` : `exit ${result.code ?? 'unknown'}`}${result.code !== null && resources.driver?.completionFile ? `; receipt ${resources.driver.completionFile}` : ''}`,
39
+ )
40
+ return code
41
+ } finally {
42
+ signal.removeEventListener('abort', onAbort)
43
+ }
44
+ }
45
+
8
46
  export function isLoopbackGuestRequest(
9
47
  req: http.IncomingMessage,
10
48
  origin: string,