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/commands/flow.ts
CHANGED
|
@@ -24,8 +24,15 @@ import {
|
|
|
24
24
|
keepFlowCandidate,
|
|
25
25
|
startFlowSession,
|
|
26
26
|
} from '../flow-session'
|
|
27
|
+
import { GLOBAL_FLAG_TAKES_VALUE } from '../parse-args'
|
|
27
28
|
import { rnxExit } from '../run-rnx'
|
|
28
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
BRIDGE_VALUE_FLAGS,
|
|
31
|
+
createBridge,
|
|
32
|
+
createBridgeFromParsed,
|
|
33
|
+
parseBridgeCliArgs,
|
|
34
|
+
resolveBridgePortForPin,
|
|
35
|
+
} from '../ws-bridge'
|
|
29
36
|
import {
|
|
30
37
|
buildShellUrl,
|
|
31
38
|
closeSimsBulk,
|
|
@@ -39,6 +46,110 @@ import type { UploadResult } from './upload'
|
|
|
39
46
|
|
|
40
47
|
export { parseFlowFile, validateFlowFile } from '../flow-file'
|
|
41
48
|
|
|
49
|
+
// the complete flag vocabulary of `runFlowPlayback`. it is both the strip list
|
|
50
|
+
// handed to parseBridgeCliArgs and the vocabulary the unknown-argument check
|
|
51
|
+
// enforces, so a flag added to the runner and left out of here gets rejected
|
|
52
|
+
// rather than silently dropped.
|
|
53
|
+
const FLOW_BOOLEAN_FLAGS = [
|
|
54
|
+
'--record',
|
|
55
|
+
'--profile',
|
|
56
|
+
'--electron',
|
|
57
|
+
'--new',
|
|
58
|
+
'--preview',
|
|
59
|
+
'--preview-open',
|
|
60
|
+
'--headless',
|
|
61
|
+
'--headed',
|
|
62
|
+
'--proof',
|
|
63
|
+
'--no-shell',
|
|
64
|
+
'--shell-only',
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
const FLOW_VALUE_FLAGS = [
|
|
68
|
+
'--out',
|
|
69
|
+
'--base-url',
|
|
70
|
+
'--device',
|
|
71
|
+
'--theme',
|
|
72
|
+
'--slow',
|
|
73
|
+
'--tail-wait',
|
|
74
|
+
'--url',
|
|
75
|
+
'--replace',
|
|
76
|
+
'--remap',
|
|
77
|
+
'--screenshots',
|
|
78
|
+
'--screenshot-paths',
|
|
79
|
+
'--preview-origin',
|
|
80
|
+
'--preview-public-origin',
|
|
81
|
+
'--driver',
|
|
82
|
+
'--billing-kind',
|
|
83
|
+
'--owner',
|
|
84
|
+
'--repo',
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
// the strip lists parseBridgeCliArgs needs to see a flow argv the way the
|
|
88
|
+
// runner does. exported so a command that pre-parses argv before handing it to
|
|
89
|
+
// runFlowPlayback reads the same vocabulary instead of keeping its own copy.
|
|
90
|
+
export const FLOW_BRIDGE_ARG_OPTIONS = {
|
|
91
|
+
stripBooleanFlags: FLOW_BOOLEAN_FLAGS,
|
|
92
|
+
stripValueFlags: FLOW_VALUE_FLAGS,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// every flag that swallows the token after it on a flow argv. a caller picking
|
|
96
|
+
// the positional flow path out of raw argv has to skip those tokens, or
|
|
97
|
+
// `maestro test --sim a2 login.yaml` resolves `a2` as the flow file.
|
|
98
|
+
export const FLOW_ARG_VALUE_FLAGS = [...FLOW_VALUE_FLAGS, ...BRIDGE_VALUE_FLAGS]
|
|
99
|
+
|
|
100
|
+
export interface UnexpectedFlowArg {
|
|
101
|
+
arg: string
|
|
102
|
+
message: string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// runFlowPlayback is the end of the line for argv: everything below it is built
|
|
106
|
+
// from named values, never forwarded raw. so an argument it does not read is
|
|
107
|
+
// read by nobody, and dropping one is how a run reports success while doing
|
|
108
|
+
// something other than what was asked — a stale build took --proof, discarded
|
|
109
|
+
// it, and produced non-proof screenshots under a proof-mode label.
|
|
110
|
+
//
|
|
111
|
+
// parseBridgeCliArgs already removes every flag this command understands and
|
|
112
|
+
// every flag the bridge owns, each with its value, so whatever it leaves behind
|
|
113
|
+
// past the flow path at args[0] is an argument nothing here consumes.
|
|
114
|
+
export function findUnexpectedFlowArgs(args: string[]): UnexpectedFlowArg[] {
|
|
115
|
+
const known = new Set([...FLOW_BOOLEAN_FLAGS, ...FLOW_VALUE_FLAGS])
|
|
116
|
+
const problems: UnexpectedFlowArg[] = parseBridgeCliArgs(args, FLOW_BRIDGE_ARG_OPTIONS)
|
|
117
|
+
.positional.slice(1)
|
|
118
|
+
.map((arg) => {
|
|
119
|
+
const eq = arg.indexOf('=')
|
|
120
|
+
const head = eq > 0 ? arg.slice(0, eq) : ''
|
|
121
|
+
if (known.has(head)) {
|
|
122
|
+
return {
|
|
123
|
+
arg,
|
|
124
|
+
message: `${head} takes its value as a separate argument: ${head} ${arg.slice(eq + 1)}`,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const globalTakesValue = GLOBAL_FLAG_TAKES_VALUE.get(arg)
|
|
128
|
+
if (globalTakesValue !== undefined) {
|
|
129
|
+
return {
|
|
130
|
+
arg,
|
|
131
|
+
message: `${arg} is a global rnx flag and goes before the command: rnx ${arg}${
|
|
132
|
+
globalTakesValue ? ' <value>' : ''
|
|
133
|
+
} <command>`,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (arg.startsWith('-')) return { arg, message: `unknown flag: ${arg}` }
|
|
137
|
+
return { arg, message: `unexpected argument: ${arg}` }
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// a value flag left without a value keeps the runner's default instead of
|
|
141
|
+
// what the caller meant to set, which is the same silent substitution.
|
|
142
|
+
for (let i = 0; i < args.length; i++) {
|
|
143
|
+
if (!FLOW_VALUE_FLAGS.includes(args[i])) continue
|
|
144
|
+
const value = args[i + 1]
|
|
145
|
+
if (value === undefined || value.startsWith('-')) {
|
|
146
|
+
problems.push({ arg: args[i], message: `${args[i]} expects a value` })
|
|
147
|
+
}
|
|
148
|
+
i++
|
|
149
|
+
}
|
|
150
|
+
return problems
|
|
151
|
+
}
|
|
152
|
+
|
|
42
153
|
let lastPreviewUploadResult: UploadResult | null = null
|
|
43
154
|
|
|
44
155
|
export function getLastFlowPreviewUploadResult(): UploadResult | null {
|
|
@@ -112,11 +223,17 @@ options:
|
|
|
112
223
|
--profile capture perf stats while the flow runs
|
|
113
224
|
--out <dir> output directory (default: /tmp/rnx-recordings)
|
|
114
225
|
--device <model> override the flow frontmatter device for this run
|
|
226
|
+
--theme <scheme> override the flow frontmatter theme (light or dark) for
|
|
227
|
+
this run
|
|
115
228
|
--slow <ms> delay between steps for natural pacing
|
|
116
229
|
--tail-wait <ms> hold the recording open after the last step so the
|
|
117
230
|
final UI state is captured (default: smart idle up to
|
|
118
231
|
6000ms for --preview, fixed 2000ms for --record, 0 otherwise)
|
|
119
232
|
--url <url> load this target before running the flow
|
|
233
|
+
--base-url <url> run the flow against the shell at this URL instead of
|
|
234
|
+
the discovered one, and poll that shell's own bridge.
|
|
235
|
+
errors when no live rnx world serves it, so a pinned run
|
|
236
|
+
can never quietly measure the default shell instead
|
|
120
237
|
--screenshot-paths <mode>
|
|
121
238
|
screenshot path mode: dir (default) or flow
|
|
122
239
|
--no-shell capture every screenshot tenant-only (no status bar,
|
|
@@ -134,6 +251,14 @@ options:
|
|
|
134
251
|
--new; overrides --headless). use when you want to
|
|
135
252
|
watch the flow run live even if the current sim is
|
|
136
253
|
headless or stale
|
|
254
|
+
--proof deterministic-capture mode: pin the TextInput caret
|
|
255
|
+
visible instead of blinking, stop notifications
|
|
256
|
+
auto-dismissing, and leave the device corners unclipped
|
|
257
|
+
to match a native simctl framebuffer. a screen holding a
|
|
258
|
+
focused text field cannot reach three identical idle
|
|
259
|
+
frames without this, so its screenshot fails after 12
|
|
260
|
+
tries. use it for any run whose screenshots get
|
|
261
|
+
compared to anything
|
|
137
262
|
--electron prefer opening the target in the desktop companion
|
|
138
263
|
--sim <id> target a specific current sim
|
|
139
264
|
--validate replay the drafted flow before clearing it
|
|
@@ -152,7 +277,7 @@ options:
|
|
|
152
277
|
|
|
153
278
|
frontmatter (top of flow.yaml between --- markers):
|
|
154
279
|
app: 8081 load this target before the flow starts
|
|
155
|
-
device: iphone-
|
|
280
|
+
device: iphone-16 set the live device model for this flow run
|
|
156
281
|
electron: true prefer the desktop companion sim for this flow
|
|
157
282
|
|
|
158
283
|
default runtime:
|
|
@@ -180,6 +305,15 @@ flow extension:
|
|
|
180
305
|
rnxExit(0)
|
|
181
306
|
}
|
|
182
307
|
|
|
308
|
+
const unexpected = findUnexpectedFlowArgs(args)
|
|
309
|
+
if (unexpected.length > 0) {
|
|
310
|
+
for (const problem of unexpected) {
|
|
311
|
+
console.error(` error: ${problem.message}`)
|
|
312
|
+
}
|
|
313
|
+
console.error('\n run `rnx maestro --help` for the flags this command accepts')
|
|
314
|
+
rnxExit(1)
|
|
315
|
+
}
|
|
316
|
+
|
|
183
317
|
const flowPath = args[0]
|
|
184
318
|
if (!fs.existsSync(flowPath)) {
|
|
185
319
|
console.error(` error: ${flowPath} not found`)
|
|
@@ -191,6 +325,10 @@ flow extension:
|
|
|
191
325
|
// --headed forces a fresh visible window: implies --new and overrides
|
|
192
326
|
// --headless (you asked to watch it run).
|
|
193
327
|
const headed = args.includes('--headed')
|
|
328
|
+
// deterministic-capture mode. pins everything that would otherwise change
|
|
329
|
+
// between two frames of an idle screen, so `takeScreenshot` reaches its
|
|
330
|
+
// three identical frames on the first try instead of hunting for them.
|
|
331
|
+
const proofMode = args.includes('--proof')
|
|
194
332
|
const openInNewSim = args.includes('--new') || headed
|
|
195
333
|
const outDir = getFlag('--out') || '/tmp/rnx-recordings'
|
|
196
334
|
// when --screenshots isn't explicitly set, default to the --out directory.
|
|
@@ -214,6 +352,12 @@ flow extension:
|
|
|
214
352
|
? 'shell'
|
|
215
353
|
: undefined
|
|
216
354
|
const deviceOverride = getFlag('--device')?.trim() || ''
|
|
355
|
+
// --device and --theme are also global flags, but a global sets the settings
|
|
356
|
+
// store inside THIS process and the sim renders in the shell's browser page,
|
|
357
|
+
// so `rnx --theme light maestro test` reaches nothing. both have to be read
|
|
358
|
+
// here and pushed over the bridge, and they are read the same way so a caller
|
|
359
|
+
// that gets one right cannot get the other silently ignored.
|
|
360
|
+
const themeOverride = getFlag('--theme')?.trim() || ''
|
|
217
361
|
const slow = args.includes('--slow') ? +(getFlag('--slow') || '500') : 0
|
|
218
362
|
const cliElectron = args.includes('--electron')
|
|
219
363
|
const preview = args.includes('--preview')
|
|
@@ -275,7 +419,7 @@ flow extension:
|
|
|
275
419
|
rnxExit(1)
|
|
276
420
|
}
|
|
277
421
|
const targetDevice = deviceOverride || frontmatter.device || ''
|
|
278
|
-
const targetTheme = frontmatter.theme || ''
|
|
422
|
+
const targetTheme = themeOverride || frontmatter.theme || ''
|
|
279
423
|
|
|
280
424
|
const wantElectron = cliElectron || frontmatter.electron === true
|
|
281
425
|
const rawTargetApp =
|
|
@@ -289,37 +433,29 @@ flow extension:
|
|
|
289
433
|
const targetAppResolution = await resolveAppFrontmatterTarget(rawTargetApp)
|
|
290
434
|
const targetApp = targetAppResolution.target
|
|
291
435
|
|
|
292
|
-
const
|
|
293
|
-
stripBooleanFlags: [
|
|
294
|
-
'--record',
|
|
295
|
-
'--profile',
|
|
296
|
-
'--electron',
|
|
297
|
-
'--new',
|
|
298
|
-
'--preview',
|
|
299
|
-
'--preview-open',
|
|
300
|
-
'--headless',
|
|
301
|
-
'--headed',
|
|
302
|
-
],
|
|
303
|
-
stripValueFlags: [
|
|
304
|
-
'--out',
|
|
305
|
-
'--device',
|
|
306
|
-
'--slow',
|
|
307
|
-
'--tail-wait',
|
|
308
|
-
'--url',
|
|
309
|
-
'--replace',
|
|
310
|
-
'--remap',
|
|
311
|
-
'--screenshots',
|
|
312
|
-
'--screenshot-paths',
|
|
313
|
-
'--preview-origin',
|
|
314
|
-
'--preview-public-origin',
|
|
315
|
-
'--driver',
|
|
316
|
-
'--billing-kind',
|
|
317
|
-
'--owner',
|
|
318
|
-
'--repo',
|
|
319
|
-
],
|
|
320
|
-
}
|
|
321
|
-
const parsedBridgeArgs = parseBridgeCliArgs(args, bridgeArgOptions)
|
|
436
|
+
const parsedBridgeArgs = parseBridgeCliArgs(args, FLOW_BRIDGE_ARG_OPTIONS)
|
|
322
437
|
const url = getFlag('--url') || ''
|
|
438
|
+
|
|
439
|
+
// --base-url pins the run to one shell, so a caller can build an engine from
|
|
440
|
+
// a known tree, serve it, and measure that build instead of whatever the
|
|
441
|
+
// discovered shell happens to hold. the page and the bridge have to stay in
|
|
442
|
+
// one world: without this retarget the flow would open the pinned shell and
|
|
443
|
+
// then poll the default world's bridge, waiting out its timeout with no sign
|
|
444
|
+
// of which shell it actually ran against.
|
|
445
|
+
const pinnedShellBaseUrl = getFlag('--base-url') || ''
|
|
446
|
+
if (pinnedShellBaseUrl) {
|
|
447
|
+
const pinned = resolveBridgePortForPin({
|
|
448
|
+
baseUrl: pinnedShellBaseUrl,
|
|
449
|
+
wsPort: parsedBridgeArgs.wsPort,
|
|
450
|
+
explicitPort: parsedBridgeArgs.explicitPort,
|
|
451
|
+
})
|
|
452
|
+
if (pinned.error !== undefined) {
|
|
453
|
+
console.error(` error: ${pinned.error}`)
|
|
454
|
+
rnxExit(1)
|
|
455
|
+
} else {
|
|
456
|
+
parsedBridgeArgs.wsPort = pinned.port
|
|
457
|
+
}
|
|
458
|
+
}
|
|
323
459
|
if (wantElectron && !findDesktopCompanion()) {
|
|
324
460
|
console.error(
|
|
325
461
|
' error: desktop companion not found. install or build it first with `bun run build:electron`',
|
|
@@ -338,7 +474,7 @@ flow extension:
|
|
|
338
474
|
const effectiveOpenInNewSim =
|
|
339
475
|
openInNewSim || (wantElectron && parsedBridgeArgs.simIdSource !== 'flag')
|
|
340
476
|
const driverFlag = getFlag('--driver') || ''
|
|
341
|
-
let
|
|
477
|
+
let closeFlowOwnedSimAfterRun = false
|
|
342
478
|
const ownedSimIds = new Set<string>()
|
|
343
479
|
|
|
344
480
|
// if we weren't told exactly where to go, and there's already a primary
|
|
@@ -379,16 +515,19 @@ flow extension:
|
|
|
379
515
|
|
|
380
516
|
if (openTarget && targetDevice) {
|
|
381
517
|
const shellBaseUrl = applyRNXConfigToUrl(
|
|
382
|
-
resolveShellBaseUrlForBridgePort(parsedBridgeArgs.wsPort),
|
|
518
|
+
pinnedShellBaseUrl || resolveShellBaseUrlForBridgePort(parsedBridgeArgs.wsPort),
|
|
383
519
|
targetAppResolution.runtimeConfig,
|
|
384
520
|
)
|
|
385
|
-
const url = new URL(
|
|
521
|
+
const url = new URL(
|
|
522
|
+
await buildShellUrl(openTarget, shellBaseUrl, { proof: proofMode }),
|
|
523
|
+
)
|
|
386
524
|
url.searchParams.set('device', targetDevice)
|
|
387
525
|
openTarget = url.toString()
|
|
388
526
|
}
|
|
389
527
|
|
|
390
528
|
if (openTarget) {
|
|
391
529
|
const openArgs = [openTarget]
|
|
530
|
+
if (pinnedShellBaseUrl) openArgs.push('--base-url', pinnedShellBaseUrl)
|
|
392
531
|
openArgs.push(...replaceArgs)
|
|
393
532
|
openArgs.push(...remapArgs)
|
|
394
533
|
if (effectiveOpenInNewSim) {
|
|
@@ -400,23 +539,30 @@ flow extension:
|
|
|
400
539
|
}
|
|
401
540
|
if (driverFlag) {
|
|
402
541
|
openArgs.push('--driver', driverFlag)
|
|
403
|
-
closeManagedDriverSimAfterRun = true
|
|
404
542
|
}
|
|
405
543
|
if (args.includes('--headless') && !headed) {
|
|
406
544
|
openArgs.push('--headless')
|
|
407
545
|
}
|
|
546
|
+
if (proofMode) openArgs.push('--proof')
|
|
408
547
|
await runOpenCommand(openArgs, {
|
|
409
548
|
port: parsedBridgeArgs.wsPort,
|
|
410
549
|
timeoutMs: parsedBridgeArgs.commandTimeoutMs,
|
|
411
550
|
runtimeConfig: targetAppResolution.runtimeConfig,
|
|
412
551
|
})
|
|
413
|
-
|
|
552
|
+
// a flow that opened its own sim owns it, and owning it means closing it.
|
|
553
|
+
// this used to be claimed only for `--driver` runs, so a `--new` run
|
|
554
|
+
// stranded its browser: the detached host's watchdog polls the *top*
|
|
555
|
+
// ancestor (getStableOwnerPid -> findTopAncestor), which is the shell or
|
|
556
|
+
// agent session, not the run. that pid lives for hours, so the watchdog
|
|
557
|
+
// stayed happy and every flow left one more Chrome behind.
|
|
558
|
+
closeFlowOwnedSimAfterRun = effectiveOpenInNewSim || !!driverFlag
|
|
559
|
+
if (closeFlowOwnedSimAfterRun) {
|
|
414
560
|
const openedSimId = readCurrentSimId()
|
|
415
561
|
if (openedSimId) ownedSimIds.add(openedSimId)
|
|
416
562
|
}
|
|
417
563
|
}
|
|
418
564
|
|
|
419
|
-
const resolvedBridgeArgs = parseBridgeCliArgs(args,
|
|
565
|
+
const resolvedBridgeArgs = parseBridgeCliArgs(args, FLOW_BRIDGE_ARG_OPTIONS)
|
|
420
566
|
const pinnedSimId =
|
|
421
567
|
resolvedBridgeArgs.simIdSource === 'flag'
|
|
422
568
|
? resolvedBridgeArgs.simId
|
|
@@ -432,7 +578,7 @@ flow extension:
|
|
|
432
578
|
simIdSource: pinnedSimIdSource,
|
|
433
579
|
})
|
|
434
580
|
let driver: SootSimBridgeFlowRunner
|
|
435
|
-
const
|
|
581
|
+
const applySimSettings = async () => {
|
|
436
582
|
const statusBarTime = frontmatter.env?.RNX_STATUS_BAR_TIME
|
|
437
583
|
if (statusBarTime) {
|
|
438
584
|
if (!/^(?:[1-9]|1[0-2]):[0-5][0-9]$/.test(statusBarTime)) {
|
|
@@ -483,7 +629,7 @@ flow extension:
|
|
|
483
629
|
// recorder lives in-page state and gets wiped by window.location.reload.
|
|
484
630
|
onAfterLaunch: async (simId) => {
|
|
485
631
|
if (simId) saveCurrentSimId(simId)
|
|
486
|
-
const deviceChanged = await
|
|
632
|
+
const deviceChanged = await applySimSettings()
|
|
487
633
|
if (deviceChanged) await driver.waitForTree(120000)
|
|
488
634
|
if (!preview) return
|
|
489
635
|
await bridge.send({
|
|
@@ -526,7 +672,7 @@ flow extension:
|
|
|
526
672
|
if (!startsWithLaunchApp) {
|
|
527
673
|
await driver.waitForTree(120000)
|
|
528
674
|
}
|
|
529
|
-
const deviceChanged = await
|
|
675
|
+
const deviceChanged = await applySimSettings()
|
|
530
676
|
if (deviceChanged && !startsWithLaunchApp) await driver.waitForTree(120000)
|
|
531
677
|
// preview replay must reproduce the SAME initial conditions the agent
|
|
532
678
|
// explored from. the explore session runs on a fresh ephemeral profile
|
|
@@ -783,7 +929,7 @@ flow extension:
|
|
|
783
929
|
|
|
784
930
|
bridge.close()
|
|
785
931
|
|
|
786
|
-
if (
|
|
932
|
+
if (closeFlowOwnedSimAfterRun) {
|
|
787
933
|
if (driver.simId) ownedSimIds.add(driver.simId)
|
|
788
934
|
await closeFlowOwnedSims(parsedBridgeArgs.wsPort, parsedBridgeArgs.commandTimeoutMs, [
|
|
789
935
|
...ownedSimIds,
|
|
@@ -33,7 +33,7 @@ export async function runGetLayoutSubcommand(opts: {
|
|
|
33
33
|
error.message === 'unsupported RNX Cloud command type: evaluate'
|
|
34
34
|
) {
|
|
35
35
|
throw new Error(
|
|
36
|
-
'get layout --styling is unavailable
|
|
36
|
+
'get layout --styling is unavailable on a remote simulator; rerun without --styling or use a local sim',
|
|
37
37
|
)
|
|
38
38
|
}
|
|
39
39
|
throw error
|
package/cli/commands/inspect.ts
CHANGED
|
@@ -436,7 +436,9 @@ function printRenderProfile(worker: string, rp: Record<string, any> | undefined)
|
|
|
436
436
|
const recordCauses = [
|
|
437
437
|
rp.boundaryRecordsInvalidated ? `invalidated ${rp.boundaryRecordsInvalidated}` : '',
|
|
438
438
|
rp.boundaryRecordsOrigin ? `moved ${rp.boundaryRecordsOrigin}` : '',
|
|
439
|
-
rp.
|
|
439
|
+
rp.boundaryRecordsFirst ? `first-record ${rp.boundaryRecordsFirst}` : '',
|
|
440
|
+
rp.boundaryRecordsScheme ? `scheme ${rp.boundaryRecordsScheme}` : '',
|
|
441
|
+
rp.boundaryRecordsFont ? `late-font ${rp.boundaryRecordsFont}` : '',
|
|
440
442
|
]
|
|
441
443
|
.filter(Boolean)
|
|
442
444
|
.join(' · ')
|
|
@@ -472,6 +474,11 @@ function printRenderProfile(worker: string, rp: Record<string, any> | undefined)
|
|
|
472
474
|
)
|
|
473
475
|
}
|
|
474
476
|
}
|
|
477
|
+
if (rp.paragraphBuilds || rp.paragraphLayouts) {
|
|
478
|
+
console.log(
|
|
479
|
+
` text shaping: ${rp.paragraphBuilds} shaped ${Number(rp.paragraphBuildMs ?? 0).toFixed(1)}ms · ${rp.paragraphLayouts} re-broken ${Number(rp.paragraphLayoutMs ?? 0).toFixed(1)}ms`,
|
|
480
|
+
)
|
|
481
|
+
}
|
|
475
482
|
console.log(` blur: ${rp.avgBlurMs}ms`)
|
|
476
483
|
if (rp.glassDownsampleActive || rp.glassBackdropCacheMisses) {
|
|
477
484
|
console.log(
|
package/cli/commands/login.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { exec } from 'node:child_process'
|
|
2
2
|
import { randomBytes } from 'node:crypto'
|
|
3
3
|
import http from 'node:http'
|
|
4
|
+
import { createInterface } from 'node:readline/promises'
|
|
4
5
|
import {
|
|
5
6
|
refreshSharedDesktopAuthSession,
|
|
7
|
+
sharedDesktopAuthAccounts,
|
|
6
8
|
writeSharedDesktopAuthSession,
|
|
9
|
+
type SharedDesktopAuthSession,
|
|
7
10
|
} from '../../src/auth/shared-session'
|
|
8
11
|
import { rnxPublicBrand } from '../../src/public-brand'
|
|
9
12
|
import { rnxExit } from '../run-rnx'
|
|
@@ -26,15 +29,80 @@ function printHelp() {
|
|
|
26
29
|
rnx login — sign in so uploads can attach to your account
|
|
27
30
|
|
|
28
31
|
usage:
|
|
29
|
-
rnx login [--origin <url>]
|
|
32
|
+
rnx login [--origin <url>] [--account <name-or-id>]
|
|
30
33
|
|
|
31
34
|
options:
|
|
32
|
-
--origin <url>
|
|
33
|
-
|
|
35
|
+
--origin <url> auth host (default: ${REMOTE_DEFAULT_ORIGIN})
|
|
36
|
+
override with RNX_AUTH_ORIGIN env var
|
|
37
|
+
--account <name-or-id> the account remote simulators and boxes bill to.
|
|
38
|
+
one account is chosen for you; several ask.
|
|
34
39
|
-h, --help
|
|
35
40
|
`)
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
// a session token names the person, not an account. the account that pays for
|
|
44
|
+
// cloud work is chosen here, once, and stored on the session; a person with one
|
|
45
|
+
// account never sees the question.
|
|
46
|
+
async function chooseAccount(
|
|
47
|
+
session: SharedDesktopAuthSession,
|
|
48
|
+
requested: string | undefined,
|
|
49
|
+
): Promise<{ id: string; name: string } | null> {
|
|
50
|
+
const accounts = sharedDesktopAuthAccounts(session.user)
|
|
51
|
+
if (requested !== undefined) {
|
|
52
|
+
const wanted = requested.trim().toLowerCase()
|
|
53
|
+
const match = accounts.find(
|
|
54
|
+
(account) =>
|
|
55
|
+
account.id === requested.trim() || account.name.toLowerCase() === wanted,
|
|
56
|
+
)
|
|
57
|
+
if (!match) {
|
|
58
|
+
console.error(
|
|
59
|
+
` no account named ${JSON.stringify(requested)}. your accounts:\n` +
|
|
60
|
+
accounts.map((account) => ` ${account.name} (${account.id})`).join('\n'),
|
|
61
|
+
)
|
|
62
|
+
rnxExit(1)
|
|
63
|
+
}
|
|
64
|
+
return match
|
|
65
|
+
}
|
|
66
|
+
if (accounts.length <= 1) return accounts[0] ?? null
|
|
67
|
+
if (!process.stdin.isTTY) {
|
|
68
|
+
console.error(
|
|
69
|
+
' this sign-in belongs to several accounts; rerun with --account <name> to choose the one to bill:\n' +
|
|
70
|
+
accounts.map((account) => ` ${account.name} (${account.id})`).join('\n'),
|
|
71
|
+
)
|
|
72
|
+
rnxExit(1)
|
|
73
|
+
}
|
|
74
|
+
console.log(' which account should remote simulators and boxes bill to?')
|
|
75
|
+
accounts.forEach((account, index) => console.log(` ${index + 1}. ${account.name}`))
|
|
76
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout })
|
|
77
|
+
try {
|
|
78
|
+
for (;;) {
|
|
79
|
+
const answer = (await prompt.question(` account [1-${accounts.length}]: `)).trim()
|
|
80
|
+
const chosen = accounts[Number(answer) - 1]
|
|
81
|
+
if (chosen && /^\d+$/.test(answer)) return chosen
|
|
82
|
+
console.log(` enter a number from 1 to ${accounts.length}`)
|
|
83
|
+
}
|
|
84
|
+
} finally {
|
|
85
|
+
prompt.close()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function finishLogin(
|
|
90
|
+
origin: string,
|
|
91
|
+
requestedAccount: string | undefined,
|
|
92
|
+
fallbackLabel: string | undefined,
|
|
93
|
+
suffix: string,
|
|
94
|
+
) {
|
|
95
|
+
const refreshed = await refreshSharedDesktopAuthSession(origin)
|
|
96
|
+
const account = refreshed ? await chooseAccount(refreshed, requestedAccount) : null
|
|
97
|
+
if (refreshed && account) {
|
|
98
|
+
writeSharedDesktopAuthSession({ ...refreshed, accountId: account.id })
|
|
99
|
+
}
|
|
100
|
+
const label = refreshed?.user?.email || fallbackLabel || refreshed?.user?.id
|
|
101
|
+
console.log(
|
|
102
|
+
` signed in${label ? ` as ${label}` : ''}${account ? ` (${account.name})` : ''}${suffix}`,
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
38
106
|
function extractStringFlag(args: string[], name: string): string | undefined {
|
|
39
107
|
const idx = args.findIndex((a) => a === name)
|
|
40
108
|
if (idx < 0) return undefined
|
|
@@ -147,6 +215,7 @@ export async function runLogin(args: string[]) {
|
|
|
147
215
|
|
|
148
216
|
const mutableArgs = [...args]
|
|
149
217
|
const origin = resolveDefaultAuthOrigin(extractStringFlag(mutableArgs, '--origin'))
|
|
218
|
+
const requestedAccount = extractStringFlag(mutableArgs, '--account')
|
|
150
219
|
|
|
151
220
|
// in dev (localhost), skip the browser and sign in as the shared dev
|
|
152
221
|
// user via POST /api/dev-login. the no-device contract is local-only, so this
|
|
@@ -161,13 +230,12 @@ export async function runLogin(args: string[]) {
|
|
|
161
230
|
origin,
|
|
162
231
|
source: 'cli',
|
|
163
232
|
})
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
devResult.email ||
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
console.log(` signed in${label ? ` as ${label}` : ''} (dev)`)
|
|
233
|
+
await finishLogin(
|
|
234
|
+
origin,
|
|
235
|
+
requestedAccount,
|
|
236
|
+
devResult.email || devResult.userId,
|
|
237
|
+
' (dev)',
|
|
238
|
+
)
|
|
171
239
|
return
|
|
172
240
|
}
|
|
173
241
|
console.log(` dev-login not available (${devResult.error}); falling back to browser`)
|
|
@@ -285,8 +353,5 @@ export async function runLogin(args: string[]) {
|
|
|
285
353
|
source: 'cli',
|
|
286
354
|
})
|
|
287
355
|
|
|
288
|
-
|
|
289
|
-
const label =
|
|
290
|
-
refreshed?.user?.email || result.email || refreshed?.user?.id || result.userId
|
|
291
|
-
console.log(` signed in${label ? ` as ${label}` : ''}`)
|
|
356
|
+
await finishLogin(origin, requestedAccount, result.email || result.userId, '')
|
|
292
357
|
}
|