@cloudbeat/atlas-cli 0.1.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +6 -0
  2. package/dist/api.js +50 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -245,3 +245,9 @@ To avoid repeating yourself:
245
245
  atlas config set project SKT
246
246
  atlas config set url https://atlas.cloudbeat.io
247
247
  ```
248
+
249
+ ## Compatibility
250
+
251
+ Works with any Atlas instance. From 0.3.0 the CLI talks to a new API path, and
252
+ falls back automatically when it finds an older instance that does not serve it —
253
+ so you do not need to match versions between the CLI and your Atlas server.
package/dist/api.js CHANGED
@@ -8,6 +8,9 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const config_1 = require("./config");
10
10
  const ui_1 = require("./ui");
11
+ /** Where the control-plane API lives today, and where it used to live. */
12
+ const API_PREFIX = '/app/api';
13
+ const LEGACY_API_PREFIX = '/admin/api';
11
14
  // ── Client ───────────────────────────────────────────────────────────────────
12
15
  class AtlasApi {
13
16
  url;
@@ -22,6 +25,33 @@ class AtlasApi {
22
25
  get baseUrl() {
23
26
  return this.url;
24
27
  }
28
+ /**
29
+ * The control-plane API prefix.
30
+ *
31
+ * It moved from /admin/api to /app/api when the browser UI moved to /app.
32
+ * Servers keep serving the old prefix so CLIs released before that keep
33
+ * working; this handles the other direction — a current CLI pointed at an
34
+ * Atlas that predates the move. The first 404 under the new prefix switches
35
+ * to the old one for the life of the process, so there is no extra round
36
+ * trip against an up-to-date server.
37
+ */
38
+ prefix = API_PREFIX;
39
+ prefixResolved = false;
40
+ get api() {
41
+ return this.prefix;
42
+ }
43
+ /** Fetch that retries once under the legacy prefix if the route is absent. */
44
+ async fetchApi(routePath, init) {
45
+ const res = await fetch(`${this.url}${routePath}`, init);
46
+ if (res.status === 404 && !this.prefixResolved && routePath.startsWith(API_PREFIX)) {
47
+ this.prefix = LEGACY_API_PREFIX;
48
+ this.prefixResolved = true;
49
+ const legacy = routePath.replace(API_PREFIX, LEGACY_API_PREFIX);
50
+ return { res: await fetch(`${this.url}${legacy}`, init), routePath: legacy };
51
+ }
52
+ this.prefixResolved = true;
53
+ return { res, routePath };
54
+ }
25
55
  authHeaders() {
26
56
  if (!this.token) {
27
57
  throw new ui_1.CliError('Not authenticated. Run `atlas login`, or set ATLAS_TOKEN to an API token.', ui_1.EXIT.USAGE);
@@ -34,9 +64,9 @@ class AtlasApi {
34
64
  const headers = auth ? this.authHeaders() : {};
35
65
  if (opts.body !== undefined)
36
66
  headers['Content-Type'] = 'application/json';
37
- let res;
67
+ let attempt;
38
68
  try {
39
- res = await fetch(`${this.url}${routePath}`, {
69
+ attempt = await this.fetchApi(routePath, {
40
70
  method,
41
71
  headers,
42
72
  body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
@@ -45,7 +75,7 @@ class AtlasApi {
45
75
  catch (err) {
46
76
  throw new ui_1.CliError(`Cannot reach ${this.url}: ${err.message}`, ui_1.EXIT.ERROR);
47
77
  }
48
- return this.parse(method, routePath, res);
78
+ return this.parse(method, attempt.routePath, attempt.res);
49
79
  }
50
80
  async parse(method, routePath, res) {
51
81
  const text = await res.text();
@@ -67,9 +97,11 @@ class AtlasApi {
67
97
  }
68
98
  // ── auth ──────────────────────────────────────────────────────────────────
69
99
  async login(username, password) {
70
- let res;
100
+ // Usually the very first call a CLI makes, so this is where the prefix
101
+ // normally gets resolved against an older server.
102
+ let attempt;
71
103
  try {
72
- res = await fetch(`${this.url}/admin/api/auth/login`, {
104
+ attempt = await this.fetchApi(`${this.api}/auth/login`, {
73
105
  method: 'POST',
74
106
  headers: { 'Content-Type': 'application/json' },
75
107
  body: JSON.stringify({ username, password }),
@@ -78,14 +110,14 @@ class AtlasApi {
78
110
  catch (err) {
79
111
  throw new ui_1.CliError(`Cannot reach ${this.url}: ${err.message}`, ui_1.EXIT.ERROR);
80
112
  }
81
- return this.parse('POST', '/admin/api/auth/login', res);
113
+ return this.parse('POST', attempt.routePath, attempt.res);
82
114
  }
83
115
  async me() {
84
- return this.request('GET', '/admin/api/auth/me');
116
+ return this.request('GET', `${this.api}/auth/me`);
85
117
  }
86
118
  // ── projects & files ──────────────────────────────────────────────────────
87
119
  async projects() {
88
- return this.request('GET', '/admin/api/projects');
120
+ return this.request('GET', `${this.api}/projects`);
89
121
  }
90
122
  /** Projects are addressed by name on the CLI; the API wants the id. */
91
123
  async projectByName(name) {
@@ -98,7 +130,7 @@ class AtlasApi {
98
130
  return hit;
99
131
  }
100
132
  async files(projectId) {
101
- return this.request('GET', `/admin/api/projects/${projectId}/files`);
133
+ return this.request('GET', `${this.api}/projects/${projectId}/files`);
102
134
  }
103
135
  async uploadFiles(projectId, paths) {
104
136
  const form = new FormData();
@@ -110,7 +142,7 @@ class AtlasApi {
110
142
  }
111
143
  let res;
112
144
  try {
113
- res = await fetch(`${this.url}/admin/api/projects/${projectId}/files`, {
145
+ res = await fetch(`${this.url}${this.api}/projects/${projectId}/files`, {
114
146
  method: 'POST',
115
147
  headers: this.authHeaders(), // no Content-Type: fetch sets the multipart boundary
116
148
  body: form,
@@ -119,33 +151,33 @@ class AtlasApi {
119
151
  catch (err) {
120
152
  throw new ui_1.CliError(`Upload failed: ${err.message}`, ui_1.EXIT.ERROR);
121
153
  }
122
- return this.parse('POST', `/admin/api/projects/${projectId}/files`, res);
154
+ return this.parse('POST', `${this.api}/projects/${projectId}/files`, res);
123
155
  }
124
156
  async downloadFile(projectId, filename) {
125
- const res = await fetch(`${this.url}/admin/api/projects/${projectId}/files/${encodeURIComponent(filename)}/download`, { headers: this.authHeaders() });
157
+ const res = await fetch(`${this.url}${this.api}/projects/${projectId}/files/${encodeURIComponent(filename)}/download`, { headers: this.authHeaders() });
126
158
  if (!res.ok)
127
159
  throw new ui_1.CliError(`Download failed: ${res.status} ${res.statusText}`, ui_1.EXIT.ERROR);
128
160
  return Buffer.from(await res.arrayBuffer());
129
161
  }
130
162
  async deleteFile(projectId, filename) {
131
- await this.request('DELETE', `/admin/api/projects/${projectId}/files/${encodeURIComponent(filename)}`);
163
+ await this.request('DELETE', `${this.api}/projects/${projectId}/files/${encodeURIComponent(filename)}`);
132
164
  }
133
165
  // ── fleet ─────────────────────────────────────────────────────────────────
134
166
  async servers() {
135
- return this.request('GET', '/admin/api/servers');
167
+ return this.request('GET', `${this.api}/servers`);
136
168
  }
137
169
  async clusters() {
138
- return this.request('GET', '/admin/api/clusters');
170
+ return this.request('GET', `${this.api}/clusters`);
139
171
  }
140
172
  // ── runs ──────────────────────────────────────────────────────────────────
141
173
  async run(projectId, body) {
142
- return this.request('POST', `/admin/api/projects/${projectId}/run`, { body });
174
+ return this.request('POST', `${this.api}/projects/${projectId}/run`, { body });
143
175
  }
144
176
  async queueEntry(queueId) {
145
- return this.request('GET', `/admin/api/tests/queue/${queueId}`);
177
+ return this.request('GET', `${this.api}/tests/queue/${queueId}`);
146
178
  }
147
179
  async stop(testId) {
148
- return this.request('POST', `/admin/api/tests/${encodeURIComponent(testId)}/stop`, { body: {} });
180
+ return this.request('POST', `${this.api}/tests/${encodeURIComponent(testId)}/stop`, { body: {} });
149
181
  }
150
182
  // ── results (public dashboard API) ─────────────────────────────────────────
151
183
  async summary(testId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudbeat/atlas-cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Atlas CLI — run JMeter load tests from CI and record scenarios",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {