diffwiki-core 0.4.0-rc.202607221918.a124bf3 → 0.4.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/dist/index.cjs +167 -1147
- package/dist/index.d.cts +14 -389
- package/dist/index.d.ts +14 -389
- package/dist/index.js +187 -608
- package/package.json +1 -7
- package/dist/chunk-AZ6SZH6P.js +0 -484
- package/dist/chunk-EEETAETA.js +0 -145
- package/dist/events-H3D7FT4O.js +0 -6
- package/dist/plugin.cjs +0 -175
- package/dist/plugin.d.cts +0 -229
- package/dist/plugin.d.ts +0 -229
- package/dist/plugin.js +0 -14
package/dist/index.js
CHANGED
|
@@ -1,79 +1,148 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var DiffwikiError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = new.target.name;
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var CollectionExistsError = class extends DiffwikiError {
|
|
9
|
+
constructor(name) {
|
|
10
|
+
super(`collection "${name}" already exists`);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var CollectionNotFoundError = class extends DiffwikiError {
|
|
14
|
+
constructor(name) {
|
|
15
|
+
super(`collection "${name}" not found`);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var NoDefaultCollectionError = class extends DiffwikiError {
|
|
19
|
+
constructor() {
|
|
20
|
+
super("no default collection set (use: diffwiki config-set defaultCollection <name>)");
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var ArticleNotFoundError = class extends DiffwikiError {
|
|
24
|
+
constructor(target) {
|
|
25
|
+
super(`article "${target}" not found`);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var InvalidTargetError = class extends DiffwikiError {
|
|
29
|
+
constructor(target, expected) {
|
|
30
|
+
super(`invalid target "${target}" \u2014 ${expected}`);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// src/paths.ts
|
|
35
|
+
import os from "os";
|
|
36
|
+
import path from "path";
|
|
37
|
+
function resolveHome() {
|
|
38
|
+
const override = process.env.DIFFWIKI_HOME;
|
|
39
|
+
return override && override.length > 0 ? override : path.join(os.homedir(), ".diffwiki");
|
|
40
|
+
}
|
|
41
|
+
function registryPath() {
|
|
42
|
+
return path.join(resolveHome(), "registry.json");
|
|
43
|
+
}
|
|
44
|
+
function configPath() {
|
|
45
|
+
return path.join(resolveHome(), "config.json");
|
|
46
|
+
}
|
|
47
|
+
function collectionsDir() {
|
|
48
|
+
return path.join(resolveHome(), "collections");
|
|
49
|
+
}
|
|
50
|
+
function collectionDir(name) {
|
|
51
|
+
return path.join(collectionsDir(), name);
|
|
52
|
+
}
|
|
43
53
|
|
|
44
54
|
// src/config.ts
|
|
55
|
+
import fs3 from "fs/promises";
|
|
56
|
+
|
|
57
|
+
// src/collections.ts
|
|
58
|
+
import fs2 from "fs/promises";
|
|
59
|
+
|
|
60
|
+
// src/registry.ts
|
|
45
61
|
import fs from "fs/promises";
|
|
46
62
|
function isMissing(err) {
|
|
47
63
|
return err?.code === "ENOENT";
|
|
48
64
|
}
|
|
65
|
+
async function readRegistry() {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(await fs.readFile(registryPath(), "utf8"));
|
|
68
|
+
return { version: parsed.version ?? 1, collections: parsed.collections ?? [] };
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (isMissing(err)) return { version: 1, collections: [] };
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function writeRegistry(registry) {
|
|
75
|
+
await fs.mkdir(resolveHome(), { recursive: true });
|
|
76
|
+
await fs.writeFile(registryPath(), `${JSON.stringify(registry, null, 2)}
|
|
77
|
+
`, "utf8");
|
|
78
|
+
}
|
|
79
|
+
async function registerCollection(entry) {
|
|
80
|
+
const registry = await readRegistry();
|
|
81
|
+
const index = registry.collections.findIndex((c) => c.name === entry.name);
|
|
82
|
+
if (index >= 0) {
|
|
83
|
+
registry.collections[index] = entry;
|
|
84
|
+
} else {
|
|
85
|
+
registry.collections.push(entry);
|
|
86
|
+
}
|
|
87
|
+
await writeRegistry(registry);
|
|
88
|
+
}
|
|
89
|
+
async function unregisterCollection(name) {
|
|
90
|
+
const registry = await readRegistry();
|
|
91
|
+
registry.collections = registry.collections.filter((c) => c.name !== name);
|
|
92
|
+
await writeRegistry(registry);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/collections.ts
|
|
96
|
+
async function listCollections() {
|
|
97
|
+
return (await readRegistry()).collections;
|
|
98
|
+
}
|
|
99
|
+
async function findCollection(name) {
|
|
100
|
+
return (await readRegistry()).collections.find((c) => c.name === name);
|
|
101
|
+
}
|
|
102
|
+
async function createCollection(name) {
|
|
103
|
+
if (await findCollection(name)) throw new CollectionExistsError(name);
|
|
104
|
+
const dir = collectionDir(name);
|
|
105
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
106
|
+
const entry = {
|
|
107
|
+
name,
|
|
108
|
+
type: "global",
|
|
109
|
+
path: dir,
|
|
110
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
111
|
+
};
|
|
112
|
+
await registerCollection(entry);
|
|
113
|
+
return entry;
|
|
114
|
+
}
|
|
115
|
+
async function removeCollection(name) {
|
|
116
|
+
const entry = await findCollection(name);
|
|
117
|
+
if (!entry) throw new CollectionNotFoundError(name);
|
|
118
|
+
await unregisterCollection(name);
|
|
119
|
+
if (entry.type === "global") {
|
|
120
|
+
await fs2.rm(entry.path, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
return entry;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/config.ts
|
|
126
|
+
function isMissing2(err) {
|
|
127
|
+
return err?.code === "ENOENT";
|
|
128
|
+
}
|
|
49
129
|
async function readConfig() {
|
|
50
130
|
try {
|
|
51
|
-
return JSON.parse(await
|
|
131
|
+
return JSON.parse(await fs3.readFile(configPath(), "utf8"));
|
|
52
132
|
} catch (err) {
|
|
53
|
-
if (
|
|
133
|
+
if (isMissing2(err)) return {};
|
|
54
134
|
throw err;
|
|
55
135
|
}
|
|
56
136
|
}
|
|
57
137
|
async function writeConfig(config) {
|
|
58
|
-
await
|
|
59
|
-
await
|
|
138
|
+
await fs3.mkdir(resolveHome(), { recursive: true });
|
|
139
|
+
await fs3.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
|
|
60
140
|
`, "utf8");
|
|
61
141
|
}
|
|
62
142
|
async function setConfigValue(key, value) {
|
|
63
143
|
if (key === "defaultCollection" && !await findCollection(value)) {
|
|
64
144
|
throw new CollectionNotFoundError(value);
|
|
65
145
|
}
|
|
66
|
-
if (key === "defaultSearch") {
|
|
67
|
-
const engine = value.split(":")[0];
|
|
68
|
-
if (engine !== NATIVE_ENGINE) {
|
|
69
|
-
const enabled = await listEnabledPlugins("search");
|
|
70
|
-
if (!enabled.some((p) => p.name === engine)) {
|
|
71
|
-
console.warn(
|
|
72
|
-
`! defaultSearch "${value}" names an engine that is not installed/enabled \u2014 native will be used until it is`
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
146
|
const config = await readConfig();
|
|
78
147
|
config[key] = value;
|
|
79
148
|
await writeConfig(config);
|
|
@@ -101,33 +170,12 @@ var YAML_ENGINE = {
|
|
|
101
170
|
stringify: (data) => stringifyYaml(data)
|
|
102
171
|
};
|
|
103
172
|
var MATTER_OPTS = { engines: { yaml: YAML_ENGINE } };
|
|
104
|
-
function parseFallback(raw) {
|
|
105
|
-
const fenceRe = /^---[ \t]*\n([\s\S]*?)\n---[ \t]*\n/;
|
|
106
|
-
const fenceMatch = fenceRe.exec(raw);
|
|
107
|
-
let title = "";
|
|
108
|
-
let body;
|
|
109
|
-
if (fenceMatch) {
|
|
110
|
-
const inner = fenceMatch[1];
|
|
111
|
-
const titleMatch = /^title:\s*(.+)$/m.exec(inner);
|
|
112
|
-
if (titleMatch) {
|
|
113
|
-
title = titleMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
114
|
-
}
|
|
115
|
-
body = raw.slice(fenceMatch[0].length);
|
|
116
|
-
} else {
|
|
117
|
-
body = raw;
|
|
118
|
-
}
|
|
119
|
-
return { title, tags: [], body: body.replace(/^\n+/, "") };
|
|
120
|
-
}
|
|
121
173
|
function parseArticle(raw) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return { title, tags, created, body: content.replace(/^\n+/, "") };
|
|
128
|
-
} catch {
|
|
129
|
-
return parseFallback(raw);
|
|
130
|
-
}
|
|
174
|
+
const { data, content } = matter(raw, MATTER_OPTS);
|
|
175
|
+
const title = typeof data.title === "string" ? data.title : "";
|
|
176
|
+
const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
|
|
177
|
+
const created = typeof data.created === "string" ? data.created : void 0;
|
|
178
|
+
return { title, tags, created, body: content.replace(/^\n+/, "") };
|
|
131
179
|
}
|
|
132
180
|
function serializeArticle(article) {
|
|
133
181
|
const front = { title: article.title, tags: article.tags };
|
|
@@ -137,15 +185,8 @@ function serializeArticle(article) {
|
|
|
137
185
|
}
|
|
138
186
|
|
|
139
187
|
// src/articles.ts
|
|
140
|
-
import
|
|
141
|
-
import
|
|
142
|
-
async function fireHook(event, collection, relPath) {
|
|
143
|
-
try {
|
|
144
|
-
const { emitPluginEvent: emitPluginEvent2 } = await import("./events-H3D7FT4O.js");
|
|
145
|
-
await emitPluginEvent2(event, collection, relPath);
|
|
146
|
-
} catch {
|
|
147
|
-
}
|
|
148
|
-
}
|
|
188
|
+
import fs4 from "fs/promises";
|
|
189
|
+
import path2 from "path";
|
|
149
190
|
function parseAddTarget(target) {
|
|
150
191
|
const idx = target.indexOf(":");
|
|
151
192
|
if (idx === -1) {
|
|
@@ -178,38 +219,37 @@ async function resolveCollectionDir(name) {
|
|
|
178
219
|
}
|
|
179
220
|
function ensureMdPath(file) {
|
|
180
221
|
if (/\.mdx?$/.test(file)) return file;
|
|
181
|
-
const dir =
|
|
182
|
-
const base = slugify(
|
|
183
|
-
const rel = dir === "." ? `${base}.md` :
|
|
222
|
+
const dir = path2.dirname(file);
|
|
223
|
+
const base = slugify(path2.basename(file));
|
|
224
|
+
const rel = dir === "." ? `${base}.md` : path2.join(dir, `${base}.md`);
|
|
184
225
|
return rel;
|
|
185
226
|
}
|
|
186
227
|
async function createArticle(opts) {
|
|
187
228
|
const { collection, title } = parseAddTarget(opts.target);
|
|
188
229
|
const { name, dir } = await resolveCollectionDir(collection);
|
|
189
|
-
const filePath =
|
|
230
|
+
const filePath = path2.join(dir, `${slugify(title)}.md`);
|
|
190
231
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
191
232
|
const body = opts.body ?? `# ${title}
|
|
192
233
|
`;
|
|
193
|
-
await
|
|
194
|
-
await
|
|
195
|
-
void fireHook("article-created", name, path.relative(dir, filePath));
|
|
234
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
235
|
+
await fs4.writeFile(filePath, serializeArticle({ title, tags: opts.tags, created: now, updated: now, body }), "utf8");
|
|
196
236
|
return { title, tags: opts.tags, path: filePath, collection: name };
|
|
197
237
|
}
|
|
198
238
|
async function loadArticle(target) {
|
|
199
239
|
const { collection, file } = parsePathTarget(target);
|
|
200
240
|
const { dir } = await resolveCollectionDir(collection);
|
|
201
|
-
const filePath =
|
|
241
|
+
const filePath = path2.join(dir, ensureMdPath(file));
|
|
202
242
|
try {
|
|
203
|
-
return { filePath,
|
|
243
|
+
return { filePath, raw: await fs4.readFile(filePath, "utf8"), collection };
|
|
204
244
|
} catch (err) {
|
|
205
245
|
if (err.code === "ENOENT") throw new ArticleNotFoundError(target);
|
|
206
246
|
throw err;
|
|
207
247
|
}
|
|
208
248
|
}
|
|
209
249
|
async function updateArticleBody(target, body) {
|
|
210
|
-
const { filePath,
|
|
250
|
+
const { filePath, raw, collection } = await loadArticle(target);
|
|
211
251
|
const parsed = parseArticle(raw);
|
|
212
|
-
await
|
|
252
|
+
await fs4.writeFile(
|
|
213
253
|
filePath,
|
|
214
254
|
serializeArticle({
|
|
215
255
|
title: parsed.title,
|
|
@@ -220,14 +260,13 @@ async function updateArticleBody(target, body) {
|
|
|
220
260
|
}),
|
|
221
261
|
"utf8"
|
|
222
262
|
);
|
|
223
|
-
void fireHook("article-updated", collection, path.relative(dir, filePath));
|
|
224
263
|
return { title: parsed.title, tags: parsed.tags, path: filePath, collection };
|
|
225
264
|
}
|
|
226
265
|
async function mutateTags(target, fn) {
|
|
227
|
-
const { filePath,
|
|
266
|
+
const { filePath, raw, collection } = await loadArticle(target);
|
|
228
267
|
const parsed = parseArticle(raw);
|
|
229
268
|
const tags = fn(parsed.tags);
|
|
230
|
-
await
|
|
269
|
+
await fs4.writeFile(
|
|
231
270
|
filePath,
|
|
232
271
|
serializeArticle({
|
|
233
272
|
title: parsed.title,
|
|
@@ -238,7 +277,6 @@ async function mutateTags(target, fn) {
|
|
|
238
277
|
}),
|
|
239
278
|
"utf8"
|
|
240
279
|
);
|
|
241
|
-
void fireHook("article-updated", collection, path.relative(dir, filePath));
|
|
242
280
|
return { title: parsed.title, tags, path: filePath, collection };
|
|
243
281
|
}
|
|
244
282
|
function addTags(target, tags) {
|
|
@@ -252,15 +290,14 @@ function setTags(target, tags) {
|
|
|
252
290
|
return mutateTags(target, () => Array.from(new Set(tags)));
|
|
253
291
|
}
|
|
254
292
|
async function removeArticle(target) {
|
|
255
|
-
const { filePath
|
|
256
|
-
await
|
|
257
|
-
void fireHook("article-removed", collection, path.relative(dir, filePath));
|
|
293
|
+
const { filePath } = await loadArticle(target);
|
|
294
|
+
await fs4.rm(filePath);
|
|
258
295
|
return filePath;
|
|
259
296
|
}
|
|
260
297
|
|
|
261
298
|
// src/repo.ts
|
|
262
|
-
import
|
|
263
|
-
import
|
|
299
|
+
import fs5 from "fs/promises";
|
|
300
|
+
import path3 from "path";
|
|
264
301
|
import { execFile } from "child_process";
|
|
265
302
|
import { promisify } from "util";
|
|
266
303
|
import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
|
|
@@ -269,18 +306,18 @@ var REPO_CONFIG_FILE = "diffwiki.yaml";
|
|
|
269
306
|
async function detectRepoName(cwd) {
|
|
270
307
|
try {
|
|
271
308
|
const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
272
|
-
return
|
|
309
|
+
return path3.basename(stdout.trim());
|
|
273
310
|
} catch {
|
|
274
|
-
return
|
|
311
|
+
return path3.basename(path3.resolve(cwd));
|
|
275
312
|
}
|
|
276
313
|
}
|
|
277
314
|
async function readRepoConfig(cwd) {
|
|
278
315
|
try {
|
|
279
|
-
const raw = await
|
|
316
|
+
const raw = await fs5.readFile(path3.join(cwd, REPO_CONFIG_FILE), "utf8");
|
|
280
317
|
const parsed = parseYaml2(raw) ?? {};
|
|
281
318
|
if (!parsed.path) return void 0;
|
|
282
319
|
return {
|
|
283
|
-
collection: parsed.collection ??
|
|
320
|
+
collection: parsed.collection ?? path3.basename(path3.resolve(cwd)),
|
|
284
321
|
path: parsed.path
|
|
285
322
|
};
|
|
286
323
|
} catch (err) {
|
|
@@ -289,9 +326,9 @@ async function readRepoConfig(cwd) {
|
|
|
289
326
|
}
|
|
290
327
|
}
|
|
291
328
|
async function initRepoWiki(opts) {
|
|
292
|
-
const cwd =
|
|
329
|
+
const cwd = path3.resolve(opts.cwd);
|
|
293
330
|
const wikiPath = opts.wikiPath ?? "wiki";
|
|
294
|
-
const absWiki =
|
|
331
|
+
const absWiki = path3.join(cwd, wikiPath);
|
|
295
332
|
let name = opts.collection ?? await detectRepoName(cwd);
|
|
296
333
|
const existing = await findCollection(name);
|
|
297
334
|
if (existing && existing.path !== absWiki) {
|
|
@@ -306,9 +343,9 @@ async function initRepoWiki(opts) {
|
|
|
306
343
|
n += 1;
|
|
307
344
|
}
|
|
308
345
|
}
|
|
309
|
-
await
|
|
346
|
+
await fs5.mkdir(absWiki, { recursive: true });
|
|
310
347
|
const config = { collection: name, path: wikiPath };
|
|
311
|
-
await
|
|
348
|
+
await fs5.writeFile(path3.join(cwd, REPO_CONFIG_FILE), stringifyYaml2(config), "utf8");
|
|
312
349
|
const entry = {
|
|
313
350
|
name,
|
|
314
351
|
type: "repo",
|
|
@@ -320,387 +357,44 @@ async function initRepoWiki(opts) {
|
|
|
320
357
|
}
|
|
321
358
|
|
|
322
359
|
// src/query.ts
|
|
323
|
-
import
|
|
324
|
-
import
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
let entries;
|
|
328
|
-
try {
|
|
329
|
-
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
330
|
-
} catch {
|
|
331
|
-
return [];
|
|
332
|
-
}
|
|
333
|
-
const files = [];
|
|
334
|
-
for (const entry of entries) {
|
|
335
|
-
const full = path3.join(dir, entry.name);
|
|
336
|
-
if (entry.isDirectory()) {
|
|
337
|
-
files.push(...await collectMarkdownFiles(full));
|
|
338
|
-
} else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
|
|
339
|
-
files.push(full);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
return files;
|
|
343
|
-
}
|
|
344
|
-
var K1 = 1.5;
|
|
345
|
-
var B = 0.75;
|
|
346
|
-
var FIELD_WEIGHTS = { title: 3, tags: 2, collection: 1.5, body: 1 };
|
|
347
|
-
function tokenize(text) {
|
|
348
|
-
return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
|
|
349
|
-
}
|
|
350
|
-
function termFreqs(tokens) {
|
|
351
|
-
const freqs = /* @__PURE__ */ new Map();
|
|
352
|
-
for (const t of tokens) {
|
|
353
|
-
freqs.set(t, (freqs.get(t) ?? 0) + 1);
|
|
354
|
-
}
|
|
355
|
-
return freqs;
|
|
356
|
-
}
|
|
357
|
-
function bm25FieldScore(queryTerms, fieldTokens, avgFieldLen, idf) {
|
|
358
|
-
const tf = termFreqs(fieldTokens);
|
|
359
|
-
const dl = fieldTokens.length;
|
|
360
|
-
const avgDl = avgFieldLen || 1;
|
|
361
|
-
let score = 0;
|
|
362
|
-
for (const term of queryTerms) {
|
|
363
|
-
const freq = tf.get(term) ?? 0;
|
|
364
|
-
if (freq === 0) continue;
|
|
365
|
-
const termIdf = idf.get(term) ?? 0;
|
|
366
|
-
const numerator = freq * (K1 + 1);
|
|
367
|
-
const denominator = freq + K1 * (1 - B + B * (dl / avgDl));
|
|
368
|
-
score += termIdf * (numerator / denominator);
|
|
369
|
-
}
|
|
370
|
-
return score;
|
|
371
|
-
}
|
|
372
|
-
async function nativeSearch(term, collections, collection) {
|
|
373
|
-
let targets = collections;
|
|
360
|
+
import fs6 from "fs/promises";
|
|
361
|
+
import path4 from "path";
|
|
362
|
+
async function query(term, collection) {
|
|
363
|
+
let targets;
|
|
374
364
|
if (collection) {
|
|
375
|
-
const one =
|
|
365
|
+
const one = await findCollection(collection);
|
|
376
366
|
if (!one) throw new CollectionNotFoundError(collection);
|
|
377
367
|
targets = [one];
|
|
368
|
+
} else {
|
|
369
|
+
targets = await listCollections();
|
|
378
370
|
}
|
|
379
|
-
const
|
|
371
|
+
const needle = term.toLowerCase();
|
|
372
|
+
const hits = [];
|
|
380
373
|
for (const entry of targets) {
|
|
381
|
-
|
|
382
|
-
const fallbackTitle = path3.basename(full).replace(/\.mdx?$/, "");
|
|
383
|
-
let title = fallbackTitle;
|
|
384
|
-
let tags = [];
|
|
385
|
-
let body = "";
|
|
386
|
-
try {
|
|
387
|
-
const parsed = parseArticle(await fs4.readFile(full, "utf8"));
|
|
388
|
-
if (parsed.title) title = parsed.title;
|
|
389
|
-
tags = parsed.tags;
|
|
390
|
-
body = parsed.body;
|
|
391
|
-
} catch {
|
|
392
|
-
}
|
|
393
|
-
docs.push({
|
|
394
|
-
collectionName: entry.name,
|
|
395
|
-
filePath: full,
|
|
396
|
-
title,
|
|
397
|
-
tags,
|
|
398
|
-
fields: {
|
|
399
|
-
title: tokenize(title),
|
|
400
|
-
tags: tokenize(tags.join(" ")),
|
|
401
|
-
collection: tokenize(entry.name),
|
|
402
|
-
body: tokenize(body)
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
if (!term.trim()) {
|
|
408
|
-
return docs.map((d) => ({
|
|
409
|
-
collection: d.collectionName,
|
|
410
|
-
path: d.filePath,
|
|
411
|
-
title: d.title,
|
|
412
|
-
tags: d.tags
|
|
413
|
-
}));
|
|
414
|
-
}
|
|
415
|
-
const queryTerms = tokenize(term);
|
|
416
|
-
if (queryTerms.length === 0) {
|
|
417
|
-
return docs.map((d) => ({
|
|
418
|
-
collection: d.collectionName,
|
|
419
|
-
path: d.filePath,
|
|
420
|
-
title: d.title,
|
|
421
|
-
tags: d.tags
|
|
422
|
-
}));
|
|
423
|
-
}
|
|
424
|
-
const docTokenSets = docs.map(
|
|
425
|
-
(doc) => /* @__PURE__ */ new Set([...doc.fields.title, ...doc.fields.tags, ...doc.fields.collection, ...doc.fields.body])
|
|
426
|
-
);
|
|
427
|
-
const N = docs.length;
|
|
428
|
-
const idf = /* @__PURE__ */ new Map();
|
|
429
|
-
for (const qt of queryTerms) {
|
|
430
|
-
let docFreq = 0;
|
|
431
|
-
for (const tokenSet of docTokenSets) {
|
|
432
|
-
if (tokenSet.has(qt)) docFreq++;
|
|
433
|
-
}
|
|
434
|
-
idf.set(qt, Math.log((N - docFreq + 0.5) / (docFreq + 0.5) + 1));
|
|
435
|
-
}
|
|
436
|
-
const avgLens = {
|
|
437
|
-
title: docs.reduce((sum, d) => sum + d.fields.title.length, 0) / (N || 1),
|
|
438
|
-
tags: docs.reduce((sum, d) => sum + d.fields.tags.length, 0) / (N || 1),
|
|
439
|
-
collection: docs.reduce((sum, d) => sum + d.fields.collection.length, 0) / (N || 1),
|
|
440
|
-
body: docs.reduce((sum, d) => sum + d.fields.body.length, 0) / (N || 1)
|
|
441
|
-
};
|
|
442
|
-
const scored = [];
|
|
443
|
-
for (const doc of docs) {
|
|
444
|
-
const titleScore = bm25FieldScore(queryTerms, doc.fields.title, avgLens.title, idf) * FIELD_WEIGHTS.title;
|
|
445
|
-
const tagsScore = bm25FieldScore(queryTerms, doc.fields.tags, avgLens.tags, idf) * FIELD_WEIGHTS.tags;
|
|
446
|
-
const collScore = bm25FieldScore(queryTerms, doc.fields.collection, avgLens.collection, idf) * FIELD_WEIGHTS.collection;
|
|
447
|
-
const bodyScore = bm25FieldScore(queryTerms, doc.fields.body, avgLens.body, idf) * FIELD_WEIGHTS.body;
|
|
448
|
-
const total = titleScore + tagsScore + collScore + bodyScore;
|
|
449
|
-
if (total > 0) {
|
|
450
|
-
scored.push({ doc, score: total });
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
scored.sort((a, b) => b.score - a.score);
|
|
454
|
-
return scored.map((s) => ({
|
|
455
|
-
collection: s.doc.collectionName,
|
|
456
|
-
path: s.doc.filePath,
|
|
457
|
-
title: s.doc.title,
|
|
458
|
-
tags: s.doc.tags,
|
|
459
|
-
score: s.score
|
|
460
|
-
}));
|
|
461
|
-
}
|
|
462
|
-
async function resolveDefaultEngine() {
|
|
463
|
-
const config = await readConfig();
|
|
464
|
-
const enabled = await listEnabledPlugins("search");
|
|
465
|
-
const configured = config.defaultSearch?.split(":")[0];
|
|
466
|
-
if (configured && enabled.some((p) => p.name === configured)) return configured;
|
|
467
|
-
if (enabled.length > 0) return enabled[0].name;
|
|
468
|
-
return NATIVE_ENGINE;
|
|
469
|
-
}
|
|
470
|
-
async function resolveDefaultSearchMode() {
|
|
471
|
-
const config = await readConfig();
|
|
472
|
-
const enabled = await listEnabledPlugins("search");
|
|
473
|
-
const configured = config.defaultSearch?.split(":")[0];
|
|
474
|
-
const configuredType = config.defaultSearch?.split(":")[1];
|
|
475
|
-
let engine = NATIVE_ENGINE;
|
|
476
|
-
if (configured && enabled.some((p) => p.name === configured)) engine = configured;
|
|
477
|
-
else if (enabled.length > 0) engine = enabled[0].name;
|
|
478
|
-
if (engine === NATIVE_ENGINE) return { engine, type: "basic" };
|
|
479
|
-
const record = enabled.find((p) => p.name === engine);
|
|
480
|
-
const searchTypes = record?.capabilities?.searchTypes ?? [];
|
|
481
|
-
if (searchTypes.length === 0) return { engine: NATIVE_ENGINE, type: "basic" };
|
|
482
|
-
if (configuredType && searchTypes.includes(configuredType)) {
|
|
483
|
-
return { engine, type: configuredType };
|
|
484
|
-
}
|
|
485
|
-
return { engine, type: searchTypes[0] };
|
|
486
|
-
}
|
|
487
|
-
async function search(term, opts) {
|
|
488
|
-
const collections = await listCollections();
|
|
489
|
-
const engine = opts?.engine ?? await resolveDefaultEngine();
|
|
490
|
-
log.debug(`resolved search engine "${engine}"`);
|
|
491
|
-
if (engine === NATIVE_ENGINE) {
|
|
492
|
-
const hits = await nativeSearch(term, collections, opts?.collection);
|
|
493
|
-
return { hits, engine, fellBackToNative: false };
|
|
494
|
-
}
|
|
495
|
-
const provider = await loadSearchProvider(engine);
|
|
496
|
-
if (!provider) {
|
|
497
|
-
const error = `no provider found for engine "${engine}"`;
|
|
498
|
-
log.warn(`engine "${engine}" unavailable, falling back to native: ${error}`);
|
|
499
|
-
const hits = await nativeSearch(term, collections, opts?.collection);
|
|
500
|
-
return { hits, engine, fellBackToNative: true, error };
|
|
501
|
-
}
|
|
502
|
-
try {
|
|
503
|
-
const hits = await provider.handleSearch(term, collections, opts);
|
|
504
|
-
return { hits, engine, fellBackToNative: false };
|
|
505
|
-
} catch (err) {
|
|
506
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
507
|
-
log.warn(`engine "${engine}" failed, falling back to native: ${message}`);
|
|
508
|
-
const hits = await nativeSearch(term, collections, opts?.collection);
|
|
509
|
-
return { hits, engine, fellBackToNative: true, error: message };
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
async function query(term, opts) {
|
|
513
|
-
return (await search(term, opts)).hits;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// src/doctor.ts
|
|
517
|
-
import fs6 from "fs/promises";
|
|
518
|
-
|
|
519
|
-
// src/plugins/manager.ts
|
|
520
|
-
import fs5 from "fs/promises";
|
|
521
|
-
import path4 from "path";
|
|
522
|
-
async function readPackageJson(file) {
|
|
523
|
-
try {
|
|
524
|
-
return JSON.parse(await fs5.readFile(file, "utf8"));
|
|
525
|
-
} catch {
|
|
526
|
-
return null;
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
function binOf(manifest) {
|
|
530
|
-
if (typeof manifest.bin === "string") return manifest.bin;
|
|
531
|
-
if (manifest.bin && typeof manifest.bin === "object") {
|
|
532
|
-
const first = Object.values(manifest.bin)[0];
|
|
533
|
-
return typeof first === "string" ? first : null;
|
|
534
|
-
}
|
|
535
|
-
return null;
|
|
536
|
-
}
|
|
537
|
-
async function pluginManifestDirs(nodeModules) {
|
|
538
|
-
let entries;
|
|
539
|
-
try {
|
|
540
|
-
entries = await fs5.readdir(nodeModules);
|
|
541
|
-
} catch {
|
|
542
|
-
return [];
|
|
543
|
-
}
|
|
544
|
-
const dirs = [];
|
|
545
|
-
for (const entry of entries) {
|
|
546
|
-
if (entry.startsWith(".")) continue;
|
|
547
|
-
if (entry.startsWith("@")) {
|
|
548
|
-
let scoped;
|
|
549
|
-
try {
|
|
550
|
-
scoped = await fs5.readdir(path4.join(nodeModules, entry));
|
|
551
|
-
} catch {
|
|
552
|
-
continue;
|
|
553
|
-
}
|
|
554
|
-
for (const pkg of scoped) {
|
|
555
|
-
if (pkg.startsWith(".")) continue;
|
|
556
|
-
dirs.push(path4.join(nodeModules, entry, pkg));
|
|
557
|
-
}
|
|
558
|
-
} else {
|
|
559
|
-
dirs.push(path4.join(nodeModules, entry));
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
return dirs;
|
|
563
|
-
}
|
|
564
|
-
async function ensurePluginRoot() {
|
|
565
|
-
const dir = pluginsDir();
|
|
566
|
-
await fs5.mkdir(dir, { recursive: true });
|
|
567
|
-
const pkgPath = path4.join(dir, "package.json");
|
|
568
|
-
if (await readPackageJson(pkgPath) === null) {
|
|
569
|
-
const pkg = { name: "diffwiki-plugins", private: true, version: "0.0.0" };
|
|
570
|
-
await fs5.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
571
|
-
`, "utf8");
|
|
572
|
-
}
|
|
573
|
-
return dir;
|
|
574
|
-
}
|
|
575
|
-
async function discoverSearchPackage(dir) {
|
|
576
|
-
const nodeModules = path4.join(dir, "node_modules");
|
|
577
|
-
for (const pkgDir of await pluginManifestDirs(nodeModules)) {
|
|
578
|
-
const manifest = await readPackageJson(path4.join(pkgDir, "package.json"));
|
|
579
|
-
if (!manifest?.name || manifest.diffwiki?.kind !== "search") continue;
|
|
580
|
-
const bin = binOf(manifest);
|
|
581
|
-
if (!bin) continue;
|
|
582
|
-
return { name: manifest.name, version: manifest.version, binAbs: path4.resolve(pkgDir, bin) };
|
|
583
|
-
}
|
|
584
|
-
return null;
|
|
585
|
-
}
|
|
586
|
-
async function describePlugin(command, name) {
|
|
587
|
-
return invokePlugin(command, "describe", {}, { name });
|
|
588
|
-
}
|
|
589
|
-
async function runSetup(command, name) {
|
|
590
|
-
try {
|
|
591
|
-
const collections = await listCollections();
|
|
592
|
-
const result = await invokePlugin(
|
|
593
|
-
command,
|
|
594
|
-
"setup",
|
|
595
|
-
{
|
|
596
|
-
collections: collections.map((c) => ({ name: c.name, path: c.path })),
|
|
597
|
-
embed: false,
|
|
598
|
-
prefetchModels: false
|
|
599
|
-
},
|
|
600
|
-
{ timeoutMs: LONG_TIMEOUT_MS, name }
|
|
601
|
-
);
|
|
602
|
-
return { ready: result?.ready === true, steps: result?.steps ?? [] };
|
|
603
|
-
} catch (err) {
|
|
604
|
-
return { ready: false, steps: [{ step: "setup", ok: false, message: err.message }] };
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
async function installPlugin(spec, opts) {
|
|
608
|
-
const dir = await ensurePluginRoot();
|
|
609
|
-
try {
|
|
610
|
-
await runProcess("npm", ["install", spec, "--prefix", dir]);
|
|
611
|
-
} catch (err) {
|
|
612
|
-
throw new PluginError(`npm install ${spec} failed: ${err.message}`);
|
|
613
|
-
}
|
|
614
|
-
const discovered = await discoverSearchPackage(dir);
|
|
615
|
-
if (!discovered) {
|
|
616
|
-
throw new PluginError(
|
|
617
|
-
`no diffwiki search plugin found in "${spec}" (need package.json diffwiki.kind === "search" + bin)`
|
|
618
|
-
);
|
|
619
|
-
}
|
|
620
|
-
const command = ["node", discovered.binAbs];
|
|
621
|
-
let capabilities;
|
|
622
|
-
try {
|
|
623
|
-
const desc = await describePlugin(command, discovered.name);
|
|
624
|
-
if (desc.kind !== "search") {
|
|
625
|
-
throw new PluginError(`plugin "${discovered.name}" reported kind "${desc.kind}", expected "search"`);
|
|
626
|
-
}
|
|
627
|
-
capabilities = desc.capabilities;
|
|
628
|
-
} catch (err) {
|
|
629
|
-
if (err instanceof PluginError) throw err;
|
|
630
|
-
throw new PluginError(`plugin "${discovered.name}" failed describe: ${err.message}`);
|
|
631
|
-
}
|
|
632
|
-
const record = {
|
|
633
|
-
name: discovered.name,
|
|
634
|
-
kind: "search",
|
|
635
|
-
command,
|
|
636
|
-
version: discovered.version,
|
|
637
|
-
capabilities,
|
|
638
|
-
enabled: true,
|
|
639
|
-
managed: true
|
|
640
|
-
};
|
|
641
|
-
await upsertPlugin(record);
|
|
642
|
-
const setup = opts?.setup !== false ? await runSetup(command, discovered.name) : void 0;
|
|
643
|
-
return { record, setup };
|
|
644
|
-
}
|
|
645
|
-
async function registerPlugin(name, command) {
|
|
646
|
-
if (command.length === 0) throw new PluginError(`register "${name}": --command must not be empty`);
|
|
647
|
-
let capabilities;
|
|
648
|
-
try {
|
|
649
|
-
const desc = await describePlugin(command, name);
|
|
650
|
-
if (desc.kind !== "search") {
|
|
651
|
-
throw new PluginError(`plugin "${name}" reported kind "${desc.kind}", expected "search"`);
|
|
652
|
-
}
|
|
653
|
-
capabilities = desc.capabilities;
|
|
654
|
-
} catch (err) {
|
|
655
|
-
if (err instanceof PluginError) throw err;
|
|
656
|
-
throw new PluginError(`plugin "${name}" did not respond to describe (command: ${command.join(" ")})`);
|
|
657
|
-
}
|
|
658
|
-
const record = {
|
|
659
|
-
name,
|
|
660
|
-
kind: "search",
|
|
661
|
-
command,
|
|
662
|
-
capabilities,
|
|
663
|
-
enabled: true,
|
|
664
|
-
managed: false
|
|
665
|
-
};
|
|
666
|
-
await upsertPlugin(record);
|
|
667
|
-
await runSetup(command, name);
|
|
668
|
-
return record;
|
|
669
|
-
}
|
|
670
|
-
async function removePlugin(name) {
|
|
671
|
-
const record = (await readPluginRegistry()).plugins.find((p) => p.name === name);
|
|
672
|
-
if (!record) throw new PluginError(`plugin "${name}" is not registered`);
|
|
673
|
-
if (record.managed) {
|
|
674
|
-
const dir = await ensurePluginRoot();
|
|
374
|
+
let files;
|
|
675
375
|
try {
|
|
676
|
-
await
|
|
376
|
+
files = await fs6.readdir(entry.path);
|
|
677
377
|
} catch {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
for (const file of files) {
|
|
381
|
+
if (!/\.mdx?$/.test(file)) continue;
|
|
382
|
+
const full = path4.join(entry.path, file);
|
|
383
|
+
const parsed = parseArticle(await fs6.readFile(full, "utf8"));
|
|
384
|
+
const haystack = [parsed.title, file, ...parsed.tags].join(" ").toLowerCase();
|
|
385
|
+
if (!needle || haystack.includes(needle)) {
|
|
386
|
+
hits.push({ collection: entry.name, path: full, title: parsed.title, tags: parsed.tags });
|
|
387
|
+
}
|
|
678
388
|
}
|
|
679
389
|
}
|
|
680
|
-
|
|
681
|
-
}
|
|
682
|
-
async function probeDescribable(command, name) {
|
|
683
|
-
try {
|
|
684
|
-
await describePlugin(command, name);
|
|
685
|
-
return true;
|
|
686
|
-
} catch {
|
|
687
|
-
return false;
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
async function listPlugins() {
|
|
691
|
-
const registry = await readPluginRegistry();
|
|
692
|
-
const out = [];
|
|
693
|
-
for (const record of registry.plugins) {
|
|
694
|
-
const spawnable = record.enabled ? await probeDescribable(record.command, record.name) : false;
|
|
695
|
-
out.push({ ...record, spawnable });
|
|
696
|
-
}
|
|
697
|
-
return out;
|
|
390
|
+
return hits;
|
|
698
391
|
}
|
|
699
392
|
|
|
700
393
|
// src/doctor.ts
|
|
394
|
+
import fs7 from "fs/promises";
|
|
701
395
|
async function exists(target) {
|
|
702
396
|
try {
|
|
703
|
-
await
|
|
397
|
+
await fs7.access(target);
|
|
704
398
|
return true;
|
|
705
399
|
} catch {
|
|
706
400
|
return false;
|
|
@@ -733,80 +427,11 @@ async function doctor() {
|
|
|
733
427
|
} else {
|
|
734
428
|
out.push({ level: "warn", message: "no default collection set" });
|
|
735
429
|
}
|
|
736
|
-
await appendPluginDiagnostics(out, config);
|
|
737
430
|
return out;
|
|
738
431
|
}
|
|
739
|
-
function readinessLine(name, health) {
|
|
740
|
-
const r = health.readiness;
|
|
741
|
-
if (!r) {
|
|
742
|
-
return { level: health.ready ? "ok" : "warn", message: `plugin "${name}" ${health.ready ? "ready" : "not ready"}` };
|
|
743
|
-
}
|
|
744
|
-
if (r.firstSearchCost === "model-download") {
|
|
745
|
-
return {
|
|
746
|
-
level: "warn",
|
|
747
|
-
message: `plugin "${name}": ${r.docsIndexed} doc(s) indexed \u2014 first search will download models (~2GB)`
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
if (r.firstSearchCost === "model-load") {
|
|
751
|
-
return {
|
|
752
|
-
level: "warn",
|
|
753
|
-
message: `plugin "${name}": ${r.docsIndexed} doc(s) indexed \u2014 first search loads models (a few seconds)`
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
return { level: "ok", message: `plugin "${name}": ${r.docsIndexed} doc(s) indexed \u2014 first search is instant` };
|
|
757
|
-
}
|
|
758
|
-
async function appendPluginDiagnostics(out, config) {
|
|
759
|
-
let plugins = [];
|
|
760
|
-
try {
|
|
761
|
-
plugins = await listPlugins();
|
|
762
|
-
} catch (err) {
|
|
763
|
-
out.push({ level: "error", message: `failed to read plugin registry: ${err.message}` });
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
if (plugins.length === 0) {
|
|
767
|
-
out.push({ level: "warn", message: "no search plugins installed \u2014 using built-in native search" });
|
|
768
|
-
}
|
|
769
|
-
const collections = await listCollections();
|
|
770
|
-
for (const p of plugins) {
|
|
771
|
-
if (!p.enabled) {
|
|
772
|
-
out.push({ level: "warn", message: `plugin "${p.name}" registered but disabled` });
|
|
773
|
-
continue;
|
|
774
|
-
}
|
|
775
|
-
if (!p.spawnable) {
|
|
776
|
-
out.push({
|
|
777
|
-
level: "error",
|
|
778
|
-
message: `plugin "${p.name}" registered but its command is not spawnable: ${p.command.join(" ")}`
|
|
779
|
-
});
|
|
780
|
-
continue;
|
|
781
|
-
}
|
|
782
|
-
out.push({ level: "ok", message: `plugin "${p.name}" enabled and spawnable` });
|
|
783
|
-
try {
|
|
784
|
-
const provider = await loadSearchProvider(p.name);
|
|
785
|
-
if (!provider) {
|
|
786
|
-
out.push({ level: "error", message: `plugin "${p.name}" failed to load for a health check` });
|
|
787
|
-
continue;
|
|
788
|
-
}
|
|
789
|
-
const health = await provider.health(collections);
|
|
790
|
-
for (const d of health.diagnostics) out.push({ level: d.level, message: `${p.name} \xB7 ${d.message}` });
|
|
791
|
-
out.push(readinessLine(p.name, health));
|
|
792
|
-
} catch (err) {
|
|
793
|
-
out.push({ level: "error", message: `plugin "${p.name}" health check failed: ${err.message}` });
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
if (config.defaultSearch) {
|
|
797
|
-
const engine = config.defaultSearch.split(":")[0];
|
|
798
|
-
const known = engine === NATIVE_ENGINE || plugins.some((p) => p.name === engine && p.enabled);
|
|
799
|
-
if (!known) {
|
|
800
|
-
out.push({
|
|
801
|
-
level: "warn",
|
|
802
|
-
message: `defaultSearch "${config.defaultSearch}" is not installed/enabled \u2014 using native`
|
|
803
|
-
});
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
432
|
|
|
808
433
|
// src/browse.ts
|
|
809
|
-
import
|
|
434
|
+
import fs8 from "fs/promises";
|
|
810
435
|
import path5 from "path";
|
|
811
436
|
var INDEX_RE = /^(index|readme)\.mdx?$/i;
|
|
812
437
|
function buildArticleTree(relPaths) {
|
|
@@ -851,7 +476,7 @@ function buildArticleTree(relPaths) {
|
|
|
851
476
|
async function collectRelPaths(dir, rel = "") {
|
|
852
477
|
let dirents;
|
|
853
478
|
try {
|
|
854
|
-
dirents = await
|
|
479
|
+
dirents = await fs8.readdir(dir, { withFileTypes: true });
|
|
855
480
|
} catch {
|
|
856
481
|
return [];
|
|
857
482
|
}
|
|
@@ -869,7 +494,7 @@ async function collectRelPaths(dir, rel = "") {
|
|
|
869
494
|
}
|
|
870
495
|
async function readdirSafe(dir) {
|
|
871
496
|
try {
|
|
872
|
-
return await
|
|
497
|
+
return await fs8.readdir(dir);
|
|
873
498
|
} catch {
|
|
874
499
|
return [];
|
|
875
500
|
}
|
|
@@ -886,7 +511,7 @@ function pickIndexFile(names) {
|
|
|
886
511
|
async function cheapTitle(absPath) {
|
|
887
512
|
let handle;
|
|
888
513
|
try {
|
|
889
|
-
handle = await
|
|
514
|
+
handle = await fs8.open(absPath, "r");
|
|
890
515
|
const buf = Buffer.alloc(512);
|
|
891
516
|
const { bytesRead } = await handle.read(buf, 0, 512, 0);
|
|
892
517
|
const snippet = buf.subarray(0, bytesRead).toString("utf8");
|
|
@@ -940,7 +565,7 @@ async function readArticle(coll, slug) {
|
|
|
940
565
|
const candidate = path5.resolve(collDir, rel);
|
|
941
566
|
if (!inJail(candidate)) throw new InvalidTargetError(slug, "path traversal detected");
|
|
942
567
|
try {
|
|
943
|
-
await
|
|
568
|
+
await fs8.access(candidate);
|
|
944
569
|
abs = candidate;
|
|
945
570
|
break;
|
|
946
571
|
} catch {
|
|
@@ -954,7 +579,7 @@ async function readArticle(coll, slug) {
|
|
|
954
579
|
}
|
|
955
580
|
}
|
|
956
581
|
if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
|
|
957
|
-
const parsed = parseArticle(await
|
|
582
|
+
const parsed = parseArticle(await fs8.readFile(abs, "utf8"));
|
|
958
583
|
const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
|
|
959
584
|
return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs };
|
|
960
585
|
}
|
|
@@ -1022,36 +647,13 @@ async function renderArticle(body) {
|
|
|
1022
647
|
}).use(rehypeStringify).process(body);
|
|
1023
648
|
return { html: String(file), toc };
|
|
1024
649
|
}
|
|
1025
|
-
|
|
1026
|
-
// src/plugins/catalog.ts
|
|
1027
|
-
var KNOWN_PLUGINS = [
|
|
1028
|
-
{
|
|
1029
|
-
name: "diffwiki-qmd",
|
|
1030
|
-
kind: "search",
|
|
1031
|
-
spec: "diffwiki-qmd",
|
|
1032
|
-
description: "Local hybrid search \u2014 keyword, semantic, and reranked hybrid via the qmd CLI",
|
|
1033
|
-
requires: "qmd (npm i -g @tobilu/qmd)"
|
|
1034
|
-
},
|
|
1035
|
-
{
|
|
1036
|
-
name: "diffwiki-ripgrep",
|
|
1037
|
-
kind: "search",
|
|
1038
|
-
spec: "diffwiki-ripgrep",
|
|
1039
|
-
description: "Instant literal/regex search via ripgrep \u2014 no index, no models",
|
|
1040
|
-
requires: "rg (ripgrep)"
|
|
1041
|
-
}
|
|
1042
|
-
];
|
|
1043
|
-
function listAvailablePlugins() {
|
|
1044
|
-
return [...KNOWN_PLUGINS];
|
|
1045
|
-
}
|
|
1046
650
|
export {
|
|
1047
651
|
ArticleNotFoundError,
|
|
1048
652
|
CollectionExistsError,
|
|
1049
653
|
CollectionNotFoundError,
|
|
1050
654
|
DiffwikiError,
|
|
1051
655
|
InvalidTargetError,
|
|
1052
|
-
NATIVE_ENGINE,
|
|
1053
656
|
NoDefaultCollectionError,
|
|
1054
|
-
PluginError,
|
|
1055
657
|
REPO_CONFIG_FILE,
|
|
1056
658
|
addTags,
|
|
1057
659
|
buildArticleTree,
|
|
@@ -1060,56 +662,33 @@ export {
|
|
|
1060
662
|
configPath,
|
|
1061
663
|
createArticle,
|
|
1062
664
|
createCollection,
|
|
1063
|
-
deregisterPlugin,
|
|
1064
665
|
doctor,
|
|
1065
|
-
emitPluginEvent,
|
|
1066
|
-
ensurePluginRoot,
|
|
1067
666
|
findCollection,
|
|
1068
|
-
findEnabledPlugin,
|
|
1069
|
-
findPluginRecord,
|
|
1070
667
|
getConfigValue,
|
|
1071
|
-
getLogger,
|
|
1072
668
|
initRepoWiki,
|
|
1073
|
-
installPlugin,
|
|
1074
669
|
listArticleTree,
|
|
1075
|
-
listAvailablePlugins,
|
|
1076
670
|
listCollections,
|
|
1077
|
-
listEnabledPlugins,
|
|
1078
|
-
listPlugins,
|
|
1079
|
-
listSearchModes,
|
|
1080
|
-
loadSearchProvider,
|
|
1081
|
-
nativeSearch,
|
|
1082
671
|
parseAddTarget,
|
|
1083
672
|
parseArticle,
|
|
1084
673
|
parsePathTarget,
|
|
1085
|
-
pluginsDir,
|
|
1086
|
-
pluginsRegistryPath,
|
|
1087
674
|
query,
|
|
1088
675
|
readArticle,
|
|
1089
676
|
readConfig,
|
|
1090
|
-
readPluginRegistry,
|
|
1091
677
|
readRegistry,
|
|
1092
678
|
readRepoConfig,
|
|
1093
679
|
registerCollection,
|
|
1094
|
-
registerPlugin,
|
|
1095
680
|
registryPath,
|
|
1096
681
|
removeArticle,
|
|
1097
682
|
removeCollection,
|
|
1098
|
-
removePlugin,
|
|
1099
683
|
removeTags,
|
|
1100
684
|
renderArticle,
|
|
1101
|
-
resolveDefaultSearchMode,
|
|
1102
685
|
resolveHome,
|
|
1103
|
-
resolveLogLevel,
|
|
1104
|
-
search,
|
|
1105
686
|
serializeArticle,
|
|
1106
687
|
setConfigValue,
|
|
1107
688
|
setTags,
|
|
1108
689
|
slugify,
|
|
1109
690
|
unregisterCollection,
|
|
1110
691
|
updateArticleBody,
|
|
1111
|
-
upsertPlugin,
|
|
1112
692
|
writeConfig,
|
|
1113
|
-
writePluginRegistry,
|
|
1114
693
|
writeRegistry
|
|
1115
694
|
};
|