frontpage-host 0.1.4 → 0.1.6
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 +4 -2
- package/dist/bin.js +6 -0
- package/dist/commands/analytics.js +166 -8
- package/dist/commands/extensions.js +15 -1
- package/dist/commands/fonts.d.ts +1 -0
- package/dist/commands/fonts.js +71 -0
- package/dist/commands/modules.js +46 -1
- package/dist/commands/search.js +58 -8
- package/dist/commands/tokens.d.ts +1 -0
- package/dist/commands/tokens.js +64 -0
- package/dist/completions/generate.js +15 -4
- package/dist/registry.js +78 -12
- package/llms.txt +5 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -100,7 +100,7 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
|
|
|
100
100
|
2. **`src/config/site.ts`** — keep `pages` (path registry) and `nav` in sync when adding/renaming/deleting page files. Tab title comes from BaseLayout, not the `pages` map label.
|
|
101
101
|
3. **One file per URL** for similar page sets (services, guides, …). Do not default to `getStaticPaths()` over `src/data/*.ts`. Blog/news → `frontpage articles`, not a hand-rolled catalog.
|
|
102
102
|
4. **No backends / databases / fake forms or checkout.** Real forms use `<form data-fp-form="slug">` + `frontpage modules`. Refuse or use modules for payments.
|
|
103
|
-
5. **Tokens** in `src/config/tokens.ts` — keep required color/font/spacing/radii keys; style via CSS variables / token Tailwind. Setting a font token is not enough — apply `font-sans` / `font-serif` / `font-display`.
|
|
103
|
+
5. **Tokens** in `src/config/tokens.ts` — keep required color/font/spacing/radii keys; style via CSS variables / token Tailwind. Setting a font token is not enough — apply `font-sans` / `font-serif` / `font-display`. Browse fonts with `frontpage fonts`; set one token with `frontpage tokens set --group … --key … --value …` (CAS like push). Module how-tos: `frontpage modules docs <key>` / `frontpage extensions docs <key>`.
|
|
104
104
|
6. **Shared chrome** — prefer `src/components/Header.astro` + `Footer.astro` (or `chrome: { managed: true }` in `site.ts`).
|
|
105
105
|
7. **404** — customize `src/pages/404.astro` if needed; keep `frontpage:404` meta; never register `/404` in `nav`/`pages`.
|
|
106
106
|
8. **Platform-owned paths** stay off-limits (BaseLayout overlay, module/extension shells, `package.json`, …). Customer layouts like `DocsLayout` / `GuideLayout` are OK.
|
|
@@ -108,10 +108,12 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
|
|
|
108
108
|
## Modules & articles
|
|
109
109
|
|
|
110
110
|
- Forms: `data-fp-form` markup in pages + `frontpage modules` for enable/config. No submit JS / honeypot / `action`.
|
|
111
|
+
- Wizard modules (payments/shop/ordering/instagram): after building gated markup, `frontpage modules request-setup <key> --reason "…"` once — owner finishes in the **editor status bar** (CLI cannot show it). Auto-active modules (forms/callbar/…) need markup, not request-setup.
|
|
111
112
|
- 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`.
|
|
112
113
|
- Migrate a folder into Articles: prefer `frontpage prompt "migrate … into articles"` — do not invent a mass delete+write migration.
|
|
113
114
|
- Extensions: `frontpage extensions`; if LOCKED on plan, do not fake charts/etc.
|
|
114
115
|
- Media in markup: `/__media/uploads/<slug>/…` from `frontpage media` (not raw blob URLs).
|
|
116
|
+
- Analytics: bare `frontpage analytics` = dashboard; also `summary|query|funnel|timeseries|compare|sql` for structured reads (SQL is guard + RLS site-scoped).
|
|
115
117
|
|
|
116
118
|
## Agent prompt loop
|
|
117
119
|
|
|
@@ -128,7 +130,7 @@ Mid-task continuation is automatic. There is **no** `runs continue` in v1.
|
|
|
128
130
|
These already return JSON bodies (wrap further with global `--json` where supported):
|
|
129
131
|
|
|
130
132
|
- `usage`, `analytics`, `seo`, `articles`, `settings`, `inbox` (default JSON)
|
|
131
|
-
- Mutate: `modules enable|disable|archive|config`, `extensions …`
|
|
133
|
+
- Mutate: `modules enable|disable|archive|config|request-setup`, `extensions …`
|
|
132
134
|
|
|
133
135
|
Click-edit (exact single occurrence), **not** file push:
|
|
134
136
|
|
package/dist/bin.js
CHANGED
|
@@ -29,6 +29,8 @@ import { runSeo } from "./commands/seo.js";
|
|
|
29
29
|
import { runArticles } from "./commands/articles.js";
|
|
30
30
|
import { runSettings } from "./commands/settings.js";
|
|
31
31
|
import { runEdit } from "./commands/edit.js";
|
|
32
|
+
import { runFonts } from "./commands/fonts.js";
|
|
33
|
+
import { runTokens } from "./commands/tokens.js";
|
|
32
34
|
import { runHistory } from "./commands/history.js";
|
|
33
35
|
import { runRevert } from "./commands/revert.js";
|
|
34
36
|
import { runCompletion } from "./commands/completion.js";
|
|
@@ -110,6 +112,10 @@ async function main() {
|
|
|
110
112
|
return runSettings(args);
|
|
111
113
|
case "edit":
|
|
112
114
|
return runEdit(args);
|
|
115
|
+
case "fonts":
|
|
116
|
+
return runFonts(args);
|
|
117
|
+
case "tokens":
|
|
118
|
+
return runTokens(args);
|
|
113
119
|
case "history":
|
|
114
120
|
return runHistory(args);
|
|
115
121
|
case "revert":
|
|
@@ -1,17 +1,175 @@
|
|
|
1
1
|
import { resolveSlug } from "../site.js";
|
|
2
|
-
import { EXIT_OK, exitCodeForError, v1Get } from "../http/client.js";
|
|
2
|
+
import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get, v1Post } from "../http/client.js";
|
|
3
|
+
import { emitErr, emitOk, isJson } from "../runtime.js";
|
|
4
|
+
function takeFlag(rest, name) {
|
|
5
|
+
const i = rest.indexOf(name);
|
|
6
|
+
if (i === -1)
|
|
7
|
+
return undefined;
|
|
8
|
+
const v = rest[i + 1];
|
|
9
|
+
rest.splice(i, 2);
|
|
10
|
+
return v;
|
|
11
|
+
}
|
|
12
|
+
function parseJsonFlag(raw, label) {
|
|
13
|
+
if (raw === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(raw);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new Error(`${label} must be valid JSON`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function rangeFields(work) {
|
|
23
|
+
const days = takeFlag(work, "--days");
|
|
24
|
+
const from = takeFlag(work, "--from");
|
|
25
|
+
const to = takeFlag(work, "--to");
|
|
26
|
+
const out = {};
|
|
27
|
+
if (days)
|
|
28
|
+
out.days = Number(days);
|
|
29
|
+
if (from)
|
|
30
|
+
out.from = from;
|
|
31
|
+
if (to)
|
|
32
|
+
out.to = to;
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function printJson(data) {
|
|
36
|
+
console.log(JSON.stringify(data, null, 2));
|
|
37
|
+
}
|
|
3
38
|
export async function runAnalytics(args) {
|
|
4
39
|
try {
|
|
5
40
|
const { slug, rest } = await resolveSlug(args);
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
41
|
+
const work = [...rest];
|
|
42
|
+
const sub = work[0];
|
|
43
|
+
const ops = new Set(["summary", "query", "funnel", "timeseries", "compare", "sql", "dashboard"]);
|
|
44
|
+
// Bare `analytics` or `analytics dashboard` → legacy GET dashboard (compat).
|
|
45
|
+
if (!sub || sub === "dashboard" || !ops.has(sub) && sub.startsWith("--")) {
|
|
46
|
+
if (sub === "dashboard")
|
|
47
|
+
work.shift();
|
|
48
|
+
const days = takeFlag(work, "--days") ?? "7";
|
|
49
|
+
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/analytics?days=${encodeURIComponent(days)}`);
|
|
50
|
+
if (isJson()) {
|
|
51
|
+
emitOk("analytics", data);
|
|
52
|
+
return EXIT_OK;
|
|
53
|
+
}
|
|
54
|
+
printJson(data.dashboard);
|
|
55
|
+
return EXIT_OK;
|
|
56
|
+
}
|
|
57
|
+
if (!ops.has(sub)) {
|
|
58
|
+
console.error("Usage: frontpage analytics [dashboard|summary|query|funnel|timeseries|compare|sql] …");
|
|
59
|
+
return EXIT_USAGE;
|
|
60
|
+
}
|
|
61
|
+
work.shift();
|
|
62
|
+
const base = `/api/v1/sites/${encodeURIComponent(slug)}/analytics`;
|
|
63
|
+
if (sub === "summary") {
|
|
64
|
+
const filters = parseJsonFlag(takeFlag(work, "--filters"), "--filters");
|
|
65
|
+
const body = { op: "summary", ...rangeFields(work), ...(filters ? { filters } : {}) };
|
|
66
|
+
const data = await v1Post(base, body);
|
|
67
|
+
if (isJson())
|
|
68
|
+
emitOk("analytics", data);
|
|
69
|
+
else
|
|
70
|
+
printJson(data);
|
|
71
|
+
return EXIT_OK;
|
|
72
|
+
}
|
|
73
|
+
if (sub === "query") {
|
|
74
|
+
const dimension = takeFlag(work, "--dimension") ?? work.shift();
|
|
75
|
+
if (!dimension) {
|
|
76
|
+
console.error("Usage: frontpage analytics query --dimension <path|source|…> [--measure count] [--limit N] [--filters '{…}'] [--days N]");
|
|
77
|
+
return EXIT_USAGE;
|
|
78
|
+
}
|
|
79
|
+
const measure = takeFlag(work, "--measure");
|
|
80
|
+
const limit = takeFlag(work, "--limit");
|
|
81
|
+
const filters = parseJsonFlag(takeFlag(work, "--filters"), "--filters");
|
|
82
|
+
const body = {
|
|
83
|
+
op: "query",
|
|
84
|
+
dimension,
|
|
85
|
+
...rangeFields(work),
|
|
86
|
+
...(measure ? { measure } : {}),
|
|
87
|
+
...(limit ? { limit: Number(limit) } : {}),
|
|
88
|
+
...(filters ? { filters } : {}),
|
|
89
|
+
};
|
|
90
|
+
const data = await v1Post(base, body);
|
|
91
|
+
if (isJson())
|
|
92
|
+
emitOk("analytics", data);
|
|
93
|
+
else
|
|
94
|
+
printJson(data);
|
|
95
|
+
return EXIT_OK;
|
|
96
|
+
}
|
|
97
|
+
if (sub === "funnel") {
|
|
98
|
+
const stepsRaw = takeFlag(work, "--steps");
|
|
99
|
+
if (!stepsRaw) {
|
|
100
|
+
console.error(`Usage: frontpage analytics funnel --steps '[{"path":"/"},{"path":"/pricing"}]' [--days N]`);
|
|
101
|
+
return EXIT_USAGE;
|
|
102
|
+
}
|
|
103
|
+
const steps = parseJsonFlag(stepsRaw, "--steps");
|
|
104
|
+
const body = { op: "funnel", steps, ...rangeFields(work) };
|
|
105
|
+
const data = await v1Post(base, body);
|
|
106
|
+
if (isJson())
|
|
107
|
+
emitOk("analytics", data);
|
|
108
|
+
else
|
|
109
|
+
printJson(data);
|
|
110
|
+
return EXIT_OK;
|
|
111
|
+
}
|
|
112
|
+
if (sub === "timeseries") {
|
|
113
|
+
const metric = takeFlag(work, "--metric");
|
|
114
|
+
const interval = takeFlag(work, "--interval");
|
|
115
|
+
const filters = parseJsonFlag(takeFlag(work, "--filters"), "--filters");
|
|
116
|
+
const body = {
|
|
117
|
+
op: "timeseries",
|
|
118
|
+
...rangeFields(work),
|
|
119
|
+
...(metric ? { metric } : {}),
|
|
120
|
+
...(interval ? { interval } : {}),
|
|
121
|
+
...(filters ? { filters } : {}),
|
|
122
|
+
};
|
|
123
|
+
const data = await v1Post(base, body);
|
|
124
|
+
if (isJson())
|
|
125
|
+
emitOk("analytics", data);
|
|
126
|
+
else
|
|
127
|
+
printJson(data);
|
|
128
|
+
return EXIT_OK;
|
|
129
|
+
}
|
|
130
|
+
if (sub === "compare") {
|
|
131
|
+
const aRaw = takeFlag(work, "--a");
|
|
132
|
+
const bRaw = takeFlag(work, "--b");
|
|
133
|
+
if (!aRaw || !bRaw) {
|
|
134
|
+
console.error(`Usage: frontpage analytics compare --a '{"device":"mobile"}' --b '{"device":"desktop"}' [--metric uniqueVisitors] [--days N]`);
|
|
135
|
+
return EXIT_USAGE;
|
|
136
|
+
}
|
|
137
|
+
const a = parseJsonFlag(aRaw, "--a");
|
|
138
|
+
const b = parseJsonFlag(bRaw, "--b");
|
|
139
|
+
const metric = takeFlag(work, "--metric");
|
|
140
|
+
const body = {
|
|
141
|
+
op: "compare",
|
|
142
|
+
a,
|
|
143
|
+
b,
|
|
144
|
+
...rangeFields(work),
|
|
145
|
+
...(metric ? { metric } : {}),
|
|
146
|
+
};
|
|
147
|
+
const data = await v1Post(base, body);
|
|
148
|
+
if (isJson())
|
|
149
|
+
emitOk("analytics", data);
|
|
150
|
+
else
|
|
151
|
+
printJson(data);
|
|
152
|
+
return EXIT_OK;
|
|
153
|
+
}
|
|
154
|
+
if (sub === "sql") {
|
|
155
|
+
const sql = takeFlag(work, "--sql") ?? work.join(" ").trim();
|
|
156
|
+
if (!sql) {
|
|
157
|
+
console.error('Usage: frontpage analytics sql --sql "SELECT path, count(*) FROM analytics_events GROUP BY 1"');
|
|
158
|
+
return EXIT_USAGE;
|
|
159
|
+
}
|
|
160
|
+
const data = await v1Post(base, { op: "sql", sql });
|
|
161
|
+
if (isJson())
|
|
162
|
+
emitOk("analytics", data);
|
|
163
|
+
else
|
|
164
|
+
printJson(data);
|
|
165
|
+
return EXIT_OK;
|
|
166
|
+
}
|
|
167
|
+
console.error("Usage: frontpage analytics [dashboard|summary|query|funnel|timeseries|compare|sql] …");
|
|
168
|
+
return EXIT_USAGE;
|
|
13
169
|
}
|
|
14
170
|
catch (err) {
|
|
171
|
+
if (isJson())
|
|
172
|
+
return emitErr("analytics", err);
|
|
15
173
|
console.error(err instanceof Error ? err.message : String(err));
|
|
16
174
|
return exitCodeForError(err);
|
|
17
175
|
}
|
|
@@ -23,6 +23,20 @@ export async function runExtensions(args) {
|
|
|
23
23
|
}
|
|
24
24
|
return EXIT_OK;
|
|
25
25
|
}
|
|
26
|
+
if (sub === "docs" || sub === "doc") {
|
|
27
|
+
const key = rest[1];
|
|
28
|
+
if (!key) {
|
|
29
|
+
console.error("Usage: frontpage extensions docs <key> [--site <slug>]");
|
|
30
|
+
return EXIT_USAGE;
|
|
31
|
+
}
|
|
32
|
+
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/extensions/${encodeURIComponent(key)}`);
|
|
33
|
+
console.log(`# ${data.key} (${data.name}) [${data.state}]`);
|
|
34
|
+
if (data.note)
|
|
35
|
+
console.log(data.note);
|
|
36
|
+
console.log("");
|
|
37
|
+
console.log(data.docs);
|
|
38
|
+
return EXIT_OK;
|
|
39
|
+
}
|
|
26
40
|
if (sub === "enable" || sub === "disable" || sub === "config") {
|
|
27
41
|
const key = rest[1];
|
|
28
42
|
if (!key) {
|
|
@@ -50,7 +64,7 @@ export async function runExtensions(args) {
|
|
|
50
64
|
console.log(JSON.stringify(data, null, 2));
|
|
51
65
|
return EXIT_OK;
|
|
52
66
|
}
|
|
53
|
-
console.error("Usage: frontpage extensions [list|enable|disable|config] …");
|
|
67
|
+
console.error("Usage: frontpage extensions [list|docs|enable|disable|config] …");
|
|
54
68
|
return EXIT_USAGE;
|
|
55
69
|
}
|
|
56
70
|
catch (err) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runFonts(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get } from "../http/client.js";
|
|
2
|
+
import { emitErr, emitOk, isJson } from "../runtime.js";
|
|
3
|
+
function takeFlag(rest, name) {
|
|
4
|
+
const i = rest.indexOf(name);
|
|
5
|
+
if (i === -1)
|
|
6
|
+
return undefined;
|
|
7
|
+
const v = rest[i + 1];
|
|
8
|
+
rest.splice(i, 2);
|
|
9
|
+
return v;
|
|
10
|
+
}
|
|
11
|
+
function hasFlag(rest, name) {
|
|
12
|
+
const i = rest.indexOf(name);
|
|
13
|
+
if (i === -1)
|
|
14
|
+
return false;
|
|
15
|
+
rest.splice(i, 1);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
export async function runFonts(args) {
|
|
19
|
+
try {
|
|
20
|
+
const work = [...args];
|
|
21
|
+
let sub = work[0];
|
|
22
|
+
if (sub === "list" || sub === "search" || sub === "ls") {
|
|
23
|
+
work.shift();
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
sub = "list";
|
|
27
|
+
}
|
|
28
|
+
const slot = takeFlag(work, "--slot");
|
|
29
|
+
const query = takeFlag(work, "--query") ??
|
|
30
|
+
(sub === "search" ? work.shift() : undefined) ??
|
|
31
|
+
(work[0] && !work[0].startsWith("-") ? work.shift() : undefined);
|
|
32
|
+
const variableOnly = hasFlag(work, "--variable-only");
|
|
33
|
+
const limit = takeFlag(work, "--limit");
|
|
34
|
+
const offset = takeFlag(work, "--offset");
|
|
35
|
+
if (sub === "search" && !query) {
|
|
36
|
+
console.error("Usage: frontpage fonts search <query> [--slot …] [--variable-only] [--limit N]");
|
|
37
|
+
return EXIT_USAGE;
|
|
38
|
+
}
|
|
39
|
+
const q = new URLSearchParams();
|
|
40
|
+
if (slot)
|
|
41
|
+
q.set("slot", slot);
|
|
42
|
+
if (query)
|
|
43
|
+
q.set("query", query);
|
|
44
|
+
if (variableOnly)
|
|
45
|
+
q.set("variableOnly", "true");
|
|
46
|
+
if (limit)
|
|
47
|
+
q.set("limit", limit);
|
|
48
|
+
if (offset)
|
|
49
|
+
q.set("offset", offset);
|
|
50
|
+
const path = `/api/v1/fonts${q.toString() ? `?${q.toString()}` : ""}`;
|
|
51
|
+
const data = await v1Get(path);
|
|
52
|
+
if (isJson()) {
|
|
53
|
+
emitOk("fonts", data);
|
|
54
|
+
return EXIT_OK;
|
|
55
|
+
}
|
|
56
|
+
for (const f of data.fonts ?? []) {
|
|
57
|
+
const varMark = f.variable ? "var" : " ";
|
|
58
|
+
console.log(`${f.slot.padEnd(8)} ${varMark} ${f.family.padEnd(28)} ${f.value}`);
|
|
59
|
+
}
|
|
60
|
+
console.log(`(${data.returned}/${data.total} fonts${data.hasMore ? "; more available" : ""})`);
|
|
61
|
+
if (data.hint)
|
|
62
|
+
console.error(data.hint);
|
|
63
|
+
return EXIT_OK;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (isJson())
|
|
67
|
+
return emitErr("fonts", err);
|
|
68
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
69
|
+
return exitCodeForError(err);
|
|
70
|
+
}
|
|
71
|
+
}
|
package/dist/commands/modules.js
CHANGED
|
@@ -19,6 +19,51 @@ export async function runModules(args) {
|
|
|
19
19
|
}
|
|
20
20
|
return EXIT_OK;
|
|
21
21
|
}
|
|
22
|
+
if (sub === "docs" || sub === "doc") {
|
|
23
|
+
const key = rest[1];
|
|
24
|
+
if (!key) {
|
|
25
|
+
console.error("Usage: frontpage modules docs <key> [--instance <id>] [--site <slug>]");
|
|
26
|
+
return EXIT_USAGE;
|
|
27
|
+
}
|
|
28
|
+
const work = rest.slice(2);
|
|
29
|
+
const instanceId = takeFlag(work, "--instance");
|
|
30
|
+
const q = instanceId ? `?instanceId=${encodeURIComponent(instanceId)}` : "";
|
|
31
|
+
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/modules/${encodeURIComponent(key)}${q}`);
|
|
32
|
+
console.log(`# ${data.key} (${data.status})`);
|
|
33
|
+
if (data.note)
|
|
34
|
+
console.log(data.note);
|
|
35
|
+
console.log("");
|
|
36
|
+
console.log(data.docs);
|
|
37
|
+
return EXIT_OK;
|
|
38
|
+
}
|
|
39
|
+
if (sub === "request-setup" || sub === "request_setup") {
|
|
40
|
+
const key = rest[1];
|
|
41
|
+
if (!key) {
|
|
42
|
+
console.error("Usage: frontpage modules request-setup <key> --reason \"…\" [--prefill '{…}'] [--instance <id>] [--site <slug>]");
|
|
43
|
+
return EXIT_USAGE;
|
|
44
|
+
}
|
|
45
|
+
const work = rest.slice(2);
|
|
46
|
+
const instanceId = takeFlag(work, "--instance");
|
|
47
|
+
const reason = takeFlag(work, "--reason");
|
|
48
|
+
const prefillRaw = takeFlag(work, "--prefill");
|
|
49
|
+
if (!reason) {
|
|
50
|
+
console.error("Usage: frontpage modules request-setup <key> --reason \"…\" [--prefill '{…}']");
|
|
51
|
+
return EXIT_USAGE;
|
|
52
|
+
}
|
|
53
|
+
let prefill;
|
|
54
|
+
if (prefillRaw) {
|
|
55
|
+
try {
|
|
56
|
+
prefill = JSON.parse(prefillRaw);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
console.error("--prefill must be valid JSON");
|
|
60
|
+
return EXIT_USAGE;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/modules/${encodeURIComponent(key)}`, { action: "request-setup", reason, prefill, instanceId });
|
|
64
|
+
console.log(JSON.stringify(data, null, 2));
|
|
65
|
+
return EXIT_OK;
|
|
66
|
+
}
|
|
22
67
|
if (sub === "enable" || sub === "disable" || sub === "archive" || sub === "config") {
|
|
23
68
|
const key = rest[1];
|
|
24
69
|
if (!key) {
|
|
@@ -46,7 +91,7 @@ export async function runModules(args) {
|
|
|
46
91
|
console.log(JSON.stringify(data, null, 2));
|
|
47
92
|
return EXIT_OK;
|
|
48
93
|
}
|
|
49
|
-
console.error("Usage: frontpage modules [list|enable|disable|archive|config] …");
|
|
94
|
+
console.error("Usage: frontpage modules [list|docs|request-setup|enable|disable|archive|config] …");
|
|
50
95
|
return EXIT_USAGE;
|
|
51
96
|
}
|
|
52
97
|
catch (err) {
|
package/dist/commands/search.js
CHANGED
|
@@ -1,29 +1,79 @@
|
|
|
1
1
|
import { resolveSlug } from "../site.js";
|
|
2
2
|
import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Get } from "../http/client.js";
|
|
3
|
+
import { emitErr, emitOk, isJson } from "../runtime.js";
|
|
4
|
+
function takeFlag(rest, name) {
|
|
5
|
+
const i = rest.indexOf(name);
|
|
6
|
+
if (i === -1)
|
|
7
|
+
return undefined;
|
|
8
|
+
const v = rest[i + 1];
|
|
9
|
+
rest.splice(i, 2);
|
|
10
|
+
return v;
|
|
11
|
+
}
|
|
12
|
+
function hasFlag(rest, name) {
|
|
13
|
+
const i = rest.indexOf(name);
|
|
14
|
+
if (i === -1)
|
|
15
|
+
return false;
|
|
16
|
+
rest.splice(i, 1);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
3
19
|
export async function runSearch(args) {
|
|
4
20
|
try {
|
|
5
21
|
const { slug, rest } = await resolveSlug(args);
|
|
6
|
-
const
|
|
22
|
+
const work = [...rest];
|
|
23
|
+
const regex = hasFlag(work, "--regex");
|
|
24
|
+
const caseSensitive = hasFlag(work, "--case-sensitive");
|
|
25
|
+
const wholeWord = hasFlag(work, "--whole-word");
|
|
26
|
+
const filesOnly = hasFlag(work, "--files-only");
|
|
27
|
+
const contextLines = takeFlag(work, "--context-lines");
|
|
28
|
+
const maxResults = takeFlag(work, "--max-results");
|
|
29
|
+
const pattern = work[0];
|
|
7
30
|
if (!pattern) {
|
|
8
|
-
console.error("Usage: frontpage search <pattern> [--site <slug>]");
|
|
31
|
+
console.error("Usage: frontpage search <pattern> [path] [--regex] [--case-sensitive] [--whole-word] [--files-only] [--context-lines N] [--max-results N] [--site <slug>]");
|
|
9
32
|
return EXIT_USAGE;
|
|
10
33
|
}
|
|
34
|
+
const pathFilter = work[1] && !work[1].startsWith("-") ? work[1] : undefined;
|
|
11
35
|
const q = new URLSearchParams({ pattern });
|
|
12
|
-
const pathFilter = rest[1];
|
|
13
36
|
if (pathFilter)
|
|
14
37
|
q.set("path", pathFilter);
|
|
38
|
+
if (regex)
|
|
39
|
+
q.set("regex", "true");
|
|
40
|
+
if (caseSensitive)
|
|
41
|
+
q.set("caseSensitive", "true");
|
|
42
|
+
if (wholeWord)
|
|
43
|
+
q.set("wholeWord", "true");
|
|
44
|
+
if (filesOnly)
|
|
45
|
+
q.set("filesOnly", "true");
|
|
46
|
+
if (contextLines)
|
|
47
|
+
q.set("contextLines", contextLines);
|
|
48
|
+
if (maxResults)
|
|
49
|
+
q.set("maxResults", maxResults);
|
|
15
50
|
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/search?${q.toString()}`);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
51
|
+
if (isJson()) {
|
|
52
|
+
emitOk("search", data);
|
|
53
|
+
return EXIT_OK;
|
|
54
|
+
}
|
|
55
|
+
if (filesOnly || data.filesOnly) {
|
|
56
|
+
const files = data.files ?? [];
|
|
57
|
+
for (const f of files)
|
|
58
|
+
console.log(f);
|
|
59
|
+
if (files.length === 0)
|
|
60
|
+
console.error("(no matches)");
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const matches = data.matches ?? [];
|
|
64
|
+
for (const m of matches) {
|
|
65
|
+
console.log(`${m.file}:${m.lineNumber}: ${m.line}`);
|
|
66
|
+
}
|
|
67
|
+
if (matches.length === 0)
|
|
68
|
+
console.error("(no matches)");
|
|
19
69
|
}
|
|
20
|
-
if (matches.length === 0)
|
|
21
|
-
console.error("(no matches)");
|
|
22
70
|
if (data.truncated || data.scanTruncated)
|
|
23
71
|
console.error("# results truncated");
|
|
24
72
|
return EXIT_OK;
|
|
25
73
|
}
|
|
26
74
|
catch (err) {
|
|
75
|
+
if (isJson())
|
|
76
|
+
return emitErr("search", err);
|
|
27
77
|
console.error(err instanceof Error ? err.message : String(err));
|
|
28
78
|
return exitCodeForError(err);
|
|
29
79
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runTokens(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { resolveSlug } from "../site.js";
|
|
2
|
+
import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Post } from "../http/client.js";
|
|
3
|
+
import { readState, writeState } from "../state/frontpage-state.js";
|
|
4
|
+
import { emitErr, emitOk, emitUsage, isJson } from "../runtime.js";
|
|
5
|
+
function takeFlag(rest, name) {
|
|
6
|
+
const i = rest.indexOf(name);
|
|
7
|
+
if (i === -1)
|
|
8
|
+
return undefined;
|
|
9
|
+
const v = rest[i + 1];
|
|
10
|
+
rest.splice(i, 2);
|
|
11
|
+
return v;
|
|
12
|
+
}
|
|
13
|
+
export async function runTokens(args) {
|
|
14
|
+
try {
|
|
15
|
+
const { slug, rest } = await resolveSlug(args);
|
|
16
|
+
const sub = rest[0];
|
|
17
|
+
if (sub !== "set") {
|
|
18
|
+
console.error("Usage: frontpage tokens set --group <colors|fonts|spacing|radii|article> --key <name> --value <…> [--site <slug>]");
|
|
19
|
+
return EXIT_USAGE;
|
|
20
|
+
}
|
|
21
|
+
const work = rest.slice(1);
|
|
22
|
+
const group = takeFlag(work, "--group");
|
|
23
|
+
const key = takeFlag(work, "--key");
|
|
24
|
+
const value = takeFlag(work, "--value");
|
|
25
|
+
if (!group || !key || value === undefined) {
|
|
26
|
+
return emitUsage("tokens", "Usage: frontpage tokens set --group <…> --key <…> --value <…> [--site <slug>]", EXIT_USAGE);
|
|
27
|
+
}
|
|
28
|
+
const cwd = process.cwd();
|
|
29
|
+
const state = await readState(cwd);
|
|
30
|
+
if (!state.baseCommitSha) {
|
|
31
|
+
return emitUsage("tokens", "No baseCommitSha in .frontpage/state.json. Run: frontpage pull", EXIT_USAGE);
|
|
32
|
+
}
|
|
33
|
+
const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/tokens`, {
|
|
34
|
+
group,
|
|
35
|
+
key,
|
|
36
|
+
value,
|
|
37
|
+
baseCommitSha: state.baseCommitSha,
|
|
38
|
+
expectedHeadSha: state.baseCommitSha,
|
|
39
|
+
});
|
|
40
|
+
if (data.commitSha) {
|
|
41
|
+
await writeState({ ...state, slug, baseCommitSha: data.commitSha }, cwd);
|
|
42
|
+
}
|
|
43
|
+
if (isJson()) {
|
|
44
|
+
emitOk("tokens", { slug, ...data });
|
|
45
|
+
return EXIT_OK;
|
|
46
|
+
}
|
|
47
|
+
if (!data.changed) {
|
|
48
|
+
console.log(`No change (${group}.${key} already set).`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.log(`Set ${group}.${key} → ${data.commitSha}`);
|
|
52
|
+
console.log("Updated .frontpage/state.json baseCommitSha");
|
|
53
|
+
}
|
|
54
|
+
if (data.warning)
|
|
55
|
+
console.error(`Warning: ${data.warning}`);
|
|
56
|
+
return EXIT_OK;
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (isJson())
|
|
60
|
+
return emitErr("tokens", err);
|
|
61
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
62
|
+
return exitCodeForError(err);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -19,10 +19,13 @@ _frontpage_completions() {
|
|
|
19
19
|
runs) COMPREPLY=( $(compgen -W "list current wait cancel retry get" -- "\${cur}") ) ;;
|
|
20
20
|
deploy) COMPREPLY=( $(compgen -W "wait" -- "\${cur}") ) ;;
|
|
21
21
|
media) COMPREPLY=( $(compgen -W "list upload" -- "\${cur}") ) ;;
|
|
22
|
+
fonts) COMPREPLY=( $(compgen -W "list search" -- "\${cur}") ) ;;
|
|
23
|
+
tokens) COMPREPLY=( $(compgen -W "set" -- "\${cur}") ) ;;
|
|
22
24
|
seo|settings) COMPREPLY=( $(compgen -W "get set" -- "\${cur}") ) ;;
|
|
23
25
|
articles) COMPREPLY=( $(compgen -W "list content create-section create-article set-draft" -- "\${cur}") ) ;;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
analytics) COMPREPLY=( $(compgen -W "dashboard summary query funnel timeseries compare sql" -- "\${cur}") ) ;;
|
|
27
|
+
modules) COMPREPLY=( $(compgen -W "list docs request-setup enable disable archive config" -- "\${cur}") ) ;;
|
|
28
|
+
extensions) COMPREPLY=( $(compgen -W "list docs enable disable config" -- "\${cur}") ) ;;
|
|
26
29
|
*) COMPREPLY=( $(compgen -W "--site --help --json --quiet --yes" -- "\${cur}") ) ;;
|
|
27
30
|
esac
|
|
28
31
|
}
|
|
@@ -45,8 +48,12 @@ _frontpage() {
|
|
|
45
48
|
runs) _values 'subcommand' current wait cancel retry get ;;
|
|
46
49
|
deploy) _values 'subcommand' wait ;;
|
|
47
50
|
media) _values 'subcommand' list upload ;;
|
|
51
|
+
fonts) _values 'subcommand' list search ;;
|
|
52
|
+
tokens) _values 'subcommand' set ;;
|
|
48
53
|
seo|settings) _values 'subcommand' get set ;;
|
|
49
|
-
|
|
54
|
+
analytics) _values 'subcommand' dashboard summary query funnel timeseries compare sql ;;
|
|
55
|
+
modules) _values 'subcommand' list docs request-setup enable disable archive config ;;
|
|
56
|
+
extensions) _values 'subcommand' list docs enable disable config ;;
|
|
50
57
|
esac
|
|
51
58
|
;;
|
|
52
59
|
esac
|
|
@@ -63,8 +70,12 @@ export function generateFishCompletion() {
|
|
|
63
70
|
`complete -c frontpage -n "__fish_seen_subcommand_from runs" -a "current wait cancel retry get"`,
|
|
64
71
|
`complete -c frontpage -n "__fish_seen_subcommand_from deploy" -a "wait"`,
|
|
65
72
|
`complete -c frontpage -n "__fish_seen_subcommand_from media" -a "list upload"`,
|
|
73
|
+
`complete -c frontpage -n "__fish_seen_subcommand_from fonts" -a "list search"`,
|
|
74
|
+
`complete -c frontpage -n "__fish_seen_subcommand_from tokens" -a "set"`,
|
|
66
75
|
`complete -c frontpage -n "__fish_seen_subcommand_from seo settings" -a "get set"`,
|
|
67
|
-
`complete -c frontpage -n "__fish_seen_subcommand_from
|
|
76
|
+
`complete -c frontpage -n "__fish_seen_subcommand_from analytics" -a "dashboard summary query funnel timeseries compare sql"`,
|
|
77
|
+
`complete -c frontpage -n "__fish_seen_subcommand_from modules" -a "list docs request-setup enable disable archive config"`,
|
|
78
|
+
`complete -c frontpage -n "__fish_seen_subcommand_from extensions" -a "list docs enable disable config"`,
|
|
68
79
|
`complete -c frontpage -l site -d "Site subdomain slug"`,
|
|
69
80
|
];
|
|
70
81
|
return `${lines.join("\n")}\n`;
|
package/dist/registry.js
CHANGED
|
@@ -126,8 +126,47 @@ export const COMMANDS = [
|
|
|
126
126
|
{
|
|
127
127
|
name: "search",
|
|
128
128
|
summary: "Search remote text files",
|
|
129
|
-
usage: "frontpage search <pattern> [path] [--site <slug>]",
|
|
129
|
+
usage: "frontpage search <pattern> [path] [--regex] [--case-sensitive] [--whole-word] [--files-only] [--context-lines N] [--max-results N] [--site <slug>] [--json]",
|
|
130
130
|
topics: ["sync"],
|
|
131
|
+
flags: [
|
|
132
|
+
"--regex",
|
|
133
|
+
"--case-sensitive",
|
|
134
|
+
"--whole-word",
|
|
135
|
+
"--files-only",
|
|
136
|
+
"--context-lines <n>",
|
|
137
|
+
"--max-results <n>",
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "fonts",
|
|
142
|
+
summary: "Browse or search the self-hosted font catalog",
|
|
143
|
+
usage: "frontpage fonts [list|search] [--slot sans|serif|mono|display] [--query …] [--variable-only] [--limit N] [--offset N] [--json]",
|
|
144
|
+
topics: ["site-building", "content"],
|
|
145
|
+
examples: [
|
|
146
|
+
"frontpage fonts --slot serif",
|
|
147
|
+
'frontpage fonts search "elegant script"',
|
|
148
|
+
"frontpage fonts --query Cinzel --json",
|
|
149
|
+
],
|
|
150
|
+
flags: [
|
|
151
|
+
"Use the exact `value` string in tokens.fonts.* (via frontpage tokens set or push)",
|
|
152
|
+
"No --site required — catalog is account/platform scoped",
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: "tokens",
|
|
157
|
+
summary: "Set a single design token surgically (CAS + active-run)",
|
|
158
|
+
usage: "frontpage tokens set --group <colors|fonts|spacing|radii|article> --key <name> --value <…> [--site <slug>] [--json]",
|
|
159
|
+
topics: ["site-building", "content"],
|
|
160
|
+
examples: [
|
|
161
|
+
'frontpage tokens set --group fonts --key sans --value "\\"Inter Variable\\", system-ui, sans-serif"',
|
|
162
|
+
],
|
|
163
|
+
dangers: [
|
|
164
|
+
"Requires prior pull (baseCommitSha)",
|
|
165
|
+
"Refused while an agent run is active (exit 3)",
|
|
166
|
+
"CAS fail if remote tip moved (exit 3) — pull and retry",
|
|
167
|
+
"Off-catalog font values warn but do not block",
|
|
168
|
+
],
|
|
169
|
+
seeAlso: ["fonts", "pull", "push"],
|
|
131
170
|
},
|
|
132
171
|
{
|
|
133
172
|
name: "status",
|
|
@@ -207,9 +246,22 @@ export const COMMANDS = [
|
|
|
207
246
|
},
|
|
208
247
|
{
|
|
209
248
|
name: "analytics",
|
|
210
|
-
summary: "
|
|
211
|
-
usage: "frontpage analytics [
|
|
249
|
+
summary: "Analytics dashboard or structured query ops",
|
|
250
|
+
usage: "frontpage analytics [dashboard|summary|query|funnel|timeseries|compare|sql] … [--site <slug>] [--json]",
|
|
212
251
|
topics: ["agent"],
|
|
252
|
+
examples: [
|
|
253
|
+
"frontpage analytics --days 7",
|
|
254
|
+
"frontpage analytics summary --days 30",
|
|
255
|
+
"frontpage analytics query --dimension path --measure count --limit 10",
|
|
256
|
+
`frontpage analytics funnel --steps '[{"path":"/"},{"path":"/pricing"}]'`,
|
|
257
|
+
"frontpage analytics timeseries --metric pageviews --interval day --days 14",
|
|
258
|
+
`frontpage analytics compare --a '{"device":"mobile"}' --b '{"device":"desktop"}'`,
|
|
259
|
+
'frontpage analytics sql --sql "SELECT path, count(*)::int AS n FROM analytics_events GROUP BY 1 ORDER BY n DESC LIMIT 10"',
|
|
260
|
+
],
|
|
261
|
+
flags: [
|
|
262
|
+
"Bare analytics / dashboard = legacy GET dashboard (compat)",
|
|
263
|
+
"Ops use POST …/analytics with allowlisted dimensions/measures; SQL is guard-validated + RLS",
|
|
264
|
+
],
|
|
213
265
|
},
|
|
214
266
|
{
|
|
215
267
|
name: "pages",
|
|
@@ -219,16 +271,25 @@ export const COMMANDS = [
|
|
|
219
271
|
},
|
|
220
272
|
{
|
|
221
273
|
name: "modules",
|
|
222
|
-
summary: "List or mutate modules (secret masks only)",
|
|
223
|
-
usage: "frontpage modules [list|enable|disable|archive|config] <key> [--instance <id>] [--config '{…}'] [--site <slug>]",
|
|
274
|
+
summary: "List, read docs, or mutate modules (secret masks only)",
|
|
275
|
+
usage: "frontpage modules [list|docs|request-setup|enable|disable|archive|config] <key> [--instance <id>] [--config '{…}'] [--site <slug>]",
|
|
224
276
|
topics: ["agent", "content"],
|
|
225
|
-
|
|
277
|
+
examples: [
|
|
278
|
+
"frontpage modules docs forms",
|
|
279
|
+
"frontpage modules list",
|
|
280
|
+
'frontpage modules request-setup payments --reason "You asked for a $50 deposit button — Stripe setup makes it live"',
|
|
281
|
+
],
|
|
282
|
+
dangers: [
|
|
283
|
+
"Secret values are never returned — masks only",
|
|
284
|
+
"request-setup surfaces an owner prompt in the editor status bar (CLI cannot show it)",
|
|
285
|
+
],
|
|
226
286
|
},
|
|
227
287
|
{
|
|
228
288
|
name: "extensions",
|
|
229
|
-
summary: "List or mutate extensions (no secret values)",
|
|
230
|
-
usage: "frontpage extensions [list|enable|disable|config] <key> [--config '{…}'] [--site <slug>]",
|
|
289
|
+
summary: "List, read docs, or mutate extensions (no secret values)",
|
|
290
|
+
usage: "frontpage extensions [list|docs|enable|disable|config] <key> [--config '{…}'] [--site <slug>]",
|
|
231
291
|
topics: ["agent", "content"],
|
|
292
|
+
examples: ["frontpage extensions docs charts"],
|
|
232
293
|
},
|
|
233
294
|
{
|
|
234
295
|
name: "media",
|
|
@@ -352,7 +413,7 @@ export const TOPICS = {
|
|
|
352
413
|
frontpage push [--message …] # uploads allowlisted files; CAS + active-run guard
|
|
353
414
|
frontpage ls [prefix]
|
|
354
415
|
frontpage cat <path>
|
|
355
|
-
frontpage search <pattern>
|
|
416
|
+
frontpage search <pattern> [path] [--regex] [--case-sensitive] [--whole-word] [--files-only]
|
|
356
417
|
frontpage open # opens https://<slug>.frontpage.host/ (--prod is a no-op stub)
|
|
357
418
|
|
|
358
419
|
Local state: .frontpage/state.json { slug, baseCommitSha }
|
|
@@ -389,7 +450,8 @@ export const TOPICS = {
|
|
|
389
450
|
Tokens & fonts:
|
|
390
451
|
- Keep required colors/fonts/spacing/radii keys; style via CSS variables / token Tailwind.
|
|
391
452
|
- Setting fonts.sans is not enough — apply font-sans / font-serif / font-display on elements.
|
|
392
|
-
-
|
|
453
|
+
- Browse fonts: frontpage fonts [--slot serif] [--query elegant] — use the exact value string.
|
|
454
|
+
- Surgical token edit: frontpage tokens set --group fonts --key sans --value "…" (CAS like push).
|
|
393
455
|
|
|
394
456
|
Chrome / SEO / 404:
|
|
395
457
|
- Prefer src/components/Header.astro + Footer.astro (or chrome: { managed: true } in site.ts).
|
|
@@ -414,8 +476,10 @@ export const TOPICS = {
|
|
|
414
476
|
frontpage unpublish --yes # admin only; never unless asked; --yes when non-TTY
|
|
415
477
|
frontpage usage
|
|
416
478
|
frontpage analytics [--days 7]
|
|
479
|
+
frontpage analytics summary|query|funnel|timeseries|compare|sql …
|
|
417
480
|
frontpage pages
|
|
418
481
|
frontpage modules
|
|
482
|
+
frontpage modules request-setup <key> --reason "…"
|
|
419
483
|
frontpage extensions
|
|
420
484
|
|
|
421
485
|
Mid-task continuation is automatic — there is no runs continue.
|
|
@@ -426,8 +490,10 @@ export const TOPICS = {
|
|
|
426
490
|
frontpage inbox [leads|subscribers|orders|…] [--mark-read] [--csv]
|
|
427
491
|
frontpage seo get|set --path /
|
|
428
492
|
frontpage articles list|content|create-section|create-article|set-draft|…
|
|
429
|
-
frontpage
|
|
430
|
-
frontpage
|
|
493
|
+
frontpage fonts [list|search] [--slot …] [--query …]
|
|
494
|
+
frontpage tokens set --group … --key … --value …
|
|
495
|
+
frontpage modules list|docs|request-setup|enable|disable|archive|config <key>
|
|
496
|
+
frontpage extensions list|docs|enable|disable|config <key>
|
|
431
497
|
frontpage settings get|set
|
|
432
498
|
frontpage edit --path / --old "…" --new "…"
|
|
433
499
|
|
package/llms.txt
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
- Introspect: frontpage help | frontpage help --json | frontpage help <topic|command>
|
|
6
6
|
- Topics: quickstart, auth, sync, site-building, agent, content, history, isolation, exit-codes
|
|
7
7
|
- Site building: frontpage help site-building — Astro pages map, BaseLayout, tokens, no backends/fake forms
|
|
8
|
+
- Fonts: frontpage fonts [--slot] [--query] — use exact value; tokens: frontpage tokens set --group --key --value (CAS)
|
|
9
|
+
- Module/extension docs: frontpage modules docs <key> | frontpage extensions docs <key>
|
|
10
|
+
- Search flags: --regex --case-sensitive --whole-word --files-only --context-lines --max-results
|
|
11
|
+
- Modules setup: frontpage modules request-setup <key> --reason "…" (owner status bar; CLI cannot show it)
|
|
12
|
+
- Analytics ops: frontpage analytics summary|query|funnel|timeseries|compare|sql (bare analytics = dashboard)
|
|
8
13
|
- Auth: mint fp_live_ key in Frontpage UI (Developers); frontpage login KEY or FRONTPAGE_API_KEY
|
|
9
14
|
- Site id: subdomain only (frontpage use <subdomain>). Not custom domains.
|
|
10
15
|
- Staging: https://<subdomain>.frontpage.host — push/prompt/edit update staging; no separate staging deploy step for agents
|