@letterstory/cli 0.98.4 → 0.100.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 +19 -2
- package/lib/commands/admin.mjs +250 -10
- package/lib/commands/repo.mjs +19 -2
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
} from "./commands.mjs";
|
|
60
60
|
|
|
61
61
|
// Keep in sync with cli/package.json.
|
|
62
|
-
export const VERSION = "0.
|
|
62
|
+
export const VERSION = "0.100.0";
|
|
63
63
|
|
|
64
64
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
65
65
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
@@ -93,6 +93,7 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
93
93
|
"stdin",
|
|
94
94
|
// `authors generate <id> --with-bios`.
|
|
95
95
|
"with-bios",
|
|
96
|
+
"all-live",
|
|
96
97
|
// `admin tuning climb-new … --no-run`; `rung-propose --accept --with-replay` / `rung-accept --with-replay`
|
|
97
98
|
// (not `--replay`: `evals` takes `--replay <stage>`).
|
|
98
99
|
"no-run",
|
|
@@ -213,6 +214,12 @@ export const BOOLEAN_FLAGS = new Set([
|
|
|
213
214
|
"suggested",
|
|
214
215
|
"sync",
|
|
215
216
|
"texts",
|
|
217
|
+
// admin crm outreach (launch-day list)
|
|
218
|
+
"archive",
|
|
219
|
+
"lookup",
|
|
220
|
+
"not-reached",
|
|
221
|
+
"reached",
|
|
222
|
+
"todo",
|
|
216
223
|
]);
|
|
217
224
|
|
|
218
225
|
// Tiny argv parser: `--flag value`, `--flag=value`, boolean `--flag`, and positionals.
|
|
@@ -653,6 +660,11 @@ Domain onboarding (ONE SHOT: a bare domain in, a fully-configured organization o
|
|
|
653
660
|
|
|
654
661
|
Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org exists):
|
|
655
662
|
admin ask "<question>" [--sql] [--json] Ask the whole fleet a question in English.
|
|
663
|
+
admin republish [<deployment-id>…] [--domains a.com,b.com] [--all-live] [--dry-run] [--json]
|
|
664
|
+
Rebuild live phantom sites (no content regenerated), 12 per
|
|
665
|
+
request; prints each failure's reason and exits 1 if any
|
|
666
|
+
failed. Needs a staff key with fleet:rebuild (+ fleet:read
|
|
667
|
+
to resolve --domains / --all-live).
|
|
656
668
|
Answered from the live database with
|
|
657
669
|
read-only SQL across every org; --sql prints
|
|
658
670
|
the queries behind the answer. Needs the
|
|
@@ -691,13 +703,17 @@ Admin (STAFF key — --staff-key / LETTERSTORY_STAFF_API_KEY; acts before an org
|
|
|
691
703
|
Posts on independent sites that name the client,
|
|
692
704
|
with the paragraphs that name it and the live URL.
|
|
693
705
|
| mark <experiment-id> <article-id…> --arm <arm> | unmark <experiment-id> <article-id…>
|
|
694
|
-
| new --template sponsor_disclosure_v1|sponsor_disclosure_clients_v1|article_disclosure_v1|mention_degree_v1|citation_shape_v1
|
|
706
|
+
| new --template sponsor_disclosure_v1|sponsor_disclosure_clients_v1|sponsor_disclosure_fresh_v1|article_disclosure_v1|article_disclosure_fresh_v1|mention_degree_v1|citation_shape_v1
|
|
695
707
|
| new --spec-file spec.json A custom experiment ({key, hypothesis, unit, arms, response_var}).
|
|
696
708
|
| assign <experiment-id> <unit-id…> | assign-eligible <experiment-id> | apply <experiment-id>
|
|
697
709
|
| assign-orgs <experiment-id> <org-domain-or-id…>
|
|
698
710
|
Site/collection arms (safe mode: our own sites, or one
|
|
699
711
|
site per named client). A running experiment applies
|
|
700
712
|
each new unit's arm at once; start applies them all.
|
|
713
|
+
| post-sites | spin-up-posts <experiment-id> <collection-id> [--pairs 1-5]
|
|
714
|
+
Fresh-post experiment: pairs of new posts on sibling
|
|
715
|
+
topics of one site, both naming the client once, one
|
|
716
|
+
born disclosed. Written and published on autopilot.
|
|
701
717
|
| start|stop <experiment-id> | decide <experiment-id> --decision "…"
|
|
702
718
|
Reads need fleet:read; writes fleet:experiment.
|
|
703
719
|
admin experiments list [--q colons] [--status proposed] [--primitive write] [--stage rarity] | show <id>
|
|
@@ -1102,6 +1118,7 @@ Insights:
|
|
|
1102
1118
|
|
|
1103
1119
|
Repo changes (Claude Code edits your repo in a sandbox; the Letterstory GitHub App opens a PR):
|
|
1104
1120
|
repo repos Repos the Letterstory GitHub App can reach
|
|
1121
|
+
repo branches --repo owner/name Branches a change can start from (default first)
|
|
1105
1122
|
repo start --repo <owner/name> --instructions <text>|--instructions-file <path|->
|
|
1106
1123
|
[--branch <base>] [--attach <path>]... [--attach-url <name>=<https url>]... [--wait]
|
|
1107
1124
|
Start a change (attached files go to the sandbox);
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// The old names — evals, eval-add, eval-set, proposals, propose-eval, capture-eval, reject-eval — still work.)
|
|
15
15
|
// admin evals scoreboard|cases|runs|run|triage|guard|retire … (fleet:read / tuning:experiment)
|
|
16
16
|
// admin fleet-experiments list|articles|clients|mark|unmark|new|assign|assign-eligible|assign-orgs|spin-up|apply|start|stop|decide … (fleet:read / fleet:experiment)
|
|
17
|
-
// admin crm import|capture|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|undo-churn|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (crm:read / crm:write)
|
|
17
|
+
// admin crm import|capture|outreach|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|undo-churn|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (crm:read / crm:write)
|
|
18
18
|
// admin spend summary|reconcile|pnl|entries|add|import|usage|remove|reference|stripe-sync|services|service … (spend:read / spend:write)
|
|
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)
|
|
@@ -543,7 +543,7 @@ async function cmdAdminTuningContext(ctx) {
|
|
|
543
543
|
* admin tuning kernels every catalog kernel, live version, writer, candidates
|
|
544
544
|
* admin tuning kernel <service-kernel-id> [--version N|live] one version's file (JSON)
|
|
545
545
|
* admin tuning candidate <service-kernel-id> --file f.json [--note "…"] save a file as a candidate version
|
|
546
|
-
* admin tuning compare --title "…" --candidates Hazel:21,Aspen:20 [--experiment <id>] [--pairs 20] start a comparison (--pairs caps it:
|
|
546
|
+
* admin tuning compare --title "…" --candidates Hazel:21,Aspen:20 [--experiment <id>] [--pairs 20] start a comparison (default 10 pairs; --pairs caps it: 20 for breadth, 66 for depth; comparison-extend adds more)
|
|
547
547
|
* (Hazel:23@22 reads v23 against the test version v22 instead of live; it cannot promote)
|
|
548
548
|
* admin tuning comparisons | comparison <id> list / read one (per-measure verdicts)
|
|
549
549
|
* admin tuning picks <comparison-id> [--criterion opening] the blind queue (next pairs); with a criterion, the pairs not yet read for it (Eye Test mode)
|
|
@@ -656,7 +656,7 @@ async function cmdAdminTuningContext(ctx) {
|
|
|
656
656
|
* admin tuning research-compare --title "…" --candidates <version-id>,… | --patches '[{…},{…}]' several
|
|
657
657
|
* candidates against ONE control arm (k + 1 runs per input, not 2k)
|
|
658
658
|
* admin tuning arena-dials every arena setting: help, default, evidence, live's value
|
|
659
|
-
* admin tuning comparison-extend <comparison-id> --keys k1
|
|
659
|
+
* admin tuning comparison-extend <comparison-id> --pairs N | --keys k1 --keys k2 | --all add the next N pairs to a Write comparison, or inputs to one on fixed inputs
|
|
660
660
|
* admin tuning research-inputs [--set v1] research's fixed inputs
|
|
661
661
|
* admin tuning research-split [--since-hours 72] [--at <iso>,…] what each deploy did to research in production
|
|
662
662
|
* admin tuning research-standing live's level on the fixed inputs, week by week
|
|
@@ -845,7 +845,7 @@ async function cmdAdminTuning(ctx) {
|
|
|
845
845
|
const list = flagList(flags.candidates);
|
|
846
846
|
if (!title || !list?.length)
|
|
847
847
|
throw new CliError(
|
|
848
|
-
'Usage: admin tuning compare --title "…" --candidates Hazel:21,Aspen:20 [--experiment <id>] [--pairs 20]'
|
|
848
|
+
'Usage: admin tuning compare --title "…" --candidates Hazel:21,Aspen:20 [--experiment <id>] [--pairs 20] (default 10 pairs)'
|
|
849
849
|
);
|
|
850
850
|
// kernel:version, or kernel:version@control to read it against a test version instead of live.
|
|
851
851
|
const candidates = list.map((item) => {
|
|
@@ -2238,14 +2238,25 @@ async function cmdAdminTuning(ctx) {
|
|
|
2238
2238
|
return io.log(lines.join("\n"));
|
|
2239
2239
|
}
|
|
2240
2240
|
case "comparison-extend": {
|
|
2241
|
+
// A Write comparison grows by the next N pairs; one on fixed inputs by named keys or all.
|
|
2242
|
+
const pairs = flagNum(flags.pairs);
|
|
2241
2243
|
const keys = flagBool(flags.all) ? "all" : flagList(flags.keys);
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2244
|
+
const usage =
|
|
2245
|
+
"Usage: admin tuning comparison-extend <comparison-id> --pairs N | --keys k1 --keys k2 | --all";
|
|
2246
|
+
if (!id || (pairs === undefined && !keys?.length) || (pairs !== undefined && keys?.length))
|
|
2247
|
+
throw new CliError(usage);
|
|
2248
|
+
if (pairs !== undefined && (!Number.isInteger(pairs) || pairs < 1))
|
|
2249
|
+
throw new CliError("--pairs must be a whole number, 1 or more");
|
|
2250
|
+
const r = await staffRequest(
|
|
2251
|
+
ctx,
|
|
2252
|
+
"POST",
|
|
2253
|
+
`${T}/comparisons/${id}/extend`,
|
|
2254
|
+
pairs !== undefined ? { pairs } : { keys }
|
|
2255
|
+
);
|
|
2245
2256
|
return json
|
|
2246
2257
|
? printResult(io, flags, r)
|
|
2247
2258
|
: io.log(
|
|
2248
|
-
`comparison ${r.comparison.id}: ${r.comparison.added.length} input(s) added, ${r.comparison.pairs} in all`
|
|
2259
|
+
`comparison ${r.comparison.id}: ${r.comparison.added.length} ${pairs !== undefined ? "pair" : "input"}(s) added, ${r.comparison.pairs} in all`
|
|
2249
2260
|
);
|
|
2250
2261
|
}
|
|
2251
2262
|
case "research-inputs": {
|
|
@@ -2819,6 +2830,133 @@ async function cmdAdminCrmCapture(ctx, paths) {
|
|
|
2819
2830
|
}
|
|
2820
2831
|
}
|
|
2821
2832
|
|
|
2833
|
+
/**
|
|
2834
|
+
* `admin crm outreach …` — the launch-day list (or `--list <key>`), the same operations the
|
|
2835
|
+
* CRM's Launch day tab calls (src/lib/crm/outreach.ts, /api/superadmin/crm/outreach):
|
|
2836
|
+
*
|
|
2837
|
+
* outreach [list] [--owner Neha] [--todo] who is on it; --todo = not reached yet
|
|
2838
|
+
* outreach add "Jane Doe" [--company …] [--owner …] [--channel x] [--relationship …]
|
|
2839
|
+
* [--ask …] [--linkedin …] [--email …] [--lookup]
|
|
2840
|
+
* outreach lookup "Jane Doe from Acme" [shot.png …] prefilled from the web; writes nothing
|
|
2841
|
+
* outreach set <id> [--reached|--not-reached] [--response "…" --tone positive]
|
|
2842
|
+
* [--owner-id <uuid>] [--channel …] [--ask …] [--draft "…"] [--archive|--restore]
|
|
2843
|
+
*/
|
|
2844
|
+
async function cmdAdminCrmOutreach(ctx, args) {
|
|
2845
|
+
const { flags, io } = ctx;
|
|
2846
|
+
const [sub = "list", ...rest] = args;
|
|
2847
|
+
const list = flagStr(flags.list);
|
|
2848
|
+
const json = flagBool(flags.json);
|
|
2849
|
+
const line = (p) =>
|
|
2850
|
+
[
|
|
2851
|
+
p.reachedAt ? "✓" : "·",
|
|
2852
|
+
p.name,
|
|
2853
|
+
p.company ? `(${p.company})` : null,
|
|
2854
|
+
p.ownerName ? `— ${p.ownerName}` : "— unowned",
|
|
2855
|
+
p.channel ? `via ${p.channel}` : null,
|
|
2856
|
+
p.response ? `“${p.response}”` : null,
|
|
2857
|
+
` ${p.id}`,
|
|
2858
|
+
]
|
|
2859
|
+
.filter(Boolean)
|
|
2860
|
+
.join(" ");
|
|
2861
|
+
switch (sub) {
|
|
2862
|
+
case "list": {
|
|
2863
|
+
const r = await staffRequest(
|
|
2864
|
+
ctx,
|
|
2865
|
+
"GET",
|
|
2866
|
+
`${CRM}/outreach${list ? `?list=${encodeURIComponent(list)}` : ""}`
|
|
2867
|
+
);
|
|
2868
|
+
const owner = flagStr(flags.owner)?.toLowerCase();
|
|
2869
|
+
const people = r.people
|
|
2870
|
+
.filter((p) => !owner || (p.ownerName ?? "").toLowerCase().includes(owner))
|
|
2871
|
+
.filter((p) => !flagBool(flags.todo) || !p.reachedAt);
|
|
2872
|
+
if (json) return printResult(io, flags, { ...r, people });
|
|
2873
|
+
const reached = r.people.filter((p) => p.reachedAt).length;
|
|
2874
|
+
io.log(`${r.list}: ${r.people.length} people, ${reached} reached`);
|
|
2875
|
+
return io.log(people.length ? people.map(line).join("\n") : "(nobody)");
|
|
2876
|
+
}
|
|
2877
|
+
case "add": {
|
|
2878
|
+
const name = rest.join(" ").trim();
|
|
2879
|
+
if (!name)
|
|
2880
|
+
throw new CliError('Usage: admin crm outreach add "Jane Doe" [--company …] [--owner …] [--lookup]');
|
|
2881
|
+
const r = await staffPost(
|
|
2882
|
+
ctx,
|
|
2883
|
+
`${CRM}/outreach`,
|
|
2884
|
+
compact({
|
|
2885
|
+
list,
|
|
2886
|
+
source: "api",
|
|
2887
|
+
lookup: flagBool(flags.lookup) || undefined,
|
|
2888
|
+
people: [
|
|
2889
|
+
compact({
|
|
2890
|
+
name,
|
|
2891
|
+
company: flagStr(flags.company),
|
|
2892
|
+
title: flagStr(flags.title),
|
|
2893
|
+
owner: flagStr(flags.owner),
|
|
2894
|
+
channel: flagStr(flags.channel),
|
|
2895
|
+
relationship: flagStr(flags.relationship),
|
|
2896
|
+
ask: flagStr(flags.ask),
|
|
2897
|
+
linkedinUrl: flagStr(flags.linkedin),
|
|
2898
|
+
email: flagStr(flags.email),
|
|
2899
|
+
notes: flagStr(flags.notes),
|
|
2900
|
+
}),
|
|
2901
|
+
],
|
|
2902
|
+
})
|
|
2903
|
+
);
|
|
2904
|
+
if (json) return printResult(io, flags, r);
|
|
2905
|
+
for (const p of r.created) io.log(`added ${line(p)}`);
|
|
2906
|
+
for (const d of r.duplicates)
|
|
2907
|
+
io.log(`already on the list: ${d.existingName} (matched on ${d.matchedOn}) ${d.existingId}`);
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
case "lookup": {
|
|
2911
|
+
const images = rest
|
|
2912
|
+
.filter((a) => CRM_CAPTURE_TYPES[a.split(".").pop()?.toLowerCase() ?? ""])
|
|
2913
|
+
.map((path) => ({
|
|
2914
|
+
media_type: CRM_CAPTURE_TYPES[path.split(".").pop().toLowerCase()],
|
|
2915
|
+
data: readFileSync(path).toString("base64"),
|
|
2916
|
+
}));
|
|
2917
|
+
const text = rest
|
|
2918
|
+
.filter((a) => !CRM_CAPTURE_TYPES[a.split(".").pop()?.toLowerCase() ?? ""])
|
|
2919
|
+
.join(" ")
|
|
2920
|
+
.trim();
|
|
2921
|
+
if (!text && !images.length)
|
|
2922
|
+
throw new CliError('Usage: admin crm outreach lookup "Jane Doe from Acme" [shot.png …]');
|
|
2923
|
+
const r = await staffPost(
|
|
2924
|
+
ctx,
|
|
2925
|
+
`${CRM}/outreach/lookup`,
|
|
2926
|
+
compact({ text: text || undefined, images: images.length ? images : undefined })
|
|
2927
|
+
);
|
|
2928
|
+
if (json) return printResult(io, flags, r);
|
|
2929
|
+
for (const p of r.people)
|
|
2930
|
+
io.log(
|
|
2931
|
+
`${p.name}${p.title ? `, ${p.title}` : ""}${p.company ? ` @ ${p.company}` : ""}${p.fundingStage ? ` · ${p.fundingStage}` : ""}${p.ycBatch ? ` · YC ${p.ycBatch}` : ""}${p.lookup ? ` [${p.lookup.confidence}: ${p.lookup.why}]` : ""}`
|
|
2932
|
+
);
|
|
2933
|
+
if (r.notes) io.log(`notes: ${r.notes}`);
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
case "set": {
|
|
2937
|
+
const [id] = rest;
|
|
2938
|
+
if (!id)
|
|
2939
|
+
throw new CliError('Usage: admin crm outreach set <id> [--reached|--not-reached] [--response "…"] …');
|
|
2940
|
+
const body = compact({
|
|
2941
|
+
reached: flagBool(flags.reached) ? true : flagBool(flags["not-reached"]) ? false : undefined,
|
|
2942
|
+
response: flagStr(flags.response),
|
|
2943
|
+
responseTone: flagStr(flags.tone),
|
|
2944
|
+
ownerId: flagStr(flags["owner-id"]),
|
|
2945
|
+
channel: flagStr(flags.channel),
|
|
2946
|
+
ask: flagStr(flags.ask),
|
|
2947
|
+
relationship: flagStr(flags.relationship),
|
|
2948
|
+
draftMessage: flagStr(flags.draft),
|
|
2949
|
+
notes: flagStr(flags.notes),
|
|
2950
|
+
archived: flagBool(flags.archive) ? true : flagBool(flags.restore) ? false : undefined,
|
|
2951
|
+
});
|
|
2952
|
+
const r = await staffRequest(ctx, "PATCH", `${CRM}/outreach/${id}`, body);
|
|
2953
|
+
return json ? printResult(io, flags, r) : io.log(line(r));
|
|
2954
|
+
}
|
|
2955
|
+
default:
|
|
2956
|
+
throw new CliError("Usage: admin crm outreach [list|add|lookup|set] … (see --help)");
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2822
2960
|
/**
|
|
2823
2961
|
* `admin crm …` — the headless half of the staff CRM, so a background agent can watch the
|
|
2824
2962
|
* pipeline and record the signals that have no webhook (Bookface, tweet likes, DMs).
|
|
@@ -2835,6 +2973,8 @@ async function cmdAdminCrm(ctx) {
|
|
|
2835
2973
|
return cmdAdminCrmImport(ctx, id);
|
|
2836
2974
|
case "capture":
|
|
2837
2975
|
return cmdAdminCrmCapture(ctx, ctx.positionals.slice(1));
|
|
2976
|
+
case "outreach":
|
|
2977
|
+
return cmdAdminCrmOutreach(ctx, ctx.positionals.slice(1));
|
|
2838
2978
|
case "signals": {
|
|
2839
2979
|
const params = new URLSearchParams(
|
|
2840
2980
|
compact({
|
|
@@ -3076,7 +3216,7 @@ async function cmdAdminCrm(ctx) {
|
|
|
3076
3216
|
}
|
|
3077
3217
|
default:
|
|
3078
3218
|
throw new CliError(
|
|
3079
|
-
"Usage: admin crm import|capture|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|undo-churn|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (see --help)"
|
|
3219
|
+
"Usage: admin crm import|capture|outreach|signals|record|signal|accounts|account|score|slack-channels|link-slack|white-glove|set-white-glove|churn|record-churn|undo-churn|onboard|onboarding|retry-onboarding|onboard-plan|payment-link … (see --help)"
|
|
3080
3220
|
);
|
|
3081
3221
|
}
|
|
3082
3222
|
}
|
|
@@ -3847,6 +3987,42 @@ async function cmdAdminFleetExperiments(ctx) {
|
|
|
3847
3987
|
].join("\n")
|
|
3848
3988
|
);
|
|
3849
3989
|
}
|
|
3990
|
+
case "post-sites": {
|
|
3991
|
+
const result = await staffRequest(ctx, "GET", `${FLEET_EXPERIMENTS}/post-sites`);
|
|
3992
|
+
return out(result, (r) =>
|
|
3993
|
+
r.sites.length
|
|
3994
|
+
? r.sites
|
|
3995
|
+
.map(
|
|
3996
|
+
(s) =>
|
|
3997
|
+
`${s.collectionId} ${s.orgName} · ${s.siteName} ${s.notEligible ? `(can't: ${s.notEligible})` : `up to ${s.pairsAvailable} pair${s.pairsAvailable === 1 ? "" : "s"}`}`
|
|
3998
|
+
)
|
|
3999
|
+
.join("\n")
|
|
4000
|
+
: "(no sites)"
|
|
4001
|
+
);
|
|
4002
|
+
}
|
|
4003
|
+
case "spin-up-posts": {
|
|
4004
|
+
const collectionId = rest.find(Boolean);
|
|
4005
|
+
if (!id || !collectionId)
|
|
4006
|
+
throw new CliError(
|
|
4007
|
+
"Usage: admin fleet-experiments spin-up-posts <experiment-id> <collection-id> [--pairs n] (see post-sites)"
|
|
4008
|
+
);
|
|
4009
|
+
const body = {
|
|
4010
|
+
action: "spin_up_posts",
|
|
4011
|
+
experiment_id: id,
|
|
4012
|
+
collection_id: collectionId,
|
|
4013
|
+
...(flagStr(flags.pairs) ? { pairs: Number(flagStr(flags.pairs)) } : {}),
|
|
4014
|
+
};
|
|
4015
|
+
return out(await action(body), (r) =>
|
|
4016
|
+
[
|
|
4017
|
+
`launching ${r.launched.length} pair${r.launched.length === 1 ? "" : "s"} on ${r.site}:`,
|
|
4018
|
+
...r.launched.map(
|
|
4019
|
+
(p) =>
|
|
4020
|
+
` ${p.pair}: ${p.posts.map((x) => `${x.arm} "${x.topic ?? x.topicId}" (run ${x.runId})`).join(" vs ")}`
|
|
4021
|
+
),
|
|
4022
|
+
...r.failed.map((f) => ` not launched (${f.pair}): ${f.message}`),
|
|
4023
|
+
].join("\n")
|
|
4024
|
+
);
|
|
4025
|
+
}
|
|
3850
4026
|
case "start":
|
|
3851
4027
|
case "stop":
|
|
3852
4028
|
if (!id) throw new CliError(`Usage: admin fleet-experiments ${sub} <experiment-id>`);
|
|
@@ -3864,6 +4040,69 @@ async function cmdAdminFleetExperiments(ctx) {
|
|
|
3864
4040
|
}
|
|
3865
4041
|
}
|
|
3866
4042
|
|
|
4043
|
+
const REPUBLISH = "/api/superadmin/deployments/republish";
|
|
4044
|
+
/** Sites per POST — the same chunk the Fleet dialog sends. */
|
|
4045
|
+
const REPUBLISH_BATCH = 12;
|
|
4046
|
+
|
|
4047
|
+
/**
|
|
4048
|
+
* `admin republish`: the Fleet "Republish sites" action for an agent. Pick sites by
|
|
4049
|
+
* id, by --domains, or --all-live (every live, unpaused site); rebuild them in
|
|
4050
|
+
* batches; print every failure with its reason. Exit 1 when any site failed, so a
|
|
4051
|
+
* scheduled agent can tell a partial run from a clean one.
|
|
4052
|
+
*/
|
|
4053
|
+
async function cmdAdminRepublish(ctx) {
|
|
4054
|
+
const { flags, io } = ctx;
|
|
4055
|
+
const domains = flagList(flags.domains)
|
|
4056
|
+
.flatMap((d) => d.split(","))
|
|
4057
|
+
.map((d) =>
|
|
4058
|
+
d
|
|
4059
|
+
.trim()
|
|
4060
|
+
.toLowerCase()
|
|
4061
|
+
.replace(/^www\./, "")
|
|
4062
|
+
)
|
|
4063
|
+
.filter(Boolean);
|
|
4064
|
+
const allLive = flagBool(flags["all-live"]);
|
|
4065
|
+
let ids = ctx.positionals.filter(Boolean);
|
|
4066
|
+
let names = new Map();
|
|
4067
|
+
if (allLive || domains.length) {
|
|
4068
|
+
const { sites = [] } = await staffRequest(ctx, "GET", REPUBLISH);
|
|
4069
|
+
names = new Map(sites.map((s) => [s.id, s.domain ?? s.subdomain ?? s.name]));
|
|
4070
|
+
const picked = allLive
|
|
4071
|
+
? sites
|
|
4072
|
+
: sites.filter((s) => domains.includes((s.domain ?? "").toLowerCase().replace(/^www\./, "")));
|
|
4073
|
+
if (domains.length) {
|
|
4074
|
+
const found = new Set(picked.map((s) => (s.domain ?? "").toLowerCase().replace(/^www\./, "")));
|
|
4075
|
+
const missing = domains.filter((d) => !found.has(d));
|
|
4076
|
+
if (missing.length) io.error(`! Not a live, unpaused site: ${missing.join(", ")}`);
|
|
4077
|
+
}
|
|
4078
|
+
ids = [...new Set([...ids, ...picked.map((s) => s.id)])];
|
|
4079
|
+
}
|
|
4080
|
+
if (ids.length === 0) {
|
|
4081
|
+
throw new CliError(
|
|
4082
|
+
"Usage: admin republish [<deployment-id>…] [--domains a.com,b.com] [--all-live] [--dry-run]"
|
|
4083
|
+
);
|
|
4084
|
+
}
|
|
4085
|
+
if (flagBool(flags["dry-run"])) {
|
|
4086
|
+
io.log(`Would republish ${ids.length} site(s):`);
|
|
4087
|
+
for (const id of ids) io.log(` ${names.get(id) ?? id} ${id}`);
|
|
4088
|
+
return 0;
|
|
4089
|
+
}
|
|
4090
|
+
const results = [];
|
|
4091
|
+
for (let i = 0; i < ids.length; i += REPUBLISH_BATCH) {
|
|
4092
|
+
const res = await staffPost(ctx, REPUBLISH, { deployment_ids: ids.slice(i, i + REPUBLISH_BATCH) });
|
|
4093
|
+
results.push(...(res.results ?? []));
|
|
4094
|
+
if (!flags.json) io.error(`… ${Math.min(i + REPUBLISH_BATCH, ids.length)}/${ids.length}`);
|
|
4095
|
+
}
|
|
4096
|
+
const failed = results.filter((r) => !r.ok);
|
|
4097
|
+
printResult(io, flags, { rebuilt: results.length - failed.length, failed: failed.length, results }, () =>
|
|
4098
|
+
[
|
|
4099
|
+
`Rebuilt ${results.length - failed.length} site(s), ${failed.length} failed. Live pages refresh once each build finishes (~1–2 min).`,
|
|
4100
|
+
...failed.map((r) => ` ✗ ${r.name ?? names.get(r.id) ?? r.id} → ${r.error ?? "unknown error"}`),
|
|
4101
|
+
].join("\n")
|
|
4102
|
+
);
|
|
4103
|
+
return failed.length ? 1 : 0;
|
|
4104
|
+
}
|
|
4105
|
+
|
|
3867
4106
|
export async function cmdAdmin(ctx) {
|
|
3868
4107
|
const sub = ctx.positionals[0];
|
|
3869
4108
|
if (sub === "replays" || sub === "replay")
|
|
@@ -3880,8 +4119,9 @@ export async function cmdAdmin(ctx) {
|
|
|
3880
4119
|
if (sub === "tuning") return cmdAdminTuning({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3881
4120
|
if (sub === "evals") return cmdAdminEvals({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3882
4121
|
if (sub === "brand-detection") return cmdAdminBrandDetection({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
4122
|
+
if (sub === "republish") return cmdAdminRepublish({ ...ctx, positionals: ctx.positionals.slice(1) });
|
|
3883
4123
|
throw new CliError(
|
|
3884
|
-
'Usage: 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)'
|
|
4124
|
+
'Usage: 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)'
|
|
3885
4125
|
);
|
|
3886
4126
|
}
|
|
3887
4127
|
|
package/lib/commands/repo.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// pull request by the Letterstory GitHub App (src/lib/repo-changes). One subcommand per tool:
|
|
3
3
|
// repo start → start_repo_change repo status → get_repo_change
|
|
4
4
|
// repo list → list_repo_changes repo cancel → cancel_repo_change
|
|
5
|
-
// repo repos → list_github_app_repos
|
|
5
|
+
// repo repos → list_github_app_repos repo branches → list_repo_branches
|
|
6
6
|
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
8
8
|
import { basename } from "node:path";
|
|
@@ -64,6 +64,10 @@ const ARGS = {
|
|
|
64
64
|
audit_id: "audit",
|
|
65
65
|
},
|
|
66
66
|
list_github_app_repos: {},
|
|
67
|
+
list_repo_branches: {
|
|
68
|
+
repo_owner: { value: (flags) => splitRepo(flags).owner },
|
|
69
|
+
repo_name: { value: (flags) => splitRepo(flags).name },
|
|
70
|
+
},
|
|
67
71
|
};
|
|
68
72
|
|
|
69
73
|
const TERMINAL = new Set(["succeeded", "no_changes", "failed", "cancelled"]);
|
|
@@ -100,9 +104,11 @@ export async function cmdRepo(ctx) {
|
|
|
100
104
|
return repoCancel(rest);
|
|
101
105
|
case "repos":
|
|
102
106
|
return repoRepos(rest);
|
|
107
|
+
case "branches":
|
|
108
|
+
return repoBranches(rest);
|
|
103
109
|
default:
|
|
104
110
|
throw new CliError(
|
|
105
|
-
`Unknown repo subcommand: ${sub ?? "(none)"}. Try: start, fix-audit, sites, set-site, reset-site, status, list, cancel, repos`
|
|
111
|
+
`Unknown repo subcommand: ${sub ?? "(none)"}. Try: start, fix-audit, sites, set-site, reset-site, status, list, cancel, repos, branches`
|
|
106
112
|
);
|
|
107
113
|
}
|
|
108
114
|
}
|
|
@@ -302,4 +308,15 @@ async function repoRepos(ctx) {
|
|
|
302
308
|
return 0;
|
|
303
309
|
}
|
|
304
310
|
|
|
311
|
+
/** `repo branches --repo owner/name`: the branches a run can start from, default first. */
|
|
312
|
+
async function repoBranches(ctx) {
|
|
313
|
+
const { client, flags, io } = ctx;
|
|
314
|
+
refuseUnknownFlags(ctx, {}, ["repo", "owner", "name"]);
|
|
315
|
+
const result = await client.callTool("list_repo_branches", argsFromFlags(ARGS.list_repo_branches, flags));
|
|
316
|
+
printResult(io, flags, result, (r) =>
|
|
317
|
+
r.branches.map((b) => (b === r.defaultBranch ? `${b} (default)` : b)).join("\n")
|
|
318
|
+
);
|
|
319
|
+
return 0;
|
|
320
|
+
}
|
|
321
|
+
|
|
305
322
|
export { ARGS };
|