frontpage-host 0.1.21 → 0.1.22
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 +3 -2
- package/dist/bin.js +4 -1
- package/dist/commands/modules.js +13 -1
- package/dist/commands/people.js +29 -1
- package/dist/commands/routines.d.ts +1 -0
- package/dist/commands/routines.js +156 -0
- package/dist/commands/shop.js +95 -49
- package/dist/completions/generate.js +12 -11
- package/dist/http/client.js +1 -1
- package/dist/modules/commands.d.ts +3 -2
- package/dist/modules/commands.js +21 -89
- package/dist/modules/oauth.d.ts +2 -2
- package/dist/modules/oauth.js +7 -101
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +62 -29
- package/dist/routines-parse.d.ts +7 -0
- package/dist/routines-parse.js +78 -0
- package/llms.txt +3 -2
- package/package.json +1 -1
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
|
-
-
|
|
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
|
-
-
|
|
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 "
|
|
122
|
+
case "routines":
|
|
123
|
+
return runRoutines(args);
|
|
124
|
+
case "store":
|
|
122
125
|
return runShop(args);
|
|
123
126
|
case "mail":
|
|
124
127
|
return runMail(args);
|
package/dist/commands/modules.js
CHANGED
|
@@ -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) {
|
package/dist/commands/people.js
CHANGED
|
@@ -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
|
+
}
|