peachsim 0.1.630 → 0.1.632

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 (96) hide show
  1. package/README.md +9 -1
  2. package/cli/app-fonts.ts +23 -4
  3. package/cli/bridge-flow-runner.ts +35 -28
  4. package/cli/command-registry.ts +1 -0
  5. package/cli/commands/compat.ts +11 -60
  6. package/cli/commands/control.ts +12 -5
  7. package/cli/commands/detect.ts +289 -26
  8. package/cli/commands/inspect/core.ts +1 -1
  9. package/cli/commands/inspect.ts +4 -0
  10. package/cli/commands/maestro.ts +8 -2
  11. package/cli/commands/project-report.ts +386 -0
  12. package/cli/commands/projectReportContract.ts +42 -0
  13. package/cli/commands/report-issue.ts +38 -54
  14. package/cli/commands/setup.ts +6 -4
  15. package/cli/commands/telemetry.ts +8 -3
  16. package/cli/commands/test.ts +374 -133
  17. package/cli/drivers/playwright-core-pin.ts +10 -0
  18. package/cli/drivers/playwright-provisioning.ts +36 -1
  19. package/cli/drivers/playwright.ts +26 -19
  20. package/cli/main.ts +11 -1
  21. package/cli/outbound-endpoints.ts +2 -1
  22. package/cli/privacy.ts +1 -2
  23. package/cli/runtime-diagnostics.ts +51 -0
  24. package/cli/shell-init.ts +6 -1
  25. package/cli/telemetry.ts +78 -7
  26. package/cli/ws-bridge.ts +8 -2
  27. package/detox/index.ts +3 -8
  28. package/dist-lib/agent-daemon-client.cjs +1 -1
  29. package/dist-lib/agent-events.cjs +1 -1
  30. package/dist-lib/agent-identity.cjs +1 -1
  31. package/dist-lib/agent-sessions.cjs +1 -1
  32. package/dist-lib/attached-projects.cjs +1 -1
  33. package/dist-lib/auth/shared-session.cjs +1 -1
  34. package/dist-lib/backend-origin.cjs +1 -1
  35. package/dist-lib/beta.cjs +1 -1
  36. package/dist-lib/beta.mjs +1 -1
  37. package/dist-lib/bridge-constants.cjs +1 -1
  38. package/dist-lib/bridge-contract-input.cjs +1 -1
  39. package/dist-lib/bridge-contract-input.mjs +1 -1
  40. package/dist-lib/bridge-contract.cjs +1 -1
  41. package/dist-lib/bridge-contract.mjs +1 -1
  42. package/dist-lib/capture-contract.cjs +1 -1
  43. package/dist-lib/capture-contract.mjs +1 -1
  44. package/dist-lib/cli-constants.cjs +1 -1
  45. package/dist-lib/cloud-contract.cjs +1 -1
  46. package/dist-lib/cloud-contract.mjs +1 -1
  47. package/dist-lib/cloud-test-args.cjs +17 -5
  48. package/dist-lib/cloud-test-args.mjs +17 -5
  49. package/dist-lib/cloud.cjs +1 -1
  50. package/dist-lib/cloud.mjs +1 -1
  51. package/dist-lib/config.cjs +1 -1
  52. package/dist-lib/detox/index.cjs +2 -7
  53. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  54. package/dist-lib/home-paths.cjs +13 -5
  55. package/dist-lib/host/bridge-host.cjs +157 -113
  56. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  57. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  58. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  59. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  60. package/dist-lib/host/websocket-proxy.cjs +1 -1
  61. package/dist-lib/index.cjs +11 -2
  62. package/dist-lib/jump-to-source-babel.cjs +1 -1
  63. package/dist-lib/jump-to-source-native.cjs +1 -1
  64. package/dist-lib/kotlin-host.cjs +1 -1
  65. package/dist-lib/menu.cjs +11 -1
  66. package/dist-lib/menu.mjs +11 -1
  67. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  68. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  69. package/dist-lib/metro-production-bundle.cjs +1 -1
  70. package/dist-lib/metro-production-bundle.mjs +1 -1
  71. package/dist-lib/metro.cjs +1 -1
  72. package/dist-lib/native-app-identity.cjs +1 -1
  73. package/dist-lib/native-app-identity.mjs +1 -1
  74. package/dist-lib/profiles.cjs +1 -1
  75. package/dist-lib/public-brand.cjs +1 -1
  76. package/dist-lib/react-native-host-modules.cjs +1 -1
  77. package/dist-lib/react-native-host-modules.mjs +1 -1
  78. package/dist-lib/render-mode.cjs +1 -1
  79. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  80. package/dist-lib/sdk.cjs +3 -1
  81. package/dist-lib/sdk.mjs +2 -1
  82. package/dist-lib/skills.cjs +700 -329
  83. package/dist-lib/swift-compile.cjs +1 -1
  84. package/dist-lib/swift-compile.mjs +1 -1
  85. package/dist-lib/swift-package.cjs +1 -1
  86. package/dist-lib/swift-package.mjs +1 -1
  87. package/dist-lib/swift-submit.cjs +1 -1
  88. package/dist-lib/swift-submit.mjs +1 -1
  89. package/dist-lib/swift.cjs +443 -173
  90. package/dist-lib/vite-swift.cjs +1 -1
  91. package/dist-lib/vite.cjs +1 -1
  92. package/package.json +2 -1
  93. package/skills/contrast/SKILL.md +1 -0
  94. package/src/cloud-test-args.ts +18 -6
  95. package/src/home-paths.ts +19 -9
  96. package/src/menu.ts +11 -0
