@letterstory/cli 0.176.75 → 0.176.77
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 +9 -5
- package/lib/commands/admin.mjs +44 -6
- package/lib/commands/repo.mjs +15 -3
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -85,7 +85,7 @@ import {
|
|
|
85
85
|
import { fetchParkedSections, withoutParked } from "./parked.mjs";
|
|
86
86
|
|
|
87
87
|
// Matches cli/package.json; publish-cli.yml rewrites both to the published version.
|
|
88
|
-
export const VERSION = "0.176.
|
|
88
|
+
export const VERSION = "0.176.77";
|
|
89
89
|
|
|
90
90
|
/** The exit code for a gate refusal (CARD_REQUIRED, PAYMENT_FAILED, OUT_OF_TOKENS): 1 is a plain error. */
|
|
91
91
|
export const EXIT_PAYMENT_REQUIRED = 3;
|
|
@@ -1253,8 +1253,11 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
1253
1253
|
admin billing free grant <org-id> <tokens> --why "…" [--key k] | grants [--org <org-id>] | revoke <org-id> <ref> --why "…"
|
|
1254
1254
|
Give an org free tokens (a Lago org: a one-off wallet),
|
|
1255
1255
|
list staff grants, take one back; posted to #billing
|
|
1256
|
-
admin billing desk [--json]
|
|
1257
|
-
nightly reconcile, refusals, signups, grants
|
|
1256
|
+
admin billing desk [--card-forms-since ISO] [--json] The Billing desk: switches, failed payments, the
|
|
1257
|
+
nightly reconcile, refusals, signups, card forms, grants
|
|
1258
|
+
admin billing card-forms [--since ISO] [--json] Where each card form stopped (default the last 7
|
|
1259
|
+
days): the browser's last step, Stripe's status and
|
|
1260
|
+
error codes, one verdict. Read only.
|
|
1258
1261
|
admin billing free preflight [--json] Who the paywall stops the moment it is enforced:
|
|
1259
1262
|
out of tokens, low, paying but covered, undecided
|
|
1260
1263
|
admin billing org test <org-id> --why "…" [--off] Mark an org a test org (never gated or billed), or with
|
|
@@ -1858,8 +1861,9 @@ Repo changes (Claude Code edits your repo in a sandbox; the Letter GitHub App op
|
|
|
1858
1861
|
One PR adding the Visitors script tag and server snippet to the repo that
|
|
1859
1862
|
builds the site (--repo: another repo)
|
|
1860
1863
|
repo sites [--detect [--force]] Which website each repo builds, and the evidence
|
|
1861
|
-
repo set-site --repo <owner/name> [--gitlab] --site <host> [--not] [--branch <name>|none]
|
|
1862
|
-
Say which site a repo builds (--not: correct a wrong label
|
|
1864
|
+
repo set-site --repo <owner/name> [--gitlab] --site <host> [--not] [--branch <name>|none] [--sections blog,docs|none]
|
|
1865
|
+
Say which site a repo builds (--not: correct a wrong label;
|
|
1866
|
+
--sections: the parts of it, when only those)
|
|
1863
1867
|
repo reset-site --repo <owner/name> [--gitlab] --site <host>
|
|
1864
1868
|
Back to automatic detection
|
|
1865
1869
|
repo secrets --repo <owner/name> [--gitlab] Preview secrets: names and when set (never values)
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -6013,6 +6013,7 @@ async function cmdAdminRecipes(ctx) {
|
|
|
6013
6013
|
|
|
6014
6014
|
const LAGO = "/api/superadmin/billing/lago";
|
|
6015
6015
|
const LAGO_USAGE = `Usage: admin billing desk [--json] the Billing desk: switches, failed payments, reconcile, refusals, grants
|
|
6016
|
+
admin billing card-forms [--since ISO] where each card form stopped: browser step, Stripe's status, verdict
|
|
6016
6017
|
admin billing org test <org-id> --why "…" [--off] mark an org a test org (or not), kept with who and why
|
|
6017
6018
|
admin billing lago <command>
|
|
6018
6019
|
catalog [--apply] [--include-drafts] what the plan catalog would change in Lago; --apply makes it so
|
|
@@ -6299,14 +6300,33 @@ async function cmdAdminBillingFree(ctx) {
|
|
|
6299
6300
|
throw new CliError(FREE_USAGE);
|
|
6300
6301
|
}
|
|
6301
6302
|
|
|
6303
|
+
/** The desk's GET, with the card forms' window when one is named (--since / --card-forms-since). */
|
|
6304
|
+
function deskPath(since) {
|
|
6305
|
+
return `/api/superadmin/billing/desk${since ? `?${new URLSearchParams({ card_forms_since: since })}` : ""}`;
|
|
6306
|
+
}
|
|
6307
|
+
|
|
6302
6308
|
/**
|
|
6303
|
-
*
|
|
6304
|
-
*
|
|
6305
|
-
|
|
6309
|
+
* Where each card form stopped (the desk's signups.cardForms, lib/billing/lago/card-form.ts): one line per
|
|
6310
|
+
* signup with its verdict, the browser's last step and Stripe's status.
|
|
6311
|
+
*/
|
|
6312
|
+
function cardFormLines(d) {
|
|
6313
|
+
const forms = d.signups.cardForms ?? [];
|
|
6314
|
+
const lines = [`Card forms since ${String(d.signups.cardFormsSince ?? "").slice(0, 10)}: ${forms.length}`];
|
|
6315
|
+
for (const c of forms)
|
|
6316
|
+
lines.push(
|
|
6317
|
+
` ${c.orgName ?? c.orgId.slice(0, 8)} ${c.planCode} ${c.status} ${c.words} (browser: ${c.browser.lastStep ?? "nothing"}, Stripe: ${c.stripe ? (c.stripe.status ?? "no setup") : "not read yet"})${c.alertedAt ? " #billing told" : ""}`
|
|
6318
|
+
);
|
|
6319
|
+
return lines;
|
|
6320
|
+
}
|
|
6321
|
+
|
|
6322
|
+
/**
|
|
6323
|
+
* admin billing desk [--card-forms-since ISO] [--json] — the Billing desk in one read (lib/billing/desk.ts,
|
|
6324
|
+
* docs/billing/billing-desk.md): the switches, what needs a person now, and the week's numbers. The same as
|
|
6325
|
+
* GET /api/superadmin/billing/desk and MCP billing_desk.
|
|
6306
6326
|
*/
|
|
6307
6327
|
async function cmdAdminBillingDesk(ctx) {
|
|
6308
6328
|
const { flags, io } = ctx;
|
|
6309
|
-
const r = await staffRequest(ctx, "GET", "
|
|
6329
|
+
const r = await staffRequest(ctx, "GET", deskPath(flagStr(flags["card-forms-since"])));
|
|
6310
6330
|
printResult(io, flags, r, (d) => {
|
|
6311
6331
|
const n = (x) => Number(x ?? 0).toLocaleString("en-US");
|
|
6312
6332
|
const sw = d.switches;
|
|
@@ -6336,8 +6356,9 @@ async function cmdAdminBillingDesk(ctx) {
|
|
|
6336
6356
|
if (d.signups.stuck.length) lines.push(`Stuck signups: ${d.signups.stuck.length}`);
|
|
6337
6357
|
for (const s of d.signups.stuck)
|
|
6338
6358
|
lines.push(
|
|
6339
|
-
` ${s.orgName ?? s.orgId.slice(0, 8)} ${s.planCode} ${s.status}${s.lastError ? `: ${s.lastError}` : ""}`
|
|
6359
|
+
` ${s.orgName ?? s.orgId.slice(0, 8)} ${s.planCode} ${s.status}${s.lastError ? `: ${s.lastError}` : ""}${s.cardForm ? ` (card form: ${s.cardForm})` : ""}`
|
|
6340
6360
|
);
|
|
6361
|
+
lines.push(...cardFormLines(d));
|
|
6341
6362
|
const refusalWords = Object.entries(d.refusals.byReason)
|
|
6342
6363
|
.map(([k, c]) => `${c} ${k}`)
|
|
6343
6364
|
.join(", ");
|
|
@@ -6361,6 +6382,22 @@ async function cmdAdminBillingDesk(ctx) {
|
|
|
6361
6382
|
return 0;
|
|
6362
6383
|
}
|
|
6363
6384
|
|
|
6385
|
+
const CARD_FORMS_USAGE = `Usage: admin billing card-forms [--since 2026-11-08 | 2026-11-08T09:30:00Z] [--json]
|
|
6386
|
+
where each card form started since --since (a date is 00:00 UTC; default the last 7 days) stopped: the browser's last step,
|
|
6387
|
+
Stripe's SetupIntent status and error codes, and one verdict ("opened, never submitted",
|
|
6388
|
+
"submitted, Stripe refused: card_declined / insufficient_funds"). The desk's card forms, alone. Read only.`;
|
|
6389
|
+
|
|
6390
|
+
/** admin billing card-forms [--since ISO] — the desk's card forms alone (lib/billing/lago/card-form.ts). */
|
|
6391
|
+
async function cmdAdminBillingCardForms(ctx) {
|
|
6392
|
+
const { flags, positionals, io } = ctx;
|
|
6393
|
+
if (positionals.length > 1) throw new CliError(CARD_FORMS_USAGE);
|
|
6394
|
+
const r = await staffRequest(ctx, "GET", deskPath(flagStr(flags.since)));
|
|
6395
|
+
printResult(io, flags, { cardFormsSince: r.signups.cardFormsSince, cardForms: r.signups.cardForms }, () =>
|
|
6396
|
+
cardFormLines(r).join("\n")
|
|
6397
|
+
);
|
|
6398
|
+
return 0;
|
|
6399
|
+
}
|
|
6400
|
+
|
|
6364
6401
|
const EMAILS_USAGE = `Usage: admin billing emails [--bounced] [--org <org-id>] [--limit N]
|
|
6365
6402
|
the invoice and receipt emails we sent for Lago and what became of each, bounced first
|
|
6366
6403
|
(--bounced: only the ones that bounced, were marked as spam or failed)`;
|
|
@@ -6420,6 +6457,7 @@ async function cmdAdminBilling(ctx) {
|
|
|
6420
6457
|
if (positionals[0] === "legacy") return cmdAdminBillingLegacy(ctx);
|
|
6421
6458
|
if (positionals[0] === "free") return cmdAdminBillingFree(ctx);
|
|
6422
6459
|
if (positionals[0] === "desk") return cmdAdminBillingDesk(ctx);
|
|
6460
|
+
if (positionals[0] === "card-forms") return cmdAdminBillingCardForms(ctx);
|
|
6423
6461
|
if (positionals[0] === "emails") return cmdAdminBillingEmails(ctx);
|
|
6424
6462
|
if (positionals[0] === "org") return cmdAdminBillingOrg(ctx);
|
|
6425
6463
|
if (positionals[0] !== "lago") throw new CliError(LAGO_USAGE);
|
|
@@ -6553,7 +6591,7 @@ async function cmdAdminBilling(ctx) {
|
|
|
6553
6591
|
...(r.signups.length
|
|
6554
6592
|
? r.signups.map(
|
|
6555
6593
|
(x) =>
|
|
6556
|
-
` ${x.orgName ?? x.orgId} ${x.planCode}${x.ratio !== 1 ? ` ${x.ratio}x` : ""} ${x.status} ${x.source}${x.lastError ? ` (${x.lastError})` : ""}`
|
|
6594
|
+
` ${x.orgName ?? x.orgId} ${x.planCode}${x.ratio !== 1 ? ` ${x.ratio}x` : ""} ${x.status} ${x.source}${x.cardForm ? ` ${x.cardForm}` : ""}${x.lastError ? ` (${x.lastError})` : ""}`
|
|
6557
6595
|
)
|
|
6558
6596
|
: [" None."]),
|
|
6559
6597
|
].join("\n")
|
package/lib/commands/repo.mjs
CHANGED
|
@@ -131,6 +131,18 @@ const ARGS = {
|
|
|
131
131
|
builds: { flag: "not", type: "bool", invert: true },
|
|
132
132
|
// --branch <name>: the branch that deploys the site (one repo, a site per branch); none = default.
|
|
133
133
|
branch: { value: (flags) => (flags.branch === "none" ? null : flagStr(flags.branch)) },
|
|
134
|
+
// --sections blog,docs: the parts of the site the repo builds, when only those; none = the whole site.
|
|
135
|
+
sections: {
|
|
136
|
+
value: (flags) =>
|
|
137
|
+
flags.sections === undefined
|
|
138
|
+
? undefined
|
|
139
|
+
: flags.sections === "none"
|
|
140
|
+
? null
|
|
141
|
+
: String(flags.sections)
|
|
142
|
+
.split(",")
|
|
143
|
+
.map((s) => s.trim())
|
|
144
|
+
.filter(Boolean),
|
|
145
|
+
},
|
|
134
146
|
},
|
|
135
147
|
reset_repo_site: {
|
|
136
148
|
provider,
|
|
@@ -594,7 +606,7 @@ async function repoSites(ctx) {
|
|
|
594
606
|
const serving = r.sites.filter((s) => s.verdict === "serves");
|
|
595
607
|
const lines = serving.map((s) =>
|
|
596
608
|
s.decidedBy === "member"
|
|
597
|
-
? `${s.repo} builds ${s.siteHost} (set by a member)`
|
|
609
|
+
? `${s.repo} builds ${s.siteHost}${s.sections?.length ? ` ${s.sections.map((x) => `/${x}`).join(", ")} only` : ""} (set by a member)`
|
|
598
610
|
: `${s.repo} builds ${s.siteHost} (${Math.round(s.score * 100)}%: ${s.evidence.signals.join(", ")})`
|
|
599
611
|
);
|
|
600
612
|
if (r.failed?.length) lines.push(...r.failed.map((f) => `! ${f.repo}: ${f.error}`));
|
|
@@ -603,10 +615,10 @@ async function repoSites(ctx) {
|
|
|
603
615
|
return 0;
|
|
604
616
|
}
|
|
605
617
|
|
|
606
|
-
/** `repo set-site --repo owner/name --site acme.com [--not]`: say which site a repo builds (or doesn't). */
|
|
618
|
+
/** `repo set-site --repo owner/name --site acme.com [--not] [--branch b] [--sections blog,docs|none]`: say which site a repo builds (or doesn't), and which part of it. */
|
|
607
619
|
async function repoSetSite(ctx) {
|
|
608
620
|
const { client, flags, io } = ctx;
|
|
609
|
-
refuseUnknownFlags(ctx, {}, ["repo", "owner", "name", "gitlab", "site", "not", "branch"]);
|
|
621
|
+
refuseUnknownFlags(ctx, {}, ["repo", "owner", "name", "gitlab", "site", "not", "branch", "sections"]);
|
|
610
622
|
const result = await client.callTool("set_repo_site", argsFromFlags(ARGS.set_repo_site, flags));
|
|
611
623
|
ok(ctx, result.message);
|
|
612
624
|
printResult(io, flags, result.site);
|