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