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.js CHANGED
@@ -1,79 +1,148 @@
1
- import {
2
- runProcess
3
- } from "./chunk-EEETAETA.js";
4
- import {
5
- ArticleNotFoundError,
6
- CollectionExistsError,
7
- CollectionNotFoundError,
8
- DiffwikiError,
9
- InvalidTargetError,
10
- LONG_TIMEOUT_MS,
11
- NATIVE_ENGINE,
12
- NoDefaultCollectionError,
13
- PluginError,
14
- collectionDir,
15
- collectionsDir,
16
- configPath,
17
- createCollection,
18
- deregisterPlugin,
19
- emitPluginEvent,
20
- findCollection,
21
- findEnabledPlugin,
22
- findPluginRecord,
23
- getLogger,
24
- invokePlugin,
25
- listCollections,
26
- listEnabledPlugins,
27
- listSearchModes,
28
- loadSearchProvider,
29
- pluginsDir,
30
- pluginsRegistryPath,
31
- readPluginRegistry,
32
- readRegistry,
33
- registerCollection,
34
- registryPath,
35
- removeCollection,
36
- resolveHome,
37
- resolveLogLevel,
38
- unregisterCollection,
39
- upsertPlugin,
40
- writePluginRegistry,
41
- writeRegistry
42
- } from "./chunk-AZ6SZH6P.js";
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 fs.readFile(configPath(), "utf8"));
131
+ return JSON.parse(await fs3.readFile(configPath(), "utf8"));
52
132
  } catch (err) {
53
- if (isMissing(err)) return {};
133
+ if (isMissing2(err)) return {};
54
134
  throw err;
55
135
  }
56
136
  }
57
137
  async function writeConfig(config) {
58
- await fs.mkdir(resolveHome(), { recursive: true });
59
- await fs.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
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
- try {
123
- const { data, content } = matter(raw, MATTER_OPTS);
124
- const title = typeof data.title === "string" ? data.title : "";
125
- const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
126
- const created = typeof data.created === "string" ? data.created : void 0;
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 fs2 from "fs/promises";
141
- import path from "path";
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 = path.dirname(file);
182
- const base = slugify(path.basename(file));
183
- const rel = dir === "." ? `${base}.md` : path.join(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 = path.join(dir, `${slugify(title)}.md`);
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 fs2.mkdir(dir, { recursive: true });
194
- await fs2.writeFile(filePath, serializeArticle({ title, tags: opts.tags, created: now, updated: now, body }), "utf8");
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 = path.join(dir, ensureMdPath(file));
241
+ const filePath = path2.join(dir, ensureMdPath(file));
202
242
  try {
203
- return { filePath, dir, raw: await fs2.readFile(filePath, "utf8"), collection };
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, dir, raw, collection } = await loadArticle(target);
250
+ const { filePath, raw, collection } = await loadArticle(target);
211
251
  const parsed = parseArticle(raw);
212
- await fs2.writeFile(
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, dir, raw, collection } = await loadArticle(target);
266
+ const { filePath, raw, collection } = await loadArticle(target);
228
267
  const parsed = parseArticle(raw);
229
268
  const tags = fn(parsed.tags);
230
- await fs2.writeFile(
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, dir, collection } = await loadArticle(target);
256
- await fs2.rm(filePath);
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 fs3 from "fs/promises";
263
- import path2 from "path";
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 path2.basename(stdout.trim());
309
+ return path3.basename(stdout.trim());
273
310
  } catch {
274
- return path2.basename(path2.resolve(cwd));
311
+ return path3.basename(path3.resolve(cwd));
275
312
  }
276
313
  }
277
314
  async function readRepoConfig(cwd) {
278
315
  try {
279
- const raw = await fs3.readFile(path2.join(cwd, REPO_CONFIG_FILE), "utf8");
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 ?? path2.basename(path2.resolve(cwd)),
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 = path2.resolve(opts.cwd);
329
+ const cwd = path3.resolve(opts.cwd);
293
330
  const wikiPath = opts.wikiPath ?? "wiki";
294
- const absWiki = path2.join(cwd, wikiPath);
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 fs3.mkdir(absWiki, { recursive: true });
346
+ await fs5.mkdir(absWiki, { recursive: true });
310
347
  const config = { collection: name, path: wikiPath };
311
- await fs3.writeFile(path2.join(cwd, REPO_CONFIG_FILE), stringifyYaml2(config), "utf8");
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 fs4 from "fs/promises";
324
- import path3 from "path";
325
- var log = getLogger("query");
326
- async function collectMarkdownFiles(dir) {
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 = collections.find((c) => c.name === collection);
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 docs = [];
371
+ const needle = term.toLowerCase();
372
+ const hits = [];
380
373
  for (const entry of targets) {
381
- for (const full of await collectMarkdownFiles(entry.path)) {
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 runProcess("npm", ["uninstall", name, "--prefix", dir]);
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
- await deregisterPlugin(name);
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 fs6.access(target);
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 fs7 from "fs/promises";
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 fs7.readdir(dir, { withFileTypes: true });
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 fs7.readdir(dir);
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 fs7.open(absPath, "r");
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 fs7.access(candidate);
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 fs7.readFile(abs, "utf8"));
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
  };