@cloudbeat/atlas-cli 0.4.2 → 0.5.0
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 +3 -3
- package/dist/api.js +10 -4
- package/dist/commands/run.js +51 -20
- package/dist/docs/automation.md +1 -1
- package/dist/docs/commands.md +7 -7
- package/dist/docs/index.json +1 -1
- package/dist/docs/projects.md +2 -2
- package/dist/docs/recorder.md +33 -1
- package/dist/docs/results.md +1 -1
- package/dist/docs/running.md +2 -2
- package/dist/docs/troubleshooting.md +14 -1
- package/dist/skills/atlas-correlate/SKILL.md +26 -1
- package/dist/skills/atlas-record/SKILL.md +7 -0
- package/dist/ui.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,9 +101,9 @@ Without it, the command returns as soon as the test has been submitted, and you
|
|
|
101
101
|
can check on it later:
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
|
-
atlas status SHOP-20260818-
|
|
105
|
-
atlas results SHOP-20260818-
|
|
106
|
-
atlas stop SHOP-20260818-
|
|
104
|
+
atlas status SHOP-20260818-K3F9
|
|
105
|
+
atlas results SHOP-20260818-K3F9
|
|
106
|
+
atlas stop SHOP-20260818-K3F9
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
`--jmx` is only needed when a project holds more than one test plan.
|
package/dist/api.js
CHANGED
|
@@ -91,7 +91,7 @@ class AtlasApi {
|
|
|
91
91
|
// 401/403 are almost always a credential problem the user must fix, not a
|
|
92
92
|
// transient failure worth retrying — surface them as usage errors.
|
|
93
93
|
const code = res.status === 401 || res.status === 403 ? ui_1.EXIT.USAGE : ui_1.EXIT.ERROR;
|
|
94
|
-
throw new ui_1.CliError(`${method} ${routePath} → ${res.status}: ${message}`, code);
|
|
94
|
+
throw new ui_1.CliError(`${method} ${routePath} → ${res.status}: ${message}`, code, res.status);
|
|
95
95
|
}
|
|
96
96
|
return payload;
|
|
97
97
|
}
|
|
@@ -122,12 +122,18 @@ class AtlasApi {
|
|
|
122
122
|
/** Projects are addressed by name on the CLI; the API wants the id. */
|
|
123
123
|
async projectByName(name) {
|
|
124
124
|
const all = await this.projects();
|
|
125
|
-
const
|
|
126
|
-
if (!
|
|
125
|
+
const hits = all.filter(p => p.name.toLowerCase() === name.toLowerCase());
|
|
126
|
+
if (!hits.length) {
|
|
127
127
|
const known = all.map(p => p.name).join(', ') || '(none visible to this token)';
|
|
128
128
|
throw new ui_1.CliError(`No project named "${name}". Available: ${known}`, ui_1.EXIT.USAGE);
|
|
129
129
|
}
|
|
130
|
-
|
|
130
|
+
// Names are unique within an account, not across them: a login that sees
|
|
131
|
+
// several accounts can see two. An API token belongs to one account.
|
|
132
|
+
if (hits.length > 1) {
|
|
133
|
+
throw new ui_1.CliError(`${hits.length} projects named "${name}" are visible to this login, in different accounts. ` +
|
|
134
|
+
'Use an API token from the account you mean.', ui_1.EXIT.USAGE);
|
|
135
|
+
}
|
|
136
|
+
return hits[0];
|
|
131
137
|
}
|
|
132
138
|
async files(projectId) {
|
|
133
139
|
return this.request('GET', `${this.api}/projects/${projectId}/files`);
|
package/dist/commands/run.js
CHANGED
|
@@ -12,6 +12,13 @@ const gates_1 = require("../gates");
|
|
|
12
12
|
const junit_1 = require("../junit");
|
|
13
13
|
const results_1 = require("./results");
|
|
14
14
|
const TERMINAL = ['done', 'cancelled', 'error'];
|
|
15
|
+
/**
|
|
16
|
+
* Public dashboards live under /app/public since 2026-10. Older CLIs still print
|
|
17
|
+
* the root form (/<project>/<testId>); the dashboard redirects it.
|
|
18
|
+
*/
|
|
19
|
+
function dashboardUrl(baseUrl, project, testId) {
|
|
20
|
+
return `${baseUrl}/app/public/${encodeURIComponent(project)}/${encodeURIComponent(testId)}`;
|
|
21
|
+
}
|
|
15
22
|
/** Pick which load generators to target: explicit names, a count, or a cluster. */
|
|
16
23
|
async function resolveTarget(api, opts) {
|
|
17
24
|
if (opts.cluster) {
|
|
@@ -26,25 +33,30 @@ async function resolveTarget(api, opts) {
|
|
|
26
33
|
}
|
|
27
34
|
return { poolId: hit.id, unitCount, describe: `${unitCount} unit(s) on cluster ${hit.name}` };
|
|
28
35
|
}
|
|
36
|
+
// A bare count: let Atlas pick N available machines (see submit()).
|
|
37
|
+
if (!opts.server?.length) {
|
|
38
|
+
const n = parseInt(opts.servers ?? '1', 10);
|
|
39
|
+
if (!Number.isFinite(n) || n < 1)
|
|
40
|
+
throw new ui_1.CliError('--servers must be a positive integer', ui_1.EXIT.USAGE);
|
|
41
|
+
return { unitCount: n, describe: `${n} machine${n === 1 ? '' : 's'}` };
|
|
42
|
+
}
|
|
29
43
|
const fleet = await api.servers();
|
|
30
44
|
if (fleet.length === 0) {
|
|
31
45
|
throw new ui_1.CliError('No load generators available to this credential.', ui_1.EXIT.USAGE);
|
|
32
46
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
throw new ui_1.CliError(`No server named "${name}". Available: ${fleet.map(s => s.name).join(', ')}`, ui_1.EXIT.USAGE);
|
|
40
|
-
}
|
|
41
|
-
ids.push(hit.id);
|
|
47
|
+
const wanted = opts.server.flatMap(s => s.split(','));
|
|
48
|
+
const ids = [];
|
|
49
|
+
for (const name of wanted.map(w => w.trim()).filter(Boolean)) {
|
|
50
|
+
const hit = fleet.find(s => s.name.toLowerCase() === name.toLowerCase());
|
|
51
|
+
if (!hit) {
|
|
52
|
+
throw new ui_1.CliError(`No server named "${name}". Available: ${fleet.map(s => s.name).join(', ')}`, ui_1.EXIT.USAGE);
|
|
42
53
|
}
|
|
43
|
-
|
|
54
|
+
ids.push(hit.id);
|
|
44
55
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
return { serverIds: ids, describe: `${ids.length} server(s)` };
|
|
57
|
+
}
|
|
58
|
+
/** `--servers <n>` before Atlas could pick by count: the first N visible. */
|
|
59
|
+
function pickFirst(fleet, n) {
|
|
48
60
|
if (n > fleet.length) {
|
|
49
61
|
throw new ui_1.CliError(`Asked for ${n} servers but only ${fleet.length} are available.`, ui_1.EXIT.USAGE);
|
|
50
62
|
}
|
|
@@ -101,15 +113,34 @@ function registerRun(program) {
|
|
|
101
113
|
// rather than silently producing nothing.
|
|
102
114
|
const wait = opts.wait || gates.length > 0 || !!opts.junit;
|
|
103
115
|
const project = await api.projectByName(name);
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
serverIds:
|
|
107
|
-
poolId:
|
|
108
|
-
unitCount:
|
|
116
|
+
let target = await resolveTarget(api, opts);
|
|
117
|
+
const submit = (t) => api.run(project.id, {
|
|
118
|
+
serverIds: t.serverIds,
|
|
119
|
+
poolId: t.poolId,
|
|
120
|
+
unitCount: t.unitCount,
|
|
109
121
|
jmx: opts.jmx,
|
|
110
122
|
jmeterArgs: opts.args,
|
|
111
123
|
splitCsvs: opts.split,
|
|
112
124
|
});
|
|
125
|
+
let submitted;
|
|
126
|
+
try {
|
|
127
|
+
submitted = await submit(target);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
// A bare count that Atlas cannot place: an Atlas from before
|
|
131
|
+
// count-based runs ("serverIds array … required"), or an account whose
|
|
132
|
+
// machines span several pools ("Say which machines"). Fall back to
|
|
133
|
+
// choosing the first N visible servers, as this CLI always did.
|
|
134
|
+
const bareCount = target.unitCount != null && target.poolId == null && !target.serverIds;
|
|
135
|
+
if (!(bareCount && err instanceof ui_1.CliError && err.httpStatus === 400
|
|
136
|
+
&& /serverIds array|Say which machines/.test(err.message)))
|
|
137
|
+
throw err;
|
|
138
|
+
const fleet = await api.servers();
|
|
139
|
+
if (fleet.length === 0)
|
|
140
|
+
throw new ui_1.CliError('No load generators available to this credential.', ui_1.EXIT.USAGE);
|
|
141
|
+
target = pickFirst(fleet, target.unitCount);
|
|
142
|
+
submitted = await submit(target);
|
|
143
|
+
}
|
|
113
144
|
if (!opts.json) {
|
|
114
145
|
ui_1.log.ok(submitted.status === 'queued'
|
|
115
146
|
? `Queued (position ${submitted.position ?? '?'}) — servers are busy with another run.`
|
|
@@ -121,7 +152,7 @@ function registerRun(program) {
|
|
|
121
152
|
if (opts.json)
|
|
122
153
|
console.log(JSON.stringify(submitted, null, 2));
|
|
123
154
|
else if (submitted.testId) {
|
|
124
|
-
ui_1.log.dim(`dashboard: ${api.baseUrl
|
|
155
|
+
ui_1.log.dim(`dashboard: ${dashboardUrl(api.baseUrl, project.name, submitted.testId)}`);
|
|
125
156
|
}
|
|
126
157
|
return;
|
|
127
158
|
}
|
|
@@ -162,7 +193,7 @@ function registerRun(program) {
|
|
|
162
193
|
else
|
|
163
194
|
ui_1.log.error(chalk_1.default.bold('gate ') + (0, gates_1.formatGate)(r));
|
|
164
195
|
}
|
|
165
|
-
ui_1.log.dim(`dashboard: ${api.baseUrl
|
|
196
|
+
ui_1.log.dim(`dashboard: ${dashboardUrl(api.baseUrl, project.name, entry.testId)}`);
|
|
166
197
|
}
|
|
167
198
|
const failed = results.filter(r => !r.passed);
|
|
168
199
|
if (failed.length) {
|
package/dist/docs/automation.md
CHANGED
|
@@ -9,7 +9,7 @@ and everything it does is scriptable.
|
|
|
9
9
|
```sh
|
|
10
10
|
atlas projects --json
|
|
11
11
|
atlas capacity --json
|
|
12
|
-
atlas results SHOP-20260917-
|
|
12
|
+
atlas results SHOP-20260917-7QKD --json
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Every command takes `--json`. On failure it prints the error as JSON on
|
package/dist/docs/commands.md
CHANGED
|
@@ -72,10 +72,10 @@ the test ID.
|
|
|
72
72
|
## Checking on a run
|
|
73
73
|
|
|
74
74
|
```sh
|
|
75
|
-
atlas status SHOP-20260818-
|
|
76
|
-
atlas results SHOP-20260818-
|
|
77
|
-
atlas results SHOP-20260818-
|
|
78
|
-
atlas stop SHOP-20260818-
|
|
75
|
+
atlas status SHOP-20260818-K3F9
|
|
76
|
+
atlas results SHOP-20260818-K3F9
|
|
77
|
+
atlas results SHOP-20260818-K3F9 --json
|
|
78
|
+
atlas stop SHOP-20260818-K3F9
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
`results` prints the aggregation report — one row per request, plus totals.
|
|
@@ -105,9 +105,9 @@ and GUI listeners left switched on.
|
|
|
105
105
|
|
|
106
106
|
```sh
|
|
107
107
|
atlas tests -p SHOP --limit 10 # recent runs, newest first
|
|
108
|
-
atlas timeseries SHOP-20260818-
|
|
109
|
-
atlas errors SHOP-20260818-
|
|
110
|
-
atlas errors SHOP-20260818-
|
|
108
|
+
atlas timeseries SHOP-20260818-K3F9 # users, response time and errors over time
|
|
109
|
+
atlas errors SHOP-20260818-K3F9 # every distinct error, with its share
|
|
110
|
+
atlas errors SHOP-20260818-K3F9 --full --label "01.01 /home"
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
`timeseries` shows the shape of a run — where users peaked and where response
|
package/dist/docs/index.json
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
{
|
|
27
27
|
"topic": "results",
|
|
28
28
|
"title": "Reading Results",
|
|
29
|
-
"summary": "Every run has a dashboard. Open it from a project's **Results** tab, or from the test list at
|
|
29
|
+
"summary": "Every run has a dashboard. Open it from a project's **Results** tab, or from the test list at `/app/public/<project>`.",
|
|
30
30
|
"source": "guide/results.md"
|
|
31
31
|
},
|
|
32
32
|
{
|
package/dist/docs/projects.md
CHANGED
|
@@ -18,8 +18,8 @@ long it ran, and the peak number of concurrent users it reached. Click a run to
|
|
|
18
18
|
open its dashboard.
|
|
19
19
|
|
|
20
20
|
You can give a run a **name** — click the name cell and type. This is worth
|
|
21
|
-
doing when you run variations against each other, since `SHOP-20260818-
|
|
22
|
-
tells you
|
|
21
|
+
doing when you run variations against each other, since `SHOP-20260818-K3F9`
|
|
22
|
+
tells you which day a run happened but not what it was for. "Baseline, 500 users" is
|
|
23
23
|
easier to find three weeks later.
|
|
24
24
|
|
|
25
25
|
### Files
|
package/dist/docs/recorder.md
CHANGED
|
@@ -50,7 +50,10 @@ While recording you can:
|
|
|
50
50
|
|
|
51
51
|
## What's kept, and what isn't
|
|
52
52
|
|
|
53
|
-
**Kept:** the requests your application makes — pages, API calls, form posts
|
|
53
|
+
**Kept:** the requests your application makes — pages, API calls, form posts —
|
|
54
|
+
and the CORS preflight a browser sends before calling an API on another domain.
|
|
55
|
+
Preflights are marked `(OPTIONS)` and recorded once per address, the way a real
|
|
56
|
+
visitor's browser sends them.
|
|
54
57
|
|
|
55
58
|
**Skipped:** images, stylesheets, fonts and known analytics traffic. These
|
|
56
59
|
inflate a load test without telling you anything about your application. Include
|
|
@@ -64,9 +67,32 @@ and pulls it out of the live response at run time instead of replaying the
|
|
|
64
67
|
recorded one. Without this, a recorded plan usually fails on the second run,
|
|
65
68
|
when the token has changed.
|
|
66
69
|
|
|
70
|
+
This covers values wherever they are sent and wherever they came from:
|
|
71
|
+
|
|
72
|
+
- in the query, a form, a JSON body or a header;
|
|
73
|
+
- **inside the address itself** — the cart or order your session created, as in
|
|
74
|
+
`/cart/{id}/items/{id}`. Only parts of the address that are clearly generated
|
|
75
|
+
IDs are touched, and only when your application's API returned them; the fixed
|
|
76
|
+
parts of an address never are;
|
|
77
|
+
- from a response body, a response header, or a redirect on the way.
|
|
78
|
+
|
|
67
79
|
Each of these gets a check attached, so if the value ever stops coming back the
|
|
68
80
|
sample fails loudly rather than quietly posting a stale token.
|
|
69
81
|
|
|
82
|
+
## Errors you meant to get
|
|
83
|
+
|
|
84
|
+
A request that answered with an error while you recorded — "who am I?" returning
|
|
85
|
+
401 before you signed in — is part of the flow, not a failure. The plan expects
|
|
86
|
+
that same status and checks for it, so it doesn't count as an error, while a
|
|
87
|
+
different code (a 500 where a 401 was recorded) still fails the sample.
|
|
88
|
+
|
|
89
|
+
## Every page is checked by its title
|
|
90
|
+
|
|
91
|
+
When a page you visited had a title, the plan checks the same title comes back on
|
|
92
|
+
every run, so a login screen or error page served in place of the real one fails
|
|
93
|
+
the sample instead of counting as a success. If your titles change from run to run
|
|
94
|
+
(a user name, a date), record with `--no-title-assertions`.
|
|
95
|
+
|
|
70
96
|
## Adding to an existing plan
|
|
71
97
|
|
|
72
98
|
Point `-o` at a plan that already exists and the new recording is **added** to it
|
|
@@ -105,6 +131,7 @@ atlas-rec record https://app.example.com/home -o app.jmx --cookies ./cookies.jso
|
|
|
105
131
|
| `--correlations <file>` | Pin named parameters to a value of your choosing |
|
|
106
132
|
| `--no-correlate` | Replay recorded values as-is |
|
|
107
133
|
| `--no-timestamps` | Keep recorded timestamps instead of using the current time |
|
|
134
|
+
| `--no-title-assertions` | Don't check that each page returns its recorded title |
|
|
108
135
|
|
|
109
136
|
## Check the plan before running it at scale
|
|
110
137
|
|
|
@@ -117,6 +144,11 @@ on its own, so that step can hit the destination twice per iteration. Delete the
|
|
|
117
144
|
duplicate from the control panel, or from the plan afterwards, if it would skew
|
|
118
145
|
your numbers.
|
|
119
146
|
|
|
147
|
+
Start from a browser that has never visited the site, or sign out and clear the
|
|
148
|
+
site's data first. A browser that has been there before can carry things over —
|
|
149
|
+
a cart it remembers, a stored sign-in token — and the recording then depends on
|
|
150
|
+
them, while every simulated user starts from nothing.
|
|
151
|
+
|
|
120
152
|
**Think-time pauses** are recorded but switched off, so the plan runs flat out
|
|
121
153
|
until you decide otherwise. Enable the `Settings - Sleeps` block in the plan and
|
|
122
154
|
set the durations you want.
|
package/dist/docs/results.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Reading Results
|
|
2
2
|
|
|
3
3
|
Every run has a dashboard. Open it from a project's **Results** tab, or from the
|
|
4
|
-
test list at
|
|
4
|
+
test list at `/app/public/<project>`.
|
|
5
5
|
|
|
6
6
|
The dashboard refreshes itself while a test is running, so you can leave it open
|
|
7
7
|
from start to finish.
|
package/dist/docs/running.md
CHANGED
|
@@ -49,7 +49,7 @@ producing load.
|
|
|
49
49
|
From the CLI:
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
|
-
atlas status SHOP-20260818-
|
|
52
|
+
atlas status SHOP-20260818-K3F9
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
## If a machine is busy
|
|
@@ -63,7 +63,7 @@ don't need to retry.
|
|
|
63
63
|
Press **Stop**, or:
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
atlas stop SHOP-20260818-
|
|
66
|
+
atlas stop SHOP-20260818-K3F9
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
Atlas asks JMeter to shut down cleanly so the final results are recorded, and
|
|
@@ -73,7 +73,20 @@ Usually a dynamic value that wasn't correlated — a token that was valid when y
|
|
|
73
73
|
recorded and isn't now. Re-record with correlation enabled (the default), and
|
|
74
74
|
check that the step which issues the token is part of the recording.
|
|
75
75
|
|
|
76
|
+
If the value is sent but never appeared in an earlier response, the browser
|
|
77
|
+
brought it with it — a cart or sign-in token remembered from a previous visit.
|
|
78
|
+
Record again from a browser with no history for the site.
|
|
79
|
+
|
|
80
|
+
## Every request gets a blank page or a "Request Not Allowed" response
|
|
81
|
+
|
|
82
|
+
The site is protected against bots, and the load generators look like one. The
|
|
83
|
+
recorder's browser can be turned away the same way. This is not something to work
|
|
84
|
+
around in the plan: ask the site's owner to allow the load generators' addresses
|
|
85
|
+
(and yours, for recording), or to give you a test environment without the
|
|
86
|
+
protection. A captcha on sign-in needs the same treatment — a recorded captcha
|
|
87
|
+
answer is single-use and expires within minutes.
|
|
88
|
+
|
|
76
89
|
## Getting help
|
|
77
90
|
|
|
78
|
-
When reporting a problem, include the **test ID** (`SHOP-20260818-
|
|
91
|
+
When reporting a problem, include the **test ID** (`SHOP-20260818-K3F9`). It
|
|
79
92
|
identifies the run and everything recorded about it.
|
|
@@ -32,7 +32,32 @@ Judge them:
|
|
|
32
32
|
- A value that is sent but appears in **no** earlier response → the recorder
|
|
33
33
|
cannot correlate it, and it is exactly the kind that breaks. Find out what it
|
|
34
34
|
is: generated in the browser (a timestamp, a random id, a fingerprint), or
|
|
35
|
-
supposed to vary per user (a login, an account number)
|
|
35
|
+
supposed to vary per user (a login, an account number), or **left over from an
|
|
36
|
+
earlier visit** — a cart id or bearer token the browser had stored. The last
|
|
37
|
+
kind means the recording was not made as a first-time visitor; the clean fix is
|
|
38
|
+
to record again from a browser with no history for the site.
|
|
39
|
+
|
|
40
|
+
What the recorder covers, so you know where to look for gaps:
|
|
41
|
+
|
|
42
|
+
- Values in the query, a form, a JSON body, dynamic headers, and **generated IDs
|
|
43
|
+
in the URL path** (`/cart/{uuid}`) — paths only for UUIDs, long hex or opaque
|
|
44
|
+
tokens that an API JSON response or a response header returned.
|
|
45
|
+
- Sources in a response body, a response header (matched case-insensitively), or
|
|
46
|
+
the first hop of a redirect (the extractor covers sub-samples).
|
|
47
|
+
|
|
48
|
+
What it does not:
|
|
49
|
+
|
|
50
|
+
- **A list sent back joined** (`uuid_list=a,b,c` built from a JSON array). Add a
|
|
51
|
+
JSON Extractor on the array with match number `-1` and *Compute concatenation
|
|
52
|
+
var* on, and send `${name_ALL}`. If the array can be empty, put the consuming
|
|
53
|
+
request under an If Controller on `${name_matchNr}` — the application would
|
|
54
|
+
not make that call at all.
|
|
55
|
+
- **Values the user chose** — a search term, a date, a city. These are input, not
|
|
56
|
+
correlation; if the recorder tied one to a tracker's response, make it a literal
|
|
57
|
+
or a CSV column.
|
|
58
|
+
- **Browser-generated security data** — a bot-protection sensor post, a captcha
|
|
59
|
+
token. Do not correlate or replay these; they need the site owner to exempt the
|
|
60
|
+
load generators.
|
|
36
61
|
|
|
37
62
|
## Pin the rest yourself
|
|
38
63
|
|
|
@@ -152,6 +152,13 @@ The same capture always produces the same plan, so this is safe to repeat.
|
|
|
152
152
|
|
|
153
153
|
## Things that will bite you
|
|
154
154
|
|
|
155
|
+
- **A blank page or "Request Not Allowed" on the first load** means the site's
|
|
156
|
+
bot protection turned the recorder's browser away. Stop and tell the person.
|
|
157
|
+
Never try to get past it — it is the site owner's control, and the load test
|
|
158
|
+
will meet it too. The fix is for the owner to allow the person's addresses.
|
|
159
|
+
- **Record as a first-time visitor.** A browser with history for the site can
|
|
160
|
+
bring a stored cart or sign-in token along, and the recording then depends on
|
|
161
|
+
it. Check `suggest` for values that appear in no earlier response.
|
|
155
162
|
- **Never trigger a native dialog** (`alert`, `confirm`, a file picker). They
|
|
156
163
|
block the browser and the session stops responding. Avoid clicking anything
|
|
157
164
|
that might delete or confirm.
|
package/dist/ui.js
CHANGED
|
@@ -24,9 +24,12 @@ exports.EXIT = {
|
|
|
24
24
|
/** Thrown for expected failures — reported without a stack trace. */
|
|
25
25
|
class CliError extends Error {
|
|
26
26
|
code;
|
|
27
|
-
|
|
27
|
+
httpStatus;
|
|
28
|
+
/** `httpStatus`: the API's status when the error is an API refusal. */
|
|
29
|
+
constructor(message, code = exports.EXIT.ERROR, httpStatus) {
|
|
28
30
|
super(message);
|
|
29
31
|
this.code = code;
|
|
32
|
+
this.httpStatus = httpStatus;
|
|
30
33
|
this.name = 'CliError';
|
|
31
34
|
}
|
|
32
35
|
}
|