peachsim 0.1.1404 → 0.1.1463

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 (149) 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 +22 -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 +843 -46
  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 +2959 -2780
  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 +1 -1
  100. package/dist-lib/worklet-executable.mjs +1 -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/kotlin/fixtures/{kotlin-app-variants → import-scroll}/Nested.kt +0 -0
package/LICENSE CHANGED
@@ -5,10 +5,14 @@ Copyright (c) Tamagui LLC. All rights reserved.
5
5
  This package (peachsim) is proprietary software of Tamagui LLC, licensed under
6
6
  the Peach Software License Agreement: https://peachsim.com/legal/license
7
7
 
8
- In summary (the agreement governs): personal, non-commercial use is free;
9
- commercial use requires an active Pro subscription after a single 30-day
10
- evaluation period; embedding or redistributing the software requires a
11
- separate written agreement with Tamagui LLC.
8
+ In summary (the agreement governs): Seed is free for individuals and companies
9
+ under 10 people and under $1M yearly revenue, including commercial desktop and
10
+ local use. A company at or above either limit needs Team while it has under
11
+ 100 people and under $10M yearly revenue, or Enterprise at or above either of
12
+ those limits. Members, agents, API keys and local simulators are unlimited.
13
+ Company size is self-reported and enforced through the license terms only.
14
+ Embedding or redistributing the software requires a separate written agreement
15
+ with Tamagui LLC.
12
16
 
13
17
  Versions of this package published before this license change remain under
14
18
  the license they were published with.
package/README.md CHANGED
@@ -11,7 +11,8 @@ installation. The `peachsim` npm package contains bundler plugins, testing
11
11
  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
- The executable updates itself with `peach upgrade`. Successful interactive human
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
16
  commands refresh a cached stable CLI version in a detached process at most once
16
17
  every 20 hours. A newer version produces one short notice on stderr, once per
17
18
  version and no more than weekly. CI, agents, JSON output, pipes, and
@@ -151,7 +152,8 @@ graph TD
151
152
  **The CLI process** (`cli/bin.ts` routes, `cli/main.ts` parses argv) lazy-imports
152
153
  exactly one command module per run. Bare `peach` opens ConnectRN, whose simulator UI discovers
153
154
  running React Native development servers. The first human run offers the
154
- recommended background service and optional Desktop app, then opens the simulator
155
+ recommended background service and simulator window; on Mac, Desktop uses an
156
+ already installed app. It then opens the simulator
155
157
  before asking about private diagnostics or an optional repository install. `peach open`
156
158
  ensures the engine runtime exists when the bridge supplies the shell and starts
157
159
  a local foreground bridge when no daemon is reachable, then opens a `WsBridge`
@@ -173,6 +175,16 @@ browser launch inside a Team Machine session and prints the URL plus
173
175
  `--driver playwright --headless` guidance. Interactive human launches keep
174
176
  their system browser behavior.
175
177
 
178
+ For apps using `react-native-config`, open with `peach open <app-port> --load-env`.
179
+ This reads the local project's native dotenv values for `RNCConfigModule`;
180
+ `runtimeConfig.env` populates the guest's `process.env` independently. Starting a
181
+ sidecar or supplying process environment variables does not select the app's
182
+ native API roots. Verify the mounted app's `Config` values and captured request
183
+ destinations before driving a network flow. Without `--load-env`, the native
184
+ config dictionary is empty and the app can select its own public defaults.
185
+ Preserve failures from those endpoints rather than remapping them to a successful
186
+ local response. The same flag is available on `peach flow`.
187
+
176
188
  **The daemon is one process** — `SootSimBridgeHost` (`src/host/bridge-host.ts`).
177
189
  `peach serve` runs it in the foreground; the background daemon is the *same
178
190
  process* wrapped with autostart and the `~/.peach/daemon.json` lockfile. It is
@@ -194,6 +206,9 @@ shell's bridge world without reading the other process's lockfile directory.
194
206
  An explicit `--port` also participates in `--sim` ownership discovery even
