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
@@ -0,0 +1,354 @@
1
+ // rnx test --cloud live-dev-stack publisher (tunnel CLI half).
2
+ //
3
+ // when --app names a live packager (http loopback) instead of a prebuilt
4
+ // bundle, the submitter publishes that stack through a token-gated
5
+ // cloudflared quick tunnel and submits the tunnel origin plus the mode-2
6
+ // fields (hostHeader, jobToken, sidecarPorts). the recorder projects the
7
+ // origin back to loopback (scripts/runner/sootsim-recorder.ts), so flows
8
+ // keep their unmodified localhost authorities.
9
+ //
10
+ // the handshake mirrors packages/contrast-runner/src/hosted.ts, which this
11
+ // published package cannot import: Bearer-gated proxy between cloudflared
12
+ // and the dev server, --http-host-header on the dev authority, forwarded
13
+ // headers stripped so Metro emits loopback authorities the recorder
14
+ // mirrors. header names must match hostedPorts.ts and loopback-proxy.mjs.
15
+
16
+ import { spawn, spawnSync, type ChildProcess } from 'node:child_process'
17
+ import { randomBytes } from 'node:crypto'
18
+ import {
19
+ chmodSync,
20
+ existsSync,
21
+ mkdirSync,
22
+ readFileSync,
23
+ unlinkSync,
24
+ writeFileSync,
25
+ } from 'node:fs'
26
+ import { createServer, request as httpRequest } from 'node:http'
27
+ import { tmpdir } from 'node:os'
28
+ import { join } from 'node:path'
29
+
30
+ // must match packages/contrast-runner/src/hostedPorts.ts.
31
+ const UPSTREAM_PORT_HEADER = 'x-contrast-upstream-port'
32
+ const APPLICATION_AUTH_HEADER = 'x-contrast-application-authorization'
33
+ const MAX_SIDECAR_PORTS = 8
34
+
35
+ export type CloudTestSidecarResult =
36
+ | { ok: true; ports: number[] }
37
+ | { ok: false; message: string }
38
+
39
+ // same grammar as the jobs API (parseHostedSidecarPorts): comma-separated
40
+ // ports 1-65535, at most 8, deduped, primary excluded.
41
+ export function parseCloudTestSidecarPorts(
42
+ value: string | null | undefined,
43
+ primaryPort?: number,
44
+ ): CloudTestSidecarResult {
45
+ if (!value?.trim()) return { ok: true, ports: [] }
46
+ const values = value.split(',').map((part) => part.trim())
47
+ if (values.length > MAX_SIDECAR_PORTS) {
48
+ return {
49
+ ok: false,
50
+ message: `sidecar-ports accepts at most ${MAX_SIDECAR_PORTS} ports`,
51
+ }
52
+ }
53
+ const ports: number[] = []
54
+ for (const raw of values) {
55
+ if (!/^[1-9][0-9]{0,4}$/.test(raw)) {
56
+ return {
57
+ ok: false,
58
+ message: 'sidecar-ports must be a comma-separated list of ports from 1 to 65535',
59
+ }
60
+ }
61
+ const port = Number(raw)
62
+ if (port > 65_535) {
63
+ return {
64
+ ok: false,
65
+ message: 'sidecar-ports must be a comma-separated list of ports from 1 to 65535',
66
+ }
67
+ }
68
+ if (port !== primaryPort && !ports.includes(port)) ports.push(port)
69
+ }
70
+ return { ok: true, ports }
71
+ }
72
+
73
+ function isLoopbackHostname(hostname: string): boolean {
74
+ return (
75
+ hostname === 'localhost' ||
76
+ hostname === '127.0.0.1' ||
77
+ hostname === '[::1]' ||
78
+ hostname === '::1'
79
+ )
80
+ }
81
+
82
+ // a live dev stack is an http(s) loopback URL: the packager the recorder
83
+ // cannot reach directly. https loopback is accepted for tunnels that
84
+ // terminate TLS locally; everything else (files, https remotes, share ids)
85
+ // stays on the prebuilt path.
86
+ export function parseLiveDevStackApp(app: string): { port: number } | null {
87
+ if (!app.startsWith('http://') && !app.startsWith('https://')) return null
88
+ let url: URL
89
+ try {
90
+ url = new URL(app)
91
+ } catch {
92
+ return null
93
+ }
94
+ if (!isLoopbackHostname(url.hostname)) return null
95
+ const port = Number(url.port)
96
+ if (!Number.isInteger(port) || port < 1 || port > 65_535) return null
97
+ return { port }
98
+ }
99
+
100
+ function resolveUpstreamPort(
101
+ value: string | string[] | undefined,
102
+ primaryPort: number,
103
+ sidecarPorts: readonly number[],
104
+ ): number | null {
105
+ if (value === undefined) return primaryPort
106
+ if (Array.isArray(value) || !/^[1-9][0-9]{0,4}$/.test(value)) return null
107
+ const port = Number(value)
108
+ return sidecarPorts.includes(port) ? port : null
109
+ }
110
+
111
+ // Bearer-gating reverse proxy between cloudflared and the dev server.
112
+ // rejects without the per-job token; strips cloudflared forwarding headers
113
+ // so Metro builds absolute URLs on the dev authority the recorder mirrors.
114
+ export function startCloudTestTokenProxy(
115
+ listenPort: number,
116
+ upstreamPort: number,
117
+ sidecarPorts: readonly number[],
118
+ token: string,
119
+ ): Promise<{ close: () => void } | null> {
120
+ const server = createServer((req, res) => {
121
+ const auth = req.headers.authorization || ''
122
+ if (auth !== `Bearer ${token}`) {
123
+ res.writeHead(401, { 'content-type': 'text/plain' })
124
+ res.end('unauthorized')
125
+ return
126
+ }
127
+ const headers = { ...req.headers }
128
+ const applicationAuthorization = headers[APPLICATION_AUTH_HEADER]
129
+ delete headers[APPLICATION_AUTH_HEADER]
130
+ delete headers.authorization
131
+ if (typeof applicationAuthorization === 'string') {
132
+ headers.authorization = applicationAuthorization
133
+ }
134
+ const selectedPort = resolveUpstreamPort(
135
+ headers[UPSTREAM_PORT_HEADER],
136
+ upstreamPort,
137
+ sidecarPorts,
138
+ )
139
+ delete headers[UPSTREAM_PORT_HEADER]
140
+ if (selectedPort === null) {
141
+ res.writeHead(403, { 'content-type': 'text/plain' })
142
+ res.end('upstream port is not enabled for this run')
143
+ return
144
+ }
145
+ if (selectedPort !== upstreamPort) headers.host = `127.0.0.1:${selectedPort}`
146
+ for (const key of Object.keys(headers)) {
147
+ const lower = key.toLowerCase()
148
+ if (lower.startsWith('x-forwarded-') || lower.startsWith('cf-')) {
149
+ delete headers[key]
150
+ }
151
+ }
152
+ const up = httpRequest(
153
+ {
154
+ host: '127.0.0.1',
155
+ port: selectedPort,
156
+ path: req.url,
157
+ method: req.method,
158
+ headers,
159
+ },
160
+ (upRes) => {
161
+ res.writeHead(upRes.statusCode || 502, upRes.headers)
162
+ upRes.pipe(res)
163
+ },
164
+ )
165
+ up.on('error', (err) => {
166
+ res.writeHead(502, { 'content-type': 'text/plain' })
167
+ res.end(`upstream error: ${err.message}`)
168
+ })
169
+ req.pipe(up)
170
+ })
171
+ return new Promise((resolve) => {
172
+ server.on('error', () => resolve(null))
173
+ server.listen(listenPort, '127.0.0.1', () => {
174
+ resolve({ close: () => server.close() })
175
+ })
176
+ })
177
+ }
178
+
179
+ function cloudflaredAvailable(): boolean {
180
+ const probe = spawnSync('cloudflared', ['--version'], { stdio: 'ignore' })
181
+ return probe.status === 0
182
+ }
183
+
184
+ async function ensureCloudflared(): Promise<boolean> {
185
+ if (cloudflaredAvailable()) return true
186
+ const archMap: Record<string, string> = { x64: 'amd64', arm64: 'arm64' }
187
+ const arch = archMap[process.arch]
188
+ const osMap: Record<string, string> = { linux: 'linux', darwin: 'darwin' }
189
+ const os = osMap[process.platform]
190
+ if (!arch || !os) return false
191
+ const dest = join(tmpdir(), 'rnx-cloudflared')
192
+ const binary = join(dest, 'cloudflared')
193
+ if (existsSync(binary)) {
194
+ process.env.PATH = `${dest}:${process.env.PATH ?? ''}`
195
+ return cloudflaredAvailable()
196
+ }
197
+ // linux publishes a bare binary; darwin publishes a tarball.
198
+ const asset =
199
+ os === 'darwin' ? `cloudflared-${os}-${arch}.tgz` : `cloudflared-${os}-${arch}`
200
+ try {
201
+ const res = await fetch(
202
+ `https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`,
203
+ )
204
+ if (!res.ok) return false
205
+ mkdirSync(dest, { recursive: true })
206
+ if (os === 'darwin') {
207
+ const tgz = join(dest, asset)
208
+ writeFileSync(tgz, Buffer.from(await res.arrayBuffer()))
209
+ const untar = spawnSync('tar', ['-xzf', tgz, '-C', dest])
210
+ try {
211
+ unlinkSync(tgz)
212
+ } catch {}
213
+ if (untar.status !== 0 || !existsSync(binary)) return false
214
+ } else {
215
+ writeFileSync(binary, Buffer.from(await res.arrayBuffer()))
216
+ }
217
+ } catch {
218
+ return false
219
+ }
220
+ chmodSync(binary, 0o755)
221
+ process.env.PATH = `${dest}:${process.env.PATH ?? ''}`
222
+ return cloudflaredAvailable()
223
+ }
224
+
225
+ function sleep(ms: number): Promise<void> {
226
+ return new Promise((resolve) => setTimeout(resolve, ms))
227
+ }
228
+
229
+ function childAlive(child: ChildProcess): boolean {
230
+ return child.exitCode === null && child.signalCode === null
231
+ }
232
+
233
+ async function launchQuickTunnel(
234
+ originUrl: string,
235
+ hostHeader: string,
236
+ logFile: string,
237
+ ): Promise<{ url: string; child: ChildProcess } | null> {
238
+ writeFileSync(logFile, '')
239
+ const child = spawn(
240
+ 'cloudflared',
241
+ [
242
+ 'tunnel',
243
+ '--url',
244
+ originUrl,
245
+ '--http-host-header',
246
+ hostHeader,
247
+ '--protocol',
248
+ 'http2',
249
+ '--no-autoupdate',
250
+ ],
251
+ { stdio: ['ignore', 'pipe', 'pipe'] },
252
+ )
253
+ // cloudflared logs the tunnel URL to stderr; mirror both streams into the
254
+ // log file and poll it for the trycloudflare origin.
255
+ const lines: string[] = []
256
+ child.stderr?.on('data', (chunk) => {
257
+ lines.push(String(chunk))
258
+ writeFileSync(logFile, lines.join(''))
259
+ })
260
+ child.stdout?.on('data', (chunk) => {
261
+ lines.push(String(chunk))
262
+ writeFileSync(logFile, lines.join(''))
263
+ })
264
+ for (let i = 0; i < 60; i++) {
265
+ let text = ''
266
+ try {
267
+ text = readFileSync(logFile, 'utf8')
268
+ } catch {
269
+ text = ''
270
+ }
271
+ const match = text.match(/https:\/\/[-a-z0-9]+\.trycloudflare\.com/g)
272
+ const url = match?.[match.length - 1]
273
+ if (url) return { url, child }
274
+ if (!childAlive(child)) return null
275
+ await sleep(1000)
276
+ }
277
+ child.kill()
278
+ return null
279
+ }
280
+
281
+ export interface PublishedLiveDevStack {
282
+ targetUrl: string
283
+ hostHeader: string
284
+ jobToken: string
285
+ sidecarPorts: number[]
286
+ close: () => void
287
+ }
288
+
289
+ // publish a live dev stack for one hosted test run. the caller keeps the
290
+ // returned tunnel open until the job reaches a terminal state, then closes
291
+ // it. throws on any setup failure with a message the CLI prints.
292
+ export async function publishLiveDevStack(args: {
293
+ devPort: number
294
+ sidecarPorts: readonly number[]
295
+ }): Promise<PublishedLiveDevStack> {
296
+ const { devPort, sidecarPorts } = args
297
+ const jobToken = randomBytes(16).toString('hex')
298
+ // free loopback port for the token proxy; the advertised authority stays
299
+ // the dev server's own port so Metro URLs resolve on the recorder.
300
+ const { Server } = await import('node:net')
301
+ const proxyPort = await new Promise<number>((resolve, reject) => {
302
+ const probe = new Server()
303
+ probe.once('error', reject)
304
+ probe.listen(0, '127.0.0.1', () => {
305
+ const address = probe.address()
306
+ const port = address && typeof address === 'object' ? address.port : 0
307
+ probe.close(() => resolve(port))
308
+ })
309
+ })
310
+ if (!proxyPort) throw new Error('could not allocate a tunnel proxy port')
311
+ const proxy = await startCloudTestTokenProxy(proxyPort, devPort, sidecarPorts, jobToken)
312
+ if (!proxy) {
313
+ throw new Error(`could not start the tunnel token proxy on 127.0.0.1:${proxyPort}`)
314
+ }
315
+ const closeProxy = proxy.close
316
+ const hostHeader = `127.0.0.1:${devPort}`
317
+ if (!(await ensureCloudflared())) {
318
+ closeProxy()
319
+ throw new Error(
320
+ 'cloudflared is required for live dev stacks (rnx test --cloud with http://localhost:<port>). install it or check your network.',
321
+ )
322
+ }
323
+ const logFile = join(tmpdir(), `rnx-test-tunnel-${Date.now()}-${proxyPort}.log`)
324
+ const tunnel = await launchQuickTunnel(
325
+ `http://127.0.0.1:${proxyPort}`,
326
+ hostHeader,
327
+ logFile,
328
+ )
329
+ if (!tunnel) {
330
+ closeProxy()
331
+ let tail = ''
332
+ try {
333
+ tail = readFileSync(logFile, 'utf8').slice(-500)
334
+ } catch {
335
+ tail = ''
336
+ }
337
+ throw new Error(
338
+ `could not establish the cloudflared tunnel for the live dev stack${tail ? `: ${tail.trim().split('\n').pop()}` : ''}`,
339
+ )
340
+ }
341
+ let closed = false
342
+ return {
343
+ targetUrl: tunnel.url,
344
+ hostHeader,
345
+ jobToken,
346
+ sidecarPorts: [...sidecarPorts],
347
+ close: () => {
348
+ if (closed) return
349
+ closed = true
350
+ tunnel.child.kill()
351
+ closeProxy()
352
+ },
353
+ }
354
+ }
@@ -8,8 +8,8 @@
8
8
  // the bundle is split here rather than by the caller, so the one place that
