@letterstory/cli 0.176.76 → 0.176.78
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 +6 -3
- package/lib/commands/admin.mjs +44 -6
- package/lib/commands/replicate.mjs +24 -0
- 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.78";
|
|
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
|
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")
|
|
@@ -107,6 +107,7 @@ function headline(x) {
|
|
|
107
107
|
` ${x.summary.replicated} of ${x.summary.pages} pages replicated; ${x.summary.passing} match the original at every width (mean ${pct(x.summary.meanPixel)})`,
|
|
108
108
|
]
|
|
109
109
|
: []),
|
|
110
|
+
...motionLines(x.motion),
|
|
110
111
|
...(x.agent_pass
|
|
111
112
|
? [
|
|
112
113
|
` Agent pass: ${x.agent_pass.model}, $${x.agent_pass.cost_usd.toFixed(2)} of $${x.agent_pass.budget_usd.toFixed(2)} spent`,
|
|
@@ -134,6 +135,24 @@ function headline(x) {
|
|
|
134
135
|
];
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
const TRIGGERS = ["load", "scroll", "hover", "loop"];
|
|
139
|
+
|
|
140
|
+
// Motion: what moves on the original against the replica, beside the still scores (never the bar).
|
|
141
|
+
function motionLines(m) {
|
|
142
|
+
if (!m) return [];
|
|
143
|
+
const found = TRIGGERS.reduce((s, t) => s + m.found[t], 0);
|
|
144
|
+
const reproduced = TRIGGERS.reduce((s, t) => s + m.reproduced[t], 0);
|
|
145
|
+
return [
|
|
146
|
+
` Motion ${reproduced} of ${found} animations reproduced (${TRIGGERS.map((t) => `${t} ${m.reproduced[t]}/${m.found[t]}`).join(", ")})${m.mean_score !== null ? `, frames ${pct(m.mean_score)}` : ""}${m.not_measured ? `; ${m.not_measured} page(s) not measured` : ""}`,
|
|
147
|
+
...m.gaps
|
|
148
|
+
.slice(0, 8)
|
|
149
|
+
.map(
|
|
150
|
+
(g) =>
|
|
151
|
+
` ${g.issue}: ${g.trigger} "${g.element}" (${g.what}) on ${g.pages} page${g.pages === 1 ? "" : "s"}, e.g. ${g.example}`
|
|
152
|
+
),
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
|
|
137
156
|
// The staging link: the replica live on a Letter domain, to share before (or without) a repo.
|
|
138
157
|
function previewLines(x) {
|
|
139
158
|
if (x.staging_url)
|
|
@@ -175,6 +194,11 @@ function replicationDetail(ctx, result) {
|
|
|
175
194
|
lines.push(` ${s.viewport}: "${worst.label}" ${pct(worst.pixel)}`);
|
|
176
195
|
}
|
|
177
196
|
if (p.refined) lines.push(` changed by the agent pass: ${p.refined}`);
|
|
197
|
+
if (p.motion?.error) lines.push(` motion not measured: ${p.motion.error}`);
|
|
198
|
+
for (const g of p.motion?.gaps ?? [])
|
|
199
|
+
lines.push(
|
|
200
|
+
` motion ${g.viewport} ${g.trigger} ${g.issue}: "${g.element}" ${g.what}${g.score !== null ? ` (${pct(g.score)})` : ""}`
|
|
201
|
+
);
|
|
178
202
|
}
|
|
179
203
|
}
|
|
180
204
|
if (r.dropped?.length)
|