koguma 0.6.6 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -139
- package/cli/auth.ts +101 -0
- package/cli/config.ts +149 -0
- package/cli/constants.ts +38 -0
- package/cli/content.ts +506 -0
- package/cli/dev-sync.ts +305 -0
- package/cli/exec.ts +61 -0
- package/cli/index.ts +779 -1545
- package/cli/log.ts +49 -0
- package/cli/preflight.ts +105 -0
- package/cli/scaffold.ts +680 -0
- package/cli/typegen.ts +190 -0
- package/cli/ui.ts +55 -0
- package/cli/wrangler.ts +367 -0
- package/package.json +7 -4
- package/src/admin/_bundle.ts +1 -1
- package/src/api/router.integration.test.ts +63 -80
- package/src/api/router.ts +85 -59
- package/src/config/define.ts +1 -1
- package/src/config/field.ts +10 -9
- package/src/config/index.ts +1 -13
- package/src/config/meta.ts +7 -7
- package/src/config/types.ts +1 -95
- package/src/db/init.ts +68 -0
- package/src/db/queries.ts +120 -211
- package/src/db/sql.ts +10 -25
- package/src/media/index.ts +105 -47
- package/src/react/Markdown.test.tsx +195 -0
- package/src/react/Markdown.tsx +40 -0
- package/src/react/index.ts +6 -22
- package/src/react/types.ts +3 -112
- package/src/db/migrate.ts +0 -182
- package/src/db/schema.ts +0 -122
- package/src/react/RichText.test.tsx +0 -535
- package/src/react/RichText.tsx +0 -350
- package/src/rich-text/index.ts +0 -4
- package/src/rich-text/koguma-to-lexical.ts +0 -340
- package/src/rich-text/lexical-compat.test.ts +0 -513
- package/src/rich-text/lexical-to-koguma.test.ts +0 -906
- package/src/rich-text/lexical-to-koguma.ts +0 -400
- package/src/rich-text/markdown-to-koguma.ts +0 -164
- package/src/rich-text/plain.test.ts +0 -208
- package/src/rich-text/plain.ts +0 -114
- 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 —
|
|
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,
|
|
28
|
+
import { defineConfig, contentType, field } from 'koguma';
|
|
29
29
|
|
|
30
30
|
export default defineConfig({
|
|
31
|
-
siteName:
|
|
31
|
+
siteName: 'My Site',
|
|
32
32
|
contentTypes: [
|
|
33
|
-
contentType(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
61
|
+
### 4. Configure
|
|
64
62
|
|
|
65
|
-
|
|
63
|
+
Create `koguma.toml`:
|
|
66
64
|
|
|
67
65
|
```toml
|
|
68
66
|
name = "my-site"
|
|
69
|
-
|
|
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
|
|
91
|
-
koguma
|
|
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
|
|
105
|
-
|
|
|
106
|
-
| `field.
|
|
107
|
-
| `field.longText(label)`
|
|
108
|
-
| `field.
|
|
109
|
-
| `field.number(label)`
|
|
110
|
-
| `field.boolean(label)`
|
|
111
|
-
| `field.
|
|
112
|
-
| `field.
|
|
113
|
-
| `field.
|
|
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(
|
|
119
|
-
|
|
116
|
+
contentType({
|
|
117
|
+
id: "page",
|
|
118
|
+
name: "Page",
|
|
119
|
+
displayField: "title",
|
|
120
120
|
singleton: true, // Only one entry allowed (e.g. site settings)
|
|
121
|
-
|
|
121
|
+
fields: { ... },
|
|
122
122
|
});
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
---
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
## CLI Reference
|
|
128
128
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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.md # singletons use index.md
|
|
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
|
-
##
|
|
168
|
+
## Who We Are
|
|
142
169
|
|
|
143
|
-
|
|
170
|
+
Rich text body, stored as markdown.
|
|
171
|
+
```
|
|
144
172
|
|
|
145
|
-
|
|
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 {
|
|
215
|
-
import type {
|
|
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
|
|
228
|
-
|
|
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
|
-
├──
|
|
241
|
+
├── koguma.toml ← Project config (name + database_id)
|
|
243
242
|
├── .dev.vars ← Local secrets (KOGUMA_SECRET)
|
|
244
|
-
└──
|
|
245
|
-
├──
|
|
246
|
-
└──
|
|
243
|
+
└── content/ ← Version-controlled content files
|
|
244
|
+
├── post/
|
|
245
|
+
│ └── hello-world.md
|
|
246
|
+
└── siteSettings/
|
|
247
|
+
└── index.md
|
|
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
|
|
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
|
|
258
|
+
│ └── react/ ← React hooks + <Markdown> component
|
|
258
259
|
├── admin/ ← Vite + React admin dashboard source
|
|
259
|
-
└── cli/ ← CLI commands (init,
|
|
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
|
+
}
|
package/cli/constants.ts
ADDED
|
@@ -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';
|