servcraft 0.1.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.
Files changed (106) hide show
  1. package/.dockerignore +45 -0
  2. package/.env.example +46 -0
  3. package/.husky/commit-msg +1 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.prettierignore +4 -0
  6. package/.prettierrc +11 -0
  7. package/Dockerfile +76 -0
  8. package/Dockerfile.dev +31 -0
  9. package/README.md +232 -0
  10. package/commitlint.config.js +24 -0
  11. package/dist/cli/index.cjs +3968 -0
  12. package/dist/cli/index.cjs.map +1 -0
  13. package/dist/cli/index.d.cts +1 -0
  14. package/dist/cli/index.d.ts +1 -0
  15. package/dist/cli/index.js +3945 -0
  16. package/dist/cli/index.js.map +1 -0
  17. package/dist/index.cjs +2458 -0
  18. package/dist/index.cjs.map +1 -0
  19. package/dist/index.d.cts +828 -0
  20. package/dist/index.d.ts +828 -0
  21. package/dist/index.js +2332 -0
  22. package/dist/index.js.map +1 -0
  23. package/docker-compose.prod.yml +118 -0
  24. package/docker-compose.yml +147 -0
  25. package/eslint.config.js +27 -0
  26. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  27. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  28. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  29. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  30. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +5 -0
  31. package/npm-cache/_update-notifier-last-checked +0 -0
  32. package/package.json +112 -0
  33. package/prisma/schema.prisma +157 -0
  34. package/src/cli/commands/add-module.ts +422 -0
  35. package/src/cli/commands/db.ts +137 -0
  36. package/src/cli/commands/docs.ts +16 -0
  37. package/src/cli/commands/generate.ts +459 -0
  38. package/src/cli/commands/init.ts +640 -0
  39. package/src/cli/index.ts +32 -0
  40. package/src/cli/templates/controller.ts +67 -0
  41. package/src/cli/templates/dynamic-prisma.ts +89 -0
  42. package/src/cli/templates/dynamic-schemas.ts +232 -0
  43. package/src/cli/templates/dynamic-types.ts +60 -0
  44. package/src/cli/templates/module-index.ts +33 -0
  45. package/src/cli/templates/prisma-model.ts +17 -0
  46. package/src/cli/templates/repository.ts +104 -0
  47. package/src/cli/templates/routes.ts +70 -0
  48. package/src/cli/templates/schemas.ts +26 -0
  49. package/src/cli/templates/service.ts +58 -0
  50. package/src/cli/templates/types.ts +27 -0
  51. package/src/cli/utils/docs-generator.ts +47 -0
  52. package/src/cli/utils/field-parser.ts +315 -0
  53. package/src/cli/utils/helpers.ts +89 -0
  54. package/src/config/env.ts +80 -0
  55. package/src/config/index.ts +97 -0
  56. package/src/core/index.ts +5 -0
  57. package/src/core/logger.ts +43 -0
  58. package/src/core/server.ts +132 -0
  59. package/src/database/index.ts +7 -0
  60. package/src/database/prisma.ts +54 -0
  61. package/src/database/seed.ts +59 -0
  62. package/src/index.ts +63 -0
  63. package/src/middleware/error-handler.ts +73 -0
  64. package/src/middleware/index.ts +3 -0
  65. package/src/middleware/security.ts +116 -0
  66. package/src/modules/audit/audit.service.ts +192 -0
  67. package/src/modules/audit/index.ts +2 -0
  68. package/src/modules/audit/types.ts +37 -0
  69. package/src/modules/auth/auth.controller.ts +182 -0
  70. package/src/modules/auth/auth.middleware.ts +87 -0
  71. package/src/modules/auth/auth.routes.ts +123 -0
  72. package/src/modules/auth/auth.service.ts +142 -0
  73. package/src/modules/auth/index.ts +49 -0
  74. package/src/modules/auth/schemas.ts +52 -0
  75. package/src/modules/auth/types.ts +69 -0
  76. package/src/modules/email/email.service.ts +212 -0
  77. package/src/modules/email/index.ts +10 -0
  78. package/src/modules/email/templates.ts +213 -0
  79. package/src/modules/email/types.ts +57 -0
  80. package/src/modules/swagger/index.ts +3 -0
  81. package/src/modules/swagger/schema-builder.ts +263 -0
  82. package/src/modules/swagger/swagger.service.ts +169 -0
  83. package/src/modules/swagger/types.ts +68 -0
  84. package/src/modules/user/index.ts +30 -0
  85. package/src/modules/user/schemas.ts +49 -0
  86. package/src/modules/user/types.ts +78 -0
  87. package/src/modules/user/user.controller.ts +139 -0
  88. package/src/modules/user/user.repository.ts +156 -0
  89. package/src/modules/user/user.routes.ts +199 -0
  90. package/src/modules/user/user.service.ts +145 -0
  91. package/src/modules/validation/index.ts +18 -0
  92. package/src/modules/validation/validator.ts +104 -0
  93. package/src/types/common.ts +61 -0
  94. package/src/types/index.ts +10 -0
  95. package/src/utils/errors.ts +66 -0
  96. package/src/utils/index.ts +33 -0
  97. package/src/utils/pagination.ts +38 -0
  98. package/src/utils/response.ts +63 -0
  99. package/tests/integration/auth.test.ts +59 -0
  100. package/tests/setup.ts +17 -0
  101. package/tests/unit/modules/validation.test.ts +88 -0
  102. package/tests/unit/utils/errors.test.ts +113 -0
  103. package/tests/unit/utils/pagination.test.ts +82 -0
  104. package/tsconfig.json +33 -0
  105. package/tsup.config.ts +14 -0
  106. package/vitest.config.ts +34 -0
