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/box.ts
CHANGED
|
@@ -1,48 +1,40 @@
|
|
|
1
1
|
// `rnx box` — the box environment.
|
|
2
2
|
//
|
|
3
|
-
// bare `rnx box` is LOCAL and mounts the current checkout at /project.
|
|
4
|
-
// create
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// fails.
|
|
3
|
+
// bare `rnx box` is LOCAL and mounts the current checkout at /project. cloud
|
|
4
|
+
// create provisions through the shared client and enters the same shell unless
|
|
5
|
+
// --json asks for a noninteractive result. reconnect still needs an authorized
|
|
6
|
+
// provider lookup from public box identity to the provider reference.
|
|
8
7
|
|
|
9
8
|
import path from 'node:path'
|
|
10
9
|
import { createInterface } from 'node:readline'
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from '@
|
|
10
|
+
import { BoxClientError, createBoxClient } from '@rnx/box/client'
|
|
11
|
+
import { createBoxHostProvider } from '@rnx/box/host'
|
|
12
|
+
import { PROJECT_MOUNT } from '@rnx/box/shell'
|
|
13
|
+
import { terminalBoxStack } from '@rnx/box/stack'
|
|
14
|
+
import { BoxTemplateRegistry, terminalBoxTemplate } from '@rnx/box/template'
|
|
15
|
+
import { createCloudBoxProvider, heartbeatSession } from 'rnx-cloud-box/client'
|
|
16
16
|
import { rnxPublicBrand } from '../../src/public-brand'
|
|
17
17
|
import { authHeaderOrExit } from '../auth'
|
|
18
18
|
import { findRepositoryPackageRoot } from '../setup-repository'
|
|
19
19
|
import { resolvePlacement } from '../ws-bridge'
|
|
20
20
|
import { CheckoutFilePlane, CheckoutRequiresGitError } from './box/checkout-plane'
|
|
21
|
-
import {
|
|
22
|
-
CloudBoxError,
|
|
23
|
-
connectCloudBox,
|
|
24
|
-
createCloudBox,
|
|
25
|
-
heartbeatSession,
|
|
26
|
-
listCloudBoxes,
|
|
27
|
-
stopCloudBox,
|
|
28
|
-
} from './box/cloud-box'
|
|
29
21
|
import { sharePlane } from './box/plane-share'
|
|
30
22
|
import { createRnxCommand } from './box/rnx-command'
|
|
31
23
|
import { createSearchCommand } from './box/search-command'
|
|
32
|
-
import type {
|
|
24
|
+
import type { BoxShellSession } from '@rnx/box/client'
|
|
25
|
+
import type { BoxTemplateResolution } from '@rnx/box/template'
|
|
33
26
|
|
|
34
27
|
interface BoxCommandOptions {
|
|
35
28
|
port?: number
|
|
36
29
|
verbose?: boolean
|
|
37
30
|
}
|
|
38
31
|
|
|
39
|
-
const CLOUD_SUBCOMMANDS = new Set(['connect'])
|
|
40
|
-
|
|
41
32
|
// no default. a cloud box service has to be deployed before one can exist, and
|
|
42
33
|
// defaulting to an origin that answers nothing would advertise a capability
|
|
43
34
|
// that always fails.
|
|
44
35
|
const ENDPOINT_ENV = 'RNX_BOX_ENDPOINT'
|
|
45
36
|
const API_ORIGIN_ENV = 'RNX_API_ORIGIN'
|
|
37
|
+
const BOX_TEMPLATES = new BoxTemplateRegistry([terminalBoxTemplate])
|
|
46
38
|
|
|
47
39
|
function usage(): void {
|
|
48
40
|
const rnx = rnxPublicBrand.commandName
|
|
@@ -53,9 +45,12 @@ function usage(): void {
|
|
|
53
45
|
${rnx} box start a local box over the current checkout
|
|
54
46
|
${rnx} box create create a cloud box and enter its shell
|
|
55
47
|
${rnx} box create --name=<n> create a named cloud box
|
|
48
|
+
${rnx} box create --template=<id>
|
|
49
|
+
create from a registered Box template
|
|
56
50
|
${rnx} box create --endpoint=<url>
|
|
57
51
|
the box service to create it in, or set
|
|
58
52
|
${ENDPOINT_ENV}
|
|
53
|
+
${rnx} box create --json create without entering the shell
|
|
59
54
|
${rnx} box ls cloud boxes running on this account
|
|
60
55
|
${rnx} box stop --name=<n> stop a cloud box and its billing
|
|
61
56
|
${rnx} box delete --name=<n> stop it and wipe its files
|
|
@@ -89,18 +84,18 @@ export async function runBox(
|
|
|
89
84
|
|
|
90
85
|
const subcommand = args.find((arg) => !arg.startsWith('-'))
|
|
91
86
|
if (subcommand === 'create') return runCloudBoxCreate(args, opts)
|
|
87
|
+
if (subcommand === 'connect') {
|
|
88
|
+
console.error(
|
|
89
|
+
` ${rnxPublicBrand.commandName} box connect: the cloud provider cannot yet resolve a Box ID or name to an authorized Box reference.\n` +
|
|
90
|
+
` the shared Box client requires that reference's access token, and the CLI does not persist Box tokens.`,
|
|
91
|
+
)
|
|
92
|
+
return 1
|
|
93
|
+
}
|
|
92
94
|
if (subcommand === 'stop' || subcommand === 'delete') {
|
|
93
95
|
return runCloudBoxStop(args, subcommand === 'delete')
|
|
94
96
|
}
|
|
95
97
|
if (subcommand === 'ls') return runCloudBoxList(args)
|
|
96
98
|
if (subcommand !== undefined) {
|
|
97
|
-
if (CLOUD_SUBCOMMANDS.has(subcommand)) {
|
|
98
|
-
console.error(
|
|
99
|
-
` ${rnxPublicBrand.commandName} box ${subcommand} needs a cloud box, which is not built yet.\n` +
|
|
100
|
-
` \`${rnxPublicBrand.commandName} box\` runs the same shell locally over this checkout.`,
|
|
101
|
-
)
|
|
102
|
-
return 1
|
|
103
|
-
}
|
|
104
99
|
console.error(
|
|
105
100
|
` unknown command: ${rnxPublicBrand.commandName} box ${subcommand}\n` +
|
|
106
101
|
` run \`${rnxPublicBrand.commandName} box --help\` to see the box surface.`,
|
|
@@ -108,6 +103,13 @@ export async function runBox(
|
|
|
108
103
|
return 1
|
|
109
104
|
}
|
|
110
105
|
|
|
106
|
+
if (args.includes('--json')) {
|
|
107
|
+
console.error(
|
|
108
|
+
` ${rnxPublicBrand.commandName} box --json requires a noninteractive cloud subcommand such as create.`,
|
|
109
|
+
)
|
|
110
|
+
return 1
|
|
111
|
+
}
|
|
112
|
+
|
|
111
113
|
return runLocalBox(opts)
|
|
112
114
|
}
|
|
113
115
|
|
|
@@ -152,13 +154,14 @@ async function runCloudBoxStop(args: string[], purge: boolean): Promise<number>
|
|
|
152
154
|
return 1
|
|
153
155
|
}
|
|
154
156
|
const { header } = authHeaderOrExit(`box ${label}`)
|
|
157
|
+
const apiKey = header.replace(/^Bearer /, '')
|
|
158
|
+
const client = createBoxClient(
|
|
159
|
+
createCloudBoxProvider({ endpoint, apiOrigin: apiOrigin(args), apiKey }),
|
|
160
|
+
)
|
|
155
161
|
try {
|
|
156
|
-
const result =
|
|
157
|
-
|
|
158
|
-
name
|
|
159
|
-
apiKey: header.replace(/^Bearer /, ''),
|
|
160
|
-
purge,
|
|
161
|
-
})
|
|
162
|
+
const result = purge
|
|
163
|
+
? await client.delete({ boxId: name })
|
|
164
|
+
: await client.stop({ boxId: name })
|
|
162
165
|
console.log(
|
|
163
166
|
purge
|
|
164
167
|
? ` deleted ${name}, freeing ${Number(result.freedBytes ?? 0)} bytes`
|
|
@@ -166,7 +169,7 @@ async function runCloudBoxStop(args: string[], purge: boolean): Promise<number>
|
|
|
166
169
|
)
|
|
167
170
|
return 0
|
|
168
171
|
} catch (error) {
|
|
169
|
-
if (error instanceof
|
|
172
|
+
if (error instanceof BoxClientError) {
|
|
170
173
|
console.error(` ${rnx} box ${label}: ${error.message}`)
|
|
171
174
|
return 1
|
|
172
175
|
}
|
|
@@ -177,22 +180,29 @@ async function runCloudBoxStop(args: string[], purge: boolean): Promise<number>
|
|
|
177
180
|
async function runCloudBoxList(args: string[]): Promise<number> {
|
|
178
181
|
const rnx = rnxPublicBrand.commandName
|
|
179
182
|
const { header } = authHeaderOrExit('box ls')
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
const apiKey = header.replace(/^Bearer /, '')
|
|
184
|
+
const client = createBoxClient(
|
|
185
|
+
createCloudBoxProvider({
|
|
186
|
+
endpoint: flag(args, 'endpoint') ?? process.env[ENDPOINT_ENV]?.trim() ?? '',
|
|
182
187
|
apiOrigin: apiOrigin(args),
|
|
183
|
-
apiKey
|
|
184
|
-
})
|
|
188
|
+
apiKey,
|
|
189
|
+
}),
|
|
190
|
+
)
|
|
191
|
+
try {
|
|
192
|
+
const boxes = await client.list()
|
|
185
193
|
if (boxes.length === 0) {
|
|
186
194
|
console.log(` no cloud boxes running on this account`)
|
|
187
195
|
return 0
|
|
188
196
|
}
|
|
189
197
|
for (const box of boxes) {
|
|
190
198
|
const minutes = Math.round(box.meteredMs / 60_000)
|
|
191
|
-
console.log(
|
|
199
|
+
console.log(
|
|
200
|
+
` ${box.name ?? box.boxId} running ${minutes}m since ${box.startedAt}`,
|
|
201
|
+
)
|
|
192
202
|
}
|
|
193
203
|
return 0
|
|
194
204
|
} catch (error) {
|
|
195
|
-
if (error instanceof
|
|
205
|
+
if (error instanceof BoxClientError) {
|
|
196
206
|
console.error(` ${rnx} box ls: ${error.message}`)
|
|
197
207
|
return 1
|
|
198
208
|
}
|
|
@@ -205,6 +215,7 @@ async function runCloudBoxCreate(
|
|
|
205
215
|
opts: BoxCommandOptions,
|
|
206
216
|
): Promise<number> {
|
|
207
217
|
const rnx = rnxPublicBrand.commandName
|
|
218
|
+
const json = args.includes('--json')
|
|
208
219
|
const endpoint = flag(args, 'endpoint') ?? process.env[ENDPOINT_ENV]?.trim()
|
|
209
220
|
if (!endpoint) {
|
|
210
221
|
console.error(
|
|
@@ -215,6 +226,17 @@ async function runCloudBoxCreate(
|
|
|
215
226
|
return 1
|
|
216
227
|
}
|
|
217
228
|
|
|
229
|
+
const templateName = flag(args, 'template') ?? terminalBoxTemplate.name
|
|
230
|
+
let template: BoxTemplateResolution
|
|
231
|
+
try {
|
|
232
|
+
template = BOX_TEMPLATES.resolve({ name: templateName })
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error(
|
|
235
|
+
` ${rnx} box create: ${error instanceof Error ? error.message : String(error)}.`,
|
|
236
|
+
)
|
|
237
|
+
return 1
|
|
238
|
+
}
|
|
239
|
+
|
|
218
240
|
const root = findRepositoryPackageRoot(process.cwd())
|
|
219
241
|
if (!root) {
|
|
220
242
|
console.error(
|
|
@@ -224,7 +246,7 @@ async function runCloudBoxCreate(
|
|
|
224
246
|
return 1
|
|
225
247
|
}
|
|
226
248
|
|
|
227
|
-
const plane = new CheckoutFilePlane(root)
|
|
249
|
+
const plane = new CheckoutFilePlane(root, { trackedOnly: true })
|
|
228
250
|
try {
|
|
229
251
|
await plane.refresh()
|
|
230
252
|
} catch (error) {
|
|
@@ -242,36 +264,55 @@ async function runCloudBoxCreate(
|
|
|
242
264
|
const { header } = authHeaderOrExit('box create')
|
|
243
265
|
const apiKey = header.replace(/^Bearer /, '')
|
|
244
266
|
|
|
245
|
-
console.log(` ...creating cloud box ${name}`)
|
|
246
|
-
|
|
267
|
+
if (!json) console.log(` ...creating cloud box ${name}`)
|
|
268
|
+
const client = createBoxClient(
|
|
269
|
+
createCloudBoxProvider({ endpoint, apiOrigin: apiOrigin(args), apiKey }),
|
|
270
|
+
)
|
|
271
|
+
let session: BoxShellSession | null = null
|
|
247
272
|
let heartbeat: { stop: () => void } | null = null
|
|
248
273
|
try {
|
|
249
|
-
const
|
|
250
|
-
endpoint,
|
|
274
|
+
const box = await client.create({
|
|
251
275
|
name,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
276
|
+
stack: template.stack,
|
|
277
|
+
template: template.provenance,
|
|
278
|
+
source: plane,
|
|
279
|
+
onProgress:
|
|
280
|
+
opts.verbose && !json
|
|
281
|
+
? (seeded, total) => console.log(` ...seeded ${seeded}/${total} files`)
|
|
282
|
+
: undefined,
|
|
257
283
|
})
|
|
258
|
-
|
|
284
|
+
if (json) {
|
|
285
|
+
console.log(
|
|
286
|
+
JSON.stringify({
|
|
287
|
+
...box.description,
|
|
288
|
+
...(box.accessToken ? { accessToken: box.accessToken } : {}),
|
|
289
|
+
}),
|
|
290
|
+
)
|
|
291
|
+
return 0
|
|
292
|
+
}
|
|
293
|
+
console.log(` ...created Box ${box.id}`)
|
|
294
|
+
if (box.description.stableUrl) {
|
|
295
|
+
console.log(` ...live ${box.description.stableUrl}`)
|
|
296
|
+
}
|
|
297
|
+
console.log(` ...seeded ${box.description.files} project files`)
|
|
259
298
|
// shown once and never stored: this process does not own a credential
|
|
260
299
|
// store, and inventing one here would be a second place secrets live.
|
|
261
|
-
console.log(` ...box token (shown once): ${
|
|
300
|
+
console.log(` ...box token (shown once): ${box.accessToken}`)
|
|
262
301
|
console.log(` ...accessing shell\n`)
|
|
263
|
-
session = await
|
|
302
|
+
session = await box.connect()
|
|
264
303
|
// bank the wall clock while the shell is open. the box holds no account
|
|
265
304
|
// credential, so the client that is using it is what keeps the session
|
|
266
305
|
// alive; when this process dies the sweep closes it at the last heartbeat.
|
|
306
|
+
const sessionId = box.description.sessionId
|
|
307
|
+
if (!sessionId) throw new BoxClientError('the box service returned no session id')
|
|
267
308
|
heartbeat = heartbeatSession({
|
|
268
309
|
apiOrigin: apiOrigin(args),
|
|
269
310
|
apiKey,
|
|
270
|
-
sessionId
|
|
311
|
+
sessionId,
|
|
271
312
|
})
|
|
272
313
|
return await repl(session, '')
|
|
273
314
|
} catch (error) {
|
|
274
|
-
if (error instanceof
|
|
315
|
+
if (error instanceof BoxClientError) {
|
|
275
316
|
console.error(` ${rnx} box create: ${error.message}`)
|
|
276
317
|
return 1
|
|
277
318
|
}
|
|
@@ -311,13 +352,28 @@ async function runLocalBox(opts: BoxCommandOptions): Promise<number> {
|
|
|
311
352
|
throw error
|
|
312
353
|
}
|
|
313
354
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
355
|
+
let session: BoxShellSession | null = null
|
|
356
|
+
const client = createBoxClient(
|
|
357
|
+
createBoxHostProvider({
|
|
358
|
+
shell: {
|
|
359
|
+
commands: [
|
|
360
|
+
createRnxCommand({
|
|
361
|
+
root,
|
|
362
|
+
shellCwd: () => session?.cwd ?? PROJECT_MOUNT,
|
|
363
|
+
}),
|
|
364
|
+
],
|
|
365
|
+
// just-bash's JavaScript rg reads every file body through the plane.
|
|
366
|
+
// see search-command.ts for the numbers.
|
|
367
|
+
builtInCommandOverrides: [createSearchCommand({ root })],
|
|
368
|
+
},
|
|
369
|
+
}),
|
|
370
|
+
)
|
|
371
|
+
const box = await client.create({
|
|
372
|
+
name: path.basename(root),
|
|
373
|
+
stack: terminalBoxStack,
|
|
374
|
+
source: plane,
|
|
320
375
|
})
|
|
376
|
+
session = await box.connect()
|
|
321
377
|
|
|
322
378
|
const relative = path.relative(root, process.cwd())
|
|
323
379
|
console.log(` ...starting local box`)
|
|
@@ -362,26 +418,23 @@ async function runLocalBox(opts: BoxCommandOptions): Promise<number> {
|
|
|
362
418
|
|
|
363
419
|
try {
|
|
364
420
|
// the checkout is shared with editors, agents, and the user's own terminal.
|
|
365
|
-
//
|
|
421
|
+
// BoxProjectFs arms a stale bit on each shell turn and refreshes the
|
|
366
422
|
// plane synchronously on the first actual file access, so commands that
|
|
367
423
|
// never touch project files pay nothing, while reads always see fresh disk state.
|
|
368
|
-
return await repl(
|
|
424
|
+
return await repl(session, relative)
|
|
369
425
|
} finally {
|
|
426
|
+
session.close()
|
|
370
427
|
shared?.close()
|
|
371
428
|
}
|
|
372
429
|
}
|
|
373
430
|
|
|
374
431
|
// the REPL drives whatever holds the shell. locally that is an in-process
|
|
375
|
-
//
|
|
432
|
+
// BoxShell over this checkout; for a cloud box it is a socket to the box,
|
|
376
433
|
// which holds both the files and the shell. same loop, because the only
|
|
377
434
|
// difference is where exec runs.
|
|
378
|
-
interface BoxSession {
|
|
379
|
-
readonly cwd: string
|
|
380
|
-
exec(command: string): Promise<HarnessShellResult>
|
|
381
|
-
}
|
|
382
435
|
|
|
383
436
|
async function repl(
|
|
384
|
-
session:
|
|
437
|
+
session: BoxShellSession,
|
|
385
438
|
startRelative: string,
|
|
386
439
|
beforeCommand?: () => Promise<void>,
|
|
387
440
|
): Promise<number> {
|
package/cli/commands/config.ts
CHANGED
|
@@ -9,8 +9,9 @@ usage:
|
|
|
9
9
|
rnx config telemetry [on|off|status]
|
|
10
10
|
|
|
11
11
|
environment:
|
|
12
|
-
RNX_HINTS=off
|
|
13
|
-
RNX_HINTS=always
|
|
12
|
+
RNX_HINTS=off suppress CLI hints
|
|
13
|
+
RNX_HINTS=always show every CLI hint every time
|
|
14
|
+
RNX_UPDATE_CHECK=off disable background CLI update checks and notices
|
|
14
15
|
`)
|
|
15
16
|
}
|
|
16
17
|
|
package/cli/commands/control.ts
CHANGED
|
@@ -339,7 +339,7 @@ function buildShellPathOpenUrl(target: string, baseUrl: string): string {
|
|
|
339
339
|
return url.toString()
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
//
|
|
342
|
+
// rnx share/preview/build links (e.g. https://rnxsim.com/preview/<id>,
|
|
343
343
|
// https://contrast.dev/build/<owner>/<repo>/<branch>) are full player pages
|
|
344
344
|
// served by the Contrast app — NOT Metro bundles and NOT driveable shell sims.
|
|
345
345
|
// passing one to `rnx open` used to fall through to the bundle resolver,
|
|
@@ -942,7 +942,7 @@ export function printConnectedSims(sims: BridgeSimInfo[], selectedId?: string) {
|
|
|
942
942
|
|
|
943
943
|
// classify which engine a sim is running so dev-vs-published is never ambiguous
|
|
944
944
|
// to agents or humans reading `rnx list`. dev shell serves local source on
|
|
945
|
-
// :5173; the Contrast app embeds
|
|
945
|
+
// :5173; the Contrast app embeds rnx on :3000; the bridge/daemon and the
|
|
946
946
|
// /__soot plugin serve the active runtime dir — which, inside a dev checkout, is
|
|
947
947
|
// the fresh dev-stack build (resolveDevCheckoutRuntimeRoot mode boundary), not a
|
|
948
948
|
// published runtime. keep this label honest so it never says "published" while
|
|
@@ -1126,9 +1126,9 @@ async function waitForProcessExit(pid: number, timeoutMs = 2_500): Promise<boole
|
|
|
1126
1126
|
|
|
1127
1127
|
// a sim record's host pid can outlive its process; after pid reuse the number
|
|
1128
1128
|
// may point at an unrelated process (real incident 2026-07-14: a stale record
|
|
1129
|
-
// made `
|
|
1129
|
+
// made `rnx close` SIGTERM the shared vite shell dev server, taking the
|
|
1130
1130
|
// whole dev stack down). only signal a pid whose argv proves it is still a
|
|
1131
|
-
//
|
|
1131
|
+
// rnx playwright host launcher.
|
|
1132
1132
|
function isPlaywrightHostProcess(pid: number): boolean {
|
|
1133
1133
|
// `ps eww` prints argv then the process ENVIRONMENT for same-user
|
|
1134
1134
|
// processes. the marker must live in the env SUFFIX (everything past the
|
|
@@ -1530,9 +1530,9 @@ export async function runOpenCommand(
|
|
|
1530
1530
|
}
|
|
1531
1531
|
|
|
1532
1532
|
// metro HMR mode. interactive opens default to hot=true so edits hot-apply
|
|
1533
|
-
// through
|
|
1533
|
+
// through rnx's HmrClient — expo CLI's createBundleUrlSearchParams
|
|
1534
1534
|
// hardcodes hot=false on every launchAsset URL (a leftover toggle real iOS
|
|
1535
|
-
// sims tolerate but
|
|
1535
|
+
// sims tolerate but rnx couldn't until now), so without this flip the
|
|
1536
1536
|
// bundle prelude doesn't include metro's HMR runtime + react-refresh and
|
|
1537
1537
|
// edits silently no-op. `--driver playwright` (agent / CI runs) defaults
|
|
1538
1538
|
// to hot=false for deterministic e2e. explicit `--hot` / `--no-hmr` win.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// rnx perf cpu — capture sampled CPU traces from the page and every worker.
|
|
2
2
|
//
|
|
3
3
|
// the JS Self-Profiler API (`new Profiler()`) cannot be constructed in a
|
|
4
|
-
// dedicated-worker scope, and
|
|
4
|
+
// dedicated-worker scope, and rnx runs the guest app in the tenant worker,
|
|
5
5
|
// so the old in-worker approach was unsupported (F28). this implementation
|
|
6
6
|
// attaches Chromium's `Profiler` CDP domain to the page and worker targets —
|
|
7
7
|
// which returns the .cpuprofile shape directly for every execution context.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// target, `Target.setAutoAttach` to discover the worker child sessions, and
|
|
11
11
|
// route `Profiler.*` to each worker via its sessionId (flatten mode).
|
|
12
12
|
//
|
|
13
|
-
// the user's normal
|
|
13
|
+
// the user's normal rnx tab runs in plain Chrome without a debug port.
|
|
14
14
|
// the easiest way to get a profilable sim is to open one with a CDP port —
|
|
15
15
|
// it stays driveable over the WS bridge, so you can scroll/navigate while
|
|
16
16
|
// profiling (the playwright driver passes --remote-debugging-port through):
|
package/cli/commands/daemon.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// rnx daemon — manage the persistent
|
|
1
|
+
// rnx daemon — manage the persistent rnx bridge daemon
|
|
2
2
|
//
|
|
3
3
|
// wraps `rnx serve` so it starts automatically on login and restarts if
|
|
4
|
-
// it crashes. the agent is tied to whichever `
|
|
5
|
-
// registration time, so `npm update -g
|
|
4
|
+
// it crashes. the agent is tied to whichever `rnxsim` binary resolves at
|
|
5
|
+
// registration time, so `npm update -g rnxsim` + `rnx daemon restart`
|
|
6
6
|
// picks up the upgrade without touching the plist/unit file.
|
|
7
7
|
//
|
|
8
8
|
// `daemonInstall` is shared by `rnx setup` and the explicit
|
|
@@ -104,10 +104,10 @@ function resolveSootsimInvocation(): SootsimInvocation {
|
|
|
104
104
|
// launchd / systemd run agents with a stripped PATH (basically just
|
|
105
105
|
// /usr/bin:/bin:/usr/sbin:/sbin), so a `#!/usr/bin/env node` shebang
|
|
106
106
|
// resolves nothing and the daemon never starts. spawn the absolute node
|
|
107
|
-
// binary directly with the
|
|
107
|
+
// binary directly with the rnx entry script as an argument.
|
|
108
108
|
//
|
|
109
109
|
// process.execPath is whichever node ran the install command, which is
|
|
110
|
-
// also the node the user expects
|
|
110
|
+
// also the node the user expects rnx to run on.
|
|
111
111
|
const npxInvocation = resolveNpxDaemonInvocation()
|
|
112
112
|
if (npxInvocation) return npxInvocation
|
|
113
113
|
const entry = resolveSootsimEntryScript()
|
|
@@ -433,7 +433,7 @@ export async function daemonInstall({ port, force }: { port: number; force: bool
|
|
|
433
433
|
|
|
434
434
|
// user-scope systemd units are killed when the user's last session ends.
|
|
435
435
|
// on most distros the daemon then disappears at logout — the unit is
|
|
436
|
-
// "active" until you log off, then silently dead, and `
|
|
436
|
+
// "active" until you log off, then silently dead, and `rnx` looks
|
|
437
437
|
// broken when you ssh back in. `loginctl enable-linger` keeps the user
|
|
438
438
|
// bus alive across sessions, which is what every other long-running
|
|
439
439
|
// user agent (gpg-agent, ssh-agent --systemd) also relies on.
|
|
@@ -513,7 +513,7 @@ export function teardownLegacyMacDaemonArtifacts(
|
|
|
513
513
|
* and its support files (mac .app bundle, log dir). does NOT touch user
|
|
514
514
|
* data under ~/.rnx. idempotent: each entry is checked before remove.
|
|
515
515
|
* exported for reuse by the npm preuninstall hook (scripts/preuninstall.cjs)
|
|
516
|
-
* via the bundled CLI so a `npm rm -g
|
|
516
|
+
* via the bundled CLI so a `npm rm -g rnxsim` tears down the daemon before
|
|
517
517
|
* the bin disappears. */
|
|
518
518
|
export function teardownDaemonService(): TeardownReport {
|
|
519
519
|
const removed: string[] = []
|
package/cli/commands/debug.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// rnx debug — drive __sootsimDebug.* from the terminal
|
|
2
2
|
//
|
|
3
|
-
// connects to a running
|
|
3
|
+
// connects to a running rnx instance via the WebSocket bridge and forwards
|
|
4
4
|
// debug channel management, tree snapshots, and on-demand inspectors without
|
|
5
5
|
// requiring a browser console.
|
|
6
6
|
//
|
|
@@ -78,7 +78,7 @@ subcommands:
|
|
|
78
78
|
scroll <id>, scroll-hit <x> <y>,
|
|
79
79
|
hit <x> <y>, gesture <x> <y>
|
|
80
80
|
|
|
81
|
-
js <code> execute javascript in the running app.
|
|
81
|
+
js <code> execute javascript in the running app. rnx
|
|
82
82
|
is the canonical state object — reach into its
|
|
83
83
|
bridges / state / chrome / debug slots directly.
|
|
84
84
|
e.g. SootSim.bridges.test.findByText("Sign in")
|
package/cli/commands/detox.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
//
|
|
1
|
+
// rnx detox — run existing detox suites against rnx
|
|
2
2
|
//
|
|
3
3
|
// the command is a thin orchestration layer over `npx jest`:
|
|
4
4
|
// 1. resolves a test dir (cwd e2e|test/e2e|detox, or the flag)
|
|
5
|
-
// 2. makes sure
|
|
5
|
+
// 2. makes sure an rnx shell is reachable (auto-launches one if not)
|
|
6
6
|
// 3. spawns jest with our preset baked in so `import ... from 'detox'`
|
|
7
|
-
// resolves to the
|
|
7
|
+
// resolves to the rnx driver
|
|
8
8
|
//
|
|
9
9
|
// it deliberately shells out to the user's own jest so their jest version,
|
|
10
10
|
// transforms, and setupFiles keep working. the only thing we force is the
|
|
@@ -129,7 +129,7 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
|
|
|
129
129
|
rnxExit(1)
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
// auto-launch: make sure
|
|
132
|
+
// auto-launch: make sure an rnx shell is reachable before running jest.
|
|
133
133
|
// Reuse the Maestro discovery path: scan local dev servers.
|
|
134
134
|
// and `rnx open` the first match.
|
|
135
135
|
if (!noLaunch) {
|
|
@@ -227,7 +227,7 @@ export async function runDetox(args: string[], opts: RunDetoxOpts = {}): Promise
|
|
|
227
227
|
|
|
228
228
|
// jest owns the browsers, and it only reaps them from its own signal and exit
|
|
229
229
|
// handlers. this process is the one a caller has a pid for, so a SIGTERM aimed
|
|
230
|
-
// at `
|
|
230
|
+
// at `rnx detox` lands here and jest never hears about it: it is a
|
|
231
231
|
// grandchild (this -> npx -> jest), so it keeps running with no parent and
|
|
232
232
|
// every chrome it launched is orphaned for good. forward the signal instead of
|
|
233
233
|
// exiting out from under it, and let its exit code end this process.
|
|
@@ -362,8 +362,8 @@ function initDetoxProject(): void {
|
|
|
362
362
|
writeFileSync(
|
|
363
363
|
configPath,
|
|
364
364
|
`// rnx detox jest config.
|
|
365
|
-
// extends the
|
|
366
|
-
// to the
|
|
365
|
+
// extends the rnx preset, which rewrites \`import ... from 'detox'\`
|
|
366
|
+
// to the rnx driver and keeps your jest transforms intact.
|
|
367
367
|
|
|
368
368
|
/** @type {import('@jest/types').Config.InitialOptions} */
|
|
369
369
|
module.exports = {
|
package/cli/commands/diagnose.ts
CHANGED
package/cli/commands/electron.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// rnx desktop — launch the installed desktop companion via the
|
|
2
2
|
// electron driver. the driver wraps findDesktopCompanion +
|
|
3
3
|
// launchDesktopCompanion, so this command is a thin CLI surface that
|
|
4
4
|
// parses options, prompts for install, and delegates everything else.
|
package/cli/commands/film.ts
CHANGED
|
@@ -148,7 +148,7 @@ examples:
|
|
|
148
148
|
)
|
|
149
149
|
if (!started) {
|
|
150
150
|
console.error(
|
|
151
|
-
' film: engine recording store did not start (is @
|
|
151
|
+
' film: engine recording store did not start (is @rnx/plugin-recording installed?)',
|
|
152
152
|
)
|
|
153
153
|
return 1
|
|
154
154
|
}
|
package/cli/commands/flow.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// shared Maestro YAML playback and live-authoring implementation.
|
|
2
|
-
// discovers
|
|
2
|
+
// discovers an rnx target when needed, then reuses the shared bridge action layer
|
|
3
3
|
|
|
4
4
|
import * as fs from 'fs'
|
|
5
5
|
import { createHash, randomUUID } from 'node:crypto'
|
|
@@ -82,7 +82,7 @@ export async function resolveAppFrontmatterTarget(
|
|
|
82
82
|
return { target: trimmed }
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
// discover
|
|
85
|
+
// discover an rnx shell URL — prefer patched servers, fall back to any match.
|
|
86
86
|
// exported so `detox.ts` and `maestro.ts` can reuse the same discovery logic.
|
|
87
87
|
export async function discoverSootsimUrl(): Promise<string | null> {
|
|
88
88
|
const servers = await scanDevServers()
|
|
@@ -143,7 +143,7 @@ options:
|
|
|
143
143
|
--preview-origin <url>
|
|
144
144
|
upload target for --preview (default: auto)
|
|
145
145
|
--preview-public-origin <url>
|
|
146
|
-
public link origin for --preview (default:
|
|
146
|
+
public link origin for --preview (default: rnxsim.com
|
|
147
147
|
for prod uploads)
|
|
148
148
|
--billing-kind test_run
|
|
149
149
|
meter the chained preview upload as a test run
|
|
@@ -371,7 +371,7 @@ flow extension:
|
|
|
371
371
|
) {
|
|
372
372
|
const discovered = await discoverSootsimUrl()
|
|
373
373
|
if (!discovered) {
|
|
374
|
-
console.error(' error: no current sim and no
|
|
374
|
+
console.error(' error: no current sim and no rnx target found')
|
|
375
375
|
rnxExit(1)
|
|
376
376
|
}
|
|
377
377
|
openTarget = discovered
|