@kvbox/cli 0.5.26 → 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 +167 -9
- 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";
|
|
@@ -442,6 +442,19 @@ var SubdomainSchema = z2.object({
|
|
|
442
442
|
url: z2.string(),
|
|
443
443
|
routes: z2.array(z2.object({ name: z2.string(), port: BoxPortSchema, url: z2.string() }))
|
|
444
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
|
+
});
|
|
445
458
|
var MachineSchema = z2.object({
|
|
446
459
|
/** Host-unique short id (the tap is `kv-<id>`). Missing only on a box made before ids that has not restarted since. */
|
|
447
460
|
id: z2.string(),
|
|
@@ -1145,6 +1158,17 @@ var ServerClient = class {
|
|
|
1145
1158
|
listSubdomains() {
|
|
1146
1159
|
return this.call("GET", "/subdomains", z3.array(SubdomainSchema));
|
|
1147
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
|
+
}
|
|
1148
1172
|
/** A running box's own load, memory and disk, asked of its guest (#67). */
|
|
1149
1173
|
machineStats(name) {
|
|
1150
1174
|
return this.call("GET", `/machines/${encodeURIComponent(name)}/stats`, MachineStatsSchema);
|
|
@@ -1294,8 +1318,8 @@ var me = (client) => client.target.kind === "cloud" ? client.target.user : void
|
|
|
1294
1318
|
async function clientFor(opts, client = {}) {
|
|
1295
1319
|
return new ServerClient(await resolveTarget(opts), client);
|
|
1296
1320
|
}
|
|
1297
|
-
function sshEndpoint(box, target) {
|
|
1298
|
-
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 };
|
|
1299
1323
|
if (box.ssh === void 0) throw new KvboxError(`box ${box.name} has no public SSH port`);
|
|
1300
1324
|
return { ...box.ssh, alias: `${box.id}.${box.ssh.host}` };
|
|
1301
1325
|
}
|
|
@@ -1338,6 +1362,11 @@ async function waitForSsh(ep, timeoutMs = 3e4) {
|
|
|
1338
1362
|
}
|
|
1339
1363
|
throw new KvboxError(`box at ${ep.host}:${ep.port} did not answer within ${Math.round(timeoutMs / 1e3)}s`);
|
|
1340
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
|
+
}
|
|
1341
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";
|
|
1342
1371
|
async function lastErrorLine(logPath, from = 0) {
|
|
1343
1372
|
const { readFile: readFile8 } = await import("fs/promises");
|
|
@@ -1646,8 +1675,12 @@ function parseIdleTimeout(text2) {
|
|
|
1646
1675
|
|
|
1647
1676
|
// src/commands/vm/connect.ts
|
|
1648
1677
|
var SSH_FAILED = 255;
|
|
1649
|
-
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").passThroughOptions().action(async (nameArg, cmdArgs,
|
|
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) => {
|
|
1650
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
|
+
}
|
|
1651
1684
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1652
1685
|
let ep;
|
|
1653
1686
|
let cert;
|
|
@@ -1660,8 +1693,9 @@ var connectCommand = new Command2("connect").alias("cn").description("open a she
|
|
|
1660
1693
|
owner = box.owner;
|
|
1661
1694
|
const wasRunning = box.status === "running";
|
|
1662
1695
|
if (!wasRunning) box = await wakeUp(client, box, displayName(box, me(client)));
|
|
1663
|
-
ep = sshEndpoint(box, client.target);
|
|
1696
|
+
ep = sshEndpoint(box, client.target, opts.direct);
|
|
1664
1697
|
if (!wasRunning) await spin(`waiting for ${name} to accept SSH\u2026`, waitForSsh(ep));
|
|
1698
|
+
else if (opts.direct) await checkDirect(ep);
|
|
1665
1699
|
} finally {
|
|
1666
1700
|
await client.close();
|
|
1667
1701
|
}
|
|
@@ -1726,7 +1760,7 @@ async function runSsh(ssh, opts) {
|
|
|
1726
1760
|
}).finally(() => clearTimeout(timer));
|
|
1727
1761
|
return { status: status2, chunks, timedOut };
|
|
1728
1762
|
}
|
|
1729
|
-
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) => {
|
|
1730
1764
|
const client = await clientFor(cmd.optsWithGlobals());
|
|
1731
1765
|
let ep;
|
|
1732
1766
|
let cert;
|
|
@@ -1739,8 +1773,9 @@ var execCommand = new Command3("exec").description("run a command in a box and e
|
|
|
1739
1773
|
owner = box.owner;
|
|
1740
1774
|
const wasRunning = box.status === "running";
|
|
1741
1775
|
if (!wasRunning) box = await wakeUp(client, box, displayName(box, me(client)));
|
|
1742
|
-
ep = sshEndpoint(box, client.target);
|
|
1776
|
+
ep = sshEndpoint(box, client.target, opts.direct);
|
|
1743
1777
|
if (!wasRunning) await spin(`waiting for ${name} to accept SSH\u2026`, waitForSsh(ep));
|
|
1778
|
+
else if (opts.direct) await checkDirect(ep);
|
|
1744
1779
|
} finally {
|
|
1745
1780
|
await client.close();
|
|
1746
1781
|
}
|
|
@@ -1958,6 +1993,7 @@ import { Command as Command9 } from "commander";
|
|
|
1958
1993
|
|
|
1959
1994
|
// ../shared/src/names.ts
|
|
1960
1995
|
import { createHmac } from "crypto";
|
|
1996
|
+
var DEFAULT_PROXY_DOMAIN = "kvbox.app";
|
|
1961
1997
|
var LABEL_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
1962
1998
|
var RESERVED_SUBDOMAINS = /* @__PURE__ */ new Set([
|
|
1963
1999
|
"admin",
|
|
@@ -2006,6 +2042,21 @@ function assertRouteName(text2) {
|
|
|
2006
2042
|
if (problem) throw new KvboxError(`name ${problem}`);
|
|
2007
2043
|
return text2;
|
|
2008
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
|
+
}
|
|
2009
2060
|
|
|
2010
2061
|
// src/commands/vm/proxy/claim.ts
|
|
2011
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) => {
|
|
@@ -3955,13 +4006,120 @@ var setCommand2 = new Command59("set").description("add or change a variable; bo
|
|
|
3955
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);
|
|
3956
4007
|
for (const cmd of envCommand.commands) cmd.option("--org <slug>", "the org's variables instead of yours");
|
|
3957
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
|
+
|
|
3958
4115
|
// src/program.ts
|
|
3959
4116
|
function buildProgram() {
|
|
3960
|
-
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();
|
|
3961
4118
|
program.addCommand(vmCommand);
|
|
3962
4119
|
program.addCommand(snapshotsCommand);
|
|
3963
4120
|
program.addCommand(volumeCommand);
|
|
3964
4121
|
program.addCommand(envCommand);
|
|
4122
|
+
program.addCommand(domainCommand);
|
|
3965
4123
|
program.addCommand(serverCommand);
|
|
3966
4124
|
program.addCommand(loginCommand);
|
|
3967
4125
|
program.addCommand(logoutCommand);
|