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/inspect.ts
CHANGED
|
@@ -20,6 +20,8 @@ import { shouldPrintInspectNotice } from '../inspect-notice-state'
|
|
|
20
20
|
import { levenshtein } from '../levenshtein'
|
|
21
21
|
import { TOP_LEVEL_RUNTIME_COMMANDS } from '../parse-args'
|
|
22
22
|
import { rethrowIfExit, rnxExit } from '../run-rnx'
|
|
23
|
+
import { collectRuntimeDiagnostics } from '../runtime-diagnostics'
|
|
24
|
+
import { trackCliError } from '../telemetry'
|
|
23
25
|
import {
|
|
24
26
|
type BridgeTransportOption,
|
|
25
27
|
callInBridge,
|
|
@@ -1054,7 +1056,12 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
1054
1056
|
args[0] === 'get' || args[0] === 'do' || args[0] === 'debug' || args[0] === 'wait'
|
|
1055
1057
|
? args[0]
|
|
1056
1058
|
: null
|
|
1057
|
-
const
|
|
1059
|
+
const groupedArgs = verbPrefix ? args.slice(1) : args
|
|
1060
|
+
const tapTarget = groupedArgs[0] === 'tap' ? readTargetFlag(groupedArgs) : null
|
|
1061
|
+
const effectiveArgs =
|
|
1062
|
+
tapTarget?.mode === 'text'
|
|
1063
|
+
? ['tap-text', tapTarget.value, ...groupedArgs.slice(1)]
|
|
1064
|
+
: groupedArgs
|
|
1058
1065
|
|
|
1059
1066
|
const parsed = parseBridgeCliArgs(effectiveArgs, {
|
|
1060
1067
|
port: opts.port,
|
|
@@ -4634,6 +4641,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
4634
4641
|
}
|
|
4635
4642
|
} catch (err: any) {
|
|
4636
4643
|
rethrowIfExit(err)
|
|
4644
|
+
trackCliError(err)
|
|
4637
4645
|
const message = err instanceof Error ? err.message : String(err)
|
|
4638
4646
|
console.error(` ${subcommand ?? 'peach'} failed: ${message}`)
|
|
4639
4647
|
const unknownSim = /^no sim connected with id ([^;]+)(?:; connected sims: .+)?$/.exec(
|
|
@@ -4699,6 +4707,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
4699
4707
|
}
|
|
4700
4708
|
rnxExit(1)
|
|
4701
4709
|
} finally {
|
|
4710
|
+
if (subcommand !== 'list') await collectRuntimeDiagnostics(bridge)
|
|
4702
4711
|
bridge.close()
|
|
4703
4712
|
}
|
|
4704
4713
|
}
|
package/cli/commands/maestro.ts
CHANGED
|
@@ -244,7 +244,9 @@ const MAESTRO_COMPAT_VALUE_FLAGS = new Set([
|
|
|
244
244
|
'--screen-size',
|
|
245
245
|
])
|
|
246
246
|
|
|
247
|
-
|
|
247
|
+
// shared with the cloud submitter, which resolves tag selection before any
|
|
248
|
+
// hosted job exists the same way a local workspace run does.
|
|
249
|
+
export function splitTagList(value: string): string[] {
|
|
248
250
|
return value
|
|
249
251
|
.split(',')
|
|
250
252
|
.map((tag) => tag.trim())
|
package/cli/commands/platform.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
listSelectableDeviceModels,
|
|
13
13
|
type DeviceModel,
|
|
14
14
|
type RuntimePlatform,
|
|
15
|
-
} from 'peach-engine/settings'
|
|
15
|
+
} from 'peach-engine/settings/devices'
|
|
16
16
|
import { settingsStore } from 'peach-engine/settings/store'
|
|
17
17
|
import { rnxPublicBrand } from '../../src/public-brand'
|
|
18
18
|
import { UnsupportedRemoteHostError } from '../cloud-client'
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// local, read-only project metadata. never evaluate config or invoke a build.
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from 'node:child_process'
|
|
4
|
+
import { homedir, release, tmpdir } from 'node:os'
|
|
5
|
+
import { basename, dirname, join, relative, resolve } from 'node:path'
|
|
6
|
+
import { scanDeps } from '@peach/compat/web'
|
|
7
|
+
import { getCliVersion } from '../../src/cli-version'
|
|
8
|
+
import { readActiveRuntime } from '../../src/home-paths'
|
|
9
|
+
import {
|
|
10
|
+
detectFramework,
|
|
11
|
+
detectMonorepoType,
|
|
12
|
+
detectPackageManager,
|
|
13
|
+
findProjectRoot,
|
|
14
|
+
metadataValue,
|
|
15
|
+
readProjectJson,
|
|
16
|
+
readProjectManifest,
|
|
17
|
+
readProjectMetadata,
|
|
18
|
+
resolveManifestVersions,
|
|
19
|
+
scanRepository,
|
|
20
|
+
} from './detect'
|
|
21
|
+
import type {
|
|
22
|
+
DependencyVersion,
|
|
23
|
+
ProjectReport,
|
|
24
|
+
ProjectReportEntry,
|
|
25
|
+
} from './projectReportContract'
|
|
26
|
+
|
|
27
|
+
function toolVersion(command: string, args = ['--version']): string | null {
|
|
28
|
+
try {
|
|
29
|
+
const text = execFileSync(command, args, {
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
timeout: 3_000,
|
|
32
|
+
maxBuffer: 4_096,
|
|
33
|
+
cwd: tmpdir(),
|
|
34
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
35
|
+
}).trim()
|
|
36
|
+
const match =
|
|
37
|
+
command === 'xcodebuild'
|
|
38
|
+
? /Xcode ([\d.]+)\s+Build version ([\w.]+)/.exec(text)
|
|
39
|
+
: /^v?([\d][\w.+-]*)$/.exec(text)
|
|
40
|
+
return match ? match.slice(1).join(' (build ') + (match.length > 2 ? ')' : '') : null
|
|
41
|
+
} catch {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function sanitizeProjectReportName(value: string): string {
|
|
47
|
+
return value
|
|
48
|
+
.replace(/(?:https?|file):\/\/[^\s]+/g, '<redacted>')
|
|
49
|
+
.split(homedir())
|
|
50
|
+
.join('<home>')
|
|
51
|
+
.replace(/(?:[A-Za-z]:[\\/]|\/(?:Users|home)\/)[^\s]+/g, '<home>')
|
|
52
|
+
.replace(/[^\w@./ +()<>-]/g, '_')
|
|
53
|
+
.slice(0, 240)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function sanitizeProjectReportVersion(value: string): string {
|
|
57
|
+
// URL, git and local dependency specs can carry credentials or home paths.
|
|
58
|
+
return value.length <= 80 &&
|
|
59
|
+
/^(?:[v^~<>=*| ]*\d[\w.+*| <>=~-]*|[a-f\d]{7,64}|\*)$/.test(value)
|
|
60
|
+
? value
|
|
61
|
+
: 'non-version spec (redacted)'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function values(text: string, pattern: RegExp): string {
|
|
65
|
+
return [...new Set([...text.matchAll(pattern)].map((match) => match[1]))].join(', ')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function swiftDependencies(dir: string): Record<string, DependencyVersion> {
|
|
69
|
+
const dependencies: Record<string, DependencyVersion> = {}
|
|
70
|
+
// SwiftPM v1 and v2/v3 put the pin array in different containers.
|
|
71
|
+
const parsed =
|
|
72
|
+
readProjectJson(join(dir, 'Package.resolved')) ??
|
|
73
|
+
readProjectJson(join(dir, '.swiftpm/configuration/Package.resolved'))
|
|
74
|
+
const pins =
|
|
75
|
+
metadataValue(parsed, 'pins') ??
|
|
76
|
+
metadataValue(metadataValue(parsed, 'object'), 'pins')
|
|
77
|
+
if (Array.isArray(pins))
|
|
78
|
+
for (const pin of pins) {
|
|
79
|
+
const name = metadataValue(pin, 'identity') ?? metadataValue(pin, 'package')
|
|
80
|
+
const state = metadataValue(pin, 'state')
|
|
81
|
+
const version = metadataValue(state, 'version') ?? metadataValue(state, 'revision')
|
|
82
|
+
if (typeof name === 'string' && typeof version === 'string') {
|
|
83
|
+
dependencies[sanitizeProjectReportName(name).toLowerCase()] = {
|
|
84
|
+
version: sanitizeProjectReportVersion(version),
|
|
85
|
+
source: 'lockfile',
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return dependencies
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function podDependencies(dir: string): Record<string, DependencyVersion> {
|
|
93
|
+
const text = readProjectMetadata(join(dir, 'Podfile.lock')) ?? ''
|
|
94
|
+
const pods = text.split(/^DEPENDENCIES:/m)[0]
|
|
95
|
+
return Object.fromEntries(
|
|
96
|
+
[...pods.matchAll(/^ - ([\w./+-]+) \(([\d][\w.+-]*)\)/gm)].map((match) => [
|
|
97
|
+
match[1],
|
|
98
|
+
{
|
|
99
|
+
version: sanitizeProjectReportVersion(match[2]),
|
|
100
|
+
source: 'lockfile',
|
|
101
|
+
} satisfies DependencyVersion,
|
|
102
|
+
]),
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createProjectReport(
|
|
107
|
+
cwd = process.cwd(),
|
|
108
|
+
exactDirectory = false,
|
|
109
|
+
): ProjectReport {
|
|
110
|
+
const root = exactDirectory ? resolve(cwd) : findProjectRoot(resolve(cwd), true)
|
|
111
|
+
const scan = scanRepository(root)
|
|
112
|
+
const projects: ProjectReportEntry[] = []
|
|
113
|
+
const lockCache = new Map<string, Map<string, string>>()
|
|
114
|
+
const notes = [
|
|
115
|
+
'Only project paths, dependency versions and toolchain settings are included. No source, environment values, URLs or credentials.',
|
|
116
|
+
'Config is inspected without execution. Unspecified or dynamic engine/architecture settings are reported as unknown.',
|
|
117
|
+
'Directories named node_modules, Pods, Carthage, vendor, build, dist, dist-lib, DerivedData, coverage, target, tmp, cache, artifacts, test-results, playwright-report, and hidden directories are excluded. Directory symlinks are not followed.',
|
|
118
|
+
'Limits: 12 directory levels, 10,000 directories, 200,000 entries, 4 MiB per metadata file. Paths are relative to the scanned root.',
|
|
119
|
+
]
|
|
120
|
+
for (const dir of scan.directories) {
|
|
121
|
+
const path = sanitizeProjectReportName(relative(root, dir) || '.')
|
|
122
|
+
const manifest = readProjectManifest(dir)
|
|
123
|
+
if (manifest) {
|
|
124
|
+
const declared = { ...manifest.dependencies, ...manifest.devDependencies }
|
|
125
|
+
const framework = detectFramework(declared)
|
|
126
|
+
if (framework !== 'unknown') {
|
|
127
|
+
const resolved = resolveManifestVersions(dir, manifest, lockCache)
|
|
128
|
+
const resolvedManifest = {
|
|
129
|
+
dependencies: Object.fromEntries(
|
|
130
|
+
Object.entries(resolved).map(([name, item]) => [name, item.version]),
|
|
131
|
+
),
|
|
132
|
+
}
|
|
133
|
+
const nativeNames = scanDeps(resolvedManifest).packages.map((item) => item.name)
|
|
134
|
+
const dependencies: Record<string, DependencyVersion> = {}
|
|
135
|
+
for (const name of new Set([
|
|
136
|
+
'react',
|
|
137
|
+
'react-native',
|
|
138
|
+
'expo',
|
|
139
|
+
'one',
|
|
140
|
+
...nativeNames,
|
|
141
|
+
])) {
|
|
142
|
+
const item = resolved[name]
|
|
143
|
+
if (item)
|
|
144
|
+
dependencies[sanitizeProjectReportName(name)] = {
|
|
145
|
+
...item,
|
|
146
|
+
version: sanitizeProjectReportVersion(item.version),
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const expo = metadataValue(readProjectJson(join(dir, 'app.json')), 'expo')
|
|
150
|
+
const podProperties = readProjectJson(join(dir, 'ios/Podfile.properties.json'))
|
|
151
|
+
const gradle = readProjectMetadata(join(dir, 'android/gradle.properties')) ?? ''
|
|
152
|
+
const engine =
|
|
153
|
+
metadataValue(expo, 'jsEngine') ?? metadataValue(podProperties, 'expo.jsEngine')
|
|
154
|
+
const architecture =
|
|
155
|
+
metadataValue(expo, 'newArchEnabled') ??
|
|
156
|
+
metadataValue(podProperties, 'newArchEnabled')
|
|
157
|
+
const settings: Record<string, string> = {
|
|
158
|
+
jsEngine:
|
|
159
|
+
engine === 'hermes' || engine === 'jsc'
|
|
160
|
+
? `${engine} (configured)`
|
|
161
|
+
: 'unknown (not configured in static metadata)',
|
|
162
|
+
newArchitecture:
|
|
163
|
+
typeof architecture === 'boolean' ||
|
|
164
|
+
architecture === 'true' ||
|
|
165
|
+
architecture === 'false'
|
|
166
|
+
? `${architecture} (configured)`
|
|
167
|
+
: 'unknown (not configured in static metadata)',
|
|
168
|
+
}
|
|
169
|
+
const hermes = /^hermesEnabled\s*=\s*(true|false)\s*$/m.exec(gradle)
|
|
170
|
+
const newArch = /^newArchEnabled\s*=\s*(true|false)\s*$/m.exec(gradle)
|
|
171
|
+
if (hermes)
|
|
172
|
+
settings.androidJsEngine = `${hermes[1] === 'true' ? 'hermes' : 'jsc'} (gradle.properties)`
|
|
173
|
+
if (newArch) settings.androidNewArchitecture = `${newArch[1]} (gradle.properties)`
|
|
174
|
+
if (resolved.expo)
|
|
175
|
+
settings.expoSdk = `${sanitizeProjectReportVersion(resolved.expo.version)} (${resolved.expo.source}; expo package version)`
|
|
176
|
+
projects.push({
|
|
177
|
+
path,
|
|
178
|
+
name: sanitizeProjectReportName(manifest.name ?? basename(dir)),
|
|
179
|
+
kind: framework === 'bare' ? 'react-native' : framework,
|
|
180
|
+
settings,
|
|
181
|
+
dependencies,
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const folder = basename(dir)
|
|
187
|
+
if (folder.endsWith('.xcodeproj') || folder.endsWith('.xcworkspace')) {
|
|
188
|
+
// xcodeproj's internal workspace is not a second app.
|
|
189
|
+
if (basename(dirname(dir)).endsWith('.xcodeproj')) continue
|
|
190
|
+
const pbx = readProjectMetadata(join(dir, 'project.pbxproj')) ?? ''
|
|
191
|
+
const settings: Record<string, string> = {}
|
|
192
|
+
const deployment = values(pbx, /IPHONEOS_DEPLOYMENT_TARGET\s*=\s*"?([\d.]+)/g)
|
|
193
|
+
const swift = values(pbx, /SWIFT_VERSION\s*=\s*"?([\d.]+)/g)
|
|
194
|
+
settings.iOSDeploymentTarget =
|
|
195
|
+
deployment || 'unknown (workspace or external build settings)'
|
|
196
|
+
settings.swiftVersion = swift || 'unknown (workspace or external build settings)'
|
|
197
|
+
const pins = swiftDependencies(
|
|
198
|
+
join(dir, 'project.xcworkspace/xcshareddata/swiftpm'),
|
|
199
|
+
)
|
|
200
|
+
const workspacePins = swiftDependencies(join(dir, 'xcshareddata/swiftpm'))
|
|
201
|
+
projects.push({
|
|
202
|
+
path,
|
|
203
|
+
name: sanitizeProjectReportName(folder),
|
|
204
|
+
kind: 'native-ios',
|
|
205
|
+
settings,
|
|
206
|
+
dependencies: { ...podDependencies(dirname(dir)), ...pins, ...workspacePins },
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const swiftPackage = readProjectMetadata(join(dir, 'Package.swift'))
|
|
211
|
+
if (swiftPackage) {
|
|
212
|
+
const settings: Record<string, string> = {
|
|
213
|
+
swiftToolsVersion:
|
|
214
|
+
/swift-tools-version:\s*([\d.]+)/.exec(swiftPackage)?.[1] ?? 'unknown',
|
|
215
|
+
iOSDeploymentTarget:
|
|
216
|
+
values(swiftPackage, /\.iOS\(\s*\.v([\d_]+)/g).replace(/_/g, '.') ||
|
|
217
|
+
'unspecified',
|
|
218
|
+
}
|
|
219
|
+
const language = values(swiftPackage, /\.swiftLanguageMode\(\.v(\d+)/g)
|
|
220
|
+
if (language) settings.swiftLanguageVersion = language
|
|
221
|
+
const dependencies = swiftDependencies(dir)
|
|
222
|
+
for (const match of swiftPackage.matchAll(
|
|
223
|
+
/\.package\(\s*url:\s*"[^"]*\/([\w.-]+?)(?:\.git)?"\s*,\s*(?:from:|exact:)\s*"([\d.]+)"/g,
|
|
224
|
+
)) {
|
|
225
|
+
const name = sanitizeProjectReportName(match[1]).toLowerCase()
|
|
226
|
+
if (!dependencies[name])
|
|
227
|
+
dependencies[name] = { version: match[2], source: 'declared' }
|
|
228
|
+
}
|
|
229
|
+
const local = [...swiftPackage.matchAll(/\.package\(\s*path:/g)].length
|
|
230
|
+
if (local) settings.localDependencies = String(local)
|
|
231
|
+
projects.push({
|
|
232
|
+
path,
|
|
233
|
+
name: sanitizeProjectReportName(
|
|
234
|
+
/\bname:\s*"([\w .-]+)"/.exec(swiftPackage)?.[1] ?? basename(dir),
|
|
235
|
+
),
|
|
236
|
+
kind: 'swift-package',
|
|
237
|
+
settings,
|
|
238
|
+
dependencies,
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const gradleSettings =
|
|
243
|
+
readProjectMetadata(join(dir, 'settings.gradle')) ??
|
|
244
|
+
readProjectMetadata(join(dir, 'settings.gradle.kts'))
|
|
245
|
+
if (gradleSettings !== null) {
|
|
246
|
+
const buildFiles = [
|
|
247
|
+
'settings.gradle',
|
|
248
|
+
'settings.gradle.kts',
|
|
249
|
+
'build.gradle',
|
|
250
|
+
'build.gradle.kts',
|
|
251
|
+
'app/build.gradle',
|
|
252
|
+
'app/build.gradle.kts',
|
|
253
|
+
'gradle.properties',
|
|
254
|
+
'gradle/libs.versions.toml',
|
|
255
|
+
]
|
|
256
|
+
.map((file) => readProjectMetadata(join(dir, file)) ?? '')
|
|
257
|
+
.join('\n')
|
|
258
|
+
const wrapper =
|
|
259
|
+
readProjectMetadata(join(dir, 'gradle/wrapper/gradle-wrapper.properties')) ?? ''
|
|
260
|
+
const agp =
|
|
261
|
+
values(buildFiles, /com\.android\.tools\.build:gradle:([\d.]+)/g) ||
|
|
262
|
+
values(
|
|
263
|
+
buildFiles,
|
|
264
|
+
/id\s*(?:\(\s*)?["']com\.android\.(?:application|library)["']\s*\)?\s*version\s*["']([\d.]+)/g,
|
|
265
|
+
) ||
|
|
266
|
+
values(buildFiles, /\bagp\s*=\s*["']([\d.]+)/g)
|
|
267
|
+
const kotlin =
|
|
268
|
+
values(
|
|
269
|
+
buildFiles,
|
|
270
|
+
/(?:kotlinVersion|kotlin_version|kotlin)\s*=\s*["']([\d.]+)/g,
|
|
271
|
+
) ||
|
|
272
|
+
values(
|
|
273
|
+
buildFiles,
|
|
274
|
+
/id\s*(?:\(\s*)?["'](?:org\.jetbrains\.kotlin\.[\w.]+|kotlin-android)["']\s*\)?\s*version\s*["']([\d.]+)/g,
|
|
275
|
+
)
|
|
276
|
+
projects.push({
|
|
277
|
+
path,
|
|
278
|
+
name: sanitizeProjectReportName(basename(dir)),
|
|
279
|
+
kind: 'android',
|
|
280
|
+
settings: {
|
|
281
|
+
gradleVersion:
|
|
282
|
+
/gradle-([\d.]+)-(?:bin|all)\.zip/.exec(wrapper)?.[1] ?? 'unknown',
|
|
283
|
+
androidGradlePluginVersion: agp || 'unknown (not a static version)',
|
|
284
|
+
kotlinVersion: kotlin || 'unknown (not a static version)',
|
|
285
|
+
},
|
|
286
|
+
dependencies: {},
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (scan.truncated) notes.push('Discovery reached a limit; this report is incomplete.')
|
|
291
|
+
if (scan.unreadable)
|
|
292
|
+
notes.push('Some directories could not be read; this report may be incomplete.')
|
|
293
|
+
const manager = detectPackageManager(root)
|
|
294
|
+
const rootManifest = readProjectJson(join(root, 'package.json'))
|
|
295
|
+
const packageManager = metadataValue(rootManifest, 'packageManager')
|
|
296
|
+
const managerVersion =
|
|
297
|
+
typeof packageManager === 'string'
|
|
298
|
+
? (/^(?:bun|pnpm|npm|yarn)@([\d.]+)/.exec(packageManager)?.[1] ?? null)
|
|
299
|
+
: null
|
|
300
|
+
const runtimeVersion = readActiveRuntime()
|
|
301
|
+
return {
|
|
302
|
+
schemaVersion: 1,
|
|
303
|
+
machine: {
|
|
304
|
+
os: `${process.platform} ${release()}`,
|
|
305
|
+
arch: process.arch,
|
|
306
|
+
xcode:
|
|
307
|
+
process.platform === 'darwin' ? toolVersion('xcodebuild', ['-version']) : null,
|
|
308
|
+
node: toolVersion('node'),
|
|
309
|
+
bun: toolVersion('bun'),
|
|
310
|
+
peachCli: sanitizeProjectReportVersion(getCliVersion()),
|
|
311
|
+
peachRuntime: runtimeVersion ? sanitizeProjectReportVersion(runtimeVersion) : null,
|
|
312
|
+
},
|
|
313
|
+
repository: {
|
|
314
|
+
root: '.',
|
|
315
|
+
monorepo: detectMonorepoType(root),
|
|
316
|
+
packageManager: rootManifest ? manager : 'none detected',
|
|
317
|
+
packageManagerVersion: managerVersion,
|
|
318
|
+
directoriesScanned: scan.directories.length,
|
|
319
|
+
truncated: scan.truncated,
|
|
320
|
+
unreadableDirectories: scan.unreadable,
|
|
321
|
+
},
|
|
322
|
+
projects,
|
|
323
|
+
notes,
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function formatProjectReport(report: ProjectReport): string {
|
|
328
|
+
const machine = report.machine
|
|
329
|
+
const lines = [
|
|
330
|
+
'peach project report',
|
|
331
|
+
'',
|
|
332
|
+
`machine: ${machine.os} / ${machine.arch}`,
|
|
333
|
+
`tools: Xcode ${machine.xcode ?? 'not available'}, Node ${machine.node ?? 'not available'}, Bun ${machine.bun ?? 'not available'}`,
|
|
334
|
+
`peach: CLI ${machine.peachCli}, active runtime ${machine.peachRuntime ?? 'not installed'}`,
|
|
335
|
+
`repository: ${report.repository.monorepo}, ${report.repository.packageManager}${report.repository.packageManagerVersion ? ` ${report.repository.packageManagerVersion} (declared)` : ''}`,
|
|
336
|
+
`discovery: ${report.projects.length} app/package entries, ${report.repository.directoriesScanned} directories${report.repository.truncated ? ' (LIMIT REACHED)' : ''}`,
|
|
337
|
+
'',
|
|
338
|
+
]
|
|
339
|
+
for (const entry of report.projects) {
|
|
340
|
+
lines.push(`${entry.path} [${entry.kind}] ${entry.name}`)
|
|
341
|
+
for (const [key, value] of Object.entries(entry.settings))
|
|
342
|
+
lines.push(` ${key}: ${value}`)
|
|
343
|
+
for (const [name, item] of Object.entries(entry.dependencies))
|
|
344
|
+
lines.push(` ${name}: ${item.version} (${item.source})`)
|
|
345
|
+
lines.push('')
|
|
346
|
+
}
|
|
347
|
+
if (!report.projects.length)
|
|
348
|
+
lines.push(
|
|
349
|
+
'No recognized React Native, Expo, native iOS, Android or Swift projects found.',
|
|
350
|
+
'',
|
|
351
|
+
)
|
|
352
|
+
lines.push(
|
|
353
|
+
...report.notes,
|
|
354
|
+
'',
|
|
355
|
+
'To send this report: peach report-issue "<description>"',
|
|
356
|
+
'Review the preview and explicitly confirm before sending.',
|
|
357
|
+
'',
|
|
358
|
+
)
|
|
359
|
+
return lines.join('\n')
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function runProjectReport(args: string[]): number {
|
|
363
|
+
let app: string | undefined
|
|
364
|
+
for (let index = 0; index < args.length; index++) {
|
|
365
|
+
const arg = args[index]
|
|
366
|
+
if (arg === '--json') continue
|
|
367
|
+
if (arg === '--app' && args[index + 1] && !args[index + 1].startsWith('-')) {
|
|
368
|
+
app = args[++index]
|
|
369
|
+
continue
|
|
370
|
+
}
|
|
371
|
+
process.stderr.write(
|
|
372
|
+
`unknown flag or argument: ${arg}\nusage: peach project-report [--app <dir>] [--json]\n`,
|
|
373
|
+
)
|
|
374
|
+
return 1
|
|
375
|
+
}
|
|
376
|
+
const report = createProjectReport(
|
|
377
|
+
app ? resolve(app) : process.cwd(),
|
|
378
|
+
app !== undefined,
|
|
379
|
+
)
|
|
380
|
+
process.stdout.write(
|
|
381
|
+
args.includes('--json')
|
|
382
|
+
? `${JSON.stringify(report, null, 2)}\n`
|
|
383
|
+
: formatProjectReport(report),
|
|
384
|
+
)
|
|
385
|
+
return 0
|
|
386
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type ProjectManifest = {
|
|
2
|
+
name?: string
|
|
3
|
+
dependencies?: Record<string, string>
|
|
4
|
+
devDependencies?: Record<string, string>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type DependencyVersion = {
|
|
8
|
+
version: string
|
|
9
|
+
source: 'installed' | 'lockfile' | 'declared'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ProjectReportEntry = {
|
|
13
|
+
path: string
|
|
14
|
+
name: string
|
|
15
|
+
kind: string
|
|
16
|
+
settings: Record<string, string>
|
|
17
|
+
dependencies: Record<string, DependencyVersion>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ProjectReport = {
|
|
21
|
+
schemaVersion: 1
|
|
22
|
+
machine: {
|
|
23
|
+
os: string
|
|
24
|
+
arch: string
|
|
25
|
+
xcode: string | null
|
|
26
|
+
node: string | null
|
|
27
|
+
bun: string | null
|
|
28
|
+
peachCli: string
|
|
29
|
+
peachRuntime: string | null
|
|
30
|
+
}
|
|
31
|
+
repository: {
|
|
32
|
+
root: '.'
|
|
33
|
+
monorepo: string
|
|
34
|
+
packageManager: string
|
|
35
|
+
packageManagerVersion: string | null
|
|
36
|
+
directoriesScanned: number
|
|
37
|
+
truncated: boolean
|
|
38
|
+
unreadableDirectories: number
|
|
39
|
+
}
|
|
40
|
+
projects: ProjectReportEntry[]
|
|
41
|
+
notes: string[]
|
|
42
|
+
}
|
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
// peach report-issue — submit an explicitly approved compatibility report.
|
|
2
2
|
|
|
3
|
-
import { existsSync
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
4
|
import { resolve } from 'node:path'
|
|
5
5
|
import { getCliVersion } from '../../src/cli-version'
|
|
6
6
|
import { readActiveRuntime } from '../../src/home-paths'
|
|
7
7
|
import { authHeaderValue, resolveCliAuth } from '../auth'
|
|
8
8
|
import { confirm } from '../prompt'
|
|
9
|
+
import { installedManifest, readProjectManifest } from './detect'
|
|
10
|
+
import {
|
|
11
|
+
createProjectReport,
|
|
12
|
+
formatProjectReport,
|
|
13
|
+
sanitizeProjectReportName,
|
|
14
|
+
sanitizeProjectReportVersion,
|
|
15
|
+
} from './project-report'
|
|
16
|
+
import type { ProjectReport } from './projectReportContract'
|
|
9
17
|
import type { ScanResult } from '@peach/compat/web'
|
|
10
18
|
|
|
11
19
|
const DEFAULT_REPORT_ORIGIN = 'https://contrast.dev'
|
|
12
20
|
const MAX_DESCRIPTION_LENGTH = 8_000
|
|
13
21
|
|
|
14
|
-
type PackageManifest = {
|
|
15
|
-
name?: string
|
|
16
|
-
dependencies?: Record<string, string>
|
|
17
|
-
devDependencies?: Record<string, string>
|
|
18
|
-
}
|
|
19
|
-
|
|
20
22
|
type ReportIssueOptions = {
|
|
21
23
|
appDir: string
|
|
22
24
|
description: string
|
|
23
25
|
dryRun: boolean
|
|
24
26
|
yes: boolean
|
|
27
|
+
scopeToApp: boolean
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
export type ReportIssuePayload = {
|
|
@@ -37,6 +40,7 @@ export type ReportIssuePayload = {
|
|
|
37
40
|
platform: NodeJS.Platform
|
|
38
41
|
arch: string
|
|
39
42
|
compatibility: ScanResult | null
|
|
43
|
+
projectReport: ProjectReport
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
|
|
@@ -104,60 +108,38 @@ export function parseReportIssueOptions(
|
|
|
104
108
|
return {
|
|
105
109
|
appDir: resolve(cwd, app ?? '.'),
|
|
106
110
|
description,
|
|
111
|
+
scopeToApp: app !== null,
|
|
107
112
|
dryRun: args.includes('--dry-run'),
|
|
108
113
|
yes: args.includes('--yes') || args.includes('-y'),
|
|
109
114
|
}
|
|
110
115
|
}
|
|
111
116
|
|
|
112
|
-
function readStringRecord(value: unknown): Record<string, string> | undefined {
|
|
113
|
-
if (!value || typeof value !== 'object') return undefined
|
|
114
|
-
const output: Record<string, string> = {}
|
|
115
|
-
for (const key of Object.keys(value)) {
|
|
116
|
-
const next = Object.getOwnPropertyDescriptor(value, key)?.value
|
|
117
|
-
if (typeof next === 'string') output[key] = next
|
|
118
|
-
}
|
|
119
|
-
return output
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function readManifest(appDir: string): PackageManifest | null {
|
|
123
|
-
const path = resolve(appDir, 'package.json')
|
|
124
|
-
if (!existsSync(path)) return null
|
|
125
|
-
|
|
126
|
-
let parsed: unknown
|
|
127
|
-
try {
|
|
128
|
-
parsed = JSON.parse(readFileSync(path, 'utf8'))
|
|
129
|
-
} catch {
|
|
130
|
-
throw new Error(`could not parse ${path}`)
|
|
131
|
-
}
|
|
132
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
133
|
-
throw new Error(`${path} must contain a JSON object`)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const name = Object.getOwnPropertyDescriptor(parsed, 'name')?.value
|
|
137
|
-
const dependencies = Object.getOwnPropertyDescriptor(parsed, 'dependencies')?.value
|
|
138
|
-
const devDependencies = Object.getOwnPropertyDescriptor(
|
|
139
|
-
parsed,
|
|
140
|
-
'devDependencies',
|
|
141
|
-
)?.value
|
|
142
|
-
const dependencyRecord = readStringRecord(dependencies)
|
|
143
|
-
const devDependencyRecord = readStringRecord(devDependencies)
|
|
144
|
-
return {
|
|
145
|
-
...(typeof name === 'string' ? { name } : {}),
|
|
146
|
-
...(dependencyRecord ? { dependencies: dependencyRecord } : {}),
|
|
147
|
-
...(devDependencyRecord ? { devDependencies: devDependencyRecord } : {}),
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
117
|
export async function createReportIssuePayload(
|
|
152
|
-
options: Pick<ReportIssueOptions, 'appDir' | 'description'
|
|
118
|
+
options: Pick<ReportIssueOptions, 'appDir' | 'description'> & { scopeToApp?: boolean },
|
|
153
119
|
now = new Date(),
|
|
154
120
|
): Promise<ReportIssuePayload> {
|
|
155
|
-
const manifest =
|
|
156
|
-
const
|
|
157
|
-
|
|
121
|
+
const manifest = readProjectManifest(options.appDir)
|
|
122
|
+
const installed = manifest ? installedManifest(options.appDir, manifest) : null
|
|
123
|
+
const safeVersions = (deps: Record<string, string> | undefined) =>
|
|
124
|
+
deps &&
|
|
125
|
+
Object.fromEntries(
|
|
126
|
+
Object.entries(deps).map(([name, version]) => [
|
|
127
|
+
name,
|
|
128
|
+
sanitizeProjectReportVersion(version),
|
|
129
|
+
]),
|
|
130
|
+
)
|
|
131
|
+
const compatibility = installed
|
|
132
|
+
? (await import('@peach/compat/web')).scanDeps({
|
|
133
|
+
name: installed.name ? sanitizeProjectReportName(installed.name) : undefined,
|
|
134
|
+
dependencies: safeVersions(installed.dependencies),
|
|
135
|
+
devDependencies: safeVersions(installed.devDependencies),
|
|
136
|
+
})
|
|
137
|
+
: null
|
|
138
|
+
const cliVersion = sanitizeProjectReportVersion(getCliVersion())
|
|
139
|
+
const activeRuntime = readActiveRuntime()
|
|
140
|
+
const runtimeVersion = activeRuntime
|
|
141
|
+
? sanitizeProjectReportVersion(activeRuntime)
|
|
158
142
|
: null
|
|
159
|
-
const cliVersion = getCliVersion()
|
|
160
|
-
const runtimeVersion = readActiveRuntime()
|
|
161
143
|
|
|
162
144
|
return {
|
|
163
145
|
note: options.description,
|
|
@@ -175,6 +157,7 @@ export async function createReportIssuePayload(
|
|
|
175
157
|
platform: process.platform,
|
|
176
158
|
arch: process.arch,
|
|
177
159
|
compatibility,
|
|
160
|
+
projectReport: createProjectReport(options.appDir, options.scopeToApp),
|
|
178
161
|
},
|
|
179
162
|
}
|
|
180
163
|
}
|
|
@@ -243,7 +226,8 @@ export async function runReportIssue(
|
|
|
243
226
|
` environment: CLI ${payload.bundle.cliVersion}, runtime ${
|
|
244
227
|
payload.bundle.runtimeVersion ?? 'not installed'
|
|
245
228
|
}, ${payload.bundle.platform}/${payload.bundle.arch}\n\n` +
|
|
246
|
-
|
|
229
|
+
`${formatProjectReport(payload.bundle.projectReport)}\n` +
|
|
230
|
+
` Includes only the description, compatibility scan, project/dependency metadata, and toolchain details above.\n` +
|
|
247
231
|
` It does not include source files, environment variables, terminal output, logs, screenshots, or app data.\n`,
|
|
248
232
|
)
|
|
249
233
|
|
|
@@ -260,7 +244,7 @@ export async function runReportIssue(
|
|
|
260
244
|
return 2
|
|
261
245
|
}
|
|
262
246
|
const approved = await dependencies.confirmSubmission(
|
|
263
|
-
'send this
|
|
247
|
+
'send this issue and project report to the peach team?',
|
|
264
248
|
false,
|
|
265
249
|
)
|
|
266
250
|
if (!approved) {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { mkdirSync, writeFileSync } from 'fs'
|
|
11
11
|
import { resolve } from 'path'
|
|
12
|
-
import { devices } from 'peach-engine/settings'
|
|
12
|
+
import { devices } from 'peach-engine/settings/devices'
|
|
13
13
|
import { buildResolveRectEval, type SampleRect } from '../browser-evals'
|
|
14
14
|
import { rnxExit } from '../run-rnx'
|
|
15
15
|
import { callInBridge, createBridgeFromParsed, parseBridgeCliArgs } from '../ws-bridge'
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { mkdirSync, writeFileSync } from 'fs'
|
|
10
10
|
import { dirname, resolve } from 'path'
|
|
11
|
-
import { devices, type DeviceModel } from 'peach-engine/settings'
|
|
11
|
+
import { devices, type DeviceModel } from 'peach-engine/settings/devices'
|
|
12
12
|
import { launchReapedChrome } from '../../../../scripts/lib/reap-browser'
|
|
13
13
|
import { DEFAULT_SOOTSIM_BRIDGE_PORT } from '../../src/bridge-constants'
|
|
14
14
|
import { composeFramedScreenshot } from '../../src/screenshots/frame-compose'
|