@codebakers/cli 1.3.1 → 1.4.1
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/dist/commands/generate.d.ts +9 -0
- package/dist/commands/generate.js +653 -0
- package/dist/index.js +49 -2
- package/package.json +1 -1
- package/src/commands/generate.ts +702 -0
- package/src/index.ts +52 -2
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { createInterface } from 'readline';
|
|
6
|
+
|
|
7
|
+
async function prompt(question: string): Promise<string> {
|
|
8
|
+
const rl = createInterface({
|
|
9
|
+
input: process.stdin,
|
|
10
|
+
output: process.stdout,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
rl.question(question, (answer) => {
|
|
15
|
+
rl.close();
|
|
16
|
+
resolve(answer.trim());
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Convert to PascalCase
|
|
22
|
+
function toPascalCase(str: string): string {
|
|
23
|
+
return str
|
|
24
|
+
.split(/[-_\s]+/)
|
|
25
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
26
|
+
.join('');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Convert to kebab-case
|
|
30
|
+
function toKebabCase(str: string): string {
|
|
31
|
+
return str
|
|
32
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
33
|
+
.replace(/[\s_]+/g, '-')
|
|
34
|
+
.toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Convert to camelCase
|
|
38
|
+
function toCamelCase(str: string): string {
|
|
39
|
+
const pascal = toPascalCase(str);
|
|
40
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Component template
|
|
44
|
+
function componentTemplate(name: string, hasProps: boolean): string {
|
|
45
|
+
const pascalName = toPascalCase(name);
|
|
46
|
+
|
|
47
|
+
if (hasProps) {
|
|
48
|
+
return `import { cn } from '@/lib/utils';
|
|
49
|
+
|
|
50
|
+
interface ${pascalName}Props {
|
|
51
|
+
className?: string;
|
|
52
|
+
children?: React.ReactNode;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function ${pascalName}({ className, children }: ${pascalName}Props) {
|
|
56
|
+
return (
|
|
57
|
+
<div className={cn('', className)}>
|
|
58
|
+
{children}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return `import { cn } from '@/lib/utils';
|
|
66
|
+
|
|
67
|
+
export function ${pascalName}() {
|
|
68
|
+
return (
|
|
69
|
+
<div>
|
|
70
|
+
{/* ${pascalName} content */}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// API route template
|
|
78
|
+
function apiRouteTemplate(name: string, methods: string[]): string {
|
|
79
|
+
const lines: string[] = [
|
|
80
|
+
`import { NextRequest, NextResponse } from 'next/server';`,
|
|
81
|
+
`import { z } from 'zod';`,
|
|
82
|
+
`import { db } from '@/db';`,
|
|
83
|
+
``,
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
if (methods.includes('GET')) {
|
|
87
|
+
lines.push(`export async function GET(request: NextRequest) {
|
|
88
|
+
try {
|
|
89
|
+
// TODO: Implement GET logic
|
|
90
|
+
return NextResponse.json({ message: 'Success' });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('GET /${name} error:', error);
|
|
93
|
+
return NextResponse.json(
|
|
94
|
+
{ error: 'Internal server error' },
|
|
95
|
+
{ status: 500 }
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (methods.includes('POST')) {
|
|
103
|
+
const schemaName = `Create${toPascalCase(name)}Schema`;
|
|
104
|
+
lines.push(`const ${schemaName} = z.object({
|
|
105
|
+
// TODO: Define your schema
|
|
106
|
+
name: z.string().min(1),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export async function POST(request: NextRequest) {
|
|
110
|
+
try {
|
|
111
|
+
const body = await request.json();
|
|
112
|
+
const validated = ${schemaName}.parse(body);
|
|
113
|
+
|
|
114
|
+
// TODO: Implement POST logic
|
|
115
|
+
return NextResponse.json({ message: 'Created', data: validated });
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error instanceof z.ZodError) {
|
|
118
|
+
return NextResponse.json(
|
|
119
|
+
{ error: 'Validation failed', details: error.errors },
|
|
120
|
+
{ status: 400 }
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
console.error('POST /${name} error:', error);
|
|
124
|
+
return NextResponse.json(
|
|
125
|
+
{ error: 'Internal server error' },
|
|
126
|
+
{ status: 500 }
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (methods.includes('PUT')) {
|
|
134
|
+
const schemaName = `Update${toPascalCase(name)}Schema`;
|
|
135
|
+
lines.push(`const ${schemaName} = z.object({
|
|
136
|
+
// TODO: Define your update schema
|
|
137
|
+
id: z.string().uuid(),
|
|
138
|
+
name: z.string().min(1).optional(),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
export async function PUT(request: NextRequest) {
|
|
142
|
+
try {
|
|
143
|
+
const body = await request.json();
|
|
144
|
+
const validated = ${schemaName}.parse(body);
|
|
145
|
+
|
|
146
|
+
// TODO: Implement PUT logic
|
|
147
|
+
return NextResponse.json({ message: 'Updated', data: validated });
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error instanceof z.ZodError) {
|
|
150
|
+
return NextResponse.json(
|
|
151
|
+
{ error: 'Validation failed', details: error.errors },
|
|
152
|
+
{ status: 400 }
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
console.error('PUT /${name} error:', error);
|
|
156
|
+
return NextResponse.json(
|
|
157
|
+
{ error: 'Internal server error' },
|
|
158
|
+
{ status: 500 }
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (methods.includes('DELETE')) {
|
|
166
|
+
lines.push(`export async function DELETE(request: NextRequest) {
|
|
167
|
+
try {
|
|
168
|
+
const { searchParams } = new URL(request.url);
|
|
169
|
+
const id = searchParams.get('id');
|
|
170
|
+
|
|
171
|
+
if (!id) {
|
|
172
|
+
return NextResponse.json(
|
|
173
|
+
{ error: 'ID is required' },
|
|
174
|
+
{ status: 400 }
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// TODO: Implement DELETE logic
|
|
179
|
+
return NextResponse.json({ message: 'Deleted' });
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error('DELETE /${name} error:', error);
|
|
182
|
+
return NextResponse.json(
|
|
183
|
+
{ error: 'Internal server error' },
|
|
184
|
+
{ status: 500 }
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return lines.join('\n');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Service template
|
|
195
|
+
function serviceTemplate(name: string): string {
|
|
196
|
+
const pascalName = toPascalCase(name);
|
|
197
|
+
const camelName = toCamelCase(name);
|
|
198
|
+
|
|
199
|
+
return `import { db } from '@/db';
|
|
200
|
+
import { z } from 'zod';
|
|
201
|
+
|
|
202
|
+
// Validation schemas
|
|
203
|
+
export const Create${pascalName}Schema = z.object({
|
|
204
|
+
// TODO: Define your schema
|
|
205
|
+
name: z.string().min(1),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
export const Update${pascalName}Schema = Create${pascalName}Schema.partial();
|
|
209
|
+
|
|
210
|
+
export type Create${pascalName}Input = z.infer<typeof Create${pascalName}Schema>;
|
|
211
|
+
export type Update${pascalName}Input = z.infer<typeof Update${pascalName}Schema>;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* ${pascalName} Service
|
|
215
|
+
* Handles all ${camelName}-related business logic
|
|
216
|
+
*/
|
|
217
|
+
export const ${camelName}Service = {
|
|
218
|
+
/**
|
|
219
|
+
* Get all ${camelName}s
|
|
220
|
+
*/
|
|
221
|
+
async getAll() {
|
|
222
|
+
// TODO: Implement
|
|
223
|
+
return [];
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get a single ${camelName} by ID
|
|
228
|
+
*/
|
|
229
|
+
async getById(id: string) {
|
|
230
|
+
// TODO: Implement
|
|
231
|
+
return null;
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Create a new ${camelName}
|
|
236
|
+
*/
|
|
237
|
+
async create(input: Create${pascalName}Input) {
|
|
238
|
+
const validated = Create${pascalName}Schema.parse(input);
|
|
239
|
+
// TODO: Implement
|
|
240
|
+
return validated;
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Update an existing ${camelName}
|
|
245
|
+
*/
|
|
246
|
+
async update(id: string, input: Update${pascalName}Input) {
|
|
247
|
+
const validated = Update${pascalName}Schema.parse(input);
|
|
248
|
+
// TODO: Implement
|
|
249
|
+
return { id, ...validated };
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Delete a ${camelName}
|
|
254
|
+
*/
|
|
255
|
+
async delete(id: string) {
|
|
256
|
+
// TODO: Implement
|
|
257
|
+
return true;
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Hook template
|
|
264
|
+
function hookTemplate(name: string): string {
|
|
265
|
+
const hookName = name.startsWith('use') ? name : `use${toPascalCase(name)}`;
|
|
266
|
+
const pascalName = toPascalCase(name.replace(/^use/i, ''));
|
|
267
|
+
|
|
268
|
+
return `'use client';
|
|
269
|
+
|
|
270
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
271
|
+
|
|
272
|
+
interface ${pascalName}State {
|
|
273
|
+
data: unknown | null;
|
|
274
|
+
isLoading: boolean;
|
|
275
|
+
error: Error | null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
interface ${hookName}Options {
|
|
279
|
+
// TODO: Add options if needed
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function ${hookName}(options?: ${hookName}Options) {
|
|
283
|
+
const [state, setState] = useState<${pascalName}State>({
|
|
284
|
+
data: null,
|
|
285
|
+
isLoading: false,
|
|
286
|
+
error: null,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const fetch${pascalName} = useCallback(async () => {
|
|
290
|
+
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
// TODO: Implement fetch logic
|
|
294
|
+
const data = null;
|
|
295
|
+
setState({ data, isLoading: false, error: null });
|
|
296
|
+
} catch (error) {
|
|
297
|
+
setState(prev => ({
|
|
298
|
+
...prev,
|
|
299
|
+
isLoading: false,
|
|
300
|
+
error: error instanceof Error ? error : new Error('Unknown error'),
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
}, []);
|
|
304
|
+
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
fetch${pascalName}();
|
|
307
|
+
}, [fetch${pascalName}]);
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
...state,
|
|
311
|
+
refetch: fetch${pascalName},
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Page template
|
|
318
|
+
function pageTemplate(name: string, isServer: boolean): string {
|
|
319
|
+
const pascalName = toPascalCase(name);
|
|
320
|
+
|
|
321
|
+
if (isServer) {
|
|
322
|
+
return `import { Suspense } from 'react';
|
|
323
|
+
|
|
324
|
+
export const metadata = {
|
|
325
|
+
title: '${pascalName}',
|
|
326
|
+
description: '${pascalName} page',
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
async function ${pascalName}Content() {
|
|
330
|
+
// TODO: Fetch data server-side
|
|
331
|
+
const data = null;
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
<div>
|
|
335
|
+
<h1 className="text-2xl font-bold">${pascalName}</h1>
|
|
336
|
+
{/* Content here */}
|
|
337
|
+
</div>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export default function ${pascalName}Page() {
|
|
342
|
+
return (
|
|
343
|
+
<main className="container mx-auto py-8">
|
|
344
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
345
|
+
<${pascalName}Content />
|
|
346
|
+
</Suspense>
|
|
347
|
+
</main>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return `'use client';
|
|
354
|
+
|
|
355
|
+
import { useState } from 'react';
|
|
356
|
+
|
|
357
|
+
export default function ${pascalName}Page() {
|
|
358
|
+
const [loading, setLoading] = useState(false);
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<main className="container mx-auto py-8">
|
|
362
|
+
<h1 className="text-2xl font-bold">${pascalName}</h1>
|
|
363
|
+
{/* Content here */}
|
|
364
|
+
</main>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Schema template for Drizzle
|
|
371
|
+
function schemaTemplate(name: string): string {
|
|
372
|
+
const tableName = toKebabCase(name).replace(/-/g, '_');
|
|
373
|
+
const pascalName = toPascalCase(name);
|
|
374
|
+
|
|
375
|
+
return `import { pgTable, text, timestamp, uuid, boolean } from 'drizzle-orm/pg-core';
|
|
376
|
+
|
|
377
|
+
export const ${tableName} = pgTable('${tableName}', {
|
|
378
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
379
|
+
// TODO: Add your columns
|
|
380
|
+
name: text('name').notNull(),
|
|
381
|
+
description: text('description'),
|
|
382
|
+
isActive: boolean('is_active').default(true),
|
|
383
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
384
|
+
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
export type ${pascalName} = typeof ${tableName}.$inferSelect;
|
|
388
|
+
export type New${pascalName} = typeof ${tableName}.$inferInsert;
|
|
389
|
+
`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Form template
|
|
393
|
+
function formTemplate(name: string): string {
|
|
394
|
+
const pascalName = toPascalCase(name);
|
|
395
|
+
const camelName = toCamelCase(name);
|
|
396
|
+
|
|
397
|
+
return `'use client';
|
|
398
|
+
|
|
399
|
+
import { useForm } from 'react-hook-form';
|
|
400
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
401
|
+
import { z } from 'zod';
|
|
402
|
+
|
|
403
|
+
const ${camelName}Schema = z.object({
|
|
404
|
+
// TODO: Define your form fields
|
|
405
|
+
name: z.string().min(1, 'Name is required'),
|
|
406
|
+
email: z.string().email('Invalid email'),
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
type ${pascalName}FormData = z.infer<typeof ${camelName}Schema>;
|
|
410
|
+
|
|
411
|
+
interface ${pascalName}FormProps {
|
|
412
|
+
onSubmit: (data: ${pascalName}FormData) => void | Promise<void>;
|
|
413
|
+
defaultValues?: Partial<${pascalName}FormData>;
|
|
414
|
+
isLoading?: boolean;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function ${pascalName}Form({ onSubmit, defaultValues, isLoading }: ${pascalName}FormProps) {
|
|
418
|
+
const {
|
|
419
|
+
register,
|
|
420
|
+
handleSubmit,
|
|
421
|
+
formState: { errors },
|
|
422
|
+
} = useForm<${pascalName}FormData>({
|
|
423
|
+
resolver: zodResolver(${camelName}Schema),
|
|
424
|
+
defaultValues,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
429
|
+
<div>
|
|
430
|
+
<label htmlFor="name" className="block text-sm font-medium">
|
|
431
|
+
Name
|
|
432
|
+
</label>
|
|
433
|
+
<input
|
|
434
|
+
{...register('name')}
|
|
435
|
+
type="text"
|
|
436
|
+
id="name"
|
|
437
|
+
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
|
|
438
|
+
disabled={isLoading}
|
|
439
|
+
/>
|
|
440
|
+
{errors.name && (
|
|
441
|
+
<p className="mt-1 text-sm text-red-600">{errors.name.message}</p>
|
|
442
|
+
)}
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
<div>
|
|
446
|
+
<label htmlFor="email" className="block text-sm font-medium">
|
|
447
|
+
Email
|
|
448
|
+
</label>
|
|
449
|
+
<input
|
|
450
|
+
{...register('email')}
|
|
451
|
+
type="email"
|
|
452
|
+
id="email"
|
|
453
|
+
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
|
|
454
|
+
disabled={isLoading}
|
|
455
|
+
/>
|
|
456
|
+
{errors.email && (
|
|
457
|
+
<p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
|
|
458
|
+
)}
|
|
459
|
+
</div>
|
|
460
|
+
|
|
461
|
+
<button
|
|
462
|
+
type="submit"
|
|
463
|
+
disabled={isLoading}
|
|
464
|
+
className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
|
|
465
|
+
>
|
|
466
|
+
{isLoading ? 'Submitting...' : 'Submit'}
|
|
467
|
+
</button>
|
|
468
|
+
</form>
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
interface GenerateOptions {
|
|
475
|
+
type?: string;
|
|
476
|
+
name?: string;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Generate code from templates
|
|
481
|
+
*/
|
|
482
|
+
export async function generate(options: GenerateOptions): Promise<void> {
|
|
483
|
+
console.log(chalk.blue(`
|
|
484
|
+
╔═══════════════════════════════════════════════════════════╗
|
|
485
|
+
║ ║
|
|
486
|
+
║ ${chalk.bold('CodeBakers Code Generator')} ║
|
|
487
|
+
║ ║
|
|
488
|
+
║ Generate production-ready code from templates ║
|
|
489
|
+
║ ║
|
|
490
|
+
╚═══════════════════════════════════════════════════════════╝
|
|
491
|
+
`));
|
|
492
|
+
|
|
493
|
+
const cwd = process.cwd();
|
|
494
|
+
|
|
495
|
+
// Check if we're in a project
|
|
496
|
+
const hasPackageJson = existsSync(join(cwd, 'package.json'));
|
|
497
|
+
if (!hasPackageJson) {
|
|
498
|
+
console.log(chalk.yellow(' No package.json found. Run this in a project directory.\n'));
|
|
499
|
+
console.log(chalk.gray(' Use `codebakers scaffold` to create a new project first.\n'));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Available generators
|
|
504
|
+
const generators = [
|
|
505
|
+
{ key: '1', name: 'component', desc: 'React component with TypeScript' },
|
|
506
|
+
{ key: '2', name: 'api', desc: 'Next.js API route with validation' },
|
|
507
|
+
{ key: '3', name: 'service', desc: 'Business logic service with CRUD' },
|
|
508
|
+
{ key: '4', name: 'hook', desc: 'React hook with state management' },
|
|
509
|
+
{ key: '5', name: 'page', desc: 'Next.js page (server or client)' },
|
|
510
|
+
{ key: '6', name: 'schema', desc: 'Drizzle database table schema' },
|
|
511
|
+
{ key: '7', name: 'form', desc: 'Form with React Hook Form + Zod' },
|
|
512
|
+
];
|
|
513
|
+
|
|
514
|
+
let type = options.type;
|
|
515
|
+
let name = options.name;
|
|
516
|
+
|
|
517
|
+
// If type not provided, ask
|
|
518
|
+
if (!type) {
|
|
519
|
+
console.log(chalk.white(' What would you like to generate?\n'));
|
|
520
|
+
for (const gen of generators) {
|
|
521
|
+
console.log(chalk.gray(` ${gen.key}. `) + chalk.cyan(gen.name) + chalk.gray(` - ${gen.desc}`));
|
|
522
|
+
}
|
|
523
|
+
console.log('');
|
|
524
|
+
|
|
525
|
+
let choice = '';
|
|
526
|
+
while (!['1', '2', '3', '4', '5', '6', '7'].includes(choice)) {
|
|
527
|
+
choice = await prompt(' Enter 1-7: ');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
type = generators.find(g => g.key === choice)?.name;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// If name not provided, ask
|
|
534
|
+
if (!name) {
|
|
535
|
+
name = await prompt(` ${toPascalCase(type!)} name: `);
|
|
536
|
+
if (!name) {
|
|
537
|
+
console.log(chalk.red('\n Name is required.\n'));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const spinner = ora(' Generating...').start();
|
|
543
|
+
|
|
544
|
+
try {
|
|
545
|
+
let filePath: string;
|
|
546
|
+
let content: string;
|
|
547
|
+
|
|
548
|
+
switch (type) {
|
|
549
|
+
case 'component': {
|
|
550
|
+
const hasProps = await prompt(' Include props interface? (Y/n): ');
|
|
551
|
+
content = componentTemplate(name, hasProps.toLowerCase() !== 'n');
|
|
552
|
+
const componentDir = join(cwd, 'src/components');
|
|
553
|
+
if (!existsSync(componentDir)) {
|
|
554
|
+
mkdirSync(componentDir, { recursive: true });
|
|
555
|
+
}
|
|
556
|
+
filePath = join(componentDir, `${toPascalCase(name)}.tsx`);
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
case 'api': {
|
|
561
|
+
console.log(chalk.gray('\n Select HTTP methods (comma-separated):'));
|
|
562
|
+
console.log(chalk.gray(' Examples: GET,POST or GET,POST,PUT,DELETE\n'));
|
|
563
|
+
const methodsInput = await prompt(' Methods (GET,POST): ') || 'GET,POST';
|
|
564
|
+
const methods = methodsInput.toUpperCase().split(',').map(m => m.trim());
|
|
565
|
+
content = apiRouteTemplate(name, methods);
|
|
566
|
+
const apiDir = join(cwd, 'src/app/api', toKebabCase(name));
|
|
567
|
+
if (!existsSync(apiDir)) {
|
|
568
|
+
mkdirSync(apiDir, { recursive: true });
|
|
569
|
+
}
|
|
570
|
+
filePath = join(apiDir, 'route.ts');
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
case 'service': {
|
|
575
|
+
content = serviceTemplate(name);
|
|
576
|
+
const serviceDir = join(cwd, 'src/services');
|
|
577
|
+
if (!existsSync(serviceDir)) {
|
|
578
|
+
mkdirSync(serviceDir, { recursive: true });
|
|
579
|
+
}
|
|
580
|
+
filePath = join(serviceDir, `${toKebabCase(name)}.ts`);
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
case 'hook': {
|
|
585
|
+
content = hookTemplate(name);
|
|
586
|
+
const hookDir = join(cwd, 'src/hooks');
|
|
587
|
+
if (!existsSync(hookDir)) {
|
|
588
|
+
mkdirSync(hookDir, { recursive: true });
|
|
589
|
+
}
|
|
590
|
+
const hookName = name.startsWith('use') ? name : `use${toPascalCase(name)}`;
|
|
591
|
+
filePath = join(hookDir, `${hookName}.ts`);
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
case 'page': {
|
|
596
|
+
const isServer = await prompt(' Server component? (Y/n): ');
|
|
597
|
+
content = pageTemplate(name, isServer.toLowerCase() !== 'n');
|
|
598
|
+
const pageDir = join(cwd, 'src/app', toKebabCase(name));
|
|
599
|
+
if (!existsSync(pageDir)) {
|
|
600
|
+
mkdirSync(pageDir, { recursive: true });
|
|
601
|
+
}
|
|
602
|
+
filePath = join(pageDir, 'page.tsx');
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
case 'schema': {
|
|
607
|
+
content = schemaTemplate(name);
|
|
608
|
+
const schemaDir = join(cwd, 'src/db/schemas');
|
|
609
|
+
if (!existsSync(schemaDir)) {
|
|
610
|
+
mkdirSync(schemaDir, { recursive: true });
|
|
611
|
+
}
|
|
612
|
+
filePath = join(schemaDir, `${toKebabCase(name)}.ts`);
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
case 'form': {
|
|
617
|
+
content = formTemplate(name);
|
|
618
|
+
const formDir = join(cwd, 'src/components/forms');
|
|
619
|
+
if (!existsSync(formDir)) {
|
|
620
|
+
mkdirSync(formDir, { recursive: true });
|
|
621
|
+
}
|
|
622
|
+
filePath = join(formDir, `${toPascalCase(name)}Form.tsx`);
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
default:
|
|
627
|
+
spinner.fail('Unknown generator type');
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Check if file already exists
|
|
632
|
+
if (existsSync(filePath)) {
|
|
633
|
+
spinner.warn('File already exists');
|
|
634
|
+
const overwrite = await prompt(' Overwrite? (y/N): ');
|
|
635
|
+
if (overwrite.toLowerCase() !== 'y') {
|
|
636
|
+
console.log(chalk.gray('\n Skipped.\n'));
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Write the file
|
|
642
|
+
writeFileSync(filePath, content);
|
|
643
|
+
|
|
644
|
+
const relativePath = filePath.replace(cwd, '').replace(/\\/g, '/');
|
|
645
|
+
spinner.succeed(`Generated ${relativePath}`);
|
|
646
|
+
|
|
647
|
+
console.log(chalk.green(`
|
|
648
|
+
╔═══════════════════════════════════════════════════════════╗
|
|
649
|
+
║ ${chalk.bold('✓ Code generated successfully!')} ║
|
|
650
|
+
╚═══════════════════════════════════════════════════════════╝
|
|
651
|
+
`));
|
|
652
|
+
|
|
653
|
+
console.log(chalk.white(' Created:\n'));
|
|
654
|
+
console.log(chalk.cyan(` ${relativePath}\n`));
|
|
655
|
+
|
|
656
|
+
// Show next steps based on type
|
|
657
|
+
console.log(chalk.white(' Next steps:\n'));
|
|
658
|
+
switch (type) {
|
|
659
|
+
case 'component':
|
|
660
|
+
console.log(chalk.gray(' 1. Import and use the component in your page'));
|
|
661
|
+
console.log(chalk.gray(' 2. Add any additional props you need'));
|
|
662
|
+
console.log(chalk.gray(' 3. Style it with Tailwind classes\n'));
|
|
663
|
+
break;
|
|
664
|
+
case 'api':
|
|
665
|
+
console.log(chalk.gray(' 1. Implement the TODO sections'));
|
|
666
|
+
console.log(chalk.gray(' 2. Add authentication if needed'));
|
|
667
|
+
console.log(chalk.gray(' 3. Test with curl or your frontend\n'));
|
|
668
|
+
break;
|
|
669
|
+
case 'service':
|
|
670
|
+
console.log(chalk.gray(' 1. Update the Zod schemas'));
|
|
671
|
+
console.log(chalk.gray(' 2. Implement database queries'));
|
|
672
|
+
console.log(chalk.gray(' 3. Import and use in your API routes\n'));
|
|
673
|
+
break;
|
|
674
|
+
case 'hook':
|
|
675
|
+
console.log(chalk.gray(' 1. Implement the fetch logic'));
|
|
676
|
+
console.log(chalk.gray(' 2. Update the return type'));
|
|
677
|
+
console.log(chalk.gray(' 3. Use it in your components\n'));
|
|
678
|
+
break;
|
|
679
|
+
case 'page':
|
|
680
|
+
console.log(chalk.gray(` 1. Visit /${toKebabCase(name)} in your browser`));
|
|
681
|
+
console.log(chalk.gray(' 2. Add your page content'));
|
|
682
|
+
console.log(chalk.gray(' 3. Update the metadata\n'));
|
|
683
|
+
break;
|
|
684
|
+
case 'schema':
|
|
685
|
+
console.log(chalk.gray(' 1. Add your column definitions'));
|
|
686
|
+
console.log(chalk.gray(' 2. Export from src/db/schema.ts'));
|
|
687
|
+
console.log(chalk.gray(' 3. Run `npm run db:push` to sync\n'));
|
|
688
|
+
break;
|
|
689
|
+
case 'form':
|
|
690
|
+
console.log(chalk.gray(' 1. Update the Zod schema with your fields'));
|
|
691
|
+
console.log(chalk.gray(' 2. Add form inputs for each field'));
|
|
692
|
+
console.log(chalk.gray(' 3. Handle form submission in parent\n'));
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
} catch (error) {
|
|
697
|
+
spinner.fail('Generation failed');
|
|
698
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
699
|
+
console.log(chalk.red(`\n Error: ${message}\n`));
|
|
700
|
+
process.exit(1);
|
|
701
|
+
}
|
|
702
|
+
}
|