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,209 @@
1
+ // playwright driver — launches Playwright's Google Chrome for Testing via the
2
+ // `playwright` package. availability depends on whether the package is
3
+ // resolvable from the current working directory OR from the CLI's own
4
+ // install tree (repo-local install is typical; the latter covers a global
5
+ // CLI invoked from a directory with no node_modules).
6
+ //
7
+ // the browser runs in a *detached* child process — this CLI re-invoked as
8
+ // its own `__internal playwright-host` (see cli/internal-child.ts) — rather
9
+ // than in this CLI process. that is load-bearing: `rnx open` returns the
10
+ // moment the sim connects, and playwright tears its browser down when the
11
+ // launching process exits. an in-process launch would mean the sim vanished
12
+ // the instant `open` finished. the detached host keeps the test browser alive
13
+ // until the sim's page closes (e.g. via `rnx close`).
14
+
15
+ import { spawn } from 'child_process'
16
+ import { closeSync, mkdtempSync, openSync, readFileSync } from 'fs'
17
+ import { tmpdir } from 'os'
18
+ import { join } from 'path'
19
+ import { playwrightHostLogName } from '../../src/playwright-host-log'
20
+ import { ensureProfile, playwrightProfileUserDataDir } from '../../src/profiles'
21
+ import { RNX_INTERNAL_CHILDREN, RNX_INTERNAL_COMMAND } from '../internal-child'
22
+ import { getProcessComm } from '../parent-pid'
23
+ import { rnxSelfInvocation } from '../self-invocation'
24
+ import { ensurePlaywrightBrowser, resolvePlaywright } from './playwright-provisioning'
25
+ import type {
26
+ Driver,
27
+ DriverAvailability,
28
+ DriverLaunchOptions,
29
+ DriverLaunchResult,
30
+ } from './types'
31
+
32
+ function probe(): DriverAvailability {
33
+ const resolved = resolvePlaywright()
34
+ if (!resolved) {
35
+ return {
36
+ available: false,
37
+ reason: 'playwright not installed in the current workspace',
38
+ }
39
+ }
40
+ return {
41
+ available: true,
42
+ reason: null,
43
+ detail: `resolved via ${resolved.spec}@${resolved.version}`,
44
+ }
45
+ }
46
+
47
+ // when a headed launch fails on a Linux box with no display, chrome's own
48
+ // error ("Missing X server", "cannot open display") tells the user *what*
49
+ // is wrong but not *what to do*. surface the actionable flag.
50
+ function headlessHintForLinuxFailure(
51
+ opts: DriverLaunchOptions,
52
+ detail: string,
53
+ ): string | null {
54
+ if (process.platform !== 'linux') return null
55
+ if (opts.headless) return null
56
+ if (process.env.DISPLAY || process.env.WAYLAND_DISPLAY) return null
57
+ if (!/missing x server|cannot open display|x server|x11/i.test(detail)) {
58
+ return null
59
+ }
60
+ return ' hint: linux without DISPLAY/WAYLAND_DISPLAY — rerun without --headed (or add --headless) to run playwright headless.'
61
+ }
62
+
63
+ // resolve the Chrome user-data dir for a profiled launch. a non-empty
64
+ // return means the host should use a persistent context; '' means a plain
65
+ // (in-memory) browser launch.
66
+ function resolveUserDataDir(opts: DriverLaunchOptions): string {
67
+ if (opts.profileId) {
68
+ return playwrightProfileUserDataDir(ensureProfile(opts.profileId).id)
69
+ }
70
+ if (opts.ephemeralProfile) {
71
+ return mkdtempSync(join(tmpdir(), 'rnx-playwright-profile-'))
72
+ }
73
+ return ''
74
+ }
75
+
76
+ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
77
+ const resolved = resolvePlaywright()
78
+ if (!resolved) {
79
+ return {
80
+ launched: false,
81
+ message: 'playwright not installed — run `bun add -D playwright` first',
82
+ }
83
+ }
84
+ if (!opts.url) {
85
+ return { launched: false, message: 'playwright driver requires a target url' }
86
+ }
87
+ const ownerPid = opts.ownerPid
88
+ if (ownerPid === undefined || !Number.isInteger(ownerPid) || ownerPid <= 1) {
89
+ return {
90
+ launched: false,
91
+ message: 'playwright driver requires a valid owning session process',
92
+ }
93
+ }
94
+
95
+ // keep browser discovery, installation, and the detached host on one exact
96
+ // cache contract even if another caller mutates process.env during launch.
97
+ const playwrightEnv = { ...process.env }
98
+ const provisioning = ensurePlaywrightBrowser(resolved, playwrightEnv)
99
+ if (!provisioning.ok) {
100
+ return {
101
+ launched: false,
102
+ message: `playwright browser provisioning failed: ${provisioning.message}`,
103
+ }
104
+ }
105
+
106
+ const errLog = join(tmpdir(), playwrightHostLogName(Date.now(), process.pid))
107
+ const connectAckFile = `${errLog}.connected`
108
+ const errFd = openSync(errLog, 'a')
109
+ try {
110
+ // spawn the browser host detached so Chrome outlives `rnx
111
+ // open`. stdout is dropped; stderr is captured to errLog so an early
112
+ // crash (bad profile dir, missing browser binary) produces a real
113
+ // message instead of a silent "timed out waiting for sim".
114
+ // resolve the owning session's pid *before* we detach — after the spawn,
115
+ // the host's own ancestry is just launchd. the host polls this pid and
116
+ // tears Chrome down when the session that launched it exits.
117
+ const self = rnxSelfInvocation()
118
+ const child = spawn(
119
+ self.executable,
120
+ [...self.prefixArgs, RNX_INTERNAL_COMMAND, RNX_INTERNAL_CHILDREN.playwrightHost],
121
+ {
122
+ detached: true,
123
+ stdio: ['ignore', 'ignore', errFd],
124
+ env: {
125
+ ...playwrightEnv,
126
+ SOOTSIM_PW_MODULE: resolved.modulePath,
127
+ SOOTSIM_PW_URL: opts.url,
128
+ SOOTSIM_PW_BRIDGE_PORT: opts.bridgePort ? String(opts.bridgePort) : '',
129
+ SOOTSIM_PW_HEADLESS: (opts.headless ?? true) ? '1' : '0',
130
+ SOOTSIM_PW_USERDATADIR: resolveUserDataDir(opts),
131
+ SOOTSIM_PW_CDP_PORT: opts.cdpPort ? String(opts.cdpPort) : '',
132
+ // when the caller passed no viewport, leave the inherited
133
+ // SOOTSIM_PW_VIEWPORT (from playwrightEnv) alone instead of
134
+ // clobbering it with '' — the driver reads it as its own default.
135
+ ...(opts.viewport
136
+ ? { SOOTSIM_PW_VIEWPORT: `${opts.viewport.width}x${opts.viewport.height}` }
137
+ : {}),
138
+ SOOTSIM_PW_CONNECTED_ACK_FILE: connectAckFile,
139
+ SOOTSIM_PW_CONNECT_TIMEOUT_MS: String(opts.connectTimeoutMs ?? 120_000),
140
+ SOOTSIM_PW_OWNER_PID: String(ownerPid),
141
+ },
142
+ },
143
+ )
144
+ child.unref()
145
+
146
+ // give the host a short window to fail fast (resolve errors, browser
147
+ // binary missing). if it is still alive after that, the sim is on its
148
+ // way and the caller's own `waitForSimMatch` takes over.
149
+ const earlyExit = await new Promise<number | null>((resolve) => {
150
+ const timer = setTimeout(() => resolve(null), 4000)
151
+ child.once('exit', (code) => {
152
+ clearTimeout(timer)
153
+ resolve(code ?? 0)
154
+ })
155
+ })
156
+ if (earlyExit !== null) {
157
+ // ANY exit inside the 4s startup window is a failure: a healthy host
158
+ // stays alive until the sim disconnects (much longer than 4s). a code-0
159
+ // early exit can happen when the inline script's top-level catch fires
160
+ // before the launch even reaches a thrown error path, or when chromium
161
+ // is killed externally — both still mean "no sim is coming". emit the
162
+ // host's FULL stderr — its first line is the error message, and the
163
+ // prior `slice(-3)` kept only trailing stack frames, dropping the
164
+ // message and making CI failures undiagnosable. leave errLog on disk
165
+ // (path included) so it can be inspected after the fact.
166
+ const detail = readFileSync(errLog, 'utf8').trim()
167
+ const hint = headlessHintForLinuxFailure(opts, detail)
168
+ return {
169
+ launched: false,
170
+ message: `playwright host exited early (code ${earlyExit}) — host log ${errLog}${
171
+ detail ? `:\n${detail}` : ''
172
+ }${hint ? `\n${hint}` : ''}`,
173
+ }
174
+ }
175
+ // name the lifetime tie: the host watchdog closes chrome ~5s after the
176
+ // owning session pid exits, which surprises one-shot/detached shells
177
+ // (their topmost non-init ancestor is the shell itself, so the sim dies
178
+ // the moment the command finishes).
179
+ const ownerComm = getProcessComm(ownerPid)
180
+ const ownerNote = ` — browser closes when pid ${ownerPid}${ownerComm ? ` (${ownerComm})` : ''} exits`
181
+ return {
182
+ launched: true,
183
+ message: `playwright chrome launched → ${opts.url}${ownerNote}`,
184
+ pid: child.pid,
185
+ target: resolved.spec,
186
+ attachUrl: opts.url,
187
+ connectAckFile,
188
+ diagnosticLogPath: errLog,
189
+ }
190
+ } catch (err) {
191
+ return {
192
+ launched: false,
193
+ message: `playwright launch failed: ${err instanceof Error ? err.message : String(err)}`,
194
+ }
195
+ } finally {
196
+ // parent's copy of the fd; the detached host keeps its own inherited
197
+ // dup, so closing here does not cut off the child's stderr.
198
+ closeSync(errFd)
199
+ }
200
+ }
201
+
202
+ export const playwrightDriver: Driver = {
203
+ id: 'playwright',
204
+ name: 'playwright',
205
+ description: 'programmatic chromium via the playwright package — headless default',
206
+ kind: 'automation',
207
+ availability: probe,
208
+ launch,
209
+ }
@@ -0,0 +1,65 @@
1
+ // driver registry — hardcoded list of known drivers plus lookup helpers.
2
+ // kept intentionally flat: no DI, no plugin discovery, no lazy imports. both
3
+ // built-in drivers load unconditionally because their module bodies are cheap
4
+ // (no I/O, no side effects).
5
+
6
+ import { electronDriver } from './electron'
7
+ import { playwrightDriver } from './playwright'
8
+ import type { Driver, DriverId } from './types'
9
+
10
+ export const ALL_DRIVERS: readonly Driver[] = [electronDriver, playwrightDriver]
11
+
12
+ export function getAllDrivers(): readonly Driver[] {
13
+ return ALL_DRIVERS
14
+ }
15
+
16
+ export function getDriver(id: DriverId): Driver | null {
17
+ return ALL_DRIVERS.find((d) => d.id === id) ?? null
18
+ }
19
+
20
+ // returns the first driver in preference order that reports availability.
21
+ // used by launch sites that want "pick whatever works" behavior when the
22
+ // user didn't pass an explicit --driver flag. if `forcedId` is given, that
23
+ // driver is returned even if unavailable so the caller can surface a
24
+ // specific error message rather than silently falling back.
25
+ export function resolveDriver(
26
+ forcedId: string | null | undefined,
27
+ preference: readonly DriverId[],
28
+ ): Driver | null {
29
+ if (forcedId) {
30
+ const match = ALL_DRIVERS.find((d) => d.id === forcedId)
31
+ return match ?? null
32
+ }
33
+ for (const id of preference) {
34
+ const driver = ALL_DRIVERS.find((d) => d.id === id)
35
+ if (driver && driver.availability().available) return driver
36
+ }
37
+ return null
38
+ }
39
+
40
+ // utility for the `rnx list --drivers` view. returns a rich row per
41
+ // driver with availability + kind + description, suitable for a table.
42
+ export interface DriverListRow {
43
+ id: DriverId
44
+ name: string
45
+ kind: Driver['kind']
46
+ description: string
47
+ available: boolean
48
+ reason: string | null
49
+ detail: string | null
50
+ }
51
+
52
+ export function buildDriverListRows(): DriverListRow[] {
53
+ return ALL_DRIVERS.map((d) => {
54
+ const probe = d.availability()
55
+ return {
56
+ id: d.id,
57
+ name: d.name,
58
+ kind: d.kind,
59
+ description: d.description,
60
+ available: probe.available,
61
+ reason: probe.reason,
62
+ detail: probe.detail ?? null,
63
+ }
64
+ })
65
+ }
@@ -0,0 +1,102 @@
1
+ // driver / runner abstraction for rnx. browser sims run in an isolated
2
+ // playwright-owned profile. electron remains the native desktop surface.
3
+
4
+ export type DriverId = 'electron' | 'playwright'
5
+
6
+ // broad category for grouping / filtering. "native" is the electron host;
7
+ // "automation" is the isolated playwright browser.
8
+ export type DriverKind = 'native' | 'automation'
9
+
10
+ export interface DriverAvailability {
11
+ // true when the driver can attempt a launch on this machine. a driver is
12
+ // allowed to be available but still fail at runtime (network issues,
13
+ // permission denied, ...). false when there's a hard precondition missing
14
+ // (binary not installed, package not resolvable).
15
+ available: boolean
16
+ // short free-text explaining why a driver is unavailable. null when
17
+ // `available` is true, or when no reason is known.
18
+ reason: string | null
19
+ // optional human-readable detail shown in verbose listings — e.g. the
20
+ // resolved binary path, version string, or install location.
21
+ detail?: string
22
+ }
23
+
24
+ export interface DriverLaunchOptions {
25
+ // shell URL to load in the launched process. optional because the electron
26
+ // driver can launch the companion app with
27
+ // no URL (it picks its last window). other drivers return launched:false
28
+ // with a helpful message when url is missing.
29
+ url?: string
30
+ // run in headless mode where the driver supports it. ignored by electron.
31
+ headless?: boolean
32
+ // initial device model seed forwarded to the rnx shell. the electron
33
+ // driver forwards it via argv (--device=<model>); browser drivers receive
34
+ // it already stamped on the URL (?device=<model>) by runOpenCommand.
35
+ device?: string
36
+ // page viewport for browser-based drivers. runOpenCommand sizes this from
37
+ // the device profile (device pt size + frame chrome) so the sim isn't
38
+ // letterboxed inside playwright's 1280x720 default.
39
+ viewport?: { width: number; height: number }
40
+ // let the caller override the port used in availability / attach
41
+ // heuristics. most drivers don't need this.
42
+ port?: number
43
+ // when true, the launch is detached from the parent process and the
44
+ // CLI returns immediately. default true.
45
+ detached?: boolean
46
+ // named persistent storage profile. honored by drivers that own their
47
+ // browser context (electron, playwright). plain browser tabs reject it.
48
+ profileId?: string
49
+ // unnamed temporary storage. honored by electron/playwright.
50
+ ephemeralProfile?: boolean
51
+ // stable pid for the session that owns a CLI-opened host. electron closes
52
+ // only that session's windows when the owner exits; playwright requires it
53
+ // so a detached browser can never outlive its session. omitted only for the
54
+ // persistent `rnx desktop` app lifecycle.
55
+ ownerPid?: number
56
+ // expose Chrome's remote-debugging endpoint on this port so a
57
+ // bridge-driven sim can also be cpu-profiled. only the playwright driver
58
+ // honors it (launches Chromium with --remote-debugging-port).
59
+ cdpPort?: number
60
+ // maximum time the detached host should wait for the launched sim to
61
+ // register with the bridge before it tears itself down.
62
+ connectTimeoutMs?: number
63
+ // the rnx WS bridge port the launched sim page should register with,
64
+ // injected as window.__sootsimBridgePort. load-bearing when the dev bridge
65
+ // drifted off the shell-port-implied default (e.g. :7668 taken at the shell's
66
+ // boot -> bridge on :7669 while the shell kept :5173): the page's own
67
+ // shell-port heuristic would compute the stale :7668 and never connect.
68
+ bridgePort?: number
69
+ }
70
+
71
+ export interface DriverLaunchResult {
72
+ // true when the process was successfully spawned.
73
+ launched: boolean
74
+ // free-text describing what happened, used for CLI output and skill docs.
75
+ message: string
76
+ // optional handle details — currently free-shaped so each driver can
77
+ // expose what makes sense for its environment. pid for spawned processes,
78
+ // `target` for an install path, `attachUrl` for automation drivers.
79
+ pid?: number
80
+ target?: string
81
+ attachUrl?: string
82
+ connectAckFile?: string
83
+ diagnosticLogPath?: string
84
+ }
85
+
86
+ export interface Driver {
87
+ id: DriverId
88
+ // short human name shown in `rnx list --drivers`.
89
+ name: string
90
+ // one-line description. keep under 80 chars so the list command reads
91
+ // nicely at default terminal width.
92
+ description: string
93
+ kind: DriverKind
94
+ // probe the host environment for this driver. cheap and synchronous —
95
+ // anything expensive belongs in `launch`.
96
+ availability(): DriverAvailability
97
+ // start the driver pointed at `opts.url`. may be async because some
98
+ // drivers (electron, playwright) do real I/O. must never throw on a
99
+ // benign "not installed" condition — return { launched: false } with a
100
+ // helpful message instead.
101
+ launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult>
102
+ }
@@ -0,0 +1,55 @@
1
+ // turning a recorded flow draft into a maestro-compatible YAML file.
2
+ //
3
+ // this is separate from flow-session.ts, the draft store, because the two have
4
+ // different reach. every `do` verb writes a candidate into the store, so the
5
+ // store has to load wherever the CLI's runtime commands load, including inside
6
+ // a box shell running in workerd, where the `yaml` package does not load at
7
+ // all. YAML formatting and writing the file are the export step, and only
8
+ // `rnx flow` and `rnx maestro` ever reach it.
9
+
10
+ import { mkdirSync, writeFileSync } from 'node:fs'
11
+ import { dirname, resolve } from 'node:path'
12
+ import yaml from 'yaml'
13
+ import { validateFlowSteps } from './flow-file'
14
+ import { finalizeFlowSession, readFlowSession } from './flow-session'
15
+
16
+ export function exportFlowSession(outputPath?: string) {
17
+ const state = readFlowSession()
18
+ if (!state) return { active: false as const }
19
+
20
+ const yamlText = `${yaml.stringify(state.steps).trimEnd()}\n`
21
+ const issues = validateFlowSteps(state.steps)
22
+ if (issues.length > 0) {
23
+ return {
24
+ active: true as const,
25
+ valid: false as const,
26
+ issues,
27
+ yaml: yamlText,
28
+ stepCount: state.steps.length,
29
+ outputPath: null,
30
+ }
31
+ }
32
+
33
+ let resolvedOutputPath: string | null = null
34
+ if (outputPath) {
35
+ resolvedOutputPath = resolve(outputPath)
36
+ mkdirSync(dirname(resolvedOutputPath), { recursive: true })
37
+ writeFileSync(resolvedOutputPath, yamlText)
38
+ }
39
+
40
+ return {
41
+ active: true as const,
42
+ valid: true as const,
43
+ issues: [],
44
+ yaml: yamlText,
45
+ stepCount: state.steps.length,
46
+ outputPath: resolvedOutputPath,
47
+ }
48
+ }
49
+
50
+ export function endFlowSession(outputPath?: string) {
51
+ const result = exportFlowSession(outputPath)
52
+ if (!result.active) return result
53
+ finalizeFlowSession()
54
+ return result
55
+ }
@@ -0,0 +1,142 @@
1
+ import * as fs from 'fs'
2
+ import yaml from 'yaml'
3
+
4
+ export interface FlowFrontmatter {
5
+ // raw `rnx open` target: port, dev-server URL, bundle URL, or shell URL
6
+ app?: string | number
7
+ device?: string
8
+ // pin the sim color scheme for this flow run (screenshot baselines are
9
+ // theme-sensitive; a dark capture can never match a light baseline)
10
+ theme?: 'light' | 'dark'
11
+ electron?: boolean
12
+ // maestro-compatible: appId is accepted but not used by rnx (ios bundle id)
13
+ appId?: string
14
+ // maestro-compatible: flow-level env vars, defined into the flow's JS
15
+ // context before the first step runs
16
+ env?: Record<string, string>
17
+ // maestro-compatible: workspace tag filtering (config.yml includeTags/excludeTags)
18
+ tags?: string[]
19
+ }
20
+
21
+ // parse flow file. accepts three shapes:
22
+ // 1. plain yaml array of steps (no frontmatter)
23
+ // 2. rnx style: ---\n<fm>\n---\n<steps>
24
+ // 3. maestro style: <fm>\n---\n<steps> (multi-doc yaml, fm first)
25
+ // maestro compat matters because real users have real .maestro/*.yaml files
26
+ // and shouldn't have to rewrite them to drive rnx.
27
+ //
28
+ // `${...}` templates are NOT resolved here — interpolation happens per-step
29
+ // at execution time in the flow runner's JS context (matching maestro), so
30
+ // values produced mid-flow (runScript output.*, copyTextFrom) resolve in
31
+ // later steps and a missing variable fails loudly at the exact step.
32
+ export function parseFlowFile(content: string): {
33
+ frontmatter: FlowFrontmatter
34
+ steps: any[]
35
+ } {
36
+ let frontmatter: FlowFrontmatter = {}
37
+ let body = content
38
+ const normalizeSteps = (steps: unknown): any[] => {
39
+ return Array.isArray(steps) ? steps : []
40
+ }
41
+
42
+ // shape 2: leading ---\n<fm>\n---\n<body>
43
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?/)
44
+ if (fmMatch) {
45
+ try {
46
+ frontmatter = (yaml.parse(fmMatch[1]) as FlowFrontmatter) ?? {}
47
+ } catch (e) {
48
+ console.warn(` warn: could not parse frontmatter: ${(e as Error).message}`)
49
+ }
50
+ body = content.slice(fmMatch[0].length)
51
+ const steps = yaml.parse(body)
52
+ return { frontmatter, steps: normalizeSteps(steps) }
53
+ }
54
+
55
+ // shape 3: maestro multi-doc (fm-first, then `---`, then steps)
56
+ // detect cheaply: a `---` separator on its own line not at position 0
57
+ if (/\n---\s*\n/.test(content)) {
58
+ try {
59
+ const docs = yaml.parseAllDocuments(content)
60
+ if (docs.length >= 2) {
61
+ const fm = docs[0].toJS() as FlowFrontmatter | null
62
+ const st = docs[docs.length - 1].toJS()
63
+ return {
64
+ frontmatter: fm && typeof fm === 'object' ? fm : {},
65
+ steps: normalizeSteps(st),
66
+ }
67
+ }
68
+ } catch (e) {
69
+ console.warn(` warn: could not parse multi-doc flow: ${(e as Error).message}`)
70
+ }
71
+ }
72
+
73
+ // shape 1: plain steps array
74
+ const steps = yaml.parse(body)
75
+ return { frontmatter, steps: normalizeSteps(steps) }
76
+ }
77
+
78
+ const INTERACTION_KEYS = new Set([
79
+ 'tapOn',
80
+ 'inputText',
81
+ 'pressKey',
82
+ 'dispatchKey',
83
+ 'hideKeyboard',
84
+ 'swipe',
85
+ 'pinch',
86
+ 'scroll',
87
+ 'scrollUntilVisible',
88
+ ])
89
+
90
+ export function validateFlowSteps(steps: any[]): string[] {
91
+ const issues: string[] = []
92
+ if (!Array.isArray(steps) || steps.length === 0) {
93
+ issues.push('flow body must be a non-empty YAML array of steps')
94
+ return issues
95
+ }
96
+
97
+ let interactions = 0
98
+ for (const step of steps) {
99
+ if (step && typeof step === 'object') {
100
+ const keys = Object.keys(step)
101
+ if (keys.some((key) => INTERACTION_KEYS.has(key))) interactions += 1
102
+ } else if (typeof step === 'string' && INTERACTION_KEYS.has(step)) {
103
+ interactions += 1
104
+ }
105
+ }
106
+
107
+ if (interactions === 0) {
108
+ issues.push(
109
+ 'no interaction steps found (expected at least one tapOn / inputText / pressKey / swipe / scroll)',
110
+ )
111
+ }
112
+
113
+ return issues
114
+ }
115
+
116
+ // structural-only validator used by CI runners before uploading the flow
117
+ // artifact. we only check: file exists, parseable yaml, has at least one
118
+ // real interaction step. playback validation is a separate concern handled
119
+ // by `rnx maestro end --validate` during the authoring session.
120
+ export function validateFlowFile(flowPath: string): string[] {
121
+ if (!fs.existsSync(flowPath)) {
122
+ return [`file not found: ${flowPath}`]
123
+ }
124
+
125
+ let content: string
126
+ try {
127
+ content = fs.readFileSync(flowPath, 'utf8')
128
+ } catch (e) {
129
+ return [`cannot read file: ${(e as Error).message}`]
130
+ }
131
+
132
+ if (content.trim().length === 0) {
133
+ return ['file is empty']
134
+ }
135
+
136
+ try {
137
+ const parsed = parseFlowFile(content)
138
+ return validateFlowSteps(parsed.steps)
139
+ } catch (e) {
140
+ return [`yaml parse error: ${(e as Error).message}`]
141
+ }
142
+ }