contentbit 0.1.1 → 0.3.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;AAyMnC,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,243 @@
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 = 2;
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
+ If the project has a \`content:links\` script, use it for the internal-link
34
+ index; otherwise run \`contentbit links <content glob>\` directly.
35
+
36
+ ## The loop
37
+
38
+ 1. **Fetch the authoring guide** (always — it covers this project's custom blocks):
39
+
40
+ \`\`\`sh
41
+ contentbit instructions --audience llm [--registry <path from content:check>]
42
+ \`\`\`
43
+
44
+ Read it before writing. It documents every available block: props, body
45
+ shape, and when to use or avoid it.
46
+
47
+ 2. **Write the document.** Plain Markdown everywhere; blocks only where the
48
+ guide's use-when guidance fits. Keep frontmatter consistent with sibling
49
+ documents in the same folder. If sibling documents use \`slug\`, \`linksTo\`,
50
+ \`aliases\`, or \`keywords\`, run the link index first:
51
+
52
+ \`\`\`sh
53
+ contentbit links <content glob>
54
+ \`\`\`
55
+
56
+ Read \`.contentbit/link-index.json\` to pick existing slugs and related
57
+ pages. Author only \`slug\`, \`linksTo\`, \`aliases\`, and \`keywords\` in
58
+ frontmatter; never write derived \`linkedFrom\` into source files. When
59
+ creating a linked page, include \`keywords.primary\` and
60
+ \`keywords.secondary\` with search-intent phrases that would help future
61
+ agents choose this page as a \`linksTo\` target.
62
+
63
+ 3. **Validate and fix until clean:**
64
+
65
+ \`\`\`sh
66
+ contentbit validate <file> [--registry <path>]
67
+ \`\`\`
68
+
69
+ Diagnostics print to stderr as \`file:line:col severity CODE message\`, often
70
+ with a \`hint:\` line suggesting the fix. Exit 0 means clean; exit 1 means
71
+ errors remain. If the document has link frontmatter, validate the full
72
+ content glob so cross-file links are checked against the whole graph. Fix
73
+ every diagnostic and re-run. Never finish with a failing validate.
74
+
75
+ 4. **Refresh internal links when present:**
76
+
77
+ \`\`\`sh
78
+ contentbit links <content glob> --fix
79
+ \`\`\`
80
+
81
+ \`--fix\` only rewrites \`linksTo\` values that point at known aliases. It
82
+ does not invent links, remove aliases, or write backlinks. Re-run validate
83
+ after it changes files.
84
+
85
+ ## Failure modes
86
+
87
+ - \`contentbit\` not found or no registry resolvable: the project is not set up.
88
+ Say so and suggest \`npx contentbit@latest init\` — do not invent block syntax.
89
+ - A block you want does not exist: use plain Markdown, or ask whether to define
90
+ a custom block in the registry. Never emit an unregistered block name.
91
+ `;
92
+ const AUDIT_SKILL = `---
93
+ name: contentbit-audit
94
+ description: |
95
+ Audit contentbit Markdown content health using document stats. Use when asked
96
+ to audit, review, or find improvements across content — thin pages, missing
97
+ structure, validation issues — in a project that uses contentbit.
98
+ version: ${TEMPLATE_VERSION}
99
+ ---
100
+
101
+ # Auditing contentbit content
102
+
103
+ \`contentbit stats\` analyzes documents and prints JSON to stdout. It is a read
104
+ tool: it always exits 0, even when documents have validation errors.
105
+ \`contentbit links\` builds the frontmatter-authored internal-link graph and
106
+ prints link diagnostics.
107
+
108
+ ## Gather
109
+
110
+ Check \`package.json\` for the \`content:check\` script to find this project's
111
+ content glob and \`--registry\` flag, then:
112
+
113
+ \`\`\`sh
114
+ contentbit stats "content/**/*.md" [--registry <path>]
115
+ contentbit links "content/**/*.md"
116
+ \`\`\`
117
+
118
+ One matched file prints a single stats object; multiple files print an array.
119
+ Each entry includes the file path, frontmatter data, a heading \`outline\` with
120
+ per-section word counts, \`blocks.byName\` usage counts, \`links.domains\`, and
121
+ a \`validation\` summary (\`errors\`/\`warnings\`).
122
+ \`contentbit links\` also writes \`.contentbit/link-index.json\`, whose pages
123
+ contain \`slug\`, resolved \`linksTo\`, derived \`linkedFrom\`, \`aliases\`, and
124
+ \`keywords\`.
125
+
126
+ ## Interpret
127
+
128
+ Prioritize findings in this order:
129
+
130
+ 1. **Validation errors and warnings** — broken content ships broken pages.
131
+ 2. **Internal-link errors** — unresolved links, duplicate slugs, and alias
132
+ conflicts from \`contentbit links\`.
133
+ 3. **Orphans and self-links** — link warnings that point to isolated or noisy
134
+ pages.
135
+ 4. **Thin documents** — outline sections with very low word counts.
136
+ 5. **Block-less documents** — \`blocks.byName\` empty where sibling documents
137
+ use blocks; structure (steps, callouts, comparisons, faq) may be missing.
138
+ 6. **Missing or inconsistent frontmatter** compared to sibling documents.
139
+ 7. **Structural imbalance** — skipped heading levels, single-section walls of text.
140
+
141
+ ## Report
142
+
143
+ Report findings per file with concrete suggestions, ordered by priority. Do not
144
+ edit files during the audit. To fix a finding, follow the contentbit-author
145
+ skill (fetch the guide, edit, validate until clean) — offer that as a follow-up.
146
+ `;
147
+ const AGENTS_MD_BLOCK = `<!-- contentbit:start -->
148
+
149
+ ## contentbit content (generated — edits inside this block are overwritten)
150
+
151
+ This project validates Markdown content with contentbit. Documents are plain
152
+ Markdown plus directive blocks (\`:::name{props} ... :::\`), each with a schema.
153
+ The \`content:check\` script in package.json holds the canonical validate
154
+ command — the content glob and the \`--registry\` flag — reuse its arguments.
155
+ If the project has a \`content:links\` script, use it to build the internal-link
156
+ index; otherwise run \`contentbit links <content glob>\`.
157
+
158
+ When writing or editing content:
159
+
160
+ 1. Fetch the live authoring guide first — never guess block syntax:
161
+ \`contentbit instructions --audience llm [--registry <path>]\`
162
+ 2. Write plain Markdown; use blocks where the guide's use-when guidance fits.
163
+ 3. If sibling documents use \`slug\` / \`linksTo\`, read
164
+ \`.contentbit/link-index.json\` from \`contentbit links <content glob>\` and
165
+ author frontmatter links with existing slugs. When creating a linked page,
166
+ include \`keywords.primary\` and \`keywords.secondary\` with search-intent
167
+ phrases future agents can use to choose related pages.
168
+ 4. Validate until clean (exit 0): \`contentbit validate <file> [--registry <path>]\`.
169
+ Diagnostics print as \`file:line:col severity CODE message\` with fix hints.
170
+ For link frontmatter, validate the full content glob so cross-file checks run.
171
+
172
+ When auditing content health:
173
+
174
+ - \`contentbit stats "content/**/*.md" [--registry <path>]\` prints JSON stats
175
+ and always exits 0: outline word counts, block usage, link domains, and
176
+ validation error/warning counts. Flag validation issues, thin documents, and
177
+ block-less pages first.
178
+ - \`contentbit links "content/**/*.md" [--fix]\` builds
179
+ \`.contentbit/link-index.json\`, reports dangling links/orphans, and rewrites
180
+ alias references in \`linksTo\` when \`--fix\` is used.
181
+
182
+ If \`contentbit\` is unavailable, suggest \`npx contentbit@latest init\` instead
183
+ of inventing block syntax.
184
+
185
+ <!-- contentbit:end -->`;
186
+ const START = '<!-- contentbit:start -->';
187
+ const END = '<!-- contentbit:end -->';
188
+ /** Insert or replace the fenced contentbit block, leaving the rest untouched. */
189
+ function upsertBlock(existing) {
190
+ const start = existing.indexOf(START);
191
+ const end = existing.indexOf(END);
192
+ if (start !== -1 && end !== -1) {
193
+ return existing.slice(0, start) + AGENTS_MD_BLOCK + existing.slice(end + END.length);
194
+ }
195
+ if (existing.trim() === '')
196
+ return `${AGENTS_MD_BLOCK}\n`;
197
+ return `${existing.replace(/\n*$/, '\n\n')}${AGENTS_MD_BLOCK}\n`;
198
+ }
199
+ /** Install or refresh the agent integration. Shared by `agents` and `init`. */
200
+ export async function installAgentIntegration(cwd, options, io) {
201
+ const claude = options.claude ?? existsSync(join(cwd, '.claude'));
202
+ const agentsMd = options.agentsMd ?? true;
203
+ if (agentsMd) {
204
+ const path = join(cwd, 'AGENTS.md');
205
+ let existing = '';
206
+ try {
207
+ existing = await readFile(path, 'utf8');
208
+ }
209
+ catch {
210
+ /* not there yet */
211
+ }
212
+ const created = existing === '';
213
+ await writeFile(path, upsertBlock(existing), 'utf8');
214
+ io.stdout(`${created ? 'created' : 'updated'}: AGENTS.md (contentbit block)`);
215
+ }
216
+ if (claude) {
217
+ const skills = [
218
+ ['contentbit-author', AUTHOR_SKILL],
219
+ ['contentbit-audit', AUDIT_SKILL],
220
+ ];
221
+ for (const [name, content] of skills) {
222
+ const dir = join(cwd, '.claude/skills', name);
223
+ await mkdir(dir, { recursive: true });
224
+ await writeFile(join(dir, 'SKILL.md'), content, 'utf8');
225
+ io.stdout(`installed: .claude/skills/${name}/SKILL.md`);
226
+ }
227
+ }
228
+ }
229
+ export async function agentsCommand(args, io) {
230
+ const { values } = parseArgs({
231
+ args,
232
+ options: {
233
+ claude: { type: 'boolean', default: false },
234
+ 'no-agents-md': { type: 'boolean', default: false },
235
+ cwd: { type: 'string', default: process.cwd() },
236
+ },
237
+ });
238
+ await installAgentIntegration(values.cwd, {
239
+ claude: values.claude || undefined, // false means "detect", not "skip"
240
+ agentsMd: !values['no-agents-md'],
241
+ }, io);
242
+ return 0;
243
+ }
@@ -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;AAianC,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAqOzE"}
@@ -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:
@@ -75,7 +78,18 @@ export const blockComponents: Record<string, BlockComponent> = {
75
78
  }
