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,114 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'
|
|
2
|
+
import { tmpdir } from 'os'
|
|
3
|
+
import { dirname, join, resolve } from 'path'
|
|
4
|
+
|
|
5
|
+
const INSPECT_NOTICE_STATE_VERSION = 1 as const
|
|
6
|
+
const INSPECT_NOTICE_PATH_ENV = 'SOOTSIM_INSPECT_NOTICE_PATH'
|
|
7
|
+
const INSPECT_NOTICE_ENTRY_TTL_MS = 5 * 60 * 1000
|
|
8
|
+
|
|
9
|
+
export const INSPECT_NOTICE_COOLDOWN_MS = 15_000
|
|
10
|
+
|
|
11
|
+
type InspectNoticeKind = 'console' | 'requests'
|
|
12
|
+
|
|
13
|
+
interface InspectNoticeEntry {
|
|
14
|
+
signature: string
|
|
15
|
+
updatedAt: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface InspectNoticeState {
|
|
19
|
+
version: 1
|
|
20
|
+
entries: Record<string, InspectNoticeEntry>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getInspectNoticeStatePath() {
|
|
24
|
+
return resolve(
|
|
25
|
+
process.env[INSPECT_NOTICE_PATH_ENV] ||
|
|
26
|
+
join(tmpdir(), 'rnx-inspect-notice-state.json'),
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pruneEntries(
|
|
31
|
+
entries: Record<string, InspectNoticeEntry>,
|
|
32
|
+
nowMs: number,
|
|
33
|
+
): Record<string, InspectNoticeEntry> {
|
|
34
|
+
return Object.fromEntries(
|
|
35
|
+
Object.entries(entries).filter(([, entry]) => {
|
|
36
|
+
return (
|
|
37
|
+
typeof entry?.signature === 'string' &&
|
|
38
|
+
Number.isFinite(entry?.updatedAt) &&
|
|
39
|
+
nowMs - entry.updatedAt <= INSPECT_NOTICE_ENTRY_TTL_MS
|
|
40
|
+
)
|
|
41
|
+
}),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readInspectNoticeState(nowMs: number): InspectNoticeState {
|
|
46
|
+
const filepath = getInspectNoticeStatePath()
|
|
47
|
+
if (!existsSync(filepath)) {
|
|
48
|
+
return { version: INSPECT_NOTICE_STATE_VERSION, entries: {} }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(
|
|
53
|
+
readFileSync(filepath, 'utf8'),
|
|
54
|
+
) as Partial<InspectNoticeState>
|
|
55
|
+
if (
|
|
56
|
+
parsed.version !== INSPECT_NOTICE_STATE_VERSION ||
|
|
57
|
+
!parsed.entries ||
|
|
58
|
+
typeof parsed.entries !== 'object'
|
|
59
|
+
) {
|
|
60
|
+
rmSync(filepath, { force: true })
|
|
61
|
+
return { version: INSPECT_NOTICE_STATE_VERSION, entries: {} }
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
version: INSPECT_NOTICE_STATE_VERSION,
|
|
65
|
+
entries: pruneEntries(parsed.entries as Record<string, InspectNoticeEntry>, nowMs),
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
rmSync(filepath, { force: true })
|
|
69
|
+
return { version: INSPECT_NOTICE_STATE_VERSION, entries: {} }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeInspectNoticeState(state: InspectNoticeState) {
|
|
74
|
+
const filepath = getInspectNoticeStatePath()
|
|
75
|
+
mkdirSync(dirname(filepath), { recursive: true })
|
|
76
|
+
writeFileSync(filepath, JSON.stringify(state, null, 2) + '\n')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getInspectNoticeKey(kind: InspectNoticeKind, scope: string) {
|
|
80
|
+
const normalizedScope = scope.trim() || 'default'
|
|
81
|
+
return `${kind}:${normalizedScope}`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function shouldPrintInspectNotice(
|
|
85
|
+
kind: InspectNoticeKind,
|
|
86
|
+
scope: string,
|
|
87
|
+
signature: string,
|
|
88
|
+
opts: { nowMs?: number; cooldownMs?: number } = {},
|
|
89
|
+
) {
|
|
90
|
+
const nowMs = opts.nowMs ?? Date.now()
|
|
91
|
+
const cooldownMs = opts.cooldownMs ?? INSPECT_NOTICE_COOLDOWN_MS
|
|
92
|
+
const state = readInspectNoticeState(nowMs)
|
|
93
|
+
const key = getInspectNoticeKey(kind, scope)
|
|
94
|
+
const previous = state.entries[key]
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
previous &&
|
|
98
|
+
previous.signature === signature &&
|
|
99
|
+
nowMs - previous.updatedAt < cooldownMs
|
|
100
|
+
) {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
state.entries[key] = {
|
|
105
|
+
signature,
|
|
106
|
+
updatedAt: nowMs,
|
|
107
|
+
}
|
|
108
|
+
writeInspectNoticeState(state)
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function clearInspectNoticeState() {
|
|
113
|
+
rmSync(getInspectNoticeStatePath(), { force: true })
|
|
114
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// hidden child-process entry points.
|
|
2
|
+
//
|
|
3
|
+
// the CLI needs separate JS processes for a few jobs: a detached browser host
|
|
4
|
+
// that outlives `rnx open`, playwright's own installer, and a blocking fetch
|
|
5
|
+
// worker for maestro's synchronous `http` API. each used to run as
|
|
6
|
+
// `process.execPath -e '<source>'`, which cannot work once the CLI ships as a
|
|
7
|
+
// compiled binary: there process.execPath is rnx, and rnx has no -e. a
|
|
8
|
+
// shell-installed user has no node and no bun either, so there is nothing else
|
|
9
|
+
// to reach for. the binary is the JS runtime, and it runs these itself.
|
|
10
|
+
//
|
|
11
|
+
// spawn them through rnxSelfInvocation() so the npm and standalone shapes take
|
|
12
|
+
// the same path. bin.ts dispatches here straight from argv, before any CLI
|
|
13
|
+
// startup work, which keeps a per-request worker (sync-http) cheap.
|
|
14
|
+
|
|
15
|
+
import { existsSync, writeFileSync } from 'node:fs'
|
|
16
|
+
import { createRequire } from 'node:module'
|
|
17
|
+
import * as vm from 'node:vm'
|
|
18
|
+
|
|
19
|
+
export const RNX_INTERNAL_COMMAND = '__internal'
|
|
20
|
+
|
|
21
|
+
export const RNX_INTERNAL_CHILDREN = {
|
|
22
|
+
playwrightProbe: 'playwright-probe',
|
|
23
|
+
playwrightInstall: 'playwright-install',
|
|
24
|
+
playwrightHost: 'playwright-host',
|
|
25
|
+
syncHttp: 'sync-http',
|
|
26
|
+
} as const
|
|
27
|
+
|
|
28
|
+
/** require() rooted at the working directory, which is where a user's
|
|
29
|
+
* node_modules lives. the compiled binary carries none of its own. */
|
|
30
|
+
function requireFromCwd(): NodeJS.Require {
|
|
31
|
+
return createRequire(`${process.cwd()}/`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
35
|
+
return typeof value === 'object' && value !== null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** what a one-shot child hands back to the caller that is blocked reading it. */
|
|
39
|
+
interface ChildReply {
|
|
40
|
+
stdout: string
|
|
41
|
+
code: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function runInternalChild(args: string[]): Promise<void> {
|
|
45
|
+
const [name, ...rest] = args
|
|
46
|
+
let reply: ChildReply | undefined
|
|
47
|
+
try {
|
|
48
|
+
switch (name) {
|
|
49
|
+
case RNX_INTERNAL_CHILDREN.playwrightProbe:
|
|
50
|
+
reply = playwrightProbe()
|
|
51
|
+
break
|
|
52
|
+
case RNX_INTERNAL_CHILDREN.playwrightInstall:
|
|
53
|
+
playwrightInstall(rest)
|
|
54
|
+
break
|
|
55
|
+
case RNX_INTERNAL_CHILDREN.playwrightHost:
|
|
56
|
+
await playwrightHost()
|
|
57
|
+
break
|
|
58
|
+
case RNX_INTERNAL_CHILDREN.syncHttp:
|
|
59
|
+
reply = await syncHttp()
|
|
60
|
+
break
|
|
61
|
+
default:
|
|
62
|
+
throw new Error(`unknown internal child: ${name ?? '(none)'}`)
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
writeFileSync(2, `${error instanceof Error ? error.stack : String(error)}\n`)
|
|
66
|
+
process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// the two one-shot children answer a caller that is blocked in spawnSync
|
|
70
|
+
// until this process ends, so end it on the reply rather than whenever the
|
|
71
|
+
// runtime decides nothing is left open. that is only safe because the write
|
|
72
|
+
// goes through the file descriptor: through process.stdout a pipe write
|
|
73
|
+
// completes asynchronously on macos, and exiting would truncate the json
|
|
74
|
+
// being parsed on the other side.
|
|
75
|
+
if (reply) {
|
|
76
|
+
writeFileSync(1, reply.stdout)
|
|
77
|
+
process.exit(reply.code)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// playwright's installer and the browser host are both still working when
|
|
81
|
+
// they return here. they own the process from now on, and it ends when their
|
|
82
|
+
// own work does.
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** report where the resolved playwright package expects its chromium and
|
|
86
|
+
* whether that file is there. a separate process so the caller's environment
|
|
87
|
+
* snapshot — PLAYWRIGHT_BROWSERS_PATH above all — is the only browser cache
|
|
88
|
+
* this ever sees. */
|
|
89
|
+
function playwrightProbe(): ChildReply {
|
|
90
|
+
const modulePath = process.env.SOOTSIM_PW_MODULE
|
|
91
|
+
if (!modulePath) throw new Error('SOOTSIM_PW_MODULE is not set')
|
|
92
|
+
const playwright: unknown = requireFromCwd()(modulePath)
|
|
93
|
+
const chromium = isRecord(playwright) ? playwright.chromium : null
|
|
94
|
+
if (!isRecord(chromium) || typeof chromium.executablePath !== 'function') {
|
|
95
|
+
throw new Error('resolved package does not expose browser "chromium"')
|
|
96
|
+
}
|
|
97
|
+
const executablePath: unknown = chromium.executablePath()
|
|
98
|
+
if (typeof executablePath !== 'string') {
|
|
99
|
+
throw new Error('browser "chromium" reported no executable path')
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
stdout: JSON.stringify({ executablePath, exists: existsSync(executablePath) }),
|
|
103
|
+
code: 0,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** run playwright's own CLI entry, the only thing allowed to install the
|
|
108
|
+
* browser revision its API named. argv is shaped the way that CLI's argument
|
|
109
|
+
* parser expects to receive it from node. it owns this process from here:
|
|
110
|
+
* it sets the exit code and may exit outright. */
|
|
111
|
+
function playwrightInstall(args: string[]): void {
|
|
112
|
+
const [cliPath, ...cliArgs] = args
|
|
113
|
+
if (!cliPath) {
|
|
114
|
+
throw new Error('internal playwright-install needs the package CLI path')
|
|
115
|
+
}
|
|
116
|
+
process.argv = [process.argv[0], cliPath, ...cliArgs]
|
|
117
|
+
requireFromCwd()(cliPath)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** the detached browser host. its source is CJS that resolves playwright from
|
|
121
|
+
* an absolute path the driver hands it in the environment, launches chrome,
|
|
122
|
+
* and keeps this process alive until the browser goes away. */
|
|
123
|
+
async function playwrightHost(): Promise<void> {
|
|
124
|
+
const { PLAYWRIGHT_SIM_HOST } = await import('./drivers/playwright-sim-host')
|
|
125
|
+
const run: unknown = vm.runInThisContext(
|
|
126
|
+
`(function(require){${PLAYWRIGHT_SIM_HOST}\n})`,
|
|
127
|
+
{ filename: 'rnx-playwright-host.js' },
|
|
128
|
+
)
|
|
129
|
+
if (typeof run !== 'function') throw new Error('browser host source is not runnable')
|
|
130
|
+
run(requireFromCwd())
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** one blocking fetch. maestro's flow `http` API is synchronous and node has
|
|
134
|
+
* no synchronous fetch, so the caller round-trips each request through a
|
|
135
|
+
* spawnSync of this: JSON request on stdin, JSON response on stdout. a failed
|
|
136
|
+
* request is still a reply — the flow engine reads the error out of the same
|
|
137
|
+
* json — so only the exit code separates the two. */
|
|
138
|
+
async function syncHttp(): Promise<ChildReply> {
|
|
139
|
+
const chunks: Buffer[] = []
|
|
140
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk))
|
|
141
|
+
try {
|
|
142
|
+
const request: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
143
|
+
if (!isRecord(request) || typeof request.url !== 'string') {
|
|
144
|
+
throw new Error('sync-http needs a JSON request with a url')
|
|
145
|
+
}
|
|
146
|
+
const response = await fetch(request.url, {
|
|
147
|
+
method: typeof request.method === 'string' ? request.method : undefined,
|
|
148
|
+
headers: isRecord(request.headers) ? readStringMap(request.headers) : undefined,
|
|
149
|
+
body: typeof request.body === 'string' ? request.body : undefined,
|
|
150
|
+
})
|
|
151
|
+
const body = await response.text()
|
|
152
|
+
const headers: Record<string, string> = {}
|
|
153
|
+
for (const [key, value] of response.headers.entries()) {
|
|
154
|
+
headers[key] = headers[key] ? `${headers[key]},${value}` : value
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
stdout: JSON.stringify({ ok: response.ok, status: response.status, body, headers }),
|
|
158
|
+
code: 0,
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return {
|
|
162
|
+
stdout: JSON.stringify({
|
|
163
|
+
__error: error instanceof Error ? error.message : String(error),
|
|
164
|
+
}),
|
|
165
|
+
code: 1,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function readStringMap(value: Record<string, unknown>): Record<string, string> {
|
|
171
|
+
const result: Record<string, string> = {}
|
|
172
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
173
|
+
if (typeof entry === 'string') result[key] = entry
|
|
174
|
+
}
|
|
175
|
+
return result
|
|
176
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// maestro-compatible flow JS engine — mirrors maestro's GraalJsEngine
|
|
2
|
+
// (maestro-client/src/main/java/maestro/js/GraalJsEngine.kt) on top of
|
|
3
|
+
// node's vm module. one context per flow run, shared by the whole flow
|
|
4
|
+
// including runFlow sub-flows, exactly like upstream:
|
|
5
|
+
//
|
|
6
|
+
// globals: http, output (persistent map), maestro ({copiedText, platform}),
|
|
7
|
+
// json(), relativePoint(), env vars as bare globals (scope-stacked per
|
|
8
|
+
// sub-flow), console.log routed to the flow log. undeclared identifiers
|
|
9
|
+
// evaluate to undefined for `${MY_VAR || 'default'}` patterns (upstream uses
|
|
10
|
+
// a has-trapping Proxy as globalThis's prototype; node/V8 forbids that, so we
|
|
11
|
+
// get the same semantics from a with-scope proxy — see the constructor).
|
|
12
|
+
//
|
|
13
|
+
// `${expr}` string templates are full JS evaluation at command-execution
|
|
14
|
+
// time (maestro-orchestra-models util/Env.kt): regex (?<!\\)\$\{([^$]*)},
|
|
15
|
+
// result stringified, `\${...}` escapes to a literal, blank exprs become ''.
|
|
16
|
+
//
|
|
17
|
+
// divergences (deliberate, rnx-side ergonomics — keep this list short):
|
|
18
|
+
// 1. envBinding is seeded from the full process.env, not just MAESTRO_*.
|
|
19
|
+
// rnx flows historically resolve `${SOOTSIM_*}` credentials from the
|
|
20
|
+
// shell environment and `rnx maestro --env K=V` writes process.env.
|
|
21
|
+
// 2. a *bare identifier* template (`${NAME}`) that evaluates to undefined
|
|
22
|
+
// throws a loud "missing variable" error instead of interpolating the
|
|
23
|
+
// literal text "undefined" into a tap target or login form. complex
|
|
24
|
+
// expressions keep upstream semantics (undefined -> "undefined").
|
|
25
|
+
// 3. `faker` is not implemented — flows using it fail loudly rather than
|
|
26
|
+
// getting a half-faithful reimplementation of datafaker.
|
|
27
|
+
|
|
28
|
+
import { spawnSync } from 'child_process'
|
|
29
|
+
import * as vm from 'vm'
|
|
30
|
+
import { RNX_INTERNAL_CHILDREN, RNX_INTERNAL_COMMAND } from './internal-child'
|
|
31
|
+
import { rnxSelfInvocation } from './self-invocation'
|
|
32
|
+
|
|
33
|
+
const HTTP_TIMEOUT_MS = 300_000 // upstream: okhttp read/write/call timeout 5min
|
|
34
|
+
|
|
35
|
+
interface HttpParams {
|
|
36
|
+
body?: string
|
|
37
|
+
headers?: Record<string, string>
|
|
38
|
+
method?: string
|
|
39
|
+
multipartForm?: unknown
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function executeSyncHttp(
|
|
43
|
+
url: string,
|
|
44
|
+
method: string,
|
|
45
|
+
params?: HttpParams,
|
|
46
|
+
): { ok: boolean; status: number; body: string; headers: Record<string, string> } {
|
|
47
|
+
if (params?.multipartForm) {
|
|
48
|
+
throw new Error('http: multipartForm is not supported by rnx yet')
|
|
49
|
+
}
|
|
50
|
+
// maestro's `http` API is synchronous (flows do `var res = http.post(...)`)
|
|
51
|
+
// and there is no synchronous fetch, so each request round-trips through a
|
|
52
|
+
// blocking spawn of this CLI's own fetch worker: JSON in on stdin, JSON out
|
|
53
|
+
// on stdout.
|
|
54
|
+
//
|
|
55
|
+
// the child must be hermetic: when the runner runs under `bun` (or any
|
|
56
|
+
// process with a debug inspector), the inherited BUN_INSPECT*/NODE_OPTIONS
|
|
57
|
+
// env makes the child attach to the parent's inspector/IPC socket and exit 1
|
|
58
|
+
// with no output instead of running our fetch. scrub those so the child is a
|
|
59
|
+
// clean fetch worker regardless of how the parent was launched.
|
|
60
|
+
const childEnv = { ...process.env }
|
|
61
|
+
for (const key of Object.keys(childEnv)) {
|
|
62
|
+
if (key.startsWith('BUN_INSPECT') || key === 'NODE_OPTIONS') {
|
|
63
|
+
delete childEnv[key]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const self = rnxSelfInvocation()
|
|
67
|
+
const child = spawnSync(
|
|
68
|
+
self.executable,
|
|
69
|
+
[...self.prefixArgs, RNX_INTERNAL_COMMAND, RNX_INTERNAL_CHILDREN.syncHttp],
|
|
70
|
+
{
|
|
71
|
+
input: JSON.stringify({
|
|
72
|
+
url,
|
|
73
|
+
method,
|
|
74
|
+
headers: params?.headers,
|
|
75
|
+
body: params?.body,
|
|
76
|
+
}),
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
timeout: HTTP_TIMEOUT_MS,
|
|
79
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
80
|
+
env: childEnv,
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
if (child.error) {
|
|
84
|
+
throw new Error(`http ${method} ${url} failed: ${child.error.message}`)
|
|
85
|
+
}
|
|
86
|
+
let parsed: unknown
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(child.stdout || '')
|
|
89
|
+
} catch {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`http ${method} ${url} failed: no response (${(child.stderr || '').trim().slice(0, 200)})`,
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
const result = parsed as {
|
|
95
|
+
__error?: string
|
|
96
|
+
ok: boolean
|
|
97
|
+
status: number
|
|
98
|
+
body: string
|
|
99
|
+
headers: Record<string, string>
|
|
100
|
+
}
|
|
101
|
+
if (result.__error) {
|
|
102
|
+
throw new Error(`http ${method} ${url} failed: ${result.__error}`)
|
|
103
|
+
}
|
|
104
|
+
return result
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// matches upstream Env.kt: negative-lookbehind for the \ escape, [^$]* body.
|
|
108
|
+
const TEMPLATE_RE = /(?<!\\)\$\{([^$]*)\}/g
|
|
109
|
+
const ESCAPED_TEMPLATE_RE = /\\(\$\{[^$]*\})/g
|
|
110
|
+
const BARE_IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
|
111
|
+
|
|
112
|
+
export class MaestroJsContext {
|
|
113
|
+
private readonly context: vm.Context
|
|
114
|
+
private readonly envBinding = new Map<string, string>()
|
|
115
|
+
private readonly envScopeStack: Array<Map<string, string>> = []
|
|
116
|
+
// env keys currently materialized as context globals — cleared and re-set
|
|
117
|
+
// before each evaluation, mirroring upstream syncBindingsToContext.
|
|
118
|
+
private syncedEnvKeys = new Set<string>()
|
|
119
|
+
readonly output: Record<string, unknown> = {}
|
|
120
|
+
readonly maestro: { copiedText: string | null; platform: string }
|
|
121
|
+
|
|
122
|
+
constructor(opts: { platform?: string; onLog?: (msg: string) => void } = {}) {
|
|
123
|
+
this.maestro = { copiedText: null, platform: opts.platform ?? 'ios' }
|
|
124
|
+
const onLog = opts.onLog ?? ((msg: string) => console.log(`[flow] js: ${msg}`))
|
|
125
|
+
const sandbox: Record<string, unknown> = {
|
|
126
|
+
output: this.output,
|
|
127
|
+
maestro: this.maestro,
|
|
128
|
+
http: {
|
|
129
|
+
get: (url: string, params?: HttpParams) => executeSyncHttp(url, 'GET', params),
|
|
130
|
+
post: (url: string, params?: HttpParams) => executeSyncHttp(url, 'POST', params),
|
|
131
|
+
put: (url: string, params?: HttpParams) => executeSyncHttp(url, 'PUT', params),
|
|
132
|
+
delete: (url: string, params?: HttpParams) =>
|
|
133
|
+
executeSyncHttp(url, 'DELETE', params),
|
|
134
|
+
request: (url: string, params?: HttpParams) =>
|
|
135
|
+
executeSyncHttp(url, (params?.method ?? 'GET').toUpperCase(), params),
|
|
136
|
+
},
|
|
137
|
+
console: {
|
|
138
|
+
log: (...args: unknown[]) =>
|
|
139
|
+
onLog(args.map((a) => (typeof a === 'string' ? a : stringifyJs(a))).join(' ')),
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
this.context = vm.createContext(sandbox)
|
|
143
|
+
// upstream GraalJsEngine resolves undeclared identifiers to undefined via
|
|
144
|
+
// a has-trapping Proxy installed as globalThis's prototype, enabling the
|
|
145
|
+
// `${VAR || 'default'}` idiom. node/V8's vm forbids a Proxy in the global
|
|
146
|
+
// prototype chain ("Proxy is not allowed in the global prototype chain"),
|
|
147
|
+
// so we get the identical semantics with a `with`-scope proxy instead:
|
|
148
|
+
// evalRaw wraps every script in `with(__maestroScope){…}`, and the proxy's
|
|
149
|
+
// has→true makes any bare identifier resolve through the scope (returning
|
|
150
|
+
// the real global, or undefined for a name that isn't defined) rather than
|
|
151
|
+
// throwing ReferenceError. plus the json/relativePoint helpers.
|
|
152
|
+
vm.runInContext(
|
|
153
|
+
`
|
|
154
|
+
globalThis.__maestroScope = new Proxy(globalThis, {
|
|
155
|
+
has() { return true },
|
|
156
|
+
get(target, key) { return target[key] },
|
|
157
|
+
})
|
|
158
|
+
function json(text) { return JSON.parse(text) }
|
|
159
|
+
function relativePoint(x, y) {
|
|
160
|
+
var xPercent = Math.ceil(x * 100) + '%'
|
|
161
|
+
var yPercent = Math.ceil(y * 100) + '%'
|
|
162
|
+
return xPercent + ',' + yPercent
|
|
163
|
+
}
|
|
164
|
+
`,
|
|
165
|
+
this.context,
|
|
166
|
+
)
|
|
167
|
+
// divergence #1: seed env from the shell so `${SOOTSIM_*}` keeps working.
|
|
168
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
169
|
+
if (value !== undefined) this.envBinding.set(key, value)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
putEnv(key: string, value: string) {
|
|
174
|
+
this.envBinding.set(key, value)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
setCopiedText(text: string | null) {
|
|
178
|
+
this.maestro.copiedText = text
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
enterEnvScope() {
|
|
182
|
+
this.envScopeStack.push(new Map(this.envBinding))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
leaveEnvScope() {
|
|
186
|
+
const previous = this.envScopeStack.pop()
|
|
187
|
+
if (previous) {
|
|
188
|
+
this.envBinding.clear()
|
|
189
|
+
for (const [k, v] of previous) this.envBinding.set(k, v)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// evaluate a JS snippet in the shared context. mirrors upstream
|
|
194
|
+
// evalWithIIFE: the script is embedded in a template literal and run
|
|
195
|
+
// through eval inside an IIFE so var/let/const stay scoped to this
|
|
196
|
+
// evaluation while the last expression's value is returned.
|
|
197
|
+
evaluate(script: string, opts: { env?: Record<string, string> } = {}): unknown {
|
|
198
|
+
const env = opts.env
|
|
199
|
+
if (env && Object.keys(env).length > 0) {
|
|
200
|
+
this.enterEnvScope()
|
|
201
|
+
try {
|
|
202
|
+
for (const [k, v] of Object.entries(env)) this.envBinding.set(k, v)
|
|
203
|
+
return this.evalRaw(script)
|
|
204
|
+
} finally {
|
|
205
|
+
this.leaveEnvScope()
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return this.evalRaw(script)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private evalRaw(script: string): unknown {
|
|
212
|
+
this.syncEnvToContext()
|
|
213
|
+
const escaped = script
|
|
214
|
+
.replace(/\\/g, '\\\\')
|
|
215
|
+
.replace(/`/g, '\\`')
|
|
216
|
+
.replace(/\$\{/g, '\\${')
|
|
217
|
+
// `with(__maestroScope)` gives undeclared identifiers the upstream
|
|
218
|
+
// undefined semantics (see constructor); the IIFE + direct eval keep
|
|
219
|
+
// var/let/const scoped to this evaluation while returning the last value.
|
|
220
|
+
return vm.runInContext(
|
|
221
|
+
`(function(){ with(__maestroScope){ return eval(\`${escaped}\`) } })()`,
|
|
222
|
+
this.context,
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private syncEnvToContext() {
|
|
227
|
+
const globals = vm.runInContext('globalThis', this.context) as Record<string, unknown>
|
|
228
|
+
for (const key of this.syncedEnvKeys) {
|
|
229
|
+
if (!this.envBinding.has(key)) delete globals[key]
|
|
230
|
+
}
|
|
231
|
+
this.syncedEnvKeys = new Set()
|
|
232
|
+
for (const [key, value] of this.envBinding) {
|
|
233
|
+
try {
|
|
234
|
+
globals[key] = value
|
|
235
|
+
this.syncedEnvKeys.add(key)
|
|
236
|
+
} catch {
|
|
237
|
+
// non-writable global (e.g. a frozen builtin name in env) — skip
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// upstream Env.kt String.evaluateScripts: replace every unescaped `${expr}`
|
|
243
|
+
// with the stringified evaluation result, then unescape `\${...}`.
|
|
244
|
+
evaluateStringTemplate(value: string): string {
|
|
245
|
+
const replaced = value.replace(TEMPLATE_RE, (_match, expr: string) => {
|
|
246
|
+
if (expr.trim().length === 0) return ''
|
|
247
|
+
const result = this.evaluate(expr)
|
|
248
|
+
// divergence #2: a bare `${NAME}` that comes back undefined is a flow
|
|
249
|
+
// bug 100% of the time — fail loudly instead of typing "undefined".
|
|
250
|
+
if (result === undefined && BARE_IDENTIFIER_RE.test(expr.trim())) {
|
|
251
|
+
throw new Error(`missing variable for flow placeholder: ${expr.trim()}`)
|
|
252
|
+
}
|
|
253
|
+
return stringifyJs(result)
|
|
254
|
+
})
|
|
255
|
+
return replaced.replace(ESCAPED_TEMPLATE_RE, (_match, inner: string) => inner)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// walk a parsed step and evaluate every string template. nested command
|
|
259
|
+
// lists are skipped — each nested command evaluates its own strings when
|
|
260
|
+
// it executes (matching upstream per-command evaluateScripts timing, and
|
|
261
|
+
// required for values that change between iterations, e.g. copyTextFrom
|
|
262
|
+
// inside repeat).
|
|
263
|
+
interpolateStep<T>(step: T): T {
|
|
264
|
+
return this.interpolateValue(step, null) as T
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private interpolateValue(value: unknown, parentKey: string | null): unknown {
|
|
268
|
+
if (typeof value === 'string') {
|
|
269
|
+
// evalScript IS the evaluation — the runner template-evaluates it in
|
|
270
|
+
// its handler. interpolating here too would run the script twice.
|
|
271
|
+
if (parentKey === 'evalScript') return value
|
|
272
|
+
return this.evaluateStringTemplate(value)
|
|
273
|
+
}
|
|
274
|
+
if (Array.isArray(value)) {
|
|
275
|
+
if (
|
|
276
|
+
parentKey === 'commands' ||
|
|
277
|
+
parentKey === 'onFlowStart' ||
|
|
278
|
+
parentKey === 'onFlowComplete'
|
|
279
|
+
) {
|
|
280
|
+
return value
|
|
281
|
+
}
|
|
282
|
+
return value.map((entry) => this.interpolateValue(entry, parentKey))
|
|
283
|
+
}
|
|
284
|
+
if (value && typeof value === 'object') {
|
|
285
|
+
return Object.fromEntries(
|
|
286
|
+
Object.entries(value).map(([key, entry]) => [
|
|
287
|
+
key,
|
|
288
|
+
this.interpolateValue(entry, key),
|
|
289
|
+
]),
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
return value
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function stringifyJs(value: unknown): string {
|
|
297
|
+
if (value === undefined) return 'undefined'
|
|
298
|
+
if (value === null) return 'null'
|
|
299
|
+
if (typeof value === 'object') {
|
|
300
|
+
try {
|
|
301
|
+
return JSON.stringify(value)
|
|
302
|
+
} catch {
|
|
303
|
+
return String(value)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return String(value)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// upstream Orchestra.evaluateCondition scriptCondition semantics: the value
|
|
310
|
+
// arrives already template-evaluated; false when blank / "false" (any case) /
|
|
311
|
+
// "undefined" / "null" / numerically zero. yaml may also hand us a real
|
|
312
|
+
// boolean or number before stringification — normalize first.
|
|
313
|
+
export function scriptConditionIsTruthy(value: unknown): boolean {
|
|
314
|
+
if (value === undefined || value === null) return false
|
|
315
|
+
if (typeof value === 'boolean') return value
|
|
316
|
+
const text = String(value)
|
|
317
|
+
if (text.trim().length === 0) return false
|
|
318
|
+
if (text.toLowerCase() === 'false') return false
|
|
319
|
+
if (text === 'undefined' || text === 'null') return false
|
|
320
|
+
const num = Number(text)
|
|
321
|
+
if (!Number.isNaN(num) && num === 0) return false
|
|
322
|
+
return true
|
|
323
|
+
}
|