peachsim 0.1.920 → 0.1.922

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 (88) hide show
  1. package/README.md +6 -1
  2. package/cli/commands/compat.ts +51 -9
  3. package/cli/commands/daemon.ts +35 -14
  4. package/cli/commands/flow.ts +7 -2
  5. package/cli/commands/maestro-generate.ts +292 -98
  6. package/cli/commands/maestro.ts +51 -29
  7. package/cli/commands/report-issue.ts +32 -16
  8. package/cli/commands/skills.ts +1 -1
  9. package/cli/commands/support.ts +3 -3
  10. package/cli/main.ts +5 -0
  11. package/cli/runtime-compat.ts +127 -0
  12. package/dist-lib/agent-daemon-client.cjs +1 -1
  13. package/dist-lib/agent-events.cjs +1 -1
  14. package/dist-lib/agent-identity.cjs +1 -1
  15. package/dist-lib/agent-sessions.cjs +1 -1
  16. package/dist-lib/attached-projects.cjs +1 -1
  17. package/dist-lib/auth/shared-session.cjs +1 -1
  18. package/dist-lib/backend-origin.cjs +1 -1
  19. package/dist-lib/beta.cjs +1 -1
  20. package/dist-lib/beta.mjs +1 -1
  21. package/dist-lib/bridge-constants.cjs +1 -1
  22. package/dist-lib/bridge-contract-input.cjs +1 -1
  23. package/dist-lib/bridge-contract-input.mjs +1 -1
  24. package/dist-lib/bridge-contract.cjs +1 -1
  25. package/dist-lib/bridge-contract.mjs +1 -1
  26. package/dist-lib/bundle-channel.cjs +1 -1
  27. package/dist-lib/capture-contract.cjs +1 -1
  28. package/dist-lib/capture-contract.mjs +1 -1
  29. package/dist-lib/cli-constants.cjs +1 -1
  30. package/dist-lib/cloud-contract.cjs +1 -1
  31. package/dist-lib/cloud-contract.mjs +1 -1
  32. package/dist-lib/cloud-test-args.cjs +1 -1
  33. package/dist-lib/cloud-test-args.mjs +1 -1
  34. package/dist-lib/cloud.cjs +1 -1
  35. package/dist-lib/cloud.mjs +1 -1
  36. package/dist-lib/config.cjs +1 -1
  37. package/dist-lib/detox/index.cjs +1 -1
  38. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  39. package/dist-lib/expo-swift-bundle.cjs +1 -1
  40. package/dist-lib/expo-swift-metadata.cjs +1 -1
  41. package/dist-lib/expo-swift-metadata.mjs +1 -1
  42. package/dist-lib/home-paths.cjs +1 -1
  43. package/dist-lib/host/bridge-host.cjs +1 -1
  44. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  45. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  46. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  47. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  48. package/dist-lib/host/websocket-proxy.cjs +1 -1
  49. package/dist-lib/index.cjs +1 -1
  50. package/dist-lib/jump-to-source-babel.cjs +1 -1
  51. package/dist-lib/jump-to-source-native.cjs +1 -1
  52. package/dist-lib/kotlin-host.cjs +1 -1
  53. package/dist-lib/menu.cjs +1 -1
  54. package/dist-lib/menu.mjs +1 -1
  55. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  56. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  57. package/dist-lib/metro-production-bundle.cjs +1 -1
  58. package/dist-lib/metro-production-bundle.mjs +1 -1
  59. package/dist-lib/metro.cjs +1 -1
  60. package/dist-lib/native-app-identity.cjs +1 -1
  61. package/dist-lib/native-app-identity.mjs +1 -1
  62. package/dist-lib/native-firebase-metadata.cjs +1 -1
  63. package/dist-lib/native-firebase-metadata.mjs +1 -1
  64. package/dist-lib/plist.cjs +1 -1
  65. package/dist-lib/plist.mjs +1 -1
  66. package/dist-lib/profiles.cjs +1 -1
  67. package/dist-lib/public-brand.cjs +1 -1
  68. package/dist-lib/react-native-host-modules.cjs +1 -1
  69. package/dist-lib/react-native-host-modules.mjs +1 -1
  70. package/dist-lib/render-mode.cjs +1 -1
  71. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  72. package/dist-lib/sdk.cjs +1 -1
  73. package/dist-lib/sdk.mjs +1 -1
  74. package/dist-lib/skills.cjs +16 -1
  75. package/dist-lib/swift-compile.cjs +1 -1
  76. package/dist-lib/swift-compile.mjs +1 -1
  77. package/dist-lib/swift-package.cjs +1 -1
  78. package/dist-lib/swift-package.mjs +1 -1
  79. package/dist-lib/swift-submit.cjs +1 -1
  80. package/dist-lib/swift-submit.mjs +1 -1
  81. package/dist-lib/swift-toolchain.cjs +1 -1
  82. package/dist-lib/swift.cjs +1 -1
  83. package/dist-lib/vite-swift.cjs +1 -1
  84. package/dist-lib/vite.cjs +1 -1
  85. package/dist-lib/worklet-executable.cjs +1 -1
  86. package/dist-lib/worklet-executable.mjs +1 -1
  87. package/package.json +1 -1
  88. package/skills/peach-test/SKILL.md +8 -1
