# Peach

The source owner for the standalone `rnx` executable and the public `rnxsim`
integration library used to drive a browser-native React Native simulator.

## Overview

The CLI and library are separate public artifacts. The recommended installer
downloads one Bun-compiled `rnx` executable that needs no Node.js, npm, or Bun
installation. The `rnxsim` npm package contains bundler plugins, testing
drivers, and library exports, with no command-line binary.

The executable updates itself with `rnx upgrade`. Successful interactive human
commands refresh a cached stable CLI version in a detached process at most once
every 20 hours. A newer version produces one short notice on stderr, once per
version and no more than weekly. CI, agents, JSON output, pipes, and
noninteractive commands stay silent. Set `RNX_UPDATE_CHECK=off` to disable the
check and notice; `rnx version` checks explicitly, while `rnx --version` remains
offline.

Neither artifact contains rendering code. The canvas engine (CanvasKit
renderer, Yoga layout, iOS/Android shell chrome, Electron shell) lives in the
private `sootsim-engine` workspace package and is shipped at runtime as a
versioned tarball, fetched from the contrast.dev CDN and unpacked into `~/.rnx`.

What this workspace gives you:

- the standalone `rnx` CLI source and binary build, which provides the primary
  debugging and automation surface (inspect the UI tree, tap elements, capture
  flows, screenshot, record, run agents)
- `rnxsim/vite` and `rnxsim/metro` — bundler plugins that serve the installed
  engine runtime from your own dev server
- `rnxsim/jump-to-source-babel` — an optional Babel plugin that annotates JSX
  with source locations so inspect mode can open the selected element in your
  editor
- `rnxsim/detox` — a drop-in Detox driver + jest preset
- `rnxsim/sdk` — the inspect/interact verbs as a programmatic API
- `rnxsim/bridge-contract-input` — parsers turning untrusted JSON into
  bridge-contract arguments
- `rnxsim/skills` — a registry of Contrast-flavored automation skills

Platform status: iOS is the parity baseline. Android is bootstrapped and usable
for focused conformance slices (bundle/runtime identity, device profiles, shell
chrome, gesture + three-button nav, system UI/window metrics with cutout-safe
insets) but is not full parity until the Android launch gates in the Contrast
repo plan pass.

## What's in each distribution

| standalone `rnx` executable | `rnxsim` npm package | private engine runtime |
| --- | --- | --- |
| CLI commands and bridge client | Vite and Metro plugins | CanvasKit renderer and Yoga layout |
| bridge daemon host | SDK and bridge contracts | iOS/Android shell chrome, home grid, app switcher |
| runtime, browser, and desktop launchers | Detox driver and Jest preset | Electron shell and wasm |
| Maestro, screenshots, recording, skills | native-resolution helpers and skills registry | the actual pixels |

The engine is delivered at runtime, never bundled into this package. A
committed-but-unbuilt engine fix has no effect on a running sim until the
runtime tarball is rebuilt and re-fetched.

## Architecture

The architecture is a **three-tier relay**: a short-lived CLI process, a single
persistent bridge daemon, and the sim itself — a browser/Electron *page* that
internally splits into shell, compositor, and tenant workers.

