novatec-cli 3.0.2 → 3.0.3

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
@@ -6,150 +6,95 @@ import { execSync } 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 { 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
9
  import { isInstalled } from './utils.js';
14
10
 
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))} `);
19
- }
20
-
21
11
  export async function runCreate(nameArg, options) {
22
12
  const t0 = Date.now();
23
13
  const config = await askProjectOptions(nameArg, options);
24
14
  const root = path.resolve(process.cwd(), config.name);
25
15
  await fse.ensureDir(root);
26
16
 
27
- const extras = config.extras || [];
28
17
  const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
29
18
  const pm = config.pm || 'npm';
30
19
 
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',
44
- ].filter(Boolean);
45
-
46
20
  console.log();
47
- let step = 0;
48
- const tick = (label) => { step++; bar(step, steps.length, label); };
49
21
 
50
22
  // 1. Frontend
51
- tick('Generando frontend...');
23
+ console.log(chalk.white(' [1/4] Generando frontend...'));
52
24
  await generateFrontend(config, root);
53
- await connectFrontendToBackend(config, root);
54
25
 
55
- // 2. Backend con arquitectura
56
- tick('Generando backend...');
26
+ // 2. Backend
27
+ console.log(chalk.white(' [2/4] Generando backend...'));
57
28
  await generateBackend(config, root);
58
- await applyArchitecture(config, root);
59
29
 
60
- // 3. README
61
- tick('README...');
62
- await generateReadme(config, root);
30
+ // 3. Arquitectura
31
+ console.log(chalk.white(' [3/4] Aplicando arquitectura...'));
32
+ await applyArchitecture(config, root);
63
33
 
64
- // 4. .env
65
- tick('.env files...');
34
+ // 4. Archivos base (.env, README)
35
+ console.log(chalk.white(' [4/4] Generando archivos base...'));
66
36
  await generateEnv(config, root);
37
+ await generateReadme(config, root);
67
38
 
68
- // 5. Business mode
69
- if (config.mode === 'business') {
70
- tick('Business mode...');
71
- await generateBusiness(config, root);
72
- }
73
-
74
- // 6. Docker
75
- if (extras.includes('docker')) {
76
- tick('Docker...');
77
- await generateDocker(config, root);
78
- }
79
-
80
- // 7. Seguridad
81
- if (extras.includes('security')) {
82
- tick('Seguridad...');
83
- await generateSecurity(config, root);
84
- }
85
-
86
- // 8. GitHub Actions
87
- if (extras.includes('ci')) {
88
- tick('GitHub Actions...');
89
- await generateDeploy(config, root);
90
- }
91
-
92
- // 9. ESLint
93
- if (extras.includes('eslint')) {
94
- tick('ESLint...');
95
- await setupEslint(root);
96
- }
97
-
98
- // 10. Testing
99
- if (extras.includes('testing')) {
100
- tick('Testing...');
101
- await setupTesting(root);
102
- }
103
-
104
- // 11. Swagger (ya incluido en security/backend, solo notificar)
105
- if (extras.includes('swagger')) {
106
- tick('Swagger...');
107
- // generado en generateSecurity
108
- }
109
-
110
- // 12. Logs
111
- if (extras.includes('logs')) {
112
- tick('Logs...');
113
- await setupLogs(config, root);
114
- }
39
+ // 5. Instalar dependencias
40
+ if (config.install) {
41
+ console.log();
42
+ console.log(chalk.white(' Instalando dependencias...'));
43
+ const feDir = path.join(root, 'frontend');
44
+ const beDir = path.join(root, 'backend');
45
+ const installCmd = pm === 'yarn' ? 'yarn' : `${pm} install`;
115
46
 
116
- // 13. SEO
117
- if (extras.includes('seo')) {
118
- tick('SEO...');
119
- await setupSEO(config, root);
120
- }
47
+ if (await fse.pathExists(path.join(feDir, 'package.json'))) {
48
+ try {
49
+ console.log(chalk.gray(` → frontend (${pm} install)`));
50
+ execSync(installCmd, { cwd: feDir, stdio: 'inherit' });
51
+ } catch (e) {
52
+ console.log(chalk.yellow(' ⚠ Error instalando frontend: ' + e.message));
53
+ }
54
+ }
121
55
 
122
- // 14. Git
123
- if (extras.includes('git') && isInstalled('git')) {
124
- tick('Git init...');
125
- try {
126
- execSync('git init', { cwd: root, stdio: 'pipe' });
127
- await fse.writeFile(path.join(root, '.gitignore'), GITIGNORE);
128
- } catch {}
129
- }
56
+ if (!isPython && await fse.pathExists(path.join(beDir, 'package.json'))) {
57
+ try {
58
+ console.log(chalk.gray(` → backend (${pm} install)`));
59
+ execSync(installCmd, { cwd: beDir, stdio: 'inherit' });
60
+ } catch (e) {
61
+ console.log(chalk.yellow(' ⚠ Error instalando backend: ' + e.message));
62
+ }
63
+ }
130
64
 
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 { execSync(installCmd, { cwd: fe, stdio: 'pipe' }); } catch {}
138
- if (!isPython) {
139
- try { execSync(installCmd, { cwd: be, stdio: 'pipe' }); } catch {}
65
+ if (isPython && await fse.pathExists(path.join(beDir, 'requirements.txt'))) {
66
+ if (isInstalled('pip')) {
67
+ try {
68
+ console.log(chalk.gray(' backend (pip install -r requirements.txt)'));
69
+ execSync('pip install -r requirements.txt', { cwd: beDir, stdio: 'inherit' });
70
+ } catch (e) {
71
+ console.log(chalk.yellow(' Error instalando backend Python: ' + e.message));
72
+ }
73
+ } else {
74
+ console.log(chalk.yellow(' ⚠ pip no encontrado. Ejecuta manualmente: pip install -r requirements.txt'));
75
+ }
140
76
  }
141
77
  }
142
78
 
143
- bar(steps.length, steps.length, 'Completado');
144
- console.log('\n');
145
-
146
79
  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
80
 
81
+ const beCmd = {
82
+ express: 'npm run dev',
83
+ nestjs: 'npm run start:dev',
84
+ fastify: 'npm run dev',
85
+ hono: 'npm run dev',
86
+ fastapi: 'uvicorn main:app --reload',
87
+ django: 'python manage.py runserver',
88
+ flask: 'python run.py',
89
+ spring: 'mvn spring-boot:run',
90
+ deno: 'deno task dev',
91
+ gin: 'go run main.go',
92
+ };
93
+
94
+ console.log();
149
95
  console.log(boxen(
150
96
  chalk.bold.white(' ✔ PROYECTO LISTO — NovaTec CLI\n\n') +
151
97
  chalk.gray(' Nombre │ ') + chalk.white(config.name) + '\n' +
152
- chalk.gray(' Modo │ ') + chalk.white(config.mode || 'estándar') + '\n' +
153
98
  chalk.gray(' Frontend │ ') + chalk.white(config.frontend) + (config.typescript ? chalk.gray(' + TypeScript') : '') + '\n' +
154
99
  chalk.gray(' Backend │ ') + chalk.white(config.backend) + '\n' +
155
100
  chalk.gray(' Base datos │ ') + chalk.white(config.db || 'ninguna') + '\n' +
@@ -157,12 +102,9 @@ export async function runCreate(nameArg, options) {
157
102
  chalk.gray(' Tiempo │ ') + chalk.white(elapsed + 's') + '\n\n' +
158
103
  chalk.gray(' ─────────────────────────────────────────────\n\n') +
159
104
  chalk.white(' # Frontend\n') +
160
- chalk.gray(` cd ${config.name}/frontend\n`) +
161
- chalk.gray(` ${pm} ${pm === 'yarn' ? '' : 'run '}dev\n\n`) +
105
+ chalk.gray(` cd ${config.name}/frontend && ${pm} run dev\n\n`) +
162
106
  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') : ''),
107
+ chalk.gray(` cd ${config.name}/backend && ${beCmd[config.backend] || pm + ' run dev'}`),
166
108
  { padding: 1, margin: { top: 1, left: 2 }, borderStyle: 'round', borderColor: 'white', dimBorder: true }
167
109
  ));
168
110
 
@@ -174,32 +116,6 @@ export async function runCreate(nameArg, options) {
174
116
  console.log();
175
117
  }
176
118
 
177
- // ── CONEXIÓN FE → BE ──────────────────────────────────────────────────────────
178
- async function connectFrontendToBackend(config, root) {
179
- const feDir = path.join(root, 'frontend');
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
- }
202
-
203
119
  // ── ARQUITECTURA ──────────────────────────────────────────────────────────────
204
120
  async function applyArchitecture(config, root) {
205
121
  const beDir = path.join(root, 'backend', 'src');
@@ -217,72 +133,67 @@ async function applyArchitecture(config, root) {
217
133
  }
218
134
  }
219
135
 
220
- // ── ENV ───────────────────────────────────────────────────────────────────────
136
+ // ── .ENV ──────────────────────────────────────────────────────────────────────
221
137
  async function generateEnv(config, root) {
222
138
  const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
139
+ const bePort = isPython ? '8000' : '3001';
140
+
223
141
  const dbUrl = {
224
142
  postgres: 'postgresql://user:password@localhost:5432/mydb',
225
143
  mysql: 'mysql://user:password@localhost:3306/mydb',
226
144
  mongo: 'mongodb://localhost:27017/mydb',
227
145
  sqlite: 'file:./dev.db',
228
- supabase: config.supabaseUrl || 'https://your-project.supabase.co',
146
+ supabase: 'https://your-project.supabase.co',
229
147
  }[config.db] || '';
230
148
 
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` : '');
149
+ const feEnv = `VITE_API_URL=http://localhost:${bePort}\n`;
150
+ const beEnv = isPython
151
+ ? `DATABASE_URL=${dbUrl}\nSECRET_KEY=change_me_in_production\nDEBUG=True\n`
152
+ : `PORT=3001\nNODE_ENV=development\nJWT_SECRET=change_me_in_production\n${dbUrl ? 'DATABASE_URL=' + dbUrl + '\n' : ''}`;
233
153
 
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' : '');
154
+ const feDir = path.join(root, 'frontend');
155
+ const beDir = path.join(root, 'backend');
238
156
 
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);
157
+ if (await fse.pathExists(feDir)) {
158
+ await fse.writeFile(path.join(feDir, '.env'), feEnv);
159
+ await fse.writeFile(path.join(feDir, '.env.example'), feEnv);
160
+ }
161
+ if (await fse.pathExists(beDir)) {
162
+ await fse.writeFile(path.join(beDir, '.env'), beEnv);
163
+ await fse.writeFile(path.join(beDir, '.env.example'), beEnv);
244
164
  }
