ebm-skills 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.
@@ -0,0 +1,365 @@
1
+ # ebm-form Reference
2
+
3
+ ## Input required from user
4
+
5
+ ```
6
+ 1. Form type — create/edit | search/filter
7
+ 2. Feature name — e.g. "users", "products"
8
+ 3. Prisma model — e.g. "User" (for create/edit) or skip for search forms
9
+ 4. Fields — auto-detect from prisma/schema.prisma if exists
10
+ else: "name:string, email:email, role:select, active:boolean"
11
+ 5. For select fields: options list — e.g. "admin,user,viewer"
12
+ 6. For relation fields: related model + label/value fields
13
+ ```
14
+
15
+ ## Field type mapping
16
+
17
+ | Type | Component | Zod type |
18
+ |------|-----------|----------|
19
+ | `string` | `<input type="text">` | `z.string()` |
20
+ | `email` | `<input type="email">` | `z.string().email()` |
21
+ | `password` | `<input type="password">` | `z.string().min(8)` |
22
+ | `number` | `<input type="number">` | `z.coerce.number()` |
23
+ | `boolean` | `<input type="checkbox">` | `z.boolean()` |
24
+ | `date` | `<input type="date">` | `z.coerce.date()` |
25
+ | `select` | `<select>` | `z.enum([...options])` |
26
+ | `textarea` | `<textarea>` | `z.string()` |
27
+ | `file` | `<input type="file">` | `z.instanceof(File).optional()` |
28
+ | `richtext` | Tiptap editor | `z.string()` |
29
+ | `relation` | async `<select>` fetch from API | `z.number()` or `z.string()` |
30
+
31
+ ---
32
+
33
+ ## Files to generate
34
+
35
+ ### Create/Edit form
36
+
37
+ | File | Purpose |
38
+ |------|---------|
39
+ | `src/components/forms/[Feature]Form.tsx` | Form component (create + edit mode) |
40
+ | `src/lib/schemas/[feature].schema.ts` | Zod schema (shared with API) |
41
+ | `src/app/api/[feature]/route.ts` | POST (create) endpoint |
42
+ | `src/app/api/[feature]/[id]/route.ts` | PUT (update) + DELETE endpoint |
43
+
44
+ ### Search/Filter form
45
+
46
+ | File | Purpose |
47
+ |------|---------|
48
+ | `src/components/forms/[Feature]FilterForm.tsx` | Filter form component |
49
+ | `src/lib/schemas/[feature].schema.ts` | Zod schema for filter params |
50
+
51
+ ---
52
+
53
+ ## Template: `src/lib/schemas/[feature].schema.ts`
54
+
55
+ ```typescript
56
+ import { z } from 'zod'
57
+
58
+ export const create[Feature]Schema = z.object({
59
+ name: z.string().min(1, 'Name is required'),
60
+ email: z.string().email('Invalid email'),
61
+ // role: z.enum(['admin', 'user', 'viewer']),
62
+ // active: z.boolean().default(true),
63
+ // birthDate: z.coerce.date().optional(),
64
+ })
65
+
66
+ export const update[Feature]Schema = create[Feature]Schema.partial()
67
+
68
+ export type Create[Feature]Input = z.infer<typeof create[Feature]Schema>
69
+ export type Update[Feature]Input = z.infer<typeof update[Feature]Schema>
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Template: `src/components/forms/[Feature]Form.tsx`
75
+
76
+ ```typescript
77
+ 'use client'
78
+
79
+ import { useForm } from 'react-hook-form'
80
+ import { zodResolver } from '@hookform/resolvers/zod'
81
+ import { useRouter } from 'next/navigation'
82
+ import { create[Feature]Schema, type Create[Feature]Input } from '@/lib/schemas/[feature].schema'
83
+
84
+ interface [Feature]FormProps {
85
+ defaultValues?: Partial<Create[Feature]Input>
86
+ id?: number | string // present = edit mode
87
+ }
88
+
89
+ export function [Feature]Form({ defaultValues, id }: [Feature]FormProps) {
90
+ const router = useRouter()
91
+ const isEdit = !!id
92
+
93
+ const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<Create[Feature]Input>({
94
+ resolver: zodResolver(create[Feature]Schema),
95
+ defaultValues,
96
+ })
97
+
98
+ async function onSubmit(data: Create[Feature]Input) {
99
+ const url = isEdit ? `/api/[feature]/${id}` : `/api/[feature]`
100
+ const method = isEdit ? 'PUT' : 'POST'
101
+
102
+ const res = await fetch(url, {
103
+ method,
104
+ headers: { 'Content-Type': 'application/json' },
105
+ body: JSON.stringify(data),
106
+ })
107
+
108
+ if (!res.ok) {
109
+ const err = await res.json()
110
+ alert(err.message ?? 'Something went wrong')
111
+ return
112
+ }
113
+
114
+ router.push('/dashboard/[feature]')
115
+ router.refresh()
116
+ }
117
+
118
+ return (
119
+ <form onSubmit={handleSubmit(onSubmit)} className="space-y-5 max-w-lg">
120
+ {/* string field example */}
121
+ <div>
122
+ <label className="block text-sm font-medium text-slate-300 mb-1.5">Name</label>
123
+ <input
124
+ {...register('name')}
125
+ className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2.5 text-white placeholder-slate-500 text-sm focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-colors"
126
+ placeholder="Enter name"
127
+ />
128
+ {errors.name && <p className="mt-1 text-xs text-red-400">{errors.name.message}</p>}
129
+ </div>
130
+
131
+ {/* email field example */}
132
+ <div>
133
+ <label className="block text-sm font-medium text-slate-300 mb-1.5">Email</label>
134
+ <input
135
+ type="email"
136
+ {...register('email')}
137
+ className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2.5 text-white placeholder-slate-500 text-sm focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-colors"
138
+ placeholder="Enter email"
139
+ />
140
+ {errors.email && <p className="mt-1 text-xs text-red-400">{errors.email.message}</p>}
141
+ </div>
142
+
143
+ {/* select field example
144
+ <div>
145
+ <label className="block text-sm font-medium text-slate-300 mb-1.5">Role</label>
146
+ <select {...register('role')}
147
+ className="w-full bg-slate-900 border border-slate-600 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-blue-500">
148
+ <option value="admin">Admin</option>
149
+ <option value="user">User</option>
150
+ </select>
151
+ {errors.role && <p className="mt-1 text-xs text-red-400">{errors.role.message}</p>}
152
+ </div>
153
+ */}
154
+
155
+ {/* boolean field example
156
+ <div className="flex items-center gap-3">
157
+ <input type="checkbox" id="active" {...register('active')}
158
+ className="w-4 h-4 rounded border-slate-600 bg-slate-900 text-blue-600 focus:ring-blue-500" />
159
+ <label htmlFor="active" className="text-sm font-medium text-slate-300">Active</label>
160
+ </div>
161
+ */}
162
+
163
+ <div className="flex gap-3 pt-2">
164
+ <button
165
+ type="submit"
166
+ disabled={isSubmitting}
167
+ className="px-5 py-2.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-semibold rounded-lg transition-colors"
168
+ >
169
+ {isSubmitting ? 'Saving...' : isEdit ? 'Save Changes' : 'Create'}
170
+ </button>
171
+ <button
172
+ type="button"
173
+ onClick={() => router.back()}
174
+ className="px-5 py-2.5 bg-slate-700 hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors"
175
+ >
176
+ Cancel
177
+ </button>
178
+ </div>
179
+ </form>
180
+ )
181
+ }
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Template: `src/app/api/[feature]/route.ts` (POST)
187
+
188
+ ```typescript
189
+ import { NextRequest, NextResponse } from 'next/server'
190
+ import { getServerSession } from 'next-auth'
191
+ import { authOptions } from '@/app/api/auth/[...nextauth]/auth-options'
192
+ import { prisma } from '@/lib/prisma'
193
+ import { create[Feature]Schema } from '@/lib/schemas/[feature].schema'
194
+
195
+ export async function POST(req: NextRequest) {
196
+ const session = await getServerSession(authOptions)
197
+ if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
198
+
199
+ const body = await req.json()
200
+ const parsed = create[Feature]Schema.safeParse(body)
201
+ if (!parsed.success) {
202
+ return NextResponse.json({ error: 'Validation failed', issues: parsed.error.issues }, { status: 422 })
203
+ }
204
+
205
+ const record = await prisma.[model].create({ data: parsed.data })
206
+ return NextResponse.json(record, { status: 201 })
207
+ }
208
+ ```
209
+
210
+ ## Template: `src/app/api/[feature]/[id]/route.ts` (PUT + DELETE)
211
+
212
+ ```typescript
213
+ import { NextRequest, NextResponse } from 'next/server'
214
+ import { getServerSession } from 'next-auth'
215
+ import { authOptions } from '@/app/api/auth/[...nextauth]/auth-options'
216
+ import { prisma } from '@/lib/prisma'
217
+ import { update[Feature]Schema } from '@/lib/schemas/[feature].schema'
218
+
219
+ export async function PUT(req: NextRequest, { params }: { params: { id: string } }) {
220
+ const session = await getServerSession(authOptions)
221
+ if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
222
+
223
+ const body = await req.json()
224
+ const parsed = update[Feature]Schema.safeParse(body)
225
+ if (!parsed.success) {
226
+ return NextResponse.json({ error: 'Validation failed', issues: parsed.error.issues }, { status: 422 })
227
+ }
228
+
229
+ const record = await prisma.[model].update({
230
+ where: { id: Number(params.id) },
231
+ data: parsed.data,
232
+ })
233
+ return NextResponse.json(record)
234
+ }
235
+
236
+ export async function DELETE(_req: NextRequest, { params }: { params: { id: string } }) {
237
+ const session = await getServerSession(authOptions)
238
+ if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
239
+
240
+ await prisma.[model].delete({ where: { id: Number(params.id) } })
241
+ return NextResponse.json({ success: true })
242
+ }
243
+ ```
244
+
245
+ ---
246
+
247
+ ## Template: Search/Filter form
248
+
249
+ ```typescript
250
+ // src/components/forms/[Feature]FilterForm.tsx
251
+ 'use client'
252
+
253
+ import { useForm } from 'react-hook-form'
254
+ import { zodResolver } from '@hookform/resolvers/zod'
255
+ import { useRouter, useSearchParams, usePathname } from 'next/navigation'
256
+ import { z } from 'zod'
257
+
258
+ const filterSchema = z.object({
259
+ search: z.string().optional(),
260
+ // status: z.enum(['active', 'inactive', '']).optional(),
261
+ // dateFrom: z.string().optional(),
262
+ // dateTo: z.string().optional(),
263
+ })
264
+
265
+ type FilterInput = z.infer<typeof filterSchema>
266
+
267
+ export function [Feature]FilterForm() {
268
+ const router = useRouter()
269
+ const pathname = usePathname()
270
+ const searchParams = useSearchParams()
271
+
272
+ const { register, handleSubmit, reset } = useForm<FilterInput>({
273
+ resolver: zodResolver(filterSchema),
274
+ defaultValues: {
275
+ search: searchParams.get('search') ?? '',
276
+ },
277
+ })
278
+
279
+ function onSubmit(data: FilterInput) {
280
+ const params = new URLSearchParams()
281
+ Object.entries(data).forEach(([k, v]) => { if (v) params.set(k, v) })
282
+ params.set('page', '1')
283
+ router.push(`${pathname}?${params.toString()}`)
284
+ }
285
+
286
+ function onReset() {
287
+ reset()
288
+ router.push(pathname)
289
+ }
290
+
291
+ return (
292
+ <form onSubmit={handleSubmit(onSubmit)} className="flex flex-wrap gap-3 items-end">
293
+ <div>
294
+ <label className="block text-xs text-slate-400 mb-1">Search</label>
295
+ <input
296
+ {...register('search')}
297
+ placeholder="Search..."
298
+ className="bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500 w-56"
299
+ />
300
+ </div>
301
+ {/* Add more filter fields here */}
302
+ <button type="submit"
303
+ className="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium rounded-lg transition-colors">
304
+ Filter
305
+ </button>
306
+ <button type="button" onClick={onReset}
307
+ className="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors">
308
+ Reset
309
+ </button>
310
+ </form>
311
+ )
312
+ }
313
+ ```
314
+
315
+ ---
316
+
317
+ ## UI Library variants
318
+
319
+ ### shadcn/ui
320
+ Replace inputs with `<Input>`, `<Select>`, `<Checkbox>` from shadcn.
321
+ Wrap fields in `<FormField>`, `<FormControl>`, `<FormMessage>` from `@/components/ui/form`.
322
+
323
+ ### Ant Design
324
+ ```typescript
325
+ import { Form, Input, Select, Button } from 'antd'
326
+ // Use Form.useForm() instead of RHF
327
+ // Validation via Form rules prop
328
+ // Note: Ant Design has its own form system — skip RHF when using antd
329
+ ```
330
+
331
+ ### MUI
332
+ ```typescript
333
+ import { TextField, Select, Button } from '@mui/material'
334
+ // Keep RHF, use Controller wrapper for MUI components:
335
+ // <Controller name="field" control={control} render={({ field }) => <TextField {...field} />} />
336
+ ```
337
+
338
+ ---
339
+
340
+ ## Required dependencies
341
+
342
+ ```bash
343
+ npm install react-hook-form @hookform/resolvers zod
344
+ ```
345
+
346
+ ---
347
+
348
+ ## Post-generation summary
349
+
350
+ ```
351
+ ✓ Generated [N] files for [feature] form
352
+
353
+ Files:
354
+ src/components/forms/[Feature]Form.tsx
355
+ src/lib/schemas/[feature].schema.ts
356
+ src/app/api/[feature]/route.ts (POST)
357
+ src/app/api/[feature]/[id]/route.ts (PUT, DELETE)
358
+
359
+ TODO in generated files:
360
+ 1. Uncomment/add field blocks in [Feature]Form.tsx
361
+ 2. Add select options for enum fields
362
+ 3. Update Prisma model name in API routes (marked with [model])
363
+
364
+ Install: npm install react-hook-form @hookform/resolvers zod
365
+ ```
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: ebm-form
3
+ description: Generate a create/edit form or search/filter form using React Hook Form and Zod. Auto-detects fields from prisma/schema.prisma when available. Also generates API routes (POST, PUT, DELETE) for create/edit forms. Reads UI library from ebm.config.json. Use when user invokes /ebm-form or asks to build a form, create page, edit page, or search/filter UI.
4
+ ---
5
+
6
+ # /ebm-form
7
+
8
+ ### Step 1 — Ask form type
9
+ ```
10
+ 1. Form type: create/edit | search/filter
11
+ 2. Feature name: e.g. "products"
12
+ 3. Prisma model: e.g. "Product" (create/edit only)
13
+ 4. Fields: auto-detect from prisma/schema.prisma if exists
14
+ else ask: "name:string, price:number, status:select, active:boolean"
15
+ 5. For select fields: options list — e.g. "active,inactive,draft"
16
+ 6. For relation fields: related model + label/value fields
17
+ ```
18
+
19
+ ### Step 2 — Read config
20
+ ```
21
+ ebm.config.json → uiLib (shadcn | antd | mui | none)
22
+ ```
23
+
24
+ ### Step 3 — Generate files
25
+
26
+ **create/edit → 4 files:**
27
+ ```
28
+ src/components/forms/[Feature]Form.tsx
29
+ src/lib/schemas/[feature].schema.ts
30
+ src/app/api/[feature]/route.ts (POST)
31
+ src/app/api/[feature]/[id]/route.ts (PUT, DELETE)
32
+ ```
33
+
34
+ **search/filter → 2 files:**
35
+ ```
36
+ src/components/forms/[Feature]FilterForm.tsx
37
+ src/lib/schemas/[feature].schema.ts
38
+ ```
39
+
40
+ See [REFERENCE.md](REFERENCE.md) for field type mapping, templates, and UI lib variants.
41
+
42
+ ## Shared rules
43
+ - Always use React Hook Form + zodResolver
44
+ - Path alias: `@/*` → `./src/*` always
45
+ - Thai UI text: use formal Thai — see `/ebm-thai` glossary
@@ -0,0 +1,264 @@
1
+ # ebm-init Reference
2
+
3
+ ## Post-CLI scaffolding by project type
4
+
5
+ After `create-next-app` runs, scaffold additional files based on project type.
6
+
7
+ ---
8
+
9
+ ## Type: landing (หน้าบ้านแสดงข้อมูล)
10
+
11
+ | File | Purpose |
12
+ |------|---------|
13
+ | `src/app/layout.tsx` | Root layout, imports globals.css |
14
+ | `src/app/page.tsx` | Hero/landing page |
15
+ | `src/app/(pages)/layout.tsx` | Content route group layout |
16
+ | `src/components/ui/Navbar.tsx` | Sticky nav: logo + links |
17
+ | `src/components/ui/Footer.tsx` | Footer with copyright |
18
+
19
+ ```tsx
20
+ // src/components/ui/Navbar.tsx
21
+ 'use client'
22
+ import Link from 'next/link'
23
+ export function Navbar() {
24
+ return (
25
+ <nav className="sticky top-0 z-10 border-b border-slate-700/50 bg-slate-900/60 backdrop-blur-md">
26
+ <div className="mx-auto max-w-6xl px-6 flex items-center justify-between h-16">
27
+ <Link href="/" className="text-xl font-bold text-white">Logo</Link>
28
+ <div className="flex gap-6 text-sm text-slate-400">
29
+ <Link href="/" className="hover:text-white transition-colors">Home</Link>
30
+ <Link href="/about" className="hover:text-white transition-colors">About</Link>
31
+ </div>
32
+ </div>
33
+ </nav>
34
+ )
35
+ }
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Type: backoffice (Dashboard)
41
+
42
+ | File | Purpose |
43
+ |------|---------|
44
+ | `src/app/(dashboard)/layout.tsx` | Sidebar + Header + children |
45
+ | `src/app/(dashboard)/dashboard/page.tsx` | Overview with stat cards |
46
+ | `src/components/layout/Sidebar.tsx` | Left nav with active state |
47
+ | `src/components/layout/Header.tsx` | Top bar: page title + user avatar |
48
+
49
+ ```tsx
50
+ // src/app/(dashboard)/layout.tsx
51
+ import { Sidebar } from '@/components/layout/Sidebar'
52
+ import { Header } from '@/components/layout/Header'
53
+
54
+ export default function DashboardLayout({ children }: { children: React.ReactNode }) {
55
+ return (
56
+ <div className="flex h-screen bg-slate-900 text-white overflow-hidden">
57
+ <Sidebar />
58
+ <div className="flex flex-col flex-1 overflow-hidden">
59
+ <Header />
60
+ <main className="flex-1 overflow-y-auto p-6">{children}</main>
61
+ </div>
62
+ </div>
63
+ )
64
+ }
65
+ ```
66
+
67
+ ```tsx
68
+ // src/components/layout/Sidebar.tsx
69
+ 'use client'
70
+ import Link from 'next/link'
71
+ import { usePathname } from 'next/navigation'
72
+
73
+ const NAV = [
74
+ { href: '/dashboard', label: 'Dashboard' },
75
+ { href: '/dashboard/users', label: 'Users' },
76
+ { href: '/dashboard/settings', label: 'Settings' },
77
+ ]
78
+
79
+ export function Sidebar() {
80
+ const pathname = usePathname()
81
+ return (
82
+ <aside className="w-64 flex-shrink-0 bg-slate-800 border-r border-slate-700 flex flex-col">
83
+ <div className="h-16 flex items-center px-6 border-b border-slate-700">
84
+ <span className="text-xl font-bold text-white">Admin</span>
85
+ </div>
86
+ <nav className="flex-1 px-3 py-4 space-y-1">
87
+ {NAV.map((item) => (
88
+ <Link key={item.href} href={item.href}
89
+ className={`flex items-center px-3 py-2 rounded-lg text-sm transition-colors ${
90
+ pathname === item.href
91
+ ? 'bg-blue-600 text-white'
92
+ : 'text-slate-400 hover:text-white hover:bg-slate-700'
93
+ }`}>
94
+ {item.label}
95
+ </Link>
96
+ ))}
97
+ </nav>
98
+ </aside>
99
+ )
100
+ }
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Type: both (landing + backoffice)
106
+
107
+ Root `src/app/layout.tsx` = minimal shell only (no nav/sidebar).
108
+
109
+ Route groups:
110
+ - `src/app/(public)/` — landing files (Type A layout pattern)
111
+ - `src/app/(dashboard)/` — admin files (Type B layout pattern)
112
+
113
+ ```tsx
114
+ // src/app/layout.tsx (Type C root — minimal)
115
+ import './globals.css'
116
+ export const metadata = { title: 'App', description: '' }
117
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
118
+ return (
119
+ <html lang="en">
120
+ <body className="bg-slate-900 text-white antialiased">{children}</body>
121
+ </html>
122
+ )
123
+ }
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Tailwind v4 setup (if tailwind: true)
129
+
130
+ ```css
131
+ /* src/app/globals.css */
132
+ @import "tailwindcss";
133
+
134
+ /* Primary color override — use ebm.config.json primaryColor */
135
+ @theme {
136
+ --color-primary: #3b82f6; /* replace with user's chosen color */
137
+ }
138
+ ```
139
+
140
+ ```js
141
+ // postcss.config.mjs
142
+ const config = { plugins: { '@tailwindcss/postcss': {} } }
143
+ export default config
144
+ ```
145
+
146
+ Install: `tailwindcss @tailwindcss/postcss`
147
+
148
+ **Dark mode:** add `class="dark"` to `<html>` and use `dark:` variants
149
+ **Light mode:** default (no extra config needed)
150
+ **System mode:** toggle via JS `document.documentElement.classList`
151
+
152
+ ---
153
+
154
+ ## UI Library setup
155
+
156
+ ### shadcn/ui
157
+ ```bash
158
+ npx shadcn@latest init
159
+ # Answer prompts: style=default, baseColor=slate, cssVariables=yes
160
+ ```
161
+ Then install components as needed: `npx shadcn@latest add button input card`
162
+
163
+ ### Ant Design
164
+ ```bash
165
+ npm install antd @ant-design/icons
166
+ ```
167
+ ```tsx
168
+ // src/app/layout.tsx — wrap with AntdRegistry for SSR
169
+ import { AntdRegistry } from '@ant-design/nextjs-registry'
170
+ // ...
171
+ <AntdRegistry>{children}</AntdRegistry>
172
+ ```
173
+ Install: `npm install @ant-design/nextjs-registry`
174
+
175
+ ### MUI (Material UI)
176
+ ```bash
177
+ npm install @mui/material @emotion/react @emotion/styled @mui/icons-material
178
+ ```
179
+ ```tsx
180
+ // src/app/layout.tsx — wrap with AppRouterCacheProvider
181
+ import { AppRouterCacheProvider } from '@mui/material-nextjs/v14-appRouter'
182
+ // ...
183
+ <AppRouterCacheProvider>{children}</AppRouterCacheProvider>
184
+ ```
185
+ Install: `npm install @mui/material-nextjs`
186
+
187
+ ---
188
+
189
+ ## Database setup
190
+
191
+ ### Prisma
192
+ ```bash
193
+ npm install @prisma/client
194
+ npm install -D prisma
195
+ npx prisma init --datasource-provider [postgresql|mysql|sqlite]
196
+ ```
197
+
198
+ ### Drizzle
199
+ ```bash
200
+ npm install drizzle-orm
201
+ npm install -D drizzle-kit
202
+ # PostgreSQL: npm install pg @types/pg
203
+ # MySQL: npm install mysql2
204
+ # SQLite: npm install better-sqlite3 @types/better-sqlite3
205
+ ```
206
+ Create `drizzle.config.ts`:
207
+ ```ts
208
+ import { defineConfig } from 'drizzle-kit'
209
+ export default defineConfig({
210
+ schema: './src/db/schema.ts',
211
+ out: './drizzle',
212
+ dialect: 'postgresql', // or mysql, sqlite
213
+ dbCredentials: { url: process.env.DATABASE_URL! },
214
+ })
215
+ ```
216
+
217
+ ---
218
+
219
+ ## tsconfig.json (generate if missing or missing paths)
220
+ ```json
221
+ {
222
+ "compilerOptions": {
223
+ "lib": ["dom", "dom.iterable", "esnext"],
224
+ "allowJs": true,
225
+ "skipLibCheck": true,
226
+ "strict": false,
227
+ "noEmit": true,
228
+ "incremental": true,
229
+ "module": "esnext",
230
+ "esModuleInterop": true,
231
+ "resolveJsonModule": true,
232
+ "isolatedModules": true,
233
+ "jsx": "preserve",
234
+ "plugins": [{ "name": "next" }],
235
+ "paths": { "@/*": ["./src/*"] }
236
+ },
237
+ "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
238
+ "exclude": ["node_modules"]
239
+ }
240
+ ```
241
+
242
+ ---
243
+
244
+ ## Post-generation summary template
245
+ ```
246
+ ✓ Project initialized with create-next-app
247
+ ✓ Generated [N] scaffold files
248
+ ✓ Saved config to ebm.config.json
249
+
250
+ Stack:
251
+ Type: [projectType]
252
+ Tailwind: [yes/no] — [colorMode], primary: [color]
253
+ UI lib: [uiLib]
254
+ DB: [orm] + [dbProvider]
255
+
256
+ Next steps:
257
+ 1. npm install (or yarn/pnpm install)
258
+ 2. Copy .env.example → .env.local
259
+ 3. [if DB] npx prisma migrate dev --name init
260
+ 4. npm run dev
261
+
262
+ [if backoffice or both]
263
+ → Run /ebm-auth to add authentication
264
+ ```