```mermaid
graph TD
  subgraph cli["rnx CLI process (short-lived, cli/)"]
    BIN["bin.ts (dispatcher + privacy choice)"]
    SETUP["commands/setup.ts (guided daemon setup)"]
    RUNTIMECMD["commands/runtime.ts (runtime install/use)"]
    INSPECT["commands/inspect/* (describe/find/do/get/wait)"]
    FLOW["bridge-flow-runner.ts (Maestro YAML)"]
    DETOXCMD["commands/detox.ts + detox/ driver"]
    SHOTS["commands/screenshot + record"]
    AGENTCMD["commands/agent.ts"]
    WSB["ws-bridge.ts (WsBridge client)"]
    DRIVERS["drivers/* (chromium/electron/playwright/system)"]
  end
  subgraph daemon["bridge daemon process (persistent :7668)"]
    HOST["SootSimBridgeHost (host/bridge-host.ts, HTTP+WS)"]
    AGENTHOST["AgentHost (host/agent-host.ts, FIFO fan-out)"]
    AGENTSESS["agent-sessions.ts + attached-projects.ts"]
    SCAN["/__server-scan (dev-server-scanner.ts)"]
    PROXY["fetch-proxy-handler + websocket-proxy"]
    RTHTTP["runtime HTTP + self-update (runtime-delivery.ts)"]
  end
  subgraph sim["sim page (browser / Electron, launched by a driver)"]
    SHELLW["shell worker (iOS chrome, home grid, app switcher)"]
    COMPOSITORW["compositor worker (app canvases + independent rAF)"]
    TENANTW["tenant worker (guest RN app tree)"]
  end
  subgraph build["build-time plugins (src/)"]
    VITEONE["rnxsim/vite = vite-plugin-one.ts (serve runtime at /__soot/)"]
    VITERN["sootsim() = vite-plugin.ts (RN resolver + native stubs)"]
    METRO["rnxsim/metro = metro-plugin.ts"]
    COMPAT["@sootsim/compat stub-manifest (native seams)"]
    ENGINESHIM["sootsim-engine react-native shim"]
  end
  SKILLS["rnxsim/skills registry.ts (builtin skills)"]
  subgraph fs["~/.rnx (home-paths.ts)"]
    RTDIR["runtimes/<version>/ (engine assets)"]
    LOCK["daemon.json lockfile"]
    CFG["config.json"]
  end
  CDN["contrast.dev CDN (runtimes/manifest.json + tarballs)"]
  BIN --> SETUP
  BIN --> RUNTIMECMD
  BIN --> INSPECT
  BIN --> FLOW
  BIN --> DETOXCMD
  BIN --> SHOTS
  BIN --> AGENTCMD
  SETUP --> LOCK
  SETUP -->|"register launchd/systemd"| HOST
  RUNTIMECMD --> RTDIR
  RUNTIMECMD -->|"fetch runtime"| CDN
  INSPECT --> WSB
  FLOW --> WSB
  SHOTS --> WSB
  AGENTCMD --> WSB
  DETOXCMD --> DRIVERS
  DETOXCMD --> WSB
  WSB -->|"JSON WS cmds"| HOST
  HOST -->|"forward to registered sim"| SHELLW
  SHELLW -->|"replies + pushes"| HOST
  HOST -->|"relayed results"| WSB
  HOST --> AGENTHOST
  AGENTHOST --> AGENTSESS
  AGENTCMD --> AGENTHOST
  HOST --> SCAN
  HOST --> PROXY
  HOST --> RTHTTP
  RTHTTP --> RTDIR
  RTHTTP -->|"manifest + tarball"| CDN
  DRIVERS -->|"open sim page URL"| SHELLW
  SHELLW -->|"NativeUIRequest"| TENANTW
  VITEONE -->|"serve /__soot/ from"| RTDIR
  METRO -->|"serve /__soot/ from"| RTDIR
  VITEONE -.->|"launch electron app"| SHELLW
  VITERN -->|"alias react-native"| ENGINESHIM
  VITERN -->|"alias native pkgs"| COMPAT
  VITERN -->|"dev server hosts"| HOST
  SKILLS --> INSPECT
```

**The CLI process** (`cli/bin.ts` routes, `cli/main.ts` parses argv) lazy-imports
exactly one command module per run. Bare `rnx` opens ConnectRN, whose simulator UI discovers
running React Native development servers. The first human run offers the
recommended background service and optional Desktop app, then opens the simulator
before asking about private diagnostics or an optional repository install. `rnx open`
ensures the engine runtime exists and starts a local foreground bridge when no
daemon is reachable, then opens a `WsBridge` (`cli/ws-bridge.ts`) on **port
7668** and sends JSON commands. Inspection/interaction verbs live in
`cli/commands/inspect/*` and are re-exported as the programmatic `rnxsim/sdk`;
the Maestro runner is `cli/bridge-flow-runner.ts`; the Detox compat driver
is in `detox/`. Test and screenshot paths additionally use `cli/drivers/*`.
Browser sims launch in an isolated Playwright Chrome for Testing profile;
Electron is the native desktop surface.