245
165
  }
246
166
 
247
167
  // ── README ────────────────────────────────────────────────────────────────────
248
168
  async function generateReadme(config, root) {
249
169
  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`;
170
+ const content =
171
+ `# ${config.name}
172
+
173
+ > Generado con [NovaTec CLI](https://www.npmjs.com/package/novatec-cli)
174
+
175
+ ## Stack
176
+
177
+ | Capa | Tecnología |
178
+ |------|------------|
179
+ | Frontend | **${config.frontend}**${ts} |
180
+ | Backend | **${config.backend}** |
181
+ | Base de datos | **${config.db || 'N/A'}** |
182
+ | Arquitectura | **${config.architecture || 'mvc'}** |
183
+
184
+ ## Inicio rápido
185
+
186
+ \`\`\`bash
187
+ # Frontend
188
+ cd ${config.name}/frontend
189
+ npm install
190
+ npm run dev
191
+
192
+ # Backend
193
+ cd ${config.name}/backend
194
+ npm install
195
+ npm run dev
196
+ \`\`\`
197
+ `;
251
198
  await fse.writeFile(path.join(root, 'README.md'), content);
252
199
  }
253
-
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));
259
- }
260
-
261
- // ── TESTING ───────────────────────────────────────────────────────────────────
262
- async function setupTesting(root) {
263
- const testDir = path.join(root, 'frontend', 'src', '__tests__');
264
- await fse.ensureDir(testDir);
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`);
266
- }
267
-
268
- // ── LOGS ──────────────────────────────────────────────────────────────────────
269
- async function setupLogs(config, root) {
270
- const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
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'), '');
278
- }
279
-
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`);
286
- }
287
-
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`;
@@ -1,36 +1,40 @@
1
1
  import path from 'path';
2
2
  import chalk from 'chalk';
3
3
  import fse from 'fs-extra';
4
- import { spawnSync } from 'child_process';
5
- import { run, isInstalled } from '../utils.js';
4
+ import { execSync } from 'child_process';
5
+ import { isInstalled } from '../utils.js';
6
+
7
+ // DB packages por backend type
8
+ const NODE_DB_DEPS = {
9
+ postgres: ['pg', 'pg-hstore'],
10
+ mysql: ['mysql2'],
11
+ mongo: ['mongoose'],
12
+ sqlite: ['better-sqlite3'],
13
+ supabase: ['@supabase/supabase-js'],
14
+ none: [],
15
+ };
6
16
 
7
- function installNodeDeps(prod, dev, cwd, label) {
17
+ function installNodeDeps(prod, dev, cwd) {
8
18
  if (prod.length) {
9
- const r = spawnSync('npm', ['install', ...prod], { cwd, stdio: 'pipe', encoding: 'utf8' });
10
- if (r.status !== 0) throw new Error((r.stderr || r.stdout || '').trim() || `Error instalando: ${prod.join(', ')}`);
19
+ execSync(`npm install ${prod.join(' ')}`, { cwd, stdio: 'pipe' });
11
20
  }
12
- if (dev.length) {
13
- const r = spawnSync('npm', ['install', '-D', ...dev], { cwd, stdio: 'pipe', encoding: 'utf8' });
14
- if (r.status !== 0) throw new Error((r.stderr || r.stdout || '').trim() || `Error instalando dev: ${dev.join(', ')}`);
21
+ if (dev && dev.length) {
22
+ execSync(`npm install -D ${dev.join(' ')}`, { cwd, stdio: 'pipe' });
15
23
  }
16
24
  }
17
25
 
18
- async function generateBackend(config, projectPath) {
19
- console.log(chalk.blue(`\n🔧 Generando backend (${chalk.bold(config.backend)})...`));
26
+ function backendPath(projectPath) {
27
+ return path.join(projectPath, 'backend');
28
+ }
29
+
30
+ export async function generateBackend(config, projectPath) {
31
+ console.log(chalk.blue(`\n ⚙ Generando backend (${chalk.bold(config.backend)})...`));
20
32
  const handlers = { express, nestjs, fastify, hono, fastapi, django, flask, spring, deno, gin };
21
33
  const fn = handlers[config.backend];
22
34
  if (!fn) throw new Error(`Backend no soportado: ${config.backend}`);
23
35
  await fn(config, projectPath);
24
36
  }
25
37
 
26
- // ── HELPERS ───────────────────────────────────────────────────────────────────
27
- function backendPath(projectPath) { return path.join(projectPath, 'backend'); }
28
-
29
- async function writeDotenv(bPath, content) {
30
- await fse.writeFile(path.join(bPath, '.env'), content);
31
- await fse.writeFile(path.join(bPath, '.env.example'), content.replace(/=.+/g, '='));
32
- }
33
-
34
38
  // ── EXPRESS ───────────────────────────────────────────────────────────────────
35
39
  async function express(config, projectPath) {
36
40
  const bp = backendPath(projectPath);
@@ -38,38 +42,24 @@ async function express(config, projectPath) {
38
42
  await fse.ensureDir(path.join(bp, 'src', 'middlewares'));
39
43
  await fse.ensureDir(path.join(bp, 'src', 'controllers'));
40
44
 
41
- const deps = config.backendDeps || [];
42
- const hasDotenv = deps.includes('dotenv');
43
- const hasCors = deps.includes('cors');
44
- const hasHelmet = deps.includes('helmet');
45
- const hasMorgan = deps.includes('morgan');
46
- const hasSwagger = deps.includes('swagger-ui-express');
47
- const hasSocket = deps.includes('socket.io');
45
+ const dbDeps = NODE_DB_DEPS[config.db] || [];
48
46
 
49
- const pkg = {
47
+ await fse.writeJSON(path.join(bp, 'package.json'), {
50
48
  name: 'backend', version: '1.0.0', main: 'src/server.js',
51
49
  scripts: { start: 'node src/server.js', dev: 'nodemon src/server.js' },
52
- };
53
- await fse.writeJSON(path.join(bp, 'package.json'), pkg, { spaces: 2 });
50
+ }, { spaces: 2 });
54
51
 
55
- const server = `${hasDotenv ? "require('dotenv').config();\n" : ''}const express = require('express');
56
- ${hasCors ? "const cors = require('cors');" : ''}
57
- ${hasHelmet ? "const helmet = require('helmet');" : ''}
58
- ${hasMorgan ? "const morgan = require('morgan');" : ''}
59
- ${hasSwagger ? "const swaggerUi = require('swagger-ui-express');\nconst swaggerDoc = require('./swagger.json');" : ''}
60
- ${hasSocket ? "const { createServer } = require('http');\nconst { Server } = require('socket.io');" : ''}
52
+ await fse.writeFile(path.join(bp, 'src', 'server.js'),
53
+ `require('dotenv').config();
54
+ const express = require('express');
55
+ const cors = require('cors');
61
56
 
62
57
  const app = express();
63
- ${hasSocket ? 'const httpServer = createServer(app);\nconst io = new Server(httpServer, { cors: { origin: "*" } });' : ''}
64
- ${hasCors ? 'app.use(cors());' : ''}
65
- ${hasHelmet ? 'app.use(helmet());' : ''}
66
- ${hasMorgan ? "app.use(morgan('dev'));" : ''}
58
+ app.use(cors());
67
59
  app.use(express.json());
68
60
  app.use(express.urlencoded({ extended: true }));
69
61
 
70
62
  app.use('/api', require('./routes/index'));
71
- ${hasSwagger ? "app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDoc));" : ''}
72
- ${hasSocket ? "\nio.on('connection', (socket) => {\n console.log('Cliente conectado:', socket.id);\n socket.on('disconnect', () => console.log('Cliente desconectado'));\n});" : ''}
73
63
 
74
64
  app.get('/', (req, res) => res.json({ status: 'ok', message: 'API funcionando ✅' }));
75
65
 
@@ -79,49 +69,60 @@ app.use((err, req, res, next) => {
79
69
  });
80
70
 
81
71
  const PORT = process.env.PORT || 3001;
82
- const server${hasSocket ? ' = httpServer' : 'Inst = app'}.listen(PORT, () =>
83
- console.log(\`🚀 Servidor en http://localhost:\${PORT}\`)
84
- );
85
- `;
72
+ app.listen(PORT, () => console.log(\`🚀 Servidor en http://localhost:\${PORT}\`));
73
+ `);
86
74
 
87
- const routes = `const router = require('express').Router();
75
+ await fse.writeFile(path.join(bp, 'src', 'routes', 'index.js'),
76
+ `const router = require('express').Router();
88
77
  router.get('/health', (req, res) => res.json({ status: 'healthy', timestamp: new Date() }));
89
78
  module.exports = router;
90
- `;
79
+ `);
91
80
 
92
- await fse.writeFile(path.join(bp, 'src', 'server.js'), server);
93
- await fse.writeFile(path.join(bp, 'src', 'routes', 'index.js'), routes);
94
- if (hasDotenv) await writeDotenv(bp, 'PORT=3001\nDB_URL=\nJWT_SECRET=change_me\nNODE_ENV=development\n');
95
-
96
- await installNodeDeps(['express', ...deps], [], bp, 'Instalando dependencias Express');
81
+ console.log(chalk.gray(' Instalando dependencias Express...'));
82
+ try {
83
+ installNodeDeps(['express', 'cors', 'dotenv', ...dbDeps], ['nodemon'], bp);
84
+ console.log(chalk.green(' ✔ Dependencias instaladas'));
85
+ } catch (e) {
86
+ console.log(chalk.yellow(' ⚠ Error instalando deps: ' + e.message));
87
+ }
97
88
  }
