frontpage-host 0.1.21 → 0.1.23

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
@@ -119,7 +119,7 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
119
119
  ## Modules & articles
120
120
 
121
121
  - Forms: `data-fp-form` markup in pages + `frontpage modules` for enable/config. No submit JS / honeypot / `action`. Do not put a CRM webhook or token in HTML.
122
- - Wizard / connect-once modules (payments/shop/ordering): `frontpage modules <key> connect` opens Frontpage (you must be logged in there), then Stripe or Square. Already-connected sites stay live — do not retry connect. payments/shop connect needs an **admin** key; ordering connect needs **editor**. `--no-open` for CI. `request-setup` remains valid when you want the **editor amber bar** instead. payments and shop share one Stripe account — payments disconnect fails if the store still uses it. Auto-active modules (forms/callbar/…) need markup, not request-setup. Typed settings: `frontpage modules callbar|forms|booking|signups get|set` (forms set needs `--instance`; do not target `default`). Form destinations (HubSpot / Pipedrive / Privyr): connect once, then attach each form. `frontpage modules forms destinations|connect-destination|attach-destination|detach-destination|remove-destination --yes|request-destination-setup`. Token write is **admin**; `request-destination-setup` lights the editor amber bar (**editor** key). Do not use `request-setup forms` for a CRM (no-op). Connecting does not attach every form. Privyr: paste the Incoming Webhook URL from Automations → Lead Sources → Webhooks (`https://www.privyr.com/api/v1/incoming-leads/…`), not a token and not Outgoing Webhooks. HubSpot: Development → Keys → Service keys or Development → Legacy apps → Auth → Show token. Pipedrive: profile → Company settings → Personal preferences → API. Full docs: https://frontpage.host/developers/modules#forms-destinations. `frontpage modules <key>` prints that key's CLI usage (exit 2). Index: https://frontpage.host/developers/modules · payments/shop/ordering pages under /developers/modules/<key>
122
+ - Connect-once modules: Store Stripe connect is `frontpage store connect`. `--no-open` for CI. `request-setup` remains valid when you want the **editor amber bar** instead. Payments and the store share one Stripe account — payments disconnect fails if the store still uses it. Auto-active modules (forms/callbar/…) need markup, not request-setup. Typed settings: `frontpage modules callbar|forms|booking|signups get|set` (forms set needs `--instance`; do not target `default`). Form destinations (HubSpot / Pipedrive / Privyr): connect once, then attach each form. `frontpage modules forms destinations|connect-destination|attach-destination|detach-destination|remove-destination --yes|request-destination-setup`. Token write is **admin**; `request-destination-setup` lights the editor amber bar (**editor** key). Do not use `request-setup forms` for a CRM (no-op). Connecting does not attach every form. Privyr: paste the Incoming Webhook URL from Automations → Lead Sources → Webhooks (`https://www.privyr.com/api/v1/incoming-leads/…`), not a token and not Outgoing Webhooks. HubSpot: Development → Keys → Service keys or Development → Legacy apps → Auth → Show token. Pipedrive: profile → Company settings → Personal preferences → API. Full docs: https://frontpage.host/developers/modules#forms-destinations. `frontpage modules <key>` prints that key's CLI usage (exit 2). Index: https://frontpage.host/developers/modules. Store: https://frontpage.host/developers/store
123
123
  - Google reviews: platform nodes only (`data-fp-google-reviews` / `data-fp-google-ask` + required `data-fp-requires="google_reviews"`). `data-layout` is `feed` (default), `badge`, or `link`. Never invent stars, quotes, a 1–10, or a Maps URL. Do not pick a Place ID via Autocomplete and do not write `placeId` through `modules config`. Owner-supplied Place ID / Maps URL: `frontpage modules google_reviews connect --place-id …` or `--maps-url …` (server resolves + seeds). Curation: `list-reviews`, `hide --id`, `show --id`, `set-stars`, `set-sort`, `set-weight`, `reset-weights`, `refresh`, `disconnect`. Usage: `frontpage modules google_reviews`. Docs: https://frontpage.host/developers/modules/google-reviews. Ask-widget scores: `frontpage inbox google_reviews`.
124
124
  - Articles: `frontpage articles …` for lifecycle; bodies via sync under `content/` or `articles content get|set`. Section landing is an owned `src/pages/<section>/index.astro`.
125
125
  - Migrate a folder into Articles: prefer `frontpage prompt "migrate … into articles"` — do not invent a mass delete+write migration.