package/README.md CHANGED
@@ -254,7 +254,15 @@ through an unreachable IPv4 address. `peach open` starts a local bridge when no
254
254
  daemon is reachable. The background service is strongly recommended for local
255
255
  agent work because inspect, interaction, and test commands reuse one ready bridge
256
256
  and runtime. Enable it at any time with `peach daemon install`; it is never required
257
- in CI. The packaged Desktop app is optional and available through first-run
257
+ in CI.
258
+
259
+ Sandboxed or agent runners may stop child processes when a command ends,
260
+ including a bridge started by `peach open`. Keep `peach serve` running in a
261
+ persistent terminal or background task outside that command's lifetime, then
262
+ run `peach open` and later commands against it. When using a custom bridge port,
263
+ pass the same `--port <n>` to `peach serve` and the CLI commands.
264
+
265
+ The packaged Desktop app is optional and available through first-run
258
266
  onboarding or `peach desktop install`. A downloaded app starts its own bundled
259
267
  daemon on first launch. An installed runtime can open while the daemon checks
260
268
  for updates; a first install waits for the runtime download to finish. The app
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
  }
@@ -2710,38 +2710,45 @@ export class SootSimBridgeFlowRunner {
2710
2710
  }
2711
2711
 
2712
2712
  try {
2713
- // per-step watchdog: abort if nothing forward-progresses for 10s.
2714
- // catches dead bundles / frozen bridges / hung ws calls. explicit
2715
- // wait steps (extendedWaitUntil, waitFor) opt out and keep their
2716
- // own timeout — the user asked for that time; don't override.
2717
- const hasExplicitWait = !!(
2718
- step.extendedWaitUntil ||
2719
- step.waitFor ||
2720
- step.scrollUntilVisible ||
2721
- step.launchApp ||
2722
- step.runFlow ||
2723
- // retry reruns its body on failure, so its wall time is the body's
2724
- // times the retry count — the 10s no-progress watchdog is the wrong
2725
- // budget and would report "bridge probably hung" for a slow retry.
2726
- step.retry ||
2727
- // runScript http calls are synchronous with a 5-minute upstream
2728
- // timeout — the 10s watchdog would kill legitimate setup scripts.
2729
- step.runScript ||
2730
- // takeScreenshot self-governs: it retries a bounded visual settle that
2731
- // already fails with its own reason. sharing the watchdog's 10s budget
2732
- // meant the watchdog always won that race, reporting "bridge probably
2733
- // hung" for a screen that was merely still animating.
2734
- step.takeScreenshot ||
2735
- // assertions wait out maestro's lookup window, longer than 10s, and
2736
- // every query they make times out on its own, so they report their
2737
- // own failure instead of the watchdog's guess.
2738
- step.assertVisible ||
2739
- 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
+ )
2740
2747
  )
