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.
- package/README.md +19 -6
- package/cli/app-fonts.ts +1 -1
- package/cli/bin.ts +118 -26
- package/cli/bridge-diagnostics.ts +1 -1
- package/cli/bridge-flow-runner.ts +9 -9
- package/cli/browser-evals.ts +3 -3
- package/cli/cli-update.ts +261 -0
- package/cli/cloud-client.ts +407 -0
- package/cli/cloud-dispatch.ts +165 -0
- package/cli/cloud-session.ts +156 -0
- package/cli/command-contract.ts +2 -2
- package/cli/command-registry.ts +9 -0
- package/cli/commands/agent-wrapper.ts +4 -4
- package/cli/commands/agent.ts +3 -3
- package/cli/commands/box/checkout-plane.ts +16 -8
- package/cli/commands/box/rnx-command.ts +2 -2
- package/cli/commands/box/search-command.ts +2 -2
- package/cli/commands/box.ts +123 -70
- package/cli/commands/config.ts +3 -2
- package/cli/commands/control.ts +6 -6
- package/cli/commands/cpu-profile.ts +2 -2
- package/cli/commands/daemon.ts +7 -7
- package/cli/commands/debug.ts +2 -2
- package/cli/commands/detox.ts +7 -7
- package/cli/commands/diagnose.ts +1 -1
- package/cli/commands/electron.ts +1 -1
- package/cli/commands/film.ts +1 -1
- package/cli/commands/flow.ts +4 -4
- package/cli/commands/inspect/core.ts +58 -7
- package/cli/commands/inspect/env.ts +5 -3
- package/cli/commands/inspect/shared.ts +2 -1
- package/cli/commands/inspect/wait-event.ts +1 -1
- package/cli/commands/inspect/wait-ready.ts +15 -5
- package/cli/commands/inspect.ts +31 -9
- package/cli/commands/install-desktop.ts +2 -2
- package/cli/commands/maestro.ts +3 -3
- package/cli/commands/permissions.ts +1 -1
- package/cli/commands/platform.ts +91 -0
- package/cli/commands/record.ts +3 -3
- package/cli/commands/report-issue.ts +1 -1
- package/cli/commands/runtime.ts +1 -1
- package/cli/commands/screenshot-mode.ts +1 -1
- package/cli/commands/screenshot.ts +25 -0
- package/cli/commands/serve.ts +1 -1
- package/cli/commands/skills.ts +1 -1
- package/cli/commands/timeline.ts +1 -1
- package/cli/commands/upgrade.ts +8 -34
- package/cli/commands/upload.ts +3 -3
- package/cli/commands/version.ts +14 -3
- package/cli/commands/what-happened.ts +2 -2
- package/cli/current-sim.ts +43 -30
- package/cli/desktop-companion.ts +1 -1
- package/cli/drivers/electron.ts +1 -1
- package/cli/drivers/playwright.ts +3 -3
- package/cli/drivers/types.ts +4 -4
- package/cli/flow-file.ts +3 -3
- package/cli/help-core.ts +18 -11
- package/cli/hidden-runtime-alias.ts +1 -1
- package/cli/hints.ts +2 -2
- package/cli/maestro-js.ts +2 -2
- package/cli/outbound-endpoints.ts +40 -6
- package/cli/parse-args.ts +1 -1
- package/cli/registry.ts +1 -1
- package/cli/resolve-assets.ts +2 -2
- package/cli/secure-file.ts +31 -0
- package/cli/shell-init.ts +98 -0
- package/cli/ws-bridge.ts +18 -8
- package/detox/expectations.ts +1 -1
- package/detox/gestures.ts +3 -3
- package/detox/index.ts +3 -3
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +15 -8
- package/dist-lib/bridge-contract-input.mjs +15 -8
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +107 -0
- package/dist-lib/capture-contract.mjs +80 -0
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +32 -0
- package/dist-lib/cloud-contract.mjs +7 -0
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +4 -4
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +9 -1
- package/dist-lib/host/bridge-host.cjs +15 -10
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +248 -73
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +44 -6
- package/dist-lib/sdk.mjs +44 -6
- package/dist-lib/skills.cjs +435 -155
- package/dist-lib/vite.cjs +1 -1
- package/package.json +20 -4
- package/scripts/dev-server-scanner.ts +3 -3
- package/skills/contrast/SKILL.md +1 -1
- package/skills/rnx-perf/SKILL.md +68 -52
- package/src/app-url.ts +2 -3
- package/src/bridge-contract-input.ts +17 -6
- package/src/bridge-contract.ts +25 -4
- package/src/capture-contract.ts +423 -0
- package/src/cloud-contract.ts +15 -0
- package/src/home-paths.ts +9 -3
- package/src/host/agent-host.ts +1 -1
- package/src/host/bridge-host.ts +13 -11
- package/src/host/plane-host.ts +2 -2
- package/src/index.ts +21 -0
- package/src/native-dev-bundle-url.ts +2 -2
- package/src/screenshots/orchestrate.ts +1 -1
- package/src/screenshots/org-deck.ts +1 -1
- package/src/settings-bundle.ts +1 -1
- package/src/sim-client.ts +4 -0
- package/src/vite-plugin-one.ts +2 -2
- package/src/vite-plugin.ts +1 -1
- package/cli/commands/box/cloud-box.ts +0 -294
package/cli/commands/platform.ts
CHANGED
|
@@ -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
|
|
package/cli/commands/record.ts
CHANGED
|
@@ -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
|
|
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/
|
|
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
|
|
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
|
package/cli/commands/runtime.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
package/cli/commands/serve.ts
CHANGED
|
@@ -56,7 +56,7 @@ examples:
|
|
|
56
56
|
}
|
|
57
57
|
const quiet = args.includes('--quiet') || args.includes('-q')
|
|
58
58
|
|
|
59
|
-
// only one bridge per
|
|
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()
|
package/cli/commands/skills.ts
CHANGED
|
@@ -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
|
|
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.
|
package/cli/commands/timeline.ts
CHANGED
|
@@ -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
|
//
|
package/cli/commands/upgrade.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
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',
|
package/cli/commands/upload.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// rnx record upload — capture the currently-loaded bundle + device spec from
|
|
2
|
-
// a running
|
|
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 '@
|
|
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
|
|
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(
|
package/cli/commands/version.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// rnx version — one overview of both halves you're running.
|
|
2
2
|
//
|
|
3
|
-
// the
|
|
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
|
-
// `
|
|
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
|
|
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 '@
|
|
28
|
+
import type { SootSimTimelineEvent, SootSimTimelineQuery } from '@rnx/globals'
|
|
29
29
|
|
|
30
30
|
interface WhatHappenedOptions {
|
|
31
31
|
port?: number
|
package/cli/current-sim.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { existsSync,
|
|
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 =
|
|
7
|
+
const CURRENT_SIM_VERSION = 2 as const
|
|
7
8
|
const CURRENT_SIM_PATH_ENV = 'SOOTSIM_CLI_CURRENT_SIM_PATH'
|
|
8
9
|
|
|
9
|
-
interface
|
|
10
|
-
version:
|
|
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
|
-
|
|
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'))
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
183
|
+
const target = {
|
|
168
184
|
version: CURRENT_SIM_VERSION,
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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() {
|
package/cli/desktop-companion.ts
CHANGED
|
@@ -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. `
|
|
244
|
+
// session. `rnx desktop` omits it and remains a persistent desktop app.
|
|
245
245
|
ownerPid?: number
|
|
246
246
|
}
|
|
247
247
|
|
package/cli/drivers/electron.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// electron driver — wraps the existing desktop-companion helper. availability
|
|
2
|
-
// is "is the
|
|
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 `
|
|
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 \`
|
|
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 `
|
|
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".
|
package/cli/drivers/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// driver / runner abstraction for
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|