express-genix 4.5.1 ā 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/README.md +5 -10
- package/index.js +24 -129
- 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/README.md
CHANGED
|
@@ -319,14 +319,11 @@ When you select **AI** or **MCP** features, the CLI prompts you to install the *
|
|
|
319
319
|
### Install manually
|
|
320
320
|
|
|
321
321
|
```bash
|
|
322
|
-
#
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
# Install
|
|
326
|
-
code --install-extension coda-ai-0.1.0.vsix
|
|
322
|
+
# From VS Code Marketplace
|
|
323
|
+
code --install-extension express-genix.coda-ai
|
|
327
324
|
```
|
|
328
325
|
|
|
329
|
-
Or
|
|
326
|
+
Or search "Coda AI" in the VS Code Extensions panel.
|
|
330
327
|
|
|
331
328
|
### Configuration
|
|
332
329
|
|
|
@@ -334,10 +331,8 @@ In VS Code Settings (search "Coda"):
|
|
|
334
331
|
|
|
335
332
|
| Setting | Default | Description |
|
|
336
333
|
|---------|---------|-------------|
|
|
337
|
-
| `coda.
|
|
338
|
-
| `coda.
|
|
339
|
-
| `coda.aiProvider` | `openai` | AI provider (openai, anthropic, gemini, ollama) |
|
|
340
|
-
| `coda.model` | *(server default)* | Model override |
|
|
334
|
+
| `coda.provider` | `openai` | AI provider (openai, anthropic, gemini, ollama) |
|
|
335
|
+
| `coda.model` | *(provider default)* | Model override |
|
|
341
336
|
| `coda.systemPrompt` | *Coding assistant prompt* | Default system prompt |
|
|
342
337
|
|
|
343
338
|
## Contributing
|
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();
|
|
@@ -15,7 +16,6 @@ const prompt = inquirer.createPromptModule();
|
|
|
15
16
|
const generateSecret = (length = 64) => crypto.randomBytes(length).toString('hex');
|
|
16
17
|
|
|
17
18
|
const CODA_EXTENSION_ID = 'express-genix.coda-ai';
|
|
18
|
-
const CODA_VSIX_URL = 'https://github.com/LambdaAI001/coda/releases/download/v1.0.0/coda-ai-1.0.0.vsix';
|
|
19
19
|
|
|
20
20
|
async function promptCodaExtension() {
|
|
21
21
|
const inquirerPrompt = inquirer.createPromptModule();
|
|
@@ -40,21 +40,15 @@ async function promptCodaExtension() {
|
|
|
40
40
|
// Check if VS Code CLI is available
|
|
41
41
|
execSync('code --version', { stdio: 'pipe' });
|
|
42
42
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
console.log('\nš¦ Downloading Coda AI extension...');
|
|
47
|
-
execSync(`curl -sL -o "${tmpFile}" "${CODA_VSIX_URL}"`, { stdio: 'pipe' });
|
|
48
|
-
console.log('š¦ Installing...');
|
|
49
|
-
execSync(`code --install-extension "${tmpFile}"`, { stdio: 'pipe' });
|
|
50
|
-
// Clean up temp file
|
|
51
|
-
try { fs.unlinkSync(tmpFile); } catch {}
|
|
43
|
+
// Install from VS Code Marketplace
|
|
44
|
+
console.log('\nš¦ Installing Coda AI extension from Marketplace...');
|
|
45
|
+
execSync(`code --install-extension ${CODA_EXTENSION_ID}`, { stdio: 'pipe' });
|
|
52
46
|
console.log('ā
Coda extension installed! Open your project in VS Code to use it.');
|
|
53
47
|
console.log(' Look for the Coda AI icon in the sidebar, or type @coda in Copilot Chat.');
|
|
54
48
|
} catch {
|
|
55
49
|
console.log('\nā ļø Could not install automatically. Install it manually:');
|
|
56
|
-
console.log('
|
|
57
|
-
console.log(
|
|
50
|
+
console.log(' Search "Coda AI" in VS Code Extensions, or run:');
|
|
51
|
+
console.log(` code --install-extension ${CODA_EXTENSION_ID}`);
|
|
58
52
|
}
|
|
59
53
|
}
|
|
60
54
|
|
|
@@ -77,13 +71,8 @@ async function main() {
|
|
|
77
71
|
message: 'Project name:',
|
|
78
72
|
default: 'my-express-app',
|
|
79
73
|
validate: (input) => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
if (input.length > 100) {
|
|
84
|
-
return 'Project name must be less than 100 characters';
|
|
85
|
-
}
|
|
86
|
-
return true;
|
|
74
|
+
const error = validateProjectName(input);
|
|
75
|
+
return error ? error : true;
|
|
87
76
|
},
|
|
88
77
|
},
|
|
89
78
|
{
|
|
@@ -114,7 +103,7 @@ async function main() {
|
|
|
114
103
|
{ name: 'JWT Authentication', value: 'auth', checked: true },
|
|
115
104
|
{ name: 'Rate Limiting', value: 'rateLimit', checked: true },
|
|
116
105
|
{ name: 'Swagger/OpenAPI Docs', value: 'swagger', checked: true },
|
|
117
|
-
{ name: 'Redis Token Blacklist', value: 'redis' },
|
|
106
|
+
{ name: 'Redis Caching / Token Blacklist', value: 'redis' },
|
|
118
107
|
{ name: 'Docker & Docker Compose', value: 'docker', checked: true },
|
|
119
108
|
{ name: 'CI/CD (GitHub Actions)', value: 'cicd' },
|
|
120
109
|
{ name: 'WebSocket (Socket.io)', value: 'websocket' },
|
|
@@ -138,14 +127,17 @@ async function main() {
|
|
|
138
127
|
choices: [
|
|
139
128
|
{ name: 'Rate Limiting', value: 'rateLimit', checked: true },
|
|
140
129
|
{ name: 'Swagger/OpenAPI Docs', value: 'swagger', checked: true },
|
|
130
|
+
{ name: 'Redis Caching', value: 'redis' },
|
|
141
131
|
{ name: 'Docker & Docker Compose', value: 'docker', checked: true },
|
|
142
132
|
{ name: 'CI/CD (GitHub Actions)', value: 'cicd' },
|
|
143
133
|
{ name: 'WebSocket (Socket.io)', value: 'websocket' },
|
|
144
134
|
{ name: 'Request ID / Correlation ID', value: 'requestId', checked: true },
|
|
135
|
+
{ name: 'Email Service (Nodemailer)', value: 'email' },
|
|
145
136
|
{ name: 'File Uploads (Multer)', value: 'fileUpload' },
|
|
146
137
|
{ name: 'Audit Logging', value: 'auditLog' },
|
|
147
138
|
{ name: 'Prometheus Metrics', value: 'metrics' },
|
|
148
139
|
{ name: 'API Versioning (/api/v1)', value: 'apiVersioning' },
|
|
140
|
+
{ name: 'Background Jobs (BullMQ)', value: 'backgroundJobs' },
|
|
149
141
|
{ name: 'GraphQL (Apollo Server)', value: 'graphql' },
|
|
150
142
|
{ name: 'MCP Server (AI Agent Tools)', value: 'mcp' },
|
|
151
143
|
],
|
|
@@ -179,8 +171,8 @@ async function main() {
|
|
|
179
171
|
hasCicd: features.includes('cicd'),
|
|
180
172
|
hasWebsocket: features.includes('websocket'),
|
|
181
173
|
hasRequestId: features.includes('requestId'),
|
|
182
|
-
hasRedis: features.includes('redis')
|
|
183
|
-
hasEmail: features.includes('email')
|
|
174
|
+
hasRedis: features.includes('redis') || features.includes('backgroundJobs'),
|
|
175
|
+
hasEmail: features.includes('email'),
|
|
184
176
|
hasFileUpload: features.includes('fileUpload'),
|
|
185
177
|
hasSoftDelete: features.includes('softDelete') && answers.db !== 'none',
|
|
186
178
|
hasAuditLog: features.includes('auditLog'),
|
|
@@ -256,12 +248,17 @@ Configuration:
|
|
|
256
248
|
}
|
|
257
249
|
|
|
258
250
|
} catch (error) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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}`);
|
|
263
261
|
}
|
|
264
|
-
console.error(`\nā Failed to create project: ${error.message}`);
|
|
265
262
|
process.exit(1);
|
|
266
263
|
}
|
|
267
264
|
});
|
|
@@ -297,109 +294,7 @@ Configuration:
|
|
|
297
294
|
}
|
|
298
295
|
});
|
|
299
296
|
|
|
300
|
-
program
|
|
301
|
-
.command('ai <description>')
|
|
302
|
-
.description('Generate a project from a natural language description (requires OPENAI_API_KEY or ANTHROPIC_API_KEY)')
|
|
303
|
-
.option('--skip-install', 'Skip npm install')
|
|
304
|
-
.option('--skip-cleanup', 'Skip post-generation cleanup')
|
|
305
|
-
.action(async (description, options) => {
|
|
306
|
-
const { runAiCommand } = require('./lib/ai-cli');
|
|
307
|
-
const aiConfig = await runAiCommand(description);
|
|
308
|
-
|
|
309
|
-
const inquirerPrompt = inquirer.createPromptModule();
|
|
310
|
-
const aiFollowUp = await inquirerPrompt([
|
|
311
|
-
{
|
|
312
|
-
type: 'confirm',
|
|
313
|
-
name: 'confirmed',
|
|
314
|
-
message: 'Generate project with this configuration?',
|
|
315
|
-
default: true,
|
|
316
|
-
},
|
|
317
|
-
]);
|
|
318
|
-
|
|
319
|
-
if (!aiFollowUp.confirmed) {
|
|
320
|
-
console.log('Cancelled.');
|
|
321
|
-
process.exit(0);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
aiConfig.aiProvider = aiFollowUp.aiProvider || 'openai';
|
|
325
|
-
const features = aiConfig.features || [];
|
|
326
|
-
const config = {
|
|
327
|
-
projectName: aiConfig.projectName,
|
|
328
|
-
language: aiConfig.language || 'javascript',
|
|
329
|
-
db: aiConfig.db || 'none',
|
|
330
|
-
logger: aiConfig.logger || 'winston',
|
|
331
|
-
hasDatabase: aiConfig.db !== 'none',
|
|
332
|
-
isNoDatabase: aiConfig.db === 'none',
|
|
333
|
-
isPrisma: aiConfig.db === 'prisma',
|
|
334
|
-
isTypescript: aiConfig.language === 'typescript',
|
|
335
|
-
hasAuth: features.includes('auth') && aiConfig.db !== 'none',
|
|
336
|
-
hasRateLimit: features.includes('rateLimit'),
|
|
337
|
-
hasSwagger: features.includes('swagger'),
|
|
338
|
-
hasDocker: features.includes('docker'),
|
|
339
|
-
hasCicd: features.includes('cicd'),
|
|
340
|
-
hasWebsocket: features.includes('websocket'),
|
|
341
|
-
hasRequestId: features.includes('requestId'),
|
|
342
|
-
hasRedis: features.includes('redis') && features.includes('auth') && aiConfig.db !== 'none',
|
|
343
|
-
hasEmail: features.includes('email') && aiConfig.db !== 'none',
|
|
344
|
-
hasFileUpload: features.includes('fileUpload'),
|
|
345
|
-
hasSoftDelete: features.includes('softDelete') && aiConfig.db !== 'none',
|
|
346
|
-
hasAuditLog: features.includes('auditLog'),
|
|
347
|
-
hasMetrics: features.includes('metrics'),
|
|
348
|
-
hasApiVersioning: features.includes('apiVersioning'),
|
|
349
|
-
hasBackgroundJobs: features.includes('backgroundJobs'),
|
|
350
|
-
hasGraphQL: features.includes('graphql'),
|
|
351
|
-
hasMCP: features.includes('mcp'),
|
|
352
|
-
jwtSecret: generateSecret(),
|
|
353
|
-
jwtRefreshSecret: generateSecret(),
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
const projectDir = path.join(process.cwd(), config.projectName);
|
|
357
|
-
|
|
358
|
-
if (fs.existsSync(projectDir)) {
|
|
359
|
-
console.error(`\nā Directory "${config.projectName}" already exists!`);
|
|
360
|
-
process.exit(1);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const dbLabel = config.isNoDatabase ? 'no database' : config.db;
|
|
364
|
-
console.log(`\nš Creating ${config.projectName} (${config.language}, ${dbLabel})...\n`);
|
|
365
|
-
|
|
366
|
-
try {
|
|
367
|
-
await generateProject(config, projectDir, options);
|
|
368
|
-
|
|
369
|
-
if (!options.skipCleanup) {
|
|
370
|
-
await runPostGenerationCleanup(projectDir, config);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
try {
|
|
374
|
-
execSync('git init', { cwd: projectDir, stdio: 'pipe' });
|
|
375
|
-
execSync('git add -A', { cwd: projectDir, stdio: 'pipe' });
|
|
376
|
-
execSync('git commit -m "Initial commit from express-genix ai"', {
|
|
377
|
-
cwd: projectDir,
|
|
378
|
-
stdio: 'pipe',
|
|
379
|
-
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' },
|
|
380
|
-
});
|
|
381
|
-
} catch {
|
|
382
|
-
// Git not available
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
console.log(`\nā
Project ${config.projectName} created from AI description!\n`);
|
|
386
|
-
console.log(` cd ${config.projectName}`);
|
|
387
|
-
console.log(' npm run dev\n');
|
|
388
|
-
|
|
389
|
-
// Prompt to install Coda VS Code extension
|
|
390
|
-
if (config.hasMCP) {
|
|
391
|
-
await promptCodaExtension();
|
|
392
|
-
}
|
|
393
297
|
|
|
394
|
-
} catch (error) {
|
|
395
|
-
if (fs.existsSync(projectDir)) {
|
|
396
|
-
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
397
|
-
console.error('\nš§¹ Rolled back partial project directory.');
|
|
398
|
-
}
|
|
399
|
-
console.error(`\nā Failed to create project: ${error.message}`);
|
|
400
|
-
process.exit(1);
|
|
401
|
-
}
|
|
402
|
-
});
|
|
403
298
|
|
|
404
299
|
await program.parseAsync(process.argv);
|
|
405
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 };
|