rnxsim 0.1.380 → 0.1.382

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 (138) hide show
  1. package/README.md +43 -26
  2. package/cli/app-fonts.ts +1 -1
  3. package/cli/bin.ts +118 -26
  4. package/cli/bridge-diagnostics.ts +1 -1
  5. package/cli/bridge-flow-runner.ts +9 -9
  6. package/cli/browser-evals.ts +3 -3
  7. package/cli/cli-update.ts +261 -0
  8. package/cli/cloud-client.ts +407 -0
  9. package/cli/cloud-dispatch.ts +165 -0
  10. package/cli/cloud-session.ts +156 -0
  11. package/cli/command-contract.ts +2 -2
  12. package/cli/command-registry.ts +9 -0
  13. package/cli/commands/agent-wrapper.ts +4 -4
  14. package/cli/commands/agent.ts +3 -3
  15. package/cli/commands/box/checkout-plane.ts +16 -8
  16. package/cli/commands/box/rnx-command.ts +2 -2
  17. package/cli/commands/box/search-command.ts +2 -2
  18. package/cli/commands/box.ts +123 -70
  19. package/cli/commands/config.ts +3 -2
  20. package/cli/commands/control.ts +6 -6
  21. package/cli/commands/cpu-profile.ts +2 -2
  22. package/cli/commands/daemon.ts +7 -7
  23. package/cli/commands/debug.ts +2 -2
  24. package/cli/commands/detox.ts +7 -7
  25. package/cli/commands/diagnose.ts +1 -1
  26. package/cli/commands/electron.ts +1 -1
  27. package/cli/commands/film.ts +1 -1
  28. package/cli/commands/flow.ts +4 -4
  29. package/cli/commands/inspect/core.ts +58 -7
  30. package/cli/commands/inspect/env.ts +5 -3
  31. package/cli/commands/inspect/shared.ts +2 -1
  32. package/cli/commands/inspect/wait-event.ts +1 -1
  33. package/cli/commands/inspect/wait-ready.ts +15 -5
  34. package/cli/commands/inspect.ts +31 -9
  35. package/cli/commands/install-desktop.ts +2 -2
  36. package/cli/commands/maestro.ts +3 -3
  37. package/cli/commands/permissions.ts +1 -1
  38. package/cli/commands/platform.ts +91 -0
  39. package/cli/commands/record.ts +3 -3
  40. package/cli/commands/report-issue.ts +1 -1
  41. package/cli/commands/runtime.ts +1 -1
  42. package/cli/commands/screenshot-mode.ts +1 -1
  43. package/cli/commands/screenshot.ts +25 -0
  44. package/cli/commands/serve.ts +1 -1
  45. package/cli/commands/skills.ts +1 -1
  46. package/cli/commands/timeline.ts +1 -1
  47. package/cli/commands/upgrade.ts +8 -34
  48. package/cli/commands/upload.ts +3 -3
  49. package/cli/commands/version.ts +14 -3
  50. package/cli/commands/what-happened.ts +2 -2
  51. package/cli/current-sim.ts +43 -30
  52. package/cli/desktop-companion.ts +1 -1
  53. package/cli/drivers/electron.ts +1 -1
  54. package/cli/drivers/playwright.ts +3 -3
  55. package/cli/drivers/types.ts +4 -4
  56. package/cli/flow-file.ts +3 -3
  57. package/cli/help-core.ts +18 -11
  58. package/cli/hidden-runtime-alias.ts +1 -1
  59. package/cli/hints.ts +2 -2
  60. package/cli/maestro-js.ts +2 -2
  61. package/cli/outbound-endpoints.ts +40 -6
  62. package/cli/parse-args.ts +1 -1
  63. package/cli/registry.ts +1 -1
  64. package/cli/resolve-assets.ts +2 -2
  65. package/cli/secure-file.ts +31 -0
  66. package/cli/shell-init.ts +98 -0
  67. package/cli/ws-bridge.ts +18 -8
  68. package/detox/expectations.ts +1 -1
  69. package/detox/gestures.ts +3 -3
  70. package/detox/index.ts +3 -3
  71. package/dist-lib/agent-daemon-client.cjs +1 -1
  72. package/dist-lib/agent-events.cjs +1 -1
  73. package/dist-lib/agent-identity.cjs +1 -1
  74. package/dist-lib/agent-sessions.cjs +1 -1
  75. package/dist-lib/attached-projects.cjs +1 -1
  76. package/dist-lib/auth/shared-session.cjs +1 -1
  77. package/dist-lib/backend-origin.cjs +1 -1
  78. package/dist-lib/beta.cjs +1 -1
  79. package/dist-lib/beta.mjs +1 -1
  80. package/dist-lib/bridge-constants.cjs +1 -1
  81. package/dist-lib/bridge-contract-input.cjs +15 -8
  82. package/dist-lib/bridge-contract-input.mjs +15 -8
  83. package/dist-lib/bridge-contract.cjs +1 -1
  84. package/dist-lib/bridge-contract.mjs +1 -1
  85. package/dist-lib/capture-contract.cjs +107 -0
  86. package/dist-lib/capture-contract.mjs +80 -0
  87. package/dist-lib/cli-constants.cjs +1 -1
  88. package/dist-lib/cloud-contract.cjs +32 -0
  89. package/dist-lib/cloud-contract.mjs +7 -0
  90. package/dist-lib/config.cjs +1 -1
  91. package/dist-lib/detox/index.cjs +4 -4
  92. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  93. package/dist-lib/home-paths.cjs +9 -1
  94. package/dist-lib/host/bridge-host.cjs +15 -10
  95. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  96. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  97. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  98. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  99. package/dist-lib/host/websocket-proxy.cjs +1 -1
  100. package/dist-lib/index.cjs +248 -73
  101. package/dist-lib/jump-to-source-babel.cjs +1 -1
  102. package/dist-lib/menu.cjs +1 -1
  103. package/dist-lib/menu.mjs +1 -1
  104. package/dist-lib/metro-production-bundle.cjs +1 -1
  105. package/dist-lib/metro-production-bundle.mjs +1 -1
  106. package/dist-lib/metro.cjs +1 -1
  107. package/dist-lib/profiles.cjs +1 -1
  108. package/dist-lib/public-brand.cjs +1 -1
  109. package/dist-lib/react-native-host-modules.cjs +1 -1
  110. package/dist-lib/react-native-host-modules.mjs +1 -1
  111. package/dist-lib/render-mode.cjs +1 -1
  112. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  113. package/dist-lib/sdk.cjs +44 -6
  114. package/dist-lib/sdk.mjs +44 -6
  115. package/dist-lib/skills.cjs +435 -155
  116. package/dist-lib/vite.cjs +1 -1
  117. package/package.json +21 -5
  118. package/scripts/dev-server-scanner.ts +3 -3
  119. package/skills/contrast/SKILL.md +1 -1
  120. package/skills/rnx-perf/SKILL.md +68 -52
  121. package/src/app-url.ts +2 -3
  122. package/src/bridge-contract-input.ts +17 -6
  123. package/src/bridge-contract.ts +25 -4
  124. package/src/capture-contract.ts +423 -0
  125. package/src/cloud-contract.ts +15 -0
  126. package/src/home-paths.ts +9 -3
  127. package/src/host/agent-host.ts +1 -1
  128. package/src/host/bridge-host.ts +13 -11
  129. package/src/host/plane-host.ts +2 -2
  130. package/src/index.ts +21 -0
  131. package/src/native-dev-bundle-url.ts +2 -2
  132. package/src/screenshots/orchestrate.ts +1 -1
  133. package/src/screenshots/org-deck.ts +1 -1
  134. package/src/settings-bundle.ts +1 -1
  135. package/src/sim-client.ts +4 -0
  136. package/src/vite-plugin-one.ts +2 -2
  137. package/src/vite-plugin.ts +1 -1
  138. package/cli/commands/box/cloud-box.ts +0 -294
