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
|
@@ -8,6 +8,7 @@ import * as fs from 'fs'
|
|
|
8
8
|
import * as os from 'os'
|
|
9
9
|
import * as path from 'path'
|
|
10
10
|
import { devices } from 'peach-engine/settings/devices'
|
|
11
|
+
import { PNG } from 'pngjs'
|
|
11
12
|
import { rnxHomeDir } from '../../src/home-paths'
|
|
12
13
|
import { parseFlowFile } from '../flow-file'
|
|
13
14
|
import {
|
|
@@ -17,7 +18,8 @@ import {
|
|
|
17
18
|
comparePassed,
|
|
18
19
|
instrumentFlowSource,
|
|
19
20
|
nativeStepsFromCommandsJson,
|
|
20
|
-
|
|
21
|
+
nativeFlowVerdictsFromJunit,
|
|
22
|
+
junitFlowKey,
|
|
21
23
|
peachStepsFromCommandsJson,
|
|
22
24
|
screensOverThreshold,
|
|
23
25
|
type CompareCapture,
|
|
@@ -161,12 +163,44 @@ function statusBarPx(png: Buffer): number {
|
|
|
161
163
|
const height = png.readUInt32BE(20)
|
|
162
164
|
const spec = Object.values(devices).find(
|
|
163
165
|
(d) =>
|
|
164
|
-
|
|
165
|
-
Math.round(d.
|
|
166
|
+
// maestro can truncate fractional point bounds one pixel below simctl.
|
|
167
|
+
Math.abs(Math.round(d.width * d.scale) - width) <= 1 &&
|
|
168
|
+
Math.abs(Math.round(d.height * d.scale) - height) <= 1,
|
|
166
169
|
)
|
|
167
170
|
return spec ? spec.statusBarHeight * spec.scale : 0
|
|
168
171
|
}
|
|
169
172
|
|
|
173
|
+
// maestro's xctest shot on an ios 27 simulator drops the rightmost column of
|
|
174
|
+
// an odd framebuffer (1179 becomes 1178). diffing at that short width
|
|
175
|
+
// resamples peach off the pixel grid, so restore the column first.
|
|
176
|
+
function alignNativeCapture(png: Buffer): Buffer {
|
|
177
|
+
const width = png.readUInt32BE(16)
|
|
178
|
+
const height = png.readUInt32BE(20)
|
|
179
|
+
const spec = Object.values(devices).find(
|
|
180
|
+
(d) =>
|
|
181
|
+
Math.abs(Math.round(d.width * d.scale) - width) <= 1 &&
|
|
182
|
+
Math.abs(Math.round(d.height * d.scale) - height) <= 1,
|
|
183
|
+
)
|
|
184
|
+
if (!spec) return png
|
|
185
|
+
const expectedW = Math.round(spec.width * spec.scale)
|
|
186
|
+
const expectedH = Math.round(spec.height * spec.scale)
|
|
187
|
+
if (expectedH !== height || expectedW !== width + 1) return png
|
|
188
|
+
const src = PNG.sync.read(png)
|
|
189
|
+
const dst = new PNG({ width: expectedW, height })
|
|
190
|
+
for (let y = 0; y < height; y++) {
|
|
191
|
+
const srow = y * src.width * 4
|
|
192
|
+
const drow = y * expectedW * 4
|
|
193
|
+
src.data.copy(dst.data, drow, srow, srow + src.width * 4)
|
|
194
|
+
const last = srow + (src.width - 1) * 4
|
|
195
|
+
const o = drow + src.width * 4
|
|
196
|
+
dst.data[o] = src.data[last] ?? 0
|
|
197
|
+
dst.data[o + 1] = src.data[last + 1] ?? 0
|
|
198
|
+
dst.data[o + 2] = src.data[last + 2] ?? 0
|
|
199
|
+
dst.data[o + 3] = 255
|
|
200
|
+
}
|
|
201
|
+
return PNG.sync.write(dst)
|
|
202
|
+
}
|
|
203
|
+
|
|
170
204
|
// the flow as its author would name it: relative inside the working
|
|
171
205
|
// directory, home-relative or absolute outside it.
|
|
172
206
|
function displayPath(cwd: string, file: string): string {
|
|
@@ -201,22 +235,6 @@ function formatFailedRequest(entry: unknown): string {
|
|
|
201
235
|
return `request failed: ${e.method ?? 'GET'} ${e.url ?? '?'} ${e.status ?? e.error ?? ''}`.trim()
|
|
202
236
|
}
|
|
203
237
|
|
|
204
|
-
// peach names each flow's output dir after the file stem, suffixing repeats
|
|
205
|
-
// in run order (commands/maestro.ts uniqueFlowDir).
|
|
206
|
-
function peachFlowDirs(root: string, files: string[]): string[] {
|
|
207
|
-
const seen = new Map<string, number>()
|
|
208
|
-
return files.map((file) => {
|
|
209
|
-
const slug =
|
|
210
|
-
path
|
|
211
|
-
.basename(file)
|
|
212
|
-
.replace(/\.(ya?ml)$/i, '')
|
|
213
|
-
.replace(/[^A-Za-z0-9._-]+/g, '_') || 'flow'
|
|
214
|
-
const n = (seen.get(slug) ?? 0) + 1
|
|
215
|
-
seen.set(slug, n)
|
|
216
|
-
return path.join(root, n === 1 ? slug : `${slug}-${n}`)
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
|
|
220
238
|
// the flow's appId as maestro resolves it: its `${...}` templates evaluate
|
|
221
239
|
// against --env values (in process.env) and the flow's own env block.
|
|
222
240
|
function resolveFlowAppId(frontmatter: Record<string, unknown>): string | undefined {
|
|
@@ -424,6 +442,10 @@ export async function runMaestroCompare(
|
|
|
424
442
|
'--test-output-dir',
|
|
425
443
|
nativeOut,
|
|
426
444
|
'--no-ansi',
|
|
445
|
+
'--format',
|
|
446
|
+
'junit',
|
|
447
|
+
'--output',
|
|
448
|
+
path.join(out, 'ios-report.xml'),
|
|
427
449
|
...shared,
|
|
428
450
|
...envPairs,
|
|
429
451
|
],
|
|
@@ -433,18 +455,23 @@ export async function runMaestroCompare(
|
|
|
433
455
|
spawnSync('xcrun', ['simctl', 'status_bar', udid, 'clear'], { stdio: 'ignore' })
|
|
434
456
|
}
|
|
435
457
|
console.log(`\n ios │ maestro exited ${nativeExit}\n`)
|
|
436
|
-
const
|
|
458
|
+
const nativeVerdicts = nativeFlowVerdictsFromJunit(
|
|
459
|
+
fs.readFileSync(path.join(out, 'ios-report.xml'), 'utf8'),
|
|
460
|
+
)
|
|
437
461
|
|
|
438
462
|
// 2. peach, same instrumented flows (its --env already applied above)
|
|
439
463
|
const peachOut = path.join(out, 'peach')
|
|
440
|
-
const peachResults = new Map<
|
|
464
|
+
const peachResults = new Map<
|
|
465
|
+
string,
|
|
466
|
+
{ exit: number; playbackFailure: string; outputDir: string | null }
|
|
467
|
+
>()
|
|
441
468
|
await runMaestro(
|
|
442
469
|
['test', runTarget, '--test-output-dir', peachOut, ...shared, ...peachArgs],
|
|
443
470
|
{
|
|
444
471
|
...opts,
|
|
445
472
|
captureConsole: true,
|
|
446
|
-
onFlowResult: ({ file, exit, playbackFailure }) => {
|
|
447
|
-
peachResults.set(path.resolve(file), { exit, playbackFailure })
|
|
473
|
+
onFlowResult: ({ file, exit, playbackFailure, outputDir }) => {
|
|
474
|
+
peachResults.set(path.resolve(file), { exit, playbackFailure, outputDir })
|
|
448
475
|
},
|
|
449
476
|
},
|
|
450
477
|
)
|
|
@@ -458,26 +485,56 @@ export async function runMaestroCompare(
|
|
|
458
485
|
.sort()
|
|
459
486
|
.pop()
|
|
460
487
|
: undefined
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
488
|
+
const nativeOutputs = new Map<string, Array<{ dir: string; commands: unknown }>>()
|
|
489
|
+
for (const entry of nativeRun
|
|
490
|
+
? fs.readdirSync(nativeRun, { withFileTypes: true })
|
|
491
|
+
: []) {
|
|
492
|
+
if (!entry.isDirectory()) continue
|
|
493
|
+
const dir = path.join(nativeRun!, entry.name)
|
|
494
|
+
const commands = readJson(path.join(dir, 'commands.json'))
|
|
495
|
+
if (!Array.isArray(commands)) continue
|
|
496
|
+
// display names are not paths: maestro replaces slashes and suffixes collisions.
|
|
497
|
+
// the top-level bookkeeping retains the original flow identity.
|
|
498
|
+
let name: string | undefined
|
|
499
|
+
let fileStem: string | undefined
|
|
500
|
+
for (const command of commands) {
|
|
501
|
+
if (command?.metadata?.depth !== 0) continue
|
|
502
|
+
const config = command?.command?.applyConfigurationCommand?.config
|
|
503
|
+
if (typeof config?.name === 'string') name ??= config.name
|
|
504
|
+
const env = command?.command?.defineVariablesCommand?.env
|
|
505
|
+
if (typeof env?.MAESTRO_FILENAME === 'string') fileStem ??= env.MAESTRO_FILENAME
|
|
506
|
+
}
|
|
507
|
+
if (fileStem === undefined) continue
|
|
508
|
+
const key = JSON.stringify([name ?? fileStem, fileStem])
|
|
509
|
+
const outputs = nativeOutputs.get(key) ?? []
|
|
510
|
+
outputs.push({ dir, commands })
|
|
511
|
+
nativeOutputs.set(key, outputs)
|
|
512
|
+
}
|
|
465
513
|
const imagesDir = path.join(out, 'images')
|
|
466
514
|
const flows: CompareFlow[] = []
|
|
467
515
|
for (const [i, flow] of instrumented.entries()) {
|
|
468
|
-
const
|
|
469
|
-
const
|
|
470
|
-
const
|
|
516
|
+
const fileStem = path.basename(flow.copy).replace(/\.(ya?ml)$/i, '')
|
|
517
|
+
const nativeOutput = nativeOutputs.get(JSON.stringify([flow.name, fileStem]))?.shift()
|
|
518
|
+
const nativeDir = nativeOutput?.dir
|
|
519
|
+
const nativeRan = Boolean(nativeOutput)
|
|
471
520
|
const peachResult = peachResults.get(path.resolve(flow.copy))
|
|
472
|
-
const
|
|
473
|
-
const
|
|
521
|
+
const peachDir = peachResult?.outputDir
|
|
522
|
+
const verdicts = nativeVerdicts.get(junitFlowKey(flow.name, fileStem))
|
|
523
|
+
const nativeFailure = verdicts?.length
|
|
524
|
+
? verdicts.shift()
|
|
525
|
+
: 'no native verdict in ios-report.xml'
|
|
526
|
+
const peachRan = Boolean(
|
|
527
|
+
peachDir && fs.existsSync(path.join(peachDir, 'commands.json')),
|
|
528
|
+
)
|
|
474
529
|
const nativeSteps = nativeRan
|
|
475
|
-
? nativeStepsFromCommandsJson(
|
|
530
|
+
? nativeStepsFromCommandsJson(nativeOutput?.commands)
|
|
476
531
|
: []
|
|
477
532
|
const peachSteps = peachRan
|
|
478
|
-
? peachStepsFromCommandsJson(readJson(path.join(peachDir
|
|
533
|
+
? peachStepsFromCommandsJson(readJson(path.join(peachDir!, 'commands.json')))
|
|
479
534
|
: []
|
|
480
|
-
const consoleJson =
|
|
535
|
+
const consoleJson = (
|
|
536
|
+
peachDir ? readJson(path.join(peachDir, 'logs', 'console.json')) : null
|
|
537
|
+
) as {
|
|
481
538
|
errors?: unknown[]
|
|
482
539
|
requests?: unknown[]
|
|
483
540
|
} | null
|
|
@@ -492,15 +549,17 @@ export async function runMaestroCompare(
|
|
|
492
549
|
const nativePng = nativeDir
|
|
493
550
|
? path.join(nativeDir, 'takeScreenshot', `${capture.name}.png`)
|
|
494
551
|
: ''
|
|
495
|
-
const peachPng =
|
|
552
|
+
const peachPng = peachDir
|
|
553
|
+
? path.join(peachDir, 'screenshots', `${capture.name}.png`)
|
|
554
|
+
: ''
|
|
496
555
|
const hasNative = Boolean(nativePng) && fs.existsSync(nativePng)
|
|
497
|
-
const hasPeach = fs.existsSync(peachPng)
|
|
556
|
+
const hasPeach = Boolean(peachPng) && fs.existsSync(peachPng)
|
|
498
557
|
const imageRel = path.join(
|
|
499
558
|
'images',
|
|
500
559
|
`${i + 1}-${capture.name.split('/').pop()}.png`,
|
|
501
560
|
)
|
|
502
561
|
if (hasNative && hasPeach) {
|
|
503
|
-
const nativeBytes = fs.readFileSync(nativePng)
|
|
562
|
+
const nativeBytes = alignNativeCapture(fs.readFileSync(nativePng))
|
|
504
563
|
const { diff, triptych } = compareScreens(
|
|
505
564
|
nativeBytes,
|
|
506
565
|
fs.readFileSync(peachPng),
|
package/cli/commands/maestro.ts
CHANGED
|
@@ -51,7 +51,12 @@ export interface RunMaestroOptions {
|
|
|
51
51
|
// (maestro compare reads it for the errors Peach saw)
|
|
52
52
|
captureConsole?: boolean
|
|
53
53
|
// each flow's final verdict, including a failure before any step ran
|
|
54
|
-
onFlowResult?: (result: {
|
|
54
|
+
onFlowResult?: (result: {
|
|
55
|
+
file: string
|
|
56
|
+
exit: number
|
|
57
|
+
playbackFailure: string
|
|
58
|
+
outputDir: string | null
|
|
59
|
+
}) => void
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
const MAESTRO_DIRS = ['.maestro', 'maestro']
|
|
@@ -518,6 +523,7 @@ export async function runMaestro(
|
|
|
518
523
|
let worstExit = 0
|
|
519
524
|
const results: Array<{
|
|
520
525
|
file: string
|
|
526
|
+
outputDir: string | null
|
|
521
527
|
exit: number
|
|
522
528
|
durationMs: number
|
|
523
529
|
steps: ReturnType<typeof getLastFlowTraceSteps>
|
|
@@ -537,6 +543,7 @@ export async function runMaestro(
|
|
|
537
543
|
console.error(` error: ${playbackFailure}`)
|
|
538
544
|
results.push({
|
|
539
545
|
file,
|
|
546
|
+
outputDir: flowTestDir,
|
|
540
547
|
exit: 1,
|
|
541
548
|
durationMs: 0,
|
|
542
549
|
steps: [],
|
|
@@ -610,6 +617,7 @@ export async function runMaestro(
|
|
|
610
617
|
}
|
|
611
618
|
const result = {
|
|
612
619
|
file,
|
|
620
|
+
outputDir: flowTestDir,
|
|
613
621
|
exit,
|
|
614
622
|
durationMs,
|
|
615
623
|
steps: traceSteps,
|
package/cli/commands/record.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// peach record — drives the engine's built-in canvas recorder over
|
|
2
|
-
// the WS bridge.
|
|
2
|
+
// the WS bridge. scene export opens an isolated viewer; encoding happens in the
|
|
3
3
|
// running peach page (webm via MediaRecorder, mp4 via WebCodecs, gif
|
|
4
4
|
// via gifenc, raw png frames via a shared bitmap sampler).
|
|
5
5
|
|
|
@@ -143,6 +143,8 @@ const RECORD_VALUE_FLAGS = new Set([
|
|
|
143
143
|
'--origin',
|
|
144
144
|
'--owner',
|
|
145
145
|
'--repo',
|
|
146
|
+
'--scene',
|
|
147
|
+
'--shell-url',
|
|
146
148
|
])
|
|
147
149
|
const RECORD_SUBCOMMANDS = new Set([
|
|
148
150
|
'start',
|
|
@@ -151,6 +153,7 @@ const RECORD_SUBCOMMANDS = new Set([
|
|
|
151
153
|
'status',
|
|
152
154
|
'prelude-start',
|
|
153
155
|
'upload',
|
|
156
|
+
'render',
|
|
154
157
|
])
|
|
155
158
|
|
|
156
159
|
// the record subcommand (start/stop/cancel/status) is the first non-flag
|
|
@@ -195,6 +198,23 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
195
198
|
// drop the subcommand token; the rest (incl. any --sim/--port) flows on
|
|
196
199
|
// to the subcommand handlers, which parse bridge flags themselves.
|
|
197
200
|
const rest = [...args.slice(0, sub.index), ...args.slice(sub.index + 1)]
|
|
201
|
+
if (sub.name === 'render') {
|
|
202
|
+
const scene = valueOf(rest, '--scene')
|
|
203
|
+
const shellUrl = valueOf(rest, '--shell-url')
|
|
204
|
+
const output = valueOf(rest, '--output')
|
|
205
|
+
if (!scene || !shellUrl || !output)
|
|
206
|
+
throw new Error('record render requires --scene, --shell-url and --output')
|
|
207
|
+
const { renderSceneToVideo } = await import('../scene-video')
|
|
208
|
+
const result = await renderSceneToVideo({
|
|
209
|
+
scene: readFileSync(scene),
|
|
210
|
+
shellUrl,
|
|
211
|
+
output,
|
|
212
|
+
})
|
|
213
|
+
console.log(
|
|
214
|
+
` video: ${resolve(output)} (${result.frameCount} frames at 60 fps, ${result.sizeBytes} bytes, rendered in ${(result.renderTimeMs / 1000).toFixed(1)}s)`,
|
|
215
|
+
)
|
|
216
|
+
return
|
|
217
|
+
}
|
|
198
218
|
if (sub.name === 'start') {
|
|
199
219
|
await recordStart(rest, opts)
|
|
200
220
|
return
|
|
@@ -140,7 +140,15 @@ examples:
|
|
|
140
140
|
// --id / --text, missing device model). surface them as a one-line
|
|
141
141
|
// failure instead of an uncaught internal stack trace.
|
|
142
142
|
try {
|
|
143
|
-
await captureViaBridge(
|
|
143
|
+
await captureViaBridge(
|
|
144
|
+
parsed,
|
|
145
|
+
outputArg,
|
|
146
|
+
rect,
|
|
147
|
+
withFrame,
|
|
148
|
+
layers,
|
|
149
|
+
allowLoading,
|
|
150
|
+
opts.verbose === true || args.includes('--verbose') || args.includes('-v'),
|
|
151
|
+
)
|
|
144
152
|
} catch (err) {
|
|
145
153
|
console.error(
|
|
146
154
|
` screenshot failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -206,7 +214,16 @@ async function captureViaBridge(
|
|
|
206
214
|
withFrame: boolean,
|
|
207
215
|
layers: 'full' | 'tenant' | 'shell' | undefined,
|
|
208
216
|
allowLoading: boolean,
|
|
217
|
+
verbose: boolean,
|
|
209
218
|
) {
|
|
219
|
+
// --verbose names the phase a slow capture spent its time in.
|
|
220
|
+
const phases: string[] = []
|
|
221
|
+
let phaseStart = Date.now()
|
|
222
|
+
const phase = (name: string) => {
|
|
223
|
+
const now = Date.now()
|
|
224
|
+
phases.push(`${name} ${now - phaseStart}ms`)
|
|
225
|
+
phaseStart = now
|
|
226
|
+
}
|
|
210
227
|
// 45s, not 10s: a first-paint capture on a headless CI runner with software
|
|
211
228
|
// GL (SwiftShader) is bounded but slow. forceRenderAll caps at ~6s (two
|
|
212
229
|
// 3s-bounded passes) and the WebGL→2D drawImage readback + PNG encode of a
|
|
@@ -225,6 +242,7 @@ async function captureViaBridge(
|
|
|
225
242
|
// affects screenshot fidelity directly, so call it out explicitly so the
|
|
226
243
|
// user doesn't accept a stale frame as ground truth.
|
|
227
244
|
const health = await checkSimHealth(bridge)
|
|
245
|
+
phase('health')
|
|
228
246
|
if (health.hidden) {
|
|
229
247
|
process.stderr.write(
|
|
230
248
|
' ⚠ screenshot will capture the LAST PAINTED canvas — likely stale.\n' +
|
|
@@ -267,6 +285,7 @@ async function captureViaBridge(
|
|
|
267
285
|
}
|
|
268
286
|
}
|
|
269
287
|
|
|
288
|
+
phase('ready')
|
|
270
289
|
let resolvedRect: SampleRect | null = rect
|
|
271
290
|
const matcher = (rect as { __matcher?: { id?: string; text?: string } })?.__matcher
|
|
272
291
|
if (matcher) {
|
|
@@ -297,6 +316,7 @@ async function captureViaBridge(
|
|
|
297
316
|
if (layers) request.layers = layers
|
|
298
317
|
if (cropRect) request.crop = cropRect
|
|
299
318
|
const dataUrl: string = await bridge.send(request)
|
|
319
|
+
phase('capture')
|
|
300
320
|
const pngPrefix = 'data:image/png;base64,'
|
|
301
321
|
// never write a 0-byte "saved" file from an empty or malformed payload —
|
|
302
322
|
// the sim may be alive but not painting (check `peach get errors`)
|
|
@@ -326,6 +346,8 @@ async function captureViaBridge(
|
|
|
326
346
|
} else {
|
|
327
347
|
await writeScreenshotOutput(outputPath, rawBuffer)
|
|
328
348
|
}
|
|
349
|
+
phase('write')
|
|
350
|
+
if (verbose) console.log(` timing: ${phases.join(', ')}`)
|
|
329
351
|
console.log(` saved: ${outputPath}`)
|
|
330
352
|
} finally {
|
|
331
353
|
bridge.close()
|
package/cli/commands/serve.ts
CHANGED
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
// run in the foreground (ctrl-c to stop). the persistent launchd/systemd
|
|
9
9
|
// daemon installation is handled by `peach daemon install`.
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
assertPeachPortEnvironment,
|
|
13
|
+
DEFAULT_SOOTSIM_BRIDGE_PORT,
|
|
14
|
+
} from '../../src/bridge-constants'
|
|
12
15
|
import {
|
|
13
16
|
ensureRnxHome,
|
|
14
17
|
isDaemonLockfileFresh,
|
|
@@ -58,6 +61,7 @@ examples:
|
|
|
58
61
|
peachExit(0)
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
assertPeachPortEnvironment(process.env)
|
|
61
65
|
const portArgIdx = args.indexOf('--port')
|
|
62
66
|
const port =
|
|
63
67
|
portArgIdx >= 0 && args[portArgIdx + 1]
|
|
@@ -147,7 +151,8 @@ examples:
|
|
|
147
151
|
if (sims.length > 0) lastUsedAt = Date.now()
|
|
148
152
|
else if (
|
|
149
153
|
starterGoneAt &&
|
|
150
|
-
Date.now() - Math.max(starterGoneAt, lastUsedAt, host.lastRequestAt) >=
|
|
154
|
+
Date.now() - Math.max(starterGoneAt, lastUsedAt, host.lastRequestAt) >=
|
|
155
|
+
UNUSED_EXIT_MS
|
|
151
156
|
) {
|
|
152
157
|
void shutdown('unused')
|
|
153
158
|
return
|
package/cli/commands/setup.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
writePrivacyPreferences,
|
|
18
18
|
} from '../../src/home-paths'
|
|
19
19
|
import { rnxRuntime } from '../../src/runtime-delivery'
|
|
20
|
-
import { findDesktopCompanion } from '../desktop-companion'
|
|
20
|
+
import { findDesktopCompanion, nativeDesktopIsDefault } from '../desktop-companion'
|
|
21
21
|
import { isCi, PRIVACY_NOTICE } from '../privacy'
|
|
22
22
|
import { confirm, select } from '../prompt'
|
|
23
23
|
import {
|
|
@@ -155,12 +155,7 @@ export async function runSetup(
|
|
|
155
155
|
serviceInstalled = true
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
if (
|
|
159
|
-
simulatorDriver !== 'playwright' &&
|
|
160
|
-
process.platform !== 'darwin' &&
|
|
161
|
-
!options.dryRun &&
|
|
162
|
-
(simulatorDriver === 'desktop' || !findDesktopCompanion({ native: false }))
|
|
163
|
-
) {
|
|
158
|
+
if (simulatorDriver !== 'playwright' && !options.dryRun && !findDesktopCompanion()) {
|
|
164
159
|
await runInstallDesktop(
|
|
165
160
|
simulatorDriver === 'desktop' ? ['--yes'] : ['--yes', '--electron'],
|
|
166
161
|
)
|
|
@@ -272,7 +267,7 @@ function readSimulatorDriverFlag(args: string[]): SimulatorDriverPreference | un
|
|
|
272
267
|
if (desktop && browser) {
|
|
273
268
|
throw new Error('--desktop and --browser cannot be used together')
|
|
274
269
|
}
|
|
275
|
-
if (desktop) return desktopHostPlatform()
|
|
270
|
+
if (desktop) return desktopHostPlatform() ? 'desktop' : 'electron'
|
|
276
271
|
if (browser) return 'playwright'
|
|
277
272
|
return undefined
|
|
278
273
|
}
|
|
@@ -331,46 +326,29 @@ async function decideService(
|
|
|
331
326
|
return confirm('Enable the background service?', true)
|
|
332
327
|
}
|
|
333
328
|
|
|
329
|
+
// the native app is the simulator window wherever it ships, and setup installs
|
|
330
|
+
// it without asking. Windows chooses between the browser and its Electron app.
|
|
334
331
|
async function decideSimulatorDriver(
|
|
335
332
|
options: SetupOptions,
|
|
336
333
|
): Promise<SimulatorDriverPreference> {
|
|
337
|
-
if (process.platform === 'darwin' && !findDesktopCompanion()) {
|
|
338
|
-
if (options.simulatorDriver && options.simulatorDriver !== 'playwright') {
|
|
339
|
-
throw new Error('install the Mac app with `peach desktop install --native` first')
|
|
340
|
-
}
|
|
341
|
-
return 'playwright'
|
|
342
|
-
}
|
|
343
334
|
if (options.simulatorDriver) return options.simulatorDriver
|
|
344
335
|
const current = readSimulatorDriverPreference()
|
|
345
336
|
if (current) return current
|
|
337
|
+
if (isCi()) return 'playwright'
|
|
338
|
+
if (nativeDesktopIsDefault()) return 'desktop'
|
|
339
|
+
if (process.platform !== 'win32') return 'playwright'
|
|
346
340
|
if (options.ci || options.yes || options.dryRun) return 'playwright'
|
|
347
341
|
requireInteractive(options, '--desktop or --browser')
|
|
348
|
-
const desktopDescription =
|
|
349
|
-
process.platform === 'darwin'
|
|
350
|
-
? 'Desktop opens your installed native runtime (beta).'
|
|
351
|
-
: process.platform === 'win32'
|
|
352
|
-
? 'Desktop downloads and runs the peach installer.'
|
|
353
|
-
: 'Desktop installs the native .deb with sudo apt-get and needs a desktop session.'
|
|
354
|
-
const desktopChoice =
|
|
355
|
-
process.platform === 'darwin'
|
|
356
|
-
? 'Native app (beta) Faster, may have more issues'
|
|
357
|
-
: process.platform === 'win32'
|
|
358
|
-
? 'Desktop app Runs the peach installer'
|
|
359
|
-
: 'Native app (beta) Faster, may have more issues'
|
|
360
342
|
printPromptBlock('Simulator window', [
|
|
361
343
|
'Browser opens immediately in an isolated Chrome window.',
|
|
362
|
-
|
|
363
|
-
'The Desktop app opens in a dedicated
|
|
344
|
+
'Desktop downloads and runs the peach installer.',
|
|
345
|
+
'The Desktop app opens in a dedicated window.',
|
|
364
346
|
])
|
|
365
347
|
const choice = await select('Where should peach open?', [
|
|
366
348
|
'Browser (default) No additional download',
|
|
367
|
-
|
|
349
|
+
'Desktop app Runs the peach installer',
|
|
368
350
|
])
|
|
369
|
-
return choice === 0
|
|
370
|
-
? 'playwright'
|
|
371
|
-
: process.platform === 'darwin'
|
|
372
|
-
? 'desktop'
|
|
373
|
-
: 'electron'
|
|
351
|
+
return choice === 0 ? 'playwright' : 'electron'
|
|
374
352
|
}
|
|
375
353
|
|
|
376
354
|
// diagnostics and crash reports are on unless turned off; setup says so once
|
|
@@ -433,15 +411,14 @@ function printIntro(options: SetupOptions): void {
|
|
|
433
411
|
if (options.json) return
|
|
434
412
|
printBrand('Run any React Native app in a local simulator. No project setup required.')
|
|
435
413
|
if (desktopHostPlatform()) {
|
|
436
|
-
printPromptBlock('
|
|
437
|
-
'
|
|
438
|
-
'
|
|
439
|
-
'Open
|
|
440
|
-
'Open a browser window: peach open <port> --driver playwright --headed',
|
|
414
|
+
printPromptBlock('Native desktop app', [
|
|
415
|
+
'peach opens your app in its native desktop app, which is faster than the browser.',
|
|
416
|
+
'Setup installs it, and peach open <port> uses it.',
|
|
417
|
+
'Open a browser window instead: peach open <port> --driver playwright --headed',
|
|
441
418
|
...(process.platform === 'linux'
|
|
442
|
-
? ['Linux
|
|
419
|
+
? ['On Linux the app installs with sudo apt-get and needs a desktop session.']
|
|
443
420
|
: []),
|
|
444
|
-
'The background service is a separate choice that keeps the bridge ready for either
|
|
421
|
+
'The background service is a separate choice that keeps the bridge ready for either window.',
|
|
445
422
|
])
|
|
446
423
|
}
|
|
447
424
|
}
|
|
@@ -451,7 +428,7 @@ function printHelp(): void {
|
|
|
451
428
|
peach setup - configure optional local conveniences
|
|
452
429
|
|
|
453
430
|
peach works without project setup. This command can enable the faster background
|
|
454
|
-
service,
|
|
431
|
+
service, install the native desktop app, configure private diagnostics, and optionally
|
|
455
432
|
add an exact peachsim dependency plus a peach script for teammates.
|
|
456
433
|
|
|
457
434
|
usage:
|
package/cli/commands/swift.ts
CHANGED
|
@@ -73,6 +73,11 @@ export async function runSwift(
|
|
|
73
73
|
} else if (arg === '--scheme') {
|
|
74
74
|
scheme = input[++index]
|
|
75
75
|
if (!scheme || scheme.startsWith('-')) throw new Error('--scheme requires a value')
|
|
76
|
+
} else if (arg === '--swift-sdks-path') {
|
|
77
|
+
const value = input[++index]
|
|
78
|
+
if (!value || value.startsWith('-'))
|
|
79
|
+
throw new Error('--swift-sdks-path requires a value')
|
|
80
|
+
process.env.PEACH_SWIFT_SDKS_PATH = path.resolve(value)
|
|
76
81
|
} else if (arg === '--host-port' || valueFlags.has(arg)) {
|
|
77
82
|
const value = input[++index]
|
|
78
83
|
if (!value || value.startsWith('-')) throw new Error(`${arg} requires a value`)
|