**The daemon is one process** — `SootSimBridgeHost` (`src/host/bridge-host.ts`).
`rnx serve` runs it in the foreground; the background daemon is the *same
process* wrapped with autostart and the `~/.rnx/daemon.json` lockfile. It is
an HTTP+WS hub: sims register over WS, CLI clients connect to drive them, and
the host forwards each command to the targeted sim and relays the reply back. It
also serves the engine runtime over HTTP (with a self-update route), exposes
`/__server-scan` to discover local metro/expo/vxrn/one dev servers, and proxies
guest-app fetch/WebSocket traffic so the cross-origin tenant worker can reach
localhost. The `AgentHost` extension owns one FIFO reader per agent session and
fans agent events out to every subscriber.

During source development, each Vite bridge owns one
`~/.rnx/dev-bridges/<port>.json` heartbeat record. CLI defaults select the
current `PORT_OFFSET`, then the current checkout, then the canonical `:7668`
bridge, so concurrent worktrees cannot replace or delete one another's address.
Each Vite shell also publishes its exact bridge at `/__sootsim/bridge`, so a
launcher that reuses a shell from another isolated `RNX_HOME` stays in that
shell's bridge world without reading the other process's lockfile directory.

**The sim** is a browser/Electron *page* launched by a CLI driver. Inside that
page run three workers: the **shell worker** owns device and native state, the
**compositor worker** paints every visible CanvasKit surface, and the **tenant
worker** runs the guest RN tree and sends native-UI requests to the shell. The
bridge always addresses the **shell worker**, never an undifferentiated sim.

**Two distinct plugin families** round it out, and they must not be conflated:

- The published **`rnxsim/vite`** (`src/vite-plugin-one.ts`, `rnxPlugin`)
  and **`rnxsim/metro`** plugins do one thing: serve the installed runtime at
  `/__soot/` so an app's own dev server can host the sim shell. The Vite plugin accepts an
  opt-in `open` option that launches a named Electron window when the dev server
  starts (skipped when `CI` or `RNX_NO_OPEN` is set).
- The internal **`sootsim()`** plugin (`src/vite-plugin.ts`) is a separate, much
  larger RN-resolution / native-stub plugin: it aliases `react-native` to the
  engine shim and native packages to `@sootsim/compat` stubs, applies worklets
  transforms, and is the one that instantiates `SootSimBridgeHost` inside its
  own dev server. It is used to build the engine/shell and to load external RN
  apps — it is **not** the published `rnxsim/vite` export.

## Key components

