Skip to content

CLI

Every command takes only its own flags, and answers --help (or -h) by printing them without running: tapsmith test --help, tapsmith help test. tapsmith --help lists the commands. A flag that belongs to another command is refused (tapsmith show-trace t.zip --force-install is an error).

Value flags take their value either way — --workers 4 or --workers=4, -j 4 or -j4 or -j=4. A value flag followed by another flag is an error, not a value: tapsmith test --device --workers 2 fails with option '-d, --device <serial>' argument missing, instead of targeting a device called --workers. For a value that really starts with -, use the = form: --grep=-slow.

An empty value (--device "$SERIAL" with an empty variable) means the flag was not given for --device, --trace, --video, --grep, --grep-invert and --reporter: the run falls back to automatic device selection or the config’s setting. An empty -c / --config is an error rather than a silent fallback to tapsmith.config.*, which could run a different config than the one meant.

A usage error (unknown command or flag, missing or invalid value) prints one line naming the problem and the command’s --help, and exits with code 1. An unknown command suggests the nearest one (unknown command 'tset' / Did you mean test?), even when --help is also given. Commands with a --json mode (init, verify, doctor, list-devices, telemetry, ios setup-device) report usage errors under --json as the JSON error envelope on stdout, with code BAD_ARGS, or for init UNKNOWN_FLAG / MISSING_FLAG_VALUE like its other errors. mcp-server keeps stdout for the protocol and reports usage errors on stderr.

init, verify, doctor, list-devices, telemetry and ios setup-device take --json, for scripts and AI agents. The shapes below are a contract for scripts to rely on: new keys may be added, but renaming or removing one, or changing what it means, is a breaking change.

  • stdout holds exactly one JSON document, and nothing else from Tapsmith: no banner, no progress, no ANSI codes. Two things are outside that promise: --help still prints help, and code in your config file that writes to stdout (a top-level console.log) still lands there, since doctor, verify and telemetry load the config. A command killed by a signal (Ctrl-C, a CI cancel) may print nothing. verify does not handle interrupts reliably yet: depending on which processes the signal reaches and when, an interrupted run reports RUN_FAILED (exit 1), exits 130 with no output, or runs to completion.

  • When the command could not do its job, the document is the error envelope and the exit code is 1:

    { "error": { "code": "DAEMON_NOT_FOUND", "message": "…", "fix": "…" } }

    code is a stable SCREAMING_SNAKE identifier (table below), message a description for a person (it can span lines: verify’s RUN_FAILED includes the end of the run’s stderr), fix what to do about it, and candidates (init only) the choices that made it undecidable.

  • Otherwise the document is the command’s result, which never has a top-level error key — so "error" in result tells the two apart. A result can still mean failure: doctor with a failing check and verify with a failing test exit 1 and print their result with "ok": false.

  • Optional keys are left out when they have no value, never set to null (except telemetry’s configPath, which is null when no config was loaded: none was found, or it failed to load, which configConsulted: false tells apart).

Command Error codes
every command above BAD_ARGS (a usage error), UNEXPECTED_ERROR (a bug or an environment failure the command did not anticipate; init and verify included), UNSUPPORTED_NODE (Node.js is older than 22; nothing else runs)
init UNKNOWN_FLAG, MISSING_FLAG_VALUE, INVALID_PLATFORM, INVALID_DEVICE_TYPE, NO_PLATFORM, NO_APK, AMBIGUOUS_APK, NO_PACKAGE, NO_IOS_APP, AMBIGUOUS_IOS_APP, NO_BUNDLE_ID, IOS_REQUIRES_MACOS, IOS_PHYSICAL_INTERACTIVE_ONLY, CONFIG_EXISTS, WRITE_FAILED (a file init writes cannot be written: names it and any files already written), NON_INTERACTIVE_TTY (no terminal and no --yes), JSON_REQUIRES_YES (--json in a terminal without --yes or a setup flag: the wizard has no JSON output)
verify YARN_PNP_UNSUPPORTED (the project is a Yarn Plug’n’Play install, where Tapsmith can’t run tests; the fix is nodeLinker: node-modules), NO_CONFIG, CONFIG_ERROR, RUN_FAILED, PARSE_FAILED, NO_TESTS_RAN (the run executed no test: the file has none, every one was skipped, or a grep / grepInvert filtered them all out; a flaky test that passed on retry counts as having run)
doctor none of its own: a check that cannot run is reported in the result, usually as a warn (Could not check …)
list-devices DAEMON_NOT_FOUND, DAEMON_START_FAILED, LIST_DEVICES_FAILED
telemetry TELEMETRY_WRITE_FAILED (enable / disable could not write ~/.tapsmith/telemetry.json)
ios setup-device UNSUPPORTED_PLATFORM (not macOS)

