koguma 0.6.5 → 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 (43) 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 -1542
  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 -22
  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 -3
  37. package/src/rich-text/lexical-compat.test.ts +0 -513
  38. package/src/rich-text/lexical-to-koguma.test.ts +0 -906
  39. package/src/rich-text/lexical-to-koguma.ts +0 -400
  40. package/src/rich-text/markdown-to-koguma.ts +0 -164
  41. package/src/rich-text/plain.test.ts +0 -208
  42. package/src/rich-text/plain.ts +0 -114
  43. package/src/rich-text/snapshots.test.ts +0 -284
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  <p align="center"><strong>A little CMS with big heart</strong> — schema-driven content management that runs entirely on Cloudflare's free tier.</p>
8
8
 
9
- Koguma gives you a headless CMS with a beautiful admin dashboard, all powered by **Cloudflare Workers + D1 + R2**. Define your content types in code, and Koguma handles the rest — database schema, API routes, admin UI, and media storage.
9
+ Koguma gives you a headless CMS with a beautiful admin dashboard, all powered by **Cloudflare Workers + D1 + R2**. Define your content types in code, and Koguma handles the rest — API routes, admin UI, media storage, and a file-based `content/` directory for version-controlled content.
10
10
 
11
11
  ---
12
12
 
@@ -25,27 +25,25 @@ npm install koguma
25
25
  Create a `site.config.ts` in your project root:
26
26
 
27
27
  ```ts
28
- import { defineConfig, contentType, group, field } from "koguma";
28
+ import { defineConfig, contentType, field } from 'koguma';
29
29
 
30
30
  export default defineConfig({
31
- siteName: "My Site",
31
+ siteName: 'My Site',
32
32
  contentTypes: [
33
- contentType("blogPost", "Blog Post", {
34
- displayField: "title",
35
- groups: [
36
- group({ id: "content", name: "Content", fields: {
37
- title: field.shortText("Title"),
38
- slug: field.shortText("Slug"),
39
- body: field.richText("Body"),
40
- heroImage: field.image("Hero Image"),
41
- }),
42
- group({ id: "meta", name: "Metadata", fields: {
43
- author: field.shortText("Author"),
44
- publishedAt: field.shortText("Published Date"),
45
- }),
46
- ],
47
- }),
48
- ],
33
+ contentType({
34
+ id: 'post',
35
+ name: 'Blog Post',
36
+ displayField: 'title',
37
+ fields: {
38
+ title: field.text('Title').required(),
39
+ slug: field.text('Slug').required(),
40
+ body: field.markdown('Body'),
41
+ heroImage: field.image('Hero Image'),
42
+ published: field.boolean('Published').default(false),
43
+ date: field.date('Published Date')
44
+ }
45
+ })
46
+ ]
49
47
  });
50
48
  ```
51
49
 
@@ -60,37 +58,26 @@ import config from './site.config';
60
58
  export default createWorker(config);
61
59
  ```
62
60
 
63
- ### 4. Configure Cloudflare
61
+ ### 4. Configure
64
62
 
65
- Add to your `wrangler.toml`:
63
+ Create `koguma.toml`:
66
64
 
67
65
  ```toml
68
66
  name = "my-site"
69
- main = "worker.ts"
70
- compatibility_date = "2025-09-27"
71
- compatibility_flags = ["nodejs_compat"]
72
-
73
- [assets]
74
- directory = "./dist"
75
- binding = "ASSETS"
76
-
77
- [[d1_databases]]
78
- binding = "DB"
79
- database_name = "my-site-db"
80
- database_id = "" # filled by `koguma init`
81
-
82
- [[r2_buckets]]
83
- binding = "MEDIA"
84
- bucket_name = "my-site-media"
67
+ database_id = "" # filled by `koguma init`
85
68
  ```
86
69
 
70
+ That's it — two lines. Everything else is derived by convention:
71
+
72
+ - D1 database → `my-site-db`
73
+ - R2 bucket → `my-site-media`
74
+ - Worker name → `my-site`
75
+
87
76
  ### 5. Deploy
88
77
 
89
78
  ```bash
