@softure-ai/blog 0.1.6 → 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 (86) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +135 -10
  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 +12 -5
  8. package/dist/cli/run.d.ts.map +1 -1
  9. package/dist/cli/run.js +121 -87
  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/options.d.ts.map +1 -1
  27. package/dist/options.js +3 -1
  28. package/dist/options.js.map +1 -1
  29. package/dist/pages/accept.d.ts +7 -0
  30. package/dist/pages/accept.d.ts.map +1 -0
  31. package/dist/pages/accept.js +34 -0
  32. package/dist/pages/accept.js.map +1 -0
  33. package/dist/proxy/index.d.ts +15 -0
  34. package/dist/proxy/index.d.ts.map +1 -1
  35. package/dist/proxy/index.js +52 -2
  36. package/dist/proxy/index.js.map +1 -1
  37. package/dist/quality/catalog.d.ts.map +1 -1
  38. package/dist/quality/catalog.js +4 -1
  39. package/dist/quality/catalog.js.map +1 -1
  40. package/dist/quality/check-article.d.ts.map +1 -1
  41. package/dist/quality/check-article.js +2 -1
  42. package/dist/quality/check-article.js.map +1 -1
  43. package/dist/quality/options.d.ts.map +1 -1
  44. package/dist/quality/options.js +3 -1
  45. package/dist/quality/options.js.map +1 -1
  46. package/dist/quality/rules/blocks.d.ts +8 -1
  47. package/dist/quality/rules/blocks.d.ts.map +1 -1
  48. package/dist/quality/rules/blocks.js +26 -0
  49. package/dist/quality/rules/blocks.js.map +1 -1
  50. package/dist/render/article-markdown.d.ts +12 -0
  51. package/dist/render/article-markdown.d.ts.map +1 -0
  52. package/dist/render/article-markdown.js +21 -0
  53. package/dist/render/article-markdown.js.map +1 -0
  54. package/dist/render/index.d.ts +2 -1
  55. package/dist/render/index.d.ts.map +1 -1
  56. package/dist/render/index.js +2 -1
  57. package/dist/render/index.js.map +1 -1
  58. package/dist/render/render-article.d.ts +40 -4
  59. package/dist/render/render-article.d.ts.map +1 -1
  60. package/dist/render/render-article.js +132 -10
  61. package/dist/render/render-article.js.map +1 -1
  62. package/dist/server/index.d.ts +2 -1
  63. package/dist/server/index.d.ts.map +1 -1
  64. package/dist/server/index.js +1 -0
  65. package/dist/server/index.js.map +1 -1
  66. package/module.json +1 -1
  67. package/package.json +1 -1
  68. package/skill/references/rules.md +2 -1
  69. package/src/cli/report.ts +182 -0
  70. package/src/cli/run.ts +121 -91
  71. package/src/contract.ts +9 -1
  72. package/src/db/articles.ts +39 -4
  73. package/src/db/history.ts +83 -0
  74. package/src/db/publish-run.ts +24 -4
  75. package/src/index.ts +1 -1
  76. package/src/options.ts +6 -2
  77. package/src/pages/accept.ts +39 -0
  78. package/src/proxy/index.ts +60 -2
  79. package/src/quality/catalog.ts +4 -1
  80. package/src/quality/check-article.ts +2 -1
  81. package/src/quality/options.ts +6 -2
  82. package/src/quality/rules/blocks.ts +26 -1
  83. package/src/render/article-markdown.ts +34 -0
  84. package/src/render/index.ts +6 -0
  85. package/src/render/render-article.ts +170 -16
  86. package/src/server/index.ts +2 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ Newest first. Each version lists what changed for an app that uses `@softure-ai/
4
4
  production, the version gets a line `verified in: <app>@<commit>` ([docs/05](../../docs/05-adoption-playbook.md),
5
5
  "Definition of done"). Versions before the first one below are described in their GitHub Releases (`blog@x.y.z`).
6
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
+
7
19
  ## 0.1.6
8
20
 
9
21
  - Adapters and commands use the configured database handle.
package/README.md CHANGED
@@ -11,12 +11,15 @@ editor and no CMS, a text changes only through a commit and `softure-blog publis
11
11
  - `blog.articles` and `blog.slug_history` with database constraints for every invariant that fits one.
12
12
  - `softure-blog publish`: a dry run by default; with `--commit`, all files or none; unchanged files are
13
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`).
14
17
  - Read functions for the pages: `getPublishedArticle`, `findArticleBySlug`, `findSlugRedirect`,
15
18
  `listArticles`.
16
19
  - `renderArticle(markdown, options)`: the body as safe HTML on the server (no raw HTML, safe link
17
20
  schemes only, marked external links, images under the app's image policy), heading ids and an
18
21
  optional table of contents, glossary links on the first mention of a term, block plugins for the
19
- app's own fenced blocks, reading time.
22
+ app's own fenced blocks and `::directive{…}` lines, reading time.
20
23
  - Pages, each mounted with one re-export line (`@softure-ai/blog/next`): the listing grouped by cluster
21
24
  with the pillar first, an article (dates, summary, contents, FAQ, sources, signature, disclaimer,
22
25
  `BlogPosting`/`BreadcrumbList`/`FAQPage` JSON-LD), the glossary index and a term page (`DefinedTerm`,
@@ -24,7 +27,8 @@ editor and no CMS, a text changes only through a commit and `softure-blog publis
24
27
  Their canonical, Open Graph, JSON-LD and feed URLs follow `@softure-ai/seo`'s origin, host and
