@beechcms/cms 0.8.1 → 0.9.1
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 +418 -303
- package/bin/create.mjs +396 -391
- package/package.json +10 -4
- package/README.md +0 -1
package/bin/create.mjs
CHANGED
|
@@ -1,391 +1,396 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// @ts-check
|
|
3
|
-
|
|
4
|
-
import * as p from '@clack/prompts'
|
|
5
|
-
import pc from 'picocolors'
|
|
6
|
-
import { spawnSync, execSync } from 'node:child_process'
|
|
7
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
8
|
-
import { resolve, join } from 'node:path'
|
|
9
|
-
import { randomBytes } from 'node:crypto'
|
|
10
|
-
import { fileURLToPath } from 'node:url'
|
|
11
|
-
import { dirname } from 'node:path'
|
|
12
|
-
|
|
13
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
14
|
-
|
|
15
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
function generateSecret(bytes = 32) {
|
|
18
|
-
return randomBytes(bytes).toString('hex')
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function writeFile(path, content) {
|
|
22
|
-
writeFileSync(path, content, 'utf8')
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function buildWorkerTs() {
|
|
26
|
-
return `/// <reference types="@cloudflare/workers-types" />
|
|
27
|
-
import { createBeechApp } from '@beechcms/api'
|
|
28
|
-
|
|
29
|
-
export default createBeechApp({ seeds: [] })
|
|
30
|
-
`
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function getCurrentVersion() {
|
|
34
|
-
try {
|
|
35
|
-
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'))
|
|
36
|
-
return `^${pkg.version}`
|
|
37
|
-
} catch {
|
|
38
|
-
return '^0.6.6'
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function buildPackageJson(name) {
|
|
43
|
-
const version = getCurrentVersion()
|
|
44
|
-
return JSON.stringify({
|
|
45
|
-
name,
|
|
46
|
-
version: '0.1.0',
|
|
47
|
-
private: true,
|
|
48
|
-
type: 'module',
|
|
49
|
-
scripts: {
|
|
50
|
-
dev: 'wrangler dev --port 8789',
|
|
51
|
-
deploy: 'wrangler deploy --minify',
|
|
52
|
-
'db:migrate:local': 'wrangler d1 migrations apply ' + name + '-db --local',
|
|
53
|
-
'db:reset:local': 'node -e "require(\'fs\').rmSync(\'.wrangler/state\',{recursive:true,force:true})" && npm run db:migrate:local',
|
|
54
|
-
},
|
|
55
|
-
dependencies: {
|
|
56
|
-
'@beechcms/api': version,
|
|
57
|
-
'@beechcms/core': version,
|
|
58
|
-
},
|
|
59
|
-
devDependencies: {
|
|
60
|
-
'@beechcms/cms': version,
|
|
61
|
-
'@cloudflare/workers-types': '^4.0.0',
|
|
62
|
-
wrangler: '^4.0.0',
|
|
63
|
-
typescript: '^5.0.0',
|
|
64
|
-
},
|
|
65
|
-
}, null, 2) + '\n'
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function buildWranglerJsonc(cfg) {
|
|
69
|
-
return `{
|
|
70
|
-
"name": "${cfg.name}-api",
|
|
71
|
-
"main": "worker.ts",
|
|
72
|
-
"compatibility_date": "2025-01-01",
|
|
73
|
-
"compatibility_flags": ["nodejs_compat"],
|
|
74
|
-
|
|
75
|
-
"vars": {
|
|
76
|
-
"JWT_SECRET": "${cfg.jwtSecret}",
|
|
77
|
-
"CORS_ORIGINS": "${cfg.corsOrigins}",
|
|
78
|
-
"PUBLIC_READ_API_KEY": "${cfg.publicReadKey}",
|
|
79
|
-
"PUBLIC_WRITE_API_KEY": "${cfg.publicWriteKey}",
|
|
80
|
-
"APP_URL": "${cfg.appUrl || 'http://localhost:5173'}"
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
"assets": {
|
|
84
|
-
"binding": "ASSETS",
|
|
85
|
-
"directory": "node_modules/@beechcms/api/assets/dashboard"
|
|
86
|
-
},
|
|
87
|
-
|
|
88
|
-
"d1_databases": [
|
|
89
|
-
{
|
|
90
|
-
"binding": "DB",
|
|
91
|
-
"database_name": "${cfg.d1Name}",
|
|
92
|
-
"database_id": "${cfg.d1Id}",
|
|
93
|
-
"migrations_dir": "node_modules/@beechcms/api/migrations"
|
|
94
|
-
}
|
|
95
|
-
],
|
|
96
|
-
|
|
97
|
-
"r2_buckets": [
|
|
98
|
-
{
|
|
99
|
-
"binding": "MEDIA_BUCKET",
|
|
100
|
-
"bucket_name": "${cfg.r2Bucket}"
|
|
101
|
-
}
|
|
102
|
-
]
|
|
103
|
-
}
|
|
104
|
-
`
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function buildDevVars(cloudflare) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
`#
|
|
112
|
-
|
|
113
|
-
`
|
|
114
|
-
`
|
|
115
|
-
`
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
'
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
'
|
|
123
|
-
'
|
|
124
|
-
'
|
|
125
|
-
'
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
'
|
|
153
|
-
'
|
|
154
|
-
|
|
155
|
-
'
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
''
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
` ${pc.cyan('npx wrangler secret put
|
|
364
|
-
`
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
''
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
''
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
''
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
import * as p from '@clack/prompts'
|
|
5
|
+
import pc from 'picocolors'
|
|
6
|
+
import { spawnSync, execSync } from 'node:child_process'
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import { resolve, join } from 'node:path'
|
|
9
|
+
import { randomBytes } from 'node:crypto'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
import { dirname } from 'node:path'
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
14
|
+
|
|
15
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function generateSecret(bytes = 32) {
|
|
18
|
+
return randomBytes(bytes).toString('hex')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeFile(path, content) {
|
|
22
|
+
writeFileSync(path, content, 'utf8')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildWorkerTs() {
|
|
26
|
+
return `/// <reference types="@cloudflare/workers-types" />
|
|
27
|
+
import { createBeechApp } from '@beechcms/api'
|
|
28
|
+
|
|
29
|
+
export default createBeechApp({ seeds: [] })
|
|
30
|
+
`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getCurrentVersion() {
|
|
34
|
+
try {
|
|
35
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'))
|
|
36
|
+
return `^${pkg.version}`
|
|
37
|
+
} catch {
|
|
38
|
+
return '^0.6.6'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildPackageJson(name) {
|
|
43
|
+
const version = getCurrentVersion()
|
|
44
|
+
return JSON.stringify({
|
|
45
|
+
name,
|
|
46
|
+
version: '0.1.0',
|
|
47
|
+
private: true,
|
|
48
|
+
type: 'module',
|
|
49
|
+
scripts: {
|
|
50
|
+
dev: 'wrangler dev --port 8789',
|
|
51
|
+
deploy: 'wrangler deploy --minify',
|
|
52
|
+
'db:migrate:local': 'wrangler d1 migrations apply ' + name + '-db --local',
|
|
53
|
+
'db:reset:local': 'node -e "require(\'fs\').rmSync(\'.wrangler/state\',{recursive:true,force:true})" && npm run db:migrate:local',
|
|
54
|
+
},
|
|
55
|
+
dependencies: {
|
|
56
|
+
'@beechcms/api': version,
|
|
57
|
+
'@beechcms/core': version,
|
|
58
|
+
},
|
|
59
|
+
devDependencies: {
|
|
60
|
+
'@beechcms/cms': version,
|
|
61
|
+
'@cloudflare/workers-types': '^4.0.0',
|
|
62
|
+
wrangler: '^4.0.0',
|
|
63
|
+
typescript: '^5.0.0',
|
|
64
|
+
},
|
|
65
|
+
}, null, 2) + '\n'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildWranglerJsonc(cfg) {
|
|
69
|
+
return `{
|
|
70
|
+
"name": "${cfg.name}-api",
|
|
71
|
+
"main": "worker.ts",
|
|
72
|
+
"compatibility_date": "2025-01-01",
|
|
73
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
74
|
+
|
|
75
|
+
"vars": {
|
|
76
|
+
"JWT_SECRET": "${cfg.jwtSecret}",
|
|
77
|
+
"CORS_ORIGINS": "${cfg.corsOrigins}",
|
|
78
|
+
"PUBLIC_READ_API_KEY": "${cfg.publicReadKey}",
|
|
79
|
+
"PUBLIC_WRITE_API_KEY": "${cfg.publicWriteKey}",
|
|
80
|
+
"APP_URL": "${cfg.appUrl || 'http://localhost:5173'}"
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
"assets": {
|
|
84
|
+
"binding": "ASSETS",
|
|
85
|
+
"directory": "node_modules/@beechcms/api/assets/dashboard"
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
"d1_databases": [
|
|
89
|
+
{
|
|
90
|
+
"binding": "DB",
|
|
91
|
+
"database_name": "${cfg.d1Name}",
|
|
92
|
+
"database_id": "${cfg.d1Id}",
|
|
93
|
+
"migrations_dir": "node_modules/@beechcms/api/migrations"
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
|
|
97
|
+
"r2_buckets": [
|
|
98
|
+
{
|
|
99
|
+
"binding": "MEDIA_BUCKET",
|
|
100
|
+
"bucket_name": "${cfg.r2Bucket}"
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildDevVars(cloudflare) {
|
|
108
|
+
const PRIVACY_KEY_LINES = `# Master key for protected fields (AES-GCM + blind index). Do NOT change once data is stored.\nPRIVACY_MASTER_KEY=${randomBytes(32).toString('hex')}`
|
|
109
|
+
if (cloudflare) {
|
|
110
|
+
return [
|
|
111
|
+
`# Cloudflare R2 S3 credentials (required for direct client upload via Presigned URLs)`,
|
|
112
|
+
`# Guide: https://developers.cloudflare.com/r2/api/s3/tokens/`,
|
|
113
|
+
`R2_ACCESS_KEY_ID=${cloudflare.r2AccessKey}`,
|
|
114
|
+
`R2_SECRET_ACCESS_KEY=${cloudflare.r2SecretKey}`,
|
|
115
|
+
`R2_ENDPOINT=${cloudflare.r2Endpoint}`,
|
|
116
|
+
`R2_BUCKET_NAME=${cloudflare.r2Bucket}`,
|
|
117
|
+
'',
|
|
118
|
+
PRIVACY_KEY_LINES,
|
|
119
|
+
].join('\n') + '\n'
|
|
120
|
+
}
|
|
121
|
+
return [
|
|
122
|
+
'# Cloudflare R2 S3 credentials (required for direct client upload via Presigned URLs)',
|
|
123
|
+
'# Create an R2 API Token: Cloudflare Dashboard → R2 → "Manage R2 API Tokens" (Object Read & Write)',
|
|
124
|
+
'# Guide: https://developers.cloudflare.com/r2/api/s3/tokens/',
|
|
125
|
+
'R2_ACCESS_KEY_ID=',
|
|
126
|
+
'R2_SECRET_ACCESS_KEY=',
|
|
127
|
+
'R2_ENDPOINT=https://<YOUR_ACCOUNT_ID>.r2.cloudflarestorage.com',
|
|
128
|
+
'R2_BUCKET_NAME=',
|
|
129
|
+
'',
|
|
130
|
+
PRIVACY_KEY_LINES,
|
|
131
|
+
].join('\n') + '\n'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function buildTsConfig() {
|
|
135
|
+
return JSON.stringify({
|
|
136
|
+
compilerOptions: {
|
|
137
|
+
target: 'ES2022',
|
|
138
|
+
module: 'ES2022',
|
|
139
|
+
moduleResolution: 'bundler',
|
|
140
|
+
strict: true,
|
|
141
|
+
types: ['@cloudflare/workers-types'],
|
|
142
|
+
},
|
|
143
|
+
include: ['*.ts'],
|
|
144
|
+
}, null, 2) + '\n'
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Cloudflare prompts ────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
async function askCloudflareConfig(name) {
|
|
150
|
+
p.note(
|
|
151
|
+
[
|
|
152
|
+
'You will need a free Cloudflare account with:',
|
|
153
|
+
'',
|
|
154
|
+
` D1 database → ${pc.cyan('npx wrangler d1 create ' + name + '-db')}`,
|
|
155
|
+
` R2 bucket → ${pc.cyan('npx wrangler r2 bucket create ' + name + '-media')}`,
|
|
156
|
+
'',
|
|
157
|
+
'Docs: https://developers.cloudflare.com/d1/',
|
|
158
|
+
' https://developers.cloudflare.com/r2/',
|
|
159
|
+
].join('\n'),
|
|
160
|
+
'Prerequisites'
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
const accountId = await p.text({
|
|
164
|
+
message: 'Cloudflare Account ID',
|
|
165
|
+
hint: 'dash.cloudflare.com → right sidebar → "Account ID"',
|
|
166
|
+
validate: (v) => { if (!v.trim()) return 'Required' },
|
|
167
|
+
})
|
|
168
|
+
if (p.isCancel(accountId)) return null
|
|
169
|
+
|
|
170
|
+
const autoCreate = await p.confirm({
|
|
171
|
+
message: `Auto-create Cloudflare D1 (${name}-db) and R2 (${name}-media) now?`,
|
|
172
|
+
hint: 'Runs `wrangler d1 create` and `wrangler r2 bucket create`',
|
|
173
|
+
initialValue: true,
|
|
174
|
+
})
|
|
175
|
+
if (p.isCancel(autoCreate)) return null
|
|
176
|
+
|
|
177
|
+
let d1Id = ''
|
|
178
|
+
const d1Name = `${name}-db`
|
|
179
|
+
const r2Bucket = `${name}-media`
|
|
180
|
+
|
|
181
|
+
if (autoCreate) {
|
|
182
|
+
const s = p.spinner()
|
|
183
|
+
s.start(`Creating D1 database: ${pc.bold(d1Name)}…`)
|
|
184
|
+
const d1Res = spawnSync('npx', ['wrangler', 'd1', 'create', d1Name], { encoding: 'utf-8', shell: true })
|
|
185
|
+
const d1Out = (d1Res.stdout || '') + (d1Res.stderr || '')
|
|
186
|
+
const idMatch = d1Out.match(/database_id\s*=\s*["']?([a-f0-9-]{36})["']?/i) ||
|
|
187
|
+
d1Out.match(/"database_id":\s*"([a-f0-9-]{36})"/i) ||
|
|
188
|
+
d1Out.match(/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i)
|
|
189
|
+
if (idMatch) {
|
|
190
|
+
d1Id = idMatch[1]
|
|
191
|
+
s.stop(pc.green(`✓ D1 database ready (${d1Id})`))
|
|
192
|
+
} else {
|
|
193
|
+
s.stop(pc.yellow(`ℹ D1 status: ${d1Out.trim().slice(0, 100)}`))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
s.start(`Creating R2 bucket: ${pc.bold(r2Bucket)}…`)
|
|
197
|
+
spawnSync('npx', ['wrangler', 'r2', 'bucket', 'create', r2Bucket], { encoding: 'utf-8', shell: true })
|
|
198
|
+
s.stop(pc.green(`✓ R2 bucket ready (${r2Bucket})`))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!d1Id) {
|
|
202
|
+
const d1IdInput = await p.text({
|
|
203
|
+
message: 'D1 Database ID',
|
|
204
|
+
placeholder: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
|
|
205
|
+
hint: `Run: npx wrangler d1 create ${d1Name} — paste the "database_id" here`,
|
|
206
|
+
validate: (v) => { if (!v.trim()) return 'Required — paste database_id from D1' },
|
|
207
|
+
})
|
|
208
|
+
if (p.isCancel(d1IdInput)) return null
|
|
209
|
+
d1Id = d1IdInput.trim()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
p.note(
|
|
213
|
+
[
|
|
214
|
+
'Direct client uploads use Presigned URLs (SigV4) to stream files directly to R2.',
|
|
215
|
+
'Create an R2 API token:',
|
|
216
|
+
' Cloudflare Dashboard → R2 → "Manage R2 API Tokens"',
|
|
217
|
+
` → Create Token → Object Read & Write → bucket: ${r2Bucket}`,
|
|
218
|
+
'Guide: https://developers.cloudflare.com/r2/api/s3/tokens/',
|
|
219
|
+
].join('\n'),
|
|
220
|
+
'R2 S3 Credentials (for Presigned URLs)'
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
const r2AccessKey = await p.text({
|
|
224
|
+
message: 'R2 Access Key ID',
|
|
225
|
+
validate: (v) => { if (!v.trim()) return 'Required' },
|
|
226
|
+
})
|
|
227
|
+
if (p.isCancel(r2AccessKey)) return null
|
|
228
|
+
|
|
229
|
+
const r2SecretKey = await p.password({
|
|
230
|
+
message: 'R2 Secret Access Key',
|
|
231
|
+
validate: (v) => { if (!v.trim()) return 'Required' },
|
|
232
|
+
})
|
|
233
|
+
if (p.isCancel(r2SecretKey)) return null
|
|
234
|
+
|
|
235
|
+
const appUrl = await p.text({
|
|
236
|
+
message: 'Production dashboard URL (for CORS)',
|
|
237
|
+
placeholder: `https://cms.${name}.com`,
|
|
238
|
+
hint: 'Leave empty to configure later in wrangler.jsonc',
|
|
239
|
+
})
|
|
240
|
+
if (p.isCancel(appUrl)) return null
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
accountId: accountId.trim(),
|
|
244
|
+
d1Name: d1Name.trim(),
|
|
245
|
+
d1Id: d1Id.trim(),
|
|
246
|
+
r2Bucket: r2Bucket.trim(),
|
|
247
|
+
r2AccessKey: r2AccessKey.trim(),
|
|
248
|
+
r2SecretKey: r2SecretKey.trim(),
|
|
249
|
+
r2Endpoint: `https://${accountId.trim()}.r2.cloudflarestorage.com`,
|
|
250
|
+
appUrl: appUrl?.trim() ?? '',
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
async function main() {
|
|
257
|
+
const argv = process.argv.slice(2)
|
|
258
|
+
const silent = argv.includes('--yes') || argv.includes('-y') || !process.stdout.isTTY
|
|
259
|
+
|
|
260
|
+
console.log()
|
|
261
|
+
p.intro(pc.bgGreen(pc.black(' @beechcms/cms ')))
|
|
262
|
+
|
|
263
|
+
let name, cloudflare
|
|
264
|
+
|
|
265
|
+
if (silent) {
|
|
266
|
+
// Non-interactive: use first positional arg or default name, skip Cloudflare
|
|
267
|
+
const positional = argv.find((a) => !a.startsWith('-'))
|
|
268
|
+
name = positional ?? 'my-beech-project'
|
|
269
|
+
cloudflare = null
|
|
270
|
+
console.log(pc.dim(` Running in non-interactive mode. Project name: ${name}`))
|
|
271
|
+
} else {
|
|
272
|
+
// Project name
|
|
273
|
+
const projectName = await p.text({
|
|
274
|
+
message: 'Project name',
|
|
275
|
+
placeholder: 'my-website',
|
|
276
|
+
validate: (v) => {
|
|
277
|
+
if (!v.trim()) return 'Required'
|
|
278
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(v.trim())) return 'Lowercase letters, numbers and hyphens only'
|
|
279
|
+
},
|
|
280
|
+
})
|
|
281
|
+
if (p.isCancel(projectName)) { p.cancel('Cancelled'); process.exit(0) }
|
|
282
|
+
name = projectName.trim()
|
|
283
|
+
|
|
284
|
+
// Cloudflare now or later?
|
|
285
|
+
const configureNow = await p.confirm({
|
|
286
|
+
message: 'Configure Cloudflare credentials now?',
|
|
287
|
+
hint: 'Choose "No" to scaffold the project and fill in the values later',
|
|
288
|
+
initialValue: true,
|
|
289
|
+
})
|
|
290
|
+
if (p.isCancel(configureNow)) { p.cancel('Cancelled'); process.exit(0) }
|
|
291
|
+
|
|
292
|
+
if (configureNow) {
|
|
293
|
+
cloudflare = await askCloudflareConfig(name)
|
|
294
|
+
if (!cloudflare) { p.cancel('Cancelled'); process.exit(0) }
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const targetDir = resolve(process.cwd(), name)
|
|
299
|
+
if (existsSync(targetDir)) {
|
|
300
|
+
p.cancel(`Directory '${name}' already exists. Choose a different name or delete the folder.`)
|
|
301
|
+
process.exit(1)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const jwtSecret = generateSecret(32)
|
|
305
|
+
const publicReadKey = generateSecret(16)
|
|
306
|
+
const publicWriteKey = generateSecret(16)
|
|
307
|
+
|
|
308
|
+
const corsOrigins = cloudflare
|
|
309
|
+
? ['http://localhost:5173', 'http://localhost:5174', cloudflare.appUrl]
|
|
310
|
+
.filter(Boolean).join(',')
|
|
311
|
+
: 'http://localhost:5173,http://localhost:5174'
|
|
312
|
+
|
|
313
|
+
// Scaffold
|
|
314
|
+
const s = p.spinner()
|
|
315
|
+
s.start('Scaffolding project…')
|
|
316
|
+
|
|
317
|
+
mkdirSync(targetDir, { recursive: true })
|
|
318
|
+
|
|
319
|
+
writeFile(join(targetDir, 'worker.ts'), buildWorkerTs())
|
|
320
|
+
writeFile(join(targetDir, 'package.json'), buildPackageJson(name))
|
|
321
|
+
writeFile(join(targetDir, 'tsconfig.json'), buildTsConfig())
|
|
322
|
+
writeFile(join(targetDir, 'wrangler.jsonc'), buildWranglerJsonc({
|
|
323
|
+
name,
|
|
324
|
+
d1Name: cloudflare?.d1Name ?? `${name}-db`,
|
|
325
|
+
d1Id: cloudflare?.d1Id ?? 'FILL_IN_YOUR_D1_DATABASE_ID',
|
|
326
|
+
r2Bucket: cloudflare?.r2Bucket ?? `${name}-media`,
|
|
327
|
+
jwtSecret,
|
|
328
|
+
corsOrigins,
|
|
329
|
+
publicReadKey,
|
|
330
|
+
publicWriteKey,
|
|
331
|
+
appUrl: cloudflare?.appUrl ?? '',
|
|
332
|
+
}))
|
|
333
|
+
writeFile(join(targetDir, '.dev.vars'), buildDevVars(cloudflare))
|
|
334
|
+
writeFile(join(targetDir, '.gitignore'), '.wrangler\nnode_modules\n.dev.vars\ndist\n')
|
|
335
|
+
|
|
336
|
+
s.stop('Project scaffolded')
|
|
337
|
+
|
|
338
|
+
// Init git
|
|
339
|
+
s.start('Initialising git repository…')
|
|
340
|
+
try {
|
|
341
|
+
execSync(`git -C "${targetDir}" init -q`, { stdio: 'pipe' })
|
|
342
|
+
execSync(`git -C "${targetDir}" add -A`, { stdio: 'pipe' })
|
|
343
|
+
execSync(`git -C "${targetDir}" commit -q -m "feat: initialise BeechCMS project"`, { stdio: 'pipe' })
|
|
344
|
+
s.stop('Git initialised')
|
|
345
|
+
} catch {
|
|
346
|
+
s.stop('Git skipped (not available)')
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const pendingConfig = !cloudflare
|
|
350
|
+
const step = (n) => pc.bold(String(n + (pendingConfig ? 1 : 0)))
|
|
351
|
+
console.log()
|
|
352
|
+
p.note(
|
|
353
|
+
[
|
|
354
|
+
`${pc.bold('1. Enter the project')}`,
|
|
355
|
+
` ${pc.cyan('cd ' + name)}`,
|
|
356
|
+
'',
|
|
357
|
+
`${pc.bold('2. Install dependencies')}`,
|
|
358
|
+
` ${pc.cyan('npm install')}`,
|
|
359
|
+
'',
|
|
360
|
+
...(pendingConfig ? [
|
|
361
|
+
`${pc.bold('3. Complete Cloudflare configuration')} ${pc.yellow('← pending')}`,
|
|
362
|
+
` Edit ${pc.underline('wrangler.jsonc')} → fill in ${pc.yellow('database_id')} (D1) and ${pc.yellow('bucket_name')} (R2)`,
|
|
363
|
+
` Set R2 S3 secrets for Presigned uploads in ${pc.underline('.dev.vars')} (dev) and via ${pc.cyan('npx wrangler secret put')} (prod)`,
|
|
364
|
+
` Guide: https://developers.cloudflare.com/r2/api/s3/tokens/`,
|
|
365
|
+
'',
|
|
366
|
+
] : [
|
|
367
|
+
`${pc.bold('3. Production R2 secrets')} (when deploying to Cloudflare)`,
|
|
368
|
+
` ${pc.cyan('npx wrangler secret put R2_ACCESS_KEY_ID')}`,
|
|
369
|
+
` ${pc.cyan('npx wrangler secret put R2_SECRET_ACCESS_KEY')}`,
|
|
370
|
+
` ${pc.cyan('npx wrangler secret put R2_ENDPOINT')} → ${cloudflare.r2Endpoint}`,
|
|
371
|
+
` ${pc.cyan('npx wrangler secret put R2_BUCKET_NAME')} → ${cloudflare.r2Bucket}`,
|
|
372
|
+
'',
|
|
373
|
+
]),
|
|
374
|
+
`${step(3)}. Run local migrations`,
|
|
375
|
+
` ${pc.cyan('npm run db:migrate:local')}`,
|
|
376
|
+
'',
|
|
377
|
+
`${step(4)}. Start the dev server`,
|
|
378
|
+
` ${pc.cyan('npx wrangler dev')}`,
|
|
379
|
+
` Then open: ${pc.underline('http://localhost:8789/admin')}`,
|
|
380
|
+
'',
|
|
381
|
+
`${step(5)}. Deploy to production`,
|
|
382
|
+
` ${pc.cyan('npm run deploy')}`,
|
|
383
|
+
'',
|
|
384
|
+
`${pc.dim('Manage content types dynamically in the BeechCMS Dashboard at /admin.')}`,
|
|
385
|
+
`${pc.dim('JWT secret and API keys have been auto-generated.')}`,
|
|
386
|
+
].join('\n'),
|
|
387
|
+
'Next steps'
|
|
388
|
+
)
|
|
389
|
+
p.outro(pc.green(`✔ BeechCMS project ready — ${name}/`))
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
main().catch((err) => {
|
|
393
|
+
console.error(err)
|
|
394
|
+
process.exit(1)
|
|
395
|
+
})
|
|
396
|
+
|