@evcraddock/slug-cli 0.11.0 → 0.12.0

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/README.md CHANGED
@@ -156,6 +156,57 @@ When a relationship URL is available, the importer matches existing sources and
156
156
 
157
157
  All explicit source and author metadata is validated before API mutations. For relationship imports, missing contacts are created first, the source is created or updated second, and the post is created or updated last. The API does not provide a bulk transaction, so a later failure stops the import but records created by earlier successful requests remain; relationship mutation errors identify the failed frontmatter path.
158
158
 
159
+ ## Find posts
160
+
161
+ ```bash
162
+ slug --site my-site post find "climate change"
163
+ slug --site my-site post find energy --type article --status all --tag science --json
164
+ slug --site my-site post find example.com --type link
165
+ slug --site my-site post find "follow up" --type note --status draft
166
+ slug --site my-site post show matching-slug
167
+ ```
168
+
169
+ `post find <query> [--type article|link|note] [--status draft|published|all] [--tag <slug>] [--json]` searches titles, slugs, stored content, excerpts, and destination URLs using trimmed, case-insensitive literal substrings. Missing fields are safe; `%`, `_`, and `[` are literal. Quote spaces and shell metacharacters. All types are searched by default, but only **published** posts; use `--status all` to include drafts. Type/status/tag filters combine with the query and preserve existing list semantics, including tag normalization and use of the first repeated `--tag`.
170
+
171
+ The CLI sends encoded `q`, reads every page until an empty page, and verifies matches locally. It fetches details when summaries omit fields needed to decide a match, including content. Older sites that ignore `q` remain compatible without an upgrade, but searching them may require many detail requests. Requests are read-only. Any request failure, missing detail content, or repeated/non-advancing page fails instead of printing partial results. Offset pagination is not a snapshot; avoid concurrent collection edits while searching.
172
+
173
+ Human results include ID, slug, type, title, and `draft`/`published` status. JSON aggregates original matching list summaries into `{ "data": [...] }`, retaining their metadata/relationships and omitting misleading single-page `meta`. Fetched content is used for matching, not appended to summaries. No matches succeed with `No posts found.` or `{ "data": [] }` (exit 0); missing/blank queries and invalid filters are usage errors, and authentication/network/API failures remain errors. Existing post commands and output are unchanged.
174
+
175
+ Updated APIs filter all fields before pagination and log effective query/filters, pagination, and returned count without credentials or post contents. See [post-command details](../docs/cli-specs/02-posts-commands.md#find-behavior) and [optional existing-site upgrade instructions](../docs/package-updates.md#post-search-api-filtering).
176
+
177
+ ## Find sources
178
+
179
+ Use `slug --site <name> sources find <query> [--json]` to find sources by name or URL without knowing an ID:
180
+
181
+ ```bash
182
+ slug --site my-site sources find "example news"
183
+ slug --site my-site sources find example.com --json
184
+ slug --site my-site sources show 42
185
+ ```
186
+
187
+ Search uses case-insensitive literal substring matching on names and URLs, trims surrounding query whitespace, and safely handles sources without URLs. Quote queries containing spaces or shell metacharacters. Characters such as `%`, `_`, and `[` are literal, not wildcards or regular expressions. All matches are returned in the site's source-list order. Human-readable output includes IDs, names, URLs, and relationship counts; JSON uses the normal `{ "data": [...] }` source-list envelope with complete source records. No matches produce `No sources found.` or `{ "data": [] }` with exit code 0. Missing or blank CLI queries are usage errors; authentication, network, and API failures remain errors.
188
+
189
+ The CLI sends `GET /api/v1/sources?q=<query>` and filters the response locally as well. Updated sites filter server-side; older Slugkit sites that ignore `q` still work correctly without an upgrade. This is read-only and does not alter `sources list` or `sources show`. See [existing-site upgrade instructions](../docs/package-updates.md#source-search-api-filtering) to enable server-side filtering on a generated site.
190
+
191
+ ## Find accounts and contacts
192
+
193
+ Search account labels/URLs or contact names/URLs with the same literal, case-insensitive matching as `sources find`:
194
+
195
+ ```bash
196
+ slug --site my-site accounts find "mastodon"
197
+ slug --site my-site accounts find example.com --owner-type contact --owner-id 12 --json
198
+ slug --site my-site contact find "jane example"
199
+ slug --site my-site contact find example.com --json
200
+ slug --site my-site accounts show 42
201
+ slug --site my-site contact show 12
202
+ ```
203
+
204
+ Queries are trimmed; missing or blank queries are usage errors. Quote spaces and shell metacharacters. `%`, `_`, and `[` are literal, not wildcards or regular expressions. Missing contact URLs are safe. All matching records retain list ordering and metadata. Human-readable results include IDs, labels/names, URLs, and account owner type/ID. JSON retains the normal `{ "data": [...] }` envelope and complete records. No matches succeed with `No accounts found.` / `No contacts found.` or `{ "data": [] }` and exit code 0. Authentication, network, and API failures remain errors.
205
+
206
+ Account `--owner-type contact|source|site` and positive-integer `--owner-id` filters combine with the text query. Either filter may be used independently, as with `accounts list`; listing filters do not impose account-creation ownership rules. For example, `--owner-type site --owner-id 12` is valid but finds no accounts. The contact namespace remains singular: `contact find`.
207
+
208
+ The commands send URL-encoded `q` to `GET /api/v1/accounts` or `GET /api/v1/contacts` and also filter the returned records locally. Updated sites filter server-side; older sites that ignore `q` remain compatible without a mandatory website upgrade. Searches are read-only and existing commands remain unchanged. Updated APIs log the effective query, supplied owner filters, and returned count, excluding credentials and record contents. See [existing-site upgrade instructions](../docs/package-updates.md#account-and-contact-search-api-filtering) for optional server-side filtering and logging.
209
+
159
210
  ## Source imports
160
211
 
161
212
  Import link sources and their optional contacts and accounts from YAML with `slug --site <name> sources import <file.yaml>`. The file has one required top-level `sources` array. Each source requires `name` and may include `url`, `description`, `imageUrl`, `faviconUrl`, `contacts`, and `accounts`. Contacts require `name` and may include `url` and `accounts`. Accounts require `label` and `url` and may include `avatarUrl`, `kind`, `protocol`, `default`, and `sortOrder`.
package/dist/commands.js CHANGED
@@ -10,6 +10,7 @@ import { CliError, createInvalidUsageError, ExitCode } from "./errors.js";
10
10
  import { SlugHttpClient } from "./http.js";
11
11
  import { discoverLinkRelationships } from "./link-metadata.js";
12
12
  import { writeJson } from "./output.js";
13
+ import { findPosts } from "./post-find.js";
13
14
  const HELP_TEXT = `slug - manage Slugkit sites
14
15
 
15
16
  Usage:
@@ -21,6 +22,7 @@ Usage:
21
22
  slug [--config <file>] --site <name> login [api-base-url]
22
23
  slug [--config <file>] init <directory> --name <name> [--site-title <title>] [--template <name>] [--template-url <url>] [--template-dir <dir>] [--json]
23
24
  slug [--config <file>] --site <name> post list [--type article|link|note] [--status draft|published|all] [--tag <slug>] [--json]
25
+ slug [--config <file>] --site <name> post find <query> [--type article|link|note] [--status draft|published|all] [--tag <slug>] [--json]
24
26
  slug [--config <file>] post show <slug> [--json]
25
27
  slug [--config <file>] post import <file.md> [--json]
26
28
  slug [--config <file>] post create --type article|link|note --slug <slug> --content <text> [--title <text>] [--url <url>] [--excerpt <text>] [--banner-url <url>] [--published-at <datetime>] [--tag <slug>]... [--source-id <id>] [--credit-contact-id <id>]... [--json]
@@ -37,12 +39,14 @@ Usage:
37
39
  slug [--config <file>] media delete <id> [--json]
38
40
  slug [--config <file>] tag list [--json]
39
41
  slug [--config <file>] sources list [--json]
42
+ slug [--config <file>] sources find <query> [--json]
40
43
  slug [--config <file>] sources show <id> [--json]
41
44
  slug [--config <file>] sources create --name <name> [--url <url>] [--description <text>] [--image-url <url>] [--favicon-url <url>] [--contact-id <id>]... [--json]
42
45
  slug [--config <file>] sources import <file.yaml> [--json]
43
46
  slug [--config <file>] sources edit <id> [--name <name>] [--url <url>] [--description <text>] [--image-url <url>] [--favicon-url <url>] [--contact-id <id>]... [--json]
44
47
  slug [--config <file>] sources delete <id> [--json]
45
48
  slug [--config <file>] accounts list [--owner-type contact|source|site] [--owner-id <id>] [--json]
49
+ slug [--config <file>] accounts find <query> [--owner-type contact|source|site] [--owner-id <id>] [--json]
46
50
  slug [--config <file>] accounts show <id> [--json]
47
51
  slug [--config <file>] accounts create --owner-type contact|source|site [--owner-id <id>] --label <label> --url <url> [--avatar-url <url>] [--kind <kind>] [--protocol <protocol>] [--default] [--sort-order <number>] [--json]
48
52
  slug [--config <file>] accounts edit <id> [--label <label>] [--url <url>] [--avatar-url <url>] [--kind <kind>] [--protocol <protocol>] [--default] [--sort-order <number>] [--json]
@@ -54,6 +58,7 @@ Usage:
54
58
  slug [--config <file>] following follow <target> [--json]
55
59
  slug [--config <file>] following unfollow <id> [--json]
56
60
  slug [--config <file>] contact list [--json]
61
+ slug [--config <file>] contact find <query> [--json]
57
62
  slug [--config <file>] contact show <id> [--json]
58
63
  slug [--config <file>] contact create --name <name> [--url <url>] [--json]
59
64
  slug [--config <file>] contact edit <id> [--name <name>] [--url <url>] [--json]
@@ -73,7 +78,37 @@ Options:
73
78
  --site <name> Target a named configured Slug API website.
74
79
  --help Show this help.
75
80
  --version Show the CLI version.
76
- --json Print command output as JSON when supported.`;
81
+ --json Print command output as JSON when supported.
82
+
83
+ Source search:
84
+ sources find matches literal substrings in source names and URLs, case-insensitively.
85
+ Quote queries containing spaces or shell metacharacters; blank queries are rejected.
86
+ Results are also filtered locally, so older sites work without a website upgrade.
87
+ No matches is successful: "No sources found." or { "data": [] } with --json.
88
+
89
+ Examples:
90
+ slug --site my-site sources find "example news"
91
+ slug --site my-site sources find example.com --json
92
+
93
+ Account and contact search:
94
+ accounts find matches account labels and URLs; contact find matches names and URLs.
95
+ Both use case-insensitive literal substrings, trim queries, and reject blank queries.
96
+ Account owner filters combine with the query using existing list semantics.
97
+ Results are also filtered locally, so older sites work without a website upgrade.
98
+ No matches is successful: "No accounts found." / "No contacts found." or { "data": [] }.
99
+ slug --site my-site accounts find "news" --owner-type source --owner-id 12
100
+ slug --site my-site contact find example.com --json
101
+
102
+ Post search:
103
+ post find matches titles, slugs, content, excerpts, and destination URLs literally,
104
+ case-insensitively. Queries are trimmed; missing or blank queries are rejected.
105
+ Default: all post types, published only. Use --status all to include drafts.
106
+ Type, status, and tag filters combine with the query. Quote spaces/metacharacters.
107
+ Reads every page and fetches omitted detail fields as needed, including on older sites.
108
+ Returns IDs, slugs, types, titles, and status; JSON aggregates { "data": [...] }.
109
+ No matches succeeds with "No posts found." or { "data": [] }; request failures remain errors.
110
+ slug --site my-site post find "climate change" --type article --status all --tag science
111
+ slug --site my-site post find example.com --json`;
77
112
  export async function runCommand(context) {
78
113
  const args = context.args;
79
114
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
@@ -906,6 +941,8 @@ async function runPostsCommand(context, args) {
906
941
  switch (args[0]) {
907
942
  case "list":
908
943
  return runPostsListCommand(context, args.slice(1));
944
+ case "find":
945
+ return runPostsFindCommand(context, args.slice(1));
909
946
  case "show":
910
947
  return runPostsShowCommand(context, args.slice(1));
911
948
  case "import":
@@ -921,7 +958,7 @@ async function runPostsCommand(context, args) {
921
958
  case "unpublish":
922
959
  return runPostsLifecycleCommand(context, args.slice(1), "unpublish");
923
960
  default:
924
- throw createInvalidUsageError("Usage: slug post <list|show|import|create|edit|delete|publish|unpublish>");
961
+ throw createInvalidUsageError("Usage: slug post <list|find|show|import|create|edit|delete|publish|unpublish>");
925
962
  }
926
963
  }
927
964
  async function runPostsImportCommand(context, args) {
@@ -1262,6 +1299,42 @@ async function runPostsListCommand(context, args) {
1262
1299
  }
1263
1300
  return { exitCode: ExitCode.Ok };
1264
1301
  }
1302
+ async function runPostsFindCommand(context, args) {
1303
+ const { query, json } = readPostFindArgs(args);
1304
+ const api = await createConfiguredApiContext(context);
1305
+ const data = await findPosts({ client: api.client, query });
1306
+ if (json) {
1307
+ writeJson(context.writer, { data });
1308
+ }
1309
+ else {
1310
+ writePostFindResults(context.writer, data);
1311
+ }
1312
+ return { exitCode: ExitCode.Ok };
1313
+ }
1314
+ function readPostFindArgs(args) {
1315
+ const positional = args.filter((arg) => arg !== "--json");
1316
+ const { query: text } = readFindArgs(positional.slice(0, 1), "post");
1317
+ const parsed = parsePostFlags(positional.slice(1), ["type", "status", "tag"]);
1318
+ const query = new URLSearchParams({ q: text, status: "published" });
1319
+ for (const key of ["type", "status", "tag"])
1320
+ addOptionalQuery(query, key, parsed.options[key]);
1321
+ if (query.has("type") && !["article", "link", "note"].includes(query.get("type") ?? "")) {
1322
+ throw createInvalidUsageError("--type must be article, link, or note");
1323
+ }
1324
+ if (!["draft", "published", "all"].includes(query.get("status") ?? "")) {
1325
+ throw createInvalidUsageError("--status must be draft, published, or all");
1326
+ }
1327
+ return { query, json: args.includes("--json") };
1328
+ }
1329
+ function writePostFindResults(writer, posts) {
1330
+ if (posts.length === 0) {
1331
+ writer.stdout("No posts found.");
1332
+ return;
1333
+ }
1334
+ for (const post of posts) {
1335
+ writer.stdout(`${post.id} • ${post.slug} • ${post.type} • ${post.title ?? "(untitled)"} • ${post.publishedAt === null ? "draft" : "published"}`);
1336
+ }
1337
+ }
1265
1338
  async function runPostsShowCommand(context, args) {
1266
1339
  const { slug, json } = readSlugCommandArgs(args, "Usage: slug post show <slug> [--json]");
1267
1340
  const api = await createConfiguredApiContext(context);
@@ -1811,6 +1884,8 @@ async function runSourcesCommand(context, args) {
1811
1884
  switch (args[0]) {
1812
1885
  case "list":
1813
1886
  return runSourcesListCommand(context, args.slice(1));
1887
+ case "find":
1888
+ return runSourcesFindCommand(context, args.slice(1));
1814
1889
  case "show":
1815
1890
  return runSourcesShowCommand(context, args.slice(1));
1816
1891
  case "create":
@@ -1822,7 +1897,7 @@ async function runSourcesCommand(context, args) {
1822
1897
  case "delete":
1823
1898
  return runSourcesDeleteCommand(context, args.slice(1));
1824
1899
  default:
1825
- throw createInvalidUsageError("Usage: slug sources <list|show|create|import|edit|delete>");
1900
+ throw createInvalidUsageError("Usage: slug sources <list|find|show|create|import|edit|delete>");
1826
1901
  }
1827
1902
  }
1828
1903
  async function runSourcesListCommand(context, args) {
@@ -1837,6 +1912,32 @@ async function runSourcesListCommand(context, args) {
1837
1912
  }
1838
1913
  return { exitCode: ExitCode.Ok };
1839
1914
  }
1915
+ async function runSourcesFindCommand(context, args) {
1916
+ const { query, json } = readFindArgs(args, "sources");
1917
+ const client = await createConfiguredClient(context);
1918
+ const response = await client.requestJson({
1919
+ path: `/sources?${new URLSearchParams({ q: query })}`,
1920
+ });
1921
+ const normalized = query.toLowerCase();
1922
+ // Older sites ignore q and return every source; keep their results correct too.
1923
+ const data = response.data.filter((source) => source.name.toLowerCase().includes(normalized) ||
1924
+ (source.url?.toLowerCase().includes(normalized) ?? false));
1925
+ if (json) {
1926
+ writeJson(context.writer, { ...response, data });
1927
+ }
1928
+ else {
1929
+ writeSourceList(context.writer, data);
1930
+ }
1931
+ return { exitCode: ExitCode.Ok };
1932
+ }
1933
+ function readFindArgs(args, command) {
1934
+ const positional = args.filter((arg) => arg !== "--json");
1935
+ const query = positional[0]?.trim() ?? "";
1936
+ if (positional.length !== 1 || query === "" || query.startsWith("--")) {
1937
+ throw createInvalidUsageError(`Usage: slug ${command} find <query> [--json] (query must not be blank)`);
1938
+ }
1939
+ return { query, json: args.includes("--json") };
1940
+ }
1840
1941
  async function runSourcesShowCommand(context, args) {
1841
1942
  const { id, json } = readIdCommandArgs(args, "Usage: slug sources show <id> [--json]");
1842
1943
  const client = await createConfiguredClient(context);
@@ -2238,6 +2339,8 @@ async function runAccountsCommand(context, args) {
2238
2339
  switch (args[0]) {
2239
2340
  case "list":
2240
2341
  return runAccountsListCommand(context, args.slice(1));
2342
+ case "find":
2343
+ return runAccountsFindCommand(context, args.slice(1));
2241
2344
  case "show":
2242
2345
  return runAccountsShowCommand(context, args.slice(1));
2243
2346
  case "create":
@@ -2247,7 +2350,7 @@ async function runAccountsCommand(context, args) {
2247
2350
  case "delete":
2248
2351
  return runAccountsDeleteCommand(context, args.slice(1));
2249
2352
  default:
2250
- throw createInvalidUsageError("Usage: slug accounts <list|show|create|edit|delete>");
2353
+ throw createInvalidUsageError("Usage: slug accounts <list|find|show|create|edit|delete>");
2251
2354
  }
2252
2355
  }
2253
2356
  async function runAccountsListCommand(context, args) {
@@ -2267,6 +2370,40 @@ async function runAccountsListCommand(context, args) {
2267
2370
  }
2268
2371
  return { exitCode: ExitCode.Ok };
2269
2372
  }
2373
+ async function runAccountsFindCommand(context, args) {
2374
+ const { query, json } = readAccountFindArgs(args);
2375
+ const client = await createConfiguredClient(context);
2376
+ const response = await client.requestJson({ path: `/accounts?${query}` });
2377
+ const normalized = (query.get("q") ?? "").toLowerCase();
2378
+ // Older sites filter owners but ignore q; apply the text filter locally too.
2379
+ const data = response.data.filter((account) => account.label.toLowerCase().includes(normalized) ||
2380
+ account.url.toLowerCase().includes(normalized));
2381
+ if (json) {
2382
+ writeJson(context.writer, { ...response, data });
2383
+ }
2384
+ else {
2385
+ writeAccountList(context.writer, data);
2386
+ }
2387
+ return { exitCode: ExitCode.Ok };
2388
+ }
2389
+ function readAccountFindArgs(args) {
2390
+ const positional = args.filter((arg) => arg !== "--json");
2391
+ const { query: text } = readFindArgs(positional.slice(0, 1), "accounts");
2392
+ const parsed = parseAccountFlags(positional.slice(1));
2393
+ const query = new URLSearchParams({ q: text });
2394
+ for (const [key, value] of Object.entries(parsed.options)) {
2395
+ if (key === "owner-type" && typeof value === "string") {
2396
+ query.set("ownerType", readAccountOwnerType(value));
2397
+ }
2398
+ else if (key === "owner-id" && typeof value === "string") {
2399
+ query.set("ownerId", String(readPositiveInteger(value, "owner-id")));
2400
+ }
2401
+ else {
2402
+ throw createInvalidUsageError(`Unsupported find option: --${key}`);
2403
+ }
2404
+ }
2405
+ return { query, json: args.includes("--json") };
2406
+ }
2270
2407
  async function runAccountsShowCommand(context, args) {
2271
2408
  const { id, json } = readIdCommandArgs(args, "Usage: slug accounts show <id> [--json]");
2272
2409
  const client = await createConfiguredClient(context);
@@ -2652,6 +2789,8 @@ async function runContactCommand(context, args) {
2652
2789
  switch (args[0]) {
2653
2790
  case "list":
2654
2791
  return runContactListCommand(context, args.slice(1));
2792
+ case "find":
2793
+ return runContactFindCommand(context, args.slice(1));
2655
2794
  case "show":
2656
2795
  return runContactShowCommand(context, args.slice(1));
2657
2796
  case "create":
@@ -2661,7 +2800,7 @@ async function runContactCommand(context, args) {
2661
2800
  case "delete":
2662
2801
  return runContactDeleteCommand(context, args.slice(1));
2663
2802
  default:
2664
- throw createInvalidUsageError("Usage: slug contact <list|show|create|edit|delete>");
2803
+ throw createInvalidUsageError("Usage: slug contact <list|find|show|create|edit|delete>");
2665
2804
  }
2666
2805
  }
2667
2806
  async function runContactListCommand(context, args) {
@@ -2676,6 +2815,24 @@ async function runContactListCommand(context, args) {
2676
2815
  }
2677
2816
  return { exitCode: ExitCode.Ok };
2678
2817
  }
2818
+ async function runContactFindCommand(context, args) {
2819
+ const { query, json } = readFindArgs(args, "contact");
2820
+ const client = await createConfiguredClient(context);
2821
+ const response = await client.requestJson({
2822
+ path: `/contacts?${new URLSearchParams({ q: query })}`,
2823
+ });
2824
+ const normalized = query.toLowerCase();
2825
+ // Older sites ignore q; preserve the complete matching records and their order.
2826
+ const data = response.data.filter((contact) => contact.name.toLowerCase().includes(normalized) ||
2827
+ (contact.url?.toLowerCase().includes(normalized) ?? false));
2828
+ if (json) {
2829
+ writeJson(context.writer, { ...response, data });
2830
+ }
2831
+ else {
2832
+ writeContactList(context.writer, data);
2833
+ }
2834
+ return { exitCode: ExitCode.Ok };
2835
+ }
2679
2836
  async function runContactShowCommand(context, args) {
2680
2837
  const { id, json } = readIdCommandArgs(args, "Usage: slug contact show <id> [--json]");
2681
2838
  const client = await createConfiguredClient(context);
@@ -0,0 +1,17 @@
1
+ import type { SlugHttpClient } from "./http.js";
2
+ export interface FindPostDocument {
3
+ id: number;
4
+ slug: string;
5
+ type: "article" | "link" | "note";
6
+ title?: string | null;
7
+ content?: string;
8
+ excerpt?: string | null;
9
+ url?: string | null;
10
+ publishedAt: string | null;
11
+ }
12
+ interface FindPostsOptions {
13
+ client: SlugHttpClient;
14
+ query: URLSearchParams;
15
+ }
16
+ export declare function findPosts({ client, query }: FindPostsOptions): Promise<FindPostDocument[]>;
17
+ export {};
@@ -0,0 +1,46 @@
1
+ import { CliError, ExitCode } from "./errors.js";
2
+ export async function findPosts({ client, query }) {
3
+ const params = new URLSearchParams(query);
4
+ params.set("limit", "100");
5
+ const normalized = (params.get("q") ?? "").toLowerCase();
6
+ const matches = [];
7
+ const seen = new Set();
8
+ for (let offset = 0;;) {
9
+ params.set("offset", String(offset));
10
+ const { data } = await client.requestJson({
11
+ path: `/posts?${params}`,
12
+ });
13
+ if (data.length === 0)
14
+ return matches;
15
+ for (const post of data) {
16
+ if (seen.has(post.slug)) {
17
+ throw new CliError("Post search pagination repeated a post; cannot guarantee complete results", ExitCode.ApiError);
18
+ }
19
+ seen.add(post.slug);
20
+ if (await matchesPost({ client, post, normalized }))
21
+ matches.push(post);
22
+ }
23
+ // Even a short page may be capped by an older server. Continue until an empty page.
24
+ offset += data.length;
25
+ }
26
+ }
27
+ async function matchesPost({ client, post, normalized, }) {
28
+ if (matchesFields(post, normalized))
29
+ return true;
30
+ if (post.content !== undefined &&
31
+ post.title !== undefined &&
32
+ post.excerpt !== undefined &&
33
+ post.url !== undefined)
34
+ return false;
35
+ // Lists omit content. Do not trust q being honored by older sites or ignore detail errors.
36
+ const { data } = await client.requestJson({
37
+ path: `/posts/${encodeURIComponent(post.slug)}`,
38
+ });
39
+ if (typeof data?.content !== "string") {
40
+ throw new CliError(`Post search detail for ${post.slug} omitted content; cannot guarantee complete results`, ExitCode.ApiError);
41
+ }
42
+ return matchesFields(data, normalized);
43
+ }
44
+ function matchesFields(post, normalized) {
45
+ return [post.title, post.slug, post.content, post.excerpt, post.url].some((value) => value?.toLowerCase().includes(normalized) ?? false);
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evcraddock/slug-cli",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Command-line tool for Slugkit sites.",
5
5
  "private": false,
6
6
  "type": "module",