| component | role | key files |
| --- | --- | --- |
| Peach CLI (bin + dispatcher) | Short-lived terminal entry point. Parses argv, owns the private-by-default one-time choice, lazy-imports one command per run, and routes direct open/runtime/inspection flows. | `cli/bin.ts`, `cli/main.ts`, `cli/privacy.ts`, `cli/parse-args.ts`, `cli/help.ts`, `cli/commands/setup.ts`, `cli/commands/control.ts` |
| WsBridge client | Client side of the CLI→daemon link. Resolves the daemon port from the lockfile, opens a WS to `:7668`, sends JSON commands and awaits `{id,result}`/`{id,error}`. Used by every interactive command. | `cli/ws-bridge.ts`, `src/bridge-constants.ts`, `cli/current-sim.ts` |
| inspect / SDK command surface | Runtime inspection + interaction verbs (describe, find, do tap, get errors, wait, layout, settle). Re-exported as `rnxsim/sdk` so programmatic callers drive a sim the same way the CLI does. | `cli/commands/inspect/core.ts`, `cli/commands/inspect/actions.ts`, `cli/commands/inspect.ts`, `src/sdk.ts` |
| bridge-contract input parsers | Turn untrusted JSON into typed bridge-contract arguments, so a transport in front of a sim validates request bodies once instead of writing a second reader. | `src/bridge-contract-input.ts`, `src/bridge-contract.ts` |
| Maestro / Detox runners | Drop-in test compatibility. `bridge-flow-runner.ts` runs Maestro YAML and recorded flows over the bridge; `detox/` provides a `by`/`element`/`expect`/`device` driver + jest preset so existing Detox suites run unchanged. | `cli/bridge-flow-runner.ts`, `cli/commands/maestro.ts`, `cli/commands/detox.ts`, `detox/index.ts`, `detox/jest-preset.cjs` |
| sim drivers | Launch or attach the actual sim page. Playwright owns isolated browser profiles; Electron owns the native desktop surface. The current sim is reused unless `--new`, `--profile`, or `--ephemeral` requests a separate one. | `cli/drivers/index.ts`, `cli/drivers/registry.ts`, `cli/drivers/electron.ts`, `cli/drivers/playwright.ts` |
| SootSimBridgeHost (serve/daemon) | The bridge process (`rnx serve`, or autostarted as the daemon via launchd/systemd). HTTP+WS hub on `:7668`: relays commands between CLI clients and registered sims; also serves runtime HTTP, `/__server-scan`, and fetch/WS proxies. | `src/host/bridge-host.ts`, `cli/commands/serve.ts`, `cli/commands/daemon.ts`, `src/home-paths.ts` |
| AgentHost + sessions | Agent-routing extension of the host: owns the single FIFO reader per agent session and fans `agent:event`/`session-status` pushes to every WS subscriber. Backed by agent-sessions + attached-projects stores. | `src/host/agent-host.ts`, `src/agent-sessions.ts`, `src/attached-projects.ts`, `src/agent-daemon-client.ts` |
| host proxies + dev-server scan | Host-side helpers so a cross-origin tenant worker can reach local dev servers and same-origin APIs: `/__server-scan` discovers running metro/expo/vxrn/one servers; fetch + websocket proxies relay guest-app network through the daemon. | `src/host/fetch-proxy-handler.ts`, `src/host/websocket-proxy.ts`, `scripts/dev-server-scanner.ts`, `src/dev-bundle-resolution.ts` |
| runtime delivery + Peach home | Versioned engine runtime management. The shared machinery owns manifest fetch, sha256 verification, channels, and auto-update. Peach binds it to `https://contrast.dev` and `~/.rnx/runtimes/<version>`. A repo can select `runtimeVersion` without changing the machine default. | `src/runtime-delivery.ts`, `packages/contrast-runtime-delivery/`, `src/home-paths.ts`, `src/runtime-assets.ts` |
| standalone CLI delivery | Cached stable-version checks, bounded interactive notices, and verified atomic executable replacement. | `cli/cli-update.ts`, `cli/commands/upgrade.ts`, `cli/commands/version.ts`, `src/cli-version.ts` |
| `rnxsim/vite` + `rnxsim/metro` (runtime serving) | Published bundler plugins. Both serve the installed engine runtime at `/__soot/` from `~/.rnx/runtimes/<version>` so an app's own dev server can host the sim shell; the Vite plugin can opt into a named Electron window. | `src/vite-plugin-one.ts`, `src/metro-plugin.ts`, `src/runtime-assets.ts` |
| `sootsim()` vite resolver plugin (internal) | The large RN-resolution / native-stub vite plugin: aliases `react-native` to the engine shim, native packages to `@sootsim/compat` stubs, applies worklets/babel transforms, and instantiates `SootSimBridgeHost` in its dev server. Distinct from the published `rnxsim/vite` export. | `src/vite-plugin.ts`, `src/worklets-babel.ts`, `packages/compat/src/stub-manifest.ts` |
| skills registry | Peach agent skills (`rnx-setup`, `rnx-debug`, `rnx-test`, `rnx-visual`, and the generated `contrast` index) installable with `rnx skill`. | `skills/*.md`, `src/skills/registry.ts`, `src/skills/types.ts` |

## Getting started

```sh
curl -fsSL https://rnxsim.com/install.sh | sh
rnx                 # open ConnectRN and choose a running app in the simulator
rnx open /settings  # dispatch a React Native deep link into that app
```

