contentbit 0.1.1 → 0.2.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,323 @@ 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
+ if (items.every((l) => l.startsWith("- ")))
376
+ return items.map((l) => parseScalar(l.slice(2).trim()));
377
+ return items.join("\n");
378
+ }
379
+ return parseScalar(value);
380
+ }
381
+ function dedent(lines) {
382
+ const indent = Math.min(...lines.map((l) => l.match(/^[ \t]*/)[0].length));
383
+ return lines.map((l) => l.slice(indent));
384
+ }
385
+ function parseScalar(value) {
386
+ if (value === "" || value === "null" || value === "~")
387
+ return null;
388
+ if (value === "true")
389
+ return true;
390
+ if (value === "false")
391
+ return false;
392
+ if (/^[+-]?\d+$/.test(value))
393
+ return Number.parseInt(value, 10);
394
+ if (/^[+-]?\d*\.\d+$/.test(value))
395
+ return Number.parseFloat(value);
396
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
397
+ try {
398
+ return JSON.parse(value);
399
+ } catch {
400
+ return value.slice(1, -1);
401
+ }
402
+ }
403
+ if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
404
+ return value.slice(1, -1).replace(/''/g, "'");
405
+ }
406
+ if (value.startsWith("[") && value.endsWith("]")) {
407
+ const inner = value.slice(1, -1).trim();
408
+ if (inner === "")
409
+ return [];
410
+ return splitInlineItems(inner).map((item) => parseScalar(item.trim()));
411
+ }
412
+ return value;
413
+ }
414
+ function splitInlineItems(inner) {
415
+ const items = [];
416
+ let current = "";
417
+ let quote = null;
418
+ for (const ch of inner) {
419
+ if (quote) {
420
+ current += ch;
421
+ if (ch === quote)
422
+ quote = null;
423
+ } else if (ch === '"' || ch === "'") {
424
+ current += ch;
425
+ quote = ch;
426
+ } else if (ch === ",") {
427
+ items.push(current);
428
+ current = "";
429
+ } else {
430
+ current += ch;
431
+ }
432
+ }
433
+ items.push(current);
434
+ return items;
435
+ }
436
+ var FM_RE, KEY_RE2;
437
+ var init_frontmatter = __esm({
438
+ "../core/dist/frontmatter.js"() {
439
+ "use strict";
440
+ FM_RE = /^---[ \t]*\r?\n(?:[\s\S]*?\r?\n)?---[ \t]*(?:\r?\n|$)/;
441
+ KEY_RE2 = /^([A-Za-z0-9_.-]+):(.*)$/;
442
+ }
443
+ });
444
+
445
+ // ../core/dist/analyze.js
446
+ function utf8Length(source) {
447
+ let bytes = 0;
448
+ for (const ch of source) {
449
+ const cp = ch.codePointAt(0);
450
+ bytes += cp <= 127 ? 1 : cp <= 2047 ? 2 : cp <= 65535 ? 3 : 4;
451
+ }
452
+ return bytes;
453
+ }
454
+ function analyzeDocument(source, options = {}) {
455
+ const frontmatter = extractFrontmatter(source);
456
+ const { document } = parseDocument(stripFrontmatter(source));
457
+ const outline = [];
458
+ const blocks = { total: 0, byName: {}, maxDepth: 0, instances: [] };
459
+ const links = {
460
+ total: 0,
461
+ external: 0,
462
+ internal: 0,
463
+ domains: [],
464
+ items: []
465
+ };
466
+ const images = { total: 0, missingAlt: 0 };
467
+ const code2 = { fences: 0, languages: [], inlineSpans: 0 };
468
+ const structure = { listItems: 0, tables: 0, blockquotes: 0 };
469
+ const languages = /* @__PURE__ */ new Set();
470
+ const domains = /* @__PURE__ */ new Set();
471
+ let words = 0;
472
+ function addWords(count) {
473
+ words += count;
474
+ const section = outline[outline.length - 1];
475
+ if (section)
476
+ section.words += count;
477
+ }
478
+ function recordLink(url2, text, line) {
479
+ const external = EXTERNAL_URL_RE.test(url2);
480
+ links.total++;
481
+ if (external) {
482
+ links.external++;
483
+ const host = url2.match(/^(?:https?:)?\/\/(?:[^/?#@]*@)?([^/?#:]+)/i);
484
+ if (host)
485
+ domains.add(host[1].toLowerCase());
486
+ } else {
487
+ links.internal++;
488
+ }
489
+ links.items.push({ url: url2, text, line, external });
490
+ }
491
+ function inlineToProse(text, line) {
492
+ return text.replace(CODE_SPAN_RE, () => {
493
+ code2.inlineSpans++;
494
+ return " ";
495
+ }).replace(IMAGE_RE, (_, alt) => {
496
+ images.total++;
497
+ if (alt.trim() === "")
498
+ images.missingAlt++;
499
+ return " ";
500
+ }).replace(LINK_RE, (_, label, url2) => {
501
+ recordLink(url2, label, line);
502
+ return label;
503
+ }).replace(AUTOLINK_RE, (_, url2) => {
504
+ recordLink(url2, url2, line);
505
+ return " ";
506
+ });
507
+ }
508
+ function countWords(text) {
509
+ return text.split(/\s+/).filter((token) => /[\p{L}\p{N}]/u.test(token)).length;
510
+ }
511
+ function scanMarkdown(value, startLine) {
512
+ let fence = null;
513
+ let inBlockquote = false;
514
+ let prevLineHasPipe = false;
515
+ const lines = value.split("\n");
516
+ for (let i = 0; i < lines.length; i++) {
517
+ const line = lines[i];
518
+ const lineNo = startLine + i;
519
+ const trimmed = line.trim();
520
+ const fenceMatch = trimmed.match(CODE_FENCE_RE2);
521
+ if (fence !== null) {
522
+ if (fenceMatch && fenceMatch[1][0] === fence[0] && fenceMatch[1].length >= fence.length) {
523
+ fence = null;
524
+ }
525
+ continue;
526
+ }
527
+ if (fenceMatch) {
528
+ fence = fenceMatch[1];
529
+ code2.fences++;
530
+ const lang = fenceMatch[2].trim().split(/\s+/)[0];
531
+ if (lang)
532
+ languages.add(lang);
533
+ inBlockquote = false;
534
+ prevLineHasPipe = false;
535
+ continue;
536
+ }
537
+ if (trimmed === "") {
538
+ inBlockquote = false;
539
+ prevLineHasPipe = false;
540
+ continue;
541
+ }
542
+ const heading = trimmed.match(HEADING_RE);
543
+ if (heading) {
544
+ const raw = heading[2].replace(/\s+#+\s*$/, "");
545
+ const text = inlineToProse(raw, lineNo).replace(/\s+/g, " ").trim();
546
+ outline.push({ level: heading[1].length, text, line: lineNo, words: 0 });
547
+ addWords(countWords(text));
548
+ inBlockquote = false;
549
+ prevLineHasPipe = false;
550
+ continue;
551
+ }
552
+ let content = trimmed;
553
+ if (content.startsWith(">")) {
554
+ if (!inBlockquote) {
555
+ structure.blockquotes++;
556
+ inBlockquote = true;
557
+ }
558
+ content = content.replace(/^(>\s*)+/, "");
559
+ } else {
560
+ inBlockquote = false;
561
+ }
562
+ if (LIST_ITEM_RE.test(content)) {
563
+ structure.listItems++;
564
+ content = content.replace(LIST_ITEM_RE, "");
565
+ }
566
+ if (content.includes("|")) {
567
+ if (TABLE_SEPARATOR_RE.test(content) && content.includes("-") && prevLineHasPipe) {
568
+ structure.tables++;
569
+ }
570
+ prevLineHasPipe = true;
571
+ content = inlineToProse(content, lineNo).replaceAll("|", " ");
572
+ } else {
573
+ prevLineHasPipe = false;
574
+ content = inlineToProse(content, lineNo);
575
+ }
576
+ addWords(countWords(content));
577
+ }
578
+ }
579
+ function walk(nodes, depth) {
580
+ for (const node of nodes) {
581
+ if (node.type === "block") {
582
+ blocks.total++;
583
+ blocks.byName[node.name] = (blocks.byName[node.name] ?? 0) + 1;
584
+ blocks.maxDepth = Math.max(blocks.maxDepth, depth);
585
+ blocks.instances.push({ name: node.name, line: node.openPosition.start.line, depth });
586
+ walk(node.children, depth + 1);
587
+ } else {
588
+ scanMarkdown(node.value, node.position.start.line);
589
+ }
590
+ }
591
+ }
592
+ walk(document.children, 1);
593
+ code2.languages = [...languages];
594
+ links.domains = [...domains].sort();
595
+ const sourceLines = source === "" ? 0 : source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
596
+ return {
597
+ file: {
598
+ path: options.path ?? null,
599
+ bytes: utf8Length(source),
600
+ lines: sourceLines
601
+ },
602
+ frontmatter: frontmatter ? {
603
+ present: true,
604
+ keys: frontmatter.keys,
605
+ data: frontmatter.data,
606
+ lines: frontmatter.lines
607
+ } : { present: false, keys: [], data: {}, lines: null },
608
+ length: {
609
+ words,
610
+ characters: source.length,
611
+ readingMinutes: Math.ceil(words / 200),
612
+ approxTokens: Math.ceil(source.length / 4)
613
+ },
614
+ outline,
615
+ blocks,
616
+ links,
617
+ images,
618
+ code: code2,
619
+ structure
620
+ };
621
+ }
622
+ var CODE_FENCE_RE2, HEADING_RE, LIST_ITEM_RE, TABLE_SEPARATOR_RE, CODE_SPAN_RE, IMAGE_RE, LINK_RE, AUTOLINK_RE, EXTERNAL_URL_RE;
623
+ var init_analyze = __esm({
624
+ "../core/dist/analyze.js"() {
625
+ "use strict";
626
+ init_frontmatter();
627
+ init_parser();
628
+ CODE_FENCE_RE2 = /^(`{3,}|~{3,})(.*)$/;
629
+ HEADING_RE = /^(#{1,6})\s+(.*)$/;
630
+ LIST_ITEM_RE = /^\s*(?:[-*+]|\d+[.)])\s+/;
631
+ TABLE_SEPARATOR_RE = /^\|?[\s:|-]+$/;
632
+ CODE_SPAN_RE = /`[^`]+`/g;
633
+ IMAGE_RE = /!\[([^\]]*)\]\(([^)]*)\)/g;
634
+ LINK_RE = /\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
635
+ AUTOLINK_RE = /<(https?:\/\/[^>\s]+)>/g;
636
+ EXTERNAL_URL_RE = /^(?:https?:)?\/\//i;
637
+ }
638
+ });
639
+
323
640
  // ../core/dist/position.js
324
641
  function bodyLineRange(node, bodyLineIndex) {
325
642
  const bodyLines = node.body.split("\n");
@@ -340,6 +657,45 @@ var init_position = __esm({
340
657
  });
341
658
 
342
659
  // ../core/dist/authoring.js
660
+ function typeLabel(def) {
661
+ switch (def.type) {
662
+ case "enum":
663
+ return `one of ${Object.values(def.entries ?? {}).join("|")}`;
664
+ case "literal":
665
+ return `one of ${(def.values ?? []).map(String).join("|")}`;
666
+ case "union":
667
+ return `one of ${(def.options ?? []).map((o) => typeLabel(o.def).replace(/^one of /, "")).join("|")}`;
668
+ default:
669
+ return def.type;
670
+ }
671
+ }
672
+ function describeProps(schema) {
673
+ const root = schema.def;
674
+ if (root.type !== "object" || !root.shape)
675
+ return [];
676
+ const lines = [];
677
+ for (const [name, field] of Object.entries(root.shape)) {
678
+ let current = field;
679
+ let optional2 = false;
680
+ let defaultValue;
681
+ let description = current.description;
682
+ while (["optional", "default", "nullable"].includes(current.def.type)) {
683
+ optional2 = true;
684
+ if (current.def.type === "default") {
685
+ const dv = current.def.defaultValue;
686
+ defaultValue = typeof dv === "function" ? dv() : dv;
687
+ }
688
+ if (!current.def.innerType)
689
+ break;
690
+ current = current.def.innerType;
691
+ description ??= current.description;
692
+ }
693
+ const presence = defaultValue !== void 0 ? `(optional, default: ${String(defaultValue)})` : optional2 ? "(optional)" : "(required)";
694
+ const suffix = description ? ` \u2014 ${description}` : "";
695
+ lines.push(`- ${name}: ${typeLabel(current.def)} ${presence}${suffix}`);
696
+ }
697
+ return lines;
698
+ }
343
699
  function generateAuthoringGuide(defs, opts = {}) {
344
700
  const includeExamples = opts.includeExamples ?? true;
345
701
  const includeAvoid = opts.includeAvoidRules ?? true;
@@ -349,6 +705,11 @@ function generateAuthoringGuide(defs, opts = {}) {
349
705
  for (const def of defs) {
350
706
  const lines = [`## ${def.name}`, ""];
351
707
  lines.push(def.childOnly ? `${def.description} (child block \u2014 only inside a parent that allows it)` : def.description);
708
+ if (def.props) {
709
+ const props = describeProps(def.props);
710
+ if (props.length > 0)
711
+ lines.push("", "Props:", ...props);
712
+ }
352
713
  lines.push("", `Content: ${def.content.describe()}`);
353
714
  if (def.authoring.useWhen.length > 0) {
354
715
  lines.push("", "Use when:", ...def.authoring.useWhen.map((u) => `- ${u}`));
@@ -742,6 +1103,8 @@ var init_dist = __esm({
742
1103
  "use strict";
743
1104
  init_diagnostics();
744
1105
  init_parser();
1106
+ init_frontmatter();
1107
+ init_analyze();
745
1108
  init_registry();
746
1109
  init_content_models();
747
1110
  init_validate();
@@ -6655,13 +7018,13 @@ var init_he = __esm({
6655
7018
  // no unit
6656
7019
  };
6657
7020
  const typeEntry = (t) => t ? TypeNames[t] : void 0;
6658
- const typeLabel = (t) => {
7021
+ const typeLabel2 = (t) => {
6659
7022
  const e = typeEntry(t);
6660
7023
  if (e)
6661
7024
  return e.label;
6662
7025
  return t ?? TypeNames.unknown.label;
6663
7026
  };
6664
- const withDefinite = (t) => `\u05D4${typeLabel(t)}`;
7027
+ const withDefinite = (t) => `\u05D4${typeLabel2(t)}`;
6665
7028
  const verbFor = (t) => {
6666
7029
  const e = typeEntry(t);
6667
7030
  const gender = e?.gender ?? "m";
@@ -6711,7 +7074,7 @@ var init_he = __esm({
6711
7074
  switch (issue2.code) {
6712
7075
  case "invalid_type": {
6713
7076
  const expectedKey = issue2.expected;
6714
- const expected = TypeDictionary[expectedKey ?? ""] ?? typeLabel(expectedKey);
7077
+ const expected = TypeDictionary[expectedKey ?? ""] ?? typeLabel2(expectedKey);
6715
7078
  const receivedType = parsedType(issue2.input);
6716
7079
  const received = TypeDictionary[receivedType] ?? TypeNames[receivedType]?.label ?? receivedType;
6717
7080
  if (/^[A-Z]/.test(issue2.expected)) {
@@ -16182,16 +16545,223 @@ var init_load_registry = __esm({
16182
16545
  }
16183
16546
  });
16184
16547
 
16548
+ // src/commands/agents.ts
16549
+ var agents_exports = {};
16550
+ __export(agents_exports, {
16551
+ agentsCommand: () => agentsCommand,
16552
+ installAgentIntegration: () => installAgentIntegration
16553
+ });
16554
+ import { existsSync } from "node:fs";
16555
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
16556
+ import { join } from "node:path";
16557
+ import { parseArgs } from "node:util";
16558
+ function upsertBlock(existing) {
16559
+ const start = existing.indexOf(START);
16560
+ const end = existing.indexOf(END);
16561
+ if (start !== -1 && end !== -1) {
16562
+ return existing.slice(0, start) + AGENTS_MD_BLOCK + existing.slice(end + END.length);
16563
+ }
16564
+ if (existing.trim() === "") return `${AGENTS_MD_BLOCK}
16565
+ `;
16566
+ return `${existing.replace(/\n*$/, "\n\n")}${AGENTS_MD_BLOCK}
16567
+ `;
16568
+ }
16569
+ async function installAgentIntegration(cwd, options, io) {
16570
+ const claude = options.claude ?? existsSync(join(cwd, ".claude"));
16571
+ const agentsMd = options.agentsMd ?? true;
16572
+ if (agentsMd) {
16573
+ const path = join(cwd, "AGENTS.md");
16574
+ let existing = "";
16575
+ try {
16576
+ existing = await readFile(path, "utf8");
16577
+ } catch {
16578
+ }
16579
+ const created = existing === "";
16580
+ await writeFile(path, upsertBlock(existing), "utf8");
16581
+ io.stdout(`${created ? "created" : "updated"}: AGENTS.md (contentbit block)`);
16582
+ }
16583
+ if (claude) {
16584
+ const skills = [
16585
+ ["contentbit-author", AUTHOR_SKILL],
16586
+ ["contentbit-audit", AUDIT_SKILL]
16587
+ ];
16588
+ for (const [name, content] of skills) {
16589
+ const dir = join(cwd, ".claude/skills", name);
16590
+ await mkdir(dir, { recursive: true });
16591
+ await writeFile(join(dir, "SKILL.md"), content, "utf8");
16592
+ io.stdout(`installed: .claude/skills/${name}/SKILL.md`);
16593
+ }
16594
+ }
16595
+ }
16596
+ async function agentsCommand(args, io) {
16597
+ const { values } = parseArgs({
16598
+ args,
16599
+ options: {
16600
+ claude: { type: "boolean", default: false },
16601
+ "no-agents-md": { type: "boolean", default: false },
16602
+ cwd: { type: "string", default: process.cwd() }
16603
+ }
16604
+ });
16605
+ await installAgentIntegration(
16606
+ values.cwd,
16607
+ {
16608
+ claude: values.claude || void 0,
16609
+ // false means "detect", not "skip"
16610
+ agentsMd: !values["no-agents-md"]
16611
+ },
16612
+ io
16613
+ );
16614
+ return 0;
16615
+ }
16616
+ var TEMPLATE_VERSION, AUTHOR_SKILL, AUDIT_SKILL, AGENTS_MD_BLOCK, START, END;
16617
+ var init_agents = __esm({
16618
+ "src/commands/agents.ts"() {
16619
+ "use strict";
16620
+ TEMPLATE_VERSION = 1;
16621
+ AUTHOR_SKILL = `---
16622
+ name: contentbit-author
16623
+ description: |
16624
+ Write or edit contentbit Markdown content (directive blocks like :::callout).
16625
+ Use when asked to create or modify content documents in a project that uses
16626
+ contentbit \u2014 blog posts, docs pages, changelogs, any Markdown covered by
16627
+ \`contentbit validate\`.
16628
+ version: ${TEMPLATE_VERSION}
16629
+ ---
16630
+
16631
+ # Writing contentbit content
16632
+
16633
+ contentbit documents are plain Markdown plus directive blocks
16634
+ (\`:::name{props} ... :::\`). Every block has a schema. Never guess block names,
16635
+ props, or body shapes \u2014 fetch the live guide from the project's registry first.
16636
+
16637
+ ## Find the project conventions
16638
+
16639
+ Check \`package.json\` for a \`content:check\` script. It holds the canonical
16640
+ validate invocation for this project: the content glob and, if present, the
16641
+ \`--registry <path>\` flag pointing at custom block definitions. Reuse both
16642
+ below. No script? Default to \`content/**/*.md\` with no \`--registry\` flag.
16643
+
16644
+ ## The loop
16645
+
16646
+ 1. **Fetch the authoring guide** (always \u2014 it covers this project's custom blocks):
16647
+
16648
+ \`\`\`sh
16649
+ contentbit instructions --audience llm [--registry <path from content:check>]
16650
+ \`\`\`
16651
+
16652
+ Read it before writing. It documents every available block: props, body
16653
+ shape, and when to use or avoid it.
16654
+
16655
+ 2. **Write the document.** Plain Markdown everywhere; blocks only where the
16656
+ guide's use-when guidance fits. Keep frontmatter consistent with sibling
16657
+ documents in the same folder.
16658
+
16659
+ 3. **Validate and fix until clean:**
16660
+
16661
+ \`\`\`sh
16662
+ contentbit validate <file> [--registry <path>]
16663
+ \`\`\`
16664
+
16665
+ Diagnostics print to stderr as \`file:line:col severity CODE message\`, often
16666
+ with a \`hint:\` line suggesting the fix. Exit 0 means clean; exit 1 means
16667
+ errors remain. Fix every diagnostic and re-run. Never finish with a failing
16668
+ validate.
16669
+
16670
+ ## Failure modes
16671
+
16672
+ - \`contentbit\` not found or no registry resolvable: the project is not set up.
16673
+ Say so and suggest \`npx contentbit@latest init\` \u2014 do not invent block syntax.
16674
+ - A block you want does not exist: use plain Markdown, or ask whether to define
16675
+ a custom block in the registry. Never emit an unregistered block name.
16676
+ `;
16677
+ AUDIT_SKILL = `---
16678
+ name: contentbit-audit
16679
+ description: |
16680
+ Audit contentbit Markdown content health using document stats. Use when asked
16681
+ to audit, review, or find improvements across content \u2014 thin pages, missing
16682
+ structure, validation issues \u2014 in a project that uses contentbit.
16683
+ version: ${TEMPLATE_VERSION}
16684
+ ---
16685
+
16686
+ # Auditing contentbit content
16687
+
16688
+ \`contentbit stats\` analyzes documents and prints JSON to stdout. It is a read
16689
+ tool: it always exits 0, even when documents have validation errors.
16690
+
16691
+ ## Gather
16692
+
16693
+ Check \`package.json\` for the \`content:check\` script to find this project's
16694
+ content glob and \`--registry\` flag, then:
16695
+
16696
+ \`\`\`sh
16697
+ contentbit stats "content/**/*.md" [--registry <path>]
16698
+ \`\`\`
16699
+
16700
+ One matched file prints a single stats object; multiple files print an array.
16701
+ Each entry includes the file path, frontmatter data, a heading \`outline\` with
16702
+ per-section word counts, \`blocks.byName\` usage counts, \`links.domains\`, and
16703
+ a \`validation\` summary (\`errors\`/\`warnings\`).
16704
+
16705
+ ## Interpret
16706
+
16707
+ Prioritize findings in this order:
16708
+
16709
+ 1. **Validation errors and warnings** \u2014 broken content ships broken pages.
16710
+ 2. **Thin documents** \u2014 outline sections with very low word counts.
16711
+ 3. **Block-less documents** \u2014 \`blocks.byName\` empty where sibling documents
16712
+ use blocks; structure (steps, callouts, comparisons, faq) may be missing.
16713
+ 4. **Missing or inconsistent frontmatter** compared to sibling documents.
16714
+ 5. **Structural imbalance** \u2014 skipped heading levels, single-section walls of text.
16715
+
16716
+ ## Report
16717
+
16718
+ Report findings per file with concrete suggestions, ordered by priority. Do not
16719
+ edit files during the audit. To fix a finding, follow the contentbit-author
16720
+ skill (fetch the guide, edit, validate until clean) \u2014 offer that as a follow-up.
16721
+ `;
16722
+ AGENTS_MD_BLOCK = `<!-- contentbit:start -->
16723
+
16724
+ ## contentbit content (generated \u2014 edits inside this block are overwritten)
16725
+
16726
+ This project validates Markdown content with contentbit. Documents are plain
16727
+ Markdown plus directive blocks (\`:::name{props} ... :::\`), each with a schema.
16728
+ The \`content:check\` script in package.json holds the canonical validate
16729
+ command \u2014 the content glob and the \`--registry\` flag \u2014 reuse its arguments.
16730
+
16731
+ When writing or editing content:
16732
+
16733
+ 1. Fetch the live authoring guide first \u2014 never guess block syntax:
16734
+ \`contentbit instructions --audience llm [--registry <path>]\`
16735
+ 2. Write plain Markdown; use blocks where the guide's use-when guidance fits.
16736
+ 3. Validate until clean (exit 0): \`contentbit validate <file> [--registry <path>]\`.
16737
+ Diagnostics print as \`file:line:col severity CODE message\` with fix hints.
16738
+
16739
+ When auditing content health:
16740
+
16741
+ - \`contentbit stats "content/**/*.md" [--registry <path>]\` prints JSON stats
16742
+ and always exits 0: outline word counts, block usage, link domains, and
16743
+ validation error/warning counts. Flag validation issues, thin documents, and
16744
+ block-less pages first.
16745
+
16746
+ If \`contentbit\` is unavailable, suggest \`npx contentbit@latest init\` instead
16747
+ of inventing block syntax.
16748
+
16749
+ <!-- contentbit:end -->`;
16750
+ START = "<!-- contentbit:start -->";
16751
+ END = "<!-- contentbit:end -->";
16752
+ }
16753
+ });
16754
+
16185
16755
  // src/commands/init.ts
16186
16756
  var init_exports = {};
16187
16757
  __export(init_exports, {
16188
16758
  initCommand: () => initCommand
16189
16759
  });
16190
16760
  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";
16761
+ import { existsSync as existsSync2 } from "node:fs";
16762
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
16763
+ import { join as join2 } from "node:path";
16764
+ import { parseArgs as parseArgs2 } from "node:util";
16195
16765
  function blockComponentsTemplate(styled) {
16196
16766
  const body = styled ? ` return (
16197
16767
  <figure className="my-6 border-s-2 ps-4">
@@ -16283,7 +16853,7 @@ console.log('wrote example.html')
16283
16853
  `;
16284
16854
  }
16285
16855
  function detectFramework(cwd, deps) {
16286
- if ((deps["@tanstack/react-start"] || deps["@tanstack/react-router"]) && existsSync(join(cwd, "src/routes"))) {
16856
+ if ((deps["@tanstack/react-start"] || deps["@tanstack/react-router"]) && existsSync2(join2(cwd, "src/routes"))) {
16287
16857
  return {
16288
16858
  framework: "tanstack",
16289
16859
  componentPath: "src/components/content-blocks.tsx",
@@ -16291,8 +16861,8 @@ function detectFramework(cwd, deps) {
16291
16861
  };
16292
16862
  }
16293
16863
  if (deps.next) {
16294
- const appDir = existsSync(join(cwd, "src/app")) ? "src/app" : "app";
16295
- if (existsSync(join(cwd, appDir))) {
16864
+ const appDir = existsSync2(join2(cwd, "src/app")) ? "src/app" : "app";
16865
+ if (existsSync2(join2(cwd, appDir))) {
16296
16866
  return {
16297
16867
  framework: "next",
16298
16868
  componentPath: "components/content-blocks.tsx",
@@ -16302,6 +16872,34 @@ function detectFramework(cwd, deps) {
16302
16872
  }
16303
16873
  return { framework: null, componentPath: "components/content-blocks.tsx", pagePath: null };
16304
16874
  }
16875
+ function astroPage(styled) {
16876
+ const importLine = styled ? "import ContentRenderer from '../components/content-blocks/content-renderer.astro'" : "import { ContentBlocks } from '@contentbit/astro/components'";
16877
+ const renderer = styled ? "ContentRenderer" : "ContentBlocks";
16878
+ return `---
16879
+ import { genericBlocks } from '@contentbit/blocks'
16880
+ import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
16881
+ import { getEntry } from 'astro:content'
16882
+
16883
+ ${importLine}
16884
+
16885
+ // Definitions in blocks/registry.ts are shared with the validate CLI.
16886
+ import customBlocks from '../../blocks/registry'
16887
+ import QuoteBlock from '../../blocks/QuoteBlock.astro'
16888
+
16889
+ // Entry ids are the file path relative to the collection base, minus ".md".
16890
+ const entry = await getEntry('articles', 'example')
16891
+ if (!entry?.body) throw new Error('Entry "example" not found in the articles collection.')
16892
+
16893
+ const registry = createBlockRegistry().use(genericBlocks()).use(customBlocks)
16894
+ // Static pages render at build time, so invalid blocks fail the build here.
16895
+ const result = validateDocument(parseDocument(entry.body), registry)
16896
+ ---
16897
+
16898
+ <main style="max-width: 42rem; margin: 0 auto; padding: 3rem 1.5rem;">
16899
+ <${renderer} document={result.document} components={{ quote: QuoteBlock }} />
16900
+ </main>
16901
+ `;
16902
+ }
16305
16903
  function detectPackageManager(cwd) {
16306
16904
  const locks = [
16307
16905
  ["pnpm-lock.yaml", "pnpm"],
@@ -16311,7 +16909,7 @@ function detectPackageManager(cwd) {
16311
16909
  ["package-lock.json", "npm"]
16312
16910
  ];
16313
16911
  for (const [file2, pm] of locks) {
16314
- if (existsSync(join(cwd, file2))) return pm;
16912
+ if (existsSync2(join2(cwd, file2))) return pm;
16315
16913
  }
16316
16914
  const agent = process.env.npm_config_user_agent ?? "";
16317
16915
  for (const pm of ["pnpm", "yarn", "bun"]) {
@@ -16336,18 +16934,38 @@ function runInstall(pm, args, cwd) {
16336
16934
  child.on("error", () => resolve(1));
16337
16935
  });
16338
16936
  }
16937
+ async function installStyledPack(cwd, pack, noInstall, io) {
16938
+ const componentsJsonPath = join2(cwd, "components.json");
16939
+ const componentsJson = JSON.parse(await readFile2(componentsJsonPath, "utf8"));
16940
+ componentsJson.registries ??= {};
16941
+ if (!componentsJson.registries["@contentbit"]) {
16942
+ componentsJson.registries["@contentbit"] = "https://contentbit.dev/r/{name}.json";
16943
+ await writeFile2(componentsJsonPath, `${JSON.stringify(componentsJson, null, 2)}
16944
+ `, "utf8");
16945
+ io.stdout("added @contentbit registry to components.json");
16946
+ }
16947
+ if (noInstall) {
16948
+ io.stdout(`skipped: shadcn add ${pack}`);
16949
+ return true;
16950
+ }
16951
+ const [bin, prefix] = dlxCommand(detectPackageManager(cwd));
16952
+ io.stdout(`installing the styled pack: shadcn add ${pack}`);
16953
+ const code2 = await runInstall(bin, [...prefix, "shadcn@latest", "add", pack, "--yes"], cwd);
16954
+ if (code2 !== 0) io.stderr("styled pack install failed; falling back to headless defaults");
16955
+ return code2 === 0;
16956
+ }
16339
16957
  async function scaffold(path, content) {
16340
16958
  try {
16341
- await readFile(path, "utf8");
16959
+ await readFile2(path, "utf8");
16342
16960
  return "skipped";
16343
16961
  } catch {
16344
- await mkdir(join(path, ".."), { recursive: true });
16345
- await writeFile(path, content, "utf8");
16962
+ await mkdir2(join2(path, ".."), { recursive: true });
16963
+ await writeFile2(path, content, "utf8");
16346
16964
  return "created";
16347
16965
  }
16348
16966
  }
16349
16967
  async function initCommand(args, io) {
16350
- const { values } = parseArgs({
16968
+ const { values } = parseArgs2({
16351
16969
  args,
16352
16970
  options: {
16353
16971
  target: { type: "string", short: "t" },
@@ -16356,20 +16974,22 @@ async function initCommand(args, io) {
16356
16974
  cwd: { type: "string", default: process.cwd() },
16357
16975
  "no-install": { type: "boolean", default: false },
16358
16976
  "no-page": { type: "boolean", default: false },
16359
- "no-styled": { type: "boolean", default: false }
16977
+ "no-styled": { type: "boolean", default: false },
16978
+ "no-agents": { type: "boolean", default: false }
16360
16979
  }
16361
16980
  });
16362
16981
  const cwd = values.cwd;
16363
16982
  let pkg;
16364
- const pkgPath = join(cwd, "package.json");
16983
+ const pkgPath = join2(cwd, "package.json");
16365
16984
  try {
16366
- pkg = JSON.parse(await readFile(pkgPath, "utf8"));
16985
+ pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
16367
16986
  } catch {
16368
16987
  io.stderr("No package.json found. Run this inside a project (npm init first).");
16369
16988
  return 1;
16370
16989
  }
16371
16990
  const hasReact = Boolean(pkg.dependencies?.react ?? pkg.devDependencies?.react);
16372
- const detected = hasReact ? "react" : "html";
16991
+ const hasAstro = Boolean(pkg.dependencies?.astro ?? pkg.devDependencies?.astro);
16992
+ const detected = hasAstro ? "astro" : hasReact ? "react" : "html";
16373
16993
  let target;
16374
16994
  if (values.target) {
16375
16995
  if (!TARGETS.includes(values.target)) {
@@ -16384,6 +17004,7 @@ async function initCommand(args, io) {
16384
17004
  initialValue: detected,
16385
17005
  options: [
16386
17006
  { value: "react", label: "React", hint: "ContentBlocks component" },
17007
+ { value: "astro", label: "Astro", hint: "content collections + .astro components" },
16387
17008
  { value: "html", label: "Static HTML", hint: "renderToHtml, no framework" },
16388
17009
  { value: "markdown", label: "Plain Markdown", hint: "fallback rendering only" }
16389
17010
  ]
@@ -16420,6 +17041,7 @@ async function initCommand(args, io) {
16420
17041
  const runtime = ["@contentbit/core", "@contentbit/blocks", "zod"];
16421
17042
  if (target === "react") runtime.push("@contentbit/react");
16422
17043
  if (target === "html") runtime.push("@contentbit/html");
17044
+ if (target === "astro") runtime.push("@contentbit/astro");
16423
17045
  if (md !== "none") runtime.push(md);
16424
17046
  if (values["no-install"]) {
16425
17047
  io.stdout(`skipped install: ${runtime.join(" ")} + contentbit (dev)`);
@@ -16441,30 +17063,9 @@ async function initCommand(args, io) {
16441
17063
  ];
16442
17064
  const layout = detectFramework(cwd, { ...pkg.dependencies, ...pkg.devDependencies });
16443
17065
  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
- }
17066
+ const componentsJsonPath = join2(cwd, "components.json");
17067
+ if (target === "react" && !values["no-styled"] && existsSync2(componentsJsonPath)) {
17068
+ styled = await installStyledPack(cwd, "@contentbit/generic-pack", values["no-install"], io);
16468
17069
  }
16469
17070
  if (target === "react") {
16470
17071
  const depth = layout.componentPath.split("/").length - 1;
@@ -16484,27 +17085,52 @@ async function initCommand(args, io) {
16484
17085
  htmlRenderScript(md)
16485
17086
  ]);
16486
17087
  }
17088
+ if (target === "astro") {
17089
+ let astroStyled = false;
17090
+ if (!values["no-styled"] && existsSync2(componentsJsonPath)) {
17091
+ astroStyled = await installStyledPack(cwd, "@contentbit/astro-pack", values["no-install"], io);
17092
+ }
17093
+ files.push(["blocks/QuoteBlock.astro", ASTRO_QUOTE_BLOCK]);
17094
+ const configCandidates = ["ts", "mts", "mjs", "js"].flatMap((ext) => [
17095
+ `src/content.config.${ext}`,
17096
+ `src/content/config.${ext}`
17097
+ ]);
17098
+ const existingConfig = configCandidates.find((p) => existsSync2(join2(cwd, p)));
17099
+ if (existingConfig) {
17100
+ io.stdout(`content config exists (${existingConfig}); add this collection manually:`);
17101
+ io.stdout(ASTRO_CONTENT_CONFIG);
17102
+ io.stdout('the example page expects the "articles" collection above');
17103
+ } else {
17104
+ files.push(["src/content.config.ts", ASTRO_CONTENT_CONFIG]);
17105
+ }
17106
+ if (!values["no-page"]) files.push(["src/pages/example.astro", astroPage(astroStyled)]);
17107
+ }
16487
17108
  for (const [rel, content] of files) {
16488
- const result = await scaffold(join(cwd, rel), content);
17109
+ const result = await scaffold(join2(cwd, rel), content);
16489
17110
  io.stdout(`${result}: ${rel}`);
16490
17111
  }
16491
- const fresh = JSON.parse(await readFile(pkgPath, "utf8"));
17112
+ const fresh = JSON.parse(await readFile2(pkgPath, "utf8"));
16492
17113
  fresh.scripts ??= {};
16493
17114
  if (!fresh.scripts["content:check"]) {
16494
17115
  fresh.scripts["content:check"] = 'contentbit validate "content/**/*.md" --registry ./blocks/registry.ts';
16495
- await writeFile(pkgPath, `${JSON.stringify(fresh, null, 2)}
17116
+ await writeFile2(pkgPath, `${JSON.stringify(fresh, null, 2)}
16496
17117
  `, "utf8");
16497
17118
  io.stdout("added script: content:check");
16498
17119
  }
16499
17120
  let registry2;
16500
17121
  try {
16501
- registry2 = await loadRegistry(join(cwd, "blocks/registry.ts"));
17122
+ registry2 = await loadRegistry(join2(cwd, "blocks/registry.ts"));
16502
17123
  } catch {
16503
17124
  registry2 = await loadRegistry();
16504
17125
  }
16505
17126
  const guide = registry2.toAuthoringGuide({ audience: "llm", includeExamples: true });
16506
- await writeFile(join(cwd, "contentbit-guide.md"), guide, "utf8");
17127
+ await writeFile2(join2(cwd, "contentbit-guide.md"), guide, "utf8");
16507
17128
  io.stdout("created: contentbit-guide.md (LLM authoring instructions)");
17129
+ if (!values["no-agents"]) {
17130
+ await installAgentIntegration(cwd, {}, io);
17131
+ io.stdout("Agent integration installed \u2014 try asking your agent:");
17132
+ io.stdout(' "write a blog post about X" or "audit my content"');
17133
+ }
16508
17134
  io.stdout("");
16509
17135
  io.stdout("Done. Next steps:");
16510
17136
  io.stdout(` 1. Validate the starter content: ${detectPackageManager(cwd)} run content:check`);
@@ -16516,6 +17142,9 @@ async function initCommand(args, io) {
16516
17142
  io.stdout(" <Content source={...content/example.md as a string} />");
16517
17143
  }
16518
17144
  io.stdout(" 3. Styled components: pnpm dlx shadcn@latest add @contentbit/generic-pack");
17145
+ } else if (target === "astro") {
17146
+ io.stdout(" 2. Start the dev server and open /example to see the article rendered.");
17147
+ io.stdout(" 3. Styled components: pnpm dlx shadcn@latest add @contentbit/astro-pack");
16519
17148
  } else if (target === "html") {
16520
17149
  io.stdout(" 2. Render it: node scripts/render-example.mjs && open example.html");
16521
17150
  } else {
@@ -16524,16 +17153,19 @@ async function initCommand(args, io) {
16524
17153
  io.stdout(" Docs: https://contentbit.dev/docs");
16525
17154
  return 0;
16526
17155
  }
16527
- var TARGETS, MD_CHOICES, REGISTRY_TEMPLATE, EXAMPLE_CONTENT, TANSTACK_PAGE, NEXT_PAGE;
17156
+ var TARGETS, MD_CHOICES, REGISTRY_TEMPLATE, EXAMPLE_CONTENT, TANSTACK_PAGE, NEXT_PAGE, ASTRO_CONTENT_CONFIG, ASTRO_QUOTE_BLOCK;
16528
17157
  var init_init = __esm({
16529
17158
  "src/commands/init.ts"() {
16530
17159
  "use strict";
16531
17160
  init_load_registry();
16532
- TARGETS = ["react", "html", "markdown"];
17161
+ init_agents();
17162
+ TARGETS = ["react", "html", "markdown", "astro"];
16533
17163
  MD_CHOICES = {
16534
17164
  react: ["react-markdown", "none"],
16535
17165
  html: ["marked", "markdown-it", "none"],
16536
- markdown: ["none"]
17166
+ markdown: ["none"],
17167
+ // @contentbit/astro ships its own marked-based default; nothing to install.
17168
+ astro: ["none"]
16537
17169
  };
16538
17170
  REGISTRY_TEMPLATE = `// Custom block definitions for this project. The CLI and your app share
16539
17171
  // this module \u2014 Node 22.18+ imports TypeScript directly:
@@ -16614,6 +17246,36 @@ export default async function ExamplePage() {
16614
17246
  </main>
16615
17247
  )
16616
17248
  }
17249
+ `;
17250
+ ASTRO_CONTENT_CONFIG = `import { defineCollection } from 'astro:content'
17251
+ import { glob } from 'astro/loaders'
17252
+
17253
+ export const collections = {
17254
+ articles: defineCollection({
17255
+ // Astro's builtin Markdown loader. Entry bodies are parsed and validated
17256
+ // where they render (see src/pages/example.astro); \`contentbit validate\`
17257
+ // covers the same files in CI.
17258
+ loader: glob({ pattern: '**/*.md', base: './content' }),
17259
+ }),
17260
+ }
17261
+ `;
17262
+ ASTRO_QUOTE_BLOCK = `---
17263
+ // The Astro component for the custom \`quote\` block defined in blocks/registry.ts.
17264
+ // Block props arrive as component props; nested content arrives via <slot />.
17265
+ interface Props {
17266
+ author: string
17267
+ role?: string
17268
+ }
17269
+
17270
+ const { author, role } = Astro.props
17271
+ ---
17272
+
17273
+ <figure style="margin: 1.5rem 0; border-left: 2px solid #d4d4d4; padding-left: 1rem;">
17274
+ <blockquote style="font-style: italic;"><slot /></blockquote>
17275
+ <figcaption style="margin-top: 0.5rem; font-size: 0.875rem; opacity: 0.7;">
17276
+ \u2014 {author}{role ? \`, \${role}\` : null}
17277
+ </figcaption>
17278
+ </figure>
16617
17279
  `;
16618
17280
  }
16619
17281
  });
@@ -16623,11 +17285,11 @@ var validate_exports = {};
16623
17285
  __export(validate_exports, {
16624
17286
  validateCommand: () => validateCommand
16625
17287
  });
16626
- import { readFile as readFile2 } from "node:fs/promises";
16627
- import { parseArgs as parseArgs2 } from "node:util";
17288
+ import { readFile as readFile3 } from "node:fs/promises";
17289
+ import { parseArgs as parseArgs3 } from "node:util";
16628
17290
  import { glob } from "tinyglobby";
16629
17291
  async function validateCommand(args, io) {
16630
- const { values, positionals } = parseArgs2({
17292
+ const { values, positionals } = parseArgs3({
16631
17293
  args,
16632
17294
  allowPositionals: true,
16633
17295
  options: {
@@ -16648,8 +17310,8 @@ async function validateCommand(args, io) {
16648
17310
  let errors = 0;
16649
17311
  let warnings = 0;
16650
17312
  for (const file2 of files.sort()) {
16651
- const source = await readFile2(file2, "utf8");
16652
- const result = validateDocument(parseDocument(source), registry2);
17313
+ const source = await readFile3(file2, "utf8");
17314
+ const result = validateDocument(parseDocument(stripFrontmatter(source)), registry2);
16653
17315
  for (const d of result.diagnostics) {
16654
17316
  io.stderr(formatDiagnostic(d, file2));
16655
17317
  if (d.severity === "error") errors++;
@@ -16669,6 +17331,63 @@ var init_validate2 = __esm({
16669
17331
  }
16670
17332
  });
16671
17333
 
17334
+ // src/commands/stats.ts
17335
+ var stats_exports = {};
17336
+ __export(stats_exports, {
17337
+ statsCommand: () => statsCommand
17338
+ });
17339
+ import { readFile as readFile4 } from "node:fs/promises";
17340
+ import { parseArgs as parseArgs4 } from "node:util";
17341
+ import { glob as glob2 } from "tinyglobby";
17342
+ async function fileStats(file2, registry2) {
17343
+ const source = await readFile4(file2, "utf8");
17344
+ const stats = analyzeDocument(source, { path: file2 });
17345
+ if (registry2) {
17346
+ const result = validateDocument(parseDocument(stripFrontmatter(source)), registry2);
17347
+ let errors = 0;
17348
+ let warnings = 0;
17349
+ for (const d of result.diagnostics) {
17350
+ if (d.severity === "error") errors++;
17351
+ else if (d.severity === "warning") warnings++;
17352
+ }
17353
+ stats.validation = { errors, warnings };
17354
+ }
17355
+ return stats;
17356
+ }
17357
+ async function statsCommand(args, io) {
17358
+ const { values, positionals } = parseArgs4({
17359
+ args,
17360
+ allowPositionals: true,
17361
+ options: {
17362
+ registry: { type: "string" },
17363
+ "no-validate": { type: "boolean", default: false }
17364
+ }
17365
+ });
17366
+ if (positionals.length === 0) {
17367
+ io.stderr("stats: provide at least one file or glob.");
17368
+ return 2;
17369
+ }
17370
+ const files = await glob2(positionals, { absolute: true });
17371
+ if (files.length === 0) {
17372
+ io.stderr(`stats: no files matched ${positionals.join(" ")}`);
17373
+ return 2;
17374
+ }
17375
+ const registry2 = values["no-validate"] ? null : await loadRegistry(values.registry);
17376
+ const all = [];
17377
+ for (const file2 of files.sort()) {
17378
+ all.push(await fileStats(file2, registry2));
17379
+ }
17380
+ io.stdout(JSON.stringify(all.length === 1 ? all[0] : all, null, 2));
17381
+ return 0;
17382
+ }
17383
+ var init_stats = __esm({
17384
+ "src/commands/stats.ts"() {
17385
+ "use strict";
17386
+ init_dist();
17387
+ init_load_registry();
17388
+ }
17389
+ });
17390
+
16672
17391
  // ../html/dist/escape.js
16673
17392
  function escapeHtml(value) {
16674
17393
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -16749,12 +17468,18 @@ var init_blocks = __esm({
16749
17468
  });
16750
17469
 
16751
17470
  // ../html/dist/render.js
16752
- function defaultMarkdown(md) {
17471
+ function fallbackMarkdown(md) {
16753
17472
  return md.trim().split(/\n{2,}/).map((p) => `<p>${escapeHtml(p)}</p>`).join("\n");
16754
17473
  }
17474
+ function invalidBlockHtml(node, prefix) {
17475
+ return `<div class="${prefix}invalid" data-cb-invalid="${escapeHtml(node.name)}"><pre>${escapeHtml(node.body)}</pre></div>`;
17476
+ }
17477
+ function unrenderableBlockError(name) {
17478
+ return new Error(`Cannot render block "${name}": not validated or no renderer registered.`);
17479
+ }
16755
17480
  function renderToHtml(document, opts = {}) {
16756
17481
  const prefix = opts.classPrefix ?? "cb-";
16757
- const renderMarkdown = opts.renderMarkdown ?? defaultMarkdown;
17482
+ const renderMarkdown = opts.renderMarkdown ?? fallbackMarkdown;
16758
17483
  const renderers = { ...genericHtmlRenderers, ...opts.renderers };
16759
17484
  const onInvalid = opts.onInvalid ?? "fallback";
16760
17485
  const ctx = {
@@ -16768,13 +17493,11 @@ function renderToHtml(document, opts = {}) {
16768
17493
  const renderer = renderers[node.name];
16769
17494
  if (renderer && isValidatedBlock(node))
16770
17495
  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);
17496
+ if (onInvalid === "strict")
17497
+ throw unrenderableBlockError(node.name);
17498
+ if (onInvalid === "annotated")
17499
+ return invalidBlockHtml(node, prefix);
17500
+ return fallbackMarkdown(node.body);
16778
17501
  }).join("\n");
16779
17502
  }
16780
17503
  };
@@ -16802,10 +17525,10 @@ var render_exports = {};
16802
17525
  __export(render_exports, {
16803
17526
  renderCommand: () => renderCommand
16804
17527
  });
16805
- import { readFile as readFile3 } from "node:fs/promises";
16806
- import { parseArgs as parseArgs3 } from "node:util";
17528
+ import { readFile as readFile5 } from "node:fs/promises";
17529
+ import { parseArgs as parseArgs5 } from "node:util";
16807
17530
  async function renderCommand(args, io) {
16808
- const { values, positionals } = parseArgs3({
17531
+ const { values, positionals } = parseArgs5({
16809
17532
  args,
16810
17533
  allowPositionals: true,
16811
17534
  options: {
@@ -16820,7 +17543,7 @@ async function renderCommand(args, io) {
16820
17543
  return 2;
16821
17544
  }
16822
17545
  const registry2 = await loadRegistry(values.registry);
16823
- const source = await readFile3(file2, "utf8");
17546
+ const source = await readFile5(file2, "utf8");
16824
17547
  const result = validateDocument(parseDocument(source), registry2);
16825
17548
  if (!result.ok) {
16826
17549
  for (const d of result.diagnostics) io.stderr(formatDiagnostic(d, file2));
@@ -16846,9 +17569,9 @@ var instructions_exports = {};
16846
17569
  __export(instructions_exports, {
16847
17570
  instructionsCommand: () => instructionsCommand
16848
17571
  });
16849
- import { parseArgs as parseArgs4 } from "node:util";
17572
+ import { parseArgs as parseArgs6 } from "node:util";
16850
17573
  async function instructionsCommand(args, io) {
16851
- const { values } = parseArgs4({
17574
+ const { values } = parseArgs6({
16852
17575
  args,
16853
17576
  options: {
16854
17577
  audience: { type: "string", default: "llm" },
@@ -16878,9 +17601,9 @@ var docs_exports = {};
16878
17601
  __export(docs_exports, {
16879
17602
  docsCommand: () => docsCommand
16880
17603
  });
16881
- import { parseArgs as parseArgs5 } from "node:util";
17604
+ import { parseArgs as parseArgs7 } from "node:util";
16882
17605
  async function docsCommand(args, io) {
16883
- const { values } = parseArgs5({
17606
+ const { values } = parseArgs7({
16884
17607
  args,
16885
17608
  options: {
16886
17609
  registry: { type: "string" },
@@ -16901,23 +17624,27 @@ var init_docs = __esm({
16901
17624
  });
16902
17625
 
16903
17626
  // src/bin.ts
16904
- import { writeFile as writeFile2 } from "node:fs/promises";
17627
+ import { writeFile as writeFile3 } from "node:fs/promises";
16905
17628
 
16906
17629
  // src/run.ts
16907
- var USAGE = `Usage: contentbit <init|validate|render|instructions|docs> [options]
17630
+ var USAGE = `Usage: contentbit <init|validate|stats|render|instructions|docs|agents> [options]
16908
17631
 
16909
- init [-t react|html|markdown] [--md ...] [-y] [--no-install] [--no-page]
17632
+ init [-t react|html|markdown|astro] [--md ...] [-y] [--no-install] [--no-page] [--no-agents]
17633
+ agents [--claude] [--no-agents-md]
16910
17634
 
16911
17635
  validate <globs...> [--registry <module.mjs>] [--strict-warnings]
17636
+ stats <globs...> [--registry <module.mjs>] [--no-validate]
16912
17637
  render <file> --target html|markdown [--registry <module.mjs>] [--out <file>]
16913
17638
  instructions [--audience llm|human] [--no-examples] [--registry <module.mjs>] [--out <file>]
16914
17639
  docs [--registry <module.mjs>] [--out <file>]`;
16915
17640
  var commands = {
16916
17641
  init: async () => (await Promise.resolve().then(() => (init_init(), init_exports))).initCommand,
16917
17642
  validate: async () => (await Promise.resolve().then(() => (init_validate2(), validate_exports))).validateCommand,
17643
+ stats: async () => (await Promise.resolve().then(() => (init_stats(), stats_exports))).statsCommand,
16918
17644
  render: async () => (await Promise.resolve().then(() => (init_render2(), render_exports))).renderCommand,
16919
17645
  instructions: async () => (await Promise.resolve().then(() => (init_instructions(), instructions_exports))).instructionsCommand,
16920
- docs: async () => (await Promise.resolve().then(() => (init_docs(), docs_exports))).docsCommand
17646
+ docs: async () => (await Promise.resolve().then(() => (init_docs(), docs_exports))).docsCommand,
17647
+ agents: async () => (await Promise.resolve().then(() => (init_agents(), agents_exports))).agentsCommand
16921
17648
  };
16922
17649
  async function run(argv, io) {
16923
17650
  const [name, ...rest] = argv;
@@ -16939,6 +17666,6 @@ async function run(argv, io) {
16939
17666
  var code = await run(process.argv.slice(2), {
16940
17667
  stdout: (s) => console.log(s),
16941
17668
  stderr: (s) => console.error(s),
16942
- writeFile: (path, content) => writeFile2(path, content, "utf8")
17669
+ writeFile: (path, content) => writeFile3(path, content, "utf8")
16943
17670
  });
16944
17671
  process.exit(code);