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.
- package/LICENSE +22 -0
- package/README.md +640 -0
- package/cli/app-config.ts +82 -0
- package/cli/app-fonts.ts +388 -0
- package/cli/app-project.ts +59 -0
- package/cli/app-splash.ts +171 -0
- package/cli/app-state-reset.ts +26 -0
- package/cli/auth.ts +184 -0
- package/cli/bin.ts +59 -0
- package/cli/bridge-diagnostics.ts +226 -0
- package/cli/bridge-flow-runner.ts +2806 -0
- package/cli/browser-backend.ts +44 -0
- package/cli/browser-evals.ts +96 -0
- package/cli/cli-update.ts +261 -0
- package/cli/cloud-client.ts +1530 -0
- package/cli/cloud-dispatch.ts +206 -0
- package/cli/cloud-session.ts +227 -0
- package/cli/command-contract.ts +221 -0
- package/cli/command-registry.ts +304 -0
- package/cli/commands/agent-wrapper.ts +1048 -0
- package/cli/commands/agent.ts +439 -0
- package/cli/commands/app-fonts.ts +99 -0
- package/cli/commands/assert.ts +542 -0
- package/cli/commands/auth.ts +71 -0
- package/cli/commands/box/bundle-plane.ts +71 -0
- package/cli/commands/box/checkout-plane.ts +256 -0
- package/cli/commands/box/plane-share.ts +208 -0
- package/cli/commands/box/rnx-command.ts +60 -0
- package/cli/commands/box/search-command.ts +157 -0
- package/cli/commands/box.ts +925 -0
- package/cli/commands/camera.ts +266 -0
- package/cli/commands/cleanup.ts +169 -0
- package/cli/commands/compat.ts +176 -0
- package/cli/commands/config.ts +34 -0
- package/cli/commands/control.ts +2154 -0
- package/cli/commands/cpu-profile.ts +941 -0
- package/cli/commands/daemon-mac-app.ts +165 -0
- package/cli/commands/daemon.ts +800 -0
- package/cli/commands/debug.ts +751 -0
- package/cli/commands/desktop.ts +39 -0
- package/cli/commands/detect.ts +201 -0
- package/cli/commands/detox.ts +481 -0
- package/cli/commands/device.ts +134 -0
- package/cli/commands/diagnose.ts +589 -0
- package/cli/commands/do-chain.ts +408 -0
- package/cli/commands/electron.ts +94 -0
- package/cli/commands/film.ts +379 -0
- package/cli/commands/flow.ts +1240 -0
- package/cli/commands/inspect/actions.ts +516 -0
- package/cli/commands/inspect/core.ts +2086 -0
- package/cli/commands/inspect/count.ts +17 -0
- package/cli/commands/inspect/describe.ts +119 -0
- package/cli/commands/inspect/env.ts +25 -0
- package/cli/commands/inspect/find.ts +169 -0
- package/cli/commands/inspect/get-layout.ts +53 -0
- package/cli/commands/inspect/keyboard.ts +54 -0
- package/cli/commands/inspect/list.ts +175 -0
- package/cli/commands/inspect/memory.ts +215 -0
- package/cli/commands/inspect/redaction.ts +39 -0
- package/cli/commands/inspect/resolve-target.ts +77 -0
- package/cli/commands/inspect/screens.ts +78 -0
- package/cli/commands/inspect/settle.ts +32 -0
- package/cli/commands/inspect/settling.ts +197 -0
- package/cli/commands/inspect/shared.ts +259 -0
- package/cli/commands/inspect/sleep.ts +15 -0
- package/cli/commands/inspect/tree.ts +23 -0
- package/cli/commands/inspect/url.ts +17 -0
- package/cli/commands/inspect/wait-event.ts +211 -0
- package/cli/commands/inspect/wait-idle.ts +25 -0
- package/cli/commands/inspect/wait-ready.ts +87 -0
- package/cli/commands/inspect/wait-selector.ts +59 -0
- package/cli/commands/inspect/wait.ts +32 -0
- package/cli/commands/inspect.ts +4738 -0
- package/cli/commands/install-cli.ts +275 -0
- package/cli/commands/install-desktop.ts +385 -0
- package/cli/commands/login.ts +357 -0
- package/cli/commands/logout.ts +24 -0
- package/cli/commands/maestro-generate.ts +375 -0
- package/cli/commands/maestro.ts +536 -0
- package/cli/commands/mode.ts +58 -0
- package/cli/commands/no-bridge-hint.ts +80 -0
- package/cli/commands/perf.ts +67 -0
- package/cli/commands/permissions.ts +203 -0
- package/cli/commands/platform.ts +410 -0
- package/cli/commands/preview.ts +103 -0
- package/cli/commands/profile.ts +106 -0
- package/cli/commands/react.ts +353 -0
- package/cli/commands/record.ts +1435 -0
- package/cli/commands/report-issue.ts +305 -0
- package/cli/commands/reset.ts +89 -0
- package/cli/commands/runtime.ts +348 -0
- package/cli/commands/screenshot-command.ts +106 -0
- package/cli/commands/screenshot-layers.ts +144 -0
- package/cli/commands/screenshot-mode.ts +38 -0
- package/cli/commands/screenshot.ts +492 -0
- package/cli/commands/screenshots-capture.ts +608 -0
- package/cli/commands/screenshots.ts +127 -0
- package/cli/commands/serve.ts +139 -0
- package/cli/commands/setup.ts +416 -0
- package/cli/commands/shell-boolean-mode.ts +82 -0
- package/cli/commands/skills.ts +468 -0
- package/cli/commands/slides.ts +357 -0
- package/cli/commands/state.ts +94 -0
- package/cli/commands/storage.ts +58 -0
- package/cli/commands/telemetry.ts +55 -0
- package/cli/commands/test.ts +535 -0
- package/cli/commands/three-mode.ts +771 -0
- package/cli/commands/timeline.ts +121 -0
- package/cli/commands/upgrade.ts +167 -0
- package/cli/commands/upload.ts +1203 -0
- package/cli/commands/version.ts +63 -0
- package/cli/commands/welcome.ts +17 -0
- package/cli/commands/what-happened.ts +328 -0
- package/cli/current-sim.ts +217 -0
- package/cli/desktop-companion.ts +315 -0
- package/cli/drivers/electron.ts +70 -0
- package/cli/drivers/index.ts +20 -0
- package/cli/drivers/playwright-provisioning.ts +184 -0
- package/cli/drivers/playwright-sim-host.ts +537 -0
- package/cli/drivers/playwright.ts +209 -0
- package/cli/drivers/registry.ts +65 -0
- package/cli/drivers/types.ts +102 -0
- package/cli/flow-export.ts +55 -0
- package/cli/flow-file.ts +142 -0
- package/cli/flow-live-status.ts +126 -0
- package/cli/flow-session.ts +140 -0
- package/cli/help-core.ts +143 -0
- package/cli/help.ts +44 -0
- package/cli/hidden-runtime-alias.ts +19 -0
- package/cli/hints.ts +214 -0
- package/cli/inspect-notice-state.ts +114 -0
- package/cli/internal-child.ts +176 -0
- package/cli/maestro-js.ts +323 -0
- package/cli/main.ts +704 -0
- package/cli/open-url.ts +8 -0
- package/cli/outbound-endpoints.ts +830 -0
- package/cli/parent-pid.ts +204 -0
- package/cli/parse-args.ts +189 -0
- package/cli/privacy.ts +53 -0
- package/cli/prompt.ts +56 -0
- package/cli/recording-access.ts +108 -0
- package/cli/registry.ts +2 -0
- package/cli/resolve-assets.ts +63 -0
- package/cli/rnx-in-process.ts +98 -0
- package/cli/run-registry.ts +226 -0
- package/cli/run-rnx-core.ts +148 -0
- package/cli/run-rnx.ts +137 -0
- package/cli/runtime-notes.ts +66 -0
- package/cli/runtime-summary.ts +25 -0
- package/cli/secure-file.ts +31 -0
- package/cli/self-invocation.ts +58 -0
- package/cli/send-to-box.ts +109 -0
- package/cli/setup-repository.ts +191 -0
- package/cli/shell-init.ts +98 -0
- package/cli/standalone.ts +4 -0
- package/cli/telemetry.ts +84 -0
- package/cli/ui.ts +68 -0
- package/cli/ws-bridge.ts +1020 -0
- package/detox/attempt-events.cjs +23 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +48 -0
- package/detox/expectations.ts +534 -0
- package/detox/gestures.ts +501 -0
- package/detox/index.ts +2694 -0
- package/detox/jest-preset.cjs +29 -0
- package/detox/jest-setup-after-env.cjs +22 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation-lifecycle.ts +290 -0
- package/detox/navigation.ts +43 -0
- package/detox/proof-frame.cjs +114 -0
- package/detox/record-block-execution.cjs +78 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-lib/agent-daemon-client.cjs +502 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +759 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +208 -0
- package/dist-lib/backend-origin.cjs +88 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/bridge-contract-input.cjs +451 -0
- package/dist-lib/bridge-contract-input.mjs +414 -0
- package/dist-lib/bridge-contract.cjs +130 -0
- package/dist-lib/bridge-contract.mjs +94 -0
- package/dist-lib/capture-contract.cjs +220 -0
- package/dist-lib/capture-contract.mjs +190 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/cloud-contract.cjs +194 -0
- package/dist-lib/cloud-contract.mjs +145 -0
- package/dist-lib/cloud.cjs +670 -0
- package/dist-lib/cloud.mjs +639 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +3789 -0
- package/dist-lib/detox/jest-preset.cjs +29 -0
- package/dist-lib/dev-bundle-resolution.cjs +290 -0
- package/dist-lib/home-paths.cjs +638 -0
- package/dist-lib/host/bridge-host.cjs +10903 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/replacement-module-handler.cjs +469 -0
- package/dist-lib/host/websocket-proxy.cjs +298 -0
- package/dist-lib/index.cjs +4268 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/jump-to-source-native.cjs +112 -0
- package/dist-lib/menu.cjs +646 -0
- package/dist-lib/menu.mjs +604 -0
- package/dist-lib/metro-fingerprint-registry.cjs +186 -0
- package/dist-lib/metro-fingerprint-registry.mjs +156 -0
- package/dist-lib/metro-production-bundle.cjs +329 -0
- package/dist-lib/metro-production-bundle.mjs +295 -0
- package/dist-lib/metro.cjs +904 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/react-native-host-modules.cjs +51 -0
- package/dist-lib/react-native-host-modules.mjs +25 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
- package/dist-lib/sdk.cjs +2433 -0
- package/dist-lib/sdk.mjs +2336 -0
- package/dist-lib/skills.cjs +39132 -0
- package/dist-lib/swift-submit.cjs +92 -0
- package/dist-lib/swift-submit.mjs +65 -0
- package/dist-lib/swift.cjs +1646 -0
- package/dist-lib/vite.cjs +1591 -0
- package/package.json +290 -0
- package/scripts/dev-server-scanner.ts +1291 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/skills/contrast/SKILL.md +165 -0
- package/skills/rnx-debug/SKILL.md +433 -0
- package/skills/rnx-perf/SKILL.md +399 -0
- package/skills/rnx-setup/SKILL.md +172 -0
- package/skills/rnx-test/SKILL.md +321 -0
- package/skills/rnx-visual/SKILL.md +126 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +580 -0
- package/src/app-url.ts +39 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +235 -0
- package/src/backend-origin.ts +70 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +175 -0
- package/src/bridge-contract-input.ts +502 -0
- package/src/bridge-contract.ts +558 -0
- package/src/browser-cache.ts +16 -0
- package/src/capture-contract.ts +587 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +40 -0
- package/src/cloud-contract.ts +274 -0
- package/src/cloud.ts +762 -0
- package/src/codex-client.ts +234 -0
- package/src/config.ts +132 -0
- package/src/connect.ts +101 -0
- package/src/dev-bundle-resolution.ts +346 -0
- package/src/dev-server-open.ts +198 -0
- package/src/disk-cleanup.ts +540 -0
- package/src/flow-timeout-scale.ts +15 -0
- package/src/home-paths.ts +806 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2788 -0
- package/src/host/fetch-proxy-handler.ts +449 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/plane-host.ts +203 -0
- package/src/host/replacement-module-handler.ts +158 -0
- package/src/host/websocket-proxy.ts +315 -0
- package/src/index.ts +71 -0
- package/src/jump-to-source-babel.ts +135 -0
- package/src/jump-to-source-native.ts +102 -0
- package/src/menu.ts +777 -0
- package/src/metro-fingerprint-registry.ts +302 -0
- package/src/metro-plugin.ts +341 -0
- package/src/metro-production-bundle.ts +471 -0
- package/src/native-dev-bundle-url.ts +118 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/playwright-host-log.ts +16 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +123 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/react-native-host-modules.ts +26 -0
- package/src/render-mode.ts +27 -0
- package/src/rolldown-plugin-swift.ts +240 -0
- package/src/runtime-assets.ts +233 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +514 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +15 -0
- package/src/settings-bundle.ts +418 -0
- package/src/shell-bridge-identity.ts +15 -0
- package/src/sim-client.ts +697 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/swift-submit.ts +185 -0
- package/src/swift.ts +7 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +264 -0
- package/src/vite-plugin-swift.ts +620 -0
- package/src/vite-plugin.ts +1946 -0
- 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
|
+
}
|