frontpage-host 0.1.19 → 0.1.21

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/AGENTS.md CHANGED
@@ -128,7 +128,7 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
128
128
  - Generated images: `frontpage images generate --prompt "…"` `[--count N]` — one prompt, 1–4 variations of that same picture. Prints `sitePath:` first. Counts against AI $ and storage. Flag-off environments return 404 (`Image generation is not enabled.`). Do not call xAI / Imagine yourself.
129
129
  - Analytics: bare `frontpage analytics` = dashboard; also `summary|query|funnel|timeseries|compare|sql` for structured reads (SQL is guard + RLS site-scoped).
130
130
  - People CRM: `frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide|tag|export`. Site-scoped person graph (email or phone). Inbox stays the raw ledger. Hide is not forget. List export is CSV (not a one-person legal dump). Full guide: https://frontpage.host/developers/people
131
- - Store catalog: `frontpage shop items|variants|settings|orders|payments`. Integer cents only (`$49.99` is `4999`). Stock `--add` / `--remove` become signed delta (`added 5` / `removed 5` / `set to 20` / `stockNow`). Connect stays `frontpage modules shop|payments connect`. Square `ordering` is a different module. Full guide: https://frontpage.host/developers/shop
131
+ - Store catalog: `frontpage shop items|variants|categories|settings|orders|payments|connect|status|disconnect`. Integer cents only (`$49.99` is `4999`). Stock `--add` / `--remove` become signed delta (`added 5` / `removed 5` / `set to 20` / `stockNow`). Create a category before assigning it (`categories create hats --label Hats`, then `items create … --categories hats`). `frontpage shop connect [--no-open]` opens Frontpage then Stripe (admin key; finish logged-in in the browser), then `status`. Disconnect keeps the shared Stripe account on payments. `frontpage modules shop|payments connect` still works. Square `ordering` is a different module. Full guide: https://frontpage.host/developers/shop
132
132
 
133
133
  ## Agent prompt loop
134
134
 
@@ -2,9 +2,11 @@ import { readFile } from "node:fs/promises";
2
2
  import { resolveSlug } from "../site.js";
3
3
  import { CliHttpError, EXIT_OK, EXIT_USAGE, v1Get, v1Patch, v1Post, v1Put, } from "../http/client.js";
4
4
  import { assertNoUnknownFlags, hasFlag, takeFlag } from "../argv.js";
5
- import { emitErr, emitOk, emitUsage, isJson } from "../runtime.js";
5
+ import { emitErr, emitOk, emitUsage, isJson, requireYesOrTty } from "../runtime.js";
6
+ import { openBrowser, resolveOpenUrl } from "../modules/oauth.js";
6
7
  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 USAGE = "Usage: frontpage shop items|variants|categories|settings|orders|payments|connect|status|disconnect … [--site <slug>] [--json]";
9
+ const CONNECT_FINISH = "Open the Frontpage URL while logged in, finish Stripe in the browser, then re-run status. This CLI cannot complete OAuth.";
8
10
  const CENTS_HINT = "$49.99 is 4999";
