@openfactu/cli 0.0.5 → 0.0.7

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Angel Acedo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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) -- ERP de facturación open source.
3
+ CLI oficial para instalar, gestionar y desplegar [OpenFactu](https://github.com/OpenFactu/platform) 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
- # Descargar e instalar OpenFactu (te deja elegir version)
15
- openfactu install
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 e instala OpenFactu. Muestra las releases de GitHub para elegir version. Soporta Docker en Windows/Mac/Linux. |
34
- | `openfactu update` | Actualiza a la ultima version desde GitHub sin perder datos (plugins, storage, .env se preservan). |
35
- | `openfactu update:check` | Comprueba si hay versiones nuevas disponibles. |
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: red local, dominio publico o localhost. Genera `docker-compose.prod.yml`. |
53
- | `openfactu deploy:status` | Muestra el estado de los contenedores Docker y las URLs de acceso. |
54
-
55
- ```bash
56
- # Configurar para que sea accesible en la red
57
- openfactu deploy
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: verifica BD, crea admin, primer tenant. |
68
- | `openfactu migrate` | Ejecuta migraciones pendientes en todos los tenants. |
69
- | `openfactu migrate:status` | Muestra tabla con estado de migraciones por tenant. |
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 todas las empresas. |
84
- | `openfactu tenant create [nombre]` | Crea una empresa nueva con schema y migraciones. |
85
- | `openfactu tenant sync [nombre]` | Sincroniza migraciones de un tenant o todos. |
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 su estado por tenant. |
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` | Muestra versiones del CLI, server, web y Node. |
75
+ | `openfactu version` | Versiones del sistema |
98
76
 
99
- ## Uso desde cualquier directorio
77
+ ## Desarrollo remoto de plugins
100
78
 
101
- El CLI detecta automaticamente la instalacion de OpenFactu. Si no estas dentro del proyecto:
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
- # Opcion 2: variable de entorno
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 (para descargar releases)
103
+ - Git
117
104
 
118
105
  ## Links
119
106
 
120
- - [GitHub](https://github.com/AngelAcedo12/OpenFactu)
121
- - [Reportar un problema](https://github.com/AngelAcedo12/OpenFactu/issues)
107
+ - [GitHub](https://github.com/OpenFactu/platform)
108
+ - [Documentacion](https://openfactuerp.org)
109
+ - [Marketplace](https://openfactuerp.org/marketplace/)
110
+ - [Reportar problema](https://github.com/OpenFactu/platform/issues)
@@ -82,6 +82,7 @@ function registerDeployCommand(program) {
82
82
  let host = 'localhost';
83
83
  let serverPort = '3000';
84
84
  let webPort = '8080';
85
+ let dbPort = '5432';
85
86
  let useSSL = false;
86
87
  if (mode === 'lan') {
87
88
  const ipChoices = localIPs.map((ip) => ({ name: ip, value: ip }));
@@ -128,7 +129,7 @@ function registerDeployCommand(program) {
128
129
  {
129
130
  type: 'confirm',
130
131
  name: 'ports',
131
- message: `¿Usar puertos por defecto? (web: 8080, api: 3000)`,
132
+ message: `¿Usar puertos por defecto? (web: 8080, api: 3000, db: 5432)`,
132
133
  default: true,
133
134
  },
134
135
  ]);
@@ -136,9 +137,11 @@ function registerDeployCommand(program) {
136
137
  const answers = await inquirer_1.default.prompt([
137
138
  { type: 'input', name: 'webPort', message: 'Puerto web:', default: '8080' },
138
139
  { type: 'input', name: 'serverPort', message: 'Puerto API:', default: '3000' },
140
+ { type: 'input', name: 'dbPort', message: 'Puerto BD (host):', default: '5432' },
139
141
  ]);
140
142
  webPort = answers.webPort;
141
143
  serverPort = answers.serverPort;
144
+ dbPort = answers.dbPort;
142
145
  }
143
146
  // Password de BD
144
147
  const { dbPassword } = await inquirer_1.default.prompt([
@@ -161,6 +164,7 @@ function registerDeployCommand(program) {
161
164
  logger_1.log.title(' Resumen de configuración');
162
165
  logger_1.log.info(`Web: ${chalk_1.default.cyan(webUrl)}`);
163
166
  logger_1.log.info(`API: ${chalk_1.default.cyan(apiUrl)}`);
167
+ logger_1.log.info(`BD Puerto: ${chalk_1.default.cyan(dbPort)} ${chalk_1.default.dim('(host)')}`);
164
168
  logger_1.log.info(`BD Password: ${chalk_1.default.dim(dbPassword === 'openfactu_pass' ? '(default)' : '****')}`);
165
169
  logger_1.log.info(`SSL: ${useSSL ? chalk_1.default.green('Si') : chalk_1.default.dim('No')}`);
166
170
  logger_1.log.blank();
@@ -176,7 +180,7 @@ function registerDeployCommand(program) {
176
180
  const env = readEnv(envPath);
177
181
  env.SERVER_PORT = serverPort;
178
182
  env.WEB_PORT = webPort;
179
- env.DB_PORT = env.DB_PORT || '5432';
183
+ env.DB_PORT = dbPort;
180
184
  env.POSTGRES_USER = env.POSTGRES_USER || 'openfactu';
181
185
  env.POSTGRES_PASSWORD = dbPassword;
182
186
  env.POSTGRES_DB = env.POSTGRES_DB || 'openfactudb';
@@ -365,4 +369,128 @@ networks:
365
369
  logger_1.log.dim(' ' + err.message);
366
370
  }
367
371
  });
372
+ // ── openfactu rebuild ──
373
+ program
374
+ .command('rebuild')
375
+ .description('Reconstruye y reinicia los contenedores Docker')
376
+ .option('--service <name>', 'Reconstruir solo un servicio (web, server, db)')
377
+ .option('--no-cache', 'Construir sin cache de Docker')
378
+ .action(async (opts) => {
379
+ try {
380
+ const root = (0, paths_1.getProjectRoot)();
381
+ const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
382
+ const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
383
+ const service = opts.service || '';
384
+ const noCache = opts.cache === false ? ' --no-cache' : '';
385
+ logger_1.log.info(`Usando: ${chalk_1.default.dim(composeFile)}`);
386
+ logger_1.log.blank();
387
+ const buildSpinner = (0, ora_1.default)(`Construyendo${service ? ' ' + service : ' todos los servicios'}...`).start();
388
+ try {
389
+ (0, child_process_1.execSync)(`docker compose -f ${composeFile} build${noCache} ${service}`, {
390
+ cwd: root,
391
+ stdio: 'pipe',
392
+ timeout: 600000,
393
+ });
394
+ buildSpinner.succeed('Build completado');
395
+ }
396
+ catch (err) {
397
+ buildSpinner.fail('Error en el build');
398
+ // Mostrar output del error
399
+ const output = err.stdout?.toString() || err.stderr?.toString() || err.message;
400
+ const errorLines = output.split('\n').filter((l) => l.includes('error') || l.includes('Error') || l.includes('>>>'));
401
+ if (errorLines.length > 0) {
402
+ logger_1.log.blank();
403
+ for (const line of errorLines.slice(0, 10)) {
404
+ logger_1.log.error(line.trim());
405
+ }
406
+ }
407
+ return;
408
+ }
409
+ const upSpinner = (0, ora_1.default)('Reiniciando servicios...').start();
410
+ try {
411
+ (0, child_process_1.execSync)(`docker compose -f ${composeFile} up -d ${service}`, {
412
+ cwd: root,
413
+ stdio: 'pipe',
414
+ timeout: 60000,
415
+ });
416
+ upSpinner.succeed('Servicios levantados');
417
+ }
418
+ catch (err) {
419
+ upSpinner.fail('Error al levantar: ' + err.message);
420
+ return;
421
+ }
422
+ logger_1.log.blank();
423
+ logger_1.log.success('Rebuild completado');
424
+ // Mostrar URLs
425
+ const envPath = path_1.default.join(root, '.env');
426
+ const env = readEnv(envPath);
427
+ const host = env.HOST || 'localhost';
428
+ const webPort = env.WEB_PORT || '8080';
429
+ const serverPort = env.SERVER_PORT || '3000';
430
+ logger_1.log.info(`Web: ${chalk_1.default.cyan(`http://${host}:${webPort}`)}`);
431
+ logger_1.log.info(`API: ${chalk_1.default.cyan(`http://${host}:${serverPort}`)}`);
432
+ }
433
+ catch (err) {
434
+ logger_1.log.error(err.message);
435
+ process.exitCode = 1;
436
+ }
437
+ });
438
+ // ── openfactu logs ──
439
+ program
440
+ .command('logs')
441
+ .description('Muestra los logs de los servicios Docker')
442
+ .option('--service <name>', 'Logs de un servicio especifico (web, server, db)')
443
+ .option('-n, --lines <number>', 'Numero de lineas', '50')
444
+ .action(async (opts) => {
445
+ try {
446
+ const root = (0, paths_1.getProjectRoot)();
447
+ const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
448
+ const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
449
+ const service = opts.service || '';
450
+ const lines = opts.lines || '50';
451
+ (0, child_process_1.execSync)(`docker compose -f ${composeFile} logs --tail ${lines} ${service}`, {
452
+ cwd: root,
453
+ stdio: 'inherit',
454
+ });
455
+ }
456
+ catch (err) {
457
+ logger_1.log.error(err.message);
458
+ }
459
+ });
460
+ // ── openfactu stop ──
461
+ program
462
+ .command('stop')
463
+ .description('Para todos los servicios Docker')
464
+ .action(async () => {
465
+ try {
466
+ const root = (0, paths_1.getProjectRoot)();
467
+ const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
468
+ const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
469
+ const spinner = (0, ora_1.default)('Parando servicios...').start();
470
+ (0, child_process_1.execSync)(`docker compose -f ${composeFile} down`, { cwd: root, stdio: 'pipe' });
471
+ spinner.succeed('Servicios parados');
472
+ }
473
+ catch (err) {
474
+ logger_1.log.error(err.message);
475
+ }
476
+ });
477
+ // ── openfactu restart ──
478
+ program
479
+ .command('restart')
480
+ .description('Reinicia los servicios Docker (sin rebuild)')
481
+ .option('--service <name>', 'Reiniciar solo un servicio')
482
+ .action(async (opts) => {
483
+ try {
484
+ const root = (0, paths_1.getProjectRoot)();
485
+ const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
486
+ const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
487
+ const service = opts.service || '';
488
+ const spinner = (0, ora_1.default)('Reiniciando...').start();
489
+ (0, child_process_1.execSync)(`docker compose -f ${composeFile} restart ${service}`, { cwd: root, stdio: 'pipe' });
490
+ spinner.succeed('Servicios reiniciados');
491
+ }
492
+ catch (err) {
493
+ logger_1.log.error(err.message);
494
+ }
495
+ });
368
496
  }
@@ -13,9 +13,9 @@ 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 REPO_URL = 'https://github.com/AngelAcedo12/OpenFactu.git';
17
- const GITHUB_OWNER = 'AngelAcedo12';
18
- const GITHUB_REPO = 'OpenFactu';
16
+ const REPO_URL = 'https://github.com/OpenFactu/platform.git';
17
+ const GITHUB_OWNER = 'OpenFactu';
18
+ const GITHUB_REPO = 'platform';
19
19
  function fetchJSON(url) {
20
20
  return new Promise((resolve, reject) => {
21
21
  https_1.default.get(url, { headers: { 'User-Agent': 'openfactu-cli' } }, (res) => {
@@ -345,4 +345,348 @@ function registerPluginCommand(program) {
345
345
  spinner.fail('Error: ' + err.message);
346
346
  }
347
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
+ });
348
692
  }
@@ -24,7 +24,27 @@ let db = null;
24
24
  let _schema = null;
25
25
  function getSchema() {
26
26
  if (!_schema) {
27
- _schema = require(path_1.default.join((0, paths_1.getServerSrcDir)(), 'db/schema'));
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.5",
3
+ "version": "0.0.7",
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",