195
207
  without a local heartbeat record. The bridge must report that simulator as
196
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.
197
212
 
198
213
  **The sim** is a browser/Electron *page* launched by a CLI driver. Inside that
199
214
  page run three workers: the **shell worker** owns device and native state, the
@@ -278,6 +293,11 @@ library path, so no host needs `apt-get` or root. The bundle is pinned in
278
293
  `cli/drivers/linux-browser-libs.lock.json`; `bun scripts/linux-browser-libs.ts
279
294
  resolve` re-pins it, and the CLI release publishes any bundle the bucket lacks.
280
295
 
296
+ On Mac, headless launches attach an installed compatible native host. With no
297
+ host, they render in the browser without downloading or placing the app. The
298
+ browser logo menu offers the app; `peach desktop install --native` remains the
299
+ explicit CLI installer. Linux still acquires its standalone host under `~/.peach`.
300
+
281
301
  Headed launches on Linux use full Chromium, whose extra libraries the bundle
282
302
  also carries. Later opens reuse that sim and
283
303
  reload it at the requested target. Use `peach do reload` when the target has not
package/cli/app-fonts.ts CHANGED
@@ -22,6 +22,11 @@ export type DiscoveredAppFontSpec = ConfigPluginFontSpec
22
22
 
23
23
  type ExpoConfigGetter = (dir: string, opts: Record<string, unknown>) => { exp?: unknown }
24
24
 
