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 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
- **How changes become visible on staging**
29
+ **Three clocks (do not collapse these)**
30
30
 
31
- - `frontpage push`, `frontpage prompt`, and `frontpage edit` update the site branch and the **staging** surface.
32
- - After a push or agent run finishes building, changes are on the site branch behind `https://<subdomain>.frontpage.host`.
33
- - There is **no separate “deploy to staging” step** agents must run. Do not treat `frontpage deploy wait` as “going live.” At most it waits for an in-flight build to finish before you open the subdomain to review.
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 current **staging tip** to **production** (custom domain / published site).
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. Prefer reviewing the subdomain after push; only use `deploy wait` if you need to block until a build finishes.
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
- - `deploy wait` is fail-closed: exit 2 if the waited run (or latest, when already idle) is `failed`/`canceled`.
138
- - `frontpage push --wait` waits for agent-run idle after push (same busy statuses; not a separate Vercel oracle).
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.** Optional build wait only; staging visibility is the subdomain.
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` (no separate staging-deploy step).
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` — wait until no run is in deploying/queued/running. */
1
+ /** `frontpage deploy wait` — agent-run idle, then the tip's site compile. */
2
2
  export declare function runDeploy(args: string[]): Promise<number>;
@@ -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 { emitWaitOutcome, waitForAgentIdle } from "./wait-agent-idle.js";
5
- /** `frontpage deploy wait` — wait until no run is in deploying/queued/running. */
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
- const result = await waitForAgentIdle({
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
+ }
@@ -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
  }
@@ -1,19 +1,77 @@
1
1
  import { resolveSlug } from "../site.js";
2
- import { EXIT_OK, EXIT_USAGE, v1Delete, v1Post } from "../http/client.js";
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 data = await v1Post(`/api/v1/sites/${encodeURIComponent(slug)}/publish`);
8
- if (isJson()) {
9
- emitOk("publish", { slug, productionUrl: data.productionUrl ?? null });
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
- else {
12
- console.log(`Published ${slug}`);
13
- if (data.productionUrl)
14
- console.log(data.productionUrl);
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
- return EXIT_OK;
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);
@@ -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 { emitWaitOutcome, waitForAgentIdle } from "./wait-agent-idle.js";
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: after push, wait for agent-run busy to clear (same statuses as deploy 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
- const result = await waitForAgentIdle({
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
- result,
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
- * §CLI Phase D — shared wait for agent-run busy states (queued/running/deploying).
3
- * Used by `deploy wait` and `push --wait`. Not a Vercel deploy oracle.
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: decided.run,
134
+ run: sanitized.run,
117
135
  observedBusy,
118
- status: decided.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 active deploy.");
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
+ };
@@ -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 && (\${COMP_WORDS[2]} == callbar || \${COMP_WORDS[2]} == forms || \${COMP_WORDS[2]} == booking || \${COMP_WORDS[2]} == signups) ]]; then
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] == callbar || $words[2] == forms || $words[2] == booking || $words[2] == signups ]]; then
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 callbar forms booking signups" -a "get set"`,
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"`,
@@ -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 === "callbar" || key === "forms" || key === "booking" || key === "signups") {
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
- 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
- console.log(`${id.padEnd(20)} ${status.padEnd(12)} ${label ? `${label} ` : ""}${email}`);
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 no agent run is queued/running/deploying (same as deploy wait; not a Vercel oracle)",
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 current staging tip to PRODUCTION (custom domain)",
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 an in-flight build to finish (not publish)",
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
- "Optional build wait only — staging is https://<subdomain>.frontpage.host",
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 (or latest, if already idle) is failed/canceled",
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. Review staging: https://<subdomain>.frontpage.host when that subdomain is publicly shareable.
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 # ONLY if the user asked — promotes to production
510
+ 9. frontpage publish [--wait] # ONLY if the user asked — promotes the ready compile
485
511
 
486
- Staging is the subdomain. There is no separate staging-deploy step for agents.
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 no agent run is queued/running/deploying (same as deploy wait).
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 STAGING: https://<subdomain>.frontpage.host
524
- - No separate "deploy to staging" step for agents — review the subdomain after the build.
525
- - frontpage deploy wait only waits for an in-flight build; it is NOT go-live.
526
- - frontpage publish promotes staging → production (custom domain). Never publish unless asked.`,
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 # optional build wait; fail-closed on failed/canceled; NOT publish
572
- frontpage publish # ONLY if the user asked — production promote
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
- - Staging: https://<subdomain>.frontpage.host — push/prompt/edit update staging; no separate staging deploy step. With a custom domain, subdomain may be owner-private (login wall ≠ failed push); public review is published custom domain.
23
- - Production: custom domain after frontpage publish — NEVER publish/unpublish unless the user explicitly asks
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 = optional build wait only (fail-closed on failed/canceled); it is NOT go-live / publish
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontpage-host",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Frontpage CLI — manage your site from the terminal",
5
5
  "type": "module",
6
6
  "bin": {