create-sonicjs 3.0.0-beta.2 → 3.0.0-beta.4
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/package.json +1 -1
- package/src/cli.js +89 -132
- package/templates/starter/wrangler.toml +1 -1
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import fs from 'fs-extra'
|
|
4
4
|
import path from 'path'
|
|
5
5
|
import { fileURLToPath } from 'url'
|
|
6
|
+
import { randomBytes } from 'crypto'
|
|
6
7
|
import prompts from 'prompts'
|
|
7
8
|
import kleur from 'kleur'
|
|
8
9
|
import ora from 'ora'
|
|
@@ -50,7 +51,9 @@ const flags = {
|
|
|
50
51
|
databaseName: args.find(arg => arg.startsWith('--database='))?.split('=')[1],
|
|
51
52
|
bucketName: args.find(arg => arg.startsWith('--bucket='))?.split('=')[1],
|
|
52
53
|
skipExample: args.includes('--skip-example'),
|
|
53
|
-
includeExample: args.includes('--include-example')
|
|
54
|
+
includeExample: args.includes('--include-example'),
|
|
55
|
+
adminEmail: args.find(arg => arg.startsWith('--admin-email='))?.split('=')[1],
|
|
56
|
+
adminPassword: args.find(arg => arg.startsWith('--admin-password='))?.split('=')[1],
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
async function main() {
|
|
@@ -171,48 +174,39 @@ async function getProjectDetails(initialName) {
|
|
|
171
174
|
})
|
|
172
175
|
}
|
|
173
176
|
|
|
174
|
-
// Seed admin user
|
|
175
|
-
|
|
176
|
-
type: 'confirm',
|
|
177
|
-
name: 'seedAdmin',
|
|
178
|
-
message: 'Create admin user?',
|
|
179
|
-
initial: true
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
// Admin email (only if seeding)
|
|
183
|
-
questions.push({
|
|
184
|
-
type: (prev, values) => values.seedAdmin ? 'text' : null,
|
|
185
|
-
name: 'adminEmail',
|
|
186
|
-
message: 'Admin email:',
|
|
187
|
-
validate: (value) => {
|
|
188
|
-
if (!value) return 'Admin email is required'
|
|
189
|
-
// Basic email validation
|
|
190
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
191
|
-
if (!emailRegex.test(value)) return 'Please enter a valid email address'
|
|
192
|
-
return true
|
|
193
|
-
}
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
// Admin password (only if seeding)
|
|
197
|
-
questions.push({
|
|
198
|
-
type: (prev, values) => values.seedAdmin ? 'password' : null,
|
|
199
|
-
name: 'adminPassword',
|
|
200
|
-
message: 'Admin password:',
|
|
201
|
-
validate: (value) => {
|
|
202
|
-
if (!value) return 'Admin password is required'
|
|
203
|
-
if (value.length < 8) return 'Password must be at least 8 characters'
|
|
204
|
-
return true
|
|
205
|
-
}
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
// Include example collection (only ask if neither flag is set)
|
|
209
|
-
if (!flags.skipExample && !flags.includeExample) {
|
|
177
|
+
// Seed admin user (skip prompt if credentials provided via flags)
|
|
178
|
+
if (!flags.adminEmail || !flags.adminPassword) {
|
|
210
179
|
questions.push({
|
|
211
180
|
type: 'confirm',
|
|
212
|
-
name: '
|
|
213
|
-
message: '
|
|
181
|
+
name: 'seedAdmin',
|
|
182
|
+
message: 'Create admin user?',
|
|
214
183
|
initial: true
|
|
215
184
|
})
|
|
185
|
+
|
|
186
|
+
// Admin email (only if seeding)
|
|
187
|
+
questions.push({
|
|
188
|
+
type: (prev, values) => values.seedAdmin ? 'text' : null,
|
|
189
|
+
name: 'adminEmail',
|
|
190
|
+
message: 'Admin email:',
|
|
191
|
+
validate: (value) => {
|
|
192
|
+
if (!value) return 'Admin email is required'
|
|
193
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
194
|
+
if (!emailRegex.test(value)) return 'Please enter a valid email address'
|
|
195
|
+
return true
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// Admin password (only if seeding)
|
|
200
|
+
questions.push({
|
|
201
|
+
type: (prev, values) => values.seedAdmin ? 'password' : null,
|
|
202
|
+
name: 'adminPassword',
|
|
203
|
+
message: 'Admin password:',
|
|
204
|
+
validate: (value) => {
|
|
205
|
+
if (!value) return 'Admin password is required'
|
|
206
|
+
if (value.length < 8) return 'Password must be at least 8 characters'
|
|
207
|
+
return true
|
|
208
|
+
}
|
|
209
|
+
})
|
|
216
210
|
}
|
|
217
211
|
|
|
218
212
|
// Create Cloudflare resources
|
|
@@ -246,10 +240,10 @@ async function getProjectDetails(initialName) {
|
|
|
246
240
|
template: flags.template || 'starter', // Always default to starter template
|
|
247
241
|
databaseName: flags.databaseName || answers.databaseName || `${initialName || answers.projectName}-db`,
|
|
248
242
|
bucketName: flags.bucketName || answers.bucketName || `${initialName || answers.projectName}-media`,
|
|
249
|
-
seedAdmin: answers.seedAdmin !== undefined ? answers.seedAdmin : true,
|
|
250
|
-
adminEmail: answers.adminEmail,
|
|
251
|
-
adminPassword: answers.adminPassword,
|
|
252
|
-
includeExample:
|
|
243
|
+
seedAdmin: (flags.adminEmail && flags.adminPassword) ? true : (answers.seedAdmin !== undefined ? answers.seedAdmin : true),
|
|
244
|
+
adminEmail: flags.adminEmail || answers.adminEmail,
|
|
245
|
+
adminPassword: flags.adminPassword || answers.adminPassword,
|
|
246
|
+
includeExample: true,
|
|
253
247
|
createResources: flags.skipCloudflare ? false : answers.createResources,
|
|
254
248
|
runMigrations: true, // Always run migrations automatically
|
|
255
249
|
initGit: flags.skipGit ? false : answers.initGit,
|
|
@@ -415,7 +409,7 @@ async function copyTemplate(templateName, targetDir, options) {
|
|
|
415
409
|
|
|
416
410
|
// Add @sonicjs-cms/core dependency
|
|
417
411
|
packageJson.dependencies = {
|
|
418
|
-
'@sonicjs-cms/core': '^3.0.0-beta.
|
|
412
|
+
'@sonicjs-cms/core': '^3.0.0-beta.4',
|
|
419
413
|
...packageJson.dependencies
|
|
420
414
|
}
|
|
421
415
|
|
|
@@ -454,100 +448,88 @@ async function copyTemplate(templateName, targetDir, options) {
|
|
|
454
448
|
password: options.adminPassword
|
|
455
449
|
})
|
|
456
450
|
}
|
|
451
|
+
|
|
452
|
+
// Generate .dev.vars with a random BETTER_AUTH_SECRET for local dev
|
|
453
|
+
const devVarsPath = path.join(targetDir, '.dev.vars')
|
|
454
|
+
if (!fs.existsSync(devVarsPath)) {
|
|
455
|
+
const secret = randomBytes(32).toString('hex')
|
|
456
|
+
await fs.writeFile(devVarsPath, `BETTER_AUTH_SECRET="${secret}"\n`)
|
|
457
|
+
}
|
|
457
458
|
}
|
|
458
459
|
|
|
459
460
|
async function createAdminSeedScript(targetDir, { email, password }) {
|
|
460
|
-
const seedScriptContent = `import {
|
|
461
|
-
import { eq } from 'drizzle-orm'
|
|
462
|
-
import * as crypto from 'crypto'
|
|
461
|
+
const seedScriptContent = `import { bootstrapDocumentTypes, RbacService } from '@sonicjs-cms/core'
|
|
463
462
|
import { getPlatformProxy } from 'wrangler'
|
|
464
463
|
|
|
465
464
|
/**
|
|
466
465
|
* Seed script to create initial admin user
|
|
467
466
|
*
|
|
468
|
-
* Run this script after migrations:
|
|
469
|
-
* npm run db:migrate:local
|
|
470
|
-
* npm run seed
|
|
471
|
-
*
|
|
472
467
|
* Admin credentials:
|
|
473
468
|
* Email: ${email}
|
|
474
469
|
* Password: [as entered during setup]
|
|
475
470
|
*/
|
|
476
471
|
|
|
472
|
+
async function hashPassword(password) {
|
|
473
|
+
const iterations = 100000
|
|
474
|
+
const salt = new Uint8Array(16)
|
|
475
|
+
crypto.getRandomValues(salt)
|
|
476
|
+
const encoder = new TextEncoder()
|
|
477
|
+
const keyMaterial = await crypto.subtle.importKey('raw', encoder.encode(password), 'PBKDF2', false, ['deriveBits'])
|
|
478
|
+
const hashBuffer = await crypto.subtle.deriveBits({ name: 'PBKDF2', salt, iterations, hash: 'SHA-256' }, keyMaterial, 256)
|
|
479
|
+
const saltHex = Array.from(salt).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
480
|
+
const hashHex = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
481
|
+
return \`pbkdf2:\${iterations}:\${saltHex}:\${hashHex}\`
|
|
482
|
+
}
|
|
483
|
+
|
|
477
484
|
async function seed() {
|
|
478
|
-
// Get D1 database from Cloudflare environment using wrangler's getPlatformProxy
|
|
479
485
|
const { env, dispose } = await getPlatformProxy()
|
|
480
486
|
|
|
481
487
|
if (!env?.DB) {
|
|
482
|
-
console.error('❌ Error: DB binding not found')
|
|
483
|
-
console.error('')
|
|
484
|
-
console.error('Make sure you have:')
|
|
485
|
-
console.error('1. Created your D1 database: wrangler d1 create <database-name>')
|
|
486
|
-
console.error('2. Updated wrangler.toml with the database_id')
|
|
487
|
-
console.error('3. Run migrations: npm run db:migrate:local')
|
|
488
|
-
console.error('')
|
|
488
|
+
console.error('❌ Error: DB binding not found. Run migrations first: npm run db:migrate:local')
|
|
489
489
|
process.exit(1)
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
-
const db = createDb(env.DB)
|
|
493
|
-
|
|
494
492
|
try {
|
|
495
493
|
// Check if admin user already exists
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
.from(users)
|
|
499
|
-
.where(eq(users.email, '${email}'))
|
|
500
|
-
.get()
|
|
501
|
-
|
|
502
|
-
if (existingUser) {
|
|
494
|
+
const existing = await env.DB.prepare('SELECT id FROM auth_user WHERE email = ?').bind('${email}').first()
|
|
495
|
+
if (existing) {
|
|
503
496
|
console.log('✓ Admin user already exists')
|
|
504
|
-
|
|
505
|
-
console.log(\` Role: \${existingUser.role}\`)
|
|
497
|
+
await dispose()
|
|
506
498
|
return
|
|
507
499
|
}
|
|
508
500
|
|
|
509
|
-
|
|
510
|
-
const
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
.
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
isActive: true,
|
|
527
|
-
createdAt: now,
|
|
528
|
-
updatedAt: now
|
|
529
|
-
})
|
|
530
|
-
.run()
|
|
501
|
+
const passwordHash = await hashPassword('${password}')
|
|
502
|
+
const nowMs = Date.now()
|
|
503
|
+
const odid = \`admin-\${nowMs}-\${Math.random().toString(36).substr(2, 9)}\`
|
|
504
|
+
|
|
505
|
+
await env.DB.batch([
|
|
506
|
+
env.DB.prepare(
|
|
507
|
+
'INSERT INTO auth_user (id, email, first_name, last_name, role, is_active, created_at, updated_at, name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
|
508
|
+
).bind(odid, '${email}', 'Admin', 'User', 'admin', 1, nowMs, nowMs, 'Admin User'),
|
|
509
|
+
env.DB.prepare(
|
|
510
|
+
'INSERT INTO auth_account (id, user_id, account_id, provider_id, password, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
511
|
+
).bind(crypto.randomUUID(), odid, odid, 'credential', passwordHash, nowMs, nowMs),
|
|
512
|
+
])
|
|
513
|
+
|
|
514
|
+
await bootstrapDocumentTypes(env.DB)
|
|
515
|
+
const rbac = new RbacService(env.DB)
|
|
516
|
+
await rbac.ensureSystemRbacSeed()
|
|
517
|
+
await rbac.addUserRoleByName(odid, 'admin')
|
|
531
518
|
|
|
532
519
|
console.log('✓ Admin user created successfully')
|
|
533
520
|
console.log(\` Email: ${email}\`)
|
|
534
521
|
console.log(\` Role: admin\`)
|
|
535
|
-
console.log('')
|
|
536
|
-
console.log('You can now login at: http://localhost:8787/auth/login')
|
|
537
522
|
} catch (error) {
|
|
538
523
|
console.error('❌ Error creating admin user:', error)
|
|
539
524
|
await dispose()
|
|
540
525
|
process.exit(1)
|
|
541
526
|
}
|
|
542
527
|
|
|
543
|
-
// Clean up the platform proxy
|
|
544
528
|
await dispose()
|
|
545
529
|
}
|
|
546
530
|
|
|
547
|
-
// Run seed
|
|
548
531
|
seed()
|
|
549
532
|
.then(() => {
|
|
550
|
-
console.log('')
|
|
551
533
|
console.log('✓ Seeding complete')
|
|
552
534
|
process.exit(0)
|
|
553
535
|
})
|
|
@@ -774,49 +756,25 @@ function printSuccessMessage(answers) {
|
|
|
774
756
|
console.log()
|
|
775
757
|
console.log(kleur.bold().green('🎉 Success!'))
|
|
776
758
|
console.log()
|
|
777
|
-
console.log(kleur.bold('
|
|
759
|
+
console.log(kleur.bold('Get started:'))
|
|
778
760
|
console.log()
|
|
779
761
|
console.log(kleur.cyan(` cd ${projectName}`))
|
|
780
762
|
|
|
781
763
|
if (skipInstall) {
|
|
782
764
|
console.log(kleur.cyan(' npm install'))
|
|
783
765
|
console.log()
|
|
784
|
-
console.log(kleur.yellow('⚠
|
|
766
|
+
console.log(kleur.yellow('⚠ After npm install, copy migrations:'))
|
|
785
767
|
console.log(kleur.dim(' cp -r node_modules/@sonicjs-cms/core/migrations ./'))
|
|
786
768
|
}
|
|
787
769
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
console.log()
|
|
791
|
-
console.log(kleur.bold('Create Cloudflare resources:'))
|
|
792
|
-
if (!databaseIdSet) {
|
|
793
|
-
console.log(kleur.cyan(` wrangler d1 create ${answers.databaseName}`))
|
|
794
|
-
console.log(kleur.dim(' # Copy database_id to wrangler.toml'))
|
|
795
|
-
}
|
|
796
|
-
console.log(kleur.cyan(` wrangler r2 bucket create ${answers.bucketName}`))
|
|
770
|
+
if (!migrationsRan) {
|
|
771
|
+
console.log(kleur.cyan(' npm run db:migrate:local'))
|
|
797
772
|
}
|
|
798
773
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
const needsSeeding = seedAdmin && !adminSeeded
|
|
802
|
-
|
|
803
|
-
if (needsMigrations || needsSeeding) {
|
|
804
|
-
console.log()
|
|
805
|
-
console.log(kleur.bold('Complete setup:'))
|
|
806
|
-
if (needsMigrations) {
|
|
807
|
-
console.log(kleur.cyan(' npm run db:migrate:local'))
|
|
808
|
-
}
|
|
809
|
-
if (needsSeeding) {
|
|
810
|
-
console.log(kleur.cyan(' npm run seed'))
|
|
811
|
-
}
|
|
774
|
+
if (seedAdmin && !adminSeeded) {
|
|
775
|
+
console.log(kleur.cyan(' npm run seed'))
|
|
812
776
|
}
|
|
813
777
|
|
|
814
|
-
console.log()
|
|
815
|
-
if (migrationsRan && (!seedAdmin || adminSeeded)) {
|
|
816
|
-
console.log(kleur.bold().green('✓ Database is ready! Start development:'))
|
|
817
|
-
} else {
|
|
818
|
-
console.log(kleur.bold('Start development:'))
|
|
819
|
-
}
|
|
820
778
|
console.log(kleur.cyan(' npm run dev'))
|
|
821
779
|
|
|
822
780
|
if (seedAdmin && answers.adminEmail) {
|
|
@@ -826,15 +784,14 @@ function printSuccessMessage(answers) {
|
|
|
826
784
|
console.log(kleur.dim(` Password: [as entered]`))
|
|
827
785
|
}
|
|
828
786
|
|
|
829
|
-
if (migrationsRan && (!seedAdmin || adminSeeded)) {
|
|
830
|
-
console.log()
|
|
831
|
-
console.log(kleur.green('✓ Everything is set up! Just run npm run dev and login.'))
|
|
832
|
-
}
|
|
833
|
-
|
|
834
787
|
console.log()
|
|
835
788
|
console.log(kleur.bold('Visit:'))
|
|
836
789
|
console.log(kleur.cyan(' http://localhost:8787/admin'))
|
|
837
790
|
|
|
791
|
+
console.log()
|
|
792
|
+
console.log(kleur.bold('Deploy to Cloudflare (when ready):'))
|
|
793
|
+
console.log(kleur.cyan(' npm run deploy'))
|
|
794
|
+
|
|
838
795
|
console.log()
|
|
839
796
|
console.log(kleur.dim('Need help? Visit https://sonicjs.com'))
|
|
840
797
|
console.log()
|