peachsim 0.1.1635 → 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/README.md +5 -1
- package/cli/app-config.ts +14 -5
- package/cli/app-stubs.ts +62 -13
- package/cli/bridge-flow-runner.ts +2 -2
- package/cli/cloud-client.ts +60 -40
- package/cli/commands/doctor.ts +13 -5
- package/cli/commands/flow.ts +1 -1
- package/cli/commands/inspect/shared.ts +9 -4
- package/cli/commands/inspect.ts +65 -47
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +97 -47
- package/cli/commands/swift.ts +9 -3
- package/cli/commands/upload.ts +48 -42
- package/cli/drivers/playwright-sim-host.ts +65 -42
- package/cli/main.ts +1 -1
- package/cli/outbound-endpoints.ts +7 -5
- package/cli/pause-aware-timeout.ts +4 -0
- package/cli/ws-bridge.ts +19 -10
- package/detox/attempt-events.cjs +62 -5
- package/detox/attempt-events.d.cts +7 -0
- package/detox/expectations.ts +7 -3
- package/detox/index.ts +121 -47
- 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 +8 -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 +4038 -1983
- 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 +509 -86
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +3551 -1574
- 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 +43633 -2635
- 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 +2748 -128
- 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 +2547 -83
- package/dist-lib/native-app-identity-project.cjs +237 -50
- package/dist-lib/native-app-identity-project.mjs +237 -50
- 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 +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +65403 -22739
- package/dist-lib/swift-compile.cjs +93 -32
- package/dist-lib/swift-compile.mjs +93 -32
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +7 -5
- package/dist-lib/swift-submit.mjs +7 -5
- package/dist-lib/swift-toolchain.cjs +316 -41
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +7335 -5432
- 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 +13 -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 +21 -6
- package/src/react-native-host-modules.ts +4 -1
- package/src/settings-bundle.ts +1 -4
- package/src/swift-compile.ts +91 -33
- package/src/swift-distribution.lock.json +22 -12
- package/src/swift-distribution.ts +103 -4
- package/src/swift-host.ts +89 -26
- package/src/swift-icu.ts +16 -10
- package/src/swift-objc-compile.ts +355 -0
- package/src/swift-objc-sdk.ts +689 -18
- package/src/swift-objc-standin.ts +126 -14
- package/src/swift-package-manifest.ts +39 -1
- package/src/swift-packages.ts +35 -21
- package/src/swift-standin-emit.ts +40 -7
- package/src/swift-standins.ts +404 -138
- package/src/swift-submit.ts +3 -5
- package/src/swift-toolchain.ts +378 -46
- 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/inspect.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// peach inspect — dump node tree, accessibility info, query nodes
|
|
2
2
|
// connects to the peach WS bridge (no Playwright)
|
|
3
3
|
|
|
4
|
+
import { Script } from 'node:vm'
|
|
4
5
|
import { getCliInspectVerbMeta } from '@peach/skills/cli'
|
|
5
6
|
import { renderCliCommandHelp, renderCliGroupHelp } from '@peach/skills/cli/renderers'
|
|
6
7
|
import { DEFAULT_SOOTSIM_BRIDGE_PORT } from '../../src/bridge-constants'
|
|
@@ -194,6 +195,8 @@ async function performSingleStep(
|
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
// runs after a command already spent its whole budget, so an explicit --timeout
|
|
199
|
+
// must not make the liveness check wait that long a second time
|
|
197
200
|
async function probeSimResponsive(bridge: WsBridge): Promise<boolean> {
|
|
198
201
|
try {
|
|
199
202
|
await bridge.send({ type: 'evaluate', code: '1' }, { timeoutMs: 3000 })
|
|
@@ -210,6 +213,18 @@ async function reportMissingTarget(
|
|
|
210
213
|
verb: 'tap' | 'double-tap' | 'long-press',
|
|
211
214
|
target: { mode: 'testid' | 'text'; value: string },
|
|
212
215
|
): Promise<void> {
|
|
216
|
+
if (target.mode === 'testid') {
|
|
217
|
+
// the tree can hold the id while the engine has no point to press: the node
|
|
218
|
+
// is not painted yet, or was replaced since the query. say so, so the
|
|
219
|
+
// suggestion list never names the id that was just reported missing.
|
|
220
|
+
const exact = await inspectFind(bridge, { testId: target.value }).catch(() => null)
|
|
221
|
+
if (exact?.result) {
|
|
222
|
+
console.error(
|
|
223
|
+
` found ${target.value} in the tree but it has no tap target yet; it may be unpainted, hidden or covered: wait for it with peach wait selector ${target.value}`,
|
|
224
|
+
)
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
}
|
|
213
228
|
console.error(` not found: ${target.value}`)
|
|
214
229
|
if (target.mode !== 'testid') return
|
|
215
230
|
const byText = await inspectFind(bridge, { text: target.value }).catch(() => null)
|
|
@@ -858,11 +873,10 @@ async function waitForKeyboardVisible(
|
|
|
858
873
|
}
|
|
859
874
|
|
|
860
875
|
/**
|
|
861
|
-
*
|
|
862
|
-
*
|
|
863
|
-
* whatever holds focus and fails the batch when nothing does.
|
|
876
|
+
* dispatch through the shared input executor, which checks the requested
|
|
877
|
+
* node identity before targeted typing and carries refusals out of the CLI.
|
|
864
878
|
*/
|
|
865
|
-
async function
|
|
879
|
+
async function performKeys(
|
|
866
880
|
bridge: WsBridge,
|
|
867
881
|
steps: Array<Extract<PerformStep, { type: 'type' | 'typeInto' | 'key' }>>,
|
|
868
882
|
actionLabel: string,
|
|
@@ -2462,7 +2476,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2462
2476
|
console.error(' type-into requires a resolved text input node identity')
|
|
2463
2477
|
peachExit(1)
|
|
2464
2478
|
}
|
|
2465
|
-
const typed = await
|
|
2479
|
+
const typed = await performKeys(
|
|
2466
2480
|
bridge,
|
|
2467
2481
|
[{ type: 'typeInto', text, nodeId }],
|
|
2468
2482
|
'type-into',
|
|
@@ -2500,6 +2514,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2500
2514
|
return {
|
|
2501
2515
|
cx: n.absolutePosition.x + (n.layout.width || 0) / 2,
|
|
2502
2516
|
cy: n.absolutePosition.y + (n.layout.height || 0) / 2,
|
|
2517
|
+
nodeId: n.nodeId,
|
|
2503
2518
|
id: n.id,
|
|
2504
2519
|
testID: n.testID,
|
|
2505
2520
|
type: n.type,
|
|
@@ -2514,7 +2529,13 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2514
2529
|
peachExit(1)
|
|
2515
2530
|
}
|
|
2516
2531
|
if (!tiPayload.isTextInput) {
|
|
2517
|
-
console.error(`
|
|
2532
|
+
console.error(` ${targetId} is not a text input (isTextInput: false)`)
|
|
2533
|
+
peachExit(1)
|
|
2534
|
+
}
|
|
2535
|
+
const nodeId: unknown = tiPayload.nodeId
|
|
2536
|
+
if (typeof nodeId !== 'number' || !Number.isSafeInteger(nodeId) || nodeId === 0) {
|
|
2537
|
+
console.error(' type-into requires a resolved text input node identity')
|
|
2538
|
+
peachExit(1)
|
|
2518
2539
|
}
|
|
2519
2540
|
// step 2: tap to focus
|
|
2520
2541
|
const tapResult = await bridge.send({
|
|
@@ -2522,12 +2543,17 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2522
2543
|
x: tiPayload.cx,
|
|
2523
2544
|
y: tiPayload.cy,
|
|
2524
2545
|
target: {
|
|
2546
|
+
nodeId,
|
|
2525
2547
|
id: tiPayload.id ?? targetId,
|
|
2526
2548
|
testID: tiPayload.testID ?? targetId,
|
|
2527
2549
|
text: null,
|
|
2528
2550
|
type: tiPayload.type ?? null,
|
|
2529
2551
|
},
|
|
2530
2552
|
})
|
|
2553
|
+
if (!isTapSuccess(tapResult)) {
|
|
2554
|
+
console.error(` type-into could not tap ${targetId}`)
|
|
2555
|
+
peachExit(1)
|
|
2556
|
+
}
|
|
2531
2557
|
const targetSecureTextEntry = tiPayload.secureTextEntry === true
|
|
2532
2558
|
const keyboardState = await waitForKeyboardVisible(bridge, 1000, {
|
|
2533
2559
|
targetId,
|
|
@@ -2558,38 +2584,26 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2558
2584
|
peachExit(1)
|
|
2559
2585
|
}
|
|
2560
2586
|
}
|
|
2561
|
-
//
|
|
2562
|
-
//
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
peachExit(1)
|
|
2576
|
-
}
|
|
2577
|
-
return count
|
|
2578
|
-
}
|
|
2579
|
-
const existingCharacters = await drawnCharacters()
|
|
2580
|
-
for (let i = 0; i < existingCharacters; i++) {
|
|
2581
|
-
await bridge.send({ type: 'keyboard', action: 'press', text: 'delete' })
|
|
2582
|
-
}
|
|
2583
|
-
// backspace clears only what precedes the caret the tap placed
|
|
2584
|
-
if (existingCharacters > 0 && (await drawnCharacters()) > 0) {
|
|
2585
|
-
console.error(
|
|
2586
|
-
` type-into could not clear ${targetId}: text remains after the caret the tap placed`,
|
|
2587
|
-
)
|
|
2587
|
+
// type at the live selection through the same guarded operation as
|
|
2588
|
+
// chained and cloud typing; clearing first changes controlled values.
|
|
2589
|
+
const typed = await performKeys(
|
|
2590
|
+
bridge,
|
|
2591
|
+
[{ type: 'typeInto', text, nodeId }],
|
|
2592
|
+
'type-into',
|
|
2593
|
+
)
|
|
2594
|
+
const receipt: unknown = typed.steps[0]?.value
|
|
2595
|
+
if (
|
|
2596
|
+
!receipt ||
|
|
2597
|
+
typeof receipt !== 'object' ||
|
|
2598
|
+
typeof Reflect.get(receipt, 'secureTextEntry') !== 'boolean'
|
|
2599
|
+
) {
|
|
2600
|
+
console.error(' type-into did not return its matched input secure state')
|
|
2588
2601
|
peachExit(1)
|
|
2589
2602
|
}
|
|
2590
|
-
await bridge.send({ type: 'keyboard', action: 'type', text })
|
|
2591
2603
|
const secureTextEntry =
|
|
2592
|
-
targetSecureTextEntry ||
|
|
2604
|
+
targetSecureTextEntry ||
|
|
2605
|
+
Reflect.get(receipt, 'secureTextEntry') === true ||
|
|
2606
|
+
isSecureKeyboardState(keyboardState as any)
|
|
2593
2607
|
const outputText = secureTextEntry
|
|
2594
2608
|
? SECURE_TEXT_REDACTION
|
|
2595
2609
|
: redactTextForKeyboardState(text, keyboardState as any)
|
|
@@ -2629,7 +2643,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2629
2643
|
peachExit(1)
|
|
2630
2644
|
}
|
|
2631
2645
|
if (bridge.plane === 'cloud') {
|
|
2632
|
-
await
|
|
2646
|
+
await performKeys(bridge, [{ type: 'type', text }], 'type')
|
|
2633
2647
|
await recordInspectAction(
|
|
2634
2648
|
'inspect type',
|
|
2635
2649
|
{ inputText: text },
|
|
@@ -2663,7 +2677,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2663
2677
|
peachExit(1)
|
|
2664
2678
|
}
|
|
2665
2679
|
if (bridge.plane === 'cloud') {
|
|
2666
|
-
await
|
|
2680
|
+
await performKeys(bridge, [{ type: 'key', key: name }], 'key')
|
|
2667
2681
|
} else {
|
|
2668
2682
|
await requireVisualKeyboard(bridge, 'key')
|
|
2669
2683
|
await bridge.send({ type: 'keyboard', action: 'press', text: name })
|
|
@@ -2680,7 +2694,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
2680
2694
|
peachExit(1)
|
|
2681
2695
|
}
|
|
2682
2696
|
if (bridge.plane === 'cloud') {
|
|
2683
|
-
await
|
|
2697
|
+
await performKeys(
|
|
2684
2698
|
bridge,
|
|
2685
2699
|
keys.map((key) => ({ type: 'key' as const, key })),
|
|
2686
2700
|
'key-sequence',
|
|
@@ -3717,13 +3731,17 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
3717
3731
|
// wrap in async to support await in worker mode. no identifier
|
|
3718
3732
|
// rewriting — the snippet runs verbatim so tokens like `root`,
|
|
3719
3733
|
// `debug`, `test` refer to whatever the user actually typed.
|
|
3720
|
-
//
|
|
3721
|
-
//
|
|
3734
|
+
// compile without executing to distinguish expressions from statements;
|
|
3735
|
+
// punctuation inside callbacks, literals and comments is not a separator.
|
|
3722
3736
|
let expandedCode = code
|
|
3723
3737
|
if (!expandedCode.startsWith('(async')) {
|
|
3724
|
-
expandedCode =
|
|
3725
|
-
|
|
3726
|
-
|
|
3738
|
+
expandedCode = `(async () => (\n${code}\n))()`
|
|
3739
|
+
try {
|
|
3740
|
+
new Script(expandedCode)
|
|
3741
|
+
} catch (error) {
|
|
3742
|
+
if (!(error instanceof SyntaxError)) throw error
|
|
3743
|
+
expandedCode = `(async () => {\n${code}\n})()`
|
|
3744
|
+
}
|
|
3727
3745
|
}
|
|
3728
3746
|
const result = await bridge.send({ type: 'evaluate', code: expandedCode })
|
|
3729
3747
|
printJson(result)
|
|
@@ -3959,11 +3977,11 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
3959
3977
|
case 'start': {
|
|
3960
3978
|
const result = await bridge.send({
|
|
3961
3979
|
type: 'evaluate',
|
|
3962
|
-
code: `(() => {
|
|
3980
|
+
code: `(async () => {
|
|
3963
3981
|
if (!window.__sootsimShellPerf) {
|
|
3964
3982
|
return { error: 'shell frame profile unavailable (__sootsimShellPerf missing on the page)' }
|
|
3965
3983
|
}
|
|
3966
|
-
window.__sootsimShellPerf.start()
|
|
3984
|
+
await window.__sootsimShellPerf.start()
|
|
3967
3985
|
return { started: true }
|
|
3968
3986
|
})()`,
|
|
3969
3987
|
})
|
|
@@ -4038,7 +4056,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
4038
4056
|
if (!window.__sootsimShellPerf) {
|
|
4039
4057
|
return { error: 'shell frame profile unavailable (__sootsimShellPerf missing on the page)' }
|
|
4040
4058
|
}
|
|
4041
|
-
window.__sootsimShellPerf.start()
|
|
4059
|
+
await window.__sootsimShellPerf.start()
|
|
4042
4060
|
// give a frame for profiling to engage before the event
|
|
4043
4061
|
await new Promise(r => requestAnimationFrame(() => r(undefined)))
|
|
4044
4062
|
window.dispatchEvent(new Event('${eventName}'))
|
|
@@ -4626,7 +4644,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
4626
4644
|
// re-incurs the full per-command timeout. three sequential probes turn a
|
|
4627
4645
|
// single 15s timeout into ~60s of apparent hang (QA F19-1). there is
|
|
4628
4646
|
// nothing useful to fetch from a sim that just timed out, so skip them.
|
|
4629
|
-
const timedOut = /^command timed out after (\d+)s$/.exec(message)
|
|
4647
|
+
const timedOut = /^command timed out after (\d+(?:\.\d+)?)s$/.exec(message)
|
|
4630
4648
|
// a disconnected bridge is genuinely gone; recover is right and probing
|
|
4631
4649
|
// it would just re-incur a full timeout. no bridge at all means there is
|
|
4632
4650
|
// no sim to recover, only one to open.
|
package/cli/commands/preview.ts
CHANGED
|
@@ -88,7 +88,7 @@ export async function runPreview(args: string[]): Promise<number> {
|
|
|
88
88
|
bundleSizeBytes: artifact.bytes.byteLength,
|
|
89
89
|
bundleContentType: 'application/javascript',
|
|
90
90
|
bundleEncoding: 'br',
|
|
91
|
-
bundleMinified:
|
|
91
|
+
bundleMinified: artifact.minified,
|
|
92
92
|
bundleOrigin: null,
|
|
93
93
|
entry: null,
|
|
94
94
|
isTransformed: false,
|
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` +
|
|
@@ -242,6 +265,8 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
242
265
|
|
|
243
266
|
const parsed = parseBridgeCliArgs(args, {
|
|
244
267
|
port: opts.port,
|
|
268
|
+
// an explicit --timeout wins over the default
|
|
269
|
+
commandTimeoutMs: 60_000,
|
|
245
270
|
stripBooleanFlags: ['--no-shell', '--shell-only', '--open', '--lockstep'],
|
|
246
271
|
stripValueFlags: [
|
|
247
272
|
'--mode',
|
|
@@ -356,7 +381,7 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
356
381
|
originOverride,
|
|
357
382
|
allowGitHubAuth: true,
|
|
358
383
|
})
|
|
359
|
-
const bridge = createBridgeFromParsed(
|
|
384
|
+
const bridge = createBridgeFromParsed(parsed)
|
|
360
385
|
try {
|
|
361
386
|
await assertStoreBridgesAvailable(bridge)
|
|
362
387
|
await assertGuestBundleLoaded(bridge, mode)
|
|
@@ -376,7 +401,7 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
376
401
|
peachExit(1)
|
|
377
402
|
}
|
|
378
403
|
console.log(` recording ${mode} for ${durationSec}s`)
|
|
379
|
-
await
|
|
404
|
+
await holdCapture(bridge, durationMs)
|
|
380
405
|
await evalStoreStop(bridge)
|
|
381
406
|
const result = await pollStoreUpload(bridge)
|
|
382
407
|
handleStoreUploadResult(result, openAfter, mode)
|
|
@@ -398,7 +423,7 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
398
423
|
)
|
|
399
424
|
}
|
|
400
425
|
|
|
401
|
-
const bridge = createBridgeFromParsed(
|
|
426
|
+
const bridge = createBridgeFromParsed(parsed)
|
|
402
427
|
try {
|
|
403
428
|
await assertRecorderAvailable(bridge)
|
|
404
429
|
await assertTabVisibleForCapture(bridge)
|
|
@@ -434,7 +459,7 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
434
459
|
console.error(` frame capture start failed: ${start.error ?? 'unknown error'}`)
|
|
435
460
|
peachExit(1)
|
|
436
461
|
}
|
|
437
|
-
await
|
|
462
|
+
await holdCapture(bridge, durationMs)
|
|
438
463
|
const deadline = Date.now() + Math.max(5_000, durationMs)
|
|
439
464
|
// frames come back a few at a time: one message holding every base64 png
|
|
440
465
|
// exceeds the bridge socket's message cap and closes it.
|
|
@@ -490,6 +515,7 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
490
515
|
}${layers ? `, layers: ${JSON.stringify(layers)}` : ''} })`,
|
|
491
516
|
'gif encode',
|
|
492
517
|
durationMs,
|
|
518
|
+
captureStepTimeoutMs(parsed, durationMs, true),
|
|
493
519
|
)
|
|
494
520
|
if (!result) {
|
|
495
521
|
console.error(' gif capture returned no frames')
|
|
@@ -523,6 +549,7 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
523
549
|
`window.__sootsimRecorder.start(${JSON.stringify(startOpts)})`,
|
|
524
550
|
`${format} start`,
|
|
525
551
|
durationMs,
|
|
552
|
+
captureStepTimeoutMs(parsed, durationMs, false),
|
|
526
553
|
)
|
|
527
554
|
if (!startResult.ok) {
|
|
528
555
|
console.error(` start failed: ${startResult.error ?? 'unknown error'}`)
|
|
@@ -543,7 +570,7 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
543
570
|
}
|
|
544
571
|
|
|
545
572
|
console.log(` recording ${format} for ${durationSec}s → ${out}`)
|
|
546
|
-
await
|
|
573
|
+
await holdCapture(bridge, durationMs)
|
|
547
574
|
|
|
548
575
|
const stopResult: {
|
|
549
576
|
ok: boolean
|
|
@@ -557,6 +584,7 @@ export async function runRecord(args: string[], opts: RecordOptions) {
|
|
|
557
584
|
`window.__sootsimRecorder.stop()`,
|
|
558
585
|
`${format} encode/flush`,
|
|
559
586
|
durationMs,
|
|
587
|
+
captureStepTimeoutMs(parsed, durationMs, false),
|
|
560
588
|
)
|
|
561
589
|
if (!stopResult.ok) {
|
|
562
590
|
console.error(` stop failed: ${stopResult.error ?? 'unknown error'}`)
|
|
@@ -590,11 +618,11 @@ async function probeSim(bridge: WsBridge, code: string): Promise<boolean> {
|
|
|
590
618
|
try {
|
|
591
619
|
return await bridge.send(
|
|
592
620
|
{ type: 'evaluate', code },
|
|
593
|
-
{
|
|
621
|
+
{ defaultTimeoutMs: SIM_PROBE_TIMEOUT_MS },
|
|
594
622
|
)
|
|
595
623
|
} catch (err) {
|
|
596
624
|
const msg = err instanceof Error ? err.message : String(err)
|
|
597
|
-
if (/^command timed out after \d+s$/.test(msg)) {
|
|
625
|
+
if (/^command timed out after \d+(?:\.\d+)?s$/.test(msg)) {
|
|
598
626
|
console.error(
|
|
599
627
|
' sim did not respond — it is connected to the bridge but has not\n' +
|
|
600
628
|
' loaded an app (or its page is unresponsive). run `peach list`\n' +
|
|
@@ -786,7 +814,7 @@ async function refuseLiveRecording(parsed: ParsedBridgeCliArgs) {
|
|
|
786
814
|
...parsed,
|
|
787
815
|
simId: existing.simId ?? parsed.simId,
|
|
788
816
|
simIdSource: existing.simId ? 'saved' : parsed.simIdSource,
|
|
789
|
-
commandTimeoutMs: 8_000,
|
|
817
|
+
commandTimeoutMs: parsed.explicitTimeout ? parsed.commandTimeoutMs : 8_000,
|
|
790
818
|
})
|
|
791
819
|
try {
|
|
792
820
|
const sims = await bridge.listSims()
|
|
@@ -808,8 +836,12 @@ async function refuseLiveRecording(parsed: ParsedBridgeCliArgs) {
|
|
|
808
836
|
}
|
|
809
837
|
|
|
810
838
|
async function recordStart(args: string[], opts: RecordOptions) {
|
|
839
|
+
const mode = parseMode(valueOf(args, '--mode'))
|
|
811
840
|
const parsed = parseBridgeCliArgs(args, {
|
|
812
841
|
port: opts.port,
|
|
842
|
+
// an explicit --timeout wins over the per-mode default
|
|
843
|
+
commandTimeoutMs:
|
|
844
|
+
mode === 'live' || mode === 'combined' ? STORE_RECORD_START_TIMEOUT_MS : 15_000,
|
|
813
845
|
stripBooleanFlags: ['--no-shell', '--shell-only', '--lockstep'],
|
|
814
846
|
stripValueFlags: [
|
|
815
847
|
'--mode',
|
|
@@ -823,7 +855,6 @@ async function recordStart(args: string[], opts: RecordOptions) {
|
|
|
823
855
|
],
|
|
824
856
|
})
|
|
825
857
|
await refuseLiveRecording(parsed)
|
|
826
|
-
const mode = parseMode(valueOf(args, '--mode'))
|
|
827
858
|
const layers: 'tenant' | 'shell' | undefined = args.includes('--shell-only')
|
|
828
859
|
? 'shell'
|
|
829
860
|
: args.includes('--no-shell')
|
|
@@ -840,11 +871,19 @@ async function recordStart(args: string[], opts: RecordOptions) {
|
|
|
840
871
|
const fps = Number(valueOf(args, '--fps') ?? '30')
|
|
841
872
|
const repoScope = readRepoScope(args)
|
|
842
873
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
874
|
+
// the session is recorded before the engine is asked to start, so a start
|
|
875
|
+
// that times out on this side while the engine records stays stoppable. a
|
|
876
|
+
// refusal clears it.
|
|
877
|
+
const session: RecordingState = {
|
|
878
|
+
simId: parsed.simId ?? null,
|
|
879
|
+
mode,
|
|
880
|
+
format,
|
|
881
|
+
fps,
|
|
882
|
+
layers,
|
|
883
|
+
startedAt: new Date().toISOString(),
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const bridge = createBridgeFromParsed(parsed)
|
|
848
887
|
try {
|
|
849
888
|
if (mode === 'live' || mode === 'combined') {
|
|
850
889
|
// live mode captures events only — --no-shell / --shell-only have no
|
|
@@ -869,6 +908,7 @@ async function recordStart(args: string[], opts: RecordOptions) {
|
|
|
869
908
|
await assertGuestBundleLoaded(bridge, mode)
|
|
870
909
|
if (mode === 'combined') await assertTabVisibleForCapture(bridge)
|
|
871
910
|
await injectPreviewSessionAuth(bridge, valueOf(args, '--origin'), repoScope)
|
|
911
|
+
writeRecordingState(session)
|
|
872
912
|
const started = await evalStoreStart(
|
|
873
913
|
bridge,
|
|
874
914
|
mode,
|
|
@@ -876,6 +916,7 @@ async function recordStart(args: string[], opts: RecordOptions) {
|
|
|
876
916
|
mode === 'combined' ? layers : undefined,
|
|
877
917
|
)
|
|
878
918
|
if (!started.ok) {
|
|
919
|
+
clearRecordingState()
|
|
879
920
|
console.error(
|
|
880
921
|
` start failed: recording store refused to start (${mode})` +
|
|
881
922
|
(started.error ? `: ${started.error}` : ''),
|
|
@@ -898,23 +939,17 @@ async function recordStart(args: string[], opts: RecordOptions) {
|
|
|
898
939
|
} = { format, fps }
|
|
899
940
|
if (layers) startOpts.layers = layers
|
|
900
941
|
if (lockstep) startOpts.lockstep = true
|
|
942
|
+
writeRecordingState(session)
|
|
901
943
|
const result: { ok: boolean; error?: string } = await bridge.send({
|
|
902
944
|
type: 'evaluate',
|
|
903
945
|
code: `window.__sootsimRecorder.start(${JSON.stringify(startOpts)})`,
|
|
904
946
|
})
|
|
905
947
|
if (!result.ok) {
|
|
948
|
+
clearRecordingState()
|
|
906
949
|
console.error(` start failed: ${result.error ?? 'unknown error'}`)
|
|
907
950
|
peachExit(1)
|
|
908
951
|
}
|
|
909
952
|
}
|
|
910
|
-
writeRecordingState({
|
|
911
|
-
simId: parsed.simId ?? null,
|
|
912
|
-
mode,
|
|
913
|
-
format,
|
|
914
|
-
fps,
|
|
915
|
-
layers,
|
|
916
|
-
startedAt: new Date().toISOString(),
|
|
917
|
-
})
|
|
918
953
|
if (mode === 'video') {
|
|
919
954
|
console.log(
|
|
920
955
|
` recording ${format} @ ${fps}fps${layers ? ` (${layers})` : ''} — run \`peach record stop --output <path>\` when done`,
|
|
@@ -936,7 +971,7 @@ async function recordPreludeStart(args: string[], opts: RecordOptions) {
|
|
|
936
971
|
stripValueFlags: ['--origin'],
|
|
937
972
|
})
|
|
938
973
|
await refuseLiveRecording(parsed)
|
|
939
|
-
const bridge = createBridgeFromParsed(
|
|
974
|
+
const bridge = createBridgeFromParsed(parsed)
|
|
940
975
|
try {
|
|
941
976
|
await assertStoreBridgesAvailable(bridge)
|
|
942
977
|
await assertTabVisibleForCapture(bridge)
|
|
@@ -1069,8 +1104,8 @@ async function recordStatus(args: string[], opts: RecordOptions) {
|
|
|
1069
1104
|
}
|
|
1070
1105
|
// no `record start` session — but an atomic run or the rail button may
|
|
1071
1106
|
// still be recording. ask the engine before claiming nothing is happening.
|
|
1072
|
-
const parsed = parseBridgeCliArgs(args, { port: opts.port })
|
|
1073
|
-
const bridge = createBridgeFromParsed(
|
|
1107
|
+
const parsed = parseBridgeCliArgs(args, { port: opts.port, commandTimeoutMs: 8_000 })
|
|
1108
|
+
const bridge = createBridgeFromParsed(parsed)
|
|
1074
1109
|
try {
|
|
1075
1110
|
const engineState = await probeEngineRecordingState(bridge)
|
|
1076
1111
|
if (
|
|
@@ -1111,11 +1146,11 @@ async function recordCancel(args: string[], opts: RecordOptions) {
|
|
|
1111
1146
|
if (!state) {
|
|
1112
1147
|
// no `record start` session to cancel — but check the engine so we
|
|
1113
1148
|
// don't falsely claim nothing is happening while an atomic run records.
|
|
1114
|
-
const probeParsed = parseBridgeCliArgs(args, {
|
|
1115
|
-
|
|
1116
|
-
...probeParsed,
|
|
1149
|
+
const probeParsed = parseBridgeCliArgs(args, {
|
|
1150
|
+
port: opts.port,
|
|
1117
1151
|
commandTimeoutMs: 8_000,
|
|
1118
1152
|
})
|
|
1153
|
+
const probeBridge = createBridgeFromParsed(probeParsed)
|
|
1119
1154
|
try {
|
|
1120
1155
|
const engineState = await probeEngineRecordingState(probeBridge)
|
|
1121
1156
|
if (
|
|
@@ -1152,18 +1187,14 @@ async function recordCancel(args: string[], opts: RecordOptions) {
|
|
|
1152
1187
|
}
|
|
1153
1188
|
return
|
|
1154
1189
|
}
|
|
1155
|
-
const parsed = parseBridgeCliArgs(args, { port: opts.port })
|
|
1190
|
+
const parsed = parseBridgeCliArgs(args, { port: opts.port, commandTimeoutMs: 15_000 })
|
|
1156
1191
|
noteSimMismatch(parsed.simId, state)
|
|
1157
1192
|
// the recorder (and its buffered blob) lives in the page of the sim the
|
|
1158
1193
|
// recording was *started* on — `state.simId` wins over the requested
|
|
1159
1194
|
// --sim, otherwise cancel would stop an idle recorder on the wrong sim
|
|
1160
1195
|
// and leave the real recording running (F11-1).
|
|
1161
1196
|
const simId = state.simId ?? parsed.simId ?? undefined
|
|
1162
|
-
const bridge = createBridgeFromParsed({
|
|
1163
|
-
...parsed,
|
|
1164
|
-
simId,
|
|
1165
|
-
commandTimeoutMs: 15_000,
|
|
1166
|
-
})
|
|
1197
|
+
const bridge = createBridgeFromParsed({ ...parsed, simId })
|
|
1167
1198
|
try {
|
|
1168
1199
|
if (state.mode === 'live' || state.mode === 'combined') {
|
|
1169
1200
|
await bridge.send({
|
|
@@ -1194,6 +1225,9 @@ async function recordStop(args: string[], opts: RecordOptions) {
|
|
|
1194
1225
|
|
|
1195
1226
|
const parsed = parseBridgeCliArgs(args, {
|
|
1196
1227
|
port: opts.port,
|
|
1228
|
+
// an explicit --timeout wins over the per-mode default
|
|
1229
|
+
commandTimeoutMs:
|
|
1230
|
+
state.mode === 'live' || state.mode === 'combined' ? UPLOAD_TIMEOUT_MS : 60_000,
|
|
1197
1231
|
stripBooleanFlags: ['--open'],
|
|
1198
1232
|
stripValueFlags: ['--output'],
|
|
1199
1233
|
})
|
|
@@ -1204,15 +1238,17 @@ async function recordStop(args: string[], opts: RecordOptions) {
|
|
|
1204
1238
|
const simId = state.simId ?? parsed.simId ?? undefined
|
|
1205
1239
|
const openAfter = args.includes('--open')
|
|
1206
1240
|
|
|
1207
|
-
const bridge = createBridgeFromParsed({
|
|
1208
|
-
...parsed,
|
|
1209
|
-
simId,
|
|
1210
|
-
commandTimeoutMs:
|
|
1211
|
-
state.mode === 'live' || state.mode === 'combined' ? UPLOAD_TIMEOUT_MS : 60_000,
|
|
1212
|
-
})
|
|
1241
|
+
const bridge = createBridgeFromParsed({ ...parsed, simId })
|
|
1213
1242
|
try {
|
|
1214
1243
|
if (state.mode === 'live' || state.mode === 'combined') {
|
|
1215
|
-
await evalStoreStop(bridge)
|
|
1244
|
+
if (!(await evalStoreStop(bridge))) {
|
|
1245
|
+
clearRecordingState()
|
|
1246
|
+
console.error(
|
|
1247
|
+
` stop failed: the ${state.mode} start still in flight never began a take, so there is nothing to upload.\n` +
|
|
1248
|
+
' run `peach record start` again.',
|
|
1249
|
+
)
|
|
1250
|
+
peachExit(1)
|
|
1251
|
+
}
|
|
1216
1252
|
const result = await pollStoreUpload(bridge)
|
|
1217
1253
|
clearRecordingState()
|
|
1218
1254
|
handleStoreUploadResult(result, openAfter, state.mode)
|
|
@@ -1235,7 +1271,17 @@ async function recordStop(args: string[], opts: RecordOptions) {
|
|
|
1235
1271
|
})
|
|
1236
1272
|
if (!stopResult.ok) {
|
|
1237
1273
|
console.error(` stop failed: ${stopResult.error ?? 'unknown error'}`)
|
|
1238
|
-
|
|
1274
|
+
// a start that timed out on the client can still be on its way to the
|
|
1275
|
+
// engine. keep the state so a later stop finds the recording instead of
|
|
1276
|
+
// leaving it running with nothing to stop it.
|
|
1277
|
+
if (stopResult.error === 'not recording') {
|
|
1278
|
+
console.error(
|
|
1279
|
+
' the start may not have reached the engine yet. retry `peach record stop`,\n' +
|
|
1280
|
+
' or run `peach record cancel` to give up on it.',
|
|
1281
|
+
)
|
|
1282
|
+
} else {
|
|
1283
|
+
clearRecordingState()
|
|
1284
|
+
}
|
|
1239
1285
|
peachExit(1)
|
|
1240
1286
|
}
|
|
1241
1287
|
if (!stopResult.size) {
|
|
@@ -1396,11 +1442,15 @@ async function evalStoreStart(
|
|
|
1396
1442
|
return { ok: false, error: error ?? null }
|
|
1397
1443
|
}
|
|
1398
1444
|
|
|
1399
|
-
|
|
1400
|
-
|
|
1445
|
+
// the engine stop waits for a start still in flight. false means that start
|
|
1446
|
+
// never began a take, so no upload will come and the store's last one belongs
|
|
1447
|
+
// to an earlier take.
|
|
1448
|
+
async function evalStoreStop(bridge: WsBridge): Promise<boolean> {
|
|
1449
|
+
const stopped: boolean | null = await bridge.send({
|
|
1401
1450
|
type: 'evaluate',
|
|
1402
|
-
code: `
|
|
1451
|
+
code: `window.SootSim.bridges.stopRecording()`,
|
|
1403
1452
|
})
|
|
1453
|
+
return stopped !== false
|
|
1404
1454
|
}
|
|
1405
1455
|
|
|
1406
1456
|
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,
|
|
@@ -168,7 +169,9 @@ export async function runSwift(
|
|
|
168
169
|
const packageName = path.basename(packagePath)
|
|
169
170
|
const appInfo = host.swiftProjectAppInfo(packagePath)
|
|
170
171
|
const guestSource = swiftGuestBundle(packageName)
|
|
171
|
-
|
|
172
|
+
const withIdentity = (source: string) =>
|
|
173
|
+
appInfo.identity ? insertNativeAppIdentityFooter(source, appInfo.identity) : source
|
|
174
|
+
let guest = withIdentity(guestSource)
|
|
172
175
|
const controller = new AbortController()
|
|
173
176
|
const resources: OpenCommandResources = {}
|
|
174
177
|
let metadata: (SwiftEntryModuleArgs & { hmrPath: string }) | null = null
|
|
@@ -192,7 +195,9 @@ export async function runSwift(
|
|
|
192
195
|
try {
|
|
193
196
|
const config = await loadRNXAppConfig(appProject)
|
|
194
197
|
host.configureSwiftStandinBehavior(packagePath, config)
|
|
195
|
-
guest =
|
|
198
|
+
guest = withIdentity(
|
|
199
|
+
await produceConfiguredBundle(guestSource, config, new Set()),
|
|
200
|
+
)
|
|
196
201
|
const next = await host.swiftPackageMetadata(
|
|
197
202
|
packagePath,
|
|
198
203
|
packageName,
|
|
@@ -348,7 +353,8 @@ export async function runSwift(
|
|
|
348
353
|
console.log(` Swift preview host: ${origin}`)
|
|
349
354
|
await refresh()
|
|
350
355
|
controller.signal.throwIfAborted()
|
|
351
|
-
|
|
356
|
+
// refresh already printed the error
|
|
357
|
+
if (!metadata || compileError) throw new Error('Swift compilation failed, see above')
|
|
352
358
|
if (opts.device) openArgs.push('--device', opts.device)
|
|
353
359
|
if (opts.driver) openArgs.push('--driver', opts.driver)
|
|
354
360
|
if (opts.headless) openArgs.push('--headless')
|