frontpage-host 0.1.16 → 0.1.17
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 +19 -12
- package/README.md +3 -3
- package/dist/bin.js +3 -0
- package/dist/commands/deploy.d.ts +1 -1
- package/dist/commands/deploy.js +4 -5
- package/dist/commands/images.d.ts +1 -0
- package/dist/commands/images.js +57 -0
- package/dist/commands/modules.js +4 -0
- package/dist/commands/publish.js +69 -11
- package/dist/commands/push.js +5 -11
- package/dist/commands/wait-agent-idle.d.ts +12 -0
- package/dist/commands/wait-agent-idle.js +23 -5
- package/dist/commands/wait-site-compile.d.ts +21 -0
- package/dist/commands/wait-site-compile.js +94 -0
- package/dist/compile.d.ts +41 -0
- package/dist/compile.js +71 -0
- package/dist/completions/generate.js +11 -3
- package/dist/modules/commands.js +24 -1
- package/dist/modules/forms_destinations.d.ts +3 -0
- package/dist/modules/forms_destinations.js +147 -0
- package/dist/modules/typed_config.js +21 -9
- package/dist/registry.js +52 -19
- package/llms.txt +5 -5
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -26,17 +26,22 @@ Frontpage has two surfaces:
|
|
|
26
26
|
| **Staging** | `https://<subdomain>.frontpage.host` | Working copy for edits, previews, and review |
|
|
27
27
|
| **Production** | Custom domain (when connected), after **publish** | What end customers see on the live domain |
|
|
28
28
|
|
|
29
|
-
**
|
|
29
|
+
**Three clocks (do not collapse these)**
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
1. **Git tip** — `frontpage push` / `prompt` / `edit` land a new SHA. Fast.
|
|
32
|
+
2. **Site compile** — Fly (or Vercel on legacy hosts) builds that SHA. This is what visitors get. Poll `GET /api/v1/sites/<slug>/deploy` until `ready` is true (`liveCommit` equals the tip).
|
|
33
|
+
3. **Editor agent run** — a chat job (`queued` / `running` / `deploying`). `frontpage runs wait` watches this clock only.
|
|
34
|
+
|
|
35
|
+
- After the compile is ready, staging is `https://<subdomain>.frontpage.host`.
|
|
36
|
+
- There is **no separate “deploy to staging” step**. `frontpage deploy wait` waits for agent-run idle (if a run is in flight) **and** for the tip compile. It is not go-live.
|
|
37
|
+
- A file-only push has **no** agent run. Do not treat a historical run that says “Your changes are live” as this push.
|
|
34
38
|
- `frontpage open` opens the subdomain staging URL.
|
|
35
39
|
- **Custom-domain caveat:** `<subdomain>.frontpage.host` is publicly viewable (anonymous) only when the site has **no** custom domain and subdomain sharing is public. If a custom domain is connected (e.g. `arbiter.wiki`), the subdomain is often **owner-private** — an unauthenticated browser/curl may see the app login page. That login wall is **not** a failed push. Anonymous public review is then on the **published custom domain**; editing the subdomain still needs an owner browser session (CLI API key ≠ browser cookie).
|
|
36
40
|
|
|
37
41
|
**Production publish is a deliberate promote**
|
|
38
42
|
|
|
39
|
-
- `frontpage publish` copies the
|
|
43
|
+
- `frontpage publish` copies the last **ready staging compile** to **production** (custom domain / published site). It 409s if the tip is not compiled yet. Use `frontpage publish --wait` (or `deploy wait` then `publish`) so you do not promote the previous build.
|
|
44
|
+
- After publish, compare `<meta name="frontpage:commit">` on the custom domain to the push SHA. History’s `P` marker is the stamped tip; it is only trustworthy when it matches the SHA in `productionUrl`.
|
|
40
45
|
- **Never run `publish` or `unpublish` unless the user explicitly asked**, or the user’s own automation/workflow clearly requires autonomous publishing.
|
|
41
46
|
- Default: finish work on staging, tell the user to review the staging subdomain when it is publicly shareable (see custom-domain caveat above), and let **them** publish (or ask you to publish after they approve).
|
|
42
47
|
|
|
@@ -90,14 +95,14 @@ Rules:
|
|
|
90
95
|
4. Push updates **staging** (subdomain). Production is unchanged until someone runs `publish`.
|
|
91
96
|
5. Pushing module markup may auto-activate modules (same as the editor agent).
|
|
92
97
|
6. Binaries use base64; do not UTF-8 round-trip them.
|
|
93
|
-
7.
|
|
98
|
+
7. 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.
|
|
94
99
|
8. The **UI / canvas agent does not use this CLI** — it edits the live tip in-process. Only CLI checkouts need `pull`.
|
|
95
100
|
|
|
96
101
|
## Site contract (Astro)
|
|
97
102
|
|
|
98
103
|
Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or https://frontpage.host/developers/site-building
|
|
99
104
|
|
|
100
|
-
1. **Every page** wraps content in `<BaseLayout title="…" description="…">` (import from `src/layouts/BaseLayout.astro`).
|
|
105
|
+
1. **Every page** wraps content in `<BaseLayout title="…" description="…">` (import from `src/layouts/BaseLayout.astro`), or in a site layout that itself wraps `BaseLayout`. A custom wrapper MUST identity-forward `description={description}` and `ogImage={ogImage}` from a 1:1 `Astro.props` destructure (no default, no rename, no reassignment), and `title={seoTitle ?? <derived fallback>}` so the H1 can stay on `title` while Search & Social edits `seoTitle`. NEVER pass a derived local as `title={pageTitle}` with no `seoTitle` alias: the editor will lock Title and Share image on every page that uses that layout. Pages set literal `title` / `description` / optional `seoTitle` / `ogImage` on that tag.
|
|
101
106
|
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.
|
|
102
107
|
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.
|
|
103
108
|
4. **No backends / databases / fake forms or checkout.** Real forms use `<form data-fp-form="slug">` + `frontpage modules`. Refuse or use modules for payments.
|
|
@@ -108,13 +113,14 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
|
|
|
108
113
|
|
|
109
114
|
## Modules & articles
|
|
110
115
|
|
|
111
|
-
- Forms: `data-fp-form` markup in pages + `frontpage modules` for enable/config. No submit JS / honeypot / `action`.
|
|
112
|
-
- Wizard / connect-once modules (payments/shop/ordering): `frontpage modules <key> connect` opens Frontpage (you must be logged in there), then Stripe or Square. Already-connected sites stay live — do not retry connect. payments/shop connect needs an **admin** key; ordering connect needs **editor**. `--no-open` for CI. `request-setup` remains valid when you want the **editor amber bar** instead. payments and shop share one Stripe account — payments disconnect fails if the store still uses it. Auto-active modules (forms/callbar/…) need markup, not request-setup. Typed settings: `frontpage modules callbar|forms|booking|signups get|set` (forms set needs `--instance`; do not target `default`). `frontpage modules <key>` prints that key's CLI usage (exit 2). Index: https://frontpage.host/developers/modules · payments/shop/ordering pages under /developers/modules/<key>
|
|
116
|
+
- Forms: `data-fp-form` markup in pages + `frontpage modules` for enable/config. No submit JS / honeypot / `action`. Do not put a CRM webhook or token in HTML.
|
|
117
|
+
- Wizard / connect-once modules (payments/shop/ordering): `frontpage modules <key> connect` opens Frontpage (you must be logged in there), then Stripe or Square. Already-connected sites stay live — do not retry connect. payments/shop connect needs an **admin** key; ordering connect needs **editor**. `--no-open` for CI. `request-setup` remains valid when you want the **editor amber bar** instead. payments and shop share one Stripe account — payments disconnect fails if the store still uses it. Auto-active modules (forms/callbar/…) need markup, not request-setup. Typed settings: `frontpage modules callbar|forms|booking|signups get|set` (forms set needs `--instance`; do not target `default`). Form destinations (HubSpot / Pipedrive / Privyr): connect once, then attach each form. `frontpage modules forms destinations|connect-destination|attach-destination|detach-destination|remove-destination --yes|request-destination-setup`. Token write is **admin**; `request-destination-setup` lights the editor amber bar (**editor** key). Do not use `request-setup forms` for a CRM (no-op). Connecting does not attach every form. Privyr: paste the Incoming Webhook URL from Automations → Lead Sources → Webhooks (`https://www.privyr.com/api/v1/incoming-leads/…`), not a token and not Outgoing Webhooks. HubSpot: Development → Keys → Service keys or Development → Legacy apps → Auth → Show token. Pipedrive: profile → Company settings → Personal preferences → API. Full docs: https://frontpage.host/developers/modules#forms-destinations. `frontpage modules <key>` prints that key's CLI usage (exit 2). Index: https://frontpage.host/developers/modules · payments/shop/ordering pages under /developers/modules/<key>
|
|
113
118
|
- Google reviews: platform nodes only (`data-fp-google-reviews` / `data-fp-google-ask` + required `data-fp-requires="google_reviews"`). `data-layout` is `feed` (default), `badge`, or `link`. Never invent stars, quotes, a 1–10, or a Maps URL. Do not pick a Place ID via Autocomplete and do not write `placeId` through `modules config`. Owner-supplied Place ID / Maps URL: `frontpage modules google_reviews connect --place-id …` or `--maps-url …` (server resolves + seeds). Curation: `list-reviews`, `hide --id`, `show --id`, `set-stars`, `set-sort`, `set-weight`, `reset-weights`, `refresh`, `disconnect`. Usage: `frontpage modules google_reviews`. Docs: https://frontpage.host/developers/modules/google-reviews. Ask-widget scores: `frontpage inbox google_reviews`.
|
|
114
119
|
- 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`.
|
|
115
120
|
- Migrate a folder into Articles: prefer `frontpage prompt "migrate … into articles"` — do not invent a mass delete+write migration.
|
|
116
121
|
- Extensions: `frontpage extensions`; if LOCKED on plan, do not fake charts/etc.
|
|
117
122
|
- 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
|
+
- 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.
|
|
118
124
|
- Analytics: bare `frontpage analytics` = dashboard; also `summary|query|funnel|timeseries|compare|sql` for structured reads (SQL is guard + RLS site-scoped).
|
|
119
125
|
|
|
120
126
|
## Agent prompt loop
|
|
@@ -134,8 +140,9 @@ frontpage runs wait # wait for the agent run to finish (failed/canceled
|
|
|
134
140
|
- Caps: images/PDF ≤25 MB, video ≤50 MB, SVG/text via proxy ≤4 MB (large text/data Blob upload is editor-only).
|
|
135
141
|
- Prompt text is still required (API). Attachments ride on the same enqueue as the editor.
|
|
136
142
|
- `runs wait` with `--json`: failures use `ok: false` and include `data.run` (with `run.error` when present).
|
|
137
|
-
- `
|
|
138
|
-
- `
|
|
143
|
+
- `runs wait` watches an editor agent run only.
|
|
144
|
+
- `deploy wait` is fail-closed on a failed/canceled agent run it actually watched (or a latest failed/canceled when idle). It then waits until `GET /deploy` `ready` (live compile SHA equals the tip). Exit 2 on compile failure or timeout.
|
|
145
|
+
- `frontpage push --wait` does the same compile wait for the SHA just pushed. A file-only push still waits for the compile.
|
|
139
146
|
|
|
140
147
|
Mid-task continuation is automatic. There is **no** `runs continue` in v1.
|
|
141
148
|
|
|
@@ -217,7 +224,7 @@ Safe default after finishing work: summarize what changed and give the staging U
|
|
|
217
224
|
6. **Active-run guard.** Push/revert refuse while an editor agent run is active (exit 3).
|
|
218
225
|
7. **CAS.** Tip moved since pull → exit 3; pull and retry.
|
|
219
226
|
8. **No invented v1 commands:** `domains*`, `billing*`, `members*`, `suggestions*`, `sites delete|restore`, `runs continue`, MCP, git remotes / clone URLs.
|
|
220
|
-
9. **`deploy wait` ≠ publish.**
|
|
227
|
+
9. **`deploy wait` ≠ publish.** It waits for the tip compile. Production still requires `publish`. Verify live HTML with `<meta name="frontpage:commit">`.
|
|
221
228
|
10. **Honesty on stubs.** `open --prod` is currently a no-op stub (still opens the subdomain).
|
|
222
229
|
|
|
223
230
|
## Isolation (hard fence)
|
package/README.md
CHANGED
|
@@ -23,9 +23,9 @@ Requires Node.js ≥ 20.
|
|
|
23
23
|
3. `frontpage sites` — note the **subdomain** (not a custom domain).
|
|
24
24
|
4. `mkdir my-site && cd my-site && frontpage use <subdomain>`
|
|
25
25
|
5. `frontpage pull` → edit files under `src/`, `public/`, or `content/`
|
|
26
|
-
6. `frontpage push --message "describe the change"`
|
|
27
|
-
7. Review staging at `https://<subdomain>.frontpage.host`
|
|
28
|
-
8. `frontpage publish` only when you intentionally promote to production.
|
|
26
|
+
6. `frontpage push --message "describe the change"` (add `--wait` to block until the site compile is live)
|
|
27
|
+
7. Review staging at `https://<subdomain>.frontpage.host` after the compile is ready.
|
|
28
|
+
8. `frontpage publish --wait` only when you intentionally promote the ready compile to production.
|
|
29
29
|
|
|
30
30
|
## Environment
|
|
31
31
|
|
package/dist/bin.js
CHANGED
|
@@ -24,6 +24,7 @@ import { runPages } from "./commands/pages.js";
|
|
|
24
24
|
import { runModules } from "./commands/modules.js";
|
|
25
25
|
import { runExtensions } from "./commands/extensions.js";
|
|
26
26
|
import { runMedia } from "./commands/media.js";
|
|
27
|
+
import { runImages } from "./commands/images.js";
|
|
27
28
|
import { runInbox } from "./commands/inbox.js";
|
|
28
29
|
import { runSeo } from "./commands/seo.js";
|
|
29
30
|
import { runArticles } from "./commands/articles.js";
|
|
@@ -107,6 +108,8 @@ async function main() {
|
|
|
107
108
|
return runExtensions(args);
|
|
108
109
|
case "media":
|
|
109
110
|
return runMedia(args);
|
|
111
|
+
case "images":
|
|
112
|
+
return runImages(args);
|
|
110
113
|
case "inbox":
|
|
111
114
|
return runInbox(args);
|
|
112
115
|
case "seo":
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/** `frontpage deploy wait` —
|
|
1
|
+
/** `frontpage deploy wait` — agent-run idle, then the tip's site compile. */
|
|
2
2
|
export declare function runDeploy(args: string[]): Promise<number>;
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { resolveSlug } from "../site.js";
|
|
2
2
|
import { EXIT_USAGE } from "../http/client.js";
|
|
3
3
|
import { emitErr, emitUsage } from "../runtime.js";
|
|
4
|
-
import {
|
|
5
|
-
/** `frontpage deploy wait` —
|
|
4
|
+
import { waitForAgentThenCompile } from "./wait-site-compile.js";
|
|
5
|
+
/** `frontpage deploy wait` — agent-run idle, then the tip's site compile. */
|
|
6
6
|
export async function runDeploy(args) {
|
|
7
7
|
try {
|
|
8
8
|
const sub = args[0];
|
|
@@ -10,12 +10,11 @@ export async function runDeploy(args) {
|
|
|
10
10
|
return emitUsage("deploy", "Usage: frontpage deploy wait [--site <slug>]", EXIT_USAGE);
|
|
11
11
|
}
|
|
12
12
|
const { slug } = await resolveSlug(args.slice(1));
|
|
13
|
-
|
|
13
|
+
return waitForAgentThenCompile({
|
|
14
|
+
command: "deploy",
|
|
14
15
|
slug,
|
|
15
|
-
// Fail-closed: idle with latest failed/canceled is not success.
|
|
16
16
|
failClosedOnIdleTerminalFailure: true,
|
|
17
17
|
});
|
|
18
|
-
return emitWaitOutcome({ command: "deploy", slug, result });
|
|
19
18
|
}
|
|
20
19
|
catch (err) {
|
|
21
20
|
return emitErr("deploy", err);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runImages(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { resolveSlug } from "../site.js";
|
|
2
|
+
import { EXIT_OK, EXIT_USAGE, exitCodeForError, v1Post } from "../http/client.js";
|
|
3
|
+
import { assertNoUnknownFlags, takeFlag } from "../argv.js";
|
|
4
|
+
import { emitErr, emitOk, isJson } from "../runtime.js";
|
|
5
|
+
const USAGE = 'Usage: frontpage images generate --prompt "…" [--count N] [--site <slug>] [--json]';
|
|
6
|
+
const GENERATE_TIMEOUT_MS = 180_000;
|
|
7
|
+
export async function runImages(args) {
|
|
8
|
+
try {
|
|
9
|
+
const { slug, rest } = await resolveSlug(args);
|
|
10
|
+
const sub = rest[0];
|
|
11
|
+
if (sub !== "generate") {
|
|
12
|
+
console.error(USAGE);
|
|
13
|
+
return EXIT_USAGE;
|
|
14
|
+
}
|
|
15
|
+
const work = rest.slice(1);
|
|
16
|
+
const prompt = takeFlag(work, "--prompt");
|
|
17
|
+
const countRaw = takeFlag(work, "--count");
|
|
18
|
+
assertNoUnknownFlags(work, "images");
|
|
19
|
+
if (work.length > 0) {
|
|
20
|
+
console.error(`Unexpected argument: ${work[0]}\n${USAGE}`);
|
|
21
|
+
return EXIT_USAGE;
|
|
22
|
+
}
|
|
23
|
+
if (!prompt) {
|
|
24
|
+
console.error(USAGE);
|
|
25
|
+
return EXIT_USAGE;
|
|
26
|
+
}
|
|
27
|
+
let count;
|
|
28
|
+
if (countRaw !== undefined) {
|
|
29
|
+
const n = Number(countRaw);
|
|
30
|
+
if (!Number.isInteger(n) || n < 1 || n > 4) {
|
|
31
|
+
console.error("Ask for 1 to 4 variations.");
|
|
32
|
+
return EXIT_USAGE;
|
|
33
|
+
}
|
|
34
|
+
count = n;
|
|
35
|
+
}
|
|
36
|
+
const body = { prompt };
|
|
37
|
+
if (count !== undefined)
|
|
38
|
+
body.count = count;
|
|
39
|
+
const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/images`, body, { timeoutMs: GENERATE_TIMEOUT_MS });
|
|
40
|
+
if (isJson()) {
|
|
41
|
+
emitOk("images", { slug, images: data.images });
|
|
42
|
+
return EXIT_OK;
|
|
43
|
+
}
|
|
44
|
+
for (const img of data.images ?? []) {
|
|
45
|
+
console.log(`sitePath: ${img.sitePath}`);
|
|
46
|
+
if (img.filename)
|
|
47
|
+
console.log(`filename: ${img.filename}`);
|
|
48
|
+
}
|
|
49
|
+
return EXIT_OK;
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (isJson())
|
|
53
|
+
return emitErr("images", err);
|
|
54
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
55
|
+
return exitCodeForError(err);
|
|
56
|
+
}
|
|
57
|
+
}
|
package/dist/commands/modules.js
CHANGED
|
@@ -5,6 +5,7 @@ import { emitUsage } from "../runtime.js";
|
|
|
5
5
|
import { formatModuleUsage, formatModulesIndex, isCliModuleAction, isDocumentedModuleKey, isGenericModuleVerb, } from "../modules/commands.js";
|
|
6
6
|
import { runGoogleReviews } from "../modules/google_reviews.js";
|
|
7
7
|
import { isTypedConfigCliKey, runTypedConfig } from "../modules/typed_config.js";
|
|
8
|
+
import { isFormsDestinationCliAction, runFormsDestinations } from "../modules/forms_destinations.js";
|
|
8
9
|
import { isOauthCliKey, runOauthHandoff } from "../modules/oauth.js";
|
|
9
10
|
/** Drop --site so we can print local usage without resolving a checkout. */
|
|
10
11
|
function positionalArgs(args) {
|
|
@@ -44,6 +45,9 @@ export async function runModules(args) {
|
|
|
44
45
|
if (peekedKey === "google_reviews") {
|
|
45
46
|
return await runGoogleReviews(slug, peekedAction, extraFlags);
|
|
46
47
|
}
|
|
48
|
+
if (peekedKey === "forms" && isFormsDestinationCliAction(peekedAction)) {
|
|
49
|
+
return await runFormsDestinations(slug, peekedAction, extraFlags);
|
|
50
|
+
}
|
|
47
51
|
if (isTypedConfigCliKey(peekedKey)) {
|
|
48
52
|
return await runTypedConfig(peekedKey, slug, peekedAction, extraFlags);
|
|
49
53
|
}
|
package/dist/commands/publish.js
CHANGED
|
@@ -1,19 +1,77 @@
|
|
|
1
1
|
import { resolveSlug } from "../site.js";
|
|
2
|
-
import { EXIT_OK, EXIT_USAGE,
|
|
3
|
-
import { emitErr, emitOk, emitUsage, isJson, requireYesOrTty } from "../runtime.js";
|
|
2
|
+
import { EXIT_OK, EXIT_USAGE, v1Post, v1Delete } from "../http/client.js";
|
|
3
|
+
import { emitErr, emitFail, emitOk, emitUsage, isJson, progress, requireYesOrTty } from "../runtime.js";
|
|
4
|
+
import { describePublishResult, waitForCompileReady } from "../compile.js";
|
|
5
|
+
function takeWaitFlag(rest) {
|
|
6
|
+
const i = rest.indexOf("--wait");
|
|
7
|
+
if (i === -1)
|
|
8
|
+
return false;
|
|
9
|
+
rest.splice(i, 1);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
function emitPublished(slug, data) {
|
|
13
|
+
const described = describePublishResult({
|
|
14
|
+
productionCommit: data.productionCommit ?? null,
|
|
15
|
+
productionUrl: data.productionUrl ?? null,
|
|
16
|
+
});
|
|
17
|
+
if (isJson()) {
|
|
18
|
+
emitOk("publish", {
|
|
19
|
+
slug,
|
|
20
|
+
productionUrl: data.productionUrl ?? null,
|
|
21
|
+
productionCommit: data.productionCommit ?? null,
|
|
22
|
+
servingCommit: described.servingCommit,
|
|
23
|
+
matchesTip: described.matchesTip,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.log(`Published ${slug}`);
|
|
28
|
+
for (const line of described.lines)
|
|
29
|
+
console.log(line);
|
|
30
|
+
if (data.productionUrl)
|
|
31
|
+
console.log(data.productionUrl);
|
|
32
|
+
if (!described.matchesTip) {
|
|
33
|
+
console.error("Publish promoted a build that is not the current tip. Wait for the site compile, then publish again.");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return EXIT_OK;
|
|
37
|
+
}
|
|
4
38
|
export async function runPublish(args) {
|
|
5
39
|
try {
|
|
6
|
-
const { slug } = await resolveSlug(args);
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
40
|
+
const { slug, rest } = await resolveSlug(args);
|
|
41
|
+
const work = [...rest];
|
|
42
|
+
const wait = takeWaitFlag(work);
|
|
43
|
+
if (work.length > 0) {
|
|
44
|
+
return emitUsage("publish", "Usage: frontpage publish [--site <slug>] [--wait] [--json]", EXIT_USAGE);
|
|
10
45
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
46
|
+
if (wait) {
|
|
47
|
+
if (!isJson())
|
|
48
|
+
progress("Waiting for the site compile…\n");
|
|
49
|
+
const compile = await waitForCompileReady({ slug });
|
|
50
|
+
if (compile.exitCode !== EXIT_OK) {
|
|
51
|
+
const message = compile.failed
|
|
52
|
+
? "Site compile failed. Refusing to publish a previous build."
|
|
53
|
+
: compile.timedOut
|
|
54
|
+
? "Timed out waiting for the site compile. Refusing to publish a previous build."
|
|
55
|
+
: "Site compile is not ready. Refusing to publish a previous build.";
|
|
56
|
+
return emitFail("publish", {
|
|
57
|
+
message,
|
|
58
|
+
exitCode: compile.exitCode,
|
|
59
|
+
data: {
|
|
60
|
+
slug,
|
|
61
|
+
compile: compile.status,
|
|
62
|
+
tip: compile.status?.stagingCommit ?? null,
|
|
63
|
+
live: compile.status?.liveCommit ?? null,
|
|
64
|
+
status: compile.failed
|
|
65
|
+
? "compile_failed"
|
|
66
|
+
: compile.timedOut
|
|
67
|
+
? "compile_timeout"
|
|
68
|
+
: "compile_not_ready",
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
15
72
|
}
|
|
16
|
-
|
|
73
|
+
const data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/publish`);
|
|
74
|
+
return emitPublished(slug, data);
|
|
17
75
|
}
|
|
18
76
|
catch (err) {
|
|
19
77
|
return emitErr("publish", err);
|
package/dist/commands/push.js
CHANGED
|
@@ -4,7 +4,7 @@ import { resolveSlug } from "../site.js";
|
|
|
4
4
|
import { EXIT_OK, EXIT_USAGE, 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
|
-
import {
|
|
7
|
+
import { waitForAgentThenCompile } from "./wait-site-compile.js";
|
|
8
8
|
const TEXT_EXTS = new Set([
|
|
9
9
|
".astro",
|
|
10
10
|
".ts",
|
|
@@ -136,8 +136,7 @@ export async function runPush(args) {
|
|
|
136
136
|
console.error(`Warning: ${w}`);
|
|
137
137
|
return EXIT_OK;
|
|
138
138
|
}
|
|
139
|
-
// --wait:
|
|
140
|
-
// Does not invent a Vercel deploy oracle; file-only pushes with no active run return promptly.
|
|
139
|
+
// --wait: agent-run idle (if any), then the new commit's site compile.
|
|
141
140
|
if (!isJson()) {
|
|
142
141
|
if (!data.changed) {
|
|
143
142
|
console.log("Nothing to commit (remote already matches).");
|
|
@@ -148,17 +147,12 @@ export async function runPush(args) {
|
|
|
148
147
|
}
|
|
149
148
|
for (const w of data.warnings ?? [])
|
|
150
149
|
console.error(`Warning: ${w}`);
|
|
151
|
-
console.error("Waiting for agent run / deploy idle…");
|
|
152
150
|
}
|
|
153
|
-
|
|
154
|
-
slug,
|
|
155
|
-
// Do not fail on an ancient canceled run when this push never saw busy work.
|
|
156
|
-
failClosedOnIdleTerminalFailure: false,
|
|
157
|
-
});
|
|
158
|
-
return emitWaitOutcome({
|
|
151
|
+
return waitForAgentThenCompile({
|
|
159
152
|
command: "push",
|
|
160
153
|
slug,
|
|
161
|
-
|
|
154
|
+
expectCommit: data.commitSha ?? null,
|
|
155
|
+
failClosedOnIdleTerminalFailure: false,
|
|
162
156
|
extra: { ...pushPayload, waited: true },
|
|
163
157
|
});
|
|
164
158
|
}
|
|
@@ -16,6 +16,18 @@ export type WaitIdleResult = {
|
|
|
16
16
|
};
|
|
17
17
|
export declare function isBusyStatus(status: string): boolean;
|
|
18
18
|
export declare function isFailTerminal(status: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* A historical succeeded run is unrelated when this wait never saw busy work.
|
|
21
|
+
* Keep failed/canceled latest so deploy wait stays fail-closed (Phase D).
|
|
22
|
+
*/
|
|
23
|
+
export declare function sanitizeUnrelatedIdleRun(args: {
|
|
24
|
+
observedBusy: boolean;
|
|
25
|
+
run: WaitRunSnapshot | null;
|
|
26
|
+
status: string;
|
|
27
|
+
}): {
|
|
28
|
+
run: WaitRunSnapshot | null;
|
|
29
|
+
status: string;
|
|
30
|
+
};
|
|
19
31
|
/**
|
|
20
32
|
* Decide exit after idle. Pure — unit-tested.
|
|
21
33
|
* - If we watched a busy run end: succeeded → 0; failed/canceled → 2.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Shared wait for editor agent-run busy states (queued/running/deploying).
|
|
3
|
+
* This is the agent-run clock, not the site compile. File-only pushes have
|
|
4
|
+
* no agent run — success here does not mean the latest commit is built.
|
|
4
5
|
*/
|
|
5
6
|
import { EXIT_OK, EXIT_USAGE, v1Get } from "../http/client.js";
|
|
6
7
|
import { emitFail, emitOk, isJson, progress } from "../runtime.js";
|
|
@@ -12,6 +13,18 @@ export function isBusyStatus(status) {
|
|
|
12
13
|
export function isFailTerminal(status) {
|
|
13
14
|
return FAIL_TERMINAL.has(status);
|
|
14
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* A historical succeeded run is unrelated when this wait never saw busy work.
|
|
18
|
+
* Keep failed/canceled latest so deploy wait stays fail-closed (Phase D).
|
|
19
|
+
*/
|
|
20
|
+
export function sanitizeUnrelatedIdleRun(args) {
|
|
21
|
+
if (args.observedBusy)
|
|
22
|
+
return { run: args.run, status: args.status };
|
|
23
|
+
if (args.run && isFailTerminal(args.run.status)) {
|
|
24
|
+
return { run: args.run, status: args.status };
|
|
25
|
+
}
|
|
26
|
+
return { run: null, status: "idle" };
|
|
27
|
+
}
|
|
15
28
|
/**
|
|
16
29
|
* Decide exit after idle. Pure — unit-tested.
|
|
17
30
|
* - If we watched a busy run end: succeeded → 0; failed/canceled → 2.
|
|
@@ -111,11 +124,16 @@ export async function waitForAgentIdle(args) {
|
|
|
111
124
|
observedBusy,
|
|
112
125
|
failClosedOnIdleTerminalFailure: args.failClosedOnIdleTerminalFailure,
|
|
113
126
|
});
|
|
127
|
+
const sanitized = sanitizeUnrelatedIdleRun({
|
|
128
|
+
observedBusy,
|
|
129
|
+
run: decided.run,
|
|
130
|
+
status: decided.status,
|
|
131
|
+
});
|
|
114
132
|
return {
|
|
115
133
|
exitCode: decided.exitCode,
|
|
116
|
-
run:
|
|
134
|
+
run: sanitized.run,
|
|
117
135
|
observedBusy,
|
|
118
|
-
status:
|
|
136
|
+
status: sanitized.status,
|
|
119
137
|
};
|
|
120
138
|
}
|
|
121
139
|
progress("\n");
|
|
@@ -175,7 +193,7 @@ export function emitWaitOutcome(args) {
|
|
|
175
193
|
console.log(`${result.run.id} ${result.run.status}${result.run.statusMessage ? ` ${result.run.statusMessage}` : ""}`);
|
|
176
194
|
}
|
|
177
195
|
else {
|
|
178
|
-
console.log("No
|
|
196
|
+
console.log("No agent run was in flight. This does not mean the latest commit is compiled.");
|
|
179
197
|
}
|
|
180
198
|
return EXIT_OK;
|
|
181
199
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type DeployStatus } from "../compile.js";
|
|
2
|
+
import { type WaitIdleResult } from "./wait-agent-idle.js";
|
|
3
|
+
export declare function waitForAgentThenCompile(args: {
|
|
4
|
+
command: "deploy" | "push";
|
|
5
|
+
slug: string;
|
|
6
|
+
expectCommit?: string | null;
|
|
7
|
+
failClosedOnIdleTerminalFailure: boolean;
|
|
8
|
+
extra?: Record<string, unknown>;
|
|
9
|
+
}): Promise<number>;
|
|
10
|
+
export declare function emitCompileWaitOutcome(args: {
|
|
11
|
+
command: "deploy" | "push";
|
|
12
|
+
slug: string;
|
|
13
|
+
agent: WaitIdleResult;
|
|
14
|
+
compile: {
|
|
15
|
+
exitCode: number;
|
|
16
|
+
timedOut: boolean;
|
|
17
|
+
failed?: boolean;
|
|
18
|
+
status: DeployStatus | null;
|
|
19
|
+
};
|
|
20
|
+
extra?: Record<string, unknown>;
|
|
21
|
+
}): number;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* After the agent-run clock is idle, wait for the site compile of the tip.
|
|
3
|
+
*/
|
|
4
|
+
import { EXIT_OK } from "../http/client.js";
|
|
5
|
+
import { emitFail, emitOk, isJson, progress } from "../runtime.js";
|
|
6
|
+
import { waitForCompileReady } from "../compile.js";
|
|
7
|
+
import { emitWaitOutcome, waitForAgentIdle, } from "./wait-agent-idle.js";
|
|
8
|
+
export async function waitForAgentThenCompile(args) {
|
|
9
|
+
const agent = await waitForAgentIdle({
|
|
10
|
+
slug: args.slug,
|
|
11
|
+
failClosedOnIdleTerminalFailure: args.failClosedOnIdleTerminalFailure,
|
|
12
|
+
});
|
|
13
|
+
if (agent.exitCode !== EXIT_OK) {
|
|
14
|
+
return emitWaitOutcome({
|
|
15
|
+
command: args.command,
|
|
16
|
+
slug: args.slug,
|
|
17
|
+
result: agent,
|
|
18
|
+
extra: args.extra,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (!isJson()) {
|
|
22
|
+
if (agent.observedBusy && agent.run) {
|
|
23
|
+
console.log(`${agent.run.id} ${agent.run.status}${agent.run.statusMessage ? ` ${agent.run.statusMessage}` : ""}`);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
console.log("No agent run was in flight.");
|
|
27
|
+
}
|
|
28
|
+
progress("Waiting for the site compile…\n");
|
|
29
|
+
}
|
|
30
|
+
const compile = await waitForCompileReady({
|
|
31
|
+
slug: args.slug,
|
|
32
|
+
expectCommit: args.expectCommit,
|
|
33
|
+
});
|
|
34
|
+
return emitCompileWaitOutcome({
|
|
35
|
+
command: args.command,
|
|
36
|
+
slug: args.slug,
|
|
37
|
+
agent,
|
|
38
|
+
compile,
|
|
39
|
+
extra: args.extra,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function emitCompileWaitOutcome(args) {
|
|
43
|
+
const compileData = {
|
|
44
|
+
compile: args.compile.status,
|
|
45
|
+
compileReady: Boolean(args.compile.status?.ready) && !args.compile.timedOut,
|
|
46
|
+
};
|
|
47
|
+
if (args.compile.exitCode !== EXIT_OK) {
|
|
48
|
+
const message = args.compile.failed
|
|
49
|
+
? "Site compile failed. The git tip is not live."
|
|
50
|
+
: args.compile.timedOut
|
|
51
|
+
? "Timed out waiting for the site compile. The git tip is not live yet."
|
|
52
|
+
: "Site compile is not ready.";
|
|
53
|
+
if (isJson()) {
|
|
54
|
+
return emitFail(args.command, {
|
|
55
|
+
message,
|
|
56
|
+
exitCode: args.compile.exitCode,
|
|
57
|
+
data: {
|
|
58
|
+
slug: args.slug,
|
|
59
|
+
waited: true,
|
|
60
|
+
observedBusy: args.agent.observedBusy,
|
|
61
|
+
status: args.compile.failed
|
|
62
|
+
? "compile_failed"
|
|
63
|
+
: args.compile.timedOut
|
|
64
|
+
? "compile_timeout"
|
|
65
|
+
: "compile_not_ready",
|
|
66
|
+
run: args.agent.run,
|
|
67
|
+
...compileData,
|
|
68
|
+
...args.extra,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
console.error(message);
|
|
73
|
+
if (args.compile.status) {
|
|
74
|
+
console.error(`tip ${args.compile.status.stagingCommit ?? "(none)"} live ${args.compile.status.liveCommit ?? "(none)"}`);
|
|
75
|
+
}
|
|
76
|
+
return args.compile.exitCode;
|
|
77
|
+
}
|
|
78
|
+
if (isJson()) {
|
|
79
|
+
emitOk(args.command, {
|
|
80
|
+
slug: args.slug,
|
|
81
|
+
waited: true,
|
|
82
|
+
observedBusy: args.agent.observedBusy,
|
|
83
|
+
status: "ready",
|
|
84
|
+
run: args.agent.run,
|
|
85
|
+
...compileData,
|
|
86
|
+
...args.extra,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const live = args.compile.status?.liveCommit ?? args.compile.status?.stagingCommit;
|
|
91
|
+
console.log(`Site compile ready ${live ?? ""}`.trim());
|
|
92
|
+
}
|
|
93
|
+
return EXIT_OK;
|
|
94
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type DeployStatus = {
|
|
2
|
+
slug?: string;
|
|
3
|
+
stagingCommit: string | null;
|
|
4
|
+
servingCommit: string | null;
|
|
5
|
+
liveCommit: string | null;
|
|
6
|
+
ready: boolean;
|
|
7
|
+
splitBrain: boolean;
|
|
8
|
+
compileState?: "ready" | "building" | "failed" | "canceled" | "unknown";
|
|
9
|
+
stagingUrl: string | null;
|
|
10
|
+
productionCommit: string | null;
|
|
11
|
+
productionServingCommit: string | null;
|
|
12
|
+
productionUrl: string | null;
|
|
13
|
+
};
|
|
14
|
+
export declare function normalizeSha(raw: string | null | undefined): string | null;
|
|
15
|
+
export declare function shortSha(raw: string | null | undefined): string;
|
|
16
|
+
/** SHA encoded in a Blob artifact prefix URL. */
|
|
17
|
+
export declare function commitFromArtifactUrl(url: string | null | undefined): string | null;
|
|
18
|
+
export declare function compileIsReady(status: Pick<DeployStatus, "ready" | "liveCommit">, expectCommit?: string | null): boolean;
|
|
19
|
+
export type CompileWaitResult = {
|
|
20
|
+
exitCode: number;
|
|
21
|
+
timedOut: boolean;
|
|
22
|
+
failed: boolean;
|
|
23
|
+
status: DeployStatus | null;
|
|
24
|
+
};
|
|
25
|
+
export declare function fetchDeployStatus(slug: string): Promise<DeployStatus>;
|
|
26
|
+
export declare function waitForCompileReady(args: {
|
|
27
|
+
slug: string;
|
|
28
|
+
expectCommit?: string | null;
|
|
29
|
+
maxMs?: number;
|
|
30
|
+
pollMs?: number;
|
|
31
|
+
fetchStatus?: (slug: string) => Promise<DeployStatus>;
|
|
32
|
+
}): Promise<CompileWaitResult>;
|
|
33
|
+
export declare function describePublishResult(args: {
|
|
34
|
+
productionCommit: string | null;
|
|
35
|
+
productionUrl: string | null;
|
|
36
|
+
}): {
|
|
37
|
+
tipCommit: string | null;
|
|
38
|
+
servingCommit: string | null;
|
|
39
|
+
matchesTip: boolean;
|
|
40
|
+
lines: string[];
|
|
41
|
+
};
|
package/dist/compile.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site compile clock (not the editor agent run).
|
|
3
|
+
*
|
|
4
|
+
* GET /api/v1/sites/{slug}/deploy → ready when the live compile SHA equals the tip.
|
|
5
|
+
*/
|
|
6
|
+
import { EXIT_OK, EXIT_USAGE, v1Get } from "./http/client.js";
|
|
7
|
+
import { progress } from "./runtime.js";
|
|
8
|
+
const SHA_RE = /^[0-9a-f]{40}$/i;
|
|
9
|
+
export function normalizeSha(raw) {
|
|
10
|
+
const sha = (raw ?? "").trim().toLowerCase();
|
|
11
|
+
return SHA_RE.test(sha) ? sha : null;
|
|
12
|
+
}
|
|
13
|
+
export function shortSha(raw) {
|
|
14
|
+
const sha = normalizeSha(raw);
|
|
15
|
+
return sha ? sha.slice(0, 7) : "(none)";
|
|
16
|
+
}
|
|
17
|
+
/** SHA encoded in a Blob artifact prefix URL. */
|
|
18
|
+
export function commitFromArtifactUrl(url) {
|
|
19
|
+
if (!url)
|
|
20
|
+
return null;
|
|
21
|
+
const m = String(url).match(/\/artifacts\/[^/]+\/([0-9a-f]{40})(?:\/|$)/i);
|
|
22
|
+
return m ? m[1].toLowerCase() : null;
|
|
23
|
+
}
|
|
24
|
+
export function compileIsReady(status, expectCommit) {
|
|
25
|
+
if (!status.ready)
|
|
26
|
+
return false;
|
|
27
|
+
const live = normalizeSha(status.liveCommit);
|
|
28
|
+
if (!live)
|
|
29
|
+
return false;
|
|
30
|
+
const expect = normalizeSha(expectCommit ?? null);
|
|
31
|
+
if (!expect)
|
|
32
|
+
return true;
|
|
33
|
+
return live === expect;
|
|
34
|
+
}
|
|
35
|
+
export async function fetchDeployStatus(slug) {
|
|
36
|
+
return v1Get(`/api/v1/sites/${encodeURIComponent(slug)}/deploy`);
|
|
37
|
+
}
|
|
38
|
+
export async function waitForCompileReady(args) {
|
|
39
|
+
const maxMs = args.maxMs ?? 30 * 60 * 1000;
|
|
40
|
+
const pollMs = args.pollMs ?? 2000;
|
|
41
|
+
const fetchStatus = args.fetchStatus ?? fetchDeployStatus;
|
|
42
|
+
const started = Date.now();
|
|
43
|
+
let last = null;
|
|
44
|
+
while (Date.now() - started < maxMs) {
|
|
45
|
+
last = await fetchStatus(args.slug);
|
|
46
|
+
if (last.compileState === "failed" || last.compileState === "canceled") {
|
|
47
|
+
progress("\n");
|
|
48
|
+
return { exitCode: EXIT_USAGE, timedOut: false, failed: true, status: last };
|
|
49
|
+
}
|
|
50
|
+
if (compileIsReady(last, args.expectCommit)) {
|
|
51
|
+
progress("\n");
|
|
52
|
+
return { exitCode: EXIT_OK, timedOut: false, failed: false, status: last };
|
|
53
|
+
}
|
|
54
|
+
const tip = shortSha(args.expectCommit ?? last.stagingCommit);
|
|
55
|
+
const live = shortSha(last.liveCommit ?? last.servingCommit);
|
|
56
|
+
progress(`\rwaiting for site compile tip ${tip} live ${live} `);
|
|
57
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
58
|
+
}
|
|
59
|
+
progress("\n");
|
|
60
|
+
return { exitCode: EXIT_USAGE, timedOut: true, failed: false, status: last };
|
|
61
|
+
}
|
|
62
|
+
export function describePublishResult(args) {
|
|
63
|
+
const tipCommit = normalizeSha(args.productionCommit);
|
|
64
|
+
const servingCommit = commitFromArtifactUrl(args.productionUrl) ?? tipCommit;
|
|
65
|
+
const matchesTip = Boolean(tipCommit && servingCommit && tipCommit === servingCommit);
|
|
66
|
+
const lines = [
|
|
67
|
+
`tip ${tipCommit ?? "(none)"}`,
|
|
68
|
+
`serving ${servingCommit ?? "(none)"}${matchesTip ? "" : " ← not the tip; this is the previous build"}`,
|
|
69
|
+
];
|
|
70
|
+
return { tipCommit, servingCommit, matchesTip, lines };
|
|
71
|
+
}
|
|
@@ -19,6 +19,7 @@ _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
|
+
images) COMPREPLY=( $(compgen -W "generate" -- "\${cur}") ) ;;
|
|
22
23
|
fonts) COMPREPLY=( $(compgen -W "list search" -- "\${cur}") ) ;;
|
|
23
24
|
tokens) COMPREPLY=( $(compgen -W "set" -- "\${cur}") ) ;;
|
|
24
25
|
seo|settings) COMPREPLY=( $(compgen -W "get set" -- "\${cur}") ) ;;
|
|
@@ -27,7 +28,9 @@ _frontpage_completions() {
|
|
|
27
28
|
modules)
|
|
28
29
|
if [[ \${COMP_CWORD} -ge 3 && \${COMP_WORDS[2]} == google_reviews ]]; then
|
|
29
30
|
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}") )
|
|
30
|
-
elif [[ \${COMP_CWORD} -ge 3 &&
|
|
31
|
+
elif [[ \${COMP_CWORD} -ge 3 && \${COMP_WORDS[2]} == forms ]]; then
|
|
32
|
+
COMPREPLY=( $(compgen -W "docs request-setup enable disable archive config get set destinations connect-destination remove-destination disconnect-destination attach-destination detach-destination request-destination-setup" -- "\${cur}") )
|
|
33
|
+
elif [[ \${COMP_CWORD} -ge 3 && (\${COMP_WORDS[2]} == callbar || \${COMP_WORDS[2]} == booking || \${COMP_WORDS[2]} == signups) ]]; then
|
|
31
34
|
COMPREPLY=( $(compgen -W "docs request-setup enable disable archive config get set" -- "\${cur}") )
|
|
32
35
|
elif [[ \${COMP_CWORD} -ge 3 && (\${COMP_WORDS[2]} == payments || \${COMP_WORDS[2]} == shop) ]]; then
|
|
33
36
|
COMPREPLY=( $(compgen -W "docs request-setup enable disable archive config status connect disconnect" -- "\${cur}") )
|
|
@@ -61,6 +64,7 @@ _frontpage() {
|
|
|
61
64
|
runs) _values 'subcommand' current wait cancel retry get ;;
|
|
62
65
|
deploy) _values 'subcommand' wait ;;
|
|
63
66
|
media) _values 'subcommand' list upload ;;
|
|
67
|
+
images) _values 'subcommand' generate ;;
|
|
64
68
|
fonts) _values 'subcommand' list search ;;
|
|
65
69
|
tokens) _values 'subcommand' set ;;
|
|
66
70
|
seo|settings) _values 'subcommand' get set ;;
|
|
@@ -68,7 +72,9 @@ _frontpage() {
|
|
|
68
72
|
modules)
|
|
69
73
|
if [[ $words[2] == google_reviews ]]; then
|
|
70
74
|
_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
|
|
71
|
-
elif [[ $words[2] ==
|
|
75
|
+
elif [[ $words[2] == forms ]]; then
|
|
76
|
+
_values 'subcommand' docs request-setup enable disable archive config get set destinations connect-destination remove-destination disconnect-destination attach-destination detach-destination request-destination-setup
|
|
77
|
+
elif [[ $words[2] == callbar || $words[2] == booking || $words[2] == signups ]]; then
|
|
72
78
|
_values 'subcommand' docs request-setup enable disable archive config get set
|
|
73
79
|
elif [[ $words[2] == payments || $words[2] == shop ]]; then
|
|
74
80
|
_values 'subcommand' docs request-setup enable disable archive config status connect disconnect
|
|
@@ -95,13 +101,15 @@ export function generateFishCompletion() {
|
|
|
95
101
|
`complete -c frontpage -n "__fish_seen_subcommand_from runs" -a "current wait cancel retry get"`,
|
|
96
102
|
`complete -c frontpage -n "__fish_seen_subcommand_from deploy" -a "wait"`,
|
|
97
103
|
`complete -c frontpage -n "__fish_seen_subcommand_from media" -a "list upload"`,
|
|
104
|
+
`complete -c frontpage -n "__fish_seen_subcommand_from images" -a "generate"`,
|
|
98
105
|
`complete -c frontpage -n "__fish_seen_subcommand_from fonts" -a "list search"`,
|
|
99
106
|
`complete -c frontpage -n "__fish_seen_subcommand_from tokens" -a "set"`,
|
|
100
107
|
`complete -c frontpage -n "__fish_seen_subcommand_from seo settings" -a "get set"`,
|
|
101
108
|
`complete -c frontpage -n "__fish_seen_subcommand_from analytics" -a "dashboard summary query funnel timeseries compare sql"`,
|
|
102
109
|
`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"`,
|
|
103
110
|
`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"`,
|
|
104
|
-
`complete -c frontpage -n "__fish_seen_subcommand_from modules; and __fish_seen_subcommand_from
|
|
111
|
+
`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"`,
|
|
112
|
+
`complete -c frontpage -n "__fish_seen_subcommand_from modules; and __fish_seen_subcommand_from callbar booking signups" -a "get set"`,
|
|
105
113
|
`complete -c frontpage -n "__fish_seen_subcommand_from modules; and __fish_seen_subcommand_from payments shop" -a "status connect disconnect"`,
|
|
106
114
|
`complete -c frontpage -n "__fish_seen_subcommand_from modules; and __fish_seen_subcommand_from ordering" -a "status connect disconnect sync"`,
|
|
107
115
|
`complete -c frontpage -n "__fish_seen_subcommand_from extensions" -a "list docs enable disable config"`,
|
package/dist/modules/commands.js
CHANGED
|
@@ -63,6 +63,13 @@ export const MODULE_COMMANDS = [
|
|
|
63
63
|
extraActions: [
|
|
64
64
|
"get [--instance <slug>]",
|
|
65
65
|
"set --instance <slug> [--notify-email] [--confirm yes|no]",
|
|
66
|
+
"destinations [--instance <slug>]",
|
|
67
|
+
"connect-destination <privyr|hubspot|pipedrive> --token \"…\" [--lead-source \"…\"]",
|
|
68
|
+
"attach-destination --instance <slug> --provider <id>",
|
|
69
|
+
"detach-destination --instance <slug> --provider <id>",
|
|
70
|
+
"remove-destination <id> --yes",
|
|
71
|
+
"disconnect-destination <id> --yes",
|
|
72
|
+
"request-destination-setup <id> --reason \"…\"",
|
|
66
73
|
],
|
|
67
74
|
notYet: [],
|
|
68
75
|
examples: [
|
|
@@ -70,12 +77,16 @@ export const MODULE_COMMANDS = [
|
|
|
70
77
|
"frontpage modules forms get",
|
|
71
78
|
"frontpage modules forms get --instance lead-gen",
|
|
72
79
|
"frontpage modules forms set --instance lead-gen --notify-email hi@example.com",
|
|
80
|
+
"frontpage modules forms destinations",
|
|
81
|
+
"frontpage modules forms connect-destination privyr --token \"…\"",
|
|
82
|
+
"frontpage modules forms attach-destination --instance lead-gen --provider privyr",
|
|
73
83
|
"frontpage modules forms docs",
|
|
74
84
|
`frontpage modules config forms --instance lead-gen --config '{"notifyEmail":"hi@example.com"}'`,
|
|
75
85
|
],
|
|
76
86
|
notes: [
|
|
77
87
|
"Without --instance, get lists each form (id, status, notifyEmail). Do not target `default`.",
|
|
78
88
|
"Confirmation emails require a paid plan (same as Modules).",
|
|
89
|
+
"Destinations copy leads to HubSpot, Pipedrive, or Privyr. Connect once, then attach each form. Token write is admin (Privyr: paste the Incoming Webhook URL, not a token). request-destination-setup lights the editor amber bar (editor key). remove-destination requires --yes. Locations: https://frontpage.host/developers/modules#forms-destinations",
|
|
79
90
|
],
|
|
80
91
|
},
|
|
81
92
|
{
|
|
@@ -275,7 +286,19 @@ export function isCliModuleAction(key, action) {
|
|
|
275
286
|
if (key === "google_reviews") {
|
|
276
287
|
return GOOGLE_REVIEWS_CLI_ACTIONS.includes(action);
|
|
277
288
|
}
|
|
278
|
-
if (key === "
|
|
289
|
+
if (key === "forms") {
|
|
290
|
+
return (TYPED_CONFIG_CLI_ACTIONS.includes(action) ||
|
|
291
|
+
[
|
|
292
|
+
"destinations",
|
|
293
|
+
"connect-destination",
|
|
294
|
+
"remove-destination",
|
|
295
|
+
"disconnect-destination",
|
|
296
|
+
"attach-destination",
|
|
297
|
+
"detach-destination",
|
|
298
|
+
"request-destination-setup",
|
|
299
|
+
].includes(action));
|
|
300
|
+
}
|
|
301
|
+
if (key === "callbar" || key === "booking" || key === "signups") {
|
|
279
302
|
return TYPED_CONFIG_CLI_ACTIONS.includes(action);
|
|
280
303
|
}
|
|
281
304
|
if (key === "payments" || key === "shop") {
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const FORMS_DESTINATION_CLI_ACTIONS: readonly ["destinations", "connect-destination", "remove-destination", "disconnect-destination", "attach-destination", "detach-destination", "request-destination-setup"];
|
|
2
|
+
export declare function isFormsDestinationCliAction(action: string): boolean;
|
|
3
|
+
export declare function runFormsDestinations(slug: string, action: string, flags: string[]): Promise<number>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §FD.CLI — form destination verbs.
|
|
3
|
+
*/
|
|
4
|
+
import { assertNoUnknownFlags, hasFlag, takeFlag } from "../argv.js";
|
|
5
|
+
import { EXIT_OK, EXIT_USAGE, v1Get, v1Post } from "../http/client.js";
|
|
6
|
+
import { emitOk, emitUsage, isJson } from "../runtime.js";
|
|
7
|
+
import { formatModuleUsage } from "./commands.js";
|
|
8
|
+
const USAGE = formatModuleUsage("forms");
|
|
9
|
+
const REMOVE_CONFIRM = "This deletes the destination from Frontpage and stops every form from sending a copy. Your leads stay in Frontpage and still email you.";
|
|
10
|
+
function pathFor(slug) {
|
|
11
|
+
return `/api/v1/sites/${encodeURIComponent(slug)}/modules/forms`;
|
|
12
|
+
}
|
|
13
|
+
export const FORMS_DESTINATION_CLI_ACTIONS = [
|
|
14
|
+
"destinations",
|
|
15
|
+
"connect-destination",
|
|
16
|
+
"remove-destination",
|
|
17
|
+
"disconnect-destination",
|
|
18
|
+
"attach-destination",
|
|
19
|
+
"detach-destination",
|
|
20
|
+
"request-destination-setup",
|
|
21
|
+
];
|
|
22
|
+
export function isFormsDestinationCliAction(action) {
|
|
23
|
+
return FORMS_DESTINATION_CLI_ACTIONS.includes(action);
|
|
24
|
+
}
|
|
25
|
+
function printDestinations(data, instanceFilter) {
|
|
26
|
+
const providers = data.destinations?.providers ?? [];
|
|
27
|
+
const connected = providers.filter((p) => p.connected);
|
|
28
|
+
if (!connected.length) {
|
|
29
|
+
console.log("(no destinations)");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const by = data.destinations?.byInstance ?? {};
|
|
33
|
+
for (const p of connected) {
|
|
34
|
+
console.log(`${p.id.padEnd(10)} connected ${p.last4 ? `••••${p.last4}` : ""}`.trimEnd());
|
|
35
|
+
const slugs = instanceFilter ? [instanceFilter] : Object.keys(by);
|
|
36
|
+
for (const slug of slugs) {
|
|
37
|
+
const row = (by[slug] ?? []).find((r) => r.provider === p.id);
|
|
38
|
+
const sending = row?.enabled ? "sending" : "—";
|
|
39
|
+
const err = row?.lastError ? ` error ${row.lastError}` : "";
|
|
40
|
+
if (instanceFilter && !row) {
|
|
41
|
+
console.log(` ${slug.padEnd(16)} —`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!instanceFilter && !row)
|
|
45
|
+
continue;
|
|
46
|
+
console.log(` ${slug.padEnd(16)} ${sending}${err}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function runFormsDestinations(slug, action, flags) {
|
|
51
|
+
const work = [...flags];
|
|
52
|
+
if (action === "destinations") {
|
|
53
|
+
const instanceId = takeFlag(work, "--instance");
|
|
54
|
+
assertNoUnknownFlags(work, "modules");
|
|
55
|
+
const data = await v1Get(pathFor(slug));
|
|
56
|
+
if (isJson()) {
|
|
57
|
+
emitOk("modules", data.destinations ?? { providers: [], byInstance: {} });
|
|
58
|
+
return EXIT_OK;
|
|
59
|
+
}
|
|
60
|
+
printDestinations(data, instanceId);
|
|
61
|
+
return EXIT_OK;
|
|
62
|
+
}
|
|
63
|
+
if (action === "connect-destination") {
|
|
64
|
+
const provider = work.shift();
|
|
65
|
+
const token = takeFlag(work, "--token");
|
|
66
|
+
const leadSource = takeFlag(work, "--lead-source");
|
|
67
|
+
assertNoUnknownFlags(work, "modules");
|
|
68
|
+
if (!provider || !token) {
|
|
69
|
+
return emitUsage("modules", `Usage: frontpage modules forms connect-destination <privyr|hubspot|pipedrive> --token "…"\n\n${USAGE}`);
|
|
70
|
+
}
|
|
71
|
+
const data = await v1Post(pathFor(slug), {
|
|
72
|
+
action: "destination_connect",
|
|
73
|
+
provider,
|
|
74
|
+
token,
|
|
75
|
+
...(leadSource ? { leadSource } : {}),
|
|
76
|
+
});
|
|
77
|
+
if (isJson()) {
|
|
78
|
+
emitOk("modules", data);
|
|
79
|
+
return EXIT_OK;
|
|
80
|
+
}
|
|
81
|
+
const last4 = typeof data.last4 === "string" ? data.last4 : "";
|
|
82
|
+
console.log(`Connected ${provider}${last4 ? ` ••••${last4}` : ""}.`);
|
|
83
|
+
return EXIT_OK;
|
|
84
|
+
}
|
|
85
|
+
if (action === "remove-destination" || action === "disconnect-destination") {
|
|
86
|
+
const provider = work.shift();
|
|
87
|
+
const yes = hasFlag(work, "--yes");
|
|
88
|
+
assertNoUnknownFlags(work, "modules");
|
|
89
|
+
if (!provider) {
|
|
90
|
+
return emitUsage("modules", `Usage: frontpage modules forms ${action} <privyr|hubspot|pipedrive> --yes\n\n${USAGE}`);
|
|
91
|
+
}
|
|
92
|
+
if (!yes) {
|
|
93
|
+
return emitUsage("modules", REMOVE_CONFIRM, EXIT_USAGE);
|
|
94
|
+
}
|
|
95
|
+
const data = await v1Post(pathFor(slug), {
|
|
96
|
+
action: "destination_remove",
|
|
97
|
+
provider,
|
|
98
|
+
});
|
|
99
|
+
if (isJson()) {
|
|
100
|
+
emitOk("modules", data);
|
|
101
|
+
return EXIT_OK;
|
|
102
|
+
}
|
|
103
|
+
console.log(`Removed ${provider}. Leads still land in Frontpage.`);
|
|
104
|
+
return EXIT_OK;
|
|
105
|
+
}
|
|
106
|
+
if (action === "attach-destination" || action === "detach-destination") {
|
|
107
|
+
const instanceId = takeFlag(work, "--instance");
|
|
108
|
+
const provider = takeFlag(work, "--provider") ?? work.shift();
|
|
109
|
+
assertNoUnknownFlags(work, "modules");
|
|
110
|
+
if (!instanceId || !provider) {
|
|
111
|
+
return emitUsage("modules", `Usage: frontpage modules forms ${action} --instance <slug> --provider <privyr|hubspot|pipedrive>\n\n${USAGE}`);
|
|
112
|
+
}
|
|
113
|
+
const data = await v1Post(pathFor(slug), {
|
|
114
|
+
action: action === "attach-destination" ? "destination_attach" : "destination_detach",
|
|
115
|
+
provider,
|
|
116
|
+
instanceId,
|
|
117
|
+
});
|
|
118
|
+
if (isJson()) {
|
|
119
|
+
emitOk("modules", data);
|
|
120
|
+
return EXIT_OK;
|
|
121
|
+
}
|
|
122
|
+
console.log(action === "attach-destination"
|
|
123
|
+
? `${instanceId} will also send a copy to ${provider}.`
|
|
124
|
+
: `Stopped sending ${instanceId} to ${provider}.`);
|
|
125
|
+
return EXIT_OK;
|
|
126
|
+
}
|
|
127
|
+
if (action === "request-destination-setup") {
|
|
128
|
+
const provider = work.shift();
|
|
129
|
+
const reason = takeFlag(work, "--reason") ?? "";
|
|
130
|
+
assertNoUnknownFlags(work, "modules");
|
|
131
|
+
if (!provider || !reason) {
|
|
132
|
+
return emitUsage("modules", `Usage: frontpage modules forms request-destination-setup <privyr|hubspot|pipedrive> --reason "…"\n\n${USAGE}`);
|
|
133
|
+
}
|
|
134
|
+
const data = await v1Post(pathFor(slug), {
|
|
135
|
+
action: "destination_request_setup",
|
|
136
|
+
provider,
|
|
137
|
+
reason,
|
|
138
|
+
});
|
|
139
|
+
if (isJson()) {
|
|
140
|
+
emitOk("modules", data);
|
|
141
|
+
return EXIT_OK;
|
|
142
|
+
}
|
|
143
|
+
console.log(`Setup requested for ${provider}. The owner sees an amber prompt in the Frontpage editor.`);
|
|
144
|
+
return EXIT_OK;
|
|
145
|
+
}
|
|
146
|
+
return emitUsage("modules", `${USAGE}\n\nUnknown action "${action}".`);
|
|
147
|
+
}
|
|
@@ -46,17 +46,29 @@ function printConfirmable(config, confirmations) {
|
|
|
46
46
|
else
|
|
47
47
|
printLine("confirm", yesNo(on));
|
|
48
48
|
}
|
|
49
|
-
function printFormsList(instances) {
|
|
49
|
+
function printFormsList(instances, destinations) {
|
|
50
50
|
if (!instances.length) {
|
|
51
51
|
console.log("(no forms)");
|
|
52
|
-
return;
|
|
53
52
|
}
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
else {
|
|
54
|
+
for (const i of instances) {
|
|
55
|
+
const id = i.instance ?? "";
|
|
56
|
+
const label = i.label && i.label !== id ? i.label : "";
|
|
57
|
+
const status = String(i.status ?? "—");
|
|
58
|
+
const email = typeof i.config?.notifyEmail === "string" ? i.config.notifyEmail : "(account email)";
|
|
59
|
+
const attached = (destinations?.byInstance?.[id] ?? [])
|
|
60
|
+
.filter((r) => r.enabled)
|
|
61
|
+
.map((r) => r.provider)
|
|
62
|
+
.join(",");
|
|
63
|
+
console.log(`${id.padEnd(20)} ${status.padEnd(12)} ${label ? `${label} ` : ""}${email.padEnd(22)} ${attached || "—"}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const connected = (destinations?.providers ?? []).filter((p) => p.connected);
|
|
67
|
+
if (connected.length) {
|
|
68
|
+
console.log("");
|
|
69
|
+
for (const p of connected) {
|
|
70
|
+
console.log(`${p.id} connected${p.last4 ? ` ••••${p.last4}` : ""}`);
|
|
71
|
+
}
|
|
60
72
|
}
|
|
61
73
|
}
|
|
62
74
|
function parseYesNoFlag(raw, flag) {
|
|
@@ -81,7 +93,7 @@ export async function runTypedConfig(key, slug, action, flags) {
|
|
|
81
93
|
return EXIT_OK;
|
|
82
94
|
}
|
|
83
95
|
if (key === "forms" && !instanceId) {
|
|
84
|
-
printFormsList(data.instances ?? []);
|
|
96
|
+
printFormsList(data.instances ?? [], data.destinations);
|
|
85
97
|
return EXIT_OK;
|
|
86
98
|
}
|
|
87
99
|
if (key === "callbar")
|
package/dist/registry.js
CHANGED
|
@@ -104,7 +104,7 @@ export const COMMANDS = [
|
|
|
104
104
|
flags: [
|
|
105
105
|
"--site <slug>",
|
|
106
106
|
"--message <text>",
|
|
107
|
-
"--wait: after push, wait until
|
|
107
|
+
"--wait: after push, wait until the new commit's site compile is live (agent-run idle first, then GET /deploy ready)",
|
|
108
108
|
],
|
|
109
109
|
dangers: [
|
|
110
110
|
"Requires prior pull (baseCommitSha)",
|
|
@@ -249,10 +249,16 @@ export const COMMANDS = [
|
|
|
249
249
|
{
|
|
250
250
|
name: "publish",
|
|
251
251
|
summary: "Promote staging to production",
|
|
252
|
-
usage: "frontpage publish [--site <slug>] [--json]",
|
|
252
|
+
usage: "frontpage publish [--site <slug>] [--wait] [--json]",
|
|
253
253
|
topics: ["agent"],
|
|
254
|
+
examples: ["frontpage publish", "frontpage publish --wait"],
|
|
255
|
+
flags: [
|
|
256
|
+
"--wait: wait until the staging tip's site compile is live, then publish (refuses a previous build)",
|
|
257
|
+
"Prints tip SHA and serving SHA; warns if they differ",
|
|
258
|
+
],
|
|
254
259
|
dangers: [
|
|
255
|
-
"Promotes the
|
|
260
|
+
"Promotes the last ready staging compile to PRODUCTION (custom domain)",
|
|
261
|
+
"409 if the tip is not compiled yet — wait, then publish",
|
|
256
262
|
"Agents: never run unless the user explicitly asked to publish",
|
|
257
263
|
],
|
|
258
264
|
seeAlso: ["unpublish", "deploy"],
|
|
@@ -270,13 +276,14 @@ export const COMMANDS = [
|
|
|
270
276
|
},
|
|
271
277
|
{
|
|
272
278
|
name: "deploy",
|
|
273
|
-
summary: "Wait for
|
|
279
|
+
summary: "Wait for the current tip's site compile (not publish)",
|
|
274
280
|
usage: "frontpage deploy wait [--site <slug>] [--json]",
|
|
275
281
|
topics: ["agent"],
|
|
276
282
|
flags: [
|
|
277
|
-
"
|
|
283
|
+
"Waits for editor-agent idle (if a run is in flight), then until GET /deploy ready",
|
|
284
|
+
"ready = the live compile SHA equals the git tip — not a historical agent run",
|
|
278
285
|
"Does NOT promote to production; use publish only when the user asks",
|
|
279
|
-
"Fail-closed: exit 2 when the waited run
|
|
286
|
+
"Fail-closed: exit 2 when the waited agent run is failed/canceled, or the compile fails or times out",
|
|
280
287
|
],
|
|
281
288
|
},
|
|
282
289
|
{
|
|
@@ -361,6 +368,24 @@ export const COMMANDS = [
|
|
|
361
368
|
topics: ["agent", "content"],
|
|
362
369
|
examples: ["frontpage extensions docs charts"],
|
|
363
370
|
},
|
|
371
|
+
{
|
|
372
|
+
name: "images",
|
|
373
|
+
summary: "Generate 1–4 AI image variations of one prompt (counts against AI $ and storage)",
|
|
374
|
+
usage: 'frontpage images generate --prompt "…" [--count N] [--site <slug>] [--json]',
|
|
375
|
+
topics: ["content"],
|
|
376
|
+
examples: [
|
|
377
|
+
'frontpage images generate --prompt "flat illustration of a red stand mixer on marble"',
|
|
378
|
+
'frontpage images generate --prompt "watercolor cafe storefront at dusk" --count 4',
|
|
379
|
+
],
|
|
380
|
+
flags: [
|
|
381
|
+
"One prompt; --count 1–4 is variations of that same picture (default 1)",
|
|
382
|
+
"Human success: sitePath first (markup-safe), then filename",
|
|
383
|
+
"Counts against the site AI $ budget and storage; 402 when the cycle is spent",
|
|
384
|
+
"--count outside 1–4 fails locally (exit 2) with no request",
|
|
385
|
+
"Unknown leftover flags fail closed (exit 2)",
|
|
386
|
+
],
|
|
387
|
+
seeAlso: ["media", "prompt", "help content", "help site-building"],
|
|
388
|
+
},
|
|
364
389
|
{
|
|
365
390
|
name: "media",
|
|
366
391
|
summary: "List or upload site media (images/PDF ≤25 MB, video ≤50 MB)",
|
|
@@ -478,12 +503,13 @@ export const TOPICS = {
|
|
|
478
503
|
5. frontpage pull
|
|
479
504
|
6. Edit files under src/, public/, or content/
|
|
480
505
|
7. frontpage push --message "…"
|
|
481
|
-
8.
|
|
506
|
+
8. frontpage deploy wait # tip compile ready (optional; push --wait does this)
|
|
507
|
+
Review staging: https://<subdomain>.frontpage.host when that subdomain is publicly shareable.
|
|
482
508
|
With a custom domain connected, the subdomain is often owner-private (login wall ≠ failed push);
|
|
483
509
|
anonymous review is on the published custom domain after publish.
|
|
484
|
-
9. frontpage publish
|
|
510
|
+
9. frontpage publish [--wait] # ONLY if the user asked — promotes the ready compile
|
|
485
511
|
|
|
486
|
-
|
|
512
|
+
Push updates the git tip. The site compile is a second clock. deploy wait is not go-live.
|
|
487
513
|
Never publish/unpublish unless the user explicitly asks.
|
|
488
514
|
Agents: read AGENTS.md + frontpage help site-building; use --json for structured I/O.
|
|
489
515
|
More: frontpage help auth | sync | site-building | agent | content | history | isolation | exit-codes`,
|
|
@@ -511,7 +537,7 @@ export const TOPICS = {
|
|
|
511
537
|
Local state: .frontpage/state.json { slug, baseCommitSha }
|
|
512
538
|
Pull BEFORE EDITING when tip may have moved (UI agent / another checkout) — check tipDrift.
|
|
513
539
|
Pull before push so CAS has baseCommitSha. Do not push while the editor agent is building.
|
|
514
|
-
--wait after push blocks until
|
|
540
|
+
--wait after push blocks until the new commit's site compile is live (agent idle, then GET /deploy ready).
|
|
515
541
|
The UI/canvas agent does not use this CLI; only CLI checkouts need pull.
|
|
516
542
|
Pushing module markup may auto-activate modules (same as the agent).
|
|
517
543
|
tokens.ts may be auto-repaired at commit if it would break the build.
|
|
@@ -520,12 +546,15 @@ export const TOPICS = {
|
|
|
520
546
|
Do not store secrets in the site tree (.env, *.pem, secrets/, credentials.json) — rejected by the API.
|
|
521
547
|
|
|
522
548
|
Staging vs production:
|
|
523
|
-
- Push/prompt/edit update
|
|
524
|
-
-
|
|
525
|
-
- frontpage deploy wait
|
|
526
|
-
-
|
|
549
|
+
- Push/prompt/edit update the git tip. The site compile is a second clock.
|
|
550
|
+
- Review STAGING at https://<subdomain>.frontpage.host after GET /deploy is ready.
|
|
551
|
+
- frontpage deploy wait = agent-run idle (if any) + tip compile ready. It is NOT go-live.
|
|
552
|
+
- A file-only push has no agent run. deploy wait / push --wait must still wait for the compile.
|
|
553
|
+
- frontpage publish promotes the last ready staging compile. 409 if the tip is not built yet.
|
|
554
|
+
- frontpage publish --wait waits for that compile, then publishes. Never publish unless asked.`,
|
|
527
555
|
"site-building": `Site building (Astro contract)
|
|
528
|
-
Stack: Astro 6 + Tailwind v4. Pages in src/pages/. Every page wraps <BaseLayout title description
|
|
556
|
+
Stack: Astro 6 + Tailwind v4. Pages in src/pages/. Every page wraps <BaseLayout title description>,
|
|
557
|
+
or a site layout that itself wraps BaseLayout.
|
|
529
558
|
Control files: src/config/tokens.ts (design tokens), src/config/site.ts (title, nav, pages map, seo).
|
|
530
559
|
|
|
531
560
|
Pages map:
|
|
@@ -538,7 +567,10 @@ export const TOPICS = {
|
|
|
538
567
|
- No API routes, serverless backends, or databases.
|
|
539
568
|
- No fake lead forms / checkout / card fields — use data-fp-* modules + frontpage modules, or refuse.
|
|
540
569
|
- Platform-owned paths are not syncable (BaseLayout overlay, module/extension shells, package.json…).
|
|
541
|
-
- Customer layouts (DocsLayout, GuideLayout, …) are fine when site-owned.
|
|
570
|
+
- Customer layouts (DocsLayout, GuideLayout, …) are fine when site-owned. A wrapper MUST render
|
|
571
|
+
<BaseLayout> and forward SEO: description={description}, ogImage={ogImage}, and
|
|
572
|
+
title={seoTitle ?? derivedFallback} from a 1:1 Astro.props destructure. NEVER title={pageTitle}
|
|
573
|
+
with no seoTitle alias (locks Title/Share image in Search & Social for every page on that layout).
|
|
542
574
|
|
|
543
575
|
Tokens & fonts:
|
|
544
576
|
- Keep required colors/fonts/spacing/radii keys; style via CSS variables / token Tailwind.
|
|
@@ -556,7 +588,7 @@ export const TOPICS = {
|
|
|
556
588
|
Modules & articles (short):
|
|
557
589
|
- Forms: <form data-fp-form="slug"> with named inputs; no action/onsubmit/honeypot.
|
|
558
590
|
- Articles: frontpage articles …; migrate folders via frontpage prompt (don't invent mass delete+write).
|
|
559
|
-
- Media: frontpage media upload (images/PDF ≤25 MB, video ≤50 MB). Use sitePath /__media/uploads/<slug>/… in markup (not raw blob URLs). Optional ?w= on rasters only.
|
|
591
|
+
- Media: frontpage media upload (images/PDF ≤25 MB, video ≤50 MB). frontpage images generate --prompt "…" [--count N] for 1–4 AI variations of one invented picture (AI $ + storage). Use sitePath /__media/uploads/<slug>/… in markup (not raw blob URLs). Optional ?w= on rasters only.
|
|
560
592
|
|
|
561
593
|
Full guide: https://frontpage.host/developers/site-building
|
|
562
594
|
See also: frontpage help sync | content | AGENTS.md`,
|
|
@@ -568,8 +600,8 @@ export const TOPICS = {
|
|
|
568
600
|
frontpage runs wait [runId] # failed/canceled → exit 2; JSON ok:false + run.error
|
|
569
601
|
frontpage runs cancel <runId>
|
|
570
602
|
frontpage runs retry <runId>
|
|
571
|
-
frontpage deploy wait #
|
|
572
|
-
frontpage publish
|
|
603
|
+
frontpage deploy wait # agent idle + tip compile ready; fail-closed; NOT publish
|
|
604
|
+
frontpage publish [--wait] # ONLY if the user asked — promotes the ready compile
|
|
573
605
|
frontpage unpublish --yes # admin only; never unless asked; --yes when non-TTY
|
|
574
606
|
frontpage usage
|
|
575
607
|
frontpage analytics [--days 7]
|
|
@@ -589,6 +621,7 @@ export const TOPICS = {
|
|
|
589
621
|
Do not publish unless asked.`,
|
|
590
622
|
content: `Content
|
|
591
623
|
frontpage media list|upload <file> # images/PDF/video; text/CSV = inline not library; sitePath first when stored
|
|
624
|
+
frontpage images generate --prompt "…" [--count N] # 1–4 AI variations of one prompt; sitePath first; AI $ + storage
|
|
592
625
|
frontpage inbox [leads|subscribers|orders|google_reviews|…] [--mark-read] [--csv]
|
|
593
626
|
frontpage seo get [/path|--path /path] | seo set --path /
|
|
594
627
|
frontpage articles list|content|create-section|create-article|set-draft|…
|
package/llms.txt
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- Fonts: frontpage fonts [--slot] [--query] → exact value → tokens set fonts.<role> → APPLY font-sans|serif|mono|display. Build bundles catalog faces from tokens.ts and/or CSS font-family contexts (not prose). Inline-only without tokens works for catalog faces after rebuild; prefer tokens+utilities for brand roles.
|
|
9
9
|
- Module/extension docs: frontpage modules docs <key> | frontpage extensions docs <key>
|
|
10
10
|
- Search flags: --regex --case-sensitive --whole-word --files-only --context-lines --max-results
|
|
11
|
-
- Modules setup: wizard modules (payments/shop/ordering): frontpage modules <key> connect [--no-open] opens Frontpage (logged-in browser) then Stripe/Square; already-connected stays live. payments/shop connect = admin key; ordering = editor. payments disconnect 409s if shop still uses the shared Stripe account. request-setup remains valid for the editor amber bar. frontpage modules <key> prints that key's CLI usage. Index: https://frontpage.host/developers/modules. Typed config: frontpage modules callbar|forms|booking|signups get|set (forms set needs --instance; confirm emails paid-plan only). google_reviews: frontpage modules google_reviews connect --place-id | --maps-url (server resolve + seed; no Autocomplete; do not write placeId via config). Also status, list-reviews, hide/show, set-stars, set-sort, set-weight, reset-weights, refresh, disconnect. Docs: https://frontpage.host/developers/modules/google-reviews. Markup: data-fp-google-reviews / data-fp-google-ask + required data-fp-requires="google_reviews". Ask scores: frontpage inbox google_reviews.
|
|
11
|
+
- Modules setup: wizard modules (payments/shop/ordering): frontpage modules <key> connect [--no-open] opens Frontpage (logged-in browser) then Stripe/Square; already-connected stays live. payments/shop connect = admin key; ordering = editor. payments disconnect 409s if shop still uses the shared Stripe account. request-setup remains valid for the editor amber bar. frontpage modules <key> prints that key's CLI usage. Index: https://frontpage.host/developers/modules. Typed config: frontpage modules callbar|forms|booking|signups get|set (forms set needs --instance; confirm emails paid-plan only). Form destinations: frontpage modules forms destinations|connect-destination|attach-destination|detach-destination|remove-destination --yes|request-destination-setup (token = admin; request-destination-setup lights the editor bar; request-setup forms is a no-op; connect does not attach every form). Privyr: paste Incoming Webhook URL (https://www.privyr.com/api/v1/incoming-leads/…), not a token. HubSpot: Development → Keys → Service keys or Legacy apps → Auth → Show token. Pipedrive: profile → Company settings → Personal preferences → API. Docs: https://frontpage.host/developers/modules#forms-destinations. google_reviews: frontpage modules google_reviews connect --place-id | --maps-url (server resolve + seed; no Autocomplete; do not write placeId via config). Also status, list-reviews, hide/show, set-stars, set-sort, set-weight, reset-weights, refresh, disconnect. Docs: https://frontpage.host/developers/modules/google-reviews. Markup: data-fp-google-reviews / data-fp-google-ask + required data-fp-requires="google_reviews". Ask scores: frontpage inbox google_reviews.
|
|
12
12
|
- Analytics ops: frontpage analytics summary|query|funnel|timeseries|compare|sql (bare analytics = dashboard)
|
|
13
13
|
- Media: frontpage media upload <file> (images/PDF ≤25 MB, video ≤50 MB, SVG/text ≤4 MB); human prints sitePath first — use /__media/uploads/<slug>/… in markup. Text/CSV is inline only (not media library; --json omits full content).
|
|
14
14
|
- Prompt attachments: frontpage prompt "…" --attach <file|/__media/…|https://…> [--asset-url https://…] (same media caps as media upload)
|
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
- SEO get: frontpage seo get [/path|--path /path]; search path filter: [path|--path <path>]; open [/path|--path /path]
|
|
17
17
|
- Analytics funnel: --steps JSON array or comma paths (/, /pricing); bad JSON → Invalid JSON for --steps
|
|
18
18
|
- Modules: unknown key errors list Known: …; mailing list module key is signups (not subscribe)
|
|
19
|
-
- 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 fail-closed; push --wait
|
|
19
|
+
- 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
|
|
20
20
|
- Auth: mint fp_live_ key in Frontpage UI (Developers); frontpage login KEY or FRONTPAGE_API_KEY
|
|
21
21
|
- Site id: subdomain only (frontpage use <subdomain>). Not custom domains.
|
|
22
|
-
-
|
|
23
|
-
- Production:
|
|
22
|
+
- Three clocks: git tip (push/prompt/edit) ≠ site compile (GET /deploy ready) ≠ editor agent run (runs wait). File-only push has no agent run.
|
|
23
|
+
- Production: frontpage publish promotes the last ready compile (409 if tip not built). frontpage publish --wait waits then publishes. NEVER publish/unpublish unless the user explicitly asks. Verify live <meta name="frontpage:commit">.
|
|
24
24
|
- Sync: allowlist src/ public/ content/; platform overlay + secret-like paths rejected; pull before edit if tip may have moved (status/diff tipDrift); pull → edit → diff (blobSha) → push [--wait] (CAS); no push during active agent run; UI agent does not use CLI pull; review on subdomain
|
|
25
25
|
- Machine: global --json / --quiet; exit 0/2/3/4/5 (see help exit-codes)
|
|
26
|
-
- deploy wait =
|
|
26
|
+
- deploy wait = agent-run idle (if any) + tip compile ready; it is NOT go-live / publish
|
|
27
27
|
- No runs continue, domains, billing, members, MCP, or git remotes in v1
|