novatec-cli 1.0.1 → 2.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,278 +1,378 @@
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
6
  import { askProjectOptions } from './prompts.js';
5
7
  import { generateFrontend } from './generators/frontend.js';
6
8
  import { generateBackend } from './generators/backend.js';
7
9
  import { generateReadme } from './generators/readme.js';
8
10
  import { run, isInstalled } from './utils.js';
11
+ import boxen from 'boxen';
9
12
 
10
- const boxen = await import('boxen');
11
- const gradient = await import('gradient-string');
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)} `);
21
+ }
12
22
 
13
- async function runCreate(nameArg, options) {
23
+ // ── MAIN ──────────────────────────────────────────────────────────────────────
24
+ export async function runCreate(nameArg, options) {
25
+ const startTime = Date.now();
14
26
  const config = await askProjectOptions(nameArg, options);
15
27
  const projectPath = path.resolve(process.cwd(), config.name);
16
28
 
17
- if (await fse.pathExists(projectPath)) {
18
- throw new Error(`La carpeta "${config.name}" ya existe en este directorio.`);
19
- }
20
-
21
- console.log(chalk.cyan(`\n📁 Creando proyecto en: ${chalk.bold(projectPath)}\n`));
29
+ // Asegurar carpeta
22
30
  await fse.ensureDir(projectPath);
23
31
 
24
- await generateFrontend(config, projectPath);
25
- await generateBackend(config, projectPath);
26
- await generateReadme(config, projectPath);
32
+ const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
33
+ const isNode = !isPython && config.backend !== 'spring' && config.backend !== 'gin';
27
34
 
35
+ // Calcular pasos totales
28
36
  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',
53
+ ].filter(Boolean);
54
+
55
+ console.log();
56
+ let step = 0;
57
+
58
+ const tick = (label) => {
59
+ step++;
60
+ progressBar(step, steps.length, label);
61
+ };
62
+
63
+ // 1. Frontend
64
+ tick('Generando frontend...');
65
+ await generateFrontend(config, projectPath);
29
66
 
30
- if (extras.includes('env')) await generateEnvFiles(config, projectPath);
31
- if (extras.includes('editorconfig')) await fse.writeFile(path.join(projectPath, '.editorconfig'), EDITORCONFIG);
32
- if (extras.includes('eslint')) await setupEslint(config, projectPath);
33
- if (extras.includes('husky')) await setupHusky(config, projectPath);
34
- if (extras.includes('testing')) await setupTesting(config, projectPath);
35
- if (extras.includes('ci')) await setupCI(config, projectPath);
36
- if (extras.includes('docker')) await generateDocker(config, projectPath);
67
+ // 2. Backend
68
+ tick('Generando backend...');
69
+ await generateBackend(config, projectPath);
70
+
71
+ // 3. README personalizado
72
+ tick('Generando README...');
73
+ await generateCustomReadme(config, projectPath);
37
74
 
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);
87
+ }
88
+ if (extras.includes('husky')) {
89
+ tick('Husky...');
90
+ await setupHusky(config, projectPath);
91
+ }
92
+ if (extras.includes('testing')) {
93
+ tick('Vitest...');
94
+ await setupTesting(config, projectPath);
95
+ }
96
+ if (extras.includes('ci')) {
97
+ tick('GitHub Actions...');
98
+ await setupCI(config, projectPath);
99
+ }
100
+ if (extras.includes('docker')) {
101
+ tick('Docker...');
102
+ await generateDocker(config, projectPath);
103
+ }
104
+ if (extras.includes('auth')) {
105
+ tick('Auth JWT...');
106
+ await setupAuth(config, projectPath);
107
+ }
108
+ if (extras.includes('tailwind')) {
109
+ tick('Tailwind CSS...');
110
+ await setupTailwind(config, projectPath);
111
+ }
38
112
  if (extras.includes('git') && isInstalled('git')) {
39
- try { run('git init', projectPath, 'Inicializando repositorio git'); } catch {}
113
+ tick('Git init...');
114
+ try { run('git init', projectPath, 'git init'); } catch {}
40
115
  await fse.writeFile(path.join(projectPath, '.gitignore'), GITIGNORE);
41
116
  }
42
117
 
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 {}
125
+ }
126
+ if (config.backendDeps?.length && isNode && isInstalled('npm')) {
127
+ tick('Instalando deps backend...');
128
+ const backendPath = path.join(projectPath, 'backend');
129
+ try {
130
+ spawnSync('npm', ['install', ...config.backendDeps], { cwd: backendPath, shell: true, stdio: 'pipe' });
131
+ } catch {}
132
+ }
133
+
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);
140
+
43
141
  // Resumen final
44
- const novatecGradient = gradient.default(['#00C6FF', '#0072FF', '#7B2FF7']);
45
142
  const backendCmd = {
46
- express: 'npm run dev',
47
- nestjs: 'npm run start:dev',
48
- fastify: 'npm run dev',
49
- hono: 'npm run dev',
50
- fastapi: 'uvicorn main:app --reload',
51
- django: 'python manage.py runserver',
52
- flask: 'python run.py',
53
- spring: 'mvn spring-boot:run',
54
- deno: 'deno task dev',
55
- gin: 'go run main.go'
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',
56
146
  };
57
147
 
58
148
  console.log(
59
- boxen.default(
60
- novatecGradient(' FULLSTACK CLI ◆\n\n') +
61
- chalk.green.bold(' ✅ Proyecto creado exitosamente!\n\n') +
62
- chalk.white(' 📁 Proyecto : ') + chalk.cyan.bold(config.name) + '\n' +
63
- chalk.white(' ⚛ Frontend : ') + chalk.cyan(config.frontend) + '\n' +
64
- chalk.white(' 🔧 Backend : ') + chalk.cyan(config.backend) + '\n\n' +
65
- chalk.bold.white(' ─── Cómo iniciar ───────────────────────────\n\n') +
66
- chalk.yellow(' Frontend:\n') +
67
- chalk.white(` cd ${config.name}\\frontend\n`) +
68
- chalk.cyan(' npm run dev\n\n') +
69
- chalk.yellow(' Backend:\n') +
70
- chalk.white(` cd ${config.name}\\backend\n`) +
71
- chalk.cyan(` ${backendCmd[config.backend] || 'npm start'}`),
72
- {
73
- padding: 1,
74
- margin: { top: 1, left: 2 },
75
- borderStyle: 'double',
76
- borderColor: 'cyan',
77
- }
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 }
78
160
  )
79
161
  );
162
+
163
+ // Abrir VS Code si está instalado
164
+ 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 {}
169
+ }
170
+
171
+ console.log();
172
+ }
173
+
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');
225
+ const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
226
+
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);
231
+
232
+ const middleware = `import jwt from 'jsonwebtoken';
233
+
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' });
242
+ }
243
+ }
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 {}
80
278
  }
