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,171 @@
1
+ import { createHash } from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import { createRequire } from 'node:module'
4
+ import path from 'node:path'
5
+ import {
6
+ configPluginSplashFromExpoConfig,
7
+ type ConfigPluginSplashSpec,
8
+ } from 'sootsim-engine/engine/config-plugin-splash'
9
+ import { resolveAppProject } from './app-project'
10
+
11
+ const require = createRequire(import.meta.url)
12
+
13
+ // the filesystem scan and the engine's metro-manifest scan share the same
14
+ // parser (config-plugin-splash.ts) and produce the same resolved spec.
15
+ export type DiscoveredAppSplashSpec = ConfigPluginSplashSpec
16
+
17
+ type ExpoConfigGetter = (dir: string, opts: Record<string, unknown>) => { exp?: unknown }
18
+
19
+ function localOriginOf(value: string): string | null {
20
+ try {
21
+ const url = new URL(value)
22
+ const host = url.hostname.replace(/^\[|\]$/g, '').toLowerCase()
23
+ if (
24
+ host === 'localhost' ||
25
+ host.endsWith('.localhost') ||
26
+ host === '0.0.0.0' ||
27
+ host === '::1' ||
28
+ /^127(?:\.\d{1,3}){3}$/.test(host)
29
+ ) {
30
+ return url.origin
31
+ }
32
+ } catch {}
33
+ return null
34
+ }
35
+
36
+ // discover the app's configured splash by evaluating its expo config on the
37
+ // filesystem (mirrors discoverNativeLinkedAppFonts exactly). returns null when
38
+ // no splash is configured, no app config is found, or the bundle isn't local.
39
+ export function discoverConfigPluginAppSplash(opts: {
40
+ bundleUrl: string
41
+ projectDir: string
42
+ platform?: string
43
+ }): DiscoveredAppSplashSpec | null {
44
+ const metroOrigin = localOriginOf(opts.bundleUrl)
45
+ if (!metroOrigin) return null
46
+
47
+ const project = resolveAppProject(opts.projectDir)
48
+ const selectedAppDir = project?.appDir ?? null
49
+ if (!selectedAppDir) return null
50
+
51
+ const platform = (opts.platform || process.env.CONTRAST_PLATFORM || 'ios').toLowerCase()
52
+
53
+ let getConfig: ExpoConfigGetter | null = null
54
+ try {
55
+ const modulePath = require.resolve('@expo/config', {
56
+ paths: [selectedAppDir],
57
+ })
58
+ const mod = require(modulePath) as { getConfig?: unknown }
59
+ if (typeof mod.getConfig === 'function') {
60
+ getConfig = mod.getConfig as ExpoConfigGetter
61
+ }
62
+ } catch {}
63
+ if (!getConfig) return null
64
+
65
+ let exp: unknown
66
+ try {
67
+ const result = getConfig(selectedAppDir, { skipSDKVersionRequirement: true })
68
+ if (result?.exp && typeof result.exp === 'object') exp = result.exp
69
+ } catch {}
70
+ if (!exp || typeof exp !== 'object') return null
71
+
72
+ return configPluginSplashFromExpoConfig(
73
+ exp,
74
+ platform === 'android' ? 'android' : 'ios',
75
+ metroOrigin,
76
+ )
77
+ }
78
+
79
+ // encode a resolved splash spec into the `?appSplash=` engine-URL wire value
80
+ // (URL-safe JSON). the engine decodes it with parseAppSplashSpec.
81
+ export function encodeAppSplashWire(spec: DiscoveredAppSplashSpec): string {
82
+ return encodeURIComponent(JSON.stringify(spec))
83
+ }
84
+
85
+ // parse a stamped `?appSplash=` wire value back into a spec (or null). used by
86
+ // the preview upload path to learn the splash image url it must stage.
87
+ export function parseAppSplashWire(wire: string): DiscoveredAppSplashSpec | null {
88
+ if (!wire) return null
89
+ try {
90
+ const decoded = JSON.parse(decodeURIComponent(wire)) as unknown
91
+ if (!decoded || typeof decoded !== 'object') return null
92
+ const spec = decoded as Record<string, unknown>
93
+ if (
94
+ spec.source !== 'expo-splash-screen' &&
95
+ spec.source !== 'expo-legacy' &&
96
+ spec.source !== 'react-native-bootsplash'
97
+ )
98
+ return null
99
+ if (typeof spec.backgroundColor !== 'string') return null
100
+ return decoded as DiscoveredAppSplashSpec
101
+ } catch {
102
+ return null
103
+ }
104
+ }
105
+
106
+ // every distinct image url a splash spec references (base + dark variant). used
107
+ // by the preview upload path so both the light and dark splash images get
108
+ // staged as share assets.
109
+ export function splashImageUrls(spec: DiscoveredAppSplashSpec): string[] {
110
+ const urls = new Set<string>()
111
+ if (spec.imageUrl) urls.add(spec.imageUrl)
112
+ if (spec.dark?.imageUrl) urls.add(spec.dark.imageUrl)
113
+ return [...urls]
114
+ }
115
+
116
+ // content type for a splash image url, by extension. config splash images ship
117
+ // as png/jpg/webp/gif/svg; fall back to octet-stream so the share store still
118
+ // serves bytes the engine's <img> can load.
119
+ export function splashImageContentType(url: string): string {
120
+ const ext = (url.split('?')[0].split('.').pop() || '').toLowerCase()
121
+ if (ext === 'png') return 'image/png'
122
+ if (ext === 'jpg' || ext === 'jpeg') return 'image/jpeg'
123
+ if (ext === 'webp') return 'image/webp'
124
+ if (ext === 'gif') return 'image/gif'
125
+ if (ext === 'svg') return 'image/svg+xml'
126
+ return 'application/octet-stream'
127
+ }
128
+
129
+ export type FetchedAppSplashImage = {
130
+ url: string
131
+ // sha256(url) — the share-store key, matching the font/asset convention.
132
+ urlhash: string
133
+ contentType: string
134
+ bytes: Uint8Array
135
+ }
136
+
137
+ // fetch every splash image declared on an `?appSplash=` wire. like the config-
138
+ // plugin fonts, these images live in the native binary / launch storyboard,
139
+ // never in the metro bundle graph, so the preview upload path backfills them by
140
+ // re-fetching the declared urls off the still-live metro — the same staging
141
+ // pattern fetchAppFontFiles uses, keyed by sha256(url).
142
+ export async function fetchAppSplashImages(
143
+ wire: string,
144
+ hooks?: {
145
+ onStaged?: (info: { url: string; byteLength: number }) => void
146
+ onError?: (url: string, err: unknown) => void
147
+ },
148
+ ): Promise<FetchedAppSplashImage[]> {
149
+ const spec = parseAppSplashWire(wire)
150
+ if (!spec) return []
151
+ const out: FetchedAppSplashImage[] = []
152
+ const seen = new Set<string>()
153
+ for (const url of splashImageUrls(spec)) {
154
+ const urlhash = createHash('sha256').update(url).digest('hex')
155
+ if (seen.has(urlhash)) continue
156
+ seen.add(urlhash)
157
+ try {
158
+ const res = await fetch(url)
159
+ if (!res.ok) {
160
+ hooks?.onError?.(url, new Error(`${res.status} ${res.statusText}`))
161
+ continue
162
+ }
163
+ const bytes = new Uint8Array(await res.arrayBuffer())
164
+ out.push({ url, urlhash, contentType: splashImageContentType(url), bytes })
165
+ hooks?.onStaged?.({ url, byteLength: bytes.byteLength })
166
+ } catch (err) {
167
+ hooks?.onError?.(url, err)
168
+ }
169
+ }
170
+ return out
171
+ }
@@ -0,0 +1,26 @@
1
+ // resets the guest app through the engine lifecycle bridge. a storage reset
2
+ // replaces the tenant worker before clearing its scoped stores, which is the
3
+ // maestro `clearState` contract. a plain reload keeps the faster reload path.
4
+ //
5
+ // this replaces a host-realm `localStorage.clear()` + `indexedDB.deleteDatabase`
6
+ // loop that was wrong twice: (1) `localStorage.clear()` wiped engine settings
7
+ // and every co-tenant sim's tenant storage, not just this app's; (2) the delete
8
+ // loop ran in the host realm where the only database is the engine's own
9
+ // `preview-share-state`, which the engine holds open — `deleteDatabase` then
10
+ // hung forever (no success/error/blocked event fired), so the whole eval timed
11
+ // out at 60s and never cleared the guest's secure-store session either. that
12
+ // was the bluesky e2e suite's blanket setupApp failure.
13
+ export function resetGuestAppStateEval(resetStorage: boolean): string {
14
+ return `(async () => {
15
+ const bridge = globalThis.SootSim && globalThis.SootSim.bridges && globalThis.SootSim.bridges.hotRemount
16
+ if (!bridge) return false
17
+ if (${resetStorage ? 'true' : 'false'}) {
18
+ if (typeof bridge.resetExternalApp !== 'function') return false
19
+ const result = await bridge.resetExternalApp({ strategy: 'full' })
20
+ return result.failures.length === 0 && result.relaunched
21
+ }
22
+ if (typeof bridge.reloadExternalApp !== 'function') return false
23
+ await bridge.reloadExternalApp()
24
+ return true
25
+ })()`
26
+ }
package/cli/auth.ts ADDED
@@ -0,0 +1,184 @@
1
+ // rnx cli auth resolution for cloud-hitting commands.
2
+ //
3
+ // priority order (first match wins):
4
+ // 1. RNX_API_KEY — ci path
5
+ // 2. CONTRAST_INSTALLATION_TOKEN/GITHUB_TOKEN + repo env — github action
6
+ // preview uploads from the Contrast runner, no extra secret
7
+ // 3. desktop session token from shared desktop auth (`rnx login`)
8
+ //
9
+ // callers wanting an auth header: `authHeaderOrExit('upload')`.
10
+
11
+ import {
12
+ readSharedDesktopAuthSession,
13
+ sharedDesktopAuthAccounts,
14
+ } from '../src/auth/shared-session.ts'
15
+ import { rnxExit } from './run-rnx.ts'
16
+
17
+ export type CliApiKeyEnvName = 'RNX_API_KEY'
18
+
19
+ export type CliAuth =
20
+ | { kind: 'api-key'; secret: string; source: 'env'; envName: CliApiKeyEnvName }
21
+ | {
22
+ kind: 'github'
23
+ token: string
24
+ // historical name: this is the github owner/repo slug, not the
25
+ // numeric repository id.
26
+ repoId: string
27
+ repositoryId: string | null
28
+ owner: string | null
29
+ repo: string | null
30
+ installationId: string | null
31
+ source: 'contrast-runner' | 'github-actions'
32
+ }
33
+ | {
34
+ kind: 'session'
35
+ token: string
36
+ origin: string
37
+ // the account this sign-in bills cloud sessions to. null when the person
38
+ // belongs to several accounts and has not chosen one with `rnx login
39
+ // --account`; a person with exactly one account never has to choose.
40
+ accountId: string | null
41
+ }
42
+
43
+ export type GitHubUploadIdentity = {
44
+ repoId: string
45
+ owner?: string
46
+ repo?: string
47
+ installationId?: string | null
48
+ }
49
+
50
+ // ─── resolution ────────────────────────────────────────────────────────────
51
+
52
+ const GITHUB_TOKEN_PREFIXES = ['ghs_', 'ghp_', 'gho_', 'ghu_', 'github_pat_']
53
+
54
+ function looksLikeGitHubToken(token: string | undefined): boolean {
55
+ // github's 2026 stateless installation tokens (`secrets.GITHUB_TOKEN` /
56
+ // github.token in actions) are ~520 chars and vary by payload — keep them
57
+ // opaque with NO upper length cap, matching the canonical server-side check
58
+ // in src/github/token.ts. the length floor + prefix is only a cheap typo
59
+ // guard before the cloud endpoints verify real repo access. an upper cap
60
+ // here silently rejects the long actions token, so resolveCliAuth() returns
61
+ // null and every CI `record start` dies with "set RNX_API_KEY".
62
+ return Boolean(
63
+ token &&
64
+ token.length >= 20 &&
65
+ GITHUB_TOKEN_PREFIXES.some((prefix) => token.startsWith(prefix)),
66
+ )
67
+ }
68
+
69
+ function resolveGitHubActionsAuth(): CliAuth | null {
70
+ const contrastRunnerToken = process.env.CONTRAST_INSTALLATION_TOKEN?.trim()
71
+ const githubActionsToken = process.env.GITHUB_TOKEN?.trim()
72
+ const token = contrastRunnerToken || githubActionsToken
73
+ if (!looksLikeGitHubToken(token)) return null
74
+
75
+ const repoSlug = (
76
+ process.env.CONTRAST_REPO ||
77
+ process.env.GITHUB_REPOSITORY ||
78
+ ''
79
+ ).trim()
80
+ if (!repoSlug) return null
81
+ const [owner, repo] = repoSlug.includes('/') ? repoSlug.split('/', 2) : [null, null]
82
+ const repositoryId = (
83
+ process.env.CONTRAST_REPO_ID ||
84
+ process.env.GITHUB_REPOSITORY_ID ||
85
+ ''
86
+ ).trim()
87
+
88
+ const installationId = (
89
+ process.env.CONTRAST_INSTALLATION_ID ||
90
+ process.env.GITHUB_APP_INSTALLATION_ID ||
91
+ ''
92
+ ).trim()
93
+
94
+ return {
95
+ kind: 'github',
96
+ token: token!,
97
+ repoId: repoSlug,
98
+ repositoryId: repositoryId || null,
99
+ owner: owner || null,
100
+ repo: repo || null,
101
+ installationId: installationId || null,
102
+ source: contrastRunnerToken ? 'contrast-runner' : 'github-actions',
103
+ }
104
+ }
105
+
106
+ function isCliApiKeySecret(secret: string): boolean {
107
+ return secret.startsWith('sk_rnx_')
108
+ }
109
+
110
+ function readApiKeyFromEnv(): { secret: string; envName: CliApiKeyEnvName } | null {
111
+ const envName = 'RNX_API_KEY'
112
+ const secret = process.env[envName]?.trim()
113
+ return secret && isCliApiKeySecret(secret) ? { secret, envName } : null
114
+ }
115
+
116
+ export function resolveCliAuth(): CliAuth | null {
117
+ const envKey = readApiKeyFromEnv()
118
+ if (envKey) {
119
+ return {
120
+ kind: 'api-key',
121
+ secret: envKey.secret,
122
+ source: 'env',
123
+ envName: envKey.envName,
124
+ }
125
+ }
126
+ const githubAuth = resolveGitHubActionsAuth()
127
+ if (githubAuth) return githubAuth
128
+ const session = readSharedDesktopAuthSession()
129
+ if (session?.token) {
130
+ const accounts = sharedDesktopAuthAccounts(session.user)
131
+ return {
132
+ kind: 'session',
133
+ token: session.token,
134
+ origin: session.origin,
135
+ accountId:
136
+ session.accountId ?? (accounts.length === 1 ? (accounts[0]?.id ?? null) : null),
137
+ }
138
+ }
139
+ return null
140
+ }
141
+
142
+ export const CHOOSE_ACCOUNT_HINT =
143
+ 'this sign-in belongs to several accounts; run `rnx login --account <name>` to choose the one to bill'
144
+
145
+ // the account a request that opens a paid cloud session names. an api key
146
+ // names its own account, so it sends none; a session token has to say.
147
+ export function cloudAccountIdOrExit(auth: CliAuth): string | null {
148
+ if (auth.kind !== 'session') return null
149
+ if (auth.accountId) return auth.accountId
150
+ process.stderr.write(`\n ${CHOOSE_ACCOUNT_HINT}\n\n`)
151
+ rnxExit(1)
152
+ }
153
+
154
+ export function authHeaderValue(auth: CliAuth): string {
155
+ return auth.kind === 'api-key' ? `Bearer ${auth.secret}` : `Bearer ${auth.token}`
156
+ }
157
+
158
+ export function githubUploadIdentity(auth: CliAuth | null): GitHubUploadIdentity | null {
159
+ if (!auth || auth.kind !== 'github') return null
160
+ return {
161
+ repoId: auth.repoId,
162
+ owner: auth.owner ?? undefined,
163
+ repo: auth.repo ?? undefined,
164
+ installationId: auth.installationId,
165
+ }
166
+ }
167
+
168
+ // fail fast with a friendly message — use at the top of cloud-hitting
169
+ // commands that can't proceed without auth.
170
+ export function authHeaderOrExit(commandLabel: string): {
171
+ auth: CliAuth
172
+ header: string
173
+ } {
174
+ const auth = resolveCliAuth()
175
+ if (auth) return { auth, header: authHeaderValue(auth) }
176
+
177
+ process.stderr.write(
178
+ `\n rnx ${commandLabel} needs to be authenticated.\n\n` +
179
+ ` pick one:\n` +
180
+ ` • run \`rnx login\` to sign in with your GH account\n` +
181
+ ` • set RNX_API_KEY=sk_rnx_... (recommended for CI)\n\n`,
182
+ )
183
+ rnxExit(1)
184
+ }
package/cli/bin.ts ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+
3
+ // rnx CLI entry point. the full command and verb surface lives in the
4
+ // registry at `packages/rnx-skills/src/cli/meta.ts` — help output,
5
+ // website docs, and agent skills all read from there. do not duplicate
6
+ // command listings in this file; run `rnx --help` for the canonical
7
+ // surface.
8
+ //
9
+ // this file only routes. the hidden entry points below each take over the
10
+ // whole process, so the CLI itself lives in `./main` and is imported only once
11
+ // none of them has claimed the run.
12
+
13
+ import { RNX_INTERNAL_COMMAND } from './internal-child'
14
+
15
+ if (process.argv[2] === '__shell-init') {
16
+ const { renderRnxShellInit } = await import('./shell-init')
17
+ try {
18
+ process.stdout.write(renderRnxShellInit(process.argv[3] ?? ''))
19
+ process.exit(0)
20
+ } catch (error) {
21
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
22
+ process.exit(1)
23
+ }
24
+ }
25
+
26
+ if (process.argv[2] === '__cloud-confirm') {
27
+ try {
28
+ const [{ confirmCloudSimulator }, { readCloudSession }] = await Promise.all([
29
+ import('./cloud-client'),
30
+ import('./cloud-session'),
31
+ ])
32
+ const session = readCloudSession()
33
+ if (!session) throw new Error('RNX cloud confirmation has no shell session')
34
+ // a box has no claim to keep alive: its simulator is held by the Box page
35
+ // for as long as the box runs, so a shell on a box confirms nothing.
36
+ if (session.service === 'sim') await confirmCloudSimulator(session)
37
+ process.exit(0)
38
+ } catch (error) {
39
+ process.stderr.write(
40
+ `RNX cloud confirmation failed: ${
41
+ error instanceof Error ? error.message : String(error)
42
+ }\n`,
43
+ )
44
+ process.exit(1)
45
+ }
46
+ }
47
+
48
+ // child processes this CLI spawns as itself: the detached browser host,
49
+ // playwright's installer, maestro's blocking fetch worker. unlike the two
50
+ // above, these do not exit on the way out — the installer and the browser host
51
+ // are still working when their handler returns, and the process ends when that
52
+ // work does. so this is the branch that must skip the CLI rather than the CLI
53
+ // that must skip it.
54
+ if (process.argv[2] === RNX_INTERNAL_COMMAND) {
55
+ const { runInternalChild } = await import('./internal-child')
56
+ await runInternalChild(process.argv.slice(3))
57
+ } else {
58
+ await import('./main')
59
+ }
@@ -0,0 +1,226 @@
1
+ import { MERGED_CONSOLE_COUNT_EVAL, inspectErrors } from './commands/inspect/core'
2
+ import { formatLogTimestamp } from './commands/inspect/shared'
3
+ import type { WsBridge } from './ws-bridge'
4
+ import type { SootSimRequestEntry } from '@rnx/globals'
5
+
6
+ interface ConsoleSummaryOptions {
7
+ includeTail?: boolean
8
+ errorsCommand?: string
9
+ warningsCommand?: string
10
+ }
11
+
12
+ interface RequestSummaryOptions {
13
+ includeTail?: boolean
14
+ requestsCommand?: string
15
+ }
16
+
17
+ interface FailureDiagnosticsOptions {
18
+ errorsCommand?: string
19
+ warningsCommand?: string
20
+ requestsCommand?: string
21
+ }
22
+
23
+ function formatRequestMessage(entry: SootSimRequestEntry) {
24
+ const target = entry.displayUrl || entry.url
25
+ if (entry.status != null) {
26
+ return `${entry.method} ${target} -> ${entry.status}${entry.statusText ? ` ${entry.statusText}` : ''}`
27
+ }
28
+ if (entry.error) {
29
+ return `${entry.method} ${target} -> ${entry.error}`
30
+ }
31
+ return `${entry.method} ${target}`
32
+ }
33
+
34
+ async function callTestBridge<T = unknown>(
35
+ bridge: WsBridge,
36
+ method: string,
37
+ ...args: unknown[]
38
+ ) {
39
+ return bridge.send({
40
+ type: 'call',
41
+ path: `__sootsimTest.${method}`,
42
+ args,
43
+ }) as Promise<T>
44
+ }
45
+
46
+ export async function printBridgeConsoleSummary(
47
+ bridge: WsBridge,
48
+ opts: ConsoleSummaryOptions = {},
49
+ ) {
50
+ const counts = (await bridge.send({
51
+ type: 'evaluate',
52
+ code: MERGED_CONSOLE_COUNT_EVAL,
53
+ })) as { errors?: number; warnings?: number; total?: number } | null
54
+
55
+ if (!counts || typeof counts !== 'object') return
56
+
57
+ const errors = Math.max(0, Number(counts.errors) || 0)
58
+ const warnings = Math.max(0, Number(counts.warnings) || 0)
59
+ if (errors === 0 && warnings === 0) return
60
+
61
+ const parts = []
62
+ if (errors > 0) parts.push(`${errors} console error${errors === 1 ? '' : 's'}`)
63
+ if (warnings > 0) {
64
+ parts.push(`${warnings} console warning${warnings === 1 ? '' : 's'}`)
65
+ }
66
+ console.log(`\n console: ${parts.join(', ')}`)
67
+ if (opts.errorsCommand) console.log(` inspect: ${opts.errorsCommand}`)
68
+ if (warnings > 0 && opts.warningsCommand) {
69
+ console.log(` inspect: ${opts.warningsCommand}`)
70
+ }
71
+
72
+ if (!opts.includeTail || errors === 0) return
73
+
74
+ // merged read (ws-bridge buffer + observability) so forwarded render-worker
75
+ // errors are shown in the tail, not just counted.
76
+ const recentErrors = await inspectErrors(bridge, 5)
77
+ if (!Array.isArray(recentErrors) || recentErrors.length === 0) return
78
+
79
+ console.log('\n recent console errors:\n')
80
+ for (const entry of recentErrors as Array<{
81
+ timestamp: number
82
+ args?: unknown[]
83
+ }>) {
84
+ const time = formatLogTimestamp(entry.timestamp)
85
+ const msg = Array.isArray(entry.args)
86
+ ? entry.args
87
+ .map((value) =>
88
+ typeof value === 'object' ? JSON.stringify(value) : String(value),
89
+ )
90
+ .join(' ')
91
+ : String(entry)
92
+ console.log(` [${time}] ${msg}`)
93
+ }
94
+ }
95
+
96
+ export async function printBridgeRequestSummary(
97
+ bridge: WsBridge,
98
+ opts: RequestSummaryOptions = {},
99
+ ) {
100
+ const counts = await callTestBridge<{ failed?: number; total?: number }>(
101
+ bridge,
102
+ 'getRequestCounts',
103
+ )
104
+ if (!counts || typeof counts !== 'object') return
105
+
106
+ const failed = Math.max(0, Number(counts.failed) || 0)
107
+ if (failed === 0) return
108
+
109
+ console.log(`\n network: ${failed} failed request${failed === 1 ? '' : 's'}`)
110
+ if (opts.requestsCommand) console.log(` inspect: ${opts.requestsCommand}`)
111
+
112
+ if (!opts.includeTail) return
113
+
114
+ const recentFailed = await callTestBridge<SootSimRequestEntry[]>(
115
+ bridge,
116
+ 'getFailedRequests',
117
+ 5,
118
+ )
119
+ if (!Array.isArray(recentFailed) || recentFailed.length === 0) return
120
+
121
+ console.log('\n recent failed requests:\n')
122
+ for (const entry of recentFailed) {
123
+ const time = formatLogTimestamp(entry.timestamp)
124
+ console.log(` [${time}] ${formatRequestMessage(entry)}`)
125
+ if (entry.responseBody) {
126
+ console.log(` ${entry.responseBody}`)
127
+ } else if (entry.error) {
128
+ console.log(` ${entry.error}`)
129
+ }
130
+ }
131
+ }
132
+
133
+ interface BridgeStateSummary {
134
+ url?: string
135
+ simId?: string | null
136
+ nodeCount?: number
137
+ mode?: string
138
+ shell?: {
139
+ state?: string | null
140
+ activeApp?: string | null
141
+ showSwitcher?: boolean
142
+ switcherPhase?: string | null
143
+ } | null
144
+ }
145
+
146
+ export async function printBridgeStateSummary(bridge: WsBridge) {
147
+ const state = (await bridge.send({
148
+ type: 'evaluate',
149
+ code: `(async () => {
150
+ const test = window.__sootsimTest
151
+ const mainShell = window.SootSim?.bridges?.mainShell
152
+ let shell = null
153
+ try {
154
+ shell = typeof mainShell?.getState === 'function' ? await mainShell.getState() : null
155
+ } catch {}
156
+ let nodeCount = 0
157
+ try {
158
+ nodeCount = typeof test?.getNodeCount === 'function' ? await test.getNodeCount() : 0
159
+ } catch {}
160
+ return {
161
+ url: window.location.href,
162
+ simId: window.__sootsimSimId || null,
163
+ nodeCount,
164
+ mode: window.__sootsimRenderHost ? 'render-worker' : 'main-thread',
165
+ shell: shell
166
+ ? {
167
+ state: shell.state || null,
168
+ activeApp: shell.activeApp || null,
169
+ showSwitcher: !!shell.showSwitcher,
170
+ switcherPhase: shell.switcherPhase || null,
171
+ }
172
+ : null,
173
+ }
174
+ })()`,
175
+ })) as BridgeStateSummary | null
176
+
177
+ if (!state || typeof state !== 'object') return
178
+
179
+ console.log('\n state:')
180
+ if (state.simId) console.log(` sim: ${state.simId}`)
181
+ if (state.url) console.log(` url: ${state.url}`)
182
+ if (state.mode) console.log(` mode: ${state.mode}`)
183
+ const nodeCount = Number(state.nodeCount) || 0
184
+ console.log(` nodes: ${nodeCount}${nodeCount > 10 ? ' (ready)' : ' (not ready)'}`)
185
+
186
+ const shell = state.shell
187
+ if (shell && typeof shell === 'object') {
188
+ const shellParts = [
189
+ shell.state ? `state=${shell.state}` : null,
190
+ shell.activeApp ? `app=${shell.activeApp}` : null,
191
+ shell.showSwitcher ? 'switcher=open' : null,
192
+ shell.switcherPhase ? `phase=${shell.switcherPhase}` : null,
193
+ ].filter(Boolean)
194
+ if (shellParts.length > 0) {
195
+ console.log(` shell: ${shellParts.join(' ')}`)
196
+ }
197
+ }
198
+ }
199
+
200
+ export async function printBridgeFailureDiagnostics(
201
+ bridge: WsBridge,
202
+ opts: FailureDiagnosticsOptions = {},
203
+ ) {
204
+ try {
205
+ await printBridgeStateSummary(bridge)
206
+ } catch {
207
+ // ignore state snapshot failures while already handling a failure
208
+ }
209
+ try {
210
+ await printBridgeConsoleSummary(bridge, {
211
+ includeTail: true,
212
+ errorsCommand: opts.errorsCommand,
213
+ warningsCommand: opts.warningsCommand,
214
+ })
215
+ } catch {
216
+ // ignore console snapshot failures while already handling a failure
217
+ }
218
+ try {
219
+ await printBridgeRequestSummary(bridge, {
220
+ includeTail: true,
221
+ requestsCommand: opts.requestsCommand,
222
+ })
223
+ } catch {
224
+ // ignore request snapshot failures while already handling a failure
225
+ }
226
+ }