@letterstory/cli 0.146.1 → 0.148.0

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/lib/cli.mjs CHANGED
@@ -65,7 +65,7 @@ import {
65
65
 
66
66
  // Matches cli/package.json; publish-cli.yml rewrites both to the published version.
67
67
  // Matches cli/package.json; publish-cli.yml rewrites both to the published version.
68
- export const VERSION = "0.146.1";
68
+ export const VERSION = "0.148.0";
69
69
 
70
70
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
71
71
  // can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
@@ -945,7 +945,7 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
945
945
  | reconcile [--org <org-id>] | forward
946
946
  | payments [--org <org-id>] [--open] | payments retry <lago-invoice-id> | payments run [--dry]
947
947
  | signups [--status open|card_saved|provisioned|paid|abandoned]
948
- | cap <org-id> <dollars|off> [--why "…"]
948
+ | cap <org-id> <dollars|off> [--why "…"] | cancel <org-id> --why "…" --yes
949
949
  Usage billing on Lago: the plan catalog, an org's
950
950
  plan and gifted ratio, Lago's tokens against ours,
951
951
  forwarding usage now, failed payments and the
@@ -1103,10 +1103,12 @@ Seers (event-driven signals -> drafts):
1103
1103
  org export --split <dir> …as one file per record (for keeping config in git)
1104
1104
  org join <dir> [--file <path>] Rebuild the bundle from a --split directory
1105
1105
  org integrations Every integration and whether it's connected
1106
- org billing [cap <dollars|off> [--why "…"] | request [--note "…"]]
1106
+ org billing [cap <dollars|off> [--why "…"] | request [--note "…"] | cancel [--why "…"] --yes]
1107
1107
  Usage billing: plan, this period against the
1108
1108
  allowance and spend cap, failed payments; set
1109
1109
  the cap (admins) or ask the admins for more
1110
+ org billing usage This period by product and collection: quantities,
1111
+ and tokens and dollars when billed by usage
1110
1112
  org linear [teams [--team <id|key>] | set --team <id|key> [--project <id>|none] | disconnect]
1111
1113
  Where Letter files Linear issues (connect in Settings)
1112
1114
  org diff <file|dir> What applying a bundle would change (reads only)
@@ -5213,7 +5213,8 @@ const LAGO_USAGE = `Usage: admin billing lago <command>
5213
5213
  payments retry <lago-invoice-id> charge one failed invoice again now
5214
5214
  payments run [--dry] one pass of the failed-payment loop now (--dry: report only)
5215
5215
  signups [--status open|card_saved|provisioned|paid|abandoned] new paying orgs on their way onto Lago
5216
- cap <org-id> <dollars|off> [--why "…"] set an account's monthly spend cap on overage (off: no cap)`;
5216
+ cap <org-id> <dollars|off> [--why "…"] set an account's monthly spend cap on overage (off: no cap)
5217
+ cancel <org-id> --why "…" --yes end an account's Lago plan now and refund its first invoice`;
5217
5218
 
5218
5219
  /** admin billing lago … — staff operations on Lago (docs/billing/lago-integration.md). */
5219
5220
  async function cmdAdminBilling(ctx) {
@@ -5336,6 +5337,23 @@ async function cmdAdminBilling(ctx) {
5336
5337
  );
5337
5338
  return 0;
5338
5339
  }
5340
+ case "cancel": {
5341
+ const orgId = positionals[2];
5342
+ if (!orgId || !flagStr(flags.why)) throw new CliError(LAGO_USAGE);
5343
+ if (!flagBool(flags.yes))
5344
+ throw new CliError(
5345
+ "This ends the plan now and refunds its first invoice. Run again with --yes to do it."
5346
+ );
5347
+ const result = await staffRequest(ctx, "POST", `${LAGO}/cancel`, { orgId, reason: flagStr(flags.why) });
5348
+ printResult(
5349
+ io,
5350
+ flags,
5351
+ result,
5352
+ (r) =>
5353
+ `Cancelled ${orgId}: first invoice ${r.firstInvoice}${r.refundedCents ? `, $${(r.refundedCents / 100).toFixed(2)} refunded` : ""}.`
5354
+ );
5355
+ return 0;
5356
+ }
5339
5357
  case "cap": {
5340
5358
  const orgId = positionals[2];
5341
5359
  const value = positionals[3];
@@ -28,11 +28,13 @@ export const ARGS = {
28
28
  describe_org_config_fields: {},
29
29
  get_org_config_source: {},
30
30
  get_billing_payments: {},
31
+ get_billing: {},
31
32
  set_billing_spend_cap: {
32
33
  cap_usd: { value: (_flags, positionals) => capFrom(requirePositional(positionals, 0, "dollars|off")) },
33
34
  reason: { flag: "why" },
34
35
  },
35
36
  request_billing_tokens: { note: { flag: "note" } },
37
+ cancel_billing_plan: { reason: { flag: "why" } },
36
38
  list_integrations: {},
37
39
  get_linear_connection: {},
38
40
  list_linear_targets: { team_id: { flag: "team" } },
@@ -110,7 +112,7 @@ const usd = (n) => `$${Number(n).toFixed(2)}`;
110
112
  * `org billing`: the org's plan on Lago, this period against its allowance and spend cap, whether
111
113
  * it is past due, and its failed payments (get_billing_payments). `org billing cap <dollars|off>`
112
114
  * sets the cap (set_billing_spend_cap, admins); `org billing request` asks the admins for more
113
- * (request_billing_tokens).
115
+ * (request_billing_tokens); `org billing usage` is this period by product and collection (get_billing).
114
116
  */
115
117
  async function orgBilling(ctx) {
116
118
  const { client, flags, io, positionals } = ctx;
@@ -125,6 +127,20 @@ async function orgBilling(ctx) {
125
127
  );
126
128
  return 0;
127
129
  }
130
+ if (positionals[0] === "cancel") {
131
+ refuseUnknownFlags(ctx, ARGS.cancel_billing_plan, ["yes"]);
132
+ if (!flagBool(flags.yes))
133
+ throw new CliError("This ends the plan now and refunds its first invoice. Run again with --yes to do it.");
134
+ const result = await client.callTool("cancel_billing_plan", argsFromFlags(ARGS.cancel_billing_plan, flags, []));
135
+ printResult(io, flags, result, (r) =>
136
+ r.firstInvoice === "refunded"
137
+ ? `Plan cancelled; $${(r.refundedCents / 100).toFixed(2)} refunded to the card.`
138
+ : r.firstInvoice === "voided"
139
+ ? "Plan cancelled; its unpaid first invoice is voided."
140
+ : "Plan cancelled."
141
+ );
142
+ return 0;
143
+ }
128
144
  if (positionals[0] === "request") {
129
145
  refuseUnknownFlags(ctx, ARGS.request_billing_tokens, []);
130
146
  const result = await client.callTool(
@@ -134,6 +150,12 @@ async function orgBilling(ctx) {
134
150
  printResult(io, flags, result, (r) => `Asked ${r.emailedTo} admin${r.emailedTo === 1 ? "" : "s"} for more.`);
135
151
  return 0;
136
152
  }
153
+ if (positionals[0] === "usage") {
154
+ refuseUnknownFlags(ctx, ARGS.get_billing, []);
155
+ const result = await client.callTool("get_billing", argsFromFlags(ARGS.get_billing, flags, []));
156
+ printResult(io, flags, result, billingUsageText);
157
+ return 0;
158
+ }
137
159
  refuseUnknownFlags(ctx, {}, []);
138
160
  const result = await client.callTool("get_billing_payments", {});
139
161
  printResult(io, flags, result, (r) =>
@@ -165,6 +187,29 @@ async function orgBilling(ctx) {
165
187
  return 0;
166
188
  }
167
189
 
190
+ const tokensOrQty = (p) => `${p.billableQuantity}${p.tokens === null ? "" : ` ${p.tokens} tokens`}`;
191
+
192
+ /** `org billing usage` as text: the period, the totals, then each product and each collection. */
193
+ function billingUsageText(r) {
194
+ const lines = [
195
+ `${r.plan ? `Plan: ${r.plan.name}${r.plan.ratio !== 1 ? ` (${r.plan.ratio}x)` : ""}. ` : ""}This period: ${r.period.from.slice(0, 10)} to ${r.period.to.slice(0, 10)}${r.period.basis === "ours" ? " (Lago unreadable: our own rating)" : ""}.`,
196
+ ];
197
+ if (r.tokens)
198
+ lines.push(
199
+ `Tokens: ${r.tokens.used} used of ${r.tokens.allowance} included${r.tokens.complimentary ? ` (+${r.tokens.complimentary} complimentary)` : ""}${r.tokens.arriving ? ` (${r.tokens.arriving} of them still being counted)` : ""}${r.tokens.running ? `, ${r.tokens.running} held by running work` : ""}.`
200
+ );
201
+ else lines.push("Not billed by usage: quantities only.");
202
+ if (r.usd) lines.push(`Plan fee ${usd(r.usd.planFee)}; overage so far ${usd(r.usd.overage)}.`);
203
+ if (r.byProduct.length === 0) return [...lines, "No usage this period."].join("\n");
204
+ lines.push("", "By product:", ...r.byProduct.map((p) => ` ${p.label} ${tokensOrQty(p)}`));
205
+ lines.push("", "By collection:");
206
+ for (const c of r.byCollection) {
207
+ lines.push(` ${c.name}${c.tokens === null ? "" : ` ${c.tokens} tokens`}`);
208
+ for (const p of c.products) lines.push(` ${p.label} ${tokensOrQty(p)}`);
209
+ }
210
+ return lines.join("\n");
211
+ }
212
+
168
213
  /** `org fields [section]`: what each setting in the bundle means (describe_org_config_fields). */
169
214
  async function orgFields(ctx) {
170
215
  const { client, flags, io, positionals } = ctx;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.146.1",
3
+ "version": "0.148.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",