The results:

  • init --yes --json: { configPath, filesCreated, warnings, nextSteps } — the config file written, every file created (including .gitignore when init added entries to it), warnings (strings) and next steps (strings). When the project cannot resolve tapsmith, which the config and example test import, the first next step is the install command for its package manager (--yes never runs it) and a warning says so.
  • verify --json: { ok, passed, failed, skipped, duration, failures, testFile } — counts, duration in milliseconds, failures as { fullName, error, screenshotPath? }, and the test file run, relative to the config’s rootDir.
  • doctor --json: { ok, checks, inventory } — ok is false when any check has status fail; checks are health checks (below); inventory is { avds, simulators, connectedDevices }: AVD names, simulators as { name, udid, state, runtime } (runtime like iOS 26.5), and connected Android devices as { serial, state }.
  • ios setup-device --json: { ok, checks, devices, notConnected } — ok is the same verdict as the exit code; checks are health checks (below), ending with device-connected, which fails when no device is connected or a connected device is unpaired or has Developer Mode off, so ok is false exactly when some check fails; devices are the physical devices xcrun devicectl can reach now, as { udid, name, osVersion, paired, developerMode, transport, fix? }, where osVersion is empty when devicectl does not report it, developerMode is enabled, disabled or unknown, transport is wired, localNetwork or unknown, and fix is set on a device that is unpaired or has Developer Mode disabled (one line per problem). developerMode and transport pass through what devicectl reports, so treat a value not listed here as unknown. notConnected lists, as { udid, name }, the devices devicectl only remembers (unplugged, out of Wi-Fi range, or paired with another Mac); they never affect ok, and list-devices leaves them out.
  • list-devices --json: { devices }, each { ready, platform, serial, name, osLabel, blockers, inUseBy } — platform is android, android-emu, ios-sim or ios-device; osLabel is like iOS 18.1 or empty when unknown; blockers are the fixes that would make it ready, empty when ready. inUseBy is the other running Tapsmith session that has claimed the device (see Several sessions on one machine), as { command, pid, project, since, daemonAddress }, or null when none has; ready does not take it into account. command is like tapsmith test --ui or tapsmith mcp-server, pid is that session’s root process, project the directory it was started in, since the ISO time it claimed the device, and daemonAddress the daemon it drives the device through, or null when unknown.
  • telemetry --json (every action): { enabled, reason?, debug, stateFile, anonymousId?, endpoint, configPath, configConsulted, docs } — reason is env, config or machine when disabled; see Telemetry.

A health check is { id, status, label, detail?, fix? }. id is stable — match on it, not on label, which can hold values (Node.js 22.1.0) and whose wording may change. status is pass, warn (not blocking) or fail. detail is extra context (a path, where something was found, device names), which doctor’s text output prints dimmed after the label; fix says how to resolve a warn or fail and may span several lines. doctor’s ids include node, node-arch, yarn-pnp, daemon, config, config-load, config-platform, no-platform, adb, android-home, android-devices, android-agent, app-apk, android-emulator, avd-images, xcode, simctl, ios-sim-agent, mitm-ca, mitmproxy, network-extension and system-proxy, each only where it applies; ios setup-device’s are xcode-clt, devicectl, iproxy, signing, sudo-ddi-mount, ios-agent-runner, profile-expiry and device-connected.

Run test files. If no files are specified, discovers tests using the testMatch patterns from your config.

Terminal window
npx tapsmith test
npx tapsmith test tests/login.test.ts tests/signup.test.ts
npx tapsmith test tests/auth # every test file under a directory
npx tapsmith test 'tests/auth/*.test.ts' # a glob (quote it so the shell leaves it alone)
npx tapsmith test login checkout # files whose path matches "login" or "checkout"

As in Playwright, each argument is one of:

  • a file (relative to rootDir or the working directory, or absolute): run as named, even if testMatch would not discover it — except in a config with projects, where a file runs under the projects whose testMatch covers it, so one that no (selected) project covers is reported and not run;
  • a directory: the test files under it;
  • a glob: the test files it matches (a glob that matches none is read as the regular expression below instead, with a note saying so — *, ?, [] and {} are regex syntax too);
  • anything else: a case-insensitive regular expression matched against each test file’s path relative to rootDir (/pattern/flags sets the flags; an invalid expression is matched as plain text).

Directories, globs and filters select from the files testMatch discovers, so they never pick up a helper module. With projects, a project that another project in the run depends on (a setup project) runs whole, as in Playwright, even when an argument also matched some of its files; an argument that selects only setup files, with nothing depending on them in the run, runs just those files. An argument that matches nothing prints a warning; if nothing is selected at all, the run fails with No tests found — before it starts the daemon or touches a device.

tapsmith test --device <serial> / tapsmith test -d <serial>

Section titled “tapsmith test --device <serial> / tapsmith test -d <serial>”

Target a specific device by its ADB serial number (or simulator UDID). This is mainly useful for single-device debugging or reproducing an issue on one known device.

Terminal window
npx tapsmith test --device emulator-5554

A pinned device hosts exactly one worker, in every mode — sequential, parallel, watch and UI. --device combined with a --workers N the run cannot use is refused: both are on the command line and ask for different runs. In a single-platform config that is any N > 1 the run could otherwise use (more than one file in a wave — --device X --workers 2 one.test.ts just runs on X); in a config whose projects span Android and iOS, --device (like a root device in the config) pins only the projects of that device’s platform, and the other platform’s projects may use the remaining workers — unless the device is pinned by several projects of its platform (two apps, say), in which case the whole run uses one worker and runs them one after another (UI and watch mode refuse that: they keep a worker per target alive). Any other worker count — a workers value in the config, or --workers N with a device pinned in the config or by use.devices members — is capped to one for that device target, with a note saying so. For multi-worker emulator runs, use config-based provisioning with workers, launchEmulators, and avd instead.

