@occam-scaly/scaly-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 +39 -0
- package/bin/scaly-public.js +64 -0
- package/bin/scaly.js +3083 -0
- package/lib/scaly-api.js +331 -0
- package/lib/scaly-apply.js +85 -0
- package/lib/scaly-auth.js +411 -0
- package/lib/scaly-deploy.js +137 -0
- package/lib/scaly-logs.js +110 -0
- package/lib/scaly-plan.js +392 -0
- package/lib/scaly-project.js +303 -0
- package/lib/scaly-secrets.js +91 -0
- package/lib/stable-json.js +45 -0
- package/package.json +27 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
function normalizeStage(stage) {
|
|
11
|
+
const s = String(stage || '')
|
|
12
|
+
.trim()
|
|
13
|
+
.toLowerCase();
|
|
14
|
+
if (!s) return 'prod';
|
|
15
|
+
if (s === 'production') return 'prod';
|
|
16
|
+
if (s === 'staging') return 'qa';
|
|
17
|
+
return s;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveScalyDomain(stage) {
|
|
21
|
+
const s = normalizeStage(stage);
|
|
22
|
+
if (s === 'prod') return 'scalyapps.io';
|
|
23
|
+
return `${s}.scalyapps.io`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getConfigDir() {
|
|
27
|
+
const base =
|
|
28
|
+
process.env.SCALY_CONFIG_DIR ||
|
|
29
|
+
process.env.XDG_CONFIG_HOME ||
|
|
30
|
+
path.join(os.homedir(), '.config');
|
|
31
|
+
return path.join(base, 'scaly');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getAuthStorePath() {
|
|
35
|
+
return path.join(getConfigDir(), 'auth.json');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function decodeBase64Url(input) {
|
|
39
|
+
const padLen = (4 - (input.length % 4)) % 4;
|
|
40
|
+
const padded =
|
|
41
|
+
input.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat(padLen);
|
|
42
|
+
return Buffer.from(padded, 'base64').toString('utf8');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function tryGetJwtExpMs(token) {
|
|
46
|
+
if (!token || typeof token !== 'string') return null;
|
|
47
|
+
const parts = token.split('.');
|
|
48
|
+
if (parts.length !== 3) return null;
|
|
49
|
+
try {
|
|
50
|
+
const payloadJson = decodeBase64Url(parts[1] || '');
|
|
51
|
+
const payload = JSON.parse(payloadJson);
|
|
52
|
+
const exp = payload && payload.exp;
|
|
53
|
+
return typeof exp === 'number' ? exp * 1000 : null;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function safeJsonParse(text) {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(text);
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readAuthStore() {
|
|
68
|
+
const p = process.env.SCALY_AUTH_STORE_PATH || getAuthStorePath();
|
|
69
|
+
try {
|
|
70
|
+
const text = fs.readFileSync(p, 'utf8');
|
|
71
|
+
const obj = safeJsonParse(text);
|
|
72
|
+
if (!obj || typeof obj !== 'object') return null;
|
|
73
|
+
if (!obj.access_token || typeof obj.access_token !== 'string') return null;
|
|
74
|
+
return { path: p, session: obj };
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function ensureFilePerms0600(p) {
|
|
81
|
+
try {
|
|
82
|
+
fs.chmodSync(p, 0o600);
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function writeAuthStore(session) {
|
|
87
|
+
const dir = getConfigDir();
|
|
88
|
+
const p = process.env.SCALY_AUTH_STORE_PATH || getAuthStorePath();
|
|
89
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
90
|
+
fs.writeFileSync(p, JSON.stringify(session, null, 2));
|
|
91
|
+
ensureFilePerms0600(p);
|
|
92
|
+
return p;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function clearAuthStore() {
|
|
96
|
+
const p = process.env.SCALY_AUTH_STORE_PATH || getAuthStorePath();
|
|
97
|
+
try {
|
|
98
|
+
fs.unlinkSync(p);
|
|
99
|
+
return true;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function openUrl(url) {
|
|
106
|
+
const plat = process.platform;
|
|
107
|
+
const trySpawn = (cmd, args) => {
|
|
108
|
+
const res = spawnSync(cmd, args, { stdio: 'ignore', shell: false });
|
|
109
|
+
return res && res.status === 0;
|
|
110
|
+
};
|
|
111
|
+
if (plat === 'darwin') return trySpawn('open', [url]);
|
|
112
|
+
if (plat === 'win32') return trySpawn('cmd', ['/c', 'start', '', url]);
|
|
113
|
+
return trySpawn('xdg-open', [url]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function base64UrlEncode(buf) {
|
|
117
|
+
return Buffer.from(buf)
|
|
118
|
+
.toString('base64')
|
|
119
|
+
.replace(/\+/g, '-')
|
|
120
|
+
.replace(/\//g, '_')
|
|
121
|
+
.replace(/=+$/g, '');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function createPkcePair() {
|
|
125
|
+
const verifier = base64UrlEncode(crypto.randomBytes(32));
|
|
126
|
+
const challenge = base64UrlEncode(
|
|
127
|
+
crypto.createHash('sha256').update(verifier).digest()
|
|
128
|
+
);
|
|
129
|
+
return { verifier, challenge };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function fetchScalyOidcConfig({ stage, domainOverride }) {
|
|
133
|
+
const axios = require('axios');
|
|
134
|
+
const stageNorm = normalizeStage(stage);
|
|
135
|
+
const domain = domainOverride || resolveScalyDomain(stageNorm);
|
|
136
|
+
const url = `https://${domain}/.well-known/scaly-oidc.json`;
|
|
137
|
+
const res = await axios.get(url, {
|
|
138
|
+
timeout: 15_000,
|
|
139
|
+
validateStatus: () => true
|
|
140
|
+
});
|
|
141
|
+
if (res.status !== 200 || !res.data || typeof res.data !== 'object') {
|
|
142
|
+
const e = new Error(`Failed to fetch ${url} (HTTP ${res.status})`);
|
|
143
|
+
e.code = 'SCALY_OIDC_CONFIG_FETCH_FAILED';
|
|
144
|
+
throw e;
|
|
145
|
+
}
|
|
146
|
+
return { domain, stage: stageNorm, config: res.data };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function pick(obj, keys) {
|
|
150
|
+
const out = {};
|
|
151
|
+
for (const k of keys) {
|
|
152
|
+
if (obj && Object.prototype.hasOwnProperty.call(obj, k)) out[k] = obj[k];
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function oauthPkceLogin({
|
|
158
|
+
authorizationEndpoint,
|
|
159
|
+
tokenEndpoint,
|
|
160
|
+
clientId,
|
|
161
|
+
redirectUri,
|
|
162
|
+
scopes,
|
|
163
|
+
noOpen
|
|
164
|
+
}) {
|
|
165
|
+
const axios = require('axios');
|
|
166
|
+
const { verifier, challenge } = createPkcePair();
|
|
167
|
+
const state = base64UrlEncode(crypto.randomBytes(16));
|
|
168
|
+
|
|
169
|
+
const authUrl = new URL(authorizationEndpoint);
|
|
170
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
171
|
+
authUrl.searchParams.set('client_id', clientId);
|
|
172
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
173
|
+
authUrl.searchParams.set('scope', scopes.join(' '));
|
|
174
|
+
authUrl.searchParams.set('state', state);
|
|
175
|
+
authUrl.searchParams.set('code_challenge', challenge);
|
|
176
|
+
authUrl.searchParams.set('code_challenge_method', 'S256');
|
|
177
|
+
|
|
178
|
+
const redirect = new URL(redirectUri);
|
|
179
|
+
const expectedPath = redirect.pathname || '/';
|
|
180
|
+
const expectedPort = Number(redirect.port || '80');
|
|
181
|
+
|
|
182
|
+
const code = await new Promise((resolve, reject) => {
|
|
183
|
+
const server = http.createServer((req, res) => {
|
|
184
|
+
try {
|
|
185
|
+
const reqUrl = new URL(req.url || '/', redirectUri);
|
|
186
|
+
if (reqUrl.pathname !== expectedPath) {
|
|
187
|
+
res.statusCode = 404;
|
|
188
|
+
res.end('Not found');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const gotState = reqUrl.searchParams.get('state');
|
|
192
|
+
const gotCode = reqUrl.searchParams.get('code');
|
|
193
|
+
const gotErr = reqUrl.searchParams.get('error');
|
|
194
|
+
if (gotErr) {
|
|
195
|
+
res.statusCode = 400;
|
|
196
|
+
res.end(`Login failed: ${gotErr}`);
|
|
197
|
+
reject(new Error(`OAuth error: ${gotErr}`));
|
|
198
|
+
server.close();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (!gotCode || gotState !== state) {
|
|
202
|
+
res.statusCode = 400;
|
|
203
|
+
res.end('Invalid OAuth callback');
|
|
204
|
+
reject(new Error('Invalid OAuth callback (missing code or state)'));
|
|
205
|
+
server.close();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
res.statusCode = 200;
|
|
209
|
+
res.setHeader('content-type', 'text/plain');
|
|
210
|
+
res.end('Scaly CLI login complete. You can close this tab.');
|
|
211
|
+
resolve(gotCode);
|
|
212
|
+
server.close();
|
|
213
|
+
} catch (e) {
|
|
214
|
+
reject(e);
|
|
215
|
+
try {
|
|
216
|
+
server.close();
|
|
217
|
+
} catch {}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
server.listen(expectedPort, redirect.hostname, () => {
|
|
222
|
+
if (noOpen) {
|
|
223
|
+
console.log(authUrl.toString());
|
|
224
|
+
} else {
|
|
225
|
+
const opened = openUrl(authUrl.toString());
|
|
226
|
+
if (!opened) console.log(authUrl.toString());
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
server.on('error', (e) => reject(e));
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const tokenRes = await axios.post(
|
|
234
|
+
tokenEndpoint,
|
|
235
|
+
new URLSearchParams({
|
|
236
|
+
grant_type: 'authorization_code',
|
|
237
|
+
client_id: clientId,
|
|
238
|
+
code,
|
|
239
|
+
redirect_uri: redirectUri,
|
|
240
|
+
code_verifier: verifier
|
|
241
|
+
}).toString(),
|
|
242
|
+
{
|
|
243
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
244
|
+
timeout: 30_000,
|
|
245
|
+
validateStatus: () => true
|
|
246
|
+
}
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
if (tokenRes.status !== 200 || !tokenRes.data) {
|
|
250
|
+
const e = new Error(`Token exchange failed (HTTP ${tokenRes.status})`);
|
|
251
|
+
e.code = 'SCALY_OIDC_TOKEN_EXCHANGE_FAILED';
|
|
252
|
+
e.details = tokenRes.data;
|
|
253
|
+
throw e;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const data = tokenRes.data;
|
|
257
|
+
const accessToken = data.access_token;
|
|
258
|
+
if (!accessToken)
|
|
259
|
+
throw new Error('Token exchange did not return access_token');
|
|
260
|
+
|
|
261
|
+
const expiresInSec =
|
|
262
|
+
typeof data.expires_in === 'number'
|
|
263
|
+
? data.expires_in
|
|
264
|
+
: Number(data.expires_in || 0) || null;
|
|
265
|
+
const jwtExpMs = tryGetJwtExpMs(accessToken);
|
|
266
|
+
const expiresAt =
|
|
267
|
+
jwtExpMs || (expiresInSec ? Date.now() + expiresInSec * 1000 : null);
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
access_token: accessToken,
|
|
271
|
+
refresh_token: data.refresh_token || null,
|
|
272
|
+
id_token: data.id_token || null,
|
|
273
|
+
token_type: data.token_type || 'Bearer',
|
|
274
|
+
expires_at: expiresAt
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function runAuthLogin(flags) {
|
|
279
|
+
const stage = normalizeStage(
|
|
280
|
+
flags.stage || process.env.SCALY_STAGE || 'prod'
|
|
281
|
+
);
|
|
282
|
+
const domainOverride = flags.domain || null;
|
|
283
|
+
const noOpen = String(flags['no-open'] || '').toLowerCase() === 'true';
|
|
284
|
+
|
|
285
|
+
const { domain, config } = await fetchScalyOidcConfig({
|
|
286
|
+
stage,
|
|
287
|
+
domainOverride
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const oauth = config.oauth || {};
|
|
291
|
+
const clientId = config.client_id;
|
|
292
|
+
const redirectUri = config.redirect_uri;
|
|
293
|
+
const scopes = Array.isArray(config.scopes)
|
|
294
|
+
? config.scopes
|
|
295
|
+
: ['openid', 'profile', 'email'];
|
|
296
|
+
|
|
297
|
+
if (
|
|
298
|
+
!clientId ||
|
|
299
|
+
!redirectUri ||
|
|
300
|
+
!oauth.authorization_endpoint ||
|
|
301
|
+
!oauth.token_endpoint
|
|
302
|
+
) {
|
|
303
|
+
const e = new Error(
|
|
304
|
+
'Incomplete OIDC config from Scaly. Missing client_id/redirect_uri/oauth endpoints.'
|
|
305
|
+
);
|
|
306
|
+
e.code = 'SCALY_OIDC_CONFIG_INVALID';
|
|
307
|
+
e.details = pick(config, ['client_id', 'redirect_uri', 'oauth']);
|
|
308
|
+
throw e;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const tokens = await oauthPkceLogin({
|
|
312
|
+
authorizationEndpoint: oauth.authorization_endpoint,
|
|
313
|
+
tokenEndpoint: oauth.token_endpoint,
|
|
314
|
+
clientId,
|
|
315
|
+
redirectUri,
|
|
316
|
+
scopes,
|
|
317
|
+
noOpen
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const session = {
|
|
321
|
+
version: 1,
|
|
322
|
+
stage,
|
|
323
|
+
domain,
|
|
324
|
+
client_id: clientId,
|
|
325
|
+
scopes,
|
|
326
|
+
issued_at: Date.now(),
|
|
327
|
+
expires_at: tokens.expires_at,
|
|
328
|
+
access_token: tokens.access_token,
|
|
329
|
+
refresh_token: tokens.refresh_token,
|
|
330
|
+
id_token: tokens.id_token
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const p = writeAuthStore(session);
|
|
334
|
+
console.log(`Saved Scaly session to ${p}`);
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function runAuthStatus(flags) {
|
|
339
|
+
const found = readAuthStore();
|
|
340
|
+
const json = String(flags.json || '').toLowerCase() === 'true';
|
|
341
|
+
if (!found) {
|
|
342
|
+
if (json) console.log(JSON.stringify({ authenticated: false }));
|
|
343
|
+
else console.log('Not authenticated. Run: scaly auth login');
|
|
344
|
+
return 1;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const s = found.session;
|
|
348
|
+
const expMs =
|
|
349
|
+
typeof s.expires_at === 'number'
|
|
350
|
+
? s.expires_at
|
|
351
|
+
: tryGetJwtExpMs(s.access_token) || null;
|
|
352
|
+
const expiresInSec = expMs
|
|
353
|
+
? Math.max(0, Math.floor((expMs - Date.now()) / 1000))
|
|
354
|
+
: null;
|
|
355
|
+
|
|
356
|
+
const out = {
|
|
357
|
+
authenticated: true,
|
|
358
|
+
stage: s.stage || null,
|
|
359
|
+
domain: s.domain || null,
|
|
360
|
+
expires_in_seconds: expiresInSec,
|
|
361
|
+
expired_at:
|
|
362
|
+
expMs && expMs <= Date.now() ? new Date(expMs).toISOString() : null,
|
|
363
|
+
store_path: found.path
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
if (json) console.log(JSON.stringify(out));
|
|
367
|
+
else {
|
|
368
|
+
console.log(
|
|
369
|
+
`Authenticated (${out.stage || 'unknown'}). Expires in ${
|
|
370
|
+
out.expires_in_seconds === null
|
|
371
|
+
? 'unknown'
|
|
372
|
+
: `${out.expires_in_seconds}s`
|
|
373
|
+
}.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function runAuthLogout(flags) {
|
|
381
|
+
const json = String(flags.json || '').toLowerCase() === 'true';
|
|
382
|
+
const ok = clearAuthStore();
|
|
383
|
+
if (json) console.log(JSON.stringify({ ok }));
|
|
384
|
+
else console.log(ok ? 'Logged out.' : 'No auth session found.');
|
|
385
|
+
return ok ? 0 : 1;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function runAuth(sub, flags) {
|
|
389
|
+
const action = String(sub || '')
|
|
390
|
+
.trim()
|
|
391
|
+
.toLowerCase();
|
|
392
|
+
if (!action || action === 'help' || action === '--help' || action === '-h') {
|
|
393
|
+
console.log(
|
|
394
|
+
`Usage:\n scaly auth login [--stage prod|dev|qa] [--no-open]\n scaly auth status [--json]\n scaly auth logout [--json]\n`
|
|
395
|
+
);
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
|
398
|
+
if (action === 'login') return await runAuthLogin(flags);
|
|
399
|
+
if (action === 'status') return runAuthStatus(flags);
|
|
400
|
+
if (action === 'logout') return runAuthLogout(flags);
|
|
401
|
+
console.error(`Unknown auth command: ${action}`);
|
|
402
|
+
return 2;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
module.exports = {
|
|
406
|
+
normalizeStage,
|
|
407
|
+
resolveScalyDomain,
|
|
408
|
+
getAuthStorePath,
|
|
409
|
+
readAuthStore,
|
|
410
|
+
runAuth
|
|
411
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const api = require('./scaly-api');
|
|
4
|
+
|
|
5
|
+
const GET_APP = `
|
|
6
|
+
query GetApp($where: AppWhereUniqueInput!) {
|
|
7
|
+
getApp(where: $where) {
|
|
8
|
+
id
|
|
9
|
+
name
|
|
10
|
+
accountId
|
|
11
|
+
stackId
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const GET_APP_GIT_SOURCE = `
|
|
17
|
+
query GetAppGitSource($appId: String!) {
|
|
18
|
+
getAppGitSource(appId: $appId) {
|
|
19
|
+
id
|
|
20
|
+
provider
|
|
21
|
+
repoFullName
|
|
22
|
+
branch
|
|
23
|
+
path
|
|
24
|
+
autoDeploy
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const TRIGGER_GIT_DEPLOY = `
|
|
30
|
+
mutation TriggerGitDeploy($appId: String!) {
|
|
31
|
+
triggerGitDeploy(appId: $appId) {
|
|
32
|
+
id
|
|
33
|
+
status
|
|
34
|
+
triggeredAt
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
const LIST_GIT_DEPLOYMENTS = `
|
|
40
|
+
query ListGitDeployments($appId: String!, $limit: Int) {
|
|
41
|
+
listGitDeployments(appId: $appId, limit: $limit) {
|
|
42
|
+
id
|
|
43
|
+
status
|
|
44
|
+
trigger
|
|
45
|
+
branch
|
|
46
|
+
commitSha
|
|
47
|
+
commitMessage
|
|
48
|
+
commitAuthor
|
|
49
|
+
triggeredAt
|
|
50
|
+
startedAt
|
|
51
|
+
finishedAt
|
|
52
|
+
errorMessage
|
|
53
|
+
deploymentId
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const RESTART_STACK_SERVICES = `
|
|
59
|
+
mutation RestartStackServices($stackId: String!) {
|
|
60
|
+
restartStackServices(stackId: $stackId) {
|
|
61
|
+
id
|
|
62
|
+
status
|
|
63
|
+
step
|
|
64
|
+
progressPct
|
|
65
|
+
errorCode
|
|
66
|
+
errorMessage
|
|
67
|
+
remediationHint
|
|
68
|
+
queuedAt
|
|
69
|
+
startedAt
|
|
70
|
+
finishedAt
|
|
71
|
+
updatedAt
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
const GET_DEPLOYMENT = `
|
|
77
|
+
query GetDeployment($where: DeploymentWhereUniqueInput!) {
|
|
78
|
+
getDeployment(where: $where) {
|
|
79
|
+
id
|
|
80
|
+
status
|
|
81
|
+
step
|
|
82
|
+
progressPct
|
|
83
|
+
errorCode
|
|
84
|
+
errorMessage
|
|
85
|
+
remediationHint
|
|
86
|
+
queuedAt
|
|
87
|
+
startedAt
|
|
88
|
+
finishedAt
|
|
89
|
+
updatedAt
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
`;
|
|
93
|
+
|
|
94
|
+
async function getAppBasic(appId) {
|
|
95
|
+
const data = await api.graphqlRequest(GET_APP, { where: { id: appId } });
|
|
96
|
+
return data?.getApp || null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function getAppGitSource(appId) {
|
|
100
|
+
try {
|
|
101
|
+
const data = await api.graphqlRequest(GET_APP_GIT_SOURCE, { appId });
|
|
102
|
+
return data?.getAppGitSource || null;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function triggerGitDeploy(appId) {
|
|
109
|
+
const data = await api.graphqlRequest(TRIGGER_GIT_DEPLOY, { appId });
|
|
110
|
+
return data?.triggerGitDeploy || null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function listGitDeployments(appId, limit = 10) {
|
|
114
|
+
const data = await api.graphqlRequest(LIST_GIT_DEPLOYMENTS, { appId, limit });
|
|
115
|
+
return data?.listGitDeployments || [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function restartStackServices(stackId) {
|
|
119
|
+
const data = await api.graphqlRequest(RESTART_STACK_SERVICES, { stackId });
|
|
120
|
+
return data?.restartStackServices || null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function getDeployment(deploymentId) {
|
|
124
|
+
const data = await api.graphqlRequest(GET_DEPLOYMENT, {
|
|
125
|
+
where: { id: deploymentId }
|
|
126
|
+
});
|
|
127
|
+
return data?.getDeployment || null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
getAppBasic,
|
|
132
|
+
getAppGitSource,
|
|
133
|
+
triggerGitDeploy,
|
|
134
|
+
listGitDeployments,
|
|
135
|
+
restartStackServices,
|
|
136
|
+
getDeployment
|
|
137
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const api = require('./scaly-api');
|
|
4
|
+
|
|
5
|
+
const GET_APP = `
|
|
6
|
+
query GetApp($where: AppWhereUniqueInput!) {
|
|
7
|
+
getApp(where: $where) {
|
|
8
|
+
id
|
|
9
|
+
name
|
|
10
|
+
accountId
|
|
11
|
+
stackId
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const GET_UNIFIED_LOGS = `
|
|
17
|
+
query GetUnifiedLogs($where: UnifiedLogsWhereInput!) {
|
|
18
|
+
getUnifiedLogs(where: $where) {
|
|
19
|
+
truncated
|
|
20
|
+
nextToken
|
|
21
|
+
cursorStart
|
|
22
|
+
cursorEnd
|
|
23
|
+
diagnosticsBundleUrl
|
|
24
|
+
events {
|
|
25
|
+
id
|
|
26
|
+
ts
|
|
27
|
+
message
|
|
28
|
+
level
|
|
29
|
+
source
|
|
30
|
+
appId
|
|
31
|
+
appName
|
|
32
|
+
stackId
|
|
33
|
+
stackName
|
|
34
|
+
deploymentId
|
|
35
|
+
logStreamName
|
|
36
|
+
ingestionTime
|
|
37
|
+
jsonPayload
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
function parseTimeRangeToLookbackHours(timeRange) {
|
|
44
|
+
const m = String(timeRange || '')
|
|
45
|
+
.trim()
|
|
46
|
+
.match(/^(\d+)([smhd])$/i);
|
|
47
|
+
if (!m) return null;
|
|
48
|
+
const value = Number(m[1]);
|
|
49
|
+
const unit = m[2].toLowerCase();
|
|
50
|
+
if (!Number.isFinite(value) || value <= 0) return null;
|
|
51
|
+
switch (unit) {
|
|
52
|
+
case 's':
|
|
53
|
+
return value / 3600;
|
|
54
|
+
case 'm':
|
|
55
|
+
return value / 60;
|
|
56
|
+
case 'h':
|
|
57
|
+
return value;
|
|
58
|
+
case 'd':
|
|
59
|
+
return value * 24;
|
|
60
|
+
default:
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function mapLevel(level) {
|
|
66
|
+
const l = String(level || 'all').toLowerCase();
|
|
67
|
+
if (l === 'all') return undefined;
|
|
68
|
+
if (l === 'error') return ['Error'];
|
|
69
|
+
if (l === 'warn') return ['Warn'];
|
|
70
|
+
if (l === 'info') return ['Info'];
|
|
71
|
+
if (l === 'debug') return ['Debug'];
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function getAppBasic(appId) {
|
|
76
|
+
const data = await api.graphqlRequest(GET_APP, { where: { id: appId } });
|
|
77
|
+
return data?.getApp || null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function getUnifiedLogs({
|
|
81
|
+
accountId,
|
|
82
|
+
appIds,
|
|
83
|
+
lookbackHours,
|
|
84
|
+
liveFromTs,
|
|
85
|
+
limit,
|
|
86
|
+
q,
|
|
87
|
+
level,
|
|
88
|
+
pageToken
|
|
89
|
+
}) {
|
|
90
|
+
const where = {
|
|
91
|
+
accountId,
|
|
92
|
+
appIds,
|
|
93
|
+
lookbackHours,
|
|
94
|
+
liveFromTs,
|
|
95
|
+
limit,
|
|
96
|
+
q,
|
|
97
|
+
pageToken
|
|
98
|
+
};
|
|
99
|
+
const levels = mapLevel(level);
|
|
100
|
+
if (levels) where.levels = levels;
|
|
101
|
+
|
|
102
|
+
const data = await api.graphqlRequest(GET_UNIFIED_LOGS, { where });
|
|
103
|
+
return data?.getUnifiedLogs || null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
parseTimeRangeToLookbackHours,
|
|
108
|
+
getAppBasic,
|
|
109
|
+
getUnifiedLogs
|
|
110
|
+
};
|