diffwiki-core 0.2.0-rc.202607210907.c99416d

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,538 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ ArticleNotFoundError: () => ArticleNotFoundError,
34
+ CollectionExistsError: () => CollectionExistsError,
35
+ CollectionNotFoundError: () => CollectionNotFoundError,
36
+ DiffwikiError: () => DiffwikiError,
37
+ InvalidTargetError: () => InvalidTargetError,
38
+ NoDefaultCollectionError: () => NoDefaultCollectionError,
39
+ REPO_CONFIG_FILE: () => REPO_CONFIG_FILE,
40
+ addTags: () => addTags,
41
+ 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
72
+ });
73
+ module.exports = __toCommonJS(index_exports);
74
+
75
+ // src/errors.ts
76
+ var DiffwikiError = class extends Error {
77
+ constructor(message) {
78
+ super(message);
79
+ this.name = new.target.name;
80
+ }
81
+ };
82
+ var CollectionExistsError = class extends DiffwikiError {
83
+ constructor(name) {
84
+ super(`collection "${name}" already exists`);
85
+ }
86
+ };
87
+ var CollectionNotFoundError = class extends DiffwikiError {
88
+ constructor(name) {
89
+ super(`collection "${name}" not found`);
90
+ }
91
+ };
92
+ var NoDefaultCollectionError = class extends DiffwikiError {
93
+ constructor() {
94
+ super("no default collection set (use: diffwiki config-set defaultCollection <name>)");
95
+ }
96
+ };
97
+ var ArticleNotFoundError = class extends DiffwikiError {
98
+ constructor(target) {
99
+ super(`article "${target}" not found`);
100
+ }
101
+ };
102
+ var InvalidTargetError = class extends DiffwikiError {
103
+ constructor(target, expected) {
104
+ super(`invalid target "${target}" \u2014 ${expected}`);
105
+ }
106
+ };
107
+
108
+ // src/paths.ts
109
+ var import_node_os = __toESM(require("os"), 1);
110
+ var import_node_path = __toESM(require("path"), 1);
111
+ function resolveHome() {
112
+ const override = process.env.DIFFWIKI_HOME;
113
+ return override && override.length > 0 ? override : import_node_path.default.join(import_node_os.default.homedir(), ".diffwiki");
114
+ }
115
+ function registryPath() {
116
+ return import_node_path.default.join(resolveHome(), "registry.json");
117
+ }
118
+ function configPath() {
119
+ return import_node_path.default.join(resolveHome(), "config.json");
120
+ }
121
+ function collectionsDir() {
122
+ return import_node_path.default.join(resolveHome(), "collections");
123
+ }
124
+ function collectionDir(name) {
125
+ return import_node_path.default.join(collectionsDir(), name);
126
+ }
127
+
128
+ // src/config.ts
129
+ var import_promises3 = __toESM(require("fs/promises"), 1);
130
+
131
+ // src/collections.ts
132
+ var import_promises2 = __toESM(require("fs/promises"), 1);
133
+
134
+ // src/registry.ts
135
+ var import_promises = __toESM(require("fs/promises"), 1);
136
+ function isMissing(err) {
137
+ return err?.code === "ENOENT";
138
+ }
139
+ async function readRegistry() {
140
+ try {
141
+ const parsed = JSON.parse(await import_promises.default.readFile(registryPath(), "utf8"));
142
+ return { version: parsed.version ?? 1, collections: parsed.collections ?? [] };
143
+ } catch (err) {
144
+ if (isMissing(err)) return { version: 1, collections: [] };
145
+ throw err;
146
+ }
147
+ }
148
+ async function writeRegistry(registry) {
149
+ await import_promises.default.mkdir(resolveHome(), { recursive: true });
150
+ await import_promises.default.writeFile(registryPath(), `${JSON.stringify(registry, null, 2)}
151
+ `, "utf8");
152
+ }
153
+ async function registerCollection(entry) {
154
+ const registry = await readRegistry();
155
+ const index = registry.collections.findIndex((c) => c.name === entry.name);
156
+ if (index >= 0) {
157
+ registry.collections[index] = entry;
158
+ } else {
159
+ registry.collections.push(entry);
160
+ }
161
+ await writeRegistry(registry);
162
+ }
163
+ async function unregisterCollection(name) {
164
+ const registry = await readRegistry();
165
+ registry.collections = registry.collections.filter((c) => c.name !== name);
166
+ await writeRegistry(registry);
167
+ }
168
+
169
+ // src/collections.ts
170
+ async function listCollections() {
171
+ return (await readRegistry()).collections;
172
+ }
173
+ async function findCollection(name) {
174
+ return (await readRegistry()).collections.find((c) => c.name === name);
175
+ }
176
+ async function createCollection(name) {
177
+ if (await findCollection(name)) throw new CollectionExistsError(name);
178
+ const dir = collectionDir(name);
179
+ await import_promises2.default.mkdir(dir, { recursive: true });
180
+ const entry = {
181
+ name,
182
+ type: "global",
183
+ path: dir,
184
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
185
+ };
186
+ await registerCollection(entry);
187
+ return entry;
188
+ }
189
+ async function removeCollection(name) {
190
+ const entry = await findCollection(name);
191
+ if (!entry) throw new CollectionNotFoundError(name);
192
+ await unregisterCollection(name);
193
+ if (entry.type === "global") {
194
+ await import_promises2.default.rm(entry.path, { recursive: true, force: true });
195
+ }
196
+ return entry;
197
+ }
198
+
199
+ // src/config.ts
200
+ function isMissing2(err) {
201
+ return err?.code === "ENOENT";
202
+ }
203
+ async function readConfig() {
204
+ try {
205
+ return JSON.parse(await import_promises3.default.readFile(configPath(), "utf8"));
206
+ } catch (err) {
207
+ if (isMissing2(err)) return {};
208
+ throw err;
209
+ }
210
+ }
211
+ async function writeConfig(config) {
212
+ await import_promises3.default.mkdir(resolveHome(), { recursive: true });
213
+ await import_promises3.default.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
214
+ `, "utf8");
215
+ }
216
+ async function setConfigValue(key, value) {
217
+ if (key === "defaultCollection" && !await findCollection(value)) {
218
+ throw new CollectionNotFoundError(value);
219
+ }
220
+ const config = await readConfig();
221
+ config[key] = value;
222
+ await writeConfig(config);
223
+ }
224
+ async function getConfigValue(key) {
225
+ const config = await readConfig();
226
+ const value = config[key];
227
+ return typeof value === "string" ? value : void 0;
228
+ }
229
+
230
+ // src/slugify.ts
231
+ function slugify(input) {
232
+ const slug = input.toLowerCase().replace(/^#+\s*/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/g, "");
233
+ return slug.length > 0 ? slug : "untitled";
234
+ }
235
+
236
+ // src/frontmatter.ts
237
+ var import_gray_matter = __toESM(require("gray-matter"), 1);
238
+ function parseArticle(raw) {
239
+ const { data, content } = (0, import_gray_matter.default)(raw);
240
+ const title = typeof data.title === "string" ? data.title : "";
241
+ const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
242
+ const created = typeof data.created === "string" ? data.created : void 0;
243
+ return { title, tags, created, body: content.replace(/^\n+/, "") };
244
+ }
245
+ function serializeArticle(article) {
246
+ const front = { title: article.title, tags: article.tags };
247
+ if (article.created) front.created = article.created;
248
+ if (article.updated) front.updated = article.updated;
249
+ return import_gray_matter.default.stringify(article.body, front);
250
+ }
251
+
252
+ // src/articles.ts
253
+ var import_promises4 = __toESM(require("fs/promises"), 1);
254
+ var import_node_path2 = __toESM(require("path"), 1);
255
+ function parseAddTarget(target) {
256
+ const idx = target.indexOf(":");
257
+ if (idx === -1) {
258
+ const title2 = target.trim();
259
+ if (!title2) throw new InvalidTargetError(target, 'expected "[collection:]title"');
260
+ return { title: title2 };
261
+ }
262
+ const collection = target.slice(0, idx).trim();
263
+ const title = target.slice(idx + 1).trim();
264
+ if (!title) throw new InvalidTargetError(target, 'expected "[collection:]title"');
265
+ return collection ? { collection, title } : { title };
266
+ }
267
+ function parsePathTarget(target) {
268
+ const idx = target.indexOf(":");
269
+ if (idx === -1) throw new InvalidTargetError(target, 'expected "collection:path"');
270
+ const collection = target.slice(0, idx).trim();
271
+ const file = target.slice(idx + 1).trim();
272
+ if (!collection || !file) throw new InvalidTargetError(target, 'expected "collection:path"');
273
+ return { collection, file };
274
+ }
275
+ async function resolveCollectionDir(name) {
276
+ let coll = name;
277
+ if (!coll) {
278
+ coll = (await readConfig()).defaultCollection;
279
+ if (!coll) throw new NoDefaultCollectionError();
280
+ }
281
+ const entry = await findCollection(coll);
282
+ if (!entry) throw new CollectionNotFoundError(coll);
283
+ return { name: coll, dir: entry.path };
284
+ }
285
+ function ensureMdPath(file) {
286
+ if (/\.mdx?$/.test(file)) return file;
287
+ const dir = import_node_path2.default.dirname(file);
288
+ const base = slugify(import_node_path2.default.basename(file));
289
+ const rel = dir === "." ? `${base}.md` : import_node_path2.default.join(dir, `${base}.md`);
290
+ return rel;
291
+ }
292
+ async function createArticle(opts) {
293
+ const { collection, title } = parseAddTarget(opts.target);
294
+ const { name, dir } = await resolveCollectionDir(collection);
295
+ const filePath = import_node_path2.default.join(dir, `${slugify(title)}.md`);
296
+ const now = (/* @__PURE__ */ new Date()).toISOString();
297
+ const body = opts.body ?? `# ${title}
298
+ `;
299
+ await import_promises4.default.mkdir(dir, { recursive: true });
300
+ await import_promises4.default.writeFile(filePath, serializeArticle({ title, tags: opts.tags, created: now, updated: now, body }), "utf8");
301
+ return { title, tags: opts.tags, path: filePath, collection: name };
302
+ }
303
+ async function loadArticle(target) {
304
+ const { collection, file } = parsePathTarget(target);
305
+ const { dir } = await resolveCollectionDir(collection);
306
+ const filePath = import_node_path2.default.join(dir, ensureMdPath(file));
307
+ try {
308
+ return { filePath, raw: await import_promises4.default.readFile(filePath, "utf8"), collection };
309
+ } catch (err) {
310
+ if (err.code === "ENOENT") throw new ArticleNotFoundError(target);
311
+ throw err;
312
+ }
313
+ }
314
+ async function updateArticleBody(target, body) {
315
+ const { filePath, raw, collection } = await loadArticle(target);
316
+ const parsed = parseArticle(raw);
317
+ await import_promises4.default.writeFile(
318
+ filePath,
319
+ serializeArticle({
320
+ title: parsed.title,
321
+ tags: parsed.tags,
322
+ created: parsed.created,
323
+ updated: (/* @__PURE__ */ new Date()).toISOString(),
324
+ body
325
+ }),
326
+ "utf8"
327
+ );
328
+ return { title: parsed.title, tags: parsed.tags, path: filePath, collection };
329
+ }
330
+ async function mutateTags(target, fn) {
331
+ const { filePath, raw, collection } = await loadArticle(target);
332
+ const parsed = parseArticle(raw);
333
+ const tags = fn(parsed.tags);
334
+ await import_promises4.default.writeFile(
335
+ filePath,
336
+ serializeArticle({
337
+ title: parsed.title,
338
+ tags,
339
+ created: parsed.created,
340
+ updated: (/* @__PURE__ */ new Date()).toISOString(),
341
+ body: parsed.body
342
+ }),
343
+ "utf8"
344
+ );
345
+ return { title: parsed.title, tags, path: filePath, collection };
346
+ }
347
+ function addTags(target, tags) {
348
+ return mutateTags(target, (current) => Array.from(/* @__PURE__ */ new Set([...current, ...tags])));
349
+ }
350
+ function removeTags(target, tags) {
351
+ const drop = new Set(tags);
352
+ return mutateTags(target, (current) => current.filter((t) => !drop.has(t)));
353
+ }
354
+ function setTags(target, tags) {
355
+ return mutateTags(target, () => Array.from(new Set(tags)));
356
+ }
357
+ async function removeArticle(target) {
358
+ const { filePath } = await loadArticle(target);
359
+ await import_promises4.default.rm(filePath);
360
+ return filePath;
361
+ }
362
+
363
+ // src/repo.ts
364
+ var import_promises5 = __toESM(require("fs/promises"), 1);
365
+ var import_node_path3 = __toESM(require("path"), 1);
366
+ var import_node_child_process = require("child_process");
367
+ var import_node_util = require("util");
368
+ var import_yaml = require("yaml");
369
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
370
+ var REPO_CONFIG_FILE = "diffwiki.yaml";
371
+ async function detectRepoName(cwd) {
372
+ try {
373
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd });
374
+ return import_node_path3.default.basename(stdout.trim());
375
+ } catch {
376
+ return import_node_path3.default.basename(import_node_path3.default.resolve(cwd));
377
+ }
378
+ }
379
+ async function readRepoConfig(cwd) {
380
+ try {
381
+ const raw = await import_promises5.default.readFile(import_node_path3.default.join(cwd, REPO_CONFIG_FILE), "utf8");
382
+ const parsed = (0, import_yaml.parse)(raw) ?? {};
383
+ if (!parsed.path) return void 0;
384
+ return {
385
+ collection: parsed.collection ?? import_node_path3.default.basename(import_node_path3.default.resolve(cwd)),
386
+ path: parsed.path
387
+ };
388
+ } catch (err) {
389
+ if (err.code === "ENOENT") return void 0;
390
+ throw err;
391
+ }
392
+ }
393
+ async function initRepoWiki(opts) {
394
+ const cwd = import_node_path3.default.resolve(opts.cwd);
395
+ const wikiPath = opts.wikiPath ?? "wiki";
396
+ const absWiki = import_node_path3.default.join(cwd, wikiPath);
397
+ let name = opts.collection ?? await detectRepoName(cwd);
398
+ const existing = await findCollection(name);
399
+ if (existing && existing.path !== absWiki) {
400
+ let n = 2;
401
+ for (; ; ) {
402
+ const candidate = `${name}-${n}`;
403
+ const clash = await findCollection(candidate);
404
+ if (!clash || clash.path === absWiki) {
405
+ name = candidate;
406
+ break;
407
+ }
408
+ n += 1;
409
+ }
410
+ }
411
+ await import_promises5.default.mkdir(absWiki, { recursive: true });
412
+ const config = { collection: name, path: wikiPath };
413
+ await import_promises5.default.writeFile(import_node_path3.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml.stringify)(config), "utf8");
414
+ const entry = {
415
+ name,
416
+ type: "repo",
417
+ path: absWiki,
418
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
419
+ };
420
+ await registerCollection(entry);
421
+ return entry;
422
+ }
423
+
424
+ // src/query.ts
425
+ var import_promises6 = __toESM(require("fs/promises"), 1);
426
+ var import_node_path4 = __toESM(require("path"), 1);
427
+ async function query(term, collection) {
428
+ let targets;
429
+ if (collection) {
430
+ const one = await findCollection(collection);
431
+ if (!one) throw new CollectionNotFoundError(collection);
432
+ targets = [one];
433
+ } else {
434
+ targets = await listCollections();
435
+ }
436
+ const needle = term.toLowerCase();
437
+ const hits = [];
438
+ for (const entry of targets) {
439
+ let files;
440
+ try {
441
+ files = await import_promises6.default.readdir(entry.path);
442
+ } catch {
443
+ continue;
444
+ }
445
+ for (const file of files) {
446
+ if (!/\.mdx?$/.test(file)) continue;
447
+ const full = import_node_path4.default.join(entry.path, file);
448
+ const parsed = parseArticle(await import_promises6.default.readFile(full, "utf8"));
449
+ const haystack = [parsed.title, file, ...parsed.tags].join(" ").toLowerCase();
450
+ if (!needle || haystack.includes(needle)) {
451
+ hits.push({ collection: entry.name, path: full, title: parsed.title, tags: parsed.tags });
452
+ }
453
+ }
454
+ }
455
+ return hits;
456
+ }
457
+
458
+ // src/doctor.ts
459
+ var import_promises7 = __toESM(require("fs/promises"), 1);
460
+ async function exists(target) {
461
+ try {
462
+ await import_promises7.default.access(target);
463
+ return true;
464
+ } catch {
465
+ return false;
466
+ }
467
+ }
468
+ async function doctor() {
469
+ const out = [];
470
+ const home = resolveHome();
471
+ out.push(
472
+ await exists(home) ? { level: "ok", message: `home directory present: ${home}` } : { level: "warn", message: `home directory missing (created on first write): ${home}` }
473
+ );
474
+ out.push(
475
+ await exists(registryPath()) ? { level: "ok", message: `registry present: ${registryPath()}` } : { level: "warn", message: `registry missing: ${registryPath()}` }
476
+ );
477
+ const registry = await readRegistry();
478
+ for (const c of registry.collections) {
479
+ out.push(
480
+ 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}` }
481
+ );
482
+ }
483
+ const config = await readConfig();
484
+ if (config.defaultCollection) {
485
+ const found = registry.collections.some((c) => c.name === config.defaultCollection);
486
+ out.push(
487
+ found ? { level: "ok", message: `default collection: ${config.defaultCollection}` } : {
488
+ level: "error",
489
+ message: `default collection "${config.defaultCollection}" is not registered`
490
+ }
491
+ );
492
+ } else {
493
+ out.push({ level: "warn", message: "no default collection set" });
494
+ }
495
+ return out;
496
+ }
497
+ // Annotate the CommonJS export names for ESM import in node:
498
+ 0 && (module.exports = {
499
+ ArticleNotFoundError,
500
+ CollectionExistsError,
501
+ CollectionNotFoundError,
502
+ DiffwikiError,
503
+ InvalidTargetError,
504
+ NoDefaultCollectionError,
505
+ REPO_CONFIG_FILE,
506
+ addTags,
507
+ collectionDir,
508
+ collectionsDir,
509
+ configPath,
510
+ createArticle,
511
+ createCollection,
512
+ doctor,
513
+ findCollection,
514
+ getConfigValue,
515
+ initRepoWiki,
516
+ listCollections,
517
+ parseAddTarget,
518
+ parseArticle,
519
+ parsePathTarget,
520
+ query,
521
+ readConfig,
522
+ readRegistry,
523
+ readRepoConfig,
524
+ registerCollection,
525
+ registryPath,
526
+ removeArticle,
527
+ removeCollection,
528
+ removeTags,
529
+ resolveHome,
530
+ serializeArticle,
531
+ setConfigValue,
532
+ setTags,
533
+ slugify,
534
+ unregisterCollection,
535
+ updateArticleBody,
536
+ writeConfig,
537
+ writeRegistry
538
+ });
@@ -0,0 +1,145 @@
1
+ type CollectionType = 'global' | 'repo';
2
+ interface CollectionEntry {
3
+ name: string;
4
+ type: CollectionType;
5
+ /** Absolute path to the collection's directory. */
6
+ path: string;
7
+ createdAt: string;
8
+ }
9
+ interface Registry {
10
+ version: number;
11
+ collections: CollectionEntry[];
12
+ }
13
+ interface Config {
14
+ defaultCollection?: string;
15
+ }
16
+ interface Article {
17
+ title: string;
18
+ tags: string[];
19
+ /** Absolute path to the article file. */
20
+ path: string;
21
+ collection: string;
22
+ }
23
+ interface RepoConfig {
24
+ collection: string;
25
+ /** Path to the wiki directory, relative to the repo root. */
26
+ path: string;
27
+ }
28
+
29
+ /** Base class for all expected, user-facing diffwiki failures. */
30
+ declare class DiffwikiError extends Error {
31
+ constructor(message: string);
32
+ }
33
+ declare class CollectionExistsError extends DiffwikiError {
34
+ constructor(name: string);
35
+ }
36
+ declare class CollectionNotFoundError extends DiffwikiError {
37
+ constructor(name: string);
38
+ }
39
+ declare class NoDefaultCollectionError extends DiffwikiError {
40
+ constructor();
41
+ }
42
+ declare class ArticleNotFoundError extends DiffwikiError {
43
+ constructor(target: string);
44
+ }
45
+ declare class InvalidTargetError extends DiffwikiError {
46
+ constructor(target: string, expected: string);
47
+ }
48
+
49
+ /** Root of all global diffwiki state. Overridable via DIFFWIKI_HOME (used by tests). */
50
+ declare function resolveHome(): string;
51
+ declare function registryPath(): string;
52
+ declare function configPath(): string;
53
+ declare function collectionsDir(): string;
54
+ declare function collectionDir(name: string): string;
55
+
56
+ declare function readConfig(): Promise<Config>;
57
+ declare function writeConfig(config: Config): Promise<void>;
58
+ declare function setConfigValue(key: string, value: string): Promise<void>;
59
+ declare function getConfigValue(key: string): Promise<string | undefined>;
60
+
61
+ declare function readRegistry(): Promise<Registry>;
62
+ declare function writeRegistry(registry: Registry): Promise<void>;
63
+ /** Insert or replace the entry with the same name. */
64
+ declare function registerCollection(entry: CollectionEntry): Promise<void>;
65
+ declare function unregisterCollection(name: string): Promise<void>;
66
+
67
+ /** Turn a title (or a "# Heading" line) into a filesystem-safe slug. */
68
+ declare function slugify(input: string): string;
69
+
70
+ interface ParsedArticle {
71
+ title: string;
72
+ tags: string[];
73
+ created?: string;
74
+ body: string;
75
+ }
76
+ interface SerializableArticle {
77
+ title: string;
78
+ tags: string[];
79
+ created?: string;
80
+ updated?: string;
81
+ body: string;
82
+ }
83
+ declare function parseArticle(raw: string): ParsedArticle;
84
+ declare function serializeArticle(article: SerializableArticle): string;
85
+
86
+ declare function listCollections(): Promise<CollectionEntry[]>;
87
+ declare function findCollection(name: string): Promise<CollectionEntry | undefined>;
88
+ declare function createCollection(name: string): Promise<CollectionEntry>;
89
+ /**
90
+ * Remove a collection from the registry. For `global` collections we own the
91
+ * storage, so its directory is deleted too; for `repo` wikis we only deregister
92
+ * (the files live in the user's repo and are left untouched).
93
+ */
94
+ declare function removeCollection(name: string): Promise<CollectionEntry>;
95
+
96
+ /** Parse "[collection:]title" (collection optional) for `add`. */
97
+ declare function parseAddTarget(target: string): {
98
+ collection?: string;
99
+ title: string;
100
+ };
101
+ /** Parse "collection:path" (both required) for update/remove/tags. */
102
+ declare function parsePathTarget(target: string): {
103
+ collection: string;
104
+ file: string;
105
+ };
106
+ declare function createArticle(opts: {
107
+ target: string;
108
+ tags: string[];
109
+ body?: string;
110
+ }): Promise<Article>;
111
+ declare function updateArticleBody(target: string, body: string): Promise<Article>;
112
+ declare function addTags(target: string, tags: string[]): Promise<Article>;
113
+ declare function removeTags(target: string, tags: string[]): Promise<Article>;
114
+ declare function setTags(target: string, tags: string[]): Promise<Article>;
115
+ declare function removeArticle(target: string): Promise<string>;
116
+
117
+ declare const REPO_CONFIG_FILE = "diffwiki.yaml";
118
+ declare function readRepoConfig(cwd: string): Promise<RepoConfig | undefined>;
119
+ interface InitOptions {
120
+ cwd: string;
121
+ collection?: string;
122
+ wikiPath?: string;
123
+ }
124
+ declare function initRepoWiki(opts: InitOptions): Promise<CollectionEntry>;
125
+
126
+ interface QueryHit {
127
+ collection: string;
128
+ path: string;
129
+ title: string;
130
+ tags: string[];
131
+ }
132
+ /**
133
+ * DUMMY search: case-insensitive substring match over title, filename, and tags.
134
+ * Real QMD-based semantic search replaces this later.
135
+ */
136
+ declare function query(term: string, collection?: string): Promise<QueryHit[]>;
137
+
138
+ type DiagnosticLevel = 'ok' | 'warn' | 'error';
139
+ interface Diagnostic {
140
+ level: DiagnosticLevel;
141
+ message: string;
142
+ }
143
+ declare function doctor(): Promise<Diagnostic[]>;
144
+
145
+ export { type Article, ArticleNotFoundError, type CollectionEntry, CollectionExistsError, CollectionNotFoundError, type CollectionType, type Config, type Diagnostic, type DiagnosticLevel, DiffwikiError, type InitOptions, InvalidTargetError, NoDefaultCollectionError, type ParsedArticle, type QueryHit, REPO_CONFIG_FILE, type Registry, type RepoConfig, type SerializableArticle, addTags, collectionDir, collectionsDir, configPath, createArticle, createCollection, doctor, findCollection, getConfigValue, initRepoWiki, listCollections, parseAddTarget, parseArticle, parsePathTarget, query, readConfig, readRegistry, readRepoConfig, registerCollection, registryPath, removeArticle, removeCollection, removeTags, resolveHome, serializeArticle, setConfigValue, setTags, slugify, unregisterCollection, updateArticleBody, writeConfig, writeRegistry };
@@ -0,0 +1,145 @@
1
+ type CollectionType = 'global' | 'repo';
2
+ interface CollectionEntry {
3
+ name: string;
4
+ type: CollectionType;
5
+ /** Absolute path to the collection's directory. */
6
+ path: string;
7
+ createdAt: string;
8
+ }
9
+ interface Registry {
10
+ version: number;
11
+ collections: CollectionEntry[];
12
+ }
13
+ interface Config {
14
+ defaultCollection?: string;
15
+ }
16
+ interface Article {
17
+ title: string;
18
+ tags: string[];
19
+ /** Absolute path to the article file. */
20
+ path: string;
21
+ collection: string;
22
+ }
23
+ interface RepoConfig {
24
+ collection: string;
25
+ /** Path to the wiki directory, relative to the repo root. */
26
+ path: string;
27
+ }
28
+
29
+ /** Base class for all expected, user-facing diffwiki failures. */
30
+ declare class DiffwikiError extends Error {
31
+ constructor(message: string);
32
+ }
33
+ declare class CollectionExistsError extends DiffwikiError {
34
+ constructor(name: string);
35
+ }
36
+ declare class CollectionNotFoundError extends DiffwikiError {
37
+ constructor(name: string);
38
+ }
39
+ declare class NoDefaultCollectionError extends DiffwikiError {
40
+ constructor();
41
+ }
42
+ declare class ArticleNotFoundError extends DiffwikiError {
43
+ constructor(target: string);
44
+ }
45
+ declare class InvalidTargetError extends DiffwikiError {
46
+ constructor(target: string, expected: string);
47
+ }
48
+
49
+ /** Root of all global diffwiki state. Overridable via DIFFWIKI_HOME (used by tests). */
50
+ declare function resolveHome(): string;
51
+ declare function registryPath(): string;
52
+ declare function configPath(): string;
53
+ declare function collectionsDir(): string;
54
+ declare function collectionDir(name: string): string;
55
+
56
+ declare function readConfig(): Promise<Config>;
57
+ declare function writeConfig(config: Config): Promise<void>;
58
+ declare function setConfigValue(key: string, value: string): Promise<void>;
59
+ declare function getConfigValue(key: string): Promise<string | undefined>;
60
+
61
+ declare function readRegistry(): Promise<Registry>;
62
+ declare function writeRegistry(registry: Registry): Promise<void>;
63
+ /** Insert or replace the entry with the same name. */
64
+ declare function registerCollection(entry: CollectionEntry): Promise<void>;
65
+ declare function unregisterCollection(name: string): Promise<void>;
66
+
67
+ /** Turn a title (or a "# Heading" line) into a filesystem-safe slug. */
68
+ declare function slugify(input: string): string;
69
+
70
+ interface ParsedArticle {
71
+ title: string;
72
+ tags: string[];
73
+ created?: string;
74
+ body: string;
75
+ }
76
+ interface SerializableArticle {
77
+ title: string;
78
+ tags: string[];
79
+ created?: string;
80
+ updated?: string;
81
+ body: string;
82
+ }
83
+ declare function parseArticle(raw: string): ParsedArticle;
84
+ declare function serializeArticle(article: SerializableArticle): string;
85
+
86
+ declare function listCollections(): Promise<CollectionEntry[]>;
87
+ declare function findCollection(name: string): Promise<CollectionEntry | undefined>;
88
+ declare function createCollection(name: string): Promise<CollectionEntry>;
89
+ /**
90
+ * Remove a collection from the registry. For `global` collections we own the
91
+ * storage, so its directory is deleted too; for `repo` wikis we only deregister
92
+ * (the files live in the user's repo and are left untouched).
93
+ */
94
+ declare function removeCollection(name: string): Promise<CollectionEntry>;
95
+
96
+ /** Parse "[collection:]title" (collection optional) for `add`. */
97
+ declare function parseAddTarget(target: string): {
98
+ collection?: string;
99
+ title: string;
100
+ };
101
+ /** Parse "collection:path" (both required) for update/remove/tags. */
102
+ declare function parsePathTarget(target: string): {
103
+ collection: string;
104
+ file: string;
105
+ };
106
+ declare function createArticle(opts: {
107
+ target: string;
108
+ tags: string[];
109
+ body?: string;
110
+ }): Promise<Article>;
111
+ declare function updateArticleBody(target: string, body: string): Promise<Article>;
112
+ declare function addTags(target: string, tags: string[]): Promise<Article>;
113
+ declare function removeTags(target: string, tags: string[]): Promise<Article>;
114
+ declare function setTags(target: string, tags: string[]): Promise<Article>;
115
+ declare function removeArticle(target: string): Promise<string>;
116
+
117
+ declare const REPO_CONFIG_FILE = "diffwiki.yaml";
118
+ declare function readRepoConfig(cwd: string): Promise<RepoConfig | undefined>;
119
+ interface InitOptions {
120
+ cwd: string;
121
+ collection?: string;
122
+ wikiPath?: string;
123
+ }
124
+ declare function initRepoWiki(opts: InitOptions): Promise<CollectionEntry>;
125
+
126
+ interface QueryHit {
127
+ collection: string;
128
+ path: string;
129
+ title: string;
130
+ tags: string[];
131
+ }
132
+ /**
133
+ * DUMMY search: case-insensitive substring match over title, filename, and tags.
134
+ * Real QMD-based semantic search replaces this later.
135
+ */
136
+ declare function query(term: string, collection?: string): Promise<QueryHit[]>;
137
+
138
+ type DiagnosticLevel = 'ok' | 'warn' | 'error';
139
+ interface Diagnostic {
140
+ level: DiagnosticLevel;
141
+ message: string;
142
+ }
143
+ declare function doctor(): Promise<Diagnostic[]>;
144
+
145
+ export { type Article, ArticleNotFoundError, type CollectionEntry, CollectionExistsError, CollectionNotFoundError, type CollectionType, type Config, type Diagnostic, type DiagnosticLevel, DiffwikiError, type InitOptions, InvalidTargetError, NoDefaultCollectionError, type ParsedArticle, type QueryHit, REPO_CONFIG_FILE, type Registry, type RepoConfig, type SerializableArticle, addTags, collectionDir, collectionsDir, configPath, createArticle, createCollection, doctor, findCollection, getConfigValue, initRepoWiki, listCollections, parseAddTarget, parseArticle, parsePathTarget, query, readConfig, readRegistry, readRepoConfig, registerCollection, registryPath, removeArticle, removeCollection, removeTags, resolveHome, serializeArticle, setConfigValue, setTags, slugify, unregisterCollection, updateArticleBody, writeConfig, writeRegistry };
package/dist/index.js ADDED
@@ -0,0 +1,463 @@
1
+ // src/errors.ts
2
+ var DiffwikiError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = new.target.name;
6
+ }
7
+ };
8
+ var CollectionExistsError = class extends DiffwikiError {
9
+ constructor(name) {
10
+ super(`collection "${name}" already exists`);
11
+ }
12
+ };
13
+ var CollectionNotFoundError = class extends DiffwikiError {
14
+ constructor(name) {
15
+ super(`collection "${name}" not found`);
16
+ }
17
+ };
18
+ var NoDefaultCollectionError = class extends DiffwikiError {
19
+ constructor() {
20
+ super("no default collection set (use: diffwiki config-set defaultCollection <name>)");
21
+ }
22
+ };
23
+ var ArticleNotFoundError = class extends DiffwikiError {
24
+ constructor(target) {
25
+ super(`article "${target}" not found`);
26
+ }
27
+ };
28
+ var InvalidTargetError = class extends DiffwikiError {
29
+ constructor(target, expected) {
30
+ super(`invalid target "${target}" \u2014 ${expected}`);
31
+ }
32
+ };
33
+
34
+ // src/paths.ts
35
+ import os from "os";
36
+ import path from "path";
37
+ function resolveHome() {
38
+ const override = process.env.DIFFWIKI_HOME;
39
+ return override && override.length > 0 ? override : path.join(os.homedir(), ".diffwiki");
40
+ }
41
+ function registryPath() {
42
+ return path.join(resolveHome(), "registry.json");
43
+ }
44
+ function configPath() {
45
+ return path.join(resolveHome(), "config.json");
46
+ }
47
+ function collectionsDir() {
48
+ return path.join(resolveHome(), "collections");
49
+ }
50
+ function collectionDir(name) {
51
+ return path.join(collectionsDir(), name);
52
+ }
53
+
54
+ // src/config.ts
55
+ import fs3 from "fs/promises";
56
+
57
+ // src/collections.ts
58
+ import fs2 from "fs/promises";
59
+
60
+ // src/registry.ts
61
+ import fs from "fs/promises";
62
+ function isMissing(err) {
63
+ return err?.code === "ENOENT";
64
+ }
65
+ async function readRegistry() {
66
+ try {
67
+ const parsed = JSON.parse(await fs.readFile(registryPath(), "utf8"));
68
+ return { version: parsed.version ?? 1, collections: parsed.collections ?? [] };
69
+ } catch (err) {
70
+ if (isMissing(err)) return { version: 1, collections: [] };
71
+ throw err;
72
+ }
73
+ }
74
+ async function writeRegistry(registry) {
75
+ await fs.mkdir(resolveHome(), { recursive: true });
76
+ await fs.writeFile(registryPath(), `${JSON.stringify(registry, null, 2)}
77
+ `, "utf8");
78
+ }
79
+ async function registerCollection(entry) {
80
+ const registry = await readRegistry();
81
+ const index = registry.collections.findIndex((c) => c.name === entry.name);
82
+ if (index >= 0) {
83
+ registry.collections[index] = entry;
84
+ } else {
85
+ registry.collections.push(entry);
86
+ }
87
+ await writeRegistry(registry);
88
+ }
89
+ async function unregisterCollection(name) {
90
+ const registry = await readRegistry();
91
+ registry.collections = registry.collections.filter((c) => c.name !== name);
92
+ await writeRegistry(registry);
93
+ }
94
+
95
+ // src/collections.ts
96
+ async function listCollections() {
97
+ return (await readRegistry()).collections;
98
+ }
99
+ async function findCollection(name) {
100
+ return (await readRegistry()).collections.find((c) => c.name === name);
101
+ }
102
+ async function createCollection(name) {
103
+ if (await findCollection(name)) throw new CollectionExistsError(name);
104
+ const dir = collectionDir(name);
105
+ await fs2.mkdir(dir, { recursive: true });
106
+ const entry = {
107
+ name,
108
+ type: "global",
109
+ path: dir,
110
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
111
+ };
112
+ await registerCollection(entry);
113
+ return entry;
114
+ }
115
+ async function removeCollection(name) {
116
+ const entry = await findCollection(name);
117
+ if (!entry) throw new CollectionNotFoundError(name);
118
+ await unregisterCollection(name);
119
+ if (entry.type === "global") {
120
+ await fs2.rm(entry.path, { recursive: true, force: true });
121
+ }
122
+ return entry;
123
+ }
124
+
125
+ // src/config.ts
126
+ function isMissing2(err) {
127
+ return err?.code === "ENOENT";
128
+ }
129
+ async function readConfig() {
130
+ try {
131
+ return JSON.parse(await fs3.readFile(configPath(), "utf8"));
132
+ } catch (err) {
133
+ if (isMissing2(err)) return {};
134
+ throw err;
135
+ }
136
+ }
137
+ async function writeConfig(config) {
138
+ await fs3.mkdir(resolveHome(), { recursive: true });
139
+ await fs3.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
140
+ `, "utf8");
141
+ }
142
+ async function setConfigValue(key, value) {
143
+ if (key === "defaultCollection" && !await findCollection(value)) {
144
+ throw new CollectionNotFoundError(value);
145
+ }
146
+ const config = await readConfig();
147
+ config[key] = value;
148
+ await writeConfig(config);
149
+ }
150
+ async function getConfigValue(key) {
151
+ const config = await readConfig();
152
+ const value = config[key];
153
+ return typeof value === "string" ? value : void 0;
154
+ }
155
+
156
+ // src/slugify.ts
157
+ function slugify(input) {
158
+ const slug = input.toLowerCase().replace(/^#+\s*/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80).replace(/-+$/g, "");
159
+ return slug.length > 0 ? slug : "untitled";
160
+ }
161
+
162
+ // src/frontmatter.ts
163
+ import matter from "gray-matter";
164
+ function parseArticle(raw) {
165
+ const { data, content } = matter(raw);
166
+ const title = typeof data.title === "string" ? data.title : "";
167
+ const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
168
+ const created = typeof data.created === "string" ? data.created : void 0;
169
+ return { title, tags, created, body: content.replace(/^\n+/, "") };
170
+ }
171
+ function serializeArticle(article) {
172
+ const front = { title: article.title, tags: article.tags };
173
+ if (article.created) front.created = article.created;
174
+ if (article.updated) front.updated = article.updated;
175
+ return matter.stringify(article.body, front);
176
+ }
177
+
178
+ // src/articles.ts
179
+ import fs4 from "fs/promises";
180
+ import path2 from "path";
181
+ function parseAddTarget(target) {
182
+ const idx = target.indexOf(":");
183
+ if (idx === -1) {
184
+ const title2 = target.trim();
185
+ if (!title2) throw new InvalidTargetError(target, 'expected "[collection:]title"');
186
+ return { title: title2 };
187
+ }
188
+ const collection = target.slice(0, idx).trim();
189
+ const title = target.slice(idx + 1).trim();
190
+ if (!title) throw new InvalidTargetError(target, 'expected "[collection:]title"');
191
+ return collection ? { collection, title } : { title };
192
+ }
193
+ function parsePathTarget(target) {
194
+ const idx = target.indexOf(":");
195
+ if (idx === -1) throw new InvalidTargetError(target, 'expected "collection:path"');
196
+ const collection = target.slice(0, idx).trim();
197
+ const file = target.slice(idx + 1).trim();
198
+ if (!collection || !file) throw new InvalidTargetError(target, 'expected "collection:path"');
199
+ return { collection, file };
200
+ }
201
+ async function resolveCollectionDir(name) {
202
+ let coll = name;
203
+ if (!coll) {
204
+ coll = (await readConfig()).defaultCollection;
205
+ if (!coll) throw new NoDefaultCollectionError();
206
+ }
207
+ const entry = await findCollection(coll);
208
+ if (!entry) throw new CollectionNotFoundError(coll);
209
+ return { name: coll, dir: entry.path };
210
+ }
211
+ function ensureMdPath(file) {
212
+ if (/\.mdx?$/.test(file)) return file;
213
+ const dir = path2.dirname(file);
214
+ const base = slugify(path2.basename(file));
215
+ const rel = dir === "." ? `${base}.md` : path2.join(dir, `${base}.md`);
216
+ return rel;
217
+ }
218
+ async function createArticle(opts) {
219
+ const { collection, title } = parseAddTarget(opts.target);
220
+ const { name, dir } = await resolveCollectionDir(collection);
221
+ const filePath = path2.join(dir, `${slugify(title)}.md`);
222
+ const now = (/* @__PURE__ */ new Date()).toISOString();
223
+ const body = opts.body ?? `# ${title}
224
+ `;
225
+ await fs4.mkdir(dir, { recursive: true });
226
+ await fs4.writeFile(filePath, serializeArticle({ title, tags: opts.tags, created: now, updated: now, body }), "utf8");
227
+ return { title, tags: opts.tags, path: filePath, collection: name };
228
+ }
229
+ async function loadArticle(target) {
230
+ const { collection, file } = parsePathTarget(target);
231
+ const { dir } = await resolveCollectionDir(collection);
232
+ const filePath = path2.join(dir, ensureMdPath(file));
233
+ try {
234
+ return { filePath, raw: await fs4.readFile(filePath, "utf8"), collection };
235
+ } catch (err) {
236
+ if (err.code === "ENOENT") throw new ArticleNotFoundError(target);
237
+ throw err;
238
+ }
239
+ }
240
+ async function updateArticleBody(target, body) {
241
+ const { filePath, raw, collection } = await loadArticle(target);
242
+ const parsed = parseArticle(raw);
243
+ await fs4.writeFile(
244
+ filePath,
245
+ serializeArticle({
246
+ title: parsed.title,
247
+ tags: parsed.tags,
248
+ created: parsed.created,
249
+ updated: (/* @__PURE__ */ new Date()).toISOString(),
250
+ body
251
+ }),
252
+ "utf8"
253
+ );
254
+ return { title: parsed.title, tags: parsed.tags, path: filePath, collection };
255
+ }
256
+ async function mutateTags(target, fn) {
257
+ const { filePath, raw, collection } = await loadArticle(target);
258
+ const parsed = parseArticle(raw);
259
+ const tags = fn(parsed.tags);
260
+ await fs4.writeFile(
261
+ filePath,
262
+ serializeArticle({
263
+ title: parsed.title,
264
+ tags,
265
+ created: parsed.created,
266
+ updated: (/* @__PURE__ */ new Date()).toISOString(),
267
+ body: parsed.body
268
+ }),
269
+ "utf8"
270
+ );
271
+ return { title: parsed.title, tags, path: filePath, collection };
272
+ }
273
+ function addTags(target, tags) {
274
+ return mutateTags(target, (current) => Array.from(/* @__PURE__ */ new Set([...current, ...tags])));
275
+ }
276
+ function removeTags(target, tags) {
277
+ const drop = new Set(tags);
278
+ return mutateTags(target, (current) => current.filter((t) => !drop.has(t)));
279
+ }
280
+ function setTags(target, tags) {
281
+ return mutateTags(target, () => Array.from(new Set(tags)));
282
+ }
283
+ async function removeArticle(target) {
284
+ const { filePath } = await loadArticle(target);
285
+ await fs4.rm(filePath);
286
+ return filePath;
287
+ }
288
+
289
+ // src/repo.ts
290
+ import fs5 from "fs/promises";
291
+ import path3 from "path";
292
+ import { execFile } from "child_process";
293
+ import { promisify } from "util";
294
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
295
+ var execFileAsync = promisify(execFile);
296
+ var REPO_CONFIG_FILE = "diffwiki.yaml";
297
+ async function detectRepoName(cwd) {
298
+ try {
299
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd });
300
+ return path3.basename(stdout.trim());
301
+ } catch {
302
+ return path3.basename(path3.resolve(cwd));
303
+ }
304
+ }
305
+ async function readRepoConfig(cwd) {
306
+ try {
307
+ const raw = await fs5.readFile(path3.join(cwd, REPO_CONFIG_FILE), "utf8");
308
+ const parsed = parseYaml(raw) ?? {};
309
+ if (!parsed.path) return void 0;
310
+ return {
311
+ collection: parsed.collection ?? path3.basename(path3.resolve(cwd)),
312
+ path: parsed.path
313
+ };
314
+ } catch (err) {
315
+ if (err.code === "ENOENT") return void 0;
316
+ throw err;
317
+ }
318
+ }
319
+ async function initRepoWiki(opts) {
320
+ const cwd = path3.resolve(opts.cwd);
321
+ const wikiPath = opts.wikiPath ?? "wiki";
322
+ const absWiki = path3.join(cwd, wikiPath);
323
+ let name = opts.collection ?? await detectRepoName(cwd);
324
+ const existing = await findCollection(name);
325
+ if (existing && existing.path !== absWiki) {
326
+ let n = 2;
327
+ for (; ; ) {
328
+ const candidate = `${name}-${n}`;
329
+ const clash = await findCollection(candidate);
330
+ if (!clash || clash.path === absWiki) {
331
+ name = candidate;
332
+ break;
333
+ }
334
+ n += 1;
335
+ }
336
+ }
337
+ await fs5.mkdir(absWiki, { recursive: true });
338
+ const config = { collection: name, path: wikiPath };
339
+ await fs5.writeFile(path3.join(cwd, REPO_CONFIG_FILE), stringifyYaml(config), "utf8");
340
+ const entry = {
341
+ name,
342
+ type: "repo",
343
+ path: absWiki,
344
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
345
+ };
346
+ await registerCollection(entry);
347
+ return entry;
348
+ }
349
+
350
+ // src/query.ts
351
+ import fs6 from "fs/promises";
352
+ import path4 from "path";
353
+ async function query(term, collection) {
354
+ let targets;
355
+ if (collection) {
356
+ const one = await findCollection(collection);
357
+ if (!one) throw new CollectionNotFoundError(collection);
358
+ targets = [one];
359
+ } else {
360
+ targets = await listCollections();
361
+ }
362
+ const needle = term.toLowerCase();
363
+ const hits = [];
364
+ for (const entry of targets) {
365
+ let files;
366
+ try {
367
+ files = await fs6.readdir(entry.path);
368
+ } catch {
369
+ continue;
370
+ }
371
+ for (const file of files) {
372
+ if (!/\.mdx?$/.test(file)) continue;
373
+ const full = path4.join(entry.path, file);
374
+ const parsed = parseArticle(await fs6.readFile(full, "utf8"));
375
+ const haystack = [parsed.title, file, ...parsed.tags].join(" ").toLowerCase();
376
+ if (!needle || haystack.includes(needle)) {
377
+ hits.push({ collection: entry.name, path: full, title: parsed.title, tags: parsed.tags });
378
+ }
379
+ }
380
+ }
381
+ return hits;
382
+ }
383
+
384
+ // src/doctor.ts
385
+ import fs7 from "fs/promises";
386
+ async function exists(target) {
387
+ try {
388
+ await fs7.access(target);
389
+ return true;
390
+ } catch {
391
+ return false;
392
+ }
393
+ }
394
+ async function doctor() {
395
+ const out = [];
396
+ const home = resolveHome();
397
+ out.push(
398
+ await exists(home) ? { level: "ok", message: `home directory present: ${home}` } : { level: "warn", message: `home directory missing (created on first write): ${home}` }
399
+ );
400
+ out.push(
401
+ await exists(registryPath()) ? { level: "ok", message: `registry present: ${registryPath()}` } : { level: "warn", message: `registry missing: ${registryPath()}` }
402
+ );
403
+ const registry = await readRegistry();
404
+ for (const c of registry.collections) {
405
+ out.push(
406
+ 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}` }
407
+ );
408
+ }
409
+ const config = await readConfig();
410
+ if (config.defaultCollection) {
411
+ const found = registry.collections.some((c) => c.name === config.defaultCollection);
412
+ out.push(
413
+ found ? { level: "ok", message: `default collection: ${config.defaultCollection}` } : {
414
+ level: "error",
415
+ message: `default collection "${config.defaultCollection}" is not registered`
416
+ }
417
+ );
418
+ } else {
419
+ out.push({ level: "warn", message: "no default collection set" });
420
+ }
421
+ return out;
422
+ }
423
+ export {
424
+ ArticleNotFoundError,
425
+ CollectionExistsError,
426
+ CollectionNotFoundError,
427
+ DiffwikiError,
428
+ InvalidTargetError,
429
+ NoDefaultCollectionError,
430
+ REPO_CONFIG_FILE,
431
+ addTags,
432
+ collectionDir,
433
+ collectionsDir,
434
+ configPath,
435
+ createArticle,
436
+ createCollection,
437
+ doctor,
438
+ findCollection,
439
+ getConfigValue,
440
+ initRepoWiki,
441
+ listCollections,
442
+ parseAddTarget,
443
+ parseArticle,
444
+ parsePathTarget,
445
+ query,
446
+ readConfig,
447
+ readRegistry,
448
+ readRepoConfig,
449
+ registerCollection,
450
+ registryPath,
451
+ removeArticle,
452
+ removeCollection,
453
+ removeTags,
454
+ resolveHome,
455
+ serializeArticle,
456
+ setConfigValue,
457
+ setTags,
458
+ slugify,
459
+ unregisterCollection,
460
+ updateArticleBody,
461
+ writeConfig,
462
+ writeRegistry
463
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "diffwiki-core",
3
+ "version": "0.2.0-rc.202607210907.c99416d",
4
+ "description": "Core configuration, registry, and article management for diffwiki",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "dependencies": {
24
+ "gray-matter": "^4.0.3",
25
+ "yaml": "^2.5.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^22.0.0"
29
+ },
30
+ "scripts": {
31
+ "build": "tsup",
32
+ "test": "vitest run --passWithNoTests",
33
+ "lint": "eslint src tests --no-error-on-unmatched-pattern && tsc --noEmit",
34
+ "lint:fix": "eslint src tests --no-error-on-unmatched-pattern --fix"
35
+ }
36
+ }