@kvbox/cli 0.5.21 → 0.5.23

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
@@ -149,7 +149,7 @@ async function spin(message, work, opts = {}) {
149
149
  }
150
150
 
151
151
  // src/program.ts
152
- import { Command as Command50 } from "commander";
152
+ import { Command as Command57 } from "commander";
153
153
 
154
154
  // ../shared/src/version.ts
155
155
  import { createRequire } from "module";
@@ -164,10 +164,10 @@ function versionOf(srcFileUrl, injected) {
164
164
  }
165
165
 
166
166
  // src/version.ts
167
- var VERSION = versionOf(import.meta.url, true ? "0.5.21" : void 0);
167
+ var VERSION = versionOf(import.meta.url, true ? "0.5.23" : void 0);
168
168
 
169
169
  // src/commands/vm/index.ts
170
- import { Command as Command25 } from "commander";
170
+ import { Command as Command26 } from "commander";
171
171
 
172
172
  // src/commands/vm/connect.ts
173
173
  import { spawnSync as spawnSync2 } from "child_process";
@@ -241,7 +241,25 @@ function paths(home = kvboxHome()) {
241
241
  sock: join(dir, "fc.sock"),
242
242
  /** Firecracker's vsock endpoint: the host connects here to reach kvbox-agent inside the box (core/agent.ts). */
243
243
  vsock: join(dir, "vsock.sock"),
244
- log: join(dir, "fc.log")
244
+ log: join(dir, "fc.log"),
245
+ /**
246
+ * An attached volume's drive as Firecracker sees it (#52): a symlink in the box dir to the volume's
247
+ * disk, given to Firecracker by its relative name (`volumeDriveRel`), so a snapshot's vmstate names
248
+ * it relative to whichever box dir it is restored into. There it is a same-size empty placeholder.
249
+ */
250
+ volumeDrive: (volume) => join(dir, volumeDriveRel(volume))
251
+ };
252
+ },
253
+ /** Volumes are filed like boxes, by owner: volumes/<owner>/<name> (#52). */
254
+ volumes: join(home, "volumes"),
255
+ ownerVolumes: (owner) => join(home, "volumes", owner),
256
+ volume: (owner, name) => {
257
+ const dir = join(home, "volumes", owner, name);
258
+ return {
259
+ dir,
260
+ meta: join(dir, "meta.json"),
261
+ /** The volume itself: a sparse ext4 the guest sees as a whole disk. */
262
+ disk: join(dir, "disk.ext4")
245
263
  };
246
264
  },
247
265
  snapshot: (owner, name) => {
@@ -257,6 +275,12 @@ function paths(home = kvboxHome()) {
257
275
  }
258
276
  };
259
277
  }
278
+ function volumeDriveRel(volume) {
279
+ return `volume-${volume}.ext4`;
280
+ }
281
+ function volumeMountPath(volume) {
282
+ return `/mnt/${volume}`;
283
+ }
260
284
 
261
285
  // src/session.ts
262
286
  import { chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
@@ -450,7 +474,36 @@ var MachineSchema = z2.object({
450
474
  /** Whether the owner slug is a user or an org. Only the control plane knows; it fills it in on `GET /machines`. */
451
475
  ownerKind: z2.enum(["user", "org"]).optional(),
452
476
  /** Seconds without network traffic before the host hibernates the box (#47). Absent: never. */
453
- autoHibernateSec: z2.number().int().positive().optional()
477
+ autoHibernateSec: z2.number().int().positive().optional(),
478
+ /** Volumes attached to the box (#52), in drive order; each is mounted at `mountPath` while the box runs. Absent: none. */
479
+ volumes: z2.array(z2.object({ name: z2.string(), sizeGib: z2.number().int(), mountPath: z2.string() })).optional()
480
+ });
481
+ var VolumeSchema = z2.object({
482
+ /** The host's 6-hex volume id (#91): the ledger's key, so a name removed and made again is two volumes. */
483
+ id: z2.string(),
484
+ name: z2.string(),
485
+ owner: z2.string(),
486
+ sizeGib: z2.number().int(),
487
+ /** What the sparse file really occupies on the host. */
488
+ usedBytes: z2.number().int(),
489
+ attachedTo: z2.string().nullable(),
490
+ createdAt: z2.string(),
491
+ /** Whether the owner slug is a user or an org. Only the control plane knows; it fills it in on `GET /volumes`. */
492
+ ownerKind: z2.enum(["user", "org"]).optional()
493
+ });
494
+ var MAX_VOLUME_GIB = 4096;
495
+ var CreateVolumeRequest = z2.object({
496
+ name: z2.string(),
497
+ sizeGib: z2.number().int().min(1).max(MAX_VOLUME_GIB),
498
+ /**
499
+ * Through a control plane (#91): the box this volume is for, so it is made on that box's host. A
500
+ * volume attaches only to a box on its own host. Absent: the host with the most room for volumes.
501
+ * A host ignores it.
502
+ */
503
+ box: z2.string().optional()
504
+ });
505
+ var AttachVolumeRequest = z2.object({
506
+ box: z2.string()
454
507
  });
455
508
  var CreateMachineRequest = z2.object({
456
509
  name: z2.string().optional(),
@@ -512,12 +565,21 @@ var CreateSnapshotRequest = z2.object({
512
565
  box: z2.string(),
513
566
  name: z2.string()
514
567
  });
568
+ var DiskCapacitySchema = z2.object({
569
+ totalGib: z2.number().int(),
570
+ freeGib: z2.number().int(),
571
+ volumesGib: z2.number().int(),
572
+ volumesUsedGib: z2.number().int(),
573
+ headroomGib: z2.number().int()
574
+ });
515
575
  var CapacitySchema = z2.object({
516
576
  hostMemMib: z2.number().int(),
517
577
  hostCores: z2.number().int(),
518
578
  reservedMemMib: z2.number().int(),
519
579
  reservedVcpu: z2.number().int(),
520
- running: z2.number().int()
580
+ running: z2.number().int(),
581
+ /** Absent on a host from before volumes (#91): it cannot take one. */
582
+ disk: DiskCapacitySchema.optional()
521
583
  });
522
584
  var RecoveryReportSchema = z2.object({
523
585
  at: z2.string(),
@@ -604,14 +666,33 @@ var BoxUsageSchema = MachineUsageSchema.omit({ window: true, status: true }).ext
604
666
  */
605
667
  createdBy: z2.string().optional()
606
668
  });
669
+ var VolumeUsageSchema = z2.object({
670
+ /** The host's volume id; a name removed and made again appears twice, with two ids. */
671
+ id: z2.string(),
672
+ name: z2.string(),
673
+ owner: z2.string(),
674
+ ownerKind: z2.enum(["user", "org"]).optional(),
675
+ /** The size at the end of the window (or when removed). */
676
+ sizeGib: z2.number().int(),
677
+ status: z2.enum(["present", "removed"]),
678
+ volumeHours: z2.number(),
679
+ volumeGibHours: z2.number(),
680
+ cost: z2.object({ amount: z2.number(), currency: z2.string() }).optional(),
681
+ historyFrom: z2.string().nullable()
682
+ });
607
683
  var UsageReportSchema = z2.object({
608
684
  window: z2.object({ from: z2.string(), to: z2.string() }),
609
685
  boxes: z2.array(BoxUsageSchema),
686
+ /** Absent from a side older than #91. */
687
+ volumes: z2.array(VolumeUsageSchema).optional(),
610
688
  totals: z2.object({
611
689
  seconds: MachineUsageSchema.shape.seconds,
612
690
  vcpuHours: z2.number(),
613
691
  ramGibHours: z2.number(),
614
692
  diskGibHours: z2.number(),
693
+ volumeHours: z2.number().optional(),
694
+ volumeGibHours: z2.number().optional(),
695
+ /** Boxes and volumes together. */
615
696
  cost: z2.object({ amount: z2.number(), currency: z2.string() }).optional()
616
697
  })
617
698
  });
@@ -632,6 +713,20 @@ var UsageEventsResponseSchema = z2.object({
632
713
  events: z2.array(UsageEventRowSchema),
633
714
  next: z2.number().int()
634
715
  });
716
+ var VolumeEventRowSchema = z2.object({
717
+ id: z2.number().int(),
718
+ volumeId: z2.string(),
719
+ owner: z2.string(),
720
+ name: z2.string(),
721
+ at: z2.string(),
722
+ sizeGib: z2.number().int(),
723
+ by: z2.enum(["create", "resize", "rm"])
724
+ });
725
+ var VolumeEventsResponseSchema = z2.object({
726
+ ledger: z2.string().min(1),
727
+ events: z2.array(VolumeEventRowSchema),
728
+ next: z2.number().int()
729
+ });
635
730
  var ApiErrorSchema = z2.object({
636
731
  error: z2.string()
637
732
  });
@@ -1042,6 +1137,23 @@ var ServerClient = class {
1042
1137
  deleteSnapshot(name) {
1043
1138
  return this.call("DELETE", `/snapshots/${encodeURIComponent(name)}`, z3.undefined());
1044
1139
  }
1140
+ // ---- volumes (#52) ----
1141
+ listVolumes(opts = {}) {
1142
+ return this.call("GET", `/volumes${allQuery(opts)}`, z3.array(VolumeSchema), void 0, opts.org);
1143
+ }
1144
+ createVolume(req) {
1145
+ return this.call("POST", "/volumes", VolumeSchema, req);
1146
+ }
1147
+ deleteVolume(name) {
1148
+ return this.call("DELETE", `/volumes/${encodeURIComponent(name)}`, z3.undefined());
1149
+ }
1150
+ /** Both answer with the box as it is afterwards (restarted when it was running). */
1151
+ attachVolume(name, req) {
1152
+ return this.call("POST", `/volumes/${encodeURIComponent(name)}/attach`, MachineSchema, req);
1153
+ }
1154
+ detachVolume(name, req) {
1155
+ return this.call("POST", `/volumes/${encodeURIComponent(name)}/detach`, MachineSchema, req);
1156
+ }
1045
1157
  // ---- control plane only (`kvbox login`, `logout`, `whoami`) ----
1046
1158
  login(req) {
1047
1159
  return this.call("POST", "/login", LoginResponseSchema, req);
@@ -1441,21 +1553,11 @@ var forkCommand = new Command4("fork").description("copy a box, running or stopp
1441
1553
  }
1442
1554
  });
1443
1555
 
1444
- // src/commands/vm/hibernate.ts
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) => {
1447
- const client = await clientFor(cmd.optsWithGlobals());
1448
- try {
1449
- const box = await spin(`hibernating ${name} (saving its memory to disk)\u2026`, client.hibernateMachine(name));
1450
- if (opts.json) json(box);
1451
- else info(`${color.green("\u2713")} box ${name} hibernated (memory saved to disk, RAM freed; kvbox wake ${name} continues it where it left off)`);
1452
- } finally {
1453
- await client.close();
1454
- }
1455
- });
1556
+ // src/commands/vm/get.ts
1557
+ import { Command as Command6 } from "commander";
1456
1558
 
1457
1559
  // src/commands/vm/ls.ts
1458
- import { Command as Command6 } from "commander";
1560
+ import { Command as Command5 } from "commander";
1459
1561
  function ownerCell(b, me2) {
1460
1562
  const slug = slugOf(b.owner);
1461
1563
  if (slug === me2) return "private";
@@ -1465,7 +1567,7 @@ function addressOf(b) {
1465
1567
  if (b.ssh !== void 0) return b.ssh.host;
1466
1568
  return b.ip === "" ? "-" : b.ip;
1467
1569
  }
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) => {
1570
+ 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) => {
1469
1571
  const client = await clientFor(cmd.optsWithGlobals());
1470
1572
  try {
1471
1573
  const boxes = await spin("listing boxes\u2026", client.listMachines({ all: opts.all }));
@@ -1491,13 +1593,108 @@ function lsRows(boxes, cloud) {
1491
1593
  status: status(b.status),
1492
1594
  address: addressOf(b),
1493
1595
  size: b.status === "unknown" ? "-" : `${shape(b.vcpu, b.memMib, b.diskBytes)} ${color.dim(`(${gbShort(b.diskUsedBytes)} used)`)}`,
1494
- created: age(b.createdAt)
1596
+ created: age(b.createdAt),
1597
+ base: b.status === "unknown" || b.base === "" ? "-" : b.base
1495
1598
  }));
1496
1599
  }
