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/control.ts
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs'
|
|
|
3
3
|
import http from 'node:http'
|
|
4
4
|
import https from 'node:https'
|
|
5
5
|
import { createRequire } from 'node:module'
|
|
6
|
+
import net from 'node:net'
|
|
6
7
|
import os from 'node:os'
|
|
7
8
|
import path from 'node:path'
|
|
8
9
|
import { bundleUrlForPlatform } from 'peach-engine/bundle-platform-url'
|
|
@@ -27,20 +28,28 @@ import {
|
|
|
27
28
|
readPeachConfigFromSearchParams,
|
|
28
29
|
type PeachConfig,
|
|
29
30
|
} from '../../src/config'
|
|
31
|
+
import {
|
|
32
|
+
desktopHostDelivery,
|
|
33
|
+
desktopHostPlatform,
|
|
34
|
+
headlessHostCommand,
|
|
35
|
+
} from '../../src/desktop-host-delivery'
|
|
30
36
|
import {
|
|
31
37
|
resolveConnectionInput,
|
|
32
38
|
type ResolvedDevBundle,
|
|
33
39
|
} from '../../src/dev-bundle-resolution'
|
|
34
40
|
import { readProjectDotEnv } from '../../src/dot-env'
|
|
41
|
+
import { awaitStampedExpoSwiftReady } from '../../src/expo-swift-ready'
|
|
35
42
|
import { flowTimeoutScale } from '../../src/flow-timeout-scale'
|
|
36
43
|
import {
|
|
37
44
|
isDaemonLockfileFresh,
|
|
38
45
|
isDevBridgeLockfileFresh,
|
|
46
|
+
PEACH_HOME_ENV,
|
|
39
47
|
readActiveRuntime,
|
|
40
48
|
readDaemonLockfile,
|
|
41
49
|
readDevBridgeLockfiles,
|
|
42
50
|
readLiveRuntimeVersions,
|
|
43
51
|
readSimulatorDriverPreference,
|
|
52
|
+
rnxHomeDir,
|
|
44
53
|
runtimeDir,
|
|
45
54
|
runtimesDir,
|
|
46
55
|
} from '../../src/home-paths'
|
|
@@ -102,10 +111,11 @@ import {
|
|
|
102
111
|
readCurrentSimId,
|
|
103
112
|
saveCurrentSimId,
|
|
104
113
|
} from '../current-sim'
|
|
114
|
+
import { findDesktopCompanion, nativeDesktopIsDefault } from '../desktop-companion'
|
|
105
115
|
import { validateDeviceOption } from '../device-option'
|
|
106
116
|
import { getDriver } from '../drivers/registry'
|
|
107
117
|
import { isTeamMachineSession, openUrl as openUrlInBrowser } from '../open-url'
|
|
108
|
-
import { diagnosticsAllowedForCurrentProcess } from '../privacy'
|
|
118
|
+
import { diagnosticsAllowedForCurrentProcess, isCi } from '../privacy'
|
|
109
119
|
import { rethrowIfExit, peachExit } from '../run-rnx'
|
|
110
120
|
import { collectRuntimeDiagnostics } from '../runtime-diagnostics'
|
|
111
121
|
import { rnxSelfInvocation } from '../self-invocation'
|
|
@@ -123,8 +133,8 @@ import {
|
|
|
123
133
|
} from '../ws-bridge'
|
|
124
134
|
import { readNativeProjectRenderer } from './detect'
|
|
125
135
|
import { inspectDescribe, inspectReadySnapshot, waitReadyReason } from './inspect/core'
|
|
126
|
-
import { isAgentEnv } from './inspect/env'
|
|
127
136
|
import { waitForSimReady } from './inspect/shared'
|
|
137
|
+
import { runInstallDesktop } from './install-desktop'
|
|
128
138
|
import { hasInstalledPeachSkills } from './skills'
|
|
129
139
|
import type { DriverId, DriverLaunchResult } from '../drivers/types'
|
|
130
140
|
|
|
@@ -152,8 +162,13 @@ interface DriverConnectWaitOptions {
|
|
|
152
162
|
const DEFAULT_DRIVER_CONNECT_TIMEOUT_MS = 120_000
|
|
153
163
|
const DRIVER_CONNECT_INTERVAL_MS = 100
|
|
154
164
|
const DRIVER_CONNECT_HOST_GRACE_MS = 5_000
|
|
155
|
-
|
|
156
|
-
|
|
165
|
+
// ceiling for a bridge that is alive but never becomes ready. a bridge that
|
|
166
|
+
// exits fails at once and a slow one is judged by its own lockfile and
|
|
167
|
+
// handshake, so this is not the expected startup time.
|
|
168
|
+
const OPEN_BRIDGE_SPAWN_TIMEOUT_MS = 60_000
|
|
169
|
+
const OPEN_BRIDGE_RUNTIME_TIMEOUT_MS = 600_000
|
|
170
|
+
const OPEN_BRIDGE_HEALTH_TIMEOUT_MS = 10_000
|
|
171
|
+
const OPEN_BRIDGE_SILENCE_MS = 30_000
|
|
157
172
|
// a closed port refuses at once, so this window only matters when something
|
|
158
173
|
// listens: a live bridge on a loaded machine can take longer than 250 ms to
|
|
159
174
|
// finish the handshake, and missing it spawned a second bridge or reported a
|
|
@@ -197,9 +212,19 @@ function isDriverId(value: string): value is DriverId {
|
|
|
197
212
|
return value === 'desktop' || value === 'electron' || value === 'playwright'
|
|
198
213
|
}
|
|
199
214
|
|
|
200
|
-
function simUsesDriver(
|
|
215
|
+
function simUsesDriver(
|
|
216
|
+
sim: BridgeSimInfo,
|
|
217
|
+
driverId: DriverId,
|
|
218
|
+
renderer?: 'chromium' | 'native',
|
|
219
|
+
hostExecutable?: string | null,
|
|
220
|
+
): boolean {
|
|
201
221
|
if (driverId === 'playwright') {
|
|
202
|
-
return
|
|
222
|
+
return (
|
|
223
|
+
sim.meta?.sootsimHostDriver === 'playwright' &&
|
|
224
|
+
(!renderer || sim.meta?.peachRenderer === renderer) &&
|
|
225
|
+
(hostExecutable === undefined ||
|
|
226
|
+
(hostExecutable !== null && sim.meta?.peachHostExecutable === hostExecutable))
|
|
227
|
+
)
|
|
203
228
|
}
|
|
204
229
|
if (driverId === 'desktop') {
|
|
205
230
|
return Boolean(
|
|
@@ -236,11 +261,21 @@ function normalizeKnownTarget(target: string): string {
|
|
|
236
261
|
return target.trim()
|
|
237
262
|
}
|
|
238
263
|
|
|
264
|
+
type BridgeOpenGate = {
|
|
265
|
+
newShell: boolean
|
|
266
|
+
simId?: string
|
|
267
|
+
simIdSource?: 'flag' | 'saved' | 'none'
|
|
268
|
+
driverId?: DriverId
|
|
269
|
+
renderer?: 'chromium' | 'native'
|
|
270
|
+
pinnedHostExecutable?: string | null
|
|
271
|
+
}
|
|
272
|
+
|
|
239
273
|
export async function ensureBridgeForOpen(
|
|
240
274
|
preferredPort: number,
|
|
241
275
|
explicitPort = false,
|
|
242
276
|
resources?: OpenCommandResources,
|
|
243
277
|
runtimeSource: 'bridge' | 'external' = 'bridge',
|
|
278
|
+
gate: BridgeOpenGate = { newShell: true },
|
|
244
279
|
): Promise<number> {
|
|
245
280
|
const port = await connectBridgeForOpen(
|
|
246
281
|
preferredPort,
|
|
@@ -249,22 +284,107 @@ export async function ensureBridgeForOpen(
|
|
|
249
284
|
runtimeSource,
|
|
250
285
|
)
|
|
251
286
|
// a bridge accepts connections while its first runtime download is still
|
|
252
|
-
// running, and
|
|
287
|
+
// running, and a shell page served by that bridge answers 503 until the
|
|
288
|
+
// runtime directory exists. a registered page is not that runtime: --new
|
|
289
|
+
// would hand the driver the 503 url. skip the wait only when this open
|
|
290
|
+
// will reuse a page whose origin already answers. a new shell waits until
|
|
291
|
+
// the bridge install settles or the shell itself answers, and does not
|
|
292
|
+
// start a second install over the one already in flight.
|
|
253
293
|
if (runtimeSource === 'bridge') {
|
|
254
|
-
|
|
255
|
-
|
|
294
|
+
const reusingReadyOrigin =
|
|
295
|
+
!gate.newShell && (await provenReadyReuseOrigin(port, gate))
|
|
296
|
+
if (!reusingReadyOrigin) {
|
|
297
|
+
const readiness = await waitForBridgeRuntime(port)
|
|
298
|
+
if (readiness === 'settled') await ensureRuntimeForOpen()
|
|
299
|
+
}
|
|
256
300
|
}
|
|
257
301
|
return port
|
|
258
302
|
}
|
|
259
303
|
|
|
260
|
-
async function
|
|
304
|
+
async function provenReadyReuseOrigin(
|
|
305
|
+
port: number,
|
|
306
|
+
gate: BridgeOpenGate,
|
|
307
|
+
): Promise<boolean> {
|
|
308
|
+
if (!gate.driverId || !gate.simId || gate.simIdSource === 'none' || !gate.simIdSource) {
|
|
309
|
+
return false
|
|
310
|
+
}
|
|
311
|
+
const bridge = createBridge(port, { commandTimeoutMs: 2_000 })
|
|
312
|
+
try {
|
|
313
|
+
const sims = await bridge.listSims()
|
|
314
|
+
const sim = sims.find((candidate) => candidate.id === gate.simId)
|
|
315
|
+
if (!sim?.url || sim.readyState !== 'open') return false
|
|
316
|
+
if (
|
|
317
|
+
!simUsesDriver(
|
|
318
|
+
sim,
|
|
319
|
+
gate.driverId,
|
|
320
|
+
gate.renderer,
|
|
321
|
+
gate.pinnedHostExecutable ?? undefined,
|
|
322
|
+
)
|
|
323
|
+
) {
|
|
324
|
+
return false
|
|
325
|
+
}
|
|
326
|
+
return await originServesShell(sim.url)
|
|
327
|
+
} catch {
|
|
328
|
+
return false
|
|
329
|
+
} finally {
|
|
330
|
+
bridge.close()
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function originServesShell(pageOrOrigin: string): Promise<boolean> {
|
|
335
|
+
try {
|
|
336
|
+
const origin = new URL(pageOrOrigin).origin
|
|
337
|
+
const res = await fetch(origin, { signal: AbortSignal.timeout(2_000) })
|
|
338
|
+
return res.status !== 503
|
|
339
|
+
} catch {
|
|
340
|
+
return false
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// the install runs inside the bridge and reports completion through
|
|
345
|
+
// /healthz. a bridge that answers is installing, so only a bridge that is gone
|
|
346
|
+
// (connection refused) or silent for a while fails the wait; the ceiling only
|
|
347
|
+
// bounds an install that never finishes.
|
|
348
|
+
function portRefusesConnections(port: number): Promise<boolean> {
|
|
349
|
+
return new Promise((resolve) => {
|
|
350
|
+
let refused = false
|
|
351
|
+
const socket = net.connect(port, '127.0.0.1')
|
|
352
|
+
socket.setTimeout(OPEN_BRIDGE_HEALTH_TIMEOUT_MS, () => socket.destroy())
|
|
353
|
+
socket.once('connect', () => socket.destroy())
|
|
354
|
+
socket.once('close', () => resolve(refused))
|
|
355
|
+
socket.once('error', (err: NodeJS.ErrnoException) => {
|
|
356
|
+
refused = err.code === 'ECONNREFUSED'
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function waitForBridgeRuntime(port: number): Promise<'settled' | 'serving'> {
|
|
261
362
|
const deadline = Date.now() + OPEN_BRIDGE_RUNTIME_TIMEOUT_MS
|
|
363
|
+
let lastAnswerAt = Date.now()
|
|
262
364
|
while (Date.now() < deadline) {
|
|
263
365
|
try {
|
|
264
|
-
const res = await fetch(`http://127.0.0.1:${port}/healthz
|
|
366
|
+
const res = await fetch(`http://127.0.0.1:${port}/healthz`, {
|
|
367
|
+
signal: AbortSignal.timeout(OPEN_BRIDGE_HEALTH_TIMEOUT_MS),
|
|
368
|
+
})
|
|
265
369
|
const health = (await res.json()) as { bootstrapping?: boolean }
|
|
266
|
-
if (!health.bootstrapping) return
|
|
267
|
-
|
|
370
|
+
if (!health.bootstrapping) return 'settled'
|
|
371
|
+
lastAnswerAt = Date.now()
|
|
372
|
+
// the install flag can stay true after the shell directory is already
|
|
373
|
+
// being served. a new page can use that origin, and installing again
|
|
374
|
+
// would race the download still in flight.
|
|
375
|
+
if (await originServesShell(`http://127.0.0.1:${port}/`)) return 'serving'
|
|
376
|
+
} catch {
|
|
377
|
+
if (await portRefusesConnections(port)) {
|
|
378
|
+
throw new Error(
|
|
379
|
+
`peach bridge on port ${port} exited while installing its runtime`,
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
if (Date.now() - lastAnswerAt > OPEN_BRIDGE_SILENCE_MS) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
`peach bridge on port ${port} stopped answering for ${Math.round(OPEN_BRIDGE_SILENCE_MS / 1000)}s while installing its runtime`,
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
268
388
|
await sleep(150)
|
|
269
389
|
}
|
|
270
390
|
throw new Error(
|
|
@@ -278,8 +398,8 @@ async function connectBridgeForOpen(
|
|
|
278
398
|
resources: OpenCommandResources | undefined,
|
|
279
399
|
runtimeSource: 'bridge' | 'external',
|
|
280
400
|
): Promise<number> {
|
|
401
|
+
isolateHomeForExplicitPort(preferredPort, explicitPort, readDaemonLockfile())
|
|
281
402
|
const lock = readDaemonLockfile()
|
|
282
|
-
assertExplicitBridgeLock(preferredPort, explicitPort, lock)
|
|
283
403
|
const preferredWorld = worldForBridgePort(preferredPort)
|
|
284
404
|
const handshakeTimeoutMs = LIVE_BRIDGE_HANDSHAKE_TIMEOUT_MS * flowTimeoutScale()
|
|
285
405
|
const handshake = await bridgeHandshake(preferredPort, handshakeTimeoutMs)
|
|
@@ -340,8 +460,10 @@ async function connectBridgeForOpen(
|
|
|
340
460
|
resources.port = preferredPort
|
|
341
461
|
}
|
|
342
462
|
let childExit: string | null = null
|
|
463
|
+
const exited = new AbortController()
|
|
343
464
|
child.once('exit', (code, signal) => {
|
|
344
465
|
childExit = String(code ?? signal ?? 'unknown')
|
|
466
|
+
exited.abort()
|
|
345
467
|
})
|
|
346
468
|
child.unref()
|
|
347
469
|
|
|
@@ -364,11 +486,18 @@ async function connectBridgeForOpen(
|
|
|
364
486
|
)
|
|
365
487
|
}
|
|
366
488
|
const selectedPort = explicitPort ? preferredPort : nextLock?.bridgePort
|
|
489
|
+
// ready is the spawned bridge's own lockfile plus a completed handshake. a
|
|
490
|
+
// loaded machine needs the full handshake budget, not a 250ms probe.
|
|
367
491
|
if (
|
|
368
492
|
nextLock &&
|
|
493
|
+
nextLock.pid === child.pid &&
|
|
369
494
|
isDaemonLockfileFresh(nextLock) &&
|
|
370
495
|
selectedPort !== undefined &&
|
|
371
|
-
(await canConnectToBridge(
|
|
496
|
+
(await canConnectToBridge(
|
|
497
|
+
selectedPort,
|
|
498
|
+
Math.max(1, Math.min(handshakeTimeoutMs, deadline - Date.now())),
|
|
499
|
+
exited.signal,
|
|
500
|
+
))
|
|
372
501
|
) {
|
|
373
502
|
console.error(
|
|
374
503
|
` Started a local bridge on port ${selectedPort}. Run \`peach daemon install\` to keep it ready between commands.`,
|
|
@@ -380,7 +509,7 @@ async function connectBridgeForOpen(
|
|
|
380
509
|
return selectedPort
|
|
381
510
|
}
|
|
382
511
|
if (childExit) break
|
|
383
|
-
await sleep(150)
|
|
512
|
+
await sleep(Math.max(0, Math.min(150, deadline - Date.now())))
|
|
384
513
|
}
|
|
385
514
|
|
|
386
515
|
const reason = childExit
|
|
@@ -392,7 +521,10 @@ async function connectBridgeForOpen(
|
|
|
392
521
|
)
|
|
393
522
|
}
|
|
394
523
|
|
|
395
|
-
|
|
524
|
+
// one bridge per peach home, so an explicit port the active daemon does not own
|
|
525
|
+
// gets its own home: this command and the bridge it starts both run in it,
|
|
526
|
+
// sharing the daemon's installed runtimes.
|
|
527
|
+
function isolateHomeForExplicitPort(
|
|
396
528
|
preferredPort: number,
|
|
397
529
|
explicitPort: boolean,
|
|
398
530
|
lock: ReturnType<typeof readDaemonLockfile>,
|
|
@@ -413,9 +545,14 @@ function assertExplicitBridgeLock(
|
|
|
413
545
|
) {
|
|
414
546
|
return
|
|
415
547
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
)
|
|
548
|
+
const sharedRuntimes = runtimesDir()
|
|
549
|
+
const home = path.join(rnxHomeDir(), 'port-homes', String(preferredPort))
|
|
550
|
+
fs.mkdirSync(home, { recursive: true })
|
|
551
|
+
const runtimes = path.join(home, 'runtimes')
|
|
552
|
+
if (fs.existsSync(sharedRuntimes) && !fs.existsSync(runtimes)) {
|
|
553
|
+
fs.symlinkSync(sharedRuntimes, runtimes)
|
|
554
|
+
}
|
|
555
|
+
process.env[PEACH_HOME_ENV] = home
|
|
419
556
|
}
|
|
420
557
|
|
|
421
558
|
async function ensureRuntimeForOpen() {
|
|
@@ -439,7 +576,11 @@ async function ensureRuntimeForOpen() {
|
|
|
439
576
|
|
|
440
577
|
type BridgeHandshake = 'open' | 'refused' | 'timeout'
|
|
441
578
|
|
|
442
|
-
function bridgeHandshake(
|
|
579
|
+
function bridgeHandshake(
|
|
580
|
+
port: number,
|
|
581
|
+
timeoutMs: number,
|
|
582
|
+
signal?: AbortSignal,
|
|
583
|
+
): Promise<BridgeHandshake> {
|
|
443
584
|
return new Promise((resolve) => {
|
|
444
585
|
const ws = new WebSocket(`ws://127.0.0.1:${port}`, { handshakeTimeout: timeoutMs })
|
|
445
586
|
let settled = false
|
|
@@ -447,11 +588,15 @@ function bridgeHandshake(port: number, timeoutMs: number): Promise<BridgeHandsha
|
|
|
447
588
|
if (settled) return
|
|
448
589
|
settled = true
|
|
449
590
|
clearTimeout(timer)
|
|
591
|
+
signal?.removeEventListener('abort', onAbort)
|
|
450
592
|
if (ws.readyState === WebSocket.OPEN) ws.close()
|
|
451
593
|
else if (ws.readyState === WebSocket.CONNECTING) ws.terminate()
|
|
452
594
|
resolve(result)
|
|
453
595
|
}
|
|
454
596
|
const timer = setTimeout(() => finish('timeout'), timeoutMs)
|
|
597
|
+
const onAbort = () => finish('timeout')
|
|
598
|
+
if (signal?.aborted) onAbort()
|
|
599
|
+
else signal?.addEventListener('abort', onAbort)
|
|
455
600
|
ws.once('open', () => finish('open'))
|
|
456
601
|
ws.on('error', (err: NodeJS.ErrnoException) => {
|
|
457
602
|
finish(
|
|
@@ -476,8 +621,12 @@ function unreachableBridgeMessage(
|
|
|
476
621
|
return `peach open: requested bridge port ${port} is not reachable.`
|
|
477
622
|
}
|
|
478
623
|
|
|
479
|
-
function canConnectToBridge(
|
|
480
|
-
|
|
624
|
+
function canConnectToBridge(
|
|
625
|
+
port: number,
|
|
626
|
+
timeoutMs: number,
|
|
627
|
+
signal?: AbortSignal,
|
|
628
|
+
): Promise<boolean> {
|
|
629
|
+
return bridgeHandshake(port, timeoutMs, signal).then((result) => result === 'open')
|
|
481
630
|
}
|
|
482
631
|
|
|
483
632
|
function looksLikeSootsimUrl(target: string): boolean {
|
|
@@ -884,10 +1033,8 @@ function stampAppFonts(
|
|
|
884
1033
|
): string {
|
|
885
1034
|
try {
|
|
886
1035
|
const url = new URL(urlStr)
|
|
887
|
-
|
|
888
|
-
if (!bundleUrl || !project) return urlStr
|
|
1036
|
+
if (!url.searchParams.get('bundle') || !project) return urlStr
|
|
889
1037
|
const specs = discoverNativeLinkedAppFonts({
|
|
890
|
-
bundleUrl,
|
|
891
1038
|
projectDir: project.appDir,
|
|
892
1039
|
platform,
|
|
893
1040
|
nativeAssetOrigin,
|
|
@@ -1035,6 +1182,10 @@ export interface BuildShellUrlOptions {
|
|
|
1035
1182
|
// after the app path lands inside the bundle param and never reaches the
|
|
1036
1183
|
// shell. non-foldable devices resolve it to closed at boot.
|
|
1037
1184
|
posture?: string
|
|
1185
|
+
// raster quality (high|medium|low) stamped as ?renderResolution=. high
|
|
1186
|
+
// renders at the device's native scale, so a native-parity capture is not
|
|
1187
|
+
// left at the interactive medium default of 2.
|
|
1188
|
+
renderResolution?: string
|
|
1038
1189
|
// interface orientation (portrait|landscape) stamped as ?orientation=,
|
|
1039
1190
|
// which the shell reads at boot over the device's default: an open duo
|
|
1040
1191
|
// boots landscape unless this asks for it upright.
|
|
@@ -1262,6 +1413,12 @@ export async function buildShellUrl(
|
|
|
1262
1413
|
}
|
|
1263
1414
|
const expoConfigRead = await takeExpoConfigRead(project)
|
|
1264
1415
|
const stampedBundle = expoStamped.searchParams.get('bundle')
|
|
1416
|
+
if (project && !expoConfigRead && stampedBundle) {
|
|
1417
|
+
// no Expo config tooling, but the native project still configures Firebase
|
|
1418
|
+
const nativeFirebase = nativeFirebaseFromExpoConfig(project, { config: {} })
|
|
1419
|
+
if (Object.keys(nativeFirebase).length > 0)
|
|
1420
|
+
expoStamped.searchParams.set('nativeFirebase', JSON.stringify(nativeFirebase))
|
|
1421
|
+
}
|
|
1265
1422
|
if (project && expoConfigRead && stampedBundle) {
|
|
1266
1423
|
expoStamped.searchParams.set(
|
|
1267
1424
|
'nativeFirebase',
|
|
@@ -1292,6 +1449,7 @@ export async function buildShellUrl(
|
|
|
1292
1449
|
!opts.proof &&
|
|
1293
1450
|
!opts.ephemeral &&
|
|
1294
1451
|
!opts.posture &&
|
|
1452
|
+
!opts.renderResolution &&
|
|
1295
1453
|
!opts.orientation &&
|
|
1296
1454
|
!opts.osVersion &&
|
|
1297
1455
|
!opts.theme &&
|
|
@@ -1303,6 +1461,8 @@ export async function buildShellUrl(
|
|
|
1303
1461
|
if (opts.dotEnv) url.searchParams.set('dotEnv', JSON.stringify(opts.dotEnv))
|
|
1304
1462
|
if (opts.device) url.searchParams.set('device', opts.device)
|
|
1305
1463
|
if (opts.posture) url.searchParams.set('posture', opts.posture)
|
|
1464
|
+
if (opts.renderResolution)
|
|
1465
|
+
url.searchParams.set('renderResolution', opts.renderResolution)
|
|
1306
1466
|
if (opts.orientation) url.searchParams.set('orientation', opts.orientation)
|
|
1307
1467
|
if (opts.osVersion) url.searchParams.set('osVersion', opts.osVersion)
|
|
1308
1468
|
if (opts.theme) url.searchParams.set('theme', opts.theme)
|
|
@@ -1395,13 +1555,27 @@ export async function waitForSimMatchOrHostExit(
|
|
|
1395
1555
|
commandTimeoutMs: number,
|
|
1396
1556
|
predicate: (sim: BridgeSimInfo) => boolean,
|
|
1397
1557
|
hostPid: number | undefined,
|
|
1398
|
-
opts: {
|
|
1558
|
+
opts: {
|
|
1559
|
+
attempts?: number
|
|
1560
|
+
intervalMs?: number
|
|
1561
|
+
signal?: AbortSignal
|
|
1562
|
+
admittedFile?: string
|
|
1563
|
+
} = {},
|
|
1399
1564
|
): Promise<WaitForSimMatchOrHostExitResult> {
|
|
1400
1565
|
const attempts = opts.attempts ?? 30
|
|
1401
1566
|
const intervalMs = opts.intervalMs ?? 500
|
|
1402
1567
|
const watchHost = !!hostPid && Number.isInteger(hostPid) && hostPid > 1
|
|
1403
1568
|
const hostExited = () =>
|
|
1404
1569
|
watchHost && hostPid !== undefined && !isProcessRunning(hostPid)
|
|
1570
|
+
// a host queued behind the machine's browser cap has not started its connect
|
|
1571
|
+
// budget, so neither does this wait; the caller's signal still bounds it.
|
|
1572
|
+
if (opts.admittedFile && watchHost) {
|
|
1573
|
+
while (!fs.existsSync(opts.admittedFile)) {
|
|
1574
|
+
opts.signal?.throwIfAborted()
|
|
1575
|
+
if (hostExited()) return { kind: 'host-exit' }
|
|
1576
|
+
await sleep(intervalMs)
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1405
1579
|
for (let i = 0; i < attempts; i++) {
|
|
1406
1580
|
opts.signal?.throwIfAborted()
|
|
1407
1581
|
if (hostExited()) {
|
|
@@ -1809,6 +1983,7 @@ export async function runOpenCommand(
|
|
|
1809
1983
|
stripBooleanFlags: [
|
|
1810
1984
|
'--new',
|
|
1811
1985
|
'--headless',
|
|
1986
|
+
'--chromium',
|
|
1812
1987
|
'--headed',
|
|
1813
1988
|
'--ephemeral',
|
|
1814
1989
|
'--hot',
|
|
@@ -1828,6 +2003,7 @@ export async function runOpenCommand(
|
|
|
1828
2003
|
'--cdp-port',
|
|
1829
2004
|
'--device',
|
|
1830
2005
|
'--posture',
|
|
2006
|
+
'--render-resolution',
|
|
1831
2007
|
'--orientation',
|
|
1832
2008
|
'--os-version',
|
|
1833
2009
|
'--ios-target',
|
|
@@ -1971,25 +2147,49 @@ export async function runOpenCommand(
|
|
|
1971
2147
|
)
|
|
1972
2148
|
peachExit(1)
|
|
1973
2149
|
}
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
isAgentEnv() ||
|
|
1983
|
-
!process.stdin.isTTY
|
|
2150
|
+
// a person watching the sim, directly or through their coding agent, gets the
|
|
2151
|
+
// native app where it ships. CI and Team Machine fleet sessions stay in the
|
|
2152
|
+
// headless browser, as do the browser-only --headless and --chromium.
|
|
2153
|
+
const defaultDriverId: DriverId =
|
|
2154
|
+
isCi() ||
|
|
2155
|
+
isTeamMachineSession() ||
|
|
2156
|
+
args.includes('--headless') ||
|
|
2157
|
+
args.includes('--chromium')
|
|
1984
2158
|
? 'playwright'
|
|
1985
|
-
: readSimulatorDriverPreference()
|
|
1986
|
-
? 'desktop'
|
|
1987
|
-
|
|
2159
|
+
: (readSimulatorDriverPreference() ??
|
|
2160
|
+
(nativeDesktopIsDefault() ? 'desktop' : 'playwright'))
|
|
2161
|
+
const selectedDriverId = isDriverId(requestedDriverId)
|
|
2162
|
+
? requestedDriverId
|
|
2163
|
+
: defaultDriverId
|
|
2164
|
+
const driverId: DriverId =
|
|
2165
|
+
selectedDriverId === 'electron' && process.platform !== 'win32'
|
|
2166
|
+
? 'desktop'
|
|
2167
|
+
: selectedDriverId
|
|
1988
2168
|
if (requestedDriverId === 'electron' && driverId === 'desktop')
|
|
1989
2169
|
console.error(
|
|
1990
2170
|
' Electron is Windows-only; using the native desktop app (peach desktop install).',
|
|
1991
2171
|
)
|
|
2172
|
+
// the first open on a machine without the native app installs it.
|
|
2173
|
+
if (driverId === 'desktop' && !findDesktopCompanion())
|
|
2174
|
+
await runInstallDesktop(['--yes'])
|
|
1992
2175
|
const driverHeadless = resolveDriverHeadless(args, driverId)
|
|
2176
|
+
const chromium = args.includes('--chromium')
|
|
2177
|
+
if (chromium && driverId !== 'playwright') {
|
|
2178
|
+
console.error(' peach open: --chromium requires the playwright driver')
|
|
2179
|
+
peachExit(1)
|
|
2180
|
+
}
|
|
2181
|
+
const renderer = chromium
|
|
2182
|
+
? 'chromium'
|
|
2183
|
+
: driverHeadless && desktopHostPlatform() === 'linux-x64'
|
|
2184
|
+
? 'native'
|
|
2185
|
+
: undefined
|
|
2186
|
+
const pinnedHostExecutable =
|
|
2187
|
+
!chromium &&
|
|
2188
|
+
driverHeadless &&
|
|
2189
|
+
desktopHostPlatform() === 'linux-x64' &&
|
|
2190
|
+
desktopHostDelivery().resolvePin()
|
|
2191
|
+
? (headlessHostCommand()?.[0] ?? null)
|
|
2192
|
+
: undefined
|
|
1993
2193
|
if (cdpPort && driverId !== 'playwright') {
|
|
1994
2194
|
console.error(' peach open: --cdp-port is only honored by the playwright driver')
|
|
1995
2195
|
peachExit(1)
|
|
@@ -2071,9 +2271,21 @@ export async function runOpenCommand(
|
|
|
2071
2271
|
parsed.wsPort,
|
|
2072
2272
|
parsed.explicitPort,
|
|
2073
2273
|
opts.resources,
|
|
2074
|
-
!configuredRuntimeVersion &&
|
|
2274
|
+
!configuredRuntimeVersion &&
|
|
2275
|
+
(explicitBaseUrl ||
|
|
2276
|
+
looksLikeSootsimUrl(target) ||
|
|
2277
|
+
(parsed.explicitPort &&
|
|
2278
|
+
worldForBridgePort(parsed.wsPort)?.source === 'dev-bridge'))
|
|
2075
2279
|
? 'external'
|
|
2076
2280
|
: 'bridge',
|
|
2281
|
+
{
|
|
2282
|
+
newShell: openInNewSim,
|
|
2283
|
+
simId: parsed.simId,
|
|
2284
|
+
simIdSource: parsed.simIdSource,
|
|
2285
|
+
driverId,
|
|
2286
|
+
renderer,
|
|
2287
|
+
pinnedHostExecutable,
|
|
2288
|
+
},
|
|
2077
2289
|
)
|
|
2078
2290
|
if (opts.resources) opts.resources.port = parsed.wsPort
|
|
2079
2291
|
opts.signal?.throwIfAborted()
|
|
@@ -2100,6 +2312,18 @@ export async function runOpenCommand(
|
|
|
2100
2312
|
console.error(` peach open: --posture must be open or closed, got "${postureArg}"`)
|
|
2101
2313
|
peachExit(1)
|
|
2102
2314
|
}
|
|
2315
|
+
const renderResolutionArg = args.find((_, i) => args[i - 1] === '--render-resolution')
|
|
2316
|
+
if (
|
|
2317
|
+
args.includes('--render-resolution') &&
|
|
2318
|
+
renderResolutionArg !== 'high' &&
|
|
2319
|
+
renderResolutionArg !== 'medium' &&
|
|
2320
|
+
renderResolutionArg !== 'low'
|
|
2321
|
+
) {
|
|
2322
|
+
console.error(
|
|
2323
|
+
` peach open: --render-resolution must be high, medium, or low, got "${renderResolutionArg ?? ''}"`,
|
|
2324
|
+
)
|
|
2325
|
+
peachExit(1)
|
|
2326
|
+
}
|
|
2103
2327
|
const orientationFlag = args.find((_, i) => args[i - 1] === '--orientation')
|
|
2104
2328
|
const orientationArg =
|
|
2105
2329
|
orientationFlag === 'portrait' || orientationFlag === 'landscape'
|
|
@@ -2196,6 +2420,7 @@ export async function runOpenCommand(
|
|
|
2196
2420
|
hostDriver: driverId === 'desktop' ? 'desktop' : undefined,
|
|
2197
2421
|
device: deviceArg,
|
|
2198
2422
|
posture: postureArg,
|
|
2423
|
+
renderResolution: renderResolutionArg,
|
|
2199
2424
|
orientation: orientationArg,
|
|
2200
2425
|
osVersion: osVersionArg,
|
|
2201
2426
|
theme: themeArg,
|
|
@@ -2250,14 +2475,14 @@ export async function runOpenCommand(
|
|
|
2250
2475
|
(sim) =>
|
|
2251
2476
|
sim.id === savedSimId &&
|
|
2252
2477
|
sim.readyState === 'open' &&
|
|
2253
|
-
simUsesDriver(sim, driverId),
|
|
2478
|
+
simUsesDriver(sim, driverId, renderer, pinnedHostExecutable),
|
|
2254
2479
|
) ?? null
|
|
2255
2480
|
if (!targetSim) {
|
|
2256
2481
|
clearCurrentSimId()
|
|
2257
2482
|
}
|
|
2258
2483
|
} else if (parsed.simIdSource === 'flag') {
|
|
2259
2484
|
targetSim = resolveTargetSim(sims, parsed.simId)
|
|
2260
|
-
if (!simUsesDriver(targetSim, driverId)) {
|
|
2485
|
+
if (!simUsesDriver(targetSim, driverId, renderer, pinnedHostExecutable)) {
|
|
2261
2486
|
throw new Error(
|
|
2262
2487
|
`sim ${targetSim.id} is not managed by the ${driverId} driver`,
|
|
2263
2488
|
)
|
|
@@ -2276,7 +2501,7 @@ export async function runOpenCommand(
|
|
|
2276
2501
|
if (
|
|
2277
2502
|
sim.readyState !== 'open' ||
|
|
2278
2503
|
!sim.url ||
|
|
2279
|
-
!simUsesDriver(sim, driverId)
|
|
2504
|
+
!simUsesDriver(sim, driverId, renderer, pinnedHostExecutable)
|
|
2280
2505
|
) {
|
|
2281
2506
|
return false
|
|
2282
2507
|
}
|
|
@@ -2344,7 +2569,7 @@ export async function runOpenCommand(
|
|
|
2344
2569
|
parsed.commandTimeoutMs,
|
|
2345
2570
|
(sim) =>
|
|
2346
2571
|
sim.readyState === 'open' &&
|
|
2347
|
-
simUsesDriver(sim, driverId) &&
|
|
2572
|
+
simUsesDriver(sim, driverId, renderer, pinnedHostExecutable) &&
|
|
2348
2573
|
simMatchesToken(sim),
|
|
2349
2574
|
{ attempts: Math.round(30 * flowTimeoutScale()) },
|
|
2350
2575
|
)
|
|
@@ -2395,11 +2620,11 @@ export async function runOpenCommand(
|
|
|
2395
2620
|
}
|
|
2396
2621
|
|
|
2397
2622
|
const configuredBaseUrl = applyPeachConfigToUrl(baseUrl, runtimeConfig)
|
|
2398
|
-
const
|
|
2623
|
+
const builtShellUrl =
|
|
2399
2624
|
newSimShellUrl ??
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
)
|
|
2625
|
+
(await buildShellUrl(shellTarget, configuredBaseUrl, buildShellUrlOpts))
|
|
2626
|
+
await awaitStampedExpoSwiftReady(builtShellUrl)
|
|
2627
|
+
const openUrl = withOpenToken(builtShellUrl, token)
|
|
2403
2628
|
const ownerPid = opts.ownerPid ?? getStableOwnerPid()
|
|
2404
2629
|
if (!ownerPid) {
|
|
2405
2630
|
console.error(
|
|
@@ -2424,6 +2649,7 @@ export async function runOpenCommand(
|
|
|
2424
2649
|
// live world of its own.
|
|
2425
2650
|
bridgePort: parsed.wsPort,
|
|
2426
2651
|
headless: driverHeadless,
|
|
2652
|
+
chromium,
|
|
2427
2653
|
profileId,
|
|
2428
2654
|
ephemeralProfile,
|
|
2429
2655
|
cdpPort,
|
|
@@ -2451,6 +2677,7 @@ export async function runOpenCommand(
|
|
|
2451
2677
|
attempts: connectWait.attempts,
|
|
2452
2678
|
intervalMs: connectWait.intervalMs,
|
|
2453
2679
|
signal: opts.signal,
|
|
2680
|
+
admittedFile: result.admittedFile,
|
|
2454
2681
|
},
|
|
2455
2682
|
)
|
|
2456
2683
|
if (outcome.kind === 'host-exit') {
|
|
@@ -2501,6 +2728,13 @@ export async function runOpenCommand(
|
|
|
2501
2728
|
simId: match.id,
|
|
2502
2729
|
simIdSource: 'flag',
|
|
2503
2730
|
})
|
|
2731
|
+
const attached = await bridge
|
|
2732
|
+
.listSims()
|
|
2733
|
+
.then((sims) => sims.find((sim) => sim.id === match.id)?.readyState ?? 'gone')
|
|
2734
|
+
.catch(() => 'unknown')
|
|
2735
|
+
console.error(
|
|
2736
|
+
` sim ${match.id} bridge state: ${attached}; host load ${os.loadavg()[0].toFixed(1)} on ${os.cpus().length} cpus (a loaded host slows a cold first bundle past the budget; a sim that is open but never answers has a blocked page)`,
|
|
2737
|
+
)
|
|
2504
2738
|
await printBridgeFailureDiagnostics(bridge, {
|
|
2505
2739
|
errorsCommand: `peach get errors 5${hint}`,
|
|
2506
2740
|
warningsCommand: `peach get warnings 5${hint}`,
|