peachsim 0.1.577 → 0.1.579
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 +11 -9
- package/cli/app-catalog.ts +77 -0
- package/cli/app-fonts.ts +4 -20
- package/cli/app-splash.ts +2 -18
- package/cli/bridge-flow-runner.ts +505 -139
- package/cli/cloud-client.ts +1 -4
- package/cli/cloud-session.ts +1 -4
- package/cli/cloud-test-result-files.ts +3 -0
- package/cli/cloud-test-tunnel.ts +2 -5
- package/cli/command-registry.ts +2 -0
- package/cli/commands/agent.ts +149 -23
- package/cli/commands/assert.ts +1 -4
- package/cli/commands/box/peach-command.ts +62 -0
- package/cli/commands/box/plane-share.ts +0 -70
- package/cli/commands/box/rnx-command.ts +1 -60
- package/cli/commands/box/search-command.ts +14 -0
- package/cli/commands/box.ts +2 -2
- package/cli/commands/control.ts +121 -27
- package/cli/commands/cpu-profile.ts +23 -10
- package/cli/commands/daemon.ts +1 -9
- package/cli/commands/detox.ts +26 -18
- package/cli/commands/device.ts +129 -1
- package/cli/commands/film.ts +25 -16
- package/cli/commands/flow.ts +9 -2
- package/cli/commands/inspect/actions.ts +75 -1
- package/cli/commands/inspect/core.ts +81 -2
- package/cli/commands/inspect/describe.ts +2 -6
- package/cli/commands/inspect/env.ts +1 -1
- package/cli/commands/inspect/settling.ts +20 -3
- package/cli/commands/inspect/shared.ts +1 -4
- package/cli/commands/inspect/sleep.ts +1 -1
- package/cli/commands/inspect.ts +16 -49
- package/cli/commands/install-desktop.ts +8 -6
- package/cli/commands/login.ts +3 -5
- package/cli/commands/maestro-generate.ts +1 -4
- package/cli/commands/maestro.ts +38 -21
- package/cli/commands/no-bridge-hint.ts +0 -14
- package/cli/commands/permissions.ts +2 -2
- package/cli/commands/platform.ts +13 -6
- package/cli/commands/screenshot.ts +6 -1
- package/cli/commands/skills.ts +1 -1
- package/cli/commands/test.ts +6 -2
- package/cli/commands/three-mode.ts +162 -19
- package/cli/commands/upload.ts +3 -18
- package/cli/current-sim.ts +0 -1
- package/cli/desktop-companion.ts +40 -8
- package/cli/drivers/playwright-provisioning.ts +1 -4
- package/cli/flow-export.ts +1 -8
- package/cli/flow-session.ts +0 -4
- package/cli/hints.ts +1 -16
- package/cli/inspect-notice-state.ts +0 -4
- package/cli/main.ts +2 -4
- package/cli/outbound-endpoints.ts +1 -24
- package/cli/parse-args.ts +1 -2
- package/cli/rnx-in-process.ts +3 -1
- package/cli/run-rnx-core.ts +4 -2
- package/cli/run-rnx.ts +18 -10
- package/cli/send-to-box.ts +1 -4
- package/cli/telemetry.ts +1 -1
- package/cli/ws-bridge.ts +1 -5
- package/detox/gestures.ts +20 -6
- package/detox/index.ts +379 -123
- package/detox/navigation-lifecycle.ts +22 -13
- package/dist-lib/agent-daemon-client.cjs +21 -15
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +3 -17
- package/dist-lib/agent-sessions.cjs +5 -4
- package/dist-lib/attached-projects.cjs +1 -77
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +11 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +7 -5
- package/dist-lib/bridge-contract-input.mjs +7 -5
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +30 -7
- package/dist-lib/capture-contract.mjs +30 -7
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +17 -11
- package/dist-lib/cloud.mjs +17 -11
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +321 -122
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +427 -212
- package/dist-lib/host/fetch-proxy-handler.cjs +5 -28
- 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 +315 -58
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/menu.cjs +67 -31
- package/dist-lib/menu.mjs +64 -28
- package/dist-lib/metro-fingerprint-registry.cjs +8 -4
- package/dist-lib/metro-fingerprint-registry.mjs +6 -4
- package/dist-lib/metro-production-bundle.cjs +2 -2
- package/dist-lib/metro-production-bundle.mjs +2 -2
- package/dist-lib/metro.cjs +8 -4
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +7 -4
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +3 -13
- package/dist-lib/scripts/dev-server-scanner.cjs +5 -17
- package/dist-lib/sdk.cjs +129 -10
- package/dist-lib/sdk.mjs +127 -10
- package/dist-lib/skills.cjs +1635 -1121
- package/dist-lib/swift-compile.cjs +277 -70
- package/dist-lib/swift-compile.mjs +275 -70
- package/dist-lib/swift-package.cjs +17 -5
- package/dist-lib/swift-package.mjs +15 -4
- package/dist-lib/swift-submit.cjs +32 -8
- package/dist-lib/swift-submit.mjs +29 -7
- package/dist-lib/swift.cjs +30912 -577
- package/dist-lib/vite-kotlin.cjs +356 -0
- package/dist-lib/vite-swift.cjs +1743 -0
- package/dist-lib/vite.cjs +208 -1039
- package/kotlin/README.md +18 -0
- package/kotlin/Sources/androidx/annotation/Keep.kt +4 -0
- package/kotlin/Sources/dev/onejs/one/OneNativeSourceDispatch.kt +7 -0
- package/kotlin/Sources/dev/onejs/one/source/OneModule.kt +4 -0
- package/kotlin/Sources/org/json/JSON.kt +244 -0
- package/kotlin/toolchain.json +19 -0
- package/package.json +14 -2
- package/scripts/dev-server-scanner.ts +0 -18
- package/scripts/optional-demo-registry.ts +0 -17
- package/skills/contrast/SKILL.md +2 -2
- package/skills/peach-setup/SKILL.md +16 -0
- package/src/agent-daemon-client.ts +36 -22
- package/src/agent-identity.ts +0 -15
- package/src/agent-prompt.ts +12 -0
- package/src/attached-projects.ts +0 -72
- package/src/backend-origin.ts +10 -0
- package/src/bridge-contract-input.ts +2 -5
- package/src/bridge-contract.ts +2 -0
- package/src/capture-contract.ts +53 -11
- package/src/cloud.ts +4 -9
- package/src/dev-server-open.ts +0 -99
- package/src/home-paths.ts +2 -3
- package/src/host/agent-host.ts +82 -36
- package/src/host/bridge-host.ts +5 -11
- package/src/host/fetch-proxy-handler.ts +19 -43
- package/src/is-record.ts +4 -0
- package/src/menu.ts +82 -37
- package/src/metro-fingerprint-registry.ts +1 -4
- package/src/metro-production-bundle.ts +1 -1
- package/src/plist.ts +1 -4
- package/src/public-brand.ts +5 -3
- package/src/render-mode.ts +0 -12
- package/src/runtime-assets.ts +9 -2
- package/src/screenshots/compose.ts +7 -1
- package/src/screenshots/frame-compose.ts +21 -10
- package/src/screenshots/schema.ts +1 -4
- package/src/sim-client.ts +2 -4
- package/src/skills/builtin/maestro-test.ts +1 -4
- package/src/sleep.ts +4 -0
- package/src/swift-compile.ts +209 -47
- package/src/swift-icu.ts +140 -0
- package/src/swift-package.ts +26 -2
- package/src/swift-submit.ts +84 -4
- package/src/swift.ts +2 -0
- package/src/vite-plugin-kotlin.ts +376 -0
- package/src/vite-plugin-one.ts +224 -36
- package/src/vite-plugin-swift.ts +382 -96
- package/src/vite-plugin.ts +121 -134
- package/src/vite.d.ts +10 -0
- package/cli/resolve-assets.ts +0 -63
- package/src/rolldown-plugin-swift.ts +0 -244
- package/src/team-machine-prompt.ts +0 -73
package/cli/commands/control.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { spawn, spawnSync } from 'node:child_process'
|
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import os from 'node:os'
|
|
4
4
|
import path from 'node:path'
|
|
5
|
+
import { encodeAppCatalogWire } from 'peach-engine/engine/app-catalog-wire'
|
|
5
6
|
import {
|
|
6
7
|
devices,
|
|
7
8
|
listSelectableDeviceModels,
|
|
9
|
+
resolveDeviceFitBox,
|
|
8
10
|
type DeviceModel,
|
|
9
11
|
} from 'peach-engine/settings'
|
|
10
12
|
import { WebSocket } from 'ws'
|
|
@@ -41,6 +43,8 @@ import {
|
|
|
41
43
|
runtimeVersionHostname,
|
|
42
44
|
} from '../../src/runtime-assets'
|
|
43
45
|
import { rnxRuntime } from '../../src/runtime-delivery'
|
|
46
|
+
import { sleep } from '../../src/sleep'
|
|
47
|
+
import { discoverAppCatalog } from '../app-catalog'
|
|
44
48
|
import { loadRNXAppConfig } from '../app-config'
|
|
45
49
|
import {
|
|
46
50
|
type DiscoveredAppFontSpec,
|
|
@@ -108,10 +112,6 @@ const SHELL_APPS: Record<string, string> = {
|
|
|
108
112
|
camera: '/app/camera',
|
|
109
113
|
}
|
|
110
114
|
|
|
111
|
-
function sleep(ms: number) {
|
|
112
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
113
|
-
}
|
|
114
|
-
|
|
115
115
|
function parsePositiveIntegerEnv(name: string): number | null {
|
|
116
116
|
const value = process.env[name]?.trim()
|
|
117
117
|
if (!value) return null
|
|
@@ -176,14 +176,19 @@ export async function ensureBridgeForOpen(
|
|
|
176
176
|
const lock = readDaemonLockfile()
|
|
177
177
|
assertExplicitBridgeLock(preferredPort, explicitPort, lock)
|
|
178
178
|
const preferredWorld = worldForBridgePort(preferredPort)
|
|
179
|
+
// a closed port refuses at once, so this window only matters when something
|
|
180
|
+
// listens: a live bridge on a loaded machine can take longer than 250 ms to
|
|
181
|
+
// finish the handshake, and missing it spawned a second bridge.
|
|
179
182
|
if (
|
|
180
183
|
(explicitPort || preferredWorld !== null) &&
|
|
181
|
-
(await canConnectToBridge(preferredPort,
|
|
184
|
+
(await canConnectToBridge(preferredPort, 2_000))
|
|
182
185
|
) {
|
|
183
186
|
return preferredPort
|
|
184
187
|
}
|
|
185
188
|
if (explicitPort && preferredWorld !== null) {
|
|
186
|
-
throw new Error(
|
|
189
|
+
throw new Error(
|
|
190
|
+
`peach open: requested bridge port ${preferredPort} is not reachable.`,
|
|
191
|
+
)
|
|
187
192
|
}
|
|
188
193
|
if (
|
|
189
194
|
!explicitPort &&
|
|
@@ -210,7 +215,21 @@ export async function ensureBridgeForOpen(
|
|
|
210
215
|
const deadline = Date.now() + OPEN_BRIDGE_SPAWN_TIMEOUT_MS
|
|
211
216
|
while (Date.now() < deadline) {
|
|
212
217
|
const nextLock = readDaemonLockfile()
|
|
213
|
-
|
|
218
|
+
if (
|
|
219
|
+
explicitPort &&
|
|
220
|
+
nextLock &&
|
|
221
|
+
isDaemonLockfileFresh(nextLock) &&
|
|
222
|
+
nextLock.bridgePort !== preferredPort
|
|
223
|
+
) {
|
|
224
|
+
// serve fell back because something already holds the pinned port. the
|
|
225
|
+
// bridge it started is one nobody asked for: stop it, and use the pinned
|
|
226
|
+
// port if what holds it is a bridge that answers given longer.
|
|
227
|
+
child.kill()
|
|
228
|
+
if (await canConnectToBridge(preferredPort, 5_000)) return preferredPort
|
|
229
|
+
throw new Error(
|
|
230
|
+
`peach open: requested bridge port ${preferredPort} is held by a process that does not answer as a peach bridge.`,
|
|
231
|
+
)
|
|
232
|
+
}
|
|
214
233
|
const selectedPort = explicitPort ? preferredPort : nextLock?.bridgePort
|
|
215
234
|
if (
|
|
216
235
|
nextLock &&
|
|
@@ -261,6 +280,9 @@ function assertExplicitBridgeLock(
|
|
|
261
280
|
}
|
|
262
281
|
|
|
263
282
|
async function ensureRuntimeForOpen() {
|
|
283
|
+
// a dev checkout serves its own build; staging a published runtime into an
|
|
284
|
+
// isolated home would make the daemon serve that instead.
|
|
285
|
+
if (resolveDevCheckoutRuntimeRoot()) return
|
|
264
286
|
const active = readActiveRuntime()
|
|
265
287
|
if (active && fs.existsSync(runtimeDir(active))) return
|
|
266
288
|
console.error(' installing peach engine runtime...')
|
|
@@ -351,7 +373,7 @@ function buildShellPathOpenUrl(target: string, baseUrl: string): string {
|
|
|
351
373
|
return url.toString()
|
|
352
374
|
}
|
|
353
375
|
|
|
354
|
-
// peach share/preview/build links (e.g. https://
|
|
376
|
+
// peach share/preview/build links (e.g. https://peachsim.com/preview/<id>,
|
|
355
377
|
// https://contrast.dev/build/<owner>/<repo>/<branch>) are full player pages
|
|
356
378
|
// served by the Contrast app — NOT Metro bundles and NOT driveable shell sims.
|
|
357
379
|
// passing one to `peach open` used to fall through to the bundle resolver,
|
|
@@ -374,12 +396,6 @@ function looksLikeSootSimShareUrl(target: string): boolean {
|
|
|
374
396
|
}
|
|
375
397
|
}
|
|
376
398
|
|
|
377
|
-
export async function resolveBundleTarget(target: string): Promise<string> {
|
|
378
|
-
const normalized = normalizeKnownTarget(target)
|
|
379
|
-
const resolved = await resolveConnectionInputForCli(normalized)
|
|
380
|
-
return resolved.bundleUrl
|
|
381
|
-
}
|
|
382
|
-
|
|
383
399
|
function localBasePort(target: string): number | null {
|
|
384
400
|
try {
|
|
385
401
|
const url = new URL(target.startsWith('http') ? target : `http://${target}`)
|
|
@@ -749,6 +765,23 @@ async function stampAppSplash(
|
|
|
749
765
|
}
|
|
750
766
|
}
|
|
751
767
|
|
|
768
|
+
// stamp the engine page url with `?appCatalog=` so an app's asset-catalog
|
|
769
|
+
// colors and its declared accent reach the tenant, which applies them to the
|
|
770
|
+
// bundle this url opens. the catalog lives in the native binary, never in
|
|
771
|
+
// the js bundle, so only the app's ios project on disk has it.
|
|
772
|
+
function stampAppCatalog(urlStr: string, project?: ResolvedAppProject | null): string {
|
|
773
|
+
try {
|
|
774
|
+
const url = new URL(urlStr)
|
|
775
|
+
if (!url.searchParams.get('bundle') || !project) return urlStr
|
|
776
|
+
const catalog = discoverAppCatalog(project.appDir)
|
|
777
|
+
if (!catalog) return urlStr
|
|
778
|
+
url.searchParams.set('appCatalog', encodeAppCatalogWire(catalog))
|
|
779
|
+
return url.toString()
|
|
780
|
+
} catch {
|
|
781
|
+
return urlStr
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
752
785
|
async function splashImageReachable(imageUrl: string): Promise<boolean> {
|
|
753
786
|
const controller = new AbortController()
|
|
754
787
|
const timer = setTimeout(() => controller.abort(), 1500)
|
|
@@ -775,6 +808,17 @@ export interface BuildShellUrlOptions {
|
|
|
775
808
|
// uses the same param. without this, every driver-opened sim silently ran
|
|
776
809
|
// whatever profile the shell last persisted.
|
|
777
810
|
device?: string
|
|
811
|
+
// fold posture (open|closed) stamped onto the engine URL as ?posture= next
|
|
812
|
+
// to ?device=. the shell reads it at boot (readUrlSettingOverrides) and
|
|
813
|
+
// keeps it in the URL on every pose change, so it round-trips the way
|
|
814
|
+
// device and orientation do. without this a manual ?posture= appended
|
|
815
|
+
// after the app path lands inside the bundle param and never reaches the
|
|
816
|
+
// shell. non-foldable devices resolve it to closed at boot.
|
|
817
|
+
posture?: string
|
|
818
|
+
// portrayed os version (major.minor) stamped as ?osVersion=, which the
|
|
819
|
+
// shell pins at boot over the device profile's own version. an oracle on a
|
|
820
|
+
// newer os than the profile compares only when the sim runs that os era.
|
|
821
|
+
osVersion?: string
|
|
778
822
|
resolvedBundle?: ResolvedDevBundle
|
|
779
823
|
project?: ResolvedAppProject | null
|
|
780
824
|
// deterministic-capture mode, stamped onto the engine URL as ?proof=1. the
|
|
@@ -810,10 +854,15 @@ export async function buildShellUrl(
|
|
|
810
854
|
// fallback typeface, which then reads as a conformance diff.
|
|
811
855
|
const project =
|
|
812
856
|
opts.project ?? (await resolveAppProjectForBundleUrl(bundleUrlOf(resolved)))
|
|
813
|
-
const stamped =
|
|
814
|
-
|
|
857
|
+
const stamped = stampAppCatalog(
|
|
858
|
+
await stampAppSplash(await stampAppFonts(resolved, project), project),
|
|
859
|
+
project,
|
|
860
|
+
)
|
|
861
|
+
if (!opts.device && !opts.proof && !opts.posture && !opts.osVersion) return stamped
|
|
815
862
|
const url = new URL(stamped)
|
|
816
863
|
if (opts.device) url.searchParams.set('device', opts.device)
|
|
864
|
+
if (opts.posture) url.searchParams.set('posture', opts.posture)
|
|
865
|
+
if (opts.osVersion) url.searchParams.set('osVersion', opts.osVersion)
|
|
817
866
|
if (opts.proof) url.searchParams.set('proof', '1')
|
|
818
867
|
return url.toString()
|
|
819
868
|
}
|
|
@@ -837,9 +886,11 @@ function deriveCurrentSimBaseUrl(
|
|
|
837
886
|
// and `stampAppFonts` doesn't get the chance to re-derive against the
|
|
838
887
|
// current bundle. drop it so the rebuild starts clean.
|
|
839
888
|
url.searchParams.delete('appFonts')
|
|
840
|
-
// appSplash
|
|
841
|
-
// splash re-
|
|
889
|
+
// appSplash and appCatalog are target-specific in the same way: drop them
|
|
890
|
+
// so the configured splash and catalog re-derive against the current
|
|
891
|
+
// bundle.
|
|
842
892
|
url.searchParams.delete('appSplash')
|
|
893
|
+
url.searchParams.delete('appCatalog')
|
|
843
894
|
return url.toString()
|
|
844
895
|
} catch {
|
|
845
896
|
return fallback
|
|
@@ -1182,6 +1233,12 @@ export function printOpenedSim(
|
|
|
1182
1233
|
console.log(JSON.stringify({ simId: sim.id, url: printableSimUrl }, null, 2))
|
|
1183
1234
|
}
|
|
1184
1235
|
|
|
1236
|
+
function printAgentListenHint(quiet: boolean): void {
|
|
1237
|
+
if (!quiet) {
|
|
1238
|
+
console.log(' inspect messages: run `peach agent listen` when you finish your turn')
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1185
1242
|
// after a successful open, agents almost always want to know what's on the
|
|
1186
1243
|
// screen. running describe inline saves a separate roundtrip. tightly
|
|
1187
1244
|
// bounded — a cold guest bundle takes 5-15s to fully route, so we don't
|
|
@@ -1343,6 +1400,8 @@ export async function runOpenCommand(
|
|
|
1343
1400
|
'--profile',
|
|
1344
1401
|
'--cdp-port',
|
|
1345
1402
|
'--device',
|
|
1403
|
+
'--posture',
|
|
1404
|
+
'--os-version',
|
|
1346
1405
|
'--viewport',
|
|
1347
1406
|
],
|
|
1348
1407
|
})
|
|
@@ -1475,7 +1534,9 @@ export async function runOpenCommand(
|
|
|
1475
1534
|
// native desktop surface. neither path opens the user's browser profile.
|
|
1476
1535
|
const requestedDriverId = args.find((_, i) => args[i - 1] === '--driver') || ''
|
|
1477
1536
|
if (requestedDriverId && !isDriverId(requestedDriverId)) {
|
|
1478
|
-
console.error(
|
|
1537
|
+
console.error(
|
|
1538
|
+
` unknown driver "${requestedDriverId}" — run \`peach list --drivers\``,
|
|
1539
|
+
)
|
|
1479
1540
|
rnxExit(1)
|
|
1480
1541
|
}
|
|
1481
1542
|
const driverId: DriverId = isDriverId(requestedDriverId)
|
|
@@ -1518,7 +1579,38 @@ export async function runOpenCommand(
|
|
|
1518
1579
|
console.error(` known devices: ${listSelectableDeviceModels().join(', ')}`)
|
|
1519
1580
|
rnxExit(1)
|
|
1520
1581
|
}
|
|
1521
|
-
|
|
1582
|
+
|
|
1583
|
+
// --posture <open|closed> seeds the fold posture for the opened sim,
|
|
1584
|
+
// stamped on the engine URL next to ?device= so it round-trips instead of
|
|
1585
|
+
// being swallowed into the bundle param. only foldable devices (iphone-duo)
|
|
1586
|
+
// have a second posture; anything else resolves to closed at boot.
|
|
1587
|
+
const postureArg = args.find((_, i) => args[i - 1] === '--posture')
|
|
1588
|
+
if (postureArg && postureArg !== 'open' && postureArg !== 'closed') {
|
|
1589
|
+
console.error(` peach open: --posture must be open or closed, got "${postureArg}"`)
|
|
1590
|
+
rnxExit(1)
|
|
1591
|
+
}
|
|
1592
|
+
// the window fits the box the browser stage fits the device into, which for
|
|
1593
|
+
// a foldable spans every posture and orientation, so the device draws at its
|
|
1594
|
+
// natural size in the pose it opens in instead of shrinking into a window
|
|
1595
|
+
// sized for one pose.
|
|
1596
|
+
const deviceFitBox = deviceArg
|
|
1597
|
+
? resolveDeviceFitBox(
|
|
1598
|
+
deviceArg as DeviceModel,
|
|
1599
|
+
false,
|
|
1600
|
+
undefined,
|
|
1601
|
+
postureArg === 'open' ? 'open' : 'closed',
|
|
1602
|
+
true,
|
|
1603
|
+
)
|
|
1604
|
+
: undefined
|
|
1605
|
+
|
|
1606
|
+
// --os-version <major.minor> pins the portrayed os over the profile's.
|
|
1607
|
+
const osVersionArg = args.find((_, i) => args[i - 1] === '--os-version')
|
|
1608
|
+
if (osVersionArg && !/^\d+\.\d+$/.test(osVersionArg)) {
|
|
1609
|
+
console.error(
|
|
1610
|
+
` peach open: --os-version must be <major>.<minor>, got "${osVersionArg}"`,
|
|
1611
|
+
)
|
|
1612
|
+
rnxExit(1)
|
|
1613
|
+
}
|
|
1522
1614
|
|
|
1523
1615
|
// --viewport <WxH> sizes the driver-launched page explicitly (film and
|
|
1524
1616
|
// promo captures frame the 3d stage in landscape). takes precedence over
|
|
@@ -1527,7 +1619,9 @@ export async function runOpenCommand(
|
|
|
1527
1619
|
const viewportArg = args.find((_, i) => args[i - 1] === '--viewport')
|
|
1528
1620
|
const viewportMatch = viewportArg ? /^(\d+)x(\d+)$/.exec(viewportArg) : null
|
|
1529
1621
|
if (viewportArg && !viewportMatch) {
|
|
1530
|
-
console.error(
|
|
1622
|
+
console.error(
|
|
1623
|
+
` peach open: --viewport must be <width>x<height>, got "${viewportArg}"`,
|
|
1624
|
+
)
|
|
1531
1625
|
rnxExit(1)
|
|
1532
1626
|
}
|
|
1533
1627
|
const explicitViewport = viewportMatch
|
|
@@ -1537,6 +1631,8 @@ export async function runOpenCommand(
|
|
|
1537
1631
|
const buildShellUrlOpts: BuildShellUrlOptions = {
|
|
1538
1632
|
hot: hotMode,
|
|
1539
1633
|
device: deviceArg,
|
|
1634
|
+
posture: postureArg,
|
|
1635
|
+
osVersion: osVersionArg,
|
|
1540
1636
|
resolvedBundle,
|
|
1541
1637
|
project,
|
|
1542
1638
|
proof: args.includes('--proof'),
|
|
@@ -1713,6 +1809,7 @@ export async function runOpenCommand(
|
|
|
1713
1809
|
quiet,
|
|
1714
1810
|
)
|
|
1715
1811
|
await maybeDescribeAfterOpen(parsed.wsPort, targetSim.id, args)
|
|
1812
|
+
printAgentListenHint(quiet)
|
|
1716
1813
|
return { simId: targetSim.id, launched: false }
|
|
1717
1814
|
}
|
|
1718
1815
|
} finally {
|
|
@@ -1752,8 +1849,8 @@ export async function runOpenCommand(
|
|
|
1752
1849
|
ownerPid,
|
|
1753
1850
|
viewport:
|
|
1754
1851
|
explicitViewport ??
|
|
1755
|
-
(
|
|
1756
|
-
? { width:
|
|
1852
|
+
(deviceFitBox
|
|
1853
|
+
? { width: deviceFitBox.width + 120, height: deviceFitBox.height + 60 }
|
|
1757
1854
|
: undefined),
|
|
1758
1855
|
connectTimeoutMs: connectWait.hostTimeoutMs,
|
|
1759
1856
|
})
|
|
@@ -1791,6 +1888,7 @@ export async function runOpenCommand(
|
|
|
1791
1888
|
signalDriverLaunchConnected(result)
|
|
1792
1889
|
printOpenedSim(openUrl, match, `${driver.name} driver`, quiet)
|
|
1793
1890
|
await maybeDescribeAfterOpen(parsed.wsPort, match.id, args)
|
|
1891
|
+
printAgentListenHint(quiet)
|
|
1794
1892
|
return { simId: match.id, launched: true }
|
|
1795
1893
|
}
|
|
1796
1894
|
|
|
@@ -1859,10 +1957,6 @@ export async function runUseCommand(args: string[], opts: ControlCommandOptions
|
|
|
1859
1957
|
await runUseLikeCommand(args, opts)
|
|
1860
1958
|
}
|
|
1861
1959
|
|
|
1862
|
-
export async function runFocusCommand(args: string[], opts: ControlCommandOptions = {}) {
|
|
1863
|
-
await runUseLikeCommand(args, opts)
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
1960
|
export async function runClaimCommand(args: string[], opts: ControlCommandOptions = {}) {
|
|
1867
1961
|
const parsed = parseBridgeCliArgs(args, {
|
|
1868
1962
|
port: opts.port,
|
|
@@ -30,6 +30,7 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
|
30
30
|
import { dirname, resolve } from 'node:path'
|
|
31
31
|
import { WebSocket } from 'ws'
|
|
32
32
|
import { getCliVersion } from '../../src/cli-version'
|
|
33
|
+
import { sleep } from '../../src/sleep'
|
|
33
34
|
import { IS_STANDALONE } from '../standalone'
|
|
34
35
|
|
|
35
36
|
interface ProfileOptions {
|
|
@@ -37,8 +38,6 @@ interface ProfileOptions {
|
|
|
37
38
|
verbose?: boolean
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
|
41
|
-
|
|
42
41
|
// compact CDP client with flatten-mode sessionId routing — one ws to the page
|
|
43
42
|
// target carries the page session plus every auto-attached worker session.
|
|
44
43
|
class Cdp {
|
|
@@ -495,6 +494,7 @@ export async function runCpuProfile(
|
|
|
495
494
|
const gate = new Promise((resolve) => {
|
|
496
495
|
releaseGate = resolve
|
|
497
496
|
})
|
|
497
|
+
Reflect.set(globalThis, '__sootsimCpuProfileCaptureComplete', false)
|
|
498
498
|
Reflect.set(globalThis, '__sootsimCpuProfileBarrierState', state)
|
|
499
499
|
Reflect.set(globalThis, '__sootsimCpuProfileBarrier', async (marker) => {
|
|
500
500
|
if (state.marker === null) {
|
|
@@ -714,14 +714,23 @@ export async function runCpuProfile(
|
|
|
714
714
|
)
|
|
715
715
|
// querying the page while five high-frequency profilers are still active
|
|
716
716
|
// can itself time out and lengthen the requested sampling interval. stop
|
|
717
|
-
// first, then read the already-recorded workload completion evidence.
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
717
|
+
// first, then read the already-recorded workload completion evidence. a
|
|
718
|
+
// failed read must not skip the profile/manifest writes below: the
|
|
719
|
+
// captured profiles are preserved and the manifest records the explicit
|
|
720
|
+
// evidence error, which already forces complete:false downstream.
|
|
721
|
+
let workloadEvidence: CpuProfileWorkloadEvidence | null = null
|
|
722
|
+
let workloadEvidenceError: string | null = null
|
|
723
|
+
try {
|
|
724
|
+
workloadEvidence = interactionBarrierEnabled
|
|
725
|
+
? readCpuProfileWorkloadEvidence(
|
|
726
|
+
await evaluatePage(
|
|
727
|
+
`Reflect.get(globalThis, '__sootsimInteractionPartialResult') ?? null`,
|
|
728
|
+
),
|
|
729
|
+
)
|
|
730
|
+
: null
|
|
731
|
+
} catch (error) {
|
|
732
|
+
workloadEvidenceError = error instanceof Error ? error.message : String(error)
|
|
733
|
+
}
|
|
725
734
|
const workloadActionEpochMs =
|
|
726
735
|
workloadEvidence?.captures.map((capture) => capture.actionEpochMs) ?? []
|
|
727
736
|
|
|
@@ -835,6 +844,7 @@ export async function runCpuProfile(
|
|
|
835
844
|
captureWindowEndedAt: new Date(captureWindowEndedAtEpochMs).toISOString(),
|
|
836
845
|
interactionBarrier,
|
|
837
846
|
workloadEvidence,
|
|
847
|
+
workloadEvidenceError,
|
|
838
848
|
workloadActionEpochMs,
|
|
839
849
|
manifest,
|
|
840
850
|
},
|
|
@@ -857,6 +867,9 @@ export async function runCpuProfile(
|
|
|
857
867
|
return complete ? 0 : 1
|
|
858
868
|
} finally {
|
|
859
869
|
if (interactionBarrierInstalled) {
|
|
870
|
+
await evaluatePage(
|
|
871
|
+
`Reflect.set(globalThis, '__sootsimCpuProfileCaptureComplete', true)`,
|
|
872
|
+
).catch(() => {})
|
|
860
873
|
await releaseInteractionBarrier('cleanup').catch(() => {})
|
|
861
874
|
}
|
|
862
875
|
cdp.close()
|
package/cli/commands/daemon.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
} from '../../src/home-paths'
|
|
37
37
|
import { rnxPublicBrand } from '../../src/public-brand'
|
|
38
38
|
import { printCommandHelp } from '../help'
|
|
39
|
+
import { escapeXml } from '../maestro-report'
|
|
39
40
|
import { rnxExit } from '../run-rnx'
|
|
40
41
|
import { rnxSelfInvocation, type RnxSelfInvocation } from '../self-invocation'
|
|
41
42
|
import { ensureDaemonApp } from './daemon-mac-app'
|
|
@@ -167,15 +168,6 @@ WantedBy=default.target
|
|
|
167
168
|
`
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
function escapeXml(value: string): string {
|
|
171
|
-
return value
|
|
172
|
-
.replace(/&/g, '&')
|
|
173
|
-
.replace(/</g, '<')
|
|
174
|
-
.replace(/>/g, '>')
|
|
175
|
-
.replace(/"/g, '"')
|
|
176
|
-
.replace(/'/g, ''')
|
|
177
|
-
}
|
|
178
|
-
|
|
179
171
|
function shellQuote(value: string): string {
|
|
180
172
|
return /[^\w@%+=:,./-]/.test(value) ? `'${value.replace(/'/g, `'\\''`)}'` : value
|
|
181
173
|
}
|
package/cli/commands/detox.ts
CHANGED
|
@@ -198,21 +198,28 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
|
|
|
198
198
|
jestArgs.push('--config', tempConfig)
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
|
|
201
|
+
// proof runs need the test count too: jest exits zero when a name filter
|
|
202
|
+
// skips every test, which would otherwise report an empty proof as passing.
|
|
203
|
+
const proofCapture =
|
|
204
|
+
process.env.CONFORMANCE_PROOF === '1' ||
|
|
205
|
+
!!process.env.CONFORMANCE_PROOF_DIR ||
|
|
206
|
+
process.env.CONFORMANCE_TARGET === 'sootsim'
|
|
207
|
+
|
|
208
|
+
// hosted run registration (org dashboard pass/fail row) also uses jest's
|
|
209
|
+
// structured results for test counts and the first failure.
|
|
204
210
|
const auth = resolveCliAuth()
|
|
205
211
|
const wantsRegistration =
|
|
206
212
|
!watch &&
|
|
207
213
|
(process.env.PEACH_REQUIRE_REGISTRATION === 'true' ||
|
|
208
214
|
shouldRegisterRun({ uploadedShare: false, auth }))
|
|
215
|
+
const needsResults = !watch && (proofCapture || wantsRegistration)
|
|
209
216
|
let resultsFile: string | null =
|
|
210
217
|
getFlag('--outputFile') || process.env.SOOTSIM_JEST_OUTPUT_FILE || null
|
|
211
|
-
if ((process.env.SOOTSIM_JEST_JSON === '1' ||
|
|
218
|
+
if ((process.env.SOOTSIM_JEST_JSON === '1' || needsResults) && !hasFlag('--json'))
|
|
212
219
|
jestArgs.push('--json')
|
|
213
220
|
if (!getFlag('--outputFile') && process.env.SOOTSIM_JEST_OUTPUT_FILE) {
|
|
214
221
|
jestArgs.push('--outputFile', process.env.SOOTSIM_JEST_OUTPUT_FILE)
|
|
215
|
-
} else if (!resultsFile &&
|
|
222
|
+
} else if (!resultsFile && needsResults) {
|
|
216
223
|
resultsFile = join(tmpdir(), `rnx-detox-results-${process.pid}.json`)
|
|
217
224
|
jestArgs.push('--outputFile', resultsFile)
|
|
218
225
|
}
|
|
@@ -235,10 +242,6 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
|
|
|
235
242
|
// then visits as tagged heap pointers. disable both Maglev and Sparkplug so
|
|
236
243
|
// execution stays on the stable Ignition + TurboFan pipeline under Node 24.
|
|
237
244
|
// normal user detox runs retain their node defaults and resolve jest through npx.
|
|
238
|
-
const proofCapture =
|
|
239
|
-
process.env.CONFORMANCE_PROOF === '1' ||
|
|
240
|
-
!!process.env.CONFORMANCE_PROOF_DIR ||
|
|
241
|
-
process.env.CONFORMANCE_TARGET === 'sootsim'
|
|
242
245
|
const command = proofCapture ? 'node' : 'npx'
|
|
243
246
|
const commandArgs = proofCapture
|
|
244
247
|
? [
|
|
@@ -294,16 +297,21 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
|
|
|
294
297
|
if (tempConfig) unlinkSync(tempConfig)
|
|
295
298
|
}
|
|
296
299
|
|
|
300
|
+
const results =
|
|
301
|
+
resultsFile && existsSync(resultsFile) && statSync(resultsFile).mtimeMs >= startedAt
|
|
302
|
+
? readJestResults(resultsFile)
|
|
303
|
+
: null
|
|
304
|
+
// a selection that skipped every test is not a passing suite. a proof run
|
|
305
|
+
// without structured results has no test-count verdict either.
|
|
306
|
+
if (code === 0 && results?.numPassedTests === 0) {
|
|
307
|
+
console.error(' error: no tests were selected or executed')
|
|
308
|
+
code = 1
|
|
309
|
+
} else if (code === 0 && proofCapture && !watch && !results) {
|
|
310
|
+
console.error(' error: jest did not report test results')
|
|
311
|
+
code = 1
|
|
312
|
+
}
|
|
313
|
+
|
|
297
314
|
if (wantsRegistration) {
|
|
298
|
-
const results =
|
|
299
|
-
resultsFile && existsSync(resultsFile) && statSync(resultsFile).mtimeMs >= startedAt
|
|
300
|
-
? readJestResults(resultsFile)
|
|
301
|
-
: null
|
|
302
|
-
// a selection that skipped every test is not a passing suite.
|
|
303
|
-
if (code === 0 && results && results.numPassedTests === 0) {
|
|
304
|
-
console.error(' error: no tests were selected or executed')
|
|
305
|
-
code = 1
|
|
306
|
-
}
|
|
307
315
|
const origin = await resolveDefaultUploadOrigin()
|
|
308
316
|
const run = await registerRun({
|
|
309
317
|
origin,
|
package/cli/commands/device.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
devices,
|
|
3
|
+
DUO_REST_PROGRESS,
|
|
3
4
|
getDevice,
|
|
4
5
|
listSelectableDeviceModels,
|
|
5
6
|
type DeviceModel,
|
|
7
|
+
type DuoFoldRest,
|
|
6
8
|
} from 'peach-engine/settings'
|
|
9
|
+
import { SOOTSIM_SHELL_COMMAND_NAME } from '../../src/menu'
|
|
7
10
|
import { rnxExit } from '../run-rnx'
|
|
8
11
|
import {
|
|
9
12
|
callInBridge,
|
|
10
13
|
createBridgeFromParsed,
|
|
14
|
+
evalInBridge,
|
|
11
15
|
parseBridgeCliArgs,
|
|
12
16
|
type ParsedBridgeCliArgs,
|
|
13
17
|
} from '../ws-bridge'
|
|
@@ -33,11 +37,14 @@ usage:
|
|
|
33
37
|
peach device get
|
|
34
38
|
peach device set <model> [--sim <id>]
|
|
35
39
|
peach device list
|
|
40
|
+
peach device posture [closed|book|open|laptop|tent|toggle] [--sim <id>]
|
|
36
41
|
|
|
37
42
|
examples:
|
|
38
43
|
peach device
|
|
39
44
|
peach device set iphone-16
|
|
40
45
|
peach device set iphone-16-pro-max --sim a7
|
|
46
|
+
peach device posture open
|
|
47
|
+
peach device posture laptop
|
|
41
48
|
`)
|
|
42
49
|
}
|
|
43
50
|
|
|
@@ -59,7 +66,16 @@ async function readCurrentDevice(
|
|
|
59
66
|
)) ?? { deviceModel: null }
|
|
60
67
|
const model = typeof settings.deviceModel === 'string' ? settings.deviceModel : null
|
|
61
68
|
if (!model || !(model in devices)) return null
|
|
62
|
-
const spec = getDevice(
|
|
69
|
+
const spec = getDevice(
|
|
70
|
+
model as DeviceModel,
|
|
71
|
+
settings.posture === 'open' || settings.posture === 'closed'
|
|
72
|
+
? settings.posture
|
|
73
|
+
: undefined,
|
|
74
|
+
settings.orientation === 'portrait' || settings.orientation === 'landscape'
|
|
75
|
+
? settings.orientation
|
|
76
|
+
: undefined,
|
|
77
|
+
settings.landscapeSide === 'right' ? 'right' : 'left',
|
|
78
|
+
)
|
|
63
79
|
return {
|
|
64
80
|
model,
|
|
65
81
|
width: spec.width,
|
|
@@ -72,6 +88,37 @@ async function readCurrentDevice(
|
|
|
72
88
|
}
|
|
73
89
|
}
|
|
74
90
|
|
|
91
|
+
function isDuoRest(value: unknown): value is DuoFoldRest {
|
|
92
|
+
return typeof value === 'string' && value in DUO_REST_PROGRESS
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const DUO_RESTS = Object.keys(DUO_REST_PROGRESS).filter(isDuoRest)
|
|
96
|
+
|
|
97
|
+
type CurrentPostureSnapshot = {
|
|
98
|
+
model: DeviceModel
|
|
99
|
+
rest: DuoFoldRest | null
|
|
100
|
+
} | null
|
|
101
|
+
|
|
102
|
+
// the binary display posture cannot name laptop, book, or tent. only the fold
|
|
103
|
+
// bus names the rest; it is null while a drag holds the hinge between rests.
|
|
104
|
+
async function readCurrentPosture(
|
|
105
|
+
parsed: ParsedBridgeCliArgs,
|
|
106
|
+
): Promise<CurrentPostureSnapshot> {
|
|
107
|
+
const bridge = createBridgeFromParsed(parsed)
|
|
108
|
+
try {
|
|
109
|
+
const settings = (await callInBridge<Record<string, unknown> | null>(
|
|
110
|
+
bridge,
|
|
111
|
+
'SootSim.bridges.settings.get',
|
|
112
|
+
)) ?? { deviceModel: null }
|
|
113
|
+
const model = typeof settings.deviceModel === 'string' ? settings.deviceModel : null
|
|
114
|
+
if (!model || !(model in devices)) return null
|
|
115
|
+
const rest = await evalInBridge(bridge, 'globalThis.__sootsimDuoFold?.rest ?? null')
|
|
116
|
+
return { model: model as DeviceModel, rest: isDuoRest(rest) ? rest : null }
|
|
117
|
+
} finally {
|
|
118
|
+
bridge.close()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
75
122
|
export async function runDeviceCommand(args: string[], opts: DeviceCommandOptions) {
|
|
76
123
|
if (args.includes('--help') || args.includes('-h')) {
|
|
77
124
|
usage()
|
|
@@ -128,6 +175,87 @@ export async function runDeviceCommand(args: string[], opts: DeviceCommandOption
|
|
|
128
175
|
return
|
|
129
176
|
}
|
|
130
177
|
|
|
178
|
+
if (subcommand === 'posture') {
|
|
179
|
+
const parsed = parseBridgeCliArgs(args, { port: opts.port })
|
|
180
|
+
// the want is optional, so read it past the bridge flags: raw args[1]
|
|
181
|
+
// would take --sim as the want on a bare `posture --sim <id>` read.
|
|
182
|
+
const raw = parsed.positional[1]
|
|
183
|
+
const want = raw === undefined || raw === 'toggle' || isDuoRest(raw) ? raw : null
|
|
184
|
+
if (want === null) {
|
|
185
|
+
console.error(` error: device posture expects ${DUO_RESTS.join(', ')}, or toggle`)
|
|
186
|
+
rnxExit(1)
|
|
187
|
+
}
|
|
188
|
+
const current = await readCurrentPosture(parsed)
|
|
189
|
+
if (!current) {
|
|
190
|
+
console.error(' error: could not read current posture from the target sim')
|
|
191
|
+
rnxExit(1)
|
|
192
|
+
}
|
|
193
|
+
if (!devices[current.model].supportedPostures) {
|
|
194
|
+
console.error(
|
|
195
|
+
` error: ${current.model} has a single posture — fold posture is only available on iphone-duo`,
|
|
196
|
+
)
|
|
197
|
+
rnxExit(1)
|
|
198
|
+
}
|
|
199
|
+
if (!want) {
|
|
200
|
+
if (!current.rest) {
|
|
201
|
+
console.error(
|
|
202
|
+
' error: the Duo fold has no current rest; choose a posture explicitly',
|
|
203
|
+
)
|
|
204
|
+
rnxExit(1)
|
|
205
|
+
}
|
|
206
|
+
console.log(` ${current.rest} (${current.model})`)
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
if (want === 'toggle' && !current.rest) {
|
|
210
|
+
console.error(
|
|
211
|
+
' error: the Duo fold has no current rest; choose a posture explicitly',
|
|
212
|
+
)
|
|
213
|
+
rnxExit(1)
|
|
214
|
+
}
|
|
215
|
+
const target: DuoFoldRest =
|
|
216
|
+
want === 'toggle' ? (current.rest === 'closed' ? 'open' : 'closed') : want
|
|
217
|
+
if (target === current.rest) {
|
|
218
|
+
console.log(` kept posture: ${current.rest} (${current.model})`)
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
const bridge = createBridgeFromParsed(parsed)
|
|
222
|
+
try {
|
|
223
|
+
// the typed command the duo's pose buttons dispatch: the fold engine
|
|
224
|
+
// sweeps the hinge there, turning the body for laptop and tent, so the
|
|
225
|
+
// app sees the same fold and reserved regions as from the rail. the
|
|
226
|
+
// command resolves once the fold bus reports the hinge resting there.
|
|
227
|
+
const settled = await evalInBridge<boolean>(
|
|
228
|
+
bridge,
|
|
229
|
+
`new Promise((resolve) => {
|
|
230
|
+
const deadline = Date.now() + 10000
|
|
231
|
+
const settled = () => {
|
|
232
|
+
const fold = globalThis.__sootsimDuoFold
|
|
233
|
+
return !!fold && fold.rest === ${JSON.stringify(target)} && !fold.active &&
|
|
234
|
+
Math.abs(fold.progress - ${DUO_REST_PROGRESS[target]}) < 1e-6
|
|
235
|
+
}
|
|
236
|
+
const check = () => {
|
|
237
|
+
if (settled()) resolve(true)
|
|
238
|
+
else if (Date.now() > deadline) resolve(false)
|
|
239
|
+
else setTimeout(check, 50)
|
|
240
|
+
}
|
|
241
|
+
window.dispatchEvent(new CustomEvent(${JSON.stringify(SOOTSIM_SHELL_COMMAND_NAME)}, { detail: { type: 'fold-to-rest', rest: ${JSON.stringify(target)} } }))
|
|
242
|
+
check()
|
|
243
|
+
})`,
|
|
244
|
+
{ acquireLock: true },
|
|
245
|
+
)
|
|
246
|
+
if (!settled) {
|
|
247
|
+
console.error(
|
|
248
|
+
` error: the fold did not come to rest at ${target} within 10s (${current.model})`,
|
|
249
|
+
)
|
|
250
|
+
rnxExit(1)
|
|
251
|
+
}
|
|
252
|
+
console.log(` set posture: ${target} (${current.model})`)
|
|
253
|
+
} finally {
|
|
254
|
+
bridge.close()
|
|
255
|
+
}
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
131
259
|
usage()
|
|
132
260
|
console.error(` unknown subcommand: ${subcommand}`)
|
|
133
261
|
rnxExit(1)
|