1497
1600
 
1498
- // src/commands/vm/pause.ts
1601
+ // src/commands/vm/proxy/shared.ts
1602
+ var boxOf = (sub, user) => user !== void 0 && sub.owner !== user ? `${sub.owner}/${sub.box}` : sub.box;
1603
+ function subdomainJson(sub, user) {
1604
+ return { box: boxOf(sub, user), subdomain: sub.subdomain, port: sub.port, url: sub.url, routes: sub.routes };
1605
+ }
1606
+ function httpsRowsOf(sub, user) {
1607
+ const box = boxOf(sub, user);
1608
+ return [{ box, guestPort: sub.port, proto: "https", url: sub.url }, ...sub.routes.map((r) => ({ box, guestPort: r.port, proto: "https", url: r.url, name: r.name }))];
1609
+ }
1610
+ function requireCloud(client) {
1611
+ if (client.target.kind !== "cloud") throw new KvboxError("HTTPS names need the kvbox cloud: kvbox login (this host's own socket has none)");
1612
+ }
1613
+ function rowsOf(box, user, only) {
1614
+ return (box.ports ?? []).filter(only ?? (() => true)).map((f) => ({ box: displayName(box, user), ...f }));
1615
+ }
1616
+ function reaches(row, short = false) {
1617
+ if (row.address !== void 0) return `${row.address}:${row.hostPort}`;
1618
+ return short ? `:${row.hostPort}` : `port ${row.hostPort} of this host's public address`;
1619
+ }
1620
+ function parsePorts(texts) {
1621
+ return texts.map((t) => {
1622
+ const n = Number(t);
1623
+ if (!/^\d+$/.test(t) || n < 1 || n > 65535) throw new KvboxError(`"${t}" is not a port (1-65535)`);
1624
+ return n;
1625
+ });
1626
+ }
1627
+ async function findBox(client, text2) {
1628
+ const found = pickRef(await boxesFor(client, text2), text2, me(client));
1629
+ if (!found) throw new KvboxError(`box ${text2}: not found (kvbox ls)`);
1630
+ return found;
1631
+ }
1632
+
1633
+ // src/commands/vm/get.ts
1634
+ var getCommand = new Command6("get").description("show one box in full: status, size, address, HTTPS name, ports, auto-hibernate").argument("<name>", "box name").option("--json", "print the box as JSON").action(async (name, opts, cmd) => {
1635
+ const client = await clientFor(cmd.optsWithGlobals());
1636
+ try {
1637
+ const user = me(client);
1638
+ const box = await findBox(client, name);
1639
+ const sub = client.target.kind === "cloud" ? await subdomainOf(client, box) : void 0;
1640
+ if (opts.json) {
1641
+ json({ ...box, ...sub ? { subdomain: subdomainJson(sub, user) } : {} });
1642
+ return;
1643
+ }
1644
+ const rows2 = getRows(box, sub, user);
1645
+ const width2 = Math.max(...rows2.map(([k]) => k.length));
1646
+ for (const [k, v] of rows2) info(`${color.dim(k.padStart(width2))} ${v}`);
1647
+ } finally {
1648
+ await client.close();
1649
+ }
1650
+ });
1651
+ async function subdomainOf(client, box) {
1652
+ try {
1653
+ const slug = slugOf(box.owner);
1654
+ return (await client.listSubdomains()).find((s) => s.box === box.name && s.owner === slug);
1655
+ } catch (err) {
1656
+ warn(`HTTPS name not shown: ${err.message}`);
1657
+ return void 0;
1658
+ }
1659
+ }
1660
+ function getRows(box, sub, user) {
1661
+ const rows2 = [
1662
+ ["name", color.bold(displayName(box, user))],
1663
+ ["status", status(box.status)]
1664
+ ];
1665
+ if (user !== void 0) rows2.push(["owner", ownerCell(box, user)]);
1666
+ if (box.status !== "unknown") {
1667
+ rows2.push(["size", `${shape(box.vcpu, box.memMib, box.diskBytes)} ${color.dim(`(${gb(box.diskUsedBytes)} used)`)}`]);
1668
+ rows2.push(["address", box.ssh?.host ?? (box.ip === "" ? "-" : box.ip)]);
1669
+ rows2.push(["ssh", box.ssh ? `ssh -p ${box.ssh.port} ${box.ssh.user}@${box.ssh.host}` : `kvbox connect ${displayName(box, user)}`]);
1670
+ }
1671
+ rows2.push(["url", sub ? [`${sub.url} ${color.dim(`(port ${sub.port})`)}`, ...sub.routes.map((r) => `${r.url} ${color.dim(`(port ${r.port})`)}`)].join(", ") : "-"]);
1672
+ const ports = rowsOf(box, user).map((r) => `${r.guestPort}/${r.proto} \u2192 ${reaches(r, true)}`);
1673
+ rows2.push(["ports", ports.length === 0 ? "-" : ports.join(", ")]);
1674
+ rows2.push(["volumes", box.volumes?.length ? box.volumes.map((v) => `${v.name} ${color.dim(`(${v.sizeGib} GB at ${v.mountPath})`)}`).join(", ") : "-"]);
1675
+ rows2.push(["auto-hibernate", box.autoHibernateSec ? `after ${duration(box.autoHibernateSec)} without network traffic` : "off"]);
1676
+ rows2.push(["source", box.source]);
1677
+ rows2.push(["base", box.base]);
1678
+ rows2.push(["created", `${age(box.createdAt)} ${color.dim(`(${box.createdAt})`)}`]);
1679
+ return rows2;
1680
+ }
1681
+
1682
+ // src/commands/vm/hibernate.ts
1499
1683
  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) => {
1684
+ var hibernateCommand = new Command7("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) => {
1685
+ const client = await clientFor(cmd.optsWithGlobals());
1686
+ try {
1687
+ const box = await spin(`hibernating ${name} (saving its memory to disk)\u2026`, client.hibernateMachine(name));
1688
+ if (opts.json) json(box);
1689
+ else info(`${color.green("\u2713")} box ${name} hibernated (memory saved to disk, RAM freed; kvbox wake ${name} continues it where it left off)`);
1690
+ } finally {
1691
+ await client.close();
1692
+ }
1693
+ });
1694
+
1695
+ // src/commands/vm/pause.ts
1696
+ import { Command as Command8 } from "commander";
1697
+ var pauseCommand = new Command8("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) => {
1501
1698
  const client = await clientFor(cmd.optsWithGlobals());
1502
1699
  try {
1503
1700
  const box = await spin(`pausing ${name}\u2026`, client.pauseMachine(name));
@@ -1509,7 +1706,7 @@ var pauseCommand = new Command7("pause").description("freeze a box in place (mem
1509
1706
  });
1510
1707
 
1511
1708
  // src/commands/vm/proxy/claim.ts
1512
- import { Command as Command8 } from "commander";
1709
+ import { Command as Command9 } from "commander";
1513
1710
 
1514
1711
  // ../shared/src/names.ts
1515
1712
  import { createHmac } from "crypto";
@@ -1562,40 +1759,8 @@ function assertRouteName(text2) {
1562
1759
  return text2;
1563
1760
  }
1564
1761
 
1565
- // src/commands/vm/proxy/shared.ts
1566
- var boxOf = (sub, user) => user !== void 0 && sub.owner !== user ? `${sub.owner}/${sub.box}` : sub.box;
1567
- function subdomainJson(sub, user) {
1568
- return { box: boxOf(sub, user), subdomain: sub.subdomain, port: sub.port, url: sub.url, routes: sub.routes };
1569
- }
1570
- function httpsRowsOf(sub, user) {
1571
- const box = boxOf(sub, user);
1572
- return [{ box, guestPort: sub.port, proto: "https", url: sub.url }, ...sub.routes.map((r) => ({ box, guestPort: r.port, proto: "https", url: r.url, name: r.name }))];
1573
- }
1574
- function requireCloud(client) {
1575
- if (client.target.kind !== "cloud") throw new KvboxError("HTTPS names need the kvbox cloud: kvbox login (this host's own socket has none)");
1576
- }
1577
- function rowsOf(box, user, only) {
1578
- return (box.ports ?? []).filter(only ?? (() => true)).map((f) => ({ box: displayName(box, user), ...f }));
1579
- }
1580
- function reaches(row, short = false) {
1581
- if (row.address !== void 0) return `${row.address}:${row.hostPort}`;
1582
- return short ? `:${row.hostPort}` : `port ${row.hostPort} of this host's public address`;
1583
- }
1584
- function parsePorts(texts) {
1585
- return texts.map((t) => {
1586
- const n = Number(t);
1587
- if (!/^\d+$/.test(t) || n < 1 || n > 65535) throw new KvboxError(`"${t}" is not a port (1-65535)`);
1588
- return n;
1589
- });
1590
- }
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)`);
1594
- return found;
1595
- }
1596
-
1597
1762
  // src/commands/vm/proxy/claim.ts
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) => {
1763
+ var claimCommand = new Command9("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) => {
1599
1764
  assertSubdomain(subdomain);
1600
1765
  const [port] = parsePorts([opts.port]);
1601
1766
  const client = await clientFor(cmd.optsWithGlobals());
@@ -1618,11 +1783,11 @@ var claimCommand = new Command8("claim").description("give a box its HTTPS addre
1618
1783
  });
1619
1784
 
1620
1785
  // src/commands/vm/proxy/index.ts
1621
- import { Command as Command14 } from "commander";
1786
+ import { Command as Command15 } from "commander";
1622
1787
 
1623
1788
  // src/commands/vm/proxy/add.ts
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) => {
1789
+ import { Command as Command10 } from "commander";
1790
+ var addCommand = new Command10("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) => {
1626
1791
  const guestPorts = parsePorts(portTexts);
1627
1792
  if (opts.name !== void 0) {
1628
1793
  assertRouteName(opts.name);
@@ -1663,7 +1828,7 @@ var addCommand = new Command9("add").description("publish box ports on the inter
1663
1828
  });
1664
1829
 
1665
1830
  // src/commands/vm/proxy/ls.ts
1666
- import { Command as Command10 } from "commander";
1831
+ import { Command as Command11 } from "commander";
1667
1832
  async function subdomainsOf(client) {
1668
1833
  try {
1669
1834
  return await client.listSubdomains();
@@ -1672,7 +1837,7 @@ async function subdomainsOf(client) {
1672
1837
  return [];
1673
1838
  }
1674
1839
  }
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) => {
1840
+ var lsCommand2 = new Command11("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) => {
1676
1841
  const client = await clientFor(cmd.optsWithGlobals());
1677
1842
  try {
1678
1843
  const user = me(client);
@@ -1698,8 +1863,8 @@ var lsCommand2 = new Command10("ls").description("list the box ports published o
1698
1863
  });
1699
1864
 
1700
1865
  // src/commands/vm/proxy/release.ts
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) => {
1866
+ import { Command as Command12 } from "commander";
1867
+ var releaseCommand = new Command12("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) => {
1703
1868
  const client = await clientFor(cmd.optsWithGlobals());
1704
1869
  try {
1705
1870
  requireCloud(client);
@@ -1718,8 +1883,8 @@ var releaseCommand = new Command11("release").description("give up a box's HTTPS
1718
1883
  });
1719
1884
 
1720
1885
  // src/commands/vm/proxy/rm.ts
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) => {
1886
+ import { Command as Command13 } from "commander";
1887
+ var rmCommand = new Command13("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) => {
1723
1888
  if (opts.name !== void 0) {
1724
1889
  assertRouteName(opts.name);
1725
1890
  if (portTexts.length > 0) throw new KvboxError("--name frees a name, not ports: kvbox vm proxy rm <box> --name <name>");
@@ -1751,8 +1916,8 @@ var rmCommand = new Command12("rm").description("stop publishing box ports, or a
1751
1916
  });
1752
1917
 
1753
1918
  // src/commands/vm/proxy/set-port.ts
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) => {
1919
+ import { Command as Command14 } from "commander";
1920
+ var setPortCommand = new Command14("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) => {
1756
1921
  const [port] = parsePorts([portText]);
1757
1922
  const client = await clientFor(cmd.optsWithGlobals());
1758
1923
  try {
@@ -1770,12 +1935,12 @@ var setPortCommand = new Command13("set-port").description("point a box's HTTPS
1770
1935
  });
1771
1936
 
1772
1937
  // src/commands/vm/proxy/index.ts
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);
1938
+ var proxyCommand = new Command15("proxy").description("publish box ports on the internet").addCommand(addCommand).addCommand(lsCommand2).addCommand(rmCommand).addCommand(claimCommand).addCommand(setPortCommand).addCommand(releaseCommand);
1774
1939
  for (const cmd of proxyCommand.commands) cmd.option("--org <slug>", "act in this org (- for your personal boxes)");
1775
1940
 
1776
1941
  // src/commands/vm/rescale.ts
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) => {
1942
+ import { Command as Command16, Option as Option3 } from "commander";
1943
+ var rescaleCommand = new Command16("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) => {
1779
1944
  const req = {
1780
1945
  ...opts.size !== void 0 ? { size: opts.size } : {},
1781
1946
  ...opts.vcpu !== void 0 ? { vcpu: parseCount(opts.vcpu) } : {},
@@ -1820,8 +1985,8 @@ function parseGib(flag, text2) {
1820
1985
  }
1821
1986
 
1822
1987
  // src/commands/vm/resume.ts
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) => {
1988
+ import { Command as Command17 } from "commander";
1989
+ var resumeCommand = new Command17("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) => {
1825
1990
  const client = await clientFor(cmd.optsWithGlobals());
1826
1991
  try {
1827
1992
  const box = await spin(`resuming ${name}\u2026`, client.resumeMachine(name));
@@ -1833,8 +1998,8 @@ var resumeCommand = new Command16("resume").description("continue a box that is
1833
1998
  });
1834
1999
 
1835
2000
  // src/commands/vm/rm.ts
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) => {
2001
+ import { Command as Command18 } from "commander";
2002
+ var rmCommand2 = new Command18("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) => {
1838
2003
  const client = await clientFor(cmd.optsWithGlobals());
1839
2004
  try {
1840
2005
  const box = pickRef(await boxesFor(client, name), name, me(client));
@@ -1846,15 +2011,16 @@ var rmCommand2 = new Command17("rm").description("destroy a box (its disk is del
1846
2011
  return;
1847
2012
  }
1848
2013
  await spin(`destroying ${ref}\u2026`, client.deleteMachine(ref));
1849
- info(`${color.green("\u2713")} box ${ref} destroyed`);
2014
+ const volumes = box.volumes?.map((v) => v.name) ?? [];
2015
+ info(`${color.green("\u2713")} box ${ref} destroyed${volumes.length > 0 ? `; its volume${volumes.length > 1 ? "s" : ""} ${volumes.join(", ")} ${volumes.length > 1 ? "are" : "is"} kept, detached` : ""}`);
1850
2016
  } finally {
1851
2017
  await client.close();
1852
2018
  }
1853
2019
  });
1854
2020
 
1855
2021
  // src/commands/vm/set.ts
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) => {
2022
+ import { Command as Command19 } from "commander";
2023
+ var setCommand = new Command19("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) => {
1858
2024
  if (opts.autoHibernate === void 0) throw new KvboxError("nothing to change: give --auto-hibernate");
1859
2025
  const autoHibernateSec = parseIdleTimeout(opts.autoHibernate);
1860
2026
  const client = await clientFor(cmd.optsWithGlobals());
@@ -1873,8 +2039,8 @@ var setCommand = new Command18("set").description("change a box's settings, e.g.
1873
2039
  });
1874
2040
 
1875
2041
  // src/commands/vm/start.ts
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) => {
2042
+ import { Command as Command20 } from "commander";
2043
+ var startCommand = new Command20("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) => {
1878
2044
  const client = await clientFor(cmd.optsWithGlobals());
1879
2045
  try {
1880
2046
  const box = await spin(`booting ${name}\u2026`, client.startMachine(name));
@@ -1886,8 +2052,8 @@ var startCommand = new Command19("start").description("boot a stopped box again
1886
2052
  });
1887
2053
 
1888
2054
  // src/commands/vm/stop.ts
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) => {
2055
+ import { Command as Command21 } from "commander";
2056
+ var stopCommand = new Command21("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) => {
1891
2057
  const client = await clientFor(cmd.optsWithGlobals());
1892
2058
  try {
1893
2059
  const box = await spin(`shutting ${name} down\u2026`, client.stopMachine(name));
@@ -1899,8 +2065,8 @@ var stopCommand = new Command20("stop").description("shut a box down (its disk i
1899
2065
  });
1900
2066
 
1901
2067
  // src/commands/vm/top.ts
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) => {
2068
+ import { Command as Command22 } from "commander";
2069
+ var topCommand = new Command22("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) => {
1904
2070
  const client = await clientFor(cmd.optsWithGlobals());
1905
2071
  try {
1906
2072
  const running = (await client.listMachines({ all: opts.all })).filter((b) => b.status === "running");
@@ -1960,8 +2126,8 @@ function topRows(entries, cloud) {
1960
2126
  }
1961
2127
 
1962
2128
  // src/commands/vm/update.ts
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) => {
2129
+ import { Command as Command23 } from "commander";
2130
+ var updateCommand = new Command23("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) => {
1965
2131
  const client = await clientFor(cmd.optsWithGlobals());
1966
2132
  try {
1967
2133
  const before = pickRef(await boxesFor(client, name), name, me(client));
@@ -1983,8 +2149,8 @@ var updateCommand = new Command22("update").description("move a box onto the cur
1983
2149
  });
1984
2150
 
1985
2151
  // src/commands/vm/usage.ts
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) => {
2152
+ import { Command as Command24 } from "commander";
2153
+ var usageCommand = new Command24("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) => {
1988
2154
  let since;
1989
2155
  if (opts.since !== void 0) {
1990
2156
  const t = Date.parse(opts.since);
@@ -2031,8 +2197,8 @@ function block(u) {
2031
2197
  }
2032
2198
 
2033
2199
  // src/commands/vm/wake.ts
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) => {
2200
+ import { Command as Command25 } from "commander";
2201
+ var wakeCommand = new Command25("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) => {
2036
2202
  const client = await clientFor(cmd.optsWithGlobals());
2037
2203
  try {
2038
2204
  const box = await spin(`waking ${name} (loading its memory from disk)\u2026`, client.wakeMachine(name));
@@ -2044,15 +2210,15 @@ var wakeCommand = new Command24("wake").description("continue a hibernated box w
2044
2210
  });
2045
2211
 
2046
2212
  // src/commands/vm/index.ts
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);
2213
+ var vmCommand = new Command26("vm").enablePositionalOptions().description("manage boxes (each one is a Firecracker microVM)").addCommand(newCommand).addCommand(forkCommand).addCommand(lsCommand).addCommand(getCommand).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);
2048
2214
  for (const cmd of vmCommand.commands) if (cmd !== proxyCommand) cmd.option("--org <slug>", "act in this org (- for your personal boxes)");
2049
- var vmAliases = [newCommand, lsCommand, connectCommand, execCommand, stopCommand, startCommand, pauseCommand, resumeCommand, hibernateCommand, wakeCommand, rmCommand2, claimCommand];
2215
+ var vmAliases = [newCommand, lsCommand, getCommand, connectCommand, execCommand, stopCommand, startCommand, pauseCommand, resumeCommand, hibernateCommand, wakeCommand, rmCommand2, claimCommand];
2050
2216
 
2051
2217
  // src/commands/server/index.ts
2052
- import { Command as Command33 } from "commander";
2218
+ import { Command as Command34 } from "commander";
2053
2219
 
2054
2220
  // src/commands/server/join.ts
2055
- import { Command as Command26 } from "commander";
2221
+ import { Command as Command27 } from "commander";
2056
2222
 
2057
2223
  // src/commands/server/daemon.ts
2058
2224
  import { spawn as spawn3 } from "child_process";
@@ -2106,7 +2272,7 @@ function runKvboxServer(args) {
2106
2272
  }
2107
2273
 
2108
2274
  // src/commands/server/join.ts
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) => {
2275
+ var joinCommand = new Command27("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) => {
2110
2276
  const args = ["join", url, "--token", opts.token];
2111
2277
  if (opts.advertise !== void 0) args.push("--advertise", opts.advertise);
2112
2278
  if (opts.name !== void 0) args.push("--name", opts.name);
@@ -2116,8 +2282,8 @@ var joinCommand = new Command26("join").description("put this machine behind a c
2116
2282
  });
2117
2283
 
2118
2284
  // src/commands/server/leave.ts
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 () => {
2285
+ import { Command as Command28 } from "commander";
2286
+ var leaveCommand = new Command28("leave").description("take this machine out from behind its control plane and restart its server").action(async () => {
2121
2287
  const p = paths();
2122
2288
  for (const line of (await runKvboxServer(["leave"])).split("\n")) info(line);
2123
2289
  if (await runningPid(p) === null) return;
@@ -2126,14 +2292,14 @@ var leaveCommand = new Command27("leave").description("take this machine out fro
2126
2292
  });
2127
2293
 
2128
2294
  // src/commands/server/relay.ts
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) => {
2295
+ import { Command as Command29 } from "commander";
2296
+ var relayCommand = new Command29("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) => {
2131
2297
  for (const line of (await runKvboxServer(["relay", joinString])).split("\n")) info(line);
2132
2298
  });
2133
2299
 
2134
2300
  // src/commands/server/start.ts
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) => {
2301
+ import { Command as Command30 } from "commander";
2302
+ var startCommand2 = new Command30("start").description("start the local kvbox-server in the background").action(async (_opts, cmd) => {
2137
2303
  const client = await clientFor({ ...cmd.optsWithGlobals(), local: true }, { autoStart: false });
2138
2304
  try {
2139
2305
  const health = await client.health();
@@ -2149,8 +2315,8 @@ var startCommand2 = new Command29("start").description("start the local kvbox-se
2149
2315
  });
2150
2316
 
2151
2317
  // src/commands/server/status.ts
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) => {
2318
+ import { Command as Command31 } from "commander";
2319
+ var statusCommand = new Command31("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) => {
2154
2320
  const client = await clientFor({ ...cmd.optsWithGlobals(), local: true });
2155
2321
  try {
2156
2322
  const health = await client.health();
@@ -2182,8 +2348,8 @@ var statusCommand = new Command30("status").description("show whether this machi
2182
2348
  });
2183
2349
 
2184
2350
  // src/commands/server/stop.ts
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 () => {
2351
+ import { Command as Command32 } from "commander";
2352
+ var stopCommand2 = new Command32("stop").description("stop the local server (running boxes are not affected)").action(async () => {
2187
2353
  const pid = await runningPid(paths());
2188
2354
  if (pid === null) throw new KvboxError("kvbox-server is not running");
2189
2355
  await stopLocalServer(pid);
@@ -2191,8 +2357,8 @@ var stopCommand2 = new Command31("stop").description("stop the local server (run
2191
2357
  });
2192
2358
 
2193
2359
  // src/commands/server/trust-ca.ts
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) => {
2360
+ import { Command as Command33 } from "commander";
2361
+ var trustCaCommand = new Command33("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) => {
2196
2362
  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");
2197
2363
  if (publicKey !== void 0 && opts.fromCloud) throw new KvboxError("either a public key or --from-cloud, not both");
2198
2364
  const client = await clientFor({ ...cmd.optsWithGlobals(), local: true });
@@ -2230,7 +2396,7 @@ function describeReport(report) {
2230
2396
  }
2231
2397
 
2232
2398
  // src/commands/server/index.ts
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);
2399
+ var serverCommand = new Command34("server").description("run or manage the kvbox server").addCommand(startCommand2).addCommand(statusCommand).addCommand(stopCommand2).addCommand(joinCommand).addCommand(leaveCommand).addCommand(relayCommand).addCommand(trustCaCommand);
2234
2400
 
2235
2401
  // src/commands/login.ts
2236
2402
  import { spawnSync as spawnSync3 } from "child_process";
@@ -2238,7 +2404,7 @@ import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from
2238
2404
  import { hostname as osHostname } from "os";
2239
2405
  import { createInterface as createInterface2 } from "readline/promises";
2240
2406
  import { Writable } from "stream";
2241
- import { Command as Command34 } from "commander";
2407
+ import { Command as Command35 } from "commander";
2242
2408
 
2243
2409
  // src/prompt.ts
2244
2410
  import { createInterface } from "readline/promises";
@@ -2363,7 +2529,7 @@ function abortIsCancel(err) {
2363
2529
  }
2364
2530
 
2365
2531
  // 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) => {
2532
+ var loginCommand = new Command35("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) => {
2367
2533
  const url = normalizeUrl(urlArg ?? DEFAULT_CLOUD_URL);
2368
2534
  const creds = await readCredentials(opts);
2369
2535
  const session = await runLogin(url, creds, paths());
@@ -2443,8 +2609,8 @@ async function firstLine(stream) {
2443
2609
  }
2444
2610
 
2445
2611
  // src/commands/logout.ts
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 () => {
2612
+ import { Command as Command36 } from "commander";
2613
+ var logoutCommand = new Command36("logout").description("log out of the control plane; commands target this machine's kvbox-server again").action(async () => {
2448
2614
  const p = paths();
2449
2615
  const session = await readSession(p).catch(() => null);
2450
2616
  switch (await runLogout(p)) {
@@ -2481,8 +2647,8 @@ async function runLogout(p = paths()) {
2481
2647
  }
2482
2648
 
2483
2649
  // src/commands/whoami.ts
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) => {
2650
+ import { Command as Command37 } from "commander";
2651
+ var whoamiCommand = new Command37("whoami").description("show which account and control plane commands target (or local mode)").option("--json", "print as JSON").action(async (opts) => {
2486
2652
  const session = await readSession();
2487
2653
  if (opts.json) {
2488
2654
  json(session ? { user: session.user, url: session.url, sshCertExpires: session.sshCertExpires ?? null } : null);
@@ -2498,11 +2664,11 @@ function describeSession(session) {
2498
2664
  }
2499
2665
 
2500
2666
  // src/commands/snapshots/index.ts
2501
- import { Command as Command40 } from "commander";
2667
+ import { Command as Command41 } from "commander";
2502
2668
 
2503
2669
  // src/commands/snapshots/ls.ts
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) => {
2670
+ import { Command as Command38 } from "commander";
2671
+ var lsCommand3 = new Command38("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) => {
2506
2672
  const client = await clientFor(cmd.optsWithGlobals());
2507
2673
  try {
2508
2674
  const cloud = client.target.kind === "cloud";
@@ -2533,8 +2699,8 @@ var lsCommand3 = new Command37("ls").description("list snapshots (yours and your
2533
2699
  });
2534
2700
 
2535
2701
  // src/commands/snapshots/new.ts
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) => {
2702
+ import { Command as Command39 } from "commander";
2703
+ var newCommand2 = new Command39("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) => {
2538
2704
  const client = await clientFor(cmd.optsWithGlobals());
2539
2705
  try {
2540
2706
  const snap = await spin(`saving snapshot ${name} from box ${box} (memory + disk)\u2026`, client.createSnapshot({ box, name }));
@@ -2546,8 +2712,8 @@ var newCommand2 = new Command38("new").description("freeze a running box (memory
2546
2712
  });
2547
2713
 
2548
2714
  // src/commands/snapshots/rm.ts
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) => {
2715
+ import { Command as Command40 } from "commander";
2716
+ var rmCommand3 = new Command40("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) => {
2551
2717
  if (!await confirm(`Delete snapshot ${name}?`, opts.yes ?? false)) {
2552
2718
  info("cancelled");
2553
2719
  process.exitCode = 1;
@@ -2563,14 +2729,14 @@ var rmCommand3 = new Command39("rm").description("delete a snapshot (boxes made
2563
2729
  });
2564
2730
 
2565
2731
  // src/commands/snapshots/index.ts
2566
- var snapshotsCommand = new Command40("snapshots").aliases(["snapshot", "snap"]).description("freeze running boxes into snapshots and manage them").addCommand(newCommand2).addCommand(lsCommand3).addCommand(rmCommand3);
2732
+ var snapshotsCommand = new Command41("snapshots").aliases(["snapshot", "snap"]).description("freeze running boxes into snapshots and manage them").addCommand(newCommand2).addCommand(lsCommand3).addCommand(rmCommand3);
2567
2733
  for (const cmd of snapshotsCommand.commands) cmd.option("--org <slug>", "act in this org (- for your personal boxes)");
2568
2734
 
2569
2735
  // src/commands/org/index.ts
2570
- import { Command as Command46 } from "commander";
2736
+ import { Command as Command47 } from "commander";
2571
2737
 
2572
2738
  // src/commands/org/ls.ts
2573
- import { Command as Command41 } from "commander";
2739
+ import { Command as Command42 } from "commander";
2574
2740
 
2575
2741
  // src/commands/org/context.ts
2576
2742
  var NOT_LOGGED_IN = "orgs live on the control plane: kvbox login first";
@@ -2590,7 +2756,7 @@ function orgFor(opts, target) {
2590
2756
  }
2591
2757
 
2592
2758
  // src/commands/org/ls.ts
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) => {
2759
+ var lsCommand4 = new Command42("ls").description("list the orgs you belong to and your role in each").option("--json", "print as JSON").action(async (opts, cmd) => {
2594
2760
  const client = await cloudClientFor(cmd.optsWithGlobals());
2595
2761
  try {
2596
2762
  const orgs = await client.listOrgs();
@@ -2612,8 +2778,8 @@ function orgRows(orgs) {
2612
2778
  }
2613
2779
 
2614
2780
  // src/commands/org/members.ts
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) => {
2781
+ import { Command as Command43, Option as Option4 } from "commander";
2782
+ var lsCommand5 = new Command43("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) => {
2617
2783
  const client = await cloudClientFor(cmd.optsWithGlobals());
2618
2784
  try {
2619
2785
  const org = await client.getOrg(orgFor(opts, client.target));
@@ -2627,7 +2793,7 @@ var lsCommand5 = new Command42("ls").description("list an org's members and thei
2627
2793
  await client.close();
2628
2794
  }
2629
2795
  });
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) => {
2796
+ var addCommand2 = new Command43("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) => {
2631
2797
  const client = await cloudClientFor(cmd.optsWithGlobals());
2632
2798
  try {
2633
2799
  const slug = orgFor(opts, client.target);
@@ -2637,7 +2803,7 @@ var addCommand2 = new Command42("add").description("add a user to an org (admins
2637
2803
  await client.close();
2638
2804
  }
2639
2805
  });
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) => {
2806
+ var rmCommand4 = new Command43("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) => {
2641
2807
  const client = await cloudClientFor(cmd.optsWithGlobals());
2642
2808
  try {
2643
2809
  const slug = orgFor(opts, client.target);
@@ -2653,11 +2819,11 @@ var rmCommand4 = new Command42("rm").description("remove a member (or leave: rem
2653
2819
  await client.close();
2654
2820
  }
2655
2821
  });
2656
- var membersCommand = new Command42("members").description("who is in an org and what they may do").addCommand(lsCommand5).addCommand(addCommand2).addCommand(rmCommand4);
2822
+ var membersCommand = new Command43("members").description("who is in an org and what they may do").addCommand(lsCommand5).addCommand(addCommand2).addCommand(rmCommand4);
2657
2823
 
2658
2824
  // src/commands/org/new.ts
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) => {
2825
+ import { Command as Command44 } from "commander";
2826
+ var newCommand3 = new Command44("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) => {
2661
2827
  const client = await cloudClientFor(cmd.optsWithGlobals());
2662
2828
  try {
2663
2829
  const org = await client.createOrg({ slug, name: opts.name ?? slug });
@@ -2673,8 +2839,8 @@ var newCommand3 = new Command43("new").description("create an org; you become it
2673
2839
  });
2674
2840
 
2675
2841
  // src/commands/org/rm.ts
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) => {
2842
+ import { Command as Command45 } from "commander";
2843
+ var rmCommand5 = new Command45("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) => {
2678
2844
  if (!await confirm(`Delete org ${slug}? Its members lose access; its slug stays taken.`, opts.yes ?? false)) {
2679
2845
  info("cancelled");
2680
2846
  process.exitCode = 1;
@@ -2690,8 +2856,8 @@ var rmCommand5 = new Command44("rm").description("delete an org (owners only; re
2690
2856
  });
2691
2857
 
2692
2858
  // src/commands/org/role.ts
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) => {
2859
+ import { Argument, Command as Command46 } from "commander";
2860
+ var roleCommand = new Command46("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) => {
2695
2861
  const client = await cloudClientFor(cmd.optsWithGlobals());
2696
2862
  try {
2697
2863
  const slug = orgFor(opts, client.target);
@@ -2703,10 +2869,10 @@ var roleCommand = new Command45("role").description("change a member's role (own
2703
2869
  });
2704
2870
 
2705
2871
  // src/commands/org/index.ts
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);
2872
+ var orgCommand = new Command47("org").description("orgs: shared slugs that own boxes, with owners, admins and members").addCommand(lsCommand4).addCommand(newCommand3).addCommand(membersCommand).addCommand(roleCommand).addCommand(rmCommand5);
2707
2873
 
2708
2874
  // src/commands/usage.ts
2709
- import { Command as Command47 } from "commander";
2875
+ import { Command as Command48 } from "commander";
2710
2876
 
2711
2877
  // ../shared/src/usage.ts
2712
2878
  import { z as z4 } from "zod";
@@ -2714,7 +2880,9 @@ var PricingSchema = z4.object({
2714
2880
  currency: z4.string().min(1),
2715
2881
  vcpuHour: z4.number().nonnegative(),
2716
2882
  ramGibHour: z4.number().nonnegative(),
2717
- diskGibHour: z4.number().nonnegative()
2883
+ diskGibHour: z4.number().nonnegative(),
2884
+ volumeHour: z4.number().nonnegative().default(0),
2885
+ volumeGibHour: z4.number().nonnegative().default(0)
2718
2886
  });
2719
2887
  var round = (n, places) => Math.round(n * 10 ** places) / 10 ** places;
2720
2888
  function costOf(t, pricing) {
@@ -2741,7 +2909,7 @@ function sumUsage(items, pricing) {
2741
2909
  }
2742
2910
 
2743
2911
  // 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) => {
2912
+ var usageCommand2 = new Command48("usage").description("what your boxes and volumes 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) => {
2745
2913
  for (const [name, value] of [
2746
2914
  ["since", opts.since],
2747
2915
  ["until", opts.until]
@@ -2760,8 +2928,14 @@ var usageCommand2 = new Command47("usage").description("what your boxes used thi
2760
2928
  }
2761
2929
  const cloud = client.target.kind === "cloud" ? { me: client.target.user } : void 0;
2762
2930
  info(`${day2(report.window.from)} to ${day2(report.window.to)}`);
2763
- if (report.boxes.length === 0) info("no usage in this window");
2764
- else table(perUser ? userRows(perUser) : usageRows(report, cloud));
2931
+ const volumes = report.volumes ?? [];
2932
+ if (report.boxes.length === 0 && volumes.length === 0) info("no usage in this window");
2933
+ else if (report.boxes.length > 0) table(perUser ? userRows(perUser) : usageRows(report, cloud));
2934
+ if (volumes.length > 0) {
2935
+ info(volumes.length === 1 ? "1 volume" : `${volumes.length} volumes`);
2936
+ table(volumeRows(report, cloud));
2937
+ }
2938
+ if (volumes.length > 0 && report.totals.cost !== void 0) info(`total: ${money(report.totals.cost.amount, report.totals.cost.currency)} ${color.dim("(boxes and volumes, estimated)")}`);
2765
2939
  if (cloud) {
2766
2940
  const line = await creditLine(client, cloud.me);
2767
2941
  if (line !== void 0) info(line);
@@ -2810,7 +2984,32 @@ function usageRows(report, cloud) {
2810
2984
  "vcpu-h": hours(t.vcpuHours),
2811
2985
  "ram GiB-h": hours(t.ramGibHours),
2812
2986
  "disk GiB-h": hours(t.diskGibHours),
2813
- ...priced ? { cost: `${money(t.cost.amount, t.cost.currency)} ${color.dim("(estimated)")}` } : {}
2987
+ ...priced ? { cost: `${money(sumCost(report.boxes), t.cost.currency)} ${color.dim("(estimated)")}` } : {}
2988
+ });
2989
+ return rows2;
2990
+ }
2991
+ var sumCost = (rows2) => round(rows2.reduce((n, r) => n + (r.cost?.amount ?? 0), 0), 4);
2992
+ function volumeRows(report, cloud) {
2993
+ const priced = report.totals.cost !== void 0;
2994
+ const volumes = report.volumes ?? [];
2995
+ const currency = report.totals.cost?.currency ?? "";
2996
+ const rows2 = volumes.map((v) => ({
2997
+ ...cloud ? { owner: ownerCell2(v, cloud.me) } : {},
2998
+ volume: boxName(v.name),
2999
+ status: v.status === "removed" ? color.dim("removed") : "present",
3000
+ size: `${v.sizeGib}GB`,
3001
+ hours: hours(v.volumeHours),
3002
+ "GiB-h": hours(v.volumeGibHours),
3003
+ ...priced ? { cost: money(v.cost?.amount ?? 0, currency) } : {}
3004
+ }));
3005
+ rows2.push({
3006
+ ...cloud ? { owner: "" } : {},
3007
+ volume: color.bold("total"),
3008
+ status: "",
3009
+ size: "",
3010
+ hours: hours(report.totals.volumeHours ?? 0),
3011
+ "GiB-h": hours(report.totals.volumeGibHours ?? 0),
3012
+ ...priced ? { cost: money(sumCost(volumes), currency) } : {}
2814
3013
  });
2815
3014
  return rows2;
2816
3015
  }
@@ -2830,7 +3029,7 @@ function byUser(report) {
2830
3029
  return u;
2831
3030
  });
2832
3031
  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 };
3032
+ return { window: report.window, users, ...report.volumes === void 0 ? {} : { volumes: report.volumes }, totals: report.totals };
2834
3033
  }
2835
3034
  function userRows(r) {
2836
3035
  const priced = r.totals.cost !== void 0;
@@ -2850,14 +3049,14 @@ function userRows(r) {
2850
3049
  user: color.bold("total"),
2851
3050
  boxes: String(r.users.reduce((n, u) => n + u.boxes, 0)),
2852
3051
  ...cells(r.totals),
2853
- ...priced ? { cost: `${money(r.totals.cost.amount, r.totals.cost.currency)} ${color.dim("(estimated)")}` } : {}
3052
+ ...priced ? { cost: `${money(sumCost(r.users), r.totals.cost.currency)} ${color.dim("(estimated)")}` } : {}
2854
3053
  });
2855
3054
  return rows2;
2856
3055
  }
2857
3056
 
2858
3057
  // src/commands/credit.ts
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) => {
3058
+ import { Command as Command49 } from "commander";
3059
+ var creditCommand = new Command49("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) => {
2861
3060
  const client = await clientFor(cmd.optsWithGlobals(), { autoStart: false });
2862
3061
  try {
2863
3062
  if (client.target.kind === "local") throw new KvboxError("credit lives on the control plane: kvbox login first");
@@ -2887,7 +3086,7 @@ function creditRow(b, me2) {
2887
3086
  // src/commands/deploy/index.ts
2888
3087
  import { readFile as readFile7 } from "fs/promises";
2889
3088
  import { basename, resolve as resolve2 } from "path";
2890
- import { Command as Command49, Option as Option5 } from "commander";
3089
+ import { Command as Command50, Option as Option5 } from "commander";
2891
3090
 
2892
3091
  // src/commands/deploy/config.ts
2893
3092
  import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
@@ -2973,7 +3172,8 @@ var nextjsTemplate = {
2973
3172
  "mkdir -p /srv/app/.incoming",
2974
3173
  "tar -xf - -C /srv/app/.incoming",
2975
3174
  "cd /srv/app/.incoming",
2976
- "export CI=1 NEXT_TELEMETRY_DISABLED=1 NODE_ENV=production",
3175
+ // Not NODE_ENV=production here: the install would skip devDependencies, which the build needs (TypeScript, Tailwind).
3176
+ "export CI=1 NEXT_TELEMETRY_DISABLED=1",
2977
3177
  ...c.setup,
2978
3178
  c.install,
2979
3179
  c.build,
@@ -3173,7 +3373,7 @@ async function ship(ep, dir, remote, exclude = []) {
3173
3373
  var shipping = { ship, measure, waitForSsh };
3174
3374
 
3175
3375
  // src/commands/deploy/index.ts
3176
- var deployCommand = new Command49("deploy").description("put the app in this directory (React or Next.js) on a box with its own HTTPS address; redeploy by running it again").argument("[dir]", "the app's directory (default: the current one)").addOption(new Option5("-t, --template <template>", "what kind of app it is (detected from package.json when not given)").choices(TEMPLATE_IDS)).option("-n, --name <name>", "the box's name (default: the directory's name)").option("--sub <subdomain>", "its HTTPS name, https://<subdomain>.kvbox.app (default: the box's name)").addOption(new Option5("-s, --size <size>", "size of the box when it has to be made").choices(SIZE_NAMES)).option("-y, --yes", "ask nothing: take the defaults for whatever the flags and kvbox.json leave open").option("--json", "print the result as JSON").option("--org <slug>", "deploy into this org's boxes (saved to kvbox.json)").action(async (dirArg, opts, cmd) => {
3376
+ var deployCommand = new Command50("deploy").description("put the app in this directory (React or Next.js) on a box with its own HTTPS address; redeploy by running it again").argument("[dir]", "the app's directory (default: the current one)").addOption(new Option5("-t, --template <template>", "what kind of app it is (detected from package.json when not given)").choices(TEMPLATE_IDS)).option("-n, --name <name>", "the box's name (default: the directory's name)").option("--sub <subdomain>", "its HTTPS name, https://<subdomain>.kvbox.app (default: the box's name)").addOption(new Option5("-s, --size <size>", "size of the box when it has to be made").choices(SIZE_NAMES)).option("-y, --yes", "ask nothing: take the defaults for whatever the flags and kvbox.json leave open").option("--json", "print the result as JSON").option("--org <slug>", "deploy into this org's boxes (saved to kvbox.json)").action(async (dirArg, opts, cmd) => {
3177
3377
  const dir = resolve2(dirArg ?? ".");
3178
3378
  const pkg = await readPackageJson(dir);
3179
3379
  const saved = await readConfig(dir);
@@ -3351,11 +3551,123 @@ the app is on box ${ref} but has no address yet: pick another with kvbox deploy
3351
3551
  }
3352
3552
  }
3353
3553
 
3554
+ // src/commands/volume/index.ts
3555
+ import { Command as Command56 } from "commander";
3556
+
3557
+ // src/commands/volume/attach.ts
3558
+ import { Command as Command51 } from "commander";
3559
+ var attachCommand = new Command51("attach").description("attach a volume to a box at /mnt/<volume> (a running box is restarted to take it)").argument("<volume>", "volume name").argument("<box>", "box name").option("--json", "print the box as JSON").action(async (volume, box, opts, cmd) => {
3560
+ const client = await clientFor(cmd.optsWithGlobals());
3561
+ try {
3562
+ const machine = await spin(`attaching volume ${volume} to box ${box} (a running box is restarted)\u2026`, client.attachVolume(volume, { box }));
3563
+ if (opts.json) {
3564
+ json(machine);
3565
+ return;
3566
+ }
3567
+ const path = machine.volumes?.find((v) => volume === v.name || volume.endsWith(`/${v.name}`))?.mountPath ?? volumeMountPath(volume.slice(volume.lastIndexOf("/") + 1));
3568
+ info(`${color.green("\u2713")} volume ${volume} attached to box ${box}`);
3569
+ info(machine.status === "running" ? ` the box was restarted; the volume is at ${path} inside it` : ` box ${box} is ${machine.status}; the volume is at ${path} once it runs`);
3570
+ } finally {
3571
+ await client.close();
3572
+ }
3573
+ });
3574
+
3575
+ // src/commands/volume/create.ts
3576
+ import { Command as Command52 } from "commander";
3577
+ function parseGib2(text2) {
3578
+ const mib = parseMib(text2);
3579
+ if (mib === void 0 || mib % 1024 !== 0) throw new KvboxError(`--size ${text2}: write whole GB, like 50 or 50GB`);
3580
+ const gib = mib / 1024;
3581
+ if (gib > MAX_VOLUME_GIB) throw new KvboxError(`--size ${text2}: a volume is at most ${MAX_VOLUME_GIB} GB`);
3582
+ return gib;
3583
+ }
3584
+ var createCommand = new Command52("create").alias("new").description("create a volume: an empty disk of its own, ready to attach to a box").argument("<name>", "volume name (1-12 chars; the box mounts it at /mnt/<name>)").requiredOption("--size <gb>", "size in GB, like 50 or 50GB (whole GB; it only occupies what is written)").option("--box <box>", "the box it is for: made on that box's host (a volume attaches only to a box on its own host)").option("--json", "print the volume as JSON").action(async (name, opts, cmd) => {
3585
+ const sizeGib = parseGib2(opts.size);
3586
+ const client = await clientFor(cmd.optsWithGlobals());
3587
+ try {
3588
+ const vol = await spin(`creating volume ${name} (${sizeGib} GB)\u2026`, client.createVolume({ name, sizeGib, ...opts.box !== void 0 ? { box: opts.box } : {} }));
3589
+ if (opts.json) json(vol);
3590
+ else info(`${color.green("\u2713")} volume ${name} created (${vol.sizeGib} GB); attach it to a box with: kvbox volume attach ${name} ${opts.box ?? "<box>"}`);
3591
+ } finally {
3592
+ await client.close();
3593
+ }
3594
+ });
3595
+
3596
+ // src/commands/volume/detach.ts
3597
+ import { Command as Command53 } from "commander";
3598
+ var detachCommand = new Command53("detach").description("detach a volume from a box; its data is kept (a running box is restarted)").argument("<volume>", "volume name").argument("<box>", "box name").option("--json", "print the box as JSON").action(async (volume, box, opts, cmd) => {
3599
+ const client = await clientFor(cmd.optsWithGlobals());
3600
+ try {
3601
+ const machine = await spin(`detaching volume ${volume} from box ${box} (a running box is restarted)\u2026`, client.detachVolume(volume, { box }));
3602
+ if (opts.json) {
3603
+ json(machine);
3604
+ return;
3605
+ }
3606
+ info(`${color.green("\u2713")} volume ${volume} detached from box ${box}; its data is kept${machine.status === "running" ? " (the box was restarted)" : ""}`);
3607
+ } finally {
3608
+ await client.close();
3609
+ }
3610
+ });
3611
+
3612
+ // src/commands/volume/ls.ts
3613
+ import { Command as Command54 } from "commander";
3614
+ var lsCommand6 = new Command54("ls").alias("list").description("list volumes: size, what they occupy, which box has them").option("-a, --all", "every volume on the platform (platform admins only)").option("--json", "print as JSON").action(async (opts, cmd) => {
3615
+ const client = await clientFor(cmd.optsWithGlobals());
3616
+ try {
3617
+ const cloud = client.target.kind === "cloud";
3618
+ const vols = await client.listVolumes({ all: opts.all });
3619
+ if (opts.json) {
3620
+ json(vols);
3621
+ return;
3622
+ }
3623
+ if (vols.length === 0) {
3624
+ info("no volumes yet; make one with: kvbox volume create <name> --size 50GB");
3625
+ return;
3626
+ }
3627
+ const org = cloud ? client.target.org : void 0;
3628
+ if (org !== void 0) info(`volumes of ${org}`);
3629
+ table(
3630
+ vols.map((v) => ({
3631
+ ...cloud ? { owner: slugOf(v.owner) === client.target.user ? "private" : slugOf(v.owner) } : {},
3632
+ name: boxName(v.name),
3633
+ size: `${v.sizeGib}GB`,
3634
+ used: gbShort(v.usedBytes),
3635
+ "attached to": v.attachedTo ?? "-",
3636
+ created: age(v.createdAt)
3637
+ }))
3638
+ );
3639
+ } finally {
3640
+ await client.close();
3641
+ }
3642
+ });
3643
+
3644
+ // src/commands/volume/rm.ts
3645
+ import { Command as Command55 } from "commander";
3646
+ var rmCommand6 = new Command55("rm").alias("remove").description("delete a volume and everything on it (detach it first)").argument("<name>", "volume name").option("-y, --yes", "do not ask for confirmation").action(async (name, opts, cmd) => {
3647
+ if (!await confirm(`Delete volume ${name} and everything on it?`, opts.yes ?? false)) {
3648
+ info("cancelled");
3649
+ process.exitCode = 1;
3650
+ return;
3651
+ }
3652
+ const client = await clientFor(cmd.optsWithGlobals());
3653
+ try {
3654
+ await spin(`deleting volume ${name}\u2026`, client.deleteVolume(name));
3655
+ info(`${color.green("\u2713")} volume ${name} deleted`);
3656
+ } finally {
3657
+ await client.close();
3658
+ }
3659
+ });
3660
+
3661
+ // src/commands/volume/index.ts
3662
+ var volumeCommand = new Command56("volume").aliases(["volumes", "vol"]).description("disks of their own: create one, attach it to a box at /mnt/<name>, move it to another box later").addCommand(createCommand).addCommand(lsCommand6).addCommand(attachCommand).addCommand(detachCommand).addCommand(rmCommand6);
3663
+ for (const cmd of volumeCommand.commands) cmd.option("--org <slug>", "act in this org (- for your personal volumes)");
3664
+
3354
3665
  // src/program.ts
3355
3666
  function buildProgram() {
3356
- const program = new Command50("kvbox").description("self-hosted Firecracker microVMs with Docker-like ergonomics").version(VERSION, "-v, --version").option("--local", "use this machine's kvbox-server over its socket, ignoring kvbox login").option("--org <slug>", "the org: where kvbox new creates, and which box a name means when it is not one of yours; before the command here, or after it").showHelpAfterError().enablePositionalOptions();
3667
+ const program = new Command57("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();
3357
3668
  program.addCommand(vmCommand);
3358
3669
  program.addCommand(snapshotsCommand);
3670
+ program.addCommand(volumeCommand);
3359
3671
  program.addCommand(serverCommand);
3360
3672
  program.addCommand(loginCommand);
3361
3673
  program.addCommand(logoutCommand);