contentbit 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ import type { Io } from '../run.js';
2
+ export interface AgentOptions {
3
+ /** Install Claude Code skills; defaults to detecting a .claude/ directory. */
4
+ claude?: boolean;
5
+ /** Manage the AGENTS.md block; defaults to true. */
6
+ agentsMd?: boolean;
7
+ }
8
+ /** Install or refresh the agent integration. Shared by `agents` and `init`. */
9
+ export declare function installAgentIntegration(cwd: string, options: AgentOptions, io: Io): Promise<void>;
10
+ export declare function agentsCommand(args: string[], io: Io): Promise<number>;
11
+ //# sourceMappingURL=agents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/commands/agents.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AA2JnC,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,+EAA+E;AAC/E,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,YAAY,EACrB,EAAE,EAAE,EAAE,GACL,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAkB3E"}
@@ -0,0 +1,197 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { parseArgs } from 'node:util';
5
+ // Everything here is static and project-independent by design: skills fetch
6
+ // live data (authoring guide, stats, diagnostics) by running the CLI, so the
7
+ // registry stays the single source of truth and nothing can drift. Bump the
8
+ // frontmatter version when a template changes; `contentbit agents` re-runs
9
+ // overwrite in place.
10
+ const TEMPLATE_VERSION = 1;
11
+ const AUTHOR_SKILL = `---
12
+ name: contentbit-author
13
+ description: |
14
+ Write or edit contentbit Markdown content (directive blocks like :::callout).
15
+ Use when asked to create or modify content documents in a project that uses
16
+ contentbit — blog posts, docs pages, changelogs, any Markdown covered by
17
+ \`contentbit validate\`.
18
+ version: ${TEMPLATE_VERSION}
19
+ ---
20
+
21
+ # Writing contentbit content
22
+
23
+ contentbit documents are plain Markdown plus directive blocks
24
+ (\`:::name{props} ... :::\`). Every block has a schema. Never guess block names,
25
+ props, or body shapes — fetch the live guide from the project's registry first.
26
+
27
+ ## Find the project conventions
28
+
29
+ Check \`package.json\` for a \`content:check\` script. It holds the canonical
30
+ validate invocation for this project: the content glob and, if present, the
31
+ \`--registry <path>\` flag pointing at custom block definitions. Reuse both
32
+ below. No script? Default to \`content/**/*.md\` with no \`--registry\` flag.
33
+
34
+ ## The loop
35
+
36
+ 1. **Fetch the authoring guide** (always — it covers this project's custom blocks):
37
+
38
+ \`\`\`sh
39
+ contentbit instructions --audience llm [--registry <path from content:check>]
40
+ \`\`\`
41
+
42
+ Read it before writing. It documents every available block: props, body
43
+ shape, and when to use or avoid it.
44
+
45
+ 2. **Write the document.** Plain Markdown everywhere; blocks only where the
46
+ guide's use-when guidance fits. Keep frontmatter consistent with sibling
47
+ documents in the same folder.
48
+
49
+ 3. **Validate and fix until clean:**
50
+
51
+ \`\`\`sh
52
+ contentbit validate <file> [--registry <path>]
53
+ \`\`\`
54
+
55
+ Diagnostics print to stderr as \`file:line:col severity CODE message\`, often
56
+ with a \`hint:\` line suggesting the fix. Exit 0 means clean; exit 1 means
57
+ errors remain. Fix every diagnostic and re-run. Never finish with a failing
58
+ validate.
59
+
60
+ ## Failure modes
61
+
62
+ - \`contentbit\` not found or no registry resolvable: the project is not set up.
63
+ Say so and suggest \`npx contentbit@latest init\` — do not invent block syntax.
64
+ - A block you want does not exist: use plain Markdown, or ask whether to define
65
+ a custom block in the registry. Never emit an unregistered block name.
66
+ `;
67
+ const AUDIT_SKILL = `---
68
+ name: contentbit-audit
69
+ description: |
70
+ Audit contentbit Markdown content health using document stats. Use when asked
71
+ to audit, review, or find improvements across content — thin pages, missing
72
+ structure, validation issues — in a project that uses contentbit.
73
+ version: ${TEMPLATE_VERSION}
74
+ ---
75
+
76
+ # Auditing contentbit content
77
+
78
+ \`contentbit stats\` analyzes documents and prints JSON to stdout. It is a read
79
+ tool: it always exits 0, even when documents have validation errors.
80
+
81
+ ## Gather
82
+
83
+ Check \`package.json\` for the \`content:check\` script to find this project's
84
+ content glob and \`--registry\` flag, then:
85
+
86
+ \`\`\`sh
87
+ contentbit stats "content/**/*.md" [--registry <path>]
88
+ \`\`\`
89
+
90
+ One matched file prints a single stats object; multiple files print an array.
91
+ Each entry includes the file path, frontmatter data, a heading \`outline\` with
92
+ per-section word counts, \`blocks.byName\` usage counts, \`links.domains\`, and
93
+ a \`validation\` summary (\`errors\`/\`warnings\`).
94
+
95
+ ## Interpret
96
+
97
+ Prioritize findings in this order:
98
+
99
+ 1. **Validation errors and warnings** — broken content ships broken pages.
100
+ 2. **Thin documents** — outline sections with very low word counts.
101
+ 3. **Block-less documents** — \`blocks.byName\` empty where sibling documents
102
+ use blocks; structure (steps, callouts, comparisons, faq) may be missing.
103
+ 4. **Missing or inconsistent frontmatter** compared to sibling documents.
104
+ 5. **Structural imbalance** — skipped heading levels, single-section walls of text.
105
+
106
+ ## Report
107
+
108
+ Report findings per file with concrete suggestions, ordered by priority. Do not
109
+ edit files during the audit. To fix a finding, follow the contentbit-author
110
+ skill (fetch the guide, edit, validate until clean) — offer that as a follow-up.
111
+ `;
112
+ const AGENTS_MD_BLOCK = `<!-- contentbit:start -->
113
+
114
+ ## contentbit content (generated — edits inside this block are overwritten)
115
+
116
+ This project validates Markdown content with contentbit. Documents are plain
117
+ Markdown plus directive blocks (\`:::name{props} ... :::\`), each with a schema.
118
+ The \`content:check\` script in package.json holds the canonical validate
119
+ command — the content glob and the \`--registry\` flag — reuse its arguments.
120
+
121
+ When writing or editing content:
122
+
123
+ 1. Fetch the live authoring guide first — never guess block syntax:
124
+ \`contentbit instructions --audience llm [--registry <path>]\`
125
+ 2. Write plain Markdown; use blocks where the guide's use-when guidance fits.
126
+ 3. Validate until clean (exit 0): \`contentbit validate <file> [--registry <path>]\`.
127
+ Diagnostics print as \`file:line:col severity CODE message\` with fix hints.
128
+
129
+ When auditing content health:
130
+
131
+ - \`contentbit stats "content/**/*.md" [--registry <path>]\` prints JSON stats
132
+ and always exits 0: outline word counts, block usage, link domains, and
133
+ validation error/warning counts. Flag validation issues, thin documents, and
134
+ block-less pages first.
135
+
136
+ If \`contentbit\` is unavailable, suggest \`npx contentbit@latest init\` instead
137
+ of inventing block syntax.
138
+
139
+ <!-- contentbit:end -->`;
140
+ const START = '<!-- contentbit:start -->';
141
+ const END = '<!-- contentbit:end -->';
142
+ /** Insert or replace the fenced contentbit block, leaving the rest untouched. */
143
+ function upsertBlock(existing) {
144
+ const start = existing.indexOf(START);
145
+ const end = existing.indexOf(END);
146
+ if (start !== -1 && end !== -1) {
147
+ return existing.slice(0, start) + AGENTS_MD_BLOCK + existing.slice(end + END.length);
148
+ }
149
+ if (existing.trim() === '')
150
+ return `${AGENTS_MD_BLOCK}\n`;
151
+ return `${existing.replace(/\n*$/, '\n\n')}${AGENTS_MD_BLOCK}\n`;
152
+ }
153
+ /** Install or refresh the agent integration. Shared by `agents` and `init`. */
154
+ export async function installAgentIntegration(cwd, options, io) {
155
+ const claude = options.claude ?? existsSync(join(cwd, '.claude'));
156
+ const agentsMd = options.agentsMd ?? true;
157
+ if (agentsMd) {
158
+ const path = join(cwd, 'AGENTS.md');
159
+ let existing = '';
160
+ try {
161
+ existing = await readFile(path, 'utf8');
162
+ }
163
+ catch {
164
+ /* not there yet */
165
+ }
166
+ const created = existing === '';
167
+ await writeFile(path, upsertBlock(existing), 'utf8');
168
+ io.stdout(`${created ? 'created' : 'updated'}: AGENTS.md (contentbit block)`);
169
+ }
170
+ if (claude) {
171
+ const skills = [
172
+ ['contentbit-author', AUTHOR_SKILL],
173
+ ['contentbit-audit', AUDIT_SKILL],
174
+ ];
175
+ for (const [name, content] of skills) {
176
+ const dir = join(cwd, '.claude/skills', name);
177
+ await mkdir(dir, { recursive: true });
178
+ await writeFile(join(dir, 'SKILL.md'), content, 'utf8');
179
+ io.stdout(`installed: .claude/skills/${name}/SKILL.md`);
180
+ }
181
+ }
182
+ }
183
+ export async function agentsCommand(args, io) {
184
+ const { values } = parseArgs({
185
+ args,
186
+ options: {
187
+ claude: { type: 'boolean', default: false },
188
+ 'no-agents-md': { type: 'boolean', default: false },
189
+ cwd: { type: 'string', default: process.cwd() },
190
+ },
191
+ });
192
+ await installAgentIntegration(values.cwd, {
193
+ claude: values.claude || undefined, // false means "detect", not "skip"
194
+ agentsMd: !values['no-agents-md'],
195
+ }, io);
196
+ return 0;
197
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAiSnC,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAyMzE"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAgYnC,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAyNzE"}
@@ -4,12 +4,15 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
4
  import { join } from 'node:path';