Start Metro, Expo, One, or another supported React Native development server
the way you normally do, then run `rnx` from any directory. The simulator
opens ConnectRN, which shows the available local apps and lets you connect one.
Pass a target to `rnx open <port-or-url>` when a script or agent needs an explicit
selection.

The first `rnx open` launches an isolated Chrome for Testing sim. The CLI
resolves Playwright from the app or CLI install, asks that exact package for
its required browser executable, and runs that package's installer once when
the executable is absent. It never substitutes another revision found in the
shared Playwright cache. Later opens reuse that sim and reload it at the
requested target. Use `rnx do reload` when the target has not changed.
Reserve `--new` for a genuinely concurrent sim; repeated new browser trees
consume memory and CPU on shared machines.

The public installer places one standalone `rnx` executable in `~/.local/bin`.
The `rnxsim` npm package contains bundler plugins and library exports, not a CLI
binary. Start your Metro, Expo, or One dev server the way you already do, then
open it from the CLI. There is no iOS native build in the inner loop; Metro alone is
enough. The manifest bundle path and query remain authoritative. When both the
discovery request and manifest target are loopback addresses, Peach keeps the
loopback host that answered discovery so an IPv6-only listener is not reopened
through an unreachable IPv4 address. `rnx open` starts a local bridge when no
daemon is reachable. The background service is strongly recommended for local
agent work because inspect, interaction, and test commands reuse one ready bridge
and runtime. Enable it at any time with `rnx daemon install`; it is never required
in CI. The packaged Desktop app is optional and available through first-run
onboarding or `rnx desktop install`. A downloaded app starts its own bundled
daemon on first launch, then offers **Install the Peach command** over the running
simulator. That action installs `~/.local/bin/rnx`, updates shell startup, and
hands the daemon to launchd or systemd without reloading the window. Windows
keeps the desktop-owned daemon because it has no daemon service yet. Native Electron windows created by `rnx open` belong to that CLI
session and close when the session exits; `rnx desktop` keeps the persistent
desktop-app lifecycle. In Electron, `File > New Window`
duplicates the focused simulator, while `File > New Simulator >` opens a new
window for a selected device (same device list as the `Window >` menu).

### Privacy

Peach sends no analytics, crash report, command event, app data, project data, or
identity by default. An unsigned-in interactive run may request the runtime it
is about to execute and the stable standalone CLI version document. The latter
runs after the command in a detached process at most once every 20 hours and can
be disabled with `RNX_UPDATE_CHECK=off`. The first human run asks once about
bounded diagnostics, defaults to no, and never prompts in a non-TTY shell, CI,
or an agent. An automated first run is declined for that process without
consuming the later human choice. Legacy browser and Electron telemetry senders
are removed rather than being enabled by this consent.

Opt-in diagnostics are limited to package name/version/unsupported API or error
class. Peach never sends source, file paths, project names, app content, commands,
identity, screenshots, or recordings as diagnostics. Cloud commands such as
login, upload, billing, issue reporting, generated flows, and screenshot decks
contact their documented service only when explicitly invoked. The complete
enforced network contract is in [`specs/rnx-privacy.md`](../../specs/rnx-privacy.md).

`rnx daemon uninstall` removes the machine daemon: it stops current and legacy
launchd / systemd services and removes their logs, launcher, and generated
`.app` wrapper. `~/.rnx/` survives, because runtimes, device profiles and their
app storage, and live dev-bridge records belong to simulators the user may
still be running. `rnx daemon uninstall --purge` deletes `~/.rnx/` as well.
Neither form removes the optional `/Applications/rnx.app` desktop bundle or the
macOS preference plist. To inspect existing disk usage first, run `rnx cleanup`;
add `--apply` only after reviewing the preview.

## Jump to source in inspect mode

Inspect mode can open a selected React Native element in your editor when the
bundle includes source metadata. Add the optional Babel plugin to your app's
Metro/Babel config:

```js
plugins: [
  [
    'rnxsim/jump-to-source-babel',
    {
      include: [__dirname],
    },
  ],
  'react-native-worklets/plugin',
]
```

