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.
package/dist/index.cjs CHANGED
@@ -113,6 +113,9 @@ function collectionsDir() {
113
113
  function templatesDir() {
114
114
  return import_node_path.default.join(resolveHome(), "templates");
115
115
  }
116
+ function metaDir() {
117
+ return import_node_path.default.join(resolveHome(), "meta");
118
+ }
116
119
  function collectionDir(name) {
117
120
  return import_node_path.default.join(collectionsDir(), name);
118
121
  }
@@ -131,13 +134,295 @@ var init_paths = __esm({
131
134
  }
132
135
  });
133
136
 
137
+ // src/meta.ts
138
+ function validateMetaArticlePath(file) {
139
+ if (!/\.mdx?$/i.test(file) || import_node_path3.default.isAbsolute(file) || /[\\\0]/.test(file) || file.split("/").some((part) => !part || part === "." || part === ".." || part.startsWith("."))) {
140
+ throw new InvalidTargetError(file, "expected an exact relative .md/.mdx file path without traversal");
141
+ }
142
+ return file;
143
+ }
144
+ async function ensureCollectionMeta(entry) {
145
+ const root = collectionRoot(entry.name);
146
+ const source = import_node_path3.default.resolve(entry.path);
147
+ if (within(source, import_node_path3.default.resolve(metaDir())) || within(import_node_path3.default.resolve(metaDir()), source)) {
148
+ throw new InvalidTargetError(entry.path, "collection content overlaps the metadata store");
149
+ }
150
+ const index = import_node_path3.default.join(root, ".meta", "META.md");
151
+ await seed(index, `Sources for ${entry.name}`);
152
+ await globalIndex();
153
+ return index;
154
+ }
155
+ async function ensureArticleMeta(entry, articlePath) {
156
+ const relative = validateMetaArticlePath(articlePath);
157
+ await ensureCollectionMeta(entry);
158
+ const mirror = import_node_path3.default.join(collectionRoot(entry.name), relative);
159
+ if (await seed(mirror, `Sources for ${entry.name}:${relative}`)) await reindexMeta(entry.name);
160
+ return mirror;
161
+ }
162
+ async function mirrorSavedArticle(entry, file) {
163
+ try {
164
+ await ensureArticleMeta(entry, import_node_path3.default.relative(entry.path, file).split(import_node_path3.default.sep).join("/"));
165
+ } catch (err) {
166
+ throw new DiffwikiError(
167
+ `Article saved at ${file}, but metadata setup failed: ${String(err)}. Repair with: diffwiki meta init ${entry.name}`
168
+ );
169
+ }
170
+ }
171
+ async function reindexMeta(collection) {
172
+ await import_promises3.default.mkdir(await checked(metaDir()), { recursive: true });
173
+ const result = { created: [], kept: [], index_paths: [], warnings: [], errors: [] };
174
+ const names = collection ? [collection] : (await import_promises3.default.readdir(metaDir(), { withFileTypes: true })).filter((item) => item.isDirectory() && !item.name.startsWith(".")).map((item) => item.name).sort();
175
+ for (const name of names) {
176
+ try {
177
+ const root = await checked(collectionRoot(name));
178
+ const index = import_node_path3.default.join(root, ".meta", "META.md");
179
+ await import_promises3.default.access(await checked(index));
180
+ const pages = await markdownFiles(root);
181
+ const recordDir = await checked(import_node_path3.default.join(root, ".meta", "records"));
182
+ let records = [];
183
+ try {
184
+ records = (await import_promises3.default.readdir(recordDir)).filter((file) => file.endsWith(".md")).sort();
185
+ } catch (err) {
186
+ if (!missing(err)) throw err;
187
+ }
188
+ const pageLines = new Map(pages.map((file) => [file, []]));
189
+ const sourceLines = [];
190
+ const dreamLines = [];
191
+ for (const record of records) {
192
+ const file = await checked(import_node_path3.default.join(recordDir, record));
193
+ try {
194
+ const raw = await import_promises3.default.readFile(file, "utf8");
195
+ const front = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(raw);
196
+ if (!front) throw new Error("missing record header");
197
+ const doc = (0, import_yaml2.parseDocument)(front[1]);
198
+ if (doc.errors.length) throw new Error("invalid YAML header");
199
+ const data = doc.toJS();
200
+ if (!data || !META_KINDS.includes(data.kind) || data.collection !== name || typeof data.captured_at !== "string") {
201
+ throw new Error("invalid kind, collection, or capture time");
202
+ }
203
+ const article = data.article_path === void 0 ? void 0 : validateMetaArticlePath(String(data.article_path));
204
+ const label = `${data.kind} \xB7 ${record.replace(/\.md$/, "")}`;
205
+ const bucket = data.kind === "dream-learning" ? dreamLines : sourceLines;
206
+ bucket.push(`- ${link(index, file, label)}${article ? ` \u2014 \`${article}\`` : " \u2014 collection"}`);
207
+ if (article) {
208
+ if (!pageLines.has(article))
209
+ result.warnings.push(`${file}: no manifest for ${article}; run meta init if the article exists`);
210
+ else pageLines.get(article).push(`- ${link(import_node_path3.default.join(root, article), file, label)}`);
211
+ }
212
+ } catch (err) {
213
+ result.warnings.push(`${file}: ${String(err)}`);
214
+ }
215
+ }
216
+ for (const [page, lines] of pageLines) {
217
+ const file = import_node_path3.default.join(root, page);
218
+ await listing(file, `Sources for ${name}:${page}`, [
219
+ `Collection: ${link(file, index, "collection sources")}`,
220
+ "",
221
+ ...lines.length ? lines : ["No page source history captured."]
222
+ ]);
223
+ }
224
+ await listing(index, `Sources for ${name}`, [
225
+ "## Raw sources",
226
+ "",
227
+ ...sourceLines.length ? sourceLines : ["No raw sources captured."],
228
+ "",
229
+ "## Dream learnings",
230
+ "",
231
+ ...dreamLines.length ? dreamLines : ["No dream learnings captured."],
232
+ "",
233
+ "## Pages",
234
+ "",
235
+ ...pages.map((page) => `- ${link(index, import_node_path3.default.join(root, page), page)}`)
236
+ ]);
237
+ result.index_paths.push(index);
238
+ } catch (err) {
239
+ result.errors.push(`${name}: ${String(err)}`);
240
+ }
241
+ }
242
+ result.index_paths.push(await globalIndex());
243
+ return result;
244
+ }
245
+ async function initMeta(entries) {
246
+ const result = { created: [], kept: [], index_paths: [], warnings: [], errors: [] };
247
+ for (const entry of entries) {
248
+ try {
249
+ const files = await markdownFiles(entry.path);
250
+ await ensureCollectionMeta(entry);
251
+ for (const file of files) {
252
+ const mirror = import_node_path3.default.join(collectionRoot(entry.name), validateMetaArticlePath(file));
253
+ (await seed(mirror, `Sources for ${entry.name}:${file}`) ? result.created : result.kept).push(mirror);
254
+ }
255
+ const indexed = await reindexMeta(entry.name);
256
+ result.index_paths.push(...indexed.index_paths);
257
+ result.warnings.push(...indexed.warnings);
258
+ result.errors.push(...indexed.errors);
259
+ } catch (err) {
260
+ result.errors.push(`${entry.name}: ${String(err)}`);
261
+ }
262
+ }
263
+ if (!entries.length) result.index_paths.push(await globalIndex());
264
+ result.index_paths = [...new Set(result.index_paths)];
265
+ return result;
266
+ }
267
+ async function createMetaRecord(entry, articlePath, kind, body) {
268
+ if (!META_KINDS.includes(kind)) throw new InvalidTargetError(kind, "unknown metadata template");
269
+ let mirror;
270
+ if (articlePath !== void 0) {
271
+ validateMetaArticlePath(articlePath);
272
+ const sourceRoot = await import_promises3.default.realpath(entry.path);
273
+ const source = await import_promises3.default.realpath(import_node_path3.default.resolve(entry.path, articlePath));
274
+ if (!within(sourceRoot, source) || !(await import_promises3.default.stat(source)).isFile()) {
275
+ throw new InvalidTargetError(articlePath, "article must be a file within its collection");
276
+ }
277
+ mirror = await ensureArticleMeta(entry, articlePath);
278
+ } else await ensureCollectionMeta(entry);
279
+ const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
280
+ const id = `${capturedAt.replace(/[-:.]/g, "")}-${(0, import_node_crypto.randomUUID)().slice(0, 8)}`;
281
+ const file = await checked(import_node_path3.default.join(collectionRoot(entry.name), ".meta", "records", `${id}.md`));
282
+ await import_promises3.default.mkdir(import_node_path3.default.dirname(file), { recursive: true });
283
+ const header = {
284
+ kind,
285
+ collection: entry.name,
286
+ ...articlePath ? { article_path: articlePath } : {},
287
+ captured_at: capturedAt
288
+ };
289
+ await import_promises3.default.writeFile(file, `---
290
+ ${(0, import_yaml2.stringify)(header)}---
291
+ ${body}`, { flag: "wx" });
292
+ const indexed = await reindexMeta(entry.name);
293
+ if (indexed.errors.length)
294
+ throw new DiffwikiError(
295
+ `Record saved at ${file}, but indexing failed: ${indexed.errors.join("; ")}. Repair with: diffwiki meta reindex ${entry.name}`
296
+ );
297
+ return {
298
+ id,
299
+ path: file,
300
+ target: articlePath ? `${entry.name}:${articlePath}` : entry.name,
301
+ ...mirror ? { mirror_path: mirror } : {},
302
+ index_paths: indexed.index_paths
303
+ };
304
+ }
305
+ function collectionRoot(name) {
306
+ if (!name || name.startsWith(".") || /[/\\\0]/.test(name) || name.toLowerCase() === "meta.md") {
307
+ throw new InvalidTargetError(name, "expected a collection name without path segments or reserved names");
308
+ }
309
+ return import_node_path3.default.resolve(metaDir(), name);
310
+ }
311
+ async function checked(file) {
312
+ const root = import_node_path3.default.resolve(metaDir());
313
+ const absolute = import_node_path3.default.resolve(file);
314
+ if (!within(root, absolute)) throw new InvalidTargetError(file, "path escapes the metadata root");
315
+ const parts = import_node_path3.default.relative(root, absolute).split(import_node_path3.default.sep).filter(Boolean);
316
+ let current = root;
317
+ for (const part of ["", ...parts]) {
318
+ current = part ? import_node_path3.default.join(current, part) : current;
319
+ try {
320
+ if ((await import_promises3.default.lstat(current)).isSymbolicLink()) throw new DiffwikiError(`metadata path is a symlink: ${current}`);
321
+ } catch (err) {
322
+ if (missing(err)) break;
323
+ throw err;
324
+ }
325
+ }
326
+ return absolute;
327
+ }
328
+ async function seed(file, title) {
329
+ await checked(file);
330
+ await import_promises3.default.mkdir(import_node_path3.default.dirname(file), { recursive: true });
331
+ try {
332
+ await import_promises3.default.writeFile(file, `# ${title}
333
+
334
+ ${START}
335
+ No source history captured.
336
+ ${END}
337
+ `, { flag: "wx" });
338
+ return true;
339
+ } catch (err) {
340
+ if (err.code === "EEXIST") return false;
341
+ throw err;
342
+ }
343
+ }
344
+ async function listing(file, title, lines) {
345
+ await seed(file, title);
346
+ const raw = await import_promises3.default.readFile(await checked(file), "utf8");
347
+ const block = `${START}
348
+ ${lines.length ? lines.join("\n") : "No source history captured."}
349
+ ${END}`;
350
+ const start = raw.indexOf(START);
351
+ const end = raw.indexOf(END, start);
352
+ if (start >= 0 !== end >= 0) throw new DiffwikiError(`incomplete generated section in ${file}`);
353
+ const next = start >= 0 ? raw.slice(0, start) + block + raw.slice(end + END.length) : `${raw}
354
+ ${block}
355
+ `;
356
+ if (next === raw) return;
357
+ const temporary = `${file}.${(0, import_node_crypto.randomUUID)()}.tmp`;
358
+ try {
359
+ await import_promises3.default.writeFile(await checked(temporary), next, { flag: "wx" });
360
+ await checked(file);
361
+ await import_promises3.default.rename(temporary, file);
362
+ } finally {
363
+ await import_promises3.default.rm(temporary, { force: true });
364
+ }
365
+ }
366
+ function link(from, to, label) {
367
+ const href = import_node_path3.default.relative(import_node_path3.default.dirname(from), to).split(import_node_path3.default.sep).map(encodeURIComponent).join("/");
368
+ return `[${label.replaceAll("[", "").replaceAll("]", "").replace(/[\r\n]/g, "")}](${href})`;
369
+ }
370
+ async function markdownFiles(root, rel = "") {
371
+ const out = [];
372
+ for (const item of (await import_promises3.default.readdir(import_node_path3.default.join(root, rel), { withFileTypes: true })).sort(
373
+ (a, b) => a.name.localeCompare(b.name)
374
+ )) {
375
+ if (item.name.startsWith(".")) continue;
376
+ const file = rel ? `${rel}/${item.name}` : item.name;
377
+ if (item.isDirectory()) out.push(...await markdownFiles(root, file));
378
+ else if (item.isFile() && /\.mdx?$/i.test(file)) out.push(file);
379
+ }
380
+ return out;
381
+ }
382
+ async function globalIndex() {
383
+ await import_promises3.default.mkdir(await checked(metaDir()), { recursive: true });
384
+ const index = import_node_path3.default.join(metaDir(), "META.md");
385
+ const lines = [];
386
+ for (const item of (await import_promises3.default.readdir(metaDir(), { withFileTypes: true })).sort(
387
+ (a, b) => a.name.localeCompare(b.name)
388
+ )) {
389
+ if (!item.isDirectory() || item.name.startsWith(".")) continue;
390
+ const collectionIndex = import_node_path3.default.join(collectionRoot(item.name), ".meta", "META.md");
391
+ try {
392
+ await import_promises3.default.access(await checked(collectionIndex));
393
+ lines.push(`- ${link(index, collectionIndex, item.name)}`);
394
+ } catch (err) {
395
+ if (!missing(err)) throw err;
396
+ }
397
+ }
398
+ await listing(index, "Source collections", lines);
399
+ return index;
400
+ }
401
+ var import_promises3, import_node_path3, import_node_crypto, import_yaml2, META_KINDS, START, END, within, missing;
402
+ var init_meta = __esm({
403
+ "src/meta.ts"() {
404
+ "use strict";
405
+ import_promises3 = __toESM(require("fs/promises"), 1);
406
+ import_node_path3 = __toESM(require("path"), 1);
407
+ import_node_crypto = require("crypto");
408
+ import_yaml2 = require("yaml");
409
+ init_paths();
410
+ init_errors();
411
+ META_KINDS = ["history", "external-source-extract", "dream-learning"];
412
+ START = "<!-- diffwiki:meta:start -->";
413
+ END = "<!-- diffwiki:meta:end -->";
414
+ within = (root, file) => file === root || file.startsWith(root + import_node_path3.default.sep);
415
+ missing = (err) => err.code === "ENOENT";
416
+ }
417
+ });
418
+
134
419
  // src/registry.ts
135
420
  function isMissing(err) {
136
421
  return err?.code === "ENOENT";
137
422
  }
138
423
  async function readRegistry() {
139
424
  try {
140
- const parsed = JSON.parse(await import_promises3.default.readFile(registryPath(), "utf8"));
425
+ const parsed = JSON.parse(await import_promises4.default.readFile(registryPath(), "utf8"));
141
426
  return { version: parsed.version ?? 1, collections: parsed.collections ?? [] };
142
427
  } catch (err) {
143
428
  if (isMissing(err)) return { version: 1, collections: [] };
@@ -145,8 +430,8 @@ async function readRegistry() {
145
430
  }
146
431
  }
147
432
  async function writeRegistry(registry) {
148
- await import_promises3.default.mkdir(resolveHome(), { recursive: true });
149
- await import_promises3.default.writeFile(registryPath(), `${JSON.stringify(registry, null, 2)}
433
+ await import_promises4.default.mkdir(resolveHome(), { recursive: true });
434
+ await import_promises4.default.writeFile(registryPath(), `${JSON.stringify(registry, null, 2)}
150
435
  `, "utf8");
151
436
  }
152
437
  async function registerCollection(entry) {
@@ -158,18 +443,27 @@ async function registerCollection(entry) {
158
443
  registry.collections.push(entry);
159
444
  }
160
445
  await writeRegistry(registry);
446
+ try {
447
+ await ensureCollectionMeta(entry);
448
+ } catch (err) {
449
+ throw new DiffwikiError(
450
+ `Collection ${entry.name} registered, but metadata setup failed: ${String(err)}. Repair with: diffwiki meta init ${entry.name}`
451
+ );
452
+ }
161
453
  }
162
454
  async function unregisterCollection(name) {
163
455
  const registry = await readRegistry();
164
456
  registry.collections = registry.collections.filter((c) => c.name !== name);
165
457
  await writeRegistry(registry);
166
458
  }
167
- var import_promises3;
459
+ var import_promises4;
168
460
  var init_registry = __esm({
169
461
  "src/registry.ts"() {
170
462
  "use strict";
171
- import_promises3 = __toESM(require("fs/promises"), 1);
463
+ import_promises4 = __toESM(require("fs/promises"), 1);
172
464
  init_paths();
465
+ init_meta();
466
+ init_errors();
173
467
  }
174
468
  });
175
469
 
@@ -179,7 +473,7 @@ function isMissing2(err) {
179
473
  }
180
474
  async function readPluginRegistry() {
181
475
  try {
182
- const parsed = JSON.parse(await import_promises4.default.readFile(pluginsRegistryPath(), "utf8"));
476
+ const parsed = JSON.parse(await import_promises5.default.readFile(pluginsRegistryPath(), "utf8"));
183
477
  return { version: parsed.version ?? 1, plugins: parsed.plugins ?? [] };
184
478
  } catch (err) {
185
479
  if (isMissing2(err)) return { version: 1, plugins: [] };
@@ -187,8 +481,8 @@ async function readPluginRegistry() {
187
481
  }
188
482
  }
189
483
  async function writePluginRegistry(registry) {
190
- await import_promises4.default.mkdir(resolveHome(), { recursive: true });
191
- await import_promises4.default.writeFile(pluginsRegistryPath(), `${JSON.stringify(registry, null, 2)}
484
+ await import_promises5.default.mkdir(resolveHome(), { recursive: true });
485
+ await import_promises5.default.writeFile(pluginsRegistryPath(), `${JSON.stringify(registry, null, 2)}
192
486
  `, "utf8");
193
487
  }
194
488
  async function upsertPlugin(record) {
@@ -219,11 +513,11 @@ async function findPluginRecord(name) {
219
513
  const registry = await readPluginRegistry();
220
514
  return registry.plugins.find((p) => p.name === name);
221
515
  }
222
- var import_promises4;
516
+ var import_promises5;
223
517
  var init_registry2 = __esm({
224
518
  "src/plugins/registry.ts"() {
225
519
  "use strict";
226
- import_promises4 = __toESM(require("fs/promises"), 1);
520
+ import_promises5 = __toESM(require("fs/promises"), 1);
227
521
  init_paths();
228
522
  }
229
523
  });
@@ -250,7 +544,7 @@ var init_logger = __esm({
250
544
  // src/plugins/host.ts
251
545
  async function invokePlugin(command, op, params, opts) {
252
546
  const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
253
- const scope = opts?.name ?? (0, import_node_path3.basename)(command[1] ?? command[0] ?? "plugin");
547
+ const scope = opts?.name ?? (0, import_node_path4.basename)(command[1] ?? command[0] ?? "plugin");
254
548
  return new Promise((resolve, reject) => {
255
549
  const child = (0, import_node_child_process.spawn)(command[0], command.slice(1), {
256
550
  stdio: ["pipe", "pipe", "pipe"]
@@ -354,7 +648,7 @@ function mapHits(result, collections) {
354
648
  if (typeof h.collection !== "string" || typeof h.relPath !== "string") continue;
355
649
  const title = typeof h.title === "string" ? h.title : h.relPath;
356
650
  const coll = collections.find((c) => c.name === h.collection);
357
- const abs = coll ? import_node_path3.default.join(coll.path, h.relPath) : h.relPath;
651
+ const abs = coll ? import_node_path4.default.join(coll.path, h.relPath) : h.relPath;
358
652
  out.push({
359
653
  collection: h.collection,
360
654
  path: abs,
@@ -455,12 +749,12 @@ async function listSearchModes() {
455
749
  }
456
750
  return modes;
457
751
  }
458
- var import_node_child_process, import_node_path3, LEVEL_METHOD, DEFAULT_TIMEOUT_MS, SEARCH_TIMEOUT_MS, LONG_TIMEOUT_MS;
752
+ var import_node_child_process, import_node_path4, LEVEL_METHOD, DEFAULT_TIMEOUT_MS, SEARCH_TIMEOUT_MS, LONG_TIMEOUT_MS;
459
753
  var init_host = __esm({
460
754
  "src/plugins/host.ts"() {
461
755
  "use strict";
462
756
  import_node_child_process = require("child_process");
463
- import_node_path3 = __toESM(require("path"), 1);
757
+ import_node_path4 = __toESM(require("path"), 1);
464
758
  init_logger();
465
759
  init_types();
466
760
  init_registry2();
@@ -534,7 +828,7 @@ async function findCollectionById(id) {
534
828
  async function createCollection(name, opts) {
535
829
  if (await findCollection(name)) throw new CollectionExistsError(name);
536
830
  const dir = collectionDir(name);
537
- await import_promises5.default.mkdir(dir, { recursive: true });
831
+ await import_promises6.default.mkdir(dir, { recursive: true });
538
832
  const entry = {
539
833
  name,
540
834
  type: "global",
@@ -551,16 +845,16 @@ async function removeCollection(name) {
551
845
  if (!entry) throw new CollectionNotFoundError(name);
552
846
  await unregisterCollection(name);
553
847
  if (entry.type === "global") {
554
- await import_promises5.default.rm(entry.path, { recursive: true, force: true });
848
+ await import_promises6.default.rm(entry.path, { recursive: true, force: true });
555
849
  }
556
850
  void fireHook("collection-removed", name);
557
851
  return entry;
558
852
  }
559
- var import_promises5;
853
+ var import_promises6;
560
854
  var init_collections = __esm({
561
855
  "src/collections.ts"() {
562
856
  "use strict";
563
- import_promises5 = __toESM(require("fs/promises"), 1);
857
+ import_promises6 = __toESM(require("fs/promises"), 1);
564
858
  init_paths();
565
859
  init_registry();
566
860
  init_errors();
@@ -577,6 +871,7 @@ __export(index_exports, {
577
871
  DiffwikiError: () => DiffwikiError,
578
872
  InvalidExportSelectionError: () => InvalidExportSelectionError,
579
873
  InvalidTargetError: () => InvalidTargetError,
874
+ META_KINDS: () => META_KINDS,
580
875
  NATIVE_ENGINE: () => NATIVE_ENGINE,
581
876
  NoDefaultCollectionError: () => NoDefaultCollectionError,
582
877
  PROJECT_CONFIG_FILE: () => PROJECT_CONFIG_FILE,
@@ -598,10 +893,13 @@ __export(index_exports, {
598
893
  configPath: () => configPath,
599
894
  createArticle: () => createArticle,
600
895
  createCollection: () => createCollection,
896
+ createMetaRecord: () => createMetaRecord,
601
897
  currentBranch: () => currentBranch,
602
898
  deregisterPlugin: () => deregisterPlugin,
603
899
  doctor: () => doctor,
604
900
  emitPluginEvent: () => emitPluginEvent,
901
+ ensureArticleMeta: () => ensureArticleMeta,
902
+ ensureCollectionMeta: () => ensureCollectionMeta,
605
903
  ensureGitRepo: () => ensureGitRepo,
606
904
  ensureHome: () => ensureHome,
607
905
  ensurePluginRoot: () => ensurePluginRoot,
@@ -613,6 +911,7 @@ __export(index_exports, {
613
911
  getConfigValue: () => getConfigValue,
614
912
  getLogger: () => getLogger,
615
913
  gitToplevel: () => gitToplevel,
914
+ initMeta: () => initMeta,
616
915
  initRepoWiki: () => initRepoWiki,
617
916
  installPlugin: () => installPlugin,
618
917
  invalidateLinkGraph: () => invalidateLinkGraph,
@@ -629,11 +928,14 @@ __export(index_exports, {
629
928
  listTemplates: () => listTemplates,
630
929
  loadSearchProvider: () => loadSearchProvider,
631
930
  localGraph: () => localGraph,
931
+ metaDir: () => metaDir,
932
+ mirrorSavedArticle: () => mirrorSavedArticle,
632
933
  nativeSearch: () => nativeSearch,
633
934
  parseAddTarget: () => parseAddTarget,
634
935
  parseArticle: () => parseArticle,
635
936
  parseFrontmatter: () => parseFrontmatter,
636
937
  parsePathTarget: () => parsePathTarget,
938
+ patchArticle: () => patchArticle,
637
939
  pluginsDir: () => pluginsDir,
638
940
  pluginsRegistryPath: () => pluginsRegistryPath,
639
941
  query: () => query,
@@ -649,6 +951,7 @@ __export(index_exports, {
649
951
  registerCollection: () => registerCollection,
650
952
  registerPlugin: () => registerPlugin,
651
953
  registryPath: () => registryPath,
954
+ reindexMeta: () => reindexMeta,
652
955
  removeArticle: () => removeArticle,
653
956
  removeCollection: () => removeCollection,
654
957
  removePlugin: () => removePlugin,
@@ -682,9 +985,11 @@ __export(index_exports, {
682
985
  toTreeNodes: () => toTreeNodes,
683
986
  tokenize: () => tokenize,
684
987
  unregisterCollection: () => unregisterCollection,
988
+ updateArticle: () => updateArticle,
685
989
  updateArticleBody: () => updateArticleBody,
686
990
  upsertPlugin: () => upsertPlugin,
687
991
  upstreamStatus: () => upstreamStatus,
992
+ validateMetaArticlePath: () => validateMetaArticlePath,
688
993
  writeConfig: () => writeConfig,
689
994
  writePluginRegistry: () => writePluginRegistry,
690
995
  writeRegistry: () => writeRegistry
@@ -724,9 +1029,10 @@ init_paths();
724
1029
  // src/frontmatter.ts
725
1030
  var import_gray_matter = __toESM(require("gray-matter"), 1);
726
1031
  var import_yaml = require("yaml");
1032
+ init_errors();
727
1033
  var YAML_ENGINE = {
728
1034
  parse: (input) => {
729
- const data = (0, import_yaml.parse)(input);
1035
+ const data = (0, import_yaml.parse)(input.replace(/\r\n?/g, "\n"));
730
1036
  return data && typeof data === "object" ? data : {};
731
1037
  },
732
1038
  stringify: (data) => (0, import_yaml.stringify)(data)
@@ -770,6 +1076,31 @@ var FRONTMATTER_BLOCK_RE = /^\uFEFF?[ \t]*---\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n
770
1076
  function stripFrontmatter(raw) {
771
1077
  return raw.replace(FRONTMATTER_BLOCK_RE, "");
772
1078
  }
1079
+ function patchArticle(raw, fields, body) {
1080
+ const fence = /^(\uFEFF?[ \t]*---[ \t]*\r?\n)([\s\S]*?)(\r?\n---[ \t]*(?:\r?\n|$))/.exec(raw);
1081
+ if (Object.keys(fields).length === 0) return fence ? fence[0] + (body ?? raw.slice(fence[0].length)) : body ?? raw;
1082
+ const originalYaml = fence?.[2] ?? "";
1083
+ const doc = (0, import_yaml.parseDocument)(originalYaml);
1084
+ if (doc.errors.length)
1085
+ throw new DiffwikiError("Cannot update article with invalid YAML frontmatter. Fix its header first.");
1086
+ const titleNode = doc.get("title", true);
1087
+ if (fence && body === void 0 && Object.keys(fields).length === 1 && typeof fields.title === "string" && !/[\r\n]/.test(fields.title) && (0, import_yaml.isScalar)(titleNode) && titleNode.range) {
1088
+ const [start, end] = titleNode.range;
1089
+ if (!/[\r\n]/.test(originalYaml.slice(start, end))) {
1090
+ const scalar = (0, import_yaml.stringify)(fields.title, { lineWidth: 0 }).trimEnd();
1091
+ return fence[1] + originalYaml.slice(0, start) + scalar + originalYaml.slice(end) + fence[3] + raw.slice(fence[0].length);
1092
+ }
1093
+ }
1094
+ for (const [key, value] of Object.entries(fields)) {
1095
+ if (value === void 0) doc.delete(key);
1096
+ else doc.set(key, value);
1097
+ }
1098
+ const newline = fence?.[1].endsWith("\r\n") ? "\r\n" : "\n";
1099
+ const yaml = doc.toString({ lineWidth: 0 }).replace(/\n$/, "").replace(/\r?\n/g, newline);
1100
+ const content = body ?? (fence ? raw.slice(fence[0].length) : raw);
1101
+ const close = fence?.[3] ?? `${newline}---${newline}`;
1102
+ return (fence?.[1] ?? `---${newline}`) + yaml + close + (body && !close.endsWith("\n") ? newline : "") + content;
1103
+ }
773
1104
  function parseFrontmatter(raw) {
774
1105
  try {
775
1106
  const { data, content } = (0, import_gray_matter.default)(raw, MATTER_OPTS);
@@ -819,28 +1150,40 @@ async function seedTemplates(srcDir) {
819
1150
  return { seeded, skipped, dir };
820
1151
  }
821
1152
  async function listTemplateShapes(srcDir) {
822
- const files = await import_promises2.default.readdir(srcDir);
823
- return files.filter((f) => f.endsWith("-template.md")).map((f) => f.replace(/-template\.md$/, "")).sort();
1153
+ return (await listTemplates(srcDir)).map((info) => info.shape).sort();
824
1154
  }
825
- async function listTemplates(srcDir) {
826
- const files = (await import_promises2.default.readdir(srcDir)).filter((f) => f.endsWith("-template.md")).sort();
1155
+ async function listTemplates(srcDir, category = "article") {
1156
+ const files = /* @__PURE__ */ new Map();
1157
+ for (const dir of [srcDir, templatesDir()]) {
1158
+ let names;
1159
+ try {
1160
+ names = await import_promises2.default.readdir(dir);
1161
+ } catch (err) {
1162
+ if (err.code === "ENOENT") continue;
1163
+ throw err;
1164
+ }
1165
+ for (const name of names.filter((name2) => name2.endsWith("-template.md")).sort())
1166
+ files.set(name, import_node_path2.default.resolve(dir, name));
1167
+ }
827
1168
  const out = [];
828
- for (const file of files) {
1169
+ for (const [file, filePath] of [...files].sort(([a], [b]) => a.localeCompare(b))) {
829
1170
  let data = {};
830
1171
  try {
831
- ({ data } = parseFrontmatter(await import_promises2.default.readFile(import_node_path2.default.join(srcDir, file), "utf8")));
1172
+ ({ data } = parseFrontmatter(await import_promises2.default.readFile(filePath, "utf8")));
832
1173
  } catch {
833
1174
  }
834
1175
  const shape = typeof data.shape === "string" && data.shape ? data.shape : file.replace(/-template\.md$/, "");
835
1176
  const purpose = typeof data.purpose === "string" ? data.purpose : "";
836
1177
  const budget = Array.isArray(data.budget) && data.budget.length === 2 ? [Number(data.budget[0]), Number(data.budget[1])] : void 0;
837
- out.push({ shape, purpose, budget, file });
1178
+ const templateCategory = data.category === "meta" ? "meta" : "article";
1179
+ if (category === "all" || category === templateCategory)
1180
+ out.push({ shape, purpose, budget, file, path: filePath, category: templateCategory });
838
1181
  }
839
1182
  return out;
840
1183
  }
841
1184
 
842
1185
  // src/config.ts
843
- var import_promises6 = __toESM(require("fs/promises"), 1);
1186
+ var import_promises7 = __toESM(require("fs/promises"), 1);
844
1187
  init_paths();
845
1188
  init_collections();
846
1189
  init_errors();
@@ -851,15 +1194,15 @@ function isMissing3(err) {
851
1194
  }
852
1195
  async function readConfig() {
853
1196
  try {
854
- return JSON.parse(await import_promises6.default.readFile(configPath(), "utf8"));
1197
+ return JSON.parse(await import_promises7.default.readFile(configPath(), "utf8"));
855
1198
  } catch (err) {
856
1199
  if (isMissing3(err)) return {};
857
1200
  throw err;
858
1201
  }
859
1202
  }
860
1203
  async function writeConfig(config) {
861
- await import_promises6.default.mkdir(resolveHome(), { recursive: true });
862
- await import_promises6.default.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
1204
+ await import_promises7.default.mkdir(resolveHome(), { recursive: true });
1205
+ await import_promises7.default.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
863
1206
  `, "utf8");
864
1207
  }
865
1208
  async function setConfigValue(key, value) {
@@ -892,6 +1235,7 @@ async function showSystemEnabled() {
892
1235
  }
893
1236
 
894
1237
  // src/index.ts
1238
+ init_meta();
895
1239
  init_registry();
896
1240
 
897
1241
  // src/slugify.ts
@@ -908,9 +1252,10 @@ function slugifyPath(input) {
908
1252
  init_collections();
909
1253
 
910
1254
  // src/articles.ts
911
- var import_promises7 = __toESM(require("fs/promises"), 1);
912
- var import_node_path4 = __toESM(require("path"), 1);
1255
+ var import_promises8 = __toESM(require("fs/promises"), 1);
1256
+ var import_node_path5 = __toESM(require("path"), 1);
913
1257
  init_collections();
1258
+ init_meta();
914
1259
  init_errors();
915
1260
  async function fireHook2(event, collection, relPath) {
916
1261
  try {
@@ -956,63 +1301,53 @@ function ensureMdPath(file) {
956
1301
  async function createArticle(opts) {
957
1302
  const { collection, title } = parseAddTarget(opts.target);
958
1303
  const { name, dir } = await resolveCollectionDir(collection);
959
- const filePath = import_node_path4.default.join(dir, `${slugifyPath(title)}.md`);
1304
+ const filePath = import_node_path5.default.join(dir, `${slugifyPath(title)}.md`);
960
1305
  const body = opts.body ?? `# ${title}
961
1306
  `;
962
- await import_promises7.default.mkdir(import_node_path4.default.dirname(filePath), { recursive: true });
963
- await import_promises7.default.writeFile(
1307
+ await import_promises8.default.mkdir(import_node_path5.default.dirname(filePath), { recursive: true });
1308
+ await import_promises8.default.writeFile(
964
1309
  filePath,
965
1310
  serializeArticle({ title, tags: opts.tags, body, audience: opts.audience, status: opts.status }),
966
1311
  "utf8"
967
1312
  );
968
- void fireHook2("article-created", name, import_node_path4.default.relative(dir, filePath));
1313
+ void fireHook2("article-created", name, import_node_path5.default.relative(dir, filePath));
1314
+ await mirrorSavedArticle({ name, path: dir }, filePath);
969
1315
  return { title, tags: opts.tags, path: filePath, collection: name };
970
1316
  }
971
1317
  async function loadArticle(target) {
972
1318
  const { collection, file } = parsePathTarget(target);
973
1319
  const { dir } = await resolveCollectionDir(collection);
974
- const filePath = import_node_path4.default.join(dir, ensureMdPath(file));
1320
+ const filePath = import_node_path5.default.join(dir, validateMetaArticlePath(ensureMdPath(file)));
975
1321
  try {
976
- return { filePath, dir, raw: await import_promises7.default.readFile(filePath, "utf8"), collection };
1322
+ return { filePath, dir, raw: await import_promises8.default.readFile(filePath, "utf8"), collection };
977
1323
  } catch (err) {
978
1324
  if (err.code === "ENOENT") throw new ArticleNotFoundError(target);
979
1325
  throw err;
980
1326
  }
981
1327
  }
982
- async function updateArticleBody(target, body) {
1328
+ async function updateArticle(target, updates) {
983
1329
  const { filePath, dir, raw, collection } = await loadArticle(target);
984
- const parsed = parseArticle(raw);
985
- await import_promises7.default.writeFile(
986
- filePath,
987
- serializeArticle({
988
- title: parsed.title,
989
- tags: parsed.tags,
990
- audience: parsed.audience,
991
- status: parsed.status,
992
- body
993
- }),
994
- "utf8"
995
- );
996
- void fireHook2("article-updated", collection, import_node_path4.default.relative(dir, filePath));
1330
+ if (updates.title !== void 0 && !updates.title.trim())
1331
+ throw new InvalidTargetError(updates.title, "title cannot be empty");
1332
+ const fields = {};
1333
+ if (updates.title !== void 0) fields.title = updates.title;
1334
+ if (updates.tags !== void 0) fields.tags = Array.from(new Set(updates.tags));
1335
+ if (updates.audience !== void 0) fields.audience = updates.audience === "public" ? void 0 : updates.audience;
1336
+ if (updates.status !== void 0) fields.status = updates.status === "published" ? void 0 : updates.status;
1337
+ const next = patchArticle(raw, fields, updates.body);
1338
+ await import_promises8.default.writeFile(filePath, next, "utf8");
1339
+ const parsed = parseArticle(next);
1340
+ void fireHook2("article-updated", collection, import_node_path5.default.relative(dir, filePath));
1341
+ await mirrorSavedArticle({ name: collection, path: dir }, filePath);
997
1342
  return { title: parsed.title, tags: parsed.tags, path: filePath, collection };
998
1343
  }
1344
+ function updateArticleBody(target, body) {
1345
+ return updateArticle(target, { body });
1346
+ }
999
1347
  async function mutateTags(target, fn) {
1000
- const { filePath, dir, raw, collection } = await loadArticle(target);
1348
+ const { raw } = await loadArticle(target);
1001
1349
  const parsed = parseArticle(raw);
1002
- const tags = fn(parsed.tags);
1003
- await import_promises7.default.writeFile(
1004
- filePath,
1005
- serializeArticle({
1006
- title: parsed.title,
1007
- tags,
1008
- audience: parsed.audience,
1009
- status: parsed.status,
1010
- body: parsed.body
1011
- }),
1012
- "utf8"
1013
- );
1014
- void fireHook2("article-updated", collection, import_node_path4.default.relative(dir, filePath));
1015
- return { title: parsed.title, tags, path: filePath, collection };
1350
+ return updateArticle(target, { tags: fn(parsed.tags) });
1016
1351
  }
1017
1352
  function addTags(target, tags) {
1018
1353
  return mutateTags(target, (current) => Array.from(/* @__PURE__ */ new Set([...current, ...tags])));
@@ -1025,37 +1360,27 @@ function setTags(target, tags) {
1025
1360
  return mutateTags(target, () => Array.from(new Set(tags)));
1026
1361
  }
1027
1362
  async function setArticleMeta(target, meta) {
1028
- const { filePath, dir, raw, collection } = await loadArticle(target);
1029
- const parsed = parseArticle(raw);
1030
- const audience = meta.audience ?? parsed.audience;
1031
- const status = meta.status ?? parsed.status;
1032
- await import_promises7.default.writeFile(
1033
- filePath,
1034
- serializeArticle({ title: parsed.title, tags: parsed.tags, audience, status, body: parsed.body }),
1035
- "utf8"
1036
- );
1037
- void fireHook2("article-updated", collection, import_node_path4.default.relative(dir, filePath));
1038
- return { title: parsed.title, tags: parsed.tags, path: filePath, collection };
1363
+ return updateArticle(target, meta);
1039
1364
  }
1040
1365
  async function removeArticle(target) {
1041
1366
  const { filePath, dir, collection } = await loadArticle(target);
1042
- await import_promises7.default.rm(filePath);
1043
- void fireHook2("article-removed", collection, import_node_path4.default.relative(dir, filePath));
1367
+ await import_promises8.default.rm(filePath);
1368
+ void fireHook2("article-removed", collection, import_node_path5.default.relative(dir, filePath));
1044
1369
  return filePath;
1045
1370
  }
1046
1371
 
1047
1372
  // src/repo.ts
1048
- var import_promises9 = __toESM(require("fs/promises"), 1);
1049
- var import_node_path6 = __toESM(require("path"), 1);
1373
+ var import_promises10 = __toESM(require("fs/promises"), 1);
1374
+ var import_node_path7 = __toESM(require("path"), 1);
1050
1375
  var import_node_child_process3 = require("child_process");
1051
1376
  var import_node_util2 = require("util");
1052
- var import_yaml2 = require("yaml");
1377
+ var import_yaml3 = require("yaml");
1053
1378
  init_collections();
1054
1379
  init_registry();
1055
1380
 
1056
1381
  // src/git.ts
1057
- var import_promises8 = __toESM(require("fs/promises"), 1);
1058
- var import_node_path5 = __toESM(require("path"), 1);
1382
+ var import_promises9 = __toESM(require("fs/promises"), 1);
1383
+ var import_node_path6 = __toESM(require("path"), 1);
1059
1384
  var import_node_child_process2 = require("child_process");
1060
1385
  var import_node_util = require("util");
1061
1386
  init_logger();
@@ -1081,8 +1406,8 @@ async function isLinkedWorktree(cwd) {
1081
1406
  const gitDir = await git(cwd, ["rev-parse", "--absolute-git-dir"]);
1082
1407
  const commonDir = await git(cwd, ["rev-parse", "--git-common-dir"]);
1083
1408
  if (!gitDir || !commonDir) return false;
1084
- const commonAbs = import_node_path5.default.isAbsolute(commonDir) ? commonDir : import_node_path5.default.resolve(cwd, commonDir);
1085
- return import_node_path5.default.resolve(gitDir) !== import_node_path5.default.resolve(commonAbs);
1409
+ const commonAbs = import_node_path6.default.isAbsolute(commonDir) ? commonDir : import_node_path6.default.resolve(cwd, commonDir);
1410
+ return import_node_path6.default.resolve(gitDir) !== import_node_path6.default.resolve(commonAbs);
1086
1411
  }
1087
1412
  async function repoRemoteName(cwd) {
1088
1413
  const url = await git(cwd, ["remote", "get-url", "origin"]);
@@ -1124,8 +1449,8 @@ var timestampCache = /* @__PURE__ */ new Map();
1124
1449
  async function fileTimestamps(absPath) {
1125
1450
  const cached2 = timestampCache.get(absPath);
1126
1451
  if (cached2) return cached2;
1127
- const dir = import_node_path5.default.dirname(absPath);
1128
- const base = import_node_path5.default.basename(absPath);
1452
+ const dir = import_node_path6.default.dirname(absPath);
1453
+ const base = import_node_path6.default.basename(absPath);
1129
1454
  const [addLog, lastLog] = await Promise.all([
1130
1455
  git(dir, ["log", "--follow", "--diff-filter=A", "--format=%aI", "--", base]),
1131
1456
  git(dir, ["log", "-1", "--format=%aI", "--", base])
@@ -1136,7 +1461,7 @@ async function fileTimestamps(absPath) {
1136
1461
  let updated = gitUpdated;
1137
1462
  if (!created || !updated) {
1138
1463
  try {
1139
- const st = await import_promises8.default.stat(absPath);
1464
+ const st = await import_promises9.default.stat(absPath);
1140
1465
  if (!updated) updated = st.mtime.toISOString();
1141
1466
  if (!created) created = isEpochish(st.birthtime) ? st.mtime.toISOString() : st.birthtime.toISOString();
1142
1467
  } catch {
@@ -1154,18 +1479,18 @@ var REPO_CONFIG_FILE = "diffwiki.yaml";
1154
1479
  async function detectRepoName(cwd) {
1155
1480
  try {
1156
1481
  const { stdout } = await execFileAsync2("git", ["rev-parse", "--show-toplevel"], { cwd });
1157
- return import_node_path6.default.basename(stdout.trim());
1482
+ return import_node_path7.default.basename(stdout.trim());
1158
1483
  } catch {
1159
- return import_node_path6.default.basename(import_node_path6.default.resolve(cwd));
1484
+ return import_node_path7.default.basename(import_node_path7.default.resolve(cwd));
1160
1485
  }
1161
1486
  }
1162
1487
  async function readRepoConfig(cwd) {
1163
1488
  try {
1164
- const raw = await import_promises9.default.readFile(import_node_path6.default.join(cwd, REPO_CONFIG_FILE), "utf8");
1165
- const parsed = (0, import_yaml2.parse)(raw) ?? {};
1489
+ const raw = await import_promises10.default.readFile(import_node_path7.default.join(cwd, REPO_CONFIG_FILE), "utf8");
1490
+ const parsed = (0, import_yaml3.parse)(raw) ?? {};
1166
1491
  if (!parsed.path) return void 0;
1167
1492
  return {
1168
- collection: parsed.collection ?? import_node_path6.default.basename(import_node_path6.default.resolve(cwd)),
1493
+ collection: parsed.collection ?? import_node_path7.default.basename(import_node_path7.default.resolve(cwd)),
1169
1494
  path: parsed.path
1170
1495
  };
1171
1496
  } catch (err) {
@@ -1174,9 +1499,9 @@ async function readRepoConfig(cwd) {
1174
1499
  }
1175
1500
  }
1176
1501
  async function initRepoWiki(opts) {
1177
- const cwd = import_node_path6.default.resolve(opts.cwd);
1502
+ const cwd = import_node_path7.default.resolve(opts.cwd);
1178
1503
  const wikiPath = opts.wikiPath ?? "wiki";
1179
- const absWiki = import_node_path6.default.join(cwd, wikiPath);
1504
+ const absWiki = import_node_path7.default.join(cwd, wikiPath);
1180
1505
  let name = opts.collection ?? await detectRepoName(cwd);
1181
1506
  const existing = await findCollection(name);
1182
1507
  if (existing && existing.path !== absWiki) {
@@ -1191,9 +1516,9 @@ async function initRepoWiki(opts) {
1191
1516
  n += 1;
1192
1517
  }
1193
1518
  }
1194
- await import_promises9.default.mkdir(absWiki, { recursive: true });
1519
+ await import_promises10.default.mkdir(absWiki, { recursive: true });
1195
1520
  const config = { collection: name, path: wikiPath };
1196
- await import_promises9.default.writeFile(import_node_path6.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml2.stringify)(config), "utf8");
1521
+ await import_promises10.default.writeFile(import_node_path7.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml3.stringify)(config), "utf8");
1197
1522
  const entry = {
1198
1523
  name,
1199
1524
  type: "repo",
@@ -1204,15 +1529,15 @@ async function initRepoWiki(opts) {
1204
1529
  return entry;
1205
1530
  }
1206
1531
  async function addExternalCollection(opts) {
1207
- const cwd = import_node_path6.default.resolve(opts.cwd);
1532
+ const cwd = import_node_path7.default.resolve(opts.cwd);
1208
1533
  const root = await ensureGitRepo(cwd);
1209
1534
  if (await isLinkedWorktree(cwd)) throw new WorktreeNotAllowedError(root);
1210
1535
  const id = await repoId(root) ?? `path:${root}`;
1211
1536
  const already = await findCollectionById(id);
1212
1537
  if (already) throw new CollectionAlreadyAddedError(already.name);
1213
1538
  const docsRel = opts.docsDir ?? "docs";
1214
- const contentDir = import_node_path6.default.join(root, docsRel);
1215
- let name = await repoRemoteName(root) ?? import_node_path6.default.basename(root);
1539
+ const contentDir = import_node_path7.default.join(root, docsRel);
1540
+ let name = await repoRemoteName(root) ?? import_node_path7.default.basename(root);
1216
1541
  if (await findCollection(name)) {
1217
1542
  let n = 2;
1218
1543
  while (await findCollection(`${name}-${n}`)) n += 1;
@@ -1231,21 +1556,21 @@ async function addExternalCollection(opts) {
1231
1556
  return entry;
1232
1557
  }
1233
1558
  async function resolveCwdCollection(cwd) {
1234
- const abs = import_node_path6.default.resolve(cwd);
1559
+ const abs = import_node_path7.default.resolve(cwd);
1235
1560
  const root = await gitToplevel(abs);
1236
1561
  if (root) {
1237
1562
  const id = await repoId(root) ?? `path:${root}`;
1238
1563
  const byId = await findCollectionById(id);
1239
1564
  if (byId) return byId;
1240
1565
  }
1241
- const within = (base) => abs === base || abs.startsWith(base + import_node_path6.default.sep);
1566
+ const within2 = (base) => abs === base || abs.startsWith(base + import_node_path7.default.sep);
1242
1567
  const all = await listCollections();
1243
1568
  let best;
1244
1569
  let bestLen = -1;
1245
1570
  for (const c of all) {
1246
1571
  const bases = [c.path, c.repoRoot].filter((b) => b !== void 0);
1247
1572
  for (const b of bases) {
1248
- if (within(b) && b.length > bestLen) {
1573
+ if (within2(b) && b.length > bestLen) {
1249
1574
  best = c;
1250
1575
  bestLen = b.length;
1251
1576
  }
@@ -1262,10 +1587,10 @@ async function collectionGitStatus(entry) {
1262
1587
  }
1263
1588
 
1264
1589
  // src/project.ts
1265
- var import_promises10 = __toESM(require("fs/promises"), 1);
1590
+ var import_promises11 = __toESM(require("fs/promises"), 1);
1266
1591
  var import_node_os2 = __toESM(require("os"), 1);
1267
- var import_node_path7 = __toESM(require("path"), 1);
1268
- var import_yaml3 = require("yaml");
1592
+ var import_node_path8 = __toESM(require("path"), 1);
1593
+ var import_yaml4 = require("yaml");
1269
1594
  var PROJECT_CONFIG_FILE = "diffwiki.yaml";
1270
1595
  function normaliseSite(raw) {
1271
1596
  if (!raw || typeof raw !== "object") return void 0;
@@ -1319,12 +1644,12 @@ function normaliseContext(raw) {
1319
1644
  async function readProjectConfig(dir) {
1320
1645
  let raw;
1321
1646
  try {
1322
- raw = await import_promises10.default.readFile(import_node_path7.default.join(dir, PROJECT_CONFIG_FILE), "utf8");
1647
+ raw = await import_promises11.default.readFile(import_node_path8.default.join(dir, PROJECT_CONFIG_FILE), "utf8");
1323
1648
  } catch (err) {
1324
1649
  if (err.code === "ENOENT") return void 0;
1325
1650
  throw err;
1326
1651
  }
1327
- const parsed = (0, import_yaml3.parse)(raw) ?? {};
1652
+ const parsed = (0, import_yaml4.parse)(raw) ?? {};
1328
1653
  const collections = [];
1329
1654
  if (Array.isArray(parsed.collections)) {
1330
1655
  for (const c of parsed.collections) {
@@ -1340,7 +1665,7 @@ async function readProjectConfig(dir) {
1340
1665
  }
1341
1666
  collections.sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
1342
1667
  } else if (typeof parsed.path === "string") {
1343
- const name = typeof parsed.collection === "string" ? parsed.collection : import_node_path7.default.basename(import_node_path7.default.resolve(dir));
1668
+ const name = typeof parsed.collection === "string" ? parsed.collection : import_node_path8.default.basename(import_node_path8.default.resolve(dir));
1344
1669
  collections.push({ name, path: parsed.path });
1345
1670
  }
1346
1671
  const site = normaliseSite(parsed.site);
@@ -1351,27 +1676,27 @@ async function readContextCollections(dir) {
1351
1676
  return (await readProjectConfig(dir))?.context?.collections ?? [];
1352
1677
  }
1353
1678
  async function setContextCollections(dir, collections) {
1354
- const file = import_node_path7.default.join(dir, PROJECT_CONFIG_FILE);
1679
+ const file = import_node_path8.default.join(dir, PROJECT_CONFIG_FILE);
1355
1680
  let raw = "";
1356
1681
  try {
1357
- raw = await import_promises10.default.readFile(file, "utf8");
1682
+ raw = await import_promises11.default.readFile(file, "utf8");
1358
1683
  } catch (err) {
1359
1684
  if (err.code !== "ENOENT") throw err;
1360
1685
  }
1361
- const doc = (0, import_yaml3.parseDocument)(raw);
1686
+ const doc = (0, import_yaml4.parseDocument)(raw);
1362
1687
  doc.setIn(["context", "collections"], collections);
1363
- await import_promises10.default.writeFile(file, doc.toString(), "utf8");
1688
+ await import_promises11.default.writeFile(file, doc.toString(), "utf8");
1364
1689
  return file;
1365
1690
  }
1366
1691
  async function resolveProjectCollections(dir) {
1367
1692
  const config = await readProjectConfig(dir);
1368
1693
  if (!config) return [];
1369
- const root = import_node_path7.default.resolve(dir);
1694
+ const root = import_node_path8.default.resolve(dir);
1370
1695
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
1371
1696
  return config.collections.map((c) => ({
1372
1697
  name: c.name,
1373
1698
  type: "repo",
1374
- path: import_node_path7.default.resolve(root, c.path),
1699
+ path: import_node_path8.default.resolve(root, c.path),
1375
1700
  createdAt
1376
1701
  }));
1377
1702
  }
@@ -1381,11 +1706,11 @@ async function readSiteConfig(dir) {
1381
1706
  async function seedProjectHome(dir) {
1382
1707
  const entries = await resolveProjectCollections(dir);
1383
1708
  if (entries.length === 0) {
1384
- throw new Error(`No collections to render from diffwiki.yaml in ${import_node_path7.default.resolve(dir)}`);
1709
+ throw new Error(`No collections to render from diffwiki.yaml in ${import_node_path8.default.resolve(dir)}`);
1385
1710
  }
1386
- const home = await import_promises10.default.mkdtemp(import_node_path7.default.join(import_node_os2.default.tmpdir(), "diffwiki-project-"));
1387
- await import_promises10.default.writeFile(
1388
- import_node_path7.default.join(home, "registry.json"),
1711
+ const home = await import_promises11.default.mkdtemp(import_node_path8.default.join(import_node_os2.default.tmpdir(), "diffwiki-project-"));
1712
+ await import_promises11.default.writeFile(
1713
+ import_node_path8.default.join(home, "registry.json"),
1389
1714
  `${JSON.stringify({ version: 1, collections: entries }, null, 2)}
1390
1715
  `,
1391
1716
  "utf8"
@@ -1397,8 +1722,8 @@ async function seedProjectHome(dir) {
1397
1722
  init_collections();
1398
1723
 
1399
1724
  // src/browse.ts
1400
- var import_promises11 = __toESM(require("fs/promises"), 1);
1401
- var import_node_path8 = __toESM(require("path"), 1);
1725
+ var import_promises12 = __toESM(require("fs/promises"), 1);
1726
+ var import_node_path9 = __toESM(require("path"), 1);
1402
1727
  init_collections();
1403
1728
  init_errors();
1404
1729
  var INDEX_RE = /^(index|readme)\.mdx?$/i;
@@ -1444,7 +1769,7 @@ function buildArticleTree(relPaths) {
1444
1769
  async function collectRelPaths(dir, rel = "") {
1445
1770
  let dirents;
1446
1771
  try {
1447
- dirents = await import_promises11.default.readdir(dir, { withFileTypes: true });
1772
+ dirents = await import_promises12.default.readdir(dir, { withFileTypes: true });
1448
1773
  } catch {
1449
1774
  return [];
1450
1775
  }
@@ -1453,7 +1778,7 @@ async function collectRelPaths(dir, rel = "") {
1453
1778
  if (d.name.startsWith(".")) continue;
1454
1779
  const childRel = rel ? `${rel}/${d.name}` : d.name;
1455
1780
  if (d.isDirectory()) {
1456
- out.push(...await collectRelPaths(import_node_path8.default.join(dir, d.name), childRel));
1781
+ out.push(...await collectRelPaths(import_node_path9.default.join(dir, d.name), childRel));
1457
1782
  } else if (/\.(mdx?)$/.test(d.name)) {
1458
1783
  out.push(childRel);
1459
1784
  }
@@ -1462,7 +1787,7 @@ async function collectRelPaths(dir, rel = "") {
1462
1787
  }
1463
1788
  async function readdirSafe(dir) {
1464
1789
  try {
1465
- return await import_promises11.default.readdir(dir);
1790
+ return await import_promises12.default.readdir(dir);
1466
1791
  } catch {
1467
1792
  return [];
1468
1793
  }
@@ -1480,7 +1805,7 @@ var CHEAP_META_BYTES = 8192;
1480
1805
  async function cheapMeta(absPath) {
1481
1806
  let handle;
1482
1807
  try {
1483
- handle = await import_promises11.default.open(absPath, "r");
1808
+ handle = await import_promises12.default.open(absPath, "r");
1484
1809
  const buf = Buffer.alloc(CHEAP_META_BYTES);
1485
1810
  const { bytesRead } = await handle.read(buf, 0, CHEAP_META_BYTES, 0);
1486
1811
  const snippet = buf.subarray(0, bytesRead).toString("utf8");
@@ -1514,10 +1839,10 @@ async function enrichTitles(nodes, collDir) {
1514
1839
  let status = node.status;
1515
1840
  let order = node.order;
1516
1841
  if (node.slug) {
1517
- const dir = import_node_path8.default.join(collDir, node.slug);
1842
+ const dir = import_node_path9.default.join(collDir, node.slug);
1518
1843
  const idx = pickIndexFile(await readdirSafe(dir));
1519
1844
  if (idx) {
1520
- const meta = await cheapMeta(import_node_path8.default.join(dir, idx));
1845
+ const meta = await cheapMeta(import_node_path9.default.join(dir, idx));
1521
1846
  title = meta.title ?? title;
1522
1847
  audience = meta.audience ?? audience;
1523
1848
  status = meta.status ?? status;
@@ -1527,7 +1852,7 @@ async function enrichTitles(nodes, collDir) {
1527
1852
  return { ...node, title, audience, status, order, children };
1528
1853
  }
1529
1854
  for (const ext of [".mdx", ".md"]) {
1530
- const meta = await cheapMeta(import_node_path8.default.join(collDir, `${node.slug}${ext}`));
1855
+ const meta = await cheapMeta(import_node_path9.default.join(collDir, `${node.slug}${ext}`));
1531
1856
  if (meta.title || meta.audience || meta.status || meta.order != null) {
1532
1857
  return {
1533
1858
  ...node,
@@ -1546,49 +1871,49 @@ async function enrichTitles(nodes, collDir) {
1546
1871
  async function collectionMeta(collDir) {
1547
1872
  const idx = pickIndexFile(await readdirSafe(collDir));
1548
1873
  if (!idx) return {};
1549
- const meta = await cheapMeta(import_node_path8.default.join(collDir, idx));
1874
+ const meta = await cheapMeta(import_node_path9.default.join(collDir, idx));
1550
1875
  return { title: meta.title, audience: meta.audience, status: meta.status };
1551
1876
  }
1552
1877
  async function listArticleTree(coll) {
1553
1878
  const entry = await findCollection(coll);
1554
1879
  if (!entry) throw new CollectionNotFoundError(coll);
1555
1880
  const rels = await collectRelPaths(entry.path);
1556
- return enrichTitles(buildArticleTree(rels), import_node_path8.default.resolve(entry.path));
1881
+ return enrichTitles(buildArticleTree(rels), import_node_path9.default.resolve(entry.path));
1557
1882
  }
1558
1883
  async function readArticle(coll, slug) {
1559
1884
  const entry = await findCollection(coll);
1560
1885
  if (!entry) throw new CollectionNotFoundError(coll);
1561
- const collDir = import_node_path8.default.resolve(entry.path);
1886
+ const collDir = import_node_path9.default.resolve(entry.path);
1562
1887
  const normalized = slug.replace(/\\/g, "/");
1563
1888
  if (normalized.split("/").some((s) => s === "..")) {
1564
1889
  throw new InvalidTargetError(slug, "path traversal detected");
1565
1890
  }
1566
1891
  const safeSlug = normalized.split("/").filter((s) => s !== "" && s !== ".").join("/");
1567
- const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path8.default.sep);
1892
+ const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path9.default.sep);
1568
1893
  let abs;
1569
1894
  let isIndex = false;
1570
1895
  for (const rel of safeSlug ? [`${safeSlug}.mdx`, `${safeSlug}.md`] : []) {
1571
- const candidate = import_node_path8.default.resolve(collDir, rel);
1896
+ const candidate = import_node_path9.default.resolve(collDir, rel);
1572
1897
  if (!inJail(candidate)) throw new InvalidTargetError(slug, "path traversal detected");
1573
1898
  try {
1574
- await import_promises11.default.access(candidate);
1899
+ await import_promises12.default.access(candidate);
1575
1900
  abs = candidate;
1576
1901
  break;
1577
1902
  } catch {
1578
1903
  }
1579
1904
  }
1580
1905
  if (!abs) {
1581
- const dir = import_node_path8.default.resolve(collDir, safeSlug);
1906
+ const dir = import_node_path9.default.resolve(collDir, safeSlug);
1582
1907
  if (inJail(dir)) {
1583
1908
  const idx = pickIndexFile(await readdirSafe(dir));
1584
1909
  if (idx) {
1585
- abs = import_node_path8.default.join(dir, idx);
1910
+ abs = import_node_path9.default.join(dir, idx);
1586
1911
  isIndex = true;
1587
1912
  }
1588
1913
  }
1589
1914
  }
1590
1915
  if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
1591
- const parsed = parseArticle(await import_promises11.default.readFile(abs, "utf8"));
1916
+ const parsed = parseArticle(await import_promises12.default.readFile(abs, "utf8"));
1592
1917
  const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
1593
1918
  return {
1594
1919
  coll,
@@ -1604,7 +1929,7 @@ async function readArticle(coll, slug) {
1604
1929
  }
1605
1930
 
1606
1931
  // src/render.ts
1607
- var import_node_path9 = __toESM(require("path"), 1);
1932
+ var import_node_path10 = __toESM(require("path"), 1);
1608
1933
  var import_unified = require("unified");
1609
1934
  var import_remark_parse = __toESM(require("remark-parse"), 1);
1610
1935
  var import_remark_gfm = __toESM(require("remark-gfm"), 1);
@@ -1621,7 +1946,7 @@ function isNonRelativeHref(href) {
1621
1946
  return /^[a-z][a-z0-9+.-]*:/i.test(href);
1622
1947
  }
1623
1948
  function rehypeRewriteLinks(ctx) {
1624
- const slugDir = ctx.isIndex ? ctx.slug || "." : import_node_path9.default.posix.dirname(ctx.slug || ".");
1949
+ const slugDir = ctx.isIndex ? ctx.slug || "." : import_node_path10.default.posix.dirname(ctx.slug || ".");
1625
1950
  return (tree) => {
1626
1951
  (0, import_unist_util_visit.visit)(tree, "element", (node) => {
1627
1952
  if (node.tagName !== "a") return;
@@ -1631,8 +1956,8 @@ function rehypeRewriteLinks(ctx) {
1631
1956
  const suffix = suffixMatch ? suffixMatch[0] : "";
1632
1957
  const pathPart = suffix ? raw.slice(0, raw.length - suffix.length) : raw;
1633
1958
  if (pathPart === "") return;
1634
- const joined = import_node_path9.default.posix.join(slugDir === "." ? "" : slugDir, pathPart);
1635
- const normalized = import_node_path9.default.posix.normalize(joined);
1959
+ const joined = import_node_path10.default.posix.join(slugDir === "." ? "" : slugDir, pathPart);
1960
+ const normalized = import_node_path10.default.posix.normalize(joined);
1636
1961
  const toRoute = (p) => p.replace(/\.(mdx?)$/i, "").replace(/(^|\/)(index|readme)$/i, "$1").replace(/\/+$/, "").replace(/^\/+/, "");
1637
1962
  if (normalized.startsWith("../")) {
1638
1963
  const sibling = normalized.slice(3);
@@ -2035,8 +2360,8 @@ function resolveCollectionSelection(all, sel = {}) {
2035
2360
  }
2036
2361
 
2037
2362
  // src/query.ts
2038
- var import_promises12 = __toESM(require("fs/promises"), 1);
2039
- var import_node_path10 = __toESM(require("path"), 1);
2363
+ var import_promises13 = __toESM(require("fs/promises"), 1);
2364
+ var import_node_path11 = __toESM(require("path"), 1);
2040
2365
  init_collections();
2041
2366
  init_errors();
2042
2367
  init_logger();
@@ -2111,13 +2436,13 @@ var log2 = getLogger("query");
2111
2436
  async function collectMarkdownFiles(dir) {
2112
2437
  let entries;
2113
2438
  try {
2114
- entries = await import_promises12.default.readdir(dir, { withFileTypes: true });
2439
+ entries = await import_promises13.default.readdir(dir, { withFileTypes: true });
2115
2440
  } catch {
2116
2441
  return [];
2117
2442
  }
2118
2443
  const files = [];
2119
2444
  for (const entry of entries) {
2120
- const full = import_node_path10.default.join(dir, entry.name);
2445
+ const full = import_node_path11.default.join(dir, entry.name);
2121
2446
  if (entry.isDirectory()) {
2122
2447
  files.push(...await collectMarkdownFiles(full));
2123
2448
  } else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
@@ -2144,12 +2469,12 @@ async function nativeSearch(term, collections, collection, hideSystem = false) {
2144
2469
  const docs = [];
2145
2470
  for (const entry of targets) {
2146
2471
  for (const full of await collectMarkdownFiles(entry.path)) {
2147
- const fallbackTitle = import_node_path10.default.basename(full).replace(/\.mdx?$/, "");
2472
+ const fallbackTitle = import_node_path11.default.basename(full).replace(/\.mdx?$/, "");
2148
2473
  let title = fallbackTitle;
2149
2474
  let tags = [];
2150
2475
  let body = "";
2151
2476
  try {
2152
- const parsed = parseArticle(await import_promises12.default.readFile(full, "utf8"));
2477
+ const parsed = parseArticle(await import_promises13.default.readFile(full, "utf8"));
2153
2478
  if (hideSystem && parsed.audience === "system") continue;
2154
2479
  if (parsed.title) title = parsed.title;
2155
2480
  tags = parsed.tags;
@@ -2207,7 +2532,7 @@ async function dropSystemHits(hits) {
2207
2532
  const kept = [];
2208
2533
  for (const hit of hits) {
2209
2534
  try {
2210
- if (parseArticle(await import_promises12.default.readFile(hit.path, "utf8")).audience === "system") continue;
2535
+ if (parseArticle(await import_promises13.default.readFile(hit.path, "utf8")).audience === "system") continue;
2211
2536
  } catch {
2212
2537
  }
2213
2538
  kept.push(hit);
@@ -2255,14 +2580,14 @@ async function query(term, opts) {
2255
2580
  }
2256
2581
 
2257
2582
  // src/search.ts
2258
- var import_promises13 = __toESM(require("fs/promises"), 1);
2259
- var import_node_path11 = __toESM(require("path"), 1);
2583
+ var import_promises14 = __toESM(require("fs/promises"), 1);
2584
+ var import_node_path12 = __toESM(require("path"), 1);
2260
2585
  init_collections();
2261
2586
  var isStatic2 = () => process.env.DIFFWIKI_STATIC === "1";
2262
2587
  async function readArticleParts(collDir, slug) {
2263
2588
  for (const ext of [".mdx", ".md"]) {
2264
2589
  try {
2265
- const parsed = parseArticle(await import_promises13.default.readFile(import_node_path11.default.join(collDir, `${slug}${ext}`), "utf8"));
2590
+ const parsed = parseArticle(await import_promises14.default.readFile(import_node_path12.default.join(collDir, `${slug}${ext}`), "utf8"));
2266
2591
  return { tags: parsed.tags, body: parsed.body };
2267
2592
  } catch {
2268
2593
  }
@@ -2298,7 +2623,7 @@ async function buildSearchIndex(collection) {
2298
2623
  for (const e of entries) {
2299
2624
  try {
2300
2625
  const collEntry = await findCollection(e.name);
2301
- const collDir = import_node_path11.default.resolve(collEntry?.path ?? e.path);
2626
+ const collDir = import_node_path12.default.resolve(collEntry?.path ?? e.path);
2302
2627
  if (collEntry?.audience === "system") continue;
2303
2628
  if (isStatic2()) {
2304
2629
  const cm = await collectionMeta(collDir);
@@ -2312,15 +2637,15 @@ async function buildSearchIndex(collection) {
2312
2637
  }
2313
2638
 
2314
2639
  // src/doctor.ts
2315
- var import_promises15 = __toESM(require("fs/promises"), 1);
2640
+ var import_promises16 = __toESM(require("fs/promises"), 1);
2316
2641
  init_paths();
2317
2642
  init_registry();
2318
2643
  init_collections();
2319
2644
 
2320
2645
  // src/plugins/manager.ts
2321
- var import_promises14 = __toESM(require("fs/promises"), 1);
2646
+ var import_promises15 = __toESM(require("fs/promises"), 1);
2322
2647
  var import_node_os3 = __toESM(require("os"), 1);
2323
- var import_node_path13 = __toESM(require("path"), 1);
2648
+ var import_node_path14 = __toESM(require("path"), 1);
2324
2649
  init_paths();
2325
2650
  init_errors();
2326
2651
  init_collections();
@@ -2331,7 +2656,7 @@ init_host();
2331
2656
  var import_node_child_process4 = require("child_process");
2332
2657
  var import_node_util3 = require("util");
2333
2658
  var import_node_fs = require("fs");
2334
- var import_node_path12 = require("path");
2659
+ var import_node_path13 = require("path");
2335
2660
  var import_node_url = require("url");
2336
2661
  var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process4.execFile);
2337
2662
  async function runProcess(file, args, opts) {
@@ -2347,7 +2672,7 @@ async function runProcess(file, args, opts) {
2347
2672
  // src/plugins/manager.ts
2348
2673
  async function readPackageJson(file) {
2349
2674
  try {
2350
- return JSON.parse(await import_promises14.default.readFile(file, "utf8"));
2675
+ return JSON.parse(await import_promises15.default.readFile(file, "utf8"));
2351
2676
  } catch {
2352
2677
  return null;
2353
2678
  }
@@ -2363,7 +2688,7 @@ function binOf(manifest) {
2363
2688
  async function pluginManifestDirs(nodeModules) {
2364
2689
  let entries;
2365
2690
  try {
2366
- entries = await import_promises14.default.readdir(nodeModules);
2691
+ entries = await import_promises15.default.readdir(nodeModules);
2367
2692
  } catch {
2368
2693
  return [];
2369
2694
  }
@@ -2373,44 +2698,44 @@ async function pluginManifestDirs(nodeModules) {
2373
2698
  if (entry.startsWith("@")) {
2374
2699
  let scoped;
2375
2700
  try {
2376
- scoped = await import_promises14.default.readdir(import_node_path13.default.join(nodeModules, entry));
2701
+ scoped = await import_promises15.default.readdir(import_node_path14.default.join(nodeModules, entry));
2377
2702
  } catch {
2378
2703
  continue;
2379
2704
  }
2380
2705
  for (const pkg of scoped) {
2381
2706
  if (pkg.startsWith(".")) continue;
2382
- dirs.push(import_node_path13.default.join(nodeModules, entry, pkg));
2707
+ dirs.push(import_node_path14.default.join(nodeModules, entry, pkg));
2383
2708
  }
2384
2709
  } else {
2385
- dirs.push(import_node_path13.default.join(nodeModules, entry));
2710
+ dirs.push(import_node_path14.default.join(nodeModules, entry));
2386
2711
  }
2387
2712
  }
2388
2713
  return dirs;
2389
2714
  }
2390
2715
  async function ensurePluginRoot() {
2391
2716
  const dir = pluginsDir();
2392
- await import_promises14.default.mkdir(dir, { recursive: true });
2393
- const pkgPath = import_node_path13.default.join(dir, "package.json");
2717
+ await import_promises15.default.mkdir(dir, { recursive: true });
2718
+ const pkgPath = import_node_path14.default.join(dir, "package.json");
2394
2719
  if (await readPackageJson(pkgPath) === null) {
2395
2720
  const pkg = { name: "diffwiki-plugins", private: true, version: "0.0.0" };
2396
- await import_promises14.default.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
2721
+ await import_promises15.default.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
2397
2722
  `, "utf8");
2398
2723
  }
2399
2724
  return dir;
2400
2725
  }
2401
2726
  async function asSearchPackage(pkgDir) {
2402
- const manifest = await readPackageJson(import_node_path13.default.join(pkgDir, "package.json"));
2727
+ const manifest = await readPackageJson(import_node_path14.default.join(pkgDir, "package.json"));
2403
2728
  if (!manifest?.name || manifest.diffwiki?.kind !== "search") return null;
2404
2729
  const bin = binOf(manifest);
2405
2730
  if (!bin) return null;
2406
- return { name: manifest.name, version: manifest.version, binAbs: import_node_path13.default.resolve(pkgDir, bin) };
2731
+ return { name: manifest.name, version: manifest.version, binAbs: import_node_path14.default.resolve(pkgDir, bin) };
2407
2732
  }
2408
2733
  async function packageNameFromSpec(spec) {
2409
2734
  const looksLikePath = spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~") || spec.startsWith("file:");
2410
2735
  if (looksLikePath) {
2411
2736
  const raw = spec.startsWith("file:") ? spec.slice("file:".length) : spec;
2412
- const abs = import_node_path13.default.resolve(raw.replace(/^~(?=$|\/)/, import_node_os3.default.homedir()));
2413
- return (await readPackageJson(import_node_path13.default.join(abs, "package.json")))?.name ?? null;
2737
+ const abs = import_node_path14.default.resolve(raw.replace(/^~(?=$|\/)/, import_node_os3.default.homedir()));
2738
+ return (await readPackageJson(import_node_path14.default.join(abs, "package.json")))?.name ?? null;
2414
2739
  }
2415
2740
  if (/^(git\+|git:|https?:|ssh:)/.test(spec)) return null;
2416
2741
  if (spec.startsWith("@")) {
@@ -2421,10 +2746,10 @@ async function packageNameFromSpec(spec) {
2421
2746
  return at <= 0 ? spec : spec.slice(0, at);
2422
2747
  }
2423
2748
  async function resolveSearchPackage(dir, name) {
2424
- return asSearchPackage(import_node_path13.default.join(dir, "node_modules", ...name.split("/")));
2749
+ return asSearchPackage(import_node_path14.default.join(dir, "node_modules", ...name.split("/")));
2425
2750
  }
2426
2751
  async function discoverSearchPackage(dir) {
2427
- const nodeModules = import_node_path13.default.join(dir, "node_modules");
2752
+ const nodeModules = import_node_path14.default.join(dir, "node_modules");
2428
2753
  for (const pkgDir of await pluginManifestDirs(nodeModules)) {
2429
2754
  const found = await asSearchPackage(pkgDir);
2430
2755
  if (found) return found;
@@ -2564,7 +2889,7 @@ init_host();
2564
2889
  init_types();
2565
2890
  async function exists(target) {
2566
2891
  try {
2567
- await import_promises15.default.access(target);
2892
+ await import_promises16.default.access(target);
2568
2893
  return true;
2569
2894
  } catch {
2570
2895
  return false;
@@ -2670,8 +2995,8 @@ async function appendPluginDiagnostics(out, config) {
2670
2995
  }
2671
2996
 
2672
2997
  // src/lint.ts
2673
- var import_promises16 = __toESM(require("fs/promises"), 1);
2674
- var import_node_path14 = __toESM(require("path"), 1);
2998
+ var import_promises17 = __toESM(require("fs/promises"), 1);
2999
+ var import_node_path15 = __toESM(require("path"), 1);
2675
3000
  init_collections();
2676
3001
  init_errors();
2677
3002
  var BUDGETS = {
@@ -2765,13 +3090,13 @@ function lintArticle(article) {
2765
3090
  async function walkMarkdown(dir) {
2766
3091
  let entries;
2767
3092
  try {
2768
- entries = await import_promises16.default.readdir(dir, { withFileTypes: true });
3093
+ entries = await import_promises17.default.readdir(dir, { withFileTypes: true });
2769
3094
  } catch {
2770
3095
  return [];
2771
3096
  }
2772
3097
  const out = [];
2773
3098
  for (const e of entries) {
2774
- const full = import_node_path14.default.join(dir, e.name);
3099
+ const full = import_node_path15.default.join(dir, e.name);
2775
3100
  if (e.isDirectory()) out.push(...await walkMarkdown(full));
2776
3101
  else if (e.isFile() && /\.mdx?$/.test(e.name) && !/-template\.mdx?$/.test(e.name)) out.push(full);
2777
3102
  }
@@ -2801,11 +3126,11 @@ async function lintCollections(scope) {
2801
3126
  let warnCount = 0;
2802
3127
  for (const coll of collections) {
2803
3128
  for (const full of await walkMarkdown(coll.path)) {
2804
- const rel = import_node_path14.default.relative(coll.path, full);
3129
+ const rel = import_node_path15.default.relative(coll.path, full);
2805
3130
  const label = `${coll.name}/${rel}`;
2806
3131
  let findings = [];
2807
3132
  try {
2808
- const parsed = parseArticle(await import_promises16.default.readFile(full, "utf8"));
3133
+ const parsed = parseArticle(await import_promises17.default.readFile(full, "utf8"));
2809
3134
  findings = lintArticle(parsed);
2810
3135
  } catch (err) {
2811
3136
  findings = [{ level: "error", rule: "parse", message: `could not parse: ${err.message}` }];
@@ -2885,6 +3210,7 @@ init_registry2();
2885
3210
  DiffwikiError,
2886
3211
  InvalidExportSelectionError,
2887
3212
  InvalidTargetError,
3213
+ META_KINDS,
2888
3214
  NATIVE_ENGINE,
2889
3215
  NoDefaultCollectionError,
2890
3216
  PROJECT_CONFIG_FILE,
@@ -2906,10 +3232,13 @@ init_registry2();
2906
3232
  configPath,
2907
3233
  createArticle,
2908
3234
  createCollection,
3235
+ createMetaRecord,
2909
3236
  currentBranch,
2910
3237
  deregisterPlugin,
2911
3238
  doctor,
2912
3239
  emitPluginEvent,
3240
+ ensureArticleMeta,
3241
+ ensureCollectionMeta,
2913
3242
  ensureGitRepo,
2914
3243
  ensureHome,
2915
3244
  ensurePluginRoot,
@@ -2921,6 +3250,7 @@ init_registry2();
2921
3250
  getConfigValue,
2922
3251
  getLogger,
2923
3252
  gitToplevel,
3253
+ initMeta,
2924
3254
  initRepoWiki,
2925
3255
  installPlugin,
2926
3256
  invalidateLinkGraph,
@@ -2937,11 +3267,14 @@ init_registry2();
2937
3267
  listTemplates,
2938
3268
  loadSearchProvider,
2939
3269
  localGraph,
3270
+ metaDir,
3271
+ mirrorSavedArticle,
2940
3272
  nativeSearch,
2941
3273
  parseAddTarget,
2942
3274
  parseArticle,
2943
3275
  parseFrontmatter,
2944
3276
  parsePathTarget,
3277
+ patchArticle,
2945
3278
  pluginsDir,
2946
3279
  pluginsRegistryPath,
2947
3280
  query,
@@ -2957,6 +3290,7 @@ init_registry2();
2957
3290
  registerCollection,
2958
3291
  registerPlugin,
2959
3292
  registryPath,
3293
+ reindexMeta,
2960
3294
  removeArticle,
2961
3295
  removeCollection,
2962
3296
  removePlugin,
@@ -2990,9 +3324,11 @@ init_registry2();
2990
3324
  toTreeNodes,
2991
3325
  tokenize,
2992
3326
  unregisterCollection,
3327
+ updateArticle,
2993
3328
  updateArticleBody,
2994
3329
  upsertPlugin,
2995
3330
  upstreamStatus,
3331
+ validateMetaArticlePath,
2996
3332
  writeConfig,
2997
3333
  writePluginRegistry,
2998
3334
  writeRegistry