koguma 0.6.6 → 2.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.
Files changed (44) hide show
  1. package/README.md +109 -139
  2. package/cli/auth.ts +101 -0
  3. package/cli/config.ts +149 -0
  4. package/cli/constants.ts +38 -0
  5. package/cli/content.ts +503 -0
  6. package/cli/dev-sync.ts +305 -0
  7. package/cli/exec.ts +61 -0
  8. package/cli/index.ts +779 -1545
  9. package/cli/log.ts +49 -0
  10. package/cli/preflight.ts +105 -0
  11. package/cli/scaffold.ts +680 -0
  12. package/cli/typegen.ts +190 -0
  13. package/cli/ui.ts +55 -0
  14. package/cli/wrangler.ts +367 -0
  15. package/package.json +7 -4
  16. package/src/admin/_bundle.ts +1 -1
  17. package/src/api/router.integration.test.ts +63 -80
  18. package/src/api/router.ts +85 -59
  19. package/src/config/define.ts +1 -1
  20. package/src/config/field.ts +10 -9
  21. package/src/config/index.ts +1 -13
  22. package/src/config/meta.ts +7 -7
  23. package/src/config/types.ts +1 -95
  24. package/src/db/init.ts +68 -0
  25. package/src/db/queries.ts +120 -211
  26. package/src/db/sql.ts +10 -25
  27. package/src/media/index.ts +105 -47
  28. package/src/react/Markdown.test.tsx +195 -0
  29. package/src/react/Markdown.tsx +40 -0
  30. package/src/react/index.ts +6 -22
  31. package/src/react/types.ts +3 -112
  32. package/src/db/migrate.ts +0 -182
  33. package/src/db/schema.ts +0 -122
  34. package/src/react/RichText.test.tsx +0 -535
  35. package/src/react/RichText.tsx +0 -350
  36. package/src/rich-text/index.ts +0 -4
  37. package/src/rich-text/koguma-to-lexical.ts +0 -340
  38. package/src/rich-text/lexical-compat.test.ts +0 -513
  39. package/src/rich-text/lexical-to-koguma.test.ts +0 -906
  40. package/src/rich-text/lexical-to-koguma.ts +0 -400
  41. package/src/rich-text/markdown-to-koguma.ts +0 -164
  42. package/src/rich-text/plain.test.ts +0 -208
  43. package/src/rich-text/plain.ts +0 -114
  44. package/src/rich-text/snapshots.test.ts +0 -284
