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:
--tokenflag — pass a token directly on each command.VIBEVIEW_API_TOKENenvironment variable — best for CI environments.- Config file — saved automatically by
vibeview loginat~/.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
| Option | Description |
|---|---|
--base-url <url> | VibeView server URL (defaults to https://vibeview.io) |
The command:
- Opens your browser to the VibeView login page (you are taken straight to the authorization screen if you’re already signed in).
- Asks you to confirm with an Authorize button — only approve requests you started from your own terminal.
- Waits up to 2 minutes for you to complete the authorization.
- Saves the token to
~/.vibeview/config.json(file permissions0600).
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]
| Option | Description | Default |
|---|---|---|
--json | Print 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
| Option | Description |
|---|---|
--project | Install the skill into this project’s .claude/skills/ instead of ~/.claude/skills/ |
--mcp | Register the MCP server with Claude Code without asking |
--no-mcp | Skip 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
| Option | Description | Default |
|---|---|---|
--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) |
--build | Run 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) |
--standalone | Run 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.json | vibeview.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-open | Don’t open the browser automatically | (opens by default) |
--detach | Run 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) |
--json | Emit 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) |
--debug | Log 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 devuses it as-is — it never touches a Metro it didn’t start. - If nothing is running on the port and
metroCommandis configured (top-level or per-platform),vibeview devstarts it for you, prefixing its output with[metro], and stops it automatically when the session ends. - If
vibeview devstarted 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
metroCommandis configured,vibeview devstops with a clear error telling you to start Metro yourself or addmetroCommandtovibeview.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:
| Key | Action |
|---|---|
r | Re-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.
| Option | Description | Default |
|---|---|---|
--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 block | vibeview.json’s defaultPlatform |
--json | Print the result as one JSON line (app_id, name, platform) instead of human-readable output | (human output) |
--cloud | Build 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
| Option | Description | Default |
|---|---|---|
--token <token> | API token | (from auth resolution) |
--output <format> | Output format: human, json | human |
--tag <tag> | Filter by tag (repeatable) | (all groups) |
--match <mode> | any or all — how multiple --tag values are combined | any |
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]
| Option | Description | Default |
|---|---|---|
--token <token> | API token | (from auth resolution) |
--output <format> | Output format: human, json, junit | human |
--verbose | Show per-case breakdown | false |
--timeout <seconds> | Max wait time in seconds, queue wait plus execution | 1800 |
--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, tv | tv 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) |
--retry | Retry 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
| Option | Description | Default |
|---|---|---|
--tag <tag> | Tag to match (repeatable) | required |
--match <mode> | any matches if a group has any of the tags; all requires every tag | any |
--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, junit | human |
--verbose | Print per-group progress as they complete | false |
--timeout <seconds> | Per-group timeout. Total wait is capped at 2 hours regardless of N. | 1800 |
--build <id> | AppBuild id, validated against every matched group | latest |
--commit-sha <sha> | Git SHA propagated to every SuiteRun | - |
--model <model> | AI model | inferred |
--provider <provider> | AI provider | inferred |
--mode <mode> | AI use: hybrid (Smart), ai (AI only), replay (No AI) | hybrid |
--device-category <category> | Device category to allocate: phone, tablet, tv | tv 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) |
--retry | Retry 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]
| Option | Description | Default |
|---|---|---|
--token <token> | API token | (from auth resolution) |
--output <format> | Output format: human, json, junit | human |
--verbose | Show step details | false |
--timeout <seconds> | Max wait time in seconds, queue wait plus execution | 1800 |
--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, tv | tv 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) |
--retry | Retry 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.
| Option | Description | Default |
|---|---|---|
--token <token> | API token | (from auth resolution) |
--output <format> | Output format: human, json | human |
--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]
| Option | Description | Default |
|---|---|---|
[name] | Test case name (positional argument) | CLI Test |
--token <token> | API token | (from auth resolution) |
--output <format> | Output format: human, json | human |
--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]
| Option | Description | Default |
|---|---|---|
<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-run | List what would be imported and every flagged command; save nothing | false |
--token <token> | API token | (from auth resolution) |
--output <format> | Output format: human, json | human |
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]
| Option | Description | Default |
|---|---|---|
--token <token> | API token | (from auth resolution) |
--output <format> | Output format: human, json | human |
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]
| Option | Description | Default |
|---|---|---|
--json | Print 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]
| Option | Description | Default |
|---|---|---|
--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) |
--models | List 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 json | human |
--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]
| Option | Description | Default |
|---|---|---|
--json | Print 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
| Code | Meaning |
|---|---|
0 | All 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 |
1 | One or more tests failed (a quarantined test’s failure doesn’t count, in run-test as in group runs) |
2 | Execution 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.