diffwiki-core 0.17.3 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,63 @@
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
+ var PluginError = class extends DiffwikiError {
34
+ };
35
+ var WorktreeNotAllowedError = class extends DiffwikiError {
36
+ constructor(root) {
37
+ super(`refusing to add a linked worktree \u2014 add the source repository instead (${root})`);
38
+ }
39
+ };
40
+ var CollectionAlreadyAddedError = class extends DiffwikiError {
41
+ constructor(name) {
42
+ super(`this repository is already added as collection "${name}"`);
43
+ }
44
+ };
45
+ var InvalidExportSelectionError = class extends DiffwikiError {
46
+ constructor(problems) {
47
+ super(`export aborted \u2014 ${problems.length} selection problem(s):
48
+ ${problems.map((p) => ` - ${p}`).join("\n")}`);
49
+ }
50
+ };
51
+
52
+ export {
53
+ DiffwikiError,
54
+ CollectionExistsError,
55
+ CollectionNotFoundError,
56
+ NoDefaultCollectionError,
57
+ ArticleNotFoundError,
58
+ InvalidTargetError,
59
+ PluginError,
60
+ WorktreeNotAllowedError,
61
+ CollectionAlreadyAddedError,
62
+ InvalidExportSelectionError
63
+ };
@@ -1,5 +1,12 @@
1
+ import {
2
+ CollectionExistsError,
3
+ CollectionNotFoundError,
4
+ DiffwikiError,
5
+ InvalidTargetError
6
+ } from "./chunk-FJ5XD2JN.js";
7
+
1
8
  // src/collections.ts
2
- import fs2 from "fs/promises";
9
+ import fs3 from "fs/promises";
3
10
 
4
11
  // src/paths.ts
5
12
  import os from "os";
