@kvbox/cli 0.5.25 → 0.5.27
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 +505 -186
- 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 Command65 } from "commander";
|
|
153
153
|
|
|
154
154
|
// ../shared/src/version.ts
|
|
155
155
|
import { createRequire } from "module";
|
|
@@ -164,7 +164,7 @@ 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.27" : void 0);
|
|
168
168
|
|
|
169
169
|
// src/commands/vm/index.ts
|
|
170
170
|
import { Command as Command26 } from "commander";
|
|
@@ -193,6 +193,8 @@ function paths(home = kvboxHome()) {
|
|
|
193
193
|
serverLog: join(home, "server.log"),
|
|
194
194
|
/** The CLI's login to the control plane (`kvbox login`): url, bearer token, user. Mode 600. */
|
|
195
195
|
cli: join(home, "cli.json"),
|
|
196
|
+
/** The CLI's last look at npm for a newer @kvbox/cli: `{checkedAt, latest}`, at most once a day (cli update.ts). */
|
|
197
|
+
updateCheck: join(home, "update-check.json"),
|
|
196
198
|
ssh: join(home, "ssh"),
|
|
197
199
|
sshKey: join(home, "ssh", "id_ed25519"),
|
|
198
200
|
/** The device key signed by the control plane's CA (#51); ssh loads it by itself next to `-i id_ed25519`. */
|
|
@@ -440,6 +442,19 @@ var SubdomainSchema = z2.object({
|
|
|
440
442
|
url: z2.string(),
|
|
441
443
|
routes: z2.array(z2.object({ name: z2.string(), port: BoxPortSchema, url: z2.string() }))
|
|
442
444
|
});
|
|
445
|
+
var AddDomainRequest = z2.object({ domain: z2.string(), port: BoxPortSchema.optional() });
|
|
446
|
+
var DomainSchema = z2.object({
|
|
447
|
+
owner: z2.string(),
|
|
448
|
+
box: z2.string(),
|
|
449
|
+
domain: z2.string(),
|
|
450
|
+
port: BoxPortSchema,
|
|
451
|
+
status: z2.enum(["pending", "live"]),
|
|
452
|
+
www: z2.boolean(),
|
|
453
|
+
url: z2.string(),
|
|
454
|
+
error: z2.string().nullable(),
|
|
455
|
+
records: z2.array(z2.object({ type: z2.enum(["A", "TXT", "CNAME"]), name: z2.string(), value: z2.string() })),
|
|
456
|
+
createdAt: z2.string()
|
|
457
|
+
});
|
|
443
458
|
var MachineSchema = z2.object({
|
|
444
459
|
/** Host-unique short id (the tap is `kv-<id>`). Missing only on a box made before ids that has not restarted since. */
|
|
445
460
|
id: z2.string(),
|
|
@@ -495,6 +510,8 @@ var VolumeSchema = z2.object({
|
|
|
495
510
|
ownerKind: z2.enum(["user", "org"]).optional()
|
|
496
511
|
});
|
|
497
512
|
var MAX_VOLUME_GIB = 4096;
|
|
513
|
+
var VOLUME_NAME_RE = /^[a-z0-9][a-z0-9-]{0,11}$/;
|
|
514
|
+
var VOLUME_NAME_RULE = "1-12 chars, lowercase letters, digits and '-', starting with a letter or digit";
|
|
498
515
|
var CreateVolumeRequest = z2.object({
|
|
499
516
|
name: z2.string(),
|
|
500
517
|
sizeGib: z2.number().int().min(1).max(MAX_VOLUME_GIB),
|
|
@@ -825,10 +842,26 @@ var TopUpSchema = z2.object({
|
|
|
825
842
|
slug: z2.string(),
|
|
826
843
|
amount: z2.number(),
|
|
827
844
|
currency: z2.string(),
|
|
828
|
-
|
|
845
|
+
/** `transfer` (#93): an owner moving credit to an org they own, a negative row on their slug and a positive one on the org's. */
|
|
846
|
+
source: z2.enum(["admin", "card", "trial", "transfer"]),
|
|
829
847
|
note: z2.string().nullable(),
|
|
830
848
|
at: z2.string()
|
|
831
849
|
});
|
|
850
|
+
var TOP_UP_AMOUNTS = [10, 25, 50, 100];
|
|
851
|
+
var BillingInfoSchema = z2.object({
|
|
852
|
+
card: z2.object({ brand: z2.string(), last4: z2.string() }).nullable(),
|
|
853
|
+
cardInUse: z2.boolean(),
|
|
854
|
+
balance: BalanceSchema
|
|
855
|
+
});
|
|
856
|
+
var BillingRedirectSchema = z2.object({ url: z2.string() });
|
|
857
|
+
var TopUpCheckoutRequestSchema = z2.object({
|
|
858
|
+
amount: z2.literal(TOP_UP_AMOUNTS)
|
|
859
|
+
});
|
|
860
|
+
var TransferRequestSchema = z2.object({
|
|
861
|
+
org: z2.string().min(1),
|
|
862
|
+
amount: z2.number().positive().multipleOf(0.01, "amount is in cents at most")
|
|
863
|
+
});
|
|
864
|
+
var TransferResponseSchema = z2.object({ from: BalanceSchema, to: BalanceSchema, activated: z2.boolean() });
|
|
832
865
|
var LoginRequestSchema = z2.object({
|
|
833
866
|
email: z2.string().min(1),
|
|
834
867
|
password: z2.string().min(1),
|
|
@@ -1125,6 +1158,17 @@ var ServerClient = class {
|
|
|
1125
1158
|
listSubdomains() {
|
|
1126
1159
|
return this.call("GET", "/subdomains", z3.array(SubdomainSchema));
|
|
1127
1160
|
}
|
|
1161
|
+
/** Custom domains (#89), the control plane's only: serve the box at the user's own name. */
|
|
1162
|
+
addDomain(name, req) {
|
|
1163
|
+
return this.call("POST", `/machines/${encodeURIComponent(name)}/domains`, DomainSchema, req);
|
|
1164
|
+
}
|
|
1165
|
+
removeDomain(name, domain) {
|
|
1166
|
+
return this.call("DELETE", `/machines/${encodeURIComponent(name)}/domains/${encodeURIComponent(domain)}`, DomainSchema);
|
|
1167
|
+
}
|
|
1168
|
+
/** Every readable box's custom domains. */
|
|
1169
|
+
listDomains() {
|
|
1170
|
+
return this.call("GET", "/domains", z3.array(DomainSchema));
|
|
1171
|
+
}
|
|
1128
1172
|
/** A running box's own load, memory and disk, asked of its guest (#67). */
|
|
1129
1173
|
machineStats(name) {
|
|
1130
1174
|
return this.call("GET", `/machines/${encodeURIComponent(name)}/stats`, MachineStatsSchema);
|
|
@@ -1274,8 +1318,8 @@ var me = (client) => client.target.kind === "cloud" ? client.target.user : void
|
|
|
1274
1318
|
async function clientFor(opts, client = {}) {
|
|
1275
1319
|
return new ServerClient(await resolveTarget(opts), client);
|
|
1276
1320
|
}
|
|
1277
|
-
function sshEndpoint(box, target) {
|
|
1278
|
-
if (target.kind === "local") return { host: box.ip, port: 22, user: BOX_SSH_USER };
|
|
1321
|
+
function sshEndpoint(box, target, direct = false) {
|
|
1322
|
+
if (target.kind === "local" || direct) return { host: box.ip, port: 22, user: BOX_SSH_USER };
|
|
1279
1323
|
if (box.ssh === void 0) throw new KvboxError(`box ${box.name} has no public SSH port`);
|
|
1280
1324
|
return { ...box.ssh, alias: `${box.id}.${box.ssh.host}` };
|
|
1281
1325
|
}
|
|
@@ -1318,6 +1362,11 @@ async function waitForSsh(ep, timeoutMs = 3e4) {
|
|
|
1318
1362
|
}
|
|
1319
1363
|
throw new KvboxError(`box at ${ep.host}:${ep.port} did not answer within ${Math.round(timeoutMs / 1e3)}s`);
|
|
1320
1364
|
}
|
|
1365
|
+
async function checkDirect(ep) {
|
|
1366
|
+
await waitForSsh(ep, 2e3).catch(() => {
|
|
1367
|
+
throw new KvboxError(`--direct: cannot reach ${ep.host}:${ep.port}; it works only on the box's host (leave it out to go through the public address)`);
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1321
1370
|
var NOT_A_HOST_HINT = "kvbox-server is not installed, so this machine is not a kvbox host. Set it up with install-server.sh, or log in to the cloud with: kvbox login";
|
|
1322
1371
|
async function lastErrorLine(logPath, from = 0) {
|
|
1323
1372
|
const { readFile: readFile8 } = await import("fs/promises");
|
|
@@ -1381,6 +1430,199 @@ async function serving(p, timeoutMs, check) {
|
|
|
1381
1430
|
throw new KvboxError(`kvbox-server did not start within ${timeoutMs / 1e3}s; see ${p.serverLog}`);
|
|
1382
1431
|
}
|
|
1383
1432
|
|
|
1433
|
+
// src/prompt.ts
|
|
1434
|
+
import { createInterface } from "readline/promises";
|
|
1435
|
+
function interactive() {
|
|
1436
|
+
return process.stdin.isTTY === true && process.stderr.isTTY === true;
|
|
1437
|
+
}
|
|
1438
|
+
var ESC = "\x1B";
|
|
1439
|
+
var ENTER = "\r";
|
|
1440
|
+
var CTRL_C = "";
|
|
1441
|
+
var CTRL_D = "";
|
|
1442
|
+
var cancelled = () => new KvboxError("cancelled", 130);
|
|
1443
|
+
function keysIn(chunk) {
|
|
1444
|
+
const keys = [];
|
|
1445
|
+
for (let i = 0; i < chunk.length; ) {
|
|
1446
|
+
if (chunk[i] === ESC && chunk[i + 1] === "[" && i + 2 < chunk.length) {
|
|
1447
|
+
let j = i + 2;
|
|
1448
|
+
while (j < chunk.length && /[0-9;?]/.test(chunk[j])) j += 1;
|
|
1449
|
+
const end = j < chunk.length ? j + 1 : j;
|
|
1450
|
+
keys.push(chunk.slice(i, end));
|
|
1451
|
+
i = end;
|
|
1452
|
+
} else if (chunk[i] === ESC && chunk[i + 1] === "O" && i + 2 < chunk.length) {
|
|
1453
|
+
keys.push(chunk.slice(i, i + 3));
|
|
1454
|
+
i += 3;
|
|
1455
|
+
} else {
|
|
1456
|
+
keys.push(chunk[i]);
|
|
1457
|
+
i += 1;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return keys;
|
|
1461
|
+
}
|
|
1462
|
+
var isUp = (k) => k === `${ESC}[A` || k === `${ESC}OA` || k === "k";
|
|
1463
|
+
var isDown = (k) => k === `${ESC}[B` || k === `${ESC}OB` || k === "j";
|
|
1464
|
+
var ANSI2 = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
1465
|
+
function rows(line, columns = process.stderr.columns ?? 80) {
|
|
1466
|
+
const width2 = line.replace(ANSI2, "").length;
|
|
1467
|
+
return Math.max(1, Math.ceil(width2 / Math.max(1, columns)));
|
|
1468
|
+
}
|
|
1469
|
+
function settled(message, answer) {
|
|
1470
|
+
return `${color.green("\u2713")} ${color.bold(message)} ${color.cyan(answer)}`;
|
|
1471
|
+
}
|
|
1472
|
+
async function select(message, choices, initial = 0) {
|
|
1473
|
+
const out = process.stderr;
|
|
1474
|
+
const inp = process.stdin;
|
|
1475
|
+
let index = Math.min(Math.max(initial, 0), choices.length - 1);
|
|
1476
|
+
let drawn = 0;
|
|
1477
|
+
const render = () => {
|
|
1478
|
+
if (drawn > 0) out.write(`${ESC}[${drawn}A`);
|
|
1479
|
+
const lines = [
|
|
1480
|
+
`${color.green("?")} ${color.bold(message)} ${color.dim("\u2191\u2193 to move, enter to pick")}`,
|
|
1481
|
+
...choices.map((c, i) => {
|
|
1482
|
+
const hint = c.hint ? color.dim(` ${c.hint}`) : "";
|
|
1483
|
+
return i === index ? `${color.cyan(`\u276F ${c.label}`)}${hint}` : ` ${c.label}${hint}`;
|
|
1484
|
+
})
|
|
1485
|
+
];
|
|
1486
|
+
out.write(`\r${ESC}[J${lines.join("\n")}
|
|
1487
|
+
`);
|
|
1488
|
+
drawn = lines.reduce((n, l) => n + rows(l), 0);
|
|
1489
|
+
};
|
|
1490
|
+
inp.setRawMode(true);
|
|
1491
|
+
inp.resume();
|
|
1492
|
+
inp.setEncoding("utf8");
|
|
1493
|
+
render();
|
|
1494
|
+
let done = false;
|
|
1495
|
+
try {
|
|
1496
|
+
return await new Promise((resolve3, reject) => {
|
|
1497
|
+
const onData = (chunk) => {
|
|
1498
|
+
for (const key of keysIn(chunk)) {
|
|
1499
|
+
if (isUp(key)) index = (index - 1 + choices.length) % choices.length;
|
|
1500
|
+
else if (isDown(key)) index = (index + 1) % choices.length;
|
|
1501
|
+
else if (key === ENTER || key === "\n") {
|
|
1502
|
+
inp.off("data", onData);
|
|
1503
|
+
done = true;
|
|
1504
|
+
resolve3(choices[index].value);
|
|
1505
|
+
return;
|
|
1506
|
+
} else if (key === CTRL_C || key === CTRL_D) {
|
|
1507
|
+
inp.off("data", onData);
|
|
1508
|
+
reject(cancelled());
|
|
1509
|
+
return;
|
|
1510
|
+
} else continue;
|
|
1511
|
+
render();
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
inp.on("data", onData);
|
|
1515
|
+
});
|
|
1516
|
+
} finally {
|
|
1517
|
+
inp.setRawMode(false);
|
|
1518
|
+
inp.pause();
|
|
1519
|
+
out.write(`${ESC}[${drawn}A\r${ESC}[J${done ? `${settled(message, choices[index].label)}
|
|
1520
|
+
` : ""}`);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
async function text(message, opts = {}) {
|
|
1524
|
+
const out = process.stderr;
|
|
1525
|
+
for (; ; ) {
|
|
1526
|
+
const hint = opts.default !== void 0 ? ` ${color.dim(`(${opts.default})`)}` : "";
|
|
1527
|
+
const question = `${color.green("?")} ${color.bold(message)}${hint} ${color.dim("\u203A")} `;
|
|
1528
|
+
const rl = createInterface({ input: process.stdin, output: out });
|
|
1529
|
+
const line = await askLine(rl, question);
|
|
1530
|
+
const raw = opts.trim === false ? line : line.trim();
|
|
1531
|
+
const answer = raw === "" && opts.default !== void 0 ? opts.default : raw;
|
|
1532
|
+
const problem = opts.validate?.(answer);
|
|
1533
|
+
out.write(`${ESC}[${rows(question + raw)}A\r${ESC}[J`);
|
|
1534
|
+
if (problem === void 0) {
|
|
1535
|
+
out.write(`${settled(message, answer)}
|
|
1536
|
+
`);
|
|
1537
|
+
return answer;
|
|
1538
|
+
}
|
|
1539
|
+
out.write(`${color.yellow("!")} ${problem}
|
|
1540
|
+
`);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
async function hiddenText(message) {
|
|
1544
|
+
const out = process.stderr;
|
|
1545
|
+
const inp = process.stdin;
|
|
1546
|
+
const question = `${color.green("?")} ${color.bold(message)} ${color.dim("(hidden) \u203A")} `;
|
|
1547
|
+
out.write(question);
|
|
1548
|
+
inp.setRawMode(true);
|
|
1549
|
+
inp.resume();
|
|
1550
|
+
inp.setEncoding("utf8");
|
|
1551
|
+
let chars = [];
|
|
1552
|
+
let done = false;
|
|
1553
|
+
try {
|
|
1554
|
+
return await new Promise((resolve3, reject) => {
|
|
1555
|
+
const onData = (chunk) => {
|
|
1556
|
+
for (const key of keysIn(chunk)) {
|
|
1557
|
+
if (key === ENTER || key === "\n") {
|
|
1558
|
+
inp.off("data", onData);
|
|
1559
|
+
done = true;
|
|
1560
|
+
resolve3(chars.join(""));
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
if (key === CTRL_C || key === CTRL_D) {
|
|
1564
|
+
inp.off("data", onData);
|
|
1565
|
+
reject(cancelled());
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
if (key === "\x7F" || key === "\b") chars = chars.slice(0, -1);
|
|
1569
|
+
else if (key.length === 1 && (key >= " " || key === " ")) chars.push(key);
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
inp.on("data", onData);
|
|
1573
|
+
});
|
|
1574
|
+
} finally {
|
|
1575
|
+
inp.setRawMode(false);
|
|
1576
|
+
inp.pause();
|
|
1577
|
+
out.write(`\r${ESC}[J${done ? `${settled(message, "\u2022\u2022\u2022\u2022\u2022")}
|
|
1578
|
+
` : "\n"}`);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
async function askLine(rl, question) {
|
|
1582
|
+
let onClose = () => {
|
|
1583
|
+
};
|
|
1584
|
+
const closed = new Promise((_, reject) => {
|
|
1585
|
+
onClose = () => reject(cancelled());
|
|
1586
|
+
rl.once("close", onClose);
|
|
1587
|
+
});
|
|
1588
|
+
try {
|
|
1589
|
+
return await Promise.race([rl.question(question).catch(abortIsCancel), closed]);
|
|
1590
|
+
} finally {
|
|
1591
|
+
rl.off("close", onClose);
|
|
1592
|
+
rl.close();
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
function abortIsCancel(err) {
|
|
1596
|
+
if (err.code === "ABORT_ERR") throw cancelled();
|
|
1597
|
+
throw err;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// src/pick.ts
|
|
1601
|
+
function askOrUsage(usage) {
|
|
1602
|
+
if (!interactive()) throw new KvboxError(`${usage} (or run it in a terminal to be asked)`);
|
|
1603
|
+
}
|
|
1604
|
+
function refOf(item, user) {
|
|
1605
|
+
return user !== void 0 && slugOf(item.owner) !== user ? `${slugOf(item.owner)}/${item.name}` : item.name;
|
|
1606
|
+
}
|
|
1607
|
+
async function pickBox(client, message = "Box", extra = []) {
|
|
1608
|
+
const user = me(client);
|
|
1609
|
+
const boxes = await spin("listing your boxes\u2026", client.listMachines());
|
|
1610
|
+
const choices = boxes.map((b) => ({ value: refOf(b, user), label: refOf(b, user), hint: b.status }));
|
|
1611
|
+
if (choices.length === 0 && extra.length === 0) throw new KvboxError("you have no boxes yet: make one with kvbox new");
|
|
1612
|
+
return select(message, [...choices, ...extra]);
|
|
1613
|
+
}
|
|
1614
|
+
async function pickVolume(client, keep, none) {
|
|
1615
|
+
const user = me(client);
|
|
1616
|
+
const volumes = (await spin("listing your volumes\u2026", client.listVolumes())).filter(keep);
|
|
1617
|
+
if (volumes.length === 0) throw new KvboxError(none);
|
|
1618
|
+
const refs = volumes.map((v) => refOf(v, user));
|
|
1619
|
+
const ref = await select(
|
|
1620
|
+
"Volume",
|
|
1621
|
+
volumes.map((v, i) => ({ value: refs[i], label: refs[i], hint: `${v.sizeGib} GB, ${v.attachedTo === null ? "not attached" : `on ${v.attachedTo}`}` }))
|
|
1622
|
+
);
|
|
1623
|
+
return { ...volumes[refs.indexOf(ref)], ref };
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1384
1626
|
// src/commands/vm/new.ts
|
|
1385
1627
|
import { Command, Option } from "commander";
|
|
1386
1628
|
var newCommand = new Command("new").description("boot a new box from the base image or a snapshot").argument("[name]", "box name (box1, box2, \u2026 when omitted)").addOption(new Option("-s, --size <size>", "box size (default: server config, s)").choices(SIZE_NAMES)).option("-f, --from <snapshot>", "start from a snapshot instead of the base image").option("--auto-hibernate <duration>", "hibernate the box after this long without network traffic (30m, 4h, 1d; off: never; default: server config, off)").option("--force", "skip the capacity guard").option("--json", "print the box as JSON").action(async (name, opts, cmd) => {
|
|
@@ -1433,20 +1675,27 @@ function parseIdleTimeout(text2) {
|
|
|
1433
1675
|
|
|
1434
1676
|
// src/commands/vm/connect.ts
|
|
1435
1677
|
var SSH_FAILED = 255;
|
|
1436
|
-
var connectCommand = new Command2("connect").alias("cn").description("open a shell in a box over SSH (or run a command: kvbox connect <name> -- <cmd>)").argument("
|
|
1678
|
+
var connectCommand = new Command2("connect").alias("cn").description("open a shell in a box over SSH (or run a command: kvbox connect <name> -- <cmd>)").argument("[name]", "box name (picked from a list when left out)").argument("[cmd...]", "command to run instead of a shell").option("--direct", "on the box's host: ssh to the box's private ip, not through the public address or relay").passThroughOptions().action(async (nameArg, cmdArgs, opts, cmd) => {
|
|
1679
|
+
if (nameArg === void 0) askOrUsage("give the box: kvbox connect <name>");
|
|
1680
|
+
if (cmdArgs[0] === "--direct") {
|
|
1681
|
+
opts.direct = true;
|
|
1682
|
+
cmdArgs = cmdArgs.slice(cmdArgs[1] === "--" ? 2 : 1);
|
|
1683
|
+
}
|
|
1437
1684
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1438
1685
|
let ep;
|
|
1439
1686
|
let cert;
|
|
1440
1687
|
let owner;
|
|
1441
1688
|
try {
|
|
1689
|
+
const name = nameArg ?? await pickBox(client);
|
|
1442
1690
|
let box = pickRef(await boxesFor(client, name), name, me(client));
|
|
1443
1691
|
if (!box) throw new KvboxError(`box ${name}: not found (kvbox ls)`);
|
|
1444
1692
|
cert = await ensureCert(client, void 0, void 0, { owner: box.owner });
|
|
1445
1693
|
owner = box.owner;
|
|
1446
1694
|
const wasRunning = box.status === "running";
|
|
1447
1695
|
if (!wasRunning) box = await wakeUp(client, box, displayName(box, me(client)));
|
|
1448
|
-
ep = sshEndpoint(box, client.target);
|
|
1696
|
+
ep = sshEndpoint(box, client.target, opts.direct);
|
|
1449
1697
|
if (!wasRunning) await spin(`waiting for ${name} to accept SSH\u2026`, waitForSsh(ep));
|
|
1698
|
+
else if (opts.direct) await checkDirect(ep);
|
|
1450
1699
|
} finally {
|
|
1451
1700
|
await client.close();
|
|
1452
1701
|
}
|
|
@@ -1511,7 +1760,7 @@ async function runSsh(ssh, opts) {
|
|
|
1511
1760
|
}).finally(() => clearTimeout(timer));
|
|
1512
1761
|
return { status: status2, chunks, timedOut };
|
|
1513
1762
|
}
|
|
1514
|
-
var execCommand = new Command3("exec").description("run a command in a box and exit with its status (kvbox exec <name> -- <cmd>)").argument("<name>", "box name").argument("<cmd...>", "command, passed to the box's shell as one string (pipes, &&, cd work)").option("-e, --env <KEY=VALUE>", "set an environment variable for the command (repeatable)", collect).option("--timeout <seconds>", "kill the command after N seconds and exit 124", positiveSeconds).option("--tty", "allocate a pseudo-terminal (for interactive tools like htop)").option("--json", 'capture output and print {"output": "...", "exit_code": N}').action(async (name, cmdArgs, opts, cmd) => {
|
|
1763
|
+
var execCommand = new Command3("exec").description("run a command in a box and exit with its status (kvbox exec <name> -- <cmd>)").argument("<name>", "box name").argument("<cmd...>", "command, passed to the box's shell as one string (pipes, &&, cd work)").option("-e, --env <KEY=VALUE>", "set an environment variable for the command (repeatable)", collect).option("--timeout <seconds>", "kill the command after N seconds and exit 124", positiveSeconds).option("--tty", "allocate a pseudo-terminal (for interactive tools like htop)").option("--json", 'capture output and print {"output": "...", "exit_code": N}').option("--direct", "on the box's host: ssh to the box's private ip, not through the public address or relay").action(async (name, cmdArgs, opts, cmd) => {
|
|
1515
1764
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1516
1765
|
let ep;
|
|
1517
1766
|
let cert;
|
|
@@ -1524,8 +1773,9 @@ var execCommand = new Command3("exec").description("run a command in a box and e
|
|
|
1524
1773
|
owner = box.owner;
|
|
1525
1774
|
const wasRunning = box.status === "running";
|
|
1526
1775
|
if (!wasRunning) box = await wakeUp(client, box, displayName(box, me(client)));
|
|
1527
|
-
ep = sshEndpoint(box, client.target);
|
|
1776
|
+
ep = sshEndpoint(box, client.target, opts.direct);
|
|
1528
1777
|
if (!wasRunning) await spin(`waiting for ${name} to accept SSH\u2026`, waitForSsh(ep));
|
|
1778
|
+
else if (opts.direct) await checkDirect(ep);
|
|
1529
1779
|
} finally {
|
|
1530
1780
|
await client.close();
|
|
1531
1781
|
}
|
|
@@ -1743,6 +1993,7 @@ import { Command as Command9 } from "commander";
|
|
|
1743
1993
|
|
|
1744
1994
|
// ../shared/src/names.ts
|
|
1745
1995
|
import { createHmac } from "crypto";
|
|
1996
|
+
var DEFAULT_PROXY_DOMAIN = "kvbox.app";
|
|
1746
1997
|
var LABEL_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
1747
1998
|
var RESERVED_SUBDOMAINS = /* @__PURE__ */ new Set([
|
|
1748
1999
|
"admin",
|
|
@@ -1791,14 +2042,34 @@ function assertRouteName(text2) {
|
|
|
1791
2042
|
if (problem) throw new KvboxError(`name ${problem}`);
|
|
1792
2043
|
return text2;
|
|
1793
2044
|
}
|
|
2045
|
+
var PLATFORM_DOMAINS = ["kvbox.sh", DEFAULT_PROXY_DOMAIN];
|
|
2046
|
+
function domainProblem(text2, proxyDomain) {
|
|
2047
|
+
if (text2.length > 253) return `"${text2}" is longer than a domain may be`;
|
|
2048
|
+
const labels = text2.split(".");
|
|
2049
|
+
if (labels.length < 2 || labels.some((l) => l.length > 63 || !LABEL_RE.test(l))) return `"${text2}" is not a domain like example.com or www.example.com (lowercase, no "*", no "https://")`;
|
|
2050
|
+
if (!/^[a-z]{2,}$/.test(labels.at(-1)) && !labels.at(-1).startsWith("xn--")) return `"${text2}" does not end in a top-level domain`;
|
|
2051
|
+
const ours = [...PLATFORM_DOMAINS, ...proxyDomain ? [proxyDomain] : []];
|
|
2052
|
+
if (ours.some((d) => text2 === d || text2.endsWith(`.${d}`))) return `"${text2}" belongs to kvbox; bring a domain of your own`;
|
|
2053
|
+
return null;
|
|
2054
|
+
}
|
|
2055
|
+
function assertDomain(text2, proxyDomain) {
|
|
2056
|
+
const problem = domainProblem(text2, proxyDomain);
|
|
2057
|
+
if (problem) throw new KvboxError(`domain ${problem}`);
|
|
2058
|
+
return text2;
|
|
2059
|
+
}
|
|
1794
2060
|
|
|
1795
2061
|
// src/commands/vm/proxy/claim.ts
|
|
1796
|
-
var claimCommand = new Command9("claim").description("give a box its HTTPS address, https://<subdomain>.kvbox.app, serving one of its ports").argument("
|
|
1797
|
-
|
|
1798
|
-
|
|
2062
|
+
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 (picked from a list when left out)").argument("[subdomain]", "3-63 lowercase letters, digits and '-'; must be free (asked for when left out)").option("--port <port>", "the box port the address serves (asked for when left out)").option("--json", "print the subdomain as JSON").action(async (nameArg, subArg, opts, cmd) => {
|
|
2063
|
+
if (nameArg === void 0 || subArg === void 0 || opts.port === void 0) askOrUsage("give the box, the subdomain and the port: kvbox vm proxy claim <box> <subdomain> --port <port>");
|
|
2064
|
+
if (subArg !== void 0) assertSubdomain(subArg);
|
|
2065
|
+
if (opts.port !== void 0) parsePorts([opts.port]);
|
|
1799
2066
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1800
2067
|
try {
|
|
1801
2068
|
requireCloud(client);
|
|
2069
|
+
const name = nameArg ?? await pickBox(client);
|
|
2070
|
+
const subdomain = subArg ?? await text("Subdomain", { validate: (a) => subdomainProblem(a) ?? void 0 });
|
|
2071
|
+
const portText = opts.port ?? await text("Port inside the box", { default: "3000", validate: (a) => /^\d+$/.test(a) && +a >= 1 && +a <= 65535 ? void 0 : "a port is 1-65535" });
|
|
2072
|
+
const [port] = parsePorts([portText]);
|
|
1802
2073
|
const user = me(client);
|
|
1803
2074
|
const found = await findBox(client, name);
|
|
1804
2075
|
const box = displayName(found, user);
|
|
@@ -2438,175 +2709,6 @@ import { hostname as osHostname } from "os";
|
|
|
2438
2709
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
2439
2710
|
import { Writable } from "stream";
|
|
2440
2711
|
import { Command as Command35 } from "commander";
|
|
2441
|
-
|
|
2442
|
-
// src/prompt.ts
|
|
2443
|
-
import { createInterface } from "readline/promises";
|
|
2444
|
-
function interactive() {
|
|
2445
|
-
return process.stdin.isTTY === true && process.stderr.isTTY === true;
|
|
2446
|
-
}
|
|
2447
|
-
var ESC = "\x1B";
|
|
2448
|
-
var ENTER = "\r";
|
|
2449
|
-
var CTRL_C = "";
|
|
2450
|
-
var CTRL_D = "";
|
|
2451
|
-
var cancelled = () => new KvboxError("cancelled", 130);
|
|
2452
|
-
function keysIn(chunk) {
|
|
2453
|
-
const keys = [];
|
|
2454
|
-
for (let i = 0; i < chunk.length; ) {
|
|
2455
|
-
if (chunk[i] === ESC && chunk[i + 1] === "[" && i + 2 < chunk.length) {
|
|
2456
|
-
let j = i + 2;
|
|
2457
|
-
while (j < chunk.length && /[0-9;?]/.test(chunk[j])) j += 1;
|
|
2458
|
-
const end = j < chunk.length ? j + 1 : j;
|
|
2459
|
-
keys.push(chunk.slice(i, end));
|
|
2460
|
-
i = end;
|
|
2461
|
-
} else if (chunk[i] === ESC && chunk[i + 1] === "O" && i + 2 < chunk.length) {
|
|
2462
|
-
keys.push(chunk.slice(i, i + 3));
|
|
2463
|
-
i += 3;
|
|
2464
|
-
} else {
|
|
2465
|
-
keys.push(chunk[i]);
|
|
2466
|
-
i += 1;
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
|
-
return keys;
|
|
2470
|
-
}
|
|
2471
|
-
var isUp = (k) => k === `${ESC}[A` || k === `${ESC}OA` || k === "k";
|
|
2472
|
-
var isDown = (k) => k === `${ESC}[B` || k === `${ESC}OB` || k === "j";
|
|
2473
|
-
var ANSI2 = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
2474
|
-
function rows(line, columns = process.stderr.columns ?? 80) {
|
|
2475
|
-
const width2 = line.replace(ANSI2, "").length;
|
|
2476
|
-
return Math.max(1, Math.ceil(width2 / Math.max(1, columns)));
|
|
2477
|
-
}
|
|
2478
|
-
function settled(message, answer) {
|
|
2479
|
-
return `${color.green("\u2713")} ${color.bold(message)} ${color.cyan(answer)}`;
|
|
2480
|
-
}
|
|
2481
|
-
async function select(message, choices, initial = 0) {
|
|
2482
|
-
const out = process.stderr;
|
|
2483
|
-
const inp = process.stdin;
|
|
2484
|
-
let index = Math.min(Math.max(initial, 0), choices.length - 1);
|
|
2485
|
-
let drawn = 0;
|
|
2486
|
-
const render = () => {
|
|
2487
|
-
if (drawn > 0) out.write(`${ESC}[${drawn}A`);
|
|
2488
|
-
const lines = [
|
|
2489
|
-
`${color.green("?")} ${color.bold(message)} ${color.dim("\u2191\u2193 to move, enter to pick")}`,
|
|
2490
|
-
...choices.map((c, i) => {
|
|
2491
|
-
const hint = c.hint ? color.dim(` ${c.hint}`) : "";
|
|
2492
|
-
return i === index ? `${color.cyan(`\u276F ${c.label}`)}${hint}` : ` ${c.label}${hint}`;
|
|
2493
|
-
})
|
|
2494
|
-
];
|
|
2495
|
-
out.write(`\r${ESC}[J${lines.join("\n")}
|
|
2496
|
-
`);
|
|
2497
|
-
drawn = lines.reduce((n, l) => n + rows(l), 0);
|
|
2498
|
-
};
|
|
2499
|
-
inp.setRawMode(true);
|
|
2500
|
-
inp.resume();
|
|
2501
|
-
inp.setEncoding("utf8");
|
|
2502
|
-
render();
|
|
2503
|
-
let done = false;
|
|
2504
|
-
try {
|
|
2505
|
-
return await new Promise((resolve3, reject) => {
|
|
2506
|
-
const onData = (chunk) => {
|
|
2507
|
-
for (const key of keysIn(chunk)) {
|
|
2508
|
-
if (isUp(key)) index = (index - 1 + choices.length) % choices.length;
|
|
2509
|
-
else if (isDown(key)) index = (index + 1) % choices.length;
|
|
2510
|
-
else if (key === ENTER || key === "\n") {
|
|
2511
|
-
inp.off("data", onData);
|
|
2512
|
-
done = true;
|
|
2513
|
-
resolve3(choices[index].value);
|
|
2514
|
-
return;
|
|
2515
|
-
} else if (key === CTRL_C || key === CTRL_D) {
|
|
2516
|
-
inp.off("data", onData);
|
|
2517
|
-
reject(cancelled());
|
|
2518
|
-
return;
|
|
2519
|
-
} else continue;
|
|
2520
|
-
render();
|
|
2521
|
-
}
|
|
2522
|
-
};
|
|
2523
|
-
inp.on("data", onData);
|
|
2524
|
-
});
|
|
2525
|
-
} finally {
|
|
2526
|
-
inp.setRawMode(false);
|
|
2527
|
-
inp.pause();
|
|
2528
|
-
out.write(`${ESC}[${drawn}A\r${ESC}[J${done ? `${settled(message, choices[index].label)}
|
|
2529
|
-
` : ""}`);
|
|
2530
|
-
}
|
|
2531
|
-
}
|
|
2532
|
-
async function text(message, opts = {}) {
|
|
2533
|
-
const out = process.stderr;
|
|
2534
|
-
for (; ; ) {
|
|
2535
|
-
const hint = opts.default !== void 0 ? ` ${color.dim(`(${opts.default})`)}` : "";
|
|
2536
|
-
const question = `${color.green("?")} ${color.bold(message)}${hint} ${color.dim("\u203A")} `;
|
|
2537
|
-
const rl = createInterface({ input: process.stdin, output: out });
|
|
2538
|
-
const line = await askLine(rl, question);
|
|
2539
|
-
const raw = opts.trim === false ? line : line.trim();
|
|
2540
|
-
const answer = raw === "" && opts.default !== void 0 ? opts.default : raw;
|
|
2541
|
-
const problem = opts.validate?.(answer);
|
|
2542
|
-
out.write(`${ESC}[${rows(question + raw)}A\r${ESC}[J`);
|
|
2543
|
-
if (problem === void 0) {
|
|
2544
|
-
out.write(`${settled(message, answer)}
|
|
2545
|
-
`);
|
|
2546
|
-
return answer;
|
|
2547
|
-
}
|
|
2548
|
-
out.write(`${color.yellow("!")} ${problem}
|
|
2549
|
-
`);
|
|
2550
|
-
}
|
|
2551
|
-
}
|
|
2552
|
-
async function hiddenText(message) {
|
|
2553
|
-
const out = process.stderr;
|
|
2554
|
-
const inp = process.stdin;
|
|
2555
|
-
const question = `${color.green("?")} ${color.bold(message)} ${color.dim("(hidden) \u203A")} `;
|
|
2556
|
-
out.write(question);
|
|
2557
|
-
inp.setRawMode(true);
|
|
2558
|
-
inp.resume();
|
|
2559
|
-
inp.setEncoding("utf8");
|
|
2560
|
-
let chars = [];
|
|
2561
|
-
let done = false;
|
|
2562
|
-
try {
|
|
2563
|
-
return await new Promise((resolve3, reject) => {
|
|
2564
|
-
const onData = (chunk) => {
|
|
2565
|
-
for (const key of keysIn(chunk)) {
|
|
2566
|
-
if (key === ENTER || key === "\n") {
|
|
2567
|
-
inp.off("data", onData);
|
|
2568
|
-
done = true;
|
|
2569
|
-
resolve3(chars.join(""));
|
|
2570
|
-
return;
|
|
2571
|
-
}
|
|
2572
|
-
if (key === CTRL_C || key === CTRL_D) {
|
|
2573
|
-
inp.off("data", onData);
|
|
2574
|
-
reject(cancelled());
|
|
2575
|
-
return;
|
|
2576
|
-
}
|
|
2577
|
-
if (key === "\x7F" || key === "\b") chars = chars.slice(0, -1);
|
|
2578
|
-
else if (key.length === 1 && (key >= " " || key === " ")) chars.push(key);
|
|
2579
|
-
}
|
|
2580
|
-
};
|
|
2581
|
-
inp.on("data", onData);
|
|
2582
|
-
});
|
|
2583
|
-
} finally {
|
|
2584
|
-
inp.setRawMode(false);
|
|
2585
|
-
inp.pause();
|
|
2586
|
-
out.write(`\r${ESC}[J${done ? `${settled(message, "\u2022\u2022\u2022\u2022\u2022")}
|
|
2587
|
-
` : "\n"}`);
|
|
2588
|
-
}
|
|
2589
|
-
}
|
|
2590
|
-
async function askLine(rl, question) {
|
|
2591
|
-
let onClose = () => {
|
|
2592
|
-
};
|
|
2593
|
-
const closed = new Promise((_, reject) => {
|
|
2594
|
-
onClose = () => reject(cancelled());
|
|
2595
|
-
rl.once("close", onClose);
|
|
2596
|
-
});
|
|
2597
|
-
try {
|
|
2598
|
-
return await Promise.race([rl.question(question).catch(abortIsCancel), closed]);
|
|
2599
|
-
} finally {
|
|
2600
|
-
rl.off("close", onClose);
|
|
2601
|
-
rl.close();
|
|
2602
|
-
}
|
|
2603
|
-
}
|
|
2604
|
-
function abortIsCancel(err) {
|
|
2605
|
-
if (err.code === "ABORT_ERR") throw cancelled();
|
|
2606
|
-
throw err;
|
|
2607
|
-
}
|
|
2608
|
-
|
|
2609
|
-
// src/commands/login.ts
|
|
2610
2712
|
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) => {
|
|
2611
2713
|
const url = normalizeUrl(urlArg ?? DEFAULT_CLOUD_URL);
|
|
2612
2714
|
const creds = await readCredentials(opts);
|
|
@@ -3643,9 +3745,12 @@ function describeMount(machine, volume, box) {
|
|
|
3643
3745
|
const path = machine.volumes?.find((v) => volume === v.name || volume.endsWith(`/${v.name}`))?.mountPath ?? volumeMountPath(volume.slice(volume.lastIndexOf("/") + 1));
|
|
3644
3746
|
return 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`;
|
|
3645
3747
|
}
|
|
3646
|
-
var attachCommand = new Command51("attach").description("attach a volume to a box at /mnt/<volume> (a running box is restarted to take it)").argument("
|
|
3748
|
+
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 (picked from a list when left out)").argument("[box]", "box name (picked from a list when left out)").option("--json", "print the box as JSON").action(async (volumeArg, boxArg, opts, cmd) => {
|
|
3749
|
+
if (volumeArg === void 0 || boxArg === void 0) askOrUsage("give the volume and the box: kvbox volume attach <volume> <box>");
|
|
3647
3750
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
3648
3751
|
try {
|
|
3752
|
+
const volume = volumeArg ?? (await pickVolume(client, (v) => v.attachedTo === null, "no volume is free to attach (kvbox volume ls); make one with kvbox volume new")).ref;
|
|
3753
|
+
const box = boxArg ?? await pickBox(client);
|
|
3649
3754
|
const machine = await spin(`attaching volume ${volume} to box ${box} (a running box is restarted)\u2026`, client.attachVolume(volume, { box }));
|
|
3650
3755
|
if (opts.json) {
|
|
3651
3756
|
json(machine);
|
|
@@ -3667,10 +3772,26 @@ function parseGib2(text2) {
|
|
|
3667
3772
|
if (gib > MAX_VOLUME_GIB) throw new KvboxError(`--size ${text2}: a volume is at most ${MAX_VOLUME_GIB} GB`);
|
|
3668
3773
|
return gib;
|
|
3669
3774
|
}
|
|
3670
|
-
|
|
3671
|
-
|
|
3775
|
+
function sizeProblem(answer) {
|
|
3776
|
+
try {
|
|
3777
|
+
parseGib2(answer);
|
|
3778
|
+
return void 0;
|
|
3779
|
+
} catch (err) {
|
|
3780
|
+
return err instanceof KvboxError ? err.message.replace(/^--size \S+: /, "") : String(err);
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
var createCommand = new Command52("create").alias("new").description("create a volume for a box: an empty disk of its own at /mnt/<name> inside it (a running box is restarted)").argument("[name]", "volume name (1-12 chars; the box mounts it at /mnt/<name>; asked for when left out)").option("--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 and attached to it (required on kvbox.sh; optional with --local)").option("--json", "print the volume as JSON").action(async (nameArg, opts, cmd) => {
|
|
3784
|
+
const ask = interactive();
|
|
3785
|
+
if (nameArg === void 0 || opts.size === void 0) askOrUsage("give the name and the size: kvbox volume create <name> --size <gb> --box <box>");
|
|
3786
|
+
const name = nameArg ?? await text("Volume name", { validate: (a) => VOLUME_NAME_RE.test(a) ? void 0 : VOLUME_NAME_RULE });
|
|
3787
|
+
const sizeGib = opts.size !== void 0 ? parseGib2(opts.size) : parseGib2(await text("Size in GB", { default: "10", validate: sizeProblem }));
|
|
3672
3788
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
3673
3789
|
try {
|
|
3790
|
+
if (opts.box === void 0 && ask) {
|
|
3791
|
+
const none = client.target.kind === "cloud" ? [] : [{ value: "", label: "none", hint: "attach it later with kvbox volume attach" }];
|
|
3792
|
+
const picked = await pickBox(client, "Box", none);
|
|
3793
|
+
if (picked !== "") opts.box = picked;
|
|
3794
|
+
}
|
|
3674
3795
|
if (opts.box === void 0 && client.target.kind === "cloud") {
|
|
3675
3796
|
throw new KvboxError(`a volume is made for a box, on that box's host: kvbox volume create ${name} --size ${sizeGib} --box <box>`);
|
|
3676
3797
|
}
|
|
@@ -3693,9 +3814,19 @@ var createCommand = new Command52("create").alias("new").description("create a v
|
|
|
3693
3814
|
|
|
3694
3815
|
// src/commands/volume/detach.ts
|
|
3695
3816
|
import { Command as Command53 } from "commander";
|
|
3696
|
-
var detachCommand = new Command53("detach").description("detach a volume from a box; its data is kept (a running box is restarted)").argument("
|
|
3817
|
+
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 (picked from a list when left out)").argument("[box]", "box name (the one the volume is on when left out)").option("--json", "print the box as JSON").action(async (volumeArg, boxArg, opts, cmd) => {
|
|
3818
|
+
if (volumeArg === void 0) askOrUsage("give the volume: kvbox volume detach <volume> [box]");
|
|
3697
3819
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
3698
3820
|
try {
|
|
3821
|
+
let volume = volumeArg;
|
|
3822
|
+
let box = boxArg;
|
|
3823
|
+
if (volume === void 0 || box === void 0) {
|
|
3824
|
+
const found = volume === void 0 ? await pickVolume(client, (v) => v.attachedTo !== null, "no volume is attached to a box (kvbox volume ls)") : pickRef(await spin("finding the volume\u2026", client.listVolumes()), volume, me(client), "volume");
|
|
3825
|
+
if (!found) throw new KvboxError(`volume ${volume}: not found (kvbox volume ls)`);
|
|
3826
|
+
if (found.attachedTo === null) throw new KvboxError(`volume ${volume}: not attached to a box`);
|
|
3827
|
+
volume ??= refOf(found, me(client));
|
|
3828
|
+
box ??= refOf({ name: found.attachedTo, owner: found.owner }, me(client));
|
|
3829
|
+
}
|
|
3699
3830
|
const machine = await spin(`detaching volume ${volume} from box ${box} (a running box is restarted)\u2026`, client.detachVolume(volume, { box }));
|
|
3700
3831
|
if (opts.json) {
|
|
3701
3832
|
json(machine);
|
|
@@ -3875,13 +4006,120 @@ var setCommand2 = new Command59("set").description("add or change a variable; bo
|
|
|
3875
4006
|
var envCommand = new Command60("env").description("environment variables every box of yours gets when it starts (secrets too)").addCommand(lsCommand7, { isDefault: true }).addCommand(setCommand2).addCommand(rmCommand7);
|
|
3876
4007
|
for (const cmd of envCommand.commands) cmd.option("--org <slug>", "the org's variables instead of yours");
|
|
3877
4008
|
|
|
4009
|
+
// src/commands/domain/index.ts
|
|
4010
|
+
import { Command as Command64 } from "commander";
|
|
4011
|
+
|
|
4012
|
+
// src/commands/domain/add.ts
|
|
4013
|
+
import { Command as Command61 } from "commander";
|
|
4014
|
+
|
|
4015
|
+
// src/commands/domain/shared.ts
|
|
4016
|
+
function requireCloud3(client) {
|
|
4017
|
+
if (client.target.kind !== "cloud") throw new KvboxError("custom domains need the kvbox cloud: kvbox login (this host's own socket has none)");
|
|
4018
|
+
}
|
|
4019
|
+
var boxOf2 = (d, user) => user !== void 0 && d.owner !== user ? `${d.owner}/${d.box}` : d.box;
|
|
4020
|
+
function domainJson(d, user) {
|
|
4021
|
+
return { domain: d.domain, box: boxOf2(d, user), port: d.port, status: d.status, www: d.www, url: d.url, error: d.error, records: d.records, createdAt: d.createdAt };
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
// src/commands/domain/add.ts
|
|
4025
|
+
var addCommand3 = new Command61("add").description("serve a box at a domain of your own, https://<domain>, with its certificate made for you").argument("<domain>", "a domain you control, like example.com or app.example.com").argument("<box>", "box name").option("--port <port>", "the box port the domain serves (default: the port of its kvbox.app address)").option("--json", "print the domain as JSON").action(async (domainArg, name, opts, cmd) => {
|
|
4026
|
+
const domain = assertDomain(domainArg.trim().toLowerCase());
|
|
4027
|
+
const port = opts.port === void 0 ? void 0 : parsePorts([opts.port])[0];
|
|
4028
|
+
const client = await clientFor(cmd.optsWithGlobals());
|
|
4029
|
+
try {
|
|
4030
|
+
requireCloud3(client);
|
|
4031
|
+
const user = me(client);
|
|
4032
|
+
const box = displayName(await findBox(client, name), user);
|
|
4033
|
+
const d = await spin(`adding ${domain}\u2026`, client.addDomain(box, { domain, ...port !== void 0 ? { port } : {} }));
|
|
4034
|
+
if (opts.json) {
|
|
4035
|
+
json(domainJson(d, user));
|
|
4036
|
+
return;
|
|
4037
|
+
}
|
|
4038
|
+
if (d.status === "live") {
|
|
4039
|
+
info(`${color.green("\u2713")} ${color.bold(d.url)} \u2192 box ${color.bold(box)} port ${d.port}`);
|
|
4040
|
+
info(" the certificate is made on the first visit, which can take a few seconds");
|
|
4041
|
+
return;
|
|
4042
|
+
}
|
|
4043
|
+
info(`${color.green("\u2713")} ${domain} added for box ${color.bold(box)} port ${d.port}, waiting for its DNS`);
|
|
4044
|
+
info(`
|
|
4045
|
+
At the company that runs the DNS of ${domain}, add:
|
|
4046
|
+
`);
|
|
4047
|
+
table(d.records.map((r) => ({ type: r.type, name: r.name, value: r.value })));
|
|
4048
|
+
info(`
|
|
4049
|
+
(the TXT record shows ${domain} is yours; keep it, it is checked again if the domain ever goes back to pending)`);
|
|
4050
|
+
if (d.records.some((r) => r.type === "CNAME")) info(` (the www record is optional: with it, www.${domain} serves the same site)`);
|
|
4051
|
+
info(`
|
|
4052
|
+
kvbox checks every minute; ${domain} is served at ${d.url} once its records are set (kvbox domain ls).`);
|
|
4053
|
+
if (d.error) info(color.dim(` now: ${d.error}`));
|
|
4054
|
+
} finally {
|
|
4055
|
+
await client.close();
|
|
4056
|
+
}
|
|
4057
|
+
});
|
|
4058
|
+
|
|
4059
|
+
// src/commands/domain/ls.ts
|
|
4060
|
+
import { Command as Command62 } from "commander";
|
|
4061
|
+
var lsCommand8 = new Command62("ls").alias("list").description("list your custom domains and whether they are live").option("--json", "print as JSON").action(async (opts, cmd) => {
|
|
4062
|
+
const client = await clientFor(cmd.optsWithGlobals());
|
|
4063
|
+
try {
|
|
4064
|
+
requireCloud3(client);
|
|
4065
|
+
const user = me(client);
|
|
4066
|
+
const domains = await client.listDomains();
|
|
4067
|
+
if (opts.json) {
|
|
4068
|
+
json(domains.map((d) => domainJson(d, user)));
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
4071
|
+
if (domains.length === 0) {
|
|
4072
|
+
info("no custom domains (kvbox domain add <domain> <box>)");
|
|
4073
|
+
return;
|
|
4074
|
+
}
|
|
4075
|
+
table(
|
|
4076
|
+
domains.map((d) => ({
|
|
4077
|
+
domain: d.www ? `${d.domain} (+www)` : d.domain,
|
|
4078
|
+
box: boxOf2(d, user),
|
|
4079
|
+
port: d.port,
|
|
4080
|
+
status: d.status === "live" ? "live" : "pending dns",
|
|
4081
|
+
note: d.status === "live" ? d.url : d.error ?? ""
|
|
4082
|
+
}))
|
|
4083
|
+
);
|
|
4084
|
+
} finally {
|
|
4085
|
+
await client.close();
|
|
4086
|
+
}
|
|
4087
|
+
});
|
|
4088
|
+
|
|
4089
|
+
// src/commands/domain/rm.ts
|
|
4090
|
+
import { Command as Command63 } from "commander";
|
|
4091
|
+
var rmCommand8 = new Command63("rm").alias("remove").description("stop serving a box at a custom domain").argument("<domain>", "the domain").option("-y, --yes", "do not ask for confirmation").action(async (domainArg, opts, cmd) => {
|
|
4092
|
+
const domain = domainArg.trim().toLowerCase();
|
|
4093
|
+
const client = await clientFor(cmd.optsWithGlobals());
|
|
4094
|
+
try {
|
|
4095
|
+
requireCloud3(client);
|
|
4096
|
+
const found = (await client.listDomains()).find((d) => d.domain === domain);
|
|
4097
|
+
if (!found) throw new KvboxError(`domain ${domain}: not found (kvbox domain ls)`);
|
|
4098
|
+
const box = boxOf2(found, me(client));
|
|
4099
|
+
if (!await confirm(`Stop serving box ${box} at ${domain}?`, opts.yes ?? false)) {
|
|
4100
|
+
info("cancelled");
|
|
4101
|
+
process.exitCode = 1;
|
|
4102
|
+
return;
|
|
4103
|
+
}
|
|
4104
|
+
await spin(`removing ${domain}\u2026`, client.removeDomain(`${found.owner}/${found.box}`, domain));
|
|
4105
|
+
info(`${color.green("\u2713")} ${domain} no longer serves box ${box} (you can remove its DNS record)`);
|
|
4106
|
+
} finally {
|
|
4107
|
+
await client.close();
|
|
4108
|
+
}
|
|
4109
|
+
});
|
|
4110
|
+
|
|
4111
|
+
// src/commands/domain/index.ts
|
|
4112
|
+
var domainCommand = new Command64("domain").alias("domains").description("serve a box at a domain of your own").addCommand(addCommand3).addCommand(lsCommand8).addCommand(rmCommand8);
|
|
4113
|
+
for (const cmd of domainCommand.commands) cmd.option("--org <slug>", "act in this org (- for your personal boxes)");
|
|
4114
|
+
|
|
3878
4115
|
// src/program.ts
|
|
3879
4116
|
function buildProgram() {
|
|
3880
|
-
const program = new
|
|
4117
|
+
const program = new Command65("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();
|
|
3881
4118
|
program.addCommand(vmCommand);
|
|
3882
4119
|
program.addCommand(snapshotsCommand);
|
|
3883
4120
|
program.addCommand(volumeCommand);
|
|
3884
4121
|
program.addCommand(envCommand);
|
|
4122
|
+
program.addCommand(domainCommand);
|
|
3885
4123
|
program.addCommand(serverCommand);
|
|
3886
4124
|
program.addCommand(loginCommand);
|
|
3887
4125
|
program.addCommand(logoutCommand);
|
|
@@ -3894,9 +4132,90 @@ function buildProgram() {
|
|
|
3894
4132
|
return program;
|
|
3895
4133
|
}
|
|
3896
4134
|
|
|
4135
|
+
// src/update.ts
|
|
4136
|
+
import { mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
4137
|
+
import { dirname } from "path";
|
|
4138
|
+
var REGISTRY_URL = "https://registry.npmjs.org/@kvbox/cli/latest";
|
|
4139
|
+
var CHECK_EVERY_MS = 24 * 60 * 60 * 1e3;
|
|
4140
|
+
var FETCH_TIMEOUT_MS = 1500;
|
|
4141
|
+
var UPDATE_COMMAND = "npm install -g @kvbox/cli";
|
|
4142
|
+
function isNewer(a, b) {
|
|
4143
|
+
const pa = parse(a);
|
|
4144
|
+
const pb = parse(b);
|
|
4145
|
+
if (!pa || !pb) return false;
|
|
4146
|
+
for (let i = 0; i < 3; i++) {
|
|
4147
|
+
if (pa[i] !== pb[i]) return pa[i] > pb[i];
|
|
4148
|
+
}
|
|
4149
|
+
return false;
|
|
4150
|
+
}
|
|
4151
|
+
function parse(v) {
|
|
4152
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
|
|
4153
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
4154
|
+
}
|
|
4155
|
+
function readUpdateState(file = paths().updateCheck) {
|
|
4156
|
+
try {
|
|
4157
|
+
const raw = JSON.parse(readFileSync3(file, "utf8"));
|
|
4158
|
+
if (typeof raw.checkedAt !== "number" || typeof raw.latest !== "string") return null;
|
|
4159
|
+
return { checkedAt: raw.checkedAt, latest: raw.latest };
|
|
4160
|
+
} catch {
|
|
4161
|
+
return null;
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
function writeUpdateState(state, file = paths().updateCheck) {
|
|
4165
|
+
try {
|
|
4166
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
4167
|
+
writeFileSync(file, JSON.stringify(state) + "\n");
|
|
4168
|
+
} catch {
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
async function fetchLatest(fetchImpl = fetch) {
|
|
4172
|
+
try {
|
|
4173
|
+
const res = await fetchImpl(REGISTRY_URL, {
|
|
4174
|
+
headers: { accept: "application/json" },
|
|
4175
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
4176
|
+
});
|
|
4177
|
+
if (!res.ok) return null;
|
|
4178
|
+
const body = await res.json();
|
|
4179
|
+
return typeof body.version === "string" ? body.version : null;
|
|
4180
|
+
} catch {
|
|
4181
|
+
return null;
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
async function refreshUpdateState(opts = {}) {
|
|
4185
|
+
const file = opts.file ?? paths().updateCheck;
|
|
4186
|
+
const now = opts.now ?? Date.now();
|
|
4187
|
+
const cached = readUpdateState(file);
|
|
4188
|
+
if (cached && now - cached.checkedAt < CHECK_EVERY_MS) return cached;
|
|
4189
|
+
const latest = await fetchLatest(opts.fetchImpl);
|
|
4190
|
+
const state = { checkedAt: now, latest: latest ?? cached?.latest ?? VERSION };
|
|
4191
|
+
writeUpdateState(state, file);
|
|
4192
|
+
return state;
|
|
4193
|
+
}
|
|
4194
|
+
function updateNotice(latest, current = VERSION) {
|
|
4195
|
+
if (!isNewer(latest, current)) return null;
|
|
4196
|
+
return `
|
|
4197
|
+
A new version of kvbox is available (v${latest.replace(/^v/, "")}). Update with:
|
|
4198
|
+
${color.bold(UPDATE_COMMAND)}
|
|
4199
|
+
`;
|
|
4200
|
+
}
|
|
4201
|
+
function enabled() {
|
|
4202
|
+
return process.stderr.isTTY === true && !process.env["CI"] && !process.env["KVBOX_NO_UPDATE_CHECK"];
|
|
4203
|
+
}
|
|
4204
|
+
function startUpdateCheck() {
|
|
4205
|
+
if (!enabled()) return Promise.resolve();
|
|
4206
|
+
process.on("exit", () => {
|
|
4207
|
+
const state = readUpdateState();
|
|
4208
|
+
const notice = state ? updateNotice(state.latest) : null;
|
|
4209
|
+
if (notice) process.stderr.write(notice);
|
|
4210
|
+
});
|
|
4211
|
+
return refreshUpdateState();
|
|
4212
|
+
}
|
|
4213
|
+
|
|
3897
4214
|
// src/index.ts
|
|
4215
|
+
var updateCheck = startUpdateCheck();
|
|
3898
4216
|
try {
|
|
3899
4217
|
await buildProgram().parseAsync(process.argv);
|
|
4218
|
+
await updateCheck;
|
|
3900
4219
|
} catch (err) {
|
|
3901
4220
|
if (err instanceof KvboxError) {
|
|
3902
4221
|
process.stderr.write(color.red(`error: ${err.message}`) + "\n");
|