@kvbox/cli 0.5.25 → 0.5.26

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
@@ -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.25" : void 0);
167
+ var VERSION = versionOf(import.meta.url, true ? "0.5.26" : void 0);
168
168
 
169
169
  // src/commands/vm/index.ts
170
170
  import { Command as Command26 } from "commander";
@@ -193,6 +193,8 @@ function paths(home = kvboxHome()) {
193
193
  serverLog: join(home, "server.log"),
194
194
  /** The CLI's login to the control plane (`kvbox login`): url, bearer token, user. Mode 600. */
195
195
  cli: join(home, "cli.json"),
196
+ /** The CLI's last look at npm for a newer @kvbox/cli: `{checkedAt, latest}`, at most once a day (cli update.ts). */
197
+ updateCheck: join(home, "update-check.json"),
196
198
  ssh: join(home, "ssh"),
197
199
  sshKey: join(home, "ssh", "id_ed25519"),
198
200
  /** The device key signed by the control plane's CA (#51); ssh loads it by itself next to `-i id_ed25519`. */
@@ -495,6 +497,8 @@ var VolumeSchema = z2.object({
495
497
  ownerKind: z2.enum(["user", "org"]).optional()
496
498
  });
497
499
  var MAX_VOLUME_GIB = 4096;
500
+ var VOLUME_NAME_RE = /^[a-z0-9][a-z0-9-]{0,11}$/;
501
+ var VOLUME_NAME_RULE = "1-12 chars, lowercase letters, digits and '-', starting with a letter or digit";
498
502
  var CreateVolumeRequest = z2.object({
499
503
  name: z2.string(),
500
504
  sizeGib: z2.number().int().min(1).max(MAX_VOLUME_GIB),
@@ -825,10 +829,26 @@ var TopUpSchema = z2.object({
825
829
  slug: z2.string(),
826
830
  amount: z2.number(),
827
831
  currency: z2.string(),
828
- source: z2.enum(["admin", "card", "trial"]),
832
+ /** `transfer` (#93): an owner moving credit to an org they own, a negative row on their slug and a positive one on the org's. */
833
+ source: z2.enum(["admin", "card", "trial", "transfer"]),
829
834
  note: z2.string().nullable(),
830
835
  at: z2.string()
831
836
  });
837
+ var TOP_UP_AMOUNTS = [10, 25, 50, 100];
838
+ var BillingInfoSchema = z2.object({
839
+ card: z2.object({ brand: z2.string(), last4: z2.string() }).nullable(),
840
+ cardInUse: z2.boolean(),
841
+ balance: BalanceSchema
842
+ });
843
+ var BillingRedirectSchema = z2.object({ url: z2.string() });
844
+ var TopUpCheckoutRequestSchema = z2.object({
845
+ amount: z2.literal(TOP_UP_AMOUNTS)
846
+ });
847
+ var TransferRequestSchema = z2.object({
848
+ org: z2.string().min(1),
849
+ amount: z2.number().positive().multipleOf(0.01, "amount is in cents at most")
850
+ });
851
+ var TransferResponseSchema = z2.object({ from: BalanceSchema, to: BalanceSchema, activated: z2.boolean() });
832
852
  var LoginRequestSchema = z2.object({
833
853
  email: z2.string().min(1),
834
854
  password: z2.string().min(1),
@@ -1381,6 +1401,199 @@ async function serving(p, timeoutMs, check) {
1381
1401
  throw new KvboxError(`kvbox-server did not start within ${timeoutMs / 1e3}s; see ${p.serverLog}`);
1382
1402
  }
1383
1403
 
1404
+ // src/prompt.ts
1405
+ import { createInterface } from "readline/promises";
1406
+ function interactive() {
1407
+ return process.stdin.isTTY === true && process.stderr.isTTY === true;
1408
+ }
1409
+ var ESC = "\x1B";
1410
+ var ENTER = "\r";
1411
+ var CTRL_C = "";
1412
+ var CTRL_D = "";
1413
+ var cancelled = () => new KvboxError("cancelled", 130);
1414
+ function keysIn(chunk) {
1415
+ const keys = [];
1416
+ for (let i = 0; i < chunk.length; ) {
1417
+ if (chunk[i] === ESC && chunk[i + 1] === "[" && i + 2 < chunk.length) {
1418
+ let j = i + 2;
1419
+ while (j < chunk.length && /[0-9;?]/.test(chunk[j])) j += 1;
1420
+ const end = j < chunk.length ? j + 1 : j;
1421
+ keys.push(chunk.slice(i, end));
1422
+ i = end;
1423
+ } else if (chunk[i] === ESC && chunk[i + 1] === "O" && i + 2 < chunk.length) {
1424
+ keys.push(chunk.slice(i, i + 3));
1425
+ i += 3;
1426
+ } else {
1427
+ keys.push(chunk[i]);
1428
+ i += 1;
1429
+ }
1430
+ }
1431
+ return keys;
1432
+ }
1433
+ var isUp = (k) => k === `${ESC}[A` || k === `${ESC}OA` || k === "k";
1434
+ var isDown = (k) => k === `${ESC}[B` || k === `${ESC}OB` || k === "j";
1435
+ var ANSI2 = /\x1b\[[0-9;]*[A-Za-z]/g;
1436
+ function rows(line, columns = process.stderr.columns ?? 80) {
1437
+ const width2 = line.replace(ANSI2, "").length;
1438
+ return Math.max(1, Math.ceil(width2 / Math.max(1, columns)));
1439
+ }
1440
+ function settled(message, answer) {
1441
+ return `${color.green("\u2713")} ${color.bold(message)} ${color.cyan(answer)}`;
1442
+ }
1443
+ async function select(message, choices, initial = 0) {
1444
+ const out = process.stderr;
1445
+ const inp = process.stdin;
1446
+ let index = Math.min(Math.max(initial, 0), choices.length - 1);
1447
+ let drawn = 0;
1448
+ const render = () => {
1449
+ if (drawn > 0) out.write(`${ESC}[${drawn}A`);
1450
+ const lines = [
1451
+ `${color.green("?")} ${color.bold(message)} ${color.dim("\u2191\u2193 to move, enter to pick")}`,
1452
+ ...choices.map((c, i) => {
1453
+ const hint = c.hint ? color.dim(` ${c.hint}`) : "";
1454
+ return i === index ? `${color.cyan(`\u276F ${c.label}`)}${hint}` : ` ${c.label}${hint}`;
1455
+ })
1456
+ ];
1457
+ out.write(`\r${ESC}[J${lines.join("\n")}
1458
+ `);
1459
+ drawn = lines.reduce((n, l) => n + rows(l), 0);
1460
+ };
1461
+ inp.setRawMode(true);
1462
+ inp.resume();
1463
+ inp.setEncoding("utf8");
1464
+ render();
1465
+ let done = false;
1466
+ try {
1467
+ return await new Promise((resolve3, reject) => {
1468
+ const onData = (chunk) => {
1469
+ for (const key of keysIn(chunk)) {
1470
+ if (isUp(key)) index = (index - 1 + choices.length) % choices.length;
1471
+ else if (isDown(key)) index = (index + 1) % choices.length;
1472
+ else if (key === ENTER || key === "\n") {
1473
+ inp.off("data", onData);
1474
+ done = true;
1475
+ resolve3(choices[index].value);
1476
+ return;
1477
+ } else if (key === CTRL_C || key === CTRL_D) {
1478
+ inp.off("data", onData);
1479
+ reject(cancelled());
1480
+ return;
1481
+ } else continue;
1482
+ render();
1483
+ }
1484
+ };
1485
+ inp.on("data", onData);
1486
+ });
1487
+ } finally {
1488
+ inp.setRawMode(false);
1489
+ inp.pause();
1490
+ out.write(`${ESC}[${drawn}A\r${ESC}[J${done ? `${settled(message, choices[index].label)}
1491
+ ` : ""}`);
1492
+ }
1493
+ }
1494
+ async function text(message, opts = {}) {
1495
+ const out = process.stderr;
1496
+ for (; ; ) {
1497
+ const hint = opts.default !== void 0 ? ` ${color.dim(`(${opts.default})`)}` : "";
1498
+ const question = `${color.green("?")} ${color.bold(message)}${hint} ${color.dim("\u203A")} `;
1499
+ const rl = createInterface({ input: process.stdin, output: out });
1500
+ const line = await askLine(rl, question);
1501
+ const raw = opts.trim === false ? line : line.trim();
1502
+ const answer = raw === "" && opts.default !== void 0 ? opts.default : raw;
1503
+ const problem = opts.validate?.(answer);
1504
+ out.write(`${ESC}[${rows(question + raw)}A\r${ESC}[J`);
1505
+ if (problem === void 0) {
1506
+ out.write(`${settled(message, answer)}
1507
+ `);
1508
+ return answer;
1509
+ }
1510
+ out.write(`${color.yellow("!")} ${problem}
1511
+ `);
1512
+ }
1513
+ }
1514
+ async function hiddenText(message) {
1515
+ const out = process.stderr;
1516
+ const inp = process.stdin;
1517
+ const question = `${color.green("?")} ${color.bold(message)} ${color.dim("(hidden) \u203A")} `;
1518
+ out.write(question);
1519
+ inp.setRawMode(true);
1520
+ inp.resume();
1521
+ inp.setEncoding("utf8");
1522
+ let chars = [];
1523
+ let done = false;
1524
+ try {
1525
+ return await new Promise((resolve3, reject) => {
1526
+ const onData = (chunk) => {
1527
+ for (const key of keysIn(chunk)) {
1528
+ if (key === ENTER || key === "\n") {
1529
+ inp.off("data", onData);
1530
+ done = true;
1531
+ resolve3(chars.join(""));
1532
+ return;
1533
+ }
1534
+ if (key === CTRL_C || key === CTRL_D) {
1535
+ inp.off("data", onData);
1536
+ reject(cancelled());
1537
+ return;
1538
+ }
1539
+ if (key === "\x7F" || key === "\b") chars = chars.slice(0, -1);
1540
+ else if (key.length === 1 && (key >= " " || key === " ")) chars.push(key);
1541
+ }
1542
+ };
1543
+ inp.on("data", onData);
1544
+ });
1545
+ } finally {
1546
+ inp.setRawMode(false);
1547
+ inp.pause();
1548
+ out.write(`\r${ESC}[J${done ? `${settled(message, "\u2022\u2022\u2022\u2022\u2022")}
1549
+ ` : "\n"}`);
1550
+ }
1551
+ }
1552
+ async function askLine(rl, question) {
1553
+ let onClose = () => {
1554
+ };
1555
+ const closed = new Promise((_, reject) => {
1556
+ onClose = () => reject(cancelled());
1557
+ rl.once("close", onClose);
1558
+ });
1559
+ try {
1560
+ return await Promise.race([rl.question(question).catch(abortIsCancel), closed]);
1561
+ } finally {
1562
+ rl.off("close", onClose);
1563
+ rl.close();
1564
+ }
1565
+ }
1566
+ function abortIsCancel(err) {
1567
+ if (err.code === "ABORT_ERR") throw cancelled();
1568
+ throw err;
1569
+ }
1570
+
1571
+ // src/pick.ts
1572
+ function askOrUsage(usage) {
1573
+ if (!interactive()) throw new KvboxError(`${usage} (or run it in a terminal to be asked)`);
1574
+ }
1575
+ function refOf(item, user) {
1576
+ return user !== void 0 && slugOf(item.owner) !== user ? `${slugOf(item.owner)}/${item.name}` : item.name;
1577
+ }
1578
+ async function pickBox(client, message = "Box", extra = []) {
1579
+ const user = me(client);
1580
+ const boxes = await spin("listing your boxes\u2026", client.listMachines());
1581
+ const choices = boxes.map((b) => ({ value: refOf(b, user), label: refOf(b, user), hint: b.status }));
1582
+ if (choices.length === 0 && extra.length === 0) throw new KvboxError("you have no boxes yet: make one with kvbox new");
1583
+ return select(message, [...choices, ...extra]);
1584
+ }
1585
+ async function pickVolume(client, keep, none) {
1586
+ const user = me(client);
1587
+ const volumes = (await spin("listing your volumes\u2026", client.listVolumes())).filter(keep);
1588
+ if (volumes.length === 0) throw new KvboxError(none);
1589
+ const refs = volumes.map((v) => refOf(v, user));
1590
+ const ref = await select(
1591
+ "Volume",
1592
+ volumes.map((v, i) => ({ value: refs[i], label: refs[i], hint: `${v.sizeGib} GB, ${v.attachedTo === null ? "not attached" : `on ${v.attachedTo}`}` }))
1593
+ );
1594
+ return { ...volumes[refs.indexOf(ref)], ref };
1595
+ }
1596
+
1384
1597
  // src/commands/vm/new.ts
1385
1598
  import { Command, Option } from "commander";
1386
1599
  var newCommand = new Command("new").description("boot a new box from the base image or a snapshot").argument("[name]", "box name (box1, box2, \u2026 when omitted)").addOption(new Option("-s, --size <size>", "box size (default: server config, s)").choices(SIZE_NAMES)).option("-f, --from <snapshot>", "start from a snapshot instead of the base image").option("--auto-hibernate <duration>", "hibernate the box after this long without network traffic (30m, 4h, 1d; off: never; default: server config, off)").option("--force", "skip the capacity guard").option("--json", "print the box as JSON").action(async (name, opts, cmd) => {
@@ -1433,12 +1646,14 @@ function parseIdleTimeout(text2) {
1433
1646
 
1434
1647
  // src/commands/vm/connect.ts
1435
1648
  var SSH_FAILED = 255;
1436
- var connectCommand = new Command2("connect").alias("cn").description("open a shell in a box over SSH (or run a command: kvbox connect <name> -- <cmd>)").argument("<name>", "box name").argument("[cmd...]", "command to run instead of a shell").passThroughOptions().action(async (name, cmdArgs, _opts, cmd) => {
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) => {
1650
+ if (nameArg === void 0) askOrUsage("give the box: kvbox connect <name>");
1437
1651
  const client = await clientFor(cmd.optsWithGlobals());
1438
1652
  let ep;
1439
1653
  let cert;
1440
1654
  let owner;
1441
1655
  try {
1656
+ const name = nameArg ?? await pickBox(client);
1442
1657
  let box = pickRef(await boxesFor(client, name), name, me(client));
1443
1658
  if (!box) throw new KvboxError(`box ${name}: not found (kvbox ls)`);
1444
1659
  cert = await ensureCert(client, void 0, void 0, { owner: box.owner });
@@ -1793,12 +2008,17 @@ function assertRouteName(text2) {
1793
2008
  }
1794
2009
 
1795
2010
  // src/commands/vm/proxy/claim.ts
1796
- var claimCommand = new Command9("claim").description("give a box its HTTPS address, https://<subdomain>.kvbox.app, serving one of its ports").argument("<box>", "box name").argument("<subdomain>", "3-63 lowercase letters, digits and '-'; must be free").requiredOption("--port <port>", "the box port the address serves").option("--json", "print the subdomain as JSON").action(async (name, subdomain, opts, cmd) => {
1797
- assertSubdomain(subdomain);
1798
- const [port] = parsePorts([opts.port]);
2011
+ 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) => {
2012
+ if (nameArg === void 0 || subArg === void 0 || opts.port === void 0) askOrUsage("give the box, the subdomain and the port: kvbox vm proxy claim <box> <subdomain> --port <port>");
2013
+ if (subArg !== void 0) assertSubdomain(subArg);
2014
+ if (opts.port !== void 0) parsePorts([opts.port]);
1799
2015
  const client = await clientFor(cmd.optsWithGlobals());
1800
2016
  try {
1801
2017
  requireCloud(client);
2018
+ const name = nameArg ?? await pickBox(client);
2019
+ const subdomain = subArg ?? await text("Subdomain", { validate: (a) => subdomainProblem(a) ?? void 0 });
2020
+ const portText = opts.port ?? await text("Port inside the box", { default: "3000", validate: (a) => /^\d+$/.test(a) && +a >= 1 && +a <= 65535 ? void 0 : "a port is 1-65535" });
2021
+ const [port] = parsePorts([portText]);
1802
2022
  const user = me(client);
1803
2023
  const found = await findBox(client, name);
1804
2024
  const box = displayName(found, user);
@@ -2438,175 +2658,6 @@ import { hostname as osHostname } from "os";
2438
2658
  import { createInterface as createInterface2 } from "readline/promises";
2439
2659
  import { Writable } from "stream";
2440
2660
  import { Command as Command35 } from "commander";
2441
-
2442
- // src/prompt.ts
2443
- import { createInterface } from "readline/promises";
2444
- function interactive() {
2445
- return process.stdin.isTTY === true && process.stderr.isTTY === true;
2446
- }
2447
- var ESC = "\x1B";
2448
- var ENTER = "\r";
2449
- var CTRL_C = "";
2450
- var CTRL_D = "";
2451
- var cancelled = () => new KvboxError("cancelled", 130);
2452
- function keysIn(chunk) {
2453
- const keys = [];
2454
- for (let i = 0; i < chunk.length; ) {
2455
- if (chunk[i] === ESC && chunk[i + 1] === "[" && i + 2 < chunk.length) {
2456
- let j = i + 2;
2457
- while (j < chunk.length && /[0-9;?]/.test(chunk[j])) j += 1;
2458
- const end = j < chunk.length ? j + 1 : j;
2459
- keys.push(chunk.slice(i, end));
2460
- i = end;
2461
- } else if (chunk[i] === ESC && chunk[i + 1] === "O" && i + 2 < chunk.length) {
2462
- keys.push(chunk.slice(i, i + 3));
2463
- i += 3;
2464
- } else {
2465
- keys.push(chunk[i]);
2466
- i += 1;
2467
- }
2468
- }
2469
- return keys;
2470
- }
2471
- var isUp = (k) => k === `${ESC}[A` || k === `${ESC}OA` || k === "k";
2472
- var isDown = (k) => k === `${ESC}[B` || k === `${ESC}OB` || k === "j";
2473
- var ANSI2 = /\x1b\[[0-9;]*[A-Za-z]/g;
2474
- function rows(line, columns = process.stderr.columns ?? 80) {
2475
- const width2 = line.replace(ANSI2, "").length;
2476
- return Math.max(1, Math.ceil(width2 / Math.max(1, columns)));
2477
- }
2478
- function settled(message, answer) {
2479
- return `${color.green("\u2713")} ${color.bold(message)} ${color.cyan(answer)}`;
2480
- }
2481
- async function select(message, choices, initial = 0) {
2482
- const out = process.stderr;
2483
- const inp = process.stdin;
2484
- let index = Math.min(Math.max(initial, 0), choices.length - 1);
2485
- let drawn = 0;
2486
- const render = () => {
2487
- if (drawn > 0) out.write(`${ESC}[${drawn}A`);
2488
- const lines = [
2489
- `${color.green("?")} ${color.bold(message)} ${color.dim("\u2191\u2193 to move, enter to pick")}`,
2490
- ...choices.map((c, i) => {
2491
- const hint = c.hint ? color.dim(` ${c.hint}`) : "";
2492
- return i === index ? `${color.cyan(`\u276F ${c.label}`)}${hint}` : ` ${c.label}${hint}`;
2493
- })
2494
- ];
2495
- out.write(`\r${ESC}[J${lines.join("\n")}
2496
- `);
2497
- drawn = lines.reduce((n, l) => n + rows(l), 0);
2498
- };
2499
- inp.setRawMode(true);
2500
- inp.resume();
2501
- inp.setEncoding("utf8");
2502
- render();
2503
- let done = false;
2504
- try {
2505
- return await new Promise((resolve3, reject) => {
2506
- const onData = (chunk) => {
2507
- for (const key of keysIn(chunk)) {
2508
- if (isUp(key)) index = (index - 1 + choices.length) % choices.length;
2509
- else if (isDown(key)) index = (index + 1) % choices.length;
2510
- else if (key === ENTER || key === "\n") {
2511
- inp.off("data", onData);
2512
- done = true;
2513
- resolve3(choices[index].value);
2514
- return;
2515
- } else if (key === CTRL_C || key === CTRL_D) {
2516
- inp.off("data", onData);
2517
- reject(cancelled());
2518
- return;
2519
- } else continue;
2520
- render();
2521
- }
2522
- };
2523
- inp.on("data", onData);
2524
- });
2525
- } finally {
2526
- inp.setRawMode(false);
2527
- inp.pause();
2528
- out.write(`${ESC}[${drawn}A\r${ESC}[J${done ? `${settled(message, choices[index].label)}
2529
- ` : ""}`);
2530
- }
2531
- }
2532
- async function text(message, opts = {}) {
2533
- const out = process.stderr;
2534
- for (; ; ) {
2535
- const hint = opts.default !== void 0 ? ` ${color.dim(`(${opts.default})`)}` : "";
2536
- const question = `${color.green("?")} ${color.bold(message)}${hint} ${color.dim("\u203A")} `;
2537
- const rl = createInterface({ input: process.stdin, output: out });
2538
- const line = await askLine(rl, question);
2539
- const raw = opts.trim === false ? line : line.trim();
2540
- const answer = raw === "" && opts.default !== void 0 ? opts.default : raw;
2541
- const problem = opts.validate?.(answer);
2542
- out.write(`${ESC}[${rows(question + raw)}A\r${ESC}[J`);
2543
- if (problem === void 0) {
2544
- out.write(`${settled(message, answer)}
2545
- `);
2546
- return answer;
2547
- }
2548
- out.write(`${color.yellow("!")} ${problem}
2549
- `);
2550
- }
2551
- }
2552
- async function hiddenText(message) {
2553
- const out = process.stderr;
2554
- const inp = process.stdin;
2555
- const question = `${color.green("?")} ${color.bold(message)} ${color.dim("(hidden) \u203A")} `;
2556
- out.write(question);
2557
- inp.setRawMode(true);
2558
- inp.resume();
2559
- inp.setEncoding("utf8");
2560
- let chars = [];
2561
- let done = false;
2562
- try {
2563
- return await new Promise((resolve3, reject) => {
2564
- const onData = (chunk) => {
2565
- for (const key of keysIn(chunk)) {
2566
- if (key === ENTER || key === "\n") {
2567
- inp.off("data", onData);
2568
- done = true;
2569
- resolve3(chars.join(""));
2570
- return;
2571
- }
2572
- if (key === CTRL_C || key === CTRL_D) {
2573
- inp.off("data", onData);
2574
- reject(cancelled());
2575
- return;
2576
- }
2577
- if (key === "\x7F" || key === "\b") chars = chars.slice(0, -1);
2578
- else if (key.length === 1 && (key >= " " || key === " ")) chars.push(key);
2579
- }
2580
- };
2581
- inp.on("data", onData);
2582
- });
2583
- } finally {
2584
- inp.setRawMode(false);
2585
- inp.pause();
2586
- out.write(`\r${ESC}[J${done ? `${settled(message, "\u2022\u2022\u2022\u2022\u2022")}
2587
- ` : "\n"}`);
2588
- }
2589
- }
2590
- async function askLine(rl, question) {
2591
- let onClose = () => {
2592
- };
2593
- const closed = new Promise((_, reject) => {
2594
- onClose = () => reject(cancelled());
2595
- rl.once("close", onClose);
2596
- });
2597
- try {
2598
- return await Promise.race([rl.question(question).catch(abortIsCancel), closed]);
2599
- } finally {
2600
- rl.off("close", onClose);
2601
- rl.close();
2602
- }
2603
- }
2604
- function abortIsCancel(err) {
2605
- if (err.code === "ABORT_ERR") throw cancelled();
2606
- throw err;
2607
- }
2608
-
2609
- // src/commands/login.ts
2610
2661
  var loginCommand = new Command35("login").description("log in to the kvbox cloud (or your own control plane) and target it from now on").argument("[url]", `control plane URL (default: ${DEFAULT_CLOUD_URL})`).option("-e, --email <email>", "account email (prompted otherwise)").option("--password-stdin", "read the password from the first line of stdin (for scripts)").action(async (urlArg, opts) => {
2611
2662
  const url = normalizeUrl(urlArg ?? DEFAULT_CLOUD_URL);
2612
2663
  const creds = await readCredentials(opts);
@@ -3643,9 +3694,12 @@ function describeMount(machine, volume, box) {
3643
3694
  const path = machine.volumes?.find((v) => volume === v.name || volume.endsWith(`/${v.name}`))?.mountPath ?? volumeMountPath(volume.slice(volume.lastIndexOf("/") + 1));
3644
3695
  return machine.status === "running" ? ` the box was restarted; the volume is at ${path} inside it` : ` box ${box} is ${machine.status}; the volume is at ${path} once it runs`;
3645
3696
  }
3646
- var attachCommand = new Command51("attach").description("attach a volume to a box at /mnt/<volume> (a running box is restarted to take it)").argument("<volume>", "volume name").argument("<box>", "box name").option("--json", "print the box as JSON").action(async (volume, box, opts, cmd) => {
3697
+ var attachCommand = new Command51("attach").description("attach a volume to a box at /mnt/<volume> (a running box is restarted to take it)").argument("[volume]", "volume name (picked from a list when left out)").argument("[box]", "box name (picked from a list when left out)").option("--json", "print the box as JSON").action(async (volumeArg, boxArg, opts, cmd) => {
3698
+ if (volumeArg === void 0 || boxArg === void 0) askOrUsage("give the volume and the box: kvbox volume attach <volume> <box>");
3647
3699
  const client = await clientFor(cmd.optsWithGlobals());
3648
3700
  try {
3701
+ const volume = volumeArg ?? (await pickVolume(client, (v) => v.attachedTo === null, "no volume is free to attach (kvbox volume ls); make one with kvbox volume new")).ref;
3702
+ const box = boxArg ?? await pickBox(client);
3649
3703
  const machine = await spin(`attaching volume ${volume} to box ${box} (a running box is restarted)\u2026`, client.attachVolume(volume, { box }));
3650
3704
  if (opts.json) {
3651
3705
  json(machine);
@@ -3667,10 +3721,26 @@ function parseGib2(text2) {
3667
3721
  if (gib > MAX_VOLUME_GIB) throw new KvboxError(`--size ${text2}: a volume is at most ${MAX_VOLUME_GIB} GB`);
3668
3722
  return gib;
3669
3723
  }
3670
- var createCommand = new Command52("create").alias("new").description("create a volume for a box: an empty disk of its own at /mnt/<name> inside it (a running box is restarted)").argument("<name>", "volume name (1-12 chars; the box mounts it at /mnt/<name>)").requiredOption("--size <gb>", "size in GB, like 50 or 50GB (whole GB; it only occupies what is written)").option("--box <box>", "the box it is for: made on that box's host and attached to it (required on kvbox.sh; optional with --local)").option("--json", "print the volume as JSON").action(async (name, opts, cmd) => {
3671
- const sizeGib = parseGib2(opts.size);
3724
+ function sizeProblem(answer) {
3725
+ try {
3726
+ parseGib2(answer);
3727
+ return void 0;
3728
+ } catch (err) {
3729
+ return err instanceof KvboxError ? err.message.replace(/^--size \S+: /, "") : String(err);
3730
+ }
3731
+ }
3732
+ var createCommand = new Command52("create").alias("new").description("create a volume for a box: an empty disk of its own at /mnt/<name> inside it (a running box is restarted)").argument("[name]", "volume name (1-12 chars; the box mounts it at /mnt/<name>; asked for when left out)").option("--size <gb>", "size in GB, like 50 or 50GB (whole GB; it only occupies what is written)").option("--box <box>", "the box it is for: made on that box's host and attached to it (required on kvbox.sh; optional with --local)").option("--json", "print the volume as JSON").action(async (nameArg, opts, cmd) => {
3733
+ const ask = interactive();
3734
+ if (nameArg === void 0 || opts.size === void 0) askOrUsage("give the name and the size: kvbox volume create <name> --size <gb> --box <box>");
3735
+ const name = nameArg ?? await text("Volume name", { validate: (a) => VOLUME_NAME_RE.test(a) ? void 0 : VOLUME_NAME_RULE });
3736
+ const sizeGib = opts.size !== void 0 ? parseGib2(opts.size) : parseGib2(await text("Size in GB", { default: "10", validate: sizeProblem }));
3672
3737
  const client = await clientFor(cmd.optsWithGlobals());
3673
3738
  try {
3739
+ if (opts.box === void 0 && ask) {
3740
+ const none = client.target.kind === "cloud" ? [] : [{ value: "", label: "none", hint: "attach it later with kvbox volume attach" }];
3741
+ const picked = await pickBox(client, "Box", none);
3742
+ if (picked !== "") opts.box = picked;
3743
+ }
3674
3744
  if (opts.box === void 0 && client.target.kind === "cloud") {
3675
3745
  throw new KvboxError(`a volume is made for a box, on that box's host: kvbox volume create ${name} --size ${sizeGib} --box <box>`);
3676
3746
  }
@@ -3693,9 +3763,19 @@ var createCommand = new Command52("create").alias("new").description("create a v
3693
3763
 
3694
3764
  // src/commands/volume/detach.ts
3695
3765
  import { Command as Command53 } from "commander";
3696
- var detachCommand = new Command53("detach").description("detach a volume from a box; its data is kept (a running box is restarted)").argument("<volume>", "volume name").argument("<box>", "box name").option("--json", "print the box as JSON").action(async (volume, box, opts, cmd) => {
3766
+ var detachCommand = new Command53("detach").description("detach a volume from a box; its data is kept (a running box is restarted)").argument("[volume]", "volume name (picked from a list when left out)").argument("[box]", "box name (the one the volume is on when left out)").option("--json", "print the box as JSON").action(async (volumeArg, boxArg, opts, cmd) => {
3767
+ if (volumeArg === void 0) askOrUsage("give the volume: kvbox volume detach <volume> [box]");
3697
3768
  const client = await clientFor(cmd.optsWithGlobals());
3698
3769
  try {
3770
+ let volume = volumeArg;
3771
+ let box = boxArg;
3772
+ if (volume === void 0 || box === void 0) {
3773
+ const found = volume === void 0 ? await pickVolume(client, (v) => v.attachedTo !== null, "no volume is attached to a box (kvbox volume ls)") : pickRef(await spin("finding the volume\u2026", client.listVolumes()), volume, me(client), "volume");
3774
+ if (!found) throw new KvboxError(`volume ${volume}: not found (kvbox volume ls)`);
3775
+ if (found.attachedTo === null) throw new KvboxError(`volume ${volume}: not attached to a box`);
3776
+ volume ??= refOf(found, me(client));
3777
+ box ??= refOf({ name: found.attachedTo, owner: found.owner }, me(client));
3778
+ }
3699
3779
  const machine = await spin(`detaching volume ${volume} from box ${box} (a running box is restarted)\u2026`, client.detachVolume(volume, { box }));
3700
3780
  if (opts.json) {
3701
3781
  json(machine);
@@ -3894,9 +3974,90 @@ function buildProgram() {
3894
3974
  return program;
3895
3975
  }
3896
3976
 
3977
+ // src/update.ts
3978
+ import { mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
3979
+ import { dirname } from "path";
3980
+ var REGISTRY_URL = "https://registry.npmjs.org/@kvbox/cli/latest";
3981
+ var CHECK_EVERY_MS = 24 * 60 * 60 * 1e3;
3982
+ var FETCH_TIMEOUT_MS = 1500;
3983
+ var UPDATE_COMMAND = "npm install -g @kvbox/cli";
3984
+ function isNewer(a, b) {
3985
+ const pa = parse(a);
3986
+ const pb = parse(b);
3987
+ if (!pa || !pb) return false;
3988
+ for (let i = 0; i < 3; i++) {
3989
+ if (pa[i] !== pb[i]) return pa[i] > pb[i];
3990
+ }
3991
+ return false;
3992
+ }
3993
+ function parse(v) {
3994
+ const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
3995
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
3996
+ }
3997
+ function readUpdateState(file = paths().updateCheck) {
3998
+ try {
3999
+ const raw = JSON.parse(readFileSync3(file, "utf8"));
4000
+ if (typeof raw.checkedAt !== "number" || typeof raw.latest !== "string") return null;
4001
+ return { checkedAt: raw.checkedAt, latest: raw.latest };
4002
+ } catch {
4003
+ return null;
4004
+ }
4005
+ }
4006
+ function writeUpdateState(state, file = paths().updateCheck) {
4007
+ try {
4008
+ mkdirSync(dirname(file), { recursive: true });
4009
+ writeFileSync(file, JSON.stringify(state) + "\n");
4010
+ } catch {
4011
+ }
4012
+ }
4013
+ async function fetchLatest(fetchImpl = fetch) {
4014
+ try {
4015
+ const res = await fetchImpl(REGISTRY_URL, {
4016
+ headers: { accept: "application/json" },
4017
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
4018
+ });
4019
+ if (!res.ok) return null;
4020
+ const body = await res.json();
4021
+ return typeof body.version === "string" ? body.version : null;
4022
+ } catch {
4023
+ return null;
4024
+ }
4025
+ }
4026
+ async function refreshUpdateState(opts = {}) {
4027
+ const file = opts.file ?? paths().updateCheck;
4028
+ const now = opts.now ?? Date.now();
4029
+ const cached = readUpdateState(file);
4030
+ if (cached && now - cached.checkedAt < CHECK_EVERY_MS) return cached;
4031
+ const latest = await fetchLatest(opts.fetchImpl);
4032
+ const state = { checkedAt: now, latest: latest ?? cached?.latest ?? VERSION };
4033
+ writeUpdateState(state, file);
4034
+ return state;
4035
+ }
4036
+ function updateNotice(latest, current = VERSION) {
4037
+ if (!isNewer(latest, current)) return null;
4038
+ return `
4039
+ A new version of kvbox is available (v${latest.replace(/^v/, "")}). Update with:
4040
+ ${color.bold(UPDATE_COMMAND)}
4041
+ `;
4042
+ }
4043
+ function enabled() {
4044
+ return process.stderr.isTTY === true && !process.env["CI"] && !process.env["KVBOX_NO_UPDATE_CHECK"];
4045
+ }
4046
+ function startUpdateCheck() {
4047
+ if (!enabled()) return Promise.resolve();
4048
+ process.on("exit", () => {
4049
+ const state = readUpdateState();
4050
+ const notice = state ? updateNotice(state.latest) : null;
4051
+ if (notice) process.stderr.write(notice);
4052
+ });
4053
+ return refreshUpdateState();
4054
+ }
4055
+
3897
4056
  // src/index.ts
4057
+ var updateCheck = startUpdateCheck();
3898
4058
  try {
3899
4059
  await buildProgram().parseAsync(process.argv);
4060
+ await updateCheck;
3900
4061
  } catch (err) {
3901
4062
  if (err instanceof KvboxError) {
3902
4063
  process.stderr.write(color.red(`error: ${err.message}`) + "\n");