76
79
  `;
77
80
  }
78
- const EXAMPLE_CONTENT = `# Hello, Content Blocks
81
+ const EXAMPLE_CONTENT = `---
82
+ slug: hello-content-blocks
83
+ linksTo:
84
+ - related-contentbit-workflows
85
+ aliases:
86
+ - getting-started-contentbit
87
+ keywords:
88
+ primary: validated Markdown blocks
89
+ secondary: [content workflow, agent writing]
90
+ ---
91
+
92
+ # Hello, Content Blocks
79
93
 
80
94
  Regular Markdown works everywhere. Blocks add validated structure:
81
95
 
@@ -97,6 +111,27 @@ The Analytical Engine weaves algebraic patterns just as the Jacquard loom
97
111
  weaves flowers and leaves.
98
112
  :::
99
113
  `;
114
+ const RELATED_CONTENT = `---
115
+ slug: related-contentbit-workflows
116
+ linksTo:
117
+ - hello-content-blocks
118
+ keywords:
119
+ primary: contentbit workflow
120
+ secondary: [validation loop, internal links]
121
+ ---
122
+
123
+ # Related contentbit workflows
124
+
125
+ This supporting page exists to show internal links in frontmatter. The link
126
+ graph is authored once with \`slug\` and \`linksTo\`, then contentbit derives
127
+ \`linkedFrom\` in \`.contentbit/link-index.json\`.
128
+
129
+ :::callout{type="note"}
130
+ Run \`contentbit links "content/**/*.md" --fix\` after renaming a page. Alias
131
+ references in \`linksTo\` are rewritten to the current slug, while \`aliases\`
132
+ stays as the rename record.
133
+ :::
134
+ `;
100
135
  /** The wrapper component: styled pack or headless, with or without a Markdown lib. */
