@cloudbeat/atlas-cli 0.1.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 ADDED
@@ -0,0 +1,247 @@
1
+ # @cloudbeat/atlas-cli
2
+
3
+ Run Atlas load tests from your terminal or a CI pipeline, and record browser
4
+ sessions into JMeter test plans.
5
+
6
+ ```bash
7
+ npm install -g @cloudbeat/atlas-cli
8
+ atlas --help
9
+ ```
10
+
11
+ Requires Node.js 18 or newer.
12
+
13
+ ## Getting started
14
+
15
+ Point the CLI at your Atlas instance and sign in.
16
+
17
+ For everyday use:
18
+
19
+ ```bash
20
+ atlas login --url https://atlas.cloudbeat.io -u you@company.com
21
+ ```
22
+
23
+ You'll be prompted for your password. The session lasts 8 hours.
24
+
25
+ For CI, create an **API token** on the Profile page of your Atlas instance and
26
+ use that instead — it doesn't expire on its own and can be revoked at any time:
27
+
28
+ ```bash
29
+ atlas login --token atlas_pat_...
30
+ ```
31
+
32
+ Credentials are stored in `~/.atlas/config.json`, readable only by you.
33
+
34
+ An API token acts as the person who created it and has the same permissions.
35
+ It cannot be used to create more tokens or to change a password, so it is safe
36
+ to hand to a build pipeline. Revoking it on the Profile page stops it working
37
+ immediately.
38
+
39
+ ## Uploading files
40
+
41
+ A project holds your test plan and any data files it reads. Upload them
42
+ whenever they change — this is independent of running a test, and uploaded
43
+ files stay in the project until you replace or delete them.
44
+
45
+ ```bash
46
+ atlas files upload -p SKT ./SKT-2026.jmx ./data/mosdot.csv ./data/rashut.csv
47
+ atlas files list -p SKT
48
+ ```
49
+
50
+ Uploading a file with a name that already exists replaces it.
51
+
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
55
+ ```
56
+
57
+ ### How files are laid out — and how to reference them
58
+
59
+ **All of a project's files live together in one flat folder.** There are no
60
+ subdirectories: whatever you upload lands side by side, and when a test runs,
61
+ the test plan and its data files sit in the same directory on every load
62
+ generator.
63
+
64
+ So in your test plan, reference data files by a plain relative path:
65
+
66
+ ```
67
+ ./mosdot.csv ✅ recommended
68
+ mosdot.csv ✅ also works
69
+ ```
70
+
71
+ Not by an absolute path, and not through a folder:
72
+
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
77
+ ```
78
+
79
+ This catches people out most often with test plans authored in JMeter on
80
+ Windows, where **CSV Data Set Config** entries usually hold a full `C:\…` path.
81
+ Open each one and reduce it to `./filename`. If a path can't be resolved at run
82
+ time, every thread in that thread group stops immediately — the test appears to
83
+ run but that group reports no results at all.
84
+
85
+ Two related things worth checking before your first run:
86
+
87
+ - Every file the plan references must actually be uploaded. Upload the plan and
88
+ its data files together.
89
+ - Disable GUI listeners left switched on in the plan — **View Results Tree**,
90
+ **Aggregate Report**, **Simple Data Writer**. They consume a lot of memory
91
+ during a headless run, and Atlas collects its results separately.
92
+
93
+ ## Running a test
94
+
95
+ ```bash
96
+ atlas run -p SKT --jmx SKT-2026.jmx --servers 5 --wait
97
+ ```
98
+
99
+ `--wait` follows the run to completion and prints the aggregation report.
100
+ Without it, the command returns as soon as the test has been submitted, and you
101
+ can check on it later:
102
+
103
+ ```bash
104
+ atlas status SKT-20260818-194201
105
+ atlas results SKT-20260818-194201
106
+ atlas stop SKT-20260818-194201
107
+ ```
108
+
109
+ `--jmx` is only needed when a project holds more than one test plan.
110
+
111
+ Load generators that are switched off are started for you and shut down again
112
+ when the test finishes.
113
+
114
+ ### Choosing where it runs
115
+
116
+ | | |
117
+ |---|---|
118
+ | `--servers 5` | use any 5 available load generators |
119
+ | `--server gen-01 --server gen-02` | pick specific ones by name |
120
+ | `--cluster my-cluster --units 10` | run on 10 containers in an elastic cluster |
121
+
122
+ ### Other run options
123
+
124
+ | | |
125
+ |---|---|
126
+ | `--jmx <filename>` | which uploaded plan to run (defaults to the project's) |
127
+ | `--args "<jmeter args>"` | extra JMeter arguments |
128
+ | `--no-split` | send whole data files to every generator instead of dividing them up |
129
+ | `--timeout 30m` | how long `--wait` will wait before giving up (default `2h`) |
130
+ | `--json` | machine-readable output |
131
+
132
+ ## Using it in CI
133
+
134
+ Set the connection details as environment variables and let the exit code fail
135
+ the build:
136
+
137
+ ```bash
138
+ export ATLAS_URL=https://atlas.cloudbeat.io
139
+ export ATLAS_TOKEN=atlas_pat_...
140
+
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
143
+
144
+ atlas run -p SKT --jmx SKT-2026.jmx --servers 5 \
145
+ --gate "error_rate<1,p95<2000" \
146
+ --junit results.xml
147
+ ```
148
+
149
+ If your test plan is maintained in Atlas rather than in the repository, drop the
150
+ upload step — the run command works on its own.
151
+
152
+ ### Pass/fail thresholds
153
+
154
+ `--gate` fails the command when a threshold is breached. Thresholds are checked
155
+ against the run's overall totals:
156
+
157
+ | metric | meaning |
158
+ |--------|---------|
159
+ | `error_rate` | percentage of failed requests |
160
+ | `avg` | average response time, ms |
161
+ | `p90`, `p95`, `p99` | percentile response times, ms |
162
+ | `max` | slowest response, ms |
163
+ | `throughput` | requests per second |
164
+ | `requests` | total requests |
165
+
166
+ Use `<`, `<=`, `>` or `>=`. Separate several with commas, or repeat `--gate`.
167
+ Setting a gate implies `--wait`, since there is nothing to check until the test
168
+ has finished.
169
+
170
+ ### Exit codes
171
+
172
+ | code | meaning |
173
+ |------|---------|
174
+ | `0` | the test ran and every threshold passed |
175
+ | `1` | the test ran but a `--gate` threshold was breached |
176
+ | `2` | something was wrong with the request — bad option, unknown project, not signed in |
177
+ | `3` | the test failed or timed out, or the server could not be reached |
178
+
179
+ ### JUnit report
180
+
181
+ `--junit results.xml` writes a standard JUnit file with one test case per
182
+ threshold, so CI shows each one as its own pass or fail. The run's headline
183
+ numbers are included as properties.
184
+
185
+ ## Recording a scenario
186
+
187
+ `atlas record` opens a browser, records what you do, and writes a JMeter test
188
+ plan:
189
+
190
+ ```bash
191
+ atlas record https://example.com -o flow.jmx
192
+ ```
193
+
194
+ Add `--upload -p SKT` to put the finished plan straight into a project, ready
195
+ to run.
196
+
197
+ | | |
198
+ |---|---|
199
+ | `-o, --out <file>` | output file (an existing plan is added to, not replaced) |
200
+ | `-n, --name <name>` | name for the recorded scenario |
201
+ | `--cookies <file>` | preload cookies to skip a login step |
202
+ | `--profile <dir>` | reuse a browser profile for a persistent session |
203
+
204
+ The recorder is distributed separately because it downloads its own browser,
205
+ which most CI machines don't need. `atlas record` will tell you how to install
206
+ it if it isn't present.
207
+
208
+ ## Command reference
209
+
210
+ | | |
211
+ |---|---|
212
+ | `atlas login` | sign in with a password or an API token |
213
+ | `atlas logout` | forget the stored credential |
214
+ | `atlas whoami` | show who you're signed in as |
215
+ | `atlas projects` | list the projects you can access |
216
+ | `atlas files list` | list a project's files |
217
+ | `atlas files upload <files...>` | upload test plans and data files |
218
+ | `atlas files download <filename>` | download one file |
219
+ | `atlas files rm <filename>` | delete a file (`--yes` to skip the prompt) |
220
+ | `atlas run` | start a test |
221
+ | `atlas status <testId>` | check whether a test is still running |
222
+ | `atlas results <testId>` | show the aggregation report for a test |
223
+ | `atlas stop <testId>` | stop a running test |
224
+ | `atlas record [url]` | record a browser session |
225
+ | `atlas config get` / `set` | default instance URL and project |
226
+
227
+ Most commands take `-p, --project <name>` and `--json`. Run
228
+ `atlas <command> --help` for the full list of options.
229
+
230
+ ## Settings
231
+
232
+ Command-line options win over environment variables, which win over the saved
233
+ configuration file.
234
+
235
+ | variable | meaning |
236
+ |----------|---------|
237
+ | `ATLAS_URL` | your Atlas instance |
238
+ | `ATLAS_TOKEN` | API token |
239
+ | `ATLAS_PROJECT` | project to use when `--project` is omitted |
240
+ | `ATLAS_DEBUG` | set to any value to print full error details |
241
+
242
+ To avoid repeating yourself:
243
+
244
+ ```bash
245
+ atlas config set project SKT
246
+ atlas config set url https://atlas.cloudbeat.io
247
+ ```
package/dist/api.js ADDED
@@ -0,0 +1,159 @@
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.AtlasApi = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const config_1 = require("./config");
10
+ const ui_1 = require("./ui");
11
+ // ── Client ───────────────────────────────────────────────────────────────────
12
+ class AtlasApi {
13
+ url;
14
+ token;
15
+ constructor(cfg) {
16
+ this.url = (cfg.url ?? '').replace(/\/+$/, '');
17
+ this.token = cfg.token;
18
+ }
19
+ static fromConfig(flags = {}) {
20
+ return new AtlasApi((0, config_1.loadConfig)(flags));
21
+ }
22
+ get baseUrl() {
23
+ return this.url;
24
+ }
25
+ authHeaders() {
26
+ if (!this.token) {
27
+ throw new ui_1.CliError('Not authenticated. Run `atlas login`, or set ATLAS_TOKEN to an API token.', ui_1.EXIT.USAGE);
28
+ }
29
+ return { Authorization: `Bearer ${this.token}` };
30
+ }
31
+ /** Any request against the control plane; `auth: false` for the public API. */
32
+ async request(method, routePath, opts = {}) {
33
+ const auth = opts.auth !== false;
34
+ const headers = auth ? this.authHeaders() : {};
35
+ if (opts.body !== undefined)
36
+ headers['Content-Type'] = 'application/json';
37
+ let res;
38
+ try {
39
+ res = await fetch(`${this.url}${routePath}`, {
40
+ method,
41
+ headers,
42
+ body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
43
+ });
44
+ }
45
+ catch (err) {
46
+ throw new ui_1.CliError(`Cannot reach ${this.url}: ${err.message}`, ui_1.EXIT.ERROR);
47
+ }
48
+ return this.parse(method, routePath, res);
49
+ }
50
+ async parse(method, routePath, res) {
51
+ const text = await res.text();
52
+ let payload;
53
+ try {
54
+ payload = text ? JSON.parse(text) : {};
55
+ }
56
+ catch {
57
+ payload = { error: text.slice(0, 200) };
58
+ }
59
+ if (!res.ok) {
60
+ const message = payload.error ?? res.statusText;
61
+ // 401/403 are almost always a credential problem the user must fix, not a
62
+ // transient failure worth retrying — surface them as usage errors.
63
+ const code = res.status === 401 || res.status === 403 ? ui_1.EXIT.USAGE : ui_1.EXIT.ERROR;
64
+ throw new ui_1.CliError(`${method} ${routePath} → ${res.status}: ${message}`, code);
65
+ }
66
+ return payload;
67
+ }
68
+ // ── auth ──────────────────────────────────────────────────────────────────
69
+ async login(username, password) {
70
+ let res;
71
+ try {
72
+ res = await fetch(`${this.url}/admin/api/auth/login`, {
73
+ method: 'POST',
74
+ headers: { 'Content-Type': 'application/json' },
75
+ body: JSON.stringify({ username, password }),
76
+ });
77
+ }
78
+ catch (err) {
79
+ throw new ui_1.CliError(`Cannot reach ${this.url}: ${err.message}`, ui_1.EXIT.ERROR);
80
+ }
81
+ return this.parse('POST', '/admin/api/auth/login', res);
82
+ }
83
+ async me() {
84
+ return this.request('GET', '/admin/api/auth/me');
85
+ }
86
+ // ── projects & files ──────────────────────────────────────────────────────
87
+ async projects() {
88
+ return this.request('GET', '/admin/api/projects');
89
+ }
90
+ /** Projects are addressed by name on the CLI; the API wants the id. */
91
+ async projectByName(name) {
92
+ const all = await this.projects();
93
+ const hit = all.find(p => p.name.toLowerCase() === name.toLowerCase());
94
+ if (!hit) {
95
+ const known = all.map(p => p.name).join(', ') || '(none visible to this token)';
96
+ throw new ui_1.CliError(`No project named "${name}". Available: ${known}`, ui_1.EXIT.USAGE);
97
+ }
98
+ return hit;
99
+ }
100
+ async files(projectId) {
101
+ return this.request('GET', `/admin/api/projects/${projectId}/files`);
102
+ }
103
+ async uploadFiles(projectId, paths) {
104
+ const form = new FormData();
105
+ for (const p of paths) {
106
+ if (!fs_1.default.existsSync(p))
107
+ throw new ui_1.CliError(`File not found: ${p}`, ui_1.EXIT.USAGE);
108
+ const buf = fs_1.default.readFileSync(p);
109
+ form.append('files', new Blob([buf]), path_1.default.basename(p));
110
+ }
111
+ let res;
112
+ try {
113
+ res = await fetch(`${this.url}/admin/api/projects/${projectId}/files`, {
114
+ method: 'POST',
115
+ headers: this.authHeaders(), // no Content-Type: fetch sets the multipart boundary
116
+ body: form,
117
+ });
118
+ }
119
+ catch (err) {
120
+ throw new ui_1.CliError(`Upload failed: ${err.message}`, ui_1.EXIT.ERROR);
121
+ }
122
+ return this.parse('POST', `/admin/api/projects/${projectId}/files`, res);
123
+ }
124
+ async downloadFile(projectId, filename) {
125
+ const res = await fetch(`${this.url}/admin/api/projects/${projectId}/files/${encodeURIComponent(filename)}/download`, { headers: this.authHeaders() });
126
+ if (!res.ok)
127
+ throw new ui_1.CliError(`Download failed: ${res.status} ${res.statusText}`, ui_1.EXIT.ERROR);
128
+ return Buffer.from(await res.arrayBuffer());
129
+ }
130
+ async deleteFile(projectId, filename) {
131
+ await this.request('DELETE', `/admin/api/projects/${projectId}/files/${encodeURIComponent(filename)}`);
132
+ }
133
+ // ── fleet ─────────────────────────────────────────────────────────────────
134
+ async servers() {
135
+ return this.request('GET', '/admin/api/servers');
136
+ }
137
+ async clusters() {
138
+ return this.request('GET', '/admin/api/clusters');
139
+ }
140
+ // ── runs ──────────────────────────────────────────────────────────────────
141
+ async run(projectId, body) {
142
+ return this.request('POST', `/admin/api/projects/${projectId}/run`, { body });
143
+ }
144
+ async queueEntry(queueId) {
145
+ return this.request('GET', `/admin/api/tests/queue/${queueId}`);
146
+ }
147
+ async stop(testId) {
148
+ return this.request('POST', `/admin/api/tests/${encodeURIComponent(testId)}/stop`, { body: {} });
149
+ }
150
+ // ── results (public dashboard API) ─────────────────────────────────────────
151
+ async summary(testId) {
152
+ return this.request('GET', `/api/tests/${encodeURIComponent(testId)}/summary`, { auth: false });
153
+ }
154
+ async testStatus(testId) {
155
+ return this.request('GET', `/api/tests/${encodeURIComponent(testId)}/status`, { auth: false });
156
+ }
157
+ }
158
+ exports.AtlasApi = AtlasApi;
159
+ //# sourceMappingURL=api.js.map
package/dist/cli.js ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const ui_1 = require("./ui");
10
+ const config_1 = require("./config");
11
+ const auth_1 = require("./commands/auth");
12
+ const projects_1 = require("./commands/projects");
13
+ const files_1 = require("./commands/files");
14
+ const run_1 = require("./commands/run");
15
+ const results_1 = require("./commands/results");
16
+ const record_1 = require("./commands/record");
17
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
18
+ const { version } = require('../package.json');
19
+ const program = new commander_1.Command();
20
+ program
21
+ .name('atlas')
22
+ .description('Atlas — run JMeter load tests from CI and record scenarios')
23
+ .version(version)
24
+ .addHelpText('after', `
25
+ Environment:
26
+ ATLAS_URL control plane URL (default https://atlas.cloudbeat.io)
27
+ ATLAS_TOKEN API token — create one on the Profile page
28
+ ATLAS_PROJECT default project for --project
29
+
30
+ Exit codes:
31
+ 0 success 1 a --gate threshold was breached
32
+ 2 usage error 3 the run failed, timed out, or the API was unreachable
33
+
34
+ Examples:
35
+ atlas login --token atlas_pat_...
36
+ atlas files upload -p SKT ./SKT-2026.jmx ./data/*.txt
37
+ atlas run -p SKT --jmx SKT-2026.jmx --servers 5 --wait \\
38
+ --gate "error_rate<1,p95<2000" --junit results.xml
39
+ atlas record https://example.com -o flow.jmx --upload -p SKT
40
+ `);
41
+ (0, auth_1.registerAuth)(program);
42
+ (0, projects_1.registerProjects)(program);
43
+ (0, files_1.registerFiles)(program);
44
+ (0, run_1.registerRun)(program);
45
+ (0, results_1.registerResults)(program);
46
+ (0, record_1.registerRecord)(program);
47
+ // Small config helper so CI/dev can pin a default project or URL without env.
48
+ const config = program.command('config').description('Read or write CLI settings');
49
+ config
50
+ .command('get')
51
+ .description('Show the effective settings')
52
+ .action(() => {
53
+ const cfg = (0, config_1.loadConfig)();
54
+ ui_1.log.info(`url ${cfg.url}`);
55
+ ui_1.log.info(`project ${cfg.project ?? chalk_1.default.dim('(unset)')}`);
56
+ ui_1.log.info(`token ${cfg.token ? chalk_1.default.dim(cfg.token.slice(0, 16) + '…') : chalk_1.default.dim('(unset)')}`);
57
+ ui_1.log.dim((0, config_1.configPath)());
58
+ });
59
+ config
60
+ .command('set <key> <value>')
61
+ .description('Persist a setting (url | project)')
62
+ .action((key, value) => {
63
+ if (key !== 'url' && key !== 'project') {
64
+ throw new ui_1.CliError('Only "url" and "project" can be set. Use `atlas login` for credentials.', ui_1.EXIT.USAGE);
65
+ }
66
+ (0, config_1.saveConfig)({ [key]: value });
67
+ ui_1.log.ok(`${key} = ${value}`);
68
+ });
69
+ async function main() {
70
+ await program.parseAsync(process.argv);
71
+ }
72
+ main().catch((err) => {
73
+ // 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));
79
+ if (process.env.ATLAS_DEBUG)
80
+ console.error(err);
81
+ process.exit(ui_1.EXIT.ERROR);
82
+ });
83
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,89 @@
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.registerAuth = registerAuth;
7
+ const readline_1 = __importDefault(require("readline"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const api_1 = require("../api");
10
+ const config_1 = require("../config");
11
+ const ui_1 = require("../ui");
12
+ function ask(question, { hidden = false } = {}) {
13
+ const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
14
+ return new Promise(resolve => {
15
+ if (!hidden) {
16
+ rl.question(question, answer => { rl.close(); resolve(answer.trim()); });
17
+ return;
18
+ }
19
+ // Suppress echo for the password: intercept the output stream while typing.
20
+ const rlAny = rl;
21
+ process.stdout.write(question);
22
+ rlAny._writeToOutput = () => { };
23
+ rl.question('', answer => {
24
+ rl.close();
25
+ process.stdout.write('\n');
26
+ resolve(answer.trim());
27
+ });
28
+ });
29
+ }
30
+ function registerAuth(program) {
31
+ program
32
+ .command('login')
33
+ .description('Authenticate against an Atlas control plane')
34
+ .option('--url <url>', `control plane URL (default: ${config_1.DEFAULT_URL})`)
35
+ .option('--token <token>', 'API token (atlas_pat_…) — preferred for CI')
36
+ .option('-u, --user <email>', 'email address, for interactive login')
37
+ .action(async (opts) => {
38
+ const cfg = (0, config_1.loadConfig)({ url: opts.url });
39
+ const url = cfg.url;
40
+ if (opts.token) {
41
+ // Prove the token works before writing it — a saved bad token is worse
42
+ // than a failed login, because every later command fails obscurely.
43
+ const api = new api_1.AtlasApi({ url, token: opts.token });
44
+ const me = await api.me();
45
+ (0, config_1.saveConfig)({ url, token: opts.token });
46
+ ui_1.log.ok(`Authenticated as ${chalk_1.default.bold(me.user.username)} (${me.user.role}) at ${url}`);
47
+ ui_1.log.dim(`Token saved to ${(0, config_1.configPath)()}`);
48
+ return;
49
+ }
50
+ const username = opts.user ?? (await ask('Email: '));
51
+ if (!username)
52
+ throw new ui_1.CliError('Email required', ui_1.EXIT.USAGE);
53
+ const password = await ask('Password: ', { hidden: true });
54
+ if (!password)
55
+ throw new ui_1.CliError('Password required', ui_1.EXIT.USAGE);
56
+ const api = new api_1.AtlasApi({ url });
57
+ const res = await api.login(username, password);
58
+ (0, config_1.saveConfig)({ url, token: res.token });
59
+ ui_1.log.ok(`Signed in as ${chalk_1.default.bold(res.user.username)} (${res.user.role}) at ${url}`);
60
+ ui_1.log.dim(`Session saved to ${(0, config_1.configPath)()}`);
61
+ ui_1.log.warn('This session expires in 8 hours. For CI, create an API token on the Profile page and use `atlas login --token`.');
62
+ });
63
+ program
64
+ .command('logout')
65
+ .description('Forget the stored credential')
66
+ .action(() => {
67
+ (0, config_1.clearCredentials)();
68
+ ui_1.log.ok('Credential removed.');
69
+ });
70
+ program
71
+ .command('whoami')
72
+ .description('Show the authenticated identity')
73
+ .option('--json', 'machine-readable output')
74
+ .action(async (opts) => {
75
+ const api = api_1.AtlasApi.fromConfig();
76
+ const me = await api.me();
77
+ if (opts.json) {
78
+ console.log(JSON.stringify(me, null, 2));
79
+ return;
80
+ }
81
+ const active = me.activeAccountId != null
82
+ ? me.accounts.find(a => a.id === me.activeAccountId)?.name ?? String(me.activeAccountId)
83
+ : (me.superAdmin ? 'all accounts' : '—');
84
+ ui_1.log.info(`${chalk_1.default.bold(me.user.username)} ${chalk_1.default.dim(me.user.role + (me.superAdmin ? ' · super-admin' : ''))}`);
85
+ ui_1.log.info(`account: ${active}`);
86
+ ui_1.log.info(`server : ${api.baseUrl}`);
87
+ });
88
+ }
89
+ //# sourceMappingURL=auth.js.map