rnxsim 0.1.432 → 0.1.434

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 (83) hide show
  1. package/README.md +20 -50
  2. package/cli/auth.ts +33 -3
  3. package/cli/cloud-client.ts +577 -99
  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 +270 -26
  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 +32 -5
  21. package/cli/parse-args.ts +17 -0
  22. package/cli/send-to-box.ts +112 -0
  23. package/cli/shell-init.ts +4 -4
  24. package/cli/ws-bridge.ts +109 -62
  25. package/dist-lib/agent-daemon-client.cjs +1 -1
  26. package/dist-lib/agent-events.cjs +1 -1
  27. package/dist-lib/agent-identity.cjs +1 -1
  28. package/dist-lib/agent-sessions.cjs +1 -1
  29. package/dist-lib/attached-projects.cjs +1 -1
  30. package/dist-lib/auth/shared-session.cjs +17 -4
  31. package/dist-lib/backend-origin.cjs +1 -1
  32. package/dist-lib/beta.cjs +1 -1
  33. package/dist-lib/beta.mjs +1 -1
  34. package/dist-lib/bridge-constants.cjs +1 -1
  35. package/dist-lib/bridge-contract-input.cjs +1 -1
  36. package/dist-lib/bridge-contract-input.mjs +1 -1
  37. package/dist-lib/bridge-contract.cjs +1 -1
  38. package/dist-lib/bridge-contract.mjs +1 -1
  39. package/dist-lib/capture-contract.cjs +1 -1
  40. package/dist-lib/capture-contract.mjs +1 -1
  41. package/dist-lib/cli-constants.cjs +1 -1
  42. package/dist-lib/cloud-contract.cjs +1 -1
  43. package/dist-lib/cloud-contract.mjs +1 -1
  44. package/dist-lib/cloud.cjs +108 -93
  45. package/dist-lib/cloud.mjs +106 -91
  46. package/dist-lib/config.cjs +1 -1
  47. package/dist-lib/detox/index.cjs +1 -1
  48. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  49. package/dist-lib/home-paths.cjs +1 -1
  50. package/dist-lib/host/bridge-host.cjs +5324 -1371
  51. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  52. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  53. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  54. package/dist-lib/host/replacement-module-handler.cjs +298 -91
  55. package/dist-lib/host/websocket-proxy.cjs +1 -1
  56. package/dist-lib/index.cjs +369 -241
  57. package/dist-lib/jump-to-source-babel.cjs +1 -1
  58. package/dist-lib/menu.cjs +7 -1
  59. package/dist-lib/menu.mjs +7 -1
  60. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  61. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  62. package/dist-lib/metro-production-bundle.cjs +1 -1
  63. package/dist-lib/metro-production-bundle.mjs +1 -1
  64. package/dist-lib/metro.cjs +327 -299
  65. package/dist-lib/profiles.cjs +1 -1
  66. package/dist-lib/public-brand.cjs +1 -1
  67. package/dist-lib/react-native-host-modules.cjs +1 -1
  68. package/dist-lib/react-native-host-modules.mjs +1 -1
  69. package/dist-lib/render-mode.cjs +1 -1
  70. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  71. package/dist-lib/sdk.cjs +1 -1
  72. package/dist-lib/sdk.mjs +1 -1
  73. package/dist-lib/skills.cjs +4849 -1199
  74. package/dist-lib/vite.cjs +298 -91
  75. package/package.json +3 -2
  76. package/src/auth/shared-session.ts +31 -0
  77. package/src/bridge-contract.ts +6 -0
  78. package/src/cloud.ts +83 -113
  79. package/src/host/bridge-host.ts +79 -0
  80. package/src/host/replacement-module-handler.ts +20 -96
  81. package/src/menu.ts +8 -0
  82. package/src/metro-plugin.ts +30 -113
  83. package/src/vite-plugin.ts +11 -2
@@ -115,7 +115,7 @@ export const OUTBOUND_ENDPOINTS: Record<string, OutboundEndpointDeclaration> = {
115
115
  'https://rnxsim.com by default, or the test or disposable origin named by RNX_CLOUD_ORIGIN',
116
116
  preConsent: false,
117
117
  payload:
118
- 'the Metro JavaScript bundle selected by rnx ios --nano, then structured simulator commands and lifecycle requests',
118
+ 'the Metro JavaScript bundle selected by rnx ios --remote, then structured simulator commands and lifecycle requests',
119
119
  },
