@beechcms/cms 0.6.7 → 0.8.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/bin/cli.mjs CHANGED
@@ -1,10 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // @ts-check
3
3
 
4
- import { spawnSync } from 'node:child_process'
5
- import { existsSync } from 'node:fs'
6
- import { resolve } from 'node:path'
7
- import { pathToFileURL } from 'node:url'
8
4
  import pc from 'picocolors'
9
5
 
10
6
  let [,, command, ...args] = process.argv
@@ -13,6 +9,14 @@ if (command && args[0] && ['db', 'seed', 'schema', 'dev', 'generate', 'mailpit']
13
9
  command = `${command}:${args.shift()}`
14
10
  }
15
11
 
12
+ if (command === 'gen' && args[0] === 'types') {
13
+ command = 'gen-types'
14
+ args.shift()
15
+ if (args[0] === 'typescript') {
16
+ args.shift()
17
+ }
18
+ }
19
+
16
20
  const COMMANDS = {
17
21
  build: cmdBuild,
18
22
  'seed:load': cmdSeedLoad,
@@ -24,7 +28,12 @@ const COMMANDS = {
24
28
  'update': cmdUpdate,
25
29
  'onboard': cmdOnboard,
26
30
  'reset': cmdReset,
27
- 'generate:types': cmdGenerateTypes,
31
+ 'forms': cmdForms,
32
+ 'form': cmdForms,
33
+ 'forms:add': cmdForms,
34
+ 'gen-types': cmdGenerateTypes,
35
+ 'gen:types': cmdGenerateTypes,
36
+ 'generate:types': cmdGenerateTypes,
28
37
  // New unified command mappings:
29
38
  'db:migrate': cmdDbMigrate,
30
39
  'db:reset': cmdDbReset,
@@ -38,6 +47,8 @@ const COMMANDS = {
38
47
  'test': cmdTest,
39
48
  'lint': cmdLint,
40
49
  'doctor': cmdDoctor,
50
+ 'setup:cloudflare': cmdSetupCloudflare,
51
+ 'setup:cf': cmdSetupCloudflare,
41
52
  }
42
53
 
43
54
  function help() {
@@ -50,7 +61,7 @@ function help() {
50
61
  --remote Target remote D1 instead of local (default: local)
51
62
  --db-name <n> Override D1 database name
52
63
  --yes, -y Run in non-interactive mode
53
- ${pc.cyan('onboard')} One-command local provisioning (init --db + seed:load)
64
+ ${pc.cyan('onboard')} One-command local provisioning (init --db)
54
65
  --remote Target remote D1 instead of local (default: local)
55
66
  --yes, -y Skip all interactive prompts (non-interactive mode)
56
67
  --db <name> Override D1 database name
@@ -60,24 +71,23 @@ function help() {
60
71
  ${pc.cyan('db:migrate')} Apply all pending local migrations
61
72
  ${pc.cyan('db:reset')} Remove local Wrangler state and re-bootstrap database
62
73
 
63
- ${pc.bold('3. Seed & Schema Management')}
64
- ${pc.cyan('seed:create')} Interactive wizard — generate a new Seed schema in seeds.ts
65
- ${pc.cyan('seed:load')} Create/update content tables from SEED_REGISTRY
66
- --dry-run Print SQL without executing
67
- --diff Show schema differences vs current DB
68
- --remote Execute against remote D1 (default: local)
69
- --db <name> Override D1 database name
70
- ${pc.cyan('schema:diff')} Diff SEED_REGISTRY vs D1 and generate additive SQL migration
71
- --write Write the migration file (default: preview only)
72
- --name <name> Migration name used in the filename
73
- --remote Diff against remote D1 (default: local)
74
+ ${pc.bold('3. Database & Types Management')}
75
+ ${pc.cyan('gen types typescript')} (alias: ${pc.cyan('gen-types')})
76
+ Generate TypeScript interfaces from active D1 database
77
+ --local Target local D1 SQLite state (default)
78
+ --remote Target remote Cloudflare D1
74
79
  --db <name> Override D1 database name
75
- ${pc.cyan('validate')} Validate seeds registry for errors
76
- ${pc.cyan('generate:types')} Generate TypeScript interfaces from seed definitions
77
- --out <path> Output file (default: src/types/beech.ts)
78
- --local Read from seeds.ts instead of querying live D1
80
+ -o, --output Output file path (default: standard output)
81
+ ${pc.cyan('validate')} Validate runtime schema status
79
82
 
80
- ${pc.bold('4. Local Stack & Docker')}
83
+ ${pc.bold('4. Forms & Frontend Generation')}
84
+ ${pc.cyan('forms / form')} Interactive wizard to generate React, Vue, Svelte, or Web Component forms
85
+ --framework <f> Framework: react, vue, svelte, vanilla
86
+ --seed <slug> Seed slug to bind to (e.g. clienti)
87
+ --mode <mode> styled (Tailwind) or headless
88
+ --yes, -y Skip interactive prompts
89
+
90
+ ${pc.bold('5. Local Stack & Docker')}
81
91
  ${pc.cyan('dev / start')} Start the local dev environment (Docker + API + Dashboard)
82
92
  --plain Avoid Ink visual TUI and run clean log streaming
83
93
  ${pc.cyan('dev:stop')} Stop Docker containers without wiping data
@@ -85,16 +95,19 @@ function help() {
85
95
  ${pc.cyan('dev:tunnel')} Display Cloudflare tunnel public testing URL
86
96
  ${pc.cyan('mailpit:clear')} Clear local test inbox in Mailpit
87
97
 
88
- ${pc.bold('5. Logs Streaming')}
98
+ ${pc.bold('6. Logs Streaming')}
89
99
  ${pc.cyan('logs <service>')} Show streaming logs for docker service: mailpit, db, tunnel, storage
90
100
 
91
- ${pc.bold('6. Quality & Deployment')}
101
+ ${pc.bold('7. Quality & Deployment')}
92
102
  ${pc.cyan('test')} Run the test suite via Turborepo / Vitest
93
103
  --coverage Generate coverage reports
94
104
  --diff Run test coverage only for files modified on the branch
95
105
  ${pc.cyan('lint')} Run ESLint quality checks
106
+ ${pc.cyan('setup:cloudflare')} (alias: ${pc.cyan('setup:cf')})
107
+ Interactive 1-step Cloudflare provisioning (D1, R2, Presigned S3 secrets)
108
+ --name <n> Project name override
109
+ --yes, -y Non-interactive mode
96
110
  ${pc.cyan('deploy')} Compile, test, deploy to Cloudflare environment
97
- --skip-seed Skip remote seed:load step
98
111
  --skip-check Skip /admin reachability check
99
112
  ${pc.cyan('doctor')} Execute React diagnostics check on Dashboard
100
113
  `)
@@ -103,78 +116,10 @@ function help() {
103
116
  function cmdBuild() {
104
117
  console.log(
105
118
  '\nNo build step needed for BeechCMS projects.\n' +
106
- 'Edit seeds.ts then run `npx beech seed:load` to sync schema changes to D1.\n'
119
+ 'Manage content types dynamically in the BeechCMS Dashboard at /admin.\n'
107
120
  )
108
121
  }
109
122
 
110
- async function tryLoadLocalRegistry() {
111
- const cwd = process.cwd()
112
-
113
- // Try compiled JS first (root or apps/api)
114
- const searchDirs = [cwd, resolve(cwd, 'apps', 'api')]
115
- for (const dir of searchDirs) {
116
- for (const name of ['seeds.js', 'seeds.mjs', 'seed.js', 'seed.mjs']) {
117
- const p = resolve(dir, name)
118
- if (existsSync(p)) {
119
- try {
120
- const mod = await import(pathToFileURL(p).href)
121
- if (mod.SEED_REGISTRY && typeof mod.SEED_REGISTRY === 'object') {
122
- return mod.SEED_REGISTRY
123
- }
124
- if (Array.isArray(mod.seeds)) {
125
- return Object.fromEntries(mod.seeds.map(s => [s.slug, s]))
126
- }
127
- if (Array.isArray(mod.default)) {
128
- return Object.fromEntries(mod.default.map(s => [s.slug, s]))
129
- }
130
- } catch {}
131
- }
132
- }
133
- }
134
-
135
- // Try seeds.ts / seed.ts (root or apps/api)
136
- let tsPath = null
137
- for (const dir of searchDirs) {
138
- const p = existsSync(resolve(dir, 'seeds.ts'))
139
- ? resolve(dir, 'seeds.ts')
140
- : resolve(dir, 'seed.ts')
141
- if (existsSync(p)) {
142
- tsPath = p
143
- break
144
- }
145
- }
146
-
147
- if (tsPath) {
148
- const result = spawnSync(process.execPath, [
149
- '--experimental-strip-types',
150
- '--input-type=module',
151
- '--eval',
152
- `
153
- import * as mod from ${JSON.stringify(pathToFileURL(tsPath).href)};
154
- let out = null;
155
- if (mod.SEED_REGISTRY && typeof mod.SEED_REGISTRY === 'object') out = mod.SEED_REGISTRY;
156
- else if (Array.isArray(mod.seeds)) out = Object.fromEntries(mod.seeds.map(s => [s.slug, s]));
157
- else if (Array.isArray(mod.default)) out = Object.fromEntries(mod.default.map(s => [s.slug, s]));
158
- if (out) process.stdout.write(JSON.stringify(out));
159
- `.trim(),
160
- ], { encoding: 'utf-8' })
161
-
162
- if (result.status === 0 && result.stdout) {
163
- try { return JSON.parse(result.stdout) } catch (err) {
164
- console.error(' Failed to parse seeds.ts output:', err)
165
- }
166
- } else if (result.status !== 0) {
167
- console.error(' Error loading seeds.ts:')
168
- console.error(result.stderr || result.stdout || 'Unknown error')
169
- if (process.version.slice(1).split('.')[0] < 22) {
170
- console.warn(' Note: Node.js 22.6+ is required to load .ts files directly. Current version:', process.version)
171
- }
172
- }
173
- }
174
-
175
- return null
176
- }
177
-
178
123
  async function cmdInit(args) {
179
124
  const initDb = args.includes('--db')
180
125
  const remote = args.includes('--remote')
@@ -183,26 +128,17 @@ async function cmdInit(args) {
183
128
  const yes = args.includes('--yes') || args.includes('-y')
184
129
 
185
130
  const { init } = await import('@beechcms/cli')
186
- await init({ initDb, local: !remote, db, yes })
131
+ await init({ initDb, local: !remote, db, nonInteractive: yes })
187
132
  }
188
133
 
189
- async function cmdSeedLoad(args) {
190
- const dryRun = args.includes('--dry-run')
191
- const diff = args.includes('--diff')
192
- const remote = args.includes('--remote')
193
- const dbIdx = args.indexOf('--db')
194
- const db = dbIdx !== -1 ? args[dbIdx + 1] : undefined
195
-
196
- const registry = await tryLoadLocalRegistry()
197
-
134
+ async function cmdSeedLoad(_args) {
198
135
  const { seedLoad } = await import('@beechcms/cli')
199
- await seedLoad({ dryRun, diff, local: !remote, db, registry })
136
+ await seedLoad({})
200
137
  }
201
138
 
202
- async function cmdValidate(args) {
203
- const registry = await tryLoadLocalRegistry()
139
+ async function cmdValidate(_args) {
204
140
  const { validate } = await import('@beechcms/cli')
205
- await validate({ registry })
141
+ await validate({})
206
142
  }
207
143
 
208
144
  async function cmdSeedCreate(_args) {
@@ -213,9 +149,8 @@ async function cmdSeedCreate(_args) {
213
149
  async function cmdDeploy(args) {
214
150
  const skipSeed = args.includes('--skip-seed')
215
151
  const skipCheck = args.includes('--skip-check')
216
- const registry = skipSeed ? null : await tryLoadLocalRegistry()
217
152
  const { deploy } = await import('@beechcms/cli')
218
- await deploy({ registry, skipSeed, skipCheck })
153
+ await deploy({ skipSeed, skipCheck })
219
154
  }
220
155
 
221
156
  async function cmdUpdate(_args) {
@@ -228,9 +163,8 @@ async function cmdOnboard(args) {
228
163
  const yes = args.includes('--yes') || args.includes('-y')
229
164
  const dbIdx = args.indexOf('--db')
230
165
  const db = dbIdx !== -1 ? args[dbIdx + 1] : undefined
231
- const registry = await tryLoadLocalRegistry()
232
166
  const { onboard } = await import('@beechcms/cli')
233
- await onboard({ local, yes, db, registry })
167
+ await onboard({ local, yes, db })
234
168
  }
235
169
 
236
170
  async function cmdReset(args) {
@@ -250,22 +184,44 @@ async function cmdSchemaDiff(args) {
250
184
  const name = nameIdx !== -1 ? args[nameIdx + 1] : undefined
251
185
  const dbIdx = args.indexOf('--db')
252
186
  const db = dbIdx !== -1 ? args[dbIdx + 1] : undefined
253
- const registry = await tryLoadLocalRegistry()
254
187
  const { schemaDiff } = await import('@beechcms/cli')
255
- await schemaDiff({ local: !remote, write, name, db, registry })
188
+ await schemaDiff({ local: !remote, write, name, db })
256
189
  }
257
190
 
258
191
  async function cmdGenerateTypes(args) {
192
+ const remote = args.includes('--remote')
193
+ const local = !remote
194
+
195
+ let out = null
259
196
  const outIdx = args.indexOf('--out')
260
- const out = outIdx !== -1 ? args[outIdx + 1] : 'src/types/beech.ts'
261
- const local = args.includes('--local')
262
- const dbIdx = args.indexOf('--db')
263
- const db = dbIdx !== -1 ? args[dbIdx + 1] : undefined
197
+ const outputIdx = args.indexOf('--output')
198
+ const oIdx = args.indexOf('-o')
199
+
200
+ if (outIdx !== -1 && args[outIdx + 1]) out = args[outIdx + 1]
201
+ else if (outputIdx !== -1 && args[outputIdx + 1]) out = args[outputIdx + 1]
202
+ else if (oIdx !== -1 && args[oIdx + 1]) out = args[oIdx + 1]
264
203
 
265
- const registry = local ? await tryLoadLocalRegistry() : null
204
+ const dbIdx = args.indexOf('--db')
205
+ const db = dbIdx !== -1 ? args[dbIdx + 1] : undefined
266
206
 
267
207
  const { generateTypes } = await import('@beechcms/cli')
268
- await generateTypes({ out, local, db, registry })
208
+ await generateTypes({ out, local, db })
209
+ }
210
+
211
+ async function cmdForms(args) {
212
+ const yes = args.includes('--yes') || args.includes('-y')
213
+ const json = args.includes('--json')
214
+ const frameworkIdx = args.indexOf('--framework')
215
+ const framework = frameworkIdx !== -1 ? args[frameworkIdx + 1] : undefined
216
+ const seedIdx = args.indexOf('--seed')
217
+ const seed = seedIdx !== -1 ? args[seedIdx + 1] : undefined
218
+ const modeIdx = args.indexOf('--mode')
219
+ const mode = modeIdx !== -1 ? args[modeIdx + 1] : undefined
220
+ const outIdx = args.indexOf('--out')
221
+ const out = outIdx !== -1 ? args[outIdx + 1] : undefined
222
+
223
+ const { forms } = await import('@beechcms/cli')
224
+ await forms({ framework, seed, mode, out, yes, json })
269
225
  }
270
226
 
271
227
  // New unified command wrappers:
@@ -328,10 +284,18 @@ async function cmdDoctor(args) {
328
284
  await doctor()
329
285
  }
330
286
 
287
+ async function cmdSetupCloudflare(args) {
288
+ const yes = args.includes('--yes') || args.includes('-y')
289
+ const nameIdx = args.indexOf('--name')
290
+ const projectName = nameIdx !== -1 ? args[nameIdx + 1] : undefined
291
+ const { setupCloudflare } = await import('@beechcms/cli')
292
+ await setupCloudflare({ projectName, nonInteractive: yes })
293
+ }
294
+
331
295
  const handler = COMMANDS[command]
332
296
  if (!handler) {
333
297
  help()
334
- if (command) process.exit(1)
298
+ if (command && command !== '--help' && command !== '-h' && command !== 'help') process.exit(1)
335
299
  } else if (args.includes('--help') || args.includes('-h')) {
336
300
  help()
337
301
  } else {
package/bin/create.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  import * as p from '@clack/prompts'
5
5
  import pc from 'picocolors'
6
- import { execSync } from 'node:child_process'
6
+ import { spawnSync, execSync } from 'node:child_process'
7
7
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
8
8
  import { resolve, join } from 'node:path'
9
9
  import { randomBytes } from 'node:crypto'
@@ -12,78 +12,6 @@ import { dirname } from 'node:path'
12
12
 
13
13
  const __dirname = dirname(fileURLToPath(import.meta.url))
14
14
 
15
- // ── Template registry ─────────────────────────────────────────────────────────
16
-
17
- const TEMPLATES = {
18
- blog: {
19
- label: 'Blog',
20
- hint: 'posts with rich text, cover image, tags and authors',
21
- file: 'blog.ts',
22
- registryEntries: ['posts: POST_SEED', 'authors: AUTHOR_SEED'],
23
- },
24
- gallery: {
25
- label: 'Gallery',
26
- hint: 'media items with image, tags and featured flag',
27
- file: 'gallery.ts',
28
- registryEntries: ['gallery: GALLERY_SEED'],
29
- },
30
- contact: {
31
- label: 'Contact',
32
- hint: 'public form submissions with masked email and read status',
33
- file: 'contact.ts',
34
- registryEntries: ['messages: MESSAGE_SEED'],
35
- },
36
- commerce: {
37
- label: 'Commerce',
38
- hint: 'e-commerce product catalog with prices, inventory and ratings',
39
- file: 'commerce.ts',
40
- registryEntries: ['products: PRODUCT_SEED', 'reviews: REVIEW_SEED'],
41
- },
42
- tasks: {
43
- label: 'Tasks',
44
- hint: 'project management tasks with progress slider',
45
- file: 'tasks.ts',
46
- registryEntries: ['tasks: TASK_SEED'],
47
- },
48
- }
49
-
50
- function readTemplate(filename) {
51
- return readFileSync(join(__dirname, 'templates', filename), 'utf8')
52
- }
53
-
54
- function buildSeedsFile(selectedKeys) {
55
- const header = `import type { Seed } from '@beechcms/core'\n\n`
56
-
57
- if (selectedKeys.length === 0) {
58
- const example = readFileSync(join(__dirname, 'templates', 'empty.ts'), 'utf8')
59
- return (
60
- header +
61
- example +
62
- '\nexport const SEED_REGISTRY: Record<string, Seed> = {}\n\n' +
63
- 'export function getSeed(slug: string): Seed | null {\n' +
64
- ' return SEED_REGISTRY[slug] ?? null\n' +
65
- '}\n'
66
- )
67
- }
68
-
69
- const blocks = selectedKeys.map((key) =>
70
- readFileSync(join(__dirname, 'templates', TEMPLATES[key].file), 'utf8')
71
- )
72
-
73
- const registryEntries = selectedKeys.flatMap((key) => TEMPLATES[key].registryEntries)
74
- const registry =
75
- 'export const SEED_REGISTRY: Record<string, Seed> = {\n' +
76
- registryEntries.map((e) => ` ${e},`).join('\n') +
77
- '\n}\n'
78
-
79
- const getSeed =
80
- '\nexport function getSeed(slug: string): Seed | null {\n' +
81
- ' return SEED_REGISTRY[slug] ?? null\n' +
82
- '}\n'
83
-
84
- return header + blocks.join('\n') + '\n' + registry + getSeed
85
- }
86
-
87
15
  // ── Helpers ───────────────────────────────────────────────────────────────────
88
16
 
89
17
  function generateSecret(bytes = 32) {
@@ -97,16 +25,11 @@ function writeFile(path, content) {
97
25
  function buildWorkerTs() {
98
26
  return `/// <reference types="@cloudflare/workers-types" />
99
27
  import { createBeechApp } from '@beechcms/api'
100
- import { SEED_REGISTRY } from './seeds'
101
28
 
102
- export default createBeechApp({ seeds: Object.values(SEED_REGISTRY) })
29
+ export default createBeechApp({ seeds: [] })
103
30
  `
104
31
  }
105
32
 
106
- function buildSeedsTs(selectedKeys) {
107
- return buildSeedsFile(selectedKeys)
108
- }
109
-
110
33
  function getCurrentVersion() {
111
34
  try {
112
35
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'))
@@ -126,8 +49,6 @@ function buildPackageJson(name) {
126
49
  scripts: {
127
50
  dev: 'wrangler dev --port 8789',
128
51
  deploy: 'wrangler deploy --minify',
129
- 'seed:load': 'npx beech seed:load',
130
- 'seed:load:local': 'npx beech seed:load --local',
131
52
  'db:migrate:local': 'wrangler d1 migrations apply ' + name + '-db --local',
132
53
  'db:reset:local': 'node -e "require(\'fs\').rmSync(\'.wrangler/state\',{recursive:true,force:true})" && npm run db:migrate:local',
133
54
  },
@@ -136,6 +57,7 @@ function buildPackageJson(name) {
136
57
  '@beechcms/core': version,
137
58
  },
138
59
  devDependencies: {
60
+ '@beechcms/cms': version,
139
61
  '@cloudflare/workers-types': '^4.0.0',
140
62
  wrangler: '^4.0.0',
141
63
  typescript: '^5.0.0',
@@ -148,6 +70,7 @@ function buildWranglerJsonc(cfg) {
148
70
  "name": "${cfg.name}-api",
149
71
  "main": "worker.ts",
150
72
  "compatibility_date": "2025-01-01",
73
+ "compatibility_flags": ["nodejs_compat"],
151
74
 
152
75
  "vars": {
153
76
  "JWT_SECRET": "${cfg.jwtSecret}",
@@ -184,6 +107,8 @@ function buildWranglerJsonc(cfg) {
184
107
  function buildDevVars(cloudflare) {
185
108
  if (cloudflare) {
186
109
  return [
110
+ `# Cloudflare R2 S3 credentials (required for direct client upload via Presigned URLs)`,
111
+ `# Guide: https://developers.cloudflare.com/r2/api/s3/tokens/`,
187
112
  `R2_ACCESS_KEY_ID=${cloudflare.r2AccessKey}`,
188
113
  `R2_SECRET_ACCESS_KEY=${cloudflare.r2SecretKey}`,
189
114
  `R2_ENDPOINT=${cloudflare.r2Endpoint}`,
@@ -191,9 +116,8 @@ function buildDevVars(cloudflare) {
191
116
  ].join('\n') + '\n'
192
117
  }
193
118
  return [
194
- '# R2 credentials only needed if you want production-like S3 media uploads locally.',
195
- '# For local development, media uploads work automatically via the Miniflare R2 binding.',
196
- '# Fill these in only when testing production media behaviour:',
119
+ '# Cloudflare R2 S3 credentials (required for direct client upload via Presigned URLs)',
120
+ '# Create an R2 API Token: Cloudflare Dashboard R2 "Manage R2 API Tokens" (Object Read & Write)',
197
121
  '# Guide: https://developers.cloudflare.com/r2/api/s3/tokens/',
198
122
  'R2_ACCESS_KEY_ID=',
199
123
  'R2_SECRET_ACCESS_KEY=',
@@ -238,35 +162,57 @@ async function askCloudflareConfig(name) {
238
162
  })
239
163
  if (p.isCancel(accountId)) return null
240
164
 
241
- const d1Name = await p.text({
242
- message: 'D1 Database name',
243
- initialValue: `${name}-db`,
244
- validate: (v) => { if (!v.trim()) return 'Required' },
165
+ const autoCreate = await p.confirm({
166
+ message: `Auto-create Cloudflare D1 (${name}-db) and R2 (${name}-media) now?`,
167
+ hint: 'Runs `wrangler d1 create` and `wrangler r2 bucket create`',
168
+ initialValue: true,
245
169
  })
246
- if (p.isCancel(d1Name)) return null
170
+ if (p.isCancel(autoCreate)) return null
171
+
172
+ let d1Id = ''
173
+ const d1Name = `${name}-db`
174
+ const r2Bucket = `${name}-media`
175
+
176
+ if (autoCreate) {
177
+ const s = p.spinner()
178
+ s.start(`Creating D1 database: ${pc.bold(d1Name)}…`)
179
+ const d1Res = spawnSync('npx', ['wrangler', 'd1', 'create', d1Name], { encoding: 'utf-8', shell: true })
180
+ const d1Out = (d1Res.stdout || '') + (d1Res.stderr || '')
181
+ const idMatch = d1Out.match(/database_id\s*=\s*["']?([a-f0-9-]{36})["']?/i) ||
182
+ d1Out.match(/"database_id":\s*"([a-f0-9-]{36})"/i) ||
183
+ d1Out.match(/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i)
184
+ if (idMatch) {
185
+ d1Id = idMatch[1]
186
+ s.stop(pc.green(`✓ D1 database ready (${d1Id})`))
187
+ } else {
188
+ s.stop(pc.yellow(`ℹ D1 status: ${d1Out.trim().slice(0, 100)}`))
189
+ }
247
190
 
248
- const d1Id = await p.text({
249
- message: 'D1 Database ID',
250
- placeholder: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
251
- hint: `Run: npx wrangler d1 create ${d1Name} — copy the "database_id" from the output`,
252
- validate: (v) => { if (!v.trim()) return 'Required — create the D1 database first and paste its ID here' },
253
- })
254
- if (p.isCancel(d1Id)) return null
191
+ s.start(`Creating R2 bucket: ${pc.bold(r2Bucket)}…`)
192
+ spawnSync('npx', ['wrangler', 'r2', 'bucket', 'create', r2Bucket], { encoding: 'utf-8', shell: true })
193
+ s.stop(pc.green(`✓ R2 bucket ready (${r2Bucket})`))
194
+ }
255
195
 
256
- const r2Bucket = await p.text({
257
- message: 'R2 Bucket name',
258
- initialValue: `${name}-media`,
259
- validate: (v) => { if (!v.trim()) return 'Required' },
260
- })
261
- if (p.isCancel(r2Bucket)) return null
196
+ if (!d1Id) {
197
+ const d1IdInput = await p.text({
198
+ message: 'D1 Database ID',
199
+ placeholder: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
200
+ hint: `Run: npx wrangler d1 create ${d1Name} — paste the "database_id" here`,
201
+ validate: (v) => { if (!v.trim()) return 'Required — paste database_id from D1' },
202
+ })
203
+ if (p.isCancel(d1IdInput)) return null
204
+ d1Id = d1IdInput.trim()
205
+ }
262
206
 
263
207
  p.note(
264
208
  [
209
+ 'Direct client uploads use Presigned URLs (SigV4) to stream files directly to R2.',
265
210
  'Create an R2 API token:',
266
211
  ' Cloudflare Dashboard → R2 → "Manage R2 API Tokens"',
267
212
  ` → Create Token → Object Read & Write → bucket: ${r2Bucket}`,
213
+ 'Guide: https://developers.cloudflare.com/r2/api/s3/tokens/',
268
214
  ].join('\n'),
269
- 'R2 credentials'
215
+ 'R2 S3 Credentials (for Presigned URLs)'
270
216
  )
271
217
 
272
218
  const r2AccessKey = await p.text({
@@ -309,17 +255,14 @@ async function main() {
309
255
  console.log()
310
256
  p.intro(pc.bgGreen(pc.black(' @beechcms/cms ')))
311
257
 
312
- let name, selectedTemplates, cloudflare
258
+ let name, cloudflare
313
259
 
314
260
  if (silent) {
315
261
  // Non-interactive: use first positional arg or default name, skip Cloudflare
316
262
  const positional = argv.find((a) => !a.startsWith('-'))
317
263
  name = positional ?? 'my-beech-project'
318
- const withExamples = argv.includes('--with-examples') || argv.includes('--examples')
319
- selectedTemplates = withExamples ? ['blog'] : []
320
264
  cloudflare = null
321
- const examplesNote = withExamples ? ' (with blog example content types)' : ''
322
- console.log(pc.dim(` Running in non-interactive mode. Project name: ${name}${examplesNote}`))
265
+ console.log(pc.dim(` Running in non-interactive mode. Project name: ${name}`))
323
266
  } else {
324
267
  // Project name
325
268
  const projectName = await p.text({
@@ -333,22 +276,6 @@ async function main() {
333
276
  if (p.isCancel(projectName)) { p.cancel('Cancelled'); process.exit(0) }
334
277
  name = projectName.trim()
335
278
 
336
- // Content types
337
- const tmpl = await p.multiselect({
338
- message: 'Which content types do you need?',
339
- hint: 'Space to select, Enter to confirm. You can add more later in seeds.ts',
340
- options: [
341
- { value: 'blog', label: 'Blog', hint: 'posts with rich text, cover image, tags and authors' },
342
- { value: 'gallery', label: 'Gallery', hint: 'media items with image, tags and featured flag' },
343
- { value: 'contact', label: 'Contact', hint: 'public form submissions with masked email and read status' },
344
- { value: 'commerce',label: 'Commerce',hint: 'e-commerce product catalog with prices, inventory and ratings' },
345
- { value: 'tasks', label: 'Tasks', hint: 'project management tasks with progress slider' },
346
- ],
347
- required: false,
348
- })
349
- if (p.isCancel(tmpl)) { p.cancel('Cancelled'); process.exit(0) }
350
- selectedTemplates = tmpl
351
-
352
279
  // Cloudflare now or later?
353
280
  const configureNow = await p.confirm({
354
281
  message: 'Configure Cloudflare credentials now?',
@@ -384,7 +311,6 @@ async function main() {
384
311
 
385
312
  mkdirSync(targetDir, { recursive: true })
386
313
 
387
- writeFile(join(targetDir, 'seeds.ts'), buildSeedsTs(selectedTemplates))
388
314
  writeFile(join(targetDir, 'worker.ts'), buildWorkerTs())
389
315
  writeFile(join(targetDir, 'package.json'), buildPackageJson(name))
390
316
  writeFile(join(targetDir, 'tsconfig.json'), buildTsConfig())
@@ -429,10 +355,17 @@ async function main() {
429
355
  ...(pendingConfig ? [
430
356
  `${pc.bold('3. Complete Cloudflare configuration')} ${pc.yellow('← pending')}`,
431
357
  ` Edit ${pc.underline('wrangler.jsonc')} → fill in ${pc.yellow('database_id')} (D1) and ${pc.yellow('bucket_name')} (R2)`,
432
- ` Guide: https://developers.cloudflare.com/d1/`,
433
- ` ${pc.dim('Note: media uploads work locally without R2 credentials (.dev.vars optional)')}`,
358
+ ` Set R2 S3 secrets for Presigned uploads in ${pc.underline('.dev.vars')} (dev) and via ${pc.cyan('npx wrangler secret put')} (prod)`,
359
+ ` Guide: https://developers.cloudflare.com/r2/api/s3/tokens/`,
434
360
  '',
435
- ] : []),
361
+ ] : [
362
+ `${pc.bold('3. Production R2 secrets')} (when deploying to Cloudflare)`,
363
+ ` ${pc.cyan('npx wrangler secret put R2_ACCESS_KEY_ID')}`,
364
+ ` ${pc.cyan('npx wrangler secret put R2_SECRET_ACCESS_KEY')}`,
365
+ ` ${pc.cyan('npx wrangler secret put R2_ENDPOINT')} → ${cloudflare.r2Endpoint}`,
366
+ ` ${pc.cyan('npx wrangler secret put R2_BUCKET_NAME')} → ${cloudflare.r2Bucket}`,
367
+ '',
368
+ ]),
436
369
  `${step(3)}. Run local migrations`,
437
370
  ` ${pc.cyan('npm run db:migrate:local')}`,
438
371
  '',
@@ -443,7 +376,7 @@ async function main() {
443
376
  `${step(5)}. Deploy to production`,
444
377
  ` ${pc.cyan('npm run deploy')}`,
445
378
  '',
446
- `${pc.dim('Your content types are defined in seeds.ts')}`,
379
+ `${pc.dim('Manage content types dynamically in the BeechCMS Dashboard at /admin.')}`,
447
380
  `${pc.dim('JWT secret and API keys have been auto-generated.')}`,
448
381
  ].join('\n'),
449
382
  'Next steps'
@@ -455,3 +388,4 @@ main().catch((err) => {
455
388
  console.error(err)
456
389
  process.exit(1)
457
390
  })
391
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beechcms/cms",
3
- "version": "0.6.7",
3
+ "version": "0.8.0",
4
4
  "description": "Edge-Native, Schema-Driven Headless CMS built on Cloudflare Workers, D1, and R2. Features the Botanical Engine for alias-stable field management, a modular widget layer, and a React + Vite admin dashboard.",
5
5
  "keywords": [
6
6
  "cms",
@@ -33,6 +33,8 @@
33
33
  "node": ">=18.0.0"
34
34
  },
35
35
  "bin": {
36
+ "cms": "bin/create.mjs",
37
+ "@beechcms/cms": "bin/create.mjs",
36
38
  "beechcms": "bin/create.mjs",
37
39
  "beech": "bin/cli.mjs"
38
40
  },
@@ -59,7 +61,7 @@
59
61
  "vitest": "^4.1.0"
60
62
  },
61
63
  "dependencies": {
62
- "@beechcms/cli": "^0.6.7",
64
+ "@beechcms/cli": "^0.8.0",
63
65
  "@clack/prompts": "^0.9.1",
64
66
  "picocolors": "^1.1.1"
65
67
  },
@@ -76,9 +78,10 @@
76
78
  }
77
79
  },
78
80
  "scripts": {
79
- "docs:generate": "typedoc",
81
+ "docs:generate": "turbo run build && typedoc",
80
82
  "docs:dev": "vitepress dev docs",
81
- "docs:build": "typedoc && vitepress build docs",
83
+ "docs:check": "node scripts/docs-fact-check.mjs",
84
+ "docs:build": "turbo run build && typedoc && node scripts/docs-fact-check.mjs && vitepress build docs",
82
85
  "docs:preview": "vitepress preview docs",
83
86
  "beech": "node bin/cli.mjs",
84
87
  "dev": "node scripts/dev.mjs",
@@ -103,6 +106,10 @@
103
106
  "setup:graph": "node scripts/setup-graph.mjs",
104
107
  "release": "node scripts/release.mjs",
105
108
  "release:preview": "node scripts/release.mjs --preview",
109
+ "release:current": "node scripts/release.mjs --current",
110
+ "version:list": "node scripts/release.mjs list",
111
+ "version:get": "node scripts/release.mjs get",
112
+ "version:set": "node scripts/release.mjs set",
106
113
  "postinstall": "node scripts/check-docker.mjs",
107
114
  "doctor": "react-doctor -y --no-dead-code"
108
115
  }
@@ -1,35 +0,0 @@
1
- /** Post: blog article with rich text, cover image, tags and SEO */
2
- export const POST_SEED: Seed = {
3
- slug: 'posts',
4
- label: 'Post',
5
- labelPlural: 'Posts',
6
- displayNameAlias: 'title',
7
- allowPublicRead: true,
8
- allowDrafts: true,
9
- branches: [
10
- { id: 'pst_01', alias: 'title', label: 'Title', type: 'text', requiredOnCreate: true },
11
- { id: 'pst_02', alias: 'publishedAt', label: 'Published at', type: 'date' },
12
- { id: 'pst_03', alias: 'coverImage', label: 'Cover image', type: 'file', fileOptions: { accept: 'image' } },
13
- { id: 'pst_04', alias: 'excerpt', label: 'Excerpt', type: 'text' },
14
- { id: 'pst_05', alias: 'tags', label: 'Tags', type: 'json', options: ['news', 'tutorial', 'release', 'guide', 'opinion'] },
15
- { id: 'pst_06', alias: 'body', label: 'Body', type: 'richtext' },
16
- { id: 'pst_07', alias: 'metaTitle', label: 'Meta title (SEO)', type: 'text' },
17
- { id: 'pst_08', alias: 'metaDescription', label: 'Meta description (SEO)', type: 'text' },
18
- ],
19
- }
20
-
21
- /** Author: content creator profile with photo and social link */
22
- export const AUTHOR_SEED: Seed = {
23
- slug: 'authors',
24
- label: 'Author',
25
- labelPlural: 'Authors',
26
- displayNameAlias: 'name',
27
- allowPublicRead: true,
28
- branches: [
29
- { id: 'aut_01', alias: 'name', label: 'Name', type: 'text', requiredOnCreate: true },
30
- { id: 'aut_02', alias: 'bio', label: 'Bio', type: 'text' },
31
- { id: 'aut_03', alias: 'photo', label: 'Photo', type: 'file', fileOptions: { accept: 'image' } },
32
- { id: 'aut_04', alias: 'website', label: 'Website URL', type: 'text' },
33
- { id: 'aut_05', alias: 'active', label: 'Active', type: 'boolean' },
34
- ],
35
- }
@@ -1,29 +0,0 @@
1
- /** Product: e-commerce product catalog with prices, inventory and ratings */
2
- export const PRODUCT_SEED: Seed = {
3
- slug: 'products',
4
- label: 'Product',
5
- labelPlural: 'Products',
6
- displayNameAlias: 'name',
7
- allowPublicRead: true,
8
- branches: [
9
- { alias: 'name', label: 'Product Name', type: 'text', requiredOnCreate: true },
10
- { alias: 'description', label: 'Description', type: 'richtext' },
11
- { alias: 'price', label: 'Price', type: 'number', numberOptions: { format: 'currency', currency: 'EUR', decimals: 2, min: 0, control: 'input', prefix: '€' } },
12
- { alias: 'inventory', label: 'Stock level', type: 'number', numberOptions: { min: 0, step: 1, control: 'stepper' } },
13
- { alias: 'rating', label: 'Rating', type: 'number', numberOptions: { min: 1, max: 5, step: 0.5, control: 'rating' } }
14
- ],
15
- }
16
-
17
- /** Review: product review */
18
- export const REVIEW_SEED: Seed = {
19
- slug: 'reviews',
20
- label: 'Review',
21
- labelPlural: 'Reviews',
22
- displayNameAlias: 'title',
23
- allowPublicRead: true,
24
- branches: [
25
- { alias: 'title', label: 'Review Title', type: 'text', requiredOnCreate: true },
26
- { alias: 'body', label: 'Body', type: 'text' },
27
- { alias: 'score', label: 'Score', type: 'number', numberOptions: { min: 1, max: 5, step: 1, control: 'rating' } }
28
- ],
29
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * Message: public contact form submission.
3
- *
4
- * allowPublicPost: true — the Public API accepts POST requests without authentication,
5
- * so any frontend form can submit directly to this seed.
6
- * email visibility:masked — the email is stored in full but returned partially
7
- * redacted in API responses (e.g. "jo**@example.com").
8
- * email public:false — the email is never exposed through the Public API.
9
- */
10
- export const MESSAGE_SEED: Seed = {
11
- slug: 'messages',
12
- label: 'Message',
13
- labelPlural: 'Messages',
14
- displayNameAlias: 'name',
15
- allowPublicPost: true,
16
- branches: [
17
- { id: 'msg_01', alias: 'name', label: 'Name', type: 'text', requiredOnCreate: true },
18
- { id: 'msg_02', alias: 'email', label: 'Email', type: 'text', requiredOnCreate: true, policies: { visibility: 'masked', public: false } },
19
- { id: 'msg_03', alias: 'subject', label: 'Subject', type: 'text', requiredOnCreate: true },
20
- { id: 'msg_04', alias: 'message', label: 'Message', type: 'richtext', requiredOnCreate: true },
21
- { id: 'msg_05', alias: 'read', label: 'Read', type: 'boolean' },
22
- ],
23
- }
@@ -1,41 +0,0 @@
1
- /**
2
- * Your seeds go here.
3
- *
4
- * A Seed defines a content type: its slug (used in API URLs), its display labels,
5
- * and its branches (fields).
6
- *
7
- * Each Branch has:
8
- * id — immutable database key (e.g. 'abc_01'). Never change this after
9
- * the first migration or stored data will break.
10
- * alias — the name used in API payloads (e.g. 'title'). Safe to rename.
11
- * label — human-readable label shown in the dashboard.
12
- * type — 'text' | 'number' | 'boolean' | 'date' | 'richtext' | 'file' | 'json'
13
- *
14
- * Seed-level flags:
15
- * allowPublicRead — expose entries via the unauthenticated Public API (GET).
16
- * allowPublicPost — accept submissions via the Public API (POST). Useful for forms.
17
- * allowDrafts — enable a pending-draft workflow for this content type.
18
- * displayNameAlias — which branch alias is used as the entry title in the dashboard.
19
- *
20
- * Branch policies (all optional):
21
- * policies.visibility — 'full' (default) | 'masked' | 'hidden'
22
- * policies.public — false to exclude the field from Public API responses
23
- * policies.search — false to exclude the field from full-text search
24
- *
25
- * Example — uncomment and adapt to define your first content type:
26
- *
27
- * export const PROJECT_SEED: Seed = {
28
- * slug: 'projects',
29
- * label: 'Project',
30
- * labelPlural: 'Projects',
31
- * displayNameAlias: 'title',
32
- * allowPublicRead: true,
33
- * branches: [
34
- * { id: 'prj_01', alias: 'title', label: 'Title', type: 'text', requiredOnCreate: true },
35
- * { id: 'prj_02', alias: 'description', label: 'Description', type: 'text' },
36
- * { id: 'prj_03', alias: 'coverImage', label: 'Cover image', type: 'file' },
37
- * { id: 'prj_04', alias: 'tags', label: 'Tags', type: 'json' },
38
- * { id: 'prj_05', alias: 'published', label: 'Published', type: 'boolean' },
39
- * ],
40
- * }
41
- */
@@ -1,16 +0,0 @@
1
- /** Gallery item: image or media asset with title, tags and featured flag */
2
- export const GALLERY_SEED: Seed = {
3
- slug: 'gallery',
4
- label: 'Item',
5
- labelPlural: 'Gallery',
6
- displayNameAlias: 'title',
7
- allowPublicRead: true,
8
- branches: [
9
- { id: 'gal_01', alias: 'title', label: 'Title', type: 'text', requiredOnCreate: true },
10
- { id: 'gal_02', alias: 'description', label: 'Description', type: 'text' },
11
- { id: 'gal_03', alias: 'image', label: 'Image', type: 'file', requiredOnCreate: true, fileOptions: { accept: 'image' } },
12
- { id: 'gal_04', alias: 'tags', label: 'Tags', type: 'json' },
13
- { id: 'gal_05', alias: 'date', label: 'Date', type: 'date' },
14
- { id: 'gal_06', alias: 'featured', label: 'Featured', type: 'boolean' },
15
- ],
16
- }
@@ -1,12 +0,0 @@
1
- /** Task: project management tasks with progress slider */
2
- export const TASK_SEED: Seed = {
3
- slug: 'tasks',
4
- label: 'Task',
5
- labelPlural: 'Tasks',
6
- displayNameAlias: 'title',
7
- branches: [
8
- { alias: 'title', label: 'Task Title', type: 'text', requiredOnCreate: true },
9
- { alias: 'taskStatus', label: 'Status', type: 'text', options: ['todo', 'in_progress', 'done'] },
10
- { alias: 'completion', label: 'Completion', type: 'number', numberOptions: { format: 'percentage', min: 0, max: 100, step: 5, control: 'slider' } }
11
- ],
12
- }