@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
package/lib/scaly-api.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const axios = require('axios');
|
|
4
|
+
|
|
5
|
+
const STAGE_ENDPOINTS = {
|
|
6
|
+
prod: 'https://api.scaly.cloud/graphql',
|
|
7
|
+
dev: 'https://api-dev.scaly.cloud/graphql',
|
|
8
|
+
staging: 'https://api-staging.scaly.cloud/graphql'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function resolveApiEndpoint() {
|
|
12
|
+
return (
|
|
13
|
+
process.env.SCALY_API_URL ||
|
|
14
|
+
process.env.API_ENDPOINT ||
|
|
15
|
+
STAGE_ENDPOINTS[process.env.SCALY_STAGE || 'prod'] ||
|
|
16
|
+
STAGE_ENDPOINTS.prod
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveAuth() {
|
|
21
|
+
const bearer =
|
|
22
|
+
process.env.SCALY_API_BEARER ||
|
|
23
|
+
process.env.API_BEARER_TOKEN ||
|
|
24
|
+
process.env.API_BEARER;
|
|
25
|
+
if (bearer) {
|
|
26
|
+
return { mode: 'bearer', token: bearer };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const scalyApiKey = process.env.SCALY_API_KEY;
|
|
30
|
+
const appSyncApiKey = process.env.SCALY_APPSYNC_KEY;
|
|
31
|
+
if (scalyApiKey && appSyncApiKey) {
|
|
32
|
+
return { mode: 'keys', scalyApiKey, appSyncApiKey };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildHeaders(auth) {
|
|
39
|
+
if (!auth) return {};
|
|
40
|
+
if (auth.mode === 'bearer') {
|
|
41
|
+
return { authorization: `Bearer ${auth.token}` };
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
'x-api-key': auth.appSyncApiKey,
|
|
45
|
+
'x-scaly-api-key': auth.scalyApiKey
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function graphqlRequest(query, variables) {
|
|
50
|
+
const endpoint = resolveApiEndpoint();
|
|
51
|
+
const auth = resolveAuth();
|
|
52
|
+
if (!auth) {
|
|
53
|
+
const e = new Error(
|
|
54
|
+
'Missing auth. Provide SCALY_API_BEARER (or run `scaly login`) OR set SCALY_API_KEY + SCALY_APPSYNC_KEY.'
|
|
55
|
+
);
|
|
56
|
+
e.code = 'SCALY_AUTH_MISSING';
|
|
57
|
+
throw e;
|
|
58
|
+
}
|
|
59
|
+
const res = await axios.post(
|
|
60
|
+
endpoint,
|
|
61
|
+
{ query, variables },
|
|
62
|
+
{
|
|
63
|
+
headers: {
|
|
64
|
+
'content-type': 'application/json',
|
|
65
|
+
...buildHeaders(auth)
|
|
66
|
+
},
|
|
67
|
+
timeout: 60_000
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
if (res.data?.errors?.length) {
|
|
71
|
+
const message =
|
|
72
|
+
res.data.errors?.[0]?.message || JSON.stringify(res.data.errors);
|
|
73
|
+
const e = new Error(`Scaly API error: ${message}`);
|
|
74
|
+
e.code = 'SCALY_API_ERROR';
|
|
75
|
+
e.details = res.data.errors;
|
|
76
|
+
throw e;
|
|
77
|
+
}
|
|
78
|
+
return res.data?.data;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const QUERIES = {
|
|
82
|
+
listAccountsBySlug: `
|
|
83
|
+
query ListAccountsBySlug($slug: String!) {
|
|
84
|
+
listAccounts(
|
|
85
|
+
where: {
|
|
86
|
+
AND: [
|
|
87
|
+
{ slug: { equals: $slug } }
|
|
88
|
+
{ isDeleted: { equals: false } }
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
take: 5
|
|
92
|
+
) {
|
|
93
|
+
id
|
|
94
|
+
name
|
|
95
|
+
slug
|
|
96
|
+
region
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
`,
|
|
100
|
+
listStacksByName: `
|
|
101
|
+
query ListStacksByName($name: String!) {
|
|
102
|
+
listStacks(
|
|
103
|
+
where: {
|
|
104
|
+
AND: [
|
|
105
|
+
{ name: { equals: $name } }
|
|
106
|
+
{ isDeleted: { equals: false } }
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
take: 5
|
|
110
|
+
) {
|
|
111
|
+
id
|
|
112
|
+
name
|
|
113
|
+
accountId
|
|
114
|
+
size
|
|
115
|
+
minIdle
|
|
116
|
+
status
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
`,
|
|
120
|
+
listAppsByName: `
|
|
121
|
+
query ListAppsByName($name: String!) {
|
|
122
|
+
listApps(
|
|
123
|
+
where: {
|
|
124
|
+
AND: [
|
|
125
|
+
{ name: { equals: $name } }
|
|
126
|
+
{ isDeleted: { equals: false } }
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
take: 5
|
|
130
|
+
) {
|
|
131
|
+
id
|
|
132
|
+
name
|
|
133
|
+
accountId
|
|
134
|
+
stackId
|
|
135
|
+
status
|
|
136
|
+
minIdle
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
`,
|
|
140
|
+
listAddOnsByName: `
|
|
141
|
+
query ListAddOnsByName($name: String!) {
|
|
142
|
+
listAddOns(
|
|
143
|
+
where: {
|
|
144
|
+
AND: [
|
|
145
|
+
{ name: { equals: $name } }
|
|
146
|
+
{ isDeleted: { equals: false } }
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
take: 5
|
|
150
|
+
) {
|
|
151
|
+
id
|
|
152
|
+
name
|
|
153
|
+
type
|
|
154
|
+
accountId
|
|
155
|
+
status
|
|
156
|
+
addOnDatabase { size type }
|
|
157
|
+
addOnCognito { userPoolName userPoolId }
|
|
158
|
+
addOnStorageS3 { bucket }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
`,
|
|
162
|
+
listStacks: `
|
|
163
|
+
query ListStacks($take: Int) {
|
|
164
|
+
listStacks(
|
|
165
|
+
where: { isDeleted: { equals: false } }
|
|
166
|
+
take: $take
|
|
167
|
+
) {
|
|
168
|
+
id
|
|
169
|
+
name
|
|
170
|
+
accountId
|
|
171
|
+
size
|
|
172
|
+
minIdle
|
|
173
|
+
status
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
`,
|
|
177
|
+
listApps: `
|
|
178
|
+
query ListApps($take: Int) {
|
|
179
|
+
listApps(
|
|
180
|
+
where: { isDeleted: { equals: false } }
|
|
181
|
+
take: $take
|
|
182
|
+
) {
|
|
183
|
+
id
|
|
184
|
+
name
|
|
185
|
+
accountId
|
|
186
|
+
stackId
|
|
187
|
+
status
|
|
188
|
+
minIdle
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
`
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const MUTATIONS = {
|
|
195
|
+
createStack: `
|
|
196
|
+
mutation CreateStack($data: StackCreateInput!) {
|
|
197
|
+
createStack(data: $data) { id name accountId size minIdle status }
|
|
198
|
+
}
|
|
199
|
+
`,
|
|
200
|
+
updateStack: `
|
|
201
|
+
mutation UpdateStack($where: StackWhereUniqueInput!, $data: StackUpdateInput) {
|
|
202
|
+
updateStack(where: $where, data: $data) { id name accountId size minIdle status }
|
|
203
|
+
}
|
|
204
|
+
`,
|
|
205
|
+
createApp: `
|
|
206
|
+
mutation CreateApp($data: AppCreateInput!) {
|
|
207
|
+
createApp(data: $data) { id name accountId stackId status minIdle }
|
|
208
|
+
}
|
|
209
|
+
`,
|
|
210
|
+
updateApp: `
|
|
211
|
+
mutation UpdateApp($where: AppWhereUniqueInput!, $data: AppUpdateInput) {
|
|
212
|
+
updateApp(where: $where, data: $data) { id name accountId stackId status minIdle }
|
|
213
|
+
}
|
|
214
|
+
`,
|
|
215
|
+
createAddOn: `
|
|
216
|
+
mutation CreateAddOn($data: AddOnCreateInput!) {
|
|
217
|
+
createAddOn(data: $data) { id name type accountId status }
|
|
218
|
+
}
|
|
219
|
+
`
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
async function findStackByName(name) {
|
|
223
|
+
const data = await graphqlRequest(QUERIES.listStacksByName, { name });
|
|
224
|
+
return data?.listStacks || [];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function findAppByName(name) {
|
|
228
|
+
const data = await graphqlRequest(QUERIES.listAppsByName, { name });
|
|
229
|
+
return data?.listApps || [];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function listStacks({ take = 50 } = {}) {
|
|
233
|
+
const data = await graphqlRequest(QUERIES.listStacks, { take });
|
|
234
|
+
return data?.listStacks || [];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function listApps({ take = 100 } = {}) {
|
|
238
|
+
const data = await graphqlRequest(QUERIES.listApps, { take });
|
|
239
|
+
return data?.listApps || [];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function findAccountBySlug(slug) {
|
|
243
|
+
try {
|
|
244
|
+
const data = await graphqlRequest(QUERIES.listAccountsBySlug, { slug });
|
|
245
|
+
return data?.listAccounts?.[0] || null;
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function findAddOnByName(name) {
|
|
252
|
+
const data = await graphqlRequest(QUERIES.listAddOnsByName, { name });
|
|
253
|
+
return data?.listAddOns || [];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function createStack({ accountId, name, size, minIdle }) {
|
|
257
|
+
const data = await graphqlRequest(MUTATIONS.createStack, {
|
|
258
|
+
data: {
|
|
259
|
+
name,
|
|
260
|
+
size,
|
|
261
|
+
minIdle: typeof minIdle === 'number' ? minIdle : undefined,
|
|
262
|
+
account: { connect: { id: accountId } }
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
return data?.createStack;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function updateStack({ id, name, size, minIdle }) {
|
|
269
|
+
const data = await graphqlRequest(MUTATIONS.updateStack, {
|
|
270
|
+
where: { id },
|
|
271
|
+
data: {
|
|
272
|
+
name: name || undefined,
|
|
273
|
+
size: size || undefined,
|
|
274
|
+
minIdle: typeof minIdle === 'number' ? minIdle : undefined
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
return data?.updateStack;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function createApp({ accountId, stackId, name, minIdle }) {
|
|
281
|
+
const data = await graphqlRequest(MUTATIONS.createApp, {
|
|
282
|
+
data: {
|
|
283
|
+
name,
|
|
284
|
+
minIdle: typeof minIdle === 'number' ? minIdle : undefined,
|
|
285
|
+
account: { connect: { id: accountId } },
|
|
286
|
+
stack: stackId ? { connect: { id: stackId } } : undefined
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
return data?.createApp;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function updateApp({ id, stackId, name, minIdle }) {
|
|
293
|
+
const data = await graphqlRequest(MUTATIONS.updateApp, {
|
|
294
|
+
where: { id },
|
|
295
|
+
data: {
|
|
296
|
+
name: name || undefined,
|
|
297
|
+
minIdle: typeof minIdle === 'number' ? minIdle : undefined,
|
|
298
|
+
stack: stackId ? { connect: { id: stackId } } : undefined
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
return data?.updateApp;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function createAddOn({ accountId, name, type, database }) {
|
|
305
|
+
const data = await graphqlRequest(MUTATIONS.createAddOn, {
|
|
306
|
+
data: {
|
|
307
|
+
name,
|
|
308
|
+
type,
|
|
309
|
+
account: { connect: { id: accountId } },
|
|
310
|
+
addOnDatabase: database ? { create: database } : undefined
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
return data?.createAddOn;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
module.exports = {
|
|
317
|
+
resolveApiEndpoint,
|
|
318
|
+
resolveAuth,
|
|
319
|
+
graphqlRequest,
|
|
320
|
+
findAccountBySlug,
|
|
321
|
+
findStackByName,
|
|
322
|
+
findAppByName,
|
|
323
|
+
findAddOnByName,
|
|
324
|
+
listStacks,
|
|
325
|
+
listApps,
|
|
326
|
+
createStack,
|
|
327
|
+
updateStack,
|
|
328
|
+
createApp,
|
|
329
|
+
updateApp,
|
|
330
|
+
createAddOn
|
|
331
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const api = require('./scaly-api');
|
|
4
|
+
|
|
5
|
+
async function applyPlan({ config, plan, autoApprove = false }) {
|
|
6
|
+
const actionable = (plan.operations || []).filter(
|
|
7
|
+
(o) => o.action === 'create' || o.action === 'update'
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const results = [];
|
|
11
|
+
|
|
12
|
+
let createdStack = null;
|
|
13
|
+
let stackId = null;
|
|
14
|
+
const stackOp = (plan.operations || []).find((o) => o.kind === 'stack');
|
|
15
|
+
if (stackOp?.current?.id) stackId = stackOp.current.id;
|
|
16
|
+
|
|
17
|
+
for (const op of actionable) {
|
|
18
|
+
if (op.kind === 'stack' && op.action === 'create') {
|
|
19
|
+
const res = await api.createStack({
|
|
20
|
+
accountId: op.desired.accountId,
|
|
21
|
+
name: op.desired.name,
|
|
22
|
+
size: op.desired.size,
|
|
23
|
+
minIdle: op.desired.minIdle
|
|
24
|
+
});
|
|
25
|
+
createdStack = res;
|
|
26
|
+
stackId = res?.id || stackId;
|
|
27
|
+
results.push({ op_id: op.id, ok: true, result: res });
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (op.kind === 'stack' && op.action === 'update') {
|
|
32
|
+
const res = await api.updateStack({
|
|
33
|
+
id: op.resource.id,
|
|
34
|
+
name: op.desired.name,
|
|
35
|
+
size: op.desired.size,
|
|
36
|
+
minIdle: op.desired.minIdle
|
|
37
|
+
});
|
|
38
|
+
stackId = res?.id || stackId;
|
|
39
|
+
results.push({ op_id: op.id, ok: true, result: res });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (op.kind === 'app' && op.action === 'create') {
|
|
44
|
+
if (!stackId) {
|
|
45
|
+
const stacks = await api.findStackByName(config.stack.name);
|
|
46
|
+
stackId = stacks?.[0]?.id || null;
|
|
47
|
+
}
|
|
48
|
+
const res = await api.createApp({
|
|
49
|
+
accountId: op.desired.accountId,
|
|
50
|
+
stackId,
|
|
51
|
+
name: op.desired.name
|
|
52
|
+
});
|
|
53
|
+
results.push({ op_id: op.id, ok: true, result: res });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (op.kind === 'addon' && op.action === 'create') {
|
|
58
|
+
const res = await api.createAddOn({
|
|
59
|
+
accountId: op.desired.accountId,
|
|
60
|
+
name: op.desired.name,
|
|
61
|
+
type: op.desired.type,
|
|
62
|
+
database: op.desired.database || undefined
|
|
63
|
+
});
|
|
64
|
+
results.push({ op_id: op.id, ok: true, result: res });
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
results.push({
|
|
69
|
+
op_id: op.id,
|
|
70
|
+
ok: false,
|
|
71
|
+
error: { message: `Unsupported operation: ${op.kind}:${op.action}` }
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const ok = results.every((r) => r.ok);
|
|
76
|
+
return {
|
|
77
|
+
ok,
|
|
78
|
+
auto_approve: !!autoApprove,
|
|
79
|
+
applied: actionable.length,
|
|
80
|
+
results,
|
|
81
|
+
created_stack_id: createdStack?.id || null
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { applyPlan };
|