@letterstory/cli 0.177.11 → 0.177.13
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 +2 -1
- package/lib/commands/build.mjs +11 -0
- package/lib/commands/deploy.mjs +37 -5
- package/lib/commands/domains.mjs +18 -9
- package/lib/commands/onboard.mjs +32 -0
- package/lib/commands/phantom.mjs +20 -2
- package/lib/commands/repo.mjs +19 -1
- package/lib/commands/shared.mjs +18 -0
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -88,7 +88,7 @@ import {
|
|
|
88
88
|
import { fetchParkedSections, withoutParked } from "./parked.mjs";
|
|
89
89
|
|
|
90
90
|
// Matches cli/package.json; publish-cli.yml rewrites both to the published version.
|
|
91
|
-
export const VERSION = "0.177.
|
|
91
|
+
export const VERSION = "0.177.13";
|
|
92
92
|
|
|
93
93
|
/** The exit code for a gate refusal (CARD_REQUIRED, PAYMENT_FAILED, OUT_OF_TOKENS): 1 is a plain error. */
|
|
94
94
|
export const EXIT_PAYMENT_REQUIRED = 3;
|
|
@@ -1908,6 +1908,7 @@ Repo changes (Claude Code edits your repo in a sandbox; the Letter GitHub App op
|
|
|
1908
1908
|
repo repos [--gitlab [--search <text>] [--page n]] Repos the Letter GitHub App can reach (--gitlab: the GitLab
|
|
1909
1909
|
projects the organization's connection can push to)
|
|
1910
1910
|
repo connect [--gitlab] Is GitHub (or GitLab) connected; the link a person opens to connect it
|
|
1911
|
+
repo disconnect --installation <id> Disconnect a GitHub account (its installationId)
|
|
1911
1912
|
repo branches --repo owner/name Branches a change can start from (default first)
|
|
1912
1913
|
repo prs [--repo owner/name] Every open PR (whoever opened it), longest-idle first
|
|
1913
1914
|
repo suggestions Fixes found and not acted on yet, and how to start each
|
package/lib/commands/build.mjs
CHANGED
|
@@ -46,6 +46,17 @@ const ARGS = {
|
|
|
46
46
|
ask: "ask",
|
|
47
47
|
// --confirms <build id>: the person's yes to the price a build stopped on.
|
|
48
48
|
confirms: "confirms",
|
|
49
|
+
// --purchase '{"tool":"register_domain","args":{"domain":"olapdb.com"}}': priced and held for a yes, never bought.
|
|
50
|
+
purchase: {
|
|
51
|
+
value: (flags) => {
|
|
52
|
+
if (typeof flags.purchase !== "string") return undefined;
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(flags.purchase);
|
|
55
|
+
} catch {
|
|
56
|
+
throw new CliError("--purchase takes JSON: {tool, args}.");
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
},
|
|
49
60
|
// --build-it: the words are a request to build, never read as a question.
|
|
50
61
|
asked_to_build: { flag: "build-it", type: "bool" },
|
|
51
62
|
domain: "site",
|
package/lib/commands/deploy.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// letterstory deploy page <id>
|
|
13
13
|
|
|
14
14
|
import { CliError } from "../client.mjs";
|
|
15
|
-
import { argsFromFlags, flagStr, printResult, refuseUnknownFlags } from "./shared.mjs";
|
|
15
|
+
import { argsFromFlags, domainPrice, flagStr, printResult, refuseUnknownFlags } from "./shared.mjs";
|
|
16
16
|
|
|
17
17
|
const appId = { positional: 0, label: "app-id", required: true };
|
|
18
18
|
|
|
@@ -46,6 +46,8 @@ export const ARGS = {
|
|
|
46
46
|
writing: { flag: "writing", enum: ["on"] },
|
|
47
47
|
// --no-spend: with --domain, buy nothing; the domain is one you hold, or one registered elsewhere.
|
|
48
48
|
no_spend: { flag: "no-spend", type: "bool" },
|
|
49
|
+
// A new domain's price: read with register_domain's dry run on this key and echoed with --yes (deploy create's way).
|
|
50
|
+
accepted_quote: { internal: "the new domain's price read by the command (domainPrice), echoed with --yes" },
|
|
49
51
|
},
|
|
50
52
|
advance_deployment: { id: appId, retry: { flag: "retry", type: "bool" } },
|
|
51
53
|
add_deployment_page: { id: appId },
|
|
@@ -70,19 +72,49 @@ export async function cmdDeploy(ctx) {
|
|
|
70
72
|
case "ls":
|
|
71
73
|
return deployList(rest);
|
|
72
74
|
case "create": {
|
|
73
|
-
refuseUnknownFlags(rest, ARGS.create_deployment, ["prompt"]);
|
|
75
|
+
refuseUnknownFlags(rest, ARGS.create_deployment, ["prompt", "yes"]);
|
|
74
76
|
const args = argsFromFlags(ARGS.create_deployment, rest.flags, rest.positionals);
|
|
75
77
|
if (!args.prompt)
|
|
76
78
|
throw new CliError(
|
|
77
79
|
'Say what to build: letterstory deploy create "a website for HVAC contractors to collect leads"'
|
|
78
80
|
);
|
|
81
|
+
// Its domain is bought only at the price Letter shows this key, with --yes, echoed (domains buy's own way).
|
|
82
|
+
if (args.domain && !args.no_spend && args.domain_quote === undefined) {
|
|
83
|
+
const priced = await domainPrice(rest.client, rest.io, args.domain);
|
|
84
|
+
if (priced?.held) args.no_spend = true;
|
|
85
|
+
else if (priced?.quote) {
|
|
86
|
+
if (rest.flags.yes !== true) {
|
|
87
|
+
rest.io.error(
|
|
88
|
+
"Building it buys this domain (real money). Run again with --yes to buy it at that price."
|
|
89
|
+
);
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
args.domain_quote = priced.quote;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
79
95
|
return deployShow(rest, await rest.client.callTool("create_deployment", args));
|
|
80
96
|
}
|
|
81
97
|
case "show":
|
|
82
98
|
return deployShow(rest, await call(rest, "get_deployment"));
|
|
83
|
-
case "update":
|
|
84
|
-
refuseUnknownFlags(rest, ARGS.update_deployment, []);
|
|
85
|
-
|
|
99
|
+
case "update": {
|
|
100
|
+
refuseUnknownFlags(rest, ARGS.update_deployment, ["yes"]);
|
|
101
|
+
const args = argsFromFlags(ARGS.update_deployment, rest.flags, rest.positionals);
|
|
102
|
+
// A new domain is bought only at the price Letter shows this key, with --yes, echoed (deploy create's way).
|
|
103
|
+
if (args.domain && !args.no_spend) {
|
|
104
|
+
const priced = await domainPrice(rest.client, rest.io, args.domain);
|
|
105
|
+
if (priced?.held) args.no_spend = true;
|
|
106
|
+
else if (priced?.quote) {
|
|
107
|
+
if (rest.flags.yes !== true) {
|
|
108
|
+
rest.io.error(
|
|
109
|
+
"Its new domain is bought (real money). Run again with --yes to buy it at that price."
|
|
110
|
+
);
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
args.accepted_quote = priced.quote;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return deployShow(rest, await rest.client.callTool("update_deployment", args));
|
|
117
|
+
}
|
|
86
118
|
case "pause":
|
|
87
119
|
case "resume":
|
|
88
120
|
case "archive":
|
package/lib/commands/domains.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// plan and puts it live (spends money; domains:write)
|
|
17
17
|
|
|
18
18
|
import { CliError } from "../client.mjs";
|
|
19
|
-
import { flagCsv, requirePositional, printResult, argsFromFlags } from "./shared.mjs";
|
|
19
|
+
import { flagCsv, requirePositional, printResult, argsFromFlags, domainPrice } from "./shared.mjs";
|
|
20
20
|
|
|
21
21
|
export const ARGS = {
|
|
22
22
|
list_domains: {},
|
|
@@ -152,10 +152,16 @@ async function domainsList(ctx) {
|
|
|
152
152
|
|
|
153
153
|
async function domainsBuy(ctx) {
|
|
154
154
|
const { client, flags, io } = ctx;
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
)
|
|
155
|
+
const args = argsFromFlags(ARGS.register_domain, flags, ctx.positionals);
|
|
156
|
+
// A purchase goes only at a price Letter issued to this key (Mahir, 2026-11-11): --yes reads it first and buys at
|
|
157
|
+
// that price, echoing its quote, unless one was given (--accepted-quote).
|
|
158
|
+
if (flags.yes === true && !args.dry_run && args.accepted_quote === undefined && !args.no_spend) {
|
|
159
|
+
const priced = await domainPrice(client, io, args.domain);
|
|
160
|
+
// One the org holds is made its own with nothing bought; anything else at its price.
|
|
161
|
+
if (priced?.held) args.no_spend = true;
|
|
162
|
+
else if (priced?.quote) args.accepted_quote = priced.quote;
|
|
163
|
+
}
|
|
164
|
+
const result = await client.callTool("register_domain", args);
|
|
159
165
|
printResult(io, flags, result, (r) => {
|
|
160
166
|
const p = r.purchase;
|
|
161
167
|
const on = p?.on === "org_vercel_key" ? "on your own Vercel account" : "by Letter, billed in tokens";
|
|
@@ -214,10 +220,13 @@ async function domainsPlan(ctx) {
|
|
|
214
220
|
|
|
215
221
|
async function domainsLaunch(ctx) {
|
|
216
222
|
const { client, flags, io } = ctx;
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
223
|
+
const args = argsFromFlags(ARGS.launch_domain_site, flags, ctx.positionals);
|
|
224
|
+
// --yes buys its domain at the price Letter shows this key, echoed (domains buy's own way): never above it.
|
|
225
|
+
if (args.accepted_quote === undefined) {
|
|
226
|
+
const priced = await domainPrice(client, io, args.domain);
|
|
227
|
+
if (priced?.quote) args.accepted_quote = priced.quote;
|
|
228
|
+
}
|
|
229
|
+
const result = await client.callTool("launch_domain_site", args);
|
|
221
230
|
printResult(io, flags, result, (r) =>
|
|
222
231
|
[
|
|
223
232
|
...(r.plan ? planLines(r.plan) : []),
|
package/lib/commands/onboard.mjs
CHANGED
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
pollUntil,
|
|
62
62
|
readTextInput,
|
|
63
63
|
argsFromFlags,
|
|
64
|
+
domainPrice,
|
|
64
65
|
} from "./shared.mjs";
|
|
65
66
|
|
|
66
67
|
const COVER_STYLES = ["editorial_image", "full_ai", "line_art"];
|
|
@@ -120,6 +121,8 @@ export const ARGS = {
|
|
|
120
121
|
is_test: { flag: "test", type: "bool" },
|
|
121
122
|
readiness_minutes: { flag: "readiness-minutes", type: "num" },
|
|
122
123
|
client_email: "client-email",
|
|
124
|
+
// A buy_now phantom's domain price: read with register_domain's dry run on this key and echoed with --yes.
|
|
125
|
+
accepted_quote: { internal: "the phantom domain's price read by the command (domainPrice), echoed with --yes" },
|
|
123
126
|
},
|
|
124
127
|
get_domain_onboard_status: {
|
|
125
128
|
job_id: "attach",
|
|
@@ -134,6 +137,8 @@ export const ARGS = {
|
|
|
134
137
|
// flag mapping straight through: it comes from `--accept-plan <path|->` /
|
|
135
138
|
// `--accept-plan-json`, or from the dry run this same command just printed.
|
|
136
139
|
plan: { internal: "the reviewed plan — from --accept-plan, or the dry run just printed" },
|
|
140
|
+
// A buy_now launch's domain price: read with register_domain's dry run on this key and echoed with --yes.
|
|
141
|
+
accepted_quote: { internal: "the domain's price read by the command (domainPrice), echoed with --yes" },
|
|
137
142
|
overrides: {
|
|
138
143
|
object: {
|
|
139
144
|
name: "name",
|
|
@@ -654,6 +659,19 @@ async function cmdOnboardDomain(ctx) {
|
|
|
654
659
|
}
|
|
655
660
|
|
|
656
661
|
const args = argsFromFlags(ARGS.onboard_domain, flags, ctx.positionals);
|
|
662
|
+
// Its phantom's domain is bought only at the price Letter shows this key, with --yes, echoed (domains buy's way).
|
|
663
|
+
if (args.phantom?.domain_mode === "buy_now" && args.phantom.site_domain) {
|
|
664
|
+
const priced = await domainPrice(client, ctx.io, args.phantom.site_domain);
|
|
665
|
+
if (priced?.quote) {
|
|
666
|
+
if (!flagBool(flags.yes)) {
|
|
667
|
+
ctx.io.error(
|
|
668
|
+
"Onboarding buys the phantom's domain (real money). Run again with --yes to buy it at that price."
|
|
669
|
+
);
|
|
670
|
+
return 1;
|
|
671
|
+
}
|
|
672
|
+
args.accepted_quote = priced.quote;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
657
675
|
|
|
658
676
|
ok(ctx, "Starting domain onboarding…");
|
|
659
677
|
const started = await client.callTool("onboard_domain", args);
|
|
@@ -889,6 +907,19 @@ export async function cmdOnboard(ctx) {
|
|
|
889
907
|
reviewedPlan = dryRun.plan;
|
|
890
908
|
}
|
|
891
909
|
|
|
910
|
+
// A launch that buys its domain goes only at the price Letter shows this key, with --yes, echoed (domains buy's way).
|
|
911
|
+
let accepted_quote;
|
|
912
|
+
if (overrides.domain_mode === "buy_now" && overrides.site_domain) {
|
|
913
|
+
const priced = await domainPrice(client, io, overrides.site_domain);
|
|
914
|
+
if (priced?.quote) {
|
|
915
|
+
if (!flagBool(flags.yes)) {
|
|
916
|
+
io.error("Launching it buys this domain (real money). Run again with --yes to buy it at that price.");
|
|
917
|
+
return 1;
|
|
918
|
+
}
|
|
919
|
+
accepted_quote = priced.quote;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
892
923
|
// Step 3: apply — start the durable onboarding job.
|
|
893
924
|
ok(ctx, "Applying — starting the onboarding job…");
|
|
894
925
|
const started = await client.callTool(
|
|
@@ -898,6 +929,7 @@ export async function cmdOnboard(ctx) {
|
|
|
898
929
|
accept: true,
|
|
899
930
|
overrides: Object.keys(overrides).length ? overrides : undefined,
|
|
900
931
|
plan: reviewedPlan,
|
|
932
|
+
accepted_quote,
|
|
901
933
|
})
|
|
902
934
|
);
|
|
903
935
|
const jobId = started.job_id;
|
package/lib/commands/phantom.mjs
CHANGED
|
@@ -8,7 +8,16 @@
|
|
|
8
8
|
|
|
9
9
|
import { readFileSync } from "node:fs";
|
|
10
10
|
import { CliError } from "../client.mjs";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
flagStr,
|
|
13
|
+
requirePositional,
|
|
14
|
+
printResult,
|
|
15
|
+
ok,
|
|
16
|
+
toolField,
|
|
17
|
+
followJob,
|
|
18
|
+
argsFromFlags,
|
|
19
|
+
domainPrice,
|
|
20
|
+
} from "./shared.mjs";
|
|
12
21
|
|
|
13
22
|
function readJsonFileFlag(flags, flag) {
|
|
14
23
|
const path = flagStr(flags[flag]);
|
|
@@ -130,6 +139,8 @@ export const ARGS = {
|
|
|
130
139
|
buy_domain: {
|
|
131
140
|
phantom_id: { positional: 0, label: "phantom-id", required: true },
|
|
132
141
|
domain: { value: (flags, positionals) => stripScheme(requirePositional(positionals, 1, "domain")) },
|
|
142
|
+
// Its price: read with register_domain's dry run on this key and echoed with --yes (domains buy's way).
|
|
143
|
+
accepted_quote: { internal: "the domain's price read by the command (domainPrice), echoed with --yes" },
|
|
133
144
|
},
|
|
134
145
|
connect_domain: {
|
|
135
146
|
phantom_id: { positional: 0, label: "phantom-id", required: true },
|
|
@@ -265,7 +276,14 @@ async function phantomDomain(ctx) {
|
|
|
265
276
|
rest.io.error("Buying a domain spends real money. Re-run with --yes.");
|
|
266
277
|
return 1;
|
|
267
278
|
}
|
|
268
|
-
|
|
279
|
+
{
|
|
280
|
+
const args = argsFromFlags(ARGS.buy_domain, rest.flags, rest.positionals);
|
|
281
|
+
const priced = await domainPrice(rest.client, rest.io, args.domain);
|
|
282
|
+
if (priced?.quote) args.accepted_quote = priced.quote;
|
|
283
|
+
const result = await rest.client.callTool("buy_domain", args);
|
|
284
|
+
printResult(rest.io, rest.flags, result);
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
269
287
|
case "connect":
|
|
270
288
|
return call(rest, "connect_domain");
|
|
271
289
|
default:
|
package/lib/commands/repo.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// repo say → send_repo_change_message (steer a build while its agent works)
|
|
12
12
|
// repo repos → list_github_app_repos repo branches → list_repo_branches
|
|
13
13
|
// repo connect → connect_github (the link a person opens to connect GitHub)
|
|
14
|
+
// repo disconnect --installation <id> → disconnect_github_account
|
|
14
15
|
// repo repos --gitlab → list_gitlab_repos repo connect --gitlab → connect_gitlab
|
|
15
16
|
// repo prs → list_open_pull_requests (every open PR, whoever opened it)
|
|
16
17
|
// repo install-letterprove → letterprove_open_install_pr
|
|
@@ -304,6 +305,7 @@ const ARGS = {
|
|
|
304
305
|
},
|
|
305
306
|
list_github_app_repos: {},
|
|
306
307
|
connect_github: {},
|
|
308
|
+
disconnect_github_account: { installation_id: { flag: "installation", type: "num" } },
|
|
307
309
|
list_gitlab_repos: { search: "search", page: { flag: "page", type: "num" } },
|
|
308
310
|
connect_gitlab: {},
|
|
309
311
|
list_repo_branches: {
|
|
@@ -406,6 +408,8 @@ export async function cmdRepo(ctx) {
|
|
|
406
408
|
return repoRepos(rest);
|
|
407
409
|
case "connect":
|
|
408
410
|
return repoConnect(rest);
|
|
411
|
+
case "disconnect":
|
|
412
|
+
return repoDisconnect(rest);
|
|
409
413
|
case "branches":
|
|
410
414
|
return repoBranches(rest);
|
|
411
415
|
case "prs":
|
|
@@ -420,7 +424,7 @@ export async function cmdRepo(ctx) {
|
|
|
420
424
|
return repoSlackTest(rest);
|
|
421
425
|
default:
|
|
422
426
|
throw new CliError(
|
|
423
|
-
`Unknown repo subcommand: ${sub ?? "(none)"}. Try: start, fix-audit, install-letterprove, install-visitors, sites, set-site, reset-site, secrets, set-secret, delete-secret, status, list, publish, merge, retry, revise, say, cancel, repos, branches, prs, settings, suggestions, slack-targets, slack-test`
|
|
427
|
+
`Unknown repo subcommand: ${sub ?? "(none)"}. Try: start, fix-audit, install-letterprove, install-visitors, sites, set-site, reset-site, secrets, set-secret, delete-secret, status, list, publish, merge, retry, revise, say, cancel, repos, branches, prs, settings, suggestions, slack-targets, slack-test, connect, disconnect`
|
|
424
428
|
);
|
|
425
429
|
}
|
|
426
430
|
}
|
|
@@ -874,6 +878,20 @@ async function repoConnect(ctx) {
|
|
|
874
878
|
return 0;
|
|
875
879
|
}
|
|
876
880
|
|
|
881
|
+
/** `repo disconnect --installation <id>`: disconnect one GitHub account (its installationId on `repo repos --json`). */
|
|
882
|
+
async function repoDisconnect(ctx) {
|
|
883
|
+
const { client, flags, io } = ctx;
|
|
884
|
+
refuseUnknownFlags(ctx, {}, ["installation"]);
|
|
885
|
+
if (flags.installation === undefined)
|
|
886
|
+
throw new CliError("Pass --installation <id>: the installationId shown by `repo repos --json`.");
|
|
887
|
+
const result = await client.callTool(
|
|
888
|
+
"disconnect_github_account",
|
|
889
|
+
argsFromFlags(ARGS.disconnect_github_account, flags)
|
|
890
|
+
);
|
|
891
|
+
printResult(io, flags, result, (r) => r.message);
|
|
892
|
+
return 0;
|
|
893
|
+
}
|
|
894
|
+
|
|
877
895
|
/** `repo prs [--repo owner/name]`: every open PR, longest-idle first, and which Letter opened. */
|
|
878
896
|
async function repoPrs(ctx) {
|
|
879
897
|
const { client, flags, io } = ctx;
|
package/lib/commands/shared.mjs
CHANGED
|
@@ -385,3 +385,21 @@ export function readTextInput(flags, valueFlag, fileFlag) {
|
|
|
385
385
|
export function readBodyInput(flags) {
|
|
386
386
|
return readTextInput(flags, "body", "file");
|
|
387
387
|
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The price Letter shows for buying `domain`, read with register_domain's dry run on this key, for a command that buys
|
|
391
|
+
* (domains buy, domains launch, deploy create, onboard): a purchase goes only at a price Letter issued to this key,
|
|
392
|
+
* echoed, and never above it (Mahir, 2026-11-11). `held`: the organization has it, or takes it over: nothing is bought.
|
|
393
|
+
* `quote`: what to echo, the price said on stderr. Null: no purchase to price (the dispatcher says why if one is made).
|
|
394
|
+
*/
|
|
395
|
+
export async function domainPrice(client, io, domain) {
|
|
396
|
+
if (!domain) return null;
|
|
397
|
+
const priced = await client.callTool("register_domain", { domain, dry_run: true });
|
|
398
|
+
const held = priced?.outcome === "already_owned" || priced?.outcome === "would_adopt";
|
|
399
|
+
// A held domain the registrar would still sell (recorded, never bought) carries a quote too: a purchase of it (a
|
|
400
|
+
// phantom's buy, a launch) moves money and goes only at that price (review of #6952).
|
|
401
|
+
if (!priced?.quote) return held ? { held: true } : null;
|
|
402
|
+
const usd = priced.purchase?.price_usd ?? priced.quote.purchase_usd;
|
|
403
|
+
io.error(`${priced.domain}: $${usd} for the first year, if bought.`);
|
|
404
|
+
return { ...(held ? { held: true } : {}), quote: priced.quote, price_usd: usd ?? null };
|
|
405
|
+
}
|