peachsim 0.1.1481 → 0.1.1619

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 (292) hide show
  1. package/README.md +56 -13
  2. package/cli/app-catalog.ts +7 -5
  3. package/cli/app-config.ts +47 -8
  4. package/cli/app-fonts.ts +84 -49
  5. package/cli/app-stubs.ts +433 -0
  6. package/cli/bin.ts +15 -2
  7. package/cli/bridge-flow-runner.ts +383 -123
  8. package/cli/cloud-client.ts +26 -25
  9. package/cli/cloud-test-tunnel.ts +2 -0
  10. package/cli/command-registry.ts +2 -1
  11. package/cli/commands/agent-indicator-mode.ts +39 -0
  12. package/cli/commands/box/bundle-plane.ts +4 -0
  13. package/cli/commands/box.ts +16 -23
  14. package/cli/commands/control.ts +221 -126
  15. package/cli/commands/daemon.ts +99 -24
  16. package/cli/commands/debug.ts +31 -3
  17. package/cli/commands/detect.ts +1 -1
  18. package/cli/commands/device.ts +6 -8
  19. package/cli/commands/diagnose.ts +2 -0
  20. package/cli/commands/do-chain.ts +2 -2
  21. package/cli/commands/doctor.ts +1030 -0
  22. package/cli/commands/flow.ts +447 -406
  23. package/cli/commands/inspect/actions.ts +2 -0
  24. package/cli/commands/inspect/core.ts +138 -46
  25. package/cli/commands/inspect/env.ts +10 -0
  26. package/cli/commands/inspect/list.ts +25 -8
  27. package/cli/commands/inspect/memory.ts +9 -0
  28. package/cli/commands/inspect/settling.ts +219 -66
  29. package/cli/commands/inspect/shared.ts +53 -12
  30. package/cli/commands/inspect/wait-event.ts +0 -1
  31. package/cli/commands/inspect/wait.ts +2 -2
  32. package/cli/commands/inspect.ts +51 -16
  33. package/cli/commands/install-desktop.ts +3 -2
  34. package/cli/commands/kotlin.ts +81 -22
  35. package/cli/commands/login.ts +1 -1
  36. package/cli/commands/maestro-compare.ts +41 -5
  37. package/cli/commands/maestro-generate.ts +11 -7
  38. package/cli/commands/maestro.ts +43 -15
  39. package/cli/commands/mode.ts +13 -1
  40. package/cli/commands/no-bridge-hint.ts +26 -3
  41. package/cli/commands/perf-record.ts +296 -0
  42. package/cli/commands/perf.ts +6 -1
  43. package/cli/commands/platform.ts +15 -4
  44. package/cli/commands/preview.ts +1 -1
  45. package/cli/commands/record.ts +37 -6
  46. package/cli/commands/report-issue.ts +2 -1
  47. package/cli/commands/runtime.ts +9 -11
  48. package/cli/commands/screenshots.ts +4 -3
  49. package/cli/commands/serve.ts +30 -2
  50. package/cli/commands/setup.ts +32 -7
  51. package/cli/commands/skills.ts +16 -43
  52. package/cli/commands/swift.ts +22 -3
  53. package/cli/commands/upgrade.ts +7 -6
  54. package/cli/commands/upload.ts +118 -69
  55. package/cli/commands/window.ts +449 -0
  56. package/cli/current-sim.ts +1 -2
  57. package/cli/desktop-companion.ts +84 -10
  58. package/cli/device-option.ts +8 -0
  59. package/cli/drivers/desktop.ts +3 -1
  60. package/cli/drivers/playwright-sim-host.ts +30 -11
  61. package/cli/drivers/playwright.ts +1 -4
  62. package/cli/flow-export.ts +2 -2
  63. package/cli/flow-file.ts +19 -18
  64. package/cli/internal-child.ts +7 -6
  65. package/cli/maestro-compare.ts +28 -6
  66. package/cli/maestro-js.ts +2 -1
  67. package/cli/main.ts +25 -4
  68. package/cli/material-symbol-fonts.ts +171 -0
  69. package/cli/outbound-endpoints.ts +132 -54
  70. package/cli/parse-args.ts +7 -0
  71. package/cli/preview-capture.ts +12 -0
  72. package/cli/privacy.ts +1 -1
  73. package/cli/run-registry.ts +5 -2
  74. package/cli/runtime-compat.ts +16 -0
  75. package/cli/telemetry.ts +6 -3
  76. package/cli/ui.ts +5 -0
  77. package/cli/ws-bridge.ts +121 -62
  78. package/detox/attempt-events.cjs +762 -0
  79. package/detox/attempt-events.d.cts +106 -0
  80. package/detox/gestures.ts +115 -2
  81. package/detox/index.ts +762 -426
  82. package/detox/jest-environment.cjs +86 -0
  83. package/detox/jest-preset.cjs +3 -1
  84. package/detox/jest-setup-after-env.cjs +1 -0
  85. package/detox/matchers.ts +3 -6
  86. package/detox/motion-change.cjs +259 -0
  87. package/detox/motion-change.d.cts +73 -0
  88. package/detox/proof-frame.cjs +83 -9
  89. package/dist-lib/agent-daemon-client.cjs +1 -1
  90. package/dist-lib/agent-events.cjs +1 -1
  91. package/dist-lib/attached-projects.cjs +1 -1
  92. package/dist-lib/auth/shared-session.cjs +15 -5
  93. package/dist-lib/backend-origin.cjs +1 -1
  94. package/dist-lib/beta.cjs +1 -1
  95. package/dist-lib/beta.mjs +1 -1
  96. package/dist-lib/bridge-constants.cjs +3 -1
  97. package/dist-lib/bridge-contract-input.cjs +1 -1
  98. package/dist-lib/bridge-contract-input.mjs +1 -1
  99. package/dist-lib/bridge-contract.cjs +1 -1
  100. package/dist-lib/bridge-contract.mjs +1 -1
  101. package/dist-lib/bundle-channel.cjs +5950 -1775
  102. package/dist-lib/capture-contract.cjs +1 -1
  103. package/dist-lib/capture-contract.mjs +1 -1
  104. package/dist-lib/cloud-contract.cjs +6 -1
  105. package/dist-lib/cloud-contract.mjs +5 -1
  106. package/dist-lib/cloud-test-args.cjs +1 -1
  107. package/dist-lib/cloud-test-args.mjs +1 -1
  108. package/dist-lib/cloud.cjs +1 -1
  109. package/dist-lib/cloud.mjs +1 -1
  110. package/dist-lib/config.cjs +34 -3
  111. package/dist-lib/config.mjs +33 -3
  112. package/dist-lib/detox/index.cjs +3443 -564
  113. package/dist-lib/detox/jest-environment.cjs +86 -0
  114. package/dist-lib/detox/jest-preset.cjs +3 -1
  115. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  116. package/dist-lib/expo-swift-bundle.cjs +5518 -1446
  117. package/dist-lib/expo-swift-metadata.cjs +21 -13
  118. package/dist-lib/expo-swift-metadata.mjs +21 -13
  119. package/dist-lib/home-paths.cjs +1 -1
  120. package/dist-lib/host/bridge-host.cjs +21728 -16808
  121. package/dist-lib/host/fetch-proxy-handler.cjs +177 -102
  122. package/dist-lib/host/fetch-proxy-overrides.cjs +84 -4
  123. package/dist-lib/host/fetch-proxy-overrides.mjs +74 -3
  124. package/dist-lib/host/replacement-module-handler.cjs +16 -15
  125. package/dist-lib/host/websocket-proxy.cjs +243 -6
  126. package/dist-lib/index.cjs +6063 -4113
  127. package/dist-lib/jump-to-source-babel.cjs +73 -35
  128. package/dist-lib/jump-to-source-native.cjs +118 -10
  129. package/dist-lib/kotlin-host.cjs +264 -121
  130. package/dist-lib/menu.cjs +1 -1
  131. package/dist-lib/menu.mjs +1 -1
  132. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  133. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  134. package/dist-lib/metro-production-bundle.cjs +147 -99
  135. package/dist-lib/metro-production-bundle.mjs +142 -99
  136. package/dist-lib/metro.cjs +4249 -360
  137. package/dist-lib/native-app-identity-project.cjs +318 -131
  138. package/dist-lib/native-app-identity-project.mjs +317 -131
  139. package/dist-lib/native-app-identity.cjs +322 -33
  140. package/dist-lib/native-app-identity.mjs +319 -32
  141. package/dist-lib/native-firebase-metadata.cjs +1 -1
  142. package/dist-lib/native-firebase-metadata.mjs +1 -1
  143. package/dist-lib/plist.cjs +1 -1
  144. package/dist-lib/plist.mjs +1 -1
  145. package/dist-lib/profiles.cjs +1 -1
  146. package/dist-lib/public-brand.cjs +1 -1
  147. package/dist-lib/react-native-host-modules.cjs +11 -1
  148. package/dist-lib/react-native-host-modules.mjs +11 -1
  149. package/dist-lib/scripts/dev-server-scanner.cjs +127 -42
  150. package/dist-lib/sdk.cjs +311 -103
  151. package/dist-lib/sdk.mjs +311 -103
  152. package/dist-lib/skills.cjs +18344 -11121
  153. package/dist-lib/swift-compile.cjs +3291 -544
  154. package/dist-lib/swift-compile.mjs +3227 -478
  155. package/dist-lib/swift-package.cjs +25 -3
  156. package/dist-lib/swift-package.mjs +22 -2
  157. package/dist-lib/swift-submit.cjs +2 -2
  158. package/dist-lib/swift-submit.mjs +2 -2
  159. package/dist-lib/swift-toolchain.cjs +1 -1
  160. package/dist-lib/swift.cjs +1 -1
  161. package/dist-lib/vite-swift.cjs +5523 -1428
  162. package/dist-lib/vite.cjs +251 -60
  163. package/dist-lib/worklet-executable.cjs +18 -4
  164. package/dist-lib/worklet-executable.mjs +18 -4
  165. package/install.sh +8 -3
  166. package/kotlin/AndroidViewSources/android/graphics/Canvas.kt +2 -0
  167. package/kotlin/AndroidViewSources/android/graphics/Paint.kt +18 -0
  168. package/kotlin/AndroidViewSources/dev/peach/app/JavaFormat.kt +16 -8
  169. package/kotlin/AppSources/android/view/Window.kt +4 -1
  170. package/kotlin/AppSources/android/window/BackEvent.kt +24 -0
  171. package/kotlin/AppSources/android/window/OnBackAnimationCallback.kt +7 -0
  172. package/kotlin/AppSources/android/window/OnBackInvokedDispatcher.kt +37 -1
  173. package/kotlin/AppSources/androidx/activity/BackEventCompat.kt +5 -0
  174. package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +8 -1
  175. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +14 -0
  176. package/kotlin/CompilerSources/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar +1 -0
  177. package/kotlin/CompilerSources/PeachJniIncoming.kt +243 -0
  178. package/kotlin/CompilerSources/PeachJniPlugin.kt +116 -0
  179. package/kotlin/ComposeSources/PeachComposeHost.kt +58 -32
  180. package/kotlin/ComposeSources/PeachMagnifier.kt +185 -0
  181. package/kotlin/ExpoComposeSources/PeachAndroidView.kt +16 -0
  182. package/kotlin/JniPlatformSources/androidx/annotation/CallSuper.kt +5 -0
  183. package/kotlin/JniPlatformSources/com/facebook/proguard/annotations/DoNotStrip.kt +5 -0
  184. package/kotlin/JniPlatformSources/dalvik/annotation/optimization/FastNative.kt +5 -0
  185. package/kotlin/JniPlatformSources/kotlin/Synchronized.kt +6 -0
  186. package/kotlin/JniPlatformSources/kotlin/jvm/JvmField.kt +5 -0
  187. package/kotlin/JniPlatformSources/kotlin/jvm/JvmStatic.kt +5 -0
  188. package/kotlin/JniSources/com/facebook/jni/HybridData.kt +68 -0
  189. package/kotlin/JniSources/dev/peach/jni/IncomingBindings.kt +157 -0
  190. package/kotlin/JniSources/dev/peach/jni/NativeBindings.kt +79 -0
  191. package/kotlin/JniTransportSources/dev/peach/jni/integration/JniTransport.kt +91 -0
  192. package/kotlin/README.md +18 -0
  193. package/kotlin/Sources/java/util/IllegalFormatCodePointException.kt +10 -0
  194. package/kotlin/Sources/java/util/IllegalFormatException.kt +5 -0
  195. package/kotlin/fixtures/counter/CounterApp.native.tsx +30 -3
  196. package/kotlin/fixtures/counter/CounterScreen.d.kt.ts +1 -0
  197. package/kotlin/fixtures/counter/CounterScreen.kt +2 -1
  198. package/kotlin/fixtures/counter/package.json +15 -2
  199. package/kotlin/fixtures/counter/vite.config.ts +11 -1
  200. package/kotlin/fork/NOTICE +1 -0
  201. package/kotlin/fork/README.md +5 -4
  202. package/kotlin/fork/compose-worker-globals.patch +629 -0
  203. package/kotlin/fork/fork.json +1 -1
  204. package/kotlin/toolchain.json +3 -3
  205. package/package.json +1 -1
  206. package/scripts/dev-server-scanner.ts +166 -81
  207. package/skills/contrast/SKILL.md +4 -4
  208. package/skills/peach-debug/SKILL.md +1 -1
  209. package/skills/peach-perf/SKILL.md +25 -0
  210. package/skills/peach-setup/SKILL.md +47 -13
  211. package/skills/peach-test/SKILL.md +2 -3
  212. package/src/agent-identity.ts +18 -0
  213. package/src/assetCatalogContract.ts +31 -3
  214. package/src/auth/shared-session.ts +3 -2
  215. package/src/bridge-constants.ts +1 -1
  216. package/src/bridge-contract.ts +5 -0
  217. package/src/bundle-channel.mjs +38 -7
  218. package/src/bundleGlobalFooter.ts +70 -20
  219. package/src/cloud-contract.ts +11 -0
  220. package/src/config.ts +47 -1
  221. package/src/configured-bundle-producer.ts +4 -1
  222. package/src/desktop-host-delivery.ts +4 -3
  223. package/src/expo-kotlin-host.ts +7 -2
  224. package/src/guest-bundle.ts +15 -8
  225. package/src/host/agent-host.ts +2 -4
  226. package/src/host/bridge-host.ts +260 -119
  227. package/src/host/fetch-proxy-handler.ts +237 -146
  228. package/src/host/fetch-proxy-overrides.ts +118 -0
  229. package/src/host/fetch-proxy-transport.ts +77 -63
  230. package/src/host/lease-owner.test.ts +18 -1
  231. package/src/host/lease-owner.ts +4 -5
  232. package/src/host/loopback-ports.ts +34 -0
  233. package/src/host/replacement-module-handler.ts +24 -15
  234. package/src/host/server-scan-handler.ts +87 -0
  235. package/src/host/staged-host-files.ts +92 -0
  236. package/src/host/websocket-proxy.ts +271 -5
  237. package/src/installed-package-versions.ts +66 -0
  238. package/src/jump-to-source-babel.ts +97 -89
  239. package/src/jump-to-source-native.ts +109 -11
  240. package/src/kotlin-host.ts +144 -27
  241. package/src/kotlin-module.ts +52 -0
  242. package/src/kotlinDiscoveryContract.ts +27 -0
  243. package/src/metro-layout-parser.ts +78 -0
  244. package/src/metro-plugin.ts +129 -39
  245. package/src/metro-production-bundle.ts +140 -139
  246. package/src/native-app-identity-project.ts +103 -5
  247. package/src/native-app-identity.ts +173 -11
  248. package/src/native-firebase-config.ts +83 -80
  249. package/src/native-seam-manifest.ts +0 -44
  250. package/src/nativeAppIconContract.ts +137 -0
  251. package/src/nativeViewSourceContract.ts +49 -0
  252. package/src/nativeViewWebContract.ts +41 -0
  253. package/src/nitro-bundle.ts +43 -0
  254. package/src/nitro-compile.ts +107 -0
  255. package/src/nitro-source.ts +310 -0
  256. package/src/nitroContract.ts +51 -0
  257. package/src/one-widget-metadata.ts +23 -3
  258. package/src/oneWidgetContract.ts +36 -3
  259. package/src/packageVersionsContract.ts +52 -0
  260. package/src/process-group.ts +306 -0
  261. package/src/process-liveness.ts +24 -0
  262. package/src/process-output.ts +21 -2
  263. package/src/react-devtools-frontend-react.ts +45 -0
  264. package/src/react-native-host-modules.ts +10 -0
  265. package/src/serverDiscoveryContract.ts +41 -0
  266. package/src/skills/builtin/maestro-test.ts +2 -4
  267. package/src/swift-compile.ts +3195 -650
  268. package/src/swift-design-time.ts +552 -0
  269. package/src/swift-distribution-contract.ts +11 -0
  270. package/src/swift-distribution.lock.json +46 -11
  271. package/src/swift-host.ts +144 -30
  272. package/src/swift-icu.ts +10 -2
  273. package/src/swift-objc-sdk.ts +726 -0
  274. package/src/swift-objc-standin.ts +441 -0
  275. package/src/swift-package.ts +25 -0
  276. package/src/swift-packages.ts +25 -10
  277. package/src/swift-standin-config.ts +94 -0
  278. package/src/swift-standin-emit.ts +5 -3
  279. package/src/swift-standins.ts +225 -81
  280. package/src/swift-submit.ts +2 -1
  281. package/src/swiftContract.ts +4 -0
  282. package/src/target-matcher.ts +67 -0
  283. package/src/visit-parsed-program.ts +21 -0
  284. package/src/vite-plugin-one.ts +65 -17
  285. package/src/vite-plugin-swift.ts +1 -0
  286. package/src/vite-plugin.ts +168 -70
  287. package/src/worker-system-chunks.ts +89 -10
  288. package/src/worklet-executable.ts +21 -3
  289. package/src/worklets-babel.ts +85 -13
  290. package/src/xcode-project.ts +81 -134
  291. package/src/xcode-target-members.ts +107 -0
  292. package/cli/commands/compat.ts +0 -395
