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/cli/cloud-client.ts
CHANGED
|
@@ -62,6 +62,7 @@ import type {
|
|
|
62
62
|
ParsedBridgeCliArgs,
|
|
63
63
|
WsBridge,
|
|
64
64
|
} from './ws-bridge.ts'
|
|
65
|
+
import { isRecord } from '../src/is-record'
|
|
65
66
|
|
|
66
67
|
export const DEFAULT_RNX_CLOUD_ORIGIN = rnxPublicBrand.origin
|
|
67
68
|
export const MAX_RNX_CLOUD_BUNDLE_BYTES = RNX_CLOUD_MAX_ARTIFACT_BYTES
|
|
@@ -115,10 +116,6 @@ interface CloudCommandEnvelope {
|
|
|
115
116
|
command: Record<string, unknown>
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
119
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
119
|
// three services answer this client and each states a failure its own way:
|
|
123
120
|
// the box service says { error: '<sentence>' }, the site's public api says
|
|
124
121
|
// { error: '<code>', message: '<sentence>' }, and its service-to-service api
|
package/cli/cloud-session.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fstatSync, writeSync } from 'node:fs'
|
|
2
2
|
import { isLoopbackHost } from '../src/backend-origin.ts'
|
|
3
|
+
import { isRecord } from '../src/is-record'
|
|
3
4
|
|
|
4
5
|
const CLOUD_SESSION_VERSION = 3 as const
|
|
5
6
|
const SHELL_UPDATE_FD_ENV = 'PEACH_SHELL_UPDATE_FD'
|
|
@@ -61,10 +62,6 @@ export interface CloudBoxSession {
|
|
|
61
62
|
artifact?: CloudSessionArtifactIdentity
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
65
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
65
|
export function normalizeCloudOrigin(source: string): string {
|
|
69
66
|
let parsed: URL
|
|
70
67
|
try {
|
|
@@ -18,6 +18,8 @@ export interface CloudTestFlowResult {
|
|
|
18
18
|
previewId: string | null
|
|
19
19
|
previewUrl: string | null
|
|
20
20
|
runId: string | null
|
|
21
|
+
// the step a failed flow stopped on and why, or null
|
|
22
|
+
failure: string | null
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
// every file one flow's ids land in: the bare file always (last flow wins,
|
|
@@ -77,6 +79,7 @@ export function collectCloudTestFlowResults(
|
|
|
77
79
|
stringField(preview, 'previewUrl') ??
|
|
78
80
|
(previewId ? `${cloudOrigin.replace(/\/$/, '')}/preview/${previewId}` : null),
|
|
79
81
|
runId: stringField(test, 'runId'),
|
|
82
|
+
failure: stringField(preview, 'failure'),
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
let numbered: number[] = []
|
package/cli/cloud-test-tunnel.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// bundle, the submitter publishes that stack through a token-gated
|
|
5
5
|
// cloudflared quick tunnel and submits the tunnel origin plus the mode-2
|
|
6
6
|
// fields (hostHeader, jobToken, sidecarPorts). the recorder projects the
|
|
7
|
-
// origin back to loopback (scripts/runner/
|
|
7
|
+
// origin back to loopback (scripts/runner/peach-recorder.ts), so flows
|
|
8
8
|
// keep their unmodified localhost authorities.
|
|
9
9
|
//
|
|
10
10
|
// the handshake mirrors packages/contrast-runner/src/hosted.ts, which this
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { createServer, request as httpRequest } from 'node:http'
|
|
27
27
|
import { tmpdir } from 'node:os'
|
|
28
28
|
import { join } from 'node:path'
|
|
29
|
+
import { sleep } from '../src/sleep'
|
|
29
30
|
|
|
30
31
|
// must match packages/contrast-runner/src/hostedPorts.ts.
|
|
31
32
|
const UPSTREAM_PORT_HEADER = 'x-contrast-upstream-port'
|
|
@@ -222,10 +223,6 @@ async function ensureCloudflared(): Promise<boolean> {
|
|
|
222
223
|
return cloudflaredAvailable()
|
|
223
224
|
}
|
|
224
225
|
|
|
225
|
-
function sleep(ms: number): Promise<void> {
|
|
226
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
227
|
-
}
|
|
228
|
-
|
|
229
226
|
function childAlive(child: ChildProcess): boolean {
|
|
230
227
|
return child.exitCode === null && child.signalCode === null
|
|
231
228
|
}
|
package/cli/command-registry.ts
CHANGED
package/cli/commands/agent.ts
CHANGED
|
@@ -12,21 +12,14 @@
|
|
|
12
12
|
// wrapper is live.
|
|
13
13
|
|
|
14
14
|
import fs from 'node:fs'
|
|
15
|
+
import os from 'node:os'
|
|
15
16
|
import path from 'node:path'
|
|
16
17
|
import {
|
|
17
18
|
AgentDaemonError,
|
|
18
19
|
connectToDaemon,
|
|
19
20
|
type AgentDaemonClient,
|
|
20
21
|
} from '../../src/agent-daemon-client'
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
eventsFifoPath,
|
|
24
|
-
pidIsAlive,
|
|
25
|
-
promptFifoPath,
|
|
26
|
-
sessionDir,
|
|
27
|
-
transcriptPath,
|
|
28
|
-
type Provider,
|
|
29
|
-
} from '../../src/agent-sessions'
|
|
22
|
+
import { pidIsAlive, transcriptPath, type Provider } from '../../src/agent-sessions'
|
|
30
23
|
import {
|
|
31
24
|
costThisWeek,
|
|
32
25
|
deleteProject,
|
|
@@ -35,11 +28,13 @@ import {
|
|
|
35
28
|
listProjects,
|
|
36
29
|
listSessions,
|
|
37
30
|
upsertProject,
|
|
38
|
-
type AgentSession,
|
|
39
|
-
type AttachedProject,
|
|
40
31
|
} from '../../src/attached-projects'
|
|
32
|
+
import { readCurrentSimId } from '../current-sim'
|
|
41
33
|
import { printCommandHelp } from '../help'
|
|
42
|
-
import { resolveDefaultBridgePort } from '../ws-bridge'
|
|
34
|
+
import { createBridge, resolveDefaultBridgePort } from '../ws-bridge'
|
|
35
|
+
import { inspectDescribe } from './inspect/core'
|
|
36
|
+
import type { AgentEvent } from '../../src/agent-events'
|
|
37
|
+
import type { AgentInspectMessage } from '../../src/agent-prompt'
|
|
43
38
|
|
|
44
39
|
// the bridge this invocation talks to: `-p`, else the same dev-bridge or
|
|
45
40
|
// daemon pick every other command makes (PORT_OFFSET and SOOTSIM_BRIDGE_PORT
|
|
@@ -74,6 +69,10 @@ export async function runAgentCommand(
|
|
|
74
69
|
return await cmdPrompt(rest)
|
|
75
70
|
case 'watch':
|
|
76
71
|
return await cmdWatch(rest)
|
|
72
|
+
case 'listen':
|
|
73
|
+
return await cmdListen(rest)
|
|
74
|
+
case 'info':
|
|
75
|
+
return await cmdInfo(rest)
|
|
77
76
|
case 'transcript':
|
|
78
77
|
return await cmdTranscript(rest)
|
|
79
78
|
case 'end':
|
|
@@ -342,6 +341,144 @@ async function cmdWatch(argv: string[]): Promise<number> {
|
|
|
342
341
|
return exit
|
|
343
342
|
}
|
|
344
343
|
|
|
344
|
+
async function cmdListen(argv: string[]): Promise<number> {
|
|
345
|
+
const { flags } = parseFlags(argv, { '--sim': 'value' })
|
|
346
|
+
const simId = ((flags['--sim'] as string) || readCurrentSimId() || '').trim()
|
|
347
|
+
if (!simId) {
|
|
348
|
+
process.stderr.write('no current sim. run `peach use <id>` or pass `--sim <id>`.\n')
|
|
349
|
+
return 1
|
|
350
|
+
}
|
|
351
|
+
const client = await connectToDaemon({
|
|
352
|
+
port: daemonPort,
|
|
353
|
+
commandTimeoutMs: 0,
|
|
354
|
+
clientLabel: 'peach-agent-listen',
|
|
355
|
+
})
|
|
356
|
+
try {
|
|
357
|
+
const message = await client.listenForInspect(simId)
|
|
358
|
+
const artifacts = await saveInspectArtifacts(message)
|
|
359
|
+
printInspectMessage(message, artifacts)
|
|
360
|
+
return 0
|
|
361
|
+
} finally {
|
|
362
|
+
client.close()
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function saveInspectArtifacts(message: AgentInspectMessage): Promise<{
|
|
367
|
+
inspectPath: string
|
|
368
|
+
describePath: string
|
|
369
|
+
screenshotPath: string | null
|
|
370
|
+
}> {
|
|
371
|
+
const dir = path.join(os.tmpdir(), 'peach-inspect', message.id)
|
|
372
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
373
|
+
const inspectPath = path.join(dir, 'inspect.json')
|
|
374
|
+
const describePath = path.join(dir, 'describe.txt')
|
|
375
|
+
const screenshotPath = path.join(dir, 'screenshot.png')
|
|
376
|
+
fs.writeFileSync(inspectPath, JSON.stringify(message, null, 2) + '\n', {
|
|
377
|
+
mode: 0o600,
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
const bridge = createBridge(daemonPort, {
|
|
381
|
+
commandTimeoutMs: 15_000,
|
|
382
|
+
simId: message.simId,
|
|
383
|
+
})
|
|
384
|
+
let savedScreenshot: string | null = null
|
|
385
|
+
try {
|
|
386
|
+
try {
|
|
387
|
+
const described = await inspectDescribe(bridge, {
|
|
388
|
+
describe: true,
|
|
389
|
+
verbose: false,
|
|
390
|
+
filter: '',
|
|
391
|
+
compact: false,
|
|
392
|
+
hideXy: false,
|
|
393
|
+
fullText: true,
|
|
394
|
+
})
|
|
395
|
+
fs.writeFileSync(describePath, described.tree ?? 'no visible nodes\n', {
|
|
396
|
+
mode: 0o600,
|
|
397
|
+
})
|
|
398
|
+
} catch (error) {
|
|
399
|
+
fs.writeFileSync(
|
|
400
|
+
describePath,
|
|
401
|
+
`describe unavailable: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
402
|
+
{ mode: 0o600 },
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
const dataUrl: unknown = await bridge.send({ type: 'screenshot' })
|
|
407
|
+
const prefix = 'data:image/png;base64,'
|
|
408
|
+
if (typeof dataUrl === 'string' && dataUrl.startsWith(prefix)) {
|
|
409
|
+
fs.writeFileSync(
|
|
410
|
+
screenshotPath,
|
|
411
|
+
Buffer.from(dataUrl.slice(prefix.length), 'base64'),
|
|
412
|
+
{
|
|
413
|
+
mode: 0o600,
|
|
414
|
+
},
|
|
415
|
+
)
|
|
416
|
+
savedScreenshot = screenshotPath
|
|
417
|
+
}
|
|
418
|
+
} catch {}
|
|
419
|
+
} finally {
|
|
420
|
+
bridge.close()
|
|
421
|
+
}
|
|
422
|
+
return { inspectPath, describePath, screenshotPath: savedScreenshot }
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function printInspectMessage(
|
|
426
|
+
message: AgentInspectMessage,
|
|
427
|
+
artifacts: { inspectPath: string; describePath: string; screenshotPath: string | null },
|
|
428
|
+
): void {
|
|
429
|
+
const request = message.prompt.displayText ?? message.prompt.text
|
|
430
|
+
const context = [
|
|
431
|
+
...(message.prompt.inspectSummary?.split('\n') ?? []),
|
|
432
|
+
...(message.prompt.inspectTrace?.split('\n') ?? []),
|
|
433
|
+
]
|
|
434
|
+
.map((line) => line.trim())
|
|
435
|
+
.filter((line, index, lines) => line && lines.indexOf(line) === index)
|
|
436
|
+
.slice(0, 4)
|
|
437
|
+
const lines = [
|
|
438
|
+
`PEACH INSPECT ${message.id}`,
|
|
439
|
+
`Request: ${request}`,
|
|
440
|
+
`Sim: ${message.simId}`,
|
|
441
|
+
...context.map((line, index) => `${index === 0 ? 'Target' : 'Context'}: ${line}`),
|
|
442
|
+
`Describe: ${artifacts.describePath}`,
|
|
443
|
+
`Inspect: ${artifacts.inspectPath}`,
|
|
444
|
+
...(artifacts.screenshotPath ? [`Screenshot: ${artifacts.screenshotPath}`] : []),
|
|
445
|
+
`More: peach agent info ${message.id}`,
|
|
446
|
+
]
|
|
447
|
+
process.stdout.write(lines.join('\n') + '\n')
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function cmdInfo(argv: string[]): Promise<number> {
|
|
451
|
+
const messageId = argv[0]?.trim() ?? ''
|
|
452
|
+
if (!/^inspect-[a-f0-9-]+$/.test(messageId)) {
|
|
453
|
+
process.stderr.write('usage: peach agent info <message-id>\n')
|
|
454
|
+
return 2
|
|
455
|
+
}
|
|
456
|
+
const dir = path.join(os.tmpdir(), 'peach-inspect', messageId)
|
|
457
|
+
const inspectPath = path.join(dir, 'inspect.json')
|
|
458
|
+
if (!fs.existsSync(inspectPath)) {
|
|
459
|
+
process.stderr.write(`inspect message not found: ${messageId}\n`)
|
|
460
|
+
return 1
|
|
461
|
+
}
|
|
462
|
+
const message: unknown = JSON.parse(fs.readFileSync(inspectPath, 'utf8'))
|
|
463
|
+
process.stdout.write(
|
|
464
|
+
JSON.stringify(
|
|
465
|
+
{
|
|
466
|
+
message,
|
|
467
|
+
artifacts: {
|
|
468
|
+
inspect: inspectPath,
|
|
469
|
+
describe: path.join(dir, 'describe.txt'),
|
|
470
|
+
screenshot: fs.existsSync(path.join(dir, 'screenshot.png'))
|
|
471
|
+
? path.join(dir, 'screenshot.png')
|
|
472
|
+
: null,
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
null,
|
|
476
|
+
2,
|
|
477
|
+
) + '\n',
|
|
478
|
+
)
|
|
479
|
+
return 0
|
|
480
|
+
}
|
|
481
|
+
|
|
345
482
|
function formatEvent(ev: AgentEvent): string {
|
|
346
483
|
const ts = fmtTs(ev.ts)
|
|
347
484
|
switch (ev.type) {
|
|
@@ -426,14 +563,3 @@ function shortId(id: string): string {
|
|
|
426
563
|
function fmtTs(ts: number): string {
|
|
427
564
|
return new Date(ts).toISOString().replace('T', ' ').slice(0, 19)
|
|
428
565
|
}
|
|
429
|
-
|
|
430
|
-
// keep these importable for the legacy e2e script paths — the canonical
|
|
431
|
-
// source of truth is now `sootsim/agent-sessions`.
|
|
432
|
-
export const __internals = {
|
|
433
|
-
sessionDir,
|
|
434
|
-
promptFifoPath,
|
|
435
|
-
eventsFifoPath,
|
|
436
|
-
transcriptPath,
|
|
437
|
-
parseAgentEventLine,
|
|
438
|
-
}
|
|
439
|
-
export type { AgentSession, AttachedProject }
|
package/cli/commands/assert.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// 2 misuse — bad flags, unknown verb (treated as bad by bisect)
|
|
15
15
|
|
|
16
16
|
import { spawn } from 'child_process'
|
|
17
|
+
import { sleep } from '../../src/sleep'
|
|
17
18
|
import { rnxExit } from '../run-rnx'
|
|
18
19
|
import { rnxSelfInvocation } from '../self-invocation'
|
|
19
20
|
|
|
@@ -496,10 +497,6 @@ export async function runAssert(rawArgs: string[]): Promise<void> {
|
|
|
496
497
|
}
|
|
497
498
|
}
|
|
498
499
|
|
|
499
|
-
function sleep(ms: number): Promise<void> {
|
|
500
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
501
|
-
}
|
|
502
|
-
|
|
503
500
|
function printHelp(): void {
|
|
504
501
|
process.stdout.write(`
|
|
505
502
|
peach assert — run any read verb and convert its output into a bisect-friendly exit code.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// peach operations registered as just-bash commands.
|
|
2
|
+
//
|
|
3
|
+
// `peach` inside a box shell is the same CLI, re-invoked as a child process with
|
|
4
|
+
// the shell's own cwd. that is deliberate — spelling the verbs a second time
|
|
5
|
+
// in shell-command form would be a second implementation of every command, and
|
|
6
|
+
// the spec is explicit that the CLI and the in-shell commands are clients of
|
|
7
|
+
// one implementation, not two.
|
|
8
|
+
|
|
9
|
+
import { spawn } from 'node:child_process'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
import { defineCommand, type CustomCommand } from '@peach/box/shell'
|
|
12
|
+
import { PROJECT_MOUNT } from '@peach/box/shell'
|
|
13
|
+
import { peachPublicBrand } from '../../../src/public-brand'
|
|
14
|
+
import { rnxSelfInvocation } from '../../self-invocation'
|
|
15
|
+
import { PEACH_PLACEMENT_ENV } from '../../ws-bridge'
|
|
16
|
+
|
|
17
|
+
// map a shell path under /project back onto the real checkout, so a child
|
|
18
|
+
// process started from `cd src && peach describe` sees the directory the user
|
|
19
|
+
// is actually standing in.
|
|
20
|
+
function hostCwd(root: string, shellCwd: string): string {
|
|
21
|
+
if (shellCwd === PROJECT_MOUNT) return root
|
|
22
|
+
if (!shellCwd.startsWith(`${PROJECT_MOUNT}/`)) return root
|
|
23
|
+
return path.join(root, shellCwd.slice(PROJECT_MOUNT.length + 1))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createPeachCommand(options: {
|
|
27
|
+
root: string
|
|
28
|
+
shellCwd: () => string
|
|
29
|
+
}): CustomCommand {
|
|
30
|
+
return defineCommand(peachPublicBrand.commandName, async (args) => {
|
|
31
|
+
const { executable, prefixArgs } = rnxSelfInvocation()
|
|
32
|
+
const child = spawn(executable, [...prefixArgs, ...args], {
|
|
33
|
+
cwd: hostCwd(options.root, options.shellCwd()),
|
|
34
|
+
// mark the child as running inside a local box. this is the only thing
|
|
35
|
+
// that sets a placement — nothing infers one from cwd — and it is what
|
|
36
|
+
// lets `peach box` refuse to nest inside itself.
|
|
37
|
+
env: { ...process.env, [PEACH_PLACEMENT_ENV]: 'local-box' },
|
|
38
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
let stdout = ''
|
|
42
|
+
let stderr = ''
|
|
43
|
+
child.stdout.on('data', (chunk) => {
|
|
44
|
+
stdout += String(chunk)
|
|
45
|
+
})
|
|
46
|
+
child.stderr.on('data', (chunk) => {
|
|
47
|
+
stderr += String(chunk)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
51
|
+
child.once('error', (error) => {
|
|
52
|
+
stderr += `${peachPublicBrand.commandName}: ${error.message}\n`
|
|
53
|
+
resolve(1)
|
|
54
|
+
})
|
|
55
|
+
child.once('close', (code) => resolve(code ?? 0))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
return { stdout, stderr, exitCode }
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const createRnxCommand = createPeachCommand
|
|
@@ -136,73 +136,3 @@ export function sharePlane(root: string, opts: { port?: number } = {}): PlaneSha
|
|
|
136
136
|
},
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* How many boxes hold `root` right now, without registering anything.
|
|
142
|
-
*
|
|
143
|
-
* `plane:register` cannot answer this: registering is what makes a root
|
|
144
|
-
* reachable over a loopback socket, so a readiness probe that asked with it
|
|
145
|
-
* would both authorize the root and count itself in the answer.
|
|
146
|
-
*/
|
|
147
|
-
export async function planeStatus(
|
|
148
|
-
root: string,
|
|
149
|
-
opts: { port?: number } = {},
|
|
150
|
-
): Promise<{ holders: number; registered: boolean } | { declined: string }> {
|
|
151
|
-
const chosen = target(opts.port)
|
|
152
|
-
if ('declined' in chosen) return chosen
|
|
153
|
-
const port = chosen.port
|
|
154
|
-
|
|
155
|
-
return new Promise((resolve) => {
|
|
156
|
-
let done = false
|
|
157
|
-
let socket: WebSocket | null = null
|
|
158
|
-
const finish = (
|
|
159
|
-
value: { holders: number; registered: boolean } | { declined: string },
|
|
160
|
-
) => {
|
|
161
|
-
if (done) return
|
|
162
|
-
done = true
|
|
163
|
-
clearTimeout(timer)
|
|
164
|
-
try {
|
|
165
|
-
socket?.close()
|
|
166
|
-
} catch {}
|
|
167
|
-
resolve(value)
|
|
168
|
-
}
|
|
169
|
-
const timer = setTimeout(
|
|
170
|
-
() => finish({ declined: `bridge on port ${port} did not answer plane:status` }),
|
|
171
|
-
REGISTER_TIMEOUT_MS,
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
socket = new WebSocket(bridgeWsUrl(port))
|
|
176
|
-
} catch (error) {
|
|
177
|
-
finish({ declined: error instanceof Error ? error.message : String(error) })
|
|
178
|
-
return
|
|
179
|
-
}
|
|
180
|
-
socket.on('error', (error) => finish({ declined: error.message }))
|
|
181
|
-
socket.on('close', () => finish({ declined: `bridge on port ${port} closed` }))
|
|
182
|
-
socket.on('open', () => {
|
|
183
|
-
socket?.send(JSON.stringify({ type: 'plane:status', id: 'status', root }))
|
|
184
|
-
})
|
|
185
|
-
socket.on('message', (data) => {
|
|
186
|
-
let parsed: unknown
|
|
187
|
-
try {
|
|
188
|
-
parsed = JSON.parse(String(data))
|
|
189
|
-
} catch {
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
if (typeof parsed !== 'object' || parsed === null) return
|
|
193
|
-
const reply: Record<string, unknown> = { ...parsed }
|
|
194
|
-
if (reply.id !== 'status') return
|
|
195
|
-
if (reply.error) {
|
|
196
|
-
finish({ declined: String(reply.error) })
|
|
197
|
-
return
|
|
198
|
-
}
|
|
199
|
-
// the daemon nests its payload under `result`, the same shape
|
|
200
|
-
// plane:register answers in.
|
|
201
|
-
const result = (reply.result ?? {}) as Record<string, unknown>
|
|
202
|
-
finish({
|
|
203
|
-
holders: Number(result.holders ?? 0),
|
|
204
|
-
registered: Boolean(result.registered),
|
|
205
|
-
})
|
|
206
|
-
})
|
|
207
|
-
})
|
|
208
|
-
}
|
|
@@ -1,60 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// `peach` inside a box shell is the same CLI, re-invoked as a child process with
|
|
4
|
-
// the shell's own cwd. that is deliberate — spelling the verbs a second time
|
|
5
|
-
// in shell-command form would be a second implementation of every command, and
|
|
6
|
-
// the spec is explicit that the CLI and the in-shell commands are clients of
|
|
7
|
-
// one implementation, not two.
|
|
8
|
-
|
|
9
|
-
import { spawn } from 'node:child_process'
|
|
10
|
-
import path from 'node:path'
|
|
11
|
-
import { defineCommand, type CustomCommand } from '@peach/box/shell'
|
|
12
|
-
import { PROJECT_MOUNT } from '@peach/box/shell'
|
|
13
|
-
import { rnxPublicBrand } from '../../../src/public-brand'
|
|
14
|
-
import { rnxSelfInvocation } from '../../self-invocation'
|
|
15
|
-
import { PEACH_PLACEMENT_ENV } from '../../ws-bridge'
|
|
16
|
-
|
|
17
|
-
// map a shell path under /project back onto the real checkout, so a child
|
|
18
|
-
// process started from `cd src && peach describe` sees the directory the user
|
|
19
|
-
// is actually standing in.
|
|
20
|
-
function hostCwd(root: string, shellCwd: string): string {
|
|
21
|
-
if (shellCwd === PROJECT_MOUNT) return root
|
|
22
|
-
if (!shellCwd.startsWith(`${PROJECT_MOUNT}/`)) return root
|
|
23
|
-
return path.join(root, shellCwd.slice(PROJECT_MOUNT.length + 1))
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function createRnxCommand(options: {
|
|
27
|
-
root: string
|
|
28
|
-
shellCwd: () => string
|
|
29
|
-
}): CustomCommand {
|
|
30
|
-
return defineCommand(rnxPublicBrand.commandName, async (args) => {
|
|
31
|
-
const { executable, prefixArgs } = rnxSelfInvocation()
|
|
32
|
-
const child = spawn(executable, [...prefixArgs, ...args], {
|
|
33
|
-
cwd: hostCwd(options.root, options.shellCwd()),
|
|
34
|
-
// mark the child as running inside a local box. this is the only thing
|
|
35
|
-
// that sets a placement — nothing infers one from cwd — and it is what
|
|
36
|
-
// lets `peach box` refuse to nest inside itself.
|
|
37
|
-
env: { ...process.env, [PEACH_PLACEMENT_ENV]: 'local-box' },
|
|
38
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
let stdout = ''
|
|
42
|
-
let stderr = ''
|
|
43
|
-
child.stdout.on('data', (chunk) => {
|
|
44
|
-
stdout += String(chunk)
|
|
45
|
-
})
|
|
46
|
-
child.stderr.on('data', (chunk) => {
|
|
47
|
-
stderr += String(chunk)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
const exitCode = await new Promise<number>((resolve) => {
|
|
51
|
-
child.once('error', (error) => {
|
|
52
|
-
stderr += `${rnxPublicBrand.commandName}: ${error.message}\n`
|
|
53
|
-
resolve(1)
|
|
54
|
-
})
|
|
55
|
-
child.once('close', (code) => resolve(code ?? 0))
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
return { stdout, stderr, exitCode }
|
|
59
|
-
})
|
|
60
|
-
}
|
|
1
|
+
export { createPeachCommand, createRnxCommand } from './peach-command'
|
|
@@ -103,6 +103,19 @@ const FLAG_MAP: Record<string, string> = {
|
|
|
103
103
|
'--fixed-strings': '-F',
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// presentational no-ops, accepted without effect because they match the output
|
|
107
|
+
// by construction: it never prints headings, always numbers lines, and never
|
|
108
|
+
// emits color. refusing them burned a wave turn on `rg -n x --no-heading`.
|
|
109
|
+
// --no-line-number is deliberately absent: the output is numbered, so it
|
|
110
|
+
// contradicts the output and stays refused like every semantic flag.
|
|
111
|
+
const NOOP_FLAGS = new Set([
|
|
112
|
+
'--no-heading',
|
|
113
|
+
'--heading',
|
|
114
|
+
'--color',
|
|
115
|
+
'-n',
|
|
116
|
+
'--line-number',
|
|
117
|
+
])
|
|
118
|
+
|
|
106
119
|
export function createSearchCommand(options: { root: string }): CustomCommand {
|
|
107
120
|
return defineCommand('rg', async (args) => {
|
|
108
121
|
const flags: string[] = []
|
|
@@ -112,6 +125,7 @@ export function createSearchCommand(options: { root: string }): CustomCommand {
|
|
|
112
125
|
bare.push(arg)
|
|
113
126
|
continue
|
|
114
127
|
}
|
|
128
|
+
if (NOOP_FLAGS.has(arg) || arg.startsWith('--color=')) continue
|
|
115
129
|
const mapped = FLAG_MAP[arg]
|
|
116
130
|
if (!mapped) {
|
|
117
131
|
return {
|
package/cli/commands/box.ts
CHANGED
|
@@ -38,8 +38,8 @@ import { findRepositoryPackageRoot } from '../setup-repository'
|
|
|
38
38
|
import { resolvePlacement } from '../ws-bridge'
|
|
39
39
|
import { BundleFilePlane } from './box/bundle-plane'
|
|
40
40
|
import { CheckoutFilePlane, CheckoutRequiresGitError } from './box/checkout-plane'
|
|
41
|
+
import { createPeachCommand } from './box/peach-command'
|
|
41
42
|
import { sharePlane } from './box/plane-share'
|
|
42
|
-
import { createRnxCommand } from './box/rnx-command'
|
|
43
43
|
import { createSearchCommand } from './box/search-command'
|
|
44
44
|
import type { BoxShellSession } from '@peach/box/client'
|
|
45
45
|
import type { BoxTemplateResolution } from '@peach/box/template'
|
|
@@ -747,7 +747,7 @@ async function runLocalBox(opts: BoxCommandOptions): Promise<number> {
|
|
|
747
747
|
createBoxHostProvider({
|
|
748
748
|
shell: {
|
|
749
749
|
commands: [
|
|
750
|
-
|
|
750
|
+
createPeachCommand({
|
|
751
751
|
root,
|
|
752
752
|
shellCwd: () => session?.cwd ?? PROJECT_MOUNT,
|
|
753
753
|
}),
|