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