express-genix 4.5.2 → 4.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,24 @@
1
1
  # templates/core/docker-compose.yml.ejs
2
2
  version: '3.8'
3
3
 
4
- services:
4
+ services:<% if (typeof hasNginx !== 'undefined' && hasNginx) { %>
5
+ nginx:
6
+ image: nginx:alpine
7
+ ports:
8
+ - "80:80"
9
+ volumes:
10
+ - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
11
+ depends_on:
12
+ - app
13
+ restart: unless-stopped
14
+ networks:
15
+ - app-network
16
+ <% } %>
5
17
  app:
6
18
  build: .
7
- ports:
8
- - "3000:3000"
19
+ ports:<% if (typeof hasNginx !== 'undefined' && hasNginx) { %>
20
+ - "3000" # Exposed to Nginx, not host directly<% } else { %>
21
+ - "3000:3000"<% } %>
9
22
  environment:
10
23
  - NODE_ENV=production
11
24
  env_file:
@@ -9,10 +9,8 @@ JWT_REFRESH_SECRET=<%= jwtRefreshSecret %>
9
9
  JWT_EXPIRE=15m
10
10
  JWT_REFRESH_EXPIRE=7d
11
11
  <% } %><% if (hasRedis) { %>
12
- # Redis (token blacklist store)
12
+ # Redis
13
13
  REDIS_URL=redis://localhost:6379
14
- <% } %><% if (hasBackgroundJobs && !hasRedis) { %>
15
- # Redis (required for BullMQ)
16
14
  REDIS_HOST=localhost
17
15
  REDIS_PORT=6379
18
16
  <% } %><% if (hasEmail) { %>
@@ -9,10 +9,8 @@ JWT_REFRESH_SECRET=CHANGE_ME_generate_a_64_char_hex_secret
9
9
  JWT_EXPIRE=15m
10
10
  JWT_REFRESH_EXPIRE=7d
11
11
  <% } %><% if (hasRedis) { %>
12
- # Redis (token blacklist store)
12
+ # Redis
13
13
  REDIS_URL=redis://localhost:6379
14
- <% } %><% if (hasBackgroundJobs && !hasRedis) { %>
15
- # Redis (required for BullMQ)
16
14
  REDIS_HOST=localhost
17
15
  REDIS_PORT=6379
18
16
  <% } %><% if (hasEmail) { %>
@@ -1,7 +1,5 @@
1
1
  require('dotenv').config();
2
2
 
3
- const cluster = require('cluster');
4
- const os = require('os');
5
3
  <% if (hasWebsocket) { %>const http = require('http');<% } %>
6
4
  const app = require('./app');<% if (hasDatabase) { %>
7
5
  const db = require('./config/database');<% } %><% if (hasRedis) { %>
@@ -12,22 +10,9 @@ const { createLogger } = require('./utils/logger');
12
10
 
13
11
  const logger = createLogger('Server');
14
12
  const port = process.env.PORT || 3000;
15
- const isPrimary = cluster.isPrimary ?? cluster.isMaster;
16
13
 
17
14
  const startServer = async () => {
18
- if (process.env.NODE_ENV === 'production' && isPrimary) {
19
- const numCPUs = os.cpus().length;
20
- logger.info(`Master ${process.pid} is running, forking ${numCPUs} workers`);
21
-
22
- for (let i = 0; i < numCPUs; i++) {
23
- cluster.fork();
24
- }
25
-
26
- cluster.on('exit', (worker, code, signal) => {
27
- logger.warn(`Worker ${worker.process.pid} died (code: ${code}, signal: ${signal}). Restarting...`);
28
- cluster.fork();
29
- });
30
- } else {<% if (hasDatabase) { %>
15
+ try {<% if (hasDatabase) { %>
31
16
  await db.connect();<% } %><% if (hasRedis) { %>
32
17
  await connectRedis();<% } %><% if (hasGraphQL) { %>
33
18
  await app.initGraphQL();<% } %><% if (hasBackgroundJobs) { %>
@@ -38,7 +23,7 @@ const startServer = async () => {
38
23
  app.set('io', io);
39
24
 
40
25
  const server = httpServer.listen(port, () => {<% } else { %> const server = app.listen(port, () => {<% } %>
41
- logger.info(`Worker ${process.pid} running on http://localhost:${port}`);
26
+ logger.info(`Server running on http://localhost:${port}`);
42
27
  logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`);
43
28
  });
44
29
 
@@ -59,10 +44,10 @@ const startServer = async () => {
59
44
 
60
45
  process.on('SIGTERM', () => shutdown('SIGTERM'));
61
46
  process.on('SIGINT', () => shutdown('SIGINT'));
47
+ } catch (err) {
48
+ logger.error('Failed to start server', { error: err.message });
49
+ process.exit(1);
62
50
  }
63
51
  };
64
52
 
65
- startServer().catch((err) => {
66
- logger.error('Failed to start server', { error: err.message });
67
- process.exit(1);
68
- });
53
+ startServer();
@@ -0,0 +1,30 @@
1
+ .PHONY: build up down logs test lint clean
2
+
3
+ # Build the Docker image
4
+ build:
5
+ docker-compose build
6
+
7
+ # Start the application in detached mode
8
+ up:
9
+ docker-compose up -d
10
+
11
+ # Stop the application
12
+ down:
13
+ docker-compose down
14
+
15
+ # View logs
16
+ logs:
17
+ docker-compose logs -f app
18
+
19
+ # Run tests
20
+ test:
21
+ npm test
22
+
23
+ # Run linter
24
+ lint:
25
+ npm run lint
26
+
27
+ # Clean up Docker resources
28
+ clean:
29
+ docker-compose down -v --remove-orphans
30
+ docker system prune -f
@@ -0,0 +1,25 @@
1
+ events {
2
+ worker_connections 1024;
3
+ }
4
+
5
+ http {
6
+ upstream app_servers {
7
+ server app:3000;
8
+ }
9
+
10
+ server {
11
+ listen 80;
12
+ server_name localhost;
13
+
14
+ location / {
15
+ proxy_pass http://app_servers;
16
+ proxy_http_version 1.1;
17
+ proxy_set_header Upgrade $http_upgrade;
18
+ proxy_set_header Connection 'upgrade';
19
+ proxy_set_header Host $host;
20
+ proxy_cache_bypass $http_upgrade;
21
+ proxy_set_header X-Real-IP $remote_addr;
22
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
23
+ }
24
+ }
25
+ }
@@ -31,6 +31,10 @@ const errorHandler = (err, req, res, next) => {
31
31
  const message = err.errors.map((e) => e.message).join(', ');
32
32
  error = new AppError(message, 400);
33
33
  }
34
+ <% } %><% if (db === 'prisma') { %>
35
+ if (err.code === 'P2002') {
36
+ error = new AppError('Duplicate field value entered', 400);
37
+ }
34
38
  <% } %><% if (hasAuth) { %>
35
39
  if (err.name === 'JsonWebTokenError') {
36
40
  error = new AppError('Invalid token', 401);
@@ -41,9 +45,11 @@ const errorHandler = (err, req, res, next) => {
41
45
  }
42
46
  <% } %>
43
47
 
48
+ const message = error.isOperational ? error.message : 'Internal Server Error';
49
+
44
50
  res.status(error.statusCode || 500).json({
45
51
  success: false,
46
- error: error.message || 'Internal Server Error',
52
+ error: process.env.NODE_ENV === 'development' ? (error.message || 'Internal Server Error') : message,
47
53
  ...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
48
54
  });
49
55
  };
@@ -9,11 +9,11 @@ const { AppError } = require('../utils/errors');
9
9
  * Usage:
10
10
  * router.post('/users', validate({ body: createUserSchema }), controller.create);
11
11
  */
12
- const validate = (schemas) => (req, res, next) => {
12
+ const validate = (schemas) => async (req, res, next) => {
13
13
  const errors = [];
14
14
 
15
15
  for (const [source, schema] of Object.entries(schemas)) {
16
- const result = schema.safeParse(req[source]);
16
+ const result = await schema.safeParseAsync(req[source]);
17
17
  if (!result.success) {
18
18
  result.error.issues.forEach((issue) => {
19
19
  errors.push(`${source}.${issue.path.join('.')}: ${issue.message}`);
@@ -37,7 +37,11 @@ module.exports = {
37
37
  type: Sequelize.DATE,
38
38
  allowNull: false,
39
39
  defaultValue: Sequelize.literal('NOW()'),
40
- },
40
+ },<% if (hasSoftDelete) { %>
41
+ deletedAt: {
42
+ type: Sequelize.DATE,
43
+ allowNull: true,
44
+ },<% } %>
41
45
  });
42
46
 
43
47
  await queryInterface.addIndex('users', ['email'], { unique: true });
@@ -0,0 +1,94 @@
1
+ const <%= resourceNameCamel %>Service = require('../services/<%= resourceNameCamel %>Service');
2
+ const { AppError } = require('../utils/errors');
3
+ const { success, created, paginated } = require('../utils/response');
4
+ const { createLogger } = require('../utils/logger');
5
+
6
+ const logger = createLogger('<%= resourceNamePascal %>Controller');
7
+
8
+ const getAll<%= resourceNamePascal %>s = async (req, res, next) => {
9
+ try {
10
+ const page = Math.max(1, parseInt(req.query.page, 10) || 1);
11
+ const limit = Math.min(100, Math.max(1, parseInt(req.query.limit, 10) || 10));
12
+
13
+ logger.info('Fetching <%= resourceNamePlural %>', { page, limit });
14
+ const result = await <%= resourceNameCamel %>Service.getAll<%= resourceNamePascal %>s(page, limit);
15
+
16
+ return paginated(res, result.<%= resourceNamePlural %>, {
17
+ page,
18
+ limit,
19
+ total: result.total,
20
+ totalPages: result.totalPages,
21
+ });
22
+ } catch (error) {
23
+ logger.error('Error fetching <%= resourceNamePlural %>', { error: error.message });
24
+ next(error);
25
+ }
26
+ };
27
+
28
+ const get<%= resourceNamePascal %>ById = async (req, res, next) => {
29
+ try {
30
+ const { id } = req.params;
31
+ const <%= resourceNameCamel %> = await <%= resourceNameCamel %>Service.get<%= resourceNamePascal %>ById(id);
32
+
33
+ if (!<%= resourceNameCamel %>) {
34
+ throw new AppError('<%= resourceNamePascal %> not found', 404);
35
+ }
36
+
37
+ return success(res, <%= resourceNameCamel %>);
38
+ } catch (error) {
39
+ next(error);
40
+ }
41
+ };
42
+
43
+ const create<%= resourceNamePascal %> = async (req, res, next) => {
44
+ try {
45
+ const data = req.body;
46
+
47
+ logger.info('Creating <%= resourceNameCamel %>', { data });
48
+ const <%= resourceNameCamel %> = await <%= resourceNameCamel %>Service.create<%= resourceNamePascal %>(data);
49
+
50
+ return created(res, <%= resourceNameCamel %>);
51
+ } catch (error) {
52
+ next(error);
53
+ }
54
+ };
55
+
56
+ const update<%= resourceNamePascal %> = async (req, res, next) => {
57
+ try {
58
+ const { id } = req.params;
59
+ const data = req.body;
60
+
61
+ const <%= resourceNameCamel %> = await <%= resourceNameCamel %>Service.update<%= resourceNamePascal %>(id, data);
62
+
63
+ if (!<%= resourceNameCamel %>) {
64
+ throw new AppError('<%= resourceNamePascal %> not found', 404);
65
+ }
66
+
67
+ return success(res, <%= resourceNameCamel %>);
68
+ } catch (error) {
69
+ next(error);
70
+ }
71
+ };
72
+
73
+ const delete<%= resourceNamePascal %> = async (req, res, next) => {
74
+ try {
75
+ const { id } = req.params;
76
+ const deleted = await <%= resourceNameCamel %>Service.delete<%= resourceNamePascal %>(id);
77
+
78
+ if (!deleted) {
79
+ throw new AppError('<%= resourceNamePascal %> not found', 404);
80
+ }
81
+
82
+ return success(res, { message: '<%= resourceNamePascal %> deleted successfully' });
83
+ } catch (error) {
84
+ next(error);
85
+ }
86
+ };
87
+
88
+ module.exports = {
89
+ getAll<%= resourceNamePascal %>s,
90
+ get<%= resourceNamePascal %>ById,
91
+ create<%= resourceNamePascal %>,
92
+ update<%= resourceNamePascal %>,
93
+ delete<%= resourceNamePascal %>
94
+ };
@@ -0,0 +1,88 @@
1
+ const express = require('express');
2
+ const <%= resourceNameCamel %>Controller = require('../controllers/<%= resourceNameCamel %>Controller');
3
+ <% if (hasAuth) { %>const { authenticate } = require('../middleware/auth');<% } %>
4
+
5
+ const router = express.Router();
6
+
7
+ <% if (hasAuth) { %>// Apply authentication middleware to all routes
8
+ router.use(authenticate);
9
+ <% } %>
10
+ /**
11
+ * @swagger
12
+ * /<%= resourceNamePlural %>:
13
+ * get:
14
+ * summary: Get all <%= resourceNamePlural %>
15
+ * tags: [<%= resourceNamePascal %>]
16
+ * responses:
17
+ * 200:
18
+ * description: List of <%= resourceNamePlural %>
19
+ */
20
+ router.get('/', <%= resourceNameCamel %>Controller.getAll<%= resourceNamePascal %>s);
21
+
22
+ /**
23
+ * @swagger
24
+ * /<%= resourceNamePlural %>/{id}:
25
+ * get:
26
+ * summary: Get a <%= resourceNameCamel %> by ID
27
+ * tags: [<%= resourceNamePascal %>]
28
+ * parameters:
29
+ * - in: path
30
+ * name: id
31
+ * required: true
32
+ * schema:
33
+ * type: string
34
+ * responses:
35
+ * 200:
36
+ * description: <%= resourceNamePascal %> details
37
+ */
38
+ router.get('/:id', <%= resourceNameCamel %>Controller.get<%= resourceNamePascal %>ById);
39
+
40
+ /**
41
+ * @swagger
42
+ * /<%= resourceNamePlural %>:
43
+ * post:
44
+ * summary: Create a new <%= resourceNameCamel %>
45
+ * tags: [<%= resourceNamePascal %>]
46
+ * responses:
47
+ * 201:
48
+ * description: Created <%= resourceNameCamel %>
49
+ */
50
+ router.post('/', <%= resourceNameCamel %>Controller.create<%= resourceNamePascal %>);
51
+
52
+ /**
53
+ * @swagger
54
+ * /<%= resourceNamePlural %>/{id}:
55
+ * put:
56
+ * summary: Update a <%= resourceNameCamel %>
57
+ * tags: [<%= resourceNamePascal %>]
58
+ * parameters:
59
+ * - in: path
60
+ * name: id
61
+ * required: true
62
+ * schema:
63
+ * type: string
64
+ * responses:
65
+ * 200:
66
+ * description: Updated <%= resourceNameCamel %>
67
+ */
68
+ router.put('/:id', <%= resourceNameCamel %>Controller.update<%= resourceNamePascal %>);
69
+
70
+ /**
71
+ * @swagger
72
+ * /<%= resourceNamePlural %>/{id}:
73
+ * delete:
74
+ * summary: Delete a <%= resourceNameCamel %>
75
+ * tags: [<%= resourceNamePascal %>]
76
+ * parameters:
77
+ * - in: path
78
+ * name: id
79
+ * required: true
80
+ * schema:
81
+ * type: string
82
+ * responses:
83
+ * 200:
84
+ * description: <%= resourceNamePascal %> deleted successfully
85
+ */
86
+ router.delete('/:id', <%= resourceNameCamel %>Controller.delete<%= resourceNamePascal %>);
87
+
88
+ module.exports = router;
@@ -0,0 +1,77 @@
1
+ const { createLogger } = require('../utils/logger');
2
+
3
+ const logger = createLogger('<%= resourceNamePascal %>Service');
4
+
5
+ /**
6
+ * Get all <%= resourceNamePlural %> with pagination
7
+ */
8
+ const getAll<%= resourceNamePascal %>s = async (page = 1, limit = 10) => {
9
+ logger.info('Fetching all <%= resourceNamePlural %>', { page, limit });
10
+
11
+ // TODO: Implement database logic here
12
+ // Example (Prisma): return prisma.<%= resourceNameCamel %>.findMany({ skip: (page - 1) * limit, take: limit });
13
+ // Example (Mongoose): return <%= resourceNamePascal %>.find().skip((page - 1) * limit).limit(limit);
14
+
15
+ return {
16
+ <%= resourceNamePlural %>: [],
17
+ total: 0,
18
+ totalPages: 0,
19
+ currentPage: page,
20
+ };
21
+ };
22
+
23
+ /**
24
+ * Get <%= resourceNameCamel %> by ID
25
+ */
26
+ const get<%= resourceNamePascal %>ById = async (id) => {
27
+ logger.info('Fetching <%= resourceNameCamel %> by ID', { id });
28
+
29
+ // TODO: Implement database logic here
30
+ // Example (Prisma): return prisma.<%= resourceNameCamel %>.findUnique({ where: { id } });
31
+
32
+ return null;
33
+ };
34
+
35
+ /**
36
+ * Create new <%= resourceNameCamel %>
37
+ */
38
+ const create<%= resourceNamePascal %> = async (data) => {
39
+ logger.info('Creating new <%= resourceNameCamel %>');
40
+
41
+ // TODO: Implement database logic here
42
+ // Example (Prisma): return prisma.<%= resourceNameCamel %>.create({ data });
43
+
44
+ return { id: 'new-id', ...data };
45
+ };
46
+
47
+ /**
48
+ * Update <%= resourceNameCamel %> by ID
49
+ */
50
+ const update<%= resourceNamePascal %> = async (id, data) => {
51
+ logger.info('Updating <%= resourceNameCamel %>', { id });
52
+
53
+ // TODO: Implement database logic here
54
+ // Example (Prisma): return prisma.<%= resourceNameCamel %>.update({ where: { id }, data });
55
+
56
+ return { id, ...data };
57
+ };
58
+
59
+ /**
60
+ * Delete <%= resourceNameCamel %> by ID
61
+ */
62
+ const delete<%= resourceNamePascal %> = async (id) => {
63
+ logger.info('Deleting <%= resourceNameCamel %>', { id });
64
+
65
+ // TODO: Implement database logic here
66
+ // Example (Prisma): await prisma.<%= resourceNameCamel %>.delete({ where: { id } });
67
+
68
+ return true;
69
+ };
70
+
71
+ module.exports = {
72
+ getAll<%= resourceNamePascal %>s,
73
+ get<%= resourceNamePascal %>ById,
74
+ create<%= resourceNamePascal %>,
75
+ update<%= resourceNamePascal %>,
76
+ delete<%= resourceNamePascal %>,
77
+ };
@@ -4,8 +4,19 @@ const crypto = require('crypto');
4
4
  <% } %>
5
5
  <% if (!hasRedis) { %>
6
6
  // In-memory token blacklist — replace with Redis for production.
7
- const tokenBlacklist = new Set();
7
+ const tokenBlacklist = new Map(); // token → expiresAt
8
8
  const resetTokens = new Map(); // token → { email, expiresAt }
9
+
10
+ // Periodic cleanup to prevent memory leaks
11
+ setInterval(() => {
12
+ const now = Date.now();
13
+ for (const [token, expiresAt] of tokenBlacklist.entries()) {
14
+ if (expiresAt < now) tokenBlacklist.delete(token);
15
+ }
16
+ for (const [hash, entry] of resetTokens.entries()) {
17
+ if (entry.expiresAt < now) resetTokens.delete(hash);
18
+ }
19
+ }, 60 * 60 * 1000).unref(); // Run every hour
9
20
  <% } %>
10
21
  const generateTokens = (user) => {
11
22
  const payload = {
@@ -66,10 +77,17 @@ const consumeResetToken = async (token) => {
66
77
  };
67
78
  <% } else { %>
68
79
  const blacklistToken = (token) => {
69
- tokenBlacklist.add(token);
80
+ const decoded = jwt.decode(token);
81
+ const expiresAt = decoded && decoded.exp ? decoded.exp * 1000 : Date.now() + 86400000;
82
+ tokenBlacklist.set(token, expiresAt);
70
83
  };
71
84
 
72
85
  const isTokenBlacklisted = (token) => {
86
+ const expiresAt = tokenBlacklist.get(token);
87
+ if (expiresAt && expiresAt < Date.now()) {
88
+ tokenBlacklist.delete(token);
89
+ return false;
90
+ }
73
91
  return tokenBlacklist.has(token);
74
92
  };
75
93
 
@@ -58,12 +58,13 @@ const findAll = async ({ page = 1, limit = 20 } = {}) => {
58
58
  const skip = (page - 1) * limit;
59
59
  const [users, total] = await Promise.all([
60
60
  prisma.user.findMany({
61
- select: { id: true, username: true, email: true, role: true, createdAt: true, updatedAt: true },
61
+ <% if (hasSoftDelete) { %> where: { deletedAt: null },
62
+ <% } %> select: { id: true, username: true, email: true, role: true, createdAt: true, updatedAt: true },
62
63
  orderBy: { createdAt: 'desc' },
63
64
  skip,
64
65
  take: limit,
65
66
  }),
66
- prisma.user.count(),
67
+ prisma.user.count(<% if (hasSoftDelete) { %>{ where: { deletedAt: null } }<% } %>),
67
68
  ]);
68
69
  return { users, total, page, totalPages: Math.ceil(total / limit) };
69
70
  };
package/lib/ai-cli.js DELETED
@@ -1,133 +0,0 @@
1
- /**
2
- * AI CLI handler — interprets natural language project descriptions
3
- * and maps them to express-genix configuration flags.
4
- *
5
- * Requires @langchain/openai or @langchain/anthropic to be installed.
6
- * These are NOT bundled with express-genix to keep the CLI lightweight.
7
- *
8
- * Install with: npm install -g @langchain/core @langchain/openai
9
- */
10
-
11
- const SYSTEM_PROMPT = `You are an expert Express.js project configurator for the express-genix CLI tool.
12
- Given a natural language description of a web application, output a JSON configuration object.
13
-
14
- Available options:
15
- - projectName: string (lowercase, hyphens only, no spaces)
16
- - language: "javascript" | "typescript"
17
- - db: "mongodb" | "postgresql" | "prisma" | "none"
18
- - logger: "winston" | "pino"
19
- - features: array of feature strings from this list:
20
- - "auth" (JWT authentication — requires a database)
21
- - "rateLimit" (rate limiting)
22
- - "swagger" (Swagger/OpenAPI docs)
23
- - "redis" (Redis token blacklist — requires auth)
24
- - "docker" (Docker & Docker Compose)
25
- - "cicd" (GitHub Actions CI/CD)
26
- - "websocket" (WebSocket via Socket.io)
27
- - "requestId" (request ID / correlation tracking)
28
- - "email" (Nodemailer email service — requires database)
29
- - "fileUpload" (Multer file uploads)
30
- - "softDelete" (soft deletes — requires database + auth)
31
- - "auditLog" (audit logging middleware)
32
- - "metrics" (Prometheus metrics)
33
- - "apiVersioning" (API versioning /api/v1)
34
- - "backgroundJobs" (BullMQ background jobs)
35
- - "graphql" (GraphQL via Apollo Server)
36
- - "mcp" (MCP Server — exposes API as AI-agent tools)
37
- - "ai" (LangChain/LangGraph AI service)
38
-
39
- Rules:
40
- - Always include "auth", "rateLimit", "swagger", "requestId" unless explicitly unwanted
41
- - Include "docker" for production-ready apps
42
- - Include "ai" if the description mentions AI, chatbot, LLM, or intelligence
43
- - Include "mcp" if the description mentions AI agents, tools, or MCP
44
- - Choose "prisma" for modern stacks, "mongodb" for documents, "postgresql" for relational
45
- - Infer a short kebab-case project name from the description
46
- - Output ONLY valid JSON. No explanation, no markdown fences.`;
47
-
48
- const runAiCommand = async (description) => {
49
- // Check for API key
50
- const hasOpenAI = !!process.env.OPENAI_API_KEY;
51
- const hasAnthropic = !!process.env.ANTHROPIC_API_KEY;
52
- const hasGemini = !!process.env.GOOGLE_API_KEY;
53
-
54
- if (!hasOpenAI && !hasAnthropic && !hasGemini) {
55
- console.error('❌ The AI command requires an API key.');
56
- console.error(' Set one of these environment variables:');
57
- console.error(' export OPENAI_API_KEY=sk-...');
58
- console.error(' export ANTHROPIC_API_KEY=sk-ant-...');
59
- console.error(' export GOOGLE_API_KEY=AI...');
60
- process.exit(1);
61
- }
62
-
63
- // Try to load LangChain (not bundled — must be installed separately)
64
- let ChatModel;
65
- try {
66
- if (hasGemini) {
67
- const { ChatGoogleGenerativeAI } = require('@langchain/google-genai');
68
- ChatModel = new ChatGoogleGenerativeAI({
69
- model: 'gemini-2.0-flash',
70
- maxOutputTokens: 1024,
71
- apiKey: process.env.GOOGLE_API_KEY,
72
- });
73
- } else if (hasAnthropic) {
74
- const { ChatAnthropic } = require('@langchain/anthropic');
75
- ChatModel = new ChatAnthropic({
76
- modelName: 'claude-sonnet-4-20250514',
77
- maxTokens: 1024,
78
- anthropicApiKey: process.env.ANTHROPIC_API_KEY,
79
- });
80
- } else {
81
- const { ChatOpenAI } = require('@langchain/openai');
82
- ChatModel = new ChatOpenAI({
83
- modelName: 'gpt-4o-mini',
84
- maxTokens: 1024,
85
- openAIApiKey: process.env.OPENAI_API_KEY,
86
- });
87
- }
88
- } catch {
89
- console.error('❌ LangChain packages not found.');
90
- console.error(' Install them globally to use the AI command:');
91
- console.error(' npm install -g @langchain/core @langchain/openai @langchain/anthropic @langchain/google-genai');
92
- process.exit(1);
93
- }
94
-
95
- const { HumanMessage, SystemMessage } = require('@langchain/core/messages');
96
-
97
- console.log('🤖 Analyzing your description...\n');
98
-
99
- const response = await ChatModel.invoke([
100
- new SystemMessage(SYSTEM_PROMPT),
101
- new HumanMessage(description),
102
- ]);
103
-
104
- let config;
105
- try {
106
- const jsonMatch = response.content.match(/\{[\s\S]*\}/);
107
- if (!jsonMatch) throw new Error('No JSON found in response');
108
- config = JSON.parse(jsonMatch[0]);
109
- } catch {
110
- console.error('❌ Could not parse AI response. Try a clearer description.');
111
- console.error(' Raw output:', response.content);
112
- process.exit(1);
113
- }
114
-
115
- // Validate required fields
116
- if (!config.projectName || !config.language || !config.db) {
117
- console.error('❌ AI response missing required fields. Try again.');
118
- process.exit(1);
119
- }
120
-
121
- // Display interpreted config
122
- console.log('📋 Interpreted configuration:\n');
123
- console.log(` Project: ${config.projectName}`);
124
- console.log(` Language: ${config.language}`);
125
- console.log(` Database: ${config.db}`);
126
- console.log(` Logger: ${config.logger || 'winston'}`);
127
- console.log(` Features: ${(config.features || []).join(', ') || 'base'}`);
128
- console.log('');
129
-
130
- return config;
131
- };
132
-
133
- module.exports = { runAiCommand };