frontpage-host 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +30 -19
- package/README.md +3 -3
- package/dist/bin-path.d.ts +21 -0
- package/dist/bin-path.js +65 -0
- package/dist/bin.js +9 -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/people.d.ts +1 -0
- package/dist/commands/people.js +239 -0
- package/dist/commands/publish.js +73 -11
- package/dist/commands/push.d.ts +5 -0
- package/dist/commands/push.js +18 -13
- package/dist/commands/redirects.d.ts +1 -0
- package/dist/commands/redirects.js +83 -0
- 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 +22 -0
- package/dist/commands/wait-site-compile.js +98 -0
- package/dist/compile.d.ts +46 -0
- package/dist/compile.js +99 -0
- package/dist/completions/generate.js +14 -3
- package/dist/modules/commands.js +24 -1
- package/dist/modules/forms_destinations.d.ts +3 -0
- package/dist/modules/forms_destinations.js +147 -0
- package/dist/modules/typed_config.js +21 -9
- package/dist/registry.js +103 -22
- package/llms.txt +8 -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
|
|
|
@@ -88,34 +93,39 @@ Rules:
|
|
|
88
93
|
2. **Pull before push** so `baseCommitSha` is set (CAS). Still required even if you already pulled to edit.
|
|
89
94
|
3. **Do not push** while an agent run is active (`status` / `runs`).
|
|
90
95
|
4. Push updates **staging** (subdomain). Production is unchanged until someone runs `publish`.
|
|
91
|
-
5.
|
|
92
|
-
6.
|
|
93
|
-
7.
|
|
94
|
-
8.
|
|
96
|
+
5. A local delete of an allowlisted path is part of the next push (remote-only files are sent as deletes). `frontpage diff` shows them as `D`.
|
|
97
|
+
6. Pushing module markup may auto-activate modules (same as the editor agent).
|
|
98
|
+
7. Binaries use base64; do not UTF-8 round-trip them.
|
|
99
|
+
8. After push, wait for the site compile (`frontpage push --wait` or `frontpage deploy wait`) before reviewing or publishing. Verify `<meta name="frontpage:commit">` when checking production.
|
|
100
|
+
9. The **UI / canvas agent does not use this CLI** — it edits the live tip in-process. Only CLI checkouts need `pull`.
|
|
95
101
|
|
|
96
102
|
## Site contract (Astro)
|
|
97
103
|
|
|
98
104
|
Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or https://frontpage.host/developers/site-building
|
|
99
105
|
|
|
100
|
-
1. **Every page** wraps content in `<BaseLayout title="…" description="…">` (import from `src/layouts/BaseLayout.astro`).
|
|
106
|
+
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
107
|
2. **`src/config/site.ts`** — keep `pages` (path registry) and `nav` in sync when adding/renaming/deleting page files. Tab title comes from BaseLayout, not the `pages` map label.
|
|
102
108
|
3. **One file per URL** for similar page sets (services, guides, …). Do not default to `getStaticPaths()` over `src/data/*.ts`. Blog/news → `frontpage articles`, not a hand-rolled catalog.
|
|
103
109
|
4. **No backends / databases / fake forms or checkout.** Real forms use `<form data-fp-form="slug">` + `frontpage modules`. Refuse or use modules for payments.
|
|
104
|
-
5. **
|
|
105
|
-
6. **
|
|
106
|
-
7. **
|
|
107
|
-
8. **
|
|
110
|
+
5. **Quoted attributes vs frontmatter.** In frontmatter JavaScript, a backslash-quote is an escape. In an Astro attribute it is not — the quote after `href=` closes the string and the next token fails the compile. Put HTML in a frontmatter const and pass it as an expression (`auditBody={auditBody}`), or use an expression attribute. Never put an HTML tag or a backslash-quote inside a quoted JSX attribute.
|
|
111
|
+
6. **Tokens** in `src/config/tokens.ts` — keep required color/font/spacing/radii keys; style via CSS variables / token Tailwind. **Fonts load rule:** the build bundles catalog faces when the family string appears in `tokens.ts` and/or in a real CSS `font-family` / `fontFamily` / `font-['…']` context under `src/pages|components|styles` — not from prose alone. Preferred path: `frontpage fonts` → exact `value` → `frontpage tokens set --group fonts --key <role> --value "…"` → **APPLY** `font-sans` / `font-serif` / `font-mono` / `font-display` (or `var(--font-*)`) on elements. Inline catalog `font-family` accents can bundle after rebuild, but brand roles still prefer tokens + utilities. Off-catalog families silently fall back to system fonts (push may warn). Module how-tos: `frontpage modules docs <key>` / `frontpage extensions docs <key>`.
|
|
112
|
+
7. **Shared chrome** — prefer `src/components/Header.astro` + `Footer.astro` (or `chrome: { managed: true }` in `site.ts`).
|
|
113
|
+
8. **404** — customize `src/pages/404.astro` if needed; keep `frontpage:404` meta; never register `/404` in `nav`/`pages`.
|
|
114
|
+
9. **Redirects** — real 301/308s are `frontpage redirects list|set|rm`, not site files. `set --from /old --to /new` is instant (no push/build). Do not ship `Astro.redirect`, `location.replace`, or meta refresh for a permanent move. `frontpage revert` does not undo redirects; `redirects rm` does.
|
|
115
|
+
10. **Platform-owned paths** stay off-limits (BaseLayout overlay, module/extension shells, `package.json`, …). Customer layouts like `DocsLayout` / `GuideLayout` are OK.
|
|
108
116
|
|
|
109
117
|
## Modules & articles
|
|
110
118
|
|
|
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>
|
|
119
|
+
- 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.
|
|
120
|
+
- 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
121
|
- 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
122
|
- 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
123
|
- Migrate a folder into Articles: prefer `frontpage prompt "migrate … into articles"` — do not invent a mass delete+write migration.
|
|
116
124
|
- Extensions: `frontpage extensions`; if LOCKED on plan, do not fake charts/etc.
|
|
117
125
|
- Media: `frontpage media upload <file>` — images/PDF ≤25 MB, video (MP4/WebM/MOV) ≤50 MB, SVG/text ≤4 MB (sanitizing proxy). Human output prints `sitePath:` first (use that in markup — never raw blob hosts), then optional `url:`. Optional `?w=` on rasters only. **Text/CSV** uploads are inline only — **not** added to the media library and **cannot** be linked via `/__media` (`media upload --json` omits full `content`; use `inbox … --csv` for submission exports).
|
|
126
|
+
- Generated images: `frontpage images generate --prompt "…"` `[--count N]` — one prompt, 1–4 variations of that same picture. Prints `sitePath:` first. Counts against AI $ and storage. Flag-off environments return 404 (`Image generation is not enabled.`). Do not call xAI / Imagine yourself.
|
|
118
127
|
- Analytics: bare `frontpage analytics` = dashboard; also `summary|query|funnel|timeseries|compare|sql` for structured reads (SQL is guard + RLS site-scoped).
|
|
128
|
+
- People: `frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide`. Site-scoped person graph (email or phone). Inbox stays the raw ledger. Hide is not forget.
|
|
119
129
|
|
|
120
130
|
## Agent prompt loop
|
|
121
131
|
|
|
@@ -134,8 +144,9 @@ frontpage runs wait # wait for the agent run to finish (failed/canceled
|
|
|
134
144
|
- Caps: images/PDF ≤25 MB, video ≤50 MB, SVG/text via proxy ≤4 MB (large text/data Blob upload is editor-only).
|
|
135
145
|
- Prompt text is still required (API). Attachments ride on the same enqueue as the editor.
|
|
136
146
|
- `runs wait` with `--json`: failures use `ok: false` and include `data.run` (with `run.error` when present).
|
|
137
|
-
- `
|
|
138
|
-
- `
|
|
147
|
+
- `runs wait` watches an editor agent run only.
|
|
148
|
+
- `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.
|
|
149
|
+
- `frontpage push --wait` does the same compile wait for the SHA just pushed. A file-only push still waits for the compile.
|
|
139
150
|
|
|
140
151
|
Mid-task continuation is automatic. There is **no** `runs continue` in v1.
|
|
141
152
|
|
|
@@ -217,7 +228,7 @@ Safe default after finishing work: summarize what changed and give the staging U
|
|
|
217
228
|
6. **Active-run guard.** Push/revert refuse while an editor agent run is active (exit 3).
|
|
218
229
|
7. **CAS.** Tip moved since pull → exit 3; pull and retry.
|
|
219
230
|
8. **No invented v1 commands:** `domains*`, `billing*`, `members*`, `suggestions*`, `sites delete|restore`, `runs continue`, MCP, git remotes / clone URLs.
|
|
220
|
-
9. **`deploy wait` ≠ publish.**
|
|
231
|
+
9. **`deploy wait` ≠ publish.** It waits for the tip compile. Production still requires `publish`. Verify live HTML with `<meta name="frontpage:commit">`.
|
|
221
232
|
10. **Honesty on stubs.** `open --prod` is currently a no-op stub (still opens the subdomain).
|
|
222
233
|
|
|
223
234
|
## 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
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type PathFrontpageVerdict = {
|
|
2
|
+
stale: boolean;
|
|
3
|
+
path: string | null;
|
|
4
|
+
pathVersion: string | null;
|
|
5
|
+
reason: "older-than-compile-wait" | "older-than-this-package" | "unreadable" | null;
|
|
6
|
+
};
|
|
7
|
+
export declare function resolveRealPath(path: string): string;
|
|
8
|
+
export declare function interpretPathFrontpage(args: {
|
|
9
|
+
thisVersion: string;
|
|
10
|
+
thisBin: string | null;
|
|
11
|
+
pathBin: string | null;
|
|
12
|
+
pathVersionLine: string | null;
|
|
13
|
+
minVersion?: string;
|
|
14
|
+
}): PathFrontpageVerdict;
|
|
15
|
+
export declare function whichFrontpage(): string | null;
|
|
16
|
+
export declare function frontpageVersionAt(bin: string): string | null;
|
|
17
|
+
export declare function inspectPathFrontpage(opts?: {
|
|
18
|
+
thisBin?: string | null;
|
|
19
|
+
thisVersion?: string;
|
|
20
|
+
}): PathFrontpageVerdict;
|
|
21
|
+
export declare function stalePathUpgradeMessage(v: PathFrontpageVerdict): string;
|
package/dist/bin-path.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect a stale `frontpage` on PATH (global 0.1.9 vs this package).
|
|
3
|
+
* Does not uninstall anything — doctor prints the uninstall command.
|
|
4
|
+
*/
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { realpathSync } from "node:fs";
|
|
7
|
+
import { COMPILE_WAIT_MIN_VERSION, isOlderThan, packageIdentity, parseVersionLine, } from "./version.js";
|
|
8
|
+
export function resolveRealPath(path) {
|
|
9
|
+
try {
|
|
10
|
+
return realpathSync(path);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return path;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function interpretPathFrontpage(args) {
|
|
17
|
+
if (!args.pathBin) {
|
|
18
|
+
return { stale: false, path: null, pathVersion: null, reason: null };
|
|
19
|
+
}
|
|
20
|
+
if (args.thisBin && resolveRealPath(args.pathBin) === resolveRealPath(args.thisBin)) {
|
|
21
|
+
return { stale: false, path: args.pathBin, pathVersion: args.thisVersion, reason: null };
|
|
22
|
+
}
|
|
23
|
+
const pathVersion = parseVersionLine(args.pathVersionLine ?? "");
|
|
24
|
+
if (!pathVersion) {
|
|
25
|
+
return { stale: true, path: args.pathBin, pathVersion: null, reason: "unreadable" };
|
|
26
|
+
}
|
|
27
|
+
const floor = args.minVersion ?? COMPILE_WAIT_MIN_VERSION;
|
|
28
|
+
if (isOlderThan(pathVersion, floor)) {
|
|
29
|
+
return { stale: true, path: args.pathBin, pathVersion, reason: "older-than-compile-wait" };
|
|
30
|
+
}
|
|
31
|
+
if (isOlderThan(pathVersion, args.thisVersion)) {
|
|
32
|
+
return { stale: true, path: args.pathBin, pathVersion, reason: "older-than-this-package" };
|
|
33
|
+
}
|
|
34
|
+
return { stale: false, path: args.pathBin, pathVersion, reason: null };
|
|
35
|
+
}
|
|
36
|
+
export function whichFrontpage() {
|
|
37
|
+
const r = spawnSync("sh", ["-c", "command -v frontpage"], { encoding: "utf8" });
|
|
38
|
+
const line = (r.stdout ?? "").trim().split("\n")[0]?.trim();
|
|
39
|
+
return line || null;
|
|
40
|
+
}
|
|
41
|
+
export function frontpageVersionAt(bin) {
|
|
42
|
+
const r = spawnSync(bin, ["--version"], { encoding: "utf8" });
|
|
43
|
+
const text = `${r.stdout ?? ""}\n${r.stderr ?? ""}`;
|
|
44
|
+
return parseVersionLine(text);
|
|
45
|
+
}
|
|
46
|
+
export function inspectPathFrontpage(opts) {
|
|
47
|
+
const thisVersion = opts?.thisVersion ?? packageIdentity().version;
|
|
48
|
+
const thisBin = opts?.thisBin ?? process.argv[1] ?? null;
|
|
49
|
+
const pathBin = whichFrontpage();
|
|
50
|
+
const pathVersionLine = pathBin ? frontpageVersionAt(pathBin) : null;
|
|
51
|
+
return interpretPathFrontpage({
|
|
52
|
+
thisVersion,
|
|
53
|
+
thisBin,
|
|
54
|
+
pathBin,
|
|
55
|
+
pathVersionLine,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function stalePathUpgradeMessage(v) {
|
|
59
|
+
const found = v.pathVersion ? `frontpage-host ${v.pathVersion}` : "an unknown frontpage-host";
|
|
60
|
+
const where = v.path ? ` at ${v.path}` : "";
|
|
61
|
+
return (`PATH frontpage is ${found}${where}. Compile-wait needs frontpage-host ${COMPILE_WAIT_MIN_VERSION} or newer. ` +
|
|
62
|
+
`Uninstall the stale global (npm uninstall -g frontpage-host) and install ` +
|
|
63
|
+
`npm i -g frontpage-host@${COMPILE_WAIT_MIN_VERSION} (or npx frontpage-host@${COMPILE_WAIT_MIN_VERSION}). ` +
|
|
64
|
+
`In the Frontpage repo, run node <repo>/cli/dist/bin.js instead of a global binary.`);
|
|
65
|
+
}
|
package/dist/bin.js
CHANGED
|
@@ -24,8 +24,11 @@ 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";
|
|
30
|
+
import { runRedirects } from "./commands/redirects.js";
|
|
31
|
+
import { runPeople } from "./commands/people.js";
|
|
29
32
|
import { runArticles } from "./commands/articles.js";
|
|
30
33
|
import { runSettings } from "./commands/settings.js";
|
|
31
34
|
import { runEdit } from "./commands/edit.js";
|
|
@@ -107,10 +110,16 @@ async function main() {
|
|
|
107
110
|
return runExtensions(args);
|
|
108
111
|
case "media":
|
|
109
112
|
return runMedia(args);
|
|
113
|
+
case "images":
|
|
114
|
+
return runImages(args);
|
|
110
115
|
case "inbox":
|
|
111
116
|
return runInbox(args);
|
|
117
|
+
case "people":
|
|
118
|
+
return runPeople(args);
|
|
112
119
|
case "seo":
|
|
113
120
|
return runSeo(args);
|
|
121
|
+
case "redirects":
|
|
122
|
+
return runRedirects(args);
|
|
114
123
|
case "articles":
|
|
115
124
|
return runArticles(args);
|
|
116
125
|
case "settings":
|
|
@@ -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
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runPeople(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { resolveSlug } from "../site.js";
|
|
2
|
+
import { CliHttpError, EXIT_OK, EXIT_USAGE, exitCodeForError, v1Delete, v1Get, v1Patch, v1Post, } from "../http/client.js";
|
|
3
|
+
import { assertNoUnknownFlags, hasFlag, takeFlag } from "../argv.js";
|
|
4
|
+
import { emitErr, emitOk, isJson } from "../runtime.js";
|
|
5
|
+
const USAGE = "Usage: frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide [--site <slug>] [--json]";
|
|
6
|
+
function requireId(work) {
|
|
7
|
+
const fromFlag = takeFlag(work, "--id");
|
|
8
|
+
if (fromFlag)
|
|
9
|
+
return fromFlag;
|
|
10
|
+
const positional = work.find((a) => !a.startsWith("-"));
|
|
11
|
+
if (positional) {
|
|
12
|
+
work.splice(work.indexOf(positional), 1);
|
|
13
|
+
return positional;
|
|
14
|
+
}
|
|
15
|
+
throw new CliHttpError(`Person id is required. Run: frontpage help people`, EXIT_USAGE, 0, "bad_request");
|
|
16
|
+
}
|
|
17
|
+
function displayName(row) {
|
|
18
|
+
return row.name || row.email || row.phone || row.id;
|
|
19
|
+
}
|
|
20
|
+
function printPerson(row) {
|
|
21
|
+
const bits = [row.status ?? "—", displayName(row)];
|
|
22
|
+
if (row.email && row.email !== row.name)
|
|
23
|
+
bits.push(row.email);
|
|
24
|
+
if (row.phone && row.phone !== row.name)
|
|
25
|
+
bits.push(row.phone);
|
|
26
|
+
if (row.followUpAt)
|
|
27
|
+
bits.push(`follow-up ${row.followUpAt}`);
|
|
28
|
+
if (row.hiddenAt)
|
|
29
|
+
bits.push("hidden");
|
|
30
|
+
if (row.lastEvent?.summary)
|
|
31
|
+
bits.push(row.lastEvent.summary);
|
|
32
|
+
console.log(`${row.id} ${bits.join(" · ")}`);
|
|
33
|
+
}
|
|
34
|
+
export async function runPeople(args) {
|
|
35
|
+
try {
|
|
36
|
+
const { slug, rest } = await resolveSlug(args);
|
|
37
|
+
const sub = rest[0];
|
|
38
|
+
const base = `/api/v1/sites/${encodeURIComponent(slug)}/people`;
|
|
39
|
+
if (!sub || sub === "list") {
|
|
40
|
+
const work = sub === "list" ? rest.slice(1) : [...rest];
|
|
41
|
+
const q = takeFlag(work, "--q") ?? takeFlag(work, "--query");
|
|
42
|
+
const status = takeFlag(work, "--status");
|
|
43
|
+
const source = takeFlag(work, "--source");
|
|
44
|
+
const formSlug = takeFlag(work, "--form-slug");
|
|
45
|
+
const followUp = takeFlag(work, "--follow-up");
|
|
46
|
+
const sort = takeFlag(work, "--sort");
|
|
47
|
+
const lastSeenDays = takeFlag(work, "--last-seen-days");
|
|
48
|
+
const limit = takeFlag(work, "--limit");
|
|
49
|
+
const cursor = takeFlag(work, "--cursor");
|
|
50
|
+
const hidden = hasFlag(work, "--hidden");
|
|
51
|
+
assertNoUnknownFlags(work, "people");
|
|
52
|
+
const params = new URLSearchParams();
|
|
53
|
+
if (q)
|
|
54
|
+
params.set("q", q);
|
|
55
|
+
if (status)
|
|
56
|
+
params.set("status", status);
|
|
57
|
+
if (source)
|
|
58
|
+
params.set("source", source);
|
|
59
|
+
if (formSlug)
|
|
60
|
+
params.set("formSlug", formSlug);
|
|
61
|
+
if (followUp)
|
|
62
|
+
params.set("followUp", followUp);
|
|
63
|
+
if (sort)
|
|
64
|
+
params.set("sort", sort);
|
|
65
|
+
if (lastSeenDays)
|
|
66
|
+
params.set("lastSeenDays", lastSeenDays);
|
|
67
|
+
if (limit)
|
|
68
|
+
params.set("limit", limit);
|
|
69
|
+
if (cursor)
|
|
70
|
+
params.set("cursor", cursor);
|
|
71
|
+
if (hidden)
|
|
72
|
+
params.set("hidden", "1");
|
|
73
|
+
const qs = params.toString();
|
|
74
|
+
const data = await v1Get(qs ? `${base}?${qs}` : base);
|
|
75
|
+
if (isJson()) {
|
|
76
|
+
emitOk("people", data);
|
|
77
|
+
return EXIT_OK;
|
|
78
|
+
}
|
|
79
|
+
const rows = data.people ?? [];
|
|
80
|
+
if (rows.length === 0) {
|
|
81
|
+
console.log(hidden ? "No hidden people." : "No people.");
|
|
82
|
+
return EXIT_OK;
|
|
83
|
+
}
|
|
84
|
+
for (const row of rows)
|
|
85
|
+
printPerson(row);
|
|
86
|
+
if (data.nextCursor)
|
|
87
|
+
console.log(`next: ${data.nextCursor}`);
|
|
88
|
+
return EXIT_OK;
|
|
89
|
+
}
|
|
90
|
+
if (sub === "get") {
|
|
91
|
+
const work = rest.slice(1);
|
|
92
|
+
const id = requireId(work);
|
|
93
|
+
assertNoUnknownFlags(work, "people");
|
|
94
|
+
const data = await v1Get(`${base}/${encodeURIComponent(id)}`);
|
|
95
|
+
if (isJson()) {
|
|
96
|
+
emitOk("people", data);
|
|
97
|
+
return EXIT_OK;
|
|
98
|
+
}
|
|
99
|
+
printPerson(data.person);
|
|
100
|
+
const events = data.events ?? [];
|
|
101
|
+
for (const ev of events) {
|
|
102
|
+
const row = ev;
|
|
103
|
+
const extra = typeof row.payload?.body === "string" ? ` ${row.payload.body}` : "";
|
|
104
|
+
console.log(` ${row.occurredAt ?? ""} ${row.type ?? ""}${extra}`);
|
|
105
|
+
}
|
|
106
|
+
return EXIT_OK;
|
|
107
|
+
}
|
|
108
|
+
if (sub === "add") {
|
|
109
|
+
const work = rest.slice(1);
|
|
110
|
+
const email = takeFlag(work, "--email");
|
|
111
|
+
const phone = takeFlag(work, "--phone");
|
|
112
|
+
const name = takeFlag(work, "--name");
|
|
113
|
+
const note = takeFlag(work, "--note");
|
|
114
|
+
assertNoUnknownFlags(work, "people");
|
|
115
|
+
if (!email && !phone) {
|
|
116
|
+
console.error("Usage: frontpage people add --email you@example.com | --phone +15551234567 [--name …] [--note …]");
|
|
117
|
+
return EXIT_USAGE;
|
|
118
|
+
}
|
|
119
|
+
const data = await v1Post(base, {
|
|
120
|
+
email,
|
|
121
|
+
phone,
|
|
122
|
+
name,
|
|
123
|
+
note,
|
|
124
|
+
});
|
|
125
|
+
if (isJson()) {
|
|
126
|
+
emitOk("people", data);
|
|
127
|
+
return EXIT_OK;
|
|
128
|
+
}
|
|
129
|
+
printPerson(data.person);
|
|
130
|
+
return EXIT_OK;
|
|
131
|
+
}
|
|
132
|
+
if (sub === "set") {
|
|
133
|
+
const work = rest.slice(1);
|
|
134
|
+
const id = requireId(work);
|
|
135
|
+
const body = {};
|
|
136
|
+
for (const field of ["name", "email", "phone", "notes", "status"]) {
|
|
137
|
+
const v = takeFlag(work, `--${field}`);
|
|
138
|
+
if (v !== undefined)
|
|
139
|
+
body[field] = v === "" ? null : v;
|
|
140
|
+
}
|
|
141
|
+
assertNoUnknownFlags(work, "people");
|
|
142
|
+
if (Object.keys(body).length === 0) {
|
|
143
|
+
console.error("Usage: frontpage people set <id> [--name …] [--email …] [--phone …] [--notes …] [--status …]");
|
|
144
|
+
return EXIT_USAGE;
|
|
145
|
+
}
|
|
146
|
+
const data = await v1Patch(`${base}/${encodeURIComponent(id)}`, body);
|
|
147
|
+
if (isJson()) {
|
|
148
|
+
emitOk("people", data);
|
|
149
|
+
return EXIT_OK;
|
|
150
|
+
}
|
|
151
|
+
printPerson(data.person);
|
|
152
|
+
return EXIT_OK;
|
|
153
|
+
}
|
|
154
|
+
if (sub === "note") {
|
|
155
|
+
const work = rest.slice(1);
|
|
156
|
+
const id = requireId(work);
|
|
157
|
+
const body = takeFlag(work, "--body") ?? takeFlag(work, "--note");
|
|
158
|
+
assertNoUnknownFlags(work, "people");
|
|
159
|
+
if (!body) {
|
|
160
|
+
console.error("Usage: frontpage people note <id> --body \"…\"");
|
|
161
|
+
return EXIT_USAGE;
|
|
162
|
+
}
|
|
163
|
+
const data = await v1Post(`${base}/${encodeURIComponent(id)}/events`, { body });
|
|
164
|
+
if (isJson()) {
|
|
165
|
+
emitOk("people", data);
|
|
166
|
+
return EXIT_OK;
|
|
167
|
+
}
|
|
168
|
+
console.log(`Noted ${id} (${data.event.id})`);
|
|
169
|
+
return EXIT_OK;
|
|
170
|
+
}
|
|
171
|
+
if (sub === "rm-note") {
|
|
172
|
+
const work = rest.slice(1);
|
|
173
|
+
const id = requireId(work);
|
|
174
|
+
const eventId = takeFlag(work, "--event") ?? takeFlag(work, "--event-id");
|
|
175
|
+
assertNoUnknownFlags(work, "people");
|
|
176
|
+
if (!eventId) {
|
|
177
|
+
console.error("Usage: frontpage people rm-note <id> --event <eventId>");
|
|
178
|
+
return EXIT_USAGE;
|
|
179
|
+
}
|
|
180
|
+
const data = await v1Delete(`${base}/${encodeURIComponent(id)}/events/${encodeURIComponent(eventId)}`);
|
|
181
|
+
if (isJson()) {
|
|
182
|
+
emitOk("people", data);
|
|
183
|
+
return EXIT_OK;
|
|
184
|
+
}
|
|
185
|
+
console.log(`Removed note ${data.removed.id}`);
|
|
186
|
+
return EXIT_OK;
|
|
187
|
+
}
|
|
188
|
+
if (sub === "follow-up") {
|
|
189
|
+
const work = rest.slice(1);
|
|
190
|
+
const id = requireId(work);
|
|
191
|
+
const clear = hasFlag(work, "--clear");
|
|
192
|
+
const at = takeFlag(work, "--at");
|
|
193
|
+
assertNoUnknownFlags(work, "people");
|
|
194
|
+
if (!clear && !at) {
|
|
195
|
+
console.error("Usage: frontpage people follow-up <id> --at <iso> | --clear");
|
|
196
|
+
return EXIT_USAGE;
|
|
197
|
+
}
|
|
198
|
+
const data = await v1Patch(`${base}/${encodeURIComponent(id)}/follow-up`, { at: clear ? null : at });
|
|
199
|
+
if (isJson()) {
|
|
200
|
+
emitOk("people", data);
|
|
201
|
+
return EXIT_OK;
|
|
202
|
+
}
|
|
203
|
+
printPerson(data.person);
|
|
204
|
+
return EXIT_OK;
|
|
205
|
+
}
|
|
206
|
+
if (sub === "hide") {
|
|
207
|
+
const work = rest.slice(1);
|
|
208
|
+
const id = requireId(work);
|
|
209
|
+
assertNoUnknownFlags(work, "people");
|
|
210
|
+
const data = await v1Post(`${base}/${encodeURIComponent(id)}/hide`, {});
|
|
211
|
+
if (isJson()) {
|
|
212
|
+
emitOk("people", data);
|
|
213
|
+
return EXIT_OK;
|
|
214
|
+
}
|
|
215
|
+
console.log(`Hidden ${displayName(data.person)}`);
|
|
216
|
+
return EXIT_OK;
|
|
217
|
+
}
|
|
218
|
+
if (sub === "unhide") {
|
|
219
|
+
const work = rest.slice(1);
|
|
220
|
+
const id = requireId(work);
|
|
221
|
+
assertNoUnknownFlags(work, "people");
|
|
222
|
+
const data = await v1Post(`${base}/${encodeURIComponent(id)}/unhide`, {});
|
|
223
|
+
if (isJson()) {
|
|
224
|
+
emitOk("people", data);
|
|
225
|
+
return EXIT_OK;
|
|
226
|
+
}
|
|
227
|
+
console.log(`Restored ${displayName(data.person)}`);
|
|
228
|
+
return EXIT_OK;
|
|
229
|
+
}
|
|
230
|
+
console.error(USAGE);
|
|
231
|
+
return EXIT_USAGE;
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (isJson())
|
|
235
|
+
return emitErr("people", err);
|
|
236
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
237
|
+
return exitCodeForError(err);
|
|
238
|
+
}
|
|
239
|
+
}
|