elestio 1.0.3 → 1.2.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/LICENSE +21 -21
- package/README.md +523 -279
- package/bin/elestio.js +5 -5
- package/package.json +49 -42
- package/src/api.js +143 -105
- package/src/cli.js +32 -838
- package/src/commands/access.js +224 -132
- package/src/commands/actions.js +427 -427
- package/src/commands/auth.js +146 -146
- package/src/commands/backups.js +215 -215
- package/src/commands/billing.js +106 -106
- package/src/commands/cicd-template.js +363 -0
- package/src/commands/cicd.js +509 -486
- package/src/commands/clusters.js +589 -0
- package/src/commands/projects.js +145 -145
- package/src/commands/services.js +306 -294
- package/src/commands/templates.js +188 -188
- package/src/commands/volumes.js +117 -117
- package/src/config.js +104 -84
- package/src/constants.js +76 -0
- package/src/deployment.js +33 -0
- package/src/payloads/pipeline.js +191 -0
- package/src/payloads/server.js +130 -0
- package/src/registry.js +715 -0
- package/src/router.js +188 -0
- package/src/templates/elestio-config.js +188 -0
- package/src/utils.js +174 -162
package/bin/elestio.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { run } from '../src/cli.js';
|
|
4
|
-
|
|
5
|
-
run(process.argv.slice(2));
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { run } from '../src/cli.js';
|
|
4
|
+
|
|
5
|
+
run(process.argv.slice(2));
|
package/package.json
CHANGED
|
@@ -1,42 +1,49 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "elestio",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Elestio CLI - Deploy and manage services on the Elestio DevOps platform",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "src/cli.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"elestio": "
|
|
9
|
-
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"start": "node bin/elestio.js"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "elestio",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Elestio CLI - Deploy and manage services on the Elestio DevOps platform",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"elestio": "bin/elestio.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/elestio.js",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest",
|
|
14
|
+
"test:coverage": "vitest run --coverage"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"elestio",
|
|
18
|
+
"cli",
|
|
19
|
+
"devops",
|
|
20
|
+
"deploy",
|
|
21
|
+
"cloud",
|
|
22
|
+
"hosting",
|
|
23
|
+
"managed",
|
|
24
|
+
"open-source"
|
|
25
|
+
],
|
|
26
|
+
"author": "Elestio <support@elest.io>",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18.0.0"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"bin/",
|
|
33
|
+
"src/",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/elestio/elestio-cli.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://elest.io",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/elestio/elestio-cli/issues"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@vitest/coverage-v8": "^2.1.8",
|
|
47
|
+
"vitest": "^2.1.8"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/api.js
CHANGED
|
@@ -1,105 +1,143 @@
|
|
|
1
|
-
import { loadConfig, saveConfig
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
method,
|
|
56
|
-
headers: { 'Content-Type': 'application/json' }
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
method,
|
|
100
|
-
headers: { 'Content-Type': 'application/json' }
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
1
|
+
import { loadConfig, saveConfig } from './config.js';
|
|
2
|
+
|
|
3
|
+
const BASE_URL = 'https://api.elest.io';
|
|
4
|
+
|
|
5
|
+
// Requests that exceed this are aborted rather than hanging the CLI forever.
|
|
6
|
+
const REQUEST_TIMEOUT_MS = 60000;
|
|
7
|
+
|
|
8
|
+
// ── JWT management ──
|
|
9
|
+
|
|
10
|
+
function isJwtExpired(config) {
|
|
11
|
+
if (!config.jwt || !config.jwtExpiry) return true;
|
|
12
|
+
return Date.now() > (config.jwtExpiry - 300000); // 5 min buffer
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Wraps fetch with a timeout and turns transport failures into readable errors.
|
|
17
|
+
*/
|
|
18
|
+
async function httpRequest(url, options) {
|
|
19
|
+
let response;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
response = await fetch(url, { ...options, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if (err.name === 'TimeoutError' || err.name === 'AbortError') {
|
|
25
|
+
throw new Error(`Request to ${BASE_URL} timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Cannot reach ${BASE_URL}: ${err.message}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return response;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The API returns HTML on gateway errors, so response.json() alone would surface
|
|
35
|
+
* "Unexpected token <" instead of the real problem.
|
|
36
|
+
*/
|
|
37
|
+
async function parseJson(response, endpoint) {
|
|
38
|
+
const text = await response.text();
|
|
39
|
+
|
|
40
|
+
if (text === '') {
|
|
41
|
+
if (response.ok) return {};
|
|
42
|
+
throw new Error(`${endpoint} failed: HTTP ${response.status} ${response.statusText} (empty response)`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(text);
|
|
47
|
+
} catch {
|
|
48
|
+
const snippet = text.replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
49
|
+
throw new Error(`${endpoint} returned a non-JSON response (HTTP ${response.status}): ${snippet}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function authenticate(email, token) {
|
|
54
|
+
const response = await httpRequest(`${BASE_URL}/api/auth/checkAPIToken`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'Content-Type': 'application/json' },
|
|
57
|
+
body: JSON.stringify({ email, token })
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const data = await parseJson(response, '/api/auth/checkAPIToken');
|
|
61
|
+
|
|
62
|
+
if (data.status !== 'OK' || !data.jwt) {
|
|
63
|
+
throw new Error(data.message || 'Authentication failed');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
jwt: data.jwt,
|
|
68
|
+
jwtExpiry: Date.now() + (23 * 60 * 60 * 1000) // 23h
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function getJwt() {
|
|
73
|
+
const config = loadConfig();
|
|
74
|
+
|
|
75
|
+
if (!config.email || !config.apiToken) {
|
|
76
|
+
throw new Error('Not configured. Run: elestio login');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isJwtExpired(config)) {
|
|
80
|
+
const auth = await authenticate(config.email, config.apiToken);
|
|
81
|
+
saveConfig({ ...config, jwt: auth.jwt, jwtExpiry: auth.jwtExpiry });
|
|
82
|
+
return auth.jwt;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return config.jwt;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── API requests ──
|
|
89
|
+
|
|
90
|
+
function clearJwt() {
|
|
91
|
+
const config = loadConfig();
|
|
92
|
+
saveConfig({ ...config, jwt: null, jwtExpiry: null });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function apiRequest(endpoint, method = 'POST', body = {}, retried = false) {
|
|
96
|
+
const jwt = await getJwt();
|
|
97
|
+
|
|
98
|
+
const options = {
|
|
99
|
+
method,
|
|
100
|
+
headers: { 'Content-Type': 'application/json' }
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (method === 'POST' || method === 'PUT' || method === 'DELETE') {
|
|
104
|
+
options.body = JSON.stringify({ jwt, ...body });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// GET routes read parameters from the query string only (backend requirement).
|
|
108
|
+
const url = method === 'GET'
|
|
109
|
+
? `${BASE_URL}${endpoint}?${new URLSearchParams({ jwt, ...body })}`
|
|
110
|
+
: `${BASE_URL}${endpoint}`;
|
|
111
|
+
|
|
112
|
+
const response = await httpRequest(url, options);
|
|
113
|
+
|
|
114
|
+
if (response.status === 401 && !retried) {
|
|
115
|
+
clearJwt();
|
|
116
|
+
return apiRequest(endpoint, method, body, true);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const data = await parseJson(response, endpoint);
|
|
120
|
+
|
|
121
|
+
const isAuthError = !retried && (
|
|
122
|
+
(data.status === 'error' && data.message?.toLowerCase().includes('auth')) ||
|
|
123
|
+
(data.code === 'InvalidToken') ||
|
|
124
|
+
(data.message?.toLowerCase().includes('invalid token'))
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (isAuthError) {
|
|
128
|
+
clearJwt();
|
|
129
|
+
return apiRequest(endpoint, method, body, true);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return data;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function apiRequestNoAuth(endpoint, method = 'GET') {
|
|
136
|
+
const response = await httpRequest(`${BASE_URL}${endpoint}`, {
|
|
137
|
+
method,
|
|
138
|
+
headers: { 'Content-Type': 'application/json' }
|
|
139
|
+
});
|
|
140
|
+
return parseJson(response, endpoint);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export { BASE_URL, authenticate };
|