The plugin adds an `srcloc="/absolute/path/App.tsx:12:3"` prop to JSX elements
outside `node_modules`, `dist`, and `build`. Keep it before
`react-native-worklets/plugin` in a custom Babel stack that lists both plugins.
Apps using `babel-preset-expo` omit the explicit Worklets plugin because the
preset configures it automatically.

For One's Rolldown native bundler, use the native transform in `vite.config.ts`:

```ts
import { jumpToSourceNativePlugin } from 'rnxsim/jump-to-source-native'

one({
  native: {
    bundler: 'vite',
    bundlerOptions: {
      plugins: [jumpToSourceNativePlugin({ include: [import.meta.dirname] })],
    },
  },
})
```

This transform uses the same source props and filters without running Babel.
For One's `nativeTransformModules`, export
`createJumpToSourceNativeTransform(options)` from a module and pass its module id.

## CLI

The CLI is the primary debugging surface for Peach — use it first for runtime
inspection, interaction, animation debugging, shell tracing, screenshots, and
flow capture, not just as a test runner.

```sh
rnx list              # connected tabs
rnx open 8081         # load a running metro/expo dev server
rnx open /settings    # route via React Native Linking
rnx describe          # dump UI tree
rnx get errors 5      # recent runtime failures
rnx compat            # scan native package compatibility
rnx report-issue "…"  # preview an opt-in compatibility report
rnx find --testid cta # inspect one node deeply
rnx debug enable animated,layout
rnx debug trace shell on 240
rnx do tap-text "..." # interact
rnx do tap email --then type user@example.com --then tap submit
rnx debug snapshot before
rnx debug snapshot after
rnx debug diff before after
rnx maestro start     # begin a Maestro draft
rnx maestro keep      # persist the last action
rnx maestro end --output .maestro/login.yaml --validate
rnx detox             # run Detox/Jest suites against rnx
```

See the full command reference via `rnx --help` or at
`src/features/site/docs/rnx/cli/` in the Contrast repo.

Sim-scoped commands use the primary fallback only when one driveable sim is
connected. With multiple driveable sims, the bridge lists their ids and refuses
to choose. Run `rnx use <id>` to pin later commands, or pass `--sim <id>` to
target one command.

Use an inline `rnx do` chain for a short action sequence that should execute as
one engine batch. Repeat `--then` before each additional action. Each phrase
uses the same positional grammar as its standalone `do` command. Touch timing
stays inside the sim, the batch stops at the first failure, and `--json` prints
the complete per-step result. Reusable file-based flows stay in Maestro YAML.

```sh
rnx do tap email --then type user@example.com --then tap submit
```

## Bundler plugins (`rnxsim/vite` and `rnxsim/metro`)

The Vite plugin serves the installed Peach runtime from your existing dev server.

```ts
// vite.config.ts
import { rnxPlugin } from 'rnxsim/vite'

export default {
  plugins: [rnxPlugin({ open: { appName: 'My App' } })],
}
```

```js
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config')
const { withRNX } = require('rnxsim/metro')

const config = getDefaultConfig(__dirname)

module.exports = withRNX(config)
```

Peach works directly with regular, unannotated Metro production JavaScript bundles
out of the box. It identifies modules by matching package graph fingerprints
against a separately updated, integrity-checked registry. Hermes bytecode is not
a supported input; pass Peach the JavaScript bundle produced before Hermes
compilation.

If an app has unusual dependencies or fingerprint inference fails, you can
optionally wrap your Metro configuration with `withRNX(config)`. It takes no
options and enables two integrations:

1. **Deterministic module identity:** appends Metro's exact module identity map
   as a trailing `globalThis.__sootsimModulePaths = { ... }` footer to every
   bundle. Resolution, transforms, minification, and source maps stay the app's
   own, so the annotated bundle is the exact bundle you ship: the footer assigns
   one unused global on a device, and Hermes drops it during bytecode compilation.
2. **In-browser dev shell:** serves the Peach simulator shell at `/__soot/` on
   the Metro dev server.