90
- koguma init # Create D1 + R2 on Cloudflare
91
- koguma secret # Set your admin password
92
- koguma seed --remote # Seed the production database
93
- koguma deploy # Build + deploy everything
79
+ koguma init # Scaffold project, create D1 + R2 on Cloudflare
80
+ koguma push # Build + deploy + sync content
94
81
  ```
95
82
 
96
83
  Your admin dashboard is at `/admin`. Your API is at `/api/content/:type`.
@@ -101,56 +88,89 @@ Your admin dashboard is at `/admin`. Your API is at `/api/content/:type`.
101
88
 
102
89
  ### Field Types
103
90
 
104
- | Field | Usage | Stored As |
105
- | --------------------------------- | ---------------------- | ---------------------- |
106
- | `field.shortText(label)` | Titles, slugs, URLs | `TEXT` |
107
- | `field.longText(label)` | Descriptions, bios | `TEXT` |
108
- | `field.richText(label)` | Rich formatted content | `JSON` (document tree) |
109
- | `field.number(label)` | Counts, order | `REAL` |
110
- | `field.boolean(label)` | Toggles | `INTEGER` (0/1) |
111
- | `field.image(label)` | Image from R2 media | `TEXT` (asset ID) |
112
- | `field.reference(label, typeId)` | Link to another entry | `TEXT` (entry ID) |
113
- | `field.references(label, typeId)` | Array of entry links | `JSON` (ID array) |
91
+ | Field | Usage | Stored As |
92
+ | -------------------------------- | ---------------------- | ------------------------ |
93
+ | `field.text(label)` | Titles, slugs, URLs | `TEXT` |
94
+ | `field.longText(label)` | Descriptions, bios | `TEXT` |
95
+ | `field.markdown(label)` | Rich formatted content | `TEXT` (markdown string) |
96
+ | `field.number(label)` | Counts, order | `number` in JSON |
97
+ | `field.boolean(label)` | Toggles | `boolean` in JSON |
98
+ | `field.date(label)` | Timestamps | `string` (ISO 8601) |
99
+ | `field.select(label, {options})` | Dropdowns | `string` |
100
+ | `field.url(label)` | URLs | `string` |
101
+ | `field.email(label)` | Email addresses | `string` |
102
+ | `field.phone(label)` | Phone numbers | `string` |
103
+ | `field.color(label)` | Hex colours | `string` |
104
+ | `field.youtube(label)` | YouTube video IDs | `string` |
105
+ | `field.instagram(label)` | Instagram handles | `string` |
106
+ | `field.image(label)` | Image from R2 media | `string` (asset ID) |
107
+ | `field.images(label)` | Array of images | `string[]` (asset IDs) |
108
+ | `field.ref(typeId, label)` | Link to another entry | `string` (entry ID) |
109
+ | `field.refs(typeId, label)` | Array of entry links | `string[]` (entry IDs) |
110
+
111
+ All fields stored inside a JSON `data` blob in the `entries` table — no per-field columns, no migrations.
114
112
 
115
113
  ### Content Type Options
116
114
 
117
115
  ```ts
