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,275 @@
1
+ // installs the standalone rnx executable into the user's command path.
2
+ //
3
+ // the public shell installer owns download and checksum verification. this
4
+ // command mirrors its local mutation after those bytes are trusted, while the
5
+ // desktop app copies its bundled executable without a network download.
6
+
7
+ import { spawnSync } from 'node:child_process'
8
+ import {
9
+ appendFileSync,
10
+ chmodSync,
11
+ copyFileSync,
12
+ existsSync,
13
+ mkdirSync,
14
+ readFileSync,
15
+ renameSync,
16
+ unlinkSync,
17
+ } from 'node:fs'
18
+ import { createServer } from 'node:net'
19
+ import { homedir } from 'node:os'
20
+ import { dirname, join } from 'node:path'
21
+ import { isDaemonLockfileFresh, readDaemonLockfile } from '../../src/home-paths'
22
+ import { IS_STANDALONE } from '../standalone'
23
+
24
+ const INSTALL_WAIT_MS = 10_000
25
+
26
+ type CliInstallOptions = {
27
+ alreadyInstalled: boolean
28
+ handoffPid: number | null
29
+ port: number | null
30
+ }
31
+
32
+ export async function runCliInstall(args: string[]): Promise<void> {
33
+ const options = parseCliInstallOptions(args)
34
+ if (!IS_STANDALONE) {
35
+ throw new Error('rnx cli install requires the standalone rnx executable')
36
+ }
37
+
38
+ const target = installedCliPath()
39
+ if (!options.alreadyInstalled) installCurrentExecutable(target)
40
+ else if (!existsSync(target)) {
41
+ throw new Error(`rnx cli install expected ${target} to exist`)
42
+ }
43
+
44
+ const profile = installCommandPath(target)
45
+ console.log(` command: ${target}`)
46
+ if (profile) console.log(` shell: ${profile}`)
47
+
48
+ if (options.handoffPid === null) return
49
+ if (process.platform === 'win32') {
50
+ console.log(' desktop daemon: remains app-owned on Windows')
51
+ return
52
+ }
53
+
54
+ await handOffDaemon({ target, pid: options.handoffPid, port: options.port })
55
+ }
56
+
57
+ function parseCliInstallOptions(args: string[]): CliInstallOptions {
58
+ const [subcommand] = args
59
+ if (subcommand === '--help' || subcommand === '-h') {
60
+ console.log(`\nrnx cli install - install the rnx command in your PATH
61
+
62
+ usage:
63
+ rnx cli install
64
+
65
+ internal options:
66
+ --installed update shell startup after an installer copied rnx
67
+ --handoff-pid <pid> replace this desktop-owned daemon with the service
68
+ --port <port> daemon bridge port to preserve during handoff
69
+ `)
70
+ process.exit(0)
71
+ }
72
+ if (subcommand !== 'install') {
73
+ throw new Error('usage: rnx cli install')
74
+ }
75
+
76
+ const handoffPid = readHandoffPid(args)
77
+ const port = readPort(args)
78
+ if (handoffPid !== null && port === null) {
79
+ throw new Error('--handoff-pid requires --port')
80
+ }
81
+ return {
82
+ alreadyInstalled: args.includes('--installed'),
83
+ handoffPid,
84
+ port,
85
+ }
86
+ }
87
+
88
+ function readHandoffPid(args: string[]): number | null {
89
+ const flag = '--handoff-pid'
90
+ const index = args.indexOf(flag)
91
+ if (index < 0) return null
92
+ const value = Number(args[index + 1])
93
+ if (!Number.isSafeInteger(value) || value < 2) {
94
+ throw new Error(`${flag} requires a process id`)
95
+ }
96
+ return value
97
+ }
98
+
99
+ function readPort(args: string[]): number | null {
100
+ const flag = '--port'
101
+ const index = args.indexOf(flag)
102
+ if (index < 0) return null
103
+ const value = Number(args[index + 1])
104
+ if (!Number.isInteger(value) || value < 1 || value > 65_535) {
105
+ throw new Error(`${flag} requires a port number`)
106
+ }
107
+ return value
108
+ }
109
+
110
+ function installedCliPath(): string {
111
+ const home = process.env.HOME || homedir()
112
+ const prefix = process.env.RNX_CLI_PREFIX || join(home, '.local')
113
+ return join(prefix, 'bin', process.platform === 'win32' ? 'rnx.exe' : 'rnx')
114
+ }
115
+
116
+ function installCurrentExecutable(target: string): void {
117
+ if (process.execPath === target) return
118
+ mkdirSync(dirname(target), { recursive: true })
119
+ const temporary = join(dirname(target), `.rnx-install-${process.pid}`)
120
+ try {
121
+ copyFileSync(process.execPath, temporary)
122
+ chmodSync(temporary, 0o755)
123
+ renameSync(temporary, target)
124
+ } finally {
125
+ try {
126
+ unlinkSync(temporary)
127
+ } catch {}
128
+ }
129
+ }
130
+
131
+ function installCommandPath(target: string): string | null {
132
+ if (process.platform === 'win32') {
133
+ const bin = dirname(target)
134
+ const pathValue = process.env.Path || process.env.PATH || ''
135
+ if (
136
+ !pathValue.split(';').some((entry) => entry.toLowerCase() === bin.toLowerCase())
137
+ ) {
138
+ const next = pathValue ? `${bin};${pathValue}` : bin
139
+ const result = spawnSync(
140
+ 'reg.exe',
141
+ [
142
+ 'add',
143
+ 'HKCU\\Environment',
144
+ '/v',
145
+ 'Path',
146
+ '/t',
147
+ 'REG_EXPAND_SZ',
148
+ '/d',
149
+ next,
150
+ '/f',
151
+ ],
152
+ { encoding: 'utf8' },
153
+ )
154
+ if (result.status !== 0) {
155
+ throw new Error(`could not add rnx to the user PATH: ${result.stderr.trim()}`)
156
+ }
157
+ }
158
+ return 'the user PATH'
159
+ }
160
+
161
+ const home = process.env.HOME || homedir()
162
+ const shell = process.env.SHELL || ''
163
+ const shellName = shell.endsWith('/zsh') ? 'zsh' : shell.endsWith('/bash') ? 'bash' : ''
164
+ const profile = join(
165
+ home,
166
+ shellName === 'zsh' ? '.zshrc' : shellName === 'bash' ? '.bashrc' : '.profile',
167
+ )
168
+ const bin = dirname(target)
169
+ const pathLine = `export PATH="${bin}:$PATH"`
170
+ const integration = shellName ? `eval "$(command rnx __shell-init ${shellName})"` : ''
171
+ const current = existsSync(profile) ? readFileSync(profile, 'utf8') : ''
172
+ if (current.includes(pathLine) && (!integration || current.includes(integration)))
173
+ return null
174
+
175
+ const additions = [
176
+ '',
177
+ '# added by the rnx installer; delete this block to remove rnx from your shell',
178
+ ...(current.includes(pathLine) ? [] : [pathLine]),
179
+ ...(integration && !current.includes(integration) ? [integration] : []),
180
+ ]
181
+ appendFileSync(profile, `${additions.join('\n')}\n`)
182
+ return profile
183
+ }
184
+
185
+ async function handOffDaemon({
186
+ target,
187
+ pid,
188
+ port,
189
+ }: {
190
+ target: string
191
+ pid: number
192
+ port: number | null
193
+ }): Promise<void> {
194
+ if (port === null) throw new Error('the desktop daemon did not report its port')
195
+ const deadline = Date.now() + INSTALL_WAIT_MS
196
+ try {
197
+ process.kill(pid, 'SIGTERM')
198
+ } catch (error) {
199
+ const code = error && typeof error === 'object' ? Reflect.get(error, 'code') : null
200
+ if (code !== 'ESRCH') throw error
201
+ }
202
+ await waitFor(
203
+ () => {
204
+ const lock = readDaemonLockfile()
205
+ return !isDaemonLockfileFresh(lock) || lock.pid !== pid
206
+ },
207
+ 'the desktop daemon to stop',
208
+ deadline,
209
+ )
210
+ await waitForPortToClose(port, deadline)
211
+
212
+ const result = spawnSync(
213
+ target,
214
+ ['--port', String(port), 'daemon', 'install', '--force'],
215
+ {
216
+ encoding: 'utf8',
217
+ timeout: Math.max(1, deadline - Date.now()),
218
+ env: {
219
+ ...process.env,
220
+ SOOTSIM_FORCE_DAEMON_INSTALL: '1',
221
+ },
222
+ },
223
+ )
224
+ if (result.status !== 0) {
225
+ throw new Error(
226
+ `could not register the rnx daemon: ${(result.stderr || result.stdout || 'unknown error').trim()}`,
227
+ )
228
+ }
229
+
230
+ await waitFor(
231
+ () => {
232
+ const lock = readDaemonLockfile()
233
+ return isDaemonLockfileFresh(lock) && lock.pid !== pid && lock.bridgePort === port
234
+ },
235
+ 'the installed rnx daemon to start',
236
+ deadline,
237
+ )
238
+ console.log(' desktop daemon: handed off to the installed rnx command')
239
+ }
240
+
241
+ async function waitFor(
242
+ condition: () => boolean,
243
+ description: string,
244
+ deadline: number,
245
+ ): Promise<void> {
246
+ while (Date.now() < deadline) {
247
+ if (condition()) return
248
+ await new Promise((resolve) => setTimeout(resolve, 100))
249
+ }
250
+ throw new Error(`timed out waiting for ${description}`)
251
+ }
252
+
253
+ async function waitForPortToClose(port: number, deadline: number): Promise<void> {
254
+ while (Date.now() < deadline) {
255
+ const available = await new Promise<boolean>((resolve, reject) => {
256
+ const server = createServer()
257
+ server.once('error', (error) => {
258
+ if ('code' in error && error.code === 'EADDRINUSE') {
259
+ resolve(false)
260
+ return
261
+ }
262
+ reject(error)
263
+ })
264
+ server.listen(port, '127.0.0.1', () => {
265
+ server.close((error) => {
266
+ if (error) reject(error)
267
+ else resolve(true)
268
+ })
269
+ })
270
+ })
271
+ if (available) return
272
+ await new Promise((resolve) => setTimeout(resolve, 100))
273
+ }
274
+ throw new Error(`timed out waiting for port ${port} to close`)
275
+ }
@@ -0,0 +1,385 @@
1
+ // rnx desktop install — download and install the optional desktop GUI
2
+ //
3
+ // the CLI + daemon are the canonical rnx surface; this command is only
4
+ // for users who want the Electron-based GUI companion on top of that.
5
+ //
6
+ // fetches the right platform artifact from https://contrast.dev/api/electron-release/
7
+ // and installs it into the OS-native location that findDesktopCompanion() looks for:
8
+ // macos: ~/Applications/rnx.app (or /Applications if writable)
9
+ // linux: ~/Applications/rnx.AppImage
10
+ // win: spawns the NSIS installer
11
+ //
12
+ // post-install the electron-updater feed (same URL) handles upgrades inside the
13
+ // running app, so this command is a first-install bootstrap, not an update path.
14
+
15
+ import { spawn, spawnSync } from 'child_process'
16
+ import {
17
+ chmodSync,
18
+ copyFileSync,
19
+ createWriteStream,
20
+ existsSync,
21
+ mkdirSync,
22
+ rmSync,
23
+ statSync,
24
+ unlinkSync,
25
+ } from 'fs'
26
+ import { tmpdir } from 'os'
27
+ import { dirname, join, resolve } from 'path'
28
+ import { findDesktopCompanion } from '../desktop-companion'
29
+ import { confirm } from '../prompt'
30
+ import { rnxExit } from '../run-rnx'
31
+ import { accent, bold, failure, muted, success, withSpinner } from '../ui'
32
+
33
+ const DOWNLOAD_BASE = 'https://contrast.dev/api/electron-release'
34
+
35
+ type TargetPlatform = 'mac-arm64' | 'mac-x64' | 'win-x64' | 'linux-x64' | 'linux-arm64'
36
+
37
+ interface Target {
38
+ platform: TargetPlatform
39
+ filename: string
40
+ url: string
41
+ install: (downloaded: string) => Promise<string>
42
+ }
43
+
44
+ function detectTarget(): Target | null {
45
+ const arch = process.arch
46
+ if (process.platform === 'darwin') {
47
+ const platform: TargetPlatform = arch === 'arm64' ? 'mac-arm64' : 'mac-x64'
48
+ const filename = `sootsim-latest-${platform}.dmg`
49
+ return {
50
+ platform,
51
+ filename,
52
+ url: `${DOWNLOAD_BASE}/${filename}`,
53
+ install: installMacDmg,
54
+ }
55
+ }
56
+ if (process.platform === 'linux') {
57
+ // ship arch-appropriate AppImages. on Apple Silicon Macs (the default
58
+ // host for orb/multipass VMs) the linux VM is arm64, so the x64
59
+ // AppImage fails at runtime with "Dynamic loader not found:
60
+ // /lib64/ld-linux-x86-64.so.2". detect the running arch and pick the
61
+ // matching artifact.
62
+ const platform: TargetPlatform = arch === 'arm64' ? 'linux-arm64' : 'linux-x64'
63
+ const filename = `sootsim-latest-${platform}.AppImage`
64
+ return {
65
+ platform,
66
+ filename,
67
+ url: `${DOWNLOAD_BASE}/${filename}`,
68
+ install: installLinuxAppImage,
69
+ }
70
+ }
71
+ if (process.platform === 'win32') {
72
+ const filename = `sootsim-latest-win-x64.exe`
73
+ return {
74
+ platform: 'win-x64',
75
+ filename,
76
+ url: `${DOWNLOAD_BASE}/${filename}`,
77
+ install: installWindowsExe,
78
+ }
79
+ }
80
+ return null
81
+ }
82
+
83
+ interface DownloadOptions {
84
+ url: string
85
+ dest: string
86
+ onProgress?: (received: number, total: number | null) => void
87
+ }
88
+
89
+ async function download({ url, dest, onProgress }: DownloadOptions): Promise<void> {
90
+ const response = await fetch(url, { redirect: 'follow' })
91
+ if (!response.ok) {
92
+ throw new Error(`${response.status} ${response.statusText} (${url})`)
93
+ }
94
+ const contentLength = response.headers.get('content-length')
95
+ const total = contentLength ? Number(contentLength) : null
96
+ if (!response.body) {
97
+ throw new Error(`empty response body (${url})`)
98
+ }
99
+
100
+ mkdirSync(dirname(dest), { recursive: true })
101
+ const file = createWriteStream(dest)
102
+ const reader = response.body.getReader()
103
+ let received = 0
104
+
105
+ try {
106
+ while (true) {
107
+ const { done, value } = await reader.read()
108
+ if (done) break
109
+ file.write(Buffer.from(value))
110
+ received += value.byteLength
111
+ onProgress?.(received, total)
112
+ }
113
+ } finally {
114
+ await new Promise<void>((resolve, reject) => {
115
+ file.end((err?: Error | null) => (err ? reject(err) : resolve()))
116
+ })
117
+ }
118
+ }
119
+
120
+ function renderProgress(received: number, total: number | null): string {
121
+ const mb = (received / (1024 * 1024)).toFixed(1)
122
+ if (!total) return ` ${mb} MB`
123
+ const pct = Math.min(100, Math.round((received / total) * 100))
124
+ const totalMb = (total / (1024 * 1024)).toFixed(1)
125
+ const barWidth = 24
126
+ const filled = Math.round((pct / 100) * barWidth)
127
+ const bar = accent('█'.repeat(filled)) + muted('░'.repeat(barWidth - filled))
128
+ return ` ${bar} ${pct}% ${mb} / ${totalMb} MB`
129
+ }
130
+
131
+ async function installMacDmg(dmgPath: string): Promise<string> {
132
+ const mountOutput = spawnSync(
133
+ 'hdiutil',
134
+ ['attach', '-nobrowse', '-readonly', dmgPath],
135
+ {
136
+ encoding: 'utf8',
137
+ },
138
+ )
139
+ if (mountOutput.status !== 0) {
140
+ throw new Error(
141
+ `hdiutil attach failed: ${mountOutput.stderr || mountOutput.stdout}`.trim(),
142
+ )
143
+ }
144
+ const mountLines = mountOutput.stdout.trim().split('\n')
145
+ const mountLine = mountLines[mountLines.length - 1]
146
+ const mountPoint = mountLine.split('\t').pop()?.trim()
147
+ if (!mountPoint || !existsSync(mountPoint)) {
148
+ throw new Error(`could not determine dmg mount point (output: ${mountOutput.stdout})`)
149
+ }
150
+
151
+ const appSource = join(mountPoint, 'rnx.app')
152
+ if (!existsSync(appSource)) {
153
+ spawnSync('hdiutil', ['detach', '-force', mountPoint])
154
+ throw new Error(`rnx.app not found inside ${mountPoint}`)
155
+ }
156
+
157
+ // prefer /Applications if writable, otherwise ~/Applications (same paths
158
+ // findDesktopCompanion checks, so no extra config needed post-install).
159
+ const systemApps = '/Applications'
160
+ const userApps = resolve(process.env.HOME || '', 'Applications')
161
+ let destRoot = systemApps
162
+ try {
163
+ const probe = join(systemApps, `.rnx-write-probe-${process.pid}`)
164
+ spawnSync('touch', [probe])
165
+ if (existsSync(probe)) rmSync(probe, { force: true })
166
+ else destRoot = userApps
167
+ } catch {
168
+ destRoot = userApps
169
+ }
170
+ if (destRoot === userApps) {
171
+ mkdirSync(destRoot, { recursive: true })
172
+ }
173
+
174
+ const appDest = join(destRoot, 'rnx.app')
175
+
176
+ // copy alongside the destination and swap, rather than deleting the installed
177
+ // app first. removing it up front means a failed copy leaves the user with no
178
+ // app at all, having destroyed a working install to do it. the staging path is
179
+ // in the same directory so the swap is a rename on one volume.
180
+ const staging = join(destRoot, `.rnx-incoming-${process.pid}.app`)
181
+ rmSync(staging, { recursive: true, force: true })
182
+
183
+ const copy = spawnSync('cp', ['-R', appSource, staging])
184
+ if (copy.status !== 0) {
185
+ spawnSync('hdiutil', ['detach', '-force', mountPoint])
186
+ rmSync(staging, { recursive: true, force: true })
187
+ throw new Error(`copy to ${destRoot} failed: ${copy.stderr?.toString() || ''}`.trim())
188
+ }
189
+ const detach = spawnSync('hdiutil', ['detach', '-force', mountPoint])
190
+
191
+ const previous = join(destRoot, `.rnx-previous-${process.pid}.app`)
192
+ if (existsSync(appDest)) {
193
+ const park = spawnSync('mv', [appDest, previous])
194
+ if (park.status !== 0) {
195
+ rmSync(staging, { recursive: true, force: true })
196
+ throw new Error(
197
+ `could not replace ${appDest}: ${park.stderr?.toString() || ''}`.trim(),
198
+ )
199
+ }
200
+ }
201
+ const swap = spawnSync('mv', [staging, appDest])
202
+ if (swap.status !== 0) {
203
+ // put the working install back before reporting failure
204
+ if (existsSync(previous)) spawnSync('mv', [previous, appDest])
205
+ rmSync(staging, { recursive: true, force: true })
206
+ throw new Error(
207
+ `could not move into ${appDest}: ${swap.stderr?.toString() || ''}`.trim(),
208
+ )
209
+ }
210
+ rmSync(previous, { recursive: true, force: true })
211
+
212
+ if (detach.status !== 0) {
213
+ console.warn(
214
+ ` warning: failed to unmount ${mountPoint} (${detach.stderr?.toString().trim() || 'unknown error'})`,
215
+ )
216
+ }
217
+
218
+ // clear the quarantine attribute so gatekeeper doesn't nag on first launch;
219
+ // the app is already notarized so this is safe.
220
+ spawnSync('xattr', ['-dr', 'com.apple.quarantine', appDest])
221
+
222
+ return appDest
223
+ }
224
+
225
+ async function installLinuxAppImage(appImagePath: string): Promise<string> {
226
+ const destDir = resolve(process.env.HOME || '', 'Applications')
227
+ mkdirSync(destDir, { recursive: true })
228
+ const dest = join(destDir, 'rnx.AppImage')
229
+ if (existsSync(dest)) rmSync(dest, { force: true })
230
+ // copyFile + unlink instead of rename: /tmp and ~/Applications are
231
+ // routinely on separate mounts in containers / VMs, where renameSync
232
+ // fails EXDEV. copy is portable across mounts and the unlink at the
233
+ // end keeps the staging dir clean.
234
+ copyFileSync(appImagePath, dest)
235
+ unlinkSync(appImagePath)
236
+ chmodSync(dest, 0o755)
237
+ return dest
238
+ }
239
+
240
+ async function installWindowsExe(exePath: string): Promise<string> {
241
+ // nsis installer prompts and self-elevates; run it and let it take over.
242
+ await new Promise<void>((resolvePromise, rejectPromise) => {
243
+ const child = spawn('cmd', ['/c', 'start', '""', '/wait', exePath], {
244
+ stdio: 'inherit',
245
+ })
246
+ child.once('error', rejectPromise)
247
+ child.once('exit', (code) => {
248
+ if (code === 0) resolvePromise()
249
+ else rejectPromise(new Error(`installer exited with code ${code}`))
250
+ })
251
+ })
252
+ const installed = findDesktopCompanion()
253
+ if (!installed) {
254
+ throw new Error('the installer finished but the rnx application was not registered')
255
+ }
256
+ return installed.path
257
+ }
258
+
259
+ export async function runInstallDesktop(args: string[]) {
260
+ if (args.includes('--help') || args.includes('-h')) {
261
+ console.log(`
262
+ rnx desktop install
263
+
264
+ Download and install the optional rnx Desktop app.
265
+ The CLI works without it. Choose Desktop when you want a dedicated native window.
266
+
267
+ usage:
268
+ rnx desktop install [options]
269
+
270
+ options:
271
+ -y, --yes skip confirmation and install immediately
272
+ --force reinstall even if the companion is already present
273
+
274
+ examples:
275
+ rnx desktop install
276
+ rnx desktop install --yes
277
+ `)
278
+ rnxExit(0)
279
+ }
280
+
281
+ const skipConfirm =
282
+ args.includes('--yes') ||
283
+ args.includes('-y') ||
284
+ process.env.RNX_NO_PROMPT === '1' ||
285
+ process.env.CI === '1' ||
286
+ !process.stdin.isTTY
287
+ const force = args.includes('--force')
288
+
289
+ const existing = findDesktopCompanion()
290
+ if (existing && !force) {
291
+ console.log(` ${success('✓')} Desktop app is already installed at ${existing.path}`)
292
+ console.log(` ${muted('Pass --force to reinstall.')}`)
293
+ return
294
+ }
295
+
296
+ const target = detectTarget()
297
+ if (!target) {
298
+ console.error(
299
+ ` ${failure('×')} No desktop build is available for ${process.platform}/${process.arch}.`,
300
+ )
301
+ console.error(
302
+ ' Supported: darwin-arm64, darwin-x64, linux-x64, linux-arm64, win32-x64',
303
+ )
304
+ rnxExit(1)
305
+ }
306
+
307
+ console.log(` ${bold('Desktop app')}`)
308
+ console.log(` Platform ${target.platform}`)
309
+ console.log(` Download ${target.url}`)
310
+ console.log()
311
+
312
+ if (!skipConfirm) {
313
+ const ok = await confirm('Download and install now?', true)
314
+ if (!ok) {
315
+ console.log(` ${muted('Cancelled.')}`)
316
+ return
317
+ }
318
+ console.log()
319
+ }
320
+
321
+ const workDir = join(tmpdir(), `rnx-install-${Date.now()}`)
322
+ const downloadPath = join(workDir, target.filename)
323
+
324
+ let lastDraw = 0
325
+ process.stdout.write(` Downloading ${target.filename}...\n`)
326
+
327
+ try {
328
+ await download({
329
+ url: target.url,
330
+ dest: downloadPath,
331
+ onProgress: (received, total) => {
332
+ const now = Date.now()
333
+ if (now - lastDraw < 100 && received < (total ?? Infinity)) return
334
+ lastDraw = now
335
+ if (process.stdout.isTTY) {
336
+ process.stdout.write(`\r\x1b[2K${renderProgress(received, total)}`)
337
+ }
338
+ },
339
+ })
340
+ } catch (err) {
341
+ rmSync(workDir, { recursive: true, force: true })
342
+ const message = err instanceof Error ? err.message : String(err)
343
+ console.error(`\n ${failure('×')} Download failed: ${message}`)
344
+ console.error(
345
+ ` The ${target.platform} desktop build may not be published yet, or your`,
346
+ )
347
+ console.error(
348
+ ' network blocked the request. Check your connection and try again later.',
349
+ )
350
+ console.error(' The CLI and daemon work without the desktop app. Run `rnx --help`.')
351
+ rnxExit(1)
352
+ }
353
+
354
+ if (process.stdout.isTTY) process.stdout.write('\n')
355
+ const size = statSync(downloadPath).size
356
+ console.log(` ${success('✓')} Downloaded ${(size / (1024 * 1024)).toFixed(1)} MB`)
357
+ console.log()
358
+
359
+ try {
360
+ const installedAt = await withSpinner('Installing the desktop app', () =>
361
+ target.install(downloadPath),
362
+ )
363
+ console.log(` Installed at ${installedAt}`)
364
+ rmSync(workDir, { recursive: true, force: true })
365
+ } catch (err) {
366
+ console.error(
367
+ ` ${failure('×')} Install failed: ${err instanceof Error ? err.message : String(err)}`,
368
+ )
369
+ // the download is good, only placing the app failed. on macOS the disk
370
+ // image installs itself perfectly well by hand, so open it and let the user
371
+ // drag rather than leaving them at a dead end. keep workDir for that.
372
+ if (process.platform === 'darwin') {
373
+ spawnSync('open', [downloadPath])
374
+ console.error(` Opened ${target.filename} — drag rnx to Applications to finish.`)
375
+ } else {
376
+ console.error(` Keeping the download at ${downloadPath} for manual installation.`)
377
+ }
378
+ rnxExit(1)
379
+ }
380
+
381
+ console.log()
382
+ console.log(` ${bold('Ready')}`)
383
+ console.log(' rnx desktop Launch the desktop app')
384
+ console.log(' rnx open <target> Open a demo or bundle in it')
385
+ }