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,315 @@
1
+ import { execFileSync, spawn } from 'child_process'
2
+ import { existsSync, readdirSync } from 'fs'
3
+ import { dirname, join, resolve } from 'path'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ // resolved lazily — sootsim-engine is a workspace package in dev, but absent
7
+ // from the published npm tarball. don't crash module load just because the
8
+ // engine package isn't installed; commands that don't need the dev-electron
9
+ // path (most of them) shouldn't care.
10
+ let _sootsimRoot: string | null | undefined
11
+ function getSootsimRoot(): string | null {
12
+ if (_sootsimRoot !== undefined) return _sootsimRoot
13
+ try {
14
+ _sootsimRoot = dirname(
15
+ fileURLToPath(import.meta.resolve('sootsim-engine/package.json')),
16
+ )
17
+ } catch {
18
+ _sootsimRoot = null
19
+ }
20
+ return _sootsimRoot
21
+ }
22
+ const SOOTSIM_BUNDLE_ID = 'dev.sootsim.simulator'
23
+
24
+ export interface DesktopCompanionInstall {
25
+ path: string
26
+ platform: NodeJS.Platform
27
+ kind: 'mac-app' | 'linux-appimage' | 'linux-binary' | 'win-exe' | 'dev-electron'
28
+ // present for dev-electron: the sootsim-engine dir that electron treats
29
+ // as the app root (its package.json "main" points at dist-electron/main.cjs).
30
+ engineDir?: string
31
+ }
32
+
33
+ export interface DesktopCompanionLaunchResult {
34
+ launched: boolean
35
+ via?: 'mac-app' | 'linux-appimage' | 'linux-binary' | 'win-exe' | 'dev-electron'
36
+ target?: string
37
+ }
38
+
39
+ // dev-electron: when running from inside the Contrast checkout with sootsim-engine
40
+ // built and the electron dev dep present, prefer launching the local unpacked
41
+ // electron instead of the stale packaged /Applications/rnx.app.
42
+ //
43
+ // rationale: in dev we want electron to hit http://localhost:5173
44
+ // (src-electron/main.ts does this when !app.isPackaged).
45
+ // using the local `node_modules/.bin/electron` binary against the engine dir
46
+ // gives us that: isPackaged=false → loads the live dev server.
47
+ function findDevElectron(): DesktopCompanionInstall | null {
48
+ // engine dir must have a built main.cjs for electron to boot. the dev build
49
+ // chain (`bun run dev:electron` / `bun run build:electron-main`) produces
50
+ // this; bun dev keeps it fresh via a one-shot step added to run-all.
51
+ const engineDir = getSootsimRoot()
52
+ if (!engineDir) return null
53
+ const engineMain = join(engineDir, 'dist-electron/main.cjs')
54
+ if (!existsSync(engineMain)) return null
55
+
56
+ // walk up from engine dir looking for a node_modules/.bin/electron.
57
+ // the repo layout is packages/sootsim-engine next to a root node_modules,
58
+ // but also works when installed as a dependency.
59
+ const electronBin = findElectronBin(engineDir)
60
+ if (!electronBin) return null
61
+
62
+ return {
63
+ path: electronBin,
64
+ platform: process.platform,
65
+ kind: 'dev-electron',
66
+ engineDir,
67
+ }
68
+ }
69
+
70
+ // detect whether the sootsim-engine we resolved is the in-repo workspace
71
+ // checkout (has src-electron/main.ts) vs something pulled in as a dep.
72
+ // this matters because repo development has one supported path: dev electron
73
+ // against the live :5173 renderer.
74
+ function isInRepoCheckout(): boolean {
75
+ const root = getSootsimRoot()
76
+ if (!root) return false
77
+ return existsSync(join(root, 'src-electron/main.ts'))
78
+ }
79
+
80
+ function findElectronBin(startDir: string): string | null {
81
+ let dir = startDir
82
+ for (let i = 0; i < 6; i++) {
83
+ const candidate = join(dir, 'node_modules/.bin/electron')
84
+ if (existsSync(candidate)) return candidate
85
+ const parent = dirname(dir)
86
+ if (parent === dir) break
87
+ dir = parent
88
+ }
89
+ return null
90
+ }
91
+
92
+ function findMacApp(): DesktopCompanionInstall | null {
93
+ const root = getSootsimRoot()
94
+ const candidates = [
95
+ '/Applications/rnx.app',
96
+ resolve(process.env.HOME || '', 'Applications/rnx.app'),
97
+ ...(root ? [resolve(root, 'app/rnx.app')] : []),
98
+ ]
99
+
100
+ const found = candidates.find((candidate) => existsSync(candidate))
101
+ if (found) {
102
+ return {
103
+ path: found,
104
+ platform: 'darwin',
105
+ kind: 'mac-app',
106
+ }
107
+ }
108
+
109
+ try {
110
+ const result = execFileSync(
111
+ 'mdfind',
112
+ [`kMDItemCFBundleIdentifier == "${SOOTSIM_BUNDLE_ID}"`],
113
+ { encoding: 'utf8', timeout: 3000 },
114
+ ).trim()
115
+ if (result) {
116
+ return {
117
+ path: result.split('\n')[0],
118
+ platform: 'darwin',
119
+ kind: 'mac-app',
120
+ }
121
+ }
122
+ } catch {}
123
+
124
+ return null
125
+ }
126
+
127
+ function findFirstAppImage(dir: string): string | null {
128
+ if (!existsSync(dir)) return null
129
+ const match = readdirSync(dir)
130
+ .filter((entry) => entry.startsWith('rnx') && entry.endsWith('.AppImage'))
131
+ .sort()
132
+ .reverse()[0]
133
+ return match ? join(dir, match) : null
134
+ }
135
+
136
+ function findLinuxInstall(): DesktopCompanionInstall | null {
137
+ const root = getSootsimRoot()
138
+ const directCandidates = [
139
+ resolve(process.env.HOME || '', 'Applications/rnx.AppImage'),
140
+ resolve(process.env.HOME || '', '.local/bin/rnx.AppImage'),
141
+ '/opt/rnx/rnx.AppImage',
142
+ '/opt/rnx/rnx',
143
+ ...(root
144
+ ? [resolve(root, 'app/rnx.AppImage'), resolve(root, 'release/linux-unpacked/rnx')]
145
+ : []),
146
+ ]
147
+
148
+ const directMatch = directCandidates.find((candidate) => existsSync(candidate))
149
+ if (directMatch) {
150
+ return {
151
+ path: directMatch,
152
+ platform: 'linux',
153
+ kind: directMatch.endsWith('.AppImage') ? 'linux-appimage' : 'linux-binary',
154
+ }
155
+ }
156
+
157
+ const scannedMatch = root
158
+ ? findFirstAppImage(resolve(root, 'app')) ||
159
+ findFirstAppImage(resolve(root, 'release'))
160
+ : null
161
+
162
+ if (!scannedMatch) return null
163
+
164
+ return {
165
+ path: scannedMatch,
166
+ platform: 'linux',
167
+ kind: 'linux-appimage',
168
+ }
169
+ }
170
+
171
+ function findWindowsInstall(): DesktopCompanionInstall | null {
172
+ for (const hive of ['HKCU', 'HKLM']) {
173
+ try {
174
+ const output = execFileSync(
175
+ 'reg',
176
+ ['query', `${hive}\\Software\\Classes\\rnx\\shell\\open\\command`, '/ve'],
177
+ { encoding: 'utf8', timeout: 3000 },
178
+ )
179
+ const executable = /"([^"\r\n]+\.exe)"/i.exec(output)?.[1]
180
+ if (executable && existsSync(executable)) {
181
+ return { path: executable, platform: 'win32', kind: 'win-exe' }
182
+ }
183
+ } catch {}
184
+ }
185
+ return null
186
+ }
187
+
188
+ export function findDesktopCompanion(): DesktopCompanionInstall | null {
189
+ const dev = findDevElectron()
190
+ if (dev) return dev
191
+ if (isInRepoCheckout()) {
192
+ const engineDir = getSootsimRoot()!
193
+ const engineMain = join(engineDir, 'dist-electron/main.cjs')
194
+ if (!existsSync(engineMain)) {
195
+ console.error(
196
+ ` dist-electron/main.cjs missing in ${engineDir}.\n` +
197
+ ` run: bun run --cwd packages/sootsim-engine build:electron-main\n` +
198
+ ` (or keep it fresh with: bun run watch:sootsim:electron-main)`,
199
+ )
200
+ } else {
201
+ console.error(
202
+ ` no node_modules/.bin/electron found near ${engineDir}. run \`bun install\`.`,
203
+ )
204
+ }
205
+ return null
206
+ }
207
+ if (process.platform === 'darwin') return findMacApp()
208
+ if (process.platform === 'linux') return findLinuxInstall()
209
+ if (process.platform === 'win32') return findWindowsInstall()
210
+ return null
211
+ }
212
+
213
+ async function spawnDetached(
214
+ command: string,
215
+ args: string[],
216
+ opts: { inheritStdio?: boolean } = {},
217
+ ) {
218
+ // dev-electron streams main-process console.log + renderer crash dumps to
219
+ // the terminal so the developer sees them. inherit stdout/stderr but
220
+ // ignore stdin so the child doesn't fight the shell for keystrokes.
221
+ // detached + unref means the CLI still returns the prompt immediately;
222
+ // electron keeps writing to the inherited fds until the user quits it
223
+ // or closes the terminal.
224
+ const stdio: 'ignore' | ['ignore', 'inherit', 'inherit'] = opts.inheritStdio
225
+ ? ['ignore', 'inherit', 'inherit']
226
+ : 'ignore'
227
+ await new Promise<void>((resolve, reject) => {
228
+ const child = spawn(command, args, { detached: true, stdio })
229
+ child.once('error', reject)
230
+ child.once('spawn', () => {
231
+ child.unref()
232
+ resolve()
233
+ })
234
+ })
235
+ }
236
+
237
+ export interface LaunchCompanionOptions {
238
+ // seeds the initial device model for the first-created window. wins
239
+ // over any persisted window state; read by main.ts readCliDeviceOverride.
240
+ device?: string
241
+ profileId?: string
242
+ ephemeralProfile?: boolean
243
+ // set by `rnx open`, whose native window belongs to the launching
244
+ // session. `rnx desktop` omits it and remains a persistent desktop app.
245
+ ownerPid?: number
246
+ }
247
+
248
+ export async function launchDesktopCompanion(
249
+ url?: string,
250
+ install = findDesktopCompanion(),
251
+ opts: LaunchCompanionOptions = {},
252
+ ): Promise<DesktopCompanionLaunchResult> {
253
+ if (!install) return { launched: false }
254
+
255
+ // electron argv extra: main.ts reads --device <model> from process.argv
256
+ // and uses it as the seed for the first createWindow.
257
+ //
258
+ // ⚠ chromium reorders argv before electron's main handler (and before any
259
+ // `second-instance` event) sees it: `--flag value` becomes the bare switch
260
+ // `--flag` followed by `value` as a stray positional. it also injects its
261
+ // own switches (e.g. `--allow-file-access-from-files`) between the no-value
262
+ // flags, so the original pairing is lost entirely. anything that takes a
263
+ // value MUST use the `--flag=value` single-arg form so chromium preserves
264
+ // it intact. ephemeral-profile has no value so it survives as-is.
265
+ const extraArgs: string[] = []
266
+ if (opts.device) extraArgs.push(`--device=${opts.device}`)
267
+ if (opts.profileId) extraArgs.push(`--profile=${opts.profileId}`)
268
+ if (opts.ephemeralProfile) extraArgs.push('--ephemeral-profile')
269
+ if (opts.ownerPid && opts.ownerPid > 1) extraArgs.push(`--owner-pid=${opts.ownerPid}`)
270
+
271
+ if (install.kind === 'mac-app') {
272
+ // `open -a` splits app args before and after `--args`. anything after
273
+ // --args is forwarded to electron's process.argv.
274
+ const openArgs: string[] = ['-g', '-a', install.path]
275
+ if (url) openArgs.push(buildMacLaunchUrl(url, opts))
276
+ if (extraArgs.length > 0) openArgs.push('--args', ...extraArgs)
277
+ await spawnDetached('open', openArgs)
278
+ return { launched: true, via: 'mac-app', target: install.path }
279
+ }
280
+
281
+ if (install.kind === 'dev-electron') {
282
+ // electron <app-dir> loads the dir's package.json "main" (dist-electron/main.cjs).
283
+ // main.ts checks app.isPackaged and loads http://localhost:5173 in dev.
284
+ // local dev shells commonly run under sandbox-exec/safehouse. chromium's
285
+ // own sandbox cannot initialize inside that outer sandbox, so pass the
286
+ // standard electron switch for the unpacked dev app only.
287
+ const args: string[] = ['--no-sandbox', install.engineDir || getSootsimRoot()!]
288
+ if (url) args.push(url)
289
+ if (extraArgs.length > 0) args.push(...extraArgs)
290
+ await spawnDetached(install.path, args, { inheritStdio: true })
291
+ return { launched: true, via: 'dev-electron', target: install.path }
292
+ }
293
+
294
+ const binaryArgs: string[] = []
295
+ if (url) binaryArgs.push(url)
296
+ if (extraArgs.length > 0) binaryArgs.push(...extraArgs)
297
+ await spawnDetached(install.path, binaryArgs)
298
+ return {
299
+ launched: true,
300
+ via: install.kind,
301
+ target: install.path,
302
+ }
303
+ }
304
+
305
+ function buildMacLaunchUrl(url: string, opts: LaunchCompanionOptions): string {
306
+ if (!opts.profileId && !opts.ephemeralProfile && !opts.ownerPid) return url
307
+ const deepLink = new URL('rnx://dev')
308
+ deepLink.searchParams.set('url', url)
309
+ if (opts.profileId) deepLink.searchParams.set('profile', opts.profileId)
310
+ if (opts.ephemeralProfile) deepLink.searchParams.set('ephemeral', '1')
311
+ if (opts.ownerPid && opts.ownerPid > 1) {
312
+ deepLink.searchParams.set('ownerPid', String(opts.ownerPid))
313
+ }
314
+ return deepLink.toString()
315
+ }
@@ -0,0 +1,70 @@
1
+ // electron driver — wraps the existing desktop-companion helper. availability
2
+ // is "is the rnx desktop app installed on this machine". launch delegates
3
+ // to launchDesktopCompanion, which already handles mac `open -a`, linux
4
+ // AppImage, and the --device seed.
5
+
6
+ import { findDesktopCompanion, launchDesktopCompanion } from '../desktop-companion'
7
+ import type {
8
+ Driver,
9
+ DriverAvailability,
10
+ DriverLaunchOptions,
11
+ DriverLaunchResult,
12
+ } from './types'
13
+
14
+ function probe(): DriverAvailability {
15
+ const install = findDesktopCompanion()
16
+ if (!install) {
17
+ return {
18
+ available: false,
19
+ reason: 'rnx desktop app not installed (run `rnx desktop install`)',
20
+ }
21
+ }
22
+ return {
23
+ available: true,
24
+ reason: null,
25
+ detail: `${install.kind} @ ${install.path}`,
26
+ }
27
+ }
28
+
29
+ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
30
+ const install = findDesktopCompanion()
31
+ if (!install) {
32
+ return {
33
+ launched: false,
34
+ message: 'rnx desktop app not installed',
35
+ }
36
+ }
37
+ try {
38
+ const result = await launchDesktopCompanion(opts.url, install, {
39
+ device: opts.device,
40
+ profileId: opts.profileId,
41
+ ephemeralProfile: opts.ephemeralProfile,
42
+ ownerPid: opts.ownerPid,
43
+ })
44
+ if (!result.launched) {
45
+ return { launched: false, message: 'desktop companion failed to start' }
46
+ }
47
+ return {
48
+ launched: true,
49
+ message: opts.url
50
+ ? `electron launched via ${result.via} → ${opts.url}`
51
+ : `electron launched via ${result.via}`,
52
+ target: result.target,
53
+ attachUrl: opts.url,
54
+ }
55
+ } catch (err) {
56
+ return {
57
+ launched: false,
58
+ message: `electron launch failed: ${err instanceof Error ? err.message : String(err)}`,
59
+ }
60
+ }
61
+ }
62
+
63
+ export const electronDriver: Driver = {
64
+ id: 'electron',
65
+ name: 'electron',
66
+ description: 'rnx desktop companion app (native window, menu bar)',
67
+ kind: 'native',
68
+ availability: probe,
69
+ launch,
70
+ }
@@ -0,0 +1,20 @@
1
+ // sootsim driver registry — public entry.
2
+
3
+ export type {
4
+ Driver,
5
+ DriverId,
6
+ DriverKind,
7
+ DriverAvailability,
8
+ DriverLaunchOptions,
9
+ DriverLaunchResult,
10
+ } from './types'
11
+ export {
12
+ ALL_DRIVERS,
13
+ buildDriverListRows,
14
+ getAllDrivers,
15
+ getDriver,
16
+ resolveDriver,
17
+ type DriverListRow,
18
+ } from './registry'
19
+ export { electronDriver } from './electron'
20
+ export { playwrightDriver } from './playwright'
@@ -0,0 +1,184 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { existsSync, readFileSync } from 'node:fs'
3
+ import { createRequire } from 'node:module'
4
+ import { dirname, join } from 'node:path'
5
+ import { RNX_INTERNAL_CHILDREN, RNX_INTERNAL_COMMAND } from '../internal-child'
6
+ import { rnxSelfInvocation } from '../self-invocation'
7
+
8
+ export interface ResolvedPlaywright {
9
+ // package name that resolved (for human-facing messaging)
10
+ spec: string
11
+ // absolute package entry loaded by the detached browser host
12
+ modulePath: string
13
+ // absolute package-owned CLI entry used to install this package's browser
14
+ cliPath: string
15
+ // package version shown in availability and provisioning failures
16
+ version: string
17
+ }
18
+
19
+ export type PlaywrightBrowserProvisioningResult =
20
+ | {
21
+ ok: true
22
+ executablePath: string
23
+ installed: boolean
24
+ }
25
+ | {
26
+ ok: false
27
+ message: string
28
+ }
29
+
30
+ function isRecord(value: unknown): value is Record<string, unknown> {
31
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
32
+ }
33
+
34
+ export function resolvePlaywright(): ResolvedPlaywright | null {
35
+ // resolve from several roots, not just cwd. a user can invoke a global CLI
36
+ // from an arbitrary directory while Playwright is installed beside the CLI.
37
+ const roots = [`${process.cwd()}/`]
38
+ if (process.argv[1]) roots.push(process.argv[1])
39
+ for (const root of roots) {
40
+ try {
41
+ const requireFromRoot = createRequire(root)
42
+ for (const spec of ['playwright', 'playwright-chromium', 'playwright-core']) {
43
+ try {
44
+ const modulePath = requireFromRoot.resolve(spec)
45
+ const packageJsonPath = requireFromRoot.resolve(`${spec}/package.json`)
46
+ const parsed: unknown = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
47
+ if (!isRecord(parsed) || typeof parsed.version !== 'string') continue
48
+ const bin = parsed.bin
49
+ let binPath: string | null = null
50
+ if (typeof bin === 'string') {
51
+ binPath = bin
52
+ } else if (isRecord(bin)) {
53
+ const named = bin.playwright ?? bin['playwright-core']
54
+ const first = Object.values(bin).find((value) => typeof value === 'string')
55
+ binPath =
56
+ typeof named === 'string' ? named : typeof first === 'string' ? first : null
57
+ }
58
+ if (!binPath) continue
59
+ const cliPath = join(dirname(packageJsonPath), binPath)
60
+ if (!existsSync(cliPath)) continue
61
+ return {
62
+ spec,
63
+ modulePath,
64
+ cliPath,
65
+ version: parsed.version,
66
+ }
67
+ } catch {}
68
+ }
69
+ } catch {}
70
+ }
71
+ return null
72
+ }
73
+
74
+ type BrowserProbeResult =
75
+ | { ok: true; executablePath: string; exists: boolean }
76
+ | { ok: false; message: string }
77
+
78
+ function probeBrowser(
79
+ resolved: ResolvedPlaywright,
80
+ env: NodeJS.ProcessEnv,
81
+ ): BrowserProbeResult {
82
+ const self = rnxSelfInvocation()
83
+ const probe = spawnSync(
84
+ self.executable,
85
+ [...self.prefixArgs, RNX_INTERNAL_COMMAND, RNX_INTERNAL_CHILDREN.playwrightProbe],
86
+ {
87
+ env: {
88
+ ...env,
89
+ SOOTSIM_PW_MODULE: resolved.modulePath,
90
+ },
91
+ encoding: 'utf8',
92
+ timeout: 30_000,
93
+ },
94
+ )
95
+ if (probe.status !== 0) {
96
+ const detail = (probe.stderr || probe.error?.message || 'browser probe failed').trim()
97
+ return {
98
+ ok: false,
99
+ message: `could not inspect ${resolved.spec}@${resolved.version} chromium: ${detail}`,
100
+ }
101
+ }
102
+ try {
103
+ const parsed: unknown = JSON.parse(probe.stdout)
104
+ if (
105
+ !isRecord(parsed) ||
106
+ typeof parsed.executablePath !== 'string' ||
107
+ typeof parsed.exists !== 'boolean'
108
+ ) {
109
+ throw new Error('probe returned an invalid result')
110
+ }
111
+ return {
112
+ ok: true,
113
+ executablePath: parsed.executablePath,
114
+ exists: parsed.exists && existsSync(parsed.executablePath),
115
+ }
116
+ } catch (error) {
117
+ return {
118
+ ok: false,
119
+ message: `could not inspect ${resolved.spec}@${resolved.version} chromium: ${
120
+ error instanceof Error ? error.message : String(error)
121
+ }`,
122
+ }
123
+ }
124
+ }
125
+
126
+ export function ensurePlaywrightBrowser(
127
+ resolved: ResolvedPlaywright,
128
+ env: NodeJS.ProcessEnv = process.env,
129
+ ): PlaywrightBrowserProvisioningResult {
130
+ const before = probeBrowser(resolved, env)
131
+ if (!before.ok) return before
132
+ if (before.exists) {
133
+ return { ok: true, executablePath: before.executablePath, installed: false }
134
+ }
135
+
136
+ // the resolved package is the authority for both the required executable
137
+ // path and the installer. never scan a shared cache or select a neighboring
138
+ // revision: Playwright's own CLI installs the exact revision its API named.
139
+ // that CLI is a javascript file, so this CLI runs it as itself.
140
+ const installer = rnxSelfInvocation()
141
+ const install = spawnSync(
142
+ installer.executable,
143
+ [
144
+ ...installer.prefixArgs,
145
+ RNX_INTERNAL_COMMAND,
146
+ RNX_INTERNAL_CHILDREN.playwrightInstall,
147
+ resolved.cliPath,
148
+ 'install',
149
+ 'chromium',
150
+ ],
151
+ {
152
+ env,
153
+ encoding: 'utf8',
154
+ timeout: 10 * 60_000,
155
+ },
156
+ )
157
+ if (install.status !== 0) {
158
+ const rawDetail = (
159
+ install.stderr ||
160
+ install.stdout ||
161
+ install.error?.message ||
162
+ 'browser install failed'
163
+ ).trim()
164
+ const detail = rawDetail.length <= 4_000 ? rawDetail : rawDetail.slice(-4_000)
165
+ return {
166
+ ok: false,
167
+ message:
168
+ `${resolved.spec}@${resolved.version} requires chromium at ` +
169
+ `${before.executablePath}, but its browser install failed${detail ? `:\n${detail}` : ''}`,
170
+ }
171
+ }
172
+
173
+ const after = probeBrowser(resolved, env)
174
+ if (!after.ok) return after
175
+ if (!after.exists) {
176
+ return {
177
+ ok: false,
178
+ message:
179
+ `${resolved.spec}@${resolved.version} browser install completed, but its required ` +
180
+ `chromium executable is still missing at ${after.executablePath}`,
181
+ }
182
+ }
183
+ return { ok: true, executablePath: after.executablePath, installed: true }
184
+ }