peachsim 0.1.1406 → 0.1.1465

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/LICENSE +8 -4
  2. package/README.md +22 -2
  3. package/cli/app-fonts.ts +50 -44
  4. package/cli/bin.ts +5 -4
  5. package/cli/bridge-diagnostics.ts +5 -0
  6. package/cli/bridge-flow-runner.ts +21 -13
  7. package/cli/cloud-client.ts +4 -3
  8. package/cli/commands/control.ts +155 -123
  9. package/cli/commands/daemon-mac-app.ts +3 -3
  10. package/cli/commands/daemon.ts +2 -2
  11. package/cli/commands/do-chain.ts +9 -2
  12. package/cli/commands/electron.ts +4 -1
  13. package/cli/commands/inspect/core.ts +1 -1
  14. package/cli/commands/inspect/settle.ts +19 -7
  15. package/cli/commands/inspect/settling.ts +92 -56
  16. package/cli/commands/inspect/wait-idle.ts +21 -9
  17. package/cli/commands/inspect/wait-ready.ts +22 -8
  18. package/cli/commands/inspect.ts +101 -134
  19. package/cli/commands/setup.ts +26 -16
  20. package/cli/commands/upgrade.ts +53 -9
  21. package/cli/desktop-companion.ts +1 -1
  22. package/cli/drivers/playwright-sim-host.ts +32 -13
  23. package/cli/drivers/playwright.ts +3 -3
  24. package/cli/main.ts +14 -5
  25. package/cli/outbound-endpoints.ts +1 -9
  26. package/detox/index.ts +6 -1
  27. package/dist-lib/agent-daemon-client.cjs +1 -1
  28. package/dist-lib/agent-events.cjs +1 -1
  29. package/dist-lib/attached-projects.cjs +1 -1
  30. package/dist-lib/auth/shared-session.cjs +1 -1
  31. package/dist-lib/backend-origin.cjs +1 -1
  32. package/dist-lib/beta.cjs +1 -1
  33. package/dist-lib/beta.mjs +1 -1
  34. package/dist-lib/bridge-constants.cjs +1 -1
  35. package/dist-lib/bridge-contract-input.cjs +1 -1
  36. package/dist-lib/bridge-contract-input.mjs +1 -1
  37. package/dist-lib/bridge-contract.cjs +3 -3
  38. package/dist-lib/bridge-contract.mjs +3 -3
  39. package/dist-lib/bundle-channel.cjs +322 -274
  40. package/dist-lib/capture-contract.cjs +1 -1
  41. package/dist-lib/capture-contract.mjs +1 -1
  42. package/dist-lib/cloud-contract.cjs +1 -1
  43. package/dist-lib/cloud-contract.mjs +1 -1
  44. package/dist-lib/cloud-test-args.cjs +1 -1
  45. package/dist-lib/cloud-test-args.mjs +1 -1
  46. package/dist-lib/cloud.cjs +1 -1
  47. package/dist-lib/cloud.mjs +1 -1
  48. package/dist-lib/config.cjs +1 -1
  49. package/dist-lib/config.mjs +1 -1
  50. package/dist-lib/detox/index.cjs +512 -122
  51. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  52. package/dist-lib/expo-swift-bundle.cjs +69 -33
  53. package/dist-lib/expo-swift-metadata.cjs +1 -1
  54. package/dist-lib/expo-swift-metadata.mjs +1 -1
  55. package/dist-lib/home-paths.cjs +1 -1
  56. package/dist-lib/host/bridge-host.cjs +3650 -3528
  57. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  58. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  59. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  60. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  61. package/dist-lib/host/websocket-proxy.cjs +1 -1
  62. package/dist-lib/index.cjs +327 -123
  63. package/dist-lib/jump-to-source-babel.cjs +1 -1
  64. package/dist-lib/jump-to-source-native.cjs +1 -1
  65. package/dist-lib/kotlin-host.cjs +10 -5
  66. package/dist-lib/menu.cjs +1 -1
  67. package/dist-lib/menu.mjs +1 -1
  68. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  69. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  70. package/dist-lib/metro-production-bundle.cjs +1 -1
  71. package/dist-lib/metro-production-bundle.mjs +1 -1
  72. package/dist-lib/metro.cjs +49 -47
  73. package/dist-lib/native-app-identity-project.cjs +1 -1
  74. package/dist-lib/native-app-identity-project.mjs +1 -1
  75. package/dist-lib/native-app-identity.cjs +1 -1
  76. package/dist-lib/native-app-identity.mjs +1 -1
  77. package/dist-lib/native-firebase-metadata.cjs +1 -1
  78. package/dist-lib/native-firebase-metadata.mjs +1 -1
  79. package/dist-lib/plist.cjs +1 -1
  80. package/dist-lib/plist.mjs +1 -1
  81. package/dist-lib/profiles.cjs +1 -1
  82. package/dist-lib/public-brand.cjs +1 -1
  83. package/dist-lib/react-native-host-modules.cjs +1 -1
  84. package/dist-lib/react-native-host-modules.mjs +1 -1
  85. package/dist-lib/scripts/dev-server-scanner.cjs +23 -17
  86. package/dist-lib/sdk.cjs +99 -61
  87. package/dist-lib/sdk.mjs +97 -61
  88. package/dist-lib/skills.cjs +3347 -3051
  89. package/dist-lib/swift-compile.cjs +26 -12
  90. package/dist-lib/swift-compile.mjs +26 -12
  91. package/dist-lib/swift-package.cjs +1 -1
  92. package/dist-lib/swift-package.mjs +1 -1
  93. package/dist-lib/swift-submit.cjs +2 -2
  94. package/dist-lib/swift-submit.mjs +2 -2
  95. package/dist-lib/swift-toolchain.cjs +1 -1
  96. package/dist-lib/swift.cjs +1 -1
  97. package/dist-lib/vite-swift.cjs +66 -31
  98. package/dist-lib/vite.cjs +1 -1
  99. package/dist-lib/worklet-executable.cjs +5 -1
  100. package/dist-lib/worklet-executable.mjs +5 -1
  101. package/kotlin/AppSources/android/view/Window.kt +2 -0
  102. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +1 -1
  103. package/kotlin/ComposeSources/PeachComposeHost.kt +35 -9
  104. package/kotlin/ExpoComposeSources/dev/peach/expo/PeachExpoCompose.kt +2 -2
  105. package/kotlin/ExpoSources/dev/peach/expo/PeachExpoRuntime.kt +9 -6
  106. package/kotlin/ExpoSources/dev/peach/expo/PeachExpoTypes.kt +4 -4
  107. package/kotlin/ExpoSources/expo/modules/kotlin/modules/Module.kt +2 -2
  108. package/kotlin/ExpoSources/expo/modules/kotlin/viewevent/ViewEvent.kt +22 -8
  109. package/kotlin/ExpoSources/expo/modules/kotlin/views/ExpoView.kt +0 -3
  110. package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewDefinitionBuilder.kt +10 -9
  111. package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewProps.kt +6 -5
  112. package/kotlin/fixtures/import-drawing/Drawing.d.kt.ts +7 -0
  113. package/kotlin/fixtures/import-drawing/Drawing.kt +101 -0
  114. package/kotlin/fixtures/import-drawing/ImportDrawing.native.tsx +18 -0
  115. package/kotlin/fixtures/import-drawing/README.md +34 -0
  116. package/kotlin/fixtures/import-drawing/app/_layout.tsx +4 -0
  117. package/kotlin/fixtures/import-drawing/app/index+spa.tsx +1 -0
  118. package/kotlin/fixtures/import-drawing/package.json +23 -0
  119. package/kotlin/fixtures/import-drawing/tsconfig.json +20 -0
  120. package/kotlin/fixtures/import-drawing/vite.config.ts +20 -0
  121. package/kotlin/fixtures/import-scroll/ImportNested.native.tsx +10 -0
  122. package/kotlin/fixtures/import-scroll/Nested.d.kt.ts +4 -0
  123. package/kotlin/fixtures/import-scroll/README.md +15 -0
  124. package/kotlin/fixtures/import-scroll/app/nested+spa.tsx +1 -0
  125. package/kotlin/fixtures/import-values/ImportValues.native.tsx +26 -0
  126. package/kotlin/fixtures/import-values/README.md +6 -1
  127. package/kotlin/fixtures/import-values/Values.d.kt.ts +4 -0
  128. package/kotlin/fixtures/import-values/Values.kt +18 -0
  129. package/kotlin/tools/expo-types.main.kts +17 -5
  130. package/package.json +2 -1
  131. package/scripts/dev-server-scanner.ts +33 -23
  132. package/src/assetCatalogContract.ts +7 -0
  133. package/src/bridge-contract.ts +21 -3
  134. package/src/connect.ts +18 -5
  135. package/src/desktop-host-delivery.ts +33 -3
  136. package/src/expo-swift-host.ts +6 -3
  137. package/src/host/bridge-host.ts +2 -0
  138. package/src/host/fetch-proxy-transport.ts +8 -0
  139. package/src/kotlin-host.ts +11 -5
  140. package/src/native-firebase-config.ts +95 -54
  141. package/src/process-output.ts +16 -0
  142. package/src/sdk.ts +6 -1
  143. package/src/sim-client.ts +6 -1
  144. package/src/swift-compile.ts +36 -15
  145. package/src/swift-distribution.lock.json +8 -8
  146. package/src/swift-standin-emit.ts +10 -1
  147. package/src/swift-standins.ts +21 -7
  148. package/src/swift-submit.ts +6 -1
  149. package/src/worklet-executable.ts +7 -2
  150. /package/kotlin/fixtures/{kotlin-app-variants → import-scroll}/Nested.kt +0 -0