tapsmith test --workers <n> / tapsmith test -j <n>

Section titled “tapsmith test --workers <n> / tapsmith test -j <n>”

Run tests in parallel across n devices. Each worker gets its own device/emulator and daemon instance. Tests are distributed via a work-stealing queue for natural load balancing.

Terminal window
npx tapsmith test --workers 4
npx tapsmith test -j 2

Overrides the workers config option. Requires enough connected devices or launchEmulators: true with an avd configured. In parallel mode, each test result includes a workerIndex field and console reporters show [worker N] tags.

tapsmith test --shard=x/y / tapsmith test --shard x/y

Section titled “tapsmith test --shard=x/y / tapsmith test --shard x/y”

Split the test suite deterministically across y machines, running only shard x. Each project’s files are split on their own, by position in that project’s sorted file list (file_index % total === current - 1), so every machine gets the same split whatever order the files are named in. A project another project depends on (a setup project) is not split: it runs whole on every shard that has files from one of its dependents, and is skipped on a shard that has none.

Terminal window
# In a CI matrix with 4 jobs:
npx tapsmith test --shard=1/4
npx tapsmith test --shard=2/4
npx tapsmith test --shard=3/4
npx tapsmith test --shard=4/4

When sharding is active, the blob reporter is automatically added so results can be merged later with tapsmith merge-reports.

tapsmith test --grep <pattern> / tapsmith test -g <pattern>

Section titled “tapsmith test --grep <pattern> / tapsmith test -g <pattern>”

Run only the tests whose fullName (describe > test) matches the given regular expression. Mirrors Playwright’s --grep. The tests it filters out are left out of the run entirely: they are not listed, and not counted as skipped.

Terminal window
npx tapsmith test --grep checkout # Only tests with "checkout" in their fullName
npx tapsmith test -g "^login > " # Only tests inside the top-level "login" describe
npx tapsmith test --grep "@smoke|@critical" # Run smoke + critical tests by tag-style suffixes

The pattern is compiled as a JavaScript RegExp. Combine with --grep-invert to further narrow the selection.

If a --grep / --grep-invert filter selects zero tests (e.g. a typo’d pattern), the run fails with No tests found and lists the tests there are, rather than reporting a green “0 tests” success. Like Playwright, Tapsmith reads the test names first, so this happens before it starts the daemon or touches a device (a file that fails to load leaves the question open, and the run goes ahead to report it). Watch and UI mode skip this check: an edit can add the test the pattern is waiting for, and UI mode lists every test to pick from.

The same filter can be set in tapsmith.config.ts:

export default defineConfig({
grep: /checkout/, // single pattern
})
// Or match any of several patterns:
export default defineConfig({
grep: [/login/, /signup/], // any pattern matches
})

Per-project filters are intersected with the root filter (a test must match both):

export default defineConfig({
grep: /smoke/,
projects: [
{ name: 'android', grep: /android/ }, // runs tests matching BOTH /smoke/ and /android/
],
})

Leave out the tests whose fullName matches the given regular expression: like --grep’s non-matches, they are not listed or counted as skipped. Mirrors Playwright’s --grep-invert.

Terminal window
npx tapsmith test --grep-invert slow # Run everything except "slow" tests
npx tapsmith test -g checkout --grep-invert wip # Checkout tests, excluding work-in-progress

Also configurable in tapsmith.config.ts as grepInvert: RegExp | RegExp[], with per-project entries unioned with the root entry.

Run only the named project from your config. Repeat the flag to run several. Mirrors Playwright’s --project. Any dependencies of the selected projects run automatically, so setup projects are never skipped.

Terminal window
npx tapsmith test --project android # Only the "android" project (+ its deps)
npx tapsmith test --project android --project ios # Both platforms

The flag requires a projects array in your config. An unknown name fails the run with the list of available projects. Combine with file arguments to further narrow a project to specific test files:

Terminal window
npx tapsmith test --project android tests/login.test.ts

Watch test files for changes and re-run them automatically. The daemon, device, and agent are kept alive across re-runs, so only the app reset + test execution cost is paid (~1-2s per re-run).

Terminal window
npx tapsmith test --watch
npx tapsmith test -w tests/login.test.ts # Watch a specific file

See Watch Mode for details.

Open the interactive UI mode in the browser. Provides a web-based test runner with a test tree, click-to-run, live progress, and an MCP endpoint for AI agent integration.

Terminal window
npx tapsmith test --ui
npx tapsmith test --ui --ui-port 8080 # Use a specific port

See UI Mode for details.

tapsmith test --config <path> / tapsmith test -c <path>

Section titled “tapsmith test --config <path> / tapsmith test -c <path>”

Use a specific config file instead of the default tapsmith.config.ts:

Terminal window
npx tapsmith test -c tapsmith.config.ci.ts
npx tapsmith test --config=./configs/ios.config.mjs

