rnxsim 0.1.381 → 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 +19 -6
  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 +20 -4
  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
@@ -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',
@@ -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