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 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`. No CLI font catalog yet; use existing tokens or `frontpage prompt`.
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
- let days = "7";
7
- const daysIdx = rest.indexOf("--days");
8
- if (daysIdx >= 0 && rest[daysIdx + 1])
9
- days = rest[daysIdx + 1];
10
- const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/analytics?days=${encodeURIComponent(days)}`);
11
- console.log(JSON.stringify(data.dashboard, null, 2));
12
- return EXIT_OK;
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
+ }
@@ -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) {
@@ -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 pattern = rest[0];
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
- const matches = data.matches ?? [];
17
- for (const m of matches) {
18
- console.log(`${m.file}:${m.lineNumber}: ${m.line}`);
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
- modules) COMPREPLY=( $(compgen -W "list enable disable archive config" -- "\${cur}") ) ;;
25
- extensions) COMPREPLY=( $(compgen -W "list enable disable config" -- "\${cur}") ) ;;
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
- modules|extensions) _values 'subcommand' list enable disable config ;;
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 modules extensions" -a "list enable disable config"`,
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: "Show analytics dashboard JSON",
211
- usage: "frontpage analytics [--days 7] [--site <slug>]",
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
- dangers: ["Secret values are never returned — masks only"],
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
- - No frontpage fonts catalog command yet; read tokens.ts or frontpage prompt.
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 modules enable|disable|archive|config <key>
430
- frontpage extensions enable|disable|config <key>
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontpage-host",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Frontpage CLI — manage your site from the terminal",
5
5
  "type": "module",
6
6
  "bin": {