81
279
 
82
- // ── ESLint + Prettier ─────────────────────────────────────────────────────────
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);
306
+ }
307
+
308
+ try {
309
+ spawnSync('npm', ['install', '-D', 'tailwindcss', 'postcss', 'autoprefixer'], { cwd: frontendPath, shell: true, stdio: 'pipe' });
310
+ } catch {}
311
+ }
312
+
313
+ // ── ESLint ────────────────────────────────────────────────────────────────────
83
314
  async function setupEslint(config, projectPath) {
84
315
  const frontendPath = path.join(projectPath, 'frontend');
85
- const eslintConfig = `{
86
- "env": { "browser": true, "es2021": true, "node": true },
87
- "extends": ["eslint:recommended"],
88
- "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" },
89
- "rules": { "no-unused-vars": "warn", "no-console": "off" }
90
- }\n`;
91
- const prettierConfig = `{
92
- "semi": true,
93
- "singleQuote": true,
94
- "tabWidth": 2,
95
- "trailingComma": "es5"
96
- }\n`;
97
-
98
- await fse.writeFile(path.join(frontendPath, '.eslintrc.json'), eslintConfig);
99
- await fse.writeFile(path.join(frontendPath, '.prettierrc'), prettierConfig);
100
- run('npm install -D eslint prettier eslint-config-prettier', frontendPath, 'Instalando ESLint + Prettier');
101
- console.log(chalk.green('✔ ESLint + Prettier configurados'));
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 {}
102
319
  }