120
120
  }
121
121
 
@@ -152,8 +152,6 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
152
152
  { category: 'rnx_cloud_sim', count: 1 },
153
153
  'packages/sootsim/src/cloud.ts :: fetch :: `${this.session.origin}/v1/sims/${encodeURIComponent(this.session.simId)}/claim`':
154
154
  { category: 'rnx_cloud_sim', count: 1 },
155
- 'packages/sootsim/src/cloud.ts :: fetch :: `${this.session.origin}/v1/boxes/${encodeURIComponent(this.session.boxId)}/simulators`':
156
- { category: 'rnx_cloud_sim', count: 1 },
157
155
  'packages/sootsim/src/cloud.ts :: fetch :: `${this.session.origin}/v1/boxes/${encodeURIComponent(this.session.boxId)}`':
158
156
  { category: 'rnx_cloud_sim', count: 1 },
159
157
  // maestro's flow `http` API. the url is whatever the flow author wrote, and
@@ -173,17 +171,36 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
173
171
  category: 'rnx_cloud_box',
174
172
  count: 1,
175
173
  },
174
+ // a micro box's own Browser Run session, over the worker's BROWSER binding.
175
+ // the binding routes on path alone and ignores the host, so `browser.invalid`
176
+ // is a placeholder that exists only so URL can parse the search parameters.
177
+ // nothing leaves Cloudflare and no project content is sent.
178
+ 'packages/rnx-cloud-box/src/browser-run.ts :: fetch :: sessionEndpoint(undefined, { keepAliveMs, includeTargets: true })':
179
+ { category: 'rnx_cloud_box', count: 1 },
180
+ 'packages/rnx-cloud-box/src/browser-run.ts :: fetch :: `https://browser.invalid/v1/devtools/browser/${sessionId}/json/list`':
181
+ { category: 'rnx_cloud_box', count: 1 },
182
+ // twice: closing the session, and the CDP websocket upgrade that navigates a
183
+ // target. both address the same session over the same binding.
184
+ 'packages/rnx-cloud-box/src/browser-run.ts :: fetch :: `https://browser.invalid/v1/devtools/browser/${sessionId}`':
185
+ { category: 'rnx_cloud_box', count: 2 },
176
186
  // the account's own session meter: starting, listing and heartbeating the
177
187
  // session a cloud box is billed against. no project content.
178
- "packages/rnx-cloud-box/src/client.ts :: fetch :: `${options.apiOrigin.replace(/\\/+$/, '')}/api/v1/rnx/sessions`":
188
+ "packages/rnx-cloud-box/src/client.ts :: fetch :: `${options.apiOrigin.replace(/\\/+$/, '')}/api/v1/rnx/sessions${ options.accountId ? `?accountId=${encodeURIComponent(options.accountId)}` : '' }`":
179
189
  { category: 'contrast_user_action', count: 1 },
180
190
  'packages/rnx-cloud-box/src/client.ts :: fetch :: url': {
181
191
  category: 'contrast_user_action',
182
192
  count: 1,
183
193
  },
194
+ // `rnx box connect` asks the account service which box that ID or name is
195
+ // and for the box's own token. the account is the only thing that knows, so
196
+ // this is the one call that turns a name into an authorized Box reference.
197
+ 'packages/rnx-cloud-box/src/client.ts :: fetch :: connectUrl': {
198
+ category: 'contrast_user_action',
199
+ count: 1,
200
+ },
184
201
  "packages/rnx-cloud-box/src/box-do.ts :: fetch :: `${origin}/api/v1/rnx/sessions${serviceAuthorized ? '/internal' : ''}`":
185
202
  { category: 'contrast_user_action', count: 1 },
186
- "packages/rnx-cloud-box/src/box-do.ts :: fetch :: `${origin}/api/v1/rnx/sessions/${ serviceAuthority ? 'internal/' : '' }${encodeURIComponent(sessionId)}`":
203
+ "packages/rnx-cloud-box/src/box-do.ts :: fetch :: `${origin}/api/v1/rnx/sessions/${ serviceAuthority ? 'internal/' : '' }${encodeURIComponent(sessionId)}${ accountId ? `?accountId=${encodeURIComponent(accountId)}` : '' }`":
187
204
  { category: 'contrast_user_action', count: 1 },
