peachsim 0.1.629 → 0.1.631

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 (128) hide show
  1. package/README.md +8 -4
  2. package/cli/app-fonts.ts +23 -4
  3. package/cli/bridge-flow-runner.ts +45 -31
  4. package/cli/cli-update.ts +2 -7
  5. package/cli/cloud-test-tunnel.ts +16 -3
  6. package/cli/command-registry.ts +1 -0
  7. package/cli/commands/compat.ts +11 -60
  8. package/cli/commands/config.ts +1 -1
  9. package/cli/commands/control.ts +40 -8
  10. package/cli/commands/daemon-mac-app.ts +4 -2
  11. package/cli/commands/daemon.ts +4 -4
  12. package/cli/commands/detect.ts +289 -26
  13. package/cli/commands/device.ts +2 -3
  14. package/cli/commands/inspect/actions.ts +125 -90
  15. package/cli/commands/inspect/core.ts +1 -1
  16. package/cli/commands/inspect.ts +10 -1
  17. package/cli/commands/maestro.ts +3 -1
  18. package/cli/commands/platform.ts +1 -1
  19. package/cli/commands/project-report.ts +386 -0
  20. package/cli/commands/projectReportContract.ts +42 -0
  21. package/cli/commands/report-issue.ts +38 -54
  22. package/cli/commands/screenshot-layers.ts +1 -1
  23. package/cli/commands/screenshot.ts +1 -1
  24. package/cli/commands/setup.ts +6 -4
  25. package/cli/commands/skills.ts +0 -31
  26. package/cli/commands/telemetry.ts +8 -3
  27. package/cli/commands/test.ts +347 -134
  28. package/cli/drivers/playwright-provisioning.ts +3 -2
  29. package/cli/drivers/playwright-sim-host.ts +25 -9
  30. package/cli/main.ts +19 -3
  31. package/cli/outbound-endpoints.ts +2 -5
  32. package/cli/privacy.ts +1 -2
  33. package/cli/runtime-diagnostics.ts +51 -0
  34. package/cli/telemetry.ts +78 -7
  35. package/detox/index.ts +77 -51
  36. package/dist-lib/agent-daemon-client.cjs +1 -1
  37. package/dist-lib/agent-events.cjs +1 -1
  38. package/dist-lib/agent-identity.cjs +1 -1
  39. package/dist-lib/agent-sessions.cjs +1 -1
  40. package/dist-lib/attached-projects.cjs +1 -1
  41. package/dist-lib/auth/shared-session.cjs +1 -1
  42. package/dist-lib/backend-origin.cjs +1 -1
  43. package/dist-lib/beta.cjs +1 -1
  44. package/dist-lib/beta.mjs +1 -1
  45. package/dist-lib/bridge-constants.cjs +1 -1
  46. package/dist-lib/bridge-contract-input.cjs +1 -1
  47. package/dist-lib/bridge-contract-input.mjs +1 -1
  48. package/dist-lib/bridge-contract.cjs +1 -1
  49. package/dist-lib/bridge-contract.mjs +1 -1
  50. package/dist-lib/capture-contract.cjs +1 -1
  51. package/dist-lib/capture-contract.mjs +1 -1
  52. package/dist-lib/cli-constants.cjs +1 -1
  53. package/dist-lib/cloud-contract.cjs +1 -1
  54. package/dist-lib/cloud-contract.mjs +1 -1
  55. package/dist-lib/cloud-test-args.cjs +1 -1
  56. package/dist-lib/cloud-test-args.mjs +1 -1
  57. package/dist-lib/cloud.cjs +1 -1
  58. package/dist-lib/cloud.mjs +1 -1
  59. package/dist-lib/config.cjs +1 -1
  60. package/dist-lib/detox/index.cjs +130 -33
  61. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  62. package/dist-lib/home-paths.cjs +8 -5
  63. package/dist-lib/host/bridge-host.cjs +353 -290
  64. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  65. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  66. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  67. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  68. package/dist-lib/host/websocket-proxy.cjs +1 -1
  69. package/dist-lib/index.cjs +257 -121
  70. package/dist-lib/jump-to-source-babel.cjs +1 -1
  71. package/dist-lib/jump-to-source-native.cjs +1 -1
  72. package/dist-lib/{vite-kotlin.cjs → kotlin-host.cjs} +130 -19
  73. package/dist-lib/menu.cjs +11 -1
  74. package/dist-lib/menu.mjs +11 -1
  75. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  76. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  77. package/dist-lib/metro-production-bundle.cjs +1 -1
  78. package/dist-lib/metro-production-bundle.mjs +1 -1
  79. package/dist-lib/metro.cjs +132 -9
  80. package/dist-lib/native-app-identity.cjs +278 -0
  81. package/dist-lib/native-app-identity.mjs +245 -0
  82. package/dist-lib/profiles.cjs +1 -1
  83. package/dist-lib/public-brand.cjs +1 -1
  84. package/dist-lib/react-native-host-modules.cjs +1 -1
  85. package/dist-lib/react-native-host-modules.mjs +1 -1
  86. package/dist-lib/render-mode.cjs +1 -1
  87. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  88. package/dist-lib/sdk.cjs +112 -87
  89. package/dist-lib/sdk.mjs +111 -87
  90. package/dist-lib/skills.cjs +1270 -709
  91. package/dist-lib/swift-compile.cjs +1 -1
  92. package/dist-lib/swift-compile.mjs +1 -1
  93. package/dist-lib/swift-package.cjs +1 -1
  94. package/dist-lib/swift-package.mjs +1 -1
  95. package/dist-lib/swift-submit.cjs +1 -1
  96. package/dist-lib/swift-submit.mjs +1 -1
  97. package/dist-lib/swift.cjs +672 -275
  98. package/dist-lib/vite-swift.cjs +1 -1
  99. package/dist-lib/vite.cjs +11 -28
  100. package/kotlin/README.md +32 -4
  101. package/kotlin/fixtures/counter/Counter.kt +26 -0
  102. package/kotlin/fixtures/counter/CounterScreen.kt +40 -0
  103. package/kotlin/fixtures/headless-counter/Headless.kt +228 -0
  104. package/kotlin/fixtures/runtime-state/RuntimeState.kt +200 -0
  105. package/kotlin/fork/LICENSE +178 -0
  106. package/kotlin/fork/NOTICE +25 -0
  107. package/kotlin/fork/README.md +59 -0
  108. package/kotlin/fork/build.sh +128 -0
  109. package/kotlin/fork/compose-worker-globals.patch +96 -0
  110. package/kotlin/fork/fork.json +33 -0
  111. package/kotlin/fork/publish.ts +77 -0
  112. package/kotlin/fork/skiko-worker-clock.patch +33 -0
  113. package/kotlin/toolchain.json +288 -0
  114. package/package.json +13 -5
  115. package/skills/contrast/SKILL.md +1 -0
  116. package/src/automatic-update-policy.ts +17 -0
  117. package/src/home-paths.ts +11 -9
  118. package/src/host/bridge-host.ts +3 -1
  119. package/src/{vite-plugin-kotlin.ts → kotlin-host.ts} +155 -12
  120. package/src/menu.ts +11 -0
  121. package/src/metro-plugin.ts +18 -0
  122. package/src/native-app-identity.ts +376 -0
  123. package/src/native-firebase-config.ts +90 -0
  124. package/src/screenshots/frame-compose.ts +1 -1
  125. package/src/screenshots/registry.ts +1 -1
  126. package/src/screenshots/schema.ts +2 -2
  127. package/src/vite-plugin-one.ts +11 -28
  128. package/src/vite-plugin.ts +6 -1
