peachsim 0.1.629 → 0.1.631
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 +8 -4
- package/cli/app-fonts.ts +23 -4
- package/cli/bridge-flow-runner.ts +45 -31
- package/cli/cli-update.ts +2 -7
- package/cli/cloud-test-tunnel.ts +16 -3
- package/cli/command-registry.ts +1 -0
- package/cli/commands/compat.ts +11 -60
- package/cli/commands/config.ts +1 -1
- package/cli/commands/control.ts +40 -8
- package/cli/commands/daemon-mac-app.ts +4 -2
- package/cli/commands/daemon.ts +4 -4
- package/cli/commands/detect.ts +289 -26
- package/cli/commands/device.ts +2 -3
- package/cli/commands/inspect/actions.ts +125 -90
- package/cli/commands/inspect/core.ts +1 -1
- package/cli/commands/inspect.ts +10 -1
- package/cli/commands/maestro.ts +3 -1
- package/cli/commands/platform.ts +1 -1
- package/cli/commands/project-report.ts +386 -0
- package/cli/commands/projectReportContract.ts +42 -0
- package/cli/commands/report-issue.ts +38 -54
- package/cli/commands/screenshot-layers.ts +1 -1
- package/cli/commands/screenshot.ts +1 -1
- package/cli/commands/setup.ts +6 -4
- package/cli/commands/skills.ts +0 -31
- package/cli/commands/telemetry.ts +8 -3
- package/cli/commands/test.ts +347 -134
- package/cli/drivers/playwright-provisioning.ts +3 -2
- package/cli/drivers/playwright-sim-host.ts +25 -9
- package/cli/main.ts +19 -3
- package/cli/outbound-endpoints.ts +2 -5
- package/cli/privacy.ts +1 -2
- package/cli/runtime-diagnostics.ts +51 -0
- package/cli/telemetry.ts +78 -7
- package/detox/index.ts +77 -51
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.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 +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/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- 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 +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +130 -33
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +8 -5
- package/dist-lib/host/bridge-host.cjs +353 -290
- 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 +257 -121
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/{vite-kotlin.cjs → kotlin-host.cjs} +130 -19
- package/dist-lib/menu.cjs +11 -1
- package/dist-lib/menu.mjs +11 -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 +132 -9
- package/dist-lib/native-app-identity.cjs +278 -0
- package/dist-lib/native-app-identity.mjs +245 -0
- 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/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +112 -87
- package/dist-lib/sdk.mjs +111 -87
- package/dist-lib/skills.cjs +1270 -709
- package/dist-lib/swift-compile.cjs +1 -1
- package/dist-lib/swift-compile.mjs +1 -1
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +672 -275
- package/dist-lib/vite-swift.cjs +1 -1
- package/dist-lib/vite.cjs +11 -28
- package/kotlin/README.md +32 -4
- package/kotlin/fixtures/counter/Counter.kt +26 -0
- package/kotlin/fixtures/counter/CounterScreen.kt +40 -0
- package/kotlin/fixtures/headless-counter/Headless.kt +228 -0
- package/kotlin/fixtures/runtime-state/RuntimeState.kt +200 -0
- package/kotlin/fork/LICENSE +178 -0
- package/kotlin/fork/NOTICE +25 -0
- package/kotlin/fork/README.md +59 -0
- package/kotlin/fork/build.sh +128 -0
- package/kotlin/fork/compose-worker-globals.patch +96 -0
- package/kotlin/fork/fork.json +33 -0
- package/kotlin/fork/publish.ts +77 -0
- package/kotlin/fork/skiko-worker-clock.patch +33 -0
- package/kotlin/toolchain.json +288 -0
- package/package.json +13 -5
- package/skills/contrast/SKILL.md +1 -0
- package/src/automatic-update-policy.ts +17 -0
- package/src/home-paths.ts +11 -9
- package/src/host/bridge-host.ts +3 -1
- package/src/{vite-plugin-kotlin.ts → kotlin-host.ts} +155 -12
- package/src/menu.ts +11 -0
- package/src/metro-plugin.ts +18 -0
- package/src/native-app-identity.ts +376 -0
- package/src/native-firebase-config.ts +90 -0
- package/src/screenshots/frame-compose.ts +1 -1
- package/src/screenshots/registry.ts +1 -1
- package/src/screenshots/schema.ts +2 -2
- package/src/vite-plugin-one.ts +11 -28
- package/src/vite-plugin.ts +6 -1
package/cli/commands/detect.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
// project detection — finds project root, detects framework, scans monorepo workspaces
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
opendirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
statSync,
|
|
10
|
+
} from 'node:fs'
|
|
11
|
+
import { basename, dirname, join } from 'node:path'
|
|
12
|
+
import JSON5 from 'json5'
|
|
13
|
+
import { parse as parseYaml } from 'yaml'
|
|
14
|
+
import type { DependencyVersion, ProjectManifest } from './projectReportContract'
|
|
5
15
|
|
|
6
16
|
export type Framework = 'one' | 'expo' | 'bare' | 'unknown'
|
|
7
17
|
export type PackageManager = 'bun' | 'pnpm' | 'npm' | 'yarn'
|
|
8
|
-
export type MonorepoType = 'turbo' | 'pnpm' | 'nx' | 'npm-workspaces' | 'single'
|
|
18
|
+
export type MonorepoType = 'turbo' | 'pnpm' | 'nx' | 'lerna' | 'npm-workspaces' | 'single'
|
|
9
19
|
|
|
10
20
|
export interface DetectedProject {
|
|
11
21
|
dir: string
|
|
@@ -25,7 +35,8 @@ export interface DetectedWorkspace {
|
|
|
25
35
|
|
|
26
36
|
const CONFIG_EXTENSIONS = ['ts', 'js', 'cjs', 'mjs', 'mts', 'cts']
|
|
27
37
|
|
|
28
|
-
export function findProjectRoot(cwd: string): string {
|
|
38
|
+
export function findProjectRoot(cwd: string, includeNativeRepository = false): string {
|
|
39
|
+
let swiftRoot: string | undefined
|
|
29
40
|
let dir = cwd
|
|
30
41
|
while (true) {
|
|
31
42
|
if (
|
|
@@ -37,22 +48,30 @@ export function findProjectRoot(cwd: string): string {
|
|
|
37
48
|
return dir
|
|
38
49
|
}
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
44
|
-
if (pkg.workspaces) return dir
|
|
45
|
-
} catch {}
|
|
46
|
-
}
|
|
51
|
+
if (metadataValue(readProjectJson(join(dir, 'package.json')), 'workspaces'))
|
|
52
|
+
return dir
|
|
47
53
|
|
|
54
|
+
if (includeNativeRepository) {
|
|
55
|
+
if (existsSync(join(dir, '.git'))) return dir
|
|
56
|
+
if (!swiftRoot && existsSync(join(dir, 'Package.swift'))) swiftRoot = dir
|
|
57
|
+
}
|
|
48
58
|
const parent = dirname(dir)
|
|
49
59
|
if (parent === dir) break
|
|
50
60
|
dir = parent
|
|
51
61
|
}
|
|
52
|
-
return cwd
|
|
62
|
+
return swiftRoot ?? cwd
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
export function detectPackageManager(root: string): PackageManager {
|
|
66
|
+
const manager = metadataValue(
|
|
67
|
+
readProjectJson(join(root, 'package.json')),
|
|
68
|
+
'packageManager',
|
|
69
|
+
)
|
|
70
|
+
if (typeof manager === 'string') {
|
|
71
|
+
const name = manager.split('@')[0]
|
|
72
|
+
if (name === 'bun' || name === 'pnpm' || name === 'npm' || name === 'yarn')
|
|
73
|
+
return name
|
|
74
|
+
}
|
|
56
75
|
if (existsSync(join(root, 'bun.lock')) || existsSync(join(root, 'bun.lockb'))) {
|
|
57
76
|
return 'bun'
|
|
58
77
|
}
|
|
@@ -66,13 +85,9 @@ export function detectMonorepoType(root: string): MonorepoType {
|
|
|
66
85
|
if (existsSync(join(root, 'nx.json'))) return 'nx'
|
|
67
86
|
if (existsSync(join(root, 'pnpm-workspace.yaml'))) return 'pnpm'
|
|
68
87
|
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
73
|
-
if (pkg.workspaces) return 'npm-workspaces'
|
|
74
|
-
} catch {}
|
|
75
|
-
}
|
|
88
|
+
if (existsSync(join(root, 'lerna.json'))) return 'lerna'
|
|
89
|
+
if (metadataValue(readProjectJson(join(root, 'package.json')), 'workspaces'))
|
|
90
|
+
return 'npm-workspaces'
|
|
76
91
|
return 'single'
|
|
77
92
|
}
|
|
78
93
|
|
|
@@ -87,14 +102,14 @@ export function detectProject(dir: string): DetectedProject | null {
|
|
|
87
102
|
const pkgPath = join(dir, 'package.json')
|
|
88
103
|
if (!existsSync(pkgPath)) return null
|
|
89
104
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
93
|
-
} catch {
|
|
94
|
-
return null
|
|
95
|
-
}
|
|
105
|
+
const pkg = readProjectManifest(dir)
|
|
106
|
+
if (!pkg) return null
|
|
96
107
|
|
|
97
108
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
109
|
+
const devCommand = metadataValue(
|
|
110
|
+
metadataValue(readProjectJson(pkgPath), 'scripts'),
|
|
111
|
+
'dev',
|
|
112
|
+
)
|
|
98
113
|
const framework = detectFramework(deps)
|
|
99
114
|
|
|
100
115
|
if (framework === 'unknown') return null
|
|
@@ -112,7 +127,7 @@ export function detectProject(dir: string): DetectedProject | null {
|
|
|
112
127
|
hasMetroConfig: CONFIG_EXTENSIONS.some((ext) =>
|
|
113
128
|
existsSync(join(dir, `metro.config.${ext}`)),
|
|
114
129
|
),
|
|
115
|
-
devCommand:
|
|
130
|
+
devCommand: typeof devCommand === 'string' ? devCommand : null,
|
|
116
131
|
}
|
|
117
132
|
}
|
|
118
133
|
|
|
@@ -199,3 +214,251 @@ export function scanWorkspace(root: string): DetectedWorkspace {
|
|
|
199
214
|
|
|
200
215
|
return { root, type, packageManager, apps }
|
|
201
216
|
}
|
|
217
|
+
|
|
218
|
+
// reports inspect metadata without evaluating app config or following directory
|
|
219
|
+
// symlinks. this also finds native projects outside package-manager workspaces.
|
|
220
|
+
const REPORT_SKIP_DIRS = new Set([
|
|
221
|
+
'node_modules',
|
|
222
|
+
'Pods',
|
|
223
|
+
'Carthage',
|
|
224
|
+
'build',
|
|
225
|
+
'dist',
|
|
226
|
+
'dist-lib',
|
|
227
|
+
'DerivedData',
|
|
228
|
+
'vendor',
|
|
229
|
+
'coverage',
|
|
230
|
+
'target',
|
|
231
|
+
'tmp',
|
|
232
|
+
'cache',
|
|
233
|
+
'artifacts',
|
|
234
|
+
'test-results',
|
|
235
|
+
'playwright-report',
|
|
236
|
+
])
|
|
237
|
+
|
|
238
|
+
export function scanRepository(root: string): {
|
|
239
|
+
directories: string[]
|
|
240
|
+
truncated: boolean
|
|
241
|
+
unreadable: number
|
|
242
|
+
} {
|
|
243
|
+
const directories: string[] = []
|
|
244
|
+
const pending = [{ dir: root, depth: 0 }]
|
|
245
|
+
let entries = 0
|
|
246
|
+
let cursor = 0
|
|
247
|
+
let truncated = false
|
|
248
|
+
let unreadable = 0
|
|
249
|
+
while (cursor < pending.length && entries < 200_000 && directories.length < 10_000) {
|
|
250
|
+
const next = pending[cursor++]
|
|
251
|
+
if (!next) break
|
|
252
|
+
directories.push(next.dir)
|
|
253
|
+
let handle: ReturnType<typeof opendirSync>
|
|
254
|
+
try {
|
|
255
|
+
handle = opendirSync(next.dir)
|
|
256
|
+
} catch {
|
|
257
|
+
unreadable++
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
let entry
|
|
262
|
+
while (entries < 200_000 && (entry = handle.readSync())) {
|
|
263
|
+
entries++
|
|
264
|
+
if (
|
|
265
|
+
!entry.isDirectory() ||
|
|
266
|
+
entry.name.startsWith('.') ||
|
|
267
|
+
REPORT_SKIP_DIRS.has(entry.name)
|
|
268
|
+
)
|
|
269
|
+
continue
|
|
270
|
+
if (next.depth >= 12) {
|
|
271
|
+
truncated = true
|
|
272
|
+
continue
|
|
273
|
+
}
|
|
274
|
+
pending.push({ dir: join(next.dir, entry.name), depth: next.depth + 1 })
|
|
275
|
+
}
|
|
276
|
+
} finally {
|
|
277
|
+
handle.closeSync()
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
directories: directories.sort(),
|
|
282
|
+
truncated: truncated || cursor < pending.length || entries >= 200_000,
|
|
283
|
+
unreadable,
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// inspect only regular, bounded metadata files. errors are data, never a reason
|
|
288
|
+
// to execute project code. symlinks may point outside the checkout.
|
|
289
|
+
export function readProjectMetadata(path: string): string | null {
|
|
290
|
+
try {
|
|
291
|
+
const info = lstatSync(path)
|
|
292
|
+
if (!info.isFile() || info.size > 4 * 1024 * 1024) return null
|
|
293
|
+
return readFileSync(path, 'utf8')
|
|
294
|
+
} catch {
|
|
295
|
+
return null
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function metadataValue(value: unknown, key: string): unknown {
|
|
300
|
+
if (!value || typeof value !== 'object') return undefined
|
|
301
|
+
return Object.getOwnPropertyDescriptor(value, key)?.value
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function readProjectJson(path: string): unknown {
|
|
305
|
+
const text = readProjectMetadata(path)
|
|
306
|
+
if (text === null) return null
|
|
307
|
+
try {
|
|
308
|
+
const value: unknown = JSON.parse(text)
|
|
309
|
+
return value
|
|
310
|
+
} catch {
|
|
311
|
+
return null
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function readProjectManifest(dir: string): ProjectManifest | null {
|
|
316
|
+
const parsed = readProjectJson(join(dir, 'package.json'))
|
|
317
|
+
if (!parsed || typeof parsed !== 'object') return null
|
|
318
|
+
const record = (value: unknown): Record<string, string> => {
|
|
319
|
+
if (!value || typeof value !== 'object') return {}
|
|
320
|
+
return Object.fromEntries(
|
|
321
|
+
Object.entries(value).filter(
|
|
322
|
+
(entry): entry is [string, string] =>
|
|
323
|
+
typeof entry[1] === 'string' &&
|
|
324
|
+
/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i.test(entry[0]) &&
|
|
325
|
+
entry[0] !== '.' &&
|
|
326
|
+
entry[0] !== '..',
|
|
327
|
+
),
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
const name = metadataValue(parsed, 'name')
|
|
331
|
+
return {
|
|
332
|
+
...(typeof name === 'string' ? { name } : {}),
|
|
333
|
+
dependencies: record(metadataValue(parsed, 'dependencies')),
|
|
334
|
+
devDependencies: record(metadataValue(parsed, 'devDependencies')),
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// resolve each dependency at its nearest installation/lockfile boundary, so an
|
|
339
|
+
// uninstalled nested project never inherits another app's hoisted version.
|
|
340
|
+
export function resolveManifestVersions(
|
|
341
|
+
dir: string,
|
|
342
|
+
manifest: ProjectManifest,
|
|
343
|
+
locks = new Map<string, Map<string, string>>(),
|
|
344
|
+
): Record<string, DependencyVersion> {
|
|
345
|
+
const resolveVersion = (name: string, declared: string): DependencyVersion => {
|
|
346
|
+
if (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i.test(name))
|
|
347
|
+
return { version: declared, source: 'declared' }
|
|
348
|
+
let current = dir
|
|
349
|
+
while (true) {
|
|
350
|
+
// installed packages commonly use symlinks; read just this known manifest,
|
|
351
|
+
// never traverse their dependency trees.
|
|
352
|
+
try {
|
|
353
|
+
const file = join(current, 'node_modules', name, 'package.json')
|
|
354
|
+
if (statSync(file).size <= 256 * 1024) {
|
|
355
|
+
const value: unknown = JSON.parse(readFileSync(file, 'utf8'))
|
|
356
|
+
const version = metadataValue(value, 'version')
|
|
357
|
+
if (typeof version === 'string') return { version, source: 'installed' }
|
|
358
|
+
}
|
|
359
|
+
} catch {}
|
|
360
|
+
let lock = locks.get(current)
|
|
361
|
+
if (!lock) {
|
|
362
|
+
lock = readLockedVersions(current)
|
|
363
|
+
locks.set(current, lock)
|
|
364
|
+
}
|
|
365
|
+
const version = lock.get(name)
|
|
366
|
+
if (version) return { version, source: 'lockfile' }
|
|
367
|
+
if (
|
|
368
|
+
[
|
|
369
|
+
'bun.lock',
|
|
370
|
+
'bun.lockb',
|
|
371
|
+
'package-lock.json',
|
|
372
|
+
'pnpm-lock.yaml',
|
|
373
|
+
'yarn.lock',
|
|
374
|
+
].some((file) => existsSync(join(current, file)))
|
|
375
|
+
)
|
|
376
|
+
break
|
|
377
|
+
const parent = dirname(current)
|
|
378
|
+
if (parent === current) break
|
|
379
|
+
current = parent
|
|
380
|
+
}
|
|
381
|
+
return { version: declared, source: 'declared' }
|
|
382
|
+
}
|
|
383
|
+
return Object.fromEntries(
|
|
384
|
+
Object.entries({ ...manifest.dependencies, ...manifest.devDependencies }).map(
|
|
385
|
+
([name, declared]) => [name, resolveVersion(name, declared)],
|
|
386
|
+
),
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function readLockedVersions(dir: string): Map<string, string> {
|
|
391
|
+
const versions = new Map<string, string>()
|
|
392
|
+
const put = (name: string, value: unknown) => {
|
|
393
|
+
if (typeof value !== 'string') return
|
|
394
|
+
// multiple resolutions without an installed tree are ambiguous; report the
|
|
395
|
+
// declared range rather than choosing a transitive package's version.
|
|
396
|
+
const before = versions.get(name)
|
|
397
|
+
versions.set(name, before === undefined || before === value ? value : '')
|
|
398
|
+
}
|
|
399
|
+
const bun = readProjectMetadata(join(dir, 'bun.lock'))
|
|
400
|
+
if (bun) {
|
|
401
|
+
try {
|
|
402
|
+
const rootVersions = new Map<string, string>()
|
|
403
|
+
const parsed: unknown = JSON5.parse(bun)
|
|
404
|
+
const packages = metadataValue(parsed, 'packages')
|
|
405
|
+
if (packages && typeof packages === 'object') {
|
|
406
|
+
for (const [key, tuple] of Object.entries(packages)) {
|
|
407
|
+
if (!Array.isArray(tuple) || typeof tuple[0] !== 'string') continue
|
|
408
|
+
const match = /^((?:@[^/]+\/)?[^@]+)@([\d][\w.+-]*)$/.exec(tuple[0])
|
|
409
|
+
if (match) put(match[1], match[2])
|
|
410
|
+
// bun's root resolution wins over nested duplicate package keys.
|
|
411
|
+
if (match && key === match[1]) rootVersions.set(match[1], match[2])
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
for (const [name, version] of rootVersions) versions.set(name, version)
|
|
415
|
+
} catch {}
|
|
416
|
+
}
|
|
417
|
+
const npm = metadataValue(readProjectJson(join(dir, 'package-lock.json')), 'packages')
|
|
418
|
+
if (npm && typeof npm === 'object') {
|
|
419
|
+
for (const [key, value] of Object.entries(npm)) {
|
|
420
|
+
if (key.startsWith('node_modules/') && !key.slice(13).includes('/node_modules/'))
|
|
421
|
+
put(key.slice(13), metadataValue(value, 'version'))
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const pnpm = readProjectMetadata(join(dir, 'pnpm-lock.yaml'))
|
|
425
|
+
if (pnpm) {
|
|
426
|
+
try {
|
|
427
|
+
const parsed: unknown = parseYaml(pnpm)
|
|
428
|
+
const packages = metadataValue(parsed, 'packages')
|
|
429
|
+
if (packages && typeof packages === 'object')
|
|
430
|
+
for (const key of Object.keys(packages)) {
|
|
431
|
+
const match = /^\/?((?:@[^/]+\/)?[^@]+)@([\d][\w.+-]*)/.exec(key)
|
|
432
|
+
if (match) put(match[1], match[2])
|
|
433
|
+
}
|
|
434
|
+
} catch {}
|
|
435
|
+
}
|
|
436
|
+
const yarn = readProjectMetadata(join(dir, 'yarn.lock'))
|
|
437
|
+
if (yarn) {
|
|
438
|
+
let names: string[] = []
|
|
439
|
+
for (const line of yarn.split('\n')) {
|
|
440
|
+
if (/^\S.*:\s*$/.test(line))
|
|
441
|
+
names = [...line.matchAll(/(?:^|[", ]+)((?:@[^/\s]+\/)?[^@" ,]+)@/g)].map(
|
|
442
|
+
(match) => match[1],
|
|
443
|
+
)
|
|
444
|
+
const version = /^\s+version[ :]+["']?([\d][\w.+-]*)/.exec(line)
|
|
445
|
+
if (version) for (const name of names) put(name, version[1])
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return versions
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function installedManifest(
|
|
452
|
+
dir: string,
|
|
453
|
+
manifest: ProjectManifest,
|
|
454
|
+
): ProjectManifest {
|
|
455
|
+
const versions = resolveManifestVersions(dir, manifest)
|
|
456
|
+
const mapped = (deps: Record<string, string> | undefined) =>
|
|
457
|
+
deps &&
|
|
458
|
+
Object.fromEntries(Object.keys(deps).map((name) => [name, versions[name].version]))
|
|
459
|
+
return {
|
|
460
|
+
name: manifest.name,
|
|
461
|
+
dependencies: mapped(manifest.dependencies),
|
|
462
|
+
devDependencies: mapped(manifest.devDependencies),
|
|
463
|
+
}
|
|
464
|
+
}
|
package/cli/commands/device.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
devices,
|
|
3
|
-
DUO_REST_PROGRESS,
|
|
4
3
|
getDevice,
|
|
5
4
|
listSelectableDeviceModels,
|
|
6
5
|
type DeviceModel,
|
|
7
|
-
|
|
8
|
-
} from 'peach-engine/settings'
|
|
6
|
+
} from 'peach-engine/settings/devices'
|
|
7
|
+
import { DUO_REST_PROGRESS, type DuoFoldRest } from 'peach-engine/settings/duo-fold-bus'
|
|
9
8
|
import { SOOTSIM_SHELL_COMMAND_NAME } from '../../src/menu'
|
|
10
9
|
import { rnxExit } from '../run-rnx'
|
|
11
10
|
import {
|
|
@@ -332,6 +332,8 @@ function payloadFromResolved(
|
|
|
332
332
|
interface TextCandidate {
|
|
333
333
|
node: SimSemanticNode
|
|
334
334
|
ancestorTestIDs: string[]
|
|
335
|
+
actionable: boolean
|
|
336
|
+
role?: string
|
|
335
337
|
}
|
|
336
338
|
|
|
337
339
|
// a non-pressable node repeating the text of its nearest labelled ancestor,
|
|
@@ -343,16 +345,28 @@ function collectTextCandidates(
|
|
|
343
345
|
nodes: readonly SimSemanticNode[],
|
|
344
346
|
ancestors: string[] = [],
|
|
345
347
|
pressableOwnerText = '',
|
|
348
|
+
actionableAncestor = false,
|
|
349
|
+
actionableRole?: string,
|
|
346
350
|
): TextCandidate[] {
|
|
347
351
|
const out: TextCandidate[] = []
|
|
348
352
|
for (const node of nodes) {
|
|
349
353
|
const text = nodeText(node)
|
|
350
354
|
const caption = !!text && text === pressableOwnerText && !node.pressable
|
|
351
|
-
|
|
355
|
+
const actionable = !!node.pressable || actionableAncestor
|
|
356
|
+
const role = node.role ?? actionableRole
|
|
357
|
+
if (!caption) out.push({ node, ancestorTestIDs: ancestors, actionable, role })
|
|
352
358
|
if (node.children?.length) {
|
|
353
359
|
const next = node.testID ? [...ancestors, node.testID] : ancestors
|
|
354
360
|
const owner = text ? (node.pressable ? text : '') : pressableOwnerText
|
|
355
|
-
out.push(
|
|
361
|
+
out.push(
|
|
362
|
+
...collectTextCandidates(
|
|
363
|
+
node.children,
|
|
364
|
+
next,
|
|
365
|
+
owner,
|
|
366
|
+
actionable,
|
|
367
|
+
node.pressable ? node.role : actionableRole,
|
|
368
|
+
),
|
|
369
|
+
)
|
|
356
370
|
}
|
|
357
371
|
}
|
|
358
372
|
return out
|
|
@@ -388,93 +402,112 @@ export async function tapByText(
|
|
|
388
402
|
agent: opts.agent,
|
|
389
403
|
timing: opts.timing,
|
|
390
404
|
textFallback: query,
|
|
391
|
-
resolve:
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
const needle = options.exact ? query : query.toLowerCase()
|
|
395
|
-
let candidates = collectTextCandidates(tree).filter(({ node, ancestorTestIDs }) => {
|
|
396
|
-
const text = nodeText(node)
|
|
397
|
-
if (!text) return false
|
|
398
|
-
if (options.exact ? text !== needle : !text.toLowerCase().includes(needle)) {
|
|
399
|
-
return false
|
|
400
|
-
}
|
|
401
|
-
if (options.role && node.role !== options.role) return false
|
|
402
|
-
if (options.within && !ancestorTestIDs.includes(options.within)) return false
|
|
403
|
-
const ax = node.geometry.x
|
|
404
|
-
const ay = node.geometry.y
|
|
405
|
-
if (options.minX != null && !(ax >= options.minX)) return false
|
|
406
|
-
if (options.maxX != null && !(ax <= options.maxX)) return false
|
|
407
|
-
if (options.minY != null && !(ay >= options.minY)) return false
|
|
408
|
-
if (options.maxY != null && !(ay <= options.maxY)) return false
|
|
409
|
-
return true
|
|
410
|
-
})
|
|
405
|
+
resolve: () => resolveTextTapTarget(bridge, query, options),
|
|
406
|
+
})
|
|
407
|
+
}
|
|
411
408
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
409
|
+
async function resolveTextTapTarget(
|
|
410
|
+
bridge: InspectBridge,
|
|
411
|
+
query: string,
|
|
412
|
+
options: TapTextOptions = {},
|
|
413
|
+
) {
|
|
414
|
+
const { tree } = await inspectTree(bridge, 50)
|
|
415
|
+
if (!Array.isArray(tree)) return { matched: 0, total: 0 }
|
|
416
|
+
const needle = options.exact ? query : query.toLowerCase()
|
|
417
|
+
let candidates = collectTextCandidates(tree).filter(
|
|
418
|
+
({ node, ancestorTestIDs, role }) => {
|
|
419
|
+
const text = nodeText(node)
|
|
420
|
+
if (!text) return false
|
|
421
|
+
if (options.exact ? text !== needle : !text.toLowerCase().includes(needle)) {
|
|
422
|
+
return false
|
|
426
423
|
}
|
|
424
|
+
if (options.role && role !== options.role) return false
|
|
425
|
+
if (options.within && !ancestorTestIDs.includes(options.within)) return false
|
|
426
|
+
const ax = node.geometry.x
|
|
427
|
+
const ay = node.geometry.y
|
|
428
|
+
if (options.minX != null && !(ax >= options.minX)) return false
|
|
429
|
+
if (options.maxX != null && !(ax <= options.maxX)) return false
|
|
430
|
+
if (options.minY != null && !(ay >= options.minY)) return false
|
|
431
|
+
if (options.maxY != null && !(ay <= options.maxY)) return false
|
|
432
|
+
return true
|
|
433
|
+
},
|
|
434
|
+
)
|
|
427
435
|
|
|
428
|
-
|
|
429
|
-
|
|
436
|
+
// default label taps prefer controls to prose mentioning the same word.
|
|
437
|
+
// explicit position selectors keep their full candidate list and order.
|
|
438
|
+
if (options.nth == null && !options.first && !options.near) {
|
|
439
|
+
const actionable = candidates.filter((candidate) => candidate.actionable)
|
|
440
|
+
if (actionable.length > 0) candidates = actionable
|
|
441
|
+
}
|
|
430
442
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
idx: index,
|
|
443
|
-
nodeId: candidate.node.nodeId,
|
|
444
|
-
type: candidate.node.type,
|
|
445
|
-
testID: candidate.node.testID ?? null,
|
|
446
|
-
text: nodeText(candidate.node).slice(0, 60),
|
|
447
|
-
abs: { x: candidate.node.geometry.x, y: candidate.node.geometry.y },
|
|
448
|
-
layout: {
|
|
449
|
-
width: Math.round(candidate.node.geometry.width),
|
|
450
|
-
height: Math.round(candidate.node.geometry.height),
|
|
451
|
-
},
|
|
452
|
-
ancestorTestIDs: candidate.ancestorTestIDs.slice(0, 5),
|
|
453
|
-
})),
|
|
454
|
-
}
|
|
443
|
+
if (options.near) {
|
|
444
|
+
const { x, y } = options.near
|
|
445
|
+
candidates.sort(
|
|
446
|
+
(a, b) =>
|
|
447
|
+
Math.hypot(a.node.geometry.x - x, a.node.geometry.y - y) -
|
|
448
|
+
Math.hypot(b.node.geometry.x - x, b.node.geometry.y - y),
|
|
449
|
+
)
|
|
450
|
+
} else {
|
|
451
|
+
candidates.sort((a, b) => {
|
|
452
|
+
if (Math.abs(a.node.geometry.y - b.node.geometry.y) > 2) {
|
|
453
|
+
return a.node.geometry.y - b.node.geometry.y
|
|
455
454
|
}
|
|
455
|
+
return a.node.geometry.x - b.node.geometry.x
|
|
456
|
+
})
|
|
457
|
+
}
|
|
456
458
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
459
|
+
const total = candidates.length
|
|
460
|
+
if (total === 0) return { matched: 0, total: 0 }
|
|
461
|
+
|
|
462
|
+
let idx = 0
|
|
463
|
+
if (options.nth != null) {
|
|
464
|
+
idx = options.nth < 0 ? total + options.nth : options.nth
|
|
465
|
+
if (idx < 0 || idx >= total) {
|
|
466
|
+
return { matched: 0, total, nthOutOfRange: true, nth: options.nth }
|
|
467
|
+
}
|
|
468
|
+
} else if (total > 1 && !options.first && !options.near) {
|
|
469
|
+
return {
|
|
470
|
+
ambiguous: true,
|
|
471
|
+
total,
|
|
472
|
+
candidates: candidates.slice(0, 10).map((candidate, index) => ({
|
|
473
|
+
idx: index,
|
|
474
|
+
nodeId: candidate.node.nodeId,
|
|
475
|
+
type: candidate.node.type,
|
|
476
|
+
testID: candidate.node.testID ?? null,
|
|
477
|
+
text: nodeText(candidate.node).slice(0, 60),
|
|
478
|
+
abs: { x: candidate.node.geometry.x, y: candidate.node.geometry.y },
|
|
479
|
+
layout: {
|
|
480
|
+
width: Math.round(candidate.node.geometry.width),
|
|
481
|
+
height: Math.round(candidate.node.geometry.height),
|
|
482
|
+
},
|
|
483
|
+
ancestorTestIDs: candidate.ancestorTestIDs.slice(0, 5),
|
|
484
|
+
})),
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const picked = candidates[idx]
|
|
489
|
+
if (!picked) return { matched: 0, total }
|
|
490
|
+
const node = picked.node
|
|
491
|
+
// keep the selected handle: re-searching its text or testID can pick a
|
|
492
|
+
// different control, including a covered tenant behind native UI.
|
|
493
|
+
const tap: unknown = await bridge.send({
|
|
494
|
+
type: 'call',
|
|
495
|
+
path: 'SootSim.bridges.test.resolveTapTarget',
|
|
496
|
+
args: [node.nodeId],
|
|
477
497
|
})
|
|
498
|
+
if (!isTapTargetInfo(tap)) return { matched: 0, total }
|
|
499
|
+
return payloadFromResolved(
|
|
500
|
+
{
|
|
501
|
+
x: tap.cx,
|
|
502
|
+
y: tap.cy,
|
|
503
|
+
nodeId: tap.target.nodeId,
|
|
504
|
+
testID: tap.target.testID,
|
|
505
|
+
text: tap.target.text ?? tap.target.accessibilityLabel,
|
|
506
|
+
type: tap.target.type,
|
|
507
|
+
},
|
|
508
|
+
node,
|
|
509
|
+
{ total, idx },
|
|
510
|
+
)
|
|
478
511
|
}
|
|
479
512
|
|
|
480
513
|
export type TapQuery =
|
|
@@ -570,17 +603,19 @@ export async function tapBest(
|
|
|
570
603
|
},
|
|
571
604
|
}
|
|
572
605
|
}
|
|
573
|
-
const byText = await
|
|
574
|
-
if (byText) {
|
|
606
|
+
const byText = await resolveTextTapTarget(bridge, query)
|
|
607
|
+
if ('ambiguous' in byText) {
|
|
608
|
+
return {
|
|
609
|
+
ambiguous: true,
|
|
610
|
+
error: `multiple text matches for "${query}"; use tap-text with --nth, --within or --near`,
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if ('cx' in byText && typeof byText.cx === 'number') {
|
|
575
614
|
return {
|
|
576
|
-
...
|
|
615
|
+
...byText,
|
|
577
616
|
strategy: 'text',
|
|
578
617
|
node: {
|
|
579
|
-
|
|
580
|
-
id: byText.testID ?? null,
|
|
581
|
-
testID: byText.testID ?? null,
|
|
582
|
-
type: byText.type ?? null,
|
|
583
|
-
text: byText.text ?? query,
|
|
618
|
+
...byText.target,
|
|
584
619
|
},
|
|
585
620
|
}
|
|
586
621
|
}
|
|
@@ -1287,7 +1287,7 @@ export interface ConsoleEntry {
|
|
|
1287
1287
|
// One root error boundary in the tenant worker) can be live and screaming in
|
|
1288
1288
|
// the driver's console while the agent's `sootsim_errors` reports nothing.
|
|
1289
1289
|
// Merge both, dedup, so worker + root-error-boundary failures always surface.
|
|
1290
|
-
function consoleEntriesEval(level: 'error' | 'warn', limit: number): string {
|
|
1290
|
+
export function consoleEntriesEval(level: 'error' | 'warn', limit: number): string {
|
|
1291
1291
|
const getter = level === 'error' ? 'getErrors' : 'getWarnings'
|
|
1292
1292
|
return `(() => {
|
|
1293
1293
|
const out = []
|