9
11
  function takeAll(work, name) {
10
12
  const out = [];
@@ -147,6 +149,7 @@ function printItem(row) {
147
149
  prices.length ? prices.join(",") : "",
148
150
  stocks.length ? `stock ${stocks.join(",")}` : "",
149
151
  row.active === false ? "inactive" : "active",
152
+ row.categories?.length ? row.categories.join(",") : "",
150
153
  row.archivedAt ? "archived" : "",
151
154
  ].filter(Boolean);
152
155
  console.log(`${row.itemId} ${row.title ?? ""} ${bits.join(" · ")}`);
@@ -168,9 +171,15 @@ export async function runShop(args) {
168
171
  if (!sub || sub === "list" || sub.startsWith("-")) {
169
172
  const listWork = sub === "list" ? work : rest.slice(1);
170
173
  const archived = hasFlag(listWork, "--archived");
174
+ const category = takeFlag(listWork, "--category");
171
175
  assertNoUnknownFlags(listWork, "shop");
172
- const qs = archived ? "?archived=1" : "";
173
- const data = await v1Get(`${base}/items${qs}`);
176
+ const qs = new URLSearchParams();
177
+ if (archived)
178
+ qs.set("archived", "1");
179
+ if (category)
180
+ qs.set("category", category);
181
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
182
+ const data = await v1Get(`${base}/items${suffix}`);
174
183
  if (isJson()) {
175
184
  emitOk("shop", data);
176
185
  return EXIT_OK;
@@ -214,6 +223,7 @@ export async function runShop(args) {
214
223
  const minCents = parseCentsFlag(takeFlag(work, "--min-cents"), "minCents");
215
224
  const requiresShipping = parseBoolFlag(takeFlag(work, "--requires-shipping"), "requiresShipping");
216
225
  const optionNames = parseCsv(takeFlag(work, "--option-names"));
226
+ const categories = parseCsv(takeFlag(work, "--categories"));
217
227
  const priceCents = parseCentsFlag(takeFlag(work, "--price-cents"), "priceCents");
218
228
  const discountCents = parseCentsFlag(takeFlag(work, "--discount-cents"), "discountPriceCents");
219
229
  const shippingCents = parseCentsFlag(takeFlag(work, "--shipping-cents"), "shippingCents");
@@ -239,6 +249,8 @@ export async function runShop(args) {
239
249
  body.requiresShipping = requiresShipping;
240
250
  if (optionNames)
241
251
  body.optionNames = optionNames;
252
+ if (categories)
253
+ body.categories = categories;
242
254
  if (images.length)
243
255
  body.images = images;
244
256
  if (body.checkout == null)
@@ -278,6 +290,7 @@ export async function runShop(args) {
278
290
  const minCents = parseCentsFlag(takeFlag(work, "--min-cents"), "minCents");
279
291
  const requiresShipping = parseBoolFlag(takeFlag(work, "--requires-shipping"), "requiresShipping");
280
292
  const active = parseBoolFlag(takeFlag(work, "--active"), "active");
293
+ const categories = parseCsv(takeFlag(work, "--categories"));
281
294
  const images = parseUrls(work);
282
295
  assertNoUnknownFlags(work, "shop");
283
296
  const body = {};
@@ -295,6 +308,8 @@ export async function runShop(args) {
295
308
  body.requiresShipping = requiresShipping;
296
309
  if (active !== undefined)
297
310
  body.active = active;
311
+ if (categories)
312
+ body.categories = categories;
298
313
  if (images.length)
299
314
  body.images = images;
300
315
  if (Object.keys(body).length === 0) {
@@ -474,6 +489,77 @@ export async function runShop(args) {
474
489
  }
475
490
  return emitUsage("shop", USAGE);
476
491
  }
492
+ if (group === "categories") {
493
+ const sub = rest[1];
494
+ const work = rest.slice(2);
495
+ if (!sub || sub === "list" || sub.startsWith("-")) {
496
+ const listWork = sub === "list" ? work : rest.slice(1);
497
+ const archived = hasFlag(listWork, "--archived");
498
+ assertNoUnknownFlags(listWork, "shop");
499
+ const qs = archived ? "?archived=1" : "";
500
+ const data = await v1Get(`${base}/categories${qs}`);
501
+ if (isJson()) {
502
+ emitOk("shop", data);
503
+ return EXIT_OK;
504
+ }
505
+ const rows = data.categories ?? [];
506
+ if (rows.length === 0) {
507
+ console.log("No categories.");
508
+ return EXIT_OK;
509
+ }
510
+ for (const row of rows) {
511
+ console.log(`${row.slug} ${row.label}${row.archivedAt ? " archived" : ""}`);
512
+ }
513
+ return EXIT_OK;
514
+ }
515
+ if (sub === "create") {
516
+ const slug = takeFlag(work, "--slug") ?? takeFlag(work, "--id") ?? work.find((a) => !a.startsWith("-"));
517
+ if (slug && work.includes(slug))
518
+ work.splice(work.indexOf(slug), 1);
519
+ const label = takeFlag(work, "--label") ?? takeFlag(work, "--title");
520
+ assertNoUnknownFlags(work, "shop");
521
+ if (!slug || !SHOP_ID_RE.test(slug) || !label) {
522
+ throw new CliHttpError("Usage: frontpage shop categories create hats --label Hats", EXIT_USAGE, 0, "bad_request");
523
+ }
524
+ const data = await v1Post(`${base}/categories`, {
525
+ slug,
526
+ label,
527
+ });
528
+ if (isJson()) {
529
+ emitOk("shop", data);
530
+ return EXIT_OK;
531
+ }
532
+ console.log(`${data.category.slug} ${data.category.label}`);
533
+ return EXIT_OK;
534
+ }
535
+ if (sub === "rename") {
536
+ const slug = requireShopId(work, "Category slug");
537
+ const label = takeFlag(work, "--label") ?? takeFlag(work, "--title");
538
+ assertNoUnknownFlags(work, "shop");
539
+ if (!label) {
540
+ throw new CliHttpError("Usage: frontpage shop categories rename hats --label Hats", EXIT_USAGE, 0, "bad_request");
541
+ }
542
+ const data = await v1Patch(`${base}/categories/${encodeURIComponent(slug)}`, { label });
543
+ if (isJson()) {
544
+ emitOk("shop", data);
545
+ return EXIT_OK;
546
+ }
547
+ console.log(`${data.category.slug} ${data.category.label}`);
548
+ return EXIT_OK;
549
+ }
550
+ if (sub === "archive") {
551
+ const slug = requireShopId(work, "Category slug");
552
+ assertNoUnknownFlags(work, "shop");
553
+ const data = await v1Post(`${base}/categories/${encodeURIComponent(slug)}/archive`, {});
554
+ if (isJson()) {
555
+ emitOk("shop", data);
556
+ return EXIT_OK;
557
+ }
558
+ console.log(`${data.category.slug} archived`);
559
+ return EXIT_OK;
560
+ }
561
+ return emitUsage("shop", USAGE);
562
+ }
477
563
  if (group === "settings") {
478
564
  const sub = rest[1] ?? "get";
479
565
  const work = rest.slice(2);
@@ -553,6 +639,61 @@ export async function runShop(args) {
553
639
  }
554
640
  return emitUsage("shop", USAGE);
555
641
  }
642
+ if (group === "connect") {
643
+ const work = rest.slice(1);
644
+ const noOpen = hasFlag(work, "--no-open");
645
+ assertNoUnknownFlags(work, "shop");
646
+ const data = await v1Post(`${base}/connect`, { action: "connect" });
647
+ const already = data.alreadyConnected === true;
648
+ if (isJson()) {
649
+ emitOk("shop", data);
650
+ }
651
+ else {
652
+ const url = resolveOpenUrl(data.connectUrl);
653
+ if (url)
654
+ console.log(url);
655
+ console.log(typeof data.message === "string" ? data.message : already ? "Already connected." : CONNECT_FINISH);
656
+ if (typeof data.warning === "string")
657
+ console.log(data.warning);
658
+ }
659
+ const url = resolveOpenUrl(data.connectUrl);
660
+ if (url && !noOpen && !already)
661
+ openBrowser(url);
662
+ return EXIT_OK;
663
+ }
664
+ if (group === "status") {
665
+ const work = rest.slice(1);
666
+ assertNoUnknownFlags(work, "shop");
667
+ const data = await v1Get(`${base}/connect`);
668
+ if (isJson()) {
669
+ emitOk("shop", data);
670
+ return EXIT_OK;
671
+ }
672
+ const connection = (data.connection ?? {});
673
+ const display = connection.display ?? {};
674
+ console.log(`connected ${connection.ready ? "yes" : "no"}`);
675
+ console.log(`charges ${display.chargesEnabled == null ? "—" : display.chargesEnabled ? "yes" : "no"}`);
676
+ console.log(`account ${display.accountPresent ? "present" : "none"}`);
677
+ if (typeof data.sharedStripe === "string")
678
+ console.log(`note ${data.sharedStripe}`);
679
+ if (typeof data.connectUrl === "string")
680
+ console.log(`connect ${data.connectUrl}`);
681
+ return EXIT_OK;
682
+ }
683
+ if (group === "disconnect") {
684
+ const work = rest.slice(1);
685
+ assertNoUnknownFlags(work, "shop");
686
+ const refuse = requireYesOrTty("disconnect the store (shared Stripe account with payments)");
687
+ if (refuse)
688
+ return emitUsage("shop", refuse);
689
+ const data = await v1Post(`${base}/connect`, { action: "disconnect" });
690
+ if (isJson()) {
691
+ emitOk("shop", data);
692
+ return EXIT_OK;
693
+ }
694
+ console.log(typeof data.message === "string" ? data.message : "Disconnected.");
695
+ return EXIT_OK;
696
+ }
556
697
  if (group === "orders" || group === "payments") {
557
698
  const noun = group;
558
699
  const sub = rest[1] ?? "list";
package/dist/registry.js CHANGED
@@ -442,25 +442,30 @@ export const COMMANDS = [
442
442
  {
443
443
  name: "shop",
444
444
  summary: "Store catalog: items, variants, stock, and store settings (not Square, not inbox)",
445
- usage: "frontpage shop items|variants|settings|orders|payments … [--site <slug>] [--json]",
445
+ usage: "frontpage shop items|variants|categories|settings|orders|payments|connect|status|disconnect … [--site <slug>] [--json]",
446
446
  topics: ["content"],
447
447
  examples: [
448
448
  "frontpage shop items list",
449
- "frontpage shop items create camp-hat --title \"Camp hat\" --price-cents 4999 --checkout cart --requires-shipping true --stock 20",
449
+ "frontpage shop categories create hats --label Hats",
450
+ "frontpage shop items create camp-hat --title \"Camp hat\" --price-cents 4999 --checkout cart --categories hats --stock 20",
451
+ "frontpage shop items list --category hats",
450
452
  "frontpage shop variants add camp-hat --id m --option Size=M --price-cents 4999 --stock 8",
451
453
  "frontpage shop items stock camp-hat --variant m --add 5",
452
454
  "frontpage shop items stock camp-hat --variant m --remove 5",
453
455
  "frontpage shop items stock camp-hat --variant m --set 20",
454
456
  "frontpage shop settings set-fulfillment --mode pickup",
455
457
  "frontpage shop settings set-currency --currency usd",
458
+ "frontpage shop connect --no-open",
459
+ "frontpage shop status",
456
460
  ],
457
461
  flags: [
458
462
  "Integer cents only. $49.99 is 4999. Do not send 49.99 or a dollar string.",
459
463
  "--add / --remove become signed delta. 5 adds 5. --remove 5 removes 5. Result copy is added 5 / removed 5 / set to 20 / stockNow.",
460
464
  "Pass either --add, --remove, or --set. Never two together. --set unlimited returns counted stock to unlimited.",
461
- "items: list|get|create|update|archive|activate [--off] | images --urls … | stock [--variant m] --add|--remove|--set",
465
+ "items: list|get|create|update|archive|activate [--off] | images --urls … | stock [--variant m] --add|--remove|--set. list --category hats. create/update --categories hats,summer",
466
+ "categories: list|create|rename|archive. Create the category first. Assigning an unknown slug fails.",
462
467
  "variants: add|update|archive <itemId> --id m --price-cents … [--option Size=M] [--stock]",
463
- "settings: get|set-fulfillment|set-currency|set-checkout-fee. Connect stays frontpage modules shop|payments connect.",
468
+ "settings: get|set-fulfillment|set-currency|set-checkout-fee. Connect: frontpage shop connect|status|disconnect. connect opens Frontpage then Stripe; --no-open for CI. disconnect keeps the shared Stripe account on payments.",
464
469
  "orders / payments list|get read the same ledgers as frontpage inbox orders|payments",
465
470
  "Unknown leftover flags fail closed (exit 2)",
466
471
  ],
@@ -722,7 +727,7 @@ export const TOPICS = {
722
727
  frontpage inbox [leads|subscribers|orders|google_reviews|…] [--mark-read] [--csv]
723
728
  frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide|tag|export
724
729
  # People CRM guide: https://frontpage.host/developers/people
725
- frontpage shop items|variants|settings|orders|payments
730
+ frontpage shop items|variants|categories|settings|orders|payments|connect|status|disconnect
726
731
  # Store catalog: https://frontpage.host/developers/shop
727
732
  # Integer cents only. $49.99 is 4999. stock --add/--remove are signed delta.
728
733
  frontpage mail settings|email|template|series|send|sends|enrolments|suppress
package/llms.txt CHANGED
@@ -17,7 +17,7 @@
17
17
  - SEO get: frontpage seo get [/path|--path /path]; search path filter: [path|--path <path>]; open [/path|--path /path]
18
18
  - Redirects: frontpage redirects list|set|rm — HTTP 301/308, instant, not a file; revert does not undo; do not fake with Astro.redirect / meta refresh
19
19
  - People CRM: frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide|tag|export. Site-scoped person graph (email or phone). Inbox stays the raw ledger. Hide is not forget. List export is CSV. Unknown leftover flags fail closed. Guide: https://frontpage.host/developers/people
20
- - Store catalog: frontpage shop items|variants|settings|orders|payments. Integer cents only ($49.99 is 4999). stock --add/--remove are signed delta (added 5 / removed 5 / set to 20 / stockNow). Connect stays frontpage modules shop|payments connect. Not Square ordering. Guide: https://frontpage.host/developers/shop
20
+ - Store catalog: frontpage shop items|variants|categories|settings|orders|payments|connect|status|disconnect. Integer cents only ($49.99 is 4999). stock --add/--remove are signed delta (added 5 / removed 5 / set to 20 / stockNow). Create a category before assigning it. frontpage shop connect [--no-open] opens Frontpage then Stripe; disconnect keeps the shared account on payments. frontpage modules shop|payments connect still works. Not Square ordering. Guide: https://frontpage.host/developers/shop
21
21
  - Analytics funnel: --steps JSON array or comma paths (/, /pricing); bad JSON → Invalid JSON for --steps
22
22
  - Modules: unknown key errors list Known: …; mailing list module key is signups (not subscribe)
23
23
  - Errors: --json API envelopes include requestId when the server returned one; local validation omits requestId/httpStatus; runs wait failed/canceled → ok:false + run.error (exit 2); deploy wait = agent idle + tip compile ready (fail-closed / compile timeout → exit 2); push --wait and publish --wait wait for GET /deploy ready
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontpage-host",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Frontpage CLI — manage your site from the terminal",
5
5
  "type": "module",
6
6
  "bin": {