@kvbox/cli 0.5.8 → 0.5.9
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 +99 -38
- package/dist/kvbox.js.map +1 -1
- package/package.json +1 -1
package/dist/kvbox.js
CHANGED
|
@@ -162,13 +162,13 @@ function versionOf(srcFileUrl, injected) {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
// src/version.ts
|
|
165
|
-
var VERSION = versionOf(import.meta.url, true ? "0.5.
|
|
165
|
+
var VERSION = versionOf(import.meta.url, true ? "0.5.9" : void 0);
|
|
166
166
|
|
|
167
167
|
// src/commands/vm/index.ts
|
|
168
168
|
import { Command as Command15 } from "commander";
|
|
169
169
|
|
|
170
170
|
// src/commands/vm/connect.ts
|
|
171
|
-
import { spawnSync } from "child_process";
|
|
171
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
172
172
|
import { Command as Command2 } from "commander";
|
|
173
173
|
|
|
174
174
|
// src/cert.ts
|
|
@@ -466,6 +466,12 @@ var CapacitySchema = z2.object({
|
|
|
466
466
|
reservedVcpu: z2.number().int(),
|
|
467
467
|
running: z2.number().int()
|
|
468
468
|
});
|
|
469
|
+
var RecoveryReportSchema = z2.object({
|
|
470
|
+
at: z2.string(),
|
|
471
|
+
booted: z2.array(z2.string()),
|
|
472
|
+
leftStopped: z2.array(z2.string()),
|
|
473
|
+
failed: z2.array(z2.object({ box: z2.string(), error: z2.string() }))
|
|
474
|
+
});
|
|
469
475
|
var HealthSchema = z2.object({
|
|
470
476
|
ok: z2.literal(true),
|
|
471
477
|
version: z2.string(),
|
|
@@ -489,7 +495,9 @@ var HealthSchema = z2.object({
|
|
|
489
495
|
publicIp: z2.string(),
|
|
490
496
|
/** The relay's WireGuard endpoint the tunnel dials. */
|
|
491
497
|
endpoint: z2.string()
|
|
492
|
-
}).nullable().optional()
|
|
498
|
+
}).nullable().optional(),
|
|
499
|
+
/** Boxes brought back at the server's last start; null when there were none or it is still at it. Older servers omit it. */
|
|
500
|
+
recovery: RecoveryReportSchema.nullable().optional()
|
|
493
501
|
});
|
|
494
502
|
var MachineUsageSchema = z2.object({
|
|
495
503
|
name: z2.string(),
|
|
@@ -739,6 +747,34 @@ function groupWithMember(gid, user, groupFile) {
|
|
|
739
747
|
return void 0;
|
|
740
748
|
}
|
|
741
749
|
|
|
750
|
+
// src/systemd.ts
|
|
751
|
+
import { spawnSync } from "child_process";
|
|
752
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
753
|
+
import { resolve } from "path";
|
|
754
|
+
var UNIT = "kvbox-server";
|
|
755
|
+
var UNIT_FILE = `/etc/systemd/system/${UNIT}.service`;
|
|
756
|
+
function hasUnitFor(p, file = UNIT_FILE) {
|
|
757
|
+
let text;
|
|
758
|
+
try {
|
|
759
|
+
text = readFileSync2(file, "utf8");
|
|
760
|
+
} catch {
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
const home = /^Environment="?KVBOX_HOME=([^"\n]+)"?$/m.exec(text)?.[1];
|
|
764
|
+
return home !== void 0 && resolve(home) === resolve(p.home);
|
|
765
|
+
}
|
|
766
|
+
function startUnitProcess() {
|
|
767
|
+
const res = spawnSync("sudo", ["-n", "systemctl", "start", UNIT], { encoding: "utf8" });
|
|
768
|
+
return { ok: res.status === 0, out: `${res.stdout ?? ""}${res.stderr ?? ""}`.trim() };
|
|
769
|
+
}
|
|
770
|
+
function unitActive() {
|
|
771
|
+
const res = spawnSync("systemctl", ["is-active", UNIT], { encoding: "utf8" });
|
|
772
|
+
return (res.stdout ?? "").trim() === "active";
|
|
773
|
+
}
|
|
774
|
+
function startsAtBoot(p) {
|
|
775
|
+
return hasUnitFor(p) && spawnSync("systemctl", ["is-enabled", "--quiet", UNIT]).status === 0;
|
|
776
|
+
}
|
|
777
|
+
|
|
742
778
|
// src/client.ts
|
|
743
779
|
var ORG_SLUG_RE = /^[a-z0-9][a-z0-9._-]{0,31}$/;
|
|
744
780
|
function orgOption(opts) {
|
|
@@ -987,17 +1023,17 @@ function sshArgs(ep, p = paths()) {
|
|
|
987
1023
|
async function waitForSsh(ep, timeoutMs = 3e4) {
|
|
988
1024
|
const { connect } = await import("net");
|
|
989
1025
|
const deadline = Date.now() + timeoutMs;
|
|
990
|
-
const attempt = () => new Promise((
|
|
1026
|
+
const attempt = () => new Promise((resolve2) => {
|
|
991
1027
|
const sock = connect({ host: ep.host, port: ep.port, timeout: 1e3 });
|
|
992
1028
|
sock.once("connect", () => {
|
|
993
1029
|
sock.destroy();
|
|
994
|
-
|
|
1030
|
+
resolve2(true);
|
|
995
1031
|
});
|
|
996
1032
|
sock.once("timeout", () => {
|
|
997
1033
|
sock.destroy();
|
|
998
|
-
|
|
1034
|
+
resolve2(false);
|
|
999
1035
|
});
|
|
1000
|
-
sock.once("error", () =>
|
|
1036
|
+
sock.once("error", () => resolve2(false));
|
|
1001
1037
|
});
|
|
1002
1038
|
while (Date.now() < deadline) {
|
|
1003
1039
|
if (await attempt()) return;
|
|
@@ -1006,14 +1042,15 @@ async function waitForSsh(ep, timeoutMs = 3e4) {
|
|
|
1006
1042
|
throw new KvboxError(`box at ${ep.host}:${ep.port} did not answer within ${Math.round(timeoutMs / 1e3)}s`);
|
|
1007
1043
|
}
|
|
1008
1044
|
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";
|
|
1009
|
-
async function lastErrorLine(logPath) {
|
|
1045
|
+
async function lastErrorLine(logPath, from = 0) {
|
|
1010
1046
|
const { readFile: readFile6 } = await import("fs/promises");
|
|
1011
|
-
const text = await readFile6(logPath
|
|
1047
|
+
const text = (await readFile6(logPath).catch(() => Buffer.alloc(0))).subarray(from).toString("utf8");
|
|
1012
1048
|
const lines = text.trimEnd().split("\n").slice(-20).reverse();
|
|
1013
1049
|
return lines.find((l) => l.startsWith("error: "))?.slice("error: ".length);
|
|
1014
1050
|
}
|
|
1015
1051
|
async function startLocalServer(p = paths(), timeoutMs = 5e3) {
|
|
1016
1052
|
await mkdir2(p.home, { recursive: true });
|
|
1053
|
+
if (hasUnitFor(p)) return startUnit(p, timeoutMs);
|
|
1017
1054
|
const log = openSync(p.serverLog, "a");
|
|
1018
1055
|
const launcher = serverLauncher();
|
|
1019
1056
|
const child = spawn(launcher.cmd, launcher.args, { detached: true, stdio: ["ignore", log, log] });
|
|
@@ -1027,29 +1064,45 @@ async function startLocalServer(p = paths(), timeoutMs = 5e3) {
|
|
|
1027
1064
|
});
|
|
1028
1065
|
});
|
|
1029
1066
|
});
|
|
1030
|
-
|
|
1031
|
-
const deadline = Date.now() + timeoutMs;
|
|
1032
|
-
while (Date.now() < deadline) {
|
|
1033
|
-
if (await stat(p.serverSock).then(() => true, () => false)) {
|
|
1034
|
-
const probe = new ServerClient({ kind: "local", socketPath: p.serverSock }, { autoStart: false });
|
|
1035
|
-
try {
|
|
1036
|
-
await probe.health();
|
|
1037
|
-
return;
|
|
1038
|
-
} catch {
|
|
1039
|
-
} finally {
|
|
1040
|
-
await probe.close();
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
await sleep(100);
|
|
1044
|
-
}
|
|
1045
|
-
throw new KvboxError(`kvbox-server did not start within ${timeoutMs / 1e3}s; see ${p.serverLog}`);
|
|
1046
|
-
})();
|
|
1047
|
-
await Promise.race([ready, failed]);
|
|
1067
|
+
await Promise.race([serving(p, timeoutMs), failed]);
|
|
1048
1068
|
child.removeAllListeners("exit");
|
|
1049
1069
|
child.unref();
|
|
1050
1070
|
const pid = Number.parseInt(await readFile3(p.serverPid, "utf8").catch(() => ""), 10);
|
|
1051
1071
|
return { pid: Number.isNaN(pid) ? child.pid ?? -1 : pid, socketPath: p.serverSock, viaGroup: launcher.viaGroup };
|
|
1052
1072
|
}
|
|
1073
|
+
async function startUnit(p, timeoutMs) {
|
|
1074
|
+
const logFrom = await stat(p.serverLog).then((s) => s.size, () => 0);
|
|
1075
|
+
const res = startUnitProcess();
|
|
1076
|
+
if (!res.ok) {
|
|
1077
|
+
const why = res.out || await lastErrorLine(p.serverLog, logFrom) || "systemctl start failed";
|
|
1078
|
+
throw new KvboxError(`kvbox-server could not start: ${why} (journalctl -u ${UNIT}; rerun install-server.sh if sudo asked for a password)`);
|
|
1079
|
+
}
|
|
1080
|
+
await serving(p, timeoutMs, async () => {
|
|
1081
|
+
if (unitActive()) return;
|
|
1082
|
+
const why = await lastErrorLine(p.serverLog, logFrom) ?? "it exited right after starting";
|
|
1083
|
+
throw new KvboxError(`kvbox-server could not start: ${why} (journalctl -u ${UNIT})`);
|
|
1084
|
+
});
|
|
1085
|
+
const pid = Number.parseInt(await readFile3(p.serverPid, "utf8").catch(() => ""), 10);
|
|
1086
|
+
return { pid: Number.isNaN(pid) ? -1 : pid, socketPath: p.serverSock, viaGroup: void 0 };
|
|
1087
|
+
}
|
|
1088
|
+
async function serving(p, timeoutMs, check) {
|
|
1089
|
+
const deadline = Date.now() + timeoutMs;
|
|
1090
|
+
while (Date.now() < deadline) {
|
|
1091
|
+
await check?.();
|
|
1092
|
+
if (await stat(p.serverSock).then(() => true, () => false)) {
|
|
1093
|
+
const probe = new ServerClient({ kind: "local", socketPath: p.serverSock }, { autoStart: false });
|
|
1094
|
+
try {
|
|
1095
|
+
await probe.health();
|
|
1096
|
+
return;
|
|
1097
|
+
} catch {
|
|
1098
|
+
} finally {
|
|
1099
|
+
await probe.close();
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
await sleep(100);
|
|
1103
|
+
}
|
|
1104
|
+
throw new KvboxError(`kvbox-server did not start within ${timeoutMs / 1e3}s; see ${p.serverLog}`);
|
|
1105
|
+
}
|
|
1053
1106
|
|
|
1054
1107
|
// src/commands/vm/new.ts
|
|
1055
1108
|
import { Command, Option } from "commander";
|
|
@@ -1104,14 +1157,14 @@ var connectCommand = new Command2("connect").alias("cn").description("open a she
|
|
|
1104
1157
|
} finally {
|
|
1105
1158
|
await client.close();
|
|
1106
1159
|
}
|
|
1107
|
-
let res =
|
|
1160
|
+
let res = spawnSync2("ssh", [...sshArgs(ep), ...cmdArgs], { stdio: "inherit" });
|
|
1108
1161
|
if (res.error) throw new KvboxError(`could not run ssh: ${res.error.message}`);
|
|
1109
1162
|
if (res.status === SSH_FAILED) {
|
|
1110
1163
|
const again = await clientFor(cmd.optsWithGlobals());
|
|
1111
1164
|
const retry = await renewAfterRefusal(again, cert, owner).finally(() => again.close());
|
|
1112
1165
|
if (retry) {
|
|
1113
1166
|
info("renewed your SSH certificate, trying again\u2026");
|
|
1114
|
-
res =
|
|
1167
|
+
res = spawnSync2("ssh", [...sshArgs(ep), ...cmdArgs], { stdio: "inherit" });
|
|
1115
1168
|
if (res.error) throw new KvboxError(`could not run ssh: ${res.error.message}`);
|
|
1116
1169
|
}
|
|
1117
1170
|
}
|
|
@@ -1155,12 +1208,12 @@ async function runSsh(ssh, opts) {
|
|
|
1155
1208
|
timedOut = true;
|
|
1156
1209
|
child.kill("SIGTERM");
|
|
1157
1210
|
}, opts.timeout * 1e3);
|
|
1158
|
-
const status2 = await new Promise((
|
|
1211
|
+
const status2 = await new Promise((resolve2, reject) => {
|
|
1159
1212
|
child.once("error", (err) => reject(new KvboxError(`could not run ssh: ${err.message}`)));
|
|
1160
1213
|
child.once("close", (code, signal) => {
|
|
1161
|
-
if (timedOut)
|
|
1162
|
-
else if (code !== null)
|
|
1163
|
-
else
|
|
1214
|
+
if (timedOut) resolve2(TIMEOUT_EXIT);
|
|
1215
|
+
else if (code !== null) resolve2(code);
|
|
1216
|
+
else resolve2(signal === "SIGINT" ? 130 : 1);
|
|
1164
1217
|
});
|
|
1165
1218
|
}).finally(() => clearTimeout(timer));
|
|
1166
1219
|
return { status: status2, chunks, timedOut };
|
|
@@ -1515,7 +1568,7 @@ async function restartLocalServer(p = paths()) {
|
|
|
1515
1568
|
return (await startLocalServer(p)).pid;
|
|
1516
1569
|
}
|
|
1517
1570
|
function runKvboxServer(args) {
|
|
1518
|
-
return new Promise((
|
|
1571
|
+
return new Promise((resolve2, reject) => {
|
|
1519
1572
|
const child = spawn3("kvbox-server", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1520
1573
|
let out = "";
|
|
1521
1574
|
let err = "";
|
|
@@ -1525,7 +1578,7 @@ function runKvboxServer(args) {
|
|
|
1525
1578
|
reject(new KvboxError(e.code === "ENOENT" ? NOT_A_HOST_HINT : `could not run kvbox-server: ${e.message}`));
|
|
1526
1579
|
});
|
|
1527
1580
|
child.once("close", (code) => {
|
|
1528
|
-
if (code === 0)
|
|
1581
|
+
if (code === 0) resolve2(out.trimEnd());
|
|
1529
1582
|
else reject(new KvboxError(err.trim().replace(/^error: /, "") || `kvbox-server ${args[0] ?? ""} exited with code ${String(code)}`, code === 2 ? 2 : 1));
|
|
1530
1583
|
});
|
|
1531
1584
|
});
|
|
@@ -1580,9 +1633,17 @@ var statusCommand = new Command20("status").description("show whether this machi
|
|
|
1580
1633
|
const client = await clientFor({ ...cmd.optsWithGlobals(), local: true });
|
|
1581
1634
|
try {
|
|
1582
1635
|
const health = await client.health();
|
|
1583
|
-
|
|
1636
|
+
const atBoot = startsAtBoot(paths());
|
|
1637
|
+
if (opts.json) json({ ...health, startsAtBoot: atBoot });
|
|
1584
1638
|
else {
|
|
1585
1639
|
info(`kvbox-server ${health.version} running, pid ${health.pid}`);
|
|
1640
|
+
info(atBoot ? "starts at boot: yes, and brings back the boxes that were running" : "starts at boot: no (install-server.sh sets that up)");
|
|
1641
|
+
const r = health.recovery;
|
|
1642
|
+
if (r) {
|
|
1643
|
+
if (r.booted.length > 0) info(`after the last restart it booted again: ${r.booted.join(", ")}`);
|
|
1644
|
+
if (r.leftStopped.length > 0) info(`left stopped (were in standby, their memory was lost): ${r.leftStopped.join(", ")}`);
|
|
1645
|
+
for (const f of r.failed) info(`did not come back: ${f.box} (${f.error})`);
|
|
1646
|
+
}
|
|
1586
1647
|
if (health.controlPlane) info(`control plane: ${health.controlPlane.name} at ${health.controlPlane.url}, this host answers it on ${health.controlPlane.hostUrl}`);
|
|
1587
1648
|
else info("control plane: none (kvbox server join <url> --token <token> puts this machine behind one)");
|
|
1588
1649
|
if (health.relay) info(`relay: ${health.relay.name} lends this machine ${health.relay.publicIp} (tunnel to ${health.relay.endpoint})`);
|
|
@@ -1646,7 +1707,7 @@ function describeReport(report) {
|
|
|
1646
1707
|
var serverCommand = new Command23("server").description("run or manage the kvbox server").addCommand(startCommand2).addCommand(statusCommand).addCommand(stopCommand2).addCommand(joinCommand).addCommand(leaveCommand).addCommand(relayCommand).addCommand(trustCaCommand);
|
|
1647
1708
|
|
|
1648
1709
|
// src/commands/login.ts
|
|
1649
|
-
import { spawnSync as
|
|
1710
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
1650
1711
|
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
1651
1712
|
import { hostname as osHostname } from "os";
|
|
1652
1713
|
import { createInterface } from "readline/promises";
|
|
@@ -1698,7 +1759,7 @@ async function ensureSshKey(p = paths(), hostname = osHostname()) {
|
|
|
1698
1759
|
await mkdir3(p.ssh, { recursive: true, mode: 448 });
|
|
1699
1760
|
const havePrivate = await readFile5(p.sshKey).then(() => true, () => false);
|
|
1700
1761
|
const args = havePrivate ? ["-y", "-f", p.sshKey] : ["-q", "-t", "ed25519", "-N", "", "-C", `kvbox@${hostname}`, "-f", p.sshKey];
|
|
1701
|
-
const res =
|
|
1762
|
+
const res = spawnSync3("ssh-keygen", args, { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" });
|
|
1702
1763
|
if (res.error) {
|
|
1703
1764
|
if (res.error.code === "ENOENT") throw new KvboxError("ssh-keygen not found; install OpenSSH");
|
|
1704
1765
|
throw new KvboxError(`could not run ssh-keygen: ${res.error.message}`);
|