@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 +463 -151
- package/dist/kvbox.js.map +1 -1
- package/package.json +1 -1
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
|
|
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.
|
|
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
|
|
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/
|
|
1445
|
-
import { Command as
|
|
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
|
|
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
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1786
|
+
import { Command as Command15 } from "commander";
|
|
1622
1787
|
|
|
1623
1788
|
// src/commands/vm/proxy/add.ts
|
|
1624
|
-
import { Command as
|
|
1625
|
-
var addCommand = new
|
|
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
|
|
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
|
|
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
|
|
1702
|
-
var releaseCommand = new
|
|
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
|
|
1722
|
-
var rmCommand = new
|
|
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
|
|
1755
|
-
var setPortCommand = new
|
|
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
|
|
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
|
|
1778
|
-
var rescaleCommand = new
|
|
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
|
|
1824
|
-
var resumeCommand = new
|
|
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
|
|
1837
|
-
var rmCommand2 = new
|
|
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
|
-
|
|
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
|
|
1857
|
-
var setCommand = new
|
|
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
|
|
1877
|
-
var startCommand = new
|
|
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
|
|
1890
|
-
var stopCommand = new
|
|
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
|
|
1903
|
-
var topCommand = new
|
|
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
|
|
1964
|
-
var updateCommand = new
|
|
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
|
|
1987
|
-
var usageCommand = new
|
|
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
|
|
2035
|
-
var wakeCommand = new
|
|
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
|
|
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
|
|
2218
|
+
import { Command as Command34 } from "commander";
|
|
2053
2219
|
|
|
2054
2220
|
// src/commands/server/join.ts
|
|
2055
|
-
import { Command as
|
|
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
|
|
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
|
|
2120
|
-
var leaveCommand = new
|
|
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
|
|
2130
|
-
var relayCommand = new
|
|
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
|
|
2136
|
-
var startCommand2 = new
|
|
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
|
|
2153
|
-
var statusCommand = new
|
|
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
|
|
2186
|
-
var stopCommand2 = new
|
|
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
|
|
2195
|
-
var trustCaCommand = new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2447
|
-
var logoutCommand = new
|
|
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
|
|
2485
|
-
var whoamiCommand = new
|
|
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
|
|
2667
|
+
import { Command as Command41 } from "commander";
|
|
2502
2668
|
|
|
2503
2669
|
// src/commands/snapshots/ls.ts
|
|
2504
|
-
import { Command as
|
|
2505
|
-
var lsCommand3 = new
|
|
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
|
|
2537
|
-
var newCommand2 = new
|
|
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
|
|
2550
|
-
var rmCommand3 = new
|
|
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
|
|
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
|
|
2736
|
+
import { Command as Command47 } from "commander";
|
|
2571
2737
|
|
|
2572
2738
|
// src/commands/org/ls.ts
|
|
2573
|
-
import { Command as
|
|
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
|
|
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
|
|
2616
|
-
var lsCommand5 = new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2660
|
-
var newCommand3 = new
|
|
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
|
|
2677
|
-
var rmCommand5 = new
|
|
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
|
|
2694
|
-
var roleCommand = new
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2764
|
-
|
|
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(
|
|
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.
|
|
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
|
|
2860
|
-
var creditCommand = new
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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);
|