@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.
@@ -0,0 +1,244 @@
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.generatePassword = generatePassword;
7
+ exports.generateSlug = generateSlug;
8
+ exports.checkDiskSpace = checkDiskSpace;
9
+ exports.checkPortInUse = checkPortInUse;
10
+ exports.getRunningServices = getRunningServices;
11
+ exports.runPreflightChecks = runPreflightChecks;
12
+ exports.waitForService = waitForService;
13
+ exports.getDockerComposeCommand = getDockerComposeCommand;
14
+ exports.isLinux = isLinux;
15
+ exports.isSystemdAvailable = isSystemdAvailable;
16
+ exports.formatBytes = formatBytes;
17
+ exports.timestamp = timestamp;
18
+ exports.ensureDir = ensureDir;
19
+ exports.copyDirRecursive = copyDirRecursive;
20
+ const child_process_1 = require("child_process");
21
+ const fs_1 = __importDefault(require("fs"));
22
+ const path_1 = __importDefault(require("path"));
23
+ const os_1 = __importDefault(require("os"));
24
+ const crypto_1 = __importDefault(require("crypto"));
25
+ function generatePassword(length = 32) {
26
+ // Solo alfanumericos: los simbolos (! @ # $ % ^ & *) rompen el stack.
27
+ // - '$' dispara la interpolacion de variables de Docker Compose al leer el
28
+ // .env, vaciando la contraseña y haciendo que Postgres caiga al default.
29
+ // - '@ # % / : ?' rompen el parseo del connection string en DATABASE_URL.
30
+ // Con 62 caracteres y longitud >= 24 la entropia sigue siendo de sobra (>140 bits).
31
+ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
32
+ let result = '';
33
+ const bytes = crypto_1.default.randomBytes(length);
34
+ for (let i = 0; i < length; i++) {
35
+ result += chars[bytes[i] % chars.length];
36
+ }
37
+ return result;
38
+ }
39
+ function generateSlug(length = 12) {
40
+ return crypto_1.default.randomBytes(length).toString('hex').slice(0, length);
41
+ }
42
+ function checkDiskSpace(dir) {
43
+ try {
44
+ const output = (0, child_process_1.execSync)(`df -BG "${dir}" | tail -1`, { stdio: 'pipe' }).toString();
45
+ const parts = output.trim().split(/\s+/);
46
+ return {
47
+ availableGB: parseInt(parts[3]) || 0,
48
+ totalGB: parseInt(parts[1]) || 0,
49
+ };
50
+ }
51
+ catch {
52
+ return { availableGB: 0, totalGB: 0 };
53
+ }
54
+ }
55
+ function checkPortInUse(port) {
56
+ try {
57
+ (0, child_process_1.execSync)(`lsof -i :${port} -sTCP:LISTEN`, { stdio: 'pipe' });
58
+ return true;
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ function getRunningServices() {
65
+ try {
66
+ const output = (0, child_process_1.execSync)('docker compose ps --services 2>/dev/null || docker-compose ps --services 2>/dev/null', {
67
+ stdio: 'pipe',
68
+ }).toString();
69
+ return output.trim().split('\n').filter(Boolean);
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ }
75
+ function runPreflightChecks(targetDir) {
76
+ const checks = [];
77
+ // Node.js version
78
+ try {
79
+ const version = process.version;
80
+ const major = parseInt(version.slice(1).split('.')[0]);
81
+ if (major >= 18) {
82
+ checks.push({ name: 'Node.js', status: 'pass', message: `${version}` });
83
+ }
84
+ else {
85
+ checks.push({ name: 'Node.js', status: 'warn', message: `${version} (recomendado >= 18)` });
86
+ }
87
+ }
88
+ catch {
89
+ checks.push({ name: 'Node.js', status: 'fail', message: 'No detectado' });
90
+ }
91
+ // Git
92
+ try {
93
+ const version = (0, child_process_1.execSync)('git --version', { stdio: 'pipe' }).toString().trim();
94
+ checks.push({ name: 'Git', status: 'pass', message: version });
95
+ }
96
+ catch {
97
+ checks.push({ name: 'Git', status: 'fail', message: 'No instalado' });
98
+ }
99
+ // Docker
100
+ try {
101
+ const version = (0, child_process_1.execSync)('docker --version', { stdio: 'pipe' }).toString().trim();
102
+ checks.push({ name: 'Docker', status: 'pass', message: version });
103
+ }
104
+ catch {
105
+ checks.push({ name: 'Docker', status: 'fail', message: 'No instalado' });
106
+ }
107
+ // Docker Compose
108
+ try {
109
+ let version = '';
110
+ try {
111
+ version = (0, child_process_1.execSync)('docker compose version', { stdio: 'pipe' }).toString().trim();
112
+ }
113
+ catch {
114
+ version = (0, child_process_1.execSync)('docker-compose --version', { stdio: 'pipe' }).toString().trim();
115
+ }
116
+ checks.push({ name: 'Docker Compose', status: 'pass', message: version });
117
+ }
118
+ catch {
119
+ checks.push({ name: 'Docker Compose', status: 'fail', message: 'No instalado' });
120
+ }
121
+ // Disk space
122
+ if (targetDir) {
123
+ const dir = path_1.default.dirname(targetDir);
124
+ if (fs_1.default.existsSync(dir)) {
125
+ const disk = checkDiskSpace(dir);
126
+ if (disk.availableGB >= 10) {
127
+ checks.push({ name: 'Disco disponible', status: 'pass', message: `${disk.availableGB}GB libres` });
128
+ }
129
+ else if (disk.availableGB >= 5) {
130
+ checks.push({ name: 'Disco disponible', status: 'warn', message: `${disk.availableGB}GB libres (minimo 5GB)` });
131
+ }
132
+ else {
133
+ checks.push({ name: 'Disco disponible', status: 'fail', message: `${disk.availableGB}GB libres (minimo 5GB)` });
134
+ }
135
+ }
136
+ }
137
+ // Port conflicts
138
+ const commonPorts = [
139
+ { port: 5432, name: 'PostgreSQL' },
140
+ { port: 8080, name: 'Web' },
141
+ { port: 3000, name: 'API Server' },
142
+ { port: 9090, name: 'Prometheus' },
143
+ { port: 3001, name: 'Grafana' },
144
+ { port: 5050, name: 'pgAdmin' },
145
+ { port: 9000, name: 'Portainer' },
146
+ ];
147
+ const conflictedPorts = commonPorts.filter(p => checkPortInUse(p.port));
148
+ if (conflictedPorts.length === 0) {
149
+ checks.push({ name: 'Puertos', status: 'pass', message: 'Sin conflictos' });
150
+ }
151
+ else {
152
+ const portList = conflictedPorts.map(p => `${p.name}:${p.port}`).join(', ');
153
+ checks.push({ name: 'Puertos', status: 'warn', message: `En uso: ${portList}` });
154
+ }
155
+ // OS info
156
+ checks.push({ name: 'Sistema', status: 'pass', message: `${os_1.default.type()} ${os_1.default.release()} (${os_1.default.arch()})` });
157
+ return checks;
158
+ }
159
+ function waitForService(url, maxAttempts = 30, intervalMs = 2000) {
160
+ return new Promise((resolve) => {
161
+ let attempts = 0;
162
+ const http = url.startsWith('https') ? require('https') : require('http');
163
+ const check = () => {
164
+ attempts++;
165
+ const req = http.get(url, { timeout: 3000 }, (res) => {
166
+ if (res.statusCode) {
167
+ resolve(true);
168
+ }
169
+ else {
170
+ retry();
171
+ }
172
+ });
173
+ req.on('error', () => {
174
+ if (attempts >= maxAttempts) {
175
+ resolve(false);
176
+ }
177
+ else {
178
+ retry();
179
+ }
180
+ });
181
+ req.on('timeout', () => {
182
+ req.destroy();
183
+ retry();
184
+ });
185
+ };
186
+ const retry = () => {
187
+ setTimeout(check, intervalMs);
188
+ };
189
+ check();
190
+ });
191
+ }
192
+ function getDockerComposeCommand() {
193
+ try {
194
+ (0, child_process_1.execSync)('docker compose version', { stdio: 'pipe' });
195
+ return 'docker compose';
196
+ }
197
+ catch {
198
+ return 'docker-compose';
199
+ }
200
+ }
201
+ function isLinux() {
202
+ return os_1.default.platform() === 'linux';
203
+ }
204
+ function isSystemdAvailable() {
205
+ if (!isLinux())
206
+ return false;
207
+ try {
208
+ const pid = (0, child_process_1.execSync)('cat /run/systemd/system 2>/dev/null && echo 1 || echo 0', { stdio: 'pipe' }).toString().trim();
209
+ return pid === '1' || fs_1.default.existsSync('/run/systemd/system');
210
+ }
211
+ catch {
212
+ return false;
213
+ }
214
+ }
215
+ function formatBytes(bytes) {
216
+ if (bytes === 0)
217
+ return '0 B';
218
+ const k = 1024;
219
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
220
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
221
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
222
+ }
223
+ function timestamp() {
224
+ return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
225
+ }
226
+ function ensureDir(dir) {
227
+ if (!fs_1.default.existsSync(dir)) {
228
+ fs_1.default.mkdirSync(dir, { recursive: true });
229
+ }
230
+ }
231
+ function copyDirRecursive(src, dest) {
232
+ ensureDir(dest);
233
+ const entries = fs_1.default.readdirSync(src, { withFileTypes: true });
234
+ for (const entry of entries) {
235
+ const srcPath = path_1.default.join(src, entry.name);
236
+ const destPath = path_1.default.join(dest, entry.name);
237
+ if (entry.isDirectory()) {
238
+ copyDirRecursive(srcPath, destPath);
239
+ }
240
+ else {
241
+ fs_1.default.copyFileSync(srcPath, destPath);
242
+ }
243
+ }
244
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Catálogo de servicios de monitoreo. Fuente única usada tanto por el comando
3
+ * `monitoring` como por `install` para construir el checkbox de selección.
4
+ */
5
+ export interface MonitoringService {
6
+ value: string;
7
+ label: string;
8
+ /** Marcado por defecto en un stack básico. */
9
+ basic: boolean;
10
+ /** Pertenece al stack de analítica avanzada (logs/métricas extendidas). */
11
+ analytics: boolean;
12
+ }
13
+ export declare const MONITORING_CATALOG: MonitoringService[];
14
+ export declare const ALL_MONITORING_SERVICES: string[];
15
+ /** Opciones para un prompt tipo checkbox de inquirer. */
16
+ export declare function monitoringChoices(opts?: {
17
+ analytics?: boolean;
18
+ }): {
19
+ name: string;
20
+ value: string;
21
+ checked: boolean;
22
+ }[];
23
+ /** Conjunto básico (pgAdmin, Grafana, Prometheus, Portainer). */
24
+ export declare function basicMonitoringServices(): string[];
25
+ /** Conjunto completo (básico + analítica). */
26
+ export declare function fullMonitoringServices(): string[];
27
+ /** Genera el docker-compose.monitoring.yml con solo los servicios seleccionados. */
28
+ export declare function generateMonitoringCompose(serviceSet: Set<string>): string;
29
+ export declare function generatePrometheusConfig(): string;
30
+ export declare function generateLokiConfig(): string;
31
+ export declare function generatePromtailConfig(): string;
32
+ export declare function generateAlertmanagerConfig(): string;
33
+ /**
34
+ * Escribe los archivos de configuración necesarios para los servicios
35
+ * seleccionados. Cada servicio que monta un archivo de config debe generarlo
36
+ * aquí; si no, Docker crea un directorio en su lugar y el bind-mount falla.
37
+ */
38
+ export declare function writeMonitoringConfigs(root: string, serviceSet: Set<string>): void;
@@ -0,0 +1,353 @@
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.ALL_MONITORING_SERVICES = exports.MONITORING_CATALOG = void 0;
7
+ exports.monitoringChoices = monitoringChoices;
8
+ exports.basicMonitoringServices = basicMonitoringServices;
9
+ exports.fullMonitoringServices = fullMonitoringServices;
10
+ exports.generateMonitoringCompose = generateMonitoringCompose;
11
+ exports.generatePrometheusConfig = generatePrometheusConfig;
12
+ exports.generateLokiConfig = generateLokiConfig;
13
+ exports.generatePromtailConfig = generatePromtailConfig;
14
+ exports.generateAlertmanagerConfig = generateAlertmanagerConfig;
15
+ exports.writeMonitoringConfigs = writeMonitoringConfigs;
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const path_1 = __importDefault(require("path"));
18
+ exports.MONITORING_CATALOG = [
19
+ { value: 'pgadmin', label: 'pgAdmin (gestion BD)', basic: true, analytics: false },
20
+ { value: 'grafana', label: 'Grafana (dashboards)', basic: true, analytics: false },
21
+ { value: 'prometheus', label: 'Prometheus (metricas)', basic: true, analytics: false },
22
+ { value: 'loki', label: 'Loki (agregacion de logs)', basic: false, analytics: true },
23
+ { value: 'promtail', label: 'Promtail (envio de logs a Loki)', basic: false, analytics: true },
24
+ { value: 'cadvisor', label: 'cAdvisor (metricas contenedores)', basic: false, analytics: true },
25
+ { value: 'node-exporter', label: 'Node Exporter (metricas host)', basic: false, analytics: true },
26
+ { value: 'portainer', label: 'Portainer (gestion Docker)', basic: true, analytics: false },
27
+ { value: 'alertmanager', label: 'Alertmanager (alertas)', basic: false, analytics: false },
28
+ ];
29
+ exports.ALL_MONITORING_SERVICES = exports.MONITORING_CATALOG.map((s) => s.value);
30
+ /** Opciones para un prompt tipo checkbox de inquirer. */
31
+ function monitoringChoices(opts = {}) {
32
+ return exports.MONITORING_CATALOG.map((s) => ({
33
+ name: s.label,
34
+ value: s.value,
35
+ checked: s.basic || (opts.analytics ? s.analytics : false),
36
+ }));
37
+ }
38
+ /** Conjunto básico (pgAdmin, Grafana, Prometheus, Portainer). */
39
+ function basicMonitoringServices() {
40
+ return exports.MONITORING_CATALOG.filter((s) => s.basic).map((s) => s.value);
41
+ }
42
+ /** Conjunto completo (básico + analítica). */
43
+ function fullMonitoringServices() {
44
+ return exports.MONITORING_CATALOG.filter((s) => s.basic || s.analytics).map((s) => s.value);
45
+ }
46
+ /**
47
+ * Renderiza un bloque `depends_on:` con solo las dependencias que están en el
48
+ * conjunto seleccionado (evita "depends on undefined service" cuando se elige
49
+ * un subconjunto). Devuelve '' si no queda ninguna dependencia presente.
50
+ */
51
+ function dependsOn(serviceSet, deps) {
52
+ const present = deps.filter((d) => serviceSet.has(d));
53
+ if (present.length === 0)
54
+ return '';
55
+ return ` depends_on:\n` + present.map((d) => ` - ${d}`).join('\n') + '\n';
56
+ }
57
+ /** Genera el docker-compose.monitoring.yml con solo los servicios seleccionados. */
58
+ function generateMonitoringCompose(serviceSet) {
59
+ let compose = `# OpenFactu Monitoring Stack
60
+ # Generated by @openfactu/cli
61
+ # Services: ${Array.from(serviceSet).join(', ')}
62
+
63
+ services:
64
+ `;
65
+ if (serviceSet.has('pgadmin')) {
66
+ compose += `
67
+ pgadmin:
68
+ image: dpage/pgadmin4:latest
69
+ container_name: openfactu-pgadmin
70
+ environment:
71
+ PGADMIN_DEFAULT_EMAIL: \${PGADMIN_EMAIL:-admin@openfactu.local}
72
+ PGADMIN_DEFAULT_PASSWORD: \${PGADMIN_PASSWORD:-admin}
73
+ PGADMIN_CONFIG_SERVER_MODE: 'False'
74
+ ports:
75
+ - "\${PGADMIN_PORT:-5050}:80"
76
+ volumes:
77
+ - ./storage/pgadmin_data:/var/lib/pgadmin
78
+ restart: unless-stopped
79
+ networks:
80
+ - openfactu_net
81
+ `;
82
+ }
83
+ if (serviceSet.has('prometheus')) {
84
+ compose += `
85
+ prometheus:
86
+ image: prom/prometheus:latest
87
+ container_name: openfactu-prometheus
88
+ command:
89
+ - '--config.file=/etc/prometheus/prometheus.yml'
90
+ - '--storage.tsdb.path=/prometheus'
91
+ - '--storage.tsdb.retention.time=15d'
92
+ - '--web.enable-lifecycle'
93
+ - '--web.enable-admin-api'
94
+ ports:
95
+ - "\${PROMETHEUS_PORT:-9090}:9090"
96
+ volumes:
97
+ - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
98
+ - ./storage/prometheus_data:/prometheus
99
+ restart: unless-stopped
100
+ networks:
101
+ - openfactu_net
102
+ `;
103
+ }
104
+ if (serviceSet.has('grafana')) {
105
+ compose += `
106
+ grafana:
107
+ image: grafana/grafana:latest
108
+ container_name: openfactu-grafana
109
+ environment:
110
+ - GF_SECURITY_ADMIN_USER=\${GRAFANA_USER:-admin}
111
+ - GF_SECURITY_ADMIN_PASSWORD=\${GRAFANA_PASSWORD:-admin}
112
+ - GF_USERS_ALLOW_SIGN_UP=false
113
+ - GF_INSTALL_PLUGINS=grafana-piechart-panel
114
+ ports:
115
+ - "\${GRAFANA_PORT:-3001}:3000"
116
+ volumes:
117
+ - ./storage/grafana_data:/var/lib/grafana
118
+ - ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
119
+ ${dependsOn(serviceSet, ['prometheus', 'loki'])} restart: unless-stopped
120
+ networks:
121
+ - openfactu_net
122
+ `;
123
+ }
124
+ if (serviceSet.has('loki')) {
125
+ compose += `
126
+ loki:
127
+ image: grafana/loki:latest
128
+ container_name: openfactu-loki
129
+ command: -config.file=/etc/loki/local-config.yaml
130
+ ports:
131
+ - "\${LOKI_PORT:-3100}:3100"
132
+ volumes:
133
+ - ./storage/loki_data:/loki
134
+ - ./monitoring/loki/loki-config.yaml:/etc/loki/local-config.yaml:ro
135
+ restart: unless-stopped
136
+ networks:
137
+ - openfactu_net
138
+ `;
139
+ }
140
+ if (serviceSet.has('promtail')) {
141
+ compose += `
142
+ promtail:
143
+ image: grafana/promtail:latest
144
+ container_name: openfactu-promtail
145
+ command: -config.file=/etc/promtail/config.yml
146
+ volumes:
147
+ - ./monitoring/promtail/promtail-config.yaml:/etc/promtail/config.yml:ro
148
+ - /var/log:/var/log
149
+ - ./storage:/app/storage:ro
150
+ ${dependsOn(serviceSet, ['loki'])} restart: unless-stopped
151
+ networks:
152
+ - openfactu_net
153
+ `;
154
+ }
155
+ if (serviceSet.has('cadvisor')) {
156
+ compose += `
157
+ cadvisor:
158
+ image: gcr.io/cadvisor/cadvisor:latest
159
+ container_name: openfactu-cadvisor
160
+ ports:
161
+ - "\${CADVISOR_PORT:-8081}:8080"
162
+ volumes:
163
+ - /:/rootfs:ro
164
+ - /var/run:/var/run:ro
165
+ - /sys:/sys:ro
166
+ - /var/lib/docker/:/var/lib/docker:ro
167
+ - /dev/disk/:/dev/disk:ro
168
+ devices:
169
+ - /dev/kmsg
170
+ restart: unless-stopped
171
+ networks:
172
+ - openfactu_net
173
+ `;
174
+ }
175
+ if (serviceSet.has('node-exporter')) {
176
+ compose += `
177
+ node-exporter:
178
+ image: prom/node-exporter:latest
179
+ container_name: openfactu-node-exporter
180
+ command:
181
+ - '--path.rootfs=/host'
182
+ ports:
183
+ - "\${NODE_EXPORTER_PORT:-9100}:9100"
184
+ volumes:
185
+ - /:/host:ro,rslave
186
+ restart: unless-stopped
187
+ networks:
188
+ - openfactu_net
189
+ `;
190
+ }
191
+ if (serviceSet.has('portainer')) {
192
+ compose += `
193
+ portainer:
194
+ image: portainer/portainer-ce:latest
195
+ container_name: openfactu-portainer
196
+ command: -H unix:///var/run/docker.sock
197
+ ports:
198
+ - "\${PORTAINER_PORT:-9000}:9000"
199
+ volumes:
200
+ - /var/run/docker.sock:/var/run/docker.sock:ro
201
+ - ./storage/portainer_data:/data
202
+ restart: unless-stopped
203
+ networks:
204
+ - openfactu_net
205
+ `;
206
+ }
207
+ if (serviceSet.has('alertmanager')) {
208
+ compose += `
209
+ alertmanager:
210
+ image: prom/alertmanager:latest
211
+ container_name: openfactu-alertmanager
212
+ command:
213
+ - '--config.file=/etc/alertmanager/alertmanager.yml'
214
+ - '--storage.path=/alertmanager'
215
+ ports:
216
+ - "\${ALERTMANAGER_PORT:-9093}:9093"
217
+ volumes:
218
+ - ./monitoring/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
219
+ - ./storage/alertmanager_data:/alertmanager
220
+ restart: unless-stopped
221
+ networks:
222
+ - openfactu_net
223
+ `;
224
+ }
225
+ compose += `
226
+ networks:
227
+ openfactu_net:
228
+ name: openfactu_net
229
+ driver: bridge
230
+ `;
231
+ return compose;
232
+ }
233
+ function generatePrometheusConfig() {
234
+ return `global:
235
+ scrape_interval: 15s
236
+ evaluation_interval: 15s
237
+
238
+ scrape_configs:
239
+ - job_name: 'prometheus'
240
+ static_configs:
241
+ - targets: ['localhost:9090']
242
+
243
+ - job_name: 'node-exporter'
244
+ static_configs:
245
+ - targets: ['node-exporter:9100']
246
+
247
+ - job_name: 'cadvisor'
248
+ static_configs:
249
+ - targets: ['cadvisor:8080']
250
+
251
+ - job_name: 'openfactu-server'
252
+ metrics_path: '/api/metrics'
253
+ static_configs:
254
+ - targets: ['server:3000']
255
+ `;
256
+ }
257
+ function generateLokiConfig() {
258
+ return `auth_enabled: false
259
+
260
+ server:
261
+ http_listen_port: 3100
262
+
263
+ common:
264
+ path_prefix: /loki
265
+ storage:
266
+ filesystem:
267
+ chunks_directory: /loki/chunks
268
+ rules_directory: /loki/rules
269
+ replication_factor: 1
270
+ ring:
271
+ kvstore:
272
+ store: inmemory
273
+
274
+ schema_config:
275
+ configs:
276
+ - from: 2020-10-24
277
+ store: boltdb-shipper
278
+ object_store: filesystem
279
+ schema: v11
280
+ index:
281
+ prefix: index_
282
+ period: 24h
283
+
284
+ limits_config:
285
+ reject_old_samples: true
286
+ reject_old_samples_max_age: 168h
287
+ `;
288
+ }
289
+ function generatePromtailConfig() {
290
+ return `server:
291
+ http_listen_port: 9080
292
+ grpc_listen_port: 0
293
+
294
+ positions:
295
+ filename: /tmp/positions.yaml
296
+
297
+ clients:
298
+ - url: http://loki:3100/loki/api/v1/push
299
+
300
+ scrape_configs:
301
+ - job_name: openfactu-logs
302
+ static_configs:
303
+ - targets:
304
+ - localhost
305
+ labels:
306
+ job: openfactu
307
+ __path__: /app/storage/**/*.log
308
+ `;
309
+ }
310
+ function generateAlertmanagerConfig() {
311
+ return `route:
312
+ receiver: 'default'
313
+ group_by: ['alertname']
314
+ group_wait: 30s
315
+ group_interval: 5m
316
+ repeat_interval: 3h
317
+
318
+ receivers:
319
+ - name: 'default'
320
+ `;
321
+ }
322
+ /**
323
+ * Escribe los archivos de configuración necesarios para los servicios
324
+ * seleccionados. Cada servicio que monta un archivo de config debe generarlo
325
+ * aquí; si no, Docker crea un directorio en su lugar y el bind-mount falla.
326
+ */
327
+ function writeMonitoringConfigs(root, serviceSet) {
328
+ if (serviceSet.has('prometheus')) {
329
+ const dir = path_1.default.join(root, 'monitoring/prometheus');
330
+ fs_1.default.mkdirSync(dir, { recursive: true });
331
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'prometheus.yml'), generatePrometheusConfig());
332
+ }
333
+ if (serviceSet.has('loki')) {
334
+ const dir = path_1.default.join(root, 'monitoring/loki');
335
+ fs_1.default.mkdirSync(dir, { recursive: true });
336
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'loki-config.yaml'), generateLokiConfig());
337
+ }
338
+ if (serviceSet.has('promtail')) {
339
+ const dir = path_1.default.join(root, 'monitoring/promtail');
340
+ fs_1.default.mkdirSync(dir, { recursive: true });
341
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'promtail-config.yaml'), generatePromtailConfig());
342
+ }
343
+ if (serviceSet.has('alertmanager')) {
344
+ const dir = path_1.default.join(root, 'monitoring/alertmanager');
345
+ fs_1.default.mkdirSync(dir, { recursive: true });
346
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'alertmanager.yml'), generateAlertmanagerConfig());
347
+ }
348
+ if (serviceSet.has('grafana')) {
349
+ // El servicio monta ./monitoring/grafana/provisioning (directorio :ro).
350
+ // Lo creamos vacío para que el mount tenga una fuente real.
351
+ fs_1.default.mkdirSync(path_1.default.join(root, 'monitoring/grafana/provisioning'), { recursive: true });
352
+ }
353
+ }
@@ -16,3 +16,4 @@ export declare function getServerSrcDir(): string;
16
16
  export declare function getMigrationsDir(): string;
