@openfactu/cli 0.0.7 → 0.0.8
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/README.md +161 -5
- package/dist/src/commands/backup.d.ts +2 -0
- package/dist/src/commands/backup.js +424 -0
- package/dist/src/commands/deploy.js +486 -67
- package/dist/src/commands/doctor.d.ts +2 -0
- package/dist/src/commands/doctor.js +295 -0
- package/dist/src/commands/install-quick.d.ts +2 -0
- package/dist/src/commands/install-quick.js +249 -0
- package/dist/src/commands/install-script.d.ts +2 -0
- package/dist/src/commands/install-script.js +474 -0
- package/dist/src/commands/install.js +966 -72
- package/dist/src/commands/monitoring.d.ts +2 -0
- package/dist/src/commands/monitoring.js +352 -0
- package/dist/src/commands/service.d.ts +2 -0
- package/dist/src/commands/service.js +402 -0
- package/dist/src/commands/setup.js +7 -2
- package/dist/src/commands/sync-ports.d.ts +2 -0
- package/dist/src/commands/sync-ports.js +298 -0
- package/dist/src/commands/uninstall.d.ts +2 -0
- package/dist/src/commands/uninstall.js +189 -0
- package/dist/src/index.js +17 -1
- package/dist/src/utils/config.d.ts +8 -0
- package/dist/src/utils/config.js +25 -1
- package/dist/src/utils/env.d.ts +11 -0
- package/dist/src/utils/env.js +31 -0
- package/dist/src/utils/helpers.d.ts +22 -0
- package/dist/src/utils/helpers.js +244 -0
- package/dist/src/utils/monitoring.d.ts +38 -0
- package/dist/src/utils/monitoring.js +353 -0
- package/dist/src/utils/paths.d.ts +1 -0
- package/dist/src/utils/paths.js +2 -0
- package/package.json +8 -5
|
@@ -13,6 +13,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
13
13
|
const os_1 = __importDefault(require("os"));
|
|
14
14
|
const logger_1 = require("../utils/logger");
|
|
15
15
|
const paths_1 = require("../utils/paths");
|
|
16
|
+
const helpers_1 = require("../utils/helpers");
|
|
16
17
|
function getLocalIPs() {
|
|
17
18
|
const interfaces = os_1.default.networkInterfaces();
|
|
18
19
|
const ips = [];
|
|
@@ -27,6 +28,26 @@ function getLocalIPs() {
|
|
|
27
28
|
}
|
|
28
29
|
return ips;
|
|
29
30
|
}
|
|
31
|
+
function checkPortInUse(port) {
|
|
32
|
+
try {
|
|
33
|
+
const output = (0, child_process_1.execSync)(`lsof -i :${port} -sTCP:LISTEN -t 2>/dev/null | grep -v 'docker-pr' || true`, {
|
|
34
|
+
stdio: 'pipe',
|
|
35
|
+
}).toString().trim();
|
|
36
|
+
if (output) {
|
|
37
|
+
const pid = output.split('\n')[0].trim();
|
|
38
|
+
let processName = 'desconocido';
|
|
39
|
+
try {
|
|
40
|
+
processName = (0, child_process_1.execSync)(`ps -p ${pid} -o comm= 2>/dev/null || echo 'desconocido'`, { stdio: 'pipe' }).toString().trim();
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
43
|
+
return { inUse: true, process: `${processName} (PID: ${pid})` };
|
|
44
|
+
}
|
|
45
|
+
return { inUse: false };
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return { inUse: false };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
30
51
|
function readEnv(envPath) {
|
|
31
52
|
const env = {};
|
|
32
53
|
if (!fs_1.default.existsSync(envPath))
|
|
@@ -54,7 +75,8 @@ function registerDeployCommand(program) {
|
|
|
54
75
|
program
|
|
55
76
|
.command('deploy')
|
|
56
77
|
.description('Configura OpenFactu para producción (acceso externo)')
|
|
57
|
-
.
|
|
78
|
+
.option('--with-monitoring', 'Incluir stack de monitoreo (Grafana, Prometheus, etc.)')
|
|
79
|
+
.action(async (opts) => {
|
|
58
80
|
console.log();
|
|
59
81
|
console.log(chalk_1.default.bold.white(' OpenFactu — Configurar Despliegue'));
|
|
60
82
|
console.log(chalk_1.default.dim(' ────────────────────────────────────'));
|
|
@@ -104,6 +126,15 @@ function registerDeployCommand(program) {
|
|
|
104
126
|
else {
|
|
105
127
|
host = selectedIP;
|
|
106
128
|
}
|
|
129
|
+
const { ssl } = await inquirer_1.default.prompt([
|
|
130
|
+
{
|
|
131
|
+
type: 'confirm',
|
|
132
|
+
name: 'ssl',
|
|
133
|
+
message: '¿Usar HTTPS? (certificado auto-firmado para red local)',
|
|
134
|
+
default: false,
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
useSSL = ssl;
|
|
107
138
|
}
|
|
108
139
|
else if (mode === 'public') {
|
|
109
140
|
const { domain } = await inquirer_1.default.prompt([
|
|
@@ -124,6 +155,50 @@ function registerDeployCommand(program) {
|
|
|
124
155
|
]);
|
|
125
156
|
useSSL = ssl;
|
|
126
157
|
}
|
|
158
|
+
// Verificar conflictos de puertos si se usa SSL
|
|
159
|
+
let httpPort = '80';
|
|
160
|
+
let httpsPort = '443';
|
|
161
|
+
if (useSSL) {
|
|
162
|
+
const port80 = checkPortInUse(80);
|
|
163
|
+
const port443 = checkPortInUse(443);
|
|
164
|
+
if (port80.inUse || port443.inUse) {
|
|
165
|
+
logger_1.log.blank();
|
|
166
|
+
logger_1.log.warn('Puertos en conflicto detectados:');
|
|
167
|
+
if (port80.inUse)
|
|
168
|
+
logger_1.log.warn(` Puerto 80: ocupado por ${port80.process}`);
|
|
169
|
+
if (port443.inUse)
|
|
170
|
+
logger_1.log.warn(` Puerto 443: ocupado por ${port443.process}`);
|
|
171
|
+
logger_1.log.blank();
|
|
172
|
+
const { portAction } = await inquirer_1.default.prompt([
|
|
173
|
+
{
|
|
174
|
+
type: 'list',
|
|
175
|
+
name: 'portAction',
|
|
176
|
+
message: '¿Cómo resolver el conflicto?',
|
|
177
|
+
choices: [
|
|
178
|
+
{ name: 'Usar puertos alternativos (8080/8443)', value: 'alternate' },
|
|
179
|
+
{ name: 'Especificar puertos personalizados', value: 'custom' },
|
|
180
|
+
{ name: 'Continuar con 80/443 (puede fallar)', value: 'continue' },
|
|
181
|
+
{ name: 'Desactivar HTTPS', value: 'nossl' },
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
]);
|
|
185
|
+
if (portAction === 'nossl') {
|
|
186
|
+
useSSL = false;
|
|
187
|
+
}
|
|
188
|
+
else if (portAction === 'alternate') {
|
|
189
|
+
httpPort = '8080';
|
|
190
|
+
httpsPort = '8443';
|
|
191
|
+
}
|
|
192
|
+
else if (portAction === 'custom') {
|
|
193
|
+
const { customHttp, customHttps } = await inquirer_1.default.prompt([
|
|
194
|
+
{ type: 'input', name: 'customHttp', message: 'Puerto HTTP alternativo:', default: '8080' },
|
|
195
|
+
{ type: 'input', name: 'customHttps', message: 'Puerto HTTPS alternativo:', default: '8443' },
|
|
196
|
+
]);
|
|
197
|
+
httpPort = customHttp;
|
|
198
|
+
httpsPort = customHttps;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
127
202
|
// Puertos
|
|
128
203
|
const { ports } = await inquirer_1.default.prompt([
|
|
129
204
|
{
|
|
@@ -143,29 +218,36 @@ function registerDeployCommand(program) {
|
|
|
143
218
|
serverPort = answers.serverPort;
|
|
144
219
|
dbPort = answers.dbPort;
|
|
145
220
|
}
|
|
146
|
-
// Password de BD
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
name: 'dbPassword',
|
|
151
|
-
message: 'Password de PostgreSQL:',
|
|
152
|
-
default: 'openfactu_pass',
|
|
153
|
-
},
|
|
154
|
-
]);
|
|
221
|
+
// Password de BD: se reutiliza la fijada en `install` (está en el .env).
|
|
222
|
+
// No se vuelve a preguntar: cambiarla aquí no surte efecto sobre un
|
|
223
|
+
// volumen de Postgres ya inicializado, así que evitamos esa trampa.
|
|
224
|
+
const dbPassword = readEnv(envPath).POSTGRES_PASSWORD || (0, helpers_1.generatePassword)(24);
|
|
155
225
|
// 3. Construir configuración
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
226
|
+
// Con SSL el ÚNICO punto de entrada es Caddy en httpsPort: enruta
|
|
227
|
+
// "/" → web:80 y "/api/*" → server:3000. Por eso las URLs de cara al
|
|
228
|
+
// usuario (web, API, VITE_API_URL, CORS_ORIGIN) deben apuntar a Caddy,
|
|
229
|
+
// no a los puertos HTTP planos web/api (8080/3000).
|
|
230
|
+
let webUrl;
|
|
231
|
+
let apiUrl;
|
|
232
|
+
if (useSSL) {
|
|
233
|
+
const base = httpsPort === '443' ? `https://${host}` : `https://${host}:${httpsPort}`;
|
|
234
|
+
webUrl = base;
|
|
235
|
+
apiUrl = base;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
webUrl = webPort === '80' || webPort === '443'
|
|
239
|
+
? `http://${host}`
|
|
240
|
+
: `http://${host}:${webPort}`;
|
|
241
|
+
apiUrl = serverPort === '80' || serverPort === '443'
|
|
242
|
+
? `http://${host}`
|
|
243
|
+
: `http://${host}:${serverPort}`;
|
|
244
|
+
}
|
|
163
245
|
logger_1.log.blank();
|
|
164
246
|
logger_1.log.title(' Resumen de configuración');
|
|
165
247
|
logger_1.log.info(`Web: ${chalk_1.default.cyan(webUrl)}`);
|
|
166
248
|
logger_1.log.info(`API: ${chalk_1.default.cyan(apiUrl)}`);
|
|
167
249
|
logger_1.log.info(`BD Puerto: ${chalk_1.default.cyan(dbPort)} ${chalk_1.default.dim('(host)')}`);
|
|
168
|
-
logger_1.log.info(`BD Password: ${chalk_1.default.dim(
|
|
250
|
+
logger_1.log.info(`BD Password: ${chalk_1.default.dim('(reutilizada de install)')}`);
|
|
169
251
|
logger_1.log.info(`SSL: ${useSSL ? chalk_1.default.green('Si') : chalk_1.default.dim('No')}`);
|
|
170
252
|
logger_1.log.blank();
|
|
171
253
|
const { confirm } = await inquirer_1.default.prompt([
|
|
@@ -184,7 +266,7 @@ function registerDeployCommand(program) {
|
|
|
184
266
|
env.POSTGRES_USER = env.POSTGRES_USER || 'openfactu';
|
|
185
267
|
env.POSTGRES_PASSWORD = dbPassword;
|
|
186
268
|
env.POSTGRES_DB = env.POSTGRES_DB || 'openfactudb';
|
|
187
|
-
env.DATABASE_URL = `postgresql://${env.POSTGRES_USER}:${dbPassword}@db:5432/${env.POSTGRES_DB}`;
|
|
269
|
+
env.DATABASE_URL = `postgresql://${env.POSTGRES_USER}:${encodeURIComponent(dbPassword)}@db:5432/${env.POSTGRES_DB}`;
|
|
188
270
|
env.VITE_API_URL = apiUrl;
|
|
189
271
|
env.HOST = host;
|
|
190
272
|
env.CORS_ORIGIN = webUrl;
|
|
@@ -244,47 +326,164 @@ function registerDeployCommand(program) {
|
|
|
244
326
|
restart: unless-stopped
|
|
245
327
|
networks:
|
|
246
328
|
- openfactu_net
|
|
329
|
+
`;
|
|
330
|
+
// Si SSL, añadir Caddy reverse proxy con Let's Encrypt (servicio dentro de
|
|
331
|
+
// services:). El bloque top-level networks: se añade SIEMPRE al final.
|
|
332
|
+
if (useSSL) {
|
|
333
|
+
composeContent += `
|
|
334
|
+
caddy:
|
|
335
|
+
image: caddy:2-alpine
|
|
336
|
+
container_name: openfactu-caddy
|
|
337
|
+
ports:
|
|
338
|
+
- "0.0.0.0:${httpPort}:80"
|
|
339
|
+
- "0.0.0.0:${httpsPort}:443"
|
|
340
|
+
volumes:
|
|
341
|
+
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
342
|
+
- caddy_data:/data
|
|
343
|
+
- caddy_config:/config
|
|
344
|
+
depends_on:
|
|
345
|
+
- web
|
|
346
|
+
- server
|
|
347
|
+
restart: unless-stopped
|
|
348
|
+
networks:
|
|
349
|
+
- openfactu_net
|
|
247
350
|
|
|
351
|
+
volumes:
|
|
352
|
+
caddy_data:
|
|
353
|
+
caddy_config:
|
|
354
|
+
`;
|
|
355
|
+
// Generar Caddyfile
|
|
356
|
+
const caddyfilePath = path_1.default.join(root, 'Caddyfile');
|
|
357
|
+
const isLAN = mode === 'lan';
|
|
358
|
+
const tlsDirective = isLAN ? 'tls internal' : '';
|
|
359
|
+
const httpRedirect = isLAN ? '' : `
|
|
360
|
+
http://${host} {
|
|
361
|
+
redir https://{host}{uri} permanent
|
|
362
|
+
}
|
|
363
|
+
`;
|
|
364
|
+
const caddyfileContent = `${host} {
|
|
365
|
+
${tlsDirective}
|
|
366
|
+
encode gzip
|
|
367
|
+
|
|
368
|
+
handle /api/* {
|
|
369
|
+
reverse_proxy server:3000
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
handle {
|
|
373
|
+
reverse_proxy web:80
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
log {
|
|
377
|
+
output file /var/log/caddy/access.log
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
${httpRedirect}`;
|
|
381
|
+
fs_1.default.writeFileSync(caddyfilePath, caddyfileContent);
|
|
382
|
+
composeContent += `
|
|
248
383
|
networks:
|
|
249
384
|
openfactu_net:
|
|
250
385
|
driver: bridge
|
|
251
386
|
`;
|
|
252
|
-
|
|
253
|
-
|
|
387
|
+
fs_1.default.writeFileSync(prodComposePath, composeContent);
|
|
388
|
+
prodSpinner.succeed('docker-compose.prod.yml y Caddyfile generados');
|
|
389
|
+
logger_1.log.blank();
|
|
390
|
+
if (isLAN) {
|
|
391
|
+
logger_1.log.info('Caddy generará certificado auto-firmado para la red local');
|
|
392
|
+
logger_1.log.dim(' Los navegadores mostrarán advertencia de seguridad (es normal)');
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
logger_1.log.info('Caddy obtendrá certificados Let\'s Encrypt automáticamente');
|
|
396
|
+
logger_1.log.dim(' El DNS debe apuntar a este servidor');
|
|
397
|
+
logger_1.log.dim(' El primer request tardará unos segundos mientras se obtiene el certificado');
|
|
398
|
+
}
|
|
399
|
+
if (httpPort !== '80' || httpsPort !== '443') {
|
|
400
|
+
logger_1.log.blank();
|
|
401
|
+
logger_1.log.info(`Puertos alternativos: HTTP=${httpPort}, HTTPS=${httpsPort}`);
|
|
402
|
+
logger_1.log.dim(' Asegúrate de abrir estos puertos en el firewall');
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
254
406
|
composeContent += `
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
# caddy:
|
|
259
|
-
# image: caddy:2-alpine
|
|
260
|
-
# ports:
|
|
261
|
-
# - "0.0.0.0:80:80"
|
|
262
|
-
# - "0.0.0.0:443:443"
|
|
263
|
-
# volumes:
|
|
264
|
-
# - ./Caddyfile:/etc/caddy/Caddyfile
|
|
265
|
-
# - caddy_data:/data
|
|
266
|
-
# depends_on:
|
|
267
|
-
# - web
|
|
268
|
-
# - server
|
|
269
|
-
# networks:
|
|
270
|
-
# - openfactu_net
|
|
271
|
-
#
|
|
272
|
-
# volumes:
|
|
273
|
-
# caddy_data:
|
|
274
|
-
#
|
|
275
|
-
# Caddyfile:
|
|
276
|
-
# ${host} {
|
|
277
|
-
# handle /api/* {
|
|
278
|
-
# reverse_proxy server:3000
|
|
279
|
-
# }
|
|
280
|
-
# handle {
|
|
281
|
-
# reverse_proxy web:80
|
|
282
|
-
# }
|
|
283
|
-
# }
|
|
407
|
+
networks:
|
|
408
|
+
openfactu_net:
|
|
409
|
+
driver: bridge
|
|
284
410
|
`;
|
|
411
|
+
fs_1.default.writeFileSync(prodComposePath, composeContent);
|
|
412
|
+
prodSpinner.succeed('docker-compose.prod.yml generado');
|
|
413
|
+
}
|
|
414
|
+
// Generar docker-compose.prod.monitoring.yml si se pidió monitoreo
|
|
415
|
+
if (opts.withMonitoring) {
|
|
416
|
+
const monSpinner = (0, ora_1.default)('Generando docker-compose.prod.monitoring.yml...').start();
|
|
417
|
+
const monitoringComposePath = path_1.default.join(root, 'docker-compose.prod.monitoring.yml');
|
|
418
|
+
const monitoringCompose = `services:
|
|
419
|
+
pgadmin:
|
|
420
|
+
image: dpage/pgadmin4:latest
|
|
421
|
+
environment:
|
|
422
|
+
PGADMIN_DEFAULT_EMAIL: \${PGADMIN_EMAIL:-admin@openfactu.local}
|
|
423
|
+
PGADMIN_DEFAULT_PASSWORD: \${PGADMIN_PASSWORD:-admin}
|
|
424
|
+
PGADMIN_CONFIG_SERVER_MODE: 'False'
|
|
425
|
+
ports:
|
|
426
|
+
- "0.0.0.0:\${PGADMIN_PORT:-5050}:80"
|
|
427
|
+
volumes:
|
|
428
|
+
- ./storage/pgadmin_data:/var/lib/pgadmin
|
|
429
|
+
depends_on:
|
|
430
|
+
- db
|
|
431
|
+
restart: unless-stopped
|
|
432
|
+
networks:
|
|
433
|
+
- openfactu_net
|
|
434
|
+
|
|
435
|
+
grafana:
|
|
436
|
+
image: grafana/grafana:latest
|
|
437
|
+
environment:
|
|
438
|
+
- GF_SECURITY_ADMIN_USER=\${GRAFANA_USER:-admin}
|
|
439
|
+
- GF_SECURITY_ADMIN_PASSWORD=\${GRAFANA_PASSWORD:-admin}
|
|
440
|
+
- GF_USERS_ALLOW_SIGN_UP=false
|
|
441
|
+
ports:
|
|
442
|
+
- "0.0.0.0:\${GRAFANA_PORT:-3001}:3000"
|
|
443
|
+
volumes:
|
|
444
|
+
- ./storage/grafana_data:/var/lib/grafana
|
|
445
|
+
depends_on:
|
|
446
|
+
- prometheus
|
|
447
|
+
restart: unless-stopped
|
|
448
|
+
networks:
|
|
449
|
+
- openfactu_net
|
|
450
|
+
|
|
451
|
+
prometheus:
|
|
452
|
+
image: prom/prometheus:latest
|
|
453
|
+
command:
|
|
454
|
+
- '--config.file=/etc/prometheus/prometheus.yml'
|
|
455
|
+
- '--storage.tsdb.path=/prometheus'
|
|
456
|
+
- '--storage.tsdb.retention.time=15d'
|
|
457
|
+
- '--web.enable-lifecycle'
|
|
458
|
+
ports:
|
|
459
|
+
- "0.0.0.0:\${PROMETHEUS_PORT:-9090}:9090"
|
|
460
|
+
volumes:
|
|
461
|
+
- ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
|
462
|
+
- ./storage/prometheus_data:/prometheus
|
|
463
|
+
restart: unless-stopped
|
|
464
|
+
networks:
|
|
465
|
+
- openfactu_net
|
|
466
|
+
|
|
467
|
+
portainer:
|
|
468
|
+
image: portainer/portainer-ce:latest
|
|
469
|
+
command: -H unix:///var/run/docker.sock
|
|
470
|
+
ports:
|
|
471
|
+
- "0.0.0.0:\${PORTAINER_PORT:-9000}:9000"
|
|
472
|
+
volumes:
|
|
473
|
+
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
474
|
+
- ./storage/portainer_data:/data
|
|
475
|
+
restart: unless-stopped
|
|
476
|
+
networks:
|
|
477
|
+
- openfactu_net
|
|
478
|
+
|
|
479
|
+
networks:
|
|
480
|
+
openfactu_net:
|
|
481
|
+
name: openfactu_net
|
|
482
|
+
driver: bridge
|
|
483
|
+
`;
|
|
484
|
+
fs_1.default.writeFileSync(monitoringComposePath, monitoringCompose);
|
|
485
|
+
monSpinner.succeed('docker-compose.prod.monitoring.yml generado');
|
|
285
486
|
}
|
|
286
|
-
fs_1.default.writeFileSync(prodComposePath, composeContent);
|
|
287
|
-
prodSpinner.succeed('docker-compose.prod.yml generado');
|
|
288
487
|
// 6. Preguntar si levantar
|
|
289
488
|
logger_1.log.blank();
|
|
290
489
|
const { start } = await inquirer_1.default.prompt([
|
|
@@ -323,7 +522,9 @@ networks:
|
|
|
323
522
|
else if (mode === 'public') {
|
|
324
523
|
logger_1.log.info('Asegúrate de que los puertos estén abiertos en el firewall');
|
|
325
524
|
if (useSSL) {
|
|
326
|
-
logger_1.log.info('
|
|
525
|
+
logger_1.log.info('Caddy obtendrá certificado Let\'s Encrypt automáticamente');
|
|
526
|
+
logger_1.log.dim(' El DNS debe apuntar a este servidor para que funcione');
|
|
527
|
+
logger_1.log.dim(' Puertos requeridos: 80 (HTTP) y 443 (HTTPS)');
|
|
327
528
|
}
|
|
328
529
|
}
|
|
329
530
|
logger_1.log.blank();
|
|
@@ -338,31 +539,223 @@ networks:
|
|
|
338
539
|
process.exitCode = 1;
|
|
339
540
|
}
|
|
340
541
|
});
|
|
542
|
+
// ── openfactu deploy:ssl ──
|
|
543
|
+
program
|
|
544
|
+
.command('deploy:ssl')
|
|
545
|
+
.description('Activa HTTPS en un despliegue existente con Caddy')
|
|
546
|
+
.option('--domain <domain>', 'Dominio para el certificado')
|
|
547
|
+
.option('--lan', 'Usar certificado auto-firmado para red local')
|
|
548
|
+
.option('--port <port>', 'Puerto HTTPS (default: 443)', '443')
|
|
549
|
+
.action(async (opts) => {
|
|
550
|
+
console.log();
|
|
551
|
+
console.log(chalk_1.default.bold.white(' OpenFactu — Activar HTTPS'));
|
|
552
|
+
console.log(chalk_1.default.dim(' ────────────────────────────────────'));
|
|
553
|
+
console.log();
|
|
554
|
+
try {
|
|
555
|
+
const root = (0, paths_1.getProjectRoot)();
|
|
556
|
+
const envPath = path_1.default.join(root, '.env');
|
|
557
|
+
const env = readEnv(envPath);
|
|
558
|
+
const host = opts.domain || env.HOST || 'localhost';
|
|
559
|
+
const isLAN = opts.lan || false;
|
|
560
|
+
if (!isLAN && !opts.domain) {
|
|
561
|
+
const { domain } = await inquirer_1.default.prompt([
|
|
562
|
+
{
|
|
563
|
+
type: 'input',
|
|
564
|
+
name: 'domain',
|
|
565
|
+
message: 'Dominio para el certificado Let\'s Encrypt:',
|
|
566
|
+
default: host,
|
|
567
|
+
},
|
|
568
|
+
]);
|
|
569
|
+
opts.domain = domain;
|
|
570
|
+
}
|
|
571
|
+
const finalHost = opts.domain || host;
|
|
572
|
+
let httpsPort = opts.port || '443';
|
|
573
|
+
let httpPort = '80';
|
|
574
|
+
// Verificar conflictos de puertos
|
|
575
|
+
const port80 = checkPortInUse(80);
|
|
576
|
+
const port443 = checkPortInUse(443);
|
|
577
|
+
if (port80.inUse || port443.inUse) {
|
|
578
|
+
logger_1.log.blank();
|
|
579
|
+
logger_1.log.warn('Puertos en conflicto detectados:');
|
|
580
|
+
if (port80.inUse)
|
|
581
|
+
logger_1.log.warn(` Puerto 80: ocupado por ${port80.process}`);
|
|
582
|
+
if (port443.inUse)
|
|
583
|
+
logger_1.log.warn(` Puerto 443: ocupado por ${port443.process}`);
|
|
584
|
+
logger_1.log.blank();
|
|
585
|
+
const { portAction } = await inquirer_1.default.prompt([
|
|
586
|
+
{
|
|
587
|
+
type: 'list',
|
|
588
|
+
name: 'portAction',
|
|
589
|
+
message: '¿Cómo resolver el conflicto?',
|
|
590
|
+
choices: [
|
|
591
|
+
{ name: 'Usar puertos alternativos (8080/8443)', value: 'alternate' },
|
|
592
|
+
{ name: 'Especificar puertos personalizados', value: 'custom' },
|
|
593
|
+
{ name: 'Continuar con 80/443 (puede fallar)', value: 'continue' },
|
|
594
|
+
],
|
|
595
|
+
},
|
|
596
|
+
]);
|
|
597
|
+
if (portAction === 'alternate') {
|
|
598
|
+
httpPort = '8080';
|
|
599
|
+
httpsPort = '8443';
|
|
600
|
+
}
|
|
601
|
+
else if (portAction === 'custom') {
|
|
602
|
+
const { customHttp, customHttps } = await inquirer_1.default.prompt([
|
|
603
|
+
{ type: 'input', name: 'customHttp', message: 'Puerto HTTP alternativo:', default: '8080' },
|
|
604
|
+
{ type: 'input', name: 'customHttps', message: 'Puerto HTTPS alternativo:', default: '8443' },
|
|
605
|
+
]);
|
|
606
|
+
httpPort = customHttp;
|
|
607
|
+
httpsPort = customHttps;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// Generar Caddyfile
|
|
611
|
+
const tlsDirective = isLAN ? 'tls internal' : '';
|
|
612
|
+
const httpRedirect = isLAN ? '' : `
|
|
613
|
+
http://${finalHost} {
|
|
614
|
+
redir https://${finalHost}{uri} permanent
|
|
615
|
+
}
|
|
616
|
+
`;
|
|
617
|
+
const caddyfileContent = `${finalHost} {
|
|
618
|
+
${tlsDirective}
|
|
619
|
+
encode gzip
|
|
620
|
+
|
|
621
|
+
handle /api/* {
|
|
622
|
+
reverse_proxy server:3000
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
handle {
|
|
626
|
+
reverse_proxy web:80
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
log {
|
|
630
|
+
output file /var/log/caddy/access.log
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
${httpRedirect}`;
|
|
634
|
+
const caddyfilePath = path_1.default.join(root, 'Caddyfile');
|
|
635
|
+
fs_1.default.writeFileSync(caddyfilePath, caddyfileContent);
|
|
636
|
+
// Actualizar docker-compose.prod.yml para añadir Caddy
|
|
637
|
+
const prodComposePath = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
638
|
+
let composeContent = fs_1.default.existsSync(prodComposePath)
|
|
639
|
+
? fs_1.default.readFileSync(prodComposePath, 'utf-8')
|
|
640
|
+
: '';
|
|
641
|
+
if (!composeContent.includes('caddy:')) {
|
|
642
|
+
const caddyBlock = `
|
|
643
|
+
caddy:
|
|
644
|
+
image: caddy:2-alpine
|
|
645
|
+
container_name: openfactu-caddy
|
|
646
|
+
ports:
|
|
647
|
+
- "0.0.0.0:${httpPort}:80"
|
|
648
|
+
- "0.0.0.0:${httpsPort}:443"
|
|
649
|
+
volumes:
|
|
650
|
+
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
651
|
+
- caddy_data:/data
|
|
652
|
+
- caddy_config:/config
|
|
653
|
+
depends_on:
|
|
654
|
+
- web
|
|
655
|
+
- server
|
|
656
|
+
restart: unless-stopped
|
|
657
|
+
networks:
|
|
658
|
+
- openfactu_net
|
|
659
|
+
`;
|
|
660
|
+
const volumesBlock = `
|
|
661
|
+
volumes:
|
|
662
|
+
caddy_data:
|
|
663
|
+
caddy_config:
|
|
664
|
+
`;
|
|
665
|
+
// Insertar caddy DENTRO de services (antes del bloque top-level
|
|
666
|
+
// "networks:"), no al final, para que no quede anidado bajo networks:.
|
|
667
|
+
if (/\nnetworks:/.test(composeContent)) {
|
|
668
|
+
composeContent = composeContent.replace(/\nnetworks:/, () => `${caddyBlock}${volumesBlock}\nnetworks:`);
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
composeContent += caddyBlock + volumesBlock;
|
|
672
|
+
}
|
|
673
|
+
fs_1.default.writeFileSync(prodComposePath, composeContent);
|
|
674
|
+
}
|
|
675
|
+
// Actualizar .env
|
|
676
|
+
const protocol = 'https';
|
|
677
|
+
const webUrl = httpsPort === '443'
|
|
678
|
+
? `${protocol}://${finalHost}`
|
|
679
|
+
: `${protocol}://${finalHost}:${httpsPort}`;
|
|
680
|
+
env.VITE_API_URL = webUrl;
|
|
681
|
+
env.CORS_ORIGIN = webUrl;
|
|
682
|
+
env.HOST = finalHost;
|
|
683
|
+
writeEnv(envPath, env);
|
|
684
|
+
logger_1.log.blank();
|
|
685
|
+
logger_1.log.success('HTTPS configurado');
|
|
686
|
+
logger_1.log.blank();
|
|
687
|
+
logger_1.log.info(`URL: ${chalk_1.default.cyan(webUrl)}`);
|
|
688
|
+
if (httpPort !== '80' || httpsPort !== '443') {
|
|
689
|
+
logger_1.log.info(`Puertos: HTTP=${httpPort}, HTTPS=${httpsPort}`);
|
|
690
|
+
}
|
|
691
|
+
if (isLAN) {
|
|
692
|
+
logger_1.log.dim(' Certificado auto-firmado (los navegadores mostrarán advertencia)');
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
logger_1.log.dim(' Caddy obtendrá certificado Let\'s Encrypt en el primer request');
|
|
696
|
+
logger_1.log.dim(' El DNS debe apuntar a este servidor');
|
|
697
|
+
}
|
|
698
|
+
logger_1.log.blank();
|
|
699
|
+
const { restart } = await inquirer_1.default.prompt([
|
|
700
|
+
{
|
|
701
|
+
type: 'confirm',
|
|
702
|
+
name: 'restart',
|
|
703
|
+
message: '¿Reiniciar servicios para aplicar HTTPS?',
|
|
704
|
+
default: true,
|
|
705
|
+
},
|
|
706
|
+
]);
|
|
707
|
+
if (restart) {
|
|
708
|
+
const spinner = (0, ora_1.default)('Reiniciando servicios...').start();
|
|
709
|
+
try {
|
|
710
|
+
(0, child_process_1.execSync)('docker compose -f docker-compose.prod.yml up -d', {
|
|
711
|
+
cwd: root,
|
|
712
|
+
stdio: 'pipe',
|
|
713
|
+
timeout: 120000,
|
|
714
|
+
});
|
|
715
|
+
spinner.succeed('Servicios reiniciados');
|
|
716
|
+
}
|
|
717
|
+
catch (err) {
|
|
718
|
+
spinner.fail('Error: ' + err.message);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
catch (err) {
|
|
723
|
+
logger_1.log.error(err.message);
|
|
724
|
+
process.exitCode = 1;
|
|
725
|
+
}
|
|
726
|
+
});
|
|
341
727
|
// ── openfactu deploy:status ──
|
|
342
728
|
program
|
|
343
729
|
.command('deploy:status')
|
|
344
730
|
.description('Muestra el estado de los servicios Docker')
|
|
345
|
-
.
|
|
731
|
+
.option('--with-monitoring', 'Incluir servicios de monitoreo')
|
|
732
|
+
.action(async (opts) => {
|
|
346
733
|
try {
|
|
347
734
|
const root = (0, paths_1.getProjectRoot)();
|
|
348
735
|
const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
349
736
|
const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
|
|
737
|
+
const monitoringCompose = path_1.default.join(root, 'docker-compose.prod.monitoring.yml');
|
|
738
|
+
const useMonitoring = opts.withMonitoring && fs_1.default.existsSync(monitoringCompose);
|
|
350
739
|
logger_1.log.info(`Usando: ${chalk_1.default.dim(composeFile)}`);
|
|
740
|
+
if (useMonitoring)
|
|
741
|
+
logger_1.log.info(`Monitoreo: ${chalk_1.default.dim('docker-compose.prod.monitoring.yml')}`);
|
|
351
742
|
logger_1.log.blank();
|
|
352
|
-
const
|
|
743
|
+
const files = useMonitoring
|
|
744
|
+
? `-f ${composeFile} -f docker-compose.prod.monitoring.yml`
|
|
745
|
+
: `-f ${composeFile}`;
|
|
746
|
+
const output = (0, child_process_1.execSync)(`docker compose ${files} ps`, {
|
|
353
747
|
cwd: root,
|
|
354
748
|
}).toString();
|
|
355
749
|
console.log(output);
|
|
356
|
-
// Mostrar URLs
|
|
750
|
+
// Mostrar URLs realmente configuradas en .env (correctas con o sin SSL/Caddy)
|
|
357
751
|
const envPath = path_1.default.join(root, '.env');
|
|
358
752
|
const env = readEnv(envPath);
|
|
359
753
|
const host = env.HOST || 'localhost';
|
|
360
|
-
const
|
|
361
|
-
const
|
|
362
|
-
const protocol = env.VITE_API_URL?.startsWith('https') ? 'https' : 'http';
|
|
754
|
+
const webUrl = env.CORS_ORIGIN || `http://${host}:${env.WEB_PORT || '8080'}`;
|
|
755
|
+
const apiUrl = env.VITE_API_URL || `http://${host}:${env.SERVER_PORT || '3000'}`;
|
|
363
756
|
logger_1.log.blank();
|
|
364
|
-
logger_1.log.info(`Web: ${chalk_1.default.cyan(
|
|
365
|
-
logger_1.log.info(`API: ${chalk_1.default.cyan(
|
|
757
|
+
logger_1.log.info(`Web: ${chalk_1.default.cyan(webUrl)}`);
|
|
758
|
+
logger_1.log.info(`API: ${chalk_1.default.cyan(apiUrl)}`);
|
|
366
759
|
}
|
|
367
760
|
catch (err) {
|
|
368
761
|
logger_1.log.error('Docker no disponible o servicios no levantados');
|
|
@@ -375,18 +768,26 @@ networks:
|
|
|
375
768
|
.description('Reconstruye y reinicia los contenedores Docker')
|
|
376
769
|
.option('--service <name>', 'Reconstruir solo un servicio (web, server, db)')
|
|
377
770
|
.option('--no-cache', 'Construir sin cache de Docker')
|
|
771
|
+
.option('--with-monitoring', 'Incluir servicios de monitoreo')
|
|
378
772
|
.action(async (opts) => {
|
|
379
773
|
try {
|
|
380
774
|
const root = (0, paths_1.getProjectRoot)();
|
|
381
775
|
const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
382
776
|
const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
|
|
777
|
+
const monitoringCompose = path_1.default.join(root, 'docker-compose.prod.monitoring.yml');
|
|
778
|
+
const useMonitoring = opts.withMonitoring && fs_1.default.existsSync(monitoringCompose);
|
|
383
779
|
const service = opts.service || '';
|
|
384
780
|
const noCache = opts.cache === false ? ' --no-cache' : '';
|
|
385
781
|
logger_1.log.info(`Usando: ${chalk_1.default.dim(composeFile)}`);
|
|
782
|
+
if (useMonitoring)
|
|
783
|
+
logger_1.log.info(`Monitoreo: ${chalk_1.default.dim('docker-compose.prod.monitoring.yml')}`);
|
|
386
784
|
logger_1.log.blank();
|
|
785
|
+
const files = useMonitoring
|
|
786
|
+
? `-f ${composeFile} -f docker-compose.prod.monitoring.yml`
|
|
787
|
+
: `-f ${composeFile}`;
|
|
387
788
|
const buildSpinner = (0, ora_1.default)(`Construyendo${service ? ' ' + service : ' todos los servicios'}...`).start();
|
|
388
789
|
try {
|
|
389
|
-
(0, child_process_1.execSync)(`docker compose
|
|
790
|
+
(0, child_process_1.execSync)(`docker compose ${files} build${noCache} ${service}`, {
|
|
390
791
|
cwd: root,
|
|
391
792
|
stdio: 'pipe',
|
|
392
793
|
timeout: 600000,
|
|
@@ -408,7 +809,7 @@ networks:
|
|
|
408
809
|
}
|
|
409
810
|
const upSpinner = (0, ora_1.default)('Reiniciando servicios...').start();
|
|
410
811
|
try {
|
|
411
|
-
(0, child_process_1.execSync)(`docker compose
|
|
812
|
+
(0, child_process_1.execSync)(`docker compose ${files} up -d ${service}`, {
|
|
412
813
|
cwd: root,
|
|
413
814
|
stdio: 'pipe',
|
|
414
815
|
timeout: 60000,
|
|
@@ -441,14 +842,20 @@ networks:
|
|
|
441
842
|
.description('Muestra los logs de los servicios Docker')
|
|
442
843
|
.option('--service <name>', 'Logs de un servicio especifico (web, server, db)')
|
|
443
844
|
.option('-n, --lines <number>', 'Numero de lineas', '50')
|
|
845
|
+
.option('--with-monitoring', 'Incluir servicios de monitoreo')
|
|
444
846
|
.action(async (opts) => {
|
|
445
847
|
try {
|
|
446
848
|
const root = (0, paths_1.getProjectRoot)();
|
|
447
849
|
const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
448
850
|
const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
|
|
851
|
+
const monitoringCompose = path_1.default.join(root, 'docker-compose.prod.monitoring.yml');
|
|
852
|
+
const useMonitoring = opts.withMonitoring && fs_1.default.existsSync(monitoringCompose);
|
|
449
853
|
const service = opts.service || '';
|
|
450
854
|
const lines = opts.lines || '50';
|
|
451
|
-
|
|
855
|
+
const files = useMonitoring
|
|
856
|
+
? `-f ${composeFile} -f docker-compose.prod.monitoring.yml`
|
|
857
|
+
: `-f ${composeFile}`;
|
|
858
|
+
(0, child_process_1.execSync)(`docker compose ${files} logs --tail ${lines} ${service}`, {
|
|
452
859
|
cwd: root,
|
|
453
860
|
stdio: 'inherit',
|
|
454
861
|
});
|
|
@@ -461,13 +868,19 @@ networks:
|
|
|
461
868
|
program
|
|
462
869
|
.command('stop')
|
|
463
870
|
.description('Para todos los servicios Docker')
|
|
464
|
-
.
|
|
871
|
+
.option('--with-monitoring', 'Incluir servicios de monitoreo')
|
|
872
|
+
.action(async (opts) => {
|
|
465
873
|
try {
|
|
466
874
|
const root = (0, paths_1.getProjectRoot)();
|
|
467
875
|
const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
468
876
|
const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
|
|
877
|
+
const monitoringCompose = path_1.default.join(root, 'docker-compose.prod.monitoring.yml');
|
|
878
|
+
const useMonitoring = opts.withMonitoring && fs_1.default.existsSync(monitoringCompose);
|
|
879
|
+
const files = useMonitoring
|
|
880
|
+
? `-f ${composeFile} -f docker-compose.prod.monitoring.yml`
|
|
881
|
+
: `-f ${composeFile}`;
|
|
469
882
|
const spinner = (0, ora_1.default)('Parando servicios...').start();
|
|
470
|
-
(0, child_process_1.execSync)(`docker compose
|
|
883
|
+
(0, child_process_1.execSync)(`docker compose ${files} down`, { cwd: root, stdio: 'pipe' });
|
|
471
884
|
spinner.succeed('Servicios parados');
|
|
472
885
|
}
|
|
473
886
|
catch (err) {
|
|
@@ -479,14 +892,20 @@ networks:
|
|
|
479
892
|
.command('restart')
|
|
480
893
|
.description('Reinicia los servicios Docker (sin rebuild)')
|
|
481
894
|
.option('--service <name>', 'Reiniciar solo un servicio')
|
|
895
|
+
.option('--with-monitoring', 'Incluir servicios de monitoreo')
|
|
482
896
|
.action(async (opts) => {
|
|
483
897
|
try {
|
|
484
898
|
const root = (0, paths_1.getProjectRoot)();
|
|
485
899
|
const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
486
900
|
const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
|
|
901
|
+
const monitoringCompose = path_1.default.join(root, 'docker-compose.prod.monitoring.yml');
|
|
902
|
+
const useMonitoring = opts.withMonitoring && fs_1.default.existsSync(monitoringCompose);
|
|
903
|
+
const files = useMonitoring
|
|
904
|
+
? `-f ${composeFile} -f docker-compose.prod.monitoring.yml`
|
|
905
|
+
: `-f ${composeFile}`;
|
|
487
906
|
const service = opts.service || '';
|
|
488
907
|
const spinner = (0, ora_1.default)('Reiniciando...').start();
|
|
489
|
-
(0, child_process_1.execSync)(`docker compose
|
|
908
|
+
(0, child_process_1.execSync)(`docker compose ${files} restart ${service}`, { cwd: root, stdio: 'pipe' });
|
|
490
909
|
spinner.succeed('Servicios reiniciados');
|
|
491
910
|
}
|
|
492
911
|
catch (err) {
|