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,535 @@
1
+ // rnx test — choose Maestro or Detox, support local bundle loopback and standalone cloud execution.
2
+
3
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
4
+ import { createServer, type Server } from 'node:http'
5
+ import { basename, dirname, extname, join, resolve } from 'node:path'
6
+ import {
7
+ RNX_CLOUD_TEST_FILES_MAX_BYTES,
8
+ RNX_CLOUD_TEST_FILES_MAX_COUNT,
9
+ } from '../../src/cloud-contract'
10
+ import { authHeaderValue, resolveCliAuth } from '../auth'
11
+ import { buildShellUrl, resolveShellBaseUrlForBridgePort } from './control'
12
+ import { hoistLeadingSimFlag } from './flow'
13
+ import { resolveDefaultUploadOrigin } from './upload'
14
+
15
+ export interface RunTestOptions {
16
+ port?: number
17
+ verbose?: boolean
18
+ pollIntervalMs?: number
19
+ timeoutMs?: number
20
+ }
21
+
22
+ interface TestAppTarget {
23
+ target: string
24
+ close: () => Promise<void>
25
+ }
26
+
27
+ const VALUE_FLAGS = new Set([
28
+ '--app',
29
+ '--bundle',
30
+ '--bundle-url',
31
+ '--config',
32
+ '--sim',
33
+ '--session',
34
+ '--tab',
35
+ '--port',
36
+ '--device',
37
+ '--driver',
38
+ '--base-url',
39
+ '--url',
40
+ '-t',
41
+ '--testNamePattern',
42
+ '--grep',
43
+ '--maxWorkers',
44
+ '--shard',
45
+ '--outputFile',
46
+ '--origin',
47
+ '--owner',
48
+ '--repo',
49
+ '--poll-interval',
50
+ '--timeout',
51
+ ])
52
+
53
+ function valueAfter(args: string[], name: string): string | null {
54
+ const inline = args.find((arg) => arg.startsWith(`${name}=`))
55
+ if (inline) return inline.slice(name.length + 1) || null
56
+ const index = args.indexOf(name)
57
+ if (index < 0) return null
58
+ const value = args[index + 1]
59
+ return value && !value.startsWith('-') ? value : null
60
+ }
61
+
62
+ function targetFromArgs(args: string[]): string | null {
63
+ for (let index = 0; index < args.length; index++) {
64
+ const arg = args[index]
65
+ if (VALUE_FLAGS.has(arg)) {
66
+ index += 1
67
+ continue
68
+ }
69
+ if (arg.startsWith('--') && arg.includes('=')) {
70
+ continue
71
+ }
72
+ if (!arg.startsWith('-')) return arg
73
+ }
74
+ return null
75
+ }
76
+
77
+ function withoutApp(args: string[]): string[] {
78
+ const result: string[] = []
79
+ for (let i = 0; i < args.length; i++) {
80
+ const arg = args[i]
81
+ if (arg === '--app') {
82
+ i += 1
83
+ continue
84
+ }
85
+ if (arg.startsWith('--app=')) {
86
+ continue
87
+ }
88
+ result.push(arg)
89
+ }
90
+ return result
91
+ }
92
+
93
+ function closeServer(server: Server): Promise<void> {
94
+ return new Promise((resolveClose, rejectClose) => {
95
+ server.close((error) => {
96
+ if (error) rejectClose(error)
97
+ else resolveClose()
98
+ })
99
+ })
100
+ }
101
+
102
+ export async function servePrebuiltBundle(path: string): Promise<TestAppTarget> {
103
+ const file = resolve(process.cwd(), path)
104
+ const stat = statSync(file)
105
+ if (!stat.isFile() || extname(file) !== '.js') {
106
+ throw new Error('--app must name a regular Metro .js bundle or a packager target')
107
+ }
108
+ const bundle = readFileSync(file)
109
+ if (bundle.length === 0) throw new Error('--app bundle is empty')
110
+
111
+ const server = createServer((request, response) => {
112
+ if (new URL(request.url || '/', 'http://127.0.0.1').pathname !== '/bundle.js') {
113
+ response.writeHead(404).end()
114
+ return
115
+ }
116
+ response.writeHead(200, {
117
+ 'access-control-allow-origin': '*',
118
+ 'cache-control': 'no-store',
119
+ 'content-type': 'application/javascript; charset=utf-8',
120
+ 'content-length': String(bundle.length),
121
+ })
122
+ response.end(bundle)
123
+ })
124
+ await new Promise<void>((resolveListen, rejectListen) => {
125
+ server.once('error', rejectListen)
126
+ server.listen(0, '127.0.0.1', () => {
127
+ server.off('error', rejectListen)
128
+ resolveListen()
129
+ })
130
+ })
131
+ const address = server.address()
132
+ if (!address || typeof address === 'string') {
133
+ await closeServer(server)
134
+ throw new Error('could not start the local prebuilt-bundle server')
135
+ }
136
+ return {
137
+ target: `http://127.0.0.1:${address.port}/bundle.js`,
138
+ close: () => closeServer(server),
139
+ }
140
+ }
141
+
142
+ export async function resolveTestAppTarget(app: string): Promise<TestAppTarget> {
143
+ const localPath = resolve(process.cwd(), app)
144
+ if (existsSync(localPath)) return servePrebuiltBundle(localPath)
145
+ return { target: app, close: async () => {} }
146
+ }
147
+
148
+ function restoreRnxUrl(previous: string | undefined): void {
149
+ if (previous === undefined) {
150
+ delete process.env.RNX_URL
151
+ } else {
152
+ process.env.RNX_URL = previous
153
+ }
154
+ }
155
+
156
+ // dispatch only: each interpreter retains its own selection and execution rules.
157
+ export function resolveTestCommand(args: string[], cwd = process.cwd()) {
158
+ const forwarded = hoistLeadingSimFlag(args)
159
+ const target = forwarded[0]?.startsWith('-') ? undefined : forwarded[0]
160
+ const config = forwarded.some(
161
+ (arg) => arg === '--config' || arg.startsWith('--config='),
162
+ )
163
+ if (config) return { command: 'detox', args: forwarded }
164
+ if (!target) {
165
+ throw new Error(
166
+ 'pass a Maestro flow/directory, a Jest suite, or --config <jest-config>',
167
+ )
168
+ }
169
+ const absolute = resolve(cwd, target)
170
+ if (!existsSync(absolute)) throw new Error(`test target not found: ${target}`)
171
+ const directory = statSync(absolute).isDirectory()
172
+ if (
173
+ !directory &&
174
+ /^(?:jest|rnx-detox)\.config\.[cm]?[jt]s$|^jest\.config\.json$/.test(
175
+ basename(absolute),
176
+ )
177
+ ) {
178
+ return { command: 'detox', args: ['--config', target, ...forwarded.slice(1)] }
179
+ }
180
+ const maestro = directory
181
+ ? readdirSync(absolute).some((name) => /\.ya?ml$/.test(name)) ||
182
+ ['.maestro', 'maestro'].includes(basename(absolute))
183
+ : /\.ya?ml$/.test(extname(absolute))
184
+ if (maestro) {
185
+ return {
186
+ command: 'maestro',
187
+ args: [
188
+ 'test',
189
+ ...forwarded.map((arg, index) => {
190
+ if (index === 0 && !directory && /^config\.ya?ml$/.test(basename(absolute)))
191
+ return dirname(absolute)
192
+ return arg === '--app' || arg.startsWith('--app=')
193
+ ? arg.replace('--app', '--url')
194
+ : arg
195
+ }),
196
+ ],
197
+ }
198
+ }
199
+ if (!directory && !/\.[cm]?[jt]sx?$/.test(extname(absolute))) {
200
+ throw new Error(`expected Maestro YAML or a Detox/Jest suite: ${target}`)
201
+ }
202
+ return { command: 'detox', args: forwarded }
203
+ }
204
+
205
+ function collectTestFiles(
206
+ targetPath: string,
207
+ ignoredPath: string | null,
208
+ ): Record<string, string> {
209
+ const files: Record<string, string> = {}
210
+ let sourceBytes = 0
211
+ const add = (path: string, relativePath: string) => {
212
+ if (Object.keys(files).length >= RNX_CLOUD_TEST_FILES_MAX_COUNT) {
213
+ throw new Error(
214
+ `cloud test suites may contain at most ${RNX_CLOUD_TEST_FILES_MAX_COUNT} files`,
215
+ )
216
+ }
217
+ sourceBytes += statSync(path).size
218
+ if (sourceBytes > RNX_CLOUD_TEST_FILES_MAX_BYTES) {
219
+ throw new Error(
220
+ `cloud test files may contain at most ${RNX_CLOUD_TEST_FILES_MAX_BYTES} bytes`,
221
+ )
222
+ }
223
+ files[relativePath] = readFileSync(path, 'utf8')
224
+ }
225
+ const stat = statSync(targetPath)
226
+ if (stat.isDirectory()) {
227
+ function walk(dir: string, base: string) {
228
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
229
+ if (entry.name.startsWith('.') && entry.name !== '.maestro') continue
230
+ if (['node_modules', 'dist', 'build', '.git'].includes(entry.name)) continue
231
+ const full = join(dir, entry.name)
232
+ if (ignoredPath && resolve(full) === ignoredPath) continue
233
+ const rel = base ? `${base}/${entry.name}` : entry.name
234
+ if (entry.isDirectory()) {
235
+ walk(full, rel)
236
+ } else if (entry.isFile()) {
237
+ add(full, rel)
238
+ }
239
+ }
240
+ }
241
+ walk(targetPath, '')
242
+ } else if (stat.isFile()) {
243
+ add(targetPath, basename(targetPath))
244
+ const parent = dirname(targetPath)
245
+ for (const configName of [
246
+ 'rnx-detox.config.cjs',
247
+ 'jest.config.cjs',
248
+ 'jest.config.js',
249
+ ]) {
250
+ const configPath = join(parent, configName)
251
+ if (existsSync(configPath) && !files[configName]) {
252
+ add(configPath, configName)
253
+ }
254
+ }
255
+ }
256
+ const serializedBytes = Buffer.byteLength(JSON.stringify(files))
257
+ if (serializedBytes > RNX_CLOUD_TEST_FILES_MAX_BYTES) {
258
+ throw new Error(
259
+ `cloud test files serialize to ${serializedBytes} bytes; the limit is ${RNX_CLOUD_TEST_FILES_MAX_BYTES}`,
260
+ )
261
+ }
262
+ return files
263
+ }
264
+
265
+ export async function runCloudTest(
266
+ args: string[],
267
+ opts: RunTestOptions = {},
268
+ ): Promise<number> {
269
+ const forwarded = args.filter((a) => a !== '--cloud' && a !== '--hosted')
270
+ const originFlag = valueAfter(forwarded, '--origin')
271
+ const origin = await resolveDefaultUploadOrigin(originFlag ?? undefined)
272
+ const app =
273
+ valueAfter(forwarded, '--app') ||
274
+ valueAfter(forwarded, '--bundle') ||
275
+ valueAfter(forwarded, '--bundle-url') ||
276
+ valueAfter(forwarded, '--url') ||
277
+ forwarded.find((a) => a.startsWith('--app='))?.split('=')[1] ||
278
+ forwarded.find((a) => a.startsWith('--bundle='))?.split('=')[1] ||
279
+ forwarded.find((a) => a.startsWith('--url='))?.split('=')[1]
280
+
281
+ const owner = valueAfter(forwarded, '--owner')
282
+ const repo = valueAfter(forwarded, '--repo')
283
+
284
+ let target = targetFromArgs(forwarded)
285
+ const config = valueAfter(forwarded, '--config')
286
+ if (!target && config) {
287
+ target = config
288
+ }
289
+ if (!target) {
290
+ for (const cand of ['.maestro', 'maestro', 'flows', 'e2e']) {
291
+ if (existsSync(resolve(process.cwd(), cand))) {
292
+ target = cand
293
+ break
294
+ }
295
+ }
296
+ }
297
+ if (!target) {
298
+ console.error(' usage: rnx test <flow-or-suite> --app <bundle> --cloud')
299
+ return 1
300
+ }
301
+
302
+ const targetPath = resolve(process.cwd(), target)
303
+ if (!existsSync(targetPath)) {
304
+ console.error(` error: test target not found: ${target}`)
305
+ return 1
306
+ }
307
+
308
+ if (!app) {
309
+ console.error(
310
+ ' error: rnx test --cloud requires --app <bundle.js|https://bundle-url>',
311
+ )
312
+ return 1
313
+ }
314
+
315
+ const auth = resolveCliAuth()
316
+ if (!auth) {
317
+ console.error(' error: rnx test --cloud requires `rnx login` or RNX_API_KEY')
318
+ return 1
319
+ }
320
+
321
+ const pollIntervalFlag = valueAfter(forwarded, '--poll-interval')
322
+ const timeoutFlag = valueAfter(forwarded, '--timeout')
323
+ const pollIntervalMs =
324
+ pollIntervalFlag === null ? (opts.pollIntervalMs ?? 1000) : Number(pollIntervalFlag)
325
+ const timeoutMs =
326
+ timeoutFlag === null ? (opts.timeoutMs ?? 20 * 60 * 1000) : Number(timeoutFlag)
327
+ if (!Number.isFinite(pollIntervalMs) || pollIntervalMs < 1) {
328
+ console.error(' error: --poll-interval must be a positive number of milliseconds')
329
+ return 1
330
+ }
331
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 1) {
332
+ console.error(' error: --timeout must be a positive number of milliseconds')
333
+ return 1
334
+ }
335
+
336
+ let bundleUrl: string | undefined
337
+ let bundle: string | undefined
338
+ let bundleShareId: string | undefined
339
+
340
+ if (app.startsWith('http://') || app.startsWith('https://')) {
341
+ bundleUrl = app
342
+ } else if (app.startsWith('srb_') || app.includes('/api/preview/share/bundle')) {
343
+ bundleShareId = app.replace(/^.*id=/, '')
344
+ } else {
345
+ const localBundle = resolve(process.cwd(), app)
346
+ if (!existsSync(localBundle) || !statSync(localBundle).isFile()) {
347
+ console.error(` error: --app bundle file not found: ${app}`)
348
+ return 1
349
+ }
350
+ bundle = readFileSync(localBundle, 'utf8')
351
+ }
352
+
353
+ const targetName = basename(targetPath)
354
+ const collectionPath =
355
+ statSync(targetPath).isFile() &&
356
+ (/^(?:jest|rnx-detox)\.config\.[cm]?[jt]s$|^jest\.config\.json$/.test(targetName) ||
357
+ /\.[cm]?[jt]sx?$/.test(extname(targetPath)))
358
+ ? dirname(targetPath)
359
+ : targetPath
360
+ const localBundlePath =
361
+ app.startsWith('http://') ||
362
+ app.startsWith('https://') ||
363
+ app.startsWith('srb_') ||
364
+ app.includes('/api/preview/share/bundle')
365
+ ? null
366
+ : resolve(process.cwd(), app)
367
+ const testFiles = collectTestFiles(collectionPath, localBundlePath)
368
+ if (Object.keys(testFiles).length === 0) {
369
+ console.error(` error: no test files found at ${target}`)
370
+ return 1
371
+ }
372
+
373
+ const submitPayload: Record<string, unknown> = {
374
+ kind: 'test',
375
+ testFiles,
376
+ }
377
+ if (bundleUrl) submitPayload.bundleUrl = bundleUrl
378
+ if (bundle) submitPayload.bundle = bundle
379
+ if (bundleShareId) submitPayload.bundleShareId = bundleShareId
380
+ if (owner) submitPayload.owner = owner
381
+ if (repo) submitPayload.repo = repo
382
+
383
+ const endpoint = `${origin.replace(/\/$/, '')}/api/sootsim/runner/jobs`
384
+ let response: Response
385
+ try {
386
+ response = await fetch(endpoint, {
387
+ method: 'POST',
388
+ headers: {
389
+ 'content-type': 'application/json',
390
+ authorization: authHeaderValue(auth),
391
+ },
392
+ body: JSON.stringify(submitPayload),
393
+ })
394
+ } catch (err) {
395
+ console.error(
396
+ ` error: could not connect to ${endpoint}: ${err instanceof Error ? err.message : String(err)}`,
397
+ )
398
+ return 1
399
+ }
400
+
401
+ if (!response.ok) {
402
+ const body = await response.text()
403
+ let message = body
404
+ try {
405
+ const json = JSON.parse(body)
406
+ if (json.message) message = json.message
407
+ } catch {}
408
+ console.error(
409
+ ` error: cloud test submission failed (${response.status}): ${message}`,
410
+ )
411
+ return 1
412
+ }
413
+
414
+ const { id } = (await response.json()) as { id: string }
415
+ console.log(` cloud test job submitted: ${id}`)
416
+
417
+ const pollUrl = `${origin.replace(/\/$/, '')}/api/sootsim/runner/jobs?id=${encodeURIComponent(id)}`
418
+ const startedAt = Date.now()
419
+
420
+ while (Date.now() - startedAt < timeoutMs) {
421
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs))
422
+ let pollRes: Response
423
+ try {
424
+ pollRes = await fetch(pollUrl, {
425
+ headers: { authorization: authHeaderValue(auth) },
426
+ })
427
+ } catch (err) {
428
+ console.warn(
429
+ ` warn: error polling job: ${err instanceof Error ? err.message : String(err)}`,
430
+ )
431
+ continue
432
+ }
433
+ if (!pollRes.ok) {
434
+ console.warn(` warn: polling error: HTTP ${pollRes.status}`)
435
+ continue
436
+ }
437
+ const job = (await pollRes.json()) as {
438
+ id: string
439
+ status: string
440
+ result?: {
441
+ previewId?: string | null
442
+ previewUrl?: string | null
443
+ runId?: string | null
444
+ exitCode?: number | null
445
+ failureReason?: string | null
446
+ } | null
447
+ }
448
+
449
+ if (job.status === 'done' || job.status === 'failed' || job.status === 'expired') {
450
+ const result = job.result
451
+ // a job the recorder failed after a passing suite (missing recording or
452
+ // registration) is still a failed run.
453
+ const exitCode =
454
+ job.status === 'done' ? (result?.exitCode ?? 0) : result?.exitCode || 1
455
+ console.log(` status: ${job.status}`)
456
+ console.log(` exit code: ${exitCode}`)
457
+ if (result?.previewUrl) {
458
+ console.log(` preview: ${result.previewUrl}`)
459
+ } else if (result?.previewId) {
460
+ console.log(
461
+ ` preview: ${origin.replace(/\/$/, '')}/preview/${result.previewId}`,
462
+ )
463
+ }
464
+ if (result?.runId) {
465
+ console.log(` run: ${result.runId}`)
466
+ }
467
+ if (result?.failureReason) {
468
+ console.error(` failure: ${result.failureReason}`)
469
+ }
470
+ return exitCode
471
+ }
472
+ }
473
+
474
+ console.error(` error: cloud test timed out after ${Math.round(timeoutMs / 1000)}s`)
475
+ return 1
476
+ }
477
+
478
+ export async function runTest(
479
+ args: string[],
480
+ opts: RunTestOptions = {},
481
+ ): Promise<number> {
482
+ if (args.includes('--cloud') || args.includes('--hosted')) {
483
+ return runCloudTest(args, opts)
484
+ }
485
+
486
+ const selected = resolveTestCommand(args)
487
+ const app = valueAfter(args, '--app')
488
+ const appTarget = app
489
+ ? await resolveTestAppTarget(app)
490
+ : { target: '', close: async () => {} }
491
+
492
+ try {
493
+ if (selected.command === 'maestro') {
494
+ const { flowTimeoutScale } = await import('../../src/flow-timeout-scale')
495
+ if (flowTimeoutScale() !== 1) {
496
+ throw new Error(
497
+ 'rnx test requires original flow timeouts; unset SOOTSIM_FLOW_TIMEOUT_SCALE',
498
+ )
499
+ }
500
+ const { runMaestro } = await import('./maestro')
501
+ const maestroArgs = appTarget.target
502
+ ? selected.args.map((arg, idx) => {
503
+ const prev = selected.args[idx - 1]
504
+ if (prev === '--url') return appTarget.target
505
+ if (arg.startsWith('--url=')) return `--url=${appTarget.target}`
506
+ return arg
507
+ })
508
+ : selected.args
509
+ const code = await runMaestro(maestroArgs, opts)
510
+ return typeof code === 'number' ? code : 0
511
+ }
512
+
513
+ const { runDetox } = await import('./detox')
514
+ const detoxArgs = withoutApp(selected.args)
515
+ const previousRnxUrl = process.env.RNX_URL
516
+ if (appTarget.target) {
517
+ process.env.RNX_URL = await buildShellUrl(
518
+ appTarget.target,
519
+ resolveShellBaseUrlForBridgePort(
520
+ opts.port ?? (Number(process.env.SOOTSIM_PORT) || 5173),
521
+ ),
522
+ )
523
+ }
524
+ try {
525
+ await runDetox(detoxArgs, opts)
526
+ return 0
527
+ } finally {
528
+ if (appTarget.target) {
529
+ restoreRnxUrl(previousRnxUrl)
530
+ }
531
+ }
532
+ } finally {
533
+ await appTarget.close()
534
+ }
535
+ }