5
5
  import { parseArgs } from 'node:util';
6
6
  import { loadRegistry } from '../load-registry.js';
7
- const TARGETS = ['react', 'html', 'markdown'];
7
+ import { installAgentIntegration } from './agents.js';
8
+ const TARGETS = ['react', 'html', 'markdown', 'astro'];
8
9
  /** Markdown library choices per target; the first entry is the default. */
9
10
  const MD_CHOICES = {
10
11
  react: ['react-markdown', 'none'],
11
12
  html: ['marked', 'markdown-it', 'none'],
12
13
  markdown: ['none'],
14
+ // @contentbit/astro ships its own marked-based default; nothing to install.
15
+ astro: ['none'],
13
16
  };
14
17
  const REGISTRY_TEMPLATE = `// Custom block definitions for this project. The CLI and your app share
15
18
  // this module — Node 22.18+ imports TypeScript directly:
@@ -213,6 +216,67 @@ export default async function ExamplePage() {
213
216
  )
214
217
  }
215
218
  `;
219
+ const ASTRO_CONTENT_CONFIG = `import { defineCollection } from 'astro:content'
220
+ import { glob } from 'astro/loaders'
221
+
222
+ export const collections = {
223
+ articles: defineCollection({
224
+ // Astro's builtin Markdown loader. Entry bodies are parsed and validated
225
+ // where they render (see src/pages/example.astro); \`contentbit validate\`
226
+ // covers the same files in CI.
227
+ loader: glob({ pattern: '**/*.md', base: './content' }),
228
+ }),
229
+ }
230
+ `;
231
+ const ASTRO_QUOTE_BLOCK = `---
232
+ // The Astro component for the custom \`quote\` block defined in blocks/registry.ts.
233
+ // Block props arrive as component props; nested content arrives via <slot />.
234
+ interface Props {
235
+ author: string
236
+ role?: string
237
+ }
238
+
239
+ const { author, role } = Astro.props
240
+ ---
241
+
242
+ <figure style="margin: 1.5rem 0; border-left: 2px solid #d4d4d4; padding-left: 1rem;">
243
+ <blockquote style="font-style: italic;"><slot /></blockquote>
244
+ <figcaption style="margin-top: 0.5rem; font-size: 0.875rem; opacity: 0.7;">
245
+ — {author}{role ? \`, \${role}\` : null}
246
+ </figcaption>
247
+ </figure>
248
+ `;
249
+ /** The example page: styled pack renderer or the headless ContentBlocks. */
250
+ function astroPage(styled) {
251
+ const importLine = styled
252
+ ? "import ContentRenderer from '../components/content-blocks/content-renderer.astro'"
253
+ : "import { ContentBlocks } from '@contentbit/astro/components'";
254
+ const renderer = styled ? 'ContentRenderer' : 'ContentBlocks';
255
+ return `---
256
+ import { genericBlocks } from '@contentbit/blocks'
257
+ import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
258
+ import { getEntry } from 'astro:content'
259
+
260
+ ${importLine}
261
+
262
+ // Definitions in blocks/registry.ts are shared with the validate CLI.
263
+ import customBlocks from '../../blocks/registry'
264
+ import QuoteBlock from '../../blocks/QuoteBlock.astro'
265
+
266
+ // Entry ids are the file path relative to the collection base, minus ".md".
267
+ const entry = await getEntry('articles', 'example')
268
+ if (!entry?.body) throw new Error('Entry "example" not found in the articles collection.')
269
+
270
+ const registry = createBlockRegistry().use(genericBlocks()).use(customBlocks)
271
+ // Static pages render at build time, so invalid blocks fail the build here.
272
+ const result = validateDocument(parseDocument(entry.body), registry)
273
+ ---
274
+
275
+ <main style="max-width: 42rem; margin: 0 auto; padding: 3rem 1.5rem;">
276
+ <${renderer} document={result.document} components={{ quote: QuoteBlock }} />
277
+ </main>
278
+ `;
279
+ }
216
280
  function detectPackageManager(cwd) {
217
281
  // The project's lockfile outranks however the CLI itself was launched.
218
282
  const locks = [
@@ -253,6 +317,27 @@ function runInstall(pm, args, cwd) {
253
317
  child.on('error', () => resolve(1));
254
318
  });
255
319
  }
