rnxsim 0.1.441 → 0.1.443
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/cli/bin.ts +3 -1
- package/cli/cloud-client.ts +153 -17
- package/cli/cloud-dispatch.ts +15 -0
- package/cli/cloud-session.ts +69 -17
- package/cli/commands/box/bundle-plane.ts +71 -0
- package/cli/commands/box.ts +274 -115
- package/cli/commands/control.ts +1 -137
- package/cli/commands/flow.ts +1 -1
- package/cli/commands/inspect/list.ts +119 -2
- package/cli/commands/inspect.ts +3 -2
- package/cli/commands/platform.ts +95 -12
- package/cli/commands/reset.ts +7 -3
- package/cli/commands/state.ts +7 -3
- package/cli/flow-export.ts +55 -0
- package/cli/flow-session.ts +0 -47
- package/cli/outbound-endpoints.ts +5 -0
- package/cli/rnx-in-process.ts +98 -0
- package/cli/ws-bridge.ts +30 -0
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +69 -14
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +17 -1
- package/dist-lib/host/bridge-host.cjs +24 -13
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +1 -3
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +6 -19
- package/dist-lib/menu.mjs +6 -19
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +274 -195
- package/dist-lib/vite.cjs +1 -1
- package/package.json +8 -1
- package/src/home-paths.ts +31 -1
- package/src/menu.ts +4 -17
package/cli/commands/box.ts
CHANGED
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
// --json asks for a noninteractive result. connect asks the account service to
|
|
6
6
|
// turn a Box name into an authorized reference and enters that same shell, so
|
|
7
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.
|
|
8
12
|
|
|
9
13
|
import { spawn } from 'node:child_process'
|
|
14
|
+
import fs from 'node:fs/promises'
|
|
10
15
|
import path from 'node:path'
|
|
11
16
|
import { createInterface } from 'node:readline'
|
|
12
17
|
import { BoxClientError, createBoxClient } from '@rnx/box/client'
|
|
@@ -15,21 +20,23 @@ import { PROJECT_MOUNT } from '@rnx/box/shell'
|
|
|
15
20
|
import { terminalBoxStack } from '@rnx/box/stack'
|
|
16
21
|
import {
|
|
17
22
|
BoxTemplateRegistry,
|
|
23
|
+
prebuiltBundleBoxTemplate,
|
|
18
24
|
sourceControlBoxTemplate,
|
|
19
25
|
terminalBoxTemplate,
|
|
20
26
|
} from '@rnx/box/template'
|
|
21
27
|
import {
|
|
22
|
-
cloudBoxBrowserLiveView,
|
|
23
28
|
cloudBoxConnectUrl,
|
|
24
29
|
createCloudBoxProvider,
|
|
25
30
|
ensureCloudBoxBrowser,
|
|
26
31
|
heartbeatSession,
|
|
27
32
|
resolveCloudBoxReference,
|
|
28
33
|
} from 'rnx-cloud-box/client'
|
|
34
|
+
import { readDefaultBox, writeDefaultBox } from '../../src/home-paths'
|
|
29
35
|
import { rnxPublicBrand } from '../../src/public-brand'
|
|
30
36
|
import { authHeaderOrExit, cloudAccountIdOrExit, type CliAuth } from '../auth'
|
|
31
37
|
import { findRepositoryPackageRoot } from '../setup-repository'
|
|
32
38
|
import { resolvePlacement } from '../ws-bridge'
|
|
39
|
+
import { BundleFilePlane } from './box/bundle-plane'
|
|
33
40
|
import { CheckoutFilePlane, CheckoutRequiresGitError } from './box/checkout-plane'
|
|
34
41
|
import { sharePlane } from './box/plane-share'
|
|
35
42
|
import { createRnxCommand } from './box/rnx-command'
|
|
@@ -50,49 +57,13 @@ const API_ORIGIN_ENV = 'RNX_API_ORIGIN'
|
|
|
50
57
|
const BOX_TEMPLATES = new BoxTemplateRegistry([
|
|
51
58
|
terminalBoxTemplate,
|
|
52
59
|
sourceControlBoxTemplate,
|
|
60
|
+
prebuiltBundleBoxTemplate,
|
|
53
61
|
])
|
|
54
62
|
|
|
55
|
-
function usage(): void {
|
|
56
|
-
const rnx = rnxPublicBrand.commandName
|
|
57
|
-
console.log(`
|
|
58
|
-
${rnx} box — start a local box and enter its shell
|
|
59
|
-
|
|
60
|
-
usage:
|
|
61
|
-
${rnx} box start a local box over the current checkout
|
|
62
|
-
${rnx} box create create a cloud box and enter its shell
|
|
63
|
-
${rnx} box create --name=<n> create a named cloud box
|
|
64
|
-
${rnx} box create --template=<id>
|
|
65
|
-
create from a registered Box template
|
|
66
|
-
${rnx} box create --endpoint=<url>
|
|
67
|
-
the box service to create it in, or set
|
|
68
|
-
${ENDPOINT_ENV}
|
|
69
|
-
${rnx} box create --json create without entering the shell
|
|
70
|
-
${rnx} box connect <id> enter the shell of a cloud box you own
|
|
71
|
-
${rnx} box connect <id> --json print its reference without a shell
|
|
72
|
-
${rnx} box send send the running local app to a nano box
|
|
73
|
-
${rnx} box send --name=<n> name the box it creates
|
|
74
|
-
${rnx} box send --no-auth leave the signed-in session behind
|
|
75
|
-
${rnx} box open <id> watch a cloud box in your browser
|
|
76
|
-
${rnx} box open <id> --print print the viewing URL instead of opening it
|
|
77
|
-
${rnx} box ls cloud boxes running on this account
|
|
78
|
-
${rnx} box stop --name=<n> stop a cloud box and its billing
|
|
79
|
-
${rnx} box delete --name=<n> stop it and wipe its files
|
|
80
|
-
|
|
81
|
-
a local box mounts this checkout at ${PROJECT_MOUNT}, gives you a shell with
|
|
82
|
-
ls, cat, find, rg, sed, pipes and redirects, and registers ${rnx} itself as a
|
|
83
|
-
shell command. writes go straight to the checkout — there is no second copy.
|
|
84
|
-
`)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
63
|
export async function runBox(
|
|
88
64
|
args: string[],
|
|
89
65
|
opts: BoxCommandOptions = {},
|
|
90
66
|
): Promise<number> {
|
|
91
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
92
|
-
usage()
|
|
93
|
-
return 0
|
|
94
|
-
}
|
|
95
|
-
|
|
96
67
|
// a box inside a box would mount the same checkout twice and give the
|
|
97
68
|
// user two shells writing through one filesystem. refuse at the top rather
|
|
98
69
|
// than letting the second one start and quietly race the first.
|
|
@@ -107,6 +78,7 @@ export async function runBox(
|
|
|
107
78
|
|
|
108
79
|
const subcommand = args.find((arg) => !arg.startsWith('-'))
|
|
109
80
|
if (subcommand === 'create') return runCloudBoxCreate(args, opts)
|
|
81
|
+
if (subcommand === 'use') return runCloudBoxUse(args)
|
|
110
82
|
if (subcommand === 'connect') return runCloudBoxConnect(args)
|
|
111
83
|
if (subcommand === 'send') return runBoxSend(args, opts)
|
|
112
84
|
if (subcommand === 'open') return runCloudBoxOpen(args)
|
|
@@ -153,17 +125,113 @@ function apiOrigin(args: string[], auth: CliAuth): string {
|
|
|
153
125
|
).replace(/\/+$/, '')
|
|
154
126
|
}
|
|
155
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
|
+
|
|
156
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 ios --remote` 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 {
|
|
157
221
|
const endpoint = flag(args, 'endpoint') ?? process.env[ENDPOINT_ENV]?.trim()
|
|
158
222
|
if (endpoint) return endpoint
|
|
159
|
-
const rnx = rnxPublicBrand.commandName
|
|
160
223
|
console.error(
|
|
161
|
-
` ${
|
|
224
|
+
` ${rnxPublicBrand.commandName} ${command}: no cloud box service to talk to.\n` +
|
|
162
225
|
` set ${ENDPOINT_ENV} or pass --endpoint=<url>.`,
|
|
163
226
|
)
|
|
164
227
|
return null
|
|
165
228
|
}
|
|
166
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
|
+
|
|
167
235
|
// `rnx box send` — the CLI half of "send to box". the shell's rail button runs
|
|
168
236
|
// the same function inside the daemon; this is the same send for an agent or a
|
|
169
237
|
// terminal, and it prints the id and URL either way.
|
|
@@ -209,12 +277,8 @@ async function runCloudBoxStop(args: string[], purge: boolean): Promise<number>
|
|
|
209
277
|
const label = purge ? 'delete' : 'stop'
|
|
210
278
|
const endpoint = requireEndpoint(args, label)
|
|
211
279
|
if (!endpoint) return 1
|
|
212
|
-
const name =
|
|
213
|
-
|
|
214
|
-
if (!name) {
|
|
215
|
-
console.error(` ${rnx} box ${label}: which box? pass --name=<n>.`)
|
|
216
|
-
return 1
|
|
217
|
-
}
|
|
280
|
+
const name = boxTarget(args, label)
|
|
281
|
+
if (!name) return 1
|
|
218
282
|
const { auth, header } = authHeaderOrExit(`box ${label}`)
|
|
219
283
|
const apiKey = header.replace(/^Bearer /, '')
|
|
220
284
|
const accountId = cloudAccountIdOrExit(auth)
|
|
@@ -228,6 +292,10 @@ async function runCloudBoxStop(args: string[], purge: boolean): Promise<number>
|
|
|
228
292
|
const result = purge
|
|
229
293
|
? await client.delete({ boxId: name })
|
|
230
294
|
: await client.stop({ boxId: name })
|
|
295
|
+
// the box the default named is gone, so the default goes with it rather
|
|
296
|
+
// than pointing every later command at a box that stopped answering.
|
|
297
|
+
const stored = readDefaultBox()
|
|
298
|
+
if (stored && (stored.boxId === name || stored.name === name)) writeDefaultBox(null)
|
|
231
299
|
console.log(
|
|
232
300
|
purge
|
|
233
301
|
? ` deleted ${name}, freeing ${Number(result.freedBytes ?? 0)} bytes`
|
|
@@ -279,14 +347,8 @@ async function runCloudBoxList(args: string[]): Promise<number> {
|
|
|
279
347
|
async function runCloudBoxConnect(args: string[]): Promise<number> {
|
|
280
348
|
const rnx = rnxPublicBrand.commandName
|
|
281
349
|
const json = args.includes('--json')
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (!boxId) {
|
|
285
|
-
console.error(
|
|
286
|
-
` ${rnx} box connect: which box? pass its ID or name, as \`${rnx} box ls\` prints it.`,
|
|
287
|
-
)
|
|
288
|
-
return 1
|
|
289
|
-
}
|
|
350
|
+
const boxId = boxTarget(args, 'connect')
|
|
351
|
+
if (!boxId) return 1
|
|
290
352
|
|
|
291
353
|
// the ACCOUNT's credential. the account service is the only thing that knows
|
|
292
354
|
// which account a Box belongs to, so it resolves the name and hands back the
|
|
@@ -344,23 +406,21 @@ async function runCloudBoxConnect(args: string[]): Promise<number> {
|
|
|
344
406
|
/**
|
|
345
407
|
* watch a cloud box in a browser.
|
|
346
408
|
*
|
|
347
|
-
* the box
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
409
|
+
* the box runs its app in one Browser Run page, and this opens the Box page,
|
|
410
|
+
* which streams THAT instance rather than booting a second copy. so what the
|
|
411
|
+
* person sees is what the agent is working on, and two people opening the same
|
|
412
|
+
* box see the same screen.
|
|
413
|
+
*
|
|
414
|
+
* the url carries no credential: it is the box's page on the Contrast origin,
|
|
415
|
+
* and opening it signed in resolves the box through the account service. that
|
|
416
|
+
* makes it a link somebody can keep, paste, or reload.
|
|
351
417
|
*/
|
|
352
418
|
async function runCloudBoxOpen(args: string[]): Promise<number> {
|
|
353
419
|
const rnx = rnxPublicBrand.commandName
|
|
354
420
|
const json = args.includes('--json')
|
|
355
421
|
const print = args.includes('--print')
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (!boxId) {
|
|
359
|
-
console.error(
|
|
360
|
-
` ${rnx} box open: which box? pass its ID or name, as \`${rnx} box ls\` prints it.`,
|
|
361
|
-
)
|
|
362
|
-
return 1
|
|
363
|
-
}
|
|
422
|
+
const boxId = boxTarget(args, 'open')
|
|
423
|
+
if (!boxId) return 1
|
|
364
424
|
|
|
365
425
|
// the ACCOUNT's credential, for the same reason connect takes one: the
|
|
366
426
|
// account service is what knows which account owns this Box, and it hands
|
|
@@ -377,7 +437,8 @@ async function runCloudBoxOpen(args: string[]): Promise<number> {
|
|
|
377
437
|
const connectUrl = cloudBoxConnectUrl(reference.endpoint, reference.boxId)
|
|
378
438
|
// ensure first: it is what navigates the generation's tab to the Box page,
|
|
379
439
|
// so a box whose browser lapsed gets a new generation with its page
|
|
380
|
-
// restored before anybody is looking at it.
|
|
440
|
+
// restored before anybody is looking at it. without it the page opens onto
|
|
441
|
+
// a box with no simulator running and nothing to stream.
|
|
381
442
|
const browser = await ensureCloudBoxBrowser({
|
|
382
443
|
connectUrl,
|
|
383
444
|
accessToken: reference.token,
|
|
@@ -385,25 +446,32 @@ async function runCloudBoxOpen(args: string[]): Promise<number> {
|
|
|
385
446
|
if (!browser.reused && !json && !print) {
|
|
386
447
|
console.log(` ...started this box's browser`)
|
|
387
448
|
}
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
449
|
+
const box = await createBoxClient(
|
|
450
|
+
createCloudBoxProvider({
|
|
451
|
+
endpoint: reference.endpoint,
|
|
452
|
+
authority: { kind: 'attach' },
|
|
453
|
+
}),
|
|
454
|
+
).attach({
|
|
455
|
+
reference: { boxId: reference.boxId, accessToken: reference.token },
|
|
392
456
|
})
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
457
|
+
// the box owns this url because it is the only side that knows both the
|
|
458
|
+
// page origin and the route.
|
|
459
|
+
const url = box.description.pageUrl
|
|
460
|
+
if (!url) {
|
|
461
|
+
console.error(
|
|
462
|
+
` ${rnx} box open: this box service has no page origin configured, so it has no page to open.`,
|
|
399
463
|
)
|
|
464
|
+
return 1
|
|
465
|
+
}
|
|
466
|
+
if (json) {
|
|
467
|
+
console.log(JSON.stringify({ url }))
|
|
400
468
|
return 0
|
|
401
469
|
}
|
|
402
470
|
if (print) {
|
|
403
|
-
console.log(
|
|
471
|
+
console.log(url)
|
|
404
472
|
return 0
|
|
405
473
|
}
|
|
406
|
-
return await openInBrowser(
|
|
474
|
+
return await openInBrowser(url)
|
|
407
475
|
} catch (error) {
|
|
408
476
|
if (error instanceof BoxClientError) {
|
|
409
477
|
console.error(` ${rnx} box open: ${error.message}`)
|
|
@@ -421,7 +489,7 @@ async function runCloudBoxOpen(args: string[]): Promise<number> {
|
|
|
421
489
|
* `open` cannot run it is printed instead: this process is the only place the
|
|
422
490
|
* URL exists, so dropping it would cost the caller another mint.
|
|
423
491
|
*/
|
|
424
|
-
async function openInBrowser(url: string): Promise<number> {
|
|
492
|
+
export async function openInBrowser(url: string): Promise<number> {
|
|
425
493
|
const child = spawn('open', [url], { stdio: 'ignore' })
|
|
426
494
|
const opened = await new Promise<boolean>((resolve) => {
|
|
427
495
|
child.once('error', () => resolve(false))
|
|
@@ -431,6 +499,49 @@ async function openInBrowser(url: string): Promise<number> {
|
|
|
431
499
|
return 0
|
|
432
500
|
}
|
|
433
501
|
|
|
502
|
+
function cloudBoxClient(options: {
|
|
503
|
+
endpoint: string
|
|
504
|
+
apiOrigin: string
|
|
505
|
+
apiKey: string
|
|
506
|
+
accountId: string | null
|
|
507
|
+
}) {
|
|
508
|
+
return createBoxClient(
|
|
509
|
+
createCloudBoxProvider({
|
|
510
|
+
endpoint: options.endpoint,
|
|
511
|
+
authority: {
|
|
512
|
+
kind: 'account',
|
|
513
|
+
apiOrigin: options.apiOrigin,
|
|
514
|
+
apiKey: options.apiKey,
|
|
515
|
+
accountId: options.accountId,
|
|
516
|
+
},
|
|
517
|
+
}),
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* create a Box that runs an app somebody already built.
|
|
523
|
+
*
|
|
524
|
+
* `box create --bundle` reaches this with a file from disk and `rnx ios
|
|
525
|
+
* --remote` with the artifact it just produced. both are the same box: the
|
|
526
|
+
* bundle is the whole project, the provenance says so, and the page that opens
|
|
527
|
+
* it boots that bundle instead of building anything.
|
|
528
|
+
*/
|
|
529
|
+
export async function createPrebuiltBundleBox(options: {
|
|
530
|
+
bundle: Uint8Array
|
|
531
|
+
name: string
|
|
532
|
+
endpoint: string
|
|
533
|
+
apiOrigin: string
|
|
534
|
+
apiKey: string
|
|
535
|
+
accountId: string | null
|
|
536
|
+
}) {
|
|
537
|
+
return cloudBoxClient(options).create({
|
|
538
|
+
name: options.name,
|
|
539
|
+
stack: prebuiltBundleBoxTemplate.stack,
|
|
540
|
+
template: BOX_TEMPLATES.resolve({ name: prebuiltBundleBoxTemplate.name }).provenance,
|
|
541
|
+
source: new BundleFilePlane(options.bundle),
|
|
542
|
+
})
|
|
543
|
+
}
|
|
544
|
+
|
|
434
545
|
async function runCloudBoxCreate(
|
|
435
546
|
args: string[],
|
|
436
547
|
opts: BoxCommandOptions,
|
|
@@ -447,7 +558,20 @@ async function runCloudBoxCreate(
|
|
|
447
558
|
return 1
|
|
448
559
|
}
|
|
449
560
|
|
|
450
|
-
|
|
561
|
+
// --bundle IS the template choice: the box runs an app that is already
|
|
562
|
+
// built, so naming a second template would ask for two different boxes.
|
|
563
|
+
const bundlePath = flag(args, 'bundle')
|
|
564
|
+
const namedTemplate = flag(args, 'template')
|
|
565
|
+
if (bundlePath && namedTemplate) {
|
|
566
|
+
console.error(
|
|
567
|
+
` ${rnx} box create: --bundle creates a ${prebuiltBundleBoxTemplate.name} box,\n` +
|
|
568
|
+
` so it cannot be combined with --template ${namedTemplate}.`,
|
|
569
|
+
)
|
|
570
|
+
return 1
|
|
571
|
+
}
|
|
572
|
+
const templateName = bundlePath
|
|
573
|
+
? prebuiltBundleBoxTemplate.name
|
|
574
|
+
: (namedTemplate ?? sourceControlBoxTemplate.name)
|
|
451
575
|
let template: BoxTemplateResolution
|
|
452
576
|
try {
|
|
453
577
|
template = BOX_TEMPLATES.resolve({ name: templateName })
|
|
@@ -458,27 +582,55 @@ async function runCloudBoxCreate(
|
|
|
458
582
|
return 1
|
|
459
583
|
}
|
|
460
584
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
585
|
+
// a prebuilt box holds the bundle; a source box holds the checkout it is
|
|
586
|
+
// built from. nothing holds both, so they are not two nullable variables.
|
|
587
|
+
let source: { bundle: Uint8Array } | { checkout: CheckoutFilePlane }
|
|
588
|
+
let defaultName: string
|
|
589
|
+
if (bundlePath) {
|
|
590
|
+
// a prebuilt box carries no sources, so there is no checkout to read and
|
|
591
|
+
// this can run anywhere, including a directory that is not a repository.
|
|
592
|
+
const resolved = path.resolve(bundlePath)
|
|
593
|
+
let read: Buffer
|
|
594
|
+
try {
|
|
595
|
+
read = await fs.readFile(resolved)
|
|
596
|
+
} catch (error) {
|
|
597
|
+
console.error(
|
|
598
|
+
` ${rnx} box create: could not read the bundle at ${resolved}: ${
|
|
599
|
+
error instanceof Error ? error.message : String(error)
|
|
600
|
+
}`,
|
|
601
|
+
)
|
|
476
602
|
return 1
|
|
477
603
|
}
|
|
478
|
-
|
|
604
|
+
if (read.byteLength === 0) {
|
|
605
|
+
console.error(` ${rnx} box create: the bundle at ${resolved} is empty.`)
|
|
606
|
+
return 1
|
|
607
|
+
}
|
|
608
|
+
source = { bundle: new Uint8Array(read) }
|
|
609
|
+
defaultName = path.basename(resolved).replace(/\.[^.]+$/, '')
|
|
610
|
+
} else {
|
|
611
|
+
const root = findRepositoryPackageRoot(process.cwd())
|
|
612
|
+
if (!root) {
|
|
613
|
+
console.error(
|
|
614
|
+
` ${rnx} box create: no project here. a box is created from a checkout,\n` +
|
|
615
|
+
` so run this inside a git repository that has a package.json.`,
|
|
616
|
+
)
|
|
617
|
+
return 1
|
|
618
|
+
}
|
|
619
|
+
const checkout = new CheckoutFilePlane(root, { trackedOnly: true })
|
|
620
|
+
try {
|
|
621
|
+
await checkout.refresh()
|
|
622
|
+
} catch (error) {
|
|
623
|
+
if (error instanceof CheckoutRequiresGitError) {
|
|
624
|
+
console.error(` ${rnx} box create: ${error.message}`)
|
|
625
|
+
return 1
|
|
626
|
+
}
|
|
627
|
+
throw error
|
|
628
|
+
}
|
|
629
|
+
source = { checkout }
|
|
630
|
+
defaultName = path.basename(root)
|
|
479
631
|
}
|
|
480
632
|
|
|
481
|
-
const name = flag(args, 'name') ??
|
|
633
|
+
const name = flag(args, 'name') ?? defaultName
|
|
482
634
|
// the ACCOUNT's key. the box service forwards it to /v1/rnx/sessions, which
|
|
483
635
|
// is what authenticates this caller, names the account, and starts the
|
|
484
636
|
// session the box is billed against.
|
|
@@ -487,25 +639,28 @@ async function runCloudBoxCreate(
|
|
|
487
639
|
const accountId = cloudAccountIdOrExit(auth)
|
|
488
640
|
|
|
489
641
|
if (!json) console.log(` ...creating cloud box ${name}`)
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
642
|
+
const connection = {
|
|
643
|
+
endpoint,
|
|
644
|
+
apiOrigin: apiOrigin(args, auth),
|
|
645
|
+
apiKey,
|
|
646
|
+
accountId,
|
|
647
|
+
}
|
|
496
648
|
let session: BoxShellSession | null = null
|
|
497
649
|
let heartbeat: { stop: () => void } | null = null
|
|
498
650
|
try {
|
|
499
|
-
const box =
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
651
|
+
const box =
|
|
652
|
+
'bundle' in source
|
|
653
|
+
? await createPrebuiltBundleBox({ ...connection, bundle: source.bundle, name })
|
|
654
|
+
: await cloudBoxClient(connection).create({
|
|
655
|
+
name,
|
|
656
|
+
stack: template.stack,
|
|
657
|
+
template: template.provenance,
|
|
658
|
+
source: source.checkout,
|
|
659
|
+
onProgress:
|
|
660
|
+
opts.verbose && !json
|
|
661
|
+
? (seeded, total) => console.log(` ...seeded ${seeded}/${total} files`)
|
|
662
|
+
: undefined,
|
|
663
|
+
})
|
|
509
664
|
if (json) {
|
|
510
665
|
console.log(
|
|
511
666
|
JSON.stringify({
|
|
@@ -516,7 +671,11 @@ async function runCloudBoxCreate(
|
|
|
516
671
|
return 0
|
|
517
672
|
}
|
|
518
673
|
console.log(` ...created Box ${box.id}`)
|
|
519
|
-
console.log(
|
|
674
|
+
console.log(
|
|
675
|
+
'bundle' in source
|
|
676
|
+
? ` ...seeded a prebuilt bundle of ${source.bundle.byteLength} bytes`
|
|
677
|
+
: ` ...seeded ${box.description.files} project files`,
|
|
678
|
+
)
|
|
520
679
|
// shown once and never stored: this process does not own a credential
|
|
521
680
|
// store, and inventing one here would be a second place secrets live.
|
|
522
681
|
console.log(` ...box token (shown once): ${box.accessToken}`)
|