express-genix 4.5.2 → 4.6.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.
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
  });
@@ -290,109 +294,7 @@ 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
-
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
-
359
- 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
297
 
387
- } 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}`);
393
- process.exit(1);
394
- }
395
- });
396
298
 
397
299
  await program.parseAsync(process.argv);
398
300
  }
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
@@ -79,10 +79,13 @@ const addAuth = (projectDir, templatesDir, pkg) => {
79
79
  const files = [
80
80
  { template: 'controllers/authController.js.ejs', output: `src/controllers/authController.${ext}` },
81
81
  { template: 'controllers/userController.js.ejs', output: `src/controllers/userController.${ext}` },
82
+ { template: 'controllers/adminController.js.ejs', output: `src/controllers/adminController.${ext}` },
82
83
  { template: 'middleware/auth.js.ejs', output: `src/middleware/auth.${ext}` },
83
84
  { template: 'middleware/validation.js.ejs', output: `src/middleware/validation.${ext}` },
85
+ { template: 'middleware/rbac.js.ejs', output: `src/middleware/rbac.${ext}` },
84
86
  { template: 'routes/authRoutes.js.ejs', output: `src/routes/authRoutes.${ext}` },
85
87
  { template: 'routes/userRoutes.js.ejs', output: `src/routes/userRoutes.${ext}` },
88
+ { template: 'routes/adminRoutes.js.ejs', output: `src/routes/adminRoutes.${ext}` },
86
89
  { template: 'services/authService.js.ejs', output: `src/services/authService.${ext}` },
87
90
  { template: 'utils/validators.js.ejs', output: `src/utils/validators.${ext}` },
88
91
  ];
@@ -93,12 +96,14 @@ const addAuth = (projectDir, templatesDir, pkg) => {
93
96
  } else if (config.db === 'mongodb') {
94
97
  files.push(
95
98
  { template: 'services/userService.mongodb.js.ejs', output: `src/services/userService.${ext}` },
96
- { template: 'models/User.mongo.js.ejs', output: `src/models/User.${ext}` }
99
+ { template: 'models/User.mongo.js.ejs', output: `src/models/User.${ext}` },
100
+ { template: 'models/index.mongo.js.ejs', output: `src/models/index.${ext}` }
97
101
  );
98
102
  } else if (config.db === 'postgresql') {
99
103
  files.push(
100
104
  { template: 'services/userService.postgres.js.ejs', output: `src/services/userService.${ext}` },
101
- { template: 'models/User.postgres.js.ejs', output: `src/models/User.${ext}` }
105
+ { template: 'models/User.postgres.js.ejs', output: `src/models/User.${ext}` },
106
+ { template: 'models/index.postgres.js.ejs', output: `src/models/index.${ext}` }
102
107
  );
103
108
  }
104
109
 
@@ -109,8 +114,28 @@ const addAuth = (projectDir, templatesDir, pkg) => {
109
114
  fs.writeFileSync(outputPath, content);
110
115
  }
111
116
 
117
+ // Update routes/index.js
118
+ const routesIndexPath = path.join(projectDir, `src/routes/index.${ext}`);
119
+ if (fs.existsSync(routesIndexPath)) {
120
+ let routesContent = fs.readFileSync(routesIndexPath, 'utf8');
121
+
122
+ // Add imports if not present
123
+ if (!routesContent.includes('authRoutes')) {
124
+ const imports = `const authRoutes = require('./authRoutes');\nconst userRoutes = require('./userRoutes');\nconst adminRoutes = require('./adminRoutes');\n`;
125
+ routesContent = routesContent.replace(/(const express = require\('express'\);)/, `$1\n${imports}`);
126
+ }
127
+
128
+ // Add routes if not present
129
+ if (!routesContent.includes('/auth')) {
130
+ const routes = `router.use('/auth', authRoutes);\nrouter.use('/users', userRoutes);\nrouter.use('/admin', adminRoutes);\n`;
131
+ routesContent = routesContent.replace(/(const router = express\.Router\(\);)/, `$1\n\n${routes}`);
132
+ }
133
+
134
+ fs.writeFileSync(routesIndexPath, routesContent);
135
+ }
136
+
112
137
  // Add auth dependencies
113
- const depsToAdd = ['jsonwebtoken', 'bcryptjs', 'validator'];
138
+ const depsToAdd = ['jsonwebtoken', 'bcryptjs', 'validator', 'zod'];
114
139
  const missing = depsToAdd.filter((dep) => !(pkg.dependencies && pkg.dependencies[dep]));
115
140
  if (missing.length > 0) {
116
141
  console.log(` Installing: ${missing.join(', ')}`);
@@ -142,13 +167,20 @@ const addWebsocket = (projectDir, templatesDir, pkg) => {
142
167
  execSync('npm install socket.io', { cwd: projectDir, stdio: 'pipe' });
143
168
  }
144
169
 
170
+ // Update server.js
171
+ const serverPath = path.join(projectDir, `src/server.${ext}`);
172
+ if (fs.existsSync(serverPath)) {
173
+ let serverContent = fs.readFileSync(serverPath, 'utf8');
174
+
175
+ if (!serverContent.includes('setupWebSocket')) {
176
+ serverContent = serverContent.replace(/(const app = require\('\.\/app'\);)/, `$1\nconst { setupWebSocket } = require('./config/websocket');\nconst http = require('http');`);
177
+ 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, () => {`);
178
+ fs.writeFileSync(serverPath, serverContent);
179
+ }
180
+ }
181
+
145
182
  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);');
