contentbit 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -372,12 +372,58 @@ function parseValue(value, indented) {
372
372
  if (indented.length === 0)
373
373
  return null;
374
374
  const items = dedent(indented);
375
- if (items.every((l) => l.startsWith("- ")))
376
- return items.map((l) => parseScalar(l.slice(2).trim()));
375
+ const list = parseDashList(items);
376
+ if (list)
377
+ return list;
378
+ const mapping = parseNestedMapping(items);
379
+ if (mapping)
380
+ return mapping;
377
381
  return items.join("\n");
378
382
  }
379
383
  return parseScalar(value);
380
384
  }
385
+ function parseNestedMapping(items) {
386
+ const out = {};
387
+ for (const line of items) {
388
+ if (/^[ \t]/.test(line))
389
+ return null;
390
+ const m = line.match(KEY_RE2);
391
+ if (!m)
392
+ return null;
393
+ const [, key, rawValue] = m;
394
+ const v = rawValue.trim();
395
+ if (v === "")
396
+ return null;
397
+ out[key] = parseScalar(v);
398
+ }
399
+ return Object.keys(out).length > 0 ? out : null;
400
+ }
401
+ function parseDashList(items) {
402
+ const groups = [];
403
+ let current = null;
404
+ for (const line of items) {
405
+ if (line.startsWith("- ")) {
406
+ if (current)
407
+ groups.push(current);
408
+ current = [line.slice(2)];
409
+ } else if (current && /^[ \t]/.test(line)) {
410
+ current.push(line);
411
+ } else {
412
+ return null;
413
+ }
414
+ }
415
+ if (current)
416
+ groups.push(current);
417
+ return groups.map((group) => parseDashItem(group));
418
+ }
419
+ function parseDashItem(lines) {
420
+ const first = lines[0].trim();
421
+ if (lines.length === 1)
422
+ return parseNestedMapping([first]) ?? parseScalar(first);
423
+ const rest = dedent(lines.slice(1));
424
+ const mapping = parseNestedMapping([first, ...rest]);
425
+ return mapping ?? lines.join("\n");
426
+ }
381
427
  function dedent(lines) {
382
428
  const indent = Math.min(...lines.map((l) => l.match(/^[ \t]*/)[0].length));
383
429
  return lines.map((l) => l.slice(indent));
@@ -409,6 +455,20 @@ function parseScalar(value) {
409
455
  return [];
410
456
  return splitInlineItems(inner).map((item) => parseScalar(item.trim()));
411
457
  }
458
+ if (value.startsWith("{") && value.endsWith("}")) {
459
+ const inner = value.slice(1, -1).trim();
460
+ if (inner === "")
461
+ return {};
462
+ const out = {};
463
+ for (const item of splitInlineItems(inner)) {
464
+ const m = item.trim().match(KEY_RE2);
465
+ if (!m)
466
+ return value;
467
+ const [, key, rawValue] = m;
468
+ out[key] = parseScalar(rawValue.trim());
469
+ }
470
+ return out;
471
+ }
412
472
  return value;
413
473
  }
414
474
  function splitInlineItems(inner) {
@@ -1097,21 +1157,6 @@ var init_render_markdown = __esm({
1097
1157
  }
1098
1158
  });
1099
1159
 
1100
- // ../core/dist/index.js
1101
- var init_dist = __esm({
1102
- "../core/dist/index.js"() {
1103
- "use strict";
1104
- init_diagnostics();
1105
- init_parser();
1106
- init_frontmatter();
1107
- init_analyze();
1108
- init_registry();
1109
- init_content_models();
1110
- init_validate();
1111
- init_render_markdown();
1112
- }
1113
- });
1114
-
1115
1160
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
1116
1161
  // @__NO_SIDE_EFFECTS__