@@ -20,6 +27,9 @@ function collectionsDir() {
20
27
  function templatesDir() {
21
28
  return path.join(resolveHome(), "templates");
22
29
  }
30
+ function metaDir() {
31
+ return path.join(resolveHome(), "meta");
32
+ }
23
33
  function collectionDir(name) {
24
34
  return path.join(collectionsDir(), name);
25
35
  }
@@ -31,13 +41,289 @@ function pluginsRegistryPath() {
31
41
  }
32
42
 
33
43
  // src/registry.ts
44
+ import fs2 from "fs/promises";
45
+
46
+ // src/meta.ts
34
47
  import fs from "fs/promises";
48
+ import path2 from "path";
49
+ import { randomUUID } from "crypto";
50
+ import { stringify, parseDocument } from "yaml";
51
+ var META_KINDS = ["history", "external-source-extract", "dream-learning"];
52
+ function validateMetaArticlePath(file) {
53
+ if (!/\.mdx?$/i.test(file) || path2.isAbsolute(file) || /[\\\0]/.test(file) || file.split("/").some((part) => !part || part === "." || part === ".." || part.startsWith("."))) {
54
+ throw new InvalidTargetError(file, "expected an exact relative .md/.mdx file path without traversal");
55
+ }
56
+ return file;
57
+ }
58
+ async function ensureCollectionMeta(entry) {
59
+ const root = collectionRoot(entry.name);
60
+ const source = path2.resolve(entry.path);
61
+ if (within(source, path2.resolve(metaDir())) || within(path2.resolve(metaDir()), source)) {
62
+ throw new InvalidTargetError(entry.path, "collection content overlaps the metadata store");
63
+ }
64
+ const index = path2.join(root, ".meta", "META.md");
65
+ await seed(index, `Sources for ${entry.name}`);
66
+ await globalIndex();
67
+ return index;
68
+ }
69
+ async function ensureArticleMeta(entry, articlePath) {
70
+ const relative = validateMetaArticlePath(articlePath);
71
+ await ensureCollectionMeta(entry);
72
+ const mirror = path2.join(collectionRoot(entry.name), relative);
73
+ if (await seed(mirror, `Sources for ${entry.name}:${relative}`)) await reindexMeta(entry.name);
74
+ return mirror;
75
+ }
76
+ async function mirrorSavedArticle(entry, file) {
77
+ try {
78
+ await ensureArticleMeta(entry, path2.relative(entry.path, file).split(path2.sep).join("/"));
79
+ } catch (err) {
80
+ throw new DiffwikiError(
81
+ `Article saved at ${file}, but metadata setup failed: ${String(err)}. Repair with: diffwiki meta init ${entry.name}`
82
+ );
83
+ }
84
+ }
85
+ async function reindexMeta(collection) {
86
+ await fs.mkdir(await checked(metaDir()), { recursive: true });
87
+ const result = { created: [], kept: [], index_paths: [], warnings: [], errors: [] };
88
+ const names = collection ? [collection] : (await fs.readdir(metaDir(), { withFileTypes: true })).filter((item) => item.isDirectory() && !item.name.startsWith(".")).map((item) => item.name).sort();
89
+ for (const name of names) {
90
+ try {
91
+ const root = await checked(collectionRoot(name));
92
+ const index = path2.join(root, ".meta", "META.md");
93
+ await fs.access(await checked(index));
94
+ const pages = await markdownFiles(root);
95
+ const recordDir = await checked(path2.join(root, ".meta", "records"));
96
+ let records = [];
97
+ try {
98
+ records = (await fs.readdir(recordDir)).filter((file) => file.endsWith(".md")).sort();
99
+ } catch (err) {
100
+ if (!missing(err)) throw err;
101
+ }
102
+ const pageLines = new Map(pages.map((file) => [file, []]));
103
+ const sourceLines = [];
104
+ const dreamLines = [];
105
+ for (const record of records) {
106
+ const file = await checked(path2.join(recordDir, record));
107
+ try {
108
+ const raw = await fs.readFile(file, "utf8");
109
+ const front = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(raw);
110
+ if (!front) throw new Error("missing record header");
111
+ const doc = parseDocument(front[1]);
112
+ if (doc.errors.length) throw new Error("invalid YAML header");
113
+ const data = doc.toJS();
114
+ if (!data || !META_KINDS.includes(data.kind) || data.collection !== name || typeof data.captured_at !== "string") {
115
+ throw new Error("invalid kind, collection, or capture time");
116
+ }
117
+ const article = data.article_path === void 0 ? void 0 : validateMetaArticlePath(String(data.article_path));
118
+ const label = `${data.kind} \xB7 ${record.replace(/\.md$/, "")}`;
119
+ const bucket = data.kind === "dream-learning" ? dreamLines : sourceLines;
120
+ bucket.push(`- ${link(index, file, label)}${article ? ` \u2014 \`${article}\`` : " \u2014 collection"}`);
121
+ if (article) {
122
+ if (!pageLines.has(article))
123
+ result.warnings.push(`${file}: no manifest for ${article}; run meta init if the article exists`);
124
+ else pageLines.get(article).push(`- ${link(path2.join(root, article), file, label)}`);
125
+ }
126
+ } catch (err) {
127
+ result.warnings.push(`${file}: ${String(err)}`);
128
+ }
129
+ }
130
+ for (const [page, lines] of pageLines) {
131
+ const file = path2.join(root, page);
132
+ await listing(file, `Sources for ${name}:${page}`, [
133
+ `Collection: ${link(file, index, "collection sources")}`,
134
+ "",
135
+ ...lines.length ? lines : ["No page source history captured."]
136
+ ]);
137
+ }
138
+ await listing(index, `Sources for ${name}`, [
139
+ "## Raw sources",
140
+ "",
141
+ ...sourceLines.length ? sourceLines : ["No raw sources captured."],
142
+ "",
143
+ "## Dream learnings",
144
+ "",
145
+ ...dreamLines.length ? dreamLines : ["No dream learnings captured."],
146
+ "",
147
+ "## Pages",
148
+ "",
149
+ ...pages.map((page) => `- ${link(index, path2.join(root, page), page)}`)
150
+ ]);
151
+ result.index_paths.push(index);
152
+ } catch (err) {
153
+ result.errors.push(`${name}: ${String(err)}`);
154
+ }
155
+ }
156
+ result.index_paths.push(await globalIndex());
157
+ return result;
158
+ }
159
+ async function initMeta(entries) {
160
+ const result = { created: [], kept: [], index_paths: [], warnings: [], errors: [] };
161
+ for (const entry of entries) {
162
+ try {
163
+ const files = await markdownFiles(entry.path);
164
+ await ensureCollectionMeta(entry);
165
+ for (const file of files) {
166
+ const mirror = path2.join(collectionRoot(entry.name), validateMetaArticlePath(file));
167
+ (await seed(mirror, `Sources for ${entry.name}:${file}`) ? result.created : result.kept).push(mirror);
168
+ }
169
+ const indexed = await reindexMeta(entry.name);
170
+ result.index_paths.push(...indexed.index_paths);
171
+ result.warnings.push(...indexed.warnings);
172
+ result.errors.push(...indexed.errors);
173
+ } catch (err) {
174
+ result.errors.push(`${entry.name}: ${String(err)}`);
175
+ }
176
+ }
177
+ if (!entries.length) result.index_paths.push(await globalIndex());
178
+ result.index_paths = [...new Set(result.index_paths)];
179
+ return result;
180
+ }
181
+ async function createMetaRecord(entry, articlePath, kind, body) {
182
+ if (!META_KINDS.includes(kind)) throw new InvalidTargetError(kind, "unknown metadata template");
183
+ let mirror;
184
+ if (articlePath !== void 0) {
185
+ validateMetaArticlePath(articlePath);
186
+ const sourceRoot = await fs.realpath(entry.path);
187
+ const source = await fs.realpath(path2.resolve(entry.path, articlePath));
188
+ if (!within(sourceRoot, source) || !(await fs.stat(source)).isFile()) {
189
+ throw new InvalidTargetError(articlePath, "article must be a file within its collection");
190
+ }
191
+ mirror = await ensureArticleMeta(entry, articlePath);
192
+ } else await ensureCollectionMeta(entry);
193
+ const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
194
+ const id = `${capturedAt.replace(/[-:.]/g, "")}-${randomUUID().slice(0, 8)}`;
195
+ const file = await checked(path2.join(collectionRoot(entry.name), ".meta", "records", `${id}.md`));
196
+ await fs.mkdir(path2.dirname(file), { recursive: true });
197
+ const header = {
198
+ kind,
199
+ collection: entry.name,
200
+ ...articlePath ? { article_path: articlePath } : {},
201
+ captured_at: capturedAt
202
+ };
203
+ await fs.writeFile(file, `---
204
+ ${stringify(header)}---
205
+ ${body}`, { flag: "wx" });
206
+ const indexed = await reindexMeta(entry.name);
207
+ if (indexed.errors.length)
208
+ throw new DiffwikiError(
209
+ `Record saved at ${file}, but indexing failed: ${indexed.errors.join("; ")}. Repair with: diffwiki meta reindex ${entry.name}`
210
+ );
211
+ return {
212
+ id,
213
+ path: file,
214
+ target: articlePath ? `${entry.name}:${articlePath}` : entry.name,
215
+ ...mirror ? { mirror_path: mirror } : {},
216
+ index_paths: indexed.index_paths
217
+ };
218
+ }
219
+ var START = "<!-- diffwiki:meta:start -->";
220
+ var END = "<!-- diffwiki:meta:end -->";
221
+ var within = (root, file) => file === root || file.startsWith(root + path2.sep);
222
+ var missing = (err) => err.code === "ENOENT";
223
+ function collectionRoot(name) {
224
+ if (!name || name.startsWith(".") || /[/\\\0]/.test(name) || name.toLowerCase() === "meta.md") {
225
+ throw new InvalidTargetError(name, "expected a collection name without path segments or reserved names");
226
+ }
227
+ return path2.resolve(metaDir(), name);
228
+ }
229
+ async function checked(file) {
230
+ const root = path2.resolve(metaDir());
231
+ const absolute = path2.resolve(file);
232
+ if (!within(root, absolute)) throw new InvalidTargetError(file, "path escapes the metadata root");
233
+ const parts = path2.relative(root, absolute).split(path2.sep).filter(Boolean);
234
+ let current = root;
235
+ for (const part of ["", ...parts]) {
236
+ current = part ? path2.join(current, part) : current;
237
+ try {
238
+ if ((await fs.lstat(current)).isSymbolicLink()) throw new DiffwikiError(`metadata path is a symlink: ${current}`);
239
+ } catch (err) {
240
+ if (missing(err)) break;
241
+ throw err;
242
+ }
243
+ }
244
+ return absolute;
245
+ }
246
+ async function seed(file, title) {
247
+ await checked(file);
248
+ await fs.mkdir(path2.dirname(file), { recursive: true });
249
+ try {
250
+ await fs.writeFile(file, `# ${title}
251
+
252
+ ${START}
253
+ No source history captured.
254
+ ${END}
255
+ `, { flag: "wx" });
256
+ return true;
257
+ } catch (err) {
258
+ if (err.code === "EEXIST") return false;
259
+ throw err;
260
+ }
261
+ }
262
+ async function listing(file, title, lines) {
263
+ await seed(file, title);
264
+ const raw = await fs.readFile(await checked(file), "utf8");
265
+ const block = `${START}
266
+ ${lines.length ? lines.join("\n") : "No source history captured."}
267
+ ${END}`;
268
+ const start = raw.indexOf(START);
269
+ const end = raw.indexOf(END, start);
270
+ if (start >= 0 !== end >= 0) throw new DiffwikiError(`incomplete generated section in ${file}`);
271
+ const next = start >= 0 ? raw.slice(0, start) + block + raw.slice(end + END.length) : `${raw}
272
+ ${block}
273
+ `;
274
+ if (next === raw) return;
275
+ const temporary = `${file}.${randomUUID()}.tmp`;
276
+ try {
277
+ await fs.writeFile(await checked(temporary), next, { flag: "wx" });
278
+ await checked(file);
279
+ await fs.rename(temporary, file);
280
+ } finally {
281
+ await fs.rm(temporary, { force: true });
282
+ }
283
+ }
284
+ function link(from, to, label) {
285
+ const href = path2.relative(path2.dirname(from), to).split(path2.sep).map(encodeURIComponent).join("/");
286
+ return `[${label.replaceAll("[", "").replaceAll("]", "").replace(/[\r\n]/g, "")}](${href})`;
287
+ }
288
+ async function markdownFiles(root, rel = "") {
289
+ const out = [];
290
+ for (const item of (await fs.readdir(path2.join(root, rel), { withFileTypes: true })).sort(
291
+ (a, b) => a.name.localeCompare(b.name)
292
+ )) {
293
+ if (item.name.startsWith(".")) continue;
294
+ const file = rel ? `${rel}/${item.name}` : item.name;
295
+ if (item.isDirectory()) out.push(...await markdownFiles(root, file));
296
+ else if (item.isFile() && /\.mdx?$/i.test(file)) out.push(file);
297
+ }
298
+ return out;
299
+ }
300
+ async function globalIndex() {
301
+ await fs.mkdir(await checked(metaDir()), { recursive: true });
302
+ const index = path2.join(metaDir(), "META.md");
303
+ const lines = [];
304
+ for (const item of (await fs.readdir(metaDir(), { withFileTypes: true })).sort(
305
+ (a, b) => a.name.localeCompare(b.name)
306
+ )) {
307
+ if (!item.isDirectory() || item.name.startsWith(".")) continue;
308
+ const collectionIndex = path2.join(collectionRoot(item.name), ".meta", "META.md");
309
+ try {
310
+ await fs.access(await checked(collectionIndex));
311
+ lines.push(`- ${link(index, collectionIndex, item.name)}`);
312
+ } catch (err) {
313
+ if (!missing(err)) throw err;
314
+ }
315
+ }
316
+ await listing(index, "Source collections", lines);
317
+ return index;
318
+ }
319
+
320
+ // src/registry.ts
35
321
  function isMissing(err) {
36
322
  return err?.code === "ENOENT";
37
323
  }
38
324
  async function readRegistry() {
39
325
  try {
40
- const parsed = JSON.parse(await fs.readFile(registryPath(), "utf8"));
326
+ const parsed = JSON.parse(await fs2.readFile(registryPath(), "utf8"));
41
327
  return { version: parsed.version ?? 1, collections: parsed.collections ?? [] };
42
328
  } catch (err) {
43
329
  if (isMissing(err)) return { version: 1, collections: [] };
@@ -45,8 +331,8 @@ async function readRegistry() {
45
331
  }
46
332
  }
47
333
  async function writeRegistry(registry) {
48
- await fs.mkdir(resolveHome(), { recursive: true });
49
- await fs.writeFile(registryPath(), `${JSON.stringify(registry, null, 2)}
334
+ await fs2.mkdir(resolveHome(), { recursive: true });
335
+ await fs2.writeFile(registryPath(), `${JSON.stringify(registry, null, 2)}
50
336
  `, "utf8");
51
337
  }
52
338
  async function registerCollection(entry) {
@@ -58,6 +344,13 @@ async function registerCollection(entry) {
58
344
  registry.collections.push(entry);
59
345
  }
60
346
  await writeRegistry(registry);
347
+ try {
348
+ await ensureCollectionMeta(entry);
349
+ } catch (err) {
350
+ throw new DiffwikiError(
351
+ `Collection ${entry.name} registered, but metadata setup failed: ${String(err)}. Repair with: diffwiki meta init ${entry.name}`
352
+ );
353
+ }
61
354
  }
62
355
  async function unregisterCollection(name) {
63
356
  const registry = await readRegistry();
@@ -65,61 +358,10 @@ async function unregisterCollection(name) {
65
358
  await writeRegistry(registry);
66
359
  }
67
360
 
68
- // src/errors.ts
69
- var DiffwikiError = class extends Error {
70
- constructor(message) {
71
- super(message);
72
- this.name = new.target.name;
73
- }
74
- };
75
- var CollectionExistsError = class extends DiffwikiError {
76
- constructor(name) {
77
- super(`collection "${name}" already exists`);
78
- }
79
- };
80
- var CollectionNotFoundError = class extends DiffwikiError {
81
- constructor(name) {
82
- super(`collection "${name}" not found`);
83
- }
84
- };
85
- var NoDefaultCollectionError = class extends DiffwikiError {
86
- constructor() {
87
- super("no default collection set (use: diffwiki config-set defaultCollection <name>)");
88
- }
89
- };
90
- var ArticleNotFoundError = class extends DiffwikiError {
91
- constructor(target) {
92
- super(`article "${target}" not found`);
93
- }
94
- };
95
- var InvalidTargetError = class extends DiffwikiError {
96
- constructor(target, expected) {
97
- super(`invalid target "${target}" \u2014 ${expected}`);
98
- }
99
- };
100
- var PluginError = class extends DiffwikiError {
101
- };
102
- var WorktreeNotAllowedError = class extends DiffwikiError {
103
- constructor(root) {
104
- super(`refusing to add a linked worktree \u2014 add the source repository instead (${root})`);
105
- }
106
- };
107
- var CollectionAlreadyAddedError = class extends DiffwikiError {
108
- constructor(name) {
109
- super(`this repository is already added as collection "${name}"`);
110
- }
111
- };
112
- var InvalidExportSelectionError = class extends DiffwikiError {
113
- constructor(problems) {
114
- super(`export aborted \u2014 ${problems.length} selection problem(s):
115
- ${problems.map((p) => ` - ${p}`).join("\n")}`);
116
- }
117
- };
118
-
119
361
  // src/collections.ts
120
362
  async function fireHook(event, collection) {
121
363
  try {
122
- const { emitPluginEvent: emitPluginEvent2 } = await import("./events-K3MKWACA.js");
364
+ const { emitPluginEvent: emitPluginEvent2 } = await import("./events-UG4JPZDZ.js");
123
365
  await emitPluginEvent2(event, collection);
124
366
  } catch {
125
367
  }
@@ -136,7 +378,7 @@ async function findCollectionById(id) {
136
378
  async function createCollection(name, opts) {
137
379
  if (await findCollection(name)) throw new CollectionExistsError(name);
138
380
  const dir = collectionDir(name);
139
- await fs2.mkdir(dir, { recursive: true });
381
+ await fs3.mkdir(dir, { recursive: true });
140
382
  const entry = {
141
383
  name,
142
384
  type: "global",
@@ -153,20 +395,20 @@ async function removeCollection(name) {
153
395
  if (!entry) throw new CollectionNotFoundError(name);
154
396
  await unregisterCollection(name);
155
397
  if (entry.type === "global") {
156
- await fs2.rm(entry.path, { recursive: true, force: true });
398
+ await fs3.rm(entry.path, { recursive: true, force: true });
157
399
  }
158
400
  void fireHook("collection-removed", name);
159
401
  return entry;
160
402
  }
161
403
 
162
404
  // src/plugins/registry.ts
163
- import fs3 from "fs/promises";
405
+ import fs4 from "fs/promises";
164
406
  function isMissing2(err) {
165
407
  return err?.code === "ENOENT";
166
408
  }
167
409
  async function readPluginRegistry() {
168
410
  try {
169
- const parsed = JSON.parse(await fs3.readFile(pluginsRegistryPath(), "utf8"));
411
+ const parsed = JSON.parse(await fs4.readFile(pluginsRegistryPath(), "utf8"));
170
412
  return { version: parsed.version ?? 1, plugins: parsed.plugins ?? [] };
171
413
  } catch (err) {
172
414
  if (isMissing2(err)) return { version: 1, plugins: [] };
@@ -174,8 +416,8 @@ async function readPluginRegistry() {
174
416
  }
175
417
  }
176
418
  async function writePluginRegistry(registry) {
177
- await fs3.mkdir(resolveHome(), { recursive: true });
178
- await fs3.writeFile(pluginsRegistryPath(), `${JSON.stringify(registry, null, 2)}
419
+ await fs4.mkdir(resolveHome(), { recursive: true });
420
+ await fs4.writeFile(pluginsRegistryPath(), `${JSON.stringify(registry, null, 2)}
179
421
  `, "utf8");
180
422
  }
181
423
  async function upsertPlugin(record) {
@@ -209,7 +451,7 @@ async function findPluginRecord(name) {
209
451
 
210
452
  // src/plugins/host.ts
211
453
  import { spawn } from "child_process";
212
- import path2, { basename } from "path";
454
+ import path3, { basename } from "path";
213
455
 
214
456
  // src/logger.ts
215
457
  import { getLogger as logtapeGetLogger } from "@logtape/logtape";
@@ -346,7 +588,7 @@ function mapHits(result, collections) {
346
588
  if (typeof h.collection !== "string" || typeof h.relPath !== "string") continue;
347
589
  const title = typeof h.title === "string" ? h.title : h.relPath;
348
590
  const coll = collections.find((c) => c.name === h.collection);
349
- const abs = coll ? path2.join(coll.path, h.relPath) : h.relPath;
591
+ const abs = coll ? path3.join(coll.path, h.relPath) : h.relPath;
350
592
  out.push({
351
593
  collection: h.collection,
352
594
  path: abs,
@@ -473,24 +715,23 @@ async function emitPluginEvent(event, collection, relPath) {
473
715
 
474
716
  export {
475
717
  NATIVE_ENGINE,
476
- DiffwikiError,
477
- CollectionExistsError,
478
- CollectionNotFoundError,
479
- NoDefaultCollectionError,
480
- ArticleNotFoundError,
481
- InvalidTargetError,
482
- PluginError,
483
- WorktreeNotAllowedError,
484
- CollectionAlreadyAddedError,
485
- InvalidExportSelectionError,
486
718
  resolveHome,
487
719
  registryPath,
488
720
  configPath,
489
721
  collectionsDir,
490
722
  templatesDir,
723
+ metaDir,
491
724
  collectionDir,
492
725
  pluginsDir,
493
726
  pluginsRegistryPath,
727
+ META_KINDS,
728
+ validateMetaArticlePath,
729
+ ensureCollectionMeta,
730
+ ensureArticleMeta,
731
+ mirrorSavedArticle,
732
+ reindexMeta,
733
+ initMeta,
734
+ createMetaRecord,
494
735
  readRegistry,
495
736
  writeRegistry,
496
737
  registerCollection,
@@ -1,9 +1,13 @@
1
+ import {
2
+ DiffwikiError
3
+ } from "./chunk-FJ5XD2JN.js";
4
+
1
5
  // src/frontmatter.ts
2
6
  import matter from "gray-matter";
3
- import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
7
+ import { parse as parseYaml, stringify as stringifyYaml, parseDocument, isScalar } from "yaml";
4
8
  var YAML_ENGINE = {
5
9
  parse: (input) => {
6
- const data = parseYaml(input);
10
+ const data = parseYaml(input.replace(/\r\n?/g, "\n"));
7
11
  return data && typeof data === "object" ? data : {};
8
12
  },
9
13
  stringify: (data) => stringifyYaml(data)
@@ -47,6 +51,31 @@ var FRONTMATTER_BLOCK_RE = /^\uFEFF?[ \t]*---\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n
47
51
  function stripFrontmatter(raw) {
48
52
  return raw.replace(FRONTMATTER_BLOCK_RE, "");
49
53
  }
54
+ function patchArticle(raw, fields, body) {
55
+ const fence = /^(\uFEFF?[ \t]*---[ \t]*\r?\n)([\s\S]*?)(\r?\n---[ \t]*(?:\r?\n|$))/.exec(raw);
56
+ if (Object.keys(fields).length === 0) return fence ? fence[0] + (body ?? raw.slice(fence[0].length)) : body ?? raw;
57
+ const originalYaml = fence?.[2] ?? "";
58
+ const doc = parseDocument(originalYaml);
59
+ if (doc.errors.length)
60
+ throw new DiffwikiError("Cannot update article with invalid YAML frontmatter. Fix its header first.");
61
+ const titleNode = doc.get("title", true);
62
+ if (fence && body === void 0 && Object.keys(fields).length === 1 && typeof fields.title === "string" && !/[\r\n]/.test(fields.title) && isScalar(titleNode) && titleNode.range) {
63
+ const [start, end] = titleNode.range;
64
+ if (!/[\r\n]/.test(originalYaml.slice(start, end))) {
65
+ const scalar = stringifyYaml(fields.title, { lineWidth: 0 }).trimEnd();
66
+ return fence[1] + originalYaml.slice(0, start) + scalar + originalYaml.slice(end) + fence[3] + raw.slice(fence[0].length);
67
+ }
68
+ }
69
+ for (const [key, value] of Object.entries(fields)) {
70
+ if (value === void 0) doc.delete(key);
71
+ else doc.set(key, value);
72
+ }
73
+ const newline = fence?.[1].endsWith("\r\n") ? "\r\n" : "\n";
74
+ const yaml = doc.toString({ lineWidth: 0 }).replace(/\n$/, "").replace(/\r?\n/g, newline);
75
+ const content = body ?? (fence ? raw.slice(fence[0].length) : raw);
76
+ const close = fence?.[3] ?? `${newline}---${newline}`;
77
+ return (fence?.[1] ?? `---${newline}`) + yaml + close + (body && !close.endsWith("\n") ? newline : "") + content;
78
+ }
50
79
  function parseFrontmatter(raw) {
51
80
  try {
52
81
  const { data, content } = matter(raw, MATTER_OPTS);
@@ -266,6 +295,7 @@ function definePlugin(def) {
266
295
 
267
296
  export {
268
297
  stripFrontmatter,
298
+ patchArticle,
269
299
  parseFrontmatter,
270
300
  parseArticle,
271
301
  serializeArticle,
@@ -0,0 +1,7 @@
1
+ import {
2
+ emitPluginEvent
3
+ } from "./chunk-KUX7GHHM.js";
4
+ import "./chunk-FJ5XD2JN.js";
5
+ export {
6
+ emitPluginEvent
7
+ };
@@ -569,6 +569,8 @@ interface SerializableArticle {
569
569
  * span. Returns the input unchanged when there is no frontmatter.
570
570
  */
571
571
  declare function stripFrontmatter(raw: string): string;
572
+ /** Edit requested fields while preserving unknown YAML keys, comments, and the untouched body. */
573
+ declare function patchArticle(raw: string, fields: Record<string, unknown>, body?: string): string;
572
574
  /**
573
575
  * Parse a document's YAML frontmatter into a raw key/value object (every key,
574
576
  * not just the article axes), plus the body. Bundler-safe via the pinned `yaml`
@@ -582,4 +584,4 @@ declare function parseFrontmatter(raw: string): {
582
584
  declare function parseArticle(raw: string): ParsedArticle;
583
585
  declare function serializeArticle(article: SerializableArticle): string;
584
586
 
585
- export { type ResolvedDependency as $, type Audience as A, type PluginHealth as B, type Config as C, type Diagnostic as D, type PluginReadiness as E, type ProjectCollection as F, type GraphEdge as G, type HomeCard as H, type ProjectContext as I, type SearchExpansion as J, type SerializableArticle as K, type LinkGraph as L, type SiteCopyright as M, NATIVE_ENGINE as N, type SiteHome as O, type PublishStatus as P, type QueryHit as Q, type Registry as R, type SiteConfig as S, type TreeNode as T, parseArticle as U, parseFrontmatter as V, serializeArticle as W, stripFrontmatter as X, type PluginDefinition as Y, type DefinedPlugin as Z, type PluginDependency as _, type CollectionEntry as a, type RunProcessOptions as a0, type EventHandlerResult as a1, type EventParams as a2, type HealthHandlerResult as a3, type HealthParams as a4, type IndexHandlerResult as a5, type IndexParams as a6, type LogLevel as a7, type PluginCapabilities$1 as a8, type PluginHandlers as a9, type PluginLogFn as aa, type SearchHandlerHit as ab, type SearchHandlerResult as ac, type SearchParams as ad, type SetupHandlerResult as ae, type SetupParams as af, type WireCollection as ag, type WireDiagnostic as ah, type WireReadiness as ai, type Article as b, type RepoConfig as c, type ProjectConfig as d, type CollectionInfo as e, type CollectionTree as f, type PageData as g, type ArticleNode as h, type QueryOptions as i, type ParsedArticle as j, type ArticleContent as k, type TocItem as l, type LocalGraph as m, type SearchMode as n, type SearchProvider as o, type PluginRecord as p, type PluginEvent as q, type PluginKind as r, type PluginRegistryFile as s, type CollectionType as t, type DiagnosticLevel as u, type DocData as v, type GraphNode as w, type ListingEntry as x, type NodeMeta as y, type PluginCapabilities as z };
587
+ export { type PluginDependency as $, type Audience as A, type PluginHealth as B, type Config as C, type Diagnostic as D, type PluginReadiness as E, type ProjectCollection as F, type GraphEdge as G, type HomeCard as H, type ProjectContext as I, type SearchExpansion as J, type SerializableArticle as K, type LinkGraph as L, type SiteCopyright as M, NATIVE_ENGINE as N, type SiteHome as O, type PublishStatus as P, type QueryHit as Q, type Registry as R, type SiteConfig as S, type TreeNode as T, parseArticle as U, parseFrontmatter as V, patchArticle as W, serializeArticle as X, stripFrontmatter as Y, type PluginDefinition as Z, type DefinedPlugin as _, type CollectionEntry as a, type ResolvedDependency as a0, type RunProcessOptions as a1, type EventHandlerResult as a2, type EventParams as a3, type HealthHandlerResult as a4, type HealthParams as a5, type IndexHandlerResult as a6, type IndexParams as a7, type LogLevel as a8, type PluginCapabilities$1 as a9, type PluginHandlers as aa, type PluginLogFn as ab, type SearchHandlerHit as ac, type SearchHandlerResult as ad, type SearchParams as ae, type SetupHandlerResult as af, type SetupParams as ag, type WireCollection as ah, type WireDiagnostic as ai, type WireReadiness as aj, type Article as b, type RepoConfig as c, type ProjectConfig as d, type CollectionInfo as e, type CollectionTree as f, type PageData as g, type ArticleNode as h, type QueryOptions as i, type ParsedArticle as j, type ArticleContent as k, type TocItem as l, type LocalGraph as m, type SearchMode as n, type SearchProvider as o, type PluginRecord as p, type PluginEvent as q, type PluginKind as r, type PluginRegistryFile as s, type CollectionType as t, type DiagnosticLevel as u, type DocData as v, type GraphNode as w, type ListingEntry as x, type NodeMeta as y, type PluginCapabilities as z };
@@ -569,6 +569,8 @@ interface SerializableArticle {
569
569
  * span. Returns the input unchanged when there is no frontmatter.
570
570
  */
571
571
  declare function stripFrontmatter(raw: string): string;
572
+ /** Edit requested fields while preserving unknown YAML keys, comments, and the untouched body. */
573
+ declare function patchArticle(raw: string, fields: Record<string, unknown>, body?: string): string;
572
574
  /**
573
575
  * Parse a document's YAML frontmatter into a raw key/value object (every key,
574
576
  * not just the article axes), plus the body. Bundler-safe via the pinned `yaml`
@@ -582,4 +584,4 @@ declare function parseFrontmatter(raw: string): {
582
584
  declare function parseArticle(raw: string): ParsedArticle;
583
585
  declare function serializeArticle(article: SerializableArticle): string;
584
586
 
585
- export { type ResolvedDependency as $, type Audience as A, type PluginHealth as B, type Config as C, type Diagnostic as D, type PluginReadiness as E, type ProjectCollection as F, type GraphEdge as G, type HomeCard as H, type ProjectContext as I, type SearchExpansion as J, type SerializableArticle as K, type LinkGraph as L, type SiteCopyright as M, NATIVE_ENGINE as N, type SiteHome as O, type PublishStatus as P, type QueryHit as Q, type Registry as R, type SiteConfig as S, type TreeNode as T, parseArticle as U, parseFrontmatter as V, serializeArticle as W, stripFrontmatter as X, type PluginDefinition as Y, type DefinedPlugin as Z, type PluginDependency as _, type CollectionEntry as a, type RunProcessOptions as a0, type EventHandlerResult as a1, type EventParams as a2, type HealthHandlerResult as a3, type HealthParams as a4, type IndexHandlerResult as a5, type IndexParams as a6, type LogLevel as a7, type PluginCapabilities$1 as a8, type PluginHandlers as a9, type PluginLogFn as aa, type SearchHandlerHit as ab, type SearchHandlerResult as ac, type SearchParams as ad, type SetupHandlerResult as ae, type SetupParams as af, type WireCollection as ag, type WireDiagnostic as ah, type WireReadiness as ai, type Article as b, type RepoConfig as c, type ProjectConfig as d, type CollectionInfo as e, type CollectionTree as f, type PageData as g, type ArticleNode as h, type QueryOptions as i, type ParsedArticle as j, type ArticleContent as k, type TocItem as l, type LocalGraph as m, type SearchMode as n, type SearchProvider as o, type PluginRecord as p, type PluginEvent as q, type PluginKind as r, type PluginRegistryFile as s, type CollectionType as t, type DiagnosticLevel as u, type DocData as v, type GraphNode as w, type ListingEntry as x, type NodeMeta as y, type PluginCapabilities as z };
587
+ export { type PluginDependency as $, type Audience as A, type PluginHealth as B, type Config as C, type Diagnostic as D, type PluginReadiness as E, type ProjectCollection as F, type GraphEdge as G, type HomeCard as H, type ProjectContext as I, type SearchExpansion as J, type SerializableArticle as K, type LinkGraph as L, type SiteCopyright as M, NATIVE_ENGINE as N, type SiteHome as O, type PublishStatus as P, type QueryHit as Q, type Registry as R, type SiteConfig as S, type TreeNode as T, parseArticle as U, parseFrontmatter as V, patchArticle as W, serializeArticle as X, stripFrontmatter as Y, type PluginDefinition as Z, type DefinedPlugin as _, type CollectionEntry as a, type ResolvedDependency as a0, type RunProcessOptions as a1, type EventHandlerResult as a2, type EventParams as a3, type HealthHandlerResult as a4, type HealthParams as a5, type IndexHandlerResult as a6, type IndexParams as a7, type LogLevel as a8, type PluginCapabilities$1 as a9, type PluginHandlers as aa, type PluginLogFn as ab, type SearchHandlerHit as ac, type SearchHandlerResult as ad, type SearchParams as ae, type SetupHandlerResult as af, type SetupParams as ag, type WireCollection as ah, type WireDiagnostic as ai, type WireReadiness as aj, type Article as b, type RepoConfig as c, type ProjectConfig as d, type CollectionInfo as e, type CollectionTree as f, type PageData as g, type ArticleNode as h, type QueryOptions as i, type ParsedArticle as j, type ArticleContent as k, type TocItem as l, type LocalGraph as m, type SearchMode as n, type SearchProvider as o, type PluginRecord as p, type PluginEvent as q, type PluginKind as r, type PluginRegistryFile as s, type CollectionType as t, type DiagnosticLevel as u, type DocData as v, type GraphNode as w, type ListingEntry as x, type NodeMeta as y, type PluginCapabilities as z };