peachsim 0.1.1625 → 0.1.1640
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 +15 -2
- package/cli/app-config.ts +18 -6
- package/cli/app-fonts.ts +37 -17
- package/cli/app-stubs.ts +284 -20
- package/cli/bridge-flow-runner.ts +66 -45
- package/cli/cloud-client.ts +60 -40
- package/cli/command-registry.ts +1 -0
- package/cli/commands/control.ts +283 -49
- package/cli/commands/doctor.ts +30 -189
- 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/shared.ts +9 -4
- package/cli/commands/inspect/wait-ready.ts +12 -0
- package/cli/commands/inspect.ts +91 -50
- 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/preview.ts +1 -1
- package/cli/commands/record.ts +118 -48
- 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 +14 -3
- package/cli/commands/upload.ts +93 -48
- package/cli/desktop-companion.ts +102 -17
- package/cli/drivers/desktop.ts +14 -1
- package/cli/drivers/playwright-sim-host.ts +103 -45
- 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 +3 -2
- package/cli/outbound-endpoints.ts +20 -6
- package/cli/pause-aware-timeout.ts +106 -0
- package/cli/scene-video.ts +89 -0
- package/cli/ws-bridge.ts +37 -19
- package/detox/attempt-events.cjs +66 -5
- package/detox/attempt-events.d.cts +7 -0
- package/detox/expectations.ts +7 -3
- package/detox/index.ts +269 -170
- package/detox/motion-change.cjs +206 -21
- package/detox/motion-change.d.cts +39 -2
- package/detox/navigation-lifecycle.ts +35 -3
- 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 +8 -7
- 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 +11887 -7900
- 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 +1379 -725
- package/dist-lib/dev-bundle-resolution.cjs +14 -2
- package/dist-lib/expo-swift-bundle.cjs +9341 -5462
- package/dist-lib/expo-swift-metadata.cjs +11 -7
- package/dist-lib/expo-swift-metadata.mjs +11 -7
- package/dist-lib/home-paths.cjs +32 -3
- package/dist-lib/host/bridge-host.cjs +50204 -7258
- 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 +3541 -448
- 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 +2931 -104
- package/dist-lib/native-app-identity-project.cjs +289 -49
- package/dist-lib/native-app-identity-project.mjs +289 -49
- package/dist-lib/native-app-identity.cjs +77 -26
- package/dist-lib/native-app-identity.mjs +76 -26
- 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 +6 -2
- package/dist-lib/react-native-host-modules.mjs +6 -2
- package/dist-lib/scripts/dev-server-scanner.cjs +22 -8
- package/dist-lib/sdk.cjs +27 -11
- package/dist-lib/sdk.mjs +27 -10
- package/dist-lib/skills.cjs +72201 -26208
- package/dist-lib/swift-compile.cjs +1007 -140
- package/dist-lib/swift-compile.mjs +1049 -174
- package/dist-lib/swift-package.cjs +12 -3
- package/dist-lib/swift-package.mjs +12 -3
- package/dist-lib/swift-submit.cjs +13 -5
- package/dist-lib/swift-submit.mjs +11 -5
- package/dist-lib/swift-toolchain.cjs +423 -33
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +8628 -5268
- package/dist-lib/vite.cjs +2859 -12
- package/dist-lib/worklet-executable.cjs +14 -2
- package/dist-lib/worklet-executable.mjs +14 -2
- 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/backend-origin.ts +13 -6
- 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 +196 -49
- package/src/expo-swift-ready.ts +78 -0
- package/src/expoKotlinContract.ts +4 -7
- package/src/expoSwiftContract.ts +17 -6
- package/src/home-paths.ts +48 -2
- package/src/host/bridge-host.ts +5 -2
- package/src/host/staged-host-files.ts +9 -0
- package/src/native-app-identity-project.ts +27 -6
- package/src/native-app-identity.ts +91 -4
- package/src/native-firebase-config.ts +209 -13
- package/src/nativeAppIconContract.ts +1 -4
- package/src/output-lock-stdio.ts +36 -0
- package/src/process-group.ts +25 -2
- package/src/react-native-bridge-modules.ts +431 -0
- package/src/react-native-host-modules.ts +8 -1
- package/src/settings-bundle.ts +1 -4
- package/src/swift-binary-modules.ts +2 -2
- package/src/swift-compile.ts +918 -109
- package/src/swift-compiler-digest.ts +14 -0
- package/src/swift-distribution.lock.json +24 -12
- package/src/swift-distribution.ts +103 -4
- package/src/swift-host.ts +180 -55
- package/src/swift-icu.ts +27 -13
- package/src/swift-objc-compile.ts +355 -0
- package/src/swift-objc-sdk.ts +821 -27
- package/src/swift-objc-standin.ts +148 -14
- package/src/swift-package-manifest.ts +60 -7
- package/src/swift-package.ts +15 -1
- package/src/swift-packages.ts +154 -81
- package/src/swift-standin-emit.ts +96 -15
- package/src/swift-standins.ts +530 -116
- package/src/swift-submit.ts +13 -5
- package/src/swift-toolchain.ts +500 -42
- package/src/vite-plugin.ts +12 -1
- package/src/worker-system-chunks.ts +11 -12
- package/src/worklet-executable.ts +19 -1
- package/src/xcode-project.ts +270 -30
package/cli/commands/upload.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// peach record upload —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// peach record upload — publish a recording from a running simulator. scene
|
|
2
|
+
// recordings carry their own content identity; --live-bundle adds the app
|
|
3
|
+
// bundle for interactive playback under the existing bundle identity.
|
|
4
4
|
//
|
|
5
5
|
// uploads with no repository association are private by default: only
|
|
6
6
|
// signed-in members of the uploader's account can open the link until the
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// associated with the user's account.
|
|
10
10
|
|
|
11
11
|
import { createHash, randomUUID } from 'crypto'
|
|
12
|
-
import { readFileSync } from 'fs'
|
|
12
|
+
import { readFileSync, rmSync } from 'fs'
|
|
13
|
+
import { tmpdir } from 'os'
|
|
13
14
|
import path from 'path'
|
|
14
15
|
import { gzipSync, gunzipSync } from 'zlib'
|
|
15
16
|
import { readProfileTrace } from '@peach/globals/profile-trace'
|
|
@@ -36,11 +37,13 @@ import {
|
|
|
36
37
|
mergeProfileTimeline,
|
|
37
38
|
profileFrameSummary,
|
|
38
39
|
} from 'peach-engine/profiler/profile-trace'
|
|
40
|
+
import { readSceneRecordingUpload } from 'peach-engine/remote-stream/scene-recording'
|
|
39
41
|
import {
|
|
40
42
|
resolvedExpoAppIconUrl,
|
|
41
43
|
expoConfigFromManifest,
|
|
42
44
|
} from '../../../peach-engine/src/engine/expo-manifest'
|
|
43
45
|
import { loopbackOriginOf } from '../../src/backend-origin'
|
|
46
|
+
import { parseExpoManifestBody } from '../../src/expo-manifest-body'
|
|
44
47
|
import { rnxPublicBrand } from '../../src/public-brand'
|
|
45
48
|
import { fetchAppFontFiles } from '../app-fonts'
|
|
46
49
|
import { fetchAppSplashImages } from '../app-splash'
|
|
@@ -57,6 +60,7 @@ import {
|
|
|
57
60
|
parseBridgeCliArgs,
|
|
58
61
|
} from '../ws-bridge'
|
|
59
62
|
import type { SootSimTimelineEvent } from '@peach/globals'
|
|
63
|
+
import type { BundleSnapshot } from 'peach-engine/preview/capture-bundle'
|
|
60
64
|
|
|
61
65
|
interface UploadOptions {
|
|
62
66
|
port?: number
|
|
@@ -83,19 +87,6 @@ function parseServerMessage(responseText?: string): string | null {
|
|
|
83
87
|
}
|
|
84
88
|
}
|
|
85
89
|
|
|
86
|
-
type BundleSnapshot = {
|
|
87
|
-
bundleUrl: string | null
|
|
88
|
-
entry: string | null
|
|
89
|
-
deviceSpec: InitRequestBody['deviceSpec']
|
|
90
|
-
runtimeVersion: string | null
|
|
91
|
-
pageHref: string
|
|
92
|
-
transformedBundle: {
|
|
93
|
-
url: string
|
|
94
|
-
byteLength: number
|
|
95
|
-
timestamp: number
|
|
96
|
-
} | null
|
|
97
|
-
}
|
|
98
|
-
|
|
99
90
|
type AttachedInteractionSnapshot = {
|
|
100
91
|
meta: NonNullable<InitRequestBody['snapshots']>[number] & { id: string }
|
|
101
92
|
bytes: Uint8Array
|
|
@@ -181,10 +172,10 @@ export function resolvePublicPreviewOrigin(
|
|
|
181
172
|
|
|
182
173
|
function printHelp() {
|
|
183
174
|
console.log(`
|
|
184
|
-
peach record upload — publish
|
|
175
|
+
peach record upload — publish a recording as a /preview/<id> link
|
|
185
176
|
|
|
186
177
|
usage:
|
|
187
|
-
peach record upload [--origin <url>] [--public-origin <url>] (--events <path> | --video <path>) [--screenshot <path>] [--owner <org> --repo <repo>] [--sim <sim>] [--open] [--assets-only]
|
|
178
|
+
peach record upload [--origin <url>] [--public-origin <url>] (--events <path> | --video <path> | --scene <path>) [--live-bundle] [--screenshot <path>] [--owner <org> --repo <repo>] [--sim <sim>] [--open] [--assets-only]
|
|
188
179
|
|
|
189
180
|
options:
|
|
190
181
|
--origin <url> upload target (default: ${REMOTE_DEFAULT_ORIGIN})
|
|
@@ -194,8 +185,8 @@ options:
|
|
|
194
185
|
for prod uploads, upload origin for localhost/custom origins).
|
|
195
186
|
override with PEACH_PREVIEW_ORIGIN env var
|
|
196
187
|
--events <path> path to a gzipped events .jsonl.gz file to attach
|
|
197
|
-
required unless --video is present;
|
|
198
|
-
have replay or recording playback data
|
|
188
|
+
required unless --video or --scene is present;
|
|
189
|
+
previews must have replay or recording playback data
|
|
199
190
|
--screenshot <path>
|
|
200
191
|
path to a PNG thumbnail to attach to the share
|
|
201
192
|
--snapshot-manifest <path>
|
|
@@ -205,6 +196,11 @@ options:
|
|
|
205
196
|
/api/preview/flow-video?id=<share-id>
|
|
206
197
|
--video-duration-ms <ms>
|
|
207
198
|
duration hint for attached flow video
|
|
199
|
+
--scene <path> a scene recording (.psr.gz) of a remote viewer's display
|
|
200
|
+
stream. the preview paints it through the simulator's
|
|
201
|
+
renderer instead of playing a video; uploads without a
|
|
202
|
+
live bundle by default
|
|
203
|
+
--live-bundle include the running app bundle for Live alongside --scene
|
|
208
204
|
--timeline-events <path>
|
|
209
205
|
extra trace timeline rows, JSONL or JSONL gz
|
|
210
206
|
--trace-profile <path>
|
|
@@ -436,6 +432,13 @@ export async function runUpload(
|
|
|
436
432
|
const screenshotPath = extractStringFlag(mutableArgs, '--screenshot')
|
|
437
433
|
const snapshotManifestPath = extractStringFlag(mutableArgs, '--snapshot-manifest')
|
|
438
434
|
const videoPath = extractStringFlag(mutableArgs, '--video')
|
|
435
|
+
const scenePath = extractStringFlag(mutableArgs, '--scene')
|
|
436
|
+
const includeLiveBundle = extractBoolFlag(mutableArgs, '--live-bundle')
|
|
437
|
+
if (includeLiveBundle && !scenePath) {
|
|
438
|
+
console.error(' --live-bundle requires --scene')
|
|
439
|
+
peachExit(1)
|
|
440
|
+
}
|
|
441
|
+
const hasBundle = includeLiveBundle || !scenePath
|
|
439
442
|
const timelineEventsPath = extractStringFlag(mutableArgs, '--timeline-events')
|
|
440
443
|
const traceProfilePath = extractStringFlag(mutableArgs, '--trace-profile')
|
|
441
444
|
const failureBundleDir = extractStringFlag(mutableArgs, '--failure-bundle')
|
|
@@ -468,9 +471,9 @@ export async function runUpload(
|
|
|
468
471
|
console.error(' --billing-kind test_run is only for hosted test jobs')
|
|
469
472
|
peachExit(1)
|
|
470
473
|
}
|
|
471
|
-
if (!eventsPath && !videoPath) {
|
|
474
|
+
if (!eventsPath && !videoPath && !scenePath) {
|
|
472
475
|
console.error(
|
|
473
|
-
' preview uploads require playback data: pass --events or --
|
|
476
|
+
' preview uploads require playback data: pass --events, --video or --scene.\n' +
|
|
474
477
|
' for a normal local preview, use `peach record --mode combined --open`\n' +
|
|
475
478
|
' or `peach maestro test --preview` instead of uploading a bundle-only snapshot.',
|
|
476
479
|
)
|
|
@@ -493,6 +496,7 @@ export async function runUpload(
|
|
|
493
496
|
})
|
|
494
497
|
|
|
495
498
|
const bridge = createBridgeFromParsed(parsed)
|
|
499
|
+
let shellUrl: string | null = null
|
|
496
500
|
let snapshot: BundleSnapshot
|
|
497
501
|
// bundle-origin manifest (no bodies) — CLI re-fetches these over http so
|
|
498
502
|
// they can be large without blowing the ws frame cap. sibling bundle
|
|
@@ -513,6 +517,7 @@ export async function runUpload(
|
|
|
513
517
|
const localTraceTimelineCount = traceTimelineEvents.length
|
|
514
518
|
let transformedBundleText: string | null = null
|
|
515
519
|
try {
|
|
520
|
+
if (scenePath) shellUrl = await evalInBridge<string>(bridge, 'location.href')
|
|
516
521
|
snapshot = await evalInBridge<BundleSnapshot>(
|
|
517
522
|
bridge,
|
|
518
523
|
'(typeof window.__sootsimCaptureBundle === "function") ? window.__sootsimCaptureBundle() : null',
|
|
@@ -660,7 +665,7 @@ export async function runUpload(
|
|
|
660
665
|
}
|
|
661
666
|
}
|
|
662
667
|
}
|
|
663
|
-
if (snapshot?.transformedBundle) {
|
|
668
|
+
if (hasBundle && snapshot?.transformedBundle) {
|
|
664
669
|
transformedBundleText = await pullTransformedBundle(
|
|
665
670
|
bridge,
|
|
666
671
|
snapshot.transformedBundle.byteLength,
|
|
@@ -703,7 +708,7 @@ export async function runUpload(
|
|
|
703
708
|
)
|
|
704
709
|
peachExit(2)
|
|
705
710
|
}
|
|
706
|
-
if (!snapshot.bundleUrl) {
|
|
711
|
+
if (hasBundle && !snapshot.bundleUrl) {
|
|
707
712
|
console.error(
|
|
708
713
|
' no ?bundle= URL on the current peach tab.\n' +
|
|
709
714
|
' open the app you want to share first (e.g. peach open 8082), then run upload.',
|
|
@@ -716,15 +721,15 @@ export async function runUpload(
|
|
|
716
721
|
// does not have to re-run sootsim's transforms. fall back to re-fetching
|
|
717
722
|
// the raw metro URL + forcing lazy=false when the transform hasn't
|
|
718
723
|
// published yet (e.g., tab was opened but bundle not finished loading).
|
|
719
|
-
let bundleBytes: Uint8Array
|
|
724
|
+
let bundleBytes: Uint8Array | undefined
|
|
720
725
|
let isTransformed = false
|
|
721
|
-
if (transformedBundleText !== null) {
|
|
726
|
+
if (hasBundle && transformedBundleText !== null) {
|
|
722
727
|
bundleBytes = new TextEncoder().encode(transformedBundleText)
|
|
723
728
|
isTransformed = true
|
|
724
729
|
console.log(
|
|
725
730
|
` using post-transform bundle: ${(bundleBytes.byteLength / 1024).toFixed(1)} KiB`,
|
|
726
731
|
)
|
|
727
|
-
} else {
|
|
732
|
+
} else if (hasBundle && snapshot.bundleUrl) {
|
|
728
733
|
const fetchBundleUrl = forceInlineBundle(snapshot.bundleUrl)
|
|
729
734
|
if (fetchBundleUrl !== snapshot.bundleUrl) {
|
|
730
735
|
console.log(` forcing lazy=false for self-contained bundle`)
|
|
@@ -872,6 +877,17 @@ export async function runUpload(
|
|
|
872
877
|
)
|
|
873
878
|
}
|
|
874
879
|
|
|
880
|
+
let sceneBytes: Uint8Array | undefined
|
|
881
|
+
let sceneStream: InitRequestBody['sceneStream']
|
|
882
|
+
if (scenePath) {
|
|
883
|
+
sceneBytes = readFileSync(scenePath)
|
|
884
|
+
const { header, slot } = await readSceneRecordingUpload(sceneBytes)
|
|
885
|
+
sceneStream = slot
|
|
886
|
+
console.log(
|
|
887
|
+
` attaching scene stream: ${(sceneBytes.byteLength / 1024).toFixed(1)} KiB, ${header.frameCount} frames over ${Math.round(header.durationMs)}ms`,
|
|
888
|
+
)
|
|
889
|
+
}
|
|
890
|
+
|
|
875
891
|
// priority: PEACH_API_KEY env → github runner token → saved credential
|
|
876
892
|
// file → session token. github runner auth is what keeps installed PR
|
|
877
893
|
// previews from requiring a new Contrast secret.
|
|
@@ -883,11 +899,26 @@ export async function runUpload(
|
|
|
883
899
|
console.error(' set PEACH_API_KEY=sk_rnx_... or run `peach login`.')
|
|
884
900
|
peachExit(1)
|
|
885
901
|
}
|
|
902
|
+
if (sceneBytes && shellUrl && cliAuth.kind === 'github') {
|
|
903
|
+
const { renderSceneToVideo } = await import('../scene-video')
|
|
904
|
+
const output = path.join(tmpdir(), `peach-scene-video-${Date.now()}.mp4`)
|
|
905
|
+
try {
|
|
906
|
+
const rendered = await renderSceneToVideo({ scene: sceneBytes, shellUrl, output })
|
|
907
|
+
videoBytes = readFileSync(output)
|
|
908
|
+
videoContentType = 'video/mp4'
|
|
909
|
+
videoDurationMs = rendered.durationMs
|
|
910
|
+
console.log(
|
|
911
|
+
` rendered scene video: ${rendered.frameCount} frames at 60 fps, ${rendered.sizeBytes} bytes in ${(rendered.renderTimeMs / 1000).toFixed(1)}s`,
|
|
912
|
+
)
|
|
913
|
+
} finally {
|
|
914
|
+
rmSync(output, { force: true })
|
|
915
|
+
}
|
|
916
|
+
}
|
|
886
917
|
const resolvedAuthHeader = authHeaderValue(cliAuth)
|
|
887
918
|
|
|
888
919
|
const bundleOrigin = (() => {
|
|
889
920
|
try {
|
|
890
|
-
return new URL(snapshot.bundleUrl).origin
|
|
921
|
+
return snapshot.bundleUrl ? new URL(snapshot.bundleUrl).origin : null
|
|
891
922
|
} catch {
|
|
892
923
|
return null
|
|
893
924
|
}
|
|
@@ -1002,16 +1033,16 @@ export async function runUpload(
|
|
|
1002
1033
|
// explicitly so the installed app keeps its identity in share replay.
|
|
1003
1034
|
const appIdentityFiles: PreparedFile[] = []
|
|
1004
1035
|
let appManifest: string | undefined
|
|
1005
|
-
const metroOrigin = loopbackOriginOf(snapshot.bundleUrl)
|
|
1006
|
-
if (metroOrigin) {
|
|
1036
|
+
const metroOrigin = snapshot.bundleUrl ? loopbackOriginOf(snapshot.bundleUrl) : null
|
|
1037
|
+
if (metroOrigin && snapshot.bundleUrl) {
|
|
1007
1038
|
const platform = new URL(snapshot.bundleUrl).searchParams.get('platform') || 'ios'
|
|
1008
1039
|
const manifestUrl = `${metroOrigin}/`
|
|
1009
1040
|
const response = await fetch(manifestUrl, {
|
|
1010
1041
|
headers: { 'expo-platform': platform, accept: 'application/json' },
|
|
1011
1042
|
signal: AbortSignal.timeout(30_000),
|
|
1012
1043
|
})
|
|
1013
|
-
if (response.ok && response.headers.get('content-type')?.includes('
|
|
1014
|
-
const manifest: unknown = await response.
|
|
1044
|
+
if (response.ok && !response.headers.get('content-type')?.includes('html')) {
|
|
1045
|
+
const manifest: unknown = parseExpoManifestBody(await response.text())
|
|
1015
1046
|
const config = expoConfigFromManifest(manifest)
|
|
1016
1047
|
if (config) {
|
|
1017
1048
|
appManifest = manifestUrl
|
|
@@ -1085,19 +1116,31 @@ export async function runUpload(
|
|
|
1085
1116
|
// brotli source bytes for the upload. finalize verifies the source hash,
|
|
1086
1117
|
// minifies in the node build container, and stores one canonical brotli
|
|
1087
1118
|
// bundle. assets stay gzip below.
|
|
1088
|
-
const bundleBr = brotliBundle(bundleBytes)
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
)
|
|
1119
|
+
const bundleBr = bundleBytes ? brotliBundle(bundleBytes) : undefined
|
|
1120
|
+
const identityBytes = bundleBytes ?? sceneBytes
|
|
1121
|
+
if (!identityBytes) throw new Error('upload requires a bundle or scene recording')
|
|
1122
|
+
const contentHash = createHash('sha256').update(identityBytes).digest('hex')
|
|
1123
|
+
if (bundleBytes && bundleBr) {
|
|
1124
|
+
console.log(
|
|
1125
|
+
` bundle raw=${(bundleBytes.byteLength / 1024).toFixed(1)} KiB br=${(bundleBr.byteLength / 1024).toFixed(1)} KiB`,
|
|
1126
|
+
)
|
|
1127
|
+
}
|
|
1093
1128
|
|
|
1094
1129
|
const initBody: InitRequestBody = {
|
|
1095
|
-
contentHash
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1130
|
+
contentHash,
|
|
1131
|
+
hasBundle,
|
|
1132
|
+
...(bundleBytes && snapshot.bundleUrl
|
|
1133
|
+
? {
|
|
1134
|
+
bundleSizeBytes: bundleBytes.byteLength,
|
|
1135
|
+
bundleNativeAppIdentityHeader: snapshot.bundleNativeAppIdentityHeader,
|
|
1136
|
+
bundleContentType: guessBundleContentType(snapshot.bundleUrl),
|
|
1137
|
+
bundleEncoding: 'br',
|
|
1138
|
+
bundleMinified: false,
|
|
1139
|
+
bundleOrigin,
|
|
1140
|
+
entry: snapshot.entry,
|
|
1141
|
+
isTransformed,
|
|
1142
|
+
}
|
|
1143
|
+
: {}),
|
|
1101
1144
|
runtimeVersion: snapshot.runtimeVersion ?? undefined,
|
|
1102
1145
|
// config-only fonts resolved by `peach open` and carried on the engine
|
|
1103
1146
|
// URL. persist them onto the share meta so /preview/<id> re-applies
|
|
@@ -1108,8 +1151,6 @@ export async function runUpload(
|
|
|
1108
1151
|
// /preview/<id> re-applies `?appSplash=` and paints the same splash; the
|
|
1109
1152
|
// image bytes are staged above so the engine's <img> resolves in preview.
|
|
1110
1153
|
appSplash: appSplashWire || undefined,
|
|
1111
|
-
entry: snapshot.entry,
|
|
1112
|
-
isTransformed,
|
|
1113
1154
|
deviceSpec: snapshot.deviceSpec,
|
|
1114
1155
|
installationId: gitHubIdentity?.installationId ?? undefined,
|
|
1115
1156
|
repoId: gitHubIdentity?.repoId,
|
|
@@ -1179,6 +1220,7 @@ export async function runUpload(
|
|
|
1179
1220
|
durationMs: videoDurationMs,
|
|
1180
1221
|
}
|
|
1181
1222
|
: undefined,
|
|
1223
|
+
sceneStream,
|
|
1182
1224
|
}
|
|
1183
1225
|
|
|
1184
1226
|
const filesByHash = new Map<string, Uint8Array>()
|
|
@@ -1191,7 +1233,7 @@ export async function runUpload(
|
|
|
1191
1233
|
}
|
|
1192
1234
|
|
|
1193
1235
|
const putT0 = Date.now()
|
|
1194
|
-
let putBytes = bundleBr
|
|
1236
|
+
let putBytes = bundleBr?.byteLength ?? 0
|
|
1195
1237
|
for (const f of prepared) putBytes += f.bytes.byteLength
|
|
1196
1238
|
if (eventsGz) putBytes += eventsGz.byteLength
|
|
1197
1239
|
putBytes += timelineGz.byteLength
|
|
@@ -1201,6 +1243,7 @@ export async function runUpload(
|
|
|
1201
1243
|
for (const snapshot of interactionSnapshots) putBytes += snapshot.bytes.byteLength
|
|
1202
1244
|
if (failureBundleGz) putBytes += failureBundleGz.byteLength
|
|
1203
1245
|
if (videoBytes) putBytes += videoBytes.byteLength
|
|
1246
|
+
if (sceneBytes) putBytes += sceneBytes.byteLength
|
|
1204
1247
|
|
|
1205
1248
|
const initUrl = `${normalizeOrigin(origin)}/api/preview/upload/init`
|
|
1206
1249
|
console.log(` init: ${initUrl}`)
|
|
@@ -1217,6 +1260,7 @@ export async function runUpload(
|
|
|
1217
1260
|
timelineBytes: timelineGz,
|
|
1218
1261
|
storageBytes: storageGz,
|
|
1219
1262
|
videoBytes,
|
|
1263
|
+
sceneStreamBytes: sceneBytes,
|
|
1220
1264
|
failureBundleBytes: failureBundleGz,
|
|
1221
1265
|
snapshotBytes,
|
|
1222
1266
|
},
|
|
@@ -1225,13 +1269,14 @@ export async function runUpload(
|
|
|
1225
1269
|
})
|
|
1226
1270
|
finalize = result.finalize
|
|
1227
1271
|
const totalObjects =
|
|
1228
|
-
1 +
|
|
1272
|
+
(result.init.bundle ? 1 : 0) +
|
|
1229
1273
|
result.init.files.length +
|
|
1230
1274
|
(result.init.events ? 1 : 0) +
|
|
1231
1275
|
(result.init.timeline ? 1 : 0) +
|
|
1232
1276
|
(result.init.storage ? 1 : 0) +
|
|
1233
1277
|
result.init.snapshots.length +
|
|
1234
1278
|
(result.init.flowVideo ? 1 : 0) +
|
|
1279
|
+
(result.init.sceneStream ? 1 : 0) +
|
|
1235
1280
|
(result.init.failureBundle ? 1 : 0)
|
|
1236
1281
|
console.log(
|
|
1237
1282
|
` PUT ${totalObjects} objects in ${Date.now() - putT0}ms (${(putBytes / 1024).toFixed(1)} KiB)`,
|
package/cli/desktop-companion.ts
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import { execFileSync, spawn } from 'child_process'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
symlinkSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'fs'
|
|
11
|
+
import { createHash } from 'node:crypto'
|
|
12
|
+
import { tmpdir } from 'os'
|
|
3
13
|
import { dirname, join, resolve } from 'path'
|
|
4
14
|
import { fileURLToPath } from 'url'
|
|
5
|
-
import {
|
|
6
|
-
|
|
15
|
+
import {
|
|
16
|
+
activeDesktopHostApp,
|
|
17
|
+
desktopHostPlatform,
|
|
18
|
+
isNativeHostBundle,
|
|
19
|
+
} from '../src/desktop-host-delivery'
|
|
20
|
+
import {
|
|
21
|
+
isDaemonLockfileFresh,
|
|
22
|
+
isDevBridgeLockfileFresh,
|
|
23
|
+
readDaemonLockfile,
|
|
24
|
+
readDevBridgeLockfiles,
|
|
25
|
+
rnxHomeDir,
|
|
26
|
+
} from '../src/home-paths'
|
|
7
27
|
|
|
8
28
|
// resolved lazily: peach-engine is a workspace package in dev, but absent
|
|
9
29
|
// from the published npm tarball. don't crash module load just because the
|
|
@@ -95,22 +115,31 @@ export function findInstalledDesktopCompanion(): DesktopCompanionInstall | null
|
|
|
95
115
|
: findNativeDesktopCompanion()
|
|
96
116
|
}
|
|
97
117
|
|
|
118
|
+
// leftover Electron bundles. /Applications/Peach.app is the native host on a
|
|
119
|
+
// case-insensitive volume, so bundle id decides, not the path's letter case.
|
|
120
|
+
function retiredElectronApps(): string[] {
|
|
121
|
+
if (process.platform === 'darwin') {
|
|
122
|
+
return [
|
|
123
|
+
'/Applications/peach.app',
|
|
124
|
+
resolve(process.env.HOME || '', 'Applications/peach.app'),
|
|
125
|
+
].filter((candidate) => existsSync(candidate) && !isNativeHostBundle(candidate))
|
|
126
|
+
}
|
|
127
|
+
if (process.platform !== 'linux') return []
|
|
128
|
+
return [
|
|
129
|
+
resolve(process.env.HOME || '', 'Applications/peach.AppImage'),
|
|
130
|
+
resolve(process.env.HOME || '', '.local/bin/peach.AppImage'),
|
|
131
|
+
'/opt/peach/peach.AppImage',
|
|
132
|
+
'/opt/peach/peach',
|
|
133
|
+
].filter((candidate) => existsSync(candidate))
|
|
134
|
+
}
|
|
135
|
+
|
|
98
136
|
// existing Electron apps stay on disk; CLI users receive the migration once.
|
|
137
|
+
// the notice names those apps and does not reject a native host beside them.
|
|
99
138
|
export function notifyDesktopMigration(): void {
|
|
100
139
|
if (process.platform !== 'darwin' && process.platform !== 'linux') return
|
|
101
|
-
const legacy =
|
|
102
|
-
process.platform === 'darwin'
|
|
103
|
-
? [
|
|
104
|
-
'/Applications/peach.app',
|
|
105
|
-
resolve(process.env.HOME || '', 'Applications/peach.app'),
|
|
106
|
-
].some((candidate) => existsSync(candidate) && !isNativeHostBundle(candidate))
|
|
107
|
-
: ['Applications/peach.AppImage', '.local/bin/peach.AppImage'].some((candidate) =>
|
|
108
|
-
existsSync(resolve(process.env.HOME || '', candidate)),
|
|
109
|
-
) ||
|
|
110
|
-
existsSync('/opt/peach/peach.AppImage') ||
|
|
111
|
-
existsSync('/opt/peach/peach')
|
|
140
|
+
const legacy = retiredElectronApps()
|
|
112
141
|
const marker = join(rnxHomeDir(), 'electron-native-migration')
|
|
113
|
-
if (
|
|
142
|
+
if (legacy.length === 0 || existsSync(marker)) return
|
|
114
143
|
mkdirSync(rnxHomeDir(), { recursive: true })
|
|
115
144
|
try {
|
|
116
145
|
writeFileSync(marker, '', { flag: 'wx' })
|
|
@@ -119,7 +148,7 @@ export function notifyDesktopMigration(): void {
|
|
|
119
148
|
throw error
|
|
120
149
|
}
|
|
121
150
|
console.error(
|
|
122
|
-
|
|
151
|
+
` Electron has retired on Mac and Linux (${legacy.join(', ')}). Install the native app: peach desktop install`,
|
|
123
152
|
)
|
|
124
153
|
}
|
|
125
154
|
|
|
@@ -154,6 +183,21 @@ export function wantsNativeDesktop(opts: DesktopCompanionOptions = {}): boolean
|
|
|
154
183
|
)
|
|
155
184
|
}
|
|
156
185
|
|
|
186
|
+
// the native app is the simulator window wherever it ships: always on Apple
|
|
187
|
+
// silicon, and on Linux x64 in a desktop session once installed or when a
|
|
188
|
+
// person is present to approve its package install's sudo prompt.
|
|
189
|
+
export function nativeDesktopIsDefault(): boolean {
|
|
190
|
+
const platform = desktopHostPlatform()
|
|
191
|
+
if (platform === 'mac-arm64') return true
|
|
192
|
+
if (platform !== 'linux-x64') return false
|
|
193
|
+
if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false
|
|
194
|
+
return (
|
|
195
|
+
findNativeDesktopCompanion() !== null ||
|
|
196
|
+
process.stdin.isTTY === true ||
|
|
197
|
+
process.getuid?.() === 0
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
157
201
|
export function getNativeDesktopAppPath(): string | null {
|
|
158
202
|
const explicitApp = process.env.PEACH_DESKTOP_APP
|
|
159
203
|
if (!explicitApp)
|
|
@@ -284,6 +328,47 @@ async function openInConnectedMacApp(
|
|
|
284
328
|
}
|
|
285
329
|
}
|
|
286
330
|
|
|
331
|
+
// PeachDesktop shipped before PEACH_BRIDGE_PORT seeds the page from
|
|
332
|
+
// PEACH_HOME/daemon.json and ignores the launch URL's wsPort. when this
|
|
333
|
+
// command is waiting on a different bridge, give the child a home whose
|
|
334
|
+
// lock names that bridge. the caller's daemon.json is left untouched.
|
|
335
|
+
// a newer host that prefers the env, the URL pin, or a fresh daemon still
|
|
336
|
+
// sees this same port (the isolated-home rule in desktop host bridge selection).
|
|
337
|
+
function macDesktopLaunchHome(bridgePort: number): string {
|
|
338
|
+
const home = rnxHomeDir()
|
|
339
|
+
const daemon = readDaemonLockfile()
|
|
340
|
+
if (isDaemonLockfileFresh(daemon) && daemon.bridgePort === bridgePort) return home
|
|
341
|
+
const dev = readDevBridgeLockfiles().find(
|
|
342
|
+
(lock) => lock.bridgePort === bridgePort && isDevBridgeLockfileFresh(lock),
|
|
343
|
+
)
|
|
344
|
+
const homeKey = createHash('sha256').update(home).digest('hex')
|
|
345
|
+
const dir = join(tmpdir(), 'peach-desktop-launch', homeKey, String(bridgePort))
|
|
346
|
+
mkdirSync(dir, { recursive: true })
|
|
347
|
+
if (existsSync(home)) {
|
|
348
|
+
for (const name of readdirSync(home)) {
|
|
349
|
+
if (name === 'daemon.json') continue
|
|
350
|
+
const dest = join(dir, name)
|
|
351
|
+
try {
|
|
352
|
+
lstatSync(dest)
|
|
353
|
+
} catch {
|
|
354
|
+
symlinkSync(join(home, name), dest)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const lockPath = join(dir, 'daemon.json')
|
|
359
|
+
try {
|
|
360
|
+
if (lstatSync(lockPath).isSymbolicLink()) unlinkSync(lockPath)
|
|
361
|
+
} catch {
|
|
362
|
+
// no lock yet
|
|
363
|
+
}
|
|
364
|
+
const pid = dev && dev.pid > 1 ? dev.pid : process.pid
|
|
365
|
+
writeFileSync(
|
|
366
|
+
lockPath,
|
|
367
|
+
JSON.stringify({ schema: 1, pid, bridgePort, heartbeatAt: Date.now() }),
|
|
368
|
+
)
|
|
369
|
+
return dir
|
|
370
|
+
}
|
|
371
|
+
|
|
287
372
|
export async function launchDesktopCompanion(
|
|
288
373
|
url?: string,
|
|
289
374
|
install = findDesktopCompanion(),
|
|
@@ -339,7 +424,7 @@ export async function launchDesktopCompanion(
|
|
|
339
424
|
'-a',
|
|
340
425
|
install.path,
|
|
341
426
|
'--env',
|
|
342
|
-
`PEACH_HOME=${rnxHomeDir()}`,
|
|
427
|
+
`PEACH_HOME=${opts.bridgePort && launchUrl ? macDesktopLaunchHome(opts.bridgePort) : rnxHomeDir()}`,
|
|
343
428
|
]
|
|
344
429
|
if (opts.bridgePort) {
|
|
345
430
|
openArgs.push('--env', `PEACH_BRIDGE_PORT=${opts.bridgePort}`)
|
package/cli/drivers/desktop.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// desktop driver selection is shared with companion discovery.
|
|
2
2
|
|
|
3
|
+
import { desktopHostPlatform, headlessHostCommand } from '../../src/desktop-host-delivery'
|
|
3
4
|
import {
|
|
4
5
|
findDesktopCompanion,
|
|
5
6
|
launchDesktopCompanion,
|
|
@@ -18,7 +19,19 @@ export function createDesktopDriver(id: 'desktop' | 'electron'): Driver {
|
|
|
18
19
|
const missing = `peach ${id} app not installed (run \`peach desktop install${native ? '' : ' --electron'}\`)`
|
|
19
20
|
function probe(): DriverAvailability {
|
|
20
21
|
const install = findDesktopCompanion({ native })
|
|
21
|
-
if (!install)
|
|
22
|
+
if (!install) {
|
|
23
|
+
const host =
|
|
24
|
+
id === 'desktop' && native && desktopHostPlatform() === 'linux-x64'
|
|
25
|
+
? headlessHostCommand()
|
|
26
|
+
: null
|
|
27
|
+
return host
|
|
28
|
+
? {
|
|
29
|
+
available: true,
|
|
30
|
+
reason: null,
|
|
31
|
+
detail: `headless host @ ${host[0]} (use --driver playwright --headless)`,
|
|
32
|
+
}
|
|
33
|
+
: { available: false, reason: missing }
|
|
34
|
+
}
|
|
22
35
|
return {
|
|
23
36
|
available: true,
|
|
24
37
|
reason: null,
|