Reinstall the app under test (APK or .app) on every device the run sets up, even when the installed build already matches. Applies in every mode: the sequential run, each --workers worker, and the workers watch and UI mode start (a UI worker respawned mid-session does not reinstall). The Tapsmith agent is not reinstalled by this flag. Useful when the app’s installed state has drifted outside Tapsmith — a rebuilt APK is already detected and reinstalled without it.

Terminal window
npx tapsmith test --force-install

Without this flag, Tapsmith checks if the package is already installed and skips the install step to save time.

Use one reporter for this run instead of the config’s reporter: a built-in one (list, line, dot, json, junit, html, github or blob), or the path or package name of a custom reporter module (see Custom reporters, under Reporters). For several reporters or reporter options, set reporter in the config.

Terminal window
npx tapsmith test --reporter dot
npx tapsmith test --reporter json # writes tapsmith-results/results.json

Set up a project. With no options, in a terminal, it runs the interactive wizard: it detects your environment (ADB, Xcode, simulators, emulators), offers the app builds it finds under android/ and ios/ (a typed path must exist), walks you through platform and app configuration, and generates a tapsmith.config.ts, an example test (tests/example.tapsmith.ts) and an AGENTS.md section. It adds tapsmith-results/ (traces, screenshots, JSON and JUnit results) and tapsmith-report/ (the HTML report) to the project’s .gitignore (creating one if there is none), skipping any git already ignores (a .gitignore it cannot update is a warning, not an error). Before writing anything it checks it can write every file (and, before its setup questions or the iOS agent build, that it can write to the project directory), and it writes the config last, replacing any existing one only once the new one is complete: a file it cannot write stops init with exit code 1 and WRITE_FAILED, naming the file and anything already written, in the wizard as with --yes. Ctrl-C or Esc at a wizard question stops it with “Setup cancelled.” and exit code 130, before it has written anything (at the install offer, which comes after the files are written, the message says so and gives the install command). In the wizard, it offers every iOS simulator, with a booted one selected, and its next steps start with npx tapsmith verify. The config’s testMatch is ['**/*.tapsmith.ts'], a suffix Jest and Vitest don’t run by default (outside a __tests__ folder, where Jest runs every file), so the project’s own unit-test run doesn’t pick up Tapsmith tests and Tapsmith doesn’t pick up its unit tests; when the project already has Tapsmith tests named *.test.ts or *.spec.ts (from an older init, say), init warns that the new config won’t run them. Run outside a project that has Tapsmith installed (npx tapsmith init before npm i -D tapsmith, or a global install), it offers to install it with the project’s package manager, since the files it writes import tapsmith; declined, it prints the command as the first next step. In a directory without a package.json it does not offer the install, since a bare npm i -D tapsmith would install into the nearest parent directory that has one (a monorepo root, say): it warns, naming that directory (run the install there if it is the project’s root), and the next step creates the package.json first (npm init -y && npm i -D tapsmith, or the package manager’s equivalent). doctor, verify and the config-load error give the same command. An install that exits non-zero but leaves tapsmith importable counts as done: pnpm 11 and later exit 1 over the dependency build scripts they skipped, and for pnpm init then prints the allowBuilds snippet that silences that error (see Installation). In an Expo project (expo among the dependencies, plus app.json or app.config.*) it gives the Expo build commands when it finds no build (npx expo prebuild, then a release ./gradlew assembleRelease or xcodebuild build, with EXPO_PUBLIC_TAPSMITH_HOOKS=1 when the app uses @tapsmith/react-native), and pre-fills the package and bundle ID prompts from the app config when it can’t read them from the build.

Pass --yes or any setup flag below (every flag but --json) to run non-interactively instead, for scripts and AI agents: anything not given is auto-detected, and a choice that cannot be made (two APKs, say) exits 1 naming the candidates and the flag that picks one. --json only changes the output, so an agent passes --yes --json. Without a terminal and without --yes or a setup flag, init exits 1 rather than waiting for input. iOS physical devices need the interactive wizard, for its code-signing preflight.

Flag Meaning
-y, --yes Accept auto-detected defaults for anything not specified
--platform <list> android, ios, or android,ios (default: inferred — Android when --apk is given or there is an android/ directory, iOS when --app is given or, on macOS, there is an ios/ directory; in a React Native or Expo project on macOS that leaves iOS out, init warns, with how to add it)
--apk <path> Android APK (default: auto-detected under android/**/build/outputs/apk/)
--package <id> Android package name (default: read from the APK, else, in an Expo project, expo.android.package from the app config, with a warning)
--app <path> iOS simulator .app bundle (default: auto-detected under ios/)
--bundle-id <id> iOS bundle identifier (default: read from Info.plist, else, in an Expo project, expo.ios.bundleIdentifier from the app config, with a warning)
--avd <name> Android AVD to auto-launch (default: first available; with --network-capture, the first that supports HTTPS capture — Google Play images don’t, and init warns if it can only pick one)
--simulator <name> iOS simulator name (default: a booted iOS simulator, else the newest available iPhone)
--device-type <type> emulator, physical or both (default: emulator)
--network-capture Enable HTTP(S) capture in traces
--no-example-test Skip scaffolding tests/example.tapsmith.ts
--no-agents-md Skip scaffolding the AGENTS.md section
--force Overwrite an existing tapsmith.config.*
--json Machine-readable output, on success and on errors (see JSON output); needs --yes or a setup flag
Terminal window
npx tapsmith init # interactive wizard
npx tapsmith init --yes # accept every detected default
npx tapsmith init --yes --platform android --apk ./app-debug.apk --json

