diffwiki-core 0.4.0-rc.202607221918.a124bf3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5,9 +5,6 @@ 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
- };
11
8
  var __export = (target, all) => {
12
9
  for (var name in all)
13
10
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -30,57 +27,91 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
27
  ));
31
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
29
 
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
- }
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
+ buildArticleTree: () => buildArticleTree,
42
+ collectionDir: () => collectionDir,
43
+ collectionsDir: () => collectionsDir,
44
+ configPath: () => configPath,
45
+ createArticle: () => createArticle,
46
+ createCollection: () => createCollection,
47
+ doctor: () => doctor,
48
+ findCollection: () => findCollection,
49
+ getConfigValue: () => getConfigValue,
50
+ initRepoWiki: () => initRepoWiki,
51
+ listArticleTree: () => listArticleTree,
52
+ listCollections: () => listCollections,
53
+ parseAddTarget: () => parseAddTarget,
54
+ parseArticle: () => parseArticle,
55
+ parsePathTarget: () => parsePathTarget,
56
+ query: () => query,
57
+ readArticle: () => readArticle,
58
+ readConfig: () => readConfig,
59
+ readRegistry: () => readRegistry,
60
+ readRepoConfig: () => readRepoConfig,
61
+ registerCollection: () => registerCollection,
62
+ registryPath: () => registryPath,
63
+ removeArticle: () => removeArticle,
64
+ removeCollection: () => removeCollection,
65
+ removeTags: () => removeTags,
66
+ renderArticle: () => renderArticle,
67
+ resolveHome: () => resolveHome,
68
+ serializeArticle: () => serializeArticle,
69
+ setConfigValue: () => setConfigValue,
70
+ setTags: () => setTags,
71
+ slugify: () => slugify,
72
+ unregisterCollection: () => unregisterCollection,
73
+ updateArticleBody: () => updateArticleBody,
74
+ writeConfig: () => writeConfig,
75
+ writeRegistry: () => writeRegistry
40
76
  });
77
+ module.exports = __toCommonJS(index_exports);
41
78
 
42
79
  // src/errors.ts
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
- };
80
+ var DiffwikiError = class extends Error {
81
+ constructor(message) {
82
+ super(message);
83
+ this.name = new.target.name;
80
84
  }
81
- });
85
+ };
86
+ var CollectionExistsError = class extends DiffwikiError {
87
+ constructor(name) {
88
+ super(`collection "${name}" already exists`);
89
+ }
90
+ };
91
+ var CollectionNotFoundError = class extends DiffwikiError {
92
+ constructor(name) {
93
+ super(`collection "${name}" not found`);
94
+ }
95
+ };
96
+ var NoDefaultCollectionError = class extends DiffwikiError {
97
+ constructor() {
98
+ super("no default collection set (use: diffwiki config-set defaultCollection <name>)");
99
+ }
100
+ };
101
+ var ArticleNotFoundError = class extends DiffwikiError {
102
+ constructor(target) {
103
+ super(`article "${target}" not found`);
104
+ }
105
+ };
106
+ var InvalidTargetError = class extends DiffwikiError {
107
+ constructor(target, expected) {
108
+ super(`invalid target "${target}" \u2014 ${expected}`);
109
+ }
110
+ };
82
111
 
83
112
  // src/paths.ts
113
+ var import_node_os = __toESM(require("os"), 1);
114
+ var import_node_path = __toESM(require("path"), 1);
84
115
  function resolveHome() {
85
116
  const override = process.env.DIFFWIKI_HOME;
86
117
  return override && override.length > 0 ? override : import_node_path.default.join(import_node_os.default.homedir(), ".diffwiki");
@@ -97,22 +128,15 @@ function collectionsDir() {
97
128
  function collectionDir(name) {
98
129
  return import_node_path.default.join(collectionsDir(), name);
99
130
  }
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
- });
131
+
132
+ // src/config.ts
133
+ var import_promises3 = __toESM(require("fs/promises"), 1);
134
+
135
+ // src/collections.ts
136
+ var import_promises2 = __toESM(require("fs/promises"), 1);
114
137
 
115
138
  // src/registry.ts
139
+ var import_promises = __toESM(require("fs/promises"), 1);
116
140
  function isMissing(err) {
117
141
  return err?.code === "ENOENT";
118
142
  }
