@letterstory/cli 0.177.12 → 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 +1 -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/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;
|
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/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
|
+
}
|