Skip to content

CLI — @restura/cli

@restura/cli runs your collections headlessly with the same protocol implementations as the desktop and web apps. Drop it into CI to assert your API, get JUnit XML for your dashboard, ship an HTML report to your team.

flowchart LR
  CI[CI runner] -->|exec| CLI[restura run]
  CLI -->|load| COL[(OpenCollection<br/>dir / .yaml)]
  CLI -->|exec| H[HTTP / GraphQL]
  CLI -->|exec| G[gRPC Connect]
  CLI -->|exec| S[SSE]
  CLI -->|exec| M[MCP]
  CLI -->|exec| W[WebSocket]
  H --> R1[live · json · junit · html]
  G --> R1
  S --> R1
  M --> R1
  W --> R1
  R1 -->|exit code| CI

  classDef cli fill:#7b6ef6,stroke:#4f46e5,color:#fff;
  classDef proto fill:#6366f122,stroke:#7b6ef6,color:#7b6ef6;
  class CLI cli;
  class H,G,S,M,W proto;
Terminal window
npm install -g @restura/cli
# or, no install:
npx @restura/cli run ./my-collection

Requires Node.js 24+.

Export a collection from the Restura app (File → Export → OpenCollection directory), then:

Terminal window
restura run ./my-collection --reporter junit --reporter-output junit=results.xml

Exit code is 0 when every request passed, 1 if any failed, 2 on internal errors (missing collection, bad flags).

The loader auto-detects three layouts:

LayoutDetected when…
OpenCollection directory (preferred)the target directory contains opencollection.yml (or .yaml)
OpenCollection bundled filethe target path ends in .yaml / .yml
Legacy file-collection (deprecated)the target directory contains _collection.yaml

The legacy format prints a stderr deprecation warning the first time it’s loaded.

ProtocolNotes
HTTP / RESTFull support
GraphQLRuns as HTTP with body type graphql
gRPCVia Connect protocol (JSON-encoded, no proto compilation needed)
SSECaptures events for --sse-duration ms, or until --sse-events N
MCPSingle JSON-RPC POST per request
WebSocketExecutor available; not yet wired into the dispatcher
Terminal window
restura run <collection> [options]

<collection> accepts a directory (any supported layout) or a bundled .yaml / .yml file.

FlagDefaultDescription
--env <file>JSON or YAML env file. ${VAR} placeholders are expanded from process.env.
--reporter <list>liveComma-separated. Mix and match: live, json, junit, html.
--output <file>Shorthand for single file reporter.
--reporter-output <kv...>Per-reporter output: --reporter-output junit=results.xml html=report.html.
--bailfalseStop on first failure.
--timeout <ms>30000Per-request timeout.
--allow-localhostfalsePermit requests to localhost / 127.0.0.1. Off by default (SSRF guard).
--folder <path>Only run requests under this folder path (slash-joined).
--include <pattern...>Substring or glob (e.g. users/*). Repeatable.
--exclude <pattern...>Same syntax as --include. Applied after.
--data <file>CSV (with header row) or JSON array. Runs the collection once per row.
--max-iterations <n>Cap iterations when a --data file is large.
--retry <n>0Retry attempts per failing request.
--retry-on <list>network,5xxTriggers: network, 5xx, 4xx, or specific status codes (429,503).
--sse-duration <ms>5000How long to keep SSE streams open.
--sse-events <n>Stop SSE early after N events.
--ws-duration <ms>5000How long to keep WebSocket connections open.
--ws-messages <n>Stop WebSocket early after N messages.

Pre-request and test scripts run in a sandboxed QuickJS WASM VM — no DOM, no filesystem, no network escape; 10 MB memory cap, 5 s execution timeout. See Scripts for the full Postman API surface.

request.http.yaml
name: Get user
method: GET
url: "{{API_BASE}}/users/1"
testScript: |
pm.test("status is 200", () => pm.response.to.have.status(200));
pm.test("response has name", () => {
pm.expect(pm.response.json()).to.have.property("name");
});

When a test script runs and defines any pm.test(...) assertion, those drive pass/fail. Otherwise pass/fail falls back to the transport outcome (HTTP 2xx, gRPC OK, etc.).

Variables set inside a script (pm.environment.set('K', 'v')) propagate to subsequent requests in the same run.

Three layered sources, in order of precedence (later wins):

  1. --env file
  2. Collection variables (declared in opencollection.yml or _collection.yaml)
  3. Iteration row (when --data is set)

Substitutions use {{NAME}}. Unknown vars are left in place so the upstream sees them and you notice the gap.

Postman-compatible {{$random*}} / {{$timestamp}} helpers are expanded after user var substitution:

HelperExample
{{$randomUUID}}f4d2e3...
{{$timestamp}}1700000000000
{{$isoTimestamp}}2026-05-22T13:42:00Z
{{$randomEmail}}alice.42@example.com
{{$randomFirstName}}Olivia
{{$randomIP}}192.0.2.4

Full list in src/lib/shared/dynamicVariables.ts.

Terminal window
restura run ./users-api --data ./users.csv --reporter junit --reporter-output junit=junit.xml
users.csv
username,role
alice,admin
bob,viewer
charlie,editor

Each row exposes username and role as variables, overriding any same-named env or collection variable for that iteration only. Reporter output groups results by iteration index.

Coloured progress to stdout. Default. Best for local runs.

Combine with a comma: --reporter live,junit --reporter-output junit=results.xml.

CodeMeaning
0Every request passed AND at least one request ran
1One or more requests failed or errored (or no requests matched after filtering)
2Internal error: missing collection, bad reporter name, IO failure, …
name: API smoke tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '24' }
- run: npx @restura/cli run ./api-tests --env env.staging.json --reporter live,junit --reporter-output junit=results.xml
- uses: dorny/test-reporter@v1
if: always()
with:
name: API smoke tests
path: results.xml
reporter: java-junit
SymptomLikely cause / fix
No recognised collection layoutTarget needs opencollection.yml / .yaml or legacy _collection.yaml. Re-export from the Restura app.
Invalid URLURL after {{var}} resolution isn’t a valid absolute URL. Check --env is loaded and names match.
Localhost URLs are not allowedAdd --allow-localhost for local upstreams. Off by default to prevent SSRF in shared CI.
gRPC requests return UNKNOWNUpstream doesn’t speak Connect protocol. Restura’s CLI uses Connect-over-HTTP, not gRPC-over-HTTP/2 binary.
a secret handle ref is unresolvable in CLIYour auth uses a desktop-only secret handle. Re-export with inline values for CI use.
Terminal window
# from cli/
npm install
npm test # vitest
npm run type-check # tsc --noEmit
npm run build # tsup → dist/

The CLI imports from the parent project at compile-time via path aliases (@/, @shared/); cli/tsconfig.json controls which parent modules participate in type-checking.