novatec-cli 2.0.0 → 3.0.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/lib/create.js CHANGED
@@ -1,378 +1,288 @@
1
1
  import path from 'path';
2
2
  import chalk from 'chalk';
3
3
  import fse from 'fs-extra';
4
- import ora from 'ora';
5
- import { execSync, spawnSync } from 'child_process';
4
+ import boxen from 'boxen';
5
+ import { spawnSync } from 'child_process';
6
6
  import { askProjectOptions } from './prompts.js';
7
7
  import { generateFrontend } from './generators/frontend.js';
8
8
  import { generateBackend } from './generators/backend.js';
9
- import { generateReadme } from './generators/readme.js';
10
- import { run, isInstalled } from './utils.js';
11
- import boxen from 'boxen';
12
-
13
- // ── PROGRESO ──────────────────────────────────────────────────────────────────
14
- function progressBar(current, total, label) {
15
- const width = 30;
16
- const filled = Math.round((current / total) * width);
17
- const empty = width - filled;
18
- const pct = Math.round((current / total) * 100);
19
- const bar = chalk.white('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
20
- process.stdout.write(`\r [${bar}] ${chalk.white(pct + '%')} ${chalk.gray(label)} `);
9
+ import { generateBusiness } from './generators/business.js';
10
+ import { generateDocker } from './generators/docker.js';
11
+ import { generateSecurity } from './generators/security.js';
12
+ import { generateDeploy } from './generators/deploy.js';
13
+ import { isInstalled, runCmd } from './utils.js';
14
+
15
+ function bar(cur, tot, label) {
16
+ const w = 28, f = Math.round((cur / tot) * w);
17
+ const pct = Math.round((cur / tot) * 100);
18
+ process.stdout.write(`\r [${'█'.repeat(f)}${'░'.repeat(w - f)}] ${chalk.white(pct + '%')} ${chalk.gray(label.substring(0, 30))} `);
21
19
  }
22
20
 
23
- // ── MAIN ──────────────────────────────────────────────────────────────────────
24
21
  export async function runCreate(nameArg, options) {
25
- const startTime = Date.now();
22
+ const t0 = Date.now();
26
23
  const config = await askProjectOptions(nameArg, options);
27
- const projectPath = path.resolve(process.cwd(), config.name);
28
-
29
- // Asegurar carpeta
30
- await fse.ensureDir(projectPath);
24
+ const root = path.resolve(process.cwd(), config.name);
25
+ await fse.ensureDir(root);
31
26
 
32
- const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
33
- const isNode = !isPython && config.backend !== 'spring' && config.backend !== 'gin';
34
-
35
- // Calcular pasos totales
36
27
  const extras = config.extras || [];
37
- const steps = [
38
- 'Generando frontend',
39
- 'Generando backend',
40
- 'Generando README',
41
- extras.includes('env') && 'Archivos .env',
42
- extras.includes('editorconfig') && '.editorconfig',
43
- extras.includes('eslint') && 'ESLint + Prettier',
44
- extras.includes('husky') && 'Husky',
45
- extras.includes('testing') && 'Vitest',
46
- extras.includes('ci') && 'GitHub Actions',
47
- extras.includes('docker') && 'Docker',
48
- extras.includes('auth') && 'Auth JWT',
49
- extras.includes('tailwind') && 'Tailwind CSS',
50
- extras.includes('git') && 'Git init',
51
- config.frontendDeps?.length && 'Instalando deps frontend',
52
- config.backendDeps?.length && 'Instalando deps backend',
28
+ const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
29
+ const pm = config.pm || 'npm';
30
+
31
+ // Calcular pasos
32
+ const steps = ['Frontend', 'Backend', 'README', 'Env files',
33
+ config.mode === 'business' && 'Business mode',
34
+ extras.includes('docker') && 'Docker',
35
+ extras.includes('security') && 'Seguridad',
36
+ extras.includes('ci') && 'GitHub Actions',
37
+ extras.includes('eslint') && 'ESLint',
38
+ extras.includes('testing') && 'Testing',
39
+ extras.includes('swagger') && 'Swagger',
40
+ extras.includes('logs') && 'Logs',
41
+ extras.includes('seo') && 'SEO',
42
+ extras.includes('git') && 'Git',
43
+ config.install && 'Instalando deps',
53
44
  ].filter(Boolean);
54
45
 
55
46
  console.log();
56
47
  let step = 0;
57
-
58
- const tick = (label) => {
59
- step++;
60
- progressBar(step, steps.length, label);
61
- };
48
+ const tick = (label) => { step++; bar(step, steps.length, label); };
62
49
 
63
50
  // 1. Frontend
64
51
  tick('Generando frontend...');
65
- await generateFrontend(config, projectPath);
52
+ await generateFrontend(config, root);
53
+ await connectFrontendToBackend(config, root);
66
54
 
67
- // 2. Backend
55
+ // 2. Backend con arquitectura
68
56
  tick('Generando backend...');
69
- await generateBackend(config, projectPath);
57
+ await generateBackend(config, root);
58
+ await applyArchitecture(config, root);
70
59
 
71
- // 3. README personalizado
72
- tick('Generando README...');
73
- await generateCustomReadme(config, projectPath);
60
+ // 3. README
61
+ tick('README...');
62
+ await generateReadme(config, root);
74
63
 
75
- // 4. Extras
76
- if (extras.includes('env')) {
77
- tick('Archivos .env...');
78
- await generateEnvFiles(config, projectPath);
79
- }
80
- if (extras.includes('editorconfig')) {
81
- tick('.editorconfig...');
82
- await fse.writeFile(path.join(projectPath, '.editorconfig'), EDITORCONFIG);
83
- }
84
- if (extras.includes('eslint')) {
85
- tick('ESLint + Prettier...');
86
- await setupEslint(config, projectPath);
64
+ // 4. .env
65
+ tick('.env files...');
66
+ await generateEnv(config, root);
67
+
68
+ // 5. Business mode
69
+ if (config.mode === 'business') {
70
+ tick('Business mode...');
71
+ await generateBusiness(config, root);
87
72
  }
88
- if (extras.includes('husky')) {
89
- tick('Husky...');
90
- await setupHusky(config, projectPath);
73
+
74
+ // 6. Docker
75
+ if (extras.includes('docker')) {
76
+ tick('Docker...');
77
+ await generateDocker(config, root);
91
78
  }
92
- if (extras.includes('testing')) {
93
- tick('Vitest...');
94
- await setupTesting(config, projectPath);
79
+
80
+ // 7. Seguridad
81
+ if (extras.includes('security')) {
82
+ tick('Seguridad...');
83
+ await generateSecurity(config, root);
95
84
  }
85
+
86
+ // 8. GitHub Actions
96
87
  if (extras.includes('ci')) {
97
88
  tick('GitHub Actions...');
98
- await setupCI(config, projectPath);
89
+ await generateDeploy(config, root);
99
90
  }
100
- if (extras.includes('docker')) {
101
- tick('Docker...');
102
- await generateDocker(config, projectPath);
91
+
92
+ // 9. ESLint
93
+ if (extras.includes('eslint')) {
94
+ tick('ESLint...');
95
+ await setupEslint(root);
103
96
  }
104
- if (extras.includes('auth')) {
105
- tick('Auth JWT...');
106
- await setupAuth(config, projectPath);
97
+
98
+ // 10. Testing
99
+ if (extras.includes('testing')) {
100
+ tick('Testing...');
101
+ await setupTesting(root);
107
102
  }
108
- if (extras.includes('tailwind')) {
109
- tick('Tailwind CSS...');
110
- await setupTailwind(config, projectPath);
103
+
104
+ // 11. Swagger (ya incluido en security/backend, solo notificar)
105
+ if (extras.includes('swagger')) {
106
+ tick('Swagger...');
107
+ // generado en generateSecurity
111
108
  }
112
- if (extras.includes('git') && isInstalled('git')) {
113
- tick('Git init...');
114
- try { run('git init', projectPath, 'git init'); } catch {}
115
- await fse.writeFile(path.join(projectPath, '.gitignore'), GITIGNORE);
109
+
110
+ // 12. Logs
111
+ if (extras.includes('logs')) {
112
+ tick('Logs...');
113
+ await setupLogs(config, root);
116
114
  }
117
115
 
118
- // 5. Instalar dependencias automáticamente
119
- if (config.frontendDeps?.length && isInstalled('npm')) {
120
- tick('Instalando deps frontend...');
121
- const frontendPath = path.join(projectPath, 'frontend');
122
- try {
123
- spawnSync('npm', ['install', ...config.frontendDeps], { cwd: frontendPath, shell: true, stdio: 'pipe' });
124
- } catch {}
116
+ // 13. SEO
117
+ if (extras.includes('seo')) {
118
+ tick('SEO...');
119
+ await setupSEO(config, root);
125
120
  }
126
- if (config.backendDeps?.length && isNode && isInstalled('npm')) {
127
- tick('Instalando deps backend...');
128
- const backendPath = path.join(projectPath, 'backend');
121
+
122
+ // 14. Git
123
+ if (extras.includes('git') && isInstalled('git')) {
124
+ tick('Git init...');
129
125
  try {
130
- spawnSync('npm', ['install', ...config.backendDeps], { cwd: backendPath, shell: true, stdio: 'pipe' });
126
+ spawnSync('git', ['init'], { cwd: root, shell: true, stdio: 'pipe' });
127
+ await fse.writeFile(path.join(root, '.gitignore'), GITIGNORE);
131
128
  } catch {}
132
129
  }
133
130
 
134
- // Barra al 100%
135
- progressBar(steps.length, steps.length, 'Completado');
136
- console.log('\n');
137
-
138
- // Tiempo total
139
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
131
+ // 15. Instalar deps
132
+ if (config.install && isInstalled(pm)) {
133
+ tick('Instalando dependencias...');
134
+ const fe = path.join(root, 'frontend');
135
+ const be = path.join(root, 'backend');
136
+ const installCmd = pm === 'yarn' ? 'yarn' : `${pm} install`;
137
+ try { spawnSync(installCmd, { cwd: fe, shell: true, stdio: 'pipe' }); } catch {}
138
+ if (!isPython) {
139
+ try { spawnSync(installCmd, { cwd: be, shell: true, stdio: 'pipe' }); } catch {}
140
+ }
141
+ }
140
142
 
141
- // Resumen final
142
- const backendCmd = {
143
- express: 'npm run dev', nestjs: 'npm run start:dev', fastify: 'npm run dev',
144
- hono: 'npm run dev', fastapi: 'uvicorn main:app --reload', django: 'python manage.py runserver',
145
- flask: 'python run.py', spring: 'mvn spring-boot:run', deno: 'deno task dev', gin: 'go run main.go',
146
- };
143
+ bar(steps.length, steps.length, 'Completado');
144
+ console.log('\n');
147
145
 
148
- console.log(
149
- boxen(
150
- chalk.bold.white(' ✔ PROYECTO LISTO\n\n') +
151
- chalk.gray(' Nombre │ ') + chalk.white(config.name) + '\n' +
152
- chalk.gray(' Frontend │ ') + chalk.white(config.frontend) + (config.typescript ? chalk.gray(' + TypeScript') : '') + '\n' +
153
- chalk.gray(' Backend │ ') + chalk.white(config.backend) + '\n' +
154
- chalk.gray(' Base datos │ ') + chalk.white(config.db || 'ninguna') + '\n' +
155
- chalk.gray(' Tiempo │ ') + chalk.white(elapsed + 's') + '\n\n' +
156
- chalk.gray(' ─────────────────────────────────────\n\n') +
157
- chalk.white(' cd ' + config.name + '/frontend → npm run dev\n') +
158
- chalk.white(' cd ' + config.name + '/backend → ' + (backendCmd[config.backend] || 'npm start')),
159
- { padding: 1, margin: { top: 1, left: 2 }, borderStyle: 'round', borderColor: 'white', dimBorder: true }
160
- )
161
- );
162
-
163
- // Abrir VS Code si está instalado
146
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
147
+ const beCmd = { express:'npm run dev', nestjs:'npm run start:dev', fastify:'npm run dev', hono:'npm run dev', fastapi:'uvicorn main:app --reload', django:'python manage.py runserver', flask:'python run.py', spring:'mvn spring-boot:run', deno:'deno task dev', gin:'go run main.go' };
148
+
149
+ console.log(boxen(
150
+ chalk.bold.white(' ✔ PROYECTO LISTO — NovaTec CLI\n\n') +
151
+ chalk.gray(' Nombre │ ') + chalk.white(config.name) + '\n' +
152
+ chalk.gray(' Modo │ ') + chalk.white(config.mode || 'estándar') + '\n' +
153
+ chalk.gray(' Frontend │ ') + chalk.white(config.frontend) + (config.typescript ? chalk.gray(' + TypeScript') : '') + '\n' +
154
+ chalk.gray(' Backend │ ') + chalk.white(config.backend) + '\n' +
155
+ chalk.gray(' Base datos │ ') + chalk.white(config.db || 'ninguna') + '\n' +
156
+ chalk.gray(' Arquitectura ') + chalk.white(config.architecture || 'mvc') + '\n' +
157
+ chalk.gray(' Tiempo │ ') + chalk.white(elapsed + 's') + '\n\n' +
158
+ chalk.gray(' ─────────────────────────────────────────────\n\n') +
159
+ chalk.white(' # Frontend\n') +
160
+ chalk.gray(` cd ${config.name}/frontend\n`) +
161
+ chalk.gray(` ${pm} ${pm === 'yarn' ? '' : 'run '}dev\n\n`) +
162
+ chalk.white(' # Backend\n') +
163
+ chalk.gray(` cd ${config.name}/backend\n`) +
164
+ chalk.gray(` ${beCmd[config.backend] || pm + ' run dev'}\n`) +
165
+ (config.mode === 'business' ? '\n' + chalk.gray(' # Dashboard → http://localhost:5173/dashboard\n') + chalk.gray(' # API Docs → http://localhost:3001/api-docs') : ''),
166
+ { padding: 1, margin: { top: 1, left: 2 }, borderStyle: 'round', borderColor: 'white', dimBorder: true }
167
+ ));
168
+
169
+ // Abrir VS Code
164
170
  if (isInstalled('code')) {
165
- try {
166
- spawnSync('code', [projectPath], { shell: true, stdio: 'ignore', detached: true });
167
- console.log(' ' + chalk.gray('VS Code abierto automáticamente ✔'));
168
- } catch {}
171
+ try { spawnSync('code', [root], { shell: true, stdio: 'ignore', detached: true }); } catch {}
172
+ console.log(' ' + chalk.gray('VS Code abierto '));
169
173
  }
170
-
171
174
  console.log();
172
175
  }
173
176
 
174
- // ── README PERSONALIZADO ──────────────────────────────────────────────────────
175
- async function generateCustomReadme(config, projectPath) {
176
- const ts = config.typescript ? ' + TypeScript' : '';
177
- const dbSection = config.db && config.db !== 'none'
178
- ? `\n## Base de datos\n\n**${config.db}** — Configura la conexión en \`backend/.env\`\n` : '';
179
-
180
- const content = `# ${config.name}
181
-
182
- > Proyecto generado con [NOVATEC CLI](https://www.npmjs.com/package/novatec-cli)
183
-
184
- ## Stack
185
-
186
- | Capa | Tecnología |
187
- |------|-----------|
188
- | Frontend | **${config.frontend}**${ts} |
189
- | Backend | **${config.backend}**${ts} |
190
- | Base de datos | **${config.db || 'No configurada'}** |
191
- ${dbSection}
192
- ## Inicio rápido
193
-
194
- \`\`\`bash
195
- # Frontend
196
- cd ${config.name}/frontend
197
- npm install
198
- npm run dev
199
-
200
- # Backend
201
- cd ${config.name}/backend
202
- ${['fastapi','django','flask'].includes(config.backend) ? 'pip install -r requirements.txt\n' : 'npm install\n'}\
203
- ${config.backend === 'fastapi' ? 'uvicorn main:app --reload' :
204
- config.backend === 'django' ? 'python manage.py runserver' :
205
- config.backend === 'flask' ? 'python run.py' :
206
- config.backend === 'spring' ? 'mvn spring-boot:run' :
207
- config.backend === 'gin' ? 'go run main.go' : 'npm run dev'}
208
- \`\`\`
209
- ${config.extras?.includes('docker') ? '\n## Docker\n\n```bash\ndocker-compose up --build\n```\n' : ''}
210
- ## Generado con
211
-
212
- \`\`\`
213
- novatec create ${config.name}
214
- \`\`\`
215
-
216
- ---
217
- *Generado el ${new Date().toLocaleDateString('es-MX')} con NOVATEC CLI*
218
- `;
219
- await fse.writeFile(path.join(projectPath, 'README.md'), content);
220
- }
221
-
222
- // ── AUTH JWT ──────────────────────────────────────────────────────────────────
223
- async function setupAuth(config, projectPath) {
224
- const backendPath = path.join(projectPath, 'backend');
177
+ // ── CONEXIÓN FE → BE ──────────────────────────────────────────────────────────
178
+ async function connectFrontendToBackend(config, root) {
179
+ const feDir = path.join(root, 'frontend');
225
180
  const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
181
+ const bePort = isPython ? '8000' : '3001';
182
+
183
+ // Archivo de API client
184
+ const apiClient = config.db === 'supabase'
185
+ ? `import { createClient } from '@supabase/supabase-js';\n\nexport const supabase = createClient(\n import.meta.env.VITE_SUPABASE_URL,\n import.meta.env.VITE_SUPABASE_ANON_KEY\n);\n\nexport const api = {\n get: (url) => fetch(import.meta.env.VITE_API_URL + url).then(r => r.json()),\n post: (url, data) => fetch(import.meta.env.VITE_API_URL + url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).then(r => r.json()),\n put: (url, data) => fetch(import.meta.env.VITE_API_URL + url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).then(r => r.json()),\n delete: (url) => fetch(import.meta.env.VITE_API_URL + url, { method: 'DELETE' }).then(r => r.json()),\n};\n`
186
+ : `const BASE = import.meta.env.VITE_API_URL || 'http://localhost:${bePort}';\n\nconst headers = () => ({\n 'Content-Type': 'application/json',\n ...(localStorage.getItem('token') ? { Authorization: \`Bearer \${localStorage.getItem('token')}\` } : {}),\n});\n\nexport const api = {\n get: (url) => fetch(BASE + url, { headers: headers() }).then(r => r.json()),\n post: (url, data) => fetch(BASE + url, { method: 'POST', headers: headers(), body: JSON.stringify(data) }).then(r => r.json()),\n put: (url, data) => fetch(BASE + url, { method: 'PUT', headers: headers(), body: JSON.stringify(data) }).then(r => r.json()),\n delete: (url) => fetch(BASE + url, { method: 'DELETE', headers: headers() }).then(r => r.json()),\n};\n`;
187
+
188
+ const srcDir = path.join(feDir, 'src', 'lib');
189
+ await fse.ensureDir(srcDir);
190
+ await fse.writeFile(path.join(srcDir, 'api.js'), apiClient);
191
+
192
+ // Proxy en vite.config si aplica
193
+ const viteConfig = path.join(feDir, 'vite.config.js');
194
+ if (await fse.pathExists(viteConfig)) {
195
+ const content = await fse.readFile(viteConfig, 'utf8');
196
+ if (!content.includes('proxy')) {
197
+ const proxy = `\n server: {\n proxy: {\n '/api': {\n target: 'http://localhost:${bePort}',\n changeOrigin: true,\n },\n },\n },`;
198
+ await fse.writeFile(viteConfig, content.replace('plugins:', proxy + '\n plugins:'));
199
+ }
200
+ }
201
+ }
226
202
 
227
- if (isPython) return; // Django/FastAPI ya tienen su propio auth
228
-
229
- const authDir = path.join(backendPath, 'src', 'auth');
230
- await fse.ensureDir(authDir);
203
+ // ── ARQUITECTURA ──────────────────────────────────────────────────────────────
204
+ async function applyArchitecture(config, root) {
205
+ const beDir = path.join(root, 'backend', 'src');
206
+ const arch = config.architecture || 'mvc';
231
207
 
232
- const middleware = `import jwt from 'jsonwebtoken';
208
+ const dirs = {
209
+ mvc: ['controllers', 'models', 'routes', 'middlewares', 'services'],
210
+ hexagonal: ['domain/entities', 'domain/ports', 'application/usecases', 'infrastructure/adapters', 'infrastructure/repositories', 'interfaces/http'],
211
+ clean: ['domain/entities', 'domain/usecases', 'data/repositories', 'data/datasources', 'presentation/controllers', 'presentation/routes'],
212
+ };
233
213
 
234
- export function authMiddleware(req, res, next) {
235
- const token = req.headers.authorization?.split(' ')[1];
236
- if (!token) return res.status(401).json({ message: 'Token requerido' });
237
- try {
238
- req.user = jwt.verify(token, process.env.JWT_SECRET || 'change_me');
239
- next();
240
- } catch {
241
- res.status(401).json({ message: 'Token inválido' });
214
+ for (const dir of (dirs[arch] || dirs.mvc)) {
215
+ await fse.ensureDir(path.join(beDir, dir));
216
+ await fse.writeFile(path.join(beDir, dir, '.gitkeep'), '');
242
217
  }
243
218
  }
244
- `;
245
- const authRoutes = `import { Router } from 'express';
246
- import jwt from 'jsonwebtoken';
247
- import bcrypt from 'bcryptjs';
248
-
249
- const router = Router();
250
-
251
- // POST /api/auth/register
252
- router.post('/register', async (req, res) => {
253
- const { email, password } = req.body;
254
- const hash = await bcrypt.hash(password, 10);
255
- // TODO: guardar usuario en BD
256
- const token = jwt.sign({ email }, process.env.JWT_SECRET || 'change_me', { expiresIn: '7d' });
257
- res.json({ token });
258
- });
259
-
260
- // POST /api/auth/login
261
- router.post('/login', async (req, res) => {
262
- const { email, password } = req.body;
263
- // TODO: buscar usuario en BD y verificar hash
264
- const token = jwt.sign({ email }, process.env.JWT_SECRET || 'change_me', { expiresIn: '7d' });
265
- res.json({ token });
266
- });
267
-
268
- export default router;
269
- `;
270
-
271
- await fse.writeFile(path.join(authDir, 'middleware.js'), middleware);
272
- await fse.writeFile(path.join(authDir, 'routes.js'), authRoutes);
273
-
274
- // Instalar deps de auth
275
- try {
276
- spawnSync('npm', ['install', 'jsonwebtoken', 'bcryptjs'], { cwd: backendPath, shell: true, stdio: 'pipe' });
277
- } catch {}
278
- }
279
219
 
280
- // ── TAILWIND AUTO ─────────────────────────────────────────────────────────────
281
- async function setupTailwind(config, projectPath) {
282
- const frontendPath = path.join(projectPath, 'frontend');
283
- const tailwindConfig = `/** @type {import('tailwindcss').Config} */
284
- export default {
285
- content: ['./index.html', './src/**/*.{js,ts,jsx,tsx,vue,svelte}'],
286
- theme: { extend: {} },
287
- plugins: [],
288
- }
289
- `;
290
- const postcssConfig = `export default {
291
- plugins: { tailwindcss: {}, autoprefixer: {} },
292
- }
293
- `;
294
- await fse.writeFile(path.join(frontendPath, 'tailwind.config.js'), tailwindConfig);
295
- await fse.writeFile(path.join(frontendPath, 'postcss.config.js'), postcssConfig);
296
-
297
- // Agregar imports al CSS principal
298
- const cssPath = path.join(frontendPath, 'src', 'index.css');
299
- const tailwindImports = `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n`;
300
- if (await fse.pathExists(cssPath)) {
301
- const existing = await fse.readFile(cssPath, 'utf8');
302
- await fse.writeFile(cssPath, tailwindImports + existing);
303
- } else {
304
- await fse.ensureDir(path.join(frontendPath, 'src'));
305
- await fse.writeFile(cssPath, tailwindImports);
220
+ // ── ENV ───────────────────────────────────────────────────────────────────────
221
+ async function generateEnv(config, root) {
222
+ const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
223
+ const dbUrl = {
224
+ postgres: 'postgresql://user:password@localhost:5432/mydb',
225
+ mysql: 'mysql://user:password@localhost:3306/mydb',
226
+ mongo: 'mongodb://localhost:27017/mydb',
227
+ sqlite: 'file:./dev.db',
228
+ supabase: config.supabaseUrl || 'https://your-project.supabase.co',
229
+ }[config.db] || '';
230
+
231
+ const feEnv = `VITE_API_URL=http://localhost:${isPython ? '8000' : '3001'}\n` +
232
+ (config.db === 'supabase' ? `VITE_SUPABASE_URL=${config.supabaseUrl || 'https://your-project.supabase.co'}\nVITE_SUPABASE_ANON_KEY=${config.supabaseKey || 'your-anon-key'}\n` : '');
233
+
234
+ const beEnv = isPython ? `DATABASE_URL=${dbUrl}\nSECRET_KEY=change_me_in_production\nDEBUG=True\n`
235
+ : `PORT=3001\nNODE_ENV=development\nJWT_SECRET=change_me_in_production\n${dbUrl ? 'DATABASE_URL=' + dbUrl + '\n' : ''}` +
236
+ (config.db === 'supabase' ? `SUPABASE_URL=${config.supabaseUrl || ''}\nSUPABASE_KEY=${config.supabaseKey || ''}\n` : '') +
237
+ (config.extras?.includes('stripe') ? 'STRIPE_SECRET_KEY=sk_test_...\nSTRIPE_WEBHOOK_SECRET=whsec_...\n' : '');
238
+
239
+ await fse.writeFile(path.join(root, 'frontend', '.env.example'), feEnv);
240
+ await fse.writeFile(path.join(root, 'frontend', '.env'), feEnv);
241
+ if (!isPython) {
242
+ await fse.writeFile(path.join(root, 'backend', '.env.example'), beEnv);
243
+ await fse.writeFile(path.join(root, 'backend', '.env'), beEnv);
306
244
  }
307
-
308
- try {
309
- spawnSync('npm', ['install', '-D', 'tailwindcss', 'postcss', 'autoprefixer'], { cwd: frontendPath, shell: true, stdio: 'pipe' });
310
- } catch {}
311
245
  }
312
246
 
313
- // ── ESLint ────────────────────────────────────────────────────────────────────
314
- async function setupEslint(config, projectPath) {
315
- const frontendPath = path.join(projectPath, 'frontend');
316
- await fse.writeFile(path.join(frontendPath, '.eslintrc.json'), `{"env":{"browser":true,"es2021":true},"extends":["eslint:recommended"],"parserOptions":{"ecmaVersion":"latest","sourceType":"module"},"rules":{"no-unused-vars":"warn"}}\n`);
317
- await fse.writeFile(path.join(frontendPath, '.prettierrc'), `{"semi":true,"singleQuote":true,"tabWidth":2,"trailingComma":"es5"}\n`);
318
- try { spawnSync('npm', ['install', '-D', 'eslint', 'prettier'], { cwd: frontendPath, shell: true, stdio: 'pipe' }); } catch {}
247
+ // ── README ────────────────────────────────────────────────────────────────────
248
+ async function generateReadme(config, root) {
249
+ const ts = config.typescript ? ' + TypeScript' : '';
250
+ const content = `# ${config.name}\n\n> Generado con [NovaTec CLI](https://www.npmjs.com/package/novatec-cli) — Enterprise Fullstack Generator\n\n## Stack\n\n| Capa | Tecnología |\n|------|------------|\n| Frontend | **${config.frontend}**${ts} |\n| Backend | **${config.backend}** |\n| Base de datos | **${config.db || 'N/A'}** |\n| Arquitectura | **${config.architecture || 'mvc'}** |\n| Modo | **${config.mode || 'estándar'}** |\n\n## Inicio rápido\n\n\`\`\`bash\n# Frontend\ncd ${config.name}/frontend && npm install && npm run dev\n\n# Backend\ncd ${config.name}/backend && npm install && npm run dev\n\`\`\`\n${config.mode === 'business' ? '\n## Rutas del sistema\n\n| Ruta | Descripción |\n|------|-------------|\n| `/` | Landing page |\n| `/login` | Autenticación |\n| `/register` | Registro |\n| `/dashboard` | Panel administrativo |\n| `/api-docs` | Swagger UI |\n' : ''}\n---\n*Generado el ${new Date().toLocaleDateString('es-MX')} con NovaTec CLI v3.0.0*\n`;
251
+ await fse.writeFile(path.join(root, 'README.md'), content);
319
252
  }
320
253
 
321
- // ── Husky ─────────────────────────────────────────────────────────────────────
322
- async function setupHusky(config, projectPath) {
323
- const frontendPath = path.join(projectPath, 'frontend');
324
- try {
325
- spawnSync('npm', ['install', '-D', 'husky', 'lint-staged'], { cwd: frontendPath, shell: true, stdio: 'pipe' });
326
- spawnSync('npx', ['husky', 'init'], { cwd: frontendPath, shell: true, stdio: 'pipe' });
327
- await fse.ensureDir(path.join(frontendPath, '.husky'));
328
- await fse.writeFile(path.join(frontendPath, '.husky', 'pre-commit'), '#!/bin/sh\nnpx lint-staged\n');
329
- } catch {}
254
+ // ── ESLINT ────────────────────────────────────────────────────────────────────
255
+ async function setupEslint(root) {
256
+ const fe = path.join(root, 'frontend');
257
+ await fse.writeFile(path.join(fe, '.eslintrc.json'), JSON.stringify({ env: { browser: true, es2021: true }, extends: ['eslint:recommended'], parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, rules: { 'no-unused-vars': 'warn' } }, null, 2));
258
+ await fse.writeFile(path.join(fe, '.prettierrc'), JSON.stringify({ semi: true, singleQuote: true, tabWidth: 2, trailingComma: 'es5' }, null, 2));
330
259
  }
331
260
 
332
- // ── Testing ───────────────────────────────────────────────────────────────────
333
- async function setupTesting(config, projectPath) {
334
- const frontendPath = path.join(projectPath, 'frontend');
335
- const testDir = path.join(frontendPath, 'src', '__tests__');
261
+ // ── TESTING ───────────────────────────────────────────────────────────────────
262
+ async function setupTesting(root) {
263
+ const testDir = path.join(root, 'frontend', 'src', '__tests__');
336
264
  await fse.ensureDir(testDir);
337
- await fse.writeFile(path.join(testDir, 'app.test.js'), `import { describe, it, expect } from 'vitest';\n\ndescribe('App', () => {\n it('works', () => expect(1 + 1).toBe(2));\n});\n`);
338
- try { spawnSync('npm', ['install', '-D', 'vitest'], { cwd: frontendPath, shell: true, stdio: 'pipe' }); } catch {}
339
- }
340
-
341
- // ── GitHub Actions ────────────────────────────────────────────────────────────
342
- async function setupCI(config, projectPath) {
343
- const ciDir = path.join(projectPath, '.github', 'workflows');
344
- await fse.ensureDir(ciDir);
345
- const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
346
- await fse.writeFile(path.join(ciDir, 'ci.yml'), `name: CI\non:\n push:\n branches: [main]\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 20\n - run: cd frontend && npm ci && npm run build\n${isPython ? '' : ' - run: cd backend && npm ci\n'}`);
265
+ await fse.writeFile(path.join(testDir, 'app.test.js'), `import { describe, it, expect } from 'vitest';\ndescribe('App', () => { it('works', () => expect(1 + 1).toBe(2)); });\n`);
347
266
  }
348
267
 
349
- // ── Docker ────────────────────────────────────────────────────────────────────
350
- async function generateDocker(config, projectPath) {
268
+ // ── LOGS ──────────────────────────────────────────────────────────────────────
269
+ async function setupLogs(config, root) {
351
270
  const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
352
- const backendPort = isPython ? '8000' : '3001';
353
- const dbService = config.db === 'postgres' ? `\n db:\n image: postgres:16-alpine\n environment:\n POSTGRES_DB: mydb\n POSTGRES_USER: user\n POSTGRES_PASSWORD: password\n ports:\n - "5432:5432"\n volumes:\n - pgdata:/var/lib/postgresql/data\n\nvolumes:\n pgdata:` :
354
- config.db === 'mongo' ? `\n db:\n image: mongo:7\n ports:\n - "27017:27017"\n volumes:\n - mongodata:/data/db\n\nvolumes:\n mongodata:` :
355
- config.db === 'mysql' ? `\n db:\n image: mysql:8\n environment:\n MYSQL_DATABASE: mydb\n MYSQL_ROOT_PASSWORD: password\n ports:\n - "3306:3306"` : '';
356
-
357
- await fse.writeFile(path.join(projectPath, 'docker-compose.yml'),
358
- `version: '3.9'\nservices:\n frontend:\n build: ./frontend\n ports:\n - "5173:5173"\n backend:\n build: ./backend\n ports:\n - "${backendPort}:${backendPort}"\n env_file: ./backend/.env${dbService}\n`
359
- );
271
+ if (isPython) return;
272
+ const loggerFile = `import winston from 'winston';\n\nexport const logger = winston.createLogger({\n level: process.env.LOG_LEVEL || 'info',\n format: winston.format.combine(\n winston.format.timestamp(),\n winston.format.colorize(),\n winston.format.printf(({ timestamp, level, message }) => \`\${timestamp} [\${level}]: \${message}\`)\n ),\n transports: [\n new winston.transports.Console(),\n new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),\n new winston.transports.File({ filename: 'logs/combined.log' }),\n ],\n});\n`;
273
+ const logDir = path.join(root, 'backend', 'src', 'utils');
274
+ await fse.ensureDir(logDir);
275
+ await fse.writeFile(path.join(logDir, 'logger.js'), loggerFile);
276
+ await fse.ensureDir(path.join(root, 'backend', 'logs'));
277
+ await fse.writeFile(path.join(root, 'backend', 'logs', '.gitkeep'), '');
360
278
  }
361
279
 
362
- // ── .env ──────────────────────────────────────────────────────────────────────
363
- async function generateEnvFiles(config, projectPath) {
364
- await fse.writeFile(path.join(projectPath, 'frontend', '.env.example'), 'VITE_API_URL=http://localhost:3001\n');
365
- const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
366
- const dbUrl = config.db === 'postgres' ? 'postgresql://user:password@localhost:5432/mydb' :
367
- config.db === 'mysql' ? 'mysql://user:password@localhost:3306/mydb' :
368
- config.db === 'mongo' ? 'mongodb://localhost:27017/mydb' :
369
- config.db === 'sqlite' ? 'file:./dev.db' : '';
370
- if (!isPython) {
371
- await fse.writeFile(path.join(projectPath, 'backend', '.env.example'),
372
- `PORT=3001\nNODE_ENV=development\nJWT_SECRET=change_me_in_production\n${dbUrl ? 'DATABASE_URL=' + dbUrl + '\n' : ''}`
373
- );
374
- }
280
+ // ── SEO ───────────────────────────────────────────────────────────────────────
281
+ async function setupSEO(config, root) {
282
+ const publicDir = path.join(root, 'frontend', 'public');
283
+ await fse.ensureDir(publicDir);
284
+ await fse.writeFile(path.join(publicDir, 'robots.txt'), `User-agent: *\nAllow: /\nSitemap: https://yourdomain.com/sitemap.xml\n`);
285
+ await fse.writeFile(path.join(publicDir, 'sitemap.xml'), `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n <url><loc>https://yourdomain.com/</loc><priority>1.0</priority></url>\n</urlset>\n`);
375
286
  }
376
287
 
377
- const EDITORCONFIG = `root = true\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n`;
378
- const GITIGNORE = `node_modules/\ndist/\n.next/\n.nuxt/\n.svelte-kit/\n.env\n*.log\n__pycache__/\n.venv/\n*.pyc\ntarget/\n*.class\n.DS_Store\n`;
288
+ const GITIGNORE = `node_modules/\ndist/\n.next/\n.nuxt/\n.svelte-kit/\n.env\n*.log\nlogs/\n__pycache__/\n.venv/\n*.pyc\ntarget/\n*.class\n.DS_Store\n`;