frontpage-host 0.1.15 → 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 +29 -6
- package/dist/modules/forms_destinations.d.ts +3 -0
- package/dist/modules/forms_destinations.js +147 -0
- package/dist/modules/oauth.d.ts +8 -0
- package/dist/modules/oauth.js +25 -9
- package/dist/modules/typed_config.js +21 -9
- package/dist/registry.js +55 -22
- 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
|
|
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
|
+
};
|