wayfarer-cli 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,30 +1,231 @@
1
1
  # wayfarer-cli
2
2
 
3
- Runs a [Wayfarer](https://github.com/AshwinSathian/wayfarer) collection, a
4
- Wayfarer workspace backup or a Postman collection from a terminal or a CI
5
- job, with the engine the Wayfarer app sends with: the same scripts, the
6
- same variables and vault, the same results. It exits with `1` when a test
7
- fails or a request gets no response.
3
+ Run a [Wayfarer](https://wayfarer.ashwinsathian.com) or Postman collection
4
+ from a terminal or a CI job. The command line sends with the same engine as
5
+ the Wayfarer app: the same script sandbox, variables, vault and assertions.
6
+ A run here gives the results the same run gives in the app, and a test in the
7
+ repository checks that in Chromium, Firefox and WebKit.
8
8
 
9
9
  ```bash
10
- npx wayfarer-cli run collection.json -e environment.json -d data.csv --allow-scripts --reporter cli,junit
11
- ```
12
-
13
- - `-e` an environment (Wayfarer or Postman), `-d` a data file (CSV or JSON),
14
- `-n` iterations, `--bail`, `--timeout <ms>`, `--insecure`.
15
- - `--reporter cli,json,junit` writes `wayfarer-run.json` and
16
- `wayfarer-run.junit.xml` (in `--out`, default the current folder).
17
- - **A file's scripts run only with `--allow-scripts`.** Without it, a
18
- collection that has a script is not run and nothing is sent: read a
19
- file's scripts before you allow them.
20
- - A request that uses a vault secret needs the vault: a workspace backup
21
- brings it, or `--vault-file` names an exported one. The passphrase is read
22
- from `WAYFARER_VAULT_PASSPHRASE`.
23
- - Every request goes straight from Node: no CORS, no Local Bridge.
24
-
25
- Node.js 22.12 or later. The full guide, with what the command line does not
26
- do and a GitHub Actions example, is
27
- [`docs/cli.md`](https://github.com/AshwinSathian/wayfarer/blob/main/docs/cli.md).
28
-
29
- MIT licence. The packages bundled into `dist/wayfarer-cli.mjs` are listed,
30
- with their licences, in `dist/THIRD_PARTY_LICENSES.txt`.
10
+ npx wayfarer-cli run orders.postman_collection.json -e local.postman_environment.json --allow-scripts
11
+ ```
12
+
13
+ ```
14
+ ✓ Create order: 200 OK, 16 ms
15
+ ✓ status is 200
16
+ ✓ the SKU was sent
17
+ ✓ Fetch order: 200 OK, 2 ms
18
+ ✓ the SKU came back
19
+
20
+ 2 results in 1 iteration; 0 failed; 3 of 3 tests passed.
21
+ ```
22
+
23
+ When a test fails, the line says what was expected, and the exit code is `1`:
24
+
25
+ ```
26
+ ✗ Fetch order: 200 OK, 3 ms
27
+ ✗ the SKU came back: expected 'ABC-2' to deeply equal 'ABC-1'
28
+
29
+ 2 results in 1 iteration; 1 failed; 2 of 3 tests passed.
30
+ ```
31
+
32
+ ## Install
33
+
34
+ It needs Node.js 22.12 or later. Run it with `npx`, or install it:
35
+
36
+ ```bash
37
+ npm install --global wayfarer-cli
38
+ wayfarer-cli --version
39
+ ```
40
+
41
+ In a project, add it as a dev dependency and pin the major version
42
+ (`wayfarer-cli@2`): a new major version may change what a run does.
43
+
44
+ ## What it runs
45
+
46
+ ```bash
47
+ wayfarer-cli run <file> [options]
48
+ ```
49
+
50
+ `<file>` can be any of:
51
+
52
+ - a Wayfarer collection (Export in the app);
53
+ - a Wayfarer workspace backup (Settings → Backup). It brings its active
54
+ environment, its globals and its vault. A backup with several collections
55
+ needs `--collection <name>`;
56
+ - a Postman collection, v2.0 or v2.1;
57
+ - an Insomnia export or an OpenAPI description, read the way the app
58
+ imports them.
59
+
60
+ ## Options
61
+
62
+ | Option | What it does |
63
+ |---|---|
64
+ | `-e`, `--environment <file>` | The variables: a Wayfarer environments file or a Postman environment. |
65
+ | `-d`, `--data <file>` | A data file, CSV or JSON. Each row is one iteration. |
66
+ | `-n`, `--iterations <n>` | How many iterations. Default: one per row of the data file, or 1. |
67
+ | `--collection <name>` | Which collection to run, from a file that holds several. |
68
+ | `--reporter <list>` | `cli`, `json`, `junit`, comma-separated. Default `cli`. |
69
+ | `--out <dir>` | Where `json` and `junit` reports go. Default: the current folder. |
70
+ | `--bail` | Stop at the first failure. |
71
+ | `--timeout <ms>` | Give up on a request after this long. Default: no limit. |
72
+ | `--insecure` | Don't verify TLS certificates, for every request of the run. |
73
+ | `--allow-scripts` | Run the file's scripts (see below). |
74
+ | `--vault-file <file>` | A vault exported from the app, for requests that use a vault secret. |
75
+ | `-v`, `--version` | Print the version. |
76
+ | `-h`, `--help` | Print the usage. |
77
+
78
+ ### Exit codes
79
+
80
+ | Code | Meaning |
81
+ |---|---|
82
+ | `0` | Every request got a response and every test passed. |
83
+ | `1` | A test failed, a request got no response, or a script ended in an error. |
84
+ | `2` | Nothing was run. The message on stderr says why. |
85
+
86
+ ## Scripts run only when you allow them
87
+
88
+ In the app, scripts from an imported file run only after you've read them and
89
+ marked them as trusted. A file passed to the command line has had no such
90
+ review, so a collection with any pre-request or post-response script is not
91
+ run until you add `--allow-scripts`:
92
+
93
+ ```
94
+ $ npx wayfarer-cli run orders.postman_collection.json -e local.postman_environment.json
95
+ This collection has scripts, in the request "Create order", the request "Fetch order". A file's scripts have not been reviewed: read them, and run again with --allow-scripts only if you trust them. Nothing was sent.
96
+ ```
97
+
98
+ That run exits `2`. With the flag, every script in the file runs in QuickJS
99
+ (compiled to WebAssembly), with the same allow-list of globals as the app.
100
+ There is no `process`, and `require` gives only the five bundled libraries
101
+ (chai, crypto-js, lodash, moment, uuid), so a script can't read your files or
102
+ environment variables. It can still send
103
+ requests of its own with `pm.sendRequest` and change the request it belongs
104
+ to, so read a file's scripts before you allow them.
105
+
106
+ What `pm` supports is listed row by row in
107
+ [docs/postman-compatibility.md](https://github.com/AshwinSathian/wayfarer/blob/main/docs/postman-compatibility.md).
108
+ That file is generated from the cases the test suite runs.
109
+
110
+ ## Coming from Newman
111
+
112
+ Most flags carry over:
113
+
114
+ | Newman | wayfarer-cli |
115
+ |---|---|
116
+ | `newman run c.json` | `wayfarer-cli run c.json` (add `--allow-scripts` if it has scripts) |
117
+ | `-e env.json` | `-e env.json` |
118
+ | `-d data.csv` | `-d data.csv` |
119
+ | `-n 3` | `-n 3` |
120
+ | `--bail` | `--bail` |
121
+ | `--timeout-request 5000` | `--timeout 5000` |
122
+ | `-k`, `--insecure` | `--insecure` |
123
+ | `-r cli,junit --reporter-junit-export out/report.xml` | `--reporter cli,junit --out out` (the file is `wayfarer-run.junit.xml`) |
124
+
125
+ Not supported: a globals file (`-g`), `--env-var`, and running a single
126
+ folder (`--folder`). Put those values in the environment file, or use a
127
+ workspace backup, which carries its globals.
128
+
129
+ The order of a run follows Newman, including `setNextRequest`, skipped
130
+ requests and data-file iterations. That order is checked against
131
+ golden files written by Newman.
132
+
133
+ ## Reports
134
+
135
+ `--reporter json` writes `wayfarer-run.json` and `--reporter junit` writes
136
+ `wayfarer-run.junit.xml`, the same files the app's run view saves. Both go
137
+ in `--out`, which is created if it doesn't exist (2.0.1 and later). The
138
+ JUnit file has one `<testsuite>` per request and one `<testcase>` per test:
139
+
140
+ ```xml
141
+ <testsuites name="Orders API" tests="3" failures="0" errors="0" time="0.019">
142
+ <testsuite name="Create order" id="01a2e75e-…" tests="2" failures="0" errors="0" skipped="0" time="0.016">
143
+ <testcase name="status is 200" classname="Create order"/>
144
+ <testcase name="the SKU was sent" classname="Create order"/>
145
+ </testsuite>
146
+ …
147
+ </testsuites>
148
+ ```
149
+
150
+ ## Secrets and the vault
151
+
152
+ A Wayfarer variable can be protected: its value is a reference to a secret
153
+ in the app's encrypted vault, written `{{$secret.<id>}}`. A request that
154
+ uses one needs the vault. A workspace backup includes it, or `--vault-file`
155
+ names one exported from the app. The passphrase comes from the
156
+ `WAYFARER_VAULT_PASSPHRASE` environment variable, never from an argument,
157
+ because other users of the machine can see a process's arguments.
158
+
159
+ Without the vault, such a request isn't sent: a secret's reference never goes
160
+ out as text. Secrets are masked in both reports, as they are in the app's
161
+ history. When a script sets a protected variable, the new value lasts for the
162
+ rest of the run; the vault file isn't written.
163
+
164
+ The app asks before a pre-request script sends a request with a vault secret
165
+ to a different host. Nobody can answer that in a terminal, so the answer is
166
+ no: the request isn't sent, and the run ends with the reason.
167
+
168
+ ## In CI
169
+
170
+ A GitHub Actions job that runs a collection on every push, keeps the JUnit
171
+ report and fails when a test fails:
172
+
173
+ ```yaml
174
+ name: API tests
175
+ on: [push]
176
+ jobs:
177
+ api:
178
+ runs-on: ubuntu-latest
179
+ steps:
180
+ - uses: actions/checkout@v5
181
+ - uses: actions/setup-node@v5
182
+ with:
183
+ node-version: 24
184
+ - name: Run the collection
185
+ run: npx --yes wayfarer-cli@2 run api/collection.json -e api/ci.environment.json --allow-scripts --reporter cli,junit --out reports
186
+ env:
187
+ # Only needed for requests that use a vault secret.
188
+ WAYFARER_VAULT_PASSPHRASE: ${{ secrets.WAYFARER_VAULT_PASSPHRASE }}
189
+ - uses: actions/upload-artifact@v4
190
+ if: always()
191
+ with:
192
+ name: api-report
193
+ path: reports/wayfarer-run.junit.xml
194
+ ```
195
+
196
+ Every release is published from this repository's
197
+ [`release.yml`](https://github.com/AshwinSathian/wayfarer/blob/main/.github/workflows/release.yml)
198
+ with an npm provenance attestation. `npm audit signatures` in a project that
199
+ installs it checks the attestation.
200
+
201
+ ## Where requests go
202
+
203
+ Every request goes straight from Node. Node has no CORS, no mixed-content
204
+ rule and no forbidden headers, so a route set in the app (Auto, Local
205
+ Bridge) is ignored here, and nothing is ever sent twice.
206
+
207
+ For a proxy, set `NODE_USE_ENV_PROXY=1` together with `HTTPS_PROXY`,
208
+ `HTTP_PROXY` and `NO_PROXY`. That is Node's own proxy support, in Node 22.21
209
+ and 24.5 and later.
210
+
211
+ ## Not supported
212
+
213
+ - **OAuth 2.0 tokens.** A request with OAuth 2.0 auth isn't sent, and the
214
+ result says so. Get the token in an earlier CI step and pass it as a
215
+ variable.
216
+ - **Files in a request body.** A file you pick in the app stays in that
217
+ browser and isn't in the collection file, so such a request isn't sent.
218
+ - **Cookies.** There's no cookie jar: a request carries only the cookies
219
+ it sets itself.
220
+ - **Event streams and WebSockets.** An event stream is read as one body
221
+ when it ends, so give a `--timeout` for one that doesn't end. A `ws:` or
222
+ `wss:` address gets no response.
223
+
224
+ The full guide is
225
+ [docs/cli.md](https://github.com/AshwinSathian/wayfarer/blob/main/docs/cli.md).
226
+
227
+ ## Licence
228
+
229
+ MIT. The packages bundled into `dist/wayfarer-cli.mjs` are listed with their
230
+ licences in `dist/THIRD_PARTY_LICENSES.txt`. The script engine,
231
+ `quickjs-emscripten`, is an ordinary dependency and carries its own.
@@ -7445,7 +7445,7 @@ var init_uuid = __esm({
7445
7445
  });
7446
7446
 
7447
7447
  // src/main.ts
7448
- import { readFile, writeFile } from "node:fs/promises";
7448
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
7449
7449
  import { basename, join } from "node:path";
7450
7450
  import { parseArgs } from "node:util";
7451
7451
 
@@ -8477,7 +8477,6 @@ function validateVaultFile(value) {
8477
8477
  if (typeof vault !== "object" || vault === null) {
8478
8478
  fail("vault", "Value must be an object.");
8479
8479
  } else {
8480
- if (field2(vault, "v") !== 2) fail("vault.v", "Value must be 2.");
8481
8480
  const kdf = field2(vault, "kdf");
8482
8481
  if (typeof kdf !== "object" || kdf === null) {
8483
8482
  fail("vault.kdf", "Value must be an object.");
@@ -8512,7 +8511,6 @@ function validateVaultFile(value) {
8512
8511
  fail(`${path}.envelope`, "Value must be an object.");
8513
8512
  return;
8514
8513
  }
8515
- if (field2(envelope, "v") !== 2) fail(`${path}.envelope.v`, "Value must be 2.");
8516
8514
  if (!isBase64Url(field2(envelope, "iv"))) fail(`${path}.envelope.iv`, "Value must be base64url text.");
8517
8515
  if (!isBase64Url(field2(envelope, "ct"))) fail(`${path}.envelope.ct`, "Value must be base64url text.");
8518
8516
  });
@@ -8720,7 +8718,7 @@ async function withDeadline(options, run) {
8720
8718
  if (cause instanceof TransportError) throw cause;
8721
8719
  if (timedOut) throw new TransportError("timeout", `Timed out after ${options.timeoutMs} ms`);
8722
8720
  if (options.signal.aborted) throw new TransportError("aborted", "The request was cancelled.");
8723
- throw new TransportError("network", cause instanceof Error ? cause.message : String(cause));
8721
+ throw new TransportError("network", networkMessage(cause));
8724
8722
  } finally {
8725
8723
  clearTimeout(timer);
8726
8724
  options.signal.removeEventListener("abort", cancel);
@@ -8778,6 +8776,11 @@ async function readEventStream(response, signal, onEvent) {
8778
8776
  }
8779
8777
  return { body: kept.slice(0, size).buffer, stream: { stopped, ...truncated && { truncated } } };
8780
8778
  }
8779
+ function networkMessage(cause) {
8780
+ if (!(cause instanceof Error)) return String(cause);
8781
+ const code = cause.cause instanceof Error && "code" in cause.cause ? cause.cause.code : void 0;
8782
+ return typeof code === "string" && /^[A-Z][A-Z0-9_]*$/.test(code) ? `${cause.message} (${code})` : cause.message;
8783
+ }
8781
8784
 
8782
8785
  // ../core/src/transport/fetch.ts
8783
8786
  var FetchTransport = class {
@@ -10006,7 +10009,7 @@ function vmDecode(encoding, words) {
10006
10009
  return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
10007
10010
  } catch (error) {
10008
10011
  if (!(error instanceof TypeError)) throw error;
10009
- throw new Error("Malformed UTF-8 data");
10012
+ throw new Error("Malformed UTF-8 data", { cause: error });
10010
10013
  }
10011
10014
  }
10012
10015
  function vmRandom(count) {
@@ -10680,6 +10683,7 @@ var RequestSkipped = class extends SendBlockedError {
10680
10683
  nextRequest;
10681
10684
  };
10682
10685
  var NETWORK_ERROR_TEXT = "Network error \u2014 no response was received. The host may not resolve (DNS), may have refused the connection, or may not allow cross-origin requests from this site (CORS). The browser does not reveal which. Check the URL and your connection; for CORS, the Local Bridge can relay the request.";
10686
+ var networkErrorWithoutPage = (reason) => `Network error \u2014 no response was received: ${reason}. Check the URL, that the host resolves, and that it accepts connections.`;
10683
10687
  var SECRET_PLACEHOLDER_BLOCKED = "This request refers to a vault secret that could not be read (it may have been deleted), so it was not sent. Give the variable a value again.";
10684
10688
  function sentBodyPreview(body3) {
10685
10689
  if (body3 === void 0 || typeof body3 === "string") return body3;
@@ -10826,7 +10830,7 @@ async function exchange(spec, ports) {
10826
10830
  const durationMs = Math.round(performance.now() - startedAt);
10827
10831
  timings.requestMs = durationMs;
10828
10832
  ports.watch?.received(request.url);
10829
- const shaped = outcome instanceof TransportError ? shapeFailure(outcome, request.url) : shapeResponse(outcome, request.url, ports.page);
10833
+ const shaped = outcome instanceof TransportError ? shapeFailure(outcome, request.url, ports.page) : shapeResponse(outcome, request.url, ports.page);
10830
10834
  const redactor = new Redactor([...request.secrets, ...request.credentials, ...cookieValues]);
10831
10835
  const secrets = new Redactor(request.secrets);
10832
10836
  const post = await runPostScriptAndAssertions(
@@ -10926,7 +10930,7 @@ function shapeResponse(envelope, url, page) {
10926
10930
  }
10927
10931
  };
10928
10932
  }
10929
- function shapeFailure(failure, url) {
10933
+ function shapeFailure(failure, url, page) {
10930
10934
  const network = failure.kind === "network";
10931
10935
  const statusCode = network ? 0 : failure.kind === "bridge" ? failure.status ?? 0 : void 0;
10932
10936
  return {
@@ -10940,7 +10944,7 @@ function shapeFailure(failure, url) {
10940
10944
  statusText: network ? "Unknown Error" : "",
10941
10945
  bodyIsJson: false,
10942
10946
  dataText: "",
10943
- errorText: network ? NETWORK_ERROR_TEXT : failure.message,
10947
+ errorText: !network ? failure.message : page === void 0 ? networkErrorWithoutPage(failure.message) : NETWORK_ERROR_TEXT,
10944
10948
  headersView: []
10945
10949
  }
10946
10950
  };
@@ -13267,7 +13271,7 @@ async function main(argv) {
13267
13271
  }
13268
13272
  });
13269
13273
  if (values.version) {
13270
- console.log("2.0.0");
13274
+ console.log("2.0.2");
13271
13275
  return 0;
13272
13276
  }
13273
13277
  const [command, file, ...rest] = positionals;
@@ -13306,6 +13310,11 @@ ${USAGE}`);
13306
13310
  }
13307
13311
  if (values.insecure) process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
13308
13312
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
13313
+ if (reporters.has("json") || reporters.has("junit")) {
13314
+ await mkdir(values.out, { recursive: true }).catch((error) => {
13315
+ throw new Refusal(`--out ${values.out}: ${error instanceof Error ? error.message : String(error)}`);
13316
+ });
13317
+ }
13309
13318
  const report = await runCollection(
13310
13319
  { source: read.source, ...environment2 && { environment: environment2 }, globals: read.globals, ...vault && { vault }, allowScripts: values["allow-scripts"], timeoutMs },
13311
13320
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfarer-cli",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Runs Wayfarer and Postman collections from the command line, with the engine the Wayfarer app sends with: the same scripts, variables, vault and results.",
5
5
  "license": "MIT",
6
6
  "author": "Ashwin Sathian",