peachsim 0.1.578 → 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.
Files changed (178) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +11 -9
  3. package/cli/app-catalog.ts +77 -0
  4. package/cli/app-fonts.ts +4 -20
  5. package/cli/app-splash.ts +2 -18
  6. package/cli/bridge-flow-runner.ts +505 -139
  7. package/cli/cloud-client.ts +1 -4
  8. package/cli/cloud-session.ts +1 -4
  9. package/cli/cloud-test-result-files.ts +3 -0
  10. package/cli/cloud-test-tunnel.ts +2 -5
  11. package/cli/command-registry.ts +2 -0
  12. package/cli/commands/agent.ts +149 -23
  13. package/cli/commands/assert.ts +1 -4
  14. package/cli/commands/box/peach-command.ts +62 -0
  15. package/cli/commands/box/plane-share.ts +0 -70
  16. package/cli/commands/box/rnx-command.ts +1 -60
  17. package/cli/commands/box/search-command.ts +14 -0
  18. package/cli/commands/box.ts +2 -2
  19. package/cli/commands/control.ts +121 -27
  20. package/cli/commands/cpu-profile.ts +23 -10
  21. package/cli/commands/daemon.ts +1 -9
  22. package/cli/commands/detox.ts +26 -18
  23. package/cli/commands/device.ts +129 -1
  24. package/cli/commands/film.ts +25 -16
  25. package/cli/commands/flow.ts +9 -2
  26. package/cli/commands/inspect/actions.ts +75 -1
  27. package/cli/commands/inspect/core.ts +81 -2
  28. package/cli/commands/inspect/describe.ts +2 -6
  29. package/cli/commands/inspect/env.ts +1 -1
  30. package/cli/commands/inspect/settling.ts +20 -3
  31. package/cli/commands/inspect/shared.ts +1 -4
  32. package/cli/commands/inspect/sleep.ts +1 -1
  33. package/cli/commands/inspect.ts +16 -49
  34. package/cli/commands/install-desktop.ts +8 -6
  35. package/cli/commands/login.ts +3 -5
  36. package/cli/commands/maestro-generate.ts +1 -4
  37. package/cli/commands/maestro.ts +38 -21
  38. package/cli/commands/no-bridge-hint.ts +0 -14
  39. package/cli/commands/permissions.ts +2 -2
  40. package/cli/commands/platform.ts +13 -6
  41. package/cli/commands/screenshot.ts +6 -1
  42. package/cli/commands/skills.ts +1 -1
  43. package/cli/commands/test.ts +6 -2
  44. package/cli/commands/three-mode.ts +162 -19
  45. package/cli/commands/upload.ts +3 -18
  46. package/cli/current-sim.ts +0 -1
  47. package/cli/desktop-companion.ts +40 -8
  48. package/cli/drivers/playwright-provisioning.ts +1 -4
  49. package/cli/flow-export.ts +1 -8
  50. package/cli/flow-session.ts +0 -4
  51. package/cli/hints.ts +1 -16
  52. package/cli/inspect-notice-state.ts +0 -4
  53. package/cli/main.ts +2 -4
  54. package/cli/outbound-endpoints.ts +1 -24
  55. package/cli/parse-args.ts +1 -2
  56. package/cli/rnx-in-process.ts +3 -1
  57. package/cli/run-rnx-core.ts +4 -2
  58. package/cli/run-rnx.ts +18 -10
  59. package/cli/send-to-box.ts +1 -4
  60. package/cli/telemetry.ts +1 -1
  61. package/cli/ws-bridge.ts +1 -5
  62. package/detox/gestures.ts +20 -6
  63. package/detox/index.ts +379 -123
  64. package/detox/navigation-lifecycle.ts +22 -13
  65. package/dist-lib/agent-daemon-client.cjs +21 -15
  66. package/dist-lib/agent-events.cjs +1 -1
  67. package/dist-lib/agent-identity.cjs +3 -17
  68. package/dist-lib/agent-sessions.cjs +5 -4
  69. package/dist-lib/attached-projects.cjs +1 -77
  70. package/dist-lib/auth/shared-session.cjs +1 -1
  71. package/dist-lib/backend-origin.cjs +11 -1
  72. package/dist-lib/beta.cjs +1 -1
  73. package/dist-lib/beta.mjs +1 -1
  74. package/dist-lib/bridge-constants.cjs +1 -1
  75. package/dist-lib/bridge-contract-input.cjs +7 -5
  76. package/dist-lib/bridge-contract-input.mjs +7 -5
  77. package/dist-lib/bridge-contract.cjs +1 -1
  78. package/dist-lib/bridge-contract.mjs +1 -1
  79. package/dist-lib/capture-contract.cjs +30 -7
  80. package/dist-lib/capture-contract.mjs +30 -7
  81. package/dist-lib/cli-constants.cjs +1 -1
  82. package/dist-lib/cloud-contract.cjs +1 -1
  83. package/dist-lib/cloud-contract.mjs +1 -1
  84. package/dist-lib/cloud-test-args.cjs +1 -1
  85. package/dist-lib/cloud-test-args.mjs +1 -1
  86. package/dist-lib/cloud.cjs +17 -11
  87. package/dist-lib/cloud.mjs +17 -11
  88. package/dist-lib/config.cjs +1 -1
  89. package/dist-lib/detox/index.cjs +321 -122
  90. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  91. package/dist-lib/home-paths.cjs +1 -1
  92. package/dist-lib/host/bridge-host.cjs +427 -212
  93. package/dist-lib/host/fetch-proxy-handler.cjs +5 -28
  94. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  95. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  96. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  97. package/dist-lib/host/websocket-proxy.cjs +1 -1
  98. package/dist-lib/index.cjs +315 -58
  99. package/dist-lib/jump-to-source-babel.cjs +1 -1
  100. package/dist-lib/jump-to-source-native.cjs +1 -1
  101. package/dist-lib/menu.cjs +67 -31
  102. package/dist-lib/menu.mjs +64 -28
  103. package/dist-lib/metro-fingerprint-registry.cjs +8 -4
  104. package/dist-lib/metro-fingerprint-registry.mjs +6 -4
  105. package/dist-lib/metro-production-bundle.cjs +2 -2
  106. package/dist-lib/metro-production-bundle.mjs +2 -2
  107. package/dist-lib/metro.cjs +8 -4
  108. package/dist-lib/profiles.cjs +1 -1
  109. package/dist-lib/public-brand.cjs +7 -4
  110. package/dist-lib/react-native-host-modules.cjs +1 -1
  111. package/dist-lib/react-native-host-modules.mjs +1 -1
  112. package/dist-lib/render-mode.cjs +3 -13
  113. package/dist-lib/scripts/dev-server-scanner.cjs +5 -17
  114. package/dist-lib/sdk.cjs +129 -10
  115. package/dist-lib/sdk.mjs +127 -10
  116. package/dist-lib/skills.cjs +1635 -1121
  117. package/dist-lib/swift-compile.cjs +277 -70
  118. package/dist-lib/swift-compile.mjs +275 -70
  119. package/dist-lib/swift-package.cjs +17 -5
  120. package/dist-lib/swift-package.mjs +15 -4
  121. package/dist-lib/swift-submit.cjs +32 -8
  122. package/dist-lib/swift-submit.mjs +29 -7
  123. package/dist-lib/swift.cjs +30912 -577
  124. package/dist-lib/vite-kotlin.cjs +356 -0
  125. package/dist-lib/vite-swift.cjs +1743 -0
  126. package/dist-lib/vite.cjs +208 -1039
  127. package/kotlin/README.md +18 -0
  128. package/kotlin/Sources/androidx/annotation/Keep.kt +4 -0
  129. package/kotlin/Sources/dev/onejs/one/OneNativeSourceDispatch.kt +7 -0
  130. package/kotlin/Sources/dev/onejs/one/source/OneModule.kt +4 -0
  131. package/kotlin/Sources/org/json/JSON.kt +244 -0
  132. package/kotlin/toolchain.json +19 -0
  133. package/package.json +14 -2
  134. package/scripts/dev-server-scanner.ts +0 -18
  135. package/scripts/optional-demo-registry.ts +0 -17
  136. package/skills/contrast/SKILL.md +2 -2
  137. package/skills/peach-setup/SKILL.md +16 -0
  138. package/src/agent-daemon-client.ts +36 -22
  139. package/src/agent-identity.ts +0 -15
  140. package/src/agent-prompt.ts +12 -0
  141. package/src/attached-projects.ts +0 -72
  142. package/src/backend-origin.ts +10 -0
  143. package/src/bridge-contract-input.ts +2 -5
  144. package/src/bridge-contract.ts +2 -0
  145. package/src/capture-contract.ts +53 -11
  146. package/src/cloud.ts +4 -9
  147. package/src/dev-server-open.ts +0 -99
  148. package/src/home-paths.ts +2 -3
  149. package/src/host/agent-host.ts +82 -36
  150. package/src/host/bridge-host.ts +5 -11
  151. package/src/host/fetch-proxy-handler.ts +19 -43
  152. package/src/is-record.ts +4 -0
  153. package/src/menu.ts +82 -37
  154. package/src/metro-fingerprint-registry.ts +1 -4
  155. package/src/metro-production-bundle.ts +1 -1
  156. package/src/plist.ts +1 -4
  157. package/src/public-brand.ts +5 -3
  158. package/src/render-mode.ts +0 -12
  159. package/src/runtime-assets.ts +9 -2
  160. package/src/screenshots/compose.ts +7 -1
  161. package/src/screenshots/frame-compose.ts +21 -10
  162. package/src/screenshots/schema.ts +1 -4
  163. package/src/sim-client.ts +2 -4
  164. package/src/skills/builtin/maestro-test.ts +1 -4
  165. package/src/sleep.ts +4 -0
  166. package/src/swift-compile.ts +209 -47
  167. package/src/swift-icu.ts +140 -0
  168. package/src/swift-package.ts +26 -2
  169. package/src/swift-submit.ts +84 -4
  170. package/src/swift.ts +2 -0
  171. package/src/vite-plugin-kotlin.ts +376 -0
  172. package/src/vite-plugin-one.ts +224 -36
  173. package/src/vite-plugin-swift.ts +382 -96
  174. package/src/vite-plugin.ts +121 -134
  175. package/src/vite.d.ts +10 -0
  176. package/cli/resolve-assets.ts +0 -63
  177. package/src/rolldown-plugin-swift.ts +0 -244
  178. package/src/team-machine-prompt.ts +0 -73
@@ -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
@@ -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[] = []
@@ -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/sootsim-recorder.ts), so flows
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
  }
@@ -123,6 +123,8 @@ const VALUE_FLAGS = new Set([
123
123
  '-p',
124
124
  '--timeout',
125
125
  '--device',
126
+ '--posture',
127
+ '--os-version',
126
128
  '--output',
127
129
  '-o',
128
130
  '--out',
@@ -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 { parseAgentEventLine, type AgentEvent } from '../../src/agent-events'
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 }
@@ -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
- // step 4: RNX 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 { 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 {
@@ -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
- createRnxCommand({
750
+ createPeachCommand({
751
751
  root,
752
752
  shellCwd: () => session?.cwd ?? PROJECT_MOUNT,
753
753
  }),