relight-cli 0.1.0 → 0.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.
Files changed (42) hide show
  1. package/README.md +77 -34
  2. package/package.json +12 -4
  3. package/src/cli.js +305 -1
  4. package/src/commands/apps.js +128 -0
  5. package/src/commands/auth.js +75 -4
  6. package/src/commands/config.js +282 -0
  7. package/src/commands/cost.js +593 -0
  8. package/src/commands/db.js +531 -0
  9. package/src/commands/deploy.js +298 -0
  10. package/src/commands/doctor.js +41 -9
  11. package/src/commands/domains.js +223 -0
  12. package/src/commands/logs.js +111 -0
  13. package/src/commands/open.js +42 -0
  14. package/src/commands/ps.js +121 -0
  15. package/src/commands/scale.js +132 -0
  16. package/src/lib/clouds/aws.js +309 -35
  17. package/src/lib/clouds/cf.js +401 -2
  18. package/src/lib/clouds/gcp.js +234 -3
  19. package/src/lib/clouds/slicervm.js +139 -0
  20. package/src/lib/config.js +40 -0
  21. package/src/lib/docker.js +34 -0
  22. package/src/lib/link.js +20 -5
  23. package/src/lib/providers/aws/app.js +481 -0
  24. package/src/lib/providers/aws/db.js +513 -0
  25. package/src/lib/providers/aws/dns.js +232 -0
  26. package/src/lib/providers/aws/registry.js +59 -0
  27. package/src/lib/providers/cf/app.js +596 -0
  28. package/src/lib/providers/cf/bundle.js +70 -0
  29. package/src/lib/providers/cf/db.js +279 -0
  30. package/src/lib/providers/cf/dns.js +148 -0
  31. package/src/lib/providers/cf/registry.js +17 -0
  32. package/src/lib/providers/gcp/app.js +429 -0
  33. package/src/lib/providers/gcp/db.js +457 -0
  34. package/src/lib/providers/gcp/dns.js +166 -0
  35. package/src/lib/providers/gcp/registry.js +30 -0
  36. package/src/lib/providers/resolve.js +49 -0
  37. package/src/lib/providers/slicervm/app.js +396 -0
  38. package/src/lib/providers/slicervm/db.js +33 -0
  39. package/src/lib/providers/slicervm/dns.js +58 -0
  40. package/src/lib/providers/slicervm/registry.js +7 -0
  41. package/worker-template/package.json +10 -0
  42. package/worker-template/src/index.js +260 -0