package/README.md CHANGED
@@ -12,7 +12,8 @@ drivers, and library exports; its only command, `npx peachsim`, runs that same
12
12
  installer from the copy of `install.sh` it ships.
13
13
 
14
14
  The executable updates itself with `peach upgrade`, which then restarts a running
15
- background daemon onto the new executable and runtime. Successful interactive human
15
+ background daemon onto the new executable. A new runtime needs no restart: the
16
+ next new sim opens on it while open sims keep theirs. Successful interactive human
16
17
  commands refresh a cached stable CLI version in a detached process at most once
17
18
  every 20 hours. A newer version produces one short notice on stderr, once per
18
19
  version and no more than weekly. CI, agents, JSON output, pipes, and
@@ -162,6 +163,11 @@ a local foreground bridge when no daemon is reachable, then opens a `WsBridge`
162
163
  `cli/commands/inspect/*` and are re-exported as the programmatic `peachsim/sdk`;
163
164
  the Maestro runner is `cli/bridge-flow-runner.ts`; the Detox compat driver
164
165
  is in `detox/`. Test and screenshot paths additionally use `cli/drivers/*`.
166
+ Opening a simulator and waiting for reconnection share one app-tree readiness
167
+ poller. Its nominal wait budget includes WebSocket handshakes, target lookup,
168
+ evaluation and poll pauses; unanswered commands do not restart the deadline.
169
+ A timeout reports the last node count, missing test hook or bridge error and
170
+ elapsed time. Registering a simulator alone does not satisfy readiness.
165
171
  Browser sims launch in an isolated Playwright Chrome for Testing profile.
