cosmolo 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/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "cosmolo",
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "cosmolo": "./src/cli/index.ts"
7
+ },
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "default": "./src/index.ts"
12
+ },
13
+ "./plugin": {
14
+ "types": "./src/plugin.ts",
15
+ "default": "./src/plugin.ts"
16
+ }
17
+ },
18
+ "files": ["src", "templates", "!src/**/*.test.ts"],
19
+ "dependencies": {
20
+ "gray-matter": "^4.0.3",
21
+ "marked": "^15.0.0",
22
+ "zod": "^3.0.0"
23
+ },
24
+ "peerDependencies": {
25
+ "vite": ">=5.0.0",
26
+ "@sveltejs/kit": ">=2.0.0"
27
+ }
28
+ }
@@ -0,0 +1,124 @@
1
+ import { z } from 'zod';
2
+ import matter from 'gray-matter';
3
+ import { renderMarkdown, generateToc } from './markdown.js';
4
+ import { isKnownCategory } from './categories.js';
5
+ import type { Article, ResolvedCosmoloConfig } from './types.js';
6
+ import { rawMdFiles, svxModules } from 'cosmolo:content';
7
+
8
+ export const articleFrontmatterSchema = z.object({
9
+ title: z.string(),
10
+ category: z.string(),
11
+ excerpt: z.string(),
12
+ sort: z.number().default(0),
13
+ date: z.preprocess(
14
+ (val) => (val instanceof Date ? val.toISOString().split('T')[0] : val),
15
+ z.string().default('')
16
+ ),
17
+ tags: z.array(z.string()).default([]),
18
+ series: z.string().optional(),
19
+ seriesOrder: z.number().optional(),
20
+ draft: z.boolean().default(false),
21
+ related: z.array(z.string()).default([]),
22
+ });
23
+
24
+ function slugFromPath(filePath: string, dir: string): string {
25
+ const prefix = dir.replace(/^\//, '');
26
+ return filePath
27
+ .replace(new RegExp(`^/?${prefix}/`), '')
28
+ .replace(/\.(md|svx)$/, '');
29
+ }
30
+
31
+ /** Returns all non-draft articles sorted by `sort` descending. */
32
+ export function getArticles(config: ResolvedCosmoloConfig): Article[] {
33
+ const articles: Article[] = [];
34
+
35
+ for (const [filePath, raw] of Object.entries(rawMdFiles)) {
36
+ const slug = slugFromPath(filePath, config.articlesDir);
37
+ const { data } = matter(raw);
38
+ const frontmatter = articleFrontmatterSchema.parse(data);
39
+ if (frontmatter.draft) continue;
40
+ articles.push({ ...frontmatter, slug, html: '', markdown: '', toc: [] });
41
+ }
42
+
43
+ for (const [filePath, mod] of Object.entries(svxModules)) {
44
+ const slug = slugFromPath(filePath, config.articlesDir);
45
+ const frontmatter = articleFrontmatterSchema.parse(
46
+ (mod as { metadata: Record<string, unknown> }).metadata
47
+ );
48
+ if (frontmatter.draft) continue;
49
+ articles.push({ ...frontmatter, slug, html: '', markdown: '', toc: [] });
50
+ }
51
+
52
+ return articles.sort((a, b) => b.sort - a.sort);
53
+ }
54
+
55
+ /** Returns all non-draft article slugs. */
56
+ export function getSlugs(config: ResolvedCosmoloConfig): string[] {
57
+ return getArticles(config).map((a) => a.slug);
58
+ }
59
+
60
+ /** Returns a single article with rendered HTML and TOC. Draft articles are included. */
61
+ export async function getArticle(config: ResolvedCosmoloConfig, slug: string): Promise<Article> {
62
+ const dir = config.articlesDir.replace(/^\//, '');
63
+ const mdPath = `/${dir}/${slug}.md`;
64
+ const svxPath = `/${dir}/${slug}.svx`;
65
+
66
+ if (rawMdFiles[mdPath] !== undefined) {
67
+ const raw = rawMdFiles[mdPath];
68
+ const { data, content } = matter(raw);
69
+ const frontmatter = articleFrontmatterSchema.parse(data);
70
+ const html = await renderMarkdown(content);
71
+ const toc = generateToc(content);
72
+ return { ...frontmatter, slug, html, markdown: content, toc };
73
+ }
74
+
75
+ if (svxModules[svxPath] !== undefined) {
76
+ const frontmatter = articleFrontmatterSchema.parse(
77
+ (svxModules[svxPath] as { metadata: Record<string, unknown> }).metadata
78
+ );
79
+ return { ...frontmatter, slug, html: '', markdown: '', toc: [] };
80
+ }
81
+
82
+ throw new Error(`Article not found: ${slug}`);
83
+ }
84
+
85
+ export function getArticlesByCategory(
86
+ config: ResolvedCosmoloConfig,
87
+ categorySlug: string,
88
+ excludeSlug?: string
89
+ ): Article[] {
90
+ const all = getArticles(config);
91
+ const filtered =
92
+ categorySlug === 'other'
93
+ ? all.filter((a) => !isKnownCategory(config, a.category))
94
+ : all.filter((a) => a.category === categorySlug);
95
+ return excludeSlug ? filtered.filter((a) => a.slug !== excludeSlug) : filtered;
96
+ }
97
+
98
+ export function getArticlesByTag(config: ResolvedCosmoloConfig, tag: string): Article[] {
99
+ return getArticles(config).filter((a) => a.tags.includes(tag));
100
+ }
101
+
102
+ export function getArticlesBySeries(config: ResolvedCosmoloConfig, seriesKey: string): Article[] {
103
+ return getArticles(config)
104
+ .filter((a) => a.series === seriesKey)
105
+ .sort((a, b) => (a.seriesOrder ?? 0) - (b.seriesOrder ?? 0));
106
+ }
107
+
108
+ export function getTags(config: ResolvedCosmoloConfig): string[] {
109
+ const tags = new Set<string>();
110
+ getArticles(config).forEach((a) => a.tags.forEach((t) => tags.add(t)));
111
+ return Array.from(tags).sort();
112
+ }
113
+
114
+ /**
115
+ * Returns the Svelte component constructor for an .svx article, or undefined
116
+ * if the article is a plain .md file. Safe to call from Svelte components.
117
+ */
118
+ export function getSvxComponent(
119
+ config: ResolvedCosmoloConfig,
120
+ slug: string
121
+ ): unknown | undefined {
122
+ const dir = '/' + config.articlesDir.replace(/^\/|\/$/g, '');
123
+ return (svxModules[`${dir}/${slug}.svx`] as { default: unknown } | undefined)?.default;
124
+ }
@@ -0,0 +1,35 @@
1
+ import { categoriesData, siteConfigData } from 'cosmolo:content';
2
+ import type { CategoryEntry, ResolvedCosmoloConfig, SiteConfig } from './types.js';
3
+
4
+ type CategoriesMap = Record<string, { label: string; description: string }>;
5
+
6
+ const map = categoriesData as CategoriesMap;
7
+
8
+ export function getAllCategories(_config: ResolvedCosmoloConfig): CategoryEntry[] {
9
+ return Object.entries(map).map(([slug, { label, description }]) => ({
10
+ slug,
11
+ label,
12
+ description,
13
+ }));
14
+ }
15
+
16
+ export function isKnownCategory(_config: ResolvedCosmoloConfig, key: string): boolean {
17
+ return Object.prototype.hasOwnProperty.call(map, key);
18
+ }
19
+
20
+ export function getCategoryLabel(_config: ResolvedCosmoloConfig, key: string): string {
21
+ if (Object.prototype.hasOwnProperty.call(map, key)) return map[key].label;
22
+ return (siteConfigData as SiteConfig).fallbackCategoryLabel;
23
+ }
24
+
25
+ export function getCategoryDescription(_config: ResolvedCosmoloConfig, key: string): string {
26
+ return map[key]?.description ?? '';
27
+ }
28
+
29
+ export function getCategorySlugs(_config: ResolvedCosmoloConfig): string[] {
30
+ return [...Object.keys(map), 'other'];
31
+ }
32
+
33
+ export function loadSiteConfig(_config: ResolvedCosmoloConfig): SiteConfig {
34
+ return siteConfigData as SiteConfig;
35
+ }
@@ -0,0 +1,190 @@
1
+ import * as readline from 'readline';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { DEFAULT_CONFIG } from '../config.js';
5
+ import type { ResolvedCosmoloConfig } from '../types.js';
6
+
7
+ // ─── config ──────────────────────────────────────────────────────────────────
8
+
9
+ async function loadConfig(): Promise<ResolvedCosmoloConfig> {
10
+ const root = process.cwd();
11
+ for (const name of ['cosmolo.config.ts', 'cosmolo.config.js']) {
12
+ const p = path.join(root, name);
13
+ if (fs.existsSync(p)) {
14
+ try {
15
+ const mod = await import(p);
16
+ return mod.default as ResolvedCosmoloConfig;
17
+ } catch {
18
+ // fall through to defaults
19
+ }
20
+ }
21
+ }
22
+ return DEFAULT_CONFIG;
23
+ }
24
+
25
+ // ─── helpers ─────────────────────────────────────────────────────────────────
26
+
27
+ function ask(rl: readline.Interface, question: string, fallback = ''): Promise<string> {
28
+ const hint = fallback ? ` [${fallback}]` : '';
29
+ return new Promise((resolve) =>
30
+ rl.question(` ${question}${hint}: `, (ans) => resolve(ans.trim() || fallback))
31
+ );
32
+ }
33
+
34
+ function slugify(text: string): string {
35
+ return text
36
+ .toLowerCase()
37
+ .replace(/[^\w\s-]/g, '')
38
+ .trim()
39
+ .replace(/[\s_]+/g, '-')
40
+ .replace(/-+/g, '-');
41
+ }
42
+
43
+ function today(): string {
44
+ return new Date().toISOString().split('T')[0];
45
+ }
46
+
47
+ function readCategories(config: ResolvedCosmoloConfig): Record<string, { label: string; description: string }> {
48
+ const p = path.resolve(process.cwd(), config.categoriesConfigPath);
49
+ if (!fs.existsSync(p)) return {};
50
+ return JSON.parse(fs.readFileSync(p, 'utf-8'));
51
+ }
52
+
53
+ // ─── generators ──────────────────────────────────────────────────────────────
54
+
55
+ async function generateArticle(rl: readline.Interface, config: ResolvedCosmoloConfig): Promise<void> {
56
+ console.log('\nGenerate Article\n');
57
+
58
+ const title = await ask(rl, 'Title');
59
+ if (!title) { console.error('\nError: Title is required.'); process.exit(1); }
60
+
61
+ const defaultSlug = slugify(title);
62
+ const slug = await ask(rl, 'Slug', defaultSlug);
63
+
64
+ const articlesDir = path.resolve(process.cwd(), config.articlesDir);
65
+ const filePath = path.join(articlesDir, `${slug}.md`);
66
+
67
+ if (fs.existsSync(filePath)) {
68
+ console.error(`\nError: ${config.articlesDir}/${slug}.md already exists.`);
69
+ process.exit(1);
70
+ }
71
+
72
+ const categories = readCategories(config);
73
+ const catKeys = Object.keys(categories);
74
+ if (catKeys.length > 0) console.log(`\n Categories: ${catKeys.join(', ')}`);
75
+
76
+ const category = await ask(rl, 'Category', catKeys[0] ?? 'other');
77
+ const excerpt = await ask(rl, 'Excerpt', '');
78
+ const tagsRaw = await ask(rl, 'Tags (comma-separated)', '');
79
+ const tags = tagsRaw ? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean) : [];
80
+ const sort = await ask(rl, 'Sort', '0');
81
+ const date = await ask(rl, 'Date', today());
82
+ const draftAns = await ask(rl, 'Draft? (y/N)', 'N');
83
+ const draft = draftAns.toLowerCase() === 'y';
84
+ const series = await ask(rl, 'Series (optional)', '');
85
+ const seriesOrder = series ? await ask(rl, 'Series order', '1') : '';
86
+
87
+ const lines = [
88
+ '---',
89
+ `title: "${title}"`,
90
+ `category: "${category}"`,
91
+ `excerpt: "${excerpt}"`,
92
+ `sort: ${parseInt(sort, 10) || 0}`,
93
+ `date: "${date}"`,
94
+ ];
95
+ if (tags.length > 0) lines.push(`tags: [${tags.map((t) => `"${t}"`).join(', ')}]`);
96
+ if (series) {
97
+ lines.push(`series: "${series}"`);
98
+ lines.push(`seriesOrder: ${parseInt(seriesOrder, 10) || 1}`);
99
+ }
100
+ if (draft) lines.push('draft: true');
101
+ lines.push('---', '', '');
102
+
103
+ fs.mkdirSync(articlesDir, { recursive: true });
104
+ fs.writeFileSync(filePath, lines.join('\n'));
105
+ console.log(`\nCreated: ${config.articlesDir}/${slug}.md`);
106
+ }
107
+
108
+ async function generatePage(rl: readline.Interface, config: ResolvedCosmoloConfig): Promise<void> {
109
+ console.log('\nGenerate Page\n');
110
+
111
+ const title = await ask(rl, 'Title');
112
+ if (!title) { console.error('\nError: Title is required.'); process.exit(1); }
113
+
114
+ const defaultSlug = slugify(title);
115
+ const slug = await ask(rl, 'Slug', defaultSlug);
116
+
117
+ const pagesDir = path.resolve(process.cwd(), config.pagesDir);
118
+ const filePath = path.join(pagesDir, `${slug}.md`);
119
+
120
+ if (fs.existsSync(filePath)) {
121
+ console.error(`\nError: ${config.pagesDir}/${slug}.md already exists.`);
122
+ process.exit(1);
123
+ }
124
+
125
+ const content = ['---', `title: "${title}"`, '---', '', ''].join('\n');
126
+ fs.mkdirSync(pagesDir, { recursive: true });
127
+ fs.writeFileSync(filePath, content);
128
+ console.log(`\nCreated: ${config.pagesDir}/${slug}.md`);
129
+ }
130
+
131
+ async function generateCategory(rl: readline.Interface, config: ResolvedCosmoloConfig): Promise<void> {
132
+ console.log('\nGenerate Category\n');
133
+
134
+ const key = await ask(rl, 'Key (slug)');
135
+ if (!key) { console.error('\nError: Key is required.'); process.exit(1); }
136
+
137
+ const categoriesPath = path.resolve(process.cwd(), config.categoriesConfigPath);
138
+ const categories = readCategories(config);
139
+
140
+ if (categories[key]) {
141
+ console.error(`\nError: Category "${key}" already exists.`);
142
+ process.exit(1);
143
+ }
144
+
145
+ const label = await ask(rl, 'Label');
146
+ const description = await ask(rl, 'Description', '');
147
+
148
+ categories[key] = { label, description };
149
+ fs.mkdirSync(path.dirname(categoriesPath), { recursive: true });
150
+ fs.writeFileSync(categoriesPath, JSON.stringify(categories, null, '\t') + '\n');
151
+ console.log(`\nAdded: "${key}" to ${config.categoriesConfigPath}`);
152
+ }
153
+
154
+ // ─── main ─────────────────────────────────────────────────────────────────────
155
+
156
+ export async function main(): Promise<void> {
157
+ const config = await loadConfig();
158
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
159
+
160
+ const subcommand = process.argv[3]; // argv[2] is "generate"
161
+
162
+ const commands: Record<string, (rl: readline.Interface, config: ResolvedCosmoloConfig) => Promise<void>> = {
163
+ article: generateArticle,
164
+ page: generatePage,
165
+ category: generateCategory,
166
+ };
167
+
168
+ try {
169
+ if (subcommand && commands[subcommand]) {
170
+ await commands[subcommand](rl, config);
171
+ } else if (subcommand) {
172
+ console.error(`Unknown subcommand: ${subcommand}`);
173
+ console.error('Available: article, page, category');
174
+ process.exit(1);
175
+ } else {
176
+ console.log('Cosmolo Generator\n');
177
+ console.log(' 1. Article');
178
+ console.log(' 2. Page');
179
+ console.log(' 3. Category');
180
+ const choice = await new Promise<string>((resolve) =>
181
+ rl.question('\nWhat would you like to generate? (1/2/3): ', (ans) => resolve(ans.trim()))
182
+ );
183
+ const picked = ({ '1': generateArticle, '2': generatePage, '3': generateCategory } as Record<string, typeof generateArticle>)[choice];
184
+ if (!picked) { console.error('Invalid choice.'); process.exit(1); }
185
+ await picked(rl, config);
186
+ }
187
+ } finally {
188
+ rl.close();
189
+ }
190
+ }
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bun
2
+ export {};
3
+
4
+ const cmd = process.argv[2];
5
+
6
+ switch (cmd) {
7
+ case 'init':
8
+ await (await import('./init.js')).main();
9
+ break;
10
+ case 'generate':
11
+ await (await import('./generate.js')).main();
12
+ break;
13
+ default: {
14
+ const isUnknown = Boolean(cmd);
15
+ if (isUnknown) console.error(`Unknown command: ${cmd}\n`);
16
+ console.log('Usage: cosmolo <command>\n');
17
+ console.log('Commands:');
18
+ console.log(' init Scaffold routes into an existing SvelteKit project');
19
+ console.log(' generate [article|page|category] Create content files');
20
+ process.exit(isUnknown ? 1 : 0);
21
+ }
22
+ }
@@ -0,0 +1,173 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as readline from 'readline';
4
+ import { fileURLToPath } from 'url';
5
+
6
+ // ─── helpers ─────────────────────────────────────────────────────────────────
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const TEMPLATE_DIR = path.resolve(__dirname, '../../templates');
10
+
11
+ function ask(rl: readline.Interface, question: string): Promise<string> {
12
+ return new Promise((resolve) => rl.question(question, resolve));
13
+ }
14
+
15
+ function copyFile(src: string, dest: string): void {
16
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
17
+ fs.copyFileSync(src, dest);
18
+ }
19
+
20
+ function writeFile(dest: string, content: string): void {
21
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
22
+ fs.writeFileSync(dest, content, 'utf-8');
23
+ }
24
+
25
+ function collectFiles(dir: string): Array<[string, string]> {
26
+ const results: Array<[string, string]> = [];
27
+ function walk(current: string) {
28
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
29
+ const full = path.join(current, entry.name);
30
+ if (entry.isDirectory()) walk(full);
31
+ else results.push([full, path.relative(dir, full)]);
32
+ }
33
+ }
34
+ walk(dir);
35
+ return results;
36
+ }
37
+
38
+ function destPath(relativePath: string, projectRoot: string): string {
39
+ const mapped = relativePath
40
+ .replace(/^routes\//, 'src/routes/')
41
+ .replace(/^lib\//, 'src/lib/');
42
+ return path.join(projectRoot, mapped);
43
+ }
44
+
45
+ function injectPackageScripts(projectRoot: string): void {
46
+ const pkgPath = path.join(projectRoot, 'package.json');
47
+ if (!fs.existsSync(pkgPath)) return;
48
+
49
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
50
+ pkg.scripts = pkg.scripts ?? {};
51
+
52
+ let added = false;
53
+ const scripts: Record<string, string> = {
54
+ 'generate:article': 'cosmolo generate article',
55
+ 'generate:page': 'cosmolo generate page',
56
+ 'generate:category': 'cosmolo generate category',
57
+ };
58
+ for (const [key, val] of Object.entries(scripts)) {
59
+ if (!pkg.scripts[key]) {
60
+ pkg.scripts[key] = val;
61
+ added = true;
62
+ }
63
+ }
64
+
65
+ if (added) {
66
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, '\t') + '\n');
67
+ console.log(' updated package.json (added generate:* scripts)');
68
+ }
69
+ }
70
+
71
+ // ─── main ─────────────────────────────────────────────────────────────────────
72
+
73
+ export async function main(): Promise<void> {
74
+ const PROJECT_ROOT = process.cwd();
75
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
76
+
77
+ console.log('\ncosmolo init\n');
78
+
79
+ // ── Mode selection ──────────────────────────────────────────────────────
80
+ console.log('Choose an initialization mode:\n');
81
+ console.log(' A) Full — server routes + Svelte page components');
82
+ console.log(' B) Slim — server routes only (bring your own Svelte UI)\n');
83
+
84
+ let modeRaw = '';
85
+ while (!['a', 'b'].includes(modeRaw)) {
86
+ modeRaw = (await ask(rl, 'Mode [A/B]: ')).trim().toLowerCase();
87
+ if (!['a', 'b'].includes(modeRaw)) console.log(' Please enter A or B.');
88
+ }
89
+ const mode: 'full' | 'slim' = modeRaw === 'a' ? 'full' : 'slim';
90
+
91
+ // ── Adapter selection ───────────────────────────────────────────────────
92
+ console.log('\nChoose your deployment adapter:\n');
93
+ console.log(' 1) SSG — @sveltejs/adapter-static (Cloudflare Pages static, GitHub Pages, etc.)');
94
+ console.log(' 2) Serverless/SSR — Cloudflare Workers, Vercel, Node, etc.\n');
95
+
96
+ let adapterRaw = '';
97
+ while (!['1', '2'].includes(adapterRaw)) {
98
+ adapterRaw = (await ask(rl, 'Adapter [1/2]: ')).trim();
99
+ if (!['1', '2'].includes(adapterRaw)) console.log(' Please enter 1 or 2.');
100
+ }
101
+ const isSSG = adapterRaw === '1';
102
+
103
+ rl.close();
104
+
105
+ // ── Collect files ───────────────────────────────────────────────────────
106
+ const sharedFiles = collectFiles(path.join(TEMPLATE_DIR, 'shared'));
107
+ const fullFiles = mode === 'full' ? collectFiles(path.join(TEMPLATE_DIR, 'full')) : [];
108
+
109
+ const allFiles: Array<[string, string]> = [
110
+ ...sharedFiles.map(([src, rel]) => [src, rel] as [string, string]),
111
+ ...fullFiles.map(([src, rel]) => [src, rel] as [string, string]),
112
+ ];
113
+
114
+ const layoutTsPath = path.join(PROJECT_ROOT, 'src/routes/+layout.ts');
115
+ const layoutTsContent = 'export const prerender = true;\n';
116
+
117
+ // ── Conflict detection ──────────────────────────────────────────────────
118
+ const conflicts: string[] = [];
119
+
120
+ for (const [, rel] of allFiles) {
121
+ const dest = destPath(rel, PROJECT_ROOT);
122
+ if (fs.existsSync(dest)) conflicts.push(path.relative(PROJECT_ROOT, dest));
123
+ }
124
+ if (isSSG && fs.existsSync(layoutTsPath)) {
125
+ conflicts.push(path.relative(PROJECT_ROOT, layoutTsPath));
126
+ }
127
+
128
+ if (conflicts.length > 0) {
129
+ console.error('\nError: The following files already exist and would be overwritten:\n');
130
+ for (const f of conflicts) console.error(` ${f}`);
131
+ console.error('\nTo resolve, either:');
132
+ console.error(' 1. Remove or rename the conflicting files, then run cosmolo init again.');
133
+ console.error(' 2. Manually copy the needed templates from the cosmolo package:');
134
+ console.error(` node_modules/cosmolo/templates/shared/`);
135
+ if (mode === 'full') console.error(` node_modules/cosmolo/templates/full/`);
136
+ if (isSSG) {
137
+ console.error('\n For SSG prerendering, add this to src/routes/+layout.ts manually:');
138
+ console.error(' export const prerender = true;');
139
+ }
140
+ process.exit(1);
141
+ }
142
+
143
+ // ── Write files ─────────────────────────────────────────────────────────
144
+ for (const [src, rel] of allFiles) {
145
+ const dest = destPath(rel, PROJECT_ROOT);
146
+ copyFile(src, dest);
147
+ console.log(` created ${path.relative(PROJECT_ROOT, dest)}`);
148
+ }
149
+
150
+ if (isSSG) {
151
+ writeFile(layoutTsPath, layoutTsContent);
152
+ console.log(` created src/routes/+layout.ts`);
153
+ }
154
+
155
+ injectPackageScripts(PROJECT_ROOT);
156
+
157
+ // ── Next steps ──────────────────────────────────────────────────────────
158
+ console.log('\nDone! Next steps:\n');
159
+ console.log(' 1. Install cosmolo: npm install cosmolo (or bun add cosmolo)');
160
+ if (isSSG) {
161
+ console.log(' 2. Install adapter: npm install -D @sveltejs/adapter-static');
162
+ } else {
163
+ console.log(' 2. Install adapter: npm install -D @sveltejs/adapter-cloudflare (or your adapter)');
164
+ }
165
+ if (mode === 'full') {
166
+ console.log(' 3. Install sass: npm install -D sass (SCSS used in Svelte templates)');
167
+ console.log(' 4. Run: npm run dev');
168
+ } else {
169
+ console.log(' 3. Add your own +page.svelte files for each route.');
170
+ console.log(' 4. Run: npm run dev');
171
+ }
172
+ console.log('\n See https://github.com/alcogy/cosmolo for full documentation.\n');
173
+ }
package/src/config.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { CosmoloConfig, ResolvedCosmoloConfig } from './types.js';
2
+
3
+ export const DEFAULT_CONFIG: ResolvedCosmoloConfig = {
4
+ articlesDir: 'src/content/articles',
5
+ pagesDir: 'src/content/pages',
6
+ siteConfigPath: 'config/site.json',
7
+ categoriesConfigPath: 'config/categories.json',
8
+ };
9
+
10
+ export function resolveConfig(userConfig: CosmoloConfig = {}): ResolvedCosmoloConfig {
11
+ return { ...DEFAULT_CONFIG, ...userConfig };
12
+ }
package/src/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ export type {
2
+ CosmoloConfig,
3
+ ResolvedCosmoloConfig,
4
+ TocEntry,
5
+ ArticleFrontmatter,
6
+ Article,
7
+ Page,
8
+ CategoryEntry,
9
+ SiteConfig,
10
+ } from './types.js';
11
+
12
+ export { DEFAULT_CONFIG, resolveConfig } from './config.js';
13
+ export {
14
+ getAllCategories,
15
+ isKnownCategory,
16
+ getCategoryLabel,
17
+ getCategoryDescription,
18
+ getCategorySlugs,
19
+ loadSiteConfig,
20
+ } from './categories.js';
21
+ export { articleFrontmatterSchema, getArticles, getArticle, getSlugs, getArticlesByCategory, getArticlesByTag, getArticlesBySeries, getTags, getSvxComponent } from './articles.js';
22
+ export { getPageSlugs, getPage } from './pages.js';
23
+ export { renderMarkdown, generateToc } from './markdown.js';
24
+ export {
25
+ createArticlesLoader,
26
+ createArticleLoader,
27
+ createCategoryLoader,
28
+ createTagLoader,
29
+ createPageLoader,
30
+ createArticleEntries,
31
+ createCategoryEntries,
32
+ createTagEntries,
33
+ createPageEntries,
34
+ } from './loaders.js';