@softure-ai/blog 0.1.6 → 0.1.8
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/CHANGELOG.md +30 -0
- package/README.md +211 -26
- package/dist/cli/report.d.ts +17 -0
- package/dist/cli/report.d.ts.map +1 -0
- package/dist/cli/report.js +149 -0
- package/dist/cli/report.js.map +1 -0
- package/dist/cli/run.d.ts +12 -5
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +122 -88
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/skill.d.ts.map +1 -1
- package/dist/cli/skill.js +2 -1
- package/dist/cli/skill.js.map +1 -1
- package/dist/contract.d.ts +4 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/db/articles.d.ts +10 -1
- package/dist/db/articles.d.ts.map +1 -1
- package/dist/db/articles.js +41 -5
- package/dist/db/articles.js.map +1 -1
- package/dist/db/history.d.ts +36 -0
- package/dist/db/history.d.ts.map +1 -0
- package/dist/db/history.js +70 -0
- package/dist/db/history.js.map +1 -0
- package/dist/db/publish-run.d.ts +11 -0
- package/dist/db/publish-run.d.ts.map +1 -1
- package/dist/db/publish-run.js +14 -4
- package/dist/db/publish-run.js.map +1 -1
- package/dist/index.d.ts +20 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/next/index.d.ts +2 -0
- package/dist/next/index.d.ts.map +1 -1
- package/dist/next/index.js +2 -0
- package/dist/next/index.js.map +1 -1
- package/dist/next/json-ld.d.ts +13 -0
- package/dist/next/json-ld.d.ts.map +1 -0
- package/dist/next/json-ld.js +42 -0
- package/dist/next/json-ld.js.map +1 -0
- package/dist/next/metadata.d.ts +18 -0
- package/dist/next/metadata.d.ts.map +1 -0
- package/dist/next/metadata.js +84 -0
- package/dist/next/metadata.js.map +1 -0
- package/dist/next/pages.d.ts.map +1 -1
- package/dist/next/pages.js +16 -77
- package/dist/next/pages.js.map +1 -1
- package/dist/options.d.ts +23 -7
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +26 -1
- package/dist/options.js.map +1 -1
- package/dist/pages/accept.d.ts +7 -0
- package/dist/pages/accept.d.ts.map +1 -0
- package/dist/pages/accept.js +34 -0
- package/dist/pages/accept.js.map +1 -0
- package/dist/pages/body.d.ts +1 -1
- package/dist/pages/body.d.ts.map +1 -1
- package/dist/pages/body.js +1 -0
- package/dist/pages/body.js.map +1 -1
- package/dist/pages/index.d.ts +1 -1
- package/dist/pages/index.d.ts.map +1 -1
- package/dist/pages/index.js.map +1 -1
- package/dist/pages/redirects.d.ts +22 -6
- package/dist/pages/redirects.d.ts.map +1 -1
- package/dist/pages/redirects.js +5 -2
- package/dist/pages/redirects.js.map +1 -1
- package/dist/proxy/index.d.ts +15 -0
- package/dist/proxy/index.d.ts.map +1 -1
- package/dist/proxy/index.js +60 -3
- package/dist/proxy/index.js.map +1 -1
- package/dist/quality/catalog.d.ts.map +1 -1
- package/dist/quality/catalog.js +4 -1
- package/dist/quality/catalog.js.map +1 -1
- package/dist/quality/check-article.d.ts.map +1 -1
- package/dist/quality/check-article.js +2 -1
- package/dist/quality/check-article.js.map +1 -1
- package/dist/quality/index.d.ts +1 -1
- package/dist/quality/index.d.ts.map +1 -1
- package/dist/quality/index.js.map +1 -1
- package/dist/quality/link-targets.d.ts.map +1 -1
- package/dist/quality/link-targets.js +6 -5
- package/dist/quality/link-targets.js.map +1 -1
- package/dist/quality/options.d.ts +3 -3
- package/dist/quality/options.d.ts.map +1 -1
- package/dist/quality/options.js +9 -4
- package/dist/quality/options.js.map +1 -1
- package/dist/quality/rules/blocks.d.ts +8 -1
- package/dist/quality/rules/blocks.d.ts.map +1 -1
- package/dist/quality/rules/blocks.js +26 -0
- package/dist/quality/rules/blocks.js.map +1 -1
- package/dist/quality/settings.d.ts +11 -0
- package/dist/quality/settings.d.ts.map +1 -1
- package/dist/quality/settings.js +7 -1
- package/dist/quality/settings.js.map +1 -1
- package/dist/render/article-markdown.d.ts +12 -0
- package/dist/render/article-markdown.d.ts.map +1 -0
- package/dist/render/article-markdown.js +21 -0
- package/dist/render/article-markdown.js.map +1 -0
- package/dist/render/index.d.ts +2 -1
- package/dist/render/index.d.ts.map +1 -1
- package/dist/render/index.js +2 -1
- package/dist/render/index.js.map +1 -1
- package/dist/render/render-article.d.ts +48 -4
- package/dist/render/render-article.d.ts.map +1 -1
- package/dist/render/render-article.js +146 -17
- package/dist/render/render-article.js.map +1 -1
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/options.js +1 -1
- package/dist/server/options.js.map +1 -1
- package/module.json +1 -1
- package/package.json +1 -1
- package/skill/references/rules.md +2 -1
- package/src/cli/report.ts +182 -0
- package/src/cli/run.ts +122 -92
- package/src/cli/skill.ts +2 -1
- package/src/contract.ts +9 -1
- package/src/db/articles.ts +48 -8
- package/src/db/history.ts +90 -0
- package/src/db/publish-run.ts +24 -4
- package/src/index.ts +1 -1
- package/src/next/index.ts +9 -0
- package/src/next/json-ld.ts +47 -0
- package/src/next/metadata.ts +103 -0
- package/src/next/pages.tsx +16 -82
- package/src/options.ts +32 -3
- package/src/pages/accept.ts +39 -0
- package/src/pages/body.ts +2 -1
- package/src/pages/index.ts +3 -0
- package/src/pages/redirects.ts +27 -3
- package/src/proxy/index.ts +69 -3
- package/src/quality/catalog.ts +4 -1
- package/src/quality/check-article.ts +2 -1
- package/src/quality/index.ts +1 -1
- package/src/quality/link-targets.ts +6 -5
- package/src/quality/options.ts +12 -5
- package/src/quality/rules/blocks.ts +26 -1
- package/src/quality/settings.ts +17 -1
- package/src/render/article-markdown.ts +34 -0
- package/src/render/index.ts +8 -0
- package/src/render/render-article.ts +192 -23
- package/src/server/index.ts +2 -0
- package/src/server/options.ts +1 -1
package/dist/server/options.js
CHANGED
|
@@ -50,7 +50,7 @@ export function getQualitySettings(config) {
|
|
|
50
50
|
const { quality, images } = getBlogOptions(config);
|
|
51
51
|
if (quality === false)
|
|
52
52
|
return null;
|
|
53
|
-
const site = { appOrigin: config.appOrigin, siteOrigin: getSiteUrls(config).origin, timezone: config.timezone };
|
|
53
|
+
const site = { appOrigin: config.appOrigin, siteOrigin: getSiteUrls(config).origin, timezone: config.timezone, routes: getBlogRoutes(config) };
|
|
54
54
|
return resolveQualitySettings(quality, site, images ?? null);
|
|
55
55
|
}
|
|
56
56
|
//# sourceMappingURL=options.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"options.js","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,OAAO,EAAE,SAAS,EAAE,WAAW,EAA6C,MAAM,kBAAkB,CAAC;AAGrG,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAmB,MAAM,mBAAmB,CAAC;AACtF,OAAO,EAAE,sBAAsB,EAAwB,MAAM,wBAAwB,CAAC;AAEtF,MAAM,SAAS,GAAG,MAAM,CAAC;AAEzB,sGAAsG;AACtG,SAAS,aAAa,CAAC,MAAqB;IAC1C,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAC5C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,yFAAyF,CAAC,CAAC;IAC7G,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAqB;IAClD,kEAAkE;IAClE,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC,OAAsB,CAAC;AACtD,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,eAAe,CAAC,MAAqB;IACnD,+EAA+E;IAC/E,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAiB,CAAC;AACvE,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,aAAa,CAAC,MAAqB;IACjD,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC;IAC5C,MAAM,IAAI,GAAG,CAAC,IAAsB,EAAU,EAAE;QAC9C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1B,wFAAwF;QACxF,IAAI,IAAI,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,IAAI,uCAAuC,CAAC,CAAC;QACjH,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC,CAAC;IACF,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;AACxG,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,kBAAkB,CAAC,MAAqB;IACtD,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;IAClD,sFAAsF;IACtF,IAAI,IAAI,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,uEAAuE,CAAC,CAAC;IACjH,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,oBAAoB,CAAC,MAAqB;IACxD,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IACvC,OAAO,gBAAgB,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC;AAC1D,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,kBAAkB,CAAC,MAAqB;IACtD,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IACnD,IAAI,OAAO,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,IAAI,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"options.js","sourceRoot":"","sources":["../../src/server/options.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,OAAO,EAAE,SAAS,EAAE,WAAW,EAA6C,MAAM,kBAAkB,CAAC;AAGrG,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAmB,MAAM,mBAAmB,CAAC;AACtF,OAAO,EAAE,sBAAsB,EAAwB,MAAM,wBAAwB,CAAC;AAEtF,MAAM,SAAS,GAAG,MAAM,CAAC;AAEzB,sGAAsG;AACtG,SAAS,aAAa,CAAC,MAAqB;IAC1C,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAC5C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,yFAAyF,CAAC,CAAC;IAC7G,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAqB;IAClD,kEAAkE;IAClE,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC,OAAsB,CAAC;AACtD,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,eAAe,CAAC,MAAqB;IACnD,+EAA+E;IAC/E,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAiB,CAAC;AACvE,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,aAAa,CAAC,MAAqB;IACjD,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC;IAC5C,MAAM,IAAI,GAAG,CAAC,IAAsB,EAAU,EAAE;QAC9C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1B,wFAAwF;QACxF,IAAI,IAAI,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,IAAI,uCAAuC,CAAC,CAAC;QACjH,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC,CAAC;IACF,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;AACxG,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,kBAAkB,CAAC,MAAqB;IACtD,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;IAClD,sFAAsF;IACtF,IAAI,IAAI,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,uEAAuE,CAAC,CAAC;IACjH,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED,uGAAuG;AACvG,MAAM,UAAU,oBAAoB,CAAC,MAAqB;IACxD,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IACvC,OAAO,gBAAgB,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC;AAC1D,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,kBAAkB,CAAC,MAAqB;IACtD,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC;IACnD,IAAI,OAAO,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,IAAI,GAAG,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;IAC/I,OAAO,sBAAsB,CAAC,OAAO,EAAE,IAAI,EAAE,MAAM,IAAI,IAAI,CAAC,CAAC;AAC/D,CAAC"}
|
package/module.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softure-ai/blog",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "SOFTURE AI blog module: articles and glossary terms kept as Markdown files with a strict YAML frontmatter (extendable by the app's own fields), stored in the module's Postgres schema by the softure-blog publish command (dry run by default, all or nothing, content hash, slug history, one pillar per cluster), a server-side Markdown renderer (no raw HTML, safe links, glossary links, block plugins), a text quality gate (softure-blog check, run before every publish) with language rulesets and rule plugins, the blog's pages ready to mount in Next.js (listing, article, glossary, method page, OG image, JSON-LD) with a proxy piece for 301 and 410, an article writing skill (softure-blog skill install) generated from the gate's rules, and discovery: an RSS feed, read next, sitemap entries and an IndexNow submit on publish through the optional @softure-ai/seo, with a rate-limited cache refresh route the publish calls first (through the optional @softure-ai/security).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,8 @@ rewrite the paragraph around its one thought; do not swap a word for a synonym.
|
|
|
31
31
|
| `length` | | | An article of {{articleWordsMin}} to {{articleWordsMax}} words, a term of {{termWordsMin}} to {{termWordsMax}}. |
|
|
32
32
|
| `footnote-undefined` | | | Define every `[^id]` at the end of the file. |
|
|
33
33
|
| `footnote-unused` | | | Remove a footnote definition nothing refers to, or refer to it. |
|
|
34
|
-
| `block-requires` | | | A
|
|
34
|
+
| `block-requires` | | | A block of the app's block plugins (a fence or a `::directive`) needs its frontmatter keys; add them. |
|
|
35
|
+
| `block-directive` | | | A `::directive` line names a directive the blog renders and writes its attributes as `key="value"` pairs, each key once. |
|
|
35
36
|
|
|
36
37
|
## Links
|
|
37
38
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// How `softure-blog publish` prints its result: for a person (the default) or as a line contract a
|
|
2
|
+
// release script parses (`--format lines`). Both get the same events; only the wording differs.
|
|
3
|
+
//
|
|
4
|
+
// The line contract: every line starts `blog|`, fields are separated by `|`, and a `|` or a line break
|
|
5
|
+
// inside a value becomes a space. Keys are stable English words; a new key may be added, an existing
|
|
6
|
+
// one never changes meaning. A run prints exactly one outcome line: `blog|written`, `blog|dry-run`,
|
|
7
|
+
// `blog|refused` or `blog|failed|<message>`.
|
|
8
|
+
//
|
|
9
|
+
// blog|warning|<subject>|<message>
|
|
10
|
+
// blog|error|<subject>|<message> (before blog|refused)
|
|
11
|
+
// blog|change|<added|changed|unchanged>|<id>|<status/slug before or none>|<status/slug after>
|
|
12
|
+
// blog|moved|<id>|<old slug>|<new slug>
|
|
13
|
+
// blog|imported|<id>|<published_at ISO or none>|<old slugs>
|
|
14
|
+
// blog|summary|<added>|<changed>|<unchanged>
|
|
15
|
+
// blog|cache|<off|skipped|dry-run|refreshed|failed>|…
|
|
16
|
+
// blog|indexnow|<off|skipped|dry-run|submitted|failed>|…
|
|
17
|
+
import type { BlogRefreshOutcome } from "../discovery/refresh.js";
|
|
18
|
+
import { BLOG_REFRESH_SECRET_ENV } from "../discovery/refresh.js";
|
|
19
|
+
import type { BlogIndexNowSubmit } from "../discovery/submit.js";
|
|
20
|
+
import type { BlogPublishRun, PublishedChange, PublishProblem } from "../db/publish-run.js";
|
|
21
|
+
|
|
22
|
+
export interface CliOutput {
|
|
23
|
+
readonly log: (line: string) => void;
|
|
24
|
+
readonly error: (line: string) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type PublishFormat = "text" | "lines";
|
|
28
|
+
|
|
29
|
+
export interface PublishReporter {
|
|
30
|
+
/** A problem before or outside the run (a file it cannot read, the database); the run's outcome. */
|
|
31
|
+
readonly failed: (message: string) => void;
|
|
32
|
+
readonly run: (run: BlogPublishRun) => void;
|
|
33
|
+
readonly refresh: (outcome: BlogRefreshOutcome) => void;
|
|
34
|
+
readonly indexNow: (submit: BlogIndexNowSubmit) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createPublishReporter(format: PublishFormat, output: CliOutput): PublishReporter {
|
|
38
|
+
return format === "lines" ? createLineReporter(output) : createTextReporter(output);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function countActions(run: Extract<BlogPublishRun, { status: "done" }>): Record<PublishedChange["action"], number> {
|
|
42
|
+
const count = (action: PublishedChange["action"]) => run.changes.filter((change) => change.action === action).length;
|
|
43
|
+
return { added: count("added"), changed: count("changed"), unchanged: count("unchanged") };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatBefore(change: PublishedChange): string {
|
|
47
|
+
return change.statusBefore === null || change.slugBefore === null ? "none" : `${change.statusBefore}/${change.slugBefore}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatDate(date: Date | null): string {
|
|
51
|
+
return date === null ? "none" : date.toISOString();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createTextReporter(output: CliOutput): PublishReporter {
|
|
55
|
+
const formatProblem = (problem: PublishProblem) => `${problem.subject}: ${problem.message}`;
|
|
56
|
+
return {
|
|
57
|
+
failed: (message) => output.error(`softure-blog publish: ${message}`),
|
|
58
|
+
run: (run) => {
|
|
59
|
+
for (const warning of run.warnings) output.error(`warning ${formatProblem(warning)}`);
|
|
60
|
+
if (run.status === "refused") {
|
|
61
|
+
for (const problem of run.problems) output.error(`error ${formatProblem(problem)}`);
|
|
62
|
+
output.error("refused: nothing written; fix the problems above");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const change of run.changes) {
|
|
66
|
+
output.log(`${change.action} ${change.id} ${formatBefore(change)} -> ${change.statusAfter}/${change.slug}`);
|
|
67
|
+
if (change.previousSlug !== null) output.log(`moved ${change.id} ${change.previousSlug} -> ${change.slug}`);
|
|
68
|
+
if (change.imported !== undefined) {
|
|
69
|
+
output.log(`imported ${change.id} published_at=${formatDate(change.imported.publishedAt)} old_slugs=${String(change.imported.oldSlugs)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const counts = countActions(run);
|
|
73
|
+
output.log(`summary: added ${String(counts.added)}, changed ${String(counts.changed)}, unchanged ${String(counts.unchanged)}`);
|
|
74
|
+
output.log(run.committed ? "written" : "dry run: nothing written; pass --commit to write");
|
|
75
|
+
},
|
|
76
|
+
refresh: (outcome) => {
|
|
77
|
+
switch (outcome.kind) {
|
|
78
|
+
case "not_configured":
|
|
79
|
+
output.log(`cache: the running app shows the change within revalidateSeconds (${String(outcome.revalidateSeconds)} s); set ${BLOG_REFRESH_SECRET_ENV} to refresh it now`);
|
|
80
|
+
return;
|
|
81
|
+
case "skipped":
|
|
82
|
+
output.log("cache: no text changed, nothing to refresh");
|
|
83
|
+
return;
|
|
84
|
+
case "dry_run":
|
|
85
|
+
output.log(`cache: dry run, a commit would refresh ${outcome.url}`);
|
|
86
|
+
return;
|
|
87
|
+
case "refreshed":
|
|
88
|
+
output.log(`cache: refreshed ${outcome.url}`);
|
|
89
|
+
return;
|
|
90
|
+
case "failed":
|
|
91
|
+
output.error(`warning cache: ${outcome.reason} (${outcome.code}); the publish is written, the app shows it within ${String(outcome.revalidateSeconds)} s`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
indexNow: ({ paths, outcome }) => {
|
|
96
|
+
switch (outcome.kind) {
|
|
97
|
+
case "not_configured":
|
|
98
|
+
output.log(`indexnow: off, ${outcome.reason}`);
|
|
99
|
+
return;
|
|
100
|
+
case "skipped":
|
|
101
|
+
output.log("indexnow: no public address changed, nothing to submit");
|
|
102
|
+
return;
|
|
103
|
+
case "dry_run":
|
|
104
|
+
output.log(`indexnow: dry run, a commit would submit ${String(outcome.urls.length)} URL(s): ${outcome.urls.join(" ")}`);
|
|
105
|
+
return;
|
|
106
|
+
case "submitted":
|
|
107
|
+
output.log(`indexnow: submitted ${String(outcome.count)} URL(s) (${String(outcome.status)}): ${paths.join(" ")}`);
|
|
108
|
+
return;
|
|
109
|
+
case "failed":
|
|
110
|
+
output.error(`warning indexnow: ${outcome.reason} (${outcome.code}); the publish is written, submit the addresses later: ${paths.join(" ")}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A value inside a contract line: no field separator, no line break. */
|
|
118
|
+
function toField(value: string | number): string {
|
|
119
|
+
return String(value).replace(/[|\r\n]+/g, " ");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function createLineReporter(output: CliOutput): PublishReporter {
|
|
123
|
+
// Every contract line goes to standard output, so `2>&1` is not needed to read them.
|
|
124
|
+
const line = (...fields: readonly (string | number)[]) => output.log(["blog", ...fields.map(toField)].join("|"));
|
|
125
|
+
return {
|
|
126
|
+
failed: (message) => line("failed", message),
|
|
127
|
+
run: (run) => {
|
|
128
|
+
for (const warning of run.warnings) line("warning", warning.subject, warning.message);
|
|
129
|
+
if (run.status === "refused") {
|
|
130
|
+
for (const problem of run.problems) line("error", problem.subject, problem.message);
|
|
131
|
+
line("refused");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
for (const change of run.changes) {
|
|
135
|
+
line("change", change.action, change.id, formatBefore(change), `${change.statusAfter}/${change.slug}`);
|
|
136
|
+
if (change.previousSlug !== null) line("moved", change.id, change.previousSlug, change.slug);
|
|
137
|
+
if (change.imported !== undefined) line("imported", change.id, formatDate(change.imported.publishedAt), change.imported.oldSlugs);
|
|
138
|
+
}
|
|
139
|
+
const counts = countActions(run);
|
|
140
|
+
line("summary", counts.added, counts.changed, counts.unchanged);
|
|
141
|
+
line(run.committed ? "written" : "dry-run");
|
|
142
|
+
},
|
|
143
|
+
refresh: (outcome) => {
|
|
144
|
+
switch (outcome.kind) {
|
|
145
|
+
case "not_configured":
|
|
146
|
+
line("cache", "off", outcome.revalidateSeconds);
|
|
147
|
+
return;
|
|
148
|
+
case "skipped":
|
|
149
|
+
line("cache", "skipped");
|
|
150
|
+
return;
|
|
151
|
+
case "dry_run":
|
|
152
|
+
line("cache", "dry-run", outcome.url);
|
|
153
|
+
return;
|
|
154
|
+
case "refreshed":
|
|
155
|
+
line("cache", "refreshed", outcome.url);
|
|
156
|
+
return;
|
|
157
|
+
case "failed":
|
|
158
|
+
line("cache", "failed", outcome.code, outcome.reason);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
indexNow: ({ paths, outcome }) => {
|
|
163
|
+
switch (outcome.kind) {
|
|
164
|
+
case "not_configured":
|
|
165
|
+
line("indexnow", "off", outcome.reason);
|
|
166
|
+
return;
|
|
167
|
+
case "skipped":
|
|
168
|
+
line("indexnow", "skipped");
|
|
169
|
+
return;
|
|
170
|
+
case "dry_run":
|
|
171
|
+
line("indexnow", "dry-run", outcome.urls.length, outcome.urls.join(" "));
|
|
172
|
+
return;
|
|
173
|
+
case "submitted":
|
|
174
|
+
line("indexnow", "submitted", outcome.count, outcome.status, paths.join(" "));
|
|
175
|
+
return;
|
|
176
|
+
case "failed":
|
|
177
|
+
line("indexnow", "failed", outcome.code, outcome.reason, paths.join(" "));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
package/src/cli/run.ts
CHANGED
|
@@ -11,9 +11,11 @@ import { basename, dirname, join, relative, resolve } from "node:path";
|
|
|
11
11
|
import { parseArgs } from "node:util";
|
|
12
12
|
import { systemClock, type Clock, type SoftureConfig, type SoftureDatabaseConfig } from "@softure-ai/core";
|
|
13
13
|
import { openCommandDatabase, type CommandDatabase, type DatabaseHandle } from "@softure-ai/db";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { BLOG_REFRESH_SECRET_ENV, requestBlogRefresh } from "../discovery/refresh.js";
|
|
16
|
+
import { submitBlogChanges } from "../discovery/submit.js";
|
|
17
|
+
import { parseArticleHistory, type ArticleHistoryMap } from "../db/history.js";
|
|
18
|
+
import { runBlogPublish, type ArticleFile, type PublishGate } from "../db/publish-run.js";
|
|
17
19
|
import { checkArticleFiles, type FileCheckResult } from "../quality/check-files.js";
|
|
18
20
|
import type { FetchLike } from "../quality/external-links.js";
|
|
19
21
|
import { createQualityGate } from "../quality/gate.js";
|
|
@@ -22,12 +24,10 @@ import { getLocalDate } from "../quality/settings.js";
|
|
|
22
24
|
import type { OgFontSource } from "../options.js";
|
|
23
25
|
import { createOgFontLoader } from "../server/og-fonts.js";
|
|
24
26
|
import { getBlogOptions, getBlogReservedSlugs, getQualitySettings } from "../server/options.js";
|
|
27
|
+
import { createPublishReporter, type CliOutput, type PublishFormat } from "./report.js";
|
|
25
28
|
import { DEFAULT_SKILL_COMMAND, DEFAULT_SKILL_DIR, renderBlogSkill, SKILL_MARKER, type SkillFile } from "./skill.js";
|
|
26
29
|
|
|
27
|
-
export
|
|
28
|
-
readonly log: (line: string) => void;
|
|
29
|
-
readonly error: (line: string) => void;
|
|
30
|
-
}
|
|
30
|
+
export type { CliOutput };
|
|
31
31
|
|
|
32
32
|
export interface RunBlogCliOptions {
|
|
33
33
|
/** The app's config with `blog()` among its modules. */
|
|
@@ -52,6 +52,8 @@ export interface RunBlogCliOptions {
|
|
|
52
52
|
readonly refreshFetch?: typeof fetch;
|
|
53
53
|
/** Where `publish` reads BLOG_REFRESH_SECRET. Default: `process.env`. */
|
|
54
54
|
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
55
|
+
/** What `publish --stdin` reads. Default: the whole of `process.stdin`, as UTF-8. */
|
|
56
|
+
readonly readStdin?: () => Promise<string>;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
export const EXIT_OK = 0;
|
|
@@ -60,6 +62,7 @@ export const EXIT_USAGE = 2;
|
|
|
60
62
|
|
|
61
63
|
export const BLOG_USAGE = `Usage:
|
|
62
64
|
softure-blog publish [<path>...] [--commit] [--withdraw] [--no-indexnow] [--app-url <origin>]
|
|
65
|
+
[--stdin [--name <slug>.md]] [--history <file.json>] [--format text|lines]
|
|
63
66
|
softure-blog check [<path>...] [--external] [--today <YYYY-MM-DD>]
|
|
64
67
|
softure-blog skill install [--dir <path>] [--command <cmd>] [--check]
|
|
65
68
|
|
|
@@ -70,6 +73,12 @@ publish Brings the blog's tables to the state of the article files. A <path> i
|
|
|
70
73
|
--withdraw publish the one given file as withdrawn, whatever its status
|
|
71
74
|
--no-indexnow do not submit the changed addresses to IndexNow
|
|
72
75
|
--app-url the running app's origin for the cache refresh; default: appOrigin
|
|
76
|
+
--stdin read the files from standard input instead of paths: one file named by
|
|
77
|
+
--name, or without --name a JSON bundle {"files":[{"name","text"}]}
|
|
78
|
+
(optionally with "history", the content of a --history file)
|
|
79
|
+
--history a JSON file of the articles' earlier dates and old slugs, applied on the
|
|
80
|
+
first publish of each article (moving an existing blog in)
|
|
81
|
+
--format text (default) or lines: a stable blog|<key>|... contract for scripts
|
|
73
82
|
Files going public pass the quality gate first; an error writes nothing.
|
|
74
83
|
With ${BLOG_REFRESH_SECRET_ENV} set, a commit that changed a text asks the running
|
|
75
84
|
app (refreshBlogCache) to refresh its blog cache, before the IndexNow submit.
|
|
@@ -107,6 +116,11 @@ export type BlogCommand =
|
|
|
107
116
|
readonly indexNow: boolean;
|
|
108
117
|
/** The origin `--app-url` gives; `null`: `appOrigin`. */
|
|
109
118
|
readonly appUrl: string | null;
|
|
119
|
+
/** Read the files from standard input; `name`: one file, `null`: a JSON bundle. */
|
|
120
|
+
readonly stdin: { readonly name: string | null } | null;
|
|
121
|
+
/** The `--history` file, as given. */
|
|
122
|
+
readonly history: string | null;
|
|
123
|
+
readonly format: PublishFormat;
|
|
110
124
|
}
|
|
111
125
|
| { readonly kind: "check"; readonly paths: readonly string[]; readonly external: boolean; readonly today: string | null }
|
|
112
126
|
| { readonly kind: "skill-install"; readonly dir: string; readonly command: string; readonly check: boolean };
|
|
@@ -146,10 +160,28 @@ export function parseBlogCommand(argv: readonly string[]): BlogCommand | string
|
|
|
146
160
|
const { values, positionals } = parsed;
|
|
147
161
|
if (values.help === true) return { kind: "help" };
|
|
148
162
|
const withdraw = values.withdraw === true;
|
|
149
|
-
|
|
163
|
+
const isStdin = values.stdin === true;
|
|
164
|
+
const stdinName = values.name ?? null;
|
|
165
|
+
if (isStdin && positionals.length > 0) return "--stdin reads the files from standard input; give no paths with it";
|
|
166
|
+
if (!isStdin && stdinName !== null) return "--name names the file --stdin reads; use it with --stdin";
|
|
167
|
+
if (stdinName !== null && !/^[^/\\]+\.md$/.test(stdinName)) return `--name needs a file name <slug>.md, not "${stdinName}"`;
|
|
168
|
+
if (withdraw && !(isStdin ? stdinName !== null : positionals.length === 1)) return "--withdraw takes exactly one article file";
|
|
150
169
|
const appUrl = values["app-url"] === undefined ? null : parseOrigin(values["app-url"]);
|
|
151
170
|
if (appUrl === undefined) return `--app-url needs an http or https origin, e.g. http://web:3000, not "${values["app-url"] ?? ""}"`;
|
|
152
|
-
|
|
171
|
+
const format = values.format ?? "text";
|
|
172
|
+
if (format !== "text" && format !== "lines") return `--format is text or lines, not "${format}"`;
|
|
173
|
+
if (values.history !== undefined && values.history.trim() === "") return "--history needs a JSON file";
|
|
174
|
+
return {
|
|
175
|
+
kind: "publish",
|
|
176
|
+
paths: positionals,
|
|
177
|
+
commit: values.commit === true,
|
|
178
|
+
withdraw,
|
|
179
|
+
indexNow: values["no-indexnow"] !== true,
|
|
180
|
+
appUrl,
|
|
181
|
+
stdin: isStdin ? { name: stdinName } : null,
|
|
182
|
+
history: values.history ?? null,
|
|
183
|
+
format,
|
|
184
|
+
};
|
|
153
185
|
}
|
|
154
186
|
|
|
155
187
|
/** The origin of an http(s) URL, or `undefined` for anything else. */
|
|
@@ -232,6 +264,10 @@ function parsePublishArgs(args: readonly string[]) {
|
|
|
232
264
|
withdraw: { type: "boolean" },
|
|
233
265
|
"no-indexnow": { type: "boolean" },
|
|
234
266
|
"app-url": { type: "string" },
|
|
267
|
+
stdin: { type: "boolean" },
|
|
268
|
+
name: { type: "string" },
|
|
269
|
+
history: { type: "string" },
|
|
270
|
+
format: { type: "string" },
|
|
235
271
|
help: { type: "boolean" },
|
|
236
272
|
},
|
|
237
273
|
strict: true,
|
|
@@ -242,27 +278,28 @@ function parsePublishArgs(args: readonly string[]) {
|
|
|
242
278
|
async function runPublish(command: Extract<BlogCommand, { kind: "publish" }>, options: RunBlogCliOptions, output: CliOutput): Promise<number> {
|
|
243
279
|
const cwd = options.cwd ?? process.cwd();
|
|
244
280
|
const { config } = options;
|
|
281
|
+
const report = createPublishReporter(command.format, output);
|
|
282
|
+
const fail = (message: string): number => {
|
|
283
|
+
report.failed(message);
|
|
284
|
+
return EXIT_FAILED;
|
|
285
|
+
};
|
|
245
286
|
let blogOptions: ReturnType<typeof getBlogOptions>;
|
|
246
287
|
try {
|
|
247
288
|
blogOptions = getBlogOptions(config);
|
|
248
289
|
} catch (error) {
|
|
249
|
-
|
|
250
|
-
return EXIT_FAILED;
|
|
251
|
-
}
|
|
252
|
-
const paths = command.paths.length > 0 ? command.paths : [blogOptions.contentDir];
|
|
253
|
-
const files = await readArticleFiles(paths.map((path) => resolve(cwd, path)));
|
|
254
|
-
if (typeof files === "string") {
|
|
255
|
-
output.error(`softure-blog publish: ${files}`);
|
|
256
|
-
return EXIT_FAILED;
|
|
257
|
-
}
|
|
258
|
-
if (command.withdraw && files.length !== 1) {
|
|
259
|
-
output.error("softure-blog publish: --withdraw takes exactly one article file, not a folder");
|
|
260
|
-
return EXIT_FAILED;
|
|
261
|
-
}
|
|
262
|
-
if (config.database === null) {
|
|
263
|
-
output.error("softure-blog publish: the config has no database; set database.url in softure.config");
|
|
264
|
-
return EXIT_FAILED;
|
|
290
|
+
return fail(describeError(error));
|
|
265
291
|
}
|
|
292
|
+
const input = command.stdin === null
|
|
293
|
+
? { files: await readArticleFiles((command.paths.length > 0 ? command.paths : [blogOptions.contentDir]).map((path) => resolve(cwd, path))), history: undefined }
|
|
294
|
+
: await readStdinBundle(command.stdin.name, options.readStdin ?? readProcessStdin);
|
|
295
|
+
if (typeof input === "string") return fail(input);
|
|
296
|
+
const { files } = input;
|
|
297
|
+
if (typeof files === "string") return fail(files);
|
|
298
|
+
if (command.withdraw && files.length !== 1) return fail("--withdraw takes exactly one article file, not a folder");
|
|
299
|
+
if (command.history !== null && input.history !== undefined) return fail("the bundle on standard input carries a history; drop --history");
|
|
300
|
+
const history = command.history === null ? input.history : await readHistoryFile(resolve(cwd, command.history));
|
|
301
|
+
if (typeof history === "string") return fail(history);
|
|
302
|
+
if (config.database === null) return fail("the config has no database; set database.url in softure.config");
|
|
266
303
|
|
|
267
304
|
const clock = options.clock ?? systemClock;
|
|
268
305
|
let gate = options.gate;
|
|
@@ -275,8 +312,7 @@ async function runPublish(command: Extract<BlogCommand, { kind: "publish" }>, op
|
|
|
275
312
|
try {
|
|
276
313
|
opened = await openDatabase(config.database, options.openDatabase);
|
|
277
314
|
} catch (error) {
|
|
278
|
-
|
|
279
|
-
return EXIT_FAILED;
|
|
315
|
+
return fail(describeError(error));
|
|
280
316
|
}
|
|
281
317
|
try {
|
|
282
318
|
const run = await runBlogPublish(
|
|
@@ -288,9 +324,10 @@ async function runPublish(command: Extract<BlogCommand, { kind: "publish" }>, op
|
|
|
288
324
|
reservedSlugs: getBlogReservedSlugs(config),
|
|
289
325
|
...(blogOptions.fields === undefined ? {} : { fields: blogOptions.fields }),
|
|
290
326
|
...(gate === undefined ? {} : { gate }),
|
|
327
|
+
...(history === undefined ? {} : { history }),
|
|
291
328
|
},
|
|
292
329
|
);
|
|
293
|
-
|
|
330
|
+
report.run(run);
|
|
294
331
|
if (run.status === "done") {
|
|
295
332
|
// Before the IndexNow submit: a crawler that answers the ping must find the new text.
|
|
296
333
|
const refresh = await requestBlogRefresh(config, run.changes, {
|
|
@@ -299,16 +336,15 @@ async function runPublish(command: Extract<BlogCommand, { kind: "publish" }>, op
|
|
|
299
336
|
...(options.env === undefined ? {} : { env: options.env }),
|
|
300
337
|
...(options.refreshFetch === undefined ? {} : { fetchImpl: options.refreshFetch }),
|
|
301
338
|
});
|
|
302
|
-
|
|
339
|
+
report.refresh(refresh);
|
|
303
340
|
}
|
|
304
341
|
if (run.status === "done" && command.indexNow) {
|
|
305
|
-
|
|
342
|
+
report.indexNow(await submitBlogChanges(config, run.changes, { commit: run.committed, ...(options.indexNowFetch === undefined ? {} : { fetchImpl: options.indexNowFetch }) }));
|
|
306
343
|
}
|
|
307
|
-
return
|
|
344
|
+
return run.status === "done" ? EXIT_OK : EXIT_FAILED;
|
|
308
345
|
} catch (error) {
|
|
309
346
|
// Driver errors: the message only, never a stack or the database URL.
|
|
310
|
-
|
|
311
|
-
return EXIT_FAILED;
|
|
347
|
+
return fail(`${describeError(error)} (did softure migrate run?)`);
|
|
312
348
|
} finally {
|
|
313
349
|
await opened.close();
|
|
314
350
|
}
|
|
@@ -353,7 +389,7 @@ async function runCheck(command: Extract<BlogCommand, { kind: "check" }>, option
|
|
|
353
389
|
const resolveInternalLink = createInternalLinkResolver({
|
|
354
390
|
appDir: findAppDir(cwd, settings.options.appDir),
|
|
355
391
|
privateRouteSegments: settings.options.privateRouteSegments,
|
|
356
|
-
paths: settings.
|
|
392
|
+
paths: settings.paths,
|
|
357
393
|
content,
|
|
358
394
|
});
|
|
359
395
|
const results = await checkArticleFiles(files, {
|
|
@@ -450,72 +486,66 @@ function reportCheck(results: readonly FileCheckResult[], files: readonly ReadAr
|
|
|
450
486
|
return errors > 0 ? EXIT_FAILED : EXIT_OK;
|
|
451
487
|
}
|
|
452
488
|
|
|
453
|
-
function reportRun(run: BlogPublishRun, output: CliOutput): number {
|
|
454
|
-
for (const warning of run.warnings) output.error(`warning ${formatProblem(warning)}`);
|
|
455
|
-
if (run.status === "refused") {
|
|
456
|
-
for (const problem of run.problems) output.error(`error ${formatProblem(problem)}`);
|
|
457
|
-
output.error("refused: nothing written; fix the problems above");
|
|
458
|
-
return EXIT_FAILED;
|
|
459
|
-
}
|
|
460
|
-
for (const change of run.changes) {
|
|
461
|
-
output.log(formatChange(change));
|
|
462
|
-
if (change.previousSlug !== null) output.log(`moved ${change.id} ${change.previousSlug} -> ${change.slug}`);
|
|
463
|
-
}
|
|
464
|
-
const count = (action: PublishedChange["action"]) => String(run.changes.filter((change) => change.action === action).length);
|
|
465
|
-
output.log(`summary: added ${count("added")}, changed ${count("changed")}, unchanged ${count("unchanged")}`);
|
|
466
|
-
output.log(run.committed ? "written" : "dry run: nothing written; pass --commit to write");
|
|
467
|
-
return EXIT_OK;
|
|
468
|
-
}
|
|
469
489
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
case "failed":
|
|
486
|
-
output.error(`warning cache: ${outcome.reason} (${outcome.code}); the publish is written, the app shows it within ${String(outcome.revalidateSeconds)} s`);
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
const stdinBundleSchema = z.strictObject({
|
|
495
|
+
files: z
|
|
496
|
+
.array(z.strictObject({ name: z.string().regex(/^[^/\\]+\.md$/, "must be a file name <slug>.md"), text: z.string() }))
|
|
497
|
+
.min(1, "must list at least one file"),
|
|
498
|
+
/** The `--history` file's content, for a publish whose only input is standard input. */
|
|
499
|
+
history: z.unknown().optional(),
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
interface StdinBundle {
|
|
503
|
+
readonly files: ArticleFile[];
|
|
504
|
+
readonly history: ArticleHistoryMap | undefined;
|
|
489
505
|
}
|
|
490
506
|
|
|
491
|
-
/**
|
|
492
|
-
function
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
case "failed":
|
|
507
|
-
output.error(`warning indexnow: ${outcome.reason} (${outcome.code}); the publish is written, submit the addresses later: ${paths.join(" ")}`);
|
|
508
|
-
return;
|
|
507
|
+
/** What `--stdin` gives: one file named by `--name`, or the JSON bundle (files, optional history); or why it cannot be read. */
|
|
508
|
+
async function readStdinBundle(name: string | null, readStdin: () => Promise<string>): Promise<StdinBundle | string> {
|
|
509
|
+
let text: string;
|
|
510
|
+
try {
|
|
511
|
+
text = await readStdin();
|
|
512
|
+
} catch (error) {
|
|
513
|
+
return `cannot read standard input: ${describeError(error)}`;
|
|
514
|
+
}
|
|
515
|
+
if (name !== null) return text.trim() === "" ? `standard input is empty; pipe the text of ${name}` : { files: [{ name, text }], history: undefined };
|
|
516
|
+
let json: unknown;
|
|
517
|
+
try {
|
|
518
|
+
json = JSON.parse(text);
|
|
519
|
+
} catch {
|
|
520
|
+
// The parser's message names a position in a text nobody sees; say what was expected instead.
|
|
521
|
+
return 'standard input is not a JSON bundle {"files":[{"name":"<slug>.md","text":"..."}]}; pass --name <slug>.md for one file';
|
|
509
522
|
}
|
|
523
|
+
const parsed = stdinBundleSchema.safeParse(json);
|
|
524
|
+
if (!parsed.success) return `the bundle on standard input: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "bundle"}: ${issue.message}`).join("; ")}`;
|
|
525
|
+
if (parsed.data.history === undefined) return { files: parsed.data.files, history: undefined };
|
|
526
|
+
const history = parseArticleHistory(parsed.data.history);
|
|
527
|
+
if (!history.ok) return `the bundle on standard input: history.${history.errors.join("; history.")}`;
|
|
528
|
+
return { files: parsed.data.files, history: history.history };
|
|
510
529
|
}
|
|
511
530
|
|
|
512
|
-
function
|
|
513
|
-
const
|
|
514
|
-
|
|
531
|
+
async function readProcessStdin(): Promise<string> {
|
|
532
|
+
const chunks: Buffer[] = [];
|
|
533
|
+
for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : (chunk as Buffer));
|
|
534
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
515
535
|
}
|
|
516
536
|
|
|
517
|
-
|
|
518
|
-
|
|
537
|
+
/** The `--history` file, parsed; or why it cannot be used. */
|
|
538
|
+
async function readHistoryFile(path: string): Promise<ArticleHistoryMap | string> {
|
|
539
|
+
const text = await readText(path);
|
|
540
|
+
if (text === null) return `cannot read ${path}`;
|
|
541
|
+
let json: unknown;
|
|
542
|
+
try {
|
|
543
|
+
json = JSON.parse(text);
|
|
544
|
+
} catch (error) {
|
|
545
|
+
return `${path} is not JSON: ${describeError(error)}`;
|
|
546
|
+
}
|
|
547
|
+
const parsed = parseArticleHistory(json);
|
|
548
|
+
return parsed.ok ? parsed.history : `${path}: ${parsed.errors.join("; ")}`;
|
|
519
549
|
}
|
|
520
550
|
|
|
521
551
|
interface ReadArticleFile extends ArticleFile {
|
package/src/cli/skill.ts
CHANGED
|
@@ -69,7 +69,8 @@ export function renderAppSections(sections: readonly BlogSkillSection[]): string
|
|
|
69
69
|
/** The values the templates read. */
|
|
70
70
|
export function getSkillValues(config: SoftureConfig, settings: QualitySettings, options: RenderBlogSkillOptions = {}): SkillValues {
|
|
71
71
|
const blogOptions = getBlogOptions(config);
|
|
72
|
-
const { limits, ymyl, voice,
|
|
72
|
+
const { limits, ymyl, voice, plugins } = settings.options;
|
|
73
|
+
const { paths } = settings;
|
|
73
74
|
const fieldKeys = Object.keys(blogOptions.fields?.shape ?? {});
|
|
74
75
|
const reservedSlugs = getBlogReservedSlugs(config);
|
|
75
76
|
return {
|
package/src/contract.ts
CHANGED
|
@@ -84,5 +84,13 @@ export type BlogPublishResult =
|
|
|
84
84
|
readonly after: BlogArticleState;
|
|
85
85
|
/** The slug that has just entered the slug history. */
|
|
86
86
|
readonly previousSlug: string | null;
|
|
87
|
+
/** Set when the article's history (`publishArticle({ history })`) was applied. */
|
|
88
|
+
readonly imported?: true;
|
|
87
89
|
}
|
|
88
|
-
| {
|
|
90
|
+
| {
|
|
91
|
+
readonly ok: false;
|
|
92
|
+
readonly error: BlogSlugErrorCode;
|
|
93
|
+
readonly otherArticleId: string;
|
|
94
|
+
/** Set when the slug is an old slug from the article's history, not its current one. */
|
|
95
|
+
readonly slug?: string;
|
|
96
|
+
};
|