2741
2748
  const STEP_WATCHDOG_MS = 10_000
2742
2749
  const body = this.runStepInner(step)
2743
2750
  let screenshotPath: string | undefined
2744
- if (hasExplicitWait) {
2751
+ if (hasBoundedWait) {
2745
2752
  const result = await body
2746
2753
  if (typeof result === 'string') screenshotPath = result
2747
2754
  } else {
@@ -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)
@@ -73,7 +73,9 @@ import {
73
73
  } from '../current-sim'
74
74
  import { getDriver } from '../drivers/registry'
75
75
  import { openUrl as openUrlInBrowser } from '../open-url'
76
+ import { diagnosticsAllowedForCurrentProcess } from '../privacy'
76
77
  import { rethrowIfExit, rnxExit } from '../run-rnx'
78
+ import { collectRuntimeDiagnostics } from '../runtime-diagnostics'
77
79
  import { rnxSelfInvocation } from '../self-invocation'
78
80
  import {
79
81
  BridgeSimLockedError,
@@ -695,11 +697,13 @@ async function filterReachableAppFonts(
695
697
  }
696
698
  }),
697
699
  )
698
- 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>()
699
703
  for (const spec of results) {
700
- if (spec !== null) reachable.push(spec)
704
+ if (spec !== null && !reachable.has(spec.family)) reachable.set(spec.family, spec)
701
705
  }
702
- return reachable
706
+ return [...reachable.values()]
703
707
  }
704
708
 
705
709
  async function resolveShellUrl(
@@ -1293,7 +1297,8 @@ function printAgentListenHint(quiet: boolean): void {
1293
1297
  // a splash; the caller can `peach describe` again a second later. for
1294
1298
  // warm loads it's the real screen. guarded by --no-describe.
1295
1299
  async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: string[]) {
1296
- if (args.includes('--no-describe')) return
1300
+ const noDescribe = args.includes('--no-describe')
1301
+ if (noDescribe && !diagnosticsAllowedForCurrentProcess()) return
1297
1302
  const prevQuiet = process.env.SOOTSIM_QUIET_TARGET_NOTICE
1298
1303
  process.env.SOOTSIM_QUIET_TARGET_NOTICE = '1'
1299
1304
  try {
@@ -1302,7 +1307,7 @@ async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: strin
1302
1307
  // seconds on a still-routing guest app, which is exactly what we want
1303
1308
  // to skip for the open-side describe. caller can `peach describe`
1304
1309
  // again later for post-routing state.
1305
- await waitForTreeStable(wsPort, simId, { stableMs: 150, maxMs: 400 })
1310
+ if (!noDescribe) await waitForTreeStable(wsPort, simId, { stableMs: 150, maxMs: 400 })
1306
1311
  const bridge = createBridge(wsPort, {
1307
1312
  commandTimeoutMs: 3000,
1308
1313
  simId,
@@ -1310,6 +1315,7 @@ async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: strin
1310
1315
  simIdSource: 'flag',
1311
1316
  })
1312
1317
  try {
1318
+ if (noDescribe) return
1313
1319
  let readyProbe: Awaited<ReturnType<typeof inspectWaitReady>> | null = null
1314
1320
  try {
1315
1321
  readyProbe = await inspectWaitReady(bridge, 1)
@@ -1341,6 +1347,7 @@ async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: strin
1341
1347
  console.log('')
1342
1348
  console.log(result.tree)
1343
1349
  } finally {
1350
+ await collectRuntimeDiagnostics(bridge)
1344
1351
  bridge.close()
1345
1352
  }
1346
1353
  } catch {