relight-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 (45) hide show
  1. package/README.md +77 -34
  2. package/package.json +12 -4
  3. package/src/cli.js +350 -1
  4. package/src/commands/apps.js +128 -0
  5. package/src/commands/auth.js +13 -4
  6. package/src/commands/config.js +282 -0
  7. package/src/commands/cost.js +593 -0
  8. package/src/commands/db.js +775 -0
  9. package/src/commands/deploy.js +264 -0
  10. package/src/commands/doctor.js +69 -13
  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/commands/service.js +227 -0
  17. package/src/lib/clouds/aws.js +309 -35
  18. package/src/lib/clouds/cf.js +401 -2
  19. package/src/lib/clouds/gcp.js +255 -4
  20. package/src/lib/clouds/neon.js +147 -0
  21. package/src/lib/clouds/slicervm.js +139 -0
  22. package/src/lib/config.js +200 -2
  23. package/src/lib/docker.js +34 -0
  24. package/src/lib/link.js +31 -5
  25. package/src/lib/providers/aws/app.js +481 -0
  26. package/src/lib/providers/aws/db.js +504 -0
  27. package/src/lib/providers/aws/dns.js +232 -0
  28. package/src/lib/providers/aws/registry.js +59 -0
  29. package/src/lib/providers/cf/app.js +596 -0
  30. package/src/lib/providers/cf/bundle.js +70 -0
  31. package/src/lib/providers/cf/db.js +181 -0
  32. package/src/lib/providers/cf/dns.js +148 -0
  33. package/src/lib/providers/cf/registry.js +17 -0
  34. package/src/lib/providers/gcp/app.js +429 -0
  35. package/src/lib/providers/gcp/db.js +372 -0
  36. package/src/lib/providers/gcp/dns.js +166 -0
  37. package/src/lib/providers/gcp/registry.js +30 -0
  38. package/src/lib/providers/neon/db.js +306 -0
  39. package/src/lib/providers/resolve.js +79 -0
  40. package/src/lib/providers/slicervm/app.js +396 -0
  41. package/src/lib/providers/slicervm/db.js +33 -0
  42. package/src/lib/providers/slicervm/dns.js +58 -0
  43. package/src/lib/providers/slicervm/registry.js +7 -0
  44. package/worker-template/package.json +10 -0
  45. package/worker-template/src/index.js +260 -0
