create-sonicjs 3.0.0-beta.3 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +84 -98
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sonicjs",
3
- "version": "3.0.0-beta.3",
3
+ "version": "3.0.0-beta.4",
4
4
  "description": "Create a new SonicJS application with zero configuration",
5
5
  "type": "module",
6
6
  "bin": {
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,39 +174,40 @@ async function getProjectDetails(initialName) {
171
174
  })
172
175
  }
173
176
 
174
- // Seed admin user
175
- questions.push({
176
- type: 'confirm',
177
- name: 'seedAdmin',
178
- message: 'Create admin user?',
179
- initial: true
180
- })
177
+ // Seed admin user (skip prompt if credentials provided via flags)
178
+ if (!flags.adminEmail || !flags.adminPassword) {
179
+ questions.push({
180
+ type: 'confirm',
181
+ name: 'seedAdmin',
182
+ message: 'Create admin user?',
183
+ initial: true
184
+ })
181
185
 
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
- })
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
+ })
195
198
 
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
- })
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
+ })
210
+ }
207
211
 
208
212
  // Create Cloudflare resources
209
213
  if (!flags.skipCloudflare) {
@@ -236,9 +240,9 @@ async function getProjectDetails(initialName) {
236
240
  template: flags.template || 'starter', // Always default to starter template
237
241
  databaseName: flags.databaseName || answers.databaseName || `${initialName || answers.projectName}-db`,
238
242
  bucketName: flags.bucketName || answers.bucketName || `${initialName || answers.projectName}-media`,
239
- seedAdmin: answers.seedAdmin !== undefined ? answers.seedAdmin : true,
240
- adminEmail: answers.adminEmail,
241
- adminPassword: answers.adminPassword,
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,
242
246
  includeExample: true,
243
247
  createResources: flags.skipCloudflare ? false : answers.createResources,
244
248
  runMigrations: true, // Always run migrations automatically
@@ -405,7 +409,7 @@ async function copyTemplate(templateName, targetDir, options) {
405
409
 
406
410
  // Add @sonicjs-cms/core dependency
407
411
  packageJson.dependencies = {
408
- '@sonicjs-cms/core': '^3.0.0-beta.3',
412
+ '@sonicjs-cms/core': '^3.0.0-beta.4',
409
413
  ...packageJson.dependencies
410
414
  }
411
415
 
@@ -444,100 +448,88 @@ async function copyTemplate(templateName, targetDir, options) {
444
448
  password: options.adminPassword
445
449
  })
446
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
+ }
447
458
  }
448
459
 
449
460
  async function createAdminSeedScript(targetDir, { email, password }) {
450
- const seedScriptContent = `import { createDb, users } from '@sonicjs-cms/core'
451
- import { eq } from 'drizzle-orm'
452
- import * as crypto from 'crypto'
461
+ const seedScriptContent = `import { bootstrapDocumentTypes, RbacService } from '@sonicjs-cms/core'
453
462
  import { getPlatformProxy } from 'wrangler'
454
463
 
455
464
  /**
456
465
  * Seed script to create initial admin user
457
466
  *
458
- * Run this script after migrations:
459
- * npm run db:migrate:local
460
- * npm run seed
461
- *
462
467
  * Admin credentials:
463
468
  * Email: ${email}
464
469
  * Password: [as entered during setup]
465
470
  */
466
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
+
467
484
  async function seed() {
468
- // Get D1 database from Cloudflare environment using wrangler's getPlatformProxy
469
485
  const { env, dispose } = await getPlatformProxy()
470
486
 
471
487
  if (!env?.DB) {
472
- console.error('❌ Error: DB binding not found')
473
- console.error('')
474
- console.error('Make sure you have:')
475
- console.error('1. Created your D1 database: wrangler d1 create <database-name>')
476
- console.error('2. Updated wrangler.toml with the database_id')
477
- console.error('3. Run migrations: npm run db:migrate:local')
478
- console.error('')
488
+ console.error('❌ Error: DB binding not found. Run migrations first: npm run db:migrate:local')
479
489
  process.exit(1)
480
490
  }
481
491
 
482
- const db = createDb(env.DB)
483
-
484
492
  try {
485
493
  // Check if admin user already exists
486
- const existingUser = await db
487
- .select()
488
- .from(users)
489
- .where(eq(users.email, '${email}'))
490
- .get()
491
-
492
- if (existingUser) {
494
+ const existing = await env.DB.prepare('SELECT id FROM auth_user WHERE email = ?').bind('${email}').first()
495
+ if (existing) {
493
496
  console.log('✓ Admin user already exists')
494
- console.log(\` Email: ${email}\`)
495
- console.log(\` Role: \${existingUser.role}\`)
497
+ await dispose()
496
498
  return
497
499
  }
498
500
 
499
- // Hash password using SHA-256 (same as SonicJS auth system)
500
- const data = '${password}' + 'salt-change-in-production'
501
- const passwordHash = crypto.createHash('sha256').update(data).digest('hex')
502
- const now = new Date()
503
- const odid = \`admin-\${now.getTime()}-\${Math.random().toString(36).substr(2, 9)}\`
504
-
505
- // Create admin user
506
- await db
507
- .insert(users)
508
- .values({
509
- id: odid,
510
- email: '${email}',
511
- username: '${email.split('@')[0]}',
512
- firstName: 'Admin',
513
- lastName: 'User',
514
- passwordHash: passwordHash,
515
- role: 'admin',
516
- isActive: true,
517
- createdAt: now,
518
- updatedAt: now
519
- })
520
- .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')
521
518
 
522
519
  console.log('✓ Admin user created successfully')
523
520
  console.log(\` Email: ${email}\`)
524
521
  console.log(\` Role: admin\`)
525
- console.log('')
526
- console.log('You can now login at: http://localhost:8787/auth/login')
527
522
  } catch (error) {
528
523
  console.error('❌ Error creating admin user:', error)
529
524
  await dispose()
530
525
  process.exit(1)
531
526
  }
532
527
 
533
- // Clean up the platform proxy
534
528
  await dispose()
535
529
  }
536
530
 
537
- // Run seed
538
531
  seed()
539
532
  .then(() => {
540
- console.log('')
541
533
  console.log('✓ Seeding complete')
542
534
  process.exit(0)
543
535
  })
@@ -796,15 +788,9 @@ function printSuccessMessage(answers) {
796
788
  console.log(kleur.bold('Visit:'))
797
789
  console.log(kleur.cyan(' http://localhost:8787/admin'))
798
790
 
799
- if (!createResources || !resourcesCreated) {
800
- console.log()
801
- console.log(kleur.bold('Deploy to Cloudflare (when ready):'))
802
- if (!databaseIdSet) {
803
- console.log(kleur.cyan(` wrangler d1 create ${answers.databaseName}`))
804
- console.log(kleur.dim(' # Copy database_id to wrangler.toml'))
805
- }
806
- console.log(kleur.cyan(` wrangler r2 bucket create ${answers.bucketName}`))
807
- }
791
+ console.log()
792
+ console.log(kleur.bold('Deploy to Cloudflare (when ready):'))
793
+ console.log(kleur.cyan(' npm run deploy'))
808
794
 
809
795
  console.log()
810
796
  console.log(kleur.dim('Need help? Visit https://sonicjs.com'))