@softure-ai/blog 0.1.5 → 0.1.7

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.
Files changed (92) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +142 -15
  3. package/dist/cli/report.d.ts +17 -0
  4. package/dist/cli/report.d.ts.map +1 -0
  5. package/dist/cli/report.js +149 -0
  6. package/dist/cli/report.js.map +1 -0
  7. package/dist/cli/run.d.ts +13 -6
  8. package/dist/cli/run.d.ts.map +1 -1
  9. package/dist/cli/run.js +131 -94
  10. package/dist/cli/run.js.map +1 -1
  11. package/dist/contract.d.ts +4 -0
  12. package/dist/contract.d.ts.map +1 -1
  13. package/dist/db/articles.d.ts +10 -1
  14. package/dist/db/articles.d.ts.map +1 -1
  15. package/dist/db/articles.js +36 -4
  16. package/dist/db/articles.js.map +1 -1
  17. package/dist/db/history.d.ts +33 -0
  18. package/dist/db/history.d.ts.map +1 -0
  19. package/dist/db/history.js +66 -0
  20. package/dist/db/history.js.map +1 -0
  21. package/dist/db/publish-run.d.ts +11 -0
  22. package/dist/db/publish-run.d.ts.map +1 -1
  23. package/dist/db/publish-run.js +14 -4
  24. package/dist/db/publish-run.js.map +1 -1
  25. package/dist/index.js +1 -1
  26. package/dist/next/context.js +2 -2
  27. package/dist/next/context.js.map +1 -1
  28. package/dist/options.d.ts.map +1 -1
  29. package/dist/options.js +3 -1
  30. package/dist/options.js.map +1 -1
  31. package/dist/pages/accept.d.ts +7 -0
  32. package/dist/pages/accept.d.ts.map +1 -0
  33. package/dist/pages/accept.js +34 -0
  34. package/dist/pages/accept.js.map +1 -0
  35. package/dist/proxy/index.d.ts +15 -0
  36. package/dist/proxy/index.d.ts.map +1 -1
  37. package/dist/proxy/index.js +54 -4
  38. package/dist/proxy/index.js.map +1 -1
  39. package/dist/quality/catalog.d.ts.map +1 -1
  40. package/dist/quality/catalog.js +4 -1
  41. package/dist/quality/catalog.js.map +1 -1
  42. package/dist/quality/check-article.d.ts.map +1 -1
  43. package/dist/quality/check-article.js +2 -1
  44. package/dist/quality/check-article.js.map +1 -1
  45. package/dist/quality/options.d.ts.map +1 -1
  46. package/dist/quality/options.js +3 -1
  47. package/dist/quality/options.js.map +1 -1
  48. package/dist/quality/rules/blocks.d.ts +8 -1
  49. package/dist/quality/rules/blocks.d.ts.map +1 -1
  50. package/dist/quality/rules/blocks.js +26 -0
  51. package/dist/quality/rules/blocks.js.map +1 -1
  52. package/dist/render/article-markdown.d.ts +12 -0
  53. package/dist/render/article-markdown.d.ts.map +1 -0
  54. package/dist/render/article-markdown.js +21 -0
  55. package/dist/render/article-markdown.js.map +1 -0
  56. package/dist/render/index.d.ts +2 -1
  57. package/dist/render/index.d.ts.map +1 -1
  58. package/dist/render/index.js +2 -1
  59. package/dist/render/index.js.map +1 -1
  60. package/dist/render/render-article.d.ts +40 -4
  61. package/dist/render/render-article.d.ts.map +1 -1
  62. package/dist/render/render-article.js +132 -10
  63. package/dist/render/render-article.js.map +1 -1
  64. package/dist/server/index.d.ts +2 -1
  65. package/dist/server/index.d.ts.map +1 -1
  66. package/dist/server/index.js +1 -0
  67. package/dist/server/index.js.map +1 -1
  68. package/dist/sitemap.js +2 -2
  69. package/dist/sitemap.js.map +1 -1
  70. package/module.json +1 -1
  71. package/package.json +7 -3
  72. package/skill/references/rules.md +2 -1
  73. package/src/cli/report.ts +182 -0
  74. package/src/cli/run.ts +132 -100
  75. package/src/contract.ts +9 -1
  76. package/src/db/articles.ts +39 -4
  77. package/src/db/history.ts +83 -0
  78. package/src/db/publish-run.ts +24 -4
  79. package/src/index.ts +1 -1
  80. package/src/next/context.ts +2 -2
  81. package/src/options.ts +6 -2
  82. package/src/pages/accept.ts +39 -0
  83. package/src/proxy/index.ts +62 -4
  84. package/src/quality/catalog.ts +4 -1
  85. package/src/quality/check-article.ts +2 -1
  86. package/src/quality/options.ts +6 -2
  87. package/src/quality/rules/blocks.ts +26 -1
  88. package/src/render/article-markdown.ts +34 -0
  89. package/src/render/index.ts +6 -0
  90. package/src/render/render-article.ts +170 -16
  91. package/src/server/index.ts +2 -0
  92. package/src/sitemap.ts +2 -2
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ Newest first. Each version lists what changed for an app that uses `@softure-ai/blog`. When an app has run a version in
4
+ production, the version gets a line `verified in: <app>@<commit>` ([docs/05](../../docs/05-adoption-playbook.md),
5
+ "Definition of done"). Versions before the first one below are described in their GitHub Releases (`blog@x.y.z`).
6
+
7
+ ## 0.1.7
8
+
9
+ - Block plugins with `syntax: "directive"` render top-level `::name{key="value"}` lines with parsed
10
+ `attributes`; the quality gate checks their `requires` and reports an unknown directive or unreadable
11
+ attributes (`block-directive`). `ArticleBlock` and `FoundBlock` carry `syntax` and `attributes`.
12
+ - `createBlogMarkdown` (`/proxy`) answers an article or term asked for with `Accept: text/markdown` with
13
+ `toArticleMarkdown`; a block plugin may give its Markdown form (`markdown`).
14
+ - `softure-blog publish --stdin` reads one file (`--name`) or a JSON bundle (files and an optional
15
+ history) from standard input; `--format lines` prints a stable `blog|<key>|…` contract.
16
+ - `publish --history <file.json>` (and `runBlogPublish({ history })`) imports `published_at`, `updated_at`
17
+ and old slugs on the first publish of each article, for an app moving its existing blog in.
18
+
19
+ ## 0.1.6
20
+
21
+ - Adapters and commands use the configured database handle.
22
+ - `@softure-ai/ui` is a peer dependency; the package keeps its own CSS.
package/README.md CHANGED
@@ -4,9 +4,6 @@ Articles and glossary terms kept as Markdown files in the app's repository, and
4
4
  brings the module's tables to the state of those files. The files are the source of truth: there is no
