@kvbox/cli 0.5.7 → 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 +136 -45
- package/dist/kvbox.js.map +1 -1
- package/package.json +1 -1
package/dist/kvbox.js
CHANGED
|
@@ -147,7 +147,7 @@ async function spin(message, work, opts = {}) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
// src/program.ts
|
|
150
|
-
import { Command as
|
|
150
|
+
import { Command as Command39 } from "commander";
|
|
151
151
|
|
|
152
152
|
// ../shared/src/version.ts
|
|
153
153
|
import { createRequire } from "module";
|
|
@@ -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) {
|
|
@@ -773,11 +809,11 @@ var ServerClient = class {
|
|
|
773
809
|
base;
|
|
774
810
|
autoStart;
|
|
775
811
|
startedOnce = false;
|
|
776
|
-
async call(method, path, schema, body) {
|
|
812
|
+
async call(method, path, schema, body, org = this.target.kind === "cloud" ? this.target.org : void 0) {
|
|
777
813
|
const headers = {};
|
|
778
814
|
if (body !== void 0) headers["content-type"] = "application/json";
|
|
779
815
|
if (this.target.kind === "cloud" && this.target.token !== "") headers["authorization"] = `Bearer ${this.target.token}`;
|
|
780
|
-
if (this.target.kind === "cloud" &&
|
|
816
|
+
if (this.target.kind === "cloud" && org !== void 0) headers["x-kvbox-org"] = org;
|
|
781
817
|
let res;
|
|
782
818
|
try {
|
|
783
819
|
res = await request(this.base + path, {
|
|
@@ -866,9 +902,9 @@ var ServerClient = class {
|
|
|
866
902
|
const query = q.size === 0 ? "" : `?${q.toString()}`;
|
|
867
903
|
return this.call("GET", `/usage${query}`, UsageReportSchema);
|
|
868
904
|
}
|
|
869
|
-
/** The prepaid balance of the caller's slug, or of `--org`'s (#76); a control plane only. */
|
|
870
|
-
balance() {
|
|
871
|
-
return this.call("GET", "/billing/balance", BalanceSchema);
|
|
905
|
+
/** The prepaid balance of the caller's slug, or of `org` (default: `--org`'s) (#76); a control plane only. */
|
|
906
|
+
balance(org) {
|
|
907
|
+
return this.call("GET", "/billing/balance", BalanceSchema, void 0, org);
|
|
872
908
|
}
|
|
873
909
|
deleteMachine(name) {
|
|
874
910
|
return this.call("DELETE", `/machines/${encodeURIComponent(name)}`, z3.undefined());
|
|
@@ -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}`);
|
|
@@ -2086,9 +2147,38 @@ function usageRows(report, cloud) {
|
|
|
2086
2147
|
return rows;
|
|
2087
2148
|
}
|
|
2088
2149
|
|
|
2150
|
+
// src/commands/credit.ts
|
|
2151
|
+
import { Command as Command38 } from "commander";
|
|
2152
|
+
var creditCommand = new Command38("credit").description("the prepaid credit left on your account and on each of your orgs").option("--org <slug>", "one org only").option("--json", "print as JSON").action(async (opts, cmd) => {
|
|
2153
|
+
const client = await clientFor(cmd.optsWithGlobals(), { autoStart: false });
|
|
2154
|
+
try {
|
|
2155
|
+
if (client.target.kind === "local") throw new KvboxError("credit lives on the control plane: kvbox login first");
|
|
2156
|
+
const me2 = client.target.user;
|
|
2157
|
+
const balances = client.target.org !== void 0 ? [await client.balance()] : await Promise.all([void 0, ...(await client.listOrgs()).map((o) => o.slug)].map((org) => client.balance(org)));
|
|
2158
|
+
if (opts.json) {
|
|
2159
|
+
json(balances);
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
table(balances.map((b) => creditRow(b, me2)));
|
|
2163
|
+
} finally {
|
|
2164
|
+
await client.close();
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
function creditRow(b, me2) {
|
|
2168
|
+
const left = money(b.balance, b.currency);
|
|
2169
|
+
return {
|
|
2170
|
+
owner: b.slug === me2 ? "private" : boxName(b.slug),
|
|
2171
|
+
credit: b.balance > 0 ? left : color.red(left),
|
|
2172
|
+
"topped up": money(b.topUps, b.currency),
|
|
2173
|
+
spent: money(b.spent, b.currency),
|
|
2174
|
+
// Suspended (#76): the sweep hibernated the boxes at zero; a top-up brings them back.
|
|
2175
|
+
state: b.suspendedAt ? color.red("suspended") : b.since === null ? color.dim("never topped up") : "ok"
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2089
2179
|
// src/program.ts
|
|
2090
2180
|
function buildProgram() {
|
|
2091
|
-
const program = new
|
|
2181
|
+
const program = new Command39("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();
|
|
2092
2182
|
program.addCommand(vmCommand);
|
|
2093
2183
|
program.addCommand(snapshotsCommand);
|
|
2094
2184
|
program.addCommand(serverCommand);
|
|
@@ -2097,6 +2187,7 @@ function buildProgram() {
|
|
|
2097
2187
|
program.addCommand(whoamiCommand);
|
|
2098
2188
|
program.addCommand(orgCommand);
|
|
2099
2189
|
program.addCommand(usageCommand2);
|
|
2190
|
+
program.addCommand(creditCommand);
|
|
2100
2191
|
for (const cmd of vmAliases) program.addCommand(cmd);
|
|
2101
2192
|
return program;
|
|
2102
2193
|
}
|