25
28
  trailing-slash rule when the app lists `seo()` (core's `getSiteUrls`), and `appOrigin` otherwise.
26
29
  - `createBlogRedirects` (`@softure-ai/blog/proxy`): 301 from an old slug, 410 for a withdrawn text,
27
- 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.
28
32
  - `@softure-ai/blog/styles.css`: the pages and the rendered body on the `--sft-*` tokens.
29
33
  - Discovery: an RSS 2.0 feed (`serveBlogRss`), "read next" under every article (its cluster first, the
30
34
  pillar on top), and, with `@softure-ai/seo` (optional): sitemap entries with each text's real
@@ -280,7 +284,8 @@ The quality gate resolves internal links through `quality.paths` (default `/blog
280
284
  The commands:
281
285
 
282
286
  ```bash
283
- 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>]
284
289
  softure-blog check [<path>...] [--external] [--today <YYYY-MM-DD>] [--config <file>]
285
290
  softure-blog skill install [--dir <path>] [--command <cmd>] [--check] [--config <file>]
286
291
  ```
@@ -300,6 +305,13 @@ softure-blog skill install [--dir <path>] [--command <cmd>] [--check] [--config
300
305
  listing or the glossary of its kind) as canonical URLs on seo's origin; a dry run prints them;
301
306
  `--no-indexnow` skips the submit (e.g. a local or CI database). A failed submit is a warning: the
302
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.
303
315
  - Exit codes: 0 done, 1 refused or failed (nothing written), 2 usage error.
304
316
 
305
317
  Output, one line per text, then a summary:
@@ -312,6 +324,71 @@ summary: added 1, changed 1, unchanged 12
312
324
  dry run: nothing written; pass --commit to write
313
325
  ```
314
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
+
315
392
  Like `softure migrate`, the bin loads `softure.config.(ts|mts|js|mjs)` with Node and opens
316
393
  `database.handle` when the config sets one, otherwise `database.url`. When Node cannot load the config (path aliases, a bundled container), call
317
394
  `runBlogCli` from an app script:
@@ -459,9 +536,56 @@ const chartBlock: BlockPlugin = {
459
536
 
460
537
  Plugin output is the app's own code and is trusted as is: escape what goes into its HTML. A plugin
461
538
  that throws fails the render (a bug, not content). Without the plugin the same fence renders as a code
462
- block. `findArticleBlocks(markdown, plugins)` lists the blocks a text uses with their line and
463
- `requires`, without rendering. When any block returns a node, `html` is `null`: render `segments` in
464
- 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
+ ```
465
589
 
466
590
  ## 5. Migrations and tables
467
591
 
@@ -567,8 +691,8 @@ export const tickerPlugin: QualityPlugin = {
567
691
  plugins in `quality.blocks` (type, info, fence line, `requires`), `today` and the language `ruleset` (for
568
692
  its number notation); the text helpers (`toProse`, `splitSentences`, `findSignificantNumbers`, …) are
569
693
  exported from `@softure-ai/blog/server`.
570
- - `renderArticle({ blocks })` and `blog({ blocks })`: block plugins for the app's fenced blocks
571
- (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.
572
696
  - The pages' `cta` and `afterArticle` slots: the app's call to action and blocks (a waitlist form).
573
697
 
574
698
  ## 11. GDPR
@@ -580,13 +704,14 @@ Articles hold editorial content, no personal data: nothing to export or delete.
580
704
  - Two runs that rename one article away from a slug and give it to another at the same moment can leave the
581
705
  slug both current and in the slug history (BF-12).
582
706
  - The content hash is part of the contract: a field added later enters it only when present.
583
- - No `--stdin` (a deploy transport).
584
707
  - The refresh route expires the cache of the instance that answers it. With several instances and Next's
585
708
  default (in-memory) cache handler, the others show a publish after `revalidateSeconds`; a shared cache
586
709
  handler covers them.
587
710
  - The renderer has no raw HTML and no figures: an image has no caption, and the app hosts and sizes its
588
711
  images itself (no `next/image`). A plugin fence inside a list or a quote stays a code
589
- 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.
590
715
  - The gate reads Markdown line by line (blocks, not a syntax tree): enough for the rules, not a
591
716
  renderer. Fenced code and HTML comments are skipped.
592
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":"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"}
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;
@@ -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":"AAWA,OAAO,EAAe,KAAK,KAAK,EAAE,KAAK,aAAa,EAA8B,MAAM,kBAAkB,CAAC;AAC3G,OAAO,EAA6C,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGhG,OAAO,EAA+E,KAAK,WAAW,EAAuB,MAAM,sBAAsB,CAAC;AAE1J,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAS9D,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,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,0FAA0F;IAC1F,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,mEAAmE;IACnE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;IAC5B,sHAAsH;IACtH,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;IACjE,qHAAqH;IACrH,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC;IAC3B,2FAA2F;IAC3F,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAClC,4EAA4E;IAC5E,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,KAAK,CAAC;IACtC,kFAAkF;IAClF,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,KAAK,CAAC;IACrC,yEAAyE;IACzE,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;CAC7D;AAED,eAAO,MAAM,OAAO,IAAI,CAAC;AACzB,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,UAAU,IAAI,CAAC;AAE5B,eAAO,MAAM,UAAU,qoEAgCa,CAAC;AAOrC,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,yDAAyD;IACzD,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GACxH;IAAE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAEhH,sGAAsG;AACtG,wBAAsB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAc5E;AAED,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,WAAW,GAAG,MAAM,CAqB9E"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/cli/run.ts"],"names":[],"mappings":"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"}