@@ -145,359 +169,8 @@ async function unregisterCollection(name) {
145
169
  registry.collections = registry.collections.filter((c) => c.name !== name);
146
170
  await writeRegistry(registry);
147
171
  }
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
- async function findPluginRecord(name) {
200
- const registry = await readPluginRegistry();
201
- return registry.plugins.find((p) => p.name === name);
202
- }
203
- var import_promises2;
204
- var init_registry2 = __esm({
205
- "src/plugins/registry.ts"() {
206
- "use strict";
207
- import_promises2 = __toESM(require("fs/promises"), 1);
208
- init_paths();
209
- }
210
- });
211
-
212
- // src/logger.ts
213
- function getLogger(scope) {
214
- const parts = Array.isArray(scope) ? scope : [scope];
215
- return (0, import_logtape.getLogger)(["diffwiki", ...parts]);
216
- }
217
- function resolveLogLevel(env = process.env) {
218
- const raw = (env.DIFFWIKI_LOG ?? "").toLowerCase();
219
- return LEVELS.includes(raw) ? raw : DEFAULT_LEVEL;
220
- }
221
- var import_logtape, LEVELS, DEFAULT_LEVEL;
222
- var init_logger = __esm({
223
- "src/logger.ts"() {
224
- "use strict";
225
- import_logtape = require("@logtape/logtape");
226
- LEVELS = ["trace", "debug", "info", "warning", "error", "fatal"];
227
- DEFAULT_LEVEL = "warning";
228
- }
229
- });
230
-
231
- // src/plugins/host.ts
232
- async function invokePlugin(command, op, params, opts) {
233
- const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
234
- const scope = opts?.name ?? (0, import_node_path2.basename)(command[1] ?? command[0] ?? "plugin");
235
- return new Promise((resolve, reject) => {
236
- const child = (0, import_node_child_process.spawn)(command[0], command.slice(1), {
237
- stdio: ["pipe", "pipe", "pipe"]
238
- });
239
- let stdout = "";
240
- let stderr = "";
241
- let stderrLine = "";
242
- let settled = false;
243
- const forwardLog = (line) => {
244
- const trimmedLine = line.trim();
245
- if (!trimmedLine.startsWith("{")) return;
246
- let parsed;
247
- try {
248
- parsed = JSON.parse(trimmedLine);
249
- } catch {
250
- return;
251
- }
252
- const entry = parsed.log;
253
- if (!entry || typeof entry.message !== "string") return;
254
- const rawLevel = typeof entry.level === "string" ? entry.level.toLowerCase() : "";
255
- const method = LEVEL_METHOD[rawLevel] ?? "info";
256
- getLogger(["plugin", scope])[method](entry.message);
257
- };
258
- const timer = setTimeout(() => {
259
- if (settled) return;
260
- settled = true;
261
- child.kill("SIGKILL");
262
- reject(new Error(`plugin "${op}" timed out after ${timeoutMs}ms`));
263
- }, timeoutMs);
264
- const settle = (fn) => {
265
- if (settled) return;
266
- settled = true;
267
- clearTimeout(timer);
268
- fn();
269
- };
270
- child.stdout.setEncoding("utf8");
271
- child.stdout.on("data", (chunk) => {
272
- stdout += chunk;
273
- });
274
- child.stderr.setEncoding("utf8");
275
- child.stderr.on("data", (chunk) => {
276
- stderr += chunk;
277
- stderrLine += chunk;
278
- let nl;
279
- while ((nl = stderrLine.indexOf("\n")) !== -1) {
280
- forwardLog(stderrLine.slice(0, nl));
281
- stderrLine = stderrLine.slice(nl + 1);
282
- }
283
- });
284
- child.on("error", (err) => {
285
- settle(() => reject(new Error(`plugin spawn failed: ${err.message}`)));
286
- });
287
- child.on("close", (code) => {
288
- if (stderrLine.length > 0) {
289
- forwardLog(stderrLine);
290
- stderrLine = "";
291
- }
292
- settle(() => {
293
- const trimmed = stdout.trim();
294
- if (!trimmed) {
295
- reject(
296
- new Error(`plugin "${op}" produced no output (exit ${code})${stderr ? ": " + stderr.slice(0, 500) : ""}`)
297
- );
298
- return;
299
- }
300
- let response;
301
- try {
302
- response = JSON.parse(trimmed);
303
- } catch {
304
- reject(new Error(`plugin "${op}" returned invalid JSON: ${trimmed.slice(0, 200)}`));
305
- return;
306
- }
307
- if (response.ok) {
308
- resolve(response.result);
309
- } else {
310
- const message = typeof response.error?.message === "string" ? response.error.message : `plugin "${op}" failed`;
311
- reject(new Error(message));
312
- }
313
- });
314
- });
315
- const request = JSON.stringify({ op, params });
316
- child.stdin.write(request + "\n", (err) => {
317
- if (err && !settled) {
318
- settle(() => reject(new Error(`failed to write to plugin stdin: ${err.message}`)));
319
- return;
320
- }
321
- child.stdin.end();
322
- });
323
- });
324
- }
325
- function toWireCollections(collections) {
326
- return collections.map((c) => ({ name: c.name, path: c.path }));
327
- }
328
- function mapHits(result, collections) {
329
- const raw = result?.hits;
330
- const hits = Array.isArray(raw) ? raw : [];
331
- const out = [];
332
- for (const item of hits) {
333
- if (typeof item !== "object" || item === null) continue;
334
- const h = item;
335
- if (typeof h.collection !== "string" || typeof h.relPath !== "string") continue;
336
- const title = typeof h.title === "string" ? h.title : h.relPath;
337
- const coll = collections.find((c) => c.name === h.collection);
338
- const abs = coll ? import_node_path2.default.join(coll.path, h.relPath) : h.relPath;
339
- out.push({
340
- collection: h.collection,
341
- path: abs,
342
- title,
343
- tags: Array.isArray(h.tags) ? h.tags.filter((t) => typeof t === "string") : [],
344
- snippet: typeof h.snippet === "string" ? h.snippet : void 0,
345
- score: typeof h.score === "number" ? h.score : void 0,
346
- docid: typeof h.docid === "string" ? h.docid : void 0
347
- });
348
- }
349
- return out;
350
- }
351
- async function loadSearchProvider(name) {
352
- const record = await findEnabledPlugin("search", name);
353
- if (!record) return null;
354
- let capabilities;
355
- try {
356
- const desc = await invokePlugin(record.command, "describe", {}, { name: record.name });
357
- if (desc.kind !== "search") {
358
- return null;
359
- }
360
- capabilities = desc.capabilities;
361
- } catch {
362
- return null;
363
- }
364
- const provider = {
365
- name: record.name,
366
- capabilities,
367
- async handleSearch(term, collections, opts) {
368
- const result = await invokePlugin(
369
- record.command,
370
- "search",
371
- {
372
- term,
373
- collections: toWireCollections(collections),
374
- collection: opts?.collection,
375
- type: opts?.type,
376
- limit: opts?.limit
377
- },
378
- { timeoutMs: SEARCH_TIMEOUT_MS, name: record.name }
379
- );
380
- return mapHits(result, collections);
381
- },
382
- async reindex(collections, opts) {
383
- const result = await invokePlugin(
384
- record.command,
385
- "index",
386
- {
387
- collections: toWireCollections(collections),
388
- embed: opts?.embed
389
- },
390
- { timeoutMs: LONG_TIMEOUT_MS, name: record.name }
391
- );
392
- return result?.indexed ?? 0;
393
- },
394
- async emitEvent(event, collection, relPath, collections) {
395
- try {
396
- const entry = collections.find((c) => c.name === collection);
397
- await invokePlugin(
398
- record.command,
399
- "event",
400
- {
401
- event,
402
- collection: entry ? { name: entry.name, path: entry.path } : { name: collection, path: "" },
403
- relPath,
404
- collections: toWireCollections(collections)
405
- },
406
- { name: record.name }
407
- );
408
- } catch {
409
- }
410
- },
411
- async health(collections) {
412
- const result = await invokePlugin(record.command, "health", { collections: toWireCollections(collections) }, { name: record.name });
413
- return {
414
- ready: result?.ready === true,
415
- readiness: result?.readiness,
416
- diagnostics: result?.diagnostics ?? []
417
- };
418
- }
419
- };
420
- return provider;
421
- }
422
- async function listSearchModes() {
423
- const modes = [{ plugin: NATIVE_ENGINE, type: "basic", label: "native \xB7 bm25" }];
424
- const plugins = await listEnabledPlugins("search");
425
- for (const record of plugins) {
426
- const caps = record.capabilities;
427
- if (!caps) continue;
428
- for (const type of caps.searchTypes) {
429
- modes.push({ plugin: record.name, type, label: `${record.name} \xB7 ${type}` });
430
- }
431
- }
432
- return modes;
433
- }
434
- var import_node_child_process, import_node_path2, LEVEL_METHOD, DEFAULT_TIMEOUT_MS, SEARCH_TIMEOUT_MS, LONG_TIMEOUT_MS;
435
- var init_host = __esm({
436
- "src/plugins/host.ts"() {
437
- "use strict";
438
- import_node_child_process = require("child_process");
439
- import_node_path2 = __toESM(require("path"), 1);
440
- init_logger();
441
- init_types();
442
- init_registry2();
443
- LEVEL_METHOD = {
444
- trace: "trace",
445
- debug: "debug",
446
- info: "info",
447
- warn: "warn",
448
- warning: "warn",
449
- error: "error",
450
- fatal: "fatal"
451
- };
452
- DEFAULT_TIMEOUT_MS = 3e4;
453
- SEARCH_TIMEOUT_MS = 12e4;
454
- LONG_TIMEOUT_MS = 18e5;
455
- }
456
- });
457
-
458
- // src/plugins/events.ts
459
- var events_exports = {};
460
- __export(events_exports, {
461
- emitPluginEvent: () => emitPluginEvent
462
- });
463
- async function emitPluginEvent(event, collection, relPath) {
464
- try {
465
- const record = await findEnabledPlugin("search");
466
- if (!record) return;
467
- if (!record.capabilities?.events) return;
468
- const collections = await listCollections();
469
- const entry = collections.find((c) => c.name === collection);
470
- await invokePlugin(
471
- record.command,
472
- "event",
473
- {
474
- event,
475
- collection: entry ? { name: entry.name, path: entry.path } : { name: collection, path: "" },
476
- relPath,
477
- collections: collections.map((c) => ({ name: c.name, path: c.path }))
478
- },
479
- { name: record.name }
480
- );
481
- } catch {
482
- }
483
- }
484
- var init_events = __esm({
485
- "src/plugins/events.ts"() {
486
- "use strict";
487
- init_collections();
488
- init_registry2();
489
- init_host();
490
- }
491
- });
492
172
 
493
173
  // src/collections.ts
494
- async function fireHook(event, collection) {
495
- try {
496
- const { emitPluginEvent: emitPluginEvent2 } = await Promise.resolve().then(() => (init_events(), events_exports));
497
- await emitPluginEvent2(event, collection);
498
- } catch {
499
- }
500
- }
501
174
  async function listCollections() {
502
175
  return (await readRegistry()).collections;
503
176
  }
