@letterstory/cli 0.176.90 → 0.176.91

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
@@ -86,7 +86,7 @@ import {
86
86
  import { fetchParkedSections, withoutParked } from "./parked.mjs";
87
87
 
88
88
  // Matches cli/package.json; publish-cli.yml rewrites both to the published version.
89
- export const VERSION = "0.176.90";
89
+ export const VERSION = "0.176.91";
90
90
 
91
91
  /** The exit code for a gate refusal (CARD_REQUIRED, PAYMENT_FAILED, OUT_OF_TOKENS): 1 is a plain error. */
92
92
  export const EXIT_PAYMENT_REQUIRED = 3;
@@ -1276,7 +1276,7 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
1276
1276
  admin billing free exposure | backfill --why "…" [--apply]
1277
1277
  Who the free paywall would stop today; the one-time
1278
1278
  signup grant for self-serve orgs with none
1279
- admin billing free grant <org-id> <tokens> --why "…" [--key k] | grants [--org <org-id>] | revoke <org-id> <ref> --why "…"
1279
+ admin billing free grant <org-id|name> <tokens> --why "…" [--key k] | grants [--org <org-id>] | revoke <org-id> <ref> --why "…"
1280
1280
  Give an org free tokens (a Lago org: a one-off wallet),
1281
1281
  list staff grants, take one back; posted to #billing
1282
1282
  admin billing desk [--card-forms-since ISO] [--json] The Billing desk: switches, failed payments, the
@@ -1286,6 +1286,8 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
1286
1286
  error codes, one verdict. Read only.
1287
1287
  admin billing free preflight [--json] Who the paywall stops the moment it is enforced:
1288
1288
  out of tokens, low, paying but covered, undecided
1289
+ admin billing org find <name> [--limit n] Orgs whose name holds the words, with each one's id and
1290
+ how it is billed (free, lago, legacy, …). Read only.
1289
1291
  admin billing org test <org-id> --why "…" [--off] Mark an org a test org (never gated or billed), or with
1290
1292
  --off not one; kept with who and why. Not for a payer.
1291
1293
  admin billing emails [--bounced] [--org <org-id>] [--limit N]
@@ -6131,7 +6131,7 @@ async function cmdAdminBillingLegacyUnmark(ctx) {
6131
6131
  const FREE_USAGE = `Usage: admin billing free exposure [--json] who the free paywall covers, and who is unassigned
6132
6132
  admin billing free backfill --why "…" [--apply] signup grant for self-serve orgs with none; a dry run unless --apply
6133
6133
  admin billing free repair-subjects --why "…" [--apply] staff-address signup subjects fixed, refused orgs granted (R1); a dry run unless --apply
6134
- admin billing free grant <org-id> <tokens> --why "…" [--key k] give an org free tokens (a Lago org: a one-off wallet); the same key is one grant
6134
+ admin billing free grant <org-id|name> <tokens> --why "…" [--key k] give an org free tokens (a Lago org: a one-off wallet); the same key is one grant
6135
6135
  admin billing free grants [--org <org-id>] [--limit n] staff grants, newest first, live and revoked
6136
6136
  admin billing free revoke <org-id> <ref> --why "…" take a staff grant back (ref from the list: grant:<id> or wallet:staff_…)
6137
6137
  admin billing free preflight [--json] who the paywall stops the moment it is enforced, and whether it is ready`;
@@ -6237,12 +6237,13 @@ async function cmdAdminBillingFree(ctx) {
6237
6237
  return 0;
6238
6238
  }
6239
6239
  if (positionals[1] === "grant") {
6240
- const [orgId, tokensArg] = [positionals[2], positionals[3]];
6240
+ const [orgArg, tokensArg] = [positionals[2], positionals[3]];
6241
6241
  const tokens = Number(String(tokensArg ?? "").replace(/[,_]/g, ""));
6242
- if (!orgId || !Number.isInteger(tokens) || tokens <= 0) throw new CliError(FREE_USAGE);
6242
+ if (!orgArg || !Number.isInteger(tokens) || tokens <= 0) throw new CliError(FREE_USAGE);
6243
6243
  const why = flagStr(flags.why);
6244
6244
  if (!why) throw new CliError(`Say why (--why "…"): it is kept with the grant.\n${FREE_USAGE}`);
6245
6245
  const key = flagStr(flags.key);
6246
+ const { orgId, name } = await resolveBillingOrg(ctx, orgArg);
6246
6247
  const r = await staffPost(ctx, "/api/superadmin/billing/free-tokens/grants", {
6247
6248
  orgId,
6248
6249
  tokens,
@@ -6260,7 +6261,7 @@ async function cmdAdminBillingFree(ctx) {
6260
6261
  const what = x.kind === "wallet" ? "as a one-off wallet on Lago" : "as a free-token grant";
6261
6262
  const lines = [
6262
6263
  x.granted
6263
- ? `Gave ${x.tokens.toLocaleString("en-US")} tokens ${what} (${x.ref}).`
6264
+ ? `Gave ${name ? `${name} ` : ""}${x.tokens.toLocaleString("en-US")} tokens ${what} (${x.ref}).`
6264
6265
  : `Already given under this key (${x.ref}); nothing new.`,
6265
6266
  ];
6266
6267
  if (x.standing.before !== x.standing.after)
@@ -6441,13 +6442,49 @@ async function cmdAdminBillingEmails(ctx) {
6441
6442
  return 0;
6442
6443
  }
6443
6444
 
6444
- const ORG_TEST_USAGE = `Usage: admin billing org test <org-id> --why "…" [--off]
6445
+ const ORG_TEST_USAGE = `Usage: admin billing org find <name> [--limit n]
6446
+ Orgs whose name holds the words (or the org with that id), each with its id and how it is billed.
6447
+ admin billing org test <org-id> --why "…" [--off]
6445
6448
  Mark an org a test org (never gated or billed, out of the fleet's customer numbers), or with --off not
6446
6449
  one any more. Kept with who and why, posted to #billing. Refused for an org that pays us. Billing admins.`;
6447
6450
 
6448
- /** admin billing org test <org-id> --why "…" [--off] — setOrgTest (lib/billing/org-test.ts). */
6451
+ /** GET /api/superadmin/billing/orgs: orgs by name (or id), each with how it is billed (lib/billing/find-orgs.ts). */
6452
+ async function findBillingOrgs(ctx, q, limit) {
6453
+ const qs = new URLSearchParams({ q, ...(limit ? { limit } : {}) });
6454
+ return staffRequest(ctx, "GET", `/api/superadmin/billing/orgs?${qs}`);
6455
+ }
6456
+
6457
+ const orgLine = (o) => ` ${o.orgId} ${o.name} (${o.standing}, made ${o.createdAt.slice(0, 10)})`;
6458
+
6459
+ /**
6460
+ * An org named by its id or by its name: the id as given, or the one org the name finds (the exact name when
6461
+ * several hold it). Anything else is refused with the orgs it found, so nothing is done to a guessed org.
6462
+ */
6463
+ async function resolveBillingOrg(ctx, words) {
6464
+ if (UUID_PATTERN.test(words)) return { orgId: words, name: null };
6465
+ const { orgs } = await findBillingOrgs(ctx, words);
6466
+ const exact = orgs.filter((o) => o.name.toLowerCase() === words.trim().toLowerCase());
6467
+ const one = exact.length === 1 ? exact[0] : orgs.length === 1 ? orgs[0] : null;
6468
+ if (one) return { orgId: one.orgId, name: one.name };
6469
+ if (!orgs.length) throw new CliError(`No org's name holds "${words}".`);
6470
+ throw new CliError(`"${words}" could be any of these; give the org's id:\n${orgs.map(orgLine).join("\n")}`);
6471
+ }
6472
+
6473
+ /**
6474
+ * admin billing org find <name> — findBillingOrgs; admin billing org test <org-id> --why "…" [--off] —
6475
+ * setOrgTest (lib/billing/org-test.ts).
6476
+ */
6449
6477
  async function cmdAdminBillingOrg(ctx) {
6450
6478
  const { flags, positionals, io } = ctx;
6479
+ if (positionals[1] === "find") {
6480
+ const q = positionals.slice(2).join(" ").trim();
6481
+ if (!q) throw new CliError(ORG_TEST_USAGE);
6482
+ const r = await findBillingOrgs(ctx, q, flagStr(flags.limit));
6483
+ printResult(io, flags, r, (x) =>
6484
+ x.orgs.length ? x.orgs.map(orgLine).join("\n") : `No org's name holds "${q}".`
6485
+ );
6486
+ return 0;
6487
+ }
6451
6488
  const orgId = positionals[2];
6452
6489
  if (positionals[1] !== "test" || !orgId) throw new CliError(ORG_TEST_USAGE);
6453
6490
  const why = flagStr(flags.why);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.176.90",
3
+ "version": "0.176.91",
4
4
  "description": "Spin up and manage Letter phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",