package/README.md CHANGED
@@ -15,8 +15,10 @@ The executable updates itself with `peach upgrade`. Successful interactive human
15
15
  commands refresh a cached stable CLI version in a detached process at most once
16
16
  every 20 hours. A newer version produces one short notice on stderr, once per
17
17
  version and no more than weekly. CI, agents, JSON output, pipes, and
18
- noninteractive commands stay silent. Set `PEACH_UPDATE_CHECK=off` to disable the
19
- check and notice; `peach version` checks explicitly, while `peach --version` remains
18
+ noninteractive commands stay silent. Set `PEACH_UPDATE_CHECK=off` to disable
19
+ automatic CLI checks, runtime updates, and their notices. Start the bridge with
20
+ this environment, or reinstall the background service with it to persist the choice.
21
+ `peach version` checks explicitly, while `peach --version` remains
20
22
  offline.
21
23
 
22
24
  Neither artifact contains rendering code. The canvas engine (CanvasKit
@@ -233,7 +235,7 @@ Pass a target to `peach open <port-or-url>` when a script or agent needs an expl
233
235
  selection.
234
236
 
235
237
  The first `peach open` launches an isolated Chrome for Testing sim. The CLI
236
- resolves Playwright from the app or CLI install, asks that exact package for
238
+ resolves Playwright from the current directory or beside its executable, asks that exact package for
237
239
  its required browser executable, and runs that package's installer once when
238
240
  the executable is absent. It never substitutes another revision found in the
239
241
  shared Playwright cache. Later opens reuse that sim and reload it at the
@@ -557,7 +559,9 @@ frame for quick art-direction passes.
557
559
  ## Skills registry
558
560
 
559
561
  `peach skill install` installs Peach agent skills in the standard
560
- `<skill-name>/SKILL.md` layout used by Codex and Claude Code. The bundled
562
+ `<skill-name>/SKILL.md` layout used by Codex and Claude Code. Building the CLI
563
+ only writes build artifacts; run `peach skill install` explicitly to install
564
+ or refresh agent skills. The bundled
561
565
  skills cover setup, debugging/perf/accessibility, testing, visual review, and
562
566
  the generated `contrast` command index. The published CLI registry and generated
563
567
  website docs both come from `packages/peach-skills/`:
package/cli/app-fonts.ts CHANGED
@@ -139,6 +139,16 @@ export function discoverNativeLinkedAppFonts(opts: {
139
139
  collectFontFiles(root, relativeRoot.split(path.sep).join('/'))
140
140
  }
141
141
 
142
+ // the bundle path is the entry relative to metro's root, so a bundle at
143
+ // /apps/app/index.bundle served for /repo/apps/app puts that root at /repo.
144
+ const bundleDir = path.posix
145
+ .dirname(new URL(opts.bundleUrl).pathname)
146
+ .replace(/^\/+/, '')
147
+ const appRelativeDir = bundleDir.split('/').join(path.sep)
148
+ const metroRootCandidates = [selectedAppDir]
149
+ if (bundleDir && selectedAppDir.endsWith(path.sep + appRelativeDir)) {
150
+ metroRootCandidates.push(selectedAppDir.slice(0, -appRelativeDir.length - 1))
151
+ }
142
152
  const vectorIconRequire = createRequire(path.join(selectedAppDir, 'package.json'))
143
153
  for (const dependencyName of vectorIconDependencies) {
144
154
  try {
@@ -150,10 +160,19 @@ export function discoverNativeLinkedAppFonts(opts: {
150
160
  const packageRoot = entryPath.slice(0, markerIndex + marker.length - 1)
151
161
  const fontsDir =
152
162
  dependencyName === 'react-native-vector-icons' ? 'Fonts' : 'fonts'
153
- collectFontFiles(
154
- path.join(packageRoot, fontsDir),
155
- path.posix.join('node_modules', dependencyName, fontsDir),
156
- )
163
+ // the package can sit outside the app (a hoisted workspace
164
+ // node_modules or a pnpm store). metro resolves asset paths against
165
+ // its config.projectRoot, which is the app in expo's monorepo setup
166
+ // but the repo root when metro.config sets it there, so offer the
167
+ // path from each candidate root; the reachability check keeps the
168
+ // one metro actually serves.
169
+ const fontsRoot = path.join(packageRoot, fontsDir)
170
+ for (const metroRoot of metroRootCandidates) {
171
+ collectFontFiles(
172
+ fontsRoot,
173
+ path.relative(metroRoot, fontsRoot).split(path.sep).join('/'),
174
+ )
175
+ }
157
176
  } catch {}
158
177
  }
159
178
  }
@@ -1,6 +1,6 @@
1
1
  import * as fs from 'fs'
2
2
  import * as path from 'path'
3
- import { devices, type DeviceModel } from 'peach-engine/settings'
3
+ import { devices, type DeviceModel } from 'peach-engine/settings/devices'
4
4
  import yaml from 'yaml'
5
5
  import { REQUIRED_IDENTICAL_FLOW_FRAMES, framesIdentical } from '../detox/proof-frame.cjs'
6
6
  import { flowTimeoutScale } from '../src/flow-timeout-scale'
@@ -1314,6 +1314,9 @@ export class SootSimBridgeFlowRunner {
1314
1314
 
1315
1315
  async tapOn(target: string | FlowPointTarget) {
1316
1316
  const opts = typeof target === 'string' ? { text: target } : target
1317
+ // maestro settles before resolving a tap's final position as well as after
1318
+ // delivering it. a preceding assertion does not settle a pending update.
1319
+ await this.waitForAppToSettle()
1317
1320
  const repeat = Math.max(1, Math.trunc(opts.repeat ?? 1))
1318
1321
  const repeatDelay = opts.delay !== undefined && opts.delay >= 0 ? opts.delay : 100
1319
1322
  // maestro Maestro.hierarchyBasedTap with a TapRepeat: every tap is padded
@@ -2379,14 +2382,14 @@ export class SootSimBridgeFlowRunner {
2379
2382
  }
2380
2383
  }
2381
2384
 
2382
- // maestro waits after every input for the screen to stop changing, up to
2385
+ // maestro waits around taps for the screen to stop changing, up to
2383
2386
  // its ios driver's 3 s settle budget, comparing screenshots, so an action
2384
2387
  // still in flight (a submit spinner, a sheet closing) finishes before the
2385
2388
  // next command resolves its target. an app can take a few hundred ms to
2386
2389
  // react to a press, so stillness must hold about half a second, and it
2387
2390
  // counts painted frames because a spinner repaints without moving layout.
2388
2391
  // a screen that never stops proceeds when the budget runs out, as there.
2389
- private async settleAfterInput() {
2392
+ private async waitForAppToSettle() {
2390
2393
  await waitForSootsimIdle({
2391
2394
  bridge: this.bridge,
2392
2395
  simId: this.opts.simId,
@@ -2396,6 +2399,10 @@ export class SootSimBridgeFlowRunner {
2396
2399
  strict: true,
2397
2400
  paintStill: true,
2398
2401
  })
2402
+ }
2403
+
2404
+ private async settleAfterInput() {
2405
+ await this.waitForAppToSettle()
2399
2406
  this.lastInteractionAt = Date.now()
2400
2407
  }
2401
2408
 
@@ -2703,38 +2710,45 @@ export class SootSimBridgeFlowRunner {
2703
2710
  }
2704
2711
 
2705
2712
  try {
2706
- // per-step watchdog: abort if nothing forward-progresses for 10s.
2707
- // catches dead bundles / frozen bridges / hung ws calls. explicit
2708
- // wait steps (extendedWaitUntil, waitFor) opt out and keep their
2709
- // own timeout — the user asked for that time; don't override.
2710
- const hasExplicitWait = !!(
2711
- step.extendedWaitUntil ||
2712
- step.waitFor ||
2713
- step.scrollUntilVisible ||
2714
- step.launchApp ||
2715
- step.runFlow ||
2716
- // retry reruns its body on failure, so its wall time is the body's
2717
- // times the retry count — the 10s no-progress watchdog is the wrong
2718
- // budget and would report "bridge probably hung" for a slow retry.
2719
- step.retry ||
2720
- // runScript http calls are synchronous with a 5-minute upstream
2721
- // timeout — the 10s watchdog would kill legitimate setup scripts.
2722
- step.runScript ||
2723
- // takeScreenshot self-governs: it retries a bounded visual settle that
2724
- // already fails with its own reason. sharing the watchdog's 10s budget
2725
- // meant the watchdog always won that race, reporting "bridge probably
2726
- // hung" for a screen that was merely still animating.
2727
- step.takeScreenshot ||
2728
- // assertions wait out maestro's lookup window, longer than 10s, and
2729
- // every query they make times out on its own, so they report their
2730
- // own failure instead of the watchdog's guess.
2731
- step.assertVisible ||
2732
- step.assertNotVisible
2713
+ // simple commands have a wall-clock watchdog. bounded operations keep
2714
+ // their own lookup, settle and bridge deadlines, so the watchdog cannot
2715
+ // report a completed step while its action continues in the background.
2716
+ const hasBoundedWait = !!(
2717
+ // taps combine bounded lookup, settling and delivery. a wall-clock
2718
+ // watchdog can finish the step while that action is still running.
2719
+ // let these operations finish before starting the next command.
2720
+ (
2721
+ step.tapOn ||
2722
+ step.doubleTapOn ||
2723
+ step.longPressOn ||
2724
+ step.extendedWaitUntil ||
2725
+ step.waitFor ||
2726
+ step.scrollUntilVisible ||
2727
+ step.launchApp ||
2728
+ step.runFlow ||
2729
+ // retry reruns its body on failure, so its wall time is the body's
2730
+ // times the retry count — the 10s no-progress watchdog is the wrong
2731
+ // budget and would report "bridge probably hung" for a slow retry.
2732
+ step.retry ||
2733
+ // runScript http calls are synchronous with a 5-minute upstream
2734
+ // timeout — the 10s watchdog would kill legitimate setup scripts.
2735
+ step.runScript ||
2736
+ // takeScreenshot self-governs: it retries a bounded visual settle that
2737
+ // already fails with its own reason. sharing the watchdog's 10s budget
2738
+ // meant the watchdog always won that race, reporting "bridge probably
2739
+ // hung" for a screen that was merely still animating.
2740
+ step.takeScreenshot ||
2741
+ // assertions wait out maestro's lookup window, longer than 10s, and
2742
+ // every query they make times out on its own, so they report their
2743
+ // own failure instead of the watchdog's guess.
2744
+ step.assertVisible ||
2745
+ step.assertNotVisible
2746
+ )
2733
2747
  )
2734
2748
  const STEP_WATCHDOG_MS = 10_000
2735
2749
  const body = this.runStepInner(step)
2736
2750
  let screenshotPath: string | undefined
2737
- if (hasExplicitWait) {
2751
+ if (hasBoundedWait) {
2738
2752
  const result = await body
2739
2753
  if (typeof result === 'string') screenshotPath = result
2740
2754
  } else {
package/cli/cli-update.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { compareSemver } from '@contrast/runtime-delivery'
4
+ import { automaticPeachUpdatesEnabled } from '../src/automatic-update-policy'
4
5
  import { getCliVersion } from '../src/cli-version'
5
6
  import { cliUpdateFilePath } from '../src/home-paths'
6
7
  import { isAgentEnv } from './commands/inspect/env'
@@ -127,12 +128,6 @@ function envFlagIsOn(value: string | undefined): boolean {
127
128
  return Boolean(value) && !envFlagIsOff(value)
128
129
  }
129
130
 
130
- export function automaticCliUpdateChecksEnabled(
131
- env: Readonly<Record<string, string | undefined>> = process.env,
132
- ): boolean {
133
- return !envFlagIsOff(env.PEACH_UPDATE_CHECK)
134
- }
135
-
136
131
  export function shouldUseAutomaticCliUpdateCheck(
137
132
  invocation: CliUpdateInvocation,
138
133
  ): boolean {
@@ -143,7 +138,7 @@ export function shouldUseAutomaticCliUpdateCheck(
143
138
  invocation.version ||
144
139
  !invocation.stdoutIsTTY ||
145
140
  !invocation.stderrIsTTY ||
146
- !automaticCliUpdateChecksEnabled(env) ||
141
+ !automaticPeachUpdatesEnabled(env) ||
147
142
  envFlagIsOn(env.CI) ||
148
143
  envFlagIsOn(env.GITHUB_ACTIONS) ||
149
144
  envFlagIsOn(env.PEACH_NO_PROMPT) ||
@@ -26,6 +26,7 @@ import {
26
26
  import { createServer, request as httpRequest } from 'node:http'
27
27
  import { tmpdir } from 'node:os'
28
28
  import { join } from 'node:path'
29
+ import { pipeline } from 'node:stream'
29
30
  import { sleep } from '../src/sleep'
30
31
 
31
32
  // must match packages/contrast-runner/src/hostedPorts.ts.
@@ -160,12 +161,24 @@ export function startCloudTestTokenProxy(
160
161
  },
161
162
  (upRes) => {
162
163
  res.writeHead(upRes.statusCode || 502, upRes.headers)
163
- upRes.pipe(res)
164
+ // a broken upstream body must break the downstream response too. pipe()
165
+ // leaves it open after an aborted transfer, stranding the tester.
166
+ pipeline(upRes, res, (error) => {
167
+ if (error) {
168
+ console.error(
169
+ `upstream body error ${req.method} ${req.url}: ${error.message}`,
170
+ )
171
+ }
172
+ })
164
173
  },
165
174
  )
166
175
  up.on('error', (err) => {
167
- res.writeHead(502, { 'content-type': 'text/plain' })
168
- res.end(`upstream error: ${err.message}`)
176
+ // a transport failure is a network error, including before headers
177
+ // arrive; ending a partial response would report a successful transfer.
178
+ res.destroy(err)
179
+ })
180
+ res.once('close', () => {
181
+ if (!res.writableFinished) up.destroy()
169
182
  })
170
183
  req.pipe(up)
171
184
  })
@@ -48,6 +48,7 @@ const NODE_COMMAND_NAMES = [
48
48
  'claim',
49
49
  'close',
50
50
  'compat',
51
+ 'project-report',
51
52
  'report-issue',
52
53
  'desktop',
53
54
  'login',
@@ -1,7 +1,6 @@
1
1
  // peach compat — scan dependencies with the shared compatibility engine.
2
2
 
3
- import { existsSync, readFileSync } from 'node:fs'
4
- import { dirname, join, relative, resolve } from 'node:path'
3
+ import { relative, resolve } from 'node:path'
5
4
  import {
6
5
  COMPAT_RESULT_CAVEAT,
7
6
  formatDetailedScanLines,
@@ -9,13 +8,13 @@ import {
9
8
  scanDeps,
10
9
  type ScanResult,
11
10
  } from '@peach/compat/web'
12
- import { findProjectRoot, scanWorkspace } from './detect'
13
-
14
- interface PackageManifest {
15
- name?: string
16
- dependencies?: Record<string, string>
17
- devDependencies?: Record<string, string>
18
- }
11
+ import {
12
+ findProjectRoot,
13
+ scanWorkspace,
14
+ installedManifest,
15
+ readProjectManifest,
16
+ } from './detect'
17
+ import type { ProjectManifest } from './projectReportContract'
19
18
 
20
19
  export async function runCompat(args: string[]): Promise<void> {
21
20
  if (args.includes('--help') || args.includes('-h')) {
@@ -41,7 +40,7 @@ flags:
41
40
  const packageName = args.find(
42
41
  (arg, index) => !arg.startsWith('-') && (appIndex < 0 || index !== appIndex + 1),
43
42
  )
44
- const pkg = readPackageJson(appDir)
43
+ const pkg = readProjectManifest(appDir)
45
44
  if (!pkg) {
46
45
  console.error(` no package.json found in ${appDir}`)
47
46
  process.exitCode = 1
@@ -65,7 +64,7 @@ flags:
65
64
  const workspaceRoot = findProjectRoot(appDir)
66
65
  const elsewhere = scanWorkspace(workspaceRoot).apps.flatMap((app) => {
67
66
  if (resolve(app.dir) === appDir) return []
68
- const manifest = readPackageJson(app.dir)
67
+ const manifest = readProjectManifest(app.dir)
69
68
  if (!manifest) return []
70
69
  const result = scanDeps(installedManifest(app.dir, manifest))
71
70
  return result.packages.length > 0 ? [{ dir: app.dir, result }] : []
@@ -114,57 +113,9 @@ function report(args: string[], result: ScanResult): void {
114
113
  console.log(`\n ${COMPAT_RESULT_CAVEAT}\n`)
115
114
  }
116
115
 
117
- // the declared range is often not a version: pnpm and bun catalogs write
118
- // `catalog:`, a workspace dependency writes `workspace:*`, and a pinned tarball
119
- // writes a URL. none of those match a verified version range, so a whole app
120
- // reports as unverified. the installed tree holds the version that will
121
- // actually run, so ask it, and keep the declared range for anything the project
122
- // has not installed.
123
- function installedManifest(appDir: string, pkg: PackageManifest): PackageManifest {
124
- const resolveVersion = (name: string, declared: string): string => {
125
- let dir = appDir
126
- while (true) {
127
- const manifest = join(dir, 'node_modules', name, 'package.json')
128
- if (existsSync(manifest)) {
129
- try {
130
- const parsed: unknown = JSON.parse(readFileSync(manifest, 'utf8'))
131
- if (parsed && typeof parsed === 'object' && 'version' in parsed) {
132
- const version = parsed.version
133
- if (typeof version === 'string') return version
134
- }
135
- } catch {}
136
- return declared
137
- }
138
- const parent = dirname(dir)
139
- if (parent === dir) return declared
140
- dir = parent
141
- }
142
- }
143
- const mapped = (deps: Record<string, string> | undefined) =>
144
- deps &&
145
- Object.fromEntries(
146
- Object.entries(deps).map(([name, declared]) => [
147
- name,
148
- resolveVersion(name, declared),
149
- ]),
150
- )
151
- return {
152
- name: pkg.name,
153
- dependencies: mapped(pkg.dependencies),
154
- devDependencies: mapped(pkg.devDependencies),
155
- }
156
- }
157
-
158
- function readPackageJson(appDir: string): PackageManifest | null {
159
- const packagePath = resolve(appDir, 'package.json')
160
- if (!existsSync(packagePath)) return null
161
- const parsed: unknown = JSON.parse(readFileSync(packagePath, 'utf8'))
162
- return parsed && typeof parsed === 'object' ? parsed : null
163
- }
164
-
165
116
  function scanOnePackage(
166
117
  appDir: string,
167
- pkg: PackageManifest,
118
+ pkg: ProjectManifest,
168
119
  packageName: string,
169
120
  ): ScanResult {
170
121
  const installed = installedManifest(appDir, pkg)
@@ -11,7 +11,7 @@ usage:
11
11
  environment:
12
12
  PEACH_HINTS=off suppress CLI hints
13
13
  PEACH_HINTS=always show every CLI hint every time
14
- PEACH_UPDATE_CHECK=off disable background CLI update checks and notices
14
+ PEACH_UPDATE_CHECK=off disable automatic CLI checks, runtime updates, and notices
15
15
  `)
16
16
  }
17
17
 
@@ -8,7 +8,7 @@ import {
8
8
  listSelectableDeviceModels,
9
9
  resolveDeviceFitBox,
10
10
  type DeviceModel,
11
- } from 'peach-engine/settings'
11
+ } from 'peach-engine/settings/devices'
12
12
  import { WebSocket } from 'ws'
13
13
  import { probePort } from '../../scripts/dev-server-scanner'
14
14
  import { isAppDeepLinkTarget, openAppUrl } from '../../src/app-url'
@@ -36,6 +36,7 @@ import {
36
36
  runtimeDir,
37
37
  } from '../../src/home-paths'
38
38
  import { applyHotMode } from '../../src/native-dev-bundle-url'
39
+ import { readNativeFirebaseConfig } from '../../src/native-firebase-config'
39
40
  import { ensureProfile } from '../../src/profiles'
40
41
  import { rnxPublicBrand } from '../../src/public-brand'
41
42
  import {
@@ -72,7 +73,9 @@ import {
72
73
  } from '../current-sim'
73
74
  import { getDriver } from '../drivers/registry'
74
75
  import { openUrl as openUrlInBrowser } from '../open-url'
76
+ import { diagnosticsAllowedForCurrentProcess } from '../privacy'
75
77
  import { rethrowIfExit, rnxExit } from '../run-rnx'
78
+ import { collectRuntimeDiagnostics } from '../runtime-diagnostics'
76
79
  import { rnxSelfInvocation } from '../self-invocation'
77
80
  import {
78
81
  BridgeSimLockedError,
@@ -694,11 +697,13 @@ async function filterReachableAppFonts(
694
697
  }
695
698
  }),
696
699
  )
697
- const reachable: DiscoveredAppFontSpec[] = []
700
+ // a font offered from more than one candidate metro root keeps the first
701
+ // url metro served.
702
+ const reachable = new Map<string, DiscoveredAppFontSpec>()
698
703
  for (const spec of results) {
699
- if (spec !== null) reachable.push(spec)
704
+ if (spec !== null && !reachable.has(spec.family)) reachable.set(spec.family, spec)
700
705
  }
701
- return reachable
706
+ return [...reachable.values()]
702
707
  }
703
708
 
704
709
  async function resolveShellUrl(
@@ -774,6 +779,26 @@ async function stampAppSplash(
774
779
  }
775
780
  }
776
781
 
782
+ function stampNativeFirebaseConfig(
783
+ urlStr: string,
784
+ project?: ResolvedAppProject | null,
785
+ ): string {
786
+ const url = new URL(urlStr)
787
+ if (!url.searchParams.get('bundle') || !project) return urlStr
788
+ try {
789
+ url.searchParams.set(
790
+ 'nativeFirebase',
791
+ JSON.stringify(readNativeFirebaseConfig(project.appDir)),
792
+ )
793
+ } catch (error) {
794
+ url.searchParams.set(
795
+ 'nativeFirebase',
796
+ JSON.stringify({ error: error instanceof Error ? error.message : String(error) }),
797
+ )
798
+ }
799
+ return url.toString()
800
+ }
801
+
777
802
  // stamp the engine page url with `?appCatalog=` so an app's asset-catalog
778
803
  // colors and its declared accent reach the tenant, which applies them to the
779
804
  // bundle this url opens. the catalog lives in the native binary, never in
@@ -867,8 +892,11 @@ export async function buildShellUrl(
867
892
  // fallback typeface, which then reads as a conformance diff.
868
893
  const project =
869
894
  opts.project ?? (await resolveAppProjectForBundleUrl(bundleUrlOf(resolved)))
870
- const stamped = stampAppCatalog(
871
- await stampAppSplash(await stampAppFonts(resolved, project), project),
895
+ const stamped = stampNativeFirebaseConfig(
896
+ stampAppCatalog(
897
+ await stampAppSplash(await stampAppFonts(resolved, project), project),
898
+ project,
899
+ ),
872
900
  project,
873
901
  )
874
902
  if (
@@ -912,6 +940,7 @@ function deriveCurrentSimBaseUrl(
912
940
  // bundle.
913
941
  url.searchParams.delete('appSplash')
914
942
  url.searchParams.delete('appCatalog')
943
+ url.searchParams.delete('nativeFirebase')
915
944
  return url.toString()
916
945
  } catch {
917
946
  return fallback
@@ -1268,7 +1297,8 @@ function printAgentListenHint(quiet: boolean): void {
1268
1297
  // a splash; the caller can `peach describe` again a second later. for
1269
1298
  // warm loads it's the real screen. guarded by --no-describe.
1270
1299
  async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: string[]) {
1271
- if (args.includes('--no-describe')) return
1300
+ const noDescribe = args.includes('--no-describe')
1301
+ if (noDescribe && !diagnosticsAllowedForCurrentProcess()) return
1272
1302
  const prevQuiet = process.env.SOOTSIM_QUIET_TARGET_NOTICE
1273
1303
  process.env.SOOTSIM_QUIET_TARGET_NOTICE = '1'
1274
1304
  try {
@@ -1277,7 +1307,7 @@ async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: strin
1277
1307
  // seconds on a still-routing guest app, which is exactly what we want
1278
1308
  // to skip for the open-side describe. caller can `peach describe`
1279
1309
  // again later for post-routing state.
1280
- await waitForTreeStable(wsPort, simId, { stableMs: 150, maxMs: 400 })
1310
+ if (!noDescribe) await waitForTreeStable(wsPort, simId, { stableMs: 150, maxMs: 400 })
1281
1311
  const bridge = createBridge(wsPort, {
1282
1312
  commandTimeoutMs: 3000,
1283
1313
  simId,
@@ -1285,6 +1315,7 @@ async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: strin
1285
1315
  simIdSource: 'flag',
1286
1316
  })
1287
1317
  try {
1318
+ if (noDescribe) return
1288
1319
  let readyProbe: Awaited<ReturnType<typeof inspectWaitReady>> | null = null
1289
1320
  try {
1290
1321
  readyProbe = await inspectWaitReady(bridge, 1)
@@ -1316,6 +1347,7 @@ async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: strin
1316
1347
  console.log('')
1317
1348
  console.log(result.tree)
1318
1349
  } finally {
1350
+ await collectRuntimeDiagnostics(bridge)
1319
1351
  bridge.close()
1320
1352
  }
1321
1353
  } catch {
@@ -135,8 +135,10 @@ function renderLauncherScript(
135
135
  const quoted = parts.map(shellSingleQuote).join(' ')
136
136
  const stdout = shellSingleQuote(logDir.stdout)
137
137
  const stderr = shellSingleQuote(logDir.stderr)
138
- const peachHome = process.env.PEACH_HOME
139
- const environment = peachHome ? `export PEACH_HOME=${shellSingleQuote(peachHome)}\n` : ''
138
+ const environment = ['PEACH_HOME', 'PEACH_UPDATE_CHECK']
139
+ .filter((key) => process.env[key])
140
+ .map((key) => `export ${key}=${shellSingleQuote(process.env[key] ?? '')}\n`)
141
+ .join('')
140
142
  return `#!/bin/sh
141
143
  # peach daemon launcher generated by 'peach daemon install'.
142
144
  # launchd spawns this; we exec the real peach invocation in place so
@@ -149,10 +149,10 @@ ${programArgs}
149
149
  function renderSystemdUnit(invocation: RnxSelfInvocation, port: number): string {
150
150
  mkdirSync(LOG_DIR_LINUX, { recursive: true })
151
151
  const exec = [invocation.executable, ...invocation.prefixArgs].map(shellQuote).join(' ')
152
- const peachHome = process.env.PEACH_HOME
153
- const environment = peachHome
154
- ? `Environment=${shellQuote(`PEACH_HOME=${peachHome}`)}\n`
155
- : ''
152
+ const environment = ['PEACH_HOME', 'PEACH_UPDATE_CHECK']
153
+ .filter((key) => process.env[key])
154
+ .map((key) => `Environment=${shellQuote(`${key}=${process.env[key]}`)}\n`)
155
+ .join('')
156
156
  return `[Unit]
157
157
  Description=peach bridge daemon
158
158
  After=default.target