frontpage-host 0.1.18 → 0.1.19

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.
@@ -0,0 +1,595 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolveSlug } from "../site.js";
3
+ import { CliHttpError, EXIT_OK, EXIT_USAGE, v1Get, v1Patch, v1Post, v1Put, } from "../http/client.js";
4
+ import { assertNoUnknownFlags, hasFlag, takeFlag } from "../argv.js";
5
+ import { emitErr, emitOk, emitUsage, isJson } from "../runtime.js";
6
+ import { SHOP_ID_RE, VARIANT_ID_RE } from "./shop-ids.js";
7
+ const USAGE = "Usage: frontpage shop items|variants|settings|orders|payments … [--site <slug>] [--json]";
8
+ const CENTS_HINT = "$49.99 is 4999";
9
+ function takeAll(work, name) {
10
+ const out = [];
11
+ let v = takeFlag(work, name);
12
+ while (v !== undefined) {
13
+ out.push(v);
14
+ v = takeFlag(work, name);
15
+ }
16
+ return out;
17
+ }
18
+ function requireShopId(work, label = "Item id", opts) {
19
+ const fromFlag = (opts?.allowIdFlag === false ? undefined : takeFlag(work, "--id")) ?? takeFlag(work, "--item");
20
+ const positional = work.find((a) => !a.startsWith("-"));
21
+ const raw = fromFlag ?? (positional ? work.splice(work.indexOf(positional), 1)[0] : undefined);
22
+ if (!raw) {
23
+ throw new CliHttpError(`${label} is required. Run: frontpage help shop`, EXIT_USAGE, 0, "bad_request");
24
+ }
25
+ if (!SHOP_ID_RE.test(raw)) {
26
+ throw new CliHttpError("Item not found.", EXIT_USAGE, 404, "not_found");
27
+ }
28
+ return raw;
29
+ }
30
+ function requireVariantId(raw, required = true) {
31
+ if (!raw) {
32
+ if (!required)
33
+ return undefined;
34
+ throw new CliHttpError("Variant id is required. Use --variant m or --id m.", EXIT_USAGE, 0, "bad_request");
35
+ }
36
+ if (!VARIANT_ID_RE.test(raw)) {
37
+ throw new CliHttpError("Variant not found.", EXIT_USAGE, 404, "not_found");
38
+ }
39
+ return raw;
40
+ }
41
+ function requireLedgerId(work, kind) {
42
+ const fromFlag = takeFlag(work, "--id");
43
+ const positional = work.find((a) => !a.startsWith("-"));
44
+ const raw = fromFlag ?? (positional ? work.splice(work.indexOf(positional), 1)[0] : undefined);
45
+ if (!raw || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(raw)) {
46
+ throw new CliHttpError(`${kind} not found.`, EXIT_USAGE, 404, "not_found");
47
+ }
48
+ return raw;
49
+ }
50
+ function parseCentsFlag(raw, label) {
51
+ if (raw === undefined)
52
+ return undefined;
53
+ const trimmed = raw.trim();
54
+ if (!/^\d+$/.test(trimmed)) {
55
+ throw new CliHttpError(`${label} must be an integer number of cents. ${CENTS_HINT}. Do not send ${trimmed}.`, EXIT_USAGE, 0, "bad_request");
56
+ }
57
+ const n = Number(trimmed);
58
+ if (!Number.isInteger(n)) {
59
+ throw new CliHttpError(`${label} must be an integer number of cents. ${CENTS_HINT}.`, EXIT_USAGE, 0, "bad_request");
60
+ }
61
+ return n;
62
+ }
63
+ function parseCountFlag(raw, label) {
64
+ if (raw === undefined)
65
+ return undefined;
66
+ const trimmed = raw.trim();
67
+ if (!/^-?\d+$/.test(trimmed)) {
68
+ throw new CliHttpError(`${label} must be an integer count.`, EXIT_USAGE, 0, "bad_request");
69
+ }
70
+ return Number(trimmed);
71
+ }
72
+ function parseBoolFlag(raw, label) {
73
+ if (raw === undefined)
74
+ return undefined;
75
+ if (raw === "true" || raw === "yes" || raw === "1")
76
+ return true;
77
+ if (raw === "false" || raw === "no" || raw === "0")
78
+ return false;
79
+ throw new CliHttpError(`${label} must be true or false.`, EXIT_USAGE, 0, "bad_request");
80
+ }
81
+ /** Bare `--flag` is true. `--flag false` / `--flag=false` stay false. */
82
+ function takeBoolish(work, name, label) {
83
+ const eq = `${name}=`;
84
+ const i = work.findIndex((a) => a === name || a.startsWith(eq));
85
+ if (i === -1)
86
+ return undefined;
87
+ const token = work[i];
88
+ if (token.startsWith(eq)) {
89
+ work.splice(i, 1);
90
+ return parseBoolFlag(token.slice(eq.length), label);
91
+ }
92
+ const next = work[i + 1];
93
+ if (next === "true" || next === "false" || next === "yes" || next === "no" || next === "1" || next === "0") {
94
+ work.splice(i, 2);
95
+ return parseBoolFlag(next, label);
96
+ }
97
+ work.splice(i, 1);
98
+ return true;
99
+ }
100
+ function parseCsv(raw) {
101
+ if (raw === undefined)
102
+ return undefined;
103
+ return raw
104
+ .split(",")
105
+ .map((s) => s.trim())
106
+ .filter(Boolean);
107
+ }
108
+ function parseOptions(pairs) {
109
+ if (pairs.length === 0)
110
+ return undefined;
111
+ const out = {};
112
+ for (const pair of pairs) {
113
+ const eq = pair.indexOf("=");
114
+ if (eq <= 0) {
115
+ throw new CliHttpError("Each --option needs Name=Value (example: Size=M).", EXIT_USAGE, 0, "bad_request");
116
+ }
117
+ out[pair.slice(0, eq)] = pair.slice(eq + 1);
118
+ }
119
+ return out;
120
+ }
121
+ function parseUrls(work) {
122
+ const fromRepeat = takeAll(work, "--urls");
123
+ const fromAlias = takeAll(work, "--url");
124
+ const fromImage = takeAll(work, "--image");
125
+ return [...fromRepeat, ...fromAlias, ...fromImage].flatMap((v) => v.split(",").map((s) => s.trim()).filter(Boolean));
126
+ }
127
+ async function readJsonFile(path) {
128
+ const raw = await readFile(path, "utf8");
129
+ try {
130
+ const parsed = JSON.parse(raw);
131
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
132
+ throw new Error("not an object");
133
+ }
134
+ return parsed;
135
+ }
136
+ catch {
137
+ throw new CliHttpError("File must be a JSON object.", EXIT_USAGE, 0, "bad_request");
138
+ }
139
+ }
140
+ function printItem(row) {
141
+ const live = (row.variants ?? []).filter((v) => !v.archivedAt);
142
+ const prices = live.map((v) => v.discountPriceCents != null ? `${v.discountPriceCents}/${v.priceCents}` : String(v.priceCents));
143
+ const stocks = live.map((v) => (v.stock == null ? "unlimited" : String(v.stock)));
144
+ const bits = [
145
+ row.checkout ?? "",
146
+ row.amount ?? "",
147
+ prices.length ? prices.join(",") : "",
148
+ stocks.length ? `stock ${stocks.join(",")}` : "",
149
+ row.active === false ? "inactive" : "active",
150
+ row.archivedAt ? "archived" : "",
151
+ ].filter(Boolean);
152
+ console.log(`${row.itemId} ${row.title ?? ""} ${bits.join(" · ")}`);
153
+ }
154
+ function printStock(row) {
155
+ const now = row.stockNow == null ? "unlimited" : String(row.stockNow);
156
+ console.log(`${row.itemId} ${row.variantId} ${row.change} stockNow ${now}`);
157
+ }
158
+ export async function runShop(args) {
159
+ try {
160
+ const { slug, rest } = await resolveSlug(args);
161
+ const group = rest[0];
162
+ const base = `/api/v1/sites/${encodeURIComponent(slug)}/shop`;
163
+ if (!group)
164
+ return emitUsage("shop", USAGE);
165
+ if (group === "items") {
166
+ const sub = rest[1];
167
+ const work = rest.slice(2);
168
+ if (!sub || sub === "list" || sub.startsWith("-")) {
169
+ const listWork = sub === "list" ? work : rest.slice(1);
170
+ const archived = hasFlag(listWork, "--archived");
171
+ assertNoUnknownFlags(listWork, "shop");
172
+ const qs = archived ? "?archived=1" : "";
173
+ const data = await v1Get(`${base}/items${qs}`);
174
+ if (isJson()) {
175
+ emitOk("shop", data);
176
+ return EXIT_OK;
177
+ }
178
+ const rows = data.items ?? [];
179
+ if (rows.length === 0) {
180
+ console.log(archived ? "No items." : "No items.");
181
+ return EXIT_OK;
182
+ }
183
+ for (const row of rows)
184
+ printItem(row);
185
+ return EXIT_OK;
186
+ }
187
+ if (sub === "get") {
188
+ const itemId = requireShopId(work);
189
+ assertNoUnknownFlags(work, "shop");
190
+ const data = await v1Get(`${base}/items/${encodeURIComponent(itemId)}`);
191
+ if (isJson()) {
192
+ emitOk("shop", data);
193
+ return EXIT_OK;
194
+ }
195
+ printItem(data.item);
196
+ for (const v of data.item.variants ?? []) {
197
+ const sale = v.discountPriceCents != null ? ` sale ${v.discountPriceCents}` : "";
198
+ const stock = v.stock == null ? "unlimited" : String(v.stock);
199
+ const off = v.active === false ? " inactive" : "";
200
+ const archived = v.archivedAt ? " archived" : "";
201
+ console.log(` ${v.variantId} ${v.priceCents}${sale} stock ${stock}${off}${archived}`);
202
+ }
203
+ return EXIT_OK;
204
+ }
205
+ if (sub === "create") {
206
+ const file = takeFlag(work, "--file");
207
+ const itemId = takeFlag(work, "--id") ?? takeFlag(work, "--item") ?? work.find((a) => !a.startsWith("-"));
208
+ if (itemId && !file && work.includes(itemId))
209
+ work.splice(work.indexOf(itemId), 1);
210
+ const title = takeFlag(work, "--title");
211
+ const description = takeFlag(work, "--description");
212
+ const checkout = takeFlag(work, "--checkout");
213
+ const amount = takeFlag(work, "--amount");
214
+ const minCents = parseCentsFlag(takeFlag(work, "--min-cents"), "minCents");
215
+ const requiresShipping = parseBoolFlag(takeFlag(work, "--requires-shipping"), "requiresShipping");
216
+ const optionNames = parseCsv(takeFlag(work, "--option-names"));
217
+ const priceCents = parseCentsFlag(takeFlag(work, "--price-cents"), "priceCents");
218
+ const discountCents = parseCentsFlag(takeFlag(work, "--discount-cents"), "discountPriceCents");
219
+ const shippingCents = parseCentsFlag(takeFlag(work, "--shipping-cents"), "shippingCents");
220
+ const stockRaw = takeFlag(work, "--stock");
221
+ const variantId = takeFlag(work, "--variant-id") ?? takeFlag(work, "--variant");
222
+ const options = parseOptions(takeAll(work, "--option"));
223
+ const images = parseUrls(work);
224
+ assertNoUnknownFlags(work, "shop");
225
+ const body = file ? await readJsonFile(file) : {};
226
+ if (itemId)
227
+ body.itemId = itemId;
228
+ if (title)
229
+ body.title = title;
230
+ if (description !== undefined)
231
+ body.description = description;
232
+ if (checkout)
233
+ body.checkout = checkout;
234
+ if (amount)
235
+ body.amount = amount;
236
+ if (minCents !== undefined)
237
+ body.minCents = minCents;
238
+ if (requiresShipping !== undefined)
239
+ body.requiresShipping = requiresShipping;
240
+ if (optionNames)
241
+ body.optionNames = optionNames;
242
+ if (images.length)
243
+ body.images = images;
244
+ if (body.checkout == null)
245
+ body.checkout = "cart";
246
+ if (body.amount == null)
247
+ body.amount = "fixed";
248
+ if (!Array.isArray(body.variants) && priceCents !== undefined) {
249
+ const stock = stockRaw === undefined ? undefined : stockRaw === "unlimited" ? null : parseCountFlag(stockRaw, "stock");
250
+ body.variants = [
251
+ {
252
+ variantId: variantId ?? "default",
253
+ ...(options ? { options } : {}),
254
+ priceCents,
255
+ ...(discountCents !== undefined ? { discountPriceCents: discountCents } : {}),
256
+ ...(shippingCents !== undefined ? { shippingCents } : {}),
257
+ ...(stock !== undefined ? { stock } : {}),
258
+ },
259
+ ];
260
+ }
261
+ if (typeof body.itemId !== "string" || !SHOP_ID_RE.test(body.itemId)) {
262
+ throw new CliHttpError("Usage: frontpage shop items create <itemId> --title … --price-cents 4999", EXIT_USAGE, 0, "bad_request");
263
+ }
264
+ const data = await v1Post(`${base}/items`, body);
265
+ if (isJson()) {
266
+ emitOk("shop", data);
267
+ return EXIT_OK;
268
+ }
269
+ printItem(data.item);
270
+ return EXIT_OK;
271
+ }
272
+ if (sub === "update") {
273
+ const itemId = requireShopId(work);
274
+ const title = takeFlag(work, "--title");
275
+ const description = takeFlag(work, "--description");
276
+ const checkout = takeFlag(work, "--checkout");
277
+ const amount = takeFlag(work, "--amount");
278
+ const minCents = parseCentsFlag(takeFlag(work, "--min-cents"), "minCents");
279
+ const requiresShipping = parseBoolFlag(takeFlag(work, "--requires-shipping"), "requiresShipping");
280
+ const active = parseBoolFlag(takeFlag(work, "--active"), "active");
281
+ const images = parseUrls(work);
282
+ assertNoUnknownFlags(work, "shop");
283
+ const body = {};
284
+ if (title !== undefined)
285
+ body.title = title;
286
+ if (description !== undefined)
287
+ body.description = description;
288
+ if (checkout !== undefined)
289
+ body.checkout = checkout;
290
+ if (amount !== undefined)
291
+ body.amount = amount;
292
+ if (minCents !== undefined)
293
+ body.minCents = minCents;
294
+ if (requiresShipping !== undefined)
295
+ body.requiresShipping = requiresShipping;
296
+ if (active !== undefined)
297
+ body.active = active;
298
+ if (images.length)
299
+ body.images = images;
300
+ if (Object.keys(body).length === 0) {
301
+ throw new CliHttpError("Pass at least one field to update.", EXIT_USAGE, 0, "bad_request");
302
+ }
303
+ const data = await v1Patch(`${base}/items/${encodeURIComponent(itemId)}`, body);
304
+ if (isJson()) {
305
+ emitOk("shop", data);
306
+ return EXIT_OK;
307
+ }
308
+ printItem(data.item);
309
+ return EXIT_OK;
310
+ }
311
+ if (sub === "archive") {
312
+ const itemId = requireShopId(work);
313
+ assertNoUnknownFlags(work, "shop");
314
+ const data = await v1Post(`${base}/items/${encodeURIComponent(itemId)}/archive`, {});
315
+ if (isJson()) {
316
+ emitOk("shop", data);
317
+ return EXIT_OK;
318
+ }
319
+ printItem(data.item);
320
+ return EXIT_OK;
321
+ }
322
+ if (sub === "activate") {
323
+ const itemId = requireShopId(work);
324
+ const off = hasFlag(work, "--off");
325
+ const active = off ? false : parseBoolFlag(takeFlag(work, "--active"), "active");
326
+ assertNoUnknownFlags(work, "shop");
327
+ const data = await v1Post(`${base}/items/${encodeURIComponent(itemId)}/activate`, { active: active ?? true });
328
+ if (isJson()) {
329
+ emitOk("shop", data);
330
+ return EXIT_OK;
331
+ }
332
+ printItem(data.item);
333
+ return EXIT_OK;
334
+ }
335
+ if (sub === "images") {
336
+ const itemId = requireShopId(work);
337
+ const urls = parseUrls(work);
338
+ assertNoUnknownFlags(work, "shop");
339
+ const data = await v1Put(`${base}/items/${encodeURIComponent(itemId)}/images`, { images: urls });
340
+ if (isJson()) {
341
+ emitOk("shop", data);
342
+ return EXIT_OK;
343
+ }
344
+ printItem(data.item);
345
+ return EXIT_OK;
346
+ }
347
+ if (sub === "stock") {
348
+ const itemId = requireShopId(work);
349
+ const variant = takeFlag(work, "--variant") ?? takeFlag(work, "--variant-id");
350
+ const addRaw = takeFlag(work, "--add");
351
+ const removeRaw = takeFlag(work, "--remove");
352
+ const setRaw = takeFlag(work, "--set");
353
+ assertNoUnknownFlags(work, "shop");
354
+ const present = [addRaw, removeRaw, setRaw].filter((v) => v !== undefined).length;
355
+ if (present !== 1) {
356
+ throw new CliHttpError("Pass either --add 5, --remove 5, or --set 20. --add / --remove become signed delta.", EXIT_USAGE, 0, "bad_request");
357
+ }
358
+ const body = {};
359
+ if (variant)
360
+ body.variantId = requireVariantId(variant);
361
+ if (addRaw !== undefined) {
362
+ const n = parseCountFlag(addRaw, "--add");
363
+ if (n === undefined || n <= 0) {
364
+ throw new CliHttpError("--add must be a positive integer. 5 adds 5.", EXIT_USAGE, 0, "bad_request");
365
+ }
366
+ body.delta = n;
367
+ }
368
+ if (removeRaw !== undefined) {
369
+ const n = parseCountFlag(removeRaw, "--remove");
370
+ if (n === undefined || n <= 0) {
371
+ throw new CliHttpError("--remove must be a positive integer. 5 removes 5.", EXIT_USAGE, 0, "bad_request");
372
+ }
373
+ body.delta = -n;
374
+ }
375
+ if (setRaw !== undefined) {
376
+ if (setRaw === "unlimited" || setRaw === "null")
377
+ body.set = null;
378
+ else {
379
+ const n = parseCountFlag(setRaw, "--set");
380
+ if (n === undefined || n < 0) {
381
+ throw new CliHttpError("--set must be an integer count, or unlimited.", EXIT_USAGE, 0, "bad_request");
382
+ }
383
+ body.set = n;
384
+ }
385
+ }
386
+ const data = await v1Post(`${base}/items/${encodeURIComponent(itemId)}/stock`, body);
387
+ if (isJson()) {
388
+ emitOk("shop", data);
389
+ return EXIT_OK;
390
+ }
391
+ printStock(data);
392
+ return EXIT_OK;
393
+ }
394
+ return emitUsage("shop", USAGE);
395
+ }
396
+ if (group === "variants") {
397
+ const sub = rest[1];
398
+ const work = rest.slice(2);
399
+ if (sub === "add") {
400
+ const itemId = requireShopId(work, "Item id", { allowIdFlag: false });
401
+ const variantId = requireVariantId(takeFlag(work, "--id") ?? takeFlag(work, "--variant"));
402
+ const priceCents = parseCentsFlag(takeFlag(work, "--price-cents"), "priceCents");
403
+ const discountCents = parseCentsFlag(takeFlag(work, "--discount-cents"), "discountPriceCents");
404
+ const shippingCents = parseCentsFlag(takeFlag(work, "--shipping-cents"), "shippingCents");
405
+ const stockRaw = takeFlag(work, "--stock");
406
+ const options = parseOptions(takeAll(work, "--option"));
407
+ const active = parseBoolFlag(takeFlag(work, "--active"), "active");
408
+ assertNoUnknownFlags(work, "shop");
409
+ if (priceCents === undefined) {
410
+ throw new CliHttpError("Usage: frontpage shop variants add <itemId> --id m --price-cents 4999", EXIT_USAGE, 0, "bad_request");
411
+ }
412
+ const stock = stockRaw === undefined ? undefined : stockRaw === "unlimited" ? null : parseCountFlag(stockRaw, "stock");
413
+ const data = await v1Post(`${base}/items/${encodeURIComponent(itemId)}/variants`, {
414
+ variantId,
415
+ options,
416
+ priceCents,
417
+ discountPriceCents: discountCents,
418
+ shippingCents,
419
+ stock,
420
+ active,
421
+ });
422
+ if (isJson()) {
423
+ emitOk("shop", data);
424
+ return EXIT_OK;
425
+ }
426
+ printItem(data.item);
427
+ return EXIT_OK;
428
+ }
429
+ if (sub === "update") {
430
+ const itemId = requireShopId(work, "Item id", { allowIdFlag: false });
431
+ const variantId = requireVariantId(takeFlag(work, "--id") ?? takeFlag(work, "--variant"));
432
+ const priceCents = parseCentsFlag(takeFlag(work, "--price-cents"), "priceCents");
433
+ const discountCents = parseCentsFlag(takeFlag(work, "--discount-cents"), "discountPriceCents");
434
+ const shippingCents = parseCentsFlag(takeFlag(work, "--shipping-cents"), "shippingCents");
435
+ const stockRaw = takeFlag(work, "--stock");
436
+ const options = parseOptions(takeAll(work, "--option"));
437
+ const active = parseBoolFlag(takeFlag(work, "--active"), "active");
438
+ assertNoUnknownFlags(work, "shop");
439
+ const body = {};
440
+ if (priceCents !== undefined)
441
+ body.priceCents = priceCents;
442
+ if (discountCents !== undefined)
443
+ body.discountPriceCents = discountCents;
444
+ if (shippingCents !== undefined)
445
+ body.shippingCents = shippingCents;
446
+ if (stockRaw !== undefined)
447
+ body.stock = stockRaw === "unlimited" ? null : parseCountFlag(stockRaw, "stock");
448
+ if (options)
449
+ body.options = options;
450
+ if (active !== undefined)
451
+ body.active = active;
452
+ if (Object.keys(body).length === 0) {
453
+ throw new CliHttpError("Pass at least one variant field to update.", EXIT_USAGE, 0, "bad_request");
454
+ }
455
+ const data = await v1Patch(`${base}/items/${encodeURIComponent(itemId)}/variants/${encodeURIComponent(variantId)}`, body);
456
+ if (isJson()) {
457
+ emitOk("shop", data);
458
+ return EXIT_OK;
459
+ }
460
+ printItem(data.item);
461
+ return EXIT_OK;
462
+ }
463
+ if (sub === "archive") {
464
+ const itemId = requireShopId(work, "Item id", { allowIdFlag: false });
465
+ const variantId = requireVariantId(takeFlag(work, "--id") ?? takeFlag(work, "--variant"));
466
+ assertNoUnknownFlags(work, "shop");
467
+ const data = await v1Post(`${base}/items/${encodeURIComponent(itemId)}/variants/${encodeURIComponent(variantId)}/archive`, {});
468
+ if (isJson()) {
469
+ emitOk("shop", data);
470
+ return EXIT_OK;
471
+ }
472
+ printItem(data.item);
473
+ return EXIT_OK;
474
+ }
475
+ return emitUsage("shop", USAGE);
476
+ }
477
+ if (group === "settings") {
478
+ const sub = rest[1] ?? "get";
479
+ const work = rest.slice(2);
480
+ if (sub === "get") {
481
+ assertNoUnknownFlags(work, "shop");
482
+ const data = await v1Get(`${base}/settings`);
483
+ if (isJson()) {
484
+ emitOk("shop", data);
485
+ return EXIT_OK;
486
+ }
487
+ const s = data.settings ?? {};
488
+ const fee = (s.checkoutFee ?? {});
489
+ const fulfill = (s.fulfillment ?? {});
490
+ const feeText = fee.enabled ? `checkout-fee ${fee.cents ?? 0}` : "checkout-fee off";
491
+ console.log(`${s.currency ?? "(no currency)"} ${fulfill.shipping ?? "(no fulfillment)"} ${feeText}`);
492
+ return EXIT_OK;
493
+ }
494
+ if (sub === "set-fulfillment") {
495
+ const mode = takeFlag(work, "--mode") ?? takeFlag(work, "--shipping");
496
+ const flatCents = parseCentsFlag(takeFlag(work, "--flat-cents"), "flatRateCents");
497
+ const ratesRaw = takeFlag(work, "--country-rates");
498
+ const restOn = takeBoolish(work, "--rest-of-world", "restOfWorldEnabled");
499
+ const restCents = parseCentsFlag(takeFlag(work, "--rest-cents"), "restOfWorldCents");
500
+ const freeOn = takeBoolish(work, "--free-over", "freeShippingOverEnabled");
501
+ const freeCents = parseCentsFlag(takeFlag(work, "--free-over-cents"), "freeShippingOverCents");
502
+ assertNoUnknownFlags(work, "shop");
503
+ let countryRates;
504
+ if (ratesRaw) {
505
+ try {
506
+ countryRates = JSON.parse(ratesRaw);
507
+ }
508
+ catch {
509
+ throw new CliHttpError("countryRates must be JSON: [{\"country\":\"US\",\"rateCents\":800}]", EXIT_USAGE, 0, "bad_request");
510
+ }
511
+ }
512
+ const data = await v1Post(`${base}/settings/fulfillment`, {
513
+ shipping: mode,
514
+ flatRateCents: flatCents,
515
+ countryRates,
516
+ restOfWorldEnabled: restOn,
517
+ restOfWorldCents: restCents,
518
+ freeShippingOverEnabled: freeOn,
519
+ freeShippingOverCents: freeCents,
520
+ });
521
+ if (isJson())
522
+ emitOk("shop", data);
523
+ else
524
+ console.log("Fulfillment saved.");
525
+ return EXIT_OK;
526
+ }
527
+ if (sub === "set-currency") {
528
+ const currency = takeFlag(work, "--currency");
529
+ assertNoUnknownFlags(work, "shop");
530
+ if (!currency) {
531
+ throw new CliHttpError("Usage: frontpage shop settings set-currency --currency usd", EXIT_USAGE, 0, "bad_request");
532
+ }
533
+ const data = await v1Post(`${base}/settings/currency`, { currency });
534
+ if (isJson())
535
+ emitOk("shop", data);
536
+ else
537
+ console.log("Currency saved.");
538
+ return EXIT_OK;
539
+ }
540
+ if (sub === "set-checkout-fee") {
541
+ const off = hasFlag(work, "--off");
542
+ const cents = parseCentsFlag(takeFlag(work, "--cents"), "checkoutFeeCents");
543
+ assertNoUnknownFlags(work, "shop");
544
+ const data = await v1Post(`${base}/settings/checkout-fee`, {
545
+ enabled: off ? false : true,
546
+ cents,
547
+ });
548
+ if (isJson())
549
+ emitOk("shop", data);
550
+ else
551
+ console.log(off ? "Checkout fee off." : "Checkout fee saved.");
552
+ return EXIT_OK;
553
+ }
554
+ return emitUsage("shop", USAGE);
555
+ }
556
+ if (group === "orders" || group === "payments") {
557
+ const noun = group;
558
+ const sub = rest[1] ?? "list";
559
+ const work = rest.slice(2);
560
+ if (sub === "list") {
561
+ assertNoUnknownFlags(work, "shop");
562
+ const data = await v1Get(`${base}/${noun}`);
563
+ if (isJson()) {
564
+ emitOk("shop", data);
565
+ return EXIT_OK;
566
+ }
567
+ const rows = data[noun] ?? [];
568
+ if (rows.length === 0) {
569
+ console.log(noun === "orders" ? "No orders." : "No payments.");
570
+ return EXIT_OK;
571
+ }
572
+ for (const row of rows) {
573
+ console.log(`${row.id} ${row.amountCents ?? ""} ${row.currency ?? ""} ${row.status ?? ""}`);
574
+ }
575
+ return EXIT_OK;
576
+ }
577
+ if (sub === "get") {
578
+ const id = requireLedgerId(work, noun === "orders" ? "Order" : "Payment");
579
+ assertNoUnknownFlags(work, "shop");
580
+ const data = await v1Get(`${base}/${noun}/${encodeURIComponent(id)}`);
581
+ if (isJson()) {
582
+ emitOk("shop", data);
583
+ return EXIT_OK;
584
+ }
585
+ console.log(JSON.stringify(data[noun === "orders" ? "order" : "payment"], null, 2));
586
+ return EXIT_OK;
587
+ }
588
+ return emitUsage("shop", USAGE);
589
+ }
590
+ return emitUsage("shop", USAGE);
591
+ }
592
+ catch (err) {
593
+ return emitErr("shop", err);
594
+ }
595
+ }
@@ -132,8 +132,8 @@ export const MODULE_COMMANDS = [
132
132
  },
