peachsim 0.1.1625 → 0.1.1635
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 +10 -1
- package/cli/app-config.ts +4 -1
- package/cli/app-fonts.ts +37 -17
- package/cli/app-stubs.ts +224 -9
- package/cli/bridge-flow-runner.ts +64 -43
- package/cli/command-registry.ts +1 -0
- package/cli/commands/control.ts +283 -49
- package/cli/commands/doctor.ts +17 -184
- package/cli/commands/flow.ts +79 -11
- package/cli/commands/inspect/core.ts +10 -5
- package/cli/commands/inspect/settling.ts +14 -2
- package/cli/commands/inspect/wait-ready.ts +12 -0
- package/cli/commands/inspect.ts +26 -3
- package/cli/commands/install-desktop.ts +1 -0
- package/cli/commands/maestro-compare.ts +97 -38
- package/cli/commands/maestro.ts +9 -1
- package/cli/commands/record.ts +21 -1
- package/cli/commands/screenshot.ts +23 -1
- package/cli/commands/serve.ts +7 -2
- package/cli/commands/setup.ts +19 -42
- package/cli/commands/swift.ts +5 -0
- package/cli/commands/upload.ts +46 -7
- package/cli/desktop-companion.ts +102 -17
- package/cli/drivers/desktop.ts +14 -1
- package/cli/drivers/playwright-sim-host.ts +38 -3
- package/cli/drivers/playwright.ts +9 -8
- package/cli/drivers/types.ts +5 -0
- package/cli/maestro-accessibility.ts +1 -1
- package/cli/maestro-compare.ts +84 -12
- package/cli/main.ts +2 -1
- package/cli/outbound-endpoints.ts +13 -1
- package/cli/pause-aware-timeout.ts +102 -0
- package/cli/scene-video.ts +89 -0
- package/cli/ws-bridge.ts +23 -14
- package/detox/attempt-events.cjs +4 -0
- package/detox/index.ts +167 -142
- package/detox/motion-change.cjs +57 -21
- package/detox/motion-change.d.cts +7 -2
- 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 +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +13 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +14 -8
- package/dist-lib/bridge-contract.mjs +13 -7
- package/dist-lib/bundle-channel.cjs +9536 -7604
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +6 -3
- package/dist-lib/config.mjs +6 -3
- package/dist-lib/detox/index.cjs +895 -664
- package/dist-lib/dev-bundle-resolution.cjs +14 -2
- package/dist-lib/expo-swift-bundle.cjs +7818 -5916
- package/dist-lib/expo-swift-metadata.cjs +3 -1
- package/dist-lib/expo-swift-metadata.mjs +3 -1
- package/dist-lib/home-paths.cjs +32 -3
- package/dist-lib/host/bridge-host.cjs +8340 -6392
- 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 +812 -339
- 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 +391 -28
- package/dist-lib/native-app-identity-project.cjs +55 -2
- package/dist-lib/native-app-identity-project.mjs +55 -2
- package/dist-lib/native-app-identity.cjs +53 -1
- package/dist-lib/native-app-identity.mjs +52 -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 +5 -1
- package/dist-lib/react-native-host-modules.mjs +5 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +15 -3
- package/dist-lib/sdk.cjs +27 -11
- package/dist-lib/sdk.mjs +27 -10
- package/dist-lib/skills.cjs +12276 -8947
- package/dist-lib/swift-compile.cjs +924 -118
- package/dist-lib/swift-compile.mjs +966 -152
- package/dist-lib/swift-package.cjs +12 -3
- package/dist-lib/swift-package.mjs +12 -3
- package/dist-lib/swift-submit.cjs +7 -1
- package/dist-lib/swift-submit.mjs +5 -1
- package/dist-lib/swift-toolchain.cjs +133 -18
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1955 -498
- package/dist-lib/vite.cjs +3 -1
- package/dist-lib/worklet-executable.cjs +1 -1
- package/dist-lib/worklet-executable.mjs +1 -1
- package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
- package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +2 -1
- package/skills/peach-perf/SKILL.md +11 -5
- package/skills/peach-setup/SKILL.md +16 -19
- package/src/bridge-constants.ts +11 -0
- package/src/bridge-contract.ts +22 -34
- package/src/config.ts +15 -0
- package/src/desktop-host-delivery.ts +5 -1
- package/src/dev-bundle-resolution.ts +2 -1
- package/src/expo-manifest-body.ts +13 -0
- package/src/expo-swift-host.ts +191 -47
- package/src/expo-swift-ready.ts +78 -0
- package/src/expoSwiftContract.ts +13 -0
- package/src/home-paths.ts +48 -2
- package/src/host/bridge-host.ts +5 -2
- package/src/native-app-identity-project.ts +26 -2
- package/src/native-app-identity.ts +90 -0
- package/src/native-firebase-config.ts +209 -13
- package/src/process-group.ts +25 -2
- package/src/react-native-bridge-modules.ts +416 -0
- package/src/react-native-host-modules.ts +4 -0
- package/src/swift-binary-modules.ts +2 -2
- package/src/swift-compile.ts +835 -84
- package/src/swift-compiler-digest.ts +14 -0
- package/src/swift-distribution.lock.json +14 -12
- package/src/swift-host.ts +91 -29
- package/src/swift-icu.ts +14 -6
- package/src/swift-objc-sdk.ts +133 -10
- package/src/swift-objc-standin.ts +25 -3
- package/src/swift-package-manifest.ts +21 -6
- package/src/swift-package.ts +15 -1
- package/src/swift-packages.ts +119 -60
- package/src/swift-standin-emit.ts +57 -9
- package/src/swift-standins.ts +168 -20
- package/src/swift-submit.ts +10 -0
- package/src/swift-toolchain.ts +149 -23
- package/src/vite-plugin.ts +12 -1
package/cli/commands/doctor.ts
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
// native code Peach does not know with how to stub it, and a no-login report.
|
|
5
5
|
|
|
6
6
|
import { createHash } from 'node:crypto'
|
|
7
|
-
import {
|
|
8
|
-
import { dirname, join, relative, resolve } from 'node:path'
|
|
7
|
+
import { relative, resolve } from 'node:path'
|
|
9
8
|
import { PURE_JS_PACKAGES } from '@peach/compat/pure-js-whitelist'
|
|
10
9
|
import { resolveCompatVersionRange } from '@peach/compat/version-range'
|
|
11
10
|
import {
|
|
@@ -20,11 +19,11 @@ import { rnxPublicBrand } from '../../src/public-brand'
|
|
|
20
19
|
import { resolveRNXAppConfig } from '../app-config'
|
|
21
20
|
import {
|
|
22
21
|
installedPackageDir,
|
|
23
|
-
|
|
22
|
+
hasNativeCode,
|
|
23
|
+
scanLocalNative,
|
|
24
|
+
type LocalNative,
|
|
25
|
+
type AnsweredNames,
|
|
24
26
|
packageNativeRequests,
|
|
25
|
-
sourceNativeRequests,
|
|
26
|
-
readSmallFile,
|
|
27
|
-
SOURCE_FILE,
|
|
28
27
|
type AppStubs,
|
|
29
28
|
type AppliedStubs,
|
|
30
29
|
type NativeRequest as PackageNativeRequest,
|
|
@@ -33,11 +32,8 @@ import { readRuntimeCompatibility } from '../runtime-compat'
|
|
|
33
32
|
import { accent, bold, failure, muted, success, warning } from '../ui'
|
|
34
33
|
import {
|
|
35
34
|
findProjectRoot,
|
|
36
|
-
scanRepository,
|
|
37
35
|
scanWorkspace,
|
|
38
36
|
installedManifest,
|
|
39
|
-
metadataValue,
|
|
40
|
-
readProjectJson,
|
|
41
37
|
readProjectManifest,
|
|
42
38
|
} from './detect'
|
|
43
39
|
import { sanitizeProjectReportName, sanitizeProjectReportVersion } from './project-report'
|
|
@@ -129,7 +125,18 @@ export async function runDoctor(args: string[]): Promise<number> {
|
|
|
129
125
|
versions: checkVersions(target.installed, runtime.appSupport),
|
|
130
126
|
scan: target.scan,
|
|
131
127
|
unknownNative: findUnknownNative(target.dir, target.scan, answered),
|
|
132
|
-
local: scanLocalNative(
|
|
128
|
+
local: scanLocalNative(
|
|
129
|
+
target.dir,
|
|
130
|
+
target.manifest,
|
|
131
|
+
answered,
|
|
132
|
+
target.scan.scoreSummary.platform,
|
|
133
|
+
new Set([
|
|
134
|
+
...PURE_JS_PACKAGES,
|
|
135
|
+
...target.scan.packages
|
|
136
|
+
.filter((item) => item.rnx.source !== 'untracked')
|
|
137
|
+
.map((item) => item.name),
|
|
138
|
+
]),
|
|
139
|
+
),
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
if (json) printJson(findings)
|
|
@@ -817,23 +824,6 @@ function findUnknownNative(
|
|
|
817
824
|
})
|
|
818
825
|
}
|
|
819
826
|
|
|
820
|
-
function hasNativeCode(
|
|
821
|
-
dir: string,
|
|
822
|
-
platform: ScanResult['scoreSummary']['platform'],
|
|
823
|
-
): boolean {
|
|
824
|
-
const expoConfig = join(dir, 'expo-module.config.json')
|
|
825
|
-
const platforms = metadataValue(readProjectJson(expoConfig), 'platforms')
|
|
826
|
-
if (Array.isArray(platforms))
|
|
827
|
-
return (
|
|
828
|
-
platforms.includes(platform) || (platform === 'ios' && platforms.includes('apple'))
|
|
829
|
-
)
|
|
830
|
-
if ([platform, 'expo-module.config.json'].some((entry) => existsSync(join(dir, entry))))
|
|
831
|
-
return true
|
|
832
|
-
if (listDir(dir).some((name) => name.endsWith('.podspec'))) return true
|
|
833
|
-
const manifest = readSmallFile(join(dir, 'package.json'))
|
|
834
|
-
return manifest !== null && manifest.includes('"codegenConfig"')
|
|
835
|
-
}
|
|
836
|
-
|
|
837
827
|
function reportLocalNative(local: readonly LocalNative[]): void {
|
|
838
828
|
for (const item of local) {
|
|
839
829
|
const where = item.expoModule
|
|
@@ -845,26 +835,6 @@ function reportLocalNative(local: readonly LocalNative[]): void {
|
|
|
845
835
|
}
|
|
846
836
|
}
|
|
847
837
|
|
|
848
|
-
// an app's own native code, found from the names its JavaScript asks the native
|
|
849
|
-
// side for and from local Expo modules (a directory with expo-module.config.json).
|
|
850
|
-
// each name is a key under its package in peach.config.ts `packages`.
|
|
851
|
-
export interface LocalNative {
|
|
852
|
-
kind: 'module' | 'view'
|
|
853
|
-
name: string
|
|
854
|
-
// the package the name belongs to: the nearest package.json with a name
|
|
855
|
-
package: string
|
|
856
|
-
// first place the name appears, relative to the app, as file:line
|
|
857
|
-
path: string
|
|
858
|
-
// the local Expo module directory the name belongs to, relative to the app
|
|
859
|
-
expoModule: string | null
|
|
860
|
-
swift: boolean
|
|
861
|
-
// a stub already answers it
|
|
862
|
-
answered: boolean
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
const NATIVE_SOURCE_FILE = /\.(?:swift|kt)$/
|
|
866
|
-
type AnsweredNames = Record<LocalNative['kind'], Set<string>>
|
|
867
|
-
|
|
868
838
|
function answeredNames(
|
|
869
839
|
config: Awaited<ReturnType<typeof resolveRNXAppConfig>>['config'],
|
|
870
840
|
): AnsweredNames {
|
|
@@ -878,143 +848,6 @@ function answeredNames(
|
|
|
878
848
|
}
|
|
879
849
|
}
|
|
880
850
|
|
|
881
|
-
function scanLocalNative(
|
|
882
|
-
appDir: string,
|
|
883
|
-
pkg: ProjectManifest,
|
|
884
|
-
answered: AnsweredNames,
|
|
885
|
-
scan: ScanResult,
|
|
886
|
-
): LocalNative[] {
|
|
887
|
-
// linking a known dependency into a workspace does not make it app-owned native code.
|
|
888
|
-
const knownPackages = new Set([
|
|
889
|
-
...PURE_JS_PACKAGES,
|
|
890
|
-
...scan.packages
|
|
891
|
-
.filter((item) => item.rnx.source !== 'untracked')
|
|
892
|
-
.map((item) => item.name),
|
|
893
|
-
])
|
|
894
|
-
const expoModuleDirs: string[] = []
|
|
895
|
-
const excludedModuleDirs: string[] = []
|
|
896
|
-
const sourceFiles: string[] = []
|
|
897
|
-
for (const root of [appDir, ...workspacePackageDirs(appDir, pkg)]) {
|
|
898
|
-
for (const dir of scanRepository(root).directories) {
|
|
899
|
-
if (
|
|
900
|
-
excludedModuleDirs.some(
|
|
901
|
-
(excluded) => dir === excluded || dir.startsWith(`${excluded}/`),
|
|
902
|
-
)
|
|
903
|
-
)
|
|
904
|
-
continue
|
|
905
|
-
if (existsSync(join(dir, 'expo-module.config.json'))) {
|
|
906
|
-
if (!hasNativeCode(dir, scan.scoreSummary.platform)) {
|
|
907
|
-
excludedModuleDirs.push(dir)
|
|
908
|
-
continue
|
|
909
|
-
}
|
|
910
|
-
expoModuleDirs.push(dir)
|
|
911
|
-
}
|
|
912
|
-
for (const name of listDir(dir)) {
|
|
913
|
-
if (SOURCE_FILE.test(name) && !name.endsWith('.d.ts'))
|
|
914
|
-
sourceFiles.push(join(dir, name))
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
const nativeFilesByModule = new Map(
|
|
919
|
-
expoModuleDirs.map((dir) => [
|
|
920
|
-
dir,
|
|
921
|
-
scanRepository(dir).directories.flatMap((sub) =>
|
|
922
|
-
listDir(sub)
|
|
923
|
-
.filter((name) => NATIVE_SOURCE_FILE.test(name))
|
|
924
|
-
.map((name) => join(sub, name)),
|
|
925
|
-
),
|
|
926
|
-
]),
|
|
927
|
-
)
|
|
928
|
-
|
|
929
|
-
const owners = new Map<string, string>()
|
|
930
|
-
const owner = (dir: string): string => {
|
|
931
|
-
const known = owners.get(dir)
|
|
932
|
-
if (known) return known
|
|
933
|
-
const name = readSmallFile(join(dir, 'package.json'))?.match(
|
|
934
|
-
/"name"\s*:\s*"([^"]+)"/,
|
|
935
|
-
)?.[1]
|
|
936
|
-
const parent = dirname(dir)
|
|
937
|
-
const result = name ?? (parent === dir ? (pkg.name ?? 'app') : owner(parent))
|
|
938
|
-
owners.set(dir, result)
|
|
939
|
-
return result
|
|
940
|
-
}
|
|
941
|
-
const found = new Map<string, LocalNative>()
|
|
942
|
-
const add = (kind: LocalNative['kind'], name: string, file: string, line: number) => {
|
|
943
|
-
const packageName = owner(dirname(file))
|
|
944
|
-
if (knownPackages.has(packageName)) return
|
|
945
|
-
const key = `${kind}:${name}`
|
|
946
|
-
if (found.has(key)) return
|
|
947
|
-
const moduleDir = expoModuleDirs.find((dir) => file.startsWith(`${dir}/`)) ?? null
|
|
948
|
-
found.set(key, {
|
|
949
|
-
kind,
|
|
950
|
-
name,
|
|
951
|
-
package: packageName,
|
|
952
|
-
path: `${relative(appDir, file)}:${line}`,
|
|
953
|
-
expoModule: moduleDir ? relative(appDir, moduleDir) || '.' : null,
|
|
954
|
-
swift: moduleDir
|
|
955
|
-
? (nativeFilesByModule.get(moduleDir) ?? []).some((path) =>
|
|
956
|
-
path.endsWith('.swift'),
|
|
957
|
-
)
|
|
958
|
-
: false,
|
|
959
|
-
answered: answered[kind].has(name),
|
|
960
|
-
})
|
|
961
|
-
}
|
|
962
|
-
for (const file of sourceFiles) {
|
|
963
|
-
const text = readSmallFile(file)
|
|
964
|
-
if (!text) continue
|
|
965
|
-
for (const { kind, name, line } of sourceNativeRequests(file, text))
|
|
966
|
-
add(kind, name, file, line)
|
|
967
|
-
}
|
|
968
|
-
// a local expo module declares its name in its native definition, Name("X"),
|
|
969
|
-
// whether or not the app's javascript spells it out. swift first, so a
|
|
970
|
-
// module the report marks Swift points at its swift definition.
|
|
971
|
-
for (const [, files] of nativeFilesByModule) {
|
|
972
|
-
const swiftFirst = [...files].sort(
|
|
973
|
-
(a, b) => Number(b.endsWith('.swift')) - Number(a.endsWith('.swift')),
|
|
974
|
-
)
|
|
975
|
-
for (const file of swiftFirst) {
|
|
976
|
-
const text = readSmallFile(file)
|
|
977
|
-
if (!text) continue
|
|
978
|
-
for (const match of text.matchAll(/\bName\(\s*"(\w+)"\s*\)/g)) {
|
|
979
|
-
if (match[1])
|
|
980
|
-
add('module', match[1], file, text.slice(0, match.index).split('\n').length)
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
return [...found.values()].sort(
|
|
985
|
-
(a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name),
|
|
986
|
-
)
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// dependencies installed as links to source outside any node_modules are the
|
|
990
|
-
// app's own workspace packages, which hold its native code as often as the app
|
|
991
|
-
// directory does.
|
|
992
|
-
function workspacePackageDirs(appDir: string, pkg: ProjectManifest): string[] {
|
|
993
|
-
const dirs = new Set<string>()
|
|
994
|
-
for (const name of Object.keys({ ...pkg.dependencies, ...pkg.devDependencies })) {
|
|
995
|
-
let current = appDir
|
|
996
|
-
while (true) {
|
|
997
|
-
const link = join(current, 'node_modules', name)
|
|
998
|
-
if (existsSync(link)) {
|
|
999
|
-
if (lstatSync(link).isSymbolicLink()) {
|
|
1000
|
-
const target = realpathSync(link)
|
|
1001
|
-
if (
|
|
1002
|
-
!target.split('/').includes('node_modules') &&
|
|
1003
|
-
!target.startsWith(`${appDir}/`)
|
|
1004
|
-
) {
|
|
1005
|
-
dirs.add(target)
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
break
|
|
1009
|
-
}
|
|
1010
|
-
const parent = dirname(current)
|
|
1011
|
-
if (parent === current) break
|
|
1012
|
-
current = parent
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
return [...dirs]
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
851
|
function scanOnePackage(
|
|
1019
852
|
appDir: string,
|
|
1020
853
|
pkg: ProjectManifest,
|
package/cli/commands/flow.ts
CHANGED
|
@@ -434,20 +434,20 @@ flow extension:
|
|
|
434
434
|
const remapArgs = args.flatMap((arg, index) =>
|
|
435
435
|
args[index - 1] === '--remap' ? ['--remap', arg] : [],
|
|
436
436
|
)
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
|
|
437
|
+
// CI previews export video from the saved scene after the take. desktop
|
|
438
|
+
// previews can also record live video; explicit --record always requests it.
|
|
439
|
+
const record =
|
|
440
|
+
args.includes('--record') || (preview && process.env.GITHUB_ACTIONS !== 'true')
|
|
441
441
|
// hold the final UI state in the recording so viewers see the result
|
|
442
442
|
// of the flow, not just a frame mid-transition. short flows otherwise
|
|
443
443
|
// cut off ~600ms in. caller can override with --tail-wait <ms>.
|
|
444
444
|
const tailWaitMs = args.includes('--tail-wait')
|
|
445
445
|
? Math.max(0, +(getFlag('--tail-wait') || '2000'))
|
|
446
|
-
:
|
|
447
|
-
?
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
446
|
+
: preview
|
|
447
|
+
? 6000
|
|
448
|
+
: record
|
|
449
|
+
? 2000
|
|
450
|
+
: 0
|
|
451
451
|
|
|
452
452
|
// parse flow file (frontmatter + steps). `${...}` templates stay verbatim
|
|
453
453
|
// here — the runner interpolates per step at execution time.
|
|
@@ -730,6 +730,9 @@ flow extension:
|
|
|
730
730
|
let videoPath: string | null = null
|
|
731
731
|
let videoDurationMs: number | null = null
|
|
732
732
|
let capturedEvents: Array<Record<string, any>> | null = null
|
|
733
|
+
let sceneRecording = false
|
|
734
|
+
let scenePath: string | null = null
|
|
735
|
+
let previewRecordingStartedAtMs: number | null = null
|
|
733
736
|
let previewScreenshotPath: string | null = null
|
|
734
737
|
let failureBundleDir: string | null = null
|
|
735
738
|
let exitCode = 0
|
|
@@ -799,14 +802,17 @@ flow extension:
|
|
|
799
802
|
simId: driver.simId,
|
|
800
803
|
code: `(() => {
|
|
801
804
|
const r = window.SootSim?.bridges?.eventRecorder ?? window.__sootsimEventRecorder
|
|
802
|
-
|
|
805
|
+
const startedAtMs = Date.now()
|
|
806
|
+
return r?.start?.(startedAtMs) ? startedAtMs : null
|
|
803
807
|
})()`,
|
|
804
808
|
})
|
|
809
|
+
if (typeof started === 'number') previewRecordingStartedAtMs = started
|
|
805
810
|
if (!started) {
|
|
806
811
|
console.warn(
|
|
807
812
|
' warn: eventRecorder.start() returned false — preview will upload bundle without replay events',
|
|
808
813
|
)
|
|
809
814
|
}
|
|
815
|
+
sceneRecording = await startPreviewSceneRecording(bridge, driver.simId)
|
|
810
816
|
}
|
|
811
817
|
await hooks.beforeSteps?.()
|
|
812
818
|
if (hooks.runSteps) await hooks.runSteps(driver, steps)
|
|
@@ -830,6 +836,10 @@ flow extension:
|
|
|
830
836
|
})()`,
|
|
831
837
|
})) as Array<Record<string, any>>
|
|
832
838
|
console.log(` captured ${capturedEvents.length} replay events`)
|
|
839
|
+
if (sceneRecording) {
|
|
840
|
+
sceneRecording = false
|
|
841
|
+
scenePath = await stopPreviewSceneRecording(bridge, driver.simId)
|
|
842
|
+
}
|
|
833
843
|
previewScreenshotPath = path.join(screenshotDir, 'preview-final.png')
|
|
834
844
|
await driver.captureScreenshot(previewScreenshotPath)
|
|
835
845
|
console.log(` final screenshot: ${previewScreenshotPath}`)
|
|
@@ -887,6 +897,10 @@ flow extension:
|
|
|
887
897
|
)
|
|
888
898
|
}
|
|
889
899
|
}
|
|
900
|
+
if (sceneRecording) {
|
|
901
|
+
sceneRecording = false
|
|
902
|
+
scenePath = await stopPreviewSceneRecording(bridge, driver.simId)
|
|
903
|
+
}
|
|
890
904
|
lastFlowError = err.message
|
|
891
905
|
console.error(`\n x failed: ${err.message}\n`)
|
|
892
906
|
if (profileResult) {
|
|
@@ -959,6 +973,7 @@ flow extension:
|
|
|
959
973
|
// canvas during scrub for smooth motion while the real app
|
|
960
974
|
// catches up.
|
|
961
975
|
if (videoPath) uploadArgs.push('--video', videoPath)
|
|
976
|
+
if (scenePath) uploadArgs.push('--scene', scenePath)
|
|
962
977
|
if (previewScreenshotPath) {
|
|
963
978
|
uploadArgs.push('--screenshot', previewScreenshotPath)
|
|
964
979
|
}
|
|
@@ -966,7 +981,8 @@ flow extension:
|
|
|
966
981
|
uploadArgs.push('--video-duration-ms', String(Math.round(videoDurationMs)))
|
|
967
982
|
uploadArgs.push('--recorded-duration-ms', String(Math.round(videoDurationMs)))
|
|
968
983
|
}
|
|
969
|
-
const recordingStartedAtMs =
|
|
984
|
+
const recordingStartedAtMs =
|
|
985
|
+
driver.getLastRecordingStartedAtMs() ?? previewRecordingStartedAtMs
|
|
970
986
|
if (recordingStartedAtMs && Number.isFinite(recordingStartedAtMs)) {
|
|
971
987
|
uploadArgs.push(
|
|
972
988
|
'--recording-started-at-ms',
|
|
@@ -1151,6 +1167,58 @@ async function prepareEventsFile(events: Array<Record<string, any>>): Promise<st
|
|
|
1151
1167
|
return outPath
|
|
1152
1168
|
}
|
|
1153
1169
|
|
|
1170
|
+
// the tab's scene stream rides the preview share. a tab with no app on screen
|
|
1171
|
+
// has none to record, and the share keeps its events and video.
|
|
1172
|
+
async function startPreviewSceneRecording(
|
|
1173
|
+
bridge: ReturnType<typeof createBridge>,
|
|
1174
|
+
simId: string | undefined,
|
|
1175
|
+
): Promise<boolean> {
|
|
1176
|
+
try {
|
|
1177
|
+
await bridge.send({
|
|
1178
|
+
type: 'evaluate',
|
|
1179
|
+
simId,
|
|
1180
|
+
code: `window.SootSim.bridges.sceneStream.run({ action: 'recordStart' })`,
|
|
1181
|
+
})
|
|
1182
|
+
return true
|
|
1183
|
+
} catch (err) {
|
|
1184
|
+
console.warn(
|
|
1185
|
+
` warn: scene stream not recorded: ${err instanceof Error ? err.message : String(err)}`,
|
|
1186
|
+
)
|
|
1187
|
+
return false
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// write the recorded scene stream to a temp file for the upload command.
|
|
1192
|
+
async function stopPreviewSceneRecording(
|
|
1193
|
+
bridge: ReturnType<typeof createBridge>,
|
|
1194
|
+
simId: string | undefined,
|
|
1195
|
+
): Promise<string | null> {
|
|
1196
|
+
try {
|
|
1197
|
+
const encoded = await bridge.send({
|
|
1198
|
+
type: 'evaluate',
|
|
1199
|
+
simId,
|
|
1200
|
+
code: `(async () => {
|
|
1201
|
+
const { bytes } = await window.SootSim.bridges.sceneStream.run({ action: 'recordStop' })
|
|
1202
|
+
let text = ''
|
|
1203
|
+
for (let i = 0; i < bytes.length; i += 0x8000)
|
|
1204
|
+
text += String.fromCharCode(...bytes.subarray(i, i + 0x8000))
|
|
1205
|
+
return btoa(text)
|
|
1206
|
+
})()`,
|
|
1207
|
+
})
|
|
1208
|
+
if (typeof encoded !== 'string') throw new Error('the tab returned no scene stream')
|
|
1209
|
+
const bytes = Buffer.from(encoded, 'base64')
|
|
1210
|
+
const outPath = path.join(tmpdir(), `peach-scene-${Date.now()}.psr.gz`)
|
|
1211
|
+
fs.writeFileSync(outPath, bytes)
|
|
1212
|
+
console.log(` scene stream: ${outPath} (${bytes.length} bytes gz)`)
|
|
1213
|
+
return outPath
|
|
1214
|
+
} catch (err) {
|
|
1215
|
+
console.warn(
|
|
1216
|
+
` warn: scene stream stop failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1217
|
+
)
|
|
1218
|
+
return null
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1154
1222
|
async function prepareFlowStepTimelineFile(
|
|
1155
1223
|
steps: SootSimFlowTraceStep[],
|
|
1156
1224
|
): Promise<string> {
|
|
@@ -13,17 +13,14 @@
|
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
HEADLESS_TENANT_FETCH_HUNG_MARKER,
|
|
16
|
+
readyProbeContentBasis,
|
|
16
17
|
readyProbeIsReady,
|
|
17
18
|
type SemanticWaitCondition,
|
|
18
19
|
type SemanticWaitResult,
|
|
19
20
|
type WsBridgeCommand,
|
|
20
21
|
} from '../../../src/bridge-contract'
|
|
21
22
|
import { SEMANTIC_WAIT_MAX_TIMEOUT_MS } from '../../../src/bridge-contract-input'
|
|
22
|
-
export {
|
|
23
|
-
readyProbeHasContent,
|
|
24
|
-
readyProbeHasTargetContent,
|
|
25
|
-
readyProbeIsReady,
|
|
26
|
-
} from '../../../src/bridge-contract'
|
|
23
|
+
export { readyProbeHasContent, readyProbeIsReady } from '../../../src/bridge-contract'
|
|
27
24
|
import type {
|
|
28
25
|
SimSemanticNode,
|
|
29
26
|
SimSemanticQueryResult,
|
|
@@ -1175,6 +1172,11 @@ export interface WaitReadyStatus {
|
|
|
1175
1172
|
externalError: string
|
|
1176
1173
|
suppressedEntryError: string
|
|
1177
1174
|
errors: number
|
|
1175
|
+
// the probe fields that decide readiness, so a ready result shows why it
|
|
1176
|
+
// returned: which content condition held and what painted.
|
|
1177
|
+
paintedAppContent: boolean
|
|
1178
|
+
treeQuietMs: number
|
|
1179
|
+
readyBasis: string
|
|
1178
1180
|
// non-empty when the probe itself stopped reaching the sim (tab closed,
|
|
1179
1181
|
// sim never connected) — the wait failed on the bridge, not on the app.
|
|
1180
1182
|
bridgeError: string
|
|
@@ -1422,6 +1424,9 @@ function waitReadyStatus(result: SemanticWaitResult, ready: boolean): WaitReadyS
|
|
|
1422
1424
|
suppressedEntryError: probe?.suppressedEntryError ?? '',
|
|
1423
1425
|
errors: probe?.errors ?? 0,
|
|
1424
1426
|
bridgeError: result.error ?? '',
|
|
1427
|
+
paintedAppContent: probe?.paintedAppContent ?? false,
|
|
1428
|
+
treeQuietMs: probe?.treeQuietMs ?? 0,
|
|
1429
|
+
readyBasis: probe ? readyProbeContentBasis(probe) : '',
|
|
1425
1430
|
}
|
|
1426
1431
|
}
|
|
1427
1432
|
|
|
@@ -422,9 +422,15 @@ export async function waitForSootsimIdle({
|
|
|
422
422
|
webViewStarting = true
|
|
423
423
|
webViewBlocker = 'preview progress element displayed'
|
|
424
424
|
} else {
|
|
425
|
-
|
|
425
|
+
// a loading document with no area (an app's hidden worker or
|
|
426
|
+
// executor frame) paints nothing, and a screenshot on device
|
|
427
|
+
// does not wait for it.
|
|
428
|
+
const unreadyWebView = [...document.querySelectorAll(
|
|
426
429
|
'[data-sootsim-webview-loading="true"], [data-sootsim-webview-mode="placeholder"]'
|
|
427
|
-
)
|
|
430
|
+
)].find((view) => {
|
|
431
|
+
const frame = view.querySelector('iframe')
|
|
432
|
+
return !frame || frame.clientWidth > 0 || frame.clientHeight > 0
|
|
433
|
+
})
|
|
428
434
|
if (unreadyWebView) {
|
|
429
435
|
webViewStarting = true
|
|
430
436
|
webViewBlocker = 'webView document still loading'
|
|
@@ -691,6 +697,9 @@ export type SettledWaitReady = {
|
|
|
691
697
|
// the last readiness read: the settled tree once the app went idle,
|
|
692
698
|
// otherwise the read that ended the wait.
|
|
693
699
|
status: WaitReadyStatus
|
|
700
|
+
// the first read that counted as ready, before the settle: the probe the
|
|
701
|
+
// wait accepted. null when the app never became ready.
|
|
702
|
+
accepted: WaitReadyStatus | null
|
|
694
703
|
// the last settle; null when the app never became ready.
|
|
695
704
|
idle: WaitForSootsimIdleResult | null
|
|
696
705
|
elapsedMs: number
|
|
@@ -709,8 +718,10 @@ export async function waitReadyAndSettle(
|
|
|
709
718
|
): Promise<SettledWaitReady> {
|
|
710
719
|
const start = Date.now()
|
|
711
720
|
const remaining = () => Math.max(0, start + timeoutMs - Date.now())
|
|
721
|
+
let accepted: WaitReadyStatus | null = null
|
|
712
722
|
const result = (status: WaitReadyStatus, idle: WaitForSootsimIdleResult | null) => ({
|
|
713
723
|
status,
|
|
724
|
+
accepted,
|
|
714
725
|
idle,
|
|
715
726
|
elapsedMs: Date.now() - start,
|
|
716
727
|
})
|
|
@@ -719,6 +730,7 @@ export async function waitReadyAndSettle(
|
|
|
719
730
|
idle = null
|
|
720
731
|
const ready = await inspectWaitReady(bridge, remaining(), options)
|
|
721
732
|
if (!ready.ready) return result(ready, idle)
|
|
733
|
+
accepted = ready
|
|
722
734
|
idle = await waitForSootsimIdle({
|
|
723
735
|
bridge,
|
|
724
736
|
simId: options.simId,
|
|
@@ -26,10 +26,12 @@ export async function runWaitReadySubcommand(opts: {
|
|
|
26
26
|
if (bridge.plane === 'cloud') {
|
|
27
27
|
const status = await inspectWaitReady(bridge, timeoutMs)
|
|
28
28
|
if (!status.ready) timedOut(status, status.elapsedMs)
|
|
29
|
+
printAcceptedProbe(status)
|
|
29
30
|
printReady(status, status.elapsedMs)
|
|
30
31
|
return
|
|
31
32
|
}
|
|
32
33
|
let status: Awaited<ReturnType<typeof waitReadyAndSettle>>['status']
|
|
34
|
+
let accepted: Awaited<ReturnType<typeof waitReadyAndSettle>>['accepted']
|
|
33
35
|
let idle: Awaited<ReturnType<typeof waitReadyAndSettle>>['idle']
|
|
34
36
|
let elapsedMs: number
|
|
35
37
|
try {
|
|
@@ -41,6 +43,7 @@ export async function runWaitReadySubcommand(opts: {
|
|
|
41
43
|
},
|
|
42
44
|
})
|
|
43
45
|
status = settledWait.status
|
|
46
|
+
accepted = settledWait.accepted
|
|
44
47
|
idle = settledWait.idle
|
|
45
48
|
elapsedMs = settledWait.elapsedMs
|
|
46
49
|
} catch (error) {
|
|
@@ -51,6 +54,7 @@ export async function runWaitReadySubcommand(opts: {
|
|
|
51
54
|
throw error
|
|
52
55
|
}
|
|
53
56
|
if (!idle) timedOut(status, elapsedMs)
|
|
57
|
+
if (accepted) printAcceptedProbe(accepted)
|
|
54
58
|
if (!idle.settled) {
|
|
55
59
|
console.error(
|
|
56
60
|
` ⚠ wait ready timed out after ${elapsedMs}ms — app mounted but did not settle: ${idle.blockedBy} (nodes: ${status.nodes}, targets: ${status.targets})`,
|
|
@@ -66,6 +70,14 @@ export async function runWaitReadySubcommand(opts: {
|
|
|
66
70
|
printReady(status, elapsedMs)
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
// the probe the wait accepted, kept so a ready that later proves wrong can be
|
|
74
|
+
// read from the command's own output.
|
|
75
|
+
function printAcceptedProbe(status: WaitReadyStatus): void {
|
|
76
|
+
console.error(
|
|
77
|
+
` accepted probe at ${Math.round(status.elapsedMs)}ms: basis ${status.readyBasis}, flag ${String(status.flag)}, ${status.nodes} nodes (quiet ${Math.round(status.treeQuietMs)}ms), ${status.targets} targets, painted ${status.paintedAppContent}, live frame ${status.liveFrameActive} (${status.liveFrameChannels} channels, ${status.liveFramePublishes} publishes), browser surfaces ${status.browserSurfaceCount}`,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
69
81
|
function printReady(status: WaitReadyStatus, elapsedMs: number): void {
|
|
70
82
|
console.log(
|
|
71
83
|
` ready in ${elapsedMs}ms: ${status.nodes} nodes, ${status.targets} targets${status.liveFrameActive ? `, ${status.liveFrameChannels} live surfaces` : ''}${status.browserSurfaceCount ? `, ${status.browserSurfaceCount} browser surfaces` : ''}`,
|
package/cli/commands/inspect.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
parseBridgeCliArgs,
|
|
35
35
|
printBridgeWorldNotice,
|
|
36
36
|
type WsBridge,
|
|
37
|
+
type BridgeSimInfo,
|
|
37
38
|
} from '../ws-bridge'
|
|
38
39
|
import {
|
|
39
40
|
isTapSuccess,
|
|
@@ -596,13 +597,13 @@ export function printShellPerfReport(result: Record<string, any>) {
|
|
|
596
597
|
console.log(``)
|
|
597
598
|
console.log(` cadence (frame delivery, ${cadence.vsyncTicks} vsync ticks):`)
|
|
598
599
|
console.log(
|
|
599
|
-
` display clock: ${cadence.displayHz}hz · host rAF interval p50 ${cadence.hostIntervalP50} / p95 ${cadence.hostIntervalP95} / max ${cadence.hostIntervalMax} ms${cadence.hostGaps ? ` · ${cadence.hostGaps} gaps >1.5x (host rAF starved)` : ''}`,
|
|
600
|
+
` display clock: ${cadence.displayHz}hz · host rAF interval p50 ${cadence.hostIntervalP50} / p95 ${cadence.hostIntervalP95} / max ${cadence.hostIntervalMax} ms${cadence.hostGaps ? ` · ${cadence.hostGaps} gaps >1.5x (host rAF starved)` : ''}${cadence.idleBreaks ? ` · ${cadence.idleBreaks} host idle breaks excluded` : ''}`,
|
|
600
601
|
)
|
|
601
602
|
console.log(
|
|
602
603
|
` delivery lag: p50 ${cadence.deliveryLagP50} / p95 ${cadence.deliveryLagP95} / max ${cadence.deliveryLagMax} ms (vsync postMessage → shell receipt)`,
|
|
603
604
|
)
|
|
604
605
|
console.log(
|
|
605
|
-
`
|
|
606
|
+
` present interval: p50 ${cadence.paintIntervalP50} / p95 ${cadence.paintIntervalP95} / max ${cadence.paintIntervalMax} ms (during motion, ${cadence.paintIntervals} intervals, including native frames)`,
|
|
606
607
|
)
|
|
607
608
|
}
|
|
608
609
|
const compositorCadence = result.compositorCadence
|
|
@@ -1247,7 +1248,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
1247
1248
|
return
|
|
1248
1249
|
}
|
|
1249
1250
|
|
|
1250
|
-
//
|
|
1251
|
+
// driver discovery works offline; a live bridge also reports each running renderer.
|
|
1251
1252
|
// short-circuit here so users can discover drivers even when no peach
|
|
1252
1253
|
// session is running.
|
|
1253
1254
|
if (
|
|
@@ -1256,6 +1257,19 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
1256
1257
|
) {
|
|
1257
1258
|
const { buildDriverListRows } = await import('../drivers')
|
|
1258
1259
|
const rows = buildDriverListRows()
|
|
1260
|
+
const bridge = (opts.createBridge ?? createBridgeFromParsed)(parsed)
|
|
1261
|
+
let sims: BridgeSimInfo[] = []
|
|
1262
|
+
try {
|
|
1263
|
+
sims = await bridge.listSims()
|
|
1264
|
+
} catch {
|
|
1265
|
+
// local availability remains useful when no bridge is running.
|
|
1266
|
+
} finally {
|
|
1267
|
+
bridge.close()
|
|
1268
|
+
}
|
|
1269
|
+
if (wantsJson(effectiveArgs)) {
|
|
1270
|
+
printJson({ drivers: rows, sims })
|
|
1271
|
+
return
|
|
1272
|
+
}
|
|
1259
1273
|
console.log(` available drivers (${rows.length}):\n`)
|
|
1260
1274
|
const idWidth = Math.max(...rows.map((r) => r.id.length), 6)
|
|
1261
1275
|
const kindWidth = Math.max(...rows.map((r) => r.kind.length), 4)
|
|
@@ -1270,6 +1284,15 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
1270
1284
|
console.log(` unavailable: ${row.reason}`)
|
|
1271
1285
|
}
|
|
1272
1286
|
}
|
|
1287
|
+
for (const sim of sims) {
|
|
1288
|
+
const renderer = sim.meta?.peachRenderer
|
|
1289
|
+
const executable = sim.meta?.peachHostExecutable
|
|
1290
|
+
if (sim.readyState === 'open' && typeof renderer === 'string') {
|
|
1291
|
+
console.log(
|
|
1292
|
+
` in use: ${sim.id} · ${String(sim.meta?.sootsimHostDriver)} · ${renderer}${typeof executable === 'string' ? ` @ ${executable}` : ''}`,
|
|
1293
|
+
)
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1273
1296
|
return
|
|
1274
1297
|
}
|
|
1275
1298
|
|
|
@@ -126,6 +126,7 @@ async function installLinuxDeb(packagePath: string): Promise<string> {
|
|
|
126
126
|
async function installWindowsExe(exePath: string): Promise<string> {
|
|
127
127
|
// nsis installer prompts and self-elevates; run it and let it take over.
|
|
128
128
|
await new Promise<void>((resolvePromise, rejectPromise) => {
|
|
129
|
+
// runner-toolchain-platform: win32
|
|
129
130
|
const child = spawn('cmd', ['/c', 'start', '""', '/wait', exePath], {
|
|
130
131
|
stdio: 'inherit',
|
|
131
132
|
})
|