@@ -507,7 +180,7 @@ async function findCollection(name) {
507
180
  async function createCollection(name) {
508
181
  if (await findCollection(name)) throw new CollectionExistsError(name);
509
182
  const dir = collectionDir(name);
510
- await import_promises3.default.mkdir(dir, { recursive: true });
183
+ await import_promises2.default.mkdir(dir, { recursive: true });
511
184
  const entry = {
512
185
  name,
513
186
  type: "global",
@@ -515,7 +188,6 @@ async function createCollection(name) {
515
188
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
516
189
  };
517
190
  await registerCollection(entry);
518
- void fireHook("collection-added", name);
519
191
  return entry;
520
192
  }
521
193
  async function removeCollection(name) {
@@ -523,137 +195,32 @@ async function removeCollection(name) {
523
195
  if (!entry) throw new CollectionNotFoundError(name);
524
196
  await unregisterCollection(name);
525
197
  if (entry.type === "global") {
526
- await import_promises3.default.rm(entry.path, { recursive: true, force: true });
198
+ await import_promises2.default.rm(entry.path, { recursive: true, force: true });
527
199
  }
528
- void fireHook("collection-removed", name);
529
200
  return entry;
530
201
  }
531
- var import_promises3;
532
- var init_collections = __esm({
533
- "src/collections.ts"() {
534
- "use strict";
535
- import_promises3 = __toESM(require("fs/promises"), 1);
536
- init_paths();
537
- init_registry();
538
- init_errors();
539
- }
540
- });
541
-
542
- // src/index.ts
543
- var index_exports = {};
544
- __export(index_exports, {
545
- ArticleNotFoundError: () => ArticleNotFoundError,
546
- CollectionExistsError: () => CollectionExistsError,
547
- CollectionNotFoundError: () => CollectionNotFoundError,
548
- DiffwikiError: () => DiffwikiError,
549
- InvalidTargetError: () => InvalidTargetError,
550
- NATIVE_ENGINE: () => NATIVE_ENGINE,
551
- NoDefaultCollectionError: () => NoDefaultCollectionError,
552
- PluginError: () => PluginError,
553
- REPO_CONFIG_FILE: () => REPO_CONFIG_FILE,
554
- addTags: () => addTags,
555
- buildArticleTree: () => buildArticleTree,
556
- collectionDir: () => collectionDir,
557
- collectionsDir: () => collectionsDir,
558
- configPath: () => configPath,
559
- createArticle: () => createArticle,
560
- createCollection: () => createCollection,
561
- deregisterPlugin: () => deregisterPlugin,
562
- doctor: () => doctor,
563
- emitPluginEvent: () => emitPluginEvent,
564
- ensurePluginRoot: () => ensurePluginRoot,
565
- findCollection: () => findCollection,
566
- findEnabledPlugin: () => findEnabledPlugin,
567
- findPluginRecord: () => findPluginRecord,
568
- getConfigValue: () => getConfigValue,
569
- getLogger: () => getLogger,
570
- initRepoWiki: () => initRepoWiki,
571
- installPlugin: () => installPlugin,
572
- listArticleTree: () => listArticleTree,
573
- listAvailablePlugins: () => listAvailablePlugins,
574
- listCollections: () => listCollections,
575
- listEnabledPlugins: () => listEnabledPlugins,
576
- listPlugins: () => listPlugins,
577
- listSearchModes: () => listSearchModes,
578
- loadSearchProvider: () => loadSearchProvider,
579
- nativeSearch: () => nativeSearch,
580
- parseAddTarget: () => parseAddTarget,
581
- parseArticle: () => parseArticle,
582
- parsePathTarget: () => parsePathTarget,
583
- pluginsDir: () => pluginsDir,
584
- pluginsRegistryPath: () => pluginsRegistryPath,
585
- query: () => query,
586
- readArticle: () => readArticle,
587
- readConfig: () => readConfig,
588
- readPluginRegistry: () => readPluginRegistry,
589
- readRegistry: () => readRegistry,
590
- readRepoConfig: () => readRepoConfig,
591
- registerCollection: () => registerCollection,
592
- registerPlugin: () => registerPlugin,
593
- registryPath: () => registryPath,
594
- removeArticle: () => removeArticle,
595
- removeCollection: () => removeCollection,
596
- removePlugin: () => removePlugin,
597
- removeTags: () => removeTags,
598
- renderArticle: () => renderArticle,
599
- resolveDefaultSearchMode: () => resolveDefaultSearchMode,
600
- resolveHome: () => resolveHome,
601
- resolveLogLevel: () => resolveLogLevel,
602
- search: () => search,
603
- serializeArticle: () => serializeArticle,
604
- setConfigValue: () => setConfigValue,
605
- setTags: () => setTags,
606
- slugify: () => slugify,
607
- unregisterCollection: () => unregisterCollection,
608
- updateArticleBody: () => updateArticleBody,
609
- upsertPlugin: () => upsertPlugin,
610
- writeConfig: () => writeConfig,
611
- writePluginRegistry: () => writePluginRegistry,
612
- writeRegistry: () => writeRegistry
613
- });
614
- module.exports = __toCommonJS(index_exports);
615
- init_types();
616
- init_errors();
617
- init_paths();
618
202
 
619
203
  // src/config.ts
620
- var import_promises4 = __toESM(require("fs/promises"), 1);
621
- init_paths();
622
- init_collections();
623
- init_errors();
624
- init_registry2();
625
- init_types();
626
- function isMissing3(err) {
204
+ function isMissing2(err) {
627
205
  return err?.code === "ENOENT";
628
206
  }
629
207
  async function readConfig() {
630
208
  try {
631
- return JSON.parse(await import_promises4.default.readFile(configPath(), "utf8"));
209
+ return JSON.parse(await import_promises3.default.readFile(configPath(), "utf8"));
632
210
  } catch (err) {
633
- if (isMissing3(err)) return {};
211
+ if (isMissing2(err)) return {};
634
212
  throw err;
635
213
  }
636
214
  }
637
215
  async function writeConfig(config) {
638
- await import_promises4.default.mkdir(resolveHome(), { recursive: true });
639
- await import_promises4.default.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
216
+ await import_promises3.default.mkdir(resolveHome(), { recursive: true });
217
+ await import_promises3.default.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
640
218
  `, "utf8");
641
219
  }
642
220
  async function setConfigValue(key, value) {
643
221
  if (key === "defaultCollection" && !await findCollection(value)) {
644
222
  throw new CollectionNotFoundError(value);
645
223
  }
646
- if (key === "defaultSearch") {
647
- const engine = value.split(":")[0];
648
- if (engine !== NATIVE_ENGINE) {
649
- const enabled = await listEnabledPlugins("search");
650
- if (!enabled.some((p) => p.name === engine)) {
651
- console.warn(
652
- `! defaultSearch "${value}" names an engine that is not installed/enabled \u2014 native will be used until it is`
653
- );
654
- }
655
- }
656
- }
657
224
  const config = await readConfig();
658
225
  config[key] = value;
659
226
  await writeConfig(config);
@@ -664,9 +231,6 @@ async function getConfigValue(key) {
664
231
  return typeof value === "string" ? value : void 0;
665
232
  }
666
233
 
667
- // src/index.ts
668
- init_registry();
669
-
670
234
  // src/slugify.ts
671
235
  function slugify(input) {
672
236
  const slug = input.toLowerCase().replace(/^#+\s*/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/g, "");
@@ -684,33 +248,12 @@ var YAML_ENGINE = {
684
248
  stringify: (data) => (0, import_yaml.stringify)(data)
685
249
  };
686
250
  var MATTER_OPTS = { engines: { yaml: YAML_ENGINE } };
687
- function parseFallback(raw) {
688
- const fenceRe = /^---[ \t]*\n([\s\S]*?)\n---[ \t]*\n/;
689
- const fenceMatch = fenceRe.exec(raw);
690
- let title = "";
691
- let body;
692
- if (fenceMatch) {
693
- const inner = fenceMatch[1];
694
- const titleMatch = /^title:\s*(.+)$/m.exec(inner);
695
- if (titleMatch) {
696
- title = titleMatch[1].trim().replace(/^["']|["']$/g, "");
697
- }
698
- body = raw.slice(fenceMatch[0].length);
699
- } else {
700
- body = raw;
701
- }
702
- return { title, tags: [], body: body.replace(/^\n+/, "") };
703
- }
704
251
  function parseArticle(raw) {
705
- try {
706
- const { data, content } = (0, import_gray_matter.default)(raw, MATTER_OPTS);
707
- const title = typeof data.title === "string" ? data.title : "";
708
- const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
709
- const created = typeof data.created === "string" ? data.created : void 0;
710
- return { title, tags, created, body: content.replace(/^\n+/, "") };
711
- } catch {
712
- return parseFallback(raw);
713
- }
252
+ const { data, content } = (0, import_gray_matter.default)(raw, MATTER_OPTS);
253
+ const title = typeof data.title === "string" ? data.title : "";
254
+ const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
255
+ const created = typeof data.created === "string" ? data.created : void 0;
256
+ return { title, tags, created, body: content.replace(/^\n+/, "") };
714
257
  }
715
258
  function serializeArticle(article) {
716
259
  const front = { title: article.title, tags: article.tags };
@@ -719,21 +262,9 @@ function serializeArticle(article) {
719
262
  return import_gray_matter.default.stringify(article.body, front, MATTER_OPTS);
720
263
  }
721
264
 
722
- // src/index.ts
723
- init_collections();
724
-
725
265
  // src/articles.ts
726
- var import_promises5 = __toESM(require("fs/promises"), 1);
727
- var import_node_path3 = __toESM(require("path"), 1);
728
- init_collections();
729
- init_errors();
730
- async function fireHook2(event, collection, relPath) {
731
- try {
732
- const { emitPluginEvent: emitPluginEvent2 } = await Promise.resolve().then(() => (init_events(), events_exports));
733
- await emitPluginEvent2(event, collection, relPath);
734
- } catch {
735
- }
736
- }
266
+ var import_promises4 = __toESM(require("fs/promises"), 1);
267
+ var import_node_path2 = __toESM(require("path"), 1);
737
268
  function parseAddTarget(target) {
738
269
  const idx = target.indexOf(":");
739
270
  if (idx === -1) {
@@ -766,38 +297,37 @@ async function resolveCollectionDir(name) {
766
297
  }
767
298
  function ensureMdPath(file) {
768
299
  if (/\.mdx?$/.test(file)) return file;
769
- const dir = import_node_path3.default.dirname(file);
770
- const base = slugify(import_node_path3.default.basename(file));
771
- const rel = dir === "." ? `${base}.md` : import_node_path3.default.join(dir, `${base}.md`);
300
+ const dir = import_node_path2.default.dirname(file);
301
+ const base = slugify(import_node_path2.default.basename(file));
302
+ const rel = dir === "." ? `${base}.md` : import_node_path2.default.join(dir, `${base}.md`);
772
303
  return rel;
773
304
  }
774
305
  async function createArticle(opts) {
775
306
  const { collection, title } = parseAddTarget(opts.target);
776
307
  const { name, dir } = await resolveCollectionDir(collection);
777
- const filePath = import_node_path3.default.join(dir, `${slugify(title)}.md`);
308
+ const filePath = import_node_path2.default.join(dir, `${slugify(title)}.md`);
778
309
  const now = (/* @__PURE__ */ new Date()).toISOString();
779
310
  const body = opts.body ?? `# ${title}
780
311
  `;
781
- await import_promises5.default.mkdir(dir, { recursive: true });
782
- await import_promises5.default.writeFile(filePath, serializeArticle({ title, tags: opts.tags, created: now, updated: now, body }), "utf8");
783
- void fireHook2("article-created", name, import_node_path3.default.relative(dir, filePath));
312
+ await import_promises4.default.mkdir(dir, { recursive: true });
313
+ await import_promises4.default.writeFile(filePath, serializeArticle({ title, tags: opts.tags, created: now, updated: now, body }), "utf8");
784
314
  return { title, tags: opts.tags, path: filePath, collection: name };
785
315
  }
786
316
  async function loadArticle(target) {
787
317
  const { collection, file } = parsePathTarget(target);
788
318
  const { dir } = await resolveCollectionDir(collection);
789
- const filePath = import_node_path3.default.join(dir, ensureMdPath(file));
319
+ const filePath = import_node_path2.default.join(dir, ensureMdPath(file));
790
320
  try {
791
- return { filePath, dir, raw: await import_promises5.default.readFile(filePath, "utf8"), collection };
321
+ return { filePath, raw: await import_promises4.default.readFile(filePath, "utf8"), collection };
792
322
  } catch (err) {
793
323
  if (err.code === "ENOENT") throw new ArticleNotFoundError(target);
794
324
  throw err;
795
325
  }
796
326
  }
797
327
  async function updateArticleBody(target, body) {
798
- const { filePath, dir, raw, collection } = await loadArticle(target);
328
+ const { filePath, raw, collection } = await loadArticle(target);
799
329
  const parsed = parseArticle(raw);
800
- await import_promises5.default.writeFile(
330
+ await import_promises4.default.writeFile(
801
331
  filePath,
802
332
  serializeArticle({
803
333
  title: parsed.title,
@@ -808,14 +338,13 @@ async function updateArticleBody(target, body) {
808
338
  }),
809
339
  "utf8"
810
340
  );
811
- void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
812
341
  return { title: parsed.title, tags: parsed.tags, path: filePath, collection };
813
342
  }
814
343
  async function mutateTags(target, fn) {
815
- const { filePath, dir, raw, collection } = await loadArticle(target);
344
+ const { filePath, raw, collection } = await loadArticle(target);
816
345
  const parsed = parseArticle(raw);
817
346
  const tags = fn(parsed.tags);
818
- await import_promises5.default.writeFile(
347
+ await import_promises4.default.writeFile(
819
348
  filePath,
820
349
  serializeArticle({
821
350
  title: parsed.title,
@@ -826,7 +355,6 @@ async function mutateTags(target, fn) {
826
355
  }),
827
356
  "utf8"
828
357
  );
829
- void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
830
358
  return { title: parsed.title, tags, path: filePath, collection };
831
359
  }
832
360
  function addTags(target, tags) {
@@ -840,37 +368,34 @@ function setTags(target, tags) {
840
368
  return mutateTags(target, () => Array.from(new Set(tags)));
841
369
  }
842
370
  async function removeArticle(target) {
843
- const { filePath, dir, collection } = await loadArticle(target);
844
- await import_promises5.default.rm(filePath);
845
- void fireHook2("article-removed", collection, import_node_path3.default.relative(dir, filePath));
371
+ const { filePath } = await loadArticle(target);
372
+ await import_promises4.default.rm(filePath);
846
373
  return filePath;
847
374
  }
848
375
 
849
376
  // src/repo.ts
850
- var import_promises6 = __toESM(require("fs/promises"), 1);
851
- var import_node_path4 = __toESM(require("path"), 1);
852
- var import_node_child_process2 = require("child_process");
377
+ var import_promises5 = __toESM(require("fs/promises"), 1);
378
+ var import_node_path3 = __toESM(require("path"), 1);
379
+ var import_node_child_process = require("child_process");
853
380
  var import_node_util = require("util");
854
381
  var import_yaml2 = require("yaml");
855
- init_collections();
856
- init_registry();
857
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
382
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
858
383
  var REPO_CONFIG_FILE = "diffwiki.yaml";
859
384
  async function detectRepoName(cwd) {
860
385
  try {
861
386
  const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd });
862
- return import_node_path4.default.basename(stdout.trim());
387
+ return import_node_path3.default.basename(stdout.trim());
863
388
  } catch {
864
- return import_node_path4.default.basename(import_node_path4.default.resolve(cwd));
389
+ return import_node_path3.default.basename(import_node_path3.default.resolve(cwd));
865
390
  }
866
391
  }
867
392
  async function readRepoConfig(cwd) {
868
393
  try {
869
- const raw = await import_promises6.default.readFile(import_node_path4.default.join(cwd, REPO_CONFIG_FILE), "utf8");
394
+ const raw = await import_promises5.default.readFile(import_node_path3.default.join(cwd, REPO_CONFIG_FILE), "utf8");
870
395
  const parsed = (0, import_yaml2.parse)(raw) ?? {};
871
396
  if (!parsed.path) return void 0;
872
397
  return {
873
- collection: parsed.collection ?? import_node_path4.default.basename(import_node_path4.default.resolve(cwd)),
398
+ collection: parsed.collection ?? import_node_path3.default.basename(import_node_path3.default.resolve(cwd)),
874
399
  path: parsed.path
875
400
  };
876
401
  } catch (err) {
@@ -879,9 +404,9 @@ async function readRepoConfig(cwd) {
879
404
  }
880
405
  }
881
406
  async function initRepoWiki(opts) {
882
- const cwd = import_node_path4.default.resolve(opts.cwd);
407
+ const cwd = import_node_path3.default.resolve(opts.cwd);
883
408
  const wikiPath = opts.wikiPath ?? "wiki";
884
- const absWiki = import_node_path4.default.join(cwd, wikiPath);
409
+ const absWiki = import_node_path3.default.join(cwd, wikiPath);
885
410
  let name = opts.collection ?? await detectRepoName(cwd);
886
411
  const existing = await findCollection(name);
887
412
  if (existing && existing.path !== absWiki) {
@@ -896,9 +421,9 @@ async function initRepoWiki(opts) {
896
421
  n += 1;
897
422
  }
898
423
  }
899
- await import_promises6.default.mkdir(absWiki, { recursive: true });
424
+ await import_promises5.default.mkdir(absWiki, { recursive: true });
900
425
  const config = { collection: name, path: wikiPath };
901
- await import_promises6.default.writeFile(import_node_path4.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml2.stringify)(config), "utf8");
426
+ await import_promises5.default.writeFile(import_node_path3.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml2.stringify)(config), "utf8");
902
427
  const entry = {
903
428
  name,
904
429
  type: "repo",
@@ -910,422 +435,44 @@ async function initRepoWiki(opts) {
910
435
  }
911
436
 
912
437
  // src/query.ts
913
- var import_promises7 = __toESM(require("fs/promises"), 1);
914
- var import_node_path5 = __toESM(require("path"), 1);
915
- init_collections();
916
- init_errors();
917
- init_logger();
918
- init_host();
919
- init_registry2();
920
- init_types();
921
- var log = getLogger("query");
922
- async function collectMarkdownFiles(dir) {
923
- let entries;
924
- try {
925
- entries = await import_promises7.default.readdir(dir, { withFileTypes: true });
926
- } catch {
927
- return [];
928
- }
929
- const files = [];
930
- for (const entry of entries) {
931
- const full = import_node_path5.default.join(dir, entry.name);
932
- if (entry.isDirectory()) {
933
- files.push(...await collectMarkdownFiles(full));
934
- } else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
935
- files.push(full);
936
- }
937
- }
938
- return files;
939
- }
940
- var K1 = 1.5;
941
- var B = 0.75;
942
- var FIELD_WEIGHTS = { title: 3, tags: 2, collection: 1.5, body: 1 };
943
- function tokenize(text) {
944
- return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
945
- }
946
- function termFreqs(tokens) {
947
- const freqs = /* @__PURE__ */ new Map();
948
- for (const t of tokens) {
949
- freqs.set(t, (freqs.get(t) ?? 0) + 1);
950
- }
951
- return freqs;
952
- }
953
- function bm25FieldScore(queryTerms, fieldTokens, avgFieldLen, idf) {
954
- const tf = termFreqs(fieldTokens);
955
- const dl = fieldTokens.length;
956
- const avgDl = avgFieldLen || 1;
957
- let score = 0;
958
- for (const term of queryTerms) {
959
- const freq = tf.get(term) ?? 0;
960
- if (freq === 0) continue;
961
- const termIdf = idf.get(term) ?? 0;
962
- const numerator = freq * (K1 + 1);
963
- const denominator = freq + K1 * (1 - B + B * (dl / avgDl));
964
- score += termIdf * (numerator / denominator);
965
- }
966
- return score;
967
- }
968
- async function nativeSearch(term, collections, collection) {
969
- let targets = collections;
438
+ var import_promises6 = __toESM(require("fs/promises"), 1);
439
+ var import_node_path4 = __toESM(require("path"), 1);
440
+ async function query(term, collection) {
441
+ let targets;
970
442
  if (collection) {
971
- const one = collections.find((c) => c.name === collection);
443
+ const one = await findCollection(collection);
972
444
  if (!one) throw new CollectionNotFoundError(collection);
973
445
  targets = [one];
446
+ } else {
447
+ targets = await listCollections();
974
448
  }
975
- const docs = [];
449
+ const needle = term.toLowerCase();
450
+ const hits = [];
976
451
  for (const entry of targets) {
977
- for (const full of await collectMarkdownFiles(entry.path)) {
978
- const fallbackTitle = import_node_path5.default.basename(full).replace(/\.mdx?$/, "");
979
- let title = fallbackTitle;
980
- let tags = [];
981
- let body = "";
982
- try {
983
- const parsed = parseArticle(await import_promises7.default.readFile(full, "utf8"));
984
- if (parsed.title) title = parsed.title;
985
- tags = parsed.tags;
986
- body = parsed.body;
987
- } catch {
988
- }
989
- docs.push({
990
- collectionName: entry.name,
991
- filePath: full,
992
- title,
993
- tags,
994
- fields: {
995
- title: tokenize(title),
996
- tags: tokenize(tags.join(" ")),
997
- collection: tokenize(entry.name),
998
- body: tokenize(body)
999
- }
1000
- });
1001
- }
1002
- }
1003
- if (!term.trim()) {
1004
- return docs.map((d) => ({
1005
- collection: d.collectionName,
1006
- path: d.filePath,
1007
- title: d.title,
1008
- tags: d.tags
1009
- }));
1010
- }
1011
- const queryTerms = tokenize(term);
1012
- if (queryTerms.length === 0) {
1013
- return docs.map((d) => ({
1014
- collection: d.collectionName,
1015
- path: d.filePath,
1016
- title: d.title,
1017
- tags: d.tags
1018
- }));
1019
- }
1020
- const docTokenSets = docs.map(
1021
- (doc) => /* @__PURE__ */ new Set([...doc.fields.title, ...doc.fields.tags, ...doc.fields.collection, ...doc.fields.body])
1022
- );
1023
- const N = docs.length;
1024
- const idf = /* @__PURE__ */ new Map();
1025
- for (const qt of queryTerms) {
1026
- let docFreq = 0;
1027
- for (const tokenSet of docTokenSets) {
1028
- if (tokenSet.has(qt)) docFreq++;
1029
- }
1030
- idf.set(qt, Math.log((N - docFreq + 0.5) / (docFreq + 0.5) + 1));
1031
- }
1032
- const avgLens = {
1033
- title: docs.reduce((sum, d) => sum + d.fields.title.length, 0) / (N || 1),
1034
- tags: docs.reduce((sum, d) => sum + d.fields.tags.length, 0) / (N || 1),
1035
- collection: docs.reduce((sum, d) => sum + d.fields.collection.length, 0) / (N || 1),
1036
- body: docs.reduce((sum, d) => sum + d.fields.body.length, 0) / (N || 1)
1037
- };
1038
- const scored = [];
1039
- for (const doc of docs) {
1040
- const titleScore = bm25FieldScore(queryTerms, doc.fields.title, avgLens.title, idf) * FIELD_WEIGHTS.title;
1041
- const tagsScore = bm25FieldScore(queryTerms, doc.fields.tags, avgLens.tags, idf) * FIELD_WEIGHTS.tags;
1042
- const collScore = bm25FieldScore(queryTerms, doc.fields.collection, avgLens.collection, idf) * FIELD_WEIGHTS.collection;
1043
- const bodyScore = bm25FieldScore(queryTerms, doc.fields.body, avgLens.body, idf) * FIELD_WEIGHTS.body;
1044
- const total = titleScore + tagsScore + collScore + bodyScore;
1045
- if (total > 0) {
1046
- scored.push({ doc, score: total });
1047
- }
1048
- }
1049
- scored.sort((a, b) => b.score - a.score);
1050
- return scored.map((s) => ({
1051
- collection: s.doc.collectionName,
1052
- path: s.doc.filePath,
1053
- title: s.doc.title,
1054
- tags: s.doc.tags,
1055
- score: s.score
1056
- }));
1057
- }
1058
- async function resolveDefaultEngine() {
1059
- const config = await readConfig();
1060
- const enabled = await listEnabledPlugins("search");
1061
- const configured = config.defaultSearch?.split(":")[0];
1062
- if (configured && enabled.some((p) => p.name === configured)) return configured;
1063
- if (enabled.length > 0) return enabled[0].name;
1064
- return NATIVE_ENGINE;
1065
- }
1066
- async function resolveDefaultSearchMode() {
1067
- const config = await readConfig();
1068
- const enabled = await listEnabledPlugins("search");
1069
- const configured = config.defaultSearch?.split(":")[0];
1070
- const configuredType = config.defaultSearch?.split(":")[1];
1071
- let engine = NATIVE_ENGINE;
1072
- if (configured && enabled.some((p) => p.name === configured)) engine = configured;
1073
- else if (enabled.length > 0) engine = enabled[0].name;
1074
- if (engine === NATIVE_ENGINE) return { engine, type: "basic" };
1075
- const record = enabled.find((p) => p.name === engine);
1076
- const searchTypes = record?.capabilities?.searchTypes ?? [];
1077
- if (searchTypes.length === 0) return { engine: NATIVE_ENGINE, type: "basic" };
1078
- if (configuredType && searchTypes.includes(configuredType)) {
1079
- return { engine, type: configuredType };
1080
- }
1081
- return { engine, type: searchTypes[0] };
1082
- }
1083
- async function search(term, opts) {
1084
- const collections = await listCollections();
1085
- const engine = opts?.engine ?? await resolveDefaultEngine();
1086
- log.debug(`resolved search engine "${engine}"`);
1087
- if (engine === NATIVE_ENGINE) {
1088
- const hits = await nativeSearch(term, collections, opts?.collection);
1089
- return { hits, engine, fellBackToNative: false };
1090
- }
1091
- const provider = await loadSearchProvider(engine);
1092
- if (!provider) {
1093
- const error = `no provider found for engine "${engine}"`;
1094
- log.warn(`engine "${engine}" unavailable, falling back to native: ${error}`);
1095
- const hits = await nativeSearch(term, collections, opts?.collection);
1096
- return { hits, engine, fellBackToNative: true, error };
1097
- }
1098
- try {
1099
- const hits = await provider.handleSearch(term, collections, opts);
1100
- return { hits, engine, fellBackToNative: false };
1101
- } catch (err) {
1102
- const message = err instanceof Error ? err.message : String(err);
1103
- log.warn(`engine "${engine}" failed, falling back to native: ${message}`);
1104
- const hits = await nativeSearch(term, collections, opts?.collection);
1105
- return { hits, engine, fellBackToNative: true, error: message };
1106
- }
1107
- }
1108
- async function query(term, opts) {
1109
- return (await search(term, opts)).hits;
1110
- }
1111
-
1112
- // src/doctor.ts
1113
- var import_promises9 = __toESM(require("fs/promises"), 1);
1114
- init_paths();
1115
- init_registry();
1116
- init_collections();
1117
-
1118
- // src/plugins/manager.ts
1119
- var import_promises8 = __toESM(require("fs/promises"), 1);
1120
- var import_node_path7 = __toESM(require("path"), 1);
1121
- init_paths();
1122
- init_errors();
1123
- init_collections();
1124
- init_registry2();
1125
- init_host();
1126
-
1127
- // src/plugins/sdk.ts
1128
- var import_node_child_process3 = require("child_process");
1129
- var import_node_util2 = require("util");
1130
- var import_node_fs = require("fs");
1131
- var import_node_path6 = require("path");
1132
- var import_node_url = require("url");
1133
- var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
1134
- async function runProcess(file, args, opts) {
1135
- const { stdout, stderr } = await execFileAsync2(file, args, {
1136
- env: opts?.env,
1137
- timeout: opts?.timeout,
1138
- // Large: qmd emits multi-MB JSON search payloads that overflow the default 1 MB.
1139
- maxBuffer: 50 * 1024 * 1024
1140
- });
1141
- return { stdout, stderr };
1142
- }
1143
-
1144
- // src/plugins/manager.ts
1145
- async function readPackageJson(file) {
1146
- try {
1147
- return JSON.parse(await import_promises8.default.readFile(file, "utf8"));
1148
- } catch {
1149
- return null;
1150
- }
1151
- }
1152
- function binOf(manifest) {
1153
- if (typeof manifest.bin === "string") return manifest.bin;
1154
- if (manifest.bin && typeof manifest.bin === "object") {
1155
- const first = Object.values(manifest.bin)[0];
1156
- return typeof first === "string" ? first : null;
1157
- }
1158
- return null;
1159
- }
1160
- async function pluginManifestDirs(nodeModules) {
1161
- let entries;
1162
- try {
1163
- entries = await import_promises8.default.readdir(nodeModules);
1164
- } catch {
1165
- return [];
1166
- }
1167
- const dirs = [];
1168
- for (const entry of entries) {
1169
- if (entry.startsWith(".")) continue;
1170
- if (entry.startsWith("@")) {
1171
- let scoped;
1172
- try {
1173
- scoped = await import_promises8.default.readdir(import_node_path7.default.join(nodeModules, entry));
1174
- } catch {
1175
- continue;
1176
- }
1177
- for (const pkg of scoped) {
1178
- if (pkg.startsWith(".")) continue;
1179
- dirs.push(import_node_path7.default.join(nodeModules, entry, pkg));
1180
- }
1181
- } else {
1182
- dirs.push(import_node_path7.default.join(nodeModules, entry));
1183
- }
1184
- }
1185
- return dirs;
1186
- }
1187
- async function ensurePluginRoot() {
1188
- const dir = pluginsDir();
1189
- await import_promises8.default.mkdir(dir, { recursive: true });
1190
- const pkgPath = import_node_path7.default.join(dir, "package.json");
1191
- if (await readPackageJson(pkgPath) === null) {
1192
- const pkg = { name: "diffwiki-plugins", private: true, version: "0.0.0" };
1193
- await import_promises8.default.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
1194
- `, "utf8");
1195
- }
1196
- return dir;
1197
- }
1198
- async function discoverSearchPackage(dir) {
1199
- const nodeModules = import_node_path7.default.join(dir, "node_modules");
1200
- for (const pkgDir of await pluginManifestDirs(nodeModules)) {
1201
- const manifest = await readPackageJson(import_node_path7.default.join(pkgDir, "package.json"));
1202
- if (!manifest?.name || manifest.diffwiki?.kind !== "search") continue;
1203
- const bin = binOf(manifest);
1204
- if (!bin) continue;
1205
- return { name: manifest.name, version: manifest.version, binAbs: import_node_path7.default.resolve(pkgDir, bin) };
1206
- }
1207
- return null;
1208
- }
1209
- async function describePlugin(command, name) {
1210
- return invokePlugin(command, "describe", {}, { name });
1211
- }
1212
- async function runSetup(command, name) {
1213
- try {
1214
- const collections = await listCollections();
1215
- const result = await invokePlugin(
1216
- command,
1217
- "setup",
1218
- {
1219
- collections: collections.map((c) => ({ name: c.name, path: c.path })),
1220
- embed: false,
1221
- prefetchModels: false
1222
- },
1223
- { timeoutMs: LONG_TIMEOUT_MS, name }
1224
- );
1225
- return { ready: result?.ready === true, steps: result?.steps ?? [] };
1226
- } catch (err) {
1227
- return { ready: false, steps: [{ step: "setup", ok: false, message: err.message }] };
1228
- }
1229
- }
1230
- async function installPlugin(spec, opts) {
1231
- const dir = await ensurePluginRoot();
1232
- try {
1233
- await runProcess("npm", ["install", spec, "--prefix", dir]);
1234
- } catch (err) {
1235
- throw new PluginError(`npm install ${spec} failed: ${err.message}`);
1236
- }
1237
- const discovered = await discoverSearchPackage(dir);
1238
- if (!discovered) {
1239
- throw new PluginError(
1240
- `no diffwiki search plugin found in "${spec}" (need package.json diffwiki.kind === "search" + bin)`
1241
- );
1242
- }
1243
- const command = ["node", discovered.binAbs];
1244
- let capabilities;
1245
- try {
1246
- const desc = await describePlugin(command, discovered.name);
1247
- if (desc.kind !== "search") {
1248
- throw new PluginError(`plugin "${discovered.name}" reported kind "${desc.kind}", expected "search"`);
1249
- }
1250
- capabilities = desc.capabilities;
1251
- } catch (err) {
1252
- if (err instanceof PluginError) throw err;
1253
- throw new PluginError(`plugin "${discovered.name}" failed describe: ${err.message}`);
1254
- }
1255
- const record = {
1256
- name: discovered.name,
1257
- kind: "search",
1258
- command,
1259
- version: discovered.version,
1260
- capabilities,
1261
- enabled: true,
1262
- managed: true
1263
- };
1264
- await upsertPlugin(record);
1265
- const setup = opts?.setup !== false ? await runSetup(command, discovered.name) : void 0;
1266
- return { record, setup };
1267
- }
1268
- async function registerPlugin(name, command) {
1269
- if (command.length === 0) throw new PluginError(`register "${name}": --command must not be empty`);
1270
- let capabilities;
1271
- try {
1272
- const desc = await describePlugin(command, name);
1273
- if (desc.kind !== "search") {
1274
- throw new PluginError(`plugin "${name}" reported kind "${desc.kind}", expected "search"`);
1275
- }
1276
- capabilities = desc.capabilities;
1277
- } catch (err) {
1278
- if (err instanceof PluginError) throw err;
1279
- throw new PluginError(`plugin "${name}" did not respond to describe (command: ${command.join(" ")})`);
1280
- }
1281
- const record = {
1282
- name,
1283
- kind: "search",
1284
- command,
1285
- capabilities,
1286
- enabled: true,
1287
- managed: false
1288
- };
1289
- await upsertPlugin(record);
1290
- await runSetup(command, name);
1291
- return record;
1292
- }
1293
- async function removePlugin(name) {
1294
- const record = (await readPluginRegistry()).plugins.find((p) => p.name === name);
1295
- if (!record) throw new PluginError(`plugin "${name}" is not registered`);
1296
- if (record.managed) {
1297
- const dir = await ensurePluginRoot();
452
+ let files;
1298
453
  try {
1299
- await runProcess("npm", ["uninstall", name, "--prefix", dir]);
454
+ files = await import_promises6.default.readdir(entry.path);
1300
455
  } catch {
456
+ continue;
457
+ }
458
+ for (const file of files) {
459
+ if (!/\.mdx?$/.test(file)) continue;
460
+ const full = import_node_path4.default.join(entry.path, file);
461
+ const parsed = parseArticle(await import_promises6.default.readFile(full, "utf8"));
462
+ const haystack = [parsed.title, file, ...parsed.tags].join(" ").toLowerCase();
463
+ if (!needle || haystack.includes(needle)) {
464
+ hits.push({ collection: entry.name, path: full, title: parsed.title, tags: parsed.tags });
465
+ }
1301
466
  }
1302
467
  }
1303
- await deregisterPlugin(name);
1304
- }
1305
- async function probeDescribable(command, name) {
1306
- try {
1307
- await describePlugin(command, name);
1308
- return true;
1309
- } catch {
1310
- return false;
1311
- }
1312
- }
1313
- async function listPlugins() {
1314
- const registry = await readPluginRegistry();
1315
- const out = [];
1316
- for (const record of registry.plugins) {
1317
- const spawnable = record.enabled ? await probeDescribable(record.command, record.name) : false;
1318
- out.push({ ...record, spawnable });
1319
- }
1320
- return out;
468
+ return hits;
1321
469
  }
1322
470
 
1323
471
  // src/doctor.ts
1324
- init_host();
1325
- init_types();
472
+ var import_promises7 = __toESM(require("fs/promises"), 1);
1326
473
  async function exists(target) {
1327
474
  try {
1328
- await import_promises9.default.access(target);
475
+ await import_promises7.default.access(target);
1329
476
  return true;
1330
477
  } catch {
1331
478
  return false;
@@ -1358,86 +505,12 @@ async function doctor() {
1358
505
  } else {
1359
506
  out.push({ level: "warn", message: "no default collection set" });
1360
507
  }
1361
- await appendPluginDiagnostics(out, config);
1362
508
  return out;
1363
509
  }
1364
- function readinessLine(name, health) {
1365
- const r = health.readiness;
1366
- if (!r) {
1367
- return { level: health.ready ? "ok" : "warn", message: `plugin "${name}" ${health.ready ? "ready" : "not ready"}` };
1368
- }
1369
- if (r.firstSearchCost === "model-download") {
1370
- return {
1371
- level: "warn",
1372
- message: `plugin "${name}": ${r.docsIndexed} doc(s) indexed \u2014 first search will download models (~2GB)`
1373
- };
1374
- }
1375
- if (r.firstSearchCost === "model-load") {
1376
- return {
1377
- level: "warn",
1378
- message: `plugin "${name}": ${r.docsIndexed} doc(s) indexed \u2014 first search loads models (a few seconds)`
1379
- };
1380
- }
1381
- return { level: "ok", message: `plugin "${name}": ${r.docsIndexed} doc(s) indexed \u2014 first search is instant` };
1382
- }
1383
- async function appendPluginDiagnostics(out, config) {
1384
- let plugins = [];
1385
- try {
1386
- plugins = await listPlugins();
1387
- } catch (err) {
1388
- out.push({ level: "error", message: `failed to read plugin registry: ${err.message}` });
1389
- return;
1390
- }
1391
- if (plugins.length === 0) {
1392
- out.push({ level: "warn", message: "no search plugins installed \u2014 using built-in native search" });
1393
- }
1394
- const collections = await listCollections();
1395
- for (const p of plugins) {
1396
- if (!p.enabled) {
1397
- out.push({ level: "warn", message: `plugin "${p.name}" registered but disabled` });
1398
- continue;
1399
- }
1400
- if (!p.spawnable) {
1401
- out.push({
1402
- level: "error",
1403
- message: `plugin "${p.name}" registered but its command is not spawnable: ${p.command.join(" ")}`
1404
- });
1405
- continue;
1406
- }
1407
- out.push({ level: "ok", message: `plugin "${p.name}" enabled and spawnable` });
1408
- try {
1409
- const provider = await loadSearchProvider(p.name);
1410
- if (!provider) {
1411
- out.push({ level: "error", message: `plugin "${p.name}" failed to load for a health check` });
1412
- continue;
1413
- }
1414
- const health = await provider.health(collections);
1415
- for (const d of health.diagnostics) out.push({ level: d.level, message: `${p.name} \xB7 ${d.message}` });
1416
- out.push(readinessLine(p.name, health));
1417
- } catch (err) {
1418
- out.push({ level: "error", message: `plugin "${p.name}" health check failed: ${err.message}` });
1419
- }
1420
- }
1421
- if (config.defaultSearch) {
1422
- const engine = config.defaultSearch.split(":")[0];
1423
- const known = engine === NATIVE_ENGINE || plugins.some((p) => p.name === engine && p.enabled);
1424
- if (!known) {
1425
- out.push({
1426
- level: "warn",
1427
- message: `defaultSearch "${config.defaultSearch}" is not installed/enabled \u2014 using native`
1428
- });
1429
- }
1430
- }
1431
- }
1432
-
1433
- // src/index.ts
1434
- init_logger();
1435
510
 
1436
511
  // src/browse.ts
1437
- var import_promises10 = __toESM(require("fs/promises"), 1);
1438
- var import_node_path8 = __toESM(require("path"), 1);
1439
- init_collections();
1440
- init_errors();
512
+ var import_promises8 = __toESM(require("fs/promises"), 1);
513
+ var import_node_path5 = __toESM(require("path"), 1);
1441
514
  var INDEX_RE = /^(index|readme)\.mdx?$/i;
1442
515
  function buildArticleTree(relPaths) {
1443
516
  const root = {};
@@ -1481,7 +554,7 @@ function buildArticleTree(relPaths) {
1481
554
  async function collectRelPaths(dir, rel = "") {
1482
555
  let dirents;
1483
556
  try {
1484
- dirents = await import_promises10.default.readdir(dir, { withFileTypes: true });
557
+ dirents = await import_promises8.default.readdir(dir, { withFileTypes: true });
1485
558
  } catch {
1486
559
  return [];
1487
560
  }
@@ -1490,7 +563,7 @@ async function collectRelPaths(dir, rel = "") {
1490
563
  if (d.name.startsWith(".")) continue;
1491
564
  const childRel = rel ? `${rel}/${d.name}` : d.name;
1492
565
  if (d.isDirectory()) {
1493
- out.push(...await collectRelPaths(import_node_path8.default.join(dir, d.name), childRel));
566
+ out.push(...await collectRelPaths(import_node_path5.default.join(dir, d.name), childRel));
1494
567
  } else if (/\.(mdx?)$/.test(d.name)) {
1495
568
  out.push(childRel);
1496
569
  }
@@ -1499,7 +572,7 @@ async function collectRelPaths(dir, rel = "") {
1499
572
  }
1500
573
  async function readdirSafe(dir) {
1501
574
  try {
1502
- return await import_promises10.default.readdir(dir);
575
+ return await import_promises8.default.readdir(dir);
1503
576
  } catch {
1504
577
  return [];
1505
578
  }
@@ -1516,7 +589,7 @@ function pickIndexFile(names) {
1516
589
  async function cheapTitle(absPath) {
1517
590
  let handle;
1518
591
  try {
1519
- handle = await import_promises10.default.open(absPath, "r");
592
+ handle = await import_promises8.default.open(absPath, "r");
1520
593
  const buf = Buffer.alloc(512);
1521
594
  const { bytesRead } = await handle.read(buf, 0, 512, 0);
1522
595
  const snippet = buf.subarray(0, bytesRead).toString("utf8");
@@ -1535,14 +608,14 @@ async function enrichTitles(nodes, collDir) {
1535
608
  const children = await enrichTitles(node.children, collDir);
1536
609
  let title = node.title;
1537
610
  if (node.slug) {
1538
- const dir = import_node_path8.default.join(collDir, node.slug);
611
+ const dir = import_node_path5.default.join(collDir, node.slug);
1539
612
  const idx = pickIndexFile(await readdirSafe(dir));
1540
- if (idx) title = await cheapTitle(import_node_path8.default.join(dir, idx)) ?? title;
613
+ if (idx) title = await cheapTitle(import_node_path5.default.join(dir, idx)) ?? title;
1541
614
  }
1542
615
  return { ...node, title, children };
1543
616
  }
1544
617
  for (const ext of [".mdx", ".md"]) {
1545
- const title = await cheapTitle(import_node_path8.default.join(collDir, `${node.slug}${ext}`));
618
+ const title = await cheapTitle(import_node_path5.default.join(collDir, `${node.slug}${ext}`));
1546
619
  if (title) return { ...node, title };
1547
620
  }
1548
621
  return node;
@@ -1553,38 +626,38 @@ async function listArticleTree(coll) {
1553
626
  const entry = await findCollection(coll);
1554
627
  if (!entry) throw new CollectionNotFoundError(coll);
1555
628
  const rels = await collectRelPaths(entry.path);
1556
- return enrichTitles(buildArticleTree(rels), import_node_path8.default.resolve(entry.path));
629
+ return enrichTitles(buildArticleTree(rels), import_node_path5.default.resolve(entry.path));
1557
630
  }
1558
631
  async function readArticle(coll, slug) {
1559
632
  const entry = await findCollection(coll);
1560
633
  if (!entry) throw new CollectionNotFoundError(coll);
1561
- const collDir = import_node_path8.default.resolve(entry.path);
634
+ const collDir = import_node_path5.default.resolve(entry.path);
1562
635
  const normalized = slug.replace(/\\/g, "/");
1563
636
  if (normalized.split("/").some((s) => s === "..")) {
1564
637
  throw new InvalidTargetError(slug, "path traversal detected");
1565
638
  }
1566
639
  const safeSlug = normalized.split("/").filter((s) => s !== "" && s !== ".").join("/");
1567
- const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path8.default.sep);
640
+ const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path5.default.sep);
1568
641
  let abs;
1569
642
  for (const rel of safeSlug ? [`${safeSlug}.mdx`, `${safeSlug}.md`] : []) {
1570
- const candidate = import_node_path8.default.resolve(collDir, rel);
643
+ const candidate = import_node_path5.default.resolve(collDir, rel);
1571
644
  if (!inJail(candidate)) throw new InvalidTargetError(slug, "path traversal detected");
1572
645
  try {
1573
- await import_promises10.default.access(candidate);
646
+ await import_promises8.default.access(candidate);
1574
647
  abs = candidate;
1575
648
  break;
1576
649
  } catch {
1577
650
  }
1578
651
  }
1579
652
  if (!abs) {
1580
- const dir = import_node_path8.default.resolve(collDir, safeSlug);
653
+ const dir = import_node_path5.default.resolve(collDir, safeSlug);
1581
654
  if (inJail(dir)) {
1582
655
  const idx = pickIndexFile(await readdirSafe(dir));
1583
- if (idx) abs = import_node_path8.default.join(dir, idx);
656
+ if (idx) abs = import_node_path5.default.join(dir, idx);
1584
657
  }
1585
658
  }
1586
659
  if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
1587
- const parsed = parseArticle(await import_promises10.default.readFile(abs, "utf8"));
660
+ const parsed = parseArticle(await import_promises8.default.readFile(abs, "utf8"));
1588
661
  const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
1589
662
  return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs };
1590
663
  }
@@ -1652,34 +725,6 @@ async function renderArticle(body) {
1652
725
  }).use(import_rehype_stringify.default).process(body);
1653
726
  return { html: String(file), toc };
1654
727
  }
1655
-
1656
- // src/plugins/index.ts
1657
- init_host();
1658
- init_events();
1659
-
1660
- // src/plugins/catalog.ts
1661
- var KNOWN_PLUGINS = [
1662
- {
1663
- name: "diffwiki-qmd",
1664
- kind: "search",
1665
- spec: "diffwiki-qmd",
1666
- description: "Local hybrid search \u2014 keyword, semantic, and reranked hybrid via the qmd CLI",
1667
- requires: "qmd (npm i -g @tobilu/qmd)"
1668
- },
1669
- {
1670
- name: "diffwiki-ripgrep",
1671
- kind: "search",
1672
- spec: "diffwiki-ripgrep",
1673
- description: "Instant literal/regex search via ripgrep \u2014 no index, no models",
1674
- requires: "rg (ripgrep)"
1675
- }
1676
- ];
1677
- function listAvailablePlugins() {
1678
- return [...KNOWN_PLUGINS];
1679
- }
1680
-
1681
- // src/plugins/index.ts
1682
- init_registry2();
1683
728
  // Annotate the CommonJS export names for ESM import in node:
1684
729
  0 && (module.exports = {
1685
730
  ArticleNotFoundError,
@@ -1687,9 +732,7 @@ init_registry2();
1687
732
  CollectionNotFoundError,
1688
733
  DiffwikiError,
1689
734
  InvalidTargetError,
1690
- NATIVE_ENGINE,
1691
735
  NoDefaultCollectionError,
1692
- PluginError,
1693
736
  REPO_CONFIG_FILE,
1694
737
  addTags,
1695
738
  buildArticleTree,
@@ -1698,56 +741,33 @@ init_registry2();
1698
741
  configPath,
1699
742
  createArticle,
1700
743
  createCollection,
1701
- deregisterPlugin,
1702
744
  doctor,
1703
- emitPluginEvent,
1704
- ensurePluginRoot,
1705
745
  findCollection,
1706
- findEnabledPlugin,
1707
- findPluginRecord,
1708
746
  getConfigValue,
1709
- getLogger,
1710
747
  initRepoWiki,
1711
- installPlugin,
1712
748
  listArticleTree,
1713
- listAvailablePlugins,
1714
749
  listCollections,
1715
- listEnabledPlugins,
1716
- listPlugins,
1717
- listSearchModes,
1718
- loadSearchProvider,
1719
- nativeSearch,
1720
750
  parseAddTarget,
1721
751
  parseArticle,
1722
752
  parsePathTarget,
1723
- pluginsDir,
1724
- pluginsRegistryPath,
1725
753
  query,
1726
754
  readArticle,
1727
755
  readConfig,
1728
- readPluginRegistry,
1729
756
  readRegistry,
1730
757
  readRepoConfig,
1731
758
  registerCollection,
1732
- registerPlugin,
1733
759
  registryPath,
1734
760
  removeArticle,
1735
761
  removeCollection,
1736
- removePlugin,
1737
762
  removeTags,
1738
763
  renderArticle,
1739
- resolveDefaultSearchMode,
1740
764
  resolveHome,
1741
- resolveLogLevel,
1742
- search,
1743
765
  serializeArticle,
1744
766
  setConfigValue,
1745
767
  setTags,
1746
768
  slugify,
1747
769
  unregisterCollection,
1748
770
  updateArticleBody,
1749
- upsertPlugin,
1750
771
  writeConfig,
1751
- writePluginRegistry,
1752
772
  writeRegistry
1753
773
  });