101
136
  function reactComponent(styled, mdWired, blocksImport) {
102
137
  const mdImport = mdWired ? "import ReactMarkdown from 'react-markdown'\n" : '';
@@ -114,7 +149,7 @@ import { ContentRenderer } from '@/components/content-blocks/content-renderer'`
114
149
  return `'use client'
115
150
 
116
151
  import { genericBlocks } from '@contentbit/blocks'
117
- import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
152
+ import { createBlockRegistry, parseDocument, stripFrontmatter, validateDocument } from '@contentbit/core'
118
153
  ${reactImport}${mdImport}${rendererImport}
119
154
  // Everything block-related lives in the blocks/ folder: definitions in
120
155
  // registry.ts (shared with the validate CLI), components in components.tsx.
@@ -124,7 +159,7 @@ import { blockComponents } from '${blocksImport}/components'
124
159
  const registry = createBlockRegistry().use(genericBlocks()).use(customBlocks)
125
160
 
126
161
  export function Content({ source }: { source: string }) {
127
- const result = validateDocument(parseDocument(source), registry)
162
+ const result = validateDocument(parseDocument(stripFrontmatter(source)), registry)
128
163
  return (
129
164
  <${renderer}
130
165
  document={result.document}
@@ -148,14 +183,14 @@ const renderMarkdown = (md) => mdIt.render(md)`
148
183
  const renderMarkdown = undefined`;
149
184
  return `// Render content/example.md to example.html. Run: node scripts/render-example.mjs
150
185
  import { genericBlocks } from '@contentbit/blocks'
151
- import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
186
+ import { createBlockRegistry, parseDocument, stripFrontmatter, validateDocument } from '@contentbit/core'
152
187
  import { renderToHtml } from '@contentbit/html'
153
188
  import { readFile, writeFile } from 'node:fs/promises'
154
189
  ${wiring}
155
190
 
156
191
  const source = await readFile('content/example.md', 'utf8')
157
192
  const registry = createBlockRegistry().use(genericBlocks())
158
- const result = validateDocument(parseDocument(source), registry)
193
+ const result = validateDocument(parseDocument(stripFrontmatter(source)), registry)
159
194
  const html = renderToHtml(result.document, { renderMarkdown })
160
195
  await writeFile('example.html', html, 'utf8')
161
196
  console.log('wrote example.html')
@@ -213,6 +248,67 @@ export default async function ExamplePage() {
213
248
  )
214
249
  }
215
250
  `;
251
+ const ASTRO_CONTENT_CONFIG = `import { defineCollection } from 'astro:content'
252
+ import { glob } from 'astro/loaders'
253
+
254
+ export const collections = {
255
+ articles: defineCollection({
256
+ // Astro's builtin Markdown loader. Entry bodies are parsed and validated
257
+ // where they render (see src/pages/example.astro); \`contentbit validate\`
258
+ // covers the same files in CI.
259
+ loader: glob({ pattern: '**/*.md', base: './content' }),
260
+ }),
261
+ }
262
+ `;
263
+ const ASTRO_QUOTE_BLOCK = `---
264
+ // The Astro component for the custom \`quote\` block defined in blocks/registry.ts.
265
+ // Block props arrive as component props; nested content arrives via <slot />.
266
+ interface Props {
267
+ author: string
268
+ role?: string
269
+ }
270
+
271
+ const { author, role } = Astro.props
272
+ ---
273
+
274
+ <figure style="margin: 1.5rem 0; border-left: 2px solid #d4d4d4; padding-left: 1rem;">
275
+ <blockquote style="font-style: italic;"><slot /></blockquote>
276
+ <figcaption style="margin-top: 0.5rem; font-size: 0.875rem; opacity: 0.7;">
277
+ — {author}{role ? \`, \${role}\` : null}
278
+ </figcaption>
279
+ </figure>
280
+ `;
281
+ /** The example page: styled pack renderer or the headless ContentBlocks. */
282
+ function astroPage(styled) {
283
+ const importLine = styled
284
+ ? "import ContentRenderer from '../components/content-blocks/content-renderer.astro'"
285
+ : "import { ContentBlocks } from '@contentbit/astro/components'";
286
+ const renderer = styled ? 'ContentRenderer' : 'ContentBlocks';
287
+ return `---
288
+ import { genericBlocks } from '@contentbit/blocks'
289
+ import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
290
+ import { getEntry } from 'astro:content'
291
+
292
+ ${importLine}
293
+
294
+ // Definitions in blocks/registry.ts are shared with the validate CLI.
295
+ import customBlocks from '../../blocks/registry'
296
+ import QuoteBlock from '../../blocks/QuoteBlock.astro'
297
+
298
+ // Entry ids are the file path relative to the collection base, minus ".md".
299
+ const entry = await getEntry('articles', 'example')
300
+ if (!entry?.body) throw new Error('Entry "example" not found in the articles collection.')
301
+
302
+ const registry = createBlockRegistry().use(genericBlocks()).use(customBlocks)
303
+ // Static pages render at build time, so invalid blocks fail the build here.
304
+ const result = validateDocument(parseDocument(entry.body), registry)
305
+ ---
306
+
307
+ <main style="max-width: 42rem; margin: 0 auto; padding: 3rem 1.5rem;">
308
+ <${renderer} document={result.document} components={{ quote: QuoteBlock }} />
309
+ </main>
310
+ `;
311
+ }
216
312
  function detectPackageManager(cwd) {
217
313
  // The project's lockfile outranks however the CLI itself was launched.
218
314
  const locks = [
@@ -253,6 +349,27 @@ function runInstall(pm, args, cwd) {
253
349
  child.on('error', () => resolve(1));
254
350
  });
255
351
  }
352
+ /** Wire the @contentbit shadcn registry and install a styled pack. Returns true on success. */
353
+ async function installStyledPack(cwd, pack, noInstall, io) {
354
+ const componentsJsonPath = join(cwd, 'components.json');
355
+ const componentsJson = JSON.parse(await readFile(componentsJsonPath, 'utf8'));
356
+ componentsJson.registries ??= {};
357
+ if (!componentsJson.registries['@contentbit']) {
358
+ componentsJson.registries['@contentbit'] = 'https://contentbit.dev/r/{name}.json';
359
+ await writeFile(componentsJsonPath, `${JSON.stringify(componentsJson, null, 2)}\n`, 'utf8');
360
+ io.stdout('added @contentbit registry to components.json');
361
+ }
362
+ if (noInstall) {
363
+ io.stdout(`skipped: shadcn add ${pack}`);
364
+ return true;
365
+ }
366
+ const [bin, prefix] = dlxCommand(detectPackageManager(cwd));
367
+ io.stdout(`installing the styled pack: shadcn add ${pack}`);
368
+ const code = await runInstall(bin, [...prefix, 'shadcn@latest', 'add', pack, '--yes'], cwd);
369
+ if (code !== 0)
370
+ io.stderr('styled pack install failed; falling back to headless defaults');
371
+ return code === 0;
372
+ }
256
373
  /** Write a file unless it already exists; returns what happened for the summary. */
257
374
  async function scaffold(path, content) {
258
375
  try {
@@ -276,6 +393,7 @@ export async function initCommand(args, io) {
276
393
  'no-install': { type: 'boolean', default: false },
277
394
  'no-page': { type: 'boolean', default: false },
278
395
  'no-styled': { type: 'boolean', default: false },
396
+ 'no-agents': { type: 'boolean', default: false },
279
397
  },
280
398
  });
281
399
  const cwd = values.cwd;
@@ -291,7 +409,8 @@ export async function initCommand(args, io) {
291
409
  }
292
410
  // Resolve the render target: flag > prompt (interactive) > detection.
293
411
  const hasReact = Boolean(pkg.dependencies?.react ?? pkg.devDependencies?.react);
294
- const detected = hasReact ? 'react' : 'html';
412
+ const hasAstro = Boolean(pkg.dependencies?.astro ?? pkg.devDependencies?.astro);
413
+ const detected = hasAstro ? 'astro' : hasReact ? 'react' : 'html';
295
414
  let target;
296
415
  if (values.target) {
297
416
  if (!TARGETS.includes(values.target)) {
@@ -307,6 +426,7 @@ export async function initCommand(args, io) {
307
426
  initialValue: detected,
308
427
  options: [
309
428
  { value: 'react', label: 'React', hint: 'ContentBlocks component' },
429
+ { value: 'astro', label: 'Astro', hint: 'content collections + .astro components' },
310
430
  { value: 'html', label: 'Static HTML', hint: 'renderToHtml, no framework' },
311
431
  { value: 'markdown', label: 'Plain Markdown', hint: 'fallback rendering only' },
312
432
  ],
@@ -353,6 +473,8 @@ export async function initCommand(args, io) {
353
473
  runtime.push('@contentbit/react');
354
474
  if (target === 'html')
355
475
  runtime.push('@contentbit/html');
476
+ if (target === 'astro')
477
+ runtime.push('@contentbit/astro');
356
478
  if (md !== 'none')
357
479
  runtime.push(md);
358
480
  if (values['no-install']) {
@@ -374,32 +496,14 @@ export async function initCommand(args, io) {
374
496
  const files = [
375
497
  ['blocks/registry.ts', REGISTRY_TEMPLATE],
376
498
  ['content/example.md', EXAMPLE_CONTENT],
499
+ ['content/related.md', RELATED_CONTENT],
377
500
  ];
378
501
  const layout = detectFramework(cwd, { ...pkg.dependencies, ...pkg.devDependencies });
379
502
  // shadcn project? Pull the styled component pack from the contentbit registry.
380
503
  let styled = false;
381
504
  const componentsJsonPath = join(cwd, 'components.json');
382
505
  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
- }
506
+ styled = await installStyledPack(cwd, '@contentbit/generic-pack', values['no-install'], io);
403
507
  }
404
508
  if (target === 'react') {
405
509
  const depth = layout.componentPath.split('/').length - 1;
@@ -420,19 +524,50 @@ export async function initCommand(args, io) {
420
524
  htmlRenderScript(md),
421
525
  ]);
422
526
  }
527
+ if (target === 'astro') {
528
+ let astroStyled = false;
529
+ if (!values['no-styled'] && existsSync(componentsJsonPath)) {
530
+ astroStyled = await installStyledPack(cwd, '@contentbit/astro-pack', values['no-install'], io);
531
+ }
532
+ files.push(['blocks/QuoteBlock.astro', ASTRO_QUOTE_BLOCK]);
533
+ // Every config filename Astro resolves (src/content.config.* plus the
534
+ // legacy src/content/config.* location), so we never scaffold a second
535
+ // config that Astro would silently ignore.
536
+ const configCandidates = ['ts', 'mts', 'mjs', 'js'].flatMap((ext) => [
537
+ `src/content.config.${ext}`,
538
+ `src/content/config.${ext}`,
539
+ ]);
540
+ const existingConfig = configCandidates.find((p) => existsSync(join(cwd, p)));
541
+ if (existingConfig) {
542
+ io.stdout(`content config exists (${existingConfig}); add this collection manually:`);
543
+ io.stdout(ASTRO_CONTENT_CONFIG);
544
+ io.stdout('the example page expects the "articles" collection above');
545
+ }
546
+ else {
547
+ files.push(['src/content.config.ts', ASTRO_CONTENT_CONFIG]);
548
+ }
549
+ if (!values['no-page'])
550
+ files.push(['src/pages/example.astro', astroPage(astroStyled)]);
551
+ }
423
552
  for (const [rel, content] of files) {
424
553
  const result = await scaffold(join(cwd, rel), content);
425
554
  io.stdout(`${result}: ${rel}`);
426
555
  }
427
- // Wire the validate script.
556
+ // Wire content scripts.
428
557
  const fresh = JSON.parse(await readFile(pkgPath, 'utf8'));
429
558
  fresh.scripts ??= {};
430
559
  if (!fresh.scripts['content:check']) {
431
560
  fresh.scripts['content:check'] =
432
561
  'contentbit validate "content/**/*.md" --registry ./blocks/registry.ts';
433
- await writeFile(pkgPath, `${JSON.stringify(fresh, null, 2)}\n`, 'utf8');
434
562
  io.stdout('added script: content:check');
435
563
  }
564
+ if (!fresh.scripts['content:links']) {
565
+ fresh.scripts['content:links'] = 'contentbit links "content/**/*.md"';
566
+ io.stdout('added script: content:links');
567
+ }
568
+ if (!pkg.scripts?.['content:check'] || !pkg.scripts?.['content:links']) {
569
+ await writeFile(pkgPath, `${JSON.stringify(fresh, null, 2)}\n`, 'utf8');
570
+ }
436
571
  // Generate the LLM authoring guide from the registry, ready to paste into a prompt.
437
572
  let registry;
438
573
  try {
@@ -445,9 +580,17 @@ export async function initCommand(args, io) {
445
580
  const guide = registry.toAuthoringGuide({ audience: 'llm', includeExamples: true });
446
581
  await writeFile(join(cwd, 'contentbit-guide.md'), guide, 'utf8');
447
582
  io.stdout('created: contentbit-guide.md (LLM authoring instructions)');
583
+ // Coding-agent integration: an AGENTS.md block for every agent, plus Claude
584
+ // Code skills when a .claude/ directory exists. `contentbit agents` refreshes.
585
+ if (!values['no-agents']) {
586
+ await installAgentIntegration(cwd, {}, io);
587
+ io.stdout('Agent integration installed — try asking your agent:');
588
+ io.stdout(' "write a blog post about X" or "audit my content"');
589
+ }
448
590
  io.stdout('');
449
591
  io.stdout('Done. Next steps:');
450
592
  io.stdout(` 1. Validate the starter content: ${detectPackageManager(cwd)} run content:check`);
593
+ io.stdout(` Build the link index: ${detectPackageManager(cwd)} run content:links`);
451
594
  if (target === 'react') {
452
595
  if (!values['no-page'] && layout.pagePath) {
453
596
  io.stdout(' 2. Start the dev server and open /example to see the article rendered.');
@@ -458,6 +601,10 @@ export async function initCommand(args, io) {
458
601
  }
459
602
  io.stdout(' 3. Styled components: pnpm dlx shadcn@latest add @contentbit/generic-pack');
460
603
  }
604
+ else if (target === 'astro') {
605
+ io.stdout(' 2. Start the dev server and open /example to see the article rendered.');
606
+ io.stdout(' 3. Styled components: pnpm dlx shadcn@latest add @contentbit/astro-pack');
607
+ }
461
608
  else if (target === 'html') {
462
609
  io.stdout(' 2. Render it: node scripts/render-example.mjs && open example.html');
463
610
  }
@@ -0,0 +1,3 @@
1
+ import type { Io } from '../run.js';
2
+ export declare function linksCommand(args: string[], io: Io): Promise<number>;
3
+ //# sourceMappingURL=links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../../src/commands/links.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAKnC,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuF1E"}