166
172
  An explicit `--base-url` keeps that shell's origin and uses its assets without
167
173
  installing an active runtime. A bridge started for that shell skips automatic
@@ -185,6 +191,12 @@ config dictionary is empty and the app can select its own public defaults.
185
191
  Preserve failures from those endpoints rather than remapping them to a successful
186
192
  local response. The same flag is available on `peach flow`.
187
193
 
194
+ A project with several Xcode application targets names the one to run, as an
195
+ Xcode scheme does: `peach open <app-port> --ios-target <name>` (also on
196
+ `peach flow` and `peach maestro test`) reads that target's version, build,
197
+ bundle id and display name. Without it, such an app served by a plain Metro
198
+ reads the runtime defaults.
199
+
188
200
  **The daemon is one process** — `SootSimBridgeHost` (`src/host/bridge-host.ts`).
189
201
  `peach serve` runs it in the foreground; the background daemon is the *same
190
202
  process* wrapped with autostart and the `~/.peach/daemon.json` lockfile. It is
@@ -196,6 +208,14 @@ guest-app fetch/WebSocket traffic so the cross-origin tenant worker can reach
196
208
  localhost. The `AgentHost` extension owns one FIFO reader per agent session and
197
209
  fans agent events out to every subscriber.
198
210
 
211
+ `peach open` auto-installs a published runtime only when the runtime directory
212
+ is empty and no live bridge already owns the runtime. When a live bridge is
213
+ performing its first download, `open` waits for that bridge's bootstrap state
214
+ to finish, then uses its installed runtime. Without a live bridge, runtime
215
+ files without a selected runtime containing `index.html` still cause a refusal.
216
+ Finish copying the runtime, then select it with `peach runtime use <version>`.
217
+ Write the active marker after staging is complete when preparing an isolated home.
218
+
199
219
  During source development, each Vite bridge owns one
