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,111 @@
1
+ import { fatal, fmt } from "../lib/output.js";
2
+ import { resolveAppName } from "../lib/link.js";
3
+ import { resolveCloudId, getCloudCfg, getProvider } from "../lib/providers/resolve.js";
4
+ import kleur from "kleur";
5
+
6
+ export async function logs(name, options) {
7
+ name = resolveAppName(name);
8
+ var cloud = resolveCloudId(options.cloud);
9
+ var cfg = getCloudCfg(cloud);
10
+ var appProvider = await getProvider(cloud, "app");
11
+
12
+ process.stderr.write(
13
+ `Tailing logs for ${fmt.app(name)}... ${fmt.dim("(ctrl+c to stop)")}\n\n`
14
+ );
15
+
16
+ var tail;
17
+ try {
18
+ tail = await appProvider.streamLogs(cfg, name);
19
+ } catch (e) {
20
+ fatal(
21
+ `Could not start log tail for ${fmt.app(name)}.`,
22
+ e.message
23
+ );
24
+ }
25
+
26
+ // Non-WebSocket path (GCP polling)
27
+ if (!tail.url) {
28
+ process.on("SIGINT", async function () {
29
+ process.stderr.write(`\n${fmt.dim("Stopping tail...")}\n`);
30
+ await tail.cleanup();
31
+ process.exit(0);
32
+ });
33
+ // Keep process alive while polling happens in the background
34
+ await new Promise(() => {});
35
+ return;
36
+ }
37
+
38
+ // WebSocket path (Cloudflare)
39
+ var ws = new WebSocket(tail.url, "trace-v1");
40
+
41
+ ws.addEventListener("open", function () {
42
+ ws.send(JSON.stringify({}));
43
+ });
44
+
45
+ ws.addEventListener("message", async function (event) {
46
+ var raw = typeof event.data === "string" ? event.data : await event.data.text();
47
+ var data = JSON.parse(raw);
48
+
49
+ for (var evt of Array.isArray(data) ? data : [data]) {
50
+ var ts = evt.eventTimestamp
51
+ ? new Date(evt.eventTimestamp).toISOString()
52
+ : new Date().toISOString();
53
+ var method = evt.event?.request?.method || "";
54
+ var url = evt.event?.request?.url || "";
55
+ var statusCode = evt.event?.response?.status || "";
56
+ var outcome = evt.outcome || "";
57
+
58
+ if (method) {
59
+ var statusColor =
60
+ statusCode >= 500
61
+ ? kleur.red(statusCode)
62
+ : statusCode >= 400
63
+ ? kleur.yellow(statusCode)
64
+ : kleur.green(statusCode);
65
+ console.log(
66
+ `${kleur.dim(ts)} ${kleur.bold(method)} ${url} ${statusColor} ${kleur.dim(`[${outcome}]`)}`
67
+ );
68
+ }
69
+
70
+ if (evt.logs) {
71
+ for (var log of evt.logs) {
72
+ var level = log.level || "log";
73
+ var msg = (log.message || []).join(" ");
74
+ var levelColor =
75
+ level === "error"
76
+ ? kleur.red(`[${level}]`)
77
+ : level === "warn"
78
+ ? kleur.yellow(`[${level}]`)
79
+ : kleur.dim(`[${level}]`);
80
+ console.log(`${kleur.dim(ts)} ${levelColor} ${msg}`);
81
+ }
82
+ }
83
+
84
+ if (evt.exceptions) {
85
+ for (var ex of evt.exceptions) {
86
+ console.error(
87
+ `${kleur.dim(ts)} ${kleur.red("[exception]")} ${ex.name}: ${ex.message}`
88
+ );
89
+ }
90
+ }
91
+ }
92
+ });
93
+
94
+ ws.addEventListener("error", function (event) {
95
+ process.stderr.write(
96
+ `${kleur.red("WebSocket error:")} ${event.message || "connection failed"}\n`
97
+ );
98
+ });
99
+
100
+ ws.addEventListener("close", function () {
101
+ process.stderr.write(`\n${fmt.dim("Tail disconnected.")}\n`);
102
+ process.exit(0);
103
+ });
104
+
105
+ process.on("SIGINT", async function () {
106
+ process.stderr.write(`\n${fmt.dim("Stopping tail...")}\n`);
107
+ ws.close();
108
+ await tail.cleanup();
109
+ process.exit(0);
110
+ });
111
+ }
@@ -0,0 +1,42 @@
1
+ import { execSync } from "child_process";
2
+ import { platform } from "os";
3
+ import { fatal, fmt } from "../lib/output.js";
4
+ import { resolveAppName } from "../lib/link.js";
5
+ import { resolveCloudId, getCloudCfg, getProvider } from "../lib/providers/resolve.js";
6
+
7
+ export async function open(name, options) {
8
+ name = resolveAppName(name);
9
+ var cloud = resolveCloudId(options.cloud);
10
+ var cfg = getCloudCfg(cloud);
11
+ var appProvider = await getProvider(cloud, "app");
12
+
13
+ var url = await appProvider.getAppUrl(cfg, name);
14
+
15
+ if (!url) {
16
+ fatal(
17
+ "Could not resolve app URL.",
18
+ "Ensure your app is deployed and has a URL configured."
19
+ );
20
+ }
21
+
22
+ process.stderr.write(`Opening ${fmt.url(url)}...\n`);
23
+
24
+ var cmd;
25
+ switch (platform()) {
26
+ case "darwin":
27
+ cmd = "open";
28
+ break;
29
+ case "win32":
30
+ cmd = "start";
31
+ break;
32
+ default:
33
+ cmd = "xdg-open";
34
+ break;
35
+ }
36
+
37
+ try {
38
+ execSync(`${cmd} "${url}"`, { stdio: "ignore" });
39
+ } catch {
40
+ console.log(url);
41
+ }
42
+ }
@@ -0,0 +1,121 @@
1
+ import { fatal, fmt, table } from "../lib/output.js";
2
+ import { resolveAppName } from "../lib/link.js";
3
+ import { resolveCloudId, getCloudCfg, getProvider } from "../lib/providers/resolve.js";
4
+ import kleur from "kleur";
5
+
6
+ export async function ps(name, options) {
7
+ name = resolveAppName(name);
8
+ var cloud = resolveCloudId(options.cloud);
9
+ var cfg = getCloudCfg(cloud);
10
+ var appProvider = await getProvider(cloud, "app");
11
+
12
+ var appConfig = await appProvider.getAppConfig(cfg, name);
13
+
14
+ if (!appConfig) {
15
+ fatal(
16
+ `App ${fmt.app(name)} not found.`,
17
+ `Run ${fmt.cmd(`relight deploy ${name} .`)} first.`
18
+ );
19
+ }
20
+
21
+ var instances = appConfig.instances || 2;
22
+
23
+ // Fetch live metrics
24
+ var metrics = await appProvider.getContainerStatus(cfg, name);
25
+
26
+ // Aggregate: group by region + durableObjectId, keep only active instances
27
+ var containers = [];
28
+ for (var row of metrics) {
29
+ var dim = row.dimensions;
30
+ if (!dim.active) continue;
31
+ var existing = containers.find(
32
+ (c) => c.region === dim.region && c.doId === dim.durableObjectId
33
+ );
34
+ if (existing) {
35
+ existing.cpuSamples++;
36
+ existing.cpuLoad += row.avg?.cpuLoad || 0;
37
+ existing.memory += row.avg?.memory || 0;
38
+ } else {
39
+ containers.push({
40
+ region: dim.region,
41
+ doId: dim.durableObjectId,
42
+ cpuLoad: row.avg?.cpuLoad || 0,
43
+ memory: row.avg?.memory || 0,
44
+ cpuSamples: 1,
45
+ });
46
+ }
47
+ }
48
+ for (var c of containers) {
49
+ c.cpuLoad = c.cpuLoad / c.cpuSamples;
50
+ c.memory = c.memory / c.cpuSamples;
51
+ }
52
+ containers.sort((a, b) => a.region.localeCompare(b.region) || a.doId.localeCompare(b.doId));
53
+
54
+ if (options.json) {
55
+ console.log(
56
+ JSON.stringify(
57
+ {
58
+ name,
59
+ image: appConfig.image,
60
+ regions: appConfig.regions,
61
+ instances,
62
+ containers: containers.map((c) => ({
63
+ region: c.region,
64
+ id: c.doId,
65
+ cpu: +(c.cpuLoad * 100).toFixed(1),
66
+ memoryMiB: +(c.memory / 1024 / 1024).toFixed(0),
67
+ })),
68
+ },
69
+ null,
70
+ 2
71
+ )
72
+ );
73
+ return;
74
+ }
75
+
76
+ var url = await appProvider.getAppUrl(cfg, name);
77
+ var customDomains = appConfig.domains || [];
78
+
79
+ console.log("");
80
+ console.log(`${fmt.bold("App:")} ${fmt.app(name)}`);
81
+ if (customDomains.length > 0) {
82
+ console.log(`${fmt.bold("URL:")} ${fmt.url(`https://${customDomains[0]}`)}`);
83
+ for (var d of customDomains.slice(1)) {
84
+ console.log(` ${fmt.url(`https://${d}`)}`);
85
+ }
86
+ if (url) {
87
+ console.log(` ${fmt.dim(url)}`);
88
+ }
89
+ } else if (url) {
90
+ console.log(`${fmt.bold("URL:")} ${fmt.url(url)}`);
91
+ }
92
+ console.log(
93
+ `${fmt.bold("Image:")} ${appConfig.image || fmt.dim("(not deployed)")}`
94
+ );
95
+ console.log(`${fmt.bold("Regions:")} ${appConfig.regions.join(", ")}`);
96
+ console.log(`${fmt.bold("Instances:")} ${instances} per region`);
97
+
98
+ if (appConfig.deployedAt) {
99
+ console.log(`${fmt.bold("Deployed:")} ${appConfig.deployedAt}`);
100
+ }
101
+
102
+ console.log(`\n${fmt.bold("Containers:")}`);
103
+
104
+ if (containers.length > 0) {
105
+ var headers = ["", "REGION", "ID", "CPU", "MEMORY"];
106
+ var rows = containers.map((c) => [
107
+ kleur.green("*"),
108
+ c.region,
109
+ c.doId.slice(0, 8),
110
+ (c.cpuLoad * 100).toFixed(1) + "%",
111
+ (c.memory / 1024 / 1024).toFixed(0) + " MiB",
112
+ ]);
113
+ console.log(table(headers, rows));
114
+ } else {
115
+ console.log(fmt.dim(" No active containers (app may be sleeping)"));
116
+ }
117
+
118
+ console.log(
119
+ `\n${fmt.dim("Metrics from the last 15 minutes. Expect some delay in reporting.")}`
120
+ );
121
+ }
@@ -0,0 +1,132 @@
1
+ import { success, fatal, fmt } from "../lib/output.js";
2
+ import { resolveAppName } from "../lib/link.js";
3
+ import { resolveCloudId, getCloudCfg, getProvider } from "../lib/providers/resolve.js";
4
+
5
+ export async function scale(name, options) {
6
+ name = resolveAppName(name);
7
+ var cloud = resolveCloudId(options.cloud);
8
+ var cfg = getCloudCfg(cloud);
9
+ var appProvider = await getProvider(cloud, "app");
10
+
11
+ var appConfig = await appProvider.getAppConfig(cfg, name);
12
+
13
+ if (!appConfig) {
14
+ fatal(
15
+ `App ${fmt.app(name)} not found.`,
16
+ `Run ${fmt.cmd(`relight deploy ${name} .`)} first.`
17
+ );
18
+ }
19
+
20
+ var changed = false;
21
+
22
+ if (options.regions) {
23
+ var validRegions = appProvider.getRegions();
24
+ var validCodes = validRegions.map((r) => r.code);
25
+ var regions = options.regions.split(",").map((r) => r.trim().toLowerCase());
26
+ for (var r of regions) {
27
+ if (!validCodes.includes(r)) {
28
+ fatal(
29
+ `Invalid location hint '${r}'.`,
30
+ `Valid hints: ${validCodes.join(", ")}`
31
+ );
32
+ }
33
+ }
34
+ appConfig.regions = regions;
35
+ changed = true;
36
+ }
37
+
38
+ if (options.instances) {
39
+ appConfig.instances = options.instances;
40
+ changed = true;
41
+ }
42
+
43
+ if (options.instanceType) {
44
+ appConfig.instanceType = options.instanceType;
45
+ delete appConfig.vcpu;
46
+ delete appConfig.memory;
47
+ delete appConfig.disk;
48
+ changed = true;
49
+ }
50
+ if (options.vcpu) {
51
+ appConfig.vcpu = options.vcpu;
52
+ delete appConfig.instanceType;
53
+ changed = true;
54
+ }
55
+ if (options.memory) {
56
+ appConfig.memory = options.memory;
57
+ delete appConfig.instanceType;
58
+ changed = true;
59
+ }
60
+ if (options.disk) {
61
+ appConfig.disk = options.disk;
62
+ delete appConfig.instanceType;
63
+ changed = true;
64
+ }
65
+
66
+ if (!changed) {
67
+ // Show current scale
68
+ if (options.json) {
69
+ console.log(
70
+ JSON.stringify(
71
+ {
72
+ regions: appConfig.regions,
73
+ instances: appConfig.instances,
74
+ instanceType: appConfig.instanceType,
75
+ vcpu: appConfig.vcpu,
76
+ memory: appConfig.memory,
77
+ disk: appConfig.disk,
78
+ },
79
+ null,
80
+ 2
81
+ )
82
+ );
83
+ return;
84
+ }
85
+
86
+ console.log(`\n${fmt.bold("App:")} ${fmt.app(name)}`);
87
+ console.log(`${fmt.bold("Regions:")} ${appConfig.regions.join(", ")}`);
88
+ console.log(`${fmt.bold("Instances:")} ${appConfig.instances} per region`);
89
+ if (appConfig.vcpu || appConfig.memory || appConfig.disk) {
90
+ if (appConfig.vcpu) console.log(`${fmt.bold("vCPU:")} ${appConfig.vcpu}`);
91
+ if (appConfig.memory) console.log(`${fmt.bold("Memory:")} ${appConfig.memory} MiB`);
92
+ if (appConfig.disk) console.log(`${fmt.bold("Disk:")} ${appConfig.disk} MB`);
93
+ } else {
94
+ console.log(`${fmt.bold("Type:")} ${appConfig.instanceType || "lite"}`);
95
+ }
96
+ console.log(
97
+ `\n${fmt.dim("Geo-routing is automatic - requests route to the closest deployed region.")}`
98
+ );
99
+ return;
100
+ }
101
+
102
+ await appProvider.scale(cfg, name, { appConfig });
103
+
104
+ if (options.json) {
105
+ console.log(
106
+ JSON.stringify(
107
+ {
108
+ regions: appConfig.regions,
109
+ instances: appConfig.instances,
110
+ instanceType: appConfig.instanceType,
111
+ vcpu: appConfig.vcpu,
112
+ memory: appConfig.memory,
113
+ disk: appConfig.disk,
114
+ },
115
+ null,
116
+ 2
117
+ )
118
+ );
119
+ return;
120
+ }
121
+
122
+ success(`Scaled ${fmt.app(name)} (live).`);
123
+ process.stderr.write(` Regions: ${appConfig.regions.join(", ")}\n`);
124
+ process.stderr.write(` Instances: ${appConfig.instances}\n`);
125
+ if (appConfig.vcpu || appConfig.memory || appConfig.disk) {
126
+ if (appConfig.vcpu) process.stderr.write(` vCPU: ${appConfig.vcpu}\n`);
127
+ if (appConfig.memory) process.stderr.write(` Memory: ${appConfig.memory} MiB\n`);
128
+ if (appConfig.disk) process.stderr.write(` Disk: ${appConfig.disk} MB\n`);
129
+ } else {
130
+ process.stderr.write(` Type: ${appConfig.instanceType || "lite"}\n`);
131
+ }
132
+ }