peachsim 0.1.1635 → 0.1.1641
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 +10 -1
- package/cli/app-config.ts +14 -5
- package/cli/app-stubs.ts +96 -22
- package/cli/bridge-flow-runner.ts +2 -2
- package/cli/cloud-client.ts +60 -40
- package/cli/cloud-session.ts +16 -0
- package/cli/commands/assert.ts +17 -4
- package/cli/commands/box.ts +33 -19
- package/cli/commands/doctor.ts +37 -20
- package/cli/commands/flow.ts +1 -1
- package/cli/commands/inspect/actions.ts +3 -2
- package/cli/commands/inspect/core.ts +0 -8
- package/cli/commands/inspect/shared.ts +9 -4
- package/cli/commands/inspect.ts +65 -61
- package/cli/commands/platform.ts +2 -1
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +154 -68
- package/cli/commands/swift.ts +18 -4
- package/cli/commands/three-mode.ts +4 -38
- package/cli/commands/upload.ts +55 -49
- package/cli/drivers/playwright-sim-host.ts +65 -42
- package/cli/flow-live-status.ts +0 -2
- package/cli/main.ts +2 -2
- package/cli/outbound-endpoints.ts +7 -5
- package/cli/pause-aware-timeout.ts +4 -0
- package/cli/ws-bridge.ts +19 -16
- package/detox/attempt-events.cjs +99 -6
- package/detox/attempt-events.d.cts +7 -0
- package/detox/expectations.ts +7 -3
- package/detox/index.ts +259 -139
- package/detox/jest-environment.cjs +5 -1
- package/detox/motion-change.cjs +170 -21
- package/detox/motion-change.d.cts +32 -0
- 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 +17 -7
- 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/bundle-channel.cjs +5014 -2268
- 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 +1 -1
- package/dist-lib/config.mjs +1 -1
- package/dist-lib/detox/index.cjs +1036 -166
- package/dist-lib/detox/jest-environment.cjs +5 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +4613 -1963
- package/dist-lib/expo-swift-metadata.cjs +9 -7
- package/dist-lib/expo-swift-metadata.mjs +9 -7
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +58072 -16400
- 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 +2790 -143
- 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 +2579 -95
- package/dist-lib/native-app-identity-project.cjs +243 -53
- package/dist-lib/native-app-identity-project.mjs +243 -53
- package/dist-lib/native-app-identity.cjs +29 -30
- package/dist-lib/native-app-identity.mjs +29 -30
- 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 +2 -2
- package/dist-lib/react-native-host-modules.mjs +2 -2
- package/dist-lib/scripts/dev-server-scanner.cjs +8 -6
- package/dist-lib/sdk.cjs +4 -3
- package/dist-lib/sdk.mjs +4 -3
- package/dist-lib/skills.cjs +66475 -23097
- package/dist-lib/swift-compile.cjs +452 -260
- package/dist-lib/swift-compile.mjs +451 -252
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +14 -7
- package/dist-lib/swift-submit.mjs +14 -7
- package/dist-lib/swift-toolchain.cjs +316 -41
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +8188 -5636
- package/dist-lib/vite.cjs +2857 -12
- package/dist-lib/worklet-executable.cjs +14 -2
- package/dist-lib/worklet-executable.mjs +14 -2
- package/package.json +1 -1
- package/src/backend-origin.ts +21 -6
- package/src/expo-swift-host.ts +5 -2
- package/src/expoKotlinContract.ts +4 -7
- package/src/expoSwiftContract.ts +4 -6
- package/src/host/staged-host-files.ts +9 -0
- package/src/native-app-identity-project.ts +1 -4
- package/src/native-app-identity.ts +1 -4
- package/src/nativeAppIconContract.ts +1 -4
- package/src/output-lock-stdio.ts +36 -0
- package/src/react-native-bridge-modules.ts +106 -78
- package/src/react-native-host-modules.ts +4 -1
- package/src/settings-bundle.ts +1 -4
- package/src/swift-compile.ts +224 -139
- package/src/swift-distribution.lock.json +23 -12
- package/src/swift-distribution.ts +103 -4
- package/src/swift-host.ts +95 -30
- package/src/swift-icu.ts +16 -10
- package/src/swift-objc-compile.ts +547 -0
- package/src/swift-objc-metadata.ts +204 -0
- package/src/swift-objc-sdk.ts +883 -19
- package/src/swift-objc-standin.ts +126 -14
- package/src/swift-package-manifest.ts +42 -1
- package/src/swift-packages.ts +35 -21
- package/src/swift-root-lock.ts +153 -0
- package/src/swift-standin-emit.ts +40 -7
- package/src/swift-standins.ts +404 -138
- package/src/swift-submit.ts +18 -7
- package/src/swift-toolchain.ts +378 -46
- package/src/swiftContract.ts +12 -0
- package/src/worker-system-chunks.ts +11 -12
- package/src/worklet-executable.ts +19 -1
- package/src/xcode-project.ts +280 -33
package/cli/commands/record.ts
CHANGED
|
@@ -76,18 +76,41 @@ const STORE_RECORD_START_TIMEOUT_MS = 90_000
|
|
|
76
76
|
// (bitmap-capture.ts), so recovery is simply to retry.
|
|
77
77
|
const CAPTURE_STEP_ENCODE_BUDGET_MS = 30_000
|
|
78
78
|
|
|
79
|
+
// waits out a recording's duration as an in-flight bridge command. the host
|
|
80
|
+
// closes a cli socket that sends nothing for 60s, but never one with a command
|
|
81
|
+
// awaiting its sim, so a client-side sleep through a long capture dropped the
|
|
82
|
+
// connection before the encode/flush step could use it.
|
|
83
|
+
async function holdCapture(bridge: WsBridge, durationMs: number) {
|
|
84
|
+
await bridge.send(
|
|
85
|
+
{ type: 'evaluate', code: `new Promise((r) => setTimeout(r, ${durationMs}))` },
|
|
86
|
+
{ timeoutMs: durationMs + CAPTURE_STEP_ENCODE_BUDGET_MS },
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// an explicit --timeout is the step's budget. a step that records for the
|
|
91
|
+
// whole duration (a gif) gets that duration on top of it.
|
|
92
|
+
function captureStepTimeoutMs(
|
|
93
|
+
parsed: ParsedBridgeCliArgs,
|
|
94
|
+
durationMs: number,
|
|
95
|
+
spansCapture: boolean,
|
|
96
|
+
): number {
|
|
97
|
+
if (!parsed.explicitTimeout)
|
|
98
|
+
return Math.max(20_000, durationMs + CAPTURE_STEP_ENCODE_BUDGET_MS)
|
|
99
|
+
return spansCapture ? durationMs + parsed.commandTimeoutMs : parsed.commandTimeoutMs
|
|
100
|
+
}
|
|
101
|
+
|
|
79
102
|
async function evalCaptureStep<T>(
|
|
80
103
|
bridge: WsBridge,
|
|
81
104
|
code: string,
|
|
82
105
|
step: string,
|
|
83
106
|
durationMs: number,
|
|
107
|
+
timeoutMs: number,
|
|
84
108
|
): Promise<T> {
|
|
85
|
-
const timeoutMs = Math.max(20_000, durationMs + CAPTURE_STEP_ENCODE_BUDGET_MS)
|
|
86
109
|
try {
|
|
87
110
|
return (await bridge.send({ type: 'evaluate', code }, { timeoutMs })) as T
|
|
88
111
|
} catch (err) {
|
|
89
112
|
const msg = err instanceof Error ? err.message : String(err)
|
|
90
|
-
if (/^command timed out after \d+s$/.test(msg)) {
|
|
113
|
+
if (/^command timed out after \d+(?:\.\d+)?s$/.test(msg)) {
|
|
91
114
|
console.error(
|
|
92
115
|
` ${step} stalled — the render host did not finish within ` +
|
|
93
116
|
`${Math.round(timeoutMs / 1000)}s; it is likely contended or wedged.\n` +
|
|
@@ -145,6 +168,7 @@ const RECORD_VALUE_FLAGS = new Set([
|
|
|
145
168
|
'--repo',
|
|
146
169
|
'--scene',
|
|
147
170
|
'--shell-url',
|
|
171
|
+
'--timeout',
|
|
148
172
|
])
|
|
149
173
|
const RECORD_SUBCOMMANDS = new Set([
|
|
150
174
|
'start',
|
|
@@ -242,6 +266,8 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
242
266
|
|
|
243
267
|
const parsed = parseBridgeCliArgs(args, {
|
|
244
268
|
port: opts.port,
|
|
269
|
+
// an explicit --timeout wins over the default
|
|
270
|
+
commandTimeoutMs: 60_000,
|
|
245
271
|
stripBooleanFlags: ['--no-shell', '--shell-only', '--open', '--lockstep'],
|
|
246
272
|
stripValueFlags: [
|
|
247
273
|
'--mode',
|
|
@@ -356,7 +382,7 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
356
382
|
originOverride,
|
|
357
383
|
allowGitHubAuth: true,
|
|
358
384
|
})
|
|
359
|
-
const bridge = createBridgeFromParsed(
|
|
385
|
+
const bridge = createBridgeFromParsed(parsed)
|
|
360
386
|
try {
|
|
361
387
|
await assertStoreBridgesAvailable(bridge)
|
|
362
388
|
await assertGuestBundleLoaded(bridge, mode)
|
|
@@ -376,7 +402,7 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
376
402
|
peachExit(1)
|
|
377
403
|
}
|
|
378
404
|
console.log(` recording ${mode} for ${durationSec}s`)
|
|
379
|
-
await
|
|
405
|
+
await holdCapture(bridge, durationMs)
|
|
380
406
|
await evalStoreStop(bridge)
|
|
381
407
|
const result = await pollStoreUpload(bridge)
|
|
382
408
|
handleStoreUploadResult(result, openAfter, mode)
|
|
@@ -398,7 +424,7 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
398
424
|
)
|
|
399
425
|
}
|
|
400
426
|
|
|
401
|
-
const bridge = createBridgeFromParsed(
|
|
427
|
+
const bridge = createBridgeFromParsed(parsed)
|
|
402
428
|
try {
|
|
403
429
|
await assertRecorderAvailable(bridge)
|
|
404
430
|
await assertTabVisibleForCapture(bridge)
|
|
@@ -434,7 +460,7 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
434
460
|
console.error(` frame capture start failed: ${start.error ?? 'unknown error'}`)
|
|
435
461
|
peachExit(1)
|
|
436
462
|
}
|
|
437
|
-
await
|
|
463
|
+
await holdCapture(bridge, durationMs)
|
|
438
464
|
const deadline = Date.now() + Math.max(5_000, durationMs)
|
|
439
465
|
// frames come back a few at a time: one message holding every base64 png
|
|
440
466
|
// exceeds the bridge socket's message cap and closes it.
|
|
@@ -490,6 +516,7 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
490
516
|
}${layers ? `, layers: ${JSON.stringify(layers)}` : ''} })`,
|
|
491
517
|
'gif encode',
|
|
492
518
|
durationMs,
|
|
519
|
+
captureStepTimeoutMs(parsed, durationMs, true),
|
|
493
520
|
)
|
|
494
521
|
if (!result) {
|
|
495
522
|
console.error(' gif capture returned no frames')
|
|
@@ -523,6 +550,7 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
523
550
|
`window.__sootsimRecorder.start(${JSON.stringify(startOpts)})`,
|
|
524
551
|
`${format} start`,
|
|
525
552
|
durationMs,
|
|
553
|
+
captureStepTimeoutMs(parsed, durationMs, false),
|
|
526
554
|
)
|
|
527
555
|
if (!startResult.ok) {
|
|
528
556
|
console.error(` start failed: ${startResult.error ?? 'unknown error'}`)
|
|
@@ -543,7 +571,7 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
543
571
|
}
|
|
544
572
|
|
|
545
573
|
console.log(` recording ${format} for ${durationSec}s → ${out}`)
|
|
546
|
-
await
|
|
574
|
+
await holdCapture(bridge, durationMs)
|
|
547
575
|
|
|
548
576
|
const stopResult: {
|
|
549
577
|
ok: boolean
|
|
@@ -557,6 +585,7 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
557
585
|
`window.__sootsimRecorder.stop()`,
|
|
558
586
|
`${format} encode/flush`,
|
|
559
587
|
durationMs,
|
|
588
|
+
captureStepTimeoutMs(parsed, durationMs, false),
|
|
560
589
|
)
|
|
561
590
|
if (!stopResult.ok) {
|
|
562
591
|
console.error(` stop failed: ${stopResult.error ?? 'unknown error'}`)
|
|
@@ -590,11 +619,11 @@ async function probeSim(bridge: WsBridge, code: string): Promise<boolean> {
|
|
|
590
619
|
try {
|
|
591
620
|
return await bridge.send(
|
|
592
621
|
{ type: 'evaluate', code },
|
|
593
|
-
{
|
|
622
|
+
{ defaultTimeoutMs: SIM_PROBE_TIMEOUT_MS },
|
|
594
623
|
)
|
|
595
624
|
} catch (err) {
|
|
596
625
|
const msg = err instanceof Error ? err.message : String(err)
|
|
597
|
-
if (/^command timed out after \d+s$/.test(msg)) {
|
|
626
|
+
if (/^command timed out after \d+(?:\.\d+)?s$/.test(msg)) {
|
|
598
627
|
console.error(
|
|
599
628
|
' sim did not respond — it is connected to the bridge but has not\n' +
|
|
600
629
|
' loaded an app (or its page is unresponsive). run `peach list`\n' +
|
|
@@ -786,7 +815,7 @@ async function refuseLiveRecording(parsed: ParsedBridgeCliArgs) {
|
|
|
786
815
|
...parsed,
|
|
787
816
|
simId: existing.simId ?? parsed.simId,
|
|
788
817
|
simIdSource: existing.simId ? 'saved' : parsed.simIdSource,
|
|
789
|
-
commandTimeoutMs: 8_000,
|
|
818
|
+
commandTimeoutMs: parsed.explicitTimeout ? parsed.commandTimeoutMs : 8_000,
|
|
790
819
|
})
|
|
791
820
|
try {
|
|
792
821
|
const sims = await bridge.listSims()
|
|
@@ -808,8 +837,12 @@ async function refuseLiveRecording(parsed: ParsedBridgeCliArgs) {
|
|
|
808
837
|
}
|
|
809
838
|
|
|
810
839
|
async function recordStart(args: string[], opts: RecordOptions) {
|
|
840
|
+
const mode = parseMode(valueOf(args, '--mode'))
|
|
811
841
|
const parsed = parseBridgeCliArgs(args, {
|
|
812
842
|
port: opts.port,
|
|
843
|
+
// an explicit --timeout wins over the per-mode default
|
|
844
|
+
commandTimeoutMs:
|
|
845
|
+
mode === 'live' || mode === 'combined' ? STORE_RECORD_START_TIMEOUT_MS : 15_000,
|
|
813
846
|
stripBooleanFlags: ['--no-shell', '--shell-only', '--lockstep'],
|
|
814
847
|
stripValueFlags: [
|
|
815
848
|
'--mode',
|
|
@@ -823,7 +856,6 @@ async function recordStart(args: string[], opts: RecordOptions) {
|
|
|
823
856
|
],
|
|
824
857
|
})
|
|
825
858
|
await refuseLiveRecording(parsed)
|
|
826
|
-
const mode = parseMode(valueOf(args, '--mode'))
|
|
827
859
|
const layers: 'tenant' | 'shell' | undefined = args.includes('--shell-only')
|
|
828
860
|
? 'shell'
|
|
829
861
|
: args.includes('--no-shell')
|
|
@@ -840,11 +872,19 @@ async function recordStart(args: string[], opts: RecordOptions) {
|
|
|
840
872
|
const fps = Number(valueOf(args, '--fps') ?? '30')
|
|
841
873
|
const repoScope = readRepoScope(args)
|
|
842
874
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
875
|
+
// the session is recorded before the engine is asked to start, so a start
|
|
876
|
+
// that times out on this side while the engine records stays stoppable. a
|
|
877
|
+
// refusal clears it.
|
|
878
|
+
const session: RecordingState = {
|
|
879
|
+
simId: parsed.simId ?? null,
|
|
880
|
+
mode,
|
|
881
|
+
format,
|
|
882
|
+
fps,
|
|
883
|
+
layers,
|
|
884
|
+
startedAt: new Date().toISOString(),
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const bridge = createBridgeFromParsed(parsed)
|
|
848
888
|
try {
|
|
849
889
|
if (mode === 'live' || mode === 'combined') {
|
|
850
890
|
// live mode captures events only — --no-shell / --shell-only have no
|
|
@@ -869,6 +909,7 @@ async function recordStart(args: string[], opts: RecordOptions) {
|
|
|
869
909
|
await assertGuestBundleLoaded(bridge, mode)
|
|
870
910
|
if (mode === 'combined') await assertTabVisibleForCapture(bridge)
|
|
871
911
|
await injectPreviewSessionAuth(bridge, valueOf(args, '--origin'), repoScope)
|
|
912
|
+
writeRecordingState(session)
|
|
872
913
|
const started = await evalStoreStart(
|
|
873
914
|
bridge,
|
|
874
915
|
mode,
|
|
@@ -876,6 +917,7 @@ async function recordStart(args: string[], opts: RecordOptions) {
|
|
|
876
917
|
mode === 'combined' ? layers : undefined,
|
|
877
918
|
)
|
|
878
919
|
if (!started.ok) {
|
|
920
|
+
clearRecordingState()
|
|
879
921
|
console.error(
|
|
880
922
|
` start failed: recording store refused to start (${mode})` +
|
|
881
923
|
(started.error ? `: ${started.error}` : ''),
|
|
@@ -883,8 +925,6 @@ async function recordStart(args: string[], opts: RecordOptions) {
|
|
|
883
925
|
peachExit(1)
|
|
884
926
|
}
|
|
885
927
|
} else {
|
|
886
|
-
await assertRecorderAvailable(bridge)
|
|
887
|
-
await assertTabVisibleForCapture(bridge)
|
|
888
928
|
const lockstep = args.includes('--lockstep')
|
|
889
929
|
if (lockstep && format !== 'mp4') {
|
|
890
930
|
console.error(' --lockstep requires --format mp4 (webm is realtime-clocked)')
|
|
@@ -898,23 +938,21 @@ async function recordStart(args: string[], opts: RecordOptions) {
|
|
|
898
938
|
} = { format, fps }
|
|
899
939
|
if (layers) startOpts.layers = layers
|
|
900
940
|
if (lockstep) startOpts.lockstep = true
|
|
941
|
+
writeRecordingState(session)
|
|
901
942
|
const result: { ok: boolean; error?: string } = await bridge.send({
|
|
902
943
|
type: 'evaluate',
|
|
903
|
-
code: `
|
|
944
|
+
code: `(() => {
|
|
945
|
+
if (!window.__sootsimRecorder) return { ok: false, error: 'recorder unavailable in this sim' };
|
|
946
|
+
if (document.hidden) return { ok: false, error: 'the peach tab is hidden; bring it to the foreground before recording' };
|
|
947
|
+
return window.__sootsimRecorder.start(${JSON.stringify(startOpts)});
|
|
948
|
+
})()`,
|
|
904
949
|
})
|
|
905
950
|
if (!result.ok) {
|
|
951
|
+
clearRecordingState()
|
|
906
952
|
console.error(` start failed: ${result.error ?? 'unknown error'}`)
|
|
907
953
|
peachExit(1)
|
|
908
954
|
}
|
|
909
955
|
}
|
|
910
|
-
writeRecordingState({
|
|
911
|
-
simId: parsed.simId ?? null,
|
|
912
|
-
mode,
|
|
913
|
-
format,
|
|
914
|
-
fps,
|
|
915
|
-
layers,
|
|
916
|
-
startedAt: new Date().toISOString(),
|
|
917
|
-
})
|
|
918
956
|
if (mode === 'video') {
|
|
919
957
|
console.log(
|
|
920
958
|
` recording ${format} @ ${fps}fps${layers ? ` (${layers})` : ''} — run \`peach record stop --output <path>\` when done`,
|
|
@@ -936,7 +974,7 @@ async function recordPreludeStart(args: string[], opts: RecordOptions) {
|
|
|
936
974
|
stripValueFlags: ['--origin'],
|
|
937
975
|
})
|
|
938
976
|
await refuseLiveRecording(parsed)
|
|
939
|
-
const bridge = createBridgeFromParsed(
|
|
977
|
+
const bridge = createBridgeFromParsed(parsed)
|
|
940
978
|
try {
|
|
941
979
|
await assertStoreBridgesAvailable(bridge)
|
|
942
980
|
await assertTabVisibleForCapture(bridge)
|
|
@@ -987,7 +1025,13 @@ async function recordPreludeStart(args: string[], opts: RecordOptions) {
|
|
|
987
1025
|
// - the event recorder alone drives hidden PR-preview preludes before a
|
|
988
1026
|
// visible live/combined segment exists
|
|
989
1027
|
// returns null when the engine can't be reached.
|
|
990
|
-
type EngineRecordingState =
|
|
1028
|
+
type EngineRecordingState =
|
|
1029
|
+
| 'idle'
|
|
1030
|
+
| 'starting'
|
|
1031
|
+
| 'recording'
|
|
1032
|
+
| 'processing'
|
|
1033
|
+
| 'ready'
|
|
1034
|
+
| 'prelude'
|
|
991
1035
|
|
|
992
1036
|
async function probeEngineRecordingState(
|
|
993
1037
|
bridge: WsBridge,
|
|
@@ -1008,8 +1052,10 @@ async function probeEngineRecordingState(
|
|
|
1008
1052
|
})()`,
|
|
1009
1053
|
})
|
|
1010
1054
|
if (!snap) return null
|
|
1055
|
+
if (snap.headless === 'starting') return 'starting'
|
|
1011
1056
|
if (snap.store === 'recording' || snap.headless === 'recording') return 'recording'
|
|
1012
|
-
if (snap.store === 'processing') return 'processing'
|
|
1057
|
+
if (snap.store === 'processing' || snap.headless === 'processing') return 'processing'
|
|
1058
|
+
if (snap.headless === 'ready') return 'ready'
|
|
1013
1059
|
if (snap.eventRecording) return 'prelude'
|
|
1014
1060
|
if (snap.store === 'idle' || snap.headless === 'idle') return 'idle'
|
|
1015
1061
|
return null
|
|
@@ -1054,26 +1100,36 @@ async function cancelEngineRecording(bridge: WsBridge): Promise<{
|
|
|
1054
1100
|
|
|
1055
1101
|
async function recordStatus(args: string[], opts: RecordOptions) {
|
|
1056
1102
|
const state = readRecordingState()
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
)
|
|
1067
|
-
}
|
|
1068
|
-
return
|
|
1069
|
-
}
|
|
1070
|
-
// no `record start` session — but an atomic run or the rail button may
|
|
1071
|
-
// still be recording. ask the engine before claiming nothing is happening.
|
|
1072
|
-
const parsed = parseBridgeCliArgs(args, { port: opts.port })
|
|
1073
|
-
const bridge = createBridgeFromParsed({ ...parsed, commandTimeoutMs: 8_000 })
|
|
1103
|
+
const parsed = parseBridgeCliArgs(args, {
|
|
1104
|
+
port: opts.port,
|
|
1105
|
+
commandTimeoutMs: 8_000,
|
|
1106
|
+
})
|
|
1107
|
+
if (state) noteSimMismatch(parsed.simId, state)
|
|
1108
|
+
const bridge = createBridgeFromParsed({
|
|
1109
|
+
...parsed,
|
|
1110
|
+
simId: state?.simId ?? parsed.simId,
|
|
1111
|
+
})
|
|
1074
1112
|
try {
|
|
1075
1113
|
const engineState = await probeEngineRecordingState(bridge)
|
|
1114
|
+
if (state) {
|
|
1115
|
+
if (engineState === 'idle') {
|
|
1116
|
+
clearRecordingState()
|
|
1117
|
+
console.log(' no recording in progress')
|
|
1118
|
+
} else if (engineState === 'ready') {
|
|
1119
|
+
console.log(' recording ready to save — run `peach record stop --output <path>`')
|
|
1120
|
+
} else if (engineState === null) {
|
|
1121
|
+
console.log(
|
|
1122
|
+
' recording state unavailable — the sim did not respond; the tracked session is preserved',
|
|
1123
|
+
)
|
|
1124
|
+
} else {
|
|
1125
|
+
console.log(
|
|
1126
|
+
` ${engineState} ${state.mode}${state.mode === 'video' ? ` (${state.format} @ ${state.fps}fps)` : ''} on sim ${state.simId ?? '?'} since ${state.startedAt}`,
|
|
1127
|
+
)
|
|
1128
|
+
}
|
|
1129
|
+
return
|
|
1130
|
+
}
|
|
1076
1131
|
if (
|
|
1132
|
+
engineState === 'starting' ||
|
|
1077
1133
|
engineState === 'recording' ||
|
|
1078
1134
|
engineState === 'processing' ||
|
|
1079
1135
|
engineState === 'prelude'
|
|
@@ -1111,16 +1167,18 @@ async function recordCancel(args: string[], opts: RecordOptions) {
|
|
|
1111
1167
|
if (!state) {
|
|
1112
1168
|
// no `record start` session to cancel — but check the engine so we
|
|
1113
1169
|
// don't falsely claim nothing is happening while an atomic run records.
|
|
1114
|
-
const probeParsed = parseBridgeCliArgs(args, {
|
|
1115
|
-
|
|
1116
|
-
...probeParsed,
|
|
1170
|
+
const probeParsed = parseBridgeCliArgs(args, {
|
|
1171
|
+
port: opts.port,
|
|
1117
1172
|
commandTimeoutMs: 8_000,
|
|
1118
1173
|
})
|
|
1174
|
+
const probeBridge = createBridgeFromParsed(probeParsed)
|
|
1119
1175
|
try {
|
|
1120
1176
|
const engineState = await probeEngineRecordingState(probeBridge)
|
|
1121
1177
|
if (
|
|
1178
|
+
engineState === 'starting' ||
|
|
1122
1179
|
engineState === 'recording' ||
|
|
1123
1180
|
engineState === 'processing' ||
|
|
1181
|
+
engineState === 'ready' ||
|
|
1124
1182
|
engineState === 'prelude'
|
|
1125
1183
|
) {
|
|
1126
1184
|
// `record cancel` is explicit intent: the caller wants whatever is
|
|
@@ -1152,18 +1210,14 @@ async function recordCancel(args: string[], opts: RecordOptions) {
|
|
|
1152
1210
|
}
|
|
1153
1211
|
return
|
|
1154
1212
|
}
|
|
1155
|
-
const parsed = parseBridgeCliArgs(args, { port: opts.port })
|
|
1213
|
+
const parsed = parseBridgeCliArgs(args, { port: opts.port, commandTimeoutMs: 15_000 })
|
|
1156
1214
|
noteSimMismatch(parsed.simId, state)
|
|
1157
1215
|
// the recorder (and its buffered blob) lives in the page of the sim the
|
|
1158
1216
|
// recording was *started* on — `state.simId` wins over the requested
|
|
1159
1217
|
// --sim, otherwise cancel would stop an idle recorder on the wrong sim
|
|
1160
1218
|
// and leave the real recording running (F11-1).
|
|
1161
1219
|
const simId = state.simId ?? parsed.simId ?? undefined
|
|
1162
|
-
const bridge = createBridgeFromParsed({
|
|
1163
|
-
...parsed,
|
|
1164
|
-
simId,
|
|
1165
|
-
commandTimeoutMs: 15_000,
|
|
1166
|
-
})
|
|
1220
|
+
const bridge = createBridgeFromParsed({ ...parsed, simId })
|
|
1167
1221
|
try {
|
|
1168
1222
|
if (state.mode === 'live' || state.mode === 'combined') {
|
|
1169
1223
|
await bridge.send({
|
|
@@ -1173,7 +1227,11 @@ async function recordCancel(args: string[], opts: RecordOptions) {
|
|
|
1173
1227
|
} else {
|
|
1174
1228
|
await bridge.send({
|
|
1175
1229
|
type: 'evaluate',
|
|
1176
|
-
code: `
|
|
1230
|
+
code: `(async () => {
|
|
1231
|
+
const recorder = window.__sootsimRecorder;
|
|
1232
|
+
await recorder.stop();
|
|
1233
|
+
return recorder.forceRelease();
|
|
1234
|
+
})()`,
|
|
1177
1235
|
})
|
|
1178
1236
|
}
|
|
1179
1237
|
} catch {
|
|
@@ -1194,6 +1252,9 @@ async function recordStop(args: string[], opts: RecordOptions) {
|
|
|
1194
1252
|
|
|
1195
1253
|
const parsed = parseBridgeCliArgs(args, {
|
|
1196
1254
|
port: opts.port,
|
|
1255
|
+
// an explicit --timeout wins over the per-mode default
|
|
1256
|
+
commandTimeoutMs:
|
|
1257
|
+
state.mode === 'live' || state.mode === 'combined' ? UPLOAD_TIMEOUT_MS : 60_000,
|
|
1197
1258
|
stripBooleanFlags: ['--open'],
|
|
1198
1259
|
stripValueFlags: ['--output'],
|
|
1199
1260
|
})
|
|
@@ -1204,15 +1265,25 @@ async function recordStop(args: string[], opts: RecordOptions) {
|
|
|
1204
1265
|
const simId = state.simId ?? parsed.simId ?? undefined
|
|
1205
1266
|
const openAfter = args.includes('--open')
|
|
1206
1267
|
|
|
1207
|
-
const bridge = createBridgeFromParsed({
|
|
1208
|
-
...parsed,
|
|
1209
|
-
simId,
|
|
1210
|
-
commandTimeoutMs:
|
|
1211
|
-
state.mode === 'live' || state.mode === 'combined' ? UPLOAD_TIMEOUT_MS : 60_000,
|
|
1212
|
-
})
|
|
1268
|
+
const bridge = createBridgeFromParsed({ ...parsed, simId })
|
|
1213
1269
|
try {
|
|
1214
1270
|
if (state.mode === 'live' || state.mode === 'combined') {
|
|
1215
|
-
await evalStoreStop(bridge)
|
|
1271
|
+
const stopped = await evalStoreStop(bridge)
|
|
1272
|
+
if (stopped === 'pending') {
|
|
1273
|
+
console.error(
|
|
1274
|
+
` stop failed: the ${state.mode} start is still in flight and has not begun a take yet.\n` +
|
|
1275
|
+
' run `peach record stop` again once it begins.',
|
|
1276
|
+
)
|
|
1277
|
+
peachExit(1)
|
|
1278
|
+
}
|
|
1279
|
+
if (!stopped) {
|
|
1280
|
+
clearRecordingState()
|
|
1281
|
+
console.error(
|
|
1282
|
+
` stop failed: the ${state.mode} start still in flight never began a take, so there is nothing to upload.\n` +
|
|
1283
|
+
' run `peach record start` again.',
|
|
1284
|
+
)
|
|
1285
|
+
peachExit(1)
|
|
1286
|
+
}
|
|
1216
1287
|
const result = await pollStoreUpload(bridge)
|
|
1217
1288
|
clearRecordingState()
|
|
1218
1289
|
handleStoreUploadResult(result, openAfter, state.mode)
|
|
@@ -1235,7 +1306,17 @@ async function recordStop(args: string[], opts: RecordOptions) {
|
|
|
1235
1306
|
})
|
|
1236
1307
|
if (!stopResult.ok) {
|
|
1237
1308
|
console.error(` stop failed: ${stopResult.error ?? 'unknown error'}`)
|
|
1238
|
-
|
|
1309
|
+
// a start that timed out on the client can still be on its way to the
|
|
1310
|
+
// engine. keep the state so a later stop finds the recording instead of
|
|
1311
|
+
// leaving it running with nothing to stop it.
|
|
1312
|
+
if (stopResult.error === 'not recording') {
|
|
1313
|
+
console.error(
|
|
1314
|
+
' the start may not have reached the engine yet. retry `peach record stop`,\n' +
|
|
1315
|
+
' or run `peach record cancel` to give up on it.',
|
|
1316
|
+
)
|
|
1317
|
+
} else {
|
|
1318
|
+
clearRecordingState()
|
|
1319
|
+
}
|
|
1239
1320
|
peachExit(1)
|
|
1240
1321
|
}
|
|
1241
1322
|
if (!stopResult.size) {
|
|
@@ -1396,11 +1477,16 @@ async function evalStoreStart(
|
|
|
1396
1477
|
return { ok: false, error: error ?? null }
|
|
1397
1478
|
}
|
|
1398
1479
|
|
|
1399
|
-
|
|
1400
|
-
|
|
1480
|
+
// the engine stop waits for a start still in flight. false means that start
|
|
1481
|
+
// never began a take, so no upload will come and the store's last one belongs
|
|
1482
|
+
// to an earlier take. 'pending' means the start was still pending when the
|
|
1483
|
+
// engine stopped waiting, so it may yet begin one.
|
|
1484
|
+
async function evalStoreStop(bridge: WsBridge): Promise<boolean | 'pending'> {
|
|
1485
|
+
const stopped: boolean | 'pending' | null = await bridge.send({
|
|
1401
1486
|
type: 'evaluate',
|
|
1402
|
-
code: `
|
|
1487
|
+
code: `window.SootSim.bridges.stopRecording()`,
|
|
1403
1488
|
})
|
|
1489
|
+
return stopped === 'pending' ? stopped : stopped !== false
|
|
1404
1490
|
}
|
|
1405
1491
|
|
|
1406
1492
|
interface StoreUploadResult {
|
package/cli/commands/swift.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { WebSocket, WebSocketServer } from 'ws'
|
|
|
5
5
|
import { resolveContrastBackendOrigin } from '../../src/backend-origin'
|
|
6
6
|
import { produceConfiguredBundle } from '../../src/configured-bundle-producer'
|
|
7
7
|
import { swiftGuestBundle } from '../../src/guest-bundle'
|
|
8
|
+
import { insertNativeAppIdentityFooter } from '../../src/native-app-identity'
|
|
8
9
|
import {
|
|
9
10
|
resolveSwiftModule,
|
|
10
11
|
SWIFT_ENDPOINT_ENV,
|
|
@@ -164,11 +165,21 @@ export async function runSwift(
|
|
|
164
165
|
const appProject = resolveAppProject(packagePath)
|
|
165
166
|
host.configureSwiftStandinBehavior(packagePath, await loadRNXAppConfig(appProject))
|
|
166
167
|
// collection preflights every input path before any source bytes are read.
|
|
167
|
-
|
|
168
|
+
try {
|
|
169
|
+
host.swiftPackageWatchFiles(packagePath, undefined)
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const { SwiftObjCCompilationError } = await import('../../src/swift-objc-compile')
|
|
172
|
+
if (!(error instanceof SwiftObjCCompilationError)) throw error
|
|
173
|
+
console.error(error.message)
|
|
174
|
+
host.disposeSwiftProject(packagePath)
|
|
175
|
+
return 1
|
|
176
|
+
}
|
|
168
177
|
const packageName = path.basename(packagePath)
|
|
169
178
|
const appInfo = host.swiftProjectAppInfo(packagePath)
|
|
170
179
|
const guestSource = swiftGuestBundle(packageName)
|
|
171
|
-
|
|
180
|
+
const withIdentity = (source: string) =>
|
|
181
|
+
appInfo.identity ? insertNativeAppIdentityFooter(source, appInfo.identity) : source
|
|
182
|
+
let guest = withIdentity(guestSource)
|
|
172
183
|
const controller = new AbortController()
|
|
173
184
|
const resources: OpenCommandResources = {}
|
|
174
185
|
let metadata: (SwiftEntryModuleArgs & { hmrPath: string }) | null = null
|
|
@@ -192,7 +203,9 @@ export async function runSwift(
|
|
|
192
203
|
try {
|
|
193
204
|
const config = await loadRNXAppConfig(appProject)
|
|
194
205
|
host.configureSwiftStandinBehavior(packagePath, config)
|
|
195
|
-
guest =
|
|
206
|
+
guest = withIdentity(
|
|
207
|
+
await produceConfiguredBundle(guestSource, config, new Set()),
|
|
208
|
+
)
|
|
196
209
|
const next = await host.swiftPackageMetadata(
|
|
197
210
|
packagePath,
|
|
198
211
|
packageName,
|
|
@@ -348,7 +361,8 @@ export async function runSwift(
|
|
|
348
361
|
console.log(` Swift preview host: ${origin}`)
|
|
349
362
|
await refresh()
|
|
350
363
|
controller.signal.throwIfAborted()
|
|
351
|
-
|
|
364
|
+
// refresh already printed the error
|
|
365
|
+
if (!metadata || compileError) return 1
|
|
352
366
|
if (opts.device) openArgs.push('--device', opts.device)
|
|
353
367
|
if (opts.driver) openArgs.push('--driver', opts.driver)
|
|
354
368
|
if (opts.headless) openArgs.push('--headless')
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
import { createHash } from 'node:crypto'
|
|
7
7
|
import { mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
8
8
|
import { dirname, extname, resolve } from 'node:path'
|
|
9
|
+
import {
|
|
10
|
+
collectTimelineAssetRefs,
|
|
11
|
+
timelineAssetRefOf,
|
|
12
|
+
} from 'peach-engine/screenshots/timeline-assets'
|
|
9
13
|
import {
|
|
10
14
|
isThreeModeSetting,
|
|
11
15
|
THREE_MODE_SETTING_IDS,
|
|
@@ -353,44 +357,6 @@ function mimeTypeForAsset(path: string, contentType: unknown) {
|
|
|
353
357
|
// timeline as blob: URLs.
|
|
354
358
|
const TIMELINE_ASSET_CHUNK_BYTES = 1_048_576
|
|
355
359
|
|
|
356
|
-
type TimelineAssetRef = {
|
|
357
|
-
src: string
|
|
358
|
-
recording: boolean
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function timelineAssetRefOf(value: Record<string, unknown>): TimelineAssetRef | null {
|
|
362
|
-
if (
|
|
363
|
-
(value.type === 'recording' ||
|
|
364
|
-
value.type === 'screenshot' ||
|
|
365
|
-
value.kind === 'screen') &&
|
|
366
|
-
typeof value.src === 'string' &&
|
|
367
|
-
!/^(?:blob:|data:|https?:)/i.test(value.src)
|
|
368
|
-
) {
|
|
369
|
-
return { src: value.src, recording: value.type === 'recording' }
|
|
370
|
-
}
|
|
371
|
-
return null
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function collectTimelineAssetRefs(value: unknown): TimelineAssetRef[] {
|
|
375
|
-
const refs: TimelineAssetRef[] = []
|
|
376
|
-
const seen = new Set<string>()
|
|
377
|
-
const visit = (node: unknown): void => {
|
|
378
|
-
if (Array.isArray(node)) {
|
|
379
|
-
for (const entry of node) visit(entry)
|
|
380
|
-
return
|
|
381
|
-
}
|
|
382
|
-
if (!isRecord(node)) return
|
|
383
|
-
for (const entry of Object.values(node)) visit(entry)
|
|
384
|
-
const ref = timelineAssetRefOf(node)
|
|
385
|
-
if (ref && !seen.has(ref.src)) {
|
|
386
|
-
seen.add(ref.src)
|
|
387
|
-
refs.push(ref)
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
visit(value)
|
|
391
|
-
return refs
|
|
392
|
-
}
|
|
393
|
-
|
|
394
360
|
type DistinctTimelineAsset = {
|
|
395
361
|
hash: string
|
|
396
362
|
mime: string
|