200
220
  `~/.peach/dev-bridges/<port>.json` heartbeat record. CLI defaults select the
201
221
  current `PORT_OFFSET`, then the current checkout, then the canonical `:7668`
@@ -203,12 +223,13 @@ bridge, so concurrent worktrees cannot replace or delete one another's address.
203
223
  Each Vite shell also publishes its exact bridge at `/__sootsim/bridge`, so a
204
224
  launcher that reuses a shell from another isolated `PEACH_HOME` stays in that
205
225
  shell's bridge world without reading the other process's lockfile directory.
206
- An explicit `--port` also participates in `--sim` ownership discovery even
207
- without a local heartbeat record. The bridge must report that simulator as
208
- open; a port that names another simulator's world is rejected.
209
- SDK `connect({ sim })` and batched `do --then` actions use that same ownership
210
- discovery. An inferred default port does not override the named sim's world;
211
- an explicit SDK port or CLI `--port` retains the ownership check.
226
+ An explicit CLI `--port` or SDK port connects directly to that bridge without
227
+ scanning other worlds or requiring a local heartbeat record. That bridge handles
228
+ the named sim and reports an error if it is absent.
229
+ Without an explicit port, `--sim`, SDK `connect({ sim })`, and batched `do --then`
230
+ actions discover the sim's owning world. Each world probe has one two-second
231
+ wall deadline covering both connection and response. An inferred default port
232
+ does not override the named sim's world.
212
233
 
