express-genix 4.5.2 → 4.6.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/README.md CHANGED
@@ -112,6 +112,7 @@ npm run dev
112
112
 
113
113
  ```bash
114
114
  cd my-express-app
115
+ express-genix add infra # Adds Production-Ready Infra (Docker, CI/CD, Nginx, Makefile)
115
116
  express-genix add docker # Adds Dockerfile, docker-compose.yml, .dockerignore
116
117
  express-genix add cicd # Adds GitHub Actions CI workflow
117
118
  express-genix add auth # Adds JWT auth, controllers, routes, middleware
@@ -119,6 +120,23 @@ express-genix add websocket # Adds Socket.io setup
119
120
  express-genix add prisma # Adds Prisma schema, client config, migrations
120
121
  ```
121
122
 
123
+ ## Interactive Resource Scaffolding
124
+
125
+ Generate a complete resource (Controller, Service, and Routes) inside an existing Express Genix project.
126
+
127
+ ```bash
128
+ cd my-express-app
129
+ express-genix generate product
130
+ # or use the alias
131
+ express-genix g product
132
+ ```
133
+
134
+ This command automatically:
135
+ 1. Creates `src/controllers/productController.js` (with CRUD operations)
136
+ 2. Creates `src/services/productService.js` (with business logic structure)
137
+ 3. Creates `src/routes/productRoutes.js` (with Swagger annotations and auth middleware if enabled)
138
+ 4. **Auto-injects** the new route into `src/routes/index.js` (`router.use('/products', productRoutes);`)
139
+
122
140
  ### Generate from natural language (AI)
123
141
 
124
142
  Requires `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` in your environment.
package/index.js CHANGED
@@ -8,6 +8,7 @@ const crypto = require('crypto');
8
8
  const { execSync } = require('child_process');
9
9
  const { runPostGenerationCleanup } = require('./lib/cleanup');
10
10
  const { generateProject } = require('./lib/generator');
11
+ const { validateProjectName } = require('./lib/utils');
11
12
 
12
13
  const pkg = require('./package.json');
13
14
  const prompt = inquirer.createPromptModule();
@@ -70,13 +71,8 @@ async function main() {
70
71
  message: 'Project name:',
71
72
  default: 'my-express-app',
72
73
  validate: (input) => {
73
- if (!/^[a-zA-Z0-9-_]+$/.test(input)) {
74
- return 'Project name can only contain letters, numbers, hyphens, and underscores';
75
- }
76
- if (input.length > 100) {
77
- return 'Project name must be less than 100 characters';
78
- }
79
- return true;
74
+ const error = validateProjectName(input);
75
+ return error ? error : true;
80
76
  },
81
77
  },
