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/bin/index.js +140 -118
- package/lib/add.js +122 -0
- package/lib/create.js +248 -237
- package/lib/doctor.js +167 -0
- package/lib/generators/business.js +262 -0
- package/lib/generators/deploy.js +166 -0
- package/lib/generators/docker.js +97 -0
- package/lib/generators/security.js +132 -0
- package/lib/prompts.js +208 -310
- package/package.json +1 -1
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 {
|
|
8
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
17
|
-
|
|
74
|
+
// 6. Docker
|
|
75
|
+
if (extras.includes('docker')) {
|
|
76
|
+
tick('Docker...');
|
|
77
|
+
await generateDocker(config, root);
|
|
18
78
|
}
|
|
19
79
|
|
|
20
|
-
|
|
21
|
-
|
|
80
|
+
// 7. Seguridad
|
|
81
|
+
if (extras.includes('security')) {
|
|
82
|
+
tick('Seguridad...');
|
|
83
|
+
await generateSecurity(config, root);
|
|
84
|
+
}
|
|
22
85
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
86
|
+
// 8. GitHub Actions
|
|
87
|
+
if (extras.includes('ci')) {
|
|
88
|
+
tick('GitHub Actions...');
|
|
89
|
+
await generateDeploy(config, root);
|
|
90
|
+
}
|
|
26
91
|
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
if (extras.includes('
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
// ──
|
|
82
|
-
async function
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}\n
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
await fse.
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
// ──
|
|
104
|
-
async function
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
// ──
|
|
119
|
-
async function
|
|
120
|
-
const
|
|
121
|
-
const
|
|
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
|
-
|
|
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
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
await fse.
|
|
138
|
-
|
|
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
|
-
// ──
|
|
142
|
-
async function
|
|
143
|
-
const
|
|
144
|
-
await fse.ensureDir(
|
|
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
|
-
// ──
|
|
190
|
-
async function
|
|
268
|
+
// ── LOGS ──────────────────────────────────────────────────────────────────────
|
|
269
|
+
async function setupLogs(config, root) {
|
|
191
270
|
const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
// ──
|
|
244
|
-
async function
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
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`;
|