See Using Tapsmith with AI coding agents for the non-interactive setup loop.

Run a non-interactive system health check. Verifies all prerequisites: Node.js version, daemon binary, config file, ADB (Android), connected devices, agent APKs, AVD system image compatibility, Xcode (iOS), simulators, and network capture dependencies. Each check prints ✓, ⚠ (warning) or ✗ (error); most warnings and errors are followed by a ↳ line with the fix, often the exact command to run. Exits with code 0 unless a check is an error.

Which platforms are checked follows the config. A platform the config targets (per project use.platform, else the root platform; when unset, iOS if app, simulator or iosXctestrun is set, otherwise Android) is always checked. When it is the config’s only platform, a missing ADB or Xcode is an error, as is an iOS-only config on a non-Mac; when the config targets both, it is a warning as long as the other platform can run here (select its projects with --project), and an error when neither can. A config (or project) that sets app, simulator or iosXctestrun without platform is an error (config-platform), because tapsmith test refuses it. A platform the config does not target is skipped, with a – skipped: … line saying so. With no config file (or one that cannot be loaded), doctor checks whatever is installed: a missing ADB or Xcode is a warning that the platform’s checks were skipped, not an error — unless neither platform can run here (no ADB on PATH, and no Xcode or not a Mac), which is a no-platform error: doctor exits 1 and its summary line ends — no platform can run tests on this machine. A missing ADB’s fix depends on what is there: the platform-tools directory to add to PATH when an Android SDK has it, installing platform-tools when the SDK lacks them, or installing Android Studio (or the command-line tools) and setting ANDROID_HOME when there is no SDK at all. An x64 Node running under Rosetta on Apple Silicon is a node-arch warning, with the fix (an arm64 Node, then reinstall dependencies); tapsmith init and tapsmith test print the same warning. A Yarn Plug’n’Play install is a yarn-pnp error, since tapsmith test, verify and mcp-server refuse to run there (see Installation). The mitmproxy and Network Extension checks run only when iOS is checked. When an Android project sets avd, doctor also looks for the emulator binary the way tapsmith test does ($ANDROID_HOME/emulator, $ANDROID_SDK_ROOT/emulator, the default SDK location, then PATH) and lists every path it tried; a missing emulator is an error when Tapsmith would launch the AVD, and a warning with launchEmulators: false or when this machine only runs a mixed config’s iOS projects.

--json prints { ok, checks, inventory }: the checks (with a fix wherever the text output has one) and the device inventory (AVDs, simulators, connected devices), described under JSON output. A skipped platform has no checks in the JSON. -c / --config checks a specific config file; without it, doctor finds the config the same way tapsmith test does (tapsmith.config.ts, .js or .mjs in the current directory).

Terminal window
npx tapsmith doctor
npx tapsmith doctor --json -c tapsmith.config.ci.mjs # machine-readable, against a specific config

Prove the setup works end to end: runs one test file through the real tapsmith test path (daemon, device or emulator launch, app install) and reports whether it passed. It picks example.tapsmith.ts (or an older init’s example.test.ts) if the project has one, otherwise the first test file; a project with no tests yet gets a throwaway smoke test, removed afterwards. Exits 1 when the run fails, when no test ran (the file has none, every one was skipped, or a grep / grepInvert filtered them all out — a pass with nothing run proves nothing), when there is no tapsmith.config.* (unless -c names one), or when the config cannot be loaded.

--json prints { ok, passed, failed, skipped, duration, failures, testFile }, or the JSON error envelope. -c / --config uses a specific config file.

Terminal window
npx tapsmith verify
npx tapsmith verify --json

tapsmith telemetry [status|enable|disable] [--json]

Section titled “tapsmith telemetry [status|enable|disable] [--json]”

Show or switch the anonymous usage telemetry described in Telemetry. status (the default) says whether this process would report and, if not, which switch decided it: the environment (TAPSMITH_TELEMETRY, DO_NOT_TRACK), the project config (telemetry: false), or the machine-wide switch. disable turns it off for every project on this machine by writing to ~/.tapsmith/telemetry.json; enable turns it back on (it cannot override the environment or a config opt-out). --json emits the status object for scripting (see JSON output). Pass -c <file> to consult a specific config.

Terminal window
npx tapsmith telemetry # status
npx tapsmith telemetry disable
npx tapsmith telemetry enable
npx tapsmith telemetry status --json

Run the MCP server on stdio, for an AI coding agent to launch as a subprocess. It runs its own headless test session, with its own daemon and device. -c / --config uses a specific config file. Stdout carries the MCP protocol, so messages and usage errors go to stderr. See the MCP Server Guide for setup.

Terminal window
claude mcp add tapsmith -- npx tapsmith mcp-server
codex mcp add tapsmith -- npx tapsmith mcp-server --config tapsmith.config.ios.mjs

tapsmith create-avd [--api <level>] [--name <name>] [--device <profile>] [--abi <abi>] [--force] [--install-tools]

