@letterstory/cli 0.176.26 → 0.176.28

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,11 +78,12 @@ import {
78
78
  cmdVary,
79
79
  cmdDomains,
80
80
  cmdBench,
81
+ cmdDeploy,
81
82
  } from "./commands.mjs";
82
83
  import { fetchParkedSections, withoutParked } from "./parked.mjs";
83
84
 
84
85
  // Matches cli/package.json; publish-cli.yml rewrites both to the published version.
85
- export const VERSION = "0.176.26";
86
+ export const VERSION = "0.176.28";
86
87
 
87
88
  /** The exit code for a gate refusal (CARD_REQUIRED, PAYMENT_FAILED, OUT_OF_TOKENS): 1 is a plain error. */
88
89
  export const EXIT_PAYMENT_REQUIRED = 3;
@@ -93,6 +94,9 @@ export const EXIT_PAYMENT_REQUIRED = 3;
93
94
  export const BOOLEAN_FLAGS = new Set([
94
95
  // build list: also move the builds' open changes along while polling.
95
96
  "advance",
97
+ // build: a website or app of its own (deploy create's path). deploy create: hold until its build starts.
98
+ "new-site",
99
+ "wait",
96
100
  // automations set: stop reading a data feed.
97
101
  "no-reads",
98
102
  // vary start: pause the test running on the page first.
@@ -615,7 +619,7 @@ Reviewers:
615
619
  automate list Your automations: an event, which ones it acts on,
616
620
  and what Letterpress then does
617
621
  automate show <automation-id>
618
- automate add [--template post_new_issues|fix_new_issues|recommended_actions|competitor_pages|icp_visitors|plan_updates]
622
+ automate add [--template post_new_issues|recommended_actions|competitor_pages|icp_visitors|plan_updates]
619
623
  [--name <n>] [--trigger competitor_page|visitor_identified|issue_found|build_suggestion|plan_update|custom]
620
624
  [--prompt <what Letterpress does with each event>] [--slack-channel <C…>] [--instruction <which events>]
621
625
  [--event <name>] [--kind post|landing] [--competitor <host>] [--site <host>]
@@ -1689,11 +1693,12 @@ Insights:
1689
1693
  Build (a change to your site, or launching what isn't code; the Build box's own door):
1690
1694
  build <words…> [--prompt <text>] [--site <host>] [--subject <host/path>]... [--action build|build_pr|build_merge|slack|linear]
1691
1695
  [--attach <path>]... [--attach-url <name>=<https url>]... [--attach-ref <path>]... [--attach-url-ref <name>=<https url>]...
1692
- [--harness letterpress-1|claude-code|codex] [--effort low|medium|high|xhigh] [--notify-slack] [--again]
1696
+ [--harness letterpress-1|claude-code|codex] [--effort low|medium|high|xhigh] [--notify-slack] [--again] [--new-site]
1693
1697
  Build from your words: "add a pricing page" opens a preview or PR;
1694
1698
  "launch a phantom for us", "set up mailers" or "onboard my blog" is
1695
1699
  done by Build itself (a phantom site buys its own domain), and the
1696
- build reads Done with no PR to merge. --again repeats an open build's ask
1700
+ build reads Done with no PR to merge. --again repeats an open build's ask.
1701
+ --new-site: a website of its own, built and hosted (deploy create)
1697
1702
  build talk <words…> [--conversation <id>|--build <id>] [--site <host>] [--harness letterpress-1|claude-code|codex]
1698
1703
  [--repo owner/name] [--action build|build_pr|build_merge]
1699
1704
  One turn of a conversation with Build (questions, follow-ups on a build)
@@ -1859,6 +1864,19 @@ Vary (A/B tests of a page's words and images on your own site):
1859
1864
  vary start <id> [--pause-running] | vary pause <id> | vary end <id> [--winner b]
1860
1865
  vary remove-variant <id> <key> | vary delete <id>
1861
1866
 
1867
+ Deploy (websites and apps Letter builds from your words and hosts on their own domains):
1868
+ deploy [list] [--kind app|phantom|bench] Everything deployed: apps, phantoms (and benches, for staff)
1869
+ deploy create "<what it is and who it is for>" [--name <n>] [--domain <d>] [--wait]
1870
+ Named, given a domain (bought under $30 unless you name one),
1871
+ built by Letterpress-1 with Visitors in it, and put live
1872
+ deploy show <id> Where it stands, its domain, the records to set, recent events
1873
+ deploy update <id> [--name <n>] [--domain <d>] The domain only before it is built
1874
+ deploy pause <id> | deploy resume <id> | deploy archive <id>
1875
+ deploy advance <id> [--retry] Move it on now; --retry once blocked_on is done
1876
+ deploy source <id> A link to download its source, as Letter keeps it
1877
+ deploy connect <id> <owner/name> Move its source to your GitHub repo (empty); it's
1878
+ pushed once the Letter GitHub App can reach it
1879
+
1862
1880
  Bench (Benches: open-source benchmarks on their own domains, kept running; @letterstory.com only):
1863
1881
  bench [list] Your benches: stage, what each waits on, domain
1864
1882
  bench create (--prompt "<what it is and how it works>"|--prompt-file <path>) [--name <n>] [--domain <d>]
@@ -1982,6 +2000,7 @@ const CLIENT_COMMANDS = {
1982
2000
  vary: cmdVary,
1983
2001
  domains: cmdDomains,
1984
2002
  bench: cmdBench,
2003
+ deploy: cmdDeploy,
1985
2004
  // Platform-scoped (staff key) — the only commands that act BEFORE an org exists.
1986
2005
  admin: cmdAdmin,
1987
2006
  };
@@ -8,14 +8,7 @@ import { CliError } from "../client.mjs";
8
8
  import { printResult, ok, argsFromFlags, flagList, refuseUnknownFlags } from "./shared.mjs";
9
9
 
10
10
  const TRIGGERS = ["competitor_page", "visitor_identified", "issue_found", "build_suggestion", "plan_update", "custom"];
11
- const TEMPLATES = [
12
- "post_new_issues",
13
- "fix_new_issues",
14
- "recommended_actions",
15
- "competitor_pages",
16
- "icp_visitors",
17
- "plan_updates",
18
- ];
11
+ const TEMPLATES = ["post_new_issues", "recommended_actions", "competitor_pages", "icp_visitors", "plan_updates"];
19
12
  const STATUSES = ["pending", "proposed", "acting", "acted", "skipped", "failed", "rejected"];
20
13
 
21
14
  const triggerParams = {
@@ -39,6 +39,8 @@ const ARGS = {
39
39
  again: { flag: "again", type: "bool" },
40
40
  // --recipe <id>: the recipe the build carries out (a Plan row's), recorded on it.
41
41
  recipe: "recipe",
42
+ // --new-site: a website or app of its own, built and hosted on its own domain (deploy create's path).
43
+ new_site: { flag: "new-site", type: "bool" },
42
44
  // --plan-only: answer what it would do and start nothing (--json for the plan to send back).
43
45
  plan_only: { flag: "plan-only", type: "bool" },
44
46
  // --plan-file <path>: build exactly the plan --plan-only answered.
@@ -0,0 +1,150 @@
1
+ // `deploy` — Deployments from a terminal (docs/architecture/deployments.md): websites and apps Letter
2
+ // builds from your words and hosts on their own domains, with your phantoms and benches in the list.
3
+ // Thin wrappers over the deployment tools (create_deployment, list_deployments, get_deployment,
4
+ // update_deployment, advance_deployment), the same lib functions the app calls.
5
+ //
6
+ // letterstory deploy create "Build me a website for HVAC contractors to collect leads" [--name <n>] [--domain <d>] [--wait]
7
+ // letterstory deploy list [--kind app|phantom|bench]
8
+ // letterstory deploy show <id>
9
+ // letterstory deploy update <id> [--name <n>] [--domain <d>]
10
+ // letterstory deploy pause <id> | resume <id> | archive <id>
11
+ // letterstory deploy advance <id> [--retry]
12
+
13
+ import { CliError } from "../client.mjs";
14
+ import { argsFromFlags, flagStr, printResult, refuseUnknownFlags } from "./shared.mjs";
15
+
16
+ const appId = { positional: 0, label: "app-id", required: true };
17
+
18
+ /** `owner/name` from the command line, one part of it. */
19
+ function repoPart(value, i) {
20
+ const parts = String(value ?? "").split("/");
21
+ if (parts.length !== 2 || !parts[0] || !parts[1])
22
+ throw new CliError("Name the repo as owner/name: deploy connect <id> acme/site");
23
+ return parts[i];
24
+ }
25
+
26
+ export const ARGS = {
27
+ create_deployment: {
28
+ // The words after `create`, or --prompt for a longer ask.
29
+ prompt: { value: (flags, positionals) => flagStr(flags.prompt) ?? (positionals.join(" ").trim() || undefined) },
30
+ name: "name",
31
+ domain: "domain",
32
+ wait: { flag: "wait", type: "bool" },
33
+ },
34
+ list_deployments: { kind: { flag: "kind", enum: ["app", "phantom", "bench"] } },
35
+ get_deployment: { id: appId },
36
+ update_deployment: {
37
+ id: appId,
38
+ name: "name",
39
+ domain: "domain",
40
+ status: { flag: "status", enum: ["live", "paused", "archived"] },
41
+ },
42
+ advance_deployment: { id: appId, retry: { flag: "retry", type: "bool" } },
43
+ connect_deployment_repo: {
44
+ id: appId,
45
+ repo_owner: { value: (_flags, positionals) => repoPart(positionals[1], 0) },
46
+ repo_name: { value: (_flags, positionals) => repoPart(positionals[1], 1) },
47
+ },
48
+ export_deployment_source: { id: appId },
49
+ };
50
+
51
+ function call(ctx, tool) {
52
+ return ctx.client.callTool(tool, argsFromFlags(ARGS[tool], ctx.flags, ctx.positionals));
53
+ }
54
+
55
+ export async function cmdDeploy(ctx) {
56
+ const sub = ctx.positionals[0];
57
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
58
+ switch (sub) {
59
+ case undefined:
60
+ case "list":
61
+ case "ls":
62
+ return deployList(rest);
63
+ case "create": {
64
+ refuseUnknownFlags(rest, ARGS.create_deployment, ["prompt"]);
65
+ const args = argsFromFlags(ARGS.create_deployment, rest.flags, rest.positionals);
66
+ if (!args.prompt)
67
+ throw new CliError(
68
+ 'Say what to build: letterstory deploy create "a website for HVAC contractors to collect leads"'
69
+ );
70
+ return deployShow(rest, await rest.client.callTool("create_deployment", args));
71
+ }
72
+ case "show":
73
+ return deployShow(rest, await call(rest, "get_deployment"));
74
+ case "update":
75
+ refuseUnknownFlags(rest, ARGS.update_deployment, []);
76
+ return deployShow(rest, await call(rest, "update_deployment"));
77
+ case "pause":
78
+ case "resume":
79
+ case "archive":
80
+ rest.flags = { ...rest.flags, status: { pause: "paused", resume: "live", archive: "archived" }[sub] };
81
+ return deployShow(rest, await call(rest, "update_deployment"));
82
+ case "advance":
83
+ refuseUnknownFlags(rest, ARGS.advance_deployment, []);
84
+ return deployShow(rest, await call(rest, "advance_deployment"));
85
+ case "connect":
86
+ return deployShow(rest, await call(rest, "connect_deployment_repo"));
87
+ case "source": {
88
+ const result = await call(rest, "export_deployment_source");
89
+ printResult(
90
+ rest.io,
91
+ rest.flags,
92
+ result,
93
+ (r) => `${r.url}\n(the source of main, ${r.sha.slice(0, 7)}, for an hour)`
94
+ );
95
+ return 0;
96
+ }
97
+ default:
98
+ throw new CliError("Usage: deploy [list|create|show|update|pause|resume|archive|advance|connect|source]");
99
+ }
100
+ }
101
+
102
+ async function deployList(ctx) {
103
+ const result = await call(ctx, "list_deployments");
104
+ printResult(ctx.io, ctx.flags, result, (r) => {
105
+ if (!r.deployments?.length)
106
+ return 'Nothing deployed yet. Build a site: deploy create "<what it is and who it is for>"';
107
+ return r.deployments
108
+ .map(
109
+ (d) =>
110
+ `${d.id} ${d.kind.padEnd(7)} ${d.name.slice(0, 28).padEnd(28)} ${(d.blocked ? "blocked" : d.status).padEnd(10)} ${d.url ?? d.domain ?? ""}`
111
+ )
112
+ .join("\n");
113
+ });
114
+ return 0;
115
+ }
116
+
117
+ function deployShow(ctx, result) {
118
+ printResult(ctx.io, ctx.flags, result, (r) => {
119
+ const a = r.app;
120
+ const lines = [
121
+ `${a.name} — ${a.status}, stage ${a.stage}`,
122
+ ...(a.blocked_on ? [` Blocked: ${a.blocked_on}`, ` Then: deploy advance ${a.id} --retry`] : []),
123
+ ...(a.stage_note && !a.blocked_on ? [` ${a.stage_note}`] : []),
124
+ ...(a.domain ? [` Domain ${a.domain}${a.domain_set_by === "letter" ? " (chosen and bought)" : ""}`] : []),
125
+ ...(a.url ? [` Site ${a.url}${a.site_ok === false ? " (not serving at the last check)" : ""}`] : []),
126
+ ...(a.run_id ? [` Run ${a.run_id} (repo show ${a.run_id})`] : []),
127
+ ...(a.deployed ? [` Live ${a.deployed.sha.slice(0, 7)} since ${a.deployed.at}`] : []),
128
+ ...(a.last_deploy_error ? [` Last release didn't start: ${a.last_deploy_error.message}`] : []),
129
+ ...(a.source?.kept_by === "github"
130
+ ? [` Source ${a.source.url}`]
131
+ : a.source?.repo
132
+ ? [` Source kept by Letter (deploy source ${a.id}; deploy connect ${a.id} owner/name)`]
133
+ : []),
134
+ ...(a.source?.pending
135
+ ? [` Moving to ${a.source.pending.repo}: ${a.source.pending.note ?? "trying now"}`]
136
+ : []),
137
+ ];
138
+ if (a.dns?.to_set?.length) {
139
+ lines.push("", `Set these where ${a.domain}'s DNS is:`);
140
+ for (const rec of a.dns.to_set) lines.push(` ${rec.type.padEnd(6)} ${rec.name.padEnd(6)} ${rec.value}`);
141
+ }
142
+ if (r.build_id) lines.push("", `Building: build show ${r.build_id}`);
143
+ if (r.events?.length) {
144
+ lines.push("", "Recent:");
145
+ for (const e of r.events.slice(0, 8)) lines.push(` ${e.at} ${e.message}`);
146
+ }
147
+ return lines.join("\n");
148
+ });
149
+ return 0;
150
+ }
package/lib/commands.mjs CHANGED
@@ -55,3 +55,4 @@ 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/deploy.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.176.26",
3
+ "version": "0.176.28",
4
4
  "description": "Spin up and manage Letter phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",