213
234
  **The sim** is a browser/Electron *page* launched by a CLI driver. Inside that
214
235
  page run three workers: the **shell worker** owns device and native state, the
@@ -267,6 +288,7 @@ therefore runs before assertions in each flow.
267
288
  curl -fsSL https://peachsim.com/install.sh | sh # or: npx peachsim
268
289
  peach # open ConnectRN and choose a running app in the simulator
269
290
  peach open /settings # dispatch a React Native deep link into that app
291
+ peach open /settings --no-describe --quiet # keep open options with deep links
270
292
  ```
271
293
 
272
294
  Start Metro, Expo, One, or another supported React Native development server
@@ -389,6 +411,8 @@ outside `node_modules`, `dist`, and `build`. Keep it before
389
411
  `react-native-worklets/plugin` in a custom Babel stack that lists both plugins.
390
412
  Apps using `babel-preset-expo` omit the explicit Worklets plugin because the
391
413
  preset configures it automatically.
414
+ Runtime Fragment aliases are excluded before element creation. Component tag
415
+ expressions are evaluated once, and ordinary components retain their locations.
392
416
 
393
417
  For One's Rolldown native bundler, use the native transform in `vite.config.ts`:
394
418
 
@@ -421,13 +445,14 @@ peach open 8081 # load a running metro/expo dev server
421
445
  peach open /settings # route via React Native Linking
422
446
  peach describe # dump UI tree
423
447
  peach get errors 5 # recent runtime failures
424
- peach compat # scan native package compatibility
448
+ peach doctor # check what of this app Peach runs
425
449
  peach support report "…" # preview an opt-in compatibility report
426
450
  peach find --testid cta # inspect one node deeply
427
451
  peach debug enable animated,layout
428
452
  peach debug trace shell on 240
429
453
  peach do tap-text "..." # interact
430
454
  peach do tap email --then type user@example.com --then tap submit
455
+ peach shell window --x 120 --y 140 --scale fit # place/scale desktop sim window
431
456
  peach debug snapshot before
432
457
  peach debug snapshot after
433
458
  peach debug diff before after
@@ -499,6 +524,11 @@ options and enables two integrations:
499
524
  The URL helpers request ordinary Metro graphs: `dev=false&minify=true` for
500
525
  production and `dev=true&minify=false` for development.
501
526
 
527
+ Metro registration readers retain the original factory bytes and module IDs.
528
+ Empty dependencies may be an empty array, an omitted production argument, or a
529
+ null development argument before the module name. Identity validation and
530
+ fingerprinting treat each as zero dependencies and reject malformed arguments.
531
+
502
532
  ```js
503
533
  const {
504
534
  toPeachProductionBundleUrl,
@@ -531,6 +561,16 @@ peach detox # auto-discovers e2e/ tests and launches a shell
531
561
  peach detox init # scaffold a config + sample test
532
562
  ```
533
563
 
564
+ `device.launchApp()` reuses a running guest by default. With a URL,
565
+ `device.launchApp({ url })` and `device.openURL({ url })` deliver a Linking
566
+ URL event to the running guest through the shell's deep-link bridge. Warm `launchApp` brings the guest forward; `openURL` retains its
567
+ foreground or background state. Both retain its browser session and app state.
568
+ `sourceApp` does not add fields to React Native's `{ url }` event. A
569
+ stopped guest rejects URL delivery. Explicit `newInstance: true`, deletion,
570
+ and video recording create a new browser context; cold launches still use the
571
+ supplied shell URL. When synchronization is enabled, URL delivery waits for
572
+ existing work before dispatch and for navigation and painting afterward.
573
+
534
574
  See `../../docs/migrating-from-detox.md` for details.
535
575
 
536
576
  ## Coming from Maestro
@@ -584,6 +624,9 @@ the framed export inline without changing Maestro syntax:
584
624
  native content, a populated node tree, or a live-frame channel whose publish
585
625
  count advances across probes. Canvas-only GL, WebGPU, and video surfaces can
586
626
  therefore prove that they are painting without weakening the boot-card guard.
627
+ A confirmed guest launch error ends readiness immediately, including waits longer
628
+ than one bridge request. Loading and suppressed entry errors keep their existing
629
+ readiness rules.
587
630
 
588
631
  ```yaml
589
632
  - takeScreenshot: hero
@@ -695,11 +738,11 @@ serves its selected version from a version-specific localhost origin, so a
695
738
  background activation cannot mix an already loaded shell with chunks from the
696
739
  next runtime. Without config, each new open selects the active stable version.
697
740
  When `peach.config.ts` sets `runtimeVersion`, `peach open` installs that
698
- version without changing the machine default. The same file replaces an app's own
699
- native modules (`nativeModules`, `turboModules`) and native views
700
- (`nativeViews`, keyed by the `requireNativeComponent` name or an Expo view's
701
- `Module_View` name) with project-local files; the configuration guide in the
702
- docs lists every slot. App config, native-linked fonts,
741
+ version without changing the machine default. The same file answers native code Peach does
742
+ not know under `packages`, keyed by the package and then its native module
743
+ and view names, and an installed `peach-stub-<package>` package answers in the
744
+ same shape with no config line; the configuration guide in the docs lists
745
+ every slot. App config, native-linked fonts,
703
746
  and splash assets resolve from the local directory named by `--project-root`
704
747
  when supplied, otherwise the project root advertised by the selected development
705
748
  server. Hosted recorders pass their checked-out repository explicitly because
@@ -3,6 +3,7 @@ import path from 'node:path'
3
3
  import {
4
4
  enumerateBundleColors,
5
5
  enumerateBundleImages,
6
+ enumerateBundleSymbols,
6
7
  } from '../../sootsim-swift/src/bundleAssets'
7
8
  import type { AppCatalog } from 'peach-engine/engine/app-catalog-wire'
8
9
 
@@ -36,7 +37,7 @@ function catalogFiles(dir: string, found: string[]) {
36
37
  }
37
38
  }
