@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.
@@ -0,0 +1,402 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerServiceCommand = registerServiceCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const ora_1 = __importDefault(require("ora"));
9
+ const inquirer_1 = __importDefault(require("inquirer"));
10
+ const child_process_1 = require("child_process");
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const os_1 = __importDefault(require("os"));
14
+ const logger_1 = require("../utils/logger");
15
+ const paths_1 = require("../utils/paths");
16
+ const helpers_1 = require("../utils/helpers");
17
+ function getServiceName(name) {
18
+ return `openfactu${name !== 'openfactu' ? `-${name}` : ''}`;
19
+ }
20
+ function generateUnitFile(serviceName, workDir, composeFile, options) {
21
+ const dockerCmd = (0, helpers_1.getDockerComposeCommand)();
22
+ const user = options.user || os_1.default.userInfo().username;
23
+ const envFile = path_1.default.join(workDir, '.env');
24
+ let composeFlags = `-f ${composeFile}`;
25
+ if (options.includeMonitoring && options.monitoringComposeFile) {
26
+ composeFlags += ` -f ${options.monitoringComposeFile}`;
27
+ }
28
+ let envVars = '';
29
+ if (options.environment) {
30
+ for (const [key, value] of Object.entries(options.environment)) {
31
+ envVars += `Environment="${key}=${value}"\n`;
32
+ }
33
+ }
34
+ return `[Unit]
35
+ Description=OpenFactu ${serviceName} Service
36
+ After=docker.service network-online.target
37
+ Requires=docker.service
38
+ Wants=network-online.target
39
+
40
+ [Service]
41
+ Type=oneshot
42
+ RemainAfterExit=yes
43
+ WorkingDirectory=${workDir}
44
+ User=${user}
45
+ ExecStart=${dockerCmd} ${composeFlags} up -d
46
+ ExecStop=${dockerCmd} ${composeFlags} down
47
+ ExecReload=${dockerCmd} ${composeFlags} restart
48
+ Restart=${options.restartPolicy}
49
+ RestartSec=30
50
+ TimeoutStartSec=300
51
+ TimeoutStopSec=120
52
+
53
+ ${envVars}
54
+ EnvironmentFile=${envFile}
55
+
56
+ StandardOutput=journal
57
+ StandardError=journal
58
+ SyslogIdentifier=${serviceName}
59
+
60
+ [Install]
61
+ WantedBy=multi-user.target
62
+ `;
63
+ }
64
+ function generateTimerFile(serviceName, interval) {
65
+ return `[Unit]
66
+ Description=OpenFactu ${serviceName} Health Check Timer
67
+
68
+ [Timer]
69
+ OnBootSec=5min
70
+ OnUnitActiveSec=${interval}
71
+ Persistent=true
72
+
73
+ [Install]
74
+ WantedBy=timers.target
75
+ `;
76
+ }
77
+ function generateHealthCheckScript(workDir) {
78
+ const dockerCmd = (0, helpers_1.getDockerComposeCommand)();
79
+ return `#!/bin/bash
80
+ # OpenFactu Health Check Script
81
+ set -e
82
+
83
+ cd "${workDir}"
84
+
85
+ # Check if containers are running
86
+ FAILED=$(${dockerCmd} ps --format '{{.Name}}:{{.Status}}' | grep -v "Up" || true)
87
+
88
+ if [ -n "$FAILED" ]; then
89
+ echo "$(date): Some containers are not running:"
90
+ echo "$FAILED"
91
+ echo "Attempting restart..."
92
+ ${dockerCmd} restart
93
+ exit 1
94
+ fi
95
+
96
+ echo "$(date): All containers healthy"
97
+ exit 0
98
+ `;
99
+ }
100
+ function registerServiceCommand(program) {
101
+ const serviceCmd = program
102
+ .command('service')
103
+ .description('Gestionar OpenFactu como servicio del sistema');
104
+ serviceCmd
105
+ .command('install')
106
+ .description('Instalar OpenFactu como servicio systemd')
107
+ .option('--name <name>', 'Nombre del servicio', 'openfactu')
108
+ .option('--restart <policy>', 'Politica de reinicio (no, on-failure, always)', 'on-failure')
109
+ .option('--with-monitoring', 'Incluir servicios de monitoreo')
110
+ .option('--healthcheck', 'Agregar health check automatico')
111
+ .option('--healthcheck-interval <interval>', 'Intervalo del health check', '5min')
112
+ .option('--user <user>', 'Usuario para ejecutar el servicio')
113
+ .option('--path <path>', 'Ruta del proyecto OpenFactu')
114
+ .action(async (opts) => {
115
+ console.log();
116
+ console.log(chalk_1.default.bold.white(' OpenFactu — Instalar Servicio'));
117
+ console.log(chalk_1.default.dim(' ────────────────────────────────────'));
118
+ console.log();
119
+ if (!(0, helpers_1.isSystemdAvailable)()) {
120
+ logger_1.log.error('systemd no esta disponible en este sistema');
121
+ logger_1.log.dim(' Este comando solo funciona en Linux con systemd');
122
+ return;
123
+ }
124
+ try {
125
+ const root = opts.path || (0, paths_1.getProjectRoot)();
126
+ const serviceName = getServiceName(opts.name);
127
+ // Detectar compose files
128
+ const composeFiles = [];
129
+ const possibleFiles = [
130
+ 'docker-compose.prod.yml',
131
+ 'docker-compose.yml',
132
+ ];
133
+ for (const f of possibleFiles) {
134
+ if (fs_1.default.existsSync(path_1.default.join(root, f))) {
135
+ composeFiles.push(f);
136
+ }
137
+ }
138
+ if (composeFiles.length === 0) {
139
+ logger_1.log.error('No se encontro docker-compose.yml en el proyecto');
140
+ return;
141
+ }
142
+ const mainCompose = composeFiles[0];
143
+ // Monitoring
144
+ let includeMonitoring = opts.withMonitoring || false;
145
+ let monitoringCompose;
146
+ if (!includeMonitoring) {
147
+ const { addMonitoring } = await inquirer_1.default.prompt([
148
+ {
149
+ type: 'confirm',
150
+ name: 'addMonitoring',
151
+ message: 'Incluir stack de monitoreo en el servicio?',
152
+ default: false,
153
+ },
154
+ ]);
155
+ includeMonitoring = addMonitoring;
156
+ }
157
+ if (includeMonitoring) {
158
+ const monFiles = [
159
+ 'docker-compose.prod.monitoring.yml',
160
+ 'docker-compose.monitoring.yml',
161
+ ];
162
+ for (const f of monFiles) {
163
+ if (fs_1.default.existsSync(path_1.default.join(root, f))) {
164
+ monitoringCompose = f;
165
+ break;
166
+ }
167
+ }
168
+ if (!monitoringCompose) {
169
+ logger_1.log.warn('No se encontro compose de monitoreo, se omitira');
170
+ }
171
+ }
172
+ // Confirmar configuracion
173
+ logger_1.log.blank();
174
+ logger_1.log.info(`${chalk_1.default.dim('Servicio:')} ${chalk_1.default.cyan(serviceName)}`);
175
+ logger_1.log.info(`${chalk_1.default.dim('Directorio:')} ${chalk_1.default.cyan(root)}`);
176
+ logger_1.log.info(`${chalk_1.default.dim('Compose:')} ${chalk_1.default.cyan(mainCompose)}`);
177
+ logger_1.log.info(`${chalk_1.default.dim('Reinicio:')} ${chalk_1.default.cyan(opts.restart)}`);
178
+ logger_1.log.blank();
179
+ const { confirm } = await inquirer_1.default.prompt([
180
+ { type: 'confirm', name: 'confirm', message: 'Instalar servicio?', default: true },
181
+ ]);
182
+ if (!confirm)
183
+ return;
184
+ // Generar unit file
185
+ const spinner = (0, ora_1.default)('Generando unit file...').start();
186
+ const unitContent = generateUnitFile(serviceName, root, mainCompose, {
187
+ restartPolicy: opts.restart,
188
+ includeMonitoring: includeMonitoring && !!monitoringCompose,
189
+ monitoringComposeFile: monitoringCompose,
190
+ user: opts.user,
191
+ });
192
+ const unitPath = `/etc/systemd/system/${serviceName}.service`;
193
+ const tempPath = `/tmp/${serviceName}.service`;
194
+ fs_1.default.writeFileSync(tempPath, unitContent);
195
+ // Instalar con sudo
196
+ (0, child_process_1.execSync)(`sudo mv ${tempPath} ${unitPath}`, { stdio: 'pipe' });
197
+ (0, child_process_1.execSync)('sudo systemctl daemon-reload', { stdio: 'pipe' });
198
+ spinner.succeed('Unit file instalado');
199
+ // Health check opcional
200
+ if (opts.healthcheck) {
201
+ const hcSpinner = (0, ora_1.default)('Configurando health check...').start();
202
+ const scriptPath = path_1.default.join(root, '.openfactu-healthcheck.sh');
203
+ fs_1.default.mkdirSync(path_1.default.join(root), { recursive: true });
204
+ fs_1.default.writeFileSync(scriptPath, generateHealthCheckScript(root));
205
+ (0, child_process_1.execSync)(`chmod +x "${scriptPath}"`, { stdio: 'pipe' });
206
+ const timerName = `${serviceName}-healthcheck`;
207
+ const timerContent = generateTimerFile(serviceName, opts.healthcheckInterval);
208
+ const timerPath = `/etc/systemd/system/${timerName}.timer`;
209
+ const serviceContent = `[Unit]
210
+ Description=OpenFactu Health Check
211
+
212
+ [Service]
213
+ Type=oneshot
214
+ ExecStart=${scriptPath}
215
+ `;
216
+ const servicePath = `/etc/systemd/system/${timerName}.service`;
217
+ fs_1.default.writeFileSync(`/tmp/${timerName}.timer`, timerContent);
218
+ fs_1.default.writeFileSync(`/tmp/${timerName}.service`, serviceContent);
219
+ (0, child_process_1.execSync)(`sudo mv /tmp/${timerName}.timer ${timerPath}`, { stdio: 'pipe' });
220
+ (0, child_process_1.execSync)(`sudo mv /tmp/${timerName}.service ${servicePath}`, { stdio: 'pipe' });
221
+ (0, child_process_1.execSync)('sudo systemctl daemon-reload', { stdio: 'pipe' });
222
+ (0, child_process_1.execSync)(`sudo systemctl enable ${timerName}.timer`, { stdio: 'pipe' });
223
+ hcSpinner.succeed('Health check configurado');
224
+ }
225
+ // Enable service
226
+ const enableSpinner = (0, ora_1.default)('Habilitando servicio...').start();
227
+ (0, child_process_1.execSync)(`sudo systemctl enable ${serviceName}`, { stdio: 'pipe' });
228
+ enableSpinner.succeed('Servicio habilitado');
229
+ logger_1.log.blank();
230
+ console.log(chalk_1.default.bold.green(' Servicio instalado'));
231
+ console.log(chalk_1.default.dim(' ────────────────────────────────────'));
232
+ logger_1.log.blank();
233
+ logger_1.log.dim(' Comandos utiles:');
234
+ logger_1.log.dim(` sudo systemctl start ${serviceName}`);
235
+ logger_1.log.dim(` sudo systemctl stop ${serviceName}`);
236
+ logger_1.log.dim(` sudo systemctl restart ${serviceName}`);
237
+ logger_1.log.dim(` sudo systemctl status ${serviceName}`);
238
+ logger_1.log.dim(` journalctl -u ${serviceName} -f`);
239
+ logger_1.log.blank();
240
+ const { startNow } = await inquirer_1.default.prompt([
241
+ { type: 'confirm', name: 'startNow', message: 'Iniciar servicio ahora?', default: true },
242
+ ]);
243
+ if (startNow) {
244
+ const startSpinner = (0, ora_1.default)('Iniciando servicio...').start();
245
+ try {
246
+ (0, child_process_1.execSync)(`sudo systemctl start ${serviceName}`, { stdio: 'pipe' });
247
+ startSpinner.succeed('Servicio iniciado');
248
+ }
249
+ catch (err) {
250
+ startSpinner.fail('Error al iniciar');
251
+ logger_1.log.dim(` sudo systemctl status ${serviceName}`);
252
+ }
253
+ }
254
+ }
255
+ catch (err) {
256
+ logger_1.log.error(err.message);
257
+ process.exitCode = 1;
258
+ }
259
+ });
260
+ serviceCmd
261
+ .command('status')
262
+ .description('Ver estado del servicio')
263
+ .option('--name <name>', 'Nombre del servicio', 'openfactu')
264
+ .action(async (opts) => {
265
+ try {
266
+ const serviceName = getServiceName(opts.name);
267
+ const output = (0, child_process_1.execSync)(`systemctl status ${serviceName} 2>&1 || true`, { stdio: 'pipe' }).toString();
268
+ console.log(output);
269
+ }
270
+ catch (err) {
271
+ logger_1.log.error(err.message);
272
+ }
273
+ });
274
+ serviceCmd
275
+ .command('start')
276
+ .description('Iniciar servicio')
277
+ .option('--name <name>', 'Nombre del servicio', 'openfactu')
278
+ .action(async (opts) => {
279
+ try {
280
+ const serviceName = getServiceName(opts.name);
281
+ const spinner = (0, ora_1.default)('Iniciando servicio...').start();
282
+ (0, child_process_1.execSync)(`sudo systemctl start ${serviceName}`, { stdio: 'pipe' });
283
+ spinner.succeed('Servicio iniciado');
284
+ }
285
+ catch (err) {
286
+ logger_1.log.error(err.message);
287
+ }
288
+ });
289
+ serviceCmd
290
+ .command('stop')
291
+ .description('Detener servicio')
292
+ .option('--name <name>', 'Nombre del servicio', 'openfactu')
293
+ .action(async (opts) => {
294
+ try {
295
+ const serviceName = getServiceName(opts.name);
296
+ const spinner = (0, ora_1.default)('Deteniendo servicio...').start();
297
+ (0, child_process_1.execSync)(`sudo systemctl stop ${serviceName}`, { stdio: 'pipe' });
298
+ spinner.succeed('Servicio detenido');
299
+ }
300
+ catch (err) {
301
+ logger_1.log.error(err.message);
302
+ }
303
+ });
304
+ serviceCmd
305
+ .command('restart')
306
+ .description('Reiniciar servicio')
307
+ .option('--name <name>', 'Nombre del servicio', 'openfactu')
308
+ .action(async (opts) => {
309
+ try {
310
+ const serviceName = getServiceName(opts.name);
311
+ const spinner = (0, ora_1.default)('Reiniciando servicio...').start();
312
+ (0, child_process_1.execSync)(`sudo systemctl restart ${serviceName}`, { stdio: 'pipe' });
313
+ spinner.succeed('Servicio reiniciado');
314
+ }
315
+ catch (err) {
316
+ logger_1.log.error(err.message);
317
+ }
318
+ });
319
+ serviceCmd
320
+ .command('uninstall')
321
+ .description('Remover servicio systemd')
322
+ .option('--name <name>', 'Nombre del servicio', 'openfactu')
323
+ .option('--keep-data', 'Mantener datos del proyecto')
324
+ .action(async (opts) => {
325
+ console.log();
326
+ console.log(chalk_1.default.bold.white(' OpenFactu — Remover Servicio'));
327
+ console.log(chalk_1.default.dim(' ────────────────────────────────────'));
328
+ console.log();
329
+ try {
330
+ const serviceName = getServiceName(opts.name);
331
+ const { confirm } = await inquirer_1.default.prompt([
332
+ {
333
+ type: 'confirm',
334
+ name: 'confirm',
335
+ message: `Remover servicio ${serviceName}?`,
336
+ default: false,
337
+ },
338
+ ]);
339
+ if (!confirm)
340
+ return;
341
+ const spinner = (0, ora_1.default)('Removiendo servicio...').start();
342
+ (0, child_process_1.execSync)(`sudo systemctl stop ${serviceName} 2>/dev/null || true`, { stdio: 'pipe' });
343
+ (0, child_process_1.execSync)(`sudo systemctl disable ${serviceName} 2>/dev/null || true`, { stdio: 'pipe' });
344
+ const unitPath = `/etc/systemd/system/${serviceName}.service`;
345
+ if (fs_1.default.existsSync(unitPath)) {
346
+ (0, child_process_1.execSync)(`sudo rm ${unitPath}`, { stdio: 'pipe' });
347
+ }
348
+ // Remover health check si existe
349
+ const timerName = `${serviceName}-healthcheck`;
350
+ const timerPath = `/etc/systemd/system/${timerName}.timer`;
351
+ const hcServicePath = `/etc/systemd/system/${timerName}.service`;
352
+ if (fs_1.default.existsSync(timerPath))
353
+ (0, child_process_1.execSync)(`sudo rm ${timerPath}`, { stdio: 'pipe' });
354
+ if (fs_1.default.existsSync(hcServicePath))
355
+ (0, child_process_1.execSync)(`sudo rm ${hcServicePath}`, { stdio: 'pipe' });
356
+ (0, child_process_1.execSync)('sudo systemctl daemon-reload', { stdio: 'pipe' });
357
+ spinner.succeed('Servicio removido');
358
+ if (!opts.keepData) {
359
+ const { removeData } = await inquirer_1.default.prompt([
360
+ {
361
+ type: 'confirm',
362
+ name: 'removeData',
363
+ message: 'Remover tambien los contenedores Docker?',
364
+ default: false,
365
+ },
366
+ ]);
367
+ if (removeData) {
368
+ try {
369
+ const root = (0, paths_1.getProjectRoot)();
370
+ const dockerCmd = (0, helpers_1.getDockerComposeCommand)();
371
+ (0, child_process_1.execSync)(`${dockerCmd} down`, { cwd: root, stdio: 'pipe' });
372
+ logger_1.log.success('Contenedores removidos');
373
+ }
374
+ catch {
375
+ logger_1.log.warn('No se pudieron remover los contenedores');
376
+ }
377
+ }
378
+ }
379
+ }
380
+ catch (err) {
381
+ logger_1.log.error(err.message);
382
+ }
383
+ });
384
+ serviceCmd
385
+ .command('logs')
386
+ .description('Ver logs del servicio')
387
+ .option('--name <name>', 'Nombre del servicio', 'openfactu')
388
+ .option('-f, --follow', 'Seguir logs en tiempo real')
389
+ .option('-n, --lines <number>', 'Numero de lineas', '100')
390
+ .action(async (opts) => {
391
+ try {
392
+ const serviceName = getServiceName(opts.name);
393
+ const follow = opts.follow ? ' -f' : '';
394
+ (0, child_process_1.execSync)(`journalctl -u ${serviceName}${follow} -n ${opts.lines}`, {
395
+ stdio: 'inherit',
396
+ });
397
+ }
398
+ catch (err) {
399
+ logger_1.log.error(err.message);
400
+ }
401
+ });
402
+ }
@@ -8,7 +8,6 @@ const chalk_1 = __importDefault(require("chalk"));
8
8
  const ora_1 = __importDefault(require("ora"));
9
9
  const inquirer_1 = __importDefault(require("inquirer"));
10
10
  const crypto_1 = __importDefault(require("crypto"));
11
- const bcrypt_1 = __importDefault(require("bcrypt"));
12
11
  const db_1 = require("../utils/db");
13
12
  const logger_1 = require("../utils/logger");
14
13
  function registerSetupCommand(program) {
@@ -66,7 +65,13 @@ function registerSetupCommand(program) {
66
65
  mask: '*',
67
66
  },
68
67
  ]);
69
- const hashedPassword = await bcrypt_1.default.hash(adminPassword, 10);
68
+ const hashedPassword = await new Promise((resolve, reject) => {
69
+ crypto_1.default.scrypt(adminPassword, 'openfactu-salt', 64, (err, derivedKey) => {
70
+ if (err)
71
+ reject(err);
72
+ resolve(derivedKey.toString('hex'));
73
+ });
74
+ });
70
75
  await publicDb.insert((0, db_1.schema)().globalUsers).values({
71
76
  id: crypto_1.default.randomUUID(),
72
77
  email: 'admin@openfactu.com',
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerSyncPortsCommand(program: Command): void;