rnxsim 0.1.431 → 0.1.433
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/README.md +20 -50
- package/cli/auth.ts +33 -3
- package/cli/cloud-client.ts +524 -88
- package/cli/cloud-dispatch.ts +5 -10
- package/cli/cloud-session.ts +5 -5
- package/cli/commands/auth.ts +13 -2
- package/cli/commands/box.ts +189 -20
- package/cli/commands/control.ts +40 -13
- package/cli/commands/device.ts +1 -1
- package/cli/commands/flow.ts +189 -43
- package/cli/commands/inspect/get-layout.ts +1 -1
- package/cli/commands/inspect.ts +8 -1
- package/cli/commands/login.ts +79 -14
- package/cli/commands/maestro-generate.ts +70 -54
- package/cli/commands/maestro.ts +69 -20
- package/cli/commands/platform.ts +30 -32
- package/cli/commands/screenshot.ts +3 -2
- package/cli/commands/upload.ts +28 -10
- package/cli/help-core.ts +11 -1
- package/cli/outbound-endpoints.ts +27 -5
- package/cli/parse-args.ts +17 -0
- package/cli/shell-init.ts +4 -4
- package/cli/ws-bridge.ts +109 -62
- 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 +17 -4
- 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 +108 -93
- package/dist-lib/cloud.mjs +106 -91
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- 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 +58 -139
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- 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 +33 -212
- 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 +436 -351
- package/dist-lib/vite.cjs +1 -1
- package/package.json +2 -1
- package/src/auth/shared-session.ts +31 -0
- package/src/cloud.ts +83 -113
- package/src/metro-plugin.ts +30 -113
- package/src/vite-plugin.ts +11 -2
package/cli/cloud-dispatch.ts
CHANGED
|
@@ -121,7 +121,7 @@ function refuseCloudInvocation(
|
|
|
121
121
|
): CloudBoundaryResult {
|
|
122
122
|
const verb = firstVerb(positional)
|
|
123
123
|
const name = verb ? `${command} ${verb}` : command
|
|
124
|
-
console.error(` rnx ${name} is not available for
|
|
124
|
+
console.error(` rnx ${name} is not available for a remote simulator`)
|
|
125
125
|
return { handled: true, code: 1 }
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -129,18 +129,13 @@ export async function dispatchCloudBoundary({
|
|
|
129
129
|
command,
|
|
130
130
|
args,
|
|
131
131
|
}: CloudBoundaryInvocation): Promise<CloudBoundaryResult> {
|
|
132
|
-
if (command === 'ios' &&
|
|
133
|
-
|
|
134
|
-
if (command === 'android' && (args.includes('--nano') || args.includes('--cloud'))) {
|
|
132
|
+
if (command === 'ios' && args.includes('--remote')) return { handled: false }
|
|
133
|
+
if (command === 'android' && args.includes('--remote')) {
|
|
135
134
|
console.error(
|
|
136
|
-
' rnx android --
|
|
135
|
+
' rnx android --remote is not available; remote simulators are iOS only',
|
|
137
136
|
)
|
|
138
137
|
return { handled: true, code: 1 }
|
|
139
138
|
}
|
|
140
|
-
if ((command === 'ios' || command === 'android') && args.includes('--micro')) {
|
|
141
|
-
console.error(` rnx ${command} --micro is not implemented`)
|
|
142
|
-
return { handled: true, code: 1 }
|
|
143
|
-
}
|
|
144
139
|
if (!CLOUD_TARGET_COMMANDS.has(command)) return { handled: false }
|
|
145
140
|
|
|
146
141
|
const [{ parseBridgeCliArgs }, cloudClient, cloudSession] = await Promise.all([
|
|
@@ -189,7 +184,7 @@ export async function dispatchCloudBoundary({
|
|
|
189
184
|
const descriptor = cloudSession.requireCloudSessionShellUpdate()
|
|
190
185
|
await cloudClient.closeCloudBox(session)
|
|
191
186
|
cloudSession.publishCloudSessionToShell(null, descriptor)
|
|
192
|
-
console.log(` closed:
|
|
187
|
+
console.log(` closed: remote simulator ${session.simId}`)
|
|
193
188
|
return { handled: true, code: 0 }
|
|
194
189
|
} catch (error) {
|
|
195
190
|
console.error(
|
package/cli/cloud-session.ts
CHANGED
|
@@ -10,7 +10,7 @@ export const RNX_SHELL_UPDATE_PREFIX = '__RNX_SHELL_UPDATE_V1__'
|
|
|
10
10
|
export const RNX_SHELL_UPDATE_SET_PREFIX = `${RNX_SHELL_UPDATE_PREFIX}set:`
|
|
11
11
|
export const RNX_SHELL_UPDATE_UNSET = `${RNX_SHELL_UPDATE_PREFIX}unset`
|
|
12
12
|
|
|
13
|
-
// the shell's target: one
|
|
13
|
+
// the shell's target: one remote host, addressed by the box token, and the
|
|
14
14
|
// simulator inside it that ordinary commands reach with the simulator token
|
|
15
15
|
export interface CloudSession {
|
|
16
16
|
version: 2
|
|
@@ -31,7 +31,7 @@ export function normalizeCloudOrigin(source: string): string {
|
|
|
31
31
|
try {
|
|
32
32
|
parsed = new URL(source)
|
|
33
33
|
} catch {
|
|
34
|
-
throw new Error(`invalid
|
|
34
|
+
throw new Error(`invalid remote simulator origin: ${source}`)
|
|
35
35
|
}
|
|
36
36
|
if (
|
|
37
37
|
(parsed.protocol !== 'https:' && parsed.protocol !== 'http:') ||
|
|
@@ -41,10 +41,10 @@ export function normalizeCloudOrigin(source: string): string {
|
|
|
41
41
|
parsed.search ||
|
|
42
42
|
parsed.hash
|
|
43
43
|
) {
|
|
44
|
-
throw new Error(`
|
|
44
|
+
throw new Error(`remote simulator origin must be an HTTP(S) origin: ${source}`)
|
|
45
45
|
}
|
|
46
46
|
if (parsed.protocol === 'http:' && !isLoopbackHost(parsed.hostname)) {
|
|
47
|
-
throw new Error(`
|
|
47
|
+
throw new Error(`remote simulator origin requires HTTPS outside loopback: ${source}`)
|
|
48
48
|
}
|
|
49
49
|
return parsed.origin
|
|
50
50
|
}
|
|
@@ -157,7 +157,7 @@ export function requireCloudSessionShellUpdate(): number {
|
|
|
157
157
|
const descriptor = shellUpdateFd()
|
|
158
158
|
if (descriptor === null) {
|
|
159
159
|
throw new Error(
|
|
160
|
-
'RNX shell integration is required so this
|
|
160
|
+
'RNX shell integration is required so this remote simulator stays connected to the current shell; restart the shell after installing RNX',
|
|
161
161
|
)
|
|
162
162
|
}
|
|
163
163
|
return descriptor
|
package/cli/commands/auth.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
refreshSharedDesktopAuthSession,
|
|
3
|
+
sharedDesktopAuthAccounts,
|
|
4
|
+
} from '../../src/auth/shared-session'
|
|
5
|
+
import { CHOOSE_ACCOUNT_HINT, resolveCliAuth } from '../auth'
|
|
3
6
|
import { rnxExit } from '../run-rnx'
|
|
4
7
|
|
|
5
8
|
function printHelp(): void {
|
|
@@ -36,7 +39,15 @@ async function printStatus(): Promise<number> {
|
|
|
36
39
|
|
|
37
40
|
const current = await refreshSharedDesktopAuthSession(auth.origin)
|
|
38
41
|
const user = current?.user
|
|
42
|
+
const accounts = sharedDesktopAuthAccounts(user)
|
|
43
|
+
const accountId = current?.accountId ?? auth.accountId
|
|
44
|
+
const account = accounts.find((candidate) => candidate.id === accountId)
|
|
39
45
|
console.log(` ${user?.email || user?.name || user?.id || 'signed in'}`)
|
|
46
|
+
console.log(
|
|
47
|
+
` account: ${
|
|
48
|
+
account ? `${account.name} (${account.id})` : (accountId ?? CHOOSE_ACCOUNT_HINT)
|
|
49
|
+
}`,
|
|
50
|
+
)
|
|
40
51
|
console.log(` origin: ${current?.origin ?? auth.origin}`)
|
|
41
52
|
if (current?.updatedAt) console.log(` updated: ${current.updatedAt}`)
|
|
42
53
|
return 0
|
package/cli/commands/box.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
//
|
|
3
3
|
// bare `rnx box` is LOCAL and mounts the current checkout at /project. cloud
|
|
4
4
|
// create provisions through the shared client and enters the same shell unless
|
|
5
|
-
// --json asks for a noninteractive result.
|
|
6
|
-
//
|
|
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.
|
|
7
8
|
|
|
9
|
+
import { spawn } from 'node:child_process'
|
|
8
10
|
import path from 'node:path'
|
|
9
11
|
import { createInterface } from 'node:readline'
|
|
10
12
|
import { BoxClientError, createBoxClient } from '@rnx/box/client'
|
|
@@ -16,9 +18,16 @@ import {
|
|
|
16
18
|
sourceControlBoxTemplate,
|
|
17
19
|
terminalBoxTemplate,
|
|
18
20
|
} from '@rnx/box/template'
|
|
19
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
cloudBoxBrowserLiveView,
|
|
23
|
+
cloudBoxConnectUrl,
|
|
24
|
+
createCloudBoxProvider,
|
|
25
|
+
ensureCloudBoxBrowser,
|
|
26
|
+
heartbeatSession,
|
|
27
|
+
resolveCloudBoxReference,
|
|
28
|
+
} from 'rnx-cloud-box/client'
|
|
20
29
|
import { rnxPublicBrand } from '../../src/public-brand'
|
|
21
|
-
import { authHeaderOrExit } from '../auth'
|
|
30
|
+
import { authHeaderOrExit, cloudAccountIdOrExit } from '../auth'
|
|
22
31
|
import { findRepositoryPackageRoot } from '../setup-repository'
|
|
23
32
|
import { resolvePlacement } from '../ws-bridge'
|
|
24
33
|
import { CheckoutFilePlane, CheckoutRequiresGitError } from './box/checkout-plane'
|
|
@@ -58,6 +67,10 @@ function usage(): void {
|
|
|
58
67
|
the box service to create it in, or set
|
|
59
68
|
${ENDPOINT_ENV}
|
|
60
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 open <id> watch a cloud box in your browser
|
|
73
|
+
${rnx} box open <id> --print print the viewing URL instead of opening it
|
|
61
74
|
${rnx} box ls cloud boxes running on this account
|
|
62
75
|
${rnx} box stop --name=<n> stop a cloud box and its billing
|
|
63
76
|
${rnx} box delete --name=<n> stop it and wipe its files
|
|
@@ -91,13 +104,8 @@ export async function runBox(
|
|
|
91
104
|
|
|
92
105
|
const subcommand = args.find((arg) => !arg.startsWith('-'))
|
|
93
106
|
if (subcommand === 'create') return runCloudBoxCreate(args, opts)
|
|
94
|
-
if (subcommand === 'connect')
|
|
95
|
-
|
|
96
|
-
` ${rnxPublicBrand.commandName} box connect: the cloud provider cannot yet resolve a Box ID or name to an authorized Box reference.\n` +
|
|
97
|
-
` the shared Box client requires that reference's access token, and the CLI does not persist Box tokens.`,
|
|
98
|
-
)
|
|
99
|
-
return 1
|
|
100
|
-
}
|
|
107
|
+
if (subcommand === 'connect') return runCloudBoxConnect(args)
|
|
108
|
+
if (subcommand === 'open') return runCloudBoxOpen(args)
|
|
101
109
|
if (subcommand === 'stop' || subcommand === 'delete') {
|
|
102
110
|
return runCloudBoxStop(args, subcommand === 'delete')
|
|
103
111
|
}
|
|
@@ -160,12 +168,13 @@ async function runCloudBoxStop(args: string[], purge: boolean): Promise<number>
|
|
|
160
168
|
console.error(` ${rnx} box ${label}: which box? pass --name=<n>.`)
|
|
161
169
|
return 1
|
|
162
170
|
}
|
|
163
|
-
const { header } = authHeaderOrExit(`box ${label}`)
|
|
171
|
+
const { auth, header } = authHeaderOrExit(`box ${label}`)
|
|
164
172
|
const apiKey = header.replace(/^Bearer /, '')
|
|
173
|
+
const accountId = cloudAccountIdOrExit(auth)
|
|
165
174
|
const client = createBoxClient(
|
|
166
175
|
createCloudBoxProvider({
|
|
167
176
|
endpoint,
|
|
168
|
-
authority: { kind: 'account', apiOrigin: apiOrigin(args), apiKey },
|
|
177
|
+
authority: { kind: 'account', apiOrigin: apiOrigin(args), apiKey, accountId },
|
|
169
178
|
}),
|
|
170
179
|
)
|
|
171
180
|
try {
|
|
@@ -189,12 +198,13 @@ async function runCloudBoxStop(args: string[], purge: boolean): Promise<number>
|
|
|
189
198
|
|
|
190
199
|
async function runCloudBoxList(args: string[]): Promise<number> {
|
|
191
200
|
const rnx = rnxPublicBrand.commandName
|
|
192
|
-
const { header } = authHeaderOrExit('box ls')
|
|
201
|
+
const { auth, header } = authHeaderOrExit('box ls')
|
|
193
202
|
const apiKey = header.replace(/^Bearer /, '')
|
|
203
|
+
const accountId = cloudAccountIdOrExit(auth)
|
|
194
204
|
const client = createBoxClient(
|
|
195
205
|
createCloudBoxProvider({
|
|
196
206
|
endpoint: flag(args, 'endpoint') ?? process.env[ENDPOINT_ENV]?.trim() ?? '',
|
|
197
|
-
authority: { kind: 'account', apiOrigin: apiOrigin(args), apiKey },
|
|
207
|
+
authority: { kind: 'account', apiOrigin: apiOrigin(args), apiKey, accountId },
|
|
198
208
|
}),
|
|
199
209
|
)
|
|
200
210
|
try {
|
|
@@ -219,6 +229,161 @@ async function runCloudBoxList(args: string[]): Promise<number> {
|
|
|
219
229
|
}
|
|
220
230
|
}
|
|
221
231
|
|
|
232
|
+
async function runCloudBoxConnect(args: string[]): Promise<number> {
|
|
233
|
+
const rnx = rnxPublicBrand.commandName
|
|
234
|
+
const json = args.includes('--json')
|
|
235
|
+
// the first positional is `connect`; the second is the Box.
|
|
236
|
+
const boxId = args.filter((arg) => !arg.startsWith('-'))[1]
|
|
237
|
+
if (!boxId) {
|
|
238
|
+
console.error(
|
|
239
|
+
` ${rnx} box connect: which box? pass its ID or name, as \`${rnx} box ls\` prints it.`,
|
|
240
|
+
)
|
|
241
|
+
return 1
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// the ACCOUNT's credential. the account service is the only thing that knows
|
|
245
|
+
// which account a Box belongs to, so it resolves the name and hands back the
|
|
246
|
+
// Box's own token; nothing here writes that token down.
|
|
247
|
+
const { header } = authHeaderOrExit('box connect')
|
|
248
|
+
const apiKey = header.replace(/^Bearer /, '')
|
|
249
|
+
const origin = apiOrigin(args)
|
|
250
|
+
|
|
251
|
+
let session: BoxShellSession | null = null
|
|
252
|
+
let heartbeat: { stop: () => void } | null = null
|
|
253
|
+
try {
|
|
254
|
+
const reference = await resolveCloudBoxReference({ apiOrigin: origin, apiKey, boxId })
|
|
255
|
+
const box = await createBoxClient(
|
|
256
|
+
createCloudBoxProvider({
|
|
257
|
+
endpoint: reference.endpoint,
|
|
258
|
+
authority: { kind: 'attach' },
|
|
259
|
+
}),
|
|
260
|
+
).attach({
|
|
261
|
+
// no stack here on purpose: the Box reports the one it was created
|
|
262
|
+
// with, and holding somebody else's Box to a stack this process guessed
|
|
263
|
+
// would offer primitives it does not actually have.
|
|
264
|
+
reference: { boxId: reference.boxId, accessToken: reference.token },
|
|
265
|
+
})
|
|
266
|
+
if (json) {
|
|
267
|
+
// the reference without its token. a caller scripting against this gets
|
|
268
|
+
// the Box's identity; the credential stays out of stdout and out of
|
|
269
|
+
// whatever collects it.
|
|
270
|
+
console.log(JSON.stringify({ endpoint: reference.endpoint, ...box.description }))
|
|
271
|
+
return 0
|
|
272
|
+
}
|
|
273
|
+
console.log(` ...connected to Box ${box.id}`)
|
|
274
|
+
if (box.description.pageUrl) {
|
|
275
|
+
console.log(` ...open ${box.description.pageUrl}`)
|
|
276
|
+
}
|
|
277
|
+
console.log(` ...accessing shell\n`)
|
|
278
|
+
session = await box.connect()
|
|
279
|
+
// the same reason `create` heartbeats: the box holds no account credential,
|
|
280
|
+
// so the client using it is what keeps its session alive.
|
|
281
|
+
const sessionId = box.description.sessionId
|
|
282
|
+
if (!sessionId) throw new BoxClientError('the box service returned no session id')
|
|
283
|
+
heartbeat = heartbeatSession({ apiOrigin: origin, apiKey, sessionId })
|
|
284
|
+
return await repl(session, '')
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (error instanceof BoxClientError) {
|
|
287
|
+
console.error(` ${rnx} box connect: ${error.message}`)
|
|
288
|
+
return 1
|
|
289
|
+
}
|
|
290
|
+
throw error
|
|
291
|
+
} finally {
|
|
292
|
+
heartbeat?.stop()
|
|
293
|
+
session?.close()
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* watch a cloud box in a browser.
|
|
299
|
+
*
|
|
300
|
+
* the box is already running its page in a headless Browser Run tab; this
|
|
301
|
+
* attaches a viewer to that same tab rather than starting a second one. the
|
|
302
|
+
* URL it mints is a short-lived bearer capability, so it is handed straight to
|
|
303
|
+
* the browser and this output is the only place it ever exists.
|
|
304
|
+
*/
|
|
305
|
+
async function runCloudBoxOpen(args: string[]): Promise<number> {
|
|
306
|
+
const rnx = rnxPublicBrand.commandName
|
|
307
|
+
const json = args.includes('--json')
|
|
308
|
+
const print = args.includes('--print')
|
|
309
|
+
// the first positional is `open`; the second is the Box.
|
|
310
|
+
const boxId = args.filter((arg) => !arg.startsWith('-'))[1] ?? flag(args, 'name')
|
|
311
|
+
if (!boxId) {
|
|
312
|
+
console.error(
|
|
313
|
+
` ${rnx} box open: which box? pass its ID or name, as \`${rnx} box ls\` prints it.`,
|
|
314
|
+
)
|
|
315
|
+
return 1
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// the ACCOUNT's credential, for the same reason connect takes one: the
|
|
319
|
+
// account service is what knows which account owns this Box, and it hands
|
|
320
|
+
// back the Box's own token rather than this process storing one.
|
|
321
|
+
const { header } = authHeaderOrExit('box open')
|
|
322
|
+
const apiKey = header.replace(/^Bearer /, '')
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const reference = await resolveCloudBoxReference({
|
|
326
|
+
apiOrigin: apiOrigin(args),
|
|
327
|
+
apiKey,
|
|
328
|
+
boxId,
|
|
329
|
+
})
|
|
330
|
+
const connectUrl = cloudBoxConnectUrl(reference.endpoint, reference.boxId)
|
|
331
|
+
// ensure first: it is what navigates the generation's tab to the Box page,
|
|
332
|
+
// so a box whose browser lapsed gets a new generation with its page
|
|
333
|
+
// restored before anybody is looking at it.
|
|
334
|
+
const browser = await ensureCloudBoxBrowser({
|
|
335
|
+
connectUrl,
|
|
336
|
+
accessToken: reference.token,
|
|
337
|
+
})
|
|
338
|
+
if (!browser.reused && !json && !print) {
|
|
339
|
+
console.log(` ...started this box's browser`)
|
|
340
|
+
}
|
|
341
|
+
const view = await cloudBoxBrowserLiveView({
|
|
342
|
+
connectUrl,
|
|
343
|
+
accessToken: reference.token,
|
|
344
|
+
mode: 'tab',
|
|
345
|
+
})
|
|
346
|
+
if (json) {
|
|
347
|
+
console.log(
|
|
348
|
+
JSON.stringify({
|
|
349
|
+
url: view.url,
|
|
350
|
+
expiresAt: new Date(Date.now() + view.expiresInMs).toISOString(),
|
|
351
|
+
}),
|
|
352
|
+
)
|
|
353
|
+
return 0
|
|
354
|
+
}
|
|
355
|
+
if (print) {
|
|
356
|
+
console.log(view.url)
|
|
357
|
+
return 0
|
|
358
|
+
}
|
|
359
|
+
return await openInBrowser(view.url)
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (error instanceof BoxClientError) {
|
|
362
|
+
console.error(` ${rnx} box open: ${error.message}`)
|
|
363
|
+
return 1
|
|
364
|
+
}
|
|
365
|
+
throw error
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* hand a URL to the operating system's browser.
|
|
371
|
+
*
|
|
372
|
+
* argv rather than a shell string, because this URL is a bearer capability and
|
|
373
|
+
* a shell would take it through history, expansion and quoting on the way. when
|
|
374
|
+
* `open` cannot run it is printed instead: this process is the only place the
|
|
375
|
+
* URL exists, so dropping it would cost the caller another mint.
|
|
376
|
+
*/
|
|
377
|
+
async function openInBrowser(url: string): Promise<number> {
|
|
378
|
+
const child = spawn('open', [url], { stdio: 'ignore' })
|
|
379
|
+
const opened = await new Promise<boolean>((resolve) => {
|
|
380
|
+
child.once('error', () => resolve(false))
|
|
381
|
+
child.once('close', (code) => resolve(code === 0))
|
|
382
|
+
})
|
|
383
|
+
if (!opened) console.log(url)
|
|
384
|
+
return 0
|
|
385
|
+
}
|
|
386
|
+
|
|
222
387
|
async function runCloudBoxCreate(
|
|
223
388
|
args: string[],
|
|
224
389
|
opts: BoxCommandOptions,
|
|
@@ -270,14 +435,15 @@ async function runCloudBoxCreate(
|
|
|
270
435
|
// the ACCOUNT's key. the box service forwards it to /v1/rnx/sessions, which
|
|
271
436
|
// is what authenticates this caller, names the account, and starts the
|
|
272
437
|
// session the box is billed against.
|
|
273
|
-
const { header } = authHeaderOrExit('box create')
|
|
438
|
+
const { auth, header } = authHeaderOrExit('box create')
|
|
274
439
|
const apiKey = header.replace(/^Bearer /, '')
|
|
440
|
+
const accountId = cloudAccountIdOrExit(auth)
|
|
275
441
|
|
|
276
442
|
if (!json) console.log(` ...creating cloud box ${name}`)
|
|
277
443
|
const client = createBoxClient(
|
|
278
444
|
createCloudBoxProvider({
|
|
279
445
|
endpoint,
|
|
280
|
-
authority: { kind: 'account', apiOrigin: apiOrigin(args), apiKey },
|
|
446
|
+
authority: { kind: 'account', apiOrigin: apiOrigin(args), apiKey, accountId },
|
|
281
447
|
}),
|
|
282
448
|
)
|
|
283
449
|
let session: BoxShellSession | null = null
|
|
@@ -303,13 +469,16 @@ async function runCloudBoxCreate(
|
|
|
303
469
|
return 0
|
|
304
470
|
}
|
|
305
471
|
console.log(` ...created Box ${box.id}`)
|
|
306
|
-
if (box.description.stableUrl) {
|
|
307
|
-
console.log(` ...live ${box.description.stableUrl}`)
|
|
308
|
-
}
|
|
309
472
|
console.log(` ...seeded ${box.description.files} project files`)
|
|
310
473
|
// shown once and never stored: this process does not own a credential
|
|
311
474
|
// store, and inventing one here would be a second place secrets live.
|
|
312
475
|
console.log(` ...box token (shown once): ${box.accessToken}`)
|
|
476
|
+
// last, so it is the line still on screen when the shell takes over. it
|
|
477
|
+
// carries no credential: opening it signed in resolves the box through the
|
|
478
|
+
// account service.
|
|
479
|
+
if (box.description.pageUrl) {
|
|
480
|
+
console.log(` ...open ${box.description.pageUrl}`)
|
|
481
|
+
}
|
|
313
482
|
console.log(` ...accessing shell\n`)
|
|
314
483
|
session = await box.connect()
|
|
315
484
|
// bank the wall clock while the shell is open. the box holds no account
|
package/cli/commands/control.ts
CHANGED
|
@@ -62,11 +62,11 @@ import { rethrowIfExit, rnxExit } from '../run-rnx'
|
|
|
62
62
|
import { rnxSelfInvocation } from '../self-invocation'
|
|
63
63
|
import {
|
|
64
64
|
BridgeSimLockedError,
|
|
65
|
-
bridgePortForShellBaseUrl,
|
|
66
65
|
createBridge,
|
|
67
66
|
createBridgeFromParsed,
|
|
68
67
|
parseBridgeCliArgs,
|
|
69
68
|
printBridgeWorldNotice,
|
|
69
|
+
resolveBridgePortForPin,
|
|
70
70
|
resolveBridgeWorld,
|
|
71
71
|
worldForBridgePort,
|
|
72
72
|
type BridgeSimInfo,
|
|
@@ -747,6 +747,16 @@ export interface BuildShellUrlOptions {
|
|
|
747
747
|
device?: string
|
|
748
748
|
resolvedBundle?: ResolvedDevBundle
|
|
749
749
|
project?: ResolvedAppProject | null
|
|
750
|
+
// deterministic-capture mode, stamped onto the engine URL as ?proof=1. the
|
|
751
|
+
// shell worker, compositor worker and tenant worker each read it off the
|
|
752
|
+
// host page URL at boot (syncProofModeFromInitUrl) and pin everything that
|
|
753
|
+
// would otherwise differ frame to frame: the TextInput caret stops blinking
|
|
754
|
+
// and stays visible, and notifications stop auto-dismissing. capture-frame
|
|
755
|
+
// also reads the raw param and stops clipping the device corners, so a
|
|
756
|
+
// capture matches simctl's rectangular framebuffer. without it a
|
|
757
|
+
// screen holding a focused text field never reaches three identical idle
|
|
758
|
+
// frames, so the screenshot guard burns all 12 attempts and then fails.
|
|
759
|
+
proof?: boolean
|
|
750
760
|
}
|
|
751
761
|
|
|
752
762
|
function bundleUrlOf(urlStr: string): string | undefined {
|
|
@@ -771,9 +781,10 @@ export async function buildShellUrl(
|
|
|
771
781
|
const project =
|
|
772
782
|
opts.project ?? (await resolveAppProjectForBundleUrl(bundleUrlOf(resolved)))
|
|
773
783
|
const stamped = await stampAppSplash(await stampAppFonts(resolved, project), project)
|
|
774
|
-
if (!opts.device) return stamped
|
|
784
|
+
if (!opts.device && !opts.proof) return stamped
|
|
775
785
|
const url = new URL(stamped)
|
|
776
|
-
url.searchParams.set('device', opts.device)
|
|
786
|
+
if (opts.device) url.searchParams.set('device', opts.device)
|
|
787
|
+
if (opts.proof) url.searchParams.set('proof', '1')
|
|
777
788
|
return url.toString()
|
|
778
789
|
}
|
|
779
790
|
|
|
@@ -1417,6 +1428,7 @@ export async function runOpenCommand(
|
|
|
1417
1428
|
'--no-hmr',
|
|
1418
1429
|
'--no-describe',
|
|
1419
1430
|
'--quiet',
|
|
1431
|
+
'--proof',
|
|
1420
1432
|
],
|
|
1421
1433
|
stripValueFlags: [
|
|
1422
1434
|
'--base-url',
|
|
@@ -1516,14 +1528,25 @@ export async function runOpenCommand(
|
|
|
1516
1528
|
await ensureConfiguredRuntime(configuredRuntimeVersion)
|
|
1517
1529
|
}
|
|
1518
1530
|
|
|
1519
|
-
// keep the bridge port and the shell base URL in one world:
|
|
1520
|
-
//
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1531
|
+
// keep the bridge port and the shell base URL in one world: --base-url
|
|
1532
|
+
// retargets polling at THAT world's bridge (otherwise open builds the page
|
|
1533
|
+
// against one runtime while polling the other bridge for registration and
|
|
1534
|
+
// times out after ~125s), and a pin nothing serves is refused rather than
|
|
1535
|
+
// falling back to the discovered default, which used to send
|
|
1536
|
+
// ensureBridgeForOpen below off to start a second shell.
|
|
1523
1537
|
const explicitBaseUrl = args.find((_, i) => args[i - 1] === '--base-url')
|
|
1524
|
-
if (explicitBaseUrl
|
|
1525
|
-
const
|
|
1526
|
-
|
|
1538
|
+
if (explicitBaseUrl) {
|
|
1539
|
+
const pinned = resolveBridgePortForPin({
|
|
1540
|
+
baseUrl: explicitBaseUrl,
|
|
1541
|
+
wsPort: parsed.wsPort,
|
|
1542
|
+
explicitPort: parsed.explicitPort,
|
|
1543
|
+
})
|
|
1544
|
+
if (pinned.error !== undefined) {
|
|
1545
|
+
console.error(` error: ${pinned.error}`)
|
|
1546
|
+
rnxExit(1)
|
|
1547
|
+
} else {
|
|
1548
|
+
parsed.wsPort = pinned.port
|
|
1549
|
+
}
|
|
1527
1550
|
}
|
|
1528
1551
|
|
|
1529
1552
|
parsed.wsPort = await ensureBridgeForOpen(parsed.wsPort, parsed.explicitPort)
|
|
@@ -1597,6 +1620,7 @@ export async function runOpenCommand(
|
|
|
1597
1620
|
device: deviceArg,
|
|
1598
1621
|
resolvedBundle,
|
|
1599
1622
|
project,
|
|
1623
|
+
proof: args.includes('--proof'),
|
|
1600
1624
|
}
|
|
1601
1625
|
|
|
1602
1626
|
// resolve the shell base URL from the bridge port we settled on, so the page
|
|
@@ -1762,9 +1786,12 @@ export async function runOpenCommand(
|
|
|
1762
1786
|
const result = await driver.launch({
|
|
1763
1787
|
url: openUrl,
|
|
1764
1788
|
// the shell serves the sim page but its WS bridge may have drifted off
|
|
1765
|
-
// the shell-port-implied default. inject the
|
|
1766
|
-
// page does not reconnect to a stale world.
|
|
1767
|
-
|
|
1789
|
+
// the shell-port-implied default. inject the bridge this command settled
|
|
1790
|
+
// on so the page does not reconnect to a stale world. re-deriving it from
|
|
1791
|
+
// the shell URL could name a third port, since a bridge with no lockfile
|
|
1792
|
+
// resolves back to the default shell URL and that shell may belong to a
|
|
1793
|
+
// live world of its own.
|
|
1794
|
+
bridgePort: parsed.wsPort,
|
|
1768
1795
|
headless: driverHeadless,
|
|
1769
1796
|
profileId,
|
|
1770
1797
|
ephemeralProfile,
|