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.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # ebm-skills
2
+
3
+ Claude Code skills for EBM Next.js projects.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npx ebm-skills
9
+ ```
10
+
11
+ Install specific skills only:
12
+
13
+ ```bash
14
+ npx ebm-skills --only init,auth,table
15
+ ```
16
+
17
+ Restart Claude Code after installing.
18
+
19
+ ## Skills
20
+
21
+ | Command | What it does |
22
+ |---------|-------------|
23
+ | `/ebm-init` | New project wizard (create-next-app + Tailwind + UI lib + DB) |
24
+ | `/ebm-auth` | Full auth system — JWT, RBAC, signin/forgot-password/user management pages |
25
+ | `/ebm-table` | Server-side data table with pagination, search, and sort |
26
+ | `/ebm-form` | Create/edit or search/filter form with React Hook Form + Zod |
27
+ | `/ebm-upload` | Drag & drop file upload (local disk or S3-compatible) |
28
+ | `/ebm-thai` | Scan & fix informal Thai UI text using formal Thai glossary |
29
+
30
+ ## Requirements
31
+
32
+ - [Claude Code](https://claude.ai/download) installed
33
+ - Node.js 18+
34
+
35
+ ## Typical workflow
36
+
37
+ ```
38
+ /ebm-init → scaffold new project
39
+ /ebm-auth → add auth system
40
+ /ebm-table → add data tables per feature
41
+ /ebm-form → add create/edit forms per feature
42
+ /ebm-upload → add file upload if needed
43
+ /ebm-thai → fix Thai language formality before review
44
+ ```
package/bin/cli.js ADDED
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ import { cp, mkdir, readFile, writeFile, access } from 'fs/promises'
3
+ import { join, dirname } from 'path'
4
+ import { homedir } from 'os'
5
+ import { fileURLToPath } from 'url'
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url))
8
+ const ALL_SKILLS = ['ebm-init', 'ebm-auth', 'ebm-table', 'ebm-form', 'ebm-upload', 'ebm-thai']
9
+ const packageSkillsDir = join(__dirname, '..', 'skills')
10
+
11
+ const onlyIndex = process.argv.indexOf('--only')
12
+ const selectedNames = onlyIndex !== -1
13
+ ? process.argv[onlyIndex + 1].split(',').map(s => s.trim().replace(/^ebm-/, ''))
14
+ : null
15
+ const skills = selectedNames
16
+ ? selectedNames.map(s => `ebm-${s}`).filter(s => ALL_SKILLS.includes(s))
17
+ : ALL_SKILLS
18
+
19
+ async function pathExists(p) {
20
+ try { await access(p); return true } catch { return false }
21
+ }
22
+
23
+ async function convertToCursorMdc(skillDir) {
24
+ const skillMd = await readFile(join(skillDir, 'SKILL.md'), 'utf8')
25
+ const m = skillMd.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/m)
26
+ if (!m) return skillMd
27
+ const descMatch = m[1].match(/description:\s*(.+)/)
28
+ const description = descMatch ? descMatch[1].trim() : ''
29
+ return `---\ndescription: ${description}\nalwaysApply: false\n---\n${m[2]}`
30
+ }
31
+
32
+ // Read all stdin lines upfront (supports both interactive TTY and piped input)
33
+ async function readStdinLines() {
34
+ if (process.stdin.isTTY) return null // interactive — use per-question readline
35
+ return new Promise(resolve => {
36
+ const chunks = []
37
+ process.stdin.on('data', d => chunks.push(d))
38
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString().split(/\r?\n/).map(l => l.trim())))
39
+ })
40
+ }
41
+
42
+ async function makeAsker(lines) {
43
+ if (lines) {
44
+ // piped mode — pop lines from queue
45
+ let i = 0
46
+ return (q) => {
47
+ const ans = lines[i++] ?? ''
48
+ process.stdout.write(q + ans + '\n')
49
+ return Promise.resolve(ans)
50
+ }
51
+ }
52
+ // interactive mode — readline
53
+ const { createInterface } = await import('readline')
54
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
55
+ const ask = (q) => new Promise(resolve => rl.question(q, ans => resolve(ans.trim())))
56
+ ask.close = () => rl.close()
57
+ return ask
58
+ }
59
+
60
+ async function main() {
61
+ const lines = await readStdinLines()
62
+ const ask = await makeAsker(lines)
63
+
64
+ console.log('\nebm-skills installer\n')
65
+ console.log('Which platforms? (enter numbers separated by comma)\n')
66
+ console.log(' 1) Claude Code (~/.claude/skills/)')
67
+ console.log(' 2) Antigravity (~/.gemini/antigravity/skills/)')
68
+ console.log(' 3) Cursor (.cursor/rules/ or ~/.cursor/rules/)')
69
+ console.log()
70
+
71
+ const platformInput = await ask('Platforms [1,2,3]: ')
72
+ const selected = platformInput.split(',').map(s => s.trim()).filter(s => ['1','2','3'].includes(s))
73
+
74
+ if (selected.length === 0) {
75
+ ask.close?.()
76
+ console.error('No platform selected. Exiting.')
77
+ process.exit(1)
78
+ }
79
+
80
+ const targets = []
81
+
82
+ if (selected.includes('1')) {
83
+ const claudeDir = join(homedir(), '.claude')
84
+ if (!(await pathExists(claudeDir))) {
85
+ console.warn('⚠ ~/.claude not found — skipping Claude Code')
86
+ } else {
87
+ targets.push({ name: 'Claude Code', dest: join(claudeDir, 'skills'), type: 'claude' })
88
+ }
89
+ }
90
+
91
+ if (selected.includes('2')) {
92
+ targets.push({
93
+ name: 'Antigravity',
94
+ dest: join(homedir(), '.gemini', 'antigravity', 'skills'),
95
+ type: 'antigravity'
96
+ })
97
+ }
98
+
99
+ if (selected.includes('3')) {
100
+ const scope = await ask('Cursor scope — (g)lobal ~/.cursor/rules/ or (w)orkspace .cursor/rules/? [g/w]: ')
101
+ const isWorkspace = scope.toLowerCase().startsWith('w')
102
+ const dest = isWorkspace
103
+ ? join(process.cwd(), '.cursor', 'rules')
104
+ : join(homedir(), '.cursor', 'rules')
105
+ targets.push({ name: `Cursor (${isWorkspace ? 'workspace' : 'global'})`, dest, type: 'cursor' })
106
+ }
107
+
108
+ ask.close?.()
109
+
110
+ if (targets.length === 0) {
111
+ console.error('No valid targets. Exiting.')
112
+ process.exit(1)
113
+ }
114
+
115
+ console.log()
116
+
117
+ for (const target of targets) {
118
+ console.log(`Installing to ${target.name}...`)
119
+ const installed = []
120
+ const failed = []
121
+
122
+ for (const skill of skills) {
123
+ const src = join(packageSkillsDir, skill)
124
+ try {
125
+ if (target.type === 'cursor') {
126
+ await mkdir(target.dest, { recursive: true })
127
+ const mdc = await convertToCursorMdc(src)
128
+ await writeFile(join(target.dest, `${skill}.mdc`), mdc, 'utf8')
129
+ } else {
130
+ const dest = join(target.dest, skill)
131
+ await mkdir(dest, { recursive: true })
132
+ await cp(src, dest, { recursive: true })
133
+ }
134
+ console.log(` ✓ /${skill}`)
135
+ installed.push(skill)
136
+ } catch (err) {
137
+ console.error(` ✗ ${skill}: ${err.message}`)
138
+ failed.push(skill)
139
+ }
140
+ }
141
+
142
+ console.log(` → ${installed.length} skill(s) installed to ${target.dest}\n`)
143
+ if (failed.length > 0) console.error(` Failed: ${failed.join(', ')}\n`)
144
+ }
145
+
146
+ console.log('Done! Restart your editor to activate the skills.')
147
+ console.log(`\nAvailable commands: ${skills.map(s => `/${s}`).join(' ')}`)
148
+ }
149
+
150
+ main().catch(err => { console.error(err); process.exit(1) })
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "ebm-skills",
3
+ "version": "1.0.0",
4
+ "description": "Claude Code skills for EBM Next.js projects — scaffold auth, tables, forms, uploads, and more",
5
+ "type": "module",
6
+ "bin": {
7
+ "ebm-skills": "bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "skills"
12
+ ],
13
+ "keywords": [
14
+ "claude-code",
15
+ "antigravity",
16
+ "cursor",
17
+ "nextjs",
18
+ "scaffold",
19
+ "ebm"
20
+ ],
21
+ "license": "MIT"
22
+ }
@@ -0,0 +1,299 @@
1
+ # ebm-auth Reference
2
+
3
+ ## Config detection
4
+
5
+ Read `ebm.config.json` before generating anything:
6
+ ```json
7
+ {
8
+ "projectType": "landing | backoffice | both",
9
+ "colorMode": "dark | light | system",
10
+ "primaryColor": "#3b82f6",
11
+ "uiLib": "shadcn | antd | mui | none"
12
+ }
13
+ ```
14
+ If file missing → ask questions → save to `ebm.config.json`.
15
+
16
+ UI generation rules:
17
+ - **shadcn/ui**: use `<Button>`, `<Input>`, `<Card>` components
18
+ - **Ant Design**: use `<Form>`, `<Input>`, `<Button>`, `<Table>` components
19
+ - **MUI**: use `<TextField>`, `<Button>`, `<DataGrid>` components
20
+ - **none / fallback**: generate raw Tailwind (dark slate theme)
21
+
22
+ ---
23
+
24
+ ## Mode A — Next.js App Router (Fullstack)
25
+
26
+ ### Auth logic files
27
+
28
+ | File | Purpose |
29
+ |------|---------|
30
+ | `src/app/api/auth/[...nextauth]/route.ts` | NextAuth handler |
31
+ | `src/app/api/auth/[...nextauth]/auth-options.ts` | JWT config, providers, callbacks |
32
+ | `src/middleware.ts` | Route protection, first-login redirect |
33
+ | `src/lib/auth.ts` | hashPassword, comparePasswords, findUserByEmail |
34
+ | `src/lib/authorisation.ts` | CASL buildAbilityFor |
35
+ | `src/lib/permission.ts` | getDataPermission (DB query) |
36
+ | `src/lib/prisma.ts` | Prisma client singleton |
37
+ | `src/services/AuthProvider.tsx` | SessionProvider client wrapper |
38
+ | `src/services/AbilityProvider.tsx` | CASL ability context, syncs on session change |
39
+ | `src/store/useAbilityStore.ts` | Zustand store for abilities |
40
+ | `types/next-auth.d.ts` | Extended Session/JWT types |
41
+
42
+ ### auth-options.ts key config
43
+ ```typescript
44
+ // JWT strategy, 30-day session
45
+ // Credentials provider: email + password
46
+ // Brute force: 5 attempts → 1-minute block (in-memory Map)
47
+ // Session callback: embed id, email, name, phone_number,
48
+ // department_id, operation_hash[], reset_password, ad flags
49
+ ```
50
+
51
+ ### types/next-auth.d.ts
52
+ ```typescript
53
+ declare module 'next-auth' {
54
+ interface Session {
55
+ user: {
56
+ id: number; email: string; name: string
57
+ phone_number: string; department_id: number
58
+ operation_hash: string[]; reset_password: boolean; ad: boolean
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ ### middleware.ts
65
+ ```typescript
66
+ // withAuth — public routes: /signin, /forgot-password, /reset-password
67
+ // reset_password === false → redirect to /change-password-first
68
+ // no token → redirect to /signin
69
+ ```
70
+
71
+ ---
72
+
73
+ ## UI Pages
74
+
75
+ ### Route mapping by project type
76
+
77
+ | Page | backoffice route | landing route |
78
+ |------|-----------------|---------------|
79
+ | Sign in | `/signin` | `/signin` |
80
+ | Forgot password | `/forgot-password` | `/forgot-password` |
81
+ | Reset password | `/reset-password` | `/reset-password` |
82
+ | First login | `/change-password-first` | `/change-password-first` |
83
+ | User management | `/dashboard/users` | `/users` |
84
+ | User profile (own) | `/dashboard/profile` | `/profile` |
85
+ | User detail (admin) | `/dashboard/users/[id]` | `/users/[id]` |
86
+
87
+ ### Sign-in page
88
+ - Email + password form
89
+ - Error states: invalid credentials, TOO_MANY_ATTEMPTS
90
+ - Link to forgot password
91
+ - Calls `signIn('credentials', { email, password, redirect: false })`
92
+
93
+ ### Forgot password page
94
+ - Email input form
95
+ - Calls `POST /api/auth/resetpassword/request`
96
+ - Show success message after submit
97
+
98
+ ### Reset password page
99
+ - New password + confirm password fields
100
+ - Reads `token` from URL query params
101
+ - Calls `POST /api/auth/resetpassword/confirm`
102
+
103
+ ### First login page (`/change-password-first`)
104
+ - New password + confirm password
105
+ - Calls `PUT /api/change-password-first`
106
+ - Updates session `reset_password: true` on success
107
+ - Redirects to `/dashboard` or `/` based on project type
108
+
109
+ ### User management (`/dashboard/users` or `/users`)
110
+
111
+ **List page:**
112
+ - Table: name, email, role, department, status
113
+ - Search/filter by name or email
114
+ - Actions: edit, delete, reset password
115
+ - "Add user" button → create modal/page
116
+
117
+ **Create/Edit page (`/users/new`, `/users/[id]/edit`):**
118
+ - Fields: name, email, password (create only), phone_number
119
+ - Role selector (dropdown from DB)
120
+ - Department selector (dropdown from DB)
121
+ - reset_password toggle
122
+
123
+ ```typescript
124
+ // API routes needed
125
+ // GET /api/users → list users with role + department
126
+ // POST /api/users → create user
127
+ // GET /api/users/[id] → get single user
128
+ // PUT /api/users/[id] → update user
129
+ // DELETE /api/users/[id] → delete user
130
+ // PUT /api/users/[id]/reset-password → force password reset
131
+ ```
132
+
133
+ ### User profile — own (`/dashboard/profile` or `/profile`)
134
+ - Display: name, email, phone_number, role, department
135
+ - Edit: name, phone_number
136
+ - Change password section: current → new → confirm
137
+ - Calls `PUT /api/auth/passwordsetting`
138
+
139
+ ### User detail — admin (`/dashboard/users/[id]` or `/users/[id]`)
140
+ - Display all user fields: name, email, phone, role, department
141
+ - Permissions list: all operation_hash entries as badges
142
+ - Edit button → goes to edit page
143
+ - Reset password button → calls reset-password API
144
+
145
+ ---
146
+
147
+ ## Seed file (always generate)
148
+
149
+ `prisma/seed.ts` with `tsx` runner:
150
+ ```typescript
151
+ // Operations: USER/ROLE/DATA/REPORT/SETTING/DEPARTMENT/FILE × VIEW/ADD/EDIT/DELETE/DOWNLOAD/UPLOAD/OPERATE/RESET_PASSWORD
152
+ // Role: primaryAdmin → all operations
153
+ // Department: Administration
154
+ // User: admin@example.com / Admin@1234, reset_password: true
155
+ ```
156
+
157
+ Add to `package.json`:
158
+ ```json
159
+ {
160
+ "scripts": { "seed": "tsx prisma/seed.ts" },
161
+ "prisma": { "seed": "tsx prisma/seed.ts" }
162
+ }
163
+ ```
164
+ devDependencies: `tsx` (NOT ts-node — breaks on Windows)
165
+
166
+ ---
167
+
168
+ ## Optional: Azure AD
169
+
170
+ ```typescript
171
+ import AzureADProvider from 'next-auth/providers/azure-ad'
172
+ // First AD login: create user in DB with default role/department
173
+ // Set ad: true flag in session
174
+ ```
175
+ Env vars: `AZURE_AD_CLIENT_ID`, `AZURE_AD_CLIENT_SECRET`, `AZURE_AD_TENANT_ID`
176
+
177
+ ---
178
+
179
+ ## Optional: Email Password Reset
180
+
181
+ Files:
182
+ - `src/app/api/auth/resetpassword/request/route.ts` — 32-byte token, 15min expiry, send email
183
+ - `src/app/api/auth/resetpassword/confirm/route.ts` — validate token, hash password, delete token
184
+
185
+ Env vars: `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`
186
+
187
+ ---
188
+
189
+ ## Mode B — Next.js (FE) + FastAPI (BE)
190
+
191
+ ### FastAPI files
192
+
193
+ | File | Purpose |
194
+ |------|---------|
195
+ | `auth/router.py` | `/auth/login`, `/auth/logout`, `/auth/refresh` |
196
+ | `auth/dependencies.py` | `get_current_user` dependency |
197
+ | `auth/utils.py` | hash_password, verify_password, create_access_token |
198
+ | `auth/models.py` | User, Role, Permission, Department models |
199
+ | `auth/permissions.py` | casbin enforcer, buildAbilityFor |
200
+ | `auth/schemas.py` | LoginRequest, TokenResponse, UserOut |
201
+
202
+ ### Next.js files (Mode B)
203
+
204
+ | File | Purpose |
205
+ |------|---------|
206
+ | `src/app/signin/page.tsx` + `sign-in-form.tsx` | Sign-in UI |
207
+ | `src/lib/auth-client.ts` | API calls to FastAPI, httpOnly cookie storage |
208
+ | `src/middleware.ts` | Redirect to /signin if no token |
209
+
210
+ UI pages for Mode B follow the same route mapping above but call FastAPI endpoints instead of NextAuth.
211
+
212
+ ### auth/utils.py
213
+ ```python
214
+ from passlib.context import CryptContext
215
+ from jose import jwt
216
+ from datetime import datetime, timedelta
217
+
218
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
219
+
220
+ def hash_password(password: str) -> str:
221
+ return pwd_context.hash(password)
222
+
223
+ def verify_password(plain: str, hashed: str) -> bool:
224
+ return pwd_context.verify(plain, hashed)
225
+
226
+ def create_access_token(data: dict, expires_delta=None) -> str:
227
+ to_encode = data.copy()
228
+ expire = datetime.utcnow() + (expires_delta or timedelta(minutes=30))
229
+ to_encode.update({"exp": expire})
230
+ return jwt.encode(to_encode, SECRET_KEY, algorithm="HS256")
231
+ ```
232
+
233
+ ---
234
+
235
+ ## RBAC Reference
236
+
237
+ ### Permission format: `SUBJECT__ACTION`
238
+ Subjects: `USER ROLE DATA REPORT SETTING DEPARTMENT FILE`
239
+ Actions: `VIEW ADD EDIT DELETE DOWNLOAD UPLOAD OPERATE RESET_PASSWORD`
240
+
241
+ ### DB Schema
242
+ ```sql
243
+ users (id, email, password_hash, name, phone_number, department_id, role_id, reset_password, ad, created_at)
244
+ roles (id, name)
245
+ operations (id, name) -- e.g. "DATA__VIEW"
246
+ role_operations (role_id, operation_id)
247
+ departments (id, name)
248
+ department_operations (department_id, operation_id)
249
+ ```
250
+
251
+ ---
252
+
253
+ ## .env.example additions
254
+
255
+ ### Mode A
256
+ ```env
257
+ NEXTAUTH_SECRET=your-secret-here
258
+ NEXTAUTH_URL=http://localhost:3000
259
+ DATABASE_URL=postgresql://user:password@localhost:5432/dbname
260
+ # Azure AD (optional)
261
+ AZURE_AD_CLIENT_ID=
262
+ AZURE_AD_CLIENT_SECRET=
263
+ AZURE_AD_TENANT_ID=
264
+ # Email Reset (optional)
265
+ SMTP_HOST=
266
+ SMTP_PORT=587
267
+ SMTP_USER=
268
+ SMTP_PASS=
269
+ ```
270
+
271
+ ### Mode B
272
+ ```env
273
+ DATABASE_URL=postgresql://user:password@localhost:5432/dbname
274
+ SECRET_KEY=your-secret-here
275
+ ACCESS_TOKEN_EXPIRE_MINUTES=43200
276
+ NEXT_PUBLIC_API_URL=http://localhost:8000
277
+ ```
278
+
279
+ ---
280
+
281
+ ## Post-generation summary template
282
+ ```
283
+ ✓ Generated [N] auth logic files
284
+ ✓ Generated [N] UI pages
285
+ ⚠ [N] conflicts → written as *.auth.* (merge manually)
286
+ 📋 Env vars appended to .env.example
287
+
288
+ Default credentials:
289
+ Email: admin@example.com
290
+ Password: Admin@1234
291
+
292
+ Next steps:
293
+ 1. npm install next-auth bcryptjs @casl/ability zustand
294
+ 2. npm install -D @types/bcryptjs tsx
295
+ 3. Copy .env.example → .env.local and fill in values
296
+ 4. npx prisma migrate dev --name init
297
+ 5. npx prisma db seed
298
+ 6. npm run dev
299
+ ```
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: ebm-auth
3
+ description: Generate a full authentication system for a Next.js project — JWT sessions, RBAC with CASL, brute-force protection, and complete UI pages (signin, forgot-password, reset-password, first-login, user management, user profile). Reads project config from ebm.config.json. Supports Next.js fullstack (NextAuth) and Next.js + FastAPI. Use when user invokes /ebm-auth or asks to add authentication to an existing project.
4
+ ---
5
+
6
+ # /ebm-auth
7
+
8
+ ### Step 1 — Read config
9
+ ```
10
+ ebm.config.json exists? → use projectType, uiLib, primaryColor
11
+ else → ask questions → save to ebm.config.json
12
+ ```
13
+
14
+ ### Step 2 — Detect stack
15
+ ```
16
+ Has next-auth → Mode A (Next.js fullstack)
17
+ Has FastAPI indicators → Mode B (Next.js + FastAPI)
18
+ Unclear → ask
19
+ ORM: @prisma/client → Prisma | drizzle-orm → Drizzle | sqlalchemy → SQLAlchemy | else ask
20
+ ```
21
+
22
+ ### Step 3 — Ask optional modules
23
+ - Azure AD SSO?
24
+ - Email-based password reset?
25
+
26
+ ### Step 4 — Generate auth logic + UI pages
27
+ See [REFERENCE.md](REFERENCE.md) for all files, templates, routes.
28
+
29
+ ### Step 5 — Post-generation rules
30
+ - Append to `.env.example` (create if missing)
31
+ - `tsconfig.json`: ensure `"@/*": ["./src/*"]` paths exist
32
+ - Conflict: file exists → write `filename.auth.ext` + merge comment
33
+ - Seed: always `tsx prisma/seed.ts` (Windows-safe, never ts-node)
34
+ - Print summary + next steps
35
+
36
+ ## Shared rules
37
+ - Path alias: `@/*` → `./src/*` always
38
+ - Thai UI text: use formal Thai — see `/ebm-thai` glossary