@docsworth/cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -8
- package/dist/cli.js +52 -3
- package/dist/core/acl.d.ts +9 -0
- package/dist/core/acl.js +40 -6
- package/dist/core/adopt.d.ts +68 -0
- package/dist/core/adopt.js +665 -0
- package/dist/core/config.d.ts +6 -1
- package/dist/core/config.js +46 -2
- package/dist/core/edits.d.ts +13 -0
- package/dist/core/edits.js +20 -0
- package/dist/core/frontmatter.d.ts +12 -0
- package/dist/core/frontmatter.js +16 -7
- package/dist/core/gfm.d.ts +13 -0
- package/dist/core/gfm.js +244 -0
- package/dist/core/links.d.ts +82 -0
- package/dist/core/links.js +345 -0
- package/dist/core/lint.d.ts +1 -2
- package/dist/core/lint.js +40 -6
- package/dist/core/mcp/server.js +11 -2
- package/dist/core/printable.d.ts +4 -0
- package/dist/core/printable.js +16 -0
- package/dist/core/references.d.ts +38 -3
- package/dist/core/references.js +543 -41
- package/dist/core/repo.d.ts +4 -0
- package/dist/core/repo.js +3 -1
- package/dist/core/schema.d.ts +4 -0
- package/dist/core/schema.js +8 -6
- package/dist/core/search.d.ts +8 -0
- package/dist/core/search.js +34 -0
- package/dist/core/serve/server.d.ts +1 -2
- package/dist/core/serve/sessions.d.ts +5 -4
- package/dist/core/serve/sessions.js +102 -29
- package/dist/core/service.d.ts +118 -20
- package/dist/core/service.js +249 -91
- package/dist/core/stale.js +2 -1
- package/dist/core/tool-registry.js +6 -3
- package/dist/core/verify-pages.d.ts +19 -0
- package/dist/core/verify-pages.js +110 -0
- package/dist/core/version.d.ts +2 -2
- package/dist/core/version.js +2 -2
- package/dist/web/assets/index-8J-FUbb0.js +29 -0
- package/dist/web/assets/index-BGpAZzeM.css +1 -0
- package/dist/web/index.html +2 -2
- package/docs/format-spec.md +40 -2
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/bin/docsworth-mcp.mjs +1 -1
- package/dist/web/assets/index-BglLMIyN.css +0 -1
- package/dist/web/assets/index-DugFPoRu.js +0 -25
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { lstatSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { isDeepStrictEqual } from "node:util";
|
|
4
|
+
import { isMap, isScalar, parseDocument } from "yaml";
|
|
5
|
+
import { hardLinked, repoPaths, symlinkReason, walk, within } from "./adopt.js";
|
|
6
|
+
import { formatDate, utcToday } from "./duration.js";
|
|
7
|
+
import { applyEdits } from "./edits.js";
|
|
8
|
+
import { UserError } from "./errors.js";
|
|
9
|
+
import { frontmatterSpan, splitFrontmatter } from "./frontmatter.js";
|
|
10
|
+
import { validateFrontmatter } from "./schema.js";
|
|
11
|
+
// `docsworth verify <path…>`: a team's verification of its pages, in the working tree. A draft
|
|
12
|
+
// or canonical page becomes canonical with today's `last_verified`, each value replaced where
|
|
13
|
+
// it's written, so nothing else on the page moves. The PR the team reviews is the safeguard,
|
|
14
|
+
// as with editing `last_verified` by hand.
|
|
15
|
+
export class VerifyError extends UserError {
|
|
16
|
+
}
|
|
17
|
+
export function verifyPages(root, paths, opts) {
|
|
18
|
+
if (!paths.length)
|
|
19
|
+
throw new VerifyError("name the pages to verify: a page, or a directory of them");
|
|
20
|
+
const { repo, config, knowledge } = repoPaths(root, VerifyError, "verify");
|
|
21
|
+
const rel = (abs) => relative(repo, abs).split(sep).join("/");
|
|
22
|
+
// R9: each path, by real path, under the knowledge root; a directory walked as adopt walks it.
|
|
23
|
+
const found = { files: [], symlinks: [] };
|
|
24
|
+
for (const p of paths) {
|
|
25
|
+
const abs = resolve(root, p);
|
|
26
|
+
// The entry itself, not where a link leads: a dangling link is still a link.
|
|
27
|
+
let entry;
|
|
28
|
+
try {
|
|
29
|
+
entry = lstatSync(abs);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new VerifyError(`${p}: no such file or directory`);
|
|
33
|
+
}
|
|
34
|
+
const located = join(realpathSync(dirname(abs)), basename(abs));
|
|
35
|
+
if (!within(knowledge, located))
|
|
36
|
+
throw new VerifyError(`${p} is outside the knowledge root (${config.knowledge_root}); verify changes only pages there`);
|
|
37
|
+
if (entry.isSymbolicLink()) {
|
|
38
|
+
found.symlinks.push(located);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const real = realpathSync(abs);
|
|
42
|
+
if (!within(knowledge, real))
|
|
43
|
+
throw new VerifyError(`${p} is outside the knowledge root (${config.knowledge_root}); verify changes only pages there`);
|
|
44
|
+
if (statSync(real).isDirectory())
|
|
45
|
+
walk(real, found);
|
|
46
|
+
else if (real.endsWith(".md"))
|
|
47
|
+
found.files.push(real);
|
|
48
|
+
else
|
|
49
|
+
throw new VerifyError(`${p} isn't a page (a .md file) or a directory of them`);
|
|
50
|
+
}
|
|
51
|
+
const today = formatDate(utcToday(opts.now));
|
|
52
|
+
const result = { dryRun: opts.dryRun === true, verified: [], unchanged: [], left: [] };
|
|
53
|
+
for (const abs of new Set(found.symlinks))
|
|
54
|
+
result.left.push({ path: rel(abs), reason: symlinkReason("verify") });
|
|
55
|
+
const writes = [];
|
|
56
|
+
for (const abs of [...new Set(found.files)].sort((a, b) => rel(a).localeCompare(rel(b)))) {
|
|
57
|
+
const path = rel(abs);
|
|
58
|
+
const shared = hardLinked(abs, "verify");
|
|
59
|
+
const text = readFileSync(abs, "utf8");
|
|
60
|
+
const out = shared ? { reason: shared } : verified(text, today);
|
|
61
|
+
if ("reason" in out)
|
|
62
|
+
result.left.push({ path, reason: out.reason });
|
|
63
|
+
else if (out.text === text)
|
|
64
|
+
result.unchanged.push(path);
|
|
65
|
+
else {
|
|
66
|
+
writes.push({ abs, text: out.text });
|
|
67
|
+
result.verified.push(path);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!opts.dryRun) {
|
|
71
|
+
// Each was found by real path; checked again before the first write. (Defense in depth.)
|
|
72
|
+
for (const w of writes) {
|
|
73
|
+
if (!lstatSync(w.abs).isFile() || lstatSync(w.abs).nlink > 1 || !within(knowledge, realpathSync(w.abs)))
|
|
74
|
+
throw new VerifyError(`${rel(w.abs)} changed while verify ran; nothing written`);
|
|
75
|
+
}
|
|
76
|
+
for (const w of writes)
|
|
77
|
+
writeFileSync(w.abs, w.text);
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
/** The page's text with `status: canonical` and `last_verified: today`, each written over its value; or why not. */
|
|
82
|
+
function verified(text, today) {
|
|
83
|
+
const doc = splitFrontmatter(text);
|
|
84
|
+
const span = frontmatterSpan(text);
|
|
85
|
+
if (!doc.data || !span)
|
|
86
|
+
return { reason: `its frontmatter doesn't parse: ${doc.error}` };
|
|
87
|
+
const yaml = parseDocument(text.slice(span.start, span.end));
|
|
88
|
+
if (!isMap(yaml.contents) || yaml.contents.flow)
|
|
89
|
+
return { reason: "its frontmatter is a flow mapping (`{…}`); verify edits a block mapping only" };
|
|
90
|
+
const valid = validateFrontmatter(doc.data);
|
|
91
|
+
if (!valid.ok)
|
|
92
|
+
return { reason: `it isn't a valid page (${valid.issues.map((i) => i.message).join("; ")}): run \`docsworth adopt\`, or fix it by hand` };
|
|
93
|
+
if (valid.value.status === "deprecated")
|
|
94
|
+
return { reason: "it's deprecated: verify doesn't make it citable again" };
|
|
95
|
+
const want = { status: "canonical", last_verified: today };
|
|
96
|
+
const edits = [];
|
|
97
|
+
for (const [key, value] of Object.entries(want)) {
|
|
98
|
+
const node = yaml.contents.get(key, true);
|
|
99
|
+
if (!isScalar(node) || !node.range)
|
|
100
|
+
return { reason: `its \`${key}\` isn't a plain value verify can write over: set it by hand` };
|
|
101
|
+
if (node.value !== value)
|
|
102
|
+
edits.push({ start: span.start + node.range[0], end: span.start + node.range[1], insert: value });
|
|
103
|
+
}
|
|
104
|
+
const next = applyEdits(text, edits);
|
|
105
|
+
// The page must read as it did, but for the two values.
|
|
106
|
+
const after = splitFrontmatter(next);
|
|
107
|
+
if (!after.data || after.body !== doc.body || !isDeepStrictEqual(after.data, { ...doc.data, ...want }))
|
|
108
|
+
return { reason: "writing the two values would change what the page's other keys say: set them by hand" };
|
|
109
|
+
return { text: next };
|
|
110
|
+
}
|
package/dist/core/version.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Format version of the knowledge repo contract in docs/format-spec.md. */
|
|
2
2
|
export declare const FORMAT_VERSION = 1;
|
|
3
|
-
export declare const PACKAGE_VERSION = "0.
|
|
3
|
+
export declare const PACKAGE_VERSION = "0.3.0";
|
|
4
4
|
/**
|
|
5
5
|
* The npm package Docsworth is published as, with its version range: what `docsworth init
|
|
6
6
|
* --ci` writes into a workflow, and what the plugin launcher runs without DOCSWORTH_BIN.
|
|
@@ -9,7 +9,7 @@ export declare const PACKAGE_VERSION = "0.2.0";
|
|
|
9
9
|
* may fetch any of those. PUBLISHED_PACKAGE in plugin/bin/docsworth-mcp.mjs is kept equal
|
|
10
10
|
* by a test, and scripts/release-guard.ts refuses a release outside its range.
|
|
11
11
|
*/
|
|
12
|
-
export declare const PUBLISHED_PACKAGE = "@docsworth/cli@^0.
|
|
12
|
+
export declare const PUBLISHED_PACKAGE = "@docsworth/cli@^0.3";
|
|
13
13
|
/** The one package we publish (release checklist, decision 1): a release of any other name is refused. */
|
|
14
14
|
export declare const PACKAGE_NAME = "@docsworth/cli";
|
|
15
15
|
/**
|
package/dist/core/version.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Format version of the knowledge repo contract in docs/format-spec.md. */
|
|
2
2
|
export const FORMAT_VERSION = 1;
|
|
3
|
-
export const PACKAGE_VERSION = "0.
|
|
3
|
+
export const PACKAGE_VERSION = "0.3.0";
|
|
4
4
|
/**
|
|
5
5
|
* The npm package Docsworth is published as, with its version range: what `docsworth init
|
|
6
6
|
* --ci` writes into a workflow, and what the plugin launcher runs without DOCSWORTH_BIN.
|
|
@@ -9,7 +9,7 @@ export const PACKAGE_VERSION = "0.2.0";
|
|
|
9
9
|
* may fetch any of those. PUBLISHED_PACKAGE in plugin/bin/docsworth-mcp.mjs is kept equal
|
|
10
10
|
* by a test, and scripts/release-guard.ts refuses a release outside its range.
|
|
11
11
|
*/
|
|
12
|
-
export const PUBLISHED_PACKAGE = "@docsworth/cli@^0.
|
|
12
|
+
export const PUBLISHED_PACKAGE = "@docsworth/cli@^0.3";
|
|
13
13
|
/** The one package we publish (release checklist, decision 1): a release of any other name is refused. */
|
|
14
14
|
export const PACKAGE_NAME = "@docsworth/cli";
|
|
15
15
|
/**
|