@openfactu/cli 0.0.3 → 0.0.5

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.
@@ -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 = `version: '3.8'
193
-
194
- services:
192
+ let composeContent = `services:
195
193
  web:
196
194
  build:
197
195
  context: .
@@ -9,6 +9,7 @@ const ora_1 = __importDefault(require("ora"));
9
9
  const inquirer_1 = __importDefault(require("inquirer"));
10
10
  const child_process_1 = require("child_process");
11
11
  const https_1 = __importDefault(require("https"));
12
+ const os_1 = __importDefault(require("os"));
12
13
  const fs_1 = __importDefault(require("fs"));
13
14
  const path_1 = __importDefault(require("path"));
14
15
  const logger_1 = require("../utils/logger");
@@ -156,7 +157,7 @@ function registerInstallCommand(program) {
156
157
  type: 'input',
157
158
  name: 'dir',
158
159
  message: 'Directorio de instalación:',
159
- default: path_1.default.join(process.cwd(), 'openfactu'),
160
+ default: path_1.default.join(os_1.default.homedir(), 'openfactu'),
160
161
  },
161
162
  ]);
162
163
  targetDir = dir;
@@ -182,27 +183,44 @@ function registerInstallCommand(program) {
182
183
  }
183
184
  logger_1.log.info(`Directorio: ${chalk_1.default.dim(targetDir)}`);
184
185
  logger_1.log.blank();
185
- // 4. Clonar repositorio
186
+ // 4. Crear directorio si no existe (con sudo si hace falta)
187
+ if (!fs_1.default.existsSync(targetDir)) {
188
+ try {
189
+ fs_1.default.mkdirSync(targetDir, { recursive: true });
190
+ }
191
+ catch (mkdirErr) {
192
+ if (mkdirErr.code === 'EACCES') {
193
+ logger_1.log.warn('Sin permisos. Creando directorio con sudo...');
194
+ try {
195
+ const user = process.env.USER || process.env.USERNAME || 'root';
196
+ (0, child_process_1.execSync)(`sudo mkdir -p "${targetDir}" && sudo chown -R ${user}:${user} "${targetDir}"`, {
197
+ stdio: 'inherit',
198
+ });
199
+ }
200
+ catch {
201
+ logger_1.log.error(`No se pudo crear ${targetDir}. Ejecuta con sudo o elige otro directorio.`);
202
+ return;
203
+ }
204
+ }
205
+ else {
206
+ throw mkdirErr;
207
+ }
208
+ }
209
+ }
210
+ // 5. Clonar repositorio
186
211
  const cloneSpinner = (0, ora_1.default)('Descargando OpenFactu...').start();
187
212
  const isTag = releases.some((r) => r.tag_name === ref);
213
+ const cloneCmd = isTag
214
+ ? `git clone --depth 1 --branch ${ref} ${repoUrl} "${targetDir}"`
215
+ : `git clone --branch ${ref} ${repoUrl} "${targetDir}"`;
188
216
  try {
189
- if (isTag) {
190
- // Para tags: clonar y luego checkout al tag
191
- (0, child_process_1.execSync)(`git clone --depth 1 --branch ${ref} ${repoUrl} "${targetDir}"`, { stdio: 'pipe', timeout: 120000 });
192
- }
193
- else {
194
- // Para branches: clonar la branch directamente
195
- (0, child_process_1.execSync)(`git clone --branch ${ref} ${repoUrl} "${targetDir}"`, { stdio: 'pipe', timeout: 120000 });
196
- }
217
+ (0, child_process_1.execSync)(cloneCmd, { stdio: 'pipe', timeout: 120000 });
197
218
  cloneSpinner.succeed('Código descargado');
198
219
  }
199
220
  catch (err) {
200
221
  // Fallback: clonar todo y checkout
201
222
  try {
202
223
  cloneSpinner.text = 'Descargando (método alternativo)...';
203
- if (!fs_1.default.existsSync(targetDir)) {
204
- fs_1.default.mkdirSync(targetDir, { recursive: true });
205
- }
206
224
  (0, child_process_1.execSync)(`git clone ${repoUrl} "${targetDir}"`, { stdio: 'pipe', timeout: 180000 });
207
225
  (0, child_process_1.execSync)(`git checkout ${ref}`, { cwd: targetDir, stdio: 'pipe' });
208
226
  cloneSpinner.succeed('Código descargado');
@@ -212,14 +230,14 @@ function registerInstallCommand(program) {
212
230
  return;
213
231
  }
214
232
  }
215
- // 5. Copiar .env.example a .env
233
+ // 6. Copiar .env.example a .env
216
234
  const envExample = path_1.default.join(targetDir, '.env.example');
217
235
  const envFile = path_1.default.join(targetDir, '.env');
218
236
  if (fs_1.default.existsSync(envExample) && !fs_1.default.existsSync(envFile)) {
219
237
  fs_1.default.copyFileSync(envExample, envFile);
220
238
  logger_1.log.success('Archivo .env creado desde .env.example');
221
239
  }
222
- // 6. Preguntar modo de instalación
240
+ // 7. Preguntar modo de instalación
223
241
  const hasDocker = checkDocker();
224
242
  if (!hasDocker) {
225
243
  logger_1.log.warn('Docker no detectado. OpenFactu requiere Docker para funcionar.');
@@ -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('Gestión de plugins');
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,243 @@ 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
+ });
94
348
  }
package/dist/src/index.js CHANGED
@@ -15,7 +15,7 @@ function createCLI() {
15
15
  program
16
16
  .name('openfactu')
17
17
  .description('CLI para gestionar OpenFactu')
18
- .version('0.0.3');
18
+ .version('0.0.4');
19
19
  (0, version_1.registerVersionCommand)(program);
20
20
  (0, migrate_1.registerMigrateCommand)(program);
21
21
  (0, tenant_1.registerTenantCommand)(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfactu/cli",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
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
- "commander": "^12.0.0",
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
- "pg": "^8.13.0",
30
- "bcrypt": "^5.1.1"
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
- "@types/bcrypt": "^5.0.0",
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
  }