103
320
 
104
- // ── Husky + lint-staged ───────────────────────────────────────────────────────
321
+ // ── Husky ─────────────────────────────────────────────────────────────────────
105
322
  async function setupHusky(config, projectPath) {
106
323
  const frontendPath = path.join(projectPath, 'frontend');
107
324
  try {
108
- run('npm install -D husky lint-staged', frontendPath, 'Instalando Husky + lint-staged');
109
- run('npx husky init', frontendPath, 'Inicializando Husky');
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'));
110
328
  await fse.writeFile(path.join(frontendPath, '.husky', 'pre-commit'), '#!/bin/sh\nnpx lint-staged\n');
111
- const pkgPath = path.join(frontendPath, 'package.json');
112
- const pkg = await fse.readJSON(pkgPath);
113
- pkg['lint-staged'] = { '*.{js,jsx,ts,tsx,vue}': ['eslint --fix', 'prettier --write'] };
114
- await fse.writeJSON(pkgPath, pkg, { spaces: 2 });
115
- console.log(chalk.green('✔ Husky + lint-staged configurados'));
116
- } catch { console.log(chalk.yellow('⚠ Husky requiere git init primero')); }
329
+ } catch {}
117
330
  }
118
331
 
119
332
  // ── Testing ───────────────────────────────────────────────────────────────────
120
333
  async function setupTesting(config, projectPath) {
121
334
  const frontendPath = path.join(projectPath, 'frontend');
122
- const isReactLike = ['react', 'next', 'solid', 'remix', 'qwik'].includes(config.frontend);
123
-
124
- run('npm install -D vitest @vitest/ui', frontendPath, 'Instalando Vitest');
125
-
126
335
  const testDir = path.join(frontendPath, 'src', '__tests__');
127
336
  await fse.ensureDir(testDir);
128
-
129
- const sampleTest = isReactLike
130
- ? `import { describe, it, expect } from 'vitest';\n\ndescribe('App', () => {\n it('should work', () => {\n expect(1 + 1).toBe(2);\n });\n});\n`
131
- : `import { describe, it, expect } from 'vitest';\n\ndescribe('App', () => {\n it('should work', () => {\n expect(true).toBe(true);\n });\n});\n`;
132
-
133
- await fse.writeFile(path.join(testDir, 'app.test.js'), sampleTest);
134
-
135
- const pkgPath = path.join(frontendPath, 'package.json');
136
- const pkg = await fse.readJSON(pkgPath);
137
- pkg.scripts = { ...pkg.scripts, test: 'vitest', 'test:ui': 'vitest --ui' };
138
- await fse.writeJSON(pkgPath, pkg, { spaces: 2 });
139
- console.log(chalk.green('✔ Vitest configurado con test de ejemplo'));
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 {}
140
339
  }
141
340
 