183
+ console.log(' WebSocket setup injected into src/server.' + ext);
152
184
  };
153
185
 
154
186
  const addPrisma = (projectDir, templatesDir, pkg) => {
@@ -200,9 +232,23 @@ const addPrisma = (projectDir, templatesDir, pkg) => {
200
232
  console.log(' ⚠️ Run "npx prisma generate" manually');
201
233
  }
202
234
 
235
+ // Update server.js
236
+ const serverPath = path.join(projectDir, `src/server.${ext}`);
237
+ if (fs.existsSync(serverPath)) {
238
+ let serverContent = fs.readFileSync(serverPath, 'utf8');
239
+ if (!serverContent.includes('db.connect()')) {
240
+ serverContent = serverContent.replace(/(const app = require\('\.\/app'\);)/, `$1\nconst db = require('./config/database');`);
241
+ serverContent = serverContent.replace(/(try \{)/, `$1\n await db.connect();`);
242
+ serverContent = serverContent.replace(/(server\.close\(async \(\) => \{)/, `$1\n await db.disconnect();`);
243
+ fs.writeFileSync(serverPath, serverContent);
244
+ }
245
+ }
246
+
203
247
  console.log(' Created prisma/schema.prisma and src/config/database.' + ext);
248
+ console.log(' ✅ Database connection injected into src/server.' + ext);
204
249
  console.log(' ⚠️ Add DATABASE_URL to your .env file');
205
250
  console.log(' ⚠️ Run "npx prisma migrate dev --name init" to create tables');
251
+ console.log(' ⚠️ Update your health check endpoint in src/app.' + ext + ' to check Prisma connection');
206
252
  };
207
253
 
208
254
  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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "express-genix",
3
- "version": "4.5.2",
3
+ "version": "4.6.0",
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
  }
@@ -9,10 +9,8 @@ JWT_REFRESH_SECRET=<%= jwtRefreshSecret %>
9
9
  JWT_EXPIRE=15m
10
10
  JWT_REFRESH_EXPIRE=7d
11
11
  <% } %><% if (hasRedis) { %>
12
- # Redis (token blacklist store)
12
+ # Redis
13
13
  REDIS_URL=redis://localhost:6379
14
- <% } %><% if (hasBackgroundJobs && !hasRedis) { %>
15
- # Redis (required for BullMQ)
16
14
  REDIS_HOST=localhost
17
15
  REDIS_PORT=6379
18
16
  <% } %><% if (hasEmail) { %>
@@ -9,10 +9,8 @@ JWT_REFRESH_SECRET=CHANGE_ME_generate_a_64_char_hex_secret
9
9
  JWT_EXPIRE=15m
10
10
  JWT_REFRESH_EXPIRE=7d
11
11
  <% } %><% if (hasRedis) { %>
12
- # Redis (token blacklist store)
12
+ # Redis
13
13
  REDIS_URL=redis://localhost:6379
14
- <% } %><% if (hasBackgroundJobs && !hasRedis) { %>
15
- # Redis (required for BullMQ)
16
14
  REDIS_HOST=localhost
17
15
  REDIS_PORT=6379
18
16
  <% } %><% if (hasEmail) { %>
@@ -1,7 +1,5 @@
1
1
  require('dotenv').config();
2
2
 
3
- const cluster = require('cluster');
4
- const os = require('os');
5
3
  <% if (hasWebsocket) { %>const http = require('http');<% } %>
6
4
  const app = require('./app');<% if (hasDatabase) { %>
7
5
  const db = require('./config/database');<% } %><% if (hasRedis) { %>
@@ -12,22 +10,9 @@ const { createLogger } = require('./utils/logger');
12
10
 
13
11
  const logger = createLogger('Server');
14
12
  const port = process.env.PORT || 3000;
15
- const isPrimary = cluster.isPrimary ?? cluster.isMaster;
16
13
 
17
14
  const startServer = async () => {
18
- if (process.env.NODE_ENV === 'production' && isPrimary) {
19
- const numCPUs = os.cpus().length;
20
- logger.info(`Master ${process.pid} is running, forking ${numCPUs} workers`);
21
-
22
- for (let i = 0; i < numCPUs; i++) {
23
- cluster.fork();
24
- }
25
-
26
- cluster.on('exit', (worker, code, signal) => {
27
- logger.warn(`Worker ${worker.process.pid} died (code: ${code}, signal: ${signal}). Restarting...`);
28
- cluster.fork();
29
- });
30
- } else {<% if (hasDatabase) { %>
15
+ try {<% if (hasDatabase) { %>
31
16
  await db.connect();<% } %><% if (hasRedis) { %>
32
17
  await connectRedis();<% } %><% if (hasGraphQL) { %>
33
18
  await app.initGraphQL();<% } %><% if (hasBackgroundJobs) { %>
@@ -38,7 +23,7 @@ const startServer = async () => {
38
23
  app.set('io', io);
39
24
 
40
25
  const server = httpServer.listen(port, () => {<% } else { %> const server = app.listen(port, () => {<% } %>
41
- logger.info(`Worker ${process.pid} running on http://localhost:${port}`);
26
+ logger.info(`Server running on http://localhost:${port}`);
42
27
  logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`);
43
28
  });
44
29
 
@@ -59,10 +44,10 @@ const startServer = async () => {
59
44
 
60
45
  process.on('SIGTERM', () => shutdown('SIGTERM'));
61
46
  process.on('SIGINT', () => shutdown('SIGINT'));
47
+ } catch (err) {
48
+ logger.error('Failed to start server', { error: err.message });
49
+ process.exit(1);
62
50
  }
63
51
  };
64
52
 
65
- startServer().catch((err) => {
66
- logger.error('Failed to start server', { error: err.message });
67
- process.exit(1);
68
- });
53
+ startServer();
@@ -31,6 +31,10 @@ const errorHandler = (err, req, res, next) => {
31
31
  const message = err.errors.map((e) => e.message).join(', ');
32
32
  error = new AppError(message, 400);
33
33
  }
34
+ <% } %><% if (db === 'prisma') { %>
35
+ if (err.code === 'P2002') {
36
+ error = new AppError('Duplicate field value entered', 400);
37
+ }
34
38
  <% } %><% if (hasAuth) { %>
35
39
  if (err.name === 'JsonWebTokenError') {
36
40
  error = new AppError('Invalid token', 401);
@@ -41,9 +45,11 @@ const errorHandler = (err, req, res, next) => {
41
45
  }
42
46
  <% } %>
43
47
 
48
+ const message = error.isOperational ? error.message : 'Internal Server Error';
49
+
44
50
  res.status(error.statusCode || 500).json({
45
51
  success: false,
46
- error: error.message || 'Internal Server Error',
52
+ error: process.env.NODE_ENV === 'development' ? (error.message || 'Internal Server Error') : message,
47
53
  ...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
48
54
  });
49
55
  };
@@ -9,11 +9,11 @@ const { AppError } = require('../utils/errors');
9
9
  * Usage:
10
10
  * router.post('/users', validate({ body: createUserSchema }), controller.create);
11
11
  */
12
- const validate = (schemas) => (req, res, next) => {
12
+ const validate = (schemas) => async (req, res, next) => {
13
13
  const errors = [];
14
14
 
15
15
  for (const [source, schema] of Object.entries(schemas)) {
16
- const result = schema.safeParse(req[source]);
16
+ const result = await schema.safeParseAsync(req[source]);
17
17
  if (!result.success) {
18
18
  result.error.issues.forEach((issue) => {
19
19
  errors.push(`${source}.${issue.path.join('.')}: ${issue.message}`);
@@ -37,7 +37,11 @@ module.exports = {
37
37
  type: Sequelize.DATE,
38
38
  allowNull: false,
39
39
  defaultValue: Sequelize.literal('NOW()'),
40
- },
40
+ },<% if (hasSoftDelete) { %>
41
+ deletedAt: {
42
+ type: Sequelize.DATE,
43
+ allowNull: true,
44
+ },<% } %>
41
45
  });
42
46
 
43
47
  await queryInterface.addIndex('users', ['email'], { unique: true });
@@ -4,8 +4,19 @@ const crypto = require('crypto');
4
4
  <% } %>
5
5
  <% if (!hasRedis) { %>
6
6
  // In-memory token blacklist — replace with Redis for production.
7
- const tokenBlacklist = new Set();
7
+ const tokenBlacklist = new Map(); // token → expiresAt
8
8
  const resetTokens = new Map(); // token → { email, expiresAt }
9
+
10
+ // Periodic cleanup to prevent memory leaks
11
+ setInterval(() => {
12
+ const now = Date.now();
13
+ for (const [token, expiresAt] of tokenBlacklist.entries()) {
14
+ if (expiresAt < now) tokenBlacklist.delete(token);
15
+ }
16
+ for (const [hash, entry] of resetTokens.entries()) {
17
+ if (entry.expiresAt < now) resetTokens.delete(hash);
18
+ }
19
+ }, 60 * 60 * 1000).unref(); // Run every hour
9
20
  <% } %>
10
21
  const generateTokens = (user) => {
11
22
  const payload = {
@@ -66,10 +77,17 @@ const consumeResetToken = async (token) => {
66
77
  };
67
78
  <% } else { %>
68
79
  const blacklistToken = (token) => {
69
- tokenBlacklist.add(token);
80
+ const decoded = jwt.decode(token);
81
+ const expiresAt = decoded && decoded.exp ? decoded.exp * 1000 : Date.now() + 86400000;
82
+ tokenBlacklist.set(token, expiresAt);
70
83
  };
71
84
 
72
85
  const isTokenBlacklisted = (token) => {
86
+ const expiresAt = tokenBlacklist.get(token);
87
+ if (expiresAt && expiresAt < Date.now()) {
88
+ tokenBlacklist.delete(token);
89
+ return false;
90
+ }
73
91
  return tokenBlacklist.has(token);
74
92
  };
75
93
 
@@ -58,12 +58,13 @@ const findAll = async ({ page = 1, limit = 20 } = {}) => {
58
58
  const skip = (page - 1) * limit;
59
59
  const [users, total] = await Promise.all([
60
60
  prisma.user.findMany({
61
- select: { id: true, username: true, email: true, role: true, createdAt: true, updatedAt: true },
61
+ <% if (hasSoftDelete) { %> where: { deletedAt: null },
62
+ <% } %> select: { id: true, username: true, email: true, role: true, createdAt: true, updatedAt: true },
62
63
  orderBy: { createdAt: 'desc' },
63
64
  skip,
64
65
  take: limit,
65
66
  }),
66
- prisma.user.count(),
67
+ prisma.user.count(<% if (hasSoftDelete) { %>{ where: { deletedAt: null } }<% } %>),
67
68
  ]);
68
69
  return { users, total, page, totalPages: Math.ceil(total / limit) };
69
70
  };
package/lib/ai-cli.js DELETED
@@ -1,133 +0,0 @@
1
- /**
2
- * AI CLI handler — interprets natural language project descriptions
3
- * and maps them to express-genix configuration flags.
4
- *
5
- * Requires @langchain/openai or @langchain/anthropic to be installed.
6
- * These are NOT bundled with express-genix to keep the CLI lightweight.
7
- *
8
- * Install with: npm install -g @langchain/core @langchain/openai
9
- */
10
-
11
- const SYSTEM_PROMPT = `You are an expert Express.js project configurator for the express-genix CLI tool.
12
- Given a natural language description of a web application, output a JSON configuration object.
13
-
14
- Available options:
15
- - projectName: string (lowercase, hyphens only, no spaces)
16
- - language: "javascript" | "typescript"
17
- - db: "mongodb" | "postgresql" | "prisma" | "none"
18
- - logger: "winston" | "pino"
19
- - features: array of feature strings from this list:
20
- - "auth" (JWT authentication — requires a database)
21
- - "rateLimit" (rate limiting)
22
- - "swagger" (Swagger/OpenAPI docs)
23
- - "redis" (Redis token blacklist — requires auth)
24
- - "docker" (Docker & Docker Compose)
25
- - "cicd" (GitHub Actions CI/CD)
26
- - "websocket" (WebSocket via Socket.io)
27
- - "requestId" (request ID / correlation tracking)
28
- - "email" (Nodemailer email service — requires database)
29
- - "fileUpload" (Multer file uploads)
30
- - "softDelete" (soft deletes — requires database + auth)
31
- - "auditLog" (audit logging middleware)
32
- - "metrics" (Prometheus metrics)
33
- - "apiVersioning" (API versioning /api/v1)
34
- - "backgroundJobs" (BullMQ background jobs)
35
- - "graphql" (GraphQL via Apollo Server)
36
- - "mcp" (MCP Server — exposes API as AI-agent tools)
37
- - "ai" (LangChain/LangGraph AI service)
38
-
39
- Rules:
40
- - Always include "auth", "rateLimit", "swagger", "requestId" unless explicitly unwanted
41
- - Include "docker" for production-ready apps
42
- - Include "ai" if the description mentions AI, chatbot, LLM, or intelligence
43
- - Include "mcp" if the description mentions AI agents, tools, or MCP
44
- - Choose "prisma" for modern stacks, "mongodb" for documents, "postgresql" for relational
45
- - Infer a short kebab-case project name from the description
46
- - Output ONLY valid JSON. No explanation, no markdown fences.`;
47
-
48
- const runAiCommand = async (description) => {
49
- // Check for API key
50
- const hasOpenAI = !!process.env.OPENAI_API_KEY;
51
- const hasAnthropic = !!process.env.ANTHROPIC_API_KEY;
52
- const hasGemini = !!process.env.GOOGLE_API_KEY;
53
-
54
- if (!hasOpenAI && !hasAnthropic && !hasGemini) {
55
- console.error('❌ The AI command requires an API key.');
56
- console.error(' Set one of these environment variables:');
57
- console.error(' export OPENAI_API_KEY=sk-...');
58
- console.error(' export ANTHROPIC_API_KEY=sk-ant-...');
59
- console.error(' export GOOGLE_API_KEY=AI...');
60
- process.exit(1);
61
- }
62
-
63
- // Try to load LangChain (not bundled — must be installed separately)
64
- let ChatModel;
65
- try {
66
- if (hasGemini) {
67
- const { ChatGoogleGenerativeAI } = require('@langchain/google-genai');
68
- ChatModel = new ChatGoogleGenerativeAI({
69
- model: 'gemini-2.0-flash',
70
- maxOutputTokens: 1024,
71
- apiKey: process.env.GOOGLE_API_KEY,
72
- });
73
- } else if (hasAnthropic) {
74
- const { ChatAnthropic } = require('@langchain/anthropic');
75
- ChatModel = new ChatAnthropic({
76
- modelName: 'claude-sonnet-4-20250514',
77
- maxTokens: 1024,
78
- anthropicApiKey: process.env.ANTHROPIC_API_KEY,
79
- });
80
- } else {
81
- const { ChatOpenAI } = require('@langchain/openai');
82
- ChatModel = new ChatOpenAI({
83
- modelName: 'gpt-4o-mini',
84
- maxTokens: 1024,
85
- openAIApiKey: process.env.OPENAI_API_KEY,
86
- });
87
- }
88
- } catch {
89
- console.error('❌ LangChain packages not found.');
90
- console.error(' Install them globally to use the AI command:');
91
- console.error(' npm install -g @langchain/core @langchain/openai @langchain/anthropic @langchain/google-genai');
92
- process.exit(1);
93
- }
94
-
95
- const { HumanMessage, SystemMessage } = require('@langchain/core/messages');
96
-
97
- console.log('🤖 Analyzing your description...\n');
98
-
99
- const response = await ChatModel.invoke([
100
- new SystemMessage(SYSTEM_PROMPT),
101
- new HumanMessage(description),
102
- ]);
103
-
104
- let config;
105
- try {
106
- const jsonMatch = response.content.match(/\{[\s\S]*\}/);
107
- if (!jsonMatch) throw new Error('No JSON found in response');
108
- config = JSON.parse(jsonMatch[0]);
109
- } catch {
110
- console.error('❌ Could not parse AI response. Try a clearer description.');
111
- console.error(' Raw output:', response.content);
112
- process.exit(1);
113
- }
114
-
115
- // Validate required fields
116
- if (!config.projectName || !config.language || !config.db) {
117
- console.error('❌ AI response missing required fields. Try again.');
118
- process.exit(1);
119
- }
120
-
121
- // Display interpreted config
122
- console.log('📋 Interpreted configuration:\n');
123
- console.log(` Project: ${config.projectName}`);
124
- console.log(` Language: ${config.language}`);
125
- console.log(` Database: ${config.db}`);
126
- console.log(` Logger: ${config.logger || 'winston'}`);
127
- console.log(` Features: ${(config.features || []).join(', ') || 'base'}`);
128
- console.log('');
129
-
130
- return config;
131
- };
132
-
133
- module.exports = { runAiCommand };
@@ -1,84 +0,0 @@
1
- const { createReactAgent } = require('@langchain/langgraph/prebuilt');
2
- const { tool } = require('@langchain/core/tools');
3
- const { HumanMessage } = require('@langchain/core/messages');
4
- const { z } = require('zod');
5
- const { getModel } = require('../config/ai');
6
-
7
- // --- Built-in Tools ---
8
-
9
- const currentTimeTool = tool(
10
- async () => new Date().toISOString(),
11
- {
12
- name: 'current_time',
13
- description: 'Get the current date and time in ISO 8601 format',
14
- schema: z.object({}),
15
- }
16
- );
17
-
18
- const calculatorTool = tool(
19
- async ({ expression }) => {
20
- // Only allow digits and math operators — no code injection possible
21
- const sanitized = expression.replace(/[^0-9+\-*/().%\s]/g, '');
22
- if (sanitized !== expression.trim()) {
23
- return 'Error: Expression contains invalid characters. Use only numbers and +, -, *, /, (, ), %';
24
- }
25
- try {
26
- const result = new Function(`"use strict"; return (${sanitized})`)();
27
- if (!isFinite(result)) return 'Error: Result is not a finite number';
28
- return String(result);
29
- } catch {
30
- return 'Error: Could not evaluate expression';
31
- }
32
- },
33
- {
34
- name: 'calculator',
35
- description: 'Evaluate a mathematical expression using standard operators: +, -, *, /, %',
36
- schema: z.object({
37
- expression: z.string().describe('Math expression, e.g. "2 + 3 * 4"'),
38
- }),
39
- }
40
- );
41
-
42
- /**
43
- * Create a LangGraph ReAct agent with tool-calling capabilities.
44
- *
45
- * @param {Object} options
46
- * @param {Array} options.tools - Additional @langchain/core tools to register
47
- * @param {string} options.systemPrompt - System instruction for the agent
48
- * @param {string} options.model - Model name override
49
- */
50
- const createAgent = (options = {}) => {
51
- const tools = [
52
- currentTimeTool,
53
- calculatorTool,
54
- ...(options.tools || []),
55
- ];
56
-
57
- const model = getModel(options);
58
-
59
- return createReactAgent({
60
- llm: model,
61
- tools,
62
- messageModifier: options.systemPrompt || 'You are a helpful assistant. Use the provided tools when needed to answer questions accurately.',
63
- });
64
- };
65
-
66
- /**
67
- * Run the agent with a single message and return the final response.
68
- */
69
- const runAgent = async (message, options = {}) => {
70
- const agent = createAgent(options);
71
-
72
- const result = await agent.invoke({
73
- messages: [new HumanMessage(message)],
74
- });
75
-
76
- const lastMessage = result.messages[result.messages.length - 1];
77
-
78
- return {
79
- content: lastMessage.content,
80
- steps: result.messages.length,
81
- };
82
- };
83
-
84
- module.exports = { createAgent, runAgent, currentTimeTool, calculatorTool };
@@ -1,47 +0,0 @@
1
- <% if (aiProvider === 'openai') { %>
2
- const { ChatOpenAI } = require('@langchain/openai');
3
- <% } else if (aiProvider === 'anthropic') { %>
4
- const { ChatAnthropic } = require('@langchain/anthropic');
5
- <% } else if (aiProvider === 'gemini') { %>
6
- const { ChatGoogleGenerativeAI } = require('@langchain/google-genai');
7
- <% } else if (aiProvider === 'ollama') { %>
8
- const { ChatOllama } = require('@langchain/ollama');
9
- <% } %>
10
-
11
- /**
12
- * Create a LangChain chat model based on the configured provider.
13
- */
14
- const getModel = (options = {}) => {
15
- const temperature = options.temperature ?? parseFloat(process.env.AI_TEMPERATURE || '0.7');
16
- const maxTokens = options.maxTokens ?? parseInt(process.env.AI_MAX_TOKENS || '2048', 10);
17
- <% if (aiProvider === 'openai') { %>
18
- return new ChatOpenAI({
19
- modelName: options.model || process.env.AI_MODEL || 'gpt-4o-mini',
20
- temperature,
21
- maxTokens,
22
- openAIApiKey: process.env.OPENAI_API_KEY,
23
- });
24
- <% } else if (aiProvider === 'anthropic') { %>
25
- return new ChatAnthropic({
26
- modelName: options.model || process.env.AI_MODEL || 'claude-sonnet-4-20250514',
27
- temperature,
28
- maxTokens,
29
- anthropicApiKey: process.env.ANTHROPIC_API_KEY,
30
- });
31
- <% } else if (aiProvider === 'gemini') { %>
32
- return new ChatGoogleGenerativeAI({
33
- model: options.model || process.env.AI_MODEL || 'gemini-2.0-flash',
34
- temperature,
35
- maxOutputTokens: maxTokens,
36
- apiKey: process.env.GOOGLE_API_KEY,
37
- });
38
- <% } else if (aiProvider === 'ollama') { %>
39
- return new ChatOllama({
40
- model: options.model || process.env.AI_MODEL || 'llama3',
41
- temperature,
42
- baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
43
- });
44
- <% } %>
45
- };
46
-
47
- module.exports = { getModel };
@@ -1,100 +0,0 @@
1
- const aiService = require('../services/aiService');
2
- const { runAgent } = require('../agents/graph');
3
- const { success, error } = require('../utils/response');
4
-
5
- /**
6
- * POST /ai/chat — Send a message, get a complete AI response.
7
- */
8
- const chatHandler = async (req, res, next) => {
9
- try {
10
- const { message, systemPrompt, history, model, temperature, maxTokens } = req.body;
11
-
12
- if (!message) {
13
- return error(res, 'Message is required', 400);
14
- }
15
-
16
- const result = await aiService.chat(message, {
17
- systemPrompt,
18
- history,
19
- model,
20
- temperature,
21
- maxTokens,
22
- });
23
-
24
- return success(res, result);
25
- } catch (err) {
26
- next(err);
27
- }
28
- };
29
-
30
- /**
31
- * POST /ai/stream — Stream an AI response via Server-Sent Events.
32
- */
33
- const streamHandler = async (req, res, next) => {
34
- try {
35
- const { message, systemPrompt, history, model, temperature, maxTokens } = req.body;
36
-
37
- if (!message) {
38
- return error(res, 'Message is required', 400);
39
- }
40
-
41
- res.setHeader('Content-Type', 'text/event-stream');
42
- res.setHeader('Cache-Control', 'no-cache');
43
- res.setHeader('Connection', 'keep-alive');
44
-
45
- const generator = aiService.stream(message, {
46
- systemPrompt,
47
- history,
48
- model,
49
- temperature,
50
- maxTokens,
51
- });
52
-
53
- for await (const chunk of generator) {
54
- res.write(`data: ${JSON.stringify({ content: chunk })}\n\n`);
55
- }
56
-
57
- res.write('data: [DONE]\n\n');
58
- res.end();
59
- } catch (err) {
60
- next(err);
61
- }
62
- };
63
-
64
- /**
65
- * POST /ai/chain — Run a prompt template chain with variables.
66
- */
67
- const chainHandler = async (req, res, next) => {
68
- try {
69
- const { template, variables, model } = req.body;
70
-
71
- if (!template) {
72
- return error(res, 'Template is required', 400);
73
- }
74
-
75
- const result = await aiService.chain(template, variables || {}, { model });
76
- return success(res, { content: result });
77
- } catch (err) {
78
- next(err);
79
- }
80
- };
81
-
82
- /**
83
- * POST /ai/agent — Run the LangGraph ReAct agent.
84
- */
85
- const agentHandler = async (req, res, next) => {
86
- try {
87
- const { message, systemPrompt, model } = req.body;
88
-
89
- if (!message) {
90
- return error(res, 'Message is required', 400);
91
- }
92
-
93
- const result = await runAgent(message, { systemPrompt, model });
94
- return success(res, result);
95
- } catch (err) {
96
- next(err);
97
- }
98
- };
99
-
100
- module.exports = { chatHandler, streamHandler, chainHandler, agentHandler };
@@ -1,117 +0,0 @@
1
- const express = require('express');
2
- const { chatHandler, streamHandler, chainHandler, agentHandler } = require('../controllers/aiController');
3
-
4
- const router = express.Router();
5
-
6
- /**
7
- * @swagger
8
- * <% if (hasApiVersioning) { %>/v1<% } %>/ai/chat:
9
- * post:
10
- * summary: Send a chat message to the AI
11
- * tags: [AI]
12
- * requestBody:
13
- * required: true
14
- * content:
15
- * application/json:
16
- * schema:
17
- * type: object
18
- * required: [message]
19
- * properties:
20
- * message:
21
- * type: string
22
- * example: "What is Node.js?"
23
- * systemPrompt:
24
- * type: string
25
- * example: "You are a helpful coding assistant."
26
- * history:
27
- * type: array
28
- * items:
29
- * type: object
30
- * properties:
31
- * role:
32
- * type: string
33
- * enum: [user, assistant]
34
- * content:
35
- * type: string
36
- * model:
37
- * type: string
38
- * responses:
39
- * 200:
40
- * description: AI response
41
- */
42
- router.post('/chat', chatHandler);
43
-
44
- /**
45
- * @swagger
46
- * <% if (hasApiVersioning) { %>/v1<% } %>/ai/stream:
47
- * post:
48
- * summary: Stream an AI response via Server-Sent Events
49
- * tags: [AI]
50
- * requestBody:
51
- * required: true
52
- * content:
53
- * application/json:
54
- * schema:
55
- * type: object
56
- * required: [message]
57
- * properties:
58
- * message:
59
- * type: string
60
- * responses:
61
- * 200:
62
- * description: SSE stream of AI response chunks
63
- */
64
- router.post('/stream', streamHandler);
65
-
66
- /**
67
- * @swagger
68
- * <% if (hasApiVersioning) { %>/v1<% } %>/ai/chain:
69
- * post:
70
- * summary: Run a prompt template chain with variable substitution
71
- * tags: [AI]
72
- * requestBody:
73
- * required: true
74
- * content:
75
- * application/json:
76
- * schema:
77
- * type: object
78
- * required: [template]
79
- * properties:
80
- * template:
81
- * type: string
82
- * example: "Summarize the following text: {text}"
83
- * variables:
84
- * type: object
85
- * example: { "text": "Node.js is a JavaScript runtime..." }
86
- * responses:
87
- * 200:
88
- * description: Chain result
89
- */
90
- router.post('/chain', chainHandler);
91
-
92
- /**
93
- * @swagger
94
- * <% if (hasApiVersioning) { %>/v1<% } %>/ai/agent:
95
- * post:
96
- * summary: Run the LangGraph ReAct agent with tool calling
97
- * tags: [AI]
98
- * requestBody:
99
- * required: true
100
- * content:
101
- * application/json:
102
- * schema:
103
- * type: object
104
- * required: [message]
105
- * properties:
106
- * message:
107
- * type: string
108
- * example: "What time is it right now?"
109
- * systemPrompt:
110
- * type: string
111
- * responses:
112
- * 200:
113
- * description: Agent response with tool-use steps
114
- */
115
- router.post('/agent', agentHandler);
116
-
117
- module.exports = router;
@@ -1,80 +0,0 @@
1
- const { getModel } = require('../config/ai');
2
- const { HumanMessage, SystemMessage, AIMessage } = require('@langchain/core/messages');
3
- const { StringOutputParser } = require('@langchain/core/output_parsers');
4
- const { ChatPromptTemplate } = require('@langchain/core/prompts');
5
- const { logger } = require('../utils/logger');
6
-
7
- const DEFAULT_SYSTEM_PROMPT = 'You are a helpful AI assistant.';
8
-
9
- /**
10
- * Send a chat message and get a complete response.
11
- */
12
- const chat = async (message, options = {}) => {
13
- const model = getModel(options);
14
- const messages = [
15
- new SystemMessage(options.systemPrompt || DEFAULT_SYSTEM_PROMPT),
16
- ];
17
-
18
- if (Array.isArray(options.history)) {
19
- for (const msg of options.history) {
20
- if (msg.role === 'user') messages.push(new HumanMessage(msg.content));
21
- else if (msg.role === 'assistant') messages.push(new AIMessage(msg.content));
22
- }
23
- }
24
-
25
- messages.push(new HumanMessage(message));
26
-
27
- const response = await model.invoke(messages);
28
-
29
- logger.info('AI chat completed', {
30
- provider: process.env.AI_PROVIDER,
31
- });
32
-
33
- return {
34
- content: response.content,
35
- model: response.response_metadata?.model || 'unknown',
36
- usage: response.usage_metadata || null,
37
- };
38
- };
39
-
40
- /**
41
- * Stream a chat response. Returns an async generator yielding text chunks.
42
- */
43
- const stream = async function* (message, options = {}) {
44
- const model = getModel(options);
45
- const messages = [
46
- new SystemMessage(options.systemPrompt || DEFAULT_SYSTEM_PROMPT),
47
- ];
48
-
49
- if (Array.isArray(options.history)) {
50
- for (const msg of options.history) {
51
- if (msg.role === 'user') messages.push(new HumanMessage(msg.content));
52
- else if (msg.role === 'assistant') messages.push(new AIMessage(msg.content));
53
- }
54
- }
55
-
56
- messages.push(new HumanMessage(message));
57
-
58
- const streamResponse = await model.stream(messages);
59
-
60
- for await (const chunk of streamResponse) {
61
- if (chunk.content) {
62
- yield chunk.content;
63
- }
64
- }
65
- };
66
-
67
- /**
68
- * Run a prompt template chain with variable substitution.
69
- */
70
- const chain = async (template, variables = {}, options = {}) => {
71
- const model = getModel(options);
72
- const prompt = ChatPromptTemplate.fromTemplate(template);
73
- const outputParser = new StringOutputParser();
74
- const pipeline = prompt.pipe(model).pipe(outputParser);
75
-
76
- const result = await pipeline.invoke(variables);
77
- return result;
78
- };
79
-
80
- module.exports = { chat, stream, chain };