peachsim 0.1.527 → 0.1.570

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 (158) hide show
  1. package/README.md +26 -26
  2. package/cli/app-fonts.ts +1 -1
  3. package/cli/app-splash.ts +1 -1
  4. package/cli/bin.ts +1 -1
  5. package/cli/bridge-diagnostics.ts +1 -1
  6. package/cli/bridge-flow-runner.ts +57 -5
  7. package/cli/cloud-client.ts +273 -6
  8. package/cli/cloud-dispatch.ts +22 -2
  9. package/cli/cloud-session.ts +60 -4
  10. package/cli/cloud-test-tunnel.ts +354 -0
  11. package/cli/commands/box/bundle-plane.ts +2 -2
  12. package/cli/commands/box/checkout-plane.ts +1 -1
  13. package/cli/commands/box/rnx-command.ts +2 -2
  14. package/cli/commands/box/search-command.ts +2 -2
  15. package/cli/commands/box.ts +8 -8
  16. package/cli/commands/compat.ts +1 -1
  17. package/cli/commands/control.ts +22 -3
  18. package/cli/commands/detox.ts +1 -1
  19. package/cli/commands/device.ts +1 -1
  20. package/cli/commands/diagnose.ts +1 -1
  21. package/cli/commands/film.ts +1 -1
  22. package/cli/commands/flow.ts +60 -3
  23. package/cli/commands/inspect/actions.ts +1 -1
  24. package/cli/commands/inspect/core.ts +6 -7
  25. package/cli/commands/inspect/find.ts +2 -1
  26. package/cli/commands/inspect/resolve-target.ts +5 -3
  27. package/cli/commands/inspect/shared.ts +1 -1
  28. package/cli/commands/inspect/wait-event.ts +1 -1
  29. package/cli/commands/inspect/wait-selector.ts +15 -4
  30. package/cli/commands/inspect.ts +19 -14
  31. package/cli/commands/maestro.ts +464 -43
  32. package/cli/commands/permissions.ts +1 -1
  33. package/cli/commands/platform.ts +205 -8
  34. package/cli/commands/preview.ts +2 -2
  35. package/cli/commands/record.ts +2 -2
  36. package/cli/commands/report-issue.ts +2 -2
  37. package/cli/commands/screenshot-layers.ts +1 -1
  38. package/cli/commands/screenshot.ts +1 -1
  39. package/cli/commands/setup.ts +2 -2
  40. package/cli/commands/test.ts +440 -115
  41. package/cli/commands/three-mode.ts +1 -1
  42. package/cli/commands/upload.ts +5 -5
  43. package/cli/commands/what-happened.ts +1 -1
  44. package/cli/desktop-companion.ts +3 -3
  45. package/cli/drivers/playwright-sim-host.ts +2 -2
  46. package/cli/flow-file.ts +7 -0
  47. package/cli/flow-live-status.ts +65 -8
  48. package/cli/help-core.ts +3 -3
  49. package/cli/hidden-runtime-alias.ts +1 -1
  50. package/cli/maestro-report.ts +178 -0
  51. package/cli/main.ts +1 -1
  52. package/cli/outbound-endpoints.ts +185 -136
  53. package/cli/parse-args.ts +1 -1
  54. package/cli/registry.ts +1 -1
  55. package/cli/resolve-assets.ts +1 -1
  56. package/cli/run-registry.ts +8 -0
  57. package/cli/send-to-box.ts +4 -4
  58. package/cli/ws-bridge.ts +3 -5
  59. package/detox/expectations.ts +1 -1
  60. package/detox/gestures.ts +2 -2
  61. package/detox/index.ts +71 -29
  62. package/detox/jest-preset.cjs +3 -3
  63. package/detox/jest-setup-after-env.cjs +1 -1
  64. package/dist-lib/agent-daemon-client.cjs +2 -2
  65. package/dist-lib/agent-events.cjs +1 -1
  66. package/dist-lib/agent-identity.cjs +1 -1
  67. package/dist-lib/agent-sessions.cjs +2 -2
  68. package/dist-lib/attached-projects.cjs +2 -2
  69. package/dist-lib/auth/shared-session.cjs +1 -1
  70. package/dist-lib/backend-origin.cjs +1 -1
  71. package/dist-lib/beta.cjs +1 -1
  72. package/dist-lib/beta.mjs +1 -1
  73. package/dist-lib/bridge-constants.cjs +1 -1
  74. package/dist-lib/bridge-contract-input.cjs +1 -1
  75. package/dist-lib/bridge-contract-input.mjs +1 -1
  76. package/dist-lib/bridge-contract.cjs +1 -1
  77. package/dist-lib/bridge-contract.mjs +1 -1
  78. package/dist-lib/capture-contract.cjs +94 -3
  79. package/dist-lib/capture-contract.mjs +93 -3
  80. package/dist-lib/cli-constants.cjs +1 -1
  81. package/dist-lib/cloud-contract.cjs +1 -1
  82. package/dist-lib/cloud-contract.mjs +1 -1
  83. package/dist-lib/cloud.cjs +2 -2
  84. package/dist-lib/cloud.mjs +2 -2
  85. package/dist-lib/config.cjs +1 -1
  86. package/dist-lib/detox/index.cjs +106 -25
  87. package/dist-lib/detox/jest-preset.cjs +3 -3
  88. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  89. package/dist-lib/home-paths.cjs +1 -1
  90. package/dist-lib/host/bridge-host.cjs +137 -61
  91. package/dist-lib/host/fetch-proxy-handler.cjs +28 -2
  92. package/dist-lib/host/fetch-proxy-overrides.cjs +33 -3
  93. package/dist-lib/host/fetch-proxy-overrides.mjs +30 -2
  94. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  95. package/dist-lib/host/websocket-proxy.cjs +1 -1
  96. package/dist-lib/index.cjs +209 -16
  97. package/dist-lib/jump-to-source-babel.cjs +1 -1
  98. package/dist-lib/jump-to-source-native.cjs +1 -1
  99. package/dist-lib/menu.cjs +2 -2
  100. package/dist-lib/menu.mjs +2 -2
  101. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  102. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  103. package/dist-lib/metro-production-bundle.cjs +3 -3
  104. package/dist-lib/metro-production-bundle.mjs +3 -3
  105. package/dist-lib/metro.cjs +8 -8
  106. package/dist-lib/profiles.cjs +1 -1
  107. package/dist-lib/public-brand.cjs +2 -2
  108. package/dist-lib/react-native-host-modules.cjs +1 -1
  109. package/dist-lib/react-native-host-modules.mjs +1 -1
  110. package/dist-lib/render-mode.cjs +1 -1
  111. package/dist-lib/scripts/dev-server-scanner.cjs +31 -15
  112. package/dist-lib/sdk.cjs +3 -2
  113. package/dist-lib/sdk.mjs +3 -2
  114. package/dist-lib/skills.cjs +362 -163
  115. package/dist-lib/swift-submit.cjs +1 -1
  116. package/dist-lib/swift-submit.mjs +1 -1
  117. package/dist-lib/swift.cjs +583 -16
  118. package/dist-lib/vite.cjs +29 -14
  119. package/package.json +9 -9
  120. package/scripts/dev-server-scanner.ts +41 -14
  121. package/scripts/optional-demo-registry.ts +3 -3
  122. package/skills/contrast/SKILL.md +9 -9
  123. package/skills/rnx-debug/SKILL.md +14 -4
  124. package/skills/rnx-perf/SKILL.md +5 -5
  125. package/skills/rnx-setup/SKILL.md +11 -11
  126. package/skills/rnx-test/SKILL.md +11 -11
  127. package/skills/rnx-visual/SKILL.md +6 -6
  128. package/src/agent-sessions.ts +3 -3
  129. package/src/attached-projects.ts +1 -1
  130. package/src/bridge-contract-input.ts +1 -1
  131. package/src/bridge-contract.ts +23 -5
  132. package/src/capture-contract.ts +145 -3
  133. package/src/config.ts +2 -2
  134. package/src/connect.ts +2 -2
  135. package/src/home-paths.ts +3 -3
  136. package/src/host/bridge-host.ts +2 -2
  137. package/src/host/fetch-proxy-handler.ts +4 -3
  138. package/src/host/fetch-proxy-overrides.ts +41 -0
  139. package/src/host/plane-host.ts +2 -2
  140. package/src/index.ts +3 -0
  141. package/src/metro-plugin.ts +4 -4
  142. package/src/metro-production-bundle.ts +2 -2
  143. package/src/native-seam-manifest.ts +6 -6
  144. package/src/public-brand.ts +1 -1
  145. package/src/rolldown-plugin-swift.ts +8 -5
  146. package/src/runtime-assets.ts +8 -6
  147. package/src/screenshots/frame-compose.ts +24 -56
  148. package/src/screenshots/org-deck.ts +1 -1
  149. package/src/screenshots/registry.ts +4 -4
  150. package/src/screenshots/schema.ts +1 -1
  151. package/src/sdk.ts +2 -2
  152. package/src/settings-bundle.ts +1 -1
  153. package/src/sim-client.ts +2 -4
  154. package/src/skills/builtin/compat-check.ts +1 -1
  155. package/src/vite-plugin-one.ts +1 -1
  156. package/src/vite-plugin-swift.ts +11 -6
  157. package/src/vite-plugin.ts +30 -17
  158. package/src/worklets-babel.ts +2 -2