118
- contentType("page", "Page", {
119
- displayField: "title", // Field shown in admin list view
116
+ contentType({
117
+ id: "page",
118
+ name: "Page",
119
+ displayField: "title",
120
120
  singleton: true, // Only one entry allowed (e.g. site settings)
121
- groups: [ ... ],
121
+ fields: { ... },
122
122
  });
123
123
  ```
124
124
 
125
- ### Groups
125
+ ---
126
126
 
127
- Groups organize fields into collapsible sections in the admin UI:
127
+ ## CLI Reference
128
128
 
129
- ```ts
130
- group({ id: "details", name: "Details", helpText: "Main content fields", collapsed: false, fields: {
131
- title: field.shortText("Title"),
132
- body: field.richText("Body"),
133
- }, {
134
- helpText: "Main content fields",
135
- collapsed: false, // Start expanded (default)
136
- });
129
+ All commands auto-detect your project root by looking for `koguma.toml`.
130
+
131
+ | Command | Description |
132
+ | ------------------ | ----------------------------------------------------------------- |
133
+ | `koguma init` | Interactive project setup — scaffold, create D1 + R2, set secret |
134
+ | `koguma dev` | Auto-sync `content/` → local D1, generate types, start dev server |
135
+ | `koguma push` | Build + deploy + sync content to remote |
136
+ | `koguma pull` | Download remote content + media → local `content/` files |
137
+ | `koguma gen-types` | Generate `koguma.d.ts` typed interfaces |
138
+ | `koguma tidy` | Validate `content/` against config, sync dirs, check fields |
139
+
140
+ ---
141
+
142
+ ## Content Directory
143
+
144
+ Koguma uses a `content/` directory for file-based content that lives in your git repo:
145
+
146
+ ```
147
+ content/
148
+ ├── post/ # folder = content type ID
149
+ │ ├── hello-world.md # file = one entry
150
+ │ └── our-mission.md
151
+ ├── siteSettings/
152
+ │ └── index.yml # singletons use index.yml
153
+ └── media/ # optional local images
154
+ └── hero-banner.jpg
137
155
  ```
138
156
 
157
+ **Markdown files with frontmatter:**
158
+
159
+ ```markdown
160
+ ---
161
+ title: Our Mission
162
+ slug: our-mission
163
+ heroImage: hero-banner.jpg
164
+ published: true
165
+ date: 2026-03-01
139
166
  ---
140
167
 
141
- ## CLI Reference
168
+ ## Who We Are
142
169
 
143
- All commands auto-detect your project root by looking for `wrangler.toml`.
170
+ Rich text body, stored as markdown.
171
+ ```
144
172
 
145
- | Command | Description |
146
- | ----------------------------------- | ------------------------------------------------------------------------------------------ |
147
- | `koguma init` | Create D1 database and R2 bucket on Cloudflare, patch `wrangler.toml` with the database ID |
148
- | `koguma secret` | Set `KOGUMA_SECRET` (admin password) as a Cloudflare secret |
149
- | `koguma build` | Build the admin dashboard (Vite) and generate the `_bundle.ts` |
150
- | `koguma seed` | Run `db/seed.sql` against local D1 |
151
- | `koguma seed --remote` | Run `db/seed.sql` against production D1 |
152
- | `koguma migrate-media --remote URL` | Download images from source, upload to R2, update D1 URLs |
153
- | `koguma deploy` | Build admin + frontend, then `wrangler deploy` |
173
+ **Git is your version history.** No custom versioning table needed.
154
174
 
155
175
  ---
156
176
 
@@ -181,26 +201,6 @@ All commands auto-detect your project root by looking for `wrangler.toml`.
181
201
  | `POST` | `/api/admin/media` | Upload media (multipart form) |
182
202
  | `DELETE` | `/api/admin/media/:id` | Delete media |
183
203
 
184
- ### Response Format
185
-
186
- ```json
187
- // GET /api/content/blogPost
188
- {
189
- "entries": [
190
- {
191
- "id": "abc-123",
192
- "title": "Hello World",
193
- "heroImage": {
194
- "id": "img-456",
195
- "url": "/api/media/img-456.jpg",
196
- "title": "Hero",
197
- "content_type": "image/jpeg"
198
- }
199
- }
200
- ]
201
- }
202
- ```
203
-
204
204
  References and images are automatically resolved to nested objects in the public API (up to 2 levels deep).
205
205
 
206
206
  ---
@@ -208,12 +208,11 @@ References and images are automatically resolved to nested objects in the public
208
208
  ## Package Exports
209
209
 
210
210
  ```ts
211
- import { defineConfig, contentType, group, field, Infer } from 'koguma';
211
+ import { defineConfig, contentType, group, field, type Infer } from 'koguma';
212
212
  import { createWorker } from 'koguma/worker';
213
213
  import { createClient } from 'koguma/client';
214
- import { useKoguma } from 'koguma/react';
215
- import type { RichTextDocument, RichTextNode } from 'koguma/types';
216
- import { generateSchema } from 'koguma/db';
214
+ import { Markdown, useEntry, useEntries } from 'koguma/react';
215
+ import type { KogumaAsset, EntryReference } from 'koguma/types';
217
216
  ```
218
217
 
219
218
  ---
@@ -224,8 +223,8 @@ import { generateSchema } from 'koguma/db';
224
223
  # Create .dev.vars with your local admin password
225
224
  echo "KOGUMA_SECRET=your-password" > .dev.vars
226
225
 
227
- # Start wrangler dev
228
- npx wrangler dev
226
+ # Start dev server (auto-syncs content/, generates types)
227
+ koguma dev
229
228
 
230
229
  # Admin dashboard is at http://localhost:8787/admin
231
230
  # API is at http://localhost:8787/api/content/:type
@@ -239,59 +238,30 @@ npx wrangler dev
239
238
  Your Project
240
239
  ├── site.config.ts ← Content type definitions
241
240
  ├── worker.ts ← Entry point (imports koguma/worker)
242
- ├── wrangler.toml Cloudflare config (D1 + R2 bindings)
241
+ ├── koguma.toml Project config (name + database_id)
243
242
  ├── .dev.vars ← Local secrets (KOGUMA_SECRET)
244
- └── db/
245
- ├── seed.sql ← Database schema + seed data
246
- └── seed.json ← Raw content data
243
+ └── content/ ← Version-controlled content files
244
+ ├── post/
245
+ └── hello-world.md
246
+ └── siteSettings/
247
+ └── index.yml
247
248
 
248
249
  Koguma (this package)
249
250
  ├── src/
250
251
  │ ├── config/ ← Schema definitions (defineConfig, field types)
251
252
  │ ├── api/ ← Hono router (CRUD + media + auth)
252
- │ ├── db/ ← D1 queries and schema generation
253
+ │ ├── db/ ← D1 queries (JSON document store)
253
254
  │ ├── auth/ ← HMAC-signed cookie sessions
254
255
  │ ├── media/ ← R2 upload/serve/delete
255
256
  │ ├── admin/ ← Dashboard HTML shell + JS/CSS bundle
256
257
  │ ├── client/ ← Fetch client for consuming the API
257
- │ └── react/ ← React hooks (useKoguma)
258
+ │ └── react/ ← React hooks + <Markdown> component
258
259
  ├── admin/ ← Vite + React admin dashboard source
259
- └── cli/ ← CLI commands (init, build, seed, deploy)
260
+ └── cli/ ← CLI commands (init, dev, push, pull, gen-types, tidy)
260
261
  ```
261
262
 
262
263
  ---
263
264
 
264
- ## Roadmap
265
-
266
- ### v0.2 — Admin Power-ups
267
-
268
- - [ ] **Rich text editor** — replace JSON textarea with Tiptap WYSIWYG (bold, italic, headings, links, lists, inline images)
269
- - [ ] **Media picker** — browse/upload media from within entry editor, image preview thumbnails
270
- - [ ] **Entry list search & sort** — filter by display field, sortable columns, pagination
271
- - [ ] **Unsaved changes warning** — dirty form detection, Cmd+S to save
272
- - [ ] **Breadcrumb navigation** — show current path in the editor toolbar
273
-
274
- ### v0.3 — Content Workflow
275
-
276
- - [ ] **Draft / publish** — entries have `status: draft | published`, public API filters by default
277
- - [ ] **Content versioning** — save revision history, view diffs, one-click rollback
278
- - [ ] **Field validation** — required fields, min/max length, regex, custom validators
279
-
280
- ### v0.4 — Auth & Multi-site
281
-
282
- - [ ] **Multi-user auth** — user accounts, roles (admin/editor/viewer), audit log
283
- - [ ] **Multi-site support** — single install, per-site config and content isolation
284
-
285
- ### v0.5 — DX & Media
286
-
287
- - [ ] **Image optimization** — auto-resize, WebP/AVIF conversion, responsive srcset
288
- - [ ] **Webhooks** — fire on content changes, configurable URLs, retry logic
289
- - [ ] **CLI: export/import** — dump and restore content as JSON
290
- - [ ] **CLI: schema diff & migrate** — detect config ↔ DB drift, apply changes safely
291
- - [ ] **Typed SDK** — auto-generate TypeScript types from `site.config.ts`
292
-
293
- ---
294
-
295
265
  ## License
296
266
 
297
267
  MIT
package/cli/auth.ts ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * cli/auth.ts — Authentication helpers for remote Koguma operations.
3
+ *
4
+ * Handles reading KOGUMA_SECRET from .dev.vars and authenticating
5
+ * against a remote Koguma instance via the admin API.
6
+ */
7
+
8
+ import { existsSync, readFileSync } from 'fs';
9
+ import { resolve } from 'path';
10
+ import { fail } from './log.ts';
11
+
12
+ const SECRET_ENV_VAR = 'KOGUMA_SECRET';
13
+ const DEV_VARS_FILE = '.dev.vars';
14
+ const SESSION_COOKIE_NAME = 'koguma_session';
15
+
16
+ // ── Secret resolution ──────────────────────────────────────────────
17
+
18
+ /**
19
+ * Read KOGUMA_SECRET from the project's .dev.vars file.
20
+ * Exits with an actionable error if not found.
21
+ */
22
+ export function requireSecret(root: string): string {
23
+ const devVarsPath = resolve(root, DEV_VARS_FILE);
24
+ if (!existsSync(devVarsPath)) {
25
+ fail(
26
+ `${DEV_VARS_FILE} not found. Create it with:\n` +
27
+ ` echo '${SECRET_ENV_VAR}=your-password' > ${DEV_VARS_FILE}`
28
+ );
29
+ process.exit(1);
30
+ }
31
+
32
+ const content = readFileSync(devVarsPath, 'utf-8');
33
+ const match = content.match(new RegExp(`${SECRET_ENV_VAR}=(.+)`));
34
+ const password = match?.[1]?.trim();
35
+
36
+ if (!password) {
37
+ fail(
38
+ `${SECRET_ENV_VAR} not found in ${DEV_VARS_FILE}. Add it:\n` +
39
+ ` echo '${SECRET_ENV_VAR}=your-password' >> ${DEV_VARS_FILE}`
40
+ );
41
+ process.exit(1);
42
+ }
43
+
44
+ return password;
45
+ }
46
+
47
+ // ── Remote authentication ──────────────────────────────────────────
48
+
49
+ /**
50
+ * Authenticate against a remote Koguma instance.
51
+ * Returns the session cookie string for use in subsequent requests.
52
+ */
53
+ export async function authenticate(
54
+ remoteUrl: string,
55
+ root: string
56
+ ): Promise<string> {
57
+ const password = requireSecret(root);
58
+
59
+ const loginRes = await fetch(`${remoteUrl}/api/auth/login`, {
60
+ method: 'POST',
61
+ headers: { 'Content-Type': 'application/json' },
62
+ body: JSON.stringify({ password }),
63
+ redirect: 'manual'
64
+ });
65
+
66
+ const setCookie = loginRes.headers.get('set-cookie') ?? '';
67
+ const cookieMatch = setCookie.match(
68
+ new RegExp(`${SESSION_COOKIE_NAME}=[^;]+`)
69
+ );
70
+
71
+ if (!cookieMatch) {
72
+ fail(
73
+ 'Authentication failed — check your KOGUMA_SECRET.\n' +
74
+ ` Tried: ${remoteUrl}/api/auth/login`
75
+ );
76
+ process.exit(1);
77
+ }
78
+
79
+ return cookieMatch[0];
80
+ }
81
+
82
+ // ── Remote URL parsing ─────────────────────────────────────────────
83
+
84
+ /**
85
+ * Extract the --remote URL from process.argv.
86
+ * Exits with usage info if missing or malformed.
87
+ */
88
+ export function getRemoteUrl(): string {
89
+ const idx = process.argv.indexOf('--remote');
90
+ const url = idx >= 0 ? process.argv[idx + 1] : undefined;
91
+
92
+ if (!url || url.startsWith('-')) {
93
+ fail(
94
+ 'Missing remote URL.\n' +
95
+ ' Usage: koguma <command> --remote https://your-site.workers.dev'
96
+ );
97
+ process.exit(1);
98
+ }
99
+
100
+ return url.replace(/\/$/, '');
101
+ }
package/cli/config.ts ADDED
@@ -0,0 +1,149 @@
1
+ /**
2
+ * cli/config.ts — koguma.toml + .wrangler.toml generation
3
+ *
4
+ * koguma.toml has two fields:
5
+ * name = "my-blog"
6
+ * database_id = "" # filled by koguma init
7
+ *
8
+ * Everything else is derived from the name:
9
+ * D1 database name → {name}-db
10
+ * R2 bucket → {name}-media
11
+ * Worker name → {name}
12
+ */
13
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
14
+ import { resolve, dirname } from 'path';
15
+
16
+ // ── Types ──────────────────────────────────────────────────────────
17
+
18
+ export interface KogumaProjectConfig {
19
+ name: string;
20
+ databaseId: string;
21
+ }
22
+
23
+ export interface DerivedNames {
24
+ workerName: string;
25
+ dbName: string;
26
+ bucketName: string;
27
+ }
28
+
29
+ // ── Derive conventional names from project name ────────────────────
30
+
31
+ export function deriveNames(name: string): DerivedNames {
32
+ return {
33
+ workerName: name,
34
+ dbName: `${name}-db`,
35
+ bucketName: `${name}-media`
36
+ };
37
+ }
38
+
39
+ // ── Read / Write koguma.toml ────────────────────────────────────────
40
+
41
+ export function readConfig(root: string): KogumaProjectConfig {
42
+ const path = resolve(root, 'koguma.toml');
43
+ if (!existsSync(path)) {
44
+ throw new Error(`koguma.toml not found in ${root}`);
45
+ }
46
+ const raw = readFileSync(path, 'utf-8');
47
+
48
+ const nameMatch = raw.match(/^name\s*=\s*"([^"]*)"/m);
49
+ const dbIdMatch = raw.match(/^database_id\s*=\s*"([^"]*)"/m);
50
+
51
+ return {
52
+ name: nameMatch?.[1] ?? '',
53
+ databaseId: dbIdMatch?.[1] ?? ''
54
+ };
55
+ }
56
+
57
+ export function writeConfig(root: string, config: KogumaProjectConfig): void {
58
+ const path = resolve(root, 'koguma.toml');
59
+ const content = `name = "${config.name}"\ndatabase_id = "${config.databaseId}"\n`;
60
+ writeFileSync(path, content);
61
+ }
62
+
63
+ // ── Generate transient .koguma/ contents ───────────────────────────
64
+
65
+ export function generateWranglerToml(config: KogumaProjectConfig): string {
66
+ const { workerName, dbName, bucketName } = deriveNames(config.name);
67
+
68
+ // Paths are relative to .koguma/ where this config lives
69
+ return `# Auto-generated by koguma — do not edit
70
+ name = "${workerName}"
71
+ main = "worker.ts"
72
+ compatibility_date = "2024-11-01"
73
+ compatibility_flags = ["nodejs_compat"]
74
+
75
+ [assets]
76
+ directory = "./dashboard-public"
77
+ binding = "ASSETS"
78
+
79
+ # ── D1 Database ──
80
+ [[d1_databases]]
81
+ binding = "DB"
82
+ database_name = "${dbName}"
83
+ database_id = "${config.databaseId}"
84
+
85
+ # ── R2 Media Storage ──
86
+ [[r2_buckets]]
87
+ binding = "MEDIA"
88
+ bucket_name = "${bucketName}"
89
+ `;
90
+ }
91
+
92
+ function generateWorkerEntry(): string {
93
+ return `// Auto-generated by koguma — do not edit
94
+ import { createWorker } from 'koguma/worker';
95
+ import config from '../site.config';
96
+
97
+ export default createWorker(config);
98
+ `;
99
+ }
100
+
101
+ export function ensureWranglerConfig(root: string): string {
102
+ const config = readConfig(root);
103
+ const kogumaDir = resolve(root, '.koguma');
104
+
105
+ // Create .koguma/ and .koguma/dashboard-public/
106
+ mkdirSync(kogumaDir, { recursive: true });
107
+ const dashDir = resolve(kogumaDir, 'dashboard-public');
108
+ if (!existsSync(dashDir)) {
109
+ mkdirSync(dashDir, { recursive: true });
110
+ }
111
+
112
+ // Write wrangler.toml
113
+ const wranglerPath = resolve(kogumaDir, 'wrangler.toml');
114
+ writeFileSync(wranglerPath, generateWranglerToml(config));
115
+
116
+ // Write worker.ts entry point
117
+ const workerPath = resolve(kogumaDir, 'worker.ts');
118
+ writeFileSync(workerPath, generateWorkerEntry());
119
+
120
+ // Write .dev.vars with a local dev secret (only if not already present,
121
+ // so users can override with their own password)
122
+ const devVarsPath = resolve(kogumaDir, '.dev.vars');
123
+ if (!existsSync(devVarsPath)) {
124
+ writeFileSync(devVarsPath, 'KOGUMA_SECRET=koguma-dev-local\n');
125
+ }
126
+
127
+ return wranglerPath;
128
+ }
129
+
130
+ // ── Find project root ──────────────────────────────────────────────
131
+
132
+ export function findProjectRoot(): string {
133
+ let dir = process.cwd();
134
+ for (let i = 0; i < 10; i++) {
135
+ if (existsSync(resolve(dir, 'koguma.toml'))) return dir;
136
+ const parent = dirname(dir);
137
+ if (parent === dir) break;
138
+ dir = parent;
139
+ }
140
+ throw new Error(
141
+ 'Could not find koguma.toml — run this from your project directory.'
142
+ );
143
+ }
144
+
145
+ // ── Scaffold koguma.toml for new projects ──────────────────────────
146
+
147
+ export function generateKogumaToml(projectName: string): string {
148
+ return `name = "${projectName}"\ndatabase_id = ""\n`;
149
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * cli/constants.ts — Shared constants for the Koguma CLI.
3
+ *
4
+ * Eliminates magic strings scattered across modules.
5
+ */
6
+
7
+ /** CLI version string */
8
+ export const CLI_VERSION = 'v2.0.0';
9
+
10
+ /** Project config file name */
11
+ export const CONFIG_FILE = 'koguma.toml';
12
+
13
+ /** Auto-generated directory (gitignored) */
14
+ export const KOGUMA_DIR = '.koguma';
15
+
16
+ /** Generated wrangler config inside KOGUMA_DIR */
17
+ export const WRANGLER_CONFIG_FILE = `${KOGUMA_DIR}/wrangler.toml`;
18
+
19
+ /** Generated worker entry point inside KOGUMA_DIR */
20
+ export const WORKER_ENTRY = `${KOGUMA_DIR}/worker.ts`;
21
+
22
+ /** Dashboard static assets directory inside KOGUMA_DIR */
23
+ export const DASHBOARD_DIR = `${KOGUMA_DIR}/dashboard-public`;
24
+
25
+ /** Generated TypeScript declarations */
26
+ export const TYPEGEN_OUTPUT = 'koguma.d.ts';
27
+
28
+ /** Site configuration module */
29
+ export const SITE_CONFIG_FILE = 'site.config.ts';
30
+
31
+ /** Content directory name */
32
+ export const CONTENT_DIR = 'content';
33
+
34
+ /** Temporary SQL directory (inside .koguma — transient, gitignored) */
35
+ export const DB_DIR = `${KOGUMA_DIR}/db`;
36
+
37
+ /** Temporary migration file within DB_DIR */
38
+ export const MIGRATION_FILE = 'migration.sql';