ADR 0005 — CLI Runner
Context
Section titled “Context”Postman has newman. Bruno has bru run. Hoppscotch has hoppcli. These are the wedges that drive team adoption — without CI integration, an API client is a single-developer toy. The file-collection schema (Git-native YAML files) is the natural input for a CLI; the shared protocol layer is the natural runtime backend. The CLI is the third consumer of executeHttpProxy after the Worker and Electron.
Decision
Section titled “Decision”Ship @restura/cli as a separate npm package in the cli/ directory. Single restura binary with one subcommand for v0.1: restura run <collection-dir>.
flowchart LR C[(Collection<br/>opencollection.yml)] ENV[(--env file<br/>JSON/YAML)] DATA[(--data CSV/JSON)] C --> RC[runCollection] ENV --> RC DATA --> RC RC -->|RequestSpec| SP[executeHttpProxy] SP -->|Fetcher| U[undiciFetcher] U --> UP[(upstream)] UP --> R[Reporters] R --> LIVE[live] R --> JSON[json] R --> JUNIT[junit.xml] R --> HTML[html] classDef shared fill:#7b6ef622,stroke:#7b6ef6,color:#7b6ef6; classDef cli fill:#10b98122,stroke:#10b981,color:#10b981; class SP,U shared; class RC,LIVE,JSON,JUNIT,HTML cli;
Architecture mirrors the renderer’s runtime stack:
loadCollection(dir)— walks_collection.yaml+*.{http,grpc,sse,mcp}.yamlusing the existingfile-collection-schemafromsrc/lib/shared/.loadEnv(file, options)— JSON or YAML env file with${VAR}env-var expansion for secrets.undiciFetcher—Fetcherinterface implementation usingundici.request. The CLI is the third backend (Worker usesglobalThis.fetch, Electron usesundiciviahttp-handler.ts, CLI usesundicidirectly).runCollection(dir, options, reporter)— orchestrator: per-request, buildRequestSpec, callexecuteHttpProxy(spec, undiciFetcher, options), tally results.- 4 reporters (
livedefault +json,junit,html) implementing a smallReporterinterface.
For v0.1: HTTP requests only. gRPC / SSE / MCP request types yield “unsupported” results with a clear message. Pre/post test scripts are deferred — pass/fail is HTTP 2xx for now.
Build via tsup (single 40 KB esm bundle in dist/index.js). tsc was the initial choice for simplicity but doesn’t rewrite TypeScript path aliases (@/*, @shared/*), so the emitted binary failed at runtime with ERR_MODULE_NOT_FOUND. tsup bundles all imports, resolving aliases at build time.
Consequences
Section titled “Consequences”Positive
- Teams can
npm install -g @restura/cliand integrate API tests into CI in minutes — JUnit output plugs into every CI system. - The CLI reuses 90% of the existing shared protocol layer. Adding it surfaced no design changes —
executeHttpProxyalready accepted aFetcher, andundiciwas already a Plan 4 dep. - Single 40 KB bundled binary — no Electron, no Monaco, no React in the install footprint.
- File-collection schema is the contract: the same YAML files run in the desktop app, the web app, and CI.
Negative
- Test scripts deferred.
pm.test()assertions in*.http.yaml’stestScriptfield are ignored in v0.1; the runner only checks HTTP status. Adding script execution requires bundling QuickJS-emscripten into the CLI (~2 MB extra). Tracked as a follow-up. - Bundled deps (
commander,js-yaml,undici,zod,uuid) are duplicated in the CLI bundle and the renderer. For a CLI shipped vianpm install -g, this is acceptable —npm installdeduplicates only within a project. - gRPC / SSE / MCP CLI execution not wired. Server-streaming output in a CLI also raises UX questions (write each event to stdout? buffer to JSON?). Deferred until a real user hits this.
Alternatives considered
Section titled “Alternatives considered”- Bundle the CLI into the existing renderer build. Rejected — users want
npm install -gfor CI without pulling Electron, Monaco, React, jsdom. A separate package is the only way. - Generate code from
*.yamlto a Node test file (Jest / Mocha). Rejected — adds an extra build step, requires teams to learn another runner’s reporting format. JUnit XML output is the lingua franca; the CLI is the runner. - Use Postman’s collection format instead of Restura’s YAML. Rejected — the file-collection schema is already first-class; importing Postman is a one-time conversion handled in the renderer.
- Build with
tscinstead oftsup. Initially chosen for minimal build deps; rejected after smoke-testing the binary failed becausetscdoesn’t rewrite path aliases.tsupadds one devDependency and produces a working single-file bundle.
References
Section titled “References”- Source:
docs/adr/0005-cli-runner.md - User guide: CLI — @restura/cli.
- Related: ADR 0001, ADR 0003 (introduces undici).