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.
- package/dist/chunk-I3PPUXYY.js +474 -0
- package/dist/events-656CR64Q.js +6 -0
- package/dist/index.cjs +1279 -149
- package/dist/index.d.cts +216 -15
- package/dist/index.d.ts +216 -15
- package/dist/index.js +779 -184
- package/package.json +11 -1
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,87 +30,57 @@ 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/
|
|
31
|
-
var
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
InvalidTargetError: () => InvalidTargetError,
|
|
38
|
-
NoDefaultCollectionError: () => NoDefaultCollectionError,
|
|
39
|
-
REPO_CONFIG_FILE: () => REPO_CONFIG_FILE,
|
|
40
|
-
addTags: () => addTags,
|
|
41
|
-
collectionDir: () => collectionDir,
|
|
42
|
-
collectionsDir: () => collectionsDir,
|
|
43
|
-
configPath: () => configPath,
|
|
44
|
-
createArticle: () => createArticle,
|
|
45
|
-
createCollection: () => createCollection,
|
|
46
|
-
doctor: () => doctor,
|
|
47
|
-
findCollection: () => findCollection,
|
|
48
|
-
getConfigValue: () => getConfigValue,
|
|
49
|
-
initRepoWiki: () => initRepoWiki,
|
|
50
|
-
listCollections: () => listCollections,
|
|
51
|
-
parseAddTarget: () => parseAddTarget,
|
|
52
|
-
parseArticle: () => parseArticle,
|
|
53
|
-
parsePathTarget: () => parsePathTarget,
|
|
54
|
-
query: () => query,
|
|
55
|
-
readConfig: () => readConfig,
|
|
56
|
-
readRegistry: () => readRegistry,
|
|
57
|
-
readRepoConfig: () => readRepoConfig,
|
|
58
|
-
registerCollection: () => registerCollection,
|
|
59
|
-
registryPath: () => registryPath,
|
|
60
|
-
removeArticle: () => removeArticle,
|
|
61
|
-
removeCollection: () => removeCollection,
|
|
62
|
-
removeTags: () => removeTags,
|
|
63
|
-
resolveHome: () => resolveHome,
|
|
64
|
-
serializeArticle: () => serializeArticle,
|
|
65
|
-
setConfigValue: () => setConfigValue,
|
|
66
|
-
setTags: () => setTags,
|
|
67
|
-
slugify: () => slugify,
|
|
68
|
-
unregisterCollection: () => unregisterCollection,
|
|
69
|
-
updateArticleBody: () => updateArticleBody,
|
|
70
|
-
writeConfig: () => writeConfig,
|
|
71
|
-
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
|
+
}
|
|
72
40
|
});
|
|
73
|
-
module.exports = __toCommonJS(index_exports);
|
|
74
41
|
|
|
75
42
|
// src/errors.ts
|
|
76
|
-
var DiffwikiError
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
43
|
+
var DiffwikiError, CollectionExistsError, CollectionNotFoundError, NoDefaultCollectionError, ArticleNotFoundError, InvalidTargetError, PluginError;
|
|
44
|
+
var init_errors = __esm({
|
|
45
|
+
"src/errors.ts"() {
|
|
46
|
+
"use strict";
|
|
47
|
+
DiffwikiError = class extends Error {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = new.target.name;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
CollectionExistsError = class extends DiffwikiError {
|
|
54
|
+
constructor(name) {
|
|
55
|
+
super(`collection "${name}" already exists`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
CollectionNotFoundError = class extends DiffwikiError {
|
|
59
|
+
constructor(name) {
|
|
60
|
+
super(`collection "${name}" not found`);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
NoDefaultCollectionError = class extends DiffwikiError {
|
|
64
|
+
constructor() {
|
|
65
|
+
super("no default collection set (use: diffwiki config-set defaultCollection <name>)");
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
ArticleNotFoundError = class extends DiffwikiError {
|
|
69
|
+
constructor(target) {
|
|
70
|
+
super(`article "${target}" not found`);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
InvalidTargetError = class extends DiffwikiError {
|
|
74
|
+
constructor(target, expected) {
|
|
75
|
+
super(`invalid target "${target}" \u2014 ${expected}`);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
PluginError = class extends DiffwikiError {
|
|
79
|
+
};
|
|
105
80
|
}
|
|
106
|
-
};
|
|
81
|
+
});
|
|
107
82
|
|
|
108
83
|
// src/paths.ts
|
|
109
|
-
var import_node_os = __toESM(require("os"), 1);
|
|
110
|
-
var import_node_path = __toESM(require("path"), 1);
|
|
111
84
|
function resolveHome() {
|
|
112
85
|
const override = process.env.DIFFWIKI_HOME;
|
|
113
86
|
return override && override.length > 0 ? override : import_node_path.default.join(import_node_os.default.homedir(), ".diffwiki");
|
|
@@ -124,15 +97,22 @@ function collectionsDir() {
|
|
|
124
97
|
function collectionDir(name) {
|
|
125
98
|
return import_node_path.default.join(collectionsDir(), name);
|
|
126
99
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
100
|
+
function pluginsDir() {
|
|
101
|
+
return import_node_path.default.join(resolveHome(), "plugins");
|
|
102
|
+
}
|
|
103
|
+
function pluginsRegistryPath() {
|
|
104
|
+
return import_node_path.default.join(resolveHome(), "plugins.json");
|
|
105
|
+
}
|
|
106
|
+
var import_node_os, import_node_path;
|
|
107
|
+
var init_paths = __esm({
|
|
108
|
+
"src/paths.ts"() {
|
|
109
|
+
"use strict";
|
|
110
|
+
import_node_os = __toESM(require("os"), 1);
|
|
111
|
+
import_node_path = __toESM(require("path"), 1);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
133
114
|
|
|
134
115
|
// src/registry.ts
|
|
135
|
-
var import_promises = __toESM(require("fs/promises"), 1);
|
|
136
116
|
function isMissing(err) {
|
|
137
117
|
return err?.code === "ENOENT";
|
|
138
118
|
}
|
|
@@ -165,8 +145,352 @@ async function unregisterCollection(name) {
|
|
|
165
145
|
registry.collections = registry.collections.filter((c) => c.name !== name);
|
|
166
146
|
await writeRegistry(registry);
|
|
167
147
|
}
|
|
148
|
+
var import_promises;
|
|
149
|
+
var init_registry = __esm({
|
|
150
|
+
"src/registry.ts"() {
|
|
151
|
+
"use strict";
|
|
152
|
+
import_promises = __toESM(require("fs/promises"), 1);
|
|
153
|
+
init_paths();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// src/plugins/registry.ts
|
|
158
|
+
function isMissing2(err) {
|
|
159
|
+
return err?.code === "ENOENT";
|
|
160
|
+
}
|
|
161
|
+
async function readPluginRegistry() {
|
|
162
|
+
try {
|
|
163
|
+
const parsed = JSON.parse(await import_promises2.default.readFile(pluginsRegistryPath(), "utf8"));
|
|
164
|
+
return { version: parsed.version ?? 1, plugins: parsed.plugins ?? [] };
|
|
165
|
+
} catch (err) {
|
|
166
|
+
if (isMissing2(err)) return { version: 1, plugins: [] };
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function writePluginRegistry(registry) {
|
|
171
|
+
await import_promises2.default.mkdir(resolveHome(), { recursive: true });
|
|
172
|
+
await import_promises2.default.writeFile(pluginsRegistryPath(), `${JSON.stringify(registry, null, 2)}
|
|
173
|
+
`, "utf8");
|
|
174
|
+
}
|
|
175
|
+
async function upsertPlugin(record) {
|
|
176
|
+
const registry = await readPluginRegistry();
|
|
177
|
+
const index = registry.plugins.findIndex((p) => p.name === record.name);
|
|
178
|
+
if (index >= 0) {
|
|
179
|
+
registry.plugins[index] = record;
|
|
180
|
+
} else {
|
|
181
|
+
registry.plugins.push(record);
|
|
182
|
+
}
|
|
183
|
+
await writePluginRegistry(registry);
|
|
184
|
+
}
|
|
185
|
+
async function deregisterPlugin(name) {
|
|
186
|
+
const registry = await readPluginRegistry();
|
|
187
|
+
registry.plugins = registry.plugins.filter((p) => p.name !== name);
|
|
188
|
+
await writePluginRegistry(registry);
|
|
189
|
+
}
|
|
190
|
+
async function listEnabledPlugins(kind) {
|
|
191
|
+
const registry = await readPluginRegistry();
|
|
192
|
+
return registry.plugins.filter((p) => p.enabled && p.kind === kind);
|
|
193
|
+
}
|
|
194
|
+
async function findEnabledPlugin(kind, name) {
|
|
195
|
+
const enabled = await listEnabledPlugins(kind);
|
|
196
|
+
if (name) return enabled.find((p) => p.name === name);
|
|
197
|
+
return enabled[0];
|
|
198
|
+
}
|
|
199
|
+
var import_promises2;
|
|
200
|
+
var init_registry2 = __esm({
|
|
201
|
+
"src/plugins/registry.ts"() {
|
|
202
|
+
"use strict";
|
|
203
|
+
import_promises2 = __toESM(require("fs/promises"), 1);
|
|
204
|
+
init_paths();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// src/plugins/client.ts
|
|
209
|
+
function toWireCollections(collections) {
|
|
210
|
+
return collections.map((c) => ({ name: c.name, path: c.path }));
|
|
211
|
+
}
|
|
212
|
+
async function spawnPluginClient(record) {
|
|
213
|
+
const child = (0, import_node_child_process.spawn)(record.command[0], record.command.slice(1), {
|
|
214
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
215
|
+
});
|
|
216
|
+
const pending = /* @__PURE__ */ new Map();
|
|
217
|
+
let nextId = 1;
|
|
218
|
+
let buffer = "";
|
|
219
|
+
let closed = false;
|
|
220
|
+
const failAll = (err) => {
|
|
221
|
+
for (const [, p] of pending) {
|
|
222
|
+
clearTimeout(p.timer);
|
|
223
|
+
p.reject(err);
|
|
224
|
+
}
|
|
225
|
+
pending.clear();
|
|
226
|
+
};
|
|
227
|
+
const handleLine = (line) => {
|
|
228
|
+
const trimmed = line.trim();
|
|
229
|
+
if (!trimmed) return;
|
|
230
|
+
let msg;
|
|
231
|
+
try {
|
|
232
|
+
msg = JSON.parse(trimmed);
|
|
233
|
+
} catch {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (typeof msg !== "object" || msg === null) return;
|
|
237
|
+
const payload = msg;
|
|
238
|
+
if ("log" in payload && !("id" in payload)) return;
|
|
239
|
+
const id = payload.id;
|
|
240
|
+
if (typeof id !== "number") return;
|
|
241
|
+
const p = pending.get(id);
|
|
242
|
+
if (!p) return;
|
|
243
|
+
pending.delete(id);
|
|
244
|
+
clearTimeout(p.timer);
|
|
245
|
+
if (payload.ok === true) {
|
|
246
|
+
p.resolve(payload.result);
|
|
247
|
+
} else {
|
|
248
|
+
const error = payload.error;
|
|
249
|
+
const message = typeof error?.message === "string" ? error.message : "plugin request failed";
|
|
250
|
+
p.reject(new Error(message));
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
child.stdout.setEncoding("utf8");
|
|
254
|
+
child.stdout.on("data", (chunk) => {
|
|
255
|
+
buffer += chunk;
|
|
256
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
257
|
+
while (newlineIndex >= 0) {
|
|
258
|
+
const line = buffer.slice(0, newlineIndex);
|
|
259
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
260
|
+
handleLine(line);
|
|
261
|
+
newlineIndex = buffer.indexOf("\n");
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
child.on("error", (err) => {
|
|
265
|
+
closed = true;
|
|
266
|
+
failAll(err);
|
|
267
|
+
});
|
|
268
|
+
child.on("exit", () => {
|
|
269
|
+
closed = true;
|
|
270
|
+
failAll(new Error("plugin process exited"));
|
|
271
|
+
});
|
|
272
|
+
const request = (op, params) => {
|
|
273
|
+
if (closed) return Promise.reject(new Error("plugin process is not running"));
|
|
274
|
+
return new Promise((resolve, reject) => {
|
|
275
|
+
const id = nextId++;
|
|
276
|
+
const timer = setTimeout(() => {
|
|
277
|
+
pending.delete(id);
|
|
278
|
+
reject(new Error(`plugin request "${op}" timed out`));
|
|
279
|
+
}, REQUEST_TIMEOUT_MS);
|
|
280
|
+
pending.set(id, {
|
|
281
|
+
resolve: (result) => resolve(result),
|
|
282
|
+
reject,
|
|
283
|
+
timer
|
|
284
|
+
});
|
|
285
|
+
child.stdin.write(`${JSON.stringify({ id, op, params })}
|
|
286
|
+
`, (err) => {
|
|
287
|
+
if (!err) return;
|
|
288
|
+
const p = pending.get(id);
|
|
289
|
+
if (!p) return;
|
|
290
|
+
pending.delete(id);
|
|
291
|
+
clearTimeout(p.timer);
|
|
292
|
+
p.reject(err);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
let handshake;
|
|
297
|
+
try {
|
|
298
|
+
handshake = await request("initialize", { protocol: 1, home: resolveHome() });
|
|
299
|
+
if (handshake.kind !== "search") {
|
|
300
|
+
throw new Error(`plugin "${record.name}" reported kind "${handshake.kind}", expected "search"`);
|
|
301
|
+
}
|
|
302
|
+
} catch (err) {
|
|
303
|
+
closed = true;
|
|
304
|
+
child.kill("SIGKILL");
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
307
|
+
const capabilities = handshake.capabilities;
|
|
308
|
+
const name = handshake.name;
|
|
309
|
+
const mapHits = (result, collections) => {
|
|
310
|
+
const raw = result?.hits;
|
|
311
|
+
const hits = Array.isArray(raw) ? raw : [];
|
|
312
|
+
const out = [];
|
|
313
|
+
for (const item of hits) {
|
|
314
|
+
if (typeof item !== "object" || item === null) continue;
|
|
315
|
+
const h = item;
|
|
316
|
+
if (typeof h.collection !== "string" || typeof h.relPath !== "string") continue;
|
|
317
|
+
const title = typeof h.title === "string" ? h.title : h.relPath;
|
|
318
|
+
const coll = collections.find((c) => c.name === h.collection);
|
|
319
|
+
const abs = coll ? import_node_path2.default.join(coll.path, h.relPath) : h.relPath;
|
|
320
|
+
out.push({
|
|
321
|
+
collection: h.collection,
|
|
322
|
+
path: abs,
|
|
323
|
+
title,
|
|
324
|
+
tags: Array.isArray(h.tags) ? h.tags.filter((t) => typeof t === "string") : [],
|
|
325
|
+
snippet: typeof h.snippet === "string" ? h.snippet : void 0,
|
|
326
|
+
score: typeof h.score === "number" ? h.score : void 0,
|
|
327
|
+
docid: typeof h.docid === "string" ? h.docid : void 0
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return out;
|
|
331
|
+
};
|
|
332
|
+
const close = async () => {
|
|
333
|
+
if (!closed) {
|
|
334
|
+
try {
|
|
335
|
+
await request("shutdown", {});
|
|
336
|
+
} catch {
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
closed = true;
|
|
340
|
+
try {
|
|
341
|
+
child.stdin.end();
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
await new Promise((resolve) => {
|
|
345
|
+
const timer = setTimeout(() => {
|
|
346
|
+
child.kill();
|
|
347
|
+
resolve();
|
|
348
|
+
}, SHUTDOWN_GRACE_MS);
|
|
349
|
+
child.once("exit", () => {
|
|
350
|
+
clearTimeout(timer);
|
|
351
|
+
resolve();
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
};
|
|
355
|
+
const provider = {
|
|
356
|
+
name,
|
|
357
|
+
capabilities,
|
|
358
|
+
async setup(collections, opts) {
|
|
359
|
+
const result = await request("setup", {
|
|
360
|
+
collections: toWireCollections(collections),
|
|
361
|
+
embed: opts?.embed,
|
|
362
|
+
prefetchModels: opts?.prefetchModels
|
|
363
|
+
});
|
|
364
|
+
return { ready: result?.ready === true };
|
|
365
|
+
},
|
|
366
|
+
async reindex(collections, opts) {
|
|
367
|
+
const result = await request("index", {
|
|
368
|
+
collections: toWireCollections(collections),
|
|
369
|
+
embed: opts?.embed
|
|
370
|
+
});
|
|
371
|
+
return result?.indexed ?? 0;
|
|
372
|
+
},
|
|
373
|
+
async search(term, collections, opts) {
|
|
374
|
+
const result = await request("search", {
|
|
375
|
+
term,
|
|
376
|
+
collections: toWireCollections(collections),
|
|
377
|
+
collection: opts?.collection,
|
|
378
|
+
type: opts?.type,
|
|
379
|
+
limit: opts?.limit
|
|
380
|
+
});
|
|
381
|
+
return mapHits(result, collections);
|
|
382
|
+
},
|
|
383
|
+
async notify(event, collection, relPath, collections) {
|
|
384
|
+
try {
|
|
385
|
+
const entry = collections.find((c) => c.name === collection);
|
|
386
|
+
await request("notify", {
|
|
387
|
+
event,
|
|
388
|
+
collection: entry ? { name: entry.name, path: entry.path } : { name: collection, path: "" },
|
|
389
|
+
relPath,
|
|
390
|
+
collections: toWireCollections(collections)
|
|
391
|
+
});
|
|
392
|
+
} catch {
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
async health(collections) {
|
|
396
|
+
const result = await request("health", { collections: toWireCollections(collections) });
|
|
397
|
+
return {
|
|
398
|
+
ready: result?.ready === true,
|
|
399
|
+
readiness: result?.readiness,
|
|
400
|
+
diagnostics: result?.diagnostics ?? []
|
|
401
|
+
};
|
|
402
|
+
},
|
|
403
|
+
close
|
|
404
|
+
};
|
|
405
|
+
return provider;
|
|
406
|
+
}
|
|
407
|
+
var import_node_child_process, import_node_path2, REQUEST_TIMEOUT_MS, SHUTDOWN_GRACE_MS;
|
|
408
|
+
var init_client = __esm({
|
|
409
|
+
"src/plugins/client.ts"() {
|
|
410
|
+
"use strict";
|
|
411
|
+
import_node_child_process = require("child_process");
|
|
412
|
+
import_node_path2 = __toESM(require("path"), 1);
|
|
413
|
+
init_paths();
|
|
414
|
+
REQUEST_TIMEOUT_MS = 3e4;
|
|
415
|
+
SHUTDOWN_GRACE_MS = 500;
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// src/plugins/loader.ts
|
|
420
|
+
async function loadSearchProvider(name) {
|
|
421
|
+
const record = await findEnabledPlugin("search", name);
|
|
422
|
+
if (!record) return null;
|
|
423
|
+
try {
|
|
424
|
+
return await spawnPluginClient(record);
|
|
425
|
+
} catch {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async function listSearchModes() {
|
|
430
|
+
const modes = [{ plugin: NATIVE_ENGINE, type: "basic", label: "native \xB7 bm25" }];
|
|
431
|
+
const plugins = await listEnabledPlugins("search");
|
|
432
|
+
for (const record of plugins) {
|
|
433
|
+
let provider = null;
|
|
434
|
+
try {
|
|
435
|
+
provider = await spawnPluginClient(record);
|
|
436
|
+
for (const type of provider.capabilities.searchTypes) {
|
|
437
|
+
modes.push({ plugin: record.name, type, label: `${record.name} \xB7 ${type}` });
|
|
438
|
+
}
|
|
439
|
+
} catch {
|
|
440
|
+
} finally {
|
|
441
|
+
if (provider) {
|
|
442
|
+
try {
|
|
443
|
+
await provider.close();
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return modes;
|
|
450
|
+
}
|
|
451
|
+
var init_loader = __esm({
|
|
452
|
+
"src/plugins/loader.ts"() {
|
|
453
|
+
"use strict";
|
|
454
|
+
init_types();
|
|
455
|
+
init_registry2();
|
|
456
|
+
init_client();
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// src/plugins/events.ts
|
|
461
|
+
var events_exports = {};
|
|
462
|
+
__export(events_exports, {
|
|
463
|
+
emitPluginEvent: () => emitPluginEvent
|
|
464
|
+
});
|
|
465
|
+
async function emitPluginEvent(event, collection, relPath) {
|
|
466
|
+
try {
|
|
467
|
+
const provider = await loadSearchProvider();
|
|
468
|
+
try {
|
|
469
|
+
if (provider?.capabilities.hooks) {
|
|
470
|
+
await provider.notify(event, collection, relPath, await listCollections());
|
|
471
|
+
}
|
|
472
|
+
} finally {
|
|
473
|
+
await provider?.close();
|
|
474
|
+
}
|
|
475
|
+
} catch {
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
var init_events = __esm({
|
|
479
|
+
"src/plugins/events.ts"() {
|
|
480
|
+
"use strict";
|
|
481
|
+
init_loader();
|
|
482
|
+
init_collections();
|
|
483
|
+
}
|
|
484
|
+
});
|
|
168
485
|
|
|
169
486
|
// src/collections.ts
|
|
487
|
+
async function fireHook(event, collection) {
|
|
488
|
+
try {
|
|
489
|
+
const { emitPluginEvent: emitPluginEvent2 } = await Promise.resolve().then(() => (init_events(), events_exports));
|
|
490
|
+
await emitPluginEvent2(event, collection);
|
|
491
|
+
} catch {
|
|
492
|
+
}
|
|
493
|
+
}
|
|
170
494
|
async function listCollections() {
|
|
171
495
|
return (await readRegistry()).collections;
|
|
172
496
|
}
|
|
@@ -176,7 +500,7 @@ async function findCollection(name) {
|
|
|
176
500
|
async function createCollection(name) {
|
|
177
501
|
if (await findCollection(name)) throw new CollectionExistsError(name);
|
|
178
502
|
const dir = collectionDir(name);
|
|
179
|
-
await
|
|
503
|
+
await import_promises3.default.mkdir(dir, { recursive: true });
|
|
180
504
|
const entry = {
|
|
181
505
|
name,
|
|
182
506
|
type: "global",
|
|
@@ -184,6 +508,7 @@ async function createCollection(name) {
|
|
|
184
508
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
185
509
|
};
|
|
186
510
|
await registerCollection(entry);
|
|
511
|
+
void fireHook("collection-added", name);
|
|
187
512
|
return entry;
|
|
188
513
|
}
|
|
189
514
|
async function removeCollection(name) {
|
|
@@ -191,32 +516,133 @@ async function removeCollection(name) {
|
|
|
191
516
|
if (!entry) throw new CollectionNotFoundError(name);
|
|
192
517
|
await unregisterCollection(name);
|
|
193
518
|
if (entry.type === "global") {
|
|
194
|
-
await
|
|
519
|
+
await import_promises3.default.rm(entry.path, { recursive: true, force: true });
|
|
195
520
|
}
|
|
521
|
+
void fireHook("collection-removed", name);
|
|
196
522
|
return entry;
|
|
197
523
|
}
|
|
524
|
+
var import_promises3;
|
|
525
|
+
var init_collections = __esm({
|
|
526
|
+
"src/collections.ts"() {
|
|
527
|
+
"use strict";
|
|
528
|
+
import_promises3 = __toESM(require("fs/promises"), 1);
|
|
529
|
+
init_paths();
|
|
530
|
+
init_registry();
|
|
531
|
+
init_errors();
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// src/index.ts
|
|
536
|
+
var index_exports = {};
|
|
537
|
+
__export(index_exports, {
|
|
538
|
+
ArticleNotFoundError: () => ArticleNotFoundError,
|
|
539
|
+
CollectionExistsError: () => CollectionExistsError,
|
|
540
|
+
CollectionNotFoundError: () => CollectionNotFoundError,
|
|
541
|
+
DiffwikiError: () => DiffwikiError,
|
|
542
|
+
InvalidTargetError: () => InvalidTargetError,
|
|
543
|
+
NATIVE_ENGINE: () => NATIVE_ENGINE,
|
|
544
|
+
NoDefaultCollectionError: () => NoDefaultCollectionError,
|
|
545
|
+
PluginError: () => PluginError,
|
|
546
|
+
REPO_CONFIG_FILE: () => REPO_CONFIG_FILE,
|
|
547
|
+
addTags: () => addTags,
|
|
548
|
+
buildArticleTree: () => buildArticleTree,
|
|
549
|
+
collectionDir: () => collectionDir,
|
|
550
|
+
collectionsDir: () => collectionsDir,
|
|
551
|
+
configPath: () => configPath,
|
|
552
|
+
createArticle: () => createArticle,
|
|
553
|
+
createCollection: () => createCollection,
|
|
554
|
+
deregisterPlugin: () => deregisterPlugin,
|
|
555
|
+
doctor: () => doctor,
|
|
556
|
+
emitPluginEvent: () => emitPluginEvent,
|
|
557
|
+
ensurePluginRoot: () => ensurePluginRoot,
|
|
558
|
+
findCollection: () => findCollection,
|
|
559
|
+
findEnabledPlugin: () => findEnabledPlugin,
|
|
560
|
+
getConfigValue: () => getConfigValue,
|
|
561
|
+
initRepoWiki: () => initRepoWiki,
|
|
562
|
+
installPlugin: () => installPlugin,
|
|
563
|
+
listArticleTree: () => listArticleTree,
|
|
564
|
+
listCollections: () => listCollections,
|
|
565
|
+
listEnabledPlugins: () => listEnabledPlugins,
|
|
566
|
+
listPlugins: () => listPlugins,
|
|
567
|
+
listSearchModes: () => listSearchModes,
|
|
568
|
+
loadSearchProvider: () => loadSearchProvider,
|
|
569
|
+
nativeSearch: () => nativeSearch,
|
|
570
|
+
parseAddTarget: () => parseAddTarget,
|
|
571
|
+
parseArticle: () => parseArticle,
|
|
572
|
+
parsePathTarget: () => parsePathTarget,
|
|
573
|
+
pluginsDir: () => pluginsDir,
|
|
574
|
+
pluginsRegistryPath: () => pluginsRegistryPath,
|
|
575
|
+
query: () => query,
|
|
576
|
+
readArticle: () => readArticle,
|
|
577
|
+
readConfig: () => readConfig,
|
|
578
|
+
readPluginRegistry: () => readPluginRegistry,
|
|
579
|
+
readRegistry: () => readRegistry,
|
|
580
|
+
readRepoConfig: () => readRepoConfig,
|
|
581
|
+
registerCollection: () => registerCollection,
|
|
582
|
+
registerPlugin: () => registerPlugin,
|
|
583
|
+
registryPath: () => registryPath,
|
|
584
|
+
removeArticle: () => removeArticle,
|
|
585
|
+
removeCollection: () => removeCollection,
|
|
586
|
+
removePlugin: () => removePlugin,
|
|
587
|
+
removeTags: () => removeTags,
|
|
588
|
+
renderArticle: () => renderArticle,
|
|
589
|
+
resolveDefaultSearchMode: () => resolveDefaultSearchMode,
|
|
590
|
+
resolveHome: () => resolveHome,
|
|
591
|
+
serializeArticle: () => serializeArticle,
|
|
592
|
+
setConfigValue: () => setConfigValue,
|
|
593
|
+
setTags: () => setTags,
|
|
594
|
+
slugify: () => slugify,
|
|
595
|
+
spawnPluginClient: () => spawnPluginClient,
|
|
596
|
+
unregisterCollection: () => unregisterCollection,
|
|
597
|
+
updateArticleBody: () => updateArticleBody,
|
|
598
|
+
upsertPlugin: () => upsertPlugin,
|
|
599
|
+
writeConfig: () => writeConfig,
|
|
600
|
+
writePluginRegistry: () => writePluginRegistry,
|
|
601
|
+
writeRegistry: () => writeRegistry
|
|
602
|
+
});
|
|
603
|
+
module.exports = __toCommonJS(index_exports);
|
|
604
|
+
init_types();
|
|
605
|
+
init_errors();
|
|
606
|
+
init_paths();
|
|
198
607
|
|
|
199
608
|
// src/config.ts
|
|
200
|
-
|
|
609
|
+
var import_promises4 = __toESM(require("fs/promises"), 1);
|
|
610
|
+
init_paths();
|
|
611
|
+
init_collections();
|
|
612
|
+
init_errors();
|
|
613
|
+
init_registry2();
|
|
614
|
+
init_types();
|
|
615
|
+
function isMissing3(err) {
|
|
201
616
|
return err?.code === "ENOENT";
|
|
202
617
|
}
|
|
203
618
|
async function readConfig() {
|
|
204
619
|
try {
|
|
205
|
-
return JSON.parse(await
|
|
620
|
+
return JSON.parse(await import_promises4.default.readFile(configPath(), "utf8"));
|
|
206
621
|
} catch (err) {
|
|
207
|
-
if (
|
|
622
|
+
if (isMissing3(err)) return {};
|
|
208
623
|
throw err;
|
|
209
624
|
}
|
|
210
625
|
}
|
|
211
626
|
async function writeConfig(config) {
|
|
212
|
-
await
|
|
213
|
-
await
|
|
627
|
+
await import_promises4.default.mkdir(resolveHome(), { recursive: true });
|
|
628
|
+
await import_promises4.default.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
|
|
214
629
|
`, "utf8");
|
|
215
630
|
}
|
|
216
631
|
async function setConfigValue(key, value) {
|
|
217
632
|
if (key === "defaultCollection" && !await findCollection(value)) {
|
|
218
633
|
throw new CollectionNotFoundError(value);
|
|
219
634
|
}
|
|
635
|
+
if (key === "defaultSearch") {
|
|
636
|
+
const engine = value.split(":")[0];
|
|
637
|
+
if (engine !== NATIVE_ENGINE) {
|
|
638
|
+
const enabled = await listEnabledPlugins("search");
|
|
639
|
+
if (!enabled.some((p) => p.name === engine)) {
|
|
640
|
+
console.warn(
|
|
641
|
+
`! defaultSearch "${value}" names an engine that is not installed/enabled \u2014 native will be used until it is`
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
220
646
|
const config = await readConfig();
|
|
221
647
|
config[key] = value;
|
|
222
648
|
await writeConfig(config);
|
|
@@ -227,6 +653,9 @@ async function getConfigValue(key) {
|
|
|
227
653
|
return typeof value === "string" ? value : void 0;
|
|
228
654
|
}
|
|
229
655
|
|
|
656
|
+
// src/index.ts
|
|
657
|
+
init_registry();
|
|
658
|
+
|
|
230
659
|
// src/slugify.ts
|
|
231
660
|
function slugify(input) {
|
|
232
661
|
const slug = input.toLowerCase().replace(/^#+\s*/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/g, "");
|
|
@@ -235,23 +664,65 @@ function slugify(input) {
|
|
|
235
664
|
|
|
236
665
|
// src/frontmatter.ts
|
|
237
666
|
var import_gray_matter = __toESM(require("gray-matter"), 1);
|
|
667
|
+
var import_yaml = require("yaml");
|
|
668
|
+
var YAML_ENGINE = {
|
|
669
|
+
parse: (input) => {
|
|
670
|
+
const data = (0, import_yaml.parse)(input);
|
|
671
|
+
return data && typeof data === "object" ? data : {};
|
|
672
|
+
},
|
|
673
|
+
stringify: (data) => (0, import_yaml.stringify)(data)
|
|
674
|
+
};
|
|
675
|
+
var MATTER_OPTS = { engines: { yaml: YAML_ENGINE } };
|
|
676
|
+
function parseFallback(raw) {
|
|
677
|
+
const fenceRe = /^---[ \t]*\n([\s\S]*?)\n---[ \t]*\n/;
|
|
678
|
+
const fenceMatch = fenceRe.exec(raw);
|
|
679
|
+
let title = "";
|
|
680
|
+
let body;
|
|
681
|
+
if (fenceMatch) {
|
|
682
|
+
const inner = fenceMatch[1];
|
|
683
|
+
const titleMatch = /^title:\s*(.+)$/m.exec(inner);
|
|
684
|
+
if (titleMatch) {
|
|
685
|
+
title = titleMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
686
|
+
}
|
|
687
|
+
body = raw.slice(fenceMatch[0].length);
|
|
688
|
+
} else {
|
|
689
|
+
body = raw;
|
|
690
|
+
}
|
|
691
|
+
return { title, tags: [], body: body.replace(/^\n+/, "") };
|
|
692
|
+
}
|
|
238
693
|
function parseArticle(raw) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
694
|
+
try {
|
|
695
|
+
const { data, content } = (0, import_gray_matter.default)(raw, MATTER_OPTS);
|
|
696
|
+
const title = typeof data.title === "string" ? data.title : "";
|
|
697
|
+
const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
|
|
698
|
+
const created = typeof data.created === "string" ? data.created : void 0;
|
|
699
|
+
return { title, tags, created, body: content.replace(/^\n+/, "") };
|
|
700
|
+
} catch {
|
|
701
|
+
return parseFallback(raw);
|
|
702
|
+
}
|
|
244
703
|
}
|
|
245
704
|
function serializeArticle(article) {
|
|
246
705
|
const front = { title: article.title, tags: article.tags };
|
|
247
706
|
if (article.created) front.created = article.created;
|
|
248
707
|
if (article.updated) front.updated = article.updated;
|
|
249
|
-
return import_gray_matter.default.stringify(article.body, front);
|
|
708
|
+
return import_gray_matter.default.stringify(article.body, front, MATTER_OPTS);
|
|
250
709
|
}
|
|
251
710
|
|
|
711
|
+
// src/index.ts
|
|
712
|
+
init_collections();
|
|
713
|
+
|
|
252
714
|
// src/articles.ts
|
|
253
|
-
var
|
|
254
|
-
var
|
|
715
|
+
var import_promises5 = __toESM(require("fs/promises"), 1);
|
|
716
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
717
|
+
init_collections();
|
|
718
|
+
init_errors();
|
|
719
|
+
async function fireHook2(event, collection, relPath) {
|
|
720
|
+
try {
|
|
721
|
+
const { emitPluginEvent: emitPluginEvent2 } = await Promise.resolve().then(() => (init_events(), events_exports));
|
|
722
|
+
await emitPluginEvent2(event, collection, relPath);
|
|
723
|
+
} catch {
|
|
724
|
+
}
|
|
725
|
+
}
|
|
255
726
|
function parseAddTarget(target) {
|
|
256
727
|
const idx = target.indexOf(":");
|
|
257
728
|
if (idx === -1) {
|
|
@@ -284,37 +755,38 @@ async function resolveCollectionDir(name) {
|
|
|
284
755
|
}
|
|
285
756
|
function ensureMdPath(file) {
|
|
286
757
|
if (/\.mdx?$/.test(file)) return file;
|
|
287
|
-
const dir =
|
|
288
|
-
const base = slugify(
|
|
289
|
-
const rel = dir === "." ? `${base}.md` :
|
|
758
|
+
const dir = import_node_path3.default.dirname(file);
|
|
759
|
+
const base = slugify(import_node_path3.default.basename(file));
|
|
760
|
+
const rel = dir === "." ? `${base}.md` : import_node_path3.default.join(dir, `${base}.md`);
|
|
290
761
|
return rel;
|
|
291
762
|
}
|
|
292
763
|
async function createArticle(opts) {
|
|
293
764
|
const { collection, title } = parseAddTarget(opts.target);
|
|
294
765
|
const { name, dir } = await resolveCollectionDir(collection);
|
|
295
|
-
const filePath =
|
|
766
|
+
const filePath = import_node_path3.default.join(dir, `${slugify(title)}.md`);
|
|
296
767
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
297
768
|
const body = opts.body ?? `# ${title}
|
|
298
769
|
`;
|
|
299
|
-
await
|
|
300
|
-
await
|
|
770
|
+
await import_promises5.default.mkdir(dir, { recursive: true });
|
|
771
|
+
await import_promises5.default.writeFile(filePath, serializeArticle({ title, tags: opts.tags, created: now, updated: now, body }), "utf8");
|
|
772
|
+
void fireHook2("article-created", name, import_node_path3.default.relative(dir, filePath));
|
|
301
773
|
return { title, tags: opts.tags, path: filePath, collection: name };
|
|
302
774
|
}
|
|
303
775
|
async function loadArticle(target) {
|
|
304
776
|
const { collection, file } = parsePathTarget(target);
|
|
305
777
|
const { dir } = await resolveCollectionDir(collection);
|
|
306
|
-
const filePath =
|
|
778
|
+
const filePath = import_node_path3.default.join(dir, ensureMdPath(file));
|
|
307
779
|
try {
|
|
308
|
-
return { filePath, raw: await
|
|
780
|
+
return { filePath, dir, raw: await import_promises5.default.readFile(filePath, "utf8"), collection };
|
|
309
781
|
} catch (err) {
|
|
310
782
|
if (err.code === "ENOENT") throw new ArticleNotFoundError(target);
|
|
311
783
|
throw err;
|
|
312
784
|
}
|
|
313
785
|
}
|
|
314
786
|
async function updateArticleBody(target, body) {
|
|
315
|
-
const { filePath, raw, collection } = await loadArticle(target);
|
|
787
|
+
const { filePath, dir, raw, collection } = await loadArticle(target);
|
|
316
788
|
const parsed = parseArticle(raw);
|
|
317
|
-
await
|
|
789
|
+
await import_promises5.default.writeFile(
|
|
318
790
|
filePath,
|
|
319
791
|
serializeArticle({
|
|
320
792
|
title: parsed.title,
|
|
@@ -325,13 +797,14 @@ async function updateArticleBody(target, body) {
|
|
|
325
797
|
}),
|
|
326
798
|
"utf8"
|
|
327
799
|
);
|
|
800
|
+
void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
|
|
328
801
|
return { title: parsed.title, tags: parsed.tags, path: filePath, collection };
|
|
329
802
|
}
|
|
330
803
|
async function mutateTags(target, fn) {
|
|
331
|
-
const { filePath, raw, collection } = await loadArticle(target);
|
|
804
|
+
const { filePath, dir, raw, collection } = await loadArticle(target);
|
|
332
805
|
const parsed = parseArticle(raw);
|
|
333
806
|
const tags = fn(parsed.tags);
|
|
334
|
-
await
|
|
807
|
+
await import_promises5.default.writeFile(
|
|
335
808
|
filePath,
|
|
336
809
|
serializeArticle({
|
|
337
810
|
title: parsed.title,
|
|
@@ -342,6 +815,7 @@ async function mutateTags(target, fn) {
|
|
|
342
815
|
}),
|
|
343
816
|
"utf8"
|
|
344
817
|
);
|
|
818
|
+
void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
|
|
345
819
|
return { title: parsed.title, tags, path: filePath, collection };
|
|
346
820
|
}
|
|
347
821
|
function addTags(target, tags) {
|
|
@@ -355,34 +829,37 @@ function setTags(target, tags) {
|
|
|
355
829
|
return mutateTags(target, () => Array.from(new Set(tags)));
|
|
356
830
|
}
|
|
357
831
|
async function removeArticle(target) {
|
|
358
|
-
const { filePath } = await loadArticle(target);
|
|
359
|
-
await
|
|
832
|
+
const { filePath, dir, collection } = await loadArticle(target);
|
|
833
|
+
await import_promises5.default.rm(filePath);
|
|
834
|
+
void fireHook2("article-removed", collection, import_node_path3.default.relative(dir, filePath));
|
|
360
835
|
return filePath;
|
|
361
836
|
}
|
|
362
837
|
|
|
363
838
|
// src/repo.ts
|
|
364
|
-
var
|
|
365
|
-
var
|
|
366
|
-
var
|
|
839
|
+
var import_promises6 = __toESM(require("fs/promises"), 1);
|
|
840
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
841
|
+
var import_node_child_process2 = require("child_process");
|
|
367
842
|
var import_node_util = require("util");
|
|
368
|
-
var
|
|
369
|
-
|
|
843
|
+
var import_yaml2 = require("yaml");
|
|
844
|
+
init_collections();
|
|
845
|
+
init_registry();
|
|
846
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process2.execFile);
|
|
370
847
|
var REPO_CONFIG_FILE = "diffwiki.yaml";
|
|
371
848
|
async function detectRepoName(cwd) {
|
|
372
849
|
try {
|
|
373
850
|
const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd });
|
|
374
|
-
return
|
|
851
|
+
return import_node_path4.default.basename(stdout.trim());
|
|
375
852
|
} catch {
|
|
376
|
-
return
|
|
853
|
+
return import_node_path4.default.basename(import_node_path4.default.resolve(cwd));
|
|
377
854
|
}
|
|
378
855
|
}
|
|
379
856
|
async function readRepoConfig(cwd) {
|
|
380
857
|
try {
|
|
381
|
-
const raw = await
|
|
382
|
-
const parsed = (0,
|
|
858
|
+
const raw = await import_promises6.default.readFile(import_node_path4.default.join(cwd, REPO_CONFIG_FILE), "utf8");
|
|
859
|
+
const parsed = (0, import_yaml2.parse)(raw) ?? {};
|
|
383
860
|
if (!parsed.path) return void 0;
|
|
384
861
|
return {
|
|
385
|
-
collection: parsed.collection ??
|
|
862
|
+
collection: parsed.collection ?? import_node_path4.default.basename(import_node_path4.default.resolve(cwd)),
|
|
386
863
|
path: parsed.path
|
|
387
864
|
};
|
|
388
865
|
} catch (err) {
|
|
@@ -391,9 +868,9 @@ async function readRepoConfig(cwd) {
|
|
|
391
868
|
}
|
|
392
869
|
}
|
|
393
870
|
async function initRepoWiki(opts) {
|
|
394
|
-
const cwd =
|
|
871
|
+
const cwd = import_node_path4.default.resolve(opts.cwd);
|
|
395
872
|
const wikiPath = opts.wikiPath ?? "wiki";
|
|
396
|
-
const absWiki =
|
|
873
|
+
const absWiki = import_node_path4.default.join(cwd, wikiPath);
|
|
397
874
|
let name = opts.collection ?? await detectRepoName(cwd);
|
|
398
875
|
const existing = await findCollection(name);
|
|
399
876
|
if (existing && existing.path !== absWiki) {
|
|
@@ -408,9 +885,9 @@ async function initRepoWiki(opts) {
|
|
|
408
885
|
n += 1;
|
|
409
886
|
}
|
|
410
887
|
}
|
|
411
|
-
await
|
|
888
|
+
await import_promises6.default.mkdir(absWiki, { recursive: true });
|
|
412
889
|
const config = { collection: name, path: wikiPath };
|
|
413
|
-
await
|
|
890
|
+
await import_promises6.default.writeFile(import_node_path4.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml2.stringify)(config), "utf8");
|
|
414
891
|
const entry = {
|
|
415
892
|
name,
|
|
416
893
|
type: "repo",
|
|
@@ -422,44 +899,371 @@ async function initRepoWiki(opts) {
|
|
|
422
899
|
}
|
|
423
900
|
|
|
424
901
|
// src/query.ts
|
|
425
|
-
var
|
|
426
|
-
var
|
|
427
|
-
|
|
428
|
-
|
|
902
|
+
var import_promises7 = __toESM(require("fs/promises"), 1);
|
|
903
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
904
|
+
init_collections();
|
|
905
|
+
init_errors();
|
|
906
|
+
init_loader();
|
|
907
|
+
init_registry2();
|
|
908
|
+
init_types();
|
|
909
|
+
async function collectMarkdownFiles(dir) {
|
|
910
|
+
let entries;
|
|
911
|
+
try {
|
|
912
|
+
entries = await import_promises7.default.readdir(dir, { withFileTypes: true });
|
|
913
|
+
} catch {
|
|
914
|
+
return [];
|
|
915
|
+
}
|
|
916
|
+
const files = [];
|
|
917
|
+
for (const entry of entries) {
|
|
918
|
+
const full = import_node_path5.default.join(dir, entry.name);
|
|
919
|
+
if (entry.isDirectory()) {
|
|
920
|
+
files.push(...await collectMarkdownFiles(full));
|
|
921
|
+
} else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
|
|
922
|
+
files.push(full);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return files;
|
|
926
|
+
}
|
|
927
|
+
var K1 = 1.5;
|
|
928
|
+
var B = 0.75;
|
|
929
|
+
var FIELD_WEIGHTS = { title: 3, tags: 2, collection: 1.5, body: 1 };
|
|
930
|
+
function tokenize(text) {
|
|
931
|
+
return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 0);
|
|
932
|
+
}
|
|
933
|
+
function termFreqs(tokens) {
|
|
934
|
+
const freqs = /* @__PURE__ */ new Map();
|
|
935
|
+
for (const t of tokens) {
|
|
936
|
+
freqs.set(t, (freqs.get(t) ?? 0) + 1);
|
|
937
|
+
}
|
|
938
|
+
return freqs;
|
|
939
|
+
}
|
|
940
|
+
function bm25FieldScore(queryTerms, fieldTokens, avgFieldLen, idf) {
|
|
941
|
+
const tf = termFreqs(fieldTokens);
|
|
942
|
+
const dl = fieldTokens.length;
|
|
943
|
+
const avgDl = avgFieldLen || 1;
|
|
944
|
+
let score = 0;
|
|
945
|
+
for (const term of queryTerms) {
|
|
946
|
+
const freq = tf.get(term) ?? 0;
|
|
947
|
+
if (freq === 0) continue;
|
|
948
|
+
const termIdf = idf.get(term) ?? 0;
|
|
949
|
+
const numerator = freq * (K1 + 1);
|
|
950
|
+
const denominator = freq + K1 * (1 - B + B * (dl / avgDl));
|
|
951
|
+
score += termIdf * (numerator / denominator);
|
|
952
|
+
}
|
|
953
|
+
return score;
|
|
954
|
+
}
|
|
955
|
+
async function nativeSearch(term, collections, collection) {
|
|
956
|
+
let targets = collections;
|
|
429
957
|
if (collection) {
|
|
430
|
-
const one =
|
|
958
|
+
const one = collections.find((c) => c.name === collection);
|
|
431
959
|
if (!one) throw new CollectionNotFoundError(collection);
|
|
432
960
|
targets = [one];
|
|
433
|
-
} else {
|
|
434
|
-
targets = await listCollections();
|
|
435
961
|
}
|
|
436
|
-
const
|
|
437
|
-
const hits = [];
|
|
962
|
+
const docs = [];
|
|
438
963
|
for (const entry of targets) {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
964
|
+
for (const full of await collectMarkdownFiles(entry.path)) {
|
|
965
|
+
const fallbackTitle = import_node_path5.default.basename(full).replace(/\.mdx?$/, "");
|
|
966
|
+
let title = fallbackTitle;
|
|
967
|
+
let tags = [];
|
|
968
|
+
let body = "";
|
|
969
|
+
try {
|
|
970
|
+
const parsed = parseArticle(await import_promises7.default.readFile(full, "utf8"));
|
|
971
|
+
if (parsed.title) title = parsed.title;
|
|
972
|
+
tags = parsed.tags;
|
|
973
|
+
body = parsed.body;
|
|
974
|
+
} catch {
|
|
975
|
+
}
|
|
976
|
+
docs.push({
|
|
977
|
+
collectionName: entry.name,
|
|
978
|
+
filePath: full,
|
|
979
|
+
title,
|
|
980
|
+
tags,
|
|
981
|
+
fields: {
|
|
982
|
+
title: tokenize(title),
|
|
983
|
+
tags: tokenize(tags.join(" ")),
|
|
984
|
+
collection: tokenize(entry.name),
|
|
985
|
+
body: tokenize(body)
|
|
986
|
+
}
|
|
987
|
+
});
|
|
444
988
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
989
|
+
}
|
|
990
|
+
if (!term.trim()) {
|
|
991
|
+
return docs.map((d) => ({
|
|
992
|
+
collection: d.collectionName,
|
|
993
|
+
path: d.filePath,
|
|
994
|
+
title: d.title,
|
|
995
|
+
tags: d.tags
|
|
996
|
+
}));
|
|
997
|
+
}
|
|
998
|
+
const queryTerms = tokenize(term);
|
|
999
|
+
if (queryTerms.length === 0) {
|
|
1000
|
+
return docs.map((d) => ({
|
|
1001
|
+
collection: d.collectionName,
|
|
1002
|
+
path: d.filePath,
|
|
1003
|
+
title: d.title,
|
|
1004
|
+
tags: d.tags
|
|
1005
|
+
}));
|
|
1006
|
+
}
|
|
1007
|
+
const docTokenSets = docs.map(
|
|
1008
|
+
(doc) => /* @__PURE__ */ new Set([...doc.fields.title, ...doc.fields.tags, ...doc.fields.collection, ...doc.fields.body])
|
|
1009
|
+
);
|
|
1010
|
+
const N = docs.length;
|
|
1011
|
+
const idf = /* @__PURE__ */ new Map();
|
|
1012
|
+
for (const qt of queryTerms) {
|
|
1013
|
+
let docFreq = 0;
|
|
1014
|
+
for (const tokenSet of docTokenSets) {
|
|
1015
|
+
if (tokenSet.has(qt)) docFreq++;
|
|
1016
|
+
}
|
|
1017
|
+
idf.set(qt, Math.log((N - docFreq + 0.5) / (docFreq + 0.5) + 1));
|
|
1018
|
+
}
|
|
1019
|
+
const avgLens = {
|
|
1020
|
+
title: docs.reduce((sum, d) => sum + d.fields.title.length, 0) / (N || 1),
|
|
1021
|
+
tags: docs.reduce((sum, d) => sum + d.fields.tags.length, 0) / (N || 1),
|
|
1022
|
+
collection: docs.reduce((sum, d) => sum + d.fields.collection.length, 0) / (N || 1),
|
|
1023
|
+
body: docs.reduce((sum, d) => sum + d.fields.body.length, 0) / (N || 1)
|
|
1024
|
+
};
|
|
1025
|
+
const scored = [];
|
|
1026
|
+
for (const doc of docs) {
|
|
1027
|
+
const titleScore = bm25FieldScore(queryTerms, doc.fields.title, avgLens.title, idf) * FIELD_WEIGHTS.title;
|
|
1028
|
+
const tagsScore = bm25FieldScore(queryTerms, doc.fields.tags, avgLens.tags, idf) * FIELD_WEIGHTS.tags;
|
|
1029
|
+
const collScore = bm25FieldScore(queryTerms, doc.fields.collection, avgLens.collection, idf) * FIELD_WEIGHTS.collection;
|
|
1030
|
+
const bodyScore = bm25FieldScore(queryTerms, doc.fields.body, avgLens.body, idf) * FIELD_WEIGHTS.body;
|
|
1031
|
+
const total = titleScore + tagsScore + collScore + bodyScore;
|
|
1032
|
+
if (total > 0) {
|
|
1033
|
+
scored.push({ doc, score: total });
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1037
|
+
return scored.map((s) => ({
|
|
1038
|
+
collection: s.doc.collectionName,
|
|
1039
|
+
path: s.doc.filePath,
|
|
1040
|
+
title: s.doc.title,
|
|
1041
|
+
tags: s.doc.tags,
|
|
1042
|
+
score: s.score
|
|
1043
|
+
}));
|
|
1044
|
+
}
|
|
1045
|
+
async function resolveDefaultEngine() {
|
|
1046
|
+
const config = await readConfig();
|
|
1047
|
+
const enabled = await listEnabledPlugins("search");
|
|
1048
|
+
const configured = config.defaultSearch?.split(":")[0];
|
|
1049
|
+
if (configured && enabled.some((p) => p.name === configured)) return configured;
|
|
1050
|
+
if (enabled.length > 0) return enabled[0].name;
|
|
1051
|
+
return NATIVE_ENGINE;
|
|
1052
|
+
}
|
|
1053
|
+
async function resolveDefaultSearchMode() {
|
|
1054
|
+
const config = await readConfig();
|
|
1055
|
+
const enabled = await listEnabledPlugins("search");
|
|
1056
|
+
const configured = config.defaultSearch?.split(":")[0];
|
|
1057
|
+
const configuredType = config.defaultSearch?.split(":")[1];
|
|
1058
|
+
let engine = NATIVE_ENGINE;
|
|
1059
|
+
if (configured && enabled.some((p) => p.name === configured)) engine = configured;
|
|
1060
|
+
else if (enabled.length > 0) engine = enabled[0].name;
|
|
1061
|
+
if (engine === NATIVE_ENGINE) return { engine, type: "basic" };
|
|
1062
|
+
const modes = await listSearchModes();
|
|
1063
|
+
const engineModes = modes.filter((m) => m.plugin === engine);
|
|
1064
|
+
if (engineModes.length === 0) return { engine: NATIVE_ENGINE, type: "basic" };
|
|
1065
|
+
if (configuredType && engineModes.some((m) => m.type === configuredType)) {
|
|
1066
|
+
return { engine, type: configuredType };
|
|
1067
|
+
}
|
|
1068
|
+
return { engine, type: engineModes[0].type };
|
|
1069
|
+
}
|
|
1070
|
+
async function query(term, opts) {
|
|
1071
|
+
const collections = await listCollections();
|
|
1072
|
+
const engine = opts?.engine ?? await resolveDefaultEngine();
|
|
1073
|
+
if (engine === NATIVE_ENGINE) return nativeSearch(term, collections, opts?.collection);
|
|
1074
|
+
const provider = await loadSearchProvider(engine);
|
|
1075
|
+
if (!provider) return nativeSearch(term, collections, opts?.collection);
|
|
1076
|
+
try {
|
|
1077
|
+
return await provider.search(term, collections, opts);
|
|
1078
|
+
} catch {
|
|
1079
|
+
return nativeSearch(term, collections, opts?.collection);
|
|
1080
|
+
} finally {
|
|
1081
|
+
await provider.close();
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// src/doctor.ts
|
|
1086
|
+
var import_promises9 = __toESM(require("fs/promises"), 1);
|
|
1087
|
+
init_paths();
|
|
1088
|
+
init_registry();
|
|
1089
|
+
init_collections();
|
|
1090
|
+
|
|
1091
|
+
// src/plugins/manager.ts
|
|
1092
|
+
var import_promises8 = __toESM(require("fs/promises"), 1);
|
|
1093
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
1094
|
+
var import_node_child_process3 = require("child_process");
|
|
1095
|
+
var import_node_util2 = require("util");
|
|
1096
|
+
init_paths();
|
|
1097
|
+
init_errors();
|
|
1098
|
+
init_collections();
|
|
1099
|
+
init_registry2();
|
|
1100
|
+
init_loader();
|
|
1101
|
+
var run = (0, import_node_util2.promisify)(import_node_child_process3.execFile);
|
|
1102
|
+
async function readPackageJson(file) {
|
|
1103
|
+
try {
|
|
1104
|
+
return JSON.parse(await import_promises8.default.readFile(file, "utf8"));
|
|
1105
|
+
} catch {
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
function binOf(manifest) {
|
|
1110
|
+
if (typeof manifest.bin === "string") return manifest.bin;
|
|
1111
|
+
if (manifest.bin && typeof manifest.bin === "object") {
|
|
1112
|
+
const first = Object.values(manifest.bin)[0];
|
|
1113
|
+
return typeof first === "string" ? first : null;
|
|
1114
|
+
}
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
async function pluginManifestDirs(nodeModules) {
|
|
1118
|
+
let entries;
|
|
1119
|
+
try {
|
|
1120
|
+
entries = await import_promises8.default.readdir(nodeModules);
|
|
1121
|
+
} catch {
|
|
1122
|
+
return [];
|
|
1123
|
+
}
|
|
1124
|
+
const dirs = [];
|
|
1125
|
+
for (const entry of entries) {
|
|
1126
|
+
if (entry.startsWith(".")) continue;
|
|
1127
|
+
if (entry.startsWith("@")) {
|
|
1128
|
+
let scoped;
|
|
1129
|
+
try {
|
|
1130
|
+
scoped = await import_promises8.default.readdir(import_node_path6.default.join(nodeModules, entry));
|
|
1131
|
+
} catch {
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
for (const pkg of scoped) {
|
|
1135
|
+
if (pkg.startsWith(".")) continue;
|
|
1136
|
+
dirs.push(import_node_path6.default.join(nodeModules, entry, pkg));
|
|
452
1137
|
}
|
|
1138
|
+
} else {
|
|
1139
|
+
dirs.push(import_node_path6.default.join(nodeModules, entry));
|
|
453
1140
|
}
|
|
454
1141
|
}
|
|
455
|
-
return
|
|
1142
|
+
return dirs;
|
|
1143
|
+
}
|
|
1144
|
+
async function ensurePluginRoot() {
|
|
1145
|
+
const dir = pluginsDir();
|
|
1146
|
+
await import_promises8.default.mkdir(dir, { recursive: true });
|
|
1147
|
+
const pkgPath = import_node_path6.default.join(dir, "package.json");
|
|
1148
|
+
if (await readPackageJson(pkgPath) === null) {
|
|
1149
|
+
const pkg = { name: "diffwiki-plugins", private: true, version: "0.0.0" };
|
|
1150
|
+
await import_promises8.default.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
1151
|
+
`, "utf8");
|
|
1152
|
+
}
|
|
1153
|
+
return dir;
|
|
1154
|
+
}
|
|
1155
|
+
async function discoverSearchPackage(dir) {
|
|
1156
|
+
const nodeModules = import_node_path6.default.join(dir, "node_modules");
|
|
1157
|
+
for (const pkgDir of await pluginManifestDirs(nodeModules)) {
|
|
1158
|
+
const manifest = await readPackageJson(import_node_path6.default.join(pkgDir, "package.json"));
|
|
1159
|
+
if (!manifest?.name || manifest.diffwiki?.kind !== "search") continue;
|
|
1160
|
+
const bin = binOf(manifest);
|
|
1161
|
+
if (!bin) continue;
|
|
1162
|
+
return { name: manifest.name, version: manifest.version, binAbs: import_node_path6.default.resolve(pkgDir, bin) };
|
|
1163
|
+
}
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
async function bestEffortClose(provider) {
|
|
1167
|
+
try {
|
|
1168
|
+
await provider?.close();
|
|
1169
|
+
} catch {
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
async function runSetup(name, prefetchModels) {
|
|
1173
|
+
let provider = null;
|
|
1174
|
+
try {
|
|
1175
|
+
provider = await loadSearchProvider(name);
|
|
1176
|
+
if (provider) {
|
|
1177
|
+
await provider.setup(await listCollections(), { embed: false, prefetchModels });
|
|
1178
|
+
}
|
|
1179
|
+
} catch {
|
|
1180
|
+
} finally {
|
|
1181
|
+
await bestEffortClose(provider);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
async function installPlugin(spec, opts) {
|
|
1185
|
+
const dir = await ensurePluginRoot();
|
|
1186
|
+
try {
|
|
1187
|
+
await run("npm", ["install", spec, "--prefix", dir]);
|
|
1188
|
+
} catch (err) {
|
|
1189
|
+
throw new PluginError(`npm install ${spec} failed: ${err.message}`);
|
|
1190
|
+
}
|
|
1191
|
+
const discovered = await discoverSearchPackage(dir);
|
|
1192
|
+
if (!discovered) {
|
|
1193
|
+
throw new PluginError(
|
|
1194
|
+
`no diffwiki search plugin found in "${spec}" (need package.json diffwiki.kind === "search" + bin)`
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
const record = {
|
|
1198
|
+
name: discovered.name,
|
|
1199
|
+
kind: "search",
|
|
1200
|
+
command: ["node", discovered.binAbs],
|
|
1201
|
+
version: discovered.version,
|
|
1202
|
+
enabled: true,
|
|
1203
|
+
managed: true
|
|
1204
|
+
};
|
|
1205
|
+
await upsertPlugin(record);
|
|
1206
|
+
if (opts?.setup !== false) {
|
|
1207
|
+
await runSetup(record.name, !!opts?.prefetchModels);
|
|
1208
|
+
}
|
|
1209
|
+
return record;
|
|
1210
|
+
}
|
|
1211
|
+
async function registerPlugin(name, command) {
|
|
1212
|
+
if (command.length === 0) throw new PluginError(`register "${name}": --command must not be empty`);
|
|
1213
|
+
const record = { name, kind: "search", command, enabled: true, managed: false };
|
|
1214
|
+
await upsertPlugin(record);
|
|
1215
|
+
const provider = await loadSearchProvider(name);
|
|
1216
|
+
if (!provider) {
|
|
1217
|
+
await deregisterPlugin(name);
|
|
1218
|
+
throw new PluginError(`plugin "${name}" did not spawn as a search plugin (command: ${command.join(" ")})`);
|
|
1219
|
+
}
|
|
1220
|
+
try {
|
|
1221
|
+
await provider.setup(await listCollections(), { embed: false, prefetchModels: false });
|
|
1222
|
+
} catch {
|
|
1223
|
+
} finally {
|
|
1224
|
+
await bestEffortClose(provider);
|
|
1225
|
+
}
|
|
1226
|
+
return record;
|
|
1227
|
+
}
|
|
1228
|
+
async function removePlugin(name) {
|
|
1229
|
+
const record = (await readPluginRegistry()).plugins.find((p) => p.name === name);
|
|
1230
|
+
if (!record) throw new PluginError(`plugin "${name}" is not registered`);
|
|
1231
|
+
if (record.managed) {
|
|
1232
|
+
const dir = await ensurePluginRoot();
|
|
1233
|
+
try {
|
|
1234
|
+
await run("npm", ["uninstall", name, "--prefix", dir]);
|
|
1235
|
+
} catch {
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
await deregisterPlugin(name);
|
|
1239
|
+
}
|
|
1240
|
+
async function probeSpawnable(name) {
|
|
1241
|
+
let provider = null;
|
|
1242
|
+
try {
|
|
1243
|
+
provider = await loadSearchProvider(name);
|
|
1244
|
+
return provider !== null;
|
|
1245
|
+
} catch {
|
|
1246
|
+
return false;
|
|
1247
|
+
} finally {
|
|
1248
|
+
await bestEffortClose(provider);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
async function listPlugins() {
|
|
1252
|
+
const registry = await readPluginRegistry();
|
|
1253
|
+
const out = [];
|
|
1254
|
+
for (const record of registry.plugins) {
|
|
1255
|
+
const spawnable = record.enabled ? await probeSpawnable(record.name) : false;
|
|
1256
|
+
out.push({ ...record, spawnable });
|
|
1257
|
+
}
|
|
1258
|
+
return out;
|
|
456
1259
|
}
|
|
457
1260
|
|
|
458
1261
|
// src/doctor.ts
|
|
459
|
-
|
|
1262
|
+
init_loader();
|
|
1263
|
+
init_types();
|
|
460
1264
|
async function exists(target) {
|
|
461
1265
|
try {
|
|
462
|
-
await
|
|
1266
|
+
await import_promises9.default.access(target);
|
|
463
1267
|
return true;
|
|
464
1268
|
} catch {
|
|
465
1269
|
return false;
|
|
@@ -492,8 +1296,309 @@ async function doctor() {
|
|
|
492
1296
|
} else {
|
|
493
1297
|
out.push({ level: "warn", message: "no default collection set" });
|
|
494
1298
|
}
|
|
1299
|
+
await appendPluginDiagnostics(out, config);
|
|
495
1300
|
return out;
|
|
496
1301
|
}
|
|
1302
|
+
function readinessLine(name, health) {
|
|
1303
|
+
const r = health.readiness;
|
|
1304
|
+
if (!r) {
|
|
1305
|
+
return { level: health.ready ? "ok" : "warn", message: `plugin "${name}" ${health.ready ? "ready" : "not ready"}` };
|
|
1306
|
+
}
|
|
1307
|
+
if (r.firstSearchCost === "model-download") {
|
|
1308
|
+
return {
|
|
1309
|
+
level: "warn",
|
|
1310
|
+
message: `plugin "${name}": ${r.docsIndexed} doc(s) indexed \u2014 first search will download models (~2GB)`
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
if (r.firstSearchCost === "model-load") {
|
|
1314
|
+
return {
|
|
1315
|
+
level: "warn",
|
|
1316
|
+
message: `plugin "${name}": ${r.docsIndexed} doc(s) indexed \u2014 first search loads models (a few seconds)`
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
return { level: "ok", message: `plugin "${name}": ${r.docsIndexed} doc(s) indexed \u2014 first search is instant` };
|
|
1320
|
+
}
|
|
1321
|
+
async function appendPluginDiagnostics(out, config) {
|
|
1322
|
+
let plugins = [];
|
|
1323
|
+
try {
|
|
1324
|
+
plugins = await listPlugins();
|
|
1325
|
+
} catch (err) {
|
|
1326
|
+
out.push({ level: "error", message: `failed to read plugin registry: ${err.message}` });
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
if (plugins.length === 0) {
|
|
1330
|
+
out.push({ level: "warn", message: "no search plugins installed \u2014 using built-in native search" });
|
|
1331
|
+
}
|
|
1332
|
+
const collections = await listCollections();
|
|
1333
|
+
for (const p of plugins) {
|
|
1334
|
+
if (!p.enabled) {
|
|
1335
|
+
out.push({ level: "warn", message: `plugin "${p.name}" registered but disabled` });
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
if (!p.spawnable) {
|
|
1339
|
+
out.push({
|
|
1340
|
+
level: "error",
|
|
1341
|
+
message: `plugin "${p.name}" registered but its command is not spawnable: ${p.command.join(" ")}`
|
|
1342
|
+
});
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
out.push({ level: "ok", message: `plugin "${p.name}" enabled and spawnable` });
|
|
1346
|
+
let provider = null;
|
|
1347
|
+
try {
|
|
1348
|
+
provider = await loadSearchProvider(p.name);
|
|
1349
|
+
if (!provider) {
|
|
1350
|
+
out.push({ level: "error", message: `plugin "${p.name}" failed to load for a health check` });
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
const health = await provider.health(collections);
|
|
1354
|
+
for (const d of health.diagnostics) out.push({ level: d.level, message: `${p.name} \xB7 ${d.message}` });
|
|
1355
|
+
out.push(readinessLine(p.name, health));
|
|
1356
|
+
} catch (err) {
|
|
1357
|
+
out.push({ level: "error", message: `plugin "${p.name}" health check failed: ${err.message}` });
|
|
1358
|
+
} finally {
|
|
1359
|
+
try {
|
|
1360
|
+
await provider?.close();
|
|
1361
|
+
} catch {
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
if (config.defaultSearch) {
|
|
1366
|
+
const engine = config.defaultSearch.split(":")[0];
|
|
1367
|
+
const known = engine === NATIVE_ENGINE || plugins.some((p) => p.name === engine && p.enabled);
|
|
1368
|
+
if (!known) {
|
|
1369
|
+
out.push({
|
|
1370
|
+
level: "warn",
|
|
1371
|
+
message: `defaultSearch "${config.defaultSearch}" is not installed/enabled \u2014 using native`
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// src/browse.ts
|
|
1378
|
+
var import_promises10 = __toESM(require("fs/promises"), 1);
|
|
1379
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
1380
|
+
init_collections();
|
|
1381
|
+
init_errors();
|
|
1382
|
+
var INDEX_RE = /^(index|readme)\.mdx?$/i;
|
|
1383
|
+
function buildArticleTree(relPaths) {
|
|
1384
|
+
const root = {};
|
|
1385
|
+
for (const raw of relPaths) {
|
|
1386
|
+
const rel = raw.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
1387
|
+
if (!rel) continue;
|
|
1388
|
+
const segs = rel.split("/");
|
|
1389
|
+
let cur = root;
|
|
1390
|
+
segs.forEach((seg, i) => {
|
|
1391
|
+
if (i === segs.length - 1) {
|
|
1392
|
+
if (!(seg in cur)) cur[seg] = null;
|
|
1393
|
+
} else {
|
|
1394
|
+
if (cur[seg] === null || !(seg in cur)) cur[seg] = {};
|
|
1395
|
+
cur = cur[seg];
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
const stripExt = (f) => f.replace(/\.(mdx?)$/, "");
|
|
1400
|
+
const walk = (trie, parents) => {
|
|
1401
|
+
const entries = Object.entries(trie);
|
|
1402
|
+
const branches = entries.filter(([, v]) => v !== null);
|
|
1403
|
+
const leaves = entries.filter(([, v]) => v === null).map(([k]) => k).filter((f) => !INDEX_RE.test(f));
|
|
1404
|
+
branches.sort(([a], [b]) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
1405
|
+
leaves.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
1406
|
+
const branchNodes = branches.map(([seg, sub]) => ({
|
|
1407
|
+
name: seg,
|
|
1408
|
+
title: seg,
|
|
1409
|
+
// Folders are navigable to their index page (an authored index.md/README,
|
|
1410
|
+
// else a generated listing).
|
|
1411
|
+
slug: [...parents, seg].join("/"),
|
|
1412
|
+
children: walk(sub, [...parents, seg])
|
|
1413
|
+
}));
|
|
1414
|
+
const leafNodes = leaves.map((file) => {
|
|
1415
|
+
const nameNoExt = stripExt(file);
|
|
1416
|
+
return { name: nameNoExt, title: nameNoExt, slug: [...parents, nameNoExt].join("/") };
|
|
1417
|
+
});
|
|
1418
|
+
return [...branchNodes, ...leafNodes];
|
|
1419
|
+
};
|
|
1420
|
+
return walk(root, []);
|
|
1421
|
+
}
|
|
1422
|
+
async function collectRelPaths(dir, rel = "") {
|
|
1423
|
+
let dirents;
|
|
1424
|
+
try {
|
|
1425
|
+
dirents = await import_promises10.default.readdir(dir, { withFileTypes: true });
|
|
1426
|
+
} catch {
|
|
1427
|
+
return [];
|
|
1428
|
+
}
|
|
1429
|
+
const out = [];
|
|
1430
|
+
for (const d of dirents) {
|
|
1431
|
+
if (d.name.startsWith(".")) continue;
|
|
1432
|
+
const childRel = rel ? `${rel}/${d.name}` : d.name;
|
|
1433
|
+
if (d.isDirectory()) {
|
|
1434
|
+
out.push(...await collectRelPaths(import_node_path7.default.join(dir, d.name), childRel));
|
|
1435
|
+
} else if (/\.(mdx?)$/.test(d.name)) {
|
|
1436
|
+
out.push(childRel);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
return out;
|
|
1440
|
+
}
|
|
1441
|
+
async function readdirSafe(dir) {
|
|
1442
|
+
try {
|
|
1443
|
+
return await import_promises10.default.readdir(dir);
|
|
1444
|
+
} catch {
|
|
1445
|
+
return [];
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
function pickIndexFile(names) {
|
|
1449
|
+
const rank = (n) => {
|
|
1450
|
+
const lower = n.toLowerCase();
|
|
1451
|
+
const base = lower.startsWith("index") ? 0 : 1;
|
|
1452
|
+
const ext = lower.endsWith(".mdx") ? 0 : 1;
|
|
1453
|
+
return base * 2 + ext;
|
|
1454
|
+
};
|
|
1455
|
+
return names.filter((n) => INDEX_RE.test(n)).sort((a, b) => rank(a) - rank(b))[0];
|
|
1456
|
+
}
|
|
1457
|
+
async function cheapTitle(absPath) {
|
|
1458
|
+
let handle;
|
|
1459
|
+
try {
|
|
1460
|
+
handle = await import_promises10.default.open(absPath, "r");
|
|
1461
|
+
const buf = Buffer.alloc(512);
|
|
1462
|
+
const { bytesRead } = await handle.read(buf, 0, 512, 0);
|
|
1463
|
+
const snippet = buf.subarray(0, bytesRead).toString("utf8");
|
|
1464
|
+
const m = snippet.match(/^---[\s\S]*?^title:\s*(.+)$/m);
|
|
1465
|
+
if (m) return m[1].trim().replace(/^["']|["']$/g, "");
|
|
1466
|
+
} catch {
|
|
1467
|
+
} finally {
|
|
1468
|
+
await handle?.close();
|
|
1469
|
+
}
|
|
1470
|
+
return void 0;
|
|
1471
|
+
}
|
|
1472
|
+
async function enrichTitles(nodes, collDir) {
|
|
1473
|
+
return Promise.all(
|
|
1474
|
+
nodes.map(async (node) => {
|
|
1475
|
+
if (node.children) {
|
|
1476
|
+
const children = await enrichTitles(node.children, collDir);
|
|
1477
|
+
let title = node.title;
|
|
1478
|
+
if (node.slug) {
|
|
1479
|
+
const dir = import_node_path7.default.join(collDir, node.slug);
|
|
1480
|
+
const idx = pickIndexFile(await readdirSafe(dir));
|
|
1481
|
+
if (idx) title = await cheapTitle(import_node_path7.default.join(dir, idx)) ?? title;
|
|
1482
|
+
}
|
|
1483
|
+
return { ...node, title, children };
|
|
1484
|
+
}
|
|
1485
|
+
for (const ext of [".mdx", ".md"]) {
|
|
1486
|
+
const title = await cheapTitle(import_node_path7.default.join(collDir, `${node.slug}${ext}`));
|
|
1487
|
+
if (title) return { ...node, title };
|
|
1488
|
+
}
|
|
1489
|
+
return node;
|
|
1490
|
+
})
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
async function listArticleTree(coll) {
|
|
1494
|
+
const entry = await findCollection(coll);
|
|
1495
|
+
if (!entry) throw new CollectionNotFoundError(coll);
|
|
1496
|
+
const rels = await collectRelPaths(entry.path);
|
|
1497
|
+
return enrichTitles(buildArticleTree(rels), import_node_path7.default.resolve(entry.path));
|
|
1498
|
+
}
|
|
1499
|
+
async function readArticle(coll, slug) {
|
|
1500
|
+
const entry = await findCollection(coll);
|
|
1501
|
+
if (!entry) throw new CollectionNotFoundError(coll);
|
|
1502
|
+
const collDir = import_node_path7.default.resolve(entry.path);
|
|
1503
|
+
const normalized = slug.replace(/\\/g, "/");
|
|
1504
|
+
if (normalized.split("/").some((s) => s === "..")) {
|
|
1505
|
+
throw new InvalidTargetError(slug, "path traversal detected");
|
|
1506
|
+
}
|
|
1507
|
+
const safeSlug = normalized.split("/").filter((s) => s !== "" && s !== ".").join("/");
|
|
1508
|
+
const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path7.default.sep);
|
|
1509
|
+
let abs;
|
|
1510
|
+
for (const rel of safeSlug ? [`${safeSlug}.mdx`, `${safeSlug}.md`] : []) {
|
|
1511
|
+
const candidate = import_node_path7.default.resolve(collDir, rel);
|
|
1512
|
+
if (!inJail(candidate)) throw new InvalidTargetError(slug, "path traversal detected");
|
|
1513
|
+
try {
|
|
1514
|
+
await import_promises10.default.access(candidate);
|
|
1515
|
+
abs = candidate;
|
|
1516
|
+
break;
|
|
1517
|
+
} catch {
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
if (!abs) {
|
|
1521
|
+
const dir = import_node_path7.default.resolve(collDir, safeSlug);
|
|
1522
|
+
if (inJail(dir)) {
|
|
1523
|
+
const idx = pickIndexFile(await readdirSafe(dir));
|
|
1524
|
+
if (idx) abs = import_node_path7.default.join(dir, idx);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
|
|
1528
|
+
const parsed = parseArticle(await import_promises10.default.readFile(abs, "utf8"));
|
|
1529
|
+
const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
|
|
1530
|
+
return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs };
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// src/render.ts
|
|
1534
|
+
var import_unified = require("unified");
|
|
1535
|
+
var import_remark_parse = __toESM(require("remark-parse"), 1);
|
|
1536
|
+
var import_remark_gfm = __toESM(require("remark-gfm"), 1);
|
|
1537
|
+
var import_remark_rehype = __toESM(require("remark-rehype"), 1);
|
|
1538
|
+
var import_rehype_slug = __toESM(require("rehype-slug"), 1);
|
|
1539
|
+
var import_rehype = __toESM(require("@shikijs/rehype"), 1);
|
|
1540
|
+
var import_rehype_stringify = __toESM(require("rehype-stringify"), 1);
|
|
1541
|
+
var import_unist_util_visit = require("unist-util-visit");
|
|
1542
|
+
var import_hast_util_to_string = require("hast-util-to-string");
|
|
1543
|
+
var LANG_ALIASES = {
|
|
1544
|
+
"c#": "csharp",
|
|
1545
|
+
cs: "csharp",
|
|
1546
|
+
"c++": "cpp",
|
|
1547
|
+
cxx: "cpp",
|
|
1548
|
+
hs: "haskell",
|
|
1549
|
+
ex: "elixir"
|
|
1550
|
+
};
|
|
1551
|
+
function rehypeCollectToc(toc) {
|
|
1552
|
+
return (tree) => {
|
|
1553
|
+
(0, import_unist_util_visit.visit)(tree, "element", (node) => {
|
|
1554
|
+
if (node.tagName !== "h2" && node.tagName !== "h3") return;
|
|
1555
|
+
const id = node.properties?.id;
|
|
1556
|
+
if (!id) return;
|
|
1557
|
+
toc.push({ slug: String(id), text: (0, import_hast_util_to_string.toString)(node), depth: node.tagName === "h2" ? 2 : 3 });
|
|
1558
|
+
});
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
function rehypeStripLeadingH1() {
|
|
1562
|
+
return (tree) => {
|
|
1563
|
+
let removed = false;
|
|
1564
|
+
(0, import_unist_util_visit.visit)(tree, "element", (node, index, parent) => {
|
|
1565
|
+
if (!removed && node.tagName === "h1" && parent && index != null) {
|
|
1566
|
+
parent.children.splice(index, 1);
|
|
1567
|
+
removed = true;
|
|
1568
|
+
return [import_unist_util_visit.SKIP, index];
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
function rehypeMarkMermaid() {
|
|
1574
|
+
return (tree) => {
|
|
1575
|
+
(0, import_unist_util_visit.visit)(tree, "element", (node) => {
|
|
1576
|
+
if (node.tagName !== "pre") return;
|
|
1577
|
+
const code = node.children.find((c) => c.type === "element" && c.tagName === "code");
|
|
1578
|
+
const cls = code?.properties?.className ?? [];
|
|
1579
|
+
if (!code || !cls.includes("language-mermaid")) return;
|
|
1580
|
+
const source = (0, import_hast_util_to_string.toString)(code);
|
|
1581
|
+
node.properties = { className: ["mermaid"] };
|
|
1582
|
+
node.children = [{ type: "text", value: source }];
|
|
1583
|
+
});
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
async function renderArticle(body) {
|
|
1587
|
+
const toc = [];
|
|
1588
|
+
const file = await (0, import_unified.unified)().use(import_remark_parse.default).use(import_remark_gfm.default).use(import_remark_rehype.default).use(import_rehype_slug.default).use(rehypeCollectToc, toc).use(rehypeStripLeadingH1).use(rehypeMarkMermaid).use(import_rehype.default, {
|
|
1589
|
+
themes: { light: "github-light", dark: "github-dark" },
|
|
1590
|
+
defaultColor: false,
|
|
1591
|
+
langAlias: LANG_ALIASES,
|
|
1592
|
+
fallbackLanguage: "plaintext"
|
|
1593
|
+
}).use(import_rehype_stringify.default).process(body);
|
|
1594
|
+
return { html: String(file), toc };
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// src/index.ts
|
|
1598
|
+
init_registry2();
|
|
1599
|
+
init_client();
|
|
1600
|
+
init_loader();
|
|
1601
|
+
init_events();
|
|
497
1602
|
// Annotate the CommonJS export names for ESM import in node:
|
|
498
1603
|
0 && (module.exports = {
|
|
499
1604
|
ArticleNotFoundError,
|
|
@@ -501,38 +1606,63 @@ async function doctor() {
|
|
|
501
1606
|
CollectionNotFoundError,
|
|
502
1607
|
DiffwikiError,
|
|
503
1608
|
InvalidTargetError,
|
|
1609
|
+
NATIVE_ENGINE,
|
|
504
1610
|
NoDefaultCollectionError,
|
|
1611
|
+
PluginError,
|
|
505
1612
|
REPO_CONFIG_FILE,
|
|
506
1613
|
addTags,
|
|
1614
|
+
buildArticleTree,
|
|
507
1615
|
collectionDir,
|
|
508
1616
|
collectionsDir,
|
|
509
1617
|
configPath,
|
|
510
1618
|
createArticle,
|
|
511
1619
|
createCollection,
|
|
1620
|
+
deregisterPlugin,
|
|
512
1621
|
doctor,
|
|
1622
|
+
emitPluginEvent,
|
|
1623
|
+
ensurePluginRoot,
|
|
513
1624
|
findCollection,
|
|
1625
|
+
findEnabledPlugin,
|
|
514
1626
|
getConfigValue,
|
|
515
1627
|
initRepoWiki,
|
|
1628
|
+
installPlugin,
|
|
1629
|
+
listArticleTree,
|
|
516
1630
|
listCollections,
|
|
1631
|
+
listEnabledPlugins,
|
|
1632
|
+
listPlugins,
|
|
1633
|
+
listSearchModes,
|
|
1634
|
+
loadSearchProvider,
|
|
1635
|
+
nativeSearch,
|
|
517
1636
|
parseAddTarget,
|
|
518
1637
|
parseArticle,
|
|
519
1638
|
parsePathTarget,
|
|
1639
|
+
pluginsDir,
|
|
1640
|
+
pluginsRegistryPath,
|
|
520
1641
|
query,
|
|
1642
|
+
readArticle,
|
|
521
1643
|
readConfig,
|
|
1644
|
+
readPluginRegistry,
|
|
522
1645
|
readRegistry,
|
|
523
1646
|
readRepoConfig,
|
|
524
1647
|
registerCollection,
|
|
1648
|
+
registerPlugin,
|
|
525
1649
|
registryPath,
|
|
526
1650
|
removeArticle,
|
|
527
1651
|
removeCollection,
|
|
1652
|
+
removePlugin,
|
|
528
1653
|
removeTags,
|
|
1654
|
+
renderArticle,
|
|
1655
|
+
resolveDefaultSearchMode,
|
|
529
1656
|
resolveHome,
|
|
530
1657
|
serializeArticle,
|
|
531
1658
|
setConfigValue,
|
|
532
1659
|
setTags,
|
|
533
1660
|
slugify,
|
|
1661
|
+
spawnPluginClient,
|
|
534
1662
|
unregisterCollection,
|
|
535
1663
|
updateArticleBody,
|
|
1664
|
+
upsertPlugin,
|
|
536
1665
|
writeConfig,
|
|
1666
|
+
writePluginRegistry,
|
|
537
1667
|
writeRegistry
|
|
538
1668
|
});
|