@@ -9,7 +9,7 @@ import { dirname, extname, resolve } from 'node:path'
9
9
  import {
10
10
  isThreeModeSetting,
11
11
  THREE_MODE_SETTING_IDS,
12
- } from 'sootsim-engine/screenshots/tokens'
12
+ } from 'peach-engine/screenshots/tokens'
13
13
  import { rnxExit } from '../run-rnx'
14
14
  import {
15
15
  callInBridgeWrite,
@@ -9,18 +9,18 @@ import { createHash } from 'crypto'
9
9
  import { readFileSync } from 'fs'
10
10
  import path from 'path'
11
11
  import { gzipSync, gunzipSync } from 'zlib'
12
- import { shouldCompress } from 'sootsim-engine/preview/compress'
13
- import { brotliBundle } from 'sootsim-engine/preview/compress-node'
12
+ import { shouldCompress } from 'peach-engine/preview/compress'
13
+ import { brotliBundle } from 'peach-engine/preview/compress-node'
14
14
  import {
15
15
  runPresignedUpload,
16
16
  type InitRequestBody,
17
- } from 'sootsim-engine/preview/presigned-upload'
17
+ } from 'peach-engine/preview/presigned-upload'
18
18
  import {
19
19
  TRACE_CAPTURE_POSTROLL_MS,
20
20
  TRACE_CAPTURE_PREROLL_MS,
21
21
  isTraceTimelineEvent,
22
22
  parseTraceTimelineJsonl,
23
- } from 'sootsim-engine/preview/trace'
23
+ } from 'peach-engine/preview/trace'
24
24
  import { rnxPublicBrand } from '../../src/public-brand'
