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
@@ -0,0 +1,554 @@
1
+ // peach maestro compare <flow-or-dir>: run a partner's existing maestro flows
2
+ // on their own iOS simulator under real maestro, then on peach, and write one
3
+ // html report pairing every screen (iOS, peach, pixel diff), both sides'
4
+ // errors and per-step timing. maestro-compare.ts holds the report pieces.
5
+
6
+ import { spawn, spawnSync } from 'child_process'
7
+ import * as fs from 'fs'
8
+ import * as os from 'os'
9
+ import * as path from 'path'
10
+ import { devices } from 'peach-engine/settings/devices'
11
+ import { rnxHomeDir } from '../../src/home-paths'
12
+ import { parseFlowFile } from '../flow-file'
13
+ import {
14
+ buildCompareHtml,
15
+ compareScreens,
16
+ comparePassed,
17
+ instrumentFlowSource,
18
+ nativeStepsFromCommandsJson,
19
+ peachStepsFromCommandsJson,
20
+ screensOverThreshold,
21
+ type CompareCapture,
22
+ type CompareFlow,
23
+ type CompareReport,
24
+ type CompareSide,
25
+ } from '../maestro-compare'
26
+ import { collectFlowFiles, loadMaestroWorkspaceConfig } from '../maestro-workspace'
27
+ import { FLOW_ARG_VALUE_FLAGS } from './flow'
28
+ import { applyEnvFlags, runMaestro, type RunMaestroOptions } from './maestro'
29
+
30
+ function printHelp() {
31
+ console.log(`
32
+ peach maestro compare — run your maestro flows on your iOS simulator and on
33
+ peach, then write an html report: every screen side by side with a pixel
34
+ diff, both sides' errors, and per-step timing.
35
+
36
+ usage:
37
+ peach maestro compare <flow-or-dir> [options]
38
+
39
+ needs real maestro (https://maestro.dev) and a booted iOS simulator with your
40
+ app installed. peach runs the same flows against the app it has open.
41
+
42
+ options:
43
+ --udid <id> iOS simulator to run maestro on (default: the one booted)
44
+ --out <dir> report directory (default ~/.peach/compare/<time>)
45
+ --threshold <percent> flag screens differing by more than this (default 0.5)
46
+ --maestro <path> maestro binary (default: maestro on PATH, then ~/.maestro/bin)
47
+ --env KEY=VALUE passed to both runs (repeatable)
48
+ --include-tags, --exclude-tags, --config
49
+ flow selection, passed to both runs
50
+ other flags (--sim, --device, --headless, ...) go to the peach run
51
+
52
+ exits 0 when both sides pass every flow and no screen is over the threshold.
53
+ `)
54
+ }
55
+
56
+ function takeFlag(args: string[], name: string): string | undefined {
57
+ const i = args.indexOf(name)
58
+ if (i < 0) return undefined
59
+ const value = args[i + 1]
60
+ if (!value || value.startsWith('-')) throw new Error(`${name} expects a value`)
61
+ args.splice(i, 2)
62
+ return value
63
+ }
64
+
65
+ function findMaestro(explicit: string | undefined): string | null {
66
+ if (explicit) return fs.existsSync(explicit) ? explicit : null
67
+ const which = spawnSync('which', ['maestro'], { encoding: 'utf8' })
68
+ if (which.status === 0 && which.stdout.trim()) return which.stdout.trim()
69
+ const home = path.join(os.homedir(), '.maestro', 'bin', 'maestro')
70
+ return fs.existsSync(home) ? home : null
71
+ }
72
+
73
+ function bootedSimulators(): Array<{ udid: string; name: string }> {
74
+ const out = spawnSync('xcrun', ['simctl', 'list', 'devices', 'booted', '-j'], {
75
+ encoding: 'utf8',
76
+ })
77
+ if (out.status !== 0) return []
78
+ const devices = JSON.parse(out.stdout).devices as Record<
79
+ string,
80
+ Array<{ udid: string; name: string; state: string }>
81
+ >
82
+ return Object.values(devices)
83
+ .flat()
84
+ .filter((d) => d.state === 'Booted')
85
+ .map(({ udid, name }) => ({ udid, name }))
86
+ }
87
+
88
+ function simulatorName(udid: string): string {
89
+ return bootedSimulators().find((d) => d.udid === udid)?.name ?? udid
90
+ }
91
+
92
+ function appInstalled(udid: string, appId: string): boolean {
93
+ return (
94
+ spawnSync('xcrun', ['simctl', 'get_app_container', udid, appId], { stdio: 'ignore' })
95
+ .status === 0
96
+ )
97
+ }
98
+
99
+ // peach draws the iOS marketing status bar (9:41, full signal and battery);
100
+ // the simulator shows the real clock unless told otherwise.
101
+ function overrideStatusBar(udid: string) {
102
+ spawnSync(
103
+ 'xcrun',
104
+ [
105
+ 'simctl',
106
+ 'status_bar',
107
+ udid,
108
+ 'override',
109
+ '--time',
110
+ '9:41',
111
+ '--dataNetwork',
112
+ 'wifi',
113
+ '--wifiMode',
114
+ 'active',
115
+ '--wifiBars',
116
+ '3',
117
+ '--cellularMode',
118
+ 'active',
119
+ '--cellularBars',
120
+ '4',
121
+ '--batteryState',
122
+ 'charged',
123
+ '--batteryLevel',
124
+ '100',
125
+ ],
126
+ { stdio: 'ignore' },
127
+ )
128
+ }
129
+
130
+ function runLogged(bin: string, args: string[], logPath: string): Promise<number> {
131
+ return new Promise((resolve) => {
132
+ const log = fs.createWriteStream(logPath)
133
+ const child = spawn(bin, args, { stdio: ['ignore', 'pipe', 'pipe'] })
134
+ const forward = (chunk: Buffer) => {
135
+ log.write(chunk)
136
+ for (const line of chunk.toString().split('\n')) {
137
+ if (line.trim()) console.log(` ios │ ${line}`)
138
+ }
139
+ }
140
+ child.stdout.on('data', forward)
141
+ child.stderr.on('data', forward)
142
+ child.on('close', (code) => {
143
+ log.end()
144
+ resolve(code ?? 1)
145
+ })
146
+ child.on('error', (err) => {
147
+ log.end()
148
+ console.error(` error: could not start maestro: ${err.message}`)
149
+ resolve(1)
150
+ })
151
+ })
152
+ }
153
+
154
+ // the status bar band of the device whose screen matches the iOS capture's
155
+ // pixel size (peach's own device table); 0 for a size no device has.
156
+ function statusBarPx(png: Buffer): number {
157
+ // png ihdr: width and height are the big-endian words at bytes 16 and 20
158
+ const width = png.readUInt32BE(16)
159
+ const height = png.readUInt32BE(20)
160
+ const spec = Object.values(devices).find(
161
+ (d) =>
162
+ Math.round(d.width * d.scale) === width &&
163
+ Math.round(d.height * d.scale) === height,
164
+ )
165
+ return spec ? spec.statusBarHeight * spec.scale : 0
166
+ }
167
+
168
+ // the flow as its author would name it: relative inside the working
169
+ // directory, home-relative or absolute outside it.
170
+ function displayPath(cwd: string, file: string): string {
171
+ const rel = path.relative(cwd, file)
172
+ if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) return rel
173
+ const home = os.homedir()
174
+ return file.startsWith(home + path.sep) ? `~${file.slice(home.length)}` : file
175
+ }
176
+
177
+ function readJson(file: string): unknown {
178
+ try {
179
+ return JSON.parse(fs.readFileSync(file, 'utf8'))
180
+ } catch {
181
+ return null
182
+ }
183
+ }
184
+
185
+ function formatConsoleEntry(entry: unknown): string {
186
+ if (typeof entry === 'string') return entry
187
+ if (entry && typeof entry === 'object') {
188
+ const e = entry as Record<string, unknown>
189
+ const text = e.message ?? e.text ?? e.args
190
+ if (typeof text === 'string') return text
191
+ if (Array.isArray(text)) return text.map(String).join(' ')
192
+ }
193
+ return JSON.stringify(entry).slice(0, 2000)
194
+ }
195
+
196
+ function formatFailedRequest(entry: unknown): string {
197
+ if (!entry || typeof entry !== 'object') return `request failed: ${String(entry)}`
198
+ const e = entry as Record<string, unknown>
199
+ return `request failed: ${e.method ?? 'GET'} ${e.url ?? '?'} ${e.status ?? e.error ?? ''}`.trim()
200
+ }
201
+
202
+ function sideFromSteps(
203
+ steps: CompareSide['steps'],
204
+ ran: boolean,
205
+ extraErrors: string[],
206
+ ): CompareSide {
207
+ const failed = steps.find((s) => s.status === 'failed')
208
+ return {
209
+ status: !ran ? 'not run' : failed ? 'failed' : 'passed',
210
+ durationMs: steps.reduce((sum, s) => sum + s.durationMs, 0),
211
+ steps,
212
+ errors: [
213
+ ...(failed
214
+ ? [`step "${failed.label}" failed: ${failed.error ?? 'no message'}`]
215
+ : []),
216
+ ...extraErrors,
217
+ ],
218
+ }
219
+ }
220
+
221
+ // peach names each flow's output dir after the file stem, suffixing repeats
222
+ // in run order (commands/maestro.ts uniqueFlowDir).
223
+ function peachFlowDirs(root: string, files: string[]): string[] {
224
+ const seen = new Map<string, number>()
225
+ return files.map((file) => {
226
+ const slug =
227
+ path
228
+ .basename(file)
229
+ .replace(/\.(ya?ml)$/i, '')
230
+ .replace(/[^A-Za-z0-9._-]+/g, '_') || 'flow'
231
+ const n = (seen.get(slug) ?? 0) + 1
232
+ seen.set(slug, n)
233
+ return path.join(root, n === 1 ? slug : `${slug}-${n}`)
234
+ })
235
+ }
236
+
237
+ export async function runMaestroCompare(
238
+ argv: string[],
239
+ opts: RunMaestroOptions = {},
240
+ ): Promise<number> {
241
+ if (argv.includes('--help') || argv.includes('-h')) {
242
+ printHelp()
243
+ return 0
244
+ }
245
+ let args: string[]
246
+ const envPairs: string[] = []
247
+ for (let i = 0; i < argv.length; i++) {
248
+ if (argv[i] === '--env' || argv[i] === '-e') envPairs.push('--env', argv[i + 1] ?? '')
249
+ else if (argv[i]!.startsWith('--env=')) envPairs.push('--env', argv[i]!.slice(6))
250
+ }
251
+ try {
252
+ // sets process.env for the peach run and drops the flags from its argv
253
+ args = applyEnvFlags(argv)
254
+ } catch (err) {
255
+ console.error(` error: ${(err as Error).message}`)
256
+ return 1
257
+ }
258
+ let udid: string | undefined
259
+ let outFlag: string | undefined
260
+ let thresholdFlag: string | undefined
261
+ let maestroFlag: string | undefined
262
+ const shared: string[] = []
263
+ try {
264
+ udid = takeFlag(args, '--udid')
265
+ outFlag = takeFlag(args, '--out')
266
+ thresholdFlag = takeFlag(args, '--threshold')
267
+ maestroFlag = takeFlag(args, '--maestro')
268
+ for (const name of ['--include-tags', '--exclude-tags', '--config']) {
269
+ const value = takeFlag(args, name)
270
+ if (value) shared.push(name, value)
271
+ }
272
+ } catch (err) {
273
+ console.error(` error: ${(err as Error).message}`)
274
+ return 1
275
+ }
276
+ const threshold = thresholdFlag === undefined ? 0.5 : Number(thresholdFlag)
277
+ if (!Number.isFinite(threshold) || threshold < 0) {
278
+ console.error(' error: --threshold expects a percentage, for example 0.5')
279
+ return 1
280
+ }
281
+ const positional = args.findIndex(
282
+ (a, i) =>
283
+ !a.startsWith('-') && !(i > 0 && FLOW_ARG_VALUE_FLAGS.includes(args[i - 1]!)),
284
+ )
285
+ if (positional < 0) {
286
+ console.error(' error: pass the flow file or directory to compare')
287
+ printHelp()
288
+ return 1
289
+ }
290
+ const cwd = process.cwd()
291
+ const target = path.resolve(cwd, args[positional]!)
292
+ const peachArgs = args.filter((_, i) => i !== positional)
293
+ if (!fs.existsSync(target)) {
294
+ console.error(` error: ${target} not found`)
295
+ return 1
296
+ }
297
+
298
+ const maestro = findMaestro(maestroFlag)
299
+ if (!maestro) {
300
+ console.error(` error: maestro not found. install it with:
301
+ curl -fsSL "https://get.maestro.mobile.dev" | bash
302
+ or pass --maestro <path>`)
303
+ return 1
304
+ }
305
+ if (!udid) {
306
+ const booted = bootedSimulators()
307
+ if (booted.length !== 1) {
308
+ console.error(
309
+ booted.length === 0
310
+ ? ' error: no iOS simulator is booted. boot the one with your app installed, or pass --udid'
311
+ : ` error: ${booted.length} simulators are booted; pass --udid with one of:\n${booted.map((d) => ` ${d.udid} ${d.name}`).join('\n')}`,
312
+ )
313
+ return 1
314
+ }
315
+ udid = booted[0]!.udid
316
+ }
317
+
318
+ // the same selection maestro's planner makes, then an instrumented copy of
319
+ // each selected flow at the same relative path.
320
+ const targetIsDir = fs.statSync(target).isDirectory()
321
+ const root = targetIsDir ? target : path.dirname(target)
322
+ let files: string[]
323
+ try {
324
+ const sharedValue = (name: string) => {
325
+ const i = shared.indexOf(name)
326
+ return i >= 0 ? shared[i + 1] : undefined
327
+ }
328
+ const tags = (name: string) =>
329
+ sharedValue(name)
330
+ ?.split(',')
331
+ .map((t) => t.trim())
332
+ .filter(Boolean) ?? null
333
+ files = targetIsDir
334
+ ? collectFlowFiles(target, {
335
+ configPath: sharedValue('--config'),
336
+ includeTags: tags('--include-tags'),
337
+ excludeTags: tags('--exclude-tags'),
338
+ })
339
+ : [target]
340
+ } catch (err) {
341
+ console.error(` error: ${(err as Error).message}`)
342
+ return 1
343
+ }
344
+ if (files.length === 0) {
345
+ console.error(` error: no flows selected in ${target}`)
346
+ return 1
347
+ }
348
+
349
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
350
+ const out = path.resolve(cwd, outFlag ?? path.join(rnxHomeDir(), 'compare', stamp))
351
+ const flowsDir = path.join(out, 'flows')
352
+ fs.mkdirSync(flowsDir, { recursive: true })
353
+ const instrumented: Array<{
354
+ file: string
355
+ copy: string
356
+ name: string
357
+ appId?: string
358
+ captures: CompareCapture[]
359
+ }> = []
360
+ for (const file of files) {
361
+ const source = fs.readFileSync(file, 'utf8')
362
+ let result: ReturnType<typeof instrumentFlowSource>
363
+ try {
364
+ result = instrumentFlowSource(source, path.dirname(file))
365
+ } catch (err) {
366
+ console.error(` error: ${path.relative(cwd, file)}: ${(err as Error).message}`)
367
+ return 1
368
+ }
369
+ const copy = path.join(flowsDir, path.relative(root, file))
370
+ fs.mkdirSync(path.dirname(copy), { recursive: true })
371
+ fs.writeFileSync(copy, result.source)
372
+ const frontmatter = parseFlowFile(source).frontmatter as Record<string, unknown>
373
+ instrumented.push({
374
+ file,
375
+ copy,
376
+ name:
377
+ typeof frontmatter.name === 'string'
378
+ ? frontmatter.name
379
+ : path.basename(file).replace(/\.(ya?ml)$/i, ''),
380
+ appId: typeof frontmatter.appId === 'string' ? frontmatter.appId : undefined,
381
+ captures: result.captures,
382
+ })
383
+ }
384
+ if (targetIsDir) {
385
+ const configAt = shared.indexOf('--config')
386
+ const explicitConfig = configAt >= 0 ? shared[configAt + 1] : undefined
387
+ const config = loadMaestroWorkspaceConfig(target, explicitConfig)
388
+ if (config.path && !explicitConfig) {
389
+ fs.copyFileSync(config.path, path.join(flowsDir, path.relative(root, config.path)))
390
+ }
391
+ }
392
+ const runTarget = targetIsDir ? flowsDir : instrumented[0]!.copy
393
+
394
+ for (const appId of new Set(instrumented.map((f) => f.appId).filter(Boolean))) {
395
+ if (!appInstalled(udid, appId!)) {
396
+ console.error(
397
+ ` error: ${appId} is not installed on simulator ${udid}. build and install your app there first (for example \`npx expo run:ios\`).`,
398
+ )
399
+ return 1
400
+ }
401
+ }
402
+
403
+ const device = simulatorName(udid)
404
+ console.log(`
405
+ peach maestro compare — ${files.length} flow${files.length === 1 ? '' : 's'}
406
+ iOS: ${device} (${udid})
407
+ report: ${out}
408
+ `)
409
+
410
+ // 1. real maestro on the iOS simulator
411
+ overrideStatusBar(udid)
412
+ const nativeOut = path.join(out, 'ios')
413
+ let nativeExit: number
414
+ try {
415
+ nativeExit = await runLogged(
416
+ maestro,
417
+ [
418
+ '--udid',
419
+ udid,
420
+ 'test',
421
+ runTarget,
422
+ '--test-output-dir',
423
+ nativeOut,
424
+ '--no-ansi',
425
+ ...shared,
426
+ ...envPairs,
427
+ ],
428
+ path.join(out, 'ios.log'),
429
+ )
430
+ } finally {
431
+ spawnSync('xcrun', ['simctl', 'status_bar', udid, 'clear'], { stdio: 'ignore' })
432
+ }
433
+ console.log(`\n ios │ maestro exited ${nativeExit}\n`)
434
+
435
+ // 2. peach, same instrumented flows (its --env already applied above)
436
+ const peachOut = path.join(out, 'peach')
437
+ await runMaestro(
438
+ ['test', runTarget, '--test-output-dir', peachOut, ...shared, ...peachArgs],
439
+ {
440
+ ...opts,
441
+ captureConsole: true,
442
+ },
443
+ )
444
+
445
+ // 3. pair and diff
446
+ const nativeRun = fs.existsSync(nativeOut)
447
+ ? fs
448
+ .readdirSync(nativeOut)
449
+ .map((d) => path.join(nativeOut, d))
450
+ .filter((d) => fs.statSync(d).isDirectory())
451
+ .sort()
452
+ .pop()
453
+ : undefined
454
+ const peachDirs = peachFlowDirs(
455
+ peachOut,
456
+ instrumented.map((f) => f.copy),
457
+ )
458
+ const imagesDir = path.join(out, 'images')
459
+ const flows: CompareFlow[] = []
460
+ for (const [i, flow] of instrumented.entries()) {
461
+ const nativeDir = nativeRun ? path.join(nativeRun, flow.name) : null
462
+ const peachDir = peachDirs[i]!
463
+ const nativeRan = Boolean(nativeDir && fs.existsSync(nativeDir))
464
+ const peachRan = fs.existsSync(path.join(peachDir, 'commands.json'))
465
+ const nativeSteps = nativeRan
466
+ ? nativeStepsFromCommandsJson(readJson(path.join(nativeDir!, 'commands.json')))
467
+ : []
468
+ const peachSteps = peachRan
469
+ ? peachStepsFromCommandsJson(readJson(path.join(peachDir, 'commands.json')))
470
+ : []
471
+ const consoleJson = readJson(path.join(peachDir, 'logs', 'console.json')) as {
472
+ errors?: unknown[]
473
+ requests?: unknown[]
474
+ } | null
475
+ const peachConsole = [
476
+ ...[...new Set((consoleJson?.errors ?? []).map(formatConsoleEntry))].map(
477
+ (e) => `console error: ${e}`,
478
+ ),
479
+ ...(consoleJson?.requests ?? []).map(formatFailedRequest),
480
+ ]
481
+ const screens = []
482
+ for (const capture of flow.captures) {
483
+ const nativePng = nativeDir
484
+ ? path.join(nativeDir, 'takeScreenshot', `${capture.name}.png`)
485
+ : ''
486
+ const peachPng = path.join(peachDir, 'screenshots', `${capture.name}.png`)
487
+ const hasNative = Boolean(nativePng) && fs.existsSync(nativePng)
488
+ const hasPeach = fs.existsSync(peachPng)
489
+ const imageRel = path.join(
490
+ 'images',
491
+ `${i + 1}-${capture.name.split('/').pop()}.png`,
492
+ )
493
+ if (hasNative && hasPeach) {
494
+ const nativeBytes = fs.readFileSync(nativePng)
495
+ const { diff, triptych } = compareScreens(
496
+ nativeBytes,
497
+ fs.readFileSync(peachPng),
498
+ {
499
+ maskTopPx: statusBarPx(nativeBytes),
500
+ },
501
+ )
502
+ fs.mkdirSync(imagesDir, { recursive: true })
503
+ fs.writeFileSync(path.join(out, imageRel), triptych)
504
+ screens.push({ ...capture, image: imageRel, diff })
505
+ } else if (hasNative || hasPeach) {
506
+ const only = path.relative(out, hasNative ? nativePng : peachPng)
507
+ screens.push({
508
+ ...capture,
509
+ ...(hasNative ? { nativeOnly: only } : { peachOnly: only }),
510
+ })
511
+ } else {
512
+ screens.push({ ...capture })
513
+ }
514
+ }
515
+ flows.push({
516
+ name: flow.name,
517
+ file: displayPath(cwd, flow.file),
518
+ native: sideFromSteps(
519
+ nativeSteps,
520
+ nativeRan,
521
+ nativeRan
522
+ ? []
523
+ : [
524
+ `maestro produced no output for this flow (exit ${nativeExit}); see ios.log`,
525
+ ],
526
+ ),
527
+ peach: sideFromSteps(peachSteps, peachRan, peachConsole),
528
+ screens,
529
+ })
530
+ }
531
+
532
+ const report: CompareReport = {
533
+ createdAt: new Date().toISOString(),
534
+ device,
535
+ thresholdPercent: threshold,
536
+ flows,
537
+ }
538
+ fs.writeFileSync(path.join(out, 'report.json'), `${JSON.stringify(report, null, 2)}\n`)
539
+ const htmlPath = path.join(out, 'report.html')
540
+ fs.writeFileSync(htmlPath, buildCompareHtml(report))
541
+
542
+ const over = screensOverThreshold(report)
543
+ const compared = flows.flatMap((f) => f.screens).filter((s) => s.diff).length
544
+ console.log(`
545
+ compare summary:`)
546
+ for (const flow of flows) {
547
+ console.log(
548
+ ` ${flow.name}: iOS ${flow.native.status}, Peach ${flow.peach.status}, ${flow.screens.filter((s) => s.diff).length}/${flow.screens.length} screens paired`,
549
+ )
550
+ }
551
+ console.log(` ${compared} screens compared, ${over.length} over ${threshold}%`)
552
+ console.log(` report: ${htmlPath}\n`)
553
+ return comparePassed(report) ? 0 : 1
554
+ }
@@ -222,9 +222,12 @@ function parseObservedSelector(
222
222
  throw new Error(`generated selector was never observed: ${JSON.stringify(raw)}`)
223
223
  }