9
9
  // decides where the parts go is the one place that decides how big they are.
10
10
 
11
- import { PREBUILT_BUNDLE_PART_BYTES, prebuiltBundlePartPath } from '@rnx/box/template'
12
- import type { ProjectFileMeta, ProjectFilePlane } from '@rnx/box/plane'
11
+ import { PREBUILT_BUNDLE_PART_BYTES, prebuiltBundlePartPath } from '@peach/box/template'
12
+ import type { ProjectFileMeta, ProjectFilePlane } from '@peach/box/plane'
13
13
 
14
14
  export class BundleFilePlane implements ProjectFilePlane {
15
15
  private readonly parts = new Map<string, Uint8Array>()
@@ -12,7 +12,7 @@
12
12
  import { execFileSync } from 'node:child_process'
13
13
  import fs from 'node:fs'
14
14
  import path from 'node:path'
15
- import type { ProjectFileMeta, ProjectFilePlane } from '@rnx/box/plane'
15
+ import type { ProjectFileMeta, ProjectFilePlane } from '@peach/box/plane'
16
16
 
17
17
  export class CheckoutRequiresGitError extends Error {}
18
18
 
@@ -8,8 +8,8 @@
8
8
 
9
9
  import { spawn } from 'node:child_process'
10
10
  import path from 'node:path'
11
- import { defineCommand, type CustomCommand } from '@rnx/box/shell'
12
- import { PROJECT_MOUNT } from '@rnx/box/shell'
11
+ import { defineCommand, type CustomCommand } from '@peach/box/shell'
12
+ import { PROJECT_MOUNT } from '@peach/box/shell'
13
13
  import { rnxPublicBrand } from '../../../src/public-brand'
14
14
  import { rnxSelfInvocation } from '../../self-invocation'
15
15
  import { RNX_PLACEMENT_ENV } from '../../ws-bridge'
@@ -32,8 +32,8 @@
32
32
  // whose "no match here" had to be merged.
33
33
 
34
34
  import { spawn } from 'node:child_process'
35
- import { defineCommand, type CustomCommand } from '@rnx/box/shell'
36
- import { PROJECT_MOUNT } from '@rnx/box/shell'
35
+ import { defineCommand, type CustomCommand } from '@peach/box/shell'
36
+ import { PROJECT_MOUNT } from '@peach/box/shell'
37
37
 
38
38
  // generous next to a 414ms p95: this bounds a wedged child, it does not cut off
39
39
  // a large search.
@@ -13,23 +13,23 @@
13
13
  import { spawn } from 'node:child_process'
14
14
  import path from 'node:path'
15
15
  import { createInterface } from 'node:readline'
16
- import { BoxClientError, createBoxClient } from '@rnx/box/client'
17
- import { createBoxHostProvider } from '@rnx/box/host'
18
- import { PROJECT_MOUNT } from '@rnx/box/shell'
19
- import { terminalBoxStack } from '@rnx/box/stack'
16
+ import { BoxClientError, createBoxClient } from '@peach/box/client'
17
+ import { createBoxHostProvider } from '@peach/box/host'
18
+ import { PROJECT_MOUNT } from '@peach/box/shell'
19
+ import { terminalBoxStack } from '@peach/box/stack'
20
20
  import {
21
21
  BoxTemplateRegistry,
22
22
  prebuiltBundleBoxTemplate,
23
23
  sourceControlBoxTemplate,
24
24
  terminalBoxTemplate,
25
- } from '@rnx/box/template'
25
+ } from '@peach/box/template'
26
26
  import {
27
27
  cloudBoxConnectUrl,
28
28
  createCloudBoxProvider,
29
29
  ensureCloudBoxBrowser,
30
30
  heartbeatSession,
31
31
  resolveCloudBoxReference,
32
- } from 'rnx-cloud-box/client'
32
+ } from 'peach-cloud-box/client'
33
33
  import { readDefaultBox, writeDefaultBox } from '../../src/home-paths'
