gufi-cli 0.1.8 → 0.1.10

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 CHANGED
@@ -604,10 +604,10 @@ SELECT id, name, code FROM marketplace.views WHERE package_id = 14;
604
604
 
605
605
  ### Estructura de Archivos de una View
606
606
 
607
- Cuando descargas una view con `gufi pull`, obtienes:
607
+ Cuando descargas una view con `gufi view:pull <id>`, obtienes:
608
608
 
609
609
  ```
610
- ~/gufi-dev/mi-vista/
610
+ ~/gufi-dev/view_<id>/
611
611
  ├── index.tsx # Entry point - exporta featureConfig y default
612
612
  ├── types.ts # Interfaces TypeScript
613
613
 
@@ -1032,6 +1032,46 @@ export default function MiVista({ gufi }) {
1032
1032
  - Permisos dinámicos expandibles (ej: warehouses individuales)
1033
1033
  - Integridad total: UI siempre refleja el estado real
1034
1034
 
1035
+ ### 💜 DevPermissionSwitcher AUTOMÁTICO (LivePreviewPage)
1036
+
1037
+ **¡Ya no necesitas incluir DevPermissionSwitcher en tu código!** LivePreviewPage detecta automáticamente si tu vista tiene `featureConfig.permissions` y muestra el botón DEV.
1038
+
1039
+ **Cómo funciona:**
1040
+ 1. Declara permisos en `metadata/permissions.ts`
1041
+ 2. Expórtalos en `featureConfig`:
1042
+ ```typescript
1043
+ // core/dataProvider.ts
1044
+ import { permissions } from '../metadata/permissions';
1045
+
1046
+ export const featureConfig = {
1047
+ dataSources,
1048
+ inputs: featureInputs,
1049
+ permissions, // 💜 Solo añadir esto
1050
+ };
1051
+ ```
1052
+ 3. LivePreviewPage lo detecta y muestra el botón DEV automáticamente
1053
+
1054
+ **Usar devPermissions del contexto:**
1055
+ ```typescript
1056
+ export default function MiVista({ gufi }) {
1057
+ // 💜 LivePreviewPage provee devPermissions automáticamente
1058
+ const effectiveDevPermissions = gufi?.context?.devPermissions || [];
1059
+
1060
+ const permissionsConfig = {
1061
+ userPermissions: gufi?.context?.userPermissions || [],
1062
+ isDevMode: gufi?.context?.isPreview || gufi?.context?.isDev,
1063
+ devPermissions: effectiveDevPermissions,
1064
+ };
1065
+
1066
+ // Usar helpers de permisos normalmente
1067
+ const canSend = hasPermission(permissionsConfig, 'send_orders');
1068
+ }
1069
+ ```
1070
+
1071
+ **Cuándo incluir DevPermissionSwitcher manualmente:**
1072
+ - Solo si la vista se ejecuta fuera de LivePreviewPage (ej: vista nativa del frontend)
1073
+ - Para vistas del marketplace en Developer Center → **NO necesario**, es automático
1074
+
1035
1075
  **Principio clave**: Inicializar con `[]` vacío, luego usar `useEffect` para otorgar todos los permisos cuando los valores dinámicos (como warehouses) terminen de cargar. Esto garantiza integridad UI.
1036
1076
 
1037
1077
  ---
@@ -1529,6 +1569,28 @@ npm install -g gufi-cli
1529
1569
  gufi login
1530
1570
  ```
1531
1571
 
1572
+ ### 💜 Autenticación Persistente
1573
+
1574
+ El CLI guarda las credenciales en `~/.gufi/config.json` para auto-login automático:
1575
+
1576
+ ```json
1577
+ {
1578
+ "apiUrl": "https://gogufi.com",
1579
+ "token": "eyJ...",
1580
+ "refreshToken": "eyJ...",
1581
+ "email": "user@example.com",
1582
+ "password": "secreto"
1583
+ }
1584
+ ```
1585
+
1586
+ **Flujo de autenticación:**
1587
+ 1. Si hay token válido → lo usa
1588
+ 2. Si token expirado + refreshToken válido → hace refresh automático
1589
+ 3. Si ambos expirados → auto-login con email/password guardados
1590
+ 4. Si no hay credenciales → pide `gufi login`
1591
+
1592
+ **Importante:** Para persistencia completa, `gufi login` guarda email y password. Si haces login desde el navegador web, el refreshToken del CLI se invalida, pero el CLI hace auto-login automáticamente con las credenciales guardadas.
1593
+
1532
1594
  ### Gestión de Companies y Módulos