package/README.md CHANGED
@@ -519,12 +519,17 @@ Maestro is Peach's single YAML test surface. Point it at your existing
519
519
  `.maestro/` directory, generate a test, or author one from live actions:
520
520
 
521
521
  ```sh
522
- peach maestro # discover .maestro/ and run all flows
522
+ peach maestro # discover .maestro/ workspace
523
523
  peach maestro test .maestro/ # explicit
524
524
  peach maestro generate "verify login"
525
525
  peach maestro --list-compat # see the verb support matrix
526
526
  ```
527
527
 
528
+ Directory discovery selects root YAML flows by default, as Maestro does.
529
+ For nested suites, pass their folder, such as `.maestro/flows`, or add
530
+ `config.yaml` with `flows: ["flows/**"]`. Keep shared subflows outside those
531
+ globs; `runFlow` can still load them.
532
+
528
533
  Tap settling compares consecutive full-screen pixels within Maestro's iOS
529
534
  three-second budget, while also draining screen transitions, native motion and
530
535
  image loads. Periodic caret paints do not require a continuous stillness window,
@@ -14,11 +14,13 @@ import {
14
14
  COMPAT_RESULT_CAVEAT,
15
15
  formatDetailedScanLines,
16
16
  formatScanCounts,
17
- scanDeps,
17
+ scanDepsWithData,
18
+ type CompatScanData,
18
19
  type ScanResult,
19
20
  } from '@peach/compat/web'
20
21
  import { rnxPublicBrand } from '../../src/public-brand'
21
22
  import { resolveRNXAppConfig } from '../app-config'
23
+ import { readRuntimeCompatibility } from '../runtime-compat'
22
24
  import {
23
25
  findProjectRoot,
24
26
  scanRepository,
@@ -59,14 +61,34 @@ flags:
59
61
  return
60
62
  }
61
63
 
64
+ const { config } = await resolveRNXAppConfig(appDir)
65
+ const runtime = readRuntimeCompatibility(config?.runtimeVersion)
66
+ if (!runtime.data) {
67
+ if (args.includes('--json'))
68
+ console.log(
69
+ JSON.stringify({
70
+ runtimeVersion: runtime.runtimeVersion,
71
+ overallScore: null,
72
+ error: runtime.reason,
73
+ }),
74
+ )
75
+ else console.error(` ${runtime.reason}`)
76
+ process.exitCode = 1
77
+ return
78
+ }
79
+ const data = runtime.data
80
+ const scanDeps = (manifest: ProjectManifest) => scanDepsWithData(manifest, data)
81
+ const reportRuntime = (result: ScanResult, local: LocalNative[] | null) =>
82
+ report(args, result, local, runtime.runtimeVersion)
83
+
62
84
  if (packageName) {
63
- report(args, scanOnePackage(appDir, pkg, packageName), null)
85
+ reportRuntime(scanOnePackage(appDir, pkg, packageName, runtime.data), null)
64
86
  return
65
87
  }
66
88
 
67
89
  const here = scanDeps(installedManifest(appDir, pkg))
68
90
  if (here.packages.length > 0 || appIndex >= 0) {
69
- report(args, here, await scanLocalNative(appDir, pkg))
91
+ reportRuntime(here, await scanLocalNative(appDir, pkg))
70
92
  return
71
93
  }
72
94
 
@@ -84,8 +106,9 @@ flags:
84
106
 
