peachsim 0.1.630 → 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/cli/app-fonts.ts +23 -4
- package/cli/bridge-flow-runner.ts +35 -28
- package/cli/command-registry.ts +1 -0
- package/cli/commands/compat.ts +11 -60
- package/cli/commands/control.ts +12 -5
- package/cli/commands/detect.ts +289 -26
- package/cli/commands/inspect/core.ts +1 -1
- package/cli/commands/inspect.ts +4 -0
- package/cli/commands/maestro.ts +3 -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/setup.ts +6 -4
- package/cli/commands/telemetry.ts +8 -3
- package/cli/commands/test.ts +347 -134
- package/cli/main.ts +11 -1
- package/cli/outbound-endpoints.ts +2 -1
- package/cli/privacy.ts +1 -2
- package/cli/runtime-diagnostics.ts +51 -0
- package/cli/telemetry.ts +78 -7
- package/detox/index.ts +3 -8
- 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 +2 -7
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +8 -5
- package/dist-lib/host/bridge-host.cjs +110 -67
- 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 +5 -1
- 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 +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 +1 -1
- package/dist-lib/native-app-identity.cjs +1 -1
- package/dist-lib/native-app-identity.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/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +3 -1
- package/dist-lib/sdk.mjs +2 -1
- package/dist-lib/skills.cjs +589 -268
- 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 +439 -167
- package/dist-lib/vite-swift.cjs +1 -1
- package/dist-lib/vite.cjs +1 -1
- package/package.json +2 -1
- package/skills/contrast/SKILL.md +1 -0
- package/src/home-paths.ts +11 -9
- package/src/menu.ts +11 -0
package/cli/app-fonts.ts
CHANGED
|
@@ -139,6 +139,16 @@ export function discoverNativeLinkedAppFonts(opts: {
|
|
|
139
139
|
collectFontFiles(root, relativeRoot.split(path.sep).join('/'))
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// the bundle path is the entry relative to metro's root, so a bundle at
|
|
143
|
+
// /apps/app/index.bundle served for /repo/apps/app puts that root at /repo.
|
|
144
|
+
const bundleDir = path.posix
|
|
145
|
+
.dirname(new URL(opts.bundleUrl).pathname)
|
|
146
|
+
.replace(/^\/+/, '')
|
|
147
|
+
const appRelativeDir = bundleDir.split('/').join(path.sep)
|
|
148
|
+
const metroRootCandidates = [selectedAppDir]
|
|
149
|
+
if (bundleDir && selectedAppDir.endsWith(path.sep + appRelativeDir)) {
|
|
150
|
+
metroRootCandidates.push(selectedAppDir.slice(0, -appRelativeDir.length - 1))
|
|
151
|
+
}
|
|
142
152
|
const vectorIconRequire = createRequire(path.join(selectedAppDir, 'package.json'))
|
|
143
153
|
for (const dependencyName of vectorIconDependencies) {
|
|
144
154
|
try {
|
|
@@ -150,10 +160,19 @@ export function discoverNativeLinkedAppFonts(opts: {
|
|
|
150
160
|
const packageRoot = entryPath.slice(0, markerIndex + marker.length - 1)
|
|
151
161
|
const fontsDir =
|
|
152
162
|
dependencyName === 'react-native-vector-icons' ? 'Fonts' : 'fonts'
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
163
|
+
// the package can sit outside the app (a hoisted workspace
|
|
164
|
+
// node_modules or a pnpm store). metro resolves asset paths against
|
|
165
|
+
// its config.projectRoot, which is the app in expo's monorepo setup
|
|
166
|
+
// but the repo root when metro.config sets it there, so offer the
|
|
167
|
+
// path from each candidate root; the reachability check keeps the
|
|
168
|
+
// one metro actually serves.
|
|
169
|
+
const fontsRoot = path.join(packageRoot, fontsDir)
|
|
170
|
+
for (const metroRoot of metroRootCandidates) {
|
|
171
|
+
collectFontFiles(
|
|
172
|
+
fontsRoot,
|
|
173
|
+
path.relative(metroRoot, fontsRoot).split(path.sep).join('/'),
|
|
174
|
+
)
|
|
175
|
+
}
|
|
157
176
|
} catch {}
|
|
158
177
|
}
|
|
159
178
|
}
|
|
@@ -2710,38 +2710,45 @@ export class SootSimBridgeFlowRunner {
|
|
|
2710
2710
|
}
|
|
2711
2711
|
|
|
2712
2712
|
try {
|
|
2713
|
-
//
|
|
2714
|
-
//
|
|
2715
|
-
//
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
step.
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2713
|
+
// simple commands have a wall-clock watchdog. bounded operations keep
|
|
2714
|
+
// their own lookup, settle and bridge deadlines, so the watchdog cannot
|
|
2715
|
+
// report a completed step while its action continues in the background.
|
|
2716
|
+
const hasBoundedWait = !!(
|
|
2717
|
+
// taps combine bounded lookup, settling and delivery. a wall-clock
|
|
2718
|
+
// watchdog can finish the step while that action is still running.
|
|
2719
|
+
// let these operations finish before starting the next command.
|
|
2720
|
+
(
|
|
2721
|
+
step.tapOn ||
|
|
2722
|
+
step.doubleTapOn ||
|
|
2723
|
+
step.longPressOn ||
|
|
2724
|
+
step.extendedWaitUntil ||
|
|
2725
|
+
step.waitFor ||
|
|
2726
|
+
step.scrollUntilVisible ||
|
|
2727
|
+
step.launchApp ||
|
|
2728
|
+
step.runFlow ||
|
|
2729
|
+
// retry reruns its body on failure, so its wall time is the body's
|
|
2730
|
+
// times the retry count — the 10s no-progress watchdog is the wrong
|
|
2731
|
+
// budget and would report "bridge probably hung" for a slow retry.
|
|
2732
|
+
step.retry ||
|
|
2733
|
+
// runScript http calls are synchronous with a 5-minute upstream
|
|
2734
|
+
// timeout — the 10s watchdog would kill legitimate setup scripts.
|
|
2735
|
+
step.runScript ||
|
|
2736
|
+
// takeScreenshot self-governs: it retries a bounded visual settle that
|
|
2737
|
+
// already fails with its own reason. sharing the watchdog's 10s budget
|
|
2738
|
+
// meant the watchdog always won that race, reporting "bridge probably
|
|
2739
|
+
// hung" for a screen that was merely still animating.
|
|
2740
|
+
step.takeScreenshot ||
|
|
2741
|
+
// assertions wait out maestro's lookup window, longer than 10s, and
|
|
2742
|
+
// every query they make times out on its own, so they report their
|
|
2743
|
+
// own failure instead of the watchdog's guess.
|
|
2744
|
+
step.assertVisible ||
|
|
2745
|
+
step.assertNotVisible
|
|
2746
|
+
)
|
|
2740
2747
|
)
|
|
2741
2748
|
const STEP_WATCHDOG_MS = 10_000
|
|
2742
2749
|
const body = this.runStepInner(step)
|
|
2743
2750
|
let screenshotPath: string | undefined
|
|
2744
|
-
if (
|
|
2751
|
+
if (hasBoundedWait) {
|
|
2745
2752
|
const result = await body
|
|
2746
2753
|
if (typeof result === 'string') screenshotPath = result
|
|
2747
2754
|
} else {
|
package/cli/command-registry.ts
CHANGED
package/cli/commands/compat.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// peach compat — scan dependencies with the shared compatibility engine.
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { dirname, join, relative, resolve } from 'node:path'
|
|
3
|
+
import { relative, resolve } from 'node:path'
|
|
5
4
|
import {
|
|
6
5
|
COMPAT_RESULT_CAVEAT,
|
|
7
6
|
formatDetailedScanLines,
|
|
@@ -9,13 +8,13 @@ import {
|
|
|
9
8
|
scanDeps,
|
|
10
9
|
type ScanResult,
|
|
11
10
|
} from '@peach/compat/web'
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
11
|
+
import {
|
|
12
|
+
findProjectRoot,
|
|
13
|
+
scanWorkspace,
|
|
14
|
+
installedManifest,
|
|
15
|
+
readProjectManifest,
|
|
16
|
+
} from './detect'
|
|
17
|
+
import type { ProjectManifest } from './projectReportContract'
|
|
19
18
|
|
|
20
19
|
export async function runCompat(args: string[]): Promise<void> {
|
|
21
20
|
if (args.includes('--help') || args.includes('-h')) {
|
|
@@ -41,7 +40,7 @@ flags:
|
|
|
41
40
|
const packageName = args.find(
|
|
42
41
|
(arg, index) => !arg.startsWith('-') && (appIndex < 0 || index !== appIndex + 1),
|
|
43
42
|
)
|
|
44
|
-
const pkg =
|
|
43
|
+
const pkg = readProjectManifest(appDir)
|
|
45
44
|
if (!pkg) {
|
|
46
45
|
console.error(` no package.json found in ${appDir}`)
|
|
47
46
|
process.exitCode = 1
|
|
@@ -65,7 +64,7 @@ flags:
|
|
|
65
64
|
const workspaceRoot = findProjectRoot(appDir)
|
|
66
65
|
const elsewhere = scanWorkspace(workspaceRoot).apps.flatMap((app) => {
|
|
67
66
|
if (resolve(app.dir) === appDir) return []
|
|
68
|
-
const manifest =
|
|
67
|
+
const manifest = readProjectManifest(app.dir)
|
|
69
68
|
if (!manifest) return []
|
|
70
69
|
const result = scanDeps(installedManifest(app.dir, manifest))
|
|
71
70
|
return result.packages.length > 0 ? [{ dir: app.dir, result }] : []
|
|
@@ -114,57 +113,9 @@ function report(args: string[], result: ScanResult): void {
|
|
|
114
113
|
console.log(`\n ${COMPAT_RESULT_CAVEAT}\n`)
|
|
115
114
|
}
|
|
116
115
|
|
|
117
|
-
// the declared range is often not a version: pnpm and bun catalogs write
|
|
118
|
-
// `catalog:`, a workspace dependency writes `workspace:*`, and a pinned tarball
|
|
119
|
-
// writes a URL. none of those match a verified version range, so a whole app
|
|
120
|
-
// reports as unverified. the installed tree holds the version that will
|
|
121
|
-
// actually run, so ask it, and keep the declared range for anything the project
|
|
122
|
-
// has not installed.
|
|
123
|
-
function installedManifest(appDir: string, pkg: PackageManifest): PackageManifest {
|
|
124
|
-
const resolveVersion = (name: string, declared: string): string => {
|
|
125
|
-
let dir = appDir
|
|
126
|
-
while (true) {
|
|
127
|
-
const manifest = join(dir, 'node_modules', name, 'package.json')
|
|
128
|
-
if (existsSync(manifest)) {
|
|
129
|
-
try {
|
|
130
|
-
const parsed: unknown = JSON.parse(readFileSync(manifest, 'utf8'))
|
|
131
|
-
if (parsed && typeof parsed === 'object' && 'version' in parsed) {
|
|
132
|
-
const version = parsed.version
|
|
133
|
-
if (typeof version === 'string') return version
|
|
134
|
-
}
|
|
135
|
-
} catch {}
|
|
136
|
-
return declared
|
|
137
|
-
}
|
|
138
|
-
const parent = dirname(dir)
|
|
139
|
-
if (parent === dir) return declared
|
|
140
|
-
dir = parent
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
const mapped = (deps: Record<string, string> | undefined) =>
|
|
144
|
-
deps &&
|
|
145
|
-
Object.fromEntries(
|
|
146
|
-
Object.entries(deps).map(([name, declared]) => [
|
|
147
|
-
name,
|
|
148
|
-
resolveVersion(name, declared),
|
|
149
|
-
]),
|
|
150
|
-
)
|
|
151
|
-
return {
|
|
152
|
-
name: pkg.name,
|
|
153
|
-
dependencies: mapped(pkg.dependencies),
|
|
154
|
-
devDependencies: mapped(pkg.devDependencies),
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function readPackageJson(appDir: string): PackageManifest | null {
|
|
159
|
-
const packagePath = resolve(appDir, 'package.json')
|
|
160
|
-
if (!existsSync(packagePath)) return null
|
|
161
|
-
const parsed: unknown = JSON.parse(readFileSync(packagePath, 'utf8'))
|
|
162
|
-
return parsed && typeof parsed === 'object' ? parsed : null
|
|
163
|
-
}
|
|
164
|
-
|
|
165
116
|
function scanOnePackage(
|
|
166
117
|
appDir: string,
|
|
167
|
-
pkg:
|
|
118
|
+
pkg: ProjectManifest,
|
|
168
119
|
packageName: string,
|
|
169
120
|
): ScanResult {
|
|
170
121
|
const installed = installedManifest(appDir, pkg)
|
package/cli/commands/control.ts
CHANGED
|
@@ -73,7 +73,9 @@ import {
|
|
|
73
73
|
} from '../current-sim'
|
|
74
74
|
import { getDriver } from '../drivers/registry'
|
|
75
75
|
import { openUrl as openUrlInBrowser } from '../open-url'
|
|
76
|
+
import { diagnosticsAllowedForCurrentProcess } from '../privacy'
|
|
76
77
|
import { rethrowIfExit, rnxExit } from '../run-rnx'
|
|
78
|
+
import { collectRuntimeDiagnostics } from '../runtime-diagnostics'
|
|
77
79
|
import { rnxSelfInvocation } from '../self-invocation'
|
|
78
80
|
import {
|
|
79
81
|
BridgeSimLockedError,
|
|
@@ -695,11 +697,13 @@ async function filterReachableAppFonts(
|
|
|
695
697
|
}
|
|
696
698
|
}),
|
|
697
699
|
)
|
|
698
|
-
|
|
700
|
+
// a font offered from more than one candidate metro root keeps the first
|
|
701
|
+
// url metro served.
|
|
702
|
+
const reachable = new Map<string, DiscoveredAppFontSpec>()
|
|
699
703
|
for (const spec of results) {
|
|
700
|
-
if (spec !== null) reachable.
|
|
704
|
+
if (spec !== null && !reachable.has(spec.family)) reachable.set(spec.family, spec)
|
|
701
705
|
}
|
|
702
|
-
return reachable
|
|
706
|
+
return [...reachable.values()]
|
|
703
707
|
}
|
|
704
708
|
|
|
705
709
|
async function resolveShellUrl(
|
|
@@ -1293,7 +1297,8 @@ function printAgentListenHint(quiet: boolean): void {
|
|
|
1293
1297
|
// a splash; the caller can `peach describe` again a second later. for
|
|
1294
1298
|
// warm loads it's the real screen. guarded by --no-describe.
|
|
1295
1299
|
async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: string[]) {
|
|
1296
|
-
|
|
1300
|
+
const noDescribe = args.includes('--no-describe')
|
|
1301
|
+
if (noDescribe && !diagnosticsAllowedForCurrentProcess()) return
|
|
1297
1302
|
const prevQuiet = process.env.SOOTSIM_QUIET_TARGET_NOTICE
|
|
1298
1303
|
process.env.SOOTSIM_QUIET_TARGET_NOTICE = '1'
|
|
1299
1304
|
try {
|
|
@@ -1302,7 +1307,7 @@ async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: strin
|
|
|
1302
1307
|
// seconds on a still-routing guest app, which is exactly what we want
|
|
1303
1308
|
// to skip for the open-side describe. caller can `peach describe`
|
|
1304
1309
|
// again later for post-routing state.
|
|
1305
|
-
await waitForTreeStable(wsPort, simId, { stableMs: 150, maxMs: 400 })
|
|
1310
|
+
if (!noDescribe) await waitForTreeStable(wsPort, simId, { stableMs: 150, maxMs: 400 })
|
|
1306
1311
|
const bridge = createBridge(wsPort, {
|
|
1307
1312
|
commandTimeoutMs: 3000,
|
|
1308
1313
|
simId,
|
|
@@ -1310,6 +1315,7 @@ async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: strin
|
|
|
1310
1315
|
simIdSource: 'flag',
|
|
1311
1316
|
})
|
|
1312
1317
|
try {
|
|
1318
|
+
if (noDescribe) return
|
|
1313
1319
|
let readyProbe: Awaited<ReturnType<typeof inspectWaitReady>> | null = null
|
|
1314
1320
|
try {
|
|
1315
1321
|
readyProbe = await inspectWaitReady(bridge, 1)
|
|
@@ -1341,6 +1347,7 @@ async function maybeDescribeAfterOpen(wsPort: number, simId: string, args: strin
|
|
|
1341
1347
|
console.log('')
|
|
1342
1348
|
console.log(result.tree)
|
|
1343
1349
|
} finally {
|
|
1350
|
+
await collectRuntimeDiagnostics(bridge)
|
|
1344
1351
|
bridge.close()
|
|
1345
1352
|
}
|
|
1346
1353
|
} catch {
|
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
|
+
}
|
|
@@ -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 = []
|