38
39
 
39
- // the app's asset-catalog colors and images and its declared accent, read off
40
+ // the app's asset-catalog colors, images, symbols and declared accent, read off
40
41
  // the ios project a prebuild or a bare app keeps on disk: the app target is
41
42
  // the folder holding the AppDelegate, the catalogs are its .xcassets, and the
42
43
  // accent is what its Info.plist or project build settings name. metro serves
@@ -65,9 +66,9 @@ export function discoverAppCatalog(
65
66
  const relative = files.map((file) =>
66
67
  path.relative(targetDir, file).split(path.sep).join(path.posix.sep),
67
68
  )
68
- const readContents = (candidate: string) =>
69
- candidate.endsWith('Contents.json') ? readText(path.join(targetDir, candidate)) : null
69
+ const readContents = (candidate: string) => readText(path.join(targetDir, candidate))
70
70
  const colors = enumerateBundleColors(relative, readContents)
71
+ const symbols = enumerateBundleSymbols(relative, readContents, () => '.')
71
72
  const served = path.relative(appDir, targetDir).split(path.sep).join(path.posix.sep)
72
73
  const images: AppCatalog['images'] = {}
73
74
  for (const { name, file, scale } of enumerateBundleImages(relative, readContents)) {
@@ -85,8 +86,9 @@ export function discoverAppCatalog(
85
86
  if (
86
87
  accentName === null &&
87
88
  Object.keys(colors).length === 0 &&
88
- Object.keys(images).length === 0
89
+ Object.keys(images).length === 0 &&
90
+ Object.keys(symbols).length === 0
89
91
  )
90
92
  return null
91
- return { colors, images, accentName }
93
+ return { colors, images, symbols, accentName }
92
94
  }
package/cli/app-config.ts CHANGED
@@ -4,6 +4,7 @@ import * as peachConfigModule from '../src/config'
4
4
  import { isPeachConfig, type PeachConfig } from '../src/config'
5
5
  import { rnxPublicBrand } from '../src/public-brand'
6
6
  import { resolveAppProject, type ResolvedAppProject } from './app-project'
7
+ import { resolveAppStubs, wireSlotMigration, type AppStubs } from './app-stubs'
7
8
 
8
9
  // `{ file }` entries are relative to the config file; the engine loads them by
9
10
  // absolute path from the runtime's replacement-module endpoint.
@@ -33,16 +34,42 @@ function resolveConfigReplacementFiles(
33
34
  return {
34
35
  ...config,
35
36
  modules: resolveFileEntries(config.modules, configDir),
36
- turboModules: resolveFileEntries(config.turboModules, configDir),
37
- nativeModules: resolveFileEntries(config.nativeModules, configDir),
38
- nativeViews: resolveFileEntries(config.nativeViews, configDir),
39
37
  }
40
38
  }
41
39
 
40
+ // the engine's wire slots; an app names native stubs by package instead
41
+ const WIRE_SLOTS = ['nativeModules', 'turboModules', 'nativeViews'] as const
42
+
43
+ // the app's config with every native stub lowered into the engine's wire slots:
44
+ // its own `packages` and the installed stub packages, applied with or without
45
+ // a peach.config.ts.
42
46
  export async function loadRNXAppConfig(
43
47
  project: ResolvedAppProject | null,
44
48
  ): Promise<PeachConfig | undefined> {
45
- if (!project?.configPath) return undefined
49
+ return (await loadRNXAppStubs(project)).config
50
+ }
51
+
52
+ export async function loadRNXAppStubs(
53
+ project: ResolvedAppProject | null,
54
+ ): Promise<{ config: PeachConfig | undefined; stubs: AppStubs }> {
55
+ if (!project) return { config: undefined, stubs: { applied: [], skipped: [] } }
56
+ const config = await loadConfigFile(project)
57
+ try {
58
+ return resolveAppStubs(
59
+ project.appDir,
60
+ config,
61
+ project.configPath ? path.dirname(project.configPath) : project.appDir,
62
+ )
63
+ } catch (error) {
64
+ const message = error instanceof Error ? error.message : String(error)
65
+ throw new Error(`could not load the native stubs of ${project.appDir}: ${message}`)
66
+ }
67
+ }
68
+
69
+ async function loadConfigFile(
70
+ project: ResolvedAppProject,
71
+ ): Promise<PeachConfig | undefined> {
72
+ if (!project.configPath) return undefined
46
73
  try {
47
74
  const jiti = createJiti(import.meta.url, {
48
75
  interopDefault: true,
@@ -57,6 +84,16 @@ export async function loadRNXAppConfig(
57
84
  if (!isPeachConfig(loaded)) {
58
85
  throw new Error('the default export must be a peach config object')
59
86
  }
87
+ const wire = WIRE_SLOTS.filter((slot) => loaded[slot] !== undefined)
88
+ if (wire.length > 0) {
89
+ const migration = wireSlotMigration(project.appDir, {
90
+ modules: { ...loaded.turboModules, ...loaded.nativeModules },
91
+ views: { ...loaded.nativeViews },
92
+ })
93
+ throw new Error(
94
+ `${wire.join(', ')} moved under packages, keyed by the package whose native side they answer. replace them with:\n${migration}`,
95
+ )
96
+ }
60
97
  return resolveConfigReplacementFiles(loaded, project.configPath)
61
98
  } catch (error) {
62
99
  const message = error instanceof Error ? error.message : String(error)
@@ -64,9 +101,11 @@ export async function loadRNXAppConfig(
64
101
  }
65
102
  }
66
103
 
67
- export async function resolveRNXAppConfig(
68
- startDir: string = process.cwd(),
69
- ): Promise<{ project: ResolvedAppProject | null; config: PeachConfig | undefined }> {
104
+ export async function resolveRNXAppConfig(startDir: string = process.cwd()): Promise<{
105
+ project: ResolvedAppProject | null
106
+ config: PeachConfig | undefined
107
+ stubs: AppStubs
108
+ }> {
70
109
  const project = resolveAppProject(startDir)
71
- return { project, config: await loadRNXAppConfig(project) }
110
+ return { project, ...(await loadRNXAppStubs(project)) }
72
111
  }
package/cli/app-fonts.ts CHANGED
@@ -3,16 +3,24 @@ import fs from 'node:fs'
3
3
  import { createRequire } from 'node:module'
4
4
  import path from 'node:path'
5
5
  import {
6
+ encodeAppFontsWire,
6
7
  expoFontPathsFromPlugins,
7
8
  nativeLinkedFontSpecsFromPaths,
8
9
  metroFontSpecsFromPaths,
10
+ appFontAssetUrlsFromWire,
9
11
  type ConfigPluginFontSpec,
12
+ type FontPlatform,
10
13
  } from 'peach-engine/engine/config-plugin-fonts'
11
14
  import { resolveListeningPid, resolveProcessCwd } from '../scripts/dev-server-scanner'
12
15
  import { loopbackOriginOf } from '../src/backend-origin'
13
16
  import { MANIFEST_PROBE_TIMEOUT_MS } from '../src/dev-bundle-resolution'
17
+ import { stageHostFile } from '../src/host/staged-host-files'
14
18
  import { parsePlistSource } from '../src/plist.ts'
15
19
  import { resolveAppProject } from './app-project'
20
+ import {
21
+ materialSymbolFontPaths,
22
+ materialSymbolFontAssets,
23
+ } from './material-symbol-fonts'
16
24
 
17
25
  const require = createRequire(import.meta.url)
18
26
 
@@ -31,24 +39,62 @@ export function discoverNativeLinkedAppFonts(opts: {
31
39
  bundleUrl: string
32
40
  projectDir: string
33
41
  platform?: string
42
+ nativeAssetOrigin: string
34
43
  }): DiscoveredAppFontSpec[] {
35
44
  const metroOrigin = loopbackOriginOf(opts.bundleUrl)
36
45
  if (!metroOrigin) return []
46
+ const { paths, materialSymbols } = discoverNativeLinkedAppFontAssets(opts)
37
47
  const specs = metroFontSpecsFromPaths(
38
- projectRelativeFontPaths(opts.projectDir, discoverNativeLinkedAppFontPaths(opts)),
48
+ projectRelativeFontPaths(opts.projectDir, paths),
39
49
  metroOrigin,
50
+ nativeFontPlatform(opts.platform),
51
+ )
52
+ attachMaterialSymbolMetadata(specs, materialSymbols, (asset) =>
53
+ stageHostFile(asset, opts.nativeAssetOrigin),
40
54
  )
41
- // a family linked from more than one file keeps the first declaration.
42
- const byFamily = new Map<string, DiscoveredAppFontSpec>()
43
- for (const spec of specs)
44
- if (!byFamily.has(spec.family)) byFamily.set(spec.family, spec)
45
- return [...byFamily.values()]
55
+ return specs
46
56
  }
47
57
 
48
- function projectRelativeFontPaths(projectDir: string, fontPaths: string[]): string[] {
49
- return fontPaths.map((fontPath) =>
50
- path.relative(path.resolve(projectDir), fontPath).split(path.sep).join('/'),
58
+ function nativeFontPlatform(platform: string | undefined): FontPlatform {
59
+ return (platform || process.env.CONTRAST_PLATFORM || 'ios').toLowerCase() === 'android'
60
+ ? 'android'
61
+ : 'ios'
62
+ }
63
+
64
+ function attachMaterialSymbolMetadata(
65
+ specs: ConfigPluginFontSpec[],
66
+ files: ReturnType<typeof materialSymbolFontAssets>,
67
+ assetUrl: (filePath: string) => string,
68
+ ): void {
69
+ const first = specs.find(
70
+ (spec) =>
71
+ spec.family !== undefined &&
72
+ /^MaterialSymbols(?:Outlined|Rounded|Sharp)_/.test(spec.family),
51
73
  )
74
+ if (first && files)
75
+ first.materialSymbols = {
76
+ hash: files.hash,
77
+ codepointsUrl: assetUrl(files.codepointsPath),
78
+ mirroredUrl: assetUrl(files.mirroredPath),
79
+ }
80
+ }
81
+
82
+ function projectRelativeFontPaths(projectDir: string, fontPaths: string[]): string[] {
83
+ // metro resolves an asset path lexically against its project root and reads
84
+ // through symlinks, so a font declared inside the project keeps its declared
85
+ // path even when node_modules is a symlink to a shared install. a font that
86
+ // node resolved outside the project (a hoisted workspace or pnpm store) is
87
+ // named through its real location, in the namespace of the real project root.
88
+ const projectRoot = path.resolve(projectDir)
89
+ const realProjectRoot = fs.realpathSync(projectDir)
90
+ return fontPaths.map((fontPath) => {
91
+ const lexical = path.relative(projectRoot, fontPath)
92
+ const relative =
93
+ lexical.startsWith('..') || path.isAbsolute(lexical)
94
+ ? path.relative(realProjectRoot, fs.realpathSync(fontPath))
95
+ : lexical
96
+ return relative.split(path.sep).join('/')
97
+ })
52
98
  }
53
99
 
54
100
  function isFile(filePath: string): boolean {
@@ -60,15 +106,15 @@ function isFile(filePath: string): boolean {
60
106
  }
61
107
 
62
108
  // absolute paths of every existing font file the native app links.
63
- function discoverNativeLinkedAppFontPaths(opts: {
109
+ function discoverNativeLinkedAppFontAssets(opts: {
64
110
  projectDir: string
65
111
  platform?: string
66
- }): string[] {
112
+ }): { paths: string[]; materialSymbols?: ReturnType<typeof materialSymbolFontAssets> } {
67
113
  const project = resolveAppProject(opts.projectDir)
68
114
  const selectedAppDir = project?.appDir ?? null
69
- if (!selectedAppDir) return []
115
+ if (!selectedAppDir) return { paths: [] }
70
116
 
71
- const platform = (opts.platform || process.env.CONTRAST_PLATFORM || 'ios').toLowerCase()
117
+ const platform = nativeFontPlatform(opts.platform)
72
118
 
73
119
  let getConfig: ExpoConfigGetter | null = null
74
120
  // resolve from the exact app root advertised by the development server.
@@ -97,13 +143,13 @@ function discoverNativeLinkedAppFontPaths(opts: {
97
143
  }
98
144
 
99
145
  // a declared path with no file behind it links nothing natively.
100
- const fontPaths = expoFontPathsFromPlugins(
101
- plugins,
102
- platform === 'android' ? 'android' : 'ios',
103
- )
146
+ const fontPaths = expoFontPathsFromPlugins(plugins, platform)
104
147
  .slice(0, 256)
105
148
  .map((fontPath) => path.resolve(selectedAppDir, fontPath))
106
149
  .filter(isFile)
150
+ const materialFonts =
151
+ platform === 'android' ? materialSymbolFontPaths(selectedAppDir) : []
152
+ fontPaths.push(...materialFonts)
107
153
  // the walk is bounded by WORK, not by a clock: at most 64 readdirs, at most 64
108
154
  // queued dirs, 4 levels deep, 256 paths. those ceilings are what keep a stray
109
155
  // node_modules from being crawled, and no machine can blow through them. a
@@ -270,7 +316,10 @@ function discoverNativeLinkedAppFontPaths(opts: {
270
316
  } catch {}
271
317
  }
272
318
 
273
- return [...new Set(fontPaths)]
319
+ return {
320
+ paths: [...new Set(fontPaths)],
321
+ materialSymbols: materialSymbolFontAssets(materialFonts),
322
+ }
274
323
  }
275
324
 
276
325
  // prebuilt guests cannot discover files linked into their native binary.
@@ -282,15 +331,23 @@ export function readNativeLinkedAppFontsWire(opts: {
282
331
  if (!resolveAppProject(opts.projectDir)) {
283
332
  throw new Error(`native font project is unavailable: ${opts.projectDir}`)
284
333
  }
285
- return encodeAppFontsWire(
286
- nativeLinkedFontSpecsFromPaths(
287
- projectRelativeFontPaths(opts.projectDir, discoverNativeLinkedAppFontPaths(opts)),
288
- (fontPath) => {
289
- const bytes = fs.readFileSync(path.resolve(opts.projectDir, fontPath))
290
- return `data:${fontContentType(fontPath)};base64,${bytes.toString('base64')}`
291
- },
292
- ),
334
+ const projectDir = fs.realpathSync(opts.projectDir)
335
+ const { paths, materialSymbols } = discoverNativeLinkedAppFontAssets(opts)
336
+ const specs = nativeLinkedFontSpecsFromPaths(
337
+ projectRelativeFontPaths(projectDir, paths),
338
+ nativeFontPlatform(opts.platform),
339
+ (fontPath) => {
340
+ const bytes = fs.readFileSync(path.resolve(projectDir, fontPath))
341
+ return `data:${fontContentType(fontPath)};base64,${bytes.toString('base64')}`
342
+ },
343
+ )
344
+ attachMaterialSymbolMetadata(
345
+ specs,
346
+ materialSymbols,
347
+ (filePath) =>
348
+ `data:application/octet-stream;base64,${fs.readFileSync(filePath).toString('base64')}`,
293
349
  )
350
+ return encodeAppFontsWire(specs)
294
351
  }
295
352
 
296
353
  // resolve a metro dev server's project root. Expo's middleware advertises it
@@ -355,28 +412,6 @@ export async function resolveMetroProjectRoot(bundleUrl: string): Promise<string
355
412
  return pid === null ? null : resolveProcessCwd(pid)
356
413
  }
357
414
 
358
- export function encodeAppFontsWire(specs: DiscoveredAppFontSpec[]): string {
359
- return specs
360
- .map((spec) => `${encodeURIComponent(spec.url)}::${encodeURIComponent(spec.family)}`)
361
- .join(',')
362
- }
363
-
364
- export function parseAppFontUrls(wire: string): string[] {
365
- const urls: string[] = []
366
- for (const entry of wire.split(',')) {
367
- const trimmed = entry.trim()
368
- if (!trimmed) continue
369
- const sep = trimmed.indexOf('::')
370
- const urlPart = sep === -1 ? trimmed : trimmed.slice(0, sep)
371
- try {
372
- urls.push(decodeURIComponent(urlPart))
373
- } catch {
374
- urls.push(urlPart)
375
- }
376
- }
377
- return urls.filter(Boolean)
378
- }
379
-
380
415
  // content type for a config-plugin font url, by extension. config fonts only
381
416
  // ever ship as ttf/otf/woff(2); fall back to octet-stream for anything else
382
417
  // so R2 still serves bytes the engine can register.
@@ -413,7 +448,7 @@ export async function fetchAppFontFiles(
413
448
  ): Promise<FetchedAppFont[]> {
414
449
  const out: FetchedAppFont[] = []
415
450
  const seen = new Set<string>()
416
- for (const url of parseAppFontUrls(wire)) {
451
+ for (const url of appFontAssetUrlsFromWire(wire)) {
417
452
  const urlhash = createHash('sha256').update(url).digest('hex')
418
453
  if (seen.has(urlhash)) continue
419
454
  seen.add(urlhash)