188
205
  'packages/rnx-cloud-box/src/box-do.ts :: fetch :: `${origin}/api/v1/rnx/sessions/internal/${encodeURIComponent(sessionId)}`':
189
206
  { category: 'contrast_user_action', count: 1 },
@@ -220,6 +237,11 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
220
237
  },
221
238
  'packages/sootsim/scripts/smoke-cli-binary.ts :: fetch :: `http://127.0.0.1:${port}/healthz`':
222
239
  { category: 'local_development', count: 1 },
240
+ // the binary smoke's replacement-module probe against its own loopback server
241
+ 'packages/sootsim/scripts/smoke-cli-binary.ts :: fetch :: url': {
242
+ category: 'local_development',
243
+ count: 1,
244
+ },
223
245
  'packages/sootsim-engine/src/auth/openLogin.ts :: fetch :: `${CONTRAST_ORIGIN}/api/dev-login`':
224
246
  { category: 'contrast_user_action', count: 1 },
225
247
  'packages/sootsim-engine/src/auth/shared-session.ts :: fetch :: `${CONTRAST_ORIGIN}/api/auth/me`':
@@ -324,6 +346,11 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
324
346
  category: 'same_origin_runtime',
325
347
  count: 1,
326
348
  },
349
+ // the cpu simulator iframe document's dedicated worker loads the headless
350
+ // canvaskit wasm and the font and asset manifests from the page's own
351
+ // `/deps-native/engine-assets/` directory.
352
+ 'packages/sootsim-engine/src/headless/browser-tenant-worker.ts :: fetch :: new URL(name, assetBase).href':
353
+ { category: 'same_origin_runtime', count: 1 },
327
354
  // the tenant build's built-in app chunk prefetch list and the chunk bodies
328
355
  // it names, resolved against the worker chunk's own url.
329
356
  'packages/sootsim-engine/src/render-worker/app-in-worker.tsx :: fetch :: listUrl': {
package/cli/parse-args.ts CHANGED
@@ -31,6 +31,23 @@ const GLOBAL_FLAGS: Record<
31
31
  '--tab': { type: 'string' },
32
32
  }
33
33
 
34
+ // every spelling parseRnxArgs consumes before the command name, mapped to
35
+ // whether it takes a value. a command that rejects unknown flags uses this to
36
+ // tell a genuinely unknown flag apart from a global one typed after the
37
+ // command, where nothing reads it, and to show how to pass that one: telling
38
+ // someone to write `rnx --verbose <value>` earns them a second error.
39
+ export const GLOBAL_FLAG_TAKES_VALUE: ReadonlyMap<string, boolean> = new Map(
40
+ Object.entries(GLOBAL_FLAGS).flatMap(([name, def]): [string, boolean][] => {
41
+ const takesValue = def.type !== 'boolean'
42
+ return def.short
43
+ ? [
44
+ [name, takesValue],
45
+ [def.short, takesValue],
46
+ ]
47
+ : [[name, takesValue]]
48
+ }),
49
+ )
50
+
34
51
  // top-level verbs that route into runInspect. the surface is intentionally
35
52
  // tiny — everything else lives under one of the grouping verbs:
36
53
  // - `rnx get <noun>` reads
