create-sks-admin 0.2.0 → 1.0.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.
Files changed (2) hide show
  1. package/bin/cli.js +774 -140
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -4,10 +4,21 @@ import { execSync } from 'child_process'
4
4
  import fs from 'fs'
5
5
  import path from 'path'
6
6
  import { fileURLToPath } from 'url'
7
+ import { createHash } from 'crypto'
7
8
  import prompts from 'prompts'
8
9
 
9
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
10
11
 
12
+ // License key validation (SHA256 hashed - original key not visible)
13
+ const VALID_KEY_HASHES = [
14
+ 'c34e63049262e049b26ea19f39b4a9635e6bab0fa7754e7dbf1b66d6b53de680', // Primary key
15
+ ]
16
+
17
+ function validateLicenseKey(key) {
18
+ const hash = createHash('sha256').update(key.trim()).digest('hex')
19
+ return VALID_KEY_HASHES.includes(hash)
20
+ }
21
+
11
22
  // Colors for terminal
12
23
  const colors = {
13
24
  green: (t) => `\x1b[32m${t}\x1b[0m`,
@@ -15,6 +26,7 @@ const colors = {
15
26
  yellow: (t) => `\x1b[33m${t}\x1b[0m`,
16
27
  red: (t) => `\x1b[31m${t}\x1b[0m`,
17
28
  cyan: (t) => `\x1b[36m${t}\x1b[0m`,
29
+ magenta: (t) => `\x1b[35m${t}\x1b[0m`,
18
30
  }
19
31
 
20
32
  const log = {
@@ -25,13 +37,24 @@ const log = {
25
37
  }
26
38
 
27
39
  // Get project name from args
28
- const projectName = process.argv[2] || 'sks-admin-test'
40
+ const projectName = process.argv[2]
41
+
42
+ if (!projectName) {
43
+ console.log(`
44
+ ${colors.cyan('Usage:')} npx create-sks-admin ${colors.yellow('<project-name>')}
45
+
46
+ ${colors.cyan('Examples:')}
47
+ npx create-sks-admin my-admin-app
48
+ npx create-sks-admin . ${colors.blue('(current directory)')}
49
+ `)
50
+ process.exit(1)
51
+ }
29
52
 
30
53
  console.log(`
31
- ╔═══════════════════════════════════════╗
32
- ║ ${colors.cyan('@sks/create-admin CLI')}
33
- Creating: ${projectName.padEnd(22)}
34
- ╚═══════════════════════════════════════╝
54
+ ${colors.cyan('╔═══════════════════════════════════════════════════╗')}
55
+ ${colors.cyan('')} ${colors.magenta('SKS Admin Panel')} - Production Ready ${colors.cyan('║')}
56
+ ${colors.cyan('')} ${colors.blue('Next.js 16 + Prisma + NextAuth + Shadcn')} ${colors.cyan('║')}
57
+ ${colors.cyan('╚═══════════════════════════════════════════════════╝')}
35
58
  `)
36
59
 
37
60
  const projectPath = path.resolve(process.cwd(), projectName)
@@ -43,124 +66,470 @@ if (!isCurrentDir && fs.existsSync(projectPath)) {
43
66
  process.exit(1)
44
67
  }
45
68
 
46
- // For current directory, check if it's empty (allow .git and other hidden files)
69
+ // For current directory, check if it's empty
47
70
  if (isCurrentDir) {
48
71
  const files = fs.readdirSync(projectPath).filter(f => !f.startsWith('.'))
49
72
  if (files.length > 0) {
50
- log.error('Current directory is not empty! Use an empty folder or provide a project name.')
73
+ log.error('Current directory is not empty!')
51
74
  process.exit(1)
52
75
  }
53
76
  }
54
77
 
55
- // Ask for token
56
- console.log('')
57
- console.log(colors.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
58
- console.log(colors.yellow(' Premium Admin Panel Components'))
59
- console.log(colors.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
78
+ // Ask for license key
79
+ console.log(colors.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
80
+ console.log(colors.yellow(' License Verification'))
81
+ console.log(colors.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
60
82
  console.log('')
61
83
 
62
- const response = await prompts({
84
+ const keyResponse = await prompts({
63
85
  type: 'password',
64
- name: 'token',
65
- message: 'Enter access token (or press Enter to skip):',
86
+ name: 'licenseKey',
87
+ message: 'Enter license key (or press Enter for basic version):',
66
88
  })
67
89
 
68
- const hasToken = response.token && response.token.trim().length > 0
69
- const authToken = response.token?.trim()
90
+ const licenseKey = keyResponse.licenseKey?.trim() || ''
91
+ const isPremium = licenseKey.length > 0 && validateLicenseKey(licenseKey)
70
92
 
71
- if (hasToken) {
72
- log.success('Token received - Premium components will be installed')
73
- } else {
74
- log.info('No token - Basic template will be created')
93
+ if (licenseKey.length > 0 && !isPremium) {
94
+ log.error('Invalid license key!')
95
+ console.log(colors.yellow('Continuing with basic version...'))
96
+ console.log('')
97
+ } else if (isPremium) {
98
+ log.success('License verified! Premium features unlocked.')
99
+ console.log('')
75
100
  }
76
101
 
77
- console.log('')
102
+ // Database configuration (only for premium)
103
+ let dbConfig = null
104
+ if (isPremium) {
105
+ console.log(colors.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
106
+ console.log(colors.cyan(' Database Configuration'))
107
+ console.log(colors.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
108
+ console.log('')
109
+
110
+ dbConfig = await prompts([
111
+ {
112
+ type: 'select',
113
+ name: 'provider',
114
+ message: 'Database provider:',
115
+ choices: [
116
+ { title: 'PostgreSQL (Recommended)', value: 'postgresql' },
117
+ { title: 'MySQL', value: 'mysql' },
118
+ { title: 'SQLite (Local dev)', value: 'sqlite' },
119
+ ],
120
+ initial: 0,
121
+ },
122
+ {
123
+ type: (prev) => prev !== 'sqlite' ? 'text' : null,
124
+ name: 'host',
125
+ message: 'Database host:',
126
+ initial: 'localhost',
127
+ },
128
+ {
129
+ type: (prev, values) => values.provider !== 'sqlite' ? 'number' : null,
130
+ name: 'port',
131
+ message: 'Database port:',
132
+ initial: (prev, values) => values.provider === 'mysql' ? 3306 : 5432,
133
+ },
134
+ {
135
+ type: (prev, values) => values.provider !== 'sqlite' ? 'text' : null,
136
+ name: 'database',
137
+ message: 'Database name:',
138
+ initial: 'admin_db',
139
+ },
140
+ {
141
+ type: (prev, values) => values.provider !== 'sqlite' ? 'text' : null,
142
+ name: 'username',
143
+ message: 'Database username:',
144
+ initial: 'postgres',
145
+ },
146
+ {
147
+ type: (prev, values) => values.provider !== 'sqlite' ? 'password' : null,
148
+ name: 'password',
149
+ message: 'Database password:',
150
+ },
151
+ ])
152
+ console.log('')
153
+ }
78
154
 
79
155
  try {
80
156
  // Step 1: Create Next.js app
81
- log.info('Creating Next.js app...')
157
+ log.info('Creating Next.js application...')
82
158
  execSync(
83
159
  `npx create-next-app@latest ${projectName} --typescript --tailwind --eslint --app --src-dir=false --import-alias="@/*" --turbopack`,
84
160
  { stdio: 'inherit' }
85
161
  )
86
162
  log.success('Next.js app created')
87
163
 
88
- // Step 2: Navigate to project
164
+ // Navigate to project
89
165
  process.chdir(projectPath)
90
166
 
91
- // Step 3: Create .npmrc for GitHub Packages (with token if provided)
92
- log.info('Configuring npm registry...')
93
- if (hasToken) {
94
- fs.writeFileSync(
95
- '.npmrc',
96
- `@sks:registry=https://npm.pkg.github.com
97
- //npm.pkg.github.com/:_authToken=${authToken}
98
- `
167
+ if (isPremium) {
168
+ // Step 2: Install dependencies
169
+ log.info('Installing premium dependencies...')
170
+ execSync(
171
+ 'npm install prisma @prisma/client next-auth@beta @auth/prisma-adapter bcryptjs zod react-hook-form @hookform/resolvers sonner',
172
+ { stdio: 'inherit' }
99
173
  )
100
- } else {
101
- fs.writeFileSync(
102
- '.npmrc',
103
- `@sks:registry=https://npm.pkg.github.com
104
- # Add your token here to install premium packages:
105
- # //npm.pkg.github.com/:_authToken=YOUR_TOKEN
106
- `
174
+ execSync(
175
+ 'npm install -D @types/bcryptjs',
176
+ { stdio: 'inherit' }
107
177
  )
108
- }
109
- log.success('.npmrc created')
110
-
111
- // Step 4: Install @sks packages only if token provided
112
- let packagesInstalled = false
113
- if (hasToken) {
114
- log.info('Installing @sks premium packages...')
115
- try {
116
- execSync('npm install @sks/admin-core @sks/admin-ui', { stdio: 'inherit' })
117
- log.success('@sks packages installed')
118
- packagesInstalled = true
119
- } catch {
120
- log.warn('Could not install @sks packages - token may be invalid')
121
- log.info('Check your token and run: npm install @sks/admin-core @sks/admin-ui')
178
+ log.success('Dependencies installed')
179
+
180
+ // Step 3: Install Shadcn components
181
+ log.info('Installing Shadcn UI components...')
182
+ execSync('npx shadcn@latest init -d', { stdio: 'inherit' })
183
+ execSync(
184
+ 'npx shadcn@latest add button input label card form toast dialog dropdown-menu avatar badge table tabs separator alert alert-dialog checkbox select textarea switch skeleton sheet sidebar tooltip',
185
+ { stdio: 'inherit' }
186
+ )
187
+ log.success('Shadcn components installed')
188
+
189
+ // Step 4: Initialize Prisma
190
+ log.info('Setting up Prisma...')
191
+ execSync(`npx prisma init --datasource-provider ${dbConfig.provider}`, { stdio: 'inherit' })
192
+ log.success('Prisma initialized')
193
+
194
+ // Step 5: Create .env file
195
+ log.info('Creating environment files...')
196
+ let databaseUrl = ''
197
+ if (dbConfig.provider === 'sqlite') {
198
+ databaseUrl = 'file:./dev.db'
199
+ } else if (dbConfig.provider === 'postgresql') {
200
+ databaseUrl = `postgresql://${dbConfig.username}:${dbConfig.password}@${dbConfig.host}:${dbConfig.port}/${dbConfig.database}?schema=public`
201
+ } else if (dbConfig.provider === 'mysql') {
202
+ databaseUrl = `mysql://${dbConfig.username}:${dbConfig.password}@${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`
122
203
  }
123
- } else {
124
- log.info('Skipping premium packages (no token)')
204
+
205
+ fs.writeFileSync('.env', `# Database
206
+ DATABASE_URL="${databaseUrl}"
207
+
208
+ # NextAuth
209
+ AUTH_SECRET="${createHash('sha256').update(Date.now().toString()).digest('hex').slice(0, 32)}"
210
+ AUTH_TRUST_HOST=true
211
+
212
+ # App
213
+ NEXT_PUBLIC_APP_URL="http://localhost:3000"
214
+ `)
215
+
216
+ fs.writeFileSync('.env.example', `# Database
217
+ DATABASE_URL="postgresql://user:password@localhost:5432/admin_db?schema=public"
218
+
219
+ # NextAuth
220
+ AUTH_SECRET="your-secret-key-here"
221
+ AUTH_TRUST_HOST=true
222
+
223
+ # App
224
+ NEXT_PUBLIC_APP_URL="http://localhost:3000"
225
+ `)
226
+ log.success('Environment files created')
227
+
228
+ // Step 6: Create Prisma schema
229
+ log.info('Creating database schema...')
230
+ fs.writeFileSync('prisma/schema.prisma', `generator client {
231
+ provider = "prisma-client-js"
232
+ }
233
+
234
+ datasource db {
235
+ provider = "${dbConfig.provider}"
236
+ url = env("DATABASE_URL")
237
+ }
238
+
239
+ enum Role {
240
+ USER
241
+ ADMIN
242
+ SUPER_ADMIN
243
+ }
244
+
245
+ model User {
246
+ id String @id @default(cuid())
247
+ name String?
248
+ email String @unique
249
+ emailVerified DateTime?
250
+ image String?
251
+ password String?
252
+ role Role @default(USER)
253
+ createdAt DateTime @default(now())
254
+ updatedAt DateTime @updatedAt
255
+ accounts Account[]
256
+ sessions Session[]
257
+ }
258
+
259
+ model Account {
260
+ userId String
261
+ type String
262
+ provider String
263
+ providerAccountId String
264
+ refresh_token String?
265
+ access_token String?
266
+ expires_at Int?
267
+ token_type String?
268
+ scope String?
269
+ id_token String?
270
+ session_state String?
271
+ createdAt DateTime @default(now())
272
+ updatedAt DateTime @updatedAt
273
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
274
+
275
+ @@id([provider, providerAccountId])
276
+ }
277
+
278
+ model Session {
279
+ sessionToken String @unique
280
+ userId String
281
+ expires DateTime
282
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
283
+ createdAt DateTime @default(now())
284
+ updatedAt DateTime @updatedAt
285
+ }
286
+
287
+ model VerificationToken {
288
+ identifier String
289
+ token String
290
+ expires DateTime
291
+
292
+ @@id([identifier, token])
293
+ }
294
+ `)
295
+ log.success('Database schema created')
296
+
297
+ // Step 7: Create lib files
298
+ log.info('Creating library files...')
299
+ fs.mkdirSync('lib', { recursive: true })
300
+
301
+ // lib/db.ts
302
+ fs.writeFileSync('lib/db.ts', `import { PrismaClient } from '@prisma/client'
303
+
304
+ const globalForPrisma = globalThis as unknown as {
305
+ prisma: PrismaClient | undefined
306
+ }
307
+
308
+ export const prisma = globalForPrisma.prisma ?? new PrismaClient()
309
+
310
+ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
311
+
312
+ export default prisma
313
+ `)
314
+
315
+ // lib/utils.ts (update existing)
316
+ const utilsContent = fs.readFileSync('lib/utils.ts', 'utf-8')
317
+ fs.writeFileSync('lib/utils.ts', utilsContent + `
318
+
319
+ export function formatDate(date: Date | string) {
320
+ return new Intl.DateTimeFormat('en-US', {
321
+ month: 'short',
322
+ day: 'numeric',
323
+ year: 'numeric',
324
+ }).format(new Date(date))
325
+ }
326
+ `)
327
+
328
+ // lib/dal.ts - Data Access Layer
329
+ fs.writeFileSync('lib/dal.ts', `import { auth } from '@/auth'
330
+ import { redirect } from 'next/navigation'
331
+ import { cache } from 'react'
332
+
333
+ export const verifySession = cache(async () => {
334
+ const session = await auth()
335
+
336
+ if (!session?.user) {
337
+ redirect('/admin/login')
338
+ }
339
+
340
+ return session.user
341
+ })
342
+
343
+ export const verifyAdminSession = cache(async () => {
344
+ const session = await auth()
345
+
346
+ if (!session?.user) {
347
+ redirect('/admin/login')
348
+ }
349
+
350
+ const role = session.user.role
351
+ if (role !== 'ADMIN' && role !== 'SUPER_ADMIN') {
352
+ redirect('/unauthorized')
353
+ }
354
+
355
+ return session.user
356
+ })
357
+
358
+ export const getOptionalSession = cache(async () => {
359
+ const session = await auth()
360
+ return session?.user ?? null
361
+ })
362
+ `)
363
+
364
+ // lib/api/response.ts
365
+ fs.mkdirSync('lib/api', { recursive: true })
366
+ fs.writeFileSync('lib/api/response.ts', `import { NextResponse } from 'next/server'
367
+
368
+ interface ApiSuccessResponse<T> {
369
+ success: true
370
+ data: T
371
+ meta: {
372
+ timestamp: string
125
373
  }
374
+ }
375
+
376
+ interface ApiErrorResponse {
377
+ success: false
378
+ error: {
379
+ code: string
380
+ message: string
381
+ details?: unknown
382
+ }
383
+ meta: {
384
+ timestamp: string
385
+ }
386
+ }
126
387
 
127
- // Step 5: Create lib/dal.ts
128
- log.info('Creating boilerplate files...')
129
-
130
- fs.mkdirSync('lib', { recursive: true })
131
- fs.writeFileSync(
132
- 'lib/dal.ts',
133
- `// Data Access Layer for Authentication
134
- // Uncomment after installing @sks/admin-core
135
-
136
- // import { createDAL } from '@sks/admin-core/auth'
137
- // import { auth } from '@/auth'
138
-
139
- // export const {
140
- // verifySession,
141
- // verifyAdminSession,
142
- // redirectIfAuthenticated,
143
- // } = createDAL({
144
- // auth,
145
- // loginPath: '/admin/login',
146
- // adminRoles: ['ADMIN'],
147
- // })
148
-
149
- // Placeholder exports for now
150
- export const verifySession = async () => ({ userId: '1', email: 'test@test.com', role: 'ADMIN' })
151
- export const verifyAdminSession = verifySession
152
- `
388
+ export function apiSuccess<T>(data: T, status = 200): NextResponse<ApiSuccessResponse<T>> {
389
+ return NextResponse.json(
390
+ {
391
+ success: true,
392
+ data,
393
+ meta: { timestamp: new Date().toISOString() },
394
+ },
395
+ { status }
153
396
  )
397
+ }
154
398
 
155
- // Step 6: Create admin layout
156
- fs.mkdirSync('app/admin', { recursive: true })
157
- fs.writeFileSync(
158
- 'app/admin/layout.tsx',
159
- `// Admin Layout with @sks/admin-ui
160
- // Uncomment after installing packages
399
+ export function apiError(
400
+ code: string,
401
+ message: string,
402
+ details?: unknown,
403
+ status = 500
404
+ ): NextResponse<ApiErrorResponse> {
405
+ return NextResponse.json(
406
+ {
407
+ success: false,
408
+ error: { code, message, details },
409
+ meta: { timestamp: new Date().toISOString() },
410
+ },
411
+ { status }
412
+ )
413
+ }
161
414
 
162
- // import { AdminShell, ThemeProvider } from '@sks/admin-ui'
163
- // import '@sks/admin-ui/themes/base.css'
415
+ export const ErrorCodes = {
416
+ UNAUTHORIZED: 'UNAUTHORIZED',
417
+ FORBIDDEN: 'FORBIDDEN',
418
+ NOT_FOUND: 'NOT_FOUND',
419
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
420
+ INTERNAL_ERROR: 'INTERNAL_ERROR',
421
+ } as const
422
+ `)
423
+
424
+ // Step 8: Create auth.ts
425
+ log.info('Setting up authentication...')
426
+ fs.writeFileSync('auth.ts', `import NextAuth from 'next-auth'
427
+ import { PrismaAdapter } from '@auth/prisma-adapter'
428
+ import Credentials from 'next-auth/providers/credentials'
429
+ import bcrypt from 'bcryptjs'
430
+ import prisma from '@/lib/db'
431
+
432
+ export const { handlers, signIn, signOut, auth } = NextAuth({
433
+ adapter: PrismaAdapter(prisma),
434
+ session: { strategy: 'jwt' },
435
+ pages: {
436
+ signIn: '/admin/login',
437
+ },
438
+ providers: [
439
+ Credentials({
440
+ name: 'credentials',
441
+ credentials: {
442
+ email: { label: 'Email', type: 'email' },
443
+ password: { label: 'Password', type: 'password' },
444
+ },
445
+ async authorize(credentials) {
446
+ if (!credentials?.email || !credentials?.password) {
447
+ return null
448
+ }
449
+
450
+ const user = await prisma.user.findUnique({
451
+ where: { email: credentials.email as string },
452
+ })
453
+
454
+ if (!user || !user.password) {
455
+ return null
456
+ }
457
+
458
+ const passwordMatch = await bcrypt.compare(
459
+ credentials.password as string,
460
+ user.password
461
+ )
462
+
463
+ if (!passwordMatch) {
464
+ return null
465
+ }
466
+
467
+ return {
468
+ id: user.id,
469
+ email: user.email,
470
+ name: user.name,
471
+ role: user.role,
472
+ }
473
+ },
474
+ }),
475
+ ],
476
+ callbacks: {
477
+ async jwt({ token, user }) {
478
+ if (user) {
479
+ token.role = user.role
480
+ }
481
+ return token
482
+ },
483
+ async session({ session, token }) {
484
+ if (session.user) {
485
+ session.user.id = token.sub!
486
+ session.user.role = token.role as string
487
+ }
488
+ return session
489
+ },
490
+ },
491
+ })
492
+ `)
493
+
494
+ // types/next-auth.d.ts
495
+ fs.mkdirSync('types', { recursive: true })
496
+ fs.writeFileSync('types/next-auth.d.ts', `import { DefaultSession } from 'next-auth'
497
+
498
+ declare module 'next-auth' {
499
+ interface Session {
500
+ user: {
501
+ id: string
502
+ role: string
503
+ } & DefaultSession['user']
504
+ }
505
+
506
+ interface User {
507
+ role: string
508
+ }
509
+ }
510
+
511
+ declare module 'next-auth/jwt' {
512
+ interface JWT {
513
+ role?: string
514
+ }
515
+ }
516
+ `)
517
+
518
+ // Step 9: Create API route for auth
519
+ fs.mkdirSync('app/api/auth/[...nextauth]', { recursive: true })
520
+ fs.writeFileSync('app/api/auth/[...nextauth]/route.ts', `import { handlers } from '@/auth'
521
+
522
+ export const { GET, POST } = handlers
523
+ `)
524
+
525
+ // Step 10: Create admin pages
526
+ log.info('Creating admin pages...')
527
+
528
+ // Admin layout
529
+ fs.mkdirSync('app/admin', { recursive: true })
530
+ fs.writeFileSync('app/admin/layout.tsx', `import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'
531
+ import { AppSidebar } from '@/components/app-sidebar'
532
+ import { Toaster } from '@/components/ui/sonner'
164
533
 
165
534
  export default function AdminLayout({
166
535
  children,
@@ -168,76 +537,341 @@ export default function AdminLayout({
168
537
  children: React.ReactNode
169
538
  }) {
170
539
  return (
171
- <div className="min-h-screen bg-gray-100">
172
- {/* Replace with AdminShell after installing @sks/admin-ui */}
173
- <aside className="fixed left-0 top-0 h-screen w-64 bg-gray-900 text-white p-4">
174
- <h1 className="text-xl font-bold mb-8">Admin Panel</h1>
175
- <nav className="space-y-2">
176
- <a href="/admin" className="block p-2 rounded hover:bg-gray-800">Dashboard</a>
177
- <a href="/admin/users" className="block p-2 rounded hover:bg-gray-800">Users</a>
178
- <a href="/admin/settings" className="block p-2 rounded hover:bg-gray-800">Settings</a>
179
- </nav>
180
- </aside>
181
- <main className="ml-64 p-8">
182
- {children}
540
+ <SidebarProvider>
541
+ <AppSidebar />
542
+ <main className="flex-1">
543
+ <header className="flex h-14 items-center gap-4 border-b bg-background px-6">
544
+ <SidebarTrigger />
545
+ </header>
546
+ <div className="p-6">
547
+ {children}
548
+ </div>
183
549
  </main>
550
+ <Toaster />
551
+ </SidebarProvider>
552
+ )
553
+ }
554
+ `)
555
+
556
+ // Admin dashboard
557
+ fs.writeFileSync('app/admin/page.tsx', `import { verifyAdminSession } from '@/lib/dal'
558
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
559
+
560
+ export default async function AdminDashboard() {
561
+ const user = await verifyAdminSession()
562
+
563
+ return (
564
+ <div className="space-y-6">
565
+ <div>
566
+ <h1 className="text-3xl font-bold">Dashboard</h1>
567
+ <p className="text-muted-foreground">Welcome back, {user.name || user.email}</p>
568
+ </div>
569
+
570
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
571
+ <Card>
572
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
573
+ <CardTitle className="text-sm font-medium">Total Users</CardTitle>
574
+ </CardHeader>
575
+ <CardContent>
576
+ <div className="text-2xl font-bold">1,234</div>
577
+ <p className="text-xs text-muted-foreground">+12% from last month</p>
578
+ </CardContent>
579
+ </Card>
580
+ <Card>
581
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
582
+ <CardTitle className="text-sm font-medium">Revenue</CardTitle>
583
+ </CardHeader>
584
+ <CardContent>
585
+ <div className="text-2xl font-bold">$12,345</div>
586
+ <p className="text-xs text-muted-foreground">+8% from last month</p>
587
+ </CardContent>
588
+ </Card>
589
+ <Card>
590
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
591
+ <CardTitle className="text-sm font-medium">Active Sessions</CardTitle>
592
+ </CardHeader>
593
+ <CardContent>
594
+ <div className="text-2xl font-bold">573</div>
595
+ <p className="text-xs text-muted-foreground">+2% from last hour</p>
596
+ </CardContent>
597
+ </Card>
598
+ <Card>
599
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
600
+ <CardTitle className="text-sm font-medium">Conversion Rate</CardTitle>
601
+ </CardHeader>
602
+ <CardContent>
603
+ <div className="text-2xl font-bold">3.2%</div>
604
+ <p className="text-xs text-muted-foreground">+0.5% from last week</p>
605
+ </CardContent>
606
+ </Card>
607
+ </div>
184
608
  </div>
185
609
  )
186
610
  }
187
- `
611
+ `)
612
+
613
+ // Login page
614
+ fs.mkdirSync('app/admin/login', { recursive: true })
615
+ fs.writeFileSync('app/admin/login/page.tsx', `'use client'
616
+
617
+ import { useState } from 'react'
618
+ import { signIn } from 'next-auth/react'
619
+ import { useRouter } from 'next/navigation'
620
+ import { Button } from '@/components/ui/button'
621
+ import { Input } from '@/components/ui/input'
622
+ import { Label } from '@/components/ui/label'
623
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
624
+
625
+ export default function LoginPage() {
626
+ const [email, setEmail] = useState('')
627
+ const [password, setPassword] = useState('')
628
+ const [error, setError] = useState('')
629
+ const [loading, setLoading] = useState(false)
630
+ const router = useRouter()
631
+
632
+ const handleSubmit = async (e: React.FormEvent) => {
633
+ e.preventDefault()
634
+ setLoading(true)
635
+ setError('')
636
+
637
+ const result = await signIn('credentials', {
638
+ email,
639
+ password,
640
+ redirect: false,
641
+ })
642
+
643
+ if (result?.error) {
644
+ setError('Invalid email or password')
645
+ setLoading(false)
646
+ } else {
647
+ router.push('/admin')
648
+ router.refresh()
649
+ }
650
+ }
651
+
652
+ return (
653
+ <div className="min-h-screen flex items-center justify-center bg-gray-100">
654
+ <Card className="w-full max-w-md">
655
+ <CardHeader className="text-center">
656
+ <CardTitle className="text-2xl">Admin Login</CardTitle>
657
+ <CardDescription>Enter your credentials to continue</CardDescription>
658
+ </CardHeader>
659
+ <CardContent>
660
+ <form onSubmit={handleSubmit} className="space-y-4">
661
+ {error && (
662
+ <div className="p-3 text-sm text-red-500 bg-red-50 rounded-md">
663
+ {error}
664
+ </div>
665
+ )}
666
+ <div className="space-y-2">
667
+ <Label htmlFor="email">Email</Label>
668
+ <Input
669
+ id="email"
670
+ type="email"
671
+ placeholder="admin@example.com"
672
+ value={email}
673
+ onChange={(e) => setEmail(e.target.value)}
674
+ required
675
+ />
676
+ </div>
677
+ <div className="space-y-2">
678
+ <Label htmlFor="password">Password</Label>
679
+ <Input
680
+ id="password"
681
+ type="password"
682
+ value={password}
683
+ onChange={(e) => setPassword(e.target.value)}
684
+ required
685
+ />
686
+ </div>
687
+ <Button type="submit" className="w-full" disabled={loading}>
688
+ {loading ? 'Signing in...' : 'Sign In'}
689
+ </Button>
690
+ </form>
691
+ </CardContent>
692
+ </Card>
693
+ </div>
188
694
  )
695
+ }
696
+ `)
189
697
 
190
- // Step 7: Create admin dashboard page
191
- fs.writeFileSync(
192
- 'app/admin/page.tsx',
193
- `export default function AdminDashboard() {
698
+ // Sidebar component
699
+ fs.mkdirSync('components', { recursive: true })
700
+ fs.writeFileSync('components/app-sidebar.tsx', `'use client'
701
+
702
+ import {
703
+ Sidebar,
704
+ SidebarContent,
705
+ SidebarFooter,
706
+ SidebarGroup,
707
+ SidebarGroupContent,
708
+ SidebarGroupLabel,
709
+ SidebarHeader,
710
+ SidebarMenu,
711
+ SidebarMenuButton,
712
+ SidebarMenuItem,
713
+ } from '@/components/ui/sidebar'
714
+ import { Home, Users, Settings, LogOut } from 'lucide-react'
715
+ import { signOut } from 'next-auth/react'
716
+ import Link from 'next/link'
717
+
718
+ const menuItems = [
719
+ { title: 'Dashboard', icon: Home, href: '/admin' },
720
+ { title: 'Users', icon: Users, href: '/admin/users' },
721
+ { title: 'Settings', icon: Settings, href: '/admin/settings' },
722
+ ]
723
+
724
+ export function AppSidebar() {
194
725
  return (
195
- <div>
196
- <h1 className="text-3xl font-bold mb-4">Dashboard</h1>
197
- <div className="grid grid-cols-3 gap-4">
198
- <div className="bg-white p-6 rounded-lg shadow">
199
- <h2 className="text-gray-500">Total Users</h2>
200
- <p className="text-3xl font-bold">1,234</p>
201
- </div>
202
- <div className="bg-white p-6 rounded-lg shadow">
203
- <h2 className="text-gray-500">Revenue</h2>
204
- <p className="text-3xl font-bold">$12,345</p>
726
+ <Sidebar>
727
+ <SidebarHeader className="border-b px-6 py-4">
728
+ <h1 className="text-xl font-bold">Admin Panel</h1>
729
+ </SidebarHeader>
730
+ <SidebarContent>
731
+ <SidebarGroup>
732
+ <SidebarGroupLabel>Menu</SidebarGroupLabel>
733
+ <SidebarGroupContent>
734
+ <SidebarMenu>
735
+ {menuItems.map((item) => (
736
+ <SidebarMenuItem key={item.title}>
737
+ <SidebarMenuButton asChild>
738
+ <Link href={item.href}>
739
+ <item.icon className="h-4 w-4" />
740
+ <span>{item.title}</span>
741
+ </Link>
742
+ </SidebarMenuButton>
743
+ </SidebarMenuItem>
744
+ ))}
745
+ </SidebarMenu>
746
+ </SidebarGroupContent>
747
+ </SidebarGroup>
748
+ </SidebarContent>
749
+ <SidebarFooter className="border-t p-4">
750
+ <SidebarMenu>
751
+ <SidebarMenuItem>
752
+ <SidebarMenuButton onClick={() => signOut({ callbackUrl: '/admin/login' })}>
753
+ <LogOut className="h-4 w-4" />
754
+ <span>Logout</span>
755
+ </SidebarMenuButton>
756
+ </SidebarMenuItem>
757
+ </SidebarMenu>
758
+ </SidebarFooter>
759
+ </Sidebar>
760
+ )
761
+ }
762
+ `)
763
+
764
+ // Install lucide-react
765
+ execSync('npm install lucide-react', { stdio: 'inherit' })
766
+
767
+ // Create seed script
768
+ fs.mkdirSync('prisma', { recursive: true })
769
+ fs.writeFileSync('prisma/seed.ts', `import { PrismaClient } from '@prisma/client'
770
+ import bcrypt from 'bcryptjs'
771
+
772
+ const prisma = new PrismaClient()
773
+
774
+ async function main() {
775
+ const hashedPassword = await bcrypt.hash('admin123', 10)
776
+
777
+ const admin = await prisma.user.upsert({
778
+ where: { email: 'admin@example.com' },
779
+ update: {},
780
+ create: {
781
+ email: 'admin@example.com',
782
+ name: 'Admin User',
783
+ password: hashedPassword,
784
+ role: 'ADMIN',
785
+ },
786
+ })
787
+
788
+ console.log('Created admin user:', admin.email)
789
+ }
790
+
791
+ main()
792
+ .catch((e) => {
793
+ console.error(e)
794
+ process.exit(1)
795
+ })
796
+ .finally(async () => {
797
+ await prisma.$disconnect()
798
+ })
799
+ `)
800
+
801
+ // Update package.json with prisma seed
802
+ const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
803
+ packageJson.prisma = { seed: 'npx tsx prisma/seed.ts' }
804
+ fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2))
805
+
806
+ execSync('npm install -D tsx', { stdio: 'inherit' })
807
+
808
+ log.success('Admin pages created')
809
+ } else {
810
+ // Basic version - just create simple admin page
811
+ log.info('Creating basic admin template...')
812
+ fs.mkdirSync('app/admin', { recursive: true })
813
+ fs.writeFileSync('app/admin/page.tsx', `export default function AdminPage() {
814
+ return (
815
+ <div className="min-h-screen bg-gray-100 p-8">
816
+ <div className="max-w-4xl mx-auto">
817
+ <h1 className="text-3xl font-bold mb-4">Admin Panel</h1>
818
+ <p className="text-gray-600 mb-8">Basic version - upgrade to premium for full features.</p>
819
+
820
+ <div className="grid grid-cols-3 gap-4">
821
+ <div className="bg-white p-6 rounded-lg shadow">
822
+ <h2 className="text-gray-500 text-sm">Total Users</h2>
823
+ <p className="text-3xl font-bold">--</p>
824
+ </div>
825
+ <div className="bg-white p-6 rounded-lg shadow">
826
+ <h2 className="text-gray-500 text-sm">Revenue</h2>
827
+ <p className="text-3xl font-bold">--</p>
828
+ </div>
829
+ <div className="bg-white p-6 rounded-lg shadow">
830
+ <h2 className="text-gray-500 text-sm">Orders</h2>
831
+ <p className="text-3xl font-bold">--</p>
832
+ </div>
205
833
  </div>
206
- <div className="bg-white p-6 rounded-lg shadow">
207
- <h2 className="text-gray-500">Orders</h2>
208
- <p className="text-3xl font-bold">567</p>
834
+
835
+ <div className="mt-8 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
836
+ <p className="text-yellow-800">
837
+ <strong>Upgrade to Premium</strong> to unlock: Prisma ORM, NextAuth, Shadcn UI,
838
+ Admin Dashboard, User Management, and more!
839
+ </p>
209
840
  </div>
210
841
  </div>
211
842
  </div>
212
843
  )
213
844
  }
214
- `
215
- )
216
-
217
- log.success('Boilerplate files created')
845
+ `)
846
+ log.success('Basic template created')
847
+ }
218
848
 
219
- // Done!
849
+ // Final message
220
850
  console.log('')
221
- console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
851
+ console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
222
852
  console.log(colors.green(' ✓ Project created successfully!'))
223
- console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
224
- console.log('')
225
-
226
- console.log(colors.blue('Next steps:'))
227
- console.log(` cd ${isCurrentDir ? '.' : projectName}`)
228
- console.log(' npm run dev')
229
- console.log('')
230
- console.log(colors.blue('Admin panel:'))
231
- console.log(' http://localhost:3000/admin')
853
+ console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
232
854
  console.log('')
233
855
 
234
- if (packagesInstalled) {
235
- console.log(colors.green('Premium packages installed! Full admin panel ready.'))
856
+ if (isPremium) {
857
+ console.log(colors.cyan('Next steps:'))
858
+ console.log(` 1. cd ${isCurrentDir ? '.' : projectName}`)
859
+ console.log(' 2. npx prisma db push # Create database tables')
860
+ console.log(' 3. npx prisma db seed # Create admin user')
861
+ console.log(' 4. npm run dev # Start development server')
862
+ console.log('')
863
+ console.log(colors.cyan('Default admin credentials:'))
864
+ console.log(' Email: admin@example.com')
865
+ console.log(' Password: admin123')
866
+ console.log('')
867
+ console.log(colors.cyan('Admin panel:'))
868
+ console.log(' http://localhost:3000/admin')
236
869
  } else {
237
- console.log(colors.yellow('To unlock premium features:'))
238
- console.log(' 1. Get your access token from admin')
239
- console.log(' 2. Run: npx create-sks-admin my-app')
240
- console.log(' 3. Enter token when prompted')
870
+ console.log(colors.blue('Next steps:'))
871
+ console.log(` cd ${isCurrentDir ? '.' : projectName}`)
872
+ console.log(' npm run dev')
873
+ console.log('')
874
+ console.log(colors.yellow('Get premium license for full features!'))
241
875
  }
242
876
  console.log('')
243
877
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sks-admin",
3
- "version": "0.2.0",
3
+ "version": "1.0.0",
4
4
  "description": "CLI to scaffold SKS Admin projects",
5
5
  "type": "module",
6
6
  "bin": {