project-mcp-server 2.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/LICENSE +21 -0
- package/README.md +335 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.js +38 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/scanner/actions.d.ts +9 -0
- package/dist/scanner/actions.js +77 -0
- package/dist/scanner/actions.js.map +1 -0
- package/dist/scanner/cache.d.ts +4 -0
- package/dist/scanner/cache.js +34 -0
- package/dist/scanner/cache.js.map +1 -0
- package/dist/scanner/components.d.ts +17 -0
- package/dist/scanner/components.js +74 -0
- package/dist/scanner/components.js.map +1 -0
- package/dist/scanner/prisma.d.ts +17 -0
- package/dist/scanner/prisma.js +75 -0
- package/dist/scanner/prisma.js.map +1 -0
- package/dist/scanner/routes.d.ts +11 -0
- package/dist/scanner/routes.js +98 -0
- package/dist/scanner/routes.js.map +1 -0
- package/dist/setup.d.ts +10 -0
- package/dist/setup.js +289 -0
- package/dist/setup.js.map +1 -0
- package/dist/tools/env/index.d.ts +2 -0
- package/dist/tools/env/index.js +152 -0
- package/dist/tools/env/index.js.map +1 -0
- package/dist/tools/generate/index.d.ts +2 -0
- package/dist/tools/generate/index.js +320 -0
- package/dist/tools/generate/index.js.map +1 -0
- package/dist/tools/project/index.d.ts +2 -0
- package/dist/tools/project/index.js +193 -0
- package/dist/tools/project/index.js.map +1 -0
- package/package.json +35 -0
- package/skills/commit.md +109 -0
- package/skills/infra.md +218 -0
- package/skills/nextauth.md +256 -0
- package/skills/nextjs.md +262 -0
- package/skills/prisma.md +281 -0
- package/skills/project-intelligence.SKILL.md +141 -0
- package/skills/security.md +353 -0
- package/skills/shadcn.md +299 -0
- package/skills/testing.md +188 -0
- package/skills/zod.md +253 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing
|
|
3
|
+
description: "Activar cuando se escriben tests unitarios o de integracion, o se testean Server Actions, Route Handlers o componentes React"
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Skill: Testing
|
|
10
|
+
|
|
11
|
+
## Cuándo cargar esta skill
|
|
12
|
+
- Escribir tests unitarios o de integración
|
|
13
|
+
- Testear Server Actions o Route Handlers
|
|
14
|
+
- Testear componentes React
|
|
15
|
+
- Correr o debuggear la suite de tests
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Setup recomendado (si no está instalado)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Vitest para unit/integration (más rápido que Jest, nativo ESM)
|
|
23
|
+
pnpm add -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/user-event
|
|
24
|
+
|
|
25
|
+
# O Jest si ya está configurado en el proyecto
|
|
26
|
+
pnpm add -D jest @types/jest jest-environment-jsdom ts-jest
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Tests de Server Actions
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// server/actions/__tests__/users.test.ts
|
|
35
|
+
import { describe, it, expect, vi, beforeEach } from "vitest"
|
|
36
|
+
import { createUser } from "../users"
|
|
37
|
+
|
|
38
|
+
// Mock de Prisma — nunca tocar la DB real en tests
|
|
39
|
+
vi.mock("@/lib/db/postgres", () => ({
|
|
40
|
+
prismaPostgres: {
|
|
41
|
+
user: {
|
|
42
|
+
create: vi.fn(),
|
|
43
|
+
findUnique: vi.fn()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
// Mock de auth
|
|
49
|
+
vi.mock("@/lib/auth", () => ({
|
|
50
|
+
auth: vi.fn().mockResolvedValue({
|
|
51
|
+
user: { id: "user-1", email: "test@test.com", roleId: "admin" }
|
|
52
|
+
})
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
import { prismaPostgres } from "@/lib/db/postgres"
|
|
56
|
+
|
|
57
|
+
describe("createUser", () => {
|
|
58
|
+
beforeEach(() => vi.clearAllMocks())
|
|
59
|
+
|
|
60
|
+
it("crea un usuario con datos válidos", async () => {
|
|
61
|
+
const mockUser = { id: "new-id", name: "Ana", email: "ana@test.com" }
|
|
62
|
+
vi.mocked(prismaPostgres.user.create).mockResolvedValue(mockUser as any)
|
|
63
|
+
|
|
64
|
+
const formData = new FormData()
|
|
65
|
+
formData.set("name", "Ana")
|
|
66
|
+
formData.set("email", "ana@test.com")
|
|
67
|
+
formData.set("roleId", "cluid123")
|
|
68
|
+
|
|
69
|
+
const result = await createUser(null, formData)
|
|
70
|
+
|
|
71
|
+
expect(result.success).toBe(true)
|
|
72
|
+
expect(prismaPostgres.user.create).toHaveBeenCalledWith({
|
|
73
|
+
data: { name: "Ana", email: "ana@test.com", roleId: "cluid123" }
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("retorna error con email inválido", async () => {
|
|
78
|
+
const formData = new FormData()
|
|
79
|
+
formData.set("name", "Ana")
|
|
80
|
+
formData.set("email", "no-es-email")
|
|
81
|
+
formData.set("roleId", "cluid123")
|
|
82
|
+
|
|
83
|
+
const result = await createUser(null, formData)
|
|
84
|
+
|
|
85
|
+
expect(result.success).toBe(false)
|
|
86
|
+
if (!result.success) {
|
|
87
|
+
expect(result.error).toHaveProperty("email")
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it("retorna error si no hay sesión", async () => {
|
|
92
|
+
const { auth } = await import("@/lib/auth")
|
|
93
|
+
vi.mocked(auth).mockResolvedValueOnce(null)
|
|
94
|
+
|
|
95
|
+
const formData = new FormData()
|
|
96
|
+
formData.set("name", "Ana")
|
|
97
|
+
formData.set("email", "ana@test.com")
|
|
98
|
+
formData.set("roleId", "cluid123")
|
|
99
|
+
|
|
100
|
+
const result = await createUser(null, formData)
|
|
101
|
+
expect(result.success).toBe(false)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Tests de Schemas Zod
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// types/schemas/__tests__/auth.test.ts
|
|
112
|
+
import { describe, it, expect } from "vitest"
|
|
113
|
+
import { LoginSchema, RegisterSchema } from "../auth"
|
|
114
|
+
|
|
115
|
+
describe("LoginSchema", () => {
|
|
116
|
+
it("acepta credenciales válidas", () => {
|
|
117
|
+
const result = LoginSchema.safeParse({
|
|
118
|
+
email: "user@example.com",
|
|
119
|
+
password: "password123"
|
|
120
|
+
})
|
|
121
|
+
expect(result.success).toBe(true)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it("rechaza email inválido", () => {
|
|
125
|
+
const result = LoginSchema.safeParse({
|
|
126
|
+
email: "no-es-email",
|
|
127
|
+
password: "password123"
|
|
128
|
+
})
|
|
129
|
+
expect(result.success).toBe(false)
|
|
130
|
+
expect(result.error?.flatten().fieldErrors.email).toBeDefined()
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe("RegisterSchema", () => {
|
|
135
|
+
it("rechaza contraseñas que no coinciden", () => {
|
|
136
|
+
const result = RegisterSchema.safeParse({
|
|
137
|
+
name: "Ana",
|
|
138
|
+
email: "ana@test.com",
|
|
139
|
+
password: "Password1",
|
|
140
|
+
confirmPassword: "Diferente1"
|
|
141
|
+
})
|
|
142
|
+
expect(result.success).toBe(false)
|
|
143
|
+
expect(result.error?.flatten().fieldErrors.confirmPassword).toBeDefined()
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Tests de componentes React
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// components/users/__tests__/user-card.test.tsx
|
|
154
|
+
import { describe, it, expect } from "vitest"
|
|
155
|
+
import { render, screen } from "@testing-library/react"
|
|
156
|
+
import { UserCard } from "../user-card"
|
|
157
|
+
|
|
158
|
+
const mockUser = {
|
|
159
|
+
name: "Ana García",
|
|
160
|
+
email: "ana@test.com",
|
|
161
|
+
role: "admin"
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
describe("UserCard", () => {
|
|
165
|
+
it("muestra el nombre y email del usuario", () => {
|
|
166
|
+
render(<UserCard user={mockUser} />)
|
|
167
|
+
|
|
168
|
+
expect(screen.getByText("Ana García")).toBeInTheDocument()
|
|
169
|
+
expect(screen.getByText("ana@test.com")).toBeInTheDocument()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it("muestra el badge del rol", () => {
|
|
173
|
+
render(<UserCard user={mockUser} />)
|
|
174
|
+
expect(screen.getByText("admin")).toBeInTheDocument()
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Comandos
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
pnpm test # correr tests en watch mode
|
|
185
|
+
pnpm test --run # correr una sola vez (CI)
|
|
186
|
+
pnpm test --coverage # con reporte de cobertura
|
|
187
|
+
pnpm test src/server # solo un directorio
|
|
188
|
+
```
|
package/skills/zod.md
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zod
|
|
3
|
+
description: "Activar cuando se crean o modifican schemas de validacion con Zod en Server Actions, Route Handlers o formularios"
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
version: "1.0.0"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Skill: Zod 4 — validación y schemas
|
|
10
|
+
|
|
11
|
+
## Cuándo cargar esta skill
|
|
12
|
+
- Crear o modificar schemas de validación
|
|
13
|
+
- Validar datos en Server Actions o Route Handlers
|
|
14
|
+
- Definir DTOs de entrada/salida
|
|
15
|
+
- Inferir tipos TypeScript desde schemas
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Patrones base de Zod 4
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { z } from "zod"
|
|
23
|
+
|
|
24
|
+
// Schema básico con mensajes en español
|
|
25
|
+
const UserSchema = z.object({
|
|
26
|
+
name: z.string().min(2, "Mínimo 2 caracteres").max(100, "Máximo 100 caracteres"),
|
|
27
|
+
email: z.string().email("Email inválido"),
|
|
28
|
+
age: z.number().int().min(18, "Debe ser mayor de 18").optional(),
|
|
29
|
+
role: z.enum(["admin", "editor", "viewer"], {
|
|
30
|
+
errorMap: () => ({ message: "Rol inválido" })
|
|
31
|
+
}),
|
|
32
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Inferir tipo TypeScript automáticamente
|
|
36
|
+
type User = z.infer<typeof UserSchema>
|
|
37
|
+
// → { name: string; email: string; age?: number; role: "admin"|"editor"|"viewer" }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Dónde ubicar los schemas
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
src/
|
|
46
|
+
└── types/
|
|
47
|
+
└── schemas/
|
|
48
|
+
├── auth.ts → LoginSchema, RegisterSchema
|
|
49
|
+
├── users.ts → CreateUserSchema, UpdateUserSchema, UserFiltersSchema
|
|
50
|
+
├── posts.ts → CreatePostSchema, UpdatePostSchema
|
|
51
|
+
└── index.ts → re-exporta todo
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Validación en Server Actions
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// server/actions/users.ts
|
|
60
|
+
"use server"
|
|
61
|
+
|
|
62
|
+
import { z } from "zod"
|
|
63
|
+
import { prismaPostgres } from "@/lib/db/postgres"
|
|
64
|
+
import { auth } from "@/lib/auth"
|
|
65
|
+
|
|
66
|
+
// Schema del DTO de entrada
|
|
67
|
+
const CreateUserSchema = z.object({
|
|
68
|
+
name: z.string().min(2).max(100),
|
|
69
|
+
email: z.string().email(),
|
|
70
|
+
roleId: z.string().cuid()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Tipo de respuesta tipado
|
|
74
|
+
type ActionResult<T> =
|
|
75
|
+
| { success: true; data: T }
|
|
76
|
+
| { success: false; error: Record<string, string[]> | string }
|
|
77
|
+
|
|
78
|
+
export async function createUser(
|
|
79
|
+
_prevState: unknown,
|
|
80
|
+
formData: FormData
|
|
81
|
+
): Promise<ActionResult<{ id: string }>> {
|
|
82
|
+
// 1. Autenticar
|
|
83
|
+
const session = await auth()
|
|
84
|
+
if (!session) return { success: false, error: "No autenticado" }
|
|
85
|
+
|
|
86
|
+
// 2. Parsear y validar
|
|
87
|
+
const parsed = CreateUserSchema.safeParse({
|
|
88
|
+
name: formData.get("name"),
|
|
89
|
+
email: formData.get("email"),
|
|
90
|
+
roleId: formData.get("roleId")
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
if (!parsed.success) {
|
|
94
|
+
return {
|
|
95
|
+
success: false,
|
|
96
|
+
error: parsed.error.flatten().fieldErrors
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 3. Ejecutar
|
|
101
|
+
const user = await prismaPostgres.user.create({ data: parsed.data })
|
|
102
|
+
|
|
103
|
+
return { success: true, data: { id: user.id } }
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Validación en Route Handlers
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// app/api/users/route.ts
|
|
113
|
+
import { NextRequest, NextResponse } from "next/server"
|
|
114
|
+
import { z } from "zod"
|
|
115
|
+
|
|
116
|
+
const QueryParamsSchema = z.object({
|
|
117
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
118
|
+
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
119
|
+
search: z.string().optional(),
|
|
120
|
+
role: z.enum(["admin", "editor", "viewer"]).optional()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const CreateUserBodySchema = z.object({
|
|
124
|
+
name: z.string().min(2),
|
|
125
|
+
email: z.string().email(),
|
|
126
|
+
password: z.string().min(8).regex(/[A-Z]/, "Debe tener al menos una mayúscula")
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
export async function GET(request: NextRequest) {
|
|
130
|
+
const { searchParams } = new URL(request.url)
|
|
131
|
+
const params = QueryParamsSchema.safeParse(Object.fromEntries(searchParams))
|
|
132
|
+
|
|
133
|
+
if (!params.success) {
|
|
134
|
+
return NextResponse.json(
|
|
135
|
+
{ error: params.error.flatten().fieldErrors },
|
|
136
|
+
{ status: 400 }
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { page, limit, search, role } = params.data
|
|
141
|
+
// ...
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function POST(request: NextRequest) {
|
|
145
|
+
const body = await request.json().catch(() => null)
|
|
146
|
+
if (!body) {
|
|
147
|
+
return NextResponse.json({ error: "Body inválido" }, { status: 400 })
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const parsed = CreateUserBodySchema.safeParse(body)
|
|
151
|
+
if (!parsed.success) {
|
|
152
|
+
return NextResponse.json(
|
|
153
|
+
{ error: parsed.error.flatten().fieldErrors },
|
|
154
|
+
{ status: 422 }
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ...
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Schemas de autenticación
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// types/schemas/auth.ts
|
|
168
|
+
import { z } from "zod"
|
|
169
|
+
|
|
170
|
+
export const LoginSchema = z.object({
|
|
171
|
+
email: z.string().email("Email inválido"),
|
|
172
|
+
password: z.string().min(1, "La contraseña es requerida")
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
export const RegisterSchema = z.object({
|
|
176
|
+
name: z.string().min(2, "Mínimo 2 caracteres"),
|
|
177
|
+
email: z.string().email("Email inválido"),
|
|
178
|
+
password: z
|
|
179
|
+
.string()
|
|
180
|
+
.min(8, "Mínimo 8 caracteres")
|
|
181
|
+
.regex(/[A-Z]/, "Al menos una mayúscula")
|
|
182
|
+
.regex(/[0-9]/, "Al menos un número"),
|
|
183
|
+
confirmPassword: z.string()
|
|
184
|
+
}).refine(
|
|
185
|
+
(data) => data.password === data.confirmPassword,
|
|
186
|
+
{ message: "Las contraseñas no coinciden", path: ["confirmPassword"] }
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
export type LoginInput = z.infer<typeof LoginSchema>
|
|
190
|
+
export type RegisterInput = z.infer<typeof RegisterSchema>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Transformaciones y refinements útiles
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// Transformar y sanitizar datos
|
|
199
|
+
const SlugSchema = z
|
|
200
|
+
.string()
|
|
201
|
+
.toLowerCase()
|
|
202
|
+
.trim()
|
|
203
|
+
.transform(s => s.replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, ""))
|
|
204
|
+
|
|
205
|
+
// Validaciones condicionales
|
|
206
|
+
const PaymentSchema = z.discriminatedUnion("method", [
|
|
207
|
+
z.object({
|
|
208
|
+
method: z.literal("credit_card"),
|
|
209
|
+
cardNumber: z.string().length(16),
|
|
210
|
+
cvv: z.string().length(3)
|
|
211
|
+
}),
|
|
212
|
+
z.object({
|
|
213
|
+
method: z.literal("bank_transfer"),
|
|
214
|
+
bankCode: z.string(),
|
|
215
|
+
accountNumber: z.string()
|
|
216
|
+
})
|
|
217
|
+
])
|
|
218
|
+
|
|
219
|
+
// Coerciones útiles para FormData (siempre strings)
|
|
220
|
+
const FilterSchema = z.object({
|
|
221
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
222
|
+
active: z.coerce.boolean().default(true),
|
|
223
|
+
date: z.coerce.date().optional()
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
// Parsear desde FormData con coerción
|
|
227
|
+
const data = FilterSchema.parse({
|
|
228
|
+
page: formData.get("page"), // "2" → 2
|
|
229
|
+
active: formData.get("active"), // "true" → true
|
|
230
|
+
})
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Zod 4 — cambios respecto a v3
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// Zod 4: .flatten() sigue igual para errores
|
|
239
|
+
const result = schema.safeParse(input)
|
|
240
|
+
if (!result.success) {
|
|
241
|
+
result.error.flatten().fieldErrors // { campo: ["mensaje"] }
|
|
242
|
+
result.error.flatten().formErrors // ["error general"]
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Zod 4: z.coerce es igual pero más rápido
|
|
246
|
+
z.coerce.number() // "123" → 123
|
|
247
|
+
|
|
248
|
+
// Zod 4: z.pipe() para transformaciones en cadena
|
|
249
|
+
const schema = z.string().pipe(z.coerce.number())
|
|
250
|
+
|
|
251
|
+
// Zod 4: mejor performance en .parse() con objetos grandes
|
|
252
|
+
// No necesita cambios en el código, mejora automática
|
|
253
|
+
```
|