@@ -128,7 +128,8 @@ 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|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
131
+ - Routines: `frontpage routines list|get|add|set|pause|resume|rm|run|runs`. One routine can hold several hours and several days. `8am,12pm,4pm` on `tue,thu,fri` stores hours `[8, 12, 16]` and weekdays `[2, 4, 5]`. Ask for an IANA timezone. Do not guess UTC. Full guide: https://frontpage.host/developers/routines
132
+ - Store catalog: `frontpage store items|variants|categories|settings|orders|payments|connect|status|disconnect`. Integer cents only (`$49.99` is `4999`). `--price-cents` is the usual cost. Optional `--discount-cents` is the sale the buyer pays (must be lower). Create a category before assigning it. Photos are a site path `/__media/uploads/<this-slug>/…` or an HTTPS URL already saved on this site. `frontpage store connect [--no-open]` opens Frontpage then Stripe. Checkout is store-wide (`settings set-checkout`); do not use leftover `items --checkout` to pick cart vs Pay now. Service fee is `settings set-checkout-fee`. The cart starts hidden. Product pages are the default: unless they said they do not want product pages, write one `src/pages/products/item.astro` (or shop) with `data-fp-store-product`. If they said no, skip that file and do not bind `href` or `url`. Description is a small markdown subset; product-page bind is `<div data-fp-store-bind="description">`. Every store control carries `data-fp-requires="store"`. Do not bake titles or prices. Full guide: https://frontpage.host/developers/store
132
133
 
133
134
  ## Agent prompt loop
134
135
 
package/dist/bin.js CHANGED
@@ -29,6 +29,7 @@ import { runInbox } from "./commands/inbox.js";
29
29
  import { runSeo } from "./commands/seo.js";
30
30
  import { runRedirects } from "./commands/redirects.js";
31
31
  import { runPeople } from "./commands/people.js";
32
+ import { runRoutines } from "./commands/routines.js";
32
33
  import { runShop } from "./commands/shop.js";
33
34
  import { runMail } from "./commands/mail.js";
34
35
  import { runArticles } from "./commands/articles.js";
