diffwiki-core 0.3.0 → 0.4.0-rc.202607220518.0ade9d3

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 CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,87 +30,57 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
30
- // src/index.ts
31
- var index_exports = {};
32
- __export(index_exports, {
33
- ArticleNotFoundError: () => ArticleNotFoundError,
34
- CollectionExistsError: () => CollectionExistsError,
35
- CollectionNotFoundError: () => CollectionNotFoundError,
36
- DiffwikiError: () => DiffwikiError,
37
- InvalidTargetError: () => InvalidTargetError,
38
- NoDefaultCollectionError: () => NoDefaultCollectionError,
39
- REPO_CONFIG_FILE: () => REPO_CONFIG_FILE,
40
- addTags: () => addTags,
41
- collectionDir: () => collectionDir,
42
- collectionsDir: () => collectionsDir,
43
- configPath: () => configPath,
44
- createArticle: () => createArticle,
45
- createCollection: () => createCollection,
46
- doctor: () => doctor,
47
- findCollection: () => findCollection,
48
- getConfigValue: () => getConfigValue,
49
- initRepoWiki: () => initRepoWiki,
50
- listCollections: () => listCollections,
51
- parseAddTarget: () => parseAddTarget,
52
- parseArticle: () => parseArticle,
53
- parsePathTarget: () => parsePathTarget,
54
- query: () => query,
55
- readConfig: () => readConfig,
56
- readRegistry: () => readRegistry,
57
- readRepoConfig: () => readRepoConfig,
58
- registerCollection: () => registerCollection,
59
- registryPath: () => registryPath,
60
- removeArticle: () => removeArticle,
61
- removeCollection: () => removeCollection,
62
- removeTags: () => removeTags,
63
- resolveHome: () => resolveHome,
64
- serializeArticle: () => serializeArticle,
65
- setConfigValue: () => setConfigValue,
66
- setTags: () => setTags,
67
- slugify: () => slugify,
68
- unregisterCollection: () => unregisterCollection,
69
- updateArticleBody: () => updateArticleBody,
70
- writeConfig: () => writeConfig,
71
- writeRegistry: () => writeRegistry
33
+ // src/types.ts
34
+ var NATIVE_ENGINE;
35
+ var init_types = __esm({
36
+ "src/types.ts"() {
37
+ "use strict";
38
+ NATIVE_ENGINE = "native";
39
+ }
72
40
  });
73
- module.exports = __toCommonJS(index_exports);
74
41
 
75
42
  // src/errors.ts
76
- var DiffwikiError = class extends Error {
77
- constructor(message) {
78
- super(message);
79
- this.name = new.target.name;
80
- }
81
- };
82
- var CollectionExistsError = class extends DiffwikiError {
83
- constructor(name) {
84
- super(`collection "${name}" already exists`);
85
- }
86
- };
87
- var CollectionNotFoundError = class extends DiffwikiError {
88
- constructor(name) {
89
- super(`collection "${name}" not found`);
90
- }
91
- };
92
- var NoDefaultCollectionError = class extends DiffwikiError {
93
- constructor() {
94
- super("no default collection set (use: diffwiki config-set defaultCollection <name>)");
95
- }
96
- };
97
- var ArticleNotFoundError = class extends DiffwikiError {
98
- constructor(target) {
99
- super(`article "${target}" not found`);
100
- }
101
- };
102
- var InvalidTargetError = class extends DiffwikiError {
103
- constructor(target, expected) {
104
- super(`invalid target "${target}" \u2014 ${expected}`);
43
+ var DiffwikiError, CollectionExistsError, CollectionNotFoundError, NoDefaultCollectionError, ArticleNotFoundError, InvalidTargetError, PluginError;
44
+ var init_errors = __esm({
45
+ "src/errors.ts"() {
46
+ "use strict";
47
+ DiffwikiError = class extends Error {
48
+ constructor(message) {
49
+ super(message);
50
+ this.name = new.target.name;
51
+ }
52
+ };
53
+ CollectionExistsError = class extends DiffwikiError {
54
+ constructor(name) {
55
+ super(`collection "${name}" already exists`);
56
+ }
57
+ };
58
+ CollectionNotFoundError = class extends DiffwikiError {
59
+ constructor(name) {
60
+ super(`collection "${name}" not found`);
61
+ }
62
+ };
63
+ NoDefaultCollectionError = class extends DiffwikiError {
64
+ constructor() {
65
+ super("no default collection set (use: diffwiki config-set defaultCollection <name>)");
66
+ }
67
+ };
68
+ ArticleNotFoundError = class extends DiffwikiError {
69
+ constructor(target) {
70
+ super(`article "${target}" not found`);
71
+ }
72
+ };
73
+ InvalidTargetError = class extends DiffwikiError {
74
+ constructor(target, expected) {
75
+ super(`invalid target "${target}" \u2014 ${expected}`);
76
+ }
77
+ };
78
+ PluginError = class extends DiffwikiError {
79
+ };
105
80
  }
106
- };
81
+ });
107
82
 
108
83
  // src/paths.ts
109
- var import_node_os = __toESM(require("os"), 1);
110
- var import_node_path = __toESM(require("path"), 1);
111
84
  function resolveHome() {
112
85
  const override = process.env.DIFFWIKI_HOME;
113
86
  return override && override.length > 0 ? override : import_node_path.default.join(import_node_os.default.homedir(), ".diffwiki");
@@ -124,15 +97,22 @@ function collectionsDir() {
124
97
  function collectionDir(name) {
125
98
  return import_node_path.default.join(collectionsDir(), name);
126
99
  }
127
-
128
- // src/config.ts
129
- var import_promises3 = __toESM(require("fs/promises"), 1);
130
-
131
- // src/collections.ts
132
- var import_promises2 = __toESM(require("fs/promises"), 1);
100
+ function pluginsDir() {
101
+ return import_node_path.default.join(resolveHome(), "plugins");
102
+ }
103
+ function pluginsRegistryPath() {
104
+ return import_node_path.default.join(resolveHome(), "plugins.json");
105
+ }
106
+ var import_node_os, import_node_path;
107
+ var init_paths = __esm({
108
+ "src/paths.ts"() {
109
+ "use strict";
110
+ import_node_os = __toESM(require("os"), 1);
111
+ import_node_path = __toESM(require("path"), 1);
112
+ }
113
+ });
133
114
 
134
115
  // src/registry.ts
135
- var import_promises = __toESM(require("fs/promises"), 1);
136
116
  function isMissing(err) {
137
117
  return err?.code === "ENOENT";
138
118
  }
@@ -165,8 +145,352 @@ async function unregisterCollection(name) {
165
145
  registry.collections = registry.collections.filter((c) => c.name !== name);
166
146
  await writeRegistry(registry);
167
147
  }
148
+ var import_promises;
149
+ var init_registry = __esm({
150
+ "src/registry.ts"() {
151
+ "use strict";
152
+ import_promises = __toESM(require("fs/promises"), 1);
153
+ init_paths();
154
+ }
155
+ });
156
+
157
+ // src/plugins/registry.ts
158
+ function isMissing2(err) {
159
+ return err?.code === "ENOENT";
160
+ }
161
+ async function readPluginRegistry() {
162
+ try {
163
+ const parsed = JSON.parse(await import_promises2.default.readFile(pluginsRegistryPath(), "utf8"));
164
+ return { version: parsed.version ?? 1, plugins: parsed.plugins ?? [] };
165
+ } catch (err) {
166
+ if (isMissing2(err)) return { version: 1, plugins: [] };
167
+ throw err;
168
+ }
169
+ }
170
+ async function writePluginRegistry(registry) {
171
+ await import_promises2.default.mkdir(resolveHome(), { recursive: true });
172
+ await import_promises2.default.writeFile(pluginsRegistryPath(), `${JSON.stringify(registry, null, 2)}
173
+ `, "utf8");
174
+ }
175
+ async function upsertPlugin(record) {
176
+ const registry = await readPluginRegistry();
177
+ const index = registry.plugins.findIndex((p) => p.name === record.name);
178
+ if (index >= 0) {
179
+ registry.plugins[index] = record;
180
+ } else {
181
+ registry.plugins.push(record);
182
+ }
183
+ await writePluginRegistry(registry);
184
+ }
185
+ async function deregisterPlugin(name) {
186
+ const registry = await readPluginRegistry();
187
+ registry.plugins = registry.plugins.filter((p) => p.name !== name);
188
+ await writePluginRegistry(registry);
189
+ }
190
+ async function listEnabledPlugins(kind) {
191
+ const registry = await readPluginRegistry();
192
+ return registry.plugins.filter((p) => p.enabled && p.kind === kind);
193
+ }
194
+ async function findEnabledPlugin(kind, name) {
195
+ const enabled = await listEnabledPlugins(kind);
196
+ if (name) return enabled.find((p) => p.name === name);
197
+ return enabled[0];
198
+ }
199
+ var import_promises2;
200
+ var init_registry2 = __esm({
201
+ "src/plugins/registry.ts"() {
202
+ "use strict";
203
+ import_promises2 = __toESM(require("fs/promises"), 1);
204
+ init_paths();
205
+ }
206
+ });
207
+
208
+ // src/plugins/client.ts
209
+ function toWireCollections(collections) {
210
+ return collections.map((c) => ({ name: c.name, path: c.path }));
211
+ }
212
+ async function spawnPluginClient(record) {
213
+ const child = (0, import_node_child_process.spawn)(record.command[0], record.command.slice(1), {
214
+ stdio: ["pipe", "pipe", "pipe"]
215
+ });
216
+ const pending = /* @__PURE__ */ new Map();
217
+ let nextId = 1;
218
+ let buffer = "";
219
+ let closed = false;
220
+ const failAll = (err) => {
221
+ for (const [, p] of pending) {
222
+ clearTimeout(p.timer);
223
+ p.reject(err);
224
+ }
225
+ pending.clear();
226
+ };
227
+ const handleLine = (line) => {
228
+ const trimmed = line.trim();
229
+ if (!trimmed) return;
230
+ let msg;
231
+ try {
232
+ msg = JSON.parse(trimmed);
233
+ } catch {
234
+ return;
235
+ }
236
+ if (typeof msg !== "object" || msg === null) return;
237
+ const payload = msg;
238
+ if ("log" in payload && !("id" in payload)) return;
239
+ const id = payload.id;
240
+ if (typeof id !== "number") return;
241
+ const p = pending.get(id);
242
+ if (!p) return;
243
+ pending.delete(id);
244
+ clearTimeout(p.timer);
245
+ if (payload.ok === true) {
246
+ p.resolve(payload.result);
247
+ } else {
248
+ const error = payload.error;
249
+ const message = typeof error?.message === "string" ? error.message : "plugin request failed";
250
+ p.reject(new Error(message));
251
+ }
252
+ };
253
+ child.stdout.setEncoding("utf8");
254
+ child.stdout.on("data", (chunk) => {
255
+ buffer += chunk;
256
+ let newlineIndex = buffer.indexOf("\n");
257
+ while (newlineIndex >= 0) {
258
+ const line = buffer.slice(0, newlineIndex);
259
+ buffer = buffer.slice(newlineIndex + 1);
260
+ handleLine(line);
261
+ newlineIndex = buffer.indexOf("\n");
262
+ }
263
+ });
264
+ child.on("error", (err) => {
265
+ closed = true;
266
+ failAll(err);
267
+ });
268
+ child.on("exit", () => {
269
+ closed = true;
270
+ failAll(new Error("plugin process exited"));
271
+ });
272
+ const request = (op, params) => {
273
+ if (closed) return Promise.reject(new Error("plugin process is not running"));
274
+ return new Promise((resolve, reject) => {
275
+ const id = nextId++;
276
+ const timer = setTimeout(() => {
277
+ pending.delete(id);
278
+ reject(new Error(`plugin request "${op}" timed out`));
279
+ }, REQUEST_TIMEOUT_MS);
280
+ pending.set(id, {
281
+ resolve: (result) => resolve(result),
282
+ reject,
283
+ timer
284
+ });
285
+ child.stdin.write(`${JSON.stringify({ id, op, params })}
286
+ `, (err) => {
287
+ if (!err) return;
288
+ const p = pending.get(id);
289
+ if (!p) return;
290
+ pending.delete(id);
291
+ clearTimeout(p.timer);
292
+ p.reject(err);
293
+ });
294
+ });
295
+ };
296
+ let handshake;
297
+ try {
298
+ handshake = await request("initialize", { protocol: 1, home: resolveHome() });
299
+ if (handshake.kind !== "search") {
300
+ throw new Error(`plugin "${record.name}" reported kind "${handshake.kind}", expected "search"`);
301
+ }
302
+ } catch (err) {
303
+ closed = true;
304
+ child.kill("SIGKILL");
305
+ throw err;
306
+ }
307
+ const capabilities = handshake.capabilities;
308
+ const name = handshake.name;
309
+ const mapHits = (result, collections) => {
310
+ const raw = result?.hits;
311
+ const hits = Array.isArray(raw) ? raw : [];
312
+ const out = [];
313
+ for (const item of hits) {
314
+ if (typeof item !== "object" || item === null) continue;
315
+ const h = item;
316
+ if (typeof h.collection !== "string" || typeof h.relPath !== "string") continue;
317
+ const title = typeof h.title === "string" ? h.title : h.relPath;
318
+ const coll = collections.find((c) => c.name === h.collection);
319
+ const abs = coll ? import_node_path2.default.join(coll.path, h.relPath) : h.relPath;
320
+ out.push({
321
+ collection: h.collection,
322
+ path: abs,
323
+ title,
324
+ tags: Array.isArray(h.tags) ? h.tags.filter((t) => typeof t === "string") : [],
325
+ snippet: typeof h.snippet === "string" ? h.snippet : void 0,
326
+ score: typeof h.score === "number" ? h.score : void 0,
327
+ docid: typeof h.docid === "string" ? h.docid : void 0
328
+ });
329
+ }
330
+ return out;
331
+ };
332
+ const close = async () => {
333
+ if (!closed) {
334
+ try {
335
+ await request("shutdown", {});
336
+ } catch {
337
+ }
338
+ }
339
+ closed = true;
340
+ try {
341
+ child.stdin.end();
342
+ } catch {
343
+ }
344
+ await new Promise((resolve) => {
345
+ const timer = setTimeout(() => {
346
+ child.kill();
347
+ resolve();
348
+ }, SHUTDOWN_GRACE_MS);
349
+ child.once("exit", () => {
350
+ clearTimeout(timer);
351
+ resolve();
352
+ });
353
+ });
354
+ };
355
+ const provider = {
356
+ name,
357
+ capabilities,
358
+ async setup(collections, opts) {
359
+ const result = await request("setup", {
360
+ collections: toWireCollections(collections),
361
+ embed: opts?.embed,
362
+ prefetchModels: opts?.prefetchModels
363
+ });
364
+ return { ready: result?.ready === true };
365
+ },
366
+ async reindex(collections, opts) {
367
+ const result = await request("index", {
368
+ collections: toWireCollections(collections),
369
+ embed: opts?.embed
370
+ });
371
+ return result?.indexed ?? 0;
372
+ },
373
+ async search(term, collections, opts) {
374
+ const result = await request("search", {
375
+ term,
376
+ collections: toWireCollections(collections),
377
+ collection: opts?.collection,
378
+ type: opts?.type,
379
+ limit: opts?.limit
380
+ });
381
+ return mapHits(result, collections);
382
+ },
383
+ async notify(event, collection, relPath, collections) {
384
+ try {
385
+ const entry = collections.find((c) => c.name === collection);
386
+ await request("notify", {
387
+ event,
388
+ collection: entry ? { name: entry.name, path: entry.path } : { name: collection, path: "" },
389
+ relPath,
390
+ collections: toWireCollections(collections)
391
+ });
392
+ } catch {
393
+ }
394
+ },
395
+ async health(collections) {
396
+ const result = await request("health", { collections: toWireCollections(collections) });
397
+ return {
398
+ ready: result?.ready === true,
399
+ readiness: result?.readiness,
400
+ diagnostics: result?.diagnostics ?? []
401
+ };
402
+ },
403
+ close
404
+ };
405
+ return provider;
406
+ }
407
+ var import_node_child_process, import_node_path2, REQUEST_TIMEOUT_MS, SHUTDOWN_GRACE_MS;
408
+ var init_client = __esm({
409
+ "src/plugins/client.ts"() {
410
+ "use strict";
411
+ import_node_child_process = require("child_process");
412
+ import_node_path2 = __toESM(require("path"), 1);
413
+ init_paths();
414
+ REQUEST_TIMEOUT_MS = 3e4;
415
+ SHUTDOWN_GRACE_MS = 500;
416
+ }
417
+ });
418
+
419
+ // src/plugins/loader.ts
420
+ async function loadSearchProvider(name) {
421
+ const record = await findEnabledPlugin("search", name);
422
+ if (!record) return null;
423
+ try {
424
+ return await spawnPluginClient(record);
425
+ } catch {
426
+ return null;
427
+ }
428
+ }
429
+ async function listSearchModes() {
430
+ const modes = [{ plugin: NATIVE_ENGINE, type: "basic", label: "native \xB7 bm25" }];
431
+ const plugins = await listEnabledPlugins("search");
432
+ for (const record of plugins) {
433
+ let provider = null;
434
+ try {
435
+ provider = await spawnPluginClient(record);
436
+ for (const type of provider.capabilities.searchTypes) {
437
+ modes.push({ plugin: record.name, type, label: `${record.name} \xB7 ${type}` });
438
+ }
439
+ } catch {
440
+ } finally {
441
+ if (provider) {
442
+ try {
443
+ await provider.close();
444
+ } catch {
445
+ }
446
+ }
447
+ }
448
+ }
449
+ return modes;
450
+ }
451
+ var init_loader = __esm({
452
+ "src/plugins/loader.ts"() {
453
+ "use strict";
454
+ init_types();
455
+ init_registry2();
456
+ init_client();
457
+ }
458
+ });
459
+
460
+ // src/plugins/events.ts
461
+ var events_exports = {};
462
+ __export(events_exports, {
463
+ emitPluginEvent: () => emitPluginEvent
464
+ });
465
+ async function emitPluginEvent(event, collection, relPath) {
466
+ try {
467
+ const provider = await loadSearchProvider();
468
+ try {
469
+ if (provider?.capabilities.hooks) {
470
+ await provider.notify(event, collection, relPath, await listCollections());
471
+ }
472
+ } finally {
473
+ await provider?.close();
474
+ }
475
+ } catch {
476
+ }
477
+ }
478
+ var init_events = __esm({
479
+ "src/plugins/events.ts"() {
480
+ "use strict";
481
+ init_loader();
482
+ init_collections();
483
+ }
484
+ });
168
485
 
169
486
  // src/collections.ts
487
+ async function fireHook(event, collection) {
488
+ try {
489
+ const { emitPluginEvent: emitPluginEvent2 } = await Promise.resolve().then(() => (init_events(), events_exports));
490
+ await emitPluginEvent2(event, collection);
491
+ } catch {
492
+ }
493
+ }
170
494
  async function listCollections() {
171
495
  return (await readRegistry()).collections;
172
496
  }
@@ -176,7 +500,7 @@ async function findCollection(name) {
176
500
  async function createCollection(name) {
177
501
  if (await findCollection(name)) throw new CollectionExistsError(name);
178
502
  const dir = collectionDir(name);
179
- await import_promises2.default.mkdir(dir, { recursive: true });
503
+ await import_promises3.default.mkdir(dir, { recursive: true });
180
504
  const entry = {
181
505
  name,
182
506
  type: "global",
@@ -184,6 +508,7 @@ async function createCollection(name) {
184
508
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
185
509
  };
186
510
  await registerCollection(entry);
511
+ void fireHook("collection-added", name);
187
512
  return entry;
188
513
  }
189
514
  async function removeCollection(name) {
@@ -191,32 +516,133 @@ async function removeCollection(name) {
191
516
  if (!entry) throw new CollectionNotFoundError(name);
192
517
  await unregisterCollection(name);
193
518
  if (entry.type === "global") {
194
- await import_promises2.default.rm(entry.path, { recursive: true, force: true });
519
+ await import_promises3.default.rm(entry.path, { recursive: true, force: true });
195
520
  }
521
+ void fireHook("collection-removed", name);
196
522
  return entry;
197
523
  }
524
+ var import_promises3;
525
+ var init_collections = __esm({
526
+ "src/collections.ts"() {
527
+ "use strict";
528
+ import_promises3 = __toESM(require("fs/promises"), 1);
529
+ init_paths();
530
+ init_registry();
531
+ init_errors();
532
+ }
533
+ });
534
+
535
+ // src/index.ts
536
+ var index_exports = {};
537
+ __export(index_exports, {
538
+ ArticleNotFoundError: () => ArticleNotFoundError,
539
+ CollectionExistsError: () => CollectionExistsError,
540
+ CollectionNotFoundError: () => CollectionNotFoundError,
541
+ DiffwikiError: () => DiffwikiError,
542
+ InvalidTargetError: () => InvalidTargetError,
543
+ NATIVE_ENGINE: () => NATIVE_ENGINE,
544
+ NoDefaultCollectionError: () => NoDefaultCollectionError,
545
+ PluginError: () => PluginError,
546
+ REPO_CONFIG_FILE: () => REPO_CONFIG_FILE,
547
+ addTags: () => addTags,
548
+ buildArticleTree: () => buildArticleTree,
549
+ collectionDir: () => collectionDir,
550
+ collectionsDir: () => collectionsDir,
551
+ configPath: () => configPath,
552
+ createArticle: () => createArticle,
553
+ createCollection: () => createCollection,
554
+ deregisterPlugin: () => deregisterPlugin,
555
+ doctor: () => doctor,
556
+ emitPluginEvent: () => emitPluginEvent,
557
+ ensurePluginRoot: () => ensurePluginRoot,
558
+ findCollection: () => findCollection,
559
+ findEnabledPlugin: () => findEnabledPlugin,
560
+ getConfigValue: () => getConfigValue,
561
+ initRepoWiki: () => initRepoWiki,
562
+ installPlugin: () => installPlugin,
563
+ listArticleTree: () => listArticleTree,
564
+ listCollections: () => listCollections,
565
+ listEnabledPlugins: () => listEnabledPlugins,
566
+ listPlugins: () => listPlugins,
567
+ listSearchModes: () => listSearchModes,
568
+ loadSearchProvider: () => loadSearchProvider,
569
+ nativeSearch: () => nativeSearch,
570
+ parseAddTarget: () => parseAddTarget,
571
+ parseArticle: () => parseArticle,
572
+ parsePathTarget: () => parsePathTarget,
573
+ pluginsDir: () => pluginsDir,
574
+ pluginsRegistryPath: () => pluginsRegistryPath,
575
+ query: () => query,
576
+ readArticle: () => readArticle,
577
+ readConfig: () => readConfig,
578
+ readPluginRegistry: () => readPluginRegistry,
579
+ readRegistry: () => readRegistry,
580
+ readRepoConfig: () => readRepoConfig,
581
+ registerCollection: () => registerCollection,
582
+ registerPlugin: () => registerPlugin,
583
+ registryPath: () => registryPath,
584
+ removeArticle: () => removeArticle,
585
+ removeCollection: () => removeCollection,
586
+ removePlugin: () => removePlugin,
587
+ removeTags: () => removeTags,
588
+ renderArticle: () => renderArticle,
589
+ resolveDefaultSearchMode: () => resolveDefaultSearchMode,
590
+ resolveHome: () => resolveHome,
591
+ serializeArticle: () => serializeArticle,
592
+ setConfigValue: () => setConfigValue,
593
+ setTags: () => setTags,
594
+ slugify: () => slugify,
595
+ spawnPluginClient: () => spawnPluginClient,
596
+ unregisterCollection: () => unregisterCollection,
597
+ updateArticleBody: () => updateArticleBody,
598
+ upsertPlugin: () => upsertPlugin,
599
+ writeConfig: () => writeConfig,
600
+ writePluginRegistry: () => writePluginRegistry,
601
+ writeRegistry: () => writeRegistry
602
+ });
603
+ module.exports = __toCommonJS(index_exports);
604
+ init_types();
605
+ init_errors();
606
+ init_paths();
198
607
 
199
608
  // src/config.ts
200
- function isMissing2(err) {
609
+ var import_promises4 = __toESM(require("fs/promises"), 1);
610
+ init_paths();
611
+ init_collections();
612
+ init_errors();
613
+ init_registry2();
614
+ init_types();
615
+ function isMissing3(err) {
201
616
  return err?.code === "ENOENT";
202
617
  }
203
618
  async function readConfig() {
204
619
  try {
205
- return JSON.parse(await import_promises3.default.readFile(configPath(), "utf8"));
620
+ return JSON.parse(await import_promises4.default.readFile(configPath(), "utf8"));
206
621
  } catch (err) {
207
- if (isMissing2(err)) return {};
622
+ if (isMissing3(err)) return {};
208
623
  throw err;
209
624
  }
210
625
  }
211
626
  async function writeConfig(config) {
212
- await import_promises3.default.mkdir(resolveHome(), { recursive: true });
213
- await import_promises3.default.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
627
+ await import_promises4.default.mkdir(resolveHome(), { recursive: true });
628
+ await import_promises4.default.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
214
629
  `, "utf8");
215
630
  }
216
631
  async function setConfigValue(key, value) {
217
632
  if (key === "defaultCollection" && !await findCollection(value)) {
218
633
  throw new CollectionNotFoundError(value);
219
634
  }
635
+ if (key === "defaultSearch") {
636
+ const engine = value.split(":")[0];
637
+ if (engine !== NATIVE_ENGINE) {
638
+ const enabled = await listEnabledPlugins("search");
639
+ if (!enabled.some((p) => p.name === engine)) {
640
+ console.warn(
641
+ `! defaultSearch "${value}" names an engine that is not installed/enabled \u2014 native will be used until it is`
642
+ );
643
+ }
644
+ }
645
+ }
220
646
  const config = await readConfig();
221
647
  config[key] = value;
222
648
  await writeConfig(config);
@@ -227,6 +653,9 @@ async function getConfigValue(key) {
227
653
  return typeof value === "string" ? value : void 0;
228
654
  }
229
655
 
656
+ // src/index.ts
657
+ init_registry();
658
+
230
659
  // src/slugify.ts
231
660
  function slugify(input) {
232
661
  const slug = input.toLowerCase().replace(/^#+\s*/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/g, "");
@@ -235,23 +664,65 @@ function slugify(input) {
235
664
 
236
665
  // src/frontmatter.ts
237
666
  var import_gray_matter = __toESM(require("gray-matter"), 1);
667
+ var import_yaml = require("yaml");
668
+ var YAML_ENGINE = {
669
+ parse: (input) => {
670
+ const data = (0, import_yaml.parse)(input);
671
+ return data && typeof data === "object" ? data : {};
672
+ },
673
+ stringify: (data) => (0, import_yaml.stringify)(data)
674
+ };
675
+ var MATTER_OPTS = { engines: { yaml: YAML_ENGINE } };
676
+ function parseFallback(raw) {
677
+ const fenceRe = /^---[ \t]*\n([\s\S]*?)\n---[ \t]*\n/;
678
+ const fenceMatch = fenceRe.exec(raw);
679
+ let title = "";
680
+ let body;
681
+ if (fenceMatch) {
682
+ const inner = fenceMatch[1];
683
+ const titleMatch = /^title:\s*(.+)$/m.exec(inner);
684
+ if (titleMatch) {
685
+ title = titleMatch[1].trim().replace(/^["']|["']$/g, "");
686
+ }
687
+ body = raw.slice(fenceMatch[0].length);
688
+ } else {
689
+ body = raw;
690
+ }
691
+ return { title, tags: [], body: body.replace(/^\n+/, "") };
692
+ }
238
693
  function parseArticle(raw) {
239
- const { data, content } = (0, import_gray_matter.default)(raw);
240
- const title = typeof data.title === "string" ? data.title : "";
241
- const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
242
- const created = typeof data.created === "string" ? data.created : void 0;
243
- return { title, tags, created, body: content.replace(/^\n+/, "") };
694
+ try {
695
+ const { data, content } = (0, import_gray_matter.default)(raw, MATTER_OPTS);
696
+ const title = typeof data.title === "string" ? data.title : "";
697
+ const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
698
+ const created = typeof data.created === "string" ? data.created : void 0;
699
+ return { title, tags, created, body: content.replace(/^\n+/, "") };
700
+ } catch {
701
+ return parseFallback(raw);
702
+ }
244
703
  }
245
704
  function serializeArticle(article) {
246
705
  const front = { title: article.title, tags: article.tags };
247
706
  if (article.created) front.created = article.created;
248
707
  if (article.updated) front.updated = article.updated;
249
- return import_gray_matter.default.stringify(article.body, front);
708
+ return import_gray_matter.default.stringify(article.body, front, MATTER_OPTS);
250
709
  }
251
710
 
711
+ // src/index.ts
712
+ init_collections();
713
+
252
714
  // src/articles.ts
253
- var import_promises4 = __toESM(require("fs/promises"), 1);
254
- var import_node_path2 = __toESM(require("path"), 1);
715
+ var import_promises5 = __toESM(require("fs/promises"), 1);
716
+ var import_node_path3 = __toESM(require("path"), 1);
717
+ init_collections();
718
+ init_errors();
719
+ async function fireHook2(event, collection, relPath) {
720
+ try {
721
+ const { emitPluginEvent: emitPluginEvent2 } = await Promise.resolve().then(() => (init_events(), events_exports));
722
+ await emitPluginEvent2(event, collection, relPath);
723
+ } catch {
724
+ }
725
+ }
255
726
  function parseAddTarget(target) {
256
727
  const idx = target.indexOf(":");
257
728
  if (idx === -1) {
@@ -284,37 +755,38 @@ async function resolveCollectionDir(name) {
284
755
  }
285
756
  function ensureMdPath(file) {
286
757
  if (/\.mdx?$/.test(file)) return file;
287
- const dir = import_node_path2.default.dirname(file);
288
- const base = slugify(import_node_path2.default.basename(file));
289
- const rel = dir === "." ? `${base}.md` : import_node_path2.default.join(dir, `${base}.md`);
758
+ const dir = import_node_path3.default.dirname(file);
759
+ const base = slugify(import_node_path3.default.basename(file));
760
+ const rel = dir === "." ? `${base}.md` : import_node_path3.default.join(dir, `${base}.md`);
290
761
  return rel;
291
762
  }
292
763
  async function createArticle(opts) {
293
764
  const { collection, title } = parseAddTarget(opts.target);
294
765
  const { name, dir } = await resolveCollectionDir(collection);
295
- const filePath = import_node_path2.default.join(dir, `${slugify(title)}.md`);
766
+ const filePath = import_node_path3.default.join(dir, `${slugify(title)}.md`);
296
767
  const now = (/* @__PURE__ */ new Date()).toISOString();
297
768
  const body = opts.body ?? `# ${title}
298
769
  `;
299
- await import_promises4.default.mkdir(dir, { recursive: true });
300
- await import_promises4.default.writeFile(filePath, serializeArticle({ title, tags: opts.tags, created: now, updated: now, body }), "utf8");
770
+ await import_promises5.default.mkdir(dir, { recursive: true });
771
+ await import_promises5.default.writeFile(filePath, serializeArticle({ title, tags: opts.tags, created: now, updated: now, body }), "utf8");
772
+ void fireHook2("article-created", name, import_node_path3.default.relative(dir, filePath));
301
773
  return { title, tags: opts.tags, path: filePath, collection: name };
302
774
  }
303
775
  async function loadArticle(target) {
304
776
  const { collection, file } = parsePathTarget(target);
305
777
  const { dir } = await resolveCollectionDir(collection);
306
- const filePath = import_node_path2.default.join(dir, ensureMdPath(file));
778
+ const filePath = import_node_path3.default.join(dir, ensureMdPath(file));
307
779
  try {
308
- return { filePath, raw: await import_promises4.default.readFile(filePath, "utf8"), collection };
780
+ return { filePath, dir, raw: await import_promises5.default.readFile(filePath, "utf8"), collection };
309
781
  } catch (err) {
310
782
  if (err.code === "ENOENT") throw new ArticleNotFoundError(target);
311
783
  throw err;
312
784
  }
313
785
  }
314
786
  async function updateArticleBody(target, body) {
315
- const { filePath, raw, collection } = await loadArticle(target);
787
+ const { filePath, dir, raw, collection } = await loadArticle(target);
316
788
  const parsed = parseArticle(raw);
317
- await import_promises4.default.writeFile(
789
+ await import_promises5.default.writeFile(
318
790
  filePath,
319
791
  serializeArticle({
320
792
  title: parsed.title,
@@ -325,13 +797,14 @@ async function updateArticleBody(target, body) {
325
797
  }),
326
798
  "utf8"
327
799
  );
800
+ void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
328
801
  return { title: parsed.title, tags: parsed.tags, path: filePath, collection };
329
802
  }
330
803
  async function mutateTags(target, fn) {
331
- const { filePath, raw, collection } = await loadArticle(target);
804
+ const { filePath, dir, raw, collection } = await loadArticle(target);
332
805
  const parsed = parseArticle(raw);
333
806
  const tags = fn(parsed.tags);
334
- await import_promises4.default.writeFile(
807
+ await import_promises5.default.writeFile(
335
808
  filePath,
336
809
  serializeArticle({
337
810
  title: parsed.title,
@@ -342,6 +815,7 @@ async function mutateTags(target, fn) {
342
815
  }),
343
816
  "utf8"
344
817
  );
818
+ void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
345
819
  return { title: parsed.title, tags, path: filePath, collection };
346
820
  }
347
821
  function addTags(target, tags) {
@@ -355,34 +829,37 @@ function setTags(target, tags) {
355
829
  return mutateTags(target, () => Array.from(new Set(tags)));
356
830
  }
357
831
  async function removeArticle(target) {
358
- const { filePath } = await loadArticle(target);
359
- await import_promises4.default.rm(filePath);
832
+ const { filePath, dir, collection } = await loadArticle(target);
833
+ await import_promises5.default.rm(filePath);
834
+ void fireHook2("article-removed", collection, import_node_path3.default.relative(dir, filePath));
360
835
  return filePath;
361
836
  }
362
837
 
363
838
  // src/repo.ts
364
- var import_promises5 = __toESM(require("fs/promises"), 1);
365
- var import_node_path3 = __toESM(require("path"), 1);
366
- var import_node_child_process = require("child_process");
839
+ var import_promises6 = __toESM(require("fs/promises"), 1);
840
+ var import_node_path4 = __toESM(require("path"), 1);
841
+ var import_node_child_process2 = require("child_process");
367
842
  var import_node_util = require("util");
368
- var import_yaml = require("yaml");
369
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
843
+ var import_yaml2 = require("yaml");
844
+ init_collections();
845
+ init_registry();
846
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
370
847
  var REPO_CONFIG_FILE = "diffwiki.yaml";
371
848
  async function detectRepoName(cwd) {
372
849
  try {
373
850
  const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd });
374
- return import_node_path3.default.basename(stdout.trim());
851
+ return import_node_path4.default.basename(stdout.trim());
375
852
  } catch {
376
- return import_node_path3.default.basename(import_node_path3.default.resolve(cwd));
853
+ return import_node_path4.default.basename(import_node_path4.default.resolve(cwd));
377
854
  }
378
855
  }
379
856
  async function readRepoConfig(cwd) {
380
857
  try {
381
- const raw = await import_promises5.default.readFile(import_node_path3.default.join(cwd, REPO_CONFIG_FILE), "utf8");
382
- const parsed = (0, import_yaml.parse)(raw) ?? {};
858
+ const raw = await import_promises6.default.readFile(import_node_path4.default.join(cwd, REPO_CONFIG_FILE), "utf8");
859
+ const parsed = (0, import_yaml2.parse)(raw) ?? {};
383
860
  if (!parsed.path) return void 0;
384
861
  return {
385
- collection: parsed.collection ?? import_node_path3.default.basename(import_node_path3.default.resolve(cwd)),
862
+ collection: parsed.collection ?? import_node_path4.default.basename(import_node_path4.default.resolve(cwd)),
386
863
  path: parsed.path
387
864
  };
388
865
  } catch (err) {
@@ -391,9 +868,9 @@ async function readRepoConfig(cwd) {
391
868
  }
392
869
  }
393
870
  async function initRepoWiki(opts) {
394
- const cwd = import_node_path3.default.resolve(opts.cwd);
871
+ const cwd = import_node_path4.default.resolve(opts.cwd);
395
872
  const wikiPath = opts.wikiPath ?? "wiki";
396
- const absWiki = import_node_path3.default.join(cwd, wikiPath);
873
+ const absWiki = import_node_path4.default.join(cwd, wikiPath);
397
874
  let name = opts.collection ?? await detectRepoName(cwd);
398
875
  const existing = await findCollection(name);
399
876
  if (existing && existing.path !== absWiki) {
@@ -408,9 +885,9 @@ async function initRepoWiki(opts) {
408
885
  n += 1;
409
886
  }
410
887
  }
411
- await import_promises5.default.mkdir(absWiki, { recursive: true });
888
+ await import_promises6.default.mkdir(absWiki, { recursive: true });
412
889
  const config = { collection: name, path: wikiPath };
413
- await import_promises5.default.writeFile(import_node_path3.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml.stringify)(config), "utf8");
890
+ await import_promises6.default.writeFile(import_node_path4.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml2.stringify)(config), "utf8");
414
891
  const entry = {
415
892
  name,
416
893
  type: "repo",
@@ -422,44 +899,371 @@ async function initRepoWiki(opts) {
422
899
  }
423
900
 
424
901
  // src/query.ts
425
- var import_promises6 = __toESM(require("fs/promises"), 1);
426
- var import_node_path4 = __toESM(require("path"), 1);
427
- async function query(term, collection) {
428
- let targets;
902
+ var import_promises7 = __toESM(require("fs/promises"), 1);
903
+ var import_node_path5 = __toESM(require("path"), 1);
904
+ init_collections();
905
+ init_errors();
906
+ init_loader();
907
+ init_registry2();
908
+ init_types();
909
+ async function collectMarkdownFiles(dir) {
910
+ let entries;
911
+ try {
912
+ entries = await import_promises7.default.readdir(dir, { withFileTypes: true });
913
+ } catch {
914
+ return [];
915
+ }
916
+ const files = [];
917
+ for (const entry of entries) {
918
+ const full = import_node_path5.default.join(dir, entry.name);
919
+ if (entry.isDirectory()) {
920
+ files.push(...await collectMarkdownFiles(full));
921
+ } else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
922
+ files.push(full);
923
+ }
924
+ }
925
+ return files;
926
+ }
927
+ var K1 = 1.5;
928
+ var B = 0.75;
929
+ var FIELD_WEIGHTS = { title: 3, tags: 2, collection: 1.5, body: 1 };
930
+ function tokenize(text) {
931
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
932
+ }
933
+ function termFreqs(tokens) {
934
+ const freqs = /* @__PURE__ */ new Map();
935
+ for (const t of tokens) {
936
+ freqs.set(t, (freqs.get(t) ?? 0) + 1);
937
+ }
938
+ return freqs;
939
+ }
940
+ function bm25FieldScore(queryTerms, fieldTokens, avgFieldLen, idf) {
941
+ const tf = termFreqs(fieldTokens);
942
+ const dl = fieldTokens.length;
943
+ const avgDl = avgFieldLen || 1;
944
+ let score = 0;
945
+ for (const term of queryTerms) {
946
+ const freq = tf.get(term) ?? 0;
947
+ if (freq === 0) continue;
948
+ const termIdf = idf.get(term) ?? 0;
949
+ const numerator = freq * (K1 + 1);
950
+ const denominator = freq + K1 * (1 - B + B * (dl / avgDl));
951
+ score += termIdf * (numerator / denominator);
952
+ }
953
+ return score;
954
+ }
955
+ async function nativeSearch(term, collections, collection) {
956
+ let targets = collections;
429
957
  if (collection) {
430
- const one = await findCollection(collection);
958
+ const one = collections.find((c) => c.name === collection);
431
959
  if (!one) throw new CollectionNotFoundError(collection);
432
960
  targets = [one];
433
- } else {
434
- targets = await listCollections();
435
961
  }
436
- const needle = term.toLowerCase();
437
- const hits = [];
962
+ const docs = [];
438
963
  for (const entry of targets) {
439
- let files;
440
- try {
441
- files = await import_promises6.default.readdir(entry.path);
442
- } catch {
443
- continue;
964
+ for (const full of await collectMarkdownFiles(entry.path)) {
965
+ const fallbackTitle = import_node_path5.default.basename(full).replace(/\.mdx?$/, "");
966
+ let title = fallbackTitle;
967
+ let tags = [];
968
+ let body = "";
969
+ try {
970
+ const parsed = parseArticle(await import_promises7.default.readFile(full, "utf8"));
971
+ if (parsed.title) title = parsed.title;
972
+ tags = parsed.tags;
973
+ body = parsed.body;
974
+ } catch {
975
+ }
976
+ docs.push({
977
+ collectionName: entry.name,
978
+ filePath: full,
979
+ title,
980
+ tags,
981
+ fields: {
982
+ title: tokenize(title),
983
+ tags: tokenize(tags.join(" ")),
984
+ collection: tokenize(entry.name),
985
+ body: tokenize(body)
986
+ }
987
+ });
444
988
  }
445
- for (const file of files) {
446
- if (!/\.mdx?$/.test(file)) continue;
447
- const full = import_node_path4.default.join(entry.path, file);
448
- const parsed = parseArticle(await import_promises6.default.readFile(full, "utf8"));
449
- const haystack = [parsed.title, file, ...parsed.tags].join(" ").toLowerCase();
450
- if (!needle || haystack.includes(needle)) {
451
- hits.push({ collection: entry.name, path: full, title: parsed.title, tags: parsed.tags });
989
+ }
990
+ if (!term.trim()) {
991
+ return docs.map((d) => ({
992
+ collection: d.collectionName,
993
+ path: d.filePath,
994
+ title: d.title,
995
+ tags: d.tags
996
+ }));
997
+ }
998
+ const queryTerms = tokenize(term);
999
+ if (queryTerms.length === 0) {
1000
+ return docs.map((d) => ({
1001
+ collection: d.collectionName,
1002
+ path: d.filePath,
1003
+ title: d.title,
1004
+ tags: d.tags
1005
+ }));
1006
+ }
1007
+ const docTokenSets = docs.map(
1008
+ (doc) => /* @__PURE__ */ new Set([...doc.fields.title, ...doc.fields.tags, ...doc.fields.collection, ...doc.fields.body])
1009
+ );
1010
+ const N = docs.length;
1011
+ const idf = /* @__PURE__ */ new Map();
1012
+ for (const qt of queryTerms) {
1013
+ let docFreq = 0;
1014
+ for (const tokenSet of docTokenSets) {
1015
+ if (tokenSet.has(qt)) docFreq++;
1016
+ }
1017
+ idf.set(qt, Math.log((N - docFreq + 0.5) / (docFreq + 0.5) + 1));
1018
+ }
1019
+ const avgLens = {
1020
+ title: docs.reduce((sum, d) => sum + d.fields.title.length, 0) / (N || 1),
1021
+ tags: docs.reduce((sum, d) => sum + d.fields.tags.length, 0) / (N || 1),
1022
+ collection: docs.reduce((sum, d) => sum + d.fields.collection.length, 0) / (N || 1),
1023
+ body: docs.reduce((sum, d) => sum + d.fields.body.length, 0) / (N || 1)
1024
+ };
1025
+ const scored = [];
1026
+ for (const doc of docs) {
1027
+ const titleScore = bm25FieldScore(queryTerms, doc.fields.title, avgLens.title, idf) * FIELD_WEIGHTS.title;
1028
+ const tagsScore = bm25FieldScore(queryTerms, doc.fields.tags, avgLens.tags, idf) * FIELD_WEIGHTS.tags;
1029
+ const collScore = bm25FieldScore(queryTerms, doc.fields.collection, avgLens.collection, idf) * FIELD_WEIGHTS.collection;
1030
+ const bodyScore = bm25FieldScore(queryTerms, doc.fields.body, avgLens.body, idf) * FIELD_WEIGHTS.body;
1031
+ const total = titleScore + tagsScore + collScore + bodyScore;
1032
+ if (total > 0) {
1033
+ scored.push({ doc, score: total });
1034
+ }
1035
+ }
1036
+ scored.sort((a, b) => b.score - a.score);
1037
+ return scored.map((s) => ({
1038
+ collection: s.doc.collectionName,
1039
+ path: s.doc.filePath,
1040
+ title: s.doc.title,
1041
+ tags: s.doc.tags,
1042
+ score: s.score
1043
+ }));
1044
+ }
1045
+ async function resolveDefaultEngine() {
1046
+ const config = await readConfig();
1047
+ const enabled = await listEnabledPlugins("search");
1048
+ const configured = config.defaultSearch?.split(":")[0];
1049
+ if (configured && enabled.some((p) => p.name === configured)) return configured;
1050
+ if (enabled.length > 0) return enabled[0].name;
1051
+ return NATIVE_ENGINE;
1052
+ }
1053
+ async function resolveDefaultSearchMode() {
1054
+ const config = await readConfig();
1055
+ const enabled = await listEnabledPlugins("search");
1056
+ const configured = config.defaultSearch?.split(":")[0];
1057
+ const configuredType = config.defaultSearch?.split(":")[1];
1058
+ let engine = NATIVE_ENGINE;
1059
+ if (configured && enabled.some((p) => p.name === configured)) engine = configured;
1060
+ else if (enabled.length > 0) engine = enabled[0].name;
1061
+ if (engine === NATIVE_ENGINE) return { engine, type: "basic" };
1062
+ const modes = await listSearchModes();
1063
+ const engineModes = modes.filter((m) => m.plugin === engine);
1064
+ if (engineModes.length === 0) return { engine: NATIVE_ENGINE, type: "basic" };
1065
+ if (configuredType && engineModes.some((m) => m.type === configuredType)) {
1066
+ return { engine, type: configuredType };
1067
+ }
1068
+ return { engine, type: engineModes[0].type };
1069
+ }
1070
+ async function query(term, opts) {
1071
+ const collections = await listCollections();
1072
+ const engine = opts?.engine ?? await resolveDefaultEngine();
1073
+ if (engine === NATIVE_ENGINE) return nativeSearch(term, collections, opts?.collection);
1074
+ const provider = await loadSearchProvider(engine);
1075
+ if (!provider) return nativeSearch(term, collections, opts?.collection);
1076
+ try {
1077
+ return await provider.search(term, collections, opts);
1078
+ } catch {
1079
+ return nativeSearch(term, collections, opts?.collection);
1080
+ } finally {
1081
+ await provider.close();
1082
+ }
1083
+ }
1084
+
1085
+ // src/doctor.ts
1086
+ var import_promises9 = __toESM(require("fs/promises"), 1);
1087
+ init_paths();
1088
+ init_registry();
1089
+ init_collections();
1090
+
1091
+ // src/plugins/manager.ts
1092
+ var import_promises8 = __toESM(require("fs/promises"), 1);
1093
+ var import_node_path6 = __toESM(require("path"), 1);
1094
+ var import_node_child_process3 = require("child_process");
1095
+ var import_node_util2 = require("util");
1096
+ init_paths();
1097
+ init_errors();
1098
+ init_collections();
1099
+ init_registry2();
1100
+ init_loader();
1101
+ var run = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
1102
+ async function readPackageJson(file) {
1103
+ try {
1104
+ return JSON.parse(await import_promises8.default.readFile(file, "utf8"));
1105
+ } catch {
1106
+ return null;
1107
+ }
1108
+ }
1109
+ function binOf(manifest) {
1110
+ if (typeof manifest.bin === "string") return manifest.bin;
1111
+ if (manifest.bin && typeof manifest.bin === "object") {
1112
+ const first = Object.values(manifest.bin)[0];
1113
+ return typeof first === "string" ? first : null;
1114
+ }
1115
+ return null;
1116
+ }
1117
+ async function pluginManifestDirs(nodeModules) {
1118
+ let entries;
1119
+ try {
1120
+ entries = await import_promises8.default.readdir(nodeModules);
1121
+ } catch {
1122
+ return [];
1123
+ }
1124
+ const dirs = [];
1125
+ for (const entry of entries) {
1126
+ if (entry.startsWith(".")) continue;
1127
+ if (entry.startsWith("@")) {
1128
+ let scoped;
1129
+ try {
1130
+ scoped = await import_promises8.default.readdir(import_node_path6.default.join(nodeModules, entry));
1131
+ } catch {
1132
+ continue;
1133
+ }
1134
+ for (const pkg of scoped) {
1135
+ if (pkg.startsWith(".")) continue;
1136
+ dirs.push(import_node_path6.default.join(nodeModules, entry, pkg));
452
1137
  }
1138
+ } else {
1139
+ dirs.push(import_node_path6.default.join(nodeModules, entry));
453
1140
  }
454
1141
  }
455
- return hits;
1142
+ return dirs;
1143
+ }
1144
+ async function ensurePluginRoot() {
1145
+ const dir = pluginsDir();
1146
+ await import_promises8.default.mkdir(dir, { recursive: true });
1147
+ const pkgPath = import_node_path6.default.join(dir, "package.json");
1148
+ if (await readPackageJson(pkgPath) === null) {
1149
+ const pkg = { name: "diffwiki-plugins", private: true, version: "0.0.0" };
1150
+ await import_promises8.default.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
1151
+ `, "utf8");
1152
+ }
1153
+ return dir;
1154
+ }
1155
+ async function discoverSearchPackage(dir) {
1156
+ const nodeModules = import_node_path6.default.join(dir, "node_modules");
1157
+ for (const pkgDir of await pluginManifestDirs(nodeModules)) {
1158
+ const manifest = await readPackageJson(import_node_path6.default.join(pkgDir, "package.json"));
1159
+ if (!manifest?.name || manifest.diffwiki?.kind !== "search") continue;
1160
+ const bin = binOf(manifest);
1161
+ if (!bin) continue;
1162
+ return { name: manifest.name, version: manifest.version, binAbs: import_node_path6.default.resolve(pkgDir, bin) };
1163
+ }
1164
+ return null;
1165
+ }
1166
+ async function bestEffortClose(provider) {
1167
+ try {
1168
+ await provider?.close();
1169
+ } catch {
1170
+ }
1171
+ }
1172
+ async function runSetup(name, prefetchModels) {
1173
+ let provider = null;
1174
+ try {
1175
+ provider = await loadSearchProvider(name);
1176
+ if (provider) {
1177
+ await provider.setup(await listCollections(), { embed: false, prefetchModels });
1178
+ }
1179
+ } catch {
1180
+ } finally {
1181
+ await bestEffortClose(provider);
1182
+ }
1183
+ }
1184
+ async function installPlugin(spec, opts) {
1185
+ const dir = await ensurePluginRoot();
1186
+ try {
1187
+ await run("npm", ["install", spec, "--prefix", dir]);
1188
+ } catch (err) {
1189
+ throw new PluginError(`npm install ${spec} failed: ${err.message}`);
1190
+ }
1191
+ const discovered = await discoverSearchPackage(dir);
1192
+ if (!discovered) {
1193
+ throw new PluginError(
1194
+ `no diffwiki search plugin found in "${spec}" (need package.json diffwiki.kind === "search" + bin)`
1195
+ );
1196
+ }
1197
+ const record = {
1198
+ name: discovered.name,
1199
+ kind: "search",
1200
+ command: ["node", discovered.binAbs],
1201
+ version: discovered.version,
1202
+ enabled: true,
1203
+ managed: true
1204
+ };
1205
+ await upsertPlugin(record);
1206
+ if (opts?.setup !== false) {
1207
+ await runSetup(record.name, !!opts?.prefetchModels);
1208
+ }
1209
+ return record;
1210
+ }
1211
+ async function registerPlugin(name, command) {
1212
+ if (command.length === 0) throw new PluginError(`register "${name}": --command must not be empty`);
1213
+ const record = { name, kind: "search", command, enabled: true, managed: false };
1214
+ await upsertPlugin(record);
1215
+ const provider = await loadSearchProvider(name);
1216
+ if (!provider) {
1217
+ await deregisterPlugin(name);
1218
+ throw new PluginError(`plugin "${name}" did not spawn as a search plugin (command: ${command.join(" ")})`);
1219
+ }
1220
+ try {
1221
+ await provider.setup(await listCollections(), { embed: false, prefetchModels: false });
1222
+ } catch {
1223
+ } finally {
1224
+ await bestEffortClose(provider);
1225
+ }
1226
+ return record;
1227
+ }
1228
+ async function removePlugin(name) {
1229
+ const record = (await readPluginRegistry()).plugins.find((p) => p.name === name);
1230
+ if (!record) throw new PluginError(`plugin "${name}" is not registered`);
1231
+ if (record.managed) {
1232
+ const dir = await ensurePluginRoot();
1233
+ try {
1234
+ await run("npm", ["uninstall", name, "--prefix", dir]);
1235
+ } catch {
1236
+ }
1237
+ }
1238
+ await deregisterPlugin(name);
1239
+ }
1240
+ async function probeSpawnable(name) {
1241
+ let provider = null;
1242
+ try {
1243
+ provider = await loadSearchProvider(name);
1244
+ return provider !== null;
1245
+ } catch {
1246
+ return false;
1247
+ } finally {
1248
+ await bestEffortClose(provider);
1249
+ }
1250
+ }
1251
+ async function listPlugins() {
1252
+ const registry = await readPluginRegistry();
1253
+ const out = [];
1254
+ for (const record of registry.plugins) {
1255
+ const spawnable = record.enabled ? await probeSpawnable(record.name) : false;
1256
+ out.push({ ...record, spawnable });
1257
+ }
1258
+ return out;
456
1259
  }
457
1260
 
458
1261
  // src/doctor.ts
459
- var import_promises7 = __toESM(require("fs/promises"), 1);
1262
+ init_loader();
1263
+ init_types();
460
1264
  async function exists(target) {
461
1265
  try {
462
- await import_promises7.default.access(target);
1266
+ await import_promises9.default.access(target);
463
1267
  return true;
464
1268
  } catch {
465
1269
  return false;
@@ -492,8 +1296,309 @@ async function doctor() {
492
1296
  } else {
493
1297
  out.push({ level: "warn", message: "no default collection set" });
494
1298
  }
1299
+ await appendPluginDiagnostics(out, config);
495
1300
  return out;
496
1301
  }
1302
+ function readinessLine(name, health) {
1303
+ const r = health.readiness;
1304
+ if (!r) {
1305
+ return { level: health.ready ? "ok" : "warn", message: `plugin "${name}" ${health.ready ? "ready" : "not ready"}` };
1306
+ }
1307
+ if (r.firstSearchCost === "model-download") {
1308
+ return {
1309
+ level: "warn",
1310
+ message: `plugin "${name}": ${r.docsIndexed} doc(s) indexed \u2014 first search will download models (~2GB)`
1311
+ };
1312
+ }
1313
+ if (r.firstSearchCost === "model-load") {
1314
+ return {
1315
+ level: "warn",
1316
+ message: `plugin "${name}": ${r.docsIndexed} doc(s) indexed \u2014 first search loads models (a few seconds)`
1317
+ };
1318
+ }
1319
+ return { level: "ok", message: `plugin "${name}": ${r.docsIndexed} doc(s) indexed \u2014 first search is instant` };
1320
+ }
1321
+ async function appendPluginDiagnostics(out, config) {
1322
+ let plugins = [];
1323
+ try {
1324
+ plugins = await listPlugins();
1325
+ } catch (err) {
1326
+ out.push({ level: "error", message: `failed to read plugin registry: ${err.message}` });
1327
+ return;
1328
+ }
1329
+ if (plugins.length === 0) {
1330
+ out.push({ level: "warn", message: "no search plugins installed \u2014 using built-in native search" });
1331
+ }
1332
+ const collections = await listCollections();
1333
+ for (const p of plugins) {
1334
+ if (!p.enabled) {
1335
+ out.push({ level: "warn", message: `plugin "${p.name}" registered but disabled` });
1336
+ continue;
1337
+ }
1338
+ if (!p.spawnable) {
1339
+ out.push({
1340
+ level: "error",
1341
+ message: `plugin "${p.name}" registered but its command is not spawnable: ${p.command.join(" ")}`
1342
+ });
1343
+ continue;
1344
+ }
1345
+ out.push({ level: "ok", message: `plugin "${p.name}" enabled and spawnable` });
1346
+ let provider = null;
1347
+ try {
1348
+ provider = await loadSearchProvider(p.name);
1349
+ if (!provider) {
1350
+ out.push({ level: "error", message: `plugin "${p.name}" failed to load for a health check` });
1351
+ continue;
1352
+ }
1353
+ const health = await provider.health(collections);
1354
+ for (const d of health.diagnostics) out.push({ level: d.level, message: `${p.name} \xB7 ${d.message}` });
1355
+ out.push(readinessLine(p.name, health));
1356
+ } catch (err) {
1357
+ out.push({ level: "error", message: `plugin "${p.name}" health check failed: ${err.message}` });
1358
+ } finally {
1359
+ try {
1360
+ await provider?.close();
1361
+ } catch {
1362
+ }
1363
+ }
1364
+ }
1365
+ if (config.defaultSearch) {
1366
+ const engine = config.defaultSearch.split(":")[0];
1367
+ const known = engine === NATIVE_ENGINE || plugins.some((p) => p.name === engine && p.enabled);
1368
+ if (!known) {
1369
+ out.push({
1370
+ level: "warn",
1371
+ message: `defaultSearch "${config.defaultSearch}" is not installed/enabled \u2014 using native`
1372
+ });
1373
+ }
1374
+ }
1375
+ }
1376
+
1377
+ // src/browse.ts
1378
+ var import_promises10 = __toESM(require("fs/promises"), 1);
1379
+ var import_node_path7 = __toESM(require("path"), 1);
1380
+ init_collections();
1381
+ init_errors();
1382
+ var INDEX_RE = /^(index|readme)\.mdx?$/i;
1383
+ function buildArticleTree(relPaths) {
1384
+ const root = {};
1385
+ for (const raw of relPaths) {
1386
+ const rel = raw.replace(/\\/g, "/").replace(/^\/+/, "");
1387
+ if (!rel) continue;
1388
+ const segs = rel.split("/");
1389
+ let cur = root;
1390
+ segs.forEach((seg, i) => {
1391
+ if (i === segs.length - 1) {
1392
+ if (!(seg in cur)) cur[seg] = null;
1393
+ } else {
1394
+ if (cur[seg] === null || !(seg in cur)) cur[seg] = {};
1395
+ cur = cur[seg];
1396
+ }
1397
+ });
1398
+ }
1399
+ const stripExt = (f) => f.replace(/\.(mdx?)$/, "");
1400
+ const walk = (trie, parents) => {
1401
+ const entries = Object.entries(trie);
1402
+ const branches = entries.filter(([, v]) => v !== null);
1403
+ const leaves = entries.filter(([, v]) => v === null).map(([k]) => k).filter((f) => !INDEX_RE.test(f));
1404
+ branches.sort(([a], [b]) => a.toLowerCase().localeCompare(b.toLowerCase()));
1405
+ leaves.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
1406
+ const branchNodes = branches.map(([seg, sub]) => ({
1407
+ name: seg,
1408
+ title: seg,
1409
+ // Folders are navigable to their index page (an authored index.md/README,
1410
+ // else a generated listing).
1411
+ slug: [...parents, seg].join("/"),
1412
+ children: walk(sub, [...parents, seg])
1413
+ }));
1414
+ const leafNodes = leaves.map((file) => {
1415
+ const nameNoExt = stripExt(file);
1416
+ return { name: nameNoExt, title: nameNoExt, slug: [...parents, nameNoExt].join("/") };
1417
+ });
1418
+ return [...branchNodes, ...leafNodes];
1419
+ };
1420
+ return walk(root, []);
1421
+ }
1422
+ async function collectRelPaths(dir, rel = "") {
1423
+ let dirents;
1424
+ try {
1425
+ dirents = await import_promises10.default.readdir(dir, { withFileTypes: true });
1426
+ } catch {
1427
+ return [];
1428
+ }
1429
+ const out = [];
1430
+ for (const d of dirents) {
1431
+ if (d.name.startsWith(".")) continue;
1432
+ const childRel = rel ? `${rel}/${d.name}` : d.name;
1433
+ if (d.isDirectory()) {
1434
+ out.push(...await collectRelPaths(import_node_path7.default.join(dir, d.name), childRel));
1435
+ } else if (/\.(mdx?)$/.test(d.name)) {
1436
+ out.push(childRel);
1437
+ }
1438
+ }
1439
+ return out;
1440
+ }
1441
+ async function readdirSafe(dir) {
1442
+ try {
1443
+ return await import_promises10.default.readdir(dir);
1444
+ } catch {
1445
+ return [];
1446
+ }
1447
+ }
1448
+ function pickIndexFile(names) {
1449
+ const rank = (n) => {
1450
+ const lower = n.toLowerCase();
1451
+ const base = lower.startsWith("index") ? 0 : 1;
1452
+ const ext = lower.endsWith(".mdx") ? 0 : 1;
1453
+ return base * 2 + ext;
1454
+ };
1455
+ return names.filter((n) => INDEX_RE.test(n)).sort((a, b) => rank(a) - rank(b))[0];
1456
+ }
1457
+ async function cheapTitle(absPath) {
1458
+ let handle;
1459
+ try {
1460
+ handle = await import_promises10.default.open(absPath, "r");
1461
+ const buf = Buffer.alloc(512);
1462
+ const { bytesRead } = await handle.read(buf, 0, 512, 0);
1463
+ const snippet = buf.subarray(0, bytesRead).toString("utf8");
1464
+ const m = snippet.match(/^---[\s\S]*?^title:\s*(.+)$/m);
1465
+ if (m) return m[1].trim().replace(/^["']|["']$/g, "");
1466
+ } catch {
1467
+ } finally {
1468
+ await handle?.close();
1469
+ }
1470
+ return void 0;
1471
+ }
1472
+ async function enrichTitles(nodes, collDir) {
1473
+ return Promise.all(
1474
+ nodes.map(async (node) => {
1475
+ if (node.children) {
1476
+ const children = await enrichTitles(node.children, collDir);
1477
+ let title = node.title;
1478
+ if (node.slug) {
1479
+ const dir = import_node_path7.default.join(collDir, node.slug);
1480
+ const idx = pickIndexFile(await readdirSafe(dir));
1481
+ if (idx) title = await cheapTitle(import_node_path7.default.join(dir, idx)) ?? title;
1482
+ }
1483
+ return { ...node, title, children };
1484
+ }
1485
+ for (const ext of [".mdx", ".md"]) {
1486
+ const title = await cheapTitle(import_node_path7.default.join(collDir, `${node.slug}${ext}`));
1487
+ if (title) return { ...node, title };
1488
+ }
1489
+ return node;
1490
+ })
1491
+ );
1492
+ }
1493
+ async function listArticleTree(coll) {
1494
+ const entry = await findCollection(coll);
1495
+ if (!entry) throw new CollectionNotFoundError(coll);
1496
+ const rels = await collectRelPaths(entry.path);
1497
+ return enrichTitles(buildArticleTree(rels), import_node_path7.default.resolve(entry.path));
1498
+ }
1499
+ async function readArticle(coll, slug) {
1500
+ const entry = await findCollection(coll);
1501
+ if (!entry) throw new CollectionNotFoundError(coll);
1502
+ const collDir = import_node_path7.default.resolve(entry.path);
1503
+ const normalized = slug.replace(/\\/g, "/");
1504
+ if (normalized.split("/").some((s) => s === "..")) {
1505
+ throw new InvalidTargetError(slug, "path traversal detected");
1506
+ }
1507
+ const safeSlug = normalized.split("/").filter((s) => s !== "" && s !== ".").join("/");
1508
+ const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path7.default.sep);
1509
+ let abs;
1510
+ for (const rel of safeSlug ? [`${safeSlug}.mdx`, `${safeSlug}.md`] : []) {
1511
+ const candidate = import_node_path7.default.resolve(collDir, rel);
1512
+ if (!inJail(candidate)) throw new InvalidTargetError(slug, "path traversal detected");
1513
+ try {
1514
+ await import_promises10.default.access(candidate);
1515
+ abs = candidate;
1516
+ break;
1517
+ } catch {
1518
+ }
1519
+ }
1520
+ if (!abs) {
1521
+ const dir = import_node_path7.default.resolve(collDir, safeSlug);
1522
+ if (inJail(dir)) {
1523
+ const idx = pickIndexFile(await readdirSafe(dir));
1524
+ if (idx) abs = import_node_path7.default.join(dir, idx);
1525
+ }
1526
+ }
1527
+ if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
1528
+ const parsed = parseArticle(await import_promises10.default.readFile(abs, "utf8"));
1529
+ const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
1530
+ return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs };
1531
+ }
1532
+
1533
+ // src/render.ts
1534
+ var import_unified = require("unified");
1535
+ var import_remark_parse = __toESM(require("remark-parse"), 1);
1536
+ var import_remark_gfm = __toESM(require("remark-gfm"), 1);
1537
+ var import_remark_rehype = __toESM(require("remark-rehype"), 1);
1538
+ var import_rehype_slug = __toESM(require("rehype-slug"), 1);
1539
+ var import_rehype = __toESM(require("@shikijs/rehype"), 1);
1540
+ var import_rehype_stringify = __toESM(require("rehype-stringify"), 1);
1541
+ var import_unist_util_visit = require("unist-util-visit");
1542
+ var import_hast_util_to_string = require("hast-util-to-string");
1543
+ var LANG_ALIASES = {
1544
+ "c#": "csharp",
1545
+ cs: "csharp",
1546
+ "c++": "cpp",
1547
+ cxx: "cpp",
1548
+ hs: "haskell",
1549
+ ex: "elixir"
1550
+ };
1551
+ function rehypeCollectToc(toc) {
1552
+ return (tree) => {
1553
+ (0, import_unist_util_visit.visit)(tree, "element", (node) => {
1554
+ if (node.tagName !== "h2" && node.tagName !== "h3") return;
1555
+ const id = node.properties?.id;
1556
+ if (!id) return;
1557
+ toc.push({ slug: String(id), text: (0, import_hast_util_to_string.toString)(node), depth: node.tagName === "h2" ? 2 : 3 });
1558
+ });
1559
+ };
1560
+ }
1561
+ function rehypeStripLeadingH1() {
1562
+ return (tree) => {
1563
+ let removed = false;
1564
+ (0, import_unist_util_visit.visit)(tree, "element", (node, index, parent) => {
1565
+ if (!removed && node.tagName === "h1" && parent && index != null) {
1566
+ parent.children.splice(index, 1);
1567
+ removed = true;
1568
+ return [import_unist_util_visit.SKIP, index];
1569
+ }
1570
+ });
1571
+ };
1572
+ }
1573
+ function rehypeMarkMermaid() {
1574
+ return (tree) => {
1575
+ (0, import_unist_util_visit.visit)(tree, "element", (node) => {
1576
+ if (node.tagName !== "pre") return;
1577
+ const code = node.children.find((c) => c.type === "element" && c.tagName === "code");
1578
+ const cls = code?.properties?.className ?? [];
1579
+ if (!code || !cls.includes("language-mermaid")) return;
1580
+ const source = (0, import_hast_util_to_string.toString)(code);
1581
+ node.properties = { className: ["mermaid"] };
1582
+ node.children = [{ type: "text", value: source }];
1583
+ });
1584
+ };
1585
+ }
1586
+ async function renderArticle(body) {
1587
+ const toc = [];
1588
+ const file = await (0, import_unified.unified)().use(import_remark_parse.default).use(import_remark_gfm.default).use(import_remark_rehype.default).use(import_rehype_slug.default).use(rehypeCollectToc, toc).use(rehypeStripLeadingH1).use(rehypeMarkMermaid).use(import_rehype.default, {
1589
+ themes: { light: "github-light", dark: "github-dark" },
1590
+ defaultColor: false,
1591
+ langAlias: LANG_ALIASES,
1592
+ fallbackLanguage: "plaintext"
1593
+ }).use(import_rehype_stringify.default).process(body);
1594
+ return { html: String(file), toc };
1595
+ }
1596
+
1597
+ // src/index.ts
1598
+ init_registry2();
1599
+ init_client();
1600
+ init_loader();
1601
+ init_events();
497
1602
  // Annotate the CommonJS export names for ESM import in node:
498
1603
  0 && (module.exports = {
499
1604
  ArticleNotFoundError,
@@ -501,38 +1606,63 @@ async function doctor() {
501
1606
  CollectionNotFoundError,
502
1607
  DiffwikiError,
503
1608
  InvalidTargetError,
1609
+ NATIVE_ENGINE,
504
1610
  NoDefaultCollectionError,
1611
+ PluginError,
505
1612
  REPO_CONFIG_FILE,
506
1613
  addTags,
1614
+ buildArticleTree,
507
1615
  collectionDir,
508
1616
  collectionsDir,
509
1617
  configPath,
510
1618
  createArticle,
511
1619
  createCollection,
1620
+ deregisterPlugin,
512
1621
  doctor,
1622
+ emitPluginEvent,
1623
+ ensurePluginRoot,
513
1624
  findCollection,
1625
+ findEnabledPlugin,
514
1626
  getConfigValue,
515
1627
  initRepoWiki,
1628
+ installPlugin,
1629
+ listArticleTree,
516
1630
  listCollections,
1631
+ listEnabledPlugins,
1632
+ listPlugins,
1633
+ listSearchModes,
1634
+ loadSearchProvider,
1635
+ nativeSearch,
517
1636
  parseAddTarget,
518
1637
  parseArticle,
519
1638
  parsePathTarget,
1639
+ pluginsDir,
1640
+ pluginsRegistryPath,
520
1641
  query,
1642
+ readArticle,
521
1643
  readConfig,
1644
+ readPluginRegistry,
522
1645
  readRegistry,
523
1646
  readRepoConfig,
524
1647
  registerCollection,
1648
+ registerPlugin,
525
1649
  registryPath,
526
1650
  removeArticle,
527
1651
  removeCollection,
1652
+ removePlugin,
528
1653
  removeTags,
1654
+ renderArticle,
1655
+ resolveDefaultSearchMode,
529
1656
  resolveHome,
530
1657
  serializeArticle,
531
1658
  setConfigValue,
532
1659
  setTags,
533
1660
  slugify,
1661
+ spawnPluginClient,
534
1662
  unregisterCollection,
535
1663
  updateArticleBody,
1664
+ upsertPlugin,
536
1665
  writeConfig,
1666
+ writePluginRegistry,
537
1667
  writeRegistry
538
1668
  });