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,536 @@
1
+ // rnx maestro — drop-in replacement for the real `maestro` cli.
2
+ //
3
+ // users with an existing `.maestro/` directory can swap `maestro test`
4
+ // for `rnx maestro test` and have their flows execute against
5
+ // rnx's in-browser engine. the heavy lifting lives in `flow.ts` +
6
+ // `bridge-flow-runner.ts`; this file just re-maps the maestro argv shape
7
+ // onto the existing flow runner and handles directory iteration + init.
8
+
9
+ import * as fs from 'fs'
10
+ import * as path from 'path'
11
+ import yaml from 'yaml'
12
+ import { resolveCliAuth } from '../auth'
13
+ import { parseFlowFile } from '../flow-file'
14
+ import { registerRun, shouldRegisterRun, stepSummaryFromTrace } from '../run-registry'
15
+ import { RnxExit } from '../run-rnx'
16
+ import {
17
+ findUnexpectedFlowArgs,
18
+ FLOW_ARG_VALUE_FLAGS,
19
+ getLastFlowPreviewUploadResult,
20
+ getLastFlowTraceSteps,
21
+ hoistLeadingSimFlag,
22
+ runMaestroAuthoring,
23
+ runFlowPlayback,
24
+ } from './flow'
25
+ import { resolveDefaultUploadOrigin } from './upload'
26
+
27
+ export interface RunMaestroOptions {
28
+ port?: number
29
+ verbose?: boolean
30
+ }
31
+
32
+ const MAESTRO_DIRS = ['.maestro', 'maestro']
33
+
34
+ function printHelp() {
35
+ console.log(`
36
+ rnx maestro — author and run Maestro YAML flows against rnx
37
+
38
+ usage:
39
+ rnx maestro # discover .maestro/ or maestro/ in cwd, run all
40
+ rnx maestro test <flow-or-dir> # mirrors "maestro test"
41
+ rnx maestro test .maestro/ # every *.yaml / *.yml in a directory
42
+ rnx maestro generate "<goal>" # generate, run, and upload a Maestro flow
43
+ rnx maestro start|keep|end # author a flow from live CLI actions
44
+ rnx maestro validate <flow> # validate a Maestro YAML file
45
+ rnx maestro init # scaffold .maestro/login.yaml
46
+ rnx maestro --list-compat # print supported/unsupported verbs
47
+
48
+ options:
49
+ --env KEY=VALUE set env vars for \${KEY} interpolation (repeatable)
50
+ --continuous accepted for maestro cli compat; rnx runs the flow once
51
+ --format <fmt> accepted for maestro cli compat (not used)
52
+ --device <model> override the flow frontmatter device for this run
53
+ --new force a fresh browser window for this run
54
+ --headed force a fresh, VISIBLE window (implies --new,
55
+ overrides --headless) — watch the flow run live
56
+ --record record a webm while the flow runs
57
+ --preview record events+video, upload, and print /preview/<id>
58
+ --preview-open open the uploaded preview link after a successful run
59
+ --preview-origin <url> upload target for --preview
60
+ --preview-public-origin <url>
61
+ public link origin for --preview
62
+ --slow <ms> delay between steps for natural pacing
63
+
64
+ examples:
65
+ rnx maestro test .maestro/login.yaml
66
+ rnx maestro test .maestro/
67
+ rnx maestro generate "log in and verify the welcome screen"
68
+ rnx maestro start
69
+ rnx maestro end --output .maestro/login.yaml --validate
70
+ rnx maestro --env USERNAME=alice test .maestro/login.yaml
71
+ rnx maestro init
72
+ `)
73
+ }
74
+
75
+ function printCompatMatrix() {
76
+ console.log(`
77
+ rnx maestro — compatibility matrix
78
+
79
+ supported verbs:
80
+ launchApp (including arguments), stopApp, clearState, clearKeychain (warn-only),
81
+ tapOn, tapAtCoords, longPressOn,
82
+ inputText, pressKey, dispatchKey, hideKeyboard, eraseText,
83
+ assertVisible, assertNotVisible, assertTreeContains,
84
+ waitFor, extendedWaitUntil, waitForAnimationToEnd,
85
+ scroll, scrollUntilVisible, scrollTo, swipe, pinch,
86
+ takeScreenshot, dumpTree, back,
87
+ repeat (times), retry (maxRetries + commands), runFlow (file / inline commands / env / when),
88
+ runScript (maestro JS: http, json(), output, env vars, console.log),
89
+ evalScript, copyTextFrom (\${maestro.copiedText}), openLink,
90
+ when: (visible / notVisible / platform / true),
91
+ optional:, label:, onFlowStart, onFlowComplete,
92
+ \${...} JS-expression interpolation at step execution time
93
+ (env vars as globals, output.*, maestro.*, \${EXPR || 'default'}).
94
+
95
+ workspace mode (directory runs):
96
+ config.yaml / config.yml discovery, flows: inclusion globs,
97
+ includeTags / excludeTags against flow frontmatter tags.
98
+
99
+ launch behavior:
100
+ launchApp.arguments: a JSON object available before guest app entry through
101
+ react-native-launch-arguments and NativeModules.SettingsManager.settings.
102
+ stopApp: keeps the guest stopped until launchApp or openLink starts it.
103
+ openLink: replaces the guest, then delivers the URL to the new tenant.
104
+
105
+ partial:
106
+ clearKeychain — warn-only, rnx has no keychain surface.
107
+ when.platform — rnx reports iOS for this runner.
108
+ openLink: app routing works; no OS-level Safari fallback.
109
+ launchApp.resetRuntime — rnx-only extension for recorded benchmarks:
110
+ keep tenant storage/auth, but soft-reload the guest app inside launchApp.
111
+ takeScreenshot — maestro string form works unchanged; rnx also accepts
112
+ { path, withFrame } for framed export.
113
+ env — rnx seeds the JS env from the full shell environment, not just
114
+ MAESTRO_* (so \${SOOTSIM_*} credentials resolve); a bare \${NAME} that is
115
+ undefined fails the step loudly instead of typing "undefined".
116
+
117
+ not yet implemented (will throw "unsupported maestro verb"):
118
+ travel, setLocation, setAirplaneMode, killApp, faker,
119
+ addMedia, startRecording, stopRecording (use --record flag),
120
+ repeat.while (only repeat.times is supported).
121
+ `)
122
+ }
123
+
124
+ function looksLikeFlag(s: string): boolean {
125
+ return s.startsWith('-')
126
+ }
127
+
128
+ // extract --env KEY=VALUE pairs from argv and apply to process.env for the
129
+ // duration of the run. returns argv with the consumed flags removed so the
130
+ // rest can be forwarded to runFlowPlayback verbatim.
131
+ function applyEnvFlags(args: string[]): string[] {
132
+ const out: string[] = []
133
+ for (let i = 0; i < args.length; i++) {
134
+ if (args[i] === '--env' || args[i] === '-e') {
135
+ const kv = args[i + 1]
136
+ if (!kv || !kv.includes('=')) {
137
+ throw new Error(`--env expects KEY=VALUE (got ${JSON.stringify(kv ?? '')})`)
138
+ }
139
+ const eq = kv.indexOf('=')
140
+ const key = kv.slice(0, eq)
141
+ const value = kv.slice(eq + 1)
142
+ process.env[key] = value
143
+ i += 1
144
+ continue
145
+ }
146
+ out.push(args[i])
147
+ }
148
+ return out
149
+ }
150
+
151
+ // maestro-cli flags with no rnx equivalent, accepted so an existing script can
152
+ // swap `maestro` for `rnx maestro` unedited. each one says out loud that it is
153
+ // being ignored: an option that is quietly dropped is how a run reports success
154
+ // while doing something other than what was asked. `--origin` is consumed here
155
+ // because only this command reads it; everything else this function leaves
156
+ // alone rides on to the flow runner, which rejects what it does not know.
157
+ function stripMaestroCompatFlags(args: string[]): string[] {
158
+ const out: string[] = []
159
+ for (let i = 0; i < args.length; i++) {
160
+ const a = args[i]
161
+ if (a === '--format' || a === '--output') {
162
+ console.warn(` warn: ${a} is accepted for maestro cli compatibility and ignored`)
163
+ i += 1
164
+ continue
165
+ }
166
+ if (a === '--origin') {
167
+ // read above for the run-registration origin. the flow runner has no
168
+ // --origin, so it must not ride into playbackArgs.
169
+ i += 1
170
+ continue
171
+ }
172
+ if (a === '-d') {
173
+ // maestro's short form for --device. rnx's device flag names a device
174
+ // model, and the flow runner reads it under the long spelling only.
175
+ out.push('--device')
176
+ continue
177
+ }
178
+ if (a === '--continuous') {
179
+ console.warn(' warn: --continuous is not implemented in rnx — running once')
180
+ continue
181
+ }
182
+ out.push(a)
183
+ }
184
+ return out
185
+ }
186
+
187
+ function findDefaultMaestroTarget(cwd: string): string | null {
188
+ for (const dir of MAESTRO_DIRS) {
189
+ const abs = path.join(cwd, dir)
190
+ if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) return abs
191
+ }
192
+ return null
193
+ }
194
+
195
+ interface MaestroWorkspaceConfig {
196
+ flows?: string[]
197
+ includeTags?: string[]
198
+ excludeTags?: string[]
199
+ }
200
+
201
+ // workspace planning for directory targets — mirrors maestro's
202
+ // WorkspaceExecutionPlanner: discover config.yaml/config.yml in the
203
+ // directory, walk it recursively for *.yaml/*.yml flow files (config files
204
+ // excluded), keep the ones matching the config's `flows:` globs (default
205
+ // `*` = top level only), then filter by includeTags/excludeTags against
206
+ // each flow's frontmatter `tags:`.
207
+ export function collectFlowFiles(target: string): string[] {
208
+ const stat = fs.statSync(target)
209
+ if (stat.isFile()) return [target]
210
+ if (!stat.isDirectory()) {
211
+ throw new Error(`not a file or directory: ${target}`)
212
+ }
213
+
214
+ let config: MaestroWorkspaceConfig = {}
215
+ const configPath = ['config.yaml', 'config.yml']
216
+ .map((name) => path.join(target, name))
217
+ .find((p) => fs.existsSync(p))
218
+ if (configPath) {
219
+ // yaml.parse returns any; shape-check the fields we use below instead
220
+ // of trusting the file
221
+ const parsed = yaml.parse(fs.readFileSync(configPath, 'utf8'))
222
+ if (parsed && typeof parsed === 'object') {
223
+ config = parsed
224
+ }
225
+ }
226
+
227
+ const globs =
228
+ Array.isArray(config.flows) && config.flows.length > 0 ? config.flows : ['*']
229
+ const isFlowFile = (rel: string) => {
230
+ const base = path.basename(rel)
231
+ if (base.startsWith('.')) return false
232
+ if (!base.endsWith('.yaml') && !base.endsWith('.yml')) return false
233
+ if (base === 'config.yaml' || base === 'config.yml') return false
234
+ return true
235
+ }
236
+ const matched = new Set<string>()
237
+ for (const glob of globs) {
238
+ for (const rel of fs.globSync(glob, { cwd: target })) {
239
+ if (!isFlowFile(rel)) continue
240
+ const abs = path.join(target, rel)
241
+ if (fs.existsSync(abs) && fs.statSync(abs).isFile()) matched.add(abs)
242
+ }
243
+ }
244
+ let files = [...matched].sort()
245
+
246
+ const includeTags = Array.isArray(config.includeTags) ? config.includeTags : []
247
+ const excludeTags = Array.isArray(config.excludeTags) ? config.excludeTags : []
248
+ if (includeTags.length > 0 || excludeTags.length > 0) {
249
+ files = files.filter((file) => {
250
+ const { frontmatter } = parseFlowFile(fs.readFileSync(file, 'utf8'))
251
+ const tags: string[] = Array.isArray(frontmatter.tags) ? frontmatter.tags : []
252
+ if (includeTags.length > 0 && !tags.some((t) => includeTags.includes(t))) {
253
+ return false
254
+ }
255
+ if (tags.some((t) => excludeTags.includes(t))) return false
256
+ return true
257
+ })
258
+ }
259
+ return files
260
+ }
261
+
262
+ async function runInit(cwd: string): Promise<number> {
263
+ const dir = path.join(cwd, '.maestro')
264
+ fs.mkdirSync(dir, { recursive: true })
265
+ const outPath = path.join(dir, 'login.yaml')
266
+ if (fs.existsSync(outPath)) {
267
+ console.error(` error: ${outPath} already exists`)
268
+ return 1
269
+ }
270
+ const starter = `# rnx maestro starter flow — drop-in compatible with the maestro cli.
271
+ # run: rnx maestro test .maestro/login.yaml
272
+ appId: com.example.app
273
+ ---
274
+ - launchApp
275
+ - tapOn:
276
+ id: "email"
277
+ - inputText: "user@example.com"
278
+ - tapOn:
279
+ id: "password"
280
+ - inputText: "secret123"
281
+ - tapOn: "Sign in"
282
+ - assertVisible: "Welcome"
283
+ `
284
+ fs.writeFileSync(outPath, starter, 'utf8')
285
+ console.log(` + wrote ${outPath}`)
286
+ console.log(` next: rnx maestro test .maestro/login.yaml`)
287
+ return 0
288
+ }
289
+
290
+ export async function runMaestro(
291
+ args: string[],
292
+ opts: RunMaestroOptions = {},
293
+ ): Promise<number> {
294
+ if (args.includes('--help') || args.includes('-h')) {
295
+ printHelp()
296
+ return 0
297
+ }
298
+ if (args.includes('--list-compat')) {
299
+ printCompatMatrix()
300
+ return 0
301
+ }
302
+
303
+ let remaining: string[]
304
+ try {
305
+ remaining = applyEnvFlags(args)
306
+ } catch (err) {
307
+ console.error(` error: ${(err as Error).message}`)
308
+ return 1
309
+ }
310
+ // bin.ts prepends the global `--sim <id>` to argv. without hoisting it
311
+ // out, `remaining[0]` is `--sim` instead of the `test` subcommand, so
312
+ // maestro auto-discovered a default flow and forwarded `--sim`/`test`/
313
+ // the path as bogus flow args — the explicit sim was silently dropped
314
+ // (QA F21-4). hoist it to the tail so it still rides into forwardArgs →
315
+ // runFlowPlayback, where parseBridgeCliArgs sees simIdSource === 'flag'.
316
+ remaining = hoistLeadingSimFlag(remaining)
317
+
318
+ const cwd = process.cwd()
319
+ const sub = remaining[0]
320
+
321
+ if (sub === 'init') {
322
+ return runInit(cwd)
323
+ }
324
+ if (sub === 'generate') {
325
+ const { runMaestroGenerate } = await import('./maestro-generate')
326
+ return runMaestroGenerate(remaining.slice(1))
327
+ }
328
+ if (
329
+ sub === 'start' ||
330
+ sub === 'keep' ||
331
+ sub === 'good' ||
332
+ sub === 'end' ||
333
+ sub === 'validate'
334
+ ) {
335
+ return runMaestroAuthoring(remaining)
336
+ }
337
+ // `--origin` names the Contrast instance that hosts the run row. this command
338
+ // is its only reader, so read it before stripMaestroCompatFlags consumes it.
339
+ const originFlagIndex = remaining.indexOf('--origin')
340
+ const originFlagValue =
341
+ originFlagIndex >= 0 ? remaining[originFlagIndex + 1] : undefined
342
+ remaining = stripMaestroCompatFlags(remaining)
343
+
344
+ // resolve the target flow file or directory.
345
+ let target: string | null = null
346
+ let forwardArgs: string[]
347
+
348
+ if (sub === 'test') {
349
+ // `rnx maestro test <flow-or-dir> [flags...]`. the flow path is the first
350
+ // token that is neither a flag nor the value of one — without the second
351
+ // half, `test --sim a2 login.yaml` takes `a2` as the flow file. remove it
352
+ // by index, since a path can equal a flag value elsewhere in the argv.
353
+ const rest = remaining.slice(1)
354
+ const positionalIndex = rest.findIndex(
355
+ (a, i) =>
356
+ !looksLikeFlag(a) && !(i > 0 && FLOW_ARG_VALUE_FLAGS.includes(rest[i - 1])),
357
+ )
358
+ if (positionalIndex >= 0) {
359
+ target = path.resolve(cwd, rest[positionalIndex])
360
+ forwardArgs = rest.filter((_, i) => i !== positionalIndex)
361
+ } else {
362
+ target = findDefaultMaestroTarget(cwd)
363
+ forwardArgs = rest
364
+ }
365
+ } else if (sub && !looksLikeFlag(sub)) {
366
+ // `rnx maestro <flow-or-dir>` — tolerate the shorthand.
367
+ target = path.resolve(cwd, sub)
368
+ forwardArgs = remaining.slice(1)
369
+ } else {
370
+ // no positional — auto-discover.
371
+ target = findDefaultMaestroTarget(cwd)
372
+ forwardArgs = remaining
373
+ }
374
+
375
+ // every flag here is forwarded to each flow, so check the vocabulary once,
376
+ // before anything opens a sim. runFlowPlayback checks again for its
377
+ // programmatic callers; doing it here keeps a directory run from printing
378
+ // the same rejection once per flow after it has already started work.
379
+ const unexpected = findUnexpectedFlowArgs(['<flow>', ...forwardArgs])
380
+ if (unexpected.length > 0) {
381
+ for (const problem of unexpected) {
382
+ console.error(` error: ${problem.message}`)
383
+ }
384
+ console.error('\n run `rnx maestro --help` for the flags this command accepts')
385
+ return 1
386
+ }
387
+
388
+ if (!target) {
389
+ console.error(`
390
+ error: no maestro flows found.
391
+ expected one of: ${MAESTRO_DIRS.map((d) => `./${d}/`).join(', ')}
392
+ or pass a flow explicitly: rnx maestro test path/to/flow.yaml
393
+ or scaffold a starter flow: rnx maestro init
394
+ `)
395
+ return 1
396
+ }
397
+
398
+ if (!fs.existsSync(target)) {
399
+ console.error(` error: ${target} not found`)
400
+ return 1
401
+ }
402
+
403
+ let files: string[]
404
+ try {
405
+ files = collectFlowFiles(target)
406
+ } catch (err) {
407
+ console.error(` error: ${(err as Error).message}`)
408
+ return 1
409
+ }
410
+
411
+ if (files.length === 0) {
412
+ console.error(` error: no *.yaml or *.yml files found in ${target}`)
413
+ return 1
414
+ }
415
+
416
+ console.log(`
417
+ rnx maestro — ${files.length} flow${files.length === 1 ? '' : 's'}
418
+ root: ${path.relative(cwd, target) || '.'}
419
+ `)
420
+
421
+ // registration target for hosted run rows — resolved once per invocation.
422
+ const auth = resolveCliAuth()
423
+ const flagValue = (name: string) => {
424
+ const index = forwardArgs.indexOf(name)
425
+ return index >= 0 ? forwardArgs[index + 1] : undefined
426
+ }
427
+ // only `registerRun` below consumes this, and only for runs that register.
428
+ // resolving it up front probed https://contrast.localhost:3000 and blocked
429
+ // every flow — including ones that upload and register nothing — for the
430
+ // probe's full 2s timeout whenever no local Contrast stack was listening.
431
+ // resolve on first use instead, and keep the one probe per invocation.
432
+ let registerOriginPromise: Promise<string> | null = null
433
+ const registerOrigin = () =>
434
+ (registerOriginPromise ??= resolveDefaultUploadOrigin(
435
+ flagValue('--preview-origin') ?? originFlagValue,
436
+ ))
437
+
438
+ let worstExit = 0
439
+ const results: Array<{ file: string; exit: number }> = []
440
+ for (const file of files) {
441
+ console.log(`\n ▶ ${path.relative(cwd, file)}`)
442
+ const playbackArgs = [file, ...forwardArgs]
443
+ // `--port` is the BRIDGE port everywhere else in the CLI, so forward it as
444
+ // one. sending it on as `--url` made it an app target instead, which left
445
+ // the playback bridge on whichever world resolveBridgeWorld picks by
446
+ // default (a live Vite dev shell selected from the development registry).
447
+ // so `rnx --port <installed daemon> maestro …` silently ran the flow
448
+ // against the dev engine, then failed trying to resolve a bundle at the
449
+ // bridge port. an explicit command-level `--port` still wins, since
450
+ // parseBridgeCliArgs takes the last occurrence.
451
+ if (opts.port) {
452
+ playbackArgs.push('--port', String(opts.port))
453
+ }
454
+ let exit = 0
455
+ const startedAt = Date.now()
456
+ try {
457
+ exit = await runFlowPlayback(playbackArgs)
458
+ } catch (err) {
459
+ // rnxExit is a deliberate exit that already printed its own reason.
460
+ // reporting the sentinel's own text on top of it reads like a second,
461
+ // unrelated failure.
462
+ if (err instanceof RnxExit) {
463
+ exit = err.code || 1
464
+ } else {
465
+ console.error(` x ${(err as Error).message}`)
466
+ exit = 1
467
+ }
468
+ }
469
+ results.push({ file, exit })
470
+ if (exit !== 0 && worstExit === 0) worstExit = exit
471
+
472
+ const upload = getLastFlowPreviewUploadResult()
473
+ if (upload?.shareId || upload?.previewUrl) {
474
+ try {
475
+ fs.writeFileSync(
476
+ path.join(cwd, 'preview-result.json'),
477
+ JSON.stringify({
478
+ previewId: upload.shareId,
479
+ previewUrl: upload.previewUrl,
480
+ }),
481
+ )
482
+ } catch {}
483
+ } else if (args.includes('--preview')) {
484
+ console.error(' error: required hosted recording upload failed')
485
+ if (exit === 0) exit = 1
486
+ if (worstExit === 0) worstExit = 1
487
+ }
488
+
489
+ if (shouldRegisterRun({ uploadedShare: Boolean(upload), auth })) {
490
+ const traceSteps = getLastFlowTraceSteps()
491
+ const failedStep = traceSteps.find((step) => step.status === 'failure')
492
+ const run = await registerRun({
493
+ origin: await registerOrigin(),
494
+ kind: 'maestro',
495
+ name: path.relative(cwd, file),
496
+ status: exit === 0 ? 'passed' : 'failed',
497
+ failureMessage: exit === 0 ? null : (failedStep?.error ?? 'flow playback failed'),
498
+ previewShareId: upload?.shareId ?? null,
499
+ owner: flagValue('--owner') ?? null,
500
+ repo: flagValue('--repo') ?? null,
501
+ durationMs: Date.now() - startedAt,
502
+ ...stepSummaryFromTrace(traceSteps),
503
+ auth,
504
+ })
505
+ if (run) {
506
+ console.log(` run: ${run.id}${run.traceUrl ? ` · replay: ${run.traceUrl}` : ''}`)
507
+ try {
508
+ fs.writeFileSync(
509
+ path.join(cwd, 'test-result.json'),
510
+ JSON.stringify({
511
+ runId: run.id,
512
+ previewUrl: run.previewUrl,
513
+ traceUrl: run.traceUrl,
514
+ }),
515
+ )
516
+ } catch {}
517
+ } else if (process.env.RNX_REQUIRE_REGISTRATION === 'true') {
518
+ console.error(' error: required hosted run registration failed')
519
+ if (exit === 0) exit = 1
520
+ if (worstExit === 0) worstExit = 1
521
+ }
522
+ }
523
+ }
524
+
525
+ if (files.length > 1) {
526
+ console.log(`\n maestro summary:`)
527
+ for (const r of results) {
528
+ const label = r.exit === 0 ? 'pass' : 'fail'
529
+ console.log(` ${label} ${path.relative(cwd, r.file)}`)
530
+ }
531
+ const passed = results.filter((r) => r.exit === 0).length
532
+ console.log(` ${passed}/${results.length} passed`)
533
+ }
534
+
535
+ return worstExit
536
+ }
@@ -0,0 +1,58 @@
1
+ import { rnxExit } from '../run-rnx'
2
+ import { runScreenshotMode } from './screenshot-mode'
3
+ import { runThreeMode } from './three-mode'
4
+
5
+ interface ModeOptions {
6
+ port?: number
7
+ verbose?: boolean
8
+ }
9
+
10
+ const VALUE_FLAGS = new Set(['--sim', '--port', '-p'])
11
+ const SUBCOMMANDS = new Set(['screenshot', 'three'])
12
+
13
+ function findSubcommand(args: string[]): { name: string; index: number } | null {
14
+ for (let i = 0; i < args.length; i++) {
15
+ const arg = args[i]
16
+ if (arg.startsWith('-')) {
17
+ if (VALUE_FLAGS.has(arg)) i++
18
+ continue
19
+ }
20
+ return SUBCOMMANDS.has(arg) ? { name: arg, index: i } : null
21
+ }
22
+ return null
23
+ }
24
+
25
+ function withoutSubcommand(args: string[], index: number): string[] {
26
+ return [...args.slice(0, index), ...args.slice(index + 1)]
27
+ }
28
+
29
+ function printHelp(): void {
30
+ console.log(`
31
+ rnx mode — toggle live display modes
32
+
33
+ usage:
34
+ rnx mode screenshot [on|off|toggle]
35
+ rnx mode three [on|off|toggle]
36
+ rnx mode three configure [options]
37
+ `)
38
+ }
39
+
40
+ export async function runMode(args: string[], opts: ModeOptions): Promise<void> {
41
+ if (args.includes('--help') || args.includes('-h')) {
42
+ printHelp()
43
+ return
44
+ }
45
+
46
+ const sub = findSubcommand(args)
47
+ if (!sub) {
48
+ printHelp()
49
+ rnxExit(1)
50
+ }
51
+
52
+ const rest = withoutSubcommand(args, sub.index)
53
+ if (sub.name === 'screenshot') {
54
+ await runScreenshotMode(rest, opts)
55
+ return
56
+ }
57
+ await runThreeMode(rest, opts)
58
+ }
@@ -0,0 +1,80 @@
1
+ // shared hint printed when the CLI has no bridge or no desktop companion.
2
+
3
+ import { findDesktopCompanion } from '../desktop-companion'
4
+ import type { BridgeSimInfo } from '../ws-bridge'
5
+
6
+ export function printMissingCompanionHint() {
7
+ console.error(' no rnx desktop companion found.')
8
+ console.error('')
9
+ console.error(' install the desktop app:')
10
+ console.error(' rnx desktop install')
11
+ console.error('')
12
+ console.error(' or install the recommended background service:')
13
+ console.error(' rnx daemon install')
14
+ console.error('')
15
+ }
16
+
17
+ export function printMissingBridgeHint(port: number) {
18
+ const companion = findDesktopCompanion()
19
+ console.log(` note: no rnx bridge detected on port ${port}`)
20
+ if (companion) {
21
+ console.log(` launch the installed companion with:`)
22
+ console.log(` rnx desktop`)
23
+ return
24
+ }
25
+ console.log('')
26
+ console.log(' run `rnx daemon install` to enable the faster background service,')
27
+ console.log(' or run `rnx serve` in another shell for a foreground bridge.')
28
+ }
29
+
30
+ export function printMissingSimHint(port: number) {
31
+ console.error('')
32
+ console.error(` no sim is connected to the rnx bridge on port ${port}.`)
33
+ console.error('')
34
+ console.error(' start your app dev server, then open it in rnx:')
35
+ console.error(' npx expo start --localhost --port 8081')
36
+ console.error(' rnx open 8081')
37
+ console.error('')
38
+ console.error(' then inspect the live app:')
39
+ console.error(' rnx describe')
40
+ }
41
+
42
+ // printed when a command targeted a specific `--sim <id>` that the bridge
43
+ // does not know — distinct from printMissingSimHint, which assumes the bridge
44
+ // has no sims at all. when other sims *are* connected, "no sim is connected"
45
+ // is a flat-out lie; list the valid ids instead so the user can pick one.
46
+ export async function printUnknownSimHint(
47
+ bridge: { listSims: () => Promise<BridgeSimInfo[]> },
48
+ port: number,
49
+ requestedId: string,
50
+ ) {
51
+ let sims: BridgeSimInfo[] = []
52
+ try {
53
+ sims = await bridge.listSims()
54
+ } catch {
55
+ // bridge unreachable while reporting — fall through to the generic hint
56
+ }
57
+ console.error('')
58
+ if (sims.length === 0) {
59
+ printMissingSimHint(port)
60
+ return
61
+ }
62
+ console.error(
63
+ ` no sim with id "${requestedId}" is connected to the bridge on port ${port}.`,
64
+ )
65
+ console.error('')
66
+ console.error(' connected sims:')
67
+ for (const sim of sims) {
68
+ const flags = [
69
+ sim.isPrimary ? 'primary' : null,
70
+ sim.readyState,
71
+ sim.userVisible === false ? 'hidden' : sim.userVisible === true ? 'visible' : null,
72
+ sim.userFocused ? 'focused' : null,
73
+ ]
74
+ .filter(Boolean)
75
+ .join(', ')
76
+ console.error(` ${sim.id} (${flags})`)
77
+ }
78
+ console.error('')
79
+ console.error(' pass a valid --sim id, or run `rnx list` to see all sessions.')
80
+ }