@cloudbeat/atlas-cli 0.3.0 → 0.4.1
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 +46 -19
- package/dist/api.js +20 -0
- package/dist/cli.js +34 -6
- package/dist/commands/analyze.js +118 -0
- package/dist/commands/capacity.js +104 -0
- package/dist/commands/docs.js +57 -0
- package/dist/commands/files.js +18 -4
- package/dist/commands/plan.js +100 -0
- package/dist/commands/record.js +11 -1
- package/dist/commands/results.js +13 -7
- package/dist/commands/schema.js +86 -0
- package/dist/commands/skills.js +111 -0
- package/dist/commands/tests.js +73 -0
- package/dist/docs/agent-setup.md +109 -0
- package/dist/docs/agent-tasks.md +169 -0
- package/dist/docs/ai-agents.md +90 -0
- package/dist/docs/auth.md +74 -0
- package/dist/docs/automation.md +150 -0
- package/dist/docs/ci.md +105 -0
- package/dist/docs/clusters.md +48 -0
- package/dist/docs/commands.md +155 -0
- package/dist/docs/files.md +86 -0
- package/dist/docs/generators.md +52 -0
- package/dist/docs/getting-started.md +85 -0
- package/dist/docs/index.json +110 -0
- package/dist/docs/projects.md +50 -0
- package/dist/docs/recorder.md +143 -0
- package/dist/docs/results.md +79 -0
- package/dist/docs/running.md +80 -0
- package/dist/docs/skills.md +88 -0
- package/dist/docs/test-plans.md +92 -0
- package/dist/docs/troubleshooting.md +79 -0
- package/dist/plan.js +250 -0
- package/dist/skills/AGENTS.md +70 -0
- package/dist/skills/atlas-analyze/SKILL.md +104 -0
- package/dist/skills/atlas-correlate/SKILL.md +89 -0
- package/dist/skills/atlas-load-test/SKILL.md +154 -0
- package/dist/skills/atlas-record/SKILL.md +167 -0
- package/dist/skills/atlas-run/SKILL.md +109 -0
- package/dist/skills/atlas-test-plan/SKILL.md +281 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -43,15 +43,15 @@ whenever they change — this is independent of running a test, and uploaded
|
|
|
43
43
|
files stay in the project until you replace or delete them.
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
atlas files upload -p
|
|
47
|
-
atlas files list -p
|
|
46
|
+
atlas files upload -p SHOP ./checkout.jmx ./data/users.csv ./data/products.csv
|
|
47
|
+
atlas files list -p SHOP
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
Uploading a file with a name that already exists replaces it.
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
atlas files download -p
|
|
54
|
-
atlas files rm -p
|
|
53
|
+
atlas files download -p SHOP checkout.jmx # fetch a copy back
|
|
54
|
+
atlas files rm -p SHOP old-data.csv # delete one
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
### How files are laid out — and how to reference them
|
|
@@ -64,16 +64,16 @@ generator.
|
|
|
64
64
|
So in your test plan, reference data files by a plain relative path:
|
|
65
65
|
|
|
66
66
|
```
|
|
67
|
-
./
|
|
68
|
-
|
|
67
|
+
./users.csv ✅ recommended
|
|
68
|
+
users.csv ✅ also works
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
Not by an absolute path, and not through a folder:
|
|
72
72
|
|
|
73
73
|
```
|
|
74
|
-
C:\Users\me\Data\
|
|
75
|
-
/home/me/data/
|
|
76
|
-
./data/
|
|
74
|
+
C:\Users\me\Data\users.csv ❌ a Windows path cannot be found on a Linux generator
|
|
75
|
+
/home/me/data/users.csv ❌ absolute paths won't exist there either
|
|
76
|
+
./data/users.csv ❌ the folder structure is not preserved
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
This catches people out most often with test plans authored in JMeter on
|
|
@@ -93,7 +93,7 @@ Two related things worth checking before your first run:
|
|
|
93
93
|
## Running a test
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
|
-
atlas run -p
|
|
96
|
+
atlas run -p SHOP --jmx checkout.jmx --servers 5 --wait
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
`--wait` follows the run to completion and prints the aggregation report.
|
|
@@ -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
|
|
105
|
-
atlas results
|
|
106
|
-
atlas stop
|
|
104
|
+
atlas status SHOP-20260818-194201
|
|
105
|
+
atlas results SHOP-20260818-194201
|
|
106
|
+
atlas stop SHOP-20260818-194201
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
`--jmx` is only needed when a project holds more than one test plan.
|
|
@@ -139,9 +139,9 @@ export ATLAS_URL=https://atlas.cloudbeat.io
|
|
|
139
139
|
export ATLAS_TOKEN=atlas_pat_...
|
|
140
140
|
|
|
141
141
|
# Only if the plan or its data live in this repository and may have changed.
|
|
142
|
-
atlas files upload -p
|
|
142
|
+
atlas files upload -p SHOP ./checkout.jmx ./data/users.csv
|
|
143
143
|
|
|
144
|
-
atlas run -p
|
|
144
|
+
atlas run -p SHOP --jmx checkout.jmx --servers 5 \
|
|
145
145
|
--gate "error_rate<1,p95<2000" \
|
|
146
146
|
--junit results.xml
|
|
147
147
|
```
|
|
@@ -191,7 +191,7 @@ plan:
|
|
|
191
191
|
atlas record https://example.com -o flow.jmx
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
-
Add `--upload -p
|
|
194
|
+
Add `--upload -p SHOP` to put the finished plan straight into a project, ready
|
|
195
195
|
to run.
|
|
196
196
|
|
|
197
197
|
| | |
|
|
@@ -221,11 +221,38 @@ it if it isn't present.
|
|
|
221
221
|
| `atlas status <testId>` | check whether a test is still running |
|
|
222
222
|
| `atlas results <testId>` | show the aggregation report for a test |
|
|
223
223
|
| `atlas stop <testId>` | stop a running test |
|
|
224
|
+
| `atlas tests` | list a project's recent runs |
|
|
225
|
+
| `atlas timeseries <testId>` | active users, response time and error rate over time |
|
|
226
|
+
| `atlas errors <testId>` | the distinct errors a test produced |
|
|
227
|
+
| `atlas capacity` | what you can run on: machines, clusters and usage limits |
|
|
228
|
+
| `atlas plan check [file]` | check a test plan will run and report before you use it |
|
|
224
229
|
| `atlas record [url]` | record a browser session |
|
|
230
|
+
| `atlas schema` | describe every command and option (for scripts and AI agents) |
|
|
231
|
+
| `atlas skills install` | set up an AI assistant to drive Atlas properly |
|
|
232
|
+
| `atlas docs [topic]` | read the documentation without leaving the terminal |
|
|
225
233
|
| `atlas config get` / `set` | default instance URL and project |
|
|
226
234
|
|
|
227
|
-
|
|
228
|
-
`atlas <command> --help` for the full list of options.
|
|
235
|
+
Every command takes `--json`, and `-p, --project <name>` where a project
|
|
236
|
+
applies. Run `atlas <command> --help` for the full list of options.
|
|
237
|
+
|
|
238
|
+
When a command fails with `--json`, the error is printed as JSON on standard
|
|
239
|
+
error, so a script can read clean output on standard out and still see why a
|
|
240
|
+
run stopped.
|
|
241
|
+
|
|
242
|
+
## Working with an AI assistant
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
atlas skills install
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Writes task-specific guidance into the current project — `.claude/skills/` for
|
|
249
|
+
Claude Code, and `AGENTS.md` for Copilot, Cursor and anything else that reads it
|
|
250
|
+
— covering the end-to-end flow, recording, dynamic values, sizing a run and
|
|
251
|
+
reading results. `--user` installs for every project, `--force` replaces files
|
|
252
|
+
you have edited. Start a new assistant session afterwards.
|
|
253
|
+
|
|
254
|
+
`atlas schema --json` describes the whole command surface, and `atlas docs`
|
|
255
|
+
prints the documentation without a network connection.
|
|
229
256
|
|
|
230
257
|
## Settings
|
|
231
258
|
|
|
@@ -242,7 +269,7 @@ configuration file.
|
|
|
242
269
|
To avoid repeating yourself:
|
|
243
270
|
|
|
244
271
|
```bash
|
|
245
|
-
atlas config set project
|
|
272
|
+
atlas config set project SHOP
|
|
246
273
|
atlas config set url https://atlas.cloudbeat.io
|
|
247
274
|
```
|
|
248
275
|
|
package/dist/api.js
CHANGED
|
@@ -169,6 +169,12 @@ class AtlasApi {
|
|
|
169
169
|
async clusters() {
|
|
170
170
|
return this.request('GET', `${this.api}/clusters`);
|
|
171
171
|
}
|
|
172
|
+
async quota() {
|
|
173
|
+
return this.request('GET', `${this.api}/auth/quota`);
|
|
174
|
+
}
|
|
175
|
+
async fleet() {
|
|
176
|
+
return this.request('GET', `${this.api}/auth/fleet`);
|
|
177
|
+
}
|
|
172
178
|
// ── runs ──────────────────────────────────────────────────────────────────
|
|
173
179
|
async run(projectId, body) {
|
|
174
180
|
return this.request('POST', `${this.api}/projects/${projectId}/run`, { body });
|
|
@@ -186,6 +192,20 @@ class AtlasApi {
|
|
|
186
192
|
async testStatus(testId) {
|
|
187
193
|
return this.request('GET', `/api/tests/${encodeURIComponent(testId)}/status`, { auth: false });
|
|
188
194
|
}
|
|
195
|
+
/** Runs for a project, most recent first (the dashboard's test list). */
|
|
196
|
+
async tests(project) {
|
|
197
|
+
return this.request('GET', `/api/projects/${encodeURIComponent(project)}/tests`, { auth: false });
|
|
198
|
+
}
|
|
199
|
+
/** 30s-bucketed points: active users, average response time, error rate. */
|
|
200
|
+
async timeseries(testId, label) {
|
|
201
|
+
const q = label ? `?label=${encodeURIComponent(label)}` : '';
|
|
202
|
+
return this.request('GET', `/api/tests/${encodeURIComponent(testId)}/timeseries${q}`, { auth: false });
|
|
203
|
+
}
|
|
204
|
+
/** One document per distinct error type, with the full request/response. */
|
|
205
|
+
async errors(testId, label) {
|
|
206
|
+
const q = label ? `?label=${encodeURIComponent(label)}` : '';
|
|
207
|
+
return this.request('GET', `/api/tests/${encodeURIComponent(testId)}/errors${q}`, { auth: false });
|
|
208
|
+
}
|
|
189
209
|
}
|
|
190
210
|
exports.AtlasApi = AtlasApi;
|
|
191
211
|
//# sourceMappingURL=api.js.map
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,13 @@ const files_1 = require("./commands/files");
|
|
|
14
14
|
const run_1 = require("./commands/run");
|
|
15
15
|
const results_1 = require("./commands/results");
|
|
16
16
|
const record_1 = require("./commands/record");
|
|
17
|
+
const tests_1 = require("./commands/tests");
|
|
18
|
+
const analyze_1 = require("./commands/analyze");
|
|
19
|
+
const capacity_1 = require("./commands/capacity");
|
|
20
|
+
const schema_1 = require("./commands/schema");
|
|
21
|
+
const docs_1 = require("./commands/docs");
|
|
22
|
+
const skills_1 = require("./commands/skills");
|
|
23
|
+
const plan_1 = require("./commands/plan");
|
|
17
24
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
18
25
|
const { version } = require('../package.json');
|
|
19
26
|
const program = new commander_1.Command();
|
|
@@ -37,13 +44,26 @@ Examples:
|
|
|
37
44
|
atlas run -p SKT --jmx SKT-2026.jmx --servers 5 --wait \\
|
|
38
45
|
--gate "error_rate<1,p95<2000" --junit results.xml
|
|
39
46
|
atlas record https://example.com -o flow.jmx --upload -p SKT
|
|
47
|
+
|
|
48
|
+
For scripts and AI agents:
|
|
49
|
+
atlas schema --json every command, argument and option
|
|
50
|
+
atlas docs the documentation, offline
|
|
51
|
+
atlas skills install teach an AI assistant to drive Atlas properly
|
|
52
|
+
--json on every command; failures print JSON on stderr
|
|
40
53
|
`);
|
|
41
54
|
(0, auth_1.registerAuth)(program);
|
|
42
55
|
(0, projects_1.registerProjects)(program);
|
|
43
56
|
(0, files_1.registerFiles)(program);
|
|
44
57
|
(0, run_1.registerRun)(program);
|
|
45
58
|
(0, results_1.registerResults)(program);
|
|
59
|
+
(0, tests_1.registerTests)(program);
|
|
60
|
+
(0, analyze_1.registerAnalyze)(program);
|
|
61
|
+
(0, capacity_1.registerCapacity)(program);
|
|
62
|
+
(0, plan_1.registerPlan)(program);
|
|
46
63
|
(0, record_1.registerRecord)(program);
|
|
64
|
+
(0, schema_1.registerSchema)(program);
|
|
65
|
+
(0, docs_1.registerDocs)(program);
|
|
66
|
+
(0, skills_1.registerSkills)(program);
|
|
47
67
|
// Small config helper so CI/dev can pin a default project or URL without env.
|
|
48
68
|
const config = program.command('config').description('Read or write CLI settings');
|
|
49
69
|
config
|
|
@@ -69,15 +89,23 @@ config
|
|
|
69
89
|
async function main() {
|
|
70
90
|
await program.parseAsync(process.argv);
|
|
71
91
|
}
|
|
92
|
+
/** A caller that asked for JSON gets JSON on the way out too — on stderr, so a
|
|
93
|
+
* pipeline parsing stdout never has to tell output from failure. */
|
|
94
|
+
function failed(message, code) {
|
|
95
|
+
if (process.argv.includes('--json')) {
|
|
96
|
+
console.error(JSON.stringify({ error: { message, code } }, null, 2));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
ui_1.log.error(message);
|
|
100
|
+
}
|
|
101
|
+
process.exit(code);
|
|
102
|
+
}
|
|
72
103
|
main().catch((err) => {
|
|
73
104
|
// Expected failures print a message; anything else is a bug and keeps its stack.
|
|
74
|
-
if (err instanceof ui_1.CliError)
|
|
75
|
-
|
|
76
|
-
process.exit(err.code);
|
|
77
|
-
}
|
|
78
|
-
ui_1.log.error(err.message ?? String(err));
|
|
105
|
+
if (err instanceof ui_1.CliError)
|
|
106
|
+
failed(err.message, err.code);
|
|
79
107
|
if (process.env.ATLAS_DEBUG)
|
|
80
108
|
console.error(err);
|
|
81
|
-
|
|
109
|
+
failed(err.message ?? String(err), ui_1.EXIT.ERROR);
|
|
82
110
|
});
|
|
83
111
|
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerAnalyze = registerAnalyze;
|
|
7
|
+
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const api_1 = require("../api");
|
|
10
|
+
const ui_1 = require("../ui");
|
|
11
|
+
/** Rows a human can scan; --json always carries every point. */
|
|
12
|
+
const MAX_HUMAN_ROWS = 24;
|
|
13
|
+
/** Keep the first, the last and an even spread between — a run is 175 buckets
|
|
14
|
+
* at 30s, and the shape is what a person is reading the table for. */
|
|
15
|
+
function sample(items, max) {
|
|
16
|
+
if (items.length <= max)
|
|
17
|
+
return items;
|
|
18
|
+
const step = (items.length - 1) / (max - 1);
|
|
19
|
+
return Array.from({ length: max }, (_, i) => items[Math.round(i * step)]);
|
|
20
|
+
}
|
|
21
|
+
function clock(iso) {
|
|
22
|
+
return new Date(iso).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
23
|
+
}
|
|
24
|
+
function printPoints(points) {
|
|
25
|
+
const table = new cli_table3_1.default({
|
|
26
|
+
head: ['TIME', 'USERS', 'AVG MS', 'ERR%'].map(h => chalk_1.default.dim(h)),
|
|
27
|
+
colAligns: ['left', 'right', 'right', 'right'],
|
|
28
|
+
style: { head: [], border: [] },
|
|
29
|
+
});
|
|
30
|
+
for (const p of sample(points, MAX_HUMAN_ROWS)) {
|
|
31
|
+
table.push([
|
|
32
|
+
clock(p.timestamp),
|
|
33
|
+
String(p.activeUsers),
|
|
34
|
+
String(p.avgMs),
|
|
35
|
+
p.errorRate === 0 ? chalk_1.default.green('0%') : chalk_1.default.red(`${p.errorRate}%`),
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
console.log(table.toString());
|
|
39
|
+
}
|
|
40
|
+
function truncate(s, n) {
|
|
41
|
+
const one = (s ?? '').replace(/\s+/g, ' ').trim();
|
|
42
|
+
return one.length > n ? one.slice(0, n - 1) + '…' : one;
|
|
43
|
+
}
|
|
44
|
+
function printErrors(errors) {
|
|
45
|
+
const table = new cli_table3_1.default({
|
|
46
|
+
head: ['CODE', 'COUNT', 'SHARE', 'LABEL', 'MESSAGE'].map(h => chalk_1.default.dim(h)),
|
|
47
|
+
colAligns: ['left', 'right', 'right', 'left', 'left'],
|
|
48
|
+
style: { head: [], border: [] },
|
|
49
|
+
wordWrap: false,
|
|
50
|
+
});
|
|
51
|
+
for (const e of errors) {
|
|
52
|
+
table.push([
|
|
53
|
+
truncate(e.responseCode, 24) || chalk_1.default.dim('—'),
|
|
54
|
+
String(e.count),
|
|
55
|
+
`${e.errorPct}%`,
|
|
56
|
+
truncate(e.label, 32),
|
|
57
|
+
truncate(e.failureMessage, 60),
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
console.log(table.toString());
|
|
61
|
+
}
|
|
62
|
+
function registerAnalyze(program) {
|
|
63
|
+
program
|
|
64
|
+
.command('timeseries <testId>')
|
|
65
|
+
.description('Active users, response time and error rate in 30s buckets')
|
|
66
|
+
.option('--label <label>', 'one sampler/transaction instead of the whole test')
|
|
67
|
+
.option('--json', 'machine-readable output — every point, not a sample')
|
|
68
|
+
.action(async (testId, opts) => {
|
|
69
|
+
const api = api_1.AtlasApi.fromConfig();
|
|
70
|
+
const { points } = await api.timeseries(testId, opts.label);
|
|
71
|
+
if (opts.json) {
|
|
72
|
+
(0, ui_1.json)({ testId, label: opts.label ?? '_all', points });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (points.length === 0) {
|
|
76
|
+
throw new ui_1.CliError(`No metrics found for "${testId}".`, ui_1.EXIT.ERROR);
|
|
77
|
+
}
|
|
78
|
+
const peakUsers = Math.max(...points.map(p => p.activeUsers));
|
|
79
|
+
const peakAvg = Math.max(...points.map(p => p.avgMs));
|
|
80
|
+
printPoints(points);
|
|
81
|
+
if (points.length > MAX_HUMAN_ROWS) {
|
|
82
|
+
ui_1.log.dim(`${points.length} points, ${MAX_HUMAN_ROWS} shown — use --json for all of them.`);
|
|
83
|
+
}
|
|
84
|
+
ui_1.log.info(`peak users ${chalk_1.default.bold(String(peakUsers))} · peak avg ${chalk_1.default.bold(`${peakAvg}ms`)}`);
|
|
85
|
+
});
|
|
86
|
+
program
|
|
87
|
+
.command('errors <testId>')
|
|
88
|
+
.description('Distinct error types for a run, with request and response detail')
|
|
89
|
+
.option('--label <label>', 'errors for one sampler/transaction only')
|
|
90
|
+
.option('--full', 'print the full request/response of each error')
|
|
91
|
+
.option('--json', 'machine-readable output')
|
|
92
|
+
.action(async (testId, opts) => {
|
|
93
|
+
const api = api_1.AtlasApi.fromConfig();
|
|
94
|
+
const { errors, totalErrors } = await api.errors(testId, opts.label);
|
|
95
|
+
if (opts.json) {
|
|
96
|
+
(0, ui_1.json)({ testId, label: opts.label ?? '_all', totalErrors, errors });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (errors.length === 0) {
|
|
100
|
+
ui_1.log.ok(`No errors recorded for ${testId}${opts.label ? ` on "${opts.label}"` : ''}.`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
printErrors(errors);
|
|
104
|
+
ui_1.log.info(`${errors.length} distinct error type(s), ${totalErrors} occurrence(s).`);
|
|
105
|
+
if (opts.full) {
|
|
106
|
+
for (const e of errors) {
|
|
107
|
+
console.log();
|
|
108
|
+
ui_1.log.info(chalk_1.default.bold(`${e.responseCode} ${e.label}`));
|
|
109
|
+
ui_1.log.dim(e.requestUrl);
|
|
110
|
+
if (e.requestBody)
|
|
111
|
+
ui_1.log.dim(`request: ${truncate(e.requestBody, 500)}`);
|
|
112
|
+
if (e.responseBody)
|
|
113
|
+
ui_1.log.dim(`response: ${truncate(e.responseBody, 500)}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=analyze.js.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerCapacity = registerCapacity;
|
|
7
|
+
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const api_1 = require("../api");
|
|
10
|
+
const ui_1 = require("../ui");
|
|
11
|
+
/** An agent sizing a run needs to know what it is allowed to ask for before it
|
|
12
|
+
* asks the user. This is fleet + clusters + caps + quota in one call. */
|
|
13
|
+
function printServers(servers) {
|
|
14
|
+
const table = new cli_table3_1.default({
|
|
15
|
+
head: ['SERVER', 'STATUS', 'PROVIDER', 'BUSY'].map(h => chalk_1.default.dim(h)),
|
|
16
|
+
style: { head: [], border: [] },
|
|
17
|
+
});
|
|
18
|
+
for (const s of servers) {
|
|
19
|
+
table.push([
|
|
20
|
+
s.name,
|
|
21
|
+
s.status === 'on' ? chalk_1.default.green('on') : s.status === 'off' ? chalk_1.default.dim('off') : chalk_1.default.yellow('unknown'),
|
|
22
|
+
s.provider,
|
|
23
|
+
s.jmeter_running ? chalk_1.default.yellow('running a test') : chalk_1.default.dim('free'),
|
|
24
|
+
]);
|
|
25
|
+
}
|
|
26
|
+
console.log(table.toString());
|
|
27
|
+
}
|
|
28
|
+
function printClusters(clusters) {
|
|
29
|
+
const table = new cli_table3_1.default({
|
|
30
|
+
head: ['CLUSTER', 'TYPE', 'CONTROLLERS'].map(h => chalk_1.default.dim(h)),
|
|
31
|
+
style: { head: [], border: [] },
|
|
32
|
+
});
|
|
33
|
+
for (const c of clusters) {
|
|
34
|
+
const connected = `${c.connectedControllers}/${c.controllers}`;
|
|
35
|
+
table.push([
|
|
36
|
+
c.name,
|
|
37
|
+
c.provider_type,
|
|
38
|
+
c.connectedControllers > 0 ? chalk_1.default.green(connected) : chalk_1.default.red(connected),
|
|
39
|
+
]);
|
|
40
|
+
}
|
|
41
|
+
console.log(table.toString());
|
|
42
|
+
}
|
|
43
|
+
function registerCapacity(program) {
|
|
44
|
+
program
|
|
45
|
+
.command('capacity')
|
|
46
|
+
.description('What this credential can run on: servers, clusters, fleet caps and quota')
|
|
47
|
+
.option('--json', 'machine-readable output')
|
|
48
|
+
.action(async (opts) => {
|
|
49
|
+
const api = api_1.AtlasApi.fromConfig();
|
|
50
|
+
// Only servers are essential. Caps and quota are newer than the fleet
|
|
51
|
+
// endpoints, and listing clusters needs account-admin rights an operator
|
|
52
|
+
// does not have (403) — none of that should stop the command answering
|
|
53
|
+
// the main question, which is what this credential can run on.
|
|
54
|
+
const optional = async (call) => call.catch(() => null);
|
|
55
|
+
const [servers, clusterList, fleet, quota] = await Promise.all([
|
|
56
|
+
api.servers(),
|
|
57
|
+
optional(api.clusters()),
|
|
58
|
+
optional(api.fleet()),
|
|
59
|
+
optional(api.quota()),
|
|
60
|
+
]);
|
|
61
|
+
const clusters = clusterList ?? [];
|
|
62
|
+
const free = servers.filter(s => !s.jmeter_running);
|
|
63
|
+
if (opts.json) {
|
|
64
|
+
(0, ui_1.json)({
|
|
65
|
+
servers,
|
|
66
|
+
clusters,
|
|
67
|
+
clustersVisible: clusterList !== null,
|
|
68
|
+
fleet,
|
|
69
|
+
quota,
|
|
70
|
+
available: { servers: free.length, of: servers.length },
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (servers.length) {
|
|
75
|
+
printServers(servers);
|
|
76
|
+
ui_1.log.info(`${free.length} of ${servers.length} server(s) free.`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
ui_1.log.warn('No fixed servers available to this credential.');
|
|
80
|
+
}
|
|
81
|
+
if (clusters.length) {
|
|
82
|
+
console.log();
|
|
83
|
+
printClusters(clusters);
|
|
84
|
+
ui_1.log.dim('Clusters allocate containers per run — size them with `atlas run --cluster <name> --units <n>`.');
|
|
85
|
+
}
|
|
86
|
+
if (fleet) {
|
|
87
|
+
console.log();
|
|
88
|
+
const cap = (used, max) => (max == null ? `${used} (no cap)` : `${used} of ${max}`);
|
|
89
|
+
ui_1.log.info(`nodes ${cap(fleet.nodes, fleet.maxNodes)} · concurrent agents ${cap(fleet.agents, fleet.maxAgents)}`);
|
|
90
|
+
}
|
|
91
|
+
if (quota?.limited) {
|
|
92
|
+
const used = quota.usedHours ?? 0;
|
|
93
|
+
const limit = quota.limitHours ?? 0;
|
|
94
|
+
const line = `usage ${used}h of ${limit}h${quota.pct != null ? ` (${quota.pct}%)` : ''}`;
|
|
95
|
+
if (quota.blocked)
|
|
96
|
+
ui_1.log.error(`${line} — runs are blocked until the limit is raised or the period resets.`);
|
|
97
|
+
else if ((quota.pct ?? 0) >= 80)
|
|
98
|
+
ui_1.log.warn(line);
|
|
99
|
+
else
|
|
100
|
+
ui_1.log.info(line);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=capacity.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerDocs = registerDocs;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const ui_1 = require("../ui");
|
|
11
|
+
/** Written by scripts/bundle-docs.js at build time. */
|
|
12
|
+
const DOCS_DIR = path_1.default.join(__dirname, '..', 'docs');
|
|
13
|
+
function loadIndex() {
|
|
14
|
+
const file = path_1.default.join(DOCS_DIR, 'index.json');
|
|
15
|
+
if (!fs_1.default.existsSync(file))
|
|
16
|
+
return [];
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function registerDocs(program) {
|
|
25
|
+
program
|
|
26
|
+
.command('docs [topic]')
|
|
27
|
+
.description('Print the documentation for a topic (offline); no topic lists them')
|
|
28
|
+
.option('--json', 'machine-readable output')
|
|
29
|
+
.action((topic, opts) => {
|
|
30
|
+
const index = loadIndex();
|
|
31
|
+
if (index.length === 0) {
|
|
32
|
+
throw new ui_1.CliError('No documentation is bundled with this build. Read it at https://atlas.cloudbeat.io/docs', ui_1.EXIT.ERROR);
|
|
33
|
+
}
|
|
34
|
+
if (!topic) {
|
|
35
|
+
if (opts.json) {
|
|
36
|
+
(0, ui_1.json)({ topics: index });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
ui_1.log.info(chalk_1.default.bold('Topics'));
|
|
40
|
+
for (const t of index) {
|
|
41
|
+
console.log(` ${chalk_1.default.cyan(t.topic.padEnd(16))}${t.summary || t.title}`);
|
|
42
|
+
}
|
|
43
|
+
ui_1.log.dim('atlas docs <topic> · https://atlas.cloudbeat.io/docs');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const hit = index.find(t => t.topic === topic.toLowerCase());
|
|
47
|
+
if (!hit) {
|
|
48
|
+
throw new ui_1.CliError(`No topic "${topic}". Known: ${index.map(t => t.topic).join(', ')}`, ui_1.EXIT.USAGE);
|
|
49
|
+
}
|
|
50
|
+
const body = fs_1.default.readFileSync(path_1.default.join(DOCS_DIR, `${hit.topic}.md`), 'utf8');
|
|
51
|
+
if (opts.json)
|
|
52
|
+
(0, ui_1.json)({ topic: hit.topic, title: hit.title, markdown: body });
|
|
53
|
+
else
|
|
54
|
+
console.log(body);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=docs.js.map
|
package/dist/commands/files.js
CHANGED
|
@@ -96,6 +96,7 @@ function registerFiles(program) {
|
|
|
96
96
|
.command('upload <files...>')
|
|
97
97
|
.description('Upload files into the project (replaces same-named files)')
|
|
98
98
|
.option('-p, --project <name>', 'project name')
|
|
99
|
+
.option('--json', 'machine-readable output')
|
|
99
100
|
.action(async (paths, opts) => {
|
|
100
101
|
const api = api_1.AtlasApi.fromConfig();
|
|
101
102
|
const project = await api.projectByName(projectName(opts.project));
|
|
@@ -103,8 +104,13 @@ function registerFiles(program) {
|
|
|
103
104
|
if (missing.length)
|
|
104
105
|
throw new ui_1.CliError(`File(s) not found: ${missing.join(', ')}`, ui_1.EXIT.USAGE);
|
|
105
106
|
await api.uploadFiles(project.id, paths);
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
const uploaded = paths.map(p => ({ filename: path_1.default.basename(p), bytes: fs_1.default.statSync(p).size }));
|
|
108
|
+
if (opts.json) {
|
|
109
|
+
(0, ui_1.json)({ project: project.name, uploaded });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
for (const f of uploaded) {
|
|
113
|
+
ui_1.log.ok(`${f.filename} → ${project.name} (${humanSize(f.bytes)})`);
|
|
108
114
|
}
|
|
109
115
|
});
|
|
110
116
|
files
|
|
@@ -112,19 +118,24 @@ function registerFiles(program) {
|
|
|
112
118
|
.description('Download one file from the project')
|
|
113
119
|
.option('-p, --project <name>', 'project name')
|
|
114
120
|
.option('-o, --out <path>', 'where to write it (default: the filename, in cwd)')
|
|
121
|
+
.option('--json', 'machine-readable output')
|
|
115
122
|
.action(async (filename, opts) => {
|
|
116
123
|
const api = api_1.AtlasApi.fromConfig();
|
|
117
124
|
const project = await api.projectByName(projectName(opts.project));
|
|
118
125
|
const buf = await api.downloadFile(project.id, filename);
|
|
119
126
|
const dest = opts.out ?? filename;
|
|
120
127
|
fs_1.default.writeFileSync(dest, buf);
|
|
121
|
-
|
|
128
|
+
if (opts.json)
|
|
129
|
+
(0, ui_1.json)({ project: project.name, filename, path: path_1.default.resolve(dest), bytes: buf.length });
|
|
130
|
+
else
|
|
131
|
+
ui_1.log.ok(`${filename} → ${dest} (${humanSize(buf.length)})`);
|
|
122
132
|
});
|
|
123
133
|
files
|
|
124
134
|
.command('rm <filename>')
|
|
125
135
|
.description('Delete a file from the project')
|
|
126
136
|
.option('-p, --project <name>', 'project name')
|
|
127
137
|
.option('-y, --yes', 'skip the confirmation prompt')
|
|
138
|
+
.option('--json', 'machine-readable output')
|
|
128
139
|
.action(async (filename, opts) => {
|
|
129
140
|
const api = api_1.AtlasApi.fromConfig();
|
|
130
141
|
const project = await api.projectByName(projectName(opts.project));
|
|
@@ -143,7 +154,10 @@ function registerFiles(program) {
|
|
|
143
154
|
}
|
|
144
155
|
}
|
|
145
156
|
await api.deleteFile(project.id, filename);
|
|
146
|
-
|
|
157
|
+
if (opts.json)
|
|
158
|
+
(0, ui_1.json)({ project: project.name, deleted: filename });
|
|
159
|
+
else
|
|
160
|
+
ui_1.log.ok(`Deleted ${filename} from ${project.name}`);
|
|
147
161
|
});
|
|
148
162
|
}
|
|
149
163
|
//# sourceMappingURL=files.js.map
|