@@ -1,8 +1,11 @@
1
1
  import { spawn, spawnSync } from 'node:child_process'
2
2
  import fs from 'node:fs'
3
+ import http from 'node:http'
4
+ import https from 'node:https'
3
5
  import { createRequire } from 'node:module'
4
6
  import os from 'node:os'
5
7
  import path from 'node:path'
8
+ import { bundleUrlForPlatform } from 'peach-engine/bundle-platform-url'
6
9
  import { encodeAppCatalogWire } from 'peach-engine/engine/app-catalog-wire'
7
10
  import {
8
11
  devices,
@@ -41,7 +44,10 @@ import {
41
44
  runtimeDir,
42
45
  } from '../../src/home-paths'
43
46
  import { applyHotMode } from '../../src/native-dev-bundle-url'
44
- import { readNativeFirebaseConfig } from '../../src/native-firebase-config'
47
+ import {
48
+ readExpoConfigAsync,
49
+ readNativeFirebaseConfig,
50
+ } from '../../src/native-firebase-config'
45
51
  import { ensureProfile } from '../../src/profiles'
46
52
  import { rnxPublicBrand } from '../../src/public-brand'
47
53
  import {
@@ -57,7 +63,6 @@ import { sleep } from '../../src/sleep'
57
63
  import { discoverAppCatalog } from '../app-catalog'
58
64
  import { loadRNXAppConfig } from '../app-config'
59
65
  import {
60
- type DiscoveredAppFontSpec,
61
66
  discoverNativeLinkedAppFonts,
62
67
  encodeAppFontsWire,
63
68
  resolveMetroProjectRoot,
@@ -68,6 +73,7 @@ import {
68
73
  type ResolvedAppProject,
69
74
  } from '../app-project'
70
75
  import {
76
+ type DiscoveredAppSplashSpec,
71
77
  discoverConfigPluginAppSplash,
72
78
  discoverOneNativeAppSplash,
73
79
  encodeAppSplashWire,
@@ -560,6 +566,22 @@ async function resolveConnectionInputForCli(target: string): Promise<ResolvedDev
560
566
  )
561
567
  }
562
568
 
569
+ // a dev server builds an app's graph on the first request for its bundle and
570
+ // finishes the build after that request goes away. `peach open` asks as soon as
571
+ // the bundle is resolved, so the build overlaps bridge, host and runtime boot;
572
+ // the engine's own request for the same url then joins it, so the cli drops its
573
+ // copy unread. nothing the open stamps asks the dev server, so a busy build
574
+ // cannot change the page. the socket is unreferenced: a pending build never
575
+ // holds the command open.
576
+ function startDevServerBuild(bundleUrl: string): void {
577
+ const request = (bundleUrl.startsWith('https:') ? https : http).get(
578
+ bundleUrl,
579
+ (response) => response.resume(),
580
+ )
581
+ request.on('socket', (socket) => socket.unref())
582
+ request.on('error', () => {})
583
+ }
584
+
563
585
  function getShellRoutePrefix(pathname: string): string {
564
586
  const normalized = pathname.replace(/\/+$/, '') || '/'
565
587
  if (normalized === '/__peachsim' || normalized.startsWith('/__peachsim/')) {
@@ -743,18 +765,9 @@ export function resolveShellBaseUrlForBridgePort(bridgePort: number): string {
743
765
  // loaded by JS already flow through normal runtime fetch capture; this covers
744
766
  // UIAppFonts-style assets that Metro can serve but the guest bundle never
745
767
  // fetches itself. auto-discovery only — scans the repo for expo-font config
746
- // plugin declarations, no user-facing flag.
747
- //
748
- // each discovered font is HEAD-checked against the metro host before we
749
- // include it on the URL. fonts whose metro asset URL 404s (typo in the
750
- // expo-font plugin config, missing file, or a wrong app.config picked up
751
- // by the scan) are dropped silently so the engine never tries to fetch a
752
- // known-bad URL at boot — that would just log a registration failure and
753
- // confuse anyone reading the page console.
754
- async function stampAppFonts(
755
- urlStr: string,
756
- project?: ResolvedAppProject | null,
757
- ): Promise<string> {
768
+ // plugin declarations, no user-facing flag. discovery keeps only font files
769
+ // that exist on disk, so the stamp never waits on the dev server.
770
+ function stampAppFonts(urlStr: string, project?: ResolvedAppProject | null): string {
758
771
  try {
759
772
  const url = new URL(urlStr)
760
773
  const bundleUrl = url.searchParams.get('bundle') || ''
@@ -764,48 +777,13 @@ async function stampAppFonts(
764
777
  projectDir: project.appDir,
765
778
  })
766
779
  if (specs.length === 0) return urlStr
767
- const reachable = await filterReachableAppFonts(specs)
768
- if (reachable.length === 0) return urlStr
769
- url.searchParams.set('appFonts', encodeAppFontsWire(reachable))
780
+ url.searchParams.set('appFonts', encodeAppFontsWire(specs))
770
781
  return url.toString()
771
782
  } catch {
772
783
  return urlStr
773
784
  }
774
785
  }
775
786
 
776
- // HEAD-check each font URL with a short timeout. metro returns 200 (or
777
- // sometimes a redirect) for assets it can resolve and 500 with an ENOENT
778
- // trace for paths it cannot. anything not in the 2xx-3xx range gets dropped.
779
- async function filterReachableAppFonts(
780
- specs: DiscoveredAppFontSpec[],
781
- ): Promise<DiscoveredAppFontSpec[]> {
782
- const results = await Promise.all(
783
- specs.map(async (spec): Promise<DiscoveredAppFontSpec | null> => {
784
- const controller = new AbortController()
785
- const timer = setTimeout(() => controller.abort(), 1500)
786
- try {
787
- const res = await fetch(spec.url, {
788
- method: 'HEAD',
789
- signal: controller.signal,
790
- })
791
- if (res.status >= 200 && res.status < 400) return spec
792
- return null
793
- } catch {
794
- return null
795
- } finally {
796
- clearTimeout(timer)
797
- }
798
- }),
799
- )
800
- // a font offered from more than one candidate metro root keeps the first
801
- // url metro served.
802
- const reachable = new Map<string, DiscoveredAppFontSpec>()
803
- for (const spec of results) {
804
- if (spec !== null && !reachable.has(spec.family)) reachable.set(spec.family, spec)
805
- }
806
- return [...reachable.values()]
807
- }
808
-
809
787
  async function resolveShellUrl(
810
788
  target: string,
811
789
  baseUrl: string,
@@ -856,6 +834,7 @@ async function resolveShellUrl(
856
834
  async function stampAppSplash(
857
835
  urlStr: string,
858
836
  project?: ResolvedAppProject | null,
837
+ oneAppSplash?: Promise<DiscoveredAppSplashSpec | null>,
859
838
  ): Promise<string> {
860
839
  try {
861
840
  const url = new URL(urlStr)
@@ -866,11 +845,11 @@ async function stampAppSplash(
866
845
  bundleUrl,
867
846
  projectDir: project.appDir,
868
847
  })
869
- : await discoverOneNativeAppSplash(bundleUrl)
848
+ : await (oneAppSplash ?? discoverOneNativeAppSplash(bundleUrl))
870
849
  if (!spec) return urlStr
871
850
  // native splash assets are required for the first frame.
872
851
  for (const imageUrl of splashImageUrls(spec)) {
873
- if (!(await splashImageReachable(imageUrl))) return urlStr
852
+ if (!splashImageOnDisk(imageUrl, project.appDir)) return urlStr
874
853
  }
875
854
  url.searchParams.set('appSplash', encodeAppSplashWire(spec))
876
855
  return url.toString()
@@ -897,16 +876,18 @@ function stampAppCatalog(urlStr: string, project?: ResolvedAppProject | null): s
897
876
  }
898
877
  }
899
878
 
900
- async function splashImageReachable(imageUrl: string): Promise<boolean> {
901
- const controller = new AbortController()
902
- const timer = setTimeout(() => controller.abort(), 1500)
879
+ // a splash spec names `<origin>/assets/<project-relative-path>`, the file the
880
+ // dev server serves from its project root, so the file is checked on disk and
881
+ // the stamp never waits on a dev server busy building.
882
+ function splashImageOnDisk(imageUrl: string, appDir: string): boolean {
883
+ const { pathname } = new URL(imageUrl)
884
+ if (!pathname.startsWith('/assets/')) return false
903
885
  try {
904
- const res = await fetch(imageUrl, { method: 'HEAD', signal: controller.signal })
905
- return res.status >= 200 && res.status < 400
886
+ return fs
887
+ .statSync(path.join(appDir, decodeURIComponent(pathname.slice('/assets/'.length))))
888
+ .isFile()
906
889
  } catch {
907
890
  return false
908
- } finally {
909
- clearTimeout(timer)
910
891
  }
911
892
  }
912
893
 
@@ -947,6 +928,10 @@ export interface BuildShellUrlOptions {
947
928
  theme?: string
948
929
  resolvedBundle?: ResolvedDevBundle
949
930
  project?: ResolvedAppProject | null
931
+ // a One app's splash, read from its dev server's manifest. `peach open`
932
+ // starts the read beside its early bundle request so the answer overlaps
933
+ // bridge and host setup instead of preceding the launch.
934
+ oneAppSplash?: Promise<DiscoveredAppSplashSpec | null>
950
935
  // deterministic-capture mode, stamped onto the engine URL as ?proof=1. the
951
936
  // shell worker, compositor worker and tenant worker each read it off the
952
937
  // host page URL at boot (syncProofModeFromInitUrl) and pin everything that
@@ -975,20 +960,44 @@ export interface BuildShellUrlOptions {
975
960
  }
976
961
 
977
962
  function readProjectNativeFirebaseConfig(
978
- project: ResolvedAppProject | null | undefined,
979
- ): Record<string, unknown> | null {
980
- if (!project) return null
963
+ project: ResolvedAppProject,
964
+ ): Promise<Record<string, unknown> | null> {
981
965
  const require = createRequire(path.join(project.appDir, 'package.json'))
982
966
  try {
983
967
  require.resolve('@expo/config')
984
968
  } catch {
985
- return null
986
- }
987
- try {
988
- return readNativeFirebaseConfig(project.appDir)
989
- } catch (error) {
990
- return { error: error instanceof Error ? error.message : String(error) }
969
+ return Promise.resolve(null)
991
970
  }
971
+ return readExpoConfigAsync(project.appDir)
972
+ .then((config) =>
973
+ readNativeFirebaseConfig(project.appDir, undefined, undefined, config),
974
+ )
975
+ .catch((error: unknown) => ({
976
+ error: error instanceof Error ? error.message : String(error),
977
+ }))
978
+ }
979
+
980
+ // evaluating the Expo config spawns node, so `peach open` starts the read as
981
+ // soon as the project resolves and the shell url it builds takes that read. a
982
+ // later build reads the config again.
983
+ const startedNativeFirebaseConfigs = new Map<
984
+ string,
985
+ Promise<Record<string, unknown> | null>
986
+ >()
987
+
988
+ function startNativeFirebaseConfigRead(project: ResolvedAppProject | null) {
989
+ if (project)
990
+ startedNativeFirebaseConfigs.set(
991
+ project.appDir,
992
+ readProjectNativeFirebaseConfig(project),
993
+ )
994
+ }
995
+
996
+ function takeNativeFirebaseConfig(project: ResolvedAppProject | null | undefined) {
997
+ if (!project) return Promise.resolve(null)
998
+ const started = startedNativeFirebaseConfigs.get(project.appDir)
999
+ startedNativeFirebaseConfigs.delete(project.appDir)
1000
+ return started ?? readProjectNativeFirebaseConfig(project)
992
1001
  }
993
1002
 
994
1003
  function bundleUrlOf(urlStr: string): string | undefined {
@@ -1014,7 +1023,7 @@ export async function buildShellUrl(
1014
1023
  opts.project ?? (await resolveAppProjectForBundleUrl(bundleUrlOf(resolved)))
1015
1024
  const expoRoot = project?.appDir ?? null
1016
1025
  const stamped = stampAppCatalog(
1017
- await stampAppSplash(await stampAppFonts(resolved, project), project),
1026
+ await stampAppSplash(stampAppFonts(resolved, project), project, opts.oneAppSplash),
1018
1027
  project,
1019
1028
  )
1020
1029
  const expoStamped = new URL(stamped)
@@ -1046,7 +1055,7 @@ export async function buildShellUrl(
1046
1055
  // own `@expo/config` (a dependency-free recorder) leaves it to the Peach
1047
1056
  // Metro plugin's endpoint.
1048
1057
  expoStamped.searchParams.delete('nativeFirebase')
1049
- const nativeFirebase = readProjectNativeFirebaseConfig(project)
1058
+ const nativeFirebase = await takeNativeFirebaseConfig(project)
1050
1059
  if (nativeFirebase && expoStamped.searchParams.get('bundle'))
1051
1060
  expoStamped.searchParams.set('nativeFirebase', JSON.stringify(nativeFirebase))
1052
1061
  if (opts.bundleOriginOverride)
@@ -1129,13 +1138,8 @@ function deriveCurrentSimBaseUrl(
1129
1138
  }
1130
1139
  }
1131
1140
 
1132
- export async function buildOpenUrl(
1133
- target: string,
1134
- baseUrl: string,
1135
- openToken: string,
1136
- opts: BuildShellUrlOptions = {},
1137
- ): Promise<string> {
1138
- const url = new URL(await buildShellUrl(target, baseUrl, opts))
1141
+ function withOpenToken(shellUrl: string, openToken: string): string {
1142
+ const url = new URL(shellUrl)
1139
1143
  url.searchParams.set('inspectOpen', openToken)
1140
1144
  return url.toString()
1141
1145
  }
@@ -1788,50 +1792,7 @@ export async function runOpenCommand(
1788
1792
  resolvedBundle?.bundleUrl,
1789
1793
  projectRoot,
1790
1794
  )
1791
- const appConfig = await loadRNXAppConfig(project)
1792
- const configuredRuntimeVersion = appConfig?.runtimeVersion
1793
- const runtimeConfig = mergePeachConfig(
1794
- mergePeachConfig(
1795
- appConfig ? { ...appConfig, runtimeVersion: undefined } : undefined,
1796
- opts.runtimeConfig,
1797
- ),
1798
- commandConfig,
1799
- )
1800
- if (configuredRuntimeVersion) {
1801
- await ensureConfiguredRuntime(configuredRuntimeVersion)
1802
- }
1803
-
1804
- // keep the bridge port and the shell base URL in one world: --base-url
1805
- // retargets polling at THAT world's bridge (otherwise open builds the page
1806
- // against one runtime while polling the other bridge for registration and
1807
- // times out after ~125s), and a pin nothing serves is refused rather than
1808
- // falling back to the discovered default, which used to send
1809
- // ensureBridgeForOpen below off to start a second shell.
1810
- const explicitBaseUrl = args.find((_, i) => args[i - 1] === '--base-url')
1811
- if (explicitBaseUrl) {
1812
- const pinned = resolveBridgePortForPin({
1813
- baseUrl: explicitBaseUrl,
1814
- wsPort: parsed.wsPort,
1815
- explicitPort: parsed.explicitPort,
1816
- })
1817
- if (pinned.error !== undefined) {
1818
- console.error(` error: ${pinned.error}`)
1819
- peachExit(1)
1820
- } else {
1821
- parsed.wsPort = pinned.port
1822
- }
1823
- }
1824
-
1825
- opts.signal?.throwIfAborted()
1826
- parsed.wsPort = await ensureBridgeForOpen(
1827
- parsed.wsPort,
1828
- parsed.explicitPort,
1829
- opts.resources,
1830
- explicitBaseUrl && !configuredRuntimeVersion ? 'external' : 'bridge',
1831
- )
1832
- if (opts.resources) opts.resources.port = parsed.wsPort
1833
- opts.signal?.throwIfAborted()
1834
-
1795
+ startNativeFirebaseConfigRead(project)
1835
1796
  // playwright is the browser-sim launcher. it owns an isolated profile and
1836
1797
  // can reap exactly its own browser tree. desktop opens the native host and
1837
1798
  // electron the Electron companion. none opens the user's browser profile.
@@ -1892,6 +1853,65 @@ export async function runOpenCommand(
1892
1853
  peachExit(1)
1893
1854
  }
1894
1855
 
1856
+ // asked before the bundle so the dev server answers it ahead of the build
1857
+ const oneAppSplash =
1858
+ resolvedBundle && project && !findExpoAppConfig(project.appDir)
1859
+ ? discoverOneNativeAppSplash(resolvedBundle.bundleUrl)
1860
+ : undefined
1861
+ if (resolvedBundle) {
1862
+ // the engine fetches this url for its device's platform
1863
+ const bundleUrl = applyHotMode(resolvedBundle.bundleUrl, hotMode)
1864
+ startDevServerBuild(
1865
+ deviceArg && isDeviceModel(deviceArg)
1866
+ ? bundleUrlForPlatform(bundleUrl, getRuntimePlatform(deviceArg), () => bundleUrl)
1867
+ : bundleUrl,
1868
+ )
1869
+ }
1870
+
1871
+ const appConfig = await loadRNXAppConfig(project)
1872
+ const configuredRuntimeVersion = appConfig?.runtimeVersion
1873
+ const runtimeConfig = mergePeachConfig(
1874
+ mergePeachConfig(
1875
+ appConfig ? { ...appConfig, runtimeVersion: undefined } : undefined,
1876
+ opts.runtimeConfig,
1877
+ ),
1878
+ commandConfig,
1879
+ )
1880
+ if (configuredRuntimeVersion) {
1881
+ await ensureConfiguredRuntime(configuredRuntimeVersion)
1882
+ }
1883
+
1884
+ // keep the bridge port and the shell base URL in one world: --base-url
1885
+ // retargets polling at THAT world's bridge (otherwise open builds the page
1886
+ // against one runtime while polling the other bridge for registration and
1887
+ // times out after ~125s), and a pin nothing serves is refused rather than
1888
+ // falling back to the discovered default, which used to send
1889
+ // ensureBridgeForOpen below off to start a second shell.
1890
+ const explicitBaseUrl = args.find((_, i) => args[i - 1] === '--base-url')
1891
+ if (explicitBaseUrl) {
1892
+ const pinned = resolveBridgePortForPin({
1893
+ baseUrl: explicitBaseUrl,
1894
+ wsPort: parsed.wsPort,
1895
+ explicitPort: parsed.explicitPort,
1896
+ })
1897
+ if (pinned.error !== undefined) {
1898
+ console.error(` error: ${pinned.error}`)
1899
+ peachExit(1)
1900
+ } else {
1901
+ parsed.wsPort = pinned.port
1902
+ }
1903
+ }
1904
+
1905
+ opts.signal?.throwIfAborted()
1906
+ parsed.wsPort = await ensureBridgeForOpen(
1907
+ parsed.wsPort,
1908
+ parsed.explicitPort,
1909
+ opts.resources,
1910
+ explicitBaseUrl && !configuredRuntimeVersion ? 'external' : 'bridge',
1911
+ )
1912
+ if (opts.resources) opts.resources.port = parsed.wsPort
1913
+ opts.signal?.throwIfAborted()
1914
+
1895
1915
  // --posture <open|closed> seeds the fold posture for the opened sim,
1896
1916
  // stamped on the engine URL next to ?device= so it round-trips instead of
1897
1917
  // being swallowed into the bundle param. only foldable devices (iphone-duo)
@@ -2002,6 +2022,7 @@ export async function runOpenCommand(
2002
2022
  theme: themeArg,
2003
2023
  resolvedBundle,
2004
2024
  project,
2025
+ oneAppSplash,
2005
2026
  proof: args.includes('--proof'),
2006
2027
  ephemeral: ephemeralProfile,
2007
2028
  env: envArgs.length ? envArgs : undefined,
@@ -2029,6 +2050,9 @@ export async function runOpenCommand(
2029
2050
  const simMatchesToken = (sim: BridgeSimInfo) =>
2030
2051
  sim.url?.includes(`inspectOpen=${token}`) ?? false
2031
2052
 
2053
+ // the reuse check builds the page url a new sim loads; building it once
2054
+ // keeps the project's native config read, a node spawn, to one per open
2055
+ let newSimShellUrl: string | null = null
2032
2056
  if (shouldReuseCurrentSim) {
2033
2057
  const bridge = createBridgeFromParsed(parsed)
2034
2058
  const simHint = parsed.simId ? ` --sim ${parsed.simId}` : ''
@@ -2060,7 +2084,7 @@ export async function runOpenCommand(
2060
2084
  }
2061
2085
  }
2062
2086
  if (!targetSim && parsed.simIdSource !== 'flag') {
2063
- targetShellUrl = await buildShellUrl(
2087
+ targetShellUrl = newSimShellUrl = await buildShellUrl(
2064
2088
  target,
2065
2089
  applyPeachConfigToUrl(baseUrl, runtimeConfig),
2066
2090
  buildShellUrlOpts,
@@ -2105,6 +2129,11 @@ export async function runOpenCommand(
2105
2129
  buildShellUrlOpts,
2106
2130
  ))
2107
2131
  const navigationUrl = new URL(navigatedShellUrl)
2132
+ const currentEphemeral = new URL(
2133
+ deriveCurrentSimBaseUrl(targetSim, baseUrl),
2134
+ ).searchParams.get('ephemeral')
2135
+ if (currentEphemeral) navigationUrl.searchParams.set('ephemeral', currentEphemeral)
2136
+ navigatedShellUrl = navigationUrl.toString()
2108
2137
  navigationUrl.searchParams.set('inspectOpen', token)
2109
2138
  bridge
2110
2139
  .send({
@@ -2184,7 +2213,10 @@ export async function runOpenCommand(
2184
2213
  }
2185
2214
 
2186
2215
  const configuredBaseUrl = applyPeachConfigToUrl(baseUrl, runtimeConfig)
2187
- const openUrl = await buildOpenUrl(target, configuredBaseUrl, token, buildShellUrlOpts)
2216
+ const openUrl = withOpenToken(
2217
+ newSimShellUrl ?? (await buildShellUrl(target, configuredBaseUrl, buildShellUrlOpts)),
2218
+ token,
2219
+ )
2188
2220
  const ownerPid = opts.ownerPid ?? getStableOwnerPid()
2189
2221
  if (!ownerPid) {
2190
2222
  console.error(
@@ -10,9 +10,9 @@
10
10
  // bundle Info.plist instead, so the entry reads "Peach Daemon".
11
11
  //
12
12
  // the bundle is regenerated every time `daemonInstall` runs — it's tiny
13
- // (a plist + a shell launcher) and the launcher embeds a snapshot of the
14
- // invocation, so `peach upgrade` followed by `peach daemon install --force` is
15
- // the documented upgrade path. there's no notarization
13
+ // (a plist + a shell launcher) and the launcher execs the executable path
14
+ // that installed it, which `peach upgrade` replaces in place before restarting
15
+ // the daemon, so an upgrade needs no reinstall. there's no notarization
16
16
  // or Developer ID signing here yet; ad-hoc + hardened runtime is enough to
17
17
  // fix the attribution, and a real signed bundle (shipped via the runtime
18
18
  // CDN) is a follow-up.
@@ -2,8 +2,8 @@
2
2
  //
3
3
  // wraps `peach serve` so it starts automatically on login and restarts if
4
4
  // it crashes. the agent points at the executable that ran `daemon install`,
5
- // so `peach upgrade` (which replaces that file in place) is picked up by
6
- // `peach daemon restart` without touching the plist/unit file.
5
+ // and `peach upgrade` replaces that file in place and then restarts a running
6
+ // daemon itself, so the plist/unit file never needs rewriting.
7
7
  //
8
8
  // `daemonInstall` is shared by `peach setup` and the explicit
9
9
  // `peach daemon install` machine-bootstrap path.
@@ -1,4 +1,5 @@
1
1
  import { BridgeInputError } from '../../src/bridge-contract-input'
2
+ import { IdleObservationError } from './inspect/settling'
2
3
  import type { PerformResult } from '../../src/bridge-contract'
3
4
  import type { SimPerformStep } from '../../src/sim-client'
4
5
 
@@ -342,7 +343,7 @@ export async function runDoChain(
342
343
  let sim: Awaited<ReturnType<typeof connect>> | null = null
343
344
  try {
344
345
  sim = await connect({
345
- port: parsed.wsPort,
346
+ port: parsed.explicitPort ? parsed.wsPort : undefined,
346
347
  sim: parsed.simIdSource === 'flag' ? parsed.simId : undefined,
347
348
  timeoutMs: parsed.commandTimeoutMs,
348
349
  })
@@ -436,7 +437,13 @@ export async function runDoChain(
436
437
  ` ⚠ auto-wait timed out after ${settled.elapsed ?? budgetMs}ms (${settled.blockedBy}). next command may see mid-animation state. use \`peach do settle\` for a longer wait.\n`,
437
438
  )
438
439
  }
439
- } catch {}
440
+ } catch (error) {
441
+ if (error instanceof IdleObservationError) {
442
+ process.stderr.write(
443
+ ` ⚠ ${error.message}. next command may see mid-animation state. use \`peach do settle\` to retry.\n`,
444
+ )
445
+ }
446
+ }
440
447
  }
441
448
 
442
449
  if (wantsJson) {
@@ -90,7 +90,10 @@ examples:
90
90
  if (!install) {
91
91
  printMissingCompanionHint(native)
92
92
  const canPrompt =
93
- process.stdin.isTTY && process.env.CI !== '1' && process.env.PEACH_NO_PROMPT !== '1'
93
+ process.platform !== 'darwin' &&
94
+ process.stdin.isTTY &&
95
+ process.env.CI !== '1' &&
96
+ process.env.PEACH_NO_PROMPT !== '1'
94
97
  if (canPrompt && (await confirm('run peach desktop install now?', true))) {
95
98
  console.log()
96
99
  const { runInstallDesktop } = await import('./install-desktop')
@@ -2236,7 +2236,7 @@ export function formatTimelinePayload(
2236
2236
  case 'notification':
2237
2237
  return `${d.title ?? ''}${d.body ? ` — ${d.body}` : ''}`
2238
2238
  case 'fetch':
2239
- return `${d.method ?? 'GET'} ${d.url ?? ''}${d.status ? ` -> ${d.status}` : ''}`
2239
+ return `${d.method ?? 'GET'} ${d.url ?? ''}${d.status ? ` -> ${d.status}` : ''}${d.error ? ` -> ${d.error}` : ''}`
2240
2240
  case 'console':
2241
2241
  case 'console-log':
2242
2242
  return `${d.level ?? 'log'}: ${(d.message ?? '').toString().slice(0, 120)}`
@@ -1,4 +1,5 @@
1
- import { waitForSootsimIdle } from './settling'
1
+ import { rnxExit } from '../../run-rnx'
2
+ import { IdleObservationError, waitForSootsimIdle } from './settling'
2
3
  import type { WsBridge } from '../../ws-bridge'
3
4
 
4
5
  // default: worker stillness plus mounted-root membership; budgets of a
@@ -24,11 +25,22 @@ export async function runSettleSubcommand(opts: {
24
25
  return
25
26
  }
26
27
  const strict = args.includes('--strict')
27
- const { elapsed, settled } = await waitForSootsimIdle({ bridge, maxMs, strict })
28
-
29
- if (settled) {
30
- console.log(` settled in ${elapsed}ms`)
31
- } else {
32
- console.log(` timed out after ${elapsed}ms (may still be animating)`)
28
+ try {
29
+ const { elapsed, settled, blockedBy } = await waitForSootsimIdle({
30
+ bridge,
31
+ maxMs,
32
+ strict,
33
+ })
34
+ if (settled) {
35
+ console.log(` settled in ${elapsed}ms`)
36
+ } else {
37
+ console.log(` timed out after ${elapsed}ms (${blockedBy})`)
38
+ }
39
+ } catch (error) {
40
+ if (error instanceof IdleObservationError) {
41
+ console.error(` ⚠ ${error.message}`)
42
+ rnxExit(1)
43
+ }
44
+ throw error
33
45
  }
34
46
  }