rnxsim 0.1.408 → 0.1.409

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +11 -10
  2. package/cli/bin.ts +16 -643
  3. package/cli/commands/assert.ts +6 -6
  4. package/cli/commands/box/rnx-command.ts +3 -11
  5. package/cli/commands/control.ts +3 -9
  6. package/cli/commands/daemon-mac-app.ts +5 -11
  7. package/cli/commands/daemon.ts +63 -113
  8. package/cli/drivers/playwright-provisioning.ts +33 -29
  9. package/cli/drivers/playwright-sim-host.ts +501 -0
  10. package/cli/drivers/playwright.ts +35 -522
  11. package/cli/hints.ts +2 -4
  12. package/cli/internal-child.ts +176 -0
  13. package/cli/maestro-js.ts +28 -39
  14. package/cli/main.ts +645 -0
  15. package/cli/outbound-endpoints.ts +8 -0
  16. package/cli/self-invocation.ts +34 -0
  17. package/dist-lib/agent-daemon-client.cjs +35 -25
  18. package/dist-lib/agent-events.cjs +1 -1
  19. package/dist-lib/agent-identity.cjs +1 -1
  20. package/dist-lib/agent-sessions.cjs +35 -25
  21. package/dist-lib/attached-projects.cjs +1 -1
  22. package/dist-lib/auth/shared-session.cjs +1 -1
  23. package/dist-lib/backend-origin.cjs +1 -1
  24. package/dist-lib/beta.cjs +1 -1
  25. package/dist-lib/beta.mjs +1 -1
  26. package/dist-lib/bridge-constants.cjs +1 -1
  27. package/dist-lib/bridge-contract-input.cjs +1 -1
  28. package/dist-lib/bridge-contract-input.mjs +1 -1
  29. package/dist-lib/bridge-contract.cjs +1 -1
  30. package/dist-lib/bridge-contract.mjs +1 -1
  31. package/dist-lib/capture-contract.cjs +1 -1
  32. package/dist-lib/capture-contract.mjs +1 -1
  33. package/dist-lib/cli-constants.cjs +1 -1
  34. package/dist-lib/cloud-contract.cjs +1 -4
  35. package/dist-lib/cloud-contract.mjs +1 -3
  36. package/dist-lib/config.cjs +1 -1
  37. package/dist-lib/detox/index.cjs +1 -1
  38. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  39. package/dist-lib/home-paths.cjs +1 -1
  40. package/dist-lib/host/bridge-host.cjs +25 -15
  41. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  42. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  43. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  44. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  45. package/dist-lib/host/websocket-proxy.cjs +1 -1
  46. package/dist-lib/index.cjs +1 -1
  47. package/dist-lib/jump-to-source-babel.cjs +1 -1
  48. package/dist-lib/menu.cjs +1 -1
  49. package/dist-lib/menu.mjs +1 -1
  50. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  51. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  52. package/dist-lib/metro-production-bundle.cjs +1 -1
  53. package/dist-lib/metro-production-bundle.mjs +1 -1
  54. package/dist-lib/metro.cjs +1 -1
  55. package/dist-lib/profiles.cjs +1 -1
  56. package/dist-lib/public-brand.cjs +1 -1
  57. package/dist-lib/react-native-host-modules.cjs +1 -1
  58. package/dist-lib/react-native-host-modules.mjs +1 -1
  59. package/dist-lib/render-mode.cjs +1 -1
  60. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  61. package/dist-lib/sdk.cjs +1 -1
  62. package/dist-lib/sdk.mjs +1 -1
  63. package/dist-lib/skills.cjs +134 -608
  64. package/dist-lib/vite.cjs +1 -1
  65. package/package.json +1 -1
  66. package/src/agent-daemon-client.ts +2 -2
  67. package/src/agent-sessions.ts +29 -24
  68. package/src/cloud-contract.ts +0 -1
  69. package/src/vite-plugin.ts +62 -0
