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.
- package/README.md +77 -34
- package/package.json +12 -4
- package/src/cli.js +350 -1
- package/src/commands/apps.js +128 -0
- package/src/commands/auth.js +13 -4
- package/src/commands/config.js +282 -0
- package/src/commands/cost.js +593 -0
- package/src/commands/db.js +775 -0
- package/src/commands/deploy.js +264 -0
- package/src/commands/doctor.js +69 -13
- package/src/commands/domains.js +223 -0
- package/src/commands/logs.js +111 -0
- package/src/commands/open.js +42 -0
- package/src/commands/ps.js +121 -0
- package/src/commands/scale.js +132 -0
- package/src/commands/service.js +227 -0
- package/src/lib/clouds/aws.js +309 -35
- package/src/lib/clouds/cf.js +401 -2
- package/src/lib/clouds/gcp.js +255 -4
- package/src/lib/clouds/neon.js +147 -0
- package/src/lib/clouds/slicervm.js +139 -0
- package/src/lib/config.js +200 -2
- package/src/lib/docker.js +34 -0
- package/src/lib/link.js +31 -5
- package/src/lib/providers/aws/app.js +481 -0
- package/src/lib/providers/aws/db.js +504 -0
- package/src/lib/providers/aws/dns.js +232 -0
- package/src/lib/providers/aws/registry.js +59 -0
- package/src/lib/providers/cf/app.js +596 -0
- package/src/lib/providers/cf/bundle.js +70 -0
- package/src/lib/providers/cf/db.js +181 -0
- package/src/lib/providers/cf/dns.js +148 -0
- package/src/lib/providers/cf/registry.js +17 -0
- package/src/lib/providers/gcp/app.js +429 -0
- package/src/lib/providers/gcp/db.js +372 -0
- package/src/lib/providers/gcp/dns.js +166 -0
- package/src/lib/providers/gcp/registry.js +30 -0
- package/src/lib/providers/neon/db.js +306 -0
- package/src/lib/providers/resolve.js +79 -0
- package/src/lib/providers/slicervm/app.js +396 -0
- package/src/lib/providers/slicervm/db.js +33 -0
- package/src/lib/providers/slicervm/dns.js +58 -0
- package/src/lib/providers/slicervm/registry.js +7 -0
- package/worker-template/package.json +10 -0
- 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 { resolveTarget } from "../lib/providers/resolve.js";
|
|
4
|
+
import kleur from "kleur";
|
|
5
|
+
|
|
6
|
+
export async function logs(name, options) {
|
|
7
|
+
name = resolveAppName(name);
|
|
8
|
+
var target = await resolveTarget(options);
|
|
9
|
+
var cfg = target.cfg;
|
|
10
|
+
var appProvider = await target.provider("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 { resolveTarget } from "../lib/providers/resolve.js";
|
|
6
|
+
|
|
7
|
+
export async function open(name, options) {
|
|
8
|
+
name = resolveAppName(name);
|
|
9
|
+
var target = await resolveTarget(options);
|
|
10
|
+
var cfg = target.cfg;
|
|
11
|
+
var appProvider = await target.provider("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 { resolveTarget } from "../lib/providers/resolve.js";
|
|
4
|
+
import kleur from "kleur";
|
|
5
|
+
|
|
6
|
+
export async function ps(name, options) {
|
|
7
|
+
name = resolveAppName(name);
|
|
8
|
+
var target = await resolveTarget(options);
|
|
9
|
+
var cfg = target.cfg;
|
|
10
|
+
var appProvider = await target.provider("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 { resolveTarget } from "../lib/providers/resolve.js";
|
|
4
|
+
|
|
5
|
+
export async function scale(name, options) {
|
|
6
|
+
name = resolveAppName(name);
|
|
7
|
+
var target = await resolveTarget(options);
|
|
8
|
+
var cfg = target.cfg;
|
|
9
|
+
var appProvider = await target.provider("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
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { createInterface } from "readline";
|
|
2
|
+
import { success, fatal, fmt, table } from "../lib/output.js";
|
|
3
|
+
import {
|
|
4
|
+
SERVICE_TYPES,
|
|
5
|
+
getRegisteredServices,
|
|
6
|
+
saveServiceConfig,
|
|
7
|
+
removeServiceConfig,
|
|
8
|
+
normalizeServiceConfig,
|
|
9
|
+
tryGetServiceConfig,
|
|
10
|
+
} from "../lib/config.js";
|
|
11
|
+
import { verifyConnection } from "../lib/clouds/slicervm.js";
|
|
12
|
+
import { verifyApiKey } from "../lib/clouds/neon.js";
|
|
13
|
+
import kleur from "kleur";
|
|
14
|
+
|
|
15
|
+
function prompt(rl, question) {
|
|
16
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function serviceList() {
|
|
20
|
+
var services = getRegisteredServices();
|
|
21
|
+
|
|
22
|
+
if (services.length === 0) {
|
|
23
|
+
process.stderr.write("No services registered.\n");
|
|
24
|
+
process.stderr.write(
|
|
25
|
+
`\n${fmt.dim("Hint:")} ${fmt.cmd("relight service add")} to register one.\n`
|
|
26
|
+
);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var headers = ["NAME", "LAYER", "TYPE", "ENDPOINT"];
|
|
31
|
+
var rows = services.map((a) => [
|
|
32
|
+
fmt.bold(a.name),
|
|
33
|
+
a.layer,
|
|
34
|
+
SERVICE_TYPES[a.type]?.name || a.type,
|
|
35
|
+
a.socketPath || a.apiUrl || (a.type === "neon" ? "console.neon.tech" : "-"),
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
console.log(table(headers, rows));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function serviceAdd(name) {
|
|
42
|
+
var rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
43
|
+
|
|
44
|
+
// 1. Pick layer
|
|
45
|
+
var layers = ["compute", "db"];
|
|
46
|
+
process.stderr.write(`\n${kleur.bold("Register a service")}\n\n`);
|
|
47
|
+
process.stderr.write(` ${kleur.bold("Layer:")}\n\n`);
|
|
48
|
+
for (var i = 0; i < layers.length; i++) {
|
|
49
|
+
process.stderr.write(` ${kleur.bold(`[${i + 1}]`)} ${layers[i]}\n`);
|
|
50
|
+
}
|
|
51
|
+
process.stderr.write("\n");
|
|
52
|
+
|
|
53
|
+
var layerChoice = await prompt(rl, `Select layer [1-${layers.length}]: `);
|
|
54
|
+
var layerIdx = parseInt(layerChoice, 10) - 1;
|
|
55
|
+
if (isNaN(layerIdx) || layerIdx < 0 || layerIdx >= layers.length) {
|
|
56
|
+
rl.close();
|
|
57
|
+
fatal("Invalid selection.");
|
|
58
|
+
}
|
|
59
|
+
var layer = layers[layerIdx];
|
|
60
|
+
|
|
61
|
+
// 2. Pick type (contextual to layer)
|
|
62
|
+
var types = Object.entries(SERVICE_TYPES)
|
|
63
|
+
.filter(([, v]) => v.layer === layer)
|
|
64
|
+
.map(([id, v]) => ({ id, name: v.name }));
|
|
65
|
+
|
|
66
|
+
process.stderr.write(`\n ${kleur.bold("Type:")}\n\n`);
|
|
67
|
+
for (var i = 0; i < types.length; i++) {
|
|
68
|
+
process.stderr.write(
|
|
69
|
+
` ${kleur.bold(`[${i + 1}]`)} ${types[i].name}\n`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
process.stderr.write("\n");
|
|
73
|
+
|
|
74
|
+
var typeChoice = await prompt(rl, `Select type [1-${types.length}]: `);
|
|
75
|
+
var typeIdx = parseInt(typeChoice, 10) - 1;
|
|
76
|
+
if (isNaN(typeIdx) || typeIdx < 0 || typeIdx >= types.length) {
|
|
77
|
+
rl.close();
|
|
78
|
+
fatal("Invalid selection.");
|
|
79
|
+
}
|
|
80
|
+
var serviceType = types[typeIdx].id;
|
|
81
|
+
|
|
82
|
+
// 3. Connection details (SlicerVM-specific)
|
|
83
|
+
var config = { layer, type: serviceType };
|
|
84
|
+
|
|
85
|
+
if (serviceType === "slicervm") {
|
|
86
|
+
process.stderr.write(`\n ${kleur.bold("Connection mode")}\n\n`);
|
|
87
|
+
process.stderr.write(` ${kleur.bold("[1]")} Unix socket (local dev)\n`);
|
|
88
|
+
process.stderr.write(` ${kleur.bold("[2]")} HTTP API (remote)\n\n`);
|
|
89
|
+
|
|
90
|
+
var modeChoice = await prompt(rl, "Select [1-2]: ");
|
|
91
|
+
var useSocket = modeChoice.trim() === "1";
|
|
92
|
+
|
|
93
|
+
if (useSocket) {
|
|
94
|
+
var defaultSocket = "/var/run/slicer/slicer.sock";
|
|
95
|
+
var socketPath = await prompt(rl, `Socket path [${defaultSocket}]: `);
|
|
96
|
+
socketPath = (socketPath || "").trim() || defaultSocket;
|
|
97
|
+
config.socketPath = socketPath;
|
|
98
|
+
} else {
|
|
99
|
+
var apiUrl = await prompt(
|
|
100
|
+
rl,
|
|
101
|
+
"Slicer API URL (e.g. https://slicer.example.com:8080): "
|
|
102
|
+
);
|
|
103
|
+
apiUrl = (apiUrl || "").trim().replace(/\/+$/, "");
|
|
104
|
+
if (!apiUrl) {
|
|
105
|
+
rl.close();
|
|
106
|
+
fatal("No API URL provided.");
|
|
107
|
+
}
|
|
108
|
+
config.apiUrl = apiUrl;
|
|
109
|
+
|
|
110
|
+
var token = await prompt(rl, "API token: ");
|
|
111
|
+
token = (token || "").trim();
|
|
112
|
+
if (!token) {
|
|
113
|
+
rl.close();
|
|
114
|
+
fatal("No token provided.");
|
|
115
|
+
}
|
|
116
|
+
config.token = token;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
var hostGroup = await prompt(rl, "Host group [apps]: ");
|
|
120
|
+
config.hostGroup = (hostGroup || "").trim() || "apps";
|
|
121
|
+
|
|
122
|
+
var baseDomain = await prompt(
|
|
123
|
+
rl,
|
|
124
|
+
"Base domain (e.g. apps.example.com) [localhost]: "
|
|
125
|
+
);
|
|
126
|
+
config.baseDomain = (baseDomain || "").trim() || "localhost";
|
|
127
|
+
|
|
128
|
+
// 4. Verify connection
|
|
129
|
+
process.stderr.write("\nVerifying...\n");
|
|
130
|
+
var verifyCfg = normalizeServiceConfig(config);
|
|
131
|
+
try {
|
|
132
|
+
await verifyConnection(verifyCfg);
|
|
133
|
+
} catch (e) {
|
|
134
|
+
rl.close();
|
|
135
|
+
fatal("Connection failed.", e.message);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (useSocket) {
|
|
139
|
+
process.stderr.write(` Socket: ${fmt.bold(config.socketPath)}\n`);
|
|
140
|
+
} else {
|
|
141
|
+
process.stderr.write(` API: ${fmt.bold(config.apiUrl)}\n`);
|
|
142
|
+
}
|
|
143
|
+
process.stderr.write(` Host group: ${fmt.dim(config.hostGroup)}\n`);
|
|
144
|
+
process.stderr.write(` Base domain: ${fmt.dim(config.baseDomain)}\n`);
|
|
145
|
+
} else if (serviceType === "neon") {
|
|
146
|
+
process.stderr.write(`\n ${kleur.bold("Neon API key")}\n\n`);
|
|
147
|
+
process.stderr.write(
|
|
148
|
+
` ${fmt.dim("Get your API key at https://console.neon.tech/app/settings/api-keys")}\n\n`
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
var apiKey = await prompt(rl, "API key: ");
|
|
152
|
+
apiKey = (apiKey || "").trim();
|
|
153
|
+
if (!apiKey) {
|
|
154
|
+
rl.close();
|
|
155
|
+
fatal("No API key provided.");
|
|
156
|
+
}
|
|
157
|
+
config.apiKey = apiKey;
|
|
158
|
+
|
|
159
|
+
// Verify connection
|
|
160
|
+
process.stderr.write("\nVerifying...\n");
|
|
161
|
+
try {
|
|
162
|
+
var projects = await verifyApiKey(apiKey);
|
|
163
|
+
process.stderr.write(
|
|
164
|
+
` Authenticated. ${projects.length} existing project${projects.length === 1 ? "" : "s"}.\n`
|
|
165
|
+
);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
rl.close();
|
|
168
|
+
fatal("Authentication failed.", e.message);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 5. Auto-name if not provided
|
|
173
|
+
if (!name) {
|
|
174
|
+
var existing = getRegisteredServices().filter((a) => a.type === serviceType);
|
|
175
|
+
if (existing.length === 0) {
|
|
176
|
+
name = serviceType;
|
|
177
|
+
} else {
|
|
178
|
+
name = `${serviceType}-${existing.length + 1}`;
|
|
179
|
+
}
|
|
180
|
+
var inputName = await prompt(rl, `Service name [${name}]: `);
|
|
181
|
+
name = (inputName || "").trim() || name;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check for existing
|
|
185
|
+
if (tryGetServiceConfig(name)) {
|
|
186
|
+
var overwrite = await prompt(
|
|
187
|
+
rl,
|
|
188
|
+
`Service '${name}' already exists. Overwrite? [y/N] `
|
|
189
|
+
);
|
|
190
|
+
if (!overwrite.match(/^y(es)?$/i)) {
|
|
191
|
+
rl.close();
|
|
192
|
+
process.stderr.write("Cancelled.\n");
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
rl.close();
|
|
198
|
+
|
|
199
|
+
// 6. Save
|
|
200
|
+
saveServiceConfig(name, config);
|
|
201
|
+
|
|
202
|
+
success(`Service ${fmt.bold(name)} registered!`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function serviceRemove(name) {
|
|
206
|
+
if (!name) {
|
|
207
|
+
fatal("Usage: relight service remove <name>");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!tryGetServiceConfig(name)) {
|
|
211
|
+
fatal(`Service '${name}' not found.`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
var rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
215
|
+
var answer = await new Promise((resolve) =>
|
|
216
|
+
rl.question(`Remove service '${name}'? [y/N] `, resolve)
|
|
217
|
+
);
|
|
218
|
+
rl.close();
|
|
219
|
+
|
|
220
|
+
if (!answer.match(/^y(es)?$/i)) {
|
|
221
|
+
process.stderr.write("Cancelled.\n");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
removeServiceConfig(name);
|
|
226
|
+
success(`Service ${fmt.bold(name)} removed.`);
|
|
227
|
+
}
|