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.
@@ -0,0 +1,500 @@
1
+ // src/collections.ts
2
+ import fs2 from "fs/promises";
3
+
4
+ // src/paths.ts
5
+ import os from "os";
6
+ import path from "path";
7
+ function resolveHome() {
8
+ const override = process.env.DIFFWIKI_HOME;
9
+ return override && override.length > 0 ? override : path.join(os.homedir(), ".diffwiki");
10
+ }
11
+ function registryPath() {
12
+ return path.join(resolveHome(), "registry.json");
13
+ }
14
+ function configPath() {
15
+ return path.join(resolveHome(), "config.json");
16
+ }
17
+ function collectionsDir() {
18
+ return path.join(resolveHome(), "collections");
19
+ }
20
+ function collectionDir(name) {
21
+ return path.join(collectionsDir(), name);
22
+ }
23
+ function pluginsDir() {
24
+ return path.join(resolveHome(), "plugins");
25
+ }
26
+ function pluginsRegistryPath() {
27
+ return path.join(resolveHome(), "plugins.json");
28
+ }
29
+
30
+ // src/registry.ts
31
+ import fs from "fs/promises";
32
+ function isMissing(err) {
33
+ return err?.code === "ENOENT";
34
+ }
35
+ async function readRegistry() {
36
+ try {
37
+ const parsed = JSON.parse(await fs.readFile(registryPath(), "utf8"));
38
+ return { version: parsed.version ?? 1, collections: parsed.collections ?? [] };
39
+ } catch (err) {
40
+ if (isMissing(err)) return { version: 1, collections: [] };
41
+ throw err;
42
+ }
43
+ }
44
+ async function writeRegistry(registry) {
45
+ await fs.mkdir(resolveHome(), { recursive: true });
46
+ await fs.writeFile(registryPath(), `${JSON.stringify(registry, null, 2)}
47
+ `, "utf8");
48
+ }
49
+ async function registerCollection(entry) {
50
+ const registry = await readRegistry();
51
+ const index = registry.collections.findIndex((c) => c.name === entry.name);
52
+ if (index >= 0) {
53
+ registry.collections[index] = entry;
54
+ } else {
55
+ registry.collections.push(entry);
56
+ }
57
+ await writeRegistry(registry);
58
+ }
59
+ async function unregisterCollection(name) {
60
+ const registry = await readRegistry();
61
+ registry.collections = registry.collections.filter((c) => c.name !== name);
62
+ await writeRegistry(registry);
63
+ }
64
+
65
+ // src/errors.ts
66
+ var DiffwikiError = class extends Error {
67
+ constructor(message) {
68
+ super(message);
69
+ this.name = new.target.name;
70
+ }
71
+ };
72
+ var CollectionExistsError = class extends DiffwikiError {
73
+ constructor(name) {
74
+ super(`collection "${name}" already exists`);
75
+ }
76
+ };
77
+ var CollectionNotFoundError = class extends DiffwikiError {
78
+ constructor(name) {
79
+ super(`collection "${name}" not found`);
80
+ }
81
+ };
82
+ var NoDefaultCollectionError = class extends DiffwikiError {
83
+ constructor() {
84
+ super("no default collection set (use: diffwiki config-set defaultCollection <name>)");
85
+ }
86
+ };
87
+ var ArticleNotFoundError = class extends DiffwikiError {
88
+ constructor(target) {
89
+ super(`article "${target}" not found`);
90
+ }
91
+ };
92
+ var InvalidTargetError = class extends DiffwikiError {
93
+ constructor(target, expected) {
94
+ super(`invalid target "${target}" \u2014 ${expected}`);
95
+ }
96
+ };
97
+ var PluginError = class extends DiffwikiError {
98
+ };
99
+ var WorktreeNotAllowedError = class extends DiffwikiError {
100
+ constructor(root) {
101
+ super(`refusing to add a linked worktree \u2014 add the source repository instead (${root})`);
102
+ }
103
+ };
104
+ var CollectionAlreadyAddedError = class extends DiffwikiError {
105
+ constructor(name) {
106
+ super(`this repository is already added as collection "${name}"`);
107
+ }
108
+ };
109
+
110
+ // src/collections.ts
111
+ async function fireHook(event, collection) {
112
+ try {
113
+ const { emitPluginEvent: emitPluginEvent2 } = await import("./events-XSIUMRBG.js");
114
+ await emitPluginEvent2(event, collection);
115
+ } catch {
116
+ }
117
+ }
118
+ async function listCollections() {
119
+ return (await readRegistry()).collections;
120
+ }
121
+ async function findCollection(name) {
122
+ return (await readRegistry()).collections.find((c) => c.name === name);
123
+ }
124
+ async function findCollectionById(id) {
125
+ return (await readRegistry()).collections.find((c) => c.id === id);
126
+ }
127
+ async function createCollection(name) {
128
+ if (await findCollection(name)) throw new CollectionExistsError(name);
129
+ const dir = collectionDir(name);
130
+ await fs2.mkdir(dir, { recursive: true });
131
+ const entry = {
132
+ name,
133
+ type: "global",
134
+ path: dir,
135
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
136
+ };
137
+ await registerCollection(entry);
138
+ void fireHook("collection-added", name);
139
+ return entry;
140
+ }
141
+ async function removeCollection(name) {
142
+ const entry = await findCollection(name);
143
+ if (!entry) throw new CollectionNotFoundError(name);
144
+ await unregisterCollection(name);
145
+ if (entry.type === "global") {
146
+ await fs2.rm(entry.path, { recursive: true, force: true });
147
+ }
148
+ void fireHook("collection-removed", name);
149
+ return entry;
150
+ }
151
+
152
+ // src/plugins/registry.ts
153
+ import fs3 from "fs/promises";
154
+ function isMissing2(err) {
155
+ return err?.code === "ENOENT";
156
+ }
157
+ async function readPluginRegistry() {
158
+ try {
159
+ const parsed = JSON.parse(await fs3.readFile(pluginsRegistryPath(), "utf8"));
160
+ return { version: parsed.version ?? 1, plugins: parsed.plugins ?? [] };
161
+ } catch (err) {
162
+ if (isMissing2(err)) return { version: 1, plugins: [] };
163
+ throw err;
164
+ }
165
+ }
166
+ async function writePluginRegistry(registry) {
167
+ await fs3.mkdir(resolveHome(), { recursive: true });
168
+ await fs3.writeFile(pluginsRegistryPath(), `${JSON.stringify(registry, null, 2)}
169
+ `, "utf8");
170
+ }
171
+ async function upsertPlugin(record) {
172
+ const registry = await readPluginRegistry();
173
+ const index = registry.plugins.findIndex((p) => p.name === record.name);
174
+ if (index >= 0) {
175
+ registry.plugins[index] = record;
176
+ } else {
177
+ registry.plugins.push(record);
178
+ }
179
+ await writePluginRegistry(registry);
180
+ }
181
+ async function deregisterPlugin(name) {
182
+ const registry = await readPluginRegistry();
183
+ registry.plugins = registry.plugins.filter((p) => p.name !== name);
184
+ await writePluginRegistry(registry);
185
+ }
186
+ async function listEnabledPlugins(kind) {
187
+ const registry = await readPluginRegistry();
188
+ return registry.plugins.filter((p) => p.enabled && p.kind === kind);
189
+ }
190
+ async function findEnabledPlugin(kind, name) {
191
+ const enabled = await listEnabledPlugins(kind);
192
+ if (name) return enabled.find((p) => p.name === name);
193
+ return enabled[0];
194
+ }
195
+ async function findPluginRecord(name) {
196
+ const registry = await readPluginRegistry();
197
+ return registry.plugins.find((p) => p.name === name);
198
+ }
199
+
200
+ // src/plugins/host.ts
201
+ import { spawn } from "child_process";
202
+ import path2, { basename } from "path";
203
+
204
+ // src/logger.ts
205
+ import { getLogger as logtapeGetLogger } from "@logtape/logtape";
206
+ var LEVELS = ["trace", "debug", "info", "warning", "error", "fatal"];
207
+ var DEFAULT_LEVEL = "warning";
208
+ function getLogger(scope) {
209
+ const parts = Array.isArray(scope) ? scope : [scope];
210
+ return logtapeGetLogger(["diffwiki", ...parts]);
211
+ }
212
+ function resolveLogLevel(env = process.env) {
213
+ const raw = (env.DIFFWIKI_LOG ?? "").toLowerCase();
214
+ return LEVELS.includes(raw) ? raw : DEFAULT_LEVEL;
215
+ }
216
+
217
+ // src/types.ts
218
+ var NATIVE_ENGINE = "native";
219
+
220
+ // src/plugins/host.ts
221
+ var LEVEL_METHOD = {
222
+ trace: "trace",
223
+ debug: "debug",
224
+ info: "info",
225
+ warn: "warn",
226
+ warning: "warn",
227
+ error: "error",
228
+ fatal: "fatal"
229
+ };
230
+ var DEFAULT_TIMEOUT_MS = 3e4;
231
+ var SEARCH_TIMEOUT_MS = 12e4;
232
+ var LONG_TIMEOUT_MS = 18e5;
233
+ async function invokePlugin(command, op, params, opts) {
234
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
235
+ const scope = opts?.name ?? basename(command[1] ?? command[0] ?? "plugin");
236
+ return new Promise((resolve, reject) => {
237
+ const child = spawn(command[0], command.slice(1), {
238
+ stdio: ["pipe", "pipe", "pipe"]
239
+ });
240
+ let stdout = "";
241
+ let stderr = "";
242
+ let stderrLine = "";
243
+ let settled = false;
244
+ const forwardLog = (line) => {
245
+ const trimmedLine = line.trim();
246
+ if (!trimmedLine.startsWith("{")) return;
247
+ let parsed;
248
+ try {
249
+ parsed = JSON.parse(trimmedLine);
250
+ } catch {
251
+ return;
252
+ }
253
+ const entry = parsed.log;
254
+ if (!entry || typeof entry.message !== "string") return;
255
+ const rawLevel = typeof entry.level === "string" ? entry.level.toLowerCase() : "";
256
+ const method = LEVEL_METHOD[rawLevel] ?? "info";
257
+ getLogger(["plugin", scope])[method](entry.message);
258
+ };
259
+ const timer = setTimeout(() => {
260
+ if (settled) return;
261
+ settled = true;
262
+ child.kill("SIGKILL");
263
+ reject(new Error(`plugin "${op}" timed out after ${timeoutMs}ms`));
264
+ }, timeoutMs);
265
+ const settle = (fn) => {
266
+ if (settled) return;
267
+ settled = true;
268
+ clearTimeout(timer);
269
+ fn();
270
+ };
271
+ child.stdout.setEncoding("utf8");
272
+ child.stdout.on("data", (chunk) => {
273
+ stdout += chunk;
274
+ });
275
+ child.stderr.setEncoding("utf8");
276
+ child.stderr.on("data", (chunk) => {
277
+ stderr += chunk;
278
+ stderrLine += chunk;
279
+ let nl;
280
+ while ((nl = stderrLine.indexOf("\n")) !== -1) {
281
+ forwardLog(stderrLine.slice(0, nl));
282
+ stderrLine = stderrLine.slice(nl + 1);
283
+ }
284
+ });
285
+ child.on("error", (err) => {
286
+ settle(() => reject(new Error(`plugin spawn failed: ${err.message}`)));
287
+ });
288
+ child.on("close", (code) => {
289
+ if (stderrLine.length > 0) {
290
+ forwardLog(stderrLine);
291
+ stderrLine = "";
292
+ }
293
+ settle(() => {
294
+ const trimmed = stdout.trim();
295
+ if (!trimmed) {
296
+ reject(
297
+ new Error(`plugin "${op}" produced no output (exit ${code})${stderr ? ": " + stderr.slice(0, 500) : ""}`)
298
+ );
299
+ return;
300
+ }
301
+ let response;
302
+ try {
303
+ response = JSON.parse(trimmed);
304
+ } catch {
305
+ reject(new Error(`plugin "${op}" returned invalid JSON: ${trimmed.slice(0, 200)}`));
306
+ return;
307
+ }
308
+ if (response.ok) {
309
+ resolve(response.result);
310
+ } else {
311
+ const message = typeof response.error?.message === "string" ? response.error.message : `plugin "${op}" failed`;
312
+ reject(new Error(message));
313
+ }
314
+ });
315
+ });
316
+ const request = JSON.stringify({ op, params });
317
+ child.stdin.write(request + "\n", (err) => {
318
+ if (err && !settled) {
319
+ settle(() => reject(new Error(`failed to write to plugin stdin: ${err.message}`)));
320
+ return;
321
+ }
322
+ child.stdin.end();
323
+ });
324
+ });
325
+ }
326
+ function toWireCollections(collections) {
327
+ return collections.map((c) => ({ name: c.name, path: c.path }));
328
+ }
329
+ function mapHits(result, collections) {
330
+ const raw = result?.hits;
331
+ const hits = Array.isArray(raw) ? raw : [];
332
+ const out = [];
333
+ for (const item of hits) {
334
+ if (typeof item !== "object" || item === null) continue;
335
+ const h = item;
336
+ if (typeof h.collection !== "string" || typeof h.relPath !== "string") continue;
337
+ const title = typeof h.title === "string" ? h.title : h.relPath;
338
+ const coll = collections.find((c) => c.name === h.collection);
339
+ const abs = coll ? path2.join(coll.path, h.relPath) : h.relPath;
340
+ out.push({
341
+ collection: h.collection,
342
+ path: abs,
343
+ title,
344
+ tags: Array.isArray(h.tags) ? h.tags.filter((t) => typeof t === "string") : [],
345
+ snippet: typeof h.snippet === "string" ? h.snippet : void 0,
346
+ score: typeof h.score === "number" ? h.score : void 0,
347
+ docid: typeof h.docid === "string" ? h.docid : void 0
348
+ });
349
+ }
350
+ return out;
351
+ }
352
+ async function loadSearchProvider(name) {
353
+ const record = await findEnabledPlugin("search", name);
354
+ if (!record) return null;
355
+ let capabilities;
356
+ try {
357
+ const desc = await invokePlugin(record.command, "describe", {}, { name: record.name });
358
+ if (desc.kind !== "search") {
359
+ return null;
360
+ }
361
+ capabilities = desc.capabilities;
362
+ } catch {
363
+ return null;
364
+ }
365
+ const provider = {
366
+ name: record.name,
367
+ capabilities,
368
+ async handleSearch(term, collections, opts) {
369
+ const result = await invokePlugin(
370
+ record.command,
371
+ "search",
372
+ {
373
+ term,
374
+ collections: toWireCollections(collections),
375
+ collection: opts?.collection,
376
+ type: opts?.type,
377
+ limit: opts?.limit
378
+ },
379
+ { timeoutMs: SEARCH_TIMEOUT_MS, name: record.name }
380
+ );
381
+ return mapHits(result, collections);
382
+ },
383
+ async reindex(collections, opts) {
384
+ const result = await invokePlugin(
385
+ record.command,
386
+ "index",
387
+ {
388
+ collections: toWireCollections(collections),
389
+ embed: opts?.embed
390
+ },
391
+ { timeoutMs: LONG_TIMEOUT_MS, name: record.name }
392
+ );
393
+ return result?.indexed ?? 0;
394
+ },
395
+ async emitEvent(event, collection, relPath, collections) {
396
+ try {
397
+ const entry = collections.find((c) => c.name === collection);
398
+ await invokePlugin(
399
+ record.command,
400
+ "event",
401
+ {
402
+ event,
403
+ collection: entry ? { name: entry.name, path: entry.path } : { name: collection, path: "" },
404
+ relPath,
405
+ collections: toWireCollections(collections)
406
+ },
407
+ { name: record.name }
408
+ );
409
+ } catch {
410
+ }
411
+ },
412
+ async health(collections) {
413
+ const result = await invokePlugin(record.command, "health", { collections: toWireCollections(collections) }, { name: record.name });
414
+ return {
415
+ ready: result?.ready === true,
416
+ readiness: result?.readiness,
417
+ diagnostics: result?.diagnostics ?? []
418
+ };
419
+ }
420
+ };
421
+ return provider;
422
+ }
423
+ async function listSearchModes() {
424
+ const modes = [{ plugin: NATIVE_ENGINE, type: "basic", label: "native \xB7 bm25" }];
425
+ const plugins = await listEnabledPlugins("search");
426
+ for (const record of plugins) {
427
+ const caps = record.capabilities;
428
+ if (!caps) continue;
429
+ for (const type of caps.searchTypes) {
430
+ modes.push({ plugin: record.name, type, label: `${record.name} \xB7 ${type}` });
431
+ }
432
+ }
433
+ return modes;
434
+ }
435
+
436
+ // src/plugins/events.ts
437
+ async function emitPluginEvent(event, collection, relPath) {
438
+ try {
439
+ const record = await findEnabledPlugin("search");
440
+ if (!record) return;
441
+ if (!record.capabilities?.events) return;
442
+ const collections = await listCollections();
443
+ const entry = collections.find((c) => c.name === collection);
444
+ await invokePlugin(
445
+ record.command,
446
+ "event",
447
+ {
448
+ event,
449
+ collection: entry ? { name: entry.name, path: entry.path } : { name: collection, path: "" },
450
+ relPath,
451
+ collections: collections.map((c) => ({ name: c.name, path: c.path }))
452
+ },
453
+ { name: record.name }
454
+ );
455
+ } catch {
456
+ }
457
+ }
458
+
459
+ export {
460
+ NATIVE_ENGINE,
461
+ DiffwikiError,
462
+ CollectionExistsError,
463
+ CollectionNotFoundError,
464
+ NoDefaultCollectionError,
465
+ ArticleNotFoundError,
466
+ InvalidTargetError,
467
+ PluginError,
468
+ WorktreeNotAllowedError,
469
+ CollectionAlreadyAddedError,
470
+ resolveHome,
471
+ registryPath,
472
+ configPath,
473
+ collectionsDir,
474
+ collectionDir,
475
+ pluginsDir,
476
+ pluginsRegistryPath,
477
+ readRegistry,
478
+ writeRegistry,
479
+ registerCollection,
480
+ unregisterCollection,
481
+ readPluginRegistry,
482
+ writePluginRegistry,
483
+ upsertPlugin,
484
+ deregisterPlugin,
485
+ listEnabledPlugins,
486
+ findEnabledPlugin,
487
+ findPluginRecord,
488
+ getLogger,
489
+ resolveLogLevel,
490
+ LONG_TIMEOUT_MS,
491
+ invokePlugin,
492
+ loadSearchProvider,
493
+ listSearchModes,
494
+ emitPluginEvent,
495
+ listCollections,
496
+ findCollection,
497
+ findCollectionById,
498
+ createCollection,
499
+ removeCollection
500
+ };
@@ -0,0 +1,6 @@
1
+ import {
2
+ emitPluginEvent
3
+ } from "./chunk-GVSRD5WB.js";
4
+ export {
5
+ emitPluginEvent
6
+ };