The URL helpers request ordinary Metro graphs: `dev=false&minify=true` for
production and `dev=true&minify=false` for development.

```js
const {
  toRNXProductionBundleUrl,
  toRNXDevelopmentBundleUrl,
} = require('rnxsim/metro')

const productionUrl = toRNXProductionBundleUrl(
  'http://localhost:8081/index.bundle?platform=ios',
)
const developmentUrl = toRNXDevelopmentBundleUrl(
  'http://localhost:8081/index.bundle?platform=ios',
)
```

## Coming from Detox

Peach ships a drop-in Detox driver. Existing Detox test files
(`import { by, element, expect, device } from 'detox'`) run against Peach with
no code changes — just add one line to your jest config:

```js
// jest config
{ preset: 'rnxsim/detox' }
```

Or use the CLI directly:

```sh
rnx detox            # auto-discovers e2e/ tests and launches a shell
rnx detox init       # scaffold a config + sample test
```

See `../../docs/migrating-from-detox.md` for details.

## Coming from Maestro

Maestro is Peach's single YAML test surface. Point it at your existing
`.maestro/` directory, generate a test, or author one from live actions:

```sh
rnx maestro                      # discover .maestro/ and run all flows
rnx maestro test .maestro/       # explicit
rnx maestro generate "verify login"
rnx maestro --list-compat        # see the verb support matrix
```

Most Maestro verbs work out of the box (`tapOn`, `assertVisible`, `inputText`,
`scrollUntilVisible`, `launchApp`, `when:`, `repeat`, `runFlow`, …). Verbs that
need real device hardware (GPS, radio, photo library) throw a clear error. See
`../../docs/migrating-from-maestro.md` for the full compat matrix.

## Screenshots and recordings

For animation-heavy debugging, the useful path is usually:

```sh
rnx debug enable animated
rnx debug trace shell on 240
rnx debug snapshot before
# reproduce the transition
rnx debug snapshot after
rnx debug diff before after
rnx screenshot --with-frame --output /tmp/rnx-framed.png
rnx record --duration 5 --output /tmp/rnx-anim.mp4
```

`rnx screenshot --with-frame` composes the real Peach shell device chrome
around the raw screen bitmap (reusing the shell bezel/button geometry, excluding
the Electron top bar, rail gutter, and other window chrome). Flows can request
the framed export inline without changing Maestro syntax:

`rnx wait ready` and the screenshot readiness guard accept inspectable
native content, a populated node tree, or a live-frame channel whose publish
count advances across probes. Canvas-only GL, WebGPU, and video surfaces can
therefore prove that they are painting without weakening the boot-card guard.

```yaml
- takeScreenshot: hero
- takeScreenshot:
    path: marketing/hero
    withFrame: true
```

For plan-driven app-store exports, use `rnx screenshot appstore`:

```yaml
app: 8081
device: iphone-16

capture:
  flow: .maestro/capture-app-store-screenshots.yaml
  mode: raw+framed

compose:
  canvases: [iphone-6-9, iphone-6-1]
  background: cyan
  text:
    preset: bold-top
  slides:
    - id: splash-hero
      screenshot: apple/iphone/en/01-splash.png
      headline: First punch.
      subheadline: Every pick in one place.
```

```sh
rnx screenshot appstore --plan .rnx/app-store.yaml
```

The plan runner can reuse a visible sim for capture (`--sim a9`), stop after
raw/framed intermediates (`--capture-only`), or rerender final marketing
canvases from an existing raw directory (`--compose-only`). If your flow already
writes screenshots to explicit project paths, set `from:` + `pathMode: flow` so
`rnx screenshot appstore` respects the flow's own `takeScreenshot` paths
instead of prepending `--screenshots <rawDir>`:

```yaml
capture:
  flow: .maestro/capture-app-store-screenshots.yaml
  from: ./apps/app-store-screenshots/public/screenshots/apple/iphone/en
  pathMode: flow
```

In the browser shell, `Screenshot Mode` turns the live shell into a simple DOM
composition surface: the rail + mac menu bar disappear, the device shifts down
with a short transition, and editable title/subtitle fields appear above the
frame for quick art-direction passes.