142
- // ── GitHub Actions CI ─────────────────────────────────────────────────────────
341
+ // ── GitHub Actions ────────────────────────────────────────────────────────────
143
342
  async function setupCI(config, projectPath) {
144
343
  const ciDir = path.join(projectPath, '.github', 'workflows');
145
344
  await fse.ensureDir(ciDir);
146
-
147
- const ci = `name: CI
148
-
149
- on:
150
- push:
151
- branches: [main, develop]
152
- pull_request:
153
- branches: [main]
154
-
155
- jobs:
156
- frontend:
157
- runs-on: ubuntu-latest
158
- defaults:
159
- run:
160
- working-directory: frontend
161
- steps:
162
- - uses: actions/checkout@v4
163
- - uses: actions/setup-node@v4
164
- with:
165
- node-version: 20
166
- cache: npm
167
- cache-dependency-path: frontend/package-lock.json
168
- - run: npm ci
169
- - run: npm run build
170
- ${config.extras.includes('testing') ? ' - run: npm test\n' : ''}
171
- backend:
172
- runs-on: ubuntu-latest
173
- defaults:
174
- run:
175
- working-directory: backend
176
- steps:
177
- - uses: actions/checkout@v4
178
- ${['fastapi', 'django', 'flask'].includes(config.backend) ? ` - uses: actions/setup-python@v5
179
- with:
180
- python-version: '3.11'
181
- - run: pip install -r requirements.txt` : ` - uses: actions/setup-node@v4
182
- with:
183
- node-version: 20
184
- - run: npm ci`}
185
- `;
186
- await fse.writeFile(path.join(ciDir, 'ci.yml'), ci);
187
- console.log(chalk.green('✔ GitHub Actions CI configurado'));
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'}`);
188
347
  }
189
348
 
190
349
  // ── Docker ────────────────────────────────────────────────────────────────────
191
350
  async function generateDocker(config, projectPath) {
192
351
  const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
193
- const backendPort = isPython ? (config.backend === 'gin' ? '8080' : '8000') : '3001';
194
-
195
- const backendService = isPython
196
- ? ` backend:
197
- build: ./backend
198
- ports:
199
- - "${backendPort}:${backendPort}"
200
- volumes:
201
- - ./backend:/app
202
- environment:
203
- - DEBUG=True`
204
- : ` backend:
205
- build: ./backend
206
- ports:
207
- - "${backendPort}:${backendPort}"
208
- volumes:
209
- - ./backend:/app
210
- - /app/node_modules
211
- environment:
212
- - NODE_ENV=development`;
213
-
214
- const compose = `version: '3.9'
215
- services:
216
- frontend:
217
- build: ./frontend
218
- ports:
219
- - "5173:5173"
220
- volumes:
221
- - ./frontend:/app
222
- - /app/node_modules
223
- environment:
224
- - NODE_ENV=development
225
- ${backendService}
226
- db:
227
- image: postgres:16-alpine
228
- environment:
229
- POSTGRES_DB: mydb
230
- POSTGRES_USER: user
231
- POSTGRES_PASSWORD: password
232
- ports:
233
- - "5432:5432"
234
- volumes:
235
- - pgdata:/var/lib/postgresql/data
236
-
237
- volumes:
238
- pgdata:
239
- `;
240
- await fse.writeFile(path.join(projectPath, 'docker-compose.yml'), compose);
241
- console.log(chalk.green('✔ docker-compose.yml generado (con PostgreSQL)'));
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
+ );
242
360
  }
243
361
 
244
- // ── Archivos .env ─────────────────────────────────────────────────────────────
362
+ // ── .env ──────────────────────────────────────────────────────────────────────
245
363
  async function generateEnvFiles(config, projectPath) {
246
364
  await fse.writeFile(path.join(projectPath, 'frontend', '.env.example'), 'VITE_API_URL=http://localhost:3001\n');
247
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' : '';
248
370
  if (!isPython) {
249
- await fse.writeFile(path.join(projectPath, 'backend', '.env.example'), 'PORT=3001\nDB_URL=\nJWT_SECRET=change_me\nNODE_ENV=development\n');
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
+ );
250
374
  }
251
- console.log(chalk.green('✔ Archivos .env.example generados'));
252
375
  }
253
376
 
254
- const EDITORCONFIG = `root = true
255
- [*]
256
- indent_style = space
257
- indent_size = 2
258
- end_of_line = lf
259
- charset = utf-8
260
- trim_trailing_whitespace = true
261
- insert_final_newline = true
262
- `;
263
-
264
- const GITIGNORE = `node_modules/
265
- dist/
266
- .next/
267
- .nuxt/
268
- .svelte-kit/
269
- .env
270
- *.log
271
- __pycache__/
272
- .venv/
273
- *.pyc
274
- target/
275
- *.class
276
- `;
277
-
278
- export { runCreate };
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`;