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.
Files changed (78) hide show
  1. package/README.md +20 -50
  2. package/cli/auth.ts +33 -3
  3. package/cli/cloud-client.ts +524 -88
  4. package/cli/cloud-dispatch.ts +5 -10
  5. package/cli/cloud-session.ts +5 -5
  6. package/cli/commands/auth.ts +13 -2
  7. package/cli/commands/box.ts +189 -20
  8. package/cli/commands/control.ts +40 -13
  9. package/cli/commands/device.ts +1 -1
  10. package/cli/commands/flow.ts +189 -43
  11. package/cli/commands/inspect/get-layout.ts +1 -1
  12. package/cli/commands/inspect.ts +8 -1
  13. package/cli/commands/login.ts +79 -14
  14. package/cli/commands/maestro-generate.ts +70 -54
  15. package/cli/commands/maestro.ts +69 -20
  16. package/cli/commands/platform.ts +30 -32
  17. package/cli/commands/screenshot.ts +3 -2
  18. package/cli/commands/upload.ts +28 -10
  19. package/cli/help-core.ts +11 -1
  20. package/cli/outbound-endpoints.ts +27 -5
  21. package/cli/parse-args.ts +17 -0
  22. package/cli/shell-init.ts +4 -4
  23. package/cli/ws-bridge.ts +109 -62
  24. package/dist-lib/agent-daemon-client.cjs +1 -1
  25. package/dist-lib/agent-events.cjs +1 -1
  26. package/dist-lib/agent-identity.cjs +1 -1
  27. package/dist-lib/agent-sessions.cjs +1 -1
  28. package/dist-lib/attached-projects.cjs +1 -1
  29. package/dist-lib/auth/shared-session.cjs +17 -4
  30. package/dist-lib/backend-origin.cjs +1 -1
  31. package/dist-lib/beta.cjs +1 -1
  32. package/dist-lib/beta.mjs +1 -1
  33. package/dist-lib/bridge-constants.cjs +1 -1
  34. package/dist-lib/bridge-contract-input.cjs +1 -1
  35. package/dist-lib/bridge-contract-input.mjs +1 -1
  36. package/dist-lib/bridge-contract.cjs +1 -1
  37. package/dist-lib/bridge-contract.mjs +1 -1
  38. package/dist-lib/capture-contract.cjs +1 -1
  39. package/dist-lib/capture-contract.mjs +1 -1
  40. package/dist-lib/cli-constants.cjs +1 -1
  41. package/dist-lib/cloud-contract.cjs +1 -1
  42. package/dist-lib/cloud-contract.mjs +1 -1
  43. package/dist-lib/cloud.cjs +108 -93
  44. package/dist-lib/cloud.mjs +106 -91
  45. package/dist-lib/config.cjs +1 -1
  46. package/dist-lib/detox/index.cjs +1 -1
  47. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  48. package/dist-lib/home-paths.cjs +1 -1
  49. package/dist-lib/host/bridge-host.cjs +1 -1
  50. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  51. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  52. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  53. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  54. package/dist-lib/host/websocket-proxy.cjs +1 -1
  55. package/dist-lib/index.cjs +58 -139
  56. package/dist-lib/jump-to-source-babel.cjs +1 -1
  57. package/dist-lib/menu.cjs +1 -1
  58. package/dist-lib/menu.mjs +1 -1
  59. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  60. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  61. package/dist-lib/metro-production-bundle.cjs +1 -1
  62. package/dist-lib/metro-production-bundle.mjs +1 -1
  63. package/dist-lib/metro.cjs +33 -212
  64. package/dist-lib/profiles.cjs +1 -1
  65. package/dist-lib/public-brand.cjs +1 -1
  66. package/dist-lib/react-native-host-modules.cjs +1 -1
  67. package/dist-lib/react-native-host-modules.mjs +1 -1
  68. package/dist-lib/render-mode.cjs +1 -1
  69. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  70. package/dist-lib/sdk.cjs +1 -1
  71. package/dist-lib/sdk.mjs +1 -1
  72. package/dist-lib/skills.cjs +436 -351
  73. package/dist-lib/vite.cjs +1 -1
  74. package/package.json +2 -1
  75. package/src/auth/shared-session.ts +31 -0
  76. package/src/cloud.ts +83 -113
  77. package/src/metro-plugin.ts +30 -113
  78. 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