Section titled “tapsmith create-avd [--api <level>] [--name <name>] [--device <profile>] [--abi <abi>] [--force] [--install-tools]”

Create an Android AVD that supports HTTPS network capture. Downloads a Google APIs system image with sdkmanager and creates the AVD with avdmanager — Google Play images (the ones Android Studio preselects) block adb root, so Tapsmith cannot decrypt HTTPS traffic on them (see Android emulator image requirements).

If the Android SDK command-line tools are missing (Android Studio doesn’t install them by default), the command offers to download and install them into $ANDROID_HOME/cmdline-tools/latest for you — pass --install-tools to skip the prompt in scripts/CI. When no java is available, Android Studio’s bundled JDK is used automatically. A system image that is already installed is not re-downloaded; the Android emulator is installed alongside it when the SDK lacks one. The AVD is created in the AVD home the emulator reads ($ANDROID_AVD_HOME when set), so Tapsmith can launch it straight away.

Defaults: API level 36, name Tapsmith_Phone_API_<api>, device profile medium_phone, ABI matching the machine’s architecture (arm64-v8a on Apple Silicon — including when Node runs under Rosetta — and x86_64 on Intel). --force overwrites an existing AVD with the same name.

Terminal window
npx tapsmith create-avd # Tapsmith_Phone_API_36
npx tapsmith create-avd --api 34 --device pixel_7
npx tapsmith create-avd --install-tools # non-interactive bootstrap (CI)

Print a table of every device Tapsmith can target: Android (ADB), iOS simulators (simctl), and iOS physical devices (devicectl). Each row shows a one-line status (Ready or an imperative fix). An Android device adb lists but cannot use — unauthorized (the USB-debugging prompt was not accepted), offline, or no permissions (Linux without udev rules) — is listed as not ready with the same fix tapsmith doctor gives; Tapsmith never selects one for a test run. When a run finds no usable device, its error also names any attached device adb cannot use, with its adb state. A device another running Tapsmith session has claimed shows In use by that session’s command and pid. With no devices at all, it says per platform how to start one — or, when that platform’s toolchain is missing (no ADB on PATH, no Xcode), what to install first. An Android test run that finds no device (or would launch an emulator) and an iOS run whose simulator cannot be found give the same toolchain advice. --json prints { devices }, or the JSON error envelope when the daemon cannot be started or cannot list devices.

Terminal window
npx tapsmith list-devices
npx tapsmith list-devices --json

Open the trace viewer in the default browser to inspect a recorded trace. Traces are written to <outputDir>/traces/ (tapsmith-results/traces/ by default), named trace-[<project>-]<test name>[-retry<n>]-<timestamp>.zip.

Terminal window
npx tapsmith show-trace tapsmith-results/traces/trace-my_test-1767225600000.zip

The trace viewer shows:

  • Actions panel — chronological list of actions with icons, locators, and durations
  • Timeline filmstrip — screenshot thumbnails for quick navigation
  • Screenshot panel — before/after screenshots with tap coordinate overlays
  • Detail tabs — Call info, Console output, Source code, View hierarchy, Network requests, Errors
  • Keyboard navigation — Arrow keys or j/k to move between actions

Record traces during test execution. Overrides the trace config option. mode is one of the TraceMode values: off, on, on-first-retry, on-all-retries, retain-on-failure, retain-on-first-failure, retain-on-failure-and-retries. A bare --trace means on. Any other value is an error that lists the valid modes, so a typo such as --trace retain-on-falure fails the run instead of silently recording nothing. The same check applies to trace in the config file, in a project’s use, and in test.use().

Terminal window
npx tapsmith test --trace on # Record all tests
npx tapsmith test --trace retain-on-failure # Only keep traces for failures

A bare --trace directly followed by a test file reads the file as the mode (and fails): write --trace on login.test.ts, or put the file first.

Record an MP4 of the device screen for the lifetime of each test. Mirrors Playwright’s video flag and overrides the video config option. Accepts the same mode set as --trace, validated the same way (a bare --video means on; an unknown mode, here or in the config, is an error). See Video recording.

Terminal window
npx tapsmith test --video on # Record every test
npx tapsmith test --video retain-on-failure # Only keep videos for failed tests

Merge blob reports from sharded CI runs into a single HTML report.

Terminal window
# After collecting all shard blob-report/ directories:
npx tapsmith merge-reports # reads from blob-report/
npx tapsmith merge-reports ./blobs # custom directory
npx tapsmith merge-reports -c tapsmith.config.ci.ts # reporters from a specific config

The merged results go to the reporters in the config (-c / --config picks the file).

It checks the blobs before merging. It refuses to merge, printing a one-line error and exiting with status 1, when:

  • the directory holds no blob reports (*.jsonl), for example after a failed artifact download;
  • a file is not a valid blob report (Invalid blob file <name>: …);
  • a blob was written by a newer Tapsmith (its blob format is newer than this version reads);
  • the sharded blobs aren’t one split: a shard appears twice, the blobs come from different splits (say 1/3 and 2/4), or an unsharded blob sits among sharded ones. Merge each run, such as Android and iOS, from its own directory. A set of unsharded blobs is merged without these checks.

