peachsim 0.1.527 → 0.1.571
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/README.md +28 -28
- package/cli/app-fonts.ts +1 -1
- package/cli/app-splash.ts +1 -1
- package/cli/bin.ts +1 -1
- package/cli/bridge-diagnostics.ts +1 -1
- package/cli/bridge-flow-runner.ts +72 -12
- package/cli/cloud-client.ts +273 -6
- package/cli/cloud-dispatch.ts +23 -2
- package/cli/cloud-session.ts +60 -4
- package/cli/cloud-test-result-files.ts +97 -0
- package/cli/cloud-test-tunnel.ts +354 -0
- package/cli/commands/box/bundle-plane.ts +2 -2
- package/cli/commands/box/checkout-plane.ts +1 -1
- package/cli/commands/box/rnx-command.ts +2 -2
- package/cli/commands/box/search-command.ts +2 -2
- package/cli/commands/box.ts +8 -8
- package/cli/commands/compat.ts +1 -1
- package/cli/commands/control.ts +22 -3
- package/cli/commands/detox.ts +1 -1
- package/cli/commands/device.ts +1 -1
- package/cli/commands/diagnose.ts +1 -1
- package/cli/commands/film.ts +1 -1
- package/cli/commands/flow.ts +60 -3
- package/cli/commands/inspect/actions.ts +1 -1
- package/cli/commands/inspect/core.ts +6 -7
- package/cli/commands/inspect/find.ts +2 -1
- package/cli/commands/inspect/resolve-target.ts +5 -3
- package/cli/commands/inspect/shared.ts +1 -1
- package/cli/commands/inspect/wait-event.ts +1 -1
- package/cli/commands/inspect/wait-selector.ts +15 -4
- package/cli/commands/inspect.ts +19 -14
- package/cli/commands/maestro.ts +582 -69
- package/cli/commands/permissions.ts +1 -1
- package/cli/commands/platform.ts +205 -8
- package/cli/commands/preview.ts +2 -2
- package/cli/commands/record.ts +2 -2
- package/cli/commands/report-issue.ts +2 -2
- package/cli/commands/screenshot-layers.ts +1 -1
- package/cli/commands/screenshot.ts +1 -1
- package/cli/commands/setup.ts +2 -2
- package/cli/commands/test.ts +600 -117
- package/cli/commands/three-mode.ts +1 -1
- package/cli/commands/upload.ts +5 -5
- package/cli/commands/what-happened.ts +1 -1
- package/cli/desktop-companion.ts +3 -3
- package/cli/drivers/playwright-sim-host.ts +2 -2
- package/cli/flow-file.ts +7 -0
- package/cli/flow-live-status.ts +65 -8
- package/cli/help-core.ts +3 -3
- package/cli/hidden-runtime-alias.ts +1 -1
- package/cli/maestro-report.ts +180 -0
- package/cli/main.ts +1 -1
- package/cli/outbound-endpoints.ts +187 -136
- package/cli/parse-args.ts +1 -1
- package/cli/registry.ts +1 -1
- package/cli/resolve-assets.ts +1 -1
- package/cli/run-registry.ts +10 -1
- package/cli/send-to-box.ts +4 -4
- package/cli/ws-bridge.ts +3 -5
- package/detox/expectations.ts +1 -1
- package/detox/gestures.ts +2 -2
- package/detox/index.ts +71 -29
- package/detox/jest-preset.cjs +5 -5
- package/detox/jest-setup-after-env.cjs +1 -1
- package/dist-lib/agent-daemon-client.cjs +2 -2
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +2 -2
- package/dist-lib/attached-projects.cjs +2 -2
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -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 +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +94 -3
- package/dist-lib/capture-contract.mjs +93 -3
- 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 +116 -0
- package/dist-lib/cloud-test-args.mjs +86 -0
- package/dist-lib/cloud.cjs +2 -2
- package/dist-lib/cloud.mjs +2 -2
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +106 -25
- package/dist-lib/detox/jest-preset.cjs +5 -5
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +150 -65
- package/dist-lib/host/fetch-proxy-handler.cjs +28 -2
- package/dist-lib/host/fetch-proxy-overrides.cjs +33 -3
- package/dist-lib/host/fetch-proxy-overrides.mjs +30 -2
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +224 -41
- 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 +2 -2
- package/dist-lib/menu.mjs +2 -2
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +3 -3
- package/dist-lib/metro-production-bundle.mjs +3 -3
- package/dist-lib/metro.cjs +8 -8
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +2 -2
- 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 +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +31 -15
- package/dist-lib/sdk.cjs +3 -2
- package/dist-lib/sdk.mjs +3 -2
- package/dist-lib/skills.cjs +478 -266
- package/dist-lib/swift-compile.cjs +372 -0
- package/dist-lib/swift-compile.mjs +339 -0
- package/dist-lib/swift-package.cjs +78 -0
- package/dist-lib/swift-package.mjs +52 -0
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +583 -16
- package/dist-lib/vite.cjs +483 -150
- package/package.json +30 -9
- package/scripts/dev-server-scanner.ts +41 -14
- package/scripts/optional-demo-registry.ts +3 -3
- package/skills/contrast/SKILL.md +9 -9
- package/skills/rnx-debug/SKILL.md +14 -4
- package/skills/rnx-perf/SKILL.md +5 -5
- package/skills/rnx-setup/SKILL.md +11 -11
- package/skills/rnx-test/SKILL.md +14 -14
- package/skills/rnx-visual/SKILL.md +6 -6
- package/src/agent-sessions.ts +3 -3
- package/src/attached-projects.ts +1 -1
- package/src/bridge-contract-input.ts +1 -1
- package/src/bridge-contract.ts +23 -5
- package/src/capture-contract.ts +145 -3
- package/src/cloud-test-args.ts +100 -0
- package/src/config.ts +2 -2
- package/src/connect.ts +2 -2
- package/src/home-paths.ts +3 -3
- package/src/host/bridge-host.ts +2 -2
- package/src/host/fetch-proxy-handler.ts +4 -3
- package/src/host/fetch-proxy-overrides.ts +41 -0
- package/src/host/plane-host.ts +2 -2
- package/src/index.ts +3 -0
- package/src/metro-plugin.ts +4 -4
- package/src/metro-production-bundle.ts +2 -2
- package/src/native-seam-manifest.ts +6 -6
- package/src/public-brand.ts +1 -1
- package/src/rolldown-plugin-swift.ts +16 -12
- package/src/runtime-assets.ts +8 -6
- package/src/screenshots/frame-compose.ts +24 -56
- package/src/screenshots/org-deck.ts +1 -1
- package/src/screenshots/registry.ts +4 -4
- package/src/screenshots/schema.ts +1 -1
- package/src/sdk.ts +2 -2
- package/src/settings-bundle.ts +1 -1
- package/src/sim-client.ts +2 -4
- package/src/skills/builtin/compat-check.ts +1 -1
- package/src/swift-compile.ts +359 -0
- package/src/swift-package.ts +69 -0
- package/src/swift-submit.ts +3 -2
- package/src/vite-plugin-one.ts +1 -1
- package/src/vite-plugin-swift.ts +114 -113
- package/src/vite-plugin.ts +30 -17
- 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 '@
|
|
12
|
-
import type { ProjectFileMeta, ProjectFilePlane } from '@
|
|
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 '@
|
|
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 '@
|
|
12
|
-
import { PROJECT_MOUNT } from '@
|
|
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 '@
|
|
36
|
-
import { PROJECT_MOUNT } from '@
|
|
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.
|
package/cli/commands/box.ts
CHANGED
|
@@ -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 '@
|
|
17
|
-
import { createBoxHostProvider } from '@
|
|
18
|
-
import { PROJECT_MOUNT } from '@
|
|
19
|
-
import { terminalBoxStack } from '@
|
|
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 '@
|
|
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 '
|
|
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 '@
|
|
45
|
-
import type { BoxTemplateResolution } from '@
|
|
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
|
package/cli/commands/compat.ts
CHANGED
package/cli/commands/control.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
devices,
|
|
7
7
|
listSelectableDeviceModels,
|
|
8
8
|
type DeviceModel,
|
|
9
|
-
} from '
|
|
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 (
|
|
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
|
|
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(
|
package/cli/commands/detox.ts
CHANGED
|
@@ -433,7 +433,7 @@ function initDetoxProject(): void {
|
|
|
433
433
|
|
|
434
434
|
/** @type {import('@jest/types').Config.InitialOptions} */
|
|
435
435
|
module.exports = {
|
|
436
|
-
preset: '
|
|
436
|
+
preset: 'peachsim/detox',
|
|
437
437
|
rootDir: __dirname,
|
|
438
438
|
testMatch: ['<rootDir>/e2e/**/*.test.ts', '<rootDir>/e2e/**/*.test.js'],
|
|
439
439
|
}
|
package/cli/commands/device.ts
CHANGED
package/cli/commands/diagnose.ts
CHANGED
package/cli/commands/film.ts
CHANGED
|
@@ -148,7 +148,7 @@ examples:
|
|
|
148
148
|
)
|
|
149
149
|
if (!started) {
|
|
150
150
|
console.error(
|
|
151
|
-
' film: engine recording store did not start (is @
|
|
151
|
+
' film: engine recording store did not start (is @peach/plugin-recording installed?)',
|
|
152
152
|
)
|
|
153
153
|
return 1
|
|
154
154
|
}
|
package/cli/commands/flow.ts
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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 '@
|
|
5
|
+
import type { SootSimReplayTarget } from '@peach/globals'
|
|
6
6
|
|
|
7
7
|
export type TapFailure = 'not-found' | 'missed' | 'special'
|
|
8
8
|
|