diffwiki-core 0.4.0 → 0.5.0-rc.202607230329.fb70592

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,91 +30,67 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
30
- // src/index.ts
31
- var index_exports = {};
32
- __export(index_exports, {
33
- ArticleNotFoundError: () => ArticleNotFoundError,
34
- CollectionExistsError: () => CollectionExistsError,
35
- CollectionNotFoundError: () => CollectionNotFoundError,
36
- DiffwikiError: () => DiffwikiError,
37
- InvalidTargetError: () => InvalidTargetError,
38
- NoDefaultCollectionError: () => NoDefaultCollectionError,
39
- REPO_CONFIG_FILE: () => REPO_CONFIG_FILE,
40
- addTags: () => addTags,
41
- 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
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
+ }
76
40
  });
77
- module.exports = __toCommonJS(index_exports);
78
41
 
79
42
  // src/errors.ts
80
- var DiffwikiError = class extends Error {
81
- constructor(message) {
82
- super(message);
83
- this.name = new.target.name;
84
- }
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}`);
43
+ var DiffwikiError, CollectionExistsError, CollectionNotFoundError, NoDefaultCollectionError, ArticleNotFoundError, InvalidTargetError, PluginError, WorktreeNotAllowedError, CollectionAlreadyAddedError;
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
+ WorktreeNotAllowedError = class extends DiffwikiError {
81
+ constructor(root) {
82
+ super(`refusing to add a linked worktree \u2014 add the source repository instead (${root})`);
83
+ }
84
+ };
85
+ CollectionAlreadyAddedError = class extends DiffwikiError {
86
+ constructor(name) {
87
+ super(`this repository is already added as collection "${name}"`);
88
+ }
89
+ };
109
90
  }
110
- };
91
+ });
111
92
 
112
93
  // src/paths.ts
113
- var import_node_os = __toESM(require("os"), 1);
114
- var import_node_path = __toESM(require("path"), 1);
115
94
  function resolveHome() {
116
95
  const override = process.env.DIFFWIKI_HOME;
117
96
  return override && override.length > 0 ? override : import_node_path.default.join(import_node_os.default.homedir(), ".diffwiki");
@@ -128,15 +107,22 @@ function collectionsDir() {
128
107
  function collectionDir(name) {
129
108
  return import_node_path.default.join(collectionsDir(), name);
130
109
  }
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);
110
+ function pluginsDir() {
111
+ return import_node_path.default.join(resolveHome(), "plugins");
112
+ }
113
+ function pluginsRegistryPath() {
114
+ return import_node_path.default.join(resolveHome(), "plugins.json");
115
+ }
116
+ var import_node_os, import_node_path;
117
+ var init_paths = __esm({
118
+ "src/paths.ts"() {
119
+ "use strict";
120
+ import_node_os = __toESM(require("os"), 1);
121
+ import_node_path = __toESM(require("path"), 1);
122
+ }
123
+ });
137
124
 
138
125
  // src/registry.ts
139
- var import_promises = __toESM(require("fs/promises"), 1);
140
126
  function isMissing(err) {
141
127
  return err?.code === "ENOENT";
142
128
  }
@@ -169,18 +155,372 @@ async function unregisterCollection(name) {
169
155
  registry.collections = registry.collections.filter((c) => c.name !== name);
170
156
  await writeRegistry(registry);
171
157
  }
158
+ var import_promises;
159
+ var init_registry = __esm({
160
+ "src/registry.ts"() {
161
+ "use strict";
162
+ import_promises = __toESM(require("fs/promises"), 1);
163
+ init_paths();
164
+ }
165
+ });
166
+
167
+ // src/plugins/registry.ts
168
+ function isMissing2(err) {
169
+ return err?.code === "ENOENT";
170
+ }
171
+ async function readPluginRegistry() {
172
+ try {
173
+ const parsed = JSON.parse(await import_promises2.default.readFile(pluginsRegistryPath(), "utf8"));
174
+ return { version: parsed.version ?? 1, plugins: parsed.plugins ?? [] };
175
+ } catch (err) {
176
+ if (isMissing2(err)) return { version: 1, plugins: [] };
177
+ throw err;
178
+ }
179
+ }
180
+ async function writePluginRegistry(registry) {
181
+ await import_promises2.default.mkdir(resolveHome(), { recursive: true });
182
+ await import_promises2.default.writeFile(pluginsRegistryPath(), `${JSON.stringify(registry, null, 2)}
183
+ `, "utf8");
184
+ }
185
+ async function upsertPlugin(record) {
186
+ const registry = await readPluginRegistry();
187
+ const index = registry.plugins.findIndex((p) => p.name === record.name);
188
+ if (index >= 0) {
189
+ registry.plugins[index] = record;
190
+ } else {
191
+ registry.plugins.push(record);
192
+ }
193
+ await writePluginRegistry(registry);
194
+ }
195
+ async function deregisterPlugin(name) {
196
+ const registry = await readPluginRegistry();
197
+ registry.plugins = registry.plugins.filter((p) => p.name !== name);
198
+ await writePluginRegistry(registry);
199
+ }
200
+ async function listEnabledPlugins(kind) {
201
+ const registry = await readPluginRegistry();
202
+ return registry.plugins.filter((p) => p.enabled && p.kind === kind);
203
+ }
204
+ async function findEnabledPlugin(kind, name) {
205
+ const enabled = await listEnabledPlugins(kind);
206
+ if (name) return enabled.find((p) => p.name === name);
207
+ return enabled[0];
208
+ }
209
+ async function findPluginRecord(name) {
210
+ const registry = await readPluginRegistry();
211
+ return registry.plugins.find((p) => p.name === name);
212
+ }
213
+ var import_promises2;
214
+ var init_registry2 = __esm({
215
+ "src/plugins/registry.ts"() {
216
+ "use strict";
217
+ import_promises2 = __toESM(require("fs/promises"), 1);
218
+ init_paths();
219
+ }
220
+ });
221
+
222
+ // src/logger.ts
223
+ function getLogger(scope) {
224
+ const parts = Array.isArray(scope) ? scope : [scope];
225
+ return (0, import_logtape.getLogger)(["diffwiki", ...parts]);
226
+ }
227
+ function resolveLogLevel(env = process.env) {
228
+ const raw = (env.DIFFWIKI_LOG ?? "").toLowerCase();
229
+ return LEVELS.includes(raw) ? raw : DEFAULT_LEVEL;
230
+ }
231
+ var import_logtape, LEVELS, DEFAULT_LEVEL;
232
+ var init_logger = __esm({
233
+ "src/logger.ts"() {
234
+ "use strict";
235
+ import_logtape = require("@logtape/logtape");
236
+ LEVELS = ["trace", "debug", "info", "warning", "error", "fatal"];
237
+ DEFAULT_LEVEL = "warning";
238
+ }
239
+ });
240
+
241
+ // src/plugins/host.ts
242
+ async function invokePlugin(command, op, params, opts) {
243
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
244
+ const scope = opts?.name ?? (0, import_node_path2.basename)(command[1] ?? command[0] ?? "plugin");
245
+ return new Promise((resolve, reject) => {
246
+ const child = (0, import_node_child_process.spawn)(command[0], command.slice(1), {
247
+ stdio: ["pipe", "pipe", "pipe"]
248
+ });
249
+ let stdout = "";
250
+ let stderr = "";
251
+ let stderrLine = "";
252
+ let settled = false;
253
+ const forwardLog = (line) => {
254
+ const trimmedLine = line.trim();
255
+ if (!trimmedLine.startsWith("{")) return;
256
+ let parsed;
257
+ try {
258
+ parsed = JSON.parse(trimmedLine);
259
+ } catch {
260
+ return;
261
+ }
262
+ const entry = parsed.log;
263
+ if (!entry || typeof entry.message !== "string") return;
264
+ const rawLevel = typeof entry.level === "string" ? entry.level.toLowerCase() : "";
265
+ const method = LEVEL_METHOD[rawLevel] ?? "info";
266
+ getLogger(["plugin", scope])[method](entry.message);
267
+ };
268
+ const timer = setTimeout(() => {
269
+ if (settled) return;
270
+ settled = true;
271
+ child.kill("SIGKILL");
272
+ reject(new Error(`plugin "${op}" timed out after ${timeoutMs}ms`));
273
+ }, timeoutMs);
274
+ const settle = (fn) => {
275
+ if (settled) return;
276
+ settled = true;
277
+ clearTimeout(timer);
278
+ fn();
279
+ };
280
+ child.stdout.setEncoding("utf8");
281
+ child.stdout.on("data", (chunk) => {
282
+ stdout += chunk;
283
+ });
284
+ child.stderr.setEncoding("utf8");
285
+ child.stderr.on("data", (chunk) => {
286
+ stderr += chunk;
287
+ stderrLine += chunk;
288
+ let nl;
289
+ while ((nl = stderrLine.indexOf("\n")) !== -1) {
290
+ forwardLog(stderrLine.slice(0, nl));
291
+ stderrLine = stderrLine.slice(nl + 1);
292
+ }
293
+ });
294
+ child.on("error", (err) => {
295
+ settle(() => reject(new Error(`plugin spawn failed: ${err.message}`)));
296
+ });
297
+ child.on("close", (code) => {
298
+ if (stderrLine.length > 0) {
299
+ forwardLog(stderrLine);
300
+ stderrLine = "";
301
+ }
302
+ settle(() => {
303
+ const trimmed = stdout.trim();
304
+ if (!trimmed) {
305
+ reject(
306
+ new Error(`plugin "${op}" produced no output (exit ${code})${stderr ? ": " + stderr.slice(0, 500) : ""}`)
307
+ );
308
+ return;
309
+ }
310
+ let response;
311
+ try {
312
+ response = JSON.parse(trimmed);
313
+ } catch {
314
+ reject(new Error(`plugin "${op}" returned invalid JSON: ${trimmed.slice(0, 200)}`));
315
+ return;
316
+ }
317
+ if (response.ok) {
318
+ resolve(response.result);
319
+ } else {
320
+ const message = typeof response.error?.message === "string" ? response.error.message : `plugin "${op}" failed`;
321
+ reject(new Error(message));
322
+ }
323
+ });
324
+ });
325
+ const request = JSON.stringify({ op, params });
326
+ child.stdin.write(request + "\n", (err) => {
327
+ if (err && !settled) {
328
+ settle(() => reject(new Error(`failed to write to plugin stdin: ${err.message}`)));
329
+ return;
330
+ }
331
+ child.stdin.end();
332
+ });
333
+ });
334
+ }
335
+ function toWireCollections(collections) {
336
+ return collections.map((c) => ({ name: c.name, path: c.path }));
337
+ }
338
+ function mapHits(result, collections) {
339
+ const raw = result?.hits;
340
+ const hits = Array.isArray(raw) ? raw : [];
341
+ const out = [];
342
+ for (const item of hits) {
343
+ if (typeof item !== "object" || item === null) continue;
344
+ const h = item;
345
+ if (typeof h.collection !== "string" || typeof h.relPath !== "string") continue;
346
+ const title = typeof h.title === "string" ? h.title : h.relPath;
347
+ const coll = collections.find((c) => c.name === h.collection);
348
+ const abs = coll ? import_node_path2.default.join(coll.path, h.relPath) : h.relPath;
349
+ out.push({
350
+ collection: h.collection,
351
+ path: abs,
352
+ title,
353
+ tags: Array.isArray(h.tags) ? h.tags.filter((t) => typeof t === "string") : [],
354
+ snippet: typeof h.snippet === "string" ? h.snippet : void 0,
355
+ score: typeof h.score === "number" ? h.score : void 0,
356
+ docid: typeof h.docid === "string" ? h.docid : void 0
357
+ });
358
+ }
359
+ return out;
360
+ }
361
+ async function loadSearchProvider(name) {
362
+ const record = await findEnabledPlugin("search", name);
363
+ if (!record) return null;
364
+ let capabilities;
365
+ try {
366
+ const desc = await invokePlugin(record.command, "describe", {}, { name: record.name });
367
+ if (desc.kind !== "search") {
368
+ return null;
369
+ }
370
+ capabilities = desc.capabilities;
371
+ } catch {
372
+ return null;
373
+ }
374
+ const provider = {
375
+ name: record.name,
376
+ capabilities,
377
+ async handleSearch(term, collections, opts) {
378
+ const result = await invokePlugin(
379
+ record.command,
380
+ "search",
381
+ {
382
+ term,
383
+ collections: toWireCollections(collections),
384
+ collection: opts?.collection,
385
+ type: opts?.type,
386
+ limit: opts?.limit
387
+ },
388
+ { timeoutMs: SEARCH_TIMEOUT_MS, name: record.name }
389
+ );
390
+ return mapHits(result, collections);
391
+ },
392
+ async reindex(collections, opts) {
393
+ const result = await invokePlugin(
394
+ record.command,
395
+ "index",
396
+ {
397
+ collections: toWireCollections(collections),
398
+ embed: opts?.embed
399
+ },
400
+ { timeoutMs: LONG_TIMEOUT_MS, name: record.name }
401
+ );
402
+ return result?.indexed ?? 0;
403
+ },
404
+ async emitEvent(event, collection, relPath, collections) {
405
+ try {
406
+ const entry = collections.find((c) => c.name === collection);
407
+ await invokePlugin(
408
+ record.command,
409
+ "event",
410
+ {
411
+ event,
412
+ collection: entry ? { name: entry.name, path: entry.path } : { name: collection, path: "" },
413
+ relPath,
414
+ collections: toWireCollections(collections)
415
+ },
416
+ { name: record.name }
417
+ );
418
+ } catch {
419
+ }
420
+ },
421
+ async health(collections) {
422
+ const result = await invokePlugin(record.command, "health", { collections: toWireCollections(collections) }, { name: record.name });
423
+ return {
424
+ ready: result?.ready === true,
425
+ readiness: result?.readiness,
426
+ diagnostics: result?.diagnostics ?? []
427
+ };
428
+ }
429
+ };
430
+ return provider;
431
+ }
432
+ async function listSearchModes() {
433
+ const modes = [{ plugin: NATIVE_ENGINE, type: "basic", label: "native \xB7 bm25" }];
434
+ const plugins = await listEnabledPlugins("search");
435
+ for (const record of plugins) {
436
+ const caps = record.capabilities;
437
+ if (!caps) continue;
438
+ for (const type of caps.searchTypes) {
439
+ modes.push({ plugin: record.name, type, label: `${record.name} \xB7 ${type}` });
440
+ }
441
+ }
442
+ return modes;
443
+ }
444
+ var import_node_child_process, import_node_path2, LEVEL_METHOD, DEFAULT_TIMEOUT_MS, SEARCH_TIMEOUT_MS, LONG_TIMEOUT_MS;
445
+ var init_host = __esm({
446
+ "src/plugins/host.ts"() {
447
+ "use strict";
448
+ import_node_child_process = require("child_process");
449
+ import_node_path2 = __toESM(require("path"), 1);
450
+ init_logger();
451
+ init_types();
452
+ init_registry2();
453
+ LEVEL_METHOD = {
454
+ trace: "trace",
455
+ debug: "debug",
456
+ info: "info",
457
+ warn: "warn",
458
+ warning: "warn",
459
+ error: "error",
460
+ fatal: "fatal"
461
+ };
462
+ DEFAULT_TIMEOUT_MS = 3e4;
463
+ SEARCH_TIMEOUT_MS = 12e4;
464
+ LONG_TIMEOUT_MS = 18e5;
465
+ }
466
+ });
467
+
468
+ // src/plugins/events.ts
469
+ var events_exports = {};
470
+ __export(events_exports, {
471
+ emitPluginEvent: () => emitPluginEvent
472
+ });
473
+ async function emitPluginEvent(event, collection, relPath) {
474
+ try {
475
+ const record = await findEnabledPlugin("search");
476
+ if (!record) return;
477
+ if (!record.capabilities?.events) return;
478
+ const collections = await listCollections();
479
+ const entry = collections.find((c) => c.name === collection);
480
+ await invokePlugin(
481
+ record.command,
482
+ "event",
483
+ {
484
+ event,
485
+ collection: entry ? { name: entry.name, path: entry.path } : { name: collection, path: "" },
486
+ relPath,
487
+ collections: collections.map((c) => ({ name: c.name, path: c.path }))
488
+ },
489
+ { name: record.name }
490
+ );
491
+ } catch {
492
+ }
493
+ }
494
+ var init_events = __esm({
495
+ "src/plugins/events.ts"() {
496
+ "use strict";
497
+ init_collections();
498
+ init_registry2();
499
+ init_host();
500
+ }
501
+ });
172
502
 
173
503
  // src/collections.ts
504
+ async function fireHook(event, collection) {
505
+ try {
506
+ const { emitPluginEvent: emitPluginEvent2 } = await Promise.resolve().then(() => (init_events(), events_exports));
507
+ await emitPluginEvent2(event, collection);
508
+ } catch {
509
+ }
510
+ }
174
511
  async function listCollections() {
175
512
  return (await readRegistry()).collections;
176
513
  }
177
514
  async function findCollection(name) {
178
515
  return (await readRegistry()).collections.find((c) => c.name === name);
179
516
  }
517
+ async function findCollectionById(id) {
518
+ return (await readRegistry()).collections.find((c) => c.id === id);
519
+ }
180
520
  async function createCollection(name) {
181
521
  if (await findCollection(name)) throw new CollectionExistsError(name);
182
522
  const dir = collectionDir(name);
183
- await import_promises2.default.mkdir(dir, { recursive: true });
523
+ await import_promises3.default.mkdir(dir, { recursive: true });
184
524
  const entry = {
185
525
  name,
186
526
  type: "global",
@@ -188,6 +528,7 @@ async function createCollection(name) {
188
528
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
189
529
  };
190
530
  await registerCollection(entry);
531
+ void fireHook("collection-added", name);
191
532
  return entry;
192
533
  }
193
534
  async function removeCollection(name) {
@@ -195,32 +536,157 @@ async function removeCollection(name) {
195
536
  if (!entry) throw new CollectionNotFoundError(name);
196
537
  await unregisterCollection(name);
197
538
  if (entry.type === "global") {
198
- await import_promises2.default.rm(entry.path, { recursive: true, force: true });
539
+ await import_promises3.default.rm(entry.path, { recursive: true, force: true });
199
540
  }
541
+ void fireHook("collection-removed", name);
200
542
  return entry;
201
543
  }
544
+ var import_promises3;
545
+ var init_collections = __esm({
546
+ "src/collections.ts"() {
547
+ "use strict";
548
+ import_promises3 = __toESM(require("fs/promises"), 1);
549
+ init_paths();
550
+ init_registry();
551
+ init_errors();
552
+ }
553
+ });
554
+
555
+ // src/index.ts
556
+ var index_exports = {};
557
+ __export(index_exports, {
558
+ ArticleNotFoundError: () => ArticleNotFoundError,
559
+ CollectionAlreadyAddedError: () => CollectionAlreadyAddedError,
560
+ CollectionExistsError: () => CollectionExistsError,
561
+ CollectionNotFoundError: () => CollectionNotFoundError,
562
+ DiffwikiError: () => DiffwikiError,
563
+ InvalidTargetError: () => InvalidTargetError,
564
+ NATIVE_ENGINE: () => NATIVE_ENGINE,
565
+ NoDefaultCollectionError: () => NoDefaultCollectionError,
566
+ PluginError: () => PluginError,
567
+ REPO_CONFIG_FILE: () => REPO_CONFIG_FILE,
568
+ WorktreeNotAllowedError: () => WorktreeNotAllowedError,
569
+ addExternalCollection: () => addExternalCollection,
570
+ addTags: () => addTags,
571
+ buildArticleTree: () => buildArticleTree,
572
+ buildCollections: () => buildCollections,
573
+ buildNavTree: () => buildNavTree,
574
+ buildSearchIndex: () => buildSearchIndex,
575
+ collectionDir: () => collectionDir,
576
+ collectionGitStatus: () => collectionGitStatus,
577
+ collectionsDir: () => collectionsDir,
578
+ configPath: () => configPath,
579
+ createArticle: () => createArticle,
580
+ createCollection: () => createCollection,
581
+ currentBranch: () => currentBranch,
582
+ deregisterPlugin: () => deregisterPlugin,
583
+ doctor: () => doctor,
584
+ emitPluginEvent: () => emitPluginEvent,
585
+ ensureGitRepo: () => ensureGitRepo,
586
+ ensurePluginRoot: () => ensurePluginRoot,
587
+ findCollection: () => findCollection,
588
+ findCollectionById: () => findCollectionById,
589
+ findEnabledPlugin: () => findEnabledPlugin,
590
+ findPluginRecord: () => findPluginRecord,
591
+ getConfigValue: () => getConfigValue,
592
+ getLogger: () => getLogger,
593
+ gitToplevel: () => gitToplevel,
594
+ initRepoWiki: () => initRepoWiki,
595
+ installPlugin: () => installPlugin,
596
+ isLinkedWorktree: () => isLinkedWorktree,
597
+ listArticleTree: () => listArticleTree,
598
+ listAvailablePlugins: () => listAvailablePlugins,
599
+ listCollections: () => listCollections,
600
+ listEnabledPlugins: () => listEnabledPlugins,
601
+ listPlugins: () => listPlugins,
602
+ listSearchModes: () => listSearchModes,
603
+ loadSearchProvider: () => loadSearchProvider,
604
+ nativeSearch: () => nativeSearch,
605
+ parseAddTarget: () => parseAddTarget,
606
+ parseArticle: () => parseArticle,
607
+ parsePathTarget: () => parsePathTarget,
608
+ pluginsDir: () => pluginsDir,
609
+ pluginsRegistryPath: () => pluginsRegistryPath,
610
+ query: () => query,
611
+ rankDocs: () => rankDocs,
612
+ readArticle: () => readArticle,
613
+ readConfig: () => readConfig,
614
+ readPluginRegistry: () => readPluginRegistry,
615
+ readRegistry: () => readRegistry,
616
+ readRepoConfig: () => readRepoConfig,
617
+ registerCollection: () => registerCollection,
618
+ registerPlugin: () => registerPlugin,
619
+ registryPath: () => registryPath,
620
+ removeArticle: () => removeArticle,
621
+ removeCollection: () => removeCollection,
622
+ removePlugin: () => removePlugin,
623
+ removeTags: () => removeTags,
624
+ renderArticle: () => renderArticle,
625
+ repoId: () => repoId,
626
+ repoRemoteName: () => repoRemoteName,
627
+ resolveCwdCollection: () => resolveCwdCollection,
628
+ resolveDefaultSearchMode: () => resolveDefaultSearchMode,
629
+ resolveHome: () => resolveHome,
630
+ resolveLogLevel: () => resolveLogLevel,
631
+ resolvePage: () => resolvePage,
632
+ search: () => search,
633
+ serializeArticle: () => serializeArticle,
634
+ setConfigValue: () => setConfigValue,
635
+ setTags: () => setTags,
636
+ slugify: () => slugify,
637
+ toTreeNodes: () => toTreeNodes,
638
+ tokenize: () => tokenize,
639
+ unregisterCollection: () => unregisterCollection,
640
+ updateArticleBody: () => updateArticleBody,
641
+ upsertPlugin: () => upsertPlugin,
642
+ upstreamStatus: () => upstreamStatus,
643
+ writeConfig: () => writeConfig,
644
+ writePluginRegistry: () => writePluginRegistry,
645
+ writeRegistry: () => writeRegistry
646
+ });
647
+ module.exports = __toCommonJS(index_exports);
648
+ init_types();
649
+ init_errors();
650
+ init_paths();
202
651
 
203
652
  // src/config.ts
204
- function isMissing2(err) {
653
+ var import_promises4 = __toESM(require("fs/promises"), 1);
654
+ init_paths();
655
+ init_collections();
656
+ init_errors();
657
+ init_registry2();
658
+ init_types();
659
+ function isMissing3(err) {
205
660
  return err?.code === "ENOENT";
206
661
  }
207
662
  async function readConfig() {
208
663
  try {
209
- return JSON.parse(await import_promises3.default.readFile(configPath(), "utf8"));
664
+ return JSON.parse(await import_promises4.default.readFile(configPath(), "utf8"));
210
665
  } catch (err) {
211
- if (isMissing2(err)) return {};
666
+ if (isMissing3(err)) return {};
212
667
  throw err;
213
668
  }
214
669
  }
215
670
  async function writeConfig(config) {
216
- await import_promises3.default.mkdir(resolveHome(), { recursive: true });
217
- await import_promises3.default.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
671
+ await import_promises4.default.mkdir(resolveHome(), { recursive: true });
672
+ await import_promises4.default.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
218
673
  `, "utf8");