When a shard is missing, the shards that are present are still merged and reported, so CI keeps a report to triage from. The merged status is failed, the missing shards are named, and the command exits 1.

Once a complete set is merged, the command exits 0 even if the merged run has failed tests. Playwright’s merge-reports does the same, since the shard jobs already carry the failure. The last line gives the merged status, e.g. Merged 3 blob reports (shards 1–3 of 3): failed — 41 passed, 2 failed. A blob reporter in the config is skipped here, because merging reads blobs and never writes one. If blob is the only reporter configured, the merge falls back to list.

Open the HTML test report in the default browser.

Terminal window
npx tapsmith show-report # opens tapsmith-report/index.html
npx tapsmith show-report ./my-report # custom directory

iOS setup commands (macOS only), in two tracks:

  • Physical iPhone/iPad: tapsmith ios setup-device (preflight), then tapsmith ios build-agent (the signed runner). See docs/ios-physical-devices.md for the full walkthrough; tapsmith list-devices (above) lists physical devices too, with their UDIDs.
  • Network capture, under tapsmith ios network: one setup for simulators, and a per-device profile for physical devices.

iOS simulators need no setup to run tests. tapsmith ios or tapsmith ios network on its own prints that group’s help and exits 0.

Renamed after 0.5.0. These commands used to be top-level. The old names were removed without aliases; they now fail as unknown commands.

0.5.0 and earlier Now
tapsmith setup-ios-device tapsmith ios setup-device
tapsmith build-ios-agent tapsmith ios build-agent
tapsmith setup-ios tapsmith ios network setup-simulator
tapsmith configure-ios-network <udid> tapsmith ios network configure <udid>
tapsmith refresh-ios-network <udid> tapsmith ios network configure <udid> --refresh
tapsmith verify-ios-network <udid> tapsmith ios network verify <udid>

Run the host-side preflight for physical iOS devices (macOS only). Takes no arguments: it checks the Mac, then every device xcrun devicectl lists. Host checks: Xcode command-line tools, xcrun devicectl, libimobiledevice (iproxy), a code signing identity and an Apple Developer team registered with Xcode; and, as advisory checks, passwordless sudo for the Developer Disk Image mount (so a test run does not stop at a password prompt), a signed agent runner (found the way tapsmith test finds it, taking the current directory as rootDir: see ios build-agent below; a runner an earlier Tapsmith version built for an npm install is flagged for a rebuild), and the provisioning profile’s expiry. Device check: each listed device is paired and does not have Developer Mode off (unknown, as on devices before iOS 16, passes). devicectl also lists devices it remembers that are not plugged in. It does not check the Developer Disk Image (Tapsmith mounts it when a test run starts) or the macOS firewall (ios network configure --fix-firewall handles that). Each check prints ✓, ⚠ (advisory, not blocking) or ✗ with the fix; the command exits 1 if a required check fails, no device is listed, or any listed device is unpaired or has Developer Mode off. When the required checks pass, it ends with the steps it cannot check from the Mac (trusting the developer certificate, turning off Auto-Lock) and notes that tapsmith test finds the device and the runner itself. Either way it ends by pointing at network capture: tapsmith ios network configure <udid> for the device, tapsmith ios network setup-simulator for a simulator. --json prints the same checks and the listed devices as { ok, checks, devices } (see JSON output), with the same exit code.

tapsmith ios build-agent [--team-id <id>] [--cwd <path>] [--derived-data-path <path>] [-v]

Section titled “tapsmith ios build-agent [--team-id <id>] [--cwd <path>] [--derived-data-path <path>] [-v]”

Build the signed TapsmithAgent XCUITest runner for physical devices with your provisioning profile. Auto-detects the Apple Developer team ID from Xcode’s preferences (or keychain) if --team-id is omitted. -v / --verbose streams the raw xcodebuild output.

The agent source comes from <cwd>/ios-agent/ in a Tapsmith checkout (--cwd points at one), or else from the copy the npm package extracts to ~/.tapsmith/ios-agent/. Build products go to .build-device inside that source directory (ios-agent/.build-device, or ~/.tapsmith/ios-agent/.build-device) unless --derived-data-path says otherwise. Without iosXctestrun (or the TAPSMITH_IOS_XCTESTRUN environment variable), tapsmith test uses the newest device runner under an ios-agent/.build-device in or up to five levels above rootDir (a checkout), and otherwise the one under ~/.tapsmith/ios-agent/.build-device (an npm install), but only if the installed Tapsmith version built it. Upgrading Tapsmith replaces ~/.tapsmith/ios-agent/ the next time Tapsmith builds an agent, so rebuild after an upgrade; until then tapsmith test reports that the runner was built by the earlier version. When the build finishes, the command says whether tapsmith test will find the runner on its own. For a checkout build it also prints an absolute iosXctestrun: line to pin it, and for a build the lookup will not find (such as one with --derived-data-path) the line to add; it offers no pin for the npm build, which an upgrade replaces. An iosXctestrun or TAPSMITH_IOS_XCTESTRUN naming a file that does not exist stops the run before the agent starts, with the path in the error.

