create-loadout 1.0.2 → 1.0.4
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 +1 -1
- package/dist/claude-md.js +504 -333
- package/dist/cli.js +2 -2
- package/dist/engine.js +24 -24
- package/dist/env.js +12 -25
- package/dist/generate-readme.js +123 -120
- package/dist/mcp-server.js +4 -0
- package/dist/templates/neon-drizzle.js +329 -327
- package/package.json +1 -1
package/dist/claude-md.js
CHANGED
|
@@ -99,10 +99,10 @@ export async function generateClaudeMd(projectPath, config) {
|
|
|
99
99
|
const hasPostHog = config.integrations.includes('posthog');
|
|
100
100
|
const hasSentry = config.integrations.includes('sentry');
|
|
101
101
|
const hasClerk = config.integrations.includes('clerk');
|
|
102
|
-
let content = `# ${config.name}
|
|
103
|
-
|
|
104
|
-
## Tech Stack
|
|
105
|
-
|
|
102
|
+
let content = `# ${config.name}
|
|
103
|
+
|
|
104
|
+
## Tech Stack
|
|
105
|
+
|
|
106
106
|
`;
|
|
107
107
|
for (const section of sections) {
|
|
108
108
|
content += `### ${section.name}\n`;
|
|
@@ -111,368 +111,539 @@ export async function generateClaudeMd(projectPath, config) {
|
|
|
111
111
|
}
|
|
112
112
|
content += '\n';
|
|
113
113
|
}
|
|
114
|
-
content += `## Development Commands
|
|
115
|
-
|
|
116
|
-
\`\`\`bash
|
|
117
|
-
npm run dev # Start development server
|
|
118
|
-
npm run build # Build for production
|
|
119
|
-
npm run start # Start production server
|
|
120
|
-
npm run lint # Run ESLint
|
|
121
|
-
\`\`\`
|
|
114
|
+
content += `## Development Commands
|
|
115
|
+
|
|
116
|
+
\`\`\`bash
|
|
117
|
+
npm run dev # Start development server
|
|
118
|
+
npm run build # Build for production
|
|
119
|
+
npm run start # Start production server
|
|
120
|
+
npm run lint # Run ESLint
|
|
121
|
+
\`\`\`
|
|
122
122
|
`;
|
|
123
123
|
if (hasDb) {
|
|
124
|
-
content += `
|
|
125
|
-
### Database Commands
|
|
126
|
-
|
|
127
|
-
\`\`\`bash
|
|
128
|
-
npm run db:generate # Generate migrations from schema changes
|
|
129
|
-
npm run db:migrate # Apply migrations to database
|
|
130
|
-
npm run db:studio # Open Drizzle Studio to browse data
|
|
131
|
-
\`\`\`
|
|
124
|
+
content += `
|
|
125
|
+
### Database Commands
|
|
126
|
+
|
|
127
|
+
\`\`\`bash
|
|
128
|
+
npm run db:generate # Generate migrations from schema changes
|
|
129
|
+
npm run db:migrate # Apply migrations to database
|
|
130
|
+
npm run db:studio # Open Drizzle Studio to browse data
|
|
131
|
+
\`\`\`
|
|
132
132
|
`;
|
|
133
133
|
}
|
|
134
134
|
if (config.integrations.includes('inngest')) {
|
|
135
|
-
content += `
|
|
136
|
-
### Background Jobs
|
|
137
|
-
|
|
138
|
-
\`\`\`bash
|
|
139
|
-
npm run inngest:dev # Start Inngest dev server for local testing
|
|
140
|
-
\`\`\`
|
|
135
|
+
content += `
|
|
136
|
+
### Background Jobs
|
|
137
|
+
|
|
138
|
+
\`\`\`bash
|
|
139
|
+
npm run inngest:dev # Start Inngest dev server for local testing
|
|
140
|
+
\`\`\`
|
|
141
141
|
`;
|
|
142
142
|
}
|
|
143
|
-
content += `
|
|
144
|
-
## Architecture
|
|
145
|
-
|
|
146
|
-
### Directory Structure
|
|
147
|
-
|
|
148
|
-
\`\`\`
|
|
149
|
-
├── app/ # Next.js App Router pages and API routes
|
|
150
|
-
├── components/ # React components (including shadcn/ui)
|
|
143
|
+
content += `
|
|
144
|
+
## Architecture
|
|
145
|
+
|
|
146
|
+
### Directory Structure
|
|
147
|
+
|
|
148
|
+
\`\`\`
|
|
149
|
+
├── app/ # Next.js App Router pages and API routes
|
|
150
|
+
├── components/ # React components (including shadcn/ui)
|
|
151
151
|
`;
|
|
152
152
|
if (config.integrations.includes('resend') || config.integrations.includes('postmark')) {
|
|
153
|
-
content += `│ └── emails/ # Email templates
|
|
153
|
+
content += `│ └── emails/ # Email templates
|
|
154
154
|
`;
|
|
155
155
|
}
|
|
156
156
|
if (hasPostHog || hasSentry) {
|
|
157
|
-
content += `├── instrumentation-client.ts # Client-side init
|
|
157
|
+
content += `├── instrumentation-client.ts # Client-side init
|
|
158
158
|
`;
|
|
159
159
|
}
|
|
160
160
|
if (hasSentry) {
|
|
161
|
-
content += `├── instrumentation.ts # Server-side Sentry registration
|
|
161
|
+
content += `├── instrumentation.ts # Server-side Sentry registration
|
|
162
162
|
`;
|
|
163
163
|
}
|
|
164
164
|
if (hasDb) {
|
|
165
|
-
content += `├── actions/ # Server actions (form submissions)
|
|
166
|
-
│ └── *.actions.ts
|
|
167
|
-
├── services/ # Business logic orchestration
|
|
168
|
-
│ └── *.service.ts
|
|
169
|
-
├── dao/ # Data access objects (database queries)
|
|
170
|
-
│ └── *.dao.ts
|
|
171
|
-
├── mappers/ # Data transformation
|
|
172
|
-
│ └── *.mapper.ts
|
|
173
|
-
├── models/ # Type definitions
|
|
174
|
-
│ ├── *.dto.ts # Database types (InferSelectModel)
|
|
175
|
-
│ ├── *.view.ts # View models for UI
|
|
176
|
-
│ ├── *.schema.ts # Zod validation + ServiceRequest/Result
|
|
177
|
-
│ ├── *.state.ts # Action state objects
|
|
178
|
-
│ └── *ServiceError.enum.ts # Service error enums
|
|
165
|
+
content += `├── actions/ # Server actions (form submissions)
|
|
166
|
+
│ └── *.actions.ts
|
|
167
|
+
├── services/ # Business logic orchestration
|
|
168
|
+
│ └── *.service.ts
|
|
169
|
+
├── dao/ # Data access objects (database queries)
|
|
170
|
+
│ └── *.dao.ts
|
|
171
|
+
├── mappers/ # Data transformation
|
|
172
|
+
│ └── *.mapper.ts
|
|
173
|
+
├── models/ # Type definitions
|
|
174
|
+
│ ├── *.dto.ts # Database types (InferSelectModel)
|
|
175
|
+
│ ├── *.view.ts # View models for UI
|
|
176
|
+
│ ├── *.schema.ts # Zod validation + ServiceRequest/Result
|
|
177
|
+
│ ├── *.state.ts # Action state objects
|
|
178
|
+
│ └── *ServiceError.enum.ts # Service error enums
|
|
179
179
|
`;
|
|
180
180
|
}
|
|
181
181
|
else {
|
|
182
|
-
content += `├── services/ # Business logic services
|
|
183
|
-
│ └── *.service.ts
|
|
182
|
+
content += `├── services/ # Business logic services
|
|
183
|
+
│ └── *.service.ts
|
|
184
184
|
`;
|
|
185
185
|
}
|
|
186
|
-
content += `├── lib/
|
|
187
|
-
│ ├── config.ts # Centralized environment variables
|
|
188
|
-
│ ├── stores/ # Zustand stores for client state
|
|
189
|
-
│ │ └── *.store.ts
|
|
186
|
+
content += `├── lib/
|
|
187
|
+
│ ├── config.ts # Centralized environment variables
|
|
188
|
+
│ ├── stores/ # Zustand stores for client state
|
|
189
|
+
│ │ └── *.store.ts
|
|
190
190
|
`;
|
|
191
191
|
if (hasDb) {
|
|
192
|
-
content += `│ └── db/ # Database client and schema
|
|
192
|
+
content += `│ └── db/ # Database client and schema
|
|
193
193
|
`;
|
|
194
194
|
}
|
|
195
|
-
content += `└── public/ # Static assets
|
|
196
|
-
\`\`\`
|
|
195
|
+
content += `└── public/ # Static assets
|
|
196
|
+
\`\`\`
|
|
197
197
|
`;
|
|
198
198
|
if (hasDb) {
|
|
199
|
-
content += `
|
|
200
|
-
### Layered Architecture
|
|
201
|
-
|
|
202
|
-
The application follows a strict 4-layer architecture:
|
|
203
|
-
|
|
204
|
-
\`\`\`
|
|
205
|
-
UI Components (app/, components/)
|
|
206
|
-
↓
|
|
207
|
-
Server Actions (actions/*.actions.ts)
|
|
208
|
-
↓
|
|
209
|
-
Services (services/*.service.ts)
|
|
210
|
-
↓
|
|
211
|
-
DAOs (dao/*.dao.ts)
|
|
212
|
-
↓
|
|
213
|
-
Database (Drizzle ORM)
|
|
214
|
-
\`\`\`
|
|
215
|
-
|
|
216
|
-
**Layer responsibilities:**
|
|
217
|
-
|
|
218
|
-
- **Actions** - Handle form submissions, validate with Zod, check auth, call services, revalidate cache
|
|
219
|
-
- **Services** - Orchestrate business logic, coordinate multiple DAOs
|
|
220
|
-
- **DAOs** - Encapsulate all database queries using Drizzle ORM
|
|
221
|
-
- **Mappers** - Transform between DTOs, service requests, and view models
|
|
222
|
-
|
|
223
|
-
### Model File Naming Conventions
|
|
224
|
-
|
|
225
|
-
Files in \`models/\` follow strict naming:
|
|
226
|
-
|
|
227
|
-
| Pattern | Purpose | Example |
|
|
228
|
-
|---------|---------|---------|
|
|
229
|
-
| \`*.dto.ts\` | Database types from Drizzle | \`UserDto\`, \`UserInsertDto\` |
|
|
230
|
-
| \`*.view.ts\` | View models for UI | \`UserView\` |
|
|
231
|
-
| \`*.schema.ts\` | Zod schemas + service types | \`UserCreateFormSchema\`, \`UserCreateServiceRequest\` |
|
|
232
|
-
| \`*.state.ts\` | Action state objects | \`UserCreateState\` |
|
|
233
|
-
| \`*ServiceError.enum.ts\` | Service error enums | \`UserServiceError\` |
|
|
234
|
-
|
|
235
|
-
### Action File Organization
|
|
236
|
-
|
|
237
|
-
One action file per domain entity: \`actions/{entity}.action.ts\`. Do NOT split by operation type.
|
|
238
|
-
|
|
239
|
-
\`\`\`
|
|
240
|
-
actions/
|
|
241
|
-
project.action.ts # createProject, updateProject, deleteProject, searchProjects
|
|
242
|
-
task.action.ts # createTask, updateTask, deleteTask, searchTasks
|
|
243
|
-
comment.action.ts # createComment, deleteComment
|
|
244
|
-
settings.action.ts # updateSettings
|
|
245
|
-
\`\`\`
|
|
246
|
-
|
|
247
|
-
Do NOT create separate files like \`project.create.action.ts\` or \`task.search.action.ts\`.
|
|
248
|
-
|
|
249
|
-
### Server Action Pattern
|
|
250
|
-
|
|
251
|
-
\`\`\`typescript
|
|
252
|
-
'use server';
|
|
253
|
-
|
|
254
|
-
export async function createEntity(
|
|
255
|
-
state: EntityCreateState,
|
|
256
|
-
formData: FormData
|
|
257
|
-
): Promise<EntityCreateState> {
|
|
258
|
-
const user = await currentUser();
|
|
259
|
-
if (!user) {
|
|
260
|
-
return { success: false, error: 'Not authenticated', data: null };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const rawData = Object.fromEntries(formData);
|
|
264
|
-
const validated = EntityCreateSchema.safeParse(rawData);
|
|
265
|
-
|
|
266
|
-
if (!validated.success) {
|
|
267
|
-
return { success: false, error: z.prettifyError(validated.error), data: null };
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
try {
|
|
271
|
-
const result = await entityService.createEntity(validated.data);
|
|
272
|
-
revalidatePath('/entities');
|
|
273
|
-
return { success: true, error: null, data: result };
|
|
274
|
-
} catch (error) {
|
|
275
|
-
return { success: false, error: 'Failed to create entity', data: null };
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
\`\`\`
|
|
279
|
-
|
|
280
|
-
### DAO Pattern
|
|
281
|
-
|
|
282
|
-
\`\`\`typescript
|
|
283
|
-
export class EntityDAO {
|
|
284
|
-
async create(dto: EntityInsertDto): Promise<EntityDto | undefined> {
|
|
285
|
-
const [created] = await db.insert(entities).values(dto).returning();
|
|
286
|
-
return created;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async getById(id: string): Promise<EntityDto | undefined> {
|
|
290
|
-
return await db.query.entities.findFirst({
|
|
291
|
-
where: eq(entities.id, id),
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export const entityDAO = new EntityDAO();
|
|
297
|
-
\`\`\`
|
|
298
|
-
|
|
299
|
-
### Service Error Enums
|
|
300
|
-
|
|
301
|
-
Each service class has a corresponding error enum in \`models/{serviceName}ServiceError.enum.ts\`. Services throw errors using enum values, actions catch and translate to user-friendly messages.
|
|
302
|
-
|
|
303
|
-
\`\`\`typescript
|
|
304
|
-
// models/performanceServiceError.enum.ts
|
|
305
|
-
export enum PerformanceServiceError {
|
|
306
|
-
NotFound = "PERFORMANCE_NOT_FOUND",
|
|
307
|
-
NotOwned = "PERFORMANCE_NOT_OWNED",
|
|
308
|
-
DuplicateTime = "PERFORMANCE_DUPLICATE_TIME",
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// services/performance.service.ts
|
|
312
|
-
import { PerformanceServiceError } from "@/models/performanceServiceError.enum";
|
|
313
|
-
|
|
314
|
-
if (conflict) {
|
|
315
|
-
throw new Error(PerformanceServiceError.DuplicateTime);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// actions/performance.action.ts
|
|
319
|
-
import { PerformanceServiceError } from "@/models/performanceServiceError.enum";
|
|
320
|
-
|
|
321
|
-
catch (error) {
|
|
322
|
-
if (error instanceof Error) {
|
|
323
|
-
switch (error.message) {
|
|
324
|
-
case PerformanceServiceError.DuplicateTime:
|
|
325
|
-
return { success: false, error: 'A performance already exists at this time', data: null };
|
|
326
|
-
case PerformanceServiceError.NotFound:
|
|
327
|
-
return { success: false, error: 'Performance not found', data: null };
|
|
328
|
-
case PerformanceServiceError.NotOwned:
|
|
329
|
-
return { success: false, error: 'You do not have permission to modify this performance', data: null };
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
return { success: false, error: 'Failed to update performance', data: null };
|
|
333
|
-
}
|
|
334
|
-
\`\`\`
|
|
199
|
+
content += `
|
|
200
|
+
### Layered Architecture
|
|
201
|
+
|
|
202
|
+
The application follows a strict 4-layer architecture:
|
|
203
|
+
|
|
204
|
+
\`\`\`
|
|
205
|
+
UI Components (app/, components/)
|
|
206
|
+
↓
|
|
207
|
+
Server Actions (actions/*.actions.ts)
|
|
208
|
+
↓
|
|
209
|
+
Services (services/*.service.ts)
|
|
210
|
+
↓
|
|
211
|
+
DAOs (dao/*.dao.ts)
|
|
212
|
+
↓
|
|
213
|
+
Database (Drizzle ORM)
|
|
214
|
+
\`\`\`
|
|
215
|
+
|
|
216
|
+
**Layer responsibilities:**
|
|
217
|
+
|
|
218
|
+
- **Actions** - Handle form submissions, validate with Zod, check auth, call services, revalidate cache
|
|
219
|
+
- **Services** - Orchestrate business logic, coordinate multiple DAOs
|
|
220
|
+
- **DAOs** - Encapsulate all database queries using Drizzle ORM
|
|
221
|
+
- **Mappers** - Transform between DTOs, service requests, and view models
|
|
222
|
+
|
|
223
|
+
### Model File Naming Conventions
|
|
224
|
+
|
|
225
|
+
Files in \`models/\` follow strict naming:
|
|
226
|
+
|
|
227
|
+
| Pattern | Purpose | Example |
|
|
228
|
+
|---------|---------|---------|
|
|
229
|
+
| \`*.dto.ts\` | Database types from Drizzle | \`UserDto\`, \`UserInsertDto\` |
|
|
230
|
+
| \`*.view.ts\` | View models for UI | \`UserView\` |
|
|
231
|
+
| \`*.schema.ts\` | Zod schemas + service types | \`UserCreateFormSchema\`, \`UserCreateServiceRequest\` |
|
|
232
|
+
| \`*.state.ts\` | Action state objects | \`UserCreateState\` |
|
|
233
|
+
| \`*ServiceError.enum.ts\` | Service error enums | \`UserServiceError\` |
|
|
234
|
+
|
|
235
|
+
### Action File Organization
|
|
236
|
+
|
|
237
|
+
One action file per domain entity: \`actions/{entity}.action.ts\`. Do NOT split by operation type.
|
|
238
|
+
|
|
239
|
+
\`\`\`
|
|
240
|
+
actions/
|
|
241
|
+
project.action.ts # createProject, updateProject, deleteProject, searchProjects
|
|
242
|
+
task.action.ts # createTask, updateTask, deleteTask, searchTasks
|
|
243
|
+
comment.action.ts # createComment, deleteComment
|
|
244
|
+
settings.action.ts # updateSettings
|
|
245
|
+
\`\`\`
|
|
246
|
+
|
|
247
|
+
Do NOT create separate files like \`project.create.action.ts\` or \`task.search.action.ts\`.
|
|
248
|
+
|
|
249
|
+
### Server Action Pattern
|
|
250
|
+
|
|
251
|
+
\`\`\`typescript
|
|
252
|
+
'use server';
|
|
253
|
+
|
|
254
|
+
export async function createEntity(
|
|
255
|
+
state: EntityCreateState,
|
|
256
|
+
formData: FormData
|
|
257
|
+
): Promise<EntityCreateState> {
|
|
258
|
+
const user = await currentUser();
|
|
259
|
+
if (!user) {
|
|
260
|
+
return { success: false, error: 'Not authenticated', data: null };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const rawData = Object.fromEntries(formData);
|
|
264
|
+
const validated = EntityCreateSchema.safeParse(rawData);
|
|
265
|
+
|
|
266
|
+
if (!validated.success) {
|
|
267
|
+
return { success: false, error: z.prettifyError(validated.error), data: null };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const result = await entityService.createEntity(validated.data);
|
|
272
|
+
revalidatePath('/entities');
|
|
273
|
+
return { success: true, error: null, data: result };
|
|
274
|
+
} catch (error) {
|
|
275
|
+
return { success: false, error: 'Failed to create entity', data: null };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
\`\`\`
|
|
279
|
+
|
|
280
|
+
### DAO Pattern
|
|
281
|
+
|
|
282
|
+
\`\`\`typescript
|
|
283
|
+
export class EntityDAO {
|
|
284
|
+
async create(dto: EntityInsertDto): Promise<EntityDto | undefined> {
|
|
285
|
+
const [created] = await db.insert(entities).values(dto).returning();
|
|
286
|
+
return created;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async getById(id: string): Promise<EntityDto | undefined> {
|
|
290
|
+
return await db.query.entities.findFirst({
|
|
291
|
+
where: eq(entities.id, id),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export const entityDAO = new EntityDAO();
|
|
297
|
+
\`\`\`
|
|
298
|
+
|
|
299
|
+
### Service Error Enums
|
|
300
|
+
|
|
301
|
+
Each service class has a corresponding error enum in \`models/{serviceName}ServiceError.enum.ts\`. Services throw errors using enum values, actions catch and translate to user-friendly messages.
|
|
302
|
+
|
|
303
|
+
\`\`\`typescript
|
|
304
|
+
// models/performanceServiceError.enum.ts
|
|
305
|
+
export enum PerformanceServiceError {
|
|
306
|
+
NotFound = "PERFORMANCE_NOT_FOUND",
|
|
307
|
+
NotOwned = "PERFORMANCE_NOT_OWNED",
|
|
308
|
+
DuplicateTime = "PERFORMANCE_DUPLICATE_TIME",
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// services/performance.service.ts
|
|
312
|
+
import { PerformanceServiceError } from "@/models/performanceServiceError.enum";
|
|
313
|
+
|
|
314
|
+
if (conflict) {
|
|
315
|
+
throw new Error(PerformanceServiceError.DuplicateTime);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// actions/performance.action.ts
|
|
319
|
+
import { PerformanceServiceError } from "@/models/performanceServiceError.enum";
|
|
320
|
+
|
|
321
|
+
catch (error) {
|
|
322
|
+
if (error instanceof Error) {
|
|
323
|
+
switch (error.message) {
|
|
324
|
+
case PerformanceServiceError.DuplicateTime:
|
|
325
|
+
return { success: false, error: 'A performance already exists at this time', data: null };
|
|
326
|
+
case PerformanceServiceError.NotFound:
|
|
327
|
+
return { success: false, error: 'Performance not found', data: null };
|
|
328
|
+
case PerformanceServiceError.NotOwned:
|
|
329
|
+
return { success: false, error: 'You do not have permission to modify this performance', data: null };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return { success: false, error: 'Failed to update performance', data: null };
|
|
333
|
+
}
|
|
334
|
+
\`\`\`
|
|
335
|
+
|
|
336
|
+
### No Rogue Types
|
|
337
|
+
|
|
338
|
+
Types must come from Zod schemas (\`z.infer<typeof Schema>\`) or \`models/\` files. Do NOT define interfaces in component or action files.
|
|
339
|
+
|
|
340
|
+
\`\`\`typescript
|
|
341
|
+
// ❌ Wrong - interface defined in component
|
|
342
|
+
interface TodoItem {
|
|
343
|
+
id: string;
|
|
344
|
+
title: string;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ✅ Correct - import from schema or models
|
|
348
|
+
import { type TodoCreatePayload } from "@/models/todoCreate.schema";
|
|
349
|
+
\`\`\`
|
|
350
|
+
|
|
351
|
+
### Service Request/Result Patterns
|
|
352
|
+
|
|
353
|
+
**ServiceRequest** - Derive from form schema payload, extend with context:
|
|
354
|
+
|
|
355
|
+
\`\`\`typescript
|
|
356
|
+
// ✅ Correct - derive from schema, extend with additional fields
|
|
357
|
+
export type TodoCreateServiceRequest = TodoCreateFormPayload & { userId: string };
|
|
358
|
+
|
|
359
|
+
// ✅ Correct - direct assignment when no additions needed
|
|
360
|
+
export type TodoUpdateServiceRequest = TodoUpdateFormPayload;
|
|
361
|
+
|
|
362
|
+
// ❌ Wrong - manually retyping all fields
|
|
363
|
+
export type TodoCreateServiceRequest = {
|
|
364
|
+
title: string;
|
|
365
|
+
description?: string;
|
|
366
|
+
userId: string;
|
|
367
|
+
};
|
|
368
|
+
\`\`\`
|
|
369
|
+
|
|
370
|
+
**ServiceResult** - Only for compound returns (2+ fields). Do NOT wrap single values:
|
|
371
|
+
|
|
372
|
+
\`\`\`typescript
|
|
373
|
+
// ❌ Wrong - overkill wrapper for single value
|
|
374
|
+
export type TodoCreateServiceResult = { todoId: string };
|
|
375
|
+
|
|
376
|
+
// ✅ Correct - return directly
|
|
377
|
+
async createTodo(request): Promise<string> {
|
|
378
|
+
return created.id;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ✅ Correct - compound data warrants a type
|
|
382
|
+
export type SearchServiceResult = {
|
|
383
|
+
items: ItemDto[];
|
|
384
|
+
totalCount: number;
|
|
385
|
+
};
|
|
386
|
+
\`\`\`
|
|
387
|
+
|
|
388
|
+
**Simple gets** - No ServiceRequest/ServiceResult. Let the signature be the contract:
|
|
389
|
+
|
|
390
|
+
\`\`\`typescript
|
|
391
|
+
findById(id: string): Promise<TodoDto | null>
|
|
392
|
+
findByUserId(userId: string): Promise<TodoDto[]>
|
|
393
|
+
\`\`\`
|
|
394
|
+
|
|
395
|
+
### Action Success/Error Handling
|
|
396
|
+
|
|
397
|
+
Do NOT use \`useEffect\` to react to \`useActionState\` results. Wrap the action in a \`useCallback\` that handles side effects inline.
|
|
398
|
+
|
|
399
|
+
\`\`\`typescript
|
|
400
|
+
// ❌ Wrong - useEffect watching action state
|
|
401
|
+
useEffect(() => {
|
|
402
|
+
if (createState.success) {
|
|
403
|
+
toast.success("Created");
|
|
404
|
+
setDialogOpen(false);
|
|
405
|
+
}
|
|
406
|
+
}, [createState]);
|
|
407
|
+
|
|
408
|
+
// ✅ Correct - wrapper handles side effects inline
|
|
409
|
+
const handleCreateAction = useCallback(async (prevState: State, formData: FormData) => {
|
|
410
|
+
const result = await createThing(prevState, formData);
|
|
411
|
+
if (result.success) {
|
|
412
|
+
toast.success("Created");
|
|
413
|
+
setDialogOpen(false);
|
|
414
|
+
}
|
|
415
|
+
if (result.error) {
|
|
416
|
+
toast.error(result.error);
|
|
417
|
+
}
|
|
418
|
+
return result;
|
|
419
|
+
}, []);
|
|
420
|
+
|
|
421
|
+
const [createState, createFormAction, isCreatePending] = useActionState(
|
|
422
|
+
handleCreateAction,
|
|
423
|
+
initialState
|
|
424
|
+
);
|
|
425
|
+
\`\`\`
|
|
426
|
+
|
|
427
|
+
### Error Logging in Actions
|
|
428
|
+
|
|
429
|
+
Use format \`actions/{filename}/{functionName}:\` for console.error calls:
|
|
430
|
+
|
|
431
|
+
\`\`\`typescript
|
|
432
|
+
console.error("actions/todo.actions.ts/createTodo:", error);
|
|
433
|
+
\`\`\`
|
|
335
434
|
`;
|
|
336
435
|
}
|
|
337
|
-
content += `
|
|
338
|
-
## Client State Management (Zustand)
|
|
339
|
-
|
|
340
|
-
For complex multi-step forms or flows, use Zustand stores.
|
|
341
|
-
|
|
342
|
-
**Store location**: \`lib/stores/*.store.ts\`
|
|
343
|
-
|
|
344
|
-
### Store Pattern
|
|
345
|
-
|
|
346
|
-
\`\`\`typescript
|
|
347
|
-
import { createStore, useStore } from 'zustand';
|
|
348
|
-
|
|
349
|
-
interface FormState {
|
|
350
|
-
title: string;
|
|
351
|
-
description: string;
|
|
352
|
-
setTitle: (title: string) => void;
|
|
353
|
-
setDescription: (description: string) => void;
|
|
354
|
-
reset: () => void;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const initialState = {
|
|
358
|
-
title: '',
|
|
359
|
-
description: '',
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
export const formStore = createStore<FormState>()((set) => ({
|
|
363
|
-
...initialState,
|
|
364
|
-
setTitle: (title) => set({ title }),
|
|
365
|
-
setDescription: (description) => set({ description }),
|
|
366
|
-
reset: () => set(initialState),
|
|
367
|
-
}));
|
|
368
|
-
|
|
369
|
-
export const useFormStore = <T>(selector: (state: FormState) => T): T => {
|
|
370
|
-
return useStore(formStore, selector);
|
|
371
|
-
};
|
|
372
|
-
\`\`\`
|
|
373
|
-
|
|
374
|
-
### Usage in Components
|
|
375
|
-
|
|
376
|
-
\`\`\`tsx
|
|
377
|
-
'use client';
|
|
378
|
-
|
|
379
|
-
import { Input } from '@/components/ui/input';
|
|
380
|
-
import { Label } from '@/components/ui/label';
|
|
381
|
-
|
|
382
|
-
function TitleStep() {
|
|
383
|
-
const title = useFormStore((state) => state.title);
|
|
384
|
-
const setTitle = useFormStore((state) => state.setTitle);
|
|
385
|
-
|
|
386
|
-
return (
|
|
387
|
-
<div className="space-y-2">
|
|
388
|
-
<Label htmlFor="title">Title</Label>
|
|
389
|
-
<Input
|
|
390
|
-
id="title"
|
|
391
|
-
value={title}
|
|
392
|
-
onChange={(e) => setTitle(e.target.value)}
|
|
393
|
-
placeholder="Enter a title"
|
|
394
|
-
/>
|
|
395
|
-
</div>
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
\`\`\`
|
|
399
|
-
|
|
400
|
-
## Code Style
|
|
401
|
-
|
|
402
|
-
### No Comments
|
|
403
|
-
|
|
404
|
-
Do not add comments to the codebase. Code should be self-documenting through:
|
|
405
|
-
- Clear, descriptive variable and function names
|
|
406
|
-
- Proper TypeScript types
|
|
407
|
-
- Logical code structure
|
|
408
|
-
- Small, focused functions
|
|
409
|
-
|
|
410
|
-
### Closure Variable Naming
|
|
411
|
-
|
|
412
|
-
Always use verbose singular names in closures (\`.map()\`, \`.filter()\`, etc.):
|
|
413
|
-
|
|
414
|
-
\`\`\`typescript
|
|
415
|
-
// ✅ Correct
|
|
416
|
-
users.map((user) => user.email)
|
|
417
|
-
items.filter((item) => item.isActive)
|
|
418
|
-
|
|
419
|
-
// ❌ Wrong
|
|
420
|
-
users.map((u) => u.email)
|
|
421
|
-
items.filter((i) => i.isActive)
|
|
422
|
-
\`\`\`
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
\`\`\`
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
436
|
+
content += `
|
|
437
|
+
## Client State Management (Zustand)
|
|
438
|
+
|
|
439
|
+
For complex multi-step forms or flows, use Zustand stores.
|
|
440
|
+
|
|
441
|
+
**Store location**: \`lib/stores/*.store.ts\`
|
|
442
|
+
|
|
443
|
+
### Store Pattern
|
|
444
|
+
|
|
445
|
+
\`\`\`typescript
|
|
446
|
+
import { createStore, useStore } from 'zustand';
|
|
447
|
+
|
|
448
|
+
interface FormState {
|
|
449
|
+
title: string;
|
|
450
|
+
description: string;
|
|
451
|
+
setTitle: (title: string) => void;
|
|
452
|
+
setDescription: (description: string) => void;
|
|
453
|
+
reset: () => void;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const initialState = {
|
|
457
|
+
title: '',
|
|
458
|
+
description: '',
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
export const formStore = createStore<FormState>()((set) => ({
|
|
462
|
+
...initialState,
|
|
463
|
+
setTitle: (title) => set({ title }),
|
|
464
|
+
setDescription: (description) => set({ description }),
|
|
465
|
+
reset: () => set(initialState),
|
|
466
|
+
}));
|
|
467
|
+
|
|
468
|
+
export const useFormStore = <T>(selector: (state: FormState) => T): T => {
|
|
469
|
+
return useStore(formStore, selector);
|
|
470
|
+
};
|
|
471
|
+
\`\`\`
|
|
472
|
+
|
|
473
|
+
### Usage in Components
|
|
474
|
+
|
|
475
|
+
\`\`\`tsx
|
|
476
|
+
'use client';
|
|
477
|
+
|
|
478
|
+
import { Input } from '@/components/ui/input';
|
|
479
|
+
import { Label } from '@/components/ui/label';
|
|
480
|
+
|
|
481
|
+
function TitleStep() {
|
|
482
|
+
const title = useFormStore((state) => state.title);
|
|
483
|
+
const setTitle = useFormStore((state) => state.setTitle);
|
|
484
|
+
|
|
485
|
+
return (
|
|
486
|
+
<div className="space-y-2">
|
|
487
|
+
<Label htmlFor="title">Title</Label>
|
|
488
|
+
<Input
|
|
489
|
+
id="title"
|
|
490
|
+
value={title}
|
|
491
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
492
|
+
placeholder="Enter a title"
|
|
493
|
+
/>
|
|
494
|
+
</div>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
\`\`\`
|
|
498
|
+
|
|
499
|
+
## Code Style
|
|
500
|
+
|
|
501
|
+
### No Comments
|
|
502
|
+
|
|
503
|
+
Do not add comments to the codebase. Code should be self-documenting through:
|
|
504
|
+
- Clear, descriptive variable and function names
|
|
505
|
+
- Proper TypeScript types
|
|
506
|
+
- Logical code structure
|
|
507
|
+
- Small, focused functions
|
|
508
|
+
|
|
509
|
+
### Closure Variable Naming
|
|
510
|
+
|
|
511
|
+
Always use verbose singular names in closures (\`.map()\`, \`.filter()\`, etc.):
|
|
512
|
+
|
|
513
|
+
\`\`\`typescript
|
|
514
|
+
// ✅ Correct
|
|
515
|
+
users.map((user) => user.email)
|
|
516
|
+
items.filter((item) => item.isActive)
|
|
517
|
+
|
|
518
|
+
// ❌ Wrong
|
|
519
|
+
users.map((u) => u.email)
|
|
520
|
+
items.filter((i) => i.isActive)
|
|
521
|
+
\`\`\`
|
|
522
|
+
|
|
523
|
+
### Server vs Client Components
|
|
524
|
+
|
|
525
|
+
Page files (\`page.tsx\`) must be server components - do NOT add \`"use client"\` to pages. Extract interactive parts into separate client component files.
|
|
526
|
+
|
|
527
|
+
\`\`\`
|
|
528
|
+
app/settings/
|
|
529
|
+
page.tsx # Server component (NO "use client")
|
|
530
|
+
components/
|
|
531
|
+
settings-form.tsx # Client component ("use client")
|
|
532
|
+
app/dashboard/components/ # Shared components for sibling pages
|
|
533
|
+
\`\`\`
|
|
534
|
+
|
|
535
|
+
### No Module-Scope Mutable State
|
|
536
|
+
|
|
537
|
+
Do NOT use \`let\` variables at module scope. Modules are cached and state leaks between SSR requests.
|
|
538
|
+
|
|
539
|
+
\`\`\`typescript
|
|
540
|
+
// ❌ Wrong - module-scope mutable state
|
|
541
|
+
let nextId = 1;
|
|
542
|
+
|
|
543
|
+
// ✅ Correct - generate inline
|
|
544
|
+
crypto.randomUUID();
|
|
545
|
+
\`\`\`
|
|
546
|
+
|
|
547
|
+
### User-Friendly Errors
|
|
548
|
+
|
|
549
|
+
All error messages shown to users must be actionable and understandable. Do NOT expose technical errors. Log technical details to \`console.error()\`, show friendly messages to users.
|
|
550
|
+
|
|
551
|
+
\`\`\`typescript
|
|
552
|
+
// ❌ Wrong - technical error
|
|
553
|
+
return { error: "User ID is missing from request" };
|
|
554
|
+
|
|
555
|
+
// ✅ Correct - user-friendly
|
|
556
|
+
return { error: "Something went wrong. Please try again." };
|
|
557
|
+
\`\`\`
|
|
558
|
+
|
|
559
|
+
### Date Manipulation with Luxon
|
|
560
|
+
|
|
561
|
+
Use Luxon for all date manipulation. Do NOT manually construct ISO date strings.
|
|
562
|
+
|
|
563
|
+
\`\`\`typescript
|
|
564
|
+
// ❌ Wrong - manual string construction
|
|
565
|
+
const dateFrom = \`\${year}-\${String(month).padStart(2, "0")}-01\`;
|
|
566
|
+
|
|
567
|
+
// ✅ Correct - use Luxon
|
|
568
|
+
import { DateTime } from "luxon";
|
|
569
|
+
const startOfMonth = DateTime.local(year, month, 1);
|
|
570
|
+
const dateFrom = startOfMonth.toISODate()!;
|
|
571
|
+
const dateTo = startOfMonth.plus({ months: 1 }).toISODate()!;
|
|
572
|
+
\`\`\`
|
|
573
|
+
|
|
574
|
+
### Zod Patterns
|
|
575
|
+
|
|
576
|
+
Use \`z.iso.date()\` for ISO date strings (NOT the deprecated \`z.string().date()\`).
|
|
577
|
+
|
|
578
|
+
Use \`z.enum(TheEnum)\` for enum validation (NOT \`z.nativeEnum\` or \`Object.values\`).
|
|
579
|
+
|
|
580
|
+
For JSON array fields in form schemas, use \`z.preprocess\`:
|
|
581
|
+
|
|
582
|
+
\`\`\`typescript
|
|
583
|
+
// ✅ Correct - preprocess handles JSON, Zod validates the array
|
|
584
|
+
export const MyFormSchema = z.object({
|
|
585
|
+
items: z.preprocess(
|
|
586
|
+
(val) => (typeof val === "string" ? JSON.parse(val) : val),
|
|
587
|
+
ItemSchema.array()
|
|
588
|
+
),
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// ❌ Wrong - transform with manual error handling
|
|
592
|
+
items: z.string().transform((val, ctx) => { ... ctx.addIssue(...) })
|
|
593
|
+
\`\`\`
|
|
594
|
+
|
|
595
|
+
## Utility Functions
|
|
596
|
+
|
|
597
|
+
Import from \`@/lib/utils\`:
|
|
598
|
+
|
|
599
|
+
\`\`\`typescript
|
|
600
|
+
import { cn, formatDate, formatRelative, debounce } from '@/lib/utils';
|
|
601
|
+
|
|
602
|
+
// Class name merging (shadcn/ui)
|
|
603
|
+
cn('text-sm', isActive && 'text-blue-500')
|
|
604
|
+
|
|
605
|
+
// Date formatting with Luxon
|
|
606
|
+
formatDate(new Date()) // "Jan 15, 2024"
|
|
607
|
+
formatDate('2024-01-15', 'yyyy-MM-dd') // "2024-01-15"
|
|
608
|
+
formatRelative(new Date()) // "2 hours ago"
|
|
609
|
+
|
|
610
|
+
// Debounce function calls
|
|
611
|
+
const debouncedSearch = debounce((query: string) => {
|
|
612
|
+
// search logic
|
|
613
|
+
}, 300);
|
|
614
|
+
\`\`\`
|
|
615
|
+
|
|
616
|
+
## Environment Variables
|
|
617
|
+
|
|
618
|
+
Copy \`.env.example\` to \`.env\` and fill in your API keys.
|
|
619
|
+
|
|
620
|
+
**Important:** Import environment variables from \`@/lib/config\`:
|
|
621
|
+
|
|
622
|
+
\`\`\`typescript
|
|
623
|
+
// ✅ Good
|
|
624
|
+
import { STRIPE_SECRET_KEY } from '@/lib/config';
|
|
625
|
+
|
|
626
|
+
// ❌ Avoid
|
|
627
|
+
const key = process.env.STRIPE_SECRET_KEY;
|
|
628
|
+
\`\`\`
|
|
458
629
|
`;
|
|
459
630
|
if (hasClerk) {
|
|
460
|
-
content += `
|
|
461
|
-
## Authentication
|
|
462
|
-
|
|
463
|
-
**Provider:** Clerk
|
|
464
|
-
|
|
465
|
-
- Route protection via \`proxy.ts\` (Next.js 16+)
|
|
466
|
-
- Use \`currentUser()\` in Server Components and Actions for auth checks
|
|
467
|
-
- Client-side: Use Clerk hooks (\`useUser()\`, \`useAuth()\`, \`<SignedIn>\`, \`<SignedOut>\`)
|
|
468
|
-
|
|
469
|
-
\`\`\`typescript
|
|
470
|
-
// Server-side auth check
|
|
471
|
-
const user = await currentUser();
|
|
472
|
-
if (!user) {
|
|
473
|
-
return redirect('/');
|
|
474
|
-
}
|
|
475
|
-
\`\`\`
|
|
631
|
+
content += `
|
|
632
|
+
## Authentication
|
|
633
|
+
|
|
634
|
+
**Provider:** Clerk
|
|
635
|
+
|
|
636
|
+
- Route protection via \`proxy.ts\` (Next.js 16+)
|
|
637
|
+
- Use \`currentUser()\` in Server Components and Actions for auth checks
|
|
638
|
+
- Client-side: Use Clerk hooks (\`useUser()\`, \`useAuth()\`, \`<SignedIn>\`, \`<SignedOut>\`)
|
|
639
|
+
|
|
640
|
+
\`\`\`typescript
|
|
641
|
+
// Server-side auth check
|
|
642
|
+
const user = await currentUser();
|
|
643
|
+
if (!user) {
|
|
644
|
+
return redirect('/');
|
|
645
|
+
}
|
|
646
|
+
\`\`\`
|
|
476
647
|
`;
|
|
477
648
|
}
|
|
478
649
|
await fs.writeFile(path.join(projectPath, 'CLAUDE.md'), content);
|