- const VALUE_FLAGS = new Set([
31
- '--origin',
32
- '--preview-origin',
33
- '--public-origin',
34
- '--preview-public-origin',
35
- '--owner',
36
- '--repo',
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
- function findPrompt(args: string[]): { prompt: string; index: number } | null {
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
- '--new',
70
- '--headless',
71
- '--headed',
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
- if (passthroughValues.has(arg)) {
267
+ out.push(arg)
268
+ if (VALUE_FLAGS.has(arg)) {
291
269
  const value = args[i + 1]
292
- if (value) out.push(arg, value)
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()
@@ -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 re-run the flow on file changes (alias for --watch)
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
- // Remove Maestro-only flags that the shared runner does not know.
148
- // these are accepted so users can literally s/maestro/rnx maestro/ in
149
- // their scripts without edits.
150
- function stripUnknownMaestroFlags(args: string[]): string[] {
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' || a === '--device' || a === '-d') {
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
- // map to --watch for runFlowPlayback's existing watch path if one
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
- remaining = stripUnknownMaestroFlags(remaining)
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
- const positional = remaining.slice(1).find((a) => !looksLikeFlag(a))
329
- if (positional) {
330
- target = path.resolve(cwd, positional)
331
- forwardArgs = remaining.slice(1).filter((a) => a !== positional)
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 = remaining.slice(1)
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') ?? flagValue('--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
- console.error(` x ${(err as Error).message}`)
418
- exit = 1
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
@@ -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
- // (host, local box, or cloud box). there is deliberately no per-place spelling
5
- // such as `rnx cloud ios`, so this file only resolves the device and hands the
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 CLOUD_USAGE =
30
- 'usage: rnx ios --nano <local bundle.js|https://bundle-url> [--assets <dir>] [--device <model>]'
29
+ const REMOTE_USAGE =
30
+ 'usage: rnx ios --remote <local bundle.js|https://bundle-url> [--assets <dir>] [--device <model>]'
31
31
 
32
- function cloudInputFromArgs(args: string[]): {
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 === '--nano' || arg === '--cloud') continue
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 --nano --assets requires a directory')
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 --nano --assets requires a directory')
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 --nano creates a new simulator and cannot target --sim')
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 --nano does not support ${arg}`)
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(CLOUD_USAGE)
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('--nano') || args.includes('--cloud')) {
117
+ if (args.includes('--remote')) {
125
118
  if (platform !== 'ios') {
126
119
  console.error(
127
- ` ${rnxPublicBrand.commandName} android --nano is not available; RNX Cloud currently supports iOS`,
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 = cloudInputFromArgs(args)
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 --nano')
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(` cloud box: ${existingSession.boxId} (nano)`)
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(` cloud box: ${receipt.boxId} (nano)`)
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 --nano failed: ${
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
- '--nano',
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('RNX Cloud screenshot returned invalid image data')
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) throw new Error('RNX Cloud screenshot returned no image data')
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) {
@@ -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 { createBridgeFromParsed, evalInBridge, parseBridgeCliArgs } from '../ws-bridge'
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 window.__sootsimLastTransformedBundle.text over the bridge in
317
- // slices. a single eval returning tens of MB of base64 hits the ws
318
- // message framing cap; stringly slicing lets us page it in safely.
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 = 1_500_000 // ~1.5 MB of utf-16 source per call
324
- const pieces: string[] = []
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 evalInBridge<string | null>(
335
+ const chunk = await callInBridge<string | null>(
328
336
  bridge,
329
- `(window.__sootsimLastTransformedBundle?.text || "").slice(${offset}, ${end})`,
337
+ 'SootSim.bridges.readTransformedBundleSlice',
338
+ offset,
339
+ end,
330
340
  )
331
341
  if (typeof chunk !== 'string') return null
332
- pieces.push(chunk)
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 pieces.join('')
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.`