@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.
Files changed (41) hide show
  1. package/README.md +46 -19
  2. package/dist/api.js +20 -0
  3. package/dist/cli.js +34 -6
  4. package/dist/commands/analyze.js +118 -0
  5. package/dist/commands/capacity.js +104 -0
  6. package/dist/commands/docs.js +57 -0
  7. package/dist/commands/files.js +18 -4
  8. package/dist/commands/plan.js +100 -0
  9. package/dist/commands/record.js +11 -1
  10. package/dist/commands/results.js +13 -7
  11. package/dist/commands/schema.js +86 -0
  12. package/dist/commands/skills.js +111 -0
  13. package/dist/commands/tests.js +73 -0
  14. package/dist/docs/agent-setup.md +109 -0
  15. package/dist/docs/agent-tasks.md +169 -0
  16. package/dist/docs/ai-agents.md +90 -0
  17. package/dist/docs/auth.md +74 -0
  18. package/dist/docs/automation.md +150 -0
  19. package/dist/docs/ci.md +105 -0
  20. package/dist/docs/clusters.md +48 -0
  21. package/dist/docs/commands.md +155 -0
  22. package/dist/docs/files.md +86 -0
  23. package/dist/docs/generators.md +52 -0
  24. package/dist/docs/getting-started.md +85 -0
  25. package/dist/docs/index.json +110 -0
  26. package/dist/docs/projects.md +50 -0
  27. package/dist/docs/recorder.md +143 -0
  28. package/dist/docs/results.md +79 -0
  29. package/dist/docs/running.md +80 -0
  30. package/dist/docs/skills.md +88 -0
  31. package/dist/docs/test-plans.md +92 -0
  32. package/dist/docs/troubleshooting.md +79 -0
  33. package/dist/plan.js +250 -0
  34. package/dist/skills/AGENTS.md +70 -0
  35. package/dist/skills/atlas-analyze/SKILL.md +104 -0
  36. package/dist/skills/atlas-correlate/SKILL.md +89 -0
  37. package/dist/skills/atlas-load-test/SKILL.md +154 -0
  38. package/dist/skills/atlas-record/SKILL.md +167 -0
  39. package/dist/skills/atlas-run/SKILL.md +109 -0
  40. package/dist/skills/atlas-test-plan/SKILL.md +281 -0
  41. 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 SKT ./SKT-2026.jmx ./data/mosdot.csv ./data/rashut.csv
47
- atlas files list -p SKT
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 SKT SKT-2026.jmx # fetch a copy back
54
- atlas files rm -p SKT old-data.csv # delete one
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
- ./mosdot.csv ✅ recommended
68
- mosdot.csv ✅ also works
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\mosdot.csv ❌ a Windows path cannot be found on a Linux generator
75
- /home/me/data/mosdot.csv ❌ absolute paths won't exist there either
76
- ./data/mosdot.csv ❌ the folder structure is not preserved
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 SKT --jmx SKT-2026.jmx --servers 5 --wait
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 SKT-20260818-194201
105
- atlas results SKT-20260818-194201
106
- atlas stop SKT-20260818-194201
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 SKT ./SKT-2026.jmx ./data/mosdot.csv
142
+ atlas files upload -p SHOP ./checkout.jmx ./data/users.csv
143
143
 
144
- atlas run -p SKT --jmx SKT-2026.jmx --servers 5 \
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 SKT` to put the finished plan straight into a project, ready
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
- Most commands take `-p, --project <name>` and `--json`. Run
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 SKT
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
- ui_1.log.error(err.message);
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
- process.exit(ui_1.EXIT.ERROR);
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
@@ -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
- for (const p of paths) {
107
- ui_1.log.ok(`${path_1.default.basename(p)} → ${project.name} (${humanSize(fs_1.default.statSync(p).size)})`);
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
- ui_1.log.ok(`${filename} → ${dest} (${humanSize(buf.length)})`);
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
- ui_1.log.ok(`Deleted ${filename} from ${project.name}`);
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