diffwiki-core 0.8.0 → 0.9.0-rc.202607291540.2b773e4

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
@@ -570,6 +570,7 @@ __export(index_exports, {
570
570
  InvalidTargetError: () => InvalidTargetError,
571
571
  NATIVE_ENGINE: () => NATIVE_ENGINE,
572
572
  NoDefaultCollectionError: () => NoDefaultCollectionError,
573
+ PROJECT_CONFIG_FILE: () => PROJECT_CONFIG_FILE,
573
574
  PluginError: () => PluginError,
574
575
  REPO_CONFIG_FILE: () => REPO_CONFIG_FILE,
575
576
  WorktreeNotAllowedError: () => WorktreeNotAllowedError,
@@ -580,6 +581,7 @@ __export(index_exports, {
580
581
  buildLinkGraph: () => buildLinkGraph,
581
582
  buildNavTree: () => buildNavTree,
582
583
  buildSearchIndex: () => buildSearchIndex,
584
+ buildSiteConfig: () => buildSiteConfig,
583
585
  collectionDir: () => collectionDir,
584
586
  collectionGitStatus: () => collectionGitStatus,
585
587
  collectionMeta: () => collectionMeta,
@@ -624,8 +626,10 @@ __export(index_exports, {
624
626
  readArticle: () => readArticle,
625
627
  readConfig: () => readConfig,
626
628
  readPluginRegistry: () => readPluginRegistry,
629
+ readProjectConfig: () => readProjectConfig,
627
630
  readRegistry: () => readRegistry,
628
631
  readRepoConfig: () => readRepoConfig,
632
+ readSiteConfig: () => readSiteConfig,
629
633
  registerCollection: () => registerCollection,
630
634
  registerPlugin: () => registerPlugin,
631
635
  registryPath: () => registryPath,
@@ -639,10 +643,14 @@ __export(index_exports, {
639
643
  resolveCollectionSelection: () => resolveCollectionSelection,
640
644
  resolveCwdCollection: () => resolveCwdCollection,
641
645
  resolveDefaultSearchMode: () => resolveDefaultSearchMode,
646
+ resolveEditTarget: () => resolveEditTarget,
647
+ resolveEditorCommand: () => resolveEditorCommand,
642
648
  resolveHome: () => resolveHome,
643
649
  resolveLogLevel: () => resolveLogLevel,
644
650
  resolvePage: () => resolvePage,
651
+ resolveProjectCollections: () => resolveProjectCollections,
645
652
  search: () => search,
653
+ seedProjectHome: () => seedProjectHome,
646
654
  serializeArticle: () => serializeArticle,
647
655
  setConfigValue: () => setConfigValue,
648
656
  setTags: () => setTags,
@@ -1127,12 +1135,120 @@ async function collectionGitStatus(entry) {
1127
1135
  return { branch: s.branch, ahead: s.ahead, behind: s.behind, hasUpstream: s.hasUpstream };
1128
1136
  }
1129
1137
 
1138
+ // src/project.ts
1139
+ var import_promises8 = __toESM(require("fs/promises"), 1);
1140
+ var import_node_os2 = __toESM(require("os"), 1);
1141
+ var import_node_path6 = __toESM(require("path"), 1);
1142
+ var import_yaml3 = require("yaml");
1143
+ var PROJECT_CONFIG_FILE = "diffwiki.yaml";
1144
+ function normaliseSite(raw) {
1145
+ if (!raw || typeof raw !== "object") return void 0;
1146
+ const r = raw;
1147
+ const repo = typeof r.repo === "string" ? r.repo : void 0;
1148
+ let copyright;
1149
+ if (typeof r.copyright === "string") {
1150
+ copyright = { text: r.copyright };
1151
+ } else if (r.copyright && typeof r.copyright === "object") {
1152
+ const c = r.copyright;
1153
+ if (typeof c.text === "string") {
1154
+ copyright = { text: c.text, ...typeof c.url === "string" ? { url: c.url } : {} };
1155
+ }
1156
+ }
1157
+ let home;
1158
+ if (r.home && typeof r.home === "object") {
1159
+ const h = r.home;
1160
+ const tagline = typeof h.tagline === "string" ? h.tagline : void 0;
1161
+ const cards = [];
1162
+ if (Array.isArray(h.cards)) {
1163
+ for (const c of h.cards) {
1164
+ if (!c || typeof c !== "object") continue;
1165
+ const cc = c;
1166
+ if (typeof cc.title === "string" && typeof cc.link === "string") {
1167
+ cards.push({
1168
+ title: cc.title,
1169
+ link: cc.link,
1170
+ ...typeof cc.icon === "string" ? { icon: cc.icon } : {},
1171
+ ...typeof cc.subtitle === "string" ? { subtitle: cc.subtitle } : {}
1172
+ });
1173
+ }
1174
+ }
1175
+ }
1176
+ home = { ...tagline !== void 0 ? { tagline } : {}, ...cards.length > 0 ? { cards } : {} };
1177
+ if (Object.keys(home).length === 0) home = void 0;
1178
+ }
1179
+ if (repo === void 0 && copyright === void 0 && home === void 0) return void 0;
1180
+ return {
1181
+ ...repo !== void 0 ? { repo } : {},
1182
+ ...copyright !== void 0 ? { copyright } : {},
1183
+ ...home !== void 0 ? { home } : {}
1184
+ };
1185
+ }
1186
+ async function readProjectConfig(dir) {
1187
+ let raw;
1188
+ try {
1189
+ raw = await import_promises8.default.readFile(import_node_path6.default.join(dir, PROJECT_CONFIG_FILE), "utf8");
1190
+ } catch (err) {
1191
+ if (err.code === "ENOENT") return void 0;
1192
+ throw err;
1193
+ }
1194
+ const parsed = (0, import_yaml3.parse)(raw) ?? {};
1195
+ const collections = [];
1196
+ if (Array.isArray(parsed.collections)) {
1197
+ for (const c of parsed.collections) {
1198
+ if (!c || typeof c !== "object") continue;
1199
+ const cc = c;
1200
+ if (typeof cc.name === "string" && typeof cc.path === "string") {
1201
+ collections.push({
1202
+ name: cc.name,
1203
+ path: cc.path,
1204
+ ...typeof cc.order === "number" ? { order: cc.order } : {}
1205
+ });
1206
+ }
1207
+ }
1208
+ collections.sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
1209
+ } else if (typeof parsed.path === "string") {
1210
+ const name = typeof parsed.collection === "string" ? parsed.collection : import_node_path6.default.basename(import_node_path6.default.resolve(dir));
1211
+ collections.push({ name, path: parsed.path });
1212
+ }
1213
+ const site = normaliseSite(parsed.site);
1214
+ return { ...site ? { site } : {}, collections };
1215
+ }
1216
+ async function resolveProjectCollections(dir) {
1217
+ const config = await readProjectConfig(dir);
1218
+ if (!config) return [];
1219
+ const root = import_node_path6.default.resolve(dir);
1220
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
1221
+ return config.collections.map((c) => ({
1222
+ name: c.name,
1223
+ type: "repo",
1224
+ path: import_node_path6.default.resolve(root, c.path),
1225
+ createdAt
1226
+ }));
1227
+ }
1228
+ async function readSiteConfig(dir) {
1229
+ return (await readProjectConfig(dir))?.site ?? {};
1230
+ }
1231
+ async function seedProjectHome(dir) {
1232
+ const entries = await resolveProjectCollections(dir);
1233
+ if (entries.length === 0) {
1234
+ throw new Error(`No collections to render from diffwiki.yaml in ${import_node_path6.default.resolve(dir)}`);
1235
+ }
1236
+ const home = await import_promises8.default.mkdtemp(import_node_path6.default.join(import_node_os2.default.tmpdir(), "diffwiki-project-"));
1237
+ await import_promises8.default.writeFile(
1238
+ import_node_path6.default.join(home, "registry.json"),
1239
+ `${JSON.stringify({ version: 1, collections: entries }, null, 2)}
1240
+ `,
1241
+ "utf8"
1242
+ );
1243
+ return { home, entries };
1244
+ }
1245
+
1130
1246
  // src/site.ts
1131
1247
  init_collections();
1132
1248
 
1133
1249
  // src/browse.ts
1134
- var import_promises8 = __toESM(require("fs/promises"), 1);
1135
- var import_node_path6 = __toESM(require("path"), 1);
1250
+ var import_promises9 = __toESM(require("fs/promises"), 1);
1251
+ var import_node_path7 = __toESM(require("path"), 1);
1136
1252
  init_collections();
1137
1253
  init_errors();
1138
1254
  var INDEX_RE = /^(index|readme)\.mdx?$/i;
@@ -1178,7 +1294,7 @@ function buildArticleTree(relPaths) {
1178
1294
  async function collectRelPaths(dir, rel = "") {
1179
1295
  let dirents;
1180
1296
  try {
1181
- dirents = await import_promises8.default.readdir(dir, { withFileTypes: true });
1297
+ dirents = await import_promises9.default.readdir(dir, { withFileTypes: true });
1182
1298
  } catch {
1183
1299
  return [];
1184
1300
  }
@@ -1187,7 +1303,7 @@ async function collectRelPaths(dir, rel = "") {
1187
1303
  if (d.name.startsWith(".")) continue;
1188
1304
  const childRel = rel ? `${rel}/${d.name}` : d.name;
1189
1305
  if (d.isDirectory()) {
1190
- out.push(...await collectRelPaths(import_node_path6.default.join(dir, d.name), childRel));
1306
+ out.push(...await collectRelPaths(import_node_path7.default.join(dir, d.name), childRel));
1191
1307
  } else if (/\.(mdx?)$/.test(d.name)) {
1192
1308
  out.push(childRel);
1193
1309
  }
@@ -1196,7 +1312,7 @@ async function collectRelPaths(dir, rel = "") {
1196
1312
  }
1197
1313
  async function readdirSafe(dir) {
1198
1314
  try {
1199
- return await import_promises8.default.readdir(dir);
1315
+ return await import_promises9.default.readdir(dir);
1200
1316
  } catch {
1201
1317
  return [];
1202
1318
  }
@@ -1214,7 +1330,7 @@ var CHEAP_META_BYTES = 8192;
1214
1330
  async function cheapMeta(absPath) {
1215
1331
  let handle;
1216
1332
  try {
1217
- handle = await import_promises8.default.open(absPath, "r");
1333
+ handle = await import_promises9.default.open(absPath, "r");
1218
1334
  const buf = Buffer.alloc(CHEAP_META_BYTES);
1219
1335
  const { bytesRead } = await handle.read(buf, 0, CHEAP_META_BYTES, 0);
1220
1336
  const snippet = buf.subarray(0, bytesRead).toString("utf8");
@@ -1225,10 +1341,13 @@ async function cheapMeta(absPath) {
1225
1341
  const titleM = /^title:\s*(.+)$/m.exec(inner);
1226
1342
  const audienceM = /^audience:\s*(.+)$/m.exec(inner);
1227
1343
  const statusM = /^status:\s*(.+)$/m.exec(inner);
1344
+ const orderM = /^order:\s*(.+)$/m.exec(inner);
1228
1345
  const title = titleM ? clean(titleM[1]) : void 0;
1229
1346
  const audience = audienceM && clean(audienceM[1]) === "private" ? "private" : void 0;
1230
1347
  const status = statusM && clean(statusM[1]) === "draft" ? "draft" : void 0;
1231
- return { title, audience, status };
1348
+ const orderNum = orderM ? Number(clean(orderM[1])) : NaN;
1349
+ const order = Number.isFinite(orderNum) ? orderNum : void 0;
1350
+ return { title, audience, status, order };
1232
1351
  } catch {
1233
1352
  } finally {
1234
1353
  await handle?.close();
@@ -1236,86 +1355,90 @@ async function cheapMeta(absPath) {
1236
1355
  return {};
1237
1356
  }
1238
1357
  async function enrichTitles(nodes, collDir) {
1239
- return Promise.all(
1358
+ const enriched = await Promise.all(
1240
1359
  nodes.map(async (node) => {
1241
1360
  if (node.children) {
1242
1361
  const children = await enrichTitles(node.children, collDir);
1243
1362
  let title = node.title;
1244
1363
  let audience = node.audience;
1245
1364
  let status = node.status;
1365
+ let order = node.order;
1246
1366
  if (node.slug) {
1247
- const dir = import_node_path6.default.join(collDir, node.slug);
1367
+ const dir = import_node_path7.default.join(collDir, node.slug);
1248
1368
  const idx = pickIndexFile(await readdirSafe(dir));
1249
1369
  if (idx) {
1250
- const meta = await cheapMeta(import_node_path6.default.join(dir, idx));
1370
+ const meta = await cheapMeta(import_node_path7.default.join(dir, idx));
1251
1371
  title = meta.title ?? title;
1252
1372
  audience = meta.audience ?? audience;
1253
1373
  status = meta.status ?? status;
1374
+ order = meta.order ?? order;
1254
1375
  }
1255
1376
  }
1256
- return { ...node, title, audience, status, children };
1377
+ return { ...node, title, audience, status, order, children };
1257
1378
  }
1258
1379
  for (const ext of [".mdx", ".md"]) {
1259
- const meta = await cheapMeta(import_node_path6.default.join(collDir, `${node.slug}${ext}`));
1260
- if (meta.title || meta.audience || meta.status) {
1380
+ const meta = await cheapMeta(import_node_path7.default.join(collDir, `${node.slug}${ext}`));
1381
+ if (meta.title || meta.audience || meta.status || meta.order != null) {
1261
1382
  return {
1262
1383
  ...node,
1263
1384
  title: meta.title ?? node.title,
1264
1385
  audience: meta.audience ?? node.audience,
1265
- status: meta.status ?? node.status
1386
+ status: meta.status ?? node.status,
1387
+ order: meta.order ?? node.order
1266
1388
  };
1267
1389
  }
1268
1390
  }
1269
1391
  return node;
1270
1392
  })
1271
1393
  );
1394
+ return enriched.sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
1272
1395
  }
1273
1396
  async function collectionMeta(collDir) {
1274
1397
  const idx = pickIndexFile(await readdirSafe(collDir));
1275
1398
  if (!idx) return {};
1276
- const meta = await cheapMeta(import_node_path6.default.join(collDir, idx));
1277
- return { audience: meta.audience, status: meta.status };
1399
+ const meta = await cheapMeta(import_node_path7.default.join(collDir, idx));
1400
+ return { title: meta.title, audience: meta.audience, status: meta.status };
1278
1401
  }
1279
1402
  async function listArticleTree(coll) {
1280
1403
  const entry = await findCollection(coll);
1281
1404
  if (!entry) throw new CollectionNotFoundError(coll);
1282
1405
  const rels = await collectRelPaths(entry.path);
1283
- return enrichTitles(buildArticleTree(rels), import_node_path6.default.resolve(entry.path));
1406
+ return enrichTitles(buildArticleTree(rels), import_node_path7.default.resolve(entry.path));
1284
1407
  }
1285
1408
  async function readArticle(coll, slug) {
1286
1409
  const entry = await findCollection(coll);
1287
1410
  if (!entry) throw new CollectionNotFoundError(coll);
1288
- const collDir = import_node_path6.default.resolve(entry.path);
1411
+ const collDir = import_node_path7.default.resolve(entry.path);
1289
1412
  const normalized = slug.replace(/\\/g, "/");
1290
1413
  if (normalized.split("/").some((s) => s === "..")) {
1291
1414
  throw new InvalidTargetError(slug, "path traversal detected");
1292
1415
  }
1293
1416
  const safeSlug = normalized.split("/").filter((s) => s !== "" && s !== ".").join("/");
1294
- const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path6.default.sep);
1417
+ const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path7.default.sep);
1295
1418
  let abs;
1296
1419
  let isIndex = false;
1297
1420
  for (const rel of safeSlug ? [`${safeSlug}.mdx`, `${safeSlug}.md`] : []) {
1298
- const candidate = import_node_path6.default.resolve(collDir, rel);
1421
+ const candidate = import_node_path7.default.resolve(collDir, rel);
1299
1422
  if (!inJail(candidate)) throw new InvalidTargetError(slug, "path traversal detected");
1300
1423
  try {
1301
- await import_promises8.default.access(candidate);
1424
+ await import_promises9.default.access(candidate);
1302
1425
  abs = candidate;
1303
1426
  break;
1304
1427
  } catch {
1305
1428
  }
1306
1429
  }
1307
1430
  if (!abs) {
1308
- const dir = import_node_path6.default.resolve(collDir, safeSlug);
1431
+ const dir = import_node_path7.default.resolve(collDir, safeSlug);
1309
1432
  if (inJail(dir)) {
1310
1433
  const idx = pickIndexFile(await readdirSafe(dir));
1311
1434
  if (idx) {
1312
- abs = import_node_path6.default.join(dir, idx);
1435
+ abs = import_node_path7.default.join(dir, idx);
1313
1436
  isIndex = true;
1314
1437
  }
1315
1438
  }
1316
1439
  }
1317
1440
  if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
1318
- const parsed = parseArticle(await import_promises8.default.readFile(abs, "utf8"));
1441
+ const parsed = parseArticle(await import_promises9.default.readFile(abs, "utf8"));
1319
1442
  const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
1320
1443
  return {
1321
1444
  coll,
@@ -1331,7 +1454,7 @@ async function readArticle(coll, slug) {
1331
1454
  }
1332
1455
 
1333
1456
  // src/render.ts
1334
- var import_node_path7 = __toESM(require("path"), 1);
1457
+ var import_node_path8 = __toESM(require("path"), 1);
1335
1458
  var import_unified = require("unified");
1336
1459
  var import_remark_parse = __toESM(require("remark-parse"), 1);
1337
1460
  var import_remark_gfm = __toESM(require("remark-gfm"), 1);
@@ -1348,7 +1471,7 @@ function isNonRelativeHref(href) {
1348
1471
  return /^[a-z][a-z0-9+.-]*:/i.test(href);
1349
1472
  }
1350
1473
  function rehypeRewriteLinks(ctx) {
1351
- const slugDir = ctx.isIndex ? ctx.slug || "." : import_node_path7.default.posix.dirname(ctx.slug || ".");
1474
+ const slugDir = ctx.isIndex ? ctx.slug || "." : import_node_path8.default.posix.dirname(ctx.slug || ".");
1352
1475
  return (tree) => {
1353
1476
  (0, import_unist_util_visit.visit)(tree, "element", (node) => {
1354
1477
  if (node.tagName !== "a") return;
@@ -1358,8 +1481,8 @@ function rehypeRewriteLinks(ctx) {
1358
1481
  const suffix = suffixMatch ? suffixMatch[0] : "";
1359
1482
  const pathPart = suffix ? raw.slice(0, raw.length - suffix.length) : raw;
1360
1483
  if (pathPart === "") return;
1361
- const joined = import_node_path7.default.posix.join(slugDir === "." ? "" : slugDir, pathPart);
1362
- const normalized = import_node_path7.default.posix.normalize(joined);
1484
+ const joined = import_node_path8.default.posix.join(slugDir === "." ? "" : slugDir, pathPart);
1485
+ const normalized = import_node_path8.default.posix.normalize(joined);
1363
1486
  const toRoute = (p) => p.replace(/\.(mdx?)$/i, "").replace(/(^|\/)(index|readme)$/i, "$1").replace(/\/+$/, "").replace(/^\/+/, "");
1364
1487
  if (normalized.startsWith("../")) {
1365
1488
  const sibling = normalized.slice(3);
@@ -1477,6 +1600,10 @@ async function renderArticle(body, ctx) {
1477
1600
  themes: { light: "github-light", dark: "github-dark" },
1478
1601
  defaultColor: false,
1479
1602
  langAlias: LANG_ALIASES,
1603
+ // `defaultLanguage` styles fences with no language, `fallbackLanguage` those
1604
+ // with an unknown one — both as plaintext, so every code block renders as a
1605
+ // themed `pre.shiki` (light background + copy button), never a bare dark <pre>.
1606
+ defaultLanguage: "plaintext",
1480
1607
  fallbackLanguage: "plaintext"
1481
1608
  }).use(import_rehype_stringify.default).process(body);
1482
1609
  return { html: String(file), toc, links };
@@ -1613,11 +1740,12 @@ async function buildCollections() {
1613
1740
  const entries = await listCollections();
1614
1741
  const infos = await Promise.all(
1615
1742
  entries.map(async (e) => {
1616
- const { audience, status } = await collectionMeta(e.path);
1743
+ const { title, audience, status } = await collectionMeta(e.path);
1617
1744
  return {
1618
1745
  name: e.name,
1619
1746
  type: e.type,
1620
1747
  path: e.path,
1748
+ ...title ? { title } : {},
1621
1749
  ...await gitInfo(e),
1622
1750
  ...audience ? { audience } : {},
1623
1751
  ...status ? { status } : {}
@@ -1630,12 +1758,13 @@ async function buildNavTree() {
1630
1758
  const entries = await listCollections();
1631
1759
  const trees = await Promise.all(
1632
1760
  entries.map(async (e) => {
1633
- const { audience, status } = await collectionMeta(e.path);
1761
+ const { title, audience, status } = await collectionMeta(e.path);
1634
1762
  return {
1635
1763
  collection: {
1636
1764
  name: e.name,
1637
1765
  type: e.type,
1638
1766
  path: e.path,
1767
+ ...title ? { title } : {},
1639
1768
  ...await gitInfo(e),
1640
1769
  ...audience ? { audience } : {},
1641
1770
  ...status ? { status } : {}
@@ -1708,6 +1837,15 @@ async function resolvePage(coll, slug, opts) {
1708
1837
  };
1709
1838
  }
1710
1839
  }
1840
+ async function buildSiteConfig() {
1841
+ const dir = process.env.DIFFWIKI_PROJECT_DIR;
1842
+ if (!dir) return {};
1843
+ try {
1844
+ return await readSiteConfig(dir);
1845
+ } catch {
1846
+ return {};
1847
+ }
1848
+ }
1711
1849
 
1712
1850
  // src/select.ts
1713
1851
  function globToRegExp(glob) {
@@ -1737,8 +1875,8 @@ function resolveCollectionSelection(all, sel = {}) {
1737
1875
  }
1738
1876
 
1739
1877
  // src/query.ts
1740
- var import_promises9 = __toESM(require("fs/promises"), 1);
1741
- var import_node_path8 = __toESM(require("path"), 1);
1878
+ var import_promises10 = __toESM(require("fs/promises"), 1);
1879
+ var import_node_path9 = __toESM(require("path"), 1);
1742
1880
  init_collections();
1743
1881
  init_errors();
1744
1882
  init_logger();
@@ -1807,13 +1945,13 @@ var log2 = getLogger("query");
1807
1945
  async function collectMarkdownFiles(dir) {
1808
1946
  let entries;
1809
1947
  try {
1810
- entries = await import_promises9.default.readdir(dir, { withFileTypes: true });
1948
+ entries = await import_promises10.default.readdir(dir, { withFileTypes: true });
1811
1949
  } catch {
1812
1950
  return [];
1813
1951
  }
1814
1952
  const files = [];
1815
1953
  for (const entry of entries) {
1816
- const full = import_node_path8.default.join(dir, entry.name);
1954
+ const full = import_node_path9.default.join(dir, entry.name);
1817
1955
  if (entry.isDirectory()) {
1818
1956
  files.push(...await collectMarkdownFiles(full));
1819
1957
  } else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
@@ -1832,12 +1970,12 @@ async function nativeSearch(term, collections, collection) {
1832
1970
  const docs = [];
1833
1971
  for (const entry of targets) {
1834
1972
  for (const full of await collectMarkdownFiles(entry.path)) {
1835
- const fallbackTitle = import_node_path8.default.basename(full).replace(/\.mdx?$/, "");
1973
+ const fallbackTitle = import_node_path9.default.basename(full).replace(/\.mdx?$/, "");
1836
1974
  let title = fallbackTitle;
1837
1975
  let tags = [];
1838
1976
  let body = "";
1839
1977
  try {
1840
- const parsed = parseArticle(await import_promises9.default.readFile(full, "utf8"));
1978
+ const parsed = parseArticle(await import_promises10.default.readFile(full, "utf8"));
1841
1979
  if (parsed.title) title = parsed.title;
1842
1980
  tags = parsed.tags;
1843
1981
  body = parsed.body;
@@ -1920,14 +2058,14 @@ async function query(term, opts) {
1920
2058
  }
1921
2059
 
1922
2060
  // src/search.ts
1923
- var import_promises10 = __toESM(require("fs/promises"), 1);
1924
- var import_node_path9 = __toESM(require("path"), 1);
2061
+ var import_promises11 = __toESM(require("fs/promises"), 1);
2062
+ var import_node_path10 = __toESM(require("path"), 1);
1925
2063
  init_collections();
1926
2064
  var isStatic2 = () => process.env.DIFFWIKI_STATIC === "1";
1927
2065
  async function readArticleParts(collDir, slug) {
1928
2066
  for (const ext of [".mdx", ".md"]) {
1929
2067
  try {
1930
- const parsed = parseArticle(await import_promises10.default.readFile(import_node_path9.default.join(collDir, `${slug}${ext}`), "utf8"));
2068
+ const parsed = parseArticle(await import_promises11.default.readFile(import_node_path10.default.join(collDir, `${slug}${ext}`), "utf8"));
1931
2069
  return { tags: parsed.tags, body: parsed.body };
1932
2070
  } catch {
1933
2071
  }
@@ -1962,7 +2100,7 @@ async function buildSearchIndex(collection) {
1962
2100
  for (const e of entries) {
1963
2101
  try {
1964
2102
  const collEntry = await findCollection(e.name);
1965
- const collDir = import_node_path9.default.resolve(collEntry?.path ?? e.path);
2103
+ const collDir = import_node_path10.default.resolve(collEntry?.path ?? e.path);
1966
2104
  if (isStatic2()) {
1967
2105
  const cm = await collectionMeta(collDir);
1968
2106
  if (cm.audience === "private" || cm.status === "draft") continue;
@@ -1975,15 +2113,15 @@ async function buildSearchIndex(collection) {
1975
2113
  }
1976
2114
 
1977
2115
  // src/doctor.ts
1978
- var import_promises12 = __toESM(require("fs/promises"), 1);
2116
+ var import_promises13 = __toESM(require("fs/promises"), 1);
1979
2117
  init_paths();
1980
2118
  init_registry();
1981
2119
  init_collections();
1982
2120
 
1983
2121
  // src/plugins/manager.ts
1984
- var import_promises11 = __toESM(require("fs/promises"), 1);
1985
- var import_node_os2 = __toESM(require("os"), 1);
1986
- var import_node_path11 = __toESM(require("path"), 1);
2122
+ var import_promises12 = __toESM(require("fs/promises"), 1);
2123
+ var import_node_os3 = __toESM(require("os"), 1);
2124
+ var import_node_path12 = __toESM(require("path"), 1);
1987
2125
  init_paths();
1988
2126
  init_errors();
1989
2127
  init_collections();
@@ -1994,7 +2132,7 @@ init_host();
1994
2132
  var import_node_child_process4 = require("child_process");
1995
2133
  var import_node_util3 = require("util");
1996
2134
  var import_node_fs = require("fs");
1997
- var import_node_path10 = require("path");
2135
+ var import_node_path11 = require("path");
1998
2136
  var import_node_url = require("url");
1999
2137
  var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process4.execFile);
2000
2138
  async function runProcess(file, args, opts) {
@@ -2010,7 +2148,7 @@ async function runProcess(file, args, opts) {
2010
2148
  // src/plugins/manager.ts
2011
2149
  async function readPackageJson(file) {
2012
2150
  try {
2013
- return JSON.parse(await import_promises11.default.readFile(file, "utf8"));
2151
+ return JSON.parse(await import_promises12.default.readFile(file, "utf8"));
2014
2152
  } catch {
2015
2153
  return null;
2016
2154
  }
@@ -2026,7 +2164,7 @@ function binOf(manifest) {
2026
2164
  async function pluginManifestDirs(nodeModules) {
2027
2165
  let entries;
2028
2166
  try {
2029
- entries = await import_promises11.default.readdir(nodeModules);
2167
+ entries = await import_promises12.default.readdir(nodeModules);
2030
2168
  } catch {
2031
2169
  return [];
2032
2170
  }
@@ -2036,44 +2174,44 @@ async function pluginManifestDirs(nodeModules) {
2036
2174
  if (entry.startsWith("@")) {
2037
2175
  let scoped;
2038
2176
  try {
2039
- scoped = await import_promises11.default.readdir(import_node_path11.default.join(nodeModules, entry));
2177
+ scoped = await import_promises12.default.readdir(import_node_path12.default.join(nodeModules, entry));
2040
2178
  } catch {
2041
2179
  continue;
2042
2180
  }
2043
2181
  for (const pkg of scoped) {
2044
2182
  if (pkg.startsWith(".")) continue;
2045
- dirs.push(import_node_path11.default.join(nodeModules, entry, pkg));
2183
+ dirs.push(import_node_path12.default.join(nodeModules, entry, pkg));
2046
2184
  }
2047
2185
  } else {
2048
- dirs.push(import_node_path11.default.join(nodeModules, entry));
2186
+ dirs.push(import_node_path12.default.join(nodeModules, entry));
2049
2187
  }
2050
2188
  }
2051
2189
  return dirs;
2052
2190
  }
2053
2191
  async function ensurePluginRoot() {
2054
2192
  const dir = pluginsDir();
2055
- await import_promises11.default.mkdir(dir, { recursive: true });
2056
- const pkgPath = import_node_path11.default.join(dir, "package.json");
2193
+ await import_promises12.default.mkdir(dir, { recursive: true });
2194
+ const pkgPath = import_node_path12.default.join(dir, "package.json");
2057
2195
  if (await readPackageJson(pkgPath) === null) {
2058
2196
  const pkg = { name: "diffwiki-plugins", private: true, version: "0.0.0" };
2059
- await import_promises11.default.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
2197
+ await import_promises12.default.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
2060
2198
  `, "utf8");
2061
2199
  }
2062
2200
  return dir;
2063
2201
  }
2064
2202
  async function asSearchPackage(pkgDir) {
2065
- const manifest = await readPackageJson(import_node_path11.default.join(pkgDir, "package.json"));
2203
+ const manifest = await readPackageJson(import_node_path12.default.join(pkgDir, "package.json"));
2066
2204
  if (!manifest?.name || manifest.diffwiki?.kind !== "search") return null;
2067
2205
  const bin = binOf(manifest);
2068
2206
  if (!bin) return null;
2069
- return { name: manifest.name, version: manifest.version, binAbs: import_node_path11.default.resolve(pkgDir, bin) };
2207
+ return { name: manifest.name, version: manifest.version, binAbs: import_node_path12.default.resolve(pkgDir, bin) };
2070
2208
  }
2071
2209
  async function packageNameFromSpec(spec) {
2072
2210
  const looksLikePath = spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~") || spec.startsWith("file:");
2073
2211
  if (looksLikePath) {
2074
2212
  const raw = spec.startsWith("file:") ? spec.slice("file:".length) : spec;
2075
- const abs = import_node_path11.default.resolve(raw.replace(/^~(?=$|\/)/, import_node_os2.default.homedir()));
2076
- return (await readPackageJson(import_node_path11.default.join(abs, "package.json")))?.name ?? null;
2213
+ const abs = import_node_path12.default.resolve(raw.replace(/^~(?=$|\/)/, import_node_os3.default.homedir()));
2214
+ return (await readPackageJson(import_node_path12.default.join(abs, "package.json")))?.name ?? null;
2077
2215
  }
2078
2216
  if (/^(git\+|git:|https?:|ssh:)/.test(spec)) return null;
2079
2217
  if (spec.startsWith("@")) {
@@ -2084,10 +2222,10 @@ async function packageNameFromSpec(spec) {
2084
2222
  return at <= 0 ? spec : spec.slice(0, at);
2085
2223
  }
2086
2224
  async function resolveSearchPackage(dir, name) {
2087
- return asSearchPackage(import_node_path11.default.join(dir, "node_modules", ...name.split("/")));
2225
+ return asSearchPackage(import_node_path12.default.join(dir, "node_modules", ...name.split("/")));
2088
2226
  }
2089
2227
  async function discoverSearchPackage(dir) {
2090
- const nodeModules = import_node_path11.default.join(dir, "node_modules");
2228
+ const nodeModules = import_node_path12.default.join(dir, "node_modules");
2091
2229
  for (const pkgDir of await pluginManifestDirs(nodeModules)) {
2092
2230
  const found = await asSearchPackage(pkgDir);
2093
2231
  if (found) return found;
@@ -2219,7 +2357,7 @@ init_host();
2219
2357
  init_types();
2220
2358
  async function exists(target) {
2221
2359
  try {
2222
- await import_promises12.default.access(target);
2360
+ await import_promises13.default.access(target);
2223
2361
  return true;
2224
2362
  } catch {
2225
2363
  return false;
@@ -2327,6 +2465,32 @@ async function appendPluginDiagnostics(out, config) {
2327
2465
  // src/index.ts
2328
2466
  init_logger();
2329
2467
 
2468
+ // src/edit.ts
2469
+ init_collections();
2470
+ init_paths();
2471
+ init_errors();
2472
+ async function resolveEditTarget(target, cwd) {
2473
+ if (target && target.trim()) {
2474
+ const [coll, ...rest] = target.trim().split(":");
2475
+ const slug = rest.join(":");
2476
+ const entry = await findCollection(coll);
2477
+ if (!entry) throw new CollectionNotFoundError(coll);
2478
+ if (slug) {
2479
+ const article = await readArticle(coll, slug);
2480
+ return { path: article.path, kind: "article" };
2481
+ }
2482
+ return { path: entry.path, kind: "collection" };
2483
+ }
2484
+ const cwdColl = await resolveCwdCollection(cwd);
2485
+ if (cwdColl) return { path: cwdColl.path, kind: "collection" };
2486
+ return { path: collectionsDir(), kind: "root" };
2487
+ }
2488
+ async function resolveEditorCommand(flag) {
2489
+ const raw = flag ?? await getConfigValue("editor") ?? process.env.VISUAL ?? process.env.EDITOR;
2490
+ if (!raw || !raw.trim()) return void 0;
2491
+ return raw.trim().split(/\s+/);
2492
+ }
2493
+
2330
2494
  // src/plugins/index.ts
2331
2495
  init_host();
2332
2496
  init_events();
@@ -2365,6 +2529,7 @@ init_registry2();
2365
2529
  InvalidTargetError,
2366
2530
  NATIVE_ENGINE,
2367
2531
  NoDefaultCollectionError,
2532
+ PROJECT_CONFIG_FILE,
2368
2533
  PluginError,
2369
2534
  REPO_CONFIG_FILE,
2370
2535
  WorktreeNotAllowedError,
@@ -2375,6 +2540,7 @@ init_registry2();
2375
2540
  buildLinkGraph,
2376
2541
  buildNavTree,
2377
2542
  buildSearchIndex,
2543
+ buildSiteConfig,
2378
2544
  collectionDir,
2379
2545
  collectionGitStatus,
2380
2546
  collectionMeta,
@@ -2419,8 +2585,10 @@ init_registry2();
2419
2585
  readArticle,
2420
2586
  readConfig,
2421
2587
  readPluginRegistry,
2588
+ readProjectConfig,
2422
2589
  readRegistry,
2423
2590
  readRepoConfig,
2591
+ readSiteConfig,
2424
2592
  registerCollection,
2425
2593
  registerPlugin,
2426
2594
  registryPath,
@@ -2434,10 +2602,14 @@ init_registry2();
2434
2602
  resolveCollectionSelection,
2435
2603
  resolveCwdCollection,
2436
2604
  resolveDefaultSearchMode,
2605
+ resolveEditTarget,
2606
+ resolveEditorCommand,
2437
2607
  resolveHome,
2438
2608
  resolveLogLevel,
2439
2609
  resolvePage,
2610
+ resolveProjectCollections,
2440
2611
  search,
2612
+ seedProjectHome,
2441
2613
  serializeArticle,
2442
2614
  setConfigValue,
2443
2615
  setTags,