diffwiki-core 0.3.0 → 0.4.0-rc.202607220518.0ade9d3

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