98
89
 
99
90
  // ── NESTJS ────────────────────────────────────────────────────────────────────
100
91
  async function nestjs(config, projectPath) {
101
- run('npx @nestjs/cli@latest new backend --skip-git --package-manager npm', projectPath, 'Creando proyecto NestJS');
102
92
  const bp = backendPath(projectPath);
103
- const deps = (config.backendDeps || []).filter(d => d !== 'nodemon');
104
- if (deps.length) run(`npm install ${deps.join(' ')}`, bp, `Instalando: ${deps.join(', ')}`);
105
- if (deps.includes('@nestjs/config')) await writeDotenv(bp, 'PORT=3001\nDB_URL=\nJWT_SECRET=change_me\n');
93
+ console.log(chalk.gray(' → Creando proyecto NestJS (puede tardar)...'));
94
+ try {
95
+ execSync('npx @nestjs/cli@latest new backend --skip-git --package-manager npm', {
96
+ cwd: projectPath, stdio: 'pipe',
97
+ });
98
+ } catch (e) {
99
+ throw new Error('Error creando NestJS: ' + e.message);
100
+ }
101
+ const dbDeps = NODE_DB_DEPS[config.db] || [];
102
+ if (dbDeps.length) {
103
+ try {
104
+ execSync(`npm install ${dbDeps.join(' ')}`, { cwd: bp, stdio: 'pipe' });
105
+ } catch {}
106
+ }
106
107
  }
107
108
 
108
109
  // ── FASTIFY ───────────────────────────────────────────────────────────────────
