frontpage-host 0.1.17 → 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
+ }
@@ -15,6 +15,7 @@ export declare function emitCompileWaitOutcome(args: {
15
15
  exitCode: number;
16
16
  timedOut: boolean;
17
17
  failed?: boolean;
18
+ apiMissing?: boolean;
18
19
  status: DeployStatus | null;
19
20
  };
20
21
  extra?: Record<string, unknown>;
@@ -45,11 +45,13 @@ export function emitCompileWaitOutcome(args) {
45
45
  compileReady: Boolean(args.compile.status?.ready) && !args.compile.timedOut,
46
46
  };
47
47
  if (args.compile.exitCode !== EXIT_OK) {
48
- const message = args.compile.failed
49
- ? "Site compile failed. The git tip is not live."
50
- : args.compile.timedOut
51
- ? "Timed out waiting for the site compile. The git tip is not live yet."
52
- : "Site compile is not ready.";
48
+ const message = args.compile.apiMissing
49
+ ? "Compile status API is missing. Cannot confirm the tip is live."
50
+ : args.compile.failed
51
+ ? "Site compile failed. The git tip is not live."
52
+ : args.compile.timedOut
53
+ ? "Timed out waiting for the site compile. The git tip is not live yet."
54
+ : "Site compile is not ready.";
53
55
  if (isJson()) {
54
56
  return emitFail(args.command, {
55
57
  message,
@@ -58,11 +60,13 @@ export function emitCompileWaitOutcome(args) {
58
60
  slug: args.slug,
59
61
  waited: true,
60
62
  observedBusy: args.agent.observedBusy,
61
- status: args.compile.failed
62
- ? "compile_failed"
63
- : args.compile.timedOut
64
- ? "compile_timeout"
65
- : "compile_not_ready",
63
+ status: args.compile.apiMissing
64
+ ? "compile_api_missing"
65
+ : args.compile.failed
66
+ ? "compile_failed"
67
+ : args.compile.timedOut
68
+ ? "compile_timeout"
69
+ : "compile_not_ready",
66
70
  run: args.agent.run,
67
71
  ...compileData,
68
72
  ...args.extra,
package/dist/compile.d.ts CHANGED
@@ -20,8 +20,13 @@ export type CompileWaitResult = {
20
20
  exitCode: number;
21
21
  timedOut: boolean;
22
22
  failed: boolean;
23
+ apiMissing?: boolean;
23
24
  status: DeployStatus | null;
24
25
  };
26
+ /** Compile clock is gone (flag off / old API). A site-not-found 404 is not this. */
27
+ export declare function isMissingDeployApi(err: unknown): boolean;
28
+ /** A deploy payload we can poll. Missing `ready` means the compile clock is not there. */
29
+ export declare function deployStatusLooksComplete(status: DeployStatus | null | undefined): boolean;
25
30
  export declare function fetchDeployStatus(slug: string): Promise<DeployStatus>;
26
31
  export declare function waitForCompileReady(args: {
27
32
  slug: string;