@@ -0,0 +1,112 @@
1
+ // "send to box": the running local app becomes a nano cloud simulator.
2
+ //
3
+ // one function serves both callers. the rail button in the shell reaches it
4
+ // through the local daemon's `/__send-to-box` route, and `rnx box send` calls
5
+ // it directly; both hand it a way to run a bridge command against the sim that
6
+ // is running the app, and both get the same simulator id and URL back.
7
+
8
+ import { isStorageSnapshot } from '../../sootsim-engine/src/preview/storage-snapshot-contract'
9
+ import {
10
+ DEFAULT_DEVICE_BY_PLATFORM,
11
+ getRuntimePlatform,
12
+ listDeviceModels,
13
+ } from '../../sootsim-engine/src/settings/devices'
14
+ import { toRNXProductionBundleUrl } from '../src/metro-production-bundle'
15
+ import { createCloudBox } from './cloud-client'
16
+ import type { StorageSnapshot } from '../../sootsim-engine/src/preview/storage-snapshot-contract'
17
+ import type { DeviceModel } from '../../sootsim-engine/src/settings/devices'
18
+ import type { BridgeCommandType } from '../src/bridge-contract'
19
+
20
+ export interface SendToBoxOptions {
21
+ /** runs one bridge command against the sim running the app. */
22
+ send: (command: { type: BridgeCommandType; [key: string]: unknown }) => Promise<unknown>
23
+ authorization: string
24
+ /** the account the box bills. an api key names its own, so it sends none. */
25
+ accountId: string | null
26
+ /** the cloud device model. defaults to the device the sim is running on. */
27
+ device?: DeviceModel
28
+ /**
29
+ * carry the signed-in session into the box. on by default in the shell: the
30
+ * box is the sender's own, and an app that boots signed out is not the app
31
+ * they were looking at.
32
+ */
33
+ carryAuth: boolean
34
+ boxName?: string | null
35
+ /** the `--assets-dest` directory Metro wrote, when the sender knows it. */
36
+ assetsPath?: string | null
37
+ apiOrigin?: string
38
+ simId?: string
39
+ }
40
+
41
+ export interface SendToBoxResult {
42
+ boxId: string
43
+ simId: string
44
+ url: string
45
+ carriedAuth: boolean
46
+ warnings: string[]
47
+ }
48
+
49
+ function isRecord(value: unknown): value is Record<string, unknown> {
50
+ return !!value && typeof value === 'object' && !Array.isArray(value)
51
+ }
52
+
53
+ // the box opens on the device the sender was looking at. a remote simulator is
54
+ // iOS only, so an Android sim sends to the default iPhone rather than refusing.
55
+ function cloudDevice(capturedModel: unknown): DeviceModel {
56
+ const captured = listDeviceModels().find((model) => model === capturedModel)
57
+ return captured && getRuntimePlatform(captured) === 'ios'
58
+ ? captured
59
+ : DEFAULT_DEVICE_BY_PLATFORM.ios
60
+ }
61
+
62
+ export async function sendToBox(options: SendToBoxOptions): Promise<SendToBoxResult> {
63
+ const captured: unknown = await options.send({
64
+ type: 'evaluate',
65
+ simId: options.simId,
66
+ code: '(typeof window.__sootsimCaptureBundle === "function") ? window.__sootsimCaptureBundle() : null',
67
+ })
68
+ const bundleUrl = isRecord(captured) ? captured.bundleUrl : null
69
+ if (!isRecord(captured) || typeof bundleUrl !== 'string' || !bundleUrl) {
70
+ throw new Error(
71
+ 'send to box needs a simulator running an app: this one has no bundle loaded',
72
+ )
73
+ }
74
+ // the snapshot is taken before the production bundle is fetched, so the box
75
+ // gets the state the app was in when the button was pressed rather than the
76
+ // state it drifted to while Metro rebuilt.
77
+ const dumped: unknown = await options.send({
78
+ type: 'storageSnapshot',
79
+ simId: options.simId,
80
+ storageSnapshotOptions: { includeCredentials: options.carryAuth },
81
+ })
82
+ if (!isStorageSnapshot(dumped)) {
83
+ throw new Error('the simulator returned a storage snapshot this build cannot read')
84
+ }
85
+ const storage: StorageSnapshot = dumped
86
+ const created = await createCloudBox({
87
+ bundlePath: toRNXProductionBundleUrl(bundleUrl),
88
+ assetsPath: options.assetsPath ?? undefined,
89
+ device:
90
+ options.device ??
91
+ cloudDevice(isRecord(captured.deviceSpec) ? captured.deviceSpec.model : null),
92
+ authorization: options.authorization,
93
+ accountId: options.accountId,
94
+ apiOrigin: options.apiOrigin,
95
+ boxName: options.boxName ?? null,
96
+ storage,
97
+ })
98
+ const simId = created.receipt.simulator.simId
99
+ const url = `${created.session.apiOrigin.replace(/\/$/, '')}/sim/${simId}`
100
+ // the local terminal is where an agent driving this checkout reads results,
101
+ // so the id and the URL land there whichever caller asked for the send.
102
+ console.log(` box simulator: ${simId}`)
103
+ console.log(` ${url}`)
104
+ for (const warning of created.warnings) console.warn(` warning: ${warning}`)
105
+ return {
106
+ boxId: created.receipt.boxId,
107
+ simId,
108
+ url,
109
+ carriedAuth: options.carryAuth,
110
+ warnings: created.warnings,
111
+ }
112
+ }
package/cli/shell-init.ts CHANGED
@@ -30,7 +30,7 @@ export function renderRnxShellInit(shell: string): string {
30
30
  claim|close) ;;
