gufi-cli 0.1.9 β 0.1.11
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/CLAUDE.md +25 -2
- package/dist/commands/companies.d.ts +6 -0
- package/dist/commands/companies.js +211 -22
- package/dist/commands/env.js +53 -4
- package/dist/commands/login.js +6 -2
- package/dist/commands/rows.js +53 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -1
- package/dist/lib/api.js +48 -26
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.js +4 -1
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -1569,6 +1569,29 @@ npm install -g gufi-cli
|
|
|
1569
1569
|
gufi login
|
|
1570
1570
|
```
|
|
1571
1571
|
|
|
1572
|
+
### π AutenticaciΓ³n Persistente (Auto-Login)
|
|
1573
|
+
|
|
1574
|
+
El CLI guarda las credenciales en `~/.gufi/config.json` y hace **auto-login automΓ‘tico** cuando es necesario:
|
|
1575
|
+
|
|
1576
|
+
```json
|
|
1577
|
+
{
|
|
1578
|
+
"apiUrl": "https://gogufi.com",
|
|
1579
|
+
"email": "user@example.com",
|
|
1580
|
+
"password": "secreto",
|
|
1581
|
+
"token": "eyJ...",
|
|
1582
|
+
"refreshToken": "eyJ..."
|
|
1583
|
+
}
|
|
1584
|
+
```
|
|
1585
|
+
|
|
1586
|
+
**Flujo automΓ‘tico:**
|
|
1587
|
+
1. `gufi login` β pide email/password β los guarda permanentemente
|
|
1588
|
+
2. Cualquier comando β si token expirΓ³ β auto-login con credenciales guardadas
|
|
1589
|
+
3. `gufi logout` β borra TODO (token + credenciales)
|
|
1590
|
+
|
|
1591
|
+
**Resultado:** Una vez haces `gufi login`, el CLI funciona para siempre sin volver a pedir credenciales. Si el token expira, hace auto-login automΓ‘ticamente.
|
|
1592
|
+
|
|
1593
|
+
**Nota:** Si haces login en el navegador web, el refreshToken del CLI se invalida, pero el CLI hace auto-login automΓ‘ticamente con las credenciales guardadas.
|
|
1594
|
+
|
|
1572
1595
|
### GestiΓ³n de Companies y MΓ³dulos
|
|
1573
1596
|
|
|
1574
1597
|
```bash
|
|
@@ -1718,9 +1741,9 @@ gufi view:status
|
|
|
1718
1741
|
|
|
1719
1742
|
| Error | SoluciΓ³n |
|
|
1720
1743
|
|-------|----------|
|
|
1721
|
-
| "No estΓ‘s logueado" | `gufi login` |
|
|
1744
|
+
| "No estΓ‘s logueado" | `gufi login` (guarda credenciales para auto-login) |
|
|
1722
1745
|
| "MΓ³dulo no encontrado" | Verificar `-c <company_id>` |
|
|
1723
|
-
| "Token expirado" |
|
|
1746
|
+
| "Token expirado" | AutomΓ‘tico: refresh o auto-login con credenciales guardadas |
|
|
1724
1747
|
| "JSON invΓ‘lido" | Validar estructura del JSON |
|
|
1725
1748
|
|
|
1726
1749
|
### Conceptos clave
|
|
@@ -14,6 +14,9 @@ export declare function moduleUpdateCommand(moduleId?: string, jsonFile?: string
|
|
|
14
14
|
company?: string;
|
|
15
15
|
dryRun?: boolean;
|
|
16
16
|
}): Promise<void>;
|
|
17
|
+
export declare function moduleCreateCommand(jsonFile?: string, options?: {
|
|
18
|
+
company?: string;
|
|
19
|
+
}): Promise<void>;
|
|
17
20
|
export declare function companyCreateCommand(name?: string): Promise<void>;
|
|
18
21
|
export declare function automationsCommand(_moduleId?: string, // Ignored - kept for backwards compatibility
|
|
19
22
|
options?: {
|
|
@@ -24,3 +27,6 @@ export declare function automationCommand(automationName?: string, options?: {
|
|
|
24
27
|
company?: string;
|
|
25
28
|
file?: string;
|
|
26
29
|
}): Promise<void>;
|
|
30
|
+
export declare function automationCreateCommand(name?: string, jsFile?: string, options?: {
|
|
31
|
+
company?: string;
|
|
32
|
+
}): Promise<void>;
|
|
@@ -4,19 +4,81 @@
|
|
|
4
4
|
* gufi module - View/edit module JSON
|
|
5
5
|
*/
|
|
6
6
|
import chalk from "chalk";
|
|
7
|
-
import { isLoggedIn, getApiUrl, getToken } from "../lib/config.js";
|
|
7
|
+
import { isLoggedIn, getApiUrl, getToken, getRefreshToken, setToken, loadConfig } from "../lib/config.js";
|
|
8
8
|
const API_URL_BASE = "https://gogufi.com";
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// π Auto-login with saved credentials
|
|
10
|
+
async function autoLogin() {
|
|
11
|
+
const config = loadConfig();
|
|
12
|
+
if (!config.email || !config.password) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
console.log(chalk.gray(" [gufi] Auto-login..."));
|
|
17
|
+
const apiUrl = getApiUrl() || API_URL_BASE;
|
|
18
|
+
const response = await fetch(`${apiUrl}/api/auth/login`, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { "Content-Type": "application/json", "X-Client": "cli" },
|
|
21
|
+
body: JSON.stringify({ email: config.email, password: config.password }),
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok)
|
|
24
|
+
return undefined;
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
setToken(data.accessToken, config.email, data.refreshToken);
|
|
27
|
+
console.log(chalk.green(" [gufi] Auto-login exitoso"));
|
|
28
|
+
return data.accessToken;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// π Refresh token or auto-login
|
|
35
|
+
async function refreshOrLogin() {
|
|
36
|
+
const refreshToken = getRefreshToken();
|
|
37
|
+
const apiUrl = getApiUrl() || API_URL_BASE;
|
|
38
|
+
if (refreshToken) {
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(`${apiUrl}/api/auth/refresh`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: { "Content-Type": "application/json", "X-Client": "cli", "X-Refresh-Token": refreshToken },
|
|
43
|
+
});
|
|
44
|
+
if (response.ok) {
|
|
45
|
+
const data = await response.json();
|
|
46
|
+
const config = loadConfig();
|
|
47
|
+
setToken(data.accessToken, config.email || "", data.refreshToken);
|
|
48
|
+
return data.accessToken;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch { }
|
|
52
|
+
}
|
|
53
|
+
return autoLogin();
|
|
54
|
+
}
|
|
55
|
+
// π API request with auto-login and refresh
|
|
56
|
+
async function apiRequest(path, options = {}, retry = true) {
|
|
57
|
+
let token = getToken();
|
|
11
58
|
const apiUrl = getApiUrl() || API_URL_BASE;
|
|
59
|
+
// If no token, try auto-login
|
|
60
|
+
if (!token) {
|
|
61
|
+
token = await autoLogin();
|
|
62
|
+
if (!token) {
|
|
63
|
+
throw new Error("No estΓ‘s logueado. Usa: gufi login");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
12
66
|
const res = await fetch(`${apiUrl}${path}`, {
|
|
13
67
|
...options,
|
|
14
68
|
headers: {
|
|
15
69
|
"Authorization": `Bearer ${token}`,
|
|
16
70
|
"Content-Type": "application/json",
|
|
71
|
+
"X-Client": "cli",
|
|
17
72
|
...options.headers,
|
|
18
73
|
},
|
|
19
74
|
});
|
|
75
|
+
// On 401/403, try refresh and retry once
|
|
76
|
+
if ((res.status === 401 || res.status === 403) && retry) {
|
|
77
|
+
const newToken = await refreshOrLogin();
|
|
78
|
+
if (newToken) {
|
|
79
|
+
return apiRequest(path, options, false);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
20
82
|
if (!res.ok) {
|
|
21
83
|
const error = await res.text();
|
|
22
84
|
throw new Error(`API Error ${res.status}: ${error}`);
|
|
@@ -251,11 +313,11 @@ async function editModuleJson(moduleId, currentJson, companyId) {
|
|
|
251
313
|
if (companyId) {
|
|
252
314
|
headers["X-Company-ID"] = companyId;
|
|
253
315
|
}
|
|
254
|
-
// Call update endpoint
|
|
255
|
-
const result = await apiRequest(`/api/modules/${moduleId}`, {
|
|
316
|
+
// Call update endpoint (confirm: true to actually save)
|
|
317
|
+
const result = await apiRequest(`/api/company/modules/${moduleId}`, {
|
|
256
318
|
method: "PUT",
|
|
257
319
|
headers,
|
|
258
|
-
body: JSON.stringify({
|
|
320
|
+
body: JSON.stringify({ json: newJson, confirm: true }),
|
|
259
321
|
});
|
|
260
322
|
console.log(chalk.green("\n β MΓ³dulo actualizado correctamente!\n"));
|
|
261
323
|
// Show what changed
|
|
@@ -306,32 +368,51 @@ export async function moduleUpdateCommand(moduleId, jsonFile, options) {
|
|
|
306
368
|
if (options?.company) {
|
|
307
369
|
headers["X-Company-ID"] = options.company;
|
|
308
370
|
}
|
|
309
|
-
// Validate
|
|
371
|
+
// Validate JSON structure locally
|
|
310
372
|
console.log(chalk.gray(" Validando JSON..."));
|
|
311
|
-
|
|
312
|
-
|
|
373
|
+
if (!newJson.name && !newJson.displayName) {
|
|
374
|
+
console.log(chalk.red("\n β JSON debe tener 'name' o 'displayName'\n"));
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
console.log(chalk.green(" β JSON vΓ‘lido"));
|
|
378
|
+
// π Step 1: Dry-run para ver quΓ© cambios se harΓ‘n
|
|
379
|
+
console.log(chalk.gray(" Verificando cambios (dry-run)..."));
|
|
380
|
+
const preview = await apiRequest(`/api/company/modules/${moduleId}`, {
|
|
381
|
+
method: "PUT",
|
|
313
382
|
headers,
|
|
314
|
-
body: JSON.stringify({
|
|
383
|
+
body: JSON.stringify({ json: newJson, confirm: false }),
|
|
315
384
|
});
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
385
|
+
// Mostrar preview de cambios
|
|
386
|
+
const hasChanges = preview.tablesCreated?.length || preview.columnsAdded?.length ||
|
|
387
|
+
preview.columnsRemoved?.length || preview.tablesRemoved?.length;
|
|
388
|
+
if (hasChanges) {
|
|
389
|
+
console.log(chalk.cyan("\n π Cambios detectados:"));
|
|
390
|
+
if (preview.tablesCreated?.length) {
|
|
391
|
+
console.log(chalk.green(` + Tablas a crear: ${preview.tablesCreated.join(", ")}`));
|
|
392
|
+
}
|
|
393
|
+
if (preview.columnsAdded?.length) {
|
|
394
|
+
console.log(chalk.green(` + Columnas a aΓ±adir: ${preview.columnsAdded.join(", ")}`));
|
|
395
|
+
}
|
|
396
|
+
if (preview.columnsRemoved?.length) {
|
|
397
|
+
console.log(chalk.yellow(` - Columnas a eliminar: ${preview.columnsRemoved.join(", ")}`));
|
|
398
|
+
}
|
|
399
|
+
if (preview.tablesRemoved?.length) {
|
|
400
|
+
console.log(chalk.red(` - Tablas a eliminar: ${preview.tablesRemoved.join(", ")}`));
|
|
320
401
|
}
|
|
321
|
-
console.log();
|
|
322
|
-
process.exit(1);
|
|
323
402
|
}
|
|
324
|
-
|
|
403
|
+
else {
|
|
404
|
+
console.log(chalk.gray(" Sin cambios estructurales detectados."));
|
|
405
|
+
}
|
|
325
406
|
if (options?.dryRun) {
|
|
326
|
-
console.log(chalk.gray("\n [DRY RUN]
|
|
407
|
+
console.log(chalk.gray("\n [DRY RUN] Vista previa completada. Usa sin --dry-run para aplicar.\n"));
|
|
327
408
|
return;
|
|
328
409
|
}
|
|
329
|
-
//
|
|
330
|
-
console.log(chalk.gray(" Aplicando cambios..."));
|
|
331
|
-
const result = await apiRequest(`/api/modules/${moduleId}`, {
|
|
410
|
+
// π Step 2: Aplicar cambios (confirm: true)
|
|
411
|
+
console.log(chalk.gray("\n Aplicando cambios..."));
|
|
412
|
+
const result = await apiRequest(`/api/company/modules/${moduleId}`, {
|
|
332
413
|
method: "PUT",
|
|
333
414
|
headers,
|
|
334
|
-
body: JSON.stringify({
|
|
415
|
+
body: JSON.stringify({ json: newJson, confirm: true }),
|
|
335
416
|
});
|
|
336
417
|
console.log(chalk.green("\n β MΓ³dulo actualizado correctamente!\n"));
|
|
337
418
|
if (result.tablesCreated?.length) {
|
|
@@ -353,6 +434,68 @@ export async function moduleUpdateCommand(moduleId, jsonFile, options) {
|
|
|
353
434
|
}
|
|
354
435
|
}
|
|
355
436
|
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
437
|
+
// gufi module:create <json_file> --company <id> - Create module from JSON file
|
|
438
|
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
439
|
+
export async function moduleCreateCommand(jsonFile, options) {
|
|
440
|
+
if (!isLoggedIn()) {
|
|
441
|
+
console.log(chalk.red("\n β No estΓ‘s logueado. Usa: gufi login\n"));
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
if (!jsonFile) {
|
|
445
|
+
console.log(chalk.red("\n β Uso: gufi module:create <json_file> --company <id>\n"));
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
if (!options?.company) {
|
|
449
|
+
console.log(chalk.red("\n β Debes especificar --company <id>\n"));
|
|
450
|
+
console.log(chalk.gray(" Tip: Usa 'gufi companies' para ver tus companies\n"));
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
const fs = await import("fs");
|
|
454
|
+
// Read JSON file
|
|
455
|
+
let moduleJson;
|
|
456
|
+
try {
|
|
457
|
+
const content = fs.readFileSync(jsonFile, "utf-8");
|
|
458
|
+
moduleJson = JSON.parse(content);
|
|
459
|
+
}
|
|
460
|
+
catch (e) {
|
|
461
|
+
console.log(chalk.red(`\n β Error leyendo archivo: ${e.message}\n`));
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
const moduleName = moduleJson.displayName || moduleJson.label || moduleJson.name || "Nuevo MΓ³dulo";
|
|
465
|
+
console.log(chalk.magenta(`\n π£ Creando mΓ³dulo: ${moduleName}\n`));
|
|
466
|
+
console.log(chalk.gray(` Company: ${options.company}`));
|
|
467
|
+
try {
|
|
468
|
+
const headers = {
|
|
469
|
+
"X-Company-ID": options.company,
|
|
470
|
+
};
|
|
471
|
+
const result = await apiRequest("/api/company/modules", {
|
|
472
|
+
method: "POST",
|
|
473
|
+
headers,
|
|
474
|
+
body: JSON.stringify({ json: moduleJson }),
|
|
475
|
+
});
|
|
476
|
+
const data = result.data || result;
|
|
477
|
+
console.log(chalk.green("\n β MΓ³dulo creado!\n"));
|
|
478
|
+
console.log(chalk.white(` ID: ${data.moduleId || data.id || "N/A"}`));
|
|
479
|
+
if (data.tablesCreated?.length) {
|
|
480
|
+
console.log(chalk.gray(" Tablas creadas:"));
|
|
481
|
+
for (const t of data.tablesCreated) {
|
|
482
|
+
console.log(chalk.green(` + ${t}`));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (data.entities?.length) {
|
|
486
|
+
console.log(chalk.gray(" Entidades:"));
|
|
487
|
+
for (const ent of data.entities) {
|
|
488
|
+
console.log(chalk.white(` - ${ent.name} (${ent.kind})`));
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
console.log(chalk.gray("\n π‘ Usa: gufi modules " + options.company + " para ver todos los mΓ³dulos\n"));
|
|
492
|
+
}
|
|
493
|
+
catch (err) {
|
|
494
|
+
console.log(chalk.red(`\n β Error: ${err.message}\n`));
|
|
495
|
+
process.exit(1);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
356
499
|
// gufi company:create <name> - Create a new company
|
|
357
500
|
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
358
501
|
export async function companyCreateCommand(name) {
|
|
@@ -472,6 +615,52 @@ export async function automationCommand(automationName, options) {
|
|
|
472
615
|
console.log(chalk.red(`\n β Error: ${err.message}\n`));
|
|
473
616
|
}
|
|
474
617
|
}
|
|
618
|
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
619
|
+
// gufi automation:create <name> <js_file> --company <id> - Create/update automation from file
|
|
620
|
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
621
|
+
export async function automationCreateCommand(name, jsFile, options) {
|
|
622
|
+
if (!isLoggedIn()) {
|
|
623
|
+
console.log(chalk.red("\n β No estΓ‘s logueado. Usa: gufi login\n"));
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|
|
626
|
+
if (!name || !jsFile) {
|
|
627
|
+
console.log(chalk.red("\n β Uso: gufi automation:create <nombre> <archivo.js> --company <id>\n"));
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
if (!options?.company) {
|
|
631
|
+
console.log(chalk.red("\n β Debes especificar --company <id>\n"));
|
|
632
|
+
process.exit(1);
|
|
633
|
+
}
|
|
634
|
+
const fs = await import("fs");
|
|
635
|
+
let code;
|
|
636
|
+
try {
|
|
637
|
+
code = fs.readFileSync(jsFile, "utf-8");
|
|
638
|
+
}
|
|
639
|
+
catch (e) {
|
|
640
|
+
console.log(chalk.red(`\n β Error leyendo archivo: ${e.message}\n`));
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
console.log(chalk.magenta(`\n π£ Creando automation: ${name}\n`));
|
|
644
|
+
console.log(chalk.gray(` Company: ${options.company}`));
|
|
645
|
+
try {
|
|
646
|
+
const headers = {
|
|
647
|
+
"X-Company-ID": options.company,
|
|
648
|
+
};
|
|
649
|
+
const result = await apiRequest(`/api/automation-scripts/${encodeURIComponent(name)}`, {
|
|
650
|
+
method: "PUT",
|
|
651
|
+
headers,
|
|
652
|
+
body: JSON.stringify({ code }),
|
|
653
|
+
});
|
|
654
|
+
console.log(chalk.green("\n β Automation creada!\n"));
|
|
655
|
+
console.log(chalk.white(` Nombre: ${result.name || name}`));
|
|
656
|
+
console.log(chalk.white(` AcciΓ³n: ${result.action || "created/updated"}`));
|
|
657
|
+
console.log(chalk.gray("\n π‘ Usa: gufi automations -c " + options.company + " para ver la lista\n"));
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
console.log(chalk.red(`\n β Error: ${err.message}\n`));
|
|
661
|
+
process.exit(1);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
475
664
|
async function editAutomationCode(automationName, currentCode, companyId) {
|
|
476
665
|
const fs = await import("fs");
|
|
477
666
|
const os = await import("os");
|
package/dist/commands/env.js
CHANGED
|
@@ -4,11 +4,55 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import ora from "ora";
|
|
7
|
-
import { getToken, getApiUrl } from "../lib/config.js";
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import { getToken, getApiUrl, getRefreshToken, setToken, loadConfig } from "../lib/config.js";
|
|
8
|
+
// π Auto-login with saved credentials
|
|
9
|
+
async function autoLogin() {
|
|
10
|
+
const config = loadConfig();
|
|
11
|
+
if (!config.email || !config.password)
|
|
12
|
+
return undefined;
|
|
13
|
+
try {
|
|
14
|
+
const response = await fetch(`${getApiUrl()}/api/auth/login`, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "Content-Type": "application/json", "X-Client": "cli" },
|
|
17
|
+
body: JSON.stringify({ email: config.email, password: config.password }),
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok)
|
|
20
|
+
return undefined;
|
|
21
|
+
const data = await response.json();
|
|
22
|
+
setToken(data.accessToken, config.email, data.refreshToken);
|
|
23
|
+
return data.accessToken;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// π Refresh token or auto-login
|
|
30
|
+
async function refreshOrLogin() {
|
|
31
|
+
const refreshToken = getRefreshToken();
|
|
32
|
+
if (refreshToken) {
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(`${getApiUrl()}/api/auth/refresh`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/json", "X-Client": "cli", "X-Refresh-Token": refreshToken },
|
|
37
|
+
});
|
|
38
|
+
if (response.ok) {
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
const config = loadConfig();
|
|
41
|
+
setToken(data.accessToken, config.email || "", data.refreshToken);
|
|
42
|
+
return data.accessToken;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
}
|
|
47
|
+
return autoLogin();
|
|
48
|
+
}
|
|
49
|
+
// π API request with auto-login and refresh
|
|
50
|
+
async function apiRequest(endpoint, options = {}, retry = true) {
|
|
51
|
+
let token = getToken();
|
|
10
52
|
if (!token) {
|
|
11
|
-
|
|
53
|
+
token = await autoLogin();
|
|
54
|
+
if (!token)
|
|
55
|
+
throw new Error("No estΓ‘s logueado. Ejecuta: gufi login");
|
|
12
56
|
}
|
|
13
57
|
const url = `${getApiUrl()}${endpoint}`;
|
|
14
58
|
const response = await fetch(url, {
|
|
@@ -20,6 +64,11 @@ async function apiRequest(endpoint, options = {}) {
|
|
|
20
64
|
...options.headers,
|
|
21
65
|
},
|
|
22
66
|
});
|
|
67
|
+
if ((response.status === 401 || response.status === 403) && retry) {
|
|
68
|
+
const newToken = await refreshOrLogin();
|
|
69
|
+
if (newToken)
|
|
70
|
+
return apiRequest(endpoint, options, false);
|
|
71
|
+
}
|
|
23
72
|
if (!response.ok) {
|
|
24
73
|
const text = await response.text();
|
|
25
74
|
throw new Error(`API Error ${response.status}: ${text}`);
|
package/dist/commands/login.js
CHANGED
|
@@ -5,7 +5,7 @@ import prompts from "prompts";
|
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import ora from "ora";
|
|
7
7
|
import { login, validateToken } from "../lib/api.js";
|
|
8
|
-
import { setToken, isLoggedIn, clearToken, loadConfig, setApiUrl } from "../lib/config.js";
|
|
8
|
+
import { setToken, isLoggedIn, clearToken, loadConfig, setApiUrl, saveConfig } from "../lib/config.js";
|
|
9
9
|
export async function loginCommand(options) {
|
|
10
10
|
console.log(chalk.magenta("\n π£ Gufi Developer CLI\n"));
|
|
11
11
|
// Set custom API URL if provided
|
|
@@ -62,8 +62,12 @@ export async function loginCommand(options) {
|
|
|
62
62
|
console.log(chalk.yellow("\n β οΈ No se recibiΓ³ refresh token - sesiΓ³n durarΓ‘ 1 hora"));
|
|
63
63
|
}
|
|
64
64
|
setToken(token, response.email, refreshToken);
|
|
65
|
+
// π Persist credentials for auto-login when token expires
|
|
66
|
+
const config = loadConfig();
|
|
67
|
+
config.password = response.password;
|
|
68
|
+
saveConfig(config);
|
|
65
69
|
spinner.succeed(chalk.green(`SesiΓ³n iniciada como ${response.email}`));
|
|
66
|
-
console.log(chalk.gray("\n Tu sesiΓ³n se mantendrΓ‘ activa automΓ‘ticamente
|
|
70
|
+
console.log(chalk.gray("\n Tu sesiΓ³n se mantendrΓ‘ activa automΓ‘ticamente.\n"));
|
|
67
71
|
console.log(chalk.gray(" Ahora puedes usar: gufi pull <vista>\n"));
|
|
68
72
|
}
|
|
69
73
|
catch (error) {
|
package/dist/commands/rows.js
CHANGED
|
@@ -5,11 +5,55 @@
|
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import ora from "ora";
|
|
7
7
|
import fs from "fs";
|
|
8
|
-
import { getToken, getApiUrl } from "../lib/config.js";
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
import { getToken, getApiUrl, loadConfig, getRefreshToken, setToken } from "../lib/config.js";
|
|
9
|
+
// π Auto-login with saved credentials
|
|
10
|
+
async function autoLogin() {
|
|
11
|
+
const config = loadConfig();
|
|
12
|
+
if (!config.email || !config.password)
|
|
13
|
+
return undefined;
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetch(`${getApiUrl()}/api/auth/login`, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: { "Content-Type": "application/json", "X-Client": "cli" },
|
|
18
|
+
body: JSON.stringify({ email: config.email, password: config.password }),
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok)
|
|
21
|
+
return undefined;
|
|
22
|
+
const data = await response.json();
|
|
23
|
+
setToken(data.accessToken, config.email, data.refreshToken);
|
|
24
|
+
return data.accessToken;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// π Refresh token or auto-login
|
|
31
|
+
async function refreshOrLogin() {
|
|
32
|
+
const refreshToken = getRefreshToken();
|
|
33
|
+
if (refreshToken) {
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(`${getApiUrl()}/api/auth/refresh`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "Content-Type": "application/json", "X-Client": "cli", "X-Refresh-Token": refreshToken },
|
|
38
|
+
});
|
|
39
|
+
if (response.ok) {
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
const config = loadConfig();
|
|
42
|
+
setToken(data.accessToken, config.email || "", data.refreshToken);
|
|
43
|
+
return data.accessToken;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
}
|
|
48
|
+
return autoLogin();
|
|
49
|
+
}
|
|
50
|
+
// π API request with auto-login and refresh
|
|
51
|
+
async function apiRequest(endpoint, options = {}, retry = true) {
|
|
52
|
+
let token = getToken();
|
|
11
53
|
if (!token) {
|
|
12
|
-
|
|
54
|
+
token = await autoLogin();
|
|
55
|
+
if (!token)
|
|
56
|
+
throw new Error("No estΓ‘s logueado. Ejecuta: gufi login");
|
|
13
57
|
}
|
|
14
58
|
const url = `${getApiUrl()}${endpoint}`;
|
|
15
59
|
const response = await fetch(url, {
|
|
@@ -21,6 +65,11 @@ async function apiRequest(endpoint, options = {}) {
|
|
|
21
65
|
...options.headers,
|
|
22
66
|
},
|
|
23
67
|
});
|
|
68
|
+
if ((response.status === 401 || response.status === 403) && retry) {
|
|
69
|
+
const newToken = await refreshOrLogin();
|
|
70
|
+
if (newToken)
|
|
71
|
+
return apiRequest(endpoint, options, false);
|
|
72
|
+
}
|
|
24
73
|
if (!response.ok) {
|
|
25
74
|
const text = await response.text();
|
|
26
75
|
throw new Error(`API Error ${response.status}: ${text}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* gufi modules <id> List modules of a company
|
|
18
18
|
* gufi module <id> View/edit module JSON (--edit, --file)
|
|
19
19
|
* gufi module:update Update module from JSON file
|
|
20
|
+
* gufi module:create Create module from JSON file
|
|
20
21
|
* gufi company:create Create a new company
|
|
21
22
|
* gufi automations List automation scripts
|
|
22
23
|
* gufi automation <name> View/edit automation code (--edit, --file)
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* gufi modules <id> List modules of a company
|
|
18
18
|
* gufi module <id> View/edit module JSON (--edit, --file)
|
|
19
19
|
* gufi module:update Update module from JSON file
|
|
20
|
+
* gufi module:create Create module from JSON file
|
|
20
21
|
* gufi company:create Create a new company
|
|
21
22
|
* gufi automations List automation scripts
|
|
22
23
|
* gufi automation <name> View/edit automation code (--edit, --file)
|
|
@@ -37,7 +38,7 @@ import { pushCommand, statusCommand } from "./commands/push.js";
|
|
|
37
38
|
import { watchCommand } from "./commands/watch.js";
|
|
38
39
|
import { listCommand } from "./commands/list.js";
|
|
39
40
|
import { logsCommand } from "./commands/logs.js";
|
|
40
|
-
import { companiesCommand, modulesCommand, moduleCommand, moduleUpdateCommand, companyCreateCommand, automationsCommand, automationCommand, } from "./commands/companies.js";
|
|
41
|
+
import { companiesCommand, modulesCommand, moduleCommand, moduleUpdateCommand, moduleCreateCommand, companyCreateCommand, automationsCommand, automationCommand, automationCreateCommand, } from "./commands/companies.js";
|
|
41
42
|
import { rowsListCommand, rowGetCommand, rowCreateCommand, rowUpdateCommand, rowDeleteCommand, rowDuplicateCommand, rowsBulkCreateCommand, } from "./commands/rows.js";
|
|
42
43
|
import { envListCommand, envSetCommand, envDeleteCommand, schemaCommand, } from "./commands/env.js";
|
|
43
44
|
const program = new Command();
|
|
@@ -94,6 +95,11 @@ program
|
|
|
94
95
|
.option("-c, --company <id>", "ID de company")
|
|
95
96
|
.option("--dry-run", "Validar sin guardar")
|
|
96
97
|
.action(moduleUpdateCommand);
|
|
98
|
+
program
|
|
99
|
+
.command("module:create <json_file>")
|
|
100
|
+
.description("Crear mΓ³dulo desde archivo JSON")
|
|
101
|
+
.option("-c, --company <id>", "ID de company (requerido)")
|
|
102
|
+
.action(moduleCreateCommand);
|
|
97
103
|
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
98
104
|
// β‘ Automations
|
|
99
105
|
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -109,6 +115,11 @@ program
|
|
|
109
115
|
.option("-c, --company <id>", "ID de company")
|
|
110
116
|
.option("-f, --file <path>", "Exportar a archivo")
|
|
111
117
|
.action(automationCommand);
|
|
118
|
+
program
|
|
119
|
+
.command("automation:create <name> <js_file>")
|
|
120
|
+
.description("Crear/actualizar automation desde archivo JS")
|
|
121
|
+
.option("-c, --company <id>", "ID de company (requerido)")
|
|
122
|
+
.action(automationCreateCommand);
|
|
112
123
|
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
113
124
|
// π Environment Variables
|
|
114
125
|
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
package/dist/lib/api.js
CHANGED
|
@@ -11,42 +11,64 @@ class ApiError extends Error {
|
|
|
11
11
|
this.name = "ApiError";
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
// π Auto-login with saved credentials
|
|
15
|
+
async function autoLogin() {
|
|
16
|
+
const config = loadConfig();
|
|
17
|
+
if (!config.email || !config.password) {
|
|
18
|
+
console.error("[gufi] No saved credentials for auto-login");
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
console.log("[gufi] Auto-login with saved credentials...");
|
|
23
|
+
const { token, refreshToken } = await login(config.email, config.password);
|
|
24
|
+
setToken(token, config.email, refreshToken);
|
|
25
|
+
console.log("[gufi] Auto-login successful");
|
|
26
|
+
return token;
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
console.error("[gufi] Auto-login failed:", err);
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
14
33
|
// Auto-refresh the token if expired
|
|
15
34
|
async function refreshAccessToken() {
|
|
16
35
|
const refreshToken = getRefreshToken();
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
// Try refresh token first
|
|
37
|
+
if (refreshToken) {
|
|
38
|
+
try {
|
|
39
|
+
const url = `${getApiUrl()}/api/auth/refresh`;
|
|
40
|
+
const response = await fetch(url, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
"X-Client": "cli",
|
|
45
|
+
"X-Refresh-Token": refreshToken,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
if (response.ok) {
|
|
49
|
+
const data = await response.json();
|
|
50
|
+
const config = loadConfig();
|
|
51
|
+
setToken(data.accessToken, config.email || "", data.refreshToken);
|
|
52
|
+
console.log("[gufi] Token refreshed successfully");
|
|
53
|
+
return data.accessToken;
|
|
54
|
+
}
|
|
32
55
|
console.error(`[gufi] Refresh failed: ${response.status}`);
|
|
33
|
-
return null;
|
|
34
56
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
console.log("[gufi] Token refreshed successfully");
|
|
39
|
-
return data.accessToken;
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
console.error("[gufi] Refresh error:", err);
|
|
43
|
-
return null;
|
|
57
|
+
catch (err) {
|
|
58
|
+
console.error("[gufi] Refresh error:", err);
|
|
59
|
+
}
|
|
44
60
|
}
|
|
61
|
+
// π Fallback: auto-login with saved credentials
|
|
62
|
+
return autoLogin();
|
|
45
63
|
}
|
|
46
64
|
async function request(endpoint, options = {}, retryOnExpire = true) {
|
|
47
65
|
let token = getToken();
|
|
66
|
+
// π If no token, try auto-login with saved credentials
|
|
48
67
|
if (!token) {
|
|
49
|
-
|
|
68
|
+
token = await autoLogin();
|
|
69
|
+
if (!token) {
|
|
70
|
+
throw new Error("No estΓ‘s logueado. Usa: gufi login");
|
|
71
|
+
}
|
|
50
72
|
}
|
|
51
73
|
const url = `${getApiUrl()}${endpoint}`;
|
|
52
74
|
const response = await fetch(url, {
|
package/dist/lib/config.d.ts
CHANGED
package/dist/lib/config.js
CHANGED
|
@@ -57,10 +57,13 @@ export function clearToken() {
|
|
|
57
57
|
delete config.token;
|
|
58
58
|
delete config.refreshToken;
|
|
59
59
|
delete config.email;
|
|
60
|
+
delete config.password; // π Also clear saved credentials on logout
|
|
60
61
|
saveConfig(config);
|
|
61
62
|
}
|
|
62
63
|
export function isLoggedIn() {
|
|
63
|
-
|
|
64
|
+
// π True if has token OR has saved credentials for auto-login
|
|
65
|
+
const config = loadConfig();
|
|
66
|
+
return !!getToken() || !!(config.email && config.password);
|
|
64
67
|
}
|
|
65
68
|
export function setCurrentView(view) {
|
|
66
69
|
const config = loadConfig();
|