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.
- package/README.md +11 -1
- package/dist/bin.js +1555 -105
- package/dist/commands/agents.d.ts +11 -0
- package/dist/commands/agents.d.ts.map +1 -0
- package/dist/commands/agents.js +243 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +176 -29
- package/dist/commands/links.d.ts +3 -0
- package/dist/commands/links.d.ts.map +1 -0
- package/dist/commands/links.js +97 -0
- package/dist/commands/render.d.ts.map +1 -1
- package/dist/commands/render.js +2 -2
- package/dist/commands/stats.d.ts +3 -0
- package/dist/commands/stats.d.ts.map +1 -0
- package/dist/commands/stats.js +49 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +24 -2
- package/dist/link-options.d.ts +10 -0
- package/dist/link-options.d.ts.map +1 -0
- package/dist/link-options.js +31 -0
- package/dist/links-io.d.ts +3 -0
- package/dist/links-io.d.ts.map +1 -0
- package/dist/links-io.js +14 -0
- package/dist/run.d.ts +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +1553 -103
- package/package.json +5 -5
|
@@ -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;
|
|
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"}
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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 @@
|
|
|
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"}
|