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