25
25
  import { fetchAppFontFiles } from '../app-fonts'
26
26
  import { fetchAppSplashImages } from '../app-splash'
@@ -34,7 +34,7 @@ import {
34
34
  evalInBridge,
35
35
  parseBridgeCliArgs,
36
36
  } from '../ws-bridge'
37
- import type { SootSimTimelineEvent } from '@rnx/globals'
37
+ import type { SootSimTimelineEvent } from '@peach/globals'
38
38
 
39
39
  interface UploadOptions {
40
40
  port?: number
@@ -25,7 +25,7 @@ import {
25
25
  inspectTimelineSummary,
26
26
  NOISY_TIMELINE_KINDS,
27
27
  } from './inspect/core'
28
- import type { SootSimTimelineEvent, SootSimTimelineQuery } from '@rnx/globals'
28
+ import type { SootSimTimelineEvent, SootSimTimelineQuery } from '@peach/globals'
29
29
 
30
30
  interface WhatHappenedOptions {
31
31
  port?: number
@@ -12,7 +12,7 @@ function getSootsimRoot(): string | null {
12
12
  if (_sootsimRoot !== undefined) return _sootsimRoot
13
13
  try {
14
14
  _sootsimRoot = dirname(
15
- fileURLToPath(import.meta.resolve('sootsim-engine/package.json')),
15
+ fileURLToPath(import.meta.resolve('peach-engine/package.json')),
16
16
  )
17
17
  } catch {
18
18
  _sootsimRoot = null
@@ -54,7 +54,7 @@ function findDevElectron(): DesktopCompanionInstall | null {
54
54
  if (!existsSync(engineMain)) return null
55
55
 
56
56
  // walk up from engine dir looking for a node_modules/.bin/electron.
57
- // the repo layout is packages/sootsim-engine next to a root node_modules,
57
+ // the repo layout is packages/peach-engine next to a root node_modules,
58
58
  // but also works when installed as a dependency.
59
59
  const electronBin = findElectronBin(engineDir)
60
60
  if (!electronBin) return null
@@ -194,7 +194,7 @@ export function findDesktopCompanion(): DesktopCompanionInstall | null {
194
194
  if (!existsSync(engineMain)) {
195
195
  console.error(
196
196
  ` dist-electron/main.cjs missing in ${engineDir}.\n` +
197
- ` run: bun run --cwd packages/sootsim-engine build:electron-main\n` +
197
+ ` run: bun run --cwd packages/peach-engine build:electron-main\n` +
198
198
  ` (or keep it fresh with: bun run watch:sootsim:electron-main)`,
199
199
  )
200
200
  } else {
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  readBrowserCompositeCaptureRequest,
3
3
  resolveBrowserCompositeCdpClip,
4
- } from '../../../sootsim-engine/src/capture/browser-composite'
5
- import { createBrowserPointerDispatcher } from '../../../sootsim-engine/src/render-worker/browser-pointer'
4
+ } from '../../../peach-engine/src/capture/browser-composite'
5
+ import { createBrowserPointerDispatcher } from '../../../peach-engine/src/render-worker/browser-pointer'
6
6
  // the detached browser host, as CJS source.
7
7
  //
8
8
  // it resolves playwright from the absolute path the driver hands it, launches
package/cli/flow-file.ts CHANGED
@@ -16,6 +16,13 @@ export interface FlowFrontmatter {
16
16
  env?: Record<string, string>
17
17
  // maestro-compatible: workspace tag filtering (config.yml includeTags/excludeTags)
18
18
  tags?: string[]
19
+ // maestro-compatible: display name for reports. defaults the junit
20
+ // testcase id, name, and classname when properties does not override them.
21
+ name?: string
22
+ // maestro-compatible: custom junit <property> entries. junitId and
23
+ // junitClassname are reserved: they set the testcase attributes and are
24
+ // not emitted as properties.
25
+ properties?: Record<string, string>
19
26
  }
20
27
 
21
28
  // parse flow file. accepts three shapes:
@@ -44,17 +44,21 @@ export class FlowLiveStatusReporter {
44
44
  private getSimId?: () => string | undefined,
45
45
  ) {}
46
46
 
47
- private async send(payload: Record<string, unknown>, simId: string | undefined) {
47
+ private async send(
48
+ payload: Record<string, unknown>,
49
+ simId: string | undefined,
50
+ ): Promise<unknown> {
48
51
  const reply: unknown = await this.bridge.send({
49
52
  type: 'flowStatus',
50
53
  simId,
51
54
  status: { runId: this.runId, flowName: this.flowName, ...payload },
52
55
  })
53
56
  this.paused = replyPaused(reply)
57
+ return reply
54
58
  }
55
59
 
56
- private async push(payload: Record<string, unknown>): Promise<void> {
57
- if (this.disabled) return
60
+ private async push(payload: Record<string, unknown>): Promise<unknown> {
61
+ if (this.disabled) return undefined
58
62
  try {
59
63
  const simId = this.getSimId?.()
60
64
  if (
@@ -77,19 +81,68 @@ export class FlowLiveStatusReporter {
77
81
  }
78
82
  }
79
83
  this.lastSimId = simId
80
- await this.send(payload, simId)
84
+ return await this.send(payload, simId)
81
85
  } catch (error) {
82
86
  this.disabled = true
83
87
  console.log(
84
88
  `[flow] live status unavailable (${error instanceof Error ? error.message.slice(0, 120) : error}) — continuing without the devtools rail`,
85
89
  )
90
+ return undefined
86
91
  }
87
92
  }
88
93
 
89
- async plan(steps: PlanStep[]): Promise<void> {
94
+ async plan(steps: PlanStep[], options: { dryRun?: boolean } = {}): Promise<void> {
90
95
  this.plannedSteps = steps.map((step) => ({ ...step }))
91
96
  this.completedSteps.clear()
92
- await this.push({ phase: 'plan', state: 'running', steps, source: this.flowSource })
97
+ await this.push({
98
+ phase: 'plan',
99
+ state: options.dryRun ? 'idle' : 'running',
100
+ dryRun: options.dryRun,
101
+ steps,
102
+ source: this.flowSource,
103
+ })
104
+ }
105
+
106
+ async waitForStart(): Promise<void> {
107
+ if (this.disabled) return
108
+ while (!this.disabled) {
109
+ await new Promise((resolve) => setTimeout(resolve, 200))
110
+ const reply = await this.push({ phase: 'heartbeat', state: 'idle' })
111
+ if (
112
+ reply &&
113
+ typeof reply === 'object' &&
114
+ 'control' in reply &&
115
+ typeof (reply as { control?: unknown }).control === 'object'
116
+ ) {
117
+ const ctrl = (reply as { control: { start?: boolean } }).control
118
+ if (ctrl?.start === true) {
119
+ return
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ async waitForResetOrRerun(): Promise<'reset' | 'start' | 'exit'> {
126
+ if (this.disabled) return 'exit'
127
+ while (!this.disabled) {
128
+ await new Promise((resolve) => setTimeout(resolve, 200))
129
+ const reply = await this.push({ phase: 'heartbeat' })
130
+ if (
131
+ reply &&
132
+ typeof reply === 'object' &&
133
+ 'control' in reply &&
134
+ typeof (reply as { control?: unknown }).control === 'object'
135
+ ) {
136
+ const ctrl = (reply as { control: { reset?: boolean; start?: boolean } }).control
137
+ if (ctrl?.reset === true) {
138
+ return 'reset'
139
+ }
140
+ if (ctrl?.start === true) {
141
+ return 'start'
142
+ }
143
+ }
144
+ }
145
+ return 'exit'
93
146
  }
94
147
 
95
148
  async step(
@@ -114,10 +167,14 @@ export class FlowLiveStatusReporter {
114
167
  // steps only, so a pause never interrupts a step mid-gesture.
115
168
  async waitWhilePaused(): Promise<void> {
116
169
  if (this.disabled) return
117
- while (this.paused) {
170
+ const reply = await this.push({ phase: 'heartbeat' })
171
+ if (replyPaused(reply)) {
172
+ this.paused = true
173
+ }
174
+ while (this.paused && !this.disabled) {
118
175
  console.log('[flow] paused from devtools — waiting…')
119
176
  while (this.paused && !this.disabled) {
120
- await new Promise((resolve) => setTimeout(resolve, PAUSE_POLL_MS))
177
+ await new Promise((resolve) => setTimeout(resolve, 200))
121
178
  await this.push({ phase: 'heartbeat', state: 'paused' })
122
179
  }
123
180
  if (!this.disabled) console.log('[flow] resumed')
package/cli/help-core.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { cliCommandMetas } from '@rnx/skills/cli'
1
+ import { cliCommandMetas } from '@peach/skills/cli'
2
2
  import {
3
3
  renderCliCommandHelp,
4
4
  renderCliGroupHelp,
5
5
  renderCliIndexHelp,
6
- } from '@rnx/skills/cli/renderers'
7
- import { formatFlagHelp } from 'sootsim-engine/settings/cli-flags'
6
+ } from '@peach/skills/cli/renderers'
7
+ import { formatFlagHelp } from 'peach-engine/settings/cli-flags'
8
8
  import {
9
9
  DEFAULT_SOOTSIM_BRIDGE_PORT,
10
10
  DEFAULT_SOOTSIM_BRIDGE_URL,
@@ -1,4 +1,4 @@
1
- import { getCliInspectVerbMetaByPath } from '@rnx/skills/cli'
1
+ import { getCliInspectVerbMetaByPath } from '@peach/skills/cli'
2
2
  import type { ParsedArgs } from './parse-args'
3
3
 
4
4
  const HIDDEN_RUNTIME_ALIAS_GROUPS = ['do', 'get'] as const
@@ -0,0 +1,178 @@
1
+ // maestro-compatible run reports: junit xml, per-flow commands.json, and the
2
+ // session/flow maestro.log text. pure builders over playback receipts — no
3
+ // bridge, no filesystem — so the workspace tests cover them without a sim.
4
+ //
5
+ // the shapes mirror maestro's own outputs (test-reports-and-artifacts): one
6
+ // <testsuite name="Test Suite"> per invocation, flow-header `properties:`
7
+ // as <property> children, junitId/junitClassname reserved for the testcase
8
+ // attributes, commands.json carrying one entry per executed step.
9
+
10
+ import type { SootSimFlowTraceStep } from './bridge-flow-runner'
11
+
12
+ export function escapeXml(value: string): string {
13
+ return value
14
+ .replace(/&/g, '&amp;')
15
+ .replace(/</g, '&lt;')
16
+ .replace(/>/g, '&gt;')
17
+ .replace(/"/g, '&quot;')
18
+ .replace(/'/g, '&apos;')
19
+ }
20
+
21
+ export interface JUnitCaseInput {
22
+ // flow path as printed in the run (relative to the invocation cwd)
23
+ file: string
24
+ name: string
25
+ id: string
26
+ classname: string
27
+ // custom flow-header properties with the reserved junit keys removed
28
+ properties: Record<string, string>
29
+ tags: string[]
30
+ timeSeconds: number
31
+ failure: { message: string; body: string } | null
32
+ }
33
+
34
+ export interface JUnitSuiteInput {
35
+ device: string
36
+ cases: JUnitCaseInput[]
37
+ }
38
+
39
+ function formatJUnitTime(seconds: number): string {
40
+ return (Number.isFinite(seconds) && seconds >= 0 ? seconds : 0).toFixed(3)
41
+ }
42
+
43
+ export function buildJUnitXml(suite: JUnitSuiteInput): string {
44
+ const failures = suite.cases.filter((c) => c.failure !== null).length
45
+ const totalTime = suite.cases.reduce(
46
+ (sum, c) => sum + (Number.isFinite(c.timeSeconds) ? c.timeSeconds : 0),
47
+ 0,
48
+ )
49
+ const lines = [
50
+ '<?xml version="1.0" encoding="UTF-8"?>',
51
+ '<testsuites>',
52
+ ` <testsuite name="Test Suite" device="${escapeXml(suite.device)}" tests="${suite.cases.length}" failures="${failures}" time="${formatJUnitTime(totalTime)}">`,
53
+ ]
54
+ for (const c of suite.cases) {
55
+ lines.push(
56
+ ` <testcase id="${escapeXml(c.id)}" name="${escapeXml(c.name)}" classname="${escapeXml(c.classname)}" file="${escapeXml(c.file)}" time="${formatJUnitTime(c.timeSeconds)}">`,
57
+ )
58
+ const properties: Array<{ name: string; value: string }> = []
59
+ if (c.tags.length > 0) {
60
+ properties.push({ name: 'tags', value: c.tags.join(',') })
61
+ }
62
+ for (const [name, value] of Object.entries(c.properties)) {
63
+ properties.push({ name, value })
64
+ }
65
+ if (properties.length > 0) {
66
+ lines.push(' <properties>')
67
+ for (const p of properties) {
68
+ lines.push(
69
+ ` <property name="${escapeXml(p.name)}" value="${escapeXml(p.value)}"/>`,
70
+ )
71
+ }
72
+ lines.push(' </properties>')
73
+ }
74
+ if (c.failure) {
75
+ lines.push(` <failure message="${escapeXml(c.failure.message)}">`)
76
+ lines.push(escapeXml(c.failure.body))
77
+ lines.push(' </failure>')
78
+ }
79
+ lines.push(' </testcase>')
80
+ }
81
+ lines.push(' </testsuite>')
82
+ lines.push('</testsuites>')
83
+ return `${lines.join('\n')}\n`
84
+ }
85
+
86
+ // the first failed trace step as a junit failure. a flow that died before
87
+ // producing steps (parse error, sim never attached) has no failed step, so
88
+ // the caller passes its own fallback text instead of an empty failure.
89
+ export function failureFromTraceSteps(
90
+ steps: SootSimFlowTraceStep[],
91
+ fallbackMessage: string,
92
+ ): { message: string; body: string } {
93
+ const failed = steps.find((step) => step.status === 'failure')
94
+ if (!failed) {
95
+ return { message: fallbackMessage, body: fallbackMessage }
96
+ }
97
+ const error = failed.error || 'flow step failed'
98
+ const message = error.split('\n')[0] || fallbackMessage
99
+ const bodyLines = [`step ${failed.stepIndex + 1} (${failed.stepName}): ${error}`]
100
+ if (failed.screenshotPath) {
101
+ bodyLines.push(`screenshot: ${failed.screenshotPath}`)
102
+ }
103
+ return { message, body: bodyLines.join('\n') }
104
+ }
105
+
106
+ export interface FlowCommandsEntry {
107
+ sequenceNumber: number
108
+ command: string
109
+ status: SootSimFlowTraceStep['status']
110
+ durationMs: number
111
+ error?: string
112
+ artifacts: string[]
113
+ }
114
+
115
+ export function buildFlowCommandsJson(steps: SootSimFlowTraceStep[]): string {
116
+ const entries: FlowCommandsEntry[] = steps.map((step) => ({
117
+ sequenceNumber: step.stepIndex + 1,
118
+ command: step.targetLabel ? `${step.stepName} ${step.targetLabel}` : step.stepName,
119
+ status: step.status,
120
+ durationMs: step.durationMs,
121
+ ...(step.error ? { error: step.error } : {}),
122
+ artifacts: step.screenshotPath ? [step.screenshotPath] : [],
123
+ }))
124
+ return `${JSON.stringify(entries, null, 2)}\n`
125
+ }
126
+
127
+ export interface SessionLogFlow {
128
+ file: string
129
+ passed: boolean
130
+ durationMs: number
131
+ steps: SootSimFlowTraceStep[]
132
+ }
133
+
134
+ function formatLogDuration(ms: number): string {
135
+ return `${(Math.max(0, ms) / 1000).toFixed(1)}s`
136
+ }
137
+
138
+ function formatLogStep(step: SootSimFlowTraceStep): string {
139
+ const label = step.targetLabel ? `${step.stepName} ${step.targetLabel}` : step.stepName
140
+ const base = ` ${step.stepIndex + 1}. ${label} - ${step.status} (${formatLogDuration(step.durationMs)})`
141
+ if (step.status === 'failure' && step.error) {
142
+ return `${base}: ${step.error.split('\n')[0]}`
143
+ }
144
+ return base
145
+ }
146
+
147
+ // one flow's section, shared by the session log and the per-flow logs file
148
+ // so the two never disagree about what a flow did.
149
+ export function buildFlowLogLines(flow: SessionLogFlow): string[] {
150
+ const lines = [
151
+ `# flow: ${flow.file} - ${flow.passed ? 'pass' : 'FAIL'} (${formatLogDuration(flow.durationMs)}, ${flow.steps.length} steps)`,
152
+ ]
153
+ for (const step of flow.steps) {
154
+ lines.push(formatLogStep(step))
155
+ }
156
+ return lines
157
+ }
158
+
159
+ export function buildSessionLog(input: {
160
+ startedAtIso: string
161
+ root: string
162
+ device: string
163
+ flows: SessionLogFlow[]
164
+ }): string {
165
+ const passed = input.flows.filter((flow) => flow.passed).length
166
+ const lines = [
167
+ '# rnx maestro session log',
168
+ `# started: ${input.startedAtIso}`,
169
+ `# root: ${input.root}`,
170
+ `# device: ${input.device}`,
171
+ `# result: ${passed}/${input.flows.length} passed`,
172
+ ]
173
+ for (const flow of input.flows) {
174
+ lines.push('#')
175
+ lines.push(...buildFlowLogLines(flow))
176
+ }
177
+ return `${lines.join('\n')}\n`
178
+ }
package/cli/main.ts CHANGED
@@ -254,7 +254,7 @@ const commandArgsWithSim = globalSimTarget
254
254
 
255
255
  // apply device/theme/home-screen to settings store if provided
256
256
  if (device || theme || homeScreen) {
257
- const { settingsStore } = await import('sootsim-engine/settings/store')
257
+ const { settingsStore } = await import('peach-engine/settings/store')
258
258
  const overrides: Record<string, any> = {}
259
259
  if (device) overrides.deviceModel = device
260
260
  if (theme) overrides.colorScheme = theme