create-sks-admin 0.1.1 → 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 +793 -108
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -4,15 +4,29 @@ 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'
8
+ import prompts from 'prompts'
7
9
 
8
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
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
+
10
22
  // Colors for terminal
11
23
  const colors = {
12
24
  green: (t) => `\x1b[32m${t}\x1b[0m`,
13
25
  blue: (t) => `\x1b[34m${t}\x1b[0m`,
14
26
  yellow: (t) => `\x1b[33m${t}\x1b[0m`,
15
27
  red: (t) => `\x1b[31m${t}\x1b[0m`,
28
+ cyan: (t) => `\x1b[36m${t}\x1b[0m`,
29
+ magenta: (t) => `\x1b[35m${t}\x1b[0m`,
16
30
  }
17
31
 
18
32
  const log = {
@@ -23,13 +37,24 @@ const log = {
23
37
  }
24
38
 
25
39
  // Get project name from args
26
- 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
+ }
27
52
 
28
53
  console.log(`
29
- ╔═══════════════════════════════════════╗
30
- @sks/create-admin CLI
31
- Creating: ${projectName.padEnd(22)}
32
- ╚═══════════════════════════════════════╝
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('╚═══════════════════════════════════════════════════╝')}
33
58
  `)
34
59
 
35
60
  const projectPath = path.resolve(process.cwd(), projectName)
@@ -41,84 +66,470 @@ if (!isCurrentDir && fs.existsSync(projectPath)) {
41
66
  process.exit(1)
42
67
  }
43
68
 
44
- // For current directory, check if it's empty (allow .git and other hidden files)
69
+ // For current directory, check if it's empty
45
70
  if (isCurrentDir) {
46
71
  const files = fs.readdirSync(projectPath).filter(f => !f.startsWith('.'))
47
72
  if (files.length > 0) {
48
- log.error('Current directory is not empty! Use an empty folder or provide a project name.')
73
+ log.error('Current directory is not empty!')
49
74
  process.exit(1)
50
75
  }
51
76
  }
52
77
 
78
+ // Ask for license key
79
+ console.log(colors.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
80
+ console.log(colors.yellow(' License Verification'))
81
+ console.log(colors.yellow('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
82
+ console.log('')
83
+
84
+ const keyResponse = await prompts({
85
+ type: 'password',
86
+ name: 'licenseKey',
87
+ message: 'Enter license key (or press Enter for basic version):',
88
+ })
89
+
90
+ const licenseKey = keyResponse.licenseKey?.trim() || ''
91
+ const isPremium = licenseKey.length > 0 && validateLicenseKey(licenseKey)
92
+
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('')
100
+ }
101
+
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
+ }
154
+
53
155
  try {
54
156
  // Step 1: Create Next.js app
55
- log.info('Creating Next.js app...')
157
+ log.info('Creating Next.js application...')
56
158
  execSync(
57
159
  `npx create-next-app@latest ${projectName} --typescript --tailwind --eslint --app --src-dir=false --import-alias="@/*" --turbopack`,
58
160
  { stdio: 'inherit' }
59
161
  )
60
162
  log.success('Next.js app created')
61
163
 
62
- // Step 2: Navigate to project
164
+ // Navigate to project
63
165
  process.chdir(projectPath)
64
166
 
65
- // Step 3: Create .npmrc for GitHub Packages
66
- log.info('Configuring npm registry...')
67
- fs.writeFileSync(
68
- '.npmrc',
69
- `@sks:registry=https://npm.pkg.github.com
70
- //npm.pkg.github.com/:_authToken=\${NODE_AUTH_TOKEN}
71
- `
72
- )
73
- log.success('.npmrc created')
74
-
75
- // Step 4: Install @sks packages (will fail if not published yet, that's OK)
76
- log.info('Installing @sks packages...')
77
- try {
78
- execSync('npm install @sks/admin-core @sks/admin-ui', { stdio: 'inherit' })
79
- log.success('@sks packages installed')
80
- } catch {
81
- log.warn('@sks packages not published yet - skipping')
82
- log.info('Run "npm install @sks/admin-core @sks/admin-ui" after publishing')
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' }
173
+ )
174
+ execSync(
175
+ 'npm install -D @types/bcryptjs',
176
+ { stdio: 'inherit' }
177
+ )
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}`
203
+ }
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
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
83
385
  }
386
+ }
84
387
 
85
- // Step 5: Create lib/dal.ts
86
- log.info('Creating boilerplate files...')
87
-
88
- fs.mkdirSync('lib', { recursive: true })
89
- fs.writeFileSync(
90
- 'lib/dal.ts',
91
- `// Data Access Layer for Authentication
92
- // Uncomment after installing @sks/admin-core
93
-
94
- // import { createDAL } from '@sks/admin-core/auth'
95
- // import { auth } from '@/auth'
96
-
97
- // export const {
98
- // verifySession,
99
- // verifyAdminSession,
100
- // redirectIfAuthenticated,
101
- // } = createDAL({
102
- // auth,
103
- // loginPath: '/admin/login',
104
- // adminRoles: ['ADMIN'],
105
- // })
106
-
107
- // Placeholder exports for now
108
- export const verifySession = async () => ({ userId: '1', email: 'test@test.com', role: 'ADMIN' })
109
- export const verifyAdminSession = verifySession
110
- `
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 }
111
396
  )
397
+ }
112
398
 
113
- // Step 6: Create admin layout
114
- fs.mkdirSync('app/admin', { recursive: true })
115
- fs.writeFileSync(
116
- 'app/admin/layout.tsx',
117
- `// Admin Layout with @sks/admin-ui
118
- // 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
+ }
414
+
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...')
119
527
 