34
34
  import { rnxPublicBrand } from '../../src/public-brand'
35
35
  import { authHeaderOrExit, cloudAccountIdOrExit, type CliAuth } from '../auth'
@@ -41,8 +41,8 @@ import { CheckoutFilePlane, CheckoutRequiresGitError } from './box/checkout-plan
41
41
  import { sharePlane } from './box/plane-share'
42
42
  import { createRnxCommand } from './box/rnx-command'
43
43
  import { createSearchCommand } from './box/search-command'
44
- import type { BoxShellSession } from '@rnx/box/client'
45
- import type { BoxTemplateResolution } from '@rnx/box/template'
44
+ import type { BoxShellSession } from '@peach/box/client'
45
+ import type { BoxTemplateResolution } from '@peach/box/template'
46
46
 
47
47
  interface BoxCommandOptions {
48
48
  port?: number
@@ -8,7 +8,7 @@ import {
8
8
  formatScanCounts,
9
9
  scanDeps,
10
10
  type ScanResult,
11
- } from '@sootsim/compat/web'
11
+ } from '@peach/compat/web'
12
12
  import { findProjectRoot, scanWorkspace } from './detect'
13
13
 
14
14
  interface PackageManifest {
@@ -6,7 +6,7 @@ import {
6
6
  devices,
7
7
  listSelectableDeviceModels,
8
8
  type DeviceModel,
9
- } from 'sootsim-engine/settings'
9
+ } from 'peach-engine/settings'
10
10
  import { WebSocket } from 'ws'