133
133
  {
134
134
  key: "payments",
135
- name: "Payments",
136
- summary: "Stripe payments. Connect finishes in the browser, not the terminal.",
135
+ name: "Pay buttons",
136
+ summary: "One-click Stripe pay buttons. No cart. Connect finishes in the browser, not the terminal.",
137
137
  genericActions: GENERIC_ACTIONS,
138
138
  extraActions: [
139
139
  "status",
@@ -151,14 +151,14 @@ export const MODULE_COMMANDS = [
151
151
  ],
152
152
  notes: [
153
153
  "Connect needs an admin API key. It opens Frontpage (you must be logged in there), then Stripe. Already-connected sites stay live — disconnect first to switch accounts.",
154
- "payments and shop share one Stripe account. Disconnecting payments fails if the store still uses that account.",
154
+ "Pay buttons and the Online store share one Stripe account. Disconnecting Pay buttons fails if the store still uses that account.",
155
155
  "request-setup still lights the editor amber bar when you want the owner to finish there.",
156
156
  ],
157
157
  },
158
158
  {
159
159
  key: "shop",
160
160
  name: "Online store",
161
- summary: "Stripe shop. Shares the payments Stripe account.",
161
+ summary: "Catalog, cart, and Stripe checkout. Shares the Pay buttons Stripe account.",
162
162
  genericActions: GENERIC_ACTIONS,
163
163
  extraActions: [
164
164
  "status",