@kvbox/cli 0.5.19 → 0.5.21
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/dist/kvbox.js +691 -116
- package/dist/kvbox.js.map +1 -1
- package/package.json +1 -1
package/dist/kvbox.js
CHANGED
|
@@ -26,10 +26,10 @@ function json(value) {
|
|
|
26
26
|
}
|
|
27
27
|
var ANSI = /\x1b\[[0-9;]*m/g;
|
|
28
28
|
var width = (s) => s.replace(ANSI, "").length;
|
|
29
|
-
function table(
|
|
30
|
-
if (
|
|
31
|
-
const cols = columns ?? Object.keys(
|
|
32
|
-
const cells =
|
|
29
|
+
function table(rows2, columns) {
|
|
30
|
+
if (rows2.length === 0) return;
|
|
31
|
+
const cols = columns ?? Object.keys(rows2[0]);
|
|
32
|
+
const cells = rows2.map((r) => cols.map((c) => String(r[c] ?? "")));
|
|
33
33
|
const widths = cols.map((c, i) => Math.max(c.length, ...cells.map((r) => width(r[i]))));
|
|
34
34
|
const line = (parts) => parts.map((p, i) => p + " ".repeat(Math.max(0, widths[i] - width(p)))).join(" ").trimEnd();
|
|
35
35
|
process.stdout.write(color.dim(line(cols.map((c) => c.toUpperCase()))) + "\n");
|
|
@@ -47,8 +47,8 @@ async function confirm(question, yes) {
|
|
|
47
47
|
warn("refusing without -y when stdin is not a terminal");
|
|
48
48
|
return false;
|
|
49
49
|
}
|
|
50
|
-
const { createInterface:
|
|
51
|
-
const rl =
|
|
50
|
+
const { createInterface: createInterface3 } = await import("readline/promises");
|
|
51
|
+
const rl = createInterface3({ input: process.stdin, output: process.stderr });
|
|
52
52
|
try {
|
|
53
53
|
const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
|
|
54
54
|
return answer === "y" || answer === "yes";
|
|
@@ -108,14 +108,14 @@ function slugOf(owner) {
|
|
|
108
108
|
const at = owner.indexOf("@");
|
|
109
109
|
return at === -1 ? owner : owner.slice(0, at);
|
|
110
110
|
}
|
|
111
|
-
function pickRef(items,
|
|
112
|
-
if (
|
|
113
|
-
const named = items.filter((i) => i.name ===
|
|
111
|
+
function pickRef(items, text2, me2, kind = "box") {
|
|
112
|
+
if (text2.includes("/")) return items.find((i) => text2 === `${slugOf(i.owner)}/${i.name}` || text2 === `${i.owner}/${i.name}`);
|
|
113
|
+
const named = items.filter((i) => i.name === text2);
|
|
114
114
|
const own = named.find((i) => slugOf(i.owner) === me2);
|
|
115
115
|
if (own) return own;
|
|
116
116
|
const orgs = me2 === void 0 ? named : named.filter((i) => i.ownerKind !== "user");
|
|
117
117
|
if (orgs.length <= 1) return orgs[0];
|
|
118
|
-
throw new AmbiguousRefError(kind,
|
|
118
|
+
throw new AmbiguousRefError(kind, text2, orgs.map((i) => slugOf(i.owner)));
|
|
119
119
|
}
|
|
120
120
|
function shortRef(ref, owner) {
|
|
121
121
|
return ref.startsWith(`${owner}/`) ? ref.slice(owner.length + 1) : ref;
|
|
@@ -149,7 +149,7 @@ async function spin(message, work, opts = {}) {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
// src/program.ts
|
|
152
|
-
import { Command as
|
|
152
|
+
import { Command as Command50 } from "commander";
|
|
153
153
|
|
|
154
154
|
// ../shared/src/version.ts
|
|
155
155
|
import { createRequire } from "module";
|
|
@@ -164,7 +164,7 @@ function versionOf(srcFileUrl, injected) {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
// src/version.ts
|
|
167
|
-
var VERSION = versionOf(import.meta.url, true ? "0.5.
|
|
167
|
+
var VERSION = versionOf(import.meta.url, true ? "0.5.21" : void 0);
|
|
168
168
|
|
|
169
169
|
// src/commands/vm/index.ts
|
|
170
170
|
import { Command as Command25 } from "commander";
|
|
@@ -271,15 +271,15 @@ var SessionSchema = z.object({
|
|
|
271
271
|
sshCertPrincipals: z.array(z.string()).optional()
|
|
272
272
|
});
|
|
273
273
|
async function readSession(p = paths()) {
|
|
274
|
-
let
|
|
274
|
+
let text2;
|
|
275
275
|
try {
|
|
276
|
-
|
|
276
|
+
text2 = await readFile(p.cli, "utf8");
|
|
277
277
|
} catch (err) {
|
|
278
278
|
if (err.code === "ENOENT") return null;
|
|
279
279
|
throw err;
|
|
280
280
|
}
|
|
281
281
|
try {
|
|
282
|
-
return SessionSchema.parse(JSON.parse(
|
|
282
|
+
return SessionSchema.parse(JSON.parse(text2));
|
|
283
283
|
} catch {
|
|
284
284
|
throw new KvboxError(`${p.cli} is unreadable; run: kvbox logout (then log in again)`);
|
|
285
285
|
}
|
|
@@ -370,13 +370,14 @@ import { z as z2 } from "zod";
|
|
|
370
370
|
|
|
371
371
|
// ../shared/src/sizes.ts
|
|
372
372
|
var SIZES = {
|
|
373
|
+
xs: { vcpu: 1, memMib: 2048, diskGib: 8 },
|
|
373
374
|
s: { vcpu: 2, memMib: 4096, diskGib: 8 },
|
|
374
375
|
m: { vcpu: 4, memMib: 8192, diskGib: 16 },
|
|
375
376
|
l: { vcpu: 8, memMib: 16384, diskGib: 32 }
|
|
376
377
|
};
|
|
377
378
|
var SIZE_NAMES = Object.keys(SIZES);
|
|
378
|
-
function parseMib(
|
|
379
|
-
const m = /^\s*(\d+(?:\.\d+)?)\s*([gm]i?b?)?\s*$/i.exec(
|
|
379
|
+
function parseMib(text2) {
|
|
380
|
+
const m = /^\s*(\d+(?:\.\d+)?)\s*([gm]i?b?)?\s*$/i.exec(text2);
|
|
380
381
|
if (!m) return void 0;
|
|
381
382
|
const n = Number(m[1]);
|
|
382
383
|
const unit = (m[2] ?? "g").toLowerCase();
|
|
@@ -810,13 +811,13 @@ function serverLauncher(opts = {}) {
|
|
|
810
811
|
return { cmd: "sg", args: [group, "-c", "kvbox-server start"], viaGroup: group };
|
|
811
812
|
}
|
|
812
813
|
function groupWithMember(gid, user, groupFile) {
|
|
813
|
-
let
|
|
814
|
+
let text2;
|
|
814
815
|
try {
|
|
815
|
-
|
|
816
|
+
text2 = readFileSync(groupFile, "utf8");
|
|
816
817
|
} catch {
|
|
817
818
|
return void 0;
|
|
818
819
|
}
|
|
819
|
-
for (const line of
|
|
820
|
+
for (const line of text2.split("\n")) {
|
|
820
821
|
const [name, , id, members = ""] = line.split(":");
|
|
821
822
|
if (name === void 0 || id === void 0 || Number.parseInt(id, 10) !== gid) continue;
|
|
822
823
|
return members.split(",").includes(user) ? name : void 0;
|
|
@@ -831,13 +832,13 @@ import { resolve } from "path";
|
|
|
831
832
|
var UNIT = "kvbox-server";
|
|
832
833
|
var UNIT_FILE = `/etc/systemd/system/${UNIT}.service`;
|
|
833
834
|
function hasUnitFor(p, file = UNIT_FILE) {
|
|
834
|
-
let
|
|
835
|
+
let text2;
|
|
835
836
|
try {
|
|
836
|
-
|
|
837
|
+
text2 = readFileSync2(file, "utf8");
|
|
837
838
|
} catch {
|
|
838
839
|
return false;
|
|
839
840
|
}
|
|
840
|
-
const home = /^Environment="?KVBOX_HOME=([^"\n]+)"?$/m.exec(
|
|
841
|
+
const home = /^Environment="?KVBOX_HOME=([^"\n]+)"?$/m.exec(text2)?.[1];
|
|
841
842
|
return home !== void 0 && resolve(home) === resolve(p.home);
|
|
842
843
|
}
|
|
843
844
|
function startUnitProcess() {
|
|
@@ -912,10 +913,10 @@ var ServerClient = class {
|
|
|
912
913
|
}
|
|
913
914
|
throw new KvboxError(cloudDown(this.target.url, String(code)));
|
|
914
915
|
}
|
|
915
|
-
const
|
|
916
|
+
const text2 = await res.body.text();
|
|
916
917
|
const where = this.target.kind === "cloud" ? hostOf(this.target.url) : "kvbox-server";
|
|
917
918
|
if (res.statusCode >= 300) {
|
|
918
|
-
const parsed = ApiErrorSchema.safeParse(safeJson(
|
|
919
|
+
const parsed = ApiErrorSchema.safeParse(safeJson(text2) ?? {});
|
|
919
920
|
if (!parsed.success) throw new KvboxError(`${where} answered ${res.statusCode}`);
|
|
920
921
|
if (this.target.kind === "cloud" && this.target.token !== "") {
|
|
921
922
|
if (res.statusCode === 401) throw new KvboxError("session expired: kvbox login");
|
|
@@ -925,8 +926,8 @@ var ServerClient = class {
|
|
|
925
926
|
}
|
|
926
927
|
throw new KvboxError(parsed.data.error, res.statusCode === 501 ? 2 : 1);
|
|
927
928
|
}
|
|
928
|
-
if (
|
|
929
|
-
const json2 = safeJson(
|
|
929
|
+
if (text2 === "") return schema.parse(void 0);
|
|
930
|
+
const json2 = safeJson(text2);
|
|
930
931
|
if (json2 === void 0) throw new KvboxError(`${where} answered ${res.statusCode} with something that is not JSON`);
|
|
931
932
|
return schema.parse(json2);
|
|
932
933
|
}
|
|
@@ -1097,9 +1098,9 @@ function cloudDown(url, code) {
|
|
|
1097
1098
|
const hint = host.endsWith("kvbox.sh") ? "check status.kvbox.sh" : "use --local for this machine, or kvbox logout";
|
|
1098
1099
|
return `${host} is not available or is down (${code}). You are logged in there, so kvbox talks to it instead of this machine; ${hint}`;
|
|
1099
1100
|
}
|
|
1100
|
-
function safeJson(
|
|
1101
|
+
function safeJson(text2) {
|
|
1101
1102
|
try {
|
|
1102
|
-
return JSON.parse(
|
|
1103
|
+
return JSON.parse(text2);
|
|
1103
1104
|
} catch {
|
|
1104
1105
|
return void 0;
|
|
1105
1106
|
}
|
|
@@ -1119,10 +1120,10 @@ async function wakeUp(client, box, name) {
|
|
|
1119
1120
|
return box;
|
|
1120
1121
|
}
|
|
1121
1122
|
}
|
|
1122
|
-
function boxesFor(client,
|
|
1123
|
-
const slash =
|
|
1123
|
+
function boxesFor(client, text2) {
|
|
1124
|
+
const slash = text2.indexOf("/");
|
|
1124
1125
|
if (slash === -1 || client.target.kind !== "cloud") return client.listMachines();
|
|
1125
|
-
return client.listMachines({ org:
|
|
1126
|
+
return client.listMachines({ org: text2.slice(0, slash).split("@")[0] });
|
|
1126
1127
|
}
|
|
1127
1128
|
var me = (client) => client.target.kind === "cloud" ? client.target.user : void 0;
|
|
1128
1129
|
async function clientFor(opts, client = {}) {
|
|
@@ -1154,17 +1155,17 @@ function sshArgs(ep, p = paths()) {
|
|
|
1154
1155
|
async function waitForSsh(ep, timeoutMs = 3e4) {
|
|
1155
1156
|
const { connect } = await import("net");
|
|
1156
1157
|
const deadline = Date.now() + timeoutMs;
|
|
1157
|
-
const attempt = () => new Promise((
|
|
1158
|
+
const attempt = () => new Promise((resolve3) => {
|
|
1158
1159
|
const sock = connect({ host: ep.host, port: ep.port, timeout: 1e3 });
|
|
1159
1160
|
sock.once("connect", () => {
|
|
1160
1161
|
sock.destroy();
|
|
1161
|
-
|
|
1162
|
+
resolve3(true);
|
|
1162
1163
|
});
|
|
1163
1164
|
sock.once("timeout", () => {
|
|
1164
1165
|
sock.destroy();
|
|
1165
|
-
|
|
1166
|
+
resolve3(false);
|
|
1166
1167
|
});
|
|
1167
|
-
sock.once("error", () =>
|
|
1168
|
+
sock.once("error", () => resolve3(false));
|
|
1168
1169
|
});
|
|
1169
1170
|
while (Date.now() < deadline) {
|
|
1170
1171
|
if (await attempt()) return;
|
|
@@ -1174,9 +1175,9 @@ async function waitForSsh(ep, timeoutMs = 3e4) {
|
|
|
1174
1175
|
}
|
|
1175
1176
|
var NOT_A_HOST_HINT = "kvbox-server is not installed, so this machine is not a kvbox host. Set it up with install-server.sh, or log in to the cloud with: kvbox login";
|
|
1176
1177
|
async function lastErrorLine(logPath, from = 0) {
|
|
1177
|
-
const { readFile:
|
|
1178
|
-
const
|
|
1179
|
-
const lines =
|
|
1178
|
+
const { readFile: readFile8 } = await import("fs/promises");
|
|
1179
|
+
const text2 = (await readFile8(logPath).catch(() => Buffer.alloc(0))).subarray(from).toString("utf8");
|
|
1180
|
+
const lines = text2.trimEnd().split("\n").slice(-20).reverse();
|
|
1180
1181
|
return lines.find((l) => l.startsWith("error: "))?.slice("error: ".length);
|
|
1181
1182
|
}
|
|
1182
1183
|
async function startLocalServer(p = paths(), timeoutMs = 5e3) {
|
|
@@ -1276,10 +1277,10 @@ function autoHibernateText(sec) {
|
|
|
1276
1277
|
return sec ? `hibernates after ${duration(sec)} without network traffic (kvbox connect wakes it)` : "never hibernates on its own";
|
|
1277
1278
|
}
|
|
1278
1279
|
var UNIT_SEC = { m: 60, h: 3600, d: 86400 };
|
|
1279
|
-
function parseIdleTimeout(
|
|
1280
|
-
const t =
|
|
1280
|
+
function parseIdleTimeout(text2) {
|
|
1281
|
+
const t = text2.trim().toLowerCase();
|
|
1281
1282
|
if (t === "off" || t === "never" || t === "0") return 0;
|
|
1282
|
-
if (!/^(\d+[mhd])+$/.test(t)) throw new KvboxError(`--auto-hibernate ${
|
|
1283
|
+
if (!/^(\d+[mhd])+$/.test(t)) throw new KvboxError(`--auto-hibernate ${text2}: give a duration like 30m, 4h or 1d, or off`);
|
|
1283
1284
|
let sec = 0;
|
|
1284
1285
|
for (const [, n, unit] of t.matchAll(/(\d+)([mhd])/g)) sec += Number(n) * UNIT_SEC[unit];
|
|
1285
1286
|
return sec;
|
|
@@ -1355,12 +1356,12 @@ async function runSsh(ssh, opts) {
|
|
|
1355
1356
|
timedOut = true;
|
|
1356
1357
|
child.kill("SIGTERM");
|
|
1357
1358
|
}, opts.timeout * 1e3);
|
|
1358
|
-
const status2 = await new Promise((
|
|
1359
|
+
const status2 = await new Promise((resolve3, reject) => {
|
|
1359
1360
|
child.once("error", (err) => reject(new KvboxError(`could not run ssh: ${err.message}`)));
|
|
1360
1361
|
child.once("close", (code, signal) => {
|
|
1361
|
-
if (timedOut)
|
|
1362
|
-
else if (code !== null)
|
|
1363
|
-
else
|
|
1362
|
+
if (timedOut) resolve3(TIMEOUT_EXIT);
|
|
1363
|
+
else if (code !== null) resolve3(code);
|
|
1364
|
+
else resolve3(signal === "SIGINT" ? 130 : 1);
|
|
1364
1365
|
});
|
|
1365
1366
|
}).finally(() => clearTimeout(timer));
|
|
1366
1367
|
return { status: status2, chunks, timedOut };
|
|
@@ -1539,26 +1540,26 @@ var RESERVED_SUBDOMAINS = /* @__PURE__ */ new Set([
|
|
|
1539
1540
|
"support",
|
|
1540
1541
|
"www"
|
|
1541
1542
|
]);
|
|
1542
|
-
function subdomainProblem(
|
|
1543
|
-
if (
|
|
1544
|
-
if (!LABEL_RE.test(
|
|
1545
|
-
if (RESERVED_SUBDOMAINS.has(
|
|
1543
|
+
function subdomainProblem(text2) {
|
|
1544
|
+
if (text2.length < 3 || text2.length > 63) return `"${text2}" must be 3 to 63 characters`;
|
|
1545
|
+
if (!LABEL_RE.test(text2)) return `"${text2}" may hold only lowercase letters, digits and '-', not at either end`;
|
|
1546
|
+
if (RESERVED_SUBDOMAINS.has(text2)) return `"${text2}" is reserved; pick another name`;
|
|
1546
1547
|
return null;
|
|
1547
1548
|
}
|
|
1548
|
-
function routeNameProblem(
|
|
1549
|
-
if (
|
|
1550
|
-
if (!LABEL_RE.test(
|
|
1549
|
+
function routeNameProblem(text2) {
|
|
1550
|
+
if (text2.length < 1 || text2.length > 63) return `"${text2}" must be 1 to 63 characters`;
|
|
1551
|
+
if (!LABEL_RE.test(text2)) return `"${text2}" may hold only lowercase letters, digits and '-', not at either end`;
|
|
1551
1552
|
return null;
|
|
1552
1553
|
}
|
|
1553
|
-
function assertSubdomain(
|
|
1554
|
-
const problem = subdomainProblem(
|
|
1554
|
+
function assertSubdomain(text2) {
|
|
1555
|
+
const problem = subdomainProblem(text2);
|
|
1555
1556
|
if (problem) throw new KvboxError(`subdomain ${problem}`);
|
|
1556
|
-
return
|
|
1557
|
+
return text2;
|
|
1557
1558
|
}
|
|
1558
|
-
function assertRouteName(
|
|
1559
|
-
const problem = routeNameProblem(
|
|
1559
|
+
function assertRouteName(text2) {
|
|
1560
|
+
const problem = routeNameProblem(text2);
|
|
1560
1561
|
if (problem) throw new KvboxError(`name ${problem}`);
|
|
1561
|
-
return
|
|
1562
|
+
return text2;
|
|
1562
1563
|
}
|
|
1563
1564
|
|
|
1564
1565
|
// src/commands/vm/proxy/shared.ts
|
|
@@ -1587,9 +1588,9 @@ function parsePorts(texts) {
|
|
|
1587
1588
|
return n;
|
|
1588
1589
|
});
|
|
1589
1590
|
}
|
|
1590
|
-
async function findBox(client,
|
|
1591
|
-
const found = pickRef(await boxesFor(client,
|
|
1592
|
-
if (!found) throw new KvboxError(`box ${
|
|
1591
|
+
async function findBox(client, text2) {
|
|
1592
|
+
const found = pickRef(await boxesFor(client, text2), text2, me(client));
|
|
1593
|
+
if (!found) throw new KvboxError(`box ${text2}: not found (kvbox ls)`);
|
|
1593
1594
|
return found;
|
|
1594
1595
|
}
|
|
1595
1596
|
|
|
@@ -1649,12 +1650,12 @@ var addCommand = new Command9("add").description("publish box ports on the inter
|
|
|
1649
1650
|
return;
|
|
1650
1651
|
}
|
|
1651
1652
|
const updated = await spin(`publishing ${guestPorts.join(", ")} on ${name}\u2026`, client.addPorts(box, { ports: guestPorts.map((guestPort) => ({ guestPort, proto })) }));
|
|
1652
|
-
const
|
|
1653
|
+
const rows2 = rowsOf(updated, user, (f) => f.proto === proto && guestPorts.includes(f.guestPort));
|
|
1653
1654
|
if (opts.json) {
|
|
1654
|
-
json(
|
|
1655
|
+
json(rows2);
|
|
1655
1656
|
return;
|
|
1656
1657
|
}
|
|
1657
|
-
for (const row of
|
|
1658
|
+
for (const row of rows2) info(`${color.green("\u2713")} box ${color.bold(row.box)} port ${row.guestPort} is reachable at ${reaches(row)} (${row.proto})`);
|
|
1658
1659
|
if (updated.status !== "running") info(` box ${name} is ${updated.status}: the ports open when it runs`);
|
|
1659
1660
|
} finally {
|
|
1660
1661
|
await client.close();
|
|
@@ -1681,16 +1682,16 @@ var lsCommand2 = new Command10("ls").description("list the box ports published o
|
|
|
1681
1682
|
]);
|
|
1682
1683
|
const shown = new Set(boxes.map((b) => displayName(b, user)));
|
|
1683
1684
|
const https = subs.flatMap((s) => httpsRowsOf(s, user)).filter((r) => shown.has(r.box));
|
|
1684
|
-
const
|
|
1685
|
+
const rows2 = [...boxes.flatMap((b) => rowsOf(b, user)), ...https].sort((a, b) => a.box.localeCompare(b.box) || a.guestPort - b.guestPort);
|
|
1685
1686
|
if (opts.json) {
|
|
1686
|
-
json(
|
|
1687
|
+
json(rows2);
|
|
1687
1688
|
return;
|
|
1688
1689
|
}
|
|
1689
|
-
if (
|
|
1690
|
+
if (rows2.length === 0) {
|
|
1690
1691
|
info(name === void 0 ? "no published ports (kvbox vm proxy add <box> <port>)" : `box ${name} publishes no ports (kvbox vm proxy add ${name} <port>)`);
|
|
1691
1692
|
return;
|
|
1692
1693
|
}
|
|
1693
|
-
table(
|
|
1694
|
+
table(rows2.map((r) => ({ box: r.box, port: r.guestPort, reaches: r.proto === "https" ? r.url : reaches(r, true), proto: r.proto })));
|
|
1694
1695
|
} finally {
|
|
1695
1696
|
await client.close();
|
|
1696
1697
|
}
|
|
@@ -1802,19 +1803,19 @@ var rescaleCommand = new Command15("rescale").description("change a box's vCPUs,
|
|
|
1802
1803
|
await client.close();
|
|
1803
1804
|
}
|
|
1804
1805
|
});
|
|
1805
|
-
function parseCount(
|
|
1806
|
-
const n = Number(
|
|
1807
|
-
if (!Number.isInteger(n) || n < 1) throw new KvboxError(`--vcpu ${
|
|
1806
|
+
function parseCount(text2) {
|
|
1807
|
+
const n = Number(text2);
|
|
1808
|
+
if (!Number.isInteger(n) || n < 1) throw new KvboxError(`--vcpu ${text2}: give a whole number of vCPUs`);
|
|
1808
1809
|
return n;
|
|
1809
1810
|
}
|
|
1810
|
-
function parseSize(flag,
|
|
1811
|
-
const mib = parseMib(
|
|
1812
|
-
if (mib === void 0) throw new KvboxError(`${flag} ${
|
|
1811
|
+
function parseSize(flag, text2) {
|
|
1812
|
+
const mib = parseMib(text2);
|
|
1813
|
+
if (mib === void 0) throw new KvboxError(`${flag} ${text2}: give a size like 16GB or 4096MB`);
|
|
1813
1814
|
return mib;
|
|
1814
1815
|
}
|
|
1815
|
-
function parseGib(flag,
|
|
1816
|
-
const mib = parseSize(flag,
|
|
1817
|
-
if (mib % 1024 !== 0) throw new KvboxError(`${flag} ${
|
|
1816
|
+
function parseGib(flag, text2) {
|
|
1817
|
+
const mib = parseSize(flag, text2);
|
|
1818
|
+
if (mib % 1024 !== 0) throw new KvboxError(`${flag} ${text2}: disk sizes are whole GB`);
|
|
1818
1819
|
return mib / 1024;
|
|
1819
1820
|
}
|
|
1820
1821
|
|
|
@@ -2009,7 +2010,7 @@ function money(amount, currency) {
|
|
|
2009
2010
|
}
|
|
2010
2011
|
function block(u) {
|
|
2011
2012
|
const estimated = u.estimatedStops === 0 ? "" : color.dim(` (${u.estimatedStops} stop time${u.estimatedStops === 1 ? "" : "s"} estimated)`);
|
|
2012
|
-
const
|
|
2013
|
+
const rows2 = [
|
|
2013
2014
|
["name", boxName(u.name)],
|
|
2014
2015
|
["status", status(u.status)],
|
|
2015
2016
|
["window", `${day(u.window.from)} to ${day(u.window.to)}`],
|
|
@@ -2021,12 +2022,12 @@ function block(u) {
|
|
|
2021
2022
|
["ram GiB-h", hours(u.ramGibHours)],
|
|
2022
2023
|
["disk GiB-h", hours(u.diskGibHours)]
|
|
2023
2024
|
];
|
|
2024
|
-
if (u.cost !== void 0)
|
|
2025
|
+
if (u.cost !== void 0) rows2.push(["cost", `${money(u.cost.amount, u.cost.currency)} ${color.dim("(estimated)")}`]);
|
|
2025
2026
|
if (u.historyFrom !== null && u.historyFrom > u.window.from) {
|
|
2026
|
-
|
|
2027
|
+
rows2.push(["history", color.dim(`from ${u.historyFrom.slice(0, 16).replace("T", " ")} UTC; the box is older, but its time before that was not recorded`)]);
|
|
2027
2028
|
}
|
|
2028
|
-
const w = Math.max(...
|
|
2029
|
-
return
|
|
2029
|
+
const w = Math.max(...rows2.map(([k]) => k.length));
|
|
2030
|
+
return rows2.map(([k, v]) => `${" ".repeat(w - k.length)}${k}: ${v}`).join("\n") + "\n";
|
|
2030
2031
|
}
|
|
2031
2032
|
|
|
2032
2033
|
// src/commands/vm/wake.ts
|
|
@@ -2088,7 +2089,7 @@ async function restartLocalServer(p = paths()) {
|
|
|
2088
2089
|
return (await startLocalServer(p)).pid;
|
|
2089
2090
|
}
|
|
2090
2091
|
function runKvboxServer(args) {
|
|
2091
|
-
return new Promise((
|
|
2092
|
+
return new Promise((resolve3, reject) => {
|
|
2092
2093
|
const child = spawn3("kvbox-server", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2093
2094
|
let out = "";
|
|
2094
2095
|
let err = "";
|
|
@@ -2098,7 +2099,7 @@ function runKvboxServer(args) {
|
|
|
2098
2099
|
reject(new KvboxError(e.code === "ENOENT" ? NOT_A_HOST_HINT : `could not run kvbox-server: ${e.message}`));
|
|
2099
2100
|
});
|
|
2100
2101
|
child.once("close", (code) => {
|
|
2101
|
-
if (code === 0)
|
|
2102
|
+
if (code === 0) resolve3(out.trimEnd());
|
|
2102
2103
|
else reject(new KvboxError(err.trim().replace(/^error: /, "") || `kvbox-server ${args[0] ?? ""} exited with code ${String(code)}`, code === 2 ? 2 : 1));
|
|
2103
2104
|
});
|
|
2104
2105
|
});
|
|
@@ -2235,9 +2236,133 @@ var serverCommand = new Command33("server").description("run or manage the kvbox
|
|
|
2235
2236
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
2236
2237
|
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
2237
2238
|
import { hostname as osHostname } from "os";
|
|
2238
|
-
import { createInterface } from "readline/promises";
|
|
2239
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
2239
2240
|
import { Writable } from "stream";
|
|
2240
2241
|
import { Command as Command34 } from "commander";
|
|
2242
|
+
|
|
2243
|
+
// src/prompt.ts
|
|
2244
|
+
import { createInterface } from "readline/promises";
|
|
2245
|
+
function interactive() {
|
|
2246
|
+
return process.stdin.isTTY === true && process.stderr.isTTY === true;
|
|
2247
|
+
}
|
|
2248
|
+
var ESC = "\x1B";
|
|
2249
|
+
var ENTER = "\r";
|
|
2250
|
+
var CTRL_C = "";
|
|
2251
|
+
var CTRL_D = "";
|
|
2252
|
+
var cancelled = () => new KvboxError("cancelled", 130);
|
|
2253
|
+
function keysIn(chunk) {
|
|
2254
|
+
const keys = [];
|
|
2255
|
+
for (let i = 0; i < chunk.length; ) {
|
|
2256
|
+
if (chunk[i] === ESC && (chunk[i + 1] === "[" || chunk[i + 1] === "O") && i + 2 < chunk.length) {
|
|
2257
|
+
keys.push(chunk.slice(i, i + 3));
|
|
2258
|
+
i += 3;
|
|
2259
|
+
} else {
|
|
2260
|
+
keys.push(chunk[i]);
|
|
2261
|
+
i += 1;
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
return keys;
|
|
2265
|
+
}
|
|
2266
|
+
var isUp = (k) => k === `${ESC}[A` || k === `${ESC}OA` || k === "k";
|
|
2267
|
+
var isDown = (k) => k === `${ESC}[B` || k === `${ESC}OB` || k === "j";
|
|
2268
|
+
var ANSI2 = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
2269
|
+
function rows(line, columns = process.stderr.columns ?? 80) {
|
|
2270
|
+
const width2 = line.replace(ANSI2, "").length;
|
|
2271
|
+
return Math.max(1, Math.ceil(width2 / Math.max(1, columns)));
|
|
2272
|
+
}
|
|
2273
|
+
function settled(message, answer) {
|
|
2274
|
+
return `${color.green("\u2713")} ${color.bold(message)} ${color.cyan(answer)}`;
|
|
2275
|
+
}
|
|
2276
|
+
async function select(message, choices, initial = 0) {
|
|
2277
|
+
const out = process.stderr;
|
|
2278
|
+
const inp = process.stdin;
|
|
2279
|
+
let index = Math.min(Math.max(initial, 0), choices.length - 1);
|
|
2280
|
+
let drawn = 0;
|
|
2281
|
+
const render = () => {
|
|
2282
|
+
if (drawn > 0) out.write(`${ESC}[${drawn}A`);
|
|
2283
|
+
const lines = [
|
|
2284
|
+
`${color.green("?")} ${color.bold(message)} ${color.dim("\u2191\u2193 to move, enter to pick")}`,
|
|
2285
|
+
...choices.map((c, i) => {
|
|
2286
|
+
const hint = c.hint ? color.dim(` ${c.hint}`) : "";
|
|
2287
|
+
return i === index ? `${color.cyan(`\u276F ${c.label}`)}${hint}` : ` ${c.label}${hint}`;
|
|
2288
|
+
})
|
|
2289
|
+
];
|
|
2290
|
+
out.write(`\r${ESC}[J${lines.join("\n")}
|
|
2291
|
+
`);
|
|
2292
|
+
drawn = lines.reduce((n, l) => n + rows(l), 0);
|
|
2293
|
+
};
|
|
2294
|
+
inp.setRawMode(true);
|
|
2295
|
+
inp.resume();
|
|
2296
|
+
inp.setEncoding("utf8");
|
|
2297
|
+
render();
|
|
2298
|
+
let done = false;
|
|
2299
|
+
try {
|
|
2300
|
+
return await new Promise((resolve3, reject) => {
|
|
2301
|
+
const onData = (chunk) => {
|
|
2302
|
+
for (const key of keysIn(chunk)) {
|
|
2303
|
+
if (isUp(key)) index = (index - 1 + choices.length) % choices.length;
|
|
2304
|
+
else if (isDown(key)) index = (index + 1) % choices.length;
|
|
2305
|
+
else if (key === ENTER || key === "\n") {
|
|
2306
|
+
inp.off("data", onData);
|
|
2307
|
+
done = true;
|
|
2308
|
+
resolve3(choices[index].value);
|
|
2309
|
+
return;
|
|
2310
|
+
} else if (key === CTRL_C || key === CTRL_D) {
|
|
2311
|
+
inp.off("data", onData);
|
|
2312
|
+
reject(cancelled());
|
|
2313
|
+
return;
|
|
2314
|
+
} else continue;
|
|
2315
|
+
render();
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
2318
|
+
inp.on("data", onData);
|
|
2319
|
+
});
|
|
2320
|
+
} finally {
|
|
2321
|
+
inp.setRawMode(false);
|
|
2322
|
+
inp.pause();
|
|
2323
|
+
out.write(`${ESC}[${drawn}A\r${ESC}[J${done ? `${settled(message, choices[index].label)}
|
|
2324
|
+
` : ""}`);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
async function text(message, opts = {}) {
|
|
2328
|
+
const out = process.stderr;
|
|
2329
|
+
for (; ; ) {
|
|
2330
|
+
const hint = opts.default !== void 0 ? ` ${color.dim(`(${opts.default})`)}` : "";
|
|
2331
|
+
const question = `${color.green("?")} ${color.bold(message)}${hint} ${color.dim("\u203A")} `;
|
|
2332
|
+
const rl = createInterface({ input: process.stdin, output: out });
|
|
2333
|
+
const raw = (await askLine(rl, question)).trim();
|
|
2334
|
+
const answer = raw === "" && opts.default !== void 0 ? opts.default : raw;
|
|
2335
|
+
const problem = opts.validate?.(answer);
|
|
2336
|
+
out.write(`${ESC}[${rows(question + raw)}A\r${ESC}[J`);
|
|
2337
|
+
if (problem === void 0) {
|
|
2338
|
+
out.write(`${settled(message, answer)}
|
|
2339
|
+
`);
|
|
2340
|
+
return answer;
|
|
2341
|
+
}
|
|
2342
|
+
out.write(`${color.yellow("!")} ${problem}
|
|
2343
|
+
`);
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
async function askLine(rl, question) {
|
|
2347
|
+
let onClose = () => {
|
|
2348
|
+
};
|
|
2349
|
+
const closed = new Promise((_, reject) => {
|
|
2350
|
+
onClose = () => reject(cancelled());
|
|
2351
|
+
rl.once("close", onClose);
|
|
2352
|
+
});
|
|
2353
|
+
try {
|
|
2354
|
+
return await Promise.race([rl.question(question).catch(abortIsCancel), closed]);
|
|
2355
|
+
} finally {
|
|
2356
|
+
rl.off("close", onClose);
|
|
2357
|
+
rl.close();
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
function abortIsCancel(err) {
|
|
2361
|
+
if (err.code === "ABORT_ERR") throw cancelled();
|
|
2362
|
+
throw err;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
// src/commands/login.ts
|
|
2241
2366
|
var loginCommand = new Command34("login").description("log in to the kvbox cloud (or your own control plane) and target it from now on").argument("[url]", `control plane URL (default: ${DEFAULT_CLOUD_URL})`).option("-e, --email <email>", "account email (prompted otherwise)").option("--password-stdin", "read the password from the first line of stdin (for scripts)").action(async (urlArg, opts) => {
|
|
2242
2367
|
const url = normalizeUrl(urlArg ?? DEFAULT_CLOUD_URL);
|
|
2243
2368
|
const creds = await readCredentials(opts);
|
|
@@ -2294,39 +2419,21 @@ async function ensureSshKey(p = paths(), hostname = osHostname()) {
|
|
|
2294
2419
|
return (await readFile5(pubPath, "utf8")).trim();
|
|
2295
2420
|
}
|
|
2296
2421
|
async function prompt(question, io) {
|
|
2297
|
-
const rl =
|
|
2298
|
-
return (await
|
|
2422
|
+
const rl = createInterface2({ input: io.input, output: io.output });
|
|
2423
|
+
return (await askLine(rl, question)).trim();
|
|
2299
2424
|
}
|
|
2300
2425
|
async function promptSecret(question, io) {
|
|
2301
2426
|
const sink = new Writable({ write: (_chunk, _enc, cb) => cb() });
|
|
2302
|
-
const rl =
|
|
2427
|
+
const rl = createInterface2({ input: io.input, output: sink, terminal: true });
|
|
2303
2428
|
io.output.write(question);
|
|
2304
2429
|
try {
|
|
2305
|
-
return await
|
|
2430
|
+
return await askLine(rl, question);
|
|
2306
2431
|
} finally {
|
|
2307
2432
|
io.output.write("\n");
|
|
2308
2433
|
}
|
|
2309
2434
|
}
|
|
2310
|
-
async function ask(rl, question) {
|
|
2311
|
-
let onClose = () => {
|
|
2312
|
-
};
|
|
2313
|
-
const closed = new Promise((_, reject) => {
|
|
2314
|
-
onClose = () => reject(new KvboxError("cancelled", 130));
|
|
2315
|
-
rl.once("close", onClose);
|
|
2316
|
-
});
|
|
2317
|
-
try {
|
|
2318
|
-
return await Promise.race([rl.question(question).catch(cancelled), closed]);
|
|
2319
|
-
} finally {
|
|
2320
|
-
rl.off("close", onClose);
|
|
2321
|
-
rl.close();
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
|
-
function cancelled(err) {
|
|
2325
|
-
if (err.code === "ABORT_ERR") throw new KvboxError("cancelled", 130);
|
|
2326
|
-
throw err;
|
|
2327
|
-
}
|
|
2328
2435
|
async function firstLine(stream) {
|
|
2329
|
-
const rl =
|
|
2436
|
+
const rl = createInterface2({ input: stream });
|
|
2330
2437
|
try {
|
|
2331
2438
|
for await (const line of rl) return line.trim();
|
|
2332
2439
|
return "";
|
|
@@ -2684,7 +2791,7 @@ function ownerCell2(b, me2) {
|
|
|
2684
2791
|
}
|
|
2685
2792
|
function usageRows(report, cloud) {
|
|
2686
2793
|
const priced = report.totals.cost !== void 0;
|
|
2687
|
-
const
|
|
2794
|
+
const rows2 = report.boxes.map((b) => ({
|
|
2688
2795
|
...cloud ? { owner: ownerCell2(b, cloud.me) } : {},
|
|
2689
2796
|
name: boxName(b.name),
|
|
2690
2797
|
status: b.status === "removed" ? color.dim("removed") : status(b.status),
|
|
@@ -2695,7 +2802,7 @@ function usageRows(report, cloud) {
|
|
|
2695
2802
|
...priced ? { cost: money(b.cost?.amount ?? 0, report.totals.cost.currency) } : {}
|
|
2696
2803
|
}));
|
|
2697
2804
|
const t = report.totals;
|
|
2698
|
-
|
|
2805
|
+
rows2.push({
|
|
2699
2806
|
...cloud ? { owner: "" } : {},
|
|
2700
2807
|
name: color.bold("total"),
|
|
2701
2808
|
status: "",
|
|
@@ -2705,7 +2812,7 @@ function usageRows(report, cloud) {
|
|
|
2705
2812
|
"disk GiB-h": hours(t.diskGibHours),
|
|
2706
2813
|
...priced ? { cost: `${money(t.cost.amount, t.cost.currency)} ${color.dim("(estimated)")}` } : {}
|
|
2707
2814
|
});
|
|
2708
|
-
return
|
|
2815
|
+
return rows2;
|
|
2709
2816
|
}
|
|
2710
2817
|
function byUser(report) {
|
|
2711
2818
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -2733,19 +2840,19 @@ function userRows(r) {
|
|
|
2733
2840
|
"ram GiB-h": hours(t.ramGibHours),
|
|
2734
2841
|
"disk GiB-h": hours(t.diskGibHours)
|
|
2735
2842
|
});
|
|
2736
|
-
const
|
|
2843
|
+
const rows2 = r.users.map((u) => ({
|
|
2737
2844
|
user: u.user ?? color.dim("unknown"),
|
|
2738
2845
|
boxes: String(u.boxes),
|
|
2739
2846
|
...cells(u),
|
|
2740
2847
|
...priced ? { cost: money(u.cost?.amount ?? 0, r.totals.cost.currency) } : {}
|
|
2741
2848
|
}));
|
|
2742
|
-
|
|
2849
|
+
rows2.push({
|
|
2743
2850
|
user: color.bold("total"),
|
|
2744
2851
|
boxes: String(r.users.reduce((n, u) => n + u.boxes, 0)),
|
|
2745
2852
|
...cells(r.totals),
|
|
2746
2853
|
...priced ? { cost: `${money(r.totals.cost.amount, r.totals.cost.currency)} ${color.dim("(estimated)")}` } : {}
|
|
2747
2854
|
});
|
|
2748
|
-
return
|
|
2855
|
+
return rows2;
|
|
2749
2856
|
}
|
|
2750
2857
|
|
|
2751
2858
|
// src/commands/credit.ts
|
|
@@ -2777,9 +2884,476 @@ function creditRow(b, me2) {
|
|
|
2777
2884
|
};
|
|
2778
2885
|
}
|
|
2779
2886
|
|
|
2887
|
+
// src/commands/deploy/index.ts
|
|
2888
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
2889
|
+
import { basename, resolve as resolve2 } from "path";
|
|
2890
|
+
import { Command as Command49, Option as Option5 } from "commander";
|
|
2891
|
+
|
|
2892
|
+
// src/commands/deploy/config.ts
|
|
2893
|
+
import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
|
|
2894
|
+
import { join as join4 } from "path";
|
|
2895
|
+
import { z as z5 } from "zod";
|
|
2896
|
+
|
|
2897
|
+
// src/commands/deploy/templates/pm.ts
|
|
2898
|
+
import { access } from "fs/promises";
|
|
2899
|
+
import { join as join2 } from "path";
|
|
2900
|
+
async function packageManager(dir) {
|
|
2901
|
+
const has = (f) => access(join2(dir, f)).then(() => true, () => false);
|
|
2902
|
+
if (await has("pnpm-lock.yaml")) return "pnpm";
|
|
2903
|
+
if (await has("yarn.lock")) return "yarn";
|
|
2904
|
+
if (await has("bun.lockb") || await has("bun.lock")) return "bun";
|
|
2905
|
+
return "npm";
|
|
2906
|
+
}
|
|
2907
|
+
function runScript(pm, script) {
|
|
2908
|
+
return pm === "yarn" ? [script] : ["run", script];
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
// src/commands/deploy/templates/nextjs.ts
|
|
2912
|
+
var APP_DIR = "/srv/app/current";
|
|
2913
|
+
var SERVICE = "kvbox-app";
|
|
2914
|
+
var PORT = 3e3;
|
|
2915
|
+
function commands(pm) {
|
|
2916
|
+
switch (pm) {
|
|
2917
|
+
case "pnpm":
|
|
2918
|
+
return { setup: ["sudo -n corepack enable pnpm"], install: "pnpm install --frozen-lockfile", build: "pnpm run build", start: "pnpm run start" };
|
|
2919
|
+
case "yarn":
|
|
2920
|
+
return { setup: ["sudo -n corepack enable yarn"], install: "yarn install --frozen-lockfile", build: "yarn build", start: "yarn start" };
|
|
2921
|
+
case "bun":
|
|
2922
|
+
return { setup: [], install: "npm install", build: "npm run build", start: "npm run start" };
|
|
2923
|
+
case "npm":
|
|
2924
|
+
return { setup: [], install: "if [ -f package-lock.json ]; then npm ci; else npm install; fi", build: "npm run build", start: "npm run start" };
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
var nextjsTemplate = {
|
|
2928
|
+
id: "nextjs",
|
|
2929
|
+
label: "Next.js",
|
|
2930
|
+
hint: "source shipped; installed, built and run in the box on port 3000",
|
|
2931
|
+
size: "s",
|
|
2932
|
+
port: PORT,
|
|
2933
|
+
detect(pkg) {
|
|
2934
|
+
if (!pkg) return false;
|
|
2935
|
+
return "next" in { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2936
|
+
},
|
|
2937
|
+
/** No local build: the box does it. Only the scripts it will need are checked here. */
|
|
2938
|
+
async build(ctx) {
|
|
2939
|
+
for (const script of ["build", "start"]) {
|
|
2940
|
+
if (typeof ctx.pkg?.scripts?.[script] !== "string") throw new KvboxError(`${ctx.dir}: package.json has no "${script}" script; Next.js needs "build": "next build" and "start": "next start"`);
|
|
2941
|
+
}
|
|
2942
|
+
const pm = await packageManager(ctx.dir);
|
|
2943
|
+
return { outDir: ctx.dir, exclude: ["node_modules", ".next", ".git"], pm, what: "the source" };
|
|
2944
|
+
},
|
|
2945
|
+
/**
|
|
2946
|
+
* The source lands in a staging directory and is installed and built there while the old app
|
|
2947
|
+
* keeps serving; only then is it swapped into place and the service restarted, so a failed
|
|
2948
|
+
* install or build leaves the running app alone. The unit is rewritten every time, so a changed
|
|
2949
|
+
* package manager or start script takes effect. /srv/app is the kvbox user's (image v0.8.0+).
|
|
2950
|
+
*/
|
|
2951
|
+
remote(built) {
|
|
2952
|
+
const c = commands(built.pm);
|
|
2953
|
+
const unit = [
|
|
2954
|
+
"[Unit]",
|
|
2955
|
+
"Description=kvbox deploy: the app in " + APP_DIR,
|
|
2956
|
+
"After=network.target",
|
|
2957
|
+
"",
|
|
2958
|
+
"[Service]",
|
|
2959
|
+
"User=kvbox",
|
|
2960
|
+
`WorkingDirectory=${APP_DIR}`,
|
|
2961
|
+
`ExecStart=/usr/bin/env ${c.start}`,
|
|
2962
|
+
`Environment=NODE_ENV=production PORT=${PORT}`,
|
|
2963
|
+
"Restart=always",
|
|
2964
|
+
"RestartSec=2",
|
|
2965
|
+
"",
|
|
2966
|
+
"[Install]",
|
|
2967
|
+
"WantedBy=multi-user.target"
|
|
2968
|
+
];
|
|
2969
|
+
return [
|
|
2970
|
+
"set -e",
|
|
2971
|
+
"test -d /srv/app -a -w /srv/app || exit 3",
|
|
2972
|
+
"rm -rf /srv/app/.incoming /srv/app/.old",
|
|
2973
|
+
"mkdir -p /srv/app/.incoming",
|
|
2974
|
+
"tar -xf - -C /srv/app/.incoming",
|
|
2975
|
+
"cd /srv/app/.incoming",
|
|
2976
|
+
"export CI=1 NEXT_TELEMETRY_DISABLED=1 NODE_ENV=production",
|
|
2977
|
+
...c.setup,
|
|
2978
|
+
c.install,
|
|
2979
|
+
c.build,
|
|
2980
|
+
"cd /",
|
|
2981
|
+
`sudo -n tee /etc/systemd/system/${SERVICE}.service >/dev/null <<'UNIT'`,
|
|
2982
|
+
...unit,
|
|
2983
|
+
"UNIT",
|
|
2984
|
+
`if [ -e ${APP_DIR} ]; then mv ${APP_DIR} /srv/app/.old; fi`,
|
|
2985
|
+
`mv /srv/app/.incoming ${APP_DIR}`,
|
|
2986
|
+
"sudo -n systemctl daemon-reload",
|
|
2987
|
+
`sudo -n systemctl enable --quiet ${SERVICE}`,
|
|
2988
|
+
`sudo -n systemctl restart ${SERVICE}`,
|
|
2989
|
+
"rm -rf /srv/app/.old"
|
|
2990
|
+
].join("\n");
|
|
2991
|
+
}
|
|
2992
|
+
};
|
|
2993
|
+
|
|
2994
|
+
// src/commands/deploy/templates/react.ts
|
|
2995
|
+
import { spawn as spawn4 } from "child_process";
|
|
2996
|
+
import { access as access2 } from "fs/promises";
|
|
2997
|
+
import { join as join3 } from "path";
|
|
2998
|
+
var OUT_DIRS = ["dist", "build"];
|
|
2999
|
+
async function runBuild(dir, pm) {
|
|
3000
|
+
const args = runScript(pm, "build");
|
|
3001
|
+
info(`${color.dim("$")} ${pm} ${args.join(" ")}`);
|
|
3002
|
+
const status2 = await new Promise((resolve3, reject) => {
|
|
3003
|
+
const child = spawn4(pm, args, { cwd: dir, stdio: ["ignore", process.stderr, process.stderr], shell: process.platform === "win32" });
|
|
3004
|
+
child.once("error", (err) => reject(new KvboxError(`could not run ${pm}: ${err.message}`)));
|
|
3005
|
+
child.once("close", (code) => resolve3(code ?? 1));
|
|
3006
|
+
});
|
|
3007
|
+
if (status2 !== 0) throw new KvboxError(`the build failed (${pm} exited ${status2}); nothing was deployed`);
|
|
3008
|
+
}
|
|
3009
|
+
var reactTemplate = {
|
|
3010
|
+
id: "react",
|
|
3011
|
+
label: "React",
|
|
3012
|
+
hint: "static site: built here, served by nginx in the box",
|
|
3013
|
+
size: "xs",
|
|
3014
|
+
port: 80,
|
|
3015
|
+
/** A Vite or Create React App project. A framework with its own server (Next.js, Remix, Expo) is not a static site. */
|
|
3016
|
+
detect(pkg) {
|
|
3017
|
+
if (!pkg) return false;
|
|
3018
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3019
|
+
if (!("react" in deps) || "next" in deps || "@remix-run/react" in deps || "expo" in deps) return false;
|
|
3020
|
+
return "vite" in deps || "react-scripts" in deps;
|
|
3021
|
+
},
|
|
3022
|
+
async build(ctx) {
|
|
3023
|
+
if (typeof ctx.pkg?.scripts?.["build"] !== "string") throw new KvboxError(`${ctx.dir}: package.json has no "build" script`);
|
|
3024
|
+
const pm = await packageManager(ctx.dir);
|
|
3025
|
+
await runBuild(ctx.dir, pm);
|
|
3026
|
+
const candidates = ctx.dist !== void 0 ? [ctx.dist] : OUT_DIRS;
|
|
3027
|
+
for (const dist of candidates) {
|
|
3028
|
+
const outDir = join3(ctx.dir, dist);
|
|
3029
|
+
if (await access2(join3(outDir, "index.html")).then(() => true, () => false)) return { outDir, dist, exclude: [], pm, what: `${dist}/` };
|
|
3030
|
+
}
|
|
3031
|
+
throw new KvboxError(
|
|
3032
|
+
`the build left no ${candidates.join(" or ")}/index.html. The React template ships a directory of static files; set "dist" in kvbox.json to the build output directory, or pick the right template with --template`
|
|
3033
|
+
);
|
|
3034
|
+
},
|
|
3035
|
+
/**
|
|
3036
|
+
* The archive lands in a staging directory, which then becomes /srv/app/dist in one move; the
|
|
3037
|
+
* old site is kept beside it until the new one is in place, so a failed step never leaves nginx
|
|
3038
|
+
* without a root and a visitor never sees a half-copied site. Files get read permission for
|
|
3039
|
+
* nginx's user, whatever umask the build ran under. /srv/app is the kvbox user's (image v0.8.0+);
|
|
3040
|
+
* the missing site file means an older image.
|
|
3041
|
+
*/
|
|
3042
|
+
remote() {
|
|
3043
|
+
return [
|
|
3044
|
+
"set -e",
|
|
3045
|
+
"test -e /etc/nginx/sites-enabled/kvbox-app || exit 3",
|
|
3046
|
+
"rm -rf /srv/app/.incoming /srv/app/.old",
|
|
3047
|
+
"mkdir -p /srv/app/.incoming",
|
|
3048
|
+
"tar -xf - -C /srv/app/.incoming",
|
|
3049
|
+
"chmod -R a+rX /srv/app/.incoming",
|
|
3050
|
+
"if [ -e /srv/app/dist ]; then mv /srv/app/dist /srv/app/.old; fi",
|
|
3051
|
+
"mv /srv/app/.incoming /srv/app/dist",
|
|
3052
|
+
"rm -rf /srv/app/.old",
|
|
3053
|
+
"sudo -n systemctl enable --now --quiet nginx",
|
|
3054
|
+
"sudo -n systemctl reload nginx"
|
|
3055
|
+
].join("\n");
|
|
3056
|
+
}
|
|
3057
|
+
};
|
|
3058
|
+
|
|
3059
|
+
// src/commands/deploy/templates/index.ts
|
|
3060
|
+
var TEMPLATES = [reactTemplate, nextjsTemplate];
|
|
3061
|
+
var TEMPLATE_IDS = TEMPLATES.map((t) => t.id);
|
|
3062
|
+
function templateById(id) {
|
|
3063
|
+
const t = TEMPLATES.find((x) => x.id === id);
|
|
3064
|
+
if (!t) throw new Error(`no template ${id}`);
|
|
3065
|
+
return t;
|
|
3066
|
+
}
|
|
3067
|
+
function detectTemplate(pkg) {
|
|
3068
|
+
return TEMPLATES.find((t) => t.detect(pkg));
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
// src/commands/deploy/config.ts
|
|
3072
|
+
var DEPLOY_AUTO_HIBERNATE_SEC = 3600;
|
|
3073
|
+
var CONFIG_FILE = "kvbox.json";
|
|
3074
|
+
var BOX_NAME_RE = /^[a-z0-9][a-z0-9-]{0,11}$/;
|
|
3075
|
+
var DeployConfigSchema = z5.object({
|
|
3076
|
+
template: z5.enum(TEMPLATE_IDS),
|
|
3077
|
+
name: z5.string().regex(BOX_NAME_RE, "1-12 chars, lowercase letters, digits and '-', starting with a letter or digit"),
|
|
3078
|
+
/** The org whose box it is; absent for the user's own. */
|
|
3079
|
+
org: z5.string().regex(ORG_SLUG_RE, "not an org slug").optional(),
|
|
3080
|
+
/** The HTTPS name (`<subdomain>.kvbox.app`); absent when deploying to this machine's own host. */
|
|
3081
|
+
subdomain: z5.string().check((ctx) => {
|
|
3082
|
+
const problem = subdomainProblem(ctx.value);
|
|
3083
|
+
if (problem) ctx.issues.push({ code: "custom", message: problem, input: ctx.value });
|
|
3084
|
+
}).optional(),
|
|
3085
|
+
size: z5.enum(SIZE_NAMES).optional(),
|
|
3086
|
+
/** The build output directory, relative to the app; the template's default when absent. */
|
|
3087
|
+
dist: z5.string().optional()
|
|
3088
|
+
});
|
|
3089
|
+
async function readConfig(dir) {
|
|
3090
|
+
const path = join4(dir, CONFIG_FILE);
|
|
3091
|
+
let raw;
|
|
3092
|
+
try {
|
|
3093
|
+
raw = await readFile6(path, "utf8");
|
|
3094
|
+
} catch (err) {
|
|
3095
|
+
if (err.code === "ENOENT") return void 0;
|
|
3096
|
+
throw err;
|
|
3097
|
+
}
|
|
3098
|
+
let json2;
|
|
3099
|
+
try {
|
|
3100
|
+
json2 = JSON.parse(raw);
|
|
3101
|
+
} catch {
|
|
3102
|
+
throw new KvboxError(`${path}: not valid JSON`);
|
|
3103
|
+
}
|
|
3104
|
+
const parsed = DeployConfigSchema.safeParse(json2);
|
|
3105
|
+
if (!parsed.success) {
|
|
3106
|
+
const issue = parsed.error.issues[0];
|
|
3107
|
+
throw new KvboxError(`${path}: ${issue.path.join(".") || "file"}: ${issue.message}`);
|
|
3108
|
+
}
|
|
3109
|
+
return parsed.data;
|
|
3110
|
+
}
|
|
3111
|
+
async function writeConfig(dir, config) {
|
|
3112
|
+
await writeFile4(join4(dir, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n");
|
|
3113
|
+
}
|
|
3114
|
+
function nameFromDir(dirName) {
|
|
3115
|
+
const slug = dirName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 12).replace(/-+$/, "");
|
|
3116
|
+
return slug === "" ? "app" : slug;
|
|
3117
|
+
}
|
|
3118
|
+
function subdomainFor(name) {
|
|
3119
|
+
return subdomainProblem(name) === null ? name : `${name}-site`;
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
// src/commands/deploy/ship.ts
|
|
3123
|
+
import { spawn as spawn5 } from "child_process";
|
|
3124
|
+
import { readdir, stat as stat2 } from "fs/promises";
|
|
3125
|
+
import { join as join5 } from "path";
|
|
3126
|
+
var IMAGE_TOO_OLD = 3;
|
|
3127
|
+
var SSH_FAILED3 = 255;
|
|
3128
|
+
async function measure(dir, exclude = []) {
|
|
3129
|
+
const total = { files: 0, bytes: 0 };
|
|
3130
|
+
const walk = async (d) => {
|
|
3131
|
+
for (const entry of await readdir(d, { withFileTypes: true })) {
|
|
3132
|
+
if (exclude.includes(entry.name)) continue;
|
|
3133
|
+
const path = join5(d, entry.name);
|
|
3134
|
+
if (entry.isDirectory()) await walk(path);
|
|
3135
|
+
else if (entry.isFile()) {
|
|
3136
|
+
total.files += 1;
|
|
3137
|
+
total.bytes += (await stat2(path)).size;
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
};
|
|
3141
|
+
await walk(dir);
|
|
3142
|
+
return total;
|
|
3143
|
+
}
|
|
3144
|
+
async function ship(ep, dir, remote, exclude = []) {
|
|
3145
|
+
const tar = spawn5("tar", ["-cf", "-", ...exclude.map((e) => `--exclude=${e}`), "-C", dir, "."], { stdio: ["ignore", "pipe", "pipe"] });
|
|
3146
|
+
const ssh = spawn5("ssh", [...sshArgs(ep), remote], { stdio: ["pipe", "pipe", "pipe"] });
|
|
3147
|
+
tar.stdout.pipe(ssh.stdin);
|
|
3148
|
+
const chunks = [];
|
|
3149
|
+
ssh.stdout.on("data", (c) => chunks.push(c));
|
|
3150
|
+
ssh.stderr.on("data", (c) => chunks.push(c));
|
|
3151
|
+
tar.stderr.on("data", (c) => chunks.push(c));
|
|
3152
|
+
ssh.stdin.on("error", () => {
|
|
3153
|
+
});
|
|
3154
|
+
tar.stdout.on("error", () => {
|
|
3155
|
+
});
|
|
3156
|
+
tar.once("error", () => ssh.stdin.end());
|
|
3157
|
+
tar.once("close", (code) => {
|
|
3158
|
+
if (code !== 0) ssh.stdin.end();
|
|
3159
|
+
});
|
|
3160
|
+
const tarDone = new Promise((resolve3) => tar.once("close", () => resolve3()));
|
|
3161
|
+
const status2 = await new Promise((resolve3, reject) => {
|
|
3162
|
+
ssh.once("error", (err) => reject(new KvboxError(`could not run ssh: ${err.message}`)));
|
|
3163
|
+
ssh.once("close", (code, signal) => {
|
|
3164
|
+
tar.stdout.unpipe(ssh.stdin);
|
|
3165
|
+
tar.stdout.resume();
|
|
3166
|
+
tar.kill();
|
|
3167
|
+
resolve3(code ?? (signal === "SIGINT" ? 130 : 1));
|
|
3168
|
+
});
|
|
3169
|
+
});
|
|
3170
|
+
await tarDone;
|
|
3171
|
+
return { status: status2, output: Buffer.concat(chunks).toString("utf8") };
|
|
3172
|
+
}
|
|
3173
|
+
var shipping = { ship, measure, waitForSsh };
|
|
3174
|
+
|
|
3175
|
+
// src/commands/deploy/index.ts
|
|
3176
|
+
var deployCommand = new Command49("deploy").description("put the app in this directory (React or Next.js) on a box with its own HTTPS address; redeploy by running it again").argument("[dir]", "the app's directory (default: the current one)").addOption(new Option5("-t, --template <template>", "what kind of app it is (detected from package.json when not given)").choices(TEMPLATE_IDS)).option("-n, --name <name>", "the box's name (default: the directory's name)").option("--sub <subdomain>", "its HTTPS name, https://<subdomain>.kvbox.app (default: the box's name)").addOption(new Option5("-s, --size <size>", "size of the box when it has to be made").choices(SIZE_NAMES)).option("-y, --yes", "ask nothing: take the defaults for whatever the flags and kvbox.json leave open").option("--json", "print the result as JSON").option("--org <slug>", "deploy into this org's boxes (saved to kvbox.json)").action(async (dirArg, opts, cmd) => {
|
|
3177
|
+
const dir = resolve2(dirArg ?? ".");
|
|
3178
|
+
const pkg = await readPackageJson(dir);
|
|
3179
|
+
const saved = await readConfig(dir);
|
|
3180
|
+
const globals = cmd.optsWithGlobals();
|
|
3181
|
+
const orgFlag = orgOption(globals);
|
|
3182
|
+
const org = orgFlag ?? saved?.org;
|
|
3183
|
+
const client = await clientFor({ ...globals.local ? { local: true } : {}, ...org !== void 0 ? { org } : {} });
|
|
3184
|
+
try {
|
|
3185
|
+
const cloud = client.target.kind === "cloud";
|
|
3186
|
+
if (!cloud && org !== void 0) throw new KvboxError("--org needs a control plane: kvbox login, or drop --org");
|
|
3187
|
+
const { config, template, fresh } = await settle(dir, pkg, saved, opts, orgFlag, cloud);
|
|
3188
|
+
if (cloud && config.subdomain !== void 0) await refuseOtherAddress(client, config);
|
|
3189
|
+
await writeConfig(dir, config);
|
|
3190
|
+
if (fresh) info(` ${color.dim(`saved to ${CONFIG_FILE}; next time kvbox deploy asks nothing`)}
|
|
3191
|
+
`);
|
|
3192
|
+
info(`${color.bold("Building")} ${color.cyan(config.name)} ${color.dim(`(${template.label})`)}`);
|
|
3193
|
+
const built = await template.build({ dir, pkg, dist: config.dist });
|
|
3194
|
+
if (built.dist !== void 0 && built.dist !== config.dist) await writeConfig(dir, { ...config, dist: built.dist });
|
|
3195
|
+
const size = await shipping.measure(built.outDir, built.exclude);
|
|
3196
|
+
info("");
|
|
3197
|
+
const { box: found, ref, created } = await findOrCreate(client, config);
|
|
3198
|
+
const cert = await ensureCert(client, void 0, void 0, { owner: found.owner });
|
|
3199
|
+
const wasRunning = !created && found.status === "running";
|
|
3200
|
+
const box = created || wasRunning ? found : await wakeUp(client, found, ref);
|
|
3201
|
+
const ep = sshEndpoint(box, client.target);
|
|
3202
|
+
if (!wasRunning) await spin(`waiting for ${ref} to accept SSH\u2026`, shipping.waitForSsh(ep));
|
|
3203
|
+
const shipIt = () => shipping.ship(ep, built.outDir, template.remote(built), built.exclude);
|
|
3204
|
+
const doing = built.dist === void 0 ? `shipping ${built.what} (${size.files} files, ${gb(size.bytes)}) to ${ref} and building there, a minute or two\u2026` : `shipping ${built.what} (${size.files} files, ${gb(size.bytes)}) to ${ref}\u2026`;
|
|
3205
|
+
let result = await spin(doing, shipIt);
|
|
3206
|
+
if (result.status === SSH_FAILED3 && await renewAfterRefusal(client, cert, box.owner)) {
|
|
3207
|
+
info("renewed your SSH certificate, trying again\u2026");
|
|
3208
|
+
result = await spin(doing, shipIt);
|
|
3209
|
+
}
|
|
3210
|
+
if (result.status === SSH_FAILED3) throw new KvboxError(`box ${ref}: could not reach it over SSH (still booting?)${result.output ? `
|
|
3211
|
+
${result.output.trim()}` : ""}`, SSH_FAILED3);
|
|
3212
|
+
if (result.status === IMAGE_TOO_OLD) {
|
|
3213
|
+
throw new KvboxError(
|
|
3214
|
+
`box ${ref} runs an image too old for the ${template.label} template (base v0.8.0 or newer). Update the box (kvbox vm update ${ref}) once its host has the new image, or deploy under another name (--name) to get a fresh box`
|
|
3215
|
+
);
|
|
3216
|
+
}
|
|
3217
|
+
if (result.status !== 0) throw new KvboxError(`deploying to ${ref} failed (exit ${result.status}):
|
|
3218
|
+
${result.output.trim()}`);
|
|
3219
|
+
let url = `http://${box.ip}`;
|
|
3220
|
+
if (cloud) {
|
|
3221
|
+
const got = await address(client, box, ref, config, template);
|
|
3222
|
+
url = got.url;
|
|
3223
|
+
if (got.subdomain !== config.subdomain) await writeConfig(dir, { ...config, subdomain: got.subdomain, ...built.dist !== void 0 && built.dist !== config.dist ? { dist: built.dist } : {} });
|
|
3224
|
+
}
|
|
3225
|
+
if (opts.json) {
|
|
3226
|
+
json({ box: ref, created, template: template.id, url, ...built.dist !== void 0 ? { dist: built.dist } : {}, files: size.files, bytes: size.bytes });
|
|
3227
|
+
return;
|
|
3228
|
+
}
|
|
3229
|
+
process.stdout.write(`${url}
|
|
3230
|
+
`);
|
|
3231
|
+
info(`${color.green("\u2713")} ${color.bold(config.name)} is live at ${color.bold(url)}`);
|
|
3232
|
+
info(` box ${ref}${created ? " (new)" : ""} \xB7 ${template.label} \xB7 ${size.files} files, ${gb(size.bytes)}`);
|
|
3233
|
+
info(` redeploy: kvbox deploy${dirArg !== void 0 ? ` ${dirArg}` : ""}`);
|
|
3234
|
+
} finally {
|
|
3235
|
+
await client.close();
|
|
3236
|
+
}
|
|
3237
|
+
});
|
|
3238
|
+
async function readPackageJson(dir) {
|
|
3239
|
+
try {
|
|
3240
|
+
return JSON.parse(await readFile7(resolve2(dir, "package.json"), "utf8"));
|
|
3241
|
+
} catch (err) {
|
|
3242
|
+
if (err.code === "ENOENT") return void 0;
|
|
3243
|
+
throw new KvboxError(`${resolve2(dir, "package.json")}: ${err.message}`);
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
async function settle(dir, pkg, saved, opts, orgFlag, cloud) {
|
|
3247
|
+
const detected = detectTemplate(pkg);
|
|
3248
|
+
if (opts.name !== void 0 && !BOX_NAME_RE.test(opts.name)) throw new KvboxError(`--name ${opts.name}: ${NAME_RULE}`);
|
|
3249
|
+
if (opts.sub !== void 0) assertSubdomain(opts.sub);
|
|
3250
|
+
const renamed = saved !== void 0 && (opts.name !== void 0 && opts.name !== saved.name || orgFlag !== void 0 && orgFlag !== saved.org);
|
|
3251
|
+
const savedSub = renamed ? void 0 : saved?.subdomain;
|
|
3252
|
+
const defaults = {
|
|
3253
|
+
template: opts.template ?? saved?.template ?? detected?.id,
|
|
3254
|
+
name: opts.name ?? saved?.name ?? nameFromDir(basename(dir)),
|
|
3255
|
+
subdomain: opts.sub ?? savedSub,
|
|
3256
|
+
size: opts.size ?? saved?.size
|
|
3257
|
+
};
|
|
3258
|
+
const sizeFor = (t) => defaults.size ?? t?.size ?? "xs";
|
|
3259
|
+
const open = {
|
|
3260
|
+
template: opts.template === void 0 && saved?.template === void 0,
|
|
3261
|
+
name: opts.name === void 0 && saved?.name === void 0,
|
|
3262
|
+
subdomain: cloud && opts.sub === void 0 && savedSub === void 0,
|
|
3263
|
+
size: opts.size === void 0 && saved === void 0
|
|
3264
|
+
};
|
|
3265
|
+
const anyOpen = open.template || open.name || open.subdomain || open.size;
|
|
3266
|
+
const fresh = saved === void 0;
|
|
3267
|
+
if (anyOpen && !opts.yes && !interactive()) {
|
|
3268
|
+
const sub = defaults.subdomain ?? subdomainFor(defaults.name);
|
|
3269
|
+
throw new KvboxError(
|
|
3270
|
+
`kvbox deploy asks a few questions the first time, and this is not a terminal. Pass -y to take the defaults (${defaults.template ?? "template?"}, box ${defaults.name}${cloud ? `, https://${sub}.kvbox.app` : ""}, size ${sizeFor(defaults.template !== void 0 ? templateById(defaults.template) : void 0)}), or answer with --template, --name${cloud ? ", --sub" : ""} and --size`
|
|
3271
|
+
);
|
|
3272
|
+
}
|
|
3273
|
+
const ask = anyOpen && !opts.yes;
|
|
3274
|
+
if (ask && fresh) info(`${color.bold("kvbox deploy")} ${color.dim(dir)}
|
|
3275
|
+
`);
|
|
3276
|
+
let templateId = defaults.template;
|
|
3277
|
+
if (open.template && ask) {
|
|
3278
|
+
const initial = detected ? TEMPLATES.indexOf(detected) : 0;
|
|
3279
|
+
templateId = await select("Template", TEMPLATES.map((t) => ({ value: t.id, label: t.label, hint: t === detected ? `${t.hint} \xB7 detected` : t.hint })), initial);
|
|
3280
|
+
}
|
|
3281
|
+
if (templateId === void 0) throw new KvboxError(`could not tell what kind of app ${dir} is (no package.json of a Vite, Create React App or Next.js project): say it with --template ${TEMPLATE_IDS.join("|")}`);
|
|
3282
|
+
const template = templateById(templateId);
|
|
3283
|
+
let name = defaults.name;
|
|
3284
|
+
if (open.name && ask) name = await text("Box name", { default: defaults.name, validate: (a) => BOX_NAME_RE.test(a) ? void 0 : NAME_RULE });
|
|
3285
|
+
let subdomain = defaults.subdomain ?? (cloud ? subdomainFor(name) : void 0);
|
|
3286
|
+
if (open.subdomain && ask) {
|
|
3287
|
+
subdomain = await text("Address", {
|
|
3288
|
+
default: subdomainFor(name),
|
|
3289
|
+
validate: (a) => {
|
|
3290
|
+
const problem = subdomainProblem(a);
|
|
3291
|
+
return problem ? `that name ${problem}` : void 0;
|
|
3292
|
+
}
|
|
3293
|
+
});
|
|
3294
|
+
info(` ${color.dim(`https://${subdomain}.kvbox.app`)}`);
|
|
3295
|
+
}
|
|
3296
|
+
let size = sizeFor(template);
|
|
3297
|
+
if (open.size && ask) {
|
|
3298
|
+
size = await select("Box size", SIZE_NAMES.map((s) => ({ value: s, label: s, hint: `${SIZES[s].vcpu} vCPU, ${SIZES[s].memMib / 1024} GB RAM, ${SIZES[s].diskGib} GB disk` })), SIZE_NAMES.indexOf(template.size));
|
|
3299
|
+
}
|
|
3300
|
+
const org = orgFlag ?? saved?.org;
|
|
3301
|
+
const config = {
|
|
3302
|
+
template: template.id,
|
|
3303
|
+
name,
|
|
3304
|
+
...org !== void 0 ? { org } : {},
|
|
3305
|
+
...subdomain !== void 0 ? { subdomain } : {},
|
|
3306
|
+
size,
|
|
3307
|
+
...saved?.dist !== void 0 ? { dist: saved.dist } : {}
|
|
3308
|
+
};
|
|
3309
|
+
return { config, template, fresh };
|
|
3310
|
+
}
|
|
3311
|
+
var NAME_RULE = "1-12 chars, lowercase letters, digits and '-', starting with a letter or digit";
|
|
3312
|
+
async function findOrCreate(client, config) {
|
|
3313
|
+
const user = me(client);
|
|
3314
|
+
const slug = config.org ?? user;
|
|
3315
|
+
const named = (await client.listMachines()).filter((b) => b.name === config.name);
|
|
3316
|
+
let box = slug === void 0 ? named[0] : named.find((b) => slugOf(b.owner) === slug);
|
|
3317
|
+
let created = false;
|
|
3318
|
+
if (!box) {
|
|
3319
|
+
box = await spin(`booting a new box ${config.name}\u2026`, client.createMachine({ name: config.name, ...config.size !== void 0 ? { size: config.size } : {}, autoHibernateSec: DEPLOY_AUTO_HIBERNATE_SEC }));
|
|
3320
|
+
created = true;
|
|
3321
|
+
info(`${color.green("\u2713")} box ${color.bold(config.name)} is up: ${box.vcpu} vCPU, ${gb(box.memMib * 1024 ** 2)} RAM, ${gb(box.diskBytes)} disk`);
|
|
3322
|
+
}
|
|
3323
|
+
return { box, ref: displayName(box, user), created };
|
|
3324
|
+
}
|
|
3325
|
+
async function existingAddress(client, config) {
|
|
3326
|
+
const slug = config.org ?? me(client);
|
|
3327
|
+
return (await client.listSubdomains()).find((s) => s.box === config.name && s.owner === slug);
|
|
3328
|
+
}
|
|
3329
|
+
async function refuseOtherAddress(client, config) {
|
|
3330
|
+
const have = await existingAddress(client, config);
|
|
3331
|
+
if (!have || have.subdomain === config.subdomain) return;
|
|
3332
|
+
const ref = config.org !== void 0 ? `${config.org}/${config.name}` : config.name;
|
|
3333
|
+
throw new KvboxError(
|
|
3334
|
+
`box ${ref} already answers at ${have.url}, not ${config.subdomain}. To keep the address: kvbox deploy --sub ${have.subdomain}. To move it: kvbox vm proxy release ${ref}, then deploy again`
|
|
3335
|
+
);
|
|
3336
|
+
}
|
|
3337
|
+
async function address(client, box, ref, config, template) {
|
|
3338
|
+
const have = (await client.listSubdomains()).find((s) => s.box === box.name && s.owner === slugOf(box.owner));
|
|
3339
|
+
if (have) {
|
|
3340
|
+
if (have.port !== template.port) await client.setSubdomainPort(ref, { port: template.port });
|
|
3341
|
+
return { url: have.url, subdomain: have.subdomain };
|
|
3342
|
+
}
|
|
3343
|
+
const wanted = config.subdomain ?? subdomainFor(config.name);
|
|
3344
|
+
try {
|
|
3345
|
+
const sub = await spin(`claiming https://${wanted}.kvbox.app\u2026`, client.claimSubdomain(ref, { name: wanted, port: template.port }));
|
|
3346
|
+
return { url: sub.url, subdomain: sub.subdomain };
|
|
3347
|
+
} catch (err) {
|
|
3348
|
+
if (!(err instanceof KvboxError)) throw err;
|
|
3349
|
+
throw new KvboxError(`${err.message}
|
|
3350
|
+
the app is on box ${ref} but has no address yet: pick another with kvbox deploy --sub <name> (saved to ${CONFIG_FILE})`);
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
|
|
2780
3354
|
// src/program.ts
|
|
2781
3355
|
function buildProgram() {
|
|
2782
|
-
const program = new
|
|
3356
|
+
const program = new Command50("kvbox").description("self-hosted Firecracker microVMs with Docker-like ergonomics").version(VERSION, "-v, --version").option("--local", "use this machine's kvbox-server over its socket, ignoring kvbox login").option("--org <slug>", "the org: where kvbox new creates, and which box a name means when it is not one of yours; before the command here, or after it").showHelpAfterError().enablePositionalOptions();
|
|
2783
3357
|
program.addCommand(vmCommand);
|
|
2784
3358
|
program.addCommand(snapshotsCommand);
|
|
2785
3359
|
program.addCommand(serverCommand);
|
|
@@ -2789,6 +3363,7 @@ function buildProgram() {
|
|
|
2789
3363
|
program.addCommand(orgCommand);
|
|
2790
3364
|
program.addCommand(usageCommand2);
|
|
2791
3365
|
program.addCommand(creditCommand);
|
|
3366
|
+
program.addCommand(deployCommand);
|
|
2792
3367
|
for (const cmd of vmAliases) program.addCommand(cmd);
|
|
2793
3368
|
return program;
|
|
2794
3369
|
}
|