31
31
  ios)
32
32
  for _rnx_shell_session in "$@"; do
33
- if [ "$_rnx_shell_session" = '--nano' ] || [ "$_rnx_shell_session" = '--cloud' ]; then
33
+ if [ "$_rnx_shell_session" = '--remote' ]; then
34
34
  _rnx_shell_create=1
35
35
  break
36
36
  fi
@@ -48,15 +48,15 @@ export function renderRnxShellInit(shell: string): string {
48
48
 
49
49
  if { [ -n "\${BASH_VERSION:-}" ] && [ "\${BASH_SUBSHELL:-0}" -ne 0 ]; } ||
50
50
  { [ -n "\${ZSH_VERSION:-}" ] && [ "\${ZSH_SUBSHELL:-0}" -ne 0 ]; }; then
51
- printf '%s\\n' 'rnx cloud lifecycle commands must run directly in the current shell, not in a pipeline or command substitution' >&2
51
+ printf '%s\\n' 'rnx remote simulator lifecycle commands must run directly in the current shell, not in a pipeline or command substitution' >&2
52
52
  return 1
53
53
  fi
54
54
  if ( : >&9 ) 2>/dev/null; then
55
- printf '%s\\n' 'rnx cloud shell integration requires file descriptor 9 to be unused' >&2
55
+ printf '%s\\n' 'rnx remote simulator shell integration requires file descriptor 9 to be unused' >&2
56
56
  return 1
57
57
  fi
58
58
  if ! ( export ${RNX_CLOUD_SESSION_ENV}=__rnx_shell_probe && unset ${RNX_CLOUD_SESSION_ENV} ) 2>/dev/null; then
59
- printf '%s\\n' 'rnx cloud shell integration cannot update RNX_CLOUD_SESSION in this shell' >&2
59
+ printf '%s\\n' 'rnx remote simulator shell integration cannot update RNX_CLOUD_SESSION in this shell' >&2
60
60
  return 1
61
61
  fi
62
62
 
package/cli/ws-bridge.ts CHANGED
@@ -248,6 +248,35 @@ export function bridgePortForShellBaseUrl(baseUrl: string): number | null {
248
248
  return null
249
249
  }
250
250
 
251
+ /** the bridge a `--base-url` pin implies. one rule for every command that takes
252
+ * the flag, because they used to disagree: flow refused an unserved pin while
253
+ * open kept the discovered default and then started a bridge of its own, so
254
+ * the page rendered in the pinned shell and the polling waited on another
255
+ * world's bridge. that reads as "saved sim <id> is gone" and gets retried
256
+ * rather than fixed. an explicit --port states the world outright and wins;
257
+ * otherwise the pin has to name a world something serves. */
258
+ export function resolveBridgePortForPin(pin: {
259
+ baseUrl: string
260
+ wsPort: number
261
+ explicitPort: boolean
262
+ }): { port: number; error?: undefined } | { port?: undefined; error: string } {
263
+ const served = bridgePortForShellBaseUrl(pin.baseUrl)
264
+ if (served === null) {
265
+ if (pin.explicitPort) return { port: pin.wsPort }
266
+ return {
267
+ error:
268
+ `no live rnx world serves ${pin.baseUrl} — start that shell first, ` +
269
+ 'or name its bridge with --port <port>',
270
+ }
271
+ }
272
+ if (pin.explicitPort && pin.wsPort !== served) {
273
+ return {
274
+ error: `--base-url ${pin.baseUrl} belongs to bridge port ${served}, but --port ${pin.wsPort} names another world`,
275
+ }
276
+ }
277
+ return { port: served }
278
+ }
279
+
251
280
  /** default ws bridge port for CLI commands: the fresh dev bridge when a vite
252
281
  * shell is live, else the fresh daemon, else the hardcoded default. one
253
282
  * deterministic answer regardless of how the CLI was invoked (global binary
@@ -390,76 +419,94 @@ export interface ParsedBridgeCliArgs {
390
419
  commandTimeoutMs: number
391
420
  }
392
421
 
422
+ interface BridgeFlagState {
423
+ wsPort: number
424
+ explicitPort: boolean
425
+ commandTimeoutMs: number
426
+ simId: string | undefined
427
+ simIdSource: ParsedBridgeCliArgs['simIdSource']
428
+ }
429
+
430
+ // every flag parseBridgeCliArgs consumes a value for on its own, without being
431
+ // listed in a caller's stripValueFlags. the parser iterates this table, so the
432
+ // exported name list below cannot drift from what is actually parsed. a name
433
+ // missing from that list is not a parse failure, it is a command that hunts a
434
+ // positional in raw argv and takes the flag's value instead: `maestro test
435
+ // --newflag value login.yaml` would run `value` as the flow.
436
+ const BRIDGE_VALUE_FLAGS_TABLE: readonly {
437
+ names: readonly string[]
438
+ apply: (state: BridgeFlagState, value: string | undefined) => void
439
+ }[] = [
440
+ {
441
+ names: ['--port', '-p'],
442
+ // naming the flag pins the world even when the value is missing or junk,
443
+ // so a later --base-url cannot quietly retarget the run to another shell.
444
+ apply: (state, value) => {
445
+ state.explicitPort = true
446
+ if (value !== undefined) state.wsPort = Number(value)
447
+ },
448
+ },
449
+ {
450
+ names: ['--timeout'],
451
+ apply: (state, value) => {
452
+ if (value !== undefined) state.commandTimeoutMs = Number(value)
453
+ },
454
+ },
455
+ {
456
+ // `--sim` is the canonical target flag; `--session` / `--tab` are
457
+ // documented aliases (CLAUDE.md, the rnx agent skills, and engine docs all
458
+ // say `--session <id>`). without these aliases an agent following the docs
459
+ // would have `--session ef` silently ignored and get the *currently
460
+ // pinned* sim's tree instead — wrong-app data with no error.
461
+ names: ['--sim', '--session', '--tab'],
462
+ apply: (state, value) => {
463
+ if (value === undefined) return
464
+ state.simId = value.trim() || undefined
465
+ state.simIdSource = 'flag'
466
+ },
467
+ },
468
+ ]
469
+
470
+ export const BRIDGE_VALUE_FLAGS: readonly string[] = BRIDGE_VALUE_FLAGS_TABLE.flatMap(
471
+ (flag) => [...flag.names],
472
+ )
473
+
393
474
  export function parseBridgeCliArgs(
394
475
  args: string[],
395
476
  opts: ParseBridgeCliArgsOptions = {},
396
477
  ): ParsedBridgeCliArgs {
397
478
  const flagIndices = new Set<number>()
398
- let wsPort = opts.port ?? resolveDefaultBridgePort()
399
- let explicitPort = opts.port !== undefined
400
- let commandTimeoutMs = opts.commandTimeoutMs ?? 15000
401
- let simId: string | undefined
402
- let simIdSource: ParsedBridgeCliArgs['simIdSource'] = 'none'
479
+ const state: BridgeFlagState = {
480
+ wsPort: opts.port ?? resolveDefaultBridgePort(),
481
+ explicitPort: opts.port !== undefined,
482
+ commandTimeoutMs: opts.commandTimeoutMs ?? 15000,
483
+ simId: undefined,
484
+ simIdSource: 'none',
485
+ }
403
486
 
404
487
  const stripBooleanFlags = new Set(opts.stripBooleanFlags ?? [])
405
488
  const stripValueFlags = new Set(opts.stripValueFlags ?? [])
406
489
 
407
490
  for (let i = 0; i < args.length; i++) {
408
491
  const arg = args[i]
409
- if (arg === '--port' || arg === '-p') {
492
+ // both spellings for every flag in the table: `--flag value` and
493
+ // `--flag=value`.
494
+ const valueFlag = BRIDGE_VALUE_FLAGS_TABLE.find((flag) =>
495
+ flag.names.some((name) => arg === name || arg.startsWith(`${name}=`)),
496
+ )
497
+ if (valueFlag) {
410
498
  flagIndices.add(i)
411
- explicitPort = true
412
- if (i + 1 < args.length) {
413
- flagIndices.add(i + 1)
414
- wsPort = Number(args[i + 1])
499
+ const inline = valueFlag.names.find((name) => arg.startsWith(`${name}=`))
500
+ if (inline) {
501
+ valueFlag.apply(state, arg.slice(inline.length + 1))
502
+ continue
415
503
  }
504
+ const value = i + 1 < args.length ? args[i + 1] : undefined
505
+ if (value !== undefined) flagIndices.add(i + 1)
506
+ valueFlag.apply(state, value)
416
507
  i++
417
508
  continue
418
509
  }
419
- if (arg.startsWith('--port=')) {
420
- flagIndices.add(i)
421
- explicitPort = true
422
- wsPort = Number(arg.slice('--port='.length))
423
- continue
424
- }
425
- if (arg === '--timeout') {
426
- flagIndices.add(i)
427
- if (i + 1 < args.length) {
428
- flagIndices.add(i + 1)
429
- commandTimeoutMs = Number(args[i + 1])
430
- }
431
- i++
432
- continue
433
- }
434
- // `--sim` is the canonical target flag; `--session` / `--tab` are
435
- // documented aliases (CLAUDE.md, the rnx agent skills, and engine docs all
436
- // say `--session <id>`). without these aliases an agent following the docs
437
- // would have `--session ef` silently ignored and get the *currently
438
- // pinned* sim's tree instead — wrong-app data with no error. accept the
439
- // `--flag value` and `--flag=value` forms for all three spellings.
440
- if (arg === '--sim' || arg === '--session' || arg === '--tab') {
441
- flagIndices.add(i)
442
- if (i + 1 < args.length) {
443
- flagIndices.add(i + 1)
444
- simId = args[i + 1]?.trim() || undefined
445
- simIdSource = 'flag'
446
- }
447
- i++
448
- continue
449
- }
450
- const simEq = arg.startsWith('--sim=')
451
- ? arg.slice('--sim='.length)
452
- : arg.startsWith('--session=')
453
- ? arg.slice('--session='.length)
454
- : arg.startsWith('--tab=')
455
- ? arg.slice('--tab='.length)
456
- : undefined
457
- if (simEq !== undefined) {
458
- flagIndices.add(i)
459
- simId = simEq.trim() || undefined
460
- simIdSource = 'flag'
461
- continue
462
- }
463
510
  if (stripBooleanFlags.has(arg)) {
464
511
  flagIndices.add(i)
465
512
  continue
@@ -473,21 +520,21 @@ export function parseBridgeCliArgs(
473
520
  }
474
521
  }
475
522
 
476
- if (!simId) {
523
+ if (!state.simId) {
477
524
  const savedSim = readCurrentSim()
478
525
  if (savedSim) {
479
- simId = savedSim.simId.trim() || undefined
480
- simIdSource = 'saved'
526
+ state.simId = savedSim.simId.trim() || undefined
527
+ state.simIdSource = 'saved'
481
528
  }
482
529
  }
483
530
 
484
531
  return {
485
532
  positional: args.filter((_, i) => !flagIndices.has(i)),
486
- wsPort,
487
- explicitPort,
488
- simId,
489
- simIdSource,
490
- commandTimeoutMs,
533
+ wsPort: state.wsPort,
534
+ explicitPort: state.explicitPort,
535
+ simId: state.simId,
536
+ simIdSource: state.simIdSource,
537
+ commandTimeoutMs: state.commandTimeoutMs,
491
538
  }
492
539
  }
493
540
 
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.432 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.432 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.432 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.432 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.432 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.432 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -36,6 +36,7 @@ __export(shared_session_exports, {
36
36
  getSharedDesktopAuthFilePath: () => getSharedDesktopAuthFilePath,
37
37
  readSharedDesktopAuthSession: () => readSharedDesktopAuthSession,
38
38
  refreshSharedDesktopAuthSession: () => refreshSharedDesktopAuthSession,
39
+ sharedDesktopAuthAccounts: () => sharedDesktopAuthAccounts,
39
40
  writeSharedDesktopAuthSession: () => writeSharedDesktopAuthSession
40
41
  });
41
42
  module.exports = __toCommonJS(shared_session_exports);
@@ -60,6 +61,12 @@ function electronUserDataDir() {
60
61
  var DAEMON_LOCKFILE_MAX_BYTES = 16 * 1024;
61
62
 
62
63
  // src/auth/shared-session.ts
64
+ function sharedDesktopAuthAccounts(user) {
65
+ return [
66
+ ...user?.personalAccountId ? [{ id: user.personalAccountId, name: "Personal" }] : [],
67
+ ...(user?.teams ?? []).map((team) => ({ id: team.id, name: team.name }))
68
+ ];
69
+ }
63
70
  var SESSION_VERSION = 1;
64
71
  var SESSION_FILE_ENV = "SOOTSIM_SHARED_AUTH_FILE";
65
72
  var DEFAULT_ORIGIN = "https://contrast.dev";
@@ -85,6 +92,7 @@ function normalizeUser(input) {
85
92
  email: typeof value.email === "string" ? value.email : void 0,
86
93
  image: typeof value.image === "string" ? value.image : void 0,
87
94
  githubUsername: typeof value.githubUsername === "string" ? value.githubUsername : void 0,
95
+ personalAccountId: typeof value.personalAccountId === "string" && value.personalAccountId.trim() ? value.personalAccountId.trim() : void 0,
88
96
  isAdmin: value.isAdmin === true,
89
97
  teams: Array.isArray(value.teams) ? value.teams.map((team) => {
90
98
  if (!team || typeof team !== "object") return null;
@@ -108,6 +116,7 @@ function normalizeSession(input) {
108
116
  const source = value.source === "cli" || value.source === "electron" || value.source === "browser" || value.source === "unknown" ? value.source : "unknown";
109
117
  const updatedAt = typeof value.updatedAt === "string" && value.updatedAt ? value.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
110
118
  const validatedAt = typeof value.validatedAt === "string" && value.validatedAt ? value.validatedAt : void 0;
119
+ const accountId = typeof value.accountId === "string" && value.accountId.trim() ? value.accountId.trim() : void 0;
111
120
  return {
112
121
  version: SESSION_VERSION,
113
122
  token: value.token.trim(),
@@ -115,7 +124,8 @@ function normalizeSession(input) {
115
124
  origin,
116
125
  source,
117
126
  updatedAt,
118
- validatedAt
127
+ validatedAt,
128
+ accountId
119
129
  };
120
130
  }
121
131
  function readSharedDesktopAuthSession() {
@@ -144,7 +154,8 @@ function writeSharedDesktopAuthSession(session) {
144
154
  origin: session.origin?.trim() || DEFAULT_ORIGIN,
145
155
  source: session.source,
146
156
  updatedAt: session.updatedAt || (/* @__PURE__ */ new Date()).toISOString(),