109
110
  async function fastify(config, projectPath) {
110
111
  const bp = backendPath(projectPath);
111
- await fse.ensureDir(path.join(bp, 'src', 'routes'));
112
+ await fse.ensureDir(path.join(bp, 'src'));
113
+
114
+ const dbDeps = NODE_DB_DEPS[config.db] || [];
112
115
 
113
- const pkg = {
116
+ await fse.writeJSON(path.join(bp, 'package.json'), {
114
117
  name: 'backend', version: '1.0.0', main: 'src/server.js',
115
118
  scripts: { start: 'node src/server.js', dev: 'nodemon src/server.js' },
116
- };
117
- await fse.writeJSON(path.join(bp, 'package.json'), pkg, { spaces: 2 });
119
+ }, { spaces: 2 });
118
120
 
119
- const deps = config.backendDeps || [];
120
- const hasDotenv = deps.includes('dotenv');
121
+ await fse.writeFile(path.join(bp, 'src', 'server.js'),
122
+ `require('dotenv').config();
123
+ const fastify = require('fastify')({ logger: true });
121
124
 
122
- const server = `${hasDotenv ? "require('dotenv').config();\n" : ''}const fastify = require('fastify')({ logger: true });
123
- ${deps.includes('@fastify/cors') ? "fastify.register(require('@fastify/cors'));" : ''}
124
- ${deps.includes('@fastify/rate-limit') ? "fastify.register(require('@fastify/rate-limit'), { max: 100, timeWindow: '1 minute' });" : ''}
125
+ fastify.register(require('@fastify/cors'));
125
126
 
126
127
  fastify.get('/', async () => ({ status: 'ok', message: 'API funcionando ✅' }));
127
128
  fastify.get('/api/health', async () => ({ status: 'healthy', timestamp: new Date() }));
@@ -135,25 +136,32 @@ const start = async () => {
135
136
  }
136
137
  };
137
138
  start();
138
- `;
139
- await fse.writeFile(path.join(bp, 'src', 'server.js'), server);
140
- if (hasDotenv) await writeDotenv(bp, 'PORT=3001\nDB_URL=\nJWT_SECRET=change_me\n');
141
- await installNodeDeps(['fastify', ...deps], [], bp, 'Instalando dependencias Fastify');
139
+ `);
140
+
141
+ console.log(chalk.gray(' → Instalando dependencias Fastify...'));
142
+ try {
143
+ installNodeDeps(['fastify', '@fastify/cors', 'dotenv', ...dbDeps], ['nodemon'], bp);
144
+ console.log(chalk.green(' ✔ Dependencias instaladas'));
145
+ } catch (e) {
146
+ console.log(chalk.yellow(' ⚠ Error instalando deps: ' + e.message));
147
+ }
142
148
  }
143
149
 
144
150
  // ── HONO ──────────────────────────────────────────────────────────────────────