@@ -1,4 +1,4 @@
1
- // transport-agnostic sootsim inspect-verb kernels.
1
+ // transport-agnostic rnx inspect-verb kernels.
2
2
  //
3
3
  // each function here sends the smallest bridge command its verb needs and
4
4
  // returns *structured data* — no arg parsing or stdout. the
@@ -29,7 +29,7 @@ import type {
29
29
  SootSimTimelineQuery,
30
30
  SootSimTimelineQueryResult,
31
31
  SootSimTimelineSummary,
32
- } from '@sootsim/globals'
32
+ } from '@rnx/globals'
33
33
 
34
34
  // resolve a `--max-ms <n>` flag, tolerating the spellings agents and humans
35
35
  // actually type (`--maxMs`, `--maxms`, `--max_ms`). a silently-ignored
@@ -48,8 +48,9 @@ export function resolveMaxMsFlag(args: string[], fallbackMs: number): number {
48
48
  }
49
49
 
50
50
  // the minimal bridge an inspect kernel needs: a JSON command channel into a
51
- // sootsim instance.
51
+ // rnx instance.
52
52
  export interface InspectBridge {
53
+ readonly plane?: 'local' | 'cloud'
53
54
  send(
54
55
  cmd: { type: string; [k: string]: any },
55
56
  opts?: { timeoutMs?: number },
@@ -285,7 +286,7 @@ export async function inspectDescribe(
285
286
  // fields with `styling:true` / `--styling`.
286
287
  //
287
288
  // the SAME LayoutElement shape is produced by both preview surfaces:
288
- // - native/sootsim — `LAYOUT_GET_NATIVE_EVAL` reads the engine node tree via
289
+ // - native/rnx — `LAYOUT_GET_NATIVE_EVAL` reads the engine node tree via
289
290
  // __sootsimTest.listInspectable() (the source `describe` uses), where each
290
291
  // SootSimInspectInfo already carries absolute layout and optional style /
291
292
  // box-model data.
@@ -703,7 +704,7 @@ export function layoutGetNativeEval(opts: LayoutExtractionOptions = {}): string
703
704
  export const LAYOUT_GET_NATIVE_EVAL = layoutGetNativeEval()
704
705
 
705
706
  // `get-layout` (native) — bounding boxes for visible meaningful elements on the
706
- // sootsim/native preview. transport-agnostic: the CLI runs it over its WS
707
+ // rnx/native preview. transport-agnostic: the CLI runs it over its WS
707
708
  // bridge, the agent runs it over its in-process preview bridge.
708
709
  export async function inspectGetLayout(
709
710
  bridge: InspectBridge,
@@ -1128,7 +1129,10 @@ export async function inspectWaitSelector(
1128
1129
  {
1129
1130
  type: 'waitFor',
1130
1131
  waitForOptions: {
1131
- condition: { type: gone ? 'absent' : 'present', testID: testId },
1132
+ condition: {
1133
+ type: gone ? 'absent' : 'present',
1134
+ selector: { testID: testId },
1135
+ },
1132
1136
  timeoutMs,
1133
1137
  },
1134
1138
  },
@@ -1352,6 +1356,51 @@ export interface LogFilterOptions {
1352
1356
  // not installed yet, return an empty snapshot so validation tools don't turn a
1353
1357
  // missing diagnostics buffer into the app failure being diagnosed.
1354
1358
  export async function inspectLogs(bridge: InspectBridge): Promise<LogEntry[]> {
1359
+ if (bridge.plane === 'cloud') {
1360
+ const state: unknown = await bridge.send({
1361
+ type: 'state',
1362
+ stateOptions: {
1363
+ screenshot: false,
1364
+ tree: false,
1365
+ route: false,
1366
+ errors: false,
1367
+ logs: true,
1368
+ logLimit: 100,
1369
+ },
1370
+ })
1371
+ const recentLogs =
1372
+ state &&
1373
+ typeof state === 'object' &&
1374
+ Array.isArray(Reflect.get(state, 'recentLogs'))
1375
+ ? Reflect.get(state, 'recentLogs')
1376
+ : []
1377
+ return recentLogs.flatMap((entry: unknown, index: number) => {
1378
+ if (!entry || typeof entry !== 'object') return []
1379
+ const text = Reflect.get(entry, 'text')
1380
+ const rawLevel = Reflect.get(entry, 'level')
1381
+ if (typeof text !== 'string') return []
1382
+ const level: LogLevel =
1383
+ rawLevel === 'log' ||
1384
+ rawLevel === 'info' ||
1385
+ rawLevel === 'warn' ||
1386
+ rawLevel === 'error' ||
1387
+ rawLevel === 'debug'
1388
+ ? rawLevel
1389
+ : 'error'
1390
+ const at = Reflect.get(entry, 'at')
1391
+ const ts = typeof at === 'number' && Number.isFinite(at) ? at : 0
1392
+ return [
1393
+ {
1394
+ id: `cloud-${ts}-${index}`,
1395
+ source: 'rnx-cloud',
1396
+ level,
1397
+ ts,
1398
+ args: [text],
1399
+ stack: null,
1400
+ },
1401
+ ]
1402
+ })
1403
+ }
1355
1404
  const res = (await bridge.send({
1356
1405
  type: 'evaluate',
1357
1406
  code: `(() => {
@@ -1402,7 +1451,9 @@ export function filterLogEntries(
1402
1451
  if (!opts.showInternal) {
1403
1452
  out = out.filter((e) => {
1404
1453
  const first = e.args[0]
1405
- return !(typeof first === 'string' && first.startsWith('[rnx]'))
1454
+ return !(
1455
+ typeof first === 'string' && /^\[(?:rnx|sootsim)(?:\s[^\]]*)?\]/.test(first)
1456
+ )
1406
1457
  })
1407
1458
  }
1408
1459
  if (opts.level) out = out.filter((e) => opts.level!.has(e.level))
@@ -13,10 +13,12 @@ const AGENT_ENV_VARS = [
13
13
  'AIDER_MODEL',
14
14
  ]
15
15
 
16
- export function isAgentEnv(): boolean {
17
- if (process.env.RNX_AGENT === '0') return false
16
+ export function isAgentEnv(
17
+ env: Readonly<Record<string, string | undefined>> = process.env,
18
+ ): boolean {
19
+ if (env.RNX_AGENT === '0') return false
18
20
  for (const name of AGENT_ENV_VARS) {
19
- const v = process.env[name]
21
+ const v = env[name]
20
22
  if (v && v.trim() && v !== '0') return true
21
23
  }
22
24
  return false
@@ -12,7 +12,7 @@ import {
12
12
  type WsBridge,
13
13
  } from '../../ws-bridge'
14
14
  import { getShellState, inspectWaitReady, isShellCommandUnavailable } from './core'
15
- import type { SootSimScreenTransitionWaitResult } from '@sootsim/globals'
15
+ import type { SootSimScreenTransitionWaitResult } from '@rnx/globals'
16
16
 
17
17
  // shell-state helpers live in the transport-agnostic kernel module so the CLI
18
18
  // and the in-browser agent share one implementation. re-exported here so
@@ -93,6 +93,7 @@ export async function maybeWaitForStartedScreenTransitions(
93
93
  bridge: WsBridge,
94
94
  opts: { verbose?: boolean } = {},
95
95
  ) {
96
+ if (bridge.plane === 'cloud') return
96
97
  try {
97
98
  const result = await callTestBridge<SootSimScreenTransitionWaitResult>(
98
99
  bridge,
@@ -26,7 +26,7 @@ import type {
26
26
  SootSimTimelineEvent,
27
27
  SootSimTimelineQuery,
28
28
  SootSimTimelineQueryResult,
29
- } from '@sootsim/globals'
29
+ } from '@rnx/globals'
30
30
 
31
31
  interface WaitEventOptions {
32
32
  bridge: WsBridge
@@ -33,13 +33,23 @@ export async function runWaitReadySubcommand(opts: {
33
33
  errors,
34
34
  bridgeError,
35
35
  } = await inspectWaitReady(bridge, timeoutMs, {
36
- onProgress(status) {
37
- console.error(
38
- ` still waiting after ${status.elapsedMs}ms — ${waitReadyReason(status)} (nodes: ${status.nodes}, targets: ${status.targets}, live surfaces: ${status.liveFrameActive ? status.liveFrameChannels : 0}, errors: ${status.errors})`,
39
- )
40
- },
36
+ ...(bridge.plane !== 'cloud'
37
+ ? {
38
+ onProgress(status) {
39
+ console.error(
40
+ ` still waiting after ${status.elapsedMs}ms — ${waitReadyReason(status)} (nodes: ${status.nodes}, targets: ${status.targets}, live surfaces: ${status.liveFrameActive ? status.liveFrameChannels : 0}, errors: ${status.errors})`,
41
+ )
42
+ },
43
+ }
44
+ : {}),
41
45
  })
42
46
  if (ready) {
47
+ if (bridge.plane === 'cloud') {
48
+ console.log(
49
+ ` ready in ${elapsedMs}ms: ${nodes} nodes, ${targets} targets${liveFrameActive ? `, ${liveFrameChannels} live surfaces` : ''}`,
50
+ )
51
+ return
52
+ }
43
53
  const remainingMs = timeoutMs - elapsedMs
44
54
  const idleBudgetMs = Math.max(100, Math.min(10_000, remainingMs))
45
55
  const idle = await waitForSootsimIdle({
@@ -1,8 +1,8 @@
1
- // sootsim inspect — dump node tree, accessibility info, query nodes
2
- // connects to the sootsim WS bridge (no Playwright)
1
+ // rnx inspect — dump node tree, accessibility info, query nodes
2
+ // connects to the rnx WS bridge (no Playwright)
3
3
 
4
- import { getCliInspectVerbMeta } from '@sootsim/skills/cli'
5
- import { renderCliCommandHelp, renderCliGroupHelp } from '@sootsim/skills/cli/renderers'
4
+ import { getCliInspectVerbMeta } from '@rnx/skills/cli'
5
+ import { renderCliCommandHelp, renderCliGroupHelp } from '@rnx/skills/cli/renderers'
6
6
  import { DEFAULT_SOOTSIM_BRIDGE_PORT } from '../../src/bridge-constants'
7
7
  import { DEFAULT_SOOTSIM_SHELL_URL } from '../../src/cli-constants'
8
8
  import { resetGuestAppStateEval } from '../app-state-reset'
@@ -105,7 +105,7 @@ import type {
105
105
  SootSimRequestEntry,
106
106
  SootSimScreenTransitionWaitResult,
107
107
  SootSimScrollPerformanceTrace,
108
- } from '@sootsim/globals'
108
+ } from '@rnx/globals'
109
109
 
110
110
  interface InspectOptions {
111
111
  port?: number
@@ -1112,7 +1112,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
1112
1112
  ` usage: ${inspectCommand(name)}${tail ? ` ${tail}` : ''}`
1113
1113
 
1114
1114
  if (!subcommand || args.includes('--help') || args.includes('-h')) {
1115
- // all help output comes from the registry in packages/sootsim-skills
1115
+ // all help output comes from the registry in packages/rnx-skills
1116
1116
  // so `rnx do --help`, `rnx get --help`, the website docs, and
1117
1117
  // the Contrast skill markdown stay in lock-step. a drift check in
1118
1118
  // test/sootsimCliRegistry.test.ts fails CI if a verb lands without a
@@ -1183,7 +1183,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
1183
1183
  }
1184
1184
 
1185
1185
  // `rnx list --drivers` — pure local registry view, no bridge needed.
1186
- // short-circuit here so users can discover drivers even when no sootsim
1186
+ // short-circuit here so users can discover drivers even when no rnx
1187
1187
  // session is running.
1188
1188
  if (
1189
1189
  subcommand === 'list' &&
@@ -1840,6 +1840,28 @@ export async function runInspect(args: string[], opts: InspectOptions) {
1840
1840
  console.error(' resolves testID, id, then text — dumps full node info as JSON')
1841
1841
  rnxExit(1)
1842
1842
  }
1843
+ if (bridge.plane === 'cloud') {
1844
+ const byTestId = await inspectFind(bridge, { testId: matcher })
1845
+ const byText = byTestId?.result
1846
+ ? null
1847
+ : await inspectFind(bridge, { text: matcher })
1848
+ const node = byTestId?.result ?? byText?.result ?? null
1849
+ console.log(
1850
+ JSON.stringify(
1851
+ node
1852
+ ? {
1853
+ matcher,
1854
+ found: true,
1855
+ resolvedVia: byTestId?.result ? 'testID' : 'text',
1856
+ node,
1857
+ }
1858
+ : { matcher, found: false },
1859
+ null,
1860
+ 2,
1861
+ ),
1862
+ )
1863
+ break
1864
+ }
1843
1865
  // resolve in order: testID → id → text. first hit wins, includes the
1844
1866
  // resolved transform and a parent chain so agents can skip the usual
1845
1867
  // __sootsimRoot walk.
@@ -3607,7 +3629,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3607
3629
  if (!code) {
3608
3630
  console.error(inspectUsage('js', '<javascript>'))
3609
3631
  console.error('')
3610
- console.error(' runs the snippet in the engine realm. SootSim is the')
3632
+ console.error(' runs the snippet in the engine realm. rnx is the')
3611
3633
  console.error(' canonical state surface — reach into it directly.')
3612
3634
  console.error('')
3613
3635
  console.error(' examples:')
@@ -3679,7 +3701,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3679
3701
  }
3680
3702
 
3681
3703
  case 'globals': {
3682
- // show all available sootsim globals and their methods
3704
+ // show all available rnx globals and their methods
3683
3705
  const info = await bridge.send({
3684
3706
  type: 'evaluate',
3685
3707
  code: `(async () => {
@@ -1,6 +1,6 @@
1
- // sootsim desktop install — download and install the optional desktop GUI
1
+ // rnx desktop install — download and install the optional desktop GUI
2
2
  //
3
- // the CLI + daemon are the canonical sootsim surface; this command is only
3
+ // the CLI + daemon are the canonical rnx surface; this command is only
4
4
  // for users who want the Electron-based GUI companion on top of that.
5
5
  //
6
6
  // fetches the right platform artifact from https://contrast.dev/api/electron-release/
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // users with an existing `.maestro/` directory can swap `maestro test`
4
4
  // for `rnx maestro test` and have their flows execute against
5
- // sootsim's in-browser engine. the heavy lifting lives in `flow.ts` +
5
+ // rnx's in-browser engine. the heavy lifting lives in `flow.ts` +
6
6
  // `bridge-flow-runner.ts`; this file just re-maps the maestro argv shape
7
7
  // onto the existing flow runner and handles directory iteration + init.
8
8
 
@@ -102,7 +102,7 @@ partial:
102
102
  clearKeychain — warn-only, rnx has no keychain surface.
103
103
  when.platform — rnx reports iOS for this runner.
104
104
  openLink: app routing works; no OS-level Safari fallback.
105
- launchApp.resetRuntime — sootsim-only extension for recorded benchmarks:
105
+ launchApp.resetRuntime — rnx-only extension for recorded benchmarks:
106
106
  keep tenant storage/auth, but soft-reload the guest app inside launchApp.
107
107
  takeScreenshot — maestro string form works unchanged; rnx also accepts
108
108
  { path, withFrame } for framed export.
@@ -402,7 +402,7 @@ export async function runMaestro(
402
402
  // one. sending it on as `--url` made it an app target instead, which left
403
403
  // the playback bridge on whichever world resolveBridgeWorld picks by
404
404
  // default (a live Vite dev shell selected from the development registry).
405
- // so `sootsim --port <installed daemon> maestro …` silently ran the flow
405
+ // so `rnx --port <installed daemon> maestro …` silently ran the flow
406
406
  // against the dev engine, then failed trying to resolve a bundle at the
407
407
  // bridge port. an explicit command-level `--port` still wins, since
408
408
  // parseBridgeCliArgs takes the last occurrence.
@@ -10,7 +10,7 @@ import type {
10
10
  SystemPermissionKind,
11
11
  SystemPermissionMutation,
12
12
  SystemPermissionStatus,
13
- } from '@sootsim/globals'
13
+ } from '@rnx/globals'
14
14
 
15
15
  interface PermissionsOptions {
16
16
  port?: number
@@ -15,6 +15,7 @@ import {
15
15
  } from 'sootsim-engine/settings'
16
16
  import { settingsStore } from 'sootsim-engine/settings/store'
17
17
  import { rnxPublicBrand } from '../../src/public-brand'
18
+ import { rethrowIfExit } from '../run-rnx'
18
19
 
19
20
  const PLATFORM_LABEL: Record<RuntimePlatform, string> = {
20
21
  ios: 'iOS',
@@ -25,6 +26,36 @@ function isDeviceModel(value: string): value is DeviceModel {
25
26
  return value in devices
26
27
  }
27
28
 
29
+ function cloudBundleFromArgs(args: string[]): string | null {
30
+ const positional: string[] = []
31
+ for (let index = 0; index < args.length; index++) {
32
+ const arg = args[index]
33
+ if (arg === '--cloud') continue
34
+ if (arg === '--device') {
35
+ index++
36
+ continue
37
+ }
38
+ if (
39
+ arg === '--sim' ||
40
+ arg === '--session' ||
41
+ arg === '--tab' ||
42
+ arg.startsWith('--sim=') ||
43
+ arg.startsWith('--session=') ||
44
+ arg.startsWith('--tab=')
45
+ ) {
46
+ throw new Error('rnx ios --cloud creates a new simulator and cannot target --sim')
47
+ }
48
+ if (arg.startsWith('-')) {
49
+ throw new Error(`rnx ios --cloud does not support ${arg}`)
50
+ }
51
+ positional.push(arg)
52
+ }
53
+ if (positional.length !== 1) {
54
+ throw new Error('usage: rnx ios --cloud <local bundle.js> [--device <model>]')
55
+ }
56
+ return positional[0] ?? null
57
+ }
58
+
28
59
  export async function runPlatformCommand(
29
60
  platform: RuntimePlatform,
30
61
  args: string[],
@@ -34,6 +65,7 @@ export async function runPlatformCommand(
34
65
  // contradiction shows up here as a platform mismatch. refuse it by name
35
66
  // rather than silently overriding whichever one the user meant.
36
67
  const explicitDevice = args.find((_, i) => args[i - 1] === '--device')
68
+ let selectedDevice: DeviceModel
37
69
  if (explicitDevice !== undefined) {
38
70
  if (!isDeviceModel(explicitDevice)) {
39
71
  console.error(
@@ -53,6 +85,7 @@ export async function runPlatformCommand(
53
85
  )
54
86
  return 1
55
87
  }
88
+ selectedDevice = explicitDevice
56
89
  } else {
57
90
  // keep the user's own device when it already belongs to the platform they
58
91
  // asked for, so `rnx ios` after `rnx device set iphone-16` does not
@@ -60,6 +93,64 @@ export async function runPlatformCommand(
60
93
  const current = settingsStore.get('deviceModel')
61
94
  if (getRuntimePlatform(current) !== platform) {
62
95
  settingsStore.apply({ deviceModel: DEFAULT_DEVICE_BY_PLATFORM[platform] })
96
+ selectedDevice = DEFAULT_DEVICE_BY_PLATFORM[platform]
97
+ } else {
98
+ selectedDevice = current
99
+ }
100
+ }
101
+
102
+ if (args.includes('--cloud')) {
103
+ if (platform !== 'ios') {
104
+ console.error(
105
+ ` ${rnxPublicBrand.commandName} android --cloud is not available; RNX Cloud currently supports iOS`,
106
+ )
107
+ return 1
108
+ }
109
+ try {
110
+ const bundlePath = cloudBundleFromArgs(args)
111
+ if (!bundlePath) throw new Error('rnx ios --cloud requires a local bundle.js')
112
+ const {
113
+ publishCloudSessionToShell,
114
+ readCloudSession,
115
+ requireCloudSessionShellUpdate,
116
+ } = await import('../cloud-session')
117
+ if (readCloudSession()) {
118
+ throw new Error(
119
+ 'this shell already has an RNX Cloud simulator; run `rnx close` or create the next simulator in another shell',
120
+ )
121
+ }
122
+ const descriptor = requireCloudSessionShellUpdate()
123
+ const { authHeaderOrExit } = await import('../auth')
124
+ const { closeCloudSimulator, createCloudSimulator } =
125
+ await import('../cloud-client')
126
+ const { header } = authHeaderOrExit('ios --cloud')
127
+ const created = await createCloudSimulator({
128
+ bundlePath,
129
+ device: selectedDevice,
130
+ authorization: header,
131
+ })
132
+ try {
133
+ publishCloudSessionToShell(created.session, descriptor)
134
+ } catch (error) {
135
+ await closeCloudSimulator(created.session)
136
+ throw error
137
+ }
138
+ const receipt = created.receipt
139
+ console.log(` cloud sim: ${receipt.simId}`)
140
+ console.log(` artifact: ${receipt.artifact.id} (${receipt.artifact.bytes} bytes)`)
141
+ console.log(` claim expires: ${receipt.claim.expiresAt}`)
142
+ console.log(
143
+ ' reads: describe, find, get tree|node|count, wait ready|selector, logs, state, screenshot',
144
+ )
145
+ return 0
146
+ } catch (error) {
147
+ rethrowIfExit(error)
148
+ console.error(
149
+ ` ${rnxPublicBrand.commandName} ios --cloud failed: ${
150
+ error instanceof Error ? error.message : String(error)
151
+ }`,
152
+ )
153
+ return 1
63
154
  }
64
155
  }
65
156
 
@@ -1,6 +1,6 @@
1
1
  // rnx record — drives the engine's built-in canvas recorder over
2
2
  // the WS bridge. no playwright, no ffmpeg. encoding happens in the
3
- // running sootsim page (webm via MediaRecorder, mp4 via WebCodecs, gif
3
+ // running rnx page (webm via MediaRecorder, mp4 via WebCodecs, gif
4
4
  // via gifenc, raw png frames via a shared bitmap sampler).
5
5
 
6
6
  import {
@@ -180,7 +180,7 @@ function readRepoScope(args: string[]): RepoScope {
180
180
 
181
181
  export async function runRecord(args: string[], opts: RecordOptions) {
182
182
  // note: `--help` / `-h` never reach here — bin.ts intercepts them and
183
- // renders the registry-driven help page (packages/sootsim-skills meta.ts).
183
+ // renders the registry-driven help page (packages/rnx-skills meta.ts).
184
184
 
185
185
  const sub = findRecordSubcommand(args)
186
186
  if (sub) {
@@ -583,7 +583,7 @@ async function assertRecorderAvailable(bridge: WsBridge) {
583
583
 
584
584
  // frame-capturing recordings (video / gif / png / combined) drive their
585
585
  // encoder off requestAnimationFrame and throttled timers, which browsers
586
- // clamp to ~1fps when the sootsim tab is hidden (backgrounded, or its chrome
586
+ // clamp to ~1fps when the rnx tab is hidden (backgrounded, or its chrome
587
587
  // window minimized). that silently produced a frozen-looking file and is
588
588
  // exactly why agents started pre-checking `document.hidden` by hand. refuse
589
589
  // up front with an actionable message instead. checkSimHealth already emits
@@ -1,4 +1,4 @@
1
- // sootsim report-issue — submit an explicitly approved compatibility report.
1
+ // rnx report-issue — submit an explicitly approved compatibility report.
2
2
 
3
3
  import { existsSync, readFileSync } from 'node:fs'
4
4
  import { resolve } from 'node:path'
@@ -136,7 +136,7 @@ async function activateVersion(version: string) {
136
136
  const notified = await tellDaemonRuntimeChanged(version)
137
137
  if (!notified) {
138
138
  console.log(
139
- ` (no daemon running — next sootsim/electron launch will pick up ${version})`,
139
+ ` (no daemon running — next rnx/electron launch will pick up ${version})`,
140
140
  )
141
141
  }
142
142
  }
@@ -1,5 +1,5 @@
1
1
  // rnx mode screenshot — toggle the "screenshot mode" chrome overlay
2
- // in the running sootsim sim. orthogonal to `--no-shell` on the
2
+ // in the running rnx sim. orthogonal to `--no-shell` on the
3
3
  // `screenshot` command: this hides the DOM browser chrome (rail, menubar,
4
4
  // window controls) while still rendering the iOS shell inside the canvas.
5
5
  //
@@ -13,6 +13,7 @@ import { launchReapedChrome } from '../../../../scripts/lib/reap-browser'
13
13
  import { DEFAULT_SOOTSIM_BRIDGE_PORT } from '../../src/bridge-constants'
14
14
  import { composeFramedScreenshot } from '../../src/screenshots/frame-compose'
15
15
  import { buildResolveRectEval, type SampleRect } from '../browser-evals'
16
+ import { createCloudBridgeForParsed } from '../cloud-client'
16
17
  import { rnxExit } from '../run-rnx'
17
18
  import {
18
19
  callInBridge,
@@ -120,6 +121,30 @@ examples:
120
121
  stripValueFlags: ['--output', '--area', '--id', '--text'],
121
122
  })
122
123
  const outputArg = resolveOutputPath(args)
124
+ const cloudBridge = createCloudBridgeForParsed(parsed)
125
+ if (cloudBridge) {
126
+ try {
127
+ const dataUrl: unknown = await cloudBridge.send({ type: 'screenshot' })
128
+ const pngPrefix = 'data:image/png;base64,'
129
+ if (typeof dataUrl !== 'string' || !dataUrl.startsWith(pngPrefix)) {
130
+ throw new Error('RNX Cloud screenshot returned invalid image data')
131
+ }
132
+ const outputPath = resolve(process.cwd(), outputArg || '/tmp/rnx-inspect.png')
133
+ mkdirSync(dirname(outputPath), { recursive: true })
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')
136
+ writeFileSync(outputPath, png)
137
+ console.log(` saved: ${outputPath}`)
138
+ } catch (error) {
139
+ console.error(
140
+ ` screenshot failed: ${error instanceof Error ? error.message : String(error)}`,
141
+ )
142
+ rnxExit(1)
143
+ } finally {
144
+ cloudBridge.close()
145
+ }
146
+ return
147
+ }
123
148
  const rect = await parseRect(args)
124
149
  const usingBridge = await probeBridge(parsed.wsPort)
125
150
 
@@ -56,7 +56,7 @@ examples:
56
56
  }
57
57
  const quiet = args.includes('--quiet') || args.includes('-q')
58
58
 
59
- // only one bridge per sootsim home. refuse to start a second one
59
+ // only one bridge per rnx home. refuse to start a second one
60
60
  // regardless of requested port, since both would try to write the same
61
61
  // daemon.json and electron/cli clients only read one lockfile.
62
62
  const existingLock = readDaemonLockfile()
@@ -216,7 +216,7 @@ function symlinkPointsTo(dest: string, src: string): boolean {
216
216
  }
217
217
 
218
218
  // a store entry left by an older CLI is safe to upgrade: either a real dir
219
- // whose only content is a SKILL.md symlink into a sootsim skills source (the
219
+ // whose only content is a SKILL.md symlink into an rnx skills source (the
220
220
  // pre-dir-layout install — possibly broken after the source moved), or a dir
221
221
  // holding a byte-identical copy of the skill. only our installer creates
222
222
  // those shapes, so replacing them can't lose user work.
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // `rnx record` is for video/screen capture; `rnx debug record` is
4
4
  // for the engine-internal debug-channel ring; this command is the third
5
- // surface and explicitly distinct: it manages SootSim.bridges.timeline,
5
+ // surface and explicitly distinct: it manages the SootSim.bridges.timeline API,
6
6
  // the unified, agent-facing semantic event stream across host / shell
7
7
  // worker / tenant worker.
8
8
  //
@@ -10,12 +10,15 @@ import { compareSemver } from '@contrast/runtime-delivery'
10
10
  import { getCliVersion } from '../../src/cli-version'
11
11
  import { isSootsimDevCheckout, readActiveRuntime, runtimeDir } from '../../src/home-paths'
12
12
  import { resolveRNXAppConfig } from '../app-config'
13
+ import {
14
+ DEFAULT_CLI_RELEASE_ORIGIN,
15
+ fetchCliReleaseFile,
16
+ refreshCliUpdateCache,
17
+ } from '../cli-update'
13
18
  import { fetchRuntimeReleaseNotes, formatRuntimeReleaseNotes } from '../runtime-notes'
14
19
  import { IS_STANDALONE } from '../standalone'
15
20
  import { runRuntime } from './runtime'
16
21
 
17
- const DEFAULT_CLI_RELEASE_ORIGIN = 'https://r2.rnxsim.com/rnx-cli'
18
- const CLI_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/
19
22
  const SHA256_PATTERN = /^[a-f0-9]{64}$/
20
23
 
21
24
  export interface UpgradeOptions {
@@ -100,14 +103,7 @@ export async function upgradeCli(options: UpgradeOptions = {}): Promise<void> {
100
103
  '',
101
104
  )
102
105
  const current = getCliVersion()
103
- const latest = (await fetchRequired(`${releaseOrigin}/channels/stable`, 8_000))
104
- .toString('utf8')
105
- .trim()
106
- if (!CLI_VERSION_PATTERN.test(latest)) {
107
- throw new Error(
108
- `rnx update service returned an invalid version: ${latest || '(empty)'}`,
109
- )
110
- }
106
+ const { latestVersion: latest } = await refreshCliUpdateCache({ releaseOrigin })
111
107
  if (compareSemver(latest, current) <= 0) {
112
108
  console.log(` v${current} (latest)`)
113
109
  return
@@ -115,7 +111,7 @@ export async function upgradeCli(options: UpgradeOptions = {}): Promise<void> {
115
111
 
116
112
  const artifact = cliArtifactName()
117
113
  const artifactUrl = `${releaseOrigin}/${latest}/${artifact}`
118
- const expectedSha = (await fetchRequired(`${artifactUrl}.sha256`, 8_000))
114
+ const expectedSha = (await fetchCliReleaseFile(`${artifactUrl}.sha256`, 8_000))
119
115
  .toString('utf8')
120
116
  .trim()
121
117
  if (!SHA256_PATTERN.test(expectedSha)) {
@@ -124,7 +120,7 @@ export async function upgradeCli(options: UpgradeOptions = {}): Promise<void> {
124
120
 
125
121
  console.log(` v${current} → v${latest}`)
126
122
  console.log(` downloading ${artifact}...`)
127
- const executable = await fetchRequired(artifactUrl, 120_000)
123
+ const executable = await fetchCliReleaseFile(artifactUrl, 120_000)
128
124
  const actualSha = createHash('sha256').update(executable).digest('hex')
129
125
  if (actualSha !== expectedSha) {
130
126
  throw new Error(
@@ -157,28 +153,6 @@ function cliArtifactName(): string {
157
153
  return `rnx-${platform}-${process.arch}`
158
154
  }
159
155
 
160
- async function fetchRequired(url: string, timeoutMs: number): Promise<Buffer> {
161
- let response: Response
162
- try {
163
- response = await fetch(url, {
164
- headers: { accept: 'application/octet-stream' },
165
- signal: AbortSignal.timeout(timeoutMs),
166
- })
167
- } catch (error) {
168
- throw new Error(
169
- `could not reach the rnx update service: ${
170
- error instanceof Error ? error.message : String(error)
171
- }`,
172
- )
173
- }
174
- if (!response.ok) {
175
- throw new Error(
176
- `rnx update download failed: ${response.status} ${response.statusText}`,
177
- )
178
- }
179
- return Buffer.from(await response.arrayBuffer())
180
- }
181
-
182
156
  function verifyCandidate(candidatePath: string, expectedVersion: string): void {
183
157
  const result = spawnSync(candidatePath, ['--version'], {
184
158
  encoding: 'utf8',