package/cli/main.ts ADDED
@@ -0,0 +1,645 @@
1
+ // the rnx CLI proper: argv parsing, the one-time privacy choice, and the
2
+ // single lazy-imported command a run dispatches to. `./bin` imports this only
3
+ // when none of its hidden entry points claimed the process.
4
+
5
+ import { rnxPublicBrand } from '../src/public-brand'
6
+ import { parseArgs, TOP_LEVEL_RUNTIME_COMMANDS } from './parse-args'
7
+ import { RnxExit } from './run-rnx'
8
+ import { rnxSelfInvocation } from './self-invocation'
9
+ import { IS_STANDALONE } from './standalone'
10
+
11
+ // `./help` is loaded lazily (only on help paths) — it statically pulls the full
12
+ // CLI registry renderers and engine settings, which would otherwise add ~400ms
13
+ // of eager chunk load to every command, including hot-path reads like
14
+ // `describe` / `do tap` that never render help. parse-args has no imports, so
15
+ // the startup floor stays at the runtime's own cost (~15ms).
16
+
17
+ // keep top-level dispatch crashes readable. without this, a 10s WS bridge
18
+ // timeout (the most common failure when a sim is dead) prints a 1KB minified
19
+ // chunk of the bundled CLI source straight to stderr, which buries the actual
20
+ // "command timed out after 10s" message and is uncopyable for a bug report.
21
+ // rejections that don't match this shape still get the verbose stack so we
22
+ // don't paper over real bugs.
23
+ function handleTopLevelError(err: unknown): never {
24
+ const msg = err instanceof Error ? err.message : String(err)
25
+ // rnx's known top-level errors are all single-line, narrow strings.
26
+ // everything else gets the full stack so unexpected crashes still surface.
27
+ const isKnownCliFailure =
28
+ /^command timed out after \d+s$/.test(msg) ||
29
+ msg.startsWith('sim disconnected:') ||
30
+ msg.startsWith('bridge never reconnected') ||
31
+ msg.startsWith('multiple sims are connected:') ||
32
+ msg.startsWith('saved sim ') ||
33
+ msg.startsWith('no sim connected with id ') ||
34
+ msg.startsWith('could not connect to ws://') ||
35
+ msg.startsWith('rnx bridge daemon is not running') ||
36
+ msg.startsWith('rnx bridge lockfile is fresh') ||
37
+ msg.startsWith('rnx bridge exited before becoming ready') ||
38
+ msg.startsWith(`${rnxPublicBrand.commandName} open: requested bridge port`) ||
39
+ msg.startsWith('background daemon setup is unavailable') ||
40
+ msg.startsWith('rnx bridge did not start within')
41
+ if (isKnownCliFailure && !process.env.SOOTSIM_VERBOSE) {
42
+ process.stderr.write(` ${msg}\n`)
43
+ process.exit(1)
44
+ }
45
+ if (err instanceof Error && err.stack) {
46
+ process.stderr.write(`${err.stack}\n`)
47
+ } else {
48
+ process.stderr.write(`${msg}\n`)
49
+ }
50
+ process.exit(1)
51
+ }
52
+ process.on('unhandledRejection', handleTopLevelError)
53
+ process.on('uncaughtException', handleTopLevelError)
54
+
55
+ let parsed = parseArgs(process.argv)
56
+ for (const warning of parsed.warnings) process.stderr.write(`${warning}\n`)
57
+
58
+ // runtime actions and reads also work as hidden root aliases. resolve only
59
+ // after the normal parser declined the token, so a real top-level command
60
+ // always wins. keep this lazy so ordinary commands don't load the docs registry.
61
+ if (!parsed.command && parsed.commandArgs.length > 0) {
62
+ const { resolveHiddenRuntimeAlias } = await import('./hidden-runtime-alias')
63
+ parsed = resolveHiddenRuntimeAlias(parsed)
64
+ }
65
+
66
+ const isAutomaticCleanupWorker =
67
+ parsed.command === 'cleanup' &&
68
+ parsed.commandArgs.length === 1 &&
69
+ parsed.commandArgs[0] === '--automatic-worker'
70
+ const isCliUpdateWorker =
71
+ parsed.command === 'upgrade' &&
72
+ parsed.commandArgs.length === 1 &&
73
+ parsed.commandArgs[0] === '--check-worker'
74
+ const isBareWelcome = !parsed.command && parsed.commandArgs.length === 0
75
+ const isReadOnlyInvocation =
76
+ parsed.version || parsed.help || parsed.commandArgs.includes('--dry-run')
77
+
78
+ if (isCliUpdateWorker) {
79
+ if (IS_STANDALONE) {
80
+ try {
81
+ const { refreshCliUpdateCache } = await import('./cli-update')
82
+ await refreshCliUpdateCache()
83
+ } catch {}
84
+ }
85
+ process.exit(0)
86
+ }
87
+
88
+ if (
89
+ !isAutomaticCleanupWorker &&
90
+ !isBareWelcome &&
91
+ !parsed.help &&
92
+ !parsed.version &&
93
+ parsed.command !== 'config' &&
94
+ parsed.command !== 'setup' &&
95
+ parsed.command !== 'telemetry'
96
+ ) {
97
+ const { ensurePrivacyChoice } = await import('./privacy')
98
+ await ensurePrivacyChoice({ prompt: true })
99
+ }
100
+
101
+ // one-shot "engine upgraded in the background" banner. the daemon's hourly
102
+ // auto-updater writes a notice file when it swaps the runtime; whichever
103
+ // interactive command runs next prints it once (stderr, so --json stdout
104
+ // stays parseable) and deletes it. the daemon/serve processes skip it so a
105
+ // background process can't eat the notice before a human sees it.
106
+ if (!isReadOnlyInvocation && parsed.command !== 'serve' && parsed.command !== 'daemon') {
107
+ const { consumeRuntimeUpgradeNotice } = await import('../src/home-paths')
108
+ const notice = consumeRuntimeUpgradeNotice()
109
+ if (notice) {
110
+ process.stderr.write(
111
+ ` ${rnxPublicBrand.name} engine upgraded to v${notice.to}` +
112
+ (notice.from ? ` (from v${notice.from})` : '') +
113
+ ` · what's new: ${rnxPublicBrand.origin}/changelog\n`,
114
+ )
115
+ }
116
+ }
117
+
118
+ let automaticCleanupFinished = false
119
+ async function spawnDetachedSelf(
120
+ args: string[],
121
+ stderr: 'ignore' | 'inherit',
122
+ ): Promise<import('node:child_process').ChildProcess> {
123
+ const { spawn } = await import('node:child_process')
124
+ const options: import('node:child_process').SpawnOptions = {
125
+ detached: true,
126
+ stdio: ['ignore', 'ignore', stderr],
127
+ env: process.env,
128
+ }
129
+ const { executable, prefixArgs } = rnxSelfInvocation()
130
+ return spawn(executable, [...prefixArgs, ...args], options)
131
+ }
132
+
133
+ async function scheduleAutomaticCleanup(): Promise<void> {
134
+ if (automaticCleanupFinished) return
135
+ automaticCleanupFinished = true
136
+ if (
137
+ isReadOnlyInvocation ||
138
+ parsed.command === 'cleanup' ||
139
+ parsed.command === 'serve' ||
140
+ parsed.command === 'agent-wrapper' ||
141
+ (parsed.command === 'daemon' && parsed.commandArgs[0] === 'uninstall')
142
+ ) {
143
+ return
144
+ }
145
+
146
+ try {
147
+ const { shouldSkipAutomaticSootsimCleanup } = await import('../src/disk-cleanup')
148
+ if (shouldSkipAutomaticSootsimCleanup()) return
149
+
150
+ const workerArgs = ['cleanup', '--automatic-worker']
151
+ const child = await spawnDetachedSelf(workerArgs, 'inherit')
152
+ child.once('error', (error) => {
153
+ process.stderr.write(
154
+ ` ${rnxPublicBrand.name} automatic cleanup will retry on the next run: ${error.message}\n`,
155
+ )
156
+ })
157
+ child.unref()
158
+ } catch (error) {
159
+ process.stderr.write(
160
+ ` ${rnxPublicBrand.name} automatic cleanup will retry on the next run: ${
161
+ error instanceof Error ? error.message : String(error)
162
+ }\n`,
163
+ )
164
+ }
165
+ }
166
+
167
+ let automaticCliUpdateFinished = false
168
+ async function scheduleAutomaticCliUpdate(exitCode: number): Promise<void> {
169
+ if (automaticCliUpdateFinished) return
170
+ automaticCliUpdateFinished = true
171
+ if (!IS_STANDALONE) return
172
+ const {
173
+ claimAutomaticCliUpdateCheck,
174
+ shouldUseAutomaticCliUpdateCheck,
175
+ takeCliUpdateNotification,
176
+ } = await import('./cli-update')
177
+ if (
178
+ !shouldUseAutomaticCliUpdateCheck({
179
+ command: parsed.command,
180
+ commandArgs: parsed.commandArgs,
181
+ exitCode,
182
+ standalone: IS_STANDALONE,
183
+ stderrIsTTY: process.stderr.isTTY === true,
184
+ stdoutIsTTY: process.stdout.isTTY === true,
185
+ version: parsed.version,
186
+ })
187
+ ) {
188
+ return
189
+ }
190
+
191
+ const update = takeCliUpdateNotification()
192
+ if (update) {
193
+ process.stderr.write(
194
+ ` update: rnx v${update.currentVersion} → v${update.latestVersion} · run \`rnx upgrade\`\n`,
195
+ )
196
+ }
197
+ if (!claimAutomaticCliUpdateCheck()) return
198
+ try {
199
+ const child = await spawnDetachedSelf(['upgrade', '--check-worker'], 'ignore')
200
+ child.once('error', () => {})
201
+ child.unref()
202
+ } catch {}
203
+ }
204
+
205
+ async function exitWithFlush(code: number): Promise<never> {
206
+ const { flushCliTelemetry } = await import('./telemetry')
207
+ await flushCliTelemetry()
208
+ await scheduleAutomaticCliUpdate(code)
209
+ await scheduleAutomaticCleanup()
210
+ process.exit(code)
211
+ }
212
+
213
+ // --version
214
+ if (parsed.version) {
215
+ // use the single canonical, cached version source — the same one
216
+ // `--help`, `upgrade`, the startup flow, and the bridge-host use. it
217
+ // previously re-read package.json independently here, which could
218
+ // resolve a different copy (stale global install vs repo) and drift
219
+ // from `--help` within one session (QA F20-4).
220
+ const { getCliVersion } = await import('../src/cli-version')
221
+ const { IS_BETA, BETA_LABEL } = await import('../src/beta')
222
+ const suffix = IS_BETA ? ` · ${BETA_LABEL}` : ''
223
+ console.log(`${rnxPublicBrand.commandName} v${getCliVersion()}${suffix}`)
224
+ // runtime version on its own line — keeps the first line parseable while
225
+ // surfacing the (independently-versioned) engine runtime users actually
226
+ // render with. see `rnx upgrade`.
227
+ const { readActiveRuntime } = await import('../src/home-paths')
228
+ const runtime = readActiveRuntime()
229
+ console.log(runtime ? `runtime v${runtime}` : 'runtime not installed')
230
+ await exitWithFlush(0)
231
+ }
232
+
233
+ // --help with no command
234
+ if (parsed.help && !parsed.command) {
235
+ const { printHelp } = await import('./help')
236
+ printHelp()
237
+ await exitWithFlush(0)
238
+ }
239
+
240
+ // apply settings from global flags
241
+ const port = parsed.globalFlags['port'] as number | undefined
242
+ const verbose = parsed.verbose
243
+ const device = parsed.globalFlags['device'] as string | undefined
244
+ const theme = parsed.globalFlags['theme'] as string | undefined
245
+ const driver = parsed.globalFlags['driver'] as string | undefined
246
+ const headless = (parsed.globalFlags['headless'] as boolean | undefined) === true
247
+ const globalSimTarget = (parsed.globalFlags['sim'] ??
248
+ parsed.globalFlags['session'] ??
249
+ parsed.globalFlags['tab']) as string | undefined
250
+ const commandArgsWithSim = globalSimTarget
251
+ ? ['--sim', globalSimTarget, ...parsed.commandArgs]
252
+ : parsed.commandArgs
253
+
254
+ // apply device/theme to settings store if provided
255
+ if (device || theme) {
256
+ const { settingsStore } = await import('sootsim-engine/settings/store')
257
+ const overrides: Record<string, any> = {}
258
+ if (device) overrides.deviceModel = device
259
+ if (theme) overrides.colorScheme = theme
260
+ settingsStore.apply(overrides)
261
+ }
262
+
263
+ // no command, no args: guide first-time machine setup, then open ConnectRN.
264
+ // returning runs open ConnectRN directly; app discovery belongs inside it.
265
+ if (!parsed.command && parsed.commandArgs.length === 0) {
266
+ const { runWelcome } = await import('./commands/welcome')
267
+ await exitWithFlush(await runWelcome())
268
+ }
269
+
270
+ // route to command. an empty command at this point only happens if the
271
+ // caller passed bare positional args without a verb (e.g. `rnx -- foo`),
272
+ // which used to fall through to the bundler-wrap behavior in `dev`. now that
273
+ // `dev` is gone, treat it as "show help".
274
+ const command = parsed.command ?? ''
275
+ if (!command) {
276
+ // a bare positional that isn't a known command lands here (parse-args
277
+ // leaves `command` null and pushes the token into commandArgs). don't
278
+ // silently dump the full help banner — name the unrecognized token so a
279
+ // typo'd subcommand is obvious. matches the switch `default:` wording.
280
+ if (parsed.commandArgs.length > 0) {
281
+ const attempted =
282
+ parsed.commandArgs.find((a) => !a.startsWith('-')) ?? parsed.commandArgs[0]
283
+ console.error(` unknown command: ${attempted}`)
284
+ console.error(
285
+ ` run \`${rnxPublicBrand.commandName} --help\` to see the full surface.`,
286
+ )
287
+ await exitWithFlush(1)
288
+ }
289
+ const { printHelp } = await import('./help')
290
+ printHelp()
291
+ await exitWithFlush(0)
292
+ }
293
+
294
+ // --help with a command (either global flag or in commandArgs)
295
+ if (
296
+ parsed.help ||
297
+ parsed.commandArgs.includes('--help') ||
298
+ parsed.commandArgs.includes('-h')
299
+ ) {
300
+ if (command === 'skill') {
301
+ const { runSkill } = await import('./commands/skills')
302
+ await runSkill(parsed.commandArgs)
303
+ await exitWithFlush(0)
304
+ }
305
+ if (command === 'state') {
306
+ const { runState } = await import('./commands/state')
307
+ await exitWithFlush(await runState(commandArgsWithSim, { port }))
308
+ }
309
+ if (command === 'reset') {
310
+ const { runReset } = await import('./commands/reset')
311
+ await exitWithFlush(await runReset(commandArgsWithSim, { port }))
312
+ }
313
+ // for grouping verbs, try to resolve per-verb help from the first
314
+ // positional arg. e.g. `rnx do tap --help` shows tap's help page,
315
+ // not the generic `do` grouping page. bare `rnx do --help` falls
316
+ // through to runInspect which has the full verb group listing.
317
+ const isGroupingVerb =
318
+ command === 'do' ||
319
+ command === 'get' ||
320
+ command === 'debug' ||
321
+ command === 'shell' ||
322
+ command === 'perf' ||
323
+ command === 'wait'
324
+ const subVerb = parsed.commandArgs.find((a) => !a.startsWith('-'))
325
+ if (command === 'shell' || command === 'perf') {
326
+ // shell and perf subcommands are documented by their runtime dispatchers.
327
+ // don't try to route through generated per-verb docs.
328
+ } else if (isGroupingVerb && !subVerb) {
329
+ // bare `rnx <group> --help` — render from the registry.
330
+ const { printGroupHelp } = await import('./help')
331
+ if (printGroupHelp(command)) await exitWithFlush(0)
332
+ // fall through — runInspect shows the full help for anything the
333
+ // registry doesn't yet cover (e.g. shell).
334
+ } else {
335
+ const { printCommandHelp } = await import('./help')
336
+ const helpName = isGroupingVerb && subVerb ? subVerb : command
337
+ printCommandHelp(helpName, {
338
+ prefer: isGroupingVerb && subVerb ? 'verb' : 'command',
339
+ group: isGroupingVerb && subVerb ? command : undefined,
340
+ })
341
+ await exitWithFlush(0)
342
+ }
343
+ }
344
+
345
+ const { dispatchCloudBoundary } = await import('./cloud-dispatch')
346
+ const cloudBoundary = await dispatchCloudBoundary({
347
+ command,
348
+ args: commandArgsWithSim,
349
+ })
350
+ if (cloudBoundary.handled) await exitWithFlush(cloudBoundary.code)
351
+ // the command modules stop by throwing RnxExit rather than ending the
352
+ // process, because the same command has to run in a box shell and in
353
+ // workerd, where process.exit() cancels the whole request. this is the
354
+ // host boundary that turns it back into a real exit code.
355
+ try {
356
+ if (TOP_LEVEL_RUNTIME_COMMANDS.has(command)) {
357
+ if (command === 'do') {
358
+ const { hasDoChain, runDoChain } = await import('./commands/do-chain')
359
+ if (hasDoChain(commandArgsWithSim)) {
360
+ await exitWithFlush(await runDoChain(commandArgsWithSim, { port }))
361
+ }
362
+ }
363
+ const { runInspect } = await import('./commands/inspect')
364
+ await runInspect([command, ...commandArgsWithSim], { port, verbose })
365
+ } else {
366
+ switch (command) {
367
+ case 'assert': {
368
+ // stays outside the usual dispatcher — it spawns self to get the
369
+ // inner verb's --json payload, so it never needs the shared bridge
370
+ // machinery that lives inside runInspect.
371
+ const { runAssert } = await import('./commands/assert')
372
+ await runAssert(parsed.commandArgs)
373
+ break
374
+ }
375
+
376
+ case 'detox': {
377
+ const { runDetox } = await import('./commands/detox')
378
+ await runDetox(parsed.commandArgs, { port, verbose })
379
+ break
380
+ }
381
+
382
+ case 'maestro': {
383
+ const { runMaestro } = await import('./commands/maestro')
384
+ // Maestro forwards playback argv to the shared runner, so it must
385
+ // receive the global `--sim`. passing the bare
386
+ // parsed.commandArgs dropped an explicit `rnx --sim <id> maestro
387
+ // …`, silently falling back to the current/saved sim (QA F21-4).
388
+ const code = await runMaestro(commandArgsWithSim, { port, verbose })
389
+ // bridge + dynamic imports leave handles open that keep node alive.
390
+ await exitWithFlush(typeof code === 'number' ? code : 0)
391
+ }
392
+
393
+ case 'record': {
394
+ const { runRecord } = await import('./commands/record')
395
+ await runRecord(commandArgsWithSim, { port, verbose })
396
+ break
397
+ }
398
+
399
+ case 'film': {
400
+ const { runFilm } = await import('./commands/film')
401
+ // film chains the flow runner, which opens ws bridges + dynamic
402
+ // imports that keep node alive — exit explicitly like `flow` does.
403
+ const code = await runFilm(commandArgsWithSim, { port, verbose })
404
+ await exitWithFlush(typeof code === 'number' ? code : 0)
405
+ }
406
+
407
+ case 'storage': {
408
+ const { runStorage } = await import('./commands/storage')
409
+ const code = await runStorage(commandArgsWithSim, { port, verbose })
410
+ await exitWithFlush(typeof code === 'number' ? code : 0)
411
+ }
412
+
413
+ case 'state': {
414
+ const { runState } = await import('./commands/state')
415
+ const code = await runState(commandArgsWithSim, { port })
416
+ await exitWithFlush(code)
417
+ }
418
+
419
+ case 'reset': {
420
+ const { runReset } = await import('./commands/reset')
421
+ const code = await runReset(commandArgsWithSim, { port })
422
+ await exitWithFlush(code)
423
+ }
424
+
425
+ case 'perf': {
426
+ const { runPerf } = await import('./commands/perf')
427
+ const code = await runPerf(commandArgsWithSim, { port, verbose })
428
+ await exitWithFlush(typeof code === 'number' ? code : 0)
429
+ }
430
+
431
+ case 'screenshot': {
432
+ const { runScreenshotCommand } = await import('./commands/screenshot-command')
433
+ const code = await runScreenshotCommand(commandArgsWithSim, { port, verbose })
434
+ await exitWithFlush(typeof code === 'number' ? code : 0)
435
+ }
436
+
437
+ case 'camera': {
438
+ const { runCamera } = await import('./commands/camera')
439
+ const code = await runCamera(commandArgsWithSim, { port })
440
+ await exitWithFlush(typeof code === 'number' ? code : 0)
441
+ }
442
+
443
+ case 'mode': {
444
+ const { runMode } = await import('./commands/mode')
445
+ await runMode(commandArgsWithSim, { port, verbose })
446
+ break
447
+ }
448
+
449
+ case 'permissions': {
450
+ const { runPermissions } = await import('./commands/permissions')
451
+ const code = await runPermissions(commandArgsWithSim, { port, verbose })
452
+ await exitWithFlush(code)
453
+ }
454
+
455
+ case 'debug': {
456
+ const { runDebug } = await import('./commands/debug')
457
+ await runDebug(commandArgsWithSim, { port, verbose })
458
+ break
459
+ }
460
+
461
+ case 'timeline': {
462
+ const { runTimeline } = await import('./commands/timeline')
463
+ await runTimeline(commandArgsWithSim, { port, verbose })
464
+ break
465
+ }
466
+
467
+ case 'what-happened': {
468
+ const { runWhatHappened } = await import('./commands/what-happened')
469
+ await runWhatHappened(commandArgsWithSim, { port, verbose })
470
+ break
471
+ }
472
+
473
+ case 'open': {
474
+ const { runOpenCommand } = await import('./commands/control')
475
+ await runOpenCommand(commandArgsWithSim, { port })
476
+ break
477
+ }
478
+
479
+ case 'ios':
480
+ case 'android': {
481
+ const { runPlatformCommand } = await import('./commands/platform')
482
+ const code = await runPlatformCommand(command, commandArgsWithSim, { port })
483
+ await exitWithFlush(typeof code === 'number' ? code : 0)
484
+ }
485
+
486
+ case 'web': {
487
+ const { getRnxCommandAvailability } = await import('./command-registry')
488
+ const availability = getRnxCommandAvailability('web', parsed.commandArgs, 'node')
489
+ console.error(` ${availability.reason ?? 'rnx web is unavailable'}`)
490
+ await exitWithFlush(1)
491
+ }
492
+
493
+ case 'box': {
494
+ const { runBox } = await import('./commands/box')
495
+ await exitWithFlush(await runBox(commandArgsWithSim, { port, verbose }))
496
+ }
497
+
498
+ case 'use': {
499
+ const { runUseCommand } = await import('./commands/control')
500
+ await runUseCommand(commandArgsWithSim, { port })
501
+ break
502
+ }
503
+
504
+ case 'claim': {
505
+ const { runClaimCommand } = await import('./commands/control')
506
+ await runClaimCommand(commandArgsWithSim, { port })
507
+ break
508
+ }
509
+
510
+ case 'close': {
511
+ const { runCloseCommand } = await import('./commands/control')
512
+ await runCloseCommand(commandArgsWithSim, { port })
513
+ break
514
+ }
515
+
516
+ case 'device': {
517
+ const { runDeviceCommand } = await import('./commands/device')
518
+ await runDeviceCommand(commandArgsWithSim, { port })
519
+ break
520
+ }
521
+
522
+ case 'compat': {
523
+ const { runCompat } = await import('./commands/compat')
524
+ await runCompat(parsed.commandArgs)
525
+ break
526
+ }
527
+
528
+ case 'report-issue': {
529
+ const { runReportIssue } = await import('./commands/report-issue')
530
+ const code = await runReportIssue(parsed.commandArgs)
531
+ await exitWithFlush(code)
532
+ }
533
+
534
+ case 'desktop': {
535
+ const { runDesktop } = await import('./commands/desktop')
536
+ await runDesktop(parsed.commandArgs, { port, device })
537
+ break
538
+ }
539
+
540
+ case 'login': {
541
+ const { runLogin } = await import('./commands/login')
542
+ await runLogin(parsed.commandArgs)
543
+ break
544
+ }
545
+
546
+ case 'logout': {
547
+ const { runLogout } = await import('./commands/logout')
548
+ await runLogout()
549
+ break
550
+ }
551
+
552
+ case 'auth': {
553
+ const { runAuth } = await import('./commands/auth')
554
+ await runAuth(parsed.commandArgs)
555
+ break
556
+ }
557
+
558
+ case 'setup': {
559
+ const { runSetup } = await import('./commands/setup')
560
+ await runSetup(parsed.commandArgs)
561
+ break
562
+ }
563
+
564
+ case 'serve': {
565
+ const { runServe } = await import('./commands/serve')
566
+ await runServe(parsed.commandArgs, { port })
567
+ break
568
+ }
569
+
570
+ case 'daemon': {
571
+ const { runDaemon } = await import('./commands/daemon')
572
+ await runDaemon(parsed.commandArgs, { port })
573
+ break
574
+ }
575
+
576
+ case 'runtime': {
577
+ const { runRuntime } = await import('./commands/runtime')
578
+ await runRuntime(parsed.commandArgs)
579
+ break
580
+ }
581
+
582
+ case 'upgrade': {
583
+ const { runUpgrade } = await import('./commands/upgrade')
584
+ await runUpgrade(parsed.commandArgs)
585
+ break
586
+ }
587
+
588
+ case 'version': {
589
+ const { runVersion } = await import('./commands/version')
590
+ await runVersion(parsed.commandArgs)
591
+ break
592
+ }
593
+
594
+ case 'agent': {
595
+ const { runAgentCommand } = await import('./commands/agent')
596
+ const code = await runAgentCommand(parsed.commandArgs)
597
+ await exitWithFlush(code)
598
+ }
599
+
600
+ case 'agent-wrapper': {
601
+ const { runAgentWrapper } = await import('./commands/agent-wrapper')
602
+ const code = await runAgentWrapper(parsed.commandArgs)
603
+ await exitWithFlush(code)
604
+ }
605
+
606
+ case 'skill': {
607
+ const { runSkill } = await import('./commands/skills')
608
+ await runSkill(parsed.commandArgs)
609
+ break
610
+ }
611
+
612
+ case 'app-fonts': {
613
+ const { runAppFonts } = await import('./commands/app-fonts')
614
+ await runAppFonts(parsed.commandArgs)
615
+ break
616
+ }
617
+
618
+ case 'config': {
619
+ const { runConfig } = await import('./commands/config')
620
+ await runConfig(parsed.commandArgs)
621
+ break
622
+ }
623
+
624
+ case 'cleanup': {
625
+ const { runCleanup } = await import('./commands/cleanup')
626
+ const code = await runCleanup(parsed.commandArgs)
627
+ await exitWithFlush(code)
628
+ }
629
+
630
+ default: {
631
+ console.error(` unknown command: ${command}`)
632
+ console.error(
633
+ ` run \`${rnxPublicBrand.commandName} --help\` to see the full surface.`,
634
+ )
635
+ await exitWithFlush(1)
636
+ }
637
+ }
638
+ }
639
+ } catch (error) {
640
+ if (error instanceof RnxExit) await exitWithFlush(error.code)
641
+ throw error
642
+ }
643
+
644
+ await scheduleAutomaticCliUpdate(0)
645
+ await scheduleAutomaticCleanup()
@@ -131,6 +131,12 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
131
131
  { category: 'rnx_cloud_sim', count: 1 },
