@kvbox/cli 0.5.19 → 0.5.21

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
@@ -26,10 +26,10 @@ function json(value) {
26
26
  }
27
27
  var ANSI = /\x1b\[[0-9;]*m/g;
28
28
  var width = (s) => s.replace(ANSI, "").length;
29
- function table(rows, columns) {
30
- if (rows.length === 0) return;
31
- const cols = columns ?? Object.keys(rows[0]);
32
- const cells = rows.map((r) => cols.map((c) => String(r[c] ?? "")));
29
+ function table(rows2, columns) {
30
+ if (rows2.length === 0) return;
31
+ const cols = columns ?? Object.keys(rows2[0]);
32
+ const cells = rows2.map((r) => cols.map((c) => String(r[c] ?? "")));
33
33
  const widths = cols.map((c, i) => Math.max(c.length, ...cells.map((r) => width(r[i]))));
34
34
  const line = (parts) => parts.map((p, i) => p + " ".repeat(Math.max(0, widths[i] - width(p)))).join(" ").trimEnd();
35
35
  process.stdout.write(color.dim(line(cols.map((c) => c.toUpperCase()))) + "\n");
@@ -47,8 +47,8 @@ async function confirm(question, yes) {
47
47
  warn("refusing without -y when stdin is not a terminal");
48
48
  return false;
49
49
  }
50
- const { createInterface: createInterface2 } = await import("readline/promises");
51
- const rl = createInterface2({ input: process.stdin, output: process.stderr });
50
+ const { createInterface: createInterface3 } = await import("readline/promises");
51
+ const rl = createInterface3({ input: process.stdin, output: process.stderr });
52
52
  try {
53
53
  const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase();
54
54
  return answer === "y" || answer === "yes";
@@ -108,14 +108,14 @@ function slugOf(owner) {
108
108
  const at = owner.indexOf("@");
109
109
  return at === -1 ? owner : owner.slice(0, at);
110
110
  }
111
- function pickRef(items, text, me2, kind = "box") {
112
- if (text.includes("/")) return items.find((i) => text === `${slugOf(i.owner)}/${i.name}` || text === `${i.owner}/${i.name}`);
113
- const named = items.filter((i) => i.name === text);
111
+ function pickRef(items, text2, me2, kind = "box") {
112
+ if (text2.includes("/")) return items.find((i) => text2 === `${slugOf(i.owner)}/${i.name}` || text2 === `${i.owner}/${i.name}`);
113
+ const named = items.filter((i) => i.name === text2);
114
114
  const own = named.find((i) => slugOf(i.owner) === me2);
115
115
  if (own) return own;
116
116
  const orgs = me2 === void 0 ? named : named.filter((i) => i.ownerKind !== "user");
117
117
  if (orgs.length <= 1) return orgs[0];
118
- throw new AmbiguousRefError(kind, text, orgs.map((i) => slugOf(i.owner)));
118
+ throw new AmbiguousRefError(kind, text2, orgs.map((i) => slugOf(i.owner)));
119
119
  }
120
120
  function shortRef(ref, owner) {
121
121
  return ref.startsWith(`${owner}/`) ? ref.slice(owner.length + 1) : ref;
@@ -149,7 +149,7 @@ async function spin(message, work, opts = {}) {
149
149
  }
150
150
 
151
151
  // src/program.ts
152
- import { Command as Command49 } from "commander";
152
+ import { Command as Command50 } 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.19" : void 0);
167
+ var VERSION = versionOf(import.meta.url, true ? "0.5.21" : void 0);
168
168
 
169
169
  // src/commands/vm/index.ts
170
170
  import { Command as Command25 } from "commander";
@@ -271,15 +271,15 @@ var SessionSchema = z.object({
271
271
  sshCertPrincipals: z.array(z.string()).optional()
272
272
  });
273
273
  async function readSession(p = paths()) {
274
- let text;
274
+ let text2;
275
275
  try {
276
- text = await readFile(p.cli, "utf8");
276
+ text2 = await readFile(p.cli, "utf8");
277
277
  } catch (err) {
278
278
  if (err.code === "ENOENT") return null;
279
279
  throw err;
280
280
  }
281
281
  try {
282
- return SessionSchema.parse(JSON.parse(text));
282
+ return SessionSchema.parse(JSON.parse(text2));
283
283
  } catch {
284
284
  throw new KvboxError(`${p.cli} is unreadable; run: kvbox logout (then log in again)`);
285
285
  }
@@ -370,13 +370,14 @@ import { z as z2 } from "zod";
370
370
 
371
371
  // ../shared/src/sizes.ts
372
372
  var SIZES = {
373
+ xs: { vcpu: 1, memMib: 2048, diskGib: 8 },
373
374
  s: { vcpu: 2, memMib: 4096, diskGib: 8 },
374
375
  m: { vcpu: 4, memMib: 8192, diskGib: 16 },
375
376
  l: { vcpu: 8, memMib: 16384, diskGib: 32 }
376
377
  };
377
378
  var SIZE_NAMES = Object.keys(SIZES);
378
- function parseMib(text) {
379
- const m = /^\s*(\d+(?:\.\d+)?)\s*([gm]i?b?)?\s*$/i.exec(text);
379
+ function parseMib(text2) {
380
+ const m = /^\s*(\d+(?:\.\d+)?)\s*([gm]i?b?)?\s*$/i.exec(text2);
380
381
  if (!m) return void 0;
381
382
  const n = Number(m[1]);
382
383
  const unit = (m[2] ?? "g").toLowerCase();
@@ -810,13 +811,13 @@ function serverLauncher(opts = {}) {
810
811
  return { cmd: "sg", args: [group, "-c", "kvbox-server start"], viaGroup: group };
811
812
  }
812
813
  function groupWithMember(gid, user, groupFile) {
813
- let text;
814
+ let text2;
814
815
  try {
815
- text = readFileSync(groupFile, "utf8");
816
+ text2 = readFileSync(groupFile, "utf8");
816
817
  } catch {
817
818
  return void 0;
818
819
  }
819
- for (const line of text.split("\n")) {
820
+ for (const line of text2.split("\n")) {
820
821
  const [name, , id, members = ""] = line.split(":");
821
822
  if (name === void 0 || id === void 0 || Number.parseInt(id, 10) !== gid) continue;
822
823
  return members.split(",").includes(user) ? name : void 0;
@@ -831,13 +832,13 @@ import { resolve } from "path";
831
832
  var UNIT = "kvbox-server";
832
833
  var UNIT_FILE = `/etc/systemd/system/${UNIT}.service`;
833
834
  function hasUnitFor(p, file = UNIT_FILE) {
834
- let text;
835
+ let text2;
835
836
  try {
836
- text = readFileSync2(file, "utf8");
837
+ text2 = readFileSync2(file, "utf8");
837
838
  } catch {
838
839
  return false;
839
840
  }
840
- const home = /^Environment="?KVBOX_HOME=([^"\n]+)"?$/m.exec(text)?.[1];
841
+ const home = /^Environment="?KVBOX_HOME=([^"\n]+)"?$/m.exec(text2)?.[1];
841
842
  return home !== void 0 && resolve(home) === resolve(p.home);
842
843
  }
843
844
  function startUnitProcess() {
@@ -912,10 +913,10 @@ var ServerClient = class {
912
913
  }
913
914
  throw new KvboxError(cloudDown(this.target.url, String(code)));
914
915
  }
915
- const text = await res.body.text();
916
+ const text2 = await res.body.text();
916
917
  const where = this.target.kind === "cloud" ? hostOf(this.target.url) : "kvbox-server";
917
918
  if (res.statusCode >= 300) {
918
- const parsed = ApiErrorSchema.safeParse(safeJson(text) ?? {});
919
+ const parsed = ApiErrorSchema.safeParse(safeJson(text2) ?? {});
919
920
  if (!parsed.success) throw new KvboxError(`${where} answered ${res.statusCode}`);
920
921
  if (this.target.kind === "cloud" && this.target.token !== "") {
921
922
  if (res.statusCode === 401) throw new KvboxError("session expired: kvbox login");
@@ -925,8 +926,8 @@ var ServerClient = class {
925
926
  }
926
927
  throw new KvboxError(parsed.data.error, res.statusCode === 501 ? 2 : 1);
927
928
  }
928
- if (text === "") return schema.parse(void 0);
929
- const json2 = safeJson(text);
929
+ if (text2 === "") return schema.parse(void 0);
930
+ const json2 = safeJson(text2);
930
931
  if (json2 === void 0) throw new KvboxError(`${where} answered ${res.statusCode} with something that is not JSON`);
931
932
  return schema.parse(json2);
932
933
  }
@@ -1097,9 +1098,9 @@ function cloudDown(url, code) {
1097
1098
  const hint = host.endsWith("kvbox.sh") ? "check status.kvbox.sh" : "use --local for this machine, or kvbox logout";
1098
1099
  return `${host} is not available or is down (${code}). You are logged in there, so kvbox talks to it instead of this machine; ${hint}`;
1099
1100
  }
1100
- function safeJson(text) {
1101
+ function safeJson(text2) {
1101
1102
  try {
1102
- return JSON.parse(text);
1103
+ return JSON.parse(text2);
1103
1104
  } catch {
1104
1105
  return void 0;
1105
1106
  }
@@ -1119,10 +1120,10 @@ async function wakeUp(client, box, name) {
1119
1120
  return box;
1120
1121
  }
1121
1122
  }
1122
- function boxesFor(client, text) {
1123
- const slash = text.indexOf("/");
1123
+ function boxesFor(client, text2) {
1124
+ const slash = text2.indexOf("/");
1124
1125
  if (slash === -1 || client.target.kind !== "cloud") return client.listMachines();
1125
- return client.listMachines({ org: text.slice(0, slash).split("@")[0] });
1126
+ return client.listMachines({ org: text2.slice(0, slash).split("@")[0] });
1126
1127
  }
1127
1128
  var me = (client) => client.target.kind === "cloud" ? client.target.user : void 0;
1128
1129
  async function clientFor(opts, client = {}) {
@@ -1154,17 +1155,17 @@ function sshArgs(ep, p = paths()) {
1154
1155
  async function waitForSsh(ep, timeoutMs = 3e4) {
1155
1156
  const { connect } = await import("net");
1156
1157
  const deadline = Date.now() + timeoutMs;
1157
- const attempt = () => new Promise((resolve2) => {
1158
+ const attempt = () => new Promise((resolve3) => {
1158
1159
  const sock = connect({ host: ep.host, port: ep.port, timeout: 1e3 });
1159
1160
  sock.once("connect", () => {
1160
1161
  sock.destroy();
1161
- resolve2(true);
1162
+ resolve3(true);
1162
1163
  });
1163
1164
  sock.once("timeout", () => {
1164
1165
  sock.destroy();
1165
- resolve2(false);
1166
+ resolve3(false);
1166
1167
  });
1167
- sock.once("error", () => resolve2(false));
1168
+ sock.once("error", () => resolve3(false));
1168
1169
  });
1169
1170
  while (Date.now() < deadline) {
1170
1171
  if (await attempt()) return;
@@ -1174,9 +1175,9 @@ async function waitForSsh(ep, timeoutMs = 3e4) {
1174
1175
  }
1175
1176
  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";
1176
1177
  async function lastErrorLine(logPath, from = 0) {
1177
- const { readFile: readFile6 } = await import("fs/promises");
1178
- const text = (await readFile6(logPath).catch(() => Buffer.alloc(0))).subarray(from).toString("utf8");
1179
- const lines = text.trimEnd().split("\n").slice(-20).reverse();
1178
+ const { readFile: readFile8 } = await import("fs/promises");
1179
+ const text2 = (await readFile8(logPath).catch(() => Buffer.alloc(0))).subarray(from).toString("utf8");
1180
+ const lines = text2.trimEnd().split("\n").slice(-20).reverse();
1180
1181
  return lines.find((l) => l.startsWith("error: "))?.slice("error: ".length);
1181
1182
  }
1182
1183
  async function startLocalServer(p = paths(), timeoutMs = 5e3) {
@@ -1276,10 +1277,10 @@ function autoHibernateText(sec) {
1276
1277
  return sec ? `hibernates after ${duration(sec)} without network traffic (kvbox connect wakes it)` : "never hibernates on its own";
1277
1278
  }
1278
1279
  var UNIT_SEC = { m: 60, h: 3600, d: 86400 };
1279
- function parseIdleTimeout(text) {
1280
- const t = text.trim().toLowerCase();
1280
+ function parseIdleTimeout(text2) {
1281
+ const t = text2.trim().toLowerCase();
1281
1282
  if (t === "off" || t === "never" || t === "0") return 0;
1282
- if (!/^(\d+[mhd])+$/.test(t)) throw new KvboxError(`--auto-hibernate ${text}: give a duration like 30m, 4h or 1d, or off`);
1283
+ if (!/^(\d+[mhd])+$/.test(t)) throw new KvboxError(`--auto-hibernate ${text2}: give a duration like 30m, 4h or 1d, or off`);
1283
1284
  let sec = 0;
1284
1285
  for (const [, n, unit] of t.matchAll(/(\d+)([mhd])/g)) sec += Number(n) * UNIT_SEC[unit];
1285
1286
  return sec;
@@ -1355,12 +1356,12 @@ async function runSsh(ssh, opts) {
1355
1356
  timedOut = true;
1356
1357
  child.kill("SIGTERM");
1357
1358
  }, opts.timeout * 1e3);
1358
- const status2 = await new Promise((resolve2, reject) => {
1359
+ const status2 = await new Promise((resolve3, reject) => {
1359
1360
  child.once("error", (err) => reject(new KvboxError(`could not run ssh: ${err.message}`)));
1360
1361
  child.once("close", (code, signal) => {
1361
- if (timedOut) resolve2(TIMEOUT_EXIT);
1362
- else if (code !== null) resolve2(code);
1363
- else resolve2(signal === "SIGINT" ? 130 : 1);
1362
+ if (timedOut) resolve3(TIMEOUT_EXIT);
1363
+ else if (code !== null) resolve3(code);
1364
+ else resolve3(signal === "SIGINT" ? 130 : 1);
1364
1365
  });
1365
1366
  }).finally(() => clearTimeout(timer));
1366
1367
  return { status: status2, chunks, timedOut };
@@ -1539,26 +1540,26 @@ var RESERVED_SUBDOMAINS = /* @__PURE__ */ new Set([
1539
1540
  "support",
1540
1541
  "www"
1541
1542
  ]);
1542
- function subdomainProblem(text) {
1543
- if (text.length < 3 || text.length > 63) return `"${text}" must be 3 to 63 characters`;
1544
- if (!LABEL_RE.test(text)) return `"${text}" may hold only lowercase letters, digits and '-', not at either end`;
1545
- if (RESERVED_SUBDOMAINS.has(text)) return `"${text}" is reserved; pick another name`;
1543
+ function subdomainProblem(text2) {
1544
+ if (text2.length < 3 || text2.length > 63) return `"${text2}" must be 3 to 63 characters`;
1545
+ if (!LABEL_RE.test(text2)) return `"${text2}" may hold only lowercase letters, digits and '-', not at either end`;
1546
+ if (RESERVED_SUBDOMAINS.has(text2)) return `"${text2}" is reserved; pick another name`;
1546
1547
  return null;
1547
1548
  }
1548
- function routeNameProblem(text) {
1549
- if (text.length < 1 || text.length > 63) return `"${text}" must be 1 to 63 characters`;
1550
- if (!LABEL_RE.test(text)) return `"${text}" may hold only lowercase letters, digits and '-', not at either end`;
1549
+ function routeNameProblem(text2) {
1550
+ if (text2.length < 1 || text2.length > 63) return `"${text2}" must be 1 to 63 characters`;
1551
+ if (!LABEL_RE.test(text2)) return `"${text2}" may hold only lowercase letters, digits and '-', not at either end`;
1551
1552
  return null;
1552
1553
  }
1553
- function assertSubdomain(text) {
1554
- const problem = subdomainProblem(text);
1554
+ function assertSubdomain(text2) {
1555
+ const problem = subdomainProblem(text2);
1555
1556
  if (problem) throw new KvboxError(`subdomain ${problem}`);
1556
- return text;
1557
+ return text2;
1557
1558
  }
1558
- function assertRouteName(text) {
1559
- const problem = routeNameProblem(text);
1559
+ function assertRouteName(text2) {
1560
+ const problem = routeNameProblem(text2);
1560
1561
  if (problem) throw new KvboxError(`name ${problem}`);
1561
- return text;
1562
+ return text2;
1562
1563
  }
1563
1564
 
1564
1565
  // src/commands/vm/proxy/shared.ts
@@ -1587,9 +1588,9 @@ function parsePorts(texts) {
1587
1588
  return n;
1588
1589
  });
1589
1590
  }
1590
- async function findBox(client, text) {
1591
- const found = pickRef(await boxesFor(client, text), text, me(client));
1592
- if (!found) throw new KvboxError(`box ${text}: not found (kvbox ls)`);
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)`);
1593
1594
  return found;
1594
1595
  }
1595
1596
 
@@ -1649,12 +1650,12 @@ var addCommand = new Command9("add").description("publish box ports on the inter
1649
1650
  return;
1650
1651
  }
1651
1652
  const updated = await spin(`publishing ${guestPorts.join(", ")} on ${name}\u2026`, client.addPorts(box, { ports: guestPorts.map((guestPort) => ({ guestPort, proto })) }));
1652
- const rows = rowsOf(updated, user, (f) => f.proto === proto && guestPorts.includes(f.guestPort));
1653
+ const rows2 = rowsOf(updated, user, (f) => f.proto === proto && guestPorts.includes(f.guestPort));
1653
1654
  if (opts.json) {
1654
- json(rows);
1655
+ json(rows2);
1655
1656
  return;
1656
1657
  }
1657
- for (const row of rows) info(`${color.green("\u2713")} box ${color.bold(row.box)} port ${row.guestPort} is reachable at ${reaches(row)} (${row.proto})`);
1658
+ for (const row of rows2) info(`${color.green("\u2713")} box ${color.bold(row.box)} port ${row.guestPort} is reachable at ${reaches(row)} (${row.proto})`);
1658
1659
  if (updated.status !== "running") info(` box ${name} is ${updated.status}: the ports open when it runs`);
1659
1660
  } finally {
1660
1661
  await client.close();
@@ -1681,16 +1682,16 @@ var lsCommand2 = new Command10("ls").description("list the box ports published o
1681
1682
  ]);
1682
1683
  const shown = new Set(boxes.map((b) => displayName(b, user)));
1683
1684
  const https = subs.flatMap((s) => httpsRowsOf(s, user)).filter((r) => shown.has(r.box));
1684
- const rows = [...boxes.flatMap((b) => rowsOf(b, user)), ...https].sort((a, b) => a.box.localeCompare(b.box) || a.guestPort - b.guestPort);
1685
+ const rows2 = [...boxes.flatMap((b) => rowsOf(b, user)), ...https].sort((a, b) => a.box.localeCompare(b.box) || a.guestPort - b.guestPort);
1685
1686
  if (opts.json) {
1686
- json(rows);
1687
+ json(rows2);
1687
1688
  return;
1688
1689
  }
1689
- if (rows.length === 0) {
1690
+ if (rows2.length === 0) {
1690
1691
  info(name === void 0 ? "no published ports (kvbox vm proxy add <box> <port>)" : `box ${name} publishes no ports (kvbox vm proxy add ${name} <port>)`);
1691
1692
  return;
1692
1693
  }
1693
- table(rows.map((r) => ({ box: r.box, port: r.guestPort, reaches: r.proto === "https" ? r.url : reaches(r, true), proto: r.proto })));
1694
+ table(rows2.map((r) => ({ box: r.box, port: r.guestPort, reaches: r.proto === "https" ? r.url : reaches(r, true), proto: r.proto })));
1694
1695
  } finally {
1695
1696
  await client.close();
1696
1697
  }
@@ -1802,19 +1803,19 @@ var rescaleCommand = new Command15("rescale").description("change a box's vCPUs,
1802
1803
  await client.close();
1803
1804
  }
1804
1805
  });
1805
- function parseCount(text) {
1806
- const n = Number(text);
1807
- if (!Number.isInteger(n) || n < 1) throw new KvboxError(`--vcpu ${text}: give a whole number of vCPUs`);
1806
+ function parseCount(text2) {
1807
+ const n = Number(text2);
1808
+ if (!Number.isInteger(n) || n < 1) throw new KvboxError(`--vcpu ${text2}: give a whole number of vCPUs`);
1808
1809
  return n;
1809
1810
  }
1810
- function parseSize(flag, text) {
1811
- const mib = parseMib(text);
1812
- if (mib === void 0) throw new KvboxError(`${flag} ${text}: give a size like 16GB or 4096MB`);
1811
+ function parseSize(flag, text2) {
1812
+ const mib = parseMib(text2);
1813
+ if (mib === void 0) throw new KvboxError(`${flag} ${text2}: give a size like 16GB or 4096MB`);
1813
1814
  return mib;
1814
1815
  }
1815
- function parseGib(flag, text) {
1816
- const mib = parseSize(flag, text);
1817
- if (mib % 1024 !== 0) throw new KvboxError(`${flag} ${text}: disk sizes are whole GB`);
1816
+ function parseGib(flag, text2) {
1817
+ const mib = parseSize(flag, text2);
1818
+ if (mib % 1024 !== 0) throw new KvboxError(`${flag} ${text2}: disk sizes are whole GB`);
1818
1819
  return mib / 1024;
1819
1820
  }
1820
1821
 
@@ -2009,7 +2010,7 @@ function money(amount, currency) {
2009
2010
  }
2010
2011
  function block(u) {
2011
2012
  const estimated = u.estimatedStops === 0 ? "" : color.dim(` (${u.estimatedStops} stop time${u.estimatedStops === 1 ? "" : "s"} estimated)`);
2012
- const rows = [
2013
+ const rows2 = [
2013
2014
  ["name", boxName(u.name)],
2014
2015
  ["status", status(u.status)],
2015
2016
  ["window", `${day(u.window.from)} to ${day(u.window.to)}`],
@@ -2021,12 +2022,12 @@ function block(u) {
2021
2022
  ["ram GiB-h", hours(u.ramGibHours)],
2022
2023
  ["disk GiB-h", hours(u.diskGibHours)]
2023
2024
  ];
2024
- if (u.cost !== void 0) rows.push(["cost", `${money(u.cost.amount, u.cost.currency)} ${color.dim("(estimated)")}`]);
2025
+ if (u.cost !== void 0) rows2.push(["cost", `${money(u.cost.amount, u.cost.currency)} ${color.dim("(estimated)")}`]);
2025
2026
  if (u.historyFrom !== null && u.historyFrom > u.window.from) {
2026
- rows.push(["history", color.dim(`from ${u.historyFrom.slice(0, 16).replace("T", " ")} UTC; the box is older, but its time before that was not recorded`)]);
2027
+ rows2.push(["history", color.dim(`from ${u.historyFrom.slice(0, 16).replace("T", " ")} UTC; the box is older, but its time before that was not recorded`)]);
2027
2028
  }
2028
- const w = Math.max(...rows.map(([k]) => k.length));
2029
- return rows.map(([k, v]) => `${" ".repeat(w - k.length)}${k}: ${v}`).join("\n") + "\n";
2029
+ const w = Math.max(...rows2.map(([k]) => k.length));
2030
+ return rows2.map(([k, v]) => `${" ".repeat(w - k.length)}${k}: ${v}`).join("\n") + "\n";
2030
2031
  }
2031
2032
 
2032
2033
  // src/commands/vm/wake.ts
@@ -2088,7 +2089,7 @@ async function restartLocalServer(p = paths()) {
2088
2089
  return (await startLocalServer(p)).pid;
2089
2090
  }
2090
2091
  function runKvboxServer(args) {
2091
- return new Promise((resolve2, reject) => {
2092
+ return new Promise((resolve3, reject) => {
2092
2093
  const child = spawn3("kvbox-server", args, { stdio: ["ignore", "pipe", "pipe"] });
2093
2094
  let out = "";
2094
2095
  let err = "";
@@ -2098,7 +2099,7 @@ function runKvboxServer(args) {
2098
2099
  reject(new KvboxError(e.code === "ENOENT" ? NOT_A_HOST_HINT : `could not run kvbox-server: ${e.message}`));
2099
2100
  });
2100
2101
  child.once("close", (code) => {
2101
- if (code === 0) resolve2(out.trimEnd());
2102
+ if (code === 0) resolve3(out.trimEnd());
2102
2103
  else reject(new KvboxError(err.trim().replace(/^error: /, "") || `kvbox-server ${args[0] ?? ""} exited with code ${String(code)}`, code === 2 ? 2 : 1));
2103
2104
  });
2104
2105
  });
@@ -2235,9 +2236,133 @@ var serverCommand = new Command33("server").description("run or manage the kvbox
2235
2236
  import { spawnSync as spawnSync3 } from "child_process";
2236
2237
  import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
2237
2238
  import { hostname as osHostname } from "os";
2238
- import { createInterface } from "readline/promises";
2239
+ import { createInterface as createInterface2 } from "readline/promises";
2239
2240
  import { Writable } from "stream";
2240
2241
  import { Command as Command34 } from "commander";
2242
+
2243
+ // src/prompt.ts
2244
+ import { createInterface } from "readline/promises";
2245
+ function interactive() {
2246
+ return process.stdin.isTTY === true && process.stderr.isTTY === true;
2247
+ }
2248
+ var ESC = "\x1B";
2249
+ var ENTER = "\r";
2250
+ var CTRL_C = "";
2251
+ var CTRL_D = "";
2252
+ var cancelled = () => new KvboxError("cancelled", 130);
2253
+ function keysIn(chunk) {
2254
+ const keys = [];
2255
+ for (let i = 0; i < chunk.length; ) {
2256
+ if (chunk[i] === ESC && (chunk[i + 1] === "[" || chunk[i + 1] === "O") && i + 2 < chunk.length) {
2257
+ keys.push(chunk.slice(i, i + 3));
2258
+ i += 3;
2259
+ } else {
2260
+ keys.push(chunk[i]);
2261
+ i += 1;
2262
+ }
2263
+ }
2264
+ return keys;
2265
+ }
2266
+ var isUp = (k) => k === `${ESC}[A` || k === `${ESC}OA` || k === "k";
2267
+ var isDown = (k) => k === `${ESC}[B` || k === `${ESC}OB` || k === "j";
2268
+ var ANSI2 = /\x1b\[[0-9;]*[A-Za-z]/g;
2269
+ function rows(line, columns = process.stderr.columns ?? 80) {
2270
+ const width2 = line.replace(ANSI2, "").length;
2271
+ return Math.max(1, Math.ceil(width2 / Math.max(1, columns)));
2272
+ }
2273
+ function settled(message, answer) {
2274
+ return `${color.green("\u2713")} ${color.bold(message)} ${color.cyan(answer)}`;
2275
+ }
2276
+ async function select(message, choices, initial = 0) {
2277
+ const out = process.stderr;
2278
+ const inp = process.stdin;
2279
+ let index = Math.min(Math.max(initial, 0), choices.length - 1);
2280
+ let drawn = 0;
2281
+ const render = () => {
2282
+ if (drawn > 0) out.write(`${ESC}[${drawn}A`);
2283
+ const lines = [
2284
+ `${color.green("?")} ${color.bold(message)} ${color.dim("\u2191\u2193 to move, enter to pick")}`,
2285
+ ...choices.map((c, i) => {
2286
+ const hint = c.hint ? color.dim(` ${c.hint}`) : "";
2287
+ return i === index ? `${color.cyan(`\u276F ${c.label}`)}${hint}` : ` ${c.label}${hint}`;
2288
+ })
2289
+ ];
2290
+ out.write(`\r${ESC}[J${lines.join("\n")}
2291
+ `);
2292
+ drawn = lines.reduce((n, l) => n + rows(l), 0);
2293
+ };
2294
+ inp.setRawMode(true);
2295
+ inp.resume();
2296
+ inp.setEncoding("utf8");
2297
+ render();
2298
+ let done = false;
2299
+ try {
2300
+ return await new Promise((resolve3, reject) => {
2301
+ const onData = (chunk) => {
2302
+ for (const key of keysIn(chunk)) {
2303
+ if (isUp(key)) index = (index - 1 + choices.length) % choices.length;
2304
+ else if (isDown(key)) index = (index + 1) % choices.length;
2305
+ else if (key === ENTER || key === "\n") {
2306
+ inp.off("data", onData);
2307
+ done = true;
2308
+ resolve3(choices[index].value);
2309
+ return;
2310
+ } else if (key === CTRL_C || key === CTRL_D) {
2311
+ inp.off("data", onData);
2312
+ reject(cancelled());
2313
+ return;
2314
+ } else continue;
2315
+ render();
2316
+ }
2317
+ };
2318
+ inp.on("data", onData);
2319
+ });
2320
+ } finally {
2321
+ inp.setRawMode(false);
2322
+ inp.pause();
2323
+ out.write(`${ESC}[${drawn}A\r${ESC}[J${done ? `${settled(message, choices[index].label)}
2324
+ ` : ""}`);
2325
+ }
2326
+ }
2327
+ async function text(message, opts = {}) {
2328
+ const out = process.stderr;
2329
+ for (; ; ) {
2330
+ const hint = opts.default !== void 0 ? ` ${color.dim(`(${opts.default})`)}` : "";
2331
+ const question = `${color.green("?")} ${color.bold(message)}${hint} ${color.dim("\u203A")} `;
2332
+ const rl = createInterface({ input: process.stdin, output: out });
2333
+ const raw = (await askLine(rl, question)).trim();
2334
+ const answer = raw === "" && opts.default !== void 0 ? opts.default : raw;
2335
+ const problem = opts.validate?.(answer);
2336
+ out.write(`${ESC}[${rows(question + raw)}A\r${ESC}[J`);
2337
+ if (problem === void 0) {
2338
+ out.write(`${settled(message, answer)}
2339
+ `);
2340
+ return answer;
2341
+ }
2342
+ out.write(`${color.yellow("!")} ${problem}
2343
+ `);
2344
+ }
2345
+ }
2346
+ async function askLine(rl, question) {
2347
+ let onClose = () => {
2348
+ };
2349
+ const closed = new Promise((_, reject) => {
2350
+ onClose = () => reject(cancelled());
2351
+ rl.once("close", onClose);
2352
+ });
2353
+ try {
2354
+ return await Promise.race([rl.question(question).catch(abortIsCancel), closed]);
2355
+ } finally {
2356
+ rl.off("close", onClose);
2357
+ rl.close();
2358
+ }
2359
+ }
2360
+ function abortIsCancel(err) {
2361
+ if (err.code === "ABORT_ERR") throw cancelled();
2362
+ throw err;
2363
+ }
2364
+
2365
+ // src/commands/login.ts
2241
2366
  var loginCommand = new Command34("login").description("log in to the kvbox cloud (or your own control plane) and target it from now on").argument("[url]", `control plane URL (default: ${DEFAULT_CLOUD_URL})`).option("-e, --email <email>", "account email (prompted otherwise)").option("--password-stdin", "read the password from the first line of stdin (for scripts)").action(async (urlArg, opts) => {
2242
2367
  const url = normalizeUrl(urlArg ?? DEFAULT_CLOUD_URL);
2243
2368
  const creds = await readCredentials(opts);
@@ -2294,39 +2419,21 @@ async function ensureSshKey(p = paths(), hostname = osHostname()) {
2294
2419
  return (await readFile5(pubPath, "utf8")).trim();
2295
2420
  }
2296
2421
  async function prompt(question, io) {
2297
- const rl = createInterface({ input: io.input, output: io.output });
2298
- return (await ask(rl, question)).trim();
2422
+ const rl = createInterface2({ input: io.input, output: io.output });
2423
+ return (await askLine(rl, question)).trim();
2299
2424
  }
2300
2425
  async function promptSecret(question, io) {
2301
2426
  const sink = new Writable({ write: (_chunk, _enc, cb) => cb() });
2302
- const rl = createInterface({ input: io.input, output: sink, terminal: true });
2427
+ const rl = createInterface2({ input: io.input, output: sink, terminal: true });
2303
2428
  io.output.write(question);
2304
2429
  try {
2305
- return await ask(rl, question);
2430
+ return await askLine(rl, question);
2306
2431
  } finally {
2307
2432
  io.output.write("\n");
2308
2433
  }
2309
2434
  }
2310
- async function ask(rl, question) {
2311
- let onClose = () => {
2312
- };
2313
- const closed = new Promise((_, reject) => {
2314
- onClose = () => reject(new KvboxError("cancelled", 130));
2315
- rl.once("close", onClose);
2316
- });
2317
- try {
2318
- return await Promise.race([rl.question(question).catch(cancelled), closed]);
2319
- } finally {
2320
- rl.off("close", onClose);
2321
- rl.close();
2322
- }
2323
- }
2324
- function cancelled(err) {
2325
- if (err.code === "ABORT_ERR") throw new KvboxError("cancelled", 130);
2326
- throw err;
2327
- }
2328
2435
  async function firstLine(stream) {
2329
- const rl = createInterface({ input: stream });
2436
+ const rl = createInterface2({ input: stream });
2330
2437
  try {
2331
2438
  for await (const line of rl) return line.trim();
2332
2439
  return "";
@@ -2684,7 +2791,7 @@ function ownerCell2(b, me2) {
2684
2791
  }
2685
2792
  function usageRows(report, cloud) {
2686
2793
  const priced = report.totals.cost !== void 0;
2687
- const rows = report.boxes.map((b) => ({
2794
+ const rows2 = report.boxes.map((b) => ({
2688
2795
  ...cloud ? { owner: ownerCell2(b, cloud.me) } : {},
2689
2796
  name: boxName(b.name),
2690
2797
  status: b.status === "removed" ? color.dim("removed") : status(b.status),
@@ -2695,7 +2802,7 @@ function usageRows(report, cloud) {
2695
2802
  ...priced ? { cost: money(b.cost?.amount ?? 0, report.totals.cost.currency) } : {}
2696
2803
  }));
2697
2804
  const t = report.totals;
2698
- rows.push({
2805
+ rows2.push({
2699
2806
  ...cloud ? { owner: "" } : {},
2700
2807
  name: color.bold("total"),
2701
2808
  status: "",
@@ -2705,7 +2812,7 @@ function usageRows(report, cloud) {
2705
2812
  "disk GiB-h": hours(t.diskGibHours),
2706
2813
  ...priced ? { cost: `${money(t.cost.amount, t.cost.currency)} ${color.dim("(estimated)")}` } : {}
2707
2814
  });
2708
- return rows;
2815
+ return rows2;
2709
2816
  }
2710
2817
  function byUser(report) {
2711
2818
  const groups = /* @__PURE__ */ new Map();
@@ -2733,19 +2840,19 @@ function userRows(r) {
2733
2840
  "ram GiB-h": hours(t.ramGibHours),
2734
2841
  "disk GiB-h": hours(t.diskGibHours)
2735
2842
  });
2736
- const rows = r.users.map((u) => ({
2843
+ const rows2 = r.users.map((u) => ({
2737
2844
  user: u.user ?? color.dim("unknown"),
2738
2845
  boxes: String(u.boxes),
2739
2846
  ...cells(u),
2740
2847
  ...priced ? { cost: money(u.cost?.amount ?? 0, r.totals.cost.currency) } : {}
2741
2848
  }));
2742
- rows.push({
2849
+ rows2.push({
2743
2850
  user: color.bold("total"),
2744
2851
  boxes: String(r.users.reduce((n, u) => n + u.boxes, 0)),
2745
2852
  ...cells(r.totals),
2746
2853
  ...priced ? { cost: `${money(r.totals.cost.amount, r.totals.cost.currency)} ${color.dim("(estimated)")}` } : {}
2747
2854
  });
2748
- return rows;
2855
+ return rows2;
2749
2856
  }
2750
2857
 
2751
2858
  // src/commands/credit.ts
@@ -2777,9 +2884,476 @@ function creditRow(b, me2) {
2777
2884
  };
2778
2885
  }
2779
2886
 
2887
+ // src/commands/deploy/index.ts
2888
+ import { readFile as readFile7 } from "fs/promises";
2889
+ import { basename, resolve as resolve2 } from "path";
2890
+ import { Command as Command49, Option as Option5 } from "commander";
2891
+
2892
+ // src/commands/deploy/config.ts
2893
+ import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
2894
+ import { join as join4 } from "path";
2895
+ import { z as z5 } from "zod";
2896
+
2897
+ // src/commands/deploy/templates/pm.ts
2898
+ import { access } from "fs/promises";
2899
+ import { join as join2 } from "path";
2900
+ async function packageManager(dir) {
2901
+ const has = (f) => access(join2(dir, f)).then(() => true, () => false);
2902
+ if (await has("pnpm-lock.yaml")) return "pnpm";
2903
+ if (await has("yarn.lock")) return "yarn";
2904
+ if (await has("bun.lockb") || await has("bun.lock")) return "bun";
2905
+ return "npm";
2906
+ }
2907
+ function runScript(pm, script) {
2908
+ return pm === "yarn" ? [script] : ["run", script];
2909
+ }
2910
+
2911
+ // src/commands/deploy/templates/nextjs.ts
2912
+ var APP_DIR = "/srv/app/current";
2913
+ var SERVICE = "kvbox-app";
2914
+ var PORT = 3e3;
2915
+ function commands(pm) {
2916
+ switch (pm) {
2917
+ case "pnpm":
2918
+ return { setup: ["sudo -n corepack enable pnpm"], install: "pnpm install --frozen-lockfile", build: "pnpm run build", start: "pnpm run start" };
2919
+ case "yarn":
2920
+ return { setup: ["sudo -n corepack enable yarn"], install: "yarn install --frozen-lockfile", build: "yarn build", start: "yarn start" };
2921
+ case "bun":
2922
+ return { setup: [], install: "npm install", build: "npm run build", start: "npm run start" };
2923
+ case "npm":
2924
+ return { setup: [], install: "if [ -f package-lock.json ]; then npm ci; else npm install; fi", build: "npm run build", start: "npm run start" };
2925
+ }
2926
+ }
2927
+ var nextjsTemplate = {
2928
+ id: "nextjs",
2929
+ label: "Next.js",
2930
+ hint: "source shipped; installed, built and run in the box on port 3000",
2931
+ size: "s",
2932
+ port: PORT,
2933
+ detect(pkg) {
2934
+ if (!pkg) return false;
2935
+ return "next" in { ...pkg.dependencies, ...pkg.devDependencies };
2936
+ },
2937
+ /** No local build: the box does it. Only the scripts it will need are checked here. */
2938
+ async build(ctx) {
2939
+ for (const script of ["build", "start"]) {
2940
+ if (typeof ctx.pkg?.scripts?.[script] !== "string") throw new KvboxError(`${ctx.dir}: package.json has no "${script}" script; Next.js needs "build": "next build" and "start": "next start"`);
2941
+ }
2942
+ const pm = await packageManager(ctx.dir);
2943
+ return { outDir: ctx.dir, exclude: ["node_modules", ".next", ".git"], pm, what: "the source" };
2944
+ },
2945
+ /**
2946
+ * The source lands in a staging directory and is installed and built there while the old app
2947
+ * keeps serving; only then is it swapped into place and the service restarted, so a failed
2948
+ * install or build leaves the running app alone. The unit is rewritten every time, so a changed
2949
+ * package manager or start script takes effect. /srv/app is the kvbox user's (image v0.8.0+).
2950
+ */
2951
+ remote(built) {
2952
+ const c = commands(built.pm);
2953
+ const unit = [
2954
+ "[Unit]",
2955
+ "Description=kvbox deploy: the app in " + APP_DIR,
2956
+ "After=network.target",
2957
+ "",
2958
+ "[Service]",
2959
+ "User=kvbox",
2960
+ `WorkingDirectory=${APP_DIR}`,
2961
+ `ExecStart=/usr/bin/env ${c.start}`,
2962
+ `Environment=NODE_ENV=production PORT=${PORT}`,
2963
+ "Restart=always",
2964
+ "RestartSec=2",
2965
+ "",
2966
+ "[Install]",
2967
+ "WantedBy=multi-user.target"
2968
+ ];
2969
+ return [
2970
+ "set -e",
2971
+ "test -d /srv/app -a -w /srv/app || exit 3",
2972
+ "rm -rf /srv/app/.incoming /srv/app/.old",
2973
+ "mkdir -p /srv/app/.incoming",
2974
+ "tar -xf - -C /srv/app/.incoming",
2975
+ "cd /srv/app/.incoming",
2976
+ "export CI=1 NEXT_TELEMETRY_DISABLED=1 NODE_ENV=production",
2977
+ ...c.setup,
2978
+ c.install,
2979
+ c.build,
2980
+ "cd /",
2981
+ `sudo -n tee /etc/systemd/system/${SERVICE}.service >/dev/null <<'UNIT'`,
2982
+ ...unit,
2983
+ "UNIT",
2984
+ `if [ -e ${APP_DIR} ]; then mv ${APP_DIR} /srv/app/.old; fi`,
2985
+ `mv /srv/app/.incoming ${APP_DIR}`,
2986
+ "sudo -n systemctl daemon-reload",
2987
+ `sudo -n systemctl enable --quiet ${SERVICE}`,
2988
+ `sudo -n systemctl restart ${SERVICE}`,
2989
+ "rm -rf /srv/app/.old"
2990
+ ].join("\n");
2991
+ }
2992
+ };
2993
+
2994
+ // src/commands/deploy/templates/react.ts
2995
+ import { spawn as spawn4 } from "child_process";
2996
+ import { access as access2 } from "fs/promises";
2997
+ import { join as join3 } from "path";
2998
+ var OUT_DIRS = ["dist", "build"];
2999
+ async function runBuild(dir, pm) {
3000
+ const args = runScript(pm, "build");
3001
+ info(`${color.dim("$")} ${pm} ${args.join(" ")}`);
3002
+ const status2 = await new Promise((resolve3, reject) => {
3003
+ const child = spawn4(pm, args, { cwd: dir, stdio: ["ignore", process.stderr, process.stderr], shell: process.platform === "win32" });
3004
+ child.once("error", (err) => reject(new KvboxError(`could not run ${pm}: ${err.message}`)));
3005
+ child.once("close", (code) => resolve3(code ?? 1));
3006
+ });
3007
+ if (status2 !== 0) throw new KvboxError(`the build failed (${pm} exited ${status2}); nothing was deployed`);
3008
+ }
3009
+ var reactTemplate = {
3010
+ id: "react",
3011
+ label: "React",
3012
+ hint: "static site: built here, served by nginx in the box",
3013
+ size: "xs",
3014
+ port: 80,
3015
+ /** A Vite or Create React App project. A framework with its own server (Next.js, Remix, Expo) is not a static site. */
3016
+ detect(pkg) {
3017
+ if (!pkg) return false;
3018
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
3019
+ if (!("react" in deps) || "next" in deps || "@remix-run/react" in deps || "expo" in deps) return false;
3020
+ return "vite" in deps || "react-scripts" in deps;
3021
+ },
3022
+ async build(ctx) {
3023
+ if (typeof ctx.pkg?.scripts?.["build"] !== "string") throw new KvboxError(`${ctx.dir}: package.json has no "build" script`);
3024
+ const pm = await packageManager(ctx.dir);
3025
+ await runBuild(ctx.dir, pm);
3026
+ const candidates = ctx.dist !== void 0 ? [ctx.dist] : OUT_DIRS;
3027
+ for (const dist of candidates) {
3028
+ const outDir = join3(ctx.dir, dist);
3029
+ if (await access2(join3(outDir, "index.html")).then(() => true, () => false)) return { outDir, dist, exclude: [], pm, what: `${dist}/` };
3030
+ }
3031
+ throw new KvboxError(
3032
+ `the build left no ${candidates.join(" or ")}/index.html. The React template ships a directory of static files; set "dist" in kvbox.json to the build output directory, or pick the right template with --template`
3033
+ );
3034
+ },
3035
+ /**
3036
+ * The archive lands in a staging directory, which then becomes /srv/app/dist in one move; the
3037
+ * old site is kept beside it until the new one is in place, so a failed step never leaves nginx
3038
+ * without a root and a visitor never sees a half-copied site. Files get read permission for
3039
+ * nginx's user, whatever umask the build ran under. /srv/app is the kvbox user's (image v0.8.0+);
3040
+ * the missing site file means an older image.
3041
+ */
3042
+ remote() {
3043
+ return [
3044
+ "set -e",
3045
+ "test -e /etc/nginx/sites-enabled/kvbox-app || exit 3",
3046
+ "rm -rf /srv/app/.incoming /srv/app/.old",
3047
+ "mkdir -p /srv/app/.incoming",
3048
+ "tar -xf - -C /srv/app/.incoming",
3049
+ "chmod -R a+rX /srv/app/.incoming",
3050
+ "if [ -e /srv/app/dist ]; then mv /srv/app/dist /srv/app/.old; fi",
3051
+ "mv /srv/app/.incoming /srv/app/dist",
3052
+ "rm -rf /srv/app/.old",
3053
+ "sudo -n systemctl enable --now --quiet nginx",
3054
+ "sudo -n systemctl reload nginx"
3055
+ ].join("\n");
3056
+ }
3057
+ };
3058
+
3059
+ // src/commands/deploy/templates/index.ts
3060
+ var TEMPLATES = [reactTemplate, nextjsTemplate];
3061
+ var TEMPLATE_IDS = TEMPLATES.map((t) => t.id);
3062
+ function templateById(id) {
3063
+ const t = TEMPLATES.find((x) => x.id === id);
3064
+ if (!t) throw new Error(`no template ${id}`);
3065
+ return t;
3066
+ }
3067
+ function detectTemplate(pkg) {
3068
+ return TEMPLATES.find((t) => t.detect(pkg));
3069
+ }
3070
+
3071
+ // src/commands/deploy/config.ts
3072
+ var DEPLOY_AUTO_HIBERNATE_SEC = 3600;
3073
+ var CONFIG_FILE = "kvbox.json";
3074
+ var BOX_NAME_RE = /^[a-z0-9][a-z0-9-]{0,11}$/;
3075
+ var DeployConfigSchema = z5.object({
3076
+ template: z5.enum(TEMPLATE_IDS),
3077
+ name: z5.string().regex(BOX_NAME_RE, "1-12 chars, lowercase letters, digits and '-', starting with a letter or digit"),
3078
+ /** The org whose box it is; absent for the user's own. */
3079
+ org: z5.string().regex(ORG_SLUG_RE, "not an org slug").optional(),
3080
+ /** The HTTPS name (`<subdomain>.kvbox.app`); absent when deploying to this machine's own host. */
3081
+ subdomain: z5.string().check((ctx) => {
3082
+ const problem = subdomainProblem(ctx.value);
3083
+ if (problem) ctx.issues.push({ code: "custom", message: problem, input: ctx.value });
3084
+ }).optional(),
3085
+ size: z5.enum(SIZE_NAMES).optional(),
3086
+ /** The build output directory, relative to the app; the template's default when absent. */
3087
+ dist: z5.string().optional()
3088
+ });
3089
+ async function readConfig(dir) {
3090
+ const path = join4(dir, CONFIG_FILE);
3091
+ let raw;
3092
+ try {
3093
+ raw = await readFile6(path, "utf8");
3094
+ } catch (err) {
3095
+ if (err.code === "ENOENT") return void 0;
3096
+ throw err;
3097
+ }
3098
+ let json2;
3099
+ try {
3100
+ json2 = JSON.parse(raw);
3101
+ } catch {
3102
+ throw new KvboxError(`${path}: not valid JSON`);
3103
+ }
3104
+ const parsed = DeployConfigSchema.safeParse(json2);
3105
+ if (!parsed.success) {
3106
+ const issue = parsed.error.issues[0];
3107
+ throw new KvboxError(`${path}: ${issue.path.join(".") || "file"}: ${issue.message}`);
3108
+ }
3109
+ return parsed.data;
3110
+ }
3111
+ async function writeConfig(dir, config) {
3112
+ await writeFile4(join4(dir, CONFIG_FILE), JSON.stringify(config, null, 2) + "\n");
3113
+ }
3114
+ function nameFromDir(dirName) {
3115
+ const slug = dirName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 12).replace(/-+$/, "");
3116
+ return slug === "" ? "app" : slug;
3117
+ }
3118
+ function subdomainFor(name) {
3119
+ return subdomainProblem(name) === null ? name : `${name}-site`;
3120
+ }
3121
+
3122
+ // src/commands/deploy/ship.ts
3123
+ import { spawn as spawn5 } from "child_process";
3124
+ import { readdir, stat as stat2 } from "fs/promises";
3125
+ import { join as join5 } from "path";
3126
+ var IMAGE_TOO_OLD = 3;
3127
+ var SSH_FAILED3 = 255;
3128
+ async function measure(dir, exclude = []) {
3129
+ const total = { files: 0, bytes: 0 };
3130
+ const walk = async (d) => {
3131
+ for (const entry of await readdir(d, { withFileTypes: true })) {
3132
+ if (exclude.includes(entry.name)) continue;
3133
+ const path = join5(d, entry.name);
3134
+ if (entry.isDirectory()) await walk(path);
3135
+ else if (entry.isFile()) {
3136
+ total.files += 1;
3137
+ total.bytes += (await stat2(path)).size;
3138
+ }
3139
+ }
3140
+ };
3141
+ await walk(dir);
3142
+ return total;
3143
+ }
3144
+ async function ship(ep, dir, remote, exclude = []) {
3145
+ const tar = spawn5("tar", ["-cf", "-", ...exclude.map((e) => `--exclude=${e}`), "-C", dir, "."], { stdio: ["ignore", "pipe", "pipe"] });
3146
+ const ssh = spawn5("ssh", [...sshArgs(ep), remote], { stdio: ["pipe", "pipe", "pipe"] });
3147
+ tar.stdout.pipe(ssh.stdin);
3148
+ const chunks = [];
3149
+ ssh.stdout.on("data", (c) => chunks.push(c));
3150
+ ssh.stderr.on("data", (c) => chunks.push(c));
3151
+ tar.stderr.on("data", (c) => chunks.push(c));
3152
+ ssh.stdin.on("error", () => {
3153
+ });
3154
+ tar.stdout.on("error", () => {
3155
+ });
3156
+ tar.once("error", () => ssh.stdin.end());
3157
+ tar.once("close", (code) => {
3158
+ if (code !== 0) ssh.stdin.end();
3159
+ });
3160
+ const tarDone = new Promise((resolve3) => tar.once("close", () => resolve3()));
3161
+ const status2 = await new Promise((resolve3, reject) => {
3162
+ ssh.once("error", (err) => reject(new KvboxError(`could not run ssh: ${err.message}`)));
3163
+ ssh.once("close", (code, signal) => {
3164
+ tar.stdout.unpipe(ssh.stdin);
3165
+ tar.stdout.resume();
3166
+ tar.kill();
3167
+ resolve3(code ?? (signal === "SIGINT" ? 130 : 1));
3168
+ });
3169
+ });
3170
+ await tarDone;
3171
+ return { status: status2, output: Buffer.concat(chunks).toString("utf8") };
3172
+ }
3173
+ var shipping = { ship, measure, waitForSsh };
3174
+
3175
+ // src/commands/deploy/index.ts
3176
+ var deployCommand = new Command49("deploy").description("put the app in this directory (React or Next.js) on a box with its own HTTPS address; redeploy by running it again").argument("[dir]", "the app's directory (default: the current one)").addOption(new Option5("-t, --template <template>", "what kind of app it is (detected from package.json when not given)").choices(TEMPLATE_IDS)).option("-n, --name <name>", "the box's name (default: the directory's name)").option("--sub <subdomain>", "its HTTPS name, https://<subdomain>.kvbox.app (default: the box's name)").addOption(new Option5("-s, --size <size>", "size of the box when it has to be made").choices(SIZE_NAMES)).option("-y, --yes", "ask nothing: take the defaults for whatever the flags and kvbox.json leave open").option("--json", "print the result as JSON").option("--org <slug>", "deploy into this org's boxes (saved to kvbox.json)").action(async (dirArg, opts, cmd) => {
3177
+ const dir = resolve2(dirArg ?? ".");
3178
+ const pkg = await readPackageJson(dir);
3179
+ const saved = await readConfig(dir);
3180
+ const globals = cmd.optsWithGlobals();
3181
+ const orgFlag = orgOption(globals);
3182
+ const org = orgFlag ?? saved?.org;
3183
+ const client = await clientFor({ ...globals.local ? { local: true } : {}, ...org !== void 0 ? { org } : {} });
3184
+ try {
3185
+ const cloud = client.target.kind === "cloud";
3186
+ if (!cloud && org !== void 0) throw new KvboxError("--org needs a control plane: kvbox login, or drop --org");
3187
+ const { config, template, fresh } = await settle(dir, pkg, saved, opts, orgFlag, cloud);
3188
+ if (cloud && config.subdomain !== void 0) await refuseOtherAddress(client, config);
3189
+ await writeConfig(dir, config);
3190
+ if (fresh) info(` ${color.dim(`saved to ${CONFIG_FILE}; next time kvbox deploy asks nothing`)}
3191
+ `);
3192
+ info(`${color.bold("Building")} ${color.cyan(config.name)} ${color.dim(`(${template.label})`)}`);
3193
+ const built = await template.build({ dir, pkg, dist: config.dist });
3194
+ if (built.dist !== void 0 && built.dist !== config.dist) await writeConfig(dir, { ...config, dist: built.dist });
3195
+ const size = await shipping.measure(built.outDir, built.exclude);
3196
+ info("");
3197
+ const { box: found, ref, created } = await findOrCreate(client, config);
3198
+ const cert = await ensureCert(client, void 0, void 0, { owner: found.owner });
3199
+ const wasRunning = !created && found.status === "running";
3200
+ const box = created || wasRunning ? found : await wakeUp(client, found, ref);
3201
+ const ep = sshEndpoint(box, client.target);
3202
+ if (!wasRunning) await spin(`waiting for ${ref} to accept SSH\u2026`, shipping.waitForSsh(ep));
3203
+ const shipIt = () => shipping.ship(ep, built.outDir, template.remote(built), built.exclude);
3204
+ const doing = built.dist === void 0 ? `shipping ${built.what} (${size.files} files, ${gb(size.bytes)}) to ${ref} and building there, a minute or two\u2026` : `shipping ${built.what} (${size.files} files, ${gb(size.bytes)}) to ${ref}\u2026`;
3205
+ let result = await spin(doing, shipIt);
3206
+ if (result.status === SSH_FAILED3 && await renewAfterRefusal(client, cert, box.owner)) {
3207
+ info("renewed your SSH certificate, trying again\u2026");
3208
+ result = await spin(doing, shipIt);
3209
+ }
3210
+ if (result.status === SSH_FAILED3) throw new KvboxError(`box ${ref}: could not reach it over SSH (still booting?)${result.output ? `
3211
+ ${result.output.trim()}` : ""}`, SSH_FAILED3);
3212
+ if (result.status === IMAGE_TOO_OLD) {
3213
+ throw new KvboxError(
3214
+ `box ${ref} runs an image too old for the ${template.label} template (base v0.8.0 or newer). Update the box (kvbox vm update ${ref}) once its host has the new image, or deploy under another name (--name) to get a fresh box`
3215
+ );
3216
+ }
3217
+ if (result.status !== 0) throw new KvboxError(`deploying to ${ref} failed (exit ${result.status}):
3218
+ ${result.output.trim()}`);
3219
+ let url = `http://${box.ip}`;
3220
+ if (cloud) {
3221
+ const got = await address(client, box, ref, config, template);
3222
+ url = got.url;
3223
+ if (got.subdomain !== config.subdomain) await writeConfig(dir, { ...config, subdomain: got.subdomain, ...built.dist !== void 0 && built.dist !== config.dist ? { dist: built.dist } : {} });
3224
+ }
3225
+ if (opts.json) {
3226
+ json({ box: ref, created, template: template.id, url, ...built.dist !== void 0 ? { dist: built.dist } : {}, files: size.files, bytes: size.bytes });
3227
+ return;
3228
+ }
3229
+ process.stdout.write(`${url}
3230
+ `);
3231
+ info(`${color.green("\u2713")} ${color.bold(config.name)} is live at ${color.bold(url)}`);
3232
+ info(` box ${ref}${created ? " (new)" : ""} \xB7 ${template.label} \xB7 ${size.files} files, ${gb(size.bytes)}`);
3233
+ info(` redeploy: kvbox deploy${dirArg !== void 0 ? ` ${dirArg}` : ""}`);
3234
+ } finally {
3235
+ await client.close();
3236
+ }
3237
+ });
3238
+ async function readPackageJson(dir) {
3239
+ try {
3240
+ return JSON.parse(await readFile7(resolve2(dir, "package.json"), "utf8"));
3241
+ } catch (err) {
3242
+ if (err.code === "ENOENT") return void 0;
3243
+ throw new KvboxError(`${resolve2(dir, "package.json")}: ${err.message}`);
3244
+ }
3245
+ }
3246
+ async function settle(dir, pkg, saved, opts, orgFlag, cloud) {
3247
+ const detected = detectTemplate(pkg);
3248
+ if (opts.name !== void 0 && !BOX_NAME_RE.test(opts.name)) throw new KvboxError(`--name ${opts.name}: ${NAME_RULE}`);
3249
+ if (opts.sub !== void 0) assertSubdomain(opts.sub);
3250
+ const renamed = saved !== void 0 && (opts.name !== void 0 && opts.name !== saved.name || orgFlag !== void 0 && orgFlag !== saved.org);
3251
+ const savedSub = renamed ? void 0 : saved?.subdomain;
3252
+ const defaults = {
3253
+ template: opts.template ?? saved?.template ?? detected?.id,
3254
+ name: opts.name ?? saved?.name ?? nameFromDir(basename(dir)),
3255
+ subdomain: opts.sub ?? savedSub,
3256
+ size: opts.size ?? saved?.size
3257
+ };
3258
+ const sizeFor = (t) => defaults.size ?? t?.size ?? "xs";
3259
+ const open = {
3260
+ template: opts.template === void 0 && saved?.template === void 0,
3261
+ name: opts.name === void 0 && saved?.name === void 0,
3262
+ subdomain: cloud && opts.sub === void 0 && savedSub === void 0,
3263
+ size: opts.size === void 0 && saved === void 0
3264
+ };
3265
+ const anyOpen = open.template || open.name || open.subdomain || open.size;
3266
+ const fresh = saved === void 0;
3267
+ if (anyOpen && !opts.yes && !interactive()) {
3268
+ const sub = defaults.subdomain ?? subdomainFor(defaults.name);
3269
+ throw new KvboxError(
3270
+ `kvbox deploy asks a few questions the first time, and this is not a terminal. Pass -y to take the defaults (${defaults.template ?? "template?"}, box ${defaults.name}${cloud ? `, https://${sub}.kvbox.app` : ""}, size ${sizeFor(defaults.template !== void 0 ? templateById(defaults.template) : void 0)}), or answer with --template, --name${cloud ? ", --sub" : ""} and --size`
3271
+ );
3272
+ }
3273
+ const ask = anyOpen && !opts.yes;
3274
+ if (ask && fresh) info(`${color.bold("kvbox deploy")} ${color.dim(dir)}
3275
+ `);
3276
+ let templateId = defaults.template;
3277
+ if (open.template && ask) {
3278
+ const initial = detected ? TEMPLATES.indexOf(detected) : 0;
3279
+ templateId = await select("Template", TEMPLATES.map((t) => ({ value: t.id, label: t.label, hint: t === detected ? `${t.hint} \xB7 detected` : t.hint })), initial);
3280
+ }
3281
+ if (templateId === void 0) throw new KvboxError(`could not tell what kind of app ${dir} is (no package.json of a Vite, Create React App or Next.js project): say it with --template ${TEMPLATE_IDS.join("|")}`);
3282
+ const template = templateById(templateId);
3283
+ let name = defaults.name;
3284
+ if (open.name && ask) name = await text("Box name", { default: defaults.name, validate: (a) => BOX_NAME_RE.test(a) ? void 0 : NAME_RULE });
3285
+ let subdomain = defaults.subdomain ?? (cloud ? subdomainFor(name) : void 0);
3286
+ if (open.subdomain && ask) {
3287
+ subdomain = await text("Address", {
3288
+ default: subdomainFor(name),
3289
+ validate: (a) => {
3290
+ const problem = subdomainProblem(a);
3291
+ return problem ? `that name ${problem}` : void 0;
3292
+ }
3293
+ });
3294
+ info(` ${color.dim(`https://${subdomain}.kvbox.app`)}`);
3295
+ }
3296
+ let size = sizeFor(template);
3297
+ if (open.size && ask) {
3298
+ size = await select("Box size", SIZE_NAMES.map((s) => ({ value: s, label: s, hint: `${SIZES[s].vcpu} vCPU, ${SIZES[s].memMib / 1024} GB RAM, ${SIZES[s].diskGib} GB disk` })), SIZE_NAMES.indexOf(template.size));
3299
+ }
3300
+ const org = orgFlag ?? saved?.org;
3301
+ const config = {
3302
+ template: template.id,
3303
+ name,
3304
+ ...org !== void 0 ? { org } : {},
3305
+ ...subdomain !== void 0 ? { subdomain } : {},
3306
+ size,
3307
+ ...saved?.dist !== void 0 ? { dist: saved.dist } : {}
3308
+ };
3309
+ return { config, template, fresh };
3310
+ }
3311
+ var NAME_RULE = "1-12 chars, lowercase letters, digits and '-', starting with a letter or digit";
3312
+ async function findOrCreate(client, config) {
3313
+ const user = me(client);
3314
+ const slug = config.org ?? user;
3315
+ const named = (await client.listMachines()).filter((b) => b.name === config.name);
3316
+ let box = slug === void 0 ? named[0] : named.find((b) => slugOf(b.owner) === slug);
3317
+ let created = false;
3318
+ if (!box) {
3319
+ box = await spin(`booting a new box ${config.name}\u2026`, client.createMachine({ name: config.name, ...config.size !== void 0 ? { size: config.size } : {}, autoHibernateSec: DEPLOY_AUTO_HIBERNATE_SEC }));
3320
+ created = true;
3321
+ info(`${color.green("\u2713")} box ${color.bold(config.name)} is up: ${box.vcpu} vCPU, ${gb(box.memMib * 1024 ** 2)} RAM, ${gb(box.diskBytes)} disk`);
3322
+ }
3323
+ return { box, ref: displayName(box, user), created };
3324
+ }
3325
+ async function existingAddress(client, config) {
3326
+ const slug = config.org ?? me(client);
3327
+ return (await client.listSubdomains()).find((s) => s.box === config.name && s.owner === slug);
3328
+ }
3329
+ async function refuseOtherAddress(client, config) {
3330
+ const have = await existingAddress(client, config);
3331
+ if (!have || have.subdomain === config.subdomain) return;
3332
+ const ref = config.org !== void 0 ? `${config.org}/${config.name}` : config.name;
3333
+ throw new KvboxError(
3334
+ `box ${ref} already answers at ${have.url}, not ${config.subdomain}. To keep the address: kvbox deploy --sub ${have.subdomain}. To move it: kvbox vm proxy release ${ref}, then deploy again`
3335
+ );
3336
+ }
3337
+ async function address(client, box, ref, config, template) {
3338
+ const have = (await client.listSubdomains()).find((s) => s.box === box.name && s.owner === slugOf(box.owner));
3339
+ if (have) {
3340
+ if (have.port !== template.port) await client.setSubdomainPort(ref, { port: template.port });
3341
+ return { url: have.url, subdomain: have.subdomain };
3342
+ }
3343
+ const wanted = config.subdomain ?? subdomainFor(config.name);
3344
+ try {
3345
+ const sub = await spin(`claiming https://${wanted}.kvbox.app\u2026`, client.claimSubdomain(ref, { name: wanted, port: template.port }));
3346
+ return { url: sub.url, subdomain: sub.subdomain };
3347
+ } catch (err) {
3348
+ if (!(err instanceof KvboxError)) throw err;
3349
+ throw new KvboxError(`${err.message}
3350
+ the app is on box ${ref} but has no address yet: pick another with kvbox deploy --sub <name> (saved to ${CONFIG_FILE})`);
3351
+ }
3352
+ }
3353
+
2780
3354
  // src/program.ts
2781
3355
  function buildProgram() {
2782
- const program = new Command49("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();
3356
+ const program = new Command50("kvbox").description("self-hosted Firecracker microVMs with Docker-like ergonomics").version(VERSION, "-v, --version").option("--local", "use this machine's kvbox-server over its socket, ignoring kvbox login").option("--org <slug>", "the org: where kvbox new creates, and which box a name means when it is not one of yours; before the command here, or after it").showHelpAfterError().enablePositionalOptions();
2783
3357
  program.addCommand(vmCommand);
2784
3358
  program.addCommand(snapshotsCommand);
2785
3359
  program.addCommand(serverCommand);
@@ -2789,6 +3363,7 @@ function buildProgram() {
2789
3363
  program.addCommand(orgCommand);
2790
3364
  program.addCommand(usageCommand2);
2791
3365
  program.addCommand(creditCommand);
3366
+ program.addCommand(deployCommand);
2792
3367
  for (const cmd of vmAliases) program.addCommand(cmd);
2793
3368
  return program;
2794
3369
  }