120
- // import { AdminShell, ThemeProvider } from '@sks/admin-ui'
121
- // import '@sks/admin-ui/themes/base.css'
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'
122
533
 
123
534
  export default function AdminLayout({
124
535
  children,
@@ -126,70 +537,344 @@ export default function AdminLayout({
126
537
  children: React.ReactNode
127
538
  }) {
128
539
  return (
129
- <div className="min-h-screen bg-gray-100">
130
- {/* Replace with AdminShell after installing @sks/admin-ui */}
131
- <aside className="fixed left-0 top-0 h-screen w-64 bg-gray-900 text-white p-4">
132
- <h1 className="text-xl font-bold mb-8">Admin Panel</h1>
133
- <nav className="space-y-2">
134
- <a href="/admin" className="block p-2 rounded hover:bg-gray-800">Dashboard</a>
135
- <a href="/admin/users" className="block p-2 rounded hover:bg-gray-800">Users</a>
136
- <a href="/admin/settings" className="block p-2 rounded hover:bg-gray-800">Settings</a>
137
- </nav>
138
- </aside>
139
- <main className="ml-64 p-8">
140
- {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>
141
549
  </main>
142
- </div>
550
+ <Toaster />
551
+ </SidebarProvider>
143
552
  )
144
553
  }
145
- `
146
- )
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()
147
562
 
148
- // Step 7: Create admin dashboard page
149
- fs.writeFileSync(
150
- 'app/admin/page.tsx',
151
- `export default function AdminDashboard() {
152
563
  return (
153
- <div>
154
- <h1 className="text-3xl font-bold mb-4">Dashboard</h1>
155
- <div className="grid grid-cols-3 gap-4">
156
- <div className="bg-white p-6 rounded-lg shadow">
157
- <h2 className="text-gray-500">Total Users</h2>
158
- <p className="text-3xl font-bold">1,234</p>
159
- </div>
160
- <div className="bg-white p-6 rounded-lg shadow">
161
- <h2 className="text-gray-500">Revenue</h2>
162
- <p className="text-3xl font-bold">$12,345</p>
163
- </div>
164
- <div className="bg-white p-6 rounded-lg shadow">
165
- <h2 className="text-gray-500">Orders</h2>
166
- <p className="text-3xl font-bold">567</p>
167
- </div>
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>
168
607
  </div>
169
608
  </div>
170
609
  )
171
610
  }
172
- `
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>
173
694
  )
695
+ }
696
+ `)
174
697
 
175
- log.success('Boilerplate files created')
698
+ // Sidebar component
699
+ fs.mkdirSync('components', { recursive: true })
700
+ fs.writeFileSync('components/app-sidebar.tsx', `'use client'
176
701
 
177
- // Done!
178
- console.log(`
179
- ${colors.green('✓ Project created successfully!')}
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'
180
717
 
181
- ${colors.blue('Next steps:')}
182
- cd ${projectName}
183
- npm run dev
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
+ ]
184
723
 
185
- ${colors.blue('Admin panel:')}
186
- http://localhost:3000/admin
724
+ export function AppSidebar() {
725
+ return (
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' })
187
766
 
188
- ${colors.yellow('After publishing @sks packages:')}
189
- npm install @sks/admin-core @sks/admin-ui
190
- Then uncomment imports in lib/dal.ts and app/admin/layout.tsx
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
+ })
191
799
  `)
192
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>
833
+ </div>
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>
840
+ </div>
841
+ </div>
842
+ </div>
843
+ )
844
+ }
845
+ `)
846
+ log.success('Basic template created')
847
+ }
848
+
849
+ // Final message
850
+ console.log('')
851
+ console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
852
+ console.log(colors.green(' ✓ Project created successfully!'))
853
+ console.log(colors.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
854
+ console.log('')
855
+
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')
869
+ } else {
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!'))
875
+ }
876
+ console.log('')
877
+
193
878
  } catch (error) {
194
879
  log.error('Failed to create project')
195
880
  console.error(error)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sks-admin",
3
- "version": "0.1.1",
3
+ "version": "1.0.0",
4
4
  "description": "CLI to scaffold SKS Admin projects",
5
5
  "type": "module",
6
6
  "bin": {