1533
1595
 
1534
1596
  ```bash
@@ -1629,11 +1691,13 @@ gufi rows:create m360_t16192 --file datos.json
1629
1691
  ### Desarrollo de Views
1630
1692
 
1631
1693
  ```bash
1632
- # Ver tus views del Marketplace
1694
+ # Ver tus views del Marketplace (muestra ID de cada vista)
1633
1695
  gufi views
1696
+ # 📦 Gestión de Tareas (ID: 14)
1697
+ # └─ 13 Tasks Manager (custom)
1634
1698
 
1635
- # Descargar view para editar localmente
1636
- gufi view:pull "Stock Overview"
1699
+ # Descargar view por ID (se guarda en ~/gufi-dev/view_<id>/)
1700
+ gufi view:pull 13
1637
1701
 
1638
1702
  # Auto-sync al guardar archivos
1639
1703
  gufi view:watch
@@ -1670,15 +1734,15 @@ gufi view:status
1670
1734
  | Ver estructura de una company | `gufi modules <company_id>` |
1671
1735
  | Ver/editar JSON de módulo | `gufi module <id> -c <company>` |
1672
1736
  | Ver/editar código de automation | `gufi automation <nombre> -c <company>` |
1673
- | Desarrollar una vista | `gufi pull`, `gufi watch`, `gufi logs` |
1737
+ | Desarrollar una vista | `gufi view:pull <id>`, `gufi view:watch`, `gufi view:logs` |
1674
1738
 
1675
1739
  ### Errores comunes
1676
1740
 
1677
1741
  | Error | Solución |
1678
1742
  |-------|----------|
1679
- | "No estás logueado" | `gufi login` |
1743
+ | "No estás logueado" | `gufi login` (guarda credenciales para auto-login) |
1680
1744
  | "Módulo no encontrado" | Verificar `-c <company_id>` |
1681
- | "Token expirado" | `gufi login` de nuevo |
1745
+ | "Token expirado" | Automático: refresh o auto-login con credenciales guardadas |
1682
1746
  | "JSON inválido" | Validar estructura del JSON |
1683
1747
 
1684
1748
  ### 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