82
78
  {
@@ -107,7 +103,7 @@ async function main() {
107
103
  { name: 'JWT Authentication', value: 'auth', checked: true },
108
104
  { name: 'Rate Limiting', value: 'rateLimit', checked: true },
109
105
  { name: 'Swagger/OpenAPI Docs', value: 'swagger', checked: true },
110
- { name: 'Redis Token Blacklist', value: 'redis' },
106
+ { name: 'Redis Caching / Token Blacklist', value: 'redis' },
111
107
  { name: 'Docker & Docker Compose', value: 'docker', checked: true },
112
108
  { name: 'CI/CD (GitHub Actions)', value: 'cicd' },
113
109
  { name: 'WebSocket (Socket.io)', value: 'websocket' },
@@ -131,14 +127,17 @@ async function main() {
131
127
  choices: [
132
128
  { name: 'Rate Limiting', value: 'rateLimit', checked: true },
133
129
  { name: 'Swagger/OpenAPI Docs', value: 'swagger', checked: true },
130
+ { name: 'Redis Caching', value: 'redis' },
134
131
  { name: 'Docker & Docker Compose', value: 'docker', checked: true },
135
132
  { name: 'CI/CD (GitHub Actions)', value: 'cicd' },
136
133
  { name: 'WebSocket (Socket.io)', value: 'websocket' },
137
134
  { name: 'Request ID / Correlation ID', value: 'requestId', checked: true },
135
+ { name: 'Email Service (Nodemailer)', value: 'email' },
138
136
  { name: 'File Uploads (Multer)', value: 'fileUpload' },
139
137
  { name: 'Audit Logging', value: 'auditLog' },
140
138
  { name: 'Prometheus Metrics', value: 'metrics' },
141
139
  { name: 'API Versioning (/api/v1)', value: 'apiVersioning' },
140
+ { name: 'Background Jobs (BullMQ)', value: 'backgroundJobs' },
142
141
  { name: 'GraphQL (Apollo Server)', value: 'graphql' },
143
142
  { name: 'MCP Server (AI Agent Tools)', value: 'mcp' },
144
143
  ],
@@ -172,8 +171,8 @@ async function main() {
172
171
  hasCicd: features.includes('cicd'),
173
172
  hasWebsocket: features.includes('websocket'),
174
173
  hasRequestId: features.includes('requestId'),
175
- hasRedis: features.includes('redis') && features.includes('auth') && answers.db !== 'none',
176
- hasEmail: features.includes('email') && answers.db !== 'none',
174
+ hasRedis: features.includes('redis') || features.includes('backgroundJobs'),
175
+ hasEmail: features.includes('email'),
177
176
  hasFileUpload: features.includes('fileUpload'),
178
177
  hasSoftDelete: features.includes('softDelete') && answers.db !== 'none',
179
178
  hasAuditLog: features.includes('auditLog'),
@@ -249,12 +248,17 @@ Configuration:
249
248
  }
250
249
 
251
250
  } catch (error) {
252
- // Rollback: remove partial project directory on failure
253
- if (fs.existsSync(projectDir)) {
254
- fs.rmSync(projectDir, { recursive: true, force: true });
255
- console.error('\n🧹 Rolled back partial project directory.');
251
+ if (error.message.includes('Failed to install dependencies')) {
252
+ console.error(`\n⚠️ Project generated, but dependency installation failed.`);
253
+ console.error(` Please cd into ${config.projectName} and run "npm install" manually.`);
254
+ } else {
255
+ // Rollback: remove partial project directory on failure
256
+ if (fs.existsSync(projectDir)) {
257
+ fs.rmSync(projectDir, { recursive: true, force: true });
258
+ console.error('\n🧹 Rolled back partial project directory.');
259
+ }
260
+ console.error(`\n❌ Failed to create project: ${error.message}`);
256
261
  }
257
- console.error(`\n❌ Failed to create project: ${error.message}`);
258
262
  process.exit(1);
259
263
  }
260
264
  });
@@ -263,7 +267,7 @@ Configuration:
263
267
  .command('add <feature>')
264
268
  .description('Add a feature to an existing express-genix project')
265
269
  .action(async (feature) => {
266
- const supportedFeatures = ['auth', 'websocket', 'docker', 'cicd', 'prisma'];
270
+ const supportedFeatures = ['auth', 'websocket', 'docker', 'cicd', 'prisma', 'infra'];
267
271
  if (!supportedFeatures.includes(feature)) {
268
272
  console.error(`❌ Unknown feature: "${feature}"`);
269
273
  console.log(`Supported features: ${supportedFeatures.join(', ')}`);
@@ -290,106 +294,19 @@ Configuration:
290
294
  }
291
295
  });
292
296
 
293
- program
294
- .command('ai <description>')
295
- .description('Generate a project from a natural language description (requires OPENAI_API_KEY or ANTHROPIC_API_KEY)')
296
- .option('--skip-install', 'Skip npm install')
297
- .option('--skip-cleanup', 'Skip post-generation cleanup')
298
- .action(async (description, options) => {
299
- const { runAiCommand } = require('./lib/ai-cli');
300
- const aiConfig = await runAiCommand(description);
301
-
302
- const inquirerPrompt = inquirer.createPromptModule();
303
- const aiFollowUp = await inquirerPrompt([
304
- {
305
- type: 'confirm',
306
- name: 'confirmed',
307
- message: 'Generate project with this configuration?',
308
- default: true,
309
- },
310
- ]);
311
-
312
- if (!aiFollowUp.confirmed) {
313
- console.log('Cancelled.');
314
- process.exit(0);
315
- }
316
-
317
- aiConfig.aiProvider = aiFollowUp.aiProvider || 'openai';
318
- const features = aiConfig.features || [];
319
- const config = {
320
- projectName: aiConfig.projectName,
321
- language: aiConfig.language || 'javascript',
322
- db: aiConfig.db || 'none',
323
- logger: aiConfig.logger || 'winston',
324
- hasDatabase: aiConfig.db !== 'none',
325
- isNoDatabase: aiConfig.db === 'none',
326
- isPrisma: aiConfig.db === 'prisma',
327
- isTypescript: aiConfig.language === 'typescript',
328
- hasAuth: features.includes('auth') && aiConfig.db !== 'none',
329
- hasRateLimit: features.includes('rateLimit'),
330
- hasSwagger: features.includes('swagger'),
331
- hasDocker: features.includes('docker'),
332
- hasCicd: features.includes('cicd'),
333
- hasWebsocket: features.includes('websocket'),
334
- hasRequestId: features.includes('requestId'),
335
- hasRedis: features.includes('redis') && features.includes('auth') && aiConfig.db !== 'none',
336
- hasEmail: features.includes('email') && aiConfig.db !== 'none',
337
- hasFileUpload: features.includes('fileUpload'),
338
- hasSoftDelete: features.includes('softDelete') && aiConfig.db !== 'none',
339
- hasAuditLog: features.includes('auditLog'),
340
- hasMetrics: features.includes('metrics'),
341
- hasApiVersioning: features.includes('apiVersioning'),
342
- hasBackgroundJobs: features.includes('backgroundJobs'),
343
- hasGraphQL: features.includes('graphql'),
344
- hasMCP: features.includes('mcp'),
345
- jwtSecret: generateSecret(),
346
- jwtRefreshSecret: generateSecret(),
347
- };
348
297
 
349
- const projectDir = path.join(process.cwd(), config.projectName);
350
-
351
- if (fs.existsSync(projectDir)) {
352
- console.error(`\n❌ Directory "${config.projectName}" already exists!`);
353
- process.exit(1);
354
- }
355
-
356
- const dbLabel = config.isNoDatabase ? 'no database' : config.db;
357
- console.log(`\n🚀 Creating ${config.projectName} (${config.language}, ${dbLabel})...\n`);
358
298
 
299
+ program
300
+ .command('generate <resource>')
301
+ .alias('g')
302
+ .description('Generate a new resource (controller, service, routes)')
303
+ .action(async (resource) => {
359
304
  try {
360
- await generateProject(config, projectDir, options);
361
-
362
- if (!options.skipCleanup) {
363
- await runPostGenerationCleanup(projectDir, config);
364
- }
365
-
366
- try {
367
- execSync('git init', { cwd: projectDir, stdio: 'pipe' });
368
- execSync('git add -A', { cwd: projectDir, stdio: 'pipe' });
369
- execSync('git commit -m "Initial commit from express-genix ai"', {
370
- cwd: projectDir,
371
- stdio: 'pipe',
372
- env: { ...process.env, GIT_COMMITTER_NAME: 'express-genix', GIT_COMMITTER_EMAIL: 'cli@express-genix.dev', GIT_AUTHOR_NAME: 'express-genix', GIT_AUTHOR_EMAIL: 'cli@express-genix.dev' },
373
- });
374
- } catch {
375
- // Git not available
376
- }
377
-
378
- console.log(`\n✅ Project ${config.projectName} created from AI description!\n`);
379
- console.log(` cd ${config.projectName}`);
380
- console.log(' npm run dev\n');
381
-
382
- // Prompt to install Coda VS Code extension
383
- if (config.hasMCP) {
384
- await promptCodaExtension();
385
- }
386
-
305
+ const { generateResource } = require('./lib/scaffold');
306
+ await generateResource(resource, process.cwd());
307
+ console.log(`\n🎉 Resource "${resource}" generated successfully!`);
387
308
  } catch (error) {
388
- if (fs.existsSync(projectDir)) {
389
- fs.rmSync(projectDir, { recursive: true, force: true });
390
- console.error('\n🧹 Rolled back partial project directory.');
391
- }
392
- console.error(`\n❌ Failed to create project: ${error.message}`);
309
+ console.error(`\n❌ Failed to generate resource: ${error.message}`);
393
310
  process.exit(1);
394
311
  }
395
312
  });
package/lib/cleanup.js CHANGED
@@ -6,23 +6,25 @@ const runPostGenerationCleanup = async (projectDir) => {
6
6
  try {
7
7
  // Step 1: Run eslint --fix
8
8
  try {
9
- execSync(`cd "${projectDir}" && npx eslint . --fix`, { stdio: 'pipe' });
9
+ execSync('npx eslint . --fix', { cwd: projectDir, stdio: 'pipe' });
10
10
  console.log('✅ ESLint auto-fixes applied');
11
- } catch {
11
+ } catch (error) {
12
12
  console.log('⚠️ Some ESLint issues may need manual fixing');
13
+ if (error.stderr) console.log(error.stderr.toString());
13
14
  }
14
15
 
15
16
  // Step 2: Format with Prettier
16
17
  try {
17
- execSync(`cd "${projectDir}" && npx prettier --write .`, { stdio: 'pipe' });
18
+ execSync('npx prettier --write .', { cwd: projectDir, stdio: 'pipe' });
18
19
  console.log('✅ Code formatted with Prettier');
19
- } catch {
20
+ } catch (error) {
20
21
  console.warn('⚠️ Prettier formatting encountered issues');
22
+ if (error.stderr) console.warn(error.stderr.toString());
21
23
  }
22
24
 
23
25
  // Step 3: Validate
24
26
  try {
25
- execSync(`cd "${projectDir}" && npx eslint .`, { encoding: 'utf8', stdio: 'pipe' });
27
+ execSync('npx eslint .', { cwd: projectDir, encoding: 'utf8', stdio: 'pipe' });
26
28
  console.log('✅ All linting checks passed');
27
29
  } catch (error) {
28
30
  const stdout = error.stdout || '';
package/lib/features.js CHANGED
@@ -11,6 +11,7 @@ const addFeature = async (feature, projectDir) => {
11
11
  const handlers = {
12
12
  docker: () => addDocker(projectDir, templatesDir, pkg),
13
13
  cicd: () => addCicd(projectDir, templatesDir, pkg),
14
+ infra: () => addInfra(projectDir, templatesDir, pkg),
14
15
  auth: () => addAuth(projectDir, templatesDir, pkg),
15
16
  websocket: () => addWebsocket(projectDir, templatesDir, pkg),
16
17
  prisma: () => addPrisma(projectDir, templatesDir, pkg),
@@ -62,6 +63,47 @@ const addCicd = (projectDir, templatesDir, pkg) => {
62
63
  console.log(' Created .github/workflows/ci.yml');
63
64
  };
64
65
 
66
+ const addInfra = (projectDir, templatesDir, pkg) => {
67
+ const config = inferConfig(projectDir, pkg);
68
+ config.hasNginx = true; // Enable Nginx in docker-compose
69
+
70
+ // 1. Add Docker files
71
+ const dockerFiles = [
72
+ { template: 'core/Dockerfile.ejs', output: 'Dockerfile' },
73
+ { template: 'core/docker-compose.yml.ejs', output: 'docker-compose.yml' },
74
+ { template: 'core/dockerignore.ejs', output: '.dockerignore' },
75
+ ];
76
+
77
+ for (const file of dockerFiles) {
78
+ const content = renderTemplate(templatesDir, file.template, config);
79
+ fs.writeFileSync(path.join(projectDir, file.output), content);
80
+ }
81
+
82
+ // 2. Add CI/CD files
83
+ const workflowDir = path.join(projectDir, '.github', 'workflows');
84
+ fs.mkdirSync(workflowDir, { recursive: true });
85
+ const ciContent = renderTemplate(templatesDir, 'cicd/github-actions.yml.ejs', config);
86
+ fs.writeFileSync(path.join(workflowDir, 'ci.yml'), ciContent);
87
+
88
+ // 3. Add Infra specific files (Makefile, Nginx)
89
+ const infraFiles = [
90
+ { template: 'infra/Makefile.ejs', output: 'Makefile' },
91
+ { template: 'infra/nginx.conf.ejs', output: 'nginx/nginx.conf' },
92
+ ];
93
+
94
+ for (const file of infraFiles) {
95
+ const outputPath = path.join(projectDir, file.output);
96
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
97
+ const content = renderTemplate(templatesDir, file.template, config);
98
+ fs.writeFileSync(outputPath, content);
99
+ }
100
+
101
+ console.log(' ✅ Production-Ready Infrastructure added!');
102
+ console.log(' Created Dockerfile, docker-compose.yml, .dockerignore');
103
+ console.log(' Created .github/workflows/ci.yml');
104
+ console.log(' Created Makefile and nginx/nginx.conf');
105
+ };
106
+
65
107
  const addAuth = (projectDir, templatesDir, pkg) => {
66
108
  const config = inferConfig(projectDir, pkg);
67
109
 
@@ -79,10 +121,13 @@ const addAuth = (projectDir, templatesDir, pkg) => {
79
121
  const files = [
80
122
  { template: 'controllers/authController.js.ejs', output: `src/controllers/authController.${ext}` },
81
123
  { template: 'controllers/userController.js.ejs', output: `src/controllers/userController.${ext}` },
124
+ { template: 'controllers/adminController.js.ejs', output: `src/controllers/adminController.${ext}` },
82
125
  { template: 'middleware/auth.js.ejs', output: `src/middleware/auth.${ext}` },
83
126
  { template: 'middleware/validation.js.ejs', output: `src/middleware/validation.${ext}` },
127
+ { template: 'middleware/rbac.js.ejs', output: `src/middleware/rbac.${ext}` },
84
128
  { template: 'routes/authRoutes.js.ejs', output: `src/routes/authRoutes.${ext}` },
85
129
  { template: 'routes/userRoutes.js.ejs', output: `src/routes/userRoutes.${ext}` },
130
+ { template: 'routes/adminRoutes.js.ejs', output: `src/routes/adminRoutes.${ext}` },
86
131
  { template: 'services/authService.js.ejs', output: `src/services/authService.${ext}` },
87
132
  { template: 'utils/validators.js.ejs', output: `src/utils/validators.${ext}` },
88
133
  ];
@@ -93,12 +138,14 @@ const addAuth = (projectDir, templatesDir, pkg) => {
93
138
  } else if (config.db === 'mongodb') {
94
139
  files.push(
95
140
  { template: 'services/userService.mongodb.js.ejs', output: `src/services/userService.${ext}` },
96
- { template: 'models/User.mongo.js.ejs', output: `src/models/User.${ext}` }
141
+ { template: 'models/User.mongo.js.ejs', output: `src/models/User.${ext}` },
142
+ { template: 'models/index.mongo.js.ejs', output: `src/models/index.${ext}` }
97
143
  );
98
144
  } else if (config.db === 'postgresql') {
99
145
  files.push(
100
146
  { template: 'services/userService.postgres.js.ejs', output: `src/services/userService.${ext}` },
101
- { template: 'models/User.postgres.js.ejs', output: `src/models/User.${ext}` }
147
+ { template: 'models/User.postgres.js.ejs', output: `src/models/User.${ext}` },
148
+ { template: 'models/index.postgres.js.ejs', output: `src/models/index.${ext}` }
102
149
  );
103
150
  }
104
151
 
@@ -109,8 +156,28 @@ const addAuth = (projectDir, templatesDir, pkg) => {
109
156
  fs.writeFileSync(outputPath, content);
110
157
  }
111
158
 
159
+ // Update routes/index.js
160
+ const routesIndexPath = path.join(projectDir, `src/routes/index.${ext}`);
161
+ if (fs.existsSync(routesIndexPath)) {
162
+ let routesContent = fs.readFileSync(routesIndexPath, 'utf8');
163
+
164
+ // Add imports if not present
165
+ if (!routesContent.includes('authRoutes')) {
166
+ const imports = `const authRoutes = require('./authRoutes');\nconst userRoutes = require('./userRoutes');\nconst adminRoutes = require('./adminRoutes');\n`;
167
+ routesContent = routesContent.replace(/(const express = require\('express'\);)/, `$1\n${imports}`);
168
+ }
169
+
170
+ // Add routes if not present
171
+ if (!routesContent.includes('/auth')) {
172
+ const routes = `router.use('/auth', authRoutes);\nrouter.use('/users', userRoutes);\nrouter.use('/admin', adminRoutes);\n`;
173
+ routesContent = routesContent.replace(/(const router = express\.Router\(\);)/, `$1\n\n${routes}`);
174
+ }
175
+
176
+ fs.writeFileSync(routesIndexPath, routesContent);
177
+ }
178
+
112
179
  // Add auth dependencies
113
- const depsToAdd = ['jsonwebtoken', 'bcryptjs', 'validator'];
180
+ const depsToAdd = ['jsonwebtoken', 'bcryptjs', 'validator', 'zod'];
114
181
  const missing = depsToAdd.filter((dep) => !(pkg.dependencies && pkg.dependencies[dep]));
115
182
  if (missing.length > 0) {
116
183
  console.log(` Installing: ${missing.join(', ')}`);
@@ -142,13 +209,20 @@ const addWebsocket = (projectDir, templatesDir, pkg) => {
142
209
  execSync('npm install socket.io', { cwd: projectDir, stdio: 'pipe' });
143
210
  }
144
211
 
212
+ // Update server.js
213
+ const serverPath = path.join(projectDir, `src/server.${ext}`);
214
+ if (fs.existsSync(serverPath)) {
215
+ let serverContent = fs.readFileSync(serverPath, 'utf8');
216
+
217
+ if (!serverContent.includes('setupWebSocket')) {
218
+ serverContent = serverContent.replace(/(const app = require\('\.\/app'\);)/, `$1\nconst { setupWebSocket } = require('./config/websocket');\nconst http = require('http');`);
219
+ serverContent = serverContent.replace(/const server = app\.listen\(port, \(\) => \{/, `const httpServer = http.createServer(app);\n const io = setupWebSocket(httpServer);\n app.set('io', io);\n\n const server = httpServer.listen(port, () => {`);
220
+ fs.writeFileSync(serverPath, serverContent);
221
+ }
222
+ }
223
+
145
224
  console.log(' Created src/config/websocket.' + ext);
146
- console.log(' ⚠️ Wire setupWebSocket into your server file:');
147
- console.log(' const http = require("http");');
148
- console.log(' const { setupWebSocket } = require("./config/websocket");');
149
- console.log(' const httpServer = http.createServer(app);');
150
- console.log(' setupWebSocket(httpServer);');
151
- console.log(' httpServer.listen(port);');
225
+ console.log(' WebSocket setup injected into src/server.' + ext);
152
226
  };
153
227
 
154
228
  const addPrisma = (projectDir, templatesDir, pkg) => {
@@ -200,9 +274,23 @@ const addPrisma = (projectDir, templatesDir, pkg) => {
200
274
  console.log(' ⚠️ Run "npx prisma generate" manually');
201
275
  }
202
276
 
277
+ // Update server.js
278
+ const serverPath = path.join(projectDir, `src/server.${ext}`);
279
+ if (fs.existsSync(serverPath)) {
280
+ let serverContent = fs.readFileSync(serverPath, 'utf8');
281
+ if (!serverContent.includes('db.connect()')) {
282
+ serverContent = serverContent.replace(/(const app = require\('\.\/app'\);)/, `$1\nconst db = require('./config/database');`);
283
+ serverContent = serverContent.replace(/(try \{)/, `$1\n await db.connect();`);
284
+ serverContent = serverContent.replace(/(server\.close\(async \(\) => \{)/, `$1\n await db.disconnect();`);
285
+ fs.writeFileSync(serverPath, serverContent);
286
+ }
287
+ }
288
+
203
289
  console.log(' Created prisma/schema.prisma and src/config/database.' + ext);
290
+ console.log(' ✅ Database connection injected into src/server.' + ext);
204
291
  console.log(' ⚠️ Add DATABASE_URL to your .env file');
205
292
  console.log(' ⚠️ Run "npx prisma migrate dev --name init" to create tables');
293
+ console.log(' ⚠️ Update your health check endpoint in src/app.' + ext + ' to check Prisma connection');
206
294
  };
207
295
 
208
296
  const inferConfig = (projectDir, pkg) => {
package/lib/generator.js CHANGED
@@ -371,11 +371,12 @@ const generateFiles = async (config, projectDir) => {
371
371
  const installDependencies = (projectDir) => {
372
372
  try {
373
373
  console.log('📦 Installing dependencies...');
374
- execSync(`cd "${projectDir}" && npm install`, { stdio: 'inherit' });
374
+ execSync('npm install', { cwd: projectDir, stdio: 'inherit' });
375
375
  } catch (error) {
376
376
  console.error('Failed to install dependencies:', error.message);
377
+ if (error.stderr) console.error(error.stderr.toString());
377
378
  console.log('Try running "npm install" manually in the project directory');
378
- throw error;
379
+ throw new Error('Failed to install dependencies');
379
380
  }
380
381
  };
381
382
 
@@ -0,0 +1,165 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const ejs = require('ejs');
4
+
5
+ /**
6
+ * Capitalize first letter
7
+ */
8
+ const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
9
+
10
+ /**
11
+ * Convert to camelCase
12
+ */
13
+ const toCamelCase = (str) => {
14
+ return str.replace(/[-_](.)/g, (_, c) => c.toUpperCase());
15
+ };
16
+
17
+ /**
18
+ * Convert to PascalCase
19
+ */
20
+ const toPascalCase = (str) => {
21
+ const camel = toCamelCase(str);
22
+ return capitalize(camel);
23
+ };
24
+
25
+ /**
26
+ * Simple pluralize (adds 's' if not ending in 's')
27
+ */
28
+ const pluralize = (str) => {
29
+ if (str.endsWith('s')) return str;
30
+ if (str.endsWith('y')) return str.slice(0, -1) + 'ies';
31
+ return str + 's';
32
+ };
33
+
34
+ /**
35
+ * Generate a resource (controller, service, route)
36
+ */
37
+ const generateResource = async (resourceName, projectDir) => {
38
+ const packageJsonPath = path.join(projectDir, 'package.json');
39
+
40
+ if (!fs.existsSync(packageJsonPath)) {
41
+ throw new Error('No package.json found. Run this command from your express-genix project root.');
42
+ }
43
+
44
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
45
+
46
+ // Check if it's an express-genix project (or at least has the structure)
47
+ const srcDir = path.join(projectDir, 'src');
48
+ if (!fs.existsSync(srcDir)) {
49
+ throw new Error('No "src" directory found. Are you in an express-genix project?');
50
+ }
51
+
52
+ // Determine features from package.json dependencies
53
+ const hasAuth = !!pkg.dependencies['jsonwebtoken'];
54
+
55
+ const nameCamel = toCamelCase(resourceName);
56
+ const namePascal = toPascalCase(resourceName);
57
+ const namePlural = pluralize(nameCamel);
58
+
59
+ const templateData = {
60
+ resourceName: resourceName.toLowerCase(),
61
+ resourceNameCamel: nameCamel,
62
+ resourceNamePascal: namePascal,
63
+ resourceNamePlural: namePlural,
64
+ hasAuth,
65
+ };
66
+
67
+ const templatesDir = path.join(__dirname, '..', 'templates', 'scaffold');
68
+
69
+ const filesToGenerate = [
70
+ {
71
+ template: 'controller.js.ejs',
72
+ dest: path.join(srcDir, 'controllers', `${nameCamel}Controller.js`),
73
+ },
74
+ {
75
+ template: 'service.js.ejs',
76
+ dest: path.join(srcDir, 'services', `${nameCamel}Service.js`),
77
+ },
78
+ {
79
+ template: 'route.js.ejs',
80
+ dest: path.join(srcDir, 'routes', `${nameCamel}Routes.js`),
81
+ },
82
+ ];
83
+
84
+ console.log(`\nGenerating resource: ${namePascal}...`);
85
+
86
+ for (const file of filesToGenerate) {
87
+ if (fs.existsSync(file.dest)) {
88
+ console.log(`⚠️ Skipping ${path.basename(file.dest)} (already exists)`);
89
+ continue;
90
+ }
91
+
92
+ const templatePath = path.join(templatesDir, file.template);
93
+ const templateContent = fs.readFileSync(templatePath, 'utf8');
94
+ const rendered = ejs.render(templateContent, templateData);
95
+
96
+ // Ensure directory exists
97
+ fs.mkdirSync(path.dirname(file.dest), { recursive: true });
98
+
99
+ fs.writeFileSync(file.dest, rendered);
100
+ console.log(`✅ Created ${path.relative(projectDir, file.dest)}`);
101
+ }
102
+
103
+ // Inject route into src/routes/index.js
104
+ injectRoute(srcDir, nameCamel, namePlural);
105
+ };
106
+
107
+ /**
108
+ * Inject the new route into src/routes/index.js
109
+ */
110
+ const injectRoute = (srcDir, nameCamel, namePlural) => {
111
+ const routesIndexPath = path.join(srcDir, 'routes', 'index.js');
112
+
113
+ if (!fs.existsSync(routesIndexPath)) {
114
+ console.log(`⚠️ Could not find src/routes/index.js to inject the route.`);
115
+ return;
116
+ }
117
+
118
+ let content = fs.readFileSync(routesIndexPath, 'utf8');
119
+
120
+ const requireStatement = `const ${nameCamel}Routes = require('./${nameCamel}Routes');`;
121
+ const useStatement = `router.use('/${namePlural}', ${nameCamel}Routes);`;
122
+
123
+ if (content.includes(requireStatement) || content.includes(useStatement)) {
124
+ console.log(`⚠️ Route already injected in src/routes/index.js`);
125
+ return;
126
+ }
127
+
128
+ // Find the last require statement
129
+ const requireRegex = /const .* = require\('.*'\);/g;
130
+ let lastRequireMatch;
131
+ let match;
132
+ while ((match = requireRegex.exec(content)) !== null) {
133
+ lastRequireMatch = match;
134
+ }
135
+
136
+ if (lastRequireMatch) {
137
+ const insertPos = lastRequireMatch.index + lastRequireMatch[0].length;
138
+ content = content.slice(0, insertPos) + '\n' + requireStatement + content.slice(insertPos);
139
+ } else {
140
+ // Fallback: put it after express require
141
+ content = content.replace(/const express = require\('express'\);/, `const express = require('express');\n${requireStatement}`);
142
+ }
143
+
144
+ // Find the last router.use statement
145
+ const useRegex = /router\.use\('.*', .*\);/g;
146
+ let lastUseMatch;
147
+ while ((match = useRegex.exec(content)) !== null) {
148
+ lastUseMatch = match;
149
+ }
150
+
151
+ if (lastUseMatch) {
152
+ const insertPos = lastUseMatch.index + lastUseMatch[0].length;
153
+ content = content.slice(0, insertPos) + '\n' + useStatement + content.slice(insertPos);
154
+ } else {
155
+ // Fallback: put it before router.get('/', ...)
156
+ content = content.replace(/router\.get\('\/'/, `${useStatement}\n\nrouter.get('/'`);
157
+ }
158
+
159
+ fs.writeFileSync(routesIndexPath, content);
160
+ console.log(`✅ Injected /${namePlural} route into src/routes/index.js`);
161
+ };
162
+
163
+ module.exports = {
164
+ generateResource,
165
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "express-genix",
3
- "version": "4.5.2",
3
+ "version": "4.6.1",
4
4
  "description": "Production-grade CLI to generate Express apps with JWT, RBAC, GraphQL, TypeScript, Prisma, MongoDB, PostgreSQL, file uploads, email, background jobs, and more",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -112,20 +112,29 @@ const forgotPassword = async (req, res, next) => {
112
112
  const user = await userService.findByEmail(email);
113
113
 
114
114
  // Always return success to prevent email enumeration
115
+ const responseMessage = 'If an account with that email exists, a reset link has been sent.';
116
+
115
117
  if (!user) {
116
- return success(res, { message: 'If an account with that email exists, a reset link has been sent.' });
118
+ return success(res, { message: responseMessage });
117
119
  }
118
120
 
119
- const resetToken = await authService.generateResetToken(email);
121
+ // Process asynchronously to prevent timing attacks
122
+ (async () => {
123
+ try {
124
+ const resetToken = await authService.generateResetToken(email);
120
125
  <% if (hasEmail) { %>
121
- await emailService.sendPasswordResetEmail(email, resetToken);
126
+ await emailService.sendPasswordResetEmail(email, resetToken);
122
127
  <% } else { %>
123
- // TODO: Send email with reset link
124
- // For development, log the token:
125
- console.log(`Password reset token for ${email}: ${resetToken}`);
128
+ // TODO: Send email with reset link
129
+ // For development, log the token:
130
+ console.log(`Password reset token for ${email}: ${resetToken}`);
126
131
  <% } %>
132
+ } catch (err) {
133
+ console.error('Error in forgotPassword background task:', err);
134
+ }
135
+ })();
127
136
 
128
- return success(res, { message: 'If an account with that email exists, a reset link has been sent.' });
137
+ return success(res, { message: responseMessage });
129
138
  } catch (error) {
130
139
  next(error);
131
140
  }