peachsim 0.1.2210 → 0.1.2213
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/command-registry.ts +1 -0
- package/cli/commands/inspect/settling.ts +1 -1
- package/cli/commands/map.ts +576 -0
- package/cli/commands/runtime.ts +1 -1
- package/cli/main.ts +6 -0
- package/cli/outbound-endpoints.ts +11 -0
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +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/bundle-channel.cjs +16 -7
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/config.mjs +1 -1
- package/dist-lib/detox/index.cjs +6 -3
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +14 -5
- package/dist-lib/expo-swift-metadata.cjs +1 -1
- package/dist-lib/expo-swift-metadata.mjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +999 -811
- package/dist-lib/host/fetch-proxy-handler.cjs +8 -5
- 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 +1953 -752
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1793 -593
- package/dist-lib/native-app-identity-project.cjs +6 -3
- package/dist-lib/native-app-identity-project.mjs +6 -3
- package/dist-lib/native-app-identity.cjs +1 -1
- package/dist-lib/native-app-identity.mjs +1 -1
- package/dist-lib/native-firebase-metadata.cjs +1 -1
- package/dist-lib/native-firebase-metadata.mjs +1 -1
- package/dist-lib/pdfium-raster.cjs +1 -1
- package/dist-lib/pdfium-raster.mjs +1 -1
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/public-origins-lib-esm.json +25 -24
- package/dist-lib/public-origins-lib.json +73 -53
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/reap-browser.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +2 -2
- package/dist-lib/sdk.mjs +2 -2
- package/dist-lib/skills.cjs +1535 -1289
- package/dist-lib/swift-compile.cjs +2 -2
- package/dist-lib/swift-compile.mjs +2 -2
- 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-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +7 -4
- package/dist-lib/vite.cjs +1517 -303
- package/dist-lib/worklet-executable.cjs +1 -1
- package/dist-lib/worklet-executable.mjs +1 -1
- package/dist-types/packages/peach-engine/src/engine/font-config.d.ts +1 -1
- package/dist-types/packages/peach-engine/src/engine/node.d.ts +1 -0
- package/kotlin/sdk.tar.gz +0 -0
- package/package.json +1 -1
- package/runtime-companion.json +1 -1
- package/sdk/ios/System/Library/Frameworks/CoreData.framework/Headers/CoreData.apinotes +35 -0
- package/sdk/ios/System/Library/Frameworks/CoreData.framework/Headers/CoreData.h +8 -0
- package/sdk/ios/System/Library/Frameworks/CoreData.framework/Headers/NSAttributeDescription.h +30 -0
- package/sdk/ios/System/Library/Frameworks/CoreData.framework/Headers/NSEntityDescription.h +14 -0
- package/sdk/ios/System/Library/Frameworks/CoreData.framework/Headers/NSExpressionDescription.h +10 -0
- package/sdk/ios/System/Library/Frameworks/CoreData.framework/Headers/NSFetchIndexDescription.h +13 -0
- package/sdk/ios/System/Library/Frameworks/CoreData.framework/Headers/NSFetchIndexElementDescription.h +18 -0
- package/sdk/ios/System/Library/Frameworks/CoreData.framework/Headers/NSManagedObject.h +11 -0
- package/sdk/ios/System/Library/Frameworks/CoreData.framework/Headers/NSManagedObjectContext.h +19 -0
- package/sdk/ios/System/Library/Frameworks/CoreData.framework/Headers/NSManagedObjectID.h +2 -0
- package/sdk/ios/System/Library/Frameworks/CoreData.framework/Headers/NSManagedObjectModel.h +3 -0
- package/sdk/ios/System/Library/Frameworks/CoreData.framework/Headers/NSMergePolicy.h +17 -0
- package/sdk/ios/System/Library/Frameworks/CoreData.framework/Headers/NSPersistentStore.h +8 -0
- package/sdk/ios/System/Library/Frameworks/CoreData.framework/Headers/NSPersistentStoreCoordinator.h +6 -0
- package/sdk/ios/System/Library/Frameworks/CoreData.framework/Headers/NSPropertyDescription.h +13 -0
- package/sdk/ios/System/Library/Frameworks/CoreData.framework/Headers/NSRelationshipDescription.h +21 -0
- package/sdk/ios/System/Library/Frameworks/UIKit.framework/Headers/UIImage.h +1 -10
- package/sdk/ios/System/Library/Frameworks/UIKit.framework/Headers/UINibLoading.h +1 -1
- package/sdk/ios/System/Library/Frameworks/UIKitTypes.framework/Headers/UIKitTypes.h +11 -0
- package/skills/contrast/SKILL.md +1 -0
- package/src/home-paths.ts +1 -1
- package/src/host/bridge-host.ts +101 -97
- package/src/metro-plugin.ts +4 -1
- package/src/react-native-coverage/native-source-ranges-file.ts +1 -1
- package/src/runtime-assets.ts +0 -1
- package/src/runtime-delivery.ts +1 -1
- package/src/runtime-packs.ts +212 -0
- package/src/swift-shipped-binaries.ts +1 -1
- package/src/swift-standin-config.ts +4 -2
- package/src/vite-plugin-one.ts +4 -1
package/cli/command-registry.ts
CHANGED
|
@@ -541,7 +541,7 @@ export async function waitForSootsimIdle({
|
|
|
541
541
|
const bounds = element.getBoundingClientRect()
|
|
542
542
|
return element.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }) &&
|
|
543
543
|
bounds.width > 0 && bounds.height > 0 && bounds.right > 0 && bounds.bottom > 0 &&
|
|
544
|
-
bounds.left <
|
|
544
|
+
bounds.left < globalThis.innerWidth && bounds.top < globalThis.innerHeight
|
|
545
545
|
})
|
|
546
546
|
if (busyEl) {
|
|
547
547
|
webViewStarting = true
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
// peach map: build a branch screen map screen by screen, or crawl routes
|
|
2
|
+
// by deep link, then publish it to peach cloud (/api/maps). the local
|
|
3
|
+
// working file is peach-map.json with pngs under peach-map-assets/.
|
|
4
|
+
// capture reuses `peach screenshot` so readiness and fidelity match.
|
|
5
|
+
|
|
6
|
+
import { execSync } from 'node:child_process'
|
|
7
|
+
import {
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
statSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from 'node:fs'
|
|
15
|
+
import { dirname, join, resolve } from 'node:path'
|
|
16
|
+
import { isRecord } from '../../src/is-record'
|
|
17
|
+
import { sleep } from '../../src/sleep'
|
|
18
|
+
import { authHeaderValue, resolveCliAuth } from '../auth'
|
|
19
|
+
import { createBridgeFromParsed, parseBridgeCliArgs } from '../ws-bridge'
|
|
20
|
+
import { inspectDescribe, type InspectBridge } from './inspect/core'
|
|
21
|
+
import { runScreenshot } from './screenshot'
|
|
22
|
+
import { resolveDefaultUploadOrigin } from './upload'
|
|
23
|
+
|
|
24
|
+
type LocalScreen = {
|
|
25
|
+
id: string
|
|
26
|
+
route: string | null
|
|
27
|
+
title: string
|
|
28
|
+
state: string | null
|
|
29
|
+
png: string
|
|
30
|
+
source: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type LocalEdge = {
|
|
34
|
+
from: string
|
|
35
|
+
to: string
|
|
36
|
+
label: string | null
|
|
37
|
+
testID: string | null
|
|
38
|
+
source: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type LocalMap = {
|
|
42
|
+
version: 1
|
|
43
|
+
screens: LocalScreen[]
|
|
44
|
+
edges: LocalEdge[]
|
|
45
|
+
gaps: Array<{ screen: string; reason: string }>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const HELP = `
|
|
49
|
+
peach map — capture screens tied to routes and publish the branch map
|
|
50
|
+
|
|
51
|
+
usage:
|
|
52
|
+
peach map add --route /settings --title Settings [--from / --label Settings]
|
|
53
|
+
peach map crawl --scheme myapp:// --routes /,/settings,/profile
|
|
54
|
+
peach map crawl --app ~/my-app [--scheme myapp://] [--routes /,/settings]
|
|
55
|
+
peach map list
|
|
56
|
+
peach map publish --owner acme --repo my-app [--branch main]
|
|
57
|
+
|
|
58
|
+
add options:
|
|
59
|
+
--route <path> route this screen answers to (the screen id)
|
|
60
|
+
--id <id> screen id when it is not the route
|
|
61
|
+
--title <text> display title (default: the route)
|
|
62
|
+
--state <name> a second state of the same screen (e.g. signed-in)
|
|
63
|
+
--from <screen> screen the tap started from (adds an edge)
|
|
64
|
+
--label <text> tapped control label for the edge
|
|
65
|
+
--testid <id> tapped control testID for the edge
|
|
66
|
+
--png <file> use an existing png instead of capturing the sim
|
|
67
|
+
--file <path> working file (default: peach-map.json)
|
|
68
|
+
|
|
69
|
+
crawl options:
|
|
70
|
+
--app <dir> discover routes from the app router dir, scheme from app.json
|
|
71
|
+
--scheme <url> deep link scheme, e.g. myapp:// (required unless --app finds one)
|
|
72
|
+
--routes <list> comma separated routes to open and capture (required unless --app)
|
|
73
|
+
--settle-ms <n> wait per route for the tree to change, 0 to skip (default: 15000)
|
|
74
|
+
|
|
75
|
+
publish options:
|
|
76
|
+
--owner <org> github owner the cloud repo is linked under (required)
|
|
77
|
+
--repo <name> repo name (required)
|
|
78
|
+
--branch <name> branch the map belongs to (default: current git branch)
|
|
79
|
+
--commit <sha> commit the map was captured at (default: git HEAD)
|
|
80
|
+
--device <name> device label (default: default)
|
|
81
|
+
--appearance <a> light | dark (default: light)
|
|
82
|
+
--origin <url> cloud origin (default: your session origin)
|
|
83
|
+
|
|
84
|
+
examples:
|
|
85
|
+
peach open 8081
|
|
86
|
+
peach map add --route / --title Home
|
|
87
|
+
peach do tap-text Settings
|
|
88
|
+
peach map add --route /settings --title Settings --from / --label Settings
|
|
89
|
+
peach map publish --owner acme --repo my-app
|
|
90
|
+
`
|
|
91
|
+
|
|
92
|
+
function flag(args: string[], name: string): string | undefined {
|
|
93
|
+
const index = args.indexOf(name)
|
|
94
|
+
if (index < 0) return undefined
|
|
95
|
+
const value = args[index + 1]
|
|
96
|
+
if (!value || value.startsWith('--')) throw new Error(`${name} requires a value`)
|
|
97
|
+
return value
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function screenKey(id: string, state: string | null): string {
|
|
101
|
+
return state ? `${id}#${state}` : id
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assetName(key: string): string {
|
|
105
|
+
return `${key.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'root'}.png`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readLocalMap(file: string): LocalMap {
|
|
109
|
+
if (!existsSync(file)) return { version: 1, screens: [], edges: [], gaps: [] }
|
|
110
|
+
const parsed: unknown = JSON.parse(readFileSync(file, 'utf8'))
|
|
111
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.screens)) {
|
|
112
|
+
throw new Error(`${file} is not a peach map file`)
|
|
113
|
+
}
|
|
114
|
+
const screens: LocalScreen[] = []
|
|
115
|
+
for (const raw of parsed.screens) {
|
|
116
|
+
if (!isRecord(raw)) continue
|
|
117
|
+
if (typeof raw.id !== 'string' || typeof raw.title !== 'string') continue
|
|
118
|
+
if (typeof raw.png !== 'string') continue
|
|
119
|
+
screens.push({
|
|
120
|
+
id: raw.id,
|
|
121
|
+
route: typeof raw.route === 'string' ? raw.route : null,
|
|
122
|
+
title: raw.title,
|
|
123
|
+
state: typeof raw.state === 'string' ? raw.state : null,
|
|
124
|
+
png: raw.png,
|
|
125
|
+
source: typeof raw.source === 'string' ? raw.source : 'manual',
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
const edges: LocalEdge[] = []
|
|
129
|
+
if (Array.isArray(parsed.edges)) {
|
|
130
|
+
for (const raw of parsed.edges) {
|
|
131
|
+
if (!isRecord(raw)) continue
|
|
132
|
+
if (typeof raw.from !== 'string' || typeof raw.to !== 'string') continue
|
|
133
|
+
edges.push({
|
|
134
|
+
from: raw.from,
|
|
135
|
+
to: raw.to,
|
|
136
|
+
label: typeof raw.label === 'string' ? raw.label : null,
|
|
137
|
+
testID: typeof raw.testID === 'string' ? raw.testID : null,
|
|
138
|
+
source: typeof raw.source === 'string' ? raw.source : 'manual',
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const gaps: LocalMap['gaps'] = []
|
|
143
|
+
if (Array.isArray(parsed.gaps)) {
|
|
144
|
+
for (const raw of parsed.gaps) {
|
|
145
|
+
if (!isRecord(raw)) continue
|
|
146
|
+
if (typeof raw.screen !== 'string' || typeof raw.reason !== 'string') continue
|
|
147
|
+
gaps.push({ screen: raw.screen, reason: raw.reason })
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { version: 1, screens, edges, gaps }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function writeLocalMap(file: string, map: LocalMap): void {
|
|
154
|
+
writeFileSync(file, `${JSON.stringify(map, null, 2)}\n`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function upsertScreen(map: LocalMap, screen: LocalScreen, edge: LocalEdge | null): void {
|
|
158
|
+
const key = screenKey(screen.id, screen.state)
|
|
159
|
+
const index = map.screens.findIndex((entry) => screenKey(entry.id, entry.state) === key)
|
|
160
|
+
if (index >= 0) map.screens[index] = screen
|
|
161
|
+
else map.screens.push(screen)
|
|
162
|
+
map.gaps = map.gaps.filter((gap) => gap.screen !== key)
|
|
163
|
+
if (edge && !map.edges.some((e) => e.from === edge.from && e.to === edge.to)) {
|
|
164
|
+
map.edges.push(edge)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// a crawl is rerun as the app grows: one gap per screen, the newest reason.
|
|
169
|
+
function recordGaps(file: string, gaps: LocalMap['gaps']): void {
|
|
170
|
+
const map = readLocalMap(file)
|
|
171
|
+
const named = new Set(gaps.map((gap) => gap.screen))
|
|
172
|
+
map.gaps = [...map.gaps.filter((gap) => !named.has(gap.screen)), ...gaps]
|
|
173
|
+
writeLocalMap(file, map)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function captureTo(
|
|
177
|
+
pngPath: string,
|
|
178
|
+
opts: { port?: number; verbose?: boolean },
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
mkdirSync(dirname(pngPath), { recursive: true })
|
|
181
|
+
await runScreenshot(['--output', pngPath], opts)
|
|
182
|
+
if (!existsSync(pngPath)) throw new Error(`screenshot did not write ${pngPath}`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function runMap(
|
|
186
|
+
args: string[],
|
|
187
|
+
opts: { port?: number; verbose?: boolean },
|
|
188
|
+
): Promise<number> {
|
|
189
|
+
try {
|
|
190
|
+
return await dispatch(args, opts)
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error(
|
|
193
|
+
` map failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
194
|
+
)
|
|
195
|
+
return 1
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function dispatch(
|
|
200
|
+
args: string[],
|
|
201
|
+
opts: { port?: number; verbose?: boolean },
|
|
202
|
+
): Promise<number> {
|
|
203
|
+
const [verb, ...rest] = args
|
|
204
|
+
if (!verb || verb === '--help' || verb === '-h' || verb === 'help') {
|
|
205
|
+
console.log(HELP)
|
|
206
|
+
return 0
|
|
207
|
+
}
|
|
208
|
+
const file = resolve(flag(rest, '--file') ?? 'peach-map.json')
|
|
209
|
+
if (verb === 'add') return addScreen(file, rest, opts, 'manual')
|
|
210
|
+
if (verb === 'crawl') return crawl(file, rest, opts)
|
|
211
|
+
if (verb === 'list') {
|
|
212
|
+
const map = readLocalMap(file)
|
|
213
|
+
for (const screen of map.screens) {
|
|
214
|
+
console.log(
|
|
215
|
+
` ${screenKey(screen.id, screen.state)} ${screen.title}${screen.route ? ` (${screen.route})` : ''}`,
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
for (const edge of map.edges) {
|
|
219
|
+
console.log(
|
|
220
|
+
` ${edge.from} -> ${edge.to}${edge.label ? ` tap "${edge.label}"` : ''}${edge.testID ? ` #${edge.testID}` : ''}`,
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
for (const gap of map.gaps) console.log(` gap: ${gap.screen} — ${gap.reason}`)
|
|
224
|
+
console.log(` ${map.screens.length} screens, ${map.edges.length} edges`)
|
|
225
|
+
return 0
|
|
226
|
+
}
|
|
227
|
+
if (verb === 'publish') return publish(file, rest)
|
|
228
|
+
console.error(` unknown map verb: ${verb}\n${HELP}`)
|
|
229
|
+
return 1
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function addScreen(
|
|
233
|
+
file: string,
|
|
234
|
+
args: string[],
|
|
235
|
+
opts: { port?: number; verbose?: boolean },
|
|
236
|
+
source: string,
|
|
237
|
+
prebuilt?: { id: string; route: string; title: string },
|
|
238
|
+
): Promise<number> {
|
|
239
|
+
const route = prebuilt?.route ?? flag(args, '--route')
|
|
240
|
+
const id = prebuilt?.id ?? flag(args, '--id') ?? route
|
|
241
|
+
if (!id) throw new Error('peach map add needs --route or --id')
|
|
242
|
+
const title = prebuilt?.title ?? flag(args, '--title') ?? route ?? id
|
|
243
|
+
const state = flag(args, '--state') ?? null
|
|
244
|
+
const key = screenKey(id, state)
|
|
245
|
+
const assetDir = join(dirname(file), 'peach-map-assets')
|
|
246
|
+
const assetPath = join(assetDir, assetName(key))
|
|
247
|
+
const givenPng = flag(args, '--png')
|
|
248
|
+
if (givenPng) {
|
|
249
|
+
mkdirSync(assetDir, { recursive: true })
|
|
250
|
+
writeFileSync(assetPath, readFileSync(resolve(givenPng)))
|
|
251
|
+
} else {
|
|
252
|
+
await captureTo(assetPath, opts)
|
|
253
|
+
}
|
|
254
|
+
const from = flag(args, '--from')
|
|
255
|
+
const map = readLocalMap(file)
|
|
256
|
+
upsertScreen(
|
|
257
|
+
map,
|
|
258
|
+
{
|
|
259
|
+
id,
|
|
260
|
+
route: route ?? null,
|
|
261
|
+
title,
|
|
262
|
+
state,
|
|
263
|
+
png: `peach-map-assets/${assetName(key)}`,
|
|
264
|
+
source,
|
|
265
|
+
},
|
|
266
|
+
from
|
|
267
|
+
? {
|
|
268
|
+
from,
|
|
269
|
+
to: key,
|
|
270
|
+
label: flag(args, '--label') ?? null,
|
|
271
|
+
testID: flag(args, '--testid') ?? null,
|
|
272
|
+
source,
|
|
273
|
+
}
|
|
274
|
+
: null,
|
|
275
|
+
)
|
|
276
|
+
writeLocalMap(file, map)
|
|
277
|
+
console.log(` mapped ${key} (${title}) -> ${file}`)
|
|
278
|
+
return 0
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const ROUTE_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js']
|
|
282
|
+
const PLATFORM_SUFFIX_RE = /\.(native|ios|android|web)(?=\.[^.]+$)/
|
|
283
|
+
const RENDER_MODE_SUFFIX_RE = /\+(ssg|ssr|spa)$/
|
|
284
|
+
|
|
285
|
+
export type DiscoveredAppRoutes = {
|
|
286
|
+
routes: string[]
|
|
287
|
+
gaps: Array<{ screen: string; reason: string }>
|
|
288
|
+
scheme: string | null
|
|
289
|
+
routerRoot: string | null
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function readAppJsonScheme(appRoot: string): string | null {
|
|
293
|
+
try {
|
|
294
|
+
const parsed: unknown = JSON.parse(readFileSync(join(appRoot, 'app.json'), 'utf8'))
|
|
295
|
+
if (!isRecord(parsed)) return null
|
|
296
|
+
const expo = parsed.expo
|
|
297
|
+
if (!isRecord(expo)) return null
|
|
298
|
+
const scheme = expo.scheme
|
|
299
|
+
if (typeof scheme === 'string' && scheme.trim()) return scheme.trim()
|
|
300
|
+
if (Array.isArray(scheme)) {
|
|
301
|
+
const first = scheme.find(
|
|
302
|
+
(entry): entry is string => typeof entry === 'string' && entry.trim().length > 0,
|
|
303
|
+
)
|
|
304
|
+
return first?.trim() ?? null
|
|
305
|
+
}
|
|
306
|
+
return null
|
|
307
|
+
} catch {
|
|
308
|
+
return null
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function walkRouteFiles(root: string): string[] {
|
|
313
|
+
const found: string[] = []
|
|
314
|
+
const visit = (dir: string) => {
|
|
315
|
+
for (const entry of readdirSync(dir)) {
|
|
316
|
+
const full = join(dir, entry)
|
|
317
|
+
let stat: ReturnType<typeof statSync>
|
|
318
|
+
try {
|
|
319
|
+
stat = statSync(full)
|
|
320
|
+
} catch {
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
323
|
+
if (stat.isDirectory()) visit(full)
|
|
324
|
+
else if (stat.isFile()) found.push(full)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
visit(root)
|
|
328
|
+
return found.sort()
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// file routes follow the one/expo-router conventions in
|
|
332
|
+
// src/bundler/routeDiscovery.ts: groups vanish from the url, index names the
|
|
333
|
+
// directory, layouts/api/middleware/not-found are not screens. dynamic
|
|
334
|
+
// segments become gaps: a deep link cannot invent their values.
|
|
335
|
+
export function discoverDeepLinkRoutes(appRoot: string): DiscoveredAppRoutes {
|
|
336
|
+
const root = resolve(appRoot)
|
|
337
|
+
const scheme = readAppJsonScheme(root)
|
|
338
|
+
const routerRoot = ['app', 'src/app']
|
|
339
|
+
.map((leaf) => join(root, leaf))
|
|
340
|
+
.find((candidate) => {
|
|
341
|
+
try {
|
|
342
|
+
return statSync(candidate).isDirectory()
|
|
343
|
+
} catch {
|
|
344
|
+
return false
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
if (!routerRoot) return { routes: [], gaps: [], scheme, routerRoot: null }
|
|
348
|
+
const byRoute = new Map<string, { priority: number; file: string }>()
|
|
349
|
+
const gaps: DiscoveredAppRoutes['gaps'] = []
|
|
350
|
+
for (const file of walkRouteFiles(routerRoot)) {
|
|
351
|
+
if (!ROUTE_EXTENSIONS.some((ext) => file.endsWith(ext))) continue
|
|
352
|
+
const relative = file.slice(routerRoot.length + 1)
|
|
353
|
+
const segments = relative.split('/')
|
|
354
|
+
const name = segments.pop() ?? ''
|
|
355
|
+
const platform = PLATFORM_SUFFIX_RE.exec(name)?.[1]
|
|
356
|
+
if (platform === 'web') continue
|
|
357
|
+
const base = name
|
|
358
|
+
.replace(PLATFORM_SUFFIX_RE, '')
|
|
359
|
+
.replace(/\.[^.]+$/, '')
|
|
360
|
+
.replace(RENDER_MODE_SUFFIX_RE, '')
|
|
361
|
+
if (base === '_layout' || base === '+middleware' || base === '+not-found') continue
|
|
362
|
+
if (base === '+html' || base === '+native-intent') continue
|
|
363
|
+
if (base.endsWith('+api')) continue
|
|
364
|
+
const publicSegments = segments.filter(
|
|
365
|
+
(segment) => !(segment.startsWith('(') && segment.endsWith(')')),
|
|
366
|
+
)
|
|
367
|
+
const parts = base === 'index' ? publicSegments : [...publicSegments, base]
|
|
368
|
+
const dynamic = parts.find((part) => part.startsWith('[') && part.endsWith(']'))
|
|
369
|
+
if (dynamic) {
|
|
370
|
+
gaps.push({
|
|
371
|
+
screen: `/${parts.join('/')}`,
|
|
372
|
+
reason: `dynamic segment ${dynamic} needs a value; pass --routes to capture one`,
|
|
373
|
+
})
|
|
374
|
+
continue
|
|
375
|
+
}
|
|
376
|
+
const routePath = `/${parts.join('/')}`.replace(/\/+$/, '') || '/'
|
|
377
|
+
const priority = !platform ? 0 : platform === 'native' ? 1 : 2
|
|
378
|
+
const existing = byRoute.get(routePath)
|
|
379
|
+
if (!existing || priority < existing.priority) {
|
|
380
|
+
byRoute.set(routePath, { priority, file })
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const routes = [...byRoute.keys()].sort((a, b) => {
|
|
384
|
+
if (a === '/') return -1
|
|
385
|
+
if (b === '/') return 1
|
|
386
|
+
const depthA = a.split('/').length
|
|
387
|
+
const depthB = b.split('/').length
|
|
388
|
+
return depthA === depthB ? a.localeCompare(b) : depthA - depthB
|
|
389
|
+
})
|
|
390
|
+
return { routes, gaps, scheme, routerRoot }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const DEFAULT_CRAWL_SETTLE_MS = 15000
|
|
394
|
+
const SETTLE_POLL_MS = 500
|
|
395
|
+
|
|
396
|
+
function treeDigest(tree: string): string {
|
|
397
|
+
let hash = 5381
|
|
398
|
+
for (let index = 0; index < tree.length; index++) {
|
|
399
|
+
hash = ((hash << 5) + hash + tree.charCodeAt(index)) | 0
|
|
400
|
+
}
|
|
401
|
+
return `${tree.length}:${hash}`
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// a deep link lands after openUrl returns: metro may still be
|
|
405
|
+
// transforming the route while the old screen looks settled, so the
|
|
406
|
+
// screenshot gate alone captures one navigation late. wait for the
|
|
407
|
+
// tree to actually change, bounded; identical screens never change
|
|
408
|
+
// it, so they fall through to the screenshot gate.
|
|
409
|
+
export async function settleRoute(
|
|
410
|
+
bridge: InspectBridge,
|
|
411
|
+
settleMs: number,
|
|
412
|
+
): Promise<void> {
|
|
413
|
+
const dump = { describe: true, verbose: false, filter: '', compact: true, hideXy: true }
|
|
414
|
+
let before: string | null = null
|
|
415
|
+
try {
|
|
416
|
+
const snapshot = await inspectDescribe(bridge, dump)
|
|
417
|
+
if (!snapshot.tree) return
|
|
418
|
+
before = treeDigest(snapshot.tree)
|
|
419
|
+
} catch {
|
|
420
|
+
return
|
|
421
|
+
}
|
|
422
|
+
const deadline = Date.now() + Math.max(0, settleMs)
|
|
423
|
+
while (Date.now() < deadline) {
|
|
424
|
+
await sleep(SETTLE_POLL_MS)
|
|
425
|
+
try {
|
|
426
|
+
const snapshot = await inspectDescribe(bridge, dump)
|
|
427
|
+
if (snapshot.tree && treeDigest(snapshot.tree) !== before) return
|
|
428
|
+
} catch {
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function crawl(
|
|
435
|
+
file: string,
|
|
436
|
+
args: string[],
|
|
437
|
+
opts: { port?: number; verbose?: boolean },
|
|
438
|
+
): Promise<number> {
|
|
439
|
+
const appRoot = flag(args, '--app')
|
|
440
|
+
let scheme = flag(args, '--scheme')
|
|
441
|
+
const routesRaw = flag(args, '--routes')
|
|
442
|
+
let routes: string[]
|
|
443
|
+
if (appRoot) {
|
|
444
|
+
const discovered = discoverDeepLinkRoutes(appRoot)
|
|
445
|
+
if (!scheme) scheme = discovered.scheme ?? undefined
|
|
446
|
+
if (!scheme) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`peach map crawl found no scheme in ${resolve(appRoot)}/app.json; pass --scheme myapp://`,
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
if (routesRaw) {
|
|
452
|
+
routes = routesRaw
|
|
453
|
+
.split(',')
|
|
454
|
+
.map((route) => route.trim())
|
|
455
|
+
.filter(Boolean)
|
|
456
|
+
} else {
|
|
457
|
+
if (!discovered.routerRoot) {
|
|
458
|
+
throw new Error(
|
|
459
|
+
`peach map crawl found no app/ router dir under ${resolve(appRoot)}; pass --routes /,...`,
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
if (discovered.routes.length === 0) {
|
|
463
|
+
throw new Error(
|
|
464
|
+
`peach map crawl found no static routes under ${discovered.routerRoot}; pass --routes /,...`,
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
routes = discovered.routes
|
|
468
|
+
if (discovered.gaps.length > 0) recordGaps(file, discovered.gaps)
|
|
469
|
+
console.log(` discovered ${routes.length} routes under ${discovered.routerRoot}`)
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
if (!scheme || !routesRaw) {
|
|
473
|
+
throw new Error('peach map crawl needs --scheme and --routes, or --app <dir>')
|
|
474
|
+
}
|
|
475
|
+
routes = routesRaw
|
|
476
|
+
.split(',')
|
|
477
|
+
.map((route) => route.trim())
|
|
478
|
+
.filter(Boolean)
|
|
479
|
+
}
|
|
480
|
+
const base = scheme.endsWith('://') ? scheme : `${scheme}://`
|
|
481
|
+
const bridge = createBridgeFromParsed(
|
|
482
|
+
parseBridgeCliArgs([], { port: opts.port, commandTimeoutMs: 30000 }),
|
|
483
|
+
)
|
|
484
|
+
const settleRaw = Number(flag(args, '--settle-ms'))
|
|
485
|
+
const settleMs =
|
|
486
|
+
Number.isFinite(settleRaw) && settleRaw >= 0 ? settleRaw : DEFAULT_CRAWL_SETTLE_MS
|
|
487
|
+
let captured = 0
|
|
488
|
+
let previous: string | null = null
|
|
489
|
+
try {
|
|
490
|
+
for (const route of routes) {
|
|
491
|
+
const url = `${base}${route.replace(/^\//, '')}`
|
|
492
|
+
try {
|
|
493
|
+
await bridge.send({ type: 'openUrl', url })
|
|
494
|
+
await settleRoute(bridge, settleMs)
|
|
495
|
+
const code = await addScreen(
|
|
496
|
+
file,
|
|
497
|
+
previous ? ['--from', previous] : [],
|
|
498
|
+
opts,
|
|
499
|
+
'crawl',
|
|
500
|
+
{ id: route, route, title: route },
|
|
501
|
+
)
|
|
502
|
+
if (code === 0) {
|
|
503
|
+
captured++
|
|
504
|
+
previous = route
|
|
505
|
+
}
|
|
506
|
+
} catch (error) {
|
|
507
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
508
|
+
recordGaps(file, [{ screen: route, reason: `crawl failed: ${reason}` }])
|
|
509
|
+
console.error(` gap ${route}: ${reason}`)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
} finally {
|
|
513
|
+
bridge.close()
|
|
514
|
+
}
|
|
515
|
+
console.log(` crawled ${captured}/${routes.length} routes`)
|
|
516
|
+
return captured > 0 ? 0 : 1
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function gitValue(command: string): string | undefined {
|
|
520
|
+
try {
|
|
521
|
+
return (
|
|
522
|
+
execSync(command, { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
523
|
+
.toString()
|
|
524
|
+
.trim() || undefined
|
|
525
|
+
)
|
|
526
|
+
} catch {
|
|
527
|
+
return undefined
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function publish(file: string, args: string[]): Promise<number> {
|
|
532
|
+
const owner = flag(args, '--owner')
|
|
533
|
+
const repo = flag(args, '--repo')
|
|
534
|
+
if (!owner || !repo) throw new Error('peach map publish needs --owner and --repo')
|
|
535
|
+
const auth = resolveCliAuth()
|
|
536
|
+
if (!auth) throw new Error('peach map publish needs `peach login` or PEACH_API_KEY')
|
|
537
|
+
const origin = await resolveDefaultUploadOrigin(flag(args, '--origin'))
|
|
538
|
+
const map = readLocalMap(file)
|
|
539
|
+
if (map.screens.length === 0) throw new Error(`${file} has no screens to publish`)
|
|
540
|
+
const dir = dirname(file)
|
|
541
|
+
const body = {
|
|
542
|
+
version: 1,
|
|
543
|
+
owner,
|
|
544
|
+
repo,
|
|
545
|
+
branch:
|
|
546
|
+
flag(args, '--branch') ?? gitValue('git rev-parse --abbrev-ref HEAD') ?? 'main',
|
|
547
|
+
commitSha: flag(args, '--commit') ?? gitValue('git rev-parse HEAD') ?? null,
|
|
548
|
+
device: flag(args, '--device') ?? 'default',
|
|
549
|
+
appearance: flag(args, '--appearance') === 'dark' ? 'dark' : 'light',
|
|
550
|
+
mode: map.screens.some((screen) => screen.source === 'crawl') ? 'crawl' : 'manual',
|
|
551
|
+
screens: map.screens.map((screen) => ({
|
|
552
|
+
id: screen.id,
|
|
553
|
+
route: screen.route,
|
|
554
|
+
title: screen.title,
|
|
555
|
+
state: screen.state,
|
|
556
|
+
source: screen.source,
|
|
557
|
+
screenshotPngBase64: readFileSync(join(dir, screen.png)).toString('base64'),
|
|
558
|
+
})),
|
|
559
|
+
edges: map.edges,
|
|
560
|
+
gaps: map.gaps,
|
|
561
|
+
}
|
|
562
|
+
const res = await fetch(`${origin.replace(/\/$/, '')}/api/maps`, {
|
|
563
|
+
method: 'POST',
|
|
564
|
+
headers: {
|
|
565
|
+
authorization: authHeaderValue(auth),
|
|
566
|
+
'content-type': 'application/json',
|
|
567
|
+
},
|
|
568
|
+
body: JSON.stringify(body),
|
|
569
|
+
})
|
|
570
|
+
if (!res.ok) {
|
|
571
|
+
throw new Error(`publish failed (${res.status}): ${(await res.text()).slice(0, 300)}`)
|
|
572
|
+
}
|
|
573
|
+
const url = `${origin.replace(/\/$/, '')}/org/${owner}/${repo}/map?branch=${encodeURIComponent(body.branch)}&appearance=${body.appearance}`
|
|
574
|
+
console.log(` published ${map.screens.length} screens: ${url}`)
|
|
575
|
+
return 0
|
|
576
|
+
}
|
package/cli/commands/runtime.ts
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'fs'
|
|
|
4
4
|
// runtimes are versioned tarballs hosted behind the configured CDN origin
|
|
5
5
|
// (default https://contrast.dev/runtimes/). each install:
|
|
6
6
|
// 1. fetches manifest.json to resolve a version (or confirm the literal)
|
|
7
|
-
// 2. downloads sootsim-runtime-<version>.tar.
|
|
7
|
+
// 2. downloads sootsim-runtime-<version>.tar.zst to an operation temp directory
|
|
8
8
|
// 3. verifies sha256
|
|
9
9
|
// 4. extracts to ~/.peach/runtimes/<version>/
|
|
10
10
|
// 5. (optionally) sets it active + tells the daemon to hot-swap
|
package/cli/main.ts
CHANGED
|
@@ -512,6 +512,12 @@ try {
|
|
|
512
512
|
await exitWithFlush(typeof code === 'number' ? code : 0)
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
+
case 'map': {
|
|
516
|
+
const { runMap } = await import('./commands/map')
|
|
517
|
+
const code = await runMap(commandArgsWithSim, { port, verbose })
|
|
518
|
+
await exitWithFlush(code)
|
|
519
|
+
}
|
|
520
|
+
|
|
515
521
|
case 'screenshot': {
|
|
516
522
|
const { runScreenshotCommand } = await import('./commands/screenshot-command')
|
|
517
523
|
const code = await runScreenshotCommand(commandArgsWithSim, { port, verbose })
|
|
@@ -421,6 +421,8 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
|
|
|
421
421
|
},
|
|
422
422
|
'packages/peachsim/cli/commands/preview.ts :: fetch :: `${endpoint}?id=${encodeURIComponent()}`':
|
|
423
423
|
{ category: 'contrast_user_action', count: 2 },
|
|
424
|
+
"packages/peachsim/cli/commands/map.ts :: fetch :: `${origin.replace(/\\/$/, '')}/api/maps`":
|
|
425
|
+
{ category: 'contrast_user_action', count: 1 },
|
|
424
426
|
// the live-dev-stack publisher fetches a cloudflared binary only when none
|
|
425
427
|
// is installed; the token proxy below it forwards to the user's own
|
|
426
428
|
// loopback dev server and sidecars, never anywhere remote.
|
|
@@ -750,6 +752,10 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
|
|
|
750
752
|
category: 'guest_app_network',
|
|
751
753
|
count: 1,
|
|
752
754
|
},
|
|
755
|
+
'packages/peach-engine/src/guest/host-fetch.ts :: fetch :: input': {
|
|
756
|
+
category: 'guest_app_network',
|
|
757
|
+
count: 1,
|
|
758
|
+
},
|
|
753
759
|
'packages/peach-engine/src/render-worker/rive-animation-clock.ts :: fetch :: wasmUrl': {
|
|
754
760
|
category: 'same_origin_runtime',
|
|
755
761
|
count: 1,
|
|
@@ -1034,6 +1040,11 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
|
|
|
1034
1040
|
category: 'kotlin_app_library_sources',
|
|
1035
1041
|
count: 1,
|
|
1036
1042
|
},
|
|
1043
|
+
// the idle warm of the android emoji face, fetched from the engine's own font directory.
|
|
1044
|
+
'packages/peach-engine/src/engine/android-emoji-font.ts :: fetch-alias :: url': {
|
|
1045
|
+
category: 'same_origin_runtime',
|
|
1046
|
+
count: 1,
|
|
1047
|
+
},
|
|
1037
1048
|
'packages/peach-engine/src/engine/app-font-loader.ts :: fetch-alias :: url': {
|
|
1038
1049
|
category: 'local_development',
|
|
1039
1050
|
count: 1,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! peach v0.1.
|
|
1
|
+
/*! peach v0.1.2213 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = '', __sootsim_import_meta_filename = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; __sootsim_import_meta_filename = __filename; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! peach v0.1.
|
|
1
|
+
/*! peach v0.1.2213 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = '', __sootsim_import_meta_filename = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; __sootsim_import_meta_filename = __filename; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! peach v0.1.
|
|
1
|
+
/*! peach v0.1.2213 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = '', __sootsim_import_meta_filename = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; __sootsim_import_meta_filename = __filename; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! peach v0.1.
|
|
1
|
+
/*! peach v0.1.2213 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = '', __sootsim_import_meta_filename = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; __sootsim_import_meta_filename = __filename; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! peach v0.1.
|
|
1
|
+
/*! peach v0.1.2213 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = '', __sootsim_import_meta_filename = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; __sootsim_import_meta_filename = __filename; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|