contentbit 0.1.0 → 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)) {
@@ -16153,7 +16516,17 @@ import { pathToFileURL } from "node:url";
16153
16516
  async function loadRegistry(registryPath) {
16154
16517
  const registry2 = createBlockRegistry().use(genericBlocks());
16155
16518
  if (registryPath) {
16156
- const mod = await import(pathToFileURL(registryPath).href);
16519
+ let mod;
16520
+ try {
16521
+ mod = await import(pathToFileURL(registryPath).href);
16522
+ } catch (err) {
16523
+ if (err instanceof Error && "code" in err && err.code === "ERR_UNKNOWN_FILE_EXTENSION") {
16524
+ throw new Error(
16525
+ `Importing a TypeScript registry needs Node 22.18+ (native type stripping): ${registryPath}`
16526
+ );
16527
+ }
16528
+ throw err;
16529
+ }
16157
16530
  if (!Array.isArray(mod.default)) {
16158
16531
  throw new Error(
16159
16532
  `--registry module must default-export an array of block definitions: ${registryPath}`
@@ -16171,16 +16544,372 @@ var init_load_registry = __esm({
16171
16544
  }
16172
16545
  });
16173
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
+
16174
16754
  // src/commands/init.ts
16175
16755
  var init_exports = {};
16176
16756
  __export(init_exports, {
16177
16757
  initCommand: () => initCommand
16178
16758
  });
16179
16759
  import { spawn } from "node:child_process";
16180
- import { mkdir, readFile, writeFile } from "node:fs/promises";
16181
- import { join } from "node:path";
16182
- import { parseArgs } from "node:util";
16183
- function detectPackageManager() {
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";
16764
+ function blockComponentsTemplate(styled) {
16765
+ const body = styled ? ` return (
16766
+ <figure className="my-6 border-s-2 ps-4">
16767
+ <blockquote className="text-lg italic">{ctx.renderMarkdown(data.markdown)}</blockquote>
16768
+ <figcaption className="text-muted-foreground mt-2 text-sm">
16769
+ \u2014 {String(node.props.author)}
16770
+ {node.props.role ? \`, \${String(node.props.role)}\` : null}
16771
+ </figcaption>
16772
+ </figure>
16773
+ )` : ` return (
16774
+ <figure style={{ margin: '1.5rem 0', borderLeft: '2px solid #d4d4d4', paddingLeft: '1rem' }}>
16775
+ <blockquote style={{ fontStyle: 'italic' }}>{ctx.renderMarkdown(data.markdown)}</blockquote>
16776
+ <figcaption style={{ marginTop: '0.5rem', fontSize: '0.875rem', opacity: 0.7 }}>
16777
+ \u2014 {String(node.props.author)}
16778
+ {node.props.role ? \`, \${String(node.props.role)}\` : null}
16779
+ </figcaption>
16780
+ </figure>
16781
+ )`;
16782
+ return `import type { BlockComponent, BlockComponentProps } from '@contentbit/react'
16783
+
16784
+ // One React component per custom block, keyed by block name. Definitions
16785
+ // live in ./registry.ts \u2014 add a block there, add its component here, and
16786
+ // the rest of the app never changes.
16787
+ function QuoteBlock({ node, ctx }: BlockComponentProps) {
16788
+ const data = node.data as { markdown: string }
16789
+ ${body}
16790
+ }
16791
+
16792
+ export const blockComponents: Record<string, BlockComponent> = {
16793
+ quote: QuoteBlock,
16794
+ }
16795
+ `;
16796
+ }
16797
+ function reactComponent(styled, mdWired, blocksImport) {
16798
+ const mdImport = mdWired ? "import ReactMarkdown from 'react-markdown'\n" : "";
16799
+ const mdProp = mdWired ? "\n renderMarkdown={(md) => <ReactMarkdown>{md}</ReactMarkdown>}" : `
16800
+ // TODO: plug your Markdown library in here, e.g. react-markdown.
16801
+ // One function renders all prose: https://contentbit.dev/docs/guides/markdown
16802
+ // renderMarkdown={(md) => <Markdown source={md} />}`;
16803
+ const rendererImport = styled ? `
16804
+ // The styled pack installed by shadcn. Yours to edit.
16805
+ import { ContentRenderer } from '@/components/content-blocks/content-renderer'` : "";
16806
+ const renderer = styled ? "ContentRenderer" : "ContentBlocks";
16807
+ const reactImport = styled ? "" : "import { ContentBlocks } from '@contentbit/react'\n";
16808
+ return `'use client'
16809
+
16810
+ import { genericBlocks } from '@contentbit/blocks'
16811
+ import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
16812
+ ${reactImport}${mdImport}${rendererImport}
16813
+ // Everything block-related lives in the blocks/ folder: definitions in
16814
+ // registry.ts (shared with the validate CLI), components in components.tsx.
16815
+ import customBlocks from '${blocksImport}/registry'
16816
+ import { blockComponents } from '${blocksImport}/components'
16817
+
16818
+ const registry = createBlockRegistry().use(genericBlocks()).use(customBlocks)
16819
+
16820
+ export function Content({ source }: { source: string }) {
16821
+ const result = validateDocument(parseDocument(source), registry)
16822
+ return (
16823
+ <${renderer}
16824
+ document={result.document}
16825
+ components={blockComponents}${mdProp}
16826
+ />
16827
+ )
16828
+ }
16829
+ `;
16830
+ }
16831
+ function htmlRenderScript(md) {
16832
+ const wiring = md === "marked" ? `import { marked } from 'marked'
16833
+
16834
+ const renderMarkdown = (md) => marked.parse(md, { async: false })` : md === "markdown-it" ? `import MarkdownIt from 'markdown-it'
16835
+
16836
+ const mdIt = new MarkdownIt() // html: false by default \u2014 raw HTML stays escaped
16837
+ const renderMarkdown = (md) => mdIt.render(md)` : `// TODO: plug a Markdown library in here (marked, markdown-it, remark).
16838
+ const renderMarkdown = undefined`;
16839
+ return `// Render content/example.md to example.html. Run: node scripts/render-example.mjs
16840
+ import { genericBlocks } from '@contentbit/blocks'
16841
+ import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
16842
+ import { renderToHtml } from '@contentbit/html'
16843
+ import { readFile, writeFile } from 'node:fs/promises'
16844
+ ${wiring}
16845
+
16846
+ const source = await readFile('content/example.md', 'utf8')
16847
+ const registry = createBlockRegistry().use(genericBlocks())
16848
+ const result = validateDocument(parseDocument(source), registry)
16849
+ const html = renderToHtml(result.document, { renderMarkdown })
16850
+ await writeFile('example.html', html, 'utf8')
16851
+ console.log('wrote example.html')
16852
+ `;
16853
+ }
16854
+ function detectFramework(cwd, deps) {
16855
+ if ((deps["@tanstack/react-start"] || deps["@tanstack/react-router"]) && existsSync2(join2(cwd, "src/routes"))) {
16856
+ return {
16857
+ framework: "tanstack",
16858
+ componentPath: "src/components/content-blocks.tsx",
16859
+ pagePath: "src/routes/example.tsx"
16860
+ };
16861
+ }
16862
+ if (deps.next) {
16863
+ const appDir = existsSync2(join2(cwd, "src/app")) ? "src/app" : "app";
16864
+ if (existsSync2(join2(cwd, appDir))) {
16865
+ return {
16866
+ framework: "next",
16867
+ componentPath: "components/content-blocks.tsx",
16868
+ pagePath: `${appDir}/example/page.tsx`
16869
+ };
16870
+ }
16871
+ }
16872
+ return { framework: null, componentPath: "components/content-blocks.tsx", pagePath: null };
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
+ }
16902
+ function detectPackageManager(cwd) {
16903
+ const locks = [
16904
+ ["pnpm-lock.yaml", "pnpm"],
16905
+ ["yarn.lock", "yarn"],
16906
+ ["bun.lock", "bun"],
16907
+ ["bun.lockb", "bun"],
16908
+ ["package-lock.json", "npm"]
16909
+ ];
16910
+ for (const [file2, pm] of locks) {
16911
+ if (existsSync2(join2(cwd, file2))) return pm;
16912
+ }
16184
16913
  const agent = process.env.npm_config_user_agent ?? "";
16185
16914
  for (const pm of ["pnpm", "yarn", "bun"]) {
16186
16915
  if (agent.startsWith(pm)) return pm;
@@ -16191,6 +16920,12 @@ function installArgs(pm, dev, pkgs) {
16191
16920
  const add = pm === "npm" ? "install" : "add";
16192
16921
  return dev ? [add, "-D", ...pkgs] : [add, ...pkgs];
16193
16922
  }
16923
+ function dlxCommand(pm) {
16924
+ if (pm === "pnpm") return ["pnpm", ["dlx"]];
16925
+ if (pm === "yarn") return ["yarn", ["dlx"]];
16926
+ if (pm === "bun") return ["bunx", []];
16927
+ return ["npx", ["--yes"]];
16928
+ }
16194
16929
  function runInstall(pm, args, cwd) {
16195
16930
  return new Promise((resolve) => {
16196
16931
  const child = spawn(pm, args, { cwd, stdio: "inherit", shell: process.platform === "win32" });
@@ -16198,37 +16933,62 @@ function runInstall(pm, args, cwd) {
16198
16933
  child.on("error", () => resolve(1));
16199
16934
  });
16200
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
+ }
16201
16956
  async function scaffold(path, content) {
16202
16957
  try {
16203
- await readFile(path, "utf8");
16958
+ await readFile2(path, "utf8");
16204
16959
  return "skipped";
16205
16960
  } catch {
16206
- await mkdir(join(path, ".."), { recursive: true });
16207
- await writeFile(path, content, "utf8");
16961
+ await mkdir2(join2(path, ".."), { recursive: true });
16962
+ await writeFile2(path, content, "utf8");
16208
16963
  return "created";
16209
16964
  }
16210
16965
  }
16211
16966
  async function initCommand(args, io) {
16212
- const { values } = parseArgs({
16967
+ const { values } = parseArgs2({
16213
16968
  args,
16214
16969
  options: {
16215
16970
  target: { type: "string", short: "t" },
16971
+ md: { type: "string" },
16216
16972
  yes: { type: "boolean", short: "y", default: false },
16217
16973
  cwd: { type: "string", default: process.cwd() },
16218
- "no-install": { type: "boolean", default: false }
16974
+ "no-install": { type: "boolean", default: false },
16975
+ "no-page": { type: "boolean", default: false },
16976
+ "no-styled": { type: "boolean", default: false },
16977
+ "no-agents": { type: "boolean", default: false }
16219
16978
  }
16220
16979
  });
16221
16980
  const cwd = values.cwd;
16222
16981
  let pkg;
16223
- const pkgPath = join(cwd, "package.json");
16982
+ const pkgPath = join2(cwd, "package.json");
16224
16983
  try {
16225
- pkg = JSON.parse(await readFile(pkgPath, "utf8"));
16984
+ pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
16226
16985
  } catch {
16227
16986
  io.stderr("No package.json found. Run this inside a project (npm init first).");
16228
16987
  return 1;
16229
16988
  }
16230
16989
  const hasReact = Boolean(pkg.dependencies?.react ?? pkg.devDependencies?.react);
16231
- const detected = hasReact ? "react" : "html";
16990
+ const hasAstro = Boolean(pkg.dependencies?.astro ?? pkg.devDependencies?.astro);
16991
+ const detected = hasAstro ? "astro" : hasReact ? "react" : "html";
16232
16992
  let target;
16233
16993
  if (values.target) {
16234
16994
  if (!TARGETS.includes(values.target)) {
@@ -16243,6 +17003,7 @@ async function initCommand(args, io) {
16243
17003
  initialValue: detected,
16244
17004
  options: [
16245
17005
  { value: "react", label: "React", hint: "ContentBlocks component" },
17006
+ { value: "astro", label: "Astro", hint: "content collections + .astro components" },
16246
17007
  { value: "html", label: "Static HTML", hint: "renderToHtml, no framework" },
16247
17008
  { value: "markdown", label: "Plain Markdown", hint: "fallback rendering only" }
16248
17009
  ]
@@ -16252,13 +17013,39 @@ async function initCommand(args, io) {
16252
17013
  } else {
16253
17014
  target = detected;
16254
17015
  }
16255
- const runtime = ["@contentbit/core", "@contentbit/blocks"];
17016
+ const choices = MD_CHOICES[target];
17017
+ let md;
17018
+ if (values.md) {
17019
+ if (!choices.includes(values.md)) {
17020
+ io.stderr(`Unknown markdown library "${values.md}". Use one of: ${choices.join(", ")}`);
17021
+ return 2;
17022
+ }
17023
+ md = values.md;
17024
+ } else if (choices.length > 1 && !values.yes && process.stdin.isTTY && process.stdout.isTTY) {
17025
+ const { isCancel, select } = await import("@clack/prompts");
17026
+ const answer = await select({
17027
+ message: "Markdown library for prose rendering?",
17028
+ initialValue: choices[0],
17029
+ options: choices.map((c) => ({
17030
+ value: c,
17031
+ label: c,
17032
+ hint: c === "none" ? "wire one yourself later" : "installed and wired for you"
17033
+ }))
17034
+ });
17035
+ if (isCancel(answer)) return 1;
17036
+ md = answer;
17037
+ } else {
17038
+ md = choices[0];
17039
+ }
17040
+ const runtime = ["@contentbit/core", "@contentbit/blocks", "zod"];
16256
17041
  if (target === "react") runtime.push("@contentbit/react");
16257
17042
  if (target === "html") runtime.push("@contentbit/html");
17043
+ if (target === "astro") runtime.push("@contentbit/astro");
17044
+ if (md !== "none") runtime.push(md);
16258
17045
  if (values["no-install"]) {
16259
17046
  io.stdout(`skipped install: ${runtime.join(" ")} + contentbit (dev)`);
16260
17047
  } else {
16261
- const pm = detectPackageManager();
17048
+ const pm = detectPackageManager(cwd);
16262
17049
  io.stdout(`installing with ${pm}: ${runtime.join(" ")}`);
16263
17050
  if (await runInstall(pm, installArgs(pm, false, runtime), cwd) !== 0) {
16264
17051
  io.stderr("install failed");
@@ -16270,68 +17057,142 @@ async function initCommand(args, io) {
16270
17057
  }
16271
17058
  }
16272
17059
  const files = [
16273
- ["blocks/registry.mjs", REGISTRY_TEMPLATE],
17060
+ ["blocks/registry.ts", REGISTRY_TEMPLATE],
16274
17061
  ["content/example.md", EXAMPLE_CONTENT]
16275
17062
  ];
16276
- if (target === "react") files.push(["components/content-blocks.tsx", REACT_COMPONENT]);
17063
+ const layout = detectFramework(cwd, { ...pkg.dependencies, ...pkg.devDependencies });
17064
+ let styled = false;
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);
17068
+ }
17069
+ if (target === "react") {
17070
+ const depth = layout.componentPath.split("/").length - 1;
17071
+ const blocksImport = `${"../".repeat(depth)}blocks`;
17072
+ files.push(["blocks/components.tsx", blockComponentsTemplate(styled)]);
17073
+ files.push([
17074
+ layout.componentPath,
17075
+ reactComponent(styled, md === "react-markdown", blocksImport)
17076
+ ]);
17077
+ if (!values["no-page"] && layout.pagePath) {
17078
+ files.push([layout.pagePath, layout.framework === "tanstack" ? TANSTACK_PAGE : NEXT_PAGE]);
17079
+ }
17080
+ }
17081
+ if (target === "html") {
17082
+ files.push([
17083
+ "scripts/render-example.mjs",
17084
+ htmlRenderScript(md)
17085
+ ]);
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
+ }
16277
17107
  for (const [rel, content] of files) {
16278
- const result = await scaffold(join(cwd, rel), content);
17108
+ const result = await scaffold(join2(cwd, rel), content);
16279
17109
  io.stdout(`${result}: ${rel}`);
16280
17110
  }
16281
- const fresh = JSON.parse(await readFile(pkgPath, "utf8"));
17111
+ const fresh = JSON.parse(await readFile2(pkgPath, "utf8"));
16282
17112
  fresh.scripts ??= {};
16283
17113
  if (!fresh.scripts["content:check"]) {
16284
- fresh.scripts["content:check"] = 'contentbit validate "content/**/*.md" --registry ./blocks/registry.mjs';
16285
- await writeFile(pkgPath, `${JSON.stringify(fresh, null, 2)}
17114
+ fresh.scripts["content:check"] = 'contentbit validate "content/**/*.md" --registry ./blocks/registry.ts';
17115
+ await writeFile2(pkgPath, `${JSON.stringify(fresh, null, 2)}
16286
17116
  `, "utf8");
16287
17117
  io.stdout("added script: content:check");
16288
17118
  }
16289
- const registry2 = await loadRegistry();
17119
+ let registry2;
17120
+ try {
17121
+ registry2 = await loadRegistry(join2(cwd, "blocks/registry.ts"));
17122
+ } catch {
17123
+ registry2 = await loadRegistry();
17124
+ }
16290
17125
  const guide = registry2.toAuthoringGuide({ audience: "llm", includeExamples: true });
16291
- await writeFile(join(cwd, "contentbit-guide.md"), guide, "utf8");
17126
+ await writeFile2(join2(cwd, "contentbit-guide.md"), guide, "utf8");
16292
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
+ }
16293
17133
  io.stdout("");
16294
17134
  io.stdout("Done. Next steps:");
16295
- io.stdout(` 1. Validate the starter content: ${detectPackageManager()} run content:check`);
17135
+ io.stdout(` 1. Validate the starter content: ${detectPackageManager(cwd)} run content:check`);
16296
17136
  if (target === "react") {
16297
- io.stdout(' 2. Render it: import { Content } from "./components/content-blocks"');
17137
+ if (!values["no-page"] && layout.pagePath) {
17138
+ io.stdout(" 2. Start the dev server and open /example to see the article rendered.");
17139
+ } else {
17140
+ io.stdout(' 2. Render it: import { Content } from "./components/content-blocks"');
17141
+ io.stdout(" <Content source={...content/example.md as a string} />");
17142
+ }
16298
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");
16299
17147
  } else if (target === "html") {
16300
- io.stdout(" 2. Render it: contentbit render content/example.md --target html");
17148
+ io.stdout(" 2. Render it: node scripts/render-example.mjs && open example.html");
16301
17149
  } else {
16302
17150
  io.stdout(" 2. Render it: contentbit render content/example.md --target markdown");
16303
17151
  }
16304
17152
  io.stdout(" Docs: https://contentbit.dev/docs");
16305
17153
  return 0;
16306
17154
  }
16307
- var TARGETS, REGISTRY_TEMPLATE, EXAMPLE_CONTENT, REACT_COMPONENT;
17155
+ var TARGETS, MD_CHOICES, REGISTRY_TEMPLATE, EXAMPLE_CONTENT, TANSTACK_PAGE, NEXT_PAGE, ASTRO_CONTENT_CONFIG, ASTRO_QUOTE_BLOCK;
16308
17156
  var init_init = __esm({
16309
17157
  "src/commands/init.ts"() {
16310
17158
  "use strict";
16311
17159
  init_load_registry();
16312
- TARGETS = ["react", "html", "markdown"];
16313
- REGISTRY_TEMPLATE = `// Custom blocks for this project. The CLI and your app share this module:
17160
+ init_agents();
17161
+ TARGETS = ["react", "html", "markdown", "astro"];
17162
+ MD_CHOICES = {
17163
+ react: ["react-markdown", "none"],
17164
+ html: ["marked", "markdown-it", "none"],
17165
+ markdown: ["none"],
17166
+ // @contentbit/astro ships its own marked-based default; nothing to install.
17167
+ astro: ["none"]
17168
+ };
17169
+ REGISTRY_TEMPLATE = `// Custom block definitions for this project. The CLI and your app share
17170
+ // this module \u2014 Node 22.18+ imports TypeScript directly:
16314
17171
  //
16315
- // contentbit validate "content/**/*.md" --registry ./blocks/registry.mjs
17172
+ // contentbit validate "content/**/*.md" --registry ./blocks/registry.ts
16316
17173
  //
16317
- // Define blocks with @contentbit/core and default-export them as an array.
17174
+ // Definitions stay framework-free (the CLI and every render target use
17175
+ // them); React components live next door in blocks/components.tsx.
16318
17176
  // Docs: https://contentbit.dev/docs/guides/custom-blocks
16319
- //
16320
- // import { defineBlock, pipeRows } from '@contentbit/core'
16321
- // import { z } from 'zod'
16322
- //
16323
- // const pricingTable = defineBlock({
16324
- // name: 'pricing-table',
16325
- // description: 'Compares product plans.',
16326
- // props: z.object({ currency: z.enum(['usd', 'eur']).default('usd') }),
16327
- // content: pipeRows({ columns: ['plan', 'price'], minRows: 2 }),
16328
- // authoring: {
16329
- // useWhen: ['Comparing pricing plans'],
16330
- // example: ':::pricing-table\\n- Starter | $0\\n- Pro | $12/mo\\n:::',
16331
- // },
16332
- // })
17177
+ import { defineBlock, markdownBody, type BlockDefinition } from '@contentbit/core'
17178
+ import { z } from 'zod'
16333
17179
 
16334
- export default []
17180
+ export const quote = defineBlock({
17181
+ name: 'quote',
17182
+ description: 'A pull quote with an author.',
17183
+ props: z.object({
17184
+ author: z.string().min(1),
17185
+ role: z.string().optional(),
17186
+ }),
17187
+ content: markdownBody({ minLength: 3 }),
17188
+ authoring: {
17189
+ useWhen: ['Quoting a person to support a point'],
17190
+ avoidWhen: ['Highlighting your own remark, use callout instead'],
17191
+ example: ':::quote{author="Ada Lovelace"}\\nThe Analytical Engine weaves algebraic patterns.\\n:::',
17192
+ },
17193
+ })
17194
+
17195
+ export default [quote] satisfies BlockDefinition<unknown>[]
16335
17196
  `;
16336
17197
  EXAMPLE_CONTENT = `# Hello, Content Blocks
16337
17198
 
@@ -16346,24 +17207,74 @@ Run the validate script and you will get file:line:col diagnostics.
16346
17207
  2. Run \`contentbit validate "content/**/*.md"\`.
16347
17208
  3. Render it with the target you picked at init.
16348
17209
  :::
17210
+
17211
+ This one is a **custom block**, defined in \`blocks/registry.ts\` and rendered
17212
+ by the \`QuoteBlock\` component, in about twenty lines:
17213
+
17214
+ :::quote{author="Ada Lovelace" role="Notes on the Analytical Engine, 1843"}
17215
+ The Analytical Engine weaves algebraic patterns just as the Jacquard loom
17216
+ weaves flowers and leaves.
17217
+ :::
16349
17218
  `;
16350
- REACT_COMPONENT = `import { genericBlocks } from '@contentbit/blocks'
16351
- import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
16352
- import { ContentBlocks } from '@contentbit/react'
17219
+ TANSTACK_PAGE = `import { createFileRoute } from '@tanstack/react-router'
16353
17220
 
16354
- const registry = createBlockRegistry().use(genericBlocks())
17221
+ import { Content } from '../components/content-blocks'
17222
+ // Vite's ?raw import inlines the Markdown as a string at build time.
17223
+ import source from '../../content/example.md?raw'
16355
17224
 
16356
- export function Content({ source }: { source: string }) {
16357
- const result = validateDocument(parseDocument(source), registry)
17225
+ export const Route = createFileRoute('/example')({ component: ExamplePage })
17226
+
17227
+ function ExamplePage() {
16358
17228
  return (
16359
- <ContentBlocks
16360
- document={result.document}
16361
- // TODO: plug your Markdown library in here, e.g. react-markdown.
16362
- // One function renders all prose: https://contentbit.dev/docs/guides/markdown
16363
- // renderMarkdown={(md) => <Markdown source={md} />}
16364
- />
17229
+ <main style={{ maxWidth: '42rem', margin: '0 auto', padding: '3rem 1.5rem' }}>
17230
+ <Content source={source} />
17231
+ </main>
16365
17232
  )
16366
17233
  }
17234
+ `;
17235
+ NEXT_PAGE = `import { readFile } from 'node:fs/promises'
17236
+
17237
+ // If your project has no "@/" path alias, switch to a relative import.
17238
+ import { Content } from '@/components/content-blocks'
17239
+
17240
+ export default async function ExamplePage() {
17241
+ const source = await readFile('content/example.md', 'utf8')
17242
+ return (
17243
+ <main style={{ maxWidth: '42rem', margin: '0 auto', padding: '3rem 1.5rem' }}>
17244
+ <Content source={source} />
17245
+ </main>
17246
+ )
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>
16367
17278
  `;
16368
17279
  }
16369
17280
  });
@@ -16373,11 +17284,11 @@ var validate_exports = {};
16373
17284
  __export(validate_exports, {
16374
17285
  validateCommand: () => validateCommand
16375
17286
  });
16376
- import { readFile as readFile2 } from "node:fs/promises";
16377
- import { parseArgs as parseArgs2 } from "node:util";
17287
+ import { readFile as readFile3 } from "node:fs/promises";
17288
+ import { parseArgs as parseArgs3 } from "node:util";
16378
17289
  import { glob } from "tinyglobby";
16379
17290
  async function validateCommand(args, io) {
16380
- const { values, positionals } = parseArgs2({
17291
+ const { values, positionals } = parseArgs3({
16381
17292
  args,
16382
17293
  allowPositionals: true,
16383
17294
  options: {
@@ -16398,8 +17309,8 @@ async function validateCommand(args, io) {
16398
17309
  let errors = 0;
16399
17310
  let warnings = 0;
16400
17311
  for (const file2 of files.sort()) {
16401
- const source = await readFile2(file2, "utf8");
16402
- const result = validateDocument(parseDocument(source), registry2);
17312
+ const source = await readFile3(file2, "utf8");
17313
+ const result = validateDocument(parseDocument(stripFrontmatter(source)), registry2);
16403
17314
  for (const d of result.diagnostics) {
16404
17315
  io.stderr(formatDiagnostic(d, file2));
16405
17316
  if (d.severity === "error") errors++;
@@ -16419,6 +17330,63 @@ var init_validate2 = __esm({
16419
17330
  }
16420
17331
  });
16421
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
+
16422
17390
  // ../html/dist/escape.js
16423
17391
  function escapeHtml(value) {
16424
17392
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -16499,12 +17467,18 @@ var init_blocks = __esm({
16499
17467
  });
16500
17468
 
16501
17469
  // ../html/dist/render.js
16502
- function defaultMarkdown(md) {
17470
+ function fallbackMarkdown(md) {
16503
17471
  return md.trim().split(/\n{2,}/).map((p) => `<p>${escapeHtml(p)}</p>`).join("\n");
16504
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
+ }
16505
17479
  function renderToHtml(document, opts = {}) {
16506
17480
  const prefix = opts.classPrefix ?? "cb-";
16507
- const renderMarkdown = opts.renderMarkdown ?? defaultMarkdown;
17481
+ const renderMarkdown = opts.renderMarkdown ?? fallbackMarkdown;
16508
17482
  const renderers = { ...genericHtmlRenderers, ...opts.renderers };
16509
17483
  const onInvalid = opts.onInvalid ?? "fallback";
16510
17484
  const ctx = {
@@ -16518,13 +17492,11 @@ function renderToHtml(document, opts = {}) {
16518
17492
  const renderer = renderers[node.name];
16519
17493
  if (renderer && isValidatedBlock(node))
16520
17494
  return renderer(node, ctx);
16521
- if (onInvalid === "strict") {
16522
- throw new Error(`Cannot render block "${node.name}": not validated or no renderer registered.`);
16523
- }
16524
- if (onInvalid === "annotated") {
16525
- return `<div class="${prefix}invalid" data-cb-invalid="${escapeHtml(node.name)}"><pre>${escapeHtml(node.body)}</pre></div>`;
16526
- }
16527
- 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);
16528
17500
  }).join("\n");
16529
17501
  }
16530
17502
  };
@@ -16552,10 +17524,10 @@ var render_exports = {};
16552
17524
  __export(render_exports, {
16553
17525
  renderCommand: () => renderCommand
16554
17526
  });
16555
- import { readFile as readFile3 } from "node:fs/promises";
16556
- import { parseArgs as parseArgs3 } from "node:util";
17527
+ import { readFile as readFile5 } from "node:fs/promises";
17528
+ import { parseArgs as parseArgs5 } from "node:util";
16557
17529
  async function renderCommand(args, io) {
16558
- const { values, positionals } = parseArgs3({
17530
+ const { values, positionals } = parseArgs5({
16559
17531
  args,
16560
17532
  allowPositionals: true,
16561
17533
  options: {
@@ -16570,7 +17542,7 @@ async function renderCommand(args, io) {
16570
17542
  return 2;
16571
17543
  }
16572
17544
  const registry2 = await loadRegistry(values.registry);
16573
- const source = await readFile3(file2, "utf8");
17545
+ const source = await readFile5(file2, "utf8");
16574
17546
  const result = validateDocument(parseDocument(source), registry2);
16575
17547
  if (!result.ok) {
16576
17548
  for (const d of result.diagnostics) io.stderr(formatDiagnostic(d, file2));
@@ -16596,9 +17568,9 @@ var instructions_exports = {};
16596
17568
  __export(instructions_exports, {
16597
17569
  instructionsCommand: () => instructionsCommand
16598
17570
  });
16599
- import { parseArgs as parseArgs4 } from "node:util";
17571
+ import { parseArgs as parseArgs6 } from "node:util";
16600
17572
  async function instructionsCommand(args, io) {
16601
- const { values } = parseArgs4({
17573
+ const { values } = parseArgs6({
16602
17574
  args,
16603
17575
  options: {
16604
17576
  audience: { type: "string", default: "llm" },
@@ -16628,9 +17600,9 @@ var docs_exports = {};
16628
17600
  __export(docs_exports, {
16629
17601
  docsCommand: () => docsCommand
16630
17602
  });
16631
- import { parseArgs as parseArgs5 } from "node:util";
17603
+ import { parseArgs as parseArgs7 } from "node:util";
16632
17604
  async function docsCommand(args, io) {
16633
- const { values } = parseArgs5({
17605
+ const { values } = parseArgs7({
16634
17606
  args,
16635
17607
  options: {
16636
17608
  registry: { type: "string" },
@@ -16651,20 +17623,24 @@ var init_docs = __esm({
16651
17623
  });
16652
17624
 
16653
17625
  // src/run.ts
16654
- var USAGE = `Usage: contentbit <init|validate|render|instructions|docs> [options]
17626
+ var USAGE = `Usage: contentbit <init|validate|stats|render|instructions|docs|agents> [options]
16655
17627
 
16656
- init [-t react|html|markdown] [-y] [--no-install]
17628
+ init [-t react|html|markdown|astro] [--md ...] [-y] [--no-install] [--no-page] [--no-agents]
17629
+ agents [--claude] [--no-agents-md]
16657
17630
 
16658
17631
  validate <globs...> [--registry <module.mjs>] [--strict-warnings]
17632
+ stats <globs...> [--registry <module.mjs>] [--no-validate]
16659
17633
  render <file> --target html|markdown [--registry <module.mjs>] [--out <file>]
16660
17634
  instructions [--audience llm|human] [--no-examples] [--registry <module.mjs>] [--out <file>]
16661
17635
  docs [--registry <module.mjs>] [--out <file>]`;
16662
17636
  var commands = {
16663
17637
  init: async () => (await Promise.resolve().then(() => (init_init(), init_exports))).initCommand,
16664
17638
  validate: async () => (await Promise.resolve().then(() => (init_validate2(), validate_exports))).validateCommand,
17639
+ stats: async () => (await Promise.resolve().then(() => (init_stats(), stats_exports))).statsCommand,
16665
17640
  render: async () => (await Promise.resolve().then(() => (init_render2(), render_exports))).renderCommand,
16666
17641
  instructions: async () => (await Promise.resolve().then(() => (init_instructions(), instructions_exports))).instructionsCommand,
16667
- 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
16668
17644
  };
16669
17645
  async function run(argv, io) {
16670
17646
  const [name, ...rest] = argv;