Network capture setup. Simulators capture through mitmproxy and a macOS Network Extension (setup-simulator, once per Mac). Physical devices capture through a Wi-Fi proxy profile (configure, then verify, once per device and Wi-Fi network). See iOS network capture and physical-device network tracing.

First-run setup for iOS simulator network capture. Checks, one step at a time, that mitmproxy is installed with Homebrew and that its Mitmproxy Redirector Network Extension is registered and approved; for the approval step it opens System Settings on the right pane and waits for you to allow it. Each step prints what to do next, and every run ends by pointing physical-device users at tapsmith ios setup-device.

Exits 0 when capture is ready, and also on a fresh Mac where the extension is not registered yet: that is expected, since the first simulator test run with network capture registers it and macOS then asks you to allow it. Exits 1 when mitmproxy is missing, the approval wait times out, or the extension’s state cannot be read.

Terminal window
npx tapsmith ios network setup-simulator

tapsmith ios network configure <udid> [--refresh] [--ssid <name>] [--device-name <name>] [--fix-firewall]

Section titled “tapsmith ios network configure <udid> [--refresh] [--ssid <name>] [--device-name <name>] [--fix-firewall]”

Generate a .mobileconfig profile that routes the physical device’s Wi-Fi traffic through Tapsmith’s MITM proxy, and reveal it in Finder so you can AirDrop it to the device. The command then prints the install walkthrough: send and install the profile, trust the Tapsmith CA, enter the proxy (PAC) URL in the device’s Wi-Fi settings, and verify. Decrypted capture is available for clients that trust the Tapsmith CA; pinned or embedded-root clients may need passthrough. --ssid targets a specific Wi-Fi network (defaults to the host’s current SSID); --device-name sets the profile’s PayloadDisplayName; --fix-firewall turns off macOS Application Firewall stealth mode (via sudo), which otherwise drops the device’s connections to the proxy.

--refresh regenerates the profile after the Mac’s IP address or Wi-Fi network changed. Its walkthrough starts with removing the old profile from the device, and prints the new proxy URL to enter, since that URL carries the Mac’s IP. tapsmith test warns with this command when it detects the drift.

End-to-end sanity check that the installed profile plus the trusted CA actually produce decrypted HTTPS capture for a normal system-trust client. Starts the proxy, asks you to load an HTTPS page in Safari on the device, then reports whether Tapsmith saw the request and could decrypt the body. Exits non-zero on failure with fix-it hints for each failure mode.

Print the Tapsmith version.

tapsmith --help / tapsmith -h / tapsmith help [command]

Section titled “tapsmith --help / tapsmith -h / tapsmith help [command]”

Show the available commands, or one command’s options (tapsmith help test, same as tapsmith test --help). Nested commands work the same way: tapsmith help ios network configure is tapsmith ios network configure --help. Help never runs the command: tapsmith --help init prints the command list rather than starting the wizard.


Tapsmith records continuous screen video over the duration of each test, mirroring Playwright’s video config (PILOT-114). Retained videos land in <outputDir>/videos/ as <safe-test-name>-<timestamp>.mp4 and are surfaced on TestResult.videoPath. The HTML reporter embeds an inline <video> element and a download link in each test’s detail panel.

import { defineConfig } from 'tapsmith'
export default defineConfig({
video: 'retain-on-failure', // mode shorthand
// — or —
// video: {
// mode: 'on',
// size: { width: 1280, height: 720 }, // Android only
// },
})

The supported modes are the same as trace:

Mode Behaviour
off No recording (default).
on Record every test, retain every video.
retain-on-failure Record every test; keep videos only for failed tests.
retain-on-first-failure Like retain-on-failure but limited to attempt 0.
on-first-retry Record only on the first retry attempt.
on-all-retries Record on every retry attempt (skips the first run).
retain-on-failure-and-retries Record every test; keep the video for any run that failed or that is a retry.

Recording cost: retain-on-failure still records (and pays the video encode cost for) every test — it only deletes the file for passing tests afterwards. on-first-retry starts no recorder at all on the first attempt, eliminating encode load on healthy runs while still producing a video for any test flaky or broken enough to be retried. The trade-off: it requires retries >= 1 to ever produce a video (the runner warns at startup if retries is 0), and the recorded retry won’t show first-attempt-only failures. This mirrors Playwright’s on-first-retry caveat.

Platform Recorder
Android adb shell screenrecord (3-min hard cap per recording).
iOS Simulator xcrun simctl io <udid> recordVideo --codec h264.
iOS physical device ffmpeg -f avfoundation — requires ffmpeg on PATH.

Android 3-minute cap: screenrecord truncates at 180 seconds. Recordings of longer tests are truncated by the device-side encoder; the daemon emits a one-time warning per test that exceeds the cap. Chained-segment recording is on the roadmap.

iOS physical devices: ffmpeg’s AVFoundation backend captures the iPhone’s screen via the CoreMediaIO video device that appears on macOS once the device is paired in Xcode and you have accepted the “Trust This Computer” prompt. The daemon resolves the device by friendly name; if multiple iPhones are connected, name conflicts can cause the wrong device to be recorded. See ios-physical-devices.md.

size option: honoured on Android only (passed through as screenrecord --size WxH). On iOS the daemon emits a one-time warning and records at native resolution.