Skip to content
NewiPhone Duo, live in your browser. Try it →

Reference CLI

VibeView CLI: Run Mobile Tests from a Terminal

Install the CLI, authenticate, upload a build, and run mobile tests from your terminal or CI job — full command, flag and exit-code reference.

Run VibeView tests from the command line. Integrate with CI/CD pipelines for automated mobile testing.

Quick Start

# 1. Install
npm install -g vibeview

# 2. Log in (opens browser)
vibeview login --base-url https://vibeview.io

# 3. Run a group of tests
vibeview run-suite <suite-id> --output human

Installation

The CLI is a public npm package:

npm install -g vibeview

Requirements: Node.js 20 or later.

After installation, the vibeview command is available in your terminal.

Once a day at most, the CLI checks whether a newer version is published and, if so, prints a one-line notice on stderr with the command to update. It stays silent in CI, when stderr isn’t a terminal, with --json or --output json/junit, and for vibeview mcp. Set NO_UPDATE_NOTIFIER=1 to turn it off.

Authentication

The CLI resolves your API token in this order:

  1. --token flag — pass a token directly on each command.
  2. VIBEVIEW_API_TOKEN environment variable — best for CI environments.
  3. Config file — saved automatically by vibeview login at ~/.vibeview/config.json.

If no token is found, the CLI exits with an error and suggests using the --token flag, setting VIBEVIEW_API_TOKEN, or running vibeview login.

The server URL defaults to https://vibeview.io and is saved when you log in, so commands don’t ask for it. To use a self-hosted or staging instance, pass --base-url to vibeview login (it is stored for later commands) or set VIBEVIEW_BASE_URL.

Commands

login

Log in to VibeView interactively. Opens your browser to complete authentication, then saves the token locally.