@@ -0,0 +1,429 @@
1
+ import {
2
+ mintAccessToken,
3
+ listAllServices,
4
+ getService,
5
+ createService,
6
+ updateService,
7
+ deleteService,
8
+ setIamPolicy,
9
+ listLogEntries,
10
+ queryTimeSeries,
11
+ deleteSqlInstance,
12
+ } from "../../clouds/gcp.js";
13
+
14
+ // --- Helpers ---
15
+
16
+ function serviceName(project, region, name) {
17
+ return `projects/${project}/locations/${region}/services/${name}`;
18
+ }
19
+
20
+ function parseRegionFromName(name) {
21
+ // projects/{p}/locations/{region}/services/{s}
22
+ var parts = name.split("/");
23
+ return parts[3];
24
+ }
25
+
26
+ async function findService(token, project, appName) {
27
+ var svcName = `relight-${appName}`;
28
+ var all = await listAllServices(token, project);
29
+ var svc = all.find(
30
+ (s) => s.name.split("/").pop() === svcName
31
+ );
32
+ if (!svc) return null;
33
+ return svc;
34
+ }
35
+
36
+ function buildServiceBody(appConfig, imageTag, newSecrets) {
37
+ var envVars = [];
38
+
39
+ // Master config (without env values to avoid duplication)
40
+ var configCopy = Object.assign({}, appConfig);
41
+ delete configCopy.env;
42
+ envVars.push({ name: "RELIGHT_APP_CONFIG", value: JSON.stringify(configCopy) });
43
+
44
+ // Individual env vars
45
+ for (var key of (appConfig.envKeys || [])) {
46
+ if (appConfig.env && appConfig.env[key] !== undefined && appConfig.env[key] !== "[hidden]") {
47
+ envVars.push({ name: key, value: String(appConfig.env[key]) });
48
+ }
49
+ }
50
+
51
+ // Secret keys as plain env vars (GCP encrypts at rest)
52
+ for (var key of (appConfig.secretKeys || [])) {
53
+ if (newSecrets && newSecrets[key] !== undefined) {
54
+ envVars.push({ name: key, value: String(newSecrets[key]) });
55
+ }
56
+ }
57
+
58
+ var port = appConfig.port || 8080;
59
+ var maxInstances = appConfig.instances || 2;
60
+ var vcpu = appConfig.vcpu || "1";
61
+ var memory = appConfig.memory ? `${appConfig.memory}Mi` : "512Mi";
62
+
63
+ return {
64
+ template: {
65
+ containers: [
66
+ {
67
+ image: imageTag || appConfig.image,
68
+ env: envVars,
69
+ ports: [{ containerPort: port }],
70
+ resources: {
71
+ limits: {
72
+ cpu: String(vcpu),
73
+ memory: memory,
74
+ },
75
+ },
76
+ },
77
+ ],
78
+ scaling: {
79
+ minInstanceCount: 0,
80
+ maxInstanceCount: maxInstances,
81
+ },
82
+ },
83
+ labels: {
84
+ "managed-by": "relight",
85
+ "relight-app": appConfig.name,
86
+ },
87
+ ingress: "INGRESS_TRAFFIC_ALL",
88
+ };
89
+ }
90
+
91
+ // --- App config ---
92
+
93
+ export async function getAppConfig(cfg, appName) {
94
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
95
+ var svc = await findService(token, cfg.project, appName);
96
+ if (!svc) return null;
97
+
98
+ var containers = svc.template?.containers || [];
99
+ var envVars = containers[0]?.env || [];
100
+ var configEnv = envVars.find((e) => e.name === "RELIGHT_APP_CONFIG");
101
+ if (!configEnv) return null;
102
+
103
+ var appConfig = JSON.parse(configEnv.value);
104
+
105
+ // Reconstruct env from individual env vars on the service
106
+ if (!appConfig.env) appConfig.env = {};
107
+ for (var key of (appConfig.envKeys || [])) {
108
+ var found = envVars.find((e) => e.name === key);
109
+ if (found) appConfig.env[key] = found.value;
110
+ }
111
+ for (var key of (appConfig.secretKeys || [])) {
112
+ var found = envVars.find((e) => e.name === key);
113
+ if (found) appConfig.env[key] = "[hidden]";
114
+ }
115
+
116
+ return appConfig;
117
+ }
118
+
119
+ export async function pushAppConfig(cfg, appName, appConfig, opts) {
120
+ var newSecrets = opts?.newSecrets || {};
121
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
122
+ var svc = await findService(token, cfg.project, appName);
123
+ if (!svc) throw new Error(`Service relight-${appName} not found.`);
124
+
125
+ // Carry forward existing secret values from the live service
126
+ var liveEnvVars = svc.template?.containers?.[0]?.env || [];
127
+ for (var key of (appConfig.secretKeys || [])) {
128
+ if (!newSecrets[key]) {
129
+ var existing = liveEnvVars.find((e) => e.name === key);
130
+ if (existing) newSecrets[key] = existing.value;
131
+ }
132
+ }
133
+
134
+ var body = buildServiceBody(appConfig, appConfig.image, newSecrets);
135
+ await updateService(token, svc.name, body);
136
+ }
137
+
138
+ // --- Deploy ---
139
+
140
+ export async function deploy(cfg, appName, imageTag, opts) {
141
+ var appConfig = opts.appConfig;
142
+ var isFirstDeploy = opts.isFirstDeploy;
143
+ var newSecrets = opts.newSecrets || {};
144
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
145
+
146
+ var svcId = `relight-${appName}`;
147
+ var region = appConfig.regions?.[0] || "us-central1";
148
+
149
+ var body = buildServiceBody(appConfig, imageTag, newSecrets);
150
+
151
+ if (isFirstDeploy) {
152
+ await createService(token, cfg.project, region, svcId, body);
153
+
154
+ // Make service publicly accessible
155
+ var fullName = serviceName(cfg.project, region, svcId);
156
+ await setIamPolicy(token, fullName, {
157
+ bindings: [
158
+ {
159
+ role: "roles/run.invoker",
160
+ members: ["allUsers"],
161
+ },
162
+ ],
163
+ });
164
+ } else {
165
+ var svc = await findService(token, cfg.project, appName);
166
+ if (!svc) throw new Error(`Service ${svcId} not found.`);
167
+
168
+ // Carry forward existing secret values
169
+ var liveEnvVars = svc.template?.containers?.[0]?.env || [];
170
+ for (var key of (appConfig.secretKeys || [])) {
171
+ if (!newSecrets[key]) {
172
+ var existing = liveEnvVars.find((e) => e.name === key);
173
+ if (existing) newSecrets[key] = existing.value;
174
+ }
175
+ }
176
+
177
+ body = buildServiceBody(appConfig, imageTag, newSecrets);
178
+ await updateService(token, svc.name, body);
179
+ }
180
+ }
181
+
182
+ // --- List apps ---
183
+
184
+ export async function listApps(cfg) {
185
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
186
+ var all = await listAllServices(token, cfg.project);
187
+ return all
188
+ .filter((s) => s.labels?.["managed-by"] === "relight")
189
+ .map((s) => ({
190
+ name: s.name.split("/").pop().replace("relight-", ""),
191
+ modified: s.updateTime || null,
192
+ }));
193
+ }
194
+
195
+ // --- Get app info ---
196
+
197
+ export async function getAppInfo(cfg, appName) {
198
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
199
+ var svc = await findService(token, cfg.project, appName);
200
+ if (!svc) return null;
201
+
202
+ var appConfig = await getAppConfig(cfg, appName);
203
+ return { appConfig, url: svc.uri || null };
204
+ }
205
+
206
+ // --- Destroy ---
207
+
208
+ export async function destroyApp(cfg, appName) {
209
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
210
+
211
+ // Delete Cloud SQL instance if attached
212
+ var appConfig = await getAppConfig(cfg, appName);
213
+ if (appConfig?.dbId) {
214
+ try {
215
+ await deleteSqlInstance(token, cfg.project, appConfig.dbId);
216
+ } catch {}
217
+ }
218
+
219
+ // Delete Cloud Run service
220
+ var svc = await findService(token, cfg.project, appName);
221
+ if (!svc) throw new Error(`Service relight-${appName} not found.`);
222
+ await deleteService(token, svc.name);
223
+ }
224
+
225
+ // --- Scale ---
226
+
227
+ export async function scale(cfg, appName, opts) {
228
+ var appConfig = opts.appConfig;
229
+ await pushAppConfig(cfg, appName, appConfig);
230
+ }
231
+
232
+ // --- Container status ---
233
+
234
+ export async function getContainerStatus(cfg, appName) {
235
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
236
+ var svc = await findService(token, cfg.project, appName);
237
+ if (!svc) return [];
238
+
239
+ var region = parseRegionFromName(svc.name);
240
+ var now = new Date();
241
+ var since = new Date(now.getTime() - 15 * 60000);
242
+
243
+ try {
244
+ var res = await queryTimeSeries(token, cfg.project, {
245
+ query: `fetch cloud_run_revision
246
+ | metric 'run.googleapis.com/container/instance_count'
247
+ | filter resource.service_name == 'relight-${appName}'
248
+ | within ${15}m
249
+ | group_by [resource.service_name], mean(val())`,
250
+ });
251
+
252
+ var series = res.timeSeriesData || [];
253
+ return series.map((s) => ({
254
+ dimensions: { region, active: true },
255
+ avg: {
256
+ cpuLoad: 0,
257
+ memory: 0,
258
+ },
259
+ }));
260
+ } catch {
261
+ return [];
262
+ }
263
+ }
264
+
265
+ // --- App URL ---
266
+
267
+ export async function getAppUrl(cfg, appName) {
268
+ var appConfig = await getAppConfig(cfg, appName);
269
+ if (appConfig?.domains?.length > 0) {
270
+ return `https://${appConfig.domains[0]}`;
271
+ }
272
+
273
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
274
+ var svc = await findService(token, cfg.project, appName);
275
+ return svc?.uri || null;
276
+ }
277
+
278
+ // --- Log streaming ---
279
+
280
+ export async function streamLogs(cfg, appName) {
281
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
282
+ var svc = await findService(token, cfg.project, appName);
283
+ if (!svc) throw new Error(`Service relight-${appName} not found.`);
284
+
285
+ var lastTimestamp = new Date(Date.now() - 60000).toISOString();
286
+ var running = true;
287
+
288
+ var interval = setInterval(async () => {
289
+ if (!running) return;
290
+ try {
291
+ var freshToken = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
292
+ var res = await listLogEntries(freshToken, {
293
+ resourceNames: [`projects/${cfg.project}`],
294
+ filter: `resource.type="cloud_run_revision" AND resource.labels.service_name="relight-${appName}" AND timestamp>="${lastTimestamp}"`,
295
+ orderBy: "timestamp asc",
296
+ pageSize: 100,
297
+ });
298
+
299
+ var entries = res.entries || [];
300
+ for (var entry of entries) {
301
+ var ts = entry.timestamp || new Date().toISOString();
302
+ var msg =
303
+ entry.textPayload ||
304
+ entry.jsonPayload?.message ||
305
+ (entry.httpRequest
306
+ ? `${entry.httpRequest.requestMethod} ${entry.httpRequest.requestUrl} ${entry.httpRequest.status}`
307
+ : JSON.stringify(entry.jsonPayload || ""));
308
+ var severity = entry.severity || "DEFAULT";
309
+ console.log(`${ts} [${severity}] ${msg}`);
310
+ lastTimestamp = ts;
311
+ }
312
+ } catch {}
313
+ }, 3000);
314
+
315
+ return {
316
+ url: null,
317
+ id: null,
318
+ cleanup: async () => {
319
+ running = false;
320
+ clearInterval(interval);
321
+ },
322
+ };
323
+ }
324
+
325
+ // --- Cost analytics ---
326
+
327
+ export async function getCosts(cfg, appNames, dateRange) {
328
+ var token = await mintAccessToken(cfg.clientEmail, cfg.privateKey);
329
+ var { sinceISO, untilISO } = dateRange;
330
+
331
+ // Discover apps
332
+ var apps;
333
+ if (appNames) {
334
+ apps = appNames.map((n) => ({ name: n, serviceName: `relight-${n}` }));
335
+ } else {
336
+ var allSvcs = await listAllServices(token, cfg.project);
337
+ apps = allSvcs
338
+ .filter((s) => s.labels?.["managed-by"] === "relight")
339
+ .map((s) => ({
340
+ name: s.name.split("/").pop().replace("relight-", ""),
341
+ serviceName: s.name.split("/").pop(),
342
+ }));
343
+ }
344
+
345
+ // Query Cloud Monitoring for each metric
346
+ var results = [];
347
+ for (var app of apps) {
348
+ var usage = { requests: 0, cpuSeconds: 0, memGibSeconds: 0, egressGb: 0 };
349
+
350
+ try {
351
+ // Request count
352
+ var reqRes = await queryTimeSeries(token, cfg.project, {
353
+ query: `fetch cloud_run_revision
354
+ | metric 'run.googleapis.com/request_count'
355
+ | filter resource.service_name == '${app.serviceName}'
356
+ | within d'${sinceISO}', d'${untilISO}'
357
+ | group_by [], sum(val())`,
358
+ });
359
+ var reqData = reqRes.timeSeriesData || [];
360
+ for (var ts of reqData) {
361
+ for (var pt of (ts.pointData || [])) {
362
+ usage.requests += Number(pt.values?.[0]?.int64Value || 0);
363
+ }
364
+ }
365
+ } catch {}
366
+
367
+ try {
368
+ // CPU allocation
369
+ var cpuRes = await queryTimeSeries(token, cfg.project, {
370
+ query: `fetch cloud_run_revision
371
+ | metric 'run.googleapis.com/container/cpu/allocation_time'
372
+ | filter resource.service_name == '${app.serviceName}'
373
+ | within d'${sinceISO}', d'${untilISO}'
374
+ | group_by [], sum(val())`,
375
+ });
376
+ var cpuData = cpuRes.timeSeriesData || [];
377
+ for (var ts of cpuData) {
378
+ for (var pt of (ts.pointData || [])) {
379
+ usage.cpuSeconds += Number(pt.values?.[0]?.doubleValue || 0);
380
+ }
381
+ }
382
+ } catch {}
383
+
384
+ try {
385
+ // Memory allocation
386
+ var memRes = await queryTimeSeries(token, cfg.project, {
387
+ query: `fetch cloud_run_revision
388
+ | metric 'run.googleapis.com/container/memory/allocation_time'
389
+ | filter resource.service_name == '${app.serviceName}'
390
+ | within d'${sinceISO}', d'${untilISO}'
391
+ | group_by [], sum(val())`,
392
+ });
393
+ var memData = memRes.timeSeriesData || [];
394
+ for (var ts of memData) {
395
+ for (var pt of (ts.pointData || [])) {
396
+ // allocation_time is in GiB-seconds
397
+ usage.memGibSeconds += Number(pt.values?.[0]?.doubleValue || 0);
398
+ }
399
+ }
400
+ } catch {}
401
+
402
+ results.push({ name: app.name, usage });
403
+ }
404
+
405
+ return results;
406
+ }
407
+
408
+ // --- Regions ---
409
+
410
+ export function getRegions() {
411
+ return [
412
+ { code: "us-central1", name: "Iowa", location: "Council Bluffs, Iowa, USA" },
413
+ { code: "us-east1", name: "South Carolina", location: "Moncks Corner, South Carolina, USA" },
414
+ { code: "us-east4", name: "Northern Virginia", location: "Ashburn, Virginia, USA" },
415
+ { code: "us-west1", name: "Oregon", location: "The Dalles, Oregon, USA" },
416
+ { code: "us-west2", name: "Los Angeles", location: "Los Angeles, California, USA" },
417
+ { code: "us-west4", name: "Las Vegas", location: "Las Vegas, Nevada, USA" },
418
+ { code: "europe-west1", name: "Belgium", location: "St. Ghislain, Belgium" },
419
+ { code: "europe-west2", name: "London", location: "London, England, UK" },
420
+ { code: "europe-west4", name: "Netherlands", location: "Eemshaven, Netherlands" },
421
+ { code: "europe-west9", name: "Paris", location: "Paris, France" },
422
+ { code: "asia-east1", name: "Taiwan", location: "Changhua County, Taiwan" },
423
+ { code: "asia-northeast1", name: "Tokyo", location: "Tokyo, Japan" },
424
+ { code: "asia-southeast1", name: "Singapore", location: "Jurong West, Singapore" },
425
+ { code: "australia-southeast1", name: "Sydney", location: "Sydney, Australia" },
426
+ { code: "southamerica-east1", name: "Sao Paulo", location: "Sao Paulo, Brazil" },
427
+ { code: "me-west1", name: "Tel Aviv", location: "Tel Aviv, Israel" },
428
+ ];
429
+ }