diffwiki-core 0.7.0 → 0.8.0-rc.202607252154.25b2cb6

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,8 +581,10 @@ __export(index_exports, {
580
581
  buildLinkGraph: () => buildLinkGraph,
581
582
  buildNavTree: () => buildNavTree,
582
583
  buildSearchIndex: () => buildSearchIndex,
584
+ buildSite: () => buildSite,
583
585
  collectionDir: () => collectionDir,
584
586
  collectionGitStatus: () => collectionGitStatus,
587
+ collectionMeta: () => collectionMeta,
585
588
  collectionsDir: () => collectionsDir,
586
589
  configPath: () => configPath,
587
590
  createArticle: () => createArticle,
@@ -623,8 +626,10 @@ __export(index_exports, {
623
626
  readArticle: () => readArticle,
624
627
  readConfig: () => readConfig,
625
628
  readPluginRegistry: () => readPluginRegistry,
629
+ readProjectConfig: () => readProjectConfig,
626
630
  readRegistry: () => readRegistry,
627
631
  readRepoConfig: () => readRepoConfig,
632
+ readSiteConfig: () => readSiteConfig,
628
633
  registerCollection: () => registerCollection,
629
634
  registerPlugin: () => registerPlugin,
630
635
  registryPath: () => registryPath,
@@ -641,7 +646,9 @@ __export(index_exports, {
641
646
  resolveHome: () => resolveHome,
642
647
  resolveLogLevel: () => resolveLogLevel,
643
648
  resolvePage: () => resolvePage,
649
+ resolveProjectCollections: () => resolveProjectCollections,
644
650
  search: () => search,
651
+ seedProjectHome: () => seedProjectHome,
645
652
  serializeArticle: () => serializeArticle,
646
653
  setConfigValue: () => setConfigValue,
647
654
  setTags: () => setTags,
@@ -729,22 +736,39 @@ var YAML_ENGINE = {
729
736
  stringify: (data) => (0, import_yaml.stringify)(data)
730
737
  };
731
738
  var MATTER_OPTS = { engines: { yaml: YAML_ENGINE } };
739
+ function coerceAudience(value) {
740
+ return value === "private" ? "private" : "public";
741
+ }
742
+ function coerceStatus(value) {
743
+ return value === "draft" ? "draft" : "published";
744
+ }
732
745
  function parseFallback(raw) {
733
746
  const fenceRe = /^---[ \t]*\n([\s\S]*?)\n---[ \t]*\n/;
734
747
  const fenceMatch = fenceRe.exec(raw);
735
748
  let title = "";
749
+ let audience = "public";
750
+ let status = "published";
736
751
  let body;
752
+ const clean = (s) => s.trim().replace(/^["']|["']$/g, "");
737
753
  if (fenceMatch) {
738
754
  const inner = fenceMatch[1];
739
755
  const titleMatch = /^title:\s*(.+)$/m.exec(inner);
740
756
  if (titleMatch) {
741
- title = titleMatch[1].trim().replace(/^["']|["']$/g, "");
757
+ title = clean(titleMatch[1]);
758
+ }
759
+ const audienceMatch = /^audience:\s*(.+)$/m.exec(inner);
760
+ if (audienceMatch) {
761
+ audience = coerceAudience(clean(audienceMatch[1]));
762
+ }
763
+ const statusMatch = /^status:\s*(.+)$/m.exec(inner);
764
+ if (statusMatch) {
765
+ status = coerceStatus(clean(statusMatch[1]));
742
766
  }
743
767
  body = raw.slice(fenceMatch[0].length);
744
768
  } else {
745
769
  body = raw;
746
770
  }
747
- return { title, tags: [], body: body.replace(/^\n+/, "") };
771
+ return { title, tags: [], audience, status, body: body.replace(/^\n+/, "") };
748
772
  }
749
773
  function parseArticle(raw) {
750
774
  try {
@@ -752,13 +776,17 @@ function parseArticle(raw) {
752
776
  const title = typeof data.title === "string" ? data.title : "";
753
777
  const tags = Array.isArray(data.tags) ? data.tags.map((t) => String(t)) : [];
754
778
  const created = typeof data.created === "string" ? data.created : void 0;
755
- return { title, tags, created, body: content.replace(/^\n+/, "") };
779
+ const audience = coerceAudience(data.audience);
780
+ const status = coerceStatus(data.status);
781
+ return { title, tags, created, audience, status, body: content.replace(/^\n+/, "") };
756
782
  } catch {
757
783
  return parseFallback(raw);
758
784
  }
759
785
  }
760
786
  function serializeArticle(article) {
761
787
  const front = { title: article.title, tags: article.tags };
788
+ if (article.audience === "private") front.audience = "private";
789
+ if (article.status === "draft") front.status = "draft";
762
790
  return import_gray_matter.default.stringify(article.body, front, MATTER_OPTS);
763
791
  }
764
792
 
@@ -839,7 +867,17 @@ async function loadArticle(target) {
839
867
  async function updateArticleBody(target, body) {
840
868
  const { filePath, dir, raw, collection } = await loadArticle(target);
841
869
  const parsed = parseArticle(raw);
842
- await import_promises5.default.writeFile(filePath, serializeArticle({ title: parsed.title, tags: parsed.tags, body }), "utf8");
870
+ await import_promises5.default.writeFile(
871
+ filePath,
872
+ serializeArticle({
873
+ title: parsed.title,
874
+ tags: parsed.tags,
875
+ audience: parsed.audience,
876
+ status: parsed.status,
877
+ body
878
+ }),
879
+ "utf8"
880
+ );
843
881
  void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
844
882
  return { title: parsed.title, tags: parsed.tags, path: filePath, collection };
845
883
  }
@@ -847,7 +885,17 @@ async function mutateTags(target, fn) {
847
885
  const { filePath, dir, raw, collection } = await loadArticle(target);
848
886
  const parsed = parseArticle(raw);
849
887
  const tags = fn(parsed.tags);
850
- await import_promises5.default.writeFile(filePath, serializeArticle({ title: parsed.title, tags, body: parsed.body }), "utf8");
888
+ await import_promises5.default.writeFile(
889
+ filePath,
890
+ serializeArticle({
891
+ title: parsed.title,
892
+ tags,
893
+ audience: parsed.audience,
894
+ status: parsed.status,
895
+ body: parsed.body
896
+ }),
897
+ "utf8"
898
+ );
851
899
  void fireHook2("article-updated", collection, import_node_path3.default.relative(dir, filePath));
852
900
  return { title: parsed.title, tags, path: filePath, collection };
853
901
  }
@@ -1085,12 +1133,114 @@ async function collectionGitStatus(entry) {
1085
1133
  return { branch: s.branch, ahead: s.ahead, behind: s.behind, hasUpstream: s.hasUpstream };
1086
1134
  }
1087
1135
 
1136
+ // src/project.ts
1137
+ var import_promises8 = __toESM(require("fs/promises"), 1);
1138
+ var import_node_os2 = __toESM(require("os"), 1);
1139
+ var import_node_path6 = __toESM(require("path"), 1);
1140
+ var import_yaml3 = require("yaml");
1141
+ var PROJECT_CONFIG_FILE = "diffwiki.yaml";
1142
+ function normaliseSite(raw) {
1143
+ if (!raw || typeof raw !== "object") return void 0;
1144
+ const r = raw;
1145
+ const title = typeof r.title === "string" ? r.title : void 0;
1146
+ const logo = typeof r.logo === "string" ? r.logo : void 0;
1147
+ const repo = typeof r.repo === "string" ? r.repo : void 0;
1148
+ let home;
1149
+ if (r.home && typeof r.home === "object") {
1150
+ const h = r.home;
1151
+ const tagline = typeof h.tagline === "string" ? h.tagline : void 0;
1152
+ const cards = [];
1153
+ if (Array.isArray(h.cards)) {
1154
+ for (const c of h.cards) {
1155
+ if (!c || typeof c !== "object") continue;
1156
+ const cc = c;
1157
+ if (typeof cc.title === "string" && typeof cc.link === "string") {
1158
+ cards.push({
1159
+ title: cc.title,
1160
+ link: cc.link,
1161
+ ...typeof cc.icon === "string" ? { icon: cc.icon } : {},
1162
+ ...typeof cc.subtitle === "string" ? { subtitle: cc.subtitle } : {}
1163
+ });
1164
+ }
1165
+ }
1166
+ }
1167
+ home = { ...tagline !== void 0 ? { tagline } : {}, ...cards.length > 0 ? { cards } : {} };
1168
+ if (Object.keys(home).length === 0) home = void 0;
1169
+ }
1170
+ if (title === void 0 && logo === void 0 && repo === void 0 && home === void 0) return void 0;
1171
+ return {
1172
+ ...title !== void 0 ? { title } : {},
1173
+ ...logo !== void 0 ? { logo } : {},
1174
+ ...repo !== void 0 ? { repo } : {},
1175
+ ...home !== void 0 ? { home } : {}
1176
+ };
1177
+ }
1178
+ async function readProjectConfig(dir) {
1179
+ let raw;
1180
+ try {
1181
+ raw = await import_promises8.default.readFile(import_node_path6.default.join(dir, PROJECT_CONFIG_FILE), "utf8");
1182
+ } catch (err) {
1183
+ if (err.code === "ENOENT") return void 0;
1184
+ throw err;
1185
+ }
1186
+ const parsed = (0, import_yaml3.parse)(raw) ?? {};
1187
+ const collections = [];
1188
+ if (Array.isArray(parsed.collections)) {
1189
+ for (const c of parsed.collections) {
1190
+ if (!c || typeof c !== "object") continue;
1191
+ const cc = c;
1192
+ if (typeof cc.name === "string" && typeof cc.path === "string") {
1193
+ collections.push({
1194
+ name: cc.name,
1195
+ path: cc.path,
1196
+ ...typeof cc.order === "number" ? { order: cc.order } : {}
1197
+ });
1198
+ }
1199
+ }
1200
+ collections.sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
1201
+ } else if (typeof parsed.path === "string") {
1202
+ const name = typeof parsed.collection === "string" ? parsed.collection : import_node_path6.default.basename(import_node_path6.default.resolve(dir));
1203
+ collections.push({ name, path: parsed.path });
1204
+ }
1205
+ const site = normaliseSite(parsed.site);
1206
+ return { ...site ? { site } : {}, collections };
1207
+ }
1208
+ async function resolveProjectCollections(dir) {
1209
+ const config = await readProjectConfig(dir);
1210
+ if (!config) return [];
1211
+ const root = import_node_path6.default.resolve(dir);
1212
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
1213
+ return config.collections.map((c) => ({
1214
+ name: c.name,
1215
+ type: "repo",
1216
+ path: import_node_path6.default.resolve(root, c.path),
1217
+ createdAt
1218
+ }));
1219
+ }
1220
+ async function readSiteConfig(dir) {
1221
+ return (await readProjectConfig(dir))?.site ?? {};
1222
+ }
1223
+ async function seedProjectHome(dir) {
1224
+ const entries = await resolveProjectCollections(dir);
1225
+ if (entries.length === 0) {
1226
+ throw new Error(`No collections to render from diffwiki.yaml in ${import_node_path6.default.resolve(dir)}`);
1227
+ }
1228
+ const home = await import_promises8.default.mkdtemp(import_node_path6.default.join(import_node_os2.default.tmpdir(), "diffwiki-project-"));
1229
+ await import_promises8.default.writeFile(
1230
+ import_node_path6.default.join(home, "registry.json"),
1231
+ `${JSON.stringify({ version: 1, collections: entries }, null, 2)}
1232
+ `,
1233
+ "utf8"
1234
+ );
1235
+ return home;
1236
+ }
1237
+
1088
1238
  // src/site.ts
1089
1239
  init_collections();
1090
1240
 
1091
1241
  // src/browse.ts
1092
- var import_promises8 = __toESM(require("fs/promises"), 1);
1093
- var import_node_path6 = __toESM(require("path"), 1);
1242
+ var import_promises9 = __toESM(require("fs/promises"), 1);
1243
+ var import_node_path7 = __toESM(require("path"), 1);
1094
1244
  init_collections();
1095
1245
  init_errors();
1096
1246
  var INDEX_RE = /^(index|readme)\.mdx?$/i;
@@ -1136,7 +1286,7 @@ function buildArticleTree(relPaths) {
1136
1286
  async function collectRelPaths(dir, rel = "") {
1137
1287
  let dirents;
1138
1288
  try {
1139
- dirents = await import_promises8.default.readdir(dir, { withFileTypes: true });
1289
+ dirents = await import_promises9.default.readdir(dir, { withFileTypes: true });
1140
1290
  } catch {
1141
1291
  return [];
1142
1292
  }
@@ -1145,7 +1295,7 @@ async function collectRelPaths(dir, rel = "") {
1145
1295
  if (d.name.startsWith(".")) continue;
1146
1296
  const childRel = rel ? `${rel}/${d.name}` : d.name;
1147
1297
  if (d.isDirectory()) {
1148
- out.push(...await collectRelPaths(import_node_path6.default.join(dir, d.name), childRel));
1298
+ out.push(...await collectRelPaths(import_node_path7.default.join(dir, d.name), childRel));
1149
1299
  } else if (/\.(mdx?)$/.test(d.name)) {
1150
1300
  out.push(childRel);
1151
1301
  }
@@ -1154,7 +1304,7 @@ async function collectRelPaths(dir, rel = "") {
1154
1304
  }
1155
1305
  async function readdirSafe(dir) {
1156
1306
  try {
1157
- return await import_promises8.default.readdir(dir);
1307
+ return await import_promises9.default.readdir(dir);
1158
1308
  } catch {
1159
1309
  return [];
1160
1310
  }
@@ -1168,20 +1318,30 @@ function pickIndexFile(names) {
1168
1318
  };
1169
1319
  return names.filter((n) => INDEX_RE.test(n)).sort((a, b) => rank(a) - rank(b))[0];
1170
1320
  }
1171
- async function cheapTitle(absPath) {
1321
+ var CHEAP_META_BYTES = 8192;
1322
+ async function cheapMeta(absPath) {
1172
1323
  let handle;
1173
1324
  try {
1174
- handle = await import_promises8.default.open(absPath, "r");
1175
- const buf = Buffer.alloc(512);
1176
- const { bytesRead } = await handle.read(buf, 0, 512, 0);
1325
+ handle = await import_promises9.default.open(absPath, "r");
1326
+ const buf = Buffer.alloc(CHEAP_META_BYTES);
1327
+ const { bytesRead } = await handle.read(buf, 0, CHEAP_META_BYTES, 0);
1177
1328
  const snippet = buf.subarray(0, bytesRead).toString("utf8");
1178
- const m = snippet.match(/^---[\s\S]*?^title:\s*(.+)$/m);
1179
- if (m) return m[1].trim().replace(/^["']|["']$/g, "");
1329
+ const fence = /^---[ \t]*\n([\s\S]*?)(?:\n---[ \t]*\n|$)/.exec(snippet);
1330
+ if (!fence) return {};
1331
+ const inner = fence[1];
1332
+ const clean = (s) => s.trim().replace(/^["']|["']$/g, "");
1333
+ const titleM = /^title:\s*(.+)$/m.exec(inner);
1334
+ const audienceM = /^audience:\s*(.+)$/m.exec(inner);
1335
+ const statusM = /^status:\s*(.+)$/m.exec(inner);
1336
+ const title = titleM ? clean(titleM[1]) : void 0;
1337
+ const audience = audienceM && clean(audienceM[1]) === "private" ? "private" : void 0;
1338
+ const status = statusM && clean(statusM[1]) === "draft" ? "draft" : void 0;
1339
+ return { title, audience, status };
1180
1340
  } catch {
1181
1341
  } finally {
1182
1342
  await handle?.close();
1183
1343
  }
1184
- return void 0;
1344
+ return {};
1185
1345
  }
1186
1346
  async function enrichTitles(nodes, collDir) {
1187
1347
  return Promise.all(
@@ -1189,67 +1349,97 @@ async function enrichTitles(nodes, collDir) {
1189
1349
  if (node.children) {
1190
1350
  const children = await enrichTitles(node.children, collDir);
1191
1351
  let title = node.title;
1352
+ let audience = node.audience;
1353
+ let status = node.status;
1192
1354
  if (node.slug) {
1193
- const dir = import_node_path6.default.join(collDir, node.slug);
1355
+ const dir = import_node_path7.default.join(collDir, node.slug);
1194
1356
  const idx = pickIndexFile(await readdirSafe(dir));
1195
- if (idx) title = await cheapTitle(import_node_path6.default.join(dir, idx)) ?? title;
1357
+ if (idx) {
1358
+ const meta = await cheapMeta(import_node_path7.default.join(dir, idx));
1359
+ title = meta.title ?? title;
1360
+ audience = meta.audience ?? audience;
1361
+ status = meta.status ?? status;
1362
+ }
1196
1363
  }
1197
- return { ...node, title, children };
1364
+ return { ...node, title, audience, status, children };
1198
1365
  }
1199
1366
  for (const ext of [".mdx", ".md"]) {
1200
- const title = await cheapTitle(import_node_path6.default.join(collDir, `${node.slug}${ext}`));
1201
- if (title) return { ...node, title };
1367
+ const meta = await cheapMeta(import_node_path7.default.join(collDir, `${node.slug}${ext}`));
1368
+ if (meta.title || meta.audience || meta.status) {
1369
+ return {
1370
+ ...node,
1371
+ title: meta.title ?? node.title,
1372
+ audience: meta.audience ?? node.audience,
1373
+ status: meta.status ?? node.status
1374
+ };
1375
+ }
1202
1376
  }
1203
1377
  return node;
1204
1378
  })
1205
1379
  );
1206
1380
  }
1381
+ async function collectionMeta(collDir) {
1382
+ const idx = pickIndexFile(await readdirSafe(collDir));
1383
+ if (!idx) return {};
1384
+ const meta = await cheapMeta(import_node_path7.default.join(collDir, idx));
1385
+ return { title: meta.title, audience: meta.audience, status: meta.status };
1386
+ }
1207
1387
  async function listArticleTree(coll) {
1208
1388
  const entry = await findCollection(coll);
1209
1389
  if (!entry) throw new CollectionNotFoundError(coll);
1210
1390
  const rels = await collectRelPaths(entry.path);
1211
- return enrichTitles(buildArticleTree(rels), import_node_path6.default.resolve(entry.path));
1391
+ return enrichTitles(buildArticleTree(rels), import_node_path7.default.resolve(entry.path));
1212
1392
  }
1213
1393
  async function readArticle(coll, slug) {
1214
1394
  const entry = await findCollection(coll);
1215
1395
  if (!entry) throw new CollectionNotFoundError(coll);
1216
- const collDir = import_node_path6.default.resolve(entry.path);
1396
+ const collDir = import_node_path7.default.resolve(entry.path);
1217
1397
  const normalized = slug.replace(/\\/g, "/");
1218
1398
  if (normalized.split("/").some((s) => s === "..")) {
1219
1399
  throw new InvalidTargetError(slug, "path traversal detected");
1220
1400
  }
1221
1401
  const safeSlug = normalized.split("/").filter((s) => s !== "" && s !== ".").join("/");
1222
- const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path6.default.sep);
1402
+ const inJail = (p) => p === collDir || p.startsWith(collDir + import_node_path7.default.sep);
1223
1403
  let abs;
1224
1404
  let isIndex = false;
1225
1405
  for (const rel of safeSlug ? [`${safeSlug}.mdx`, `${safeSlug}.md`] : []) {
1226
- const candidate = import_node_path6.default.resolve(collDir, rel);
1406
+ const candidate = import_node_path7.default.resolve(collDir, rel);
1227
1407
  if (!inJail(candidate)) throw new InvalidTargetError(slug, "path traversal detected");
1228
1408
  try {
1229
- await import_promises8.default.access(candidate);
1409
+ await import_promises9.default.access(candidate);
1230
1410
  abs = candidate;
1231
1411
  break;
1232
1412
  } catch {
1233
1413
  }
1234
1414
  }
1235
1415
  if (!abs) {
1236
- const dir = import_node_path6.default.resolve(collDir, safeSlug);
1416
+ const dir = import_node_path7.default.resolve(collDir, safeSlug);
1237
1417
  if (inJail(dir)) {
1238
1418
  const idx = pickIndexFile(await readdirSafe(dir));
1239
1419
  if (idx) {
1240
- abs = import_node_path6.default.join(dir, idx);
1420
+ abs = import_node_path7.default.join(dir, idx);
1241
1421
  isIndex = true;
1242
1422
  }
1243
1423
  }
1244
1424
  }
1245
1425
  if (!abs) throw new ArticleNotFoundError(`${coll}/${safeSlug}`);
1246
- const parsed = parseArticle(await import_promises8.default.readFile(abs, "utf8"));
1426
+ const parsed = parseArticle(await import_promises9.default.readFile(abs, "utf8"));
1247
1427
  const title = parsed.title?.trim() || (safeSlug ? safeSlug.split("/").pop() ?? safeSlug : coll);
1248
- return { coll, slug: safeSlug, title, tags: parsed.tags, body: parsed.body, path: abs, isIndex };
1428
+ return {
1429
+ coll,
1430
+ slug: safeSlug,
1431
+ title,
1432
+ tags: parsed.tags,
1433
+ audience: parsed.audience,
1434
+ status: parsed.status,
1435
+ body: parsed.body,
1436
+ path: abs,
1437
+ isIndex
1438
+ };
1249
1439
  }
1250
1440
 
1251
1441
  // src/render.ts
1252
- var import_node_path7 = __toESM(require("path"), 1);
1442
+ var import_node_path8 = __toESM(require("path"), 1);
1253
1443
  var import_unified = require("unified");
1254
1444
  var import_remark_parse = __toESM(require("remark-parse"), 1);
1255
1445
  var import_remark_gfm = __toESM(require("remark-gfm"), 1);
@@ -1266,7 +1456,7 @@ function isNonRelativeHref(href) {
1266
1456
  return /^[a-z][a-z0-9+.-]*:/i.test(href);
1267
1457
  }
1268
1458
  function rehypeRewriteLinks(ctx) {
1269
- const slugDir = ctx.isIndex ? ctx.slug || "." : import_node_path7.default.posix.dirname(ctx.slug || ".");
1459
+ const slugDir = ctx.isIndex ? ctx.slug || "." : import_node_path8.default.posix.dirname(ctx.slug || ".");
1270
1460
  return (tree) => {
1271
1461
  (0, import_unist_util_visit.visit)(tree, "element", (node) => {
1272
1462
  if (node.tagName !== "a") return;
@@ -1276,12 +1466,17 @@ function rehypeRewriteLinks(ctx) {
1276
1466
  const suffix = suffixMatch ? suffixMatch[0] : "";
1277
1467
  const pathPart = suffix ? raw.slice(0, raw.length - suffix.length) : raw;
1278
1468
  if (pathPart === "") return;
1279
- const joined = import_node_path7.default.posix.join(slugDir === "." ? "" : slugDir, pathPart);
1280
- const normalized = import_node_path7.default.posix.normalize(joined);
1469
+ const joined = import_node_path8.default.posix.join(slugDir === "." ? "" : slugDir, pathPart);
1470
+ const normalized = import_node_path8.default.posix.normalize(joined);
1471
+ const toRoute = (p) => p.replace(/\.(mdx?)$/i, "").replace(/(^|\/)(index|readme)$/i, "$1").replace(/\/+$/, "").replace(/^\/+/, "");
1472
+ if (normalized.startsWith("../")) {
1473
+ const sibling = normalized.slice(3);
1474
+ if (sibling === "" || sibling.startsWith("..")) return;
1475
+ node.properties = { ...node.properties, href: `/${toRoute(sibling)}${suffix}` };
1476
+ return;
1477
+ }
1281
1478
  if (normalized.startsWith("..")) return;
1282
- let resolved = normalized.replace(/\.(mdx?)$/i, "");
1283
- resolved = resolved.replace(/(^|\/)(index|readme)$/i, "$1");
1284
- resolved = resolved.replace(/\/+$/, "").replace(/^\/+/, "");
1479
+ const resolved = toRoute(normalized);
1285
1480
  node.properties = {
1286
1481
  ...node.properties,
1287
1482
  href: `/${ctx.coll}${resolved ? `/${resolved}` : ""}${suffix}`
@@ -1496,13 +1691,22 @@ function localGraph(graph, focusId, include, siblingCap = 12) {
1496
1691
  init_errors();
1497
1692
  var isStatic = () => process.env.DIFFWIKI_STATIC === "1";
1498
1693
  function toTreeNodes(coll, nodes) {
1499
- return nodes.map(
1694
+ const visible = isStatic() ? nodes.filter((n) => n.audience !== "private" && n.status !== "draft") : nodes;
1695
+ return visible.map(
1500
1696
  (n) => n.children ? {
1501
1697
  name: n.name,
1502
1698
  title: n.title ?? n.name,
1503
1699
  path: n.slug ? `/${coll}/${n.slug}` : void 0,
1700
+ ...n.audience ? { audience: n.audience } : {},
1701
+ ...n.status ? { status: n.status } : {},
1504
1702
  children: toTreeNodes(coll, n.children)
1505
- } : { name: n.name, title: n.title ?? n.name, path: `/${coll}/${n.slug}` }
1703
+ } : {
1704
+ name: n.name,
1705
+ title: n.title ?? n.name,
1706
+ path: `/${coll}/${n.slug}`,
1707
+ ...n.audience ? { audience: n.audience } : {},
1708
+ ...n.status ? { status: n.status } : {}
1709
+ }
1506
1710
  );
1507
1711
  }
1508
1712
  async function gitInfo(e) {
@@ -1515,23 +1719,42 @@ async function gitInfo(e) {
1515
1719
  }
1516
1720
  async function buildCollections() {
1517
1721
  const entries = await listCollections();
1518
- return Promise.all(
1519
- entries.map(async (e) => ({
1520
- name: e.name,
1521
- type: e.type,
1522
- path: e.path,
1523
- ...await gitInfo(e)
1524
- }))
1722
+ const infos = await Promise.all(
1723
+ entries.map(async (e) => {
1724
+ const { title, audience, status } = await collectionMeta(e.path);
1725
+ return {
1726
+ name: e.name,
1727
+ type: e.type,
1728
+ path: e.path,
1729
+ ...title ? { title } : {},
1730
+ ...await gitInfo(e),
1731
+ ...audience ? { audience } : {},
1732
+ ...status ? { status } : {}
1733
+ };
1734
+ })
1525
1735
  );
1736
+ return isStatic() ? infos.filter((c) => c.audience !== "private" && c.status !== "draft") : infos;
1526
1737
  }
1527
1738
  async function buildNavTree() {
1528
1739
  const entries = await listCollections();
1529
- return Promise.all(
1530
- entries.map(async (e) => ({
1531
- collection: { name: e.name, type: e.type, path: e.path, ...await gitInfo(e) },
1532
- nodes: toTreeNodes(e.name, await listArticleTree(e.name))
1533
- }))
1740
+ const trees = await Promise.all(
1741
+ entries.map(async (e) => {
1742
+ const { title, audience, status } = await collectionMeta(e.path);
1743
+ return {
1744
+ collection: {
1745
+ name: e.name,
1746
+ type: e.type,
1747
+ path: e.path,
1748
+ ...title ? { title } : {},
1749
+ ...await gitInfo(e),
1750
+ ...audience ? { audience } : {},
1751
+ ...status ? { status } : {}
1752
+ },
1753
+ nodes: toTreeNodes(e.name, await listArticleTree(e.name))
1754
+ };
1755
+ })
1534
1756
  );
1757
+ return isStatic() ? trees.filter((t) => t.collection.audience !== "private" && t.collection.status !== "draft") : trees;
1535
1758
  }
1536
1759
  function findNode(nodes, slug) {
1537
1760
  for (const n of nodes) {
@@ -1559,6 +1782,8 @@ async function resolvePage(coll, slug, opts) {
1559
1782
  slug: art.slug,
1560
1783
  title: art.title,
1561
1784
  tags: art.tags,
1785
+ ...art.audience === "private" ? { audience: art.audience } : {},
1786
+ ...art.status === "draft" ? { status: art.status } : {},
1562
1787
  html,
1563
1788
  toc,
1564
1789
  path: `/${art.coll}/${art.slug}`,
@@ -1593,6 +1818,15 @@ async function resolvePage(coll, slug, opts) {
1593
1818
  };
1594
1819
  }
1595
1820
  }
1821
+ async function buildSite() {
1822
+ const dir = process.env.DIFFWIKI_PROJECT_DIR;
1823
+ if (!dir) return {};
1824
+ try {
1825
+ return await readSiteConfig(dir);
1826
+ } catch {
1827
+ return {};
1828
+ }
1829
+ }
1596
1830
 
1597
1831
  // src/select.ts
1598
1832
  function globToRegExp(glob) {
@@ -1622,8 +1856,8 @@ function resolveCollectionSelection(all, sel = {}) {
1622
1856
  }
1623
1857
 
1624
1858
  // src/query.ts
1625
- var import_promises9 = __toESM(require("fs/promises"), 1);
1626
- var import_node_path8 = __toESM(require("path"), 1);
1859
+ var import_promises10 = __toESM(require("fs/promises"), 1);
1860
+ var import_node_path9 = __toESM(require("path"), 1);
1627
1861
  init_collections();
1628
1862
  init_errors();
1629
1863
  init_logger();
@@ -1692,13 +1926,13 @@ var log2 = getLogger("query");
1692
1926
  async function collectMarkdownFiles(dir) {
1693
1927
  let entries;
1694
1928
  try {
1695
- entries = await import_promises9.default.readdir(dir, { withFileTypes: true });
1929
+ entries = await import_promises10.default.readdir(dir, { withFileTypes: true });
1696
1930
  } catch {
1697
1931
  return [];
1698
1932
  }
1699
1933
  const files = [];
1700
1934
  for (const entry of entries) {
1701
- const full = import_node_path8.default.join(dir, entry.name);
1935
+ const full = import_node_path9.default.join(dir, entry.name);
1702
1936
  if (entry.isDirectory()) {
1703
1937
  files.push(...await collectMarkdownFiles(full));
1704
1938
  } else if (entry.isFile() && /\.mdx?$/.test(entry.name)) {
@@ -1717,12 +1951,12 @@ async function nativeSearch(term, collections, collection) {
1717
1951
  const docs = [];
1718
1952
  for (const entry of targets) {
1719
1953
  for (const full of await collectMarkdownFiles(entry.path)) {
1720
- const fallbackTitle = import_node_path8.default.basename(full).replace(/\.mdx?$/, "");
1954
+ const fallbackTitle = import_node_path9.default.basename(full).replace(/\.mdx?$/, "");
1721
1955
  let title = fallbackTitle;
1722
1956
  let tags = [];
1723
1957
  let body = "";
1724
1958
  try {
1725
- const parsed = parseArticle(await import_promises9.default.readFile(full, "utf8"));
1959
+ const parsed = parseArticle(await import_promises10.default.readFile(full, "utf8"));
1726
1960
  if (parsed.title) title = parsed.title;
1727
1961
  tags = parsed.tags;
1728
1962
  body = parsed.body;
@@ -1805,13 +2039,14 @@ async function query(term, opts) {
1805
2039
  }
1806
2040
 
1807
2041
  // src/search.ts
1808
- var import_promises10 = __toESM(require("fs/promises"), 1);
1809
- var import_node_path9 = __toESM(require("path"), 1);
2042
+ var import_promises11 = __toESM(require("fs/promises"), 1);
2043
+ var import_node_path10 = __toESM(require("path"), 1);
1810
2044
  init_collections();
2045
+ var isStatic2 = () => process.env.DIFFWIKI_STATIC === "1";
1811
2046
  async function readArticleParts(collDir, slug) {
1812
2047
  for (const ext of [".mdx", ".md"]) {
1813
2048
  try {
1814
- const parsed = parseArticle(await import_promises10.default.readFile(import_node_path9.default.join(collDir, `${slug}${ext}`), "utf8"));
2049
+ const parsed = parseArticle(await import_promises11.default.readFile(import_node_path10.default.join(collDir, `${slug}${ext}`), "utf8"));
1815
2050
  return { tags: parsed.tags, body: parsed.body };
1816
2051
  } catch {
1817
2052
  }
@@ -1820,6 +2055,7 @@ async function readArticleParts(collDir, slug) {
1820
2055
  }
1821
2056
  async function flatten(coll, collDir, nodes, out) {
1822
2057
  for (const n of nodes) {
2058
+ if (isStatic2() && (n.audience === "private" || n.status === "draft")) continue;
1823
2059
  if (n.slug && !n.children) {
1824
2060
  const title = n.title ?? n.name;
1825
2061
  const { tags, body } = await readArticleParts(collDir, n.slug);
@@ -1845,7 +2081,11 @@ async function buildSearchIndex(collection) {
1845
2081
  for (const e of entries) {
1846
2082
  try {
1847
2083
  const collEntry = await findCollection(e.name);
1848
- const collDir = import_node_path9.default.resolve(collEntry?.path ?? e.path);
2084
+ const collDir = import_node_path10.default.resolve(collEntry?.path ?? e.path);
2085
+ if (isStatic2()) {
2086
+ const cm = await collectionMeta(collDir);
2087
+ if (cm.audience === "private" || cm.status === "draft") continue;
2088
+ }
1849
2089
  await flatten(e.name, collDir, await listArticleTree(e.name), out);
1850
2090
  } catch {
1851
2091
  }
@@ -1854,15 +2094,15 @@ async function buildSearchIndex(collection) {
1854
2094
  }
1855
2095
 
1856
2096
  // src/doctor.ts
1857
- var import_promises12 = __toESM(require("fs/promises"), 1);
2097
+ var import_promises13 = __toESM(require("fs/promises"), 1);
1858
2098
  init_paths();
1859
2099
  init_registry();
1860
2100
  init_collections();
1861
2101
 
1862
2102
  // src/plugins/manager.ts
1863
- var import_promises11 = __toESM(require("fs/promises"), 1);
1864
- var import_node_os2 = __toESM(require("os"), 1);
1865
- var import_node_path11 = __toESM(require("path"), 1);
2103
+ var import_promises12 = __toESM(require("fs/promises"), 1);
2104
+ var import_node_os3 = __toESM(require("os"), 1);
2105
+ var import_node_path12 = __toESM(require("path"), 1);
1866
2106
  init_paths();
1867
2107
  init_errors();
1868
2108
  init_collections();
@@ -1873,7 +2113,7 @@ init_host();
1873
2113
  var import_node_child_process4 = require("child_process");
1874
2114
  var import_node_util3 = require("util");
1875
2115
  var import_node_fs = require("fs");
1876
- var import_node_path10 = require("path");
2116
+ var import_node_path11 = require("path");
1877
2117
  var import_node_url = require("url");
1878
2118
  var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process4.execFile);
1879
2119
  async function runProcess(file, args, opts) {
@@ -1889,7 +2129,7 @@ async function runProcess(file, args, opts) {
1889
2129
  // src/plugins/manager.ts
1890
2130
  async function readPackageJson(file) {
1891
2131
  try {
1892
- return JSON.parse(await import_promises11.default.readFile(file, "utf8"));
2132
+ return JSON.parse(await import_promises12.default.readFile(file, "utf8"));
1893
2133
  } catch {
1894
2134
  return null;
1895
2135
  }
@@ -1905,7 +2145,7 @@ function binOf(manifest) {
1905
2145
  async function pluginManifestDirs(nodeModules) {
1906
2146
  let entries;
1907
2147
  try {
1908
- entries = await import_promises11.default.readdir(nodeModules);
2148
+ entries = await import_promises12.default.readdir(nodeModules);
1909
2149
  } catch {
1910
2150
  return [];
1911
2151
  }
@@ -1915,44 +2155,44 @@ async function pluginManifestDirs(nodeModules) {
1915
2155
  if (entry.startsWith("@")) {
1916
2156
  let scoped;
1917
2157
  try {
1918
- scoped = await import_promises11.default.readdir(import_node_path11.default.join(nodeModules, entry));
2158
+ scoped = await import_promises12.default.readdir(import_node_path12.default.join(nodeModules, entry));
1919
2159
  } catch {
1920
2160
  continue;
1921
2161
  }
1922
2162
  for (const pkg of scoped) {
1923
2163
  if (pkg.startsWith(".")) continue;
1924
- dirs.push(import_node_path11.default.join(nodeModules, entry, pkg));
2164
+ dirs.push(import_node_path12.default.join(nodeModules, entry, pkg));
1925
2165
  }
1926
2166
  } else {
1927
- dirs.push(import_node_path11.default.join(nodeModules, entry));
2167
+ dirs.push(import_node_path12.default.join(nodeModules, entry));
1928
2168
  }
1929
2169
  }
1930
2170
  return dirs;
1931
2171
  }
1932
2172
  async function ensurePluginRoot() {
1933
2173
  const dir = pluginsDir();
1934
- await import_promises11.default.mkdir(dir, { recursive: true });
1935
- const pkgPath = import_node_path11.default.join(dir, "package.json");
2174
+ await import_promises12.default.mkdir(dir, { recursive: true });
2175
+ const pkgPath = import_node_path12.default.join(dir, "package.json");
1936
2176
  if (await readPackageJson(pkgPath) === null) {
1937
2177
  const pkg = { name: "diffwiki-plugins", private: true, version: "0.0.0" };
1938
- await import_promises11.default.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
2178
+ await import_promises12.default.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
1939
2179
  `, "utf8");
1940
2180
  }
1941
2181
  return dir;
1942
2182
  }
1943
2183
  async function asSearchPackage(pkgDir) {
1944
- const manifest = await readPackageJson(import_node_path11.default.join(pkgDir, "package.json"));
2184
+ const manifest = await readPackageJson(import_node_path12.default.join(pkgDir, "package.json"));
1945
2185
  if (!manifest?.name || manifest.diffwiki?.kind !== "search") return null;
1946
2186
  const bin = binOf(manifest);
1947
2187
  if (!bin) return null;
1948
- return { name: manifest.name, version: manifest.version, binAbs: import_node_path11.default.resolve(pkgDir, bin) };
2188
+ return { name: manifest.name, version: manifest.version, binAbs: import_node_path12.default.resolve(pkgDir, bin) };
1949
2189
  }
1950
2190
  async function packageNameFromSpec(spec) {
1951
2191
  const looksLikePath = spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("~") || spec.startsWith("file:");
1952
2192
  if (looksLikePath) {
1953
2193
  const raw = spec.startsWith("file:") ? spec.slice("file:".length) : spec;
1954
- const abs = import_node_path11.default.resolve(raw.replace(/^~(?=$|\/)/, import_node_os2.default.homedir()));
1955
- return (await readPackageJson(import_node_path11.default.join(abs, "package.json")))?.name ?? null;
2194
+ const abs = import_node_path12.default.resolve(raw.replace(/^~(?=$|\/)/, import_node_os3.default.homedir()));
2195
+ return (await readPackageJson(import_node_path12.default.join(abs, "package.json")))?.name ?? null;
1956
2196
  }
1957
2197
  if (/^(git\+|git:|https?:|ssh:)/.test(spec)) return null;
1958
2198
  if (spec.startsWith("@")) {
@@ -1963,10 +2203,10 @@ async function packageNameFromSpec(spec) {
1963
2203
  return at <= 0 ? spec : spec.slice(0, at);
1964
2204
  }
1965
2205
  async function resolveSearchPackage(dir, name) {
1966
- return asSearchPackage(import_node_path11.default.join(dir, "node_modules", ...name.split("/")));
2206
+ return asSearchPackage(import_node_path12.default.join(dir, "node_modules", ...name.split("/")));
1967
2207
  }
1968
2208
  async function discoverSearchPackage(dir) {
1969
- const nodeModules = import_node_path11.default.join(dir, "node_modules");
2209
+ const nodeModules = import_node_path12.default.join(dir, "node_modules");
1970
2210
  for (const pkgDir of await pluginManifestDirs(nodeModules)) {
1971
2211
  const found = await asSearchPackage(pkgDir);
1972
2212
  if (found) return found;
@@ -2098,7 +2338,7 @@ init_host();
2098
2338
  init_types();
2099
2339
  async function exists(target) {
2100
2340
  try {
2101
- await import_promises12.default.access(target);
2341
+ await import_promises13.default.access(target);
2102
2342
  return true;
2103
2343
  } catch {
2104
2344
  return false;
@@ -2244,6 +2484,7 @@ init_registry2();
2244
2484
  InvalidTargetError,
2245
2485
  NATIVE_ENGINE,
2246
2486
  NoDefaultCollectionError,
2487
+ PROJECT_CONFIG_FILE,
2247
2488
  PluginError,
2248
2489
  REPO_CONFIG_FILE,
2249
2490
  WorktreeNotAllowedError,
@@ -2254,8 +2495,10 @@ init_registry2();
2254
2495
  buildLinkGraph,
2255
2496
  buildNavTree,
2256
2497
  buildSearchIndex,
2498
+ buildSite,
2257
2499
  collectionDir,
2258
2500
  collectionGitStatus,
2501
+ collectionMeta,
2259
2502
  collectionsDir,
2260
2503
  configPath,
2261
2504
  createArticle,
@@ -2297,8 +2540,10 @@ init_registry2();
2297
2540
  readArticle,
2298
2541
  readConfig,
2299
2542
  readPluginRegistry,
2543
+ readProjectConfig,
2300
2544
  readRegistry,
2301
2545
  readRepoConfig,
2546
+ readSiteConfig,
2302
2547
  registerCollection,
2303
2548
  registerPlugin,
2304
2549
  registryPath,
@@ -2315,7 +2560,9 @@ init_registry2();
2315
2560
  resolveHome,
2316
2561
  resolveLogLevel,
2317
2562
  resolvePage,
2563
+ resolveProjectCollections,
2318
2564
  search,
2565
+ seedProjectHome,
2319
2566
  serializeArticle,
2320
2567
  setConfigValue,
2321
2568
  setTags,