peachsim 0.1.1640 → 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 +5 -0
- package/cli/app-stubs.ts +38 -13
- 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 +24 -15
- package/cli/commands/inspect/actions.ts +3 -2
- package/cli/commands/inspect/core.ts +0 -8
- package/cli/commands/inspect.ts +0 -14
- package/cli/commands/platform.ts +2 -1
- package/cli/commands/record.ts +64 -28
- package/cli/commands/swift.ts +10 -2
- package/cli/commands/three-mode.ts +4 -38
- package/cli/commands/upload.ts +8 -8
- package/cli/flow-live-status.ts +0 -2
- package/cli/main.ts +1 -1
- package/cli/ws-bridge.ts +0 -6
- package/detox/attempt-events.cjs +41 -5
- package/detox/index.ts +155 -109
- package/detox/jest-environment.cjs +5 -1
- 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 +10 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/bundle-channel.cjs +2059 -1368
- 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 +572 -125
- 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 +1837 -1164
- package/dist-lib/expo-swift-metadata.cjs +1 -1
- package/dist-lib/expo-swift-metadata.mjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +2290 -1616
- 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 +44 -17
- 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 +33 -13
- package/dist-lib/native-app-identity-project.cjs +7 -4
- package/dist-lib/native-app-identity-project.mjs +7 -4
- package/dist-lib/native-app-identity.cjs +1 -1
- package/dist-lib/native-app-identity.mjs +1 -1
- 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 +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +4 -3
- package/dist-lib/sdk.mjs +4 -3
- package/dist-lib/skills.cjs +2823 -2109
- package/dist-lib/swift-compile.cjs +370 -239
- package/dist-lib/swift-compile.mjs +368 -230
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +8 -3
- package/dist-lib/swift-submit.mjs +8 -3
- package/dist-lib/swift-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1757 -1108
- package/dist-lib/vite.cjs +1 -1
- package/dist-lib/worklet-executable.cjs +1 -1
- package/dist-lib/worklet-executable.mjs +1 -1
- package/package.json +1 -1
- package/src/backend-origin.ts +8 -0
- package/src/react-native-bridge-modules.ts +89 -76
- package/src/swift-compile.ts +134 -107
- package/src/swift-distribution.lock.json +9 -8
- package/src/swift-host.ts +6 -4
- package/src/swift-objc-compile.ts +385 -193
- package/src/swift-objc-metadata.ts +204 -0
- package/src/swift-objc-sdk.ts +195 -2
- package/src/swift-package-manifest.ts +6 -3
- package/src/swift-root-lock.ts +153 -0
- package/src/swift-submit.ts +15 -2
- package/src/swiftContract.ts +12 -0
- package/src/xcode-project.ts +10 -3
package/README.md
CHANGED
|
@@ -656,6 +656,11 @@ peach screenshot --with-frame --output /tmp/peach-framed.png
|
|
|
656
656
|
peach record --duration 5 --output /tmp/peach-anim.mp4
|
|
657
657
|
```
|
|
658
658
|
|
|
659
|
+
`peach record status` queries the recorder on the tracked simulator. A video
|
|
660
|
+
whose stop reply timed out remains available: status reports processing or
|
|
661
|
+
ready to save, and another `peach record stop --output <path>` joins the same
|
|
662
|
+
finalization and saves its bytes. `peach record cancel` discards them.
|
|
663
|
+
|
|
659
664
|
`peach screenshot --with-frame` composes the real Peach shell device chrome
|
|
660
665
|
around the raw screen bitmap (reusing the shell bezel/button geometry, excluding
|
|
661
666
|
the Electron top bar, rail gutter, and other window chrome). Flows can request
|
package/cli/app-stubs.ts
CHANGED
|
@@ -381,23 +381,39 @@ export function packageNativeRequests(dir: string): NativeRequest[] {
|
|
|
381
381
|
)
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
-
// the
|
|
385
|
-
//
|
|
386
|
-
//
|
|
384
|
+
// resolve the installed runtime dependency graph, including transitive native
|
|
385
|
+
// packages. workspace source is scanned as app code instead. a transitive
|
|
386
|
+
// package is scanned only when it ships native code: the graph runs to
|
|
387
|
+
// thousands of packages, and the rest name nothing a direct dependency does not.
|
|
387
388
|
function dependencyNativeOwners(
|
|
388
389
|
appDir: string,
|
|
389
390
|
manifest: ProjectManifest | null,
|
|
390
391
|
): Map<string, string> {
|
|
391
392
|
const owners = new Map<string, string>()
|
|
392
|
-
|
|
393
|
+
const pending = Object.keys({
|
|
393
394
|
...manifest?.devDependencies,
|
|
394
395
|
...manifest?.dependencies,
|
|
395
|
-
})) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
396
|
+
}).map((name) => ({ from: appDir, name }))
|
|
397
|
+
const visited = new Set<string>()
|
|
398
|
+
for (let index = 0; index < pending.length; index++) {
|
|
399
|
+
const { from, name } = pending[index]
|
|
400
|
+
const dir = installedPackageDir(from, name)
|
|
401
|
+
if (!dir || visited.has(dir)) continue
|
|
402
|
+
visited.add(dir)
|
|
403
|
+
const dependency = readProjectManifest(dir)
|
|
404
|
+
const optional = readPackageJson(dir)?.optionalDependencies
|
|
405
|
+
if (
|
|
406
|
+
dir.split('/').includes('node_modules') &&
|
|
407
|
+
(from === appDir || hasNativeCode(dir, 'ios'))
|
|
408
|
+
)
|
|
409
|
+
for (const request of packageNativeRequests(dir))
|
|
410
|
+
if (!owners.has(`${request.kind}:${request.name}`))
|
|
411
|
+
owners.set(`${request.kind}:${request.name}`, dependency?.name ?? name)
|
|
412
|
+
for (const child of Object.keys({
|
|
413
|
+
...dependency?.dependencies,
|
|
414
|
+
...(isRecord(optional) ? optional : {}),
|
|
415
|
+
}))
|
|
416
|
+
pending.push({ from: dir, name: child })
|
|
401
417
|
}
|
|
402
418
|
return owners
|
|
403
419
|
}
|
|
@@ -595,7 +611,12 @@ export function scanLocalNative(
|
|
|
595
611
|
dir,
|
|
596
612
|
scanRepository(dir).directories.flatMap((sub) =>
|
|
597
613
|
listDir(sub)
|
|
598
|
-
.filter(
|
|
614
|
+
.filter(
|
|
615
|
+
(name) =>
|
|
616
|
+
NATIVE_SOURCE_FILE.test(name) &&
|
|
617
|
+
(!platform ||
|
|
618
|
+
(platform === 'ios' ? name.endsWith('.swift') : name.endsWith('.kt'))),
|
|
619
|
+
)
|
|
599
620
|
.map((name) => join(sub, name)),
|
|
600
621
|
),
|
|
601
622
|
]),
|
|
@@ -633,8 +654,12 @@ export function scanLocalNative(
|
|
|
633
654
|
path: `${relative(appDir, file)}:${line}`,
|
|
634
655
|
expoModule: moduleDir ? relative(appDir, moduleDir) || '.' : null,
|
|
635
656
|
swift: moduleDir
|
|
636
|
-
? (nativeFilesByModule.get(moduleDir) ?? []).some(
|
|
637
|
-
path
|
|
657
|
+
? (nativeFilesByModule.get(moduleDir) ?? []).some(
|
|
658
|
+
(path) =>
|
|
659
|
+
path.endsWith('.swift') &&
|
|
660
|
+
[...(readSmallFile(path) ?? '').matchAll(/\bName\(\s*"(\w+)"\s*\)/g)].some(
|
|
661
|
+
(match) => match[1] === name,
|
|
662
|
+
),
|
|
638
663
|
)
|
|
639
664
|
: false,
|
|
640
665
|
answered: answered[kind].has(name),
|
package/cli/cloud-session.ts
CHANGED
|
@@ -60,6 +60,7 @@ export interface CloudBoxSession {
|
|
|
60
60
|
platform?: RemotePlatform
|
|
61
61
|
device?: string
|
|
62
62
|
artifact?: CloudSessionArtifactIdentity
|
|
63
|
+
appFontsWireSha256?: string
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
export function normalizeCloudOrigin(source: string): string {
|
|
@@ -128,7 +129,13 @@ export function createCloudBoxSession(input: {
|
|
|
128
129
|
platform?: RemotePlatform
|
|
129
130
|
device?: string
|
|
130
131
|
artifact?: CloudSessionArtifactIdentity
|
|
132
|
+
appFontsWireSha256?: string
|
|
131
133
|
}): CloudBoxSession {
|
|
134
|
+
if (
|
|
135
|
+
input.appFontsWireSha256 !== undefined &&
|
|
136
|
+
!/^[a-f0-9]{64}$/.test(input.appFontsWireSha256)
|
|
137
|
+
)
|
|
138
|
+
throw new Error('cloud box session has an invalid app-font wire digest')
|
|
132
139
|
const session: CloudBoxSession = {
|
|
133
140
|
version: CLOUD_SESSION_VERSION,
|
|
134
141
|
service: 'box',
|
|
@@ -138,6 +145,9 @@ export function createCloudBoxSession(input: {
|
|
|
138
145
|
...(input.platform ? { platform: input.platform } : {}),
|
|
139
146
|
...(input.device?.trim() ? { device: input.device.trim() } : {}),
|
|
140
147
|
...(input.artifact ? { artifact: input.artifact } : {}),
|
|
148
|
+
...(input.appFontsWireSha256 !== undefined
|
|
149
|
+
? { appFontsWireSha256: input.appFontsWireSha256 }
|
|
150
|
+
: {}),
|
|
141
151
|
}
|
|
142
152
|
if (!session.boxId || !session.boxToken) {
|
|
143
153
|
throw new Error('cloud box session requires a box, an origin, and a box token')
|
|
@@ -198,6 +208,11 @@ export function parseCloudSession(encoded: string): CloudSession {
|
|
|
198
208
|
try {
|
|
199
209
|
if (parsed.service === 'box') {
|
|
200
210
|
if (!boxId || !boxToken) throw new Error('unreadable cloud box session')
|
|
211
|
+
if (
|
|
212
|
+
parsed.appFontsWireSha256 !== undefined &&
|
|
213
|
+
typeof parsed.appFontsWireSha256 !== 'string'
|
|
214
|
+
)
|
|
215
|
+
throw new Error('unreadable app-font wire digest')
|
|
201
216
|
return createCloudBoxSession({
|
|
202
217
|
boxId,
|
|
203
218
|
boxToken,
|
|
@@ -205,6 +220,7 @@ export function parseCloudSession(encoded: string): CloudSession {
|
|
|
205
220
|
platform,
|
|
206
221
|
device,
|
|
207
222
|
artifact,
|
|
223
|
+
appFontsWireSha256: parsed.appFontsWireSha256,
|
|
208
224
|
})
|
|
209
225
|
}
|
|
210
226
|
const simId = parsed.simId
|
package/cli/commands/assert.ts
CHANGED
|
@@ -162,11 +162,14 @@ interface VerbResult {
|
|
|
162
162
|
reason?: string
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
async function runVerbOnce(
|
|
165
|
+
async function runVerbOnce(
|
|
166
|
+
verbArgs: string[],
|
|
167
|
+
globalArgs: string[],
|
|
168
|
+
): Promise<VerbResult> {
|
|
166
169
|
// re-invoke self through the real CLI entry, so every code path the user's
|
|
167
170
|
// verb would take, assert takes too.
|
|
168
171
|
const { executable, prefixArgs } = rnxSelfInvocation()
|
|
169
|
-
const args = [...prefixArgs, ...verbArgs, '--json']
|
|
172
|
+
const args = [...prefixArgs, ...globalArgs, ...verbArgs, '--json']
|
|
170
173
|
|
|
171
174
|
return new Promise<VerbResult>((resolve) => {
|
|
172
175
|
const proc = spawn(executable, args, {
|
|
@@ -402,7 +405,10 @@ function runJq(expr: string, payload: unknown): Promise<string> {
|
|
|
402
405
|
})
|
|
403
406
|
}
|
|
404
407
|
|
|
405
|
-
export async function runAssert(
|
|
408
|
+
export async function runAssert(
|
|
409
|
+
rawArgs: string[],
|
|
410
|
+
target: { port?: number; sim?: string } = {},
|
|
411
|
+
): Promise<void> {
|
|
406
412
|
if (rawArgs[0] === '--help' || rawArgs[0] === '-h' || rawArgs.length === 0) {
|
|
407
413
|
printHelp()
|
|
408
414
|
peachExit(rawArgs.length === 0 ? 2 : 0)
|
|
@@ -430,6 +436,13 @@ export async function runAssert(rawArgs: string[]): Promise<void> {
|
|
|
430
436
|
const effectivePredicates =
|
|
431
437
|
predicates.length > 0 ? predicates : [{ kind: '--exists', args: [] }]
|
|
432
438
|
|
|
439
|
+
// the child is a fresh CLI process: without the caller's explicit port and
|
|
440
|
+
// sim it falls back to the saved target.
|
|
441
|
+
const globalArgs = [
|
|
442
|
+
...(target.port != null ? ['--port', String(target.port)] : []),
|
|
443
|
+
...(target.sim ? ['--sim', target.sim] : []),
|
|
444
|
+
]
|
|
445
|
+
|
|
433
446
|
const deadline = withinMs > 0 ? Date.now() + withinMs : 0
|
|
434
447
|
let attempts = 0
|
|
435
448
|
let lastFailReason = ''
|
|
@@ -437,7 +450,7 @@ export async function runAssert(rawArgs: string[]): Promise<void> {
|
|
|
437
450
|
|
|
438
451
|
while (true) {
|
|
439
452
|
attempts++
|
|
440
|
-
const verbResult = await runVerbOnce(verbArgs)
|
|
453
|
+
const verbResult = await runVerbOnce(verbArgs, globalArgs)
|
|
441
454
|
if (!verbResult.ok) {
|
|
442
455
|
lastVerbSkipReason = verbResult.reason || 'verb failed'
|
|
443
456
|
if (Date.now() < deadline) {
|
package/cli/commands/box.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// name on the command line, else the one `peach box use` remembered. see
|
|
11
11
|
// boxTarget below.
|
|
12
12
|
|
|
13
|
+
import { createHash } from 'node:crypto'
|
|
13
14
|
import fs from 'node:fs'
|
|
14
15
|
import path from 'node:path'
|
|
15
16
|
import { createInterface } from 'node:readline'
|
|
@@ -536,7 +537,13 @@ export async function createPrebuiltBundleBox(options: {
|
|
|
536
537
|
const nativeRenderer = options.nativeProjectDir
|
|
537
538
|
? readNativeProjectRenderer(options.nativeProjectDir, platform)
|
|
538
539
|
: undefined
|
|
539
|
-
|
|
540
|
+
const appFonts = options.nativeProjectDir
|
|
541
|
+
? readNativeLinkedAppFontsWire({
|
|
542
|
+
projectDir: options.nativeProjectDir,
|
|
543
|
+
platform,
|
|
544
|
+
})
|
|
545
|
+
: ''
|
|
546
|
+
const box = await cloudBoxClient(options).create({
|
|
540
547
|
name: options.name,
|
|
541
548
|
stack: prebuiltBundleBoxTemplate.stack,
|
|
542
549
|
template: BOX_TEMPLATES.resolve({ name: prebuiltBundleBoxTemplate.name }).provenance,
|
|
@@ -544,16 +551,17 @@ export async function createPrebuiltBundleBox(options: {
|
|
|
544
551
|
options.bundle,
|
|
545
552
|
options.device,
|
|
546
553
|
options.initialUrl ?? null,
|
|
547
|
-
|
|
548
|
-
? readNativeLinkedAppFontsWire({
|
|
549
|
-
projectDir: options.nativeProjectDir,
|
|
550
|
-
platform,
|
|
551
|
-
})
|
|
552
|
-
: '',
|
|
554
|
+
appFonts,
|
|
553
555
|
nativeRenderer,
|
|
554
556
|
),
|
|
555
557
|
...(options.secrets ? { secrets: options.secrets } : {}),
|
|
556
558
|
})
|
|
559
|
+
return {
|
|
560
|
+
box,
|
|
561
|
+
appFontsWireSha256: box.existing
|
|
562
|
+
? undefined
|
|
563
|
+
: createHash('sha256').update(appFonts).digest('hex'),
|
|
564
|
+
}
|
|
557
565
|
}
|
|
558
566
|
|
|
559
567
|
// a plain .env file: `NAME=value` lines, an optional `export `, optional
|
|
@@ -698,7 +706,7 @@ async function runCloudBoxCreate(
|
|
|
698
706
|
}
|
|
699
707
|
let session: BoxShellSession | null = null
|
|
700
708
|
try {
|
|
701
|
-
const
|
|
709
|
+
const created =
|
|
702
710
|
'bundle' in source
|
|
703
711
|
? await createPrebuiltBundleBox({
|
|
704
712
|
...connection,
|
|
@@ -708,21 +716,27 @@ async function runCloudBoxCreate(
|
|
|
708
716
|
name,
|
|
709
717
|
...(secrets ? { secrets } : {}),
|
|
710
718
|
})
|
|
711
|
-
:
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
719
|
+
: {
|
|
720
|
+
box: await cloudBoxClient(connection).create({
|
|
721
|
+
name,
|
|
722
|
+
stack: template.stack,
|
|
723
|
+
template: template.provenance,
|
|
724
|
+
source: source.checkout,
|
|
725
|
+
...(secrets ? { secrets } : {}),
|
|
726
|
+
onProgress:
|
|
727
|
+
opts.verbose && !json
|
|
728
|
+
? (seeded, total) => console.log(` ...seeded ${seeded}/${total} files`)
|
|
729
|
+
: undefined,
|
|
730
|
+
}),
|
|
731
|
+
}
|
|
732
|
+
const { box } = created
|
|
722
733
|
if (json) {
|
|
723
734
|
console.log(
|
|
724
735
|
JSON.stringify({
|
|
725
736
|
...box.description,
|
|
737
|
+
...('appFontsWireSha256' in created
|
|
738
|
+
? { appFontsWireSha256: created.appFontsWireSha256 }
|
|
739
|
+
: {}),
|
|
726
740
|
...(box.accessToken ? { accessToken: box.accessToken } : {}),
|
|
727
741
|
}),
|
|
728
742
|
)
|
package/cli/commands/doctor.ts
CHANGED
|
@@ -373,9 +373,8 @@ function printVersions(findings: DoctorFindings): void {
|
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
// one line: there is nothing to check in them.
|
|
376
|
+
// packages appear worst first with the published implementation percent and
|
|
377
|
+
// complete capability details. javascript-only packages sit last on one line.
|
|
379
378
|
function printPackages(
|
|
380
379
|
packages: readonly PackageResult[],
|
|
381
380
|
verbose: boolean,
|
|
@@ -411,10 +410,26 @@ function printPackages(
|
|
|
411
410
|
const columns = process.stdout.columns || 120
|
|
412
411
|
for (const row of rows) {
|
|
413
412
|
const percent = percentLabel(row.item)
|
|
414
|
-
const detail =
|
|
413
|
+
const detail = row.detail
|
|
415
414
|
console.log(
|
|
416
415
|
` ${row.glyph} ${row.item.name.padEnd(nameWidth)}${muted(row.item.version.padEnd(versionWidth))}${percent.padStart(5)}${detail ? ` ${row.tone(detail)}` : ''}`,
|
|
417
416
|
)
|
|
417
|
+
if (!answering.has(row.item.name)) {
|
|
418
|
+
for (const [label, text] of [
|
|
419
|
+
['working', row.item.rnx.working],
|
|
420
|
+
['missing', row.item.rnx.missing],
|
|
421
|
+
]) {
|
|
422
|
+
if (!text) continue
|
|
423
|
+
let line = `${label}:`
|
|
424
|
+
for (const word of text.split(/\s+/)) {
|
|
425
|
+
if (line.length + word.length + 1 > columns - 6) {
|
|
426
|
+
console.log(` ${muted(line)}`)
|
|
427
|
+
line = word
|
|
428
|
+
} else line += ` ${word}`
|
|
429
|
+
}
|
|
430
|
+
console.log(` ${muted(line)}`)
|
|
431
|
+
}
|
|
432
|
+
}
|
|
418
433
|
if (verbose && row.item.note) console.log(` ${muted(row.item.note)}`)
|
|
419
434
|
}
|
|
420
435
|
if (javascriptOnly.length > 0) {
|
|
@@ -517,22 +532,12 @@ function packageRow(
|
|
|
517
532
|
return {
|
|
518
533
|
rank: RANK.partial,
|
|
519
534
|
glyph: warning('◐'),
|
|
520
|
-
detail:
|
|
535
|
+
detail: '',
|
|
521
536
|
tone: muted,
|
|
522
537
|
}
|
|
523
538
|
return { rank: RANK.supported, glyph: success('✓'), detail: '', tone: muted }
|
|
524
539
|
}
|
|
525
540
|
|
|
526
|
-
function firstClause(text: string): string {
|
|
527
|
-
const end = text.search(/[.;,](\s|$)/)
|
|
528
|
-
return end > 0 ? text.slice(0, end) : text
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// a detail ends at the terminal edge rather than wrapping under the columns
|
|
532
|
-
function fit(text: string, width: number): string {
|
|
533
|
-
return text.length > width ? `${text.slice(0, Math.max(0, width - 1))}…` : text
|
|
534
|
-
}
|
|
535
|
-
|
|
536
541
|
function printNativeGuidance(
|
|
537
542
|
unknownNative: readonly UnknownNativePackage[],
|
|
538
543
|
local: readonly LocalNative[],
|
|
@@ -577,6 +582,10 @@ function printNativeGuidance(
|
|
|
577
582
|
}
|
|
578
583
|
if (open.size) {
|
|
579
584
|
console.log(`\n stub them in peach.config.ts, by package:\n`)
|
|
585
|
+
console.log(
|
|
586
|
+
muted(` import { defineConfig } from '${rnxPublicBrand.packageName}/config'`),
|
|
587
|
+
)
|
|
588
|
+
console.log()
|
|
580
589
|
console.log(muted(` export default defineConfig({`))
|
|
581
590
|
console.log(muted(` packages: {`))
|
|
582
591
|
for (const [owner, requestsByName] of open) {
|
|
@@ -380,9 +380,10 @@ async function resolveTextTapTarget(
|
|
|
380
380
|
query: string,
|
|
381
381
|
options: TapTextOptions = {},
|
|
382
382
|
) {
|
|
383
|
+
const needle = options.exact ? query : query.trim().toLowerCase()
|
|
384
|
+
if (!needle) return { matched: 0, total: 0 }
|
|
383
385
|
const { tree } = await inspectTree(bridge, 50)
|
|
384
386
|
if (!Array.isArray(tree)) return { matched: 0, total: 0 }
|
|
385
|
-
const needle = options.exact ? query : query.toLowerCase()
|
|
386
387
|
let candidates = collectTextCandidates(tree).filter(
|
|
387
388
|
({ node, ancestorTestIDs, role }) => {
|
|
388
389
|
const text = nodeText(node)
|
|
@@ -411,7 +412,7 @@ async function resolveTextTapTarget(
|
|
|
411
412
|
if (actionable.length > 0) candidates = actionable
|
|
412
413
|
if (!options.exact) {
|
|
413
414
|
const whole = candidates.filter(
|
|
414
|
-
({ node }) => nodeText(node).trim().toLowerCase() === needle
|
|
415
|
+
({ node }) => nodeText(node).trim().toLowerCase() === needle,
|
|
415
416
|
)
|
|
416
417
|
if (whole.length > 0) candidates = whole
|
|
417
418
|
}
|
|
@@ -556,12 +556,6 @@ export interface LayoutElement {
|
|
|
556
556
|
ellipsisActive?: boolean
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
-
export interface LayoutResult {
|
|
560
|
-
preview: 'web' | 'native'
|
|
561
|
-
count: number
|
|
562
|
-
elements: LayoutElement[]
|
|
563
|
-
}
|
|
564
|
-
|
|
565
559
|
// in-page extractor for the native preview. mirrors the visibility + device
|
|
566
560
|
// sizing the a11y / wait-ready kernels use so "what's on screen" agrees across
|
|
567
561
|
// the inspect surface. returns LayoutElement[] (or [] when the bridge is cold).
|
|
@@ -2013,8 +2007,6 @@ export const DEBUG_CHANNELS = [
|
|
|
2013
2007
|
'yoga',
|
|
2014
2008
|
] as const
|
|
2015
2009
|
|
|
2016
|
-
export type DebugChannel = (typeof DEBUG_CHANNELS)[number]
|
|
2017
|
-
|
|
2018
2010
|
// `debug status` — which debug channels are currently enabled + their counts.
|
|
2019
2011
|
export async function inspectDebugStatus(bridge: InspectBridge): Promise<unknown> {
|
|
2020
2012
|
return bridge.send({ type: 'evaluate', code: 'window.__sootsimDebug.status()' })
|
package/cli/commands/inspect.ts
CHANGED
|
@@ -971,20 +971,6 @@ function keyCodeToVisualKey(code: string) {
|
|
|
971
971
|
return null
|
|
972
972
|
}
|
|
973
973
|
|
|
974
|
-
function getInspectCommandName(subcommand: string, directSubcommand: string | null) {
|
|
975
|
-
if (directSubcommand === subcommand) return `peach ${subcommand}`
|
|
976
|
-
const group = getCliInspectVerbMeta(subcommand)?.group
|
|
977
|
-
return group ? `peach ${group} ${subcommand}` : `peach ${subcommand}`
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
function getInspectUsage(
|
|
981
|
-
subcommand: string,
|
|
982
|
-
tail: string,
|
|
983
|
-
directSubcommand: string | null,
|
|
984
|
-
) {
|
|
985
|
-
return ` usage: ${getInspectCommandName(subcommand, directSubcommand)}${tail ? ` ${tail}` : ''}`
|
|
986
|
-
}
|
|
987
|
-
|
|
988
974
|
function normalizeRecordedText(value: unknown) {
|
|
989
975
|
if (typeof value !== 'string') return null
|
|
990
976
|
const text = value.replace(/\s+/g, ' ').trim()
|
package/cli/commands/platform.ts
CHANGED
|
@@ -528,7 +528,7 @@ async function runRemoteBox(
|
|
|
528
528
|
} else if (existingSession) {
|
|
529
529
|
await closeCloudSession(existingSession)
|
|
530
530
|
}
|
|
531
|
-
const created = await box.createPrebuiltBundleBox({
|
|
531
|
+
const { box: created, appFontsWireSha256 } = await box.createPrebuiltBundleBox({
|
|
532
532
|
bundle: new Uint8Array(artifact.bytes),
|
|
533
533
|
nativeProjectDir: input.nativeProjectDir,
|
|
534
534
|
device: selectedDevice,
|
|
@@ -546,6 +546,7 @@ async function runRemoteBox(
|
|
|
546
546
|
apiOrigin: endpoint,
|
|
547
547
|
platform,
|
|
548
548
|
device: selectedDevice,
|
|
549
|
+
appFontsWireSha256,
|
|
549
550
|
artifact: {
|
|
550
551
|
sha256: artifactSha256,
|
|
551
552
|
bytes: artifact.bytes.length,
|
package/cli/commands/record.ts
CHANGED
|
@@ -168,6 +168,7 @@ const RECORD_VALUE_FLAGS = new Set([
|
|
|
168
168
|
'--repo',
|
|
169
169
|
'--scene',
|
|
170
170
|
'--shell-url',
|
|
171
|
+
'--timeout',
|
|
171
172
|
])
|
|
172
173
|
const RECORD_SUBCOMMANDS = new Set([
|
|
173
174
|
'start',
|
|
@@ -924,8 +925,6 @@ async function recordStart(args: string[], opts: RecordOptions) {
|
|
|
924
925
|
peachExit(1)
|
|
925
926
|
}
|
|
926
927
|
} else {
|
|
927
|
-
await assertRecorderAvailable(bridge)
|
|
928
|
-
await assertTabVisibleForCapture(bridge)
|
|
929
928
|
const lockstep = args.includes('--lockstep')
|
|
930
929
|
if (lockstep && format !== 'mp4') {
|
|
931
930
|
console.error(' --lockstep requires --format mp4 (webm is realtime-clocked)')
|
|
@@ -942,7 +941,11 @@ async function recordStart(args: string[], opts: RecordOptions) {
|
|
|
942
941
|
writeRecordingState(session)
|
|
943
942
|
const result: { ok: boolean; error?: string } = await bridge.send({
|
|
944
943
|
type: 'evaluate',
|
|
945
|
-
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
|
+
})()`,
|
|
946
949
|
})
|
|
947
950
|
if (!result.ok) {
|
|
948
951
|
clearRecordingState()
|
|
@@ -1022,7 +1025,13 @@ async function recordPreludeStart(args: string[], opts: RecordOptions) {
|
|
|
1022
1025
|
// - the event recorder alone drives hidden PR-preview preludes before a
|
|
1023
1026
|
// visible live/combined segment exists
|
|
1024
1027
|
// returns null when the engine can't be reached.
|
|
1025
|
-
type EngineRecordingState =
|
|
1028
|
+
type EngineRecordingState =
|
|
1029
|
+
| 'idle'
|
|
1030
|
+
| 'starting'
|
|
1031
|
+
| 'recording'
|
|
1032
|
+
| 'processing'
|
|
1033
|
+
| 'ready'
|
|
1034
|
+
| 'prelude'
|
|
1026
1035
|
|
|
1027
1036
|
async function probeEngineRecordingState(
|
|
1028
1037
|
bridge: WsBridge,
|
|
@@ -1043,8 +1052,10 @@ async function probeEngineRecordingState(
|
|
|
1043
1052
|
})()`,
|
|
1044
1053
|
})
|
|
1045
1054
|
if (!snap) return null
|
|
1055
|
+
if (snap.headless === 'starting') return 'starting'
|
|
1046
1056
|
if (snap.store === 'recording' || snap.headless === 'recording') return 'recording'
|
|
1047
|
-
if (snap.store === 'processing') return 'processing'
|
|
1057
|
+
if (snap.store === 'processing' || snap.headless === 'processing') return 'processing'
|
|
1058
|
+
if (snap.headless === 'ready') return 'ready'
|
|
1048
1059
|
if (snap.eventRecording) return 'prelude'
|
|
1049
1060
|
if (snap.store === 'idle' || snap.headless === 'idle') return 'idle'
|
|
1050
1061
|
return null
|
|
@@ -1089,26 +1100,36 @@ async function cancelEngineRecording(bridge: WsBridge): Promise<{
|
|
|
1089
1100
|
|
|
1090
1101
|
async function recordStatus(args: string[], opts: RecordOptions) {
|
|
1091
1102
|
const state = readRecordingState()
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
)
|
|
1102
|
-
}
|
|
1103
|
-
return
|
|
1104
|
-
}
|
|
1105
|
-
// no `record start` session — but an atomic run or the rail button may
|
|
1106
|
-
// still be recording. ask the engine before claiming nothing is happening.
|
|
1107
|
-
const parsed = parseBridgeCliArgs(args, { port: opts.port, commandTimeoutMs: 8_000 })
|
|
1108
|
-
const bridge = createBridgeFromParsed(parsed)
|
|
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
|
+
})
|
|
1109
1112
|
try {
|
|
1110
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
|
+
}
|
|
1111
1131
|
if (
|
|
1132
|
+
engineState === 'starting' ||
|
|
1112
1133
|
engineState === 'recording' ||
|
|
1113
1134
|
engineState === 'processing' ||
|
|
1114
1135
|
engineState === 'prelude'
|
|
@@ -1154,8 +1175,10 @@ async function recordCancel(args: string[], opts: RecordOptions) {
|
|
|
1154
1175
|
try {
|
|
1155
1176
|
const engineState = await probeEngineRecordingState(probeBridge)
|
|
1156
1177
|
if (
|
|
1178
|
+
engineState === 'starting' ||
|
|
1157
1179
|
engineState === 'recording' ||
|
|
1158
1180
|
engineState === 'processing' ||
|
|
1181
|
+
engineState === 'ready' ||
|
|
1159
1182
|
engineState === 'prelude'
|
|
1160
1183
|
) {
|
|
1161
1184
|
// `record cancel` is explicit intent: the caller wants whatever is
|
|
@@ -1204,7 +1227,11 @@ async function recordCancel(args: string[], opts: RecordOptions) {
|
|
|
1204
1227
|
} else {
|
|
1205
1228
|
await bridge.send({
|
|
1206
1229
|
type: 'evaluate',
|
|
1207
|
-
code: `
|
|
1230
|
+
code: `(async () => {
|
|
1231
|
+
const recorder = window.__sootsimRecorder;
|
|
1232
|
+
await recorder.stop();
|
|
1233
|
+
return recorder.forceRelease();
|
|
1234
|
+
})()`,
|
|
1208
1235
|
})
|
|
1209
1236
|
}
|
|
1210
1237
|
} catch {
|
|
@@ -1241,7 +1268,15 @@ async function recordStop(args: string[], opts: RecordOptions) {
|
|
|
1241
1268
|
const bridge = createBridgeFromParsed({ ...parsed, simId })
|
|
1242
1269
|
try {
|
|
1243
1270
|
if (state.mode === 'live' || state.mode === 'combined') {
|
|
1244
|
-
|
|
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) {
|
|
1245
1280
|
clearRecordingState()
|
|
1246
1281
|
console.error(
|
|
1247
1282
|
` stop failed: the ${state.mode} start still in flight never began a take, so there is nothing to upload.\n` +
|
|
@@ -1444,13 +1479,14 @@ async function evalStoreStart(
|
|
|
1444
1479
|
|
|
1445
1480
|
// the engine stop waits for a start still in flight. false means that start
|
|
1446
1481
|
// never began a take, so no upload will come and the store's last one belongs
|
|
1447
|
-
// to an earlier take.
|
|
1448
|
-
|
|
1449
|
-
|
|
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({
|
|
1450
1486
|
type: 'evaluate',
|
|
1451
1487
|
code: `window.SootSim.bridges.stopRecording()`,
|
|
1452
1488
|
})
|
|
1453
|
-
return stopped !== false
|
|
1489
|
+
return stopped === 'pending' ? stopped : stopped !== false
|
|
1454
1490
|
}
|
|
1455
1491
|
|
|
1456
1492
|
interface StoreUploadResult {
|
package/cli/commands/swift.ts
CHANGED
|
@@ -165,7 +165,15 @@ export async function runSwift(
|
|
|
165
165
|
const appProject = resolveAppProject(packagePath)
|
|
166
166
|
host.configureSwiftStandinBehavior(packagePath, await loadRNXAppConfig(appProject))
|
|
167
167
|
// collection preflights every input path before any source bytes are read.
|
|
168
|
-
|
|
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
|
+
}
|
|
169
177
|
const packageName = path.basename(packagePath)
|
|
170
178
|
const appInfo = host.swiftProjectAppInfo(packagePath)
|
|
171
179
|
const guestSource = swiftGuestBundle(packageName)
|
|
@@ -354,7 +362,7 @@ export async function runSwift(
|
|
|
354
362
|
await refresh()
|
|
355
363
|
controller.signal.throwIfAborted()
|
|
356
364
|
// refresh already printed the error
|
|
357
|
-
if (!metadata || compileError)
|
|
365
|
+
if (!metadata || compileError) return 1
|
|
358
366
|
if (opts.device) openArgs.push('--device', opts.device)
|
|
359
367
|
if (opts.driver) openArgs.push('--driver', opts.driver)
|
|
360
368
|
if (opts.headless) openArgs.push('--headless')
|