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,357 @@
1
+ import { exec } from 'node:child_process'
2
+ import { randomBytes } from 'node:crypto'
3
+ import http from 'node:http'
4
+ import { createInterface } from 'node:readline/promises'
5
+ import {
6
+ refreshSharedDesktopAuthSession,
7
+ sharedDesktopAuthAccounts,
8
+ writeSharedDesktopAuthSession,
9
+ type SharedDesktopAuthSession,
10
+ } from '../../src/auth/shared-session'
11
+ import { rnxPublicBrand } from '../../src/public-brand'
12
+ import { rnxExit } from '../run-rnx'
13
+
14
+ const REMOTE_DEFAULT_ORIGIN = process.env.RNX_AUTH_ORIGIN || 'https://contrast.dev'
15
+
16
+ type LoginResult =
17
+ | { ok: true; token: string; userId?: string; email?: string }
18
+ | { ok: false; error: string }
19
+
20
+ function resolveDefaultAuthOrigin(explicitOrigin?: string) {
21
+ if (explicitOrigin) return explicitOrigin
22
+ if (process.env.RNX_AUTH_ORIGIN) return process.env.RNX_AUTH_ORIGIN
23
+ if (process.env.RNX_UPLOAD_ORIGIN) return process.env.RNX_UPLOAD_ORIGIN
24
+ return REMOTE_DEFAULT_ORIGIN
25
+ }
26
+
27
+ function printHelp() {
28
+ console.log(`
29
+ rnx login — sign in so uploads can attach to your account
30
+
31
+ usage:
32
+ rnx login [--origin <url>] [--account <name-or-id>]
33
+
34
+ options:
35
+ --origin <url> auth host (default: ${REMOTE_DEFAULT_ORIGIN})
36
+ override with RNX_AUTH_ORIGIN env var
37
+ --account <name-or-id> the account remote simulators and boxes bill to.
38
+ one account is chosen for you; several ask.
39
+ -h, --help
40
+ `)
41
+ }
42
+
43
+ // a session token names the person, not an account. the account that pays for
44
+ // cloud work is chosen here, once, and stored on the session; a person with one
45
+ // account never sees the question.
46
+ async function chooseAccount(
47
+ session: SharedDesktopAuthSession,
48
+ requested: string | undefined,
49
+ ): Promise<{ id: string; name: string } | null> {
50
+ const accounts = sharedDesktopAuthAccounts(session.user)
51
+ if (requested !== undefined) {
52
+ const wanted = requested.trim().toLowerCase()
53
+ const match = accounts.find(
54
+ (account) =>
55
+ account.id === requested.trim() || account.name.toLowerCase() === wanted,
56
+ )
57
+ if (!match) {
58
+ console.error(
59
+ ` no account named ${JSON.stringify(requested)}. your accounts:\n` +
60
+ accounts.map((account) => ` ${account.name} (${account.id})`).join('\n'),
61
+ )
62
+ rnxExit(1)
63
+ }
64
+ return match
65
+ }
66
+ if (accounts.length <= 1) return accounts[0] ?? null
67
+ if (!process.stdin.isTTY) {
68
+ console.error(
69
+ ' this sign-in belongs to several accounts; rerun with --account <name> to choose the one to bill:\n' +
70
+ accounts.map((account) => ` ${account.name} (${account.id})`).join('\n'),
71
+ )
72
+ rnxExit(1)
73
+ }
74
+ console.log(' which account should remote simulators and boxes bill to?')
75
+ accounts.forEach((account, index) => console.log(` ${index + 1}. ${account.name}`))
76
+ const prompt = createInterface({ input: process.stdin, output: process.stdout })
77
+ try {
78
+ for (;;) {
79
+ const answer = (await prompt.question(` account [1-${accounts.length}]: `)).trim()
80
+ const chosen = accounts[Number(answer) - 1]
81
+ if (chosen && /^\d+$/.test(answer)) return chosen
82
+ console.log(` enter a number from 1 to ${accounts.length}`)
83
+ }
84
+ } finally {
85
+ prompt.close()
86
+ }
87
+ }
88
+
89
+ async function finishLogin(
90
+ origin: string,
91
+ requestedAccount: string | undefined,
92
+ fallbackLabel: string | undefined,
93
+ suffix: string,
94
+ ) {
95
+ const refreshed = await refreshSharedDesktopAuthSession(origin)
96
+ const account = refreshed ? await chooseAccount(refreshed, requestedAccount) : null
97
+ if (refreshed && account) {
98
+ writeSharedDesktopAuthSession({ ...refreshed, accountId: account.id })
99
+ }
100
+ const label = refreshed?.user?.email || fallbackLabel || refreshed?.user?.id
101
+ console.log(
102
+ ` signed in${label ? ` as ${label}` : ''}${account ? ` (${account.name})` : ''}${suffix}`,
103
+ )
104
+ }
105
+
106
+ function extractStringFlag(args: string[], name: string): string | undefined {
107
+ const idx = args.findIndex((a) => a === name)
108
+ if (idx < 0) return undefined
109
+ const value = args[idx + 1]
110
+ args.splice(idx, 2)
111
+ return value
112
+ }
113
+
114
+ function openBrowser(url: string) {
115
+ const command =
116
+ process.platform === 'darwin'
117
+ ? `open "${url}"`
118
+ : process.platform === 'win32'
119
+ ? `start "" "${url}"`
120
+ : `xdg-open "${url}"`
121
+
122
+ exec(command, (err) => {
123
+ if (!err) return
124
+ console.log(' could not open browser automatically.')
125
+ console.log(` open this URL manually:\n ${url}\n`)
126
+ })
127
+ }
128
+
129
+ function renderCallbackPage(body: string) {
130
+ return `<!doctype html>
131
+ <html>
132
+ <body style="font-family:system-ui;background:#000;color:#fff;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
133
+ <div style="max-width:480px;text-align:center">
134
+ <h1 style="font-size:28px;margin:0 0 12px">${rnxPublicBrand.name}</h1>
135
+ <p style="color:#ccc">${body}</p>
136
+ </div>
137
+ </body>
138
+ </html>`
139
+ }
140
+
141
+ async function runDevLogin(origin: string): Promise<LoginResult> {
142
+ try {
143
+ const res = await fetch(`${origin.replace(/\/$/, '')}/api/dev-login`, {
144
+ method: 'POST',
145
+ })
146
+ if (res.status === 403) {
147
+ return { ok: false, error: 'local dev-login unavailable' }
148
+ }
149
+ if (!res.ok) {
150
+ const text = await res.text().catch(() => '')
151
+ return { ok: false, error: `dev-login ${res.status}: ${text}` }
152
+ }
153
+ const body = (await res.json()) as {
154
+ ok?: boolean
155
+ token?: string
156
+ email?: string
157
+ user?: { id?: string }
158
+ }
159
+ if (!body.token) {
160
+ return { ok: false, error: 'dev-login response missing token' }
161
+ }
162
+ return {
163
+ ok: true,
164
+ token: body.token,
165
+ userId: body.user?.id,
166
+ email: body.email,
167
+ }
168
+ } catch (err) {
169
+ return {
170
+ ok: false,
171
+ error: err instanceof Error ? err.message : 'dev-login fetch failed',
172
+ }
173
+ }
174
+ }
175
+
176
+ function isLocalOrigin(origin: string): boolean {
177
+ try {
178
+ const hostname = new URL(origin).hostname.replace(/^\[|\]$/g, '').toLowerCase()
179
+ if (hostname === 'localhost') return true
180
+ if (/^127(?:\.\d{1,3}){3}$/.test(hostname)) return true
181
+ if (hostname === '::1' || hostname === '0.0.0.0') return true
182
+ if (hostname.endsWith('.local')) return true
183
+ if (hostname.endsWith('.localhost')) return true
184
+ // VM↔host bridge aliases — orb, docker, lima, multipass all forward the
185
+ // host on a well-known name (IPv4 or IPv6) so a VM-internal `rnx login`
186
+ // can still take the dev-login fast path against the host's Contrast dev
187
+ // server. without this the CLI falls through to a browser callback that
188
+ // is unreachable from a headless VM.
189
+ if (hostname === 'host.containers.internal') return true
190
+ if (hostname === 'host.docker.internal') return true
191
+ if (hostname === 'host.orb.internal') return true
192
+ if (hostname === 'host.lima.internal') return true
193
+ // orb's IPv6 form of host.orb.internal: fd07:b51a:cc66:f0::fe (always
194
+ // this fixed address; it's a stable orb ULA assignment, see orb docs).
195
+ if (hostname === 'fd07:b51a:cc66:f0::fe') return true
196
+ // lima's qemu (slirp) backend does NOT resolve host.lima.internal — the
197
+ // host is the fixed slirp gateway 192.168.5.2. a lima/qemu user therefore
198
+ // targets the raw gateway IP, which would otherwise miss the dev-login fast
199
+ // path and fall through to the headless-broken browser callback. recognize
200
+ // it (and multipass's default bridge .1 gateways) so dev-login still fires.
201
+ // safe: no-device dev-login requests are accepted only in local dev.
202
+ if (hostname === '192.168.5.2') return true
203
+ if (/^192\.168\.(?:64|139)\.1$/.test(hostname)) return true
204
+ return false
205
+ } catch {
206
+ return false
207
+ }
208
+ }
209
+
210
+ export async function runLogin(args: string[]) {
211
+ if (args.includes('--help') || args.includes('-h')) {
212
+ printHelp()
213
+ rnxExit(0)
214
+ }
215
+
216
+ const mutableArgs = [...args]
217
+ const origin = resolveDefaultAuthOrigin(extractStringFlag(mutableArgs, '--origin'))
218
+ const requestedAccount = extractStringFlag(mutableArgs, '--account')
219
+
220
+ // in dev (localhost), skip the browser and sign in as the shared dev
221
+ // user via POST /api/dev-login. the no-device contract is local-only, so this
222
+ // path is safe to take unconditionally against local hosts.
223
+ if (isLocalOrigin(origin)) {
224
+ console.log(` detected local origin (${origin}) — signing in as dev user`)
225
+ const devResult = await runDevLogin(origin)
226
+ if (devResult.ok) {
227
+ writeSharedDesktopAuthSession({
228
+ token: devResult.token,
229
+ user: devResult.userId ? { id: devResult.userId, email: devResult.email } : null,
230
+ origin,
231
+ source: 'cli',
232
+ })
233
+ await finishLogin(
234
+ origin,
235
+ requestedAccount,
236
+ devResult.email || devResult.userId,
237
+ ' (dev)',
238
+ )
239
+ return
240
+ }
241
+ console.log(` dev-login not available (${devResult.error}); falling back to browser`)
242
+ }
243
+
244
+ const state = randomBytes(16).toString('hex')
245
+
246
+ // the browser round-trip needs human action (github's authorize screen on
247
+ // first sign-in), so the wait is generous, but it must not be infinite: a
248
+ // closed tab, denied consent, or blocked popup used to hang the CLI forever.
249
+ const LOGIN_TIMEOUT_MS = 5 * 60 * 1000
250
+
251
+ const result = await new Promise<LoginResult>((resolve) => {
252
+ let timer: ReturnType<typeof setTimeout>
253
+ let settled = false
254
+ const finish = (r: LoginResult) => {
255
+ if (settled) return
256
+ settled = true
257
+ clearTimeout(timer)
258
+ resolve(r)
259
+ server.close()
260
+ }
261
+
262
+ const server = http.createServer((req, res) => {
263
+ try {
264
+ const url = new URL(req.url || '/', 'http://127.0.0.1')
265
+ const returnedState = url.searchParams.get('state')
266
+ const token = url.searchParams.get('token')
267
+ const email = url.searchParams.get('email') || undefined
268
+ const userId = url.searchParams.get('userId') || undefined
269
+ const error = url.searchParams.get('error')
270
+
271
+ // stray localhost traffic (port scanners, favicon fetches, browser
272
+ // prefetch) must not abort the login. only a request carrying our
273
+ // state is the real callback; rfc 8252 loopback servers ignore the
274
+ // rest. aborting here used to kill the login mid-wait.
275
+ if (returnedState !== state) {
276
+ res.statusCode = 404
277
+ res.end('not found')
278
+ return
279
+ }
280
+
281
+ res.setHeader('content-type', 'text/html; charset=utf-8')
282
+
283
+ if (error) {
284
+ res.end(renderCallbackPage(`sign-in failed: ${error}`))
285
+ finish({ ok: false, error })
286
+ return
287
+ }
288
+
289
+ if (!token) {
290
+ res.end(
291
+ renderCallbackPage('missing token. close this tab and retry `rnx login`.'),
292
+ )
293
+ finish({ ok: false, error: 'missing token' })
294
+ return
295
+ }
296
+
297
+ res.end(renderCallbackPage('sign-in complete. you can return to the terminal.'))
298
+ finish({ ok: true, token, email, userId })
299
+ } catch (err) {
300
+ res.statusCode = 500
301
+ res.end(renderCallbackPage('sign-in callback failed. retry `rnx login`.'))
302
+ finish({
303
+ ok: false,
304
+ error: err instanceof Error ? err.message : 'callback failed',
305
+ })
306
+ }
307
+ })
308
+
309
+ timer = setTimeout(() => {
310
+ finish({
311
+ ok: false,
312
+ error:
313
+ 'timed out waiting for the browser sign-in (5 minutes). rerun `rnx login` and complete the github authorize screen if it appears.',
314
+ })
315
+ }, LOGIN_TIMEOUT_MS)
316
+
317
+ server.listen(0, '127.0.0.1', () => {
318
+ const address = server.address()
319
+ if (!address || typeof address === 'string') {
320
+ finish({ ok: false, error: 'failed to bind callback server' })
321
+ return
322
+ }
323
+
324
+ const url = new URL(`${origin.replace(/\/$/, '')}/auth/rnx-cli`)
325
+ url.searchParams.set('state', state)
326
+ url.searchParams.set('port', String(address.port))
327
+ url.searchParams.set('contrast', origin)
328
+
329
+ console.log(' opening browser for rnx login...')
330
+ console.log(
331
+ ' first time here? github will show an authorize screen. approve it to continue.',
332
+ )
333
+ console.log(` waiting up to 5 minutes for the browser (ctrl+c to cancel)`)
334
+ console.log(` if nothing opens, visit:\n ${url.toString()}\n`)
335
+ openBrowser(url.toString())
336
+ })
337
+ })
338
+
339
+ if (!result.ok) {
340
+ console.error(` login failed: ${result.error}`)
341
+ rnxExit(1)
342
+ }
343
+
344
+ writeSharedDesktopAuthSession({
345
+ token: result.token,
346
+ user: result.userId
347
+ ? {
348
+ id: result.userId,
349
+ email: result.email,
350
+ }
351
+ : null,
352
+ origin,
353
+ source: 'cli',
354
+ })
355
+
356
+ await finishLogin(origin, requestedAccount, result.email || result.userId, '')
357
+ }
@@ -0,0 +1,24 @@
1
+ import {
2
+ clearSharedDesktopAuthSession,
3
+ readSharedDesktopAuthSession,
4
+ } from '../../src/auth/shared-session'
5
+
6
+ export async function runLogout() {
7
+ const current = readSharedDesktopAuthSession()
8
+ if (!current?.token) {
9
+ console.log(' not signed in')
10
+ return
11
+ }
12
+
13
+ try {
14
+ await fetch(`${current.origin.replace(/\/$/, '')}/api/auth/sign-out`, {
15
+ method: 'POST',
16
+ headers: { authorization: `Bearer ${current.token}` },
17
+ })
18
+ } catch {
19
+ // best-effort only
20
+ }
21
+
22
+ clearSharedDesktopAuthSession()
23
+ console.log(' signed out')
24
+ }