@@ -0,0 +1,137 @@
1
+ import { Command } from 'commander';
2
+ import { execSync, spawn } from 'child_process';
3
+ import ora from 'ora';
4
+ import chalk from 'chalk';
5
+ import { success, error, info } from '../utils/helpers.js';
6
+
7
+ export const dbCommand = new Command('db')
8
+ .description('Database management commands');
9
+
10
+ dbCommand
11
+ .command('migrate')
12
+ .description('Run database migrations')
13
+ .option('-n, --name <name>', 'Migration name')
14
+ .action(async (options) => {
15
+ const spinner = ora('Running migrations...').start();
16
+
17
+ try {
18
+ const cmd = options.name
19
+ ? `npx prisma migrate dev --name ${options.name}`
20
+ : 'npx prisma migrate dev';
21
+
22
+ execSync(cmd, { stdio: 'inherit' });
23
+ spinner.succeed('Migrations completed!');
24
+ } catch (err) {
25
+ spinner.fail('Migration failed');
26
+ error(err instanceof Error ? err.message : String(err));
27
+ }
28
+ });
29
+
30
+ dbCommand
31
+ .command('push')
32
+ .description('Push schema changes to database (no migration)')
33
+ .action(async () => {
34
+ const spinner = ora('Pushing schema...').start();
35
+
36
+ try {
37
+ execSync('npx prisma db push', { stdio: 'inherit' });
38
+ spinner.succeed('Schema pushed successfully!');
39
+ } catch (err) {
40
+ spinner.fail('Push failed');
41
+ error(err instanceof Error ? err.message : String(err));
42
+ }
43
+ });
44
+
45
+ dbCommand
46
+ .command('generate')
47
+ .description('Generate Prisma client')
48
+ .action(async () => {
49
+ const spinner = ora('Generating Prisma client...').start();
50
+
51
+ try {
52
+ execSync('npx prisma generate', { stdio: 'inherit' });
53
+ spinner.succeed('Prisma client generated!');
54
+ } catch (err) {
55
+ spinner.fail('Generation failed');
56
+ error(err instanceof Error ? err.message : String(err));
57
+ }
58
+ });
59
+
60
+ dbCommand
61
+ .command('studio')
62
+ .description('Open Prisma Studio')
63
+ .action(async () => {
64
+ info('Opening Prisma Studio...');
65
+ const studio = spawn('npx', ['prisma', 'studio'], {
66
+ stdio: 'inherit',
67
+ shell: true,
68
+ });
69
+
70
+ studio.on('close', (code) => {
71
+ if (code !== 0) {
72
+ error('Prisma Studio closed with error');
73
+ }
74
+ });
75
+ });
76
+
77
+ dbCommand
78
+ .command('seed')
79
+ .description('Run database seed')
80
+ .action(async () => {
81
+ const spinner = ora('Seeding database...').start();
82
+
83
+ try {
84
+ execSync('npx prisma db seed', { stdio: 'inherit' });
85
+ spinner.succeed('Database seeded!');
86
+ } catch (err) {
87
+ spinner.fail('Seeding failed');
88
+ error(err instanceof Error ? err.message : String(err));
89
+ }
90
+ });
91
+
92
+ dbCommand
93
+ .command('reset')
94
+ .description('Reset database (drop all data and re-run migrations)')
95
+ .option('-f, --force', 'Skip confirmation')
96
+ .action(async (options) => {
97
+ if (!options.force) {
98
+ console.log(chalk.yellow('\nāš ļø WARNING: This will delete all data in your database!\n'));
99
+
100
+ const readline = await import('readline');
101
+ const rl = readline.createInterface({
102
+ input: process.stdin,
103
+ output: process.stdout,
104
+ });
105
+
106
+ const answer = await new Promise<string>((resolve) => {
107
+ rl.question('Are you sure you want to continue? (y/N) ', resolve);
108
+ });
109
+ rl.close();
110
+
111
+ if (answer.toLowerCase() !== 'y') {
112
+ info('Operation cancelled');
113
+ return;
114
+ }
115
+ }
116
+
117
+ const spinner = ora('Resetting database...').start();
118
+
119
+ try {
120
+ execSync('npx prisma migrate reset --force', { stdio: 'inherit' });
121
+ spinner.succeed('Database reset completed!');
122
+ } catch (err) {
123
+ spinner.fail('Reset failed');
124
+ error(err instanceof Error ? err.message : String(err));
125
+ }
126
+ });
127
+
128
+ dbCommand
129
+ .command('status')
130
+ .description('Show migration status')
131
+ .action(async () => {
132
+ try {
133
+ execSync('npx prisma migrate status', { stdio: 'inherit' });
134
+ } catch (err) {
135
+ error('Failed to get migration status');
136
+ }
137
+ });
@@ -0,0 +1,16 @@
1
+ import { Command } from 'commander';
2
+ import { generateDocs } from '../utils/docs-generator.js';
3
+ import { success, error } from '../utils/helpers.js';
4
+
5
+ export const docsCommand = new Command('docs')
6
+ .description('Generate Swagger/OpenAPI documentation')
7
+ .option('-o, --output <path>', 'Output file path', 'openapi.json')
8
+ .action(async (options) => {
9
+ try {
10
+ const outputPath = await generateDocs(options.output);
11
+ success(`Documentation written to ${outputPath}`);
12
+ } catch (err) {
13
+ error(err instanceof Error ? err.message : String(err));
14
+ process.exitCode = 1;
15
+ }
16
+ });
@@ -0,0 +1,459 @@
1
+ import { Command } from 'commander';
2
+ import path from 'path';
3
+ import ora from 'ora';
4
+ import inquirer from 'inquirer';
5
+ import {
6
+ toPascalCase,
7
+ toCamelCase,
8
+ toKebabCase,
9
+ pluralize,
10
+ fileExists,
11
+ writeFile,
12
+ success,
13
+ error,
14
+ warn,
15
+ info,
16
+ getModulesDir,
17
+ } from '../utils/helpers.js';
18
+ import { parseFields, type FieldDefinition } from '../utils/field-parser.js';
19
+ import { controllerTemplate } from '../templates/controller.js';
20
+ import { serviceTemplate } from '../templates/service.js';
21
+ import { repositoryTemplate } from '../templates/repository.js';
22
+ import { typesTemplate } from '../templates/types.js';
23
+ import { schemasTemplate } from '../templates/schemas.js';
24
+ import { routesTemplate } from '../templates/routes.js';
25
+ import { moduleIndexTemplate } from '../templates/module-index.js';
26
+ import { prismaModelTemplate } from '../templates/prisma-model.js';
27
+ import { dynamicTypesTemplate } from '../templates/dynamic-types.js';
28
+ import { dynamicSchemasTemplate, type ValidatorType } from '../templates/dynamic-schemas.js';
29
+ import { dynamicPrismaTemplate } from '../templates/dynamic-prisma.js';
30
+ import { generateDocs } from '../utils/docs-generator.js';
31
+
32
+ export const generateCommand = new Command('generate')
33
+ .alias('g')
34
+ .description('Generate resources (module, controller, service, etc.)');
35
+
36
+ // Generate full module
37
+ generateCommand
38
+ .command('module <name> [fields...]')
39
+ .alias('m')
40
+ .description('Generate a complete module with controller, service, repository, types, schemas, and routes')
41
+ .option('--no-routes', 'Skip routes generation')
42
+ .option('--no-repository', 'Skip repository generation')
43
+ .option('--prisma', 'Generate Prisma model suggestion')
44
+ .option('--validator <type>', 'Validator type: zod, joi, yup', 'zod')
45
+ .option('-i, --interactive', 'Interactive mode to define fields')
46
+ .action(async (name: string, fieldsArgs: string[], options) => {
47
+ let fields: FieldDefinition[] = [];
48
+
49
+ // Parse fields from command line or interactive mode
50
+ if (options.interactive) {
51
+ fields = await promptForFields();
52
+ } else if (fieldsArgs.length > 0) {
53
+ fields = parseFields(fieldsArgs.join(' '));
54
+ }
55
+
56
+ const spinner = ora('Generating module...').start();
57
+
58
+ try {
59
+ const kebabName = toKebabCase(name);
60
+ const pascalName = toPascalCase(name);
61
+ const camelName = toCamelCase(name);
62
+ const pluralName = pluralize(kebabName);
63
+ const tableName = pluralize(kebabName.replace(/-/g, '_'));
64
+ const validatorType = (options.validator || 'zod') as ValidatorType;
65
+
66
+ const moduleDir = path.join(getModulesDir(), kebabName);
67
+
68
+ // Check if module already exists
69
+ if (await fileExists(moduleDir)) {
70
+ spinner.stop();
71
+ error(`Module "${kebabName}" already exists`);
72
+ return;
73
+ }
74
+
75
+ // Use dynamic templates if fields are provided
76
+ const hasFields = fields.length > 0;
77
+
78
+ const files = [
79
+ {
80
+ name: `${kebabName}.types.ts`,
81
+ content: hasFields
82
+ ? dynamicTypesTemplate(kebabName, pascalName, fields)
83
+ : typesTemplate(kebabName, pascalName),
84
+ },
85
+ {
86
+ name: `${kebabName}.schemas.ts`,
87
+ content: hasFields
88
+ ? dynamicSchemasTemplate(kebabName, pascalName, camelName, fields, validatorType)
89
+ : schemasTemplate(kebabName, pascalName, camelName),
90
+ },
91
+ { name: `${kebabName}.service.ts`, content: serviceTemplate(kebabName, pascalName, camelName) },
92
+ { name: `${kebabName}.controller.ts`, content: controllerTemplate(kebabName, pascalName, camelName) },
93
+ { name: 'index.ts', content: moduleIndexTemplate(kebabName, pascalName, camelName) },
94
+ ];
95
+
96
+ if (options.repository !== false) {
97
+ files.push({
98
+ name: `${kebabName}.repository.ts`,
99
+ content: repositoryTemplate(kebabName, pascalName, camelName, pluralName),
100
+ });
101
+ }
102
+
103
+ if (options.routes !== false) {
104
+ files.push({
105
+ name: `${kebabName}.routes.ts`,
106
+ content: routesTemplate(kebabName, pascalName, camelName, pluralName, fields),
107
+ });
108
+ }
109
+
110
+ // Write all files
111
+ for (const file of files) {
112
+ await writeFile(path.join(moduleDir, file.name), file.content);
113
+ }
114
+
115
+ spinner.succeed(`Module "${pascalName}" generated successfully!`);
116
+
117
+ // Show Prisma model if requested or fields provided
118
+ if (options.prisma || hasFields) {
119
+ console.log('\n' + '─'.repeat(50));
120
+ info('Prisma model suggestion:');
121
+ if (hasFields) {
122
+ console.log(dynamicPrismaTemplate(pascalName, tableName, fields));
123
+ } else {
124
+ console.log(prismaModelTemplate(kebabName, pascalName, tableName));
125
+ }
126
+ }
127
+
128
+ // Show fields summary if provided
129
+ if (hasFields) {
130
+ console.log('\nšŸ“‹ Fields defined:');
131
+ fields.forEach((f) => {
132
+ const opts = [];
133
+ if (f.isOptional) opts.push('optional');
134
+ if (f.isArray) opts.push('array');
135
+ if (f.isUnique) opts.push('unique');
136
+ const optsStr = opts.length > 0 ? ` (${opts.join(', ')})` : '';
137
+ success(` ${f.name}: ${f.type}${optsStr}`);
138
+ });
139
+ }
140
+
141
+ // Show next steps
142
+ console.log('\nšŸ“ Files created:');
143
+ files.forEach((f) => success(` src/modules/${kebabName}/${f.name}`));
144
+
145
+ console.log('\nšŸ“Œ Next steps:');
146
+ if (!hasFields) {
147
+ info(' 1. Update the types in ' + `${kebabName}.types.ts`);
148
+ info(' 2. Update the schemas in ' + `${kebabName}.schemas.ts`);
149
+ info(' 3. Register the module in your app');
150
+ } else {
151
+ info(' 1. Review generated types and schemas');
152
+ info(' 2. Register the module in your app');
153
+ }
154
+ if (options.prisma || hasFields) {
155
+ info(` ${hasFields ? '3' : '4'}. Add the Prisma model to schema.prisma`);
156
+ info(` ${hasFields ? '4' : '5'}. Run: npm run db:migrate`);
157
+ }
158
+
159
+ const { generateDocsNow } = await inquirer.prompt<{ generateDocsNow: boolean }>([
160
+ {
161
+ type: 'confirm',
162
+ name: 'generateDocsNow',
163
+ message: 'Generate Swagger/OpenAPI documentation now?',
164
+ default: true,
165
+ },
166
+ ]);
167
+
168
+ if (generateDocsNow) {
169
+ await generateDocs('openapi.json', true);
170
+ }
171
+ } catch (err) {
172
+ spinner.fail('Failed to generate module');
173
+ error(err instanceof Error ? err.message : String(err));
174
+ }
175
+ });
176
+
177
+ // Generate controller only
178
+ generateCommand
179
+ .command('controller <name>')
180
+ .alias('c')
181
+ .description('Generate a controller')
182
+ .option('-m, --module <module>', 'Target module name')
183
+ .action(async (name: string, options) => {
184
+ const spinner = ora('Generating controller...').start();
185
+
186
+ try {
187
+ const kebabName = toKebabCase(name);
188
+ const pascalName = toPascalCase(name);
189
+ const camelName = toCamelCase(name);
190
+
191
+ const moduleName = options.module ? toKebabCase(options.module) : kebabName;
192
+ const moduleDir = path.join(getModulesDir(), moduleName);
193
+ const filePath = path.join(moduleDir, `${kebabName}.controller.ts`);
194
+
195
+ if (await fileExists(filePath)) {
196
+ spinner.stop();
197
+ error(`Controller "${kebabName}" already exists`);
198
+ return;
199
+ }
200
+
201
+ await writeFile(filePath, controllerTemplate(kebabName, pascalName, camelName));
202
+
203
+ spinner.succeed(`Controller "${pascalName}Controller" generated!`);
204
+ success(` src/modules/${moduleName}/${kebabName}.controller.ts`);
205
+ } catch (err) {
206
+ spinner.fail('Failed to generate controller');
207
+ error(err instanceof Error ? err.message : String(err));
208
+ }
209
+ });
210
+
211
+ // Generate service only
212
+ generateCommand
213
+ .command('service <name>')
214
+ .alias('s')
215
+ .description('Generate a service')
216
+ .option('-m, --module <module>', 'Target module name')
217
+ .action(async (name: string, options) => {
218
+ const spinner = ora('Generating service...').start();
219
+
220
+ try {
221
+ const kebabName = toKebabCase(name);
222
+ const pascalName = toPascalCase(name);
223
+ const camelName = toCamelCase(name);
224
+
225
+ const moduleName = options.module ? toKebabCase(options.module) : kebabName;
226
+ const moduleDir = path.join(getModulesDir(), moduleName);
227
+ const filePath = path.join(moduleDir, `${kebabName}.service.ts`);
228
+
229
+ if (await fileExists(filePath)) {
230
+ spinner.stop();
231
+ error(`Service "${kebabName}" already exists`);
232
+ return;
233
+ }
234
+
235
+ await writeFile(filePath, serviceTemplate(kebabName, pascalName, camelName));
236
+
237
+ spinner.succeed(`Service "${pascalName}Service" generated!`);
238
+ success(` src/modules/${moduleName}/${kebabName}.service.ts`);
239
+ } catch (err) {
240
+ spinner.fail('Failed to generate service');
241
+ error(err instanceof Error ? err.message : String(err));
242
+ }
243
+ });
244
+
245
+ // Generate repository only
246
+ generateCommand
247
+ .command('repository <name>')
248
+ .alias('r')
249
+ .description('Generate a repository')
250
+ .option('-m, --module <module>', 'Target module name')
251
+ .action(async (name: string, options) => {
252
+ const spinner = ora('Generating repository...').start();
253
+
254
+ try {
255
+ const kebabName = toKebabCase(name);
256
+ const pascalName = toPascalCase(name);
257
+ const camelName = toCamelCase(name);
258
+ const pluralName = pluralize(kebabName);
259
+
260
+ const moduleName = options.module ? toKebabCase(options.module) : kebabName;
261
+ const moduleDir = path.join(getModulesDir(), moduleName);
262
+ const filePath = path.join(moduleDir, `${kebabName}.repository.ts`);
263
+
264
+ if (await fileExists(filePath)) {
265
+ spinner.stop();
266
+ error(`Repository "${kebabName}" already exists`);
267
+ return;
268
+ }
269
+
270
+ await writeFile(filePath, repositoryTemplate(kebabName, pascalName, camelName, pluralName));
271
+
272
+ spinner.succeed(`Repository "${pascalName}Repository" generated!`);
273
+ success(` src/modules/${moduleName}/${kebabName}.repository.ts`);
274
+ } catch (err) {
275
+ spinner.fail('Failed to generate repository');
276
+ error(err instanceof Error ? err.message : String(err));
277
+ }
278
+ });
279
+
280
+ // Generate types only
281
+ generateCommand
282
+ .command('types <name>')
283
+ .alias('t')
284
+ .description('Generate types/interfaces')
285
+ .option('-m, --module <module>', 'Target module name')
286
+ .action(async (name: string, options) => {
287
+ const spinner = ora('Generating types...').start();
288
+
289
+ try {
290
+ const kebabName = toKebabCase(name);
291
+ const pascalName = toPascalCase(name);
292
+
293
+ const moduleName = options.module ? toKebabCase(options.module) : kebabName;
294
+ const moduleDir = path.join(getModulesDir(), moduleName);
295
+ const filePath = path.join(moduleDir, `${kebabName}.types.ts`);
296
+
297
+ if (await fileExists(filePath)) {
298
+ spinner.stop();
299
+ error(`Types file "${kebabName}.types.ts" already exists`);
300
+ return;
301
+ }
302
+
303
+ await writeFile(filePath, typesTemplate(kebabName, pascalName));
304
+
305
+ spinner.succeed(`Types for "${pascalName}" generated!`);
306
+ success(` src/modules/${moduleName}/${kebabName}.types.ts`);
307
+ } catch (err) {
308
+ spinner.fail('Failed to generate types');
309
+ error(err instanceof Error ? err.message : String(err));
310
+ }
311
+ });
312
+
313
+ // Generate schemas/validators only
314
+ generateCommand
315
+ .command('schema <name>')
316
+ .alias('v')
317
+ .description('Generate validation schemas')
318
+ .option('-m, --module <module>', 'Target module name')
319
+ .action(async (name: string, options) => {
320
+ const spinner = ora('Generating schemas...').start();
321
+
322
+ try {
323
+ const kebabName = toKebabCase(name);
324
+ const pascalName = toPascalCase(name);
325
+ const camelName = toCamelCase(name);
326
+
327
+ const moduleName = options.module ? toKebabCase(options.module) : kebabName;
328
+ const moduleDir = path.join(getModulesDir(), moduleName);
329
+ const filePath = path.join(moduleDir, `${kebabName}.schemas.ts`);
330
+
331
+ if (await fileExists(filePath)) {
332
+ spinner.stop();
333
+ error(`Schemas file "${kebabName}.schemas.ts" already exists`);
334
+ return;
335
+ }
336
+
337
+ await writeFile(filePath, schemasTemplate(kebabName, pascalName, camelName));
338
+
339
+ spinner.succeed(`Schemas for "${pascalName}" generated!`);
340
+ success(` src/modules/${moduleName}/${kebabName}.schemas.ts`);
341
+ } catch (err) {
342
+ spinner.fail('Failed to generate schemas');
343
+ error(err instanceof Error ? err.message : String(err));
344
+ }
345
+ });
346
+
347
+ // Generate routes only
348
+ generateCommand
349
+ .command('routes <name>')
350
+ .description('Generate routes')
351
+ .option('-m, --module <module>', 'Target module name')
352
+ .action(async (name: string, options) => {
353
+ const spinner = ora('Generating routes...').start();
354
+
355
+ try {
356
+ const kebabName = toKebabCase(name);
357
+ const pascalName = toPascalCase(name);
358
+ const camelName = toCamelCase(name);
359
+ const pluralName = pluralize(kebabName);
360
+
361
+ const moduleName = options.module ? toKebabCase(options.module) : kebabName;
362
+ const moduleDir = path.join(getModulesDir(), moduleName);
363
+ const filePath = path.join(moduleDir, `${kebabName}.routes.ts`);
364
+
365
+ if (await fileExists(filePath)) {
366
+ spinner.stop();
367
+ error(`Routes file "${kebabName}.routes.ts" already exists`);
368
+ return;
369
+ }
370
+
371
+ await writeFile(filePath, routesTemplate(kebabName, pascalName, camelName, pluralName));
372
+
373
+ spinner.succeed(`Routes for "${pascalName}" generated!`);
374
+ success(` src/modules/${moduleName}/${kebabName}.routes.ts`);
375
+ } catch (err) {
376
+ spinner.fail('Failed to generate routes');
377
+ error(err instanceof Error ? err.message : String(err));
378
+ }
379
+ });
380
+
381
+ // Interactive field prompt helper
382
+ async function promptForFields(): Promise<FieldDefinition[]> {
383
+ const fields: FieldDefinition[] = [];
384
+
385
+ console.log('\nšŸ“ Define your model fields (press Enter with empty name to finish)\n');
386
+
387
+ const fieldTypes = [
388
+ 'string',
389
+ 'number',
390
+ 'boolean',
391
+ 'date',
392
+ 'datetime',
393
+ 'text',
394
+ 'email',
395
+ 'url',
396
+ 'uuid',
397
+ 'int',
398
+ 'float',
399
+ 'decimal',
400
+ 'json',
401
+ ];
402
+
403
+ let addMore = true;
404
+
405
+ while (addMore) {
406
+ const answers = await inquirer.prompt([
407
+ {
408
+ type: 'input',
409
+ name: 'name',
410
+ message: 'Field name (empty to finish):',
411
+ },
412
+ ]);
413
+
414
+ if (!answers.name) {
415
+ addMore = false;
416
+ continue;
417
+ }
418
+
419
+ const fieldDetails = await inquirer.prompt([
420
+ {
421
+ type: 'list',
422
+ name: 'type',
423
+ message: `Type for "${answers.name}":`,
424
+ choices: fieldTypes,
425
+ default: 'string',
426
+ },
427
+ {
428
+ type: 'confirm',
429
+ name: 'isOptional',
430
+ message: 'Is optional?',
431
+ default: false,
432
+ },
433
+ {
434
+ type: 'confirm',
435
+ name: 'isUnique',
436
+ message: 'Is unique?',
437
+ default: false,
438
+ },
439
+ {
440
+ type: 'confirm',
441
+ name: 'isArray',
442
+ message: 'Is array?',
443
+ default: false,
444
+ },
445
+ ]);
446
+
447
+ fields.push({
448
+ name: answers.name,
449
+ type: fieldDetails.type,
450
+ isOptional: fieldDetails.isOptional,
451
+ isUnique: fieldDetails.isUnique,
452
+ isArray: fieldDetails.isArray,
453
+ });
454
+
455
+ console.log(` āœ“ Added: ${answers.name}: ${fieldDetails.type}\n`);
456
+ }
457
+
458
+ return fields;
459
+ }