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/bin.js CHANGED
@@ -320,6 +320,323 @@ var init_parser = __esm({
320
320
  }
321
321
  });
322
322
 
323
+ // ../core/dist/frontmatter.js
324
+ function stripFrontmatter(source) {
325
+ const match = FM_RE.exec(source);
326
+ if (!match)
327
+ return source;
328
+ return match[0].replace(/[^\n]+/g, "") + source.slice(match[0].length);
329
+ }
330
+ function extractFrontmatter(source) {
331
+ const match = source.match(FM_RE);
332
+ if (!match)
333
+ return null;
334
+ const blockLines = match[0].replace(/\r?\n$/, "").split(/\r?\n/);
335
+ const inner = blockLines.slice(1, -1);
336
+ const { data, keys } = parseYamlSubset(inner);
337
+ return { raw: inner.join("\n"), data, keys, lines: { start: 1, end: blockLines.length } };
338
+ }
339
+ function parseYamlSubset(lines) {
340
+ const data = {};
341
+ const keys = [];
342
+ let i = 0;
343
+ while (i < lines.length) {
344
+ const line = lines[i];
345
+ const trimmed = line.trim();
346
+ if (trimmed === "" || trimmed.startsWith("#") || /^[ \t]/.test(line)) {
347
+ i++;
348
+ continue;
349
+ }
350
+ const m = line.match(KEY_RE2);
351
+ if (!m) {
352
+ i++;
353
+ continue;
354
+ }
355
+ const [, key, rawValue] = m;
356
+ const value = rawValue.trim();
357
+ i++;
358
+ const indented = [];
359
+ while (i < lines.length && /^[ \t]/.test(lines[i]) && lines[i].trim() !== "") {
360
+ indented.push(lines[i]);
361
+ i++;
362
+ }
363
+ keys.push(key);
364
+ data[key] = parseValue(value, indented);
365
+ }
366
+ return { data, keys };
367
+ }
368
+ function parseValue(value, indented) {
369
+ if (/^[|>][+-]?$/.test(value))
370
+ return dedent(indented).join("\n");
371
+ if (value === "") {
372
+ if (indented.length === 0)
373
+ return null;
374
+ const items = dedent(indented);
375
+ if (items.every((l) => l.startsWith("- ")))
376
+ return items.map((l) => parseScalar(l.slice(2).trim()));
377
+ return items.join("\n");
378
+ }
379
+ return parseScalar(value);
380
+ }
381
+ function dedent(lines) {
382
+ const indent = Math.min(...lines.map((l) => l.match(/^[ \t]*/)[0].length));
383
+ return lines.map((l) => l.slice(indent));
384
+ }
385
+ function parseScalar(value) {
386
+ if (value === "" || value === "null" || value === "~")
387
+ return null;
388
+ if (value === "true")
389
+ return true;
390
+ if (value === "false")
391
+ return false;
392
+ if (/^[+-]?\d+$/.test(value))
393
+ return Number.parseInt(value, 10);
394
+ if (/^[+-]?\d*\.\d+$/.test(value))
395
+ return Number.parseFloat(value);
396
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
397
+ try {
398
+ return JSON.parse(value);
399
+ } catch {
400
+ return value.slice(1, -1);
401
+ }
402
+ }
403
+ if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
404
+ return value.slice(1, -1).replace(/''/g, "'");
405
+ }
406
+ if (value.startsWith("[") && value.endsWith("]")) {
407
+ const inner = value.slice(1, -1).trim();
408
+ if (inner === "")
409
+ return [];
410
+ return splitInlineItems(inner).map((item) => parseScalar(item.trim()));
411
+ }
412
+ return value;
413
+ }
414
+ function splitInlineItems(inner) {
415
+ const items = [];
416
+ let current = "";
417
+ let quote = null;
418
+ for (const ch of inner) {
419
+ if (quote) {
420
+ current += ch;
421
+ if (ch === quote)
422
+ quote = null;
423
+ } else if (ch === '"' || ch === "'") {
424
+ current += ch;
425
+ quote = ch;
426
+ } else if (ch === ",") {
427
+ items.push(current);
428
+ current = "";
429
+ } else {
430
+ current += ch;
431
+ }
432
+ }
433
+ items.push(current);
434
+ return items;
435
+ }
436
+ var FM_RE, KEY_RE2;
437
+ var init_frontmatter = __esm({
438
+ "../core/dist/frontmatter.js"() {
439
+ "use strict";
440
+ FM_RE = /^---[ \t]*\r?\n(?:[\s\S]*?\r?\n)?---[ \t]*(?:\r?\n|$)/;
441
+ KEY_RE2 = /^([A-Za-z0-9_.-]+):(.*)$/;
442
+ }
443
+ });
444
+
445
+ // ../core/dist/analyze.js
446
+ function utf8Length(source) {
447
+ let bytes = 0;
448
+ for (const ch of source) {
449
+ const cp = ch.codePointAt(0);
450
+ bytes += cp <= 127 ? 1 : cp <= 2047 ? 2 : cp <= 65535 ? 3 : 4;
451
+ }
452
+ return bytes;
453
+ }
454
+ function analyzeDocument(source, options = {}) {
455
+ const frontmatter = extractFrontmatter(source);
456
+ const { document } = parseDocument(stripFrontmatter(source));
457
+ const outline = [];
458
+ const blocks = { total: 0, byName: {}, maxDepth: 0, instances: [] };
459
+ const links = {
460
+ total: 0,
461
+ external: 0,
462
+ internal: 0,
463
+ domains: [],
464
+ items: []
465
+ };
466
+ const images = { total: 0, missingAlt: 0 };
467
+ const code2 = { fences: 0, languages: [], inlineSpans: 0 };
468
+ const structure = { listItems: 0, tables: 0, blockquotes: 0 };
469
+ const languages = /* @__PURE__ */ new Set();
470
+ const domains = /* @__PURE__ */ new Set();
471
+ let words = 0;
472
+ function addWords(count) {
473
+ words += count;
474
+ const section = outline[outline.length - 1];
475
+ if (section)
476
+ section.words += count;
477
+ }
478
+ function recordLink(url2, text, line) {
479
+ const external = EXTERNAL_URL_RE.test(url2);
480
+ links.total++;
481
+ if (external) {
482
+ links.external++;
483
+ const host = url2.match(/^(?:https?:)?\/\/(?:[^/?#@]*@)?([^/?#:]+)/i);
484
+ if (host)
485
+ domains.add(host[1].toLowerCase());
486
+ } else {
487
+ links.internal++;
488
+ }
489
+ links.items.push({ url: url2, text, line, external });
490
+ }
491
+ function inlineToProse(text, line) {
492
+ return text.replace(CODE_SPAN_RE, () => {
493
+ code2.inlineSpans++;
494
+ return " ";
495
+ }).replace(IMAGE_RE, (_, alt) => {
496
+ images.total++;
497
+ if (alt.trim() === "")
498
+ images.missingAlt++;
499
+ return " ";
500
+ }).replace(LINK_RE, (_, label, url2) => {
501
+ recordLink(url2, label, line);
502
+ return label;
503
+ }).replace(AUTOLINK_RE, (_, url2) => {
504
+ recordLink(url2, url2, line);
505
+ return " ";
506
+ });
507
+ }
508
+ function countWords(text) {
509
+ return text.split(/\s+/).filter((token) => /[\p{L}\p{N}]/u.test(token)).length;
510
+ }
511
+ function scanMarkdown(value, startLine) {
512
+ let fence = null;
513
+ let inBlockquote = false;
514
+ let prevLineHasPipe = false;
515
+ const lines = value.split("\n");
516
+ for (let i = 0; i < lines.length; i++) {
517
+ const line = lines[i];
518
+ const lineNo = startLine + i;
519
+ const trimmed = line.trim();
520
+ const fenceMatch = trimmed.match(CODE_FENCE_RE2);
521
+ if (fence !== null) {
522
+ if (fenceMatch && fenceMatch[1][0] === fence[0] && fenceMatch[1].length >= fence.length) {
523
+ fence = null;
524
+ }
525
+ continue;
526
+ }
527
+ if (fenceMatch) {
528
+ fence = fenceMatch[1];
529
+ code2.fences++;
530
+ const lang = fenceMatch[2].trim().split(/\s+/)[0];
531
+ if (lang)
532
+ languages.add(lang);
533
+ inBlockquote = false;
534
+ prevLineHasPipe = false;
535
+ continue;
536
+ }
537
+ if (trimmed === "") {
538
+ inBlockquote = false;
539
+ prevLineHasPipe = false;
540
+ continue;
541
+ }
542
+ const heading = trimmed.match(HEADING_RE);
543
+ if (heading) {
544
+ const raw = heading[2].replace(/\s+#+\s*$/, "");
545
+ const text = inlineToProse(raw, lineNo).replace(/\s+/g, " ").trim();
546
+ outline.push({ level: heading[1].length, text, line: lineNo, words: 0 });
547
+ addWords(countWords(text));
548
+ inBlockquote = false;
549
+ prevLineHasPipe = false;
550
+ continue;
551
+ }
552
+ let content = trimmed;
553
+ if (content.startsWith(">")) {
554
+ if (!inBlockquote) {
555
+ structure.blockquotes++;
556
+ inBlockquote = true;
557
+ }
558
+ content = content.replace(/^(>\s*)+/, "");
559
+ } else {
560
+ inBlockquote = false;
561
+ }
562
+ if (LIST_ITEM_RE.test(content)) {
563
+ structure.listItems++;
564
+ content = content.replace(LIST_ITEM_RE, "");
565
+ }
566
+ if (content.includes("|")) {
567
+ if (TABLE_SEPARATOR_RE.test(content) && content.includes("-") && prevLineHasPipe) {
568
+ structure.tables++;
569
+ }
570
+ prevLineHasPipe = true;
571
+ content = inlineToProse(content, lineNo).replaceAll("|", " ");
572
+ } else {
573
+ prevLineHasPipe = false;
574
+ content = inlineToProse(content, lineNo);
575
+ }
576
+ addWords(countWords(content));
577
+ }
578
+ }
579
+ function walk(nodes, depth) {
580
+ for (const node of nodes) {
581
+ if (node.type === "block") {
582
+ blocks.total++;
583
+ blocks.byName[node.name] = (blocks.byName[node.name] ?? 0) + 1;
584
+ blocks.maxDepth = Math.max(blocks.maxDepth, depth);
585
+ blocks.instances.push({ name: node.name, line: node.openPosition.start.line, depth });
586
+ walk(node.children, depth + 1);
587
+ } else {
588
+ scanMarkdown(node.value, node.position.start.line);
589
+ }
590
+ }
591
+ }
592
+ walk(document.children, 1);
593
+ code2.languages = [...languages];
594
+ links.domains = [...domains].sort();
595
+ const sourceLines = source === "" ? 0 : source.split("\n").length - (source.endsWith("\n") ? 1 : 0);
596
+ return {
597
+ file: {
598
+ path: options.path ?? null,
599
+ bytes: utf8Length(source),
600
+ lines: sourceLines
601
+ },
602
+ frontmatter: frontmatter ? {
603
+ present: true,
604
+ keys: frontmatter.keys,
605
+ data: frontmatter.data,
606
+ lines: frontmatter.lines
607
+ } : { present: false, keys: [], data: {}, lines: null },
608
+ length: {
609
+ words,
610
+ characters: source.length,
611
+ readingMinutes: Math.ceil(words / 200),
612
+ approxTokens: Math.ceil(source.length / 4)
613
+ },
614
+ outline,
615
+ blocks,
616
+ links,
617
+ images,
618
+ code: code2,
619
+ structure
620
+ };
621
+ }
622
+ var CODE_FENCE_RE2, HEADING_RE, LIST_ITEM_RE, TABLE_SEPARATOR_RE, CODE_SPAN_RE, IMAGE_RE, LINK_RE, AUTOLINK_RE, EXTERNAL_URL_RE;
623
+ var init_analyze = __esm({
624
+ "../core/dist/analyze.js"() {
625
+ "use strict";
626
+ init_frontmatter();
627
+ init_parser();
628
+ CODE_FENCE_RE2 = /^(`{3,}|~{3,})(.*)$/;
629
+ HEADING_RE = /^(#{1,6})\s+(.*)$/;
630
+ LIST_ITEM_RE = /^\s*(?:[-*+]|\d+[.)])\s+/;
631
+ TABLE_SEPARATOR_RE = /^\|?[\s:|-]+$/;
632
+ CODE_SPAN_RE = /`[^`]+`/g;
633
+ IMAGE_RE = /!\[([^\]]*)\]\(([^)]*)\)/g;
634
+ LINK_RE = /\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
635
+ AUTOLINK_RE = /<(https?:\/\/[^>\s]+)>/g;
636
+ EXTERNAL_URL_RE = /^(?:https?:)?\/\//i;
637
+ }
638
+ });
639
+
323
640
  // ../core/dist/position.js
324
641
  function bodyLineRange(node, bodyLineIndex) {
325
642
  const bodyLines = node.body.split("\n");
@@ -340,6 +657,45 @@ var init_position = __esm({
340
657
  });
341
658
 
342
659
  // ../core/dist/authoring.js
660
+ function typeLabel(def) {
661
+ switch (def.type) {
662
+ case "enum":
663
+ return `one of ${Object.values(def.entries ?? {}).join("|")}`;
664
+ case "literal":
665
+ return `one of ${(def.values ?? []).map(String).join("|")}`;
666
+ case "union":
667
+ return `one of ${(def.options ?? []).map((o) => typeLabel(o.def).replace(/^one of /, "")).join("|")}`;
668
+ default:
669
+ return def.type;
670
+ }
671
+ }
672
+ function describeProps(schema) {
673
+ const root = schema.def;
674
+ if (root.type !== "object" || !root.shape)
675
+ return [];
676
+ const lines = [];
677
+ for (const [name, field] of Object.entries(root.shape)) {
678
+ let current = field;
679
+ let optional2 = false;
680
+ let defaultValue;
681
+ let description = current.description;
682
+ while (["optional", "default", "nullable"].includes(current.def.type)) {
683
+ optional2 = true;
684
+ if (current.def.type === "default") {
685
+ const dv = current.def.defaultValue;
686
+ defaultValue = typeof dv === "function" ? dv() : dv;
687
+ }
688
+ if (!current.def.innerType)
689
+ break;
690
+ current = current.def.innerType;
691
+ description ??= current.description;
692
+ }
693
+ const presence = defaultValue !== void 0 ? `(optional, default: ${String(defaultValue)})` : optional2 ? "(optional)" : "(required)";
694
+ const suffix = description ? ` \u2014 ${description}` : "";
695
+ lines.push(`- ${name}: ${typeLabel(current.def)} ${presence}${suffix}`);
696
+ }
697
+ return lines;
698
+ }
343
699
  function generateAuthoringGuide(defs, opts = {}) {
344
700
  const includeExamples = opts.includeExamples ?? true;
345
701
  const includeAvoid = opts.includeAvoidRules ?? true;
@@ -349,6 +705,11 @@ function generateAuthoringGuide(defs, opts = {}) {
349
705
  for (const def of defs) {
350
706
  const lines = [`## ${def.name}`, ""];
351
707
  lines.push(def.childOnly ? `${def.description} (child block \u2014 only inside a parent that allows it)` : def.description);
708
+ if (def.props) {
709
+ const props = describeProps(def.props);
710
+ if (props.length > 0)
711
+ lines.push("", "Props:", ...props);
712
+ }
352
713
  lines.push("", `Content: ${def.content.describe()}`);
353
714
  if (def.authoring.useWhen.length > 0) {
354
715
  lines.push("", "Use when:", ...def.authoring.useWhen.map((u) => `- ${u}`));
@@ -742,6 +1103,8 @@ var init_dist = __esm({
742
1103
  "use strict";
743
1104
  init_diagnostics();
744
1105
  init_parser();
1106
+ init_frontmatter();
1107
+ init_analyze();
745
1108
  init_registry();
746
1109
  init_content_models();
747
1110
  init_validate();
@@ -6655,13 +7018,13 @@ var init_he = __esm({
6655
7018
  // no unit
6656
7019
  };
6657
7020
  const typeEntry = (t) => t ? TypeNames[t] : void 0;
6658
- const typeLabel = (t) => {
7021
+ const typeLabel2 = (t) => {
6659
7022
  const e = typeEntry(t);
6660
7023
  if (e)
6661
7024
  return e.label;
6662
7025
  return t ?? TypeNames.unknown.label;
6663
7026
  };
6664
- const withDefinite = (t) => `\u05D4${typeLabel(t)}`;
7027
+ const withDefinite = (t) => `\u05D4${typeLabel2(t)}`;
6665
7028
  const verbFor = (t) => {
6666
7029
  const e = typeEntry(t);
6667
7030
  const gender = e?.gender ?? "m";
@@ -6711,7 +7074,7 @@ var init_he = __esm({
6711
7074
  switch (issue2.code) {
6712
7075
  case "invalid_type": {
6713
7076
  const expectedKey = issue2.expected;
6714
- const expected = TypeDictionary[expectedKey ?? ""] ?? typeLabel(expectedKey);
7077
+ const expected = TypeDictionary[expectedKey ?? ""] ?? typeLabel2(expectedKey);
6715
7078
  const receivedType = parsedType(issue2.input);
6716
7079
  const received = TypeDictionary[receivedType] ?? TypeNames[receivedType]?.label ?? receivedType;
6717
7080
  if (/^[A-Z]/.test(issue2.expected)) {
@@ -16154,7 +16517,17 @@ import { pathToFileURL } from "node:url";
16154
16517
  async function loadRegistry(registryPath) {
16155
16518
  const registry2 = createBlockRegistry().use(genericBlocks());
16156
16519
  if (registryPath) {
16157
- const mod = await import(pathToFileURL(registryPath).href);
16520
+ let mod;
16521
+ try {
16522
+ mod = await import(pathToFileURL(registryPath).href);
16523
+ } catch (err) {
16524
+ if (err instanceof Error && "code" in err && err.code === "ERR_UNKNOWN_FILE_EXTENSION") {
16525
+ throw new Error(
16526
+ `Importing a TypeScript registry needs Node 22.18+ (native type stripping): ${registryPath}`
16527
+ );
16528
+ }
16529
+ throw err;
16530
+ }
16158
16531
  if (!Array.isArray(mod.default)) {
16159
16532
  throw new Error(
16160
16533
  `--registry module must default-export an array of block definitions: ${registryPath}`
@@ -16172,16 +16545,372 @@ var init_load_registry = __esm({
16172
16545
  }
16173
16546
  });
16174
16547
 
16548
+ // src/commands/agents.ts
16549
+ var agents_exports = {};
16550
+ __export(agents_exports, {
16551
+ agentsCommand: () => agentsCommand,
16552
+ installAgentIntegration: () => installAgentIntegration
16553
+ });
16554
+ import { existsSync } from "node:fs";
16555
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
16556
+ import { join } from "node:path";
16557
+ import { parseArgs } from "node:util";
16558
+ function upsertBlock(existing) {
16559
+ const start = existing.indexOf(START);
16560
+ const end = existing.indexOf(END);
16561
+ if (start !== -1 && end !== -1) {
16562
+ return existing.slice(0, start) + AGENTS_MD_BLOCK + existing.slice(end + END.length);
16563
+ }
16564
+ if (existing.trim() === "") return `${AGENTS_MD_BLOCK}
16565
+ `;
16566
+ return `${existing.replace(/\n*$/, "\n\n")}${AGENTS_MD_BLOCK}
16567
+ `;
16568
+ }
16569
+ async function installAgentIntegration(cwd, options, io) {
16570
+ const claude = options.claude ?? existsSync(join(cwd, ".claude"));
16571
+ const agentsMd = options.agentsMd ?? true;
16572
+ if (agentsMd) {
16573
+ const path = join(cwd, "AGENTS.md");
16574
+ let existing = "";
16575
+ try {
16576
+ existing = await readFile(path, "utf8");
16577
+ } catch {
16578
+ }
16579
+ const created = existing === "";
16580
+ await writeFile(path, upsertBlock(existing), "utf8");
16581
+ io.stdout(`${created ? "created" : "updated"}: AGENTS.md (contentbit block)`);
16582
+ }
16583
+ if (claude) {
16584
+ const skills = [
16585
+ ["contentbit-author", AUTHOR_SKILL],
16586
+ ["contentbit-audit", AUDIT_SKILL]
16587
+ ];
16588
+ for (const [name, content] of skills) {
16589
+ const dir = join(cwd, ".claude/skills", name);
16590
+ await mkdir(dir, { recursive: true });
16591
+ await writeFile(join(dir, "SKILL.md"), content, "utf8");
16592
+ io.stdout(`installed: .claude/skills/${name}/SKILL.md`);
16593
+ }
16594
+ }
16595
+ }
16596
+ async function agentsCommand(args, io) {
16597
+ const { values } = parseArgs({
16598
+ args,
16599
+ options: {
16600
+ claude: { type: "boolean", default: false },
16601
+ "no-agents-md": { type: "boolean", default: false },
16602
+ cwd: { type: "string", default: process.cwd() }
16603
+ }
16604
+ });
16605
+ await installAgentIntegration(
16606
+ values.cwd,
16607
+ {
16608
+ claude: values.claude || void 0,
16609
+ // false means "detect", not "skip"
16610
+ agentsMd: !values["no-agents-md"]
16611
+ },
16612
+ io
16613
+ );
16614
+ return 0;
16615
+ }
16616
+ var TEMPLATE_VERSION, AUTHOR_SKILL, AUDIT_SKILL, AGENTS_MD_BLOCK, START, END;
16617
+ var init_agents = __esm({
16618
+ "src/commands/agents.ts"() {
16619
+ "use strict";
16620
+ TEMPLATE_VERSION = 1;
16621
+ AUTHOR_SKILL = `---
16622
+ name: contentbit-author
16623
+ description: |
16624
+ Write or edit contentbit Markdown content (directive blocks like :::callout).
16625
+ Use when asked to create or modify content documents in a project that uses
16626
+ contentbit \u2014 blog posts, docs pages, changelogs, any Markdown covered by
16627
+ \`contentbit validate\`.
16628
+ version: ${TEMPLATE_VERSION}
16629
+ ---
16630
+
16631
+ # Writing contentbit content
16632
+
16633
+ contentbit documents are plain Markdown plus directive blocks
16634
+ (\`:::name{props} ... :::\`). Every block has a schema. Never guess block names,
16635
+ props, or body shapes \u2014 fetch the live guide from the project's registry first.
16636
+
16637
+ ## Find the project conventions
16638
+
16639
+ Check \`package.json\` for a \`content:check\` script. It holds the canonical
16640
+ validate invocation for this project: the content glob and, if present, the
16641
+ \`--registry <path>\` flag pointing at custom block definitions. Reuse both
16642
+ below. No script? Default to \`content/**/*.md\` with no \`--registry\` flag.
16643
+
16644
+ ## The loop
16645
+
16646
+ 1. **Fetch the authoring guide** (always \u2014 it covers this project's custom blocks):
16647
+
16648
+ \`\`\`sh
16649
+ contentbit instructions --audience llm [--registry <path from content:check>]
16650
+ \`\`\`
16651
+
16652
+ Read it before writing. It documents every available block: props, body
16653
+ shape, and when to use or avoid it.
16654
+
16655
+ 2. **Write the document.** Plain Markdown everywhere; blocks only where the
16656
+ guide's use-when guidance fits. Keep frontmatter consistent with sibling
16657
+ documents in the same folder.
16658
+
16659
+ 3. **Validate and fix until clean:**
16660
+
16661
+ \`\`\`sh
16662
+ contentbit validate <file> [--registry <path>]
16663
+ \`\`\`
16664
+
16665
+ Diagnostics print to stderr as \`file:line:col severity CODE message\`, often
16666
+ with a \`hint:\` line suggesting the fix. Exit 0 means clean; exit 1 means
16667
+ errors remain. Fix every diagnostic and re-run. Never finish with a failing
16668
+ validate.
16669
+
16670
+ ## Failure modes
16671
+
16672
+ - \`contentbit\` not found or no registry resolvable: the project is not set up.
16673
+ Say so and suggest \`npx contentbit@latest init\` \u2014 do not invent block syntax.
16674
+ - A block you want does not exist: use plain Markdown, or ask whether to define
16675
+ a custom block in the registry. Never emit an unregistered block name.
16676
+ `;
16677
+ AUDIT_SKILL = `---
16678
+ name: contentbit-audit
16679
+ description: |
16680
+ Audit contentbit Markdown content health using document stats. Use when asked
16681
+ to audit, review, or find improvements across content \u2014 thin pages, missing
16682
+ structure, validation issues \u2014 in a project that uses contentbit.
16683
+ version: ${TEMPLATE_VERSION}
16684
+ ---
16685
+
16686
+ # Auditing contentbit content
16687
+
16688
+ \`contentbit stats\` analyzes documents and prints JSON to stdout. It is a read
16689
+ tool: it always exits 0, even when documents have validation errors.
16690
+
16691
+ ## Gather
16692
+
16693
+ Check \`package.json\` for the \`content:check\` script to find this project's
16694
+ content glob and \`--registry\` flag, then:
16695
+
16696
+ \`\`\`sh
16697
+ contentbit stats "content/**/*.md" [--registry <path>]
16698
+ \`\`\`
16699
+
16700
+ One matched file prints a single stats object; multiple files print an array.
16701
+ Each entry includes the file path, frontmatter data, a heading \`outline\` with
16702
+ per-section word counts, \`blocks.byName\` usage counts, \`links.domains\`, and
16703
+ a \`validation\` summary (\`errors\`/\`warnings\`).
16704
+
16705
+ ## Interpret
16706
+
16707
+ Prioritize findings in this order:
16708
+
16709
+ 1. **Validation errors and warnings** \u2014 broken content ships broken pages.
16710
+ 2. **Thin documents** \u2014 outline sections with very low word counts.
16711
+ 3. **Block-less documents** \u2014 \`blocks.byName\` empty where sibling documents
16712
+ use blocks; structure (steps, callouts, comparisons, faq) may be missing.
16713
+ 4. **Missing or inconsistent frontmatter** compared to sibling documents.
16714
+ 5. **Structural imbalance** \u2014 skipped heading levels, single-section walls of text.
16715
+
16716
+ ## Report
16717
+
16718
+ Report findings per file with concrete suggestions, ordered by priority. Do not
16719
+ edit files during the audit. To fix a finding, follow the contentbit-author
16720
+ skill (fetch the guide, edit, validate until clean) \u2014 offer that as a follow-up.
16721
+ `;
16722
+ AGENTS_MD_BLOCK = `<!-- contentbit:start -->
16723
+
16724
+ ## contentbit content (generated \u2014 edits inside this block are overwritten)
16725
+
16726
+ This project validates Markdown content with contentbit. Documents are plain
16727
+ Markdown plus directive blocks (\`:::name{props} ... :::\`), each with a schema.
16728
+ The \`content:check\` script in package.json holds the canonical validate
16729
+ command \u2014 the content glob and the \`--registry\` flag \u2014 reuse its arguments.
16730
+
16731
+ When writing or editing content:
16732
+
16733
+ 1. Fetch the live authoring guide first \u2014 never guess block syntax:
16734
+ \`contentbit instructions --audience llm [--registry <path>]\`
16735
+ 2. Write plain Markdown; use blocks where the guide's use-when guidance fits.
16736
+ 3. Validate until clean (exit 0): \`contentbit validate <file> [--registry <path>]\`.
16737
+ Diagnostics print as \`file:line:col severity CODE message\` with fix hints.
16738
+
16739
+ When auditing content health:
16740
+
16741
+ - \`contentbit stats "content/**/*.md" [--registry <path>]\` prints JSON stats
16742
+ and always exits 0: outline word counts, block usage, link domains, and
16743
+ validation error/warning counts. Flag validation issues, thin documents, and
16744
+ block-less pages first.
16745
+
16746
+ If \`contentbit\` is unavailable, suggest \`npx contentbit@latest init\` instead
16747
+ of inventing block syntax.
16748
+
16749
+ <!-- contentbit:end -->`;
16750
+ START = "<!-- contentbit:start -->";
16751
+ END = "<!-- contentbit:end -->";
16752
+ }
16753
+ });
16754
+
16175
16755
  // src/commands/init.ts
16176
16756
  var init_exports = {};
16177
16757
  __export(init_exports, {
16178
16758
  initCommand: () => initCommand
16179
16759
  });
16180
16760
  import { spawn } from "node:child_process";
16181
- import { mkdir, readFile, writeFile } from "node:fs/promises";
16182
- import { join } from "node:path";
16183
- import { parseArgs } from "node:util";
16184
- function detectPackageManager() {
16761
+ import { existsSync as existsSync2 } from "node:fs";
16762
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
16763
+ import { join as join2 } from "node:path";
16764
+ import { parseArgs as parseArgs2 } from "node:util";
16765
+ function blockComponentsTemplate(styled) {
16766
+ const body = styled ? ` return (
16767
+ <figure className="my-6 border-s-2 ps-4">
16768
+ <blockquote className="text-lg italic">{ctx.renderMarkdown(data.markdown)}</blockquote>
16769
+ <figcaption className="text-muted-foreground mt-2 text-sm">
16770
+ \u2014 {String(node.props.author)}
16771
+ {node.props.role ? \`, \${String(node.props.role)}\` : null}
16772
+ </figcaption>
16773
+ </figure>
16774
+ )` : ` return (
16775
+ <figure style={{ margin: '1.5rem 0', borderLeft: '2px solid #d4d4d4', paddingLeft: '1rem' }}>
16776
+ <blockquote style={{ fontStyle: 'italic' }}>{ctx.renderMarkdown(data.markdown)}</blockquote>
16777
+ <figcaption style={{ marginTop: '0.5rem', fontSize: '0.875rem', opacity: 0.7 }}>
16778
+ \u2014 {String(node.props.author)}
16779
+ {node.props.role ? \`, \${String(node.props.role)}\` : null}
16780
+ </figcaption>
16781
+ </figure>
16782
+ )`;
16783
+ return `import type { BlockComponent, BlockComponentProps } from '@contentbit/react'
16784
+
16785
+ // One React component per custom block, keyed by block name. Definitions
16786
+ // live in ./registry.ts \u2014 add a block there, add its component here, and
16787
+ // the rest of the app never changes.
16788
+ function QuoteBlock({ node, ctx }: BlockComponentProps) {
16789
+ const data = node.data as { markdown: string }
16790
+ ${body}
16791
+ }
16792
+
16793
+ export const blockComponents: Record<string, BlockComponent> = {
16794
+ quote: QuoteBlock,
16795
+ }
16796
+ `;
16797
+ }
16798
+ function reactComponent(styled, mdWired, blocksImport) {
16799
+ const mdImport = mdWired ? "import ReactMarkdown from 'react-markdown'\n" : "";
16800
+ const mdProp = mdWired ? "\n renderMarkdown={(md) => <ReactMarkdown>{md}</ReactMarkdown>}" : `
16801
+ // TODO: plug your Markdown library in here, e.g. react-markdown.
16802
+ // One function renders all prose: https://contentbit.dev/docs/guides/markdown
16803
+ // renderMarkdown={(md) => <Markdown source={md} />}`;
16804
+ const rendererImport = styled ? `
16805
+ // The styled pack installed by shadcn. Yours to edit.
16806
+ import { ContentRenderer } from '@/components/content-blocks/content-renderer'` : "";
16807
+ const renderer = styled ? "ContentRenderer" : "ContentBlocks";
16808
+ const reactImport = styled ? "" : "import { ContentBlocks } from '@contentbit/react'\n";
16809
+ return `'use client'
16810
+
16811
+ import { genericBlocks } from '@contentbit/blocks'
16812
+ import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
16813
+ ${reactImport}${mdImport}${rendererImport}
16814
+ // Everything block-related lives in the blocks/ folder: definitions in
16815
+ // registry.ts (shared with the validate CLI), components in components.tsx.
16816
+ import customBlocks from '${blocksImport}/registry'
16817
+ import { blockComponents } from '${blocksImport}/components'
16818
+
16819
+ const registry = createBlockRegistry().use(genericBlocks()).use(customBlocks)
16820
+
16821
+ export function Content({ source }: { source: string }) {
16822
+ const result = validateDocument(parseDocument(source), registry)
16823
+ return (
16824
+ <${renderer}
16825
+ document={result.document}
16826
+ components={blockComponents}${mdProp}
16827
+ />
16828
+ )
16829
+ }
16830
+ `;
16831
+ }
16832
+ function htmlRenderScript(md) {
16833
+ const wiring = md === "marked" ? `import { marked } from 'marked'
16834
+
16835
+ const renderMarkdown = (md) => marked.parse(md, { async: false })` : md === "markdown-it" ? `import MarkdownIt from 'markdown-it'
16836
+
16837
+ const mdIt = new MarkdownIt() // html: false by default \u2014 raw HTML stays escaped
16838
+ const renderMarkdown = (md) => mdIt.render(md)` : `// TODO: plug a Markdown library in here (marked, markdown-it, remark).
16839
+ const renderMarkdown = undefined`;
16840
+ return `// Render content/example.md to example.html. Run: node scripts/render-example.mjs
16841
+ import { genericBlocks } from '@contentbit/blocks'
16842
+ import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
16843
+ import { renderToHtml } from '@contentbit/html'
16844
+ import { readFile, writeFile } from 'node:fs/promises'
16845
+ ${wiring}
16846
+
16847
+ const source = await readFile('content/example.md', 'utf8')
16848
+ const registry = createBlockRegistry().use(genericBlocks())
16849
+ const result = validateDocument(parseDocument(source), registry)
16850
+ const html = renderToHtml(result.document, { renderMarkdown })
16851
+ await writeFile('example.html', html, 'utf8')
16852
+ console.log('wrote example.html')
16853
+ `;
16854
+ }
16855
+ function detectFramework(cwd, deps) {
16856
+ if ((deps["@tanstack/react-start"] || deps["@tanstack/react-router"]) && existsSync2(join2(cwd, "src/routes"))) {
16857
+ return {
16858
+ framework: "tanstack",
16859
+ componentPath: "src/components/content-blocks.tsx",
16860
+ pagePath: "src/routes/example.tsx"
16861
+ };
16862
+ }
16863
+ if (deps.next) {
16864
+ const appDir = existsSync2(join2(cwd, "src/app")) ? "src/app" : "app";
16865
+ if (existsSync2(join2(cwd, appDir))) {
16866
+ return {
16867
+ framework: "next",
16868
+ componentPath: "components/content-blocks.tsx",
16869
+ pagePath: `${appDir}/example/page.tsx`
16870
+ };
16871
+ }
16872
+ }
16873
+ return { framework: null, componentPath: "components/content-blocks.tsx", pagePath: null };
16874
+ }
16875
+ function astroPage(styled) {
16876
+ const importLine = styled ? "import ContentRenderer from '../components/content-blocks/content-renderer.astro'" : "import { ContentBlocks } from '@contentbit/astro/components'";
16877
+ const renderer = styled ? "ContentRenderer" : "ContentBlocks";
16878
+ return `---
16879
+ import { genericBlocks } from '@contentbit/blocks'
16880
+ import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
16881
+ import { getEntry } from 'astro:content'
16882
+
16883
+ ${importLine}
16884
+
16885
+ // Definitions in blocks/registry.ts are shared with the validate CLI.
16886
+ import customBlocks from '../../blocks/registry'
16887
+ import QuoteBlock from '../../blocks/QuoteBlock.astro'
16888
+
16889
+ // Entry ids are the file path relative to the collection base, minus ".md".
16890
+ const entry = await getEntry('articles', 'example')
16891
+ if (!entry?.body) throw new Error('Entry "example" not found in the articles collection.')
16892
+
16893
+ const registry = createBlockRegistry().use(genericBlocks()).use(customBlocks)
16894
+ // Static pages render at build time, so invalid blocks fail the build here.
16895
+ const result = validateDocument(parseDocument(entry.body), registry)
16896
+ ---
16897
+
16898
+ <main style="max-width: 42rem; margin: 0 auto; padding: 3rem 1.5rem;">
16899
+ <${renderer} document={result.document} components={{ quote: QuoteBlock }} />
16900
+ </main>
16901
+ `;
16902
+ }
16903
+ function detectPackageManager(cwd) {
16904
+ const locks = [
16905
+ ["pnpm-lock.yaml", "pnpm"],
16906
+ ["yarn.lock", "yarn"],
16907
+ ["bun.lock", "bun"],
16908
+ ["bun.lockb", "bun"],
16909
+ ["package-lock.json", "npm"]
16910
+ ];
16911
+ for (const [file2, pm] of locks) {
16912
+ if (existsSync2(join2(cwd, file2))) return pm;
16913
+ }
16185
16914
  const agent = process.env.npm_config_user_agent ?? "";
16186
16915
  for (const pm of ["pnpm", "yarn", "bun"]) {
16187
16916
  if (agent.startsWith(pm)) return pm;
@@ -16192,6 +16921,12 @@ function installArgs(pm, dev, pkgs) {
16192
16921
  const add = pm === "npm" ? "install" : "add";
16193
16922
  return dev ? [add, "-D", ...pkgs] : [add, ...pkgs];
16194
16923
  }
16924
+ function dlxCommand(pm) {
16925
+ if (pm === "pnpm") return ["pnpm", ["dlx"]];
16926
+ if (pm === "yarn") return ["yarn", ["dlx"]];
16927
+ if (pm === "bun") return ["bunx", []];
16928
+ return ["npx", ["--yes"]];
16929
+ }
16195
16930
  function runInstall(pm, args, cwd) {
16196
16931
  return new Promise((resolve) => {
16197
16932
  const child = spawn(pm, args, { cwd, stdio: "inherit", shell: process.platform === "win32" });
@@ -16199,37 +16934,62 @@ function runInstall(pm, args, cwd) {
16199
16934
  child.on("error", () => resolve(1));
16200
16935
  });
16201
16936
  }
16937
+ async function installStyledPack(cwd, pack, noInstall, io) {
16938
+ const componentsJsonPath = join2(cwd, "components.json");
16939
+ const componentsJson = JSON.parse(await readFile2(componentsJsonPath, "utf8"));
16940
+ componentsJson.registries ??= {};
16941
+ if (!componentsJson.registries["@contentbit"]) {
16942
+ componentsJson.registries["@contentbit"] = "https://contentbit.dev/r/{name}.json";
16943
+ await writeFile2(componentsJsonPath, `${JSON.stringify(componentsJson, null, 2)}
16944
+ `, "utf8");
16945
+ io.stdout("added @contentbit registry to components.json");
16946
+ }
16947
+ if (noInstall) {
16948
+ io.stdout(`skipped: shadcn add ${pack}`);
16949
+ return true;
16950
+ }
16951
+ const [bin, prefix] = dlxCommand(detectPackageManager(cwd));
16952
+ io.stdout(`installing the styled pack: shadcn add ${pack}`);
16953
+ const code2 = await runInstall(bin, [...prefix, "shadcn@latest", "add", pack, "--yes"], cwd);
16954
+ if (code2 !== 0) io.stderr("styled pack install failed; falling back to headless defaults");
16955
+ return code2 === 0;
16956
+ }
16202
16957
  async function scaffold(path, content) {
16203
16958
  try {
16204
- await readFile(path, "utf8");
16959
+ await readFile2(path, "utf8");
16205
16960
  return "skipped";
16206
16961
  } catch {
16207
- await mkdir(join(path, ".."), { recursive: true });
16208
- await writeFile(path, content, "utf8");
16962
+ await mkdir2(join2(path, ".."), { recursive: true });
16963
+ await writeFile2(path, content, "utf8");
16209
16964
  return "created";
16210
16965
  }
16211
16966
  }
16212
16967
  async function initCommand(args, io) {
16213
- const { values } = parseArgs({
16968
+ const { values } = parseArgs2({
16214
16969
  args,
16215
16970
  options: {
16216
16971
  target: { type: "string", short: "t" },
16972
+ md: { type: "string" },
16217
16973
  yes: { type: "boolean", short: "y", default: false },
16218
16974
  cwd: { type: "string", default: process.cwd() },
16219
- "no-install": { type: "boolean", default: false }
16975
+ "no-install": { type: "boolean", default: false },
16976
+ "no-page": { type: "boolean", default: false },
16977
+ "no-styled": { type: "boolean", default: false },
16978
+ "no-agents": { type: "boolean", default: false }
16220
16979
  }
16221
16980
  });
16222
16981
  const cwd = values.cwd;
16223
16982
  let pkg;
16224
- const pkgPath = join(cwd, "package.json");
16983
+ const pkgPath = join2(cwd, "package.json");
16225
16984
  try {
16226
- pkg = JSON.parse(await readFile(pkgPath, "utf8"));
16985
+ pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
16227
16986
  } catch {
16228
16987
  io.stderr("No package.json found. Run this inside a project (npm init first).");
16229
16988
  return 1;
16230
16989
  }
16231
16990
  const hasReact = Boolean(pkg.dependencies?.react ?? pkg.devDependencies?.react);
16232
- const detected = hasReact ? "react" : "html";
16991
+ const hasAstro = Boolean(pkg.dependencies?.astro ?? pkg.devDependencies?.astro);
16992
+ const detected = hasAstro ? "astro" : hasReact ? "react" : "html";
16233
16993
  let target;
16234
16994
  if (values.target) {
16235
16995
  if (!TARGETS.includes(values.target)) {
@@ -16244,6 +17004,7 @@ async function initCommand(args, io) {
16244
17004
  initialValue: detected,
16245
17005
  options: [
16246
17006
  { value: "react", label: "React", hint: "ContentBlocks component" },
17007
+ { value: "astro", label: "Astro", hint: "content collections + .astro components" },
16247
17008
  { value: "html", label: "Static HTML", hint: "renderToHtml, no framework" },
16248
17009
  { value: "markdown", label: "Plain Markdown", hint: "fallback rendering only" }
16249
17010
  ]
@@ -16253,13 +17014,39 @@ async function initCommand(args, io) {
16253
17014
  } else {
16254
17015
  target = detected;
16255
17016
  }
16256
- const runtime = ["@contentbit/core", "@contentbit/blocks"];
17017
+ const choices = MD_CHOICES[target];
17018
+ let md;
17019
+ if (values.md) {
17020
+ if (!choices.includes(values.md)) {
17021
+ io.stderr(`Unknown markdown library "${values.md}". Use one of: ${choices.join(", ")}`);
17022
+ return 2;
17023
+ }
17024
+ md = values.md;
17025
+ } else if (choices.length > 1 && !values.yes && process.stdin.isTTY && process.stdout.isTTY) {
17026
+ const { isCancel, select } = await import("@clack/prompts");
17027
+ const answer = await select({
17028
+ message: "Markdown library for prose rendering?",
17029
+ initialValue: choices[0],
17030
+ options: choices.map((c) => ({
17031
+ value: c,
17032
+ label: c,
17033
+ hint: c === "none" ? "wire one yourself later" : "installed and wired for you"
17034
+ }))
17035
+ });
17036
+ if (isCancel(answer)) return 1;
17037
+ md = answer;
17038
+ } else {
17039
+ md = choices[0];
17040
+ }
17041
+ const runtime = ["@contentbit/core", "@contentbit/blocks", "zod"];
16257
17042
  if (target === "react") runtime.push("@contentbit/react");
16258
17043
  if (target === "html") runtime.push("@contentbit/html");
17044
+ if (target === "astro") runtime.push("@contentbit/astro");
17045
+ if (md !== "none") runtime.push(md);
16259
17046
  if (values["no-install"]) {
16260
17047
  io.stdout(`skipped install: ${runtime.join(" ")} + contentbit (dev)`);
16261
17048
  } else {
16262
- const pm = detectPackageManager();
17049
+ const pm = detectPackageManager(cwd);
16263
17050
  io.stdout(`installing with ${pm}: ${runtime.join(" ")}`);
16264
17051
  if (await runInstall(pm, installArgs(pm, false, runtime), cwd) !== 0) {
16265
17052
  io.stderr("install failed");
@@ -16271,68 +17058,142 @@ async function initCommand(args, io) {
16271
17058
  }
16272
17059
  }
16273
17060
  const files = [
16274
- ["blocks/registry.mjs", REGISTRY_TEMPLATE],
17061
+ ["blocks/registry.ts", REGISTRY_TEMPLATE],
16275
17062
  ["content/example.md", EXAMPLE_CONTENT]
16276
17063
  ];
16277
- if (target === "react") files.push(["components/content-blocks.tsx", REACT_COMPONENT]);
17064
+ const layout = detectFramework(cwd, { ...pkg.dependencies, ...pkg.devDependencies });
17065
+ let styled = false;
17066
+ const componentsJsonPath = join2(cwd, "components.json");
17067
+ if (target === "react" && !values["no-styled"] && existsSync2(componentsJsonPath)) {
17068
+ styled = await installStyledPack(cwd, "@contentbit/generic-pack", values["no-install"], io);
17069
+ }
17070
+ if (target === "react") {
17071
+ const depth = layout.componentPath.split("/").length - 1;
17072
+ const blocksImport = `${"../".repeat(depth)}blocks`;
17073
+ files.push(["blocks/components.tsx", blockComponentsTemplate(styled)]);
17074
+ files.push([
17075
+ layout.componentPath,
17076
+ reactComponent(styled, md === "react-markdown", blocksImport)
17077
+ ]);
17078
+ if (!values["no-page"] && layout.pagePath) {
17079
+ files.push([layout.pagePath, layout.framework === "tanstack" ? TANSTACK_PAGE : NEXT_PAGE]);
17080
+ }
17081
+ }
17082
+ if (target === "html") {
17083
+ files.push([
17084
+ "scripts/render-example.mjs",
17085
+ htmlRenderScript(md)
17086
+ ]);
17087
+ }
17088
+ if (target === "astro") {
17089
+ let astroStyled = false;
17090
+ if (!values["no-styled"] && existsSync2(componentsJsonPath)) {
17091
+ astroStyled = await installStyledPack(cwd, "@contentbit/astro-pack", values["no-install"], io);
17092
+ }
17093
+ files.push(["blocks/QuoteBlock.astro", ASTRO_QUOTE_BLOCK]);
17094
+ const configCandidates = ["ts", "mts", "mjs", "js"].flatMap((ext) => [
17095
+ `src/content.config.${ext}`,
17096
+ `src/content/config.${ext}`
17097
+ ]);
17098
+ const existingConfig = configCandidates.find((p) => existsSync2(join2(cwd, p)));
17099
+ if (existingConfig) {
17100
+ io.stdout(`content config exists (${existingConfig}); add this collection manually:`);
17101
+ io.stdout(ASTRO_CONTENT_CONFIG);
17102
+ io.stdout('the example page expects the "articles" collection above');
17103
+ } else {
17104
+ files.push(["src/content.config.ts", ASTRO_CONTENT_CONFIG]);
17105
+ }
17106
+ if (!values["no-page"]) files.push(["src/pages/example.astro", astroPage(astroStyled)]);
17107
+ }
16278
17108
  for (const [rel, content] of files) {
16279
- const result = await scaffold(join(cwd, rel), content);
17109
+ const result = await scaffold(join2(cwd, rel), content);
16280
17110
  io.stdout(`${result}: ${rel}`);
16281
17111
  }
16282
- const fresh = JSON.parse(await readFile(pkgPath, "utf8"));
17112
+ const fresh = JSON.parse(await readFile2(pkgPath, "utf8"));
16283
17113
  fresh.scripts ??= {};
16284
17114
  if (!fresh.scripts["content:check"]) {
16285
- fresh.scripts["content:check"] = 'contentbit validate "content/**/*.md" --registry ./blocks/registry.mjs';
16286
- await writeFile(pkgPath, `${JSON.stringify(fresh, null, 2)}
17115
+ fresh.scripts["content:check"] = 'contentbit validate "content/**/*.md" --registry ./blocks/registry.ts';
17116
+ await writeFile2(pkgPath, `${JSON.stringify(fresh, null, 2)}
16287
17117
  `, "utf8");
16288
17118
  io.stdout("added script: content:check");
16289
17119
  }
16290
- const registry2 = await loadRegistry();
17120
+ let registry2;
17121
+ try {
17122
+ registry2 = await loadRegistry(join2(cwd, "blocks/registry.ts"));
17123
+ } catch {
17124
+ registry2 = await loadRegistry();
17125
+ }
16291
17126
  const guide = registry2.toAuthoringGuide({ audience: "llm", includeExamples: true });
16292
- await writeFile(join(cwd, "contentbit-guide.md"), guide, "utf8");
17127
+ await writeFile2(join2(cwd, "contentbit-guide.md"), guide, "utf8");
16293
17128
  io.stdout("created: contentbit-guide.md (LLM authoring instructions)");
17129
+ if (!values["no-agents"]) {
17130
+ await installAgentIntegration(cwd, {}, io);
17131
+ io.stdout("Agent integration installed \u2014 try asking your agent:");
17132
+ io.stdout(' "write a blog post about X" or "audit my content"');
17133
+ }
16294
17134
  io.stdout("");
16295
17135
  io.stdout("Done. Next steps:");
16296
- io.stdout(` 1. Validate the starter content: ${detectPackageManager()} run content:check`);
17136
+ io.stdout(` 1. Validate the starter content: ${detectPackageManager(cwd)} run content:check`);
16297
17137
  if (target === "react") {
16298
- io.stdout(' 2. Render it: import { Content } from "./components/content-blocks"');
17138
+ if (!values["no-page"] && layout.pagePath) {
17139
+ io.stdout(" 2. Start the dev server and open /example to see the article rendered.");
17140
+ } else {
17141
+ io.stdout(' 2. Render it: import { Content } from "./components/content-blocks"');
17142
+ io.stdout(" <Content source={...content/example.md as a string} />");
17143
+ }
16299
17144
  io.stdout(" 3. Styled components: pnpm dlx shadcn@latest add @contentbit/generic-pack");
17145
+ } else if (target === "astro") {
17146
+ io.stdout(" 2. Start the dev server and open /example to see the article rendered.");
17147
+ io.stdout(" 3. Styled components: pnpm dlx shadcn@latest add @contentbit/astro-pack");
16300
17148
  } else if (target === "html") {
16301
- io.stdout(" 2. Render it: contentbit render content/example.md --target html");
17149
+ io.stdout(" 2. Render it: node scripts/render-example.mjs && open example.html");
16302
17150
  } else {
16303
17151
  io.stdout(" 2. Render it: contentbit render content/example.md --target markdown");
16304
17152
  }
16305
17153
  io.stdout(" Docs: https://contentbit.dev/docs");
16306
17154
  return 0;
16307
17155
  }
16308
- var TARGETS, REGISTRY_TEMPLATE, EXAMPLE_CONTENT, REACT_COMPONENT;
17156
+ var TARGETS, MD_CHOICES, REGISTRY_TEMPLATE, EXAMPLE_CONTENT, TANSTACK_PAGE, NEXT_PAGE, ASTRO_CONTENT_CONFIG, ASTRO_QUOTE_BLOCK;
16309
17157
  var init_init = __esm({
16310
17158
  "src/commands/init.ts"() {
16311
17159
  "use strict";
16312
17160
  init_load_registry();
16313
- TARGETS = ["react", "html", "markdown"];
16314
- REGISTRY_TEMPLATE = `// Custom blocks for this project. The CLI and your app share this module:
17161
+ init_agents();
17162
+ TARGETS = ["react", "html", "markdown", "astro"];
17163
+ MD_CHOICES = {
17164
+ react: ["react-markdown", "none"],
17165
+ html: ["marked", "markdown-it", "none"],
17166
+ markdown: ["none"],
17167
+ // @contentbit/astro ships its own marked-based default; nothing to install.
17168
+ astro: ["none"]
17169
+ };
17170
+ REGISTRY_TEMPLATE = `// Custom block definitions for this project. The CLI and your app share
17171
+ // this module \u2014 Node 22.18+ imports TypeScript directly:
16315
17172
  //
16316
- // contentbit validate "content/**/*.md" --registry ./blocks/registry.mjs
17173
+ // contentbit validate "content/**/*.md" --registry ./blocks/registry.ts
16317
17174
  //
16318
- // Define blocks with @contentbit/core and default-export them as an array.
17175
+ // Definitions stay framework-free (the CLI and every render target use
17176
+ // them); React components live next door in blocks/components.tsx.
16319
17177
  // Docs: https://contentbit.dev/docs/guides/custom-blocks
16320
- //
16321
- // import { defineBlock, pipeRows } from '@contentbit/core'
16322
- // import { z } from 'zod'
16323
- //
16324
- // const pricingTable = defineBlock({
16325
- // name: 'pricing-table',
16326
- // description: 'Compares product plans.',
16327
- // props: z.object({ currency: z.enum(['usd', 'eur']).default('usd') }),
16328
- // content: pipeRows({ columns: ['plan', 'price'], minRows: 2 }),
16329
- // authoring: {
16330
- // useWhen: ['Comparing pricing plans'],
16331
- // example: ':::pricing-table\\n- Starter | $0\\n- Pro | $12/mo\\n:::',
16332
- // },
16333
- // })
17178
+ import { defineBlock, markdownBody, type BlockDefinition } from '@contentbit/core'
17179
+ import { z } from 'zod'
16334
17180
 
16335
- export default []
17181
+ export const quote = defineBlock({
17182
+ name: 'quote',
17183
+ description: 'A pull quote with an author.',
17184
+ props: z.object({
17185
+ author: z.string().min(1),
17186
+ role: z.string().optional(),
17187
+ }),
17188
+ content: markdownBody({ minLength: 3 }),
17189
+ authoring: {
17190
+ useWhen: ['Quoting a person to support a point'],
17191
+ avoidWhen: ['Highlighting your own remark, use callout instead'],
17192
+ example: ':::quote{author="Ada Lovelace"}\\nThe Analytical Engine weaves algebraic patterns.\\n:::',
17193
+ },
17194
+ })
17195
+
17196
+ export default [quote] satisfies BlockDefinition<unknown>[]
16336
17197
  `;
16337
17198
  EXAMPLE_CONTENT = `# Hello, Content Blocks
16338
17199
 
@@ -16347,24 +17208,74 @@ Run the validate script and you will get file:line:col diagnostics.
16347
17208
  2. Run \`contentbit validate "content/**/*.md"\`.
16348
17209
  3. Render it with the target you picked at init.
16349
17210
  :::
17211
+
17212
+ This one is a **custom block**, defined in \`blocks/registry.ts\` and rendered
17213
+ by the \`QuoteBlock\` component, in about twenty lines:
17214
+
17215
+ :::quote{author="Ada Lovelace" role="Notes on the Analytical Engine, 1843"}
17216
+ The Analytical Engine weaves algebraic patterns just as the Jacquard loom
17217
+ weaves flowers and leaves.
17218
+ :::
16350
17219
  `;
16351
- REACT_COMPONENT = `import { genericBlocks } from '@contentbit/blocks'
16352
- import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
16353
- import { ContentBlocks } from '@contentbit/react'
17220
+ TANSTACK_PAGE = `import { createFileRoute } from '@tanstack/react-router'
16354
17221
 
16355
- const registry = createBlockRegistry().use(genericBlocks())
17222
+ import { Content } from '../components/content-blocks'
17223
+ // Vite's ?raw import inlines the Markdown as a string at build time.
17224
+ import source from '../../content/example.md?raw'
16356
17225
 
16357
- export function Content({ source }: { source: string }) {
16358
- const result = validateDocument(parseDocument(source), registry)
17226
+ export const Route = createFileRoute('/example')({ component: ExamplePage })
17227
+
17228
+ function ExamplePage() {
16359
17229
  return (
16360
- <ContentBlocks
16361
- document={result.document}
16362
- // TODO: plug your Markdown library in here, e.g. react-markdown.
16363
- // One function renders all prose: https://contentbit.dev/docs/guides/markdown
16364
- // renderMarkdown={(md) => <Markdown source={md} />}
16365
- />
17230
+ <main style={{ maxWidth: '42rem', margin: '0 auto', padding: '3rem 1.5rem' }}>
17231
+ <Content source={source} />
17232
+ </main>
16366
17233
  )
16367
17234
  }
17235
+ `;
17236
+ NEXT_PAGE = `import { readFile } from 'node:fs/promises'
17237
+
17238
+ // If your project has no "@/" path alias, switch to a relative import.
17239
+ import { Content } from '@/components/content-blocks'
17240
+
17241
+ export default async function ExamplePage() {
17242
+ const source = await readFile('content/example.md', 'utf8')
17243
+ return (
17244
+ <main style={{ maxWidth: '42rem', margin: '0 auto', padding: '3rem 1.5rem' }}>
17245
+ <Content source={source} />
17246
+ </main>
17247
+ )
17248
+ }
17249
+ `;
17250
+ ASTRO_CONTENT_CONFIG = `import { defineCollection } from 'astro:content'
17251
+ import { glob } from 'astro/loaders'
17252
+
17253
+ export const collections = {
17254
+ articles: defineCollection({
17255
+ // Astro's builtin Markdown loader. Entry bodies are parsed and validated
17256
+ // where they render (see src/pages/example.astro); \`contentbit validate\`
17257
+ // covers the same files in CI.
17258
+ loader: glob({ pattern: '**/*.md', base: './content' }),
17259
+ }),
17260
+ }
17261
+ `;
17262
+ ASTRO_QUOTE_BLOCK = `---
17263
+ // The Astro component for the custom \`quote\` block defined in blocks/registry.ts.
17264
+ // Block props arrive as component props; nested content arrives via <slot />.
17265
+ interface Props {
17266
+ author: string
17267
+ role?: string
17268
+ }
17269
+
17270
+ const { author, role } = Astro.props
17271
+ ---
17272
+
17273
+ <figure style="margin: 1.5rem 0; border-left: 2px solid #d4d4d4; padding-left: 1rem;">
17274
+ <blockquote style="font-style: italic;"><slot /></blockquote>
17275
+ <figcaption style="margin-top: 0.5rem; font-size: 0.875rem; opacity: 0.7;">
17276
+ \u2014 {author}{role ? \`, \${role}\` : null}
17277
+ </figcaption>
17278
+ </figure>
16368
17279
  `;
16369
17280
  }
16370
17281
  });
@@ -16374,11 +17285,11 @@ var validate_exports = {};
16374
17285
  __export(validate_exports, {
16375
17286
  validateCommand: () => validateCommand
16376
17287
  });
16377
- import { readFile as readFile2 } from "node:fs/promises";
16378
- import { parseArgs as parseArgs2 } from "node:util";
17288
+ import { readFile as readFile3 } from "node:fs/promises";
17289
+ import { parseArgs as parseArgs3 } from "node:util";
16379
17290
  import { glob } from "tinyglobby";
16380
17291
  async function validateCommand(args, io) {
16381
- const { values, positionals } = parseArgs2({
17292
+ const { values, positionals } = parseArgs3({
16382
17293
  args,
16383
17294
  allowPositionals: true,
16384
17295
  options: {
@@ -16399,8 +17310,8 @@ async function validateCommand(args, io) {
16399
17310
  let errors = 0;
16400
17311
  let warnings = 0;
16401
17312
  for (const file2 of files.sort()) {
16402
- const source = await readFile2(file2, "utf8");
16403
- const result = validateDocument(parseDocument(source), registry2);
17313
+ const source = await readFile3(file2, "utf8");
17314
+ const result = validateDocument(parseDocument(stripFrontmatter(source)), registry2);
16404
17315
  for (const d of result.diagnostics) {
16405
17316
  io.stderr(formatDiagnostic(d, file2));
16406
17317
  if (d.severity === "error") errors++;
@@ -16420,6 +17331,63 @@ var init_validate2 = __esm({
16420
17331
  }
16421
17332
  });
16422
17333
 
17334
+ // src/commands/stats.ts
17335
+ var stats_exports = {};
17336
+ __export(stats_exports, {
17337
+ statsCommand: () => statsCommand
17338
+ });
17339
+ import { readFile as readFile4 } from "node:fs/promises";
17340
+ import { parseArgs as parseArgs4 } from "node:util";
17341
+ import { glob as glob2 } from "tinyglobby";
17342
+ async function fileStats(file2, registry2) {
17343
+ const source = await readFile4(file2, "utf8");
17344
+ const stats = analyzeDocument(source, { path: file2 });
17345
+ if (registry2) {
17346
+ const result = validateDocument(parseDocument(stripFrontmatter(source)), registry2);
17347
+ let errors = 0;
17348
+ let warnings = 0;
17349
+ for (const d of result.diagnostics) {
17350
+ if (d.severity === "error") errors++;
17351
+ else if (d.severity === "warning") warnings++;
17352
+ }
17353
+ stats.validation = { errors, warnings };
17354
+ }
17355
+ return stats;
17356
+ }
17357
+ async function statsCommand(args, io) {
17358
+ const { values, positionals } = parseArgs4({
17359
+ args,
17360
+ allowPositionals: true,
17361
+ options: {
17362
+ registry: { type: "string" },
17363
+ "no-validate": { type: "boolean", default: false }
17364
+ }
17365
+ });
17366
+ if (positionals.length === 0) {
17367
+ io.stderr("stats: provide at least one file or glob.");
17368
+ return 2;
17369
+ }
17370
+ const files = await glob2(positionals, { absolute: true });
17371
+ if (files.length === 0) {
17372
+ io.stderr(`stats: no files matched ${positionals.join(" ")}`);
17373
+ return 2;
17374
+ }
17375
+ const registry2 = values["no-validate"] ? null : await loadRegistry(values.registry);
17376
+ const all = [];
17377
+ for (const file2 of files.sort()) {
17378
+ all.push(await fileStats(file2, registry2));
17379
+ }
17380
+ io.stdout(JSON.stringify(all.length === 1 ? all[0] : all, null, 2));
17381
+ return 0;
17382
+ }
17383
+ var init_stats = __esm({
17384
+ "src/commands/stats.ts"() {
17385
+ "use strict";
17386
+ init_dist();
17387
+ init_load_registry();
17388
+ }
17389
+ });
17390
+
16423
17391
  // ../html/dist/escape.js
16424
17392
  function escapeHtml(value) {
16425
17393
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -16500,12 +17468,18 @@ var init_blocks = __esm({
16500
17468
  });
16501
17469
 
16502
17470
  // ../html/dist/render.js
16503
- function defaultMarkdown(md) {
17471
+ function fallbackMarkdown(md) {
16504
17472
  return md.trim().split(/\n{2,}/).map((p) => `<p>${escapeHtml(p)}</p>`).join("\n");
16505
17473
  }
17474
+ function invalidBlockHtml(node, prefix) {
17475
+ return `<div class="${prefix}invalid" data-cb-invalid="${escapeHtml(node.name)}"><pre>${escapeHtml(node.body)}</pre></div>`;
17476
+ }
17477
+ function unrenderableBlockError(name) {
17478
+ return new Error(`Cannot render block "${name}": not validated or no renderer registered.`);
17479
+ }
16506
17480
  function renderToHtml(document, opts = {}) {
16507
17481
  const prefix = opts.classPrefix ?? "cb-";
16508
- const renderMarkdown = opts.renderMarkdown ?? defaultMarkdown;
17482
+ const renderMarkdown = opts.renderMarkdown ?? fallbackMarkdown;
16509
17483
  const renderers = { ...genericHtmlRenderers, ...opts.renderers };
16510
17484
  const onInvalid = opts.onInvalid ?? "fallback";
16511
17485
  const ctx = {
@@ -16519,13 +17493,11 @@ function renderToHtml(document, opts = {}) {
16519
17493
  const renderer = renderers[node.name];
16520
17494
  if (renderer && isValidatedBlock(node))
16521
17495
  return renderer(node, ctx);
16522
- if (onInvalid === "strict") {
16523
- throw new Error(`Cannot render block "${node.name}": not validated or no renderer registered.`);
16524
- }
16525
- if (onInvalid === "annotated") {
16526
- return `<div class="${prefix}invalid" data-cb-invalid="${escapeHtml(node.name)}"><pre>${escapeHtml(node.body)}</pre></div>`;
16527
- }
16528
- return defaultMarkdown(node.body);
17496
+ if (onInvalid === "strict")
17497
+ throw unrenderableBlockError(node.name);
17498
+ if (onInvalid === "annotated")
17499
+ return invalidBlockHtml(node, prefix);
17500
+ return fallbackMarkdown(node.body);
16529
17501
  }).join("\n");
16530
17502
  }
16531
17503
  };
@@ -16553,10 +17525,10 @@ var render_exports = {};
16553
17525
  __export(render_exports, {
16554
17526
  renderCommand: () => renderCommand
16555
17527
  });
16556
- import { readFile as readFile3 } from "node:fs/promises";
16557
- import { parseArgs as parseArgs3 } from "node:util";
17528
+ import { readFile as readFile5 } from "node:fs/promises";
17529
+ import { parseArgs as parseArgs5 } from "node:util";
16558
17530
  async function renderCommand(args, io) {
16559
- const { values, positionals } = parseArgs3({
17531
+ const { values, positionals } = parseArgs5({
16560
17532
  args,
16561
17533
  allowPositionals: true,
16562
17534
  options: {
@@ -16571,7 +17543,7 @@ async function renderCommand(args, io) {
16571
17543
  return 2;
16572
17544
  }
16573
17545
  const registry2 = await loadRegistry(values.registry);
16574
- const source = await readFile3(file2, "utf8");
17546
+ const source = await readFile5(file2, "utf8");
16575
17547
  const result = validateDocument(parseDocument(source), registry2);
16576
17548
  if (!result.ok) {
16577
17549
  for (const d of result.diagnostics) io.stderr(formatDiagnostic(d, file2));
@@ -16597,9 +17569,9 @@ var instructions_exports = {};
16597
17569
  __export(instructions_exports, {
16598
17570
  instructionsCommand: () => instructionsCommand
16599
17571
  });
16600
- import { parseArgs as parseArgs4 } from "node:util";
17572
+ import { parseArgs as parseArgs6 } from "node:util";
16601
17573
  async function instructionsCommand(args, io) {
16602
- const { values } = parseArgs4({
17574
+ const { values } = parseArgs6({
16603
17575
  args,
16604
17576
  options: {
16605
17577
  audience: { type: "string", default: "llm" },
@@ -16629,9 +17601,9 @@ var docs_exports = {};
16629
17601
  __export(docs_exports, {
16630
17602
  docsCommand: () => docsCommand
16631
17603
  });
16632
- import { parseArgs as parseArgs5 } from "node:util";
17604
+ import { parseArgs as parseArgs7 } from "node:util";
16633
17605
  async function docsCommand(args, io) {
16634
- const { values } = parseArgs5({
17606
+ const { values } = parseArgs7({
16635
17607
  args,
16636
17608
  options: {
16637
17609
  registry: { type: "string" },
@@ -16652,23 +17624,27 @@ var init_docs = __esm({
16652
17624
  });
16653
17625
 
16654
17626
  // src/bin.ts
16655
- import { writeFile as writeFile2 } from "node:fs/promises";
17627
+ import { writeFile as writeFile3 } from "node:fs/promises";
16656
17628
 
16657
17629
  // src/run.ts
16658
- var USAGE = `Usage: contentbit <init|validate|render|instructions|docs> [options]
17630
+ var USAGE = `Usage: contentbit <init|validate|stats|render|instructions|docs|agents> [options]
16659
17631
 
16660
- init [-t react|html|markdown] [-y] [--no-install]
17632
+ init [-t react|html|markdown|astro] [--md ...] [-y] [--no-install] [--no-page] [--no-agents]
17633
+ agents [--claude] [--no-agents-md]
16661
17634
 
16662
17635
  validate <globs...> [--registry <module.mjs>] [--strict-warnings]
17636
+ stats <globs...> [--registry <module.mjs>] [--no-validate]
16663
17637
  render <file> --target html|markdown [--registry <module.mjs>] [--out <file>]
16664
17638
  instructions [--audience llm|human] [--no-examples] [--registry <module.mjs>] [--out <file>]
16665
17639
  docs [--registry <module.mjs>] [--out <file>]`;
16666
17640
  var commands = {
16667
17641
  init: async () => (await Promise.resolve().then(() => (init_init(), init_exports))).initCommand,
16668
17642
  validate: async () => (await Promise.resolve().then(() => (init_validate2(), validate_exports))).validateCommand,
17643
+ stats: async () => (await Promise.resolve().then(() => (init_stats(), stats_exports))).statsCommand,
16669
17644
  render: async () => (await Promise.resolve().then(() => (init_render2(), render_exports))).renderCommand,
16670
17645
  instructions: async () => (await Promise.resolve().then(() => (init_instructions(), instructions_exports))).instructionsCommand,
16671
- docs: async () => (await Promise.resolve().then(() => (init_docs(), docs_exports))).docsCommand
17646
+ docs: async () => (await Promise.resolve().then(() => (init_docs(), docs_exports))).docsCommand,
17647
+ agents: async () => (await Promise.resolve().then(() => (init_agents(), agents_exports))).agentsCommand
16672
17648
  };
16673
17649
  async function run(argv, io) {
16674
17650
  const [name, ...rest] = argv;
@@ -16690,6 +17666,6 @@ async function run(argv, io) {
16690
17666
  var code = await run(process.argv.slice(2), {
16691
17667
  stdout: (s) => console.log(s),
16692
17668
  stderr: (s) => console.error(s),
16693
- writeFile: (path, content) => writeFile2(path, content, "utf8")
17669
+ writeFile: (path, content) => writeFile3(path, content, "utf8")
16694
17670
  });
16695
17671
  process.exit(code);