@kvbox/cli 0.5.6 → 0.5.8

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
@@ -147,7 +147,7 @@ async function spin(message, work, opts = {}) {
147
147
  }
148
148
 
149
149
  // src/program.ts
150
- import { Command as Command38 } from "commander";
150
+ import { Command as Command39 } 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.6" : void 0);
165
+ var VERSION = versionOf(import.meta.url, true ? "0.5.8" : void 0);
166
166
 
167
167
  // src/commands/vm/index.ts
168
168
  import { Command as Command15 } from "commander";
@@ -550,6 +550,8 @@ var ApiErrorSchema = z2.object({
550
550
  error: z2.string()
551
551
  });
552
552
  var DEFAULT_CLOUD_URL = "https://api.kvbox.sh";
553
+ var ACTIVATED_BY = ["card", "admin"];
554
+ var ActivatedBySchema = z2.enum(ACTIVATED_BY);
553
555
  var CloudUserSchema = z2.object({
554
556
  /** The handle: what hosts file boxes under (`<username>@<control plane>`). Lowercase, no `@` or `/`. */
555
557
  username: z2.string(),
@@ -558,7 +560,14 @@ var CloudUserSchema = z2.object({
558
560
  /** Login identity, unique. */
559
561
  email: z2.string(),
560
562
  role: z2.enum(["admin", "user"]),
561
- 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)
562
571
  });
563
572
  var ORG_ROLES = ["owner", "admin", "member"];
564
573
  var OrgRoleSchema = z2.enum(ORG_ROLES);
@@ -566,7 +575,10 @@ var CloudOrgSchema = z2.object({
566
575
  slug: z2.string(),
567
576
  /** Display name; the slug never changes, this may. */
568
577
  name: z2.string(),
569
- 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)
570
582
  });
571
583
  var OrgMembershipSchema = CloudOrgSchema.extend({ role: OrgRoleSchema });