- async function apiRequest(path, options = {}) {
10
- const token = getToken();
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({ json_definition: newJson }),
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,19 +368,10 @@ export async function moduleUpdateCommand(moduleId, jsonFile, options) {
306
368
  if (options?.company) {
307
369
  headers["X-Company-ID"] = options.company;
308
370
  }
309
- // Validate first
371
+ // Validate JSON structure locally
310
372
  console.log(chalk.gray(" Validando JSON..."));
311
- const validateResult = await apiRequest(`/api/modules/${moduleId}/validate`, {
312
- method: "POST",
313
- headers,
314
- body: JSON.stringify({ json_definition: newJson }),
315
- });
316
- if (validateResult.errors?.length) {
317
- console.log(chalk.red("\n ✗ Errores de validación:"));
318
- for (const err of validateResult.errors) {
319
- console.log(chalk.red(` - ${err}`));
320
- }
321
- console.log();
373
+ if (!newJson.name && !newJson.displayName) {
374
+ console.log(chalk.red("\n ✗ JSON debe tener 'name' o 'displayName'\n"));
322
375
  process.exit(1);
323
376
  }
324
377
  console.log(chalk.green(" ✓ JSON válido"));
@@ -326,12 +379,12 @@ export async function moduleUpdateCommand(moduleId, jsonFile, options) {
326
379
  console.log(chalk.gray("\n [DRY RUN] Validación exitosa. Usa sin --dry-run para aplicar.\n"));
327
380
  return;
328
381
  }
329
- // Apply changes
382
+ // Apply changes (confirm: true to actually save)
330
383
  console.log(chalk.gray(" Aplicando cambios..."));
331
- const result = await apiRequest(`/api/modules/${moduleId}`, {
384
+ const result = await apiRequest(`/api/company/modules/${moduleId}`, {
332
385
  method: "PUT",
333
386
  headers,
334
- body: JSON.stringify({ json_definition: newJson }),
387
+ body: JSON.stringify({ json: newJson, confirm: true }),
335
388
  });
336
389
  console.log(chalk.green("\n ✓ Módulo actualizado correctamente!\n"));
337
390
  if (result.tablesCreated?.length) {
@@ -353,6 +406,68 @@ export async function moduleUpdateCommand(moduleId, jsonFile, options) {
353
406
  }
354
407
  }
355
408
  // ════════════════════════════════════════════════════════════════════
409
+ // gufi module:create <json_file> --company <id> - Create module from JSON file
410
+ // ════════════════════════════════════════════════════════════════════
411
+ export async function moduleCreateCommand(jsonFile, options) {
412
+ if (!isLoggedIn()) {
413
+ console.log(chalk.red("\n ✗ No estás logueado. Usa: gufi login\n"));
414
+ process.exit(1);
415
+ }
416
+ if (!jsonFile) {
417
+ console.log(chalk.red("\n ✗ Uso: gufi module:create <json_file> --company <id>\n"));
418
+ process.exit(1);
419
+ }
420
+ if (!options?.company) {
421
+ console.log(chalk.red("\n ✗ Debes especificar --company <id>\n"));
422
+ console.log(chalk.gray(" Tip: Usa 'gufi companies' para ver tus companies\n"));
423
+ process.exit(1);
424
+ }
425
+ const fs = await import("fs");
426
+ // Read JSON file
427
+ let moduleJson;
428
+ try {
429
+ const content = fs.readFileSync(jsonFile, "utf-8");
430
+ moduleJson = JSON.parse(content);
431
+ }
432
+ catch (e) {
433
+ console.log(chalk.red(`\n ✗ Error leyendo archivo: ${e.message}\n`));
434
+ process.exit(1);
435
+ }
436
+ const moduleName = moduleJson.displayName || moduleJson.label || moduleJson.name || "Nuevo Módulo";
437
+ console.log(chalk.magenta(`\n 🟣 Creando módulo: ${moduleName}\n`));
438
+ console.log(chalk.gray(` Company: ${options.company}`));
439
+ try {
440
+ const headers = {
441
+ "X-Company-ID": options.company,
442
+ };
443
+ const result = await apiRequest("/api/company/modules", {
444
+ method: "POST",
445
+ headers,
446
+ body: JSON.stringify({ json: moduleJson }),
447
+ });
448
+ const data = result.data || result;
449
+ console.log(chalk.green("\n ✓ Módulo creado!\n"));
450
+ console.log(chalk.white(` ID: ${data.moduleId || data.id || "N/A"}`));
451
+ if (data.tablesCreated?.length) {
452
+ console.log(chalk.gray(" Tablas creadas:"));
453
+ for (const t of data.tablesCreated) {
454
+ console.log(chalk.green(` + ${t}`));
455
+ }
456
+ }
457
+ if (data.entities?.length) {
458
+ console.log(chalk.gray(" Entidades:"));
459
+ for (const ent of data.entities) {
460
+ console.log(chalk.white(` - ${ent.name} (${ent.kind})`));
461
+ }
462
+ }
463
+ console.log(chalk.gray("\n 💡 Usa: gufi modules " + options.company + " para ver todos los módulos\n"));
464
+ }
465
+ catch (err) {
466
+ console.log(chalk.red(`\n ✗ Error: ${err.message}\n`));
467
+ process.exit(1);
468
+ }
469
+ }
470
+ // ════════════════════════════════════════════════════════════════════
356
471
  // gufi company:create <name> - Create a new company
357
472
  // ════════════════════════════════════════════════════════════════════
358
473
  export async function companyCreateCommand(name) {
@@ -370,11 +485,12 @@ export async function companyCreateCommand(name) {
370
485
  method: "POST",
371
486
  body: JSON.stringify({ name }),
372
487
  });
373
- const company = result.data || result;
488
+ // API returns { company: {...}, message: "..." }
489
+ const company = result.company || result.data || result;
374
490
  console.log(chalk.green(" ✓ Company creada!\n"));
375
491
  console.log(chalk.white(` ID: ${company.id}`));
376
492
  console.log(chalk.white(` Nombre: ${company.name}`));
377
- console.log(chalk.white(` Schema: company_${company.id}`));
493
+ console.log(chalk.white(` Schema: ${company.schema || 'company_' + company.id}`));
378
494
  console.log(chalk.gray("\n 💡 Usa: gufi modules " + company.id + " para ver módulos\n"));
379
495
  }
380
496
  catch (err) {
@@ -471,6 +587,52 @@ export async function automationCommand(automationName, options) {
471
587
  console.log(chalk.red(`\n ✗ Error: ${err.message}\n`));
472
588
  }
473
589
  }
590
+ // ════════════════════════════════════════════════════════════════════
591
+ // gufi automation:create <name> <js_file> --company <id> - Create/update automation from file
592
+ // ════════════════════════════════════════════════════════════════════
593
+ export async function automationCreateCommand(name, jsFile, options) {
594
+ if (!isLoggedIn()) {
595
+ console.log(chalk.red("\n ✗ No estás logueado. Usa: gufi login\n"));
596
+ process.exit(1);
597
+ }
598
+ if (!name || !jsFile) {
599
+ console.log(chalk.red("\n ✗ Uso: gufi automation:create <nombre> <archivo.js> --company <id>\n"));
600
+ process.exit(1);
601
+ }
602
+ if (!options?.company) {
603
+ console.log(chalk.red("\n ✗ Debes especificar --company <id>\n"));
604
+ process.exit(1);
605
+ }
606
+ const fs = await import("fs");
607
+ let code;
608
+ try {
609
+ code = fs.readFileSync(jsFile, "utf-8");
610
+ }
611
+ catch (e) {
612
+ console.log(chalk.red(`\n ✗ Error leyendo archivo: ${e.message}\n`));
613
+ process.exit(1);
614
+ }
615
+ console.log(chalk.magenta(`\n 🟣 Creando automation: ${name}\n`));
616
+ console.log(chalk.gray(` Company: ${options.company}`));
617
+ try {
618
+ const headers = {
619
+ "X-Company-ID": options.company,
620
+ };
621
+ const result = await apiRequest(`/api/automation-scripts/${encodeURIComponent(name)}`, {
622
+ method: "PUT",
623
+ headers,
624
+ body: JSON.stringify({ code }),
625
+ });
626
+ console.log(chalk.green("\n ✓ Automation creada!\n"));
627
+ console.log(chalk.white(` Nombre: ${result.name || name}`));
628
+ console.log(chalk.white(` Acción: ${result.action || "created/updated"}`));
629
+ console.log(chalk.gray("\n 💡 Usa: gufi automations -c " + options.company + " para ver la lista\n"));
630
+ }
631
+ catch (err) {
632
+ console.log(chalk.red(`\n ✗ Error: ${err.message}\n`));
633
+ process.exit(1);
634
+ }
635
+ }
474
636
  async function editAutomationCode(automationName, currentCode, companyId) {
475
637
  const fs = await import("fs");
476
638
  const os = await import("os");
@@ -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
- async function apiRequest(endpoint, options = {}) {
9
- const token = getToken();
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
- throw new Error("No estás logueado. Ejecuta: gufi login");
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}`);
@@ -29,11 +29,11 @@ export async function listCommand() {
29
29
  else {
30
30
  views.forEach((view, i) => {
31
31
  const prefix = i === views.length - 1 ? "└─" : "├─";
32
- console.log(chalk.white(` ${prefix} ${view.name}`) + chalk.gray(` (${view.view_type}, ID: ${view.pk_id})`));
32
+ console.log(` ${prefix} ${chalk.cyan(view.pk_id)} ${view.name} ${chalk.gray(`(${view.view_type})`)}`);
33
33
  });
34
34
  }
35
35
  }
36
- console.log(chalk.gray("\n Usa: gufi pull <view-id> para descargar una vista\n"));
36
+ console.log(chalk.gray("\n Usa: ") + chalk.cyan("gufi view:pull <id>") + chalk.gray(" para descargar una vista\n"));
37
37
  }
38
38
  catch (error) {
39
39
  spinner.fail(chalk.red(error.message));
@@ -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 (7 días).\n"));
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) {
@@ -13,17 +13,17 @@ export async function pullCommand(viewIdentifier) {
13
13
  }
14
14
  console.log(chalk.magenta("\n 🟣 Gufi Pull\n"));
15
15
  let viewId;
16
- let viewName;
16
+ let viewKey;
17
17
  let packageId;
18
- // If view ID provided directly
18
+ // 💜 Solo acepta ID numérico
19
19
  if (viewIdentifier && /^\d+$/.test(viewIdentifier)) {
20
20
  viewId = parseInt(viewIdentifier);
21
21
  const spinner = ora("Obteniendo vista...").start();
22
22
  try {
23
23
  const view = await getView(viewId);
24
- viewName = view.name || `vista-${viewId}`;
24
+ viewKey = `view_${viewId}`;
25
25
  packageId = view.package_id;
26
- spinner.succeed(`Vista: ${viewName}`);
26
+ spinner.succeed(`Vista ${chalk.cyan(viewId)}: ${view.name || "sin nombre"}`);
27
27
  }
28
28
  catch (error) {
29
29
  spinner.fail(chalk.red(`No se encontró la vista ${viewId}`));
@@ -56,22 +56,14 @@ export async function pullCommand(viewIdentifier) {
56
56
  process.exit(0);
57
57
  }
58
58
  console.log(chalk.gray(" Vistas disponibles:\n"));
59
- views.forEach((view, i) => {
60
- const name = view.name || `Vista ${view.pk_id}`;
61
- console.log(` ${chalk.cyan(i + 1)}. ${name} ${chalk.gray(`(${view.view_type})`)}`);
59
+ views.forEach((view) => {
60
+ console.log(` ${chalk.cyan(view.pk_id)} ${view.name || "sin nombre"} ${chalk.gray(`(${view.view_type})`)}`);
62
61
  });
63
- // If viewIdentifier matches a name
64
- let selectedView = viewIdentifier
65
- ? views.find(v => v.name && v.name.toLowerCase().includes(viewIdentifier.toLowerCase()))
66
- : views[0];
67
- if (!selectedView) {
68
- console.log(chalk.yellow(`\n No se encontró vista "${viewIdentifier}"\n`));
69
- console.log(chalk.gray(" Uso: gufi pull <nombre-vista> o gufi pull <view-id>\n"));
70
- process.exit(1);
62
+ if (viewIdentifier) {
63
+ console.log(chalk.yellow(`\n "${viewIdentifier}" no es un ID válido.\n`));
71
64
  }
72
- viewId = selectedView.pk_id;
73
- viewName = selectedView.name || `vista-${selectedView.pk_id}`;
74
- console.log(chalk.gray(`\n Descargando: ${viewName}\n`));
65
+ console.log(chalk.gray("\n Uso: ") + chalk.cyan("gufi view:pull <id>") + chalk.gray(" (ej: gufi view:pull 13)\n"));
66
+ process.exit(0);
75
67
  }
76
68
  catch (error) {
77
69
  spinner.fail(chalk.red(error.message));
@@ -81,7 +73,7 @@ export async function pullCommand(viewIdentifier) {
81
73
  // Pull the view
82
74
  const pullSpinner = ora("Descargando archivos...").start();
83
75
  try {
84
- const result = await pullView(viewId, viewName, packageId);
76
+ const result = await pullView(viewId, viewKey, packageId);
85
77
  pullSpinner.succeed(chalk.green(`${result.fileCount} archivos descargados`));
86
78
  console.log(chalk.gray(`\n 📁 ${result.dir}\n`));
87
79
  console.log(chalk.gray(" Comandos útiles:"));
@@ -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
- async function apiRequest(endpoint, options = {}) {
10
- const token = getToken();
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
- throw new Error("No estás logueado. Ejecuta: gufi login");
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,14 +38,14 @@ 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();
44
45
  program
45
46
  .name("gufi")
46
47
  .description("🟣 Gufi CLI - Desarrolla módulos, vistas y automations")
47
- .version("0.1.7");
48
+ .version("0.1.8");
48
49
  // ════════════════════════════════════════════════════════════════════
49
50
  // 🔐 Auth
50
51
  // ════════════════════════════════════════════════════════════════════
@@ -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
- if (!refreshToken) {
18
- console.error("[gufi] No refresh token available");
19
- return null;
20
- }
21
- try {
22
- const url = `${getApiUrl()}/api/auth/refresh`;
23
- const response = await fetch(url, {
24
- method: "POST",
25
- headers: {
26
- "Content-Type": "application/json",
27
- "X-Client": "cli",
28
- "X-Refresh-Token": refreshToken,
29
- },
30
- });
31
- if (!response.ok) {
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
- const data = await response.json();
36
- const config = loadConfig();
37
- setToken(data.accessToken, config.email || "", data.refreshToken);
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
- throw new Error("No estás logueado. Ejecuta: gufi login");
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, {
@@ -7,6 +7,7 @@ export interface GufiConfig {
7
7
  token?: string;
8
8
  refreshToken?: string;
9
9
  email?: string;
10
+ password?: string;
10
11
  currentView?: {
11
12
  id: number;
12
13
  name: string;
@@ -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
- return !!getToken();
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();
@@ -12,12 +12,15 @@ export interface ViewMeta {
12
12
  mtime: number;
13
13
  }>;
14
14
  }
15
- export declare function getViewDir(viewName: string): string;
15
+ export declare function getViewDir(viewKey: string): string;
16
16
  export declare function loadViewMeta(viewDir: string): ViewMeta | null;
17
17
  /**
18
18
  * Pull view files from Gufi to local directory
19
+ * @param viewId - The view ID
20
+ * @param viewKey - The unique identifier in view_<id> format (e.g., view_123)
21
+ * @param packageId - The package ID
19
22
  */
20
- export declare function pullView(viewId: number, viewName: string, packageId: number): Promise<{
23
+ export declare function pullView(viewId: number, viewKey: string, packageId: number): Promise<{
21
24
  dir: string;
22
25
  fileCount: number;
23
26
  }>;
package/dist/lib/sync.js CHANGED
@@ -37,8 +37,9 @@ function getLanguage(filePath) {
37
37
  };
38
38
  return langMap[ext] || "text";
39
39
  }
40
- export function getViewDir(viewName) {
41
- return path.join(GUFI_DEV_DIR, viewName.toLowerCase().replace(/\s+/g, "-"));
40
+ // 💜 viewKey is now always in view_<id> format (e.g., view_123)
41
+ export function getViewDir(viewKey) {
42
+ return path.join(GUFI_DEV_DIR, viewKey.toLowerCase());
42
43
  }
43
44
  export function loadViewMeta(viewDir) {
44
45
  const metaPath = path.join(viewDir, META_FILE);
@@ -57,9 +58,12 @@ function saveViewMeta(viewDir, meta) {
57
58
  }
58
59
  /**
59
60
  * Pull view files from Gufi to local directory
61
+ * @param viewId - The view ID
62
+ * @param viewKey - The unique identifier in view_<id> format (e.g., view_123)
63
+ * @param packageId - The package ID
60
64
  */
61
- export async function pullView(viewId, viewName, packageId) {
62
- const viewDir = getViewDir(viewName);
65
+ export async function pullView(viewId, viewKey, packageId) {
66
+ const viewDir = getViewDir(viewKey);
63
67
  ensureDir(viewDir);
64
68
  const files = await getViewFiles(viewId);
65
69
  const fileMeta = {};
@@ -74,14 +78,14 @@ export async function pullView(viewId, viewName, packageId) {
74
78
  }
75
79
  const meta = {
76
80
  viewId,
77
- viewName,
81
+ viewName: viewKey, // 💜 Store viewKey as viewName for backwards compat
78
82
  packageId,
79
83
  lastSync: new Date().toISOString(),
80
84
  files: fileMeta,
81
85
  };
82
86
  saveViewMeta(viewDir, meta);
83
87
  // Update current view in config
84
- setCurrentView({ id: viewId, name: viewName, packageId, localPath: viewDir });
88
+ setCurrentView({ id: viewId, name: viewKey, packageId, localPath: viewDir });
85
89
  return { dir: viewDir, fileCount: files.length };
86
90
  }
87
91
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gufi-cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "CLI for developing Gufi Marketplace views locally with Claude Code",
5
5
  "bin": {
6
6
  "gufi": "./bin/gufi.js"