@@ -0,0 +1,481 @@
1
+ import { awsJsonApi, ensureEcrAccessRole } from "../../clouds/aws.js";
2
+
3
+ // --- Internal helpers ---
4
+
5
+ async function findService(cfg, appName) {
6
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
7
+ var svcName = `relight-${appName}`;
8
+ var nextToken = null;
9
+
10
+ do {
11
+ var params = {};
12
+ if (nextToken) params.NextToken = nextToken;
13
+ var res = await awsJsonApi("AppRunner.ListServices", params, "apprunner", cr, cfg.region);
14
+
15
+ var match = (res.ServiceSummaryList || []).find((s) => s.ServiceName === svcName);
16
+ if (match) return match;
17
+
18
+ nextToken = res.NextToken;
19
+ } while (nextToken);
20
+
21
+ return null;
22
+ }
23
+
24
+ async function describeService(cfg, serviceArn) {
25
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
26
+ var res = await awsJsonApi(
27
+ "AppRunner.DescribeService",
28
+ { ServiceArn: serviceArn },
29
+ "apprunner",
30
+ cr,
31
+ cfg.region
32
+ );
33
+ return res.Service;
34
+ }
35
+
36
+ async function waitForService(cfg, serviceArn) {
37
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
38
+ for (var i = 0; i < 120; i++) {
39
+ var res = await awsJsonApi(
40
+ "AppRunner.DescribeService",
41
+ { ServiceArn: serviceArn },
42
+ "apprunner",
43
+ cr,
44
+ cfg.region
45
+ );
46
+ var status = res.Service?.Status;
47
+ if (status === "RUNNING") return res.Service;
48
+ if (status === "CREATE_FAILED" || status === "DELETE_FAILED" || status === "DELETED") {
49
+ throw new Error(`Service reached status: ${status}`);
50
+ }
51
+ await new Promise((r) => setTimeout(r, 5000));
52
+ }
53
+ throw new Error("Timed out waiting for service to reach RUNNING status.");
54
+ }
55
+
56
+ function buildEnvVars(appConfig, newSecrets) {
57
+ var envVars = {};
58
+
59
+ // Master config (without env values)
60
+ var configCopy = Object.assign({}, appConfig);
61
+ delete configCopy.env;
62
+ envVars.RELIGHT_APP_CONFIG = JSON.stringify(configCopy);
63
+
64
+ // Individual env vars
65
+ for (var key of (appConfig.envKeys || [])) {
66
+ if (appConfig.env && appConfig.env[key] !== undefined && appConfig.env[key] !== "[hidden]") {
67
+ envVars[key] = String(appConfig.env[key]);
68
+ }
69
+ }
70
+
71
+ // Secret keys as plain env vars
72
+ for (var key of (appConfig.secretKeys || [])) {
73
+ if (newSecrets && newSecrets[key] !== undefined) {
74
+ envVars[key] = String(newSecrets[key]);
75
+ }
76
+ }
77
+
78
+ return envVars;
79
+ }
80
+
81
+ function buildServiceInput(appConfig, imageTag, newSecrets, opts) {
82
+ var envVars = buildEnvVars(appConfig, newSecrets);
83
+ var port = String(appConfig.port || 8080);
84
+ var vcpu = appConfig.vcpu || "1";
85
+ var memory = appConfig.memory ? `${appConfig.memory} MB` : "2048 MB";
86
+
87
+ var input = {
88
+ SourceConfiguration: {
89
+ ImageRepository: {
90
+ ImageIdentifier: imageTag || appConfig.image,
91
+ ImageRepositoryType: "ECR",
92
+ ImageConfiguration: {
93
+ Port: port,
94
+ RuntimeEnvironmentVariables: envVars,
95
+ },
96
+ },
97
+ AutoDeploymentsEnabled: false,
98
+ },
99
+ InstanceConfiguration: {
100
+ Cpu: String(vcpu) + " vCPU",
101
+ Memory: memory,
102
+ },
103
+ HealthCheckConfiguration: {
104
+ Protocol: "TCP",
105
+ Path: "/",
106
+ Interval: 10,
107
+ Timeout: 5,
108
+ HealthyThreshold: 1,
109
+ UnhealthyThreshold: 5,
110
+ },
111
+ };
112
+
113
+ if (opts?.accessRoleArn) {
114
+ input.SourceConfiguration.AuthenticationConfiguration = {
115
+ AccessRoleArn: opts.accessRoleArn,
116
+ };
117
+ }
118
+
119
+ return input;
120
+ }
121
+
122
+ // --- App config ---
123
+
124
+ export async function getAppConfig(cfg, appName) {
125
+ var svc = await findService(cfg, appName);
126
+ if (!svc) return null;
127
+
128
+ var full = await describeService(cfg, svc.ServiceArn);
129
+ var envVars = full.SourceConfiguration?.ImageRepository?.ImageConfiguration?.RuntimeEnvironmentVariables || {};
130
+
131
+ var configStr = envVars.RELIGHT_APP_CONFIG;
132
+ if (!configStr) return null;
133
+
134
+ var appConfig = JSON.parse(configStr);
135
+
136
+ // Reconstruct env from individual env vars
137
+ if (!appConfig.env) appConfig.env = {};
138
+ for (var key of (appConfig.envKeys || [])) {
139
+ if (envVars[key] !== undefined) appConfig.env[key] = envVars[key];
140
+ }
141
+ for (var key of (appConfig.secretKeys || [])) {
142
+ if (envVars[key] !== undefined) appConfig.env[key] = "[hidden]";
143
+ }
144
+
145
+ return appConfig;
146
+ }
147
+
148
+ export async function pushAppConfig(cfg, appName, appConfig, opts) {
149
+ var newSecrets = opts?.newSecrets || {};
150
+ var svc = await findService(cfg, appName);
151
+ if (!svc) throw new Error(`Service relight-${appName} not found.`);
152
+
153
+ // Carry forward existing secret values from the live service
154
+ var full = await describeService(cfg, svc.ServiceArn);
155
+ var liveEnvVars = full.SourceConfiguration?.ImageRepository?.ImageConfiguration?.RuntimeEnvironmentVariables || {};
156
+ for (var key of (appConfig.secretKeys || [])) {
157
+ if (!newSecrets[key] && liveEnvVars[key]) {
158
+ newSecrets[key] = liveEnvVars[key];
159
+ }
160
+ }
161
+
162
+ var envVars = buildEnvVars(appConfig, newSecrets);
163
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
164
+
165
+ var vcpu = appConfig.vcpu || "1";
166
+ var memory = appConfig.memory ? `${appConfig.memory} MB` : "2048 MB";
167
+
168
+ await awsJsonApi("AppRunner.UpdateService", {
169
+ ServiceArn: svc.ServiceArn,
170
+ SourceConfiguration: {
171
+ ImageRepository: {
172
+ ImageIdentifier: appConfig.image,
173
+ ImageRepositoryType: "ECR",
174
+ ImageConfiguration: {
175
+ Port: String(appConfig.port || 8080),
176
+ RuntimeEnvironmentVariables: envVars,
177
+ },
178
+ },
179
+ AutoDeploymentsEnabled: false,
180
+ AuthenticationConfiguration: full.SourceConfiguration?.AuthenticationConfiguration || undefined,
181
+ },
182
+ InstanceConfiguration: {
183
+ Cpu: String(vcpu) + " vCPU",
184
+ Memory: memory,
185
+ },
186
+ }, "apprunner", cr, cfg.region);
187
+
188
+ await waitForService(cfg, svc.ServiceArn);
189
+ }
190
+
191
+ // --- Deploy ---
192
+
193
+ export async function deploy(cfg, appName, imageTag, opts) {
194
+ var appConfig = opts.appConfig;
195
+ var isFirstDeploy = opts.isFirstDeploy;
196
+ var newSecrets = opts.newSecrets || {};
197
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
198
+
199
+ if (isFirstDeploy) {
200
+ // Ensure IAM role for ECR access
201
+ var accessRoleArn = await ensureEcrAccessRole(cr, cfg.region);
202
+
203
+ var input = buildServiceInput(appConfig, imageTag, newSecrets, { accessRoleArn });
204
+ input.ServiceName = `relight-${appName}`;
205
+ input.Tags = [
206
+ { Key: "managed-by", Value: "relight" },
207
+ { Key: "relight-app", Value: appName },
208
+ ];
209
+
210
+ var res = await awsJsonApi("AppRunner.CreateService", input, "apprunner", cr, cfg.region);
211
+ await waitForService(cfg, res.Service.ServiceArn);
212
+ } else {
213
+ var svc = await findService(cfg, appName);
214
+ if (!svc) throw new Error(`Service relight-${appName} not found.`);
215
+
216
+ // Carry forward existing secret values
217
+ var full = await describeService(cfg, svc.ServiceArn);
218
+ var liveEnvVars = full.SourceConfiguration?.ImageRepository?.ImageConfiguration?.RuntimeEnvironmentVariables || {};
219
+ for (var key of (appConfig.secretKeys || [])) {
220
+ if (!newSecrets[key] && liveEnvVars[key]) {
221
+ newSecrets[key] = liveEnvVars[key];
222
+ }
223
+ }
224
+
225
+ var envVars = buildEnvVars(appConfig, newSecrets);
226
+
227
+ await awsJsonApi("AppRunner.UpdateService", {
228
+ ServiceArn: svc.ServiceArn,
229
+ SourceConfiguration: {
230
+ ImageRepository: {
231
+ ImageIdentifier: imageTag,
232
+ ImageRepositoryType: "ECR",
233
+ ImageConfiguration: {
234
+ Port: String(appConfig.port || 8080),
235
+ RuntimeEnvironmentVariables: envVars,
236
+ },
237
+ },
238
+ AutoDeploymentsEnabled: false,
239
+ AuthenticationConfiguration: full.SourceConfiguration?.AuthenticationConfiguration || undefined,
240
+ },
241
+ InstanceConfiguration: {
242
+ Cpu: String(appConfig.vcpu || "1") + " vCPU",
243
+ Memory: appConfig.memory ? `${appConfig.memory} MB` : "2048 MB",
244
+ },
245
+ }, "apprunner", cr, cfg.region);
246
+
247
+ await waitForService(cfg, svc.ServiceArn);
248
+ }
249
+ }
250
+
251
+ // --- List apps ---
252
+
253
+ export async function listApps(cfg) {
254
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
255
+ var apps = [];
256
+ var nextToken = null;
257
+
258
+ do {
259
+ var params = {};
260
+ if (nextToken) params.NextToken = nextToken;
261
+ var res = await awsJsonApi("AppRunner.ListServices", params, "apprunner", cr, cfg.region);
262
+
263
+ for (var svc of (res.ServiceSummaryList || [])) {
264
+ if (svc.ServiceName.startsWith("relight-")) {
265
+ apps.push({
266
+ name: svc.ServiceName.replace("relight-", ""),
267
+ modified: svc.UpdatedAt || null,
268
+ });
269
+ }
270
+ }
271
+
272
+ nextToken = res.NextToken;
273
+ } while (nextToken);
274
+
275
+ return apps;
276
+ }
277
+
278
+ // --- Get app info ---
279
+
280
+ export async function getAppInfo(cfg, appName) {
281
+ var svc = await findService(cfg, appName);
282
+ if (!svc) return null;
283
+
284
+ var appConfig = await getAppConfig(cfg, appName);
285
+ var full = await describeService(cfg, svc.ServiceArn);
286
+ var url = full.ServiceUrl ? `https://${full.ServiceUrl}` : null;
287
+
288
+ return { appConfig, url };
289
+ }
290
+
291
+ // --- Destroy ---
292
+
293
+ export async function destroyApp(cfg, appName) {
294
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
295
+
296
+ // Delete RDS instance if attached
297
+ var appConfig = await getAppConfig(cfg, appName);
298
+ if (appConfig?.dbId) {
299
+ try {
300
+ var { awsQueryApi } = await import("../../clouds/aws.js");
301
+ await awsQueryApi(
302
+ "DeleteDBInstance",
303
+ { DBInstanceIdentifier: appConfig.dbId, SkipFinalSnapshot: "true" },
304
+ "rds",
305
+ cr,
306
+ cfg.region
307
+ );
308
+ } catch {}
309
+ }
310
+
311
+ var svc = await findService(cfg, appName);
312
+ if (!svc) throw new Error(`Service relight-${appName} not found.`);
313
+
314
+ await awsJsonApi("AppRunner.DeleteService", {
315
+ ServiceArn: svc.ServiceArn,
316
+ }, "apprunner", cr, cfg.region);
317
+ }
318
+
319
+ // --- Scale ---
320
+
321
+ export async function scale(cfg, appName, opts) {
322
+ var appConfig = opts.appConfig;
323
+ await pushAppConfig(cfg, appName, appConfig);
324
+ }
325
+
326
+ // --- Container status ---
327
+
328
+ export async function getContainerStatus(cfg, appName) {
329
+ var svc = await findService(cfg, appName);
330
+ if (!svc) return [];
331
+
332
+ var full = await describeService(cfg, svc.ServiceArn);
333
+ return [
334
+ {
335
+ dimensions: { region: cfg.region, status: full.Status },
336
+ avg: { cpuLoad: 0, memory: 0 },
337
+ },
338
+ ];
339
+ }
340
+
341
+ // --- App URL ---
342
+
343
+ export async function getAppUrl(cfg, appName) {
344
+ var appConfig = await getAppConfig(cfg, appName);
345
+ if (appConfig?.domains?.length > 0) {
346
+ return `https://${appConfig.domains[0]}`;
347
+ }
348
+
349
+ var svc = await findService(cfg, appName);
350
+ if (!svc) return null;
351
+
352
+ var full = await describeService(cfg, svc.ServiceArn);
353
+ return full.ServiceUrl ? `https://${full.ServiceUrl}` : null;
354
+ }
355
+
356
+ // --- Log streaming ---
357
+
358
+ export async function streamLogs(cfg, appName) {
359
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
360
+ var svc = await findService(cfg, appName);
361
+ if (!svc) throw new Error(`Service relight-${appName} not found.`);
362
+
363
+ // Extract serviceId from ARN: arn:aws:apprunner:{region}:{account}:service/{name}/{id}
364
+ var arnParts = svc.ServiceArn.split("/");
365
+ var serviceId = arnParts[arnParts.length - 1];
366
+ var serviceName = `relight-${appName}`;
367
+ var logGroup = `/aws/apprunner/${serviceName}/${serviceId}/application`;
368
+
369
+ var lastEventTime = Date.now() - 60000;
370
+ var running = true;
371
+
372
+ var interval = setInterval(async () => {
373
+ if (!running) return;
374
+ try {
375
+ var res = await awsJsonApi(
376
+ "Logs_20140328.FilterLogEvents",
377
+ {
378
+ logGroupName: logGroup,
379
+ startTime: lastEventTime,
380
+ interleaved: true,
381
+ limit: 100,
382
+ },
383
+ "logs",
384
+ cr,
385
+ cfg.region
386
+ );
387
+
388
+ for (var event of (res.events || [])) {
389
+ var ts = new Date(event.timestamp).toISOString();
390
+ console.log(`${ts} ${event.message}`);
391
+ if (event.timestamp > lastEventTime) {
392
+ lastEventTime = event.timestamp + 1;
393
+ }
394
+ }
395
+ } catch {}
396
+ }, 3000);
397
+
398
+ return {
399
+ url: null,
400
+ id: null,
401
+ cleanup: async () => {
402
+ running = false;
403
+ clearInterval(interval);
404
+ },
405
+ };
406
+ }
407
+
408
+ // --- Cost analytics ---
409
+
410
+ export async function getCosts(cfg, appNames, dateRange) {
411
+ var cr = { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey };
412
+
413
+ // Discover apps
414
+ var apps;
415
+ if (appNames) {
416
+ apps = [];
417
+ for (var n of appNames) {
418
+ var svc = await findService(cfg, n);
419
+ if (svc) apps.push({ name: n, serviceArn: svc.ServiceArn });
420
+ }
421
+ } else {
422
+ var listed = await listApps(cfg);
423
+ apps = [];
424
+ for (var a of listed) {
425
+ var svc = await findService(cfg, a.name);
426
+ if (svc) apps.push({ name: a.name, serviceArn: svc.ServiceArn });
427
+ }
428
+ }
429
+
430
+ var { sinceISO, untilISO, sinceDate, untilDate } = dateRange;
431
+ var hours = (untilDate - sinceDate) / (1000 * 60 * 60);
432
+
433
+ var results = [];
434
+ for (var app of apps) {
435
+ var full = await describeService(cfg, app.serviceArn);
436
+ var instanceCfg = full.InstanceConfiguration || {};
437
+
438
+ // Parse vCPU count from "1 vCPU" or "0.25 vCPU"
439
+ var vcpuStr = instanceCfg.Cpu || "1 vCPU";
440
+ var vcpu = parseFloat(vcpuStr);
441
+
442
+ // Parse memory from "2048 MB" or "3 GB"
443
+ var memStr = instanceCfg.Memory || "2048 MB";
444
+ var memGb = memStr.includes("GB") ? parseFloat(memStr) : parseFloat(memStr) / 1024;
445
+
446
+ // App Runner minimum 1 provisioned instance
447
+ var activeVcpuHrs = 0; // No real metrics - estimate as 0 active
448
+ var provisionedVcpuHrs = vcpu * hours;
449
+ var memGbHrs = memGb * hours;
450
+
451
+ results.push({
452
+ name: app.name,
453
+ usage: {
454
+ activeVcpuHrs,
455
+ provisionedVcpuHrs,
456
+ memGbHrs,
457
+ vcpu,
458
+ memGb,
459
+ hours,
460
+ },
461
+ });
462
+ }
463
+
464
+ return results;
465
+ }
466
+
467
+ // --- Regions ---
468
+
469
+ export function getRegions() {
470
+ return [
471
+ { code: "us-east-1", name: "N. Virginia", location: "US East (N. Virginia)" },
472
+ { code: "us-east-2", name: "Ohio", location: "US East (Ohio)" },
473
+ { code: "us-west-2", name: "Oregon", location: "US West (Oregon)" },
474
+ { code: "eu-west-1", name: "Ireland", location: "Europe (Ireland)" },
475
+ { code: "eu-central-1", name: "Frankfurt", location: "Europe (Frankfurt)" },
476
+ { code: "ap-southeast-1", name: "Singapore", location: "Asia Pacific (Singapore)" },
477
+ { code: "ap-southeast-2", name: "Sydney", location: "Asia Pacific (Sydney)" },
478
+ { code: "ap-northeast-1", name: "Tokyo", location: "Asia Pacific (Tokyo)" },
479
+ { code: "ap-south-1", name: "Mumbai", location: "Asia Pacific (Mumbai)" },
480
+ ];
481
+ }