85
107
  if (elsewhere.length === 1) {
86
108
  const only = elsewhere[0]
87
- console.log(`\n scanning ${relative(appDir, only.dir) || '.'}`)
88
- report(args, only.result, await scanLocalNative(resolve(only.dir), only.manifest))
109
+ if (!args.includes('--json'))
110
+ console.log(`\n scanning ${relative(appDir, only.dir) || '.'}`)
111
+ reportRuntime(only.result, await scanLocalNative(resolve(only.dir), only.manifest))
89
112
  return
90
113
  }
91
114
  if (elsewhere.length > 1) {
@@ -96,13 +119,31 @@ flags:
96
119
  console.log()
97
120
  return
98
121
  }
99
- report(args, here, await scanLocalNative(appDir, pkg))
122
+ reportRuntime(here, await scanLocalNative(appDir, pkg))
100
123
  }
101
124
 
102
- function report(args: string[], result: ScanResult, local: LocalNative[] | null): void {
125
+ function report(
126
+ args: string[],
127
+ result: ScanResult,
128
+ local: LocalNative[] | null,
129
+ runtimeVersion: string | null,
130
+ ): void {
103
131
  if (args.includes('--json')) {
132
+ const overallScore = result.scoreSummary.assessed === 0 ? null : result.overallScore
104
133
  console.log(
105
- JSON.stringify(local ? { ...result, localNative: local } : result, null, 2),
134
+ JSON.stringify(
135
+ {
136
+ ...result,
137
+ runtimeVersion,
138
+ overallScore,
139
+ scoreSummary: { ...result.scoreSummary, compatibility: overallScore },
140
+ implementationEstimate:
141
+ result.scoreSummary.assessed === 0 ? result.overallScore : null,
142
+ ...(local ? { localNative: local } : {}),
143
+ },
144
+ null,
145
+ 2,
146
+ ),
106
147
  )
107
148
  return
108
149
  }
@@ -343,11 +384,12 @@ function scanOnePackage(
343
384
  appDir: string,
344
385
  pkg: ProjectManifest,
345
386
  packageName: string,
387
+ data: CompatScanData,
346
388
  ): ScanResult {
347
389
  const installed = installedManifest(appDir, pkg)
348
390
  const version =
349
391
  installed.dependencies?.[packageName] ??
350
392
  installed.devDependencies?.[packageName] ??
351
393
  '*'
352
- return scanDeps({ dependencies: { [packageName]: version } })
394
+ return scanDepsWithData({ dependencies: { [packageName]: version } }, data)
353
395
  }
@@ -223,7 +223,9 @@ export async function daemonInstall({ port, force }: { port: number; force: bool
223
223
  const path = macPlistPath()
224
224
  if (existsSync(path) && !force) {
225
225
  console.log(` already installed at ${path}`)
226
- console.log(` pass --force to overwrite, or use 'peach daemon restart' to reload`)
226
+ const pid = await startMacDaemon(path)
227
+ console.log(` registered (state: running, pid ${pid})`)
228
+ console.log(` pass --force to overwrite the service configuration`)
227
229
  return
228
230
  }
229
231
  await stopMacDaemonBridge(invocation)
@@ -321,8 +323,10 @@ function macServiceTarget(): string {
321
323
  return `gui/${process.getuid!()}/${SERVICE_LABEL_MAC}`
322
324
  }
323
325
 
324
- function readMacDaemonState(): { state: string; pid: number | null } | null {
325
- const result = spawnSync('launchctl', ['print', macServiceTarget()], {
326
+ function readMacDaemonState(
327
+ target = macServiceTarget(),
328
+ ): { state: string; pid: number | null } | null {
329
+ const result = spawnSync('launchctl', ['print', target], {
326
330
  encoding: 'utf8',
327
331
  })
328
332
  if (result.status !== 0) return null
@@ -358,8 +362,11 @@ function macBridgeHasListener(pid: number, port: number): boolean {
358
362
  return result.stdout.trim() === String(pid)
359
363
  }
360
364
 
361
- async function stopMacDaemonBridge(invocation: RnxSelfInvocation): Promise<void> {
362
- const service = readMacDaemonState()
365
+ export async function stopMacDaemonBridge(
366
+ invocation: RnxSelfInvocation,
367
+ target = macServiceTarget(),
368
+ ): Promise<void> {
369
+ const service = readMacDaemonState(target)
363
370
  const lock = readDaemonLockfile()
364
371
  if (isDaemonLockfileFresh(lock) && lock.pid !== service?.pid) {
365
372
  // a lockfile alone cannot authorize a signal after pid reuse.
@@ -380,7 +387,7 @@ async function stopMacDaemonBridge(invocation: RnxSelfInvocation): Promise<void>
380
387
  }
381
388
  process.kill(lock.pid, 'SIGTERM')
382
389
  }
383
- if (service) runLaunchctl(['bootout', macServiceTarget()])
390
+ if (service) runLaunchctl(['bootout', target])
384
391
  if (!lock) return
385
392
  const deadline = Date.now() + 4000
386
393
  while (Date.now() < deadline) {
@@ -390,22 +397,36 @@ async function stopMacDaemonBridge(invocation: RnxSelfInvocation): Promise<void>
390
397
  throw new Error(`bridge pid ${lock.pid} did not release port ${lock.bridgePort}`)
391
398
  }
392
399
 
393
- async function startMacDaemon(path: string): Promise<number> {
394
- if (readMacDaemonState()) {
395
- runLaunchctl(['kickstart', macServiceTarget()])
396
- } else {
397
- runLaunchctl(['bootstrap', `gui/${process.getuid!()}`, path])
398
- }
400
+ export async function startMacDaemon(
401
+ path: string,
402
+ invocation = resolveDaemonInvocation(),
403
+ target = macServiceTarget(),
404
+ ): Promise<number> {
405
+ const service = readMacDaemonState(target)
406
+ const lock = readDaemonLockfile()
407
+ if (
408
+ service?.state === 'running' &&
409
+ service.pid !== null &&
410
+ isDaemonLockfileFresh(lock) &&
411
+ lock.pid === service.pid &&
412
+ macBridgeHasListener(lock.pid, lock.bridgePort)
413
+ )
414
+ return service.pid
415
+ // reconcile an unloaded service or a bare bridge before launchd can create
416
+ // another process. start is idempotent when the service already owns the bridge.
417
+ await stopMacDaemonBridge(invocation, target)
418
+ runLaunchctl(['bootstrap', target.slice(0, target.lastIndexOf('/')), path])
399
419
  // registration and an xpcproxy pid precede the actual bridge bind.
400
420
  const deadline = Date.now() + 4000
401
421
  while (Date.now() < deadline) {
402
- const service = readMacDaemonState()
422
+ const service = readMacDaemonState(target)
403
423
  const lock = readDaemonLockfile()
404
424
  if (
405
425
  service?.state === 'running' &&
406
426
  service.pid !== null &&
407
427
  isDaemonLockfileFresh(lock) &&
408
- lock.pid === service.pid
428
+ lock.pid === service.pid &&
429
+ macBridgeHasListener(lock.pid, lock.bridgePort)
409
430
  )
410
431
  return service.pid
411
432
  await new Promise((resolve) => setTimeout(resolve, 100))
@@ -11,6 +11,7 @@ import { applyPeachConfigToUrl, type PeachConfig } from '../../src/config'
11
11
  import { printBridgeFailureDiagnostics } from '../bridge-diagnostics'
12
12
  import {
13
13
  SootSimBridgeFlowRunner,
14
+ type MaestroStep,
14
15
  type SootSimFlowProfileResult,
15
16
  type SootSimFlowTraceStep,
16
17
  } from '../bridge-flow-runner'
@@ -217,7 +218,10 @@ export async function runFlowPlayback(
217
218
  // beforeSteps runs once the flow is on its target and about to run its first
218
219
  // step. opening a frontmatter `app:` navigates the sim, which wipes in-page
219
220
  // state, so a caller capturing the run itself (film) starts here.
220
- hooks: { beforeSteps?: () => Promise<void> } = {},
221
+ hooks: {
222
+ beforeSteps?: () => Promise<void>
223
+ runSteps?: (driver: SootSimBridgeFlowRunner, steps: MaestroStep[]) => Promise<void>
224
+ } = {},
221
225
  ): Promise<number> {
222
226
  lastPreviewUploadResult = null
223
227
  lastPreviewUploadError = null
@@ -782,7 +786,8 @@ flow extension:
782
786
  }
783
787
  }
784
788
  await hooks.beforeSteps?.()
785
- await driver.runFlow(steps, { skipPlan: dryRun })
789
+ if (hooks.runSteps) await hooks.runSteps(driver, steps)
790
+ else await driver.runFlow(steps, { skipPlan: dryRun })
786
791
  if (profile) {
787
792
  profileResult = await driver.stopProfile()
788
793
  }