frontpage-host 0.1.18 → 0.1.20
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 +4 -1
- package/dist/bin.js +6 -0
- package/dist/commands/mail.d.ts +1 -0
- package/dist/commands/mail.js +482 -0
- package/dist/commands/people.js +115 -13
- 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 +679 -0
- package/dist/modules/commands.js +4 -4
- package/dist/registry.js +65 -5
- package/llms.txt +2 -1
- 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 |
|
|
@@ -125,7 +127,8 @@ Sites are Astro 6 + Tailwind v4. More detail: `frontpage help site-building` or
|
|
|
125
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).
|
|
126
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.
|
|
127
129
|
- 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.
|
|
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|categories|settings|orders|payments`. Integer cents only (`$49.99` is `4999`). Stock `--add` / `--remove` become signed delta (`added 5` / `removed 5` / `set to 20` / `stockNow`). Create a category before assigning it (`categories create hats --label Hats`, then `items create … --categories hats`). Connect stays `frontpage modules shop|payments connect`. Square `ordering` is a different module. Full guide: https://frontpage.host/developers/shop
|
|
129
132
|
|
|
130
133
|
## Agent prompt loop
|
|
131
134
|
|
package/dist/bin.js
CHANGED
|
@@ -29,6 +29,8 @@ import { runInbox } from "./commands/inbox.js";
|
|
|
29
29
|
import { runSeo } from "./commands/seo.js";
|
|
30
30
|
import { runRedirects } from "./commands/redirects.js";
|
|
31
31
|
import { runPeople } from "./commands/people.js";
|
|
32
|
+
import { runShop } from "./commands/shop.js";
|
|
33
|
+
import { runMail } from "./commands/mail.js";
|
|
32
34
|
import { runArticles } from "./commands/articles.js";
|
|
33
35
|
import { runSettings } from "./commands/settings.js";
|
|
34
36
|
import { runEdit } from "./commands/edit.js";
|
|
@@ -116,6 +118,10 @@ async function main() {
|
|
|
116
118
|
return runInbox(args);
|
|
117
119
|
case "people":
|
|
118
120
|
return runPeople(args);
|
|
121
|
+
case "shop":
|
|
122
|
+
return runShop(args);
|
|
123
|
+
case "mail":
|
|
124
|
+
return runMail(args);
|
|
119
125
|
case "seo":
|
|
120
126
|
return runSeo(args);
|
|
121
127
|
case "redirects":
|
|
@@ -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
|
+
}
|