132
132
  'packages/sootsim/cli/cloud-client.ts :: fetch :: `${this.session.apiOrigin}/v1/sims/${encodeURIComponent(this.session.simId)}/commands`':
133
133
  { category: 'rnx_cloud_sim', count: 1 },
134
+ // maestro's flow `http` API. the url is whatever the flow author wrote, and
135
+ // it is fetched from a child process because that API is synchronous.
136
+ 'packages/sootsim/cli/internal-child.ts :: fetch :: request.url': {
137
+ category: 'guest_app_network',
138
+ count: 1,
139
+ },
134
140
  // `rnx box create` uploads the checkout to the box service and then talks to
135
141
  // it. the destination is whatever the user named; there is no default.
136
142
  'packages/rnx-cloud-box/src/client.ts :: fetch :: endpointOf(base, name, verb, search)':
@@ -187,6 +193,8 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
187
193
  category: 'local_development',
188
194
  count: 1,
189
195
  },
196
+ 'packages/sootsim/scripts/smoke-cli-binary.ts :: fetch :: `http://127.0.0.1:${port}/healthz`':
197
+ { category: 'local_development', count: 1 },
190
198
  'packages/sootsim-engine/src/auth/openLogin.ts :: fetch :: `${CONTRAST_ORIGIN}/api/dev-login`':
191
199
  { category: 'contrast_user_action', count: 1 },
192
200
  'packages/sootsim-engine/src/auth/shared-session.ts :: fetch :: `${CONTRAST_ORIGIN}/api/auth/me`':