npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

peachsim

v0.1.1594

Published

Vite and Metro plugins, testing drivers, and SDK exports for peach.

Readme

Peach

The source owner for the standalone peach executable and the public peachsim 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 peach executable that needs no Node.js, npm, or Bun installation. The peachsim npm package contains bundler plugins, testing drivers, and library exports; its only command, npx peachsim, runs that same installer from the copy of install.sh it ships.

The executable updates itself with peach upgrade, which then restarts a running background daemon onto the new executable and runtime. 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 PEACH_UPDATE_CHECK=off to disable automatic CLI checks, runtime updates, and their notices. Start the bridge with this environment, or reinstall the background service with it to persist the choice. peach version checks explicitly, while peach --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 ~/.peach.

What this workspace gives you:

  • the standalone peach 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)
  • peachsim/vite and peachsim/metro — bundler plugins that serve the installed engine runtime from your own dev server
  • peachsim/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
  • peachsim/detox — a drop-in Detox driver + jest preset
  • peachsim/sdk — the inspect/interact verbs as a programmatic API
  • peachsim/bridge-contract-input — parsers turning untrusted JSON into bridge-contract arguments
  • peachsim/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 peach executable | peachsim 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.

graph TD
  subgraph cli["peach 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["peachsim/vite = vite-plugin-one.ts (serve runtime at /__peachsim/)"]
    VITERN["sootsim() = vite-plugin.ts (RN resolver + native stubs)"]
    METRO["peachsim/metro = metro-plugin.ts"]
    COMPAT["@sootsim/compat stub-manifest (native seams)"]
    ENGINESHIM["sootsim-engine react-native shim"]
  end
  SKILLS["peachsim/skills registry.ts (builtin skills)"]
  subgraph fs["~/.peach (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 /__peachsim/ from"| RTDIR
  METRO -->|"serve /__peachsim/ 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 peach opens ConnectRN, whose simulator UI discovers running React Native development servers. The first human run offers the recommended background service and simulator window; on Mac, Desktop uses an already installed app. It then opens the simulator before asking about private diagnostics or an optional repository install. peach open ensures the engine runtime exists when the bridge supplies the shell 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 peachsim/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. An explicit --base-url keeps that shell's origin and uses its assets without installing an active runtime. A bridge started for that shell skips automatic runtime updates. An app's configured runtimeVersion still selects its installed, version-owned bridge origin. The native host supplies the desktop surface on Mac and Linux; Electron is Windows-only. Team Machine sessions (TM_SESSION) default to headless Playwright; --headed explicitly opens an isolated headed browser. System browser launches, including share links, login, box links and repo scripts, go through src/host/open-url.ts. That owner refuses a system browser launch inside a Team Machine session and prints the URL plus --driver playwright --headless guidance. Interactive human launches keep their system browser behavior.

For apps using react-native-config, open with peach open <app-port> --load-env. This reads the local project's native dotenv values for RNCConfigModule; runtimeConfig.env populates the guest's process.env independently. Starting a sidecar or supplying process environment variables does not select the app's native API roots. Verify the mounted app's Config values and captured request destinations before driving a network flow. Without --load-env, the native config dictionary is empty and the app can select its own public defaults. Preserve failures from those endpoints rather than remapping them to a successful local response. The same flag is available on peach flow.

A project with several Xcode application targets names the one to run, as an Xcode scheme does: peach open <app-port> --ios-target <name> (also on peach flow and peach maestro test) reads that target's version, build, bundle id and display name. Without it, such an app served by a plain Metro reads the runtime defaults.

The daemon is one process — SootSimBridgeHost (src/host/bridge-host.ts). peach serve runs it in the foreground; the background daemon is the same process wrapped with autostart and the ~/.peach/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.

peach open auto-installs a published runtime only when the runtime directory is empty. If runtime files exist without a selected runtime containing index.html, it refuses to open. Finish copying the runtime, then select it with peach runtime use <version>. Write the active marker after staging is complete when preparing an isolated home.

During source development, each Vite bridge owns one ~/.peach/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 PEACH_HOME stays in that shell's bridge world without reading the other process's lockfile directory. An explicit --port also participates in --sim ownership discovery even without a local heartbeat record. The bridge must report that simulator as open; a port that names another simulator's world is rejected. SDK connect({ sim }) and batched do --then actions use that same ownership discovery. An inferred default port does not override the named sim's world; an explicit SDK port or CLI --port retains the ownership check.

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 peachsim/vite (src/vite-plugin-one.ts, peachPlugin) and peachsim/metro plugins do one thing: serve the installed runtime at /__peachsim/ 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 PEACH_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 peachsim/vite export.

Preview uploads retain one response snapshot in the simulator and transfer its base64 bodies in bounded slices, including authenticated API responses. The CLI removes that temporary snapshot after transfer. A completed upload publishes its trace URL through a fresh bridge connection because compression and HTTP upload can outlive the interactive socket's idle lifetime. The finished test duration uses the last execution status timestamp; publishing the later trace URL notifies viewers without extending that duration.

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 peachsim/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 |

Maestro YAML header onFlowStart and onFlowComplete hooks reach the same runner as body hooks, including nested runFlow files. A header reset flow therefore runs before assertions in each flow. | sim drivers | Launch or attach the actual sim page. Playwright owns isolated browser profiles; the native host owns Mac and Linux desktop windows; Electron owns Windows windows. 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 (peach 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 ~/.peach/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 | | peachsim/vite + peachsim/metro (runtime serving) | Published bundler plugins. Both serve the installed engine runtime at /__peachsim/ from ~/.peach/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 peachsim/vite export. | src/vite-plugin.ts, src/worklets-babel.ts, packages/peach-compat/src/stub-manifest.ts | | skills registry | Peach agent skills (peach-setup, peach-debug, peach-test, peach-visual, and the generated contrast index) installable with peach skill. | skills/*.md, src/skills/registry.ts, src/skills/types.ts |

Getting started

curl -fsSL https://peachsim.com/install.sh | sh   # or: npx peachsim
peach                 # open ConnectRN and choose a running app in the simulator
peach open /settings  # dispatch a React Native deep link into that app
peach open /settings --no-describe --quiet  # keep open options with deep links

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

The first peach open launches an isolated Chrome for Testing sim. The CLI resolves Playwright from the current directory or beside its executable and launches the newest installed Chromium revision in its browser cache, honoring PLAYWRIGHT_BROWSERS_PATH. Set PEACH_CHROMIUM_EXECUTABLE to an existing executable to select a binary explicitly. Headless launches on Linux and Windows use Playwright's Chromium headless shell instead: the same build and software renderer without the desktop browser's printing and service discovery libraries (libcups2, libavahi-client3). Headed launches and every launch on a Mac use full Chromium. When the needed browser is not installed, the CLI runs the resolved Playwright core's absolute installer entry for just that browser. On Linux the CLI checks the browser's shared libraries before launching. When the system lacks any, it downloads peach's own bundle of them once (Ubuntu 20.04 builds, so they load on glibc 2.31 and newer; about 15 MB) into $PEACH_HOME/linux-libs/ and launches the browser with that bundle on its library path, so no host needs apt-get or root. The bundle is pinned in cli/drivers/linux-browser-libs.lock.json; bun scripts/linux-browser-libs.ts resolve re-pins it, and the CLI release publishes any bundle the bucket lacks.

On Mac, headless launches attach an installed compatible native host. With no host, they render in the browser without downloading or placing the app. The browser logo menu offers the app; peach desktop install --native remains the explicit CLI installer. Linux still acquires its standalone host under ~/.peach.

Headed launches on Linux use full Chromium, whose extra libraries the bundle also carries. Later opens reuse that sim and reload it at the requested target. Use peach 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 peach executable in ~/.local/bin. The peachsim 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. peach 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 peach daemon install; it is never required in CI.

Sandboxed or agent runners may stop child processes when a command ends, including a bridge started by peach open. Keep peach serve running in a persistent terminal or background task outside that command's lifetime, then run peach open and later commands against it. When using a custom bridge port, pass the same --port <n> to peach serve and the CLI commands.

The packaged Desktop app is optional and available through first-run onboarding or peach desktop install. A downloaded app starts its own bundled daemon on first launch. An installed runtime can open while the daemon checks for updates; a first install waits for the runtime download to finish. The app then offers Install the Peach command over the running simulator. That action installs ~/.local/bin/peach, 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 peach open belong to that CLI session and close when the session exits; peach 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 PEACH_UPDATE_CHECK=off. Bounded diagnostics and automatic crash reports are on by default; the first human run says once what they send, and peach config telemetry off or DO_NOT_TRACK=1 turns them off. Legacy browser and Electron telemetry senders are removed rather than being enabled by this default.

Diagnostics are limited to package name/version/unsupported API, error class and first-line message, and the command name with its exit code; a crash report files the same error fields in the issue queue. 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.

peach daemon uninstall removes the machine daemon: it stops current and legacy launchd / systemd services and removes their logs, launcher, and generated .app wrapper. ~/.peach/ survives, because runtimes, device profiles and their app storage, and live dev-bridge records belong to simulators the user may still be running. peach daemon uninstall --purge deletes ~/.peach/ as well. Neither form removes the optional /Applications/peach.app desktop bundle or the macOS preference plist. To inspect existing disk usage first, run peach 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:

plugins: [
  [
    'peachsim/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. Runtime Fragment aliases are excluded before element creation. Component tag expressions are evaluated once, and ordinary components retain their locations.

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

import { jumpToSourceNativePlugin } from 'peachsim/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.

peach list              # connected tabs
peach open 8081         # load a running metro/expo dev server
peach open /settings    # route via React Native Linking
peach describe          # dump UI tree
peach get errors 5      # recent runtime failures
peach doctor            # check what of this app Peach runs
peach support report "…"  # preview an opt-in compatibility report
peach find --testid cta # inspect one node deeply
peach debug enable animated,layout
peach debug trace shell on 240
peach do tap-text "..." # interact
peach do tap email --then type [email protected] --then tap submit
peach debug snapshot before
peach debug snapshot after
peach debug diff before after
peach maestro start     # begin a Maestro draft
peach maestro keep      # persist the last action
peach maestro end --output .maestro/login.yaml --validate
peach detox             # run Detox/Jest suites against Peach

See the full command reference via peach --help or at src/features/site/docs/peach/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 peach use <id> to pin later commands, or pass --sim <id> to target one command.

Use an inline peach 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.

peach do tap email --then type [email protected] --then tap submit

Bundler plugins (peachsim/vite and peachsim/metro)

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

// vite.config.ts
import { peachPlugin } from 'peachsim/vite'

export default {
  plugins: [peachPlugin({ open: { appName: 'My App' } })],
}
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config')
const { withPeach } = require('peachsim/metro')

const config = getDefaultConfig(__dirname)

module.exports = withPeach(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 withPeach(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 /__peachsim/ 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.

Metro registration readers retain the original factory bytes and module IDs. Empty dependencies may be an empty array, an omitted production argument, or a null development argument before the module name. Identity validation and fingerprinting treat each as zero dependencies and reject malformed arguments.

const {
  toPeachProductionBundleUrl,
  toPeachDevelopmentBundleUrl,
} = require('peachsim/metro')

const productionUrl = toPeachProductionBundleUrl(
  'http://localhost:8081/index.bundle?platform=ios',
)
const developmentUrl = toPeachDevelopmentBundleUrl(
  '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:

// jest config
{ preset: 'peachsim/detox' }

Or use the CLI directly:

peach detox            # auto-discovers e2e/ tests and launches a shell
peach 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:

peach maestro                      # discover .maestro/ workspace
peach maestro test .maestro/       # explicit
peach maestro generate "verify login"
peach maestro --list-compat        # see the verb support matrix

Directory discovery selects root YAML flows by default, as Maestro does. For nested suites, pass their folder, such as .maestro/flows, or add config.yaml with flows: ["flows/**"]. Keep shared subflows outside those globs; runFlow can still load them.

Tap settling compares consecutive full-screen pixels within Maestro's iOS three-second budget, while also draining screen transitions, native motion and image loads. Periodic caret paints do not require a continuous stillness window, matching Maestro's screen comparison.

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:

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

peach 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:

peach 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.

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

For plan-driven app-store exports, use peach screenshot appstore. The CLI provisions its browser dependency. Paths resolve relative to the plan file:

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.
peach screenshot appstore --plan .peach/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 peach screenshot appstore respects the flow's own takeScreenshot paths instead of prepending --screenshots <rawDir>:

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

peach skill install installs Peach agent skills in the standard <skill-name>/SKILL.md layout used by Codex and Claude Code. Building the CLI only writes build artifacts; run peach skill install explicitly to install or refresh agent skills. 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/peach-skills/:

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

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

bun run generate:peach-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 ~/.peach:

~/.peach/
├── runtimes/
│   ├── <active>/         unpacked engine assets (served at /__peachsim/)
│   └── <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; peach upgrade / peach 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 peach 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 peach.config.ts sets runtimeVersion, peach open installs that version without changing the machine default. The same file answers native code Peach does not know under packages, keyed by the package and then its native module and view names, and an installed peach-stub-<package> package answers in the same shape with no config line; the configuration guide in the docs lists every slot. App config, native-linked fonts, and splash assets resolve from the local directory named by --project-root when supplied, otherwise the project root advertised by the selected development server. Hosted recorders pass their checked-out repository explicitly because the packager's filesystem belongs to another machine. An invalid explicit root fails instead of loading empty native configuration. 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.

Bare iOS projects carry default Firebase options from their selected app target's Google services build resource when the app delegate initializes Firebase unconditionally during launch. Completed compile conditional blocks before that call are allowed. Initialization inside an open conditional block or after a possible early return is rejected.

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 peach 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:

bun run build:cli-binary

The output is dist-bin/peach-<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.