147
- validatedAt: session.validatedAt
157
+ validatedAt: session.validatedAt,
158
+ accountId: session.accountId?.trim() || void 0
148
159
  };
149
160
  (0, import_node_fs.writeFileSync)(filepath, JSON.stringify(normalized, null, 2) + "\n");
150
161
  try {
@@ -179,7 +190,8 @@ async function refreshSharedDesktopAuthSession(originOverride) {
179
190
  user: data.user,
180
191
  origin,
181
192
  source: current.source,
182
- validatedAt: (/* @__PURE__ */ new Date()).toISOString()
193
+ validatedAt: (/* @__PURE__ */ new Date()).toISOString(),
194
+ accountId: current.accountId
183
195
  });
184
196
  } catch {
185
197
  return current;
@@ -191,5 +203,6 @@ async function refreshSharedDesktopAuthSession(originOverride) {
191
203
  getSharedDesktopAuthFilePath,
192
204
  readSharedDesktopAuthSession,
193
205
  refreshSharedDesktopAuthSession,
206
+ sharedDesktopAuthAccounts,
194
207
  writeSharedDesktopAuthSession
195
208
  });
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.432 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
package/dist-lib/beta.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.432 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
package/dist-lib/beta.mjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.432 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/beta.ts
4
4
  var IS_BETA = true;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.432 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.432 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.432 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/bridge-contract.ts
4
4
  var SIM_LONG_PRESS_MAX_MS = 5e3;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.432 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.432 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/bridge-contract.ts
4
4
  var SIM_LONG_PRESS_DEFAULT_MS = 500;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.432 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.432 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/capture-contract.ts
4
4
  var RNX_SCREEN_CAPTURE_VERSION = 1;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.432 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.434 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;