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.
Files changed (324) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +640 -0
  3. package/cli/app-config.ts +82 -0
  4. package/cli/app-fonts.ts +388 -0
  5. package/cli/app-project.ts +59 -0
  6. package/cli/app-splash.ts +171 -0
  7. package/cli/app-state-reset.ts +26 -0
  8. package/cli/auth.ts +184 -0
  9. package/cli/bin.ts +59 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2806 -0
  12. package/cli/browser-backend.ts +44 -0
  13. package/cli/browser-evals.ts +96 -0
  14. package/cli/cli-update.ts +261 -0
  15. package/cli/cloud-client.ts +1530 -0
  16. package/cli/cloud-dispatch.ts +206 -0
  17. package/cli/cloud-session.ts +227 -0
  18. package/cli/command-contract.ts +221 -0
  19. package/cli/command-registry.ts +304 -0
  20. package/cli/commands/agent-wrapper.ts +1048 -0
  21. package/cli/commands/agent.ts +439 -0
  22. package/cli/commands/app-fonts.ts +99 -0
  23. package/cli/commands/assert.ts +542 -0
  24. package/cli/commands/auth.ts +71 -0
  25. package/cli/commands/box/bundle-plane.ts +71 -0
  26. package/cli/commands/box/checkout-plane.ts +256 -0
  27. package/cli/commands/box/plane-share.ts +208 -0
  28. package/cli/commands/box/rnx-command.ts +60 -0
  29. package/cli/commands/box/search-command.ts +157 -0
  30. package/cli/commands/box.ts +925 -0
  31. package/cli/commands/camera.ts +266 -0
  32. package/cli/commands/cleanup.ts +169 -0
  33. package/cli/commands/compat.ts +176 -0
  34. package/cli/commands/config.ts +34 -0
  35. package/cli/commands/control.ts +2154 -0
  36. package/cli/commands/cpu-profile.ts +941 -0
  37. package/cli/commands/daemon-mac-app.ts +165 -0
  38. package/cli/commands/daemon.ts +800 -0
  39. package/cli/commands/debug.ts +751 -0
  40. package/cli/commands/desktop.ts +39 -0
  41. package/cli/commands/detect.ts +201 -0
  42. package/cli/commands/detox.ts +481 -0
  43. package/cli/commands/device.ts +134 -0
  44. package/cli/commands/diagnose.ts +589 -0
  45. package/cli/commands/do-chain.ts +408 -0
  46. package/cli/commands/electron.ts +94 -0
  47. package/cli/commands/film.ts +379 -0
  48. package/cli/commands/flow.ts +1240 -0
  49. package/cli/commands/inspect/actions.ts +516 -0
  50. package/cli/commands/inspect/core.ts +2086 -0
  51. package/cli/commands/inspect/count.ts +17 -0
  52. package/cli/commands/inspect/describe.ts +119 -0
  53. package/cli/commands/inspect/env.ts +25 -0
  54. package/cli/commands/inspect/find.ts +169 -0
  55. package/cli/commands/inspect/get-layout.ts +53 -0
  56. package/cli/commands/inspect/keyboard.ts +54 -0
  57. package/cli/commands/inspect/list.ts +175 -0
  58. package/cli/commands/inspect/memory.ts +215 -0
  59. package/cli/commands/inspect/redaction.ts +39 -0
  60. package/cli/commands/inspect/resolve-target.ts +77 -0
  61. package/cli/commands/inspect/screens.ts +78 -0
  62. package/cli/commands/inspect/settle.ts +32 -0
  63. package/cli/commands/inspect/settling.ts +197 -0
  64. package/cli/commands/inspect/shared.ts +259 -0
  65. package/cli/commands/inspect/sleep.ts +15 -0
  66. package/cli/commands/inspect/tree.ts +23 -0
  67. package/cli/commands/inspect/url.ts +17 -0
  68. package/cli/commands/inspect/wait-event.ts +211 -0
  69. package/cli/commands/inspect/wait-idle.ts +25 -0
  70. package/cli/commands/inspect/wait-ready.ts +87 -0
  71. package/cli/commands/inspect/wait-selector.ts +59 -0
  72. package/cli/commands/inspect/wait.ts +32 -0
  73. package/cli/commands/inspect.ts +4738 -0
  74. package/cli/commands/install-cli.ts +275 -0
  75. package/cli/commands/install-desktop.ts +385 -0
  76. package/cli/commands/login.ts +357 -0
  77. package/cli/commands/logout.ts +24 -0
  78. package/cli/commands/maestro-generate.ts +375 -0
  79. package/cli/commands/maestro.ts +536 -0
  80. package/cli/commands/mode.ts +58 -0
  81. package/cli/commands/no-bridge-hint.ts +80 -0
  82. package/cli/commands/perf.ts +67 -0
  83. package/cli/commands/permissions.ts +203 -0
  84. package/cli/commands/platform.ts +410 -0
  85. package/cli/commands/preview.ts +103 -0
  86. package/cli/commands/profile.ts +106 -0
  87. package/cli/commands/react.ts +353 -0
  88. package/cli/commands/record.ts +1435 -0
  89. package/cli/commands/report-issue.ts +305 -0
  90. package/cli/commands/reset.ts +89 -0
  91. package/cli/commands/runtime.ts +348 -0
  92. package/cli/commands/screenshot-command.ts +106 -0
  93. package/cli/commands/screenshot-layers.ts +144 -0
  94. package/cli/commands/screenshot-mode.ts +38 -0
  95. package/cli/commands/screenshot.ts +492 -0
  96. package/cli/commands/screenshots-capture.ts +608 -0
  97. package/cli/commands/screenshots.ts +127 -0
  98. package/cli/commands/serve.ts +139 -0
  99. package/cli/commands/setup.ts +416 -0
  100. package/cli/commands/shell-boolean-mode.ts +82 -0
  101. package/cli/commands/skills.ts +468 -0
  102. package/cli/commands/slides.ts +357 -0
  103. package/cli/commands/state.ts +94 -0
  104. package/cli/commands/storage.ts +58 -0
  105. package/cli/commands/telemetry.ts +55 -0
  106. package/cli/commands/test.ts +535 -0
  107. package/cli/commands/three-mode.ts +771 -0
  108. package/cli/commands/timeline.ts +121 -0
  109. package/cli/commands/upgrade.ts +167 -0
  110. package/cli/commands/upload.ts +1203 -0
  111. package/cli/commands/version.ts +63 -0
  112. package/cli/commands/welcome.ts +17 -0
  113. package/cli/commands/what-happened.ts +328 -0
  114. package/cli/current-sim.ts +217 -0
  115. package/cli/desktop-companion.ts +315 -0
  116. package/cli/drivers/electron.ts +70 -0
  117. package/cli/drivers/index.ts +20 -0
  118. package/cli/drivers/playwright-provisioning.ts +184 -0
  119. package/cli/drivers/playwright-sim-host.ts +537 -0
  120. package/cli/drivers/playwright.ts +209 -0
  121. package/cli/drivers/registry.ts +65 -0
  122. package/cli/drivers/types.ts +102 -0
  123. package/cli/flow-export.ts +55 -0
  124. package/cli/flow-file.ts +142 -0
  125. package/cli/flow-live-status.ts +126 -0
  126. package/cli/flow-session.ts +140 -0
  127. package/cli/help-core.ts +143 -0
  128. package/cli/help.ts +44 -0
  129. package/cli/hidden-runtime-alias.ts +19 -0
  130. package/cli/hints.ts +214 -0
  131. package/cli/inspect-notice-state.ts +114 -0
  132. package/cli/internal-child.ts +176 -0
  133. package/cli/maestro-js.ts +323 -0
  134. package/cli/main.ts +704 -0
  135. package/cli/open-url.ts +8 -0
  136. package/cli/outbound-endpoints.ts +830 -0
  137. package/cli/parent-pid.ts +204 -0
  138. package/cli/parse-args.ts +189 -0
  139. package/cli/privacy.ts +53 -0
  140. package/cli/prompt.ts +56 -0
  141. package/cli/recording-access.ts +108 -0
  142. package/cli/registry.ts +2 -0
  143. package/cli/resolve-assets.ts +63 -0
  144. package/cli/rnx-in-process.ts +98 -0
  145. package/cli/run-registry.ts +226 -0
  146. package/cli/run-rnx-core.ts +148 -0
  147. package/cli/run-rnx.ts +137 -0
  148. package/cli/runtime-notes.ts +66 -0
  149. package/cli/runtime-summary.ts +25 -0
  150. package/cli/secure-file.ts +31 -0
  151. package/cli/self-invocation.ts +58 -0
  152. package/cli/send-to-box.ts +109 -0
  153. package/cli/setup-repository.ts +191 -0
  154. package/cli/shell-init.ts +98 -0
  155. package/cli/standalone.ts +4 -0
  156. package/cli/telemetry.ts +84 -0
  157. package/cli/ui.ts +68 -0
  158. package/cli/ws-bridge.ts +1020 -0
  159. package/detox/attempt-events.cjs +23 -0
  160. package/detox/colors.ts +54 -0
  161. package/detox/config-loader.ts +135 -0
  162. package/detox/element-types.ts +48 -0
  163. package/detox/expectations.ts +534 -0
  164. package/detox/gestures.ts +501 -0
  165. package/detox/index.ts +2694 -0
  166. package/detox/jest-preset.cjs +29 -0
  167. package/detox/jest-setup-after-env.cjs +22 -0
  168. package/detox/matchers.ts +30 -0
  169. package/detox/motion-change.cjs +109 -0
  170. package/detox/motion-change.d.cts +35 -0
  171. package/detox/navigation-lifecycle.ts +290 -0
  172. package/detox/navigation.ts +43 -0
  173. package/detox/proof-frame.cjs +114 -0
  174. package/detox/record-block-execution.cjs +78 -0
  175. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  176. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  177. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  178. package/detox/screenshots/theme-blue-toggle.png +0 -0
  179. package/detox/screenshots/theme-blue.png +0 -0
  180. package/detox/screenshots/theme-red-toggle.png +0 -0
  181. package/detox/screenshots/theme-red.png +0 -0
  182. package/dist-lib/agent-daemon-client.cjs +502 -0
  183. package/dist-lib/agent-events.cjs +48 -0
  184. package/dist-lib/agent-identity.cjs +53 -0
  185. package/dist-lib/agent-sessions.cjs +759 -0
  186. package/dist-lib/attached-projects.cjs +526 -0
  187. package/dist-lib/auth/shared-session.cjs +208 -0
  188. package/dist-lib/backend-origin.cjs +88 -0
  189. package/dist-lib/beta.cjs +41 -0
  190. package/dist-lib/beta.mjs +13 -0
  191. package/dist-lib/bridge-constants.cjs +161 -0
  192. package/dist-lib/bridge-contract-input.cjs +451 -0
  193. package/dist-lib/bridge-contract-input.mjs +414 -0
  194. package/dist-lib/bridge-contract.cjs +130 -0
  195. package/dist-lib/bridge-contract.mjs +94 -0
  196. package/dist-lib/capture-contract.cjs +220 -0
  197. package/dist-lib/capture-contract.mjs +190 -0
  198. package/dist-lib/cli-constants.cjs +32 -0
  199. package/dist-lib/cloud-contract.cjs +194 -0
  200. package/dist-lib/cloud-contract.mjs +145 -0
  201. package/dist-lib/cloud.cjs +670 -0
  202. package/dist-lib/cloud.mjs +639 -0
  203. package/dist-lib/config.cjs +96 -0
  204. package/dist-lib/detox/index.cjs +3789 -0
  205. package/dist-lib/detox/jest-preset.cjs +29 -0
  206. package/dist-lib/dev-bundle-resolution.cjs +290 -0
  207. package/dist-lib/home-paths.cjs +638 -0
  208. package/dist-lib/host/bridge-host.cjs +10903 -0
  209. package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
  210. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  211. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  212. package/dist-lib/host/replacement-module-handler.cjs +469 -0
  213. package/dist-lib/host/websocket-proxy.cjs +298 -0
  214. package/dist-lib/index.cjs +4268 -0
  215. package/dist-lib/jump-to-source-babel.cjs +93 -0
  216. package/dist-lib/jump-to-source-native.cjs +112 -0
  217. package/dist-lib/menu.cjs +646 -0
  218. package/dist-lib/menu.mjs +604 -0
  219. package/dist-lib/metro-fingerprint-registry.cjs +186 -0
  220. package/dist-lib/metro-fingerprint-registry.mjs +156 -0
  221. package/dist-lib/metro-production-bundle.cjs +329 -0
  222. package/dist-lib/metro-production-bundle.mjs +295 -0
  223. package/dist-lib/metro.cjs +904 -0
  224. package/dist-lib/profiles.cjs +246 -0
  225. package/dist-lib/public-brand.cjs +39 -0
  226. package/dist-lib/react-native-host-modules.cjs +51 -0
  227. package/dist-lib/react-native-host-modules.mjs +25 -0
  228. package/dist-lib/render-mode.cjs +54 -0
  229. package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
  230. package/dist-lib/sdk.cjs +2433 -0
  231. package/dist-lib/sdk.mjs +2336 -0
  232. package/dist-lib/skills.cjs +39132 -0
  233. package/dist-lib/swift-submit.cjs +92 -0
  234. package/dist-lib/swift-submit.mjs +65 -0
  235. package/dist-lib/swift.cjs +1646 -0
  236. package/dist-lib/vite.cjs +1591 -0
  237. package/package.json +290 -0
  238. package/scripts/dev-server-scanner.ts +1291 -0
  239. package/scripts/optional-demo-registry.ts +113 -0
  240. package/skills/contrast/SKILL.md +165 -0
  241. package/skills/rnx-debug/SKILL.md +433 -0
  242. package/skills/rnx-perf/SKILL.md +399 -0
  243. package/skills/rnx-setup/SKILL.md +172 -0
  244. package/skills/rnx-test/SKILL.md +321 -0
  245. package/skills/rnx-visual/SKILL.md +126 -0
  246. package/src/agent-daemon-client.ts +410 -0
  247. package/src/agent-events.ts +71 -0
  248. package/src/agent-identity.ts +23 -0
  249. package/src/agent-prompt.ts +71 -0
  250. package/src/agent-sessions.ts +580 -0
  251. package/src/app-url.ts +39 -0
  252. package/src/attached-projects.ts +539 -0
  253. package/src/auth/shared-session.ts +235 -0
  254. package/src/backend-origin.ts +70 -0
  255. package/src/beta.ts +19 -0
  256. package/src/bridge-constants.ts +175 -0
  257. package/src/bridge-contract-input.ts +502 -0
  258. package/src/bridge-contract.ts +558 -0
  259. package/src/browser-cache.ts +16 -0
  260. package/src/capture-contract.ts +587 -0
  261. package/src/cli-constants.ts +1 -0
  262. package/src/cli-version.ts +40 -0
  263. package/src/cloud-contract.ts +274 -0
  264. package/src/cloud.ts +762 -0
  265. package/src/codex-client.ts +234 -0
  266. package/src/config.ts +132 -0
  267. package/src/connect.ts +101 -0
  268. package/src/dev-bundle-resolution.ts +346 -0
  269. package/src/dev-server-open.ts +198 -0
  270. package/src/disk-cleanup.ts +540 -0
  271. package/src/flow-timeout-scale.ts +15 -0
  272. package/src/home-paths.ts +806 -0
  273. package/src/host/agent-host.ts +666 -0
  274. package/src/host/bridge-host.ts +2788 -0
  275. package/src/host/fetch-proxy-handler.ts +449 -0
  276. package/src/host/fetch-proxy-overrides.ts +51 -0
  277. package/src/host/open-url.ts +89 -0
  278. package/src/host/plane-host.ts +203 -0
  279. package/src/host/replacement-module-handler.ts +158 -0
  280. package/src/host/websocket-proxy.ts +315 -0
  281. package/src/index.ts +71 -0
  282. package/src/jump-to-source-babel.ts +135 -0
  283. package/src/jump-to-source-native.ts +102 -0
  284. package/src/menu.ts +777 -0
  285. package/src/metro-fingerprint-registry.ts +302 -0
  286. package/src/metro-plugin.ts +341 -0
  287. package/src/metro-production-bundle.ts +471 -0
  288. package/src/native-dev-bundle-url.ts +118 -0
  289. package/src/native-seam-manifest.ts +341 -0
  290. package/src/playwright-host-log.ts +16 -0
  291. package/src/plist.ts +27 -0
  292. package/src/poll-until.ts +123 -0
  293. package/src/profiles.ts +179 -0
  294. package/src/public-brand.ts +9 -0
  295. package/src/react-native-host-modules.ts +26 -0
  296. package/src/render-mode.ts +27 -0
  297. package/src/rolldown-plugin-swift.ts +240 -0
  298. package/src/runtime-assets.ts +233 -0
  299. package/src/runtime-delivery.ts +54 -0
  300. package/src/screenshots/compose.ts +422 -0
  301. package/src/screenshots/frame-compose.ts +514 -0
  302. package/src/screenshots/orchestrate.ts +256 -0
  303. package/src/screenshots/org-deck.ts +91 -0
  304. package/src/screenshots/registry.ts +58 -0
  305. package/src/screenshots/schema.ts +364 -0
  306. package/src/sdk.ts +15 -0
  307. package/src/settings-bundle.ts +418 -0
  308. package/src/shell-bridge-identity.ts +15 -0
  309. package/src/sim-client.ts +697 -0
  310. package/src/skills/builtin/a11y-review.ts +132 -0
  311. package/src/skills/builtin/compat-check.ts +71 -0
  312. package/src/skills/builtin/maestro-test.ts +121 -0
  313. package/src/skills/builtin/perf-profile.ts +90 -0
  314. package/src/skills/builtin/screenshot-all.ts +52 -0
  315. package/src/skills/builtin/visual-diff.ts +100 -0
  316. package/src/skills/registry.ts +107 -0
  317. package/src/skills/types.ts +41 -0
  318. package/src/swift-submit.ts +185 -0
  319. package/src/swift.ts +7 -0
  320. package/src/team-machine-prompt.ts +73 -0
  321. package/src/vite-plugin-one.ts +264 -0
  322. package/src/vite-plugin-swift.ts +620 -0
  323. package/src/vite-plugin.ts +1946 -0
  324. 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
+ }