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 +227 -26
- package/dist/wayfarer-cli.mjs +18 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,30 +1,231 @@
|
|
|
1
1
|
# wayfarer-cli
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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.
|
package/dist/wayfarer-cli.mjs
CHANGED
|
@@ -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",
|
|
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 ?
|
|
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.
|
|
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.
|
|
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",
|