320
+ /** Wire the @contentbit shadcn registry and install a styled pack. Returns true on success. */
321
+ async function installStyledPack(cwd, pack, noInstall, io) {
322
+ const componentsJsonPath = join(cwd, 'components.json');
323
+ const componentsJson = JSON.parse(await readFile(componentsJsonPath, 'utf8'));
324
+ componentsJson.registries ??= {};
325
+ if (!componentsJson.registries['@contentbit']) {
326
+ componentsJson.registries['@contentbit'] = 'https://contentbit.dev/r/{name}.json';
327
+ await writeFile(componentsJsonPath, `${JSON.stringify(componentsJson, null, 2)}\n`, 'utf8');
328
+ io.stdout('added @contentbit registry to components.json');
329
+ }
330
+ if (noInstall) {
331
+ io.stdout(`skipped: shadcn add ${pack}`);
332
+ return true;
333
+ }
334
+ const [bin, prefix] = dlxCommand(detectPackageManager(cwd));
335
+ io.stdout(`installing the styled pack: shadcn add ${pack}`);
336
+ const code = await runInstall(bin, [...prefix, 'shadcn@latest', 'add', pack, '--yes'], cwd);
337
+ if (code !== 0)
338
+ io.stderr('styled pack install failed; falling back to headless defaults');
339
+ return code === 0;
340
+ }
256
341
  /** Write a file unless it already exists; returns what happened for the summary. */