145
151
  async function hono(config, projectPath) {
146
152
  const bp = backendPath(projectPath);
147
- await fse.ensureDir(bp);
153
+ await fse.ensureDir(path.join(bp, 'src'));
148
154
 
149
- const pkg = {
155
+ const dbDeps = NODE_DB_DEPS[config.db] || [];
156
+
157
+ await fse.writeJSON(path.join(bp, 'package.json'), {
150
158
  name: 'backend', version: '1.0.0', main: 'src/index.js',
151
159
  scripts: { start: 'node src/index.js', dev: 'nodemon src/index.js' },
152
- };
153
- await fse.writeJSON(path.join(bp, 'package.json'), pkg, { spaces: 2 });
160
+ }, { spaces: 2 });
154
161
 
155
- const deps = config.backendDeps || [];
156
- const server = `const { Hono } = require('hono');
162
+ await fse.writeFile(path.join(bp, 'src', 'index.js'),
163
+ `require('dotenv').config();
164
+ const { Hono } = require('hono');
157
165
  const { serve } = require('@hono/node-server');
158
166
 
159
167
  const app = new Hono();
@@ -164,11 +172,15 @@ app.get('/api/health', (c) => c.json({ status: 'healthy', timestamp: new Date()
164
172
  serve({ fetch: app.fetch, port: process.env.PORT || 3001 }, (info) =>
165
173
  console.log(\`🚀 Servidor en http://localhost:\${info.port}\`)
166
174
  );
167
- `;
168
- await fse.ensureDir(path.join(bp, 'src'));
169
- await fse.writeFile(path.join(bp, 'src', 'index.js'), server);
170
- if (deps.includes('dotenv')) await writeDotenv(bp, 'PORT=3001\nDB_URL=\nJWT_SECRET=change_me\n');
171
- await installNodeDeps(['hono', '@hono/node-server', ...deps], ['nodemon'], bp, 'Instalando dependencias Hono');
175
+ `);
176
+
177
+ console.log(chalk.gray(' Instalando dependencias Hono...'));
178
+ try {
179
+ installNodeDeps(['hono', '@hono/node-server', 'dotenv', ...dbDeps], ['nodemon'], bp);
180
+ console.log(chalk.green(' ✔ Dependencias instaladas'));
181
+ } catch (e) {
182
+ console.log(chalk.yellow(' ⚠ Error instalando deps: ' + e.message));
183
+ }
172
184
  }
173
185
 
174
186
  // ── FASTAPI ───────────────────────────────────────────────────────────────────
@@ -178,10 +190,20 @@ async function fastapi(config, projectPath) {
178
190
  await fse.ensureDir(path.join(bp, 'app', 'models'));
179
191
  await fse.ensureDir(path.join(bp, 'app', 'schemas'));
180
192
 
181
- const deps = config.backendDeps || [];
182
- const hasDotenv = deps.includes('python-dotenv');
193
+ const pyDbDeps = {
194
+ postgres: ['psycopg2-binary', 'sqlalchemy'],
195
+ mysql: ['mysqlclient', 'sqlalchemy'],
196
+ mongo: ['motor', 'beanie'],
197
+ sqlite: ['sqlalchemy'],
198
+ supabase: ['supabase'],
199
+ none: [],
200
+ }[config.db] || [];
201
+
202
+ await fse.writeFile(path.join(bp, 'main.py'),
203
+ `from dotenv import load_dotenv
204
+ load_dotenv()
183
205
 
184
- const main = `${hasDotenv ? 'from dotenv import load_dotenv\nload_dotenv()\n\n' : ''}from fastapi import FastAPI
206
+ from fastapi import FastAPI
185
207
  from fastapi.middleware.cors import CORSMiddleware
186
208
  from app.routers import health
187
209
 
@@ -193,27 +215,23 @@ app.include_router(health.router, prefix="/api", tags=["health"])
193
215
  @app.get("/")
194
216
  def root():
195
217
  return {"status": "ok", "message": "API funcionando ✅"}
196
- `;
197
- const healthRouter = `from fastapi import APIRouter
218
+ `);
219
+
220
+ await fse.writeFile(path.join(bp, 'app', '__init__.py'), '');
221
+ await fse.writeFile(path.join(bp, 'app', 'routers', '__init__.py'), '');
222
+ await fse.writeFile(path.join(bp, 'app', 'routers', 'health.py'),
223
+ `from fastapi import APIRouter
198
224
  router = APIRouter()
199
225
 
200
226
  @router.get("/health")
201
227
  def health_check():
202
228
  return {"status": "healthy"}
203
- `;
204
- const requirements = ['fastapi', 'uvicorn[standard]', ...deps].join('\n') + '\n';
205
-
206
- await fse.writeFile(path.join(bp, 'main.py'), main);
207
- await fse.writeFile(path.join(bp, 'app', '__init__.py'), '');
208
- await fse.writeFile(path.join(bp, 'app', 'routers', '__init__.py'), '');
209
- await fse.writeFile(path.join(bp, 'app', 'routers', 'health.py'), healthRouter);
229
+ `);
210
230
  await fse.writeFile(path.join(bp, 'app', 'models', '__init__.py'), '');
211
231
  await fse.writeFile(path.join(bp, 'app', 'schemas', '__init__.py'), '');
212
- await fse.writeFile(path.join(bp, 'requirements.txt'), requirements);
213
- if (hasDotenv) await writeDotenv(bp, 'DATABASE_URL=\nSECRET_KEY=change_me\nDEBUG=True\n');
214
-
215
- if (isInstalled('pip')) run('pip install -r requirements.txt', bp, 'Instalando dependencias FastAPI');
216
- else console.log(chalk.yellow('\n ⚠ pip no encontrado. Ejecuta: pip install -r requirements.txt'));
232
+ await fse.writeFile(path.join(bp, 'requirements.txt'),
233
+ ['fastapi', 'uvicorn[standard]', 'python-dotenv', ...pyDbDeps].join('\n') + '\n'
234
+ );
217
235
  }
218
236
 
219
237
  // ── DJANGO ────────────────────────────────────────────────────────────────────
@@ -221,17 +239,32 @@ async function django(config, projectPath) {
221
239
  const bp = backendPath(projectPath);
222
240
  await fse.ensureDir(bp);
223
241
 
224
- const deps = config.backendDeps || [];
225
- const requirements = ['django', 'gunicorn', ...deps].join('\n') + '\n';
226
- await fse.writeFile(path.join(bp, 'requirements.txt'), requirements);
227
- await fse.writeFile(path.join(bp, 'README_SETUP.md'),
228
- `# Django Backend\n\n\`\`\`bash\npip install -r requirements.txt\ndjango-admin startproject core .\npython manage.py migrate\npython manage.py runserver\n\`\`\`\n`
242
+ const pyDbDeps = {
243
+ postgres: ['psycopg2-binary'],
244
+ mysql: ['mysqlclient'],
245
+ mongo: ['djongo'],
246
+ sqlite: [],
247
+ supabase: [],
248
+ none: [],
249
+ }[config.db] || [];
250
+
251
+ await fse.writeFile(path.join(bp, 'requirements.txt'),
252
+ ['django', 'gunicorn', 'django-cors-headers', 'python-dotenv', ...pyDbDeps].join('\n') + '\n'
229
253
  );
230
- if (isInstalled('pip')) {
231
- run('pip install -r requirements.txt', bp, 'Instalando Django y dependencias');
232
- run('django-admin startproject core .', bp, 'Creando proyecto Django');
233
- } else {
234
- console.log(chalk.yellow('\n ⚠ pip no encontrado. Instala manualmente.'));
254
+ await fse.writeFile(path.join(bp, 'README_SETUP.md'),
255
+ `# Django Backend
256
+
257
+ \`\`\`bash
258
+ pip install -r requirements.txt
259
+ django-admin startproject core .
260
+ python manage.py migrate
261
+ python manage.py runserver
262
+ \`\`\`
263
+ `);
264
+ if (isInstalled('django-admin') || isInstalled('python')) {
265
+ try {
266
+ execSync('django-admin startproject core .', { cwd: bp, stdio: 'pipe' });
267
+ } catch {}
235
268
  }
236
269
  }
237
270
 
@@ -240,10 +273,17 @@ async function flask(config, projectPath) {
240
273
  const bp = backendPath(projectPath);
241
274
  await fse.ensureDir(path.join(bp, 'app'));
242
275
 
243
- const deps = config.backendDeps || [];
244
- const hasDotenv = deps.includes('python-dotenv');
245
-
246
- const appInit = `from flask import Flask
276
+ const pyDbDeps = {
277
+ postgres: ['psycopg2-binary', 'flask-sqlalchemy'],
278
+ mysql: ['mysqlclient', 'flask-sqlalchemy'],
279
+ mongo: ['flask-pymongo'],
280
+ sqlite: ['flask-sqlalchemy'],
281
+ supabase: ['supabase'],
282
+ none: [],
283
+ }[config.db] || [];
284
+
285
+ await fse.writeFile(path.join(bp, 'app', '__init__.py'),
286
+ `from flask import Flask
247
287
  from flask_cors import CORS
248
288
 
249
289
  def create_app():
@@ -259,22 +299,21 @@ def create_app():
259
299
  return {'status': 'healthy'}
260
300
 
261
301
  return app
262
- `;
263
- const run_py = `${hasDotenv ? 'from dotenv import load_dotenv\nload_dotenv()\n\n' : ''}from app import create_app
302
+ `);
303
+
304
+ await fse.writeFile(path.join(bp, 'run.py'),
305
+ `from dotenv import load_dotenv
306
+ load_dotenv()
307
+ from app import create_app
264
308
  app = create_app()
265
309
 
266
310
  if __name__ == '__main__':
267
311
  app.run(debug=True, port=5000)
268
- `;
269
- const requirements = ['flask', 'flask-cors', ...deps].join('\n') + '\n';
270
-
271
- await fse.writeFile(path.join(bp, 'app', '__init__.py'), appInit);
272
- await fse.writeFile(path.join(bp, 'run.py'), run_py);
273
- await fse.writeFile(path.join(bp, 'requirements.txt'), requirements);
274
- if (hasDotenv) await writeDotenv(bp, 'FLASK_ENV=development\nSECRET_KEY=change_me\nDATABASE_URL=\n');
312
+ `);
275
313
 
276
- if (isInstalled('pip')) run('pip install -r requirements.txt', bp, 'Instalando Flask y dependencias');
277
- else console.log(chalk.yellow('\n pip no encontrado. Ejecuta: pip install -r requirements.txt'));
314
+ await fse.writeFile(path.join(bp, 'requirements.txt'),
315
+ ['flask', 'flask-cors', 'python-dotenv', ...pyDbDeps].join('\n') + '\n'
316
+ );
278
317
  }
279
318
 
280
319
  // ── SPRING BOOT ───────────────────────────────────────────────────────────────
@@ -282,10 +321,18 @@ async function spring(config, projectPath) {
282
321
  const bp = backendPath(projectPath);
283
322
  await fse.ensureDir(bp);
284
323
  await fse.writeFile(path.join(bp, 'README_SETUP.md'),
285
- `# Spring Boot Backend\n\nGenera el proyecto en: https://start.spring.io\n\nDependencias sugeridas:\n${(config.backendDeps || []).map(d => `- ${d}`).join('\n')}\n\nO usa Spring CLI:\n\`\`\`bash\nspring init --dependencies=web,data-jpa,security backend\n\`\`\`\n`
286
- );
324
+ `# Spring Boot Backend
325
+
326
+ Genera el proyecto en: https://start.spring.io
327
+
328
+ Dependencias sugeridas: Spring Web, Spring Data JPA, ${config.db === 'postgres' ? 'PostgreSQL Driver' : config.db === 'mysql' ? 'MySQL Driver' : config.db === 'mongo' ? 'Spring Data MongoDB' : 'H2 Database'}
329
+
330
+ \`\`\`bash
331
+ # Con Spring CLI
332
+ spring init --dependencies=web,data-jpa backend
333
+ \`\`\`
334
+ `);
287
335
  console.log(chalk.yellow('\n ℹ Spring Boot: genera el proyecto en https://start.spring.io'));
288
- console.log(chalk.gray(' Se creó un README_SETUP.md con instrucciones.\n'));
289
336
  }
290
337
 
291
338
  // ── DENO/OAK ──────────────────────────────────────────────────────────────────
@@ -293,7 +340,8 @@ async function deno(config, projectPath) {
293
340
  const bp = backendPath(projectPath);
294
341
  await fse.ensureDir(bp);
295
342
 
296
- const server = `import { Application, Router } from "https://deno.land/x/oak/mod.ts";
343
+ await fse.writeFile(path.join(bp, 'main.ts'),
344
+ `import { Application, Router } from "https://deno.land/x/oak/mod.ts";
297
345
 
298
346
  const app = new Application();
299
347
  const router = new Router();
@@ -306,11 +354,15 @@ app.use(router.allowedMethods());
306
354
 
307
355
  console.log("🚀 Servidor en http://localhost:8000");
308
356
  await app.listen({ port: 8000 });
309
- `;
310
- await fse.writeFile(path.join(bp, 'main.ts'), server);
311
- await fse.writeFile(path.join(bp, 'deno.json'), JSON.stringify({ tasks: { dev: 'deno run --allow-net main.ts' } }, null, 2));
312
- console.log(chalk.green(' Backend Deno/Oak generado'));
313
- if (!isInstalled('deno')) console.log(chalk.yellow(' ⚠ Deno no instalado. Instala desde https://deno.land'));
357
+ `);
358
+
359
+ await fse.writeJSON(path.join(bp, 'deno.json'), {
360
+ tasks: { dev: 'deno run --allow-net --allow-env main.ts' },
361
+ }, { spaces: 2 });
362
+
363
+ if (!isInstalled('deno')) {
364
+ console.log(chalk.yellow(' ⚠ Deno no instalado. Instala desde https://deno.land'));
365
+ }
314
366
  }
315
367
 
316
368
  // ── GO/GIN ────────────────────────────────────────────────────────────────────
@@ -318,7 +370,8 @@ async function gin(config, projectPath) {
318
370
  const bp = backendPath(projectPath);
319
371
  await fse.ensureDir(bp);
320
372
 
321
- const main = `package main
373
+ await fse.writeFile(path.join(bp, 'main.go'),
374
+ `package main
322
375
 
323
376
  import (
324
377
  "net/http"
@@ -337,13 +390,19 @@ func main() {
337
390
 
338
391
  r.Run(":8080")
339
392
  }
340
- `;
341
- await fse.writeFile(path.join(bp, 'main.go'), main);
393
+ `);
394
+
342
395
  await fse.writeFile(path.join(bp, 'README_SETUP.md'),
343
- `# Go/Gin Backend\n\n\`\`\`bash\ngo mod init backend\ngo get github.com/gin-gonic/gin\ngo run main.go\n\`\`\`\n`
344
- );
345
- console.log(chalk.green('✔ Backend Go/Gin generado'));
346
- if (!isInstalled('go')) console.log(chalk.yellow(' ⚠ Go no instalado. Instala desde https://go.dev'));
347
- }
396
+ `# Go/Gin Backend
348
397
 
349
- export { generateBackend };
398
+ \`\`\`bash
399
+ go mod init backend
400
+ go get github.com/gin-gonic/gin
401
+ go run main.go
402
+ \`\`\`
403
+ `);
404
+
405
+ if (!isInstalled('go')) {
406
+ console.log(chalk.yellow(' ⚠ Go no instalado. Instala desde https://go.dev'));
407
+ }
408
+ }
@@ -1,14 +1,14 @@
1
1
  import path from 'path';
2
2
  import chalk from 'chalk';
3
3
  import fse from 'fs-extra';
4
- import { run } from '../utils.js';
4
+ import { execSync } from 'child_process';
5
5
 
6
6
  // Comandos de scaffolding por framework
7
7
  const SCAFFOLD = {
8
8
  react: 'npm create vite@latest frontend -- --template react',
9
9
  vue: 'npm create vite@latest frontend -- --template vue',
10
10
  solid: 'npm create vite@latest frontend -- --template solid',
11
- svelte: 'npm create svelte@latest frontend -- --no-install',
11
+ svelte: 'npx sv create frontend --template minimal --no-add-ons',
12
12
  astro: 'npm create astro@latest frontend -- --template minimal --no-install --no-git',
13
13
  next: 'npx create-next-app@latest frontend --yes --no-git',
14
14
  nuxt: 'npx nuxi@latest init frontend --no-install',
@@ -17,65 +17,16 @@ const SCAFFOLD = {
17
17
  qwik: 'npm create qwik@latest frontend -- --no-install',
18
18
  };
19
19
 
20
- async function generateFrontend(config, projectPath) {
20
+ export async function generateFrontend(config, projectPath) {
21
21
  const cmd = SCAFFOLD[config.frontend];
22
22
  if (!cmd) throw new Error(`Frontend no soportado: ${config.frontend}`);
23
23
 
24
- console.log(chalk.blue(`\n⚛ Generando frontend (${chalk.bold(config.frontend)})...`));
25
- run(cmd, projectPath, `Scaffolding ${config.frontend}`);
26
-
27
- const frontendPath = path.join(projectPath, 'frontend');
28
-
29
- // Instalar dependencias base
30
- run('npm install', frontendPath, 'Instalando dependencias base del frontend');
31
-
32
- // Separar tailwind del resto (va como devDependency)
33
- const tailwindPkgs = ['tailwindcss', '@astrojs/tailwind', '@nuxtjs/tailwindcss'];
34
- const devDeps = (config.frontendDeps || []).filter(d => tailwindPkgs.includes(d));
35
- const prodDeps = (config.frontendDeps || []).filter(d => !tailwindPkgs.includes(d) && d !== '@shadcn/ui');
36
-
37
- if (prodDeps.length) {
38
- run(`npm install ${prodDeps.join(' ')}`, frontendPath, `Instalando: ${prodDeps.join(', ')}`);
39
- }
40
- if (devDeps.length) {
41
- run(`npm install -D ${devDeps.join(' ')}`, frontendPath, `Instalando dev: ${devDeps.join(', ')}`);
42
- if (devDeps.some(d => tailwindPkgs.includes(d))) {
43
- await setupTailwind(config.frontend, frontendPath);
44
- }
45
- }
46
- }
47
-
48
- async function setupTailwind(framework, frontendPath) {
49
- // Astro y Nuxt tienen su propia integración, no necesitan init manual
50
- if (['astro', 'nuxt'].includes(framework)) return;
24
+ console.log(chalk.blue(`\n ⚛ Generando frontend (${chalk.bold(config.frontend)})...`));
51
25
 
52
26
  try {
53
- run('npx tailwindcss init -p', frontendPath, 'Configurando Tailwind CSS');
54
- } catch { /* puede fallar si ya existe */ }
55
-
56
- const tailwindConfig = `/** @type {import('tailwindcss').Config} */
57
- module.exports = {
58
- content: ['./index.html', './src/**/*.{js,ts,jsx,tsx,vue,svelte}'],
59
- theme: { extend: {} },
60
- plugins: [],
61
- };
62
- `;
63
- await fse.writeFile(path.join(frontendPath, 'tailwind.config.js'), tailwindConfig);
64
-
65
- const cssPath = path.join(frontendPath, 'src', 'index.css');
66
- const directives = `@tailwind base;
67
- @tailwind components;
68
- @tailwind utilities;
69
-
70
- `;
71
- if (await fse.pathExists(cssPath)) {
72
- const existing = await fse.readFile(cssPath, 'utf8');
73
- await fse.writeFile(cssPath, directives + existing);
74
- } else {
75
- await fse.ensureDir(path.join(frontendPath, 'src'));
76
- await fse.writeFile(cssPath, directives);
27
+ execSync(cmd, { cwd: projectPath, stdio: 'pipe' });
28
+ console.log(chalk.green(` Scaffolding ${config.frontend} completado`));
29
+ } catch (e) {
30
+ throw new Error(`Error generando frontend ${config.frontend}: ${e.message}`);
77
31
  }
78
- console.log(chalk.green('✔ Tailwind CSS configurado'));
79
32
  }
80
-
81
- export { generateFrontend };
package/lib/prompts.js CHANGED
@@ -4,17 +4,6 @@ import fse from 'fs-extra';
4
4
  import path from 'path';
5
5
  import boxen from 'boxen';
6
6
 
7
- export const PRESETS = {
8
- 'ecommerce': { frontend: 'next', backend: 'express', db: 'postgres', typescript: true, mode: 'business', architecture: 'mvc' },
9
- 'reservas': { frontend: 'next', backend: 'express', db: 'postgres', typescript: true, mode: 'business', architecture: 'mvc' },
10
- 'aula-virtual': { frontend: 'react', backend: 'nestjs', db: 'postgres', typescript: true, mode: 'business', architecture: 'clean' },
11
- 'empresa-web': { frontend: 'next', backend: 'express', db: 'postgres', typescript: false, mode: 'business', architecture: 'mvc' },
12
- 'react-express': { frontend: 'react', backend: 'express', db: 'postgres', typescript: false, mode: null, architecture: 'mvc' },
13
- 'next-nestjs': { frontend: 'next', backend: 'nestjs', db: 'postgres', typescript: true, mode: null, architecture: 'clean' },
14
- 'vue-fastapi': { frontend: 'vue', backend: 'fastapi', db: 'postgres', typescript: false, mode: null, architecture: 'mvc' },
15
- 'astro-hono': { frontend: 'astro', backend: 'hono', db: 'sqlite', typescript: true, mode: null, architecture: 'mvc' },
16
- };
17
-
18
7
  export const FRONTEND_CHOICES = [
19
8
  { name: 'React — Vite + React 18', value: 'react' },
20
9
  { name: 'Next.js — SSR/SSG App Router', value: 'next' },
@@ -51,9 +40,9 @@ export const DB_CHOICES = [
51
40
  ];
52
41
 
53
42
  export const ARCH_CHOICES = [
54
- { name: 'MVC — Model-View-Controller (clásico)', value: 'mvc' },
55
- { name: 'Hexagonal — Ports & Adapters', value: 'hexagonal' },
56
- { name: 'Clean — Clean Architecture (capas)', value: 'clean' },
43
+ { name: 'MVC — Model-View-Controller (clásico)', value: 'mvc' },
44
+ { name: 'Hexagonal — Ports & Adapters', value: 'hexagonal' },
45
+ { name: 'Clean — Clean Architecture (capas)', value: 'clean' },
57
46
  ];
58
47
 
59
48
  export const PM_CHOICES = [
@@ -62,150 +51,83 @@ export const PM_CHOICES = [
62
51
  { name: 'yarn', value: 'yarn' },
63
52
  ];
64
53
 
65
- export const EXTRAS_CHOICES = [
66
- { name: 'Git init automático', value: 'git', checked: true },
67
- { name: 'ESLint + Prettier', value: 'eslint', checked: false },
68
- { name: 'Husky + lint-staged', value: 'husky', checked: false },
69
- { name: 'Vitest / Jest', value: 'testing', checked: false },
70
- { name: 'Docker profesional', value: 'docker', checked: false },
71
- { name: 'Archivos .env.example', value: 'env', checked: true },
72
- { name: '.editorconfig', value: 'editorconfig', checked: false },
73
- { name: 'GitHub Actions CI/CD', value: 'ci', checked: false },
74
- { name: 'Auth JWT completa', value: 'auth', checked: false },
75
- { name: 'Tailwind CSS auto', value: 'tailwind', checked: false },
76
- { name: 'Swagger / OpenAPI', value: 'swagger', checked: false },
77
- { name: 'Seguridad (helmet+cors+ratelimit)', value: 'security', checked: false },
78
- { name: 'Sistema de logs (winston)', value: 'logs', checked: false },
79
- { name: 'Stripe (pagos)', value: 'stripe', checked: false },
80
- { name: 'Notificaciones toast (frontend)', value: 'notifications',checked: false },
81
- { name: 'SEO base (meta+OG+sitemap)', value: 'seo', checked: false },
82
- { name: 'Dark mode toggle', value: 'darkmode', checked: false },
83
- { name: 'WhatsApp botón de contacto', value: 'whatsapp', checked: false },
84
- ];
85
-
86
54
  export async function askProjectOptions(nameArg, options) {
87
- // Preset
88
- if (options.preset) {
89
- const preset = PRESETS[options.preset];
90
- if (!preset) {
91
- console.log(chalk.red(`\n Preset "${options.preset}" no existe.`));
92
- console.log(chalk.gray(' Disponibles: ' + Object.keys(PRESETS).join(', ') + '\n'));
93
- process.exit(1);
94
- }
95
- const name = nameArg || options.preset + '-app';
96
- console.log(chalk.gray(`\n Preset: ${chalk.white(options.preset)}\n`));
97
- return { name, ...preset, pm: 'npm', frontendDeps: [], backendDeps: [], extras: ['git', 'env', 'security', 'swagger'] };
98
- }
99
-
100
- // --yes mode
101
- if (options.yes) {
102
- const name = nameArg || 'my-app';
103
- await checkFolder(name);
104
- return {
105
- name,
106
- frontend: options.frontend || 'react',
107
- backend: options.backend || 'express',
108
- db: options.db || 'postgres',
109
- mode: options.mode || null,
110
- architecture: options.architecture || 'mvc',
111
- typescript: options.typescript || false,
112
- pm: options.packageManager || 'npm',
113
- install: options.install || false,
114
- git: options.git || false,
115
- frontendDeps: [],
116
- backendDeps: [],
117
- extras: ['git', 'env'],
118
- };
119
- }
120
-
121
55
  const a = {};
122
56
 
123
57
  // Nombre
124
- if (!nameArg) {
58
+ if (nameArg) {
59
+ a.name = nameArg;
60
+ } else {
125
61
  const { name } = await inquirer.prompt([{
126
62
  type: 'input', name: 'name',
127
63
  message: chalk.white('Nombre del proyecto:'),
128
64
  default: 'my-app',
129
- validate: v => /^[a-z0-9-_]+$/i.test(v.trim()) && v.trim().length >= 2 || 'Solo letras, números, guiones. Mínimo 2 chars.',
65
+ validate: v => /^[a-z0-9-_]+$/i.test(v.trim()) && v.trim().length >= 2
66
+ || 'Solo letras, números, guiones. Mínimo 2 chars.',
130
67
  }]);
131
68
  a.name = name.trim();
132
- } else { a.name = nameArg; }
69
+ }
133
70
 
134
71
  await checkFolder(a.name);
135
72
 
136
- // Modo business
137
- const { mode } = await inquirer.prompt([{
138
- type: 'list', name: 'mode',
139
- message: chalk.white('Modo de proyecto:'),
140
- choices: [
141
- { name: 'Estándar — Solo estructura base', value: null },
142
- { name: 'Business — Landing + Auth + Dashboard + CRUD (sistema completo)', value: 'business' },
143
- ],
144
- default: options.mode || null,
145
- }]);
146
- a.mode = mode;
147
-
148
73
  // TypeScript
149
74
  const { typescript } = await inquirer.prompt([{
150
75
  type: 'confirm', name: 'typescript',
151
76
  message: chalk.white('¿Usar TypeScript?'),
152
- default: options.typescript || false,
77
+ default: false,
153
78
  }]);
154
79
  a.typescript = typescript;
155
80
 
156
81
  // Frontend
157
- if (!options.frontend) {
158
- const { frontend } = await inquirer.prompt([{ type: 'list', name: 'frontend', message: chalk.white('Framework Frontend:'), choices: FRONTEND_CHOICES, pageSize: 12 }]);
159
- a.frontend = frontend;
160
- } else { a.frontend = options.frontend; }
82
+ const { frontend } = await inquirer.prompt([{
83
+ type: 'list', name: 'frontend',
84
+ message: chalk.white('Framework Frontend:'),
85
+ choices: FRONTEND_CHOICES, pageSize: 12,
86
+ default: options.frontend,
87
+ }]);
88
+ a.frontend = frontend;
161
89
 
162
90
  // Backend
163
- if (!options.backend) {
164
- const { backend } = await inquirer.prompt([{ type: 'list', name: 'backend', message: chalk.white('Framework Backend:'), choices: BACKEND_CHOICES, pageSize: 12 }]);
165
- a.backend = backend;
166
- } else { a.backend = options.backend; }
91
+ const { backend } = await inquirer.prompt([{
92
+ type: 'list', name: 'backend',
93
+ message: chalk.white('Framework Backend:'),
94
+ choices: BACKEND_CHOICES, pageSize: 12,
95
+ default: options.backend,
96
+ }]);
97
+ a.backend = backend;
167
98
 
168
99
  // DB
169
- if (!options.db) {
170
- const { db } = await inquirer.prompt([{ type: 'list', name: 'db', message: chalk.white('Base de datos:'), choices: DB_CHOICES, pageSize: 7 }]);
171
- a.db = db;
172
- } else { a.db = options.db; }
173
-
174
- // Supabase config
175
- if (a.db === 'supabase') {
176
- console.log(chalk.gray('\n Configura Supabase (puedes dejarlo vacío y editar .env después)\n'));
177
- const { supabaseUrl, supabaseKey } = await inquirer.prompt([
178
- { type: 'input', name: 'supabaseUrl', message: chalk.white(' Supabase URL:'), default: 'https://your-project.supabase.co' },
179
- { type: 'input', name: 'supabaseKey', message: chalk.white(' Supabase Anon Key:'), default: 'your-anon-key' },
180
- ]);
181
- a.supabaseUrl = supabaseUrl;
182
- a.supabaseKey = supabaseKey;
183
- }
100
+ const { db } = await inquirer.prompt([{
101
+ type: 'list', name: 'db',
102
+ message: chalk.white('Base de datos:'),
103
+ choices: DB_CHOICES, pageSize: 7,
104
+ default: options.db,
105
+ }]);
106
+ a.db = db;
184
107
 
185
108
  // Arquitectura
186
- if (!options.architecture) {
187
- const { architecture } = await inquirer.prompt([{ type: 'list', name: 'architecture', message: chalk.white('Arquitectura del backend:'), choices: ARCH_CHOICES }]);
188
- a.architecture = architecture;
189
- } else { a.architecture = options.architecture; }
109
+ const { architecture } = await inquirer.prompt([{
110
+ type: 'list', name: 'architecture',
111
+ message: chalk.white('Arquitectura del backend:'),
112
+ choices: ARCH_CHOICES,
113
+ default: options.architecture,
114
+ }]);
115
+ a.architecture = architecture;
190
116
 
191
117
  // Package manager
192
- if (!options.packageManager) {
193
- const { pm } = await inquirer.prompt([{ type: 'list', name: 'pm', message: chalk.white('Package manager:'), choices: PM_CHOICES }]);
194
- a.pm = pm;
195
- } else { a.pm = options.packageManager; }
196
-
197
- // Extras
198
- const { extras } = await inquirer.prompt([{
199
- type: 'checkbox', name: 'extras',
200
- message: chalk.white('Extras del proyecto:'),
201
- choices: EXTRAS_CHOICES, pageSize: 14,
118
+ const { pm } = await inquirer.prompt([{
119
+ type: 'list', name: 'pm',
120
+ message: chalk.white('Package manager:'),
121
+ choices: PM_CHOICES,
122
+ default: options.packageManager || 'npm',
202
123
  }]);
124
+ a.pm = pm;
203
125
 
204
- // Auto-install
126
+ // Instalar dependencias
205
127
  const { install } = await inquirer.prompt([{
206
128
  type: 'confirm', name: 'install',
207
- message: chalk.white('¿Instalar dependencias automáticamente?'),
208
- default: options.install || false,
129
+ message: chalk.white('¿Instalar dependencias automáticamente al final?'),
130
+ default: true,
209
131
  }]);
210
132
  a.install = install;
211
133
 
@@ -214,22 +136,24 @@ export async function askProjectOptions(nameArg, options) {
214
136
  console.log(boxen(
215
137
  chalk.bold.white(' RESUMEN\n\n') +
216
138
  chalk.gray(' Nombre │ ') + chalk.white(a.name) + '\n' +
217
- chalk.gray(' Modo │ ') + chalk.white(a.mode || 'estándar') + '\n' +
218
139
  chalk.gray(' Frontend │ ') + chalk.white(a.frontend) + (a.typescript ? chalk.gray(' + TS') : '') + '\n' +
219
140
  chalk.gray(' Backend │ ') + chalk.white(a.backend) + '\n' +
220
141
  chalk.gray(' Base datos │ ') + chalk.white(a.db) + '\n' +
221
142
  chalk.gray(' Arquitectura │ ') + chalk.white(a.architecture) + '\n' +
222
143
  chalk.gray(' PM │ ') + chalk.white(a.pm) + '\n' +
223
- chalk.gray(' Instalar │ ') + chalk.white(a.install ? 'sí' : 'no') + '\n' +
224
- chalk.gray(' Extras │ ') + chalk.white(extras.length ? extras.join(', ') : 'ninguno'),
144
+ chalk.gray(' Instalar │ ') + chalk.white(a.install ? 'sí' : 'no'),
225
145
  { padding: 1, margin: { left: 2 }, borderStyle: 'round', borderColor: 'white', dimBorder: true }
226
146
  ));
227
147
  console.log();
228
148
 
229
- const { confirm } = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message: chalk.white('¿Crear el proyecto?'), default: true }]);
149
+ const { confirm } = await inquirer.prompt([{
150
+ type: 'confirm', name: 'confirm',
151
+ message: chalk.white('¿Crear el proyecto?'),
152
+ default: true,
153
+ }]);
230
154
  if (!confirm) { console.log(chalk.gray('\n Cancelado.\n')); process.exit(0); }
231
155
 
232
- return { ...a, frontendDeps: [], backendDeps: [], extras };
156
+ return a;
233
157
  }
234
158
 
235
159
  async function checkFolder(name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "novatec-cli",
3
- "version": "3.0.2",
3
+ "version": "3.0.3",
4
4
  "description": "🚀 NOVATEC FULLSTACK CLI — Generador profesional de proyectos full stack | React, Next.js, Vue, Express, NestJS, FastAPI y más",
5
5
  "type": "module",
6
6
  "main": "./lib/create.js",