@openfactu/cli 0.0.6 → 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/LICENSE +21 -0
- package/README.md +164 -8
- package/dist/src/commands/backup.d.ts +2 -0
- package/dist/src/commands/backup.js +424 -0
- package/dist/src/commands/deploy.js +492 -69
- 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 +969 -75
- 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,9 +13,12 @@ const os_1 = __importDefault(require("os"));
|
|
|
13
13
|
const fs_1 = __importDefault(require("fs"));
|
|
14
14
|
const path_1 = __importDefault(require("path"));
|
|
15
15
|
const logger_1 = require("../utils/logger");
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
16
|
+
const env_1 = require("../utils/env");
|
|
17
|
+
const monitoring_1 = require("../utils/monitoring");
|
|
18
|
+
const helpers_1 = require("../utils/helpers");
|
|
19
|
+
const REPO_URL = 'https://github.com/OpenFactu/platform.git';
|
|
20
|
+
const GITHUB_OWNER = 'OpenFactu';
|
|
21
|
+
const GITHUB_REPO = 'platform';
|
|
19
22
|
function fetchJSON(url) {
|
|
20
23
|
return new Promise((resolve, reject) => {
|
|
21
24
|
https_1.default.get(url, { headers: { 'User-Agent': 'openfactu-cli' } }, (res) => {
|
|
@@ -66,28 +69,206 @@ function getAvailableBranches() {
|
|
|
66
69
|
function checkDocker() {
|
|
67
70
|
try {
|
|
68
71
|
(0, child_process_1.execSync)('docker --version', { stdio: 'pipe' });
|
|
69
|
-
|
|
72
|
+
try {
|
|
73
|
+
(0, child_process_1.execSync)('docker compose version', { stdio: 'pipe' });
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
(0, child_process_1.execSync)('docker-compose --version', { stdio: 'pipe' });
|
|
77
|
+
}
|
|
70
78
|
return true;
|
|
71
79
|
}
|
|
72
80
|
catch {
|
|
73
81
|
return false;
|
|
74
82
|
}
|
|
75
83
|
}
|
|
84
|
+
function validateRepoStructure(targetDir) {
|
|
85
|
+
const required = [
|
|
86
|
+
'package.json',
|
|
87
|
+
'docker-compose.yml',
|
|
88
|
+
'apps/web',
|
|
89
|
+
'apps/server',
|
|
90
|
+
];
|
|
91
|
+
const missing = required.filter(f => !fs_1.default.existsSync(path_1.default.join(targetDir, f)));
|
|
92
|
+
return { valid: missing.length === 0, missing };
|
|
93
|
+
}
|
|
94
|
+
const installLog = [];
|
|
95
|
+
function logStep(message, status = 'info') {
|
|
96
|
+
const timestamp = new Date().toISOString();
|
|
97
|
+
const prefix = status === 'success' ? '✓' : status === 'error' ? '✗' : status === 'warn' ? '⚠' : '•';
|
|
98
|
+
const entry = `[${timestamp}] ${prefix} ${message}`;
|
|
99
|
+
installLog.push(entry);
|
|
100
|
+
}
|
|
101
|
+
function writeInstallLog(targetDir) {
|
|
102
|
+
try {
|
|
103
|
+
const logDir = path_1.default.join(targetDir, '.openfactu');
|
|
104
|
+
if (!fs_1.default.existsSync(logDir)) {
|
|
105
|
+
fs_1.default.mkdirSync(logDir, { recursive: true });
|
|
106
|
+
}
|
|
107
|
+
const logFile = path_1.default.join(logDir, 'install.log');
|
|
108
|
+
fs_1.default.writeFileSync(logFile, installLog.join('\n') + '\n');
|
|
109
|
+
}
|
|
110
|
+
catch { }
|
|
111
|
+
}
|
|
112
|
+
function generateEnvConfig(dbPassword) {
|
|
113
|
+
const postgresUser = 'openfactu';
|
|
114
|
+
const postgresDb = 'openfactudb';
|
|
115
|
+
const jwtSecret = (0, helpers_1.generatePassword)(48);
|
|
116
|
+
const sessionSecret = (0, helpers_1.generatePassword)(32);
|
|
117
|
+
const adminPassword = (0, helpers_1.generatePassword)(16);
|
|
118
|
+
return {
|
|
119
|
+
POSTGRES_USER: postgresUser,
|
|
120
|
+
POSTGRES_PASSWORD: dbPassword,
|
|
121
|
+
POSTGRES_DB: postgresDb,
|
|
122
|
+
DATABASE_URL: `postgresql://${postgresUser}:${encodeURIComponent(dbPassword)}@db:5432/${postgresDb}`,
|
|
123
|
+
SERVER_PORT: '3000',
|
|
124
|
+
WEB_PORT: '8080',
|
|
125
|
+
DB_PORT: '5432',
|
|
126
|
+
JWT_SECRET: jwtSecret,
|
|
127
|
+
SESSION_SECRET: sessionSecret,
|
|
128
|
+
NODE_ENV: 'production',
|
|
129
|
+
HOST: 'localhost',
|
|
130
|
+
CORS_ORIGIN: 'http://localhost:8080',
|
|
131
|
+
VITE_API_URL: 'http://localhost:3000',
|
|
132
|
+
ADMIN_EMAIL: 'admin@openfactu.local',
|
|
133
|
+
ADMIN_PASSWORD: adminPassword,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function generateEnvFileContent(envConfig) {
|
|
137
|
+
return `# ============================================================
|
|
138
|
+
# OpenFactu - Configuración
|
|
139
|
+
# Generado automáticamente por @openfactu/cli
|
|
140
|
+
# ============================================================
|
|
141
|
+
# IMPORTANTE: Revisa y ajusta los valores marcados con [EDITAR]
|
|
142
|
+
# ============================================================
|
|
143
|
+
|
|
144
|
+
# ── Base de datos ──────────────────────────────────────────
|
|
145
|
+
# [EDITAR] Si cambias el puerto de PostgreSQL, actualiza DATABASE_URL
|
|
146
|
+
POSTGRES_USER=${envConfig.POSTGRES_USER}
|
|
147
|
+
POSTGRES_PASSWORD=${envConfig.POSTGRES_PASSWORD}
|
|
148
|
+
POSTGRES_DB=${envConfig.POSTGRES_DB}
|
|
149
|
+
DB_PORT=${envConfig.DB_PORT}
|
|
150
|
+
|
|
151
|
+
# [EDITAR] La URL interna del contenedor (no cambiar a menos que uses otro host de BD)
|
|
152
|
+
DATABASE_URL=${envConfig.DATABASE_URL}
|
|
153
|
+
|
|
154
|
+
# ── Puertos de la aplicación ───────────────────────────────
|
|
155
|
+
# [EDITAR] Cambia estos puertos si hay conflictos en tu servidor
|
|
156
|
+
SERVER_PORT=${envConfig.SERVER_PORT}
|
|
157
|
+
WEB_PORT=${envConfig.WEB_PORT}
|
|
158
|
+
|
|
159
|
+
# ── Seguridad ─────────────────────────────────────────────
|
|
160
|
+
# [NO EDITAR] Secrets generados automáticamente
|
|
161
|
+
JWT_SECRET=${envConfig.JWT_SECRET}
|
|
162
|
+
SESSION_SECRET=${envConfig.SESSION_SECRET}
|
|
163
|
+
|
|
164
|
+
# ── Entorno ────────────────────────────────────────────────
|
|
165
|
+
NODE_ENV=${envConfig.NODE_ENV}
|
|
166
|
+
|
|
167
|
+
# ── URLs y acceso externo ──────────────────────────────────
|
|
168
|
+
# [EDITAR] Cambia HOST por tu dominio o IP pública
|
|
169
|
+
HOST=${envConfig.HOST}
|
|
170
|
+
|
|
171
|
+
# [EDITAR] Cambia estas URLs según tu dominio/IP y puertos
|
|
172
|
+
# Ejemplo con dominio: https://erp.miempresa.com
|
|
173
|
+
# Ejemplo con IP: http://192.168.1.100:8080
|
|
174
|
+
CORS_ORIGIN=${envConfig.CORS_ORIGIN}
|
|
175
|
+
VITE_API_URL=${envConfig.VITE_API_URL}
|
|
176
|
+
|
|
177
|
+
# ── Credenciales de administrador ──────────────────────────
|
|
178
|
+
# [EDITAR] Cambia el email y password del admin inicial
|
|
179
|
+
ADMIN_EMAIL=${envConfig.ADMIN_EMAIL}
|
|
180
|
+
ADMIN_PASSWORD=${envConfig.ADMIN_PASSWORD}
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
function installService(targetDir, dockerCmd, serviceName, unitPath) {
|
|
184
|
+
const unitContent = `[Unit]
|
|
185
|
+
Description=OpenFactu ERP Platform
|
|
186
|
+
After=docker.service network-online.target
|
|
187
|
+
Requires=docker.service
|
|
188
|
+
Wants=network-online.target
|
|
189
|
+
|
|
190
|
+
[Service]
|
|
191
|
+
Type=oneshot
|
|
192
|
+
RemainAfterExit=yes
|
|
193
|
+
WorkingDirectory=${targetDir}
|
|
194
|
+
ExecStart=${dockerCmd} -f docker-compose.yml up -d
|
|
195
|
+
ExecStop=${dockerCmd} -f docker-compose.yml down
|
|
196
|
+
Restart=on-failure
|
|
197
|
+
RestartSec=10
|
|
198
|
+
|
|
199
|
+
[Install]
|
|
200
|
+
WantedBy=multi-user.target
|
|
201
|
+
`;
|
|
202
|
+
fs_1.default.writeFileSync(`/tmp/${serviceName}.service`, unitContent);
|
|
203
|
+
(0, child_process_1.execSync)(`sudo mv /tmp/${serviceName}.service ${unitPath}`, { stdio: 'pipe' });
|
|
204
|
+
(0, child_process_1.execSync)('sudo systemctl daemon-reload', { stdio: 'pipe' });
|
|
205
|
+
(0, child_process_1.execSync)(`sudo systemctl enable ${serviceName}`, { stdio: 'pipe' });
|
|
206
|
+
logger_1.log.success(`Servicio ${serviceName} instalado y habilitado`);
|
|
207
|
+
logger_1.log.dim(` sudo systemctl start ${serviceName}`);
|
|
208
|
+
logger_1.log.dim(` sudo systemctl status ${serviceName}`);
|
|
209
|
+
}
|
|
76
210
|
function registerInstallCommand(program) {
|
|
77
211
|
program
|
|
78
212
|
.command('install [directory]')
|
|
79
213
|
.description('Descarga e instala OpenFactu en un directorio')
|
|
80
214
|
.option('-t, --tag <tag>', 'Versión/tag específico (ej: v1.0.0)')
|
|
81
|
-
.option('-b, --branch <branch>', 'Branch
|
|
215
|
+
.option('-b, --branch <branch>', 'Branch específico (default: main)')
|
|
82
216
|
.option('--repo <url>', 'URL del repositorio', REPO_URL)
|
|
83
217
|
.option('--skip-deps', 'No instalar dependencias (npm install)')
|
|
218
|
+
.option('--mode <mode>', 'Modo: full, docker, minimal, download (default: interactive)')
|
|
219
|
+
.option('--no-preflight', 'Saltar chequeos previos')
|
|
220
|
+
.option('--no-healthcheck', 'Saltar health checks post-instalación')
|
|
221
|
+
.option('--generate-env', 'Generar .env con credenciales seguras aleatorias')
|
|
222
|
+
.option('--service', 'Instalar como servicio systemd después de instalar')
|
|
223
|
+
.option('--monitoring', 'Incluir stack de monitoreo (Grafana, Prometheus, etc.)')
|
|
224
|
+
.option('--with-analytics', 'Incluir stack completo de analítica (Loki, cAdvisor, Node Exporter)')
|
|
225
|
+
.option('-y, --yes', 'Aceptar defaults sin preguntar (non-interactive)')
|
|
84
226
|
.action(async (directory, opts) => {
|
|
85
227
|
console.log();
|
|
86
228
|
console.log(chalk_1.default.bold.white(' OpenFactu — Instalación'));
|
|
87
229
|
console.log(chalk_1.default.dim(' ────────────────────────────────────'));
|
|
88
230
|
console.log();
|
|
231
|
+
let targetDir = directory;
|
|
89
232
|
try {
|
|
90
233
|
const repoUrl = opts.repo;
|
|
234
|
+
const nonInteractive = opts.yes || false;
|
|
235
|
+
logStep('Inicio de instalación', 'info');
|
|
236
|
+
// 0. Preflight checks
|
|
237
|
+
if (opts.preflight !== false) {
|
|
238
|
+
const checkSpinner = (0, ora_1.default)('Ejecutando chequeos del sistema...').start();
|
|
239
|
+
const checks = (0, helpers_1.runPreflightChecks)(directory || os_1.default.homedir());
|
|
240
|
+
const fails = checks.filter(c => c.status === 'fail');
|
|
241
|
+
const warns = checks.filter(c => c.status === 'warn');
|
|
242
|
+
if (fails.length > 0) {
|
|
243
|
+
checkSpinner.fail('Chequeos fallidos');
|
|
244
|
+
logStep(`Preflight fallido: ${fails.map(f => f.name).join(', ')}`, 'error');
|
|
245
|
+
logger_1.log.blank();
|
|
246
|
+
logger_1.log.error('Requisitos no cumplidos:');
|
|
247
|
+
for (const f of fails) {
|
|
248
|
+
logger_1.log.error(` ✗ ${f.name}: ${f.message}`);
|
|
249
|
+
}
|
|
250
|
+
logger_1.log.blank();
|
|
251
|
+
if (!nonInteractive) {
|
|
252
|
+
const { continueAnyway } = await inquirer_1.default.prompt([
|
|
253
|
+
{ type: 'confirm', name: 'continueAnyway', message: 'Continuar de todos modos?', default: false },
|
|
254
|
+
]);
|
|
255
|
+
if (!continueAnyway)
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else if (warns.length > 0) {
|
|
260
|
+
checkSpinner.warn('Chequeos con advertencias');
|
|
261
|
+
logStep(`Preflight con advertencias: ${warns.map(w => w.name).join(', ')}`, 'warn');
|
|
262
|
+
for (const w of warns) {
|
|
263
|
+
logger_1.log.warn(` ⚠ ${w.name}: ${w.message}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
checkSpinner.succeed(`${checks.length} chequeos pasados`);
|
|
268
|
+
logStep('Preflight OK', 'success');
|
|
269
|
+
}
|
|
270
|
+
logger_1.log.blank();
|
|
271
|
+
}
|
|
91
272
|
// 1. Obtener releases de GitHub
|
|
92
273
|
const fetchSpinner = (0, ora_1.default)('Consultando releases en GitHub...').start();
|
|
93
274
|
const releases = await getGithubReleases();
|
|
@@ -101,10 +282,12 @@ function registerInstallCommand(program) {
|
|
|
101
282
|
else if (opts.branch) {
|
|
102
283
|
ref = opts.branch;
|
|
103
284
|
}
|
|
285
|
+
else if (nonInteractive) {
|
|
286
|
+
const stable = releases.filter((r) => !r.prerelease);
|
|
287
|
+
ref = stable.length > 0 ? stable[0].tag_name : 'main';
|
|
288
|
+
}
|
|
104
289
|
else {
|
|
105
|
-
// Menú interactivo
|
|
106
290
|
const choices = [];
|
|
107
|
-
// Releases estables primero
|
|
108
291
|
const stable = releases.filter((r) => !r.prerelease);
|
|
109
292
|
const prerelease = releases.filter((r) => r.prerelease);
|
|
110
293
|
for (const rel of stable.slice(0, 10)) {
|
|
@@ -150,43 +333,53 @@ function registerInstallCommand(program) {
|
|
|
150
333
|
}
|
|
151
334
|
logger_1.log.info(`Versión seleccionada: ${chalk_1.default.cyan(ref)}`);
|
|
152
335
|
// 3. Directorio destino
|
|
153
|
-
let targetDir = directory;
|
|
154
336
|
if (!targetDir) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
337
|
+
if (nonInteractive) {
|
|
338
|
+
targetDir = path_1.default.join(os_1.default.homedir(), 'openfactu');
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
const { dir } = await inquirer_1.default.prompt([
|
|
342
|
+
{
|
|
343
|
+
type: 'input',
|
|
344
|
+
name: 'dir',
|
|
345
|
+
message: 'Directorio de instalación:',
|
|
346
|
+
default: path_1.default.join(os_1.default.homedir(), 'openfactu'),
|
|
347
|
+
},
|
|
348
|
+
]);
|
|
349
|
+
targetDir = dir;
|
|
350
|
+
}
|
|
164
351
|
}
|
|
165
352
|
targetDir = path_1.default.resolve(targetDir);
|
|
166
|
-
// Verificar si el directorio ya existe
|
|
167
353
|
if (fs_1.default.existsSync(targetDir)) {
|
|
168
354
|
const contents = fs_1.default.readdirSync(targetDir);
|
|
169
355
|
if (contents.length > 0) {
|
|
170
|
-
|
|
171
|
-
{
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
356
|
+
if (nonInteractive) {
|
|
357
|
+
logger_1.log.warn(`Directorio ${targetDir} no está vacío, se usará igual`);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
const { overwrite } = await inquirer_1.default.prompt([
|
|
361
|
+
{
|
|
362
|
+
type: 'confirm',
|
|
363
|
+
name: 'overwrite',
|
|
364
|
+
message: `El directorio ${targetDir} no está vacío. ¿Continuar?`,
|
|
365
|
+
default: false,
|
|
366
|
+
},
|
|
367
|
+
]);
|
|
368
|
+
if (!overwrite) {
|
|
369
|
+
logger_1.log.info('Instalación cancelada');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
181
372
|
}
|
|
182
373
|
}
|
|
183
374
|
}
|
|
184
375
|
logger_1.log.info(`Directorio: ${chalk_1.default.dim(targetDir)}`);
|
|
376
|
+
logStep(`Directorio: ${targetDir}`, 'info');
|
|
185
377
|
logger_1.log.blank();
|
|
186
|
-
// 4. Crear directorio
|
|
378
|
+
// 4. Crear directorio
|
|
187
379
|
if (!fs_1.default.existsSync(targetDir)) {
|
|
188
380
|
try {
|
|
189
381
|
fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
382
|
+
logStep('Directorio creado', 'success');
|
|
190
383
|
}
|
|
191
384
|
catch (mkdirErr) {
|
|
192
385
|
if (mkdirErr.code === 'EACCES') {
|
|
@@ -194,11 +387,13 @@ function registerInstallCommand(program) {
|
|
|
194
387
|
try {
|
|
195
388
|
const user = process.env.USER || process.env.USERNAME || 'root';
|
|
196
389
|
(0, child_process_1.execSync)(`sudo mkdir -p "${targetDir}" && sudo chown -R ${user}:${user} "${targetDir}"`, {
|
|
197
|
-
stdio: '
|
|
390
|
+
stdio: 'pipe',
|
|
198
391
|
});
|
|
392
|
+
logStep('Directorio creado con sudo', 'success');
|
|
199
393
|
}
|
|
200
394
|
catch {
|
|
201
395
|
logger_1.log.error(`No se pudo crear ${targetDir}. Ejecuta con sudo o elige otro directorio.`);
|
|
396
|
+
logStep(`Error creando directorio: ${targetDir}`, 'error');
|
|
202
397
|
return;
|
|
203
398
|
}
|
|
204
399
|
}
|
|
@@ -209,6 +404,7 @@ function registerInstallCommand(program) {
|
|
|
209
404
|
}
|
|
210
405
|
// 5. Clonar repositorio
|
|
211
406
|
const cloneSpinner = (0, ora_1.default)('Descargando OpenFactu...').start();
|
|
407
|
+
logStep('Iniciando clonación del repositorio', 'info');
|
|
212
408
|
const isTag = releases.some((r) => r.tag_name === ref);
|
|
213
409
|
const cloneCmd = isTag
|
|
214
410
|
? `git clone --depth 1 --branch ${ref} ${repoUrl} "${targetDir}"`
|
|
@@ -216,72 +412,755 @@ function registerInstallCommand(program) {
|
|
|
216
412
|
try {
|
|
217
413
|
(0, child_process_1.execSync)(cloneCmd, { stdio: 'pipe', timeout: 120000 });
|
|
218
414
|
cloneSpinner.succeed('Código descargado');
|
|
415
|
+
logStep(`Repositorio clonado: ${ref}`, 'success');
|
|
219
416
|
}
|
|
220
417
|
catch (err) {
|
|
221
|
-
// Fallback: clonar todo y checkout
|
|
222
418
|
try {
|
|
223
419
|
cloneSpinner.text = 'Descargando (método alternativo)...';
|
|
224
420
|
(0, child_process_1.execSync)(`git clone ${repoUrl} "${targetDir}"`, { stdio: 'pipe', timeout: 180000 });
|
|
225
421
|
(0, child_process_1.execSync)(`git checkout ${ref}`, { cwd: targetDir, stdio: 'pipe' });
|
|
226
422
|
cloneSpinner.succeed('Código descargado');
|
|
423
|
+
logStep(`Repositorio clonado (alternativo): ${ref}`, 'success');
|
|
227
424
|
}
|
|
228
425
|
catch (err2) {
|
|
229
426
|
cloneSpinner.fail('Error al descargar: ' + err2.message);
|
|
427
|
+
logStep(`Error clonando repositorio: ${err2.message}`, 'error');
|
|
230
428
|
return;
|
|
231
429
|
}
|
|
232
430
|
}
|
|
233
|
-
// 6.
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
logger_1.log.
|
|
431
|
+
// 6. Validar estructura del repo
|
|
432
|
+
const validation = validateRepoStructure(targetDir);
|
|
433
|
+
if (!validation.valid) {
|
|
434
|
+
logger_1.log.warn(`Estructura incompleta: ${validation.missing.join(', ')}`);
|
|
435
|
+
logStep(`Estructura incompleta: ${validation.missing.join(', ')}`, 'warn');
|
|
436
|
+
logger_1.log.dim(' Algunos comandos pueden no funcionar correctamente');
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
logStep('Estructura del repositorio válida', 'success');
|
|
239
440
|
}
|
|
240
|
-
// 7.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
441
|
+
// 7. Configurar .env — las credenciales (incluida la contraseña de BD) se
|
|
442
|
+
// fijan AQUI, antes del primer arranque, para que el volumen de Postgres
|
|
443
|
+
// se inicialice con ellas y no acabe usando la contraseña por defecto.
|
|
444
|
+
{
|
|
445
|
+
const envFile = path_1.default.join(targetDir, '.env');
|
|
446
|
+
const envExample = path_1.default.join(targetDir, '.env.example');
|
|
447
|
+
// Decidir la contraseña de PostgreSQL.
|
|
448
|
+
let dbPassword;
|
|
449
|
+
if (opts.generateEnv || nonInteractive) {
|
|
450
|
+
dbPassword = (0, helpers_1.generatePassword)(24);
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
const suggested = (0, helpers_1.generatePassword)(24);
|
|
454
|
+
const { pw } = await inquirer_1.default.prompt([
|
|
455
|
+
{
|
|
456
|
+
type: 'input',
|
|
457
|
+
name: 'pw',
|
|
458
|
+
message: 'Contraseña de PostgreSQL (Enter = generar una segura):',
|
|
459
|
+
default: suggested,
|
|
460
|
+
},
|
|
461
|
+
]);
|
|
462
|
+
dbPassword = typeof pw === 'string' && pw.trim() ? pw.trim() : suggested;
|
|
463
|
+
}
|
|
464
|
+
const envSpinner = (0, ora_1.default)('Configurando .env con credenciales seguras...').start();
|
|
465
|
+
const envConfig = generateEnvConfig(dbPassword);
|
|
466
|
+
// Plantilla base: .env.example si existe (preserva comentarios y claves
|
|
467
|
+
// propias de la plataforma); si no, un .env generado desde cero.
|
|
468
|
+
const baseContent = fs_1.default.existsSync(envExample)
|
|
469
|
+
? fs_1.default.readFileSync(envExample, 'utf-8')
|
|
470
|
+
: generateEnvFileContent(envConfig);
|
|
471
|
+
fs_1.default.writeFileSync(envFile, (0, env_1.applyEnvOverrides)(baseContent, envConfig));
|
|
472
|
+
envSpinner.succeed('.env configurado con credenciales seguras');
|
|
473
|
+
logStep('.env configurado con credenciales seguras', 'success');
|
|
474
|
+
logger_1.log.blank();
|
|
475
|
+
logger_1.log.info(`${chalk_1.default.dim('Admin:')} ${envConfig.ADMIN_EMAIL} / ${chalk_1.default.yellow(envConfig.ADMIN_PASSWORD)}`);
|
|
476
|
+
logger_1.log.info(`${chalk_1.default.dim('DB Password:')} ${chalk_1.default.yellow(envConfig.POSTGRES_PASSWORD)}`);
|
|
477
|
+
logger_1.log.dim(' Guarda estas credenciales en un lugar seguro');
|
|
245
478
|
logger_1.log.blank();
|
|
246
479
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
480
|
+
// 8. Determinar modo de instalación
|
|
481
|
+
let installMode = opts.mode;
|
|
482
|
+
if (!installMode) {
|
|
483
|
+
const hasDocker = checkDocker();
|
|
484
|
+
if (!hasDocker) {
|
|
485
|
+
logger_1.log.warn('Docker no detectado. OpenFactu requiere Docker para funcionar.');
|
|
486
|
+
logger_1.log.dim(' Instala Docker: https://docs.docker.com/get-docker/');
|
|
487
|
+
logger_1.log.blank();
|
|
488
|
+
}
|
|
489
|
+
if (nonInteractive) {
|
|
490
|
+
installMode = hasDocker ? 'docker' : 'download';
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
const disk = (0, helpers_1.checkDiskSpace)(targetDir);
|
|
494
|
+
const { selectedMode } = await inquirer_1.default.prompt([
|
|
257
495
|
{
|
|
258
|
-
|
|
259
|
-
|
|
496
|
+
type: 'list',
|
|
497
|
+
name: 'selectedMode',
|
|
498
|
+
message: 'Modo de instalación:',
|
|
499
|
+
choices: [
|
|
500
|
+
...(hasDocker ? [
|
|
501
|
+
{
|
|
502
|
+
name: `${chalk_1.default.green('Completa (Docker)')} ${chalk_1.default.dim('— build + up + setup completo')}`,
|
|
503
|
+
value: 'full',
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
name: `${chalk_1.default.cyan('Docker')} ${chalk_1.default.dim('— build + up, sin setup DB')}`,
|
|
507
|
+
value: 'docker',
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
name: `${chalk_1.default.yellow('Mínima')} ${chalk_1.default.dim('— solo compose up, sin build')}`,
|
|
511
|
+
value: 'minimal',
|
|
512
|
+
},
|
|
513
|
+
] : []),
|
|
514
|
+
{
|
|
515
|
+
name: `${chalk_1.default.dim('Solo descarga')} ${chalk_1.default.dim('— sin Docker')}`,
|
|
516
|
+
value: 'download',
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
},
|
|
520
|
+
]);
|
|
521
|
+
installMode = selectedMode;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// Verificar conflictos de puertos ANTES de hacer más preguntas
|
|
525
|
+
if (installMode === 'full' || installMode === 'docker' || installMode === 'minimal') {
|
|
526
|
+
const commonPorts = [
|
|
527
|
+
{ port: 5432, name: 'PostgreSQL', container: 'db' },
|
|
528
|
+
{ port: 8080, name: 'Web', container: 'web' },
|
|
529
|
+
{ port: 3000, name: 'API Server', container: 'server' },
|
|
530
|
+
];
|
|
531
|
+
const conflicts = commonPorts.filter(p => {
|
|
532
|
+
try {
|
|
533
|
+
// Excluir docker-pr (proxies de Docker) que pueden quedar colgados
|
|
534
|
+
const output = (0, child_process_1.execSync)(`lsof -i :${p.port} -sTCP:LISTEN 2>/dev/null | grep -v 'docker-pr' | grep -v 'COMMAND' || true`, { stdio: 'pipe' }).toString().trim();
|
|
535
|
+
return output.length > 0;
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
if (conflicts.length > 0 && !nonInteractive) {
|
|
542
|
+
logger_1.log.blank();
|
|
543
|
+
logger_1.log.warn('Puertos en conflicto detectados:');
|
|
544
|
+
for (const c of conflicts) {
|
|
545
|
+
let processInfo = '';
|
|
546
|
+
try {
|
|
547
|
+
const pid = (0, child_process_1.execSync)(`lsof -i :${c.port} -sTCP:LISTEN -t 2>/dev/null | grep -v 'docker-pr' | head -1`, { stdio: 'pipe' }).toString().trim();
|
|
548
|
+
if (pid) {
|
|
549
|
+
const procName = (0, child_process_1.execSync)(`ps -p ${pid} -o comm= 2>/dev/null || echo 'desconocido'`, { stdio: 'pipe' }).toString().trim();
|
|
550
|
+
processInfo = ` (${procName})`;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
catch { }
|
|
554
|
+
logger_1.log.warn(` Puerto ${c.port} (${c.name}): ocupado${processInfo}`);
|
|
555
|
+
}
|
|
556
|
+
logger_1.log.blank();
|
|
557
|
+
const { portAction } = await inquirer_1.default.prompt([
|
|
558
|
+
{
|
|
559
|
+
type: 'list',
|
|
560
|
+
name: 'portAction',
|
|
561
|
+
message: '¿Cómo resolver el conflicto?',
|
|
562
|
+
choices: [
|
|
563
|
+
{ name: 'Detener el proceso que ocupa el puerto', value: 'stop' },
|
|
564
|
+
{ name: 'Usar puertos alternativos (5433, 8081, 3001)', value: 'alternate' },
|
|
565
|
+
{ name: 'Continuar igual (puede fallar)', value: 'continue' },
|
|
566
|
+
{ name: 'Cancelar instalación', value: 'cancel' },
|
|
567
|
+
],
|
|
260
568
|
},
|
|
261
|
-
],
|
|
262
|
-
},
|
|
263
|
-
]);
|
|
264
|
-
if (installMode === 'docker') {
|
|
265
|
-
// Docker: build + up
|
|
266
|
-
const dockerSpinner = (0, ora_1.default)('Construyendo contenedores Docker...').start();
|
|
267
|
-
try {
|
|
268
|
-
(0, child_process_1.execSync)('docker compose build', { cwd: targetDir, stdio: 'pipe', timeout: 300000 });
|
|
269
|
-
dockerSpinner.succeed('Contenedores construidos');
|
|
270
|
-
const { startNow } = await inquirer_1.default.prompt([
|
|
271
|
-
{ type: 'confirm', name: 'startNow', message: '¿Arrancar los servicios?', default: true },
|
|
272
569
|
]);
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
(
|
|
276
|
-
|
|
570
|
+
if (portAction === 'cancel') {
|
|
571
|
+
logger_1.log.info('Instalación cancelada');
|
|
572
|
+
writeInstallLog(targetDir);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (portAction === 'stop') {
|
|
576
|
+
for (const c of conflicts) {
|
|
577
|
+
const stopSpinner = (0, ora_1.default)(`Deteniendo proceso en puerto ${c.port}...`).start();
|
|
578
|
+
try {
|
|
579
|
+
const pid = (0, child_process_1.execSync)(`lsof -i :${c.port} -sTCP:LISTEN -t 2>/dev/null | head -1`, { stdio: 'pipe' }).toString().trim();
|
|
580
|
+
if (pid) {
|
|
581
|
+
(0, child_process_1.execSync)(`kill -9 ${pid} 2>/dev/null || true`, { stdio: 'pipe' });
|
|
582
|
+
stopSpinner.succeed(`Proceso en puerto ${c.port} detenido`);
|
|
583
|
+
logStep(`Puerto ${c.port} liberado`, 'success');
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
stopSpinner.warn(`No se encontró proceso en puerto ${c.port}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
stopSpinner.fail(`No se pudo detener el proceso en puerto ${c.port}`);
|
|
591
|
+
logger_1.log.dim(` Intenta manualmente: sudo lsof -i :${c.port} -t | xargs kill -9`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
logger_1.log.blank();
|
|
277
595
|
}
|
|
596
|
+
if (portAction === 'alternate') {
|
|
597
|
+
const portMap = { 5432: 5433, 8080: 8081, 3000: 3001 };
|
|
598
|
+
// Actualizar .env
|
|
599
|
+
const envPath = path_1.default.join(targetDir, '.env');
|
|
600
|
+
if (fs_1.default.existsSync(envPath)) {
|
|
601
|
+
let envContent = fs_1.default.readFileSync(envPath, 'utf-8');
|
|
602
|
+
for (const [oldPort, newPort] of Object.entries(portMap)) {
|
|
603
|
+
const oldPortNum = parseInt(oldPort);
|
|
604
|
+
if (conflicts.some(c => c.port === oldPortNum)) {
|
|
605
|
+
envContent = envContent.replace(new RegExp(`:${oldPortNum}\\b`, 'g'), `:${newPort}`);
|
|
606
|
+
envContent = envContent.replace(new RegExp(`PORT=${oldPortNum}`, 'g'), `PORT=${newPort}`);
|
|
607
|
+
logger_1.log.info(`Puerto ${oldPort} → ${newPort}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
fs_1.default.writeFileSync(envPath, envContent);
|
|
611
|
+
logStep('Puertos alternativos configurados en .env', 'success');
|
|
612
|
+
}
|
|
613
|
+
// Actualizar docker-compose files
|
|
614
|
+
const composeFiles = [
|
|
615
|
+
'docker-compose.yml',
|
|
616
|
+
'docker-compose.prod.yml',
|
|
617
|
+
];
|
|
618
|
+
for (const composeFile of composeFiles) {
|
|
619
|
+
const composePath = path_1.default.join(targetDir, composeFile);
|
|
620
|
+
if (fs_1.default.existsSync(composePath)) {
|
|
621
|
+
let composeContent = fs_1.default.readFileSync(composePath, 'utf-8');
|
|
622
|
+
for (const [oldPort, newPort] of Object.entries(portMap)) {
|
|
623
|
+
const oldPortNum = parseInt(oldPort);
|
|
624
|
+
if (conflicts.some(c => c.port === oldPortNum)) {
|
|
625
|
+
composeContent = composeContent.replace(new RegExp(`"([^"]*):${oldPortNum}"`, 'g'), `$1:${newPort}"`);
|
|
626
|
+
composeContent = composeContent.replace(new RegExp(`- "${oldPortNum}:${oldPortNum}"`, 'g'), `- "${newPort}:${oldPortNum}"`);
|
|
627
|
+
composeContent = composeContent.replace(new RegExp(`- "0\\.0\\.0\\.0:${oldPortNum}:`, 'g'), `- "0.0.0.0:${newPort}:`);
|
|
628
|
+
composeContent = composeContent.replace(new RegExp(`- "127\\.0\\.0\\.1:${oldPortNum}:`, 'g'), `- "127.0.0.1:${newPort}:`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
fs_1.default.writeFileSync(composePath, composeContent);
|
|
632
|
+
logStep(`Puertos actualizados en ${composeFile}`, 'success');
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
logger_1.log.blank();
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// 9. Monitoreo — selección granular de servicios
|
|
640
|
+
let includeMonitoring = false;
|
|
641
|
+
let monitoringServices = [];
|
|
642
|
+
if (installMode === 'full' || installMode === 'docker') {
|
|
643
|
+
if (opts.withAnalytics) {
|
|
644
|
+
includeMonitoring = true;
|
|
645
|
+
monitoringServices = (0, monitoring_1.fullMonitoringServices)();
|
|
646
|
+
}
|
|
647
|
+
else if (opts.monitoring) {
|
|
648
|
+
includeMonitoring = true;
|
|
649
|
+
monitoringServices = (0, monitoring_1.basicMonitoringServices)();
|
|
650
|
+
}
|
|
651
|
+
else if (!nonInteractive) {
|
|
652
|
+
const { monitoring } = await inquirer_1.default.prompt([
|
|
653
|
+
{
|
|
654
|
+
type: 'confirm',
|
|
655
|
+
name: 'monitoring',
|
|
656
|
+
message: 'Incluir stack de monitoreo (Grafana, Prometheus, pgAdmin, Portainer…)?',
|
|
657
|
+
default: false,
|
|
658
|
+
},
|
|
659
|
+
]);
|
|
660
|
+
if (monitoring) {
|
|
661
|
+
const { services } = await inquirer_1.default.prompt([
|
|
662
|
+
{
|
|
663
|
+
type: 'checkbox',
|
|
664
|
+
name: 'services',
|
|
665
|
+
message: 'Servicios de monitoreo a instalar:',
|
|
666
|
+
choices: (0, monitoring_1.monitoringChoices)(),
|
|
667
|
+
},
|
|
668
|
+
]);
|
|
669
|
+
monitoringServices = services;
|
|
670
|
+
includeMonitoring = monitoringServices.length > 0;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// Generar docker-compose.monitoring.yml + configs con los servicios elegidos
|
|
675
|
+
if (includeMonitoring && monitoringServices.length > 0) {
|
|
676
|
+
const monSpinner = (0, ora_1.default)('Generando stack de monitoreo...').start();
|
|
677
|
+
const serviceSet = new Set(monitoringServices);
|
|
678
|
+
fs_1.default.writeFileSync(path_1.default.join(targetDir, 'docker-compose.monitoring.yml'), (0, monitoring_1.generateMonitoringCompose)(serviceSet));
|
|
679
|
+
(0, monitoring_1.writeMonitoringConfigs)(targetDir, serviceSet);
|
|
680
|
+
const envFile = path_1.default.join(targetDir, '.env');
|
|
681
|
+
if (fs_1.default.existsSync(envFile)) {
|
|
682
|
+
fs_1.default.writeFileSync(envFile, (0, env_1.applyEnvOverrides)(fs_1.default.readFileSync(envFile, 'utf-8'), {
|
|
683
|
+
MONITORING_SERVICES: monitoringServices.join(','),
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
686
|
+
monSpinner.succeed(`Monitoreo: ${monitoringServices.join(', ')}`);
|
|
687
|
+
logStep(`Monitoreo configurado: ${monitoringServices.join(', ')}`, 'success');
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
includeMonitoring = false;
|
|
691
|
+
}
|
|
692
|
+
// Preguntar por servicio systemd (siempre, si es Linux)
|
|
693
|
+
if (!nonInteractive && (0, helpers_1.isLinux)() && (installMode === 'full' || installMode === 'docker' || installMode === 'minimal')) {
|
|
694
|
+
const { installService } = await inquirer_1.default.prompt([
|
|
695
|
+
{
|
|
696
|
+
type: 'confirm',
|
|
697
|
+
name: 'installService',
|
|
698
|
+
message: 'Instalar como servicio systemd (auto-start al iniciar el sistema)?',
|
|
699
|
+
default: false,
|
|
700
|
+
},
|
|
701
|
+
]);
|
|
702
|
+
if (installService)
|
|
703
|
+
opts.service = true;
|
|
704
|
+
}
|
|
705
|
+
// 10. Ejecutar instalación según modo
|
|
706
|
+
const dockerCmd = (0, helpers_1.getDockerComposeCommand)();
|
|
707
|
+
logStep(`Modo de instalación: ${installMode}`, 'info');
|
|
708
|
+
// Verificar conflictos de puertos antes de levantar contenedores
|
|
709
|
+
if (installMode === 'full' || installMode === 'docker' || installMode === 'minimal') {
|
|
710
|
+
const commonPorts = [
|
|
711
|
+
{ port: 5432, name: 'PostgreSQL', container: 'db' },
|
|
712
|
+
{ port: 8080, name: 'Web', container: 'web' },
|
|
713
|
+
{ port: 3000, name: 'API Server', container: 'server' },
|
|
714
|
+
];
|
|
715
|
+
const conflicts = commonPorts.filter(p => {
|
|
716
|
+
try {
|
|
717
|
+
const output = (0, child_process_1.execSync)(`lsof -i :${p.port} -sTCP:LISTEN 2>/dev/null | grep -v 'docker-pr' | grep -v 'COMMAND' || true`, { stdio: 'pipe' }).toString().trim();
|
|
718
|
+
return output.length > 0;
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
if (conflicts.length > 0 && !nonInteractive) {
|
|
725
|
+
logger_1.log.blank();
|
|
726
|
+
logger_1.log.warn('Puertos en conflicto detectados:');
|
|
727
|
+
for (const c of conflicts) {
|
|
728
|
+
let processInfo = '';
|
|
729
|
+
try {
|
|
730
|
+
const pid = (0, child_process_1.execSync)(`lsof -i :${c.port} -sTCP:LISTEN -t 2>/dev/null | grep -v 'docker-pr' | head -1 || true`, { stdio: 'pipe' }).toString().trim();
|
|
731
|
+
if (pid) {
|
|
732
|
+
const procName = (0, child_process_1.execSync)(`ps -p ${pid} -o comm= 2>/dev/null || echo 'desconocido'`, { stdio: 'pipe' }).toString().trim();
|
|
733
|
+
processInfo = ` (${procName})`;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
catch { }
|
|
737
|
+
logger_1.log.warn(` Puerto ${c.port} (${c.name}): ocupado${processInfo}`);
|
|
738
|
+
}
|
|
739
|
+
logger_1.log.blank();
|
|
740
|
+
const { portAction } = await inquirer_1.default.prompt([
|
|
741
|
+
{
|
|
742
|
+
type: 'list',
|
|
743
|
+
name: 'portAction',
|
|
744
|
+
message: '¿Cómo resolver el conflicto?',
|
|
745
|
+
choices: [
|
|
746
|
+
{ name: 'Detener el proceso que ocupa el puerto', value: 'stop' },
|
|
747
|
+
{ name: 'Usar puertos alternativos (5433, 8081, 3001)', value: 'alternate' },
|
|
748
|
+
{ name: 'Continuar igual (puede fallar)', value: 'continue' },
|
|
749
|
+
{ name: 'Cancelar instalación', value: 'cancel' },
|
|
750
|
+
],
|
|
751
|
+
},
|
|
752
|
+
]);
|
|
753
|
+
if (portAction === 'cancel') {
|
|
754
|
+
logger_1.log.info('Instalación cancelada');
|
|
755
|
+
writeInstallLog(targetDir);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (portAction === 'stop') {
|
|
759
|
+
for (const c of conflicts) {
|
|
760
|
+
const stopSpinner = (0, ora_1.default)(`Deteniendo proceso en puerto ${c.port}...`).start();
|
|
761
|
+
try {
|
|
762
|
+
const pid = (0, child_process_1.execSync)(`lsof -i :${c.port} -sTCP:LISTEN -t 2>/dev/null | head -1`, { stdio: 'pipe' }).toString().trim();
|
|
763
|
+
if (pid) {
|
|
764
|
+
(0, child_process_1.execSync)(`kill -9 ${pid} 2>/dev/null || true`, { stdio: 'pipe' });
|
|
765
|
+
stopSpinner.succeed(`Proceso en puerto ${c.port} detenido`);
|
|
766
|
+
logStep(`Puerto ${c.port} liberado`, 'success');
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
stopSpinner.warn(`No se encontró proceso en puerto ${c.port}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
catch {
|
|
773
|
+
stopSpinner.fail(`No se pudo detener el proceso en puerto ${c.port}`);
|
|
774
|
+
logger_1.log.dim(` Intenta manualmente: sudo lsof -i :${c.port} -t | xargs kill -9`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
logger_1.log.blank();
|
|
778
|
+
}
|
|
779
|
+
if (portAction === 'alternate') {
|
|
780
|
+
const portMap = { 5432: 5433, 8080: 8081, 3000: 3001 };
|
|
781
|
+
// Actualizar .env
|
|
782
|
+
const envPath = path_1.default.join(targetDir, '.env');
|
|
783
|
+
if (fs_1.default.existsSync(envPath)) {
|
|
784
|
+
let envContent = fs_1.default.readFileSync(envPath, 'utf-8');
|
|
785
|
+
for (const [oldPort, newPort] of Object.entries(portMap)) {
|
|
786
|
+
const oldPortNum = parseInt(oldPort);
|
|
787
|
+
if (conflicts.some(c => c.port === oldPortNum)) {
|
|
788
|
+
envContent = envContent.replace(new RegExp(`:${oldPortNum}\\b`, 'g'), `:${newPort}`);
|
|
789
|
+
envContent = envContent.replace(new RegExp(`PORT=${oldPortNum}`, 'g'), `PORT=${newPort}`);
|
|
790
|
+
logger_1.log.info(`Puerto ${oldPort} → ${newPort}`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
fs_1.default.writeFileSync(envPath, envContent);
|
|
794
|
+
logStep('Puertos alternativos configurados en .env', 'success');
|
|
795
|
+
}
|
|
796
|
+
// Actualizar docker-compose files
|
|
797
|
+
const composeFiles = [
|
|
798
|
+
'docker-compose.yml',
|
|
799
|
+
'docker-compose.prod.yml',
|
|
800
|
+
];
|
|
801
|
+
for (const composeFile of composeFiles) {
|
|
802
|
+
const composePath = path_1.default.join(targetDir, composeFile);
|
|
803
|
+
if (fs_1.default.existsSync(composePath)) {
|
|
804
|
+
let composeContent = fs_1.default.readFileSync(composePath, 'utf-8');
|
|
805
|
+
for (const [oldPort, newPort] of Object.entries(portMap)) {
|
|
806
|
+
const oldPortNum = parseInt(oldPort);
|
|
807
|
+
if (conflicts.some(c => c.port === oldPortNum)) {
|
|
808
|
+
// Reemplazar puertos en mappings de Docker "host:container"
|
|
809
|
+
composeContent = composeContent.replace(new RegExp(`"([^"]*):${oldPortNum}"`, 'g'), `$1:${newPort}"`);
|
|
810
|
+
composeContent = composeContent.replace(new RegExp(`- "${oldPortNum}:${oldPortNum}"`, 'g'), `- "${newPort}:${oldPortNum}"`);
|
|
811
|
+
composeContent = composeContent.replace(new RegExp(`- "0\\.0\\.0\\.0:${oldPortNum}:`, 'g'), `- "0.0.0.0:${newPort}:`);
|
|
812
|
+
composeContent = composeContent.replace(new RegExp(`- "127\\.0\\.0\\.1:${oldPortNum}:`, 'g'), `- "127.0.0.1:${newPort}:`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
fs_1.default.writeFileSync(composePath, composeContent);
|
|
816
|
+
logStep(`Puertos actualizados en ${composeFile}`, 'success');
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
logger_1.log.blank();
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
if (installMode === 'full' || installMode === 'docker' || installMode === 'minimal') {
|
|
824
|
+
if (installMode === 'full' || installMode === 'docker') {
|
|
825
|
+
const dockerSpinner = (0, ora_1.default)('Construyendo contenedores Docker...').start();
|
|
826
|
+
logStep('Iniciando build de Docker', 'info');
|
|
827
|
+
try {
|
|
828
|
+
(0, child_process_1.execSync)(`${dockerCmd} build`, { cwd: targetDir, stdio: 'pipe', timeout: 300000 });
|
|
829
|
+
dockerSpinner.succeed('Contenedores construidos');
|
|
830
|
+
logStep('Build de Docker completado', 'success');
|
|
831
|
+
}
|
|
832
|
+
catch (err) {
|
|
833
|
+
dockerSpinner.fail('Error en build: ' + err.message);
|
|
834
|
+
logStep(`Error en build de Docker: ${err.message}`, 'error');
|
|
835
|
+
// Detectar error de permisos de Docker
|
|
836
|
+
if (err.message?.includes('permission denied') && err.message?.includes('docker.sock')) {
|
|
837
|
+
logger_1.log.blank();
|
|
838
|
+
logger_1.log.warn('Error de permisos de Docker detectado');
|
|
839
|
+
logger_1.log.dim(' Tu usuario no tiene acceso al socket de Docker');
|
|
840
|
+
logger_1.log.blank();
|
|
841
|
+
if (!nonInteractive) {
|
|
842
|
+
const { fixPermissions } = await inquirer_1.default.prompt([
|
|
843
|
+
{
|
|
844
|
+
type: 'confirm',
|
|
845
|
+
name: 'fixPermissions',
|
|
846
|
+
message: '¿Agregar tu usuario al grupo docker para arreglarlo? (requiere sudo)',
|
|
847
|
+
default: true,
|
|
848
|
+
},
|
|
849
|
+
]);
|
|
850
|
+
if (fixPermissions) {
|
|
851
|
+
const fixSpinner = (0, ora_1.default)('Agregando usuario al grupo docker...').start();
|
|
852
|
+
logStep('Arreglando permisos de Docker', 'info');
|
|
853
|
+
try {
|
|
854
|
+
const user = process.env.USER || process.env.USERNAME || 'root';
|
|
855
|
+
(0, child_process_1.execSync)(`sudo usermod -aG docker ${user}`, { stdio: 'pipe' });
|
|
856
|
+
fixSpinner.succeed('Usuario agregado al grupo docker');
|
|
857
|
+
logStep('Permisos de Docker arreglados', 'success');
|
|
858
|
+
logger_1.log.blank();
|
|
859
|
+
logger_1.log.info('Los cambios se aplican en la próxima sesión');
|
|
860
|
+
logger_1.log.dim(` Ejecuta: newgrp docker`);
|
|
861
|
+
logger_1.log.dim(` O cierra sesión y vuelve a entrar`);
|
|
862
|
+
logger_1.log.blank();
|
|
863
|
+
const { retryBuild } = await inquirer_1.default.prompt([
|
|
864
|
+
{
|
|
865
|
+
type: 'confirm',
|
|
866
|
+
name: 'retryBuild',
|
|
867
|
+
message: '¿Reintentar el build ahora?',
|
|
868
|
+
default: false,
|
|
869
|
+
},
|
|
870
|
+
]);
|
|
871
|
+
if (retryBuild) {
|
|
872
|
+
const retrySpinner = (0, ora_1.default)('Reintentando build...').start();
|
|
873
|
+
try {
|
|
874
|
+
(0, child_process_1.execSync)(`${dockerCmd} build`, { cwd: targetDir, stdio: 'pipe', timeout: 300000 });
|
|
875
|
+
retrySpinner.succeed('Build completado');
|
|
876
|
+
logStep('Build completado tras arreglar permisos', 'success');
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
retrySpinner.warn('Aún hay error de permisos');
|
|
880
|
+
logStep('Reintento de build fallido', 'warn');
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
catch (fixErr) {
|
|
885
|
+
fixSpinner.fail('No se pudieron arreglar los permisos');
|
|
886
|
+
logStep(`Error arreglando permisos: ${fixErr.message}`, 'error');
|
|
887
|
+
logger_1.log.dim(' Ejecuta manualmente: sudo usermod -aG docker $USER');
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
logger_1.log.dim(' Ejecuta: sudo usermod -aG docker $USER');
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
logger_1.log.blank();
|
|
896
|
+
const { continueWithoutBuild } = await inquirer_1.default.prompt([
|
|
897
|
+
{
|
|
898
|
+
type: 'confirm',
|
|
899
|
+
name: 'continueWithoutBuild',
|
|
900
|
+
message: 'El build falló. ¿Continuar sin build y solo levantar contenedores existentes?',
|
|
901
|
+
default: false,
|
|
902
|
+
},
|
|
903
|
+
]);
|
|
904
|
+
if (!continueWithoutBuild) {
|
|
905
|
+
logger_1.log.info('Instalación cancelada');
|
|
906
|
+
writeInstallLog(targetDir);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
const upSpinner = (0, ora_1.default)('Levantando servicios...').start();
|
|
912
|
+
logStep('Levantando servicios Docker', 'info');
|
|
913
|
+
let composeFiles = '-f docker-compose.yml';
|
|
914
|
+
if (fs_1.default.existsSync(path_1.default.join(targetDir, 'docker-compose.prod.yml'))) {
|
|
915
|
+
composeFiles = '-f docker-compose.prod.yml';
|
|
916
|
+
}
|
|
917
|
+
if (includeMonitoring) {
|
|
918
|
+
const monPath = path_1.default.join(targetDir, 'docker-compose.monitoring.yml');
|
|
919
|
+
if (fs_1.default.existsSync(monPath)) {
|
|
920
|
+
composeFiles += ` -f docker-compose.monitoring.yml`;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
try {
|
|
924
|
+
(0, child_process_1.execSync)(`${dockerCmd} ${composeFiles} up -d`, { cwd: targetDir, stdio: 'pipe', timeout: 120000 });
|
|
925
|
+
upSpinner.succeed('Servicios levantados');
|
|
926
|
+
logStep('Servicios Docker levantados', 'success');
|
|
278
927
|
}
|
|
279
928
|
catch (err) {
|
|
280
|
-
|
|
281
|
-
|
|
929
|
+
upSpinner.fail('Error: ' + err.message);
|
|
930
|
+
logStep(`Error levantando servicios: ${err.message}`, 'error');
|
|
931
|
+
// Detectar error de permisos de Docker
|
|
932
|
+
if (err.message?.includes('permission denied') && err.message?.includes('docker.sock')) {
|
|
933
|
+
logger_1.log.blank();
|
|
934
|
+
logger_1.log.warn('Error de permisos de Docker detectado');
|
|
935
|
+
logger_1.log.dim(' Tu usuario no tiene acceso al socket de Docker');
|
|
936
|
+
logger_1.log.blank();
|
|
937
|
+
if (!nonInteractive) {
|
|
938
|
+
const { fixPermissions } = await inquirer_1.default.prompt([
|
|
939
|
+
{
|
|
940
|
+
type: 'confirm',
|
|
941
|
+
name: 'fixPermissions',
|
|
942
|
+
message: '¿Agregar tu usuario al grupo docker para arreglarlo? (requiere sudo)',
|
|
943
|
+
default: true,
|
|
944
|
+
},
|
|
945
|
+
]);
|
|
946
|
+
if (fixPermissions) {
|
|
947
|
+
const fixSpinner = (0, ora_1.default)('Agregando usuario al grupo docker...').start();
|
|
948
|
+
logStep('Arreglando permisos de Docker', 'info');
|
|
949
|
+
try {
|
|
950
|
+
const user = process.env.USER || process.env.USERNAME || 'root';
|
|
951
|
+
(0, child_process_1.execSync)(`sudo usermod -aG docker ${user}`, { stdio: 'pipe' });
|
|
952
|
+
fixSpinner.succeed('Usuario agregado al grupo docker');
|
|
953
|
+
logStep('Permisos de Docker arreglados', 'success');
|
|
954
|
+
logger_1.log.blank();
|
|
955
|
+
logger_1.log.info('Los cambios se aplican en la próxima sesión');
|
|
956
|
+
logger_1.log.dim(` Ejecuta: newgrp docker`);
|
|
957
|
+
logger_1.log.dim(` O cierra sesión y vuelve a entrar`);
|
|
958
|
+
logger_1.log.blank();
|
|
959
|
+
const { retryNow } = await inquirer_1.default.prompt([
|
|
960
|
+
{
|
|
961
|
+
type: 'confirm',
|
|
962
|
+
name: 'retryNow',
|
|
963
|
+
message: '¿Intentar levantar los servicios ahora? (puede fallar hasta que apliquen los permisos)',
|
|
964
|
+
default: false,
|
|
965
|
+
},
|
|
966
|
+
]);
|
|
967
|
+
if (retryNow) {
|
|
968
|
+
const retrySpinner = (0, ora_1.default)('Reintentando...').start();
|
|
969
|
+
try {
|
|
970
|
+
(0, child_process_1.execSync)(`${dockerCmd} ${composeFiles} up -d`, { cwd: targetDir, stdio: 'pipe', timeout: 120000 });
|
|
971
|
+
retrySpinner.succeed('Servicios levantados');
|
|
972
|
+
logStep('Servicios levantados tras arreglar permisos', 'success');
|
|
973
|
+
}
|
|
974
|
+
catch (retryErr) {
|
|
975
|
+
retrySpinner.warn('Aún hay error de permisos');
|
|
976
|
+
logStep('Reintento fallido, permisos no aplicados aún', 'warn');
|
|
977
|
+
logger_1.log.dim(' Cierra sesión y vuelve a entrar, luego ejecuta:');
|
|
978
|
+
logger_1.log.dim(` cd ${targetDir} && ${dockerCmd} up -d`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
catch (fixErr) {
|
|
983
|
+
fixSpinner.fail('No se pudieron arreglar los permisos');
|
|
984
|
+
logStep(`Error arreglando permisos: ${fixErr.message}`, 'error');
|
|
985
|
+
logger_1.log.dim(' Ejecuta manualmente: sudo usermod -aG docker $USER');
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
logger_1.log.dim(' Ejecuta: sudo usermod -aG docker $USER');
|
|
991
|
+
logger_1.log.dim(' Luego cierra sesión y vuelve a entrar');
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
else if (err.message?.includes('Bind for') && err.message?.includes('port is already allocated')) {
|
|
995
|
+
// Detectar error de puerto ocupado
|
|
996
|
+
logger_1.log.blank();
|
|
997
|
+
logger_1.log.warn('Puerto ocupado detectado');
|
|
998
|
+
const portMatch = err.message.match(/Bind for [\d.]+:(\d+)/);
|
|
999
|
+
const occupiedPort = portMatch ? portMatch[1] : 'desconocido';
|
|
1000
|
+
logger_1.log.warn(` Puerto ${occupiedPort} ya está en uso`);
|
|
1001
|
+
logger_1.log.blank();
|
|
1002
|
+
if (!nonInteractive) {
|
|
1003
|
+
const { portAction } = await inquirer_1.default.prompt([
|
|
1004
|
+
{
|
|
1005
|
+
type: 'list',
|
|
1006
|
+
name: 'portAction',
|
|
1007
|
+
message: '¿Cómo resolverlo?',
|
|
1008
|
+
choices: [
|
|
1009
|
+
{ name: 'Detener el proceso que ocupa el puerto', value: 'stop' },
|
|
1010
|
+
{ name: 'Cambiar puerto en .env y reintentar', value: 'change' },
|
|
1011
|
+
{ name: 'Cancelar', value: 'cancel' },
|
|
1012
|
+
],
|
|
1013
|
+
},
|
|
1014
|
+
]);
|
|
1015
|
+
if (portAction === 'cancel') {
|
|
1016
|
+
logger_1.log.info('Instalación cancelada');
|
|
1017
|
+
writeInstallLog(targetDir);
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (portAction === 'stop') {
|
|
1021
|
+
const stopSpinner = (0, ora_1.default)(`Deteniendo proceso en puerto ${occupiedPort}...`).start();
|
|
1022
|
+
try {
|
|
1023
|
+
const pid = (0, child_process_1.execSync)(`lsof -i :${occupiedPort} -sTCP:LISTEN -t 2>/dev/null | head -1`, { stdio: 'pipe' }).toString().trim();
|
|
1024
|
+
if (pid) {
|
|
1025
|
+
(0, child_process_1.execSync)(`kill -9 ${pid} 2>/dev/null || true`, { stdio: 'pipe' });
|
|
1026
|
+
stopSpinner.succeed(`Proceso detenido`);
|
|
1027
|
+
logStep(`Puerto ${occupiedPort} liberado`, 'success');
|
|
1028
|
+
const { retry } = await inquirer_1.default.prompt([
|
|
1029
|
+
{ type: 'confirm', name: 'retry', message: '¿Reintentar levantar servicios?', default: true },
|
|
1030
|
+
]);
|
|
1031
|
+
if (retry) {
|
|
1032
|
+
const retrySpinner = (0, ora_1.default)('Reintentando...').start();
|
|
1033
|
+
try {
|
|
1034
|
+
(0, child_process_1.execSync)(`${dockerCmd} ${composeFiles} up -d`, { cwd: targetDir, stdio: 'pipe', timeout: 120000 });
|
|
1035
|
+
retrySpinner.succeed('Servicios levantados');
|
|
1036
|
+
logStep('Servicios levantados tras liberar puerto', 'success');
|
|
1037
|
+
}
|
|
1038
|
+
catch {
|
|
1039
|
+
retrySpinner.fail('Aún hay error');
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
else {
|
|
1044
|
+
stopSpinner.warn('No se encontró el proceso');
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
catch {
|
|
1048
|
+
stopSpinner.fail('No se pudo detener el proceso');
|
|
1049
|
+
logger_1.log.dim(` Manual: sudo lsof -i :${occupiedPort} -t | xargs kill -9`);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
if (portAction === 'change') {
|
|
1053
|
+
const envPath = path_1.default.join(targetDir, '.env');
|
|
1054
|
+
if (fs_1.default.existsSync(envPath)) {
|
|
1055
|
+
const { newPort } = await inquirer_1.default.prompt([
|
|
1056
|
+
{ type: 'input', name: 'newPort', message: `Nuevo puerto para reemplazar ${occupiedPort}:`, default: String(parseInt(occupiedPort) + 1) },
|
|
1057
|
+
]);
|
|
1058
|
+
let envContent = fs_1.default.readFileSync(envPath, 'utf-8');
|
|
1059
|
+
envContent = envContent.replace(new RegExp(`:${occupiedPort}\\b`, 'g'), `:${newPort}`);
|
|
1060
|
+
envContent = envContent.replace(new RegExp(`PORT=${occupiedPort}`, 'g'), `PORT=${newPort}`);
|
|
1061
|
+
fs_1.default.writeFileSync(envPath, envContent);
|
|
1062
|
+
logger_1.log.success(`Puerto cambiado a ${newPort} en .env`);
|
|
1063
|
+
logStep(`Puerto ${occupiedPort} → ${newPort}`, 'success');
|
|
1064
|
+
const { retry } = await inquirer_1.default.prompt([
|
|
1065
|
+
{ type: 'confirm', name: 'retry', message: '¿Reintentar levantar servicios?', default: true },
|
|
1066
|
+
]);
|
|
1067
|
+
if (retry) {
|
|
1068
|
+
const retrySpinner = (0, ora_1.default)('Reintentando...').start();
|
|
1069
|
+
try {
|
|
1070
|
+
(0, child_process_1.execSync)(`${dockerCmd} ${composeFiles} up -d`, { cwd: targetDir, stdio: 'pipe', timeout: 120000 });
|
|
1071
|
+
retrySpinner.succeed('Servicios levantados');
|
|
1072
|
+
}
|
|
1073
|
+
catch {
|
|
1074
|
+
retrySpinner.fail('Aún hay error');
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
logger_1.log.dim(` Puerto ${occupiedPort} ocupado. Cambia el puerto en .env o detén el proceso.`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
logger_1.log.dim(` cd ${targetDir} && ${dockerCmd} up -d`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
// Health checks
|
|
1089
|
+
if (opts.healthcheck !== false && (installMode === 'full' || installMode === 'docker')) {
|
|
1090
|
+
logger_1.log.blank();
|
|
1091
|
+
const healthSpinner = (0, ora_1.default)('Verificando servicios...').start();
|
|
1092
|
+
logStep('Ejecutando health checks', 'info');
|
|
1093
|
+
const webHealthy = await (0, helpers_1.waitForService)('http://localhost:8080', 20, 3000);
|
|
1094
|
+
const apiHealthy = await (0, helpers_1.waitForService)('http://localhost:3000/api/health', 15, 3000);
|
|
1095
|
+
if (webHealthy && apiHealthy) {
|
|
1096
|
+
healthSpinner.succeed('Servicios operativos');
|
|
1097
|
+
logStep('Health checks: Web OK, API OK', 'success');
|
|
1098
|
+
}
|
|
1099
|
+
else {
|
|
1100
|
+
healthSpinner.warn('Algunos servicios tardan en iniciar');
|
|
1101
|
+
logStep(`Health checks: Web=${webHealthy ? 'OK' : 'FAIL'}, API=${apiHealthy ? 'OK' : 'FAIL'}`, 'warn');
|
|
1102
|
+
logger_1.log.dim(' Verifica con: docker compose ps');
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
// Setup DB si es full
|
|
1106
|
+
if (installMode === 'full') {
|
|
1107
|
+
logger_1.log.blank();
|
|
1108
|
+
logger_1.log.info('Ejecutando configuración inicial de base de datos...');
|
|
1109
|
+
logStep('Configurando base de datos', 'info');
|
|
1110
|
+
try {
|
|
1111
|
+
(0, child_process_1.execSync)('docker compose exec -T server sh -c "npm run db:push || true"', {
|
|
1112
|
+
cwd: targetDir,
|
|
1113
|
+
stdio: 'pipe',
|
|
1114
|
+
timeout: 60000,
|
|
1115
|
+
});
|
|
1116
|
+
logger_1.log.success('Base de datos configurada');
|
|
1117
|
+
logStep('Base de datos configurada', 'success');
|
|
1118
|
+
}
|
|
1119
|
+
catch {
|
|
1120
|
+
logger_1.log.dim(' Ejecuta manualmente: openfactu setup');
|
|
1121
|
+
logStep('Configuración de BD omitida', 'warn');
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
// 11. Instalar como servicio si se pidió
|
|
1126
|
+
if (opts.service) {
|
|
1127
|
+
logger_1.log.blank();
|
|
1128
|
+
logger_1.log.info('Instalando servicio systemd...');
|
|
1129
|
+
try {
|
|
1130
|
+
const serviceName = 'openfactu';
|
|
1131
|
+
const unitPath = `/etc/systemd/system/${serviceName}.service`;
|
|
1132
|
+
const serviceExists = fs_1.default.existsSync(unitPath);
|
|
1133
|
+
if (serviceExists && !nonInteractive) {
|
|
1134
|
+
const { overwriteService } = await inquirer_1.default.prompt([
|
|
1135
|
+
{
|
|
1136
|
+
type: 'confirm',
|
|
1137
|
+
name: 'overwriteService',
|
|
1138
|
+
message: `El servicio ${serviceName} ya existe. ¿Sobrescribir?`,
|
|
1139
|
+
default: false,
|
|
1140
|
+
},
|
|
1141
|
+
]);
|
|
1142
|
+
if (!overwriteService) {
|
|
1143
|
+
logger_1.log.info('Servicio no modificado');
|
|
1144
|
+
}
|
|
1145
|
+
else {
|
|
1146
|
+
installService(targetDir, dockerCmd, serviceName, unitPath);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
else if (serviceExists && nonInteractive) {
|
|
1150
|
+
logger_1.log.warn(`El servicio ${serviceName} ya existe, sobrescribiendo`);
|
|
1151
|
+
installService(targetDir, dockerCmd, serviceName, unitPath);
|
|
1152
|
+
}
|
|
1153
|
+
else {
|
|
1154
|
+
installService(targetDir, dockerCmd, serviceName, unitPath);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
catch (err) {
|
|
1158
|
+
logger_1.log.warn('No se pudo instalar el servicio: ' + err.message);
|
|
282
1159
|
}
|
|
283
1160
|
}
|
|
284
|
-
//
|
|
1161
|
+
// 12. Escribir log de instalacion
|
|
1162
|
+
writeInstallLog(targetDir);
|
|
1163
|
+
// 12. Resumen
|
|
285
1164
|
const installedPkg = path_1.default.join(targetDir, 'package.json');
|
|
286
1165
|
let installedVersion = '?';
|
|
287
1166
|
if (fs_1.default.existsSync(installedPkg)) {
|
|
@@ -304,20 +1183,35 @@ function registerInstallCommand(program) {
|
|
|
304
1183
|
console.log(` ${chalk_1.default.dim('Commit:')} ${chalk_1.default.cyan(installedCommit)}`);
|
|
305
1184
|
console.log(` ${chalk_1.default.dim('Directorio:')} ${chalk_1.default.white(targetDir)}`);
|
|
306
1185
|
console.log(` ${chalk_1.default.dim('Modo:')} ${chalk_1.default.white(installMode)}`);
|
|
1186
|
+
if (includeMonitoring)
|
|
1187
|
+
console.log(` ${chalk_1.default.dim('Monitoreo:')} ${chalk_1.default.green(monitoringServices.join(', '))}`);
|
|
307
1188
|
logger_1.log.blank();
|
|
1189
|
+
logStep('Instalación completada exitosamente', 'success');
|
|
1190
|
+
logStep(`Versión: ${installedVersion}, Ref: ${ref}, Commit: ${installedCommit}`, 'info');
|
|
308
1191
|
logger_1.log.dim(' Próximos pasos:');
|
|
309
1192
|
logger_1.log.dim(` cd ${targetDir}`);
|
|
310
|
-
if (installMode
|
|
1193
|
+
if (installMode !== 'download') {
|
|
311
1194
|
logger_1.log.dim(' openfactu deploy — Configurar acceso externo');
|
|
1195
|
+
logger_1.log.dim(' openfactu setup — Configurar base de datos');
|
|
312
1196
|
logger_1.log.dim(' openfactu deploy:status — Ver estado de servicios');
|
|
1197
|
+
if (includeMonitoring) {
|
|
1198
|
+
logger_1.log.dim(' openfactu monitoring — Configurar monitoreo');
|
|
1199
|
+
}
|
|
313
1200
|
}
|
|
314
1201
|
else {
|
|
315
|
-
logger_1.log.dim(
|
|
1202
|
+
logger_1.log.dim(` ${dockerCmd} up -d — Levantar con Docker`);
|
|
316
1203
|
logger_1.log.dim(' openfactu deploy — Configurar acceso externo');
|
|
317
1204
|
}
|
|
318
1205
|
logger_1.log.blank();
|
|
1206
|
+
logger_1.log.dim(` Log de instalación: ${chalk_1.default.dim(path_1.default.join(targetDir, '.openfactu', 'install.log'))}`);
|
|
1207
|
+
logger_1.log.blank();
|
|
319
1208
|
}
|
|
320
1209
|
catch (err) {
|
|
1210
|
+
logStep(`Error fatal: ${err.message}`, 'error');
|
|
1211
|
+
try {
|
|
1212
|
+
writeInstallLog(targetDir || os_1.default.homedir());
|
|
1213
|
+
}
|
|
1214
|
+
catch { }
|
|
321
1215
|
logger_1.log.error(err.message);
|
|
322
1216
|
process.exitCode = 1;
|
|
323
1217
|
}
|