1117
1162
  function $constructor(name, initializer3, params) {
@@ -1176,7 +1221,7 @@ var init_core = __esm({
1176
1221
  NEVER = /* @__PURE__ */ Object.freeze({
1177
1222
  status: "aborted"
1178
1223
  });
1179
- $brand = Symbol("zod_brand");
1224
+ $brand = /* @__PURE__ */ Symbol("zod_brand");
1180
1225
  $ZodAsyncError = class extends Error {
1181
1226
  constructor() {
1182
1227
  super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`);
@@ -11345,8 +11390,8 @@ function registry() {
11345
11390
  var _a2, $output, $input, $ZodRegistry, globalRegistry;
11346
11391
  var init_registries = __esm({
11347
11392
  "../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/registries.js"() {
11348
- $output = Symbol("ZodOutput");
11349
- $input = Symbol("ZodInput");
11393
+ $output = /* @__PURE__ */ Symbol("ZodOutput");
11394
+ $input = /* @__PURE__ */ Symbol("ZodInput");
11350
11395
  $ZodRegistry = class {
11351
11396
  constructor() {
11352
11397
  this._map = /* @__PURE__ */ new WeakMap();
@@ -16193,6 +16238,435 @@ var init_zod = __esm({
16193
16238
  }
16194
16239
  });
16195
16240
 
16241
+ // ../core/dist/links.js
16242
+ function parseLinkFrontmatter(data, options = {}) {
16243
+ const normalized = normalizeFrontmatter(data, options);
16244
+ if (!("slug" in normalized))
16245
+ return { ok: true, value: null };
16246
+ const parsed = LinkFrontmatter.safeParse(normalized);
16247
+ if (parsed.success)
16248
+ return { ok: true, value: parsed.data };
16249
+ return { ok: false, errors: parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`) };
16250
+ }
16251
+ function buildLinkIndex(inputs, options = {}) {
16252
+ const resolvedOptions = { ...DEFAULT_OPTIONS, ...options };
16253
+ const pages = /* @__PURE__ */ new Map();
16254
+ const aliases = /* @__PURE__ */ new Map();
16255
+ const aliasEntries = [];
16256
+ for (const { path, data } of inputs) {
16257
+ const parsed = parseLinkFrontmatter(data, resolvedOptions);
16258
+ if (!parsed.ok || parsed.value === null)
16259
+ continue;
16260
+ const fm = parsed.value;
16261
+ const page = {
16262
+ slug: fm.slug,
16263
+ key: fm.key,
16264
+ locale: effectiveLocale(fm, resolvedOptions),
16265
+ path,
16266
+ title: fm.title,
16267
+ keywords: fm.keywords,
16268
+ linksTo: [],
16269
+ linkedFrom: [],
16270
+ aliases: fm.aliases ?? [],
16271
+ linkRefs: [],
16272
+ linkedFromRefs: []
16273
+ };
16274
+ pages.set(pageMapKey(page, resolvedOptions), page);
16275
+ }
16276
+ for (const page of pages.values()) {
16277
+ for (const alias of page.aliases) {
16278
+ const replacement = replacementFor(page, resolvedOptions);
16279
+ const key = aliasMapKey(alias, page.locale, resolvedOptions);
16280
+ aliases.set(key, replacement);
16281
+ aliasEntries.push({ alias, locale: page.locale, replacement, page });
16282
+ }
16283
+ }
16284
+ const lookup = buildLookup(pages, resolvedOptions);
16285
+ for (const page of pages.values()) {
16286
+ const source = parseLinkFrontmatter(inputForPage(page, inputs, resolvedOptions), resolvedOptions);
16287
+ if (!source.ok || source.value === null)
16288
+ continue;
16289
+ for (const rawTarget of source.value.linksTo ?? []) {
16290
+ const resolved = resolveTarget(rawTarget, page, lookup, resolvedOptions);
16291
+ if (!resolved.page) {
16292
+ page.linksTo.push(resolved.target);
16293
+ continue;
16294
+ }
16295
+ page.linksTo.push(replacementFor(resolved.page, resolvedOptions));
16296
+ page.linkRefs.push(referenceFor(resolved.page, resolved.target));
16297
+ if (resolved.page === page)
16298
+ continue;
16299
+ const from = replacementFor(page, resolvedOptions);
16300
+ if (!resolved.page.linkedFrom.includes(from))
16301
+ resolved.page.linkedFrom.push(from);
16302
+ if (!resolved.page.linkedFromRefs.some((r) => sameReference(r, page))) {
16303
+ resolved.page.linkedFromRefs.push(referenceFor(page));
16304
+ }
16305
+ }
16306
+ }
16307
+ return { pages, aliases, aliasEntries, options: resolvedOptions };
16308
+ }
16309
+ function serializeLinkIndex(index) {
16310
+ const scoped = [...index.pages.values()].some((page) => page.locale || page.key);
16311
+ const pages = [...index.pages.values()].map((p) => {
16312
+ const base = {
16313
+ slug: p.slug,
16314
+ ...p.key ? { key: p.key } : {},
16315
+ ...p.locale ? { locale: p.locale } : {},
16316
+ path: p.path,
16317
+ ...p.title ? { title: p.title } : {},
16318
+ ...p.keywords ? { keywords: p.keywords } : {},
16319
+ linksTo: scoped ? p.linkRefs : [...p.linksTo],
16320
+ linkedFrom: scoped ? sortedRefs(p.linkedFromRefs) : [...p.linkedFrom].sort(),
16321
+ aliases: [...p.aliases]
16322
+ };
16323
+ return base;
16324
+ }).sort((a, b) => sortIdentity(a.locale, a.slug).localeCompare(sortIdentity(b.locale, b.slug)));
16325
+ const aliases = {};
16326
+ if (scoped) {
16327
+ for (const entry of [...index.aliasEntries].sort((a, b) => sortIdentity(a.locale, a.alias).localeCompare(sortIdentity(b.locale, b.alias)))) {
16328
+ aliases[aliasMapKey(entry.alias, entry.locale, index.options)] = referenceFor(entry.page);
16329
+ }
16330
+ } else {
16331
+ for (const key of [...index.aliases.keys()].sort())
16332
+ aliases[key] = index.aliases.get(key);
16333
+ }
16334
+ return { pages, aliases };
16335
+ }
16336
+ function aliasReplacementsForPage(index, data) {
16337
+ const parsed = parseLinkFrontmatter(data, index.options);
16338
+ const out = /* @__PURE__ */ new Map();
16339
+ if (!parsed.ok || parsed.value === null)
16340
+ return out;
16341
+ const locale = effectiveLocale(parsed.value, index.options);
16342
+ for (const entry of index.aliasEntries) {
16343
+ if (index.options.resolve === "global-slug" || entry.locale === locale) {
16344
+ out.set(entry.alias, entry.replacement);
16345
+ }
16346
+ }
16347
+ return out;
16348
+ }
16349
+ function validateLinks(inputs, options = {}) {
16350
+ const resolvedOptions = { ...DEFAULT_OPTIONS, ...options };
16351
+ const out = [];
16352
+ const validInputs = [];
16353
+ const seenSlug = /* @__PURE__ */ new Map();
16354
+ const seenKey = /* @__PURE__ */ new Map();
16355
+ const seenAlias = /* @__PURE__ */ new Map();
16356
+ for (const { path, data } of inputs) {
16357
+ const parsed = parseLinkFrontmatter(data, resolvedOptions);
16358
+ if (!parsed.ok) {
16359
+ for (const e of parsed.errors)
16360
+ out.push(diag(path, "CB_LINK_SHAPE", "error", `invalid link frontmatter: ${e}`));
16361
+ continue;
16362
+ }
16363
+ if (parsed.value === null)
16364
+ continue;
16365
+ const fm = parsed.value;
16366
+ const locale = effectiveLocale(fm, resolvedOptions);
16367
+ validInputs.push({ path, fm });
16368
+ const slugKey = scopedKey(fm.slug, locale, resolvedOptions);
16369
+ const prior = seenSlug.get(slugKey);
16370
+ if (prior)
16371
+ out.push(diag(path, "CB_SLUG_DUPLICATE", "error", `slug "${fm.slug}" also used by ${prior}`));
16372
+ else
16373
+ seenSlug.set(slugKey, path);
16374
+ if (usesKeyResolution(resolvedOptions) && !fm.key) {
16375
+ out.push(diag(path, "CB_KEY_MISSING", "error", `page "${fm.slug}" is missing key`));
16376
+ }
16377
+ if (fm.key) {
16378
+ const keyKey = scopedKey(fm.key, locale, resolvedOptions);
16379
+ const priorKey = seenKey.get(keyKey);
16380
+ if (priorKey)
16381
+ out.push(diag(path, "CB_KEY_DUPLICATE", "error", `key "${fm.key}" also used by ${priorKey}`));
16382
+ else
16383
+ seenKey.set(keyKey, path);
16384
+ }
16385
+ for (const alias of fm.aliases ?? []) {
16386
+ const aliasKey = scopedKey(alias, locale, resolvedOptions);
16387
+ if (seenAlias.has(aliasKey))
16388
+ out.push(diag(path, "CB_ALIAS_CONFLICT", "error", `alias "${alias}" already declared by ${seenAlias.get(aliasKey)}`));
16389
+ else
16390
+ seenAlias.set(aliasKey, path);
16391
+ }
16392
+ }
16393
+ const index = buildLinkIndex(inputs, resolvedOptions);
16394
+ const lookup = buildLookup(index.pages, resolvedOptions);
16395
+ for (const { path, fm } of validInputs) {
16396
+ const page = index.pages.get(pageMapKey(frontmatterIdentity(fm, resolvedOptions), resolvedOptions));
16397
+ if (!page)
16398
+ continue;
16399
+ for (const alias of page.aliases) {
16400
+ if (collidesWithPageIdentity(alias, page.locale, lookup, resolvedOptions))
16401
+ out.push(diag(page.path, "CB_ALIAS_CONFLICT", "error", `alias "${alias}" collides with an existing page identity`));
16402
+ }
16403
+ for (const target of fm.linksTo ?? []) {
16404
+ const resolved = resolveTarget(target, page, lookup, resolvedOptions);
16405
+ if (resolved.page === page) {
16406
+ out.push(diag(page.path, "CB_LINK_SELF", "warning", `page "${page.slug}" links to itself`));
16407
+ continue;
16408
+ }
16409
+ if (resolved.page && resolved.crossLocale) {
16410
+ out.push(diag(page.path, "CB_LINK_CROSS_LOCALE", "warning", `linksTo "${resolved.target}" resolves to locale "${resolved.page.locale}"`));
16411
+ continue;
16412
+ }
16413
+ if (!resolved.page) {
16414
+ if (targetExistsOutsideLocale(resolved.target, page.locale, lookup, resolvedOptions)) {
16415
+ out.push(diag(page.path, "CB_LINK_LOCALE_MISSING", "error", `linksTo "${resolved.target}" exists in another locale but not "${page.locale ?? "default"}"`));
16416
+ continue;
16417
+ }
16418
+ const hint = closest(resolved.target, candidatesFor(page.locale, lookup, resolvedOptions));
16419
+ out.push(diag(page.path, "CB_LINK_UNRESOLVED", "error", `linksTo "${resolved.target}" does not resolve to any page`, hint ? `Did you mean "${hint}"?` : void 0));
16420
+ }
16421
+ }
16422
+ if (page.linkedFrom.length === 0)
16423
+ out.push(diag(page.path, "CB_LINK_ORPHAN", "warning", `page "${page.slug}" has no inbound links`));
16424
+ }
16425
+ return out;
16426
+ }
16427
+ function normalizeFrontmatter(data, options) {
16428
+ const out = { ...data };
16429
+ copyConfiguredField(out, data, options.slugField, "slug");
16430
+ copyConfiguredField(out, data, options.keyField, "key");
16431
+ copyConfiguredField(out, data, options.localeField, "locale");
16432
+ return out;
16433
+ }
16434
+ function copyConfiguredField(out, data, from, to) {
16435
+ if (!from || from === to || !(from in data) || to in out)
16436
+ return;
16437
+ out[to] = data[from];
16438
+ }
16439
+ function effectiveLocale(fm, options) {
16440
+ return fm.locale ?? options.defaultLocale;
16441
+ }
16442
+ function frontmatterIdentity(fm, options) {
16443
+ return { slug: fm.slug, key: fm.key, locale: effectiveLocale(fm, options) };
16444
+ }
16445
+ function pageMapKey(page, options) {
16446
+ if (options.resolve === "global-slug")
16447
+ return page.slug;
16448
+ return scopedKey(page.slug, page.locale, options);
16449
+ }
16450
+ function scopedKey(value, locale, options) {
16451
+ if (options.resolve === "global-slug")
16452
+ return value;
16453
+ return `${locale ?? ""}\0${value}`;
16454
+ }
16455
+ function aliasMapKey(alias, locale, options) {
16456
+ return options.resolve === "global-slug" ? alias : `${locale ?? ""}:${alias}`;
16457
+ }
16458
+ function replacementFor(page, options) {
16459
+ if (options.resolve === "same-locale-key")
16460
+ return page.key ?? page.slug;
16461
+ if (options.resolve === "prefer-same-locale-key-fallback-slug")
16462
+ return page.key ?? page.slug;
16463
+ return page.slug;
16464
+ }
16465
+ function referenceFor(page, target) {
16466
+ return {
16467
+ ...target ? { target } : {},
16468
+ ...page.locale ? { locale: page.locale } : {},
16469
+ slug: page.slug,
16470
+ ...page.key ? { key: page.key } : {}
16471
+ };
16472
+ }
16473
+ function sameReference(ref, page) {
16474
+ return ref.slug === page.slug && ref.locale === page.locale && ref.key === page.key;
16475
+ }
16476
+ function sortedRefs(refs) {
16477
+ return [...refs].sort((a, b) => sortIdentity(a.locale, a.key ?? a.slug).localeCompare(sortIdentity(b.locale, b.key ?? b.slug)));
16478
+ }
16479
+ function sortIdentity(locale, value) {
16480
+ return `${locale ?? ""}\0${value}`;
16481
+ }
16482
+ function buildLookup(pages, options) {
16483
+ const lookup = {
16484
+ bySlug: /* @__PURE__ */ new Map(),
16485
+ byScopedSlug: /* @__PURE__ */ new Map(),
16486
+ byKey: /* @__PURE__ */ new Map(),
16487
+ byScopedKey: /* @__PURE__ */ new Map(),
16488
+ aliasBySlug: /* @__PURE__ */ new Map(),
16489
+ aliasByScopedSlug: /* @__PURE__ */ new Map(),
16490
+ aliasByKey: /* @__PURE__ */ new Map(),
16491
+ aliasByScopedKey: /* @__PURE__ */ new Map()
16492
+ };
16493
+ for (const page of pages.values()) {
16494
+ pushMulti(lookup.bySlug, page.slug, page);
16495
+ lookup.byScopedSlug.set(scopedKey(page.slug, page.locale, options), page);
16496
+ if (page.key) {
16497
+ pushMulti(lookup.byKey, page.key, page);
16498
+ lookup.byScopedKey.set(scopedKey(page.key, page.locale, options), page);
16499
+ }
16500
+ for (const alias of page.aliases) {
16501
+ lookup.aliasBySlug.set(alias, page);
16502
+ lookup.aliasByScopedSlug.set(scopedKey(alias, page.locale, options), page);
16503
+ if (page.key) {
16504
+ lookup.aliasByKey.set(alias, page);
16505
+ lookup.aliasByScopedKey.set(scopedKey(alias, page.locale, options), page);
16506
+ }
16507
+ }
16508
+ }
16509
+ return lookup;
16510
+ }
16511
+ function pushMulti(map2, key, page) {
16512
+ const existing = map2.get(key);
16513
+ if (existing)
16514
+ existing.push(page);
16515
+ else
16516
+ map2.set(key, [page]);
16517
+ }
16518
+ function resolveTarget(rawTarget, source, lookup, options) {
16519
+ if (typeof rawTarget !== "string") {
16520
+ const locale2 = rawTarget.locale ?? source.locale;
16521
+ const page2 = rawTarget.key ? lookup.byScopedKey.get(scopedKey(rawTarget.key, locale2, options)) : rawTarget.slug ? lookup.byScopedSlug.get(scopedKey(rawTarget.slug, locale2, options)) : void 0;
16522
+ const target = rawTarget.key ?? rawTarget.slug ?? "";
16523
+ return {
16524
+ page: page2,
16525
+ target,
16526
+ explicitLocale: rawTarget.locale,
16527
+ crossLocale: Boolean(page2 && rawTarget.locale && rawTarget.locale !== source.locale),
16528
+ matchedBy: rawTarget.key ? "key" : "slug"
16529
+ };
16530
+ }
16531
+ const locale = source.locale;
16532
+ if (options.resolve === "global-slug") {
16533
+ const page2 = lookup.aliasBySlug.get(rawTarget) ?? single(lookup.bySlug.get(rawTarget));
16534
+ return { page: page2, target: rawTarget, crossLocale: false, matchedBy: page2 ? "slug" : void 0 };
16535
+ }
16536
+ if (options.resolve === "same-locale-key") {
16537
+ const scoped2 = scopedKey(rawTarget, locale, options);
16538
+ const page2 = lookup.byScopedKey.get(scoped2) ?? lookup.aliasByScopedKey.get(scoped2);
16539
+ return { page: page2, target: rawTarget, crossLocale: false, matchedBy: page2 ? "key" : void 0 };
16540
+ }
16541
+ if (options.resolve === "prefer-same-locale-key-fallback-slug") {
16542
+ const scoped2 = scopedKey(rawTarget, locale, options);
16543
+ const page2 = lookup.byScopedKey.get(scoped2) ?? lookup.aliasByScopedKey.get(scoped2) ?? lookup.byScopedSlug.get(scoped2) ?? lookup.aliasByScopedSlug.get(scoped2);
16544
+ return { page: page2, target: rawTarget, crossLocale: false, matchedBy: page2 ? "key" : void 0 };
16545
+ }
16546
+ const scoped = scopedKey(rawTarget, locale, options);
16547
+ const page = lookup.byScopedSlug.get(scoped) ?? lookup.aliasByScopedSlug.get(scoped);
16548
+ return { page, target: rawTarget, crossLocale: false, matchedBy: page ? "slug" : void 0 };
16549
+ }
16550
+ function single(values) {
16551
+ return values?.length === 1 ? values[0] : void 0;
16552
+ }
16553
+ function usesKeyResolution(options) {
16554
+ return options.resolve === "same-locale-key";
16555
+ }
16556
+ function collidesWithPageIdentity(alias, locale, lookup, options) {
16557
+ if (options.resolve === "global-slug")
16558
+ return lookup.bySlug.has(alias);
16559
+ const scoped = scopedKey(alias, locale, options);
16560
+ return lookup.byScopedSlug.has(scoped) || usesKeyResolution(options) && lookup.byScopedKey.has(scoped);
16561
+ }
16562
+ function targetExistsOutsideLocale(target, locale, lookup, options) {
16563
+ if (options.resolve === "global-slug")
16564
+ return false;
16565
+ const byIdentity = usesKeyResolution(options) ? lookup.byKey : lookup.bySlug;
16566
+ return (byIdentity.get(target) ?? []).some((page) => page.locale !== locale);
16567
+ }
16568
+ function candidatesFor(locale, lookup, options) {
16569
+ if (options.resolve === "global-slug")
16570
+ return [...lookup.bySlug.keys()];
16571
+ const out = [];
16572
+ for (const page of lookup.byScopedSlug.values()) {
16573
+ if (page.locale === locale)
16574
+ out.push(usesKeyResolution(options) && page.key ? page.key : page.slug);
16575
+ }
16576
+ return out;
16577
+ }
16578
+ function inputForPage(page, inputs, options) {
16579
+ for (const input of inputs) {
16580
+ const parsed = parseLinkFrontmatter(input.data, options);
16581
+ if (!parsed.ok || parsed.value === null)
16582
+ continue;
16583
+ const identity = frontmatterIdentity(parsed.value, options);
16584
+ if (pageMapKey(identity, options) === pageMapKey(page, options))
16585
+ return input.data;
16586
+ }
16587
+ return {};
16588
+ }
16589
+ function diag(file2, code2, severity, message, hint) {
16590
+ return { file: file2, diagnostic: { code: code2, severity, message, hint, position: FM_POSITION } };
16591
+ }
16592
+ function editDistance(a, b) {
16593
+ const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
16594
+ for (let j = 0; j <= b.length; j++)
16595
+ dp[0][j] = j;
16596
+ for (let i = 1; i <= a.length; i++) {
16597
+ for (let j = 1; j <= b.length; j++) {
16598
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
16599
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
16600
+ }
16601
+ }
16602
+ return dp[a.length][b.length];
16603
+ }
16604
+ function closest(target, candidates) {
16605
+ let best;
16606
+ let bestD = Infinity;
16607
+ for (const c of candidates) {
16608
+ const d = editDistance(target, c);
16609
+ if (d < bestD) {
16610
+ bestD = d;
16611
+ best = c;
16612
+ }
16613
+ }
16614
+ return best && bestD <= Math.max(2, Math.floor(target.length / 3)) ? best : void 0;
16615
+ }
16616
+ var Keywords, LinkTarget, LinkFrontmatter, DEFAULT_OPTIONS, FM_POSITION;
16617
+ var init_links = __esm({
16618
+ "../core/dist/links.js"() {
16619
+ "use strict";
16620
+ init_zod();
16621
+ Keywords = external_exports.object({
16622
+ primary: external_exports.string().optional(),
16623
+ secondary: external_exports.array(external_exports.string()).optional()
16624
+ });
16625
+ LinkTarget = external_exports.union([
16626
+ external_exports.string(),
16627
+ external_exports.object({
16628
+ slug: external_exports.string().min(1).optional(),
16629
+ key: external_exports.string().min(1).optional(),
16630
+ locale: external_exports.string().min(1).optional()
16631
+ }).refine((target) => target.slug || target.key, {
16632
+ message: "object linksTo targets must include slug or key"
16633
+ })
16634
+ ]);
16635
+ LinkFrontmatter = external_exports.object({
16636
+ slug: external_exports.string().min(1),
16637
+ key: external_exports.string().min(1).optional(),
16638
+ locale: external_exports.string().min(1).optional(),
16639
+ title: external_exports.string().optional(),
16640
+ linksTo: external_exports.array(LinkTarget).optional(),
16641
+ aliases: external_exports.array(external_exports.string()).optional(),
16642
+ keywords: Keywords.optional()
16643
+ });
16644
+ DEFAULT_OPTIONS = {
16645
+ resolve: "global-slug"
16646
+ };
16647
+ FM_POSITION = {
16648
+ start: { line: 1, column: 1, offset: 0 },
16649
+ end: { line: 1, column: 1, offset: 0 }
16650
+ };
16651
+ }
16652
+ });
16653
+
16654
+ // ../core/dist/index.js
16655
+ var init_dist = __esm({
16656
+ "../core/dist/index.js"() {
16657
+ "use strict";
16658
+ init_diagnostics();
16659
+ init_parser();
16660
+ init_frontmatter();
16661
+ init_analyze();
16662
+ init_registry();
16663
+ init_content_models();
16664
+ init_validate();
16665
+ init_render_markdown();
16666
+ init_links();
16667
+ }
16668
+ });
16669
+
16196
16670
  // ../blocks/dist/blocks/callout.js
16197
16671
  var calloutBlock, calloutMarkdown;
16198
16672
  var init_callout = __esm({
@@ -16617,7 +17091,7 @@ var TEMPLATE_VERSION, AUTHOR_SKILL, AUDIT_SKILL, AGENTS_MD_BLOCK, START, END;
16617
17091
  var init_agents = __esm({
16618
17092
  "src/commands/agents.ts"() {
16619
17093
  "use strict";
16620
- TEMPLATE_VERSION = 1;
17094
+ TEMPLATE_VERSION = 2;
16621
17095
  AUTHOR_SKILL = `---
16622
17096
  name: contentbit-author
16623
17097
  description: |
@@ -16640,6 +17114,8 @@ Check \`package.json\` for a \`content:check\` script. It holds the canonical
16640
17114
  validate invocation for this project: the content glob and, if present, the
16641
17115
  \`--registry <path>\` flag pointing at custom block definitions. Reuse both
16642
17116
  below. No script? Default to \`content/**/*.md\` with no \`--registry\` flag.
17117
+ If the project has a \`content:links\` script, use it for the internal-link
17118
+ index; otherwise run \`contentbit links <content glob>\` directly.
16643
17119
 
16644
17120
  ## The loop
16645
17121
 
@@ -16654,7 +17130,19 @@ below. No script? Default to \`content/**/*.md\` with no \`--registry\` flag.
16654
17130
 
16655
17131
  2. **Write the document.** Plain Markdown everywhere; blocks only where the
16656
17132
  guide's use-when guidance fits. Keep frontmatter consistent with sibling
16657
- documents in the same folder.
17133
+ documents in the same folder. If sibling documents use \`slug\`, \`linksTo\`,
17134
+ \`aliases\`, or \`keywords\`, run the link index first:
17135
+
17136
+ \`\`\`sh
17137
+ contentbit links <content glob>
17138
+ \`\`\`
17139
+
17140
+ Read \`.contentbit/link-index.json\` to pick existing slugs and related
17141
+ pages. Author only \`slug\`, \`linksTo\`, \`aliases\`, and \`keywords\` in
17142
+ frontmatter; never write derived \`linkedFrom\` into source files. When
17143
+ creating a linked page, include \`keywords.primary\` and
17144
+ \`keywords.secondary\` with search-intent phrases that would help future
17145
+ agents choose this page as a \`linksTo\` target.
16658
17146
 
16659
17147
  3. **Validate and fix until clean:**
16660
17148
 
@@ -16664,8 +17152,19 @@ below. No script? Default to \`content/**/*.md\` with no \`--registry\` flag.
16664
17152
 
16665
17153
  Diagnostics print to stderr as \`file:line:col severity CODE message\`, often
16666
17154
  with a \`hint:\` line suggesting the fix. Exit 0 means clean; exit 1 means
16667
- errors remain. Fix every diagnostic and re-run. Never finish with a failing
16668
- validate.
17155
+ errors remain. If the document has link frontmatter, validate the full
17156
+ content glob so cross-file links are checked against the whole graph. Fix
17157
+ every diagnostic and re-run. Never finish with a failing validate.
17158
+
17159
+ 4. **Refresh internal links when present:**
17160
+
17161
+ \`\`\`sh
17162
+ contentbit links <content glob> --fix
17163
+ \`\`\`
17164
+
17165
+ \`--fix\` only rewrites \`linksTo\` values that point at known aliases. It
17166
+ does not invent links, remove aliases, or write backlinks. Re-run validate
17167
+ after it changes files.
16669
17168
 
16670
17169
  ## Failure modes
16671
17170
 
@@ -16687,6 +17186,8 @@ version: ${TEMPLATE_VERSION}
16687
17186
 
16688
17187
  \`contentbit stats\` analyzes documents and prints JSON to stdout. It is a read
16689
17188
  tool: it always exits 0, even when documents have validation errors.
17189
+ \`contentbit links\` builds the frontmatter-authored internal-link graph and
17190
+ prints link diagnostics.
16690
17191
 
16691
17192
  ## Gather
16692
17193
 
@@ -16695,23 +17196,31 @@ content glob and \`--registry\` flag, then:
16695
17196
 
16696
17197
  \`\`\`sh
16697
17198
  contentbit stats "content/**/*.md" [--registry <path>]
17199
+ contentbit links "content/**/*.md"
16698
17200
  \`\`\`
16699
17201
 
16700
17202
  One matched file prints a single stats object; multiple files print an array.
16701
17203
  Each entry includes the file path, frontmatter data, a heading \`outline\` with
16702
17204
  per-section word counts, \`blocks.byName\` usage counts, \`links.domains\`, and
16703
17205
  a \`validation\` summary (\`errors\`/\`warnings\`).
17206
+ \`contentbit links\` also writes \`.contentbit/link-index.json\`, whose pages
17207
+ contain \`slug\`, resolved \`linksTo\`, derived \`linkedFrom\`, \`aliases\`, and
17208
+ \`keywords\`.
16704
17209
 
16705
17210
  ## Interpret
16706
17211
 
16707
17212
  Prioritize findings in this order:
16708
17213
 
16709
17214
  1. **Validation errors and warnings** \u2014 broken content ships broken pages.
16710
- 2. **Thin documents** \u2014 outline sections with very low word counts.
16711
- 3. **Block-less documents** \u2014 \`blocks.byName\` empty where sibling documents
17215
+ 2. **Internal-link errors** \u2014 unresolved links, duplicate slugs, and alias
17216
+ conflicts from \`contentbit links\`.
17217
+ 3. **Orphans and self-links** \u2014 link warnings that point to isolated or noisy
17218
+ pages.
17219
+ 4. **Thin documents** \u2014 outline sections with very low word counts.
17220
+ 5. **Block-less documents** \u2014 \`blocks.byName\` empty where sibling documents
16712
17221
  use blocks; structure (steps, callouts, comparisons, faq) may be missing.
16713
- 4. **Missing or inconsistent frontmatter** compared to sibling documents.
16714
- 5. **Structural imbalance** \u2014 skipped heading levels, single-section walls of text.
17222
+ 6. **Missing or inconsistent frontmatter** compared to sibling documents.
17223
+ 7. **Structural imbalance** \u2014 skipped heading levels, single-section walls of text.
16715
17224
 
16716
17225
  ## Report
16717
17226
 
@@ -16727,14 +17236,22 @@ This project validates Markdown content with contentbit. Documents are plain
16727
17236
  Markdown plus directive blocks (\`:::name{props} ... :::\`), each with a schema.
16728
17237
  The \`content:check\` script in package.json holds the canonical validate
16729
17238
  command \u2014 the content glob and the \`--registry\` flag \u2014 reuse its arguments.
17239
+ If the project has a \`content:links\` script, use it to build the internal-link
17240
+ index; otherwise run \`contentbit links <content glob>\`.
16730
17241
 
16731
17242
  When writing or editing content:
16732
17243
 
16733
17244
  1. Fetch the live authoring guide first \u2014 never guess block syntax:
16734
17245
  \`contentbit instructions --audience llm [--registry <path>]\`
16735
17246
  2. Write plain Markdown; use blocks where the guide's use-when guidance fits.
16736
- 3. Validate until clean (exit 0): \`contentbit validate <file> [--registry <path>]\`.
17247
+ 3. If sibling documents use \`slug\` / \`linksTo\`, read
17248
+ \`.contentbit/link-index.json\` from \`contentbit links <content glob>\` and
17249
+ author frontmatter links with existing slugs. When creating a linked page,
17250
+ include \`keywords.primary\` and \`keywords.secondary\` with search-intent
17251
+ phrases future agents can use to choose related pages.
17252
+ 4. Validate until clean (exit 0): \`contentbit validate <file> [--registry <path>]\`.
16737
17253
  Diagnostics print as \`file:line:col severity CODE message\` with fix hints.
17254
+ For link frontmatter, validate the full content glob so cross-file checks run.
16738
17255
 
16739
17256
  When auditing content health:
16740
17257
 
@@ -16742,6 +17259,9 @@ When auditing content health:
16742
17259
  and always exits 0: outline word counts, block usage, link domains, and
16743
17260
  validation error/warning counts. Flag validation issues, thin documents, and
16744
17261
  block-less pages first.
17262
+ - \`contentbit links "content/**/*.md" [--fix]\` builds
17263
+ \`.contentbit/link-index.json\`, reports dangling links/orphans, and rewrites
17264
+ alias references in \`linksTo\` when \`--fix\` is used.
16745
17265
 
16746
17266
  If \`contentbit\` is unavailable, suggest \`npx contentbit@latest init\` instead
16747
17267
  of inventing block syntax.
@@ -16809,7 +17329,7 @@ import { ContentRenderer } from '@/components/content-blocks/content-renderer'`
16809
17329
  return `'use client'
16810
17330
 
16811
17331
  import { genericBlocks } from '@contentbit/blocks'
16812
- import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
17332
+ import { createBlockRegistry, parseDocument, stripFrontmatter, validateDocument } from '@contentbit/core'
16813
17333
  ${reactImport}${mdImport}${rendererImport}
16814
17334
  // Everything block-related lives in the blocks/ folder: definitions in
16815
17335
  // registry.ts (shared with the validate CLI), components in components.tsx.
@@ -16819,7 +17339,7 @@ import { blockComponents } from '${blocksImport}/components'
16819
17339
  const registry = createBlockRegistry().use(genericBlocks()).use(customBlocks)
16820
17340
 
16821
17341
  export function Content({ source }: { source: string }) {
16822
- const result = validateDocument(parseDocument(source), registry)
17342
+ const result = validateDocument(parseDocument(stripFrontmatter(source)), registry)
16823
17343
  return (
16824
17344
  <${renderer}
16825
17345
  document={result.document}
@@ -16839,14 +17359,14 @@ const renderMarkdown = (md) => mdIt.render(md)` : `// TODO: plug a Markdown libr
16839
17359
  const renderMarkdown = undefined`;
16840
17360
  return `// Render content/example.md to example.html. Run: node scripts/render-example.mjs
16841
17361
  import { genericBlocks } from '@contentbit/blocks'
16842
- import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
17362
+ import { createBlockRegistry, parseDocument, stripFrontmatter, validateDocument } from '@contentbit/core'
16843
17363
  import { renderToHtml } from '@contentbit/html'
16844
17364
  import { readFile, writeFile } from 'node:fs/promises'
16845
17365
  ${wiring}
16846
17366
 
16847
17367
  const source = await readFile('content/example.md', 'utf8')
16848
17368
  const registry = createBlockRegistry().use(genericBlocks())
16849
- const result = validateDocument(parseDocument(source), registry)
17369
+ const result = validateDocument(parseDocument(stripFrontmatter(source)), registry)
16850
17370
  const html = renderToHtml(result.document, { renderMarkdown })
16851
17371
  await writeFile('example.html', html, 'utf8')
16852
17372
  console.log('wrote example.html')
@@ -17059,7 +17579,8 @@ async function initCommand(args, io) {
17059
17579
  }
17060
17580
  const files = [
17061
17581
  ["blocks/registry.ts", REGISTRY_TEMPLATE],
17062
- ["content/example.md", EXAMPLE_CONTENT]
17582
+ ["content/example.md", EXAMPLE_CONTENT],
17583
+ ["content/related.md", RELATED_CONTENT]
17063
17584
  ];
17064
17585
  const layout = detectFramework(cwd, { ...pkg.dependencies, ...pkg.devDependencies });
17065
17586
  let styled = false;
@@ -17113,9 +17634,15 @@ async function initCommand(args, io) {
17113
17634
  fresh.scripts ??= {};
17114
17635
  if (!fresh.scripts["content:check"]) {
17115
17636
  fresh.scripts["content:check"] = 'contentbit validate "content/**/*.md" --registry ./blocks/registry.ts';
17637
+ io.stdout("added script: content:check");
17638
+ }
17639
+ if (!fresh.scripts["content:links"]) {
17640
+ fresh.scripts["content:links"] = 'contentbit links "content/**/*.md"';
17641
+ io.stdout("added script: content:links");
17642
+ }
17643
+ if (!pkg.scripts?.["content:check"] || !pkg.scripts?.["content:links"]) {
17116
17644
  await writeFile2(pkgPath, `${JSON.stringify(fresh, null, 2)}
17117
17645
  `, "utf8");
17118
- io.stdout("added script: content:check");
17119
17646
  }
17120
17647
  let registry2;
17121
17648
  try {
@@ -17134,6 +17661,7 @@ async function initCommand(args, io) {
17134
17661
  io.stdout("");
17135
17662
  io.stdout("Done. Next steps:");
17136
17663
  io.stdout(` 1. Validate the starter content: ${detectPackageManager(cwd)} run content:check`);
17664
+ io.stdout(` Build the link index: ${detectPackageManager(cwd)} run content:links`);
17137
17665
  if (target === "react") {
17138
17666
  if (!values["no-page"] && layout.pagePath) {
17139
17667
  io.stdout(" 2. Start the dev server and open /example to see the article rendered.");
@@ -17153,7 +17681,7 @@ async function initCommand(args, io) {
17153
17681
  io.stdout(" Docs: https://contentbit.dev/docs");
17154
17682
  return 0;
17155
17683
  }
17156
- var TARGETS, MD_CHOICES, REGISTRY_TEMPLATE, EXAMPLE_CONTENT, TANSTACK_PAGE, NEXT_PAGE, ASTRO_CONTENT_CONFIG, ASTRO_QUOTE_BLOCK;
17684
+ var TARGETS, MD_CHOICES, REGISTRY_TEMPLATE, EXAMPLE_CONTENT, RELATED_CONTENT, TANSTACK_PAGE, NEXT_PAGE, ASTRO_CONTENT_CONFIG, ASTRO_QUOTE_BLOCK;
17157
17685
  var init_init = __esm({
17158
17686
  "src/commands/init.ts"() {
17159
17687
  "use strict";
@@ -17195,7 +17723,18 @@ export const quote = defineBlock({
17195
17723
 
17196
17724
  export default [quote] satisfies BlockDefinition<unknown>[]
17197
17725
  `;
17198
- EXAMPLE_CONTENT = `# Hello, Content Blocks
17726
+ EXAMPLE_CONTENT = `---
17727
+ slug: hello-content-blocks
17728
+ linksTo:
17729
+ - related-contentbit-workflows
17730
+ aliases:
17731
+ - getting-started-contentbit
17732
+ keywords:
17733
+ primary: validated Markdown blocks
17734
+ secondary: [content workflow, agent writing]
17735
+ ---
17736
+
17737
+ # Hello, Content Blocks
17199
17738
 
17200
17739
  Regular Markdown works everywhere. Blocks add validated structure:
17201
17740
 
@@ -17216,6 +17755,27 @@ by the \`QuoteBlock\` component, in about twenty lines:
17216
17755
  The Analytical Engine weaves algebraic patterns just as the Jacquard loom
17217
17756
  weaves flowers and leaves.
17218
17757
  :::
17758
+ `;
17759
+ RELATED_CONTENT = `---
17760
+ slug: related-contentbit-workflows
17761
+ linksTo:
17762
+ - hello-content-blocks
17763
+ keywords:
17764
+ primary: contentbit workflow
17765
+ secondary: [validation loop, internal links]
17766
+ ---
17767
+
17768
+ # Related contentbit workflows
17769
+
17770
+ This supporting page exists to show internal links in frontmatter. The link
17771
+ graph is authored once with \`slug\` and \`linksTo\`, then contentbit derives
17772
+ \`linkedFrom\` in \`.contentbit/link-index.json\`.
17773
+
17774
+ :::callout{type="note"}
17775
+ Run \`contentbit links "content/**/*.md" --fix\` after renaming a page. Alias
17776
+ references in \`linksTo\` are rewritten to the current slug, while \`aliases\`
17777
+ stays as the rename record.
17778
+ :::
17219
17779
  `;
17220
17780
  TANSTACK_PAGE = `import { createFileRoute } from '@tanstack/react-router'
17221
17781
 
@@ -17280,6 +17840,36 @@ const { author, role } = Astro.props
17280
17840
  }
17281
17841
  });
17282
17842
 
17843
+ // src/link-options.ts
17844
+ function linkResolverOptions(values) {
17845
+ const out = {};
17846
+ const resolve = stringValue(values["link-resolve"]);
17847
+ if (resolve) {
17848
+ if (!isResolveMode(resolve)) throw new Error(`invalid --link-resolve ${resolve}`);
17849
+ out.resolve = resolve;
17850
+ }
17851
+ const localeField = stringValue(values["locale-field"]);
17852
+ const slugField = stringValue(values["slug-field"]);
17853
+ const keyField = stringValue(values["key-field"]);
17854
+ const defaultLocale = stringValue(values["default-locale"]);
17855
+ if (localeField) out.localeField = localeField;
17856
+ if (slugField) out.slugField = slugField;
17857
+ if (keyField) out.keyField = keyField;
17858
+ if (defaultLocale) out.defaultLocale = defaultLocale;
17859
+ return out;
17860
+ }
17861
+ function stringValue(value) {
17862
+ return typeof value === "string" && value.length > 0 ? value : void 0;
17863
+ }
17864
+ function isResolveMode(value) {
17865
+ return value === "global-slug" || value === "same-locale-slug" || value === "same-locale-key" || value === "prefer-same-locale-key-fallback-slug";
17866
+ }
17867
+ var init_link_options = __esm({
17868
+ "src/link-options.ts"() {
17869
+ "use strict";
17870
+ }
17871
+ });
17872
+
17283
17873
  // src/commands/validate.ts
17284
17874
  var validate_exports = {};
17285
17875
  __export(validate_exports, {
@@ -17294,7 +17884,12 @@ async function validateCommand(args, io) {
17294
17884
  allowPositionals: true,
17295
17885
  options: {
17296
17886
  registry: { type: "string" },
17297
- "strict-warnings": { type: "boolean", default: false }
17887
+ "strict-warnings": { type: "boolean", default: false },
17888
+ "link-resolve": { type: "string" },
17889
+ "locale-field": { type: "string" },
17890
+ "slug-field": { type: "string" },
17891
+ "key-field": { type: "string" },
17892
+ "default-locale": { type: "string" }
17298
17893
  }
17299
17894
  });
17300
17895
  if (positionals.length === 0) {
@@ -17307,10 +17902,13 @@ async function validateCommand(args, io) {
17307
17902
  return 2;
17308
17903
  }
17309
17904
  const registry2 = await loadRegistry(values.registry);
17905
+ const linkOptions = linkResolverOptions(values);
17310
17906
  let errors = 0;
17311
17907
  let warnings = 0;
17908
+ const linkInputs = [];
17312
17909
  for (const file2 of files.sort()) {
17313
17910
  const source = await readFile3(file2, "utf8");
17911
+ linkInputs.push({ path: file2, data: extractFrontmatter(source)?.data ?? {} });
17314
17912
  const result = validateDocument(parseDocument(stripFrontmatter(source)), registry2);
17315
17913
  for (const d of result.diagnostics) {
17316
17914
  io.stderr(formatDiagnostic(d, file2));
@@ -17318,6 +17916,13 @@ async function validateCommand(args, io) {
17318
17916
  else if (d.severity === "warning") warnings++;
17319
17917
  }
17320
17918
  }
17919
+ if (linkInputs.some((i) => "slug" in i.data)) {
17920
+ for (const { file: file2, diagnostic } of validateLinks(linkInputs, linkOptions)) {
17921
+ io.stderr(formatDiagnostic(diagnostic, file2));
17922
+ if (diagnostic.severity === "error") errors++;
17923
+ else if (diagnostic.severity === "warning") warnings++;
17924
+ }
17925
+ }
17321
17926
  io.stdout(`${files.length} file(s): ${errors} errors, ${warnings} warnings`);
17322
17927
  if (errors > 0) return 1;
17323
17928
  if (warnings > 0 && values["strict-warnings"]) return 1;
@@ -17327,6 +17932,7 @@ var init_validate2 = __esm({
17327
17932
  "src/commands/validate.ts"() {
17328
17933
  "use strict";
17329
17934
  init_dist();
17935
+ init_link_options();
17330
17936
  init_load_registry();
17331
17937
  }
17332
17938
  });
@@ -17544,7 +18150,7 @@ async function renderCommand(args, io) {
17544
18150
  }
17545
18151
  const registry2 = await loadRegistry(values.registry);
17546
18152
  const source = await readFile5(file2, "utf8");
17547
- const result = validateDocument(parseDocument(source), registry2);
18153
+ const result = validateDocument(parseDocument(stripFrontmatter(source)), registry2);
17548
18154
  if (!result.ok) {
17549
18155
  for (const d of result.diagnostics) io.stderr(formatDiagnostic(d, file2));
17550
18156
  return 1;
@@ -17623,20 +18229,136 @@ var init_docs = __esm({
17623
18229
  }
17624
18230
  });
17625
18231
 
18232
+ // src/links-io.ts
18233
+ import { readFile as readFile6 } from "node:fs/promises";
18234
+ async function collectLinkInputs(files) {
18235
+ const inputs = [];
18236
+ for (const path of files) {
18237
+ const source = await readFile6(path, "utf8");
18238
+ const fm = extractFrontmatter(source);
18239
+ inputs.push({ path, data: fm?.data ?? {} });
18240
+ }
18241
+ return inputs;
18242
+ }
18243
+ var init_links_io = __esm({
18244
+ "src/links-io.ts"() {
18245
+ "use strict";
18246
+ init_dist();
18247
+ }
18248
+ });
18249
+
18250
+ // src/commands/links.ts
18251
+ var links_exports = {};
18252
+ __export(links_exports, {
18253
+ linksCommand: () => linksCommand
18254
+ });
18255
+ import { mkdir as mkdir3, readFile as readFile7 } from "node:fs/promises";
18256
+ import { dirname, join as join3 } from "node:path";
18257
+ import { parseArgs as parseArgs8 } from "node:util";
18258
+ import { glob as glob3 } from "tinyglobby";
18259
+ async function linksCommand(args, io) {
18260
+ const { values, positionals } = parseArgs8({
18261
+ args,
18262
+ allowPositionals: true,
18263
+ options: {
18264
+ out: { type: "string" },
18265
+ fix: { type: "boolean", default: false },
18266
+ "link-resolve": { type: "string" },
18267
+ "locale-field": { type: "string" },
18268
+ "slug-field": { type: "string" },
18269
+ "key-field": { type: "string" },
18270
+ "default-locale": { type: "string" }
18271
+ }
18272
+ });
18273
+ if (positionals.length === 0) {
18274
+ io.stderr("links: provide at least one file or glob.");
18275
+ return 2;
18276
+ }
18277
+ const files = (await glob3(positionals, { absolute: true })).sort();
18278
+ if (files.length === 0) {
18279
+ io.stderr(`links: no files matched ${positionals.join(" ")}`);
18280
+ return 2;
18281
+ }
18282
+ const inputs = await collectLinkInputs(files);
18283
+ const linkOptions = linkResolverOptions(values);
18284
+ let errors = 0;
18285
+ let warnings = 0;
18286
+ for (const { file: file2, diagnostic } of validateLinks(inputs, linkOptions)) {
18287
+ io.stderr(formatDiagnostic(diagnostic, file2));
18288
+ if (diagnostic.severity === "error") errors++;
18289
+ else if (diagnostic.severity === "warning") warnings++;
18290
+ }
18291
+ const index = buildLinkIndex(inputs, linkOptions);
18292
+ if (values.fix && errors > 0) {
18293
+ io.stderr("links: --fix skipped because link errors must be resolved first.");
18294
+ } else if (values.fix && index.aliases.size > 0) {
18295
+ for (const file2 of files) {
18296
+ const source = await readFile7(file2, "utf8");
18297
+ const fm = extractFrontmatter(source);
18298
+ if (!fm) continue;
18299
+ const lines = source.split("\n");
18300
+ let changed = false;
18301
+ let inLinksTo = false;
18302
+ for (let i = 0; i < fm.lines.end && i < lines.length; i++) {
18303
+ const line = lines[i];
18304
+ const topKey = line.match(/^([A-Za-z0-9_.-]+):(.*)$/);
18305
+ if (topKey) inLinksTo = topKey[1] === "linksTo";
18306
+ if (!inLinksTo) continue;
18307
+ let next = line;
18308
+ for (const [alias, current] of aliasReplacementsForPage(index, fm.data)) {
18309
+ const re = new RegExp(`(^|[\\s\\[,'"-])${escapeRe(alias)}($|[\\s\\],'"])`, "g");
18310
+ next = next.replace(re, (_m, p1, p2) => `${p1}${current}${p2}`);
18311
+ }
18312
+ if (next !== line) {
18313
+ lines[i] = next;
18314
+ changed = true;
18315
+ }
18316
+ }
18317
+ if (changed) {
18318
+ await io.writeFile(file2, lines.join("\n"));
18319
+ io.stdout(`fixed alias references in ${file2}`);
18320
+ }
18321
+ }
18322
+ }
18323
+ const outPath = values.out ?? join3(process.cwd(), ".contentbit", "link-index.json");
18324
+ await mkdir3(dirname(outPath), { recursive: true });
18325
+ await io.writeFile(outPath, JSON.stringify(serializeLinkIndex(index), null, 2) + "\n");
18326
+ let edges = 0;
18327
+ for (const p of index.pages.values()) edges += p.linksTo.length;
18328
+ const orphans = [...index.pages.values()].filter((p) => p.linkedFrom.length === 0).length;
18329
+ io.stdout(
18330
+ `${index.pages.size} page(s), ${edges} link(s), ${orphans} orphan(s): ${errors} errors, ${warnings} warnings`
18331
+ );
18332
+ io.stdout(`index written to ${outPath}`);
18333
+ return errors > 0 ? 1 : 0;
18334
+ }
18335
+ function escapeRe(s) {
18336
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18337
+ }
18338
+ var init_links2 = __esm({
18339
+ "src/commands/links.ts"() {
18340
+ "use strict";
18341
+ init_dist();
18342
+ init_link_options();
18343
+ init_links_io();
18344
+ }
18345
+ });
18346
+
17626
18347
  // src/bin.ts
17627
18348
  import { writeFile as writeFile3 } from "node:fs/promises";
17628
18349
 
17629
18350
  // src/run.ts
17630
- var USAGE = `Usage: contentbit <init|validate|stats|render|instructions|docs|agents> [options]
18351
+ var USAGE = `Usage: contentbit <init|validate|stats|render|instructions|docs|agents|links> [options]
17631
18352
 
17632
18353
  init [-t react|html|markdown|astro] [--md ...] [-y] [--no-install] [--no-page] [--no-agents]
17633
18354
  agents [--claude] [--no-agents-md]
17634
18355
 
17635
- validate <globs...> [--registry <module.mjs>] [--strict-warnings]
18356
+ validate <globs...> [--registry <module.mjs>] [--strict-warnings] [--link-resolve <mode>]
17636
18357
  stats <globs...> [--registry <module.mjs>] [--no-validate]
17637
18358
  render <file> --target html|markdown [--registry <module.mjs>] [--out <file>]
17638
18359
  instructions [--audience llm|human] [--no-examples] [--registry <module.mjs>] [--out <file>]
17639
- docs [--registry <module.mjs>] [--out <file>]`;
18360
+ docs [--registry <module.mjs>] [--out <file>]
18361
+ links <globs...> [--fix] [--out <file>] [--link-resolve <mode>]`;
17640
18362
  var commands = {
17641
18363
  init: async () => (await Promise.resolve().then(() => (init_init(), init_exports))).initCommand,
17642
18364
  validate: async () => (await Promise.resolve().then(() => (init_validate2(), validate_exports))).validateCommand,
@@ -17644,7 +18366,8 @@ var commands = {
17644
18366
  render: async () => (await Promise.resolve().then(() => (init_render2(), render_exports))).renderCommand,
17645
18367
  instructions: async () => (await Promise.resolve().then(() => (init_instructions(), instructions_exports))).instructionsCommand,
17646
18368
  docs: async () => (await Promise.resolve().then(() => (init_docs(), docs_exports))).docsCommand,
17647
- agents: async () => (await Promise.resolve().then(() => (init_agents(), agents_exports))).agentsCommand
18369
+ agents: async () => (await Promise.resolve().then(() => (init_agents(), agents_exports))).agentsCommand,
18370
+ links: async () => (await Promise.resolve().then(() => (init_links2(), links_exports))).linksCommand
17648
18371
  };
17649
18372
  async function run(argv, io) {
17650
18373
  const [name, ...rest] = argv;