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
|
@@ -7,6 +7,15 @@ import yaml from 'yaml'
|
|
|
7
7
|
import { authHeaderValue, resolveCliAuth } from '../auth'
|
|
8
8
|
import { registerRun, stepSummaryFromTrace } from '../run-registry'
|
|
9
9
|
import { createBridgeFromParsed, parseBridgeCliArgs } from '../ws-bridge'
|
|
10
|
+
import {
|
|
11
|
+
findUnexpectedFlowArgs,
|
|
12
|
+
FLOW_ARG_VALUE_FLAGS,
|
|
13
|
+
FLOW_BRIDGE_ARG_OPTIONS,
|
|
14
|
+
getLastFlowPreviewUploadResult,
|
|
15
|
+
getLastFlowTraceSteps,
|
|
16
|
+
runFlowPlayback,
|
|
17
|
+
type UnexpectedFlowArg,
|
|
18
|
+
} from './flow'
|
|
10
19
|
import { inspectDescribe } from './inspect/core'
|
|
11
20
|
import { resolveDefaultUploadOrigin } from './upload'
|
|
12
21
|
|
|
@@ -27,24 +36,17 @@ function flagValueAny(args: string[], names: string[]): string | undefined {
|
|
|
27
36
|
return undefined
|
|
28
37
|
}
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
'--url',
|
|
38
|
-
'--driver',
|
|
39
|
-
'--sim',
|
|
40
|
-
'--device',
|
|
41
|
-
'--slow',
|
|
42
|
-
'--tail-wait',
|
|
43
|
-
'--remap',
|
|
44
|
-
'--replace',
|
|
45
|
-
])
|
|
39
|
+
// generate is a front-end for runFlowPlayback: it inspects the sim, asks the
|
|
40
|
+
// model for a flow, then hands the argv down. these are the only flags it reads
|
|
41
|
+
// itself; everything else belongs to the runner and is forwarded verbatim. this
|
|
42
|
+
// file used to keep its own allowlist of what to forward, which silently
|
|
43
|
+
// dropped --proof, --record, --profile, --screenshots, --out and --base-url
|
|
44
|
+
// while `maestro test` honoured all six.
|
|
45
|
+
const GENERATE_VALUE_FLAGS = ['--origin', '--preview-origin', '--public-origin']
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
const VALUE_FLAGS = new Set([...GENERATE_VALUE_FLAGS, ...FLOW_ARG_VALUE_FLAGS])
|
|
48
|
+
|
|
49
|
+
export function findPrompt(args: string[]): { prompt: string; index: number } | null {
|
|
48
50
|
for (let i = 0; i < args.length; i++) {
|
|
49
51
|
const arg = args[i]
|
|
50
52
|
if (!arg) continue
|
|
@@ -65,14 +67,11 @@ function stripPromptForBridge(args: string[], promptIndex: number): string[] {
|
|
|
65
67
|
async function describeCurrentSim(args: string[], promptIndex: number): Promise<string> {
|
|
66
68
|
const bridgeArgs = stripPromptForBridge(args, promptIndex)
|
|
67
69
|
const parsedBridgeArgs = parseBridgeCliArgs(bridgeArgs, {
|
|
68
|
-
stripBooleanFlags:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
'--electron',
|
|
73
|
-
'--preview-open',
|
|
70
|
+
stripBooleanFlags: FLOW_BRIDGE_ARG_OPTIONS.stripBooleanFlags,
|
|
71
|
+
stripValueFlags: [
|
|
72
|
+
...GENERATE_VALUE_FLAGS,
|
|
73
|
+
...FLOW_BRIDGE_ARG_OPTIONS.stripValueFlags,
|
|
74
74
|
],
|
|
75
|
-
stripValueFlags: [...VALUE_FLAGS],
|
|
76
75
|
})
|
|
77
76
|
const bridge = createBridgeFromParsed(parsedBridgeArgs)
|
|
78
77
|
try {
|
|
@@ -237,7 +236,7 @@ function parseGeneratedFlow(text: string): {
|
|
|
237
236
|
return { summary, steps, yamlText: yaml.stringify(steps) }
|
|
238
237
|
}
|
|
239
238
|
|
|
240
|
-
function buildFlowArgs(
|
|
239
|
+
export function buildFlowArgs(
|
|
241
240
|
args: string[],
|
|
242
241
|
promptIndex: number,
|
|
243
242
|
flowPath: string,
|
|
@@ -251,32 +250,10 @@ function buildFlowArgs(
|
|
|
251
250
|
'--billing-kind',
|
|
252
251
|
'test_run',
|
|
253
252
|
]
|
|
254
|
-
const passthroughValues = new Set([
|
|
255
|
-
'--public-origin',
|
|
256
|
-
'--preview-public-origin',
|
|
257
|
-
'--owner',
|
|
258
|
-
'--repo',
|
|
259
|
-
'--url',
|
|
260
|
-
'--driver',
|
|
261
|
-
'--sim',
|
|
262
|
-
'--device',
|
|
263
|
-
'--slow',
|
|
264
|
-
'--tail-wait',
|
|
265
|
-
'--remap',
|
|
266
|
-
'--replace',
|
|
267
|
-
])
|
|
268
|
-
const passthroughBooleans = new Set([
|
|
269
|
-
'--new',
|
|
270
|
-
'--headless',
|
|
271
|
-
'--headed',
|
|
272
|
-
'--electron',
|
|
273
|
-
'--preview-open',
|
|
274
|
-
'--no-shell',
|
|
275
|
-
'--shell-only',
|
|
276
|
-
])
|
|
277
253
|
for (let i = 0; i < args.length; i++) {
|
|
278
254
|
if (i === promptIndex) continue
|
|
279
255
|
const arg = args[i]
|
|
256
|
+
if (!arg) continue
|
|
280
257
|
if (arg === '--origin' || arg === '--preview-origin') {
|
|
281
258
|
i += 1
|
|
282
259
|
continue
|
|
@@ -287,17 +264,50 @@ function buildFlowArgs(
|
|
|
287
264
|
i += 1
|
|
288
265
|
continue
|
|
289
266
|
}
|
|
290
|
-
|
|
267
|
+
out.push(arg)
|
|
268
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
291
269
|
const value = args[i + 1]
|
|
292
|
-
if (value) out.push(
|
|
270
|
+
if (value !== undefined) out.push(value)
|
|
293
271
|
i += 1
|
|
294
|
-
continue
|
|
295
272
|
}
|
|
296
|
-
if (passthroughBooleans.has(arg)) out.push(arg)
|
|
297
273
|
}
|
|
298
274
|
return out
|
|
299
275
|
}
|
|
300
276
|
|
|
277
|
+
// an argument nothing reads is an argument that silently changes what the run
|
|
278
|
+
// does, and generate hands its argv to runFlowPlayback, which is the end of the
|
|
279
|
+
// line for argv. so whatever survives buildFlowArgs is checked against the
|
|
280
|
+
// runner's own vocabulary, before the sim inspection and the model call rather
|
|
281
|
+
// than after them.
|
|
282
|
+
export function findUnexpectedGenerateArgs(
|
|
283
|
+
args: string[],
|
|
284
|
+
promptIndex: number,
|
|
285
|
+
): UnexpectedFlowArg[] {
|
|
286
|
+
const problems: UnexpectedFlowArg[] = []
|
|
287
|
+
for (let i = 0; i < args.length; i++) {
|
|
288
|
+
if (i === promptIndex) continue
|
|
289
|
+
const arg = args[i]
|
|
290
|
+
if (!arg) continue
|
|
291
|
+
// generate writes --billing-kind onto the flow argv itself and getFlag
|
|
292
|
+
// takes the first occurrence, so a caller's value loses to it in silence.
|
|
293
|
+
if (arg === '--billing-kind') {
|
|
294
|
+
problems.push({ arg, message: `${arg} is set by rnx maestro generate` })
|
|
295
|
+
i += 1
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
if (!GENERATE_VALUE_FLAGS.includes(arg)) continue
|
|
299
|
+
const value = args[i + 1]
|
|
300
|
+
if (value === undefined || value.startsWith('-')) {
|
|
301
|
+
problems.push({ arg, message: `${arg} expects a value` })
|
|
302
|
+
}
|
|
303
|
+
i += 1
|
|
304
|
+
}
|
|
305
|
+
return [
|
|
306
|
+
...problems,
|
|
307
|
+
...findUnexpectedFlowArgs(buildFlowArgs(args, promptIndex, '<flow>', '<origin>')),
|
|
308
|
+
]
|
|
309
|
+
}
|
|
310
|
+
|
|
301
311
|
export async function runMaestroGenerate(args: string[]): Promise<number> {
|
|
302
312
|
const promptArg = findPrompt(args)
|
|
303
313
|
if (!promptArg) {
|
|
@@ -305,6 +315,14 @@ export async function runMaestroGenerate(args: string[]): Promise<number> {
|
|
|
305
315
|
return 1
|
|
306
316
|
}
|
|
307
317
|
const { prompt, index: promptIndex } = promptArg
|
|
318
|
+
const unexpected = findUnexpectedGenerateArgs(args, promptIndex)
|
|
319
|
+
if (unexpected.length > 0) {
|
|
320
|
+
for (const problem of unexpected) {
|
|
321
|
+
console.error(` error: ${problem.message}`)
|
|
322
|
+
}
|
|
323
|
+
console.error('\n run `rnx maestro --help` for the flags this command accepts')
|
|
324
|
+
return 1
|
|
325
|
+
}
|
|
308
326
|
const cliAuth = resolveCliAuth()
|
|
309
327
|
if (!cliAuth || cliAuth.kind === 'github') {
|
|
310
328
|
console.error(' rnx maestro generate "<goal>" needs a login or RNX_API_KEY.')
|
|
@@ -326,8 +344,6 @@ export async function runMaestroGenerate(args: string[]): Promise<number> {
|
|
|
326
344
|
console.log(` flow: ${flowPath}`)
|
|
327
345
|
|
|
328
346
|
const startedAt = Date.now()
|
|
329
|
-
const { runFlowPlayback, getLastFlowPreviewUploadResult, getLastFlowTraceSteps } =
|
|
330
|
-
await import('./flow')
|
|
331
347
|
const flowArgs = buildFlowArgs(args, promptIndex, flowPath, origin)
|
|
332
348
|
const exitCode = await runFlowPlayback(flowArgs)
|
|
333
349
|
const upload = getLastFlowPreviewUploadResult()
|
package/cli/commands/maestro.ts
CHANGED
|
@@ -12,7 +12,10 @@ import yaml from 'yaml'
|
|
|
12
12
|
import { resolveCliAuth } from '../auth'
|
|
13
13
|
import { parseFlowFile } from '../flow-file'
|
|
14
14
|
import { registerRun, shouldRegisterRun, stepSummaryFromTrace } from '../run-registry'
|
|
15
|
+
import { RnxExit } from '../run-rnx'
|
|
15
16
|
import {
|
|
17
|
+
findUnexpectedFlowArgs,
|
|
18
|
+
FLOW_ARG_VALUE_FLAGS,
|
|
16
19
|
getLastFlowPreviewUploadResult,
|
|
17
20
|
getLastFlowTraceSteps,
|
|
18
21
|
hoistLeadingSimFlag,
|
|
@@ -44,8 +47,9 @@ usage:
|
|
|
44
47
|
|
|
45
48
|
options:
|
|
46
49
|
--env KEY=VALUE set env vars for \${KEY} interpolation (repeatable)
|
|
47
|
-
--continuous
|
|
50
|
+
--continuous accepted for maestro cli compat; rnx runs the flow once
|
|
48
51
|
--format <fmt> accepted for maestro cli compat (not used)
|
|
52
|
+
--device <model> override the flow frontmatter device for this run
|
|
49
53
|
--new force a fresh browser window for this run
|
|
50
54
|
--headed force a fresh, VISIBLE window (implies --new,
|
|
51
55
|
overrides --headless) — watch the flow run live
|
|
@@ -144,22 +148,35 @@ function applyEnvFlags(args: string[]): string[] {
|
|
|
144
148
|
return out
|
|
145
149
|
}
|
|
146
150
|
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
+
// maestro-cli flags with no rnx equivalent, accepted so an existing script can
|
|
152
|
+
// swap `maestro` for `rnx maestro` unedited. each one says out loud that it is
|
|
153
|
+
// being ignored: an option that is quietly dropped is how a run reports success
|
|
154
|
+
// while doing something other than what was asked. `--origin` is consumed here
|
|
155
|
+
// because only this command reads it; everything else this function leaves
|
|
156
|
+
// alone rides on to the flow runner, which rejects what it does not know.
|
|
157
|
+
function stripMaestroCompatFlags(args: string[]): string[] {
|
|
151
158
|
const out: string[] = []
|
|
152
159
|
for (let i = 0; i < args.length; i++) {
|
|
153
160
|
const a = args[i]
|
|
154
|
-
if (a === '--format' || a === '--output'
|
|
161
|
+
if (a === '--format' || a === '--output') {
|
|
162
|
+
console.warn(` warn: ${a} is accepted for maestro cli compatibility and ignored`)
|
|
155
163
|
i += 1
|
|
156
164
|
continue
|
|
157
165
|
}
|
|
166
|
+
if (a === '--origin') {
|
|
167
|
+
// read above for the run-registration origin. the flow runner has no
|
|
168
|
+
// --origin, so it must not ride into playbackArgs.
|
|
169
|
+
i += 1
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
if (a === '-d') {
|
|
173
|
+
// maestro's short form for --device. rnx's device flag names a device
|
|
174
|
+
// model, and the flow runner reads it under the long spelling only.
|
|
175
|
+
out.push('--device')
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
158
178
|
if (a === '--continuous') {
|
|
159
|
-
|
|
160
|
-
// exists; today runFlowPlayback has no watch mode, so we drop the
|
|
161
|
-
// flag and log a warning rather than silently changing behavior.
|
|
162
|
-
console.warn(' warn: --continuous is not yet implemented in rnx — running once')
|
|
179
|
+
console.warn(' warn: --continuous is not implemented in rnx — running once')
|
|
163
180
|
continue
|
|
164
181
|
}
|
|
165
182
|
out.push(a)
|
|
@@ -317,21 +334,33 @@ export async function runMaestro(
|
|
|
317
334
|
) {
|
|
318
335
|
return runMaestroAuthoring(remaining)
|
|
319
336
|
}
|
|
320
|
-
|
|
337
|
+
// `--origin` names the Contrast instance that hosts the run row. this command
|
|
338
|
+
// is its only reader, so read it before stripMaestroCompatFlags consumes it.
|
|
339
|
+
const originFlagIndex = remaining.indexOf('--origin')
|
|
340
|
+
const originFlagValue =
|
|
341
|
+
originFlagIndex >= 0 ? remaining[originFlagIndex + 1] : undefined
|
|
342
|
+
remaining = stripMaestroCompatFlags(remaining)
|
|
321
343
|
|
|
322
344
|
// resolve the target flow file or directory.
|
|
323
345
|
let target: string | null = null
|
|
324
346
|
let forwardArgs: string[]
|
|
325
347
|
|
|
326
348
|
if (sub === 'test') {
|
|
327
|
-
// `rnx maestro test <flow-or-dir> [flags...]
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
349
|
+
// `rnx maestro test <flow-or-dir> [flags...]`. the flow path is the first
|
|
350
|
+
// token that is neither a flag nor the value of one — without the second
|
|
351
|
+
// half, `test --sim a2 login.yaml` takes `a2` as the flow file. remove it
|
|
352
|
+
// by index, since a path can equal a flag value elsewhere in the argv.
|
|
353
|
+
const rest = remaining.slice(1)
|
|
354
|
+
const positionalIndex = rest.findIndex(
|
|
355
|
+
(a, i) =>
|
|
356
|
+
!looksLikeFlag(a) && !(i > 0 && FLOW_ARG_VALUE_FLAGS.includes(rest[i - 1])),
|
|
357
|
+
)
|
|
358
|
+
if (positionalIndex >= 0) {
|
|
359
|
+
target = path.resolve(cwd, rest[positionalIndex])
|
|
360
|
+
forwardArgs = rest.filter((_, i) => i !== positionalIndex)
|
|
332
361
|
} else {
|
|
333
362
|
target = findDefaultMaestroTarget(cwd)
|
|
334
|
-
forwardArgs =
|
|
363
|
+
forwardArgs = rest
|
|
335
364
|
}
|
|
336
365
|
} else if (sub && !looksLikeFlag(sub)) {
|
|
337
366
|
// `rnx maestro <flow-or-dir>` — tolerate the shorthand.
|
|
@@ -343,6 +372,19 @@ export async function runMaestro(
|
|
|
343
372
|
forwardArgs = remaining
|
|
344
373
|
}
|
|
345
374
|
|
|
375
|
+
// every flag here is forwarded to each flow, so check the vocabulary once,
|
|
376
|
+
// before anything opens a sim. runFlowPlayback checks again for its
|
|
377
|
+
// programmatic callers; doing it here keeps a directory run from printing
|
|
378
|
+
// the same rejection once per flow after it has already started work.
|
|
379
|
+
const unexpected = findUnexpectedFlowArgs(['<flow>', ...forwardArgs])
|
|
380
|
+
if (unexpected.length > 0) {
|
|
381
|
+
for (const problem of unexpected) {
|
|
382
|
+
console.error(` error: ${problem.message}`)
|
|
383
|
+
}
|
|
384
|
+
console.error('\n run `rnx maestro --help` for the flags this command accepts')
|
|
385
|
+
return 1
|
|
386
|
+
}
|
|
387
|
+
|
|
346
388
|
if (!target) {
|
|
347
389
|
console.error(`
|
|
348
390
|
error: no maestro flows found.
|
|
@@ -390,7 +432,7 @@ export async function runMaestro(
|
|
|
390
432
|
let registerOriginPromise: Promise<string> | null = null
|
|
391
433
|
const registerOrigin = () =>
|
|
392
434
|
(registerOriginPromise ??= resolveDefaultUploadOrigin(
|
|
393
|
-
flagValue('--preview-origin') ??
|
|
435
|
+
flagValue('--preview-origin') ?? originFlagValue,
|
|
394
436
|
))
|
|
395
437
|
|
|
396
438
|
let worstExit = 0
|
|
@@ -414,8 +456,15 @@ export async function runMaestro(
|
|
|
414
456
|
try {
|
|
415
457
|
exit = await runFlowPlayback(playbackArgs)
|
|
416
458
|
} catch (err) {
|
|
417
|
-
|
|
418
|
-
|
|
459
|
+
// rnxExit is a deliberate exit that already printed its own reason.
|
|
460
|
+
// reporting the sentinel's own text on top of it reads like a second,
|
|
461
|
+
// unrelated failure.
|
|
462
|
+
if (err instanceof RnxExit) {
|
|
463
|
+
exit = err.code || 1
|
|
464
|
+
} else {
|
|
465
|
+
console.error(` x ${(err as Error).message}`)
|
|
466
|
+
exit = 1
|
|
467
|
+
}
|
|
419
468
|
}
|
|
420
469
|
results.push({ file, exit })
|
|
421
470
|
if (exit !== 0 && worstExit === 0) worstExit = exit
|
package/cli/commands/platform.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// `rnx ios` and `rnx android` — start a simulator on that platform.
|
|
2
2
|
//
|
|
3
3
|
// the platform picks the device; the command context picks WHERE the sim runs
|
|
4
|
-
// (
|
|
5
|
-
// such as `rnx
|
|
6
|
-
// rest to the normal open path.
|
|
4
|
+
// (this machine, a local box, or `--remote`). there is deliberately no
|
|
5
|
+
// per-place spelling such as `rnx remote ios`, so this file only resolves the
|
|
6
|
+
// device and hands the rest to the normal open path.
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_DEVICE_BY_PLATFORM,
|
|
@@ -26,10 +26,10 @@ function isDeviceModel(value: string): value is DeviceModel {
|
|
|
26
26
|
return value in devices
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const
|
|
30
|
-
'usage: rnx ios --
|
|
29
|
+
const REMOTE_USAGE =
|
|
30
|
+
'usage: rnx ios --remote <local bundle.js|https://bundle-url> [--assets <dir>] [--device <model>]'
|
|
31
31
|
|
|
32
|
-
function
|
|
32
|
+
function remoteInputFromArgs(args: string[]): {
|
|
33
33
|
bundlePath: string
|
|
34
34
|
assetsPath: string | null
|
|
35
35
|
} {
|
|
@@ -37,19 +37,19 @@ function cloudInputFromArgs(args: string[]): {
|
|
|
37
37
|
let assetsPath: string | null = null
|
|
38
38
|
for (let index = 0; index < args.length; index++) {
|
|
39
39
|
const arg = args[index]
|
|
40
|
-
if (arg === '--
|
|
40
|
+
if (arg === '--remote') continue
|
|
41
41
|
if (arg === '--device') {
|
|
42
42
|
index++
|
|
43
43
|
continue
|
|
44
44
|
}
|
|
45
45
|
if (arg === '--assets') {
|
|
46
46
|
assetsPath = args[++index] ?? null
|
|
47
|
-
if (!assetsPath) throw new Error('rnx ios --
|
|
47
|
+
if (!assetsPath) throw new Error('rnx ios --remote --assets requires a directory')
|
|
48
48
|
continue
|
|
49
49
|
}
|
|
50
50
|
if (arg.startsWith('--assets=')) {
|
|
51
51
|
assetsPath = arg.slice('--assets='.length) || null
|
|
52
|
-
if (!assetsPath) throw new Error('rnx ios --
|
|
52
|
+
if (!assetsPath) throw new Error('rnx ios --remote --assets requires a directory')
|
|
53
53
|
continue
|
|
54
54
|
}
|
|
55
55
|
if (
|
|
@@ -60,14 +60,14 @@ function cloudInputFromArgs(args: string[]): {
|
|
|
60
60
|
arg.startsWith('--session=') ||
|
|
61
61
|
arg.startsWith('--tab=')
|
|
62
62
|
) {
|
|
63
|
-
throw new Error('rnx ios --
|
|
63
|
+
throw new Error('rnx ios --remote creates a new simulator and cannot target --sim')
|
|
64
64
|
}
|
|
65
65
|
if (arg.startsWith('-')) {
|
|
66
|
-
throw new Error(`rnx ios --
|
|
66
|
+
throw new Error(`rnx ios --remote does not support ${arg}`)
|
|
67
67
|
}
|
|
68
68
|
positional.push(arg)
|
|
69
69
|
}
|
|
70
|
-
if (positional.length !== 1 || !positional[0]) throw new Error(
|
|
70
|
+
if (positional.length !== 1 || !positional[0]) throw new Error(REMOTE_USAGE)
|
|
71
71
|
return { bundlePath: positional[0], assetsPath }
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -76,13 +76,6 @@ export async function runPlatformCommand(
|
|
|
76
76
|
args: string[],
|
|
77
77
|
opts: { port?: number } = {},
|
|
78
78
|
): Promise<number | undefined> {
|
|
79
|
-
if (args.includes('--micro')) {
|
|
80
|
-
console.error(
|
|
81
|
-
` ${rnxPublicBrand.commandName} ${platform} --micro is not implemented`,
|
|
82
|
-
)
|
|
83
|
-
return 1
|
|
84
|
-
}
|
|
85
|
-
|
|
86
79
|
// `--device` is applied to the settings store before dispatch, so a
|
|
87
80
|
// contradiction shows up here as a platform mismatch. refuse it by name
|
|
88
81
|
// rather than silently overriding whichever one the user meant.
|
|
@@ -121,33 +114,34 @@ export async function runPlatformCommand(
|
|
|
121
114
|
}
|
|
122
115
|
}
|
|
123
116
|
|
|
124
|
-
if (args.includes('--
|
|
117
|
+
if (args.includes('--remote')) {
|
|
125
118
|
if (platform !== 'ios') {
|
|
126
119
|
console.error(
|
|
127
|
-
` ${rnxPublicBrand.commandName} android --
|
|
120
|
+
` ${rnxPublicBrand.commandName} android --remote is not available; remote simulators are iOS only`,
|
|
128
121
|
)
|
|
129
122
|
return 1
|
|
130
123
|
}
|
|
131
124
|
try {
|
|
132
|
-
const input =
|
|
125
|
+
const input = remoteInputFromArgs(args)
|
|
133
126
|
const {
|
|
134
127
|
publishCloudSessionToShell,
|
|
135
128
|
readCloudSession,
|
|
136
129
|
requireCloudSessionShellUpdate,
|
|
137
130
|
} = await import('../cloud-session')
|
|
138
131
|
const descriptor = requireCloudSessionShellUpdate()
|
|
139
|
-
const { authHeaderOrExit } = await import('../auth')
|
|
140
|
-
const { header } = authHeaderOrExit('ios --
|
|
132
|
+
const { authHeaderOrExit, cloudAccountIdOrExit } = await import('../auth')
|
|
133
|
+
const { auth, header } = authHeaderOrExit('ios --remote')
|
|
134
|
+
const accountId = cloudAccountIdOrExit(auth)
|
|
141
135
|
const existingSession = readCloudSession()
|
|
142
136
|
if (existingSession) {
|
|
143
137
|
const { addCloudSimulator } = await import('../cloud-client')
|
|
144
138
|
const added = await addCloudSimulator(existingSession, {
|
|
145
139
|
device: selectedDevice,
|
|
146
140
|
authorization: header,
|
|
141
|
+
accountId,
|
|
147
142
|
})
|
|
148
143
|
publishCloudSessionToShell(added.session, descriptor)
|
|
149
|
-
console.log(`
|
|
150
|
-
console.log(` simulator: ${added.receipt.simId}`)
|
|
144
|
+
console.log(` remote simulator: ${added.receipt.simId}`)
|
|
151
145
|
console.log(` claim expires: ${added.receipt.claim.expiresAt}`)
|
|
152
146
|
console.log(
|
|
153
147
|
' commands: describe, find, get tree|node|count|memory, wait ready|selector, do, reset, logs, state, screenshot',
|
|
@@ -160,6 +154,7 @@ export async function runPlatformCommand(
|
|
|
160
154
|
assetsPath: input.assetsPath ?? undefined,
|
|
161
155
|
device: selectedDevice,
|
|
162
156
|
authorization: header,
|
|
157
|
+
accountId,
|
|
163
158
|
})
|
|
164
159
|
try {
|
|
165
160
|
publishCloudSessionToShell(created.session, descriptor)
|
|
@@ -169,8 +164,7 @@ export async function runPlatformCommand(
|
|
|
169
164
|
}
|
|
170
165
|
const receipt = created.receipt
|
|
171
166
|
const produced = created.artifact
|
|
172
|
-
console.log(`
|
|
173
|
-
console.log(` simulator: ${receipt.simulator.simId}`)
|
|
167
|
+
console.log(` remote simulator: ${receipt.simulator.simId}`)
|
|
174
168
|
console.log(` artifact: ${receipt.artifact.id} (${receipt.artifact.bytes} bytes)`)
|
|
175
169
|
console.log(
|
|
176
170
|
` source bundle: sha256:${produced.source.sha256} (${produced.source.bytes} bytes)`,
|
|
@@ -181,15 +175,21 @@ export async function runPlatformCommand(
|
|
|
181
175
|
console.log(
|
|
182
176
|
` embedded assets: ${produced.assets.descriptors} descriptors, ${produced.assets.variants} scales, ${produced.assets.bytes} bytes (${produced.assets.encodedBytes} encoded)`,
|
|
183
177
|
)
|
|
178
|
+
// the dest directory was chosen by resolving this bundle's descriptors
|
|
179
|
+
// against it, so say which one won rather than leaving it invisible.
|
|
180
|
+
if (created.assetsDirectory) {
|
|
181
|
+
console.log(` asset dest: ${created.assetsDirectory}`)
|
|
182
|
+
}
|
|
184
183
|
console.log(` claim expires: ${receipt.simulator.claim.expiresAt}`)
|
|
185
184
|
console.log(
|
|
186
185
|
' commands: describe, find, get tree|node|count|memory, wait ready|selector, do, reset, logs, state, screenshot',
|
|
187
186
|
)
|
|
187
|
+
for (const warning of created.warnings) console.warn(` warning: ${warning}`)
|
|
188
188
|
return 0
|
|
189
189
|
} catch (error) {
|
|
190
190
|
rethrowIfExit(error)
|
|
191
191
|
console.error(
|
|
192
|
-
` ${rnxPublicBrand.commandName} ios --
|
|
192
|
+
` ${rnxPublicBrand.commandName} ios --remote failed: ${
|
|
193
193
|
error instanceof Error ? error.message : String(error)
|
|
194
194
|
}`,
|
|
195
195
|
)
|
|
@@ -211,9 +211,7 @@ export async function runPlatformCommand(
|
|
|
211
211
|
'--no-hmr',
|
|
212
212
|
'--no-describe',
|
|
213
213
|
'--quiet',
|
|
214
|
-
'--
|
|
215
|
-
'--cloud',
|
|
216
|
-
'--micro',
|
|
214
|
+
'--remote',
|
|
217
215
|
],
|
|
218
216
|
stripValueFlags: [
|
|
219
217
|
'--base-url',
|
|
@@ -127,12 +127,13 @@ examples:
|
|
|
127
127
|
const dataUrl: unknown = await cloudBridge.send({ type: 'screenshot' })
|
|
128
128
|
const pngPrefix = 'data:image/png;base64,'
|
|
129
129
|
if (typeof dataUrl !== 'string' || !dataUrl.startsWith(pngPrefix)) {
|
|
130
|
-
throw new Error('
|
|
130
|
+
throw new Error('remote simulator screenshot returned invalid image data')
|
|
131
131
|
}
|
|
132
132
|
const outputPath = resolve(process.cwd(), outputArg || '/tmp/rnx-inspect.png')
|
|
133
133
|
mkdirSync(dirname(outputPath), { recursive: true })
|
|
134
134
|
const png = Buffer.from(dataUrl.slice(pngPrefix.length), 'base64')
|
|
135
|
-
if (png.length === 0)
|
|
135
|
+
if (png.length === 0)
|
|
136
|
+
throw new Error('remote simulator screenshot returned no image data')
|
|
136
137
|
writeFileSync(outputPath, png)
|
|
137
138
|
console.log(` saved: ${outputPath}`)
|
|
138
139
|
} catch (error) {
|
package/cli/commands/upload.ts
CHANGED
|
@@ -28,7 +28,12 @@ import { authHeaderValue, githubUploadIdentity, resolveCliAuth } from '../auth'
|
|
|
28
28
|
import { openUrl } from '../open-url'
|
|
29
29
|
import { resolveRunProvenance } from '../run-registry'
|
|
30
30
|
import { rnxExit } from '../run-rnx'
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
callInBridge,
|
|
33
|
+
createBridgeFromParsed,
|
|
34
|
+
evalInBridge,
|
|
35
|
+
parseBridgeCliArgs,
|
|
36
|
+
} from '../ws-bridge'
|
|
32
37
|
import type { SootSimTimelineEvent } from '@rnx/globals'
|
|
33
38
|
|
|
34
39
|
interface UploadOptions {
|
|
@@ -313,25 +318,38 @@ function forceInlineBundle(bundleUrl: string): string {
|
|
|
313
318
|
}
|
|
314
319
|
}
|
|
315
320
|
|
|
316
|
-
// pull
|
|
317
|
-
//
|
|
318
|
-
//
|
|
321
|
+
// pull the retained transformed bundle over the bridge in slices. a single
|
|
322
|
+
// message carrying tens of MB hits the ws framing cap, so this pages the
|
|
323
|
+
// bytes in. the page holds the bundle as a Blob, so the slices are byte
|
|
324
|
+
// ranges: collect the bytes and decode once at the end, because decoding
|
|
325
|
+
// each slice on its own would split multi-byte characters at the seams.
|
|
319
326
|
async function pullTransformedBundle(
|
|
320
327
|
bridge: ReturnType<typeof createBridgeFromParsed>,
|
|
321
328
|
byteLength: number,
|
|
322
329
|
): Promise<string | null> {
|
|
323
|
-
const CHUNK =
|
|
324
|
-
const pieces:
|
|
330
|
+
const CHUNK = 1_000_000 // ~1.33 MB of base64 per call
|
|
331
|
+
const pieces: Uint8Array[] = []
|
|
332
|
+
let total = 0
|
|
325
333
|
for (let offset = 0; offset < byteLength; offset += CHUNK) {
|
|
326
334
|
const end = Math.min(offset + CHUNK, byteLength)
|
|
327
|
-
const chunk = await
|
|
335
|
+
const chunk = await callInBridge<string | null>(
|
|
328
336
|
bridge,
|
|
329
|
-
|
|
337
|
+
'SootSim.bridges.readTransformedBundleSlice',
|
|
338
|
+
offset,
|
|
339
|
+
end,
|
|
330
340
|
)
|
|
331
341
|
if (typeof chunk !== 'string') return null
|
|
332
|
-
|
|
342
|
+
const bytes = Uint8Array.from(atob(chunk), (c) => c.charCodeAt(0))
|
|
343
|
+
pieces.push(bytes)
|
|
344
|
+
total += bytes.length
|
|
345
|
+
}
|
|
346
|
+
const joined = new Uint8Array(total)
|
|
347
|
+
let at = 0
|
|
348
|
+
for (const piece of pieces) {
|
|
349
|
+
joined.set(piece, at)
|
|
350
|
+
at += piece.length
|
|
333
351
|
}
|
|
334
|
-
return
|
|
352
|
+
return new TextDecoder().decode(joined)
|
|
335
353
|
}
|
|
336
354
|
|
|
337
355
|
type RecordedEntry = {
|
package/cli/help-core.ts
CHANGED
|
@@ -38,10 +38,20 @@ function backendLabel(command: string, args: readonly string[]): string {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function renderPreviewHelp(command: 'web' | 'ios' | 'android'): string {
|
|
41
|
+
// `rnx ios --remote` is the one leaf here that is not a preview selector, so
|
|
42
|
+
// the help has to name it or the flag is reachable only from the website.
|
|
43
|
+
const remote =
|
|
44
|
+
command === 'ios'
|
|
45
|
+
? `
|
|
46
|
+
rnx ios --remote <bundle.js|https://bundle-url> [--assets <dir>] [--device <model>]
|
|
47
|
+
run the bundle on a headless remote simulator.
|
|
48
|
+
its Metro assets are found for you; --assets
|
|
49
|
+
overrides that search.`
|
|
50
|
+
: ''
|
|
41
51
|
return `rnx ${command} — select or ensure the current branch's existing ${command} preview
|
|
42
52
|
|
|
43
53
|
usage:
|
|
44
|
-
rnx ${command}
|
|
54
|
+
rnx ${command}${remote}
|
|
45
55
|
|
|
46
56
|
The browser backend selects the Factory-owned ${command} iframe. It never launches a
|
|
47
57
|
host process or Cloudflare runtime.`
|