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