@@ -118,7 +119,9 @@ async function main() {
118
119
  return runInbox(args);
119
120
  case "people":
120
121
  return runPeople(args);
121
- case "shop":
122
+ case "routines":
123
+ return runRoutines(args);
124
+ case "store":
122
125
  return runShop(args);
123
126
  case "mail":
124
127
  return runMail(args);
@@ -2,7 +2,7 @@ import { resolveSlug } from "../site.js";
2
2
  import { EXIT_OK, exitCodeForError, v1Get, v1Post } from "../http/client.js";
3
3
  import { takeFlag } from "../argv.js";
4
4
  import { emitUsage } from "../runtime.js";
5
- import { formatModuleUsage, formatModulesIndex, isCliModuleAction, isDocumentedModuleKey, isGenericModuleVerb, } from "../modules/commands.js";
5
+ import { formatModuleUsage, formatModulesIndex, formatPaymentsIsNotAModule, formatShopIsNotAModule, isCliModuleAction, isDocumentedModuleKey, isGenericModuleVerb, } from "../modules/commands.js";
6
6
  import { runGoogleReviews } from "../modules/google_reviews.js";
7
7
  import { isTypedConfigCliKey, runTypedConfig } from "../modules/typed_config.js";
8
8
  import { isFormsDestinationCliAction, runFormsDestinations } from "../modules/forms_destinations.js";
@@ -35,6 +35,12 @@ export async function runModules(args) {
35
35
  const peeked = positionalArgs(args);
36
36
  const peekedKey = peeked[0];
37
37
  const peekedAction = peeked[1];
38
+ if (peekedKey === "shop") {
39
+ return emitUsage("modules", formatShopIsNotAModule());
40
+ }
41
+ if (peekedKey === "payments") {
42
+ return emitUsage("modules", formatPaymentsIsNotAModule());
43
+ }
38
44
  if (peekedKey && isDocumentedModuleKey(peekedKey)) {
39
45
  if (!peekedAction) {
40
46
  return emitUsage("modules", formatModuleUsage(peekedKey));
@@ -63,6 +69,12 @@ export async function runModules(args) {
63
69
  const slug = resolved.slug;
64
70
  const rest = rewriteKeyFirst(resolved.rest);
65
71
  const sub = rest[0];
72
+ if (rest[1] === "shop" || rest.includes("shop")) {
73
+ return emitUsage("modules", formatShopIsNotAModule());
74
+ }
75
+ if (rest[1] === "payments") {
76
+ return emitUsage("modules", formatPaymentsIsNotAModule());
77
+ }
66
78
  if (!sub || sub === "list" || sub === "ls") {
67
79
  const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/modules`);
68
80
  for (const m of data.modules) {
@@ -37,6 +37,11 @@ function printPerson(row) {
37
37
  bits.push(row.email);
38
38
  if (row.phone && row.phone !== row.name)
39
39
  bits.push(row.phone);
40
+ if (row.address?.line1) {
41
+ bits.push([row.address.line1, row.address.city, row.address.region, row.address.postal, row.address.country]
42
+ .filter(Boolean)
43
+ .join(", "));
44
+ }
40
45
  if (row.followUpAt)
41
46
  bits.push(`follow-up ${row.followUpAt}`);
42
47
  if (row.hiddenAt)
@@ -174,9 +179,32 @@ export async function runPeople(args) {
174
179
  if (v !== undefined)
175
180
  body[field] = v === "" ? null : v;
176
181
  }
182
+ const clearAddress = hasFlag(work, "--clear-address");
183
+ const addressFields = {
184
+ name: takeFlag(work, "--address-name"),
185
+ line1: takeFlag(work, "--line1"),
186
+ line2: takeFlag(work, "--line2"),
187
+ city: takeFlag(work, "--city"),
188
+ region: takeFlag(work, "--region"),
189
+ postal: takeFlag(work, "--postal"),
190
+ country: takeFlag(work, "--country"),
191
+ };
192
+ if (clearAddress)
193
+ body.address = null;
194
+ else if (Object.values(addressFields).some((value) => value !== undefined)) {
195
+ body.address = {
196
+ name: addressFields.name ?? null,
197
+ line1: addressFields.line1 ?? null,
198
+ line2: addressFields.line2 ?? null,
199
+ city: addressFields.city ?? null,
200
+ region: addressFields.region ?? null,
201
+ postal: addressFields.postal ?? null,
202
+ country: addressFields.country ?? null,
203
+ };
204
+ }
177
205
  assertNoUnknownFlags(work, "people");
178
206
  if (Object.keys(body).length === 0) {
179
- console.error("Usage: frontpage people set <id> [--name …] [--email …] [--phone …] [--notes …] [--status …]");
207
+ console.error("Usage: frontpage people set <id> [--name …] [--email …] [--phone …] [--notes …] [--status …] [--line1 … --city … --region … --postal … --country …] [--clear-address]");
180
208
  return EXIT_USAGE;
181
209
  }
182
210
  const data = await v1Patch(`${base}/${encodeURIComponent(id)}`, body);
@@ -0,0 +1 @@
1
+ export declare function runRoutines(args: string[]): Promise<number>;
@@ -0,0 +1,156 @@
1
+ import { resolveSlug } from "../site.js";
2
+ import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Delete, v1Get, v1Patch, v1Post, } from "../http/client.js";
3
+ import { CliHttpError } from "../http/client.js";
4
+ import { assertNoUnknownFlags, takeFlag } from "../argv.js";
5
+ import { emitErr, emitOk, isJson } from "../runtime.js";
6
+ import { parseHourList, parseMonthDayList, parseWeekdayList } from "../routines-parse.js";
7
+ const USAGE = "Usage: frontpage routines list|get|add|set|pause|resume|rm|run|runs [--site <slug>] [--json]";
8
+ const ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9
+ function requireId(work) {
10
+ const fromFlag = takeFlag(work, "--id");
11
+ const positional = work.find((arg) => !arg.startsWith("-"));
12
+ const raw = fromFlag ?? (positional ? work.splice(work.indexOf(positional), 1)[0] : undefined);
13
+ if (!raw) {
14
+ throw new CliHttpError("Routine id is required. Run: frontpage help routines", EXIT_USAGE, 0, "bad_request");
15
+ }
16
+ if (!ID_RE.test(raw))
17
+ throw new CliHttpError("Routine not found.", EXIT_USAGE, 404, "not_found");
18
+ return raw;
19
+ }
20
+ function scheduleBody(work, requireCore) {
21
+ const name = takeFlag(work, "--name");
22
+ const prompt = takeFlag(work, "--prompt");
23
+ const cadence = takeFlag(work, "--cadence");
24
+ const timezone = takeFlag(work, "--timezone");
25
+ const hoursRaw = takeFlag(work, "--hours");
26
+ const weekdaysRaw = takeFlag(work, "--weekdays");
27
+ const monthDaysRaw = takeFlag(work, "--month-days");
28
+ if (requireCore && (!name || !prompt || !cadence || !timezone)) {
29
+ throw new CliHttpError("add needs --name, --prompt, --cadence, and --timezone. Run: frontpage help routines", EXIT_USAGE, 0, "bad_request");
30
+ }
31
+ let hours;
32
+ let weekdays;
33
+ let monthDays;
34
+ try {
35
+ if (hoursRaw !== undefined)
36
+ hours = parseHourList(hoursRaw);
37
+ if (weekdaysRaw !== undefined)
38
+ weekdays = parseWeekdayList(weekdaysRaw);
39
+ if (monthDaysRaw !== undefined)
40
+ monthDays = parseMonthDayList(monthDaysRaw);
41
+ }
42
+ catch (err) {
43
+ throw new CliHttpError(err instanceof Error ? err.message : "Invalid schedule.", EXIT_USAGE, 0, "bad_request");
44
+ }
45
+ const body = {};
46
+ if (name)
47
+ body.name = name;
48
+ if (prompt)
49
+ body.prompt = prompt;
50
+ if (cadence)
51
+ body.cadence = cadence;
52
+ if (timezone)
53
+ body.timezone = timezone;
54
+ if (hours)
55
+ body.hours = hours;
56
+ if (weekdays)
57
+ body.weekdays = weekdays;
58
+ if (monthDays)
59
+ body.monthDays = monthDays;
60
+ return body;
61
+ }
62
+ function printRoutine(row) {
63
+ const bits = [row.name, row.status, row.sentence, row.nextRunAt ? `next ${row.nextRunAt}` : ""].filter(Boolean);
64
+ console.log(`${row.id} ${bits.join(" · ")}`);
65
+ }
66
+ export async function runRoutines(args) {
67
+ try {
68
+ const { slug, rest } = await resolveSlug(args);
69
+ const sub = rest[0];
70
+ const base = `/api/v1/sites/${encodeURIComponent(slug)}/routines`;
71
+ if (!sub || sub === "list") {
72
+ const work = sub === "list" ? rest.slice(1) : [...rest];
73
+ assertNoUnknownFlags(work, "routines");
74
+ const data = await v1Get(base);
75
+ if (isJson()) {
76
+ emitOk("routines", data);
77
+ return EXIT_OK;
78
+ }
79
+ const rows = data.routines ?? [];
80
+ if (rows.length === 0) {
81
+ console.log("No routines.");
82
+ return EXIT_OK;
83
+ }
84
+ for (const row of rows)
85
+ printRoutine(row);
86
+ return EXIT_OK;
87
+ }
88
+ if (sub === "get" || sub === "runs") {
89
+ const work = rest.slice(1);
90
+ const id = requireId(work);
91
+ assertNoUnknownFlags(work, "routines");
92
+ const path = sub === "get" ? `${base}/${id}` : `${base}/${id}/runs`;
93
+ const data = await v1Get(path);
94
+ if (isJson())
95
+ emitOk("routines", data);
96
+ else
97
+ console.log(JSON.stringify(data, null, 2));
98
+ return EXIT_OK;
99
+ }
100
+ if (sub === "add") {
101
+ const work = rest.slice(1);
102
+ const body = scheduleBody(work, true);
103
+ assertNoUnknownFlags(work, "routines");
104
+ const data = await v1Post(base, body);
105
+ if (isJson())
106
+ emitOk("routines", data);
107
+ else
108
+ printRoutine(data.routine);
109
+ return EXIT_OK;
110
+ }
111
+ if (sub === "set") {
112
+ const work = rest.slice(1);
113
+ const id = requireId(work);
114
+ const body = scheduleBody(work, false);
115
+ assertNoUnknownFlags(work, "routines");
116
+ const data = await v1Patch(`${base}/${id}`, body);
117
+ if (isJson())
118
+ emitOk("routines", data);
119
+ else
120
+ printRoutine(data.routine);
121
+ return EXIT_OK;
122
+ }
123
+ if (sub === "pause" || sub === "resume" || sub === "run") {
124
+ const work = rest.slice(1);
125
+ const id = requireId(work);
126
+ assertNoUnknownFlags(work, "routines");
127
+ const data = await v1Post(`${base}/${id}/${sub}`, {});
128
+ if (isJson())
129
+ emitOk("routines", data);
130
+ else if (data.routine)
131
+ printRoutine(data.routine);
132
+ else
133
+ console.log(JSON.stringify(data, null, 2));
134
+ return EXIT_OK;
135
+ }
136
+ if (sub === "rm") {
137
+ const work = rest.slice(1);
138
+ const id = requireId(work);
139
+ assertNoUnknownFlags(work, "routines");
140
+ const data = await v1Delete(`${base}/${id}`);
141
+ if (isJson())
142
+ emitOk("routines", data);
143
+ else
144
+ console.log(`Deleted ${data.deleted ?? id}`);
145
+ return EXIT_OK;
146
+ }
147
+ console.error(USAGE);
148
+ return EXIT_USAGE;
149
+ }
150
+ catch (err) {
151
+ if (isJson())
152
+ return emitErr("routines", err);
153
+ console.error(err instanceof Error ? err.message : String(err));
154
+ return exitCodeForError(err);
155
+ }
156
+ }