frontpage-host 0.1.17 → 0.1.19
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 +15 -8
- package/dist/bin-path.d.ts +21 -0
- package/dist/bin-path.js +65 -0
- package/dist/bin.js +12 -0
- package/dist/commands/mail.d.ts +1 -0
- package/dist/commands/mail.js +482 -0
- package/dist/commands/people.d.ts +1 -0
- package/dist/commands/people.js +341 -0
- package/dist/commands/publish.js +14 -10
- package/dist/commands/push.d.ts +5 -0
- package/dist/commands/push.js +13 -2
- package/dist/commands/redirects.d.ts +1 -0
- package/dist/commands/redirects.js +83 -0
- package/dist/commands/shop-ids.d.ts +3 -0
- package/dist/commands/shop-ids.js +3 -0
- package/dist/commands/shop.d.ts +1 -0
- package/dist/commands/shop.js +595 -0
- package/dist/commands/wait-site-compile.d.ts +1 -0
- package/dist/commands/wait-site-compile.js +14 -10
- package/dist/compile.d.ts +5 -0
- package/dist/compile.js +30 -2
- package/dist/completions/generate.js +3 -0
- package/dist/modules/commands.js +4 -4
- package/dist/registry.js +108 -3
- package/llms.txt +4 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
This file ships inside the `frontpage-host` npm package. Prefer it over scraping the product website.
|
|
4
4
|
|
|
5
|
+
The public CLI operates on Scale, Enterprise, and Internal sites. Site-scoped commands return 403 on lower tiers (`The CLI is included on Scale.`). `frontpage sites` still lists sites the key owner belongs to.
|
|
6
|
+
|
|
5
7
|
## Install and invoke
|
|
6
8
|
|
|
7
9
|
| Item | Value |
|
|
@@ -93,10 +95,11 @@ Rules:
|
|
|
93
95
|
2. **Pull before push** so `baseCommitSha` is set (CAS). Still required even if you already pulled to edit.
|
|
94
96
|
3. **Do not push** while an agent run is active (`status` / `runs`).
|
|
95
97
|
4. Push updates **staging** (subdomain). Production is unchanged until someone runs `publish`.
|
|
96
|
-
5.
|
|
97
|
-
6.
|
|
98
|
-
7.
|
|
99
|
-
8.
|
|
98
|
+
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`.
|
|
99
|
+
6. Pushing module markup may auto-activate modules (same as the editor agent).
|
|
100
|
+
7. Binaries use base64; do not UTF-8 round-trip them.
|
|
101
|
+
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.
|
|
102
|
+
9. The **UI / canvas agent does not use this CLI** — it edits the live tip in-process. Only CLI checkouts need `pull`.
|
|
100
103
|
|
|
101
104
|
## Site contract (Astro)
|
|
102
105
|
|
|
@@ -106,10 +109,12 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
|
|
|
106
109
|
2. **`src/config/site.ts`** — keep `pages` (path registry) and `nav` in sync when adding/renaming/deleting page files. Tab title comes from BaseLayout, not the `pages` map label.
|
|
107
110
|
3. **One file per URL** for similar page sets (services, guides, …). Do not default to `getStaticPaths()` over `src/data/*.ts`. Blog/news → `frontpage articles`, not a hand-rolled catalog.
|
|
108
111
|
4. **No backends / databases / fake forms or checkout.** Real forms use `<form data-fp-form="slug">` + `frontpage modules`. Refuse or use modules for payments.
|
|
109
|
-
5. **
|
|
110
|
-
6. **
|
|
111
|
-
7. **
|
|
112
|
-
8. **
|
|
112
|
+
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.
|
|
113
|
+
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>`.
|
|
114
|
+
7. **Shared chrome** — prefer `src/components/Header.astro` + `Footer.astro` (or `chrome: { managed: true }` in `site.ts`).
|
|
115
|
+
8. **404** — customize `src/pages/404.astro` if needed; keep `frontpage:404` meta; never register `/404` in `nav`/`pages`.
|
|
116
|
+
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.
|
|
117
|
+
10. **Platform-owned paths** stay off-limits (BaseLayout overlay, module/extension shells, `package.json`, …). Customer layouts like `DocsLayout` / `GuideLayout` are OK.
|
|
113
118
|
|
|
114
119
|
## Modules & articles
|
|
115
120
|
|
|
@@ -122,6 +127,8 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
|
|
|
122
127
|
- 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
128
|
- Generated images: `frontpage images generate --prompt "…"` `[--count N]` — one prompt, 1–4 variations of that same picture. Prints `sitePath:` first. Counts against AI $ and storage. Flag-off environments return 404 (`Image generation is not enabled.`). Do not call xAI / Imagine yourself.
|
|
124
129
|
- Analytics: bare `frontpage analytics` = dashboard; also `summary|query|funnel|timeseries|compare|sql` for structured reads (SQL is guard + RLS site-scoped).
|
|
130
|
+
- People CRM: `frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide|tag|export`. Site-scoped person graph (email or phone). Inbox stays the raw ledger. Hide is not forget. List export is CSV (not a one-person legal dump). Full guide: https://frontpage.host/developers/people
|
|
131
|
+
- Store catalog: `frontpage shop items|variants|settings|orders|payments`. Integer cents only (`$49.99` is `4999`). Stock `--add` / `--remove` become signed delta (`added 5` / `removed 5` / `set to 20` / `stockNow`). Connect stays `frontpage modules shop|payments connect`. Square `ordering` is a different module. Full guide: https://frontpage.host/developers/shop
|
|
125
132
|
|
|
126
133
|
## Agent prompt loop
|
|
127
134
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type PathFrontpageVerdict = {
|
|
2
|
+
stale: boolean;
|
|
3
|
+
path: string | null;
|
|
4
|
+
pathVersion: string | null;
|
|
5
|
+
reason: "older-than-compile-wait" | "older-than-this-package" | "unreadable" | null;
|
|
6
|
+
};
|
|
7
|
+
export declare function resolveRealPath(path: string): string;
|
|
8
|
+
export declare function interpretPathFrontpage(args: {
|
|
9
|
+
thisVersion: string;
|
|
10
|
+
thisBin: string | null;
|
|
11
|
+
pathBin: string | null;
|
|
12
|
+
pathVersionLine: string | null;
|
|
13
|
+
minVersion?: string;
|
|
14
|
+
}): PathFrontpageVerdict;
|
|
15
|
+
export declare function whichFrontpage(): string | null;
|
|
16
|
+
export declare function frontpageVersionAt(bin: string): string | null;
|
|
17
|
+
export declare function inspectPathFrontpage(opts?: {
|
|
18
|
+
thisBin?: string | null;
|
|
19
|
+
thisVersion?: string;
|
|
20
|
+
}): PathFrontpageVerdict;
|
|
21
|
+
export declare function stalePathUpgradeMessage(v: PathFrontpageVerdict): string;
|
package/dist/bin-path.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect a stale `frontpage` on PATH (global 0.1.9 vs this package).
|
|
3
|
+
* Does not uninstall anything — doctor prints the uninstall command.
|
|
4
|
+
*/
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { realpathSync } from "node:fs";
|
|
7
|
+
import { COMPILE_WAIT_MIN_VERSION, isOlderThan, packageIdentity, parseVersionLine, } from "./version.js";
|
|
8
|
+
export function resolveRealPath(path) {
|
|
9
|
+
try {
|
|
10
|
+
return realpathSync(path);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return path;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function interpretPathFrontpage(args) {
|
|
17
|
+
if (!args.pathBin) {
|
|
18
|
+
return { stale: false, path: null, pathVersion: null, reason: null };
|
|
19
|
+
}
|
|
20
|
+
if (args.thisBin && resolveRealPath(args.pathBin) === resolveRealPath(args.thisBin)) {
|
|
21
|
+
return { stale: false, path: args.pathBin, pathVersion: args.thisVersion, reason: null };
|
|
22
|
+
}
|
|
23
|
+
const pathVersion = parseVersionLine(args.pathVersionLine ?? "");
|
|
24
|
+
if (!pathVersion) {
|
|
25
|
+
return { stale: true, path: args.pathBin, pathVersion: null, reason: "unreadable" };
|
|
26
|
+
}
|
|
27
|
+
const floor = args.minVersion ?? COMPILE_WAIT_MIN_VERSION;
|
|
28
|
+
if (isOlderThan(pathVersion, floor)) {
|
|
29
|
+
return { stale: true, path: args.pathBin, pathVersion, reason: "older-than-compile-wait" };
|
|
30
|
+
}
|
|
31
|
+
if (isOlderThan(pathVersion, args.thisVersion)) {
|
|
32
|
+
return { stale: true, path: args.pathBin, pathVersion, reason: "older-than-this-package" };
|
|
33
|
+
}
|
|
34
|
+
return { stale: false, path: args.pathBin, pathVersion, reason: null };
|
|
35
|
+
}
|
|
36
|
+
export function whichFrontpage() {
|
|
37
|
+
const r = spawnSync("sh", ["-c", "command -v frontpage"], { encoding: "utf8" });
|
|
38
|
+
const line = (r.stdout ?? "").trim().split("\n")[0]?.trim();
|
|
39
|
+
return line || null;
|
|
40
|
+
}
|
|
41
|
+
export function frontpageVersionAt(bin) {
|
|
42
|
+
const r = spawnSync(bin, ["--version"], { encoding: "utf8" });
|
|
43
|
+
const text = `${r.stdout ?? ""}\n${r.stderr ?? ""}`;
|
|
44
|
+
return parseVersionLine(text);
|
|
45
|
+
}
|
|
46
|
+
export function inspectPathFrontpage(opts) {
|
|
47
|
+
const thisVersion = opts?.thisVersion ?? packageIdentity().version;
|
|
48
|
+
const thisBin = opts?.thisBin ?? process.argv[1] ?? null;
|
|
49
|
+
const pathBin = whichFrontpage();
|
|
50
|
+
const pathVersionLine = pathBin ? frontpageVersionAt(pathBin) : null;
|
|
51
|
+
return interpretPathFrontpage({
|
|
52
|
+
thisVersion,
|
|
53
|
+
thisBin,
|
|
54
|
+
pathBin,
|
|
55
|
+
pathVersionLine,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function stalePathUpgradeMessage(v) {
|
|
59
|
+
const found = v.pathVersion ? `frontpage-host ${v.pathVersion}` : "an unknown frontpage-host";
|
|
60
|
+
const where = v.path ? ` at ${v.path}` : "";
|
|
61
|
+
return (`PATH frontpage is ${found}${where}. Compile-wait needs frontpage-host ${COMPILE_WAIT_MIN_VERSION} or newer. ` +
|
|
62
|
+
`Uninstall the stale global (npm uninstall -g frontpage-host) and install ` +
|
|
63
|
+
`npm i -g frontpage-host@${COMPILE_WAIT_MIN_VERSION} (or npx frontpage-host@${COMPILE_WAIT_MIN_VERSION}). ` +
|
|
64
|
+
`In the Frontpage repo, run node <repo>/cli/dist/bin.js instead of a global binary.`);
|
|
65
|
+
}
|
package/dist/bin.js
CHANGED
|
@@ -27,6 +27,10 @@ import { runMedia } from "./commands/media.js";
|
|
|
27
27
|
import { runImages } from "./commands/images.js";
|
|
28
28
|
import { runInbox } from "./commands/inbox.js";
|
|
29
29
|
import { runSeo } from "./commands/seo.js";
|
|
30
|
+
import { runRedirects } from "./commands/redirects.js";
|
|
31
|
+
import { runPeople } from "./commands/people.js";
|
|
32
|
+
import { runShop } from "./commands/shop.js";
|
|
33
|
+
import { runMail } from "./commands/mail.js";
|
|
30
34
|
import { runArticles } from "./commands/articles.js";
|
|
31
35
|
import { runSettings } from "./commands/settings.js";
|
|
32
36
|
import { runEdit } from "./commands/edit.js";
|
|
@@ -112,8 +116,16 @@ async function main() {
|
|
|
112
116
|
return runImages(args);
|
|
113
117
|
case "inbox":
|
|
114
118
|
return runInbox(args);
|
|
119
|
+
case "people":
|
|
120
|
+
return runPeople(args);
|
|
121
|
+
case "shop":
|
|
122
|
+
return runShop(args);
|
|
123
|
+
case "mail":
|
|
124
|
+
return runMail(args);
|
|
115
125
|
case "seo":
|
|
116
126
|
return runSeo(args);
|
|
127
|
+
case "redirects":
|
|
128
|
+
return runRedirects(args);
|
|
117
129
|
case "articles":
|
|
118
130
|
return runArticles(args);
|
|
119
131
|
case "settings":
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runMail(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolveSlug } from "../site.js";
|
|
3
|
+
import { CliHttpError, EXIT_OK, EXIT_USAGE, exitCodeForError, v1Delete, v1Get, v1Patch, v1Post, v1Put, } from "../http/client.js";
|
|
4
|
+
import { assertNoUnknownFlags, hasFlag, takeFlag } from "../argv.js";
|
|
5
|
+
import { emitErr, emitOk, isJson } from "../runtime.js";
|
|
6
|
+
const USAGE = "Usage: frontpage mail settings|email|template|series|send|sends|enrolments|suppress … [--site <slug>] [--json]";
|
|
7
|
+
const ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8
|
+
function requireId(work, kind) {
|
|
9
|
+
const fromFlag = takeFlag(work, "--id");
|
|
10
|
+
const positional = work.find((a) => !a.startsWith("-"));
|
|
11
|
+
const raw = fromFlag ?? (positional ? work.splice(work.indexOf(positional), 1)[0] : undefined);
|
|
12
|
+
if (!raw || !ID_RE.test(raw)) {
|
|
13
|
+
throw new CliHttpError(`${kind} not found.`, EXIT_USAGE, 404, "not_found");
|
|
14
|
+
}
|
|
15
|
+
return raw;
|
|
16
|
+
}
|
|
17
|
+
function takeAll(work, name) {
|
|
18
|
+
const out = [];
|
|
19
|
+
let v = takeFlag(work, name);
|
|
20
|
+
while (v !== undefined) {
|
|
21
|
+
out.push(v);
|
|
22
|
+
v = takeFlag(work, name);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
function parseVars(pairs) {
|
|
27
|
+
const vars = {};
|
|
28
|
+
for (const pair of pairs) {
|
|
29
|
+
const eq = pair.indexOf("=");
|
|
30
|
+
if (eq <= 0)
|
|
31
|
+
continue;
|
|
32
|
+
vars[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
33
|
+
}
|
|
34
|
+
return vars;
|
|
35
|
+
}
|
|
36
|
+
function parseRecipients(work) {
|
|
37
|
+
const tos = takeAll(work, "--to").flatMap((v) => v.split(",").map((s) => s.trim()).filter(Boolean));
|
|
38
|
+
const name = takeFlag(work, "--name");
|
|
39
|
+
const vars = parseVars(takeAll(work, "--var"));
|
|
40
|
+
if (tos.length === 0) {
|
|
41
|
+
throw new CliHttpError("Usage: --to a@x.com", EXIT_USAGE, 0, "bad_request");
|
|
42
|
+
}
|
|
43
|
+
return tos.map((email) => ({
|
|
44
|
+
email,
|
|
45
|
+
...(name ? { name } : {}),
|
|
46
|
+
...(Object.keys(vars).length ? { vars } : {}),
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
async function readJsonFile(path) {
|
|
50
|
+
const raw = await readFile(path, "utf8");
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(raw);
|
|
53
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
54
|
+
throw new Error("not an object");
|
|
55
|
+
}
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new CliHttpError("File must be a JSON object.", EXIT_USAGE, 0, "bad_request");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function parseStepFlag(raw) {
|
|
63
|
+
const parts = raw.split(",").map((s) => s.trim());
|
|
64
|
+
let templateId = "";
|
|
65
|
+
let delay = 0;
|
|
66
|
+
for (const part of parts) {
|
|
67
|
+
const [k, v] = part.split("=");
|
|
68
|
+
if (k === "template" || k === "templateId")
|
|
69
|
+
templateId = v ?? "";
|
|
70
|
+
if (k === "delay")
|
|
71
|
+
delay = v ?? 0;
|
|
72
|
+
}
|
|
73
|
+
if (!templateId) {
|
|
74
|
+
throw new CliHttpError("Each --step needs template=<id>.", EXIT_USAGE, 0, "bad_request");
|
|
75
|
+
}
|
|
76
|
+
return { templateId, delayMinutes: delay };
|
|
77
|
+
}
|
|
78
|
+
export async function runMail(args) {
|
|
79
|
+
try {
|
|
80
|
+
const { slug, rest } = await resolveSlug(args);
|
|
81
|
+
const group = rest[0];
|
|
82
|
+
const base = `/api/v1/sites/${encodeURIComponent(slug)}/mail`;
|
|
83
|
+
if (group === "settings") {
|
|
84
|
+
const sub = rest[1] ?? "get";
|
|
85
|
+
const work = rest.slice(2);
|
|
86
|
+
if (sub === "get") {
|
|
87
|
+
assertNoUnknownFlags(work, "mail");
|
|
88
|
+
const data = await v1Get(`${base}/settings`);
|
|
89
|
+
if (isJson()) {
|
|
90
|
+
emitOk("mail", data);
|
|
91
|
+
return EXIT_OK;
|
|
92
|
+
}
|
|
93
|
+
const s = (data.settings ?? {});
|
|
94
|
+
console.log(`${s.timezone ?? "UTC"} ${s.postalLine ?? "(no postal)"} ${s.replyTo ?? ""}`);
|
|
95
|
+
return EXIT_OK;
|
|
96
|
+
}
|
|
97
|
+
if (sub === "set") {
|
|
98
|
+
const postal = takeFlag(work, "--postal") ?? takeFlag(work, "--postal-line");
|
|
99
|
+
const replyTo = takeFlag(work, "--reply-to");
|
|
100
|
+
const fromName = takeFlag(work, "--from-name");
|
|
101
|
+
const timezone = takeFlag(work, "--timezone");
|
|
102
|
+
assertNoUnknownFlags(work, "mail");
|
|
103
|
+
const data = await v1Patch(`${base}/settings`, {
|
|
104
|
+
postalLine: postal,
|
|
105
|
+
replyTo,
|
|
106
|
+
fromName,
|
|
107
|
+
timezone,
|
|
108
|
+
});
|
|
109
|
+
if (isJson())
|
|
110
|
+
emitOk("mail", data);
|
|
111
|
+
else
|
|
112
|
+
console.log("Settings saved.");
|
|
113
|
+
return EXIT_OK;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (group === "email" || group === "template") {
|
|
117
|
+
const noun = group;
|
|
118
|
+
const plural = group === "email" ? "emails" : "templates";
|
|
119
|
+
const sub = rest[1] ?? "list";
|
|
120
|
+
const work = rest.slice(2);
|
|
121
|
+
if (sub === "list") {
|
|
122
|
+
const q = takeFlag(work, "--q");
|
|
123
|
+
const limit = takeFlag(work, "--limit");
|
|
124
|
+
const cursor = takeFlag(work, "--cursor");
|
|
125
|
+
assertNoUnknownFlags(work, "mail");
|
|
126
|
+
const params = new URLSearchParams();
|
|
127
|
+
if (q)
|
|
128
|
+
params.set("q", q);
|
|
129
|
+
if (limit)
|
|
130
|
+
params.set("limit", limit);
|
|
131
|
+
if (cursor)
|
|
132
|
+
params.set("cursor", cursor);
|
|
133
|
+
const qs = params.toString();
|
|
134
|
+
const data = await v1Get(qs ? `${base}/${plural}?${qs}` : `${base}/${plural}`);
|
|
135
|
+
if (isJson()) {
|
|
136
|
+
emitOk("mail", data);
|
|
137
|
+
return EXIT_OK;
|
|
138
|
+
}
|
|
139
|
+
const rows = data[plural] ?? [];
|
|
140
|
+
if (rows.length === 0) {
|
|
141
|
+
console.log(group === "email" ? "No drafts." : "No templates.");
|
|
142
|
+
return EXIT_OK;
|
|
143
|
+
}
|
|
144
|
+
for (const row of rows)
|
|
145
|
+
console.log(`${row.id} ${row.name ?? ""} ${row.subject ?? ""}`);
|
|
146
|
+
return EXIT_OK;
|
|
147
|
+
}
|
|
148
|
+
if (sub === "get") {
|
|
149
|
+
const id = requireId(work, "Document");
|
|
150
|
+
assertNoUnknownFlags(work, "mail");
|
|
151
|
+
const data = await v1Get(`${base}/${plural}/${id}`);
|
|
152
|
+
if (isJson())
|
|
153
|
+
emitOk("mail", data);
|
|
154
|
+
else {
|
|
155
|
+
const doc = (data[noun] ?? data.document);
|
|
156
|
+
console.log(`${doc?.id} ${doc?.subject ?? ""}`);
|
|
157
|
+
}
|
|
158
|
+
return EXIT_OK;
|
|
159
|
+
}
|
|
160
|
+
if (sub === "create") {
|
|
161
|
+
const file = takeFlag(work, "--file");
|
|
162
|
+
const name = takeFlag(work, "--name");
|
|
163
|
+
const subject = takeFlag(work, "--subject");
|
|
164
|
+
const preheader = takeFlag(work, "--preheader");
|
|
165
|
+
assertNoUnknownFlags(work, "mail");
|
|
166
|
+
if (!file)
|
|
167
|
+
throw new CliHttpError("Usage: --file email.json", EXIT_USAGE, 0, "bad_request");
|
|
168
|
+
const body = await readJsonFile(file);
|
|
169
|
+
if (name)
|
|
170
|
+
body.name = name;
|
|
171
|
+
if (subject)
|
|
172
|
+
body.subject = subject;
|
|
173
|
+
if (preheader)
|
|
174
|
+
body.preheader = preheader;
|
|
175
|
+
const data = await v1Post(`${base}/${plural}`, body);
|
|
176
|
+
if (isJson())
|
|
177
|
+
emitOk("mail", data);
|
|
178
|
+
else {
|
|
179
|
+
const doc = (data[noun] ?? data.document);
|
|
180
|
+
console.log(doc?.id ?? "");
|
|
181
|
+
}
|
|
182
|
+
return EXIT_OK;
|
|
183
|
+
}
|
|
184
|
+
if (sub === "set") {
|
|
185
|
+
const id = requireId(work, "Document");
|
|
186
|
+
const file = takeFlag(work, "--file");
|
|
187
|
+
const name = takeFlag(work, "--name");
|
|
188
|
+
const subject = takeFlag(work, "--subject");
|
|
189
|
+
const preheader = takeFlag(work, "--preheader");
|
|
190
|
+
assertNoUnknownFlags(work, "mail");
|
|
191
|
+
const body = file ? await readJsonFile(file) : {};
|
|
192
|
+
if (name)
|
|
193
|
+
body.name = name;
|
|
194
|
+
if (subject)
|
|
195
|
+
body.subject = subject;
|
|
196
|
+
if (preheader)
|
|
197
|
+
body.preheader = preheader;
|
|
198
|
+
const data = await v1Patch(`${base}/${plural}/${id}`, body);
|
|
199
|
+
if (isJson())
|
|
200
|
+
emitOk("mail", data);
|
|
201
|
+
else
|
|
202
|
+
console.log("Saved.");
|
|
203
|
+
return EXIT_OK;
|
|
204
|
+
}
|
|
205
|
+
if (sub === "rm") {
|
|
206
|
+
const id = requireId(work, "Document");
|
|
207
|
+
assertNoUnknownFlags(work, "mail");
|
|
208
|
+
const data = await v1Delete(`${base}/${plural}/${id}`);
|
|
209
|
+
if (isJson())
|
|
210
|
+
emitOk("mail", data);
|
|
211
|
+
else
|
|
212
|
+
console.log("Deleted.");
|
|
213
|
+
return EXIT_OK;
|
|
214
|
+
}
|
|
215
|
+
if (sub === "preview") {
|
|
216
|
+
const id = requireId(work, "Document");
|
|
217
|
+
assertNoUnknownFlags(work, "mail");
|
|
218
|
+
const data = await v1Post(`${base}/${plural}/${id}/preview`, {});
|
|
219
|
+
if (isJson())
|
|
220
|
+
emitOk("mail", data);
|
|
221
|
+
else
|
|
222
|
+
console.log((data.preview ?? {}).subject ?? "Preview ready.");
|
|
223
|
+
return EXIT_OK;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (group === "series") {
|
|
227
|
+
const sub = rest[1] ?? "list";
|
|
228
|
+
const work = rest.slice(2);
|
|
229
|
+
if (sub === "list") {
|
|
230
|
+
assertNoUnknownFlags(work, "mail");
|
|
231
|
+
const data = await v1Get(`${base}/series`);
|
|
232
|
+
if (isJson()) {
|
|
233
|
+
emitOk("mail", data);
|
|
234
|
+
return EXIT_OK;
|
|
235
|
+
}
|
|
236
|
+
const rows = data.series ?? [];
|
|
237
|
+
for (const row of rows)
|
|
238
|
+
console.log(`${row.id} ${row.status ?? ""} ${row.name ?? ""}`);
|
|
239
|
+
return EXIT_OK;
|
|
240
|
+
}
|
|
241
|
+
if (sub === "get") {
|
|
242
|
+
const id = requireId(work, "Series");
|
|
243
|
+
assertNoUnknownFlags(work, "mail");
|
|
244
|
+
const data = await v1Get(`${base}/series/${id}`);
|
|
245
|
+
if (isJson())
|
|
246
|
+
emitOk("mail", data);
|
|
247
|
+
else
|
|
248
|
+
console.log((data.series ?? {}).id ?? "");
|
|
249
|
+
return EXIT_OK;
|
|
250
|
+
}
|
|
251
|
+
if (sub === "create") {
|
|
252
|
+
const name = takeFlag(work, "--name");
|
|
253
|
+
assertNoUnknownFlags(work, "mail");
|
|
254
|
+
if (!name)
|
|
255
|
+
throw new CliHttpError("Usage: frontpage mail series create --name …", EXIT_USAGE, 0, "bad_request");
|
|
256
|
+
const data = await v1Post(`${base}/series`, { name });
|
|
257
|
+
if (isJson())
|
|
258
|
+
emitOk("mail", data);
|
|
259
|
+
else
|
|
260
|
+
console.log((data.series ?? {}).id ?? "");
|
|
261
|
+
return EXIT_OK;
|
|
262
|
+
}
|
|
263
|
+
if (sub === "set") {
|
|
264
|
+
const id = requireId(work, "Series");
|
|
265
|
+
const name = takeFlag(work, "--name");
|
|
266
|
+
const status = takeFlag(work, "--status");
|
|
267
|
+
assertNoUnknownFlags(work, "mail");
|
|
268
|
+
const data = await v1Patch(`${base}/series/${id}`, { name, status });
|
|
269
|
+
if (isJson())
|
|
270
|
+
emitOk("mail", data);
|
|
271
|
+
else
|
|
272
|
+
console.log("Saved.");
|
|
273
|
+
return EXIT_OK;
|
|
274
|
+
}
|
|
275
|
+
if (sub === "rm") {
|
|
276
|
+
const id = requireId(work, "Series");
|
|
277
|
+
assertNoUnknownFlags(work, "mail");
|
|
278
|
+
const data = await v1Delete(`${base}/series/${id}`);
|
|
279
|
+
if (isJson())
|
|
280
|
+
emitOk("mail", data);
|
|
281
|
+
else
|
|
282
|
+
console.log("Deleted.");
|
|
283
|
+
return EXIT_OK;
|
|
284
|
+
}
|
|
285
|
+
if (sub === "activate" || sub === "pause") {
|
|
286
|
+
const id = requireId(work, "Series");
|
|
287
|
+
assertNoUnknownFlags(work, "mail");
|
|
288
|
+
const data = await v1Patch(`${base}/series/${id}`, {
|
|
289
|
+
status: sub === "activate" ? "active" : "paused",
|
|
290
|
+
});
|
|
291
|
+
if (isJson())
|
|
292
|
+
emitOk("mail", data);
|
|
293
|
+
else
|
|
294
|
+
console.log(sub === "activate" ? "Active." : "Paused.");
|
|
295
|
+
return EXIT_OK;
|
|
296
|
+
}
|
|
297
|
+
if (sub === "steps") {
|
|
298
|
+
const verb = work[0];
|
|
299
|
+
const restSteps = work.slice(1);
|
|
300
|
+
if (verb !== "set") {
|
|
301
|
+
throw new CliHttpError("Usage: frontpage mail series steps set <id> --file steps.json", EXIT_USAGE, 0, "bad_request");
|
|
302
|
+
}
|
|
303
|
+
const id = requireId(restSteps, "Series");
|
|
304
|
+
const file = takeFlag(restSteps, "--file");
|
|
305
|
+
const stepFlags = takeAll(restSteps, "--step");
|
|
306
|
+
assertNoUnknownFlags(restSteps, "mail");
|
|
307
|
+
let steps = [];
|
|
308
|
+
if (file) {
|
|
309
|
+
const body = await readJsonFile(file);
|
|
310
|
+
steps = Array.isArray(body.steps) ? body.steps : Array.isArray(body) ? body : [];
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
steps = stepFlags.map(parseStepFlag);
|
|
314
|
+
}
|
|
315
|
+
const data = await v1Put(`${base}/series/${id}/steps`, { steps });
|
|
316
|
+
if (isJson())
|
|
317
|
+
emitOk("mail", data);
|
|
318
|
+
else
|
|
319
|
+
console.log("Steps saved.");
|
|
320
|
+
return EXIT_OK;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (group === "send") {
|
|
324
|
+
const work = rest.slice(1);
|
|
325
|
+
const file = takeFlag(work, "--file");
|
|
326
|
+
const wait = hasFlag(work, "--wait");
|
|
327
|
+
const test = hasFlag(work, "--test");
|
|
328
|
+
const kind = work[0];
|
|
329
|
+
if (file) {
|
|
330
|
+
const recipients = parseRecipients(work);
|
|
331
|
+
assertNoUnknownFlags(work, "mail");
|
|
332
|
+
const body = await readJsonFile(file);
|
|
333
|
+
const data = await v1Post(`${base}/send`, {
|
|
334
|
+
...body,
|
|
335
|
+
recipients,
|
|
336
|
+
wait,
|
|
337
|
+
test,
|
|
338
|
+
});
|
|
339
|
+
if (isJson())
|
|
340
|
+
emitOk("mail", data);
|
|
341
|
+
else
|
|
342
|
+
console.log(data.batchId ?? "Queued.");
|
|
343
|
+
return EXIT_OK;
|
|
344
|
+
}
|
|
345
|
+
if (kind === "email" || kind === "template" || kind === "series") {
|
|
346
|
+
const restKind = work.slice(1);
|
|
347
|
+
const id = requireId(restKind, kind === "series" ? "Series" : "Document");
|
|
348
|
+
const recipients = parseRecipients(restKind);
|
|
349
|
+
assertNoUnknownFlags(restKind, "mail");
|
|
350
|
+
const path = kind === "series" ? `${base}/series/${id}/send` : `${base}/${kind === "email" ? "emails" : "templates"}/${id}/send`;
|
|
351
|
+
const data = await v1Post(path, { recipients, wait, test });
|
|
352
|
+
if (isJson())
|
|
353
|
+
emitOk("mail", data);
|
|
354
|
+
else
|
|
355
|
+
console.log(data.batchId ?? "Queued.");
|
|
356
|
+
return EXIT_OK;
|
|
357
|
+
}
|
|
358
|
+
throw new CliHttpError("Usage: frontpage mail send --file email.json --to a@x.com | send email|template|series <id> --to a@x.com", EXIT_USAGE, 0, "bad_request");
|
|
359
|
+
}
|
|
360
|
+
if (group === "sends") {
|
|
361
|
+
const sub = rest[1] ?? "list";
|
|
362
|
+
const work = rest.slice(2);
|
|
363
|
+
if (sub === "list") {
|
|
364
|
+
const batchId = takeFlag(work, "--batch");
|
|
365
|
+
const limit = takeFlag(work, "--limit");
|
|
366
|
+
const cursor = takeFlag(work, "--cursor");
|
|
367
|
+
assertNoUnknownFlags(work, "mail");
|
|
368
|
+
const params = new URLSearchParams();
|
|
369
|
+
if (batchId)
|
|
370
|
+
params.set("batchId", batchId);
|
|
371
|
+
if (limit)
|
|
372
|
+
params.set("limit", limit);
|
|
373
|
+
if (cursor)
|
|
374
|
+
params.set("cursor", cursor);
|
|
375
|
+
const qs = params.toString();
|
|
376
|
+
const data = await v1Get(qs ? `${base}/sends?${qs}` : `${base}/sends`);
|
|
377
|
+
if (isJson()) {
|
|
378
|
+
emitOk("mail", data);
|
|
379
|
+
return EXIT_OK;
|
|
380
|
+
}
|
|
381
|
+
const rows = data.sends ?? [];
|
|
382
|
+
for (const row of rows)
|
|
383
|
+
console.log(`${row.id} ${row.status ?? ""} ${row.email ?? ""}`);
|
|
384
|
+
return EXIT_OK;
|
|
385
|
+
}
|
|
386
|
+
if (sub === "get") {
|
|
387
|
+
const id = requireId(work, "Send");
|
|
388
|
+
assertNoUnknownFlags(work, "mail");
|
|
389
|
+
const data = await v1Get(`${base}/sends/${id}`);
|
|
390
|
+
if (isJson())
|
|
391
|
+
emitOk("mail", data);
|
|
392
|
+
else
|
|
393
|
+
console.log((data.send ?? {}).status ?? "");
|
|
394
|
+
return EXIT_OK;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (group === "enrolments") {
|
|
398
|
+
const sub = rest[1] ?? "list";
|
|
399
|
+
const work = rest.slice(2);
|
|
400
|
+
if (sub === "list") {
|
|
401
|
+
const seriesId = takeFlag(work, "--series");
|
|
402
|
+
assertNoUnknownFlags(work, "mail");
|
|
403
|
+
const params = new URLSearchParams();
|
|
404
|
+
if (seriesId)
|
|
405
|
+
params.set("seriesId", seriesId);
|
|
406
|
+
const qs = params.toString();
|
|
407
|
+
const data = await v1Get(qs ? `${base}/enrolments?${qs}` : `${base}/enrolments`);
|
|
408
|
+
if (isJson())
|
|
409
|
+
emitOk("mail", data);
|
|
410
|
+
else {
|
|
411
|
+
const rows = data.enrolments ?? [];
|
|
412
|
+
for (const row of rows)
|
|
413
|
+
console.log(`${row.id} ${row.status ?? ""} ${row.email ?? ""}`);
|
|
414
|
+
}
|
|
415
|
+
return EXIT_OK;
|
|
416
|
+
}
|
|
417
|
+
if (sub === "cancel") {
|
|
418
|
+
const id = requireId(work, "Enrolment");
|
|
419
|
+
assertNoUnknownFlags(work, "mail");
|
|
420
|
+
const data = await v1Post(`${base}/enrolments/${id}/cancel`, {});
|
|
421
|
+
if (isJson())
|
|
422
|
+
emitOk("mail", data);
|
|
423
|
+
else
|
|
424
|
+
console.log("Cancelled.");
|
|
425
|
+
return EXIT_OK;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (group === "suppress") {
|
|
429
|
+
const sub = rest[1] ?? "list";
|
|
430
|
+
const work = rest.slice(2);
|
|
431
|
+
if (sub === "list") {
|
|
432
|
+
assertNoUnknownFlags(work, "mail");
|
|
433
|
+
const data = await v1Get(`${base}/suppressions`);
|
|
434
|
+
if (isJson())
|
|
435
|
+
emitOk("mail", data);
|
|
436
|
+
else {
|
|
437
|
+
const rows = data.suppressions ?? [];
|
|
438
|
+
for (const row of rows)
|
|
439
|
+
console.log(`${row.email ?? ""} ${row.reason ?? ""}`);
|
|
440
|
+
}
|
|
441
|
+
return EXIT_OK;
|
|
442
|
+
}
|
|
443
|
+
if (sub === "add") {
|
|
444
|
+
const email = takeFlag(work, "--email") ?? work.find((a) => !a.startsWith("-"));
|
|
445
|
+
if (email && !email.startsWith("-") && !takeFlag(work, "--email")) {
|
|
446
|
+
work.splice(work.indexOf(email), 1);
|
|
447
|
+
}
|
|
448
|
+
assertNoUnknownFlags(work, "mail");
|
|
449
|
+
if (!email)
|
|
450
|
+
throw new CliHttpError("Usage: frontpage mail suppress add --email a@x.com", EXIT_USAGE, 0, "bad_request");
|
|
451
|
+
const data = await v1Post(`${base}/suppressions`, { email });
|
|
452
|
+
if (isJson())
|
|
453
|
+
emitOk("mail", data);
|
|
454
|
+
else
|
|
455
|
+
console.log("Suppressed.");
|
|
456
|
+
return EXIT_OK;
|
|
457
|
+
}
|
|
458
|
+
if (sub === "rm") {
|
|
459
|
+
const email = takeFlag(work, "--email") ?? work.find((a) => !a.startsWith("-"));
|
|
460
|
+
if (email && !email.startsWith("-") && work.includes(email))
|
|
461
|
+
work.splice(work.indexOf(email), 1);
|
|
462
|
+
assertNoUnknownFlags(work, "mail");
|
|
463
|
+
if (!email)
|
|
464
|
+
throw new CliHttpError("Usage: frontpage mail suppress rm --email a@x.com", EXIT_USAGE, 0, "bad_request");
|
|
465
|
+
const data = await v1Delete(`${base}/suppressions/${encodeURIComponent(email)}`);
|
|
466
|
+
if (isJson())
|
|
467
|
+
emitOk("mail", data);
|
|
468
|
+
else
|
|
469
|
+
console.log("Removed.");
|
|
470
|
+
return EXIT_OK;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
console.error(USAGE);
|
|
474
|
+
return EXIT_USAGE;
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
if (isJson())
|
|
478
|
+
return emitErr("mail", err);
|
|
479
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
480
|
+
return exitCodeForError(err);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runPeople(args: string[]): Promise<number>;
|