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,640 @@
1
+ import { Command } from 'commander';
2
+ import path from 'path';
3
+ import fs from 'fs/promises';
4
+ import ora from 'ora';
5
+ import inquirer from 'inquirer';
6
+ import chalk from 'chalk';
7
+ import { execSync } from 'child_process';
8
+ import { ensureDir, writeFile, success, error, info, warn } from '../utils/helpers.js';
9
+
10
+ interface InitOptions {
11
+ name: string;
12
+ language: 'typescript' | 'javascript';
13
+ database: 'postgresql' | 'mysql' | 'sqlite' | 'mongodb' | 'none';
14
+ validator: 'zod' | 'joi' | 'yup';
15
+ features: string[];
16
+ }
17
+
18
+ export const initCommand = new Command('init')
19
+ .alias('new')
20
+ .description('Initialize a new Servcraft project')
21
+ .argument('[name]', 'Project name')
22
+ .option('-y, --yes', 'Skip prompts and use defaults')
23
+ .option('--ts, --typescript', 'Use TypeScript (default)')
24
+ .option('--js, --javascript', 'Use JavaScript')
25
+ .option('--db <database>', 'Database type (postgresql, mysql, sqlite, mongodb, none)')
26
+ .action(async (name?: string, cmdOptions?: { yes?: boolean; typescript?: boolean; javascript?: boolean; db?: string }) => {
27
+ console.log(chalk.blue(`
28
+ ╔═══════════════════════════════════════════╗
29
+ ║ ║
30
+ ║ ${chalk.bold('🚀 Servcraft Project Generator')} ║
31
+ ║ ║
32
+ ╚═══════════════════════════════════════════╝
33
+ `));
34
+
35
+ let options: InitOptions;
36
+
37
+ if (cmdOptions?.yes) {
38
+ options = {
39
+ name: name || 'my-servcraft-app',
40
+ language: cmdOptions.javascript ? 'javascript' : 'typescript',
41
+ database: (cmdOptions.db as InitOptions['database']) || 'postgresql',
42
+ validator: 'zod',
43
+ features: ['auth', 'users', 'email'],
44
+ };
45
+ } else {
46
+ const answers = await inquirer.prompt([
47
+ {
48
+ type: 'input',
49
+ name: 'name',
50
+ message: 'Project name:',
51
+ default: name || 'my-servcraft-app',
52
+ validate: (input: string) => {
53
+ if (!/^[a-z0-9-_]+$/i.test(input)) {
54
+ return 'Project name can only contain letters, numbers, hyphens, and underscores';
55
+ }
56
+ return true;
57
+ },
58
+ },
59
+ {
60
+ type: 'list',
61
+ name: 'language',
62
+ message: 'Select language:',
63
+ choices: [
64
+ { name: 'TypeScript (Recommended)', value: 'typescript' },
65
+ { name: 'JavaScript', value: 'javascript' },
66
+ ],
67
+ default: 'typescript',
68
+ },
69
+ {
70
+ type: 'list',
71
+ name: 'database',
72
+ message: 'Select database:',
73
+ choices: [
74
+ { name: 'PostgreSQL (Recommended)', value: 'postgresql' },
75
+ { name: 'MySQL', value: 'mysql' },
76
+ { name: 'SQLite (Development)', value: 'sqlite' },
77
+ { name: 'MongoDB', value: 'mongodb' },
78
+ { name: 'None (Add later)', value: 'none' },
79
+ ],
80
+ default: 'postgresql',
81
+ },
82
+ {
83
+ type: 'list',
84
+ name: 'validator',
85
+ message: 'Select validation library:',
86
+ choices: [
87
+ { name: 'Zod (Recommended - TypeScript-first)', value: 'zod' },
88
+ { name: 'Joi (Battle-tested, feature-rich)', value: 'joi' },
89
+ { name: 'Yup (Inspired by Joi, lighter)', value: 'yup' },
90
+ ],
91
+ default: 'zod',
92
+ },
93
+ {
94
+ type: 'checkbox',
95
+ name: 'features',
96
+ message: 'Select features to include:',
97
+ choices: [
98
+ { name: 'Authentication (JWT)', value: 'auth', checked: true },
99
+ { name: 'User Management', value: 'users', checked: true },
100
+ { name: 'Email Service', value: 'email', checked: true },
101
+ { name: 'Audit Logs', value: 'audit', checked: false },
102
+ { name: 'File Upload', value: 'upload', checked: false },
103
+ { name: 'Redis Cache', value: 'redis', checked: false },
104
+ ],
105
+ },
106
+ ]);
107
+
108
+ options = answers as InitOptions;
109
+ }
110
+
111
+ const projectDir = path.resolve(process.cwd(), options.name);
112
+ const spinner = ora('Creating project...').start();
113
+
114
+ try {
115
+ // Check if directory exists
116
+ try {
117
+ await fs.access(projectDir);
118
+ spinner.stop();
119
+ error(`Directory "${options.name}" already exists`);
120
+ return;
121
+ } catch {
122
+ // Directory doesn't exist, continue
123
+ }
124
+
125
+ // Create project directory
126
+ await ensureDir(projectDir);
127
+
128
+ spinner.text = 'Generating project files...';
129
+
130
+ // Generate package.json
131
+ const packageJson = generatePackageJson(options);
132
+ await writeFile(path.join(projectDir, 'package.json'), JSON.stringify(packageJson, null, 2));
133
+
134
+ // Generate tsconfig or jsconfig
135
+ if (options.language === 'typescript') {
136
+ await writeFile(path.join(projectDir, 'tsconfig.json'), generateTsConfig());
137
+ await writeFile(path.join(projectDir, 'tsup.config.ts'), generateTsupConfig());
138
+ } else {
139
+ await writeFile(path.join(projectDir, 'jsconfig.json'), generateJsConfig());
140
+ }
141
+
142
+ // Generate .env files
143
+ await writeFile(path.join(projectDir, '.env.example'), generateEnvExample(options));
144
+ await writeFile(path.join(projectDir, '.env'), generateEnvExample(options));
145
+
146
+ // Generate .gitignore
147
+ await writeFile(path.join(projectDir, '.gitignore'), generateGitignore());
148
+
149
+ // Generate Docker files
150
+ await writeFile(path.join(projectDir, 'Dockerfile'), generateDockerfile(options));
151
+ await writeFile(path.join(projectDir, 'docker-compose.yml'), generateDockerCompose(options));
152
+
153
+ // Create directory structure
154
+ const ext = options.language === 'typescript' ? 'ts' : 'js';
155
+ const dirs = [
156
+ 'src/core',
157
+ 'src/config',
158
+ 'src/modules',
159
+ 'src/middleware',
160
+ 'src/utils',
161
+ 'src/types',
162
+ 'tests/unit',
163
+ 'tests/integration',
164
+ ];
165
+
166
+ if (options.database !== 'none' && options.database !== 'mongodb') {
167
+ dirs.push('prisma');
168
+ }
169
+
170
+ for (const dir of dirs) {
171
+ await ensureDir(path.join(projectDir, dir));
172
+ }
173
+
174
+ // Generate main entry file
175
+ await writeFile(
176
+ path.join(projectDir, `src/index.${ext}`),
177
+ generateEntryFile(options)
178
+ );
179
+
180
+ // Generate core files
181
+ await writeFile(
182
+ path.join(projectDir, `src/core/server.${ext}`),
183
+ generateServerFile(options)
184
+ );
185
+ await writeFile(
186
+ path.join(projectDir, `src/core/logger.${ext}`),
187
+ generateLoggerFile(options)
188
+ );
189
+
190
+ // Generate Prisma schema if database is selected
191
+ if (options.database !== 'none' && options.database !== 'mongodb') {
192
+ await writeFile(
193
+ path.join(projectDir, 'prisma/schema.prisma'),
194
+ generatePrismaSchema(options)
195
+ );
196
+ }
197
+
198
+ spinner.succeed('Project files generated!');
199
+
200
+ // Install dependencies
201
+ const installSpinner = ora('Installing dependencies...').start();
202
+
203
+ try {
204
+ execSync('npm install', { cwd: projectDir, stdio: 'pipe' });
205
+ installSpinner.succeed('Dependencies installed!');
206
+ } catch {
207
+ installSpinner.warn('Failed to install dependencies automatically');
208
+ warn(' Run "npm install" manually in the project directory');
209
+ }
210
+
211
+ // Print success message
212
+ console.log('\n' + chalk.green('✨ Project created successfully!'));
213
+ console.log('\n' + chalk.bold('📁 Project structure:'));
214
+ console.log(`
215
+ ${options.name}/
216
+ ├── src/
217
+ │ ├── core/ # Core server, logger
218
+ │ ├── config/ # Configuration
219
+ │ ├── modules/ # Feature modules
220
+ │ ├── middleware/ # Middlewares
221
+ │ ├── utils/ # Utilities
222
+ │ └── index.${ext} # Entry point
223
+ ├── tests/ # Tests
224
+ ├── prisma/ # Database schema
225
+ ├── docker-compose.yml
226
+ └── package.json
227
+ `);
228
+
229
+ console.log(chalk.bold('🚀 Get started:'));
230
+ console.log(`
231
+ ${chalk.cyan(`cd ${options.name}`)}
232
+ ${options.database !== 'none' ? chalk.cyan('npm run db:push # Setup database') : ''}
233
+ ${chalk.cyan('npm run dev # Start development server')}
234
+ `);
235
+
236
+ console.log(chalk.bold('📚 Available commands:'));
237
+ console.log(`
238
+ ${chalk.yellow('servcraft generate module <name>')} Generate a new module
239
+ ${chalk.yellow('servcraft generate controller <name>')} Generate a controller
240
+ ${chalk.yellow('servcraft generate service <name>')} Generate a service
241
+ ${chalk.yellow('servcraft add auth')} Add authentication module
242
+ `);
243
+
244
+ } catch (err) {
245
+ spinner.fail('Failed to create project');
246
+ error(err instanceof Error ? err.message : String(err));
247
+ }
248
+ });
249
+
250
+ function generatePackageJson(options: InitOptions): Record<string, unknown> {
251
+ const isTS = options.language === 'typescript';
252
+
253
+ const pkg: Record<string, unknown> = {
254
+ name: options.name,
255
+ version: '0.1.0',
256
+ description: 'A Servcraft application',
257
+ main: isTS ? 'dist/index.js' : 'src/index.js',
258
+ type: 'module',
259
+ scripts: {
260
+ dev: isTS ? 'tsx watch src/index.ts' : 'node --watch src/index.js',
261
+ build: isTS ? 'tsup' : 'echo "No build needed for JS"',
262
+ start: isTS ? 'node dist/index.js' : 'node src/index.js',
263
+ test: 'vitest',
264
+ lint: isTS ? 'eslint src --ext .ts' : 'eslint src --ext .js',
265
+ },
266
+ dependencies: {
267
+ fastify: '^4.28.1',
268
+ '@fastify/cors': '^9.0.1',
269
+ '@fastify/helmet': '^11.1.1',
270
+ '@fastify/jwt': '^8.0.1',
271
+ '@fastify/rate-limit': '^9.1.0',
272
+ '@fastify/cookie': '^9.3.1',
273
+ pino: '^9.5.0',
274
+ 'pino-pretty': '^11.3.0',
275
+ bcryptjs: '^2.4.3',
276
+ dotenv: '^16.4.5',
277
+ },
278
+ devDependencies: {
279
+ vitest: '^2.1.8',
280
+ },
281
+ };
282
+
283
+ // Add validator library based on choice
284
+ switch (options.validator) {
285
+ case 'zod':
286
+ (pkg.dependencies as Record<string, string>).zod = '^3.23.8';
287
+ break;
288
+ case 'joi':
289
+ (pkg.dependencies as Record<string, string>).joi = '^17.13.3';
290
+ break;
291
+ case 'yup':
292
+ (pkg.dependencies as Record<string, string>).yup = '^1.4.0';
293
+ break;
294
+ }
295
+
296
+ if (isTS) {
297
+ (pkg.devDependencies as Record<string, string>).typescript = '^5.7.2';
298
+ (pkg.devDependencies as Record<string, string>).tsx = '^4.19.2';
299
+ (pkg.devDependencies as Record<string, string>).tsup = '^8.3.5';
300
+ (pkg.devDependencies as Record<string, string>)['@types/node'] = '^22.10.1';
301
+ (pkg.devDependencies as Record<string, string>)['@types/bcryptjs'] = '^2.4.6';
302
+ }
303
+
304
+ if (options.database !== 'none' && options.database !== 'mongodb') {
305
+ (pkg.dependencies as Record<string, string>)['@prisma/client'] = '^5.22.0';
306
+ (pkg.devDependencies as Record<string, string>).prisma = '^5.22.0';
307
+ (pkg.scripts as Record<string, string>)['db:generate'] = 'prisma generate';
308
+ (pkg.scripts as Record<string, string>)['db:migrate'] = 'prisma migrate dev';
309
+ (pkg.scripts as Record<string, string>)['db:push'] = 'prisma db push';
310
+ (pkg.scripts as Record<string, string>)['db:studio'] = 'prisma studio';
311
+ }
312
+
313
+ if (options.database === 'mongodb') {
314
+ (pkg.dependencies as Record<string, string>).mongoose = '^8.8.4';
315
+ }
316
+
317
+ if (options.features.includes('email')) {
318
+ (pkg.dependencies as Record<string, string>).nodemailer = '^6.9.15';
319
+ (pkg.dependencies as Record<string, string>).handlebars = '^4.7.8';
320
+ if (isTS) {
321
+ (pkg.devDependencies as Record<string, string>)['@types/nodemailer'] = '^6.4.17';
322
+ }
323
+ }
324
+
325
+ if (options.features.includes('redis')) {
326
+ (pkg.dependencies as Record<string, string>).ioredis = '^5.4.1';
327
+ }
328
+
329
+ return pkg;
330
+ }
331
+
332
+ function generateTsConfig(): string {
333
+ return JSON.stringify({
334
+ compilerOptions: {
335
+ target: 'ES2022',
336
+ module: 'NodeNext',
337
+ moduleResolution: 'NodeNext',
338
+ lib: ['ES2022'],
339
+ outDir: './dist',
340
+ rootDir: './src',
341
+ strict: true,
342
+ esModuleInterop: true,
343
+ skipLibCheck: true,
344
+ forceConsistentCasingInFileNames: true,
345
+ resolveJsonModule: true,
346
+ declaration: true,
347
+ sourceMap: true,
348
+ },
349
+ include: ['src/**/*'],
350
+ exclude: ['node_modules', 'dist'],
351
+ }, null, 2);
352
+ }
353
+
354
+ function generateJsConfig(): string {
355
+ return JSON.stringify({
356
+ compilerOptions: {
357
+ module: 'NodeNext',
358
+ moduleResolution: 'NodeNext',
359
+ target: 'ES2022',
360
+ checkJs: true,
361
+ },
362
+ include: ['src/**/*'],
363
+ exclude: ['node_modules'],
364
+ }, null, 2);
365
+ }
366
+
367
+ function generateTsupConfig(): string {
368
+ return `import { defineConfig } from 'tsup';
369
+
370
+ export default defineConfig({
371
+ entry: ['src/index.ts'],
372
+ format: ['esm'],
373
+ dts: true,
374
+ clean: true,
375
+ sourcemap: true,
376
+ target: 'node18',
377
+ });
378
+ `;
379
+ }
380
+
381
+ function generateEnvExample(options: InitOptions): string {
382
+ let env = `# Server
383
+ NODE_ENV=development
384
+ PORT=3000
385
+ HOST=0.0.0.0
386
+
387
+ # JWT
388
+ JWT_SECRET=your-super-secret-key-min-32-characters
389
+ JWT_ACCESS_EXPIRES_IN=15m
390
+ JWT_REFRESH_EXPIRES_IN=7d
391
+
392
+ # Security
393
+ CORS_ORIGIN=http://localhost:3000
394
+ RATE_LIMIT_MAX=100
395
+
396
+ # Logging
397
+ LOG_LEVEL=info
398
+ `;
399
+
400
+ if (options.database === 'postgresql') {
401
+ env += `
402
+ # Database (PostgreSQL)
403
+ DATABASE_PROVIDER=postgresql
404
+ DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"
405
+ `;
406
+ } else if (options.database === 'mysql') {
407
+ env += `
408
+ # Database (MySQL)
409
+ DATABASE_PROVIDER=mysql
410
+ DATABASE_URL="mysql://user:password@localhost:3306/mydb"
411
+ `;
412
+ } else if (options.database === 'sqlite') {
413
+ env += `
414
+ # Database (SQLite)
415
+ DATABASE_PROVIDER=sqlite
416
+ DATABASE_URL="file:./dev.db"
417
+ `;
418
+ } else if (options.database === 'mongodb') {
419
+ env += `
420
+ # Database (MongoDB)
421
+ MONGODB_URI="mongodb://localhost:27017/mydb"
422
+ `;
423
+ }
424
+
425
+ if (options.features.includes('email')) {
426
+ env += `
427
+ # Email
428
+ SMTP_HOST=smtp.example.com
429
+ SMTP_PORT=587
430
+ SMTP_USER=
431
+ SMTP_PASS=
432
+ SMTP_FROM="App <noreply@example.com>"
433
+ `;
434
+ }
435
+
436
+ if (options.features.includes('redis')) {
437
+ env += `
438
+ # Redis
439
+ REDIS_URL=redis://localhost:6379
440
+ `;
441
+ }
442
+
443
+ return env;
444
+ }
445
+
446
+ function generateGitignore(): string {
447
+ return `node_modules/
448
+ dist/
449
+ .env
450
+ .env.local
451
+ *.log
452
+ coverage/
453
+ .DS_Store
454
+ *.db
455
+ `;
456
+ }
457
+
458
+ function generateDockerfile(options: InitOptions): string {
459
+ const isTS = options.language === 'typescript';
460
+
461
+ return `FROM node:20-alpine
462
+ WORKDIR /app
463
+ COPY package*.json ./
464
+ RUN npm ci --only=production
465
+ COPY ${isTS ? 'dist' : 'src'} ./${isTS ? 'dist' : 'src'}
466
+ ${options.database !== 'none' && options.database !== 'mongodb' ? 'COPY prisma ./prisma\nRUN npx prisma generate' : ''}
467
+ EXPOSE 3000
468
+ CMD ["node", "${isTS ? 'dist' : 'src'}/index.js"]
469
+ `;
470
+ }
471
+
472
+ function generateDockerCompose(options: InitOptions): string {
473
+ let compose = `version: '3.8'
474
+
475
+ services:
476
+ app:
477
+ build: .
478
+ ports:
479
+ - "\${PORT:-3000}:3000"
480
+ environment:
481
+ - NODE_ENV=development
482
+ `;
483
+
484
+ if (options.database === 'postgresql') {
485
+ compose += ` - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/mydb
486
+ depends_on:
487
+ - postgres
488
+
489
+ postgres:
490
+ image: postgres:16-alpine
491
+ environment:
492
+ POSTGRES_USER: postgres
493
+ POSTGRES_PASSWORD: postgres
494
+ POSTGRES_DB: mydb
495
+ ports:
496
+ - "5432:5432"
497
+ volumes:
498
+ - postgres-data:/var/lib/postgresql/data
499
+
500
+ volumes:
501
+ postgres-data:
502
+ `;
503
+ } else if (options.database === 'mysql') {
504
+ compose += ` - DATABASE_URL=mysql://root:root@mysql:3306/mydb
505
+ depends_on:
506
+ - mysql
507
+
508
+ mysql:
509
+ image: mysql:8.0
510
+ environment:
511
+ MYSQL_ROOT_PASSWORD: root
512
+ MYSQL_DATABASE: mydb
513
+ ports:
514
+ - "3306:3306"
515
+ volumes:
516
+ - mysql-data:/var/lib/mysql
517
+
518
+ volumes:
519
+ mysql-data:
520
+ `;
521
+ }
522
+
523
+ if (options.features.includes('redis')) {
524
+ compose += `
525
+ redis:
526
+ image: redis:7-alpine
527
+ ports:
528
+ - "6379:6379"
529
+ `;
530
+ }
531
+
532
+ return compose;
533
+ }
534
+
535
+ function generatePrismaSchema(options: InitOptions): string {
536
+ const provider = options.database === 'sqlite' ? 'sqlite' : options.database;
537
+
538
+ return `generator client {
539
+ provider = "prisma-client-js"
540
+ }
541
+
542
+ datasource db {
543
+ provider = "${provider}"
544
+ url = env("DATABASE_URL")
545
+ }
546
+
547
+ model User {
548
+ id String @id @default(uuid())
549
+ email String @unique
550
+ password String
551
+ name String?
552
+ role String @default("user")
553
+ status String @default("active")
554
+ emailVerified Boolean @default(false)
555
+ createdAt DateTime @default(now())
556
+ updatedAt DateTime @updatedAt
557
+
558
+ @@map("users")
559
+ }
560
+ `;
561
+ }
562
+
563
+ function generateEntryFile(options: InitOptions): string {
564
+ const isTS = options.language === 'typescript';
565
+
566
+ return `${isTS ? "import { createServer } from './core/server.js';\nimport { logger } from './core/logger.js';" : "const { createServer } = require('./core/server.js');\nconst { logger } = require('./core/logger.js');"}
567
+
568
+ async function main()${isTS ? ': Promise<void>' : ''} {
569
+ const server = createServer();
570
+
571
+ try {
572
+ await server.start();
573
+ } catch (error) {
574
+ logger.error({ err: error }, 'Failed to start server');
575
+ process.exit(1);
576
+ }
577
+ }
578
+
579
+ main();
580
+ `;
581
+ }
582
+
583
+ function generateServerFile(options: InitOptions): string {
584
+ const isTS = options.language === 'typescript';
585
+
586
+ return `${isTS ? `import Fastify from 'fastify';
587
+ import type { FastifyInstance } from 'fastify';
588
+ import { logger } from './logger.js';` : `const Fastify = require('fastify');
589
+ const { logger } = require('./logger.js');`}
590
+
591
+ ${isTS ? 'export function createServer(): { instance: FastifyInstance; start: () => Promise<void> }' : 'function createServer()'} {
592
+ const app = Fastify({ logger });
593
+
594
+ // Health check
595
+ app.get('/health', async () => ({
596
+ status: 'ok',
597
+ timestamp: new Date().toISOString(),
598
+ }));
599
+
600
+ // Graceful shutdown
601
+ const signals${isTS ? ': NodeJS.Signals[]' : ''} = ['SIGINT', 'SIGTERM'];
602
+ signals.forEach((signal) => {
603
+ process.on(signal, async () => {
604
+ logger.info(\`Received \${signal}, shutting down...\`);
605
+ await app.close();
606
+ process.exit(0);
607
+ });
608
+ });
609
+
610
+ return {
611
+ instance: app,
612
+ start: async ()${isTS ? ': Promise<void>' : ''} => {
613
+ const port = parseInt(process.env.PORT || '3000', 10);
614
+ const host = process.env.HOST || '0.0.0.0';
615
+ await app.listen({ port, host });
616
+ logger.info(\`Server listening on \${host}:\${port}\`);
617
+ },
618
+ };
619
+ }
620
+
621
+ ${isTS ? '' : 'module.exports = { createServer };'}
622
+ `;
623
+ }
624
+
625
+ function generateLoggerFile(options: InitOptions): string {
626
+ const isTS = options.language === 'typescript';
627
+
628
+ return `${isTS ? "import pino from 'pino';\nimport type { Logger } from 'pino';" : "const pino = require('pino');"}
629
+
630
+ ${isTS ? 'export const logger: Logger' : 'const logger'} = pino({
631
+ level: process.env.LOG_LEVEL || 'info',
632
+ transport: process.env.NODE_ENV !== 'production' ? {
633
+ target: 'pino-pretty',
634
+ options: { colorize: true },
635
+ } : undefined,
636
+ });
637
+
638
+ ${isTS ? '' : 'module.exports = { logger };'}
639
+ `;
640
+ }
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { initCommand } from './commands/init.js';
5
+ import { generateCommand } from './commands/generate.js';
6
+ import { addModuleCommand } from './commands/add-module.js';
7
+ import { dbCommand } from './commands/db.js';
8
+ import { docsCommand } from './commands/docs.js';
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name('servcraft')
14
+ .description('Servcraft - A modular Node.js backend framework CLI')
15
+ .version('0.1.0');
16
+
17
+ // Initialize new project
18
+ program.addCommand(initCommand);
19
+
20
+ // Generate resources (controller, service, model, etc.)
21
+ program.addCommand(generateCommand);
22
+
23
+ // Add pre-built modules
24
+ program.addCommand(addModuleCommand);
25
+
26
+ // Database commands
27
+ program.addCommand(dbCommand);
28
+
29
+ // Documentation commands
30
+ program.addCommand(docsCommand);
31
+
32
+ program.parse();