257
342
  async function scaffold(path, content) {
258
343
  try {
@@ -276,6 +361,7 @@ export async function initCommand(args, io) {
276
361
  'no-install': { type: 'boolean', default: false },
277
362
  'no-page': { type: 'boolean', default: false },
278
363
  'no-styled': { type: 'boolean', default: false },
364
+ 'no-agents': { type: 'boolean', default: false },
279
365
  },
280
366
  });
281
367
  const cwd = values.cwd;
@@ -291,7 +377,8 @@ export async function initCommand(args, io) {
291
377
  }
292
378
  // Resolve the render target: flag > prompt (interactive) > detection.
293
379
  const hasReact = Boolean(pkg.dependencies?.react ?? pkg.devDependencies?.react);
294
- const detected = hasReact ? 'react' : 'html';
380
+ const hasAstro = Boolean(pkg.dependencies?.astro ?? pkg.devDependencies?.astro);
381
+ const detected = hasAstro ? 'astro' : hasReact ? 'react' : 'html';
295
382
  let target;
296
383
  if (values.target) {
297
384
  if (!TARGETS.includes(values.target)) {
@@ -307,6 +394,7 @@ export async function initCommand(args, io) {
307
394
  initialValue: detected,
308
395
  options: [
309
396
  { value: 'react', label: 'React', hint: 'ContentBlocks component' },
397
+ { value: 'astro', label: 'Astro', hint: 'content collections + .astro components' },
310
398
  { value: 'html', label: 'Static HTML', hint: 'renderToHtml, no framework' },
311
399
  { value: 'markdown', label: 'Plain Markdown', hint: 'fallback rendering only' },
312
400
  ],
@@ -353,6 +441,8 @@ export async function initCommand(args, io) {
353
441
  runtime.push('@contentbit/react');
354
442
  if (target === 'html')
355
443
  runtime.push('@contentbit/html');
444
+ if (target === 'astro')
445
+ runtime.push('@contentbit/astro');
356
446
  if (md !== 'none')
357
447
  runtime.push(md);
358
448
  if (values['no-install']) {
@@ -380,26 +470,7 @@ export async function initCommand(args, io) {
380
470
  let styled = false;
381
471
  const componentsJsonPath = join(cwd, 'components.json');
382
472
  if (target === 'react' && !values['no-styled'] && existsSync(componentsJsonPath)) {
383
- const componentsJson = JSON.parse(await readFile(componentsJsonPath, 'utf8'));
384
- componentsJson.registries ??= {};
385
- if (!componentsJson.registries['@contentbit']) {
386
- componentsJson.registries['@contentbit'] = 'https://contentbit.dev/r/{name}.json';
387
- await writeFile(componentsJsonPath, `${JSON.stringify(componentsJson, null, 2)}\n`, 'utf8');
388
- io.stdout('added @contentbit registry to components.json');
389
- }
390
- if (values['no-install']) {
391
- io.stdout('skipped: shadcn add @contentbit/generic-pack');
392
- styled = true;
393
- }
394
- else {
395
- const [bin, prefix] = dlxCommand(detectPackageManager(cwd));
396
- io.stdout('installing the styled pack: shadcn add @contentbit/generic-pack');
397
- const code = await runInstall(bin, [...prefix, 'shadcn@latest', 'add', '@contentbit/generic-pack', '--yes'], cwd);
398
- if (code === 0)
399
- styled = true;
400
- else
401
- io.stderr('styled pack install failed; falling back to headless defaults');
402
- }
473
+ styled = await installStyledPack(cwd, '@contentbit/generic-pack', values['no-install'], io);
403
474
  }
404
475
  if (target === 'react') {
405
476
  const depth = layout.componentPath.split('/').length - 1;
@@ -420,6 +491,31 @@ export async function initCommand(args, io) {
420
491
  htmlRenderScript(md),
421
492
  ]);
422
493
  }
494
+ if (target === 'astro') {
495
+ let astroStyled = false;
496
+ if (!values['no-styled'] && existsSync(componentsJsonPath)) {
497
+ astroStyled = await installStyledPack(cwd, '@contentbit/astro-pack', values['no-install'], io);
498
+ }
499
+ files.push(['blocks/QuoteBlock.astro', ASTRO_QUOTE_BLOCK]);
500
+ // Every config filename Astro resolves (src/content.config.* plus the
501
+ // legacy src/content/config.* location), so we never scaffold a second
502
+ // config that Astro would silently ignore.
503
+ const configCandidates = ['ts', 'mts', 'mjs', 'js'].flatMap((ext) => [
504
+ `src/content.config.${ext}`,
505
+ `src/content/config.${ext}`,
506
+ ]);
507
+ const existingConfig = configCandidates.find((p) => existsSync(join(cwd, p)));
508
+ if (existingConfig) {
509
+ io.stdout(`content config exists (${existingConfig}); add this collection manually:`);
510
+ io.stdout(ASTRO_CONTENT_CONFIG);
511
+ io.stdout('the example page expects the "articles" collection above');
512
+ }
513
+ else {
514
+ files.push(['src/content.config.ts', ASTRO_CONTENT_CONFIG]);
515
+ }
516
+ if (!values['no-page'])
517
+ files.push(['src/pages/example.astro', astroPage(astroStyled)]);
518
+ }
423
519
  for (const [rel, content] of files) {
424
520
  const result = await scaffold(join(cwd, rel), content);
425
521
  io.stdout(`${result}: ${rel}`);
@@ -445,6 +541,13 @@ export async function initCommand(args, io) {
445
541
  const guide = registry.toAuthoringGuide({ audience: 'llm', includeExamples: true });
446
542
  await writeFile(join(cwd, 'contentbit-guide.md'), guide, 'utf8');
447
543
  io.stdout('created: contentbit-guide.md (LLM authoring instructions)');
544
+ // Coding-agent integration: an AGENTS.md block for every agent, plus Claude
545
+ // Code skills when a .claude/ directory exists. `contentbit agents` refreshes.
546
+ if (!values['no-agents']) {
547
+ await installAgentIntegration(cwd, {}, io);
548
+ io.stdout('Agent integration installed — try asking your agent:');
549
+ io.stdout(' "write a blog post about X" or "audit my content"');
550
+ }
448
551
  io.stdout('');
449
552
  io.stdout('Done. Next steps:');
450
553
  io.stdout(` 1. Validate the starter content: ${detectPackageManager(cwd)} run content:check`);
@@ -458,6 +561,10 @@ export async function initCommand(args, io) {
458
561
  }
459
562
  io.stdout(' 3. Styled components: pnpm dlx shadcn@latest add @contentbit/generic-pack');
460
563
  }
564
+ else if (target === 'astro') {
565
+ io.stdout(' 2. Start the dev server and open /example to see the article rendered.');
566
+ io.stdout(' 3. Styled components: pnpm dlx shadcn@latest add @contentbit/astro-pack');
567
+ }
461
568
  else if (target === 'html') {
462
569
  io.stdout(' 2. Render it: node scripts/render-example.mjs && open example.html');
463
570
  }
@@ -0,0 +1,3 @@
1
+ import type { Io } from '../run.js';
2
+ export declare function statsCommand(args: string[], io: Io): Promise<number>;
3
+ //# sourceMappingURL=stats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../../src/commands/stats.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAuBnC,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA2B1E"}
@@ -0,0 +1,49 @@
1
+ import { analyzeDocument, parseDocument, stripFrontmatter, validateDocument, } from '@contentbit/core';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { parseArgs } from 'node:util';
4
+ import { glob } from 'tinyglobby';
5
+ import { loadRegistry } from '../load-registry.js';
6
+ async function fileStats(file, registry) {
7
+ const source = await readFile(file, 'utf8');
8
+ const stats = analyzeDocument(source, { path: file });
9
+ if (registry) {
10
+ const result = validateDocument(parseDocument(stripFrontmatter(source)), registry);
11
+ let errors = 0;
12
+ let warnings = 0;
13
+ for (const d of result.diagnostics) {
14
+ if (d.severity === 'error')
15
+ errors++;
16
+ else if (d.severity === 'warning')
17
+ warnings++;
18
+ }
19
+ stats.validation = { errors, warnings };
20
+ }
21
+ return stats;
22
+ }
23
+ export async function statsCommand(args, io) {
24
+ const { values, positionals } = parseArgs({
25
+ args,
26
+ allowPositionals: true,
27
+ options: {
28
+ registry: { type: 'string' },
29
+ 'no-validate': { type: 'boolean', default: false },
30
+ },
31
+ });
32
+ if (positionals.length === 0) {
33
+ io.stderr('stats: provide at least one file or glob.');
34
+ return 2;
35
+ }
36
+ const files = await glob(positionals, { absolute: true });
37
+ if (files.length === 0) {
38
+ io.stderr(`stats: no files matched ${positionals.join(' ')}`);
39
+ return 2;
40
+ }
41
+ const registry = values['no-validate'] ? null : await loadRegistry(values.registry);
42
+ const all = [];
43
+ for (const file of files.sort()) {
44
+ all.push(await fileStats(file, registry));
45
+ }
46
+ // A single file keeps the flat object shape; multiple files emit an array.
47
+ io.stdout(JSON.stringify(all.length === 1 ? all[0] : all, null, 2));
48
+ return 0;
49
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAInC,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAmC7E"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAInC,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAsC7E"}
@@ -1,4 +1,4 @@
1
- import { formatDiagnostic, parseDocument, validateDocument } from '@contentbit/core';
1
+ import { formatDiagnostic, parseDocument, stripFrontmatter, validateDocument, } from '@contentbit/core';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { parseArgs } from 'node:util';
4
4
  import { glob } from 'tinyglobby';
@@ -26,7 +26,10 @@ export async function validateCommand(args, io) {
26
26
  let warnings = 0;
27
27
  for (const file of files.sort()) {
28
28
  const source = await readFile(file, 'utf8');
29
- const result = validateDocument(parseDocument(source), registry);
29
+ // Frontmatter is metadata, not content: blanked (positions preserved) so
30
+ // block syntax inside YAML never produces diagnostics — matching what
31
+ // frontmatter-aware consumers like Astro validate from entry bodies.
32
+ const result = validateDocument(parseDocument(stripFrontmatter(source)), registry);
30
33
  for (const d of result.diagnostics) {
31
34
  io.stderr(formatDiagnostic(d, file));
32
35
  if (d.severity === 'error')
package/dist/run.d.ts CHANGED
@@ -3,6 +3,6 @@ export interface Io {
3
3
  stderr(line: string): void;
4
4
  writeFile(path: string, content: string): Promise<void>;
5
5
  }
6
- export declare const USAGE = "Usage: contentbit <init|validate|render|instructions|docs> [options]\n\n init [-t react|html|markdown] [--md ...] [-y] [--no-install] [--no-page]\n\n validate <globs...> [--registry <module.mjs>] [--strict-warnings]\n render <file> --target html|markdown [--registry <module.mjs>] [--out <file>]\n instructions [--audience llm|human] [--no-examples] [--registry <module.mjs>] [--out <file>]\n docs [--registry <module.mjs>] [--out <file>]";
6
+ export declare const USAGE = "Usage: contentbit <init|validate|stats|render|instructions|docs|agents> [options]\n\n init [-t react|html|markdown|astro] [--md ...] [-y] [--no-install] [--no-page] [--no-agents]\n agents [--claude] [--no-agents-md]\n\n validate <globs...> [--registry <module.mjs>] [--strict-warnings]\n stats <globs...> [--registry <module.mjs>] [--no-validate]\n render <file> --target html|markdown [--registry <module.mjs>] [--out <file>]\n instructions [--audience llm|human] [--no-examples] [--registry <module.mjs>] [--out <file>]\n docs [--registry <module.mjs>] [--out <file>]";
7
7
  export declare function run(argv: string[], io: Io): Promise<number>;
8
8
  //# sourceMappingURL=run.d.ts.map
package/dist/run.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,EAAE;IACjB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACxD;AAED,eAAO,MAAM,KAAK,gcAO8B,CAAA;AAYhD,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAcjE"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,EAAE;IACjB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACxD;AAED,eAAO,MAAM,KAAK,qkBAS8B,CAAA;AAchD,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAcjE"}