@letterstory/cli 0.176.27 → 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 +22 -3
- package/lib/commands/build.mjs +2 -0
- package/lib/commands/deploy.mjs +150 -0
- package/lib/commands.mjs +1 -0
- package/package.json +1 -1
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.
|
|
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.
|
|
@@ -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
|
};
|
package/lib/commands/build.mjs
CHANGED
|
@@ -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