@kvbox/cli 0.5.5 → 0.5.7

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
@@ -81,11 +81,11 @@ function age(iso, now = Date.now()) {
81
81
  if (min < 60) return `${min}m ago`;
82
82
  const hr = Math.floor(min / 60);
83
83
  if (hr < 24) return `${hr}h ago`;
84
- const day2 = Math.floor(hr / 24);
85
- if (day2 < 30) return `${day2}d ago`;
86
- const month = Math.floor(day2 / 30);
84
+ const day3 = Math.floor(hr / 24);
85
+ if (day3 < 30) return `${day3}d ago`;
86
+ const month = Math.floor(day3 / 30);
87
87
  if (month < 12) return `${month}mo ago`;
88
- return `${Math.floor(day2 / 365)}y ago`;
88
+ return `${Math.floor(day3 / 365)}y ago`;
89
89
  }
90
90
  function duration(seconds) {
91
91
  const s = Math.max(0, Math.round(seconds));
@@ -147,7 +147,7 @@ async function spin(message, work, opts = {}) {
147
147
  }
148
148
 
149
149
  // src/program.ts
150
- import { Command as Command37 } from "commander";
150
+ import { Command as Command38 } from "commander";
151
151
 
152
152
  // ../shared/src/version.ts
153
153
  import { createRequire } from "module";
@@ -162,7 +162,7 @@ function versionOf(srcFileUrl, injected) {
162
162
  }
163
163
 
164
164
  // src/version.ts
165
- var VERSION = versionOf(import.meta.url, true ? "0.5.5" : void 0);
165
+ var VERSION = versionOf(import.meta.url, true ? "0.5.7" : void 0);
166
166
 
167
167
  // src/commands/vm/index.ts
168
168
  import { Command as Command15 } from "commander";
@@ -510,10 +510,48 @@ var MachineUsageSchema = z2.object({
510
510
  estimatedStops: z2.number().int(),
511
511
  historyFrom: z2.string().nullable()
512
512
  });
513
+ var UsageStatusSchema = z2.enum([...BoxStatusSchema.options, "removed"]);
514
+ var BoxUsageSchema = MachineUsageSchema.omit({ window: true, status: true }).extend({
515
+ /** The host's box id; a name removed and recreated appears twice, with two ids. */
516
+ id: z2.string(),
517
+ status: UsageStatusSchema,
518
+ /** Through a control plane: whether the owner slug is a user's or an org's (what `kvbox ls` shows). */
519
+ ownerKind: z2.enum(["user", "org"]).optional()
520
+ });
521
+ var UsageReportSchema = z2.object({
522
+ window: z2.object({ from: z2.string(), to: z2.string() }),
523
+ boxes: z2.array(BoxUsageSchema),
524
+ totals: z2.object({
525
+ seconds: MachineUsageSchema.shape.seconds,
526
+ vcpuHours: z2.number(),
527
+ ramGibHours: z2.number(),
528
+ diskGibHours: z2.number(),
529
+ cost: z2.object({ amount: z2.number(), currency: z2.string() }).optional()
530
+ })
531
+ });
532
+ var UsageEventRowSchema = z2.object({
533
+ id: z2.number().int(),
534
+ boxId: z2.string(),
535
+ owner: z2.string(),
536
+ name: z2.string(),
537
+ at: z2.string(),
538
+ state: z2.enum(["running", "standby", "hibernated", "stopped"]),
539
+ vcpu: z2.number().int(),
540
+ memMib: z2.number().int(),
541
+ diskGib: z2.number().int(),
542
+ by: z2.string()
543
+ });
544
+ var UsageEventsResponseSchema = z2.object({
545
+ ledger: z2.string().min(1),
546
+ events: z2.array(UsageEventRowSchema),
547
+ next: z2.number().int()
548
+ });
513
549
  var ApiErrorSchema = z2.object({
514
550
  error: z2.string()
515
551
  });
516
552
  var DEFAULT_CLOUD_URL = "https://api.kvbox.sh";
553
+ var ACTIVATED_BY = ["card", "admin"];
554
+ var ActivatedBySchema = z2.enum(ACTIVATED_BY);
517
555
  var CloudUserSchema = z2.object({
518
556
  /** The handle: what hosts file boxes under (`<username>@<control plane>`). Lowercase, no `@` or `/`. */
519
557
  username: z2.string(),
@@ -522,7 +560,14 @@ var CloudUserSchema = z2.object({
522
560
  /** Login identity, unique. */
523
561
  email: z2.string(),
524
562
  role: z2.enum(["admin", "user"]),
525
- createdAt: z2.string()
563
+ createdAt: z2.string(),
564
+ /**
565
+ * False until a card is on file or an admin activates the account (#74): then only `/me` and
566
+ * `/logout` answer. Admins are never gated. A control plane older than #74 does not send these and
567
+ * gates nobody, hence the defaults: a newer CLI must still parse its answers.
568
+ */
569
+ active: z2.boolean().default(true),
570
+ activatedBy: ActivatedBySchema.nullable().default(null)
526
571
  });
527
572
  var ORG_ROLES = ["owner", "admin", "member"];
528
573
  var OrgRoleSchema = z2.enum(ORG_ROLES);
@@ -530,7 +575,10 @@ var CloudOrgSchema = z2.object({
530
575
  slug: z2.string(),
531
576
  /** Display name; the slug never changes, this may. */
532
577
  name: z2.string(),
533
- createdAt: z2.string()
578
+ createdAt: z2.string(),
579
+ /** An org pays for itself (#74): off until it has a card or an admin activates it; its boxes are 402. Defaults as on CloudUser. */
580
+ active: z2.boolean().default(true),
581
+ activatedBy: ActivatedBySchema.nullable().default(null)
534
582
  });
535
583
  var OrgMembershipSchema = CloudOrgSchema.extend({ role: OrgRoleSchema });
536
584
  var OrgMemberSchema = z2.object({
@@ -551,6 +599,32 @@ var AddMemberRequestSchema = z2.object({
551
599
  });
552
600
  var SetRoleRequestSchema = z2.object({ role: OrgRoleSchema });
553
601
  var MeResponseSchema = CloudUserSchema.extend({ orgs: z2.array(OrgMembershipSchema) });
602
+ var BalanceSchema = z2.object({
603
+ slug: z2.string(),
604
+ balance: z2.number(),
605
+ currency: z2.string(),
606
+ topUps: z2.number(),
607
+ spent: z2.number(),
608
+ /** The first moment usage counts from: the slug's first top-up; null when it has none. */
609
+ since: z2.string().nullable(),
610
+ /** When the slug ran out and its boxes were hibernated (the `sweep-credit` job); null in good standing. Absent from a control plane older than this field. */
611
+ suspendedAt: z2.string().nullable().default(null)
612
+ });
613
+ var TopUpRequestSchema = z2.object({
614
+ slug: z2.string().min(1),
615
+ /** In the control plane's currency; negative corrects a mistake. */
616
+ amount: z2.number().finite(),
617
+ note: z2.string().max(200).optional()
618
+ });
619
+ var TopUpSchema = z2.object({
620
+ id: z2.string(),
621
+ slug: z2.string(),
622
+ amount: z2.number(),
623
+ currency: z2.string(),
624
+ source: z2.enum(["admin", "card", "trial"]),
625
+ note: z2.string().nullable(),
626
+ at: z2.string()
627
+ });
554
628
  var LoginRequestSchema = z2.object({
555
629
  email: z2.string().min(1),
556
630
  password: z2.string().min(1),
@@ -784,6 +858,18 @@ var ServerClient = class {
784
858
  const query = since === void 0 ? "" : `?since=${encodeURIComponent(since)}`;
785
859
  return this.call("GET", `/machines/${encodeURIComponent(name)}/usage${query}`, MachineUsageSchema);
786
860
  }
861
+ /** Every box of the caller over the window (#41 on a host, #42 through a control plane); dates as the user wrote them. */
862
+ usageReport(since, until) {
863
+ const q = new URLSearchParams();
864
+ if (since !== void 0) q.set("since", since);
865
+ if (until !== void 0) q.set("until", until);
866
+ const query = q.size === 0 ? "" : `?${q.toString()}`;
867
+ return this.call("GET", `/usage${query}`, UsageReportSchema);
868
+ }
869
+ /** The prepaid balance of the caller's slug, or of `--org`'s (#76); a control plane only. */
870
+ balance() {
871
+ return this.call("GET", "/billing/balance", BalanceSchema);
872
+ }
787
873
  deleteMachine(name) {
788
874
  return this.call("DELETE", `/machines/${encodeURIComponent(name)}`, z3.undefined());
789
875
  }
@@ -1343,9 +1429,10 @@ var usageCommand = new Command13("usage").description("how long a box ran, slept
1343
1429
  var day = (iso) => iso.slice(0, 10);
1344
1430
  var SYMBOLS = { EUR: "\u20AC", USD: "$", GBP: "\xA3" };
1345
1431
  function money(amount, currency) {
1346
- const n = amount.toFixed(2);
1432
+ const sign = amount < 0 ? "-" : "";
1433
+ const n = Math.abs(amount).toFixed(2);
1347
1434
  const sym = SYMBOLS[currency.toUpperCase()];
1348
- return sym === void 0 ? `${n} ${currency}` : `${sym}${n}`;
1435
+ return sym === void 0 ? `${sign}${n} ${currency}` : `${sign}${sym}${n}`;
1349
1436
  }
1350
1437
  function block(u) {
1351
1438
  const estimated = u.estimatedStops === 0 ? "" : color.dim(` (${u.estimatedStops} stop time${u.estimatedStops === 1 ? "" : "s"} estimated)`);
@@ -1925,9 +2012,83 @@ var roleCommand = new Command35("role").description("change a member's role (own
1925
2012
  // src/commands/org/index.ts
1926
2013
  var orgCommand = new Command36("org").description("orgs: shared slugs that own boxes, with owners, admins and members").addCommand(lsCommand3).addCommand(newCommand3).addCommand(membersCommand).addCommand(roleCommand).addCommand(rmCommand4);
1927
2014
 
2015
+ // src/commands/usage.ts
2016
+ import { Command as Command37 } from "commander";
2017
+ var usageCommand2 = new Command37("usage").description("what your boxes used this month (or --since/--until): running time, vCPU-h, RAM and disk GiB-h, cost").option("--since <date>", "count from this date, e.g. 2026-10-01 (default: the 1st of this month)").option("--until <date>", "count up to this date (default: now)").option("--org <slug>", "one org's boxes only").option("--json", "print as JSON").action(async (opts, cmd) => {
2018
+ for (const [name, value] of [
2019
+ ["since", opts.since],
2020
+ ["until", opts.until]
2021
+ ]) {
2022
+ if (value !== void 0 && Number.isNaN(Date.parse(value))) throw new KvboxError(`--${name} ${value}: not a date; write it like 2026-10-19 or 2026-10-19T08:00:00Z`);
2023
+ }
2024
+ const client = await clientFor(cmd.optsWithGlobals());
2025
+ try {
2026
+ if (client.target.kind === "local" && cmd.optsWithGlobals().org) throw new KvboxError("--org needs a control plane: kvbox login, or drop --org");
2027
+ const report = await client.usageReport(opts.since, opts.until);
2028
+ if (opts.json) {
2029
+ json(report);
2030
+ return;
2031
+ }
2032
+ const cloud = client.target.kind === "cloud" ? { me: client.target.user } : void 0;
2033
+ info(`${day2(report.window.from)} to ${day2(report.window.to)}`);
2034
+ if (report.boxes.length === 0) info("no usage in this window");
2035
+ else table(usageRows(report, cloud));
2036
+ if (cloud) {
2037
+ const line = await creditLine(client, cloud.me);
2038
+ if (line !== void 0) info(line);
2039
+ }
2040
+ } finally {
2041
+ await client.close();
2042
+ }
2043
+ });
2044
+ var day2 = (iso) => iso.slice(0, 10);
2045
+ async function creditLine(client, me2) {
2046
+ let b;
2047
+ try {
2048
+ b = await client.balance();
2049
+ } catch (err) {
2050
+ if (err instanceof KvboxError) return void 0;
2051
+ throw err;
2052
+ }
2053
+ const whose = b.slug === me2 ? "private" : b.slug;
2054
+ const left = money(b.balance, b.currency);
2055
+ const line = `credit: ${b.balance > 0 ? left : color.red(left)} left ${color.dim(`(${whose})`)}`;
2056
+ return b.suspendedAt ? `${line} ${color.red("out of credit: boxes hibernated until you top up")}` : line;
2057
+ }
2058
+ function ownerCell2(b, me2) {
2059
+ const slug = slugOf(b.owner);
2060
+ if (slug === me2) return "private";
2061
+ return b.ownerKind === void 0 ? slug : `${slug} ${color.dim(b.ownerKind)}`;
2062
+ }
2063
+ function usageRows(report, cloud) {
2064
+ const priced = report.totals.cost !== void 0;
2065
+ const rows = report.boxes.map((b) => ({
2066
+ ...cloud ? { owner: ownerCell2(b, cloud.me) } : {},
2067
+ name: boxName(b.name),
2068
+ status: b.status === "removed" ? color.dim("removed") : status(b.status),
2069
+ running: duration(b.seconds.running),
2070
+ "vcpu-h": hours(b.vcpuHours),
2071
+ "ram GiB-h": hours(b.ramGibHours),
2072
+ "disk GiB-h": hours(b.diskGibHours),
2073
+ ...priced ? { cost: money(b.cost?.amount ?? 0, report.totals.cost.currency) } : {}
2074
+ }));
2075
+ const t = report.totals;
2076
+ rows.push({
2077
+ ...cloud ? { owner: "" } : {},
2078
+ name: color.bold("total"),
2079
+ status: "",
2080
+ running: duration(t.seconds.running),
2081
+ "vcpu-h": hours(t.vcpuHours),
2082
+ "ram GiB-h": hours(t.ramGibHours),
2083
+ "disk GiB-h": hours(t.diskGibHours),
2084
+ ...priced ? { cost: `${money(t.cost.amount, t.cost.currency)} ${color.dim("(estimated)")}` } : {}
2085
+ });
2086
+ return rows;
2087
+ }
2088
+
1928
2089
  // src/program.ts
1929
2090
  function buildProgram() {
1930
- const program = new Command37("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();
2091
+ const program = new Command38("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();
1931
2092
  program.addCommand(vmCommand);
1932
2093
  program.addCommand(snapshotsCommand);
1933
2094
  program.addCommand(serverCommand);
@@ -1935,6 +2096,7 @@ function buildProgram() {
1935
2096
  program.addCommand(logoutCommand);
1936
2097
  program.addCommand(whoamiCommand);
1937
2098
  program.addCommand(orgCommand);
2099
+ program.addCommand(usageCommand2);
1938
2100
  for (const cmd of vmAliases) program.addCommand(cmd);
1939
2101
  return program;
1940
2102
  }