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