package/cli/typegen.ts ADDED
@@ -0,0 +1,190 @@
1
+ /**
2
+ * cli/typegen.ts — Generate koguma.d.ts from site.config.ts.
3
+ *
4
+ * Pure logic: reads config → produces type declarations string.
5
+ * The file I/O is handled by the caller.
6
+ */
7
+
8
+ import { existsSync, writeFileSync } from 'fs';
9
+ import { resolve } from 'path';
10
+ import { ok, fail, log, ANSI } from './log.ts';
11
+ import { SITE_CONFIG_FILE, TYPEGEN_OUTPUT } from './constants.ts';
12
+
13
+ // ── TypeScript type mapping ────────────────────────────────────────
14
+
15
+ interface FieldMeta {
16
+ fieldType: string;
17
+ required: boolean;
18
+ refContentType?: string;
19
+ options?: string[];
20
+ }
21
+
22
+ const SIMPLE_STRING_TYPES = new Set([
23
+ 'text',
24
+ 'longText',
25
+ 'url',
26
+ 'date',
27
+ 'markdown',
28
+ 'youtube',
29
+ 'instagram',
30
+ 'email',
31
+ 'phone',
32
+ 'color'
33
+ ]);
34
+
35
+ /**
36
+ * Map a Koguma field type to its TypeScript representation.
37
+ */
38
+ export function fieldTypeToTs(fieldType: string, meta: FieldMeta): string {
39
+ if (SIMPLE_STRING_TYPES.has(fieldType)) return 'string';
40
+
41
+ switch (fieldType) {
42
+ case 'image':
43
+ return 'KogumaAsset';
44
+ case 'images':
45
+ return 'string[]';
46
+ case 'boolean':
47
+ return 'boolean';
48
+ case 'number':
49
+ return 'number';
50
+ case 'select':
51
+ return meta.options?.map(o => `'${o}'`).join(' | ') ?? 'string';
52
+ case 'reference':
53
+ return meta.refContentType
54
+ ? `${capitalize(meta.refContentType)}Entry`
55
+ : 'Record<string, unknown>';
56
+ case 'references':
57
+ return meta.refContentType
58
+ ? `${capitalize(meta.refContentType)}Entry[]`
59
+ : 'Record<string, unknown>[]';
60
+ default:
61
+ return 'unknown';
62
+ }
63
+ }
64
+
65
+ export function capitalize(s: string): string {
66
+ return s.charAt(0).toUpperCase() + s.slice(1);
67
+ }
68
+
69
+ // ── Type declaration generation ────────────────────────────────────
70
+
71
+ interface ContentTypeConfig {
72
+ id: string;
73
+ name: string;
74
+ fieldMeta: Record<string, FieldMeta>;
75
+ }
76
+
77
+ /**
78
+ * Generate the full koguma.d.ts content string from content type configs.
79
+ */
80
+ export function generateTypeDeclarations(
81
+ contentTypes: ContentTypeConfig[]
82
+ ): string {
83
+ const lines: string[] = [
84
+ '/**',
85
+ ' * Auto-generated by `koguma gen-types`',
86
+ ' * Do not edit manually.',
87
+ ' */',
88
+ '',
89
+ 'import type { KogumaAsset } from "koguma/types";',
90
+ '',
91
+ '// ── System fields ── common to all entries',
92
+ 'interface KogumaSystemFields {',
93
+ ' id: string;',
94
+ ' created_at: string;',
95
+ ' updated_at: string;',
96
+ ' status: "draft" | "published";',
97
+ ' publishAt: string | null;',
98
+ '}',
99
+ ''
100
+ ];
101
+
102
+ const typeNames: string[] = [];
103
+
104
+ for (const ct of contentTypes) {
105
+ const typeName = capitalize(ct.id) + 'Entry';
106
+ typeNames.push(typeName);
107
+
108
+ lines.push(`// ── ${ct.name} ──`);
109
+ lines.push(`export interface ${typeName} extends KogumaSystemFields {`);
110
+
111
+ for (const [fieldId, meta] of Object.entries(ct.fieldMeta)) {
112
+ const tsType = fieldTypeToTs(meta.fieldType, meta);
113
+ const optional = meta.required ? '' : '?';
114
+ lines.push(` ${fieldId}${optional}: ${tsType};`);
115
+ }
116
+
117
+ lines.push('}');
118
+ lines.push('');
119
+ }
120
+
121
+ // Client interface
122
+ lines.push('// ── Koguma Client ──');
123
+ lines.push('export interface KogumaClient {');
124
+
125
+ for (const ct of contentTypes) {
126
+ const typeName = capitalize(ct.id) + 'Entry';
127
+ lines.push(` /** ${ct.name} */`);
128
+ lines.push(` get(type: '${ct.id}', id: string): Promise<${typeName}>;`);
129
+ lines.push(
130
+ ` list(type: '${ct.id}'): Promise<{ entries: ${typeName}[] }>;`
131
+ );
132
+ lines.push(
133
+ ` create(type: '${ct.id}', data: Partial<${typeName}>): Promise<${typeName}>;`
134
+ );
135
+ lines.push(
136
+ ` update(type: '${ct.id}', id: string, data: Partial<${typeName}>): Promise<${typeName}>;`
137
+ );
138
+ lines.push(` delete(type: '${ct.id}', id: string): Promise<void>;`);
139
+ }
140
+
141
+ lines.push('}');
142
+ lines.push('');
143
+
144
+ // Content type union
145
+ lines.push(
146
+ `export type ContentType = ${contentTypes.map(ct => `'${ct.id}'`).join(' | ')};`
147
+ );
148
+ lines.push('');
149
+
150
+ return lines.join('\n');
151
+ }
152
+
153
+ // ── CLI command ────────────────────────────────────────────────────
154
+
155
+ /**
156
+ * Load site.config.ts and write koguma.d.ts.
157
+ */
158
+ export async function runTypegen(
159
+ root: string,
160
+ opts?: { silent?: boolean }
161
+ ): Promise<string[]> {
162
+ const configPath = resolve(root, SITE_CONFIG_FILE);
163
+ if (!existsSync(configPath)) {
164
+ fail(`${SITE_CONFIG_FILE} not found in project root.`);
165
+ process.exit(1);
166
+ }
167
+
168
+ if (!opts?.silent) log(`Reading ${SITE_CONFIG_FILE}...`);
169
+ const configModule = await import(configPath);
170
+ const config = configModule.default as {
171
+ contentTypes: ContentTypeConfig[];
172
+ };
173
+
174
+ if (!config.contentTypes?.length) {
175
+ fail(`No content types found in ${SITE_CONFIG_FILE}`);
176
+ process.exit(1);
177
+ }
178
+
179
+ const output = generateTypeDeclarations(config.contentTypes);
180
+ const outPath = resolve(root, TYPEGEN_OUTPUT);
181
+ writeFileSync(outPath, output);
182
+
183
+ const typeNames = config.contentTypes.map(ct => capitalize(ct.id) + 'Entry');
184
+ if (!opts?.silent) {
185
+ ok(
186
+ `Generated ${ANSI.CYAN}${TYPEGEN_OUTPUT}${ANSI.RESET} with ${typeNames.length} type(s): ${typeNames.join(', ')}`
187
+ );
188
+ }
189
+ return typeNames;
190
+ }
package/cli/ui.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * cli/ui.ts — Brand colors and Clack wrappers for Koguma CLI.
3
+ *
4
+ * Provides Koguma-branded intro/outro and color constants derived
5
+ * from the admin dashboard theme (theme-override.css).
6
+ */
7
+
8
+ import * as p from '@clack/prompts';
9
+ import { CLI_VERSION } from './constants.ts';
10
+
11
+ // ── Brand colors (24-bit ANSI) ─────────────────────────────────────
12
+ //
13
+ // Teal: #2dd4bf → RGB(45,212,191) primary/headings
14
+ // Red: #ff4d4d → RGB(255,77,77) accent/errors
15
+ // Text: #0f172a → RGB(15,23,42) dark slate
16
+
17
+ export const BRAND = {
18
+ /** Teal primary (RGB 45,212,191) — #2dd4bf */
19
+ PRIMARY: '\x1b[38;2;45;212;191m',
20
+ /** Red accent (RGB 255,77,77) — #ff4d4d */
21
+ ACCENT: '\x1b[38;2;255;77;77m',
22
+ /** Dim slate (RGB 113,124,149) */
23
+ DIM: '\x1b[38;2;113;124;149m',
24
+ /** Bold */
25
+ BOLD: '\x1b[1m',
26
+ /** Reset */
27
+ RESET: '\x1b[0m'
28
+ } as const;
29
+
30
+ // ── Branded Clack wrappers ─────────────────────────────────────────
31
+
32
+ /** Show a branded intro banner. */
33
+ export function intro(command?: string): void {
34
+ const label = command
35
+ ? `${BRAND.PRIMARY}${BRAND.BOLD}🐻 koguma ${command}${BRAND.RESET} ${BRAND.DIM}${CLI_VERSION}${BRAND.RESET}`
36
+ : `${BRAND.PRIMARY}${BRAND.BOLD}🐻 koguma${BRAND.RESET} ${BRAND.DIM}${CLI_VERSION}${BRAND.RESET}`;
37
+ p.intro(label);
38
+ }
39
+
40
+ /** Show a branded outro message. */
41
+ export function outro(message: string): void {
42
+ p.outro(`${BRAND.PRIMARY}${message}${BRAND.RESET}`);
43
+ }
44
+
45
+ /** Handle Clack cancellation — show message and exit. */
46
+ export function handleCancel(value: unknown): value is symbol {
47
+ if (p.isCancel(value)) {
48
+ p.cancel('Operation cancelled.');
49
+ process.exit(0);
50
+ }
51
+ return false;
52
+ }
53
+
54
+ // Re-export Clack for convenience
55
+ export { p };
@@ -0,0 +1,367 @@
1
+ /**
2
+ * cli/wrangler.ts — Wrangler command wrappers.
3
+ *
4
+ * Provides typed helpers for D1 and R2 operations, centralizing
5
+ * the --config flag, --local/--remote targeting, and JSON parsing.
6
+ */
7
+
8
+ import { existsSync, writeFileSync, mkdirSync } from 'fs';
9
+ import { resolve } from 'path';
10
+ import { INIT_SQL } from '../src/db/init.ts';
11
+ import { buildInsertSql, wrapForShell } from '../src/db/sql.ts';
12
+ import { ensureWranglerConfig } from './config.ts';
13
+ import { run, runCapture, runAsync } from './exec.ts';
14
+ import { ok, warn, fail, log } from './log.ts';
15
+ import { DB_DIR, MIGRATION_FILE } from './constants.ts';
16
+
17
+ // ── Preflight checks ───────────────────────────────────────────────
18
+
19
+ /**
20
+ * Verify that `wrangler` can authenticate with Cloudflare.
21
+ * Gives a clear, actionable error message if not logged in.
22
+ */
23
+ export function checkWranglerAuth(root: string): void {
24
+ try {
25
+ runCapture('bunx wrangler whoami', root);
26
+ } catch {
27
+ fail(
28
+ "Not logged in to Cloudflare. Run 'bunx wrangler login' first, " +
29
+ "or run 'koguma init' which handles login for you."
30
+ );
31
+ process.exit(1);
32
+ }
33
+ }
34
+
35
+ // ── Config-aware wrangler execution ────────────────────────────────
36
+
37
+ /**
38
+ * Build the --config flag string. Writes .wrangler.toml if needed.
39
+ */
40
+ function configFlag(root: string): string {
41
+ const configPath = ensureWranglerConfig(root);
42
+ return `--config ${configPath}`;
43
+ }
44
+
45
+ // ── D1 operations ──────────────────────────────────────────────────
46
+
47
+ export type D1Target = '--local' | '--remote';
48
+
49
+ /**
50
+ * Execute a SQL command against D1.
51
+ */
52
+ export function d1Execute(
53
+ root: string,
54
+ dbName: string,
55
+ target: D1Target,
56
+ command: string
57
+ ): void {
58
+ run(
59
+ `bunx wrangler d1 execute ${dbName} ${target} ${configFlag(root)} --command "${wrapForShell(command)}"`,
60
+ { cwd: root, silent: true }
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Execute a SQL file against D1.
66
+ */
67
+ export function d1ExecuteFile(
68
+ root: string,
69
+ dbName: string,
70
+ target: D1Target,
71
+ filePath: string
72
+ ): void {
73
+ run(
74
+ `bunx wrangler d1 execute ${dbName} ${target} ${configFlag(root)} --file=${filePath}`,
75
+ { cwd: root, silent: true }
76
+ );
77
+ }
78
+
79
+ /**
80
+ * Execute a SQL query against D1 and return parsed JSON results.
81
+ */
82
+ export function d1Query(
83
+ root: string,
84
+ dbName: string,
85
+ target: D1Target,
86
+ query: string
87
+ ): Record<string, unknown>[] {
88
+ const output = runCapture(
89
+ `bunx wrangler d1 execute ${dbName} ${target} ${configFlag(root)} --command "${query}" --json`,
90
+ root
91
+ );
92
+ const parsed = JSON.parse(output);
93
+ return parsed?.[0]?.results ?? [];
94
+ }
95
+
96
+ /**
97
+ * Apply the INIT_SQL schema to D1 (idempotent — uses IF NOT EXISTS).
98
+ */
99
+ export function applySchema(
100
+ root: string,
101
+ dbName: string,
102
+ target: D1Target
103
+ ): void {
104
+ const dbDir = resolve(root, DB_DIR);
105
+ if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
106
+
107
+ const sqlFile = resolve(dbDir, MIGRATION_FILE);
108
+ writeFileSync(sqlFile, INIT_SQL);
109
+ d1ExecuteFile(root, dbName, target, sqlFile);
110
+ }
111
+
112
+ /**
113
+ * Insert a row into a D1 table via INSERT OR REPLACE.
114
+ */
115
+ export function d1InsertRow(
116
+ root: string,
117
+ dbName: string,
118
+ target: D1Target,
119
+ table: string,
120
+ row: Record<string, unknown>
121
+ ): void {
122
+ const sql = buildInsertSql(table, row);
123
+ d1Execute(root, dbName, target, sql);
124
+ }
125
+
126
+ /**
127
+ * Batch-insert multiple rows via a single --file execution.
128
+ * Dramatically faster than calling d1InsertRow per entry.
129
+ */
130
+ export function d1InsertBatch(
131
+ root: string,
132
+ dbName: string,
133
+ target: D1Target,
134
+ table: string,
135
+ rows: Record<string, unknown>[]
136
+ ): void {
137
+ if (rows.length === 0) return;
138
+
139
+ const dbDir = resolve(root, DB_DIR);
140
+ if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
141
+
142
+ const sqlFile = resolve(dbDir, 'batch-sync.sql');
143
+ const statements = rows.map(row => buildInsertSql(table, row) + ';');
144
+ writeFileSync(sqlFile, statements.join('\n'));
145
+ d1ExecuteFile(root, dbName, target, sqlFile);
146
+ }
147
+
148
+ // ── Async versions (non-blocking, spinner-friendly) ─────────────────
149
+
150
+ /**
151
+ * Apply schema asynchronously (non-blocking for spinner animation).
152
+ */
153
+ export async function applySchemaAsync(
154
+ root: string,
155
+ dbName: string,
156
+ target: D1Target
157
+ ): Promise<void> {
158
+ const dbDir = resolve(root, DB_DIR);
159
+ if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
160
+
161
+ const sqlFile = resolve(dbDir, MIGRATION_FILE);
162
+ writeFileSync(sqlFile, INIT_SQL);
163
+ await runAsync(
164
+ `bunx wrangler d1 execute ${dbName} ${target} ${configFlag(root)} --file=${sqlFile}`,
165
+ { cwd: root, silent: true }
166
+ );
167
+ }
168
+
169
+ /**
170
+ * Batch-insert rows asynchronously (non-blocking for spinner animation).
171
+ */
172
+ export async function d1InsertBatchAsync(
173
+ root: string,
174
+ dbName: string,
175
+ target: D1Target,
176
+ table: string,
177
+ rows: Record<string, unknown>[]
178
+ ): Promise<void> {
179
+ if (rows.length === 0) return;
180
+
181
+ const dbDir = resolve(root, DB_DIR);
182
+ if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
183
+
184
+ const sqlFile = resolve(dbDir, 'batch-sync.sql');
185
+ const statements = rows.map(row => buildInsertSql(table, row) + ';');
186
+ writeFileSync(sqlFile, statements.join('\n'));
187
+ await runAsync(
188
+ `bunx wrangler d1 execute ${dbName} ${target} ${configFlag(root)} --file=${sqlFile}`,
189
+ { cwd: root, silent: true }
190
+ );
191
+ }
192
+
193
+ /**
194
+ * Execute pre-built SQL statements as a batch file (async, non-blocking).
195
+ * Unlike d1InsertBatchAsync, this takes raw SQL strings — useful when
196
+ * inserting into multiple tables in a single batch.
197
+ */
198
+ export async function d1ExecuteBatchSqlAsync(
199
+ root: string,
200
+ dbName: string,
201
+ target: D1Target,
202
+ statements: string[]
203
+ ): Promise<void> {
204
+ if (statements.length === 0) return;
205
+
206
+ const dbDir = resolve(root, DB_DIR);
207
+ if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
208
+
209
+ const sqlFile = resolve(dbDir, 'batch-sync.sql');
210
+ writeFileSync(sqlFile, statements.map(s => s.endsWith(';') ? s : s + ';').join('\n'));
211
+ await runAsync(
212
+ `bunx wrangler d1 execute ${dbName} ${target} ${configFlag(root)} --file=${sqlFile}`,
213
+ { cwd: root, silent: true }
214
+ );
215
+ }
216
+
217
+ /**
218
+ * Put an object into a local R2 bucket.
219
+ */
220
+ export function r2PutLocal(
221
+ root: string,
222
+ bucketName: string,
223
+ key: string,
224
+ filePath: string
225
+ ): void {
226
+ run(
227
+ `bunx wrangler r2 object put ${bucketName}/${key} ${configFlag(root)} --file=${filePath} --local`,
228
+ { cwd: root, silent: true }
229
+ );
230
+ }
231
+
232
+ /**
233
+ * Put an object into a local R2 bucket (async, non-blocking).
234
+ */
235
+ export async function r2PutLocalAsync(
236
+ root: string,
237
+ bucketName: string,
238
+ key: string,
239
+ filePath: string
240
+ ): Promise<void> {
241
+ await runAsync(
242
+ `bunx wrangler r2 object put ${bucketName}/${key} ${configFlag(root)} --file=${filePath} --local`,
243
+ { cwd: root, silent: true }
244
+ );
245
+ }
246
+
247
+ // ── Wrangler dev / deploy ──────────────────────────────────────────
248
+
249
+ /**
250
+ * Start wrangler dev server with piped output.
251
+ * Filters wrangler's noisy banner and shows koguma-branded messages.
252
+ * Returns a Promise that resolves when wrangler exits.
253
+ */
254
+ export function wranglerDev(
255
+ root: string,
256
+ extraArgs: string,
257
+ env?: Record<string, string>
258
+ ): Promise<number> {
259
+ return new Promise(resolve => {
260
+ const envVars = { ...process.env, ...(env ?? {}) };
261
+ const args = ['wrangler', 'dev', ...configFlag(root).split(' ')];
262
+ if (extraArgs.trim()) args.push(...extraArgs.trim().split(' '));
263
+
264
+ const { spawn } = require('child_process');
265
+ const child = spawn('bunx', args, {
266
+ cwd: root,
267
+ env: envVars,
268
+ stdio: ['inherit', 'pipe', 'pipe'] // stdin passthrough, capture stdout/stderr
269
+ });
270
+
271
+ // Lines to suppress from wrangler output
272
+ const suppressPatterns = [
273
+ /⛅️\s*wrangler/, // wrangler banner
274
+ /─{3,}/, // separator lines
275
+ /Your Worker has access/, // bindings header
276
+ /Binding\s+Resource/, // bindings table header
277
+ /env\./, // binding rows (e.g. "env.DB", "env.MEDIA")
278
+ /Using secrets defined in/, // .dev.vars notice
279
+ /╭.*╮/, // keyboard shortcuts box top
280
+ /│.*\[b\].*\[c\].*\[x\]/, // keyboard shortcuts content
281
+ /╰.*╯/, // keyboard shortcuts box bottom
282
+ /\[wrangler:/, // [wrangler:inf] prefixed messages (handled separately below)
283
+ /^\s*$/ // blank lines
284
+ ];
285
+
286
+ const shouldSuppress = (line: string): boolean =>
287
+ suppressPatterns.some(p => p.test(line));
288
+
289
+ const handleOutput = (data: Buffer, isErr: boolean) => {
290
+ const text = data.toString();
291
+ for (const line of text.split('\n')) {
292
+ const trimmed = line.trim();
293
+ if (!trimmed) continue;
294
+
295
+ // Rebrand the "Ready" message before suppress check
296
+ if (trimmed.includes('Ready on http')) {
297
+ const urlMatch = trimmed.match(/(https?:\/\/[^\s]+)/);
298
+ const url = urlMatch?.[1] ?? 'http://localhost:8787';
299
+ ok(`Server ready → ${url}`);
300
+ continue;
301
+ }
302
+
303
+ if (shouldSuppress(line)) continue;
304
+
305
+ if (trimmed.includes('Starting local server')) continue;
306
+ if (trimmed.includes('Shutting down')) continue;
307
+
308
+ // Forward everything else (request logs, errors, warnings)
309
+ if (isErr) {
310
+ warn(trimmed);
311
+ } else {
312
+ log(trimmed);
313
+ }
314
+ }
315
+ };
316
+
317
+ child.stdout.on('data', (data: Buffer) => handleOutput(data, false));
318
+ child.stderr.on('data', (data: Buffer) => handleOutput(data, true));
319
+
320
+ // Forward SIGINT to wrangler
321
+ const forwardSignal = () => child.kill('SIGINT');
322
+ process.on('SIGINT', forwardSignal);
323
+ process.on('SIGTERM', forwardSignal);
324
+
325
+ child.on('close', (code: number | null) => {
326
+ process.removeListener('SIGINT', forwardSignal);
327
+ process.removeListener('SIGTERM', forwardSignal);
328
+ resolve(code ?? 0);
329
+ });
330
+ });
331
+ }
332
+
333
+ /**
334
+ * Deploy via wrangler.
335
+ */
336
+ export function wranglerDeploy(root: string): void {
337
+ run(`bunx wrangler deploy ${configFlag(root)}`, { cwd: root });
338
+ }
339
+
340
+ // ── D1 / R2 resource creation ──────────────────────────────────────
341
+
342
+ /**
343
+ * Create a D1 database and return the database_id (or null on failure).
344
+ */
345
+ export function createD1Database(root: string, dbName: string): string | null {
346
+ try {
347
+ const output = runCapture(`bunx wrangler d1 create ${dbName}`, root);
348
+ const idMatch = output.match(/database_id\s*=\s*"([^"]+)"/);
349
+ return idMatch?.[1] ?? null;
350
+ } catch {
351
+ return null;
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Ensure an R2 bucket exists. Returns true if it exists or was created.
357
+ */
358
+ export function ensureR2Bucket(root: string, bucketName: string): boolean {
359
+ try {
360
+ const buckets = runCapture('bunx wrangler r2 bucket list', root);
361
+ if (buckets.includes(bucketName)) return true;
362
+ runCapture(`bunx wrangler r2 bucket create ${bucketName}`, root);
363
+ return true;
364
+ } catch {
365
+ return false;
366
+ }
367
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koguma",
3
- "version": "0.6.6",
3
+ "version": "2.0.0",
4
4
  "description": "🐻 A little CMS with big heart — schema-driven, runs on Cloudflare's free tier",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "packageManager": "bun@1.3.10",
24
24
  "scripts": {
25
- "dev": "concurrently --kill-others \"bun run --cwd admin dev\" \"bunx wrangler dev --config examples/alpine/wrangler.toml\"",
25
+ "dev": "concurrently --kill-others \"bun run --cwd admin dev\" \"bun run cli/index.ts dev\"",
26
26
  "test": "bunx turbo run test:run",
27
27
  "test:run": "bun test",
28
28
  "build:admin": "bunx turbo run build:admin:run",
@@ -34,8 +34,7 @@
34
34
  "./worker": "./src/worker.ts",
35
35
  "./client": "./src/client/index.ts",
36
36
  "./react": "./src/react/index.ts",
37
- "./types": "./src/config/types.ts",
38
- "./db": "./src/db/schema.ts"
37
+ "./types": "./src/config/types.ts"
39
38
  },
40
39
  "bin": {
41
40
  "koguma": "./cli/index.ts"
@@ -47,6 +46,10 @@
47
46
  "LICENSE"
48
47
  ],
49
48
  "dependencies": {
49
+ "@clack/prompts": "^1.1.0",
50
+ "gray-matter": "^4.0.3",
51
+ "react-markdown": "^10.1.0",
52
+ "remark-gfm": "^4.0.1",
50
53
  "zod": "^4.3.6"
51
54
  },
52
55
  "peerDependencies": {