572
584
  var OrgMemberSchema = z2.object({
@@ -587,6 +599,32 @@ var AddMemberRequestSchema = z2.object({
587
599
  });
588
600
  var SetRoleRequestSchema = z2.object({ role: OrgRoleSchema });
589
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
+ });
590
628
  var LoginRequestSchema = z2.object({
591
629
  email: z2.string().min(1),
592
630
  password: z2.string().min(1),
@@ -735,11 +773,11 @@ var ServerClient = class {
735
773
  base;
736
774
  autoStart;
737
775
  startedOnce = false;
738
- async call(method, path, schema, body) {
776
+ async call(method, path, schema, body, org = this.target.kind === "cloud" ? this.target.org : void 0) {
739
777
  const headers = {};
740
778
  if (body !== void 0) headers["content-type"] = "application/json";
741
779
  if (this.target.kind === "cloud" && this.target.token !== "") headers["authorization"] = `Bearer ${this.target.token}`;
742
- if (this.target.kind === "cloud" && this.target.org !== void 0) headers["x-kvbox-org"] = this.target.org;
780
+ if (this.target.kind === "cloud" && org !== void 0) headers["x-kvbox-org"] = org;
743
781
  let res;
744
782
  try {
745
783
  res = await request(this.base + path, {
@@ -828,6 +866,10 @@ var ServerClient = class {
828
866
  const query = q.size === 0 ? "" : `?${q.toString()}`;
829
867
  return this.call("GET", `/usage${query}`, UsageReportSchema);
830
868
  }
869
+ /** The prepaid balance of the caller's slug, or of `org` (default: `--org`'s) (#76); a control plane only. */
870
+ balance(org) {
871
+ return this.call("GET", "/billing/balance", BalanceSchema, void 0, org);
872
+ }
831
873
  deleteMachine(name) {
832
874
  return this.call("DELETE", `/machines/${encodeURIComponent(name)}`, z3.undefined());
833
875
  }
@@ -1387,9 +1429,10 @@ var usageCommand = new Command13("usage").description("how long a box ran, slept
1387
1429
  var day = (iso) => iso.slice(0, 10);
1388
1430
  var SYMBOLS = { EUR: "\u20AC", USD: "$", GBP: "\xA3" };
1389
1431
  function money(amount, currency) {
1390
- const n = amount.toFixed(2);
1432
+ const sign = amount < 0 ? "-" : "";
1433
+ const n = Math.abs(amount).toFixed(2);
1391
1434
  const sym = SYMBOLS[currency.toUpperCase()];
1392
- return sym === void 0 ? `${n} ${currency}` : `${sym}${n}`;
1435
+ return sym === void 0 ? `${sign}${n} ${currency}` : `${sign}${sym}${n}`;
1393
1436
  }
1394
1437
  function block(u) {
1395
1438
  const estimated = u.estimatedStops === 0 ? "" : color.dim(` (${u.estimatedStops} stop time${u.estimatedStops === 1 ? "" : "s"} estimated)`);
@@ -1988,16 +2031,30 @@ var usageCommand2 = new Command37("usage").description("what your boxes used thi
1988
2031
  }
1989
2032
  const cloud = client.target.kind === "cloud" ? { me: client.target.user } : void 0;
1990
2033
  info(`${day2(report.window.from)} to ${day2(report.window.to)}`);
1991
- if (report.boxes.length === 0) {
1992
- info("no usage in this window");
1993
- return;
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);
1994
2039
  }
1995
- table(usageRows(report, cloud));
1996
2040
  } finally {
1997
2041
  await client.close();
1998
2042
  }
1999
2043
  });
2000
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
+ }
2001
2058
  function ownerCell2(b, me2) {
2002
2059
  const slug = slugOf(b.owner);
2003
2060
  if (slug === me2) return "private";
@@ -2029,9 +2086,38 @@ function usageRows(report, cloud) {
2029
2086
  return rows;
2030
2087
  }
2031
2088
 
2089
+ // src/commands/credit.ts
2090
+ import { Command as Command38 } from "commander";
2091
+ var creditCommand = new Command38("credit").description("the prepaid credit left on your account and on each of your orgs").option("--org <slug>", "one org only").option("--json", "print as JSON").action(async (opts, cmd) => {
2092
+ const client = await clientFor(cmd.optsWithGlobals(), { autoStart: false });
2093
+ try {
2094
+ if (client.target.kind === "local") throw new KvboxError("credit lives on the control plane: kvbox login first");
2095
+ const me2 = client.target.user;
2096
+ const balances = client.target.org !== void 0 ? [await client.balance()] : await Promise.all([void 0, ...(await client.listOrgs()).map((o) => o.slug)].map((org) => client.balance(org)));
2097
+ if (opts.json) {
2098
+ json(balances);
2099
+ return;
2100
+ }
2101
+ table(balances.map((b) => creditRow(b, me2)));
2102
+ } finally {
2103
+ await client.close();
2104
+ }
2105
+ });
2106
+ function creditRow(b, me2) {
2107
+ const left = money(b.balance, b.currency);
2108
+ return {
2109
+ owner: b.slug === me2 ? "private" : boxName(b.slug),
2110
+ credit: b.balance > 0 ? left : color.red(left),
2111
+ "topped up": money(b.topUps, b.currency),
2112
+ spent: money(b.spent, b.currency),
2113
+ // Suspended (#76): the sweep hibernated the boxes at zero; a top-up brings them back.
2114
+ state: b.suspendedAt ? color.red("suspended") : b.since === null ? color.dim("never topped up") : "ok"
2115
+ };
2116
+ }
2117
+
2032
2118
  // src/program.ts
2033
2119
  function buildProgram() {
2034
- 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();
2120
+ const program = new Command39("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();
2035
2121
  program.addCommand(vmCommand);
2036
2122
  program.addCommand(snapshotsCommand);
2037
2123
  program.addCommand(serverCommand);
@@ -2040,6 +2126,7 @@ function buildProgram() {
2040
2126
  program.addCommand(whoamiCommand);
2041
2127
  program.addCommand(orgCommand);
2042
2128
  program.addCommand(usageCommand2);
2129
+ program.addCommand(creditCommand);
2043
2130
  for (const cmd of vmAliases) program.addCommand(cmd);
2044
2131
  return program;
2045
2132
  }