@letterstory/cli 0.148.3 → 0.149.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 +22 -1
- package/lib/commands/admin.mjs +188 -3
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
|
|
67
67
|
// Matches cli/package.json; publish-cli.yml rewrites both to the published version.
|
|
68
68
|
// Matches cli/package.json; publish-cli.yml rewrites both to the published version.
|
|
69
|
-
export const VERSION = "0.
|
|
69
|
+
export const VERSION = "0.149.0";
|
|
70
70
|
|
|
71
71
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
72
72
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
@@ -134,6 +134,12 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
134
134
|
"verbose",
|
|
135
135
|
"no-color",
|
|
136
136
|
"dry-run",
|
|
137
|
+
// admin demo list --enrollable, show --moves, set --enroll|--unenroll
|
|
138
|
+
"enrollable",
|
|
139
|
+
"moves",
|
|
140
|
+
"enroll",
|
|
141
|
+
"unenroll",
|
|
142
|
+
"clean-slate",
|
|
137
143
|
// admin recipes list --enabled-only, save --enable|--disable, import --retire-missing
|
|
138
144
|
"enabled-only",
|
|
139
145
|
"enable",
|
|
@@ -850,6 +856,19 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
850
856
|
[--primitives a,b|none] [--goals a,b|none] [--company-types a,b|none] [--enable|--disable] [--position n]
|
|
851
857
|
Create one (no id) or change one. Needs recipes:write.
|
|
852
858
|
admin recipes delete <id> --yes Delete for good (save --disable retires and keeps it).
|
|
859
|
+
admin demo guide How to run a demo, step by step, and what every knob and
|
|
860
|
+
seed shows on screen. Read it first.
|
|
861
|
+
admin demo list [--enrollable] | show <org-id> [--moves]
|
|
862
|
+
Demo orgs (/superadmin/demo): what each shows beside
|
|
863
|
+
what it really has. Needs demo:read.
|
|
864
|
+
admin demo set <org-id> [--enroll] [--phantom parked] [--search-console hidden] [--inject <key>]... [--save-as n]
|
|
865
|
+
Set what a demo org shows (merged); 'shown' puts a thing
|
|
866
|
+
back. --file takes a scenario as JSON. Needs demo:write.
|
|
867
|
+
admin demo reset <org-id> <scenario> [--clean-slate] [--close-prs since_applied|all_open|none]
|
|
868
|
+
Undo the last take (its PRs closed, builds archived), then
|
|
869
|
+
show a scenario: real, fresh_geo, fresh_seo, after_approve
|
|
870
|
+
or a saved one. --clean-slate closes every open PR and
|
|
871
|
+
archives every build.
|
|
853
872
|
admin suggestion-recipes list|export [--out f.json]|import <f.json> [--dry-run] [--retire-missing]|stats
|
|
854
873
|
Build's suggestion recipes: plain-language ways
|
|
855
874
|
Letterpress finds what a site should build (look at,
|
|
@@ -952,6 +971,8 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
952
971
|
| payments [--org <org-id>] [--open] | payments retry <lago-invoice-id> | payments run [--dry]
|
|
953
972
|
| signups [--status open|card_saved|provisioned|paid|abandoned]
|
|
954
973
|
| cap <org-id> <dollars|off> [--why "…"] | cancel <org-id> --why "…" --yes
|
|
974
|
+
| move <org-id> --plan starter|growth|scale [--ratio N [--why "…"]] [--reason "…"] [--stripe-subscription sub_…] [--dry]
|
|
975
|
+
| changes [--org <org-id>] | changes cancel <org-id>
|
|
955
976
|
Usage billing on Lago: the plan catalog, an org's
|
|
956
977
|
plan and gifted ratio, Lago's tokens against ours,
|
|
957
978
|
forwarding usage now, failed payments and the
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// admin brand-detection <org-id> (fleet:read)
|
|
20
20
|
// admin brand-detection <org-id> [--term t]... [--exact t]... [--exclude p]... [--note "…"] | --clear [--note "…"] (fleet:instrument)
|
|
21
21
|
// admin recipes list|show|vocab|export|import|save|delete … (recipes:read / recipes:write)
|
|
22
|
+
// admin demo guide|list|show|set|reset … (demo:read / demo:write)
|
|
22
23
|
// admin models runs|run|star|unstar|scoreboard|requests|request|sampling … (models:read / models:write)
|
|
23
24
|
// admin replays [--org <id>] [--target <id>] [--status …] [--limit n] (fleet:read)
|
|
24
25
|
// admin replay --org <id> [<response-id> | --target <id>] [--surface …] (fleet:replay)
|
|
@@ -5214,7 +5215,11 @@ const LAGO_USAGE = `Usage: admin billing lago <command>
|
|
|
5214
5215
|
payments run [--dry] one pass of the failed-payment loop now (--dry: report only)
|
|
5215
5216
|
signups [--status open|card_saved|provisioned|paid|abandoned] new paying orgs on their way onto Lago
|
|
5216
5217
|
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
|
|
5218
|
+
cancel <org-id> --why "…" --yes end an account's Lago plan now and refund its first invoice
|
|
5219
|
+
move <org-id> --plan starter [--ratio 4] [--why "…" above 4x] [--reason "…"] [--complimentary 20000] [--review 2027-05-01] [--stripe-subscription sub_…] [--dry]
|
|
5220
|
+
move an existing customer from Stripe to Lago at their renewal
|
|
5221
|
+
changes [--org <org-id>] term changes waiting to take effect (moves, cheaper plans, new ratios)
|
|
5222
|
+
changes cancel <org-id> take back an org's scheduled change before it takes effect`;
|
|
5218
5223
|
|
|
5219
5224
|
/** admin billing lago … — staff operations on Lago (docs/billing/lago-integration.md). */
|
|
5220
5225
|
async function cmdAdminBilling(ctx) {
|
|
@@ -5258,7 +5263,9 @@ async function cmdAdminBilling(ctx) {
|
|
|
5258
5263
|
);
|
|
5259
5264
|
ok(
|
|
5260
5265
|
ctx,
|
|
5261
|
-
|
|
5266
|
+
result.action === "scheduled"
|
|
5267
|
+
? `Scheduled: from ${result.effectiveAt}, ${result.includedTokens} tokens included, overage $${result.overageUsdPerToken} a token (Lago bills today's terms until then).`
|
|
5268
|
+
: `${result.action === "created" ? "On Lago" : "Already on Lago"}: ${result.subscriptionId}, ${result.includedTokens} tokens included, overage $${result.overageUsdPerToken} a token, from ${result.startsAt}.`
|
|
5262
5269
|
);
|
|
5263
5270
|
printResult(io, flags, result);
|
|
5264
5271
|
return 0;
|
|
@@ -5359,6 +5366,55 @@ async function cmdAdminBilling(ctx) {
|
|
|
5359
5366
|
);
|
|
5360
5367
|
return 0;
|
|
5361
5368
|
}
|
|
5369
|
+
case "move": {
|
|
5370
|
+
const orgId = requirePositional(positionals, 2, "org-id");
|
|
5371
|
+
const plan = flagStr(flags.plan);
|
|
5372
|
+
if (!plan) throw new CliError(LAGO_USAGE);
|
|
5373
|
+
const result = await staffPost(
|
|
5374
|
+
ctx,
|
|
5375
|
+
`${LAGO}/move`,
|
|
5376
|
+
compact({
|
|
5377
|
+
orgId,
|
|
5378
|
+
planCode: plan,
|
|
5379
|
+
ratio: flagNum(flags.ratio),
|
|
5380
|
+
exceptionReason: flagStr(flags.why),
|
|
5381
|
+
reason: flagStr(flags.reason),
|
|
5382
|
+
complimentaryTokens: flagNum(flags.complimentary),
|
|
5383
|
+
reviewAt: flagStr(flags.review),
|
|
5384
|
+
stripeSubscriptionId: flagStr(flags["stripe-subscription"]),
|
|
5385
|
+
dryRun: flags.dry ? true : undefined,
|
|
5386
|
+
})
|
|
5387
|
+
);
|
|
5388
|
+
const p = result.plan;
|
|
5389
|
+
ok(
|
|
5390
|
+
ctx,
|
|
5391
|
+
`${result.dryRun ? "Would move" : "Moving"} ${p.orgId} from Stripe ${p.from.stripeSubscriptionId} (${p.from.product ?? "subscription"}) to Lago ${p.to.plan} at its renewal ${p.from.renewsAt}: Stripe stops then, Lago charges $${(p.to.amountCents / 100).toFixed(2)} then (${p.to.includedTokens} tokens included, overage $${p.to.overageUsdPerToken} a token).`
|
|
5392
|
+
);
|
|
5393
|
+
printResult(io, flags, result);
|
|
5394
|
+
return 0;
|
|
5395
|
+
}
|
|
5396
|
+
case "changes": {
|
|
5397
|
+
if (positionals[2] === "cancel") {
|
|
5398
|
+
const orgId = requirePositional(positionals, 3, "org-id");
|
|
5399
|
+
const result = await staffRequest(ctx, "DELETE", `${LAGO}/changes`, { orgId });
|
|
5400
|
+
ok(ctx, `Taken back: the ${result.canceled.kind} scheduled for ${result.canceled.effective_at}.`);
|
|
5401
|
+
printResult(io, flags, result);
|
|
5402
|
+
return 0;
|
|
5403
|
+
}
|
|
5404
|
+
const params = new URLSearchParams(compact({ orgId: flagStr(flags.org) }));
|
|
5405
|
+
const result = await staffRequest(ctx, "GET", `${LAGO}/changes${params.size ? `?${params}` : ""}`);
|
|
5406
|
+
printResult(io, flags, result, (r) =>
|
|
5407
|
+
r.changes.length
|
|
5408
|
+
? r.changes
|
|
5409
|
+
.map(
|
|
5410
|
+
(c) =>
|
|
5411
|
+
`${c.effective_at} ${c.kind.padEnd(9)} ${c.organizations?.name ?? c.org_id} → ${c.plan_code} ${c.ratio}x (by ${c.requested_by}${c.last_error ? `; waiting: ${c.last_error}` : ""})`
|
|
5412
|
+
)
|
|
5413
|
+
.join("\n")
|
|
5414
|
+
: "Nothing scheduled."
|
|
5415
|
+
);
|
|
5416
|
+
return 0;
|
|
5417
|
+
}
|
|
5362
5418
|
case "cap": {
|
|
5363
5419
|
const orgId = positionals[2];
|
|
5364
5420
|
const value = positionals[3];
|
|
@@ -5385,6 +5441,134 @@ async function cmdAdminBilling(ctx) {
|
|
|
5385
5441
|
}
|
|
5386
5442
|
}
|
|
5387
5443
|
|
|
5444
|
+
// `admin demo` — demo control (docs/architecture/demo-control.md): what each demo org shows,
|
|
5445
|
+
// set, rehearsed and reset. The staff routes behind /superadmin/demo, the same functions and
|
|
5446
|
+
// schema as list_demo_orgs / get_demo_state / set_demo_state / reset_demo_state.
|
|
5447
|
+
const DEMO = "/api/superadmin/demo";
|
|
5448
|
+
const DEMO_ITEM_FLAGS = [
|
|
5449
|
+
"phantom",
|
|
5450
|
+
"main-blog",
|
|
5451
|
+
"visitors",
|
|
5452
|
+
"probes",
|
|
5453
|
+
"cms",
|
|
5454
|
+
"publish-target",
|
|
5455
|
+
"search-console",
|
|
5456
|
+
"github",
|
|
5457
|
+
"domain",
|
|
5458
|
+
"setup",
|
|
5459
|
+
];
|
|
5460
|
+
const DEMO_USAGE =
|
|
5461
|
+
"Usage: admin demo guide | list [--enrollable] | show <org-id> [--moves] | set <org-id> [--enroll] [--unenroll] [--file <state.json>] [--phantom parked|shown] [--main-blog …] [--visitors …] [--probes not_run|shown] [--cms hidden|shown] [--publish-target …] [--search-console …] [--github …] [--domain …] [--setup fresh|finished] [--domain-cap n] [--inject <key>]... [--hide <key>]... [--save-as <name>] | reset <org-id> <scenario> [--clean-slate] [--close-prs since_applied|all_open|none]";
|
|
5462
|
+
|
|
5463
|
+
async function cmdAdminDemo(ctx) {
|
|
5464
|
+
const { flags, positionals, io } = ctx;
|
|
5465
|
+
const sub = positionals[0];
|
|
5466
|
+
switch (sub) {
|
|
5467
|
+
case "guide": {
|
|
5468
|
+
const guide = await staffRequest(ctx, "GET", `${DEMO}/guide`);
|
|
5469
|
+
printResult(io, flags, guide, (g) =>
|
|
5470
|
+
[
|
|
5471
|
+
"How to run a demo",
|
|
5472
|
+
...g.steps.map(
|
|
5473
|
+
(s) => `\n${s.title}\n ${s.do}\n page: ${s.page}\n cli: ${s.cli}\n -> ${s.result}`
|
|
5474
|
+
),
|
|
5475
|
+
"\nWhat each knob and seed shows",
|
|
5476
|
+
...g.entries.map(
|
|
5477
|
+
(e) =>
|
|
5478
|
+
`\n${e.label} (${e.values})\n ${e.what}\n${e.shows_as.map((x) => ` · ${x}`).join("\n")}${
|
|
5479
|
+
e.on_camera ? `\n on camera: ${e.on_camera}` : ""
|
|
5480
|
+
}${e.mind.length ? `\n${e.mind.map((m) => ` ! ${m}`).join("\n")}` : ""}`
|
|
5481
|
+
),
|
|
5482
|
+
].join("\n")
|
|
5483
|
+
);
|
|
5484
|
+
return 0;
|
|
5485
|
+
}
|
|
5486
|
+
case "list": {
|
|
5487
|
+
const result = await staffRequest(
|
|
5488
|
+
ctx,
|
|
5489
|
+
"GET",
|
|
5490
|
+
`${DEMO}${flagBool(flags.enrollable) ? "?candidates=1" : ""}`
|
|
5491
|
+
);
|
|
5492
|
+
printResult(io, flags, result, (r) =>
|
|
5493
|
+
r.orgs.length
|
|
5494
|
+
? r.orgs
|
|
5495
|
+
.map(
|
|
5496
|
+
(o) =>
|
|
5497
|
+
`${o.enrolled ? "demo" : "test"} ${o.name} ${o.domain ?? "—"} ${o.scenario ?? ""}${
|
|
5498
|
+
o.differs?.length ? ` [${o.differs.join(", ")}]` : ""
|
|
5499
|
+
} ${o.org_id}`
|
|
5500
|
+
)
|
|
5501
|
+
.join("\n")
|
|
5502
|
+
: "No demo orgs yet."
|
|
5503
|
+
);
|
|
5504
|
+
return 0;
|
|
5505
|
+
}
|
|
5506
|
+
case "show": {
|
|
5507
|
+
const orgId = requirePositional(positionals, 1, "org-id");
|
|
5508
|
+
const result = await staffRequest(
|
|
5509
|
+
ctx,
|
|
5510
|
+
"GET",
|
|
5511
|
+
`${DEMO}/${encodeURIComponent(orgId)}${flagBool(flags.moves) ? "?moves=1" : ""}`
|
|
5512
|
+
);
|
|
5513
|
+
printResult(io, flags, result, (r) =>
|
|
5514
|
+
[
|
|
5515
|
+
`${r.org.name} ${r.org.domain ?? "no domain"} scenario ${r.state.scenario ?? "—"}`,
|
|
5516
|
+
...r.knobs.map((k) => ` ${k.item.padEnd(16)} real: ${k.real} · shown: ${k.shown} (${k.how})`),
|
|
5517
|
+
` observations open: ${r.observations.open.map((o) => `${o.key}${o.injected ? " (demo)" : ""}`).join(", ") || "none"}`,
|
|
5518
|
+
` answers: ${r.answers.map((q) => `${q.named}/${q.answers} ${q.text}`).join("; ") || "never probed"}`,
|
|
5519
|
+
].join("\n")
|
|
5520
|
+
);
|
|
5521
|
+
return 0;
|
|
5522
|
+
}
|
|
5523
|
+
case "set": {
|
|
5524
|
+
const orgId = requirePositional(positionals, 1, "org-id");
|
|
5525
|
+
const file = flagStr(flags.file);
|
|
5526
|
+
const state = file ? JSON.parse(readFileSync(file, "utf8")) : {};
|
|
5527
|
+
for (const flag of DEMO_ITEM_FLAGS) {
|
|
5528
|
+
const value = flagStr(flags[flag]);
|
|
5529
|
+
if (value) state[flag.replace(/-/g, "_")] = value;
|
|
5530
|
+
}
|
|
5531
|
+
const cap = flagNum(flags["domain-cap"]);
|
|
5532
|
+
if (cap !== undefined && cap !== null) state.domain_cap = cap;
|
|
5533
|
+
const inject = flagList(flags.inject);
|
|
5534
|
+
const hide = flagList(flags.hide);
|
|
5535
|
+
if (inject.length || hide.length)
|
|
5536
|
+
state.observations = {
|
|
5537
|
+
...state.observations,
|
|
5538
|
+
...(inject.length ? { inject } : {}),
|
|
5539
|
+
...(hide.length ? { hide } : {}),
|
|
5540
|
+
};
|
|
5541
|
+
const body = compact({
|
|
5542
|
+
...(Object.keys(state).length ? { state } : {}),
|
|
5543
|
+
enroll: flagBool(flags.enroll) || undefined,
|
|
5544
|
+
unenroll: flagBool(flags.unenroll) || undefined,
|
|
5545
|
+
save_as: flagStr(flags["save-as"]),
|
|
5546
|
+
});
|
|
5547
|
+
if (!Object.keys(body).length) throw new CliError(DEMO_USAGE);
|
|
5548
|
+
const result = await staffRequest(ctx, "PATCH", `${DEMO}/${encodeURIComponent(orgId)}`, body);
|
|
5549
|
+
printResult(io, flags, result, (r) => r.notes.join("\n") || "No change.");
|
|
5550
|
+
return 0;
|
|
5551
|
+
}
|
|
5552
|
+
case "reset": {
|
|
5553
|
+
const orgId = requirePositional(positionals, 1, "org-id");
|
|
5554
|
+
const scenario = requirePositional(positionals, 2, "scenario");
|
|
5555
|
+
const result = await staffPost(
|
|
5556
|
+
ctx,
|
|
5557
|
+
`${DEMO}/${encodeURIComponent(orgId)}/reset`,
|
|
5558
|
+
compact({
|
|
5559
|
+
scenario,
|
|
5560
|
+
close_prs: flagStr(flags["close-prs"]),
|
|
5561
|
+
clean_slate: flagBool(flags["clean-slate"]) || undefined,
|
|
5562
|
+
})
|
|
5563
|
+
);
|
|
5564
|
+
printResult(io, flags, result, (r) => r.notes.join("\n"));
|
|
5565
|
+
return 0;
|
|
5566
|
+
}
|
|
5567
|
+
default:
|
|
5568
|
+
throw new CliError(DEMO_USAGE);
|
|
5569
|
+
}
|
|
5570
|
+
}
|
|
5571
|
+
|
|
5388
5572
|
export async function cmdAdmin(ctx) {
|
|
5389
5573
|
const sub = ctx.positionals[0];
|
|
5390
5574
|
if (sub === "replays" || sub === "replay")
|
|
@@ -5405,10 +5589,11 @@ export async function cmdAdmin(ctx) {
|
|
|
5405
5589
|
if (sub === "brand-detection") return cmdAdminBrandDetection({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5406
5590
|
if (sub === "republish") return cmdAdminRepublish({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5407
5591
|
if (sub === "recipes") return cmdAdminRecipes({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5592
|
+
if (sub === "demo") return cmdAdminDemo({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5408
5593
|
if (sub === "suggestion-recipes")
|
|
5409
5594
|
return cmdAdminSuggestionRecipes({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
5410
5595
|
throw new CliError(
|
|
5411
|
-
'Usage: admin recipes … | admin suggestion-recipes … | admin republish … | admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin patterns … | admin experiments … | admin fleet-experiments … | admin crm … | admin spend … | admin evals … | admin brand-detection … | admin tuning … | admin tuning-context | admin replays … (see --help)'
|
|
5596
|
+
'Usage: admin recipes … | admin demo … | admin suggestion-recipes … | admin republish … | admin create-org --domain <bare-domain> [--onboard] … | admin ask "<question>" | admin patterns … | admin experiments … | admin fleet-experiments … | admin crm … | admin spend … | admin evals … | admin brand-detection … | admin tuning … | admin tuning-context | admin replays … (see --help)'
|
|
5412
5597
|
);
|
|
5413
5598
|
}
|
|
5414
5599
|
|