peachsim 0.1.577 → 0.1.579
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/LICENSE +1 -1
- package/README.md +11 -9
- package/cli/app-catalog.ts +77 -0
- package/cli/app-fonts.ts +4 -20
- package/cli/app-splash.ts +2 -18
- package/cli/bridge-flow-runner.ts +505 -139
- package/cli/cloud-client.ts +1 -4
- package/cli/cloud-session.ts +1 -4
- package/cli/cloud-test-result-files.ts +3 -0
- package/cli/cloud-test-tunnel.ts +2 -5
- package/cli/command-registry.ts +2 -0
- package/cli/commands/agent.ts +149 -23
- package/cli/commands/assert.ts +1 -4
- package/cli/commands/box/peach-command.ts +62 -0
- package/cli/commands/box/plane-share.ts +0 -70
- package/cli/commands/box/rnx-command.ts +1 -60
- package/cli/commands/box/search-command.ts +14 -0
- package/cli/commands/box.ts +2 -2
- package/cli/commands/control.ts +121 -27
- package/cli/commands/cpu-profile.ts +23 -10
- package/cli/commands/daemon.ts +1 -9
- package/cli/commands/detox.ts +26 -18
- package/cli/commands/device.ts +129 -1
- package/cli/commands/film.ts +25 -16
- package/cli/commands/flow.ts +9 -2
- package/cli/commands/inspect/actions.ts +75 -1
- package/cli/commands/inspect/core.ts +81 -2
- package/cli/commands/inspect/describe.ts +2 -6
- package/cli/commands/inspect/env.ts +1 -1
- package/cli/commands/inspect/settling.ts +20 -3
- package/cli/commands/inspect/shared.ts +1 -4
- package/cli/commands/inspect/sleep.ts +1 -1
- package/cli/commands/inspect.ts +16 -49
- package/cli/commands/install-desktop.ts +8 -6
- package/cli/commands/login.ts +3 -5
- package/cli/commands/maestro-generate.ts +1 -4
- package/cli/commands/maestro.ts +38 -21
- package/cli/commands/no-bridge-hint.ts +0 -14
- package/cli/commands/permissions.ts +2 -2
- package/cli/commands/platform.ts +13 -6
- package/cli/commands/screenshot.ts +6 -1
- package/cli/commands/skills.ts +1 -1
- package/cli/commands/test.ts +6 -2
- package/cli/commands/three-mode.ts +162 -19
- package/cli/commands/upload.ts +3 -18
- package/cli/current-sim.ts +0 -1
- package/cli/desktop-companion.ts +40 -8
- package/cli/drivers/playwright-provisioning.ts +1 -4
- package/cli/flow-export.ts +1 -8
- package/cli/flow-session.ts +0 -4
- package/cli/hints.ts +1 -16
- package/cli/inspect-notice-state.ts +0 -4
- package/cli/main.ts +2 -4
- package/cli/outbound-endpoints.ts +1 -24
- package/cli/parse-args.ts +1 -2
- package/cli/rnx-in-process.ts +3 -1
- package/cli/run-rnx-core.ts +4 -2
- package/cli/run-rnx.ts +18 -10
- package/cli/send-to-box.ts +1 -4
- package/cli/telemetry.ts +1 -1
- package/cli/ws-bridge.ts +1 -5
- package/detox/gestures.ts +20 -6
- package/detox/index.ts +379 -123
- package/detox/navigation-lifecycle.ts +22 -13
- package/dist-lib/agent-daemon-client.cjs +21 -15
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +3 -17
- package/dist-lib/agent-sessions.cjs +5 -4
- package/dist-lib/attached-projects.cjs +1 -77
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +11 -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 +7 -5
- package/dist-lib/bridge-contract-input.mjs +7 -5
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +30 -7
- package/dist-lib/capture-contract.mjs +30 -7
- 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 +17 -11
- package/dist-lib/cloud.mjs +17 -11
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +321 -122
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +427 -212
- package/dist-lib/host/fetch-proxy-handler.cjs +5 -28
- 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 +315 -58
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/menu.cjs +67 -31
- package/dist-lib/menu.mjs +64 -28
- package/dist-lib/metro-fingerprint-registry.cjs +8 -4
- package/dist-lib/metro-fingerprint-registry.mjs +6 -4
- package/dist-lib/metro-production-bundle.cjs +2 -2
- package/dist-lib/metro-production-bundle.mjs +2 -2
- package/dist-lib/metro.cjs +8 -4
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +7 -4
- 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 +3 -13
- package/dist-lib/scripts/dev-server-scanner.cjs +5 -17
- package/dist-lib/sdk.cjs +129 -10
- package/dist-lib/sdk.mjs +127 -10
- package/dist-lib/skills.cjs +1635 -1121
- package/dist-lib/swift-compile.cjs +277 -70
- package/dist-lib/swift-compile.mjs +275 -70
- package/dist-lib/swift-package.cjs +17 -5
- package/dist-lib/swift-package.mjs +15 -4
- package/dist-lib/swift-submit.cjs +32 -8
- package/dist-lib/swift-submit.mjs +29 -7
- package/dist-lib/swift.cjs +30912 -577
- package/dist-lib/vite-kotlin.cjs +356 -0
- package/dist-lib/vite-swift.cjs +1743 -0
- package/dist-lib/vite.cjs +208 -1039
- package/kotlin/README.md +18 -0
- package/kotlin/Sources/androidx/annotation/Keep.kt +4 -0
- package/kotlin/Sources/dev/onejs/one/OneNativeSourceDispatch.kt +7 -0
- package/kotlin/Sources/dev/onejs/one/source/OneModule.kt +4 -0
- package/kotlin/Sources/org/json/JSON.kt +244 -0
- package/kotlin/toolchain.json +19 -0
- package/package.json +14 -2
- package/scripts/dev-server-scanner.ts +0 -18
- package/scripts/optional-demo-registry.ts +0 -17
- package/skills/contrast/SKILL.md +2 -2
- package/skills/peach-setup/SKILL.md +16 -0
- package/src/agent-daemon-client.ts +36 -22
- package/src/agent-identity.ts +0 -15
- package/src/agent-prompt.ts +12 -0
- package/src/attached-projects.ts +0 -72
- package/src/backend-origin.ts +10 -0
- package/src/bridge-contract-input.ts +2 -5
- package/src/bridge-contract.ts +2 -0
- package/src/capture-contract.ts +53 -11
- package/src/cloud.ts +4 -9
- package/src/dev-server-open.ts +0 -99
- package/src/home-paths.ts +2 -3
- package/src/host/agent-host.ts +82 -36
- package/src/host/bridge-host.ts +5 -11
- package/src/host/fetch-proxy-handler.ts +19 -43
- package/src/is-record.ts +4 -0
- package/src/menu.ts +82 -37
- package/src/metro-fingerprint-registry.ts +1 -4
- package/src/metro-production-bundle.ts +1 -1
- package/src/plist.ts +1 -4
- package/src/public-brand.ts +5 -3
- package/src/render-mode.ts +0 -12
- package/src/runtime-assets.ts +9 -2
- package/src/screenshots/compose.ts +7 -1
- package/src/screenshots/frame-compose.ts +21 -10
- package/src/screenshots/schema.ts +1 -4
- package/src/sim-client.ts +2 -4
- package/src/skills/builtin/maestro-test.ts +1 -4
- package/src/sleep.ts +4 -0
- package/src/swift-compile.ts +209 -47
- package/src/swift-icu.ts +140 -0
- package/src/swift-package.ts +26 -2
- package/src/swift-submit.ts +84 -4
- package/src/swift.ts +2 -0
- package/src/vite-plugin-kotlin.ts +376 -0
- package/src/vite-plugin-one.ts +224 -36
- package/src/vite-plugin-swift.ts +382 -96
- package/src/vite-plugin.ts +121 -134
- package/src/vite.d.ts +10 -0
- package/cli/resolve-assets.ts +0 -63
- package/src/rolldown-plugin-swift.ts +0 -244
- package/src/team-machine-prompt.ts +0 -73
package/src/attached-projects.ts
CHANGED
|
@@ -97,9 +97,6 @@ export function setUserDataDir(dir: string | null): void {
|
|
|
97
97
|
overrideDir = dir
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
/** back-compat alias retained for existing test imports. */
|
|
101
|
-
export const __setUserDataDirForTests = setUserDataDir
|
|
102
|
-
|
|
103
100
|
/** resolve the user-data directory where attached-projects.json lives.
|
|
104
101
|
* precedence:
|
|
105
102
|
* 1. explicit override from setUserDataDir() (tests, CLI bootstrap)
|
|
@@ -205,10 +202,6 @@ function newSessionId(): string {
|
|
|
205
202
|
return `s_${randomBytes(10).toString('hex')}`
|
|
206
203
|
}
|
|
207
204
|
|
|
208
|
-
function newPreviewId(): string {
|
|
209
|
-
return `pa_${randomBytes(10).toString('hex')}`
|
|
210
|
-
}
|
|
211
|
-
|
|
212
205
|
// --- project CRUD ---
|
|
213
206
|
|
|
214
207
|
/** upsert by canonicalized cwd — cwd is the unique index. `input.id` is
|
|
@@ -271,30 +264,6 @@ export function findProjectById(id: string): AttachedProject | null {
|
|
|
271
264
|
return loadStore().attachedProjects.find((p) => p.id === id) ?? null
|
|
272
265
|
}
|
|
273
266
|
|
|
274
|
-
export function findProjectByCwd(cwd: string): AttachedProject | null {
|
|
275
|
-
const resolved = path.resolve(cwd)
|
|
276
|
-
return loadStore().attachedProjects.find((p) => p.cwd === resolved) ?? null
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
export function findProjectByBundleUrl(bundleUrl: string): AttachedProject | null {
|
|
280
|
-
// first exact match, then prefix match. the prefix path handles metro query
|
|
281
|
-
// param drift (?platform=ios&dev=true) so the same base URL binds.
|
|
282
|
-
const store = loadStore()
|
|
283
|
-
const exact = store.attachedProjects.find((p) => p.knownBundleUrls.includes(bundleUrl))
|
|
284
|
-
if (exact) return exact
|
|
285
|
-
const base = stripQuery(bundleUrl)
|
|
286
|
-
return (
|
|
287
|
-
store.attachedProjects.find((p) =>
|
|
288
|
-
p.knownBundleUrls.some((u) => stripQuery(u) === base),
|
|
289
|
-
) ?? null
|
|
290
|
-
)
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function stripQuery(url: string): string {
|
|
294
|
-
const q = url.indexOf('?')
|
|
295
|
-
return q >= 0 ? url.slice(0, q) : url
|
|
296
|
-
}
|
|
297
|
-
|
|
298
267
|
export function listProjects(): AttachedProject[] {
|
|
299
268
|
return loadStore().attachedProjects
|
|
300
269
|
}
|
|
@@ -452,47 +421,6 @@ export function deleteSession(id: string): void {
|
|
|
452
421
|
})
|
|
453
422
|
}
|
|
454
423
|
|
|
455
|
-
// --- preview attachment CRUD ---
|
|
456
|
-
|
|
457
|
-
export function upsertPreviewAttachment(
|
|
458
|
-
input: Partial<PreviewAttachment> & { bundleUrl: string; simId: string },
|
|
459
|
-
): PreviewAttachment {
|
|
460
|
-
let result!: PreviewAttachment
|
|
461
|
-
mutateStore((store) => {
|
|
462
|
-
const existing = store.previewAttachments.find(
|
|
463
|
-
(pa) => pa.bundleUrl === input.bundleUrl && pa.simId === input.simId,
|
|
464
|
-
)
|
|
465
|
-
if (existing) {
|
|
466
|
-
const merged: PreviewAttachment = {
|
|
467
|
-
...existing,
|
|
468
|
-
...input,
|
|
469
|
-
lastSeenAt: Date.now(),
|
|
470
|
-
}
|
|
471
|
-
const idx = store.previewAttachments.indexOf(existing)
|
|
472
|
-
store.previewAttachments[idx] = merged
|
|
473
|
-
result = merged
|
|
474
|
-
return
|
|
475
|
-
}
|
|
476
|
-
const created: PreviewAttachment = {
|
|
477
|
-
id: input.id ?? newPreviewId(),
|
|
478
|
-
projectId: input.projectId ?? null,
|
|
479
|
-
bundleUrl: input.bundleUrl,
|
|
480
|
-
simId: input.simId,
|
|
481
|
-
deviceModel: input.deviceModel ?? 'unknown',
|
|
482
|
-
status: input.status ?? 'connecting',
|
|
483
|
-
lastSeenAt: Date.now(),
|
|
484
|
-
}
|
|
485
|
-
store.previewAttachments.push(created)
|
|
486
|
-
result = created
|
|
487
|
-
})
|
|
488
|
-
return result
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
export function listPreviewAttachments(projectId?: string): PreviewAttachment[] {
|
|
492
|
-
const all = loadStore().previewAttachments
|
|
493
|
-
return projectId ? all.filter((pa) => pa.projectId === projectId) : all
|
|
494
|
-
}
|
|
495
|
-
|
|
496
424
|
export function deletePreviewAttachment(id: string): void {
|
|
497
425
|
mutateStore((store) => {
|
|
498
426
|
store.previewAttachments = store.previewAttachments.filter((pa) => pa.id !== id)
|
package/src/backend-origin.ts
CHANGED
|
@@ -29,6 +29,16 @@ export function isLoopbackHost(hostname: string): boolean {
|
|
|
29
29
|
)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// the origin of a url whose host is loopback, or null for any other or unparsable url
|
|
33
|
+
export function loopbackOriginOf(value: string): string | null {
|
|
34
|
+
try {
|
|
35
|
+
const url = new URL(value)
|
|
36
|
+
return isLoopbackHost(url.hostname) ? url.origin : null
|
|
37
|
+
} catch {
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
export function buildBrowserAssetProxyUrl(targetUrl: string): string {
|
|
33
43
|
const proxyPath = isLoopbackHost(new URL(targetUrl).hostname)
|
|
34
44
|
? '/__bundle-proxy'
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
} from './bridge-contract'
|
|
14
14
|
import type { SimPerformStep, SimScreenshotOptions } from './sim-client'
|
|
15
15
|
import type { SootSimReplayTarget } from '@peach/globals'
|
|
16
|
+
import { isRecord } from './is-record'
|
|
16
17
|
|
|
17
18
|
/** the caller sent a value this bridge argument cannot be read from. */
|
|
18
19
|
export class BridgeInputError extends Error {
|
|
@@ -28,12 +29,8 @@ function validationFailure(path: string, expectation: string): never {
|
|
|
28
29
|
throw new BridgeInputError(`${path} ${expectation}`)
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
function isRecordValue(value: unknown): value is Record<string, unknown> {
|
|
32
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
32
|
function recordValue(value: unknown, path: string): Record<string, unknown> {
|
|
36
|
-
if (!
|
|
33
|
+
if (!isRecord(value)) {
|
|
37
34
|
validationFailure(path, 'must be an object')
|
|
38
35
|
}
|
|
39
36
|
return value
|
package/src/bridge-contract.ts
CHANGED
|
@@ -471,6 +471,8 @@ export interface SimSemanticNode {
|
|
|
471
471
|
role?: string
|
|
472
472
|
label?: string
|
|
473
473
|
pressable?: true
|
|
474
|
+
/** painted but hidden from the accessibility tree, as VoiceOver sees it */
|
|
475
|
+
accessibilityHidden?: true
|
|
474
476
|
geometry: SimSemanticGeometry
|
|
475
477
|
children?: SimSemanticNode[]
|
|
476
478
|
}
|
package/src/capture-contract.ts
CHANGED
|
@@ -243,13 +243,14 @@ export function rnxRasterFrames(
|
|
|
243
243
|
width: node.layout.width,
|
|
244
244
|
height: node.layout.height,
|
|
245
245
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
246
|
+
// a region is what the node paints, and nothing else: a producer that crops
|
|
247
|
+
// a composited screenshot copies every pixel inside it, so any margin past
|
|
248
|
+
// the paint carries neighbours (a header title, a hairline, a list row)
|
|
249
|
+
// frozen at their own positions over the ones the tree redraws. glass
|
|
250
|
+
// paints its shape plus the box shadows the material casts (shadow,
|
|
251
|
+
// tallShadow, contour). its capturePadding is the backdrop the shader
|
|
252
|
+
// samples for refraction, which it never paints, so it is not an outset.
|
|
253
|
+
const glassOutset = liquidGlassPaintOutset(node.style._liquidGlass, frame)
|
|
253
254
|
const shadowRadius =
|
|
254
255
|
typeof node.style.shadowRadius === 'number' ? node.style.shadowRadius : 0
|
|
255
256
|
const shadowOffset = node.style.shadowOffset
|
|
@@ -267,19 +268,21 @@ export function rnxRasterFrames(
|
|
|
267
268
|
: 0
|
|
268
269
|
const left = Math.max(
|
|
269
270
|
0,
|
|
270
|
-
frame.x - Math.max(
|
|
271
|
+
frame.x - Math.max(glassOutset.x, shadowRadius - shadowOffsetX, 0),
|
|
271
272
|
)
|
|
272
273
|
const top = Math.max(
|
|
273
274
|
0,
|
|
274
|
-
frame.y - Math.max(
|
|
275
|
+
frame.y - Math.max(glassOutset.top, shadowRadius - shadowOffsetY, 0),
|
|
275
276
|
)
|
|
276
277
|
const right = Math.min(
|
|
277
278
|
tree.width,
|
|
278
|
-
frame.x + frame.width + Math.max(
|
|
279
|
+
frame.x + frame.width + Math.max(glassOutset.x, shadowRadius + shadowOffsetX, 0),
|
|
279
280
|
)
|
|
280
281
|
const bottom = Math.min(
|
|
281
282
|
tree.height,
|
|
282
|
-
frame.y +
|
|
283
|
+
frame.y +
|
|
284
|
+
frame.height +
|
|
285
|
+
Math.max(glassOutset.bottom, shadowRadius + shadowOffsetY, 0),
|
|
283
286
|
)
|
|
284
287
|
if (right > left && bottom > top) {
|
|
285
288
|
frames.set(node.nodeId, {
|
|
@@ -293,6 +296,45 @@ export function rnxRasterFrames(
|
|
|
293
296
|
return frames
|
|
294
297
|
}
|
|
295
298
|
|
|
299
|
+
// how far a liquid glass material paints past its shape: each box shadow it
|
|
300
|
+
// casts (liquid-glass.ts drawLiquidGlassBoxShadow) is its rrect grown by spread,
|
|
301
|
+
// moved by offsetY, and blurred to about three sigma. the tall shadow only
|
|
302
|
+
// draws once the minor axis passes 62pt.
|
|
303
|
+
function liquidGlassPaintOutset(
|
|
304
|
+
glass: unknown,
|
|
305
|
+
frame: RnxCaptureRect,
|
|
306
|
+
): { x: number; top: number; bottom: number } {
|
|
307
|
+
const outset = { x: 0, top: 0, bottom: 0 }
|
|
308
|
+
if (!glass || typeof glass !== 'object') return outset
|
|
309
|
+
const contour = Reflect.get(glass, 'contour')
|
|
310
|
+
const shadows = [
|
|
311
|
+
Reflect.get(glass, 'shadow'),
|
|
312
|
+
Math.min(frame.width, frame.height) > 62 ? Reflect.get(glass, 'tallShadow') : null,
|
|
313
|
+
...(Array.isArray(contour) ? contour : []),
|
|
314
|
+
]
|
|
315
|
+
for (const shadow of shadows) {
|
|
316
|
+
if (!shadow || typeof shadow !== 'object') continue
|
|
317
|
+
const sigma = Reflect.get(shadow, 'sigma')
|
|
318
|
+
if (typeof sigma !== 'number') continue
|
|
319
|
+
const offsetY = Reflect.get(shadow, 'offsetY')
|
|
320
|
+
const spread = Reflect.get(shadow, 'spread')
|
|
321
|
+
const [spreadX, spreadY] =
|
|
322
|
+
typeof spread === 'number'
|
|
323
|
+
? [spread, spread]
|
|
324
|
+
: Array.isArray(spread) &&
|
|
325
|
+
typeof spread[0] === 'number' &&
|
|
326
|
+
typeof spread[1] === 'number'
|
|
327
|
+
? [spread[0], spread[1]]
|
|
328
|
+
: [0, 0]
|
|
329
|
+
const dy = typeof offsetY === 'number' ? offsetY : 0
|
|
330
|
+
const blur = sigma * 3
|
|
331
|
+
outset.x = Math.max(outset.x, spreadX + blur)
|
|
332
|
+
outset.top = Math.max(outset.top, spreadY + blur - dy)
|
|
333
|
+
outset.bottom = Math.max(outset.bottom, spreadY + blur + dy)
|
|
334
|
+
}
|
|
335
|
+
return outset
|
|
336
|
+
}
|
|
337
|
+
|
|
296
338
|
export function rnxCaptureRasterRegions(
|
|
297
339
|
capture: RnxScreenCapture,
|
|
298
340
|
): RnxCaptureRegionRequest[] {
|
package/src/cloud.ts
CHANGED
|
@@ -25,7 +25,9 @@ import {
|
|
|
25
25
|
type RnxCloudCreateReceipt,
|
|
26
26
|
type RnxCloudScopedToken,
|
|
27
27
|
} from './cloud-contract'
|
|
28
|
+
import { isRecord } from './is-record'
|
|
28
29
|
import { rnxPublicBrand } from './public-brand'
|
|
30
|
+
import { sleep } from './sleep'
|
|
29
31
|
import type { SimStateOptions } from './bridge-contract'
|
|
30
32
|
|
|
31
33
|
export {
|
|
@@ -121,10 +123,6 @@ interface RemoteSimulatorSession {
|
|
|
121
123
|
scopes: readonly RnxCloudCommandScope[]
|
|
122
124
|
}
|
|
123
125
|
|
|
124
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
125
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
126
|
function normalizeCloudOrigin(source: string): string {
|
|
129
127
|
let parsed: URL
|
|
130
128
|
try {
|
|
@@ -210,10 +208,6 @@ function readRetryAfterMs(response: Response, text: string): number | null {
|
|
|
210
208
|
return null
|
|
211
209
|
}
|
|
212
210
|
|
|
213
|
-
function sleep(ms: number): Promise<void> {
|
|
214
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
215
|
-
}
|
|
216
|
-
|
|
217
211
|
// one fetch with the cloud retry policy: `rate_limited` waits out its
|
|
218
212
|
// `retry-after` and 5xx goes again at once, each at most twice, and nothing
|
|
219
213
|
// else is retried. the body is buffered only on a retryable status, so every
|
|
@@ -320,7 +314,8 @@ function readSimulatorCreate(
|
|
|
320
314
|
claim: RnxCloudClaim
|
|
321
315
|
artifact: RnxCloudBoxCreateReceipt['artifact']
|
|
322
316
|
} {
|
|
323
|
-
if (!isRecord(value))
|
|
317
|
+
if (!isRecord(value))
|
|
318
|
+
throw new Error('Peach Cloud create returned an invalid simulator')
|
|
324
319
|
const claim = readClaim(value.claim)
|
|
325
320
|
if (
|
|
326
321
|
typeof value.simId !== 'string' ||
|
package/src/dev-server-open.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from 'node:child_process'
|
|
2
2
|
import { createHash } from 'node:crypto'
|
|
3
3
|
import fs from 'node:fs'
|
|
4
|
-
import http from 'node:http'
|
|
5
4
|
import path from 'node:path'
|
|
6
5
|
import { fileURLToPath } from 'node:url'
|
|
7
6
|
|
|
8
7
|
export type RnxDesktopOpenOption = boolean | { appName: string }
|
|
9
8
|
|
|
10
|
-
const scheduledMetroOpens = new Set<string>()
|
|
11
|
-
|
|
12
9
|
export function resolveRnxDesktopAppName(
|
|
13
10
|
open: RnxDesktopOpenOption | undefined,
|
|
14
11
|
projectRoot: string,
|
|
@@ -100,99 +97,3 @@ export function launchRnxDesktop(bundleUrl: string, appName: string): void {
|
|
|
100
97
|
)
|
|
101
98
|
observeRnxDesktopLaunch(child, appName)
|
|
102
99
|
}
|
|
103
|
-
|
|
104
|
-
interface MetroDesktopOpenOptions {
|
|
105
|
-
port: number
|
|
106
|
-
bundleUrl: string
|
|
107
|
-
appName: string
|
|
108
|
-
projectRoot: string
|
|
109
|
-
serverId: string
|
|
110
|
-
intervalMs?: number
|
|
111
|
-
timeoutMs?: number
|
|
112
|
-
launch?: (bundleUrl: string, appName: string) => void
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function openRnxDesktopWhenMetroIsReady({
|
|
116
|
-
port,
|
|
117
|
-
bundleUrl,
|
|
118
|
-
appName,
|
|
119
|
-
projectRoot,
|
|
120
|
-
serverId,
|
|
121
|
-
intervalMs = 100,
|
|
122
|
-
timeoutMs = 30_000,
|
|
123
|
-
launch = launchRnxDesktop,
|
|
124
|
-
}: MetroDesktopOpenOptions): Promise<boolean> {
|
|
125
|
-
const key = `${projectRoot}\0${port}`
|
|
126
|
-
if (scheduledMetroOpens.has(key)) return Promise.resolve(false)
|
|
127
|
-
scheduledMetroOpens.add(key)
|
|
128
|
-
const deadline = Date.now() + timeoutMs
|
|
129
|
-
|
|
130
|
-
return new Promise((resolve) => {
|
|
131
|
-
let finished = false
|
|
132
|
-
let retryTimer: ReturnType<typeof setTimeout> | undefined
|
|
133
|
-
const finish = (opened: boolean) => {
|
|
134
|
-
if (finished) return
|
|
135
|
-
finished = true
|
|
136
|
-
clearTimeout(retryTimer)
|
|
137
|
-
scheduledMetroOpens.delete(key)
|
|
138
|
-
resolve(opened)
|
|
139
|
-
}
|
|
140
|
-
const retry = () => {
|
|
141
|
-
if (finished) return
|
|
142
|
-
if (Date.now() >= deadline) {
|
|
143
|
-
console.error(
|
|
144
|
-
`[peach] could not open ${appName}: Metro did not become ready on port ${port}`,
|
|
145
|
-
)
|
|
146
|
-
finish(false)
|
|
147
|
-
return
|
|
148
|
-
}
|
|
149
|
-
retryTimer = setTimeout(check, intervalMs)
|
|
150
|
-
}
|
|
151
|
-
const check = () => {
|
|
152
|
-
const request = http.get(
|
|
153
|
-
`http://127.0.0.1:${port}/__server-scan`,
|
|
154
|
-
{ headers: { Accept: 'application/json' } },
|
|
155
|
-
(response) => {
|
|
156
|
-
let body = ''
|
|
157
|
-
response.setEncoding('utf8')
|
|
158
|
-
response.on('data', (chunk: string) => {
|
|
159
|
-
body = `${body}${chunk}`.slice(0, 65_536)
|
|
160
|
-
})
|
|
161
|
-
response.once('end', () => {
|
|
162
|
-
try {
|
|
163
|
-
const scan: unknown = JSON.parse(body)
|
|
164
|
-
const matches =
|
|
165
|
-
response.statusCode === 200 &&
|
|
166
|
-
Array.isArray(scan) &&
|
|
167
|
-
scan.some((entry: unknown) => {
|
|
168
|
-
if (!entry || typeof entry !== 'object') return false
|
|
169
|
-
const scannedPort = Reflect.get(entry, 'port')
|
|
170
|
-
const projectName = Reflect.get(entry, 'projectName')
|
|
171
|
-
const scannedServerId = Reflect.get(entry, 'rnxServerId')
|
|
172
|
-
const scannedBundle = Reflect.get(entry, 'bundleUrl')
|
|
173
|
-
if (
|
|
174
|
-
scannedPort !== port ||
|
|
175
|
-
projectName !== appName ||
|
|
176
|
-
scannedServerId !== serverId ||
|
|
177
|
-
typeof scannedBundle !== 'string'
|
|
178
|
-
) {
|
|
179
|
-
return false
|
|
180
|
-
}
|
|
181
|
-
return new URL(scannedBundle).searchParams.get('rnxAppName') === appName
|
|
182
|
-
})
|
|
183
|
-
if (matches) {
|
|
184
|
-
launch(bundleUrl, appName)
|
|
185
|
-
finish(true)
|
|
186
|
-
return
|
|
187
|
-
}
|
|
188
|
-
} catch {}
|
|
189
|
-
retry()
|
|
190
|
-
})
|
|
191
|
-
},
|
|
192
|
-
)
|
|
193
|
-
request.setTimeout(Math.min(1_000, timeoutMs), () => request.destroy())
|
|
194
|
-
request.once('error', retry)
|
|
195
|
-
}
|
|
196
|
-
check()
|
|
197
|
-
})
|
|
198
|
-
}
|
package/src/home-paths.ts
CHANGED
|
@@ -432,9 +432,8 @@ export interface DaemonLockfile {
|
|
|
432
432
|
/** epoch-ms of last heartbeat. daemons update this every ~5s; readers
|
|
433
433
|
* treat the lockfile as stale if now - heartbeatAt > DAEMON_HEARTBEAT_STALE_MS. */
|
|
434
434
|
heartbeatAt: number
|
|
435
|
-
/** true while the daemon is
|
|
436
|
-
* boot.
|
|
437
|
-
* false before treating the daemon as ready to serve. */
|
|
435
|
+
/** true while the daemon has no usable runtime and is fetching one on first
|
|
436
|
+
* boot. an installed runtime serves while an update downloads. */
|
|
438
437
|
bootstrapping?: boolean
|
|
439
438
|
}
|
|
440
439
|
|
package/src/host/agent-host.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// { type: 'agent:event', sessionId, event }
|
|
14
14
|
// { type: 'agent:session-status', session }
|
|
15
15
|
|
|
16
|
+
import { randomUUID } from 'node:crypto'
|
|
16
17
|
import fs from 'node:fs'
|
|
17
18
|
import path from 'node:path'
|
|
18
19
|
import {
|
|
@@ -43,12 +44,8 @@ import {
|
|
|
43
44
|
type AgentSession,
|
|
44
45
|
type AttachedProject,
|
|
45
46
|
} from '../attached-projects.ts'
|
|
46
|
-
import {
|
|
47
|
-
sendPromptToTeamMachineSession,
|
|
48
|
-
teamMachineSessionIdFromCliIdentity,
|
|
49
|
-
} from '../team-machine-prompt.ts'
|
|
50
47
|
import type { AgentEvent } from '../agent-events.ts'
|
|
51
|
-
import type { AgentPromptEnvelope } from '../agent-prompt.ts'
|
|
48
|
+
import type { AgentInspectMessage, AgentPromptEnvelope } from '../agent-prompt.ts'
|
|
52
49
|
import type { WebSocket } from 'ws'
|
|
53
50
|
|
|
54
51
|
// ws.readyState constant — avoid importing the module just for this enum.
|
|
@@ -70,15 +67,12 @@ export interface AgentHostOptions {
|
|
|
70
67
|
* the rest of the repo uses, so `peach serve` standalone does the
|
|
71
68
|
* right thing. */
|
|
72
69
|
getExcludePorts?: () => number[]
|
|
73
|
-
/** returns the active CLI lease for a sim. used only for prompt routing to
|
|
74
|
-
* an external Team Machine agent that claimed the tab. */
|
|
75
|
-
resolveCliLease?: (simId: string) => AgentCliLeaseSnapshot | null
|
|
76
70
|
}
|
|
77
71
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
72
|
+
interface InspectListener {
|
|
73
|
+
socket: WebSocket
|
|
74
|
+
resolve: (message: AgentInspectMessage) => void
|
|
75
|
+
reject: (error: Error) => void
|
|
82
76
|
}
|
|
83
77
|
|
|
84
78
|
/** default excludes keep the dev-server scanner from hitting Contrast's own
|
|
@@ -105,6 +99,8 @@ export class AgentHost {
|
|
|
105
99
|
// pushes so a CLI `peach agent sessions --watch` can react to state
|
|
106
100
|
// changes driven by another client.
|
|
107
101
|
private allSockets = new Set<WebSocket>()
|
|
102
|
+
private inspectQueues = new Map<string, AgentInspectMessage[]>()
|
|
103
|
+
private inspectListeners = new Map<string, InspectListener[]>()
|
|
108
104
|
// agent wrappers also emit prompt-received from the FIFO reader, but the
|
|
109
105
|
// shell/daemon already knows the user-facing display text when send-prompt
|
|
110
106
|
// is accepted. emit the friendly prompt immediately and suppress the raw
|
|
@@ -139,6 +135,18 @@ export class AgentHost {
|
|
|
139
135
|
}
|
|
140
136
|
this.sessionsBySocket.delete(ws)
|
|
141
137
|
}
|
|
138
|
+
for (const [simId, listeners] of this.inspectListeners) {
|
|
139
|
+
const remaining: InspectListener[] = []
|
|
140
|
+
for (const listener of listeners) {
|
|
141
|
+
if (listener.socket === ws) {
|
|
142
|
+
listener.reject(new AgentSessionError('DISCONNECT', 'listener disconnected'))
|
|
143
|
+
} else {
|
|
144
|
+
remaining.push(listener)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (remaining.length > 0) this.inspectListeners.set(simId, remaining)
|
|
148
|
+
else this.inspectListeners.delete(simId)
|
|
149
|
+
}
|
|
142
150
|
this.allSockets.delete(ws)
|
|
143
151
|
}
|
|
144
152
|
|
|
@@ -188,6 +196,13 @@ export class AgentHost {
|
|
|
188
196
|
}
|
|
189
197
|
this.subscriptions.clear()
|
|
190
198
|
this.sessionsBySocket.clear()
|
|
199
|
+
for (const listeners of this.inspectListeners.values()) {
|
|
200
|
+
for (const listener of listeners) {
|
|
201
|
+
listener.reject(new AgentSessionError('DISCONNECT', 'daemon closed'))
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
this.inspectListeners.clear()
|
|
205
|
+
this.inspectQueues.clear()
|
|
191
206
|
this.allSockets.clear()
|
|
192
207
|
}
|
|
193
208
|
|
|
@@ -208,8 +223,17 @@ export class AgentHost {
|
|
|
208
223
|
return listSessions(msg.projectId ? String(msg.projectId) : undefined)
|
|
209
224
|
case 'agent:start-session':
|
|
210
225
|
return this.doStartSession(msg.input ?? {})
|
|
211
|
-
case 'agent:
|
|
212
|
-
return this.
|
|
226
|
+
case 'agent:listen-inspect':
|
|
227
|
+
return this.listenForInspect(ws, this.requireSimId(msg, type))
|
|
228
|
+
case 'agent:get-inspect-listener-status': {
|
|
229
|
+
const simId = this.requireSimId(msg, type)
|
|
230
|
+
return { simId, listening: (this.inspectListeners.get(simId)?.length ?? 0) > 0 }
|
|
231
|
+
}
|
|
232
|
+
case 'agent:send-inspect-message':
|
|
233
|
+
return this.sendInspectMessage(
|
|
234
|
+
this.requireSimId(msg, type),
|
|
235
|
+
this.normalizePromptEnvelope(msg),
|
|
236
|
+
)
|
|
213
237
|
case 'agent:send-prompt': {
|
|
214
238
|
const sessionId = String(msg.sessionId)
|
|
215
239
|
const session = findSessionById(sessionId)
|
|
@@ -243,31 +267,53 @@ export class AgentHost {
|
|
|
243
267
|
|
|
244
268
|
// --- operation impls ---
|
|
245
269
|
|
|
246
|
-
private
|
|
247
|
-
ok: true
|
|
248
|
-
routed: 'team-machine'
|
|
249
|
-
sessionId: string
|
|
250
|
-
}> {
|
|
270
|
+
private requireSimId(msg: any, type: string): string {
|
|
251
271
|
const simId = typeof msg.simId === 'string' ? msg.simId.trim() : ''
|
|
252
|
-
if (!simId) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
)
|
|
272
|
+
if (!simId) throw new AgentSessionError('NO_SIM', `${type} requires simId`)
|
|
273
|
+
return simId
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private listenForInspect(
|
|
277
|
+
socket: WebSocket,
|
|
278
|
+
simId: string,
|
|
279
|
+
): Promise<AgentInspectMessage> {
|
|
280
|
+
const queue = this.inspectQueues.get(simId)
|
|
281
|
+
const queued = queue?.shift()
|
|
282
|
+
if (queued && queue) {
|
|
283
|
+
if (queue.length === 0) this.inspectQueues.delete(simId)
|
|
284
|
+
return Promise.resolve(queued)
|
|
265
285
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
286
|
+
return new Promise((resolve, reject) => {
|
|
287
|
+
const listeners = this.inspectListeners.get(simId) ?? []
|
|
288
|
+
listeners.push({ socket, resolve, reject })
|
|
289
|
+
this.inspectListeners.set(simId, listeners)
|
|
269
290
|
})
|
|
270
|
-
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private sendInspectMessage(
|
|
294
|
+
simId: string,
|
|
295
|
+
prompt: AgentPromptEnvelope,
|
|
296
|
+
): { ok: true; queued: boolean; messageId: string } {
|
|
297
|
+
if (!prompt.text.trim()) {
|
|
298
|
+
throw new AgentSessionError('EMPTY_PROMPT', 'inspect message cannot be empty')
|
|
299
|
+
}
|
|
300
|
+
const message: AgentInspectMessage = {
|
|
301
|
+
id: `inspect-${randomUUID()}`,
|
|
302
|
+
simId,
|
|
303
|
+
createdAt: new Date().toISOString(),
|
|
304
|
+
prompt,
|
|
305
|
+
}
|
|
306
|
+
const listeners = this.inspectListeners.get(simId)
|
|
307
|
+
const listener = listeners?.shift()
|
|
308
|
+
if (listener && listeners) {
|
|
309
|
+
if (listeners.length === 0) this.inspectListeners.delete(simId)
|
|
310
|
+
listener.resolve(message)
|
|
311
|
+
return { ok: true, queued: false, messageId: message.id }
|
|
312
|
+
}
|
|
313
|
+
const queue = this.inspectQueues.get(simId) ?? []
|
|
314
|
+
queue.push(message)
|
|
315
|
+
this.inspectQueues.set(simId, queue)
|
|
316
|
+
return { ok: true, queued: true, messageId: message.id }
|
|
271
317
|
}
|
|
272
318
|
|
|
273
319
|
private async doStartSession(input: {
|
package/src/host/bridge-host.ts
CHANGED
|
@@ -484,17 +484,6 @@ export class SootSimBridgeHost {
|
|
|
484
484
|
this.openUrlHandler = opts.openUrl
|
|
485
485
|
this.agentHost = new AgentHost({
|
|
486
486
|
getExcludePorts: opts.agentScanExcludes,
|
|
487
|
-
resolveCliLease: (simId) => {
|
|
488
|
-
const sim = this.sims.get(simId)
|
|
489
|
-
const lease = sim ? this.getActiveLease(sim) : null
|
|
490
|
-
return lease
|
|
491
|
-
? {
|
|
492
|
-
kind: lease.kind,
|
|
493
|
-
cliIdentityKey: lease.cliIdentityKey,
|
|
494
|
-
expiresAt: lease.expiresAt,
|
|
495
|
-
}
|
|
496
|
-
: null
|
|
497
|
-
},
|
|
498
487
|
})
|
|
499
488
|
this.contrastOrigin = opts.contrastOrigin?.replace(/\/$/, '') || null
|
|
500
489
|
this.simIdleReapTtlMs =
|
|
@@ -535,6 +524,11 @@ export class SootSimBridgeHost {
|
|
|
535
524
|
// seed active runtime state from disk so the http routes + lockfile
|
|
536
525
|
// reflect reality at boot. we reread this on runtime:use messages.
|
|
537
526
|
this.refreshActiveRuntime()
|
|
527
|
+
// an installed runtime can serve while the startup update runs. only a
|
|
528
|
+
// machine without usable assets needs to wait for that download.
|
|
529
|
+
this.bootstrapping =
|
|
530
|
+
!this.activeRuntimeDirPath ||
|
|
531
|
+
!fs.existsSync(path.join(this.activeRuntimeDirPath, 'index.html'))
|
|
538
532
|
|
|
539
533
|
for (let attempt = 0; attempt < this.portFallbackCount; attempt++) {
|
|
540
534
|
const candidate = this.preferredPort + attempt
|