@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 CHANGED
@@ -149,7 +149,7 @@ async function spin(message, work, opts = {}) {
149
149
  }
150
150
 
151
151
  // src/program.ts
152
- import { Command as Command61 } from "commander";
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.26" : void 0);
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, _opts, cmd) => {
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 Command61("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();
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);