11
11
  import { probePort } from '../../scripts/dev-server-scanner'
12
12
  import { isAppDeepLinkTarget, openAppUrl } from '../../src/app-url'
@@ -425,14 +425,20 @@ async function resolveConnectionInputForCli(target: string): Promise<ResolvedDev
425
425
  try {
426
426
  return await resolveConnectionInput(normalized)
427
427
  } catch (error) {
428
- if (!(error instanceof Error) || !error.message.startsWith("could not resolve a native bundle")) {
428
+ if (
429
+ !(error instanceof Error) ||
430
+ !error.message.startsWith('could not resolve a native bundle')
431
+ ) {
429
432
  throw error
430
433
  }
431
434
  lastError = error
432
435
  }
433
436
  await sleep(DEV_BUNDLE_RESOLVE_INTERVAL_MS)
434
437
  }
435
- throw lastError ?? new Error("could not resolve a native bundle for " + normalized + " after retrying.")
438
+ throw (
439
+ lastError ??
440
+ new Error('could not resolve a native bundle for ' + normalized + ' after retrying.')
441
+ )
436
442
  }
437
443
 
438
444
  function getShellRoutePrefix(pathname: string): string {
@@ -1368,6 +1374,19 @@ export async function runOpenCommand(
1368
1374
  }
1369
1375
  const target = parsed.positional[0] || ''
1370
1376
 
1377
+ // `--port` is the WS BRIDGE port; the app/dev-server port is positional. the
1378
+ // two read identically at a glance, so `rnx open --new --port 4260` used to
1379
+ // launch the Connect launcher against the default bridge and report success,
1380
+ // and the probe that followed measured whatever sim happened to be selected.
1381
+ // there is no meaningful `open --port` without a target, so refuse it.
1382
+ if (!target && args.includes('--port')) {
1383
+ const portValue = args[args.indexOf('--port') + 1] ?? '<number>'
1384
+ console.error(
1385
+ ` rnx open: --port is the WS bridge port, not the app port. to open the app on ${portValue}, pass it positionally: \`rnx open ${portValue}\``,
1386
+ )
1387
+ rnxExit(1)
1388
+ }
1389
+
1371
1390
  if (isAppDeepLinkTarget(target)) {
1372
1391
  if (openInNewSim) {
1373
1392
  console.error(
@@ -433,7 +433,7 @@ function initDetoxProject(): void {
433
433
 
434
434
  /** @type {import('@jest/types').Config.InitialOptions} */
435
435
  module.exports = {
436
- preset: 'rnxsim/detox',
436
+ preset: 'peachsim/detox',
437
437
  rootDir: __dirname,
438
438
  testMatch: ['<rootDir>/e2e/**/*.test.ts', '<rootDir>/e2e/**/*.test.js'],
439
439
  }
@@ -3,7 +3,7 @@ import {
3
3
  getDevice,
4
4
  listSelectableDeviceModels,
5
5
  type DeviceModel,
6
- } from 'sootsim-engine/settings'
6
+ } from 'peach-engine/settings'
7
7
  import { rnxExit } from '../run-rnx'
8
8
  import {
9
9
  callInBridge,
@@ -32,7 +32,7 @@ import type {
32
32
  SootSimTimelineEvent,
33
33
  SootSimTimelineQuery,
34
34
  SootSimTimelineQueryResult,
35
- } from '@rnx/globals'
35
+ } from '@peach/globals'
36
36
 
37
37
  interface DiagnoseOptions {
38
38
  port?: number
@@ -148,7 +148,7 @@ examples:
148
148
  )
149
149
  if (!started) {
150
150
  console.error(
151
- ' film: engine recording store did not start (is @rnx/plugin-recording installed?)',
151
+ ' film: engine recording store did not start (is @peach/plugin-recording installed?)',
152
152
  )
153
153
  return 1
154
154
  }
@@ -58,6 +58,8 @@ const FLOW_BOOLEAN_FLAGS = [
58
58
  '--proof',
59
59
  '--no-shell',
60
60
  '--shell-only',
61
+ '--dry-run',
62
+ '--no-run',
61
63
  ]
62
64
 
63
65
  const FLOW_VALUE_FLAGS = [
@@ -210,12 +212,14 @@ rnx maestro — author and run Maestro YAML flows
210
212
 
211
213
  usage:
212
214
  rnx maestro test <flow.yaml> [options]
215
+ rnx maestro load <flow.yaml> [options]
213
216
  rnx maestro start
214
217
  rnx maestro keep
215
218
  rnx maestro end [--output <path>] [--validate] [--video]
216
219
  rnx maestro validate <path>
217
220
 
218
221
  options:
222
+ --dry-run, --no-run load flow into the runner idle without executing steps
219
223
  --record record a webm artifact while the flow runs
220
224
  --profile capture perf stats while the flow runs
221
225
  --out <dir> output directory (default: /tmp/rnx-recordings)
@@ -621,7 +625,10 @@ flow extension:
621
625
  // pr-preview run, so allow it for preview uploads. local flow recording
622
626
  // writes a file and matches `rnx record --mode video`.
623
627
  allowGitHubRecording: preview,
624
- requireRecordingEntitlement: preview,
628
+ // a test_run bills its recording from balance at finalize (preflighted
629
+ // at upload init), not from a plan entitlement: the plan gate would
630
+ // refuse every pay-as-you-go hosted test.
631
+ requireRecordingEntitlement: preview && previewBillingKind !== 'test_run',
625
632
  // re-arm the event recorder after every launchApp-with-reload. the
626
633
  // recorder lives in-page state and gets wiped by window.location.reload.
627
634
  onAfterLaunch: async (simId) => {
@@ -665,6 +672,7 @@ flow extension:
665
672
  let previewScreenshotPath: string | null = null
666
673
  let failureBundleDir: string | null = null
667
674
  let exitCode = 0
675
+ const dryRun = args.includes('--dry-run') || args.includes('--no-run')
668
676
  const startsWithLaunchApp = !!steps[0]?.launchApp
669
677
  try {
670
678
  if (!startsWithLaunchApp) {
@@ -694,7 +702,21 @@ flow extension:
694
702
  if (preview && !startsWithLaunchApp && !process.env.RNX_PREVIEW_RESET_DONE) {
695
703
  await driver.launchApp({ clearState: true })
696
704
  }
697
- if (record) {
705
+ if (dryRun) {
706
+ await driver.planFlow(steps, { dryRun: true })
707
+ console.log(
708
+ `[flow] flow loaded (${steps.length} steps) — waiting for play in test runner…`,
709
+ )
710
+ await driver.liveStatus?.waitForStart()
711
+ console.log('[flow] starting flow playback…')
712
+ if (record) {
713
+ if (startsWithLaunchApp) {
714
+ driver.prepareRecording()
715
+ } else {
716
+ await driver.startRecording()
717
+ }
718
+ }
719
+ } else if (record) {
698
720
  if (startsWithLaunchApp) {
699
721
  driver.prepareRecording()
700
722
  } else {
@@ -723,7 +745,7 @@ flow extension:
723
745
  )
724
746
  }
725
747
  }
726
- await driver.runFlow(steps)
748
+ await driver.runFlow(steps, { skipPlan: dryRun })
727
749
  if (profile) {
728
750
  profileResult = await driver.stopProfile()
729
751
  }
@@ -923,6 +945,41 @@ flow extension:
923
945
  }
924
946
  }
925
947
 
948
+ if (dryRun) {
949
+ console.log('[flow] run completed — waiting for reset or new action in test runner…')
950
+ while (true) {
951
+ const action = await driver.liveStatus?.waitForResetOrRerun()
952
+ if (action === 'reset') {
953
+ console.log('[flow] reset requested from devtools — resetting runner state')
954
+ driver.resetFlowState()
955
+ await driver.planFlow(steps, { dryRun: true })
956
+ console.log('[flow] flow reloaded — waiting for play in test runner…')
957
+ await driver.liveStatus?.waitForStart()
958
+ console.log('[flow] re-running flow playback…')
959
+ try {
960
+ await driver.runFlow(steps, { skipPlan: true })
961
+ await driver.liveStatus?.end('passed')
962
+ console.log('\n + completed replay successfully\n')
963
+ } catch (replayErr: any) {
964
+ console.error(`\n x failed replay: ${replayErr?.message || replayErr}\n`)
965
+ await driver.liveStatus?.end('failed')
966
+ }
967
+ } else if (action === 'start') {
968
+ console.log('[flow] play requested from devtools — re-running flow playback…')
969
+ try {
970
+ await driver.runFlow(steps, { skipPlan: true })
971
+ await driver.liveStatus?.end('passed')
972
+ console.log('\n + completed replay successfully\n')
973
+ } catch (replayErr: any) {
974
+ console.error(`\n x failed replay: ${replayErr?.message || replayErr}\n`)
975
+ await driver.liveStatus?.end('failed')
976
+ }
977
+ } else {
978
+ break
979
+ }
980
+ }
981
+ }
982
+
926
983
  lastFlowTraceSteps = driver.getFlowTraceSteps()
927
984
 
928
985
  bridge.close()
@@ -2,7 +2,7 @@ import { inspectTree, type InspectBridge } from './core'
2
2
  import { resolveTargetCoords, type ResolvedTargetCoords } from './resolve-target'
3
3
  import { waitForSootsimIdle } from './settling'
4
4
  import type { SimSemanticNode } from '../../../src/bridge-contract'
5
- import type { SootSimReplayTarget } from '@rnx/globals'
5
+ import type { SootSimReplayTarget } from '@peach/globals'
6
6
 
7
7
  export type TapFailure = 'not-found' | 'missed' | 'special'
8
8