diffwiki-core 0.17.0 → 0.17.1-rc.202608210907.d3afab7

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
@@ -137,7 +137,7 @@ function isMissing(err) {
137
137
  }
138
138
  async function readRegistry() {
139
139
  try {
140
- const parsed = JSON.parse(await import_promises2.default.readFile(registryPath(), "utf8"));
140
+ const parsed = JSON.parse(await import_promises3.default.readFile(registryPath(), "utf8"));
141
141
  return { version: parsed.version ?? 1, collections: parsed.collections ?? [] };
142
142
  } catch (err) {
143
143
  if (isMissing(err)) return { version: 1, collections: [] };
@@ -145,8 +145,8 @@ async function readRegistry() {
145
145
  }
146
146
  }
147
147
  async function writeRegistry(registry) {
148
- await import_promises2.default.mkdir(resolveHome(), { recursive: true });
149
- await import_promises2.default.writeFile(registryPath(), `${JSON.stringify(registry, null, 2)}
148
+ await import_promises3.default.mkdir(resolveHome(), { recursive: true });
149
+ await import_promises3.default.writeFile(registryPath(), `${JSON.stringify(registry, null, 2)}
150
150
  `, "utf8");
151
151
  }
152
152
  async function registerCollection(entry) {
@@ -164,11 +164,11 @@ async function unregisterCollection(name) {
164
164
  registry.collections = registry.collections.filter((c) => c.name !== name);
165
165
  await writeRegistry(registry);
166
166
  }
167
- var import_promises2;
167
+ var import_promises3;
168
168
  var init_registry = __esm({
169
169
  "src/registry.ts"() {
170
170
  "use strict";
171
- import_promises2 = __toESM(require("fs/promises"), 1);
171
+ import_promises3 = __toESM(require("fs/promises"), 1);
172
172
  init_paths();
173
173
  }
174
174
  });
@@ -179,7 +179,7 @@ function isMissing2(err) {
179
179
  }
180
180
  async function readPluginRegistry() {
181
181
  try {
182
- const parsed = JSON.parse(await import_promises3.default.readFile(pluginsRegistryPath(), "utf8"));
182
+ const parsed = JSON.parse(await import_promises4.default.readFile(pluginsRegistryPath(), "utf8"));
183
183
  return { version: parsed.version ?? 1, plugins: parsed.plugins ?? [] };
184
184
  } catch (err) {
185
185
  if (isMissing2(err)) return { version: 1, plugins: [] };
@@ -187,8 +187,8 @@ async function readPluginRegistry() {
187
187
  }
188
188
  }
189
189
  async function writePluginRegistry(registry) {
190
- await import_promises3.default.mkdir(resolveHome(), { recursive: true });
191
- await import_promises3.default.writeFile(pluginsRegistryPath(), `${JSON.stringify(registry, null, 2)}
190
+ await import_promises4.default.mkdir(resolveHome(), { recursive: true });
191
+ await import_promises4.default.writeFile(pluginsRegistryPath(), `${JSON.stringify(registry, null, 2)}
192
192
  `, "utf8");
193
193
  }
194
194
  async function upsertPlugin(record) {
@@ -219,11 +219,11 @@ async function findPluginRecord(name) {
219
219
  const registry = await readPluginRegistry();
220
220
  return registry.plugins.find((p) => p.name === name);
221
221
  }
222
- var import_promises3;
222
+ var import_promises4;
223
223
  var init_registry2 = __esm({
224
224
  "src/plugins/registry.ts"() {
225
225
  "use strict";
226
- import_promises3 = __toESM(require("fs/promises"), 1);
226
+ import_promises4 = __toESM(require("fs/promises"), 1);
227
227
  init_paths();
228
228
  }
229
229
  });
@@ -534,7 +534,7 @@ async function findCollectionById(id) {
534
534
  async function createCollection(name, opts) {
535
535
  if (await findCollection(name)) throw new CollectionExistsError(name);
536
536
  const dir = collectionDir(name);
537
- await import_promises4.default.mkdir(dir, { recursive: true });
537
+ await import_promises5.default.mkdir(dir, { recursive: true });
538
538
  const entry = {
539
539
  name,
540
540
  type: "global",
@@ -551,16 +551,16 @@ async function removeCollection(name) {
551
551
  if (!entry) throw new CollectionNotFoundError(name);
552
552
  await unregisterCollection(name);
553
553
  if (entry.type === "global") {
554
- await import_promises4.default.rm(entry.path, { recursive: true, force: true });
554
+ await import_promises5.default.rm(entry.path, { recursive: true, force: true });
555
555
  }
556
556
  void fireHook("collection-removed", name);
557
557
  return entry;
558
558
  }
559
- var import_promises4;
559
+ var import_promises5;
560
560
  var init_collections = __esm({
561
561
  "src/collections.ts"() {
562
562
  "use strict";
563
- import_promises4 = __toESM(require("fs/promises"), 1);
563
+ import_promises5 = __toESM(require("fs/promises"), 1);
564
564
  init_paths();
565
565
  init_registry();
566
566
  init_errors();
@@ -603,6 +603,7 @@ __export(index_exports, {
603
603
  doctor: () => doctor,
604
604
  emitPluginEvent: () => emitPluginEvent,
605
605
  ensureGitRepo: () => ensureGitRepo,
606
+ ensureHome: () => ensureHome,
606
607
  ensurePluginRoot: () => ensurePluginRoot,
607
608
  fileTimestamps: () => fileTimestamps,
608
609
  findCollection: () => findCollection,
@@ -693,8 +694,30 @@ init_types();
693
694
  init_errors();
694
695
  init_paths();
695
696
 
696
- // src/templates.ts
697
+ // src/home.ts
697
698
  var import_promises = __toESM(require("fs/promises"), 1);
699
+ init_paths();
700
+ async function seedIfAbsent(file, contents) {
701
+ try {
702
+ await import_promises.default.writeFile(file, contents(), { encoding: "utf8", flag: "wx" });
703
+ } catch (err) {
704
+ if (err.code !== "EEXIST") throw err;
705
+ }
706
+ }
707
+ async function ensureHome() {
708
+ await import_promises.default.mkdir(resolveHome(), { recursive: true });
709
+ await Promise.all([
710
+ import_promises.default.mkdir(collectionsDir(), { recursive: true }),
711
+ import_promises.default.mkdir(pluginsDir(), { recursive: true }),
712
+ import_promises.default.mkdir(templatesDir(), { recursive: true })
713
+ ]);
714
+ await seedIfAbsent(registryPath(), () => `${JSON.stringify({ version: 1, collections: [] }, null, 2)}
715
+ `);
716
+ await seedIfAbsent(configPath(), () => "{}\n");
717
+ }
718
+
719
+ // src/templates.ts
720
+ var import_promises2 = __toESM(require("fs/promises"), 1);
698
721
  var import_node_path2 = __toESM(require("path"), 1);
699
722
  init_paths();
700
723
 
@@ -778,34 +801,34 @@ function serializeArticle(article) {
778
801
  // src/templates.ts
779
802
  async function seedTemplates(srcDir) {
780
803
  const dir = templatesDir();
781
- await import_promises.default.mkdir(dir, { recursive: true });
782
- const files = (await import_promises.default.readdir(srcDir)).filter((f) => f.endsWith(".md")).sort();
804
+ await import_promises2.default.mkdir(dir, { recursive: true });
805
+ const files = (await import_promises2.default.readdir(srcDir)).filter((f) => f.endsWith(".md")).sort();
783
806
  const seeded = [];
784
807
  const skipped = [];
785
808
  for (const file of files) {
786
809
  const dest = import_node_path2.default.join(dir, file);
787
810
  try {
788
- await import_promises.default.access(dest);
811
+ await import_promises2.default.access(dest);
789
812
  skipped.push(file);
790
813
  continue;
791
814
  } catch {
792
815
  }
793
- await import_promises.default.copyFile(import_node_path2.default.join(srcDir, file), dest);
816
+ await import_promises2.default.copyFile(import_node_path2.default.join(srcDir, file), dest);
794
817
  seeded.push(file);
795
818
  }
796
819
  return { seeded, skipped, dir };
797
820
  }
798
821
  async function listTemplateShapes(srcDir) {
799
- const files = await import_promises.default.readdir(srcDir);
822
+ const files = await import_promises2.default.readdir(srcDir);
800
823
  return files.filter((f) => f.endsWith("-template.md")).map((f) => f.replace(/-template\.md$/, "")).sort();
801
824
  }
802
825
  async function listTemplates(srcDir) {
803
- const files = (await import_promises.default.readdir(srcDir)).filter((f) => f.endsWith("-template.md")).sort();
826
+ const files = (await import_promises2.default.readdir(srcDir)).filter((f) => f.endsWith("-template.md")).sort();
804
827
  const out = [];
805
828
  for (const file of files) {
806
829
  let data = {};
807
830
  try {
808
- ({ data } = parseFrontmatter(await import_promises.default.readFile(import_node_path2.default.join(srcDir, file), "utf8")));
831
+ ({ data } = parseFrontmatter(await import_promises2.default.readFile(import_node_path2.default.join(srcDir, file), "utf8")));
809
832
  } catch {
810
833
  }
811
834
  const shape = typeof data.shape === "string" && data.shape ? data.shape : file.replace(/-template\.md$/, "");
@@ -817,7 +840,7 @@ async function listTemplates(srcDir) {
817
840
  }
818
841
 
819
842
  // src/config.ts
820
- var import_promises5 = __toESM(require("fs/promises"), 1);
843
+ var import_promises6 = __toESM(require("fs/promises"), 1);
821
844
  init_paths();
822
845
  init_collections();
823
846
  init_errors();
@@ -828,15 +851,15 @@ function isMissing3(err) {
828
851
  }
829
852
  async function readConfig() {
830
853
  try {
831
- return JSON.parse(await import_promises5.default.readFile(configPath(), "utf8"));
854
+ return JSON.parse(await import_promises6.default.readFile(configPath(), "utf8"));
832
855
  } catch (err) {
833
856
  if (isMissing3(err)) return {};
834
857
  throw err;
835
858
  }
836
859
  }
837
860
  async function writeConfig(config) {
838
- await import_promises5.default.mkdir(resolveHome(), { recursive: true });
839
- await import_promises5.default.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
861
+ await import_promises6.default.mkdir(resolveHome(), { recursive: true });
862
+ await import_promises6.default.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
840
863
  `, "utf8");
841
864
  }
842
865
  async function setConfigValue(key, value) {
@@ -885,7 +908,7 @@ function slugifyPath(input) {
885
908
  init_collections();
886
909
 
887
910
  // src/articles.ts
888
- var import_promises6 = __toESM(require("fs/promises"), 1);
911
+ var import_promises7 = __toESM(require("fs/promises"), 1);
889
912
  var import_node_path4 = __toESM(require("path"), 1);
890
913
  init_collections();
891
914
  init_errors();
@@ -936,8 +959,8 @@ async function createArticle(opts) {
936
959
  const filePath = import_node_path4.default.join(dir, `${slugifyPath(title)}.md`);
937
960
  const body = opts.body ?? `# ${title}
938
961
  `;
939
- await import_promises6.default.mkdir(import_node_path4.default.dirname(filePath), { recursive: true });
940
- await import_promises6.default.writeFile(
962
+ await import_promises7.default.mkdir(import_node_path4.default.dirname(filePath), { recursive: true });
963
+ await import_promises7.default.writeFile(
941
964
  filePath,
942
965
  serializeArticle({ title, tags: opts.tags, body, audience: opts.audience, status: opts.status }),
943
966
  "utf8"
@@ -950,7 +973,7 @@ async function loadArticle(target) {
950
973
  const { dir } = await resolveCollectionDir(collection);
951
974
  const filePath = import_node_path4.default.join(dir, ensureMdPath(file));
952
975
  try {
953
- return { filePath, dir, raw: await import_promises6.default.readFile(filePath, "utf8"), collection };
976
+ return { filePath, dir, raw: await import_promises7.default.readFile(filePath, "utf8"), collection };
954
977
  } catch (err) {
955
978
  if (err.code === "ENOENT") throw new ArticleNotFoundError(target);
956
979
  throw err;
@@ -959,7 +982,7 @@ async function loadArticle(target) {
959
982
  async function updateArticleBody(target, body) {
960
983
  const { filePath, dir, raw, collection } = await loadArticle(target);
961
984
  const parsed = parseArticle(raw);
962
- await import_promises6.default.writeFile(
985
+ await import_promises7.default.writeFile(
963
986
  filePath,
964
987
  serializeArticle({
965
988
  title: parsed.title,
@@ -977,7 +1000,7 @@ async function mutateTags(target, fn) {
977
1000
  const { filePath, dir, raw, collection } = await loadArticle(target);
978
1001
  const parsed = parseArticle(raw);
979
1002
  const tags = fn(parsed.tags);
980
- await import_promises6.default.writeFile(
1003
+ await import_promises7.default.writeFile(
981
1004
  filePath,
982
1005
  serializeArticle({
983
1006
  title: parsed.title,
@@ -1006,7 +1029,7 @@ async function setArticleMeta(target, meta) {
1006
1029
  const parsed = parseArticle(raw);
1007
1030
  const audience = meta.audience ?? parsed.audience;
1008
1031
  const status = meta.status ?? parsed.status;
1009
- await import_promises6.default.writeFile(
1032
+ await import_promises7.default.writeFile(
1010
1033
  filePath,
1011
1034
  serializeArticle({ title: parsed.title, tags: parsed.tags, audience, status, body: parsed.body }),
1012
1035
  "utf8"
@@ -1016,13 +1039,13 @@ async function setArticleMeta(target, meta) {
1016
1039
  }
1017
1040
  async function removeArticle(target) {
1018
1041
  const { filePath, dir, collection } = await loadArticle(target);
1019
- await import_promises6.default.rm(filePath);
1042
+ await import_promises7.default.rm(filePath);
1020
1043
  void fireHook2("article-removed", collection, import_node_path4.default.relative(dir, filePath));
1021
1044
  return filePath;
1022
1045
  }
1023
1046
 
1024
1047
  // src/repo.ts
1025
- var import_promises8 = __toESM(require("fs/promises"), 1);
1048
+ var import_promises9 = __toESM(require("fs/promises"), 1);
1026
1049
  var import_node_path6 = __toESM(require("path"), 1);
1027
1050
  var import_node_child_process3 = require("child_process");
1028
1051
  var import_node_util2 = require("util");
@@ -1031,7 +1054,7 @@ init_collections();
1031
1054
  init_registry();
1032
1055
 
1033
1056
  // src/git.ts
1034
- var import_promises7 = __toESM(require("fs/promises"), 1);
1057
+ var import_promises8 = __toESM(require("fs/promises"), 1);
1035
1058
  var import_node_path5 = __toESM(require("path"), 1);
1036
1059
  var import_node_child_process2 = require("child_process");
1037
1060
  var import_node_util = require("util");
@@ -1113,7 +1136,7 @@ async function fileTimestamps(absPath) {
1113
1136
  let updated = gitUpdated;
1114
1137
  if (!created || !updated) {
1115
1138
  try {
1116
- const st = await import_promises7.default.stat(absPath);
1139
+ const st = await import_promises8.default.stat(absPath);
1117
1140
  if (!updated) updated = st.mtime.toISOString();
1118
1141
  if (!created) created = isEpochish(st.birthtime) ? st.mtime.toISOString() : st.birthtime.toISOString();
1119
1142
  } catch {
@@ -1138,7 +1161,7 @@ async function detectRepoName(cwd) {
1138
1161
  }
1139
1162
  async function readRepoConfig(cwd) {
1140
1163
  try {
1141
- const raw = await import_promises8.default.readFile(import_node_path6.default.join(cwd, REPO_CONFIG_FILE), "utf8");
1164
+ const raw = await import_promises9.default.readFile(import_node_path6.default.join(cwd, REPO_CONFIG_FILE), "utf8");
1142
1165
  const parsed = (0, import_yaml2.parse)(raw) ?? {};
1143
1166
  if (!parsed.path) return void 0;
1144
1167
  return {
@@ -1168,9 +1191,9 @@ async function initRepoWiki(opts) {
1168
1191
  n += 1;
1169
1192
  }
1170
1193
  }
1171
- await import_promises8.default.mkdir(absWiki, { recursive: true });
1194
+ await import_promises9.default.mkdir(absWiki, { recursive: true });
1172
1195
  const config = { collection: name, path: wikiPath };
1173
- await import_promises8.default.writeFile(import_node_path6.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml2.stringify)(config), "utf8");
1196
+ await import_promises9.default.writeFile(import_node_path6.default.join(cwd, REPO_CONFIG_FILE), (0, import_yaml2.stringify)(config), "utf8");
1174
1197
  const entry = {
1175
1198
  name,
1176
1199
  type: "repo",
@@ -1239,7 +1262,7 @@ async function collectionGitStatus(entry) {
1239
1262
  }
1240
1263
 
1241
1264
  // src/project.ts
1242
- var import_promises9 = __toESM(require("fs/promises"), 1);
1265
+ var import_promises10 = __toESM(require("fs/promises"), 1);
1243
1266
  var import_node_os2 = __toESM(require("os"), 1);
1244
1267
  var import_node_path7 = __toESM(require("path"), 1);
1245
1268
  var import_yaml3 = require("yaml");
@@ -1296,7 +1319,7 @@ function normaliseContext(raw) {
1296
1319
  async function readProjectConfig(dir) {
1297
1320
  let raw;
1298
1321
  try {
1299
- raw = await import_promises9.default.readFile(import_node_path7.default.join(dir, PROJECT_CONFIG_FILE), "utf8");
1322
+ raw = await import_promises10.default.readFile(import_node_path7.default.join(dir, PROJECT_CONFIG_FILE), "utf8");
1300
1323
  } catch (err) {
1301
1324
  if (err.code === "ENOENT") return void 0;
1302
1325
  throw err;
@@ -1331,13 +1354,13 @@ async function setContextCollections(dir, collections) {
1331
1354
  const file = import_node_path7.default.join(dir, PROJECT_CONFIG_FILE);
1332
1355
  let raw = "";
1333
1356
  try {
1334
- raw = await import_promises9.default.readFile(file, "utf8");
1357
+ raw = await import_promises10.default.readFile(file, "utf8");
1335
1358
  } catch (err) {
1336
1359
  if (err.code !== "ENOENT") throw err;
1337
1360
  }
1338
1361
  const doc = (0, import_yaml3.parseDocument)(raw);
1339
1362
  doc.setIn(["context", "collections"], collections);
1340
- await import_promises9.default.writeFile(file, doc.toString(), "utf8");
1363
+ await import_promises10.default.writeFile(file, doc.toString(), "utf8");
1341
1364
  return file;
1342
1365
  }
1343
1366
  async function resolveProjectCollections(dir) {
@@ -1360,8 +1383,8 @@ async function seedProjectHome(dir) {
1360
1383
  if (entries.length === 0) {
1361
1384
  throw new Error(`No collections to render from diffwiki.yaml in ${import_node_path7.default.resolve(dir)}`);
1362
1385
  }
1363
- const home = await import_promises9.default.mkdtemp(import_node_path7.default.join(import_node_os2.default.tmpdir(), "diffwiki-project-"));
1364
- await import_promises9.default.writeFile(
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(
1365
1388
  import_node_path7.default.join(home, "registry.json"),
1366
1389
  `${JSON.stringify({ version: 1, collections: entries }, null, 2)}
1367
1390
  `,
@@ -1374,7 +1397,7 @@ async function seedProjectHome(dir) {
1374
1397
  init_collections();
1375
1398
 
1376
1399
  // src/browse.ts
1377
- var import_promises10 = __toESM(require("fs/promises"), 1);
1400
+ var import_promises11 = __toESM(require("fs/promises"), 1);
1378
1401
  var import_node_path8 = __toESM(require("path"), 1);
1379
1402
  init_collections();
1380
1403
  init_errors();
@@ -1421,7 +1444,7 @@ function buildArticleTree(relPaths) {
1421
1444
  async function collectRelPaths(dir, rel = "") {
1422
1445
  let dirents;
1423
1446
  try {
1424
- dirents = await import_promises10.default.readdir(dir, { withFileTypes: true });
1447
+ dirents = await import_promises11.default.readdir(dir, { withFileTypes: true });
1425
1448
  } catch {
1426
1449
  return [];
1427
1450
  }
@@ -1439,7 +1462,7 @@ async function collectRelPaths(dir, rel = "") {
1439
1462
  }
1440
1463
  async function readdirSafe(dir) {
1441
1464
  try {
1442
- return await import_promises10.default.readdir(dir);
1465
+ return await import_promises11.default.readdir(dir);
1443
1466
  } catch {
1444
1467
  return [];
1445
1468
  }
@@ -1457,7 +1480,7 @@ var CHEAP_META_BYTES = 8192;
1457
1480
  async function cheapMeta(absPath) {
1458
1481
  let handle;
1459
1482
  try {
1460
- handle = await import_promises10.default.open(absPath, "r");
1483
+ handle = await import_promises11.default.open(absPath, "r");
1461
1484
  const buf = Buffer.alloc(CHEAP_META_BYTES);
1462
1485
  const { bytesRead } = await handle.read(buf, 0, CHEAP_META_BYTES, 0);
1463
1486
  const snippet = buf.subarray(0, bytesRead).toString("utf8");
@@ -1548,7 +1571,7 @@ async function readArticle(coll, slug) {
1548
1571
  const candidate = import_node_path8.default.resolve(collDir, rel);
1549
1572
  if (!inJail(candidate)) throw new InvalidTargetError(slug, "path traversal detected");
1550
1573
  try {
1551
- await import_promises10.default.access(candidate);
1574
+ await import_promises11.default.access(candidate);
1552
1575
  abs = candidate;
1553
1576
  break;
1554
1577
  } catch {
@@ -1565,7 +1588,7 @@ async function readArticle(coll, slug) {
1565
1588
  }
1566
1589
  }
1567
1590
  if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
1568
- const parsed = parseArticle(await import_promises10.default.readFile(abs, "utf8"));
1591
+ const parsed = parseArticle(await import_promises11.default.readFile(abs, "utf8"));
1569
1592
  const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
1570
1593
  return {
1571
1594
  coll,
@@ -2012,7 +2035,7 @@ function resolveCollectionSelection(all, sel = {}) {
2012
2035
  }
2013
2036
 
2014
2037
  // src/query.ts
2015
- var import_promises11 = __toESM(require("fs/promises"), 1);
2038
+ var import_promises12 = __toESM(require("fs/promises"), 1);
2016
2039
  var import_node_path10 = __toESM(require("path"), 1);
2017
2040
  init_collections();
2018
2041
  init_errors();
@@ -2088,7 +2111,7 @@ var log2 = getLogger("query");
2088
2111
  async function collectMarkdownFiles(dir) {
2089
2112
  let entries;
2090
2113
  try {
2091
- entries = await import_promises11.default.readdir(dir, { withFileTypes: true });
2114
+ entries = await import_promises12.default.readdir(dir, { withFileTypes: true });
2092
2115
  } catch {
2093
2116
  return [];
2094
2117
  }
@@ -2126,7 +2149,7 @@ async function nativeSearch(term, collections, collection, hideSystem = false) {
2126
2149
  let tags = [];
2127
2150
  let body = "";
2128
2151
  try {
2129
- const parsed = parseArticle(await import_promises11.default.readFile(full, "utf8"));
2152
+ const parsed = parseArticle(await import_promises12.default.readFile(full, "utf8"));
2130
2153
  if (hideSystem && parsed.audience === "system") continue;
2131
2154
  if (parsed.title) title = parsed.title;
2132
2155
  tags = parsed.tags;
@@ -2184,7 +2207,7 @@ async function dropSystemHits(hits) {
2184
2207
  const kept = [];
2185
2208
  for (const hit of hits) {
2186
2209
  try {
2187
- if (parseArticle(await import_promises11.default.readFile(hit.path, "utf8")).audience === "system") continue;
2210
+ if (parseArticle(await import_promises12.default.readFile(hit.path, "utf8")).audience === "system") continue;
2188
2211
  } catch {
2189
2212
  }
2190
2213
  kept.push(hit);
@@ -2232,14 +2255,14 @@ async function query(term, opts) {
2232
2255
  }
2233
2256
 
2234
2257
  // src/search.ts
2235
- var import_promises12 = __toESM(require("fs/promises"), 1);
2258
+ var import_promises13 = __toESM(require("fs/promises"), 1);
2236
2259
  var import_node_path11 = __toESM(require("path"), 1);
2237
2260
  init_collections();
2238
2261
  var isStatic2 = () => process.env.DIFFWIKI_STATIC === "1";
2239
2262
  async function readArticleParts(collDir, slug) {
2240
2263
  for (const ext of [".mdx", ".md"]) {
2241
2264
  try {
2242
- const parsed = parseArticle(await import_promises12.default.readFile(import_node_path11.default.join(collDir, `${slug}${ext}`), "utf8"));
2265
+ const parsed = parseArticle(await import_promises13.default.readFile(import_node_path11.default.join(collDir, `${slug}${ext}`), "utf8"));
2243
2266
  return { tags: parsed.tags, body: parsed.body };
2244
2267
  } catch {
2245
2268
  }
@@ -2289,13 +2312,13 @@ async function buildSearchIndex(collection) {
2289
2312
  }
2290
2313
 
2291
2314
  // src/doctor.ts
2292
- var import_promises14 = __toESM(require("fs/promises"), 1);
2315
+ var import_promises15 = __toESM(require("fs/promises"), 1);
2293
2316
  init_paths();
2294
2317
  init_registry();
2295
2318
  init_collections();
2296
2319
 
2297
2320
  // src/plugins/manager.ts
2298
- var import_promises13 = __toESM(require("fs/promises"), 1);
2321
+ var import_promises14 = __toESM(require("fs/promises"), 1);
2299
2322
  var import_node_os3 = __toESM(require("os"), 1);
2300
2323
  var import_node_path13 = __toESM(require("path"), 1);
2301
2324
  init_paths();
@@ -2324,7 +2347,7 @@ async function runProcess(file, args, opts) {
2324
2347
  // src/plugins/manager.ts
2325
2348
  async function readPackageJson(file) {
2326
2349
  try {
2327
- return JSON.parse(await import_promises13.default.readFile(file, "utf8"));
2350
+ return JSON.parse(await import_promises14.default.readFile(file, "utf8"));
2328
2351
  } catch {
2329
2352
  return null;
2330
2353
  }
@@ -2340,7 +2363,7 @@ function binOf(manifest) {
2340
2363
  async function pluginManifestDirs(nodeModules) {
2341
2364
  let entries;
2342
2365
  try {
2343
- entries = await import_promises13.default.readdir(nodeModules);
2366
+ entries = await import_promises14.default.readdir(nodeModules);
2344
2367
  } catch {
2345
2368
  return [];
2346
2369
  }
@@ -2350,7 +2373,7 @@ async function pluginManifestDirs(nodeModules) {
2350
2373
  if (entry.startsWith("@")) {
2351
2374
  let scoped;
2352
2375
  try {
2353
- scoped = await import_promises13.default.readdir(import_node_path13.default.join(nodeModules, entry));
2376
+ scoped = await import_promises14.default.readdir(import_node_path13.default.join(nodeModules, entry));
2354
2377
  } catch {
2355
2378
  continue;
2356
2379
  }
@@ -2366,11 +2389,11 @@ async function pluginManifestDirs(nodeModules) {
2366
2389
  }
2367
2390
  async function ensurePluginRoot() {
2368
2391
  const dir = pluginsDir();
2369
- await import_promises13.default.mkdir(dir, { recursive: true });
2392
+ await import_promises14.default.mkdir(dir, { recursive: true });
2370
2393
  const pkgPath = import_node_path13.default.join(dir, "package.json");
2371
2394
  if (await readPackageJson(pkgPath) === null) {
2372
2395
  const pkg = { name: "diffwiki-plugins", private: true, version: "0.0.0" };
2373
- await import_promises13.default.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
2396
+ await import_promises14.default.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
2374
2397
  `, "utf8");
2375
2398
  }
2376
2399
  return dir;
@@ -2541,7 +2564,7 @@ init_host();
2541
2564
  init_types();
2542
2565
  async function exists(target) {
2543
2566
  try {
2544
- await import_promises14.default.access(target);
2567
+ await import_promises15.default.access(target);
2545
2568
  return true;
2546
2569
  } catch {
2547
2570
  return false;
@@ -2647,7 +2670,7 @@ async function appendPluginDiagnostics(out, config) {
2647
2670
  }
2648
2671
 
2649
2672
  // src/lint.ts
2650
- var import_promises15 = __toESM(require("fs/promises"), 1);
2673
+ var import_promises16 = __toESM(require("fs/promises"), 1);
2651
2674
  var import_node_path14 = __toESM(require("path"), 1);
2652
2675
  init_collections();
2653
2676
  init_errors();
@@ -2742,7 +2765,7 @@ function lintArticle(article) {
2742
2765
  async function walkMarkdown(dir) {
2743
2766
  let entries;
2744
2767
  try {
2745
- entries = await import_promises15.default.readdir(dir, { withFileTypes: true });
2768
+ entries = await import_promises16.default.readdir(dir, { withFileTypes: true });
2746
2769
  } catch {
2747
2770
  return [];
2748
2771
  }
@@ -2782,7 +2805,7 @@ async function lintCollections(scope) {
2782
2805
  const label = `${coll.name}/${rel}`;
2783
2806
  let findings = [];
2784
2807
  try {
2785
- const parsed = parseArticle(await import_promises15.default.readFile(full, "utf8"));
2808
+ const parsed = parseArticle(await import_promises16.default.readFile(full, "utf8"));
2786
2809
  findings = lintArticle(parsed);
2787
2810
  } catch (err) {
2788
2811
  findings = [{ level: "error", rule: "parse", message: `could not parse: ${err.message}` }];
@@ -2888,6 +2911,7 @@ init_registry2();
2888
2911
  doctor,
2889
2912
  emitPluginEvent,
2890
2913
  ensureGitRepo,
2914
+ ensureHome,
2891
2915
  ensurePluginRoot,
2892
2916
  fileTimestamps,
2893
2917
  findCollection,
package/dist/index.d.cts CHANGED
@@ -46,6 +46,14 @@ declare function collectionDir(name: string): string;
46
46
  declare function pluginsDir(): string;
47
47
  declare function pluginsRegistryPath(): string;
48
48
 
49
+ /**
50
+ * Initialize the global diffwiki home (`~/.diffwiki` or `DIFFWIKI_HOME`).
51
+ * Creates the directory tree and seeds empty-but-valid registry/config when
52
+ * absent. Idempotent — the CLI calls this at startup so the first command
53
+ * leaves a fully initialized home behind.
54
+ */
55
+ declare function ensureHome(): Promise<void>;
56
+
49
57
  /**
50
58
  * Authoring-template seeding.
51
59
  *
@@ -795,4 +803,4 @@ declare function findEnabledPlugin(kind: PluginKind, name?: string): Promise<Plu
795
803
  /** Find a plugin record by name (regardless of enabled state). */
796
804
  declare function findPluginRecord(name: string): Promise<PluginRecord | undefined>;
797
805
 
798
- export { type AddExternalOptions, Article, ArticleContent, ArticleNode, ArticleNotFoundError, Audience, CollectionAlreadyAddedError, CollectionEntry, CollectionExistsError, type CollectionGitStatus, CollectionInfo, CollectionNotFoundError, type CollectionSelection, type CollectionSelectionResult, CollectionTree, Config, Diagnostic, DiffwikiError, type DiffwikiLogLevel, type EditTarget, type FileTimestamps, type InitOptions, type InstallOutcome, InvalidExportSelectionError, InvalidTargetError, type KnownPlugin, LinkGraph, type LintFileResult, type LintFinding, type LintLevel, type LintResult, LocalGraph, NoDefaultCollectionError, PROJECT_CONFIG_FILE, PageData, ParsedArticle, PluginError, PluginEvent, PluginKind, PluginRecord, PluginRegistryFile, ProjectConfig, PublishStatus, QueryHit, QueryOptions, REPO_CONFIG_FILE, Registry, type RenderContext, type RenderedArticle, RepoConfig, SearchDoc, SearchMode, type SearchOutcome, SearchProvider, type SeedResult, type SetupOutcome, type SetupStep, SiteConfig, type TemplateInfo, TocItem, TreeNode, type UpstreamStatus, WorktreeNotAllowedError, addExternalCollection, addTags, buildArticleTree, buildCollections, buildLinkGraph, buildNavTree, buildSearchIndex, buildSiteConfig, collectionDir, collectionGitStatus, collectionMeta, collectionsDir, configPath, createArticle, createCollection, currentBranch, deregisterPlugin, doctor, emitPluginEvent, ensureGitRepo, ensurePluginRoot, fileTimestamps, findCollection, findCollectionById, findEnabledPlugin, findPluginRecord, getConfigValue, getLogger, gitToplevel, initRepoWiki, installPlugin, invalidateLinkGraph, isLinkedWorktree, lintArticle, lintCollections, listArticleTree, listAvailablePlugins, listCollections, listEnabledPlugins, listPlugins, listSearchModes, listTemplateShapes, listTemplates, loadSearchProvider, localGraph, nativeSearch, parseAddTarget, parsePathTarget, pluginsDir, pluginsRegistryPath, query, readArticle, readConfig, readContextCollections, readPluginRegistry, readProjectConfig, readRegistry, readRepoConfig, readSiteConfig, registerCollection, registerPlugin, registryPath, removeArticle, removeCollection, removePlugin, removeTags, renderArticle, repairPlugins, repoId, repoRemoteName, resolveCollectionSelection, resolveCwdCollection, resolveDefaultSearchMode, resolveEditTarget, resolveEditorCommand, resolveHome, resolveLogLevel, resolvePage, resolveProjectCollections, search, seedProjectHome, seedTemplates, setArticleMeta, setConfigValue, setContextCollections, setTags, showSystemEnabled, slugify, slugifyPath, templatesDir, toTreeNodes, unregisterCollection, updateArticleBody, upsertPlugin, upstreamStatus, writeConfig, writePluginRegistry, writeRegistry };
806
+ export { type AddExternalOptions, Article, ArticleContent, ArticleNode, ArticleNotFoundError, Audience, CollectionAlreadyAddedError, CollectionEntry, CollectionExistsError, type CollectionGitStatus, CollectionInfo, CollectionNotFoundError, type CollectionSelection, type CollectionSelectionResult, CollectionTree, Config, Diagnostic, DiffwikiError, type DiffwikiLogLevel, type EditTarget, type FileTimestamps, type InitOptions, type InstallOutcome, InvalidExportSelectionError, InvalidTargetError, type KnownPlugin, LinkGraph, type LintFileResult, type LintFinding, type LintLevel, type LintResult, LocalGraph, NoDefaultCollectionError, PROJECT_CONFIG_FILE, PageData, ParsedArticle, PluginError, PluginEvent, PluginKind, PluginRecord, PluginRegistryFile, ProjectConfig, PublishStatus, QueryHit, QueryOptions, REPO_CONFIG_FILE, Registry, type RenderContext, type RenderedArticle, RepoConfig, SearchDoc, SearchMode, type SearchOutcome, SearchProvider, type SeedResult, type SetupOutcome, type SetupStep, SiteConfig, type TemplateInfo, TocItem, TreeNode, type UpstreamStatus, WorktreeNotAllowedError, addExternalCollection, addTags, buildArticleTree, buildCollections, buildLinkGraph, buildNavTree, buildSearchIndex, buildSiteConfig, collectionDir, collectionGitStatus, collectionMeta, collectionsDir, configPath, createArticle, createCollection, currentBranch, deregisterPlugin, doctor, emitPluginEvent, ensureGitRepo, ensureHome, ensurePluginRoot, fileTimestamps, findCollection, findCollectionById, findEnabledPlugin, findPluginRecord, getConfigValue, getLogger, gitToplevel, initRepoWiki, installPlugin, invalidateLinkGraph, isLinkedWorktree, lintArticle, lintCollections, listArticleTree, listAvailablePlugins, listCollections, listEnabledPlugins, listPlugins, listSearchModes, listTemplateShapes, listTemplates, loadSearchProvider, localGraph, nativeSearch, parseAddTarget, parsePathTarget, pluginsDir, pluginsRegistryPath, query, readArticle, readConfig, readContextCollections, readPluginRegistry, readProjectConfig, readRegistry, readRepoConfig, readSiteConfig, registerCollection, registerPlugin, registryPath, removeArticle, removeCollection, removePlugin, removeTags, renderArticle, repairPlugins, repoId, repoRemoteName, resolveCollectionSelection, resolveCwdCollection, resolveDefaultSearchMode, resolveEditTarget, resolveEditorCommand, resolveHome, resolveLogLevel, resolvePage, resolveProjectCollections, search, seedProjectHome, seedTemplates, setArticleMeta, setConfigValue, setContextCollections, setTags, showSystemEnabled, slugify, slugifyPath, templatesDir, toTreeNodes, unregisterCollection, updateArticleBody, upsertPlugin, upstreamStatus, writeConfig, writePluginRegistry, writeRegistry };
package/dist/index.d.ts CHANGED
@@ -46,6 +46,14 @@ declare function collectionDir(name: string): string;
46
46
  declare function pluginsDir(): string;
47
47
  declare function pluginsRegistryPath(): string;
48
48
 
49
+ /**
50
+ * Initialize the global diffwiki home (`~/.diffwiki` or `DIFFWIKI_HOME`).
51
+ * Creates the directory tree and seeds empty-but-valid registry/config when
52
+ * absent. Idempotent — the CLI calls this at startup so the first command
53
+ * leaves a fully initialized home behind.
54
+ */
55
+ declare function ensureHome(): Promise<void>;
56
+
49
57
  /**
50
58
  * Authoring-template seeding.
51
59
  *
@@ -795,4 +803,4 @@ declare function findEnabledPlugin(kind: PluginKind, name?: string): Promise<Plu
795
803
  /** Find a plugin record by name (regardless of enabled state). */
796
804
  declare function findPluginRecord(name: string): Promise<PluginRecord | undefined>;
797
805
 
798
- export { type AddExternalOptions, Article, ArticleContent, ArticleNode, ArticleNotFoundError, Audience, CollectionAlreadyAddedError, CollectionEntry, CollectionExistsError, type CollectionGitStatus, CollectionInfo, CollectionNotFoundError, type CollectionSelection, type CollectionSelectionResult, CollectionTree, Config, Diagnostic, DiffwikiError, type DiffwikiLogLevel, type EditTarget, type FileTimestamps, type InitOptions, type InstallOutcome, InvalidExportSelectionError, InvalidTargetError, type KnownPlugin, LinkGraph, type LintFileResult, type LintFinding, type LintLevel, type LintResult, LocalGraph, NoDefaultCollectionError, PROJECT_CONFIG_FILE, PageData, ParsedArticle, PluginError, PluginEvent, PluginKind, PluginRecord, PluginRegistryFile, ProjectConfig, PublishStatus, QueryHit, QueryOptions, REPO_CONFIG_FILE, Registry, type RenderContext, type RenderedArticle, RepoConfig, SearchDoc, SearchMode, type SearchOutcome, SearchProvider, type SeedResult, type SetupOutcome, type SetupStep, SiteConfig, type TemplateInfo, TocItem, TreeNode, type UpstreamStatus, WorktreeNotAllowedError, addExternalCollection, addTags, buildArticleTree, buildCollections, buildLinkGraph, buildNavTree, buildSearchIndex, buildSiteConfig, collectionDir, collectionGitStatus, collectionMeta, collectionsDir, configPath, createArticle, createCollection, currentBranch, deregisterPlugin, doctor, emitPluginEvent, ensureGitRepo, ensurePluginRoot, fileTimestamps, findCollection, findCollectionById, findEnabledPlugin, findPluginRecord, getConfigValue, getLogger, gitToplevel, initRepoWiki, installPlugin, invalidateLinkGraph, isLinkedWorktree, lintArticle, lintCollections, listArticleTree, listAvailablePlugins, listCollections, listEnabledPlugins, listPlugins, listSearchModes, listTemplateShapes, listTemplates, loadSearchProvider, localGraph, nativeSearch, parseAddTarget, parsePathTarget, pluginsDir, pluginsRegistryPath, query, readArticle, readConfig, readContextCollections, readPluginRegistry, readProjectConfig, readRegistry, readRepoConfig, readSiteConfig, registerCollection, registerPlugin, registryPath, removeArticle, removeCollection, removePlugin, removeTags, renderArticle, repairPlugins, repoId, repoRemoteName, resolveCollectionSelection, resolveCwdCollection, resolveDefaultSearchMode, resolveEditTarget, resolveEditorCommand, resolveHome, resolveLogLevel, resolvePage, resolveProjectCollections, search, seedProjectHome, seedTemplates, setArticleMeta, setConfigValue, setContextCollections, setTags, showSystemEnabled, slugify, slugifyPath, templatesDir, toTreeNodes, unregisterCollection, updateArticleBody, upsertPlugin, upstreamStatus, writeConfig, writePluginRegistry, writeRegistry };
806
+ export { type AddExternalOptions, Article, ArticleContent, ArticleNode, ArticleNotFoundError, Audience, CollectionAlreadyAddedError, CollectionEntry, CollectionExistsError, type CollectionGitStatus, CollectionInfo, CollectionNotFoundError, type CollectionSelection, type CollectionSelectionResult, CollectionTree, Config, Diagnostic, DiffwikiError, type DiffwikiLogLevel, type EditTarget, type FileTimestamps, type InitOptions, type InstallOutcome, InvalidExportSelectionError, InvalidTargetError, type KnownPlugin, LinkGraph, type LintFileResult, type LintFinding, type LintLevel, type LintResult, LocalGraph, NoDefaultCollectionError, PROJECT_CONFIG_FILE, PageData, ParsedArticle, PluginError, PluginEvent, PluginKind, PluginRecord, PluginRegistryFile, ProjectConfig, PublishStatus, QueryHit, QueryOptions, REPO_CONFIG_FILE, Registry, type RenderContext, type RenderedArticle, RepoConfig, SearchDoc, SearchMode, type SearchOutcome, SearchProvider, type SeedResult, type SetupOutcome, type SetupStep, SiteConfig, type TemplateInfo, TocItem, TreeNode, type UpstreamStatus, WorktreeNotAllowedError, addExternalCollection, addTags, buildArticleTree, buildCollections, buildLinkGraph, buildNavTree, buildSearchIndex, buildSiteConfig, collectionDir, collectionGitStatus, collectionMeta, collectionsDir, configPath, createArticle, createCollection, currentBranch, deregisterPlugin, doctor, emitPluginEvent, ensureGitRepo, ensureHome, ensurePluginRoot, fileTimestamps, findCollection, findCollectionById, findEnabledPlugin, findPluginRecord, getConfigValue, getLogger, gitToplevel, initRepoWiki, installPlugin, invalidateLinkGraph, isLinkedWorktree, lintArticle, lintCollections, listArticleTree, listAvailablePlugins, listCollections, listEnabledPlugins, listPlugins, listSearchModes, listTemplateShapes, listTemplates, loadSearchProvider, localGraph, nativeSearch, parseAddTarget, parsePathTarget, pluginsDir, pluginsRegistryPath, query, readArticle, readConfig, readContextCollections, readPluginRegistry, readProjectConfig, readRegistry, readRepoConfig, readSiteConfig, registerCollection, registerPlugin, registryPath, removeArticle, removeCollection, removePlugin, removeTags, renderArticle, repairPlugins, repoId, repoRemoteName, resolveCollectionSelection, resolveCwdCollection, resolveDefaultSearchMode, resolveEditTarget, resolveEditorCommand, resolveHome, resolveLogLevel, resolvePage, resolveProjectCollections, search, seedProjectHome, seedTemplates, setArticleMeta, setConfigValue, setContextCollections, setTags, showSystemEnabled, slugify, slugifyPath, templatesDir, toTreeNodes, unregisterCollection, updateArticleBody, upsertPlugin, upstreamStatus, writeConfig, writePluginRegistry, writeRegistry };
package/dist/index.js CHANGED
@@ -54,39 +54,60 @@ import {
54
54
  writeRegistry
55
55
  } from "./chunk-TXM4OGBA.js";
56
56
 
57
- // src/templates.ts
57
+ // src/home.ts
58
58
  import fs from "fs/promises";
59
+ async function seedIfAbsent(file, contents) {
60
+ try {
61
+ await fs.writeFile(file, contents(), { encoding: "utf8", flag: "wx" });
62
+ } catch (err) {
63
+ if (err.code !== "EEXIST") throw err;
64
+ }
65
+ }
66
+ async function ensureHome() {
67
+ await fs.mkdir(resolveHome(), { recursive: true });
68
+ await Promise.all([
69
+ fs.mkdir(collectionsDir(), { recursive: true }),
70
+ fs.mkdir(pluginsDir(), { recursive: true }),
71
+ fs.mkdir(templatesDir(), { recursive: true })
72
+ ]);
73
+ await seedIfAbsent(registryPath(), () => `${JSON.stringify({ version: 1, collections: [] }, null, 2)}
74
+ `);
75
+ await seedIfAbsent(configPath(), () => "{}\n");
76
+ }
77
+
78
+ // src/templates.ts
79
+ import fs2 from "fs/promises";
59
80
  import path from "path";
60
81
  async function seedTemplates(srcDir) {
61
82
  const dir = templatesDir();
62
- await fs.mkdir(dir, { recursive: true });
63
- const files = (await fs.readdir(srcDir)).filter((f) => f.endsWith(".md")).sort();
83
+ await fs2.mkdir(dir, { recursive: true });
84
+ const files = (await fs2.readdir(srcDir)).filter((f) => f.endsWith(".md")).sort();
64
85
  const seeded = [];
65
86
  const skipped = [];
66
87
  for (const file of files) {
67
88
  const dest = path.join(dir, file);
68
89
  try {
69
- await fs.access(dest);
90
+ await fs2.access(dest);
70
91
  skipped.push(file);
71
92
  continue;
72
93
  } catch {
73
94
  }
74
- await fs.copyFile(path.join(srcDir, file), dest);
95
+ await fs2.copyFile(path.join(srcDir, file), dest);
75
96
  seeded.push(file);
76
97
  }
77
98
  return { seeded, skipped, dir };
78
99
  }
79
100
  async function listTemplateShapes(srcDir) {
80
- const files = await fs.readdir(srcDir);
101
+ const files = await fs2.readdir(srcDir);
81
102
  return files.filter((f) => f.endsWith("-template.md")).map((f) => f.replace(/-template\.md$/, "")).sort();
82
103
  }
83
104
  async function listTemplates(srcDir) {
84
- const files = (await fs.readdir(srcDir)).filter((f) => f.endsWith("-template.md")).sort();
105
+ const files = (await fs2.readdir(srcDir)).filter((f) => f.endsWith("-template.md")).sort();
85
106
  const out = [];
86
107
  for (const file of files) {
87
108
  let data = {};
88
109
  try {
89
- ({ data } = parseFrontmatter(await fs.readFile(path.join(srcDir, file), "utf8")));
110
+ ({ data } = parseFrontmatter(await fs2.readFile(path.join(srcDir, file), "utf8")));
90
111
  } catch {
91
112
  }
92
113
  const shape = typeof data.shape === "string" && data.shape ? data.shape : file.replace(/-template\.md$/, "");
@@ -98,21 +119,21 @@ async function listTemplates(srcDir) {
98
119
  }
99
120
 
100
121
  // src/config.ts
101
- import fs2 from "fs/promises";
122
+ import fs3 from "fs/promises";
102
123
  function isMissing(err) {
103
124
  return err?.code === "ENOENT";
104
125
  }
105
126
  async function readConfig() {
106
127
  try {
107
- return JSON.parse(await fs2.readFile(configPath(), "utf8"));
128
+ return JSON.parse(await fs3.readFile(configPath(), "utf8"));
108
129
  } catch (err) {
109
130
  if (isMissing(err)) return {};
110
131
  throw err;
111
132
  }
112
133
  }
113
134
  async function writeConfig(config) {
114
- await fs2.mkdir(resolveHome(), { recursive: true });
115
- await fs2.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
135
+ await fs3.mkdir(resolveHome(), { recursive: true });
136
+ await fs3.writeFile(configPath(), `${JSON.stringify(config, null, 2)}
116
137
  `, "utf8");
117
138
  }
118
139
  async function setConfigValue(key, value) {
@@ -155,7 +176,7 @@ function slugifyPath(input) {
155
176
  }
156
177
 
157
178
  // src/articles.ts
158
- import fs3 from "fs/promises";
179
+ import fs4 from "fs/promises";
159
180
  import path2 from "path";
160
181
  async function fireHook(event, collection, relPath) {
161
182
  try {
@@ -204,8 +225,8 @@ async function createArticle(opts) {
204
225
  const filePath = path2.join(dir, `${slugifyPath(title)}.md`);
205
226
  const body = opts.body ?? `# ${title}
206
227
  `;
207
- await fs3.mkdir(path2.dirname(filePath), { recursive: true });
208
- await fs3.writeFile(
228
+ await fs4.mkdir(path2.dirname(filePath), { recursive: true });
229
+ await fs4.writeFile(
209
230
  filePath,
210
231
  serializeArticle({ title, tags: opts.tags, body, audience: opts.audience, status: opts.status }),
211
232
  "utf8"
@@ -218,7 +239,7 @@ async function loadArticle(target) {
218
239
  const { dir } = await resolveCollectionDir(collection);
219
240
  const filePath = path2.join(dir, ensureMdPath(file));
220
241
  try {
221
- return { filePath, dir, raw: await fs3.readFile(filePath, "utf8"), collection };
242
+ return { filePath, dir, raw: await fs4.readFile(filePath, "utf8"), collection };
222
243
  } catch (err) {
223
244
  if (err.code === "ENOENT") throw new ArticleNotFoundError(target);
224
245
  throw err;
@@ -227,7 +248,7 @@ async function loadArticle(target) {
227
248
  async function updateArticleBody(target, body) {
228
249
  const { filePath, dir, raw, collection } = await loadArticle(target);
229
250
  const parsed = parseArticle(raw);
230
- await fs3.writeFile(
251
+ await fs4.writeFile(
231
252
  filePath,
232
253
  serializeArticle({
233
254
  title: parsed.title,
@@ -245,7 +266,7 @@ async function mutateTags(target, fn) {
245
266
  const { filePath, dir, raw, collection } = await loadArticle(target);
246
267
  const parsed = parseArticle(raw);
247
268
  const tags = fn(parsed.tags);
248
- await fs3.writeFile(
269
+ await fs4.writeFile(
249
270
  filePath,
250
271
  serializeArticle({
251
272
  title: parsed.title,
@@ -274,7 +295,7 @@ async function setArticleMeta(target, meta) {
274
295
  const parsed = parseArticle(raw);
275
296
  const audience = meta.audience ?? parsed.audience;
276
297
  const status = meta.status ?? parsed.status;
277
- await fs3.writeFile(
298
+ await fs4.writeFile(
278
299
  filePath,
279
300
  serializeArticle({ title: parsed.title, tags: parsed.tags, audience, status, body: parsed.body }),
280
301
  "utf8"
@@ -284,20 +305,20 @@ async function setArticleMeta(target, meta) {
284
305
  }
285
306
  async function removeArticle(target) {
286
307
  const { filePath, dir, collection } = await loadArticle(target);
287
- await fs3.rm(filePath);
308
+ await fs4.rm(filePath);
288
309
  void fireHook("article-removed", collection, path2.relative(dir, filePath));
289
310
  return filePath;
290
311
  }
291
312
 
292
313
  // src/repo.ts
293
- import fs5 from "fs/promises";
314
+ import fs6 from "fs/promises";
294
315
  import path4 from "path";
295
316
  import { execFile as execFile2 } from "child_process";
296
317
  import { promisify as promisify2 } from "util";
297
318
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
298
319
 
299
320
  // src/git.ts
300
- import fs4 from "fs/promises";
321
+ import fs5 from "fs/promises";
301
322
  import path3 from "path";
302
323
  import { execFile } from "child_process";
303
324
  import { promisify } from "util";
@@ -378,7 +399,7 @@ async function fileTimestamps(absPath) {
378
399
  let updated = gitUpdated;
379
400
  if (!created || !updated) {
380
401
  try {
381
- const st = await fs4.stat(absPath);
402
+ const st = await fs5.stat(absPath);
382
403
  if (!updated) updated = st.mtime.toISOString();
383
404
  if (!created) created = isEpochish(st.birthtime) ? st.mtime.toISOString() : st.birthtime.toISOString();
384
405
  } catch {
@@ -402,7 +423,7 @@ async function detectRepoName(cwd) {
402
423
  }
403
424
  async function readRepoConfig(cwd) {
404
425
  try {
405
- const raw = await fs5.readFile(path4.join(cwd, REPO_CONFIG_FILE), "utf8");
426
+ const raw = await fs6.readFile(path4.join(cwd, REPO_CONFIG_FILE), "utf8");
406
427
  const parsed = parseYaml(raw) ?? {};
407
428
  if (!parsed.path) return void 0;
408
429
  return {
@@ -432,9 +453,9 @@ async function initRepoWiki(opts) {
432
453
  n += 1;
433
454
  }
434
455
  }
435
- await fs5.mkdir(absWiki, { recursive: true });
456
+ await fs6.mkdir(absWiki, { recursive: true });
436
457
  const config = { collection: name, path: wikiPath };
437
- await fs5.writeFile(path4.join(cwd, REPO_CONFIG_FILE), stringifyYaml(config), "utf8");
458
+ await fs6.writeFile(path4.join(cwd, REPO_CONFIG_FILE), stringifyYaml(config), "utf8");
438
459
  const entry = {
439
460
  name,
440
461
  type: "repo",
@@ -503,7 +524,7 @@ async function collectionGitStatus(entry) {
503
524
  }
504
525
 
505
526
  // src/project.ts
506
- import fs6 from "fs/promises";
527
+ import fs7 from "fs/promises";
507
528
  import os from "os";
508
529
  import path5 from "path";
509
530
  import { parse as parseYaml2, parseDocument } from "yaml";
@@ -560,7 +581,7 @@ function normaliseContext(raw) {
560
581
  async function readProjectConfig(dir) {
561
582
  let raw;
562
583
  try {
563
- raw = await fs6.readFile(path5.join(dir, PROJECT_CONFIG_FILE), "utf8");
584
+ raw = await fs7.readFile(path5.join(dir, PROJECT_CONFIG_FILE), "utf8");
564
585
  } catch (err) {
565
586
  if (err.code === "ENOENT") return void 0;
566
587
  throw err;
@@ -595,13 +616,13 @@ async function setContextCollections(dir, collections) {
595
616
  const file = path5.join(dir, PROJECT_CONFIG_FILE);
596
617
  let raw = "";
597
618
  try {
598
- raw = await fs6.readFile(file, "utf8");
619
+ raw = await fs7.readFile(file, "utf8");
599
620
  } catch (err) {
600
621
  if (err.code !== "ENOENT") throw err;
601
622
  }
602
623
  const doc = parseDocument(raw);
603
624
  doc.setIn(["context", "collections"], collections);
604
- await fs6.writeFile(file, doc.toString(), "utf8");
625
+ await fs7.writeFile(file, doc.toString(), "utf8");
605
626
  return file;
606
627
  }
607
628
  async function resolveProjectCollections(dir) {
@@ -624,8 +645,8 @@ async function seedProjectHome(dir) {
624
645
  if (entries.length === 0) {
625
646
  throw new Error(`No collections to render from diffwiki.yaml in ${path5.resolve(dir)}`);
626
647
  }
627
- const home = await fs6.mkdtemp(path5.join(os.tmpdir(), "diffwiki-project-"));
628
- await fs6.writeFile(
648
+ const home = await fs7.mkdtemp(path5.join(os.tmpdir(), "diffwiki-project-"));
649
+ await fs7.writeFile(
629
650
  path5.join(home, "registry.json"),
630
651
  `${JSON.stringify({ version: 1, collections: entries }, null, 2)}
631
652
  `,
@@ -635,7 +656,7 @@ async function seedProjectHome(dir) {
635
656
  }
636
657
 
637
658
  // src/browse.ts
638
- import fs7 from "fs/promises";
659
+ import fs8 from "fs/promises";
639
660
  import path6 from "path";
640
661
  var INDEX_RE = /^(index|readme)\.mdx?$/i;
641
662
  function buildArticleTree(relPaths) {
@@ -680,7 +701,7 @@ function buildArticleTree(relPaths) {
680
701
  async function collectRelPaths(dir, rel = "") {
681
702
  let dirents;
682
703
  try {
683
- dirents = await fs7.readdir(dir, { withFileTypes: true });
704
+ dirents = await fs8.readdir(dir, { withFileTypes: true });
684
705
  } catch {
685
706
  return [];
686
707
  }
@@ -698,7 +719,7 @@ async function collectRelPaths(dir, rel = "") {
698
719
  }
699
720
  async function readdirSafe(dir) {
700
721
  try {
701
- return await fs7.readdir(dir);
722
+ return await fs8.readdir(dir);
702
723
  } catch {
703
724
  return [];
704
725
  }
@@ -716,7 +737,7 @@ var CHEAP_META_BYTES = 8192;
716
737
  async function cheapMeta(absPath) {
717
738
  let handle;
718
739
  try {
719
- handle = await fs7.open(absPath, "r");
740
+ handle = await fs8.open(absPath, "r");
720
741
  const buf = Buffer.alloc(CHEAP_META_BYTES);
721
742
  const { bytesRead } = await handle.read(buf, 0, CHEAP_META_BYTES, 0);
722
743
  const snippet = buf.subarray(0, bytesRead).toString("utf8");
@@ -807,7 +828,7 @@ async function readArticle(coll, slug) {
807
828
  const candidate = path6.resolve(collDir, rel);
808
829
  if (!inJail(candidate)) throw new InvalidTargetError(slug, "path traversal detected");
809
830
  try {
810
- await fs7.access(candidate);
831
+ await fs8.access(candidate);
811
832
  abs = candidate;
812
833
  break;
813
834
  } catch {
@@ -824,7 +845,7 @@ async function readArticle(coll, slug) {
824
845
  }
825
846
  }
826
847
  if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
827
- const parsed = parseArticle(await fs7.readFile(abs, "utf8"));
848
+ const parsed = parseArticle(await fs8.readFile(abs, "utf8"));
828
849
  const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
829
850
  return {
830
851
  coll,
@@ -1268,13 +1289,13 @@ function resolveCollectionSelection(all, sel = {}) {
1268
1289
  }
1269
1290
 
1270
1291
  // src/query.ts
1271
- import fs8 from "fs/promises";
1292
+ import fs9 from "fs/promises";
1272
1293
  import path8 from "path";
1273
1294
  var log2 = getLogger("query");
1274
1295
  async function collectMarkdownFiles(dir) {
1275
1296
  let entries;
1276
1297
  try {
1277
- entries = await fs8.readdir(dir, { withFileTypes: true });
1298
+ entries = await fs9.readdir(dir, { withFileTypes: true });
1278
1299
  } catch {
1279
1300
  return [];
1280
1301
  }
@@ -1312,7 +1333,7 @@ async function nativeSearch(term, collections, collection, hideSystem = false) {
1312
1333
  let tags = [];
1313
1334
  let body = "";
1314
1335
  try {
1315
- const parsed = parseArticle(await fs8.readFile(full, "utf8"));
1336
+ const parsed = parseArticle(await fs9.readFile(full, "utf8"));
1316
1337
  if (hideSystem && parsed.audience === "system") continue;
1317
1338
  if (parsed.title) title = parsed.title;
1318
1339
  tags = parsed.tags;
@@ -1370,7 +1391,7 @@ async function dropSystemHits(hits) {
1370
1391
  const kept = [];
1371
1392
  for (const hit of hits) {
1372
1393
  try {
1373
- if (parseArticle(await fs8.readFile(hit.path, "utf8")).audience === "system") continue;
1394
+ if (parseArticle(await fs9.readFile(hit.path, "utf8")).audience === "system") continue;
1374
1395
  } catch {
1375
1396
  }
1376
1397
  kept.push(hit);
@@ -1418,13 +1439,13 @@ async function query(term, opts) {
1418
1439
  }
1419
1440
 
1420
1441
  // src/search.ts
1421
- import fs9 from "fs/promises";
1442
+ import fs10 from "fs/promises";
1422
1443
  import path9 from "path";
1423
1444
  var isStatic2 = () => process.env.DIFFWIKI_STATIC === "1";
1424
1445
  async function readArticleParts(collDir, slug) {
1425
1446
  for (const ext of [".mdx", ".md"]) {
1426
1447
  try {
1427
- const parsed = parseArticle(await fs9.readFile(path9.join(collDir, `${slug}${ext}`), "utf8"));
1448
+ const parsed = parseArticle(await fs10.readFile(path9.join(collDir, `${slug}${ext}`), "utf8"));
1428
1449
  return { tags: parsed.tags, body: parsed.body };
1429
1450
  } catch {
1430
1451
  }
@@ -1474,15 +1495,15 @@ async function buildSearchIndex(collection) {
1474
1495
  }
1475
1496
 
1476
1497
  // src/doctor.ts
1477
- import fs11 from "fs/promises";
1498
+ import fs12 from "fs/promises";
1478
1499
 
1479
1500
  // src/plugins/manager.ts
1480
- import fs10 from "fs/promises";
1501
+ import fs11 from "fs/promises";
1481
1502
  import os2 from "os";
1482
1503
  import path10 from "path";
1483
1504
  async function readPackageJson(file) {
1484
1505
  try {
1485
- return JSON.parse(await fs10.readFile(file, "utf8"));
1506
+ return JSON.parse(await fs11.readFile(file, "utf8"));
1486
1507
  } catch {
1487
1508
  return null;
1488
1509
  }
@@ -1498,7 +1519,7 @@ function binOf(manifest) {
1498
1519
  async function pluginManifestDirs(nodeModules) {
1499
1520
  let entries;
1500
1521
  try {
1501
- entries = await fs10.readdir(nodeModules);
1522
+ entries = await fs11.readdir(nodeModules);
1502
1523
  } catch {
1503
1524
  return [];
1504
1525
  }
@@ -1508,7 +1529,7 @@ async function pluginManifestDirs(nodeModules) {
1508
1529
  if (entry.startsWith("@")) {
1509
1530
  let scoped;
1510
1531
  try {
1511
- scoped = await fs10.readdir(path10.join(nodeModules, entry));
1532
+ scoped = await fs11.readdir(path10.join(nodeModules, entry));
1512
1533
  } catch {
1513
1534
  continue;
1514
1535
  }
@@ -1524,11 +1545,11 @@ async function pluginManifestDirs(nodeModules) {
1524
1545
  }
1525
1546
  async function ensurePluginRoot() {
1526
1547
  const dir = pluginsDir();
1527
- await fs10.mkdir(dir, { recursive: true });
1548
+ await fs11.mkdir(dir, { recursive: true });
1528
1549
  const pkgPath = path10.join(dir, "package.json");
1529
1550
  if (await readPackageJson(pkgPath) === null) {
1530
1551
  const pkg = { name: "diffwiki-plugins", private: true, version: "0.0.0" };
1531
- await fs10.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
1552
+ await fs11.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
1532
1553
  `, "utf8");
1533
1554
  }
1534
1555
  return dir;
@@ -1697,7 +1718,7 @@ async function listPlugins() {
1697
1718
  // src/doctor.ts
1698
1719
  async function exists(target) {
1699
1720
  try {
1700
- await fs11.access(target);
1721
+ await fs12.access(target);
1701
1722
  return true;
1702
1723
  } catch {
1703
1724
  return false;
@@ -1803,7 +1824,7 @@ async function appendPluginDiagnostics(out, config) {
1803
1824
  }
1804
1825
 
1805
1826
  // src/lint.ts
1806
- import fs12 from "fs/promises";
1827
+ import fs13 from "fs/promises";
1807
1828
  import path11 from "path";
1808
1829
  var BUDGETS = {
1809
1830
  recipe: { min: 50, max: 1e3 },
@@ -1896,7 +1917,7 @@ function lintArticle(article) {
1896
1917
  async function walkMarkdown(dir) {
1897
1918
  let entries;
1898
1919
  try {
1899
- entries = await fs12.readdir(dir, { withFileTypes: true });
1920
+ entries = await fs13.readdir(dir, { withFileTypes: true });
1900
1921
  } catch {
1901
1922
  return [];
1902
1923
  }
@@ -1936,7 +1957,7 @@ async function lintCollections(scope) {
1936
1957
  const label = `${coll.name}/${rel}`;
1937
1958
  let findings = [];
1938
1959
  try {
1939
- const parsed = parseArticle(await fs12.readFile(full, "utf8"));
1960
+ const parsed = parseArticle(await fs13.readFile(full, "utf8"));
1940
1961
  findings = lintArticle(parsed);
1941
1962
  } catch (err) {
1942
1963
  findings = [{ level: "error", rule: "parse", message: `could not parse: ${err.message}` }];
@@ -2028,6 +2049,7 @@ export {
2028
2049
  doctor,
2029
2050
  emitPluginEvent,
2030
2051
  ensureGitRepo,
2052
+ ensureHome,
2031
2053
  ensurePluginRoot,
2032
2054
  fileTimestamps,
2033
2055
  findCollection,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffwiki-core",
3
- "version": "0.17.0",
3
+ "version": "0.17.1-rc.202608210907.d3afab7",
4
4
  "description": "Core configuration, registry, and article management for diffwiki",
5
5
  "license": "MIT",
6
6
  "type": "module",