@letterstory/cli 0.176.42 → 0.176.44

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
@@ -78,12 +78,13 @@ import {
78
78
  cmdVary,
79
79
  cmdDomains,
80
80
  cmdBench,
81
+ cmdReplicate,
81
82
  cmdDeploy,
82
83
  } from "./commands.mjs";
83
84
  import { fetchParkedSections, withoutParked } from "./parked.mjs";
84
85
 
85
86
  // Matches cli/package.json; publish-cli.yml rewrites both to the published version.
86
- export const VERSION = "0.176.42";
87
+ export const VERSION = "0.176.44";
87
88
 
88
89
  /** The exit code for a gate refusal (CARD_REQUIRED, PAYMENT_FAILED, OUT_OF_TOKENS): 1 is a plain error. */
89
90
  export const EXIT_PAYMENT_REQUIRED = 3;
@@ -109,6 +110,8 @@ export const BOOLEAN_FLAGS = new Set([
109
110
  "spec-changed",
110
111
  // bench update: remove the README's disclosure.
111
112
  "no-disclosure",
113
+ // replicate show: list only the pages below the bar.
114
+ "failing",
112
115
  // sim create / resample / sample-key: keep the customer's sample key, or delete it now.
113
116
  "keep-key",
114
117
  "delete-key",
@@ -1915,6 +1918,15 @@ Bench (Benches: open-source benchmarks on their own domains, kept running; @lett
1915
1918
  bench cite <id> The reference line a post cites
1916
1919
  bench replicate <id> REPLICATE.md: how an agent reproduces the results
1917
1920
 
1921
+ Replicate (the Replicator: a website rebuilt as a native Next.js repo, page for page; @letterstory.com only):
1922
+ replicate <https://site> [--max-pages <n>] [--repo-name <r>] [--repo-owner <o>]
1923
+ Capture every page, rebuild it, compare it pixel by pixel
1924
+ replicate [list] Your replications: stage, how closely each matches
1925
+ replicate show <id> [--failing] Pages with their routes and pixel match; collections; events
1926
+ replicate attach <id> <repo-name> [--repo-owner <o>] The repo to commit it into (yours; Letter never creates it)
1927
+ replicate advance <id> [--retry] Move it on now; --retry once blocked_on is done
1928
+ replicate archive <id> Stop it; the project stays in storage and in the repo
1929
+
1918
1930
  Evals (how the pipeline is doing on your posts, and the regression cases captured from them):
1919
1931
  evals scoreboard [--collection <id>] [--days N] Every measure by stage: posts, failing, rate
1920
1932
  evals cases [--status open|guarding|proposed|retired|rejected] [--stage research|brief|links|mentions|accuracy|pipeline|shred]
@@ -2021,6 +2033,7 @@ const CLIENT_COMMANDS = {
2021
2033
  vary: cmdVary,
2022
2034
  domains: cmdDomains,
2023
2035
  bench: cmdBench,
2036
+ replicate: cmdReplicate,
2024
2037
  deploy: cmdDeploy,
2025
2038
  // Platform-scoped (staff key) — the only commands that act BEFORE an org exists.
2026
2039
  admin: cmdAdmin,
@@ -0,0 +1,138 @@
1
+ // `replicate` — the Replicator from a terminal (docs/architecture/replicator.md): a website rebuilt
2
+ // as a native Next.js repo, page for page, and committed to a repo on your organization's GitHub.
3
+ // Staff only: the tools answer only a key an @letterstory.com person made. Thin wrappers over the
4
+ // replicator tools (create_replication, list_replications, get_replication,
5
+ // attach_replication_repo, advance_replication, archive_replication), the same lib functions the
6
+ // app calls.
7
+ //
8
+ // letterstory replicate https://example.com [--max-pages 300] [--repo-name example-site] [--repo-owner <account>]
9
+ // letterstory replicate list
10
+ // letterstory replicate show <id> [--failing]
11
+ // letterstory replicate attach <id> <repo-name> [--repo-owner <account>]
12
+ // letterstory replicate advance <id> [--retry]
13
+ // letterstory replicate archive <id>
14
+
15
+ import { CliError } from "../client.mjs";
16
+ import { argsFromFlags, printResult, refuseUnknownFlags } from "./shared.mjs";
17
+
18
+ const replicationId = { positional: 0, label: "replication-id", required: true };
19
+
20
+ export const ARGS = {
21
+ create_replication: {
22
+ url: { positional: 0, label: "url", required: true },
23
+ max_pages: { flag: "max-pages", type: "num" },
24
+ repo_name: "repo-name",
25
+ repo_owner: "repo-owner",
26
+ },
27
+ list_replications: {},
28
+ get_replication: {
29
+ replication_id: replicationId,
30
+ pages: { value: (flags) => (flags.failing === true ? "failing" : undefined) },
31
+ },
32
+ attach_replication_repo: {
33
+ replication_id: replicationId,
34
+ repo_name: { positional: 1, label: "repo-name", required: true },
35
+ repo_owner: "repo-owner",
36
+ },
37
+ advance_replication: { replication_id: replicationId, retry: { flag: "retry", type: "bool" } },
38
+ archive_replication: { replication_id: replicationId },
39
+ };
40
+
41
+ function call(ctx, tool) {
42
+ return ctx.client.callTool(tool, argsFromFlags(ARGS[tool], ctx.flags, ctx.positionals));
43
+ }
44
+
45
+ const pct = (x) => `${(x * 100).toFixed(2)}%`;
46
+
47
+ export async function cmdReplicate(ctx) {
48
+ const sub = ctx.positionals[0];
49
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
50
+ if (sub && /^https?:\/\//i.test(sub)) {
51
+ refuseUnknownFlags(ctx, ARGS.create_replication, []);
52
+ return replicationShow(ctx, await call(ctx, "create_replication"));
53
+ }
54
+ switch (sub) {
55
+ case undefined:
56
+ case "list":
57
+ case "ls":
58
+ return replicationList(rest);
59
+ case "show":
60
+ refuseUnknownFlags(rest, ARGS.get_replication, ["failing"]);
61
+ return replicationDetail(rest, await call(rest, "get_replication"));
62
+ case "attach":
63
+ refuseUnknownFlags(rest, ARGS.attach_replication_repo, []);
64
+ return replicationShow(rest, await call(rest, "attach_replication_repo"));
65
+ case "advance":
66
+ refuseUnknownFlags(rest, ARGS.advance_replication, []);
67
+ return replicationShow(rest, await call(rest, "advance_replication"));
68
+ case "archive":
69
+ return replicationShow(rest, await call(rest, "archive_replication"));
70
+ default:
71
+ throw new CliError("Usage: replicate <https://site> | replicate [list|show|attach|advance|archive]");
72
+ }
73
+ }
74
+
75
+ async function replicationList(ctx) {
76
+ const result = await call(ctx, "list_replications");
77
+ printResult(ctx.io, ctx.flags, result, (r) => {
78
+ if (!r.replications?.length) return "No replications yet. Start one: replicate https://example.com";
79
+ return r.replications
80
+ .map(
81
+ (x) =>
82
+ `${x.id} ${x.host.padEnd(28)} ${(x.blocked_on ? "blocked" : x.status).padEnd(10)} ${x.stage.padEnd(10)} ${
83
+ x.summary
84
+ ? `${x.summary.passing}/${x.summary.replicated} pages match, mean ${pct(x.summary.meanPixel)}`
85
+ : ""
86
+ }`
87
+ )
88
+ .join("\n");
89
+ });
90
+ return 0;
91
+ }
92
+
93
+ function headline(x) {
94
+ return [
95
+ `${x.host} — ${x.status}, stage ${x.stage}`,
96
+ ...(x.blocked_on ? [` Blocked: ${x.blocked_on}`, ` Then: replicate advance ${x.id} --retry`] : []),
97
+ ...(x.stage_note && !x.blocked_on ? [` ${x.stage_note}`] : []),
98
+ ...(x.summary
99
+ ? [
100
+ ` ${x.summary.replicated} of ${x.summary.pages} pages replicated; ${x.summary.passing} match the original at every width (mean ${pct(x.summary.meanPixel)})`,
101
+ ]
102
+ : []),
103
+ ...x.collections.map(
104
+ (c) =>
105
+ ` Collection ${c.pattern}: ${c.items} items (${c.markdown_bodies} as Markdown) in ${c.content_path_template}`
106
+ ),
107
+ ...(x.repo_url
108
+ ? [` Repo ${x.repo_url}${x.commit_sha ? ` @ ${x.commit_sha.slice(0, 7)}` : ""}`]
109
+ : x.repo_name
110
+ ? [` Repo ${x.repo_owner ? `${x.repo_owner}/` : ""}${x.repo_name} (attached)`]
111
+ : []),
112
+ ...(x.stage === "ready" && !x.repo_name ? [` Ready: replicate attach ${x.id} <repo-name> to commit it`] : []),
113
+ ];
114
+ }
115
+
116
+ function replicationShow(ctx, result) {
117
+ printResult(ctx.io, ctx.flags, result, (r) => headline(r.replication).join("\n"));
118
+ return 0;
119
+ }
120
+
121
+ function replicationDetail(ctx, result) {
122
+ printResult(ctx.io, ctx.flags, result, (r) => {
123
+ const lines = headline(r.replication);
124
+ if (r.pages?.length) {
125
+ lines.push("", "Pages:");
126
+ for (const p of r.pages)
127
+ lines.push(
128
+ ` ${p.passing ? "✓" : "✗"} ${p.path.padEnd(48)} ${p.scores.map((s) => `${s.viewport} ${pct(s.pixel)}`).join(" ")}${p.error ? ` (${p.error})` : ""}`
129
+ );
130
+ }
131
+ if (r.dropped?.length)
132
+ lines.push("", `The site's own code left out (needs its builder's runtime): ${r.dropped.length}`);
133
+ if (r.events?.length)
134
+ lines.push("", "Recent:", ...r.events.slice(0, 8).map((e) => ` ${e.created_at} ${e.message}`));
135
+ return lines.join("\n");
136
+ });
137
+ return 0;
138
+ }
@@ -566,10 +566,20 @@ async function repoInstallLetterprove(ctx) {
566
566
  return followRun(ctx, result.run);
567
567
  }
568
568
 
569
- /** `repo install-visitors [--site id] [--repo owner/name] [--branch b] [--wait]`: the Visitors script tag and server snippet → one PR. */
569
+ /** `repo install-visitors [--site id] [--repo owner/name] [--branch b] [--privacy-only] [--wait]`: the Visitors script tag and server snippet → one PR. */
570
570
  async function repoInstallVisitors(ctx) {
571
571
  const { client, flags, io } = ctx;
572
- refuseUnknownFlags(ctx, {}, ["site", "repo", "owner", "name", "gitlab", "branch", "action", "wait"]);
572
+ refuseUnknownFlags(ctx, {}, [
573
+ "site",
574
+ "repo",
575
+ "owner",
576
+ "name",
577
+ "gitlab",
578
+ "branch",
579
+ "action",
580
+ "privacy-only",
581
+ "wait",
582
+ ]);
573
583
  const result = await client.callTool(
574
584
  "visitors_open_install_pr",
575
585
  argsFromFlags(ARGS.visitors_open_install_pr, flags, ctx.positionals)
package/lib/commands.mjs CHANGED
@@ -55,4 +55,5 @@ export * from "./commands/sim.mjs";
55
55
  export * from "./commands/vary.mjs";
56
56
  export * from "./commands/domains.mjs";
57
57
  export * from "./commands/bench.mjs";
58
+ export * from "./commands/replicate.mjs";
58
59
  export * from "./commands/deploy.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.176.42",
3
+ "version": "0.176.44",
4
4
  "description": "Spin up and manage Letter phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",