contentbit 0.1.1 → 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
@@ -320,6 +320,383 @@ var init_parser = __esm({
320
320
  }
321
321
  });
322
322
 
323
+ // ../core/dist/frontmatter.js
324
+ function stripFrontmatter(source) {
325
+ const match = FM_RE.exec(source);
326
+ if (!match)
327
+ return source;
328
+ return match[0].replace(/[^\n]+/g, "") + source.slice(match[0].length);
329
+ }
330
+ function extractFrontmatter(source) {
331
+ const match = source.match(FM_RE);
332
+ if (!match)
333
+ return null;
334
+ const blockLines = match[0].replace(/\r?\n$/, "").split(/\r?\n/);
335
+ const inner = blockLines.slice(1, -1);
336
+ const { data, keys } = parseYamlSubset(inner);
337
+ return { raw: inner.join("\n"), data, keys, lines: { start: 1, end: blockLines.length } };
338
+ }
339
+ function parseYamlSubset(lines) {
340
+ const data = {};
341
+ const keys = [];
342
+ let i = 0;
343
+ while (i < lines.length) {
344
+ const line = lines[i];
345
+ const trimmed = line.trim();
346
+ if (trimmed === "" || trimmed.startsWith("#") || /^[ \t]/.test(line)) {
347
+ i++;
348
+ continue;
349
+ }
350
+ const m = line.match(KEY_RE2);
351
+ if (!m) {
352
+ i++;
353
+ continue;
354
+ }
355
+ const [, key, rawValue] = m;
356
+ const value = rawValue.trim();
357
+ i++;
358
+ const indented = [];
359
+ while (i < lines.length && /^[ \t]/.test(lines[i]) && lines[i].trim() !== "") {
360
+ indented.push(lines[i]);
361
+ i++;
362
+ }
363
+ keys.push(key);
364
+ data[key] = parseValue(value, indented);
365
+ }
366
+ return { data, keys };
367
+ }
368
+ function parseValue(value, indented) {
369
+ if (/^[|>][+-]?$/.test(value))
370
+ return dedent(indented).join("\n");
371
+ if (value === "") {
372
+ if (indented.length === 0)
373
+ return null;
374
+ const items = dedent(indented);
375
+ const list = parseDashList(items);
376
+ if (list)
377
+ return list;
378
+ const mapping = parseNestedMapping(items);
379
+ if (mapping)
380
+ return mapping;
381
+ return items.join("\n");
382
+ }
383
+ return parseScalar(value);
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
+ }
427
+ function dedent(lines) {
428
+ const indent = Math.min(...lines.map((l) => l.match(/^[ \t]*/)[0].length));
429
+ return lines.map((l) => l.slice(indent));
430
+ }
431
+ function parseScalar(value) {
432
+ if (value === "" || value === "null" || value === "~")
433
+ return null;
434
+ if (value === "true")
435
+ return true;
436
+ if (value === "false")
437
+ return false;
438
+ if (/^[+-]?\d+$/.test(value))
439
+ return Number.parseInt(value, 10);
440
+ if (/^[+-]?\d*\.\d+$/.test(value))
441
+ return Number.parseFloat(value);
442
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
443
+ try {
444
+ return JSON.parse(value);
445
+ } catch {
446
+ return value.slice(1, -1);
447
+ }
448
+ }
449
+ if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
450
+ return value.slice(1, -1).replace(/''/g, "'");
451
+ }
452
+ if (value.startsWith("[") && value.endsWith("]")) {
453
+ const inner = value.slice(1, -1).trim();
454
+ if (inner === "")
455
+ return [];
456
+ return splitInlineItems(inner).map((item) => parseScalar(item.trim()));
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
+ }
472
+ return value;
473
+ }
474
+ function splitInlineItems(inner) {
475
+ const items = [];
476
+ let current = "";
477
+ let quote = null;
478
+ for (const ch of inner) {
479
+ if (quote) {
480
+ current += ch;
481
+ if (ch === quote)
482
+ quote = null;
483
+ } else if (ch === '"' || ch === "'") {
484
+ current += ch;
485
+ quote = ch;
486
+ } else if (ch === ",") {
487
+ items.push(current);
488
+ current = "";
489
+ } else {
490
+ current += ch;
491
+ }
492
+ }
493
+ items.push(current);
494
+ return items;
495
+ }
496
+ var FM_RE, KEY_RE2;
497
+ var init_frontmatter = __esm({
498
+ "../core/dist/frontmatter.js"() {
499
+ "use strict";
500
+ FM_RE = /^---[ \t]*\r?\n(?:[\s\S]*?\r?\n)?---[ \t]*(?:\r?\n|$)/;
501
+ KEY_RE2 = /^([A-Za-z0-9_.-]+):(.*)$/;
502
+ }
503
+ });
504
+
505
+ // ../core/dist/analyze.js
506
+ function utf8Length(source) {
507
+ let bytes = 0;
508
+ for (const ch of source) {
509
+ const cp = ch.codePointAt(0);
510
+ bytes += cp <= 127 ? 1 : cp <= 2047 ? 2 : cp <= 65535 ? 3 : 4;
511
+ }
512
+ return bytes;
513
+ }
514
+ function analyzeDocument(source, options = {}) {
515
+ const frontmatter = extractFrontmatter(source);
516
+ const { document } = parseDocument(stripFrontmatter(source));
517
+ const outline = [];
518
+ const blocks = { total: 0, byName: {}, maxDepth: 0, instances: [] };
519
+ const links = {
520
+ total: 0,
521
+ external: 0,
522
+ internal: 0,
523
+ domains: [],
524
+ items: []
525
+ };
526
+ const images = { total: 0, missingAlt: 0 };
527
+ const code2 = { fences: 0, languages: [], inlineSpans: 0 };
528
+ const structure = { listItems: 0, tables: 0, blockquotes: 0 };
529
+ const languages = /* @__PURE__ */ new Set();
530
+ const domains = /* @__PURE__ */ new Set();
531
+ let words = 0;
532
+ function addWords(count) {
533
+ words += count;
534
+ const section = outline[outline.length - 1];
535
+ if (section)
536
+ section.words += count;
537
+ }
538
+ function recordLink(url2, text, line) {
539
+ const external = EXTERNAL_URL_RE.test(url2);
540
+ links.total++;
541
+ if (external) {
542
+ links.external++;
543
+ const host = url2.match(/^(?:https?:)?\/\/(?:[^/?#@]*@)?([^/?#:]+)/i);
544
+ if (host)
545
+ domains.add(host[1].toLowerCase());
546
+ } else {
547
+ links.internal++;
548
+ }
549
+ links.items.push({ url: url2, text, line, external });
550
+ }
551
+ function inlineToProse(text, line) {
552
+ return text.replace(CODE_SPAN_RE, () => {
553
+ code2.inlineSpans++;
554
+ return " ";
555
+ }).replace(IMAGE_RE, (_, alt) => {
556
+ images.total++;
557
+ if (alt.trim() === "")
558
+ images.missingAlt++;
559
+ return " ";
560
+ }).replace(LINK_RE, (_, label, url2) => {
561
+ recordLink(url2, label, line);
562
+ return label;
563
+ }).replace(AUTOLINK_RE, (_, url2) => {
564
+ recordLink(url2, url2, line);
565
+ return " ";
566
+ });
567
+ }
568
+ function countWords(text) {
569
+ return text.split(/\s+/).filter((token) => /[\p{L}\p{N}]/u.test(token)).length;
570
+ }
571
+ function scanMarkdown(value, startLine) {
572
+ let fence = null;
573
+ let inBlockquote = false;
574
+ let prevLineHasPipe = false;
575
+ const lines = value.split("\n");
576
+ for (let i = 0; i < lines.length; i++) {
577
+ const line = lines[i];
578
+ const lineNo = startLine + i;
579
+ const trimmed = line.trim();
580
+ const fenceMatch = trimmed.match(CODE_FENCE_RE2);
581
+ if (fence !== null) {
582
+ if (fenceMatch && fenceMatch[1][0] === fence[0] && fenceMatch[1].length >= fence.length) {
583
+ fence = null;
584
+ }
585
+ continue;
586
+ }
587
+ if (fenceMatch) {
588
+ fence = fenceMatch[1];
589
+ code2.fences++;
590
+ const lang = fenceMatch[2].trim().split(/\s+/)[0];
591
+ if (lang)
592
+ languages.add(lang);
593
+ inBlockquote = false;
594
+ prevLineHasPipe = false;
595
+ continue;
596
+ }
597
+ if (trimmed === "") {
598
+ inBlockquote = false;
599
+ prevLineHasPipe = false;
600
+ continue;
601
+ }
602
+ const heading = trimmed.match(HEADING_RE);
603
+ if (heading) {
604
+ const raw = heading[2].replace(/\s+#+\s*$/, "");
605
+ const text = inlineToProse(raw, lineNo).replace(/\s+/g, " ").trim();
606
+ outline.push({ level: heading[1].length, text, line: lineNo, words: 0 });
607
+ addWords(countWords(text));
608
+ inBlockquote = false;
609
+ prevLineHasPipe = false;
610
+ continue;
611
+ }
612
+ let content = trimmed;
613
+ if (content.startsWith(">")) {
614
+ if (!inBlockquote) {
615
+ structure.blockquotes++;
616
+ inBlockquote = true;
617
+ }
618
+ content = content.replace(/^(>\s*)+/, "");
619
+ } else {
620
+ inBlockquote = false;
621
+ }
622
+ if (LIST_ITEM_RE.test(content)) {
623
+ structure.listItems++;
624
+ content = content.replace(LIST_ITEM_RE, "");
625
+ }
626
+ if (content.includes("|")) {
627
+ if (TABLE_SEPARATOR_RE.test(content) && content.includes("-") && prevLineHasPipe) {
628
+ structure.tables++;
629
+ }
630
+ prevLineHasPipe = true;
631
+ content = inlineToProse(content, lineNo).replaceAll("|", " ");
632
+ } else {
633
+ prevLineHasPipe = false;
634
+ content = inlineToProse(content, lineNo);
635
+ }
636
+ addWords(countWords(content));
637
+ }
638
+ }
639
+ function walk(nodes, depth) {
640
+ for (const node of nodes) {
641
+ if (node.type === "block") {
642
+ blocks.total++;
643
+ blocks.byName[node.name] = (blocks.byName[node.name] ?? 0) + 1;
644
+ blocks.maxDepth = Math.max(blocks.maxDepth, depth);
645
+ blocks.instances.push({ name: node.name, line: node.openPosition.start.line, depth });
646
+ walk(node.children, depth + 1);
647
+ } else {
648
+ scanMarkdown(node.value, node.position.start.line);
649
+ }
650
+ }
651
+ }
652
+ walk(document.children, 1);
653
+ code2.languages = [...languages];
654
+ links.domains = [...domains].sort();
655
+ const sourceLines = source === "" ? 0 : source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
656
+ return {
657
+ file: {
658
+ path: options.path ?? null,
659
+ bytes: utf8Length(source),
660
+ lines: sourceLines
661
+ },
662
+ frontmatter: frontmatter ? {
663
+ present: true,
664
+ keys: frontmatter.keys,
665
+ data: frontmatter.data,
666
+ lines: frontmatter.lines
667
+ } : { present: false, keys: [], data: {}, lines: null },
668
+ length: {
669
+ words,
670
+ characters: source.length,
671
+ readingMinutes: Math.ceil(words / 200),
672
+ approxTokens: Math.ceil(source.length / 4)
673
+ },
674
+ outline,
675
+ blocks,
676
+ links,
677
+ images,
678
+ code: code2,
679
+ structure
680
+ };
681
+ }
682
+ var CODE_FENCE_RE2, HEADING_RE, LIST_ITEM_RE, TABLE_SEPARATOR_RE, CODE_SPAN_RE, IMAGE_RE, LINK_RE, AUTOLINK_RE, EXTERNAL_URL_RE;
683
+ var init_analyze = __esm({
684
+ "../core/dist/analyze.js"() {
685
+ "use strict";
686
+ init_frontmatter();
687
+ init_parser();
688
+ CODE_FENCE_RE2 = /^(`{3,}|~{3,})(.*)$/;
689
+ HEADING_RE = /^(#{1,6})\s+(.*)$/;
690
+ LIST_ITEM_RE = /^\s*(?:[-*+]|\d+[.)])\s+/;
691
+ TABLE_SEPARATOR_RE = /^\|?[\s:|-]+$/;
692
+ CODE_SPAN_RE = /`[^`]+`/g;
693
+ IMAGE_RE = /!\[([^\]]*)\]\(([^)]*)\)/g;
694
+ LINK_RE = /\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
695
+ AUTOLINK_RE = /<(https?:\/\/[^>\s]+)>/g;
696
+ EXTERNAL_URL_RE = /^(?:https?:)?\/\//i;
697
+ }
698
+ });
699
+
323
700
  // ../core/dist/position.js
324
701
  function bodyLineRange(node, bodyLineIndex) {
325
702
  const bodyLines = node.body.split("\n");
@@ -340,6 +717,45 @@ var init_position = __esm({
340
717
  });
341
718
 
342
719
  // ../core/dist/authoring.js
720
+ function typeLabel(def) {
721
+ switch (def.type) {
722
+ case "enum":
723
+ return `one of ${Object.values(def.entries ?? {}).join("|")}`;
724
+ case "literal":
725
+ return `one of ${(def.values ?? []).map(String).join("|")}`;
726
+ case "union":
727
+ return `one of ${(def.options ?? []).map((o) => typeLabel(o.def).replace(/^one of /, "")).join("|")}`;
728
+ default:
729
+ return def.type;
730
+ }
731
+ }
732
+ function describeProps(schema) {
733
+ const root = schema.def;
734
+ if (root.type !== "object" || !root.shape)
735
+ return [];
736
+ const lines = [];
737
+ for (const [name, field] of Object.entries(root.shape)) {
738
+ let current = field;
739
+ let optional2 = false;
740
+ let defaultValue;
741
+ let description = current.description;
742
+ while (["optional", "default", "nullable"].includes(current.def.type)) {
743
+ optional2 = true;
744
+ if (current.def.type === "default") {
745
+ const dv = current.def.defaultValue;
746
+ defaultValue = typeof dv === "function" ? dv() : dv;
747
+ }
748
+ if (!current.def.innerType)
749
+ break;
750
+ current = current.def.innerType;
751
+ description ??= current.description;
752
+ }
753
+ const presence = defaultValue !== void 0 ? `(optional, default: ${String(defaultValue)})` : optional2 ? "(optional)" : "(required)";
754
+ const suffix = description ? ` \u2014 ${description}` : "";
755
+ lines.push(`- ${name}: ${typeLabel(current.def)} ${presence}${suffix}`);
756
+ }
757
+ return lines;
758
+ }
343
759
  function generateAuthoringGuide(defs, opts = {}) {
344
760
  const includeExamples = opts.includeExamples ?? true;
345
761
  const includeAvoid = opts.includeAvoidRules ?? true;
@@ -349,6 +765,11 @@ function generateAuthoringGuide(defs, opts = {}) {
349
765
  for (const def of defs) {
350
766
  const lines = [`## ${def.name}`, ""];
351
767
  lines.push(def.childOnly ? `${def.description} (child block \u2014 only inside a parent that allows it)` : def.description);
768
+ if (def.props) {
769
+ const props = describeProps(def.props);
770
+ if (props.length > 0)
771
+ lines.push("", "Props:", ...props);
772
+ }
352
773
  lines.push("", `Content: ${def.content.describe()}`);
353
774
  if (def.authoring.useWhen.length > 0) {
354
775
  lines.push("", "Use when:", ...def.authoring.useWhen.map((u) => `- ${u}`));
@@ -736,19 +1157,6 @@ var init_render_markdown = __esm({
736
1157
  }
737
1158
  });
738
1159
 
739
- // ../core/dist/index.js
740
- var init_dist = __esm({
741
- "../core/dist/index.js"() {
742
- "use strict";
743
- init_diagnostics();
744
- init_parser();
745
- init_registry();
746
- init_content_models();
747
- init_validate();
748
- init_render_markdown();
749
- }
750
- });
751
-
752
1160
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
753
1161
  // @__NO_SIDE_EFFECTS__
754
1162
  function $constructor(name, initializer3, params) {
@@ -813,7 +1221,7 @@ var init_core = __esm({
813
1221
  NEVER = /* @__PURE__ */ Object.freeze({
814
1222
  status: "aborted"
815
1223
  });
816
- $brand = Symbol("zod_brand");
1224
+ $brand = /* @__PURE__ */ Symbol("zod_brand");
817
1225
  $ZodAsyncError = class extends Error {
818
1226
  constructor() {
819
1227
  super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`);
@@ -6655,13 +7063,13 @@ var init_he = __esm({
6655
7063
  // no unit
6656
7064
  };
6657
7065
  const typeEntry = (t) => t ? TypeNames[t] : void 0;
6658
- const typeLabel = (t) => {
7066
+ const typeLabel2 = (t) => {
6659
7067
  const e = typeEntry(t);
6660
7068
  if (e)
6661
7069
  return e.label;
6662
7070
  return t ?? TypeNames.unknown.label;
6663
7071
  };
6664
- const withDefinite = (t) => `\u05D4${typeLabel(t)}`;
7072
+ const withDefinite = (t) => `\u05D4${typeLabel2(t)}`;
6665
7073
  const verbFor = (t) => {
6666
7074
  const e = typeEntry(t);
6667
7075
  const gender = e?.gender ?? "m";
@@ -6711,7 +7119,7 @@ var init_he = __esm({
6711
7119
  switch (issue2.code) {
6712
7120
  case "invalid_type": {
6713
7121
  const expectedKey = issue2.expected;
6714
- const expected = TypeDictionary[expectedKey ?? ""] ?? typeLabel(expectedKey);
7122
+ const expected = TypeDictionary[expectedKey ?? ""] ?? typeLabel2(expectedKey);
6715
7123
  const receivedType = parsedType(issue2.input);
6716
7124
  const received = TypeDictionary[receivedType] ?? TypeNames[receivedType]?.label ?? receivedType;
6717
7125
  if (/^[A-Z]/.test(issue2.expected)) {
@@ -10982,8 +11390,8 @@ function registry() {
10982
11390
  var _a2, $output, $input, $ZodRegistry, globalRegistry;
10983
11391
  var init_registries = __esm({
10984
11392
  "../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/registries.js"() {
10985
- $output = Symbol("ZodOutput");
10986
- $input = Symbol("ZodInput");
11393
+ $output = /* @__PURE__ */ Symbol("ZodOutput");
11394
+ $input = /* @__PURE__ */ Symbol("ZodInput");
10987
11395
  $ZodRegistry = class {
10988
11396
  constructor() {
10989
11397
  this._map = /* @__PURE__ */ new WeakMap();
@@ -15830,6 +16238,435 @@ var init_zod = __esm({
15830
16238
  }
15831
16239
  });
15832
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
+
15833
16670
  // ../blocks/dist/blocks/callout.js
15834
16671
  var calloutBlock, calloutMarkdown;
15835
16672
  var init_callout = __esm({
@@ -16182,16 +17019,269 @@ var init_load_registry = __esm({
16182
17019
  }
16183
17020
  });
16184
17021
 
17022
+ // src/commands/agents.ts
17023
+ var agents_exports = {};
17024
+ __export(agents_exports, {
17025
+ agentsCommand: () => agentsCommand,
17026
+ installAgentIntegration: () => installAgentIntegration
17027
+ });
17028
+ import { existsSync } from "node:fs";
17029
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
17030
+ import { join } from "node:path";
17031
+ import { parseArgs } from "node:util";
17032
+ function upsertBlock(existing) {
17033
+ const start = existing.indexOf(START);
17034
+ const end = existing.indexOf(END);
17035
+ if (start !== -1 && end !== -1) {
17036
+ return existing.slice(0, start) + AGENTS_MD_BLOCK + existing.slice(end + END.length);
17037
+ }
17038
+ if (existing.trim() === "") return `${AGENTS_MD_BLOCK}
17039
+ `;
17040
+ return `${existing.replace(/\n*$/, "\n\n")}${AGENTS_MD_BLOCK}
17041
+ `;
17042
+ }
17043
+ async function installAgentIntegration(cwd, options, io) {
17044
+ const claude = options.claude ?? existsSync(join(cwd, ".claude"));
17045
+ const agentsMd = options.agentsMd ?? true;
17046
+ if (agentsMd) {
17047
+ const path = join(cwd, "AGENTS.md");
17048
+ let existing = "";
17049
+ try {
17050
+ existing = await readFile(path, "utf8");
17051
+ } catch {
17052
+ }
17053
+ const created = existing === "";
17054
+ await writeFile(path, upsertBlock(existing), "utf8");
17055
+ io.stdout(`${created ? "created" : "updated"}: AGENTS.md (contentbit block)`);
17056
+ }
17057
+ if (claude) {
17058
+ const skills = [
17059
+ ["contentbit-author", AUTHOR_SKILL],
17060
+ ["contentbit-audit", AUDIT_SKILL]
17061
+ ];
17062
+ for (const [name, content] of skills) {
17063
+ const dir = join(cwd, ".claude/skills", name);
17064
+ await mkdir(dir, { recursive: true });
17065
+ await writeFile(join(dir, "SKILL.md"), content, "utf8");
17066
+ io.stdout(`installed: .claude/skills/${name}/SKILL.md`);
17067
+ }
17068
+ }
17069
+ }
17070
+ async function agentsCommand(args, io) {
17071
+ const { values } = parseArgs({
17072
+ args,
17073
+ options: {
17074
+ claude: { type: "boolean", default: false },
17075
+ "no-agents-md": { type: "boolean", default: false },
17076
+ cwd: { type: "string", default: process.cwd() }
17077
+ }
17078
+ });
17079
+ await installAgentIntegration(
17080
+ values.cwd,
17081
+ {
17082
+ claude: values.claude || void 0,
17083
+ // false means "detect", not "skip"
17084
+ agentsMd: !values["no-agents-md"]
17085
+ },
17086
+ io
17087
+ );
17088
+ return 0;
17089
+ }
17090
+ var TEMPLATE_VERSION, AUTHOR_SKILL, AUDIT_SKILL, AGENTS_MD_BLOCK, START, END;
17091
+ var init_agents = __esm({
17092
+ "src/commands/agents.ts"() {
17093
+ "use strict";
17094
+ TEMPLATE_VERSION = 2;
17095
+ AUTHOR_SKILL = `---
17096
+ name: contentbit-author
17097
+ description: |
17098
+ Write or edit contentbit Markdown content (directive blocks like :::callout).
17099
+ Use when asked to create or modify content documents in a project that uses
17100
+ contentbit \u2014 blog posts, docs pages, changelogs, any Markdown covered by
17101
+ \`contentbit validate\`.
17102
+ version: ${TEMPLATE_VERSION}
17103
+ ---
17104
+
17105
+ # Writing contentbit content
17106
+
17107
+ contentbit documents are plain Markdown plus directive blocks
17108
+ (\`:::name{props} ... :::\`). Every block has a schema. Never guess block names,
17109
+ props, or body shapes \u2014 fetch the live guide from the project's registry first.
17110
+
17111
+ ## Find the project conventions
17112
+
17113
+ Check \`package.json\` for a \`content:check\` script. It holds the canonical
17114
+ validate invocation for this project: the content glob and, if present, the
17115
+ \`--registry <path>\` flag pointing at custom block definitions. Reuse both
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.
17119
+
17120
+ ## The loop
17121
+
17122
+ 1. **Fetch the authoring guide** (always \u2014 it covers this project's custom blocks):
17123
+
17124
+ \`\`\`sh
17125
+ contentbit instructions --audience llm [--registry <path from content:check>]
17126
+ \`\`\`
17127
+
17128
+ Read it before writing. It documents every available block: props, body
17129
+ shape, and when to use or avoid it.
17130
+
17131
+ 2. **Write the document.** Plain Markdown everywhere; blocks only where the
17132
+ guide's use-when guidance fits. Keep frontmatter consistent with sibling
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.
17146
+
17147
+ 3. **Validate and fix until clean:**
17148
+
17149
+ \`\`\`sh
17150
+ contentbit validate <file> [--registry <path>]
17151
+ \`\`\`
17152
+
17153
+ Diagnostics print to stderr as \`file:line:col severity CODE message\`, often
17154
+ with a \`hint:\` line suggesting the fix. Exit 0 means clean; exit 1 means
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.
17168
+
17169
+ ## Failure modes
17170
+
17171
+ - \`contentbit\` not found or no registry resolvable: the project is not set up.
17172
+ Say so and suggest \`npx contentbit@latest init\` \u2014 do not invent block syntax.
17173
+ - A block you want does not exist: use plain Markdown, or ask whether to define
17174
+ a custom block in the registry. Never emit an unregistered block name.
17175
+ `;
17176
+ AUDIT_SKILL = `---
17177
+ name: contentbit-audit
17178
+ description: |
17179
+ Audit contentbit Markdown content health using document stats. Use when asked
17180
+ to audit, review, or find improvements across content \u2014 thin pages, missing
17181
+ structure, validation issues \u2014 in a project that uses contentbit.
17182
+ version: ${TEMPLATE_VERSION}
17183
+ ---
17184
+
17185
+ # Auditing contentbit content
17186
+
17187
+ \`contentbit stats\` analyzes documents and prints JSON to stdout. It is a read
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.
17191
+
17192
+ ## Gather
17193
+
17194
+ Check \`package.json\` for the \`content:check\` script to find this project's
17195
+ content glob and \`--registry\` flag, then:
17196
+
17197
+ \`\`\`sh
17198
+ contentbit stats "content/**/*.md" [--registry <path>]
17199
+ contentbit links "content/**/*.md"
17200
+ \`\`\`
17201
+
17202
+ One matched file prints a single stats object; multiple files print an array.
17203
+ Each entry includes the file path, frontmatter data, a heading \`outline\` with
17204
+ per-section word counts, \`blocks.byName\` usage counts, \`links.domains\`, and
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\`.
17209
+
17210
+ ## Interpret
17211
+
17212
+ Prioritize findings in this order:
17213
+
17214
+ 1. **Validation errors and warnings** \u2014 broken content ships broken pages.
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
17221
+ use blocks; structure (steps, callouts, comparisons, faq) may be missing.
17222
+ 6. **Missing or inconsistent frontmatter** compared to sibling documents.
17223
+ 7. **Structural imbalance** \u2014 skipped heading levels, single-section walls of text.
17224
+
17225
+ ## Report
17226
+
17227
+ Report findings per file with concrete suggestions, ordered by priority. Do not
17228
+ edit files during the audit. To fix a finding, follow the contentbit-author
17229
+ skill (fetch the guide, edit, validate until clean) \u2014 offer that as a follow-up.
17230
+ `;
17231
+ AGENTS_MD_BLOCK = `<!-- contentbit:start -->
17232
+
17233
+ ## contentbit content (generated \u2014 edits inside this block are overwritten)
17234
+
17235
+ This project validates Markdown content with contentbit. Documents are plain
17236
+ Markdown plus directive blocks (\`:::name{props} ... :::\`), each with a schema.
17237
+ The \`content:check\` script in package.json holds the canonical validate
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>\`.
17241
+
17242
+ When writing or editing content:
17243
+
17244
+ 1. Fetch the live authoring guide first \u2014 never guess block syntax:
17245
+ \`contentbit instructions --audience llm [--registry <path>]\`
17246
+ 2. Write plain Markdown; use blocks where the guide's use-when guidance fits.
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>]\`.
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.
17255
+
17256
+ When auditing content health:
17257
+
17258
+ - \`contentbit stats "content/**/*.md" [--registry <path>]\` prints JSON stats
17259
+ and always exits 0: outline word counts, block usage, link domains, and
17260
+ validation error/warning counts. Flag validation issues, thin documents, and
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.
17265
+
17266
+ If \`contentbit\` is unavailable, suggest \`npx contentbit@latest init\` instead
17267
+ of inventing block syntax.
17268
+
17269
+ <!-- contentbit:end -->`;
17270
+ START = "<!-- contentbit:start -->";
17271
+ END = "<!-- contentbit:end -->";
17272
+ }
17273
+ });
17274
+
16185
17275
  // src/commands/init.ts
16186
17276
  var init_exports = {};
16187
17277
  __export(init_exports, {
16188
17278
  initCommand: () => initCommand
16189
17279
  });
16190
17280
  import { spawn } from "node:child_process";
16191
- import { existsSync } from "node:fs";
16192
- import { mkdir, readFile, writeFile } from "node:fs/promises";
16193
- import { join } from "node:path";
16194
- import { parseArgs } from "node:util";
17281
+ import { existsSync as existsSync2 } from "node:fs";
17282
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
17283
+ import { join as join2 } from "node:path";
17284
+ import { parseArgs as parseArgs2 } from "node:util";
16195
17285
  function blockComponentsTemplate(styled) {
16196
17286
  const body = styled ? ` return (
16197
17287
  <figure className="my-6 border-s-2 ps-4">
@@ -16239,7 +17329,7 @@ import { ContentRenderer } from '@/components/content-blocks/content-renderer'`
16239
17329
  return `'use client'
16240
17330
 
16241
17331
  import { genericBlocks } from '@contentbit/blocks'
16242
- import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
17332
+ import { createBlockRegistry, parseDocument, stripFrontmatter, validateDocument } from '@contentbit/core'
16243
17333
  ${reactImport}${mdImport}${rendererImport}
16244
17334
  // Everything block-related lives in the blocks/ folder: definitions in
16245
17335
  // registry.ts (shared with the validate CLI), components in components.tsx.
@@ -16249,7 +17339,7 @@ import { blockComponents } from '${blocksImport}/components'
16249
17339
  const registry = createBlockRegistry().use(genericBlocks()).use(customBlocks)
16250
17340
 
16251
17341
  export function Content({ source }: { source: string }) {
16252
- const result = validateDocument(parseDocument(source), registry)
17342
+ const result = validateDocument(parseDocument(stripFrontmatter(source)), registry)
16253
17343
  return (
16254
17344
  <${renderer}
16255
17345
  document={result.document}
@@ -16269,21 +17359,21 @@ const renderMarkdown = (md) => mdIt.render(md)` : `// TODO: plug a Markdown libr
16269
17359
  const renderMarkdown = undefined`;
16270
17360
  return `// Render content/example.md to example.html. Run: node scripts/render-example.mjs
16271
17361
  import { genericBlocks } from '@contentbit/blocks'
16272
- import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
17362
+ import { createBlockRegistry, parseDocument, stripFrontmatter, validateDocument } from '@contentbit/core'
16273
17363
  import { renderToHtml } from '@contentbit/html'
16274
17364
  import { readFile, writeFile } from 'node:fs/promises'
16275
17365
  ${wiring}
16276
17366
 
16277
17367
  const source = await readFile('content/example.md', 'utf8')
16278
17368
  const registry = createBlockRegistry().use(genericBlocks())
16279
- const result = validateDocument(parseDocument(source), registry)
17369
+ const result = validateDocument(parseDocument(stripFrontmatter(source)), registry)
16280
17370
  const html = renderToHtml(result.document, { renderMarkdown })
16281
17371
  await writeFile('example.html', html, 'utf8')
16282
17372
  console.log('wrote example.html')
16283
17373
  `;
16284
17374
  }
16285
17375
  function detectFramework(cwd, deps) {
16286
- if ((deps["@tanstack/react-start"] || deps["@tanstack/react-router"]) && existsSync(join(cwd, "src/routes"))) {
17376
+ if ((deps["@tanstack/react-start"] || deps["@tanstack/react-router"]) && existsSync2(join2(cwd, "src/routes"))) {
16287
17377
  return {
16288
17378
  framework: "tanstack",
16289
17379
  componentPath: "src/components/content-blocks.tsx",
@@ -16291,8 +17381,8 @@ function detectFramework(cwd, deps) {
16291
17381
  };
16292
17382
  }
16293
17383
  if (deps.next) {
16294
- const appDir = existsSync(join(cwd, "src/app")) ? "src/app" : "app";
16295
- if (existsSync(join(cwd, appDir))) {
17384
+ const appDir = existsSync2(join2(cwd, "src/app")) ? "src/app" : "app";
17385
+ if (existsSync2(join2(cwd, appDir))) {
16296
17386
  return {
16297
17387
  framework: "next",
16298
17388
  componentPath: "components/content-blocks.tsx",
@@ -16302,6 +17392,34 @@ function detectFramework(cwd, deps) {
16302
17392
  }
16303
17393
  return { framework: null, componentPath: "components/content-blocks.tsx", pagePath: null };
16304
17394
  }
17395
+ function astroPage(styled) {
17396
+ const importLine = styled ? "import ContentRenderer from '../components/content-blocks/content-renderer.astro'" : "import { ContentBlocks } from '@contentbit/astro/components'";
17397
+ const renderer = styled ? "ContentRenderer" : "ContentBlocks";
17398
+ return `---
17399
+ import { genericBlocks } from '@contentbit/blocks'
17400
+ import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
17401
+ import { getEntry } from 'astro:content'
17402
+
17403
+ ${importLine}
17404
+
17405
+ // Definitions in blocks/registry.ts are shared with the validate CLI.
17406
+ import customBlocks from '../../blocks/registry'
17407
+ import QuoteBlock from '../../blocks/QuoteBlock.astro'
17408
+
17409
+ // Entry ids are the file path relative to the collection base, minus ".md".
17410
+ const entry = await getEntry('articles', 'example')
17411
+ if (!entry?.body) throw new Error('Entry "example" not found in the articles collection.')
17412
+
17413
+ const registry = createBlockRegistry().use(genericBlocks()).use(customBlocks)
17414
+ // Static pages render at build time, so invalid blocks fail the build here.
17415
+ const result = validateDocument(parseDocument(entry.body), registry)
17416
+ ---
17417
+
17418
+ <main style="max-width: 42rem; margin: 0 auto; padding: 3rem 1.5rem;">
17419
+ <${renderer} document={result.document} components={{ quote: QuoteBlock }} />
17420
+ </main>
17421
+ `;
17422
+ }
16305
17423
  function detectPackageManager(cwd) {
16306
17424
  const locks = [
16307
17425
  ["pnpm-lock.yaml", "pnpm"],
@@ -16311,7 +17429,7 @@ function detectPackageManager(cwd) {
16311
17429
  ["package-lock.json", "npm"]
16312
17430
  ];
16313
17431
  for (const [file2, pm] of locks) {
16314
- if (existsSync(join(cwd, file2))) return pm;
17432
+ if (existsSync2(join2(cwd, file2))) return pm;
16315
17433
  }
16316
17434
  const agent = process.env.npm_config_user_agent ?? "";
16317
17435
  for (const pm of ["pnpm", "yarn", "bun"]) {
@@ -16336,18 +17454,38 @@ function runInstall(pm, args, cwd) {
16336
17454
  child.on("error", () => resolve(1));
16337
17455
  });
16338
17456
  }
17457
+ async function installStyledPack(cwd, pack, noInstall, io) {
17458
+ const componentsJsonPath = join2(cwd, "components.json");
17459
+ const componentsJson = JSON.parse(await readFile2(componentsJsonPath, "utf8"));
17460
+ componentsJson.registries ??= {};
17461
+ if (!componentsJson.registries["@contentbit"]) {
17462
+ componentsJson.registries["@contentbit"] = "https://contentbit.dev/r/{name}.json";
17463
+ await writeFile2(componentsJsonPath, `${JSON.stringify(componentsJson, null, 2)}
17464
+ `, "utf8");
17465
+ io.stdout("added @contentbit registry to components.json");
17466
+ }
17467
+ if (noInstall) {
17468
+ io.stdout(`skipped: shadcn add ${pack}`);
17469
+ return true;
17470
+ }
17471
+ const [bin, prefix] = dlxCommand(detectPackageManager(cwd));
17472
+ io.stdout(`installing the styled pack: shadcn add ${pack}`);
17473
+ const code2 = await runInstall(bin, [...prefix, "shadcn@latest", "add", pack, "--yes"], cwd);
17474
+ if (code2 !== 0) io.stderr("styled pack install failed; falling back to headless defaults");
17475
+ return code2 === 0;
17476
+ }
16339
17477
  async function scaffold(path, content) {
16340
17478
  try {
16341
- await readFile(path, "utf8");
17479
+ await readFile2(path, "utf8");
16342
17480
  return "skipped";
16343
17481
  } catch {
16344
- await mkdir(join(path, ".."), { recursive: true });
16345
- await writeFile(path, content, "utf8");
17482
+ await mkdir2(join2(path, ".."), { recursive: true });
17483
+ await writeFile2(path, content, "utf8");
16346
17484
  return "created";
16347
17485
  }
16348
17486
  }
16349
17487
  async function initCommand(args, io) {
16350
- const { values } = parseArgs({
17488
+ const { values } = parseArgs2({
16351
17489
  args,
16352
17490
  options: {
16353
17491
  target: { type: "string", short: "t" },
@@ -16356,20 +17494,22 @@ async function initCommand(args, io) {
16356
17494
  cwd: { type: "string", default: process.cwd() },
16357
17495
  "no-install": { type: "boolean", default: false },
16358
17496
  "no-page": { type: "boolean", default: false },
16359
- "no-styled": { type: "boolean", default: false }
17497
+ "no-styled": { type: "boolean", default: false },
17498
+ "no-agents": { type: "boolean", default: false }
16360
17499
  }
16361
17500
  });
16362
17501
  const cwd = values.cwd;
16363
17502
  let pkg;
16364
- const pkgPath = join(cwd, "package.json");
17503
+ const pkgPath = join2(cwd, "package.json");
16365
17504
  try {
16366
- pkg = JSON.parse(await readFile(pkgPath, "utf8"));
17505
+ pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
16367
17506
  } catch {
16368
17507
  io.stderr("No package.json found. Run this inside a project (npm init first).");
16369
17508
  return 1;
16370
17509
  }
16371
17510
  const hasReact = Boolean(pkg.dependencies?.react ?? pkg.devDependencies?.react);
16372
- const detected = hasReact ? "react" : "html";
17511
+ const hasAstro = Boolean(pkg.dependencies?.astro ?? pkg.devDependencies?.astro);
17512
+ const detected = hasAstro ? "astro" : hasReact ? "react" : "html";
16373
17513
  let target;
16374
17514
  if (values.target) {
16375
17515
  if (!TARGETS.includes(values.target)) {
@@ -16384,6 +17524,7 @@ async function initCommand(args, io) {
16384
17524
  initialValue: detected,
16385
17525
  options: [
16386
17526
  { value: "react", label: "React", hint: "ContentBlocks component" },
17527
+ { value: "astro", label: "Astro", hint: "content collections + .astro components" },
16387
17528
  { value: "html", label: "Static HTML", hint: "renderToHtml, no framework" },
16388
17529
  { value: "markdown", label: "Plain Markdown", hint: "fallback rendering only" }
16389
17530
  ]
@@ -16420,6 +17561,7 @@ async function initCommand(args, io) {
16420
17561
  const runtime = ["@contentbit/core", "@contentbit/blocks", "zod"];
16421
17562
  if (target === "react") runtime.push("@contentbit/react");
16422
17563
  if (target === "html") runtime.push("@contentbit/html");
17564
+ if (target === "astro") runtime.push("@contentbit/astro");
16423
17565
  if (md !== "none") runtime.push(md);
16424
17566
  if (values["no-install"]) {
16425
17567
  io.stdout(`skipped install: ${runtime.join(" ")} + contentbit (dev)`);
@@ -16437,34 +17579,14 @@ async function initCommand(args, io) {
16437
17579
  }
16438
17580
  const files = [
16439
17581
  ["blocks/registry.ts", REGISTRY_TEMPLATE],
16440
- ["content/example.md", EXAMPLE_CONTENT]
17582
+ ["content/example.md", EXAMPLE_CONTENT],
17583
+ ["content/related.md", RELATED_CONTENT]
16441
17584
  ];
16442
17585
  const layout = detectFramework(cwd, { ...pkg.dependencies, ...pkg.devDependencies });
16443
17586
  let styled = false;
16444
- const componentsJsonPath = join(cwd, "components.json");
16445
- if (target === "react" && !values["no-styled"] && existsSync(componentsJsonPath)) {
16446
- const componentsJson = JSON.parse(await readFile(componentsJsonPath, "utf8"));
16447
- componentsJson.registries ??= {};
16448
- if (!componentsJson.registries["@contentbit"]) {
16449
- componentsJson.registries["@contentbit"] = "https://contentbit.dev/r/{name}.json";
16450
- await writeFile(componentsJsonPath, `${JSON.stringify(componentsJson, null, 2)}
16451
- `, "utf8");
16452
- io.stdout("added @contentbit registry to components.json");
16453
- }
16454
- if (values["no-install"]) {
16455
- io.stdout("skipped: shadcn add @contentbit/generic-pack");
16456
- styled = true;
16457
- } else {
16458
- const [bin, prefix] = dlxCommand(detectPackageManager(cwd));
16459
- io.stdout("installing the styled pack: shadcn add @contentbit/generic-pack");
16460
- const code2 = await runInstall(
16461
- bin,
16462
- [...prefix, "shadcn@latest", "add", "@contentbit/generic-pack", "--yes"],
16463
- cwd
16464
- );
16465
- if (code2 === 0) styled = true;
16466
- else io.stderr("styled pack install failed; falling back to headless defaults");
16467
- }
17587
+ const componentsJsonPath = join2(cwd, "components.json");
17588
+ if (target === "react" && !values["no-styled"] && existsSync2(componentsJsonPath)) {
17589
+ styled = await installStyledPack(cwd, "@contentbit/generic-pack", values["no-install"], io);
16468
17590
  }
16469
17591
  if (target === "react") {
16470
17592
  const depth = layout.componentPath.split("/").length - 1;
@@ -16484,30 +17606,62 @@ async function initCommand(args, io) {
16484
17606
  htmlRenderScript(md)
16485
17607
  ]);
16486
17608
  }
17609
+ if (target === "astro") {
17610
+ let astroStyled = false;
17611
+ if (!values["no-styled"] && existsSync2(componentsJsonPath)) {
17612
+ astroStyled = await installStyledPack(cwd, "@contentbit/astro-pack", values["no-install"], io);
17613
+ }
17614
+ files.push(["blocks/QuoteBlock.astro", ASTRO_QUOTE_BLOCK]);
17615
+ const configCandidates = ["ts", "mts", "mjs", "js"].flatMap((ext) => [
17616
+ `src/content.config.${ext}`,
17617
+ `src/content/config.${ext}`
17618
+ ]);
17619
+ const existingConfig = configCandidates.find((p) => existsSync2(join2(cwd, p)));
17620
+ if (existingConfig) {
17621
+ io.stdout(`content config exists (${existingConfig}); add this collection manually:`);
17622
+ io.stdout(ASTRO_CONTENT_CONFIG);
17623
+ io.stdout('the example page expects the "articles" collection above');
17624
+ } else {
17625
+ files.push(["src/content.config.ts", ASTRO_CONTENT_CONFIG]);
17626
+ }
17627
+ if (!values["no-page"]) files.push(["src/pages/example.astro", astroPage(astroStyled)]);
17628
+ }
16487
17629
  for (const [rel, content] of files) {
16488
- const result = await scaffold(join(cwd, rel), content);
17630
+ const result = await scaffold(join2(cwd, rel), content);
16489
17631
  io.stdout(`${result}: ${rel}`);
16490
17632
  }
16491
- const fresh = JSON.parse(await readFile(pkgPath, "utf8"));
17633
+ const fresh = JSON.parse(await readFile2(pkgPath, "utf8"));
16492
17634
  fresh.scripts ??= {};
16493
17635
  if (!fresh.scripts["content:check"]) {
16494
17636
  fresh.scripts["content:check"] = 'contentbit validate "content/**/*.md" --registry ./blocks/registry.ts';
16495
- await writeFile(pkgPath, `${JSON.stringify(fresh, null, 2)}
16496
- `, "utf8");
16497
17637
  io.stdout("added script: content:check");
16498
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"]) {
17644
+ await writeFile2(pkgPath, `${JSON.stringify(fresh, null, 2)}
17645
+ `, "utf8");
17646
+ }
16499
17647
  let registry2;
16500
17648
  try {
16501
- registry2 = await loadRegistry(join(cwd, "blocks/registry.ts"));
17649
+ registry2 = await loadRegistry(join2(cwd, "blocks/registry.ts"));
16502
17650
  } catch {
16503
17651
  registry2 = await loadRegistry();
16504
17652
  }
16505
17653
  const guide = registry2.toAuthoringGuide({ audience: "llm", includeExamples: true });
16506
- await writeFile(join(cwd, "contentbit-guide.md"), guide, "utf8");
17654
+ await writeFile2(join2(cwd, "contentbit-guide.md"), guide, "utf8");
16507
17655
  io.stdout("created: contentbit-guide.md (LLM authoring instructions)");
17656
+ if (!values["no-agents"]) {
17657
+ await installAgentIntegration(cwd, {}, io);
17658
+ io.stdout("Agent integration installed \u2014 try asking your agent:");
17659
+ io.stdout(' "write a blog post about X" or "audit my content"');
17660
+ }
16508
17661
  io.stdout("");
16509
17662
  io.stdout("Done. Next steps:");
16510
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`);
16511
17665
  if (target === "react") {
16512
17666
  if (!values["no-page"] && layout.pagePath) {
16513
17667
  io.stdout(" 2. Start the dev server and open /example to see the article rendered.");
@@ -16516,6 +17670,9 @@ async function initCommand(args, io) {
16516
17670
  io.stdout(" <Content source={...content/example.md as a string} />");
16517
17671
  }
16518
17672
  io.stdout(" 3. Styled components: pnpm dlx shadcn@latest add @contentbit/generic-pack");
17673
+ } else if (target === "astro") {
17674
+ io.stdout(" 2. Start the dev server and open /example to see the article rendered.");
17675
+ io.stdout(" 3. Styled components: pnpm dlx shadcn@latest add @contentbit/astro-pack");
16519
17676
  } else if (target === "html") {
16520
17677
  io.stdout(" 2. Render it: node scripts/render-example.mjs && open example.html");
16521
17678
  } else {
@@ -16524,16 +17681,19 @@ async function initCommand(args, io) {
16524
17681
  io.stdout(" Docs: https://contentbit.dev/docs");
16525
17682
  return 0;
16526
17683
  }
16527
- var TARGETS, MD_CHOICES, REGISTRY_TEMPLATE, EXAMPLE_CONTENT, TANSTACK_PAGE, NEXT_PAGE;
17684
+ var TARGETS, MD_CHOICES, REGISTRY_TEMPLATE, EXAMPLE_CONTENT, RELATED_CONTENT, TANSTACK_PAGE, NEXT_PAGE, ASTRO_CONTENT_CONFIG, ASTRO_QUOTE_BLOCK;
16528
17685
  var init_init = __esm({
16529
17686
  "src/commands/init.ts"() {
16530
17687
  "use strict";
16531
17688
  init_load_registry();
16532
- TARGETS = ["react", "html", "markdown"];
17689
+ init_agents();
17690
+ TARGETS = ["react", "html", "markdown", "astro"];
16533
17691
  MD_CHOICES = {
16534
17692
  react: ["react-markdown", "none"],
16535
17693
  html: ["marked", "markdown-it", "none"],
16536
- markdown: ["none"]
17694
+ markdown: ["none"],
17695
+ // @contentbit/astro ships its own marked-based default; nothing to install.
17696
+ astro: ["none"]
16537
17697
  };
16538
17698
  REGISTRY_TEMPLATE = `// Custom block definitions for this project. The CLI and your app share
16539
17699
  // this module \u2014 Node 22.18+ imports TypeScript directly:
@@ -16563,7 +17723,18 @@ export const quote = defineBlock({
16563
17723
 
16564
17724
  export default [quote] satisfies BlockDefinition<unknown>[]
16565
17725
  `;
16566
- 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
16567
17738
 
16568
17739
  Regular Markdown works everywhere. Blocks add validated structure:
16569
17740
 
@@ -16584,6 +17755,27 @@ by the \`QuoteBlock\` component, in about twenty lines:
16584
17755
  The Analytical Engine weaves algebraic patterns just as the Jacquard loom
16585
17756
  weaves flowers and leaves.
16586
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
+ :::
16587
17779
  `;
16588
17780
  TANSTACK_PAGE = `import { createFileRoute } from '@tanstack/react-router'
16589
17781
 
@@ -16615,6 +17807,66 @@ export default async function ExamplePage() {
16615
17807
  )
16616
17808
  }
16617
17809
  `;
17810
+ ASTRO_CONTENT_CONFIG = `import { defineCollection } from 'astro:content'
17811
+ import { glob } from 'astro/loaders'
17812
+
17813
+ export const collections = {
17814
+ articles: defineCollection({
17815
+ // Astro's builtin Markdown loader. Entry bodies are parsed and validated
17816
+ // where they render (see src/pages/example.astro); \`contentbit validate\`
17817
+ // covers the same files in CI.
17818
+ loader: glob({ pattern: '**/*.md', base: './content' }),
17819
+ }),
17820
+ }
17821
+ `;
17822
+ ASTRO_QUOTE_BLOCK = `---
17823
+ // The Astro component for the custom \`quote\` block defined in blocks/registry.ts.
17824
+ // Block props arrive as component props; nested content arrives via <slot />.
17825
+ interface Props {
17826
+ author: string
17827
+ role?: string
17828
+ }
17829
+
17830
+ const { author, role } = Astro.props
17831
+ ---
17832
+
17833
+ <figure style="margin: 1.5rem 0; border-left: 2px solid #d4d4d4; padding-left: 1rem;">
17834
+ <blockquote style="font-style: italic;"><slot /></blockquote>
17835
+ <figcaption style="margin-top: 0.5rem; font-size: 0.875rem; opacity: 0.7;">
17836
+ \u2014 {author}{role ? \`, \${role}\` : null}
17837
+ </figcaption>
17838
+ </figure>
17839
+ `;
17840
+ }
17841
+ });
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";
16618
17870
  }
16619
17871
  });
16620
17872
 
@@ -16623,16 +17875,21 @@ var validate_exports = {};
16623
17875
  __export(validate_exports, {
16624
17876
  validateCommand: () => validateCommand
16625
17877
  });
16626
- import { readFile as readFile2 } from "node:fs/promises";
16627
- import { parseArgs as parseArgs2 } from "node:util";
17878
+ import { readFile as readFile3 } from "node:fs/promises";
17879
+ import { parseArgs as parseArgs3 } from "node:util";
16628
17880
  import { glob } from "tinyglobby";
16629
17881
  async function validateCommand(args, io) {
16630
- const { values, positionals } = parseArgs2({
17882
+ const { values, positionals } = parseArgs3({
16631
17883
  args,
16632
17884
  allowPositionals: true,
16633
17885
  options: {
16634
17886
  registry: { type: "string" },
16635
- "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" }
16636
17893
  }
16637
17894
  });
16638
17895
  if (positionals.length === 0) {
@@ -16645,17 +17902,27 @@ async function validateCommand(args, io) {
16645
17902
  return 2;
16646
17903
  }
16647
17904
  const registry2 = await loadRegistry(values.registry);
17905
+ const linkOptions = linkResolverOptions(values);
16648
17906
  let errors = 0;
16649
17907
  let warnings = 0;
17908
+ const linkInputs = [];
16650
17909
  for (const file2 of files.sort()) {
16651
- const source = await readFile2(file2, "utf8");
16652
- const result = validateDocument(parseDocument(source), registry2);
17910
+ const source = await readFile3(file2, "utf8");
17911
+ linkInputs.push({ path: file2, data: extractFrontmatter(source)?.data ?? {} });
17912
+ const result = validateDocument(parseDocument(stripFrontmatter(source)), registry2);
16653
17913
  for (const d of result.diagnostics) {
16654
17914
  io.stderr(formatDiagnostic(d, file2));
16655
17915
  if (d.severity === "error") errors++;
16656
17916
  else if (d.severity === "warning") warnings++;
16657
17917
  }
16658
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
+ }
16659
17926
  io.stdout(`${files.length} file(s): ${errors} errors, ${warnings} warnings`);
16660
17927
  if (errors > 0) return 1;
16661
17928
  if (warnings > 0 && values["strict-warnings"]) return 1;
@@ -16663,6 +17930,64 @@ async function validateCommand(args, io) {
16663
17930
  }
16664
17931
  var init_validate2 = __esm({
16665
17932
  "src/commands/validate.ts"() {
17933
+ "use strict";
17934
+ init_dist();
17935
+ init_link_options();
17936
+ init_load_registry();
17937
+ }
17938
+ });
17939
+
17940
+ // src/commands/stats.ts
17941
+ var stats_exports = {};
17942
+ __export(stats_exports, {
17943
+ statsCommand: () => statsCommand
17944
+ });
17945
+ import { readFile as readFile4 } from "node:fs/promises";
17946
+ import { parseArgs as parseArgs4 } from "node:util";
17947
+ import { glob as glob2 } from "tinyglobby";
17948
+ async function fileStats(file2, registry2) {
17949
+ const source = await readFile4(file2, "utf8");
17950
+ const stats = analyzeDocument(source, { path: file2 });
17951
+ if (registry2) {
17952
+ const result = validateDocument(parseDocument(stripFrontmatter(source)), registry2);
17953
+ let errors = 0;
17954
+ let warnings = 0;
17955
+ for (const d of result.diagnostics) {
17956
+ if (d.severity === "error") errors++;
17957
+ else if (d.severity === "warning") warnings++;
17958
+ }
17959
+ stats.validation = { errors, warnings };
17960
+ }
17961
+ return stats;
17962
+ }
17963
+ async function statsCommand(args, io) {
17964
+ const { values, positionals } = parseArgs4({
17965
+ args,
17966
+ allowPositionals: true,
17967
+ options: {
17968
+ registry: { type: "string" },
17969
+ "no-validate": { type: "boolean", default: false }
17970
+ }
17971
+ });
17972
+ if (positionals.length === 0) {
17973
+ io.stderr("stats: provide at least one file or glob.");
17974
+ return 2;
17975
+ }
17976
+ const files = await glob2(positionals, { absolute: true });
17977
+ if (files.length === 0) {
17978
+ io.stderr(`stats: no files matched ${positionals.join(" ")}`);
17979
+ return 2;
17980
+ }
17981
+ const registry2 = values["no-validate"] ? null : await loadRegistry(values.registry);
17982
+ const all = [];
17983
+ for (const file2 of files.sort()) {
17984
+ all.push(await fileStats(file2, registry2));
17985
+ }
17986
+ io.stdout(JSON.stringify(all.length === 1 ? all[0] : all, null, 2));
17987
+ return 0;
17988
+ }
17989
+ var init_stats = __esm({
17990
+ "src/commands/stats.ts"() {
16666
17991
  "use strict";
16667
17992
  init_dist();
16668
17993
  init_load_registry();
@@ -16749,12 +18074,18 @@ var init_blocks = __esm({
16749
18074
  });
16750
18075
 
16751
18076
  // ../html/dist/render.js
16752
- function defaultMarkdown(md) {
18077
+ function fallbackMarkdown(md) {
16753
18078
  return md.trim().split(/\n{2,}/).map((p) => `<p>${escapeHtml(p)}</p>`).join("\n");
16754
18079
  }
18080
+ function invalidBlockHtml(node, prefix) {
18081
+ return `<div class="${prefix}invalid" data-cb-invalid="${escapeHtml(node.name)}"><pre>${escapeHtml(node.body)}</pre></div>`;
18082
+ }
18083
+ function unrenderableBlockError(name) {
18084
+ return new Error(`Cannot render block "${name}": not validated or no renderer registered.`);
18085
+ }
16755
18086
  function renderToHtml(document, opts = {}) {
16756
18087
  const prefix = opts.classPrefix ?? "cb-";
16757
- const renderMarkdown = opts.renderMarkdown ?? defaultMarkdown;
18088
+ const renderMarkdown = opts.renderMarkdown ?? fallbackMarkdown;
16758
18089
  const renderers = { ...genericHtmlRenderers, ...opts.renderers };
16759
18090
  const onInvalid = opts.onInvalid ?? "fallback";
16760
18091
  const ctx = {
@@ -16768,13 +18099,11 @@ function renderToHtml(document, opts = {}) {
16768
18099
  const renderer = renderers[node.name];
16769
18100
  if (renderer && isValidatedBlock(node))
16770
18101
  return renderer(node, ctx);
16771
- if (onInvalid === "strict") {
16772
- throw new Error(`Cannot render block "${node.name}": not validated or no renderer registered.`);
16773
- }
16774
- if (onInvalid === "annotated") {
16775
- return `<div class="${prefix}invalid" data-cb-invalid="${escapeHtml(node.name)}"><pre>${escapeHtml(node.body)}</pre></div>`;
16776
- }
16777
- return defaultMarkdown(node.body);
18102
+ if (onInvalid === "strict")
18103
+ throw unrenderableBlockError(node.name);
18104
+ if (onInvalid === "annotated")
18105
+ return invalidBlockHtml(node, prefix);
18106
+ return fallbackMarkdown(node.body);
16778
18107
  }).join("\n");
16779
18108
  }
16780
18109
  };
@@ -16802,10 +18131,10 @@ var render_exports = {};
16802
18131
  __export(render_exports, {
16803
18132
  renderCommand: () => renderCommand
16804
18133
  });
16805
- import { readFile as readFile3 } from "node:fs/promises";
16806
- import { parseArgs as parseArgs3 } from "node:util";
18134
+ import { readFile as readFile5 } from "node:fs/promises";
18135
+ import { parseArgs as parseArgs5 } from "node:util";
16807
18136
  async function renderCommand(args, io) {
16808
- const { values, positionals } = parseArgs3({
18137
+ const { values, positionals } = parseArgs5({
16809
18138
  args,
16810
18139
  allowPositionals: true,
16811
18140
  options: {
@@ -16820,8 +18149,8 @@ async function renderCommand(args, io) {
16820
18149
  return 2;
16821
18150
  }
16822
18151
  const registry2 = await loadRegistry(values.registry);
16823
- const source = await readFile3(file2, "utf8");
16824
- const result = validateDocument(parseDocument(source), registry2);
18152
+ const source = await readFile5(file2, "utf8");
18153
+ const result = validateDocument(parseDocument(stripFrontmatter(source)), registry2);
16825
18154
  if (!result.ok) {
16826
18155
  for (const d of result.diagnostics) io.stderr(formatDiagnostic(d, file2));
16827
18156
  return 1;
@@ -16846,9 +18175,9 @@ var instructions_exports = {};
16846
18175
  __export(instructions_exports, {
16847
18176
  instructionsCommand: () => instructionsCommand
16848
18177
  });
16849
- import { parseArgs as parseArgs4 } from "node:util";
18178
+ import { parseArgs as parseArgs6 } from "node:util";
16850
18179
  async function instructionsCommand(args, io) {
16851
- const { values } = parseArgs4({
18180
+ const { values } = parseArgs6({
16852
18181
  args,
16853
18182
  options: {
16854
18183
  audience: { type: "string", default: "llm" },
@@ -16878,9 +18207,9 @@ var docs_exports = {};
16878
18207
  __export(docs_exports, {
16879
18208
  docsCommand: () => docsCommand
16880
18209
  });
16881
- import { parseArgs as parseArgs5 } from "node:util";
18210
+ import { parseArgs as parseArgs7 } from "node:util";
16882
18211
  async function docsCommand(args, io) {
16883
- const { values } = parseArgs5({
18212
+ const { values } = parseArgs7({
16884
18213
  args,
16885
18214
  options: {
16886
18215
  registry: { type: "string" },
@@ -16900,24 +18229,145 @@ var init_docs = __esm({
16900
18229
  }
16901
18230
  });
16902
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
+
16903
18347
  // src/bin.ts
16904
- import { writeFile as writeFile2 } from "node:fs/promises";
18348
+ import { writeFile as writeFile3 } from "node:fs/promises";
16905
18349
 
16906
18350
  // src/run.ts
16907
- var USAGE = `Usage: contentbit <init|validate|render|instructions|docs> [options]
18351
+ var USAGE = `Usage: contentbit <init|validate|stats|render|instructions|docs|agents|links> [options]
16908
18352
 
16909
- init [-t react|html|markdown] [--md ...] [-y] [--no-install] [--no-page]
18353
+ init [-t react|html|markdown|astro] [--md ...] [-y] [--no-install] [--no-page] [--no-agents]
18354
+ agents [--claude] [--no-agents-md]
16910
18355
 
16911
- validate <globs...> [--registry <module.mjs>] [--strict-warnings]
18356
+ validate <globs...> [--registry <module.mjs>] [--strict-warnings] [--link-resolve <mode>]
18357
+ stats <globs...> [--registry <module.mjs>] [--no-validate]
16912
18358
  render <file> --target html|markdown [--registry <module.mjs>] [--out <file>]
16913
18359
  instructions [--audience llm|human] [--no-examples] [--registry <module.mjs>] [--out <file>]
16914
- docs [--registry <module.mjs>] [--out <file>]`;
18360
+ docs [--registry <module.mjs>] [--out <file>]
18361
+ links <globs...> [--fix] [--out <file>] [--link-resolve <mode>]`;
16915
18362
  var commands = {
16916
18363
  init: async () => (await Promise.resolve().then(() => (init_init(), init_exports))).initCommand,
16917
18364
  validate: async () => (await Promise.resolve().then(() => (init_validate2(), validate_exports))).validateCommand,
18365
+ stats: async () => (await Promise.resolve().then(() => (init_stats(), stats_exports))).statsCommand,
16918
18366
  render: async () => (await Promise.resolve().then(() => (init_render2(), render_exports))).renderCommand,
16919
18367
  instructions: async () => (await Promise.resolve().then(() => (init_instructions(), instructions_exports))).instructionsCommand,
16920
- docs: async () => (await Promise.resolve().then(() => (init_docs(), docs_exports))).docsCommand
18368
+ docs: async () => (await Promise.resolve().then(() => (init_docs(), docs_exports))).docsCommand,
18369
+ agents: async () => (await Promise.resolve().then(() => (init_agents(), agents_exports))).agentsCommand,
18370
+ links: async () => (await Promise.resolve().then(() => (init_links2(), links_exports))).linksCommand
16921
18371
  };
16922
18372
  async function run(argv, io) {
16923
18373
  const [name, ...rest] = argv;
@@ -16939,6 +18389,6 @@ async function run(argv, io) {
16939
18389
  var code = await run(process.argv.slice(2), {
16940
18390
  stdout: (s) => console.log(s),
16941
18391
  stderr: (s) => console.error(s),
16942
- writeFile: (path, content) => writeFile2(path, content, "utf8")
18392
+ writeFile: (path, content) => writeFile3(path, content, "utf8")
16943
18393
  });
16944
18394
  process.exit(code);