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.
- package/README.md +43 -26
- 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 +21 -5
- 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/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
|
package/cli/help-core.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { cliCommandMetas } from '@
|
|
1
|
+
import { cliCommandMetas } from '@rnx/skills/cli'
|
|
2
2
|
import {
|
|
3
3
|
renderCliCommandHelp,
|
|
4
4
|
renderCliGroupHelp,
|
|
5
5
|
renderCliIndexHelp,
|
|
6
|
-
} from '@
|
|
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
|
|
41
|
-
return `rnx
|
|
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
|
|
44
|
+
rnx ${command}
|
|
45
45
|
|
|
46
|
-
The browser backend selects the Factory-owned
|
|
47
|
-
|
|
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([
|
|
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
|
|
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 =
|
|
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 {
|
package/cli/hints.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// configurable, identity-scoped hint registry for the
|
|
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 `
|
|
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,
|
|
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
|
-
//
|
|
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/
|
|
119
|
-
{
|
|
120
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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 '@
|
|
1
|
+
export * from '@rnx/skills/cli'
|
|
2
2
|
export * from './command-registry'
|
package/cli/resolve-assets.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// resolve
|
|
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
|
|
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
|
+
}
|