frontpage-host 0.1.17 → 0.1.18
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 +12 -8
- package/dist/bin-path.d.ts +21 -0
- package/dist/bin-path.js +65 -0
- package/dist/bin.js +6 -0
- package/dist/commands/people.d.ts +1 -0
- package/dist/commands/people.js +239 -0
- package/dist/commands/publish.js +14 -10
- package/dist/commands/push.d.ts +5 -0
- package/dist/commands/push.js +13 -2
- package/dist/commands/redirects.d.ts +1 -0
- package/dist/commands/redirects.js +83 -0
- package/dist/commands/wait-site-compile.d.ts +1 -0
- package/dist/commands/wait-site-compile.js +14 -10
- package/dist/compile.d.ts +5 -0
- package/dist/compile.js +30 -2
- package/dist/completions/generate.js +3 -0
- package/dist/registry.js +51 -3
- package/llms.txt +3 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -93,10 +93,11 @@ Rules:
|
|
|
93
93
|
2. **Pull before push** so `baseCommitSha` is set (CAS). Still required even if you already pulled to edit.
|
|
94
94
|
3. **Do not push** while an agent run is active (`status` / `runs`).
|
|
95
95
|
4. Push updates **staging** (subdomain). Production is unchanged until someone runs `publish`.
|
|
96
|
-
5.
|
|
97
|
-
6.
|
|
98
|
-
7.
|
|
99
|
-
8.
|
|
96
|
+
5. A local delete of an allowlisted path is part of the next push (remote-only files are sent as deletes). `frontpage diff` shows them as `D`.
|
|
97
|
+
6. Pushing module markup may auto-activate modules (same as the editor agent).
|
|
98
|
+
7. Binaries use base64; do not UTF-8 round-trip them.
|
|
99
|
+
8. After push, wait for the site compile (`frontpage push --wait` or `frontpage deploy wait`) before reviewing or publishing. Verify `<meta name="frontpage:commit">` when checking production.
|
|
100
|
+
9. The **UI / canvas agent does not use this CLI** — it edits the live tip in-process. Only CLI checkouts need `pull`.
|
|
100
101
|
|
|
101
102
|
## Site contract (Astro)
|
|
102
103
|
|
|
@@ -106,10 +107,12 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
|
|
|
106
107
|
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.
|
|
107
108
|
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.
|
|
108
109
|
4. **No backends / databases / fake forms or checkout.** Real forms use `<form data-fp-form="slug">` + `frontpage modules`. Refuse or use modules for payments.
|
|
109
|
-
5. **
|
|
110
|
-
6. **
|
|
111
|
-
7. **
|
|
112
|
-
8. **
|
|
110
|
+
5. **Quoted attributes vs frontmatter.** In frontmatter JavaScript, a backslash-quote is an escape. In an Astro attribute it is not — the quote after `href=` closes the string and the next token fails the compile. Put HTML in a frontmatter const and pass it as an expression (`auditBody={auditBody}`), or use an expression attribute. Never put an HTML tag or a backslash-quote inside a quoted JSX attribute.
|
|
111
|
+
6. **Tokens** in `src/config/tokens.ts` — keep required color/font/spacing/radii keys; style via CSS variables / token Tailwind. **Fonts load rule:** the build bundles catalog faces when the family string appears in `tokens.ts` and/or in a real CSS `font-family` / `fontFamily` / `font-['…']` context under `src/pages|components|styles` — not from prose alone. Preferred path: `frontpage fonts` → exact `value` → `frontpage tokens set --group fonts --key <role> --value "…"` → **APPLY** `font-sans` / `font-serif` / `font-mono` / `font-display` (or `var(--font-*)`) on elements. Inline catalog `font-family` accents can bundle after rebuild, but brand roles still prefer tokens + utilities. Off-catalog families silently fall back to system fonts (push may warn). Module how-tos: `frontpage modules docs <key>` / `frontpage extensions docs <key>`.
|
|
112
|
+
7. **Shared chrome** — prefer `src/components/Header.astro` + `Footer.astro` (or `chrome: { managed: true }` in `site.ts`).
|
|
113
|
+
8. **404** — customize `src/pages/404.astro` if needed; keep `frontpage:404` meta; never register `/404` in `nav`/`pages`.
|
|
114
|
+
9. **Redirects** — real 301/308s are `frontpage redirects list|set|rm`, not site files. `set --from /old --to /new` is instant (no push/build). Do not ship `Astro.redirect`, `location.replace`, or meta refresh for a permanent move. `frontpage revert` does not undo redirects; `redirects rm` does.
|
|
115
|
+
10. **Platform-owned paths** stay off-limits (BaseLayout overlay, module/extension shells, `package.json`, …). Customer layouts like `DocsLayout` / `GuideLayout` are OK.
|
|
113
116
|
|
|
114
117
|
## Modules & articles
|
|
115
118
|
|
|
@@ -122,6 +125,7 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
|
|
|
122
125
|
- Media: `frontpage media upload <file>` — images/PDF ≤25 MB, video (MP4/WebM/MOV) ≤50 MB, SVG/text ≤4 MB (sanitizing proxy). Human output prints `sitePath:` first (use that in markup — never raw blob hosts), then optional `url:`. Optional `?w=` on rasters only. **Text/CSV** uploads are inline only — **not** added to the media library and **cannot** be linked via `/__media` (`media upload --json` omits full `content`; use `inbox … --csv` for submission exports).
|
|
123
126
|
- 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.
|
|
124
127
|
- Analytics: bare `frontpage analytics` = dashboard; also `summary|query|funnel|timeseries|compare|sql` for structured reads (SQL is guard + RLS site-scoped).
|
|
128
|
+
- People: `frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide`. Site-scoped person graph (email or phone). Inbox stays the raw ledger. Hide is not forget.
|
|
125
129
|
|
|
126
130
|
## Agent prompt loop
|
|
127
131
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type PathFrontpageVerdict = {
|
|
2
|
+
stale: boolean;
|
|
3
|
+
path: string | null;
|
|
4
|
+
pathVersion: string | null;
|
|
5
|
+
reason: "older-than-compile-wait" | "older-than-this-package" | "unreadable" | null;
|
|
6
|
+
};
|
|
7
|
+
export declare function resolveRealPath(path: string): string;
|
|
8
|
+
export declare function interpretPathFrontpage(args: {
|
|
9
|
+
thisVersion: string;
|
|
10
|
+
thisBin: string | null;
|
|
11
|
+
pathBin: string | null;
|
|
12
|
+
pathVersionLine: string | null;
|
|
13
|
+
minVersion?: string;
|
|
14
|
+
}): PathFrontpageVerdict;
|
|
15
|
+
export declare function whichFrontpage(): string | null;
|
|
16
|
+
export declare function frontpageVersionAt(bin: string): string | null;
|
|
17
|
+
export declare function inspectPathFrontpage(opts?: {
|
|
18
|
+
thisBin?: string | null;
|
|
19
|
+
thisVersion?: string;
|
|
20
|
+
}): PathFrontpageVerdict;
|
|
21
|
+
export declare function stalePathUpgradeMessage(v: PathFrontpageVerdict): string;
|
package/dist/bin-path.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect a stale `frontpage` on PATH (global 0.1.9 vs this package).
|
|
3
|
+
* Does not uninstall anything — doctor prints the uninstall command.
|
|
4
|
+
*/
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { realpathSync } from "node:fs";
|
|
7
|
+
import { COMPILE_WAIT_MIN_VERSION, isOlderThan, packageIdentity, parseVersionLine, } from "./version.js";
|
|
8
|
+
export function resolveRealPath(path) {
|
|
9
|
+
try {
|
|
10
|
+
return realpathSync(path);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return path;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function interpretPathFrontpage(args) {
|
|
17
|
+
if (!args.pathBin) {
|
|
18
|
+
return { stale: false, path: null, pathVersion: null, reason: null };
|
|
19
|
+
}
|
|
20
|
+
if (args.thisBin && resolveRealPath(args.pathBin) === resolveRealPath(args.thisBin)) {
|
|
21
|
+
return { stale: false, path: args.pathBin, pathVersion: args.thisVersion, reason: null };
|
|
22
|
+
}
|
|
23
|
+
const pathVersion = parseVersionLine(args.pathVersionLine ?? "");
|
|
24
|
+
if (!pathVersion) {
|
|
25
|
+
return { stale: true, path: args.pathBin, pathVersion: null, reason: "unreadable" };
|
|
26
|
+
}
|
|
27
|
+
const floor = args.minVersion ?? COMPILE_WAIT_MIN_VERSION;
|
|
28
|
+
if (isOlderThan(pathVersion, floor)) {
|
|
29
|
+
return { stale: true, path: args.pathBin, pathVersion, reason: "older-than-compile-wait" };
|
|
30
|
+
}
|
|
31
|
+
if (isOlderThan(pathVersion, args.thisVersion)) {
|
|
32
|
+
return { stale: true, path: args.pathBin, pathVersion, reason: "older-than-this-package" };
|
|
33
|
+
}
|
|
34
|
+
return { stale: false, path: args.pathBin, pathVersion, reason: null };
|
|
35
|
+
}
|
|
36
|
+
export function whichFrontpage() {
|
|
37
|
+
const r = spawnSync("sh", ["-c", "command -v frontpage"], { encoding: "utf8" });
|
|
38
|
+
const line = (r.stdout ?? "").trim().split("\n")[0]?.trim();
|
|
39
|
+
return line || null;
|
|
40
|
+
}
|
|
41
|
+
export function frontpageVersionAt(bin) {
|
|
42
|
+
const r = spawnSync(bin, ["--version"], { encoding: "utf8" });
|
|
43
|
+
const text = `${r.stdout ?? ""}\n${r.stderr ?? ""}`;
|
|
44
|
+
return parseVersionLine(text);
|
|
45
|
+
}
|
|
46
|
+
export function inspectPathFrontpage(opts) {
|
|
47
|
+
const thisVersion = opts?.thisVersion ?? packageIdentity().version;
|
|
48
|
+
const thisBin = opts?.thisBin ?? process.argv[1] ?? null;
|
|
49
|
+
const pathBin = whichFrontpage();
|
|
50
|
+
const pathVersionLine = pathBin ? frontpageVersionAt(pathBin) : null;
|
|
51
|
+
return interpretPathFrontpage({
|
|
52
|
+
thisVersion,
|
|
53
|
+
thisBin,
|
|
54
|
+
pathBin,
|
|
55
|
+
pathVersionLine,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function stalePathUpgradeMessage(v) {
|
|
59
|
+
const found = v.pathVersion ? `frontpage-host ${v.pathVersion}` : "an unknown frontpage-host";
|
|
60
|
+
const where = v.path ? ` at ${v.path}` : "";
|
|
61
|
+
return (`PATH frontpage is ${found}${where}. Compile-wait needs frontpage-host ${COMPILE_WAIT_MIN_VERSION} or newer. ` +
|
|
62
|
+
`Uninstall the stale global (npm uninstall -g frontpage-host) and install ` +
|
|
63
|
+
`npm i -g frontpage-host@${COMPILE_WAIT_MIN_VERSION} (or npx frontpage-host@${COMPILE_WAIT_MIN_VERSION}). ` +
|
|
64
|
+
`In the Frontpage repo, run node <repo>/cli/dist/bin.js instead of a global binary.`);
|
|
65
|
+
}
|
package/dist/bin.js
CHANGED
|
@@ -27,6 +27,8 @@ import { runMedia } from "./commands/media.js";
|
|
|
27
27
|
import { runImages } from "./commands/images.js";
|
|
28
28
|
import { runInbox } from "./commands/inbox.js";
|
|
29
29
|
import { runSeo } from "./commands/seo.js";
|
|
30
|
+
import { runRedirects } from "./commands/redirects.js";
|
|
31
|
+
import { runPeople } from "./commands/people.js";
|
|
30
32
|
import { runArticles } from "./commands/articles.js";
|
|
31
33
|
import { runSettings } from "./commands/settings.js";
|
|
32
34
|
import { runEdit } from "./commands/edit.js";
|
|
@@ -112,8 +114,12 @@ async function main() {
|
|
|
112
114
|
return runImages(args);
|
|
113
115
|
case "inbox":
|
|
114
116
|
return runInbox(args);
|
|
117
|
+
case "people":
|
|
118
|
+
return runPeople(args);
|
|
115
119
|
case "seo":
|
|
116
120
|
return runSeo(args);
|
|
121
|
+
case "redirects":
|
|
122
|
+
return runRedirects(args);
|
|
117
123
|
case "articles":
|
|
118
124
|
return runArticles(args);
|
|
119
125
|
case "settings":
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runPeople(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { resolveSlug } from "../site.js";
|
|
2
|
+
import { CliHttpError, EXIT_OK, EXIT_USAGE, exitCodeForError, v1Delete, v1Get, v1Patch, v1Post, } from "../http/client.js";
|
|
3
|
+
import { assertNoUnknownFlags, hasFlag, takeFlag } from "../argv.js";
|
|
4
|
+
import { emitErr, emitOk, isJson } from "../runtime.js";
|
|
5
|
+
const USAGE = "Usage: frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide [--site <slug>] [--json]";
|
|
6
|
+
function requireId(work) {
|
|
7
|
+
const fromFlag = takeFlag(work, "--id");
|
|
8
|
+
if (fromFlag)
|
|
9
|
+
return fromFlag;
|
|
10
|
+
const positional = work.find((a) => !a.startsWith("-"));
|
|
11
|
+
if (positional) {
|
|
12
|
+
work.splice(work.indexOf(positional), 1);
|
|
13
|
+
return positional;
|
|
14
|
+
}
|
|
15
|
+
throw new CliHttpError(`Person id is required. Run: frontpage help people`, EXIT_USAGE, 0, "bad_request");
|
|
16
|
+
}
|
|
17
|
+
function displayName(row) {
|
|
18
|
+
return row.name || row.email || row.phone || row.id;
|
|
19
|
+
}
|
|
20
|
+
function printPerson(row) {
|
|
21
|
+
const bits = [row.status ?? "—", displayName(row)];
|
|
22
|
+
if (row.email && row.email !== row.name)
|
|
23
|
+
bits.push(row.email);
|
|
24
|
+
if (row.phone && row.phone !== row.name)
|
|
25
|
+
bits.push(row.phone);
|
|
26
|
+
if (row.followUpAt)
|
|
27
|
+
bits.push(`follow-up ${row.followUpAt}`);
|
|
28
|
+
if (row.hiddenAt)
|
|
29
|
+
bits.push("hidden");
|
|
30
|
+
if (row.lastEvent?.summary)
|
|
31
|
+
bits.push(row.lastEvent.summary);
|
|
32
|
+
console.log(`${row.id} ${bits.join(" · ")}`);
|
|
33
|
+
}
|
|
34
|
+
export async function runPeople(args) {
|
|
35
|
+
try {
|
|
36
|
+
const { slug, rest } = await resolveSlug(args);
|
|
37
|
+
const sub = rest[0];
|
|
38
|
+
const base = `/api/v1/sites/${encodeURIComponent(slug)}/people`;
|
|
39
|
+
if (!sub || sub === "list") {
|
|
40
|
+
const work = sub === "list" ? rest.slice(1) : [...rest];
|
|
41
|
+
const q = takeFlag(work, "--q") ?? takeFlag(work, "--query");
|
|
42
|
+
const status = takeFlag(work, "--status");
|
|
43
|
+
const source = takeFlag(work, "--source");
|
|
44
|
+
const formSlug = takeFlag(work, "--form-slug");
|
|
45
|
+
const followUp = takeFlag(work, "--follow-up");
|
|
46
|
+
const sort = takeFlag(work, "--sort");
|
|
47
|
+
const lastSeenDays = takeFlag(work, "--last-seen-days");
|
|
48
|
+
const limit = takeFlag(work, "--limit");
|
|
49
|
+
const cursor = takeFlag(work, "--cursor");
|
|
50
|
+
const hidden = hasFlag(work, "--hidden");
|
|
51
|
+
assertNoUnknownFlags(work, "people");
|
|
52
|
+
const params = new URLSearchParams();
|
|
53
|
+
if (q)
|
|
54
|
+
params.set("q", q);
|
|
55
|
+
if (status)
|
|
56
|
+
params.set("status", status);
|
|
57
|
+
if (source)
|
|
58
|
+
params.set("source", source);
|
|
59
|
+
if (formSlug)
|
|
60
|
+
params.set("formSlug", formSlug);
|
|
61
|
+
if (followUp)
|
|
62
|
+
params.set("followUp", followUp);
|
|
63
|
+
if (sort)
|
|
64
|
+
params.set("sort", sort);
|
|
65
|
+
if (lastSeenDays)
|
|
66
|
+
params.set("lastSeenDays", lastSeenDays);
|
|
67
|
+
if (limit)
|
|
68
|
+
params.set("limit", limit);
|
|
69
|
+
if (cursor)
|
|
70
|
+
params.set("cursor", cursor);
|
|
71
|
+
if (hidden)
|
|
72
|
+
params.set("hidden", "1");
|
|
73
|
+
const qs = params.toString();
|
|
74
|
+
const data = await v1Get(qs ? `${base}?${qs}` : base);
|
|
75
|
+
if (isJson()) {
|
|
76
|
+
emitOk("people", data);
|
|
77
|
+
return EXIT_OK;
|
|
78
|
+
}
|
|
79
|
+
const rows = data.people ?? [];
|
|
80
|
+
if (rows.length === 0) {
|
|
81
|
+
console.log(hidden ? "No hidden people." : "No people.");
|
|
82
|
+
return EXIT_OK;
|
|
83
|
+
}
|
|
84
|
+
for (const row of rows)
|
|
85
|
+
printPerson(row);
|
|
86
|
+
if (data.nextCursor)
|
|
87
|
+
console.log(`next: ${data.nextCursor}`);
|
|
88
|
+
return EXIT_OK;
|
|
89
|
+
}
|
|
90
|
+
if (sub === "get") {
|
|
91
|
+
const work = rest.slice(1);
|
|
92
|
+
const id = requireId(work);
|
|
93
|
+
assertNoUnknownFlags(work, "people");
|
|
94
|
+
const data = await v1Get(`${base}/${encodeURIComponent(id)}`);
|
|
95
|
+
if (isJson()) {
|
|
96
|
+
emitOk("people", data);
|
|
97
|
+
return EXIT_OK;
|
|
98
|
+
}
|
|
99
|
+
printPerson(data.person);
|
|
100
|
+
const events = data.events ?? [];
|
|
101
|
+
for (const ev of events) {
|
|
102
|
+
const row = ev;
|
|
103
|
+
const extra = typeof row.payload?.body === "string" ? ` ${row.payload.body}` : "";
|
|
104
|
+
console.log(` ${row.occurredAt ?? ""} ${row.type ?? ""}${extra}`);
|
|
105
|
+
}
|
|
106
|
+
return EXIT_OK;
|
|
107
|
+
}
|
|
108
|
+
if (sub === "add") {
|
|
109
|
+
const work = rest.slice(1);
|
|
110
|
+
const email = takeFlag(work, "--email");
|
|
111
|
+
const phone = takeFlag(work, "--phone");
|
|
112
|
+
const name = takeFlag(work, "--name");
|
|
113
|
+
const note = takeFlag(work, "--note");
|
|
114
|
+
assertNoUnknownFlags(work, "people");
|
|
115
|
+
if (!email && !phone) {
|
|
116
|
+
console.error("Usage: frontpage people add --email you@example.com | --phone +15551234567 [--name …] [--note …]");
|
|
117
|
+
return EXIT_USAGE;
|
|
118
|
+
}
|
|
119
|
+
const data = await v1Post(base, {
|
|
120
|
+
email,
|
|
121
|
+
phone,
|
|
122
|
+
name,
|
|
123
|
+
note,
|
|
124
|
+
});
|
|
125
|
+
if (isJson()) {
|
|
126
|
+
emitOk("people", data);
|
|
127
|
+
return EXIT_OK;
|
|
128
|
+
}
|
|
129
|
+
printPerson(data.person);
|
|
130
|
+
return EXIT_OK;
|
|
131
|
+
}
|
|
132
|
+
if (sub === "set") {
|
|
133
|
+
const work = rest.slice(1);
|
|
134
|
+
const id = requireId(work);
|
|
135
|
+
const body = {};
|
|
136
|
+
for (const field of ["name", "email", "phone", "notes", "status"]) {
|
|
137
|
+
const v = takeFlag(work, `--${field}`);
|
|
138
|
+
if (v !== undefined)
|
|
139
|
+
body[field] = v === "" ? null : v;
|
|
140
|
+
}
|
|
141
|
+
assertNoUnknownFlags(work, "people");
|
|
142
|
+
if (Object.keys(body).length === 0) {
|
|
143
|
+
console.error("Usage: frontpage people set <id> [--name …] [--email …] [--phone …] [--notes …] [--status …]");
|
|
144
|
+
return EXIT_USAGE;
|
|
145
|
+
}
|
|
146
|
+
const data = await v1Patch(`${base}/${encodeURIComponent(id)}`, body);
|
|
147
|
+
if (isJson()) {
|
|
148
|
+
emitOk("people", data);
|
|
149
|
+
return EXIT_OK;
|
|
150
|
+
}
|
|
151
|
+
printPerson(data.person);
|
|
152
|
+
return EXIT_OK;
|
|
153
|
+
}
|
|
154
|
+
if (sub === "note") {
|
|
155
|
+
const work = rest.slice(1);
|
|
156
|
+
const id = requireId(work);
|
|
157
|
+
const body = takeFlag(work, "--body") ?? takeFlag(work, "--note");
|
|
158
|
+
assertNoUnknownFlags(work, "people");
|
|
159
|
+
if (!body) {
|
|
160
|
+
console.error("Usage: frontpage people note <id> --body \"…\"");
|
|
161
|
+
return EXIT_USAGE;
|
|
162
|
+
}
|
|
163
|
+
const data = await v1Post(`${base}/${encodeURIComponent(id)}/events`, { body });
|
|
164
|
+
if (isJson()) {
|
|
165
|
+
emitOk("people", data);
|
|
166
|
+
return EXIT_OK;
|
|
167
|
+
}
|
|
168
|
+
console.log(`Noted ${id} (${data.event.id})`);
|
|
169
|
+
return EXIT_OK;
|
|
170
|
+
}
|
|
171
|
+
if (sub === "rm-note") {
|
|
172
|
+
const work = rest.slice(1);
|
|
173
|
+
const id = requireId(work);
|
|
174
|
+
const eventId = takeFlag(work, "--event") ?? takeFlag(work, "--event-id");
|
|
175
|
+
assertNoUnknownFlags(work, "people");
|
|
176
|
+
if (!eventId) {
|
|
177
|
+
console.error("Usage: frontpage people rm-note <id> --event <eventId>");
|
|
178
|
+
return EXIT_USAGE;
|
|
179
|
+
}
|
|
180
|
+
const data = await v1Delete(`${base}/${encodeURIComponent(id)}/events/${encodeURIComponent(eventId)}`);
|
|
181
|
+
if (isJson()) {
|
|
182
|
+
emitOk("people", data);
|
|
183
|
+
return EXIT_OK;
|
|
184
|
+
}
|
|
185
|
+
console.log(`Removed note ${data.removed.id}`);
|
|
186
|
+
return EXIT_OK;
|
|
187
|
+
}
|
|
188
|
+
if (sub === "follow-up") {
|
|
189
|
+
const work = rest.slice(1);
|
|
190
|
+
const id = requireId(work);
|
|
191
|
+
const clear = hasFlag(work, "--clear");
|
|
192
|
+
const at = takeFlag(work, "--at");
|
|
193
|
+
assertNoUnknownFlags(work, "people");
|
|
194
|
+
if (!clear && !at) {
|
|
195
|
+
console.error("Usage: frontpage people follow-up <id> --at <iso> | --clear");
|
|
196
|
+
return EXIT_USAGE;
|
|
197
|
+
}
|
|
198
|
+
const data = await v1Patch(`${base}/${encodeURIComponent(id)}/follow-up`, { at: clear ? null : at });
|
|
199
|
+
if (isJson()) {
|
|
200
|
+
emitOk("people", data);
|
|
201
|
+
return EXIT_OK;
|
|
202
|
+
}
|
|
203
|
+
printPerson(data.person);
|
|
204
|
+
return EXIT_OK;
|
|
205
|
+
}
|
|
206
|
+
if (sub === "hide") {
|
|
207
|
+
const work = rest.slice(1);
|
|
208
|
+
const id = requireId(work);
|
|
209
|
+
assertNoUnknownFlags(work, "people");
|
|
210
|
+
const data = await v1Post(`${base}/${encodeURIComponent(id)}/hide`, {});
|
|
211
|
+
if (isJson()) {
|
|
212
|
+
emitOk("people", data);
|
|
213
|
+
return EXIT_OK;
|
|
214
|
+
}
|
|
215
|
+
console.log(`Hidden ${displayName(data.person)}`);
|
|
216
|
+
return EXIT_OK;
|
|
217
|
+
}
|
|
218
|
+
if (sub === "unhide") {
|
|
219
|
+
const work = rest.slice(1);
|
|
220
|
+
const id = requireId(work);
|
|
221
|
+
assertNoUnknownFlags(work, "people");
|
|
222
|
+
const data = await v1Post(`${base}/${encodeURIComponent(id)}/unhide`, {});
|
|
223
|
+
if (isJson()) {
|
|
224
|
+
emitOk("people", data);
|
|
225
|
+
return EXIT_OK;
|
|
226
|
+
}
|
|
227
|
+
console.log(`Restored ${displayName(data.person)}`);
|
|
228
|
+
return EXIT_OK;
|
|
229
|
+
}
|
|
230
|
+
console.error(USAGE);
|
|
231
|
+
return EXIT_USAGE;
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (isJson())
|
|
235
|
+
return emitErr("people", err);
|
|
236
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
237
|
+
return exitCodeForError(err);
|
|
238
|
+
}
|
|
239
|
+
}
|
package/dist/commands/publish.js
CHANGED
|
@@ -48,11 +48,13 @@ export async function runPublish(args) {
|
|
|
48
48
|
progress("Waiting for the site compile…\n");
|
|
49
49
|
const compile = await waitForCompileReady({ slug });
|
|
50
50
|
if (compile.exitCode !== EXIT_OK) {
|
|
51
|
-
const message = compile.
|
|
52
|
-
? "
|
|
53
|
-
: compile.
|
|
54
|
-
? "
|
|
55
|
-
:
|
|
51
|
+
const message = compile.apiMissing
|
|
52
|
+
? "Compile status API is missing. Refusing to publish."
|
|
53
|
+
: compile.failed
|
|
54
|
+
? "Site compile failed. Refusing to publish a previous build."
|
|
55
|
+
: compile.timedOut
|
|
56
|
+
? "Timed out waiting for the site compile. Refusing to publish a previous build."
|
|
57
|
+
: "Site compile is not ready. Refusing to publish a previous build.";
|
|
56
58
|
return emitFail("publish", {
|
|
57
59
|
message,
|
|
58
60
|
exitCode: compile.exitCode,
|
|
@@ -61,11 +63,13 @@ export async function runPublish(args) {
|
|
|
61
63
|
compile: compile.status,
|
|
62
64
|
tip: compile.status?.stagingCommit ?? null,
|
|
63
65
|
live: compile.status?.liveCommit ?? null,
|
|
64
|
-
status: compile.
|
|
65
|
-
? "
|
|
66
|
-
: compile.
|
|
67
|
-
? "
|
|
68
|
-
:
|
|
66
|
+
status: compile.apiMissing
|
|
67
|
+
? "compile_api_missing"
|
|
68
|
+
: compile.failed
|
|
69
|
+
? "compile_failed"
|
|
70
|
+
: compile.timedOut
|
|
71
|
+
? "compile_timeout"
|
|
72
|
+
: "compile_not_ready",
|
|
69
73
|
},
|
|
70
74
|
});
|
|
71
75
|
}
|
package/dist/commands/push.d.ts
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
|
+
/** Remote allowlisted paths that are gone locally become explicit deletes. */
|
|
2
|
+
export declare function pushDeletesForMissingLocal(localPaths: Iterable<string>, remotePaths: Iterable<string>): Array<{
|
|
3
|
+
path: string;
|
|
4
|
+
delete: true;
|
|
5
|
+
}>;
|
|
1
6
|
export declare function runPush(args: string[]): Promise<number>;
|
package/dist/commands/push.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
2
|
import { join, relative, sep } from "node:path";
|
|
3
3
|
import { resolveSlug } from "../site.js";
|
|
4
|
-
import { EXIT_OK, EXIT_USAGE, v1Put } from "../http/client.js";
|
|
4
|
+
import { EXIT_OK, EXIT_USAGE, v1Get, v1Put } from "../http/client.js";
|
|
5
5
|
import { readState, writeState } from "../state/frontpage-state.js";
|
|
6
6
|
import { emitErr, emitOk, emitUsage, isJson } from "../runtime.js";
|
|
7
7
|
import { waitForAgentThenCompile } from "./wait-site-compile.js";
|
|
@@ -26,6 +26,14 @@ const TEXT_EXTS = new Set([
|
|
|
26
26
|
".toml",
|
|
27
27
|
]);
|
|
28
28
|
const ROOTS = ["src", "public", "content"];
|
|
29
|
+
/** Remote allowlisted paths that are gone locally become explicit deletes. */
|
|
30
|
+
export function pushDeletesForMissingLocal(localPaths, remotePaths) {
|
|
31
|
+
const local = new Set(localPaths);
|
|
32
|
+
return [...remotePaths]
|
|
33
|
+
.filter((path) => path && !local.has(path))
|
|
34
|
+
.sort()
|
|
35
|
+
.map((path) => ({ path, delete: true }));
|
|
36
|
+
}
|
|
29
37
|
async function walkAllowlisted(cwd) {
|
|
30
38
|
const out = [];
|
|
31
39
|
async function walk(abs) {
|
|
@@ -85,7 +93,10 @@ export async function runPush(args) {
|
|
|
85
93
|
if (paths.length === 0) {
|
|
86
94
|
return emitUsage("push", "No allowlisted files (src/, public/, content/) found in this folder.", EXIT_USAGE);
|
|
87
95
|
}
|
|
88
|
-
const
|
|
96
|
+
const listing = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/files`);
|
|
97
|
+
const files = [
|
|
98
|
+
...pushDeletesForMissingLocal(paths, listing.files.map((f) => f.path)),
|
|
99
|
+
];
|
|
89
100
|
for (const path of paths) {
|
|
90
101
|
const abs = join(cwd, path);
|
|
91
102
|
const st = await stat(abs);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runRedirects(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { resolveSlug } from "../site.js";
|
|
2
|
+
import { CliHttpError, EXIT_OK, EXIT_USAGE, exitCodeForError, v1Delete, v1Get, v1Put } from "../http/client.js";
|
|
3
|
+
import { assertNoUnknownFlags, takeFlag } from "../argv.js";
|
|
4
|
+
import { emitErr, emitOk, isJson } from "../runtime.js";
|
|
5
|
+
const USAGE = "Usage: frontpage redirects list | set --from /old --to /new [--status 301|308] | rm --from /old [--site <slug>] [--json]";
|
|
6
|
+
function requirePathFlag(work, flag) {
|
|
7
|
+
const value = takeFlag(work, flag);
|
|
8
|
+
if (!value) {
|
|
9
|
+
throw new CliHttpError(`${flag} is required (e.g. ${flag} /old).`, EXIT_USAGE, 0, "bad_request");
|
|
10
|
+
}
|
|
11
|
+
if (!value.startsWith("/") || value.startsWith("//")) {
|
|
12
|
+
throw new CliHttpError(`${flag} must be a pathname starting with /.`, EXIT_USAGE, 0, "bad_request");
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
export async function runRedirects(args) {
|
|
17
|
+
try {
|
|
18
|
+
const { slug, rest } = await resolveSlug(args);
|
|
19
|
+
const sub = rest[0];
|
|
20
|
+
if (!sub || sub === "list") {
|
|
21
|
+
const work = sub === "list" ? rest.slice(1) : [...rest];
|
|
22
|
+
assertNoUnknownFlags(work, "redirects");
|
|
23
|
+
const data = await v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/redirects`);
|
|
24
|
+
if (isJson()) {
|
|
25
|
+
emitOk("redirects", data);
|
|
26
|
+
return EXIT_OK;
|
|
27
|
+
}
|
|
28
|
+
const rows = data.redirects ?? [];
|
|
29
|
+
if (rows.length === 0) {
|
|
30
|
+
console.log("No redirects.");
|
|
31
|
+
return EXIT_OK;
|
|
32
|
+
}
|
|
33
|
+
for (const row of rows) {
|
|
34
|
+
console.log(`${row.from} → ${row.to} (${row.status})`);
|
|
35
|
+
}
|
|
36
|
+
return EXIT_OK;
|
|
37
|
+
}
|
|
38
|
+
if (sub === "set") {
|
|
39
|
+
const work = rest.slice(1);
|
|
40
|
+
const from = requirePathFlag(work, "--from");
|
|
41
|
+
const to = requirePathFlag(work, "--to");
|
|
42
|
+
const statusRaw = takeFlag(work, "--status");
|
|
43
|
+
assertNoUnknownFlags(work, "redirects");
|
|
44
|
+
const body = { from, to };
|
|
45
|
+
if (statusRaw !== undefined) {
|
|
46
|
+
const status = Number(statusRaw);
|
|
47
|
+
if (status !== 301 && status !== 308) {
|
|
48
|
+
console.error("Status must be 301 or 308.");
|
|
49
|
+
return EXIT_USAGE;
|
|
50
|
+
}
|
|
51
|
+
body.status = status;
|
|
52
|
+
}
|
|
53
|
+
const data = await v1Put(`/api/v1/sites/${encodeURIComponent(slug)}/redirects`, body);
|
|
54
|
+
if (isJson()) {
|
|
55
|
+
emitOk("redirects", data);
|
|
56
|
+
return EXIT_OK;
|
|
57
|
+
}
|
|
58
|
+
const row = data.redirect;
|
|
59
|
+
console.log(`${row.from} → ${row.to} (${row.status})`);
|
|
60
|
+
return EXIT_OK;
|
|
61
|
+
}
|
|
62
|
+
if (sub === "rm" || sub === "remove") {
|
|
63
|
+
const work = rest.slice(1);
|
|
64
|
+
const from = requirePathFlag(work, "--from");
|
|
65
|
+
assertNoUnknownFlags(work, "redirects");
|
|
66
|
+
const data = await v1Delete(`/api/v1/sites/${encodeURIComponent(slug)}/redirects?from=${encodeURIComponent(from)}`);
|
|
67
|
+
if (isJson()) {
|
|
68
|
+
emitOk("redirects", data);
|
|
69
|
+
return EXIT_OK;
|
|
70
|
+
}
|
|
71
|
+
console.log(`Removed ${data.removed.from}`);
|
|
72
|
+
return EXIT_OK;
|
|
73
|
+
}
|
|
74
|
+
console.error(USAGE);
|
|
75
|
+
return EXIT_USAGE;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
if (isJson())
|
|
79
|
+
return emitErr("redirects", err);
|
|
80
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
81
|
+
return exitCodeForError(err);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -45,11 +45,13 @@ export function emitCompileWaitOutcome(args) {
|
|
|
45
45
|
compileReady: Boolean(args.compile.status?.ready) && !args.compile.timedOut,
|
|
46
46
|
};
|
|
47
47
|
if (args.compile.exitCode !== EXIT_OK) {
|
|
48
|
-
const message = args.compile.
|
|
49
|
-
? "
|
|
50
|
-
: args.compile.
|
|
51
|
-
? "
|
|
52
|
-
:
|
|
48
|
+
const message = args.compile.apiMissing
|
|
49
|
+
? "Compile status API is missing. Cannot confirm the tip is live."
|
|
50
|
+
: args.compile.failed
|
|
51
|
+
? "Site compile failed. The git tip is not live."
|
|
52
|
+
: args.compile.timedOut
|
|
53
|
+
? "Timed out waiting for the site compile. The git tip is not live yet."
|
|
54
|
+
: "Site compile is not ready.";
|
|
53
55
|
if (isJson()) {
|
|
54
56
|
return emitFail(args.command, {
|
|
55
57
|
message,
|
|
@@ -58,11 +60,13 @@ export function emitCompileWaitOutcome(args) {
|
|
|
58
60
|
slug: args.slug,
|
|
59
61
|
waited: true,
|
|
60
62
|
observedBusy: args.agent.observedBusy,
|
|
61
|
-
status: args.compile.
|
|
62
|
-
? "
|
|
63
|
-
: args.compile.
|
|
64
|
-
? "
|
|
65
|
-
:
|
|
63
|
+
status: args.compile.apiMissing
|
|
64
|
+
? "compile_api_missing"
|
|
65
|
+
: args.compile.failed
|
|
66
|
+
? "compile_failed"
|
|
67
|
+
: args.compile.timedOut
|
|
68
|
+
? "compile_timeout"
|
|
69
|
+
: "compile_not_ready",
|
|
66
70
|
run: args.agent.run,
|
|
67
71
|
...compileData,
|
|
68
72
|
...args.extra,
|
package/dist/compile.d.ts
CHANGED
|
@@ -20,8 +20,13 @@ export type CompileWaitResult = {
|
|
|
20
20
|
exitCode: number;
|
|
21
21
|
timedOut: boolean;
|
|
22
22
|
failed: boolean;
|
|
23
|
+
apiMissing?: boolean;
|
|
23
24
|
status: DeployStatus | null;
|
|
24
25
|
};
|
|
26
|
+
/** Compile clock is gone (flag off / old API). A site-not-found 404 is not this. */
|
|
27
|
+
export declare function isMissingDeployApi(err: unknown): boolean;
|
|
28
|
+
/** A deploy payload we can poll. Missing `ready` means the compile clock is not there. */
|
|
29
|
+
export declare function deployStatusLooksComplete(status: DeployStatus | null | undefined): boolean;
|
|
25
30
|
export declare function fetchDeployStatus(slug: string): Promise<DeployStatus>;
|
|
26
31
|
export declare function waitForCompileReady(args: {
|
|
27
32
|
slug: string;
|
package/dist/compile.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* GET /api/v1/sites/{slug}/deploy → ready when the live compile SHA equals the tip.
|
|
5
5
|
*/
|
|
6
|
-
import { EXIT_OK, EXIT_USAGE, v1Get } from "./http/client.js";
|
|
6
|
+
import { CliHttpError, EXIT_OK, EXIT_USAGE, v1Get } from "./http/client.js";
|
|
7
7
|
import { progress } from "./runtime.js";
|
|
8
8
|
const SHA_RE = /^[0-9a-f]{40}$/i;
|
|
9
9
|
export function normalizeSha(raw) {
|
|
@@ -32,6 +32,21 @@ export function compileIsReady(status, expectCommit) {
|
|
|
32
32
|
return true;
|
|
33
33
|
return live === expect;
|
|
34
34
|
}
|
|
35
|
+
/** Compile clock is gone (flag off / old API). A site-not-found 404 is not this. */
|
|
36
|
+
export function isMissingDeployApi(err) {
|
|
37
|
+
if (!(err instanceof CliHttpError))
|
|
38
|
+
return false;
|
|
39
|
+
const message = err.message.toLowerCase();
|
|
40
|
+
if (message.includes("api not available"))
|
|
41
|
+
return true;
|
|
42
|
+
if (err.status === 404 && (message === "not found." || message === "not found"))
|
|
43
|
+
return true;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
/** A deploy payload we can poll. Missing `ready` means the compile clock is not there. */
|
|
47
|
+
export function deployStatusLooksComplete(status) {
|
|
48
|
+
return Boolean(status && typeof status.ready === "boolean");
|
|
49
|
+
}
|
|
35
50
|
export async function fetchDeployStatus(slug) {
|
|
36
51
|
return v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/deploy`);
|
|
37
52
|
}
|
|
@@ -42,7 +57,20 @@ export async function waitForCompileReady(args) {
|
|
|
42
57
|
const started = Date.now();
|
|
43
58
|
let last = null;
|
|
44
59
|
while (Date.now() - started < maxMs) {
|
|
45
|
-
|
|
60
|
+
try {
|
|
61
|
+
last = await fetchStatus(args.slug);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (isMissingDeployApi(err)) {
|
|
65
|
+
progress("\n");
|
|
66
|
+
return { exitCode: EXIT_USAGE, timedOut: false, failed: true, apiMissing: true, status: null };
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
if (!deployStatusLooksComplete(last)) {
|
|
71
|
+
progress("\n");
|
|
72
|
+
return { exitCode: EXIT_USAGE, timedOut: false, failed: true, apiMissing: true, status: last };
|
|
73
|
+
}
|
|
46
74
|
if (last.compileState === "failed" || last.compileState === "canceled") {
|
|
47
75
|
progress("\n");
|
|
48
76
|
return { exitCode: EXIT_USAGE, timedOut: false, failed: true, status: last };
|
|
@@ -25,6 +25,7 @@ _frontpage_completions() {
|
|
|
25
25
|
seo|settings) COMPREPLY=( $(compgen -W "get set" -- "\${cur}") ) ;;
|
|
26
26
|
articles) COMPREPLY=( $(compgen -W "list content create-section create-article set-draft" -- "\${cur}") ) ;;
|
|
27
27
|
analytics) COMPREPLY=( $(compgen -W "dashboard summary query funnel timeseries compare sql" -- "\${cur}") ) ;;
|
|
28
|
+
people) COMPREPLY=( $(compgen -W "list get add set note rm-note follow-up hide unhide" -- "\${cur}") ) ;;
|
|
28
29
|
modules)
|
|
29
30
|
if [[ \${COMP_CWORD} -ge 3 && \${COMP_WORDS[2]} == google_reviews ]]; then
|
|
30
31
|
COMPREPLY=( $(compgen -W "docs request-setup enable disable archive config connect resolve status list-reviews hide show set-stars set-sort set-weight reset-weights write-review-url refresh disconnect" -- "\${cur}") )
|
|
@@ -69,6 +70,7 @@ _frontpage() {
|
|
|
69
70
|
tokens) _values 'subcommand' set ;;
|
|
70
71
|
seo|settings) _values 'subcommand' get set ;;
|
|
71
72
|
analytics) _values 'subcommand' dashboard summary query funnel timeseries compare sql ;;
|
|
73
|
+
people) _values 'subcommand' list get add set note rm-note follow-up hide unhide ;;
|
|
72
74
|
modules)
|
|
73
75
|
if [[ $words[2] == google_reviews ]]; then
|
|
74
76
|
_values 'subcommand' docs request-setup enable disable archive config connect resolve status list-reviews hide show set-stars set-sort set-weight reset-weights write-review-url refresh disconnect
|
|
@@ -106,6 +108,7 @@ export function generateFishCompletion() {
|
|
|
106
108
|
`complete -c frontpage -n "__fish_seen_subcommand_from tokens" -a "set"`,
|
|
107
109
|
`complete -c frontpage -n "__fish_seen_subcommand_from seo settings" -a "get set"`,
|
|
108
110
|
`complete -c frontpage -n "__fish_seen_subcommand_from analytics" -a "dashboard summary query funnel timeseries compare sql"`,
|
|
111
|
+
`complete -c frontpage -n "__fish_seen_subcommand_from people" -a "list get add set note rm-note follow-up hide unhide"`,
|
|
109
112
|
`complete -c frontpage -n "__fish_seen_subcommand_from modules" -a "list docs request-setup enable disable archive config callbar forms booking signups payments shop ordering articles google_reviews"`,
|
|
110
113
|
`complete -c frontpage -n "__fish_seen_subcommand_from modules; and __fish_seen_subcommand_from google_reviews" -a "connect resolve status list-reviews hide show set-stars set-sort set-weight reset-weights write-review-url refresh disconnect"`,
|
|
111
114
|
`complete -c frontpage -n "__fish_seen_subcommand_from modules; and __fish_seen_subcommand_from forms" -a "get set destinations connect-destination remove-destination disconnect-destination attach-destination detach-destination request-destination-setup"`,
|
package/dist/registry.js
CHANGED
|
@@ -413,6 +413,45 @@ export const COMMANDS = [
|
|
|
413
413
|
topics: ["content"],
|
|
414
414
|
dangers: ["--mark-read is side-effecting with no confirm"],
|
|
415
415
|
},
|
|
416
|
+
{
|
|
417
|
+
name: "people",
|
|
418
|
+
summary: "List and manage site People (not inbox, not a module)",
|
|
419
|
+
usage: "frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide [--site <slug>] [--json]",
|
|
420
|
+
topics: ["content"],
|
|
421
|
+
examples: [
|
|
422
|
+
"frontpage people list",
|
|
423
|
+
"frontpage people add --email alex@example.com --name Alex",
|
|
424
|
+
"frontpage people set <id> --status working",
|
|
425
|
+
"frontpage people note <id> --body \"Called them\"",
|
|
426
|
+
"frontpage people follow-up <id> --at 2026-09-12T00:00:00Z",
|
|
427
|
+
"frontpage people hide <id>",
|
|
428
|
+
],
|
|
429
|
+
flags: [
|
|
430
|
+
"list: --q --status --source --form-slug --follow-up due|upcoming|none --hidden --sort --last-seen-days --limit --cursor",
|
|
431
|
+
"add requires --email and/or --phone (never name alone)",
|
|
432
|
+
"Hide is not forget — unique email/phone stay; later ingest un-hides",
|
|
433
|
+
"Unknown leftover flags fail closed (exit 2)",
|
|
434
|
+
],
|
|
435
|
+
seeAlso: ["inbox", "help content"],
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: "redirects",
|
|
439
|
+
summary: "List, set, or remove HTTP 301/308 path redirects",
|
|
440
|
+
usage: "frontpage redirects list | set --from /old --to /new [--status 301|308] | rm --from /old [--site <slug>] [--json]",
|
|
441
|
+
topics: ["content", "site-building"],
|
|
442
|
+
examples: [
|
|
443
|
+
"frontpage redirects list",
|
|
444
|
+
"frontpage redirects set --from /agents --to /agent",
|
|
445
|
+
"frontpage redirects set --from /old --to /new --status 308",
|
|
446
|
+
"frontpage redirects rm --from /old",
|
|
447
|
+
],
|
|
448
|
+
flags: [
|
|
449
|
+
"Instant — no push or compile. Not a site file. frontpage revert does not undo a redirect.",
|
|
450
|
+
"Same-origin pathnames only. Status 301 (default) or 308. Cap 50 per site.",
|
|
451
|
+
"Unknown leftover flags fail closed (exit 2)",
|
|
452
|
+
],
|
|
453
|
+
seeAlso: ["seo", "help site-building"],
|
|
454
|
+
},
|
|
416
455
|
{
|
|
417
456
|
name: "seo",
|
|
418
457
|
summary: "Get or set page SEO",
|
|
@@ -502,7 +541,7 @@ export const TOPICS = {
|
|
|
502
541
|
4. mkdir work && cd work && frontpage use <subdomain>
|
|
503
542
|
5. frontpage pull
|
|
504
543
|
6. Edit files under src/, public/, or content/
|
|
505
|
-
7. frontpage push --message "…"
|
|
544
|
+
7. frontpage push --message "…" --wait
|
|
506
545
|
8. frontpage deploy wait # tip compile ready (optional; push --wait does this)
|
|
507
546
|
Review staging: https://<subdomain>.frontpage.host when that subdomain is publicly shareable.
|
|
508
547
|
With a custom domain connected, the subdomain is often owner-private (login wall ≠ failed push);
|
|
@@ -528,7 +567,7 @@ export const TOPICS = {
|
|
|
528
567
|
frontpage status # tipDrift when remote moved since last pull
|
|
529
568
|
frontpage pull # downloads src/, public/, content/ into cwd
|
|
530
569
|
frontpage diff # local vs remote via git blobSha (--json → dirty + tipDrift)
|
|
531
|
-
frontpage push [--message …] [--wait] # uploads allowlisted files; CAS + active-run guard
|
|
570
|
+
frontpage push [--message …] [--wait] # uploads allowlisted files; deletes remote-only allowlisted paths; CAS + active-run guard
|
|
532
571
|
frontpage ls [prefix]
|
|
533
572
|
frontpage cat <path>
|
|
534
573
|
frontpage search <pattern> [path|--path <path>] [--regex] [--case-sensitive] [--whole-word] [--files-only]
|
|
@@ -566,6 +605,10 @@ export const TOPICS = {
|
|
|
566
605
|
Hard fences:
|
|
567
606
|
- No API routes, serverless backends, or databases.
|
|
568
607
|
- No fake lead forms / checkout / card fields — use data-fp-* modules + frontpage modules, or refuse.
|
|
608
|
+
- Quoted attributes: in frontmatter JS, a backslash-quote is an escape. In an Astro attribute it is
|
|
609
|
+
not. The quote after href= closes the string and the next token fails the compile. Put HTML in a
|
|
610
|
+
frontmatter const and pass it as an expression (auditBody={auditBody}), or use an expression
|
|
611
|
+
attribute. Never put an HTML tag or a backslash-quote inside a quoted JSX attribute.
|
|
569
612
|
- Platform-owned paths are not syncable (BaseLayout overlay, module/extension shells, package.json…).
|
|
570
613
|
- Customer layouts (DocsLayout, GuideLayout, …) are fine when site-owned. A wrapper MUST render
|
|
571
614
|
<BaseLayout> and forward SEO: description={description}, ogImage={ogImage}, and
|
|
@@ -584,6 +627,8 @@ export const TOPICS = {
|
|
|
584
627
|
- Prefer src/components/Header.astro + Footer.astro (or chrome: { managed: true } in site.ts).
|
|
585
628
|
- seo block in site.ts + frontpage seo get|set; customize 404.astro but keep frontpage:404 meta;
|
|
586
629
|
never put /404 in nav or pages. robots/sitemap are platform-managed.
|
|
630
|
+
- Real HTTP 301/308s: frontpage redirects list|set|rm. Instant (no push). Do not ship Astro.redirect,
|
|
631
|
+
location.replace, or a meta refresh for a permanent move. frontpage revert does not undo redirects.
|
|
587
632
|
|
|
588
633
|
Modules & articles (short):
|
|
589
634
|
- Forms: <form data-fp-form="slug"> with named inputs; no action/onsubmit/honeypot.
|
|
@@ -623,7 +668,9 @@ export const TOPICS = {
|
|
|
623
668
|
frontpage media list|upload <file> # images/PDF/video; text/CSV = inline not library; sitePath first when stored
|
|
624
669
|
frontpage images generate --prompt "…" [--count N] # 1–4 AI variations of one prompt; sitePath first; AI $ + storage
|
|
625
670
|
frontpage inbox [leads|subscribers|orders|google_reviews|…] [--mark-read] [--csv]
|
|
671
|
+
frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide
|
|
626
672
|
frontpage seo get [/path|--path /path] | seo set --path /
|
|
673
|
+
frontpage redirects list|set|rm --from /old --to /new
|
|
627
674
|
frontpage articles list|content|create-section|create-article|set-draft|…
|
|
628
675
|
frontpage fonts [list|search] [--slot …] [--query …]
|
|
629
676
|
frontpage tokens set --group … --key … --value …
|
|
@@ -652,7 +699,8 @@ export const TOPICS = {
|
|
|
652
699
|
revert creates a NEW commit whose tree matches the target (linear restore).
|
|
653
700
|
Target SHA must be on this site's history (not another site in the monorepo).
|
|
654
701
|
Refused while an agent run is active. Goes through commitSiteBranch (build gate).
|
|
655
|
-
--yes required when stdin is not a TTY
|
|
702
|
+
--yes required when stdin is not a TTY.
|
|
703
|
+
revert does not undo HTTP redirects — use frontpage redirects rm.`,
|
|
656
704
|
isolation: `Isolation
|
|
657
705
|
Every site-scoped call targets subdomain → branch site/<slug> only.
|
|
658
706
|
No client-supplied branch, ref, repo, or clone URL is accepted.
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Frontpage CLI (frontpage-host)
|
|
2
2
|
|
|
3
3
|
- Package: frontpage-host (npm). Binary: frontpage. Do NOT install npm package "frontpage".
|
|
4
|
+
- Astro quoted attributes: in frontmatter JS a backslash-quote is an escape; in an attribute it is not. Pass HTML as {auditBody}. Never HTML or a backslash-quote inside a quoted JSX attribute.
|
|
4
5
|
- Docs in this package: README.md, AGENTS.md (agent operating manual).
|
|
5
6
|
- Introspect: frontpage help | frontpage help --json | frontpage help <topic|command>
|
|
6
7
|
- Topics: quickstart, auth, sync, site-building, agent, content, history, isolation, exit-codes
|
|
@@ -14,6 +15,8 @@
|
|
|
14
15
|
- Prompt attachments: frontpage prompt "…" --attach <file|/__media/…|https://…> [--asset-url https://…] (same media caps as media upload)
|
|
15
16
|
- Version: frontpage --version | -V | version
|
|
16
17
|
- SEO get: frontpage seo get [/path|--path /path]; search path filter: [path|--path <path>]; open [/path|--path /path]
|
|
18
|
+
- Redirects: frontpage redirects list|set|rm — HTTP 301/308, instant, not a file; revert does not undo; do not fake with Astro.redirect / meta refresh
|
|
19
|
+
- People: frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide — site-scoped person graph (email or phone). Inbox stays the raw ledger. Hide is not forget. Unknown leftover flags fail closed.
|
|
17
20
|
- Analytics funnel: --steps JSON array or comma paths (/, /pricing); bad JSON → Invalid JSON for --steps
|
|
18
21
|
- Modules: unknown key errors list Known: …; mailing list module key is signups (not subscribe)
|
|
19
22
|
- Errors: --json API envelopes include requestId when the server returned one; local validation omits requestId/httpStatus; runs wait failed/canceled → ok:false + run.error (exit 2); deploy wait = agent idle + tip compile ready (fail-closed / compile timeout → exit 2); push --wait and publish --wait wait for GET /deploy ready
|