5
5
  editor and no CMS, a text changes only through a commit and `softure-blog publish`.
6
6
 
7
- This release holds the content store (roadmap item BL-2), the server-side renderer (BL-3), the pages
8
- (BL-4), the text quality gate (BL-6) and the writing skill (BL-7). RSS, sitemap and IndexNow (BL-5) build on them.
9
-
10
7
  ## 1. What it provides
11
8
 
12
9
  - A strict article file format: a YAML frontmatter with English keys (an unknown key is an error),
@@ -14,12 +11,15 @@ This release holds the content store (roadmap item BL-2), the server-side render
14
11
  - `blog.articles` and `blog.slug_history` with database constraints for every invariant that fits one.
15
12
  - `softure-blog publish`: a dry run by default; with `--commit`, all files or none; unchanged files are
16
13
  skipped by their content hash; a slug change keeps the old slug as a redirect; one pillar per cluster.
14
+ Files from paths or from standard input (`--stdin`, for a release through an ssh pipe), a line
15
+ contract for scripts (`--format lines`), and the dates and old slugs of an existing blog imported on
16
+ the first publish (`--history`).
17
17
  - Read functions for the pages: `getPublishedArticle`, `findArticleBySlug`, `findSlugRedirect`,
18
18
  `listArticles`.
19
19
  - `renderArticle(markdown, options)`: the body as safe HTML on the server (no raw HTML, safe link
20
20
  schemes only, marked external links, images under the app's image policy), heading ids and an
21
21
  optional table of contents, glossary links on the first mention of a term, block plugins for the
22
- app's own fenced blocks, reading time.
22
+ app's own fenced blocks and `::directive{…}` lines, reading time.
23
23
  - Pages, each mounted with one re-export line (`@softure-ai/blog/next`): the listing grouped by cluster
24
24
  with the pillar first, an article (dates, summary, contents, FAQ, sources, signature, disclaimer,
25
25
  `BlogPosting`/`BreadcrumbList`/`FAQPage` JSON-LD), the glossary index and a term page (`DefinedTerm`,
@@ -27,7 +27,8 @@ This release holds the content store (roadmap item BL-2), the server-side render
27
27
  Their canonical, Open Graph, JSON-LD and feed URLs follow `@softure-ai/seo`'s origin, host and
28
28
  trailing-slash rule when the app lists `seo()` (core's `getSiteUrls`), and `appOrigin` otherwise.
29
29
  - `createBlogRedirects` (`@softure-ai/blog/proxy`): 301 from an old slug, 410 for a withdrawn text,
30
- in the app's `proxy.ts`.
30
+ in the app's `proxy.ts`; `createBlogMarkdown` answers an article or term asked for with
31
+ `Accept: text/markdown` with its Markdown (`toArticleMarkdown`), for agents.
31
32
  - `@softure-ai/blog/styles.css`: the pages and the rendered body on the `--sft-*` tokens.
32
33
  - Discovery: an RSS 2.0 feed (`serveBlogRss`), "read next" under every article (its cluster first, the
33
34
  pillar on top), and, with `@softure-ai/seo` (optional): sitemap entries with each text's real
@@ -45,9 +46,14 @@ This release holds the content store (roadmap item BL-2), the server-side render
45
46
  ## 2. Installation
46
47
 
47
48
  ```bash
48
- npm install @softure-ai/blog
49
+ npm install @softure-ai/blog @softure-ai/ui
49
50
  ```
50
51
 
52
+ `@softure-ai/ui` is a peer dependency (any 0.1.x), like the optional `@softure-ai/security` and
53
+ `@softure-ai/seo`: the app installs it once, so the theme tokens come from one copy. Importing
54
+ `@softure-ai/blog/styles.css` from JavaScript is safe with tree-shaking, because the package marks its CSS as a side
55
+ effect.
56
+
51
57
  Then add `blog()` to the modules of `softure.config.ts` and run `softure migrate`.
52
58
 
53
59
  ## 3. Configuration
@@ -278,7 +284,8 @@ The quality gate resolves internal links through `quality.paths` (default `/blog
278
284
  The commands:
279
285
 
280
286
  ```bash
281
- softure-blog publish [<path>...] [--commit] [--withdraw] [--no-indexnow] [--app-url <origin>] [--config <file>]
287
+ softure-blog publish [<path>...] [--commit] [--withdraw] [--no-indexnow] [--app-url <origin>]
288
+ [--stdin [--name <slug>.md]] [--history <file.json>] [--format text|lines] [--config <file>]
282
289
  softure-blog check [<path>...] [--external] [--today <YYYY-MM-DD>] [--config <file>]
283
290
  softure-blog skill install [--dir <path>] [--command <cmd>] [--check] [--config <file>]
284
291
  ```
@@ -298,6 +305,13 @@ softure-blog skill install [--dir <path>] [--command <cmd>] [--check] [--config
298
305
  listing or the glossary of its kind) as canonical URLs on seo's origin; a dry run prints them;
299
306
  `--no-indexnow` skips the submit (e.g. a local or CI database). A failed submit is a warning: the
300
307
  publish stays written and the exit code stays 0.
308
+ - `--stdin` reads the files from standard input instead of paths: with `--name <slug>.md`, the one file's
309
+ text; without it, a JSON bundle `{"files":[{"name":"<slug>.md","text":"…"}]}` (a whole folder, and
310
+ optionally `"history"`, the content of a `--history` file, so a container needs no file for it). A
311
+ release that publishes inside a container through an ssh gateway pipes the content in, so the image
312
+ needs no copy of `content/`. `--stdin` takes no paths; `--withdraw` with it needs `--name`.
313
+ - `--history <file.json>` imports the earlier life of an existing blog (see "Moving an existing blog in").
314
+ - `--format lines` prints the line contract below instead of the text for people.
301
315
  - Exit codes: 0 done, 1 refused or failed (nothing written), 2 usage error.
302
316
 
303
317
  Output, one line per text, then a summary:
@@ -310,8 +324,73 @@ summary: added 1, changed 1, unchanged 12
310
324
  dry run: nothing written; pass --commit to write
311
325
  ```
312
326
 
327
+ **The line contract** (`--format lines`), for a release script that greps the output. Every line goes to
328
+ standard output and starts `blog|`; fields are separated by `|`, and a `|` or a line break inside a value
329
+ becomes a space. Keys are stable: a new one may be added, an existing one never changes meaning. A run
330
+ prints exactly one outcome line: `blog|written`, `blog|dry-run`, `blog|refused` or `blog|failed|<message>`.
331
+
332
+ ```text
333
+ blog|warning|<subject>|<message>
334
+ blog|error|<subject>|<message>
335
+ blog|change|<added|changed|unchanged>|<id>|<status/slug before, or none>|<status/slug after>
336
+ blog|moved|<id>|<old slug>|<new slug>
337
+ blog|imported|<id>|<published_at ISO, or none>|<old slugs>
338
+ blog|summary|<added>|<changed>|<unchanged>
339
+ blog|written
340
+ blog|cache|off|<revalidateSeconds> | skipped | dry-run|<url> | refreshed|<url> | failed|<code>|<reason>
341
+ blog|indexnow|off|<reason> | skipped | dry-run|<count>|<urls> | submitted|<count>|<status>|<paths> | failed|<code>|<reason>|<paths>
342
+ ```
343
+
344
+ ```bash
345
+ # A release step: the content goes in on stdin, the contract comes back.
346
+ node -e 'const fs = require("fs"); const dir = "content/blog";
347
+ const files = fs.readdirSync(dir).filter((n) => n.endsWith(".md") && n !== "README.md").sort()
348
+ .map((name) => ({ name, text: fs.readFileSync(`${dir}/${name}`, "utf8") }));
349
+ process.stdout.write(JSON.stringify({ files }))' > bundle.json
350
+ OUT="$(ssh deploy@host 'docker compose exec -T app node blog.cjs publish --stdin --commit --format lines' < bundle.json)"
351
+ grep -q '^blog|written$' <<< "$OUT" || { grep '^blog|\(error\|failed\)' <<< "$OUT"; exit 1; }
352
+ ```
353
+
354
+ **Moving an existing blog in.** An app that already published its texts from its own tables keeps their
355
+ `published_at` (often only in its database, not in the files), their `updated_at` and their old slugs
356
+ with `--history <file.json>`. On the first publish of each article (no row in `blog.articles` yet), the
357
+ article takes `published_at` from the history unless its file sets one, `updated_at` from the history,
358
+ and its old slugs enter `blog.slug_history` (301s keep working); the run prints `imported` for it. An
359
+ article that already has a row ignores its entry, so the same file can stay in a release script; an
360
+ entry for an article outside the run is a warning. An old slug another article holds refuses the run.
361
+
362
+ ```json
363
+ {
364
+ "articles": [
365
+ {
366
+ "id": "index-funds",
367
+ "published_at": "2026-03-01T08:00:00+01:00",
368
+ "updated_at": "2026-06-15T10:30:00Z",
369
+ "old_slugs": [{ "slug": "what-is-an-index-fund", "changed_at": "2026-04-01T00:00:00Z" }]
370
+ },
371
+ { "id": "draft-text", "published_at": null }
372
+ ]
373
+ }
374
+ ```
375
+
376
+ `published_at` is required (`null` for a text never published), `updated_at` needs it, ids and slugs are
377
+ kebab-case, each article and old slug appears once. Timestamps keep millisecond precision. One query over
378
+ an app's own tables (here `blog_articles(id, published_at, updated_at)` and
379
+ `blog_slug_history(old_slug, article_id, changed_at)`) writes the file:
380
+
381
+ ```sql
382
+ \copy (SELECT json_build_object('articles', coalesce(json_agg(json_build_object(
383
+ 'id', a.id, 'published_at', a.published_at, 'updated_at', a.updated_at,
384
+ 'old_slugs', coalesce((SELECT json_agg(json_build_object('slug', h.old_slug, 'changed_at', h.changed_at))
385
+ FROM blog_slug_history h WHERE h.article_id = a.id), '[]'::json))), '[]'::json))
386
+ FROM blog_articles a) TO 'history.json'
387
+ ```
388
+
389
+ `publishArticle(ctx, input, { history })` and `runBlogPublish(ctx, files, { history })` (with
390
+ `parseArticleHistory(json)`) are the same import without the command line.
391
+
313
392
  Like `softure migrate`, the bin loads `softure.config.(ts|mts|js|mjs)` with Node and opens
314
- `database.url`. When Node cannot load the config (path aliases, a bundled container), call
393
+ `database.handle` when the config sets one, otherwise `database.url`. When Node cannot load the config (path aliases, a bundled container), call
315
394
  `runBlogCli` from an app script:
316
395
 
317
396
  ```ts
@@ -457,9 +536,56 @@ const chartBlock: BlockPlugin = {
457
536
 
458
537
  Plugin output is the app's own code and is trusted as is: escape what goes into its HTML. A plugin
459
538
  that throws fails the render (a bug, not content). Without the plugin the same fence renders as a code
460
- block. `findArticleBlocks(markdown, plugins)` lists the blocks a text uses with their line and
461
- `requires`, without rendering. When any block returns a node, `html` is `null`: render `segments` in
462
- order (`html` segments as HTML, `node` segments as they are).
539
+ block. `findArticleBlocks(markdown, plugins)` lists the blocks a text uses with their line, syntax,
540
+ attributes and `requires`, without rendering. When any block returns a node, `html` is `null`: render
541
+ `segments` in order (`html` segments as HTML, `node` segments as they are).
542
+
543
+ **Directive plugins.** A plugin with `syntax: "directive"` renders a top-level line `::name{…}` instead
544
+ of a fence, the way many Markdown blogs embed a chart or a tool:
545
+
546
+ ```md
547
+ Paragraph before.
548
+ ::chart{type="wealth" scenario="w=35&d=300000" title="Your wealth"}
549
+ ```
550
+
551
+ ```ts
552
+ const chartDirective: BlockPlugin = {
553
+ type: "chart",
554
+ syntax: "directive",
555
+ requires: ["current_as_of"],
556
+ render: ({ attributes, article }) =>
557
+ attributes === null
558
+ ? { kind: "html", html: '<figure class="chart-error">…</figure>' }
559
+ : { kind: "html", html: renderChart(attributes, article) },
560
+ markdown: ({ attributes }) => chartAsTable(attributes), // optional, for Accept: text/markdown
561
+ };
562
+ ```
563
+
564
+ - The line stands alone (it may follow a paragraph line directly); the braces are optional; values are
565
+ double-quoted and hold no `"`; each key appears once. `attributes` holds them, `info` the raw text
566
+ inside the braces, `content` the whole line. Attributes that cannot be read reach the plugin as `null`
567
+ (show an error frame) and the gate refuses them.
568
+ - Only top-level lines count: a directive in a list, a quote, a fence or indented code stays text, and
569
+ so does a name no directive plugin registers. One type may have a fence plugin and a directive plugin.
570
+ - Register the same plugins in `quality.blocks`: the gate reports a directive's missing `requires`, and
571
+ with any directive plugin registered `block-directive` reports a `::name` line no plugin renders (a
572
+ typo would show as a paragraph) or one whose attributes cannot be read.
573
+ - `parseDirectiveLine` and `parseDirectiveAttributes` (`@softure-ai/blog/server`) are the parser.
574
+
575
+ **Markdown for agents.** `createBlogMarkdown(config)` (`@softure-ai/blog/proxy`) answers a GET or HEAD
576
+ of a published article or term whose `Accept` names `text/markdown` with at least the weight of
577
+ `text/html` (never `*/*`: browsers and crawlers keep the page) with `toArticleMarkdown(article)`: the
578
+ title, description, the day the facts were checked, the summary, the stored body, sources and FAQ.
579
+ Plugin blocks keep their source unless the plugin has `markdown`. The answer carries `Vary: Accept` and
580
+ `cache-control: private`, so a shared cache never hands Markdown to a browser. Put it before the redirects:
581
+
582
+ ```ts
583
+ const blogMarkdown = createBlogMarkdown(softureConfig);
584
+ const blogRedirects = createBlogRedirects(softureConfig);
585
+ export async function proxy(request: NextRequest) {
586
+ return (await blogMarkdown(request)) ?? (await blogRedirects(request)) ?? NextResponse.next();
587
+ }
588
+ ```
463
589
 
464
590
  ## 5. Migrations and tables
465
591
 
@@ -565,8 +691,8 @@ export const tickerPlugin: QualityPlugin = {
565
691
  plugins in `quality.blocks` (type, info, fence line, `requires`), `today` and the language `ruleset` (for
566
692
  its number notation); the text helpers (`toProse`, `splitSentences`, `findSignificantNumbers`, …) are
567
693
  exported from `@softure-ai/blog/server`.
568
- - `renderArticle({ blocks })` and `blog({ blocks })`: block plugins for the app's fenced blocks
569
- (FIRE_TRACKER's engine chart).
694
+ - `renderArticle({ blocks })` and `blog({ blocks })`: block plugins for the app's fenced blocks and
695
+ `::directive` lines (an engine chart), with an optional Markdown form for agents.
570
696
  - The pages' `cta` and `afterArticle` slots: the app's call to action and blocks (a waitlist form).
571
697
 
572
698
  ## 11. GDPR
@@ -578,13 +704,14 @@ Articles hold editorial content, no personal data: nothing to export or delete.
578
704
  - Two runs that rename one article away from a slug and give it to another at the same moment can leave the
579
705
  slug both current and in the slug history (BF-12).
580
706
  - The content hash is part of the contract: a field added later enters it only when present.
581
- - No `--stdin` (a deploy transport).
582
707
  - The refresh route expires the cache of the instance that answers it. With several instances and Next's
583
708
  default (in-memory) cache handler, the others show a publish after `revalidateSeconds`; a shared cache
584
709
  handler covers them.
585
710
  - The renderer has no raw HTML and no figures: an image has no caption, and the app hosts and sizes its
586
711
  images itself (no `next/image`). A plugin fence inside a list or a quote stays a code
587
- block (a block node cannot sit inside a list's HTML).
712
+ block (a block node cannot sit inside a list's HTML), and a `::directive` there stays text; the gate
713
+ does not report a registered directive in such a place.
714
+ - Only leaf directives (`::name{…}`, one line): no container (`:::name … :::`) or inline (`:name[…]`) ones.
588
715
  - The gate reads Markdown line by line (blocks, not a syntax tree): enough for the rules, not a
589
716
  renderer. Fenced code and HTML comments are skipped.
590
717
  - **Adopting FIRE_TRACKER's gate:** `language: "pl"`, `ymyl: { ownCalculationMark }` with its calculation
@@ -0,0 +1,17 @@
1
+ import type { BlogRefreshOutcome } from "../discovery/refresh.js";
2
+ import type { BlogIndexNowSubmit } from "../discovery/submit.js";
3
+ import type { BlogPublishRun } from "../db/publish-run.js";
4
+ export interface CliOutput {
5
+ readonly log: (line: string) => void;
6
+ readonly error: (line: string) => void;
7
+ }
8
+ export type PublishFormat = "text" | "lines";
9
+ export interface PublishReporter {
10
+ /** A problem before or outside the run (a file it cannot read, the database); the run's outcome. */
11
+ readonly failed: (message: string) => void;
12
+ readonly run: (run: BlogPublishRun) => void;
13
+ readonly refresh: (outcome: BlogRefreshOutcome) => void;
14
+ readonly indexNow: (submit: BlogIndexNowSubmit) => void;
15
+ }
16
+ export declare function createPublishReporter(format: PublishFormat, output: CliOutput): PublishReporter;
17
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAElE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAmC,MAAM,sBAAsB,CAAC;AAE5F,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,CAAC;AAE7C,MAAM,WAAW,eAAe;IAC9B,oGAAoG;IACpG,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACxD,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;CACzD;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,SAAS,GAAG,eAAe,CAE/F"}
@@ -0,0 +1,149 @@
1
+ import { BLOG_REFRESH_SECRET_ENV } from "../discovery/refresh.js";
2
+ export function createPublishReporter(format, output) {
3
+ return format === "lines" ? createLineReporter(output) : createTextReporter(output);
4
+ }
5
+ function countActions(run) {
6
+ const count = (action) => run.changes.filter((change) => change.action === action).length;
7
+ return { added: count("added"), changed: count("changed"), unchanged: count("unchanged") };
8
+ }
9
+ function formatBefore(change) {
10
+ return change.statusBefore === null || change.slugBefore === null ? "none" : `${change.statusBefore}/${change.slugBefore}`;
11
+ }
12
+ function formatDate(date) {
13
+ return date === null ? "none" : date.toISOString();
14
+ }
15
+ function createTextReporter(output) {
16
+ const formatProblem = (problem) => `${problem.subject}: ${problem.message}`;
17
+ return {
18
+ failed: (message) => output.error(`softure-blog publish: ${message}`),
19
+ run: (run) => {
20
+ for (const warning of run.warnings)
21
+ output.error(`warning ${formatProblem(warning)}`);
22
+ if (run.status === "refused") {
23
+ for (const problem of run.problems)
24
+ output.error(`error ${formatProblem(problem)}`);
25
+ output.error("refused: nothing written; fix the problems above");
26
+ return;
27
+ }
28
+ for (const change of run.changes) {
29
+ output.log(`${change.action} ${change.id} ${formatBefore(change)} -> ${change.statusAfter}/${change.slug}`);
30
+ if (change.previousSlug !== null)
31
+ output.log(`moved ${change.id} ${change.previousSlug} -> ${change.slug}`);
32
+ if (change.imported !== undefined) {
33
+ output.log(`imported ${change.id} published_at=${formatDate(change.imported.publishedAt)} old_slugs=${String(change.imported.oldSlugs)}`);
34
+ }
35
+ }
36
+ const counts = countActions(run);
37
+ output.log(`summary: added ${String(counts.added)}, changed ${String(counts.changed)}, unchanged ${String(counts.unchanged)}`);
38
+ output.log(run.committed ? "written" : "dry run: nothing written; pass --commit to write");
39
+ },
40
+ refresh: (outcome) => {
41
+ switch (outcome.kind) {
42
+ case "not_configured":
43
+ output.log(`cache: the running app shows the change within revalidateSeconds (${String(outcome.revalidateSeconds)} s); set ${BLOG_REFRESH_SECRET_ENV} to refresh it now`);
44
+ return;
45
+ case "skipped":
46
+ output.log("cache: no text changed, nothing to refresh");
47
+ return;
48
+ case "dry_run":
49
+ output.log(`cache: dry run, a commit would refresh ${outcome.url}`);
50
+ return;
51
+ case "refreshed":
52
+ output.log(`cache: refreshed ${outcome.url}`);
53
+ return;
54
+ case "failed":
55
+ output.error(`warning cache: ${outcome.reason} (${outcome.code}); the publish is written, the app shows it within ${String(outcome.revalidateSeconds)} s`);
56
+ return;
57
+ }
58
+ },
59
+ indexNow: ({ paths, outcome }) => {
60
+ switch (outcome.kind) {
61
+ case "not_configured":
62
+ output.log(`indexnow: off, ${outcome.reason}`);
63
+ return;
64
+ case "skipped":
65
+ output.log("indexnow: no public address changed, nothing to submit");
66
+ return;
67
+ case "dry_run":
68
+ output.log(`indexnow: dry run, a commit would submit ${String(outcome.urls.length)} URL(s): ${outcome.urls.join(" ")}`);
69
+ return;
70
+ case "submitted":
71
+ output.log(`indexnow: submitted ${String(outcome.count)} URL(s) (${String(outcome.status)}): ${paths.join(" ")}`);
72
+ return;
73
+ case "failed":
74
+ output.error(`warning indexnow: ${outcome.reason} (${outcome.code}); the publish is written, submit the addresses later: ${paths.join(" ")}`);
75
+ return;
76
+ }
77
+ },
78
+ };
79
+ }
80
+ /** A value inside a contract line: no field separator, no line break. */
81
+ function toField(value) {
82
+ return String(value).replace(/[|\r\n]+/g, " ");
83
+ }
84
+ function createLineReporter(output) {
85
+ // Every contract line goes to standard output, so `2>&1` is not needed to read them.
86
+ const line = (...fields) => output.log(["blog", ...fields.map(toField)].join("|"));
87
+ return {
88
+ failed: (message) => line("failed", message),
89
+ run: (run) => {
90
+ for (const warning of run.warnings)
91
+ line("warning", warning.subject, warning.message);
92
+ if (run.status === "refused") {
93
+ for (const problem of run.problems)
94
+ line("error", problem.subject, problem.message);
95
+ line("refused");
96
+ return;
97
+ }
98
+ for (const change of run.changes) {
99
+ line("change", change.action, change.id, formatBefore(change), `${change.statusAfter}/${change.slug}`);
100
+ if (change.previousSlug !== null)
101
+ line("moved", change.id, change.previousSlug, change.slug);
102
+ if (change.imported !== undefined)
103
+ line("imported", change.id, formatDate(change.imported.publishedAt), change.imported.oldSlugs);
104
+ }
105
+ const counts = countActions(run);
106
+ line("summary", counts.added, counts.changed, counts.unchanged);
107
+ line(run.committed ? "written" : "dry-run");
108
+ },
109
+ refresh: (outcome) => {
110
+ switch (outcome.kind) {
111
+ case "not_configured":
112
+ line("cache", "off", outcome.revalidateSeconds);
113
+ return;
114
+ case "skipped":
115
+ line("cache", "skipped");
116
+ return;
117
+ case "dry_run":
118
+ line("cache", "dry-run", outcome.url);
119
+ return;
120
+ case "refreshed":
121
+ line("cache", "refreshed", outcome.url);
122
+ return;
123
+ case "failed":
124
+ line("cache", "failed", outcome.code, outcome.reason);
125
+ return;
126
+ }
127
+ },
128
+ indexNow: ({ paths, outcome }) => {
129
+ switch (outcome.kind) {
130
+ case "not_configured":
131
+ line("indexnow", "off", outcome.reason);
132
+ return;
133
+ case "skipped":
134
+ line("indexnow", "skipped");
135
+ return;
136
+ case "dry_run":
137
+ line("indexnow", "dry-run", outcome.urls.length, outcome.urls.join(" "));
138
+ return;
139
+ case "submitted":
140
+ line("indexnow", "submitted", outcome.count, outcome.status, paths.join(" "));
141
+ return;
142
+ case "failed":
143
+ line("indexnow", "failed", outcome.code, outcome.reason, paths.join(" "));
144
+ return;
145
+ }
146
+ },
147
+ };
148
+ }
149
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/cli/report.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAmBlE,MAAM,UAAU,qBAAqB,CAAC,MAAqB,EAAE,MAAiB;IAC5E,OAAO,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC;AACtF,CAAC;AAED,SAAS,YAAY,CAAC,GAAgD;IACpE,MAAM,KAAK,GAAG,CAAC,MAAiC,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,MAAM,CAAC;IACrH,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC;AAC7F,CAAC;AAED,SAAS,YAAY,CAAC,MAAuB;IAC3C,OAAO,MAAM,CAAC,YAAY,KAAK,IAAI,IAAI,MAAM,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,YAAY,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;AAC7H,CAAC;AAED,SAAS,UAAU,CAAC,IAAiB;IACnC,OAAO,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;AACrD,CAAC;AAED,SAAS,kBAAkB,CAAC,MAAiB;IAC3C,MAAM,aAAa,GAAG,CAAC,OAAuB,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,OAAO,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC;IAC5F,OAAO;QACL,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,yBAAyB,OAAO,EAAE,CAAC;QACrE,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE;YACX,KAAK,MAAM,OAAO,IAAI,GAAG,CAAC,QAAQ;gBAAE,MAAM,CAAC,KAAK,CAAC,WAAW,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACtF,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;gBAC7B,KAAK,MAAM,OAAO,IAAI,GAAG,CAAC,QAAQ;oBAAE,MAAM,CAAC,KAAK,CAAC,SAAS,aAAa,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;gBACpF,MAAM,CAAC,KAAK,CAAC,kDAAkD,CAAC,CAAC;gBACjE,OAAO;YACT,CAAC;YACD,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;gBACjC,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,EAAE,IAAI,YAAY,CAAC,MAAM,CAAC,OAAO,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;gBAC5G,IAAI,MAAM,CAAC,YAAY,KAAK,IAAI;oBAAE,MAAM,CAAC,GAAG,CAAC,SAAS,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,YAAY,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;gBAC5G,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;oBAClC,MAAM,CAAC,GAAG,CAAC,YAAY,MAAM,CAAC,EAAE,iBAAiB,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,cAAc,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;gBAC5I,CAAC;YACH,CAAC;YACD,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;YACjC,MAAM,CAAC,GAAG,CAAC,kBAAkB,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,aAAa,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,eAAe,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;YAC/H,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,kDAAkD,CAAC,CAAC;QAC7F,CAAC;QACD,OAAO,EAAE,CAAC,OAAO,EAAE,EAAE;YACnB,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;gBACrB,KAAK,gBAAgB;oBACnB,MAAM,CAAC,GAAG,CAAC,qEAAqE,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,YAAY,uBAAuB,oBAAoB,CAAC,CAAC;oBAC1K,OAAO;gBACT,KAAK,SAAS;oBACZ,MAAM,CAAC,GAAG,CAAC,4CAA4C,CAAC,CAAC;oBACzD,OAAO;gBACT,KAAK,SAAS;oBACZ,MAAM,CAAC,GAAG,CAAC,0CAA0C,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;oBACpE,OAAO;gBACT,KAAK,WAAW;oBACd,MAAM,CAAC,GAAG,CAAC,oBAAoB,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;oBAC9C,OAAO;gBACT,KAAK,QAAQ;oBACX,MAAM,CAAC,KAAK,CAAC,kBAAkB,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,IAAI,sDAAsD,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;oBAC3J,OAAO;YACX,CAAC;QACH,CAAC;QACD,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE;YAC/B,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;gBACrB,KAAK,gBAAgB;oBACnB,MAAM,CAAC,GAAG,CAAC,kBAAkB,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;oBAC/C,OAAO;gBACT,KAAK,SAAS;oBACZ,MAAM,CAAC,GAAG,CAAC,wDAAwD,CAAC,CAAC;oBACrE,OAAO;gBACT,KAAK,SAAS;oBACZ,MAAM,CAAC,GAAG,CAAC,4CAA4C,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;oBACxH,OAAO;gBACT,KAAK,WAAW;oBACd,MAAM,CAAC,GAAG,CAAC,uBAAuB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,YAAY,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;oBAClH,OAAO;gBACT,KAAK,QAAQ;oBACX,MAAM,CAAC,KAAK,CAAC,qBAAqB,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,IAAI,0DAA0D,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;oBAC9I,OAAO;YACX,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED,yEAAyE;AACzE,SAAS,OAAO,CAAC,KAAsB;IACrC,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;AACjD,CAAC;AAED,SAAS,kBAAkB,CAAC,MAAiB;IAC3C,qFAAqF;IACrF,MAAM,IAAI,GAAG,CAAC,GAAG,MAAoC,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACjH,OAAO;QACL,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC;QAC5C,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE;YACX,KAAK,MAAM,OAAO,IAAI,GAAG,CAAC,QAAQ;gBAAE,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;YACtF,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;gBAC7B,KAAK,MAAM,OAAO,IAAI,GAAG,CAAC,QAAQ;oBAAE,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;gBACpF,IAAI,CAAC,SAAS,CAAC,CAAC;gBAChB,OAAO;YACT,CAAC;YACD,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;gBACjC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,YAAY,CAAC,MAAM,CAAC,EAAE,GAAG,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;gBACvG,IAAI,MAAM,CAAC,YAAY,KAAK,IAAI;oBAAE,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;gBAC7F,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS;oBAAE,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YACpI,CAAC;YACD,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;YAChE,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QAC9C,CAAC;QACD,OAAO,EAAE,CAAC,OAAO,EAAE,EAAE;YACnB,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;gBACrB,KAAK,gBAAgB;oBACnB,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAC;oBAChD,OAAO;gBACT,KAAK,SAAS;oBACZ,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;oBACzB,OAAO;gBACT,KAAK,SAAS;oBACZ,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;oBACtC,OAAO;gBACT,KAAK,WAAW;oBACd,IAAI,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;oBACxC,OAAO;gBACT,KAAK,QAAQ;oBACX,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;oBACtD,OAAO;YACX,CAAC;QACH,CAAC;QACD,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE;YAC/B,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;gBACrB,KAAK,gBAAgB;oBACnB,IAAI,CAAC,UAAU,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;oBACxC,OAAO;gBACT,KAAK,SAAS;oBACZ,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;oBAC5B,OAAO;gBACT,KAAK,SAAS;oBACZ,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;oBACzE,OAAO;gBACT,KAAK,WAAW;oBACd,IAAI,CAAC,UAAU,EAAE,WAAW,EAAE,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;oBAC9E,OAAO;gBACT,KAAK,QAAQ;oBACX,IAAI,CAAC,UAAU,EAAE,QAAQ,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;oBAC1E,OAAO;YACX,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
package/dist/cli/run.d.ts CHANGED
@@ -2,10 +2,8 @@ import { type Clock, type SoftureConfig } from "@softure-ai/core";
2
2
  import { type DatabaseHandle } from "@softure-ai/db";
3
3
  import { type PublishGate } from "../db/publish-run.js";
4
4
  import type { FetchLike } from "../quality/external-links.js";
5
- export interface CliOutput {
6
- readonly log: (line: string) => void;
7
- readonly error: (line: string) => void;
8
- }
5
+ import { type CliOutput, type PublishFormat } from "./report.js";
6
+ export type { CliOutput };
9
7
  export interface RunBlogCliOptions {
10
8
  /** The app's config with `blog()` among its modules. */
11
9
  readonly config: SoftureConfig;
@@ -14,7 +12,7 @@ export interface RunBlogCliOptions {
14
12
  /** Relative paths resolve against it. Default: `process.cwd()`. */
15
13
  readonly cwd?: string;
16
14
  readonly output?: CliOutput;
17
- /** Opens the database connection. Default: `createDatabase(url, { max: 1 })`. */
15
+ /** Opens the database connection. Default: the config's `database.handle`, else `createDatabase(url, { max: 1 })`. */
18
16
  readonly openDatabase?: (url: string) => Promise<DatabaseHandle>;
19
17
  /** The gate for files going public. Default: the quality gate of `blog({ quality })`, none with `quality: false`. */
20
18
  readonly gate?: PublishGate;
@@ -29,11 +27,13 @@ export interface RunBlogCliOptions {
29
27
  readonly refreshFetch?: typeof fetch;
30
28
  /** Where `publish` reads BLOG_REFRESH_SECRET. Default: `process.env`. */
31
29
  readonly env?: Readonly<Record<string, string | undefined>>;
30
+ /** What `publish --stdin` reads. Default: the whole of `process.stdin`, as UTF-8. */
31
+ readonly readStdin?: () => Promise<string>;
32
32
  }
33
33
  export declare const EXIT_OK = 0;
34
34
  export declare const EXIT_FAILED = 1;
35
35
  export declare const EXIT_USAGE = 2;
36
- export declare const BLOG_USAGE = "Usage:\n softure-blog publish [<path>...] [--commit] [--withdraw] [--no-indexnow] [--app-url <origin>]\n softure-blog check [<path>...] [--external] [--today <YYYY-MM-DD>]\n softure-blog skill install [--dir <path>] [--command <cmd>] [--check]\n\npublish Brings the blog's tables to the state of the article files. A <path> is a file or a\n folder (every *.md in it except README.md); without one, blog({ contentDir }).\n Every file is checked before the first write, and one problem writes nothing.\n --commit write the changes; without it, a dry run that shows them and writes nothing\n --withdraw publish the one given file as withdrawn, whatever its status\n --no-indexnow do not submit the changed addresses to IndexNow\n --app-url the running app's origin for the cache refresh; default: appOrigin\n Files going public pass the quality gate first; an error writes nothing.\n With BLOG_REFRESH_SECRET set, a commit that changed a text asks the running\n app (refreshBlogCache) to refresh its blog cache, before the IndexNow submit.\n With seo({ indexNow }) enabled, a commit submits the addresses whose answer\n changed (the text, its old slug, its listing) to IndexNow; a dry run prints them.\n\ncheck Runs the quality gate of blog({ quality }) over the files, without a database, and\n prints every finding as file:line: severity [rule] message. Exits 1 on any error.\n --external also request every external link (2xx after redirects)\n --today the date to check freshness against; default: today in the app's time zone\n\nskill install\n Writes the article writing skill, filled from blog({ quality, skill }),\n into .claude/skills/blog-write. Overwrites only a skill it generated before.\n --dir the skill folder; default .claude/skills/blog-write\n --command how the skill runs this command; default \"npx softure-blog\"\n --check write nothing; exit 1 when the folder differs from what install would write\n\nOptions:\n --config <file> the app's softure.config file (bin only)\n --help show this help";
36
+ export declare const BLOG_USAGE = "Usage:\n softure-blog publish [<path>...] [--commit] [--withdraw] [--no-indexnow] [--app-url <origin>]\n [--stdin [--name <slug>.md]] [--history <file.json>] [--format text|lines]\n softure-blog check [<path>...] [--external] [--today <YYYY-MM-DD>]\n softure-blog skill install [--dir <path>] [--command <cmd>] [--check]\n\npublish Brings the blog's tables to the state of the article files. A <path> is a file or a\n folder (every *.md in it except README.md); without one, blog({ contentDir }).\n Every file is checked before the first write, and one problem writes nothing.\n --commit write the changes; without it, a dry run that shows them and writes nothing\n --withdraw publish the one given file as withdrawn, whatever its status\n --no-indexnow do not submit the changed addresses to IndexNow\n --app-url the running app's origin for the cache refresh; default: appOrigin\n --stdin read the files from standard input instead of paths: one file named by\n --name, or without --name a JSON bundle {\"files\":[{\"name\",\"text\"}]}\n (optionally with \"history\", the content of a --history file)\n --history a JSON file of the articles' earlier dates and old slugs, applied on the\n first publish of each article (moving an existing blog in)\n --format text (default) or lines: a stable blog|<key>|... contract for scripts\n Files going public pass the quality gate first; an error writes nothing.\n With BLOG_REFRESH_SECRET set, a commit that changed a text asks the running\n app (refreshBlogCache) to refresh its blog cache, before the IndexNow submit.\n With seo({ indexNow }) enabled, a commit submits the addresses whose answer\n changed (the text, its old slug, its listing) to IndexNow; a dry run prints them.\n\ncheck Runs the quality gate of blog({ quality }) over the files, without a database, and\n prints every finding as file:line: severity [rule] message. Exits 1 on any error.\n --external also request every external link (2xx after redirects)\n --today the date to check freshness against; default: today in the app's time zone\n\nskill install\n Writes the article writing skill, filled from blog({ quality, skill }),\n into .claude/skills/blog-write. Overwrites only a skill it generated before.\n --dir the skill folder; default .claude/skills/blog-write\n --command how the skill runs this command; default \"npx softure-blog\"\n --check write nothing; exit 1 when the folder differs from what install would write\n\nOptions:\n --config <file> the app's softure.config file (bin only)\n --help show this help";
37
37
  export type BlogCommand = {
38
38
  readonly kind: "help";
39
39
  } | {
@@ -44,6 +44,13 @@ export type BlogCommand = {
44
44
  readonly indexNow: boolean;
45
45
  /** The origin `--app-url` gives; `null`: `appOrigin`. */
46
46
  readonly appUrl: string | null;
47
+ /** Read the files from standard input; `name`: one file, `null`: a JSON bundle. */
48
+ readonly stdin: {
49
+ readonly name: string | null;
50
+ } | null;
51
+ /** The `--history` file, as given. */
52
+ readonly history: string | null;
53
+ readonly format: PublishFormat;
47
54
  } | {
48
55
  readonly kind: "check";
49
56
  readonly paths: readonly string[];
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"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"}