peachsim 0.1.1640 → 0.1.1644

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 (125) hide show
  1. package/README.md +5 -0
  2. package/cli/app-bundle-resources.ts +57 -0
  3. package/cli/app-stubs.ts +38 -13
  4. package/cli/cloud-session.ts +16 -0
  5. package/cli/commands/assert.ts +17 -4
  6. package/cli/commands/box.ts +33 -19
  7. package/cli/commands/control.ts +12 -0
  8. package/cli/commands/doctor.ts +24 -15
  9. package/cli/commands/inspect/actions.ts +3 -2
  10. package/cli/commands/inspect/core.ts +0 -8
  11. package/cli/commands/inspect.ts +0 -14
  12. package/cli/commands/platform.ts +2 -1
  13. package/cli/commands/record.ts +64 -28
  14. package/cli/commands/swift.ts +10 -2
  15. package/cli/commands/three-mode.ts +4 -38
  16. package/cli/commands/upload.ts +8 -8
  17. package/cli/drivers/playwright.ts +39 -0
  18. package/cli/flow-live-status.ts +0 -2
  19. package/cli/main.ts +1 -1
  20. package/cli/outbound-endpoints.ts +4 -2
  21. package/cli/ws-bridge.ts +0 -6
  22. package/detox/attempt-events.cjs +41 -5
  23. package/detox/index.ts +155 -109
  24. package/detox/jest-environment.cjs +5 -1
  25. package/detox/motion-change.cjs +7 -5
  26. package/detox/motion-change.d.cts +1 -0
  27. package/detox/motion-leg-starts.cjs +20 -0
  28. package/detox/motion-leg-starts.d.cts +8 -0
  29. package/dist-lib/agent-daemon-client.cjs +1 -1
  30. package/dist-lib/agent-events.cjs +1 -1
  31. package/dist-lib/attached-projects.cjs +1 -1
  32. package/dist-lib/auth/shared-session.cjs +1 -1
  33. package/dist-lib/backend-origin.cjs +12 -3
  34. package/dist-lib/beta.cjs +1 -1
  35. package/dist-lib/beta.mjs +1 -1
  36. package/dist-lib/bridge-constants.cjs +1 -1
  37. package/dist-lib/bridge-contract-input.cjs +1 -1
  38. package/dist-lib/bridge-contract-input.mjs +1 -1
  39. package/dist-lib/bridge-contract.cjs +1 -1
  40. package/dist-lib/bridge-contract.mjs +1 -1
  41. package/dist-lib/bundle-channel.cjs +2671 -1833
  42. package/dist-lib/capture-contract.cjs +1 -1
  43. package/dist-lib/capture-contract.mjs +1 -1
  44. package/dist-lib/cloud-contract.cjs +1 -1
  45. package/dist-lib/cloud-contract.mjs +1 -1
  46. package/dist-lib/cloud-test-args.cjs +1 -1
  47. package/dist-lib/cloud-test-args.mjs +1 -1
  48. package/dist-lib/cloud.cjs +1 -1
  49. package/dist-lib/cloud.mjs +1 -1
  50. package/dist-lib/config.cjs +1 -1
  51. package/dist-lib/config.mjs +1 -1
  52. package/dist-lib/detox/index.cjs +578 -130
  53. package/dist-lib/detox/jest-environment.cjs +5 -1
  54. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  55. package/dist-lib/expo-swift-bundle.cjs +2374 -1554
  56. package/dist-lib/expo-swift-metadata.cjs +1 -1
  57. package/dist-lib/expo-swift-metadata.mjs +1 -1
  58. package/dist-lib/home-paths.cjs +1 -1
  59. package/dist-lib/host/bridge-host.cjs +2968 -2141
  60. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  61. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  62. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  63. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  64. package/dist-lib/host/websocket-proxy.cjs +1 -1
  65. package/dist-lib/index.cjs +132 -57
  66. package/dist-lib/jump-to-source-babel.cjs +1 -1
  67. package/dist-lib/jump-to-source-native.cjs +1 -1
  68. package/dist-lib/kotlin-host.cjs +1 -1
  69. package/dist-lib/menu.cjs +14 -6
  70. package/dist-lib/menu.mjs +14 -6
  71. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  72. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  73. package/dist-lib/metro-production-bundle.cjs +1 -1
  74. package/dist-lib/metro-production-bundle.mjs +1 -1
  75. package/dist-lib/metro.cjs +120 -53
  76. package/dist-lib/native-app-identity-project.cjs +96 -44
  77. package/dist-lib/native-app-identity-project.mjs +95 -44
  78. package/dist-lib/native-app-identity.cjs +1 -1
  79. package/dist-lib/native-app-identity.mjs +1 -1
  80. package/dist-lib/native-firebase-metadata.cjs +1 -1
  81. package/dist-lib/native-firebase-metadata.mjs +1 -1
  82. package/dist-lib/plist.cjs +1 -1
  83. package/dist-lib/plist.mjs +1 -1
  84. package/dist-lib/profiles.cjs +1 -1
  85. package/dist-lib/public-brand.cjs +1 -1
  86. package/dist-lib/react-native-host-modules.cjs +1 -1
  87. package/dist-lib/react-native-host-modules.mjs +1 -1
  88. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  89. package/dist-lib/sdk.cjs +5 -3
  90. package/dist-lib/sdk.mjs +5 -3
  91. package/dist-lib/skills.cjs +3932 -2868
  92. package/dist-lib/swift-compile.cjs +587 -374
  93. package/dist-lib/swift-compile.mjs +595 -369
  94. package/dist-lib/swift-package.cjs +1 -1
  95. package/dist-lib/swift-package.mjs +1 -1
  96. package/dist-lib/swift-submit.cjs +8 -3
  97. package/dist-lib/swift-submit.mjs +8 -3
  98. package/dist-lib/swift-toolchain.cjs +1 -1
  99. package/dist-lib/swift.cjs +1 -1
  100. package/dist-lib/vite-swift.cjs +2263 -1467
  101. package/dist-lib/vite.cjs +1 -1
  102. package/dist-lib/worklet-executable.cjs +1 -1
  103. package/dist-lib/worklet-executable.mjs +1 -1
  104. package/package.json +1 -1
  105. package/src/appBundleResourcesContract.ts +51 -0
  106. package/src/backend-origin.ts +11 -1
  107. package/src/host/bridge-host.ts +17 -11
  108. package/src/host/staged-host-files.ts +28 -4
  109. package/src/launchMetadataContract.ts +1 -0
  110. package/src/menu.ts +12 -4
  111. package/src/native-app-identity-project.ts +47 -38
  112. package/src/react-native-bridge-modules.ts +89 -76
  113. package/src/runtime-delivery.ts +6 -7
  114. package/src/swift-compile.ts +150 -112
  115. package/src/swift-compiler-digest.ts +5 -3
  116. package/src/swift-distribution.lock.json +9 -8
  117. package/src/swift-host.ts +6 -4
  118. package/src/swift-objc-compile.ts +416 -194
  119. package/src/swift-objc-metadata.ts +204 -0
  120. package/src/swift-objc-sdk.ts +195 -2
  121. package/src/swift-package-manifest.ts +6 -3
  122. package/src/swift-root-lock.ts +302 -0
  123. package/src/swift-submit.ts +15 -2
  124. package/src/swiftContract.ts +12 -0
  125. package/src/xcode-project.ts +77 -7
