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.
- package/README.md +77 -34
- package/package.json +12 -4
- package/src/cli.js +305 -1
- package/src/commands/apps.js +128 -0
- package/src/commands/auth.js +75 -4
- package/src/commands/config.js +282 -0
- package/src/commands/cost.js +593 -0
- package/src/commands/db.js +531 -0
- package/src/commands/deploy.js +298 -0
- package/src/commands/doctor.js +41 -9
- 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/lib/clouds/aws.js +309 -35
- package/src/lib/clouds/cf.js +401 -2
- package/src/lib/clouds/gcp.js +234 -3
- package/src/lib/clouds/slicervm.js +139 -0
- package/src/lib/config.js +40 -0
- package/src/lib/docker.js +34 -0
- package/src/lib/link.js +20 -5
- package/src/lib/providers/aws/app.js +481 -0
- package/src/lib/providers/aws/db.js +513 -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 +279 -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 +457 -0
- package/src/lib/providers/gcp/dns.js +166 -0
- package/src/lib/providers/gcp/registry.js +30 -0
- package/src/lib/providers/resolve.js +49 -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,139 @@
|
|
|
1
|
+
// SlicerVM API client
|
|
2
|
+
// Supports two connection modes:
|
|
3
|
+
// - Unix socket: cfg.socketPath (local dev, no auth)
|
|
4
|
+
// - HTTP: cfg.apiUrl + cfg.apiToken (remote, bearer token auth)
|
|
5
|
+
|
|
6
|
+
import http from "node:http";
|
|
7
|
+
|
|
8
|
+
function socketRequest(socketPath, method, path, body, headers) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
var opts = { socketPath, method, path, headers: headers || {} };
|
|
11
|
+
var req = http.request(opts, (res) => {
|
|
12
|
+
var chunks = [];
|
|
13
|
+
res.on("data", (c) => chunks.push(c));
|
|
14
|
+
res.on("end", () => {
|
|
15
|
+
resolve({
|
|
16
|
+
ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
17
|
+
status: res.statusCode,
|
|
18
|
+
headers: res.headers,
|
|
19
|
+
text: () => Promise.resolve(Buffer.concat(chunks).toString()),
|
|
20
|
+
json: () => Promise.resolve(JSON.parse(Buffer.concat(chunks).toString())),
|
|
21
|
+
body: null,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
req.on("error", reject);
|
|
26
|
+
if (body) req.write(body);
|
|
27
|
+
req.end();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function slicerFetch(cfg, method, path, body, opts = {}) {
|
|
32
|
+
var headers = {};
|
|
33
|
+
|
|
34
|
+
if (cfg.apiToken) {
|
|
35
|
+
headers.Authorization = `Bearer ${cfg.apiToken}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
var rawBody = body;
|
|
39
|
+
if (opts.contentType) {
|
|
40
|
+
headers["Content-Type"] = opts.contentType;
|
|
41
|
+
} else if (body && typeof body === "object" && !Buffer.isBuffer(body)) {
|
|
42
|
+
headers["Content-Type"] = "application/json";
|
|
43
|
+
rawBody = JSON.stringify(body);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
var res;
|
|
47
|
+
if (cfg.socketPath) {
|
|
48
|
+
res = await socketRequest(cfg.socketPath, method, path, method === "GET" ? undefined : rawBody, headers);
|
|
49
|
+
} else {
|
|
50
|
+
res = await fetch(`${cfg.apiUrl}${path}`, {
|
|
51
|
+
method,
|
|
52
|
+
headers,
|
|
53
|
+
body: method === "GET" ? undefined : rawBody,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
var text = await res.text();
|
|
59
|
+
throw new Error(`Slicer API ${method} ${path}: ${res.status} ${text}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
var ct = (res.headers instanceof Headers)
|
|
63
|
+
? res.headers.get("content-type") || ""
|
|
64
|
+
: res.headers["content-type"] || "";
|
|
65
|
+
|
|
66
|
+
if (ct.includes("application/json")) {
|
|
67
|
+
return res.json();
|
|
68
|
+
}
|
|
69
|
+
if (opts.stream) {
|
|
70
|
+
return res;
|
|
71
|
+
}
|
|
72
|
+
return res.text();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- Nodes ---
|
|
76
|
+
|
|
77
|
+
export async function listNodes(cfg) {
|
|
78
|
+
return slicerFetch(cfg, "GET", "/nodes");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function createNode(cfg, hostGroup, opts = {}) {
|
|
82
|
+
return slicerFetch(cfg, "POST", `/hostgroup/${hostGroup}/nodes`, {
|
|
83
|
+
tags: opts.tags || [],
|
|
84
|
+
vcpu: opts.vcpu,
|
|
85
|
+
memory: opts.memory,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function deleteNode(cfg, hostGroup, hostname) {
|
|
90
|
+
return slicerFetch(cfg, "DELETE", `/hostgroup/${hostGroup}/nodes/${hostname}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- VM lifecycle ---
|
|
94
|
+
|
|
95
|
+
export async function resumeVM(cfg, hostname) {
|
|
96
|
+
return slicerFetch(cfg, "POST", `/vm/${hostname}/resume`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function pauseVM(cfg, hostname) {
|
|
100
|
+
return slicerFetch(cfg, "POST", `/vm/${hostname}/pause`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function healthCheck(cfg, hostname) {
|
|
104
|
+
return slicerFetch(cfg, "GET", `/vm/${hostname}/health`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- Exec ---
|
|
108
|
+
|
|
109
|
+
export async function execInVM(cfg, hostname, cmd, args, opts = {}) {
|
|
110
|
+
var qs = new URLSearchParams();
|
|
111
|
+
qs.set("cmd", cmd);
|
|
112
|
+
for (var arg of (args || [])) {
|
|
113
|
+
qs.append("args", arg);
|
|
114
|
+
}
|
|
115
|
+
if (opts.uid !== undefined) qs.set("uid", String(opts.uid));
|
|
116
|
+
if (opts.gid !== undefined) qs.set("gid", String(opts.gid));
|
|
117
|
+
if (opts.workdir) qs.set("cwd", opts.workdir);
|
|
118
|
+
return slicerFetch(cfg, "POST", `/vm/${hostname}/exec?${qs}`, null, {
|
|
119
|
+
stream: opts.stream,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- File upload ---
|
|
124
|
+
|
|
125
|
+
export async function uploadToVM(cfg, hostname, path, tarBuffer, opts = {}) {
|
|
126
|
+
var qs = new URLSearchParams({ path });
|
|
127
|
+
if (opts.uid !== undefined) qs.set("uid", String(opts.uid));
|
|
128
|
+
if (opts.gid !== undefined) qs.set("gid", String(opts.gid));
|
|
129
|
+
if (opts.mode) qs.set("mode", opts.mode);
|
|
130
|
+
return slicerFetch(cfg, "POST", `/vm/${hostname}/cp?${qs}`, tarBuffer, {
|
|
131
|
+
contentType: "application/x-tar",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- Auth verification ---
|
|
136
|
+
|
|
137
|
+
export async function verifyConnection(cfg) {
|
|
138
|
+
return listNodes(cfg);
|
|
139
|
+
}
|
package/src/lib/config.js
CHANGED
|
@@ -11,6 +11,7 @@ export var CLOUD_NAMES = {
|
|
|
11
11
|
cf: "Cloudflare",
|
|
12
12
|
gcp: "GCP",
|
|
13
13
|
aws: "AWS",
|
|
14
|
+
slicervm: "SlicerVM",
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
export var CLOUD_IDS = Object.keys(CLOUD_NAMES);
|
|
@@ -56,3 +57,42 @@ export function getAuthenticatedClouds() {
|
|
|
56
57
|
(id) => config.clouds[id] && Object.keys(config.clouds[id]).length > 0
|
|
57
58
|
);
|
|
58
59
|
}
|
|
60
|
+
|
|
61
|
+
export function getDefaultCloud() {
|
|
62
|
+
var config = tryGetConfig();
|
|
63
|
+
if (!config) return null;
|
|
64
|
+
return config.default_cloud || null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function resolveCloudConfig(cloudId) {
|
|
68
|
+
var config = getConfig();
|
|
69
|
+
var cloud = config.clouds && config.clouds[cloudId];
|
|
70
|
+
if (!cloud) {
|
|
71
|
+
console.error(
|
|
72
|
+
`Not authenticated with ${CLOUD_NAMES[cloudId] || cloudId}. Run \`relight auth --cloud ${cloudId}\` first.`
|
|
73
|
+
);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Return a normalized config object that providers can use
|
|
78
|
+
if (cloudId === "cf") {
|
|
79
|
+
return { accountId: cloud.accountId, apiToken: cloud.token };
|
|
80
|
+
}
|
|
81
|
+
if (cloudId === "gcp") {
|
|
82
|
+
return { clientEmail: cloud.clientEmail, privateKey: cloud.privateKey, project: cloud.project };
|
|
83
|
+
}
|
|
84
|
+
if (cloudId === "aws") {
|
|
85
|
+
return { accessKeyId: cloud.accessKeyId, secretAccessKey: cloud.secretAccessKey, region: cloud.region };
|
|
86
|
+
}
|
|
87
|
+
if (cloudId === "slicervm") {
|
|
88
|
+
var slicerCfg = { hostGroup: cloud.hostGroup, baseDomain: cloud.baseDomain };
|
|
89
|
+
if (cloud.socketPath) {
|
|
90
|
+
slicerCfg.socketPath = cloud.socketPath;
|
|
91
|
+
} else {
|
|
92
|
+
slicerCfg.apiUrl = cloud.apiUrl;
|
|
93
|
+
slicerCfg.apiToken = cloud.token;
|
|
94
|
+
}
|
|
95
|
+
return slicerCfg;
|
|
96
|
+
}
|
|
97
|
+
return cloud;
|
|
98
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
|
|
3
|
+
function ensureDocker() {
|
|
4
|
+
try {
|
|
5
|
+
execSync("docker version", { stdio: "pipe" });
|
|
6
|
+
} catch {
|
|
7
|
+
console.error("Docker is not running. Install and start Docker first.");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function dockerBuild(contextPath, tag, opts = {}) {
|
|
13
|
+
ensureDocker();
|
|
14
|
+
var platform = opts.platform || "linux/amd64";
|
|
15
|
+
execSync(
|
|
16
|
+
`docker build --platform ${platform} --provenance=false -t ${tag} ${contextPath}`,
|
|
17
|
+
{ stdio: "pipe" }
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function dockerTag(source, target) {
|
|
22
|
+
execSync(`docker tag ${source} ${target}`, { stdio: "pipe" });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function dockerPush(tag) {
|
|
26
|
+
execSync(`docker push ${tag}`, { stdio: "pipe" });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function dockerLogin(registry, username, password) {
|
|
30
|
+
execSync(
|
|
31
|
+
`docker login --password-stdin --username ${username} ${registry}`,
|
|
32
|
+
{ input: password, stdio: "pipe" }
|
|
33
|
+
);
|
|
34
|
+
}
|
package/src/lib/link.js
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
2
|
+
import YAML from "yaml";
|
|
2
3
|
import { fatal, fmt } from "./output.js";
|
|
3
4
|
|
|
4
|
-
var LINK_FILE = ".relight";
|
|
5
|
+
var LINK_FILE = ".relight.yaml";
|
|
5
6
|
|
|
6
7
|
export function readLink() {
|
|
7
8
|
try {
|
|
8
|
-
var
|
|
9
|
-
|
|
9
|
+
var raw = readFileSync(LINK_FILE, "utf-8");
|
|
10
|
+
// Support both YAML and legacy JSON
|
|
11
|
+
return YAML.parse(raw);
|
|
10
12
|
} catch {
|
|
11
13
|
return null;
|
|
12
14
|
}
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
export function linkApp(name, cloud) {
|
|
16
|
-
|
|
17
|
+
export function linkApp(name, cloud, dns, db) {
|
|
18
|
+
var data = { app: name, cloud };
|
|
19
|
+
if (dns && dns !== cloud) data.dns = dns;
|
|
20
|
+
if (db && db !== cloud) data.db = db;
|
|
21
|
+
writeFileSync(LINK_FILE, YAML.stringify(data));
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
export function unlinkApp() {
|
|
@@ -38,3 +43,13 @@ export function resolveCloud(cloud) {
|
|
|
38
43
|
if (linked && linked.cloud) return linked.cloud;
|
|
39
44
|
return null;
|
|
40
45
|
}
|
|
46
|
+
|
|
47
|
+
export function resolveDns() {
|
|
48
|
+
var linked = readLink();
|
|
49
|
+
return linked?.dns || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resolveDb() {
|
|
53
|
+
var linked = readLink();
|
|
54
|
+
return linked?.db || null;
|
|
55
|
+
}
|