This documentation is available as Markdown. For the complete index, see llms.txt. Skip to content

CLI — web

For the complete documentation index, see llms.txt

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.

Terminal window
npx @unotest/web init [target] [--browser system|bundled|none] [--ci github]
  • --browser — Skip the interactive browser prompt: use system Chrome/Edge, bundled Chromium, or none.
  • --ci github — Also write .github/workflows/unotest.yml at the repository root: on every push it pushes the suite to your box and runs it. See Run on every push.
  • --force, -f — Take the current templates for files that already exist. What each one replaces is kept next to it as <file>.bak.
  • --help, -h — Print the usage and do nothing else — no files are created or changed. Every command takes it; an option a command does not know is refused with the usage and exit 2.

Example

Terminal window
npx @unotest/web init --browser system

e2e

Run a single scenario. <feature>/<name> resolves to unotest/e2e/<feature>/<name>.js. Unknown names suggest close matches from unotest/e2e/**.

Terminal window
npx @unotest/web e2e <feature>/<name> [--env <name>] [--debug] [--break line:col]
  • --env <name> — Named environment: layers unotest/.env.<name> / .secrets.<name> over the base files (same as UNOTEST_ENV=<name>).
  • --debug — Pause on breakpoints set in the viewer and on failure.
  • --break line:col[,line:col] — Override breakpoints (comma-separated, no spaces).
  • --test <name> — Run one test_* of a file that holds several, by function name (the test_ prefix is optional). Without it every test of the file runs, each as its own run. A name the file does not have exits 2 and lists the tests it does.
  • --update-screenshots — Write every assertScreenshot snapshot of the run as its baseline (unotest/e2e/<scenario>.screenshots/<name>.png) instead of comparing; the asserts pass and are marked updated in the journal. Refused on a box — baselines change in the repo.
  • --scheduled — Run the test the way its schedules entry does: in the entry’s env (unless you set one) and with its prepare command first, inside the same run slot — the per-test mirror of collection --scheduled. A failed prepare exits 94. --scheduled=<index> pins the entry (zero-based position in npx @unotest/web schedules).
  • --no-generate — Skip the config’s generate command for this run and read unotest/generated/ as it is. Otherwise it runs first, from the project root; when it fails the run is refused with exit 95 before anything starts.
  • --notes — Print every note() of every test under its line — a passing test’s too — each as note <label>: <value> — step "<step>". A failed test lists them in place of the failing step’s own notes, so none prints twice. Without it the output is unchanged.
  • UNOTEST_STEP_SCREENSHOTS=1 — Env: capture a PNG after every step into the run’s screenshot/ directory; the viewer shows each one inline when you click its step row.
  • UNOTEST_BROWSER=<engine> — Env: run on chromium / firefox / webkit, overriding the first entry of browsers in the config — the knob a CI browser matrix sets. Any other value is an error.
  • UNOTEST_BROWSER_CHANNEL=<channel> — Env: chrome / msedge / chrome-beta / chromium / bundled, overriding channel in the config. The config travels with the suite; this stays on the machine — a box sets bundled for every run.
  • UNOTEST_BROWSER_CHANNEL=chromium — Env: Chromium in its new headless mode — the one that loads extensions (launch.args with --load-extension) headless, for CI. Same as channel: "chromium" in the config.
  • exit 78 — The scenario calls assertJudge (itself or through a helper) and the judge cannot answer — UNOTEST_JUDGE_MODE unset, a provider variable missing, the service not running. Checked before the browser starts; the message names what to fix. A scenario that does not judge is never held up by it. MCP run_test answers this one as status: "config_error".
  • exit 1, run stopped — The pre-run lint found an error the run would refuse anyway — an unknown function, JavaScript outside the scenario language, an option the call does not read, a ${…} in evaluate code — or a rule your linter.rules raises to error. Every diagnostic is printed, then run stopped: N error(s); no browser starts and nothing is written under unotest/.runs/. Errors of shape (validator:arity, a call outside step(...)) and lint:* errors at their default severity print and the run goes on.

Example

Terminal window
npx @unotest/web e2e auth/login --env staging

collection

Run a collection (a named set of scenarios), e.g. smoke or regress.

Terminal window
npx @unotest/web collection <name> [--env <name>] [--workers=N] [--bail] [--headed]
  • --env <name> — Named environment: layers unotest/.env.<name> / .secrets.<name> over the base files (same as UNOTEST_ENV=<name>).
  • --workers=N — Run N scenarios in parallel. Overrides the manifest’s workers: 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 its schedules entry does: in the entry’s env (unless you set one) and with its prepare command 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 in schedules more than once needs --scheduled=<index> (zero-based).
  • --no-prepare — Skip the collection’s prepare: 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.
  • --no-generate — Skip the config’s generate command. Otherwise it runs once before the suite, from the project root, and each scenario reads what it wrote; when it fails the run is refused with exit 95 before any scenario starts.
  • --notes — Print every note() of every test under its line, as for e2e --notes: under a passing scenario’s line, and in a failing one’s output.
  • 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

Terminal window
npx @unotest/web collection smoke --workers=4

author

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.

Terminal window
npx @unotest/web author <feature>/<name>

Example

Terminal window
npx @unotest/web author checkout/apply-coupon

viewer

Open the local IDE-style viewer (HTTP + WebSocket). No cloud, no account.

Terminal window
npx @unotest/web viewer
  • --open — Open the viewer in the default browser even when no person is at the terminal. Without it the tab opens only from an interactive terminal (stdin and stdout are TTYs, CI unset) — not from an agent’s shell, a script or CI; the URL is printed either way.
  • UNOTEST_VIEWER_NO_OPEN=1 — Env: start the server without opening a browser.
  • UNOTEST_VIEWER_HOST=… — Env: bind host (default localhost).
  • UNOTEST_VIEWER_PORT=… — Env: bind port (default: the OS picks a free one).
  • UNOTEST_VIEWER_TERMINAL=off — Env: no docked terminal at all — for a host without the PTY addon. A box sets it for its viewers.

Example

Terminal window
npx @unotest/web viewer

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

Terminal window
npx @unotest/web app-server

Example

Terminal window
npx @unotest/web app-server

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

Terminal window
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

Terminal window
npx @unotest/web runs migrate --dry-run

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

Terminal window
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 with tar -tzf.
  • --dry-run — Pack and check, upload nothing. Exits 2 if anything would be refused.
  • --no-generate — Skip the config’s generate command and pack unotest/generated/ as it is. Otherwise it runs first, from the project root, so the bundle carries a snapshot of the source being pushed; when it fails nothing is packed (exit 95).
  • --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; box envs lists the ones it serves. 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’s schedules name — “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.
  • --follow — Wait for the runs --run ordered, with the same token, and exit with their verdict: 0 all passed, 1 a suite failed, 2 no verdict (a box too old to follow, a box that forgot the run, or an hour without an ending). From 0.48.0 it also prints what each run printed as it goes — every scenario’s start and end, failed steps with their error, log and note — with the environment’s secrets masked by the box; with --json those lines go to stderr. A box too old to give them says so once, and the verdict follows as before.

Example

Terminal window
npx @unotest/web bundle push --run --env test --collection smoke

bundle rm

Remove a bundle from the token’s project — one pushed with the wrong project’s token, or one nobody wants. The id is the one bundle push printed. The box looks in the token’s project only and answers not found in project “…” whether another project holds the bundle or none does. A bundle an environment runs, or a waiting or running ticket needs, is refused with the environments named. A box older than bundle removal refuses and removes nothing.

Terminal window
npx @unotest/web bundle rm <bundle-id> [--switch-to <bundle-id>] [--box <url>] [--token <token>] [--json]
  • --switch-to <bundle-id> — Switch the environments that run it to this bundle of the same project first, as the viewer does. The box answers as the switches start and the bundle stays; run the same bundle rm again once they have landed. Refused where an environment follows its branch (allowSwitch off).
  • --box <url> — Box address. Defaults to $UNOTEST_BOX_URL.
  • --token <token> — The project token bundle push uses. Defaults to $UNOTEST_BOX_TOKEN.
  • --json — Print the box’s answer as JSON.

Example

Terminal window
npx @unotest/web bundle rm 1a2b3c4d5e6f7a8b --switch-to 9f8e7d6c5b4a3210

env 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. From 0.48.0 the push merges into the box’s layers: a name your files do not carry stays there and is listed as kept, and --prune removes such names instead; an earlier CLI replaces the layers, removing every name your files no longer carry. Either way the command says which names went. 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.

Terminal window
npx @unotest/web env push <env> [--box <url>] [--token <token>] [--box-env <name>] [--env-file <file>]... [--secrets-file <file>]... [--prune] [--dry-run] [--json]
  • --box <url> — Box address. Defaults to $UNOTEST_BOX_URL.
  • --token <token> — Project token minted with --values on 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 of unotest/.env + .env.<env>. Repeat for layers; later files win.
  • --secrets-file <file> — Same for secrets, instead of .secrets + .secrets.<env>.
  • --prune — Also remove what the box holds and your files do not, the target override included, and list it. Without it a push merges: such names stay and are listed as kept. UNOTEST_* names on the box are never pruned. A box too old to merge refuses the push and changes nothing — update the box, then retry.
  • --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 what the push changed — as JSON.

Example

Terminal window
npx @unotest/web env push dev

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

Terminal window
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

Terminal window
printf '%s' "$GEMINI_API_KEY" | npx @unotest/web env set dev GEMINI_API_KEY --secret

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

Terminal window
npx @unotest/web env rm <env> <NAME> [--secret] [--box <url>] [--token <token>] [--box-env <name>] [--json]

Example

Terminal window
npx @unotest/web env rm dev STALE_FLAG

box

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 box serves, as <project>/<environment> — the form every other subcommand takes in --env; an empty list means the box has none yet, not that this token cannot see them. 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.

Terminal window
npx @unotest/web box envs
npx @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, in the form box envs prints. A read token says who you are, not where to look.
  • --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, then unotest/.env.
  • --token <token> — Read token. Defaults to $UNOTEST_BOX_READ_TOKEN, then unotest/.secrets.
  • --json — Print the box’s answer as JSON instead of a table.

Example

Terminal window
npx @unotest/web box run checkout-mqf3pwr1 --env acme/test --download

box repo

Map this repository to its project on a box rented before 0.54.0, so bundle push stops warning that the repository “maps to no project on this box”. box create maps the project’s git origin itself since 0.54.0; a box rented earlier gets the same line from this command, with no operator. The repository defaults to the checkout’s git origin; credentials in a URL are never sent, and the box keeps the repository without scheme or credentials. It uses the project’s push token — the one bundle push uses, from the same places — and the token decides the project: a repository another project on the box already holds is refused (repo-claimed) with that project’s name. Running it again changes nothing. A box older than this command answers with the next step: press Update on the box in the dashboard, then run it again.

Terminal window
npx @unotest/web box repo add [url] [--box <url>] [--token <token>] [--json]
  • --box <url> — Box address. Defaults to $UNOTEST_BOX_URL, then unotest/.env.
  • --token <token> — The project’s push token. Defaults to $UNOTEST_BOX_TOKEN, then unotest/.secrets.
  • --json — Print {state, project, url} — mapped, already-mapped, box-too-old or refused (with the box’s code and message).
  • exit 2 — Nothing changed: no origin and no URL, a box older than the command, or the box refused (another project holds the repository).

Example

Terminal window
npx @unotest/web box repo add

schedules

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.

Terminal window
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 marked not previewable — an executor may drop it in 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

Terminal window
npx @unotest/web schedules

lint

Static-check scenarios and helpers. Exits non-zero on errors.

Terminal window
npx @unotest/web lint [paths...]
  • <name> — A scenario by the name e2e takes — values/string-escapes is unotest/e2e/values/string-escapes.js — or a file, or a folder (every .js under it). A name that is not there exits 2 and prints where it was looked for, with the closest names.
  • --fix — Write the fixes the diagnostics offer into the files first — let i; for a variable assigned without a declaration — then report what is left.

Example

Terminal window
npx @unotest/web lint

api

Find the DSL function for what you mean. One line per function: signature, the keys its options take, the release that added it, and when you need it. Words search names, synonyms (sleep, clipboard, css, not appear) and descriptions; every word must match. Without a word it prints every function by section. Exits 1 when nothing matches. The same index ships as llms.txt at the package root.

Terminal window
npx @unotest/web api [word...]

Example

Terminal window
npx @unotest/web api wait animation

upgrade-hints

Find the places in unotest/e2e/ that do by hand what a typed step does: a pause before checking that an element is absent or to let an animation settle, page code written as a string, getComputedStyle or naturalWidth in page code. Prints the count per pattern, the step to write instead with its section in the DSL guide, and every file and line. Reads the same rules as lint (lint:typed-step-available, validator:page-code-string) and shows them even when linter.rules turns them off; a line marked // lint-ok is not counted. Always exits 0. The first e2e run on a new version prints the counts once.

Terminal window
npx @unotest/web upgrade-hints

types

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.

Terminal window
npx @unotest/web types

install-chromium

Download Playwright’s bundled Chromium (~150 MB). Not needed if you chose system Chrome/Edge.

Terminal window
npx @unotest/web install-chromium

mcp

Run as an MCP stdio server. This is what your editor launches automatically; you rarely run it by hand.

Terminal window
npx @unotest/web mcp

login

Sign this machine in to unotest cloud — the account that rents boxes. Runs the device flow: prints a link and an eight-character code, opens the browser when there is one (never over a remote shell or in CI), and waits for the approval. The key is kept in ~/.config/unotest/credentials.json (mode 0600, XDG_CONFIG_HOME honoured) and is never printed — not even with --json. A CI job exports UNOTEST_CLOUD_TOKEN instead; it wins over the file. Running login while a valid key is stored is a no-op that says so.

Terminal window
npx @unotest/web login [--json]
  • --json — Print the result as JSON (status, email, cloudUrl); the key is never included.

Example

Terminal window
npx @unotest/web login

logout

Revoke this machine’s cloud key on the server when the cloud can be reached, and remove it locally always. A key set through UNOTEST_CLOUD_TOKEN is not touched — unset the variable instead; the command says so and exits 2.

Terminal window
npx @unotest/web logout [--json]
  • --json — Print the result as JSON (status, revoked, and a note when the server could not be reached).

Example

Terminal window
npx @unotest/web logout

whoami

Who this machine is signed in as, where the key came from (the credentials file or UNOTEST_CLOUD_TOKEN), and the account’s balance. Exits 77 when not signed in, or when the stored key no longer opens the account.

Terminal window
npx @unotest/web whoami [--json]
  • --json — Print email, cloudUrl, source, balanceCents, currency as JSON; the key is never included.

Example

Terminal window
npx @unotest/web whoami

approvals

What is waiting for a person on this account: every request an agent’s key made that it may not carry out by itself — a box to rent, a box another key created to delete, the Picker to turn on — with what it would cost and when it expires. With an id, the state of that one request: pending, approved, declined or failed. An agent that restarted and lost the id it was polling finds its request here instead of asking a second time. Answering is not done here: only a person signed in to the cabinet in a browser can approve or decline, so a terminal holding the key cannot approve its own request. Exits 2 when the request was declined, 1 when it was approved and then did not go through.

Terminal window
npx @unotest/web approvals [id] [--json]
  • --json — Print the list (id, action, price, requestedAt, expiresAt, url), or the state of the one request, as JSON.

Example

Terminal window
npx @unotest/web approvals

picker

Connect this project to the account’s Picker, or disconnect it again — for an account that has the Picker on without renting a box. on fetches this project’s own access and writes the Picker’s three keys: UNOTEST_GROUNDER_MODE=remote and UNOTEST_GROUNDER_REMOTE_URL into unotest/.env, UNOTEST_GROUNDER_REMOTE_TOKEN into unotest/.secrets, with .gitignore guarded the way init does — so the token stays out of the repository. Restart your agent afterwards: it looks for the tool once, when it starts.

The command says the account’s monthly ceiling before anything is spent. There is no hourly charge for a Picker without a box, so that ceiling is the price: grounding stops at it until the month turns or the ceiling rises. The figure comes from the service, which takes it from the grounder that enforces it — when the grounder has not reported, the command says so rather than leaving the silence to read as “no limit”.

off disconnects this project and clears those three keys — but only when they name the account’s grounder: a project pointing at a grounder of its own keeps it and is told so. It leaves the Picker ON for the account; the switch that turns it off for everything, and stops it counting against your monthly ceiling, is in the cabinet. Turning the account’s Picker on is a spend decision and lives in the cabinet too, beside the price.

A box’s own Picker is a different switch over a different charge: box picker.

Terminal window
npx @unotest/web picker on|off [--json]
  • --json — Print picker, grounderUrl, tokensExpireAt and the ceiling in cents as JSON on the on side; picker, connected and what happened to the project’s settings on the off side. Tokens are never printed.

Example

Terminal window
npx @unotest/web picker on

box create

Rent a box and wire this project to it, with no token to copy. The box is named after the project (its package name, as a slug) unless a name is given, and serves the environments the suite already runs locally: every unotest/.env.<name> overlay with its APP_BASE_URL (or the config’s baseUrl) as the target — or a single default environment when there is no overlay. Credit is added in the dashboard, by card: when the balance will not cover the first hour, add it there first. Then the command waits for the box, mints its cloud-signed push and read tokens, writes UNOTEST_BOX_URL to unotest/.env and UNOTEST_BOX_TOKEN + UNOTEST_BOX_READ_TOKEN to unotest/.secrets (guarding .gitignore the way init does), pushes the suite — retrying while a fresh box has no revocation list yet — and ends with the next step: the complete bundle push --run --env … --collection … when the project has one environment and one collection, the viewer’s address plus the command with placeholders otherwise. Nothing is asked on a terminal-less run; the exit code says what happened.

Terminal window
npx @unotest/web box create [name] [--size s|m|l] [--picker] [--yes] [--env <name>]... [--json]
  • --size s|m|l — Box size (default s).
  • --env <name> — Send only the environment of this name; repeat for several. Without it every environment whose target a box can reach is sent, and one on localhost, a private network or a local-only name stays behind with a note — a box runs only your tests and reaches your app at a public address. A named environment the box cannot serve (no such overlay, no target, an unreachable one) is refused before anything is rented.
  • --picker — Rent the box with the Picker. While the Picker is included in the price of the box, the hourly list price is shown struck through and nothing is added to the bill; when it is charged for, the surcharge is quoted from the cloud’s own price list. UNOTEST_GROUNDER_MODE=remote and UNOTEST_GROUNDER_REMOTE_URL then go to unotest/.env and UNOTEST_GROUNDER_REMOTE_TOKEN to unotest/.secrets, so the MCP server grounds intents on the box.
  • --yes — Do not ask about the Picker. Without --picker, a terminal is asked once — with the hourly surcharge read from the cloud’s own price list — and this takes the safe answer, which is no. --json and a run with no terminal do the same. box picker on connects it later.
  • --ttl <hours>h|<days>d|none — The box’s lifetime, like 8h or 2d, up to 30d: at its end the box is deleted by itself and charging stops. Without it the cloud gives the box 24 hours; --ttl none keeps it until it is deleted. The lifetime is shown next to the price, and a request a person approves shows it too. box extend moves the end later.
  • --json — Print the result as JSON (name, status, url, pushed, tokensExpireAt, next; expiresAt when the box has a lifetime).
  • exit 75 — The command stopped short of renting — not enough credit for the first hour, or a wait that ran out. Nothing was created and nothing was charged; the message says which and what to do, and running the command again is the remedy.
  • exit 76 — The terms of service are not accepted for this account; a person must accept them in the browser at the printed address.
  • exit 77 — Not signed in — run login first.
  • --no-generate — Skip the config’s generate command and pack unotest/generated/ as it is. Otherwise it runs from the project root before the box is rented, so the first push carries a fresh snapshot; when it fails nothing is created and nothing is charged (exit 95).

Example

Terminal window
npx @unotest/web box create --size s

box list

Every box on the account, one line each: its name, its state, the version it runs (and the one on offer, when they differ) and the projects it serves. An account with no box says so and names the command that rents one.

Terminal window
npx @unotest/web box list [--json]
  • --json — Print the boxes as a JSON array — the same objects the cloud_boxes_list tool returns.

Example

Terminal window
npx @unotest/web box list

box access

Mint a fresh pair of cloud-signed push and read tokens for the box (they live thirty days) and rewrite unotest/.env and unotest/.secrets. The name defaults to the box UNOTEST_BOX_URL points at (acme.box.unotest.com → acme). bundle push and the box read commands do this on their own when a box says the token has expired, provided a cloud login is at hand.

Terminal window
npx @unotest/web box access [name] [--project <name>] [--json]
  • --project <name> — Which project on the box the push token is for. Only a box built with several environments across several projects needs it: the pair is otherwise scoped to the one project the box serves, and asking for a project the box does not serve is refused with the list of the ones it does. box status lists them.
  • --json — Print name, url, tokensExpireAt as JSON.

Example

Terminal window
npx @unotest/web box access

box status

The box’s state (creating, ready, failed, …), its address and how long it is paid for. The name defaults to the box UNOTEST_BOX_URL points at.

Terminal window
npx @unotest/web box status [name] [--json]
  • --json — Print the record as JSON.

Example

Terminal window
npx @unotest/web box status

box logs

The end of one of the box’s own logs, without logging in to the machine: what was written while it was being made (--bootstrap, the default), or what its updater has said since (--updater). A box speaks when it checks in, every five minutes, so the first call records the ask and says when to run the command again — with exit 0, because nothing failed. Once the box has answered, the lines come back, with a note when the start of the log was cut. The name defaults to the box UNOTEST_BOX_URL points at.

Terminal window
npx @unotest/web box logs [name] [--bootstrap|--updater] [--json]
  • --bootstrap — The log written while the machine was being made. The default.
  • --updater — What the updater has been saying since — where a version that did not install says why.
  • --json — Print the answer as JSON: the lines with collectedAt and truncated, or the receipt with requestedAt and expectedWithinSeconds.

Example

Terminal window
npx @unotest/web box logs --updater

box picker

Turn the Picker on or off on a box you already have — no re-creating it, and no re-pushing the suite. on asks the cloud to switch it, then mints the box’s tokens the way box access does and writes the Picker’s three keys: UNOTEST_GROUNDER_MODE=remote and UNOTEST_GROUNDER_REMOTE_URL into unotest/.env, UNOTEST_GROUNDER_REMOTE_TOKEN into unotest/.secrets. off switches it back and clears those three again — but only when they name THIS box’s grounder: a project pointing at a grounder of its own, or at another box’s, keeps it and is told so.

off also revokes the box’s Picker token, so a token already on somebody else’s machine stops working too: the grounder refuses it once it has re-read the operator’s revocation list — how often it does that is the grounder operator’s own setting, not a number this CLI can promise. Clearing the files is a convenience; the revocation is what actually ends the access.

While the Picker is included in the price of the box, both the question box create asks and this command say so and show what it would otherwise cost; the surcharge — when there is one — starts and stops at the next hourly tick, not at the command, because a started hour is charged at the rate it started with. The name defaults to the box UNOTEST_BOX_URL points at, and asking for the state a box is already in is not an error.

box status reports which state a box is in, with the same price beside it, and — while the Picker is on — which models the cloud grounds intent locators with: Our GPU or External provider (Gemini). That line is about the cloud’s grounder, not about this machine’s UNOTEST_GROUNDER_MODE; a cloud that has not heard from its own grounder says nothing rather than guessing.

Terminal window
npx @unotest/web box picker on|off [name] [--project <name>] [--json]
  • --project <name> — Which project on the box the push token is for, on the on side: it mints a pair the way box access does, and takes the flag for the same reason — only a box serving several projects needs one named.
  • --json — Print name, picker, grounderUrl and (turning it on) tokensExpireAt as JSON. Tokens are never printed.

Example

Terminal window
npx @unotest/web box picker on

box extend

Give a running box more lifetime: its end moves --ttl later — counted from the end it already has, not from now — or --ttl none takes the lifetime away. Nothing is charged at once: the box keeps being charged by the hour while it runs, and the end is where the charging stops. A key that may not spend by itself waits for a person to approve the extension in the dashboard, as box create does. The name defaults to the box UNOTEST_BOX_URL points at. A box with no lifetime has nothing to extend, and an end more than 30 days away is refused.

Terminal window
npx @unotest/web box extend [name] --ttl <hours>h|<days>d|none [--json]
  • --ttl <hours>h|<days>d|none — How much later, like 4h or 1d; none takes the lifetime away. Required.
  • --json — Print the box’s record as JSON, with its new expiresAt.

Example

Terminal window
npx @unotest/web box extend --ttl 4h

box destroy

Destroy the box. Its data goes with it — there is no archive — so the command asks on a terminal and requires --yes without one. A started hour is a paid hour; nothing is refunded. Names are never reused. A box another key created — a new login is a new key — waits for a person in the cabinet, and --yes does not answer for them.

Terminal window
npx @unotest/web box destroy [name] [--yes] [--json]
  • --yes — Destroy without asking — required when there is no terminal.
  • --json — Print the record as JSON.

Example

Terminal window
npx @unotest/web box destroy acme --yes