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/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,
|
|
@@ -4639,6 +4641,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
4639
4641
|
}
|
|
4640
4642
|
} catch (err: any) {
|
|
4641
4643
|
rethrowIfExit(err)
|
|
4644
|
+
trackCliError(err)
|
|
4642
4645
|
const message = err instanceof Error ? err.message : String(err)
|
|
4643
4646
|
console.error(` ${subcommand ?? 'peach'} failed: ${message}`)
|
|
4644
4647
|
const unknownSim = /^no sim connected with id ([^;]+)(?:; connected sims: .+)?$/.exec(
|
|
@@ -4704,6 +4707,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
|
|
|
4704
4707
|
}
|
|
4705
4708
|
rnxExit(1)
|
|
4706
4709
|
} finally {
|
|
4710
|
+
if (subcommand !== 'list') await collectRuntimeDiagnostics(bridge)
|
|
4707
4711
|
bridge.close()
|
|
4708
4712
|
}
|
|
4709
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())
|
|
@@ -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) {
|
package/cli/commands/setup.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from '../../src/home-paths'
|
|
18
18
|
import { rnxRuntime } from '../../src/runtime-delivery'
|
|
19
19
|
import { findDesktopCompanion } from '../desktop-companion'
|
|
20
|
+
import { canPromptForPrivacyChoice } from '../privacy'
|
|
20
21
|
import { confirm, select } from '../prompt'
|
|
21
22
|
import {
|
|
22
23
|
applyRepositorySetup,
|
|
@@ -342,11 +343,12 @@ async function decidePrivacy(
|
|
|
342
343
|
): Promise<boolean> {
|
|
343
344
|
if (options.productAnalytics !== undefined) return options.productAnalytics
|
|
344
345
|
if (current.configured) return current.productAnalytics
|
|
345
|
-
if (options.yes || options.ci || !
|
|
346
|
+
if (options.yes || options.ci || !canPromptForPrivacyChoice()) return false
|
|
346
347
|
printPromptBlock('Private by default', [
|
|
347
|
-
'Diagnostics stay off unless you opt in.',
|
|
348
|
-
'Reports
|
|
349
|
-
'They
|
|
348
|
+
'Diagnostics stay off unless you opt in, including CI, pipes and agent runs. DO_NOT_TRACK overrides opt-in.',
|
|
349
|
+
'Reports include CLI/runtime versions, OS/arch, command name and a random install ID.',
|
|
350
|
+
'They include package incompatibilities, error classes and first-line error/warning messages (300 characters, paths redacted).',
|
|
351
|
+
'Reports exclude arguments, stacks, app source and structured app data.',
|
|
350
352
|
])
|
|
351
353
|
return confirm('Share bounded diagnostics to improve peach?', false)
|
|
352
354
|
}
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
// product analytics preference shown by guided setup and Settings.
|
|
3
3
|
//
|
|
4
4
|
// this is the process-wide preference; `DO_NOT_TRACK=1` still forces opt-out
|
|
5
|
-
// for a single run regardless of
|
|
6
|
-
// telemetry.ts, the same cross-tool signal npm/turbo/supabase use).
|
|
5
|
+
// for a single run regardless of stored consent, including CI and agents.
|
|
7
6
|
|
|
8
7
|
import {
|
|
9
8
|
configFilePath,
|
|
@@ -16,7 +15,13 @@ function printStatus(): void {
|
|
|
16
15
|
const preferences = readPrivacyPreferences()
|
|
17
16
|
const enabled = preferences.configured && preferences.productAnalytics
|
|
18
17
|
console.log(` bounded diagnostics are ${enabled ? 'ON' : 'OFF'}`)
|
|
19
|
-
console.log('
|
|
18
|
+
console.log(' fields: CLI/runtime versions, OS/arch, command name, random install ID')
|
|
19
|
+
console.log(
|
|
20
|
+
' diagnostics: package name/version/unsupported API; error class and first-line error/warning messages (300 characters, paths redacted)',
|
|
21
|
+
)
|
|
22
|
+
console.log(
|
|
23
|
+
' explicit opt-in also applies in CI, pipes and agent runs; DO_NOT_TRACK wins',
|
|
24
|
+
)
|
|
20
25
|
console.log(` config: ${configFilePath()}`)
|
|
21
26
|
const doNotTrack = process.env.DO_NOT_TRACK
|
|
22
27
|
if (doNotTrack === '1' || doNotTrack === 'true') {
|