vibeview login --base-url https://vibeview.io
OptionDescription
--base-url <url>VibeView server URL (defaults to https://vibeview.io)

The command:

  1. Opens your browser to the VibeView login page (you are taken straight to the authorization screen if you’re already signed in).
  2. Asks you to confirm with an Authorize button — only approve requests you started from your own terminal.
  3. Waits up to 2 minutes for you to complete the authorization.
  4. Saves the token to ~/.vibeview/config.json (file permissions 0600).

logout

Log out of VibeView: revokes the saved login token server-side and removes it from ~/.vibeview/config.json.

vibeview logout

Each vibeview login creates an API token that stays active until revoked, so use vibeview logout (rather than just deleting the config file) when you’re done on a machine. If the server can’t be reached, the token is still removed locally and a warning tells you to revoke it in the dashboard (Settings → API token). Tokens supplied via VIBEVIEW_API_TOKEN or --token are not affected.

whoami

Show who the CLI is authenticated as: your email, organization, and role.

vibeview whoami [--json]
OptionDescriptionDefault
--jsonPrint the result as one JSON line (email, memberships, role, token_role, effective_role, …)(human output)
--token <token>API token(from auth resolution)

Sample output:

Logged in as  dev@example.com
Organization   Example Inc
Member role    admin
Token role     developer
Effective role developer
Access         Full access
Expires        never

The effective role is the lower of your member role and the API token’s own role, so an admin whose CLI token was created with the developer role can’t run admin-only commands such as vibeview env set. When that is the case, whoami says so and suggests creating an admin API token in the dashboard.

Access and Expires describe the API token the CLI used: Full access or CI only (upload builds, run tests, read results) for a CI-only token, and its expiry date with the days left, or never. With a CI-only token, whoami doesn’t suggest an admin token: a CI pipeline needs only what a CI-only token can do. In --json output these are token_access (full or ci) and token_expires_at (null when the token never expires). A missing or invalid token exits with status 2.

agent-setup

Make VibeView discoverable to your coding agent: installs the vibeview-agent skill where Claude Code auto-discovers it, optionally registers the MCP server, and suggests a CLAUDE.md line for your project.

vibeview agent-setup
OptionDescription
--projectInstall the skill into this project’s .claude/skills/ instead of ~/.claude/skills/
--mcpRegister the MCP server with Claude Code without asking
--no-mcpSkip MCP registration

See AI Agent Control for the full agent workflow.

dev

Stream your app from a live device with Metro hot-reload, for local React Native development. See the Live Development guide for the full workflow.

The first time you run vibeview dev in a project, it walks you through setup: which platform to run, an optional build command (with a detected default offered for Expo and React Native projects, and from CLI 0.42.0 for Flutter, Capacitor and native Xcode or Gradle projects — you can accept it, enter your own, or skip it), and an optional command that starts Metro (same detected-default-or-skip choice). Your answers are saved to a vibeview.json file in the project so later runs start immediately with no prompts. Any prompt can be skipped — vibeview dev still works without a build or Metro command configured, and skipped prompts print a hint showing the exact keys to add to vibeview.json later if you change your mind.

Under --json, or with no interactive terminal attached (piped input, a background/CI process), the setup prompts above never fire — there’s nothing on the other end to answer them. If --platform and --app are both passed and there’s an app to run, vibeview dev starts straight from the flags instead, writing vibeview.json from them exactly as an interactive first run would (announced on stderr) so later runs pick it up too. If something still can’t be resolved without a prompt (no --platform and no cached default, or no --app and nothing cached for the platform), it exits with a {"event":"error","code":"config_required",...} line naming the exact flag that’s missing, instead of guessing or hanging.

After setup, if there is no app to run yet for the chosen platform — right after setup, or later when vibeview.json has a build command but no appId — vibeview dev builds and uploads one when you configured a build command, or sends you to the app picker when you skipped it. If your account already has apps for the platform, it asks whether to build a fresh one now or reuse an existing upload; --build skips the question and always builds first.

Once a build is selected, vibeview dev starts a device session, connects your local Metro bundler to the app running on the device, and opens the session in your browser. Press Ctrl-C to end the session.

vibeview dev runs the latest debug build of the app you pick (Android debug .apk or iOS simulator build). If the app has no debug build, or the build can’t be installed on the device (for example an Android build without an x86_64 architecture), it stops with a clear message instead of opening a session you can’t use — upload one with vibeview build or upload-app first, or rebuild with x86_64 included.

The first run creates vibeview.json with the platform, app and Metro port it used, so the next bare vibeview dev repeats it. Your team usually commits that file, so later flags never rewrite what it says: a --metro-port, and a --platform, --app or --device-category that differs from vibeview.json, are remembered for you alone in .vibeview/local.json (git-ignored, see below), and vibeview dev prints what it saved, for example Saved to .vibeview/local.json (yours only, not committed): defaultPlatform android, android.metroPort 8093. A bare vibeview dev then uses them: a flag wins over .vibeview/local.json, which wins over vibeview.json. Passing the value vibeview.json has removes the override again.

.vibeview/ stays out of git on its own: the CLI writes a .vibeview/.gitignore that ignores everything in the folder except test-spec.json (the file CI reads, which you commit). It works whether or not your project has a .gitignore, and the CLI doesn’t add lines to yours. When nothing in vibeview.json changes, the CLI leaves the file exactly as you formatted it.

vibeview.json still gains what it doesn’t have yet (a new platform’s entry and app, or an app picked because the configured one was deleted), and vibeview dev prints each change, for example Updated vibeview.json: platforms.android.appId (added) atlas-and, so a file your team commits never changes unnoticed.

If the app configured for a platform was deleted, vibeview dev warns and asks you to pick again; a stale --app <id> exits with an error telling you to rerun without --app.

vibeview dev
OptionDescriptionDefault
--token <token>API token(from auth resolution)
--platform <platform>ios, android, tvos, androidtv, or roku(asked interactively on first run)
--app <app_id>Uploaded debug build to run (see vibeview list-apps)(asked interactively on first run)
--buildRun your configured build command and upload the result before starting the session(off)
--build <id>Run this uploaded build instead of the app’s latest debug build (ids from vibeview list-builds <app>). It must be a debug build of the app for the platform (any build with --standalone) — see below. Not available on Roku(latest debug build)
--standaloneRun the build as it is, with no Metro: for a release build, a CI build or a cloud build you just want to open. Code changes don’t hot-reload. Without --build <id> it runs the app’s newest release build. Not available on Roku(off)
--metro-port <port>Local Metro port. Remembered for you in .vibeview/local.json, never written to vibeview.jsonvibeview.json’s port, else 8081
--device-category <category>phone or tablet (TV platforms always use a TV device)phone
--model <name>Run on this exact device model, e.g. "iPhone Duo" or "Pixel 9 Pro Fold" (names from vibeview list-devices --models). The model sets the category, so --device-category isn’t needed (if you pass both, they must agree); not saved to vibeview.json(any phone that doesn’t fold)
--no-openDon’t open the browser automatically(opens by default)
--detachRun the session in the background and exit after printing its details; check on it with vibeview dev-status (which also shows the build running on the device: its id, version and build number), end it with vibeview dev-stop, and on Roku reload the channel with vibeview dev-reload. If the background process has exited while its session is still up, dev-status says so and dev-stop still ends the session; either way, a session whose background process is gone ends on its own within about 90 seconds, unless a browser tab is still watching it. A background session that nobody uses still ends after your organization’s idle timeout (30 minutes by default; see “When a dev session is idle” below)(foreground)
--jsonEmit line-delimited JSON events instead of human output(human output)
--watch [seconds]Roku only: reload automatically once file changes settle, after the given quiet period(off; a bare --watch waits 2 seconds)
--debugLog raw tunnel lifecycle events for diagnostics(off)

Example: pin the platform and app to skip the interactive prompt:

vibeview dev --platform ios --app abc123

Example: rebuild and upload after a native change, then start the session:

vibeview dev --build

Example: run an older debug build of the app, in the background:

vibeview list-builds abc123             # find the build id; KIND must be "debug"
vibeview dev --platform ios --app abc123 --build 4812 --detach --json

A bare --build builds first; --build <id> runs a build you already uploaded. The build has to be the kind vibeview dev would pick on its own: a React Native debug simulator or emulator build, which loads its JavaScript from Metro. That includes a debug build that also embeds its JS bundle, as Expo projects without expo-dev-client do (including their cloud builds): it still asks Metro first and uses the embedded bundle only when Metro isn’t there. vibeview dev stops with an error before starting a session when the build belongs to a different app, is a release build, or its kind couldn’t be detected. --build <id> combines with --model, --platform, --app and --detach --json.

Without --build <id>, it runs the app’s newest debug build, unless the project’s own app version can be read (from the Expo config, including a dynamic app.config.ts when Expo is installed in the project, or from the native project) and an older debug build has that version while the newest does not; it then runs that one and says why. If the version can’t be read, it says so. When the session starts, it prints the build it installed (id, version, upload time and note), the newest debug build of each other version, and the project’s React Native version. If the app shows a red error screen right after it starts, the build may have been made from other native code than your project: run a build of your project’s version with --build <id>. --detach prints the same lines, and with --json the session_ready event carries them as build.

When a dev session is idle. A dev session stays up while it is being used: touching the device in the browser, speaking into the device’s microphone, a test step running on it, an agent acting on it (a tap, typing, a swipe, a button press), or an app reload when you save a file (on Roku, reloading the channel). Keeping vibeview dev running doesn’t count, and neither does a stream nobody touches, reading the screen, the UI tree or the logs, finding an element, or waiting. When nobody has used the session for your organization’s idle timeout (30 minutes by default), it ends. About a minute before that, vibeview dev prints a line such as Session ends in 52 s: use the device or save a file to keep it. A Roku session suggests reloading the channel instead, and a --standalone session, which has no Metro, only suggests using the device. The line appears once, and again only if the session is used and then goes idle again. With --json, it is an idle_warning event carrying session_id and seconds_remaining. A --detach session has no terminal to print to. The session’s browser tab shows the same warning.

If another VibeView dev connection takes over the same session once it is running, vibeview dev exits quietly and leaves the session running for it. A Metro that vibeview dev started keeps running for the new connection. If that happens while the session is still starting, the start fails.

Example: open a release or cloud build as it is, with no Metro:

vibeview dev --standalone --platform ios --app abc123 --build 4790

Running on a foldable

Any device with a hinge is a foldable — the iPhone Duo (iOS) and Android foldables such as the Pixel 9 Pro Fold. vibeview list-devices --models marks each one foldable. Each has a smaller cover screen when closed and a larger inner screen when open. A session or run that asks only for a phone or tablet (the default) never gets a foldable, even when a foldable is the only device free — it waits for a phone instead. To develop on one, pass its model name:

vibeview list-devices --models          # foldables are marked "foldable"
vibeview dev --platform ios --model "iPhone Duo"
vibeview dev --platform android --model "Pixel 9 Pro Fold"

A model name that isn’t available right now stops dev with the list of models you can use instead. The session starts closed, on the cover screen. From there you (or your coding agent) can fold it to a preset or an exact hinge angle, and rotate it:

vibeview set-posture open               # closed, partial, or open
vibeview set-posture --angle 75         # any whole angle from 0 (shut) to 180 (flat)
vibeview rotate                         # iPhone Duo: a quarter turn clockwise
vibeview rotate --degrees 270           # iPhone Duo: a quarter turn back; 180 turns it upside down

Each command prints where the device ended up — the posture, the angle and which screen is lit, or the new orientation. The presets are the same angles on every foldable, but each device names an exact angle by its own ranges — 160 degrees reads partial on the iPhone Duo and open on the Pixel 9 Pro Fold. An Android foldable rotates like a phone, between portrait and landscape. See Agent Control for every command that drives the device.

Your choices are remembered per platform in the project’s vibeview.json, so one project can hold a phone app and a TV app side by side. A bare vibeview dev reruns whatever platform you used last; --platform <name> switches (and remembers the app you pick for it). Switching to another platform than vibeview.json’s defaultPlatform is remembered in your .vibeview/local.json, so a teammate’s default doesn’t change:

{
  "defaultPlatform": "android",
  "metroPort": 8081,
  "metroCommand": "yarn start",
  "platforms": {
    "android": {
      "appId": "myapp-a1b2c3",
      "deviceCategory": "phone",
      "build": {
        "command": "cd android && ./gradlew assembleDebug",
        "artifact": "android/app/build/outputs/apk/debug/*.apk"
      }
    },
    "androidtv": { "appId": "myapp-tv-d4e5f6" }
  }
}

metroCommand (and metroPort) can also be set per platform — inside a platform’s entry in platforms — to override the top-level value, for projects that start Metro differently per target:

{
  "defaultPlatform": "ios",
  "metroCommand": "yarn start",
  "platforms": {
    "ios": { "appId": "myapp-a1b2c3" },
    "tvos": { "appId": "myapp-tv-d4e5f6", "metroCommand": "yarn start:tv", "metroPort": 8082 }
  }
}

Metro auto-start

vibeview dev checks whether Metro is already answering on the configured port before starting a session:

  • If a Metro (or Metro-compatible packager) is already running there, vibeview dev uses it as-is — it never touches a Metro it didn’t start.
  • If nothing is running on the port and metroCommand is configured (top-level or per-platform), vibeview dev starts it for you, prefixing its output with [metro], and stops it automatically when the session ends.
  • If vibeview dev started Metro for one platform and you switch to a platform whose effective Metro command is different, it restarts Metro with the new platform’s command.
  • If nothing is running and no metroCommand is configured, vibeview dev stops with a clear error telling you to start Metro yourself or add metroCommand to vibeview.json.

Roku

vibeview dev --platform roku streams a Roku channel the same way, with one difference: Roku has no JS bundle to hot-reload, so instead of a Metro connection the session gives you a manual reload loop.

In a Roku channel project, the first run asks for two values instead of a build command and a Metro command, saved to vibeview.json under platforms.roku.build:

  • command — the shell command that packages your channel into a zip.
  • artifact — the path to the zip that command produces.
{
  "defaultPlatform": "roku",
  "platforms": {
    "roku": {
      "appId": "mychannel-a1b2c3",
      "build": {
        "command": "npm run package",
        "artifact": "out/channel.zip"
      },
      "watchIgnore": ["docs", "tests/fixtures"]
    }
  }
}

Once the session is running, the terminal accepts these keys — just press the key, no Enter needed:

KeyAction
rRe-package the channel, upload it, and reload it into the session you’re already watching — the channel restarts on the device a few seconds later.
q (or Ctrl-C)End the session.

A reload already in progress ignores a repeat r instead of queuing another one.

--watch [seconds] turns on automatic reload: after a file in the project changes, vibeview dev waits for the given quiet period with no further changes, then reloads on its own — no need to press r. A bare --watch uses a 2-second quiet period; --watch 5 waits 5 seconds instead.

vibeview dev --platform roku --watch
vibeview dev --platform roku --watch 5

A file you save while a reload is running is not lost: one more reload runs once the current one finishes, however many times you saved.

--watch already skips the zip your packaging command produces. Add watchIgnore (optional, shown above) to skip more: a list of paths relative to your project root, each covering everything inside it, for the folders you edit without wanting a reload — docs, test fixtures, planning notes. It adds to the paths already skipped rather than replacing them. If your packaging command writes working files into a folder that is watched, the channel keeps reloading itself — add that folder to watchIgnore to stop it.

If your packaging command only deploys straight to a device instead of producing a zip, the reload fails with a message telling you to use the package/zip step instead. A failed reload leaves the session running, so you can fix the problem and press r again.

If your channel fails to install when the session starts up — for example, the device rejects the package — vibeview dev reports the reason and ends the session instead of leaving you staring at a stream with nothing installed. This can be reported either while the session is still starting, or shortly after, once it’s already live.

If the session itself ends while you are watching it — it expires, or someone stops it from the dashboard — vibeview dev says so and exits instead of leaving you pressing r against a session that is gone.

build

Run your configured build command and upload the result — the same build-and-upload step vibeview dev --build runs before starting a session, available standalone for CI or a manual rebuild.

vibeview build

vibeview build reads platforms.<platform>.build from vibeview.json (a command to run from the project root, and an artifact path the build produces), runs the command with its output streamed to your terminal, then locates and uploads the artifact. The artifact path can end in a * glob in its filename (for example android/app/build/outputs/apk/debug/*.apk) — when more than one file matches, the most recently modified one is uploaded. A matched .app directory (iOS simulator builds) is zipped automatically before upload. On success, the uploaded app’s id is written back into vibeview.json as that platform’s appId, so the next vibeview dev run picks it up.

If no build command is configured for the target platform, vibeview build prompts for one interactively — offering a detected default command for Expo and React Native projects, and from CLI 0.42.0 for Flutter, Capacitor and native Xcode or Gradle projects, where one is recognized — and saves your answer to vibeview.json. Under --json or when not running in an interactive terminal, a missing build command is an error instead of a prompt.

With --cloud, the build command is checked before anything is uploaded: it stops when the command needs a package.json that the directory you ran it from doesn’t have, and warns when the command never installs your dependencies. See Cloud Builds.

OptionDescriptionDefault
--token <token>API token(from auth resolution)
--platform <platform>ios, android, tvos, androidtv, or roku; with --cloud, all targets every configured platform with a build blockvibeview.json’s defaultPlatform
--jsonPrint the result as one JSON line (app_id, name, platform) instead of human-readable output(human output)
--cloudBuild in the cloud instead of locally — see Cloud Builds(local build)
--production--cloud only: build a signed release artifact instead of a simulator/emulator build — see App Signing & Production Builds(simulator/emulator build)
--no-wait--cloud only: print build ids and exit without waiting(wait)
--download--cloud only: download each succeeded build’s artifact (implies wait)(no download)

Example: build and upload for a specific platform, capturing the app id in CI:

APP_ID=$(vibeview build --platform android --json | jq -r .app_id)

builds

Manage cloud builds: vibeview builds list | logs <id> [--follow] | download <id> [--out <dir>] | cancel <id> | delete-artifact <id>. delete-artifact removes a production build’s stored artifact file (irreversible; the build record, logs, and cost stay intact — only the downloadable file is removed). builds list shows each build’s creation time in UTC and its cost: - while a build is still queued or running, then the final amount ($0.00 for a build that cost nothing). See Cloud Builds for configuration, billing, and limits.

credentials

Manage signing credentials used by vibeview build --cloud --production:

vibeview credentials add --platform ios --file cert.p12 --profile app.mobileprovision [--app <id>]
vibeview credentials add --platform android --file release.jks [--app <id>]
vibeview credentials list [--app <id>]
vibeview credentials rm <name> [--app <id>]

Every password is prompted for interactively with hidden input — there is no password flag. See App Signing & Production Builds for what each platform needs, scoping, and expiry.

env

Manage build variables and secrets used by cloud builds: vibeview env list [--app <id>] [--json] | env set NAME [VALUE] [--secret|--sensitive|--plain] [--file <path>] [--app <id>] | env rm NAME [--app <id>] | env pull [--app <id>] [--out .env]. See Build Variables & Secrets for visibility levels, scopes, file variables, limits, and log masking.

list-suites

List the groups in your organization. The API and CLI call a group a suite; the command names follow.

vibeview list-suites --output human
OptionDescriptionDefault
--token <token>API token(from auth resolution)
--output <format>Output format: human, jsonhuman
--tag <tag>Filter by tag (repeatable)(all groups)
--match <mode>any or all — how multiple --tag values are combinedany

Example output (human):

  3 suite(s)

  abc123  Login Flow [ios] [smoke, auth]  (4 cases)
  def456  Checkout  [android]  (6 cases)
  ghi789  Onboarding [ios] [smoke]  (3 cases)

Tags (when present) are shown in brackets after the platform. Descriptions are not rendered in human output — use --output json to retrieve them.

Example output (JSON):

[
  {
    "id": "abc123",
    "name": "Login Flow",
    "platform": "ios",
    "case_count": 4,
    "description": "Covers email/password and SSO sign-in.",
    "tags": [
      { "name": "smoke", "display_name": "Smoke" },
      { "name": "auth", "display_name": "Auth" }
    ]
  }
]

Every group includes description (string or null) and tags (array, possibly empty). Use JSON output when you need either field — for example, to drive downstream tooling that picks groups by description text or tag membership.

Tag filtering example:

vibeview list-suites --tag profiles --tag smoke --match any

run-suite

Run every test of a group and wait for the results.

vibeview run-suite <suite-id> [options]
OptionDescriptionDefault
--token <token>API token(from auth resolution)
--output <format>Output format: human, json, junithuman
--verboseShow per-case breakdownfalse
--timeout <seconds>Max wait time in seconds, queue wait plus execution1800
--commit-sha <sha>Git commit SHA for GitHub status checks(none)
--model <model>AI model to use(server default)
--provider <provider>AI provider(server default)
--mode <mode>AI use: hybrid (Smart), ai (AI only), replay (No AI)(server default)
--device-category <category>Device category to allocate: phone, tablet, tvtv for tvOS and Android TV, otherwise phone
--device <device_id>Pin run to a specific device (see list-devices). Conflicts with --device-category.(none)
--build <id>Specific AppBuild id to run against (integer). Defaults to the latest build of the app the run installs when omitted.(latest)
--platform <platform>Run the group on another platform: ios, android, tvos or androidtv. The run installs the group’s app on that platform: the linked app of the same product, else the app with the same bundle id / package name — see Run on another platform(the group’s platform)
--app <app_id>Run the group against this app instead of its own, e.g. the other platform’s build when its bundle id differs(the group’s app)
--metadata <json>JSON object passed through unchanged to webhook payloads (e.g. PR context)(none)
--metadata-file <path>Read the metadata JSON object from a file — use for nested JSON in CI(none)
--retryRetry a failed test once, on the same device after a fresh start of the app; a pass on the retry marks the test flaky (see Retries). --no-retry is still accepted and changes nothing(no retry)

The command triggers the group run, then polls every 3 seconds until the run completes or the timeout is reached. The polled JSON result includes per-step/per-case error_kind (failure classification) and screenshot_signed_url (signed, no-auth image URL) for programmatic consumers — see Reading run results in the CI testing guide.

Example:

vibeview run-suite abc123 \
  --commit-sha $(git rev-parse HEAD) \
  --output junit \
  --timeout 1800

Pin a specific build:

vibeview run-suite abc123 --build 142

The id is the numeric build id in the ID column of vibeview list-builds <app_id>; upload-app prints it too. If the group has no app attached, --build returns an error.

run-suites

Run every group with a tag, one after another. Each matched group gets its own device session. Useful when CI doesn’t know specific group IDs but knows what areas changed in a PR.

vibeview run-suites --tag profiles --commit-sha $GIT_SHA
OptionDescriptionDefault
--tag <tag>Tag to match (repeatable)required
--match <mode>any matches if a group has any of the tags; all requires every tagany
--platform <platform>Restrict the tag-matched set to one platform: ios, android, tvos, androidtv, roku. Groups of other platforms are skipped (and reported).(no filter)
--output <format>Output format: human, json, junithuman
--verbosePrint per-group progress as they completefalse
--timeout <seconds>Per-group timeout. Total wait is capped at 2 hours regardless of N.1800
--build <id>AppBuild id, validated against every matched grouplatest
--commit-sha <sha>Git SHA propagated to every SuiteRun-
--model <model>AI modelinferred
--provider <provider>AI providerinferred
--mode <mode>AI use: hybrid (Smart), ai (AI only), replay (No AI)hybrid
--device-category <category>Device category to allocate: phone, tablet, tvtv for tvOS and Android TV, otherwise phone
--device <device_id>Pin run to a specific device (see list-devices). Conflicts with --device-category.(none)
--metadata <json>JSON object passed through unchanged to webhook payloads (e.g. PR context)(none)
--metadata-file <path>Read the metadata JSON object from a file — use for nested JSON in CI(none)
--retryRetry a failed test once, on the same device after a fresh start of the app; a pass on the retry marks the test flaky (see Retries). --no-retry is still accepted and changes nothing(no retry)
--token <token>API token (or set VIBEVIEW_API_TOKEN)-

Exit codes: 0 aggregate passed; 1 any group failed; 2 no matches / timeout / error / cancelled.

Example: run all groups tagged auth with the GitHub commit SHA propagated:

vibeview run-suites --tag auth --commit-sha $(git rev-parse HEAD) --output junit > results.xml

Scoping to one platform with --platform: A tag like prod may be shared by groups across iOS, Android, and TV. In a CI matrix where each job builds a single platform, add --platform so the job only runs the groups it actually built an app for — the rest are skipped and listed as excluded:

# A job that built the Android APK only runs the Android suites tagged prod
vibeview run-suites --tag prod --platform android

A group’s platform comes from the group itself, or its linked app. Groups with no resolvable platform are treated as a mismatch and excluded under a --platform filter. Omitting --platform keeps the previous behavior — every tag-matched group runs. --platform combines with --match: --match all --platform android runs groups that have all the given tags and target Android.

run-test

Run a single test case and wait for results.

vibeview run-test <test-id> [options]
OptionDescriptionDefault
--token <token>API token(from auth resolution)
--output <format>Output format: human, json, junithuman
--verboseShow step detailsfalse
--timeout <seconds>Max wait time in seconds, queue wait plus execution1800
--commit-sha <sha>Git commit SHA for GitHub status checks(none)
--model <model>AI model to use(server default)
--provider <provider>AI provider(server default)
--mode <mode>AI use: hybrid (Smart), ai (AI only), replay (No AI)(server default)
--device-category <category>Device category to allocate: phone, tablet, tvtv for tvOS and Android TV, otherwise phone
--device <device_id>Pin run to a specific device (see list-devices). Conflicts with --device-category.(none)
--build <id>Specific AppBuild id to run against (integer). Defaults to the latest build of the app the run installs when omitted.(latest)
--platform <platform>Run the test on another platform: ios, android, tvos or androidtv. The run installs your app on that platform with the same bundle id / package name as the test’s app — see Run on another platform(the test’s platform)
--app <app_id>Run the test against this app instead of its own, e.g. the other platform’s build when its bundle id differs(the test’s app)
--metadata <json>JSON object passed through unchanged to webhook payloads (e.g. PR context)(none)
--metadata-file <path>Read the metadata JSON object from a file — use for nested JSON in CI(none)
--retryRetry a failed test once, on the same device after a fresh start of the app; a pass on the retry marks the test flaky (see Retries). --no-retry is still accepted and changes nothing(no retry)

Example:

vibeview run-test test-789 --mode ai --output json

Over MCP. A coding agent can do the same through the MCP server with four tools: create_test (a test from plain-language steps), run_test, run_group and get_run (a run’s status, and which step failed and why). A run with no session_id starts its own new device and stops it when done, and never uses a session you hold with dev_start; pass session_id to run on the device you are driving. See AI agent control.

Pin a specific build:

vibeview run-test test-789 --build 142

The id is the numeric build id in the ID column of vibeview list-builds <app_id>; upload-app prints it too. If the test case has no app attached, --build returns an error.

Run on another platform

A test recorded on iPhone replays on Android (and Apple TV on Android TV): upload the other platform’s build as its own app, then pass --platform:

vibeview run-test test-789 --platform android
vibeview run-suite suite-123 --platform android

VibeView uses the Android app linked to the test’s app as the same product (see Same app on other platforms); an app with no link falls back to the Android app with the same bundle id / package name. It installs it and runs the test there; the CLI prints the app it picked (App: atlas-and (Android)), and the result is stored on the same test. If no app on that platform has that bundle id, or more than one does, the command stops before taking a device and lists your apps on that platform; pass the one you want with --app <app_id>. A phone test never runs on a TV app, or the reverse. --build <id> then names a build of that app. --platform and --app can’t be combined: --app already names the app, and with it the platform, so the command stops with an error when both are passed.

Builds that need a Metro dev server

A React Native debug build with no JavaScript bundle inside loads its code from a Metro dev server, and a test run has none, so the app stops at its error screen and the run fails. Without --build, a run skips these builds and uses the newest one that runs on its own. When the build a run installs is one of these anyway (you passed it with --build, or every build of the app needs Metro), run-test and run-suite print a warning right after starting, for example:

warning: Build 1.0.0 (1) of Kitlist is a debug build of a React Native app ...

The warning also appears as build_warning in --output json, as a vibeview.build_warning property in --output junit, and again under a failed result; run-suites prints one per matched group and lists them as build_warnings in its JSON. The run still starts: the check can be wrong about a build, and a --build you chose is respected. To run the test standalone, upload a release build of the app, or pass --build with the id of one (see vibeview list-builds). This matters most with --platform, where the other platform’s builds are easy to lose track of.

When the device pool is busy

If no matching device is available when you submit a run, VibeView queues you instead of failing. The CLI’s spinner shows your live position:

Queued — position 3 in line for iPhone 15 Pro (tap-pool-01)

When VibeView can tell why the run is waiting, the CLI also prints the reason on stderr, once, and again only if it changes. It shows up in a CI job log too, where there is no spinner:

⏳ Waiting for a device: All devices are in use right now.

The reasons are: every matching device is in use; the matching devices are being updated and will be back shortly; or your plan’s limit on sessions running at the same time has been reached. The run’s page in the dashboard shows the same line while the run waits.

Once a device frees up, the run automatically transitions through pending and into running without any intervention on your side.

--timeout covers the entire journey — queue wait plus execution — so set it generously when the pool is contended. On Ctrl+C or --timeout, the CLI cancels its queue entry server-side so the slot doesn’t sit unused.

If you target a specific device with --device <id>, the queue only counts users waiting for that device. Use vibeview list-devices to see what’s currently available, busy or updating.

upload-app

Upload an app build file. If an app with the same bundle/package ID and platform already exists, the build is added to the existing app instead of creating a new one.

vibeview upload-app <file> [options]

Supported file types: a compressed .app simulator bundle (.zip, .tar.gz, or .tgz archive) for iOS and tvOS, .apk for Android and Android TV, and a channel’s sideload .zip for Roku. .ipa files are rejected: iOS simulators require simulator .app builds, not device .ipa archives. See Preparing Your App Build for how to produce these files.

OptionDescriptionDefault
--token <token>API token(from auth resolution)
--output <format>Output format: human, jsonhuman
--name <name>App name (auto-detected from binary if not provided)(auto-detected)
--note <text>Note stored on the uploaded build, shown with the build in the dashboard(none)

Example:

vibeview upload-app ./build/MyApp.zip --note "nightly from main" --output json
{
  "app_id": "abc123",
  "name": "MyApp",
  "package_name": "com.example.myapp",
  "platform": "ios",
  "is_new": false,
  "build_id": 4812,
  "version": "1.4.0",
  "build_number": "57",
  "deduplicated": false,
  "note": "nightly from main",
  "url": "https://vibeview.io/apps/abc123",
  "warnings": []
}

The human output prints the same app ID and a Build 4812 v1.4.0 (57) line. Use the app ID with create-test --app and dev --app, and the build ID wherever a command takes --build. If the file is identical to a build you already uploaded, no new build is created: build_id is that existing build and deduplicated is true (a --note is then saved on that build).

create-test

Create an AI test case, optionally in one or more groups. The test name can be passed as a positional argument; steps can be provided inline, from a file, or piped via stdin.

vibeview create-test [name] [options]
OptionDescriptionDefault
[name]Test case name (positional argument)CLI Test
--token <token>API token(from auth resolution)
--output <format>Output format: human, jsonhuman
--app <id>App ID (public_id) or package name (requires --platform)(required)
--platform <platform>Platform (ios, android) when using package name for --app(optional)
--steps <json>Test steps as inline JSON array(one of steps/steps-file/stdin required)
--steps-file <path>Path to JSON file containing test steps(optional)
--context <text>Additional context for AI test execution(optional)
--test-name <name>Test case name (alternative to the positional argument; the positional wins if both are given)CLI Test
--group <id...>Group ID to add the test to; repeat the flag or list several IDs. Groups must belong to the same app as --app. The JSON output lists them under groups(none)
--if-exists <mode>When a test with this name already exists for the app (outside any group, or in one of the --group groups): fail refuses, update overwrites it, new creates another one beside it. Without it, an identical existing test is reused and a different one is refused (exit code 1)(reuse identical, refuse different)

Steps are a JSON array of plain-English instructions, each a string or an object with a text field. A test needs at least one step: an empty array is refused before anything is created.

Exit codes: 0 when the test was created, updated or reused. 1 when a test with that name is refused (see --if-exists), or for an unknown option. 2 for anything else that stops it: no steps or steps that can’t be read, an unknown app or group, a network or server error, or a missing or invalid token.

["Open the app", "Tap Login", {"text": "Verify the home screen shows"}]

Examples:

# Inline steps
vibeview create-test --app abc123 --steps '["Open the app", "Tap Login"]'

# From a file, with the name as a positional argument
vibeview create-test "Login Flow" --app abc123 --steps-file ./test-steps.json

# Piped from another tool
echo '["Verify the onboarding screen shows"]' | vibeview create-test --app abc123

# Add the test to two groups
vibeview create-test "Login Flow" --app abc123 --steps '["Tap Login"]' --group smoke nightly

import-maestro

Import Maestro flows as tests of one app. Pass a flow file (.yaml/.yml), a .zip of your flows folder, or the folder itself (its .yaml/.yml files are uploaded, subfolders included). Requires the developer role or above. See Import Maestro flows for how commands become steps.

vibeview import-maestro <path> --app <id> [options]
OptionDescriptionDefault
<path>Flow file, .zip, or flows folder(required)
--app <id>App ID (public_id) or package name (requires --platform)(required)
--platform <platform>Platform (ios, android) when using a package name for --app(optional)
--group <group>Put the imported tests in this group (name or ID)(none)
--dry-runList what would be imported and every flagged command; save nothingfalse
--token <token>API token(from auth resolution)
--output <format>Output format: human, jsonhuman

A folder upload skips node_modules, .git, symbolic links and files that aren’t YAML. Values of secret test data are never printed.

Exit codes: 0 on success (a dry run included). 1 when the command or the upload is refused: a missing --app, a path that doesn’t exist, a file that is not YAML or .zip, an upload over the size limits, or a 4xx answer from the server (a flow that can’t be read, an unknown app or group, nothing to import). 2 for a network error, a server error (5xx) or a missing or invalid token.

Examples:

# Preview your .maestro folder
vibeview import-maestro .maestro --app abc123 --dry-run

# Import one flow into the "Smoke" group
vibeview import-maestro .maestro/login.yaml --app abc123 --group Smoke

list-apps

List all apps in your organization.

vibeview list-apps [options]
OptionDescriptionDefault
--token <token>API token(from auth resolution)
--output <format>Output format: human, jsonhuman

Example output (human):

  3 app(s)

  abc123  MyApp [ios]  com.example.myapp
    https://vibeview.io/apps/abc123
  def456  MyApp [android]  com.example.myapp
    https://vibeview.io/apps/def456
  ghi789  TestApp [ios]  com.test.app
    https://vibeview.io/apps/ghi789

Each app is followed by its dashboard link.

list-builds

List an app’s uploaded builds, newest first — the build ids dev --build, run-test --build and run-suite --build take.

vibeview list-builds <app_id> [options]
OptionDescriptionDefault
--jsonPrint the builds as a JSON array (id, version, build_number, platform, kind, is_debug, can_use_metro, uploaded_at, note)(human output)
--token <token>API token(from auth resolution)

KIND is debug for a React Native debug build, which loads its JavaScript from Metro (the kind vibeview dev runs), release for a release build or any build of an app that isn’t React Native, and unknown when it couldn’t be detected. A debug build that also embeds its JS bundle is debug too. In --json, can_use_metro is the same answer as KIND, and is_debug says whether the build needs Metro (it has no embedded bundle, so it can’t run without one).

Sample output:

ID    VERSION     PLATFORM  KIND     UPLOADED                 NOTE
4812  1.4.0 (57)  ios       debug    2026-09-24 10:12:03 UTC  login fix
4790  1.4.0 (56)  ios       release  2026-09-22 16:40:11 UTC

Upload times are shown in UTC. --json prints the full ISO 8601 timestamp in UTC, ending in Z (2026-09-24T10:12:03.518204Z).

list-devices

List devices registered with the platform — useful to find a device_id to pass to --device. With --models, list the device models you can start instead — the names vibeview dev --model takes.

vibeview list-devices [options]
OptionDescriptionDefault
--type <type>Filter by device type: ios, android, roku(all)
--category <category>Filter by category: phone, tablet, tv(all)
--status <status>Filter by status: available, busy, or updating (free, but being updated or restarted, so it takes no new session until that ends). The list only includes devices that are online(all)
--modelsList startable device models (model, OS, type, category, and foldable for a device that folds) instead of individual devices. --type and --category still filter; --status does not apply(off)
--output <format>human or jsonhuman
--token <token>API token(from auth resolution)

An unknown --type, --category or --status value is refused with the list of allowed values and exit status 1. Apple TV is --type ios --category tv; Android TV is --type android --category tv.

Example: find an available Android TV:

vibeview list-devices --type android --category tv

Sample output:

DEVICE_ID         NAME              TYPE     CATEGORY  OS    STATUS
atv-01            Apple TV 4K       ios      tv        17.5  available
emu-tv-01         Android TV        android  tv        34    busy

stop

Stop any session by its ID — for example one started through the REST API, from the Devices page, or through a collaboration link. To end the session a vibeview dev --detach in the current project started, vibeview dev-stop does the same without an ID.

vibeview stop <session_id> [options]
OptionDescriptionDefault
--jsonPrint the result as one JSON line: {"stopped": true, "session_id": "..."}(human output)
--token <token>API token(from auth resolution)

Stopping a session that has already ended succeeds. An unknown session ID, or one outside your organization, exits with status 1. So does a teammate’s session: only its owner, or an organization admin, can end it.

Output Formats

human

Readable terminal output with colors and a spinner during execution. Best for local development.

json

Structured JSON for programmatic parsing. Includes full run result with per-case status, duration, and error messages.

junit

JUnit XML format for CI/CD integration. Compatible with GitHub Actions, Jenkins, GitLab CI, and other CI systems that consume JUnit reports.

Each test case is one <testcase> with its duration: a failed case carries a <failure> with the reason, an errored one an <error>, and a cancelled one is <skipped>. run-suites writes one <testsuite> per group with a <testcase> per case; a group that errored before any case ran (for example, the app failed to install) is reported as one errored <testcase> named after the group.

Example: upload JUnit results as a GitHub Actions artifact

- name: Run VibeView suites
  env:
    VIBEVIEW_API_TOKEN: ${{ secrets.VIBEVIEW_API_TOKEN }}
  run: vibeview run-suites --tag smoke --platform android --output junit > results.xml

- name: Upload test results
  if: ${{ !cancelled() }}
  uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
  with:
    name: vibeview-results
    path: results.xml

Exit Codes

CodeMeaning
0All tests passed. A test in which no step was checked (Not checked) never fails the build either: the CLI prints it as NOT CHECKED, never as passed, and JUnit output reports it as skipped
1One or more tests failed (a quarantined test’s failure doesn’t count, in run-test as in group runs)
2Execution error (network failure, authentication error, timeout, or a cancelled run)

run-test, run-suite and run-suites also exit 2 when they refuse their arguments (an unknown option, a missing test or group ID, flags that can’t be combined, or a value they don’t accept), so a typo in a CI job never reads as a failed test: from these commands, 1 always means a test failed (earlier CLI versions exit 1 for an unknown option or a missing ID). Other commands exit 1 for an unknown option or a missing argument; where a command’s exit codes differ from this table, its section says so.

A missing or invalid API token exits with 2. So does an expired token, whose error says when it expired, and a CI-only token used for a command outside what CI needs, whose error names the ci scope. The one exception is vibeview dev --json, which reports every refusal, these included, as a JSON error event and exits 1. With JSON output (--json or --output json), a failed command also prints one {"event": "error", "message": "..."} line on stdout, including a run-test or run-suite that is refused before it starts. Timestamps in JSON output are ISO 8601 in UTC, ending in Z.

Use exit codes in CI scripts to gate deployments:

vibeview run-suite $SUITE_ID --commit-sha $COMMIT_SHA || exit 1

CI/CD Integration

For GitHub Actions, start from the VibeView workflow template: it builds your app, uploads it, runs your tagged groups against that build, and comments the results on the pull request.

test-spec.json Convention

AI coding agents and automation tools can describe a test in plain English in .vibeview/test-spec.json, for the AI agent (beta) to run:

{
  "version": 1,
  "steps": ["Launch the app", "Verify the home screen loads"],
  "context": "Optional context for the AI agent",
  "test_name": "My Test"
}

create-test --steps-file and run-test run it. For the schema and a GitHub Actions step that runs it, see AI steps from test-spec.json.

GitHub Actions

The workflow template is the complete setup. Its core is two commands: upload the build the job made, then run the tagged groups against it with the pull request’s head commit:

- name: Upload the build
  id: build
  env:
    VIBEVIEW_API_TOKEN: ${{ secrets.VIBEVIEW_API_TOKEN }}
  run: |
    build_id=$(vibeview upload-app app-release.apk --output json | jq -er '.build_id')
    echo "build_id=$build_id" >> "$GITHUB_OUTPUT"

- name: Run the tagged suites
  env:
    VIBEVIEW_API_TOKEN: ${{ secrets.VIBEVIEW_API_TOKEN }}
    BUILD_ID: ${{ steps.build.outputs.build_id }}
    HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
  run: |
    vibeview run-suites --tag smoke --platform android --build "$BUILD_ID" \
      --commit-sha "$HEAD_SHA" --output junit > results.xml

Pass values to scripts through env: as above, never as ${{ }} inside run:. On a pull_request event, github.sha is a merge commit that isn’t part of the pull request, so --commit-sha takes the head commit.

Generic CI

For any CI system, set the VIBEVIEW_API_TOKEN environment variable (and VIBEVIEW_BASE_URL for a non-default instance), then call the CLI:

BUILD_ID=$(vibeview upload-app ./app-release.apk --output json | jq -er '.build_id')
vibeview run-suites --tag smoke --platform android --build "$BUILD_ID" \
  --commit-sha "$(git rev-parse HEAD)" --output junit > results.xml

The CLI exits with 0 when the tests passed, 1 when a test failed, and 2 when the run could not complete (see Exit Codes), so your pipeline can pass or fail on the exit code.

Last updated 16 Oct 2026 Something wrong on this page? Tell us

Search the docs