17
17
  export declare function getPluginsDir(): string;
18
18
  export declare function getEnvPath(): string;
19
+ export declare function getMonitoringComposePath(): string;
@@ -10,6 +10,7 @@ exports.getServerSrcDir = getServerSrcDir;
10
10
  exports.getMigrationsDir = getMigrationsDir;
11
11
  exports.getPluginsDir = getPluginsDir;
12
12
  exports.getEnvPath = getEnvPath;
13
+ exports.getMonitoringComposePath = getMonitoringComposePath;
13
14
  const path_1 = __importDefault(require("path"));
14
15
  const fs_1 = __importDefault(require("fs"));
15
16
  let _projectRoot = null;
@@ -82,3 +83,4 @@ function getServerSrcDir() { return path_1.default.join(getServerDir(), 'src');
82
83
  function getMigrationsDir() { return path_1.default.join(getServerSrcDir(), 'core/tenant/migrations'); }
83
84
  function getPluginsDir() { return path_1.default.join(getProjectRoot(), 'plugins'); }
84
85
  function getEnvPath() { return path_1.default.join(getProjectRoot(), '.env'); }
86
+ function getMonitoringComposePath() { return path_1.default.join(getProjectRoot(), 'docker-compose.monitoring.yml'); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfactu/cli",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "CLI para gestionar OpenFactu: migraciones, tenants, plugins y setup",
5
5
  "main": "./dist/src/index.js",
6
6
  "types": "./dist/src/index.d.ts",
@@ -16,10 +16,12 @@
16
16
  },
17
17
  "scripts": {
18
18
  "build": "tsc",
19
- "dev": "ts-node bin/openfactu.ts"
19
+ "dev": "ts-node bin/openfactu.ts",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "test:coverage": "vitest run --coverage"
20
23
  },
21
24
  "dependencies": {
22
- "bcrypt": "^5.1.1",
23
25
  "chalk": "^4.1.2",
24
26
  "cli-table3": "^0.6.5",
25
27
  "commander": "^12.0.0",
@@ -31,10 +33,11 @@
31
33
  "pg": "^8.13.0"
32
34
  },
33
35
  "devDependencies": {
34
- "@types/bcrypt": "^5.0.0",
35
36
  "@types/inquirer": "^8.2.0",
36
37
  "@types/pg": "^8.11.0",
38
+ "@vitest/coverage-v8": "^4.1.6",
37
39
  "ts-node": "^10.9.0",
38
- "typescript": "^5.3.3"
40
+ "typescript": "^5.3.3",
41
+ "vitest": "^4.1.6"
39
42
  }
40
43
  }