224
224
  const selector: ObservedSelector & { timeout?: number } = {}
225
- // id selectors are regexes in Maestro, so quote the observed literal id.
226
- if (typeof id === 'string') selector.id = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
227
- if (typeof text === 'string') selector.text = text
225
+ // maestro id and text selectors are regexes; generated observations are literals.
226
+ for (const key of ['id', 'text'] satisfies Array<'id' | 'text'>) {
227
+ const literal = raw[key]
228
+ if (typeof literal === 'string')
229
+ selector[key] = literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
230
+ }
228
231
  if (raw.index !== undefined) {
229
232
  if (
230
233
  typeof raw.index !== 'number' ||
@@ -46,6 +46,9 @@ import { resolveDefaultUploadOrigin } from './upload'
46
46
  export interface RunMaestroOptions {
47
47
  port?: number
48
48
  verbose?: boolean
49
+ // write each flow's app console to <test-output>/<flow>/logs/console.json
50
+ // (maestro compare reads it for the errors Peach saw)
51
+ captureConsole?: boolean
49
52
  }
50
53
 
51
54
  const MAESTRO_DIRS = ['.maestro', 'maestro']
@@ -59,6 +62,8 @@ usage:
59
62
  peach maestro test <flow-or-dir> # mirrors "maestro test"
60
63
  peach maestro load <flow> # load flow into runner without running (--dry-run)
61
64
  peach maestro test .maestro/ # workspace globs, root flows by default
65
+ peach maestro compare <flow-or-dir> # run on your iOS simulator and on peach,
66
+ # html report: screens, pixel diff, errors, timing
62
67
  peach maestro generate "<goal>" # generate, run, and upload a Maestro flow
63
68
  peach maestro start|keep|end # author a flow from live CLI actions
64
69
  peach maestro validate <flow> # validate a Maestro YAML file
@@ -307,6 +312,11 @@ export async function runMaestro(
307
312
  if (sub === 'init') {
308
313
  return runInit(cwd)
309
314
  }
315
+ if (sub === 'compare') {
316
+ const { runMaestroCompare } = await import('./maestro-compare')
317
+ // compare forwards --env to real maestro too, so it reads the raw argv
318
+ return runMaestroCompare(hoistLeadingSimFlag([...args]).slice(1), opts)
319
+ }
310
320
  if (sub === 'generate') {
311
321
  const { runMaestroGenerate } = await import('./maestro-generate')
312
322
  return runMaestroGenerate(remaining.slice(1))
@@ -544,10 +554,22 @@ export async function runMaestro(
544
554
  let exit = 0
545
555
  const startedAt = Date.now()
546
556
  try {
557
+ const consoleDir =
558
+ opts.captureConsole && flowTestDir ? path.join(flowTestDir, 'logs') : null
547
559
  exit = await runFlowPlayback(playbackArgs, {
548
560
  beforeSteps: async () => {
549
561
  console.log(`\n ▶ ${path.relative(cwd, file)}`)
550
562
  },
563
+ ...(consoleDir
564
+ ? {
565
+ afterSteps: async (driver) => {
566
+ fs.writeFileSync(
567
+ path.join(consoleDir, 'console.json'),
568
+ `${JSON.stringify(await driver.readConsole(), null, 2)}\n`,
569
+ )
570
+ },
571
+ }
572
+ : {}),
551
573
  })
552
574
  } catch (err) {
553
575
  if (err instanceof DisconnectedSimError) {
@@ -1,4 +1,4 @@
1
- import { rnxExit } from '../run-rnx'
1
+ import { peachExit } from '../run-rnx'
2
2
  import { runScreenshotMode } from './screenshot-mode'
3
3
  import { runShowTouchesMode } from './show-touches-mode'
4
4
  import { runThreeMode } from './three-mode'
@@ -44,7 +44,7 @@ export async function runMode(args: string[], opts: ModeOptions): Promise<void>
44
44
  if (!sub) {
45
45
  printHelp()
46
46
  if (!args.includes('--help') && !args.includes('-h')) {
47
- rnxExit(1)
47
+ peachExit(1)
48
48
  }
49
49
  return
50
50
  }
@@ -3,10 +3,14 @@
3
3
  import type { BridgeSimInfo } from '../ws-bridge'
4
4
 
5
5
  export function printMissingCompanionHint(native = false) {
6
- console.error(' no peach desktop companion found.')
6
+ console.error(
7
+ native
8
+ ? ' no native peach desktop host found.'
9
+ : ' no peach desktop companion found.',
10
+ )
7
11
  console.error('')
8
12
  console.error(' install the desktop app:')
9
- console.error(` peach desktop install${native ? ' --native' : ''}`)
13
+ console.error(' peach desktop install')
10
14
  console.error('')
11
15
  console.error(' or install the recommended background service:')
12
16
  console.error(' peach daemon install')