contentbit 0.1.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,3 @@
1
+ import type { Io } from '../run.js';
2
+ export declare function docsCommand(args: string[], io: Io): Promise<number>;
3
+ //# sourceMappingURL=docs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/commands/docs.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAInC,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAazE"}
@@ -0,0 +1,18 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { loadRegistry } from '../load-registry.js';
3
+ export async function docsCommand(args, io) {
4
+ const { values } = parseArgs({
5
+ args,
6
+ options: {
7
+ registry: { type: 'string' },
8
+ out: { type: 'string' },
9
+ },
10
+ });
11
+ const registry = await loadRegistry(values.registry);
12
+ const guide = registry.toAuthoringGuide({ audience: 'human', includeExamples: true });
13
+ if (values.out)
14
+ await io.writeFile(values.out, guide);
15
+ else
16
+ io.stdout(guide);
17
+ return 0;
18
+ }
@@ -0,0 +1,3 @@
1
+ import type { Io } from '../run.js';
2
+ export declare function initCommand(args: string[], io: Io): Promise<number>;
3
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAmGnC,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA8GzE"}
@@ -0,0 +1,204 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { parseArgs } from 'node:util';
5
+ import { loadRegistry } from '../load-registry.js';
6
+ const TARGETS = ['react', 'html', 'markdown'];
7
+ const REGISTRY_TEMPLATE = `// Custom blocks for this project. The CLI and your app share this module:
8
+ //
9
+ // contentbit validate "content/**/*.md" --registry ./blocks/registry.mjs
10
+ //
11
+ // Define blocks with @contentbit/core and default-export them as an array.
12
+ // Docs: https://contentbit.dev/docs/guides/custom-blocks
13
+ //
14
+ // import { defineBlock, pipeRows } from '@contentbit/core'
15
+ // import { z } from 'zod'
16
+ //
17
+ // const pricingTable = defineBlock({
18
+ // name: 'pricing-table',
19
+ // description: 'Compares product plans.',
20
+ // props: z.object({ currency: z.enum(['usd', 'eur']).default('usd') }),
21
+ // content: pipeRows({ columns: ['plan', 'price'], minRows: 2 }),
22
+ // authoring: {
23
+ // useWhen: ['Comparing pricing plans'],
24
+ // example: ':::pricing-table\\n- Starter | $0\\n- Pro | $12/mo\\n:::',
25
+ // },
26
+ // })
27
+
28
+ export default []
29
+ `;
30
+ const EXAMPLE_CONTENT = `# Hello, Content Blocks
31
+
32
+ Regular Markdown works everywhere. Blocks add validated structure:
33
+
34
+ :::callout{type="tip" title="Try breaking this file"}
35
+ Run the validate script and you will get file:line:col diagnostics.
36
+ :::
37
+
38
+ :::steps
39
+ 1. Edit this file and add or break a block.
40
+ 2. Run \`contentbit validate "content/**/*.md"\`.
41
+ 3. Render it with the target you picked at init.
42
+ :::
43
+ `;
44
+ const REACT_COMPONENT = `import { genericBlocks } from '@contentbit/blocks'
45
+ import { createBlockRegistry, parseDocument, validateDocument } from '@contentbit/core'
46
+ import { ContentBlocks } from '@contentbit/react'
47
+
48
+ const registry = createBlockRegistry().use(genericBlocks())
49
+
50
+ export function Content({ source }: { source: string }) {
51
+ const result = validateDocument(parseDocument(source), registry)
52
+ return (
53
+ <ContentBlocks
54
+ document={result.document}
55
+ // TODO: plug your Markdown library in here, e.g. react-markdown.
56
+ // One function renders all prose: https://contentbit.dev/docs/guides/markdown
57
+ // renderMarkdown={(md) => <Markdown source={md} />}
58
+ />
59
+ )
60
+ }
61
+ `;
62
+ function detectPackageManager() {
63
+ const agent = process.env.npm_config_user_agent ?? '';
64
+ for (const pm of ['pnpm', 'yarn', 'bun']) {
65
+ if (agent.startsWith(pm))
66
+ return pm;
67
+ }
68
+ return 'npm';
69
+ }
70
+ function installArgs(pm, dev, pkgs) {
71
+ const add = pm === 'npm' ? 'install' : 'add';
72
+ return dev ? [add, '-D', ...pkgs] : [add, ...pkgs];
73
+ }
74
+ function runInstall(pm, args, cwd) {
75
+ return new Promise((resolve) => {
76
+ const child = spawn(pm, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' });
77
+ child.on('close', (code) => resolve(code ?? 1));
78
+ child.on('error', () => resolve(1));
79
+ });
80
+ }
81
+ /** Write a file unless it already exists; returns what happened for the summary. */
82
+ async function scaffold(path, content) {
83
+ try {
84
+ await readFile(path, 'utf8');
85
+ return 'skipped';
86
+ }
87
+ catch {
88
+ await mkdir(join(path, '..'), { recursive: true });
89
+ await writeFile(path, content, 'utf8');
90
+ return 'created';
91
+ }
92
+ }
93
+ export async function initCommand(args, io) {
94
+ const { values } = parseArgs({
95
+ args,
96
+ options: {
97
+ target: { type: 'string', short: 't' },
98
+ yes: { type: 'boolean', short: 'y', default: false },
99
+ cwd: { type: 'string', default: process.cwd() },
100
+ 'no-install': { type: 'boolean', default: false },
101
+ },
102
+ });
103
+ const cwd = values.cwd;
104
+ // A project to init into is required.
105
+ let pkg;
106
+ const pkgPath = join(cwd, 'package.json');
107
+ try {
108
+ pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
109
+ }
110
+ catch {
111
+ io.stderr('No package.json found. Run this inside a project (npm init first).');
112
+ return 1;
113
+ }
114
+ // Resolve the render target: flag > prompt (interactive) > detection.
115
+ const hasReact = Boolean(pkg.dependencies?.react ?? pkg.devDependencies?.react);
116
+ const detected = hasReact ? 'react' : 'html';
117
+ let target;
118
+ if (values.target) {
119
+ if (!TARGETS.includes(values.target)) {
120
+ io.stderr(`Unknown target "${values.target}". Use one of: ${TARGETS.join(', ')}`);
121
+ return 2;
122
+ }
123
+ target = values.target;
124
+ }
125
+ else if (!values.yes && process.stdin.isTTY && process.stdout.isTTY) {
126
+ const { isCancel, select } = await import('@clack/prompts');
127
+ const answer = await select({
128
+ message: 'Render target?',
129
+ initialValue: detected,
130
+ options: [
131
+ { value: 'react', label: 'React', hint: 'ContentBlocks component' },
132
+ { value: 'html', label: 'Static HTML', hint: 'renderToHtml, no framework' },
133
+ { value: 'markdown', label: 'Plain Markdown', hint: 'fallback rendering only' },
134
+ ],
135
+ });
136
+ if (isCancel(answer))
137
+ return 1;
138
+ target = answer;
139
+ }
140
+ else {
141
+ target = detected;
142
+ }
143
+ // Install runtime packages plus the CLI as a dev dependency.
144
+ const runtime = ['@contentbit/core', '@contentbit/blocks'];
145
+ if (target === 'react')
146
+ runtime.push('@contentbit/react');
147
+ if (target === 'html')
148
+ runtime.push('@contentbit/html');
149
+ if (values['no-install']) {
150
+ io.stdout(`skipped install: ${runtime.join(' ')} + contentbit (dev)`);
151
+ }
152
+ else {
153
+ const pm = detectPackageManager();
154
+ io.stdout(`installing with ${pm}: ${runtime.join(' ')}`);
155
+ if ((await runInstall(pm, installArgs(pm, false, runtime), cwd)) !== 0) {
156
+ io.stderr('install failed');
157
+ return 1;
158
+ }
159
+ if ((await runInstall(pm, installArgs(pm, true, ['contentbit']), cwd)) !== 0) {
160
+ io.stderr('install failed');
161
+ return 1;
162
+ }
163
+ }
164
+ // Scaffold project files; never overwrite.
165
+ const files = [
166
+ ['blocks/registry.mjs', REGISTRY_TEMPLATE],
167
+ ['content/example.md', EXAMPLE_CONTENT],
168
+ ];
169
+ if (target === 'react')
170
+ files.push(['components/content-blocks.tsx', REACT_COMPONENT]);
171
+ for (const [rel, content] of files) {
172
+ const result = await scaffold(join(cwd, rel), content);
173
+ io.stdout(`${result}: ${rel}`);
174
+ }
175
+ // Wire the validate script.
176
+ const fresh = JSON.parse(await readFile(pkgPath, 'utf8'));
177
+ fresh.scripts ??= {};
178
+ if (!fresh.scripts['content:check']) {
179
+ fresh.scripts['content:check'] =
180
+ 'contentbit validate "content/**/*.md" --registry ./blocks/registry.mjs';
181
+ await writeFile(pkgPath, `${JSON.stringify(fresh, null, 2)}\n`, 'utf8');
182
+ io.stdout('added script: content:check');
183
+ }
184
+ // Generate the LLM authoring guide from the registry, ready to paste into a prompt.
185
+ const registry = await loadRegistry();
186
+ const guide = registry.toAuthoringGuide({ audience: 'llm', includeExamples: true });
187
+ await writeFile(join(cwd, 'contentbit-guide.md'), guide, 'utf8');
188
+ io.stdout('created: contentbit-guide.md (LLM authoring instructions)');
189
+ io.stdout('');
190
+ io.stdout('Done. Next steps:');
191
+ io.stdout(` 1. Validate the starter content: ${detectPackageManager()} run content:check`);
192
+ if (target === 'react') {
193
+ io.stdout(' 2. Render it: import { Content } from "./components/content-blocks"');
194
+ io.stdout(' 3. Styled components: pnpm dlx shadcn@latest add @contentbit/generic-pack');
195
+ }
196
+ else if (target === 'html') {
197
+ io.stdout(' 2. Render it: contentbit render content/example.md --target html');
198
+ }
199
+ else {
200
+ io.stdout(' 2. Render it: contentbit render content/example.md --target markdown');
201
+ }
202
+ io.stdout(' Docs: https://contentbit.dev/docs');
203
+ return 0;
204
+ }
@@ -0,0 +1,3 @@
1
+ import type { Io } from '../run.js';
2
+ export declare function instructionsCommand(args: string[], io: Io): Promise<number>;
3
+ //# sourceMappingURL=instructions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../../src/commands/instructions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAInC,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBjF"}
@@ -0,0 +1,23 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { loadRegistry } from '../load-registry.js';
3
+ export async function instructionsCommand(args, io) {
4
+ const { values } = parseArgs({
5
+ args,
6
+ options: {
7
+ audience: { type: 'string', default: 'llm' },
8
+ 'no-examples': { type: 'boolean', default: false },
9
+ registry: { type: 'string' },
10
+ out: { type: 'string' },
11
+ },
12
+ });
13
+ const registry = await loadRegistry(values.registry);
14
+ const guide = registry.toAuthoringGuide({
15
+ audience: values.audience === 'human' ? 'human' : 'llm',
16
+ includeExamples: !values['no-examples'],
17
+ });
18
+ if (values.out)
19
+ await io.writeFile(values.out, guide);
20
+ else
21
+ io.stdout(guide);
22
+ return 0;
23
+ }
@@ -0,0 +1,3 @@
1
+ import type { Io } from '../run.js';
2
+ export declare function renderCommand(args: string[], io: Io): Promise<number>;
3
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/commands/render.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAInC,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA6B3E"}
@@ -0,0 +1,38 @@
1
+ import { genericMarkdownRenderers } from '@contentbit/blocks';
2
+ import { formatDiagnostic, parseDocument, renderToMarkdown, validateDocument, } from '@contentbit/core';
3
+ import { renderToHtml } from '@contentbit/html';
4
+ import { readFile } from 'node:fs/promises';
5
+ import { parseArgs } from 'node:util';
6
+ import { loadRegistry } from '../load-registry.js';
7
+ export async function renderCommand(args, io) {
8
+ const { values, positionals } = parseArgs({
9
+ args,
10
+ allowPositionals: true,
11
+ options: {
12
+ target: { type: 'string', default: 'html' },
13
+ registry: { type: 'string' },
14
+ out: { type: 'string' },
15
+ },
16
+ });
17
+ const file = positionals[0];
18
+ if (!file || (values.target !== 'html' && values.target !== 'markdown')) {
19
+ io.stderr('render: usage: render <file> --target html|markdown [--out <file>]');
20
+ return 2;
21
+ }
22
+ const registry = await loadRegistry(values.registry);
23
+ const source = await readFile(file, 'utf8');
24
+ const result = validateDocument(parseDocument(source), registry);
25
+ if (!result.ok) {
26
+ for (const d of result.diagnostics)
27
+ io.stderr(formatDiagnostic(d, file));
28
+ return 1;
29
+ }
30
+ const output = values.target === 'html'
31
+ ? renderToHtml(result.document)
32
+ : renderToMarkdown(result.document, { renderers: genericMarkdownRenderers });
33
+ if (values.out)
34
+ await io.writeFile(values.out, output);
35
+ else
36
+ io.stdout(output);
37
+ return 0;
38
+ }
@@ -0,0 +1,3 @@
1
+ import type { Io } from '../run.js';
2
+ export declare function validateCommand(args: string[], io: Io): Promise<number>;
3
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAA;AAInC,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAmC7E"}
@@ -0,0 +1,44 @@
1
+ import { formatDiagnostic, parseDocument, 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
+ export async function validateCommand(args, io) {
7
+ const { values, positionals } = parseArgs({
8
+ args,
9
+ allowPositionals: true,
10
+ options: {
11
+ registry: { type: 'string' },
12
+ 'strict-warnings': { type: 'boolean', default: false },
13
+ },
14
+ });
15
+ if (positionals.length === 0) {
16
+ io.stderr('validate: provide at least one file or glob.');
17
+ return 2;
18
+ }
19
+ const files = await glob(positionals, { absolute: true });
20
+ if (files.length === 0) {
21
+ io.stderr(`validate: no files matched ${positionals.join(' ')}`);
22
+ return 2;
23
+ }
24
+ const registry = await loadRegistry(values.registry);
25
+ let errors = 0;
26
+ let warnings = 0;
27
+ for (const file of files.sort()) {
28
+ const source = await readFile(file, 'utf8');
29
+ const result = validateDocument(parseDocument(source), registry);
30
+ for (const d of result.diagnostics) {
31
+ io.stderr(formatDiagnostic(d, file));
32
+ if (d.severity === 'error')
33
+ errors++;
34
+ else if (d.severity === 'warning')
35
+ warnings++;
36
+ }
37
+ }
38
+ io.stdout(`${files.length} file(s): ${errors} errors, ${warnings} warnings`);
39
+ if (errors > 0)
40
+ return 1;
41
+ if (warnings > 0 && values['strict-warnings'])
42
+ return 1;
43
+ return 0;
44
+ }
@@ -0,0 +1,4 @@
1
+ import { type BlockRegistry } from '@contentbit/core';
2
+ /** Generic pack + optional user module (default export: BlockDefinition[]). */
3
+ export declare function loadRegistry(registryPath?: string): Promise<BlockRegistry>;
4
+ //# sourceMappingURL=load-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-registry.d.ts","sourceRoot":"","sources":["../src/load-registry.ts"],"names":[],"mappings":"AACA,OAAO,EAA6C,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAGhG,+EAA+E;AAC/E,wBAAsB,YAAY,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAchF"}
@@ -0,0 +1,15 @@
1
+ import { genericBlocks } from '@contentbit/blocks';
2
+ import { createBlockRegistry } from '@contentbit/core';
3
+ import { pathToFileURL } from 'node:url';
4
+ /** Generic pack + optional user module (default export: BlockDefinition[]). */
5
+ export async function loadRegistry(registryPath) {
6
+ const registry = createBlockRegistry().use(genericBlocks());
7
+ if (registryPath) {
8
+ const mod = (await import(pathToFileURL(registryPath).href));
9
+ if (!Array.isArray(mod.default)) {
10
+ throw new Error(`--registry module must default-export an array of block definitions: ${registryPath}`);
11
+ }
12
+ registry.use(mod.default);
13
+ }
14
+ return registry;
15
+ }
package/dist/run.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export interface Io {
2
+ stdout(line: string): void;
3
+ stderr(line: string): void;
4
+ writeFile(path: string, content: string): Promise<void>;
5
+ }
6
+ export declare const USAGE = "Usage: contentbit <init|validate|render|instructions|docs> [options]\n\n init [-t react|html|markdown] [-y] [--no-install]\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>]";
7
+ export declare function run(argv: string[], io: Io): Promise<number>;
8
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +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,yaAO8B,CAAA;AAYhD,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAcjE"}