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 +19 -117
- package/lib/cleanup.js +7 -5
- package/lib/features.js +55 -9
- package/lib/generator.js +3 -2
- package/package.json +1 -1
- package/templates/controllers/authController.js.ejs +16 -7
- package/templates/core/env.ejs +1 -3
- package/templates/core/env.example.ejs +1 -3
- package/templates/core/server.js.ejs +6 -21
- package/templates/middleware/errorHandler.js.ejs +7 -1
- package/templates/middleware/validation.js.ejs +2 -2
- package/templates/migrations/create-users.js.ejs +5 -1
- package/templates/services/authService.js.ejs +20 -2
- package/templates/services/userService.prisma.js.ejs +3 -2
- package/lib/ai-cli.js +0 -133
- package/templates/agents/graph.js.ejs +0 -84
- package/templates/config/ai.js.ejs +0 -47
- package/templates/controllers/aiController.js.ejs +0 -100
- package/templates/routes/aiRoutes.js.ejs +0 -117
- package/templates/services/aiService.js.ejs +0 -80
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
|
-
|
|
74
|
-
|
|
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')
|
|
176
|
-
hasEmail: features.includes('email')
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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('
|
|
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(
|
|
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
|
|
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.
|
|
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:
|
|
118
|
+
return success(res, { message: responseMessage });
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
|
|
121
|
+
// Process asynchronously to prevent timing attacks
|
|
122
|
+
(async () => {
|
|
123
|
+
try {
|
|
124
|
+
const resetToken = await authService.generateResetToken(email);
|
|
120
125
|
<% if (hasEmail) { %>
|
|
121
|
-
|
|
126
|
+
await emailService.sendPasswordResetEmail(email, resetToken);
|
|
122
127
|
<% } else { %>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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:
|
|
137
|
+
return success(res, { message: responseMessage });
|
|
129
138
|
} catch (error) {
|
|
130
139
|
next(error);
|
|
131
140
|
}
|
package/templates/core/env.ejs
CHANGED
|
@@ -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
|
|
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
|
|
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 (
|
|
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(`
|
|
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()
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|