@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 CHANGED
@@ -10,8 +10,10 @@ var KvboxError = class extends Error {
10
10
  exitCode;
11
11
  };
12
12
  var AmbiguousRefError = class extends KvboxError {
13
- constructor(kind, name, slugs) {
14
- super(`${kind} ${name} exists in ${slugs.slice(0, -1).join(", ")} and ${slugs.at(-1)}; say which with --org ${slugs[0]} or ${slugs[0]}/${name}`, 1);
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(rows, columns) {
28
- if (rows.length === 0) return;
29
- const cols = columns ?? Object.keys(rows[0]);
30
- const cells = rows.map((r) => cols.map((c) => String(r[c] ?? "")));
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: createInterface2 } = await import("readline/promises");
49
- const rl = createInterface2({ input: process.stdin, output: process.stderr });
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, text, me2, kind = "box") {
110
- if (text.includes("/")) return items.find((i) => text === `${slugOf(i.owner)}/${i.name}` || text === `${i.owner}/${i.name}`);
111
- const named = items.filter((i) => i.name === text);
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, text, orgs.map((i) => slugOf(i.owner)));
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 Command48 } from "commander";
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.18" : void 0);
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 Command24 } from "commander";
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 text;
274
+ let text2;
271
275
  try {
272
- text = await readFile(p.cli, "utf8");
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(text));
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
- function parseMib(text) {
375
- const m = /^\s*(\d+(?:\.\d+)?)\s*([gm]i?b?)?\s*$/i.exec(text);
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 "snapshot:<name>"
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 text;
814
+ let text2;
791
815
  try {
792
- text = readFileSync(groupFile, "utf8");
816
+ text2 = readFileSync(groupFile, "utf8");
793
817
  } catch {
794
818
  return void 0;
795
819
  }
796
- for (const line of text.split("\n")) {
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 text;
835
+ let text2;
812
836
  try {
813
- text = readFileSync2(file, "utf8");
837
+ text2 = readFileSync2(file, "utf8");
814
838
  } catch {
815
839
  return false;
816
840
  }
817
- const home = /^Environment="?KVBOX_HOME=([^"\n]+)"?$/m.exec(text)?.[1];
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 text = await res.body.text();
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(text) ?? {});
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 (text === "") return schema.parse(void 0);
906
- const json2 = safeJson(text);
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(text) {
1101
+ function safeJson(text2) {
1074
1102
  try {
1075
- return JSON.parse(text);
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, text) {
1096
- const slash = text.indexOf("/");
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: text.slice(0, slash).split("@")[0] });
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((resolve2) => {
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
- resolve2(true);
1162
+ resolve3(true);
1135
1163
  });
1136
1164
  sock.once("timeout", () => {
1137
1165
  sock.destroy();
1138
- resolve2(false);
1166
+ resolve3(false);
1139
1167
  });
1140
- sock.once("error", () => resolve2(false));
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: readFile6 } = await import("fs/promises");
1151
- const text = (await readFile6(logPath).catch(() => Buffer.alloc(0))).subarray(from).toString("utf8");
1152
- const lines = text.trimEnd().split("\n").slice(-20).reverse();
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(text) {
1253
- const t = text.trim().toLowerCase();
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 ${text}: give a duration like 30m, 4h or 1d, or off`);
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((resolve2, reject) => {
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) resolve2(TIMEOUT_EXIT);
1335
- else if (code !== null) resolve2(code);
1336
- else resolve2(signal === "SIGINT" ? 130 : 1);
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 Command4 } from "commander";
1391
- var hibernateCommand = new Command4("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) => {
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 Command5 } from "commander";
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 Command5("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) => {
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 Command6 } from "commander";
1445
- var pauseCommand = new Command6("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) => {
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 Command7 } from "commander";
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(text) {
1489
- if (text.length < 3 || text.length > 63) return `"${text}" must be 3 to 63 characters`;
1490
- if (!LABEL_RE.test(text)) return `"${text}" may hold only lowercase letters, digits and '-', not at either end`;
1491
- if (RESERVED_SUBDOMAINS.has(text)) return `"${text}" is reserved; pick another name`;
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(text) {
1495
- if (text.length < 1 || text.length > 63) return `"${text}" must be 1 to 63 characters`;
1496
- if (!LABEL_RE.test(text)) return `"${text}" may hold only lowercase letters, digits and '-', not at either end`;
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(text) {
1500
- const problem = subdomainProblem(text);
1554
+ function assertSubdomain(text2) {
1555
+ const problem = subdomainProblem(text2);
1501
1556
  if (problem) throw new KvboxError(`subdomain ${problem}`);
1502
- return text;
1557
+ return text2;
1503
1558
  }
1504
- function assertRouteName(text) {
1505
- const problem = routeNameProblem(text);
1559
+ function assertRouteName(text2) {
1560
+ const problem = routeNameProblem(text2);
1506
1561
  if (problem) throw new KvboxError(`name ${problem}`);
1507
- return text;
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, text) {
1537
- const found = pickRef(await boxesFor(client, text), text, me(client));
1538
- if (!found) throw new KvboxError(`box ${text}: not found (kvbox ls)`);
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 Command7("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) => {
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 Command13 } from "commander";
1621
+ import { Command as Command14 } from "commander";
1567
1622
 
1568
1623
  // src/commands/vm/proxy/add.ts
1569
- import { Command as Command8 } from "commander";
1570
- var addCommand = new Command8("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) => {
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 rows = rowsOf(updated, user, (f) => f.proto === proto && guestPorts.includes(f.guestPort));
1653
+ const rows2 = rowsOf(updated, user, (f) => f.proto === proto && guestPorts.includes(f.guestPort));
1599
1654
  if (opts.json) {
1600
- json(rows);
1655
+ json(rows2);
1601
1656
  return;
1602
1657
  }
1603
- for (const row of rows) info(`${color.green("\u2713")} box ${color.bold(row.box)} port ${row.guestPort} is reachable at ${reaches(row)} (${row.proto})`);
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 Command9 } from "commander";
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 Command9("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) => {
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 rows = [...boxes.flatMap((b) => rowsOf(b, user)), ...https].sort((a, b) => a.box.localeCompare(b.box) || a.guestPort - b.guestPort);
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(rows);
1687
+ json(rows2);
1633
1688
  return;
1634
1689
  }
1635
- if (rows.length === 0) {
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(rows.map((r) => ({ box: r.box, port: r.guestPort, reaches: r.proto === "https" ? r.url : reaches(r, true), proto: r.proto })));
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 Command10 } from "commander";
1647
- var releaseCommand = new Command10("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) => {
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 Command11 } from "commander";
1667
- var rmCommand = new Command11("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) => {
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 Command12 } from "commander";
1700
- var setPortCommand = new Command12("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) => {
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 Command13("proxy").description("publish box ports on the internet").addCommand(addCommand).addCommand(lsCommand2).addCommand(rmCommand).addCommand(claimCommand).addCommand(setPortCommand).addCommand(releaseCommand);
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 Command14, Option as Option2 } from "commander";
1723
- var rescaleCommand = new Command14("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 Option2("-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) => {
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(text) {
1752
- const n = Number(text);
1753
- if (!Number.isInteger(n) || n < 1) throw new KvboxError(`--vcpu ${text}: give a whole number of vCPUs`);
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, text) {
1757
- const mib = parseMib(text);
1758
- if (mib === void 0) throw new KvboxError(`${flag} ${text}: give a size like 16GB or 4096MB`);
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, text) {
1762
- const mib = parseSize(flag, text);
1763
- if (mib % 1024 !== 0) throw new KvboxError(`${flag} ${text}: disk sizes are whole GB`);
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 Command15 } from "commander";
1769
- var resumeCommand = new Command15("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) => {
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 Command16 } from "commander";
1782
- var rmCommand2 = new Command16("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) => {
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 Command17 } from "commander";
1802
- var setCommand = new Command17("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) => {
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 Command18 } from "commander";
1822
- var startCommand = new Command18("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) => {
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 Command19 } from "commander";
1835
- var stopCommand = new Command19("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) => {
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 Command20 } from "commander";
1848
- var topCommand = new Command20("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) => {
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 Command21 } from "commander";
1909
- var updateCommand = new Command21("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) => {
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 Command22 } from "commander";
1932
- var usageCommand = new Command22("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) => {
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 rows = [
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) rows.push(["cost", `${money(u.cost.amount, u.cost.currency)} ${color.dim("(estimated)")}`]);
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
- rows.push(["history", color.dim(`from ${u.historyFrom.slice(0, 16).replace("T", " ")} UTC; the box is older, but its time before that was not recorded`)]);
2027
+ 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(...rows.map(([k]) => k.length));
1975
- return rows.map(([k, v]) => `${" ".repeat(w - k.length)}${k}: ${v}`).join("\n") + "\n";
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 Command23 } from "commander";
1980
- var wakeCommand = new Command23("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) => {
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 Command24("vm").enablePositionalOptions().description("manage boxes (each one is a Firecracker microVM)").addCommand(newCommand).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);
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 Command32 } from "commander";
2052
+ import { Command as Command33 } from "commander";
1998
2053
 
1999
2054
  // src/commands/server/join.ts
2000
- import { Command as Command25 } from "commander";
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((resolve2, reject) => {
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) resolve2(out.trimEnd());
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 Command25("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) => {
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 Command26 } from "commander";
2065
- var leaveCommand = new Command26("leave").description("take this machine out from behind its control plane and restart its server").action(async () => {
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 Command27 } from "commander";
2075
- var relayCommand = new Command27("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) => {
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 Command28 } from "commander";
2081
- var startCommand2 = new Command28("start").description("start the local kvbox-server in the background").action(async (_opts, cmd) => {
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 Command29 } from "commander";
2098
- var statusCommand = new Command29("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) => {
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 Command30 } from "commander";
2131
- var stopCommand2 = new Command30("stop").description("stop the local server (running boxes are not affected)").action(async () => {
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 Command31 } from "commander";
2140
- var trustCaCommand = new Command31("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) => {
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 Command32("server").description("run or manage the kvbox server").addCommand(startCommand2).addCommand(statusCommand).addCommand(stopCommand2).addCommand(joinCommand).addCommand(leaveCommand).addCommand(relayCommand).addCommand(trustCaCommand);
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 Command33 } from "commander";
2187
- var loginCommand = new Command33("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) => {
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 = createInterface({ input: io.input, output: io.output });
2244
- return (await ask(rl, question)).trim();
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 = createInterface({ input: io.input, output: sink, terminal: true });
2427
+ const rl = createInterface2({ input: io.input, output: sink, terminal: true });
2249
2428
  io.output.write(question);
2250
2429
  try {
2251
- return await ask(rl, question);
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 = createInterface({ input: stream });
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 Command34 } from "commander";
2286
- var logoutCommand = new Command34("logout").description("log out of the control plane; commands target this machine's kvbox-server again").action(async () => {
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 Command35 } from "commander";
2324
- var whoamiCommand = new Command35("whoami").description("show which account and control plane commands target (or local mode)").option("--json", "print as JSON").action(async (opts) => {
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 Command39 } from "commander";
2501
+ import { Command as Command40 } from "commander";
2341
2502
 
2342
2503
  // src/commands/snapshots/ls.ts
2343
- import { Command as Command36 } from "commander";
2344
- var lsCommand3 = new Command36("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) => {
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 Command37 } from "commander";
2376
- var newCommand2 = new Command37("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) => {
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 Command38 } from "commander";
2389
- var rmCommand3 = new Command38("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) => {
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 Command39("snapshots").aliases(["snapshot", "snap"]).description("freeze running boxes into snapshots and manage them").addCommand(newCommand2).addCommand(lsCommand3).addCommand(rmCommand3);
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 Command45 } from "commander";
2570
+ import { Command as Command46 } from "commander";
2410
2571
 
2411
2572
  // src/commands/org/ls.ts
2412
- import { Command as Command40 } from "commander";
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 Command40("ls").description("list the orgs you belong to and your role in each").option("--json", "print as JSON").action(async (opts, cmd) => {
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 Command41, Option as Option3 } from "commander";
2455
- var lsCommand5 = new Command41("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) => {
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 Command41("add").description("add a user to an org (admins and owners; only an owner can add an owner)").argument("<user>", "their username").addOption(new Option3("-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) => {
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 Command41("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) => {
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 Command41("members").description("who is in an org and what they may do").addCommand(lsCommand5).addCommand(addCommand2).addCommand(rmCommand4);
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 Command42 } from "commander";
2499
- var newCommand3 = new Command42("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) => {
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 Command43 } from "commander";
2516
- var rmCommand5 = new Command43("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) => {
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 Command44 } from "commander";
2533
- var roleCommand = new Command44("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) => {
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 Command45("org").description("orgs: shared slugs that own boxes, with owners, admins and members").addCommand(lsCommand4).addCommand(newCommand3).addCommand(membersCommand).addCommand(roleCommand).addCommand(rmCommand5);
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 Command46 } from "commander";
2549
- var usageCommand2 = new Command46("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("--json", "print as JSON").action(async (opts, cmd) => {
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 rows = report.boxes.map((b) => ({
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
- rows.push({
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 rows;
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 Command47 } from "commander";
2623
- var creditCommand = new Command47("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) => {
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 Command48("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();
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
  }