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,5 +1,5 @@
1
1
  // rnx record upload — capture the currently-loaded bundle + device spec from
2
- // a running sootsim sim and POST it to the Contrast upload endpoint. returns a
2
+ // a running rnx sim and POST it to the Contrast upload endpoint. returns a
3
3
  // /preview/<id> url that anyone can open to load this bundle in a frame.
4
4
  //
5
5
  // uploads are unlisted-public (unguessable id) but require a signed-in
@@ -29,7 +29,7 @@ import { openUrl } from '../open-url'
29
29
  import { resolveRunProvenance } from '../run-registry'
30
30
  import { rnxExit } from '../run-rnx'
31
31
  import { createBridgeFromParsed, evalInBridge, parseBridgeCliArgs } from '../ws-bridge'
32
- import type { SootSimTimelineEvent } from '@sootsim/globals'
32
+ import type { SootSimTimelineEvent } from '@rnx/globals'
33
33
 
34
34
  interface UploadOptions {
35
35
  port?: number
@@ -110,7 +110,7 @@ function readSnapshotManifest(manifestPath: string): AttachedInteractionSnapshot
110
110
 
111
111
  const REMOTE_DEFAULT_ORIGIN = process.env.RNX_UPLOAD_ORIGIN || 'https://contrast.dev'
112
112
  // the canonical public preview link. Uploads go to the contrast backend, but
113
- // reviewers/users open SootSim preview pages on sootsim.com.
113
+ // reviewers/users open rnx preview pages on rnxsim.com.
114
114
  const REMOTE_DEFAULT_PUBLIC_PREVIEW_ORIGIN =
115
115
  process.env.RNX_PREVIEW_ORIGIN || rnxPublicBrand.origin
116
116
  export async function resolveDefaultUploadOrigin(
@@ -1,9 +1,9 @@
1
1
  // rnx version — one overview of both halves you're running.
2
2
  //
3
- // the `sootsim` CLI (npm package) and the engine runtime (versioned tarball
3
+ // the rnx CLI and the engine runtime (versioned tarball
4
4
  // rendered in the browser/electron shell) are versioned independently, which
5
5
  // is a frequent "I upgraded but the version didn't change" trap. the terse
6
- // `sootsim --version` flag prints both for scripts; this command adds the
6
+ // `rnx --version` flag prints both for scripts; this command adds the
7
7
  // channel, a "newer runtime available" hint, and a what's-new pointer.
8
8
 
9
9
  import fs from 'node:fs'
@@ -12,9 +12,14 @@ import { getCliVersion } from '../../src/cli-version'
12
12
  import { runtimeDir } from '../../src/home-paths'
13
13
  import { RNX_CHANGELOG_URL, rnxRuntime } from '../../src/runtime-delivery'
14
14
  import { resolveRNXAppConfig } from '../app-config'
15
+ import { readCachedCliUpdate, refreshCliUpdateCache } from '../cli-update'
15
16
  import { getRuntimeSummary } from '../runtime-summary'
17
+ import { IS_STANDALONE } from '../standalone'
16
18
 
17
19
  export async function runVersion(_args: string[]): Promise<void> {
20
+ const cliUpdatePromise = IS_STANDALONE
21
+ ? refreshCliUpdateCache().catch(() => readCachedCliUpdate())
22
+ : Promise.resolve(null)
18
23
  const { IS_BETA, BETA_LABEL } = await import('../../src/beta')
19
24
  const suffix = IS_BETA ? ` · ${BETA_LABEL}` : ''
20
25
  console.log(`rnx v${getCliVersion()}${suffix}`)
@@ -43,7 +48,13 @@ export async function runVersion(_args: string[]): Promise<void> {
43
48
  }
44
49
 
45
50
  // best-effort network check; never fail the command on a flaky network.
46
- const check = await rnxRuntime.checkUpToDate({ channel })
51
+ const [check, cliUpdate] = await Promise.all([
52
+ rnxRuntime.checkUpToDate({ channel }),
53
+ cliUpdatePromise,
54
+ ])
55
+ if (cliUpdate?.outdated) {
56
+ console.log(`\n ↑ CLI v${cliUpdate.latestVersion} available; run \`rnx upgrade\``)
57
+ }
47
58
  if (check.outdated && check.latest) {
48
59
  console.log(`\n ↑ default runtime v${check.latest} available; run \`rnx upgrade\``)
49
60
  }
@@ -1,6 +1,6 @@
1
1
  // rnx what-happened — show recent events from the semantic timeline
2
2
  //
3
- // the agent-facing read verb for SootSim.bridges.timeline. by default
3
+ // the agent-facing read verb for the SootSim.bridges.timeline API. by default
4
4
  // shows everything since the previous CLI call from the same agent
5
5
  // identity (cursor model). after rendering, advances the cursor past
6
6
  // the events it displayed so the next call shows only new events.
@@ -25,7 +25,7 @@ import {
25
25
  inspectTimelineSummary,
26
26
  NOISY_TIMELINE_KINDS,
27
27
  } from './inspect/core'
28
- import type { SootSimTimelineEvent, SootSimTimelineQuery } from '@sootsim/globals'
28
+ import type { SootSimTimelineEvent, SootSimTimelineQuery } from '@rnx/globals'
29
29
 
30
30
  interface WhatHappenedOptions {
31
31
  port?: number
@@ -1,13 +1,15 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs'
1
+ import { existsSync, readFileSync, rmSync } from 'fs'
2
2
  import { tmpdir } from 'os'
3
- import { dirname, join, resolve } from 'path'
3
+ import { basename, dirname, join, resolve } from 'path'
4
4
  import { getParentPid, getProcessComm } from './parent-pid'
5
+ import { writeSecureJsonAtomic } from './secure-file'
5
6
 
6
- const CURRENT_SIM_VERSION = 1 as const
7
+ const CURRENT_SIM_VERSION = 2 as const
7
8
  const CURRENT_SIM_PATH_ENV = 'SOOTSIM_CLI_CURRENT_SIM_PATH'
8
9
 
9
- interface CurrentSimState {
10
- version: 1
10
+ export interface LocalCurrentTarget {
11
+ version: 2
12
+ plane: 'local'
11
13
  simId: string
12
14
  updatedAt: string
13
15
  }
@@ -148,55 +150,66 @@ export function getCurrentSimPath() {
148
150
  return join(tmpdir(), `rnx-current-sim-${key}.json`)
149
151
  }
150
152
 
151
- export function readCurrentSim(): CurrentSimState | null {
153
+ function legacyCloudCredentialDirectory(filepath: string): string {
154
+ return join(dirname(filepath), `${basename(filepath)}.credentials`)
155
+ }
156
+
157
+ export function readCurrentTarget(): LocalCurrentTarget | null {
152
158
  const filepath = getCurrentSimPath()
159
+ rmSync(legacyCloudCredentialDirectory(filepath), { recursive: true, force: true })
153
160
  if (!existsSync(filepath)) return null
154
161
 
155
162
  try {
156
- const parsed = JSON.parse(readFileSync(filepath, 'utf8')) as Partial<CurrentSimState>
163
+ const parsed: unknown = JSON.parse(readFileSync(filepath, 'utf8'))
164
+ if (parsed === null || typeof parsed !== 'object') {
165
+ rmSync(filepath, { force: true })
166
+ return null
167
+ }
168
+ const version = Reflect.get(parsed, 'version')
169
+ const plane = Reflect.get(parsed, 'plane')
170
+ const simId = Reflect.get(parsed, 'simId')
171
+ const updatedAt = Reflect.get(parsed, 'updatedAt')
157
172
  if (
158
- parsed.version !== CURRENT_SIM_VERSION ||
159
- typeof parsed.simId !== 'string' ||
160
- !parsed.simId.trim() ||
161
- typeof parsed.updatedAt !== 'string'
173
+ (version !== 1 && version !== CURRENT_SIM_VERSION) ||
174
+ (version === CURRENT_SIM_VERSION && plane !== 'local') ||
175
+ typeof simId !== 'string' ||
176
+ !simId.trim() ||
177
+ typeof updatedAt !== 'string'
162
178
  ) {
163
179
  rmSync(filepath, { force: true })
164
180
  return null
165
181
  }
166
182
 
167
- return {
183
+ const target = {
168
184
  version: CURRENT_SIM_VERSION,
169
- simId: parsed.simId.trim(),
170
- updatedAt: parsed.updatedAt,
171
- }
185
+ plane: 'local',
186
+ simId: simId.trim(),
187
+ updatedAt,
188
+ } satisfies LocalCurrentTarget
189
+ if (version === 1) writeSecureJsonAtomic(filepath, target)
190
+ return target
172
191
  } catch {
173
192
  rmSync(filepath, { force: true })
174
193
  return null
175
194
  }
176
195
  }
177
196
 
197
+ export const readCurrentSim = readCurrentTarget
198
+
178
199
  export function readCurrentSimId() {
179
- return readCurrentSim()?.simId || null
200
+ return readCurrentTarget()?.simId ?? null
180
201
  }
181
202
 
182
203
  export function saveCurrentSimId(simId: string) {
183
204
  const normalized = simId.trim()
184
205
  if (!normalized) return
185
206
 
186
- const filepath = getCurrentSimPath()
187
- mkdirSync(dirname(filepath), { recursive: true })
188
- writeFileSync(
189
- filepath,
190
- JSON.stringify(
191
- {
192
- version: CURRENT_SIM_VERSION,
193
- simId: normalized,
194
- updatedAt: new Date().toISOString(),
195
- } satisfies CurrentSimState,
196
- null,
197
- 2,
198
- ) + '\n',
199
- )
207
+ writeSecureJsonAtomic(getCurrentSimPath(), {
208
+ version: CURRENT_SIM_VERSION,
209
+ plane: 'local',
210
+ simId: normalized,
211
+ updatedAt: new Date().toISOString(),
212
+ } satisfies LocalCurrentTarget)
200
213
  }
201
214
 
202
215
  export function clearCurrentSimId() {
@@ -241,7 +241,7 @@ export interface LaunchCompanionOptions {
241
241
  profileId?: string
242
242
  ephemeralProfile?: boolean
243
243
  // set by `rnx open`, whose native window belongs to the launching
244
- // session. `sootsim desktop` omits it and remains a persistent desktop app.
244
+ // session. `rnx desktop` omits it and remains a persistent desktop app.
245
245
  ownerPid?: number
246
246
  }
247
247
 
@@ -1,5 +1,5 @@
1
1
  // electron driver — wraps the existing desktop-companion helper. availability
2
- // is "is the sootsim desktop app installed on this machine". launch delegates
2
+ // is "is the rnx desktop app installed on this machine". launch delegates
3
3
  // to launchDesktopCompanion, which already handles mac `open -a`, linux
4
4
  // AppImage, and the --device seed.
5
5
 
@@ -9,7 +9,7 @@
9
9
  // returns the moment the sim connects, and playwright tears its browser
10
10
  // down when the launching process exits. an in-process launch would mean
11
11
  // the sim vanished the instant `open` finished. the detached host keeps the
12
- // test browser alive until the sim's page closes (e.g. via `sootsim close`).
12
+ // test browser alive until the sim's page closes (e.g. via `rnx close`).
13
13
 
14
14
  import { spawn } from 'child_process'
15
15
  import { closeSync, mkdtempSync, openSync, readFileSync } from 'fs'
@@ -286,7 +286,7 @@ const cleanupProfile = () => {
286
286
  // we are spawned detached + unref'd so \`rnx open\` returns immediately,
287
287
  // which means nothing in the OS process tree ties Chrome's lifetime to the
288
288
  // session that asked for it. once the sim connects, the connect-timeout
289
- // safety net is cleared, so absent an explicit \`sootsim close\` (or the
289
+ // safety net is cleared, so absent an explicit \`rnx close\` (or the
290
290
  // daemon's idle reaper closing the sim socket) the host would hold Chrome
291
291
  // open forever after its owner exits. poll the owning session's pid and tear
292
292
  // down when it's gone. ESRCH (kill throws) = owner exited; EPERM = alive but
@@ -601,7 +601,7 @@ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
601
601
  const connectAckFile = `${errLog}.connected`
602
602
  const errFd = openSync(errLog, 'a')
603
603
  try {
604
- // spawn the browser host detached so Chrome outlives `sootsim
604
+ // spawn the browser host detached so Chrome outlives `rnx
605
605
  // open`. stdout is dropped; stderr is captured to errLog so an early
606
606
  // crash (bad profile dir, missing browser binary) produces a real
607
607
  // message instead of a silent "timed out waiting for sim".
@@ -1,4 +1,4 @@
1
- // driver / runner abstraction for sootsim. browser sims run in an isolated
1
+ // driver / runner abstraction for rnx. browser sims run in an isolated
2
2
  // playwright-owned profile. electron remains the native desktop surface.
3
3
 
4
4
  export type DriverId = 'electron' | 'playwright'
@@ -29,7 +29,7 @@ export interface DriverLaunchOptions {
29
29
  url?: string
30
30
  // run in headless mode where the driver supports it. ignored by electron.
31
31
  headless?: boolean
32
- // initial device model seed forwarded to the sootsim shell. the electron
32
+ // initial device model seed forwarded to the rnx shell. the electron
33
33
  // driver forwards it via argv (--device=<model>); browser drivers receive
34
34
  // it already stamped on the URL (?device=<model>) by runOpenCommand.
35
35
  device?: string
@@ -51,7 +51,7 @@ export interface DriverLaunchOptions {
51
51
  // stable pid for the session that owns a CLI-opened host. electron closes
52
52
  // only that session's windows when the owner exits; playwright requires it
53
53
  // so a detached browser can never outlive its session. omitted only for the
54
- // persistent `sootsim desktop` app lifecycle.
54
+ // persistent `rnx desktop` app lifecycle.
55
55
  ownerPid?: number
56
56
  // expose Chrome's remote-debugging endpoint on this port so a
57
57
  // bridge-driven sim can also be cpu-profiled. only the playwright driver
@@ -60,7 +60,7 @@ export interface DriverLaunchOptions {
60
60
  // maximum time the detached host should wait for the launched sim to
61
61
  // register with the bridge before it tears itself down.
62
62
  connectTimeoutMs?: number
63
- // the sootsim WS bridge port the launched sim page should register with,
63
+ // the rnx WS bridge port the launched sim page should register with,
64
64
  // injected as window.__sootsimBridgePort. load-bearing when the dev bridge
65
65
  // drifted off the shell-port-implied default (e.g. :7668 taken at the shell's
66
66
  // boot -> bridge on :7669 while the shell kept :5173): the page's own
package/cli/flow-file.ts CHANGED
@@ -9,7 +9,7 @@ export interface FlowFrontmatter {
9
9
  // theme-sensitive; a dark capture can never match a light baseline)
10
10
  theme?: 'light' | 'dark'
11
11
  electron?: boolean
12
- // maestro-compatible: appId is accepted but not used by sootsim (ios bundle id)
12
+ // maestro-compatible: appId is accepted but not used by rnx (ios bundle id)
13
13
  appId?: string
14
14
  // maestro-compatible: flow-level env vars, defined into the flow's JS
15
15
  // context before the first step runs
@@ -20,10 +20,10 @@ export interface FlowFrontmatter {
20
20
 
21
21
  // parse flow file. accepts three shapes:
22
22
  // 1. plain yaml array of steps (no frontmatter)
23
- // 2. sootsim style: ---\n<fm>\n---\n<steps>
23
+ // 2. rnx style: ---\n<fm>\n---\n<steps>
24
24
  // 3. maestro style: <fm>\n---\n<steps> (multi-doc yaml, fm first)
25
25
  // maestro compat matters because real users have real .maestro/*.yaml files
26
- // and shouldn't have to rewrite them to drive sootsim.
26
+ // and shouldn't have to rewrite them to drive rnx.
27
27
  //
28
28
  // `${...}` templates are NOT resolved here — interpolation happens per-step
29
29
  // at execution time in the flow runner's JS context (matching maestro), so
package/cli/help-core.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { cliCommandMetas } from '@sootsim/skills/cli'
1
+ import { cliCommandMetas } from '@rnx/skills/cli'
2
2
  import {
3
3
  renderCliCommandHelp,
4
4
  renderCliGroupHelp,
5
5
  renderCliIndexHelp,
6
- } from '@sootsim/skills/cli/renderers'
6
+ } from '@rnx/skills/cli/renderers'
7
7
  import { formatFlagHelp } from 'sootsim-engine/settings/cli-flags'
8
8
  import {
9
9
  DEFAULT_SOOTSIM_BRIDGE_PORT,
@@ -37,14 +37,14 @@ function backendLabel(command: string, args: readonly string[]): string {
37
37
  return `available backends: ${available.join(', ')}`
38
38
  }
39
39
 
40
- function renderWebHelp(): string {
41
- return `rnx web — select or ensure the current branch's existing web preview
40
+ function renderPreviewHelp(command: 'web' | 'ios' | 'android'): string {
41
+ return `rnx ${command} — select or ensure the current branch's existing ${command} preview
42
42
 
43
43
  usage:
44
- rnx web
44
+ rnx ${command}
45
45
 
46
- The browser backend selects the Factory-owned web iframe. It never launches a
47
- browser, host process, or Cloudflare runtime.`
46
+ The browser backend selects the Factory-owned ${command} iframe. It never launches a
47
+ host process or Cloudflare runtime.`
48
48
  }
49
49
 
50
50
  function renderFactoryBrowserLeaves(command: string): string | null {
@@ -70,7 +70,12 @@ function renderFactoryBrowserLeaves(command: string): string | null {
70
70
  }
71
71
 
72
72
  function renderRootAvailability(backend: RnxBackendKind): string {
73
- const visibleCommands = new Set([...cliCommandMetas.map((meta) => meta.id), 'web'])
73
+ const visibleCommands = new Set([
74
+ ...cliCommandMetas.map((meta) => meta.id),
75
+ 'web',
76
+ 'ios',
77
+ 'android',
78
+ ])
74
79
  const visibleRegistry = RNX_COMMAND_REGISTRY.filter((entry) =>
75
80
  visibleCommands.has(entry.name),
76
81
  )
@@ -94,15 +99,17 @@ export function renderRnxHelp(options: RnxHelpOptions): string {
94
99
  if (!command) {
95
100
  return `${renderCliIndexHelp(HELP_CONTEXT, formatFlagHelp()).trimEnd()}
96
101
 
97
- browser preview selector:
102
+ browser preview selectors:
98
103
  rnx web select the existing Factory web preview
104
+ rnx ios select the existing Factory iOS preview
105
+ rnx android select the existing Factory Android preview
99
106
 
100
107
  ${renderRootAvailability(options.backend)}`
101
108
  }
102
109
 
103
110
  let body: string | null
104
- if (command === 'web') {
105
- body = renderWebHelp()
111
+ if (command === 'web' || command === 'ios' || command === 'android') {
112
+ body = renderPreviewHelp(command)
106
113
  } else if (options.group === command && !options.prefer) {
107
114
  body = renderCliGroupHelp(command, HELP_CONTEXT, formatFlagHelp())
108
115
  } else {
@@ -1,4 +1,4 @@
1
- import { getCliInspectVerbMetaByPath } from '@sootsim/skills/cli'
1
+ import { getCliInspectVerbMetaByPath } from '@rnx/skills/cli'
2
2
  import type { ParsedArgs } from './parse-args'
3
3
 
4
4
  const HIDDEN_RUNTIME_ALIAS_GROUPS = ['do', 'get'] as const
package/cli/hints.ts CHANGED
@@ -1,4 +1,4 @@
1
- // configurable, identity-scoped hint registry for the sootsim CLI.
1
+ // configurable, identity-scoped hint registry for the rnx CLI.
2
2
  //
3
3
  // two problems this solves:
4
4
  // 1. the CLI scatters "hint: ..." console.logs across many command files,
@@ -205,7 +205,7 @@ defineHint<[string]>('subtree-root-not-found', {
205
205
  })
206
206
 
207
207
  defineHint<[]>('clean-uninstall', {
208
- // shown once on the first `sootsim --help` run so users learn the clean
208
+ // shown once on the first `rnx --help` run so users learn the clean
209
209
  // teardown path. package-manager uninstall hooks are best-effort, so the
210
210
  // hint documents the explicit daemon cleanup command.
211
211
  frequency: 'once-ever',
package/cli/maestro-js.ts CHANGED
@@ -14,9 +14,9 @@
14
14
  // time (maestro-orchestra-models util/Env.kt): regex (?<!\\)\$\{([^$]*)},
15
15
  // result stringified, `\${...}` escapes to a literal, blank exprs become ''.
16
16
  //
17
- // divergences (deliberate, sootsim-side ergonomics — keep this list short):
17
+ // divergences (deliberate, rnx-side ergonomics — keep this list short):
18
18
  // 1. envBinding is seeded from the full process.env, not just MAESTRO_*.
19
- // sootsim flows historically resolve `${SOOTSIM_*}` credentials from the
19
+ // rnx flows historically resolve `${SOOTSIM_*}` credentials from the
20
20
  // shell environment and `rnx maestro --env K=V` writes process.env.
21
21
  // 2. a *bare identifier* template (`${NAME}`) that evaluates to undefined
22
22
  // throws a loud "missing variable" error instead of interpolating the
@@ -102,6 +102,14 @@ export const OUTBOUND_ENDPOINTS: Record<string, OutboundEndpointDeclaration> = {
102
102
  payload:
103
103
  'every file git reports for the current checkout, then shell commands and file reads and writes for as long as the box shell is open',
104
104
  },
105
+ rnx_cloud_sim: {
106
+ category: 'explicit-user-action',
107
+ destination:
108
+ 'https://rnxsim.com by default, or the test or disposable origin named by RNX_CLOUD_ORIGIN',
109
+ preConsent: false,
110
+ payload:
111
+ 'the Metro JavaScript bundle selected by rnx ios --cloud, then structured simulator commands and lifecycle requests',
112
+ },
105
113
  }
106
114
 
107
115
  export interface OutboundCallDeclaration {
@@ -113,22 +121,47 @@ export interface OutboundCallDeclaration {
113
121
  // a new fetch, websocket, node HTTP request, or TCP connection has no entry and
114
122
  // fails the package test until its destination and consent behavior are declared.
115
123
  export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> = {
124
+ 'packages/sootsim/cli/cloud-client.ts :: fetch :: `${apiOrigin}/v1/sims`': {
125
+ category: 'rnx_cloud_sim',
126
+ count: 1,
127
+ },
128
+ 'packages/sootsim/cli/cloud-client.ts :: fetch :: `${session.apiOrigin}/v1/sims/${encodeURIComponent(session.simId)}/claim`':
129
+ { category: 'rnx_cloud_sim', count: 1 },
130
+ 'packages/sootsim/cli/cloud-client.ts :: fetch :: `${session.apiOrigin}/v1/sims/${encodeURIComponent(session.simId)}`':
131
+ { category: 'rnx_cloud_sim', count: 1 },
132
+ 'packages/sootsim/cli/cloud-client.ts :: fetch :: `${this.session.apiOrigin}/v1/sims/${encodeURIComponent(this.session.simId)}/commands`':
133
+ { category: 'rnx_cloud_sim', count: 1 },
116
134
  // `rnx box create` uploads the checkout to the box service and then talks to
117
135
  // it. the destination is whatever the user named; there is no default.
118
- 'packages/sootsim/cli/commands/box/cloud-box.ts :: fetch :: endpointOf(base, name, verb)':
119
- { category: 'rnx_cloud_box', count: 1 },
120
- 'packages/sootsim/cli/commands/box/cloud-box.ts :: websocket :: url': {
136
+ 'packages/rnx-cloud-box/src/client.ts :: fetch :: endpointOf(base, name, verb, search)':
137
+ {
138
+ category: 'rnx_cloud_box',
139
+ count: 1,
140
+ },
141
+ 'packages/rnx-cloud-box/src/client.ts :: websocket :: url': {
121
142
  category: 'rnx_cloud_box',
122
143
  count: 1,
123
144
  },
124
145
  // the account's own session meter: starting, listing and heartbeating the
125
146
  // session a cloud box is billed against. no project content.
126
- "packages/sootsim/cli/commands/box/cloud-box.ts :: fetch :: `${options.apiOrigin.replace(/\\/+$/, '')}/v1/rnx/sessions`":
147
+ "packages/rnx-cloud-box/src/client.ts :: fetch :: `${options.apiOrigin.replace(/\\/+$/, '')}/v1/rnx/sessions`":
127
148
  { category: 'contrast_user_action', count: 1 },
128
- 'packages/sootsim/cli/commands/box/cloud-box.ts :: fetch :: url': {
149
+ 'packages/rnx-cloud-box/src/client.ts :: fetch :: url': {
150
+ category: 'contrast_user_action',
151
+ count: 1,
152
+ },
153
+ 'packages/rnx-cloud-box/src/box-do.ts :: fetch :: `${origin}/v1/rnx/sessions`': {
129
154
  category: 'contrast_user_action',
130
155
  count: 1,
131
156
  },
157
+ 'packages/rnx-cloud-box/src/box-do.ts :: fetch :: `${origin}/v1/rnx/sessions/${encodeURIComponent(sessionId)}`':
158
+ { category: 'contrast_user_action', count: 1 },
159
+ // the Cloudflare Worker forwards the already-authenticated request to the
160
+ // selected Durable Object in the same service.
161
+ 'packages/rnx-cloud-box/src/worker.ts :: fetch :: request': {
162
+ category: 'same_origin_runtime',
163
+ count: 1,
164
+ },
132
165
  // a LOCAL box authorizing its own checkout with the loopback bridge daemon.
133
166
  // this one refuses to guess a port, so it cannot reach anything but a bridge
134
167
  // the user's own session is running; see the note on ws-bridge.ts below
@@ -340,7 +373,7 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
340
373
  { category: 'contrast_user_action', count: 1 },
341
374
  'packages/sootsim/cli/commands/screenshots-capture.ts :: fetch :: `${apiOrigin}/api/sootsim/screenshot-decks?org=${encodeURIComponent(org)}&repo=${encodeURIComponent(repo)}`':
342
375
  { category: 'contrast_user_action', count: 1 },
343
- 'packages/sootsim/cli/commands/upgrade.ts :: fetch :: url': {
376
+ 'packages/sootsim/cli/cli-update.ts :: fetch :: url': {
344
377
  category: 'cli_delivery',
345
378
  count: 1,
346
379
  },
@@ -589,6 +622,7 @@ export function detectOutboundCalls(repositoryRoot: string): DetectedOutboundCal
589
622
  path.join(repositoryRoot, 'packages/sootsim-engine/src'),
590
623
  path.join(repositoryRoot, 'packages/sootsim-engine/src-electron'),
591
624
  path.join(repositoryRoot, 'packages/contrast-runtime-delivery/src'),
625
+ path.join(repositoryRoot, 'packages/rnx-cloud-box/src'),
592
626
  ]
593
627
  const counts = new Map<string, number>()
594
628
  for (const file of roots.flatMap(sourceFilesUnder)) {
package/cli/parse-args.ts CHANGED
@@ -60,7 +60,7 @@ export const HYPHENATED_GROUPING_VERBS = new Set(['do', 'get', 'wait', 'shell',
60
60
 
61
61
  // known commands — if first positional arg matches, it's a command.
62
62
  // every entry here must have a `cliCommandMetas` entry in
63
- // `packages/sootsim-skills/src/cli/meta.ts` (or sit on the internal
63
+ // `packages/rnx-skills/src/cli/meta.ts` (or sit on the internal
64
64
  // allowlist in `test/sootsimCliRegistry.test.ts`), so help, website docs,
65
65
  // and agent skills can never drift from what the parser accepts.
66
66
  export const COMMANDS = new Set(RNX_NODE_COMMAND_NAMES)
package/cli/registry.ts CHANGED
@@ -1,2 +1,2 @@
1
- export * from '@sootsim/skills/cli'
1
+ export * from '@rnx/skills/cli'
2
2
  export * from './command-registry'
@@ -1,4 +1,4 @@
1
- // resolve sootsim's bundled assets (fonts, icons, sounds)
1
+ // resolve rnx's bundled assets (fonts, icons, sounds)
2
2
  // regardless of how the package was installed (source checkout, npm install, bunx).
3
3
  //
4
4
  // uses import.meta.url to derive the package root, then checks both
@@ -13,7 +13,7 @@ import { rnxPublicBrand } from '../src/public-brand'
13
13
  // when running from source, this file lives at cli/resolve-assets.ts
14
14
  const thisDir = dirname(fileURLToPath(import.meta.url))
15
15
 
16
- // walk up to find package root: look for package.json with name "sootsim"
16
+ // walk up to find package root: look for package.json with the rnxsim package name
17
17
  function findPackageRoot(): string {
18
18
  let dir = thisDir
19
19
  for (let i = 0; i < 5; i++) {
@@ -0,0 +1,31 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import {
3
+ closeSync,
4
+ fchmodSync,
5
+ fsyncSync,
6
+ mkdirSync,
7
+ openSync,
8
+ renameSync,
9
+ rmSync,
10
+ writeFileSync,
11
+ } from 'node:fs'
12
+ import { dirname } from 'node:path'
13
+
14
+ export function writeSecureJsonAtomic(filepath: string, value: unknown): void {
15
+ mkdirSync(dirname(filepath), { recursive: true })
16
+ const temporary = `${filepath}.tmp-${process.pid}-${randomUUID()}`
17
+ let descriptor: number | null = null
18
+ try {
19
+ descriptor = openSync(temporary, 'wx', 0o600)
20
+ fchmodSync(descriptor, 0o600)
21
+ writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}\n`)
22
+ fsyncSync(descriptor)
23
+ closeSync(descriptor)
24
+ descriptor = null
25
+ renameSync(temporary, filepath)
26
+ } catch (error) {
27
+ if (descriptor !== null) closeSync(descriptor)
28
+ rmSync(temporary, { force: true })
29
+ throw error
30
+ }
31
+ }
@@ -0,0 +1,98 @@
1
+ import {
2
+ RNX_CLOUD_SESSION_ENV,
3
+ RNX_SHELL_UPDATE_SET_PREFIX,
4
+ RNX_SHELL_UPDATE_UNSET,
5
+ } from './cloud-session'
6
+
7
+ export function renderRnxShellInit(shell: string): string {
8
+ if (shell !== 'bash' && shell !== 'zsh') {
9
+ throw new Error('RNX shell integration supports bash and zsh')
10
+ }
11
+ return `rnx() {
12
+ local -a _rnx_shell_args
13
+ local _rnx_shell_command _rnx_shell_create _rnx_shell_update
14
+ local _rnx_shell_session _rnx_shell_status _rnx_shell_traced
15
+ _rnx_shell_args=("$@")
16
+ _rnx_shell_command=''
17
+ _rnx_shell_create=0
18
+ while [ "$#" -gt 0 ]; do
19
+ case "$1" in
20
+ -h|--help|-V|--version|-v|--verbose|--headless) shift ;;
21
+ -p|--port|-d|--device|-t|--theme|--driver|--sim|--session|--tab)
22
+ if [ "$#" -ge 2 ]; then shift 2; else shift; fi
23
+ ;;
24
+ --) break ;;
25
+ -*) shift ;;
26
+ *) _rnx_shell_command="$1"; shift; break ;;
27
+ esac
28
+ done
29
+ case "$_rnx_shell_command" in
30
+ claim|close) ;;
31
+ ios)
32
+ for _rnx_shell_session in "$@"; do
33
+ if [ "$_rnx_shell_session" = '--cloud' ]; then
34
+ _rnx_shell_create=1
35
+ break
36
+ fi
37
+ done
38
+ if [ "$_rnx_shell_create" -ne 1 ]; then
39
+ command rnx "\${_rnx_shell_args[@]}"
40
+ return $?
41
+ fi
42
+ ;;
43
+ *)
44
+ command rnx "\${_rnx_shell_args[@]}"
45
+ return $?
46
+ ;;
47
+ esac
48
+
49
+ if { [ -n "\${BASH_VERSION:-}" ] && [ "\${BASH_SUBSHELL:-0}" -ne 0 ]; } ||
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
52
+ return 1
53
+ fi
54
+ if ( : >&9 ) 2>/dev/null; then
55
+ printf '%s\\n' 'rnx cloud shell integration requires file descriptor 9 to be unused' >&2
56
+ return 1
57
+ fi
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
60
+ return 1
61
+ fi
62
+
63
+ case "$-" in
64
+ *x*) _rnx_shell_traced=1; set +x ;;
65
+ *) _rnx_shell_traced=0 ;;
66
+ esac
67
+ exec 9>&1
68
+ _rnx_shell_update="$(RNX_SHELL_UPDATE_FD=3 command rnx "\${_rnx_shell_args[@]}" 3>&1 1>&9)"
69
+ _rnx_shell_status=$?
70
+ exec 9>&-
71
+
72
+ if [ "$_rnx_shell_status" -eq 0 ]; then
73
+ case "$_rnx_shell_update" in
74
+ ${RNX_SHELL_UPDATE_SET_PREFIX}*)
75
+ _rnx_shell_session="\${_rnx_shell_update#${RNX_SHELL_UPDATE_SET_PREFIX}}"
76
+ case "$_rnx_shell_session" in
77
+ ''|*$'\\n'*) _rnx_shell_status=1 ;;
78
+ *) export ${RNX_CLOUD_SESSION_ENV}="$_rnx_shell_session" || _rnx_shell_status=$? ;;
79
+ esac
80
+ ;;
81
+ ${RNX_SHELL_UPDATE_UNSET})
82
+ unset ${RNX_CLOUD_SESSION_ENV} || _rnx_shell_status=$?
83
+ ;;
84
+ *) _rnx_shell_status=1 ;;
85
+ esac
86
+ if [ "$_rnx_shell_status" -ne 0 ]; then
87
+ printf '%s\\n' 'rnx refused an invalid shell session update' >&2
88
+ elif [ "$_rnx_shell_create" -eq 1 ]; then
89
+ command rnx __cloud-confirm
90
+ _rnx_shell_status=$?
91
+ fi
92
+ fi
93
+
94
+ if [ "$_rnx_shell_traced" -eq 1 ]; then set -x; fi
95
+ return "$_rnx_shell_status"
96
+ }
97
+ `
98
+ }