The unotest-web CLI. Run any command with npx @unotest/web <command> — works with npm, pnpm, yarn or bun.
init
Bootstrap the unotest/ layout, config, and MCP wiring for your editor. Re-run anytime — it never overwrites your edits.
npx @unotest/web init [target] [--browser system|bundled|none]--browser— Skip the interactive browser prompt: use system Chrome/Edge, bundled Chromium, or none.
Example
npx @unotest/web init --browser systeme2e
Run a single scenario. name resolves to unotest/e2e/<name>.js. Unknown names suggest close matches from unotest/e2e/**.
npx @unotest/web e2e <name> [--env <name>] [--debug] [--break line:col]--env <name>— Named environment: layersunotest/.env.<name>/.secrets.<name>over the base files (same asUNOTEST_ENV=<name>).--debug— Pause on breakpoints (.debugger.json) and on failure.--break line:col[,line:col]— Override breakpoints (comma-separated, no spaces).--scheduled— Run the test the way itsschedulesentry does: in the entry’senv(unless you set one) and with itspreparecommand first, inside the same run slot — the per-test mirror ofcollection --scheduled. A failed prepare exits 94.--scheduled=<index>pins the entry (zero-based position innpx @unotest/web schedules).UNOTEST_STEP_SCREENSHOTS=1— Env: capture a PNG after every step into the run’sscreenshot/directory; the viewer shows each one inline when you click its step row.UNOTEST_BROWSER=<engine>— Env: run onchromium/firefox/webkit, overriding the first entry ofbrowsersin the config — the knob a CI browser matrix sets. Any other value is an error.UNOTEST_BROWSER_CHANNEL=<channel>— Env:chrome/msedge/chrome-beta/bundled, overridingchannelin the config. The config travels with the suite; this stays on the machine — a box setsbundledfor every run.
Example
npx @unotest/web e2e auth/login --env stagingcollection
Run a collection (a named set of scenarios), e.g. smoke or regress.
npx @unotest/web collection <name> [--env <name>] [--workers=N] [--bail] [--headed]--env <name>— Named environment: layersunotest/.env.<name>/.secrets.<name>over the base files (same asUNOTEST_ENV=<name>).--workers=N— Run N scenarios in parallel. Overrides the manifest’sworkers:for this run; without either, runs are serial.--bail— Stop after the first failure.--headed— Run with a visible browser.--scheduled— Run it the way itsschedulesentry does: in the entry’senv(unless you set one) and with itspreparecommand first, inside the same run slot. A failed prepare exits 94 — the environment was not prepared and nothing was tested, which is a different alert from a red suite (exit 1). A collection listed inschedulesmore than once needs--scheduled=<index>(zero-based).--no-prepare— Skip the collection’sprepare:for this run. For a run against an environment somebody else already prepared — a CI check of a deployment, where reseeding would test the seed rather than the deployment.UNOTEST_RUNS_RETENTION_DAYS=N— Env: how long finished runs are kept, in whole days (default 180). Older runs are swept at the end of a collection run; a failed sweep never changes the exit code.
Example
npx @unotest/web collection smoke --workers=4author
Open a live authoring session: a headed browser stays open with no test running, and the agent attaches to it over MCP. You and the agent build the draft scenario together on the same page. Stop with abort or Ctrl-C.
npx @unotest/web author <feature>/<name>Example
npx @unotest/web author checkout/apply-couponviewer
Open the local IDE-style viewer (HTTP + WebSocket). No cloud, no account.
npx @unotest/web viewerUNOTEST_VIEWER_NO_OPEN=1— Env: start the server without auto-opening a browser.UNOTEST_VIEWER_HOST=…— Env: bind host (defaultlocalhost).UNOTEST_VIEWER_PORT=…— Env: bind port (default: the OS picks a free one).
Example
npx @unotest/web viewerapp-server
Start the app under test — the webServer block of your config — and hold it until Ctrl-C. Runs then reuse it instead of each spawning their own copy, which is what you want while iterating locally.
npx @unotest/web app-serverExample
npx @unotest/web app-serverruns migrate
Retire history left in the old flat unotest/.runs/<runId>/ layout. It is deleted by default — one layout, one frame format, no second resolver kept forever. This runs automatically on the first run after upgrading, so the command exists to let you look (and choose) BEFORE that happens. Set UNOTEST_RUNS_LEGACY=keep to re-file the old runs into date shards instead; either way the amount is printed before anything is touched.
npx @unotest/web runs migrate [--dry-run]--dry-run— Report how many runs and how many GB are affected, change nothing.UNOTEST_RUNS_LEGACY=keep— Env: re-file old runs into date shards instead of deleting them. The price is a mixed frame format on that machine.
Example
npx @unotest/web runs migrate --dry-runbundle push
Pack this project’s suite — the unotest/ package, with its config, its package.json and its lockfile — into one bundle and upload it to a box. Your application’s own manifest and dependencies stay behind: a box installs the suite alone, which is why it can run tests for a project whose dependency graph it could not resolve in the first place. This is how tests reach a box: a box holds no credential to your repository and never pulls anything, so nothing is on it that you did not push. Uncommitted work is included and recorded as dirty, which the box’s viewer labels wip — pushing a fix you have not committed yet is the fast loop this exists for. What would fail on the box is refused here instead: a scenario that reads a file outside unotest/ (MODULE_NOT_FOUND at 03:00 otherwise), a missing unotest/package-lock.json (npm ci refuses to run without one), a missing @unotest/web pin, a symlink. Secrets never travel — unotest/.env* and .secrets* are excluded, because environment values belong to the environment and are injected over the bundle when it runs. The bundle’s id is a hash of its content, so re-pushing an unchanged suite is a no-op the box answers instantly.
npx @unotest/web bundle push [--box <url>] [--token <token>] [--out <file>] [--dry-run] [--json] [--run --env <name> [--collection <name>] [--pr <number>]]--box <url>— Box address, e.g.https://tests.example.com. Defaults to$UNOTEST_BOX_URL.--token <token>— Upload token, issued per project on the box. Defaults to$UNOTEST_BOX_TOKEN.--out <file>— Also write the packed bundle to a file — a CI artifact, or something to look inside withtar -tzf.--dry-run— Pack and check, upload nothing. Exits 2 if anything would be refused.--json— Print{ bundleId, status, runs? }instead of prose, for a CI job that wants the run ids.--run— Ask the box to run this bundle once it is stored — what a CI job does after a deploy. The box answers as soon as the runs are queued (a suite takes minutes; an HTTP request held open that long is a timeout, not a result), so the command returns without waiting for a verdict. Watch them in the viewer, or let the box report checks back to GitHub.--env <name>— Which environment on the box to run in. Required with--run: which environments exist is the box operator’s decision, not something the suite can know.--collection <name>— Collection to run; repeat the flag for several. Without it the box runs whatever that environment’sschedulesname — “run what this environment normally runs”.--pr <number>— Pull request this push belongs to. The box shows it in the queue, and a newer push of the same pull request withdraws the older runs that are still waiting — three pushes in five minutes cost one suite, not three.
Example
npx @unotest/web bundle push --run --env test --collection smokeenv push
Send an environment’s values to a box, from the same files a local run of it reads: unotest/.env, .env.<env>, .secrets, .secrets.<env>, later files winning. .env* become the box environment’s variables, .secrets* its secrets, APP_BASE_URL its target (the URL every run’s baseUrl auto-wires from). UNOTEST_* and empty values are not sent and listed as skipped — they configure the runner on your machine. The push REPLACES the box’s layers: a name your files no longer carry is removed there, and the command says which. Values never print, names do. The token is the project token bundle push uses, minted on the box with --values; a token without that scope is refused. Admins see and change the same values on the box’s admin page.
npx @unotest/web env push <env> [--box <url>] [--token <token>] [--box-env <name>] [--env-file <file>]... [--secrets-file <file>]... [--dry-run] [--json]--box <url>— Box address. Defaults to$UNOTEST_BOX_URL.--token <token>— Project token minted with--valueson the box. Defaults to$UNOTEST_BOX_TOKEN.--box-env <name>— The environment’s name on the box, when it differs from the local one.--env-file <file>— Read variables from this file instead ofunotest/.env+.env.<env>. Repeat for layers; later files win.--secrets-file <file>— Same for secrets, instead of.secrets+.secrets.<env>.--dry-run— Read and list what would be sent (names only), send nothing.--json— Print the box’s answer — the names it now holds and the names it removed — as JSON.
Example
npx @unotest/web env push devenv set
Set one value of a box environment. The value is read from stdin, never from the command line — an argument ends up in the shell history and in ps. --secret makes it a secret: injected into runs, never printed by the CLI — on the box an admin can reveal or copy it on the environment’s values page, and every look is audited; the name APP_BASE_URL is always the environment’s target. UNOTEST_* names are allowed here, unlike in env push — this is how an operator overrides UNOTEST_BROWSER_CHANNEL on purpose.
printf '%s' "$VALUE" | npx @unotest/web env set <env> <NAME> [--secret] [--box <url>] [--token <token>] [--box-env <name>] [--json]--secret— Store it as a secret: injected into runs, never printed by the CLI; an admin can reveal it on the box’s values page, every look audited.--box-env <name>— The environment’s name on the box, when it differs from the local one.
Example
printf '%s' "$GEMINI_API_KEY" | npx @unotest/web env set dev GEMINI_API_KEY --secretenv rm
Remove one value from a box environment. --secret names a secret; APP_BASE_URL puts the environment back on the target its box configuration names.
npx @unotest/web env rm <env> <NAME> [--secret] [--box <url>] [--token <token>] [--box-env <name>] [--json]Example
npx @unotest/web env rm dev STALE_FLAGbox
Read a box’s results back — what ran, what failed, and the artifacts to look at — without a browser and without ssh. Reading uses a PERSONAL read token (UNOTEST_BOX_READ_TOKEN) that you mint for yourself on the box’s guard: it is read-only whatever your role there, and it is not the project token that pushes bundles. The address comes from UNOTEST_BOX_URL, and both are read from unotest/.env and unotest/.secrets as well as the shell, so an agent whose MCP server was started by an editor is configured by the project’s own files.
box envs lists the environments the token may look at, as <project>/<environment> — the name every other subcommand wants in --env. box runs lists recent runs, newest first. box run <id> explains one run: its status, the failure that ended it, the results of soft steps with their tags, and the judge’s verdicts; it exits 1 when the run did not pass. box queue shows who is running and who is waiting. box screenshot <id> saves one frame the run captured.
box run --download also fetches the run’s *.unotest.zip into .unotest/box/ and unpacks its failure bundle into .unotest/failures/, which is where list_failures, get_failure_* and agent_fix already look — so a run that failed on a box is debugged with the same commands as one that failed locally.
npx @unotest/web box envsnpx @unotest/web box runs --env <project>/<environment> [--latest] [--running] [--scenario <ref>] [--limit <n>]npx @unotest/web box run <runId> --env <project>/<environment> [--download] [--dir <path>] [--no-screenshots]npx @unotest/web box queue --env <project>/<environment>npx @unotest/web box screenshot <runId> --env <project>/<environment> [--step <n>] [--out <file>]--env <project>/<environment>— Which environment on the box. A read token says who you are, not where to look, so every subcommand butbox envsneeds it.--latest— One line per scenario: its last run, and the length of its current failing streak — the answer a single status cannot give.--running— Only runs in flight right now.--scenario <ref>— Only this scenario’s runs, served from its own index.--limit <n>— How many runs to list (default 20).--download— Fetch the run as a zip (into.unotest/box/) and unpack its failure bundle where the failure tools read it.--dir <path>— Where the zip goes. Default.unotest/box.--no-screenshots— Ask the BOX to leave the run’s step frames out of the zip. They are usually most of the bytes, and this is what makes the download smaller — not just the disk.--step <n>— Which frame to save, 1-based, in capture order. Default: the last one.--out <file>— Where to write the PNG.--box <url>— Box address. Defaults to$UNOTEST_BOX_URL, thenunotest/.env.--token <token>— Read token. Defaults to$UNOTEST_BOX_READ_TOKEN, thenunotest/.secrets.--json— Print the box’s answer as JSON instead of a table.
Example
npx @unotest/web box run checkout-mqf3pwr1 --env acme/test --downloadschedules
Print what this project runs on its own: the schedules of unotest.config merged with unotest/schedules.yaml — the registry the viewer writes when you schedule a test or a collection from the tree (the file wins on a duplicate target). An entry names a collection or a single test, and travels with the tests it runs. Nothing in the CLI executes them: cron is run by the box or the CI that hosts your runs, and locally these are declarations. --json prints the machine shape a scheduler reads back.
npx @unotest/web schedules [--json]--json— Print the entries as JSON instead of a table.--check— Exit non-zero when any entry uses a cron expression outside the previewable subset (such entries are markednot previewable — an executor may drop itin the table). For a pre-push hook or a CI gate: a hand-written expression the executor’s parser refuses would otherwise be dropped silently on the box.
Example
npx @unotest/web scheduleslint
Static-check scenarios and helpers. Exits non-zero on errors.
npx @unotest/web lint [paths...]Example
npx @unotest/web linttypes
Refresh editor typings: create unotest/jsconfig.json if missing and regenerate .unotest/types/env.d.ts from your unotest/.env / .secrets keys (names only, never values). Also runs automatically on lint and e2e.
npx @unotest/web typesinstall-chromium
Download Playwright’s bundled Chromium (~150 MB). Not needed if you chose system Chrome/Edge.
npx @unotest/web install-chromiummcp
Run as an MCP stdio server. This is what your editor launches automatically; you rarely run it by hand.
npx @unotest/web mcp