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