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,542 @@
1
+ // `rnx assert <verb> <verb-args...> <assertion-flags...>`
2
+ //
3
+ // thin wrapper that runs any read verb with --json forced on, captures the
4
+ // JSON payload, and converts predicate flags into a bisect-friendly exit
5
+ // code. see docs for the full flag surface — this file intentionally stays
6
+ // small: every assertion is a pure function over the parsed JSON, and the
7
+ // only runtime work is the retry loop for --within and the optional shell
8
+ // out to `jq` for --jq.
9
+ //
10
+ // exit codes:
11
+ // 0 assertion passed (bisect: good)
12
+ // 1 assertion evaluated and failed (bisect: bad)
13
+ // 125 could not evaluate (bisect: skip)
14
+ // 2 misuse — bad flags, unknown verb (treated as bad by bisect)
15
+
16
+ import { spawn } from 'child_process'
17
+ import { rnxExit } from '../run-rnx'
18
+ import { rnxSelfInvocation } from '../self-invocation'
19
+
20
+ // READ_VERBS lists every command whose stdout we know is valid JSON under
21
+ // --json. keeping this explicit means `rnx assert tap 100 200` fails
22
+ // fast with a clear message instead of silently running a write verb and
23
+ // trying to parse " tapped 100,200" as JSON.
24
+ const READ_VERBS = new Set([
25
+ // top-level
26
+ 'describe',
27
+ 'find',
28
+ 'list',
29
+ 'network',
30
+ 'logs',
31
+ // get <noun>
32
+ 'count',
33
+ 'tree',
34
+ 'url',
35
+ 'a11y',
36
+ 'node',
37
+ 'layout',
38
+ 'keyboard',
39
+ 'errors',
40
+ 'warnings',
41
+ 'requests',
42
+ 'animations',
43
+ 'animation',
44
+ 'state',
45
+ 'sample-color',
46
+ ])
47
+
48
+ // assertion flags that assert recognizes. everything else is passed through
49
+ // to the inner verb untouched. keeping this as a set (rather than positional
50
+ // parsing) means `rnx assert find --testid X --count 4` reads naturally.
51
+ const BOOL_ASSERT_FLAGS = new Set([
52
+ '--exists',
53
+ '--empty',
54
+ '--negate',
55
+ '--quiet',
56
+ '--allow-timeout',
57
+ '!',
58
+ ])
59
+ const VALUE_ASSERT_FLAGS = new Set([
60
+ '--count',
61
+ '--count-at-least',
62
+ '--count-at-most',
63
+ '--contains',
64
+ '--not-contains',
65
+ '--matches',
66
+ '--not-matches',
67
+ '--equals',
68
+ '--jq',
69
+ '--has-path',
70
+ '--within',
71
+ ])
72
+ // --path-equals takes TWO values: <dotted.path> <value>
73
+ const DUAL_VALUE_ASSERT_FLAGS = new Set(['--path-equals'])
74
+
75
+ interface Predicate {
76
+ kind: string
77
+ args: string[]
78
+ }
79
+
80
+ interface AssertArgs {
81
+ verbArgs: string[]
82
+ predicates: Predicate[]
83
+ withinMs: number
84
+ negate: boolean
85
+ quiet: boolean
86
+ allowTimeout: boolean
87
+ }
88
+
89
+ function parseAssertArgs(raw: string[]): AssertArgs | { error: string } {
90
+ const verbArgs: string[] = []
91
+ const predicates: Predicate[] = []
92
+ let withinMs = 0
93
+ let negate = false
94
+ let quiet = false
95
+ let allowTimeout = false
96
+
97
+ for (let i = 0; i < raw.length; i++) {
98
+ const a = raw[i]
99
+ if (a === '--within') {
100
+ const v = Number(raw[i + 1])
101
+ if (!Number.isFinite(v) || v < 0) {
102
+ return { error: `--within requires a non-negative number, got: ${raw[i + 1]}` }
103
+ }
104
+ withinMs = v
105
+ i++
106
+ continue
107
+ }
108
+ if (a === '--negate' || a === '!') {
109
+ negate = true
110
+ continue
111
+ }
112
+ if (a === '--quiet') {
113
+ quiet = true
114
+ continue
115
+ }
116
+ if (a === '--allow-timeout') {
117
+ allowTimeout = true
118
+ continue
119
+ }
120
+ if (BOOL_ASSERT_FLAGS.has(a)) {
121
+ predicates.push({ kind: a, args: [] })
122
+ continue
123
+ }
124
+ if (VALUE_ASSERT_FLAGS.has(a)) {
125
+ const v = raw[i + 1]
126
+ if (v == null) return { error: `${a} requires a value` }
127
+ predicates.push({ kind: a, args: [v] })
128
+ i++
129
+ continue
130
+ }
131
+ if (DUAL_VALUE_ASSERT_FLAGS.has(a)) {
132
+ const v1 = raw[i + 1]
133
+ const v2 = raw[i + 2]
134
+ if (v1 == null || v2 == null) return { error: `${a} requires <path> <value>` }
135
+ predicates.push({ kind: a, args: [v1, v2] })
136
+ i += 2
137
+ continue
138
+ }
139
+ verbArgs.push(a)
140
+ }
141
+
142
+ if (verbArgs.length === 0)
143
+ return { error: 'assert requires a verb (e.g. find, describe, get errors)' }
144
+ return { verbArgs, predicates, withinMs, negate, quiet, allowTimeout }
145
+ }
146
+
147
+ // resolve the verb name the user typed. handles `get <noun>`, `do <verb>`,
148
+ // and plain top-level verbs. returns null if the verb is unknown or is a
149
+ // known write verb (which would never produce parseable json).
150
+ function resolveReadVerb(verbArgs: string[]): string | null {
151
+ if (verbArgs[0] === 'get' || verbArgs[0] === 'debug') {
152
+ return READ_VERBS.has(verbArgs[1]) ? verbArgs[1] : null
153
+ }
154
+ if (verbArgs[0] === 'do' || verbArgs[0] === 'wait') return null
155
+ return READ_VERBS.has(verbArgs[0]) ? verbArgs[0] : null
156
+ }
157
+
158
+ interface VerbResult {
159
+ ok: boolean
160
+ payload: unknown
161
+ reason?: string
162
+ }
163
+
164
+ async function runVerbOnce(verbArgs: string[]): Promise<VerbResult> {
165
+ // re-invoke self through the real CLI entry, so every code path the user's
166
+ // verb would take, assert takes too.
167
+ const { executable, prefixArgs } = rnxSelfInvocation()
168
+ const args = [...prefixArgs, ...verbArgs, '--json']
169
+
170
+ return new Promise<VerbResult>((resolve) => {
171
+ const proc = spawn(executable, args, {
172
+ stdio: ['ignore', 'pipe', 'pipe'],
173
+ env: process.env,
174
+ })
175
+ let stdout = ''
176
+ let stderr = ''
177
+ proc.stdout.on('data', (d) => (stdout += d.toString()))
178
+ proc.stderr.on('data', (d) => (stderr += d.toString()))
179
+ proc.on('error', (err) => {
180
+ resolve({ ok: false, payload: null, reason: `spawn failed: ${err.message}` })
181
+ })
182
+ proc.on('close', (code) => {
183
+ if (code !== 0) {
184
+ // stderr tail usually has the bridge diagnostic — include a short
185
+ // slice so the skip message is actionable.
186
+ const tail = stderr.trim().split('\n').slice(-2).join(' | ').slice(0, 200)
187
+ resolve({
188
+ ok: false,
189
+ payload: null,
190
+ reason: `verb exited ${code}${tail ? `: ${tail}` : ''}`,
191
+ })
192
+ return
193
+ }
194
+ if (!stdout.trim()) {
195
+ resolve({ ok: false, payload: null, reason: 'verb produced no output' })
196
+ return
197
+ }
198
+ try {
199
+ const payload = JSON.parse(stdout)
200
+ resolve({ ok: true, payload })
201
+ } catch {
202
+ resolve({
203
+ ok: false,
204
+ payload: null,
205
+ reason: `verb output was not valid json: ${stdout.slice(0, 120)}`,
206
+ })
207
+ }
208
+ })
209
+ })
210
+ }
211
+
212
+ // read a dotted path against a JSON value. numeric segments index arrays,
213
+ // anything else reads object keys. returns { found: false } for any missing
214
+ // segment so callers can distinguish "path exists, value is null" from
215
+ // "path doesn't exist."
216
+ function readPath(
217
+ value: unknown,
218
+ path: string,
219
+ ): { found: true; value: unknown } | { found: false } {
220
+ const parts = path.split('.').filter((s) => s.length > 0)
221
+ let cur: unknown = value
222
+ for (const part of parts) {
223
+ if (cur == null) return { found: false }
224
+ if (Array.isArray(cur)) {
225
+ const idx = Number(part)
226
+ if (!Number.isInteger(idx) || idx < 0 || idx >= cur.length) return { found: false }
227
+ cur = cur[idx]
228
+ continue
229
+ }
230
+ if (typeof cur === 'object') {
231
+ const obj = cur as Record<string, unknown>
232
+ if (!(part in obj)) return { found: false }
233
+ cur = obj[part]
234
+ continue
235
+ }
236
+ return { found: false }
237
+ }
238
+ return { found: true, value: cur }
239
+ }
240
+
241
+ interface PredicateResult {
242
+ ok: boolean
243
+ reason: string
244
+ }
245
+
246
+ function lengthOf(payload: unknown): number | null {
247
+ if (Array.isArray(payload)) return payload.length
248
+ if (payload && typeof payload === 'object') return Object.keys(payload).length
249
+ return null
250
+ }
251
+
252
+ async function evalPredicate(
253
+ pred: Predicate,
254
+ payload: unknown,
255
+ ): Promise<PredicateResult> {
256
+ const { kind, args } = pred
257
+ switch (kind) {
258
+ case '--exists': {
259
+ if (payload == null) return { ok: false, reason: 'payload is null' }
260
+ const len = lengthOf(payload)
261
+ if (len === 0) return { ok: false, reason: 'payload is empty' }
262
+ return { ok: true, reason: 'exists' }
263
+ }
264
+ case '--empty': {
265
+ if (payload == null) return { ok: true, reason: 'payload is null' }
266
+ const len = lengthOf(payload)
267
+ if (len === 0) return { ok: true, reason: 'payload is empty' }
268
+ return { ok: false, reason: `payload has ${len} item(s)` }
269
+ }
270
+ case '--count': {
271
+ const want = Number(args[0])
272
+ const got = lengthOf(payload)
273
+ if (got == null)
274
+ return { ok: false, reason: `payload not countable (${typeof payload})` }
275
+ return {
276
+ ok: got === want,
277
+ reason: `count=${got}, want=${want}`,
278
+ }
279
+ }
280
+ case '--count-at-least': {
281
+ const want = Number(args[0])
282
+ const got = lengthOf(payload)
283
+ if (got == null) return { ok: false, reason: `payload not countable` }
284
+ return { ok: got >= want, reason: `count=${got}, want>=${want}` }
285
+ }
286
+ case '--count-at-most': {
287
+ const want = Number(args[0])
288
+ const got = lengthOf(payload)
289
+ if (got == null) return { ok: false, reason: `payload not countable` }
290
+ return { ok: got <= want, reason: `count=${got}, want<=${want}` }
291
+ }
292
+ case '--contains': {
293
+ const needle = args[0]
294
+ const hay = JSON.stringify(payload)
295
+ return {
296
+ ok: hay.includes(needle),
297
+ reason: hay.includes(needle) ? `contains "${needle}"` : `missing "${needle}"`,
298
+ }
299
+ }
300
+ case '--not-contains': {
301
+ const needle = args[0]
302
+ const hay = JSON.stringify(payload)
303
+ return {
304
+ ok: !hay.includes(needle),
305
+ reason: !hay.includes(needle)
306
+ ? `not contains "${needle}"`
307
+ : `unexpected "${needle}"`,
308
+ }
309
+ }
310
+ case '--matches': {
311
+ const re = new RegExp(args[0])
312
+ const hay = JSON.stringify(payload)
313
+ const ok = re.test(hay)
314
+ return {
315
+ ok,
316
+ reason: ok ? `matches /${args[0]}/` : `no match for /${args[0]}/`,
317
+ }
318
+ }
319
+ case '--not-matches': {
320
+ const re = new RegExp(args[0])
321
+ const hay = JSON.stringify(payload)
322
+ const ok = !re.test(hay)
323
+ return {
324
+ ok,
325
+ reason: ok ? `not matches /${args[0]}/` : `unexpected /${args[0]}/`,
326
+ }
327
+ }
328
+ case '--equals': {
329
+ const want = args[0]
330
+ const got =
331
+ typeof payload === 'string'
332
+ ? payload
333
+ : typeof payload === 'number' || typeof payload === 'boolean'
334
+ ? String(payload)
335
+ : JSON.stringify(payload)
336
+ return {
337
+ ok: got === want,
338
+ reason: `got=${JSON.stringify(got)}, want=${JSON.stringify(want)}`,
339
+ }
340
+ }
341
+ case '--has-path': {
342
+ const r = readPath(payload, args[0])
343
+ return {
344
+ ok: r.found,
345
+ reason: r.found ? `path ${args[0]} present` : `path ${args[0]} missing`,
346
+ }
347
+ }
348
+ case '--path-equals': {
349
+ const [path, want] = args
350
+ const r = readPath(payload, path)
351
+ if (!r.found) return { ok: false, reason: `path ${path} missing` }
352
+ const got =
353
+ typeof r.value === 'string' ||
354
+ typeof r.value === 'number' ||
355
+ typeof r.value === 'boolean'
356
+ ? String(r.value)
357
+ : JSON.stringify(r.value)
358
+ return { ok: got === want, reason: `${path}=${got}, want=${want}` }
359
+ }
360
+ case '--jq': {
361
+ // shell out to `jq`. if jq isn't installed we can't evaluate, so we
362
+ // bubble that up as a 125-skip rather than treating it as a failure.
363
+ const expr = args[0]
364
+ try {
365
+ const out = await runJq(expr, payload)
366
+ const trimmed = out.trim()
367
+ const truthy =
368
+ trimmed.length > 0 &&
369
+ trimmed !== 'null' &&
370
+ trimmed !== 'false' &&
371
+ trimmed !== '""'
372
+ return { ok: truthy, reason: `jq: ${trimmed.slice(0, 120)}` }
373
+ } catch (err: any) {
374
+ // throw so the caller can turn this into a 125 — consistent with how
375
+ // "verb crashed" is treated. we don't want bisect marking commits bad
376
+ // because jq isn't installed.
377
+ throw new Error(`jq evaluation failed: ${err.message}`)
378
+ }
379
+ }
380
+ default:
381
+ return { ok: false, reason: `unknown predicate ${kind}` }
382
+ }
383
+ }
384
+
385
+ function runJq(expr: string, payload: unknown): Promise<string> {
386
+ return new Promise((resolve, reject) => {
387
+ const proc = spawn('jq', [expr], { stdio: ['pipe', 'pipe', 'pipe'] })
388
+ let out = ''
389
+ let err = ''
390
+ proc.stdout.on('data', (d) => (out += d.toString()))
391
+ proc.stderr.on('data', (d) => (err += d.toString()))
392
+ proc.on('error', reject)
393
+ proc.on('close', (code) => {
394
+ if (code !== 0) {
395
+ reject(new Error(err.trim() || `jq exited ${code}`))
396
+ return
397
+ }
398
+ resolve(out)
399
+ })
400
+ proc.stdin.end(JSON.stringify(payload))
401
+ })
402
+ }
403
+
404
+ export async function runAssert(rawArgs: string[]): Promise<void> {
405
+ if (rawArgs[0] === '--help' || rawArgs[0] === '-h' || rawArgs.length === 0) {
406
+ printHelp()
407
+ rnxExit(rawArgs.length === 0 ? 2 : 0)
408
+ }
409
+
410
+ const parsed = parseAssertArgs(rawArgs)
411
+ if ('error' in parsed) {
412
+ process.stderr.write(` assert: ${parsed.error}\n`)
413
+ rnxExit(2)
414
+ }
415
+
416
+ const { verbArgs, predicates, withinMs, negate, quiet, allowTimeout } = parsed
417
+
418
+ const verb = resolveReadVerb(verbArgs)
419
+ if (!verb) {
420
+ process.stderr.write(
421
+ ` assert: unknown or non-read verb: ${verbArgs.join(' ')}\n` +
422
+ ` supported: ${Array.from(READ_VERBS).sort().join(', ')}\n`,
423
+ )
424
+ rnxExit(2)
425
+ }
426
+
427
+ // default assertion when none given is --exists. matches the common
428
+ // "does this node / error / entry even show up?" check.
429
+ const effectivePredicates =
430
+ predicates.length > 0 ? predicates : [{ kind: '--exists', args: [] }]
431
+
432
+ const deadline = withinMs > 0 ? Date.now() + withinMs : 0
433
+ let attempts = 0
434
+ let lastFailReason = ''
435
+ let lastVerbSkipReason = ''
436
+
437
+ while (true) {
438
+ attempts++
439
+ const verbResult = await runVerbOnce(verbArgs)
440
+ if (!verbResult.ok) {
441
+ lastVerbSkipReason = verbResult.reason || 'verb failed'
442
+ if (Date.now() < deadline) {
443
+ await sleep(150)
444
+ continue
445
+ }
446
+ // out of retry budget. default is skip (125) because "verb couldn't
447
+ // evaluate" is distinct from "assertion evaluated and failed".
448
+ // --allow-timeout flips this to fail (1) — used when an auto-wait
449
+ // timeout inside the verb is a meaningful negative signal, not a
450
+ // "please bisect-skip this commit".
451
+ const exitCode = allowTimeout ? 1 : 125
452
+ if (!quiet) {
453
+ const label = exitCode === 1 ? 'fail' : 'skip'
454
+ process.stderr.write(` assert: ${label} — ${lastVerbSkipReason}\n`)
455
+ }
456
+ rnxExit(exitCode)
457
+ }
458
+
459
+ let allPass = true
460
+ const reasons: string[] = []
461
+ try {
462
+ for (const pred of effectivePredicates) {
463
+ const r = await evalPredicate(pred, verbResult.payload)
464
+ reasons.push(`${pred.kind}: ${r.reason}`)
465
+ if (!r.ok) {
466
+ allPass = false
467
+ break
468
+ }
469
+ }
470
+ } catch (err: any) {
471
+ if (!quiet) process.stderr.write(` assert: skip — ${err.message}\n`)
472
+ rnxExit(125)
473
+ }
474
+
475
+ const pass = negate ? !allPass : allPass
476
+ if (pass) {
477
+ if (!quiet) {
478
+ process.stderr.write(
479
+ ` assert: pass${attempts > 1 ? ` (attempt ${attempts})` : ''} — ${reasons.join('; ')}\n`,
480
+ )
481
+ }
482
+ rnxExit(0)
483
+ }
484
+
485
+ lastFailReason = reasons.join('; ')
486
+ if (Date.now() < deadline) {
487
+ await sleep(150)
488
+ continue
489
+ }
490
+ if (!quiet) {
491
+ process.stderr.write(
492
+ ` assert: fail${attempts > 1 ? ` after ${attempts} attempt(s)` : ''} — ${lastFailReason}\n`,
493
+ )
494
+ }
495
+ rnxExit(1)
496
+ }
497
+ }
498
+
499
+ function sleep(ms: number): Promise<void> {
500
+ return new Promise((resolve) => setTimeout(resolve, ms))
501
+ }
502
+
503
+ function printHelp(): void {
504
+ process.stdout.write(`
505
+ rnx assert — run any read verb and convert its output into a bisect-friendly exit code.
506
+
507
+ usage:
508
+ rnx assert <verb> <verb-args...> <assertion-flags...>
509
+
510
+ exit codes:
511
+ 0 assertion passed (git bisect: good)
512
+ 1 assertion failed (git bisect: bad)
513
+ 125 could not evaluate (git bisect: skip — app not loaded, verb crashed)
514
+ 2 misuse (bad flags/verb)
515
+
516
+ assertion flags (AND'd when multiple are given):
517
+ --count <n> result array has exactly n items
518
+ --count-at-least <n> length >= n
519
+ --count-at-most <n> length <= n
520
+ --exists non-null, non-empty (default if no flag given)
521
+ --empty null or zero length
522
+ --contains <str> JSON.stringify(result) contains str (repeatable)
523
+ --not-contains <str> opposite
524
+ --matches <regex> result (stringified) matches regex
525
+ --not-matches <regex> opposite
526
+ --equals <literal> scalar equality
527
+ --has-path <dotted.path> nested path exists
528
+ --path-equals <path> <value> nested path equals value
529
+ --jq <expr> jq expression is truthy (requires jq on PATH)
530
+ --within <ms> retry verb + predicates until pass or deadline
531
+ --allow-timeout treat verb auto-wait timeout as a fail, not a skip
532
+ --negate, ! flip pass/fail (does not flip skip)
533
+ --quiet suppress the one-line pass/fail summary
534
+
535
+ examples:
536
+ rnx assert find --testid set-max-input --count 4
537
+ rnx assert describe --contains "swap-form-header"
538
+ rnx assert get errors --count 0
539
+ rnx assert find --testid submit --exists --within 3000
540
+ rnx assert describe --jq '.tree | contains("swap-form-header")'
541
+ `)
542
+ }
@@ -0,0 +1,71 @@
1
+ import {
2
+ refreshSharedDesktopAuthSession,
3
+ sharedDesktopAuthAccounts,
4
+ } from '../../src/auth/shared-session'
5
+ import { CHOOSE_ACCOUNT_HINT, resolveCliAuth } from '../auth'
6
+ import { rnxExit } from '../run-rnx'
7
+
8
+ function printHelp(): void {
9
+ console.log(`
10
+ rnx auth — inspect CLI auth
11
+
12
+ usage:
13
+ rnx auth
14
+
15
+ API keys are read from RNX_API_KEY.
16
+ Use \`rnx login\` to sign in and \`rnx logout\` to clear the desktop session.
17
+ `)
18
+ }
19
+
20
+ async function printStatus(): Promise<number> {
21
+ const auth = resolveCliAuth()
22
+ if (!auth) {
23
+ console.log(' not signed in')
24
+ console.log(' set RNX_API_KEY=sk_rnx_... or run `rnx login`')
25
+ return 1
26
+ }
27
+
28
+ if (auth.kind === 'api-key') {
29
+ console.log(` api key ${auth.secret.slice(0, 14)}...`)
30
+ console.log(` source: ${auth.envName} env var`)
31
+ return 0
32
+ }
33
+
34
+ if (auth.kind === 'github') {
35
+ console.log(` github token (${auth.source})`)
36
+ console.log(` repo: ${auth.repoId}`)
37
+ return 0
38
+ }
39
+
40
+ const current = await refreshSharedDesktopAuthSession(auth.origin)
41
+ const user = current?.user
42
+ const accounts = sharedDesktopAuthAccounts(user)
43
+ const accountId = current?.accountId ?? auth.accountId
44
+ const account = accounts.find((candidate) => candidate.id === accountId)
45
+ console.log(` ${user?.email || user?.name || user?.id || 'signed in'}`)
46
+ console.log(
47
+ ` account: ${
48
+ account ? `${account.name} (${account.id})` : (accountId ?? CHOOSE_ACCOUNT_HINT)
49
+ }`,
50
+ )
51
+ console.log(` origin: ${current?.origin ?? auth.origin}`)
52
+ if (current?.updatedAt) console.log(` updated: ${current.updatedAt}`)
53
+ return 0
54
+ }
55
+
56
+ export async function runAuth(args: string[]): Promise<void> {
57
+ if (args.includes('--help') || args.includes('-h')) {
58
+ printHelp()
59
+ return
60
+ }
61
+
62
+ const sub = args.find((arg) => !arg.startsWith('-'))
63
+ if (!sub || sub === 'status') {
64
+ process.exitCode = await printStatus()
65
+ return
66
+ }
67
+
68
+ console.error(` unknown auth command: ${sub}`)
69
+ printHelp()
70
+ rnxExit(1)
71
+ }
@@ -0,0 +1,71 @@
1
+ // ProjectFilePlane over an already-built bundle held in memory.
2
+ //
3
+ // a prebuilt Box has no sources, so there is no checkout to index and nothing
4
+ // on disk to stay in step with: the whole project is the bundle the page boots.
5
+ // CheckoutFilePlane cannot serve this because it asks git what belongs to the
6
+ // project, and a bundle produced by a build lives outside any checkout.
7
+ //
8
+ // the bundle is split here rather than by the caller, so the one place that
9
+ // decides where the parts go is the one place that decides how big they are.
10
+
11
+ import { PREBUILT_BUNDLE_PART_BYTES, prebuiltBundlePartPath } from '@rnx/box/template'
12
+ import type { ProjectFileMeta, ProjectFilePlane } from '@rnx/box/plane'
13
+
14
+ export class BundleFilePlane implements ProjectFilePlane {
15
+ private readonly parts = new Map<string, Uint8Array>()
16
+ private readonly entries = new Map<string, ProjectFileMeta>()
17
+
18
+ constructor(bundle: Uint8Array) {
19
+ if (bundle.byteLength === 0) throw new Error('a Box bundle cannot be empty')
20
+ for (
21
+ let offset = 0, index = 0;
22
+ offset < bundle.byteLength;
23
+ offset += PREBUILT_BUNDLE_PART_BYTES, index++
24
+ ) {
25
+ const part = bundle.subarray(offset, offset + PREBUILT_BUNDLE_PART_BYTES)
26
+ this.record(prebuiltBundlePartPath(index), part)
27
+ }
28
+ }
29
+
30
+ private record(path: string, content: Uint8Array): void {
31
+ this.parts.set(path, content)
32
+ this.entries.set(path, {
33
+ size: content.byteLength,
34
+ readOnly: false,
35
+ // a Metro bundle is JavaScript. the flag is what decides whether a shell
36
+ // may overwrite a file with text, and marking it binary would make the
37
+ // bundle unwritable from inside the box for no reason.
38
+ binary: false,
39
+ })
40
+ }
41
+
42
+ index(): ReadonlyMap<string, ProjectFileMeta> {
43
+ return this.entries
44
+ }
45
+
46
+ // nothing else can change this plane, so the index is already current.
47
+ refresh(): Promise<void> {
48
+ return Promise.resolve()
49
+ }
50
+
51
+ async read(path: string): Promise<string> {
52
+ return new TextDecoder().decode(await this.readBytes(path))
53
+ }
54
+
55
+ readBytes(path: string): Promise<Uint8Array> {
56
+ const part = this.parts.get(path)
57
+ if (!part) return Promise.reject(new Error(`no such file: ${path}`))
58
+ return Promise.resolve(part)
59
+ }
60
+
61
+ write(path: string, content: Uint8Array): Promise<void> {
62
+ this.record(path, content)
63
+ return Promise.resolve()
64
+ }
65
+
66
+ delete(path: string): Promise<void> {
67
+ this.parts.delete(path)
68
+ this.entries.delete(path)
69
+ return Promise.resolve()
70
+ }
71
+ }