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,925 @@
1
+ // `rnx box` — the box environment.
2
+ //
3
+ // bare `rnx box` is LOCAL and mounts the current checkout at /project. cloud
4
+ // create provisions through the shared client and enters the same shell unless
5
+ // --json asks for a noninteractive result. connect asks the account service to
6
+ // turn a Box name into an authorized reference and enters that same shell, so
7
+ // this process never stores a Box token.
8
+ //
9
+ // every verb that acts on an existing Box takes it the same way: the id or
10
+ // name on the command line, else the one `rnx box use` remembered. see
11
+ // boxTarget below.
12
+
13
+ import { spawn } from 'node:child_process'
14
+ import path from 'node:path'
15
+ import { createInterface } from 'node:readline'
16
+ import { BoxClientError, createBoxClient } from '@rnx/box/client'
17
+ import { createBoxHostProvider } from '@rnx/box/host'
18
+ import { PROJECT_MOUNT } from '@rnx/box/shell'
19
+ import { terminalBoxStack } from '@rnx/box/stack'
20
+ import {
21
+ BoxTemplateRegistry,
22
+ prebuiltBundleBoxTemplate,
23
+ sourceControlBoxTemplate,
24
+ terminalBoxTemplate,
25
+ } from '@rnx/box/template'
26
+ import {
27
+ cloudBoxConnectUrl,
28
+ createCloudBoxProvider,
29
+ ensureCloudBoxBrowser,
30
+ heartbeatSession,
31
+ resolveCloudBoxReference,
32
+ } from 'rnx-cloud-box/client'
33
+ import { readDefaultBox, writeDefaultBox } from '../../src/home-paths'
34
+ import { rnxPublicBrand } from '../../src/public-brand'
35
+ import { authHeaderOrExit, cloudAccountIdOrExit, type CliAuth } from '../auth'
36
+ import { produceCloudBoxArtifact, type CloudBoxArtifactProduction } from '../cloud-client'
37
+ import { findRepositoryPackageRoot } from '../setup-repository'
38
+ import { resolvePlacement } from '../ws-bridge'
39
+ import { BundleFilePlane } from './box/bundle-plane'
40
+ import { CheckoutFilePlane, CheckoutRequiresGitError } from './box/checkout-plane'
41
+ import { sharePlane } from './box/plane-share'
42
+ import { createRnxCommand } from './box/rnx-command'
43
+ import { createSearchCommand } from './box/search-command'
44
+ import type { BoxShellSession } from '@rnx/box/client'
45
+ import type { BoxTemplateResolution } from '@rnx/box/template'
46
+
47
+ interface BoxCommandOptions {
48
+ port?: number
49
+ verbose?: boolean
50
+ }
51
+
52
+ // no default. a cloud box service has to be deployed before one can exist, and
53
+ // defaulting to an origin that answers nothing would advertise a capability
54
+ // that always fails.
55
+ const ENDPOINT_ENV = 'RNX_BOX_ENDPOINT'
56
+ const API_ORIGIN_ENV = 'RNX_API_ORIGIN'
57
+ const BOX_TEMPLATES = new BoxTemplateRegistry([
58
+ terminalBoxTemplate,
59
+ sourceControlBoxTemplate,
60
+ prebuiltBundleBoxTemplate,
61
+ ])
62
+
63
+ export async function runBox(
64
+ args: string[],
65
+ opts: BoxCommandOptions = {},
66
+ ): Promise<number> {
67
+ // a box inside a box would mount the same checkout twice and give the
68
+ // user two shells writing through one filesystem. refuse at the top rather
69
+ // than letting the second one start and quietly race the first.
70
+ const placement = resolvePlacement()
71
+ if (placement === 'local-box') {
72
+ console.error(
73
+ ` ${rnxPublicBrand.commandName} box: you are already in a box.\n` +
74
+ ` this shell is the box — run ${rnxPublicBrand.commandName} commands directly, or \`exit\` to leave.`,
75
+ )
76
+ return 1
77
+ }
78
+
79
+ const subcommand = args.find((arg) => !arg.startsWith('-'))
80
+ if (subcommand === 'create') return runCloudBoxCreate(args, opts)
81
+ if (subcommand === 'use') return runCloudBoxUse(args)
82
+ if (subcommand === 'connect') return runCloudBoxConnect(args)
83
+ if (subcommand === 'send') return runBoxSend(args, opts)
84
+ if (subcommand === 'open') return runCloudBoxOpen(args)
85
+ if (subcommand === 'stop' || subcommand === 'delete') {
86
+ return runCloudBoxStop(args, subcommand === 'delete')
87
+ }
88
+ if (subcommand === 'ls') return runCloudBoxList(args)
89
+ if (subcommand !== undefined) {
90
+ console.error(
91
+ ` unknown command: ${rnxPublicBrand.commandName} box ${subcommand}\n` +
92
+ ` run \`${rnxPublicBrand.commandName} box --help\` to see the box surface.`,
93
+ )
94
+ return 1
95
+ }
96
+
97
+ if (args.includes('--json')) {
98
+ console.error(
99
+ ` ${rnxPublicBrand.commandName} box --json requires a noninteractive cloud subcommand such as create.`,
100
+ )
101
+ return 1
102
+ }
103
+
104
+ return runLocalBox(opts)
105
+ }
106
+
107
+ function flag(args: string[], name: string): string | undefined {
108
+ const inline = args.find((arg) => arg.startsWith(`--${name}=`))
109
+ if (inline) return inline.slice(name.length + 3)
110
+ const index = args.indexOf(`--${name}`)
111
+ if (index >= 0 && args[index + 1] && !args[index + 1].startsWith('-')) {
112
+ return args[index + 1]
113
+ }
114
+ return undefined
115
+ }
116
+
117
+ // the account service, which owns /api/v1/rnx/sessions and the box connect
118
+ // route. a login session names the origin it was issued by; an api key
119
+ // carries none, so it goes where every other key-authenticated command goes.
120
+ function apiOrigin(args: string[], auth: CliAuth): string {
121
+ return (
122
+ flag(args, 'api-origin') ??
123
+ process.env[API_ORIGIN_ENV]?.trim() ??
124
+ (auth.kind === 'session' ? auth.origin : 'https://contrast.dev')
125
+ ).replace(/\/+$/, '')
126
+ }
127
+
128
+ // the Box named on the command line: the second positional, since the first is
129
+ // the verb, or `--name` which is the same thing spelled as a flag.
130
+ function namedBox(args: string[]): string | undefined {
131
+ return args.filter((arg) => !arg.startsWith('-'))[1] ?? flag(args, 'name')
132
+ }
133
+
134
+ /**
135
+ * which Box a verb acts on.
136
+ *
137
+ * one order for every verb that acts on a Box that already exists: the id or
138
+ * name on the command line, else the one `rnx box use` remembered, else
139
+ * nothing and the reason printed here.
140
+ */
141
+ function boxTarget(args: string[], label: string): string | null {
142
+ const named = namedBox(args)
143
+ if (named) return named
144
+ const stored = readDefaultBox()
145
+ if (stored) return stored.boxId
146
+ const rnx = rnxPublicBrand.commandName
147
+ console.error(
148
+ ` ${rnx} box ${label}: which box? pass its ID or name, as \`${rnx} box ls\` prints it,\n` +
149
+ ` or set one for every box command with \`${rnx} box use <id-or-name>\`.`,
150
+ )
151
+ return null
152
+ }
153
+
154
+ /**
155
+ * `rnx box use` — the Box every other verb acts on when none is named.
156
+ *
157
+ * it resolves through the account service exactly as connect does, because
158
+ * that is the only thing that knows which account owns a Box, and a default
159
+ * pointing at somebody else's Box would fail at every later verb instead of
160
+ * here. what is written down is the id and the display name; the Box's own
161
+ * token is resolved again per command and never reaches disk.
162
+ */
163
+ async function runCloudBoxUse(args: string[]): Promise<number> {
164
+ const rnx = rnxPublicBrand.commandName
165
+ const named = namedBox(args)
166
+ if (!named) {
167
+ const stored = readDefaultBox()
168
+ if (!stored) {
169
+ console.log(
170
+ ` no default box. \`${rnx} box use <id-or-name>\` targets one, and every\n` +
171
+ ` other box command then acts on it when you name none.`,
172
+ )
173
+ return 0
174
+ }
175
+ console.log(` ${stored.name ?? stored.boxId}`)
176
+ return 0
177
+ }
178
+
179
+ const { auth, header } = authHeaderOrExit('box use')
180
+ const apiKey = header.replace(/^Bearer /, '')
181
+ try {
182
+ const reference = await resolveCloudBoxReference({
183
+ apiOrigin: apiOrigin(args, auth),
184
+ apiKey,
185
+ boxId: named,
186
+ })
187
+ // attach for the Box's own name: the caller may have typed either an id or
188
+ // a name, and the default has to be able to print the one a person reads.
189
+ const box = await createBoxClient(
190
+ createCloudBoxProvider({
191
+ endpoint: reference.endpoint,
192
+ authority: { kind: 'attach' },
193
+ }),
194
+ ).attach({
195
+ reference: { boxId: reference.boxId, accessToken: reference.token },
196
+ })
197
+ writeDefaultBox({ boxId: box.description.boxId, name: box.description.name })
198
+ console.log(
199
+ ` ...box commands now target ${box.description.name ?? box.description.boxId}`,
200
+ )
201
+ return 0
202
+ } catch (error) {
203
+ if (error instanceof BoxClientError) {
204
+ console.error(` ${rnx} box use: ${error.message}`)
205
+ return 1
206
+ }
207
+ throw error
208
+ }
209
+ }
210
+
211
+ function requireEndpoint(args: string[], label: string): string | null {
212
+ return cloudBoxEndpoint(args, `box ${label}`)
213
+ }
214
+
215
+ /**
216
+ * the cloud box service this command talks to, or null with the reason
217
+ * printed. exported because `rnx box send` creates a box too, and a second
218
+ * spelling of where boxes live is a second place to configure.
219
+ */
220
+ export function cloudBoxEndpoint(args: string[], command: string): string | null {
221
+ const endpoint = flag(args, 'endpoint') ?? process.env[ENDPOINT_ENV]?.trim()
222
+ if (endpoint) return endpoint
223
+ console.error(
224
+ ` ${rnxPublicBrand.commandName} ${command}: no cloud box service to talk to.\n` +
225
+ ` set ${ENDPOINT_ENV} or pass --endpoint=<url>.`,
226
+ )
227
+ return null
228
+ }
229
+
230
+ /** the api origin a cloud box bills against. */
231
+ export function cloudBoxApiOrigin(args: string[], auth: CliAuth): string {
232
+ return apiOrigin(args, auth)
233
+ }
234
+
235
+ // `rnx box send` — the CLI half of "send to box". the shell's rail button runs
236
+ // the same function inside the daemon; this is the same send for an agent or a
237
+ // terminal, and it prints the id and URL either way.
238
+ async function runBoxSend(args: string[], opts: BoxCommandOptions): Promise<number> {
239
+ const rnx = rnxPublicBrand.commandName
240
+ const { auth, header } = authHeaderOrExit('box send')
241
+ const accountId = cloudAccountIdOrExit(auth)
242
+ const { createBridgeFromParsed, parseBridgeCliArgs } = await import('../ws-bridge')
243
+ const parsed = parseBridgeCliArgs(
244
+ args.filter((arg) => arg !== 'send'),
245
+ {
246
+ port: opts.port,
247
+ stripBooleanFlags: ['--no-auth', '--json'],
248
+ stripValueFlags: ['--name', '--api-origin'],
249
+ },
250
+ )
251
+ const bridge = createBridgeFromParsed(parsed)
252
+ try {
253
+ const { sendToBox } = await import('../send-to-box')
254
+ const result = await sendToBox({
255
+ send: (command) => bridge.send(command),
256
+ authorization: header,
257
+ accountId,
258
+ carryAuth: !args.includes('--no-auth'),
259
+ boxName: flag(args, 'name') ?? null,
260
+ apiOrigin: flag(args, 'api-origin'),
261
+ })
262
+ if (args.includes('--json')) console.log(JSON.stringify(result))
263
+ return 0
264
+ } catch (error) {
265
+ console.error(
266
+ ` ${rnx} box send: ${error instanceof Error ? error.message : String(error)}`,
267
+ )
268
+ return 1
269
+ } finally {
270
+ bridge.close()
271
+ }
272
+ }
273
+
274
+ async function runCloudBoxStop(args: string[], purge: boolean): Promise<number> {
275
+ const rnx = rnxPublicBrand.commandName
276
+ const label = purge ? 'delete' : 'stop'
277
+ const endpoint = requireEndpoint(args, label)
278
+ if (!endpoint) return 1
279
+ const name = boxTarget(args, label)
280
+ if (!name) return 1
281
+ const { auth, header } = authHeaderOrExit(`box ${label}`)
282
+ const apiKey = header.replace(/^Bearer /, '')
283
+ const accountId = cloudAccountIdOrExit(auth)
284
+ const client = createBoxClient(
285
+ createCloudBoxProvider({
286
+ endpoint,
287
+ authority: { kind: 'account', apiOrigin: apiOrigin(args, auth), apiKey, accountId },
288
+ }),
289
+ )
290
+ try {
291
+ const result = purge
292
+ ? await client.delete({ boxId: name })
293
+ : await client.stop({ boxId: name })
294
+ // the box the default named is gone, so the default goes with it rather
295
+ // than pointing every later command at a box that stopped answering.
296
+ const stored = readDefaultBox()
297
+ if (stored && (stored.boxId === name || stored.name === name)) writeDefaultBox(null)
298
+ console.log(
299
+ purge
300
+ ? ` deleted ${name}, freeing ${Number(result.freedBytes ?? 0)} bytes`
301
+ : ` stopped ${name}`,
302
+ )
303
+ return 0
304
+ } catch (error) {
305
+ if (error instanceof BoxClientError) {
306
+ console.error(` ${rnx} box ${label}: ${error.message}`)
307
+ return 1
308
+ }
309
+ throw error
310
+ }
311
+ }
312
+
313
+ async function runCloudBoxList(args: string[]): Promise<number> {
314
+ const rnx = rnxPublicBrand.commandName
315
+ const { auth, header } = authHeaderOrExit('box ls')
316
+ const apiKey = header.replace(/^Bearer /, '')
317
+ const accountId = cloudAccountIdOrExit(auth)
318
+ const client = createBoxClient(
319
+ createCloudBoxProvider({
320
+ endpoint: flag(args, 'endpoint') ?? process.env[ENDPOINT_ENV]?.trim() ?? '',
321
+ authority: { kind: 'account', apiOrigin: apiOrigin(args, auth), apiKey, accountId },
322
+ }),
323
+ )
324
+ try {
325
+ const boxes = await client.list()
326
+ if (boxes.length === 0) {
327
+ console.log(` no cloud boxes running on this account`)
328
+ return 0
329
+ }
330
+ for (const box of boxes) {
331
+ const minutes = Math.round(box.meteredMs / 60_000)
332
+ console.log(
333
+ ` ${box.name ?? box.boxId} running ${minutes}m since ${box.startedAt}`,
334
+ )
335
+ }
336
+ return 0
337
+ } catch (error) {
338
+ if (error instanceof BoxClientError) {
339
+ console.error(` ${rnx} box ls: ${error.message}`)
340
+ return 1
341
+ }
342
+ throw error
343
+ }
344
+ }
345
+
346
+ async function runCloudBoxConnect(args: string[]): Promise<number> {
347
+ const rnx = rnxPublicBrand.commandName
348
+ const json = args.includes('--json')
349
+ const boxId = boxTarget(args, 'connect')
350
+ if (!boxId) return 1
351
+
352
+ // the ACCOUNT's credential. the account service is the only thing that knows
353
+ // which account a Box belongs to, so it resolves the name and hands back the
354
+ // Box's own token; nothing here writes that token down.
355
+ const { auth, header } = authHeaderOrExit('box connect')
356
+ const apiKey = header.replace(/^Bearer /, '')
357
+ const origin = apiOrigin(args, auth)
358
+
359
+ let session: BoxShellSession | null = null
360
+ let heartbeat: { stop: () => void } | null = null
361
+ try {
362
+ const reference = await resolveCloudBoxReference({ apiOrigin: origin, apiKey, boxId })
363
+ const box = await createBoxClient(
364
+ createCloudBoxProvider({
365
+ endpoint: reference.endpoint,
366
+ authority: { kind: 'attach' },
367
+ }),
368
+ ).attach({
369
+ // no stack here on purpose: the Box reports the one it was created
370
+ // with, and holding somebody else's Box to a stack this process guessed
371
+ // would offer primitives it does not actually have.
372
+ reference: { boxId: reference.boxId, accessToken: reference.token },
373
+ })
374
+ if (json) {
375
+ // the reference without its token. a caller scripting against this gets
376
+ // the Box's identity; the credential stays out of stdout and out of
377
+ // whatever collects it.
378
+ console.log(JSON.stringify({ endpoint: reference.endpoint, ...box.description }))
379
+ return 0
380
+ }
381
+ console.log(` ...connected to Box ${box.id}`)
382
+ if (box.description.pageUrl) {
383
+ console.log(` ...open ${box.description.pageUrl}`)
384
+ }
385
+ console.log(` ...accessing shell\n`)
386
+ session = await box.connect()
387
+ // the same reason `create` heartbeats: the box holds no account credential,
388
+ // so the client using it is what keeps its session alive.
389
+ const sessionId = box.description.sessionId
390
+ if (!sessionId) throw new BoxClientError('the box service returned no session id')
391
+ heartbeat = heartbeatSession({ apiOrigin: origin, apiKey, sessionId })
392
+ return await repl(session, '')
393
+ } catch (error) {
394
+ if (error instanceof BoxClientError) {
395
+ console.error(` ${rnx} box connect: ${error.message}`)
396
+ return 1
397
+ }
398
+ throw error
399
+ } finally {
400
+ heartbeat?.stop()
401
+ session?.close()
402
+ }
403
+ }
404
+
405
+ /**
406
+ * watch a cloud box in a browser.
407
+ *
408
+ * the box runs its app in one Browser Run page, and this opens the Box page,
409
+ * which streams THAT instance rather than booting a second copy. so what the
410
+ * person sees is what the agent is working on, and two people opening the same
411
+ * box see the same screen.
412
+ *
413
+ * the url carries no credential: it is the box's page on the Contrast origin,
414
+ * and opening it signed in resolves the box through the account service. that
415
+ * makes it a link somebody can keep, paste, or reload.
416
+ */
417
+ async function runCloudBoxOpen(args: string[]): Promise<number> {
418
+ const rnx = rnxPublicBrand.commandName
419
+ const json = args.includes('--json')
420
+ const print = args.includes('--print')
421
+ const boxId = boxTarget(args, 'open')
422
+ if (!boxId) return 1
423
+
424
+ // the ACCOUNT's credential, for the same reason connect takes one: the
425
+ // account service is what knows which account owns this Box, and it hands
426
+ // back the Box's own token rather than this process storing one.
427
+ const { auth, header } = authHeaderOrExit('box open')
428
+ const apiKey = header.replace(/^Bearer /, '')
429
+
430
+ try {
431
+ const reference = await resolveCloudBoxReference({
432
+ apiOrigin: apiOrigin(args, auth),
433
+ apiKey,
434
+ boxId,
435
+ })
436
+ const connectUrl = cloudBoxConnectUrl(reference.endpoint, reference.boxId)
437
+ // ensure first: it is what navigates the generation's tab to the Box page,
438
+ // so a box whose browser lapsed gets a new generation with its page
439
+ // restored before anybody is looking at it. without it the page opens onto
440
+ // a box with no simulator running and nothing to stream.
441
+ const browser = await ensureCloudBoxBrowser({
442
+ connectUrl,
443
+ accessToken: reference.token,
444
+ })
445
+ if (!browser.reused && !json && !print) {
446
+ console.log(` ...started this box's browser`)
447
+ }
448
+ const box = await createBoxClient(
449
+ createCloudBoxProvider({
450
+ endpoint: reference.endpoint,
451
+ authority: { kind: 'attach' },
452
+ }),
453
+ ).attach({
454
+ reference: { boxId: reference.boxId, accessToken: reference.token },
455
+ })
456
+ // the box owns this url because it is the only side that knows both the
457
+ // page origin and the route.
458
+ const url = box.description.pageUrl
459
+ if (!url) {
460
+ console.error(
461
+ ` ${rnx} box open: this box service has no page origin configured, so it has no page to open.`,
462
+ )
463
+ return 1
464
+ }
465
+ if (json) {
466
+ console.log(JSON.stringify({ url }))
467
+ return 0
468
+ }
469
+ if (print) {
470
+ console.log(url)
471
+ return 0
472
+ }
473
+ return await openInBrowser(url)
474
+ } catch (error) {
475
+ if (error instanceof BoxClientError) {
476
+ console.error(` ${rnx} box open: ${error.message}`)
477
+ return 1
478
+ }
479
+ throw error
480
+ }
481
+ }
482
+
483
+ /**
484
+ * hand a URL to the operating system's browser.
485
+ *
486
+ * argv rather than a shell string, because this URL is a bearer capability and
487
+ * a shell would take it through history, expansion and quoting on the way. when
488
+ * `open` cannot run it is printed instead: this process is the only place the
489
+ * URL exists, so dropping it would cost the caller another mint.
490
+ */
491
+ export async function openInBrowser(url: string): Promise<number> {
492
+ const child = spawn('open', [url], { stdio: 'ignore' })
493
+ const opened = await new Promise<boolean>((resolve) => {
494
+ child.once('error', () => resolve(false))
495
+ child.once('close', (code) => resolve(code === 0))
496
+ })
497
+ if (!opened) console.log(url)
498
+ return 0
499
+ }
500
+
501
+ function cloudBoxClient(options: {
502
+ endpoint: string
503
+ apiOrigin: string
504
+ apiKey: string
505
+ accountId: string | null
506
+ }) {
507
+ return createBoxClient(
508
+ createCloudBoxProvider({
509
+ endpoint: options.endpoint,
510
+ authority: {
511
+ kind: 'account',
512
+ apiOrigin: options.apiOrigin,
513
+ apiKey: options.apiKey,
514
+ accountId: options.accountId,
515
+ },
516
+ }),
517
+ )
518
+ }
519
+
520
+ /**
521
+ * create a Box that runs an app somebody already built.
522
+ *
523
+ * `box create --bundle` reaches this with a file from disk and `rnx box send`
524
+ * with the artifact it just produced. both are the same box: the bundle is
525
+ * the whole project, the provenance says so, and the page that opens it boots
526
+ * that bundle instead of building anything.
527
+ */
528
+ export async function createPrebuiltBundleBox(options: {
529
+ bundle: Uint8Array
530
+ name: string
531
+ endpoint: string
532
+ apiOrigin: string
533
+ apiKey: string
534
+ accountId: string | null
535
+ }) {
536
+ return cloudBoxClient(options).create({
537
+ name: options.name,
538
+ stack: prebuiltBundleBoxTemplate.stack,
539
+ template: BOX_TEMPLATES.resolve({ name: prebuiltBundleBoxTemplate.name }).provenance,
540
+ source: new BundleFilePlane(options.bundle),
541
+ })
542
+ }
543
+
544
+ async function runCloudBoxCreate(
545
+ args: string[],
546
+ opts: BoxCommandOptions,
547
+ ): Promise<number> {
548
+ const rnx = rnxPublicBrand.commandName
549
+ const json = args.includes('--json')
550
+ const endpoint = flag(args, 'endpoint') ?? process.env[ENDPOINT_ENV]?.trim()
551
+ if (!endpoint) {
552
+ console.error(
553
+ ` ${rnx} box create: no cloud box service to create a box in.\n` +
554
+ ` set ${ENDPOINT_ENV} or pass --endpoint=<url>. \`${rnx} box\` runs the\n` +
555
+ ` same shell locally over this checkout.`,
556
+ )
557
+ return 1
558
+ }
559
+
560
+ // --bundle IS the template choice: the box runs an app that is already
561
+ // built, so naming a second template would ask for two different boxes.
562
+ const bundlePath = flag(args, 'bundle')
563
+ const namedTemplate = flag(args, 'template')
564
+ if (bundlePath && namedTemplate) {
565
+ console.error(
566
+ ` ${rnx} box create: --bundle creates a ${prebuiltBundleBoxTemplate.name} box,\n` +
567
+ ` so it cannot be combined with --template ${namedTemplate}.`,
568
+ )
569
+ return 1
570
+ }
571
+ const templateName = bundlePath
572
+ ? prebuiltBundleBoxTemplate.name
573
+ : (namedTemplate ?? sourceControlBoxTemplate.name)
574
+ let template: BoxTemplateResolution
575
+ try {
576
+ template = BOX_TEMPLATES.resolve({ name: templateName })
577
+ } catch (error) {
578
+ console.error(
579
+ ` ${rnx} box create: ${error instanceof Error ? error.message : String(error)}.`,
580
+ )
581
+ return 1
582
+ }
583
+
584
+ // a prebuilt box holds the bundle; a source box holds the checkout it is
585
+ // built from. nothing holds both, so they are not two nullable variables.
586
+ let source: { bundle: Uint8Array } | { checkout: CheckoutFilePlane }
587
+ let defaultName: string
588
+ if (bundlePath) {
589
+ // a prebuilt box carries no sources, so there is no checkout to read and
590
+ // this can run anywhere, including a directory that is not a repository.
591
+ const resolved = path.resolve(bundlePath)
592
+ // the page boots these bytes with no asset server behind it, so the Metro
593
+ // assets beside the bundle are embedded the way every other remote path
594
+ // embeds them. the box service owns the size ceiling, not this producer.
595
+ let artifact: CloudBoxArtifactProduction
596
+ try {
597
+ artifact = await produceCloudBoxArtifact(resolved)
598
+ } catch (error) {
599
+ console.error(
600
+ ` ${rnx} box create: could not prepare the bundle at ${resolved}: ${
601
+ error instanceof Error ? error.message : String(error)
602
+ }`,
603
+ )
604
+ return 1
605
+ }
606
+ if (artifact.bytes.byteLength === 0) {
607
+ console.error(` ${rnx} box create: the bundle at ${resolved} is empty.`)
608
+ return 1
609
+ }
610
+ for (const warning of artifact.warnings) console.error(` ${warning}`)
611
+ source = { bundle: new Uint8Array(artifact.bytes) }
612
+ defaultName = path.basename(resolved).replace(/\.[^.]+$/, '')
613
+ } else {
614
+ const root = findRepositoryPackageRoot(process.cwd())
615
+ if (!root) {
616
+ console.error(
617
+ ` ${rnx} box create: no project here. a box is created from a checkout,\n` +
618
+ ` so run this inside a git repository that has a package.json.`,
619
+ )
620
+ return 1
621
+ }
622
+ const checkout = new CheckoutFilePlane(root, { trackedOnly: true })
623
+ try {
624
+ await checkout.refresh()
625
+ } catch (error) {
626
+ if (error instanceof CheckoutRequiresGitError) {
627
+ console.error(` ${rnx} box create: ${error.message}`)
628
+ return 1
629
+ }
630
+ throw error
631
+ }
632
+ source = { checkout }
633
+ defaultName = path.basename(root)
634
+ }
635
+
636
+ const name = flag(args, 'name') ?? defaultName
637
+ // the ACCOUNT's key. the box service forwards it to /v1/rnx/sessions, which
638
+ // is what authenticates this caller, names the account, and starts the
639
+ // session the box is billed against.
640
+ const { auth, header } = authHeaderOrExit('box create')
641
+ const apiKey = header.replace(/^Bearer /, '')
642
+ const accountId = cloudAccountIdOrExit(auth)
643
+
644
+ if (!json) console.log(` ...creating cloud box ${name}`)
645
+ const connection = {
646
+ endpoint,
647
+ apiOrigin: apiOrigin(args, auth),
648
+ apiKey,
649
+ accountId,
650
+ }
651
+ let session: BoxShellSession | null = null
652
+ let heartbeat: { stop: () => void } | null = null
653
+ try {
654
+ const box =
655
+ 'bundle' in source
656
+ ? await createPrebuiltBundleBox({ ...connection, bundle: source.bundle, name })
657
+ : await cloudBoxClient(connection).create({
658
+ name,
659
+ stack: template.stack,
660
+ template: template.provenance,
661
+ source: source.checkout,
662
+ onProgress:
663
+ opts.verbose && !json
664
+ ? (seeded, total) => console.log(` ...seeded ${seeded}/${total} files`)
665
+ : undefined,
666
+ })
667
+ if (json) {
668
+ console.log(
669
+ JSON.stringify({
670
+ ...box.description,
671
+ ...(box.accessToken ? { accessToken: box.accessToken } : {}),
672
+ }),
673
+ )
674
+ return 0
675
+ }
676
+ console.log(` ...created Box ${box.id}`)
677
+ console.log(
678
+ 'bundle' in source
679
+ ? ` ...seeded a prebuilt bundle of ${source.bundle.byteLength} bytes`
680
+ : ` ...seeded ${box.description.files} project files`,
681
+ )
682
+ // shown once and never stored: this process does not own a credential
683
+ // store, and inventing one here would be a second place secrets live.
684
+ console.log(` ...box token (shown once): ${box.accessToken}`)
685
+ // last, so it is the line still on screen when the shell takes over. it
686
+ // carries no credential: opening it signed in resolves the box through the
687
+ // account service.
688
+ if (box.description.pageUrl) {
689
+ console.log(` ...open ${box.description.pageUrl}`)
690
+ }
691
+ console.log(` ...accessing shell\n`)
692
+ session = await box.connect()
693
+ // bank the wall clock while the shell is open. the box holds no account
694
+ // credential, so the client that is using it is what keeps the session
695
+ // alive; when this process dies the sweep closes it at the last heartbeat.
696
+ const sessionId = box.description.sessionId
697
+ if (!sessionId) throw new BoxClientError('the box service returned no session id')
698
+ heartbeat = heartbeatSession({
699
+ apiOrigin: apiOrigin(args, auth),
700
+ apiKey,
701
+ sessionId,
702
+ })
703
+ return await repl(session, '')
704
+ } catch (error) {
705
+ if (error instanceof BoxClientError) {
706
+ console.error(` ${rnx} box create: ${error.message}`)
707
+ return 1
708
+ }
709
+ throw error
710
+ } finally {
711
+ heartbeat?.stop()
712
+ session?.close()
713
+ }
714
+ }
715
+
716
+ async function runLocalBox(opts: BoxCommandOptions): Promise<number> {
717
+ // findRepositoryPackageRoot answers both halves of what a box needs: the
718
+ // nearest package.json, and only when it sits inside a git repository. git
719
+ // is what decides which files belong to the project, so a box without it
720
+ // would have to walk the whole directory tree — measured at 230k files on
721
+ // this repo against 15k from git.
722
+ const root = findRepositoryPackageRoot(process.cwd())
723
+ if (!root) {
724
+ console.error(
725
+ ` ${rnxPublicBrand.commandName} box: no project here.\n` +
726
+ ` the box needs a directory inside a git repository that has a\n` +
727
+ ` package.json. it asks git which files belong to the project, so it\n` +
728
+ ` never walks node_modules or build output. run \`git init\` if this is\n` +
729
+ ` a new project.`,
730
+ )
731
+ return 1
732
+ }
733
+
734
+ const plane = new CheckoutFilePlane(root)
735
+ try {
736
+ await plane.refresh()
737
+ } catch (error) {
738
+ if (error instanceof CheckoutRequiresGitError) {
739
+ console.error(` ${rnxPublicBrand.commandName} box: ${error.message}`)
740
+ return 1
741
+ }
742
+ throw error
743
+ }
744
+
745
+ let session: BoxShellSession | null = null
746
+ const client = createBoxClient(
747
+ createBoxHostProvider({
748
+ shell: {
749
+ commands: [
750
+ createRnxCommand({
751
+ root,
752
+ shellCwd: () => session?.cwd ?? PROJECT_MOUNT,
753
+ }),
754
+ ],
755
+ // just-bash's JavaScript rg reads every file body through the plane.
756
+ // see search-command.ts for the numbers.
757
+ builtInCommandOverrides: [createSearchCommand({ root })],
758
+ },
759
+ }),
760
+ )
761
+ const box = await client.create({
762
+ name: path.basename(root),
763
+ stack: terminalBoxStack,
764
+ source: plane,
765
+ })
766
+ session = await box.connect()
767
+
768
+ const relative = path.relative(root, process.cwd())
769
+ console.log(` ...starting local box`)
770
+ console.log(` ...mounting ${root} at ${PROJECT_MOUNT}`)
771
+
772
+ // authorize this checkout for the bridge daemon so a browser tab can reach
773
+ // this box's plane. deliberately NOT awaited: a box must not pay for a
774
+ // bridge that may not be there, and an older daemon never answers at all.
775
+ const shared = sharePlane(root, { port: opts.port })
776
+ // two boxes over one checkout is worth SAYING, not refusing. a person with
777
+ // two terminals in one project is doing something reasonable, and the plane
778
+ // re-reads the file list before every command, so neither box works from a
779
+ // stale one. what it does break is an agent mid-run, whose file set changes
780
+ // underneath it because something else is writing the tree. that is the wave
781
+ // case, and the wave's own isolation check is where a refusal belongs; this
782
+ // reports the fact so that check has something to refuse on.
783
+ void shared.settled.then((outcome) => {
784
+ if ('declined' in outcome) {
785
+ if (opts.verbose) console.log(` ...plane not shared: ${outcome.declined}`)
786
+ return
787
+ }
788
+ if (outcome.holders > 1) {
789
+ console.error(
790
+ `\n ${rnxPublicBrand.commandName} box: ${outcome.holders - 1} other box${
791
+ outcome.holders > 2 ? 'es are' : ' is'
792
+ } already open on this checkout.\n` +
793
+ ` writes from either land on the same files. that is fine for a person\n` +
794
+ ` with two terminals, and not fine for an agent, whose file set will\n` +
795
+ ` change underneath it mid-run.\n`,
796
+ )
797
+ return
798
+ }
799
+ if (opts.verbose) {
800
+ console.log(` ...plane shared on the bridge (port ${outcome.port})`)
801
+ }
802
+ })
803
+
804
+ console.log(` ...accessing shell\n`)
805
+ if (opts.verbose) {
806
+ console.log(` ${plane.index().size} project files indexed\n`)
807
+ }
808
+
809
+ try {
810
+ // the checkout is shared with editors, agents, and the user's own terminal.
811
+ // BoxProjectFs arms a stale bit on each shell turn and refreshes the
812
+ // plane synchronously on the first actual file access, so commands that
813
+ // never touch project files pay nothing, while reads always see fresh disk state.
814
+ return await repl(session, relative)
815
+ } finally {
816
+ session.close()
817
+ shared?.close()
818
+ }
819
+ }
820
+
821
+ // the REPL drives whatever holds the shell. locally that is an in-process
822
+ // BoxShell over this checkout; for a cloud box it is a socket to the box,
823
+ // which holds both the files and the shell. same loop, because the only
824
+ // difference is where exec runs.
825
+
826
+ async function repl(
827
+ session: BoxShellSession,
828
+ startRelative: string,
829
+ beforeCommand?: () => Promise<void>,
830
+ ): Promise<number> {
831
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
832
+
833
+ // enter where the user was standing, so `rnx box` from a subdirectory does
834
+ // not silently teleport them to the project root.
835
+ if (startRelative && !startRelative.startsWith('..')) {
836
+ await session.exec(`cd ${JSON.stringify(startRelative)}`)
837
+ }
838
+
839
+ const prompt = () => {
840
+ rl.setPrompt(`${session.cwd} > `)
841
+ rl.prompt()
842
+ }
843
+
844
+ // readline delivers a pasted block, or a piped script, as a burst of `line`
845
+ // events. run them one at a time in order — dropping the ones that arrive
846
+ // while a command is running would silently execute only the first line of
847
+ // a paste.
848
+ let queue: Promise<void> = Promise.resolve()
849
+ // stdin ended: stop printing prompts, but still finish what is already
850
+ // queued — piping a script in must run every line, not just the first.
851
+ let inputEnded = false
852
+ // the user typed `exit`: abandon anything still queued behind it.
853
+ let stopped = false
854
+ // the command running right now, so Ctrl-C interrupts it rather than the CLI.
855
+ let interrupt: AbortController | null = null
856
+
857
+ const run = async (command: string) => {
858
+ if (stopped) return
859
+ const controller = new AbortController()
860
+ interrupt = controller
861
+ try {
862
+ await beforeCommand?.()
863
+ const result = await session.exec(command, {
864
+ signal: controller.signal,
865
+ // written as it arrives rather than collected: a terminal shows a
866
+ // command's output while it runs, and stderr keeps its own stream so
867
+ // piping the shell's stdout somewhere still separates the two.
868
+ onOutput: (stream, chunk) => {
869
+ if (stream === 'stdout') process.stdout.write(chunk)
870
+ else process.stderr.write(chunk)
871
+ },
872
+ })
873
+ if (result.exitCode !== 0) console.log(`exit code: ${result.exitCode}`)
874
+ } catch (error) {
875
+ console.error(error instanceof Error ? error.message : String(error))
876
+ } finally {
877
+ interrupt = null
878
+ }
879
+ if (!inputEnded && !stopped) prompt()
880
+ }
881
+
882
+ return new Promise<number>((resolve) => {
883
+ prompt()
884
+
885
+ // Ctrl-C while a command is running interrupts the command. at an idle
886
+ // prompt there is nothing to interrupt, so it still leaves the shell,
887
+ // which is the only way out of a box that has stopped answering.
888
+ rl.on('SIGINT', () => {
889
+ if (interrupt) {
890
+ process.stdout.write('^C\n')
891
+ interrupt.abort()
892
+ return
893
+ }
894
+ stopped = true
895
+ rl.close()
896
+ })
897
+
898
+ rl.on('line', (line) => {
899
+ const command = line.trim()
900
+ if (!command) {
901
+ queue = queue.then(() => {
902
+ if (!inputEnded && !stopped) prompt()
903
+ })
904
+ return
905
+ }
906
+ if (command === 'exit' || command === 'quit') {
907
+ queue = queue.then(() => {
908
+ stopped = true
909
+ rl.close()
910
+ })
911
+ return
912
+ }
913
+ queue = queue.then(() => run(command))
914
+ })
915
+
916
+ rl.on('close', () => {
917
+ inputEnded = true
918
+ // let any already-queued command finish before the process exits.
919
+ queue.then(() => {
920
+ console.log()
921
+ resolve(0)
922
+ })
923
+ })
924
+ })
925
+ }