@openfactu/cli 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -58
- package/dist/src/commands/deploy.js +125 -3
- package/dist/src/commands/plugin.js +610 -12
- package/dist/src/utils/db.js +21 -1
- package/package.json +11 -10
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @openfactu/cli
|
|
2
2
|
|
|
3
|
-
CLI oficial para instalar, gestionar y desplegar [OpenFactu](https://github.com/AngelAcedo12/OpenFactu)
|
|
3
|
+
CLI oficial para instalar, gestionar y desplegar [OpenFactu](https://github.com/AngelAcedo12/OpenFactu) — ERP de facturacion open source.
|
|
4
4
|
|
|
5
5
|
## Instalacion
|
|
6
6
|
|
|
@@ -11,17 +11,9 @@ npm i -g @openfactu/cli
|
|
|
11
11
|
## Inicio rapido
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
#
|
|
15
|
-
openfactu
|
|
16
|
-
|
|
17
|
-
# Configurar base de datos y usuario admin
|
|
18
|
-
openfactu setup
|
|
19
|
-
|
|
20
|
-
# Aplicar migraciones
|
|
21
|
-
openfactu migrate
|
|
22
|
-
|
|
23
|
-
# Desplegar en red local o internet
|
|
24
|
-
openfactu deploy
|
|
14
|
+
openfactu install # Descarga e instala OpenFactu
|
|
15
|
+
openfactu deploy # Configura acceso externo
|
|
16
|
+
openfactu setup # Configuracion inicial de BD
|
|
25
17
|
```
|
|
26
18
|
|
|
27
19
|
## Comandos
|
|
@@ -30,81 +22,76 @@ openfactu deploy
|
|
|
30
22
|
|
|
31
23
|
| Comando | Descripcion |
|
|
32
24
|
|---------|-------------|
|
|
33
|
-
| `openfactu install [dir]` | Descarga
|
|
34
|
-
| `openfactu update` | Actualiza
|
|
35
|
-
| `openfactu update:check` | Comprueba si hay versiones nuevas
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
# Instalar una release especifica
|
|
39
|
-
openfactu install ./mi-erp --tag v1.2.0
|
|
40
|
-
|
|
41
|
-
# Instalar desde una branch
|
|
42
|
-
openfactu install ./mi-erp --branch develop
|
|
43
|
-
|
|
44
|
-
# Actualizar la instalacion actual
|
|
45
|
-
openfactu update
|
|
46
|
-
```
|
|
25
|
+
| `openfactu install [dir]` | Descarga desde releases de GitHub con Docker |
|
|
26
|
+
| `openfactu update` | Actualiza sin perder datos |
|
|
27
|
+
| `openfactu update:check` | Comprueba si hay versiones nuevas |
|
|
47
28
|
|
|
48
29
|
### Despliegue
|
|
49
30
|
|
|
50
31
|
| Comando | Descripcion |
|
|
51
32
|
|---------|-------------|
|
|
52
|
-
| `openfactu deploy` | Wizard para configurar acceso externo
|
|
53
|
-
| `openfactu deploy:status` |
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
openfactu
|
|
58
|
-
|
|
59
|
-
# Ver estado de los servicios
|
|
60
|
-
openfactu deploy:status
|
|
61
|
-
```
|
|
33
|
+
| `openfactu deploy` | Wizard para configurar acceso externo (LAN/internet) |
|
|
34
|
+
| `openfactu deploy:status` | Estado de los contenedores Docker |
|
|
35
|
+
| `openfactu rebuild` | Reconstruye y reinicia contenedores |
|
|
36
|
+
| `openfactu logs` | Muestra logs de los servicios |
|
|
37
|
+
| `openfactu stop` | Para todos los servicios |
|
|
38
|
+
| `openfactu restart` | Reinicia sin rebuild |
|
|
62
39
|
|
|
63
40
|
### Base de datos
|
|
64
41
|
|
|
65
42
|
| Comando | Descripcion |
|
|
66
43
|
|---------|-------------|
|
|
67
|
-
| `openfactu setup` | Configuracion inicial:
|
|
68
|
-
| `openfactu migrate` | Ejecuta migraciones pendientes
|
|
69
|
-
| `openfactu migrate:status` |
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
# Migrar solo un tenant especifico
|
|
73
|
-
openfactu migrate --tenant "Mi Empresa"
|
|
74
|
-
|
|
75
|
-
# Ver que migraciones faltan
|
|
76
|
-
openfactu migrate:status
|
|
77
|
-
```
|
|
44
|
+
| `openfactu setup` | Configuracion inicial: BD, admin, primer tenant |
|
|
45
|
+
| `openfactu migrate` | Ejecuta migraciones pendientes |
|
|
46
|
+
| `openfactu migrate:status` | Estado de migraciones por tenant |
|
|
78
47
|
|
|
79
48
|
### Tenants (empresas)
|
|
80
49
|
|
|
81
50
|
| Comando | Descripcion |
|
|
82
51
|
|---------|-------------|
|
|
83
|
-
| `openfactu tenant list` | Lista
|
|
84
|
-
| `openfactu tenant create
|
|
85
|
-
| `openfactu tenant sync
|
|
52
|
+
| `openfactu tenant list` | Lista empresas |
|
|
53
|
+
| `openfactu tenant create` | Crea una empresa nueva |
|
|
54
|
+
| `openfactu tenant sync` | Sincroniza migraciones |
|
|
86
55
|
|
|
87
56
|
### Plugins
|
|
88
57
|
|
|
89
58
|
| Comando | Descripcion |
|
|
90
59
|
|---------|-------------|
|
|
91
|
-
| `openfactu plugin list` | Lista plugins instalados con
|
|
60
|
+
| `openfactu plugin list` | Lista plugins instalados con estado por tenant |
|
|
61
|
+
| `openfactu plugin search` | Busca en el marketplace (interactivo) |
|
|
62
|
+
| `openfactu plugin install <nombre>` | Descarga e instala del marketplace |
|
|
63
|
+
| `openfactu plugin update [nombre]` | Actualiza uno o todos |
|
|
64
|
+
| `openfactu plugin remove <nombre>` | Elimina un plugin |
|
|
65
|
+
| `openfactu plugin link [dir]` | Enlaza un plugin externo (symlink) |
|
|
66
|
+
| `openfactu plugin unlink <nombre>` | Quita el enlace |
|
|
67
|
+
| `openfactu plugin push [dir]` | Sube un plugin a un servidor remoto |
|
|
68
|
+
| `openfactu plugin watch [dir]` | Auto-sync al guardar (desarrollo remoto) |
|
|
69
|
+
| `openfactu plugin dev [nombre]` | Servidor en modo desarrollo con hot reload |
|
|
92
70
|
|
|
93
71
|
### Otros
|
|
94
72
|
|
|
95
73
|
| Comando | Descripcion |
|
|
96
74
|
|---------|-------------|
|
|
97
|
-
| `openfactu version` |
|
|
75
|
+
| `openfactu version` | Versiones del sistema |
|
|
98
76
|
|
|
99
|
-
##
|
|
77
|
+
## Desarrollo remoto de plugins
|
|
100
78
|
|
|
101
|
-
|
|
79
|
+
```bash
|
|
80
|
+
# Desde otro ordenador, sube tu plugin automaticamente al guardar
|
|
81
|
+
openfactu plugin watch \
|
|
82
|
+
--server http://mi-servidor:3000 \
|
|
83
|
+
--client-id ofk_... \
|
|
84
|
+
--client-secret ofs_...
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Las dev keys se generan desde la UI del ERP: Plugins > Desarrollo > Generar API Key.
|
|
88
|
+
|
|
89
|
+
## Uso desde cualquier directorio
|
|
102
90
|
|
|
103
91
|
```bash
|
|
104
|
-
# Opcion 1: flag --path
|
|
105
92
|
openfactu --path /ruta/a/openfactu migrate
|
|
106
93
|
|
|
107
|
-
#
|
|
94
|
+
# o con variable de entorno
|
|
108
95
|
export OPENFACTU_HOME=/ruta/a/openfactu
|
|
109
96
|
openfactu migrate
|
|
110
97
|
```
|
|
@@ -113,9 +100,11 @@ openfactu migrate
|
|
|
113
100
|
|
|
114
101
|
- Node.js >= 18
|
|
115
102
|
- Docker Desktop (para instalar y desplegar)
|
|
116
|
-
- Git
|
|
103
|
+
- Git
|
|
117
104
|
|
|
118
105
|
## Links
|
|
119
106
|
|
|
120
107
|
- [GitHub](https://github.com/AngelAcedo12/OpenFactu)
|
|
121
|
-
- [
|
|
108
|
+
- [Documentacion](https://openfactuerp.org)
|
|
109
|
+
- [Marketplace](https://openfactuerp.org/marketplace/)
|
|
110
|
+
- [Reportar problema](https://github.com/AngelAcedo12/OpenFactu/issues)
|
|
@@ -189,9 +189,7 @@ function registerDeployCommand(program) {
|
|
|
189
189
|
// 5. Generar docker-compose.prod.yml con bind a 0.0.0.0
|
|
190
190
|
const prodComposePath = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
191
191
|
const prodSpinner = (0, ora_1.default)('Generando docker-compose.prod.yml...').start();
|
|
192
|
-
let composeContent = `
|
|
193
|
-
|
|
194
|
-
services:
|
|
192
|
+
let composeContent = `services:
|
|
195
193
|
web:
|
|
196
194
|
build:
|
|
197
195
|
context: .
|
|
@@ -367,4 +365,128 @@ networks:
|
|
|
367
365
|
logger_1.log.dim(' ' + err.message);
|
|
368
366
|
}
|
|
369
367
|
});
|
|
368
|
+
// ── openfactu rebuild ──
|
|
369
|
+
program
|
|
370
|
+
.command('rebuild')
|
|
371
|
+
.description('Reconstruye y reinicia los contenedores Docker')
|
|
372
|
+
.option('--service <name>', 'Reconstruir solo un servicio (web, server, db)')
|
|
373
|
+
.option('--no-cache', 'Construir sin cache de Docker')
|
|
374
|
+
.action(async (opts) => {
|
|
375
|
+
try {
|
|
376
|
+
const root = (0, paths_1.getProjectRoot)();
|
|
377
|
+
const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
378
|
+
const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
|
|
379
|
+
const service = opts.service || '';
|
|
380
|
+
const noCache = opts.cache === false ? ' --no-cache' : '';
|
|
381
|
+
logger_1.log.info(`Usando: ${chalk_1.default.dim(composeFile)}`);
|
|
382
|
+
logger_1.log.blank();
|
|
383
|
+
const buildSpinner = (0, ora_1.default)(`Construyendo${service ? ' ' + service : ' todos los servicios'}...`).start();
|
|
384
|
+
try {
|
|
385
|
+
(0, child_process_1.execSync)(`docker compose -f ${composeFile} build${noCache} ${service}`, {
|
|
386
|
+
cwd: root,
|
|
387
|
+
stdio: 'pipe',
|
|
388
|
+
timeout: 600000,
|
|
389
|
+
});
|
|
390
|
+
buildSpinner.succeed('Build completado');
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
buildSpinner.fail('Error en el build');
|
|
394
|
+
// Mostrar output del error
|
|
395
|
+
const output = err.stdout?.toString() || err.stderr?.toString() || err.message;
|
|
396
|
+
const errorLines = output.split('\n').filter((l) => l.includes('error') || l.includes('Error') || l.includes('>>>'));
|
|
397
|
+
if (errorLines.length > 0) {
|
|
398
|
+
logger_1.log.blank();
|
|
399
|
+
for (const line of errorLines.slice(0, 10)) {
|
|
400
|
+
logger_1.log.error(line.trim());
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const upSpinner = (0, ora_1.default)('Reiniciando servicios...').start();
|
|
406
|
+
try {
|
|
407
|
+
(0, child_process_1.execSync)(`docker compose -f ${composeFile} up -d ${service}`, {
|
|
408
|
+
cwd: root,
|
|
409
|
+
stdio: 'pipe',
|
|
410
|
+
timeout: 60000,
|
|
411
|
+
});
|
|
412
|
+
upSpinner.succeed('Servicios levantados');
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
upSpinner.fail('Error al levantar: ' + err.message);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
logger_1.log.blank();
|
|
419
|
+
logger_1.log.success('Rebuild completado');
|
|
420
|
+
// Mostrar URLs
|
|
421
|
+
const envPath = path_1.default.join(root, '.env');
|
|
422
|
+
const env = readEnv(envPath);
|
|
423
|
+
const host = env.HOST || 'localhost';
|
|
424
|
+
const webPort = env.WEB_PORT || '8080';
|
|
425
|
+
const serverPort = env.SERVER_PORT || '3000';
|
|
426
|
+
logger_1.log.info(`Web: ${chalk_1.default.cyan(`http://${host}:${webPort}`)}`);
|
|
427
|
+
logger_1.log.info(`API: ${chalk_1.default.cyan(`http://${host}:${serverPort}`)}`);
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
logger_1.log.error(err.message);
|
|
431
|
+
process.exitCode = 1;
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
// ── openfactu logs ──
|
|
435
|
+
program
|
|
436
|
+
.command('logs')
|
|
437
|
+
.description('Muestra los logs de los servicios Docker')
|
|
438
|
+
.option('--service <name>', 'Logs de un servicio especifico (web, server, db)')
|
|
439
|
+
.option('-n, --lines <number>', 'Numero de lineas', '50')
|
|
440
|
+
.action(async (opts) => {
|
|
441
|
+
try {
|
|
442
|
+
const root = (0, paths_1.getProjectRoot)();
|
|
443
|
+
const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
444
|
+
const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
|
|
445
|
+
const service = opts.service || '';
|
|
446
|
+
const lines = opts.lines || '50';
|
|
447
|
+
(0, child_process_1.execSync)(`docker compose -f ${composeFile} logs --tail ${lines} ${service}`, {
|
|
448
|
+
cwd: root,
|
|
449
|
+
stdio: 'inherit',
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
logger_1.log.error(err.message);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
// ── openfactu stop ──
|
|
457
|
+
program
|
|
458
|
+
.command('stop')
|
|
459
|
+
.description('Para todos los servicios Docker')
|
|
460
|
+
.action(async () => {
|
|
461
|
+
try {
|
|
462
|
+
const root = (0, paths_1.getProjectRoot)();
|
|
463
|
+
const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
464
|
+
const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
|
|
465
|
+
const spinner = (0, ora_1.default)('Parando servicios...').start();
|
|
466
|
+
(0, child_process_1.execSync)(`docker compose -f ${composeFile} down`, { cwd: root, stdio: 'pipe' });
|
|
467
|
+
spinner.succeed('Servicios parados');
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
logger_1.log.error(err.message);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
// ── openfactu restart ──
|
|
474
|
+
program
|
|
475
|
+
.command('restart')
|
|
476
|
+
.description('Reinicia los servicios Docker (sin rebuild)')
|
|
477
|
+
.option('--service <name>', 'Reiniciar solo un servicio')
|
|
478
|
+
.action(async (opts) => {
|
|
479
|
+
try {
|
|
480
|
+
const root = (0, paths_1.getProjectRoot)();
|
|
481
|
+
const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
482
|
+
const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
|
|
483
|
+
const service = opts.service || '';
|
|
484
|
+
const spinner = (0, ora_1.default)('Reiniciando...').start();
|
|
485
|
+
(0, child_process_1.execSync)(`docker compose -f ${composeFile} restart ${service}`, { cwd: root, stdio: 'pipe' });
|
|
486
|
+
spinner.succeed('Servicios reiniciados');
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
logger_1.log.error(err.message);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
370
492
|
}
|
|
@@ -7,15 +7,37 @@ exports.registerPluginCommand = registerPluginCommand;
|
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
8
|
const ora_1 = __importDefault(require("ora"));
|
|
9
9
|
const cli_table3_1 = __importDefault(require("cli-table3"));
|
|
10
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const https_1 = __importDefault(require("https"));
|
|
10
13
|
const fs_1 = __importDefault(require("fs"));
|
|
11
14
|
const path_1 = __importDefault(require("path"));
|
|
12
15
|
const db_1 = require("../utils/db");
|
|
13
16
|
const logger_1 = require("../utils/logger");
|
|
14
17
|
const paths_1 = require("../utils/paths");
|
|
18
|
+
// Registrar el plugin de autocomplete
|
|
19
|
+
const AutocompletePrompt = require('inquirer-autocomplete-prompt');
|
|
20
|
+
inquirer_1.default.registerPrompt('autocomplete', AutocompletePrompt);
|
|
21
|
+
function fetchJSON(url) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
https_1.default.get(url, { headers: { 'User-Agent': 'openfactu-cli' } }, (res) => {
|
|
24
|
+
let data = '';
|
|
25
|
+
res.on('data', (chunk) => (data += chunk));
|
|
26
|
+
res.on('end', () => {
|
|
27
|
+
try {
|
|
28
|
+
resolve(JSON.parse(data));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
reject(new Error('Respuesta no valida'));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}).on('error', reject);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
15
37
|
function registerPluginCommand(program) {
|
|
16
38
|
const plugin = program
|
|
17
39
|
.command('plugin')
|
|
18
|
-
.description('
|
|
40
|
+
.description('Gestion de plugins');
|
|
19
41
|
// ── openfactu plugin list ──
|
|
20
42
|
plugin
|
|
21
43
|
.command('list')
|
|
@@ -23,7 +45,6 @@ function registerPluginCommand(program) {
|
|
|
23
45
|
.action(async () => {
|
|
24
46
|
const spinner = (0, ora_1.default)('Leyendo plugins...').start();
|
|
25
47
|
try {
|
|
26
|
-
// Leer plugins del filesystem
|
|
27
48
|
const installed = [];
|
|
28
49
|
if (fs_1.default.existsSync((0, paths_1.getPluginsDir)())) {
|
|
29
50
|
const dirs = fs_1.default.readdirSync((0, paths_1.getPluginsDir)()).filter((d) => fs_1.default.statSync(path_1.default.join((0, paths_1.getPluginsDir)(), d)).isDirectory());
|
|
@@ -33,15 +54,12 @@ function registerPluginCommand(program) {
|
|
|
33
54
|
spinner.warn('No hay plugins instalados');
|
|
34
55
|
return;
|
|
35
56
|
}
|
|
36
|
-
// Leer estado de activación por tenant
|
|
37
57
|
const publicDb = (0, db_1.getPublicDb)();
|
|
38
58
|
let tenantPlugins = [];
|
|
39
59
|
try {
|
|
40
60
|
tenantPlugins = await publicDb.select().from((0, db_1.schema)().tenantPlugins);
|
|
41
61
|
}
|
|
42
|
-
catch {
|
|
43
|
-
// Tabla puede no existir aún
|
|
44
|
-
}
|
|
62
|
+
catch { }
|
|
45
63
|
const tenants = await (0, db_1.getAllTenants)();
|
|
46
64
|
spinner.succeed(`${installed.length} plugin(s) instalado(s)`);
|
|
47
65
|
logger_1.log.blank();
|
|
@@ -69,15 +87,12 @@ function registerPluginCommand(program) {
|
|
|
69
87
|
];
|
|
70
88
|
for (const tenant of tenants) {
|
|
71
89
|
const tp = tenantPlugins.find((r) => r.tenantId === tenant.id && r.pluginId === pluginId);
|
|
72
|
-
if (tp?.isActive)
|
|
90
|
+
if (tp?.isActive)
|
|
73
91
|
row.push(chalk_1.default.green('Activo'));
|
|
74
|
-
|
|
75
|
-
else if (tp) {
|
|
92
|
+
else if (tp)
|
|
76
93
|
row.push(chalk_1.default.dim('Inactivo'));
|
|
77
|
-
|
|
78
|
-
else {
|
|
94
|
+
else
|
|
79
95
|
row.push(chalk_1.default.dim('-'));
|
|
80
|
-
}
|
|
81
96
|
}
|
|
82
97
|
table.push(row);
|
|
83
98
|
}
|
|
@@ -91,4 +106,587 @@ function registerPluginCommand(program) {
|
|
|
91
106
|
await (0, db_1.disconnect)();
|
|
92
107
|
}
|
|
93
108
|
});
|
|
109
|
+
// ── openfactu plugin search ──
|
|
110
|
+
plugin
|
|
111
|
+
.command('search [query]')
|
|
112
|
+
.description('Busca plugins en el marketplace (interactivo)')
|
|
113
|
+
.action(async (query) => {
|
|
114
|
+
const spinner = (0, ora_1.default)('Cargando marketplace...').start();
|
|
115
|
+
let repos = [];
|
|
116
|
+
try {
|
|
117
|
+
const data = await fetchJSON('https://api.github.com/search/repositories?q=topic:openfactu-plugin&sort=stars&order=desc&per_page=50');
|
|
118
|
+
repos = data.items || [];
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
spinner.fail('No se pudo conectar al marketplace: ' + err.message);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (repos.length === 0) {
|
|
125
|
+
spinner.warn('No hay plugins en el marketplace');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const installedDirs = fs_1.default.existsSync((0, paths_1.getPluginsDir)())
|
|
129
|
+
? fs_1.default.readdirSync((0, paths_1.getPluginsDir)()).filter((d) => fs_1.default.statSync(path_1.default.join((0, paths_1.getPluginsDir)(), d)).isDirectory())
|
|
130
|
+
: [];
|
|
131
|
+
spinner.succeed(`${repos.length} plugin(s) en el marketplace`);
|
|
132
|
+
logger_1.log.blank();
|
|
133
|
+
const { selected } = await inquirer_1.default.prompt([
|
|
134
|
+
{
|
|
135
|
+
type: 'autocomplete',
|
|
136
|
+
name: 'selected',
|
|
137
|
+
message: 'Buscar plugin:',
|
|
138
|
+
source: (_answers, input) => {
|
|
139
|
+
const q = (input || '').toLowerCase();
|
|
140
|
+
return repos
|
|
141
|
+
.filter((r) => !q || r.name.toLowerCase().includes(q) || (r.description || '').toLowerCase().includes(q))
|
|
142
|
+
.map((r) => {
|
|
143
|
+
const installed = installedDirs.includes(r.name);
|
|
144
|
+
const status = installed ? chalk_1.default.green(' [instalado]') : '';
|
|
145
|
+
const stars = chalk_1.default.yellow(`★${r.stargazers_count}`);
|
|
146
|
+
return {
|
|
147
|
+
name: `${chalk_1.default.bold(r.name)} ${chalk_1.default.dim('por ' + r.owner.login)} ${stars}${status}\n ${chalk_1.default.dim(r.description || 'Sin descripcion')}`,
|
|
148
|
+
value: r,
|
|
149
|
+
short: r.name,
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
pageSize: 10,
|
|
154
|
+
},
|
|
155
|
+
]);
|
|
156
|
+
const r = selected;
|
|
157
|
+
const isInstalled = installedDirs.includes(r.name);
|
|
158
|
+
const topics = (r.topics || []).filter((t) => t !== 'openfactu-plugin');
|
|
159
|
+
logger_1.log.blank();
|
|
160
|
+
console.log(chalk_1.default.bold.white(` ${r.name}`));
|
|
161
|
+
console.log(chalk_1.default.dim(` por ${r.owner.login} · ★ ${r.stargazers_count} · ${r.language || 'TypeScript'}`));
|
|
162
|
+
if (r.description)
|
|
163
|
+
console.log(` ${r.description}`);
|
|
164
|
+
if (topics.length > 0)
|
|
165
|
+
console.log(chalk_1.default.dim(` Tags: ${topics.join(', ')}`));
|
|
166
|
+
console.log(chalk_1.default.dim(` ${r.html_url}`));
|
|
167
|
+
logger_1.log.blank();
|
|
168
|
+
if (isInstalled) {
|
|
169
|
+
logger_1.log.success('Este plugin ya esta instalado');
|
|
170
|
+
const { action } = await inquirer_1.default.prompt([
|
|
171
|
+
{
|
|
172
|
+
type: 'list',
|
|
173
|
+
name: 'action',
|
|
174
|
+
message: 'Que quieres hacer?',
|
|
175
|
+
choices: [
|
|
176
|
+
{ name: 'Actualizar', value: 'update' },
|
|
177
|
+
{ name: 'Eliminar', value: 'remove' },
|
|
178
|
+
{ name: 'Nada', value: 'none' },
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
]);
|
|
182
|
+
if (action === 'update') {
|
|
183
|
+
const upSpinner = (0, ora_1.default)('Actualizando...').start();
|
|
184
|
+
try {
|
|
185
|
+
(0, child_process_1.execSync)('git pull --ff-only', { cwd: path_1.default.join((0, paths_1.getPluginsDir)(), r.name), stdio: 'pipe' });
|
|
186
|
+
upSpinner.succeed('Plugin actualizado');
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
upSpinner.warn('No se pudo actualizar');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (action === 'remove') {
|
|
193
|
+
fs_1.default.rmSync(path_1.default.join((0, paths_1.getPluginsDir)(), r.name), { recursive: true, force: true });
|
|
194
|
+
logger_1.log.success('Plugin eliminado');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const { install } = await inquirer_1.default.prompt([
|
|
199
|
+
{ type: 'confirm', name: 'install', message: 'Instalar este plugin?', default: true },
|
|
200
|
+
]);
|
|
201
|
+
if (install) {
|
|
202
|
+
const pluginsDir = (0, paths_1.getPluginsDir)();
|
|
203
|
+
if (!fs_1.default.existsSync(pluginsDir))
|
|
204
|
+
fs_1.default.mkdirSync(pluginsDir, { recursive: true });
|
|
205
|
+
const cloneSpinner = (0, ora_1.default)('Descargando...').start();
|
|
206
|
+
try {
|
|
207
|
+
(0, child_process_1.execSync)(`git clone ${r.clone_url} "${path_1.default.join(pluginsDir, r.name)}"`, { stdio: 'pipe', timeout: 60000 });
|
|
208
|
+
cloneSpinner.succeed(`Plugin "${r.name}" instalado`);
|
|
209
|
+
logger_1.log.dim(' Reinicia el servidor para cargarlo.');
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
cloneSpinner.fail('Error: ' + err.message);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
// ── openfactu plugin install ──
|
|
218
|
+
plugin
|
|
219
|
+
.command('install <name>')
|
|
220
|
+
.description('Instala un plugin desde el marketplace')
|
|
221
|
+
.option('--repo <url>', 'URL del repositorio (si no es del marketplace)')
|
|
222
|
+
.action(async (name, opts) => {
|
|
223
|
+
const pluginsDir = (0, paths_1.getPluginsDir)();
|
|
224
|
+
const targetDir = path_1.default.join(pluginsDir, name);
|
|
225
|
+
// Verificar si ya esta instalado
|
|
226
|
+
if (fs_1.default.existsSync(targetDir)) {
|
|
227
|
+
logger_1.log.warn(`El plugin "${name}" ya esta instalado en ${targetDir}`);
|
|
228
|
+
logger_1.log.dim(' Para actualizar: openfactu plugin update ' + name);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
let repoUrl = opts.repo;
|
|
232
|
+
if (!repoUrl) {
|
|
233
|
+
// Buscar en el marketplace
|
|
234
|
+
const spinner = (0, ora_1.default)(`Buscando "${name}" en el marketplace...`).start();
|
|
235
|
+
try {
|
|
236
|
+
const data = await fetchJSON('https://api.github.com/search/repositories?q=topic:openfactu-plugin+' + encodeURIComponent(name) + '&sort=stars&order=desc');
|
|
237
|
+
const match = (data.items || []).find((r) => r.name.toLowerCase() === name.toLowerCase());
|
|
238
|
+
if (!match) {
|
|
239
|
+
// Buscar sin filtro exacto
|
|
240
|
+
const fuzzy = (data.items || []).find((r) => r.name.toLowerCase().includes(name.toLowerCase()));
|
|
241
|
+
if (fuzzy) {
|
|
242
|
+
repoUrl = fuzzy.clone_url;
|
|
243
|
+
spinner.succeed(`Encontrado: ${fuzzy.full_name}`);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
spinner.fail(`Plugin "${name}" no encontrado en el marketplace`);
|
|
247
|
+
logger_1.log.dim(' Usa --repo <url> para instalar desde un repositorio especifico');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
repoUrl = match.clone_url;
|
|
253
|
+
spinner.succeed(`Encontrado: ${match.full_name}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
spinner.fail('Error buscando: ' + err.message);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Crear directorio de plugins si no existe
|
|
262
|
+
if (!fs_1.default.existsSync(pluginsDir)) {
|
|
263
|
+
fs_1.default.mkdirSync(pluginsDir, { recursive: true });
|
|
264
|
+
}
|
|
265
|
+
// Clonar
|
|
266
|
+
const cloneSpinner = (0, ora_1.default)('Descargando plugin...').start();
|
|
267
|
+
try {
|
|
268
|
+
(0, child_process_1.execSync)(`git clone ${repoUrl} "${targetDir}"`, { stdio: 'pipe', timeout: 60000 });
|
|
269
|
+
cloneSpinner.succeed('Plugin descargado');
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
cloneSpinner.fail('Error al descargar: ' + err.message);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// Verificar estructura
|
|
276
|
+
const hasIndex = fs_1.default.existsSync(path_1.default.join(targetDir, 'index.ts')) || fs_1.default.existsSync(path_1.default.join(targetDir, 'index.js'));
|
|
277
|
+
const hasManifest = fs_1.default.existsSync(path_1.default.join(targetDir, 'manifest.json'));
|
|
278
|
+
logger_1.log.blank();
|
|
279
|
+
logger_1.log.success(`Plugin "${name}" instalado en ${targetDir}`);
|
|
280
|
+
logger_1.log.info(`Punto de entrada: ${hasIndex ? chalk_1.default.green('Si') : chalk_1.default.yellow('No encontrado')}`);
|
|
281
|
+
logger_1.log.info(`Manifest: ${hasManifest ? chalk_1.default.green('Si') : chalk_1.default.dim('No')}`);
|
|
282
|
+
logger_1.log.blank();
|
|
283
|
+
logger_1.log.dim(' Reinicia el servidor para cargar el plugin.');
|
|
284
|
+
logger_1.log.dim(' Activa el plugin por empresa desde la UI o API.');
|
|
285
|
+
});
|
|
286
|
+
// ── openfactu plugin update ──
|
|
287
|
+
plugin
|
|
288
|
+
.command('update [name]')
|
|
289
|
+
.description('Actualiza un plugin o todos')
|
|
290
|
+
.action(async (name) => {
|
|
291
|
+
const pluginsDir = (0, paths_1.getPluginsDir)();
|
|
292
|
+
if (!fs_1.default.existsSync(pluginsDir)) {
|
|
293
|
+
logger_1.log.warn('No hay plugins instalados');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const dirs = name
|
|
297
|
+
? [name]
|
|
298
|
+
: fs_1.default.readdirSync(pluginsDir).filter((d) => fs_1.default.statSync(path_1.default.join(pluginsDir, d)).isDirectory());
|
|
299
|
+
let updated = 0;
|
|
300
|
+
for (const dir of dirs) {
|
|
301
|
+
const pluginPath = path_1.default.join(pluginsDir, dir);
|
|
302
|
+
const gitDir = path_1.default.join(pluginPath, '.git');
|
|
303
|
+
if (!fs_1.default.existsSync(gitDir)) {
|
|
304
|
+
logger_1.log.dim(` ${dir} — no es un repositorio git, omitiendo`);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const spinner = (0, ora_1.default)(`Actualizando ${dir}...`).start();
|
|
308
|
+
try {
|
|
309
|
+
(0, child_process_1.execSync)('git pull --ff-only', { cwd: pluginPath, stdio: 'pipe', timeout: 30000 });
|
|
310
|
+
const status = (0, child_process_1.execSync)('git log --oneline -1', { cwd: pluginPath }).toString().trim();
|
|
311
|
+
spinner.succeed(`${dir} — ${status}`);
|
|
312
|
+
updated++;
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
spinner.warn(`${dir} — no se pudo actualizar`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
logger_1.log.blank();
|
|
319
|
+
if (updated > 0) {
|
|
320
|
+
logger_1.log.success(`${updated} plugin(s) actualizado(s)`);
|
|
321
|
+
logger_1.log.dim(' Reinicia el servidor para aplicar los cambios.');
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
logger_1.log.info('No hay actualizaciones');
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
// ── openfactu plugin remove ──
|
|
328
|
+
plugin
|
|
329
|
+
.command('remove <name>')
|
|
330
|
+
.description('Elimina un plugin instalado')
|
|
331
|
+
.action(async (name) => {
|
|
332
|
+
const targetDir = path_1.default.join((0, paths_1.getPluginsDir)(), name);
|
|
333
|
+
if (!fs_1.default.existsSync(targetDir)) {
|
|
334
|
+
logger_1.log.error(`Plugin "${name}" no encontrado`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const spinner = (0, ora_1.default)(`Eliminando ${name}...`).start();
|
|
338
|
+
try {
|
|
339
|
+
fs_1.default.rmSync(targetDir, { recursive: true, force: true });
|
|
340
|
+
spinner.succeed(`Plugin "${name}" eliminado`);
|
|
341
|
+
logger_1.log.dim(' Reinicia el servidor para aplicar los cambios.');
|
|
342
|
+
logger_1.log.dim(' Los datos del plugin (campos, tablas) se mantienen en la BD.');
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
spinner.fail('Error: ' + err.message);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
// ── openfactu plugin link ──
|
|
349
|
+
plugin
|
|
350
|
+
.command('link [dir]')
|
|
351
|
+
.description('Enlaza un plugin externo a la carpeta de plugins de OpenFactu')
|
|
352
|
+
.action(async (dir) => {
|
|
353
|
+
const pluginsDir = (0, paths_1.getPluginsDir)();
|
|
354
|
+
const sourcePath = path_1.default.resolve(dir || process.cwd());
|
|
355
|
+
// Verificar que el directorio existe
|
|
356
|
+
if (!fs_1.default.existsSync(sourcePath)) {
|
|
357
|
+
logger_1.log.error(`Directorio no encontrado: ${sourcePath}`);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// Verificar que tiene index.ts o index.js
|
|
361
|
+
const hasIndex = fs_1.default.existsSync(path_1.default.join(sourcePath, 'index.ts')) || fs_1.default.existsSync(path_1.default.join(sourcePath, 'index.js'));
|
|
362
|
+
if (!hasIndex) {
|
|
363
|
+
logger_1.log.warn('No se encontro index.ts ni index.js en el directorio');
|
|
364
|
+
logger_1.log.dim(' Asegurate de que es un plugin valido de OpenFactu');
|
|
365
|
+
}
|
|
366
|
+
const pluginName = path_1.default.basename(sourcePath);
|
|
367
|
+
const linkPath = path_1.default.join(pluginsDir, pluginName);
|
|
368
|
+
// Verificar si ya existe
|
|
369
|
+
if (fs_1.default.existsSync(linkPath)) {
|
|
370
|
+
const stat = fs_1.default.lstatSync(linkPath);
|
|
371
|
+
if (stat.isSymbolicLink()) {
|
|
372
|
+
logger_1.log.warn(`El enlace "${pluginName}" ya existe → ${fs_1.default.readlinkSync(linkPath)}`);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
logger_1.log.error(`Ya existe un plugin "${pluginName}" (no es un symlink). Eliminalo primero.`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
// Crear directorio de plugins si no existe
|
|
379
|
+
if (!fs_1.default.existsSync(pluginsDir)) {
|
|
380
|
+
fs_1.default.mkdirSync(pluginsDir, { recursive: true });
|
|
381
|
+
}
|
|
382
|
+
// Crear symlink
|
|
383
|
+
try {
|
|
384
|
+
fs_1.default.symlinkSync(sourcePath, linkPath, 'dir');
|
|
385
|
+
logger_1.log.success(`Plugin enlazado: ${chalk_1.default.bold(pluginName)}`);
|
|
386
|
+
logger_1.log.dim(` ${sourcePath} → ${linkPath}`);
|
|
387
|
+
logger_1.log.blank();
|
|
388
|
+
logger_1.log.dim(' Ahora puedes desarrollar el plugin desde su carpeta original.');
|
|
389
|
+
logger_1.log.dim(' Los cambios se detectan automaticamente con el watcher.');
|
|
390
|
+
logger_1.log.dim(' Para arrancar: openfactu plugin dev ' + pluginName);
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
logger_1.log.error('Error al crear enlace: ' + err.message);
|
|
394
|
+
logger_1.log.dim(' En Windows ejecuta como administrador');
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
// ── openfactu plugin unlink ──
|
|
398
|
+
plugin
|
|
399
|
+
.command('unlink <name>')
|
|
400
|
+
.description('Elimina el enlace de un plugin externo')
|
|
401
|
+
.action(async (name) => {
|
|
402
|
+
const linkPath = path_1.default.join((0, paths_1.getPluginsDir)(), name);
|
|
403
|
+
if (!fs_1.default.existsSync(linkPath)) {
|
|
404
|
+
logger_1.log.error(`Plugin "${name}" no encontrado`);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const stat = fs_1.default.lstatSync(linkPath);
|
|
408
|
+
if (!stat.isSymbolicLink()) {
|
|
409
|
+
logger_1.log.error(`"${name}" no es un enlace simbolico. Usa 'plugin remove' para eliminarlo.`);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
fs_1.default.unlinkSync(linkPath);
|
|
413
|
+
logger_1.log.success(`Enlace "${name}" eliminado`);
|
|
414
|
+
logger_1.log.dim(' El directorio original no se ha tocado.');
|
|
415
|
+
});
|
|
416
|
+
// ── openfactu plugin push ──
|
|
417
|
+
plugin
|
|
418
|
+
.command('push [dir]')
|
|
419
|
+
.description('Sube un plugin a un servidor OpenFactu remoto')
|
|
420
|
+
.requiredOption('-s, --server <url>', 'URL del servidor (ej: http://192.168.1.100:3000)')
|
|
421
|
+
.option('-t, --token <token>', 'Token JWT de admin')
|
|
422
|
+
.option('--client-id <id>', 'Client ID de la dev key (ej: ofk_...)')
|
|
423
|
+
.option('--client-secret <secret>', 'Client Secret de la dev key (ej: ofs_...)')
|
|
424
|
+
.action(async (dir, opts) => {
|
|
425
|
+
// Validar autenticacion
|
|
426
|
+
if (!opts.token && (!opts.clientId || !opts.clientSecret)) {
|
|
427
|
+
logger_1.log.error('Necesitas autenticarte con --token o --client-id + --client-secret');
|
|
428
|
+
logger_1.log.dim(' Genera una dev key desde la UI: Plugins → Desarrollo → Generar API Key');
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const sourcePath = path_1.default.resolve(dir || process.cwd());
|
|
432
|
+
if (!fs_1.default.existsSync(sourcePath)) {
|
|
433
|
+
logger_1.log.error(`Directorio no encontrado: ${sourcePath}`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const pluginName = path_1.default.basename(sourcePath);
|
|
437
|
+
const hasIndex = fs_1.default.existsSync(path_1.default.join(sourcePath, 'index.ts')) || fs_1.default.existsSync(path_1.default.join(sourcePath, 'index.js'));
|
|
438
|
+
if (!hasIndex) {
|
|
439
|
+
logger_1.log.warn('No se encontro index.ts ni index.js. Seguro que es un plugin?');
|
|
440
|
+
}
|
|
441
|
+
logger_1.log.info(`Plugin: ${chalk_1.default.bold(pluginName)}`);
|
|
442
|
+
logger_1.log.info(`Servidor: ${chalk_1.default.dim(opts.server)}`);
|
|
443
|
+
logger_1.log.blank();
|
|
444
|
+
// Recoger todos los archivos del plugin
|
|
445
|
+
const spinner = (0, ora_1.default)('Leyendo archivos...').start();
|
|
446
|
+
const files = [];
|
|
447
|
+
function readDir(dirPath, basePath) {
|
|
448
|
+
const entries = fs_1.default.readdirSync(dirPath);
|
|
449
|
+
for (const entry of entries) {
|
|
450
|
+
if (entry === 'node_modules' || entry === '.git' || entry === 'dist')
|
|
451
|
+
continue;
|
|
452
|
+
const fullPath = path_1.default.join(dirPath, entry);
|
|
453
|
+
const relativePath = path_1.default.relative(basePath, fullPath);
|
|
454
|
+
const stat = fs_1.default.statSync(fullPath);
|
|
455
|
+
if (stat.isDirectory()) {
|
|
456
|
+
readDir(fullPath, basePath);
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
files.push({
|
|
460
|
+
path: relativePath,
|
|
461
|
+
content: fs_1.default.readFileSync(fullPath).toString('base64'),
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
readDir(sourcePath, sourcePath);
|
|
467
|
+
spinner.succeed(`${files.length} archivo(s) encontrado(s)`);
|
|
468
|
+
// Enviar al servidor
|
|
469
|
+
const pushSpinner = (0, ora_1.default)('Subiendo al servidor...').start();
|
|
470
|
+
try {
|
|
471
|
+
const url = `${opts.server}/api/plugins/${pluginName}/push`;
|
|
472
|
+
const response = await new Promise((resolve, reject) => {
|
|
473
|
+
const data = JSON.stringify({ files });
|
|
474
|
+
const urlObj = new (require('url').URL)(url);
|
|
475
|
+
const http = urlObj.protocol === 'https:' ? require('https') : require('http');
|
|
476
|
+
const req = http.request({
|
|
477
|
+
hostname: urlObj.hostname,
|
|
478
|
+
port: urlObj.port,
|
|
479
|
+
path: urlObj.pathname,
|
|
480
|
+
method: 'POST',
|
|
481
|
+
headers: {
|
|
482
|
+
'Content-Type': 'application/json',
|
|
483
|
+
'Content-Length': Buffer.byteLength(data),
|
|
484
|
+
...(opts.token
|
|
485
|
+
? { 'Authorization': `Bearer ${opts.token}` }
|
|
486
|
+
: { 'X-Client-Id': opts.clientId, 'X-Client-Secret': opts.clientSecret }),
|
|
487
|
+
},
|
|
488
|
+
}, (res) => {
|
|
489
|
+
let body = '';
|
|
490
|
+
res.on('data', (chunk) => body += chunk);
|
|
491
|
+
res.on('end', () => {
|
|
492
|
+
try {
|
|
493
|
+
resolve({ status: res.statusCode, body: JSON.parse(body) });
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
resolve({ status: res.statusCode, body });
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
req.on('error', reject);
|
|
501
|
+
req.write(data);
|
|
502
|
+
req.end();
|
|
503
|
+
});
|
|
504
|
+
if (response.status === 200 && response.body?.success) {
|
|
505
|
+
pushSpinner.succeed('Plugin subido correctamente');
|
|
506
|
+
if (response.body.reloaded) {
|
|
507
|
+
logger_1.log.success('Plugin recargado automaticamente en el servidor');
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
logger_1.log.info('Reinicia el servidor remoto para cargar el plugin');
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
else if (response.status === 403 || response.status === 401) {
|
|
514
|
+
pushSpinner.fail('No autorizado. Verifica el token de admin.');
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
pushSpinner.fail(`Error: ${response.body?.error || response.status}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
catch (err) {
|
|
521
|
+
pushSpinner.fail('Error de conexion: ' + err.message);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
// ── openfactu plugin watch ──
|
|
525
|
+
plugin
|
|
526
|
+
.command('watch [dir]')
|
|
527
|
+
.description('Vigila cambios en un plugin y los sube automaticamente al servidor')
|
|
528
|
+
.requiredOption('-s, --server <url>', 'URL del servidor')
|
|
529
|
+
.option('-t, --token <token>', 'Token JWT de admin')
|
|
530
|
+
.option('--client-id <id>', 'Client ID de la dev key')
|
|
531
|
+
.option('--client-secret <secret>', 'Client Secret de la dev key')
|
|
532
|
+
.action(async (dir, opts) => {
|
|
533
|
+
if (!opts.token && (!opts.clientId || !opts.clientSecret)) {
|
|
534
|
+
logger_1.log.error('Necesitas --token o --client-id + --client-secret');
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const sourcePath = path_1.default.resolve(dir || process.cwd());
|
|
538
|
+
if (!fs_1.default.existsSync(sourcePath)) {
|
|
539
|
+
logger_1.log.error(`Directorio no encontrado: ${sourcePath}`);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const pluginName = path_1.default.basename(sourcePath);
|
|
543
|
+
logger_1.log.info(`Vigilando: ${chalk_1.default.bold(pluginName)}`);
|
|
544
|
+
logger_1.log.info(`Servidor: ${chalk_1.default.dim(opts.server)}`);
|
|
545
|
+
logger_1.log.blank();
|
|
546
|
+
logger_1.log.dim(' Guardando cualquier archivo se subira automaticamente al servidor.');
|
|
547
|
+
logger_1.log.dim(' Ctrl+C para parar.');
|
|
548
|
+
logger_1.log.blank();
|
|
549
|
+
const authHeaders = opts.token
|
|
550
|
+
? { 'Authorization': `Bearer ${opts.token}` }
|
|
551
|
+
: { 'X-Client-Id': opts.clientId, 'X-Client-Secret': opts.clientSecret };
|
|
552
|
+
// Funcion para subir un archivo al servidor
|
|
553
|
+
const pushFile = async (filePath) => {
|
|
554
|
+
// Ignorar archivos temporales del editor
|
|
555
|
+
if (filePath.includes('.tmp') || filePath.endsWith('~') || filePath.includes('.swp') || filePath.includes('.swx'))
|
|
556
|
+
return;
|
|
557
|
+
if (!fs_1.default.existsSync(filePath))
|
|
558
|
+
return;
|
|
559
|
+
const relativePath = path_1.default.relative(sourcePath, filePath);
|
|
560
|
+
const content = fs_1.default.readFileSync(filePath).toString('base64');
|
|
561
|
+
try {
|
|
562
|
+
const data = JSON.stringify({ files: [{ path: relativePath, content }] });
|
|
563
|
+
const urlObj = new (require('url').URL)(`${opts.server}/api/plugins/${pluginName}/push`);
|
|
564
|
+
const http = urlObj.protocol === 'https:' ? require('https') : require('http');
|
|
565
|
+
await new Promise((resolve, reject) => {
|
|
566
|
+
const req = http.request({
|
|
567
|
+
hostname: urlObj.hostname,
|
|
568
|
+
port: urlObj.port,
|
|
569
|
+
path: urlObj.pathname,
|
|
570
|
+
method: 'POST',
|
|
571
|
+
headers: {
|
|
572
|
+
'Content-Type': 'application/json',
|
|
573
|
+
'Content-Length': Buffer.byteLength(data),
|
|
574
|
+
...authHeaders,
|
|
575
|
+
},
|
|
576
|
+
}, (res) => {
|
|
577
|
+
let body = '';
|
|
578
|
+
res.on('data', (chunk) => body += chunk);
|
|
579
|
+
res.on('end', () => {
|
|
580
|
+
if (res.statusCode === 200)
|
|
581
|
+
resolve();
|
|
582
|
+
else
|
|
583
|
+
reject(new Error(body));
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
req.on('error', reject);
|
|
587
|
+
req.write(data);
|
|
588
|
+
req.end();
|
|
589
|
+
});
|
|
590
|
+
logger_1.log.success(`${chalk_1.default.dim(relativePath)} → subido y recargado`);
|
|
591
|
+
}
|
|
592
|
+
catch (err) {
|
|
593
|
+
logger_1.log.error(`${relativePath} → ${err.message}`);
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
// Watcher
|
|
597
|
+
const chokidar = require('chokidar');
|
|
598
|
+
const debounceTimers = new Map();
|
|
599
|
+
const watcher = chokidar.watch(sourcePath, {
|
|
600
|
+
ignored: ['**/node_modules/**', '**/.git/**', '**/dist/**'],
|
|
601
|
+
ignoreInitial: true,
|
|
602
|
+
persistent: true,
|
|
603
|
+
});
|
|
604
|
+
const handleChange = (filePath) => {
|
|
605
|
+
const existing = debounceTimers.get(filePath);
|
|
606
|
+
if (existing)
|
|
607
|
+
clearTimeout(existing);
|
|
608
|
+
debounceTimers.set(filePath, setTimeout(() => {
|
|
609
|
+
debounceTimers.delete(filePath);
|
|
610
|
+
pushFile(filePath);
|
|
611
|
+
}, 300));
|
|
612
|
+
};
|
|
613
|
+
watcher.on('change', handleChange);
|
|
614
|
+
watcher.on('add', handleChange);
|
|
615
|
+
// Mantener el proceso vivo
|
|
616
|
+
process.on('SIGINT', () => {
|
|
617
|
+
watcher.close();
|
|
618
|
+
logger_1.log.blank();
|
|
619
|
+
logger_1.log.info('Watch detenido');
|
|
620
|
+
process.exit(0);
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
// ── openfactu plugin dev ──
|
|
624
|
+
plugin
|
|
625
|
+
.command('dev [name]')
|
|
626
|
+
.description('Arranca el servidor en modo desarrollo para plugins')
|
|
627
|
+
.action(async (name) => {
|
|
628
|
+
const pluginsDir = (0, paths_1.getPluginsDir)();
|
|
629
|
+
if (name) {
|
|
630
|
+
const pluginPath = path_1.default.join(pluginsDir, name);
|
|
631
|
+
if (!fs_1.default.existsSync(pluginPath)) {
|
|
632
|
+
logger_1.log.error(`Plugin "${name}" no encontrado en ${pluginsDir}`);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
logger_1.log.info(`Modo desarrollo para plugin: ${chalk_1.default.bold(name)}`);
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
logger_1.log.info('Modo desarrollo para todos los plugins');
|
|
639
|
+
}
|
|
640
|
+
logger_1.log.blank();
|
|
641
|
+
logger_1.log.dim(' El servidor recargara los plugins automaticamente al detectar cambios.');
|
|
642
|
+
logger_1.log.dim(' Los componentes UI se actualizan en el browser sin refrescar.');
|
|
643
|
+
logger_1.log.blank();
|
|
644
|
+
const { getProjectRoot } = require('../utils/paths');
|
|
645
|
+
const root = getProjectRoot();
|
|
646
|
+
try {
|
|
647
|
+
const child = require('child_process').spawn('npm', ['run', 'dev:server'], {
|
|
648
|
+
cwd: root,
|
|
649
|
+
env: { ...process.env, NODE_ENV: 'development' },
|
|
650
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
651
|
+
});
|
|
652
|
+
child.stdout.on('data', (data) => {
|
|
653
|
+
const line = data.toString().trim();
|
|
654
|
+
if (!line)
|
|
655
|
+
return;
|
|
656
|
+
// Resaltar logs del plugin
|
|
657
|
+
if (name && line.includes(name)) {
|
|
658
|
+
console.log(chalk_1.default.cyan(line));
|
|
659
|
+
}
|
|
660
|
+
else if (line.includes('[Plugins]') || line.includes('[PluginWatcher]') || line.includes('[DevSocket]') || line.includes('[HookManager]')) {
|
|
661
|
+
console.log(chalk_1.default.yellow(line));
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
console.log(chalk_1.default.dim(line));
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
child.stderr.on('data', (data) => {
|
|
668
|
+
const line = data.toString().trim();
|
|
669
|
+
if (!line)
|
|
670
|
+
return;
|
|
671
|
+
console.log(chalk_1.default.red(line));
|
|
672
|
+
});
|
|
673
|
+
child.on('close', (code) => {
|
|
674
|
+
logger_1.log.blank();
|
|
675
|
+
if (code === 0) {
|
|
676
|
+
logger_1.log.info('Servidor detenido');
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
logger_1.log.error(`Servidor terminado con codigo ${code}`);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
// Capturar Ctrl+C
|
|
683
|
+
process.on('SIGINT', () => {
|
|
684
|
+
child.kill('SIGINT');
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
catch (err) {
|
|
688
|
+
logger_1.log.error('Error al arrancar: ' + err.message);
|
|
689
|
+
logger_1.log.dim(` Ejecuta manualmente: cd ${root} && npm run dev:server`);
|
|
690
|
+
}
|
|
691
|
+
});
|
|
94
692
|
}
|
package/dist/src/utils/db.js
CHANGED
|
@@ -24,7 +24,27 @@ let db = null;
|
|
|
24
24
|
let _schema = null;
|
|
25
25
|
function getSchema() {
|
|
26
26
|
if (!_schema) {
|
|
27
|
-
|
|
27
|
+
// Registrar ts-node si esta disponible (para leer .ts directamente)
|
|
28
|
+
try {
|
|
29
|
+
require('ts-node').register({ transpileOnly: true, compilerOptions: { module: 'commonjs', esModuleInterop: true } });
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
const srcPath = path_1.default.join((0, paths_1.getServerSrcDir)(), 'db/schema');
|
|
33
|
+
const distPath = path_1.default.join((0, paths_1.getServerSrcDir)(), '..', 'dist/db/schema');
|
|
34
|
+
try {
|
|
35
|
+
_schema = require(srcPath);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
try {
|
|
39
|
+
_schema = require(distPath);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new Error(`No se pudo cargar el schema de la BD.\n` +
|
|
43
|
+
`Intentado: ${srcPath}\n` +
|
|
44
|
+
`Intentado: ${distPath}\n` +
|
|
45
|
+
`Ejecuta 'npm install' en el directorio de OpenFactu.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
28
48
|
}
|
|
29
49
|
return _schema;
|
|
30
50
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfactu/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
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",
|
|
@@ -19,21 +19,22 @@
|
|
|
19
19
|
"dev": "ts-node bin/openfactu.ts"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"
|
|
22
|
+
"bcrypt": "^5.1.1",
|
|
23
23
|
"chalk": "^4.1.2",
|
|
24
|
-
"ora": "^5.4.1",
|
|
25
|
-
"inquirer": "^8.2.6",
|
|
26
|
-
"dotenv": "^16.0.0",
|
|
27
24
|
"cli-table3": "^0.6.5",
|
|
25
|
+
"commander": "^12.0.0",
|
|
26
|
+
"dotenv": "^16.0.0",
|
|
28
27
|
"drizzle-orm": "^0.45.2",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
28
|
+
"inquirer": "^8.2.6",
|
|
29
|
+
"inquirer-autocomplete-prompt": "^2.0.1",
|
|
30
|
+
"ora": "^5.4.1",
|
|
31
|
+
"pg": "^8.13.0"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
34
|
+
"@types/bcrypt": "^5.0.0",
|
|
33
35
|
"@types/inquirer": "^8.2.0",
|
|
34
36
|
"@types/pg": "^8.11.0",
|
|
35
|
-
"
|
|
36
|
-
"typescript": "^5.3.3"
|
|
37
|
-
"ts-node": "^10.9.0"
|
|
37
|
+
"ts-node": "^10.9.0",
|
|
38
|
+
"typescript": "^5.3.3"
|
|
38
39
|
}
|
|
39
40
|
}
|