frontpage-host 0.1.18 → 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.
@@ -1,18 +1,32 @@
1
+ import { writeFile } from "node:fs/promises";
1
2
  import { resolveSlug } from "../site.js";
2
- import { CliHttpError, EXIT_OK, EXIT_USAGE, exitCodeForError, v1Delete, v1Get, v1Patch, v1Post, } from "../http/client.js";
3
+ import { CliHttpError, EXIT_OK, EXIT_USAGE, apiBase, exitCodeForError, getBearerAuthHeader, httpErrorFromV1, v1Delete, v1Get, v1Patch, v1Post, } from "../http/client.js";
3
4
  import { assertNoUnknownFlags, hasFlag, takeFlag } from "../argv.js";
4
5
  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
+ const USAGE = "Usage: frontpage people list|get|add|set|note|rm-note|follow-up|hide|unhide|tag|export [--site <slug>] [--json]";
7
+ const PERSON_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6
8
  function requireId(work) {
7
9
  const fromFlag = takeFlag(work, "--id");
8
- if (fromFlag)
9
- return fromFlag;
10
10
  const positional = work.find((a) => !a.startsWith("-"));
11
- if (positional) {
12
- work.splice(work.indexOf(positional), 1);
13
- return positional;
11
+ const raw = fromFlag ?? (positional
12
+ ? work.splice(work.indexOf(positional), 1)[0]
13
+ : undefined);
14
+ if (!raw) {
15
+ throw new CliHttpError(`Person id is required. Run: frontpage help people`, EXIT_USAGE, 0, "bad_request");
14
16
  }
15
- throw new CliHttpError(`Person id is required. Run: frontpage help people`, EXIT_USAGE, 0, "bad_request");
17
+ if (!PERSON_ID_RE.test(raw)) {
18
+ throw new CliHttpError("Person not found.", EXIT_USAGE, 404, "not_found");
19
+ }
20
+ return raw;
21
+ }
22
+ function requireEventId(raw) {
23
+ if (!raw) {
24
+ throw new CliHttpError(`Usage: frontpage people rm-note <id> --event <eventId>`, EXIT_USAGE, 0, "bad_request");
25
+ }
26
+ if (!PERSON_ID_RE.test(raw)) {
27
+ throw new CliHttpError("Event not found.", EXIT_USAGE, 404, "not_found");
28
+ }
29
+ return raw;
16
30
  }
17
31
  function displayName(row) {
18
32
  return row.name || row.email || row.phone || row.id;
@@ -27,10 +41,29 @@ function printPerson(row) {
27
41
  bits.push(`follow-up ${row.followUpAt}`);
28
42
  if (row.hiddenAt)
29
43
  bits.push("hidden");
44
+ if (row.tags?.length)
45
+ bits.push(row.tags.join(","));
30
46
  if (row.lastEvent?.summary)
31
47
  bits.push(row.lastEvent.summary);
32
48
  console.log(`${row.id} ${bits.join(" · ")}`);
33
49
  }
50
+ async function fetchPeopleCsv(path) {
51
+ const headers = await getBearerAuthHeader();
52
+ const res = await fetch(`${apiBase()}${path}`, {
53
+ headers: { ...headers, Accept: "text/csv" },
54
+ });
55
+ const text = await res.text();
56
+ if (res.ok)
57
+ return text;
58
+ let parsed = {};
59
+ try {
60
+ parsed = text ? JSON.parse(text) : {};
61
+ }
62
+ catch {
63
+ parsed = {};
64
+ }
65
+ throw httpErrorFromV1(res.status, parsed, text || `Request failed (${res.status})`);
66
+ }
34
67
  export async function runPeople(args) {
35
68
  try {
36
69
  const { slug, rest } = await resolveSlug(args);
@@ -48,6 +81,7 @@ export async function runPeople(args) {
48
81
  const limit = takeFlag(work, "--limit");
49
82
  const cursor = takeFlag(work, "--cursor");
50
83
  const hidden = hasFlag(work, "--hidden");
84
+ const tag = takeFlag(work, "--tag");
51
85
  assertNoUnknownFlags(work, "people");
52
86
  const params = new URLSearchParams();
53
87
  if (q)
@@ -70,6 +104,8 @@ export async function runPeople(args) {
70
104
  params.set("cursor", cursor);
71
105
  if (hidden)
72
106
  params.set("hidden", "1");
107
+ if (tag)
108
+ params.set("tag", tag);
73
109
  const qs = params.toString();
74
110
  const data = await v1Get(qs ? `${base}?${qs}` : base);
75
111
  if (isJson()) {
@@ -171,12 +207,8 @@ export async function runPeople(args) {
171
207
  if (sub === "rm-note") {
172
208
  const work = rest.slice(1);
173
209
  const id = requireId(work);
174
- const eventId = takeFlag(work, "--event") ?? takeFlag(work, "--event-id");
210
+ const eventId = requireEventId(takeFlag(work, "--event") ?? takeFlag(work, "--event-id"));
175
211
  assertNoUnknownFlags(work, "people");
176
- if (!eventId) {
177
- console.error("Usage: frontpage people rm-note <id> --event <eventId>");
178
- return EXIT_USAGE;
179
- }
180
212
  const data = await v1Delete(`${base}/${encodeURIComponent(id)}/events/${encodeURIComponent(eventId)}`);
181
213
  if (isJson()) {
182
214
  emitOk("people", data);
@@ -227,6 +259,76 @@ export async function runPeople(args) {
227
259
  console.log(`Restored ${displayName(data.person)}`);
228
260
  return EXIT_OK;
229
261
  }
262
+ if (sub === "tag") {
263
+ const work = rest.slice(1);
264
+ const id = requireId(work);
265
+ const add = takeFlag(work, "--add");
266
+ const remove = takeFlag(work, "--remove") ?? takeFlag(work, "--rm");
267
+ assertNoUnknownFlags(work, "people");
268
+ if ((!add && !remove) || (add && remove)) {
269
+ console.error("Usage: frontpage people tag <id> --add <tag> | --remove <tag>");
270
+ return EXIT_USAGE;
271
+ }
272
+ const data = add
273
+ ? await v1Post(`${base}/${encodeURIComponent(id)}/tags`, {
274
+ tag: add,
275
+ })
276
+ : await v1Delete(`${base}/${encodeURIComponent(id)}/tags/${encodeURIComponent(remove)}`);
277
+ if (isJson()) {
278
+ emitOk("people", data);
279
+ return EXIT_OK;
280
+ }
281
+ printPerson(data.person);
282
+ return EXIT_OK;
283
+ }
284
+ if (sub === "export") {
285
+ const work = rest.slice(1);
286
+ const q = takeFlag(work, "--q") ?? takeFlag(work, "--query");
287
+ const status = takeFlag(work, "--status");
288
+ const source = takeFlag(work, "--source");
289
+ const formSlug = takeFlag(work, "--form-slug");
290
+ const followUp = takeFlag(work, "--follow-up");
291
+ const sort = takeFlag(work, "--sort");
292
+ const lastSeenDays = takeFlag(work, "--last-seen-days");
293
+ const tag = takeFlag(work, "--tag");
294
+ const out = takeFlag(work, "--out");
295
+ const hidden = hasFlag(work, "--hidden");
296
+ assertNoUnknownFlags(work, "people");
297
+ const params = new URLSearchParams();
298
+ if (q)
299
+ params.set("q", q);
300
+ if (status)
301
+ params.set("status", status);
302
+ if (source)
303
+ params.set("source", source);
304
+ if (formSlug)
305
+ params.set("formSlug", formSlug);
306
+ if (followUp)
307
+ params.set("followUp", followUp);
308
+ if (sort)
309
+ params.set("sort", sort);
310
+ if (lastSeenDays)
311
+ params.set("lastSeenDays", lastSeenDays);
312
+ if (hidden)
313
+ params.set("hidden", "1");
314
+ if (tag)
315
+ params.set("tag", tag);
316
+ const qs = params.toString();
317
+ const csv = await fetchPeopleCsv(qs ? `${base}/export?${qs}` : `${base}/export`);
318
+ if (out) {
319
+ await writeFile(out, csv, "utf8");
320
+ if (isJson()) {
321
+ emitOk("people", { path: out, bytes: Buffer.byteLength(csv, "utf8") });
322
+ return EXIT_OK;
323
+ }
324
+ console.log(out);
325
+ return EXIT_OK;
326
+ }
327
+ process.stdout.write(csv);
328
+ if (!csv.endsWith("\n"))
329
+ process.stdout.write("\n");
330
+ return EXIT_OK;
331
+ }
230
332
  console.error(USAGE);
231
333
  return EXIT_USAGE;
232
334
  }
@@ -0,0 +1,3 @@
1
+ /** Same kebab rules as the Store silo. Kept here so the CLI does not import app lib. */
2
+ export declare const SHOP_ID_RE: RegExp;
3
+ export declare const VARIANT_ID_RE: RegExp;
@@ -0,0 +1,3 @@
1
+ /** Same kebab rules as the Store silo. Kept here so the CLI does not import app lib. */
2
+ export const SHOP_ID_RE = /^[a-z0-9][a-z0-9-]{1,39}$/;
3
+ export const VARIANT_ID_RE = /^[a-z0-9][a-z0-9-]{0,39}$/;
@@ -0,0 +1 @@
1
+ export declare function runShop(args: string[]): Promise<number>;