@@ -168,6 +168,7 @@ const RECORD_VALUE_FLAGS = new Set([
168
168
  '--repo',
169
169
  '--scene',
170
170
  '--shell-url',
171
+ '--timeout',
171
172
  ])
172
173
  const RECORD_SUBCOMMANDS = new Set([
173
174
  'start',
@@ -924,8 +925,6 @@ async function recordStart(args: string[], opts: RecordOptions) {
924
925
  peachExit(1)
925
926
  }
926
927
  } else {
927
- await assertRecorderAvailable(bridge)
928
- await assertTabVisibleForCapture(bridge)
929
928
  const lockstep = args.includes('--lockstep')
930
929
  if (lockstep && format !== 'mp4') {
931
930
  console.error(' --lockstep requires --format mp4 (webm is realtime-clocked)')
@@ -942,7 +941,11 @@ async function recordStart(args: string[], opts: RecordOptions) {
942
941
  writeRecordingState(session)
943
942
  const result: { ok: boolean; error?: string } = await bridge.send({
944
943
  type: 'evaluate',
945
- code: `window.__sootsimRecorder.start(${JSON.stringify(startOpts)})`,
944
+ code: `(() => {
945
+ if (!window.__sootsimRecorder) return { ok: false, error: 'recorder unavailable in this sim' };
946
+ if (document.hidden) return { ok: false, error: 'the peach tab is hidden; bring it to the foreground before recording' };
947
+ return window.__sootsimRecorder.start(${JSON.stringify(startOpts)});
948
+ })()`,
946
949
  })
947
950
  if (!result.ok) {
948
951
  clearRecordingState()
@@ -1022,7 +1025,13 @@ async function recordPreludeStart(args: string[], opts: RecordOptions) {
1022
1025
  // - the event recorder alone drives hidden PR-preview preludes before a
1023
1026
  // visible live/combined segment exists
1024
1027
  // returns null when the engine can't be reached.
1025
- type EngineRecordingState = 'idle' | 'recording' | 'processing' | 'prelude'
1028
+ type EngineRecordingState =
1029
+ | 'idle'
1030
+ | 'starting'
1031
+ | 'recording'
1032
+ | 'processing'
1033
+ | 'ready'
1034
+ | 'prelude'
1026
1035
 
1027
1036
  async function probeEngineRecordingState(
1028
1037
  bridge: WsBridge,
@@ -1043,8 +1052,10 @@ async function probeEngineRecordingState(
1043
1052
  })()`,
1044
1053
  })
1045
1054
  if (!snap) return null
1055
+ if (snap.headless === 'starting') return 'starting'
1046
1056
  if (snap.store === 'recording' || snap.headless === 'recording') return 'recording'
1047
- if (snap.store === 'processing') return 'processing'
1057
+ if (snap.store === 'processing' || snap.headless === 'processing') return 'processing'
1058
+ if (snap.headless === 'ready') return 'ready'
1048
1059
  if (snap.eventRecording) return 'prelude'
1049
1060
  if (snap.store === 'idle' || snap.headless === 'idle') return 'idle'
1050
1061
  return null
@@ -1089,26 +1100,36 @@ async function cancelEngineRecording(bridge: WsBridge): Promise<{
1089
1100
 
1090
1101
  async function recordStatus(args: string[], opts: RecordOptions) {
1091
1102
  const state = readRecordingState()
1092
- if (state) {
1093
- noteSimMismatch(parseBridgeCliArgs(args, { port: opts.port }).simId, state)
1094
- if (state.mode === 'video') {
1095
- console.log(
1096
- ` recording ${state.mode} (${state.format} @ ${state.fps}fps) on sim ${state.simId ?? '?'} since ${state.startedAt}`,
1097
- )
1098
- } else {
1099
- console.log(
1100
- ` recording ${state.mode} on sim ${state.simId ?? '?'} since ${state.startedAt}`,
1101
- )
1102
- }
1103
- return
1104
- }
1105
- // no `record start` session — but an atomic run or the rail button may
1106
- // still be recording. ask the engine before claiming nothing is happening.
1107
- const parsed = parseBridgeCliArgs(args, { port: opts.port, commandTimeoutMs: 8_000 })
1108
- const bridge = createBridgeFromParsed(parsed)
1103
+ const parsed = parseBridgeCliArgs(args, {
1104
+ port: opts.port,
1105
+ commandTimeoutMs: 8_000,
1106
+ })
1107
+ if (state) noteSimMismatch(parsed.simId, state)
1108
+ const bridge = createBridgeFromParsed({
1109
+ ...parsed,
1110
+ simId: state?.simId ?? parsed.simId,
1111
+ })
1109
1112
  try {
1110
1113
  const engineState = await probeEngineRecordingState(bridge)
1114
+ if (state) {
1115
+ if (engineState === 'idle') {
1116
+ clearRecordingState()
1117
+ console.log(' no recording in progress')
1118
+ } else if (engineState === 'ready') {
1119
+ console.log(' recording ready to save — run `peach record stop --output <path>`')
1120
+ } else if (engineState === null) {
1121
+ console.log(
1122
+ ' recording state unavailable — the sim did not respond; the tracked session is preserved',
1123
+ )
1124
+ } else {
1125
+ console.log(
1126
+ ` ${engineState} ${state.mode}${state.mode === 'video' ? ` (${state.format} @ ${state.fps}fps)` : ''} on sim ${state.simId ?? '?'} since ${state.startedAt}`,
1127
+ )
1128
+ }
1129
+ return
1130
+ }
1111
1131
  if (
1132
+ engineState === 'starting' ||
1112
1133
  engineState === 'recording' ||
1113
1134
  engineState === 'processing' ||
1114
1135
  engineState === 'prelude'
@@ -1154,8 +1175,10 @@ async function recordCancel(args: string[], opts: RecordOptions) {
1154
1175
  try {
1155
1176
  const engineState = await probeEngineRecordingState(probeBridge)
1156
1177
  if (
1178
+ engineState === 'starting' ||
1157
1179
  engineState === 'recording' ||
1158
1180
  engineState === 'processing' ||
1181
+ engineState === 'ready' ||
1159
1182
  engineState === 'prelude'
1160
1183
  ) {
1161
1184
  // `record cancel` is explicit intent: the caller wants whatever is
@@ -1204,7 +1227,11 @@ async function recordCancel(args: string[], opts: RecordOptions) {
1204
1227
  } else {
1205
1228
  await bridge.send({
1206
1229
  type: 'evaluate',
1207
- code: `window.__sootsimRecorder.stop()`,
1230
+ code: `(async () => {
1231
+ const recorder = window.__sootsimRecorder;
1232
+ await recorder.stop();
1233
+ return recorder.forceRelease();
1234
+ })()`,
1208
1235
  })
1209
1236
  }
1210
1237
  } catch {
@@ -1241,7 +1268,15 @@ async function recordStop(args: string[], opts: RecordOptions) {
1241
1268
  const bridge = createBridgeFromParsed({ ...parsed, simId })
1242
1269
  try {
1243
1270
  if (state.mode === 'live' || state.mode === 'combined') {
1244
- if (!(await evalStoreStop(bridge))) {
1271
+ const stopped = await evalStoreStop(bridge)
1272
+ if (stopped === 'pending') {
1273
+ console.error(
1274
+ ` stop failed: the ${state.mode} start is still in flight and has not begun a take yet.\n` +
1275
+ ' run `peach record stop` again once it begins.',
1276
+ )
1277
+ peachExit(1)
1278
+ }
1279
+ if (!stopped) {
1245
1280
  clearRecordingState()
1246
1281
  console.error(
1247
1282
  ` stop failed: the ${state.mode} start still in flight never began a take, so there is nothing to upload.\n` +
@@ -1444,13 +1479,14 @@ async function evalStoreStart(
1444
1479
 
1445
1480
  // the engine stop waits for a start still in flight. false means that start
1446
1481
  // never began a take, so no upload will come and the store's last one belongs
1447
- // to an earlier take.
1448
- async function evalStoreStop(bridge: WsBridge): Promise<boolean> {
1449
- const stopped: boolean | null = await bridge.send({
1482
+ // to an earlier take. 'pending' means the start was still pending when the
1483
+ // engine stopped waiting, so it may yet begin one.
1484
+ async function evalStoreStop(bridge: WsBridge): Promise<boolean | 'pending'> {
1485
+ const stopped: boolean | 'pending' | null = await bridge.send({
1450
1486
  type: 'evaluate',
1451
1487
  code: `window.SootSim.bridges.stopRecording()`,
1452
1488
  })
1453
- return stopped !== false
1489
+ return stopped === 'pending' ? stopped : stopped !== false
1454
1490
  }
1455
1491
 
1456
1492
  interface StoreUploadResult {
@@ -165,7 +165,15 @@ export async function runSwift(
165
165
  const appProject = resolveAppProject(packagePath)
166
166
  host.configureSwiftStandinBehavior(packagePath, await loadRNXAppConfig(appProject))
167
167
  // collection preflights every input path before any source bytes are read.
168
- host.swiftPackageWatchFiles(packagePath, undefined)
168
+ try {
169
+ host.swiftPackageWatchFiles(packagePath, undefined)
170
+ } catch (error) {
171
+ const { SwiftObjCCompilationError } = await import('../../src/swift-objc-compile')
172
+ if (!(error instanceof SwiftObjCCompilationError)) throw error
173
+ console.error(error.message)
174
+ host.disposeSwiftProject(packagePath)
175
+ return 1
176
+ }
169
177
  const packageName = path.basename(packagePath)
170
178
  const appInfo = host.swiftProjectAppInfo(packagePath)
171
179
  const guestSource = swiftGuestBundle(packageName)
@@ -354,7 +362,7 @@ export async function runSwift(
354
362
  await refresh()
355
363
  controller.signal.throwIfAborted()
356
364
  // refresh already printed the error
357
- if (!metadata || compileError) throw new Error('Swift compilation failed, see above')
365
+ if (!metadata || compileError) return 1
358
366
  if (opts.device) openArgs.push('--device', opts.device)
359
367
  if (opts.driver) openArgs.push('--driver', opts.driver)
360
368
  if (opts.headless) openArgs.push('--headless')
@@ -6,6 +6,10 @@
6
6
  import { createHash } from 'node:crypto'
7
7
  import { mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs'
8
8
  import { dirname, extname, resolve } from 'node:path'
9
+ import {
10
+ collectTimelineAssetRefs,
11
+ timelineAssetRefOf,
12
+ } from 'peach-engine/screenshots/timeline-assets'
9
13
  import {
10
14
  isThreeModeSetting,
11
15
  THREE_MODE_SETTING_IDS,
@@ -353,44 +357,6 @@ function mimeTypeForAsset(path: string, contentType: unknown) {
353
357
  // timeline as blob: URLs.
354
358
  const TIMELINE_ASSET_CHUNK_BYTES = 1_048_576
355
359
 
356
- type TimelineAssetRef = {
357
- src: string
358
- recording: boolean
359
- }
360
-
361
- function timelineAssetRefOf(value: Record<string, unknown>): TimelineAssetRef | null {
362
- if (
363
- (value.type === 'recording' ||
364
- value.type === 'screenshot' ||
365
- value.kind === 'screen') &&
366
- typeof value.src === 'string' &&
367
- !/^(?:blob:|data:|https?:)/i.test(value.src)
368
- ) {
369
- return { src: value.src, recording: value.type === 'recording' }
370
- }
371
- return null
372
- }
373
-
374
- function collectTimelineAssetRefs(value: unknown): TimelineAssetRef[] {
375
- const refs: TimelineAssetRef[] = []
376
- const seen = new Set<string>()
377
- const visit = (node: unknown): void => {
378
- if (Array.isArray(node)) {
379
- for (const entry of node) visit(entry)
380
- return
381
- }
382
- if (!isRecord(node)) return
383
- for (const entry of Object.values(node)) visit(entry)
384
- const ref = timelineAssetRefOf(node)
385
- if (ref && !seen.has(ref.src)) {
386
- seen.add(ref.src)
387
- refs.push(ref)
388
- }
389
- }
390
- visit(value)
391
- return refs
392
- }
393
-
394
360
  type DistinctTimelineAsset = {
395
361
  hash: string
396
362
  mime: string
@@ -522,7 +522,7 @@ export async function runUpload(
522
522
  bridge,
523
523
  '(typeof window.__sootsimCaptureBundle === "function") ? window.__sootsimCaptureBundle() : null',
524
524
  )
525
- if (snapshot?.bundleUrl) {
525
+ if (hasBundle && snapshot?.bundleUrl) {
526
526
  const bundleOrigin = new URL(snapshot.bundleUrl).origin
527
527
  // manifest stays the body-less re-fetch path for bundle-origin static
528
528
  // assets (chunks, fonts, wasm). bundle-origin api endpoints would also
@@ -784,7 +784,7 @@ export async function runUpload(
784
784
  .join('\n')
785
785
  const timelineGz = gzipSync(traceTimelineJsonl ? `${traceTimelineJsonl}\n` : '')
786
786
  const storageGz =
787
- opts.storageSnapshot && hasShareableStorage(opts.storageSnapshot)
787
+ hasBundle && opts.storageSnapshot && hasShareableStorage(opts.storageSnapshot)
788
788
  ? gzipSync(JSON.stringify(opts.storageSnapshot))
789
789
  : undefined
790
790
  console.log(
@@ -971,10 +971,9 @@ export async function runUpload(
971
971
  // fetcher (same code path as the build's `peach app-fonts stage`) so both
972
972
  // pipelines store identical bytes keyed by sha256(url).
973
973
  const appFontFiles: PreparedFile[] = []
974
- const appFontsWire = await resolveAppFontsWire(
975
- appFontsWireFromPageHref(snapshot.pageHref),
976
- fetch,
977
- )
974
+ const appFontsWire = hasBundle
975
+ ? await resolveAppFontsWire(appFontsWireFromPageHref(snapshot.pageHref), fetch)
976
+ : ''
978
977
  if (appFontsWire) {
979
978
  const fonts = await fetchAppFontFiles(appFontsWire, {
980
979
  onStaged: ({ url, byteLength }) =>
@@ -1003,7 +1002,7 @@ export async function runUpload(
1003
1002
  // still-live metro and persist them keyed by sha256(url). the engine's
1004
1003
  // <img src> resolves them through the share fetch proxy in preview mode.
1005
1004
  const appSplashFiles: PreparedFile[] = []
1006
- const appSplashWire = appSplashWireFromPageHref(snapshot.pageHref) || ''
1005
+ const appSplashWire = hasBundle ? appSplashWireFromPageHref(snapshot.pageHref) : ''
1007
1006
  if (appSplashWire) {
1008
1007
  const images = await fetchAppSplashImages(appSplashWire, {
1009
1008
  onStaged: ({ url, byteLength }) =>
@@ -1033,7 +1032,8 @@ export async function runUpload(
1033
1032
  // explicitly so the installed app keeps its identity in share replay.
1034
1033
  const appIdentityFiles: PreparedFile[] = []
1035
1034
  let appManifest: string | undefined
1036
- const metroOrigin = snapshot.bundleUrl ? loopbackOriginOf(snapshot.bundleUrl) : null
1035
+ const metroOrigin =
1036
+ hasBundle && snapshot.bundleUrl ? loopbackOriginOf(snapshot.bundleUrl) : null
1037
1037
  if (metroOrigin && snapshot.bundleUrl) {
1038
1038
  const platform = new URL(snapshot.bundleUrl).searchParams.get('platform') || 'ios'
1039
1039
  const manifestUrl = `${metroOrigin}/`
@@ -18,6 +18,7 @@ import { createServer } from 'net'
18
18
  import { tmpdir } from 'os'
19
19
  import { join } from 'path'
20
20
  import { AUTOMATION_SIM_IDLE_REAP_TTL_MS } from '../../src/bridge-constants'
21
+ import { getCliVersion } from '../../src/cli-version'
21
22
  import {
22
23
  desktopHostDelivery,
23
24
  desktopHostPlatform,
@@ -54,6 +55,38 @@ function probe(): DriverAvailability {
54
55
  }
55
56
  }
56
57
 
58
+ // every bridge since the fetch transport landed serves its route, so a 404
59
+ // from a server whose /healthz identifies a peach bridge means that bridge
60
+ // predates this cli. without that identity the port may hold something else,
61
+ // and the message says only what was observed. restarting closes every sim
62
+ // the bridge hosts, so the cli never does it.
63
+ async function olderBridgeMessage(bridgePort: number): Promise<string> {
64
+ const restart = `run \`peach daemon restart\` to load the matching bridge; it closes every sim that bridge hosts, so it was not restarted.`
65
+ let health: unknown = null
66
+ try {
67
+ const response = await fetch(`http://127.0.0.1:${bridgePort}/healthz`, {
68
+ signal: AbortSignal.timeout(10_000),
69
+ })
70
+ health = await response.json()
71
+ } catch {
72
+ // an unreadable /healthz leaves the server unidentified
73
+ }
74
+ const field = (key: string) =>
75
+ health && typeof health === 'object' ? Reflect.get(health, key) : null
76
+ const pid = field('pid')
77
+ if (field('ok') !== true || typeof pid !== 'number') {
78
+ return `port ${bridgePort} has no fetch transport route and its /healthz did not identify a peach bridge, so this peach cli ${getCliVersion()} cannot use it. if an older peach bridge holds that port, ${restart}`
79
+ }
80
+ const runtime = field('activeRuntime')
81
+ const details = [
82
+ `pid ${pid}`,
83
+ typeof runtime === 'string' ? `runtime ${runtime}` : null,
84
+ ]
85
+ .filter((part) => part !== null)
86
+ .join(', ')
87
+ return `the peach bridge on port ${bridgePort} (${details}) has no fetch transport route, so it predates this peach cli ${getCliVersion()}. ${restart}`
88
+ }
89
+
57
90
  // resolve the Chrome user-data dir for a profiled launch. a non-empty
58
91
  // return means the host should use a persistent context; '' means a plain
59
92
  // (in-memory) browser launch.
@@ -111,6 +144,12 @@ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
111
144
  `http://127.0.0.1:${opts.bridgePort}/__peach/fetch-transport`,
112
145
  { signal: AbortSignal.timeout(10_000) },
113
146
  )
147
+ if (response.status === 404) {
148
+ return {
149
+ launched: false,
150
+ message: await olderBridgeMessage(opts.bridgePort),
151
+ }
152
+ }
114
153
  if (!response.ok) throw new Error(`transport discovery returned ${response.status}`)
115
154
  const descriptor: unknown = await response.json()
116
155
  if (!descriptor || typeof descriptor !== 'object')
@@ -13,8 +13,6 @@ export type LiveStepStatus = 'running' | 'success' | 'failure' | 'skipped'
13
13
 
14
14
  type PlanStep = { index: number; name: string; target?: string }
15
15
 
16
- const PAUSE_POLL_MS = 400
17
-
18
16
  function replyPaused(reply: unknown) {
19
17
  if (typeof reply !== 'object' || reply == null || !('control' in reply)) return false
20
18
  const control = reply.control
package/cli/main.ts CHANGED
@@ -466,7 +466,7 @@ try {
466
466
  // inner verb's --json payload, so it never needs the shared bridge
467
467
  // machinery that lives inside runInspect.
468
468
  const { runAssert } = await import('./commands/assert')
469
- await runAssert(parsed.commandArgs)
469
+ await runAssert(parsed.commandArgs, { port, sim: globalSimTarget })
470
470
  break
471
471
  }
472
472
 
@@ -38,7 +38,7 @@ export const OUTBOUND_ENDPOINTS: Record<string, OutboundEndpointDeclaration> = {
38
38
  runtime_delivery: {
39
39
  category: 'runtime-delivery',
40
40
  destination:
41
- 'https://contrast.dev/runtimes/manifest.json and selected runtime tarball; with `peach desktop install --native`, https://r2.rnxsim.com/releases/native/<channel>/ manifest and versioned host zip',
41
+ 'https://peachsim.com/runtimes/manifest.json and selected runtime tarball; with `peach desktop install --native`, https://r2.rnxsim.com/releases/native/<channel>/ manifest and versioned host zip',
42
42
  preConsent: true,
43
43
  payload: 'HTTP runtime or desktop host version request; no project or app data',
44
44
  },
@@ -107,7 +107,7 @@ export const OUTBOUND_ENDPOINTS: Record<string, OutboundEndpointDeclaration> = {
107
107
  },
108
108
  release_notes: {
109
109
  category: 'explicit-user-action',
110
- destination: 'https://contrast.dev/api/changelog/sootsim',
110
+ destination: 'https://peachsim.com/api/changelog/sootsim',
111
111
  preConsent: false,
112
112
  payload: 'requested runtime version only',
113
113
  },
@@ -639,6 +639,8 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
639
639
  { category: 'local_development', count: 1 },
640
640
  'packages/peachsim/cli/drivers/playwright.ts :: fetch :: `http://127.0.0.1:${opts.bridgePort}/__peach/fetch-transport`':
641
641
  { category: 'local_development', count: 1 },
642
+ 'packages/peachsim/cli/drivers/playwright.ts :: fetch :: `http://127.0.0.1:${bridgePort}/healthz`':
643
+ { category: 'local_development', count: 1 },
642
644
  "packages/peach-engine/src/screenshots/capture-store.ts :: fetch :: captureApiUrl(ref, 'grid')":
643
645
  { category: 'contrast_user_action', count: 1 },
644
646
  'packages/peach-engine/src/screenshots/capture-store.ts :: fetch :: captureApiUrl(ref, variant)':
package/cli/ws-bridge.ts CHANGED
@@ -374,12 +374,6 @@ export class BridgeSimLockedError extends Error {
374
374
  }
375
375
  }
376
376
 
377
- interface WsResponse {
378
- id: number
379
- result?: any
380
- error?: string
381
- }
382
-
383
377
  interface WsBridgeOptions {
384
378
  connectTimeoutMs?: number
385
379
  commandTimeoutMs?: number
@@ -318,6 +318,10 @@ function writeDiscreteFrameReceipt(
318
318
  }
319
319
 
320
320
  function authenticateNativeCadenceCensus(root, sourceRoot, life, decoded) {
321
+ const {
322
+ authenticateNativeRefreshSamples,
323
+ } = require('../../peach-conformance/e2e/utils/native-frame-receipt.cjs')
324
+ authenticateNativeRefreshSamples(root, sourceRoot, life)
321
325
  const native = life.nativeFramebuffer
322
326
  if (
323
327
  !native ||
@@ -344,13 +348,13 @@ function authenticateNativeCadenceCensus(root, sourceRoot, life, decoded) {
344
348
  root,
345
349
  native.mediaFrames,
346
350
  native.mediaFramesSha256,
347
- 'native cadence presentation census',
351
+ 'native cadence sampled surface census',
348
352
  )
349
353
  .trim()
350
354
  .split('\n')
351
355
  .map(JSON.parse)
352
356
  if (frames.length !== decoded.observations.length || media.length !== frames.length)
353
- throw new Error('native cadence acquisition and presentation census differ')
357
+ throw new Error('native cadence acquisition and sampled surface census differ')
354
358
  const {
355
359
  decodeNativeFramebuffer,
356
360
  requireNativeRawFrame,
@@ -377,13 +381,25 @@ function authenticateNativeCadenceCensus(root, sourceRoot, life, decoded) {
377
381
  return row.callbackIndex
378
382
  }),
379
383
  )
380
- const origin = frames[0].consumerArrival
384
+ const reads = boundText(
385
+ root,
386
+ native.reads,
387
+ native.readsSha256,
388
+ 'native cadence observations',
389
+ )
390
+ .split('\n')
391
+ .filter(Boolean)
392
+ .map(JSON.parse)
381
393
  let expectedCallback = 0
382
- for (const [index, frame] of frames.entries()) {
394
+ for (const read of reads) {
383
395
  while (superseded.delete(expectedCallback)) expectedCallback++
396
+ if (read.callbackIndex !== expectedCallback++)
397
+ throw new Error('native cadence observations have invalid callback order')
398
+ }
399
+ const origin = frames[0].consumerArrival
400
+ for (const [index, frame] of frames.entries()) {
384
401
  const clock = frame.consumerArrival
385
402
  if (
386
- frame.callbackIndex !== expectedCallback++ ||
387
403
  media[index].sourceFrameIndex !== index ||
388
404
  !clock ||
389
405
  clock.domain !== 'host-mach-absolute-time' ||
@@ -771,6 +787,26 @@ function discreteFrameEvidenceFiles(root, evidence) {
771
787
  paths.add(native.receipt)
772
788
  paths.add(native.superseded)
773
789
  paths.add(native.mediaFrames)
790
+ paths.add(native.gradingMarkers)
791
+ paths.add(native.gradingPlan)
792
+ paths.add(native.windowResolution)
793
+ paths.add(native.reads)
794
+ for (const receipt of native.finalReads) {
795
+ const text = boundText(
796
+ root,
797
+ receipt.path,
798
+ receipt.sha256,
799
+ 'native sampled end state',
800
+ )
801
+ paths.add(receipt.path)
802
+ for (const row of text.split('\n').filter(Boolean).map(JSON.parse)) {
803
+ const file = proofBoundPath(
804
+ path.dirname(proofBoundPath(root, receipt.path)),
805
+ row.rawPath,
806
+ )
807
+ paths.add(path.relative(root, file).split(path.sep).join('/'))
808
+ }
809
+ }
774
810
  const rows = fs
775
811
  .readFileSync(proofBoundPath(root, native.receipt), 'utf8')
776
812
  .trim()