219
674
  }
220
675
  async function setConfigValue(key, value) {
221
676
  if (key === "defaultCollection" && !await findCollection(value)) {
222
677
  throw new CollectionNotFoundError(value);
223
678
  }
679
+ if (key === "defaultSearch") {
680
+ const engine = value.split(":")[0];
681
+ if (engine !== NATIVE_ENGINE) {
682
+ const enabled = await listEnabledPlugins("search");
683
+ if (!enabled.some((p) => p.name === engine)) {
684
+ console.warn(
685
+ `! defaultSearch "${value}" names an engine that is not installed/enabled \u2014 native will be used until it is`
686
+ );
687
+ }
688
+ }
689
+ }
224
690
  const config = await readConfig();
225
691
  config[key] = value;
226
692
  await writeConfig(config);
@@ -231,6 +697,9 @@ async function getConfigValue(key) {
231
697
  return typeof value === "string" ? value : void 0;
232
698
  }
233
699
 
700
+ // src/index.ts
701
+ init_registry();
702
+
234
703
  // src/slugify.ts
235
704
  function slugify(input) {
236
705
  const slug = input.toLowerCase().replace(/^#+\s*/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/g, "");
@@ -248,12 +717,33 @@ var YAML_ENGINE = {
248
717
  stringify: (data) => (0, import_yaml.stringify)(data)
249
718
  };
250
719
  var MATTER_OPTS = { engines: { yaml: YAML_ENGINE } };
720
+ function parseFallback(raw) {
721
+ const fenceRe = /^---[ \t]*\n([\s\S]*?)\n---[ \t]*\n/;
722
+ const fenceMatch = fenceRe.exec(raw);
723
+ let title = "";
724
+ let body;
725
+ if (fenceMatch) {
726
+ const inner = fenceMatch[1];
727
+ const titleMatch = /^title:\s*(.+)$/m.exec(inner);
728
+ if (titleMatch) {
729
+ title = titleMatch[1].trim().replace(/^["']|["']$/g, "");
730
+ }
731
+ body = raw.slice(fenceMatch[0].length);
732
+ } else {
733
+ body = raw;
734
+ }
735
+ return { title, tags: [], body: body.replace(/^\n+/, "") };
736
+ }
251
737
  function parseArticle(raw) {
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+/, "") };
738
+ try {
739
+ const { data, content } = (0, import_gray_matter.default)(raw, MATTER_OPTS);
740
+ const title = typeof data.title === "string" ? data.title : "";
741
+ const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
742
+ const created = typeof data.created === "string" ? data.created : void 0;
743
+ return { title, tags, created, body: content.replace(/^\n+/, "") };
744
+ } catch {
745
+ return parseFallback(raw);
746
+ }
257
747
  }
258
748
  function serializeArticle(article) {
259
749
  const front = { title: article.title, tags: article.tags };
@@ -262,9 +752,21 @@ function serializeArticle(article) {
262
752
  return import_gray_matter.default.stringify(article.body, front, MATTER_OPTS);
263
753
  }
264
754
 
755
+ // src/index.ts
756
+ init_collections();
757
+
265
758
  // src/articles.ts
266
- var import_promises4 = __toESM(require("fs/promises"), 1);
267
- var import_node_path2 = __toESM(require("path"), 1);
759
+ var import_promises5 = __toESM(require("fs/promises"), 1);
760
+ var import_node_path3 = __toESM(require("path"), 1);
761
+ init_collections();
762
+ init_errors();
763
+ async function fireHook2(event, collection, relPath) {
764
+ try {
765
+ const { emitPluginEvent: emitPluginEvent2 } = await Promise.resolve().then(() => (init_events(), events_exports));
766
+ await emitPluginEvent2(event, collection, relPath);
767
+ } catch {
768
+ }
769
+ }
268
770
  function parseAddTarget(target) {
269
771
  const idx = target.indexOf(":");
270
772
  if (idx === -1) {
@@ -297,37 +799,38 @@ async function resolveCollectionDir(name) {
297
799
  }
298
800
  function ensureMdPath(file) {
299
801
  if (/\.mdx?$/.test(file)) return file;
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`);
802
+ const dir = import_node_path3.default.dirname(file);
803
+ const base = slugify(import_node_path3.default.basename(file));
804
+ const rel = dir === "." ? `${base}.md` : import_node_path3.default.join(dir, `${base}.md`);
303
805
  return rel;
304
806
  }
305
807
  async function createArticle(opts) {
306
808
  const { collection, title } = parseAddTarget(opts.target);
307
809
  const { name, dir } = await resolveCollectionDir(collection);
308
- const filePath = import_node_path2.default.join(dir, `${slugify(title)}.md`);
810
+ const filePath = import_node_path3.default.join(dir, `${slugify(title)}.md`);
309
811
  const now = (/* @__PURE__ */ new Date()).toISOString();
310
812
  const body = opts.body ?? `# ${title}
311
813
  `;
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");
814
+ await import_promises5.default.mkdir(dir, { recursive: true });
815
+ await import_promises5.default.writeFile(filePath, serializeArticle({ title, tags: opts.tags, created: now, updated: now, body }), "utf8");
816
+ void fireHook2("article-created", name, import_node_path3.default.relative(dir, filePath));
314
817
  return { title, tags: opts.tags, path: filePath, collection: name };
315
818
  }
316
819
  async function loadArticle(target) {
317
820
  const { collection, file } = parsePathTarget(target);
318
821
  const { dir } = await resolveCollectionDir(collection);
319
- const filePath = import_node_path2.default.join(dir, ensureMdPath(file));
822
+ const filePath = import_node_path3.default.join(dir, ensureMdPath(file));
320
823
  try {
321
- return { filePath, raw: await import_promises4.default.readFile(filePath, "utf8"), collection };
824
+ return { filePath, dir, raw: await import_promises5.default.readFile(filePath, "utf8"), collection };
322
825
  } catch (err) {
323
826
  if (err.code === "ENOENT") throw new ArticleNotFoundError(target);
324
827
  throw err;
325
828
  }
326
829
  }
327
830
  async function updateArticleBody(target, body) {
328
- const { filePath, raw, collection } = await loadArticle(target);
831
+ const { filePath, dir, raw, collection } = await loadArticle(target);
329
832
  const parsed = parseArticle(raw);
330
- await import_promises4.default.writeFile(
833
+ await import_promises5.default.writeFile(
331
834
  filePath,
332
835
  serializeArticle({
333
836
  title: parsed.title,
@@ -338,13 +841,14 @@ async function updateArticleBody(target, body) {
338
841
  }),
339
842
  "utf8"
340
843
  );
844
+ void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
341
845
  return { title: parsed.title, tags: parsed.tags, path: filePath, collection };
342
846
  }
343
847
  async function mutateTags(target, fn) {
344
- const { filePath, raw, collection } = await loadArticle(target);
848
+ const { filePath, dir, raw, collection } = await loadArticle(target);
345
849
  const parsed = parseArticle(raw);
346
850
  const tags = fn(parsed.tags);
347
- await import_promises4.default.writeFile(
851
+ await import_promises5.default.writeFile(
348
852
  filePath,
349
853
  serializeArticle({
350
854
  title: parsed.title,
@@ -355,6 +859,7 @@ async function mutateTags(target, fn) {
355
859
  }),
356
860
  "utf8"
357
861
  );
862
+ void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
358
863
  return { title: parsed.title, tags, path: filePath, collection };
359
864
  }
360
865
  function addTags(target, tags) {
@@ -368,45 +873,108 @@ function setTags(target, tags) {
368
873
  return mutateTags(target, () => Array.from(new Set(tags)));
369
874
  }
370
875
  async function removeArticle(target) {
371
- const { filePath } = await loadArticle(target);
372
- await import_promises4.default.rm(filePath);
876
+ const { filePath, dir, collection } = await loadArticle(target);
877
+ await import_promises5.default.rm(filePath);
878
+ void fireHook2("article-removed", collection, import_node_path3.default.relative(dir, filePath));
373
879
  return filePath;
374
880
  }
375
881
 
376
882
  // src/repo.ts
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");
380
- var import_node_util = require("util");
883
+ var import_promises6 = __toESM(require("fs/promises"), 1);
884
+ var import_node_path5 = __toESM(require("path"), 1);
885
+ var import_node_child_process3 = require("child_process");
886
+ var import_node_util2 = require("util");
381
887
  var import_yaml2 = require("yaml");
382
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
383
- var REPO_CONFIG_FILE = "diffwiki.yaml";
384
- async function detectRepoName(cwd) {
888
+ init_collections();
889
+ init_registry();
890
+
891
+ // src/git.ts
892
+ var import_node_path4 = __toESM(require("path"), 1);
893
+ var import_node_child_process2 = require("child_process");
894
+ var import_node_util = require("util");
895
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
896
+ async function git(cwd, args) {
385
897
  try {
386
- const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd });
387
- return import_node_path3.default.basename(stdout.trim());
898
+ const { stdout } = await execFileAsync("git", args, { cwd });
899
+ return stdout.trim();
388
900
  } catch {
389
- return import_node_path3.default.basename(import_node_path3.default.resolve(cwd));
901
+ return void 0;
390
902
  }
391
903
  }
392
- async function readRepoConfig(cwd) {
393
- try {
394
- const raw = await import_promises5.default.readFile(import_node_path3.default.join(cwd, REPO_CONFIG_FILE), "utf8");
395
- const parsed = (0, import_yaml2.parse)(raw) ?? {};
396
- if (!parsed.path) return void 0;
397
- return {
398
- collection: parsed.collection ?? import_node_path3.default.basename(import_node_path3.default.resolve(cwd)),
399
- path: parsed.path
400
- };
904
+ async function gitToplevel(cwd) {
905
+ return git(cwd, ["rev-parse", "--show-toplevel"]);
906
+ }
907
+ async function isLinkedWorktree(cwd) {
908
+ const gitDir = await git(cwd, ["rev-parse", "--absolute-git-dir"]);
909
+ const commonDir = await git(cwd, ["rev-parse", "--git-common-dir"]);
910
+ if (!gitDir || !commonDir) return false;
911
+ const commonAbs = import_node_path4.default.isAbsolute(commonDir) ? commonDir : import_node_path4.default.resolve(cwd, commonDir);
912
+ return import_node_path4.default.resolve(gitDir) !== import_node_path4.default.resolve(commonAbs);
913
+ }
914
+ async function repoRemoteName(cwd) {
915
+ const url = await git(cwd, ["remote", "get-url", "origin"]);
916
+ if (!url) return void 0;
917
+ const base = url.replace(/\/+$/, "").split(/[/:]/).pop() ?? "";
918
+ const name = base.replace(/\.git$/, "");
919
+ return name.length > 0 ? name : void 0;
920
+ }
921
+ async function repoId(cwd) {
922
+ const roots = await git(cwd, ["rev-list", "--max-parents=0", "HEAD"]);
923
+ if (roots) return `commit:${roots.split("\n")[0]}`;
924
+ const url = await git(cwd, ["remote", "get-url", "origin"]);
925
+ if (url) return `remote:${url.replace(/\.git$/, "").replace(/\/+$/, "")}`;
926
+ return void 0;
927
+ }
928
+ async function currentBranch(cwd) {
929
+ const name = await git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
930
+ return name && name !== "HEAD" ? name : void 0;
931
+ }
932
+ async function upstreamStatus(cwd) {
933
+ const branch = await currentBranch(cwd);
934
+ const counts = await git(cwd, ["rev-list", "--left-right", "--count", "@{upstream}...HEAD"]);
935
+ if (!counts) return { branch, ahead: 0, behind: 0, hasUpstream: false };
936
+ const [behind, ahead] = counts.split(/\s+/).map((n) => Number.parseInt(n, 10) || 0);
937
+ return { branch, ahead, behind, hasUpstream: true };
938
+ }
939
+ async function ensureGitRepo(cwd) {
940
+ const top = await gitToplevel(cwd);
941
+ if (top) return top;
942
+ await execFileAsync("git", ["init"], { cwd });
943
+ const after = await gitToplevel(cwd);
944
+ if (!after) throw new Error("git init did not produce a repository");
945
+ return after;
946
+ }
947
+
948
+ // src/repo.ts
949
+ init_errors();
950
+ var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
951
+ var REPO_CONFIG_FILE = "diffwiki.yaml";
952
+ async function detectRepoName(cwd) {
953
+ try {
954
+ const { stdout } = await execFileAsync2("git", ["rev-parse", "--show-toplevel"], { cwd });
955
+ return import_node_path5.default.basename(stdout.trim());
956
+ } catch {
957
+ return import_node_path5.default.basename(import_node_path5.default.resolve(cwd));
958
+ }
959
+ }
960
+ async function readRepoConfig(cwd) {
961
+ try {
962
+ const raw = await import_promises6.default.readFile(import_node_path5.default.join(cwd, REPO_CONFIG_FILE), "utf8");
963
+ const parsed = (0, import_yaml2.parse)(raw) ?? {};
964
+ if (!parsed.path) return void 0;
965
+ return {
966
+ collection: parsed.collection ?? import_node_path5.default.basename(import_node_path5.default.resolve(cwd)),
967
+ path: parsed.path
968
+ };
401
969
  } catch (err) {
402
970
  if (err.code === "ENOENT") return void 0;
403
971
  throw err;
404
972
  }
405
973
  }
406
974
  async function initRepoWiki(opts) {
407
- const cwd = import_node_path3.default.resolve(opts.cwd);
975
+ const cwd = import_node_path5.default.resolve(opts.cwd);
408
976
  const wikiPath = opts.wikiPath ?? "wiki";
409
- const absWiki = import_node_path3.default.join(cwd, wikiPath);
977
+ const absWiki = import_node_path5.default.join(cwd, wikiPath);
410
978
  let name = opts.collection ?? await detectRepoName(cwd);
411
979
  const existing = await findCollection(name);
412
980
  if (existing && existing.path !== absWiki) {
@@ -421,9 +989,9 @@ async function initRepoWiki(opts) {
421
989
  n += 1;
422
990
  }
423
991
  }
424
- await import_promises5.default.mkdir(absWiki, { recursive: true });
992
+ await import_promises6.default.mkdir(absWiki, { recursive: true });
425
993
  const config = { collection: name, path: wikiPath };
426
- await import_promises5.default.writeFile(import_node_path3.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml2.stringify)(config), "utf8");
994
+ await import_promises6.default.writeFile(import_node_path5.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml2.stringify)(config), "utf8");
427
995
  const entry = {
428
996
  name,
429
997
  type: "repo",
@@ -433,84 +1001,61 @@ async function initRepoWiki(opts) {
433
1001
  await registerCollection(entry);
434
1002
  return entry;
435
1003
  }
436
-
437
- // src/query.ts
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;
442
- if (collection) {
443
- const one = await findCollection(collection);
444
- if (!one) throw new CollectionNotFoundError(collection);
445
- targets = [one];
446
- } else {
447
- targets = await listCollections();
448
- }
449
- const needle = term.toLowerCase();
450
- const hits = [];
451
- for (const entry of targets) {
452
- let files;
453
- try {
454
- files = await import_promises6.default.readdir(entry.path);
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
- }
466
- }
1004
+ async function addExternalCollection(opts) {
1005
+ const cwd = import_node_path5.default.resolve(opts.cwd);
1006
+ const root = await ensureGitRepo(cwd);
1007
+ if (await isLinkedWorktree(cwd)) throw new WorktreeNotAllowedError(root);
1008
+ const id = await repoId(root) ?? `path:${root}`;
1009
+ const already = await findCollectionById(id);
1010
+ if (already) throw new CollectionAlreadyAddedError(already.name);
1011
+ const docsRel = opts.docsDir ?? "docs";
1012
+ const contentDir = import_node_path5.default.join(root, docsRel);
1013
+ let name = await repoRemoteName(root) ?? import_node_path5.default.basename(root);
1014
+ if (await findCollection(name)) {
1015
+ let n = 2;
1016
+ while (await findCollection(`${name}-${n}`)) n += 1;
1017
+ name = `${name}-${n}`;
467
1018
  }
468
- return hits;
1019
+ const entry = {
1020
+ name,
1021
+ type: "external",
1022
+ path: contentDir,
1023
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1024
+ id,
1025
+ repoRoot: root,
1026
+ docsDir: docsRel
1027
+ };
1028
+ await registerCollection(entry);
1029
+ return entry;
469
1030
  }
470
-
471
- // src/doctor.ts
472
- var import_promises7 = __toESM(require("fs/promises"), 1);
473
- async function exists(target) {
474
- try {
475
- await import_promises7.default.access(target);
476
- return true;
477
- } catch {
478
- return false;
1031
+ async function resolveCwdCollection(cwd) {
1032
+ const abs = import_node_path5.default.resolve(cwd);
1033
+ const root = await gitToplevel(abs);
1034
+ if (root) {
1035
+ const id = await repoId(root) ?? `path:${root}`;
1036
+ const byId = await findCollectionById(id);
1037
+ if (byId) return byId;
479
1038
  }
1039
+ const within = (base) => abs === base || abs.startsWith(base + import_node_path5.default.sep);
1040
+ const all = await listCollections();
1041
+ return all.find((c) => within(c.path) || c.repoRoot !== void 0 && within(c.repoRoot));
480
1042
  }
481
- async function doctor() {
482
- const out = [];
483
- const home = resolveHome();
484
- out.push(
485
- await exists(home) ? { level: "ok", message: `home directory present: ${home}` } : { level: "warn", message: `home directory missing (created on first write): ${home}` }
486
- );
487
- out.push(
488
- await exists(registryPath()) ? { level: "ok", message: `registry present: ${registryPath()}` } : { level: "warn", message: `registry missing: ${registryPath()}` }
489
- );
490
- const registry = await readRegistry();
491
- for (const c of registry.collections) {
492
- out.push(
493
- await exists(c.path) ? { level: "ok", message: `collection "${c.name}" \u2192 ${c.path}` } : { level: "error", message: `collection "${c.name}" points to missing path: ${c.path}` }
494
- );
495
- }
496
- const config = await readConfig();
497
- if (config.defaultCollection) {
498
- const found = registry.collections.some((c) => c.name === config.defaultCollection);
499
- out.push(
500
- found ? { level: "ok", message: `default collection: ${config.defaultCollection}` } : {
501
- level: "error",
502
- message: `default collection "${config.defaultCollection}" is not registered`
503
- }
504
- );
505
- } else {
506
- out.push({ level: "warn", message: "no default collection set" });
507
- }
508
- return out;
1043
+ async function collectionGitStatus(entry) {
1044
+ if (entry.type !== "external" || !entry.repoRoot) return void 0;
1045
+ const top = await gitToplevel(entry.repoRoot);
1046
+ if (!top) return void 0;
1047
+ const s = await upstreamStatus(entry.repoRoot);
1048
+ return { branch: s.branch, ahead: s.ahead, behind: s.behind, hasUpstream: s.hasUpstream };
509
1049
  }
510
1050
 
1051
+ // src/site.ts
1052
+ init_collections();
1053
+
511
1054
  // src/browse.ts
512
- var import_promises8 = __toESM(require("fs/promises"), 1);
513
- var import_node_path5 = __toESM(require("path"), 1);
1055
+ var import_promises7 = __toESM(require("fs/promises"), 1);
1056
+ var import_node_path6 = __toESM(require("path"), 1);
1057
+ init_collections();
1058
+ init_errors();
514
1059
  var INDEX_RE = /^(index|readme)\.mdx?$/i;
515
1060
  function buildArticleTree(relPaths) {
516
1061
  const root = {};
@@ -554,7 +1099,7 @@ function buildArticleTree(relPaths) {
554
1099
  async function collectRelPaths(dir, rel = "") {
555
1100
  let dirents;
556
1101
  try {
557
- dirents = await import_promises8.default.readdir(dir, { withFileTypes: true });
1102
+ dirents = await import_promises7.default.readdir(dir, { withFileTypes: true });
558
1103
  } catch {
559
1104
  return [];
560
1105
  }
@@ -563,7 +1108,7 @@ async function collectRelPaths(dir, rel = "") {
563
1108
  if (d.name.startsWith(".")) continue;
564
1109
  const childRel = rel ? `${rel}/${d.name}` : d.name;
565
1110
  if (d.isDirectory()) {
566
- out.push(...await collectRelPaths(import_node_path5.default.join(dir, d.name), childRel));
1111
+ out.push(...await collectRelPaths(import_node_path6.default.join(dir, d.name), childRel));
567
1112
  } else if (/\.(mdx?)$/.test(d.name)) {
568
1113
  out.push(childRel);
569
1114
  }
@@ -572,7 +1117,7 @@ async function collectRelPaths(dir, rel = "") {
572
1117
  }
573
1118
  async function readdirSafe(dir) {
574
1119
  try {
575
- return await import_promises8.default.readdir(dir);
1120
+ return await import_promises7.default.readdir(dir);
576
1121
  } catch {
577
1122
  return [];
578
1123
  }
@@ -589,7 +1134,7 @@ function pickIndexFile(names) {
589
1134
  async function cheapTitle(absPath) {
590
1135
  let handle;
591
1136
  try {
592
- handle = await import_promises8.default.open(absPath, "r");
1137
+ handle = await import_promises7.default.open(absPath, "r");
593
1138
  const buf = Buffer.alloc(512);
594
1139
  const { bytesRead } = await handle.read(buf, 0, 512, 0);
595
1140
  const snippet = buf.subarray(0, bytesRead).toString("utf8");
@@ -608,14 +1153,14 @@ async function enrichTitles(nodes, collDir) {
608
1153
  const children = await enrichTitles(node.children, collDir);
609
1154
  let title = node.title;
610
1155
  if (node.slug) {
611
- const dir = import_node_path5.default.join(collDir, node.slug);
1156
+ const dir = import_node_path6.default.join(collDir, node.slug);
612
1157
  const idx = pickIndexFile(await readdirSafe(dir));
613
- if (idx) title = await cheapTitle(import_node_path5.default.join(dir, idx)) ?? title;
1158
+ if (idx) title = await cheapTitle(import_node_path6.default.join(dir, idx)) ?? title;
614
1159
  }
615
1160
  return { ...node, title, children };
616
1161
  }
617
1162
  for (const ext of [".mdx", ".md"]) {
618
- const title = await cheapTitle(import_node_path5.default.join(collDir, `${node.slug}${ext}`));
1163
+ const title = await cheapTitle(import_node_path6.default.join(collDir, `${node.slug}${ext}`));
619
1164
  if (title) return { ...node, title };
620
1165
  }
621
1166
  return node;
@@ -626,38 +1171,38 @@ async function listArticleTree(coll) {
626
1171
  const entry = await findCollection(coll);
627
1172
  if (!entry) throw new CollectionNotFoundError(coll);
628
1173
  const rels = await collectRelPaths(entry.path);
629
- return enrichTitles(buildArticleTree(rels), import_node_path5.default.resolve(entry.path));
1174
+ return enrichTitles(buildArticleTree(rels), import_node_path6.default.resolve(entry.path));
630
1175
  }
631
1176
  async function readArticle(coll, slug) {
632
1177
  const entry = await findCollection(coll);
633
1178
  if (!entry) throw new CollectionNotFoundError(coll);
634
- const collDir = import_node_path5.default.resolve(entry.path);
1179
+ const collDir = import_node_path6.default.resolve(entry.path);
635
1180
  const normalized = slug.replace(/\\/g, "/");
636
1181
  if (normalized.split("/").some((s) => s === "..")) {
637
1182
  throw new InvalidTargetError(slug, "path traversal detected");
638
1183
  }
639
1184
  const safeSlug = normalized.split("/").filter((s) => s !== "" && s !== ".").join("/");
640
- const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path5.default.sep);
1185
+ const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path6.default.sep);
641
1186
  let abs;
642
1187
  for (const rel of safeSlug ? [`${safeSlug}.mdx`, `${safeSlug}.md`] : []) {
643
- const candidate = import_node_path5.default.resolve(collDir, rel);
1188
+ const candidate = import_node_path6.default.resolve(collDir, rel);
644
1189
  if (!inJail(candidate)) throw new InvalidTargetError(slug, "path traversal detected");
645
1190
  try {
646
- await import_promises8.default.access(candidate);
1191
+ await import_promises7.default.access(candidate);
647
1192
  abs = candidate;
648
1193
  break;
649
1194
  } catch {
650
1195
  }
651
1196
  }
652
1197
  if (!abs) {
653
- const dir = import_node_path5.default.resolve(collDir, safeSlug);
1198
+ const dir = import_node_path6.default.resolve(collDir, safeSlug);
654
1199
  if (inJail(dir)) {
655
1200
  const idx = pickIndexFile(await readdirSafe(dir));
656
- if (idx) abs = import_node_path5.default.join(dir, idx);
1201
+ if (idx) abs = import_node_path6.default.join(dir, idx);
657
1202
  }
658
1203
  }
659
1204
  if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
660
- const parsed = parseArticle(await import_promises8.default.readFile(abs, "utf8"));
1205
+ const parsed = parseArticle(await import_promises7.default.readFile(abs, "utf8"));
661
1206
  const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
662
1207
  return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs };
663
1208
  }
@@ -725,49 +1270,771 @@ async function renderArticle(body) {
725
1270
  }).use(import_rehype_stringify.default).process(body);
726
1271
  return { html: String(file), toc };
727
1272
  }
1273
+
1274
+ // src/site.ts
1275
+ init_errors();
1276
+ var isStatic = () => process.env.DIFFWIKI_STATIC === "1";
1277
+ function toTreeNodes(coll, nodes) {
1278
+ return nodes.map(
1279
+ (n) => n.children ? {
1280
+ name: n.name,
1281
+ title: n.title ?? n.name,
1282
+ path: n.slug ? `/${coll}/${n.slug}` : void 0,
1283
+ children: toTreeNodes(coll, n.children)
1284
+ } : { name: n.name, title: n.title ?? n.name, path: `/${coll}/${n.slug}` }
1285
+ );
1286
+ }
1287
+ async function gitInfo(e) {
1288
+ if (isStatic()) {
1289
+ if (e.type === "external") console.warn(`[diffwiki] static build: omitting branch info for "${e.name}"`);
1290
+ return {};
1291
+ }
1292
+ const s = await collectionGitStatus(e);
1293
+ return s ? { branch: s.branch, behind: s.hasUpstream ? s.behind : void 0 } : {};
1294
+ }
1295
+ async function buildCollections() {
1296
+ const entries = await listCollections();
1297
+ return Promise.all(
1298
+ entries.map(async (e) => ({
1299
+ name: e.name,
1300
+ type: e.type,
1301
+ path: e.path,
1302
+ ...await gitInfo(e)
1303
+ }))
1304
+ );
1305
+ }
1306
+ async function buildNavTree() {
1307
+ const entries = await listCollections();
1308
+ return Promise.all(
1309
+ entries.map(async (e) => ({
1310
+ collection: { name: e.name, type: e.type, path: e.path, ...await gitInfo(e) },
1311
+ nodes: toTreeNodes(e.name, await listArticleTree(e.name))
1312
+ }))
1313
+ );
1314
+ }
1315
+ function findNode(nodes, slug) {
1316
+ for (const n of nodes) {
1317
+ if (n.slug === slug) return n;
1318
+ if (n.children) {
1319
+ const found = findNode(n.children, slug);
1320
+ if (found) return found;
1321
+ }
1322
+ }
1323
+ return void 0;
1324
+ }
1325
+ async function resolvePage(coll, slug) {
1326
+ try {
1327
+ const art = await readArticle(coll, slug);
1328
+ const { html, toc } = await renderArticle(art.body);
1329
+ return {
1330
+ kind: "doc",
1331
+ coll: art.coll,
1332
+ slug: art.slug,
1333
+ title: art.title,
1334
+ tags: art.tags,
1335
+ html,
1336
+ toc,
1337
+ path: `/${art.coll}/${art.slug}`
1338
+ };
1339
+ } catch (err) {
1340
+ if (err instanceof CollectionNotFoundError) return null;
1341
+ if (!(err instanceof ArticleNotFoundError)) throw err;
1342
+ let nodes;
1343
+ try {
1344
+ nodes = await listArticleTree(coll);
1345
+ } catch (e) {
1346
+ if (e instanceof CollectionNotFoundError) return null;
1347
+ throw e;
1348
+ }
1349
+ const folder = slug ? findNode(nodes, slug) : { title: void 0, children: nodes };
1350
+ if (!folder?.children) return null;
1351
+ const entries = folder.children.map((n) => ({
1352
+ name: n.name,
1353
+ title: n.title ?? n.name,
1354
+ path: `/${coll}/${n.slug ?? n.name}`,
1355
+ isDir: Array.isArray(n.children)
1356
+ }));
1357
+ return {
1358
+ kind: "listing",
1359
+ coll,
1360
+ slug,
1361
+ title: folder.title ?? (slug ? slug.split("/").pop() ?? slug : coll),
1362
+ entries
1363
+ };
1364
+ }
1365
+ }
1366
+
1367
+ // src/query.ts
1368
+ var import_promises8 = __toESM(require("fs/promises"), 1);
1369
+ var import_node_path7 = __toESM(require("path"), 1);
1370
+ init_collections();
1371
+ init_errors();
1372
+ init_logger();
1373
+ init_host();
1374
+ init_registry2();
1375
+ init_types();
1376
+
1377
+ // src/bm25.ts
1378
+ var K1 = 1.5;
1379
+ var B = 0.75;
1380
+ var FIELD_WEIGHTS = { title: 3, tags: 2, collection: 1.5, body: 1 };
1381
+ function tokenize(text) {
1382
+ return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
1383
+ }
1384
+ function termFreqs(tokens) {
1385
+ const freqs = /* @__PURE__ */ new Map();
1386
+ for (const t of tokens) freqs.set(t, (freqs.get(t) ?? 0) + 1);
1387
+ return freqs;
1388
+ }
1389
+ function bm25FieldScore(queryTerms, fieldTokens, avgFieldLen, idf) {
1390
+ const tf = termFreqs(fieldTokens);
1391
+ const dl = fieldTokens.length;
1392
+ const avgDl = avgFieldLen || 1;
1393
+ let score = 0;
1394
+ for (const term of queryTerms) {
1395
+ const freq = tf.get(term) ?? 0;
1396
+ if (freq === 0) continue;
1397
+ const termIdf = idf.get(term) ?? 0;
1398
+ const numerator = freq * (K1 + 1);
1399
+ const denominator = freq + K1 * (1 - B + B * (dl / avgDl));
1400
+ score += termIdf * (numerator / denominator);
1401
+ }
1402
+ return score;
1403
+ }
1404
+ function rankDocs(term, docs) {
1405
+ const queryTerms = tokenize(term);
1406
+ if (queryTerms.length === 0) return docs.map((d) => ({ ...d }));
1407
+ const N = docs.length;
1408
+ const docTokenSets = docs.map(
1409
+ (d) => /* @__PURE__ */ new Set([...d.fields.title, ...d.fields.tags, ...d.fields.collection, ...d.fields.body])
1410
+ );
1411
+ const idf = /* @__PURE__ */ new Map();
1412
+ for (const qt of queryTerms) {
1413
+ let docFreq = 0;
1414
+ for (const set of docTokenSets) if (set.has(qt)) docFreq++;
1415
+ idf.set(qt, Math.log((N - docFreq + 0.5) / (docFreq + 0.5) + 1));
1416
+ }
1417
+ const avg = (sel) => docs.reduce((s, d) => s + sel(d), 0) / (N || 1);
1418
+ const avgLens = {
1419
+ title: avg((d) => d.fields.title.length),
1420
+ tags: avg((d) => d.fields.tags.length),
1421
+ collection: avg((d) => d.fields.collection.length),
1422
+ body: avg((d) => d.fields.body.length)
1423
+ };
1424
+ const scored = [];
1425
+ for (const d of docs) {
1426
+ const total = bm25FieldScore(queryTerms, d.fields.title, avgLens.title, idf) * FIELD_WEIGHTS.title + bm25FieldScore(queryTerms, d.fields.tags, avgLens.tags, idf) * FIELD_WEIGHTS.tags + bm25FieldScore(queryTerms, d.fields.collection, avgLens.collection, idf) * FIELD_WEIGHTS.collection + bm25FieldScore(queryTerms, d.fields.body, avgLens.body, idf) * FIELD_WEIGHTS.body;
1427
+ if (total > 0) scored.push({ ...d, score: total });
1428
+ }
1429
+ scored.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
1430
+ return scored;
1431
+ }
1432
+
1433
+ // src/query.ts
1434
+ var log = getLogger("query");
1435
+ async function collectMarkdownFiles(dir) {
1436
+ let entries;
1437
+ try {
1438
+ entries = await import_promises8.default.readdir(dir, { withFileTypes: true });
1439
+ } catch {
1440
+ return [];
1441
+ }
1442
+ const files = [];
1443
+ for (const entry of entries) {
1444
+ const full = import_node_path7.default.join(dir, entry.name);
1445
+ if (entry.isDirectory()) {
1446
+ files.push(...await collectMarkdownFiles(full));
1447
+ } else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
1448
+ files.push(full);
1449
+ }
1450
+ }
1451
+ return files;
1452
+ }
1453
+ async function nativeSearch(term, collections, collection) {
1454
+ let targets = collections;
1455
+ if (collection) {
1456
+ const one = collections.find((c) => c.name === collection);
1457
+ if (!one) throw new CollectionNotFoundError(collection);
1458
+ targets = [one];
1459
+ }
1460
+ const docs = [];
1461
+ for (const entry of targets) {
1462
+ for (const full of await collectMarkdownFiles(entry.path)) {
1463
+ const fallbackTitle = import_node_path7.default.basename(full).replace(/\.mdx?$/, "");
1464
+ let title = fallbackTitle;
1465
+ let tags = [];
1466
+ let body = "";
1467
+ try {
1468
+ const parsed = parseArticle(await import_promises8.default.readFile(full, "utf8"));
1469
+ if (parsed.title) title = parsed.title;
1470
+ tags = parsed.tags;
1471
+ body = parsed.body;
1472
+ } catch {
1473
+ }
1474
+ docs.push({
1475
+ collectionName: entry.name,
1476
+ filePath: full,
1477
+ title,
1478
+ tags,
1479
+ fields: {
1480
+ title: tokenize(title),
1481
+ tags: tokenize(tags.join(" ")),
1482
+ collection: tokenize(entry.name),
1483
+ body: tokenize(body)
1484
+ }
1485
+ });
1486
+ }
1487
+ }
1488
+ return rankDocs(term, docs).map((d) => ({
1489
+ collection: d.collectionName,
1490
+ path: d.filePath,
1491
+ title: d.title,
1492
+ tags: d.tags,
1493
+ ...d.score === void 0 ? {} : { score: d.score }
1494
+ }));
1495
+ }
1496
+ async function resolveDefaultEngine() {
1497
+ const config = await readConfig();
1498
+ const enabled = await listEnabledPlugins("search");
1499
+ const configured = config.defaultSearch?.split(":")[0];
1500
+ if (configured && enabled.some((p) => p.name === configured)) return configured;
1501
+ if (enabled.length > 0) return enabled[0].name;
1502
+ return NATIVE_ENGINE;
1503
+ }
1504
+ async function resolveDefaultSearchMode() {
1505
+ const config = await readConfig();
1506
+ const enabled = await listEnabledPlugins("search");
1507
+ const configured = config.defaultSearch?.split(":")[0];
1508
+ const configuredType = config.defaultSearch?.split(":")[1];
1509
+ let engine = NATIVE_ENGINE;
1510
+ if (configured && enabled.some((p) => p.name === configured)) engine = configured;
1511
+ else if (enabled.length > 0) engine = enabled[0].name;
1512
+ if (engine === NATIVE_ENGINE) return { engine, type: "basic" };
1513
+ const record = enabled.find((p) => p.name === engine);
1514
+ const searchTypes = record?.capabilities?.searchTypes ?? [];
1515
+ if (searchTypes.length === 0) return { engine: NATIVE_ENGINE, type: "basic" };
1516
+ if (configuredType && searchTypes.includes(configuredType)) {
1517
+ return { engine, type: configuredType };
1518
+ }
1519
+ return { engine, type: searchTypes[0] };
1520
+ }
1521
+ async function search(term, opts) {
1522
+ const collections = await listCollections();
1523
+ const engine = opts?.engine ?? await resolveDefaultEngine();
1524
+ log.debug(`resolved search engine "${engine}"`);
1525
+ if (engine === NATIVE_ENGINE) {
1526
+ const hits = await nativeSearch(term, collections, opts?.collection);
1527
+ return { hits, engine, fellBackToNative: false };
1528
+ }
1529
+ const provider = await loadSearchProvider(engine);
1530
+ if (!provider) {
1531
+ const error = `no provider found for engine "${engine}"`;
1532
+ log.warn(`engine "${engine}" unavailable, falling back to native: ${error}`);
1533
+ const hits = await nativeSearch(term, collections, opts?.collection);
1534
+ return { hits, engine, fellBackToNative: true, error };
1535
+ }
1536
+ try {
1537
+ const hits = await provider.handleSearch(term, collections, opts);
1538
+ return { hits, engine, fellBackToNative: false };
1539
+ } catch (err) {
1540
+ const message = err instanceof Error ? err.message : String(err);
1541
+ log.warn(`engine "${engine}" failed, falling back to native: ${message}`);
1542
+ const hits = await nativeSearch(term, collections, opts?.collection);
1543
+ return { hits, engine, fellBackToNative: true, error: message };
1544
+ }
1545
+ }
1546
+ async function query(term, opts) {
1547
+ return (await search(term, opts)).hits;
1548
+ }
1549
+
1550
+ // src/search.ts
1551
+ var import_promises9 = __toESM(require("fs/promises"), 1);
1552
+ var import_node_path8 = __toESM(require("path"), 1);
1553
+ init_collections();
1554
+ async function readArticleParts(collDir, slug) {
1555
+ for (const ext of [".mdx", ".md"]) {
1556
+ try {
1557
+ const parsed = parseArticle(await import_promises9.default.readFile(import_node_path8.default.join(collDir, `${slug}${ext}`), "utf8"));
1558
+ return { tags: parsed.tags, body: parsed.body };
1559
+ } catch {
1560
+ }
1561
+ }
1562
+ return { tags: [], body: "" };
1563
+ }
1564
+ async function flatten(coll, collDir, nodes, out) {
1565
+ for (const n of nodes) {
1566
+ if (n.slug && !n.children) {
1567
+ const title = n.title ?? n.name;
1568
+ const { tags, body } = await readArticleParts(collDir, n.slug);
1569
+ out.push({
1570
+ collection: coll,
1571
+ slug: n.slug,
1572
+ title,
1573
+ tags,
1574
+ fields: {
1575
+ title: tokenize(title),
1576
+ tags: tokenize(tags.join(" ")),
1577
+ collection: tokenize(coll),
1578
+ body: tokenize(body)
1579
+ }
1580
+ });
1581
+ }
1582
+ if (n.children) await flatten(coll, collDir, n.children, out);
1583
+ }
1584
+ }
1585
+ async function buildSearchIndex(collection) {
1586
+ const entries = collection ? (await listCollections()).filter((c) => c.name === collection) : await listCollections();
1587
+ const out = [];
1588
+ for (const e of entries) {
1589
+ try {
1590
+ const collEntry = await findCollection(e.name);
1591
+ const collDir = import_node_path8.default.resolve(collEntry?.path ?? e.path);
1592
+ await flatten(e.name, collDir, await listArticleTree(e.name), out);
1593
+ } catch {
1594
+ }
1595
+ }
1596
+ return out;
1597
+ }
1598
+
1599
+ // src/doctor.ts
1600
+ var import_promises11 = __toESM(require("fs/promises"), 1);
1601
+ init_paths();
1602
+ init_registry();
1603
+ init_collections();
1604
+
1605
+ // src/plugins/manager.ts
1606
+ var import_promises10 = __toESM(require("fs/promises"), 1);
1607
+ var import_node_path10 = __toESM(require("path"), 1);
1608
+ init_paths();
1609
+ init_errors();
1610
+ init_collections();
1611
+ init_registry2();
1612
+ init_host();
1613
+
1614
+ // src/plugins/sdk.ts
1615
+ var import_node_child_process4 = require("child_process");
1616
+ var import_node_util3 = require("util");
1617
+ var import_node_fs = require("fs");
1618
+ var import_node_path9 = require("path");
1619
+ var import_node_url = require("url");
1620
+ var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process4.execFile);
1621
+ async function runProcess(file, args, opts) {
1622
+ const { stdout, stderr } = await execFileAsync3(file, args, {
1623
+ env: opts?.env,
1624
+ timeout: opts?.timeout,
1625
+ // Large: qmd emits multi-MB JSON search payloads that overflow the default 1 MB.
1626
+ maxBuffer: 50 * 1024 * 1024
1627
+ });
1628
+ return { stdout, stderr };
1629
+ }
1630
+
1631
+ // src/plugins/manager.ts
1632
+ async function readPackageJson(file) {
1633
+ try {
1634
+ return JSON.parse(await import_promises10.default.readFile(file, "utf8"));
1635
+ } catch {
1636
+ return null;
1637
+ }
1638
+ }
1639
+ function binOf(manifest) {
1640
+ if (typeof manifest.bin === "string") return manifest.bin;
1641
+ if (manifest.bin && typeof manifest.bin === "object") {
1642
+ const first = Object.values(manifest.bin)[0];
1643
+ return typeof first === "string" ? first : null;
1644
+ }
1645
+ return null;
1646
+ }
1647
+ async function pluginManifestDirs(nodeModules) {
1648
+ let entries;
1649
+ try {
1650
+ entries = await import_promises10.default.readdir(nodeModules);
1651
+ } catch {
1652
+ return [];
1653
+ }
1654
+ const dirs = [];
1655
+ for (const entry of entries) {
1656
+ if (entry.startsWith(".")) continue;
1657
+ if (entry.startsWith("@")) {
1658
+ let scoped;
1659
+ try {
1660
+ scoped = await import_promises10.default.readdir(import_node_path10.default.join(nodeModules, entry));
1661
+ } catch {
1662
+ continue;
1663
+ }
1664
+ for (const pkg of scoped) {
1665
+ if (pkg.startsWith(".")) continue;
1666
+ dirs.push(import_node_path10.default.join(nodeModules, entry, pkg));
1667
+ }
1668
+ } else {
1669
+ dirs.push(import_node_path10.default.join(nodeModules, entry));
1670
+ }
1671
+ }
1672
+ return dirs;
1673
+ }
1674
+ async function ensurePluginRoot() {
1675
+ const dir = pluginsDir();
1676
+ await import_promises10.default.mkdir(dir, { recursive: true });
1677
+ const pkgPath = import_node_path10.default.join(dir, "package.json");
1678
+ if (await readPackageJson(pkgPath) === null) {
1679
+ const pkg = { name: "diffwiki-plugins", private: true, version: "0.0.0" };
1680
+ await import_promises10.default.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
1681
+ `, "utf8");
1682
+ }
1683
+ return dir;
1684
+ }
1685
+ async function discoverSearchPackage(dir) {
1686
+ const nodeModules = import_node_path10.default.join(dir, "node_modules");
1687
+ for (const pkgDir of await pluginManifestDirs(nodeModules)) {
1688
+ const manifest = await readPackageJson(import_node_path10.default.join(pkgDir, "package.json"));
1689
+ if (!manifest?.name || manifest.diffwiki?.kind !== "search") continue;
1690
+ const bin = binOf(manifest);
1691
+ if (!bin) continue;
1692
+ return { name: manifest.name, version: manifest.version, binAbs: import_node_path10.default.resolve(pkgDir, bin) };
1693
+ }
1694
+ return null;
1695
+ }
1696
+ async function describePlugin(command, name) {
1697
+ return invokePlugin(command, "describe", {}, { name });
1698
+ }
1699
+ async function runSetup(command, name) {
1700
+ try {
1701
+ const collections = await listCollections();
1702
+ const result = await invokePlugin(
1703
+ command,
1704
+ "setup",
1705
+ {
1706
+ collections: collections.map((c) => ({ name: c.name, path: c.path })),
1707
+ embed: false,
1708
+ prefetchModels: false
1709
+ },
1710
+ { timeoutMs: LONG_TIMEOUT_MS, name }
1711
+ );
1712
+ return { ready: result?.ready === true, steps: result?.steps ?? [] };
1713
+ } catch (err) {
1714
+ return { ready: false, steps: [{ step: "setup", ok: false, message: err.message }] };
1715
+ }
1716
+ }
1717
+ async function installPlugin(spec, opts) {
1718
+ const dir = await ensurePluginRoot();
1719
+ try {
1720
+ await runProcess("npm", ["install", spec, "--prefix", dir]);
1721
+ } catch (err) {
1722
+ throw new PluginError(`npm install ${spec} failed: ${err.message}`);
1723
+ }
1724
+ const discovered = await discoverSearchPackage(dir);
1725
+ if (!discovered) {
1726
+ throw new PluginError(
1727
+ `no diffwiki search plugin found in "${spec}" (need package.json diffwiki.kind === "search" + bin)`
1728
+ );
1729
+ }
1730
+ const command = ["node", discovered.binAbs];
1731
+ let capabilities;
1732
+ try {
1733
+ const desc = await describePlugin(command, discovered.name);
1734
+ if (desc.kind !== "search") {
1735
+ throw new PluginError(`plugin "${discovered.name}" reported kind "${desc.kind}", expected "search"`);
1736
+ }
1737
+ capabilities = desc.capabilities;
1738
+ } catch (err) {
1739
+ if (err instanceof PluginError) throw err;
1740
+ throw new PluginError(`plugin "${discovered.name}" failed describe: ${err.message}`);
1741
+ }
1742
+ const record = {
1743
+ name: discovered.name,
1744
+ kind: "search",
1745
+ command,
1746
+ version: discovered.version,
1747
+ capabilities,
1748
+ enabled: true,
1749
+ managed: true
1750
+ };
1751
+ await upsertPlugin(record);
1752
+ const setup = opts?.setup !== false ? await runSetup(command, discovered.name) : void 0;
1753
+ return { record, setup };
1754
+ }
1755
+ async function registerPlugin(name, command) {
1756
+ if (command.length === 0) throw new PluginError(`register "${name}": --command must not be empty`);
1757
+ let capabilities;
1758
+ try {
1759
+ const desc = await describePlugin(command, name);
1760
+ if (desc.kind !== "search") {
1761
+ throw new PluginError(`plugin "${name}" reported kind "${desc.kind}", expected "search"`);
1762
+ }
1763
+ capabilities = desc.capabilities;
1764
+ } catch (err) {
1765
+ if (err instanceof PluginError) throw err;
1766
+ throw new PluginError(`plugin "${name}" did not respond to describe (command: ${command.join(" ")})`);
1767
+ }
1768
+ const record = {
1769
+ name,
1770
+ kind: "search",
1771
+ command,
1772
+ capabilities,
1773
+ enabled: true,
1774
+ managed: false
1775
+ };
1776
+ await upsertPlugin(record);
1777
+ await runSetup(command, name);
1778
+ return record;
1779
+ }
1780
+ async function removePlugin(name) {
1781
+ const record = (await readPluginRegistry()).plugins.find((p) => p.name === name);
1782
+ if (!record) throw new PluginError(`plugin "${name}" is not registered`);
1783
+ if (record.managed) {
1784
+ const dir = await ensurePluginRoot();
1785
+ try {
1786
+ await runProcess("npm", ["uninstall", name, "--prefix", dir]);
1787
+ } catch {
1788
+ }
1789
+ }
1790
+ await deregisterPlugin(name);
1791
+ }
1792
+ async function probeDescribable(command, name) {
1793
+ try {
1794
+ await describePlugin(command, name);
1795
+ return true;
1796
+ } catch {
1797
+ return false;
1798
+ }
1799
+ }
1800
+ async function listPlugins() {
1801
+ const registry = await readPluginRegistry();
1802
+ const out = [];
1803
+ for (const record of registry.plugins) {
1804
+ const spawnable = record.enabled ? await probeDescribable(record.command, record.name) : false;
1805
+ out.push({ ...record, spawnable });
1806
+ }
1807
+ return out;
1808
+ }
1809
+
1810
+ // src/doctor.ts
1811
+ init_host();
1812
+ init_types();
1813
+ async function exists(target) {
1814
+ try {
1815
+ await import_promises11.default.access(target);
1816
+ return true;
1817
+ } catch {
1818
+ return false;
1819
+ }
1820
+ }
1821
+ async function doctor() {
1822
+ const out = [];
1823
+ const home = resolveHome();
1824
+ out.push(
1825
+ await exists(home) ? { level: "ok", message: `home directory present: ${home}` } : { level: "warn", message: `home directory missing (created on first write): ${home}` }
1826
+ );
1827
+ out.push(
1828
+ await exists(registryPath()) ? { level: "ok", message: `registry present: ${registryPath()}` } : { level: "warn", message: `registry missing: ${registryPath()}` }
1829
+ );
1830
+ const registry = await readRegistry();
1831
+ for (const c of registry.collections) {
1832
+ out.push(
1833
+ await exists(c.path) ? { level: "ok", message: `collection "${c.name}" \u2192 ${c.path}` } : { level: "error", message: `collection "${c.name}" points to missing path: ${c.path}` }
1834
+ );
1835
+ }
1836
+ const config = await readConfig();
1837
+ if (config.defaultCollection) {
1838
+ const found = registry.collections.some((c) => c.name === config.defaultCollection);
1839
+ out.push(
1840
+ found ? { level: "ok", message: `default collection: ${config.defaultCollection}` } : {
1841
+ level: "error",
1842
+ message: `default collection "${config.defaultCollection}" is not registered`
1843
+ }
1844
+ );
1845
+ } else {
1846
+ out.push({ level: "warn", message: "no default collection set" });
1847
+ }
1848
+ await appendPluginDiagnostics(out, config);
1849
+ return out;
1850
+ }
1851
+ function readinessLine(name, health) {
1852
+ const r = health.readiness;
1853
+ if (!r) {
1854
+ return { level: health.ready ? "ok" : "warn", message: `plugin "${name}" ${health.ready ? "ready" : "not ready"}` };
1855
+ }
1856
+ if (r.firstSearchCost === "model-download") {
1857
+ return {
1858
+ level: "warn",
1859
+ message: `plugin "${name}": ${r.docsIndexed} doc(s) indexed \u2014 first search will download models (~2GB)`
1860
+ };
1861
+ }
1862
+ if (r.firstSearchCost === "model-load") {
1863
+ return {
1864
+ level: "warn",
1865
+ message: `plugin "${name}": ${r.docsIndexed} doc(s) indexed \u2014 first search loads models (a few seconds)`
1866
+ };
1867
+ }
1868
+ return { level: "ok", message: `plugin "${name}": ${r.docsIndexed} doc(s) indexed \u2014 first search is instant` };
1869
+ }
1870
+ async function appendPluginDiagnostics(out, config) {
1871
+ let plugins = [];
1872
+ try {
1873
+ plugins = await listPlugins();
1874
+ } catch (err) {
1875
+ out.push({ level: "error", message: `failed to read plugin registry: ${err.message}` });
1876
+ return;
1877
+ }
1878
+ if (plugins.length === 0) {
1879
+ out.push({ level: "warn", message: "no search plugins installed \u2014 using built-in native search" });
1880
+ }
1881
+ const collections = await listCollections();
1882
+ for (const p of plugins) {
1883
+ if (!p.enabled) {
1884
+ out.push({ level: "warn", message: `plugin "${p.name}" registered but disabled` });
1885
+ continue;
1886
+ }
1887
+ if (!p.spawnable) {
1888
+ out.push({
1889
+ level: "error",
1890
+ message: `plugin "${p.name}" registered but its command is not spawnable: ${p.command.join(" ")}`
1891
+ });
1892
+ continue;
1893
+ }
1894
+ out.push({ level: "ok", message: `plugin "${p.name}" enabled and spawnable` });
1895
+ try {
1896
+ const provider = await loadSearchProvider(p.name);
1897
+ if (!provider) {
1898
+ out.push({ level: "error", message: `plugin "${p.name}" failed to load for a health check` });
1899
+ continue;
1900
+ }
1901
+ const health = await provider.health(collections);
1902
+ for (const d of health.diagnostics) out.push({ level: d.level, message: `${p.name} \xB7 ${d.message}` });
1903
+ out.push(readinessLine(p.name, health));
1904
+ } catch (err) {
1905
+ out.push({ level: "error", message: `plugin "${p.name}" health check failed: ${err.message}` });
1906
+ }
1907
+ }
1908
+ if (config.defaultSearch) {
1909
+ const engine = config.defaultSearch.split(":")[0];
1910
+ const known = engine === NATIVE_ENGINE || plugins.some((p) => p.name === engine && p.enabled);
1911
+ if (!known) {
1912
+ out.push({
1913
+ level: "warn",
1914
+ message: `defaultSearch "${config.defaultSearch}" is not installed/enabled \u2014 using native`
1915
+ });
1916
+ }
1917
+ }
1918
+ }
1919
+
1920
+ // src/index.ts
1921
+ init_logger();
1922
+
1923
+ // src/plugins/index.ts
1924
+ init_host();
1925
+ init_events();
1926
+
1927
+ // src/plugins/catalog.ts
1928
+ var KNOWN_PLUGINS = [
1929
+ {
1930
+ name: "diffwiki-qmd",
1931
+ kind: "search",
1932
+ spec: "diffwiki-qmd",
1933
+ description: "Local hybrid search \u2014 keyword, semantic, and reranked hybrid via the qmd CLI",
1934
+ requires: "qmd (npm i -g @tobilu/qmd)"
1935
+ },
1936
+ {
1937
+ name: "diffwiki-ripgrep",
1938
+ kind: "search",
1939
+ spec: "diffwiki-ripgrep",
1940
+ description: "Instant literal/regex search via ripgrep \u2014 no index, no models",
1941
+ requires: "rg (ripgrep)"
1942
+ }
1943
+ ];
1944
+ function listAvailablePlugins() {
1945
+ return [...KNOWN_PLUGINS];
1946
+ }
1947
+
1948
+ // src/plugins/index.ts
1949
+ init_registry2();
728
1950
  // Annotate the CommonJS export names for ESM import in node:
729
1951
  0 && (module.exports = {
730
1952
  ArticleNotFoundError,
1953
+ CollectionAlreadyAddedError,
731
1954
  CollectionExistsError,
732
1955
  CollectionNotFoundError,
733
1956
  DiffwikiError,
734
1957
  InvalidTargetError,
1958
+ NATIVE_ENGINE,
735
1959
  NoDefaultCollectionError,
1960
+ PluginError,
736
1961
  REPO_CONFIG_FILE,
1962
+ WorktreeNotAllowedError,
1963
+ addExternalCollection,
737
1964
  addTags,
738
1965
  buildArticleTree,
1966
+ buildCollections,
1967
+ buildNavTree,
1968
+ buildSearchIndex,
739
1969
  collectionDir,
1970
+ collectionGitStatus,
740
1971
  collectionsDir,
741
1972
  configPath,
742
1973
  createArticle,
743
1974
  createCollection,
1975
+ currentBranch,
1976
+ deregisterPlugin,
744
1977
  doctor,
1978
+ emitPluginEvent,
1979
+ ensureGitRepo,
1980
+ ensurePluginRoot,
745
1981
  findCollection,
1982
+ findCollectionById,
1983
+ findEnabledPlugin,
1984
+ findPluginRecord,
746
1985
  getConfigValue,
1986
+ getLogger,
1987
+ gitToplevel,
747
1988
  initRepoWiki,
1989
+ installPlugin,
1990
+ isLinkedWorktree,
748
1991
  listArticleTree,
1992
+ listAvailablePlugins,
749
1993
  listCollections,
1994
+ listEnabledPlugins,
1995
+ listPlugins,
1996
+ listSearchModes,
1997
+ loadSearchProvider,
1998
+ nativeSearch,
750
1999
  parseAddTarget,
751
2000
  parseArticle,
752
2001
  parsePathTarget,
2002
+ pluginsDir,
2003
+ pluginsRegistryPath,
753
2004
  query,
2005
+ rankDocs,
754
2006
  readArticle,
755
2007
  readConfig,
2008
+ readPluginRegistry,
756
2009
  readRegistry,
757
2010
  readRepoConfig,
758
2011
  registerCollection,
2012
+ registerPlugin,
759
2013
  registryPath,
760
2014
  removeArticle,
761
2015
  removeCollection,
2016
+ removePlugin,
762
2017
  removeTags,
763
2018
  renderArticle,
2019
+ repoId,
2020
+ repoRemoteName,
2021
+ resolveCwdCollection,
2022
+ resolveDefaultSearchMode,
764
2023
  resolveHome,
2024
+ resolveLogLevel,
2025
+ resolvePage,
2026
+ search,
765
2027
  serializeArticle,
766
2028
  setConfigValue,
767
2029
  setTags,
768
2030
  slugify,
2031
+ toTreeNodes,
2032
+ tokenize,
769
2033
  unregisterCollection,
770
2034
  updateArticleBody,
2035
+ upsertPlugin,
2036
+ upstreamStatus,
771
2037
  writeConfig,
2038
+ writePluginRegistry,
772
2039
  writeRegistry
773
2040
  });