@kvbox/cli 0.5.18 → 0.5.20
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 +815 -211
- package/dist/kvbox.js.map +1 -1
- package/package.json +1 -1
package/dist/kvbox.js
CHANGED
|
@@ -10,8 +10,10 @@ var KvboxError = class extends Error {
|
|
|
10
10
|
exitCode;
|
|
11
11
|
};
|
|
12
12
|
var AmbiguousRefError = class extends KvboxError {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
/** `orgFlag` false: `--org` names something else on this command (a fork's target), so only `slug/name` is offered. */
|
|
14
|
+
constructor(kind, name, slugs, orgFlag = true) {
|
|
15
|
+
const way = orgFlag ? `--org ${slugs[0]} or ${slugs[0]}/${name}` : `${slugs[0]}/${name}`;
|
|
16
|
+
super(`${kind} ${name} exists in ${slugs.slice(0, -1).join(", ")} and ${slugs.at(-1)}; say which with ${way}`, 1);
|
|
15
17
|
this.name = "AmbiguousRefError";
|
|
16
18
|
}
|
|
17
19
|
};
|
|
@@ -24,10 +26,10 @@ function json(value) {
|
|
|
24
26
|
}
|
|
25
27
|
var ANSI = /\x1b\[[0-9;]*m/g;
|
|
26
28
|
var width = (s) => s.replace(ANSI, "").length;
|
|
27
|
-
function table(
|
|
28
|
-
if (
|
|
29
|
-
const cols = columns ?? Object.keys(
|
|
30
|
-
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] ?? "")));
|
|
31
33
|
const widths = cols.map((c, i) => Math.max(c.length, ...cells.map((r) => width(r[i]))));
|
|
32
34
|
const line = (parts) => parts.map((p, i) => p + " ".repeat(Math.max(0, widths[i] - width(p)))).join(" ").trimEnd();
|
|
33
35
|
process.stdout.write(color.dim(line(cols.map((c) => c.toUpperCase()))) + "\n");
|
|
@@ -45,8 +47,8 @@ async function confirm(question, yes) {
|
|
|
45
47
|
warn("refusing without -y when stdin is not a terminal");
|
|
46
48
|
return false;
|
|
47
49
|
}
|
|
48
|
-
const { createInterface:
|
|
49
|
-
const rl =
|
|
50
|
+
const { createInterface: createInterface3 } = await import("readline/promises");
|
|
51
|
+
const rl = createInterface3({ input: process.stdin, output: process.stderr });
|
|
50
52
|
try {
|
|
51
53
|
const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
|
|
52
54
|
return answer === "y" || answer === "yes";
|
|
@@ -106,14 +108,14 @@ function slugOf(owner) {
|
|
|
106
108
|
const at = owner.indexOf("@");
|
|
107
109
|
return at === -1 ? owner : owner.slice(0, at);
|
|
108
110
|
}
|
|
109
|
-
function pickRef(items,
|
|
110
|
-
if (
|
|
111
|
-
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);
|
|
112
114
|
const own = named.find((i) => slugOf(i.owner) === me2);
|
|
113
115
|
if (own) return own;
|
|
114
116
|
const orgs = me2 === void 0 ? named : named.filter((i) => i.ownerKind !== "user");
|
|
115
117
|
if (orgs.length <= 1) return orgs[0];
|
|
116
|
-
throw new AmbiguousRefError(kind,
|
|
118
|
+
throw new AmbiguousRefError(kind, text2, orgs.map((i) => slugOf(i.owner)));
|
|
117
119
|
}
|
|
118
120
|
function shortRef(ref, owner) {
|
|
119
121
|
return ref.startsWith(`${owner}/`) ? ref.slice(owner.length + 1) : ref;
|
|
@@ -147,7 +149,7 @@ async function spin(message, work, opts = {}) {
|
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
// src/program.ts
|
|
150
|
-
import { Command as
|
|
152
|
+
import { Command as Command50 } from "commander";
|
|
151
153
|
|
|
152
154
|
// ../shared/src/version.ts
|
|
153
155
|
import { createRequire } from "module";
|
|
@@ -162,10 +164,10 @@ function versionOf(srcFileUrl, injected) {
|
|
|
162
164
|
}
|
|
163
165
|
|
|
164
166
|
// src/version.ts
|
|
165
|
-
var VERSION = versionOf(import.meta.url, true ? "0.5.
|
|
167
|
+
var VERSION = versionOf(import.meta.url, true ? "0.5.20" : void 0);
|
|
166
168
|
|
|
167
169
|
// src/commands/vm/index.ts
|
|
168
|
-
import { Command as
|
|
170
|
+
import { Command as Command25 } from "commander";
|
|
169
171
|
|
|
170
172
|
// src/commands/vm/connect.ts
|
|
171
173
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -230,6 +232,8 @@ function paths(home = kvboxHome()) {
|
|
|
230
232
|
* The next snapshot is a diff of the pages dirtied since, merged onto this. Absent after a boot.
|
|
231
233
|
*/
|
|
232
234
|
memBase: join(dir, "mem.base"),
|
|
235
|
+
/** `kvbox vm fork` (#79): the source's VM state, captured here and gone once the box runs. Its memory is `memBase`. */
|
|
236
|
+
forkVmstate: join(dir, "fork.vmstate"),
|
|
233
237
|
/** `kvbox hibernate`: the box's memory image and VM state while it is hibernated; gone again after `wake` or `stop`. */
|
|
234
238
|
hibernateDir: join(dir, "hibernate"),
|
|
235
239
|
hibernateVmstate: join(dir, "hibernate", "vmstate"),
|
|
@@ -267,15 +271,15 @@ var SessionSchema = z.object({
|
|
|
267
271
|
sshCertPrincipals: z.array(z.string()).optional()
|
|
268
272
|
});
|
|
269
273
|
async function readSession(p = paths()) {
|
|
270
|
-
let
|
|
274
|
+
let text2;
|
|
271
275
|
try {
|
|
272
|
-
|
|
276
|
+
text2 = await readFile(p.cli, "utf8");
|
|
273
277
|
} catch (err) {
|
|
274
278
|
if (err.code === "ENOENT") return null;
|
|
275
279
|
throw err;
|
|
276
280
|
}
|
|
277
281
|
try {
|
|
278
|
-
return SessionSchema.parse(JSON.parse(
|
|
282
|
+
return SessionSchema.parse(JSON.parse(text2));
|
|
279
283
|
} catch {
|
|
280
284
|
throw new KvboxError(`${p.cli} is unreadable; run: kvbox logout (then log in again)`);
|
|
281
285
|
}
|
|
@@ -371,8 +375,9 @@ var SIZES = {
|
|
|
371
375
|
l: { vcpu: 8, memMib: 16384, diskGib: 32 }
|
|
372
376
|
};
|
|
373
377
|
var SIZE_NAMES = Object.keys(SIZES);
|
|
374
|
-
|
|
375
|
-
|
|
378
|
+
var DEFAULT_SIZE = "s";
|
|
379
|
+
function parseMib(text2) {
|
|
380
|
+
const m = /^\s*(\d+(?:\.\d+)?)\s*([gm]i?b?)?\s*$/i.exec(text2);
|
|
376
381
|
if (!m) return void 0;
|
|
377
382
|
const n = Number(m[1]);
|
|
378
383
|
const unit = (m[2] ?? "g").toLowerCase();
|
|
@@ -427,7 +432,7 @@ var MachineSchema = z2.object({
|
|
|
427
432
|
// base image version the box's overlay stacks on
|
|
428
433
|
createdAt: z2.string(),
|
|
429
434
|
source: z2.string(),
|
|
430
|
-
// "base" or "
|
|
435
|
+
// "base", "snapshot:<owner/name>" or "fork:<owner/name>" (#79)
|
|
431
436
|
status: BoxStatusSchema,
|
|
432
437
|
/**
|
|
433
438
|
* Public TCP port on the host that reaches the box's SSH while it runs (#49). Only boxes made
|
|
@@ -463,6 +468,20 @@ var CreateMachineRequest = z2.object({
|
|
|
463
468
|
/** Seconds without network traffic before the box is hibernated (#47); 0: never. Absent: the host's default. */
|
|
464
469
|
autoHibernateSec: z2.number().int().min(0).optional()
|
|
465
470
|
});
|
|
471
|
+
var ForkMachineRequest = z2.object({
|
|
472
|
+
name: z2.string().optional(),
|
|
473
|
+
/**
|
|
474
|
+
* The new box's slug, bare (`acme`): the control plane sends the target of `--org` here; the host
|
|
475
|
+
* qualifies it with the caller's realm. Absent: the source's owner. A CLI leaves it out.
|
|
476
|
+
*/
|
|
477
|
+
owner: z2.string().optional(),
|
|
478
|
+
/** A cold fork only: a warm one keeps the source's vCPU and memory. */
|
|
479
|
+
size: SizeSchema.optional(),
|
|
480
|
+
force: z2.boolean().optional(),
|
|
481
|
+
/** As in CreateMachineRequest: the control plane's CA line for the new box's owner. */
|
|
482
|
+
sshKeys: z2.array(z2.string()).optional(),
|
|
483
|
+
autoHibernateSec: z2.number().int().min(0).optional()
|
|
484
|
+
});
|
|
466
485
|
var SetSshKeysRequest = z2.object({
|
|
467
486
|
sshKeys: z2.array(z2.string())
|
|
468
487
|
});
|
|
@@ -578,7 +597,12 @@ var BoxUsageSchema = MachineUsageSchema.omit({ window: true, status: true }).ext
|
|
|
578
597
|
id: z2.string(),
|
|
579
598
|
status: UsageStatusSchema,
|
|
580
599
|
/** Through a control plane: whether the owner slug is a user's or an org's (what `kvbox ls` shows). */
|
|
581
|
-
ownerKind: z2.enum(["user", "org"]).optional()
|
|
600
|
+
ownerKind: z2.enum(["user", "org"]).optional(),
|
|
601
|
+
/**
|
|
602
|
+
* Through a control plane: the username that made the box (#60), absent when the box is gone from
|
|
603
|
+
* the index (removed), was found by reconcile, or its maker was deleted. The ledger does not record it.
|
|
604
|
+
*/
|
|
605
|
+
createdBy: z2.string().optional()
|
|
582
606
|
});
|
|
583
607
|
var UsageReportSchema = z2.object({
|
|
584
608
|
window: z2.object({ from: z2.string(), to: z2.string() }),
|
|
@@ -787,13 +811,13 @@ function serverLauncher(opts = {}) {
|
|
|
787
811
|
return { cmd: "sg", args: [group, "-c", "kvbox-server start"], viaGroup: group };
|
|
788
812
|
}
|
|
789
813
|
function groupWithMember(gid, user, groupFile) {
|
|
790
|
-
let
|
|
814
|
+
let text2;
|
|
791
815
|
try {
|
|
792
|
-
|
|
816
|
+
text2 = readFileSync(groupFile, "utf8");
|
|
793
817
|
} catch {
|
|
794
818
|
return void 0;
|
|
795
819
|
}
|
|
796
|
-
for (const line of
|
|
820
|
+
for (const line of text2.split("\n")) {
|
|
797
821
|
const [name, , id, members = ""] = line.split(":");
|
|
798
822
|
if (name === void 0 || id === void 0 || Number.parseInt(id, 10) !== gid) continue;
|
|
799
823
|
return members.split(",").includes(user) ? name : void 0;
|
|
@@ -808,13 +832,13 @@ import { resolve } from "path";
|
|
|
808
832
|
var UNIT = "kvbox-server";
|
|
809
833
|
var UNIT_FILE = `/etc/systemd/system/${UNIT}.service`;
|
|
810
834
|
function hasUnitFor(p, file = UNIT_FILE) {
|
|
811
|
-
let
|
|
835
|
+
let text2;
|
|
812
836
|
try {
|
|
813
|
-
|
|
837
|
+
text2 = readFileSync2(file, "utf8");
|
|
814
838
|
} catch {
|
|
815
839
|
return false;
|
|
816
840
|
}
|
|
817
|
-
const home = /^Environment="?KVBOX_HOME=([^"\n]+)"?$/m.exec(
|
|
841
|
+
const home = /^Environment="?KVBOX_HOME=([^"\n]+)"?$/m.exec(text2)?.[1];
|
|
818
842
|
return home !== void 0 && resolve(home) === resolve(p.home);
|
|
819
843
|
}
|
|
820
844
|
function startUnitProcess() {
|
|
@@ -889,10 +913,10 @@ var ServerClient = class {
|
|
|
889
913
|
}
|
|
890
914
|
throw new KvboxError(cloudDown(this.target.url, String(code)));
|
|
891
915
|
}
|
|
892
|
-
const
|
|
916
|
+
const text2 = await res.body.text();
|
|
893
917
|
const where = this.target.kind === "cloud" ? hostOf(this.target.url) : "kvbox-server";
|
|
894
918
|
if (res.statusCode >= 300) {
|
|
895
|
-
const parsed = ApiErrorSchema.safeParse(safeJson(
|
|
919
|
+
const parsed = ApiErrorSchema.safeParse(safeJson(text2) ?? {});
|
|
896
920
|
if (!parsed.success) throw new KvboxError(`${where} answered ${res.statusCode}`);
|
|
897
921
|
if (this.target.kind === "cloud" && this.target.token !== "") {
|
|
898
922
|
if (res.statusCode === 401) throw new KvboxError("session expired: kvbox login");
|
|
@@ -902,8 +926,8 @@ var ServerClient = class {
|
|
|
902
926
|
}
|
|
903
927
|
throw new KvboxError(parsed.data.error, res.statusCode === 501 ? 2 : 1);
|
|
904
928
|
}
|
|
905
|
-
if (
|
|
906
|
-
const json2 = safeJson(
|
|
929
|
+
if (text2 === "") return schema.parse(void 0);
|
|
930
|
+
const json2 = safeJson(text2);
|
|
907
931
|
if (json2 === void 0) throw new KvboxError(`${where} answered ${res.statusCode} with something that is not JSON`);
|
|
908
932
|
return schema.parse(json2);
|
|
909
933
|
}
|
|
@@ -923,6 +947,10 @@ var ServerClient = class {
|
|
|
923
947
|
createMachine(req) {
|
|
924
948
|
return this.call("POST", "/machines", MachineSchema, req);
|
|
925
949
|
}
|
|
950
|
+
/** `kvbox vm fork` (#79): `name` is the source; the copy goes to the `--org` slug, else the caller's own. */
|
|
951
|
+
forkMachine(name, req) {
|
|
952
|
+
return this.call("POST", `/machines/${encodeURIComponent(name)}/fork`, MachineSchema, req);
|
|
953
|
+
}
|
|
926
954
|
stopMachine(name) {
|
|
927
955
|
return this.call("POST", `/machines/${encodeURIComponent(name)}/stop`, MachineSchema);
|
|
928
956
|
}
|
|
@@ -1070,9 +1098,9 @@ function cloudDown(url, code) {
|
|
|
1070
1098
|
const hint = host.endsWith("kvbox.sh") ? "check status.kvbox.sh" : "use --local for this machine, or kvbox logout";
|
|
1071
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}`;
|
|
1072
1100
|
}
|
|
1073
|
-
function safeJson(
|
|
1101
|
+
function safeJson(text2) {
|
|
1074
1102
|
try {
|
|
1075
|
-
return JSON.parse(
|
|
1103
|
+
return JSON.parse(text2);
|
|
1076
1104
|
} catch {
|
|
1077
1105
|
return void 0;
|
|
1078
1106
|
}
|
|
@@ -1092,10 +1120,10 @@ async function wakeUp(client, box, name) {
|
|
|
1092
1120
|
return box;
|
|
1093
1121
|
}
|
|
1094
1122
|
}
|
|
1095
|
-
function boxesFor(client,
|
|
1096
|
-
const slash =
|
|
1123
|
+
function boxesFor(client, text2) {
|
|
1124
|
+
const slash = text2.indexOf("/");
|
|
1097
1125
|
if (slash === -1 || client.target.kind !== "cloud") return client.listMachines();
|
|
1098
|
-
return client.listMachines({ org:
|
|
1126
|
+
return client.listMachines({ org: text2.slice(0, slash).split("@")[0] });
|
|
1099
1127
|
}
|
|
1100
1128
|
var me = (client) => client.target.kind === "cloud" ? client.target.user : void 0;
|
|
1101
1129
|
async function clientFor(opts, client = {}) {
|
|
@@ -1127,17 +1155,17 @@ function sshArgs(ep, p = paths()) {
|
|
|
1127
1155
|
async function waitForSsh(ep, timeoutMs = 3e4) {
|
|
1128
1156
|
const { connect } = await import("net");
|
|
1129
1157
|
const deadline = Date.now() + timeoutMs;
|
|
1130
|
-
const attempt = () => new Promise((
|
|
1158
|
+
const attempt = () => new Promise((resolve3) => {
|
|
1131
1159
|
const sock = connect({ host: ep.host, port: ep.port, timeout: 1e3 });
|
|
1132
1160
|
sock.once("connect", () => {
|
|
1133
1161
|
sock.destroy();
|
|
1134
|
-
|
|
1162
|
+
resolve3(true);
|
|
1135
1163
|
});
|
|
1136
1164
|
sock.once("timeout", () => {
|
|
1137
1165
|
sock.destroy();
|
|
1138
|
-
|
|
1166
|
+
resolve3(false);
|
|
1139
1167
|
});
|
|
1140
|
-
sock.once("error", () =>
|
|
1168
|
+
sock.once("error", () => resolve3(false));
|
|
1141
1169
|
});
|
|
1142
1170
|
while (Date.now() < deadline) {
|
|
1143
1171
|
if (await attempt()) return;
|
|
@@ -1147,9 +1175,9 @@ async function waitForSsh(ep, timeoutMs = 3e4) {
|
|
|
1147
1175
|
}
|
|
1148
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";
|
|
1149
1177
|
async function lastErrorLine(logPath, from = 0) {
|
|
1150
|
-
const { readFile:
|
|
1151
|
-
const
|
|
1152
|
-
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();
|
|
1153
1181
|
return lines.find((l) => l.startsWith("error: "))?.slice("error: ".length);
|
|
1154
1182
|
}
|
|
1155
1183
|
async function startLocalServer(p = paths(), timeoutMs = 5e3) {
|
|
@@ -1242,17 +1270,17 @@ function displayName(box, user) {
|
|
|
1242
1270
|
return user !== void 0 && slugOf(box.owner) !== user ? `${slugOf(box.owner)}/${box.name}` : box.name;
|
|
1243
1271
|
}
|
|
1244
1272
|
function describe(box, user) {
|
|
1245
|
-
const from = box.source.startsWith("snapshot:") ? ` from snapshot ${shortRef(box.source.slice("snapshot:".length), box.owner)}` : "";
|
|
1273
|
+
const from = box.source.startsWith("snapshot:") ? ` from snapshot ${shortRef(box.source.slice("snapshot:".length), box.owner)}` : box.source.startsWith("fork:") ? `, a fork of ${shortRef(box.source.slice("fork:".length), box.owner)}` : "";
|
|
1246
1274
|
return `box ${color.bold(displayName(box, user))} is ${box.status} at ${box.ip}${from}: ${box.vcpu} vCPU, ${gb(box.memMib * 1024 ** 2)} RAM, ${gb(box.diskBytes)} disk`;
|
|
1247
1275
|
}
|
|
1248
1276
|
function autoHibernateText(sec) {
|
|
1249
1277
|
return sec ? `hibernates after ${duration(sec)} without network traffic (kvbox connect wakes it)` : "never hibernates on its own";
|
|
1250
1278
|
}
|
|
1251
1279
|
var UNIT_SEC = { m: 60, h: 3600, d: 86400 };
|
|
1252
|
-
function parseIdleTimeout(
|
|
1253
|
-
const t =
|
|
1280
|
+
function parseIdleTimeout(text2) {
|
|
1281
|
+
const t = text2.trim().toLowerCase();
|
|
1254
1282
|
if (t === "off" || t === "never" || t === "0") return 0;
|
|
1255
|
-
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`);
|
|
1256
1284
|
let sec = 0;
|
|
1257
1285
|
for (const [, n, unit] of t.matchAll(/(\d+)([mhd])/g)) sec += Number(n) * UNIT_SEC[unit];
|
|
1258
1286
|
return sec;
|
|
@@ -1328,12 +1356,12 @@ async function runSsh(ssh, opts) {
|
|
|
1328
1356
|
timedOut = true;
|
|
1329
1357
|
child.kill("SIGTERM");
|
|
1330
1358
|
}, opts.timeout * 1e3);
|
|
1331
|
-
const status2 = await new Promise((
|
|
1359
|
+
const status2 = await new Promise((resolve3, reject) => {
|
|
1332
1360
|
child.once("error", (err) => reject(new KvboxError(`could not run ssh: ${err.message}`)));
|
|
1333
1361
|
child.once("close", (code, signal) => {
|
|
1334
|
-
if (timedOut)
|
|
1335
|
-
else if (code !== null)
|
|
1336
|
-
else
|
|
1362
|
+
if (timedOut) resolve3(TIMEOUT_EXIT);
|
|
1363
|
+
else if (code !== null) resolve3(code);
|
|
1364
|
+
else resolve3(signal === "SIGINT" ? 130 : 1);
|
|
1337
1365
|
});
|
|
1338
1366
|
}).finally(() => clearTimeout(timer));
|
|
1339
1367
|
return { status: status2, chunks, timedOut };
|
|
@@ -1386,9 +1414,36 @@ var execCommand = new Command3("exec").description("run a command in a box and e
|
|
|
1386
1414
|
process.exit(status2);
|
|
1387
1415
|
});
|
|
1388
1416
|
|
|
1417
|
+
// src/commands/vm/fork.ts
|
|
1418
|
+
import { Command as Command4, Option as Option2 } from "commander";
|
|
1419
|
+
var forkCommand = new Command4("fork").description("copy a box, running or stopped, into a new one on the same host (--org: into an org)").argument("<box>", "the box to copy").argument("[name]", "the copy's name (box1, box2, \u2026 when omitted)").addOption(new Option2("-s, --size <size>", "the copy's size, for a stopped box only (default: the box's)").choices(SIZE_NAMES)).option("--auto-hibernate <duration>", "hibernate the copy after this long without network traffic (30m, 4h, 1d; off: never; default: the box's)").option("--force", "skip the capacity guard").option("--json", "print the new box as JSON").action(async (source, name, opts, cmd) => {
|
|
1420
|
+
const autoHibernateSec = opts.autoHibernate === void 0 ? void 0 : parseIdleTimeout(opts.autoHibernate);
|
|
1421
|
+
const client = await clientFor(cmd.optsWithGlobals());
|
|
1422
|
+
try {
|
|
1423
|
+
const box = await spin(`forking ${source}\u2026`, client.forkMachine(source, {
|
|
1424
|
+
...name !== void 0 ? { name } : {},
|
|
1425
|
+
...opts.size !== void 0 ? { size: opts.size } : {},
|
|
1426
|
+
...autoHibernateSec !== void 0 ? { autoHibernateSec } : {},
|
|
1427
|
+
...opts.force ? { force: true } : {}
|
|
1428
|
+
}));
|
|
1429
|
+
if (opts.json) {
|
|
1430
|
+
json(box);
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
const user = me(client);
|
|
1434
|
+
process.stdout.write(`${box.name}
|
|
1435
|
+
`);
|
|
1436
|
+
info(`${color.green("\u2713")} ${describe(box, user)}`);
|
|
1437
|
+
info(` connect with: kvbox connect ${displayName(box, user)}`);
|
|
1438
|
+
info(` it ${autoHibernateText(box.autoHibernateSec)}; published ports and HTTPS names stay with ${source}`);
|
|
1439
|
+
} finally {
|
|
1440
|
+
await client.close();
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1389
1444
|
// src/commands/vm/hibernate.ts
|
|
1390
|
-
import { Command as
|
|
1391
|
-
var hibernateCommand = new
|
|
1445
|
+
import { Command as Command5 } from "commander";
|
|
1446
|
+
var hibernateCommand = new Command5("hibernate").description("save a box's memory to disk and free its RAM (kvbox wake continues it where it left off)").argument("<name>", "box name").option("--json", "print the box as JSON").action(async (name, opts, cmd) => {
|
|
1392
1447
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1393
1448
|
try {
|
|
1394
1449
|
const box = await spin(`hibernating ${name} (saving its memory to disk)\u2026`, client.hibernateMachine(name));
|
|
@@ -1400,7 +1455,7 @@ var hibernateCommand = new Command4("hibernate").description("save a box's memor
|
|
|
1400
1455
|
});
|
|
1401
1456
|
|
|
1402
1457
|
// src/commands/vm/ls.ts
|
|
1403
|
-
import { Command as
|
|
1458
|
+
import { Command as Command6 } from "commander";
|
|
1404
1459
|
function ownerCell(b, me2) {
|
|
1405
1460
|
const slug = slugOf(b.owner);
|
|
1406
1461
|
if (slug === me2) return "private";
|
|
@@ -1410,7 +1465,7 @@ function addressOf(b) {
|
|
|
1410
1465
|
if (b.ssh !== void 0) return b.ssh.host;
|
|
1411
1466
|
return b.ip === "" ? "-" : b.ip;
|
|
1412
1467
|
}
|
|
1413
|
-
var lsCommand = new
|
|
1468
|
+
var lsCommand = new Command6("ls").description("list boxes with status and address (yours and your orgs'; --org <slug> for one org)").option("-a, --all", "every box on the platform (platform admins only)").option("--json", "print as JSON").action(async (opts, cmd) => {
|
|
1414
1469
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1415
1470
|
try {
|
|
1416
1471
|
const boxes = await spin("listing boxes\u2026", client.listMachines({ all: opts.all }));
|
|
@@ -1441,8 +1496,8 @@ function lsRows(boxes, cloud) {
|
|
|
1441
1496
|
}
|
|
1442
1497
|
|
|
1443
1498
|
// src/commands/vm/pause.ts
|
|
1444
|
-
import { Command as
|
|
1445
|
-
var pauseCommand = new
|
|
1499
|
+
import { Command as Command7 } from "commander";
|
|
1500
|
+
var pauseCommand = new Command7("pause").description("freeze a box in place (memory and processes kept, no CPU used; kvbox resume continues it instantly)").argument("<name>", "box name").option("--json", "print the box as JSON").action(async (name, opts, cmd) => {
|
|
1446
1501
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1447
1502
|
try {
|
|
1448
1503
|
const box = await spin(`pausing ${name}\u2026`, client.pauseMachine(name));
|
|
@@ -1454,7 +1509,7 @@ var pauseCommand = new Command6("pause").description("freeze a box in place (mem
|
|
|
1454
1509
|
});
|
|
1455
1510
|
|
|
1456
1511
|
// src/commands/vm/proxy/claim.ts
|
|
1457
|
-
import { Command as
|
|
1512
|
+
import { Command as Command8 } from "commander";
|
|
1458
1513
|
|
|
1459
1514
|
// ../shared/src/names.ts
|
|
1460
1515
|
import { createHmac } from "crypto";
|
|
@@ -1485,26 +1540,26 @@ var RESERVED_SUBDOMAINS = /* @__PURE__ */ new Set([
|
|
|
1485
1540
|
"support",
|
|
1486
1541
|
"www"
|
|
1487
1542
|
]);
|
|
1488
|
-
function subdomainProblem(
|
|
1489
|
-
if (
|
|
1490
|
-
if (!LABEL_RE.test(
|
|
1491
|
-
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`;
|
|
1492
1547
|
return null;
|
|
1493
1548
|
}
|
|
1494
|
-
function routeNameProblem(
|
|
1495
|
-
if (
|
|
1496
|
-
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`;
|
|
1497
1552
|
return null;
|
|
1498
1553
|
}
|
|
1499
|
-
function assertSubdomain(
|
|
1500
|
-
const problem = subdomainProblem(
|
|
1554
|
+
function assertSubdomain(text2) {
|
|
1555
|
+
const problem = subdomainProblem(text2);
|
|
1501
1556
|
if (problem) throw new KvboxError(`subdomain ${problem}`);
|
|
1502
|
-
return
|
|
1557
|
+
return text2;
|
|
1503
1558
|
}
|
|
1504
|
-
function assertRouteName(
|
|
1505
|
-
const problem = routeNameProblem(
|
|
1559
|
+
function assertRouteName(text2) {
|
|
1560
|
+
const problem = routeNameProblem(text2);
|
|
1506
1561
|
if (problem) throw new KvboxError(`name ${problem}`);
|
|
1507
|
-
return
|
|
1562
|
+
return text2;
|
|
1508
1563
|
}
|
|
1509
1564
|
|
|
1510
1565
|
// src/commands/vm/proxy/shared.ts
|
|
@@ -1533,14 +1588,14 @@ function parsePorts(texts) {
|
|
|
1533
1588
|
return n;
|
|
1534
1589
|
});
|
|
1535
1590
|
}
|
|
1536
|
-
async function findBox(client,
|
|
1537
|
-
const found = pickRef(await boxesFor(client,
|
|
1538
|
-
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)`);
|
|
1539
1594
|
return found;
|
|
1540
1595
|
}
|
|
1541
1596
|
|
|
1542
1597
|
// src/commands/vm/proxy/claim.ts
|
|
1543
|
-
var claimCommand = new
|
|
1598
|
+
var claimCommand = new Command8("claim").description("give a box its HTTPS address, https://<subdomain>.kvbox.app, serving one of its ports").argument("<box>", "box name").argument("<subdomain>", "3-63 lowercase letters, digits and '-'; must be free").requiredOption("--port <port>", "the box port the address serves").option("--json", "print the subdomain as JSON").action(async (name, subdomain, opts, cmd) => {
|
|
1544
1599
|
assertSubdomain(subdomain);
|
|
1545
1600
|
const [port] = parsePorts([opts.port]);
|
|
1546
1601
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
@@ -1563,11 +1618,11 @@ var claimCommand = new Command7("claim").description("give a box its HTTPS addre
|
|
|
1563
1618
|
});
|
|
1564
1619
|
|
|
1565
1620
|
// src/commands/vm/proxy/index.ts
|
|
1566
|
-
import { Command as
|
|
1621
|
+
import { Command as Command14 } from "commander";
|
|
1567
1622
|
|
|
1568
1623
|
// src/commands/vm/proxy/add.ts
|
|
1569
|
-
import { Command as
|
|
1570
|
-
var addCommand = new
|
|
1624
|
+
import { Command as Command9 } from "commander";
|
|
1625
|
+
var addCommand = new Command9("add").description("publish box ports on the internet; each gets a random public port, or an HTTPS name with --name").argument("<box>", "box name").argument("<port...>", "ports inside the box, e.g. 3000 8080").option("--udp", "UDP instead of TCP").option("--name <name>", "serve the port at https://<name>.<subdomain>.kvbox.app (one port; needs kvbox vm proxy claim first)").option("--json", "print the new forwards as JSON").action(async (name, portTexts, opts, cmd) => {
|
|
1571
1626
|
const guestPorts = parsePorts(portTexts);
|
|
1572
1627
|
if (opts.name !== void 0) {
|
|
1573
1628
|
assertRouteName(opts.name);
|
|
@@ -1595,12 +1650,12 @@ var addCommand = new Command8("add").description("publish box ports on the inter
|
|
|
1595
1650
|
return;
|
|
1596
1651
|
}
|
|
1597
1652
|
const updated = await spin(`publishing ${guestPorts.join(", ")} on ${name}\u2026`, client.addPorts(box, { ports: guestPorts.map((guestPort) => ({ guestPort, proto })) }));
|
|
1598
|
-
const
|
|
1653
|
+
const rows2 = rowsOf(updated, user, (f) => f.proto === proto && guestPorts.includes(f.guestPort));
|
|
1599
1654
|
if (opts.json) {
|
|
1600
|
-
json(
|
|
1655
|
+
json(rows2);
|
|
1601
1656
|
return;
|
|
1602
1657
|
}
|
|
1603
|
-
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})`);
|
|
1604
1659
|
if (updated.status !== "running") info(` box ${name} is ${updated.status}: the ports open when it runs`);
|
|
1605
1660
|
} finally {
|
|
1606
1661
|
await client.close();
|
|
@@ -1608,7 +1663,7 @@ var addCommand = new Command8("add").description("publish box ports on the inter
|
|
|
1608
1663
|
});
|
|
1609
1664
|
|
|
1610
1665
|
// src/commands/vm/proxy/ls.ts
|
|
1611
|
-
import { Command as
|
|
1666
|
+
import { Command as Command10 } from "commander";
|
|
1612
1667
|
async function subdomainsOf(client) {
|
|
1613
1668
|
try {
|
|
1614
1669
|
return await client.listSubdomains();
|
|
@@ -1617,7 +1672,7 @@ async function subdomainsOf(client) {
|
|
|
1617
1672
|
return [];
|
|
1618
1673
|
}
|
|
1619
1674
|
}
|
|
1620
|
-
var lsCommand2 = new
|
|
1675
|
+
var lsCommand2 = new Command10("ls").description("list the box ports published on the internet").argument("[box]", "only this box").option("--json", "print as JSON").action(async (name, opts, cmd) => {
|
|
1621
1676
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1622
1677
|
try {
|
|
1623
1678
|
const user = me(client);
|
|
@@ -1627,24 +1682,24 @@ var lsCommand2 = new Command9("ls").description("list the box ports published on
|
|
|
1627
1682
|
]);
|
|
1628
1683
|
const shown = new Set(boxes.map((b) => displayName(b, user)));
|
|
1629
1684
|
const https = subs.flatMap((s) => httpsRowsOf(s, user)).filter((r) => shown.has(r.box));
|
|
1630
|
-
const
|
|
1685
|
+
const rows2 = [...boxes.flatMap((b) => rowsOf(b, user)), ...https].sort((a, b) => a.box.localeCompare(b.box) || a.guestPort - b.guestPort);
|
|
1631
1686
|
if (opts.json) {
|
|
1632
|
-
json(
|
|
1687
|
+
json(rows2);
|
|
1633
1688
|
return;
|
|
1634
1689
|
}
|
|
1635
|
-
if (
|
|
1690
|
+
if (rows2.length === 0) {
|
|
1636
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>)`);
|
|
1637
1692
|
return;
|
|
1638
1693
|
}
|
|
1639
|
-
table(
|
|
1694
|
+
table(rows2.map((r) => ({ box: r.box, port: r.guestPort, reaches: r.proto === "https" ? r.url : reaches(r, true), proto: r.proto })));
|
|
1640
1695
|
} finally {
|
|
1641
1696
|
await client.close();
|
|
1642
1697
|
}
|
|
1643
1698
|
});
|
|
1644
1699
|
|
|
1645
1700
|
// src/commands/vm/proxy/release.ts
|
|
1646
|
-
import { Command as
|
|
1647
|
-
var releaseCommand = new
|
|
1701
|
+
import { Command as Command11 } from "commander";
|
|
1702
|
+
var releaseCommand = new Command11("release").description("give up a box's HTTPS address and every name under it").argument("<box>", "box name").option("-y, --yes", "do not ask for confirmation").action(async (name, opts, cmd) => {
|
|
1648
1703
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1649
1704
|
try {
|
|
1650
1705
|
requireCloud(client);
|
|
@@ -1663,8 +1718,8 @@ var releaseCommand = new Command10("release").description("give up a box's HTTPS
|
|
|
1663
1718
|
});
|
|
1664
1719
|
|
|
1665
1720
|
// src/commands/vm/proxy/rm.ts
|
|
1666
|
-
import { Command as
|
|
1667
|
-
var rmCommand = new
|
|
1721
|
+
import { Command as Command12 } from "commander";
|
|
1722
|
+
var rmCommand = new Command12("rm").description("stop publishing box ports, or an HTTPS name with --name").argument("<box>", "box name").argument("[port...]", "ports inside the box, as given to add").option("--udp", "the UDP forwards of those ports").option("--name <name>", "the HTTPS name to free (kvbox vm proxy release frees the whole subdomain)").option("-y, --yes", "do not ask for confirmation").action(async (name, portTexts, opts, cmd) => {
|
|
1668
1723
|
if (opts.name !== void 0) {
|
|
1669
1724
|
assertRouteName(opts.name);
|
|
1670
1725
|
if (portTexts.length > 0) throw new KvboxError("--name frees a name, not ports: kvbox vm proxy rm <box> --name <name>");
|
|
@@ -1696,8 +1751,8 @@ var rmCommand = new Command11("rm").description("stop publishing box ports, or a
|
|
|
1696
1751
|
});
|
|
1697
1752
|
|
|
1698
1753
|
// src/commands/vm/proxy/set-port.ts
|
|
1699
|
-
import { Command as
|
|
1700
|
-
var setPortCommand = new
|
|
1754
|
+
import { Command as Command13 } from "commander";
|
|
1755
|
+
var setPortCommand = new Command13("set-port").description("point a box's HTTPS address at another of its ports").argument("<box>", "box name").argument("<port>", "the box port https://<subdomain>.kvbox.app serves from now on").option("--json", "print the subdomain as JSON").action(async (name, portText, opts, cmd) => {
|
|
1701
1756
|
const [port] = parsePorts([portText]);
|
|
1702
1757
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1703
1758
|
try {
|
|
@@ -1715,12 +1770,12 @@ var setPortCommand = new Command12("set-port").description("point a box's HTTPS
|
|
|
1715
1770
|
});
|
|
1716
1771
|
|
|
1717
1772
|
// src/commands/vm/proxy/index.ts
|
|
1718
|
-
var proxyCommand = new
|
|
1773
|
+
var proxyCommand = new Command14("proxy").description("publish box ports on the internet").addCommand(addCommand).addCommand(lsCommand2).addCommand(rmCommand).addCommand(claimCommand).addCommand(setPortCommand).addCommand(releaseCommand);
|
|
1719
1774
|
for (const cmd of proxyCommand.commands) cmd.option("--org <slug>", "act in this org (- for your personal boxes)");
|
|
1720
1775
|
|
|
1721
1776
|
// src/commands/vm/rescale.ts
|
|
1722
|
-
import { Command as
|
|
1723
|
-
var rescaleCommand = new
|
|
1777
|
+
import { Command as Command15, Option as Option3 } from "commander";
|
|
1778
|
+
var rescaleCommand = new Command15("rescale").description("change a box's vCPUs, memory or disk (e.g. --vcpu 4 --mem 16GB); a running box restarts").argument("<name>", "box name").addOption(new Option3("-s, --size <size>", "a preset to start from").choices(SIZE_NAMES)).option("--vcpu <n>", "number of vCPUs").option("--mem <size>", "memory, e.g. 16GB or 4096MB").option("--disk <size>", "disk size, e.g. 32GB (grow only)").option("--json", "print the box as JSON").action(async (name, opts, cmd) => {
|
|
1724
1779
|
const req = {
|
|
1725
1780
|
...opts.size !== void 0 ? { size: opts.size } : {},
|
|
1726
1781
|
...opts.vcpu !== void 0 ? { vcpu: parseCount(opts.vcpu) } : {},
|
|
@@ -1748,25 +1803,25 @@ var rescaleCommand = new Command14("rescale").description("change a box's vCPUs,
|
|
|
1748
1803
|
await client.close();
|
|
1749
1804
|
}
|
|
1750
1805
|
});
|
|
1751
|
-
function parseCount(
|
|
1752
|
-
const n = Number(
|
|
1753
|
-
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`);
|
|
1754
1809
|
return n;
|
|
1755
1810
|
}
|
|
1756
|
-
function parseSize(flag,
|
|
1757
|
-
const mib = parseMib(
|
|
1758
|
-
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`);
|
|
1759
1814
|
return mib;
|
|
1760
1815
|
}
|
|
1761
|
-
function parseGib(flag,
|
|
1762
|
-
const mib = parseSize(flag,
|
|
1763
|
-
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`);
|
|
1764
1819
|
return mib / 1024;
|
|
1765
1820
|
}
|
|
1766
1821
|
|
|
1767
1822
|
// src/commands/vm/resume.ts
|
|
1768
|
-
import { Command as
|
|
1769
|
-
var resumeCommand = new
|
|
1823
|
+
import { Command as Command16 } from "commander";
|
|
1824
|
+
var resumeCommand = new Command16("resume").description("continue a box that is in standby (kvbox pause)").argument("<name>", "box name").option("--json", "print the box as JSON").action(async (name, opts, cmd) => {
|
|
1770
1825
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1771
1826
|
try {
|
|
1772
1827
|
const box = await spin(`resuming ${name}\u2026`, client.resumeMachine(name));
|
|
@@ -1778,8 +1833,8 @@ var resumeCommand = new Command15("resume").description("continue a box that is
|
|
|
1778
1833
|
});
|
|
1779
1834
|
|
|
1780
1835
|
// src/commands/vm/rm.ts
|
|
1781
|
-
import { Command as
|
|
1782
|
-
var rmCommand2 = new
|
|
1836
|
+
import { Command as Command17 } from "commander";
|
|
1837
|
+
var rmCommand2 = new Command17("rm").description("destroy a box (its disk is deleted; snapshots are kept)").argument("<name>", "box name").option("-y, --yes", "do not ask for confirmation").action(async (name, opts, cmd) => {
|
|
1783
1838
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1784
1839
|
try {
|
|
1785
1840
|
const box = pickRef(await boxesFor(client, name), name, me(client));
|
|
@@ -1798,8 +1853,8 @@ var rmCommand2 = new Command16("rm").description("destroy a box (its disk is del
|
|
|
1798
1853
|
});
|
|
1799
1854
|
|
|
1800
1855
|
// src/commands/vm/set.ts
|
|
1801
|
-
import { Command as
|
|
1802
|
-
var setCommand = new
|
|
1856
|
+
import { Command as Command18 } from "commander";
|
|
1857
|
+
var setCommand = new Command18("set").description("change a box's settings, e.g. --auto-hibernate 30m (off: never)").argument("<name>", "box name").option("--auto-hibernate <duration>", "hibernate the box after this long without network traffic (30m, 4h, 1d; off: never)").option("--json", "print the box as JSON").action(async (name, opts, cmd) => {
|
|
1803
1858
|
if (opts.autoHibernate === void 0) throw new KvboxError("nothing to change: give --auto-hibernate");
|
|
1804
1859
|
const autoHibernateSec = parseIdleTimeout(opts.autoHibernate);
|
|
1805
1860
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
@@ -1818,8 +1873,8 @@ var setCommand = new Command17("set").description("change a box's settings, e.g.
|
|
|
1818
1873
|
});
|
|
1819
1874
|
|
|
1820
1875
|
// src/commands/vm/start.ts
|
|
1821
|
-
import { Command as
|
|
1822
|
-
var startCommand = new
|
|
1876
|
+
import { Command as Command19 } from "commander";
|
|
1877
|
+
var startCommand = new Command19("start").description("boot a stopped box again (same name, IP and disk)").argument("<name>", "box name").option("--json", "print the box as JSON").action(async (name, opts, cmd) => {
|
|
1823
1878
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1824
1879
|
try {
|
|
1825
1880
|
const box = await spin(`booting ${name}\u2026`, client.startMachine(name));
|
|
@@ -1831,8 +1886,8 @@ var startCommand = new Command18("start").description("boot a stopped box again
|
|
|
1831
1886
|
});
|
|
1832
1887
|
|
|
1833
1888
|
// src/commands/vm/stop.ts
|
|
1834
|
-
import { Command as
|
|
1835
|
-
var stopCommand = new
|
|
1889
|
+
import { Command as Command20 } from "commander";
|
|
1890
|
+
var stopCommand = new Command20("stop").description("shut a box down (its disk is kept; start it again with kvbox start)").argument("<name>", "box name").option("--json", "print the box as JSON").action(async (name, opts, cmd) => {
|
|
1836
1891
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1837
1892
|
try {
|
|
1838
1893
|
const box = await spin(`shutting ${name} down\u2026`, client.stopMachine(name));
|
|
@@ -1844,8 +1899,8 @@ var stopCommand = new Command19("stop").description("shut a box down (its disk i
|
|
|
1844
1899
|
});
|
|
1845
1900
|
|
|
1846
1901
|
// src/commands/vm/top.ts
|
|
1847
|
-
import { Command as
|
|
1848
|
-
var topCommand = new
|
|
1902
|
+
import { Command as Command21 } from "commander";
|
|
1903
|
+
var topCommand = new Command21("top").description("load, memory and disk of each running box, as the box itself sees them").option("-a, --all", "every running box on the platform (platform admins only)").option("--json", "print as JSON").action(async (opts, cmd) => {
|
|
1849
1904
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1850
1905
|
try {
|
|
1851
1906
|
const running = (await client.listMachines({ all: opts.all })).filter((b) => b.status === "running");
|
|
@@ -1905,8 +1960,8 @@ function topRows(entries, cloud) {
|
|
|
1905
1960
|
}
|
|
1906
1961
|
|
|
1907
1962
|
// src/commands/vm/update.ts
|
|
1908
|
-
import { Command as
|
|
1909
|
-
var updateCommand = new
|
|
1963
|
+
import { Command as Command22 } from "commander";
|
|
1964
|
+
var updateCommand = new Command22("update").description("move a box onto the current base image (its files stay; a running box restarts)").argument("<name>", "box name").option("--json", "print the box as JSON").action(async (name, opts, cmd) => {
|
|
1910
1965
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1911
1966
|
try {
|
|
1912
1967
|
const before = pickRef(await boxesFor(client, name), name, me(client));
|
|
@@ -1928,8 +1983,8 @@ var updateCommand = new Command21("update").description("move a box onto the cur
|
|
|
1928
1983
|
});
|
|
1929
1984
|
|
|
1930
1985
|
// src/commands/vm/usage.ts
|
|
1931
|
-
import { Command as
|
|
1932
|
-
var usageCommand = new
|
|
1986
|
+
import { Command as Command23 } from "commander";
|
|
1987
|
+
var usageCommand = new Command23("usage").description("how long a box ran, slept and sat stopped, as vCPU-hours, RAM and disk GiB-hours (and cost, when the host prices them)").argument("<name>", "box name").option("--since <date>", "count from this date instead of the box's creation, e.g. 2026-10-01").option("--json", "print as JSON").action(async (name, opts, cmd) => {
|
|
1933
1988
|
let since;
|
|
1934
1989
|
if (opts.since !== void 0) {
|
|
1935
1990
|
const t = Date.parse(opts.since);
|
|
@@ -1955,7 +2010,7 @@ function money(amount, currency) {
|
|
|
1955
2010
|
}
|
|
1956
2011
|
function block(u) {
|
|
1957
2012
|
const estimated = u.estimatedStops === 0 ? "" : color.dim(` (${u.estimatedStops} stop time${u.estimatedStops === 1 ? "" : "s"} estimated)`);
|
|
1958
|
-
const
|
|
2013
|
+
const rows2 = [
|
|
1959
2014
|
["name", boxName(u.name)],
|
|
1960
2015
|
["status", status(u.status)],
|
|
1961
2016
|
["window", `${day(u.window.from)} to ${day(u.window.to)}`],
|
|
@@ -1967,17 +2022,17 @@ function block(u) {
|
|
|
1967
2022
|
["ram GiB-h", hours(u.ramGibHours)],
|
|
1968
2023
|
["disk GiB-h", hours(u.diskGibHours)]
|
|
1969
2024
|
];
|
|
1970
|
-
if (u.cost !== void 0)
|
|
2025
|
+
if (u.cost !== void 0) rows2.push(["cost", `${money(u.cost.amount, u.cost.currency)} ${color.dim("(estimated)")}`]);
|
|
1971
2026
|
if (u.historyFrom !== null && u.historyFrom > u.window.from) {
|
|
1972
|
-
|
|
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`)]);
|
|
1973
2028
|
}
|
|
1974
|
-
const w = Math.max(...
|
|
1975
|
-
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";
|
|
1976
2031
|
}
|
|
1977
2032
|
|
|
1978
2033
|
// src/commands/vm/wake.ts
|
|
1979
|
-
import { Command as
|
|
1980
|
-
var wakeCommand = new
|
|
2034
|
+
import { Command as Command24 } from "commander";
|
|
2035
|
+
var wakeCommand = new Command24("wake").description("continue a hibernated box where it left off (same name, IP and disk)").argument("<name>", "box name").option("--json", "print the box as JSON").action(async (name, opts, cmd) => {
|
|
1981
2036
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1982
2037
|
try {
|
|
1983
2038
|
const box = await spin(`waking ${name} (loading its memory from disk)\u2026`, client.wakeMachine(name));
|
|
@@ -1989,15 +2044,15 @@ var wakeCommand = new Command23("wake").description("continue a hibernated box w
|
|
|
1989
2044
|
});
|
|
1990
2045
|
|
|
1991
2046
|
// src/commands/vm/index.ts
|
|
1992
|
-
var vmCommand = new
|
|
2047
|
+
var vmCommand = new Command25("vm").enablePositionalOptions().description("manage boxes (each one is a Firecracker microVM)").addCommand(newCommand).addCommand(forkCommand).addCommand(lsCommand).addCommand(connectCommand).addCommand(execCommand).addCommand(stopCommand).addCommand(startCommand).addCommand(pauseCommand).addCommand(resumeCommand).addCommand(hibernateCommand).addCommand(wakeCommand).addCommand(updateCommand).addCommand(rescaleCommand).addCommand(setCommand).addCommand(usageCommand).addCommand(topCommand).addCommand(proxyCommand).addCommand(rmCommand2);
|
|
1993
2048
|
for (const cmd of vmCommand.commands) if (cmd !== proxyCommand) cmd.option("--org <slug>", "act in this org (- for your personal boxes)");
|
|
1994
2049
|
var vmAliases = [newCommand, lsCommand, connectCommand, execCommand, stopCommand, startCommand, pauseCommand, resumeCommand, hibernateCommand, wakeCommand, rmCommand2, claimCommand];
|
|
1995
2050
|
|
|
1996
2051
|
// src/commands/server/index.ts
|
|
1997
|
-
import { Command as
|
|
2052
|
+
import { Command as Command33 } from "commander";
|
|
1998
2053
|
|
|
1999
2054
|
// src/commands/server/join.ts
|
|
2000
|
-
import { Command as
|
|
2055
|
+
import { Command as Command26 } from "commander";
|
|
2001
2056
|
|
|
2002
2057
|
// src/commands/server/daemon.ts
|
|
2003
2058
|
import { spawn as spawn3 } from "child_process";
|
|
@@ -2034,7 +2089,7 @@ async function restartLocalServer(p = paths()) {
|
|
|
2034
2089
|
return (await startLocalServer(p)).pid;
|
|
2035
2090
|
}
|
|
2036
2091
|
function runKvboxServer(args) {
|
|
2037
|
-
return new Promise((
|
|
2092
|
+
return new Promise((resolve3, reject) => {
|
|
2038
2093
|
const child = spawn3("kvbox-server", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2039
2094
|
let out = "";
|
|
2040
2095
|
let err = "";
|
|
@@ -2044,14 +2099,14 @@ function runKvboxServer(args) {
|
|
|
2044
2099
|
reject(new KvboxError(e.code === "ENOENT" ? NOT_A_HOST_HINT : `could not run kvbox-server: ${e.message}`));
|
|
2045
2100
|
});
|
|
2046
2101
|
child.once("close", (code) => {
|
|
2047
|
-
if (code === 0)
|
|
2102
|
+
if (code === 0) resolve3(out.trimEnd());
|
|
2048
2103
|
else reject(new KvboxError(err.trim().replace(/^error: /, "") || `kvbox-server ${args[0] ?? ""} exited with code ${String(code)}`, code === 2 ? 2 : 1));
|
|
2049
2104
|
});
|
|
2050
2105
|
});
|
|
2051
2106
|
}
|
|
2052
2107
|
|
|
2053
2108
|
// src/commands/server/join.ts
|
|
2054
|
-
var joinCommand = new
|
|
2109
|
+
var joinCommand = new Command26("join").description("put this machine behind a control plane (needs a one-time token minted there) and restart its server").argument("<control-plane-url>", "the control plane, e.g. https://api.kvbox.sh").requiredOption("--token <token>", "one-time join token from the control plane").option("--advertise <host-or-ip>", "address the control plane reaches this machine at (default: its default-route address)").option("--name <name>", "how the control plane lists this machine (default: hostname)").action(async (url, opts) => {
|
|
2055
2110
|
const args = ["join", url, "--token", opts.token];
|
|
2056
2111
|
if (opts.advertise !== void 0) args.push("--advertise", opts.advertise);
|
|
2057
2112
|
if (opts.name !== void 0) args.push("--name", opts.name);
|
|
@@ -2061,8 +2116,8 @@ var joinCommand = new Command25("join").description("put this machine behind a c
|
|
|
2061
2116
|
});
|
|
2062
2117
|
|
|
2063
2118
|
// src/commands/server/leave.ts
|
|
2064
|
-
import { Command as
|
|
2065
|
-
var leaveCommand = new
|
|
2119
|
+
import { Command as Command27 } from "commander";
|
|
2120
|
+
var leaveCommand = new Command27("leave").description("take this machine out from behind its control plane and restart its server").action(async () => {
|
|
2066
2121
|
const p = paths();
|
|
2067
2122
|
for (const line of (await runKvboxServer(["leave"])).split("\n")) info(line);
|
|
2068
2123
|
if (await runningPid(p) === null) return;
|
|
@@ -2071,14 +2126,14 @@ var leaveCommand = new Command26("leave").description("take this machine out fro
|
|
|
2071
2126
|
});
|
|
2072
2127
|
|
|
2073
2128
|
// src/commands/server/relay.ts
|
|
2074
|
-
import { Command as
|
|
2075
|
-
var relayCommand = new
|
|
2129
|
+
import { Command as Command28 } from "commander";
|
|
2130
|
+
var relayCommand = new Command28("relay").description("reach the internet through a relay (paste what `kvbox-relay add` printed on the relay VM); `off` takes the tunnel down").argument("<join-string>", "the line `kvbox-relay add` printed, or `off`").action(async (joinString) => {
|
|
2076
2131
|
for (const line of (await runKvboxServer(["relay", joinString])).split("\n")) info(line);
|
|
2077
2132
|
});
|
|
2078
2133
|
|
|
2079
2134
|
// src/commands/server/start.ts
|
|
2080
|
-
import { Command as
|
|
2081
|
-
var startCommand2 = new
|
|
2135
|
+
import { Command as Command29 } from "commander";
|
|
2136
|
+
var startCommand2 = new Command29("start").description("start the local kvbox-server in the background").action(async (_opts, cmd) => {
|
|
2082
2137
|
const client = await clientFor({ ...cmd.optsWithGlobals(), local: true }, { autoStart: false });
|
|
2083
2138
|
try {
|
|
2084
2139
|
const health = await client.health();
|
|
@@ -2094,8 +2149,8 @@ var startCommand2 = new Command28("start").description("start the local kvbox-se
|
|
|
2094
2149
|
});
|
|
2095
2150
|
|
|
2096
2151
|
// src/commands/server/status.ts
|
|
2097
|
-
import { Command as
|
|
2098
|
-
var statusCommand = new
|
|
2152
|
+
import { Command as Command30 } from "commander";
|
|
2153
|
+
var statusCommand = new Command30("status").description("show whether this machine's server is running, its control plane and which base image new boxes get").option("--json", "print as JSON").action(async (opts, cmd) => {
|
|
2099
2154
|
const client = await clientFor({ ...cmd.optsWithGlobals(), local: true });
|
|
2100
2155
|
try {
|
|
2101
2156
|
const health = await client.health();
|
|
@@ -2127,8 +2182,8 @@ var statusCommand = new Command29("status").description("show whether this machi
|
|
|
2127
2182
|
});
|
|
2128
2183
|
|
|
2129
2184
|
// src/commands/server/stop.ts
|
|
2130
|
-
import { Command as
|
|
2131
|
-
var stopCommand2 = new
|
|
2185
|
+
import { Command as Command31 } from "commander";
|
|
2186
|
+
var stopCommand2 = new Command31("stop").description("stop the local server (running boxes are not affected)").action(async () => {
|
|
2132
2187
|
const pid = await runningPid(paths());
|
|
2133
2188
|
if (pid === null) throw new KvboxError("kvbox-server is not running");
|
|
2134
2189
|
await stopLocalServer(pid);
|
|
@@ -2136,8 +2191,8 @@ var stopCommand2 = new Command30("stop").description("stop the local server (run
|
|
|
2136
2191
|
});
|
|
2137
2192
|
|
|
2138
2193
|
// src/commands/server/trust-ca.ts
|
|
2139
|
-
import { Command as
|
|
2140
|
-
var trustCaCommand = new
|
|
2194
|
+
import { Command as Command32 } from "commander";
|
|
2195
|
+
var trustCaCommand = new Command32("trust-ca").description("make every cloud box on this machine trust a new SSH CA (after `ca rotate` on the control plane)").argument("[public-key]", "the CA public key (or its cert-authority line); or --from-cloud").option("--from-cloud", "ask the control plane this host joined for its current CA, show the fingerprint, then ask").option("-y, --yes", "do not ask for confirmation").option("--json", "print the report as JSON").action(async (publicKey, opts, cmd) => {
|
|
2141
2196
|
if (publicKey === void 0 && !opts.fromCloud) throw new KvboxError("give the CA public key, or --from-cloud to fetch it from the control plane this host joined");
|
|
2142
2197
|
if (publicKey !== void 0 && opts.fromCloud) throw new KvboxError("either a public key or --from-cloud, not both");
|
|
2143
2198
|
const client = await clientFor({ ...cmd.optsWithGlobals(), local: true });
|
|
@@ -2175,16 +2230,140 @@ function describeReport(report) {
|
|
|
2175
2230
|
}
|
|
2176
2231
|
|
|
2177
2232
|
// src/commands/server/index.ts
|
|
2178
|
-
var serverCommand = new
|
|
2233
|
+
var serverCommand = new Command33("server").description("run or manage the kvbox server").addCommand(startCommand2).addCommand(statusCommand).addCommand(stopCommand2).addCommand(joinCommand).addCommand(leaveCommand).addCommand(relayCommand).addCommand(trustCaCommand);
|
|
2179
2234
|
|
|
2180
2235
|
// src/commands/login.ts
|
|
2181
2236
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
2182
2237
|
import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
2183
2238
|
import { hostname as osHostname } from "os";
|
|
2184
|
-
import { createInterface } from "readline/promises";
|
|
2239
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
2185
2240
|
import { Writable } from "stream";
|
|
2186
|
-
import { Command as
|
|
2187
|
-
|
|
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
|
|
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) => {
|
|
2188
2367
|
const url = normalizeUrl(urlArg ?? DEFAULT_CLOUD_URL);
|
|
2189
2368
|
const creds = await readCredentials(opts);
|
|
2190
2369
|
const session = await runLogin(url, creds, paths());
|
|
@@ -2240,39 +2419,21 @@ async function ensureSshKey(p = paths(), hostname = osHostname()) {
|
|
|
2240
2419
|
return (await readFile5(pubPath, "utf8")).trim();
|
|
2241
2420
|
}
|
|
2242
2421
|
async function prompt(question, io) {
|
|
2243
|
-
const rl =
|
|
2244
|
-
return (await
|
|
2422
|
+
const rl = createInterface2({ input: io.input, output: io.output });
|
|
2423
|
+
return (await askLine(rl, question)).trim();
|
|
2245
2424
|
}
|
|
2246
2425
|
async function promptSecret(question, io) {
|
|
2247
2426
|
const sink = new Writable({ write: (_chunk, _enc, cb) => cb() });
|
|
2248
|
-
const rl =
|
|
2427
|
+
const rl = createInterface2({ input: io.input, output: sink, terminal: true });
|
|
2249
2428
|
io.output.write(question);
|
|
2250
2429
|
try {
|
|
2251
|
-
return await
|
|
2430
|
+
return await askLine(rl, question);
|
|
2252
2431
|
} finally {
|
|
2253
2432
|
io.output.write("\n");
|
|
2254
2433
|
}
|
|
2255
2434
|
}
|
|
2256
|
-
async function ask(rl, question) {
|
|
2257
|
-
let onClose = () => {
|
|
2258
|
-
};
|
|
2259
|
-
const closed = new Promise((_, reject) => {
|
|
2260
|
-
onClose = () => reject(new KvboxError("cancelled", 130));
|
|
2261
|
-
rl.once("close", onClose);
|
|
2262
|
-
});
|
|
2263
|
-
try {
|
|
2264
|
-
return await Promise.race([rl.question(question).catch(cancelled), closed]);
|
|
2265
|
-
} finally {
|
|
2266
|
-
rl.off("close", onClose);
|
|
2267
|
-
rl.close();
|
|
2268
|
-
}
|
|
2269
|
-
}
|
|
2270
|
-
function cancelled(err) {
|
|
2271
|
-
if (err.code === "ABORT_ERR") throw new KvboxError("cancelled", 130);
|
|
2272
|
-
throw err;
|
|
2273
|
-
}
|
|
2274
2435
|
async function firstLine(stream) {
|
|
2275
|
-
const rl =
|
|
2436
|
+
const rl = createInterface2({ input: stream });
|
|
2276
2437
|
try {
|
|
2277
2438
|
for await (const line of rl) return line.trim();
|
|
2278
2439
|
return "";
|
|
@@ -2282,8 +2443,8 @@ async function firstLine(stream) {
|
|
|
2282
2443
|
}
|
|
2283
2444
|
|
|
2284
2445
|
// src/commands/logout.ts
|
|
2285
|
-
import { Command as
|
|
2286
|
-
var logoutCommand = new
|
|
2446
|
+
import { Command as Command35 } from "commander";
|
|
2447
|
+
var logoutCommand = new Command35("logout").description("log out of the control plane; commands target this machine's kvbox-server again").action(async () => {
|
|
2287
2448
|
const p = paths();
|
|
2288
2449
|
const session = await readSession(p).catch(() => null);
|
|
2289
2450
|
switch (await runLogout(p)) {
|
|
@@ -2320,8 +2481,8 @@ async function runLogout(p = paths()) {
|
|
|
2320
2481
|
}
|
|
2321
2482
|
|
|
2322
2483
|
// src/commands/whoami.ts
|
|
2323
|
-
import { Command as
|
|
2324
|
-
var whoamiCommand = new
|
|
2484
|
+
import { Command as Command36 } from "commander";
|
|
2485
|
+
var whoamiCommand = new Command36("whoami").description("show which account and control plane commands target (or local mode)").option("--json", "print as JSON").action(async (opts) => {
|
|
2325
2486
|
const session = await readSession();
|
|
2326
2487
|
if (opts.json) {
|
|
2327
2488
|
json(session ? { user: session.user, url: session.url, sshCertExpires: session.sshCertExpires ?? null } : null);
|
|
@@ -2337,11 +2498,11 @@ function describeSession(session) {
|
|
|
2337
2498
|
}
|
|
2338
2499
|
|
|
2339
2500
|
// src/commands/snapshots/index.ts
|
|
2340
|
-
import { Command as
|
|
2501
|
+
import { Command as Command40 } from "commander";
|
|
2341
2502
|
|
|
2342
2503
|
// src/commands/snapshots/ls.ts
|
|
2343
|
-
import { Command as
|
|
2344
|
-
var lsCommand3 = new
|
|
2504
|
+
import { Command as Command37 } from "commander";
|
|
2505
|
+
var lsCommand3 = new Command37("ls").description("list snapshots (yours and your orgs'; --org <slug> for one org)").option("-a, --all", "every snapshot on the platform (platform admins only)").option("--json", "print as JSON").action(async (opts, cmd) => {
|
|
2345
2506
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
2346
2507
|
try {
|
|
2347
2508
|
const cloud = client.target.kind === "cloud";
|
|
@@ -2372,8 +2533,8 @@ var lsCommand3 = new Command36("ls").description("list snapshots (yours and your
|
|
|
2372
2533
|
});
|
|
2373
2534
|
|
|
2374
2535
|
// src/commands/snapshots/new.ts
|
|
2375
|
-
import { Command as
|
|
2376
|
-
var newCommand2 = new
|
|
2536
|
+
import { Command as Command38 } from "commander";
|
|
2537
|
+
var newCommand2 = new Command38("new").description("freeze a running box (memory + disk) into a snapshot; the box keeps running").argument("<box>", "box name").argument("<name>", "snapshot name").option("--json", "print the snapshot as JSON").action(async (box, name, opts, cmd) => {
|
|
2377
2538
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
2378
2539
|
try {
|
|
2379
2540
|
const snap = await spin(`saving snapshot ${name} from box ${box} (memory + disk)\u2026`, client.createSnapshot({ box, name }));
|
|
@@ -2385,8 +2546,8 @@ var newCommand2 = new Command37("new").description("freeze a running box (memory
|
|
|
2385
2546
|
});
|
|
2386
2547
|
|
|
2387
2548
|
// src/commands/snapshots/rm.ts
|
|
2388
|
-
import { Command as
|
|
2389
|
-
var rmCommand3 = new
|
|
2549
|
+
import { Command as Command39 } from "commander";
|
|
2550
|
+
var rmCommand3 = new Command39("rm").description("delete a snapshot (boxes made from it are not affected)").argument("<name>", "snapshot name").option("-y, --yes", "do not ask for confirmation").action(async (name, opts, cmd) => {
|
|
2390
2551
|
if (!await confirm(`Delete snapshot ${name}?`, opts.yes ?? false)) {
|
|
2391
2552
|
info("cancelled");
|
|
2392
2553
|
process.exitCode = 1;
|
|
@@ -2402,14 +2563,14 @@ var rmCommand3 = new Command38("rm").description("delete a snapshot (boxes made
|
|
|
2402
2563
|
});
|
|
2403
2564
|
|
|
2404
2565
|
// src/commands/snapshots/index.ts
|
|
2405
|
-
var snapshotsCommand = new
|
|
2566
|
+
var snapshotsCommand = new Command40("snapshots").aliases(["snapshot", "snap"]).description("freeze running boxes into snapshots and manage them").addCommand(newCommand2).addCommand(lsCommand3).addCommand(rmCommand3);
|
|
2406
2567
|
for (const cmd of snapshotsCommand.commands) cmd.option("--org <slug>", "act in this org (- for your personal boxes)");
|
|
2407
2568
|
|
|
2408
2569
|
// src/commands/org/index.ts
|
|
2409
|
-
import { Command as
|
|
2570
|
+
import { Command as Command46 } from "commander";
|
|
2410
2571
|
|
|
2411
2572
|
// src/commands/org/ls.ts
|
|
2412
|
-
import { Command as
|
|
2573
|
+
import { Command as Command41 } from "commander";
|
|
2413
2574
|
|
|
2414
2575
|
// src/commands/org/context.ts
|
|
2415
2576
|
var NOT_LOGGED_IN = "orgs live on the control plane: kvbox login first";
|
|
@@ -2429,7 +2590,7 @@ function orgFor(opts, target) {
|
|
|
2429
2590
|
}
|
|
2430
2591
|
|
|
2431
2592
|
// src/commands/org/ls.ts
|
|
2432
|
-
var lsCommand4 = new
|
|
2593
|
+
var lsCommand4 = new Command41("ls").description("list the orgs you belong to and your role in each").option("--json", "print as JSON").action(async (opts, cmd) => {
|
|
2433
2594
|
const client = await cloudClientFor(cmd.optsWithGlobals());
|
|
2434
2595
|
try {
|
|
2435
2596
|
const orgs = await client.listOrgs();
|
|
@@ -2451,8 +2612,8 @@ function orgRows(orgs) {
|
|
|
2451
2612
|
}
|
|
2452
2613
|
|
|
2453
2614
|
// src/commands/org/members.ts
|
|
2454
|
-
import { Command as
|
|
2455
|
-
var lsCommand5 = new
|
|
2615
|
+
import { Command as Command42, Option as Option4 } from "commander";
|
|
2616
|
+
var lsCommand5 = new Command42("ls").description("list an org's members and their roles").option("-o, --org <slug>", "the org (or --org before the command)").option("--json", "print as JSON").action(async (opts, cmd) => {
|
|
2456
2617
|
const client = await cloudClientFor(cmd.optsWithGlobals());
|
|
2457
2618
|
try {
|
|
2458
2619
|
const org = await client.getOrg(orgFor(opts, client.target));
|
|
@@ -2466,7 +2627,7 @@ var lsCommand5 = new Command41("ls").description("list an org's members and thei
|
|
|
2466
2627
|
await client.close();
|
|
2467
2628
|
}
|
|
2468
2629
|
});
|
|
2469
|
-
var addCommand2 = new
|
|
2630
|
+
var addCommand2 = new Command42("add").description("add a user to an org (admins and owners; only an owner can add an owner)").argument("<user>", "their username").addOption(new Option4("-r, --role <role>", "their role").choices(ORG_ROLES).default("member")).option("-o, --org <slug>", "the org (or --org before the command)").action(async (user, opts, cmd) => {
|
|
2470
2631
|
const client = await cloudClientFor(cmd.optsWithGlobals());
|
|
2471
2632
|
try {
|
|
2472
2633
|
const slug = orgFor(opts, client.target);
|
|
@@ -2476,7 +2637,7 @@ var addCommand2 = new Command41("add").description("add a user to an org (admins
|
|
|
2476
2637
|
await client.close();
|
|
2477
2638
|
}
|
|
2478
2639
|
});
|
|
2479
|
-
var rmCommand4 = new
|
|
2640
|
+
var rmCommand4 = new Command42("rm").description("remove a member (or leave: remove yourself)").argument("<user>", "their username").option("-o, --org <slug>", "the org (or --org before the command)").option("-y, --yes", "do not ask for confirmation").action(async (user, opts, cmd) => {
|
|
2480
2641
|
const client = await cloudClientFor(cmd.optsWithGlobals());
|
|
2481
2642
|
try {
|
|
2482
2643
|
const slug = orgFor(opts, client.target);
|
|
@@ -2492,11 +2653,11 @@ var rmCommand4 = new Command41("rm").description("remove a member (or leave: rem
|
|
|
2492
2653
|
await client.close();
|
|
2493
2654
|
}
|
|
2494
2655
|
});
|
|
2495
|
-
var membersCommand = new
|
|
2656
|
+
var membersCommand = new Command42("members").description("who is in an org and what they may do").addCommand(lsCommand5).addCommand(addCommand2).addCommand(rmCommand4);
|
|
2496
2657
|
|
|
2497
2658
|
// src/commands/org/new.ts
|
|
2498
|
-
import { Command as
|
|
2499
|
-
var newCommand3 = new
|
|
2659
|
+
import { Command as Command43 } from "commander";
|
|
2660
|
+
var newCommand3 = new Command43("new").description("create an org; you become its owner (the slug is permanent, the name is not)").argument("<slug>", "the org's slug: lowercase, like a username (acme)").option("-n, --name <name>", "display name (default: the slug)").option("--json", "print the org as JSON").action(async (slug, opts, cmd) => {
|
|
2500
2661
|
const client = await cloudClientFor(cmd.optsWithGlobals());
|
|
2501
2662
|
try {
|
|
2502
2663
|
const org = await client.createOrg({ slug, name: opts.name ?? slug });
|
|
@@ -2512,8 +2673,8 @@ var newCommand3 = new Command42("new").description("create an org; you become it
|
|
|
2512
2673
|
});
|
|
2513
2674
|
|
|
2514
2675
|
// src/commands/org/rm.ts
|
|
2515
|
-
import { Command as
|
|
2516
|
-
var rmCommand5 = new
|
|
2676
|
+
import { Command as Command44 } from "commander";
|
|
2677
|
+
var rmCommand5 = new Command44("rm").description("delete an org (owners only; remove its boxes and snapshots first)").argument("<slug>", "the org").option("-y, --yes", "do not ask for confirmation").action(async (slug, opts, cmd) => {
|
|
2517
2678
|
if (!await confirm(`Delete org ${slug}? Its members lose access; its slug stays taken.`, opts.yes ?? false)) {
|
|
2518
2679
|
info("cancelled");
|
|
2519
2680
|
process.exitCode = 1;
|
|
@@ -2529,8 +2690,8 @@ var rmCommand5 = new Command43("rm").description("delete an org (owners only; re
|
|
|
2529
2690
|
});
|
|
2530
2691
|
|
|
2531
2692
|
// src/commands/org/role.ts
|
|
2532
|
-
import { Argument, Command as
|
|
2533
|
-
var roleCommand = new
|
|
2693
|
+
import { Argument, Command as Command45 } from "commander";
|
|
2694
|
+
var roleCommand = new Command45("role").description("change a member's role (owners only)").argument("<user>", "their username").addArgument(new Argument("<role>", "owner, admin or member").choices(ORG_ROLES)).option("-o, --org <slug>", "the org (default: the current context)").action(async (user, role, opts, cmd) => {
|
|
2534
2695
|
const client = await cloudClientFor(cmd.optsWithGlobals());
|
|
2535
2696
|
try {
|
|
2536
2697
|
const slug = orgFor(opts, client.target);
|
|
@@ -2542,11 +2703,45 @@ var roleCommand = new Command44("role").description("change a member's role (own
|
|
|
2542
2703
|
});
|
|
2543
2704
|
|
|
2544
2705
|
// src/commands/org/index.ts
|
|
2545
|
-
var orgCommand = new
|
|
2706
|
+
var orgCommand = new Command46("org").description("orgs: shared slugs that own boxes, with owners, admins and members").addCommand(lsCommand4).addCommand(newCommand3).addCommand(membersCommand).addCommand(roleCommand).addCommand(rmCommand5);
|
|
2546
2707
|
|
|
2547
2708
|
// src/commands/usage.ts
|
|
2548
|
-
import { Command as
|
|
2549
|
-
|
|
2709
|
+
import { Command as Command47 } from "commander";
|
|
2710
|
+
|
|
2711
|
+
// ../shared/src/usage.ts
|
|
2712
|
+
import { z as z4 } from "zod";
|
|
2713
|
+
var PricingSchema = z4.object({
|
|
2714
|
+
currency: z4.string().min(1),
|
|
2715
|
+
vcpuHour: z4.number().nonnegative(),
|
|
2716
|
+
ramGibHour: z4.number().nonnegative(),
|
|
2717
|
+
diskGibHour: z4.number().nonnegative()
|
|
2718
|
+
});
|
|
2719
|
+
var round = (n, places) => Math.round(n * 10 ** places) / 10 ** places;
|
|
2720
|
+
function costOf(t, pricing) {
|
|
2721
|
+
return t.vcpuHours * pricing.vcpuHour + t.ramGibHours * pricing.ramGibHour + t.diskGibHours * pricing.diskGibHour;
|
|
2722
|
+
}
|
|
2723
|
+
function sumUsage(items, pricing) {
|
|
2724
|
+
const sum = {
|
|
2725
|
+
seconds: { running: 0, standby: 0, hibernated: 0, stopped: 0 },
|
|
2726
|
+
vcpuHours: 0,
|
|
2727
|
+
ramGibHours: 0,
|
|
2728
|
+
diskGibHours: 0
|
|
2729
|
+
};
|
|
2730
|
+
for (const t of items) {
|
|
2731
|
+
for (const s of Object.keys(sum.seconds)) sum.seconds[s] += t.seconds[s];
|
|
2732
|
+
sum.vcpuHours += t.vcpuHours;
|
|
2733
|
+
sum.ramGibHours += t.ramGibHours;
|
|
2734
|
+
sum.diskGibHours += t.diskGibHours;
|
|
2735
|
+
}
|
|
2736
|
+
sum.vcpuHours = round(sum.vcpuHours, 3);
|
|
2737
|
+
sum.ramGibHours = round(sum.ramGibHours, 3);
|
|
2738
|
+
sum.diskGibHours = round(sum.diskGibHours, 3);
|
|
2739
|
+
if (pricing !== void 0) sum.cost = { amount: round(costOf(sum, pricing), 4), currency: pricing.currency };
|
|
2740
|
+
return sum;
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
// src/commands/usage.ts
|
|
2744
|
+
var usageCommand2 = new Command47("usage").description("what your boxes used this month (or --since/--until): running time, vCPU-h, RAM and disk GiB-h, cost").option("--since <date>", "count from this date, e.g. 2026-10-01 (default: the 1st of this month)").option("--until <date>", "count up to this date (default: now)").option("--org <slug>", "one org's boxes only").option("-a, --all", "every box on the platform (platform admins only)").option("--by-user", "one line per user who made the boxes (e.g. who spent what in --org)").option("--json", "print as JSON").action(async (opts, cmd) => {
|
|
2550
2745
|
for (const [name, value] of [
|
|
2551
2746
|
["since", opts.since],
|
|
2552
2747
|
["until", opts.until]
|
|
@@ -2556,15 +2751,17 @@ var usageCommand2 = new Command46("usage").description("what your boxes used thi
|
|
|
2556
2751
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
2557
2752
|
try {
|
|
2558
2753
|
if (client.target.kind === "local" && cmd.optsWithGlobals().org) throw new KvboxError("--org needs a control plane: kvbox login, or drop --org");
|
|
2754
|
+
if (client.target.kind === "local" && opts.byUser) throw new KvboxError("--by-user needs a control plane: kvbox login, or drop --by-user");
|
|
2559
2755
|
const report = await client.usageReport(opts.since, opts.until, opts.all);
|
|
2756
|
+
const perUser = opts.byUser ? byUser(report) : void 0;
|
|
2560
2757
|
if (opts.json) {
|
|
2561
|
-
json(report);
|
|
2758
|
+
json(perUser ?? report);
|
|
2562
2759
|
return;
|
|
2563
2760
|
}
|
|
2564
2761
|
const cloud = client.target.kind === "cloud" ? { me: client.target.user } : void 0;
|
|
2565
2762
|
info(`${day2(report.window.from)} to ${day2(report.window.to)}`);
|
|
2566
2763
|
if (report.boxes.length === 0) info("no usage in this window");
|
|
2567
|
-
else table(usageRows(report, cloud));
|
|
2764
|
+
else table(perUser ? userRows(perUser) : usageRows(report, cloud));
|
|
2568
2765
|
if (cloud) {
|
|
2569
2766
|
const line = await creditLine(client, cloud.me);
|
|
2570
2767
|
if (line !== void 0) info(line);
|
|
@@ -2594,7 +2791,7 @@ function ownerCell2(b, me2) {
|
|
|
2594
2791
|
}
|
|
2595
2792
|
function usageRows(report, cloud) {
|
|
2596
2793
|
const priced = report.totals.cost !== void 0;
|
|
2597
|
-
const
|
|
2794
|
+
const rows2 = report.boxes.map((b) => ({
|
|
2598
2795
|
...cloud ? { owner: ownerCell2(b, cloud.me) } : {},
|
|
2599
2796
|
name: boxName(b.name),
|
|
2600
2797
|
status: b.status === "removed" ? color.dim("removed") : status(b.status),
|
|
@@ -2605,7 +2802,7 @@ function usageRows(report, cloud) {
|
|
|
2605
2802
|
...priced ? { cost: money(b.cost?.amount ?? 0, report.totals.cost.currency) } : {}
|
|
2606
2803
|
}));
|
|
2607
2804
|
const t = report.totals;
|
|
2608
|
-
|
|
2805
|
+
rows2.push({
|
|
2609
2806
|
...cloud ? { owner: "" } : {},
|
|
2610
2807
|
name: color.bold("total"),
|
|
2611
2808
|
status: "",
|
|
@@ -2615,12 +2812,52 @@ function usageRows(report, cloud) {
|
|
|
2615
2812
|
"disk GiB-h": hours(t.diskGibHours),
|
|
2616
2813
|
...priced ? { cost: `${money(t.cost.amount, t.cost.currency)} ${color.dim("(estimated)")}` } : {}
|
|
2617
2814
|
});
|
|
2618
|
-
return
|
|
2815
|
+
return rows2;
|
|
2816
|
+
}
|
|
2817
|
+
function byUser(report) {
|
|
2818
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2819
|
+
for (const b of report.boxes) {
|
|
2820
|
+
const user = b.createdBy ?? null;
|
|
2821
|
+
const list = groups.get(user);
|
|
2822
|
+
if (list === void 0) groups.set(user, [b]);
|
|
2823
|
+
else list.push(b);
|
|
2824
|
+
}
|
|
2825
|
+
const currency = report.totals.cost?.currency;
|
|
2826
|
+
const users = [...groups].map(([user, boxes]) => {
|
|
2827
|
+
const { cost: _none, ...sum } = sumUsage(boxes);
|
|
2828
|
+
const u = { user, boxes: boxes.length, ...sum };
|
|
2829
|
+
if (currency !== void 0) u.cost = { amount: round(boxes.reduce((n, b) => n + (b.cost?.amount ?? 0), 0), 4), currency };
|
|
2830
|
+
return u;
|
|
2831
|
+
});
|
|
2832
|
+
users.sort((a, b) => a.user === null ? 1 : b.user === null ? -1 : a.user.localeCompare(b.user));
|
|
2833
|
+
return { window: report.window, users, totals: report.totals };
|
|
2834
|
+
}
|
|
2835
|
+
function userRows(r) {
|
|
2836
|
+
const priced = r.totals.cost !== void 0;
|
|
2837
|
+
const cells = (t) => ({
|
|
2838
|
+
running: duration(t.seconds.running),
|
|
2839
|
+
"vcpu-h": hours(t.vcpuHours),
|
|
2840
|
+
"ram GiB-h": hours(t.ramGibHours),
|
|
2841
|
+
"disk GiB-h": hours(t.diskGibHours)
|
|
2842
|
+
});
|
|
2843
|
+
const rows2 = r.users.map((u) => ({
|
|
2844
|
+
user: u.user ?? color.dim("unknown"),
|
|
2845
|
+
boxes: String(u.boxes),
|
|
2846
|
+
...cells(u),
|
|
2847
|
+
...priced ? { cost: money(u.cost?.amount ?? 0, r.totals.cost.currency) } : {}
|
|
2848
|
+
}));
|
|
2849
|
+
rows2.push({
|
|
2850
|
+
user: color.bold("total"),
|
|
2851
|
+
boxes: String(r.users.reduce((n, u) => n + u.boxes, 0)),
|
|
2852
|
+
...cells(r.totals),
|
|
2853
|
+
...priced ? { cost: `${money(r.totals.cost.amount, r.totals.cost.currency)} ${color.dim("(estimated)")}` } : {}
|
|
2854
|
+
});
|
|
2855
|
+
return rows2;
|
|
2619
2856
|
}
|
|
2620
2857
|
|
|
2621
2858
|
// src/commands/credit.ts
|
|
2622
|
-
import { Command as
|
|
2623
|
-
var creditCommand = new
|
|
2859
|
+
import { Command as Command48 } from "commander";
|
|
2860
|
+
var creditCommand = new Command48("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) => {
|
|
2624
2861
|
const client = await clientFor(cmd.optsWithGlobals(), { autoStart: false });
|
|
2625
2862
|
try {
|
|
2626
2863
|
if (client.target.kind === "local") throw new KvboxError("credit lives on the control plane: kvbox login first");
|
|
@@ -2647,9 +2884,375 @@ function creditRow(b, me2) {
|
|
|
2647
2884
|
};
|
|
2648
2885
|
}
|
|
2649
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 join3 } from "path";
|
|
2895
|
+
import { z as z5 } from "zod";
|
|
2896
|
+
|
|
2897
|
+
// src/commands/deploy/templates/react.ts
|
|
2898
|
+
import { spawn as spawn4 } from "child_process";
|
|
2899
|
+
import { access } from "fs/promises";
|
|
2900
|
+
import { join as join2 } from "path";
|
|
2901
|
+
var OUT_DIRS = ["dist", "build"];
|
|
2902
|
+
async function packageManager(dir) {
|
|
2903
|
+
const has = (f) => access(join2(dir, f)).then(() => true, () => false);
|
|
2904
|
+
if (await has("pnpm-lock.yaml")) return "pnpm";
|
|
2905
|
+
if (await has("yarn.lock")) return "yarn";
|
|
2906
|
+
if (await has("bun.lockb") || await has("bun.lock")) return "bun";
|
|
2907
|
+
return "npm";
|
|
2908
|
+
}
|
|
2909
|
+
async function runBuild(dir, pm) {
|
|
2910
|
+
const args = pm === "yarn" ? ["build"] : ["run", "build"];
|
|
2911
|
+
info(`${color.dim("$")} ${pm} ${args.join(" ")}`);
|
|
2912
|
+
const status2 = await new Promise((resolve3, reject) => {
|
|
2913
|
+
const child = spawn4(pm, args, { cwd: dir, stdio: ["ignore", process.stderr, process.stderr], shell: process.platform === "win32" });
|
|
2914
|
+
child.once("error", (err) => reject(new KvboxError(`could not run ${pm}: ${err.message}`)));
|
|
2915
|
+
child.once("close", (code) => resolve3(code ?? 1));
|
|
2916
|
+
});
|
|
2917
|
+
if (status2 !== 0) throw new KvboxError(`the build failed (${pm} exited ${status2}); nothing was deployed`);
|
|
2918
|
+
}
|
|
2919
|
+
var reactTemplate = {
|
|
2920
|
+
id: "react",
|
|
2921
|
+
label: "React",
|
|
2922
|
+
hint: "static site: built here, served by nginx in the box",
|
|
2923
|
+
port: 80,
|
|
2924
|
+
/** A Vite or Create React App project. A framework with its own server (Next.js, Remix, Expo) is not a static site. */
|
|
2925
|
+
detect(pkg) {
|
|
2926
|
+
if (!pkg) return false;
|
|
2927
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2928
|
+
if (!("react" in deps) || "next" in deps || "@remix-run/react" in deps || "expo" in deps) return false;
|
|
2929
|
+
return "vite" in deps || "react-scripts" in deps;
|
|
2930
|
+
},
|
|
2931
|
+
async build(ctx) {
|
|
2932
|
+
if (typeof ctx.pkg?.scripts?.["build"] !== "string") throw new KvboxError(`${ctx.dir}: package.json has no "build" script`);
|
|
2933
|
+
await runBuild(ctx.dir, await packageManager(ctx.dir));
|
|
2934
|
+
const candidates = ctx.dist !== void 0 ? [ctx.dist] : OUT_DIRS;
|
|
2935
|
+
for (const dist of candidates) {
|
|
2936
|
+
const outDir = join2(ctx.dir, dist);
|
|
2937
|
+
if (await access(join2(outDir, "index.html")).then(() => true, () => false)) return { outDir, dist };
|
|
2938
|
+
}
|
|
2939
|
+
throw new KvboxError(
|
|
2940
|
+
`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`
|
|
2941
|
+
);
|
|
2942
|
+
},
|
|
2943
|
+
/**
|
|
2944
|
+
* The archive lands in a staging directory, which then becomes /srv/app/dist in one move; the
|
|
2945
|
+
* old site is kept beside it until the new one is in place, so a failed step never leaves nginx
|
|
2946
|
+
* without a root and a visitor never sees a half-copied site. Files get read permission for
|
|
2947
|
+
* nginx's user, whatever umask the build ran under. /srv/app is the kvbox user's (image v0.8.0+);
|
|
2948
|
+
* the missing site file means an older image.
|
|
2949
|
+
*/
|
|
2950
|
+
remote() {
|
|
2951
|
+
return [
|
|
2952
|
+
"set -e",
|
|
2953
|
+
"test -e /etc/nginx/sites-enabled/kvbox-app || exit 3",
|
|
2954
|
+
"rm -rf /srv/app/.incoming /srv/app/.old",
|
|
2955
|
+
"mkdir -p /srv/app/.incoming",
|
|
2956
|
+
"tar -xf - -C /srv/app/.incoming",
|
|
2957
|
+
"chmod -R a+rX /srv/app/.incoming",
|
|
2958
|
+
"if [ -e /srv/app/dist ]; then mv /srv/app/dist /srv/app/.old; fi",
|
|
2959
|
+
"mv /srv/app/.incoming /srv/app/dist",
|
|
2960
|
+
"rm -rf /srv/app/.old",
|
|
2961
|
+
"sudo -n systemctl enable --now --quiet nginx",
|
|
2962
|
+
"sudo -n systemctl reload nginx"
|
|
2963
|
+
].join("\n");
|
|
2964
|
+
}
|
|
2965
|
+
};
|
|
2966
|
+
|
|
2967
|
+
// src/commands/deploy/templates/index.ts
|
|
2968
|
+
var TEMPLATES = [reactTemplate];
|
|
2969
|
+
var TEMPLATE_IDS = TEMPLATES.map((t) => t.id);
|
|
2970
|
+
function templateById(id) {
|
|
2971
|
+
const t = TEMPLATES.find((x) => x.id === id);
|
|
2972
|
+
if (!t) throw new Error(`no template ${id}`);
|
|
2973
|
+
return t;
|
|
2974
|
+
}
|
|
2975
|
+
function detectTemplate(pkg) {
|
|
2976
|
+
return TEMPLATES.find((t) => t.detect(pkg));
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
// src/commands/deploy/config.ts
|
|
2980
|
+
var CONFIG_FILE = "kvbox.json";
|
|
2981
|
+
var BOX_NAME_RE = /^[a-z0-9][a-z0-9-]{0,11}$/;
|
|
2982
|
+
var DeployConfigSchema = z5.object({
|
|
2983
|
+
template: z5.enum(TEMPLATE_IDS),
|
|
2984
|
+
name: z5.string().regex(BOX_NAME_RE, "1-12 chars, lowercase letters, digits and '-', starting with a letter or digit"),
|
|
2985
|
+
/** The org whose box it is; absent for the user's own. */
|
|
2986
|
+
org: z5.string().regex(ORG_SLUG_RE, "not an org slug").optional(),
|
|
2987
|
+
/** The HTTPS name (`<subdomain>.kvbox.app`); absent when deploying to this machine's own host. */
|
|
2988
|
+
subdomain: z5.string().check((ctx) => {
|
|
2989
|
+
const problem = subdomainProblem(ctx.value);
|
|
2990
|
+
if (problem) ctx.issues.push({ code: "custom", message: problem, input: ctx.value });
|
|
2991
|
+
}).optional(),
|
|
2992
|
+
size: z5.enum(SIZE_NAMES).optional(),
|
|
2993
|
+
/** The build output directory, relative to the app; the template's default when absent. */
|
|
2994
|
+
dist: z5.string().optional()
|
|
2995
|
+
});
|
|
2996
|
+
async function readConfig(dir) {
|
|
2997
|
+
const path = join3(dir, CONFIG_FILE);
|
|
2998
|
+
let raw;
|
|
2999
|
+
try {
|
|
3000
|
+
raw = await readFile6(path, "utf8");
|
|
3001
|
+
} catch (err) {
|
|
3002
|
+
if (err.code === "ENOENT") return void 0;
|
|
3003
|
+
throw err;
|
|
3004
|
+
}
|
|
3005
|
+
let json2;
|
|
3006
|
+
try {
|
|
3007
|
+
json2 = JSON.parse(raw);
|
|
3008
|
+
} catch {
|
|
3009
|
+
throw new KvboxError(`${path}: not valid JSON`);
|
|
3010
|
+
}
|
|
3011
|
+
const parsed = DeployConfigSchema.safeParse(json2);
|
|
3012
|
+
if (!parsed.success) {
|
|
3013
|
+
const issue = parsed.error.issues[0];
|
|
3014
|
+
throw new KvboxError(`${path}: ${issue.path.join(".") || "file"}: ${issue.message}`);
|
|
3015
|
+
}
|
|
3016
|
+
return parsed.data;
|
|
3017
|
+
}
|
|
3018
|
+
async function writeConfig(dir, config) {
|
|
3019
|
+
await writeFile4(join3(dir, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n");
|
|
3020
|
+
}
|
|
3021
|
+
function nameFromDir(dirName) {
|
|
3022
|
+
const slug = dirName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 12).replace(/-+$/, "");
|
|
3023
|
+
return slug === "" ? "app" : slug;
|
|
3024
|
+
}
|
|
3025
|
+
function subdomainFor(name) {
|
|
3026
|
+
return subdomainProblem(name) === null ? name : `${name}-site`;
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
// src/commands/deploy/ship.ts
|
|
3030
|
+
import { spawn as spawn5 } from "child_process";
|
|
3031
|
+
import { readdir, stat as stat2 } from "fs/promises";
|
|
3032
|
+
import { join as join4 } from "path";
|
|
3033
|
+
var IMAGE_TOO_OLD = 3;
|
|
3034
|
+
var SSH_FAILED3 = 255;
|
|
3035
|
+
async function measure(dir) {
|
|
3036
|
+
let files = 0;
|
|
3037
|
+
let bytes = 0;
|
|
3038
|
+
for (const entry of await readdir(dir, { recursive: true, withFileTypes: true })) {
|
|
3039
|
+
if (!entry.isFile()) continue;
|
|
3040
|
+
files += 1;
|
|
3041
|
+
bytes += (await stat2(join4(entry.parentPath, entry.name))).size;
|
|
3042
|
+
}
|
|
3043
|
+
return { files, bytes };
|
|
3044
|
+
}
|
|
3045
|
+
async function ship(ep, dir, remote) {
|
|
3046
|
+
const tar = spawn5("tar", ["-cf", "-", "-C", dir, "."], { stdio: ["ignore", "pipe", "pipe"] });
|
|
3047
|
+
const ssh = spawn5("ssh", [...sshArgs(ep), remote], { stdio: ["pipe", "pipe", "pipe"] });
|
|
3048
|
+
tar.stdout.pipe(ssh.stdin);
|
|
3049
|
+
const chunks = [];
|
|
3050
|
+
ssh.stdout.on("data", (c) => chunks.push(c));
|
|
3051
|
+
ssh.stderr.on("data", (c) => chunks.push(c));
|
|
3052
|
+
tar.stderr.on("data", (c) => chunks.push(c));
|
|
3053
|
+
ssh.stdin.on("error", () => {
|
|
3054
|
+
});
|
|
3055
|
+
tar.stdout.on("error", () => {
|
|
3056
|
+
});
|
|
3057
|
+
tar.once("error", () => ssh.stdin.end());
|
|
3058
|
+
tar.once("close", (code) => {
|
|
3059
|
+
if (code !== 0) ssh.stdin.end();
|
|
3060
|
+
});
|
|
3061
|
+
const tarDone = new Promise((resolve3) => tar.once("close", () => resolve3()));
|
|
3062
|
+
const status2 = await new Promise((resolve3, reject) => {
|
|
3063
|
+
ssh.once("error", (err) => reject(new KvboxError(`could not run ssh: ${err.message}`)));
|
|
3064
|
+
ssh.once("close", (code, signal) => {
|
|
3065
|
+
tar.stdout.unpipe(ssh.stdin);
|
|
3066
|
+
tar.stdout.resume();
|
|
3067
|
+
tar.kill();
|
|
3068
|
+
resolve3(code ?? (signal === "SIGINT" ? 130 : 1));
|
|
3069
|
+
});
|
|
3070
|
+
});
|
|
3071
|
+
await tarDone;
|
|
3072
|
+
return { status: status2, output: Buffer.concat(chunks).toString("utf8") };
|
|
3073
|
+
}
|
|
3074
|
+
var shipping = { ship, measure, waitForSsh };
|
|
3075
|
+
|
|
3076
|
+
// src/commands/deploy/index.ts
|
|
3077
|
+
var deployCommand = new Command49("deploy").description("put the app in this directory on a box with its own HTTPS address (React for now); 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) => {
|
|
3078
|
+
const dir = resolve2(dirArg ?? ".");
|
|
3079
|
+
const pkg = await readPackageJson(dir);
|
|
3080
|
+
const saved = await readConfig(dir);
|
|
3081
|
+
const globals = cmd.optsWithGlobals();
|
|
3082
|
+
const orgFlag = orgOption(globals);
|
|
3083
|
+
const org = orgFlag ?? saved?.org;
|
|
3084
|
+
const client = await clientFor({ ...globals.local ? { local: true } : {}, ...org !== void 0 ? { org } : {} });
|
|
3085
|
+
try {
|
|
3086
|
+
const cloud = client.target.kind === "cloud";
|
|
3087
|
+
if (!cloud && org !== void 0) throw new KvboxError("--org needs a control plane: kvbox login, or drop --org");
|
|
3088
|
+
const { config, template, fresh } = await settle(dir, pkg, saved, opts, orgFlag, cloud);
|
|
3089
|
+
if (cloud && config.subdomain !== void 0) await refuseOtherAddress(client, config);
|
|
3090
|
+
await writeConfig(dir, config);
|
|
3091
|
+
if (fresh) info(` ${color.dim(`saved to ${CONFIG_FILE}; next time kvbox deploy asks nothing`)}
|
|
3092
|
+
`);
|
|
3093
|
+
info(`${color.bold("Building")} ${color.cyan(config.name)} ${color.dim(`(${template.label})`)}`);
|
|
3094
|
+
const built = await template.build({ dir, pkg, dist: config.dist });
|
|
3095
|
+
if (built.dist !== config.dist) await writeConfig(dir, { ...config, dist: built.dist });
|
|
3096
|
+
const size = await shipping.measure(built.outDir);
|
|
3097
|
+
info("");
|
|
3098
|
+
const { box: found, ref, created } = await findOrCreate(client, config);
|
|
3099
|
+
const cert = await ensureCert(client, void 0, void 0, { owner: found.owner });
|
|
3100
|
+
const wasRunning = !created && found.status === "running";
|
|
3101
|
+
const box = created || wasRunning ? found : await wakeUp(client, found, ref);
|
|
3102
|
+
const ep = sshEndpoint(box, client.target);
|
|
3103
|
+
if (!wasRunning) await spin(`waiting for ${ref} to accept SSH\u2026`, shipping.waitForSsh(ep));
|
|
3104
|
+
const shipIt = () => shipping.ship(ep, built.outDir, template.remote());
|
|
3105
|
+
let result = await spin(`shipping ${built.dist}/ (${size.files} files, ${gb(size.bytes)}) to ${ref}\u2026`, shipIt);
|
|
3106
|
+
if (result.status === SSH_FAILED3 && await renewAfterRefusal(client, cert, box.owner)) {
|
|
3107
|
+
info("renewed your SSH certificate, trying again\u2026");
|
|
3108
|
+
result = await spin(`shipping ${built.dist}/ to ${ref}\u2026`, shipIt);
|
|
3109
|
+
}
|
|
3110
|
+
if (result.status === SSH_FAILED3) throw new KvboxError(`box ${ref}: could not reach it over SSH (still booting?)${result.output ? `
|
|
3111
|
+
${result.output.trim()}` : ""}`, SSH_FAILED3);
|
|
3112
|
+
if (result.status === IMAGE_TOO_OLD) {
|
|
3113
|
+
throw new KvboxError(
|
|
3114
|
+
`box ${ref} runs an image without nginx; ${template.label} needs 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`
|
|
3115
|
+
);
|
|
3116
|
+
}
|
|
3117
|
+
if (result.status !== 0) throw new KvboxError(`deploying to ${ref} failed (exit ${result.status}):
|
|
3118
|
+
${result.output.trim()}`);
|
|
3119
|
+
let url = `http://${box.ip}`;
|
|
3120
|
+
if (cloud) {
|
|
3121
|
+
const got = await address(client, box, ref, config, template);
|
|
3122
|
+
url = got.url;
|
|
3123
|
+
if (got.subdomain !== config.subdomain) await writeConfig(dir, { ...config, subdomain: got.subdomain, ...built.dist !== config.dist ? { dist: built.dist } : {} });
|
|
3124
|
+
}
|
|
3125
|
+
if (opts.json) {
|
|
3126
|
+
json({ box: ref, created, template: template.id, url, dist: built.dist, files: size.files, bytes: size.bytes });
|
|
3127
|
+
return;
|
|
3128
|
+
}
|
|
3129
|
+
process.stdout.write(`${url}
|
|
3130
|
+
`);
|
|
3131
|
+
info(`${color.green("\u2713")} ${color.bold(config.name)} is live at ${color.bold(url)}`);
|
|
3132
|
+
info(` box ${ref}${created ? " (new)" : ""} \xB7 ${template.label} \xB7 ${size.files} files, ${gb(size.bytes)}`);
|
|
3133
|
+
info(` redeploy: kvbox deploy${dirArg !== void 0 ? ` ${dirArg}` : ""}`);
|
|
3134
|
+
} finally {
|
|
3135
|
+
await client.close();
|
|
3136
|
+
}
|
|
3137
|
+
});
|
|
3138
|
+
async function readPackageJson(dir) {
|
|
3139
|
+
try {
|
|
3140
|
+
return JSON.parse(await readFile7(resolve2(dir, "package.json"), "utf8"));
|
|
3141
|
+
} catch (err) {
|
|
3142
|
+
if (err.code === "ENOENT") return void 0;
|
|
3143
|
+
throw new KvboxError(`${resolve2(dir, "package.json")}: ${err.message}`);
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
async function settle(dir, pkg, saved, opts, orgFlag, cloud) {
|
|
3147
|
+
const detected = detectTemplate(pkg);
|
|
3148
|
+
if (opts.name !== void 0 && !BOX_NAME_RE.test(opts.name)) throw new KvboxError(`--name ${opts.name}: ${NAME_RULE}`);
|
|
3149
|
+
if (opts.sub !== void 0) assertSubdomain(opts.sub);
|
|
3150
|
+
const renamed = saved !== void 0 && (opts.name !== void 0 && opts.name !== saved.name || orgFlag !== void 0 && orgFlag !== saved.org);
|
|
3151
|
+
const savedSub = renamed ? void 0 : saved?.subdomain;
|
|
3152
|
+
const defaults = {
|
|
3153
|
+
template: opts.template ?? saved?.template ?? detected?.id,
|
|
3154
|
+
name: opts.name ?? saved?.name ?? nameFromDir(basename(dir)),
|
|
3155
|
+
subdomain: opts.sub ?? savedSub,
|
|
3156
|
+
size: opts.size ?? saved?.size ?? DEFAULT_SIZE
|
|
3157
|
+
};
|
|
3158
|
+
const open = {
|
|
3159
|
+
template: opts.template === void 0 && saved?.template === void 0,
|
|
3160
|
+
name: opts.name === void 0 && saved?.name === void 0,
|
|
3161
|
+
subdomain: cloud && opts.sub === void 0 && savedSub === void 0,
|
|
3162
|
+
size: opts.size === void 0 && saved === void 0
|
|
3163
|
+
};
|
|
3164
|
+
const anyOpen = open.template || open.name || open.subdomain || open.size;
|
|
3165
|
+
const fresh = saved === void 0;
|
|
3166
|
+
if (anyOpen && !opts.yes && !interactive()) {
|
|
3167
|
+
const sub = defaults.subdomain ?? subdomainFor(defaults.name);
|
|
3168
|
+
throw new KvboxError(
|
|
3169
|
+
`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 ${defaults.size}), or answer with --template, --name${cloud ? ", --sub" : ""} and --size`
|
|
3170
|
+
);
|
|
3171
|
+
}
|
|
3172
|
+
const ask = anyOpen && !opts.yes;
|
|
3173
|
+
if (ask && fresh) info(`${color.bold("kvbox deploy")} ${color.dim(dir)}
|
|
3174
|
+
`);
|
|
3175
|
+
let templateId = defaults.template;
|
|
3176
|
+
if (open.template && ask) {
|
|
3177
|
+
const initial = detected ? TEMPLATES.indexOf(detected) : 0;
|
|
3178
|
+
templateId = await select("Template", TEMPLATES.map((t) => ({ value: t.id, label: t.label, hint: t === detected ? `${t.hint} \xB7 detected` : t.hint })), initial);
|
|
3179
|
+
}
|
|
3180
|
+
if (templateId === void 0) throw new KvboxError(`could not tell what kind of app ${dir} is (no package.json of a Vite or Create React App project): say it with --template ${TEMPLATE_IDS.join("|")}`);
|
|
3181
|
+
const template = templateById(templateId);
|
|
3182
|
+
let name = defaults.name;
|
|
3183
|
+
if (open.name && ask) name = await text("Box name", { default: defaults.name, validate: (a) => BOX_NAME_RE.test(a) ? void 0 : NAME_RULE });
|
|
3184
|
+
let subdomain = defaults.subdomain ?? (cloud ? subdomainFor(name) : void 0);
|
|
3185
|
+
if (open.subdomain && ask) {
|
|
3186
|
+
subdomain = await text("Address", {
|
|
3187
|
+
default: subdomainFor(name),
|
|
3188
|
+
validate: (a) => {
|
|
3189
|
+
const problem = subdomainProblem(a);
|
|
3190
|
+
return problem ? `that name ${problem}` : void 0;
|
|
3191
|
+
}
|
|
3192
|
+
});
|
|
3193
|
+
info(` ${color.dim(`https://${subdomain}.kvbox.app`)}`);
|
|
3194
|
+
}
|
|
3195
|
+
let size = defaults.size;
|
|
3196
|
+
if (open.size && ask) {
|
|
3197
|
+
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(DEFAULT_SIZE));
|
|
3198
|
+
}
|
|
3199
|
+
const org = orgFlag ?? saved?.org;
|
|
3200
|
+
const config = {
|
|
3201
|
+
template: template.id,
|
|
3202
|
+
name,
|
|
3203
|
+
...org !== void 0 ? { org } : {},
|
|
3204
|
+
...subdomain !== void 0 ? { subdomain } : {},
|
|
3205
|
+
size,
|
|
3206
|
+
...saved?.dist !== void 0 ? { dist: saved.dist } : {}
|
|
3207
|
+
};
|
|
3208
|
+
return { config, template, fresh };
|
|
3209
|
+
}
|
|
3210
|
+
var NAME_RULE = "1-12 chars, lowercase letters, digits and '-', starting with a letter or digit";
|
|
3211
|
+
async function findOrCreate(client, config) {
|
|
3212
|
+
const user = me(client);
|
|
3213
|
+
const slug = config.org ?? user;
|
|
3214
|
+
const named = (await client.listMachines()).filter((b) => b.name === config.name);
|
|
3215
|
+
let box = slug === void 0 ? named[0] : named.find((b) => slugOf(b.owner) === slug);
|
|
3216
|
+
let created = false;
|
|
3217
|
+
if (!box) {
|
|
3218
|
+
box = await spin(`booting a new box ${config.name}\u2026`, client.createMachine({ name: config.name, ...config.size !== void 0 ? { size: config.size } : {} }));
|
|
3219
|
+
created = true;
|
|
3220
|
+
info(`${color.green("\u2713")} box ${color.bold(config.name)} is up: ${box.vcpu} vCPU, ${gb(box.memMib * 1024 ** 2)} RAM, ${gb(box.diskBytes)} disk`);
|
|
3221
|
+
}
|
|
3222
|
+
return { box, ref: displayName(box, user), created };
|
|
3223
|
+
}
|
|
3224
|
+
async function existingAddress(client, config) {
|
|
3225
|
+
const slug = config.org ?? me(client);
|
|
3226
|
+
return (await client.listSubdomains()).find((s) => s.box === config.name && s.owner === slug);
|
|
3227
|
+
}
|
|
3228
|
+
async function refuseOtherAddress(client, config) {
|
|
3229
|
+
const have = await existingAddress(client, config);
|
|
3230
|
+
if (!have || have.subdomain === config.subdomain) return;
|
|
3231
|
+
const ref = config.org !== void 0 ? `${config.org}/${config.name}` : config.name;
|
|
3232
|
+
throw new KvboxError(
|
|
3233
|
+
`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`
|
|
3234
|
+
);
|
|
3235
|
+
}
|
|
3236
|
+
async function address(client, box, ref, config, template) {
|
|
3237
|
+
const have = (await client.listSubdomains()).find((s) => s.box === box.name && s.owner === slugOf(box.owner));
|
|
3238
|
+
if (have) {
|
|
3239
|
+
if (have.port !== template.port) await client.setSubdomainPort(ref, { port: template.port });
|
|
3240
|
+
return { url: have.url, subdomain: have.subdomain };
|
|
3241
|
+
}
|
|
3242
|
+
const wanted = config.subdomain ?? subdomainFor(config.name);
|
|
3243
|
+
try {
|
|
3244
|
+
const sub = await spin(`claiming https://${wanted}.kvbox.app\u2026`, client.claimSubdomain(ref, { name: wanted, port: template.port }));
|
|
3245
|
+
return { url: sub.url, subdomain: sub.subdomain };
|
|
3246
|
+
} catch (err) {
|
|
3247
|
+
if (!(err instanceof KvboxError)) throw err;
|
|
3248
|
+
throw new KvboxError(`${err.message}
|
|
3249
|
+
the app is on box ${ref} but has no address yet: pick another with kvbox deploy --sub <name> (saved to ${CONFIG_FILE})`);
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
|
|
2650
3253
|
// src/program.ts
|
|
2651
3254
|
function buildProgram() {
|
|
2652
|
-
const program = new
|
|
3255
|
+
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();
|
|
2653
3256
|
program.addCommand(vmCommand);
|
|
2654
3257
|
program.addCommand(snapshotsCommand);
|
|
2655
3258
|
program.addCommand(serverCommand);
|
|
@@ -2659,6 +3262,7 @@ function buildProgram() {
|
|
|
2659
3262
|
program.addCommand(orgCommand);
|
|
2660
3263
|
program.addCommand(usageCommand2);
|
|
2661
3264
|
program.addCommand(creditCommand);
|
|
3265
|
+
program.addCommand(deployCommand);
|
|
2662
3266
|
for (const cmd of vmAliases) program.addCommand(cmd);
|
|
2663
3267
|
return program;
|
|
2664
3268
|
}
|