peachsim 0.1.527
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +640 -0
- package/cli/app-config.ts +82 -0
- package/cli/app-fonts.ts +388 -0
- package/cli/app-project.ts +59 -0
- package/cli/app-splash.ts +171 -0
- package/cli/app-state-reset.ts +26 -0
- package/cli/auth.ts +184 -0
- package/cli/bin.ts +59 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2806 -0
- package/cli/browser-backend.ts +44 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/cli-update.ts +261 -0
- package/cli/cloud-client.ts +1530 -0
- package/cli/cloud-dispatch.ts +206 -0
- package/cli/cloud-session.ts +227 -0
- package/cli/command-contract.ts +221 -0
- package/cli/command-registry.ts +304 -0
- package/cli/commands/agent-wrapper.ts +1048 -0
- package/cli/commands/agent.ts +439 -0
- package/cli/commands/app-fonts.ts +99 -0
- package/cli/commands/assert.ts +542 -0
- package/cli/commands/auth.ts +71 -0
- package/cli/commands/box/bundle-plane.ts +71 -0
- package/cli/commands/box/checkout-plane.ts +256 -0
- package/cli/commands/box/plane-share.ts +208 -0
- package/cli/commands/box/rnx-command.ts +60 -0
- package/cli/commands/box/search-command.ts +157 -0
- package/cli/commands/box.ts +925 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +176 -0
- package/cli/commands/config.ts +34 -0
- package/cli/commands/control.ts +2154 -0
- package/cli/commands/cpu-profile.ts +941 -0
- package/cli/commands/daemon-mac-app.ts +165 -0
- package/cli/commands/daemon.ts +800 -0
- package/cli/commands/debug.ts +751 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +201 -0
- package/cli/commands/detox.ts +481 -0
- package/cli/commands/device.ts +134 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/do-chain.ts +408 -0
- package/cli/commands/electron.ts +94 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1240 -0
- package/cli/commands/inspect/actions.ts +516 -0
- package/cli/commands/inspect/core.ts +2086 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +119 -0
- package/cli/commands/inspect/env.ts +25 -0
- package/cli/commands/inspect/find.ts +169 -0
- package/cli/commands/inspect/get-layout.ts +53 -0
- package/cli/commands/inspect/keyboard.ts +54 -0
- package/cli/commands/inspect/list.ts +175 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +77 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +32 -0
- package/cli/commands/inspect/settling.ts +197 -0
- package/cli/commands/inspect/shared.ts +259 -0
- package/cli/commands/inspect/sleep.ts +15 -0
- package/cli/commands/inspect/tree.ts +23 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +211 -0
- package/cli/commands/inspect/wait-idle.ts +25 -0
- package/cli/commands/inspect/wait-ready.ts +87 -0
- package/cli/commands/inspect/wait-selector.ts +59 -0
- package/cli/commands/inspect/wait.ts +32 -0
- package/cli/commands/inspect.ts +4738 -0
- package/cli/commands/install-cli.ts +275 -0
- package/cli/commands/install-desktop.ts +385 -0
- package/cli/commands/login.ts +357 -0
- package/cli/commands/logout.ts +24 -0
- package/cli/commands/maestro-generate.ts +375 -0
- package/cli/commands/maestro.ts +536 -0
- package/cli/commands/mode.ts +58 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +67 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/platform.ts +410 -0
- package/cli/commands/preview.ts +103 -0
- package/cli/commands/profile.ts +106 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1435 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +89 -0
- package/cli/commands/runtime.ts +348 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +144 -0
- package/cli/commands/screenshot-mode.ts +38 -0
- package/cli/commands/screenshot.ts +492 -0
- package/cli/commands/screenshots-capture.ts +608 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +139 -0
- package/cli/commands/setup.ts +416 -0
- package/cli/commands/shell-boolean-mode.ts +82 -0
- package/cli/commands/skills.ts +468 -0
- package/cli/commands/slides.ts +357 -0
- package/cli/commands/state.ts +94 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +55 -0
- package/cli/commands/test.ts +535 -0
- package/cli/commands/three-mode.ts +771 -0
- package/cli/commands/timeline.ts +121 -0
- package/cli/commands/upgrade.ts +167 -0
- package/cli/commands/upload.ts +1203 -0
- package/cli/commands/version.ts +63 -0
- package/cli/commands/welcome.ts +17 -0
- package/cli/commands/what-happened.ts +328 -0
- package/cli/current-sim.ts +217 -0
- package/cli/desktop-companion.ts +315 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +184 -0
- package/cli/drivers/playwright-sim-host.ts +537 -0
- package/cli/drivers/playwright.ts +209 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-export.ts +55 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +126 -0
- package/cli/flow-session.ts +140 -0
- package/cli/help-core.ts +143 -0
- package/cli/help.ts +44 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +214 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/internal-child.ts +176 -0
- package/cli/maestro-js.ts +323 -0
- package/cli/main.ts +704 -0
- package/cli/open-url.ts +8 -0
- package/cli/outbound-endpoints.ts +830 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +189 -0
- package/cli/privacy.ts +53 -0
- package/cli/prompt.ts +56 -0
- package/cli/recording-access.ts +108 -0
- package/cli/registry.ts +2 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/rnx-in-process.ts +98 -0
- package/cli/run-registry.ts +226 -0
- package/cli/run-rnx-core.ts +148 -0
- package/cli/run-rnx.ts +137 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/secure-file.ts +31 -0
- package/cli/self-invocation.ts +58 -0
- package/cli/send-to-box.ts +109 -0
- package/cli/setup-repository.ts +191 -0
- package/cli/shell-init.ts +98 -0
- package/cli/standalone.ts +4 -0
- package/cli/telemetry.ts +84 -0
- package/cli/ui.ts +68 -0
- package/cli/ws-bridge.ts +1020 -0
- package/detox/attempt-events.cjs +23 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +48 -0
- package/detox/expectations.ts +534 -0
- package/detox/gestures.ts +501 -0
- package/detox/index.ts +2694 -0
- package/detox/jest-preset.cjs +29 -0
- package/detox/jest-setup-after-env.cjs +22 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation-lifecycle.ts +290 -0
- package/detox/navigation.ts +43 -0
- package/detox/proof-frame.cjs +114 -0
- package/detox/record-block-execution.cjs +78 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-lib/agent-daemon-client.cjs +502 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +759 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +208 -0
- package/dist-lib/backend-origin.cjs +88 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/bridge-contract-input.cjs +451 -0
- package/dist-lib/bridge-contract-input.mjs +414 -0
- package/dist-lib/bridge-contract.cjs +130 -0
- package/dist-lib/bridge-contract.mjs +94 -0
- package/dist-lib/capture-contract.cjs +220 -0
- package/dist-lib/capture-contract.mjs +190 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/cloud-contract.cjs +194 -0
- package/dist-lib/cloud-contract.mjs +145 -0
- package/dist-lib/cloud.cjs +670 -0
- package/dist-lib/cloud.mjs +639 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +3789 -0
- package/dist-lib/detox/jest-preset.cjs +29 -0
- package/dist-lib/dev-bundle-resolution.cjs +290 -0
- package/dist-lib/home-paths.cjs +638 -0
- package/dist-lib/host/bridge-host.cjs +10903 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/replacement-module-handler.cjs +469 -0
- package/dist-lib/host/websocket-proxy.cjs +298 -0
- package/dist-lib/index.cjs +4268 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/jump-to-source-native.cjs +112 -0
- package/dist-lib/menu.cjs +646 -0
- package/dist-lib/menu.mjs +604 -0
- package/dist-lib/metro-fingerprint-registry.cjs +186 -0
- package/dist-lib/metro-fingerprint-registry.mjs +156 -0
- package/dist-lib/metro-production-bundle.cjs +329 -0
- package/dist-lib/metro-production-bundle.mjs +295 -0
- package/dist-lib/metro.cjs +904 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/react-native-host-modules.cjs +51 -0
- package/dist-lib/react-native-host-modules.mjs +25 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
- package/dist-lib/sdk.cjs +2433 -0
- package/dist-lib/sdk.mjs +2336 -0
- package/dist-lib/skills.cjs +39132 -0
- package/dist-lib/swift-submit.cjs +92 -0
- package/dist-lib/swift-submit.mjs +65 -0
- package/dist-lib/swift.cjs +1646 -0
- package/dist-lib/vite.cjs +1591 -0
- package/package.json +290 -0
- package/scripts/dev-server-scanner.ts +1291 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/skills/contrast/SKILL.md +165 -0
- package/skills/rnx-debug/SKILL.md +433 -0
- package/skills/rnx-perf/SKILL.md +399 -0
- package/skills/rnx-setup/SKILL.md +172 -0
- package/skills/rnx-test/SKILL.md +321 -0
- package/skills/rnx-visual/SKILL.md +126 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +580 -0
- package/src/app-url.ts +39 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +235 -0
- package/src/backend-origin.ts +70 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +175 -0
- package/src/bridge-contract-input.ts +502 -0
- package/src/bridge-contract.ts +558 -0
- package/src/browser-cache.ts +16 -0
- package/src/capture-contract.ts +587 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +40 -0
- package/src/cloud-contract.ts +274 -0
- package/src/cloud.ts +762 -0
- package/src/codex-client.ts +234 -0
- package/src/config.ts +132 -0
- package/src/connect.ts +101 -0
- package/src/dev-bundle-resolution.ts +346 -0
- package/src/dev-server-open.ts +198 -0
- package/src/disk-cleanup.ts +540 -0
- package/src/flow-timeout-scale.ts +15 -0
- package/src/home-paths.ts +806 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2788 -0
- package/src/host/fetch-proxy-handler.ts +449 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/plane-host.ts +203 -0
- package/src/host/replacement-module-handler.ts +158 -0
- package/src/host/websocket-proxy.ts +315 -0
- package/src/index.ts +71 -0
- package/src/jump-to-source-babel.ts +135 -0
- package/src/jump-to-source-native.ts +102 -0
- package/src/menu.ts +777 -0
- package/src/metro-fingerprint-registry.ts +302 -0
- package/src/metro-plugin.ts +341 -0
- package/src/metro-production-bundle.ts +471 -0
- package/src/native-dev-bundle-url.ts +118 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/playwright-host-log.ts +16 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +123 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/react-native-host-modules.ts +26 -0
- package/src/render-mode.ts +27 -0
- package/src/rolldown-plugin-swift.ts +240 -0
- package/src/runtime-assets.ts +233 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +514 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +15 -0
- package/src/settings-bundle.ts +418 -0
- package/src/shell-bridge-identity.ts +15 -0
- package/src/sim-client.ts +697 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/swift-submit.ts +185 -0
- package/src/swift.ts +7 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +264 -0
- package/src/vite-plugin-swift.ts +620 -0
- package/src/vite-plugin.ts +1946 -0
- package/src/worklets-babel.ts +250 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// playwright driver — launches Playwright's Google Chrome for Testing via the
|
|
2
|
+
// `playwright` package. availability depends on whether the package is
|
|
3
|
+
// resolvable from the current working directory OR from the CLI's own
|
|
4
|
+
// install tree (repo-local install is typical; the latter covers a global
|
|
5
|
+
// CLI invoked from a directory with no node_modules).
|
|
6
|
+
//
|
|
7
|
+
// the browser runs in a *detached* child process — this CLI re-invoked as
|
|
8
|
+
// its own `__internal playwright-host` (see cli/internal-child.ts) — rather
|
|
9
|
+
// than in this CLI process. that is load-bearing: `rnx open` returns the
|
|
10
|
+
// moment the sim connects, and playwright tears its browser down when the
|
|
11
|
+
// launching process exits. an in-process launch would mean the sim vanished
|
|
12
|
+
// the instant `open` finished. the detached host keeps the test browser alive
|
|
13
|
+
// until the sim's page closes (e.g. via `rnx close`).
|
|
14
|
+
|
|
15
|
+
import { spawn } from 'child_process'
|
|
16
|
+
import { closeSync, mkdtempSync, openSync, readFileSync } from 'fs'
|
|
17
|
+
import { tmpdir } from 'os'
|
|
18
|
+
import { join } from 'path'
|
|
19
|
+
import { playwrightHostLogName } from '../../src/playwright-host-log'
|
|
20
|
+
import { ensureProfile, playwrightProfileUserDataDir } from '../../src/profiles'
|
|
21
|
+
import { RNX_INTERNAL_CHILDREN, RNX_INTERNAL_COMMAND } from '../internal-child'
|
|
22
|
+
import { getProcessComm } from '../parent-pid'
|
|
23
|
+
import { rnxSelfInvocation } from '../self-invocation'
|
|
24
|
+
import { ensurePlaywrightBrowser, resolvePlaywright } from './playwright-provisioning'
|
|
25
|
+
import type {
|
|
26
|
+
Driver,
|
|
27
|
+
DriverAvailability,
|
|
28
|
+
DriverLaunchOptions,
|
|
29
|
+
DriverLaunchResult,
|
|
30
|
+
} from './types'
|
|
31
|
+
|
|
32
|
+
function probe(): DriverAvailability {
|
|
33
|
+
const resolved = resolvePlaywright()
|
|
34
|
+
if (!resolved) {
|
|
35
|
+
return {
|
|
36
|
+
available: false,
|
|
37
|
+
reason: 'playwright not installed in the current workspace',
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
available: true,
|
|
42
|
+
reason: null,
|
|
43
|
+
detail: `resolved via ${resolved.spec}@${resolved.version}`,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// when a headed launch fails on a Linux box with no display, chrome's own
|
|
48
|
+
// error ("Missing X server", "cannot open display") tells the user *what*
|
|
49
|
+
// is wrong but not *what to do*. surface the actionable flag.
|
|
50
|
+
function headlessHintForLinuxFailure(
|
|
51
|
+
opts: DriverLaunchOptions,
|
|
52
|
+
detail: string,
|
|
53
|
+
): string | null {
|
|
54
|
+
if (process.platform !== 'linux') return null
|
|
55
|
+
if (opts.headless) return null
|
|
56
|
+
if (process.env.DISPLAY || process.env.WAYLAND_DISPLAY) return null
|
|
57
|
+
if (!/missing x server|cannot open display|x server|x11/i.test(detail)) {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
return ' hint: linux without DISPLAY/WAYLAND_DISPLAY — rerun without --headed (or add --headless) to run playwright headless.'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// resolve the Chrome user-data dir for a profiled launch. a non-empty
|
|
64
|
+
// return means the host should use a persistent context; '' means a plain
|
|
65
|
+
// (in-memory) browser launch.
|
|
66
|
+
function resolveUserDataDir(opts: DriverLaunchOptions): string {
|
|
67
|
+
if (opts.profileId) {
|
|
68
|
+
return playwrightProfileUserDataDir(ensureProfile(opts.profileId).id)
|
|
69
|
+
}
|
|
70
|
+
if (opts.ephemeralProfile) {
|
|
71
|
+
return mkdtempSync(join(tmpdir(), 'rnx-playwright-profile-'))
|
|
72
|
+
}
|
|
73
|
+
return ''
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
|
|
77
|
+
const resolved = resolvePlaywright()
|
|
78
|
+
if (!resolved) {
|
|
79
|
+
return {
|
|
80
|
+
launched: false,
|
|
81
|
+
message: 'playwright not installed — run `bun add -D playwright` first',
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!opts.url) {
|
|
85
|
+
return { launched: false, message: 'playwright driver requires a target url' }
|
|
86
|
+
}
|
|
87
|
+
const ownerPid = opts.ownerPid
|
|
88
|
+
if (ownerPid === undefined || !Number.isInteger(ownerPid) || ownerPid <= 1) {
|
|
89
|
+
return {
|
|
90
|
+
launched: false,
|
|
91
|
+
message: 'playwright driver requires a valid owning session process',
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// keep browser discovery, installation, and the detached host on one exact
|
|
96
|
+
// cache contract even if another caller mutates process.env during launch.
|
|
97
|
+
const playwrightEnv = { ...process.env }
|
|
98
|
+
const provisioning = ensurePlaywrightBrowser(resolved, playwrightEnv)
|
|
99
|
+
if (!provisioning.ok) {
|
|
100
|
+
return {
|
|
101
|
+
launched: false,
|
|
102
|
+
message: `playwright browser provisioning failed: ${provisioning.message}`,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const errLog = join(tmpdir(), playwrightHostLogName(Date.now(), process.pid))
|
|
107
|
+
const connectAckFile = `${errLog}.connected`
|
|
108
|
+
const errFd = openSync(errLog, 'a')
|
|
109
|
+
try {
|
|
110
|
+
// spawn the browser host detached so Chrome outlives `rnx
|
|
111
|
+
// open`. stdout is dropped; stderr is captured to errLog so an early
|
|
112
|
+
// crash (bad profile dir, missing browser binary) produces a real
|
|
113
|
+
// message instead of a silent "timed out waiting for sim".
|
|
114
|
+
// resolve the owning session's pid *before* we detach — after the spawn,
|
|
115
|
+
// the host's own ancestry is just launchd. the host polls this pid and
|
|
116
|
+
// tears Chrome down when the session that launched it exits.
|
|
117
|
+
const self = rnxSelfInvocation()
|
|
118
|
+
const child = spawn(
|
|
119
|
+
self.executable,
|
|
120
|
+
[...self.prefixArgs, RNX_INTERNAL_COMMAND, RNX_INTERNAL_CHILDREN.playwrightHost],
|
|
121
|
+
{
|
|
122
|
+
detached: true,
|
|
123
|
+
stdio: ['ignore', 'ignore', errFd],
|
|
124
|
+
env: {
|
|
125
|
+
...playwrightEnv,
|
|
126
|
+
SOOTSIM_PW_MODULE: resolved.modulePath,
|
|
127
|
+
SOOTSIM_PW_URL: opts.url,
|
|
128
|
+
SOOTSIM_PW_BRIDGE_PORT: opts.bridgePort ? String(opts.bridgePort) : '',
|
|
129
|
+
SOOTSIM_PW_HEADLESS: (opts.headless ?? true) ? '1' : '0',
|
|
130
|
+
SOOTSIM_PW_USERDATADIR: resolveUserDataDir(opts),
|
|
131
|
+
SOOTSIM_PW_CDP_PORT: opts.cdpPort ? String(opts.cdpPort) : '',
|
|
132
|
+
// when the caller passed no viewport, leave the inherited
|
|
133
|
+
// SOOTSIM_PW_VIEWPORT (from playwrightEnv) alone instead of
|
|
134
|
+
// clobbering it with '' — the driver reads it as its own default.
|
|
135
|
+
...(opts.viewport
|
|
136
|
+
? { SOOTSIM_PW_VIEWPORT: `${opts.viewport.width}x${opts.viewport.height}` }
|
|
137
|
+
: {}),
|
|
138
|
+
SOOTSIM_PW_CONNECTED_ACK_FILE: connectAckFile,
|
|
139
|
+
SOOTSIM_PW_CONNECT_TIMEOUT_MS: String(opts.connectTimeoutMs ?? 120_000),
|
|
140
|
+
SOOTSIM_PW_OWNER_PID: String(ownerPid),
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
child.unref()
|
|
145
|
+
|
|
146
|
+
// give the host a short window to fail fast (resolve errors, browser
|
|
147
|
+
// binary missing). if it is still alive after that, the sim is on its
|
|
148
|
+
// way and the caller's own `waitForSimMatch` takes over.
|
|
149
|
+
const earlyExit = await new Promise<number | null>((resolve) => {
|
|
150
|
+
const timer = setTimeout(() => resolve(null), 4000)
|
|
151
|
+
child.once('exit', (code) => {
|
|
152
|
+
clearTimeout(timer)
|
|
153
|
+
resolve(code ?? 0)
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
if (earlyExit !== null) {
|
|
157
|
+
// ANY exit inside the 4s startup window is a failure: a healthy host
|
|
158
|
+
// stays alive until the sim disconnects (much longer than 4s). a code-0
|
|
159
|
+
// early exit can happen when the inline script's top-level catch fires
|
|
160
|
+
// before the launch even reaches a thrown error path, or when chromium
|
|
161
|
+
// is killed externally — both still mean "no sim is coming". emit the
|
|
162
|
+
// host's FULL stderr — its first line is the error message, and the
|
|
163
|
+
// prior `slice(-3)` kept only trailing stack frames, dropping the
|
|
164
|
+
// message and making CI failures undiagnosable. leave errLog on disk
|
|
165
|
+
// (path included) so it can be inspected after the fact.
|
|
166
|
+
const detail = readFileSync(errLog, 'utf8').trim()
|
|
167
|
+
const hint = headlessHintForLinuxFailure(opts, detail)
|
|
168
|
+
return {
|
|
169
|
+
launched: false,
|
|
170
|
+
message: `playwright host exited early (code ${earlyExit}) — host log ${errLog}${
|
|
171
|
+
detail ? `:\n${detail}` : ''
|
|
172
|
+
}${hint ? `\n${hint}` : ''}`,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// name the lifetime tie: the host watchdog closes chrome ~5s after the
|
|
176
|
+
// owning session pid exits, which surprises one-shot/detached shells
|
|
177
|
+
// (their topmost non-init ancestor is the shell itself, so the sim dies
|
|
178
|
+
// the moment the command finishes).
|
|
179
|
+
const ownerComm = getProcessComm(ownerPid)
|
|
180
|
+
const ownerNote = ` — browser closes when pid ${ownerPid}${ownerComm ? ` (${ownerComm})` : ''} exits`
|
|
181
|
+
return {
|
|
182
|
+
launched: true,
|
|
183
|
+
message: `playwright chrome launched → ${opts.url}${ownerNote}`,
|
|
184
|
+
pid: child.pid,
|
|
185
|
+
target: resolved.spec,
|
|
186
|
+
attachUrl: opts.url,
|
|
187
|
+
connectAckFile,
|
|
188
|
+
diagnosticLogPath: errLog,
|
|
189
|
+
}
|
|
190
|
+
} catch (err) {
|
|
191
|
+
return {
|
|
192
|
+
launched: false,
|
|
193
|
+
message: `playwright launch failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
194
|
+
}
|
|
195
|
+
} finally {
|
|
196
|
+
// parent's copy of the fd; the detached host keeps its own inherited
|
|
197
|
+
// dup, so closing here does not cut off the child's stderr.
|
|
198
|
+
closeSync(errFd)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export const playwrightDriver: Driver = {
|
|
203
|
+
id: 'playwright',
|
|
204
|
+
name: 'playwright',
|
|
205
|
+
description: 'programmatic chromium via the playwright package — headless default',
|
|
206
|
+
kind: 'automation',
|
|
207
|
+
availability: probe,
|
|
208
|
+
launch,
|
|
209
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// driver registry — hardcoded list of known drivers plus lookup helpers.
|
|
2
|
+
// kept intentionally flat: no DI, no plugin discovery, no lazy imports. both
|
|
3
|
+
// built-in drivers load unconditionally because their module bodies are cheap
|
|
4
|
+
// (no I/O, no side effects).
|
|
5
|
+
|
|
6
|
+
import { electronDriver } from './electron'
|
|
7
|
+
import { playwrightDriver } from './playwright'
|
|
8
|
+
import type { Driver, DriverId } from './types'
|
|
9
|
+
|
|
10
|
+
export const ALL_DRIVERS: readonly Driver[] = [electronDriver, playwrightDriver]
|
|
11
|
+
|
|
12
|
+
export function getAllDrivers(): readonly Driver[] {
|
|
13
|
+
return ALL_DRIVERS
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getDriver(id: DriverId): Driver | null {
|
|
17
|
+
return ALL_DRIVERS.find((d) => d.id === id) ?? null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// returns the first driver in preference order that reports availability.
|
|
21
|
+
// used by launch sites that want "pick whatever works" behavior when the
|
|
22
|
+
// user didn't pass an explicit --driver flag. if `forcedId` is given, that
|
|
23
|
+
// driver is returned even if unavailable so the caller can surface a
|
|
24
|
+
// specific error message rather than silently falling back.
|
|
25
|
+
export function resolveDriver(
|
|
26
|
+
forcedId: string | null | undefined,
|
|
27
|
+
preference: readonly DriverId[],
|
|
28
|
+
): Driver | null {
|
|
29
|
+
if (forcedId) {
|
|
30
|
+
const match = ALL_DRIVERS.find((d) => d.id === forcedId)
|
|
31
|
+
return match ?? null
|
|
32
|
+
}
|
|
33
|
+
for (const id of preference) {
|
|
34
|
+
const driver = ALL_DRIVERS.find((d) => d.id === id)
|
|
35
|
+
if (driver && driver.availability().available) return driver
|
|
36
|
+
}
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// utility for the `rnx list --drivers` view. returns a rich row per
|
|
41
|
+
// driver with availability + kind + description, suitable for a table.
|
|
42
|
+
export interface DriverListRow {
|
|
43
|
+
id: DriverId
|
|
44
|
+
name: string
|
|
45
|
+
kind: Driver['kind']
|
|
46
|
+
description: string
|
|
47
|
+
available: boolean
|
|
48
|
+
reason: string | null
|
|
49
|
+
detail: string | null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildDriverListRows(): DriverListRow[] {
|
|
53
|
+
return ALL_DRIVERS.map((d) => {
|
|
54
|
+
const probe = d.availability()
|
|
55
|
+
return {
|
|
56
|
+
id: d.id,
|
|
57
|
+
name: d.name,
|
|
58
|
+
kind: d.kind,
|
|
59
|
+
description: d.description,
|
|
60
|
+
available: probe.available,
|
|
61
|
+
reason: probe.reason,
|
|
62
|
+
detail: probe.detail ?? null,
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// driver / runner abstraction for rnx. browser sims run in an isolated
|
|
2
|
+
// playwright-owned profile. electron remains the native desktop surface.
|
|
3
|
+
|
|
4
|
+
export type DriverId = 'electron' | 'playwright'
|
|
5
|
+
|
|
6
|
+
// broad category for grouping / filtering. "native" is the electron host;
|
|
7
|
+
// "automation" is the isolated playwright browser.
|
|
8
|
+
export type DriverKind = 'native' | 'automation'
|
|
9
|
+
|
|
10
|
+
export interface DriverAvailability {
|
|
11
|
+
// true when the driver can attempt a launch on this machine. a driver is
|
|
12
|
+
// allowed to be available but still fail at runtime (network issues,
|
|
13
|
+
// permission denied, ...). false when there's a hard precondition missing
|
|
14
|
+
// (binary not installed, package not resolvable).
|
|
15
|
+
available: boolean
|
|
16
|
+
// short free-text explaining why a driver is unavailable. null when
|
|
17
|
+
// `available` is true, or when no reason is known.
|
|
18
|
+
reason: string | null
|
|
19
|
+
// optional human-readable detail shown in verbose listings — e.g. the
|
|
20
|
+
// resolved binary path, version string, or install location.
|
|
21
|
+
detail?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DriverLaunchOptions {
|
|
25
|
+
// shell URL to load in the launched process. optional because the electron
|
|
26
|
+
// driver can launch the companion app with
|
|
27
|
+
// no URL (it picks its last window). other drivers return launched:false
|
|
28
|
+
// with a helpful message when url is missing.
|
|
29
|
+
url?: string
|
|
30
|
+
// run in headless mode where the driver supports it. ignored by electron.
|
|
31
|
+
headless?: boolean
|
|
32
|
+
// initial device model seed forwarded to the rnx shell. the electron
|
|
33
|
+
// driver forwards it via argv (--device=<model>); browser drivers receive
|
|
34
|
+
// it already stamped on the URL (?device=<model>) by runOpenCommand.
|
|
35
|
+
device?: string
|
|
36
|
+
// page viewport for browser-based drivers. runOpenCommand sizes this from
|
|
37
|
+
// the device profile (device pt size + frame chrome) so the sim isn't
|
|
38
|
+
// letterboxed inside playwright's 1280x720 default.
|
|
39
|
+
viewport?: { width: number; height: number }
|
|
40
|
+
// let the caller override the port used in availability / attach
|
|
41
|
+
// heuristics. most drivers don't need this.
|
|
42
|
+
port?: number
|
|
43
|
+
// when true, the launch is detached from the parent process and the
|
|
44
|
+
// CLI returns immediately. default true.
|
|
45
|
+
detached?: boolean
|
|
46
|
+
// named persistent storage profile. honored by drivers that own their
|
|
47
|
+
// browser context (electron, playwright). plain browser tabs reject it.
|
|
48
|
+
profileId?: string
|
|
49
|
+
// unnamed temporary storage. honored by electron/playwright.
|
|
50
|
+
ephemeralProfile?: boolean
|
|
51
|
+
// stable pid for the session that owns a CLI-opened host. electron closes
|
|
52
|
+
// only that session's windows when the owner exits; playwright requires it
|
|
53
|
+
// so a detached browser can never outlive its session. omitted only for the
|
|
54
|
+
// persistent `rnx desktop` app lifecycle.
|
|
55
|
+
ownerPid?: number
|
|
56
|
+
// expose Chrome's remote-debugging endpoint on this port so a
|
|
57
|
+
// bridge-driven sim can also be cpu-profiled. only the playwright driver
|
|
58
|
+
// honors it (launches Chromium with --remote-debugging-port).
|
|
59
|
+
cdpPort?: number
|
|
60
|
+
// maximum time the detached host should wait for the launched sim to
|
|
61
|
+
// register with the bridge before it tears itself down.
|
|
62
|
+
connectTimeoutMs?: number
|
|
63
|
+
// the rnx WS bridge port the launched sim page should register with,
|
|
64
|
+
// injected as window.__sootsimBridgePort. load-bearing when the dev bridge
|
|
65
|
+
// drifted off the shell-port-implied default (e.g. :7668 taken at the shell's
|
|
66
|
+
// boot -> bridge on :7669 while the shell kept :5173): the page's own
|
|
67
|
+
// shell-port heuristic would compute the stale :7668 and never connect.
|
|
68
|
+
bridgePort?: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface DriverLaunchResult {
|
|
72
|
+
// true when the process was successfully spawned.
|
|
73
|
+
launched: boolean
|
|
74
|
+
// free-text describing what happened, used for CLI output and skill docs.
|
|
75
|
+
message: string
|
|
76
|
+
// optional handle details — currently free-shaped so each driver can
|
|
77
|
+
// expose what makes sense for its environment. pid for spawned processes,
|
|
78
|
+
// `target` for an install path, `attachUrl` for automation drivers.
|
|
79
|
+
pid?: number
|
|
80
|
+
target?: string
|
|
81
|
+
attachUrl?: string
|
|
82
|
+
connectAckFile?: string
|
|
83
|
+
diagnosticLogPath?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface Driver {
|
|
87
|
+
id: DriverId
|
|
88
|
+
// short human name shown in `rnx list --drivers`.
|
|
89
|
+
name: string
|
|
90
|
+
// one-line description. keep under 80 chars so the list command reads
|
|
91
|
+
// nicely at default terminal width.
|
|
92
|
+
description: string
|
|
93
|
+
kind: DriverKind
|
|
94
|
+
// probe the host environment for this driver. cheap and synchronous —
|
|
95
|
+
// anything expensive belongs in `launch`.
|
|
96
|
+
availability(): DriverAvailability
|
|
97
|
+
// start the driver pointed at `opts.url`. may be async because some
|
|
98
|
+
// drivers (electron, playwright) do real I/O. must never throw on a
|
|
99
|
+
// benign "not installed" condition — return { launched: false } with a
|
|
100
|
+
// helpful message instead.
|
|
101
|
+
launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult>
|
|
102
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// turning a recorded flow draft into a maestro-compatible YAML file.
|
|
2
|
+
//
|
|
3
|
+
// this is separate from flow-session.ts, the draft store, because the two have
|
|
4
|
+
// different reach. every `do` verb writes a candidate into the store, so the
|
|
5
|
+
// store has to load wherever the CLI's runtime commands load, including inside
|
|
6
|
+
// a box shell running in workerd, where the `yaml` package does not load at
|
|
7
|
+
// all. YAML formatting and writing the file are the export step, and only
|
|
8
|
+
// `rnx flow` and `rnx maestro` ever reach it.
|
|
9
|
+
|
|
10
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
11
|
+
import { dirname, resolve } from 'node:path'
|
|
12
|
+
import yaml from 'yaml'
|
|
13
|
+
import { validateFlowSteps } from './flow-file'
|
|
14
|
+
import { finalizeFlowSession, readFlowSession } from './flow-session'
|
|
15
|
+
|
|
16
|
+
export function exportFlowSession(outputPath?: string) {
|
|
17
|
+
const state = readFlowSession()
|
|
18
|
+
if (!state) return { active: false as const }
|
|
19
|
+
|
|
20
|
+
const yamlText = `${yaml.stringify(state.steps).trimEnd()}\n`
|
|
21
|
+
const issues = validateFlowSteps(state.steps)
|
|
22
|
+
if (issues.length > 0) {
|
|
23
|
+
return {
|
|
24
|
+
active: true as const,
|
|
25
|
+
valid: false as const,
|
|
26
|
+
issues,
|
|
27
|
+
yaml: yamlText,
|
|
28
|
+
stepCount: state.steps.length,
|
|
29
|
+
outputPath: null,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let resolvedOutputPath: string | null = null
|
|
34
|
+
if (outputPath) {
|
|
35
|
+
resolvedOutputPath = resolve(outputPath)
|
|
36
|
+
mkdirSync(dirname(resolvedOutputPath), { recursive: true })
|
|
37
|
+
writeFileSync(resolvedOutputPath, yamlText)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
active: true as const,
|
|
42
|
+
valid: true as const,
|
|
43
|
+
issues: [],
|
|
44
|
+
yaml: yamlText,
|
|
45
|
+
stepCount: state.steps.length,
|
|
46
|
+
outputPath: resolvedOutputPath,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function endFlowSession(outputPath?: string) {
|
|
51
|
+
const result = exportFlowSession(outputPath)
|
|
52
|
+
if (!result.active) return result
|
|
53
|
+
finalizeFlowSession()
|
|
54
|
+
return result
|
|
55
|
+
}
|
package/cli/flow-file.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import yaml from 'yaml'
|
|
3
|
+
|
|
4
|
+
export interface FlowFrontmatter {
|
|
5
|
+
// raw `rnx open` target: port, dev-server URL, bundle URL, or shell URL
|
|
6
|
+
app?: string | number
|
|
7
|
+
device?: string
|
|
8
|
+
// pin the sim color scheme for this flow run (screenshot baselines are
|
|
9
|
+
// theme-sensitive; a dark capture can never match a light baseline)
|
|
10
|
+
theme?: 'light' | 'dark'
|
|
11
|
+
electron?: boolean
|
|
12
|
+
// maestro-compatible: appId is accepted but not used by rnx (ios bundle id)
|
|
13
|
+
appId?: string
|
|
14
|
+
// maestro-compatible: flow-level env vars, defined into the flow's JS
|
|
15
|
+
// context before the first step runs
|
|
16
|
+
env?: Record<string, string>
|
|
17
|
+
// maestro-compatible: workspace tag filtering (config.yml includeTags/excludeTags)
|
|
18
|
+
tags?: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// parse flow file. accepts three shapes:
|
|
22
|
+
// 1. plain yaml array of steps (no frontmatter)
|
|
23
|
+
// 2. rnx style: ---\n<fm>\n---\n<steps>
|
|
24
|
+
// 3. maestro style: <fm>\n---\n<steps> (multi-doc yaml, fm first)
|
|
25
|
+
// maestro compat matters because real users have real .maestro/*.yaml files
|
|
26
|
+
// and shouldn't have to rewrite them to drive rnx.
|
|
27
|
+
//
|
|
28
|
+
// `${...}` templates are NOT resolved here — interpolation happens per-step
|
|
29
|
+
// at execution time in the flow runner's JS context (matching maestro), so
|
|
30
|
+
// values produced mid-flow (runScript output.*, copyTextFrom) resolve in
|
|
31
|
+
// later steps and a missing variable fails loudly at the exact step.
|
|
32
|
+
export function parseFlowFile(content: string): {
|
|
33
|
+
frontmatter: FlowFrontmatter
|
|
34
|
+
steps: any[]
|
|
35
|
+
} {
|
|
36
|
+
let frontmatter: FlowFrontmatter = {}
|
|
37
|
+
let body = content
|
|
38
|
+
const normalizeSteps = (steps: unknown): any[] => {
|
|
39
|
+
return Array.isArray(steps) ? steps : []
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// shape 2: leading ---\n<fm>\n---\n<body>
|
|
43
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?/)
|
|
44
|
+
if (fmMatch) {
|
|
45
|
+
try {
|
|
46
|
+
frontmatter = (yaml.parse(fmMatch[1]) as FlowFrontmatter) ?? {}
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.warn(` warn: could not parse frontmatter: ${(e as Error).message}`)
|
|
49
|
+
}
|
|
50
|
+
body = content.slice(fmMatch[0].length)
|
|
51
|
+
const steps = yaml.parse(body)
|
|
52
|
+
return { frontmatter, steps: normalizeSteps(steps) }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// shape 3: maestro multi-doc (fm-first, then `---`, then steps)
|
|
56
|
+
// detect cheaply: a `---` separator on its own line not at position 0
|
|
57
|
+
if (/\n---\s*\n/.test(content)) {
|
|
58
|
+
try {
|
|
59
|
+
const docs = yaml.parseAllDocuments(content)
|
|
60
|
+
if (docs.length >= 2) {
|
|
61
|
+
const fm = docs[0].toJS() as FlowFrontmatter | null
|
|
62
|
+
const st = docs[docs.length - 1].toJS()
|
|
63
|
+
return {
|
|
64
|
+
frontmatter: fm && typeof fm === 'object' ? fm : {},
|
|
65
|
+
steps: normalizeSteps(st),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.warn(` warn: could not parse multi-doc flow: ${(e as Error).message}`)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// shape 1: plain steps array
|
|
74
|
+
const steps = yaml.parse(body)
|
|
75
|
+
return { frontmatter, steps: normalizeSteps(steps) }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const INTERACTION_KEYS = new Set([
|
|
79
|
+
'tapOn',
|
|
80
|
+
'inputText',
|
|
81
|
+
'pressKey',
|
|
82
|
+
'dispatchKey',
|
|
83
|
+
'hideKeyboard',
|
|
84
|
+
'swipe',
|
|
85
|
+
'pinch',
|
|
86
|
+
'scroll',
|
|
87
|
+
'scrollUntilVisible',
|
|
88
|
+
])
|
|
89
|
+
|
|
90
|
+
export function validateFlowSteps(steps: any[]): string[] {
|
|
91
|
+
const issues: string[] = []
|
|
92
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
93
|
+
issues.push('flow body must be a non-empty YAML array of steps')
|
|
94
|
+
return issues
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let interactions = 0
|
|
98
|
+
for (const step of steps) {
|
|
99
|
+
if (step && typeof step === 'object') {
|
|
100
|
+
const keys = Object.keys(step)
|
|
101
|
+
if (keys.some((key) => INTERACTION_KEYS.has(key))) interactions += 1
|
|
102
|
+
} else if (typeof step === 'string' && INTERACTION_KEYS.has(step)) {
|
|
103
|
+
interactions += 1
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (interactions === 0) {
|
|
108
|
+
issues.push(
|
|
109
|
+
'no interaction steps found (expected at least one tapOn / inputText / pressKey / swipe / scroll)',
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return issues
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// structural-only validator used by CI runners before uploading the flow
|
|
117
|
+
// artifact. we only check: file exists, parseable yaml, has at least one
|
|
118
|
+
// real interaction step. playback validation is a separate concern handled
|
|
119
|
+
// by `rnx maestro end --validate` during the authoring session.
|
|
120
|
+
export function validateFlowFile(flowPath: string): string[] {
|
|
121
|
+
if (!fs.existsSync(flowPath)) {
|
|
122
|
+
return [`file not found: ${flowPath}`]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let content: string
|
|
126
|
+
try {
|
|
127
|
+
content = fs.readFileSync(flowPath, 'utf8')
|
|
128
|
+
} catch (e) {
|
|
129
|
+
return [`cannot read file: ${(e as Error).message}`]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (content.trim().length === 0) {
|
|
133
|
+
return ['file is empty']
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const parsed = parseFlowFile(content)
|
|
138
|
+
return validateFlowSteps(parsed.steps)
|
|
139
|
+
} catch (e) {
|
|
140
|
+
return [`yaml parse error: ${(e as Error).message}`]
|
|
141
|
+
}
|
|
142
|
+
}
|