25
+ // every font file is found on disk, so the asset urls are decided here and
26
+ // never by asking the dev server: metro's asset endpoint resolves a path
27
+ // against its config.projectRoot, which is the root it advertises and the one
28
+ // projectDir names. a reachability request would make the result depend on how
29
+ // busy the dev server is, and a cold build answers late enough to drop fonts.
25
30
  export function discoverNativeLinkedAppFonts(opts: {
26
31
  bundleUrl: string
27
32
  projectDir: string
@@ -29,13 +34,35 @@ export function discoverNativeLinkedAppFonts(opts: {
29
34
  }): DiscoveredAppFontSpec[] {
30
35
  const metroOrigin = loopbackOriginOf(opts.bundleUrl)
31
36
  if (!metroOrigin) return []
32
- return metroFontSpecsFromPaths(discoverNativeLinkedAppFontPaths(opts), metroOrigin)
37
+ const specs = metroFontSpecsFromPaths(
38
+ projectRelativeFontPaths(opts.projectDir, discoverNativeLinkedAppFontPaths(opts)),
39
+ metroOrigin,
40
+ )
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()]
46
+ }
47
+
48
+ function projectRelativeFontPaths(projectDir: string, fontPaths: string[]): string[] {
49
+ return fontPaths.map((fontPath) =>
50
+ path.relative(path.resolve(projectDir), fontPath).split(path.sep).join('/'),
51
+ )
33
52
  }
34
53
 
54
+ function isFile(filePath: string): boolean {
55
+ try {
56
+ return fs.statSync(filePath).isFile()
57
+ } catch {
58
+ return false
59
+ }
60
+ }
61
+
62
+ // absolute paths of every existing font file the native app links.
35
63
  function discoverNativeLinkedAppFontPaths(opts: {
36
64
  projectDir: string
37
65
  platform?: string
38
- bundleUrl?: string
39
66
  }): string[] {
40
67
  const project = resolveAppProject(opts.projectDir)
41
68
  const selectedAppDir = project?.appDir ?? null
@@ -69,10 +96,14 @@ function discoverNativeLinkedAppFontPaths(opts: {
69
96
  } catch {}
70
97
  }
71
98
 
99
+ // a declared path with no file behind it links nothing natively.
72
100
  const fontPaths = expoFontPathsFromPlugins(
73
101
  plugins,
74
102
  platform === 'android' ? 'android' : 'ios',
75
- ).slice(0, 256)
103
+ )
104
+ .slice(0, 256)
105
+ .map((fontPath) => path.resolve(selectedAppDir, fontPath))
106
+ .filter(isFile)
76
107
  // the walk is bounded by WORK, not by a clock: at most 64 readdirs, at most 64
77
108
  // queued dirs, 4 levels deep, 256 paths. those ceilings are what keep a stray
78
109
  // node_modules from being crawled, and no machine can blow through them. a
@@ -81,7 +112,7 @@ function discoverNativeLinkedAppFontPaths(opts: {
81
112
  // and no error at all.
82
113
  let scannedFontDirectories = 0
83
114
 
84
- const collectFontFiles = (root: string, metroPathPrefix: string): void => {
115
+ const collectFontFiles = (root: string): void => {
85
116
  const pending: Array<{ dir: string; depth: number }> = [{ dir: root, depth: 0 }]
86
117
  let nextIndex = 0
87
118
  while (
@@ -108,8 +139,7 @@ function discoverNativeLinkedAppFontPaths(opts: {
108
139
  continue
109
140
  }
110
141
  if (!entry.isFile() || !/\.(ttf|otf|woff|woff2)$/i.test(entry.name)) continue
111
- const relative = path.relative(root, fullPath).split(path.sep).join('/')
112
- fontPaths.push(path.posix.join(metroPathPrefix, relative))
142
+ fontPaths.push(fullPath)
113
143
  if (fontPaths.length >= 256) break
114
144
  }
115
145
  }
@@ -145,18 +175,7 @@ function discoverNativeLinkedAppFontPaths(opts: {
145
175
  const root = path.resolve(selectedAppDir, configuredDir)
146
176
  const relativeRoot = path.relative(selectedAppDir, root)
147
177
  if (!relativeRoot.startsWith('..') && !path.isAbsolute(relativeRoot)) {
148
- collectFontFiles(root, relativeRoot.split(path.sep).join('/'))
149
- }
150
-
151
- // the bundle path is the entry relative to metro's root, so a bundle at
152
- // /apps/app/index.bundle served for /repo/apps/app puts that root at /repo.
153
- const bundleDir = path.posix
154
- .dirname(opts.bundleUrl ? new URL(opts.bundleUrl).pathname : '/')
155
- .replace(/^\/+/, '')
156
- const appRelativeDir = bundleDir.split('/').join(path.sep)
157
- const metroRootCandidates = [selectedAppDir]
158
- if (bundleDir && selectedAppDir.endsWith(path.sep + appRelativeDir)) {
159
- metroRootCandidates.push(selectedAppDir.slice(0, -appRelativeDir.length - 1))
178
+ collectFontFiles(root)
160
179
  }
161
180
  const vectorIconRequire = createRequire(path.join(selectedAppDir, 'package.json'))
162
181
  for (const dependencyName of vectorIconDependencies) {
@@ -170,18 +189,9 @@ function discoverNativeLinkedAppFontPaths(opts: {
170
189
  const fontsDir =
171
190
  dependencyName === 'react-native-vector-icons' ? 'Fonts' : 'fonts'
172
191
  // the package can sit outside the app (a hoisted workspace
173
- // node_modules or a pnpm store). metro resolves asset paths against
174
- // its config.projectRoot, which is the app in expo's monorepo setup
175
- // but the repo root when metro.config sets it there, so offer the
176
- // path from each candidate root; the reachability check keeps the
177
- // one metro actually serves.
178
- const fontsRoot = path.join(packageRoot, fontsDir)
179
- for (const metroRoot of metroRootCandidates) {
180
- collectFontFiles(
181
- fontsRoot,
182
- path.relative(metroRoot, fontsRoot).split(path.sep).join('/'),
183
- )
184
- }
192
+ // node_modules or a pnpm store); its fonts are then served through
193
+ // metro's unstable_path from the watch folder that holds its js.
194
+ collectFontFiles(path.join(packageRoot, fontsDir))
185
195
  } catch {}
186
196
  }
187
197
  }
@@ -206,7 +216,7 @@ function discoverNativeLinkedAppFontPaths(opts: {
206
216
  const root = path.resolve(selectedAppDir, assetDir)
207
217
  const relativeRoot = path.relative(selectedAppDir, root)
208
218
  if (relativeRoot.startsWith('..') || path.isAbsolute(relativeRoot)) continue
209
- collectFontFiles(root, relativeRoot.split(path.sep).join('/'))
219
+ collectFontFiles(root)
210
220
  }
211
221
  }
212
222
  }
@@ -252,16 +262,9 @@ function discoverNativeLinkedAppFontPaths(opts: {
252
262
  const fontPath = candidates.find((candidate) => {
253
263
  const relative = path.relative(selectedAppDir, candidate)
254
264
  if (relative.startsWith('..') || path.isAbsolute(relative)) return false
255
- try {
256
- return fs.statSync(candidate).isFile()
257
- } catch {
258
- return false
259
- }
265
+ return isFile(candidate)
260
266
  })
261
- if (!fontPath) continue
262
- fontPaths.push(
263
- path.relative(selectedAppDir, fontPath).split(path.sep).join('/'),
264
- )
267
+ if (fontPath) fontPaths.push(fontPath)
265
268
  }
266
269
  }
267
270
  } catch {}
@@ -280,10 +283,13 @@ export function readNativeLinkedAppFontsWire(opts: {
280
283
  throw new Error(`native font project is unavailable: ${opts.projectDir}`)
281
284
  }
282
285
  return encodeAppFontsWire(
283
- nativeLinkedFontSpecsFromPaths(discoverNativeLinkedAppFontPaths(opts), (fontPath) => {
284
- const bytes = fs.readFileSync(path.resolve(opts.projectDir, fontPath))
285
- return `data:${fontContentType(fontPath)};base64,${bytes.toString('base64')}`
286
- }),
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
+ ),
287
293
  )
288
294
  }
289
295
 
package/cli/bin.ts CHANGED
@@ -10,16 +10,17 @@
10
10
  // whole process, so the CLI itself lives in `./main` and is imported only once
11
11
  // none of them has claimed the run.
12
12
 
13
+ import { exitWithFlushedOutput } from '../src/process-output'
13
14
  import { RNX_FORKED_SCRIPT_ENV, RNX_INTERNAL_COMMAND } from './internal-child'
14
15
 
15
16
  if (process.argv[2] === '__shell-init') {
16
17
  const { renderRnxShellInit } = await import('./shell-init')
17
18
  try {
18
19
  process.stdout.write(renderRnxShellInit(process.argv[3] ?? ''))
19
- process.exit(0)
20
+ await exitWithFlushedOutput(0)
20
21
  } catch (error) {
21
22
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
22
- process.exit(1)
23
+ await exitWithFlushedOutput(1)
23
24
  }
24
25
  }
25
26
 
@@ -34,14 +35,14 @@ if (process.argv[2] === '__cloud-confirm') {
34
35
  // a box has no claim to keep alive: its simulator is held by the Box page
35
36
  // for as long as the box runs, so a shell on a box confirms nothing.
36
37
  if (session.service === 'sim') await confirmCloudSimulator(session)
37
- process.exit(0)
38
+ await exitWithFlushedOutput(0)
38
39
  } catch (error) {
39
40
  process.stderr.write(
40
41
  `Peach Cloud confirmation failed: ${
41
42
  error instanceof Error ? error.message : String(error)
42
43
  }\n`,
43
44
  )
44
- process.exit(1)
45
+ await exitWithFlushedOutput(1)
45
46
  }
46
47
  }
47
48
 
@@ -201,6 +201,11 @@ export async function printBridgeFailureDiagnostics(
201
201
  bridge: WsBridge,
202
202
  opts: FailureDiagnosticsOptions = {},
203
203
  ) {
204
+ try {
205
+ if (!(await bridge.listSims()).some((sim) => sim.readyState === 'open')) return
206
+ } catch {
207
+ // an unavailable inventory must not discard the existing diagnostic reads
208
+ }
204
209
  try {
205
210
  await printBridgeStateSummary(bridge)
206
211
  } catch {
@@ -10,7 +10,7 @@ import {
10
10
  isPeachAccessibilityTree,
11
11
  waitReadyReason,
12
12
  } from './commands/inspect/core'
13
- import { waitForSootsimIdle } from './commands/inspect/settling'
13
+ import { IdleObservationError, waitForSootsimIdle } from './commands/inspect/settling'
14
14
  import { callShellCommandWhenReady, getShellState } from './commands/inspect/shared'
15
15
  import { PERMISSION_KINDS } from './commands/permissions'
16
16
  import { saveCurrentSimId } from './current-sim'
@@ -2753,18 +2753,26 @@ export class SootSimBridgeFlowRunner {
2753
2753
  // a screen that never stops proceeds when the budget runs out, as there,
2754
2754
  // and names what kept it moving: every such step pays the whole budget.
2755
2755
  private async waitForAppToSettle() {
2756
- const result = await waitForSootsimIdle({
2757
- bridge: this.bridge,
2758
- simId: this.opts.simId,
2759
- maxMs: 3000,
2760
- pollMs: 32,
2761
- strict: true,
2762
- paintStill: true,
2763
- })
2764
- if (!result.settled) {
2765
- console.warn(
2766
- ` [flow] still moving after ${result.elapsed}ms: ${result.blockedBy}`,
2767
- )
2756
+ try {
2757
+ const result = await waitForSootsimIdle({
2758
+ bridge: this.bridge,
2759
+ simId: this.opts.simId,
2760
+ maxMs: 3000,
2761
+ pollMs: 32,
2762
+ strict: true,
2763
+ paintStill: true,
2764
+ })
2765
+ if (!result.settled) {
2766
+ console.warn(
2767
+ ` [flow] still moving after ${result.elapsed}ms: ${result.blockedBy}`,
2768
+ )
2769
+ }
2770
+ } catch (error) {
2771
+ if (error instanceof IdleObservationError) {
2772
+ console.warn(` [flow] ${error.message}`)
2773
+ return
2774
+ }
2775
+ throw error
2768
2776
  }
2769
2777
  }
2770
2778
 
@@ -24,6 +24,7 @@ import {
24
24
  buildRnxCloudArtifact,
25
25
  type RnxCloudArtifactReceipt,
26
26
  } from '../../contrast-bundler/src/metroCloudArtifact.ts'
27
+ import { rewriteGuestImportMeta } from '../../peach-engine/src/guest-import-meta.ts'
27
28
  import { loadMetroFingerprintRegistry } from '../../peach-engine/src/metro-fingerprint-registry-client.ts'
28
29
  import { inferMetroModuleIdentity } from '../../peach-engine/src/metro-fingerprint.ts'
29
30
  import { isLoopbackHost } from '../src/backend-origin.ts'
@@ -260,10 +261,10 @@ async function loadBundleSource(bundleInput: string): Promise<string> {
260
261
  const source = isRemoteBundleInput(bundleInput)
261
262
  ? await fetchBundleSource(bundleInput)
262
263
  : readImmutableBundleSource(bundleInput)
263
- // a box evaluates the bundle as a script; refuse invalid syntax before
264
- // allocating a billable host. compiling does not execute guest code.
264
+ // validate the actual runtime script form without executing guest code or
265
+ // allocating a host.
265
266
  try {
266
- new Script(source, { filename: bundleInput })
267
+ new Script(rewriteGuestImportMeta(source), { filename: bundleInput })
267
268
  } catch (error) {
268
269
  throw new Error(
269
270
  `${REMOTE_COMMAND} bundle contains invalid JavaScript: ${