peachsim 0.1.629 → 0.1.631
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/README.md +8 -4
- package/cli/app-fonts.ts +23 -4
- package/cli/bridge-flow-runner.ts +45 -31
- package/cli/cli-update.ts +2 -7
- package/cli/cloud-test-tunnel.ts +16 -3
- package/cli/command-registry.ts +1 -0
- package/cli/commands/compat.ts +11 -60
- package/cli/commands/config.ts +1 -1
- package/cli/commands/control.ts +40 -8
- package/cli/commands/daemon-mac-app.ts +4 -2
- package/cli/commands/daemon.ts +4 -4
- package/cli/commands/detect.ts +289 -26
- package/cli/commands/device.ts +2 -3
- package/cli/commands/inspect/actions.ts +125 -90
- package/cli/commands/inspect/core.ts +1 -1
- package/cli/commands/inspect.ts +10 -1
- package/cli/commands/maestro.ts +3 -1
- package/cli/commands/platform.ts +1 -1
- package/cli/commands/project-report.ts +386 -0
- package/cli/commands/projectReportContract.ts +42 -0
- package/cli/commands/report-issue.ts +38 -54
- package/cli/commands/screenshot-layers.ts +1 -1
- package/cli/commands/screenshot.ts +1 -1
- package/cli/commands/setup.ts +6 -4
- package/cli/commands/skills.ts +0 -31
- package/cli/commands/telemetry.ts +8 -3
- package/cli/commands/test.ts +347 -134
- package/cli/drivers/playwright-provisioning.ts +3 -2
- package/cli/drivers/playwright-sim-host.ts +25 -9
- package/cli/main.ts +19 -3
- package/cli/outbound-endpoints.ts +2 -5
- package/cli/privacy.ts +1 -2
- package/cli/runtime-diagnostics.ts +51 -0
- package/cli/telemetry.ts +78 -7
- package/detox/index.ts +77 -51
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.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 +1 -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 +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +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 +1 -1
- package/dist-lib/detox/index.cjs +130 -33
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +8 -5
- package/dist-lib/host/bridge-host.cjs +353 -290
- 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 +257 -121
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/{vite-kotlin.cjs → kotlin-host.cjs} +130 -19
- package/dist-lib/menu.cjs +11 -1
- package/dist-lib/menu.mjs +11 -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 +132 -9
- package/dist-lib/native-app-identity.cjs +278 -0
- package/dist-lib/native-app-identity.mjs +245 -0
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +112 -87
- package/dist-lib/sdk.mjs +111 -87
- package/dist-lib/skills.cjs +1270 -709
- package/dist-lib/swift-compile.cjs +1 -1
- package/dist-lib/swift-compile.mjs +1 -1
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +672 -275
- package/dist-lib/vite-swift.cjs +1 -1
- package/dist-lib/vite.cjs +11 -28
- package/kotlin/README.md +32 -4
- package/kotlin/fixtures/counter/Counter.kt +26 -0
- package/kotlin/fixtures/counter/CounterScreen.kt +40 -0
- package/kotlin/fixtures/headless-counter/Headless.kt +228 -0
- package/kotlin/fixtures/runtime-state/RuntimeState.kt +200 -0
- package/kotlin/fork/LICENSE +178 -0
- package/kotlin/fork/NOTICE +25 -0
- package/kotlin/fork/README.md +59 -0
- package/kotlin/fork/build.sh +128 -0
- package/kotlin/fork/compose-worker-globals.patch +96 -0
- package/kotlin/fork/fork.json +33 -0
- package/kotlin/fork/publish.ts +77 -0
- package/kotlin/fork/skiko-worker-clock.patch +33 -0
- package/kotlin/toolchain.json +288 -0
- package/package.json +13 -5
- package/skills/contrast/SKILL.md +1 -0
- package/src/automatic-update-policy.ts +17 -0
- package/src/home-paths.ts +11 -9
- package/src/host/bridge-host.ts +3 -1
- package/src/{vite-plugin-kotlin.ts → kotlin-host.ts} +155 -12
- package/src/menu.ts +11 -0
- package/src/metro-plugin.ts +18 -0
- package/src/native-app-identity.ts +376 -0
- package/src/native-firebase-config.ts +90 -0
- package/src/screenshots/frame-compose.ts +1 -1
- package/src/screenshots/registry.ts +1 -1
- package/src/screenshots/schema.ts +2 -2
- package/src/vite-plugin-one.ts +11 -28
- package/src/vite-plugin.ts +6 -1
package/README.md
CHANGED
|
@@ -15,8 +15,10 @@ The executable updates itself with `peach upgrade`. Successful interactive human
|
|
|
15
15
|
commands refresh a cached stable CLI version in a detached process at most once
|
|
16
16
|
every 20 hours. A newer version produces one short notice on stderr, once per
|
|
17
17
|
version and no more than weekly. CI, agents, JSON output, pipes, and
|
|
18
|
-
noninteractive commands stay silent. Set `PEACH_UPDATE_CHECK=off` to disable
|
|
19
|
-
|
|
18
|
+
noninteractive commands stay silent. Set `PEACH_UPDATE_CHECK=off` to disable
|
|
19
|
+
automatic CLI checks, runtime updates, and their notices. Start the bridge with
|
|
20
|
+
this environment, or reinstall the background service with it to persist the choice.
|
|
21
|
+
`peach version` checks explicitly, while `peach --version` remains
|
|
20
22
|
offline.
|
|
21
23
|
|
|
22
24
|
Neither artifact contains rendering code. The canvas engine (CanvasKit
|
|
@@ -233,7 +235,7 @@ Pass a target to `peach open <port-or-url>` when a script or agent needs an expl
|
|
|
233
235
|
selection.
|
|
234
236
|
|
|
235
237
|
The first `peach open` launches an isolated Chrome for Testing sim. The CLI
|
|
236
|
-
resolves Playwright from the
|
|
238
|
+
resolves Playwright from the current directory or beside its executable, asks that exact package for
|
|
237
239
|
its required browser executable, and runs that package's installer once when
|
|
238
240
|
the executable is absent. It never substitutes another revision found in the
|
|
239
241
|
shared Playwright cache. Later opens reuse that sim and reload it at the
|
|
@@ -557,7 +559,9 @@ frame for quick art-direction passes.
|
|
|
557
559
|
## Skills registry
|
|
558
560
|
|
|
559
561
|
`peach skill install` installs Peach agent skills in the standard
|
|
560
|
-
`<skill-name>/SKILL.md` layout used by Codex and Claude Code.
|
|
562
|
+
`<skill-name>/SKILL.md` layout used by Codex and Claude Code. Building the CLI
|
|
563
|
+
only writes build artifacts; run `peach skill install` explicitly to install
|
|
564
|
+
or refresh agent skills. The bundled
|
|
561
565
|
skills cover setup, debugging/perf/accessibility, testing, visual review, and
|
|
562
566
|
the generated `contrast` command index. The published CLI registry and generated
|
|
563
567
|
website docs both come from `packages/peach-skills/`:
|
package/cli/app-fonts.ts
CHANGED
|
@@ -139,6 +139,16 @@ export function discoverNativeLinkedAppFonts(opts: {
|
|
|
139
139
|
collectFontFiles(root, relativeRoot.split(path.sep).join('/'))
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// the bundle path is the entry relative to metro's root, so a bundle at
|
|
143
|
+
// /apps/app/index.bundle served for /repo/apps/app puts that root at /repo.
|
|
144
|
+
const bundleDir = path.posix
|
|
145
|
+
.dirname(new URL(opts.bundleUrl).pathname)
|
|
146
|
+
.replace(/^\/+/, '')
|
|
147
|
+
const appRelativeDir = bundleDir.split('/').join(path.sep)
|
|
148
|
+
const metroRootCandidates = [selectedAppDir]
|
|
149
|
+
if (bundleDir && selectedAppDir.endsWith(path.sep + appRelativeDir)) {
|
|
150
|
+
metroRootCandidates.push(selectedAppDir.slice(0, -appRelativeDir.length - 1))
|
|
151
|
+
}
|
|
142
152
|
const vectorIconRequire = createRequire(path.join(selectedAppDir, 'package.json'))
|
|
143
153
|
for (const dependencyName of vectorIconDependencies) {
|
|
144
154
|
try {
|
|
@@ -150,10 +160,19 @@ export function discoverNativeLinkedAppFonts(opts: {
|
|
|
150
160
|
const packageRoot = entryPath.slice(0, markerIndex + marker.length - 1)
|
|
151
161
|
const fontsDir =
|
|
152
162
|
dependencyName === 'react-native-vector-icons' ? 'Fonts' : 'fonts'
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
163
|
+
// the package can sit outside the app (a hoisted workspace
|
|
164
|
+
// node_modules or a pnpm store). metro resolves asset paths against
|
|
165
|
+
// its config.projectRoot, which is the app in expo's monorepo setup
|
|
166
|
+
// but the repo root when metro.config sets it there, so offer the
|
|
167
|
+
// path from each candidate root; the reachability check keeps the
|
|
168
|
+
// one metro actually serves.
|
|
169
|
+
const fontsRoot = path.join(packageRoot, fontsDir)
|
|
170
|
+
for (const metroRoot of metroRootCandidates) {
|
|
171
|
+
collectFontFiles(
|
|
172
|
+
fontsRoot,
|
|
173
|
+
path.relative(metroRoot, fontsRoot).split(path.sep).join('/'),
|
|
174
|
+
)
|
|
175
|
+
}
|
|
157
176
|
} catch {}
|
|
158
177
|
}
|
|
159
178
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs'
|
|
2
2
|
import * as path from 'path'
|
|
3
|
-
import { devices, type DeviceModel } from 'peach-engine/settings'
|
|
3
|
+
import { devices, type DeviceModel } from 'peach-engine/settings/devices'
|
|
4
4
|
import yaml from 'yaml'
|
|
5
5
|
import { REQUIRED_IDENTICAL_FLOW_FRAMES, framesIdentical } from '../detox/proof-frame.cjs'
|
|
6
6
|
import { flowTimeoutScale } from '../src/flow-timeout-scale'
|
|
@@ -1314,6 +1314,9 @@ export class SootSimBridgeFlowRunner {
|
|
|
1314
1314
|
|
|
1315
1315
|
async tapOn(target: string | FlowPointTarget) {
|
|
1316
1316
|
const opts = typeof target === 'string' ? { text: target } : target
|
|
1317
|
+
// maestro settles before resolving a tap's final position as well as after
|
|
1318
|
+
// delivering it. a preceding assertion does not settle a pending update.
|
|
1319
|
+
await this.waitForAppToSettle()
|
|
1317
1320
|
const repeat = Math.max(1, Math.trunc(opts.repeat ?? 1))
|
|
1318
1321
|
const repeatDelay = opts.delay !== undefined && opts.delay >= 0 ? opts.delay : 100
|
|
1319
1322
|
// maestro Maestro.hierarchyBasedTap with a TapRepeat: every tap is padded
|
|
@@ -2379,14 +2382,14 @@ export class SootSimBridgeFlowRunner {
|
|
|
2379
2382
|
}
|
|
2380
2383
|
}
|
|
2381
2384
|
|
|
2382
|
-
// maestro waits
|
|
2385
|
+
// maestro waits around taps for the screen to stop changing, up to
|
|
2383
2386
|
// its ios driver's 3 s settle budget, comparing screenshots, so an action
|
|
2384
2387
|
// still in flight (a submit spinner, a sheet closing) finishes before the
|
|
2385
2388
|
// next command resolves its target. an app can take a few hundred ms to
|
|
2386
2389
|
// react to a press, so stillness must hold about half a second, and it
|
|
2387
2390
|
// counts painted frames because a spinner repaints without moving layout.
|
|
2388
2391
|
// a screen that never stops proceeds when the budget runs out, as there.
|
|
2389
|
-
private async
|
|
2392
|
+
private async waitForAppToSettle() {
|
|
2390
2393
|
await waitForSootsimIdle({
|
|
2391
2394
|
bridge: this.bridge,
|
|
2392
2395
|
simId: this.opts.simId,
|
|
@@ -2396,6 +2399,10 @@ export class SootSimBridgeFlowRunner {
|
|
|
2396
2399
|
strict: true,
|
|
2397
2400
|
paintStill: true,
|
|
2398
2401
|
})
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
private async settleAfterInput() {
|
|
2405
|
+
await this.waitForAppToSettle()
|
|
2399
2406
|
this.lastInteractionAt = Date.now()
|
|
2400
2407
|
}
|
|
2401
2408
|
|
|
@@ -2703,38 +2710,45 @@ export class SootSimBridgeFlowRunner {
|
|
|
2703
2710
|
}
|
|
2704
2711
|
|
|
2705
2712
|
try {
|
|
2706
|
-
//
|
|
2707
|
-
//
|
|
2708
|
-
//
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
step.
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2713
|
+
// simple commands have a wall-clock watchdog. bounded operations keep
|
|
2714
|
+
// their own lookup, settle and bridge deadlines, so the watchdog cannot
|
|
2715
|
+
// report a completed step while its action continues in the background.
|
|
2716
|
+
const hasBoundedWait = !!(
|
|
2717
|
+
// taps combine bounded lookup, settling and delivery. a wall-clock
|
|
2718
|
+
// watchdog can finish the step while that action is still running.
|
|
2719
|
+
// let these operations finish before starting the next command.
|
|
2720
|
+
(
|
|
2721
|
+
step.tapOn ||
|
|
2722
|
+
step.doubleTapOn ||
|
|
2723
|
+
step.longPressOn ||
|
|
2724
|
+
step.extendedWaitUntil ||
|
|
2725
|
+
step.waitFor ||
|
|
2726
|
+
step.scrollUntilVisible ||
|
|
2727
|
+
step.launchApp ||
|
|
2728
|
+
step.runFlow ||
|
|
2729
|
+
// retry reruns its body on failure, so its wall time is the body's
|
|
2730
|
+
// times the retry count — the 10s no-progress watchdog is the wrong
|
|
2731
|
+
// budget and would report "bridge probably hung" for a slow retry.
|
|
2732
|
+
step.retry ||
|
|
2733
|
+
// runScript http calls are synchronous with a 5-minute upstream
|
|
2734
|
+
// timeout — the 10s watchdog would kill legitimate setup scripts.
|
|
2735
|
+
step.runScript ||
|
|
2736
|
+
// takeScreenshot self-governs: it retries a bounded visual settle that
|
|
2737
|
+
// already fails with its own reason. sharing the watchdog's 10s budget
|
|
2738
|
+
// meant the watchdog always won that race, reporting "bridge probably
|
|
2739
|
+
// hung" for a screen that was merely still animating.
|
|
2740
|
+
step.takeScreenshot ||
|
|
2741
|
+
// assertions wait out maestro's lookup window, longer than 10s, and
|
|
2742
|
+
// every query they make times out on its own, so they report their
|
|
2743
|
+
// own failure instead of the watchdog's guess.
|
|
2744
|
+
step.assertVisible ||
|
|
2745
|
+
step.assertNotVisible
|
|
2746
|
+
)
|
|
2733
2747
|
)
|
|
2734
2748
|
const STEP_WATCHDOG_MS = 10_000
|
|
2735
2749
|
const body = this.runStepInner(step)
|
|
2736
2750
|
let screenshotPath: string | undefined
|
|
2737
|
-
if (
|
|
2751
|
+
if (hasBoundedWait) {
|
|
2738
2752
|
const result = await body
|
|
2739
2753
|
if (typeof result === 'string') screenshotPath = result
|
|
2740
2754
|
} else {
|
package/cli/cli-update.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { compareSemver } from '@contrast/runtime-delivery'
|
|
4
|
+
import { automaticPeachUpdatesEnabled } from '../src/automatic-update-policy'
|
|
4
5
|
import { getCliVersion } from '../src/cli-version'
|
|
5
6
|
import { cliUpdateFilePath } from '../src/home-paths'
|
|
6
7
|
import { isAgentEnv } from './commands/inspect/env'
|
|
@@ -127,12 +128,6 @@ function envFlagIsOn(value: string | undefined): boolean {
|
|
|
127
128
|
return Boolean(value) && !envFlagIsOff(value)
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
export function automaticCliUpdateChecksEnabled(
|
|
131
|
-
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
132
|
-
): boolean {
|
|
133
|
-
return !envFlagIsOff(env.PEACH_UPDATE_CHECK)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
131
|
export function shouldUseAutomaticCliUpdateCheck(
|
|
137
132
|
invocation: CliUpdateInvocation,
|
|
138
133
|
): boolean {
|
|
@@ -143,7 +138,7 @@ export function shouldUseAutomaticCliUpdateCheck(
|
|
|
143
138
|
invocation.version ||
|
|
144
139
|
!invocation.stdoutIsTTY ||
|
|
145
140
|
!invocation.stderrIsTTY ||
|
|
146
|
-
!
|
|
141
|
+
!automaticPeachUpdatesEnabled(env) ||
|
|
147
142
|
envFlagIsOn(env.CI) ||
|
|
148
143
|
envFlagIsOn(env.GITHUB_ACTIONS) ||
|
|
149
144
|
envFlagIsOn(env.PEACH_NO_PROMPT) ||
|
package/cli/cloud-test-tunnel.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { createServer, request as httpRequest } from 'node:http'
|
|
27
27
|
import { tmpdir } from 'node:os'
|
|
28
28
|
import { join } from 'node:path'
|
|
29
|
+
import { pipeline } from 'node:stream'
|
|
29
30
|
import { sleep } from '../src/sleep'
|
|
30
31
|
|
|
31
32
|
// must match packages/contrast-runner/src/hostedPorts.ts.
|
|
@@ -160,12 +161,24 @@ export function startCloudTestTokenProxy(
|
|
|
160
161
|
},
|
|
161
162
|
(upRes) => {
|
|
162
163
|
res.writeHead(upRes.statusCode || 502, upRes.headers)
|
|
163
|
-
|
|
164
|
+
// a broken upstream body must break the downstream response too. pipe()
|
|
165
|
+
// leaves it open after an aborted transfer, stranding the tester.
|
|
166
|
+
pipeline(upRes, res, (error) => {
|
|
167
|
+
if (error) {
|
|
168
|
+
console.error(
|
|
169
|
+
`upstream body error ${req.method} ${req.url}: ${error.message}`,
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
})
|
|
164
173
|
},
|
|
165
174
|
)
|
|
166
175
|
up.on('error', (err) => {
|
|
167
|
-
|
|
168
|
-
|
|
176
|
+
// a transport failure is a network error, including before headers
|
|
177
|
+
// arrive; ending a partial response would report a successful transfer.
|
|
178
|
+
res.destroy(err)
|
|
179
|
+
})
|
|
180
|
+
res.once('close', () => {
|
|
181
|
+
if (!res.writableFinished) up.destroy()
|
|
169
182
|
})
|
|
170
183
|
req.pipe(up)
|
|
171
184
|
})
|
package/cli/command-registry.ts
CHANGED
package/cli/commands/compat.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// peach compat — scan dependencies with the shared compatibility engine.
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { dirname, join, relative, resolve } from 'node:path'
|
|
3
|
+
import { relative, resolve } from 'node:path'
|
|
5
4
|
import {
|
|
6
5
|
COMPAT_RESULT_CAVEAT,
|
|
7
6
|
formatDetailedScanLines,
|
|
@@ -9,13 +8,13 @@ import {
|
|
|
9
8
|
scanDeps,
|
|
10
9
|
type ScanResult,
|
|
11
10
|
} from '@peach/compat/web'
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
11
|
+
import {
|
|
12
|
+
findProjectRoot,
|
|
13
|
+
scanWorkspace,
|
|
14
|
+
installedManifest,
|
|
15
|
+
readProjectManifest,
|
|
16
|
+
} from './detect'
|
|
17
|
+
import type { ProjectManifest } from './projectReportContract'
|
|
19
18
|
|
|
20
19
|
export async function runCompat(args: string[]): Promise<void> {
|
|
21
20
|
if (args.includes('--help') || args.includes('-h')) {
|
|
@@ -41,7 +40,7 @@ flags:
|
|
|
41
40
|
const packageName = args.find(
|
|
42
41
|
(arg, index) => !arg.startsWith('-') && (appIndex < 0 || index !== appIndex + 1),
|
|
43
42
|
)
|
|
44
|
-
const pkg =
|
|
43
|
+
const pkg = readProjectManifest(appDir)
|
|
45
44
|
if (!pkg) {
|
|
46
45
|
console.error(` no package.json found in ${appDir}`)
|
|
47
46
|
process.exitCode = 1
|
|
@@ -65,7 +64,7 @@ flags:
|
|
|
65
64
|
const workspaceRoot = findProjectRoot(appDir)
|
|
66
65
|
const elsewhere = scanWorkspace(workspaceRoot).apps.flatMap((app) => {
|
|
67
66
|
if (resolve(app.dir) === appDir) return []
|
|
68
|
-
const manifest =
|
|
67
|
+
const manifest = readProjectManifest(app.dir)
|
|
69
68
|
if (!manifest) return []
|
|
70
69
|
const result = scanDeps(installedManifest(app.dir, manifest))
|
|
71
70
|
return result.packages.length > 0 ? [{ dir: app.dir, result }] : []
|
|
@@ -114,57 +113,9 @@ function report(args: string[], result: ScanResult): void {
|
|
|
114
113
|
console.log(`\n ${COMPAT_RESULT_CAVEAT}\n`)
|
|
115
114
|
}
|
|
116
115
|
|
|
117
|
-
// the declared range is often not a version: pnpm and bun catalogs write
|
|
118
|
-
// `catalog:`, a workspace dependency writes `workspace:*`, and a pinned tarball
|
|
119
|
-
// writes a URL. none of those match a verified version range, so a whole app
|
|
120
|
-
// reports as unverified. the installed tree holds the version that will
|
|
121
|
-
// actually run, so ask it, and keep the declared range for anything the project
|
|
122
|
-
// has not installed.
|
|
123
|
-
function installedManifest(appDir: string, pkg: PackageManifest): PackageManifest {
|
|
124
|
-
const resolveVersion = (name: string, declared: string): string => {
|
|
125
|
-
let dir = appDir
|
|
126
|
-
while (true) {
|
|
127
|
-
const manifest = join(dir, 'node_modules', name, 'package.json')
|
|
128
|
-
if (existsSync(manifest)) {
|
|
129
|
-
try {
|
|
130
|
-
const parsed: unknown = JSON.parse(readFileSync(manifest, 'utf8'))
|
|
131
|
-
if (parsed && typeof parsed === 'object' && 'version' in parsed) {
|
|
132
|
-
const version = parsed.version
|
|
133
|
-
if (typeof version === 'string') return version
|
|
134
|
-
}
|
|
135
|
-
} catch {}
|
|
136
|
-
return declared
|
|
137
|
-
}
|
|
138
|
-
const parent = dirname(dir)
|
|
139
|
-
if (parent === dir) return declared
|
|
140
|
-
dir = parent
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
const mapped = (deps: Record<string, string> | undefined) =>
|
|
144
|
-
deps &&
|
|
145
|
-
Object.fromEntries(
|
|
146
|
-
Object.entries(deps).map(([name, declared]) => [
|
|
147
|
-
name,
|
|
148
|
-
resolveVersion(name, declared),
|
|
149
|
-
]),
|
|
150
|
-
)
|
|
151
|
-
return {
|
|
152
|
-
name: pkg.name,
|
|
153
|
-
dependencies: mapped(pkg.dependencies),
|
|
154
|
-
devDependencies: mapped(pkg.devDependencies),
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function readPackageJson(appDir: string): PackageManifest | null {
|
|
159
|
-
const packagePath = resolve(appDir, 'package.json')
|
|
160
|
-
if (!existsSync(packagePath)) return null
|
|
161
|
-
const parsed: unknown = JSON.parse(readFileSync(packagePath, 'utf8'))
|
|
162
|
-
return parsed && typeof parsed === 'object' ? parsed : null
|
|
163
|
-
}
|
|
164
|
-
|
|
165
116
|
function scanOnePackage(
|
|
166
117
|
appDir: string,
|
|
167
|
-
pkg:
|
|
118
|
+
pkg: ProjectManifest,
|
|
168
119
|
packageName: string,
|
|
169
120
|
): ScanResult {
|
|
170
121
|
const installed = installedManifest(appDir, pkg)
|
package/cli/commands/config.ts
CHANGED
|
@@ -11,7 +11,7 @@ usage:
|
|
|
11
11
|
environment:
|
|
12
12
|
PEACH_HINTS=off suppress CLI hints
|
|
13
13
|
PEACH_HINTS=always show every CLI hint every time
|
|
14
|
-
PEACH_UPDATE_CHECK=off disable
|
|
14
|
+
PEACH_UPDATE_CHECK=off disable automatic CLI checks, runtime updates, and notices
|
|
15
15
|
`)
|
|
16
16
|
}
|
|
17
17
|
|
package/cli/commands/control.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
listSelectableDeviceModels,
|
|
9
9
|
resolveDeviceFitBox,
|
|
10
10
|
type DeviceModel,
|
|
11
|
-
} from 'peach-engine/settings'
|
|
11
|
+
} from 'peach-engine/settings/devices'
|
|
12
12
|
import { WebSocket } from 'ws'
|
|
13
13
|
import { probePort } from '../../scripts/dev-server-scanner'
|
|
14
14
|
import { isAppDeepLinkTarget, openAppUrl } from '../../src/app-url'
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
runtimeDir,
|
|
37
37
|
} from '../../src/home-paths'
|
|
38
38
|
import { applyHotMode } from '../../src/native-dev-bundle-url'
|
|
39
|
+
import { readNativeFirebaseConfig } from '../../src/native-firebase-config'
|
|
39
40
|
import { ensureProfile } from '../../src/profiles'
|
|
40
41
|
import { rnxPublicBrand } from '../../src/public-brand'
|
|
41
42
|
import {
|
|
@@ -72,7 +73,9 @@ import {
|
|
|
72
73
|
} from '../current-sim'
|
|
73
74
|
import { getDriver } from '../drivers/registry'
|
|
74
75
|
import { openUrl as openUrlInBrowser } from '../open-url'
|
|
76
|
+
import { diagnosticsAllowedForCurrentProcess } from '../privacy'
|
|
75
77
|
import { rethrowIfExit, rnxExit } from '../run-rnx'
|
|
78
|
+
import { collectRuntimeDiagnostics } from '../runtime-diagnostics'
|
|
76
79
|
import { rnxSelfInvocation } from '../self-invocation'
|
|
77
80
|
import {
|
|
78
81
|
BridgeSimLockedError,
|
|
@@ -694,11 +697,13 @@ async function filterReachableAppFonts(
|
|
|
694
697
|
}
|
|
695
698
|
}),
|
|
696
699
|
)
|
|
697
|
-
|
|
700
|
+
// a font offered from more than one candidate metro root keeps the first
|
|
701
|
+
// url metro served.
|
|
702
|
+
const reachable = new Map<string, DiscoveredAppFontSpec>()
|
|
698
703
|
for (const spec of results) {
|
|
699
|
-
if (spec !== null) reachable.
|
|
704
|
+
if (spec !== null && !reachable.has(spec.family)) reachable.set(spec.family, spec)
|
|
700
705
|
}
|
|
701
|
-
return reachable
|
|
706
|
+
return [...reachable.values()]
|
|
702
707
|
}
|
|
703
708
|
|
|
704
709
|
async function resolveShellUrl(
|
|
@@ -774,6 +779,26 @@ async function stampAppSplash(
|
|
|
774
779
|
}
|
|
775
780
|
}
|
|
776
781
|
|
|
782
|
+
function stampNativeFirebaseConfig(
|
|
783
|
+
urlStr: string,
|
|
784
|
+
project?: ResolvedAppProject | null,
|
|
785
|
+
): string {
|
|
786
|
+
const url = new URL(urlStr)
|
|
787
|
+
if (!url.searchParams.get('bundle') || !project) return urlStr
|
|
788
|
+
try {
|
|
789
|
+
url.searchParams.set(
|
|
790
|
+
'nativeFirebase',
|
|
791
|
+
JSON.stringify(readNativeFirebaseConfig(project.appDir)),
|
|
792
|
+
)
|
|
793
|
+
} catch (error) {
|
|
794
|
+
url.searchParams.set(
|
|
795
|
+
'nativeFirebase',
|
|
796
|
+
JSON.stringify({ error: error instanceof Error ? error.message : String(error) }),
|
|
797
|
+
)
|
|
798
|
+
}
|
|
799
|
+
return url.toString()
|
|
800
|
+
}
|
|
801
|
+
|
|
777
802
|
// stamp the engine page url with `?appCatalog=` so an app's asset-catalog
|
|
778
803
|
// colors and its declared accent reach the tenant, which applies them to the
|
|
779
804
|
// bundle this url opens. the catalog lives in the native binary, never in
|
|
@@ -867,8 +892,11 @@ export async function buildShellUrl(
|
|
|
867
892
|
// fallback typeface, which then reads as a conformance diff.
|
|
868
893
|
const project =
|
|
869
894
|
opts.project ?? (await resolveAppProjectForBundleUrl(bundleUrlOf(resolved)))
|
|
870
|
-
const stamped =
|
|
871
|
-
|
|
895
|
+
const stamped = stampNativeFirebaseConfig(
|
|
896
|
+
stampAppCatalog(
|
|
897
|
+
await stampAppSplash(await stampAppFonts(resolved, project), project),
|
|
898
|
+
project,
|
|
899
|
+
),
|
|
872
900
|
project,
|
|
873
901
|
)
|
|
874
902
|
if (
|
|
@@ -912,6 +940,7 @@ function deriveCurrentSimBaseUrl(
|
|
|
912
940
|
// bundle.
|
|
913
941
|
url.searchParams.delete('appSplash')
|
|
914
942
|
url.searchParams.delete('appCatalog')
|
|
943
|
+
url.searchParams.delete('nativeFirebase')
|
|
915
944
|
return url.toString()
|
|
916
945
|
} catch {
|
|
917
946
|
return fallback
|
|
@@ -1268,7 +1297,8 @@ function printAgentListenHint(quiet: boolean): void {
|
|
|
1268
1297
|
// a splash; the caller can `peach describe` again a second later. for
|
|
1269
1298
|
// warm loads it's the real screen. guarded by --no-describe.
|
|
1270
1299
|
async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: string[]) {
|
|
1271
|
-
|
|
1300
|
+
const noDescribe = args.includes('--no-describe')
|
|
1301
|
+
if (noDescribe && !diagnosticsAllowedForCurrentProcess()) return
|
|
1272
1302
|
const prevQuiet = process.env.SOOTSIM_QUIET_TARGET_NOTICE
|
|
1273
1303
|
process.env.SOOTSIM_QUIET_TARGET_NOTICE = '1'
|
|
1274
1304
|
try {
|
|
@@ -1277,7 +1307,7 @@ async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: strin
|
|
|
1277
1307
|
// seconds on a still-routing guest app, which is exactly what we want
|
|
1278
1308
|
// to skip for the open-side describe. caller can `peach describe`
|
|
1279
1309
|
// again later for post-routing state.
|
|
1280
|
-
await waitForTreeStable(wsPort, simId, { stableMs: 150, maxMs: 400 })
|
|
1310
|
+
if (!noDescribe) await waitForTreeStable(wsPort, simId, { stableMs: 150, maxMs: 400 })
|
|
1281
1311
|
const bridge = createBridge(wsPort, {
|
|
1282
1312
|
commandTimeoutMs: 3000,
|
|
1283
1313
|
simId,
|
|
@@ -1285,6 +1315,7 @@ async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: strin
|
|
|
1285
1315
|
simIdSource: 'flag',
|
|
1286
1316
|
})
|
|
1287
1317
|
try {
|
|
1318
|
+
if (noDescribe) return
|
|
1288
1319
|
let readyProbe: Awaited<ReturnType<typeof inspectWaitReady>> | null = null
|
|
1289
1320
|
try {
|
|
1290
1321
|
readyProbe = await inspectWaitReady(bridge, 1)
|
|
@@ -1316,6 +1347,7 @@ async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: strin
|
|
|
1316
1347
|
console.log('')
|
|
1317
1348
|
console.log(result.tree)
|
|
1318
1349
|
} finally {
|
|
1350
|
+
await collectRuntimeDiagnostics(bridge)
|
|
1319
1351
|
bridge.close()
|
|
1320
1352
|
}
|
|
1321
1353
|
} catch {
|
|
@@ -135,8 +135,10 @@ function renderLauncherScript(
|
|
|
135
135
|
const quoted = parts.map(shellSingleQuote).join(' ')
|
|
136
136
|
const stdout = shellSingleQuote(logDir.stdout)
|
|
137
137
|
const stderr = shellSingleQuote(logDir.stderr)
|
|
138
|
-
const
|
|
139
|
-
|
|
138
|
+
const environment = ['PEACH_HOME', 'PEACH_UPDATE_CHECK']
|
|
139
|
+
.filter((key) => process.env[key])
|
|
140
|
+
.map((key) => `export ${key}=${shellSingleQuote(process.env[key] ?? '')}\n`)
|
|
141
|
+
.join('')
|
|
140
142
|
return `#!/bin/sh
|
|
141
143
|
# peach daemon launcher generated by 'peach daemon install'.
|
|
142
144
|
# launchd spawns this; we exec the real peach invocation in place so
|
package/cli/commands/daemon.ts
CHANGED
|
@@ -149,10 +149,10 @@ ${programArgs}
|
|
|
149
149
|
function renderSystemdUnit(invocation: RnxSelfInvocation, port: number): string {
|
|
150
150
|
mkdirSync(LOG_DIR_LINUX, { recursive: true })
|
|
151
151
|
const exec = [invocation.executable, ...invocation.prefixArgs].map(shellQuote).join(' ')
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
152
|
+
const environment = ['PEACH_HOME', 'PEACH_UPDATE_CHECK']
|
|
153
|
+
.filter((key) => process.env[key])
|
|
154
|
+
.map((key) => `Environment=${shellQuote(`${key}=${process.env[key]}`)}\n`)
|
|
155
|
+
.join('')
|
|
156
156
|
return `[Unit]
|
|
157
157
|
Description=peach bridge daemon
|
|
158
158
|
After=default.target
|