create-krantho 0.0.0-stage → 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Descoped
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/export.js ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ /**
5
+ * Simple YAML serializer for frontmatter (flat key-value only).
6
+ * Values are JSON.stringify'd, which handles quoting and escaping for strings.
7
+ * Limitation: nested objects/arrays are serialized as inline JSON, not block YAML.
8
+ */
9
+ function toYaml(data) {
10
+ return Object.entries(data)
11
+ .map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
12
+ .join('\n');
13
+ }
14
+ /**
15
+ * Parse CLI arguments, exiting with a usage message when required options are
16
+ * missing. Keeping argument parsing out of `main()` shrinks main's mixed
17
+ * responsibility list and makes arg validation testable in isolation.
18
+ */
19
+ function parseExportArgs(args) {
20
+ const urlIndex = args.indexOf('--url');
21
+ const outputIndex = args.indexOf('--output');
22
+ const baseUrl = urlIndex >= 0 ? args[urlIndex + 1] : 'http://localhost:4002';
23
+ const outputDir = outputIndex >= 0 ? args[outputIndex + 1] : './export-bundle';
24
+ if (!baseUrl || !outputDir) {
25
+ console.error('Usage: krantho-export --url <server-url> --output <directory>');
26
+ process.exit(1);
27
+ }
28
+ return { baseUrl, outputDir };
29
+ }
30
+ /** Fetch the export bundle from the running krantho server. Exits on failure. */
31
+ async function fetchBundle(baseUrl) {
32
+ const res = await fetch(`${baseUrl}/api/export/bundle`);
33
+ if (!res.ok) {
34
+ console.error(`Export failed: ${res.status} ${res.statusText}`);
35
+ const body = await res.text();
36
+ console.error(body);
37
+ process.exit(1);
38
+ }
39
+ return (await res.json());
40
+ }
41
+ /**
42
+ * Write the content items from a bundle as `.md` files with reconstructed
43
+ * YAML frontmatter. Each item lands at `{outputDir}/content/{collection}/{slug}.md`.
44
+ */
45
+ async function writeExportedContent(outputDir, bundle) {
46
+ for (const [collection, items] of Object.entries(bundle.content)) {
47
+ for (const [slug, item] of Object.entries(items)) {
48
+ const filePath = join(outputDir, 'content', collection, `${slug}.md`);
49
+ await mkdir(dirname(filePath), { recursive: true });
50
+ // The locale is always written: the file then lands in its own locale on
51
+ // any site, whatever default locale that site has.
52
+ const frontmatter = { ...item.fields, locale: item.locale };
53
+ if (item.status !== 'published')
54
+ frontmatter['status'] = item.status;
55
+ const yaml = toYaml(frontmatter);
56
+ const content = `---\n${yaml}---\n${item.body}`;
57
+ await writeFile(filePath, content, 'utf-8');
58
+ }
59
+ }
60
+ }
61
+ /**
62
+ * Write the bundle's media metadata and runtime schema extensions as JSON
63
+ * sidecars next to the exported content. Skipped when the bundle has none.
64
+ */
65
+ async function writeExportedSidecars(outputDir, bundle) {
66
+ if (bundle.media.items.length > 0) {
67
+ const mediaPath = join(outputDir, 'media', 'metadata.json');
68
+ await mkdir(dirname(mediaPath), { recursive: true });
69
+ await writeFile(mediaPath, JSON.stringify(bundle.media, null, 2), 'utf-8');
70
+ }
71
+ if (Object.keys(bundle.schemas).length > 0) {
72
+ const schemaPath = join(outputDir, 'config', 'schemas.json');
73
+ await mkdir(dirname(schemaPath), { recursive: true });
74
+ await writeFile(schemaPath, JSON.stringify(bundle.schemas, null, 2), 'utf-8');
75
+ }
76
+ }
77
+ /**
78
+ * Reconstruct and write the content manifest from the bundle's checksum map.
79
+ * The manifest is what the migration system uses on re-import to detect
80
+ * changed vs. unchanged files.
81
+ */
82
+ async function writeManifest(outputDir, bundle) {
83
+ const manifest = {
84
+ version: bundle.version,
85
+ generatedAt: bundle.timestamp,
86
+ files: Object.fromEntries(Object.entries(bundle.checksums).map(([path, checksum]) => [
87
+ `${path}.md`,
88
+ { checksum, size: 0 },
89
+ ])),
90
+ };
91
+ await writeFile(join(outputDir, 'content', 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf-8');
92
+ }
93
+ /**
94
+ * Export content from a running krantho server to local files.
95
+ *
96
+ * Usage: krantho-export --url http://localhost:4002 --output ./content
97
+ */
98
+ async function main() {
99
+ const { baseUrl, outputDir } = parseExportArgs(process.argv.slice(2));
100
+ console.log(`Exporting from ${baseUrl} to ${outputDir}...`);
101
+ const bundle = await fetchBundle(baseUrl);
102
+ await writeExportedContent(outputDir, bundle);
103
+ await writeExportedSidecars(outputDir, bundle);
104
+ await writeManifest(outputDir, bundle);
105
+ const totalItems = Object.values(bundle.content).reduce((sum, col) => sum + Object.keys(col).length, 0);
106
+ console.log(`Exported ${totalItems} content items, ${bundle.media.items.length} media items, ${Object.keys(bundle.schemas).length} schemas`);
107
+ console.log(`Manifest version: ${bundle.version}`);
108
+ console.log(`Output: ${outputDir}`);
109
+ }
110
+ main().catch((err) => {
111
+ console.error('Export failed:', err);
112
+ process.exit(1);
113
+ });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from 'node:child_process';
3
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import { runPrompts } from './prompts.js';
6
+ import { generateFiles } from './scaffold.js';
7
+ /**
8
+ * Validate the target project directory. Exits the process with a message if
9
+ * the directory already exists and is non-empty — krantho refuses to scaffold
10
+ * on top of existing files to avoid clobbering the user's work.
11
+ */
12
+ function validateProjectDir(projectDir, name) {
13
+ if (!existsSync(projectDir))
14
+ return;
15
+ let entries = [];
16
+ try {
17
+ entries = readdirSync(projectDir);
18
+ }
19
+ catch {
20
+ // Directory exists but isn't readable — treat as "empty" and continue.
21
+ return;
22
+ }
23
+ if (entries.length > 0) {
24
+ console.error(`\nError: Directory "${name}" is not empty.`);
25
+ process.exit(1);
26
+ }
27
+ }
28
+ /**
29
+ * Write every scaffold file to disk, creating parent directories as needed.
30
+ */
31
+ function scaffoldProject(projectDir, options) {
32
+ mkdirSync(projectDir, { recursive: true });
33
+ const files = generateFiles(options);
34
+ for (const [filePath, content] of files) {
35
+ const fullPath = resolve(projectDir, filePath);
36
+ mkdirSync(resolve(fullPath, '..'), { recursive: true });
37
+ writeFileSync(fullPath, content, 'utf-8');
38
+ }
39
+ }
40
+ /**
41
+ * Install dependencies using the user-selected package manager. Failures are
42
+ * non-fatal — print the manual command and continue.
43
+ */
44
+ function installDependencies(projectDir, pm) {
45
+ console.log(`Installing dependencies with ${pm}...`);
46
+ try {
47
+ execSync(`${pm} install`, { cwd: projectDir, stdio: 'inherit' });
48
+ }
49
+ catch {
50
+ console.log(`\nDependency install failed. Run "${pm} install" manually.`);
51
+ }
52
+ }
53
+ /**
54
+ * Initialize a fresh git repository in the project directory if one doesn't
55
+ * already exist. Silently skips when git is not installed or the commit fails.
56
+ */
57
+ function initializeGit(projectDir) {
58
+ if (existsSync(resolve(projectDir, '.git')))
59
+ return;
60
+ try {
61
+ execSync('git init', { cwd: projectDir, stdio: 'ignore' });
62
+ execSync('git add -A', { cwd: projectDir, stdio: 'ignore' });
63
+ execSync('git commit -m "Initial commit from create-krantho"', {
64
+ cwd: projectDir,
65
+ stdio: 'ignore',
66
+ });
67
+ }
68
+ catch {
69
+ // git not available or commit failed — skip
70
+ }
71
+ }
72
+ /** Print the "what's next" hint shown after scaffolding completes. */
73
+ function printSuccessMessage(options) {
74
+ console.log(`\nDone! Your krantho project is ready.\n`);
75
+ console.log(` cd ${options.name}`);
76
+ console.log(` ${options.packageManager} run dev\n`);
77
+ console.log(`Studio: http://localhost:${options.port}/studio`);
78
+ console.log(`API endpoint: http://localhost:${options.port}/api/content\n`);
79
+ }
80
+ async function main() {
81
+ const args = process.argv.slice(2);
82
+ // Support: create-krantho my-app
83
+ const targetArg = args.find((a) => !a.startsWith('-'));
84
+ const options = await runPrompts(targetArg);
85
+ const projectDir = resolve(process.cwd(), options.name);
86
+ validateProjectDir(projectDir, options.name);
87
+ console.log(`\nScaffolding ${options.name}...`);
88
+ console.log(` Framework: ${options.framework}`);
89
+ console.log(` UI: ${options.ui}`);
90
+ if (options.auth)
91
+ console.log(` Auth: token mode`);
92
+ if (options.seo)
93
+ console.log(` SEO: enabled`);
94
+ console.log();
95
+ scaffoldProject(projectDir, options);
96
+ installDependencies(projectDir, options.packageManager);
97
+ initializeGit(projectDir);
98
+ printSuccessMessage(options);
99
+ }
100
+ main().catch((err) => {
101
+ console.error(err);
102
+ process.exit(1);
103
+ });
@@ -0,0 +1,14 @@
1
+ export type Framework = 'nextjs' | 'sveltekit';
2
+ export type UI = 'tailwind' | 'mui' | 'plain';
3
+ export type PackageManager = 'pnpm' | 'npm' | 'yarn';
4
+ export interface ProjectOptions {
5
+ name: string;
6
+ framework: Framework;
7
+ ui: UI;
8
+ auth: boolean;
9
+ seo: boolean;
10
+ sampleContent: boolean;
11
+ packageManager: PackageManager;
12
+ port: number;
13
+ }
14
+ export declare function runPrompts(targetArg?: string): Promise<ProjectOptions>;
@@ -0,0 +1,91 @@
1
+ import prompts from 'prompts';
2
+ function detectPackageManager() {
3
+ const agent = process.env['npm_config_user_agent'] ?? '';
4
+ if (agent.startsWith('pnpm'))
5
+ return 'pnpm';
6
+ if (agent.startsWith('yarn'))
7
+ return 'yarn';
8
+ return 'npm';
9
+ }
10
+ export async function runPrompts(targetArg) {
11
+ const response = await prompts([
12
+ {
13
+ type: targetArg ? null : 'text',
14
+ name: 'name',
15
+ message: 'Project name:',
16
+ initial: 'my-krantho-site',
17
+ validate: (val) => /^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/.test(val) || 'Invalid project name',
18
+ },
19
+ {
20
+ type: 'select',
21
+ name: 'framework',
22
+ message: 'Framework:',
23
+ choices: [
24
+ { title: 'Next.js', description: 'App Router, React Server Components', value: 'nextjs' },
25
+ {
26
+ title: 'SvelteKit',
27
+ description: 'Svelte 5, server-side rendering',
28
+ value: 'sveltekit',
29
+ },
30
+ ],
31
+ },
32
+ {
33
+ type: (_, values) => (values['framework'] === 'nextjs' ? 'select' : null),
34
+ name: 'ui',
35
+ message: 'UI library:',
36
+ choices: [
37
+ { title: 'Tailwind CSS', description: 'Utility-first CSS framework', value: 'tailwind' },
38
+ { title: 'MUI', description: 'Material UI components', value: 'mui' },
39
+ { title: 'Plain CSS', description: 'Semantic CSS, no framework', value: 'plain' },
40
+ ],
41
+ },
42
+ {
43
+ type: (_, values) => (values['framework'] === 'sveltekit' ? 'select' : null),
44
+ name: 'ui',
45
+ message: 'UI library:',
46
+ choices: [
47
+ { title: 'Tailwind CSS', description: 'Utility-first CSS framework', value: 'tailwind' },
48
+ { title: 'Plain CSS', description: 'Semantic CSS, no framework', value: 'plain' },
49
+ ],
50
+ },
51
+ {
52
+ type: 'toggle',
53
+ name: 'auth',
54
+ message: 'Enable authentication (token mode)?',
55
+ initial: false,
56
+ active: 'yes',
57
+ inactive: 'no',
58
+ },
59
+ {
60
+ type: 'toggle',
61
+ name: 'seo',
62
+ message: 'Include SEO components?',
63
+ initial: true,
64
+ active: 'yes',
65
+ inactive: 'no',
66
+ },
67
+ {
68
+ type: 'toggle',
69
+ name: 'sampleContent',
70
+ message: 'Include sample content?',
71
+ initial: true,
72
+ active: 'yes',
73
+ inactive: 'no',
74
+ },
75
+ ], {
76
+ onCancel: () => {
77
+ console.log('\nCancelled.');
78
+ process.exit(0);
79
+ },
80
+ });
81
+ return {
82
+ name: targetArg ?? response['name'],
83
+ framework: response['framework'],
84
+ ui: response['ui'] ?? 'tailwind',
85
+ auth: response['auth'] ?? false,
86
+ seo: response['seo'] ?? true,
87
+ sampleContent: response['sampleContent'] ?? true,
88
+ packageManager: detectPackageManager(),
89
+ port: 3000,
90
+ };
91
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProjectOptions } from './prompts.js';
2
+ export declare function generateFiles(options: ProjectOptions): Map<string, string>;
@@ -0,0 +1,21 @@
1
+ import { generateCommonFiles } from './templates/common.js';
2
+ import { generateContentFiles } from './templates/content.js';
3
+ import { generateNextjsFiles } from './templates/nextjs.js';
4
+ import { generateSveltekitFiles } from './templates/sveltekit.js';
5
+ export function generateFiles(options) {
6
+ const files = new Map();
7
+ // Common files (gitignore, tsconfig, env, README)
8
+ for (const [path, content] of generateCommonFiles(options)) {
9
+ files.set(path, content);
10
+ }
11
+ // Content files (pages, globals)
12
+ for (const [path, content] of generateContentFiles(options)) {
13
+ files.set(path, content);
14
+ }
15
+ // Framework-specific files
16
+ const frameworkFiles = options.framework === 'nextjs' ? generateNextjsFiles(options) : generateSveltekitFiles(options);
17
+ for (const [path, content] of frameworkFiles) {
18
+ files.set(path, content);
19
+ }
20
+ return files;
21
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProjectOptions } from '../prompts.js';
2
+ export declare function generateCommonFiles(options: ProjectOptions): Map<string, string>;
@@ -0,0 +1,98 @@
1
+ export function generateCommonFiles(options) {
2
+ const files = new Map();
3
+ files.set('.gitignore', gitignore());
4
+ files.set('.env', envFile(options));
5
+ files.set('README.md', readme(options));
6
+ files.set('.claude/settings.json', claudeSettings());
7
+ return files;
8
+ }
9
+ function gitignore() {
10
+ return `node_modules/
11
+ dist/
12
+ .next/
13
+ .svelte-kit/
14
+ .krantho/
15
+ .env.local
16
+ *.db
17
+ *.db-journal
18
+ `;
19
+ }
20
+ /** The GitHub repository that hosts the krantho plugin marketplace. */
21
+ const KRANTHO_MARKETPLACE_REPO = 'descoped/krantho';
22
+ /**
23
+ * Shared Claude Code project settings. Registering the krantho marketplace and
24
+ * enabling its plugin here means every Claude Code session opened in the new
25
+ * project has the krantho skills from the start. Claude Code honors
26
+ * `extraKnownMarketplaces` from a project file only after the folder is trusted.
27
+ */
28
+ function claudeSettings() {
29
+ const settings = {
30
+ extraKnownMarketplaces: {
31
+ krantho: {
32
+ source: { source: 'github', repo: KRANTHO_MARKETPLACE_REPO },
33
+ },
34
+ },
35
+ enabledPlugins: {
36
+ 'krantho@krantho': true,
37
+ },
38
+ };
39
+ return `${JSON.stringify(settings, null, 2)}\n`;
40
+ }
41
+ function envFile(options) {
42
+ const lines = [`# Krantho configuration`, `PORT=${options.port}`];
43
+ if (options.auth) {
44
+ lines.push('');
45
+ lines.push('# Auth — token mode is set in krantho.config.ts');
46
+ lines.push('# Set a secret token for Studio access');
47
+ lines.push('KRANTHO_API_TOKEN=change-me-to-a-secret');
48
+ }
49
+ return lines.join('\n') + '\n';
50
+ }
51
+ function readme(options) {
52
+ const fw = options.framework === 'nextjs' ? 'Next.js' : 'SvelteKit';
53
+ const pm = options.packageManager;
54
+ return `# ${options.name}
55
+
56
+ Built with [krantho](https://github.com/descoped/krantho) and ${fw}.
57
+
58
+ ## Getting Started
59
+
60
+ \`\`\`bash
61
+ ${pm} run dev
62
+ \`\`\`
63
+
64
+ Open [http://localhost:${options.port}](http://localhost:${options.port}) to see your site.
65
+
66
+ ## Studio
67
+
68
+ Open [http://localhost:${options.port}/studio](http://localhost:${options.port}/studio) to manage content.
69
+
70
+ ## Project Structure
71
+
72
+ \`\`\`
73
+ content/ # Markdown content files
74
+ pages/ # Page content (frontmatter + markdown body)
75
+ globals/ # Site-wide data (navigation, footer)
76
+ ${options.framework === 'nextjs' ? 'app/ # Next.js App Router pages' : 'src/routes/ # SvelteKit route pages'}
77
+ ${options.framework === 'nextjs' ? 'lib/ # Krantho initialization' : 'src/lib/ # Krantho initialization'}
78
+ \`\`\`
79
+
80
+ ## Content
81
+
82
+ Edit markdown files in \`content/\` or use Studio.
83
+
84
+ Each page has YAML frontmatter (metadata) and a markdown body:
85
+
86
+ \`\`\`markdown
87
+ ---
88
+ title: My Page
89
+ description: A brief description
90
+ status: published
91
+ ---
92
+
93
+ ## Section Heading
94
+
95
+ Page content here...
96
+ \`\`\`
97
+ `;
98
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProjectOptions } from '../prompts.js';
2
+ export declare function generateContentFiles(options: ProjectOptions): Map<string, string>;
@@ -0,0 +1,99 @@
1
+ export function generateContentFiles(options) {
2
+ const files = new Map();
3
+ // Globals (always created)
4
+ files.set('content/globals/navigation.md', navigationContent(options));
5
+ files.set('content/globals/footer.md', footerContent(options));
6
+ if (options.sampleContent) {
7
+ files.set('content/pages/home.md', homePageContent(options));
8
+ files.set('content/pages/about.md', aboutPageContent());
9
+ }
10
+ else {
11
+ // Minimal starter page
12
+ files.set('content/pages/home.md', minimalHomeContent(options));
13
+ }
14
+ return files;
15
+ }
16
+ function navigationContent(options) {
17
+ return `---
18
+ logo-name: ${options.name}
19
+ logo-subtitle: Built with krantho
20
+ nav-home: Home
21
+ nav-about: About
22
+ ---
23
+ `;
24
+ }
25
+ function footerContent(options) {
26
+ return `---
27
+ footer-title: ${options.name}
28
+ footer-description: A content-driven site powered by krantho.
29
+ footer-copyright-text: "\u00A9 ${new Date().getFullYear()} ${options.name}. All rights reserved."
30
+ ---
31
+ `;
32
+ }
33
+ function homePageContent(options) {
34
+ return `---
35
+ title: Welcome to ${options.name}
36
+ description: ${options.name} — a content-driven site built with krantho.
37
+ status: published
38
+ ---
39
+
40
+ ## Getting Started
41
+
42
+ This is your home page. Edit it in \`content/pages/home.md\` or use Studio at [/studio](/studio).
43
+
44
+ Krantho gives you:
45
+
46
+ - **Markdown content** with YAML frontmatter
47
+ - **Studio** for content management
48
+ - **API endpoints** for headless use
49
+ - **Schema validation** with Zod
50
+ - **SQLite persistence** with auto-import from markdown
51
+
52
+ ## How It Works
53
+
54
+ Content lives in the \`content/\` directory as markdown files. The framework auto-imports these into a SQLite database on first run, then serves them through a REST API.
55
+
56
+ Studio at \`/studio\` provides a full editing experience with:
57
+
58
+ - Collection browsing and search
59
+ - Schema-driven form fields
60
+ - Split-view markdown editor
61
+ - Version history and restore
62
+ - Publishing workflow (draft/published/archived)
63
+
64
+ ## Next Steps
65
+
66
+ 1. Edit this page in Studio or in \`content/pages/home.md\`
67
+ 2. Create new pages by adding markdown files to \`content/pages/\`
68
+ 3. Define custom collections with Zod schemas
69
+ 4. Customize the layout and components
70
+ `;
71
+ }
72
+ function aboutPageContent() {
73
+ return `---
74
+ title: About
75
+ description: Learn more about this project.
76
+ status: published
77
+ ---
78
+
79
+ ## Our Story
80
+
81
+ This is a sample about page. Replace this content with your own.
82
+
83
+ ## What We Do
84
+
85
+ We build great things with krantho — a full-site content framework with Studio, API, and schema validation.
86
+ `;
87
+ }
88
+ function minimalHomeContent(options) {
89
+ return `---
90
+ title: ${options.name}
91
+ description: Welcome to ${options.name}.
92
+ status: published
93
+ ---
94
+
95
+ ## Welcome
96
+
97
+ Your krantho site is ready. Edit this page or create new content.
98
+ `;
99
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProjectOptions } from '../prompts.js';
2
+ export declare function generateNextjsFiles(options: ProjectOptions): Map<string, string>;