@markstanden/opencode-docs-plugin 0.6.0 → 0.6.2
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/package.json +1 -1
- package/src/SKILL.md +6 -4
- package/src/index.ts +229 -118
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markstanden/opencode-docs-plugin",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenCode V2 plugin: a `docs` tool and `/docs` command over configurable knowledge lakes (repo, machine, global and read-only wiki), nested README.md search, and the writing convention they share",
|
|
6
6
|
"repository": {
|
package/src/SKILL.md
CHANGED
|
@@ -21,14 +21,16 @@ means two docs; one omnibus doc is out of place everywhere.
|
|
|
21
21
|
|
|
22
22
|
A **blank `search`** lists recent docs, one line each; a **`search`** ranks
|
|
23
23
|
them, falling back to `Closest matches` and matching terms as **substrings** —
|
|
24
|
-
so a typo finds nothing
|
|
25
|
-
`help` lists the
|
|
24
|
+
so a typo finds nothing. Terms shorter than `MIN_TERM_LENGTH` (3) are dropped
|
|
25
|
+
when longer terms exist; otherwise all query terms are kept. `help` lists the
|
|
26
|
+
lakes and ranking.
|
|
26
27
|
|
|
27
28
|
Open the match with `read`: the root-relative path, or `lake:path` when several
|
|
28
29
|
lakes exist. A README result is `instructions:path` and opens with the built-in
|
|
29
30
|
`read`. Consult matching docs before a broad grep; a tie ranks repo,
|
|
30
|
-
instructions, global, machine, wiki.
|
|
31
|
-
and `wiki
|
|
31
|
+
instructions, global, machine, wiki. Knowledge lakes exclude `plans/`,
|
|
32
|
+
`requirements/` and `wiki/` at the root; wiki lakes exclude only
|
|
33
|
+
dot-directories and `node_modules`.
|
|
32
34
|
|
|
33
35
|
## Record it
|
|
34
36
|
|
package/src/index.ts
CHANGED
|
@@ -250,9 +250,77 @@ function splitKeywords(value: string): string[] {
|
|
|
250
250
|
.filter((keyword) => keyword !== "");
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
/**
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
/**
|
|
254
|
+
* An open fenced code block: the opener's delimiter character and run length.
|
|
255
|
+
* The length and character are remembered so a shorter or different-character
|
|
256
|
+
* run inside the block cannot close it.
|
|
257
|
+
*/
|
|
258
|
+
interface Fence {
|
|
259
|
+
readonly char: "`" | "~";
|
|
260
|
+
readonly length: number;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The supported Markdown fence subset: a backtick or tilde run of three or
|
|
265
|
+
* more, indented by at most three spaces. A backtick opener's info string may
|
|
266
|
+
* not contain a backtick. Four-space indentation is an indented code block, not
|
|
267
|
+
* a fence, and is not modelled; nor are tab-indented fences or the full
|
|
268
|
+
* CommonMark block grammar.
|
|
269
|
+
* @param line One source line.
|
|
270
|
+
* @returns The opened fence, or null when the line is not an opener.
|
|
271
|
+
*/
|
|
272
|
+
function fenceOpener(line: string): Fence | null {
|
|
273
|
+
const match = /^ {0,3}(`{3,}|~{3,})/.exec(line);
|
|
274
|
+
if (match === null) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
const run = match[1] ?? "";
|
|
278
|
+
const info = line.slice(match[0].length);
|
|
279
|
+
const char = run[0] as Fence["char"];
|
|
280
|
+
if (char === "`" && info.includes("`")) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
return { char, length: run.length };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Whether a line closes `fence`: the same delimiter character, a run at least
|
|
288
|
+
* as long as the opener, and nothing after it but spaces or tabs.
|
|
289
|
+
* @param line One source line.
|
|
290
|
+
* @param fence The open fence.
|
|
291
|
+
* @returns True when the line closes the block.
|
|
292
|
+
*/
|
|
293
|
+
function closesFence(line: string, fence: Fence): boolean {
|
|
294
|
+
const match = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(line);
|
|
295
|
+
if (match === null) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
const run = match[1] ?? "";
|
|
299
|
+
return run.startsWith(fence.char) && run.length >= fence.length;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The body's lines outside fenced code blocks, fence delimiters included in
|
|
304
|
+
* neither. An unclosed fence keeps every remaining line inside the block.
|
|
305
|
+
* @param body Doc body, front-matter already stripped.
|
|
306
|
+
* @returns Source lines with fenced blocks removed.
|
|
307
|
+
*/
|
|
308
|
+
function outsideFenceLines(body: string): string[] {
|
|
309
|
+
const lines: string[] = [];
|
|
310
|
+
let fence: Fence | null = null;
|
|
311
|
+
for (const line of body.split("\n")) {
|
|
312
|
+
if (fence === null) {
|
|
313
|
+
const opener = fenceOpener(line);
|
|
314
|
+
if (opener !== null) {
|
|
315
|
+
fence = opener;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
lines.push(line);
|
|
319
|
+
} else if (closesFence(line, fence)) {
|
|
320
|
+
fence = null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return lines;
|
|
256
324
|
}
|
|
257
325
|
|
|
258
326
|
/** Drop an ATX heading's trailing `#` decoration (and any trailing spaces). */
|
|
@@ -279,19 +347,11 @@ interface BodyScan {
|
|
|
279
347
|
function scanBody(body: string): BodyScan {
|
|
280
348
|
const headings: string[] = [];
|
|
281
349
|
const spans: string[] = [];
|
|
282
|
-
|
|
283
|
-
for (const line of body.split("\n")) {
|
|
284
|
-
if (isFence(line)) {
|
|
285
|
-
fenced = !fenced;
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
if (fenced) {
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
350
|
+
for (const line of outsideFenceLines(body)) {
|
|
291
351
|
const heading = /^(#{2,3})\s+/.exec(line);
|
|
292
352
|
if (heading !== null) {
|
|
293
353
|
const text = stripTrailingHashes(
|
|
294
|
-
line.slice(heading[0].length).
|
|
354
|
+
line.slice(heading[0].length).replaceAll("`", ""),
|
|
295
355
|
).trim();
|
|
296
356
|
if (text !== "") {
|
|
297
357
|
headings.push(text);
|
|
@@ -358,18 +418,7 @@ function plainProse(text: string): string {
|
|
|
358
418
|
|
|
359
419
|
/** The body with fenced code blocks removed. */
|
|
360
420
|
function withoutFences(body: string): string {
|
|
361
|
-
|
|
362
|
-
let fenced = false;
|
|
363
|
-
for (const line of body.split("\n")) {
|
|
364
|
-
if (isFence(line)) {
|
|
365
|
-
fenced = !fenced;
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
if (!fenced) {
|
|
369
|
-
lines.push(line);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
return lines.join("\n");
|
|
421
|
+
return outsideFenceLines(body).join("\n");
|
|
373
422
|
}
|
|
374
423
|
|
|
375
424
|
/** Block prefixes that are never a wiki summary. */
|
|
@@ -752,6 +801,20 @@ export function rankDocs(
|
|
|
752
801
|
};
|
|
753
802
|
}
|
|
754
803
|
|
|
804
|
+
/** The provenance prefix for a result: instructions, non-primary lake, or none. */
|
|
805
|
+
function resultPrefix(
|
|
806
|
+
entry: DocEntry,
|
|
807
|
+
primaryLake: string | undefined,
|
|
808
|
+
): string {
|
|
809
|
+
if (entry.class === "instructions") {
|
|
810
|
+
return "instructions";
|
|
811
|
+
}
|
|
812
|
+
if (entry.lake !== "" && entry.lake !== primaryLake) {
|
|
813
|
+
return entry.lake;
|
|
814
|
+
}
|
|
815
|
+
return "";
|
|
816
|
+
}
|
|
817
|
+
|
|
755
818
|
/**
|
|
756
819
|
* Render matched docs as `path — Area — date` lines with the summary indented
|
|
757
820
|
* beneath; a non-primary lake is prefixed `lake:` so provenance is clear. A
|
|
@@ -782,12 +845,8 @@ export function render(
|
|
|
782
845
|
);
|
|
783
846
|
const meta = parts.join(" — ");
|
|
784
847
|
const head = meta === "" ? entry.path : `${entry.path} — ${meta}`;
|
|
785
|
-
const
|
|
786
|
-
|
|
787
|
-
? `instructions:${head}`
|
|
788
|
-
: entry.lake !== "" && entry.lake !== options.primaryLake
|
|
789
|
-
? `${entry.lake}:${head}`
|
|
790
|
-
: head;
|
|
848
|
+
const prefix = resultPrefix(entry, options.primaryLake);
|
|
849
|
+
const labelled = prefix === "" ? head : `${prefix}:${head}`;
|
|
791
850
|
return entry.summary === ""
|
|
792
851
|
? labelled
|
|
793
852
|
: `${labelled}\n ${entry.summary}`;
|
|
@@ -824,7 +883,7 @@ export function explainRanking(): string {
|
|
|
824
883
|
"How docs search ranks:",
|
|
825
884
|
`- Fields weighted: title ${FIELD_WEIGHTS.title} > path ${FIELD_WEIGHTS.path} > keywords ${FIELD_WEIGHTS.keywords} > identifiers ${FIELD_WEIGHTS.identifiers} > area ${FIELD_WEIGHTS.area} > summary ${FIELD_WEIGHTS.summary} > wiki prose ${FIELD_WEIGHTS.text}.`,
|
|
826
885
|
`- Each term scores its best field as weight × quality; quality is exact > prefix > substring.`,
|
|
827
|
-
`- Terms shorter than MIN_TERM_LENGTH (${MIN_TERM_LENGTH}) are
|
|
886
|
+
`- Terms shorter than MIN_TERM_LENGTH (${MIN_TERM_LENGTH}) are dropped when longer terms exist; otherwise all query terms are kept.`,
|
|
828
887
|
"- Search covers every configured lake and nested project `README.md` files, honouring `.gitignore` (dependency and build directories are skipped when the project is not in git). READMEs are searchable only: `add` and `check` ignore them, and their results are prefixed `instructions:` so they read with the built-in `read` tool.",
|
|
829
888
|
`- Each source reserves up to ${DEFAULT_PER_LAKE_QUOTA} results (configurable), then leftover slots fill by score. Every call returns at most MAX_RESULTS (${MAX_RESULTS}), best matches first, then the most recently edited; anything cut is counted as "… and N more not shown."`,
|
|
830
889
|
"- Class precedence on a tie: repo > instructions > global > machine > wiki.",
|
|
@@ -945,7 +1004,7 @@ export function gitDates(
|
|
|
945
1004
|
return dates;
|
|
946
1005
|
}
|
|
947
1006
|
const result = spawnSync(
|
|
948
|
-
"git",
|
|
1007
|
+
"git", // NOSONAR: portable git lookup; cwd is trusted local config, not model input.
|
|
949
1008
|
// `--relative` keeps the output paths root-relative to `cwd`; without
|
|
950
1009
|
// it a lake rooted at `<repo>/docs` maps `docs/foo.md` while the
|
|
951
1010
|
// requested paths are `foo.md`, silently falling back to mtimes.
|
|
@@ -1059,6 +1118,22 @@ function skipDirectory(
|
|
|
1059
1118
|
return directory === root && excluded.includes(item.name);
|
|
1060
1119
|
}
|
|
1061
1120
|
|
|
1121
|
+
/**
|
|
1122
|
+
* Code-unit string order, matching the default sort but stated explicitly.
|
|
1123
|
+
* @param a Left string.
|
|
1124
|
+
* @param b Right string.
|
|
1125
|
+
* @returns Negative, zero or positive in code-unit order.
|
|
1126
|
+
*/
|
|
1127
|
+
function compareText(a: string, b: string): number {
|
|
1128
|
+
if (a < b) {
|
|
1129
|
+
return -1;
|
|
1130
|
+
}
|
|
1131
|
+
if (a > b) {
|
|
1132
|
+
return 1;
|
|
1133
|
+
}
|
|
1134
|
+
return 0;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1062
1137
|
/**
|
|
1063
1138
|
* Walk a lake root, returning its `.md` paths relative to the root. Dot
|
|
1064
1139
|
* directories are skipped (which drops `.git` and `.attachments`), as are
|
|
@@ -1106,7 +1181,7 @@ export function collectDocPaths(
|
|
|
1106
1181
|
if (existsSync(root)) {
|
|
1107
1182
|
walk(root);
|
|
1108
1183
|
}
|
|
1109
|
-
return paths.sort();
|
|
1184
|
+
return paths.sort(compareText);
|
|
1110
1185
|
}
|
|
1111
1186
|
|
|
1112
1187
|
/**
|
|
@@ -1221,7 +1296,7 @@ export function readmePaths(
|
|
|
1221
1296
|
.filter(
|
|
1222
1297
|
(path) => !skip.some((root) => isUnder(join(location, path), root)),
|
|
1223
1298
|
)
|
|
1224
|
-
.sort();
|
|
1299
|
+
.sort(compareText);
|
|
1225
1300
|
}
|
|
1226
1301
|
|
|
1227
1302
|
/**
|
|
@@ -1230,7 +1305,7 @@ export function readmePaths(
|
|
|
1230
1305
|
*/
|
|
1231
1306
|
function gitReadmePaths(location: string): string[] | undefined {
|
|
1232
1307
|
const result = spawnSync(
|
|
1233
|
-
"git",
|
|
1308
|
+
"git", // NOSONAR: portable git lookup; cwd is trusted local config, not model input.
|
|
1234
1309
|
["ls-files", "-z", "--cached", "--others", "--exclude-standard"],
|
|
1235
1310
|
{ cwd: location, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 },
|
|
1236
1311
|
);
|
|
@@ -1616,9 +1691,11 @@ export const CLAIM_SECTIONS = [
|
|
|
1616
1691
|
* @returns True when a claim-section heading is present.
|
|
1617
1692
|
*/
|
|
1618
1693
|
export function hasClaimSection(body: string): boolean {
|
|
1619
|
-
const claims =
|
|
1694
|
+
const claims = new Set(
|
|
1695
|
+
CLAIM_SECTIONS.map((section) => section.toLowerCase()),
|
|
1696
|
+
);
|
|
1620
1697
|
return scanBody(body).headings.some((heading) =>
|
|
1621
|
-
claims.
|
|
1698
|
+
claims.has(heading.toLowerCase()),
|
|
1622
1699
|
);
|
|
1623
1700
|
}
|
|
1624
1701
|
|
|
@@ -1698,7 +1775,7 @@ function nearTwinPairs(
|
|
|
1698
1775
|
const query = identityQuery(doc.title, doc.keywords);
|
|
1699
1776
|
const others = entries.filter((other) => other.path !== doc.path);
|
|
1700
1777
|
for (const twin of nearTwins(query, doc.area, others)) {
|
|
1701
|
-
const key = [doc.path, twin.path].sort().join("\n");
|
|
1778
|
+
const key = [doc.path, twin.path].sort(compareText).join("\n");
|
|
1702
1779
|
if (!pairs.has(key)) {
|
|
1703
1780
|
pairs.set(key, [doc, twin]);
|
|
1704
1781
|
}
|
|
@@ -1732,26 +1809,47 @@ function resolveLink(from: string, target: string): string | null {
|
|
|
1732
1809
|
return stack.join("/");
|
|
1733
1810
|
}
|
|
1734
1811
|
|
|
1812
|
+
/** An inline link's destination and the index of its closing `)`. */
|
|
1813
|
+
interface InlineLink {
|
|
1814
|
+
readonly destination: string | undefined;
|
|
1815
|
+
readonly end: number;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/**
|
|
1819
|
+
* Parse an inline `[text](target)` link starting at `start`.
|
|
1820
|
+
* @param line One source line.
|
|
1821
|
+
* @param start Index of the opening `[`.
|
|
1822
|
+
* @returns The destination and closing index, or null when the link is incomplete.
|
|
1823
|
+
*/
|
|
1824
|
+
function inlineLinkAt(line: string, start: number): InlineLink | null {
|
|
1825
|
+
const close = line.indexOf("](", start + 1);
|
|
1826
|
+
if (close === -1) {
|
|
1827
|
+
return null;
|
|
1828
|
+
}
|
|
1829
|
+
const end = line.indexOf(")", close + 2);
|
|
1830
|
+
if (end === -1) {
|
|
1831
|
+
return null;
|
|
1832
|
+
}
|
|
1833
|
+
const destination = line
|
|
1834
|
+
.slice(close + 2, end)
|
|
1835
|
+
.trim()
|
|
1836
|
+
.split(/\s+/)[0];
|
|
1837
|
+
return { destination, end };
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1735
1840
|
/** Inline `[text](target)` destinations on one line; images are skipped. */
|
|
1736
1841
|
function inlineTargets(line: string): string[] {
|
|
1737
1842
|
const targets: string[] = [];
|
|
1738
1843
|
let index = 0;
|
|
1739
1844
|
while (index < line.length) {
|
|
1740
1845
|
if (line[index] === "[" && line[index - 1] !== "!") {
|
|
1741
|
-
const
|
|
1742
|
-
if (
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
const destination = line
|
|
1746
|
-
.slice(close + 2, end)
|
|
1747
|
-
.trim()
|
|
1748
|
-
.split(/\s+/)[0];
|
|
1749
|
-
if (destination !== undefined && destination !== "") {
|
|
1750
|
-
targets.push(destination);
|
|
1751
|
-
}
|
|
1752
|
-
index = end + 1;
|
|
1753
|
-
continue;
|
|
1846
|
+
const link = inlineLinkAt(line, index);
|
|
1847
|
+
if (link !== null) {
|
|
1848
|
+
if (link.destination !== undefined && link.destination !== "") {
|
|
1849
|
+
targets.push(link.destination);
|
|
1754
1850
|
}
|
|
1851
|
+
index = link.end + 1;
|
|
1852
|
+
continue;
|
|
1755
1853
|
}
|
|
1756
1854
|
}
|
|
1757
1855
|
index += 1;
|
|
@@ -1768,15 +1866,7 @@ function inlineTargets(line: string): string[] {
|
|
|
1768
1866
|
*/
|
|
1769
1867
|
export function markdownTargets(body: string): string[] {
|
|
1770
1868
|
const targets: string[] = [];
|
|
1771
|
-
|
|
1772
|
-
for (const line of body.split("\n")) {
|
|
1773
|
-
if (isFence(line)) {
|
|
1774
|
-
fenced = !fenced;
|
|
1775
|
-
continue;
|
|
1776
|
-
}
|
|
1777
|
-
if (fenced) {
|
|
1778
|
-
continue;
|
|
1779
|
-
}
|
|
1869
|
+
for (const line of outsideFenceLines(body)) {
|
|
1780
1870
|
targets.push(...inlineTargets(line));
|
|
1781
1871
|
const reference = /^\s*\[[^\]]+\]:\s*(\S+)/.exec(line);
|
|
1782
1872
|
if (reference?.[1] !== undefined) {
|
|
@@ -1962,7 +2052,8 @@ export function checkDocs(
|
|
|
1962
2052
|
if (advisory.length === 0) {
|
|
1963
2053
|
return head;
|
|
1964
2054
|
}
|
|
1965
|
-
|
|
2055
|
+
const noteLines = advisory.map((note) => `- ${note}`).join("\n");
|
|
2056
|
+
return `${head}\n\nNotes:\n${noteLines}`;
|
|
1966
2057
|
}
|
|
1967
2058
|
|
|
1968
2059
|
/** The built-in tools the miss-nudge watches: content searches, both miss-prone. */
|
|
@@ -2359,13 +2450,20 @@ function readLakesJson(
|
|
|
2359
2450
|
{ cause: error },
|
|
2360
2451
|
);
|
|
2361
2452
|
}
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2453
|
+
let lakes: unknown;
|
|
2454
|
+
if (Array.isArray(parsed)) {
|
|
2455
|
+
lakes = parsed;
|
|
2456
|
+
} else if (
|
|
2457
|
+
typeof parsed === "object" &&
|
|
2458
|
+
parsed !== null &&
|
|
2459
|
+
"lakes" in parsed
|
|
2460
|
+
) {
|
|
2461
|
+
lakes = (parsed as { lakes: unknown }).lakes;
|
|
2462
|
+
} else {
|
|
2463
|
+
lakes = undefined;
|
|
2464
|
+
}
|
|
2367
2465
|
if (!Array.isArray(lakes)) {
|
|
2368
|
-
throw new
|
|
2466
|
+
throw new TypeError(
|
|
2369
2467
|
`docs plugin ${label} must contain a "lakes" array: ${path}.`,
|
|
2370
2468
|
);
|
|
2371
2469
|
}
|
|
@@ -2435,7 +2533,7 @@ function lakeRootOf(
|
|
|
2435
2533
|
return join(location, "docs");
|
|
2436
2534
|
}
|
|
2437
2535
|
if (typeof entry.root !== "string") {
|
|
2438
|
-
throw new
|
|
2536
|
+
throw new TypeError(
|
|
2439
2537
|
`docs plugin ${lakeClass} lake root must be a string, ` +
|
|
2440
2538
|
`got ${JSON.stringify(entry.root)}.`,
|
|
2441
2539
|
);
|
|
@@ -2456,12 +2554,14 @@ function lakeNameOf(
|
|
|
2456
2554
|
lakeClass: LakeClass,
|
|
2457
2555
|
): string {
|
|
2458
2556
|
const derived = slugify(basename(root)) || lakeClass;
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2557
|
+
let name: string;
|
|
2558
|
+
if (entry.name === undefined) {
|
|
2559
|
+
name = derived;
|
|
2560
|
+
} else if (typeof entry.name === "string") {
|
|
2561
|
+
name = entry.name.trim();
|
|
2562
|
+
} else {
|
|
2563
|
+
name = "";
|
|
2564
|
+
}
|
|
2465
2565
|
if (!LAKE_NAME.test(name)) {
|
|
2466
2566
|
throw new Error(
|
|
2467
2567
|
`docs plugin lake name must match ${LAKE_NAME}, ` +
|
|
@@ -2488,8 +2588,7 @@ function lakeWritableOf(
|
|
|
2488
2588
|
`got ${JSON.stringify(entry.writable)}.`,
|
|
2489
2589
|
);
|
|
2490
2590
|
}
|
|
2491
|
-
const writable =
|
|
2492
|
-
entry.writable === undefined ? lakeClass !== "wiki" : entry.writable;
|
|
2591
|
+
const writable = entry.writable ?? lakeClass !== "wiki";
|
|
2493
2592
|
if (lakeClass === "wiki" && writable) {
|
|
2494
2593
|
throw new Error(
|
|
2495
2594
|
`docs plugin lake '${name}' is a wiki lake and cannot be writable.`,
|
|
@@ -3046,6 +3145,51 @@ const TWIN_DESCRIPTION =
|
|
|
3046
3145
|
"docs; `read` returns a matched doc verbatim; `add` writes through a typed " +
|
|
3047
3146
|
"DTO; `check` validates the lakes.";
|
|
3048
3147
|
|
|
3148
|
+
/**
|
|
3149
|
+
* Run one resolved `docs` request against the runtime.
|
|
3150
|
+
* @param runtime The resolved lakes and seams.
|
|
3151
|
+
* @param input The typed tool input.
|
|
3152
|
+
* @returns The tool result.
|
|
3153
|
+
*/
|
|
3154
|
+
function executeRequest(runtime: Runtime, input: DocsInput): Result {
|
|
3155
|
+
const request = resolveRequest(input);
|
|
3156
|
+
if (request.verb === "add") {
|
|
3157
|
+
const result = addDoc(runtime, request.add, request.lake);
|
|
3158
|
+
return {
|
|
3159
|
+
content: result.content,
|
|
3160
|
+
metadata: { written: result.written },
|
|
3161
|
+
};
|
|
3162
|
+
}
|
|
3163
|
+
if (request.verb === "check") {
|
|
3164
|
+
const result = checkLakes(runtime, request.lake);
|
|
3165
|
+
return {
|
|
3166
|
+
content: result.content,
|
|
3167
|
+
metadata: { checked: result.checked },
|
|
3168
|
+
};
|
|
3169
|
+
}
|
|
3170
|
+
if (request.verb === "read") {
|
|
3171
|
+
const result = readDoc(runtime, request.read, request.lake);
|
|
3172
|
+
return {
|
|
3173
|
+
content: result.content,
|
|
3174
|
+
metadata: { read: result.read },
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
if (request.verb === "help") {
|
|
3178
|
+
return {
|
|
3179
|
+
content: helpText(lakeInfo(runtime.lakes)),
|
|
3180
|
+
metadata: { count: 0, omitted: 0 },
|
|
3181
|
+
};
|
|
3182
|
+
}
|
|
3183
|
+
const result = searchDocs(runtime, request.search, request.lake);
|
|
3184
|
+
return {
|
|
3185
|
+
content: result.content,
|
|
3186
|
+
metadata: {
|
|
3187
|
+
count: result.count,
|
|
3188
|
+
omitted: result.omitted,
|
|
3189
|
+
},
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3049
3193
|
/**
|
|
3050
3194
|
* Build the `docs` tool from its dependencies.
|
|
3051
3195
|
*
|
|
@@ -3087,43 +3231,8 @@ export function createDocsTool(
|
|
|
3087
3231
|
options: codemode
|
|
3088
3232
|
? { namespace: "docs", codemode: true }
|
|
3089
3233
|
: { codemode: false },
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
if (request.verb === "add") {
|
|
3093
|
-
const result = addDoc(runtime, request.add, request.lake);
|
|
3094
|
-
return {
|
|
3095
|
-
content: result.content,
|
|
3096
|
-
metadata: { written: result.written },
|
|
3097
|
-
};
|
|
3098
|
-
}
|
|
3099
|
-
if (request.verb === "check") {
|
|
3100
|
-
const result = checkLakes(runtime, request.lake);
|
|
3101
|
-
return {
|
|
3102
|
-
content: result.content,
|
|
3103
|
-
metadata: { checked: result.checked },
|
|
3104
|
-
};
|
|
3105
|
-
}
|
|
3106
|
-
if (request.verb === "read") {
|
|
3107
|
-
const result = readDoc(runtime, request.read, request.lake);
|
|
3108
|
-
return {
|
|
3109
|
-
content: result.content,
|
|
3110
|
-
metadata: { read: result.read },
|
|
3111
|
-
};
|
|
3112
|
-
}
|
|
3113
|
-
if (request.verb === "help") {
|
|
3114
|
-
return {
|
|
3115
|
-
content: helpText(lakeInfo(runtime.lakes)),
|
|
3116
|
-
metadata: { count: 0, omitted: 0 },
|
|
3117
|
-
};
|
|
3118
|
-
}
|
|
3119
|
-
const result = searchDocs(runtime, request.search, request.lake);
|
|
3120
|
-
return {
|
|
3121
|
-
content: result.content,
|
|
3122
|
-
metadata: {
|
|
3123
|
-
count: result.count,
|
|
3124
|
-
omitted: result.omitted,
|
|
3125
|
-
},
|
|
3126
|
-
};
|
|
3234
|
+
execute(input) {
|
|
3235
|
+
return Promise.resolve(executeRequest(runtime, input));
|
|
3127
3236
|
},
|
|
3128
3237
|
};
|
|
3129
3238
|
}
|
|
@@ -3213,7 +3322,7 @@ export function resolveRoot(
|
|
|
3213
3322
|
return join(locationDirectory, "docs");
|
|
3214
3323
|
}
|
|
3215
3324
|
if (typeof configured !== "string") {
|
|
3216
|
-
throw new
|
|
3325
|
+
throw new TypeError(
|
|
3217
3326
|
`docs plugin option 'root' must be a non-empty string path, ` +
|
|
3218
3327
|
`got ${JSON.stringify(configured)}.`,
|
|
3219
3328
|
);
|
|
@@ -3264,9 +3373,11 @@ export function unversionedNote(
|
|
|
3264
3373
|
if (!existsSync(root)) {
|
|
3265
3374
|
return [missingRootNote(root, lake)];
|
|
3266
3375
|
}
|
|
3267
|
-
const probe = spawnSync(
|
|
3268
|
-
|
|
3269
|
-
|
|
3376
|
+
const probe = spawnSync(
|
|
3377
|
+
"git", // NOSONAR: portable git lookup; cwd is trusted local config.
|
|
3378
|
+
["rev-parse", "--show-toplevel"],
|
|
3379
|
+
{ cwd: root },
|
|
3380
|
+
);
|
|
3270
3381
|
if (probe.status === 0) {
|
|
3271
3382
|
return [];
|
|
3272
3383
|
}
|