peachsim 0.1.1625 → 0.1.1635
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.
- package/LICENSE +1 -1
- package/README.md +10 -1
- package/cli/app-config.ts +4 -1
- package/cli/app-fonts.ts +37 -17
- package/cli/app-stubs.ts +224 -9
- package/cli/bridge-flow-runner.ts +64 -43
- package/cli/command-registry.ts +1 -0
- package/cli/commands/control.ts +283 -49
- package/cli/commands/doctor.ts +17 -184
- package/cli/commands/flow.ts +79 -11
- package/cli/commands/inspect/core.ts +10 -5
- package/cli/commands/inspect/settling.ts +14 -2
- package/cli/commands/inspect/wait-ready.ts +12 -0
- package/cli/commands/inspect.ts +26 -3
- package/cli/commands/install-desktop.ts +1 -0
- package/cli/commands/maestro-compare.ts +97 -38
- package/cli/commands/maestro.ts +9 -1
- package/cli/commands/record.ts +21 -1
- package/cli/commands/screenshot.ts +23 -1
- package/cli/commands/serve.ts +7 -2
- package/cli/commands/setup.ts +19 -42
- package/cli/commands/swift.ts +5 -0
- package/cli/commands/upload.ts +46 -7
- package/cli/desktop-companion.ts +102 -17
- package/cli/drivers/desktop.ts +14 -1
- package/cli/drivers/playwright-sim-host.ts +38 -3
- package/cli/drivers/playwright.ts +9 -8
- package/cli/drivers/types.ts +5 -0
- package/cli/maestro-accessibility.ts +1 -1
- package/cli/maestro-compare.ts +84 -12
- package/cli/main.ts +2 -1
- package/cli/outbound-endpoints.ts +13 -1
- package/cli/pause-aware-timeout.ts +102 -0
- package/cli/scene-video.ts +89 -0
- package/cli/ws-bridge.ts +23 -14
- package/detox/attempt-events.cjs +4 -0
- package/detox/index.ts +167 -142
- package/detox/motion-change.cjs +57 -21
- package/detox/motion-change.d.cts +7 -2
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +13 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +14 -8
- package/dist-lib/bridge-contract.mjs +13 -7
- package/dist-lib/bundle-channel.cjs +9536 -7604
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +6 -3
- package/dist-lib/config.mjs +6 -3
- package/dist-lib/detox/index.cjs +895 -664
- package/dist-lib/dev-bundle-resolution.cjs +14 -2
- package/dist-lib/expo-swift-bundle.cjs +7818 -5916
- package/dist-lib/expo-swift-metadata.cjs +3 -1
- package/dist-lib/expo-swift-metadata.mjs +3 -1
- package/dist-lib/home-paths.cjs +32 -3
- package/dist-lib/host/bridge-host.cjs +8340 -6392
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +812 -339
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +391 -28
- package/dist-lib/native-app-identity-project.cjs +55 -2
- package/dist-lib/native-app-identity-project.mjs +55 -2
- package/dist-lib/native-app-identity.cjs +53 -1
- package/dist-lib/native-app-identity.mjs +52 -1
- package/dist-lib/native-firebase-metadata.cjs +1 -1
- package/dist-lib/native-firebase-metadata.mjs +1 -1
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +5 -1
- package/dist-lib/react-native-host-modules.mjs +5 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +15 -3
- package/dist-lib/sdk.cjs +27 -11
- package/dist-lib/sdk.mjs +27 -10
- package/dist-lib/skills.cjs +12276 -8947
- package/dist-lib/swift-compile.cjs +924 -118
- package/dist-lib/swift-compile.mjs +966 -152
- package/dist-lib/swift-package.cjs +12 -3
- package/dist-lib/swift-package.mjs +12 -3
- package/dist-lib/swift-submit.cjs +7 -1
- package/dist-lib/swift-submit.mjs +5 -1
- package/dist-lib/swift-toolchain.cjs +133 -18
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1955 -498
- package/dist-lib/vite.cjs +3 -1
- package/dist-lib/worklet-executable.cjs +1 -1
- package/dist-lib/worklet-executable.mjs +1 -1
- package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
- package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +2 -1
- package/skills/peach-perf/SKILL.md +11 -5
- package/skills/peach-setup/SKILL.md +16 -19
- package/src/bridge-constants.ts +11 -0
- package/src/bridge-contract.ts +22 -34
- package/src/config.ts +15 -0
- package/src/desktop-host-delivery.ts +5 -1
- package/src/dev-bundle-resolution.ts +2 -1
- package/src/expo-manifest-body.ts +13 -0
- package/src/expo-swift-host.ts +191 -47
- package/src/expo-swift-ready.ts +78 -0
- package/src/expoSwiftContract.ts +13 -0
- package/src/home-paths.ts +48 -2
- package/src/host/bridge-host.ts +5 -2
- package/src/native-app-identity-project.ts +26 -2
- package/src/native-app-identity.ts +90 -0
- package/src/native-firebase-config.ts +209 -13
- package/src/process-group.ts +25 -2
- package/src/react-native-bridge-modules.ts +416 -0
- package/src/react-native-host-modules.ts +4 -0
- package/src/swift-binary-modules.ts +2 -2
- package/src/swift-compile.ts +835 -84
- package/src/swift-compiler-digest.ts +14 -0
- package/src/swift-distribution.lock.json +14 -12
- package/src/swift-host.ts +91 -29
- package/src/swift-icu.ts +14 -6
- package/src/swift-objc-sdk.ts +133 -10
- package/src/swift-objc-standin.ts +25 -3
- package/src/swift-package-manifest.ts +21 -6
- package/src/swift-package.ts +15 -1
- package/src/swift-packages.ts +119 -60
- package/src/swift-standin-emit.ts +57 -9
- package/src/swift-standins.ts +168 -20
- package/src/swift-submit.ts +10 -0
- package/src/swift-toolchain.ts +149 -23
- package/src/vite-plugin.ts +12 -1
package/cli/commands/upload.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// associated with the user's account.
|
|
10
10
|
|
|
11
11
|
import { createHash, randomUUID } from 'crypto'
|
|
12
|
-
import { readFileSync } from 'fs'
|
|
12
|
+
import { readFileSync, rmSync } from 'fs'
|
|
13
|
+
import { tmpdir } from 'os'
|
|
13
14
|
import path from 'path'
|
|
14
15
|
import { gzipSync, gunzipSync } from 'zlib'
|
|
15
16
|
import { readProfileTrace } from '@peach/globals/profile-trace'
|
|
@@ -36,11 +37,13 @@ import {
|
|
|
36
37
|
mergeProfileTimeline,
|
|
37
38
|
profileFrameSummary,
|
|
38
39
|
} from 'peach-engine/profiler/profile-trace'
|
|
40
|
+
import { readSceneRecordingUpload } from 'peach-engine/remote-stream/scene-recording'
|
|
39
41
|
import {
|
|
40
42
|
resolvedExpoAppIconUrl,
|
|
41
43
|
expoConfigFromManifest,
|
|
42
44
|
} from '../../../peach-engine/src/engine/expo-manifest'
|
|
43
45
|
import { loopbackOriginOf } from '../../src/backend-origin'
|
|
46
|
+
import { parseExpoManifestBody } from '../../src/expo-manifest-body'
|
|
44
47
|
import { rnxPublicBrand } from '../../src/public-brand'
|
|
45
48
|
import { fetchAppFontFiles } from '../app-fonts'
|
|
46
49
|
import { fetchAppSplashImages } from '../app-splash'
|
|
@@ -194,8 +197,8 @@ options:
|
|
|
194
197
|
for prod uploads, upload origin for localhost/custom origins).
|
|
195
198
|
override with PEACH_PREVIEW_ORIGIN env var
|
|
196
199
|
--events <path> path to a gzipped events .jsonl.gz file to attach
|
|
197
|
-
required unless --video is present;
|
|
198
|
-
have replay or recording playback data
|
|
200
|
+
required unless --video or --scene is present;
|
|
201
|
+
previews must have replay or recording playback data
|
|
199
202
|
--screenshot <path>
|
|
200
203
|
path to a PNG thumbnail to attach to the share
|
|
201
204
|
--snapshot-manifest <path>
|
|
@@ -205,6 +208,9 @@ options:
|
|
|
205
208
|
/api/preview/flow-video?id=<share-id>
|
|
206
209
|
--video-duration-ms <ms>
|
|
207
210
|
duration hint for attached flow video
|
|
211
|
+
--scene <path> a scene recording (.psr.gz) of a remote viewer's display
|
|
212
|
+
stream. the preview paints it through the simulator's
|
|
213
|
+
renderer instead of playing a video
|
|
208
214
|
--timeline-events <path>
|
|
209
215
|
extra trace timeline rows, JSONL or JSONL gz
|
|
210
216
|
--trace-profile <path>
|
|
@@ -436,6 +442,7 @@ export async function runUpload(
|
|
|
436
442
|
const screenshotPath = extractStringFlag(mutableArgs, '--screenshot')
|
|
437
443
|
const snapshotManifestPath = extractStringFlag(mutableArgs, '--snapshot-manifest')
|
|
438
444
|
const videoPath = extractStringFlag(mutableArgs, '--video')
|
|
445
|
+
const scenePath = extractStringFlag(mutableArgs, '--scene')
|
|
439
446
|
const timelineEventsPath = extractStringFlag(mutableArgs, '--timeline-events')
|
|
440
447
|
const traceProfilePath = extractStringFlag(mutableArgs, '--trace-profile')
|
|
441
448
|
const failureBundleDir = extractStringFlag(mutableArgs, '--failure-bundle')
|
|
@@ -468,9 +475,9 @@ export async function runUpload(
|
|
|
468
475
|
console.error(' --billing-kind test_run is only for hosted test jobs')
|
|
469
476
|
peachExit(1)
|
|
470
477
|
}
|
|
471
|
-
if (!eventsPath && !videoPath) {
|
|
478
|
+
if (!eventsPath && !videoPath && !scenePath) {
|
|
472
479
|
console.error(
|
|
473
|
-
' preview uploads require playback data: pass --events or --
|
|
480
|
+
' preview uploads require playback data: pass --events, --video or --scene.\n' +
|
|
474
481
|
' for a normal local preview, use `peach record --mode combined --open`\n' +
|
|
475
482
|
' or `peach maestro test --preview` instead of uploading a bundle-only snapshot.',
|
|
476
483
|
)
|
|
@@ -493,6 +500,7 @@ export async function runUpload(
|
|
|
493
500
|
})
|
|
494
501
|
|
|
495
502
|
const bridge = createBridgeFromParsed(parsed)
|
|
503
|
+
let shellUrl: string | null = null
|
|
496
504
|
let snapshot: BundleSnapshot
|
|
497
505
|
// bundle-origin manifest (no bodies) — CLI re-fetches these over http so
|
|
498
506
|
// they can be large without blowing the ws frame cap. sibling bundle
|
|
@@ -513,6 +521,7 @@ export async function runUpload(
|
|
|
513
521
|
const localTraceTimelineCount = traceTimelineEvents.length
|
|
514
522
|
let transformedBundleText: string | null = null
|
|
515
523
|
try {
|
|
524
|
+
if (scenePath) shellUrl = await evalInBridge<string>(bridge, 'location.href')
|
|
516
525
|
snapshot = await evalInBridge<BundleSnapshot>(
|
|
517
526
|
bridge,
|
|
518
527
|
'(typeof window.__sootsimCaptureBundle === "function") ? window.__sootsimCaptureBundle() : null',
|
|
@@ -872,6 +881,17 @@ export async function runUpload(
|
|
|
872
881
|
)
|
|
873
882
|
}
|
|
874
883
|
|
|
884
|
+
let sceneBytes: Uint8Array | undefined
|
|
885
|
+
let sceneStream: InitRequestBody['sceneStream']
|
|
886
|
+
if (scenePath) {
|
|
887
|
+
sceneBytes = readFileSync(scenePath)
|
|
888
|
+
const { header, slot } = await readSceneRecordingUpload(sceneBytes)
|
|
889
|
+
sceneStream = slot
|
|
890
|
+
console.log(
|
|
891
|
+
` attaching scene stream: ${(sceneBytes.byteLength / 1024).toFixed(1)} KiB, ${header.frameCount} frames over ${Math.round(header.durationMs)}ms`,
|
|
892
|
+
)
|
|
893
|
+
}
|
|
894
|
+
|
|
875
895
|
// priority: PEACH_API_KEY env → github runner token → saved credential
|
|
876
896
|
// file → session token. github runner auth is what keeps installed PR
|
|
877
897
|
// previews from requiring a new Contrast secret.
|
|
@@ -883,6 +903,21 @@ export async function runUpload(
|
|
|
883
903
|
console.error(' set PEACH_API_KEY=sk_rnx_... or run `peach login`.')
|
|
884
904
|
peachExit(1)
|
|
885
905
|
}
|
|
906
|
+
if (sceneBytes && shellUrl && cliAuth.kind === 'github') {
|
|
907
|
+
const { renderSceneToVideo } = await import('../scene-video')
|
|
908
|
+
const output = path.join(tmpdir(), `peach-scene-video-${Date.now()}.mp4`)
|
|
909
|
+
try {
|
|
910
|
+
const rendered = await renderSceneToVideo({ scene: sceneBytes, shellUrl, output })
|
|
911
|
+
videoBytes = readFileSync(output)
|
|
912
|
+
videoContentType = 'video/mp4'
|
|
913
|
+
videoDurationMs = rendered.durationMs
|
|
914
|
+
console.log(
|
|
915
|
+
` rendered scene video: ${rendered.frameCount} frames at 60 fps, ${rendered.sizeBytes} bytes in ${(rendered.renderTimeMs / 1000).toFixed(1)}s`,
|
|
916
|
+
)
|
|
917
|
+
} finally {
|
|
918
|
+
rmSync(output, { force: true })
|
|
919
|
+
}
|
|
920
|
+
}
|
|
886
921
|
const resolvedAuthHeader = authHeaderValue(cliAuth)
|
|
887
922
|
|
|
888
923
|
const bundleOrigin = (() => {
|
|
@@ -1010,8 +1045,8 @@ export async function runUpload(
|
|
|
1010
1045
|
headers: { 'expo-platform': platform, accept: 'application/json' },
|
|
1011
1046
|
signal: AbortSignal.timeout(30_000),
|
|
1012
1047
|
})
|
|
1013
|
-
if (response.ok && response.headers.get('content-type')?.includes('
|
|
1014
|
-
const manifest: unknown = await response.
|
|
1048
|
+
if (response.ok && !response.headers.get('content-type')?.includes('html')) {
|
|
1049
|
+
const manifest: unknown = parseExpoManifestBody(await response.text())
|
|
1015
1050
|
const config = expoConfigFromManifest(manifest)
|
|
1016
1051
|
if (config) {
|
|
1017
1052
|
appManifest = manifestUrl
|
|
@@ -1179,6 +1214,7 @@ export async function runUpload(
|
|
|
1179
1214
|
durationMs: videoDurationMs,
|
|
1180
1215
|
}
|
|
1181
1216
|
: undefined,
|
|
1217
|
+
sceneStream,
|
|
1182
1218
|
}
|
|
1183
1219
|
|
|
1184
1220
|
const filesByHash = new Map<string, Uint8Array>()
|
|
@@ -1201,6 +1237,7 @@ export async function runUpload(
|
|
|
1201
1237
|
for (const snapshot of interactionSnapshots) putBytes += snapshot.bytes.byteLength
|
|
1202
1238
|
if (failureBundleGz) putBytes += failureBundleGz.byteLength
|
|
1203
1239
|
if (videoBytes) putBytes += videoBytes.byteLength
|
|
1240
|
+
if (sceneBytes) putBytes += sceneBytes.byteLength
|
|
1204
1241
|
|
|
1205
1242
|
const initUrl = `${normalizeOrigin(origin)}/api/preview/upload/init`
|
|
1206
1243
|
console.log(` init: ${initUrl}`)
|
|
@@ -1217,6 +1254,7 @@ export async function runUpload(
|
|
|
1217
1254
|
timelineBytes: timelineGz,
|
|
1218
1255
|
storageBytes: storageGz,
|
|
1219
1256
|
videoBytes,
|
|
1257
|
+
sceneStreamBytes: sceneBytes,
|
|
1220
1258
|
failureBundleBytes: failureBundleGz,
|
|
1221
1259
|
snapshotBytes,
|
|
1222
1260
|
},
|
|
@@ -1232,6 +1270,7 @@ export async function runUpload(
|
|
|
1232
1270
|
(result.init.storage ? 1 : 0) +
|
|
1233
1271
|
result.init.snapshots.length +
|
|
1234
1272
|
(result.init.flowVideo ? 1 : 0) +
|
|
1273
|
+
(result.init.sceneStream ? 1 : 0) +
|
|
1235
1274
|
(result.init.failureBundle ? 1 : 0)
|
|
1236
1275
|
console.log(
|
|
1237
1276
|
` PUT ${totalObjects} objects in ${Date.now() - putT0}ms (${(putBytes / 1024).toFixed(1)} KiB)`,
|
package/cli/desktop-companion.ts
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import { execFileSync, spawn } from 'child_process'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
symlinkSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'fs'
|
|
11
|
+
import { createHash } from 'node:crypto'
|
|
12
|
+
import { tmpdir } from 'os'
|
|
3
13
|
import { dirname, join, resolve } from 'path'
|
|
4
14
|
import { fileURLToPath } from 'url'
|
|
5
|
-
import {
|
|
6
|
-
|
|
15
|
+
import {
|
|
16
|
+
activeDesktopHostApp,
|
|
17
|
+
desktopHostPlatform,
|
|
18
|
+
isNativeHostBundle,
|
|
19
|
+
} from '../src/desktop-host-delivery'
|
|
20
|
+
import {
|
|
21
|
+
isDaemonLockfileFresh,
|
|
22
|
+
isDevBridgeLockfileFresh,
|
|
23
|
+
readDaemonLockfile,
|
|
24
|
+
readDevBridgeLockfiles,
|
|
25
|
+
rnxHomeDir,
|
|
26
|
+
} from '../src/home-paths'
|
|
7
27
|
|
|
8
28
|
// resolved lazily: peach-engine is a workspace package in dev, but absent
|
|
9
29
|
// from the published npm tarball. don't crash module load just because the
|
|
@@ -95,22 +115,31 @@ export function findInstalledDesktopCompanion(): DesktopCompanionInstall | null
|
|
|
95
115
|
: findNativeDesktopCompanion()
|
|
96
116
|
}
|
|
97
117
|
|
|
118
|
+
// leftover Electron bundles. /Applications/Peach.app is the native host on a
|
|
119
|
+
// case-insensitive volume, so bundle id decides, not the path's letter case.
|
|
120
|
+
function retiredElectronApps(): string[] {
|
|
121
|
+
if (process.platform === 'darwin') {
|
|
122
|
+
return [
|
|
123
|
+
'/Applications/peach.app',
|
|
124
|
+
resolve(process.env.HOME || '', 'Applications/peach.app'),
|
|
125
|
+
].filter((candidate) => existsSync(candidate) && !isNativeHostBundle(candidate))
|
|
126
|
+
}
|
|
127
|
+
if (process.platform !== 'linux') return []
|
|
128
|
+
return [
|
|
129
|
+
resolve(process.env.HOME || '', 'Applications/peach.AppImage'),
|
|
130
|
+
resolve(process.env.HOME || '', '.local/bin/peach.AppImage'),
|
|
131
|
+
'/opt/peach/peach.AppImage',
|
|
132
|
+
'/opt/peach/peach',
|
|
133
|
+
].filter((candidate) => existsSync(candidate))
|
|
134
|
+
}
|
|
135
|
+
|
|
98
136
|
// existing Electron apps stay on disk; CLI users receive the migration once.
|
|
137
|
+
// the notice names those apps and does not reject a native host beside them.
|
|
99
138
|
export function notifyDesktopMigration(): void {
|
|
100
139
|
if (process.platform !== 'darwin' && process.platform !== 'linux') return
|
|
101
|
-
const legacy =
|
|
102
|
-
process.platform === 'darwin'
|
|
103
|
-
? [
|
|
104
|
-
'/Applications/peach.app',
|
|
105
|
-
resolve(process.env.HOME || '', 'Applications/peach.app'),
|
|
106
|
-
].some((candidate) => existsSync(candidate) && !isNativeHostBundle(candidate))
|
|
107
|
-
: ['Applications/peach.AppImage', '.local/bin/peach.AppImage'].some((candidate) =>
|
|
108
|
-
existsSync(resolve(process.env.HOME || '', candidate)),
|
|
109
|
-
) ||
|
|
110
|
-
existsSync('/opt/peach/peach.AppImage') ||
|
|
111
|
-
existsSync('/opt/peach/peach')
|
|
140
|
+
const legacy = retiredElectronApps()
|
|
112
141
|
const marker = join(rnxHomeDir(), 'electron-native-migration')
|
|
113
|
-
if (
|
|
142
|
+
if (legacy.length === 0 || existsSync(marker)) return
|
|
114
143
|
mkdirSync(rnxHomeDir(), { recursive: true })
|
|
115
144
|
try {
|
|
116
145
|
writeFileSync(marker, '', { flag: 'wx' })
|
|
@@ -119,7 +148,7 @@ export function notifyDesktopMigration(): void {
|
|
|
119
148
|
throw error
|
|
120
149
|
}
|
|
121
150
|
console.error(
|
|
122
|
-
|
|
151
|
+
` Electron has retired on Mac and Linux (${legacy.join(', ')}). Install the native app: peach desktop install`,
|
|
123
152
|
)
|
|
124
153
|
}
|
|
125
154
|
|
|
@@ -154,6 +183,21 @@ export function wantsNativeDesktop(opts: DesktopCompanionOptions = {}): boolean
|
|
|
154
183
|
)
|
|
155
184
|
}
|
|
156
185
|
|
|
186
|
+
// the native app is the simulator window wherever it ships: always on Apple
|
|
187
|
+
// silicon, and on Linux x64 in a desktop session once installed or when a
|
|
188
|
+
// person is present to approve its package install's sudo prompt.
|
|
189
|
+
export function nativeDesktopIsDefault(): boolean {
|
|
190
|
+
const platform = desktopHostPlatform()
|
|
191
|
+
if (platform === 'mac-arm64') return true
|
|
192
|
+
if (platform !== 'linux-x64') return false
|
|
193
|
+
if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false
|
|
194
|
+
return (
|
|
195
|
+
findNativeDesktopCompanion() !== null ||
|
|
196
|
+
process.stdin.isTTY === true ||
|
|
197
|
+
process.getuid?.() === 0
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
157
201
|
export function getNativeDesktopAppPath(): string | null {
|
|
158
202
|
const explicitApp = process.env.PEACH_DESKTOP_APP
|
|
159
203
|
if (!explicitApp)
|
|
@@ -284,6 +328,47 @@ async function openInConnectedMacApp(
|
|
|
284
328
|
}
|
|
285
329
|
}
|
|
286
330
|
|
|
331
|
+
// PeachDesktop shipped before PEACH_BRIDGE_PORT seeds the page from
|
|
332
|
+
// PEACH_HOME/daemon.json and ignores the launch URL's wsPort. when this
|
|
333
|
+
// command is waiting on a different bridge, give the child a home whose
|
|
334
|
+
// lock names that bridge. the caller's daemon.json is left untouched.
|
|
335
|
+
// a newer host that prefers the env, the URL pin, or a fresh daemon still
|
|
336
|
+
// sees this same port (the isolated-home rule in desktop host bridge selection).
|
|
337
|
+
function macDesktopLaunchHome(bridgePort: number): string {
|
|
338
|
+
const home = rnxHomeDir()
|
|
339
|
+
const daemon = readDaemonLockfile()
|
|
340
|
+
if (isDaemonLockfileFresh(daemon) && daemon.bridgePort === bridgePort) return home
|
|
341
|
+
const dev = readDevBridgeLockfiles().find(
|
|
342
|
+
(lock) => lock.bridgePort === bridgePort && isDevBridgeLockfileFresh(lock),
|
|
343
|
+
)
|
|
344
|
+
const homeKey = createHash('sha256').update(home).digest('hex')
|
|
345
|
+
const dir = join(tmpdir(), 'peach-desktop-launch', homeKey, String(bridgePort))
|
|
346
|
+
mkdirSync(dir, { recursive: true })
|
|
347
|
+
if (existsSync(home)) {
|
|
348
|
+
for (const name of readdirSync(home)) {
|
|
349
|
+
if (name === 'daemon.json') continue
|
|
350
|
+
const dest = join(dir, name)
|
|
351
|
+
try {
|
|
352
|
+
lstatSync(dest)
|
|
353
|
+
} catch {
|
|
354
|
+
symlinkSync(join(home, name), dest)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const lockPath = join(dir, 'daemon.json')
|
|
359
|
+
try {
|
|
360
|
+
if (lstatSync(lockPath).isSymbolicLink()) unlinkSync(lockPath)
|
|
361
|
+
} catch {
|
|
362
|
+
// no lock yet
|
|
363
|
+
}
|
|
364
|
+
const pid = dev && dev.pid > 1 ? dev.pid : process.pid
|
|
365
|
+
writeFileSync(
|
|
366
|
+
lockPath,
|
|
367
|
+
JSON.stringify({ schema: 1, pid, bridgePort, heartbeatAt: Date.now() }),
|
|
368
|
+
)
|
|
369
|
+
return dir
|
|
370
|
+
}
|
|
371
|
+
|
|
287
372
|
export async function launchDesktopCompanion(
|
|
288
373
|
url?: string,
|
|
289
374
|
install = findDesktopCompanion(),
|
|
@@ -339,7 +424,7 @@ export async function launchDesktopCompanion(
|
|
|
339
424
|
'-a',
|
|
340
425
|
install.path,
|
|
341
426
|
'--env',
|
|
342
|
-
`PEACH_HOME=${rnxHomeDir()}`,
|
|
427
|
+
`PEACH_HOME=${opts.bridgePort && launchUrl ? macDesktopLaunchHome(opts.bridgePort) : rnxHomeDir()}`,
|
|
343
428
|
]
|
|
344
429
|
if (opts.bridgePort) {
|
|
345
430
|
openArgs.push('--env', `PEACH_BRIDGE_PORT=${opts.bridgePort}`)
|
package/cli/drivers/desktop.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// desktop driver selection is shared with companion discovery.
|
|
2
2
|
|
|
3
|
+
import { desktopHostPlatform, headlessHostCommand } from '../../src/desktop-host-delivery'
|
|
3
4
|
import {
|
|
4
5
|
findDesktopCompanion,
|
|
5
6
|
launchDesktopCompanion,
|
|
@@ -18,7 +19,19 @@ export function createDesktopDriver(id: 'desktop' | 'electron'): Driver {
|
|
|
18
19
|
const missing = `peach ${id} app not installed (run \`peach desktop install${native ? '' : ' --electron'}\`)`
|
|
19
20
|
function probe(): DriverAvailability {
|
|
20
21
|
const install = findDesktopCompanion({ native })
|
|
21
|
-
if (!install)
|
|
22
|
+
if (!install) {
|
|
23
|
+
const host =
|
|
24
|
+
id === 'desktop' && native && desktopHostPlatform() === 'linux-x64'
|
|
25
|
+
? headlessHostCommand()
|
|
26
|
+
: null
|
|
27
|
+
return host
|
|
28
|
+
? {
|
|
29
|
+
available: true,
|
|
30
|
+
reason: null,
|
|
31
|
+
detail: `headless host @ ${host[0]} (use --driver playwright --headless)`,
|
|
32
|
+
}
|
|
33
|
+
: { available: false, reason: missing }
|
|
34
|
+
}
|
|
22
35
|
return {
|
|
23
36
|
available: true,
|
|
24
37
|
reason: null,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
// require, in a child process the CLI spawns as itself.
|
|
19
19
|
import {
|
|
20
20
|
chromeDisabledFeaturesSwitch,
|
|
21
|
+
LINUX_CHROME_SCOPE_ENV,
|
|
21
22
|
softwareChromeLaunchOptions,
|
|
22
23
|
} from '../../../../scripts/lib/reap-browser'
|
|
23
24
|
import { createBrowserPageTargetReader } from '../../../peach-engine/src/browser-page-target'
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
} from '../../../peach-engine/src/capture/browser-composite'
|
|
28
29
|
import { WEBVIEW_HOST_BOOTSTRAP_SOURCE } from '../../../peach-engine/src/components/native-ui/webview-frame-bridge'
|
|
29
30
|
import { createBrowserPointerDispatcher } from '../../../peach-engine/src/render-worker/browser-pointer'
|
|
31
|
+
import { automateWebAuthenticationSessions } from '../../../peach-engine/src/web-authentication-automation-host'
|
|
30
32
|
import {
|
|
31
33
|
automateWebViewFrames,
|
|
32
34
|
WEBVIEW_HOST_BINDING,
|
|
@@ -69,6 +71,9 @@ const callerUserDataDir = process.env.SOOTSIM_PW_USERDATADIR || '';
|
|
|
69
71
|
// SOOTSIM_PW_OWNER_POLL_MS is a test-only override of the 5s poll interval;
|
|
70
72
|
// production never sets it.
|
|
71
73
|
const ownerPid = Number(process.env.SOOTSIM_PW_OWNER_PID || 0);
|
|
74
|
+
// the open command waiting for this host to connect; it may queue for the
|
|
75
|
+
// browser slot and then be killed by its own caller's budget.
|
|
76
|
+
const launcherPid = Number(process.env.SOOTSIM_PW_LAUNCHER_PID || 0);
|
|
72
77
|
const ownerPollMs = Number(process.env.SOOTSIM_PW_OWNER_POLL_MS || 5000);
|
|
73
78
|
// optional CDP port — exposes Chrome's remote-debugging endpoint so a
|
|
74
79
|
// bridge-driven sim can also be cpu-profiled (peach perf cpu --cdp-port).
|
|
@@ -110,6 +115,7 @@ const viewport = viewportSpec
|
|
|
110
115
|
const deviceScaleFactor = Number(process.env.SOOTSIM_PW_DSF || 2);
|
|
111
116
|
const closeTimeoutMs = Number(process.env.SOOTSIM_PW_CLOSE_TIMEOUT_MS || 2500);
|
|
112
117
|
const connectAckFile = process.env.SOOTSIM_PW_CONNECTED_ACK_FILE || '';
|
|
118
|
+
const admittedFile = process.env.SOOTSIM_PW_ADMITTED_FILE || '';
|
|
113
119
|
const completionFile = process.env.PEACH_PW_COMPLETION_FILE || '';
|
|
114
120
|
const disposableBrowserCacheDirs = new Set(${JSON.stringify(
|
|
115
121
|
DISPOSABLE_BROWSER_CACHE_DIR_NAMES,
|
|
@@ -121,6 +127,7 @@ const { tmpdir } = require('os');
|
|
|
121
127
|
const { join } = require('path');
|
|
122
128
|
const createBrowserPointerDispatcher = ${createBrowserPointerDispatcher.toString()};
|
|
123
129
|
const automateWebViewFrames = ${automateWebViewFrames.toString()};
|
|
130
|
+
const automateWebAuthenticationSessions = ${automateWebAuthenticationSessions.toString()};
|
|
124
131
|
const webViewAutomationHelpers = ${webViewAutomationHelpersSource()};
|
|
125
132
|
const readBrowserCompositeCaptureRequest = ${readBrowserCompositeCaptureRequest.toString()};
|
|
126
133
|
const resolveBrowserCompositeCdpClip = ${resolveBrowserCompositeCdpClip.toString()};
|
|
@@ -402,6 +409,9 @@ const cleanupProfile = () => {
|
|
|
402
409
|
if (connectAckFile) {
|
|
403
410
|
try { unlinkSync(connectAckFile); } catch {}
|
|
404
411
|
}
|
|
412
|
+
if (admittedFile) {
|
|
413
|
+
try { unlinkSync(admittedFile); } catch {}
|
|
414
|
+
}
|
|
405
415
|
process.exit(code);
|
|
406
416
|
};
|
|
407
417
|
process.once('SIGTERM', () => { void shutdown(0); });
|
|
@@ -434,13 +444,25 @@ const cleanupProfile = () => {
|
|
|
434
444
|
throw new Error('[playwright-driver] missing valid owning process; refusing unsupervised browser host');
|
|
435
445
|
}
|
|
436
446
|
const intervalMs = Number.isFinite(ownerPollMs) && ownerPollMs > 0 ? ownerPollMs : 5000;
|
|
437
|
-
|
|
447
|
+
const gone = (pid) => {
|
|
438
448
|
try {
|
|
439
|
-
process.kill(
|
|
449
|
+
process.kill(pid, 0);
|
|
450
|
+
return false;
|
|
440
451
|
} catch (err) {
|
|
441
|
-
|
|
452
|
+
return !(err && err.code === 'EPERM');
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
ownerWatch = setInterval(() => {
|
|
456
|
+
if (gone(ownerPid)) {
|
|
442
457
|
process.stderr.write('[owner-gone] owning process ' + ownerPid + ' exited; closing playwright host\\n');
|
|
443
458
|
void shutdown(0);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
// until the sim connects, the open command is the only one waiting for
|
|
462
|
+
// it; once that command is gone nobody receives the sim.
|
|
463
|
+
if (!connected && !ackFileExists() && Number.isInteger(launcherPid) && launcherPid > 1 && gone(launcherPid)) {
|
|
464
|
+
process.stderr.write('[launcher-gone] open command ' + launcherPid + ' exited before the sim connected; closing playwright host\\n');
|
|
465
|
+
void shutdown(2);
|
|
444
466
|
}
|
|
445
467
|
}, intervalMs);
|
|
446
468
|
if (typeof ownerWatch.unref === 'function') ownerWatch.unref();
|
|
@@ -479,6 +501,11 @@ const cleanupProfile = () => {
|
|
|
479
501
|
LD_LIBRARY_PATH: [libraryPath, process.env.LD_LIBRARY_PATH].filter(Boolean).join(':'),
|
|
480
502
|
};
|
|
481
503
|
}
|
|
504
|
+
// keep the browser main process with its children in the caller's cgroup.
|
|
505
|
+
// otherwise chromium moves only main into a systemd app scope over dbus.
|
|
506
|
+
if (process.platform === 'linux') {
|
|
507
|
+
opts.env = { ...process.env, ...opts.env, ...${JSON.stringify(LINUX_CHROME_SCOPE_ENV)} };
|
|
508
|
+
}
|
|
482
509
|
// playwright injects --force-color-profile=srgb by default, which reads
|
|
483
510
|
// visibly washed-out next to a normal chrome window on a wide-gamut mac
|
|
484
511
|
// display. strip it so driver-launched sims color-match the user's own
|
|
@@ -549,6 +576,11 @@ const cleanupProfile = () => {
|
|
|
549
576
|
label: 'peach sim host ' + profileDir,
|
|
550
577
|
readQueue: readChromiumQueue,
|
|
551
578
|
});
|
|
579
|
+
// the launcher's connect wait starts here: time queued for the slot is not
|
|
580
|
+
// time the sim had to connect.
|
|
581
|
+
if (admittedFile) {
|
|
582
|
+
try { writeFileSync(admittedFile, JSON.stringify({ admittedAt: Date.now(), pid: process.pid }) + '\\n'); } catch {}
|
|
583
|
+
}
|
|
552
584
|
clearStaleSingletonLock();
|
|
553
585
|
let launchErr = null;
|
|
554
586
|
let launchedChannel = null;
|
|
@@ -591,6 +623,8 @@ const cleanupProfile = () => {
|
|
|
591
623
|
await context.grantPermissions(['camera']);
|
|
592
624
|
const hostMeta = {
|
|
593
625
|
sootsimHostDriver: 'playwright',
|
|
626
|
+
peachRenderer: hostCommand ? 'native' : 'chromium',
|
|
627
|
+
peachHostExecutable: hostCommand ? hostCommand[0] : null,
|
|
594
628
|
sootsimHostPid: process.pid,
|
|
595
629
|
sootsimHostOwnerPid: ownerPid,
|
|
596
630
|
sootsimHostStartedAt: Date.now(),
|
|
@@ -677,6 +711,7 @@ const cleanupProfile = () => {
|
|
|
677
711
|
}, Number.isFinite(connectTimeoutMs) && connectTimeoutMs > 0 ? connectTimeoutMs : 120000);
|
|
678
712
|
if (typeof connectTimer.unref === 'function') connectTimer.unref();
|
|
679
713
|
const page = await context.newPage();
|
|
714
|
+
await automateWebAuthenticationSessions(context, '__peachWebAuthenticationHost', page, !headless);
|
|
680
715
|
if (hardwareGpuRequired) {
|
|
681
716
|
assertNoUnsafeSwiftShaderFlag();
|
|
682
717
|
const gpu = await page.evaluate(() => {
|
|
@@ -151,18 +151,15 @@ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
|
|
|
151
151
|
// renders natively and lets the page present only on capture; the host
|
|
152
152
|
// reaches the browser over cdp, so it needs a debugging port.
|
|
153
153
|
let hostCommand: string[] | null = null
|
|
154
|
-
if (headless && desktopHostPlatform()) {
|
|
154
|
+
if (headless && !opts.chromium && desktopHostPlatform()) {
|
|
155
155
|
try {
|
|
156
156
|
hostCommand = headlessHostCommand()
|
|
157
157
|
if (!hostCommand && desktopHostPlatform() === 'linux-x64') {
|
|
158
|
-
//
|
|
159
|
-
// before the compatible headless host existed leaves this run on the browser engine alone
|
|
158
|
+
// delivery honors PEACH_DESKTOP_VERSION; a pin never follows channel latest.
|
|
160
159
|
await desktopHostDelivery().install()
|
|
161
160
|
hostCommand = headlessHostCommand()
|
|
162
161
|
if (!hostCommand)
|
|
163
|
-
|
|
164
|
-
' native host release has no compatible headless host yet; rendering in the browser',
|
|
165
|
-
)
|
|
162
|
+
throw new Error('selected native host has no compatible headless entrypoint')
|
|
166
163
|
}
|
|
167
164
|
} catch (error) {
|
|
168
165
|
return {
|
|
@@ -175,6 +172,7 @@ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
|
|
|
175
172
|
|
|
176
173
|
const errLog = join(tmpdir(), playwrightHostLogName(Date.now(), process.pid))
|
|
177
174
|
const connectAckFile = `${errLog}.connected`
|
|
175
|
+
const admittedFile = `${errLog}.admitted`
|
|
178
176
|
const completionFile = `${errLog}.completed`
|
|
179
177
|
const errFd = openSync(errLog, 'a')
|
|
180
178
|
try {
|
|
@@ -210,9 +208,11 @@ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
|
|
|
210
208
|
? { SOOTSIM_PW_VIEWPORT: `${opts.viewport.width}x${opts.viewport.height}` }
|
|
211
209
|
: {}),
|
|
212
210
|
SOOTSIM_PW_CONNECTED_ACK_FILE: connectAckFile,
|
|
211
|
+
SOOTSIM_PW_ADMITTED_FILE: admittedFile,
|
|
213
212
|
PEACH_PW_COMPLETION_FILE: completionFile,
|
|
214
213
|
SOOTSIM_PW_CONNECT_TIMEOUT_MS: String(opts.connectTimeoutMs ?? 120_000),
|
|
215
214
|
SOOTSIM_PW_OWNER_PID: String(ownerPid),
|
|
215
|
+
SOOTSIM_PW_LAUNCHER_PID: String(process.pid),
|
|
216
216
|
SOOTSIM_PW_LIBRARY_PATH: provisioning.libraryPath ?? '',
|
|
217
217
|
},
|
|
218
218
|
},
|
|
@@ -233,11 +233,12 @@ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
|
|
|
233
233
|
const ownerNote = ` — browser closes when pid ${ownerPid}${ownerComm ? ` (${ownerComm})` : ''} exits, or after ${idleMinutes} min with no peach command or input if another browser launch is waiting for its slot`
|
|
234
234
|
return {
|
|
235
235
|
launched: true,
|
|
236
|
-
message: `playwright
|
|
236
|
+
message: `playwright ${hostCommand ? 'native host' : 'Chromium'} launched → ${targetUrl.href}${ownerNote}`,
|
|
237
237
|
pid: child.pid,
|
|
238
238
|
target: resolved.spec,
|
|
239
239
|
attachUrl: targetUrl.href,
|
|
240
240
|
connectAckFile,
|
|
241
|
+
admittedFile,
|
|
241
242
|
diagnosticLogPath: errLog,
|
|
242
243
|
completion,
|
|
243
244
|
completionFile,
|
|
@@ -257,7 +258,7 @@ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
|
|
|
257
258
|
export const playwrightDriver: Driver = {
|
|
258
259
|
id: 'playwright',
|
|
259
260
|
name: 'playwright',
|
|
260
|
-
description: '
|
|
261
|
+
description: 'isolated Chromium; headless uses the native host when available',
|
|
261
262
|
kind: 'automation',
|
|
262
263
|
availability: probe,
|
|
263
264
|
launch,
|
package/cli/drivers/types.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface DriverLaunchOptions {
|
|
|
30
30
|
url?: string
|
|
31
31
|
// run in headless mode where the driver supports it. ignored by electron.
|
|
32
32
|
headless?: boolean
|
|
33
|
+
// opt this open into browser rendering without acquiring or attaching a native host.
|
|
34
|
+
chromium?: boolean
|
|
33
35
|
// initial device model seed forwarded to the peach shell. the electron
|
|
34
36
|
// driver forwards it via argv (--device=<model>); browser drivers receive
|
|
35
37
|
// it already stamped on the URL (?device=<model>) by runOpenCommand.
|
|
@@ -86,6 +88,9 @@ export interface DriverLaunchResult {
|
|
|
86
88
|
target?: string
|
|
87
89
|
attachUrl?: string
|
|
88
90
|
connectAckFile?: string
|
|
91
|
+
// written once the host holds its machine-wide browser slot; its connect
|
|
92
|
+
// budget starts there, so the launcher's wait starts there too.
|
|
93
|
+
admittedFile?: string
|
|
89
94
|
diagnosticLogPath?: string
|
|
90
95
|
// foreground guest hosts await the detached driver's exit without polling.
|
|
91
96
|
completion?: Promise<DriverCompletion>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PeachAccessibilityNode } from '@peach/globals'
|
|
2
2
|
|
|
3
3
|
// maestro reads ios frames as integer edges (AXElement.boundsString).
|
|
4
|
-
export function accessibilityBounds(node: PeachAccessibilityNode): {
|
|
4
|
+
export function accessibilityBounds(node: Pick<PeachAccessibilityNode, 'frame'>): {
|
|
5
5
|
x: number
|
|
6
6
|
y: number
|
|
7
7
|
width: number
|