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.
- package/README.md +4 -0
- package/dist/bin.js +805 -78
- package/dist/commands/agents.d.ts +11 -0
- package/dist/commands/agents.d.ts.map +1 -0
- package/dist/commands/agents.js +197 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +129 -22
- 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 +5 -2
- package/dist/run.d.ts +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +803 -76
- package/package.json +4 -4
|
@@ -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;
|
|
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"}
|
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:
|
|
@@ -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
|
|
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
|
-
|
|
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 @@
|
|
|
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":"
|
|
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
|
-
|
|
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,
|
|
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"}
|