## Skills registry

`rnx skill install` installs Peach agent skills in the standard
`<skill-name>/SKILL.md` layout used by Codex and Claude Code. The bundled
skills cover setup, debugging/perf/accessibility, testing, visual review, and
the generated `contrast` command index. The published CLI registry and generated
website docs both come from `packages/rnx-skills/`:

- `packages/sootsim/skills/contrast/SKILL.md` is generated from that registry
- `src/features/site/docs/rnx/cli/*` is generated output, not hand-edited

When command docs or examples drift, update `packages/rnx-skills/` and run:

```sh
bun run generate:sootsim-docs
```

## Runtime delivery (the Peach home and CDN)

The engine never ships inside this package. `runtime-delivery.ts` names
Peach's CDN origin (default `https://contrast.dev`, overridable in
`config.json`), its `SOOTSIM_*` env overrides, and its hosted paths, then hands
them to the shared delivery machinery in `@contrast/runtime-delivery` — an
internal workspace package the CLI bundles, so a published install has it
inlined. That machinery fetches `runtimes/manifest.json` + the runtime tarball,
verifies its sha256, and unpacks it under `~/.rnx`:

```
~/.rnx/
├── runtimes/
│   ├── <active>/         unpacked engine assets (served at /__soot/)
│   └── <rollback>/       previous version for rollback
├── cache/                legacy downloads; empty after cleanup
├── profiles/             persistent named app storage, without browser caches
├── electron/userData/    desktop app storage, with bounded browser caches
├── daemon.json           lockfile: pid, ports, active runtime, heartbeat
├── cli-update.json       cached standalone CLI version and notice state
├── automatic-cleanup-v1.json  completed legacy cleanup generation
└── config.json           user prefs: update channel, cdn origin override
```

The bridge daemon serves these assets over HTTP and exposes a self-update route;
`rnx upgrade` / `rnx runtime` drive explicit version changes from the
CLI side. Installation retains the active runtime plus one rollback and removes
the downloaded archive after extraction. Disposable Playwright caches live in a
bounded per-session temporary directory; Electron's persistent HTTP cache is
disabled for simulator profiles and globally capped for app chrome. Cookies,
localStorage, IndexedDB, service workers, recordings, and captured frames are
user-owned data and are not removed by the default cleanup command.

Per-repo configuration is optional. Every installed-runtime `rnx open`
serves its selected version from a version-specific localhost origin, so a
background activation cannot mix an already loaded shell with chunks from the
next runtime. Without config, each new open selects the active stable version.
When `rnx.config.ts` sets `runtimeVersion`, `rnx open` installs that
version without changing the machine default. App config, native-linked fonts,
and splash assets all resolve from the project root advertised by the selected
development server. Bare Metro, which has no Expo manifest project root, uses
the listening development process's cwd inside that same resolver. The invoking
CLI's cwd is never an asset/config fallback. Asset wires stay on each opened
project URL, so one daemon can serve several attached projects without sharing
font or splash state between them.

The first interactive CLI command after this cleanup generation finishes its
requested work, then launches the same safe default plan in a detached
maintenance process. The app and terminal command are usable before the legacy
filesystem scan begins. The worker prints the reclaimed size to stderr, records
completion, and does not run the scan again. Open browser profiles and a runtime
served by a live daemon or development bridge are left untouched. The automatic
pass retries on a later command after they close. The explicit `rnx cleanup --aggressive`
command remains the only path that removes the rollback runtime or user-created
recordings.

## Development and building

This package is part of the Contrast monorepo. To build the standalone executable
for the current platform:

```sh
bun run build:cli-binary
```

The output is `dist-bin/rnx-<platform>-<arch>`. `dist-cli/bin.js` remains a
repository build artifact for tests and internal tooling and is not published
as an npm executable. Library exports build to `dist-lib/` (consumed via the
`exports` map: `.`, `./vite`, `./metro`, `./sdk`, `./skills`, `./detox`, and the
host/agent helpers). `bun run pack:smoke` validates the publishable tarball.
