gufi-cli 0.1.6 → 0.1.8
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 +179 -2
- package/dist/commands/env.d.ts +30 -0
- package/dist/commands/env.js +145 -0
- package/dist/commands/login.js +31 -53
- package/dist/commands/pull.js +5 -4
- package/dist/index.js +22 -1
- package/dist/lib/api.js +9 -3
- package/package.json +3 -1
package/CLAUDE.md
CHANGED
|
@@ -612,7 +612,8 @@ Cuando descargas una view con `gufi pull`, obtienes:
|
|
|
612
612
|
├── types.ts # Interfaces TypeScript
|
|
613
613
|
│
|
|
614
614
|
├── core/
|
|
615
|
-
│
|
|
615
|
+
│ ├── dataProvider.ts # 💜 dataSources + featureConfig (MUY IMPORTANTE)
|
|
616
|
+
│ └── permissions.ts # 💜 Sistema de permisos dinámico
|
|
616
617
|
│
|
|
617
618
|
├── metadata/
|
|
618
619
|
│ ├── inputs.ts # 💜 Inputs configurables por usuario
|
|
@@ -620,7 +621,8 @@ Cuando descargas una view con `gufi pull`, obtienes:
|
|
|
620
621
|
│ └── help.en.ts # Documentación en inglés
|
|
621
622
|
│
|
|
622
623
|
├── components/
|
|
623
|
-
│
|
|
624
|
+
│ ├── MiComponente.tsx # Componentes React
|
|
625
|
+
│ └── DevPermissionSwitcher.tsx # 💜 Testing de permisos en dev
|
|
624
626
|
│
|
|
625
627
|
├── views/
|
|
626
628
|
│ └── page.tsx # Página principal
|
|
@@ -882,6 +884,158 @@ Para crear tarea, pulsa el botón **+**
|
|
|
882
884
|
|
|
883
885
|
---
|
|
884
886
|
|
|
887
|
+
### 💜 permissions - Sistema de Permisos Explícitos (Sin Wildcards)
|
|
888
|
+
|
|
889
|
+
Las vistas usan un sistema de **permisos explícitos** - sin wildcards, sin magia. Cada permiso es un string específico que existe o no en el array de permisos del usuario.
|
|
890
|
+
|
|
891
|
+
> ⚠️ **Importante**: Eliminamos el soporte de wildcards (`*`) porque causaba problemas de integridad entre el estado de la UI y los permisos reales. Siempre usa strings de permisos explícitos.
|
|
892
|
+
|
|
893
|
+
**1. Declarar permisos en `metadata/permissions.ts`:**
|
|
894
|
+
```typescript
|
|
895
|
+
// metadata/permissions.ts
|
|
896
|
+
export const permissions = [
|
|
897
|
+
// Permisos estáticos - toggles simples
|
|
898
|
+
{
|
|
899
|
+
key: 'send_orders',
|
|
900
|
+
label: { es: 'Enviar pedidos', en: 'Send orders' },
|
|
901
|
+
description: { es: 'Puede enviar pedidos a proveedores', en: 'Can send orders' },
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
key: 'view_costs',
|
|
905
|
+
label: { es: 'Ver costos', en: 'View costs' },
|
|
906
|
+
description: { es: 'Puede ver precios de compra', en: 'Can see purchase prices' },
|
|
907
|
+
},
|
|
908
|
+
|
|
909
|
+
// Permisos dinámicos - se resuelven en runtime
|
|
910
|
+
{
|
|
911
|
+
key: 'warehouse:*', // Placeholder, se resuelve a warehouse:madrid, warehouse:barcelona, etc.
|
|
912
|
+
label: { es: 'Acceso a almacén', en: 'Warehouse access' },
|
|
913
|
+
description: { es: 'Almacenes a los que tiene acceso', en: 'Warehouses user can access' },
|
|
914
|
+
dynamic: true, // Marca este como dinámico
|
|
915
|
+
},
|
|
916
|
+
] as const;
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
**2. Crear `core/permissions.ts` (sin wildcards):**
|
|
920
|
+
```typescript
|
|
921
|
+
// core/permissions.ts
|
|
922
|
+
// SIN WILDCARDS - solo permisos explícitos
|
|
923
|
+
|
|
924
|
+
export interface PermissionsConfig {
|
|
925
|
+
userPermissions: string[]; // Permisos del usuario (resueltos desde su rol)
|
|
926
|
+
isDevMode: boolean;
|
|
927
|
+
devPermissions: string[]; // Override en modo dev para testing
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Obtener permisos efectivos (dev mode override)
|
|
931
|
+
function getEffectivePermissions(config: PermissionsConfig): string[] {
|
|
932
|
+
const { userPermissions, isDevMode, devPermissions } = config;
|
|
933
|
+
return (isDevMode && devPermissions.length > 0) ? devPermissions : userPermissions;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Check simple - ¿tiene exactamente este permiso?
|
|
937
|
+
export function hasPermission(config: PermissionsConfig, permission: string): boolean {
|
|
938
|
+
const perms = getEffectivePermissions(config);
|
|
939
|
+
return perms.includes(permission);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Helpers específicos de la vista
|
|
943
|
+
export function canSendOrders(config: PermissionsConfig): boolean {
|
|
944
|
+
return hasPermission(config, 'send_orders');
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
export function canViewCosts(config: PermissionsConfig): boolean {
|
|
948
|
+
return hasPermission(config, 'view_costs');
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Obtener warehouses permitidos (null si no hay restricciones)
|
|
952
|
+
export function getAllowedWarehouses(config: PermissionsConfig): string[] | null {
|
|
953
|
+
const perms = getEffectivePermissions(config);
|
|
954
|
+
|
|
955
|
+
const warehouses: string[] = [];
|
|
956
|
+
for (const perm of perms) {
|
|
957
|
+
if (perm.startsWith('warehouse:')) {
|
|
958
|
+
warehouses.push(perm.slice('warehouse:'.length));
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return warehouses.length > 0 ? warehouses : null;
|
|
963
|
+
}
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
**3. Usar en el componente con inicialización correcta:**
|
|
967
|
+
```typescript
|
|
968
|
+
import { useState, useEffect, useRef } from 'react';
|
|
969
|
+
import { DevPermissionSwitcher } from '../components/DevPermissionSwitcher';
|
|
970
|
+
import { canSendOrders, getAllowedWarehouses } from '../core/permissions';
|
|
971
|
+
|
|
972
|
+
export default function MiVista({ gufi }) {
|
|
973
|
+
const lang = gufi?.context?.lang || 'es';
|
|
974
|
+
const isDevMode = gufi?.context?.isPreview || window.location.hostname === 'localhost';
|
|
975
|
+
|
|
976
|
+
// 💜 IMPORTANTE: Inicializar vacío, poblar cuando los datos carguen
|
|
977
|
+
const [devPermissions, setDevPermissions] = useState<string[]>([]);
|
|
978
|
+
const devPermissionsInitialized = useRef(false);
|
|
979
|
+
|
|
980
|
+
// Cargar warehouses de tu data source
|
|
981
|
+
const [warehouses, setWarehouses] = useState([]);
|
|
982
|
+
|
|
983
|
+
// Inicializar permisos cuando warehouses carguen (una sola vez)
|
|
984
|
+
useEffect(() => {
|
|
985
|
+
if (isDevMode && !devPermissionsInitialized.current && warehouses.length > 0) {
|
|
986
|
+
devPermissionsInitialized.current = true;
|
|
987
|
+
|
|
988
|
+
// Otorgar todos los permisos explícitos
|
|
989
|
+
const allPerms = [
|
|
990
|
+
'send_orders',
|
|
991
|
+
'view_costs',
|
|
992
|
+
...warehouses.map(w => `warehouse:${w.name.toLowerCase()}`),
|
|
993
|
+
];
|
|
994
|
+
setDevPermissions(allPerms);
|
|
995
|
+
}
|
|
996
|
+
}, [isDevMode, warehouses]);
|
|
997
|
+
|
|
998
|
+
// Construir config de permisos
|
|
999
|
+
const permissionsConfig = {
|
|
1000
|
+
userPermissions: gufi?.context?.user?.permissions || [],
|
|
1001
|
+
isDevMode,
|
|
1002
|
+
devPermissions,
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
// Usar helpers de permisos
|
|
1006
|
+
const showCosts = canViewCosts(permissionsConfig);
|
|
1007
|
+
const allowedWarehouses = getAllowedWarehouses(permissionsConfig);
|
|
1008
|
+
|
|
1009
|
+
return (
|
|
1010
|
+
<div>
|
|
1011
|
+
{showCosts && <CostsColumn />}
|
|
1012
|
+
|
|
1013
|
+
{/* DevPermissionSwitcher - solo en dev mode */}
|
|
1014
|
+
{isDevMode && (
|
|
1015
|
+
<DevPermissionSwitcher
|
|
1016
|
+
lang={lang}
|
|
1017
|
+
activePermissions={devPermissions}
|
|
1018
|
+
onPermissionsChange={setDevPermissions}
|
|
1019
|
+
dynamicValues={{
|
|
1020
|
+
warehouse: warehouses.map(w => w.name.toLowerCase())
|
|
1021
|
+
}}
|
|
1022
|
+
/>
|
|
1023
|
+
)}
|
|
1024
|
+
</div>
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
**DevPermissionSwitcher**: Botón flotante **purple** (💜 Gufi style) que permite:
|
|
1030
|
+
- Toggle individual de permisos on/off
|
|
1031
|
+
- Botón "Todos" en el header para acceso completo
|
|
1032
|
+
- Permisos dinámicos expandibles (ej: warehouses individuales)
|
|
1033
|
+
- Integridad total: UI siempre refleja el estado real
|
|
1034
|
+
|
|
1035
|
+
**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
|
+
|
|
1037
|
+
---
|
|
1038
|
+
|
|
885
1039
|
### 💜 automations en Vistas
|
|
886
1040
|
|
|
887
1041
|
Las vistas pueden incluir automations que se ejecutan al hacer click.
|
|
@@ -1419,6 +1573,29 @@ gufi automation calcular_stock -c 116 --edit
|
|
|
1419
1573
|
gufi automation calcular_stock -c 116 --file script.js
|
|
1420
1574
|
```
|
|
1421
1575
|
|
|
1576
|
+
### Environment Variables
|
|
1577
|
+
|
|
1578
|
+
```bash
|
|
1579
|
+
# Ver variables de entorno de la company
|
|
1580
|
+
gufi env
|
|
1581
|
+
|
|
1582
|
+
# Crear/actualizar variable
|
|
1583
|
+
gufi env:set STRIPE_API_KEY sk-live-xxx
|
|
1584
|
+
|
|
1585
|
+
# Eliminar variable
|
|
1586
|
+
gufi env:delete STRIPE_API_KEY
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1589
|
+
### Schema (Estructura de Tablas)
|
|
1590
|
+
|
|
1591
|
+
```bash
|
|
1592
|
+
# Ver todas las tablas de la company
|
|
1593
|
+
gufi schema
|
|
1594
|
+
|
|
1595
|
+
# Filtrar por módulo
|
|
1596
|
+
gufi schema -m 360
|
|
1597
|
+
```
|
|
1598
|
+
|
|
1422
1599
|
### Row CRUD (Datos de Tablas)
|
|
1423
1600
|
|
|
1424
1601
|
```bash
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gufi env - Manage company environment variables
|
|
3
|
+
* gufi schema - View company table structure
|
|
4
|
+
*/
|
|
5
|
+
interface EnvOptions {
|
|
6
|
+
company?: string;
|
|
7
|
+
set?: string;
|
|
8
|
+
delete?: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface SchemaOptions {
|
|
11
|
+
company?: string;
|
|
12
|
+
module?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* gufi env - List environment variables
|
|
16
|
+
*/
|
|
17
|
+
export declare function envListCommand(options: EnvOptions): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* gufi env:set <key> <value> - Set an environment variable
|
|
20
|
+
*/
|
|
21
|
+
export declare function envSetCommand(key: string, value: string, options: EnvOptions): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* gufi env:delete <key> - Delete an environment variable
|
|
24
|
+
*/
|
|
25
|
+
export declare function envDeleteCommand(key: string, options: EnvOptions): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* gufi schema - View company table structure
|
|
28
|
+
*/
|
|
29
|
+
export declare function schemaCommand(options: SchemaOptions): Promise<void>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gufi env - Manage company environment variables
|
|
3
|
+
* gufi schema - View company table structure
|
|
4
|
+
*/
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import ora from "ora";
|
|
7
|
+
import { getToken, getApiUrl } from "../lib/config.js";
|
|
8
|
+
async function apiRequest(endpoint, options = {}) {
|
|
9
|
+
const token = getToken();
|
|
10
|
+
if (!token) {
|
|
11
|
+
throw new Error("No estás logueado. Ejecuta: gufi login");
|
|
12
|
+
}
|
|
13
|
+
const url = `${getApiUrl()}${endpoint}`;
|
|
14
|
+
const response = await fetch(url, {
|
|
15
|
+
...options,
|
|
16
|
+
headers: {
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
Authorization: `Bearer ${token}`,
|
|
19
|
+
"X-Client": "cli",
|
|
20
|
+
...options.headers,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
const text = await response.text();
|
|
25
|
+
throw new Error(`API Error ${response.status}: ${text}`);
|
|
26
|
+
}
|
|
27
|
+
return response.json();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* gufi env - List environment variables
|
|
31
|
+
*/
|
|
32
|
+
export async function envListCommand(options) {
|
|
33
|
+
const spinner = ora("Cargando variables de entorno...").start();
|
|
34
|
+
try {
|
|
35
|
+
const data = await apiRequest("/api/cli/env");
|
|
36
|
+
spinner.stop();
|
|
37
|
+
console.log(chalk.magenta("\n 🔐 Variables de Entorno\n"));
|
|
38
|
+
if (!data || data.length === 0) {
|
|
39
|
+
console.log(chalk.gray(" No hay variables configuradas\n"));
|
|
40
|
+
console.log(chalk.gray(" Usa: gufi env:set <KEY> <VALUE>\n"));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Show as table
|
|
44
|
+
const maxKeyLen = Math.max(...data.map((v) => v.key.length), 10);
|
|
45
|
+
console.log(chalk.gray(" " + "KEY".padEnd(maxKeyLen + 2) + "VALUE"));
|
|
46
|
+
console.log(chalk.gray(" " + "─".repeat(maxKeyLen + 20)));
|
|
47
|
+
for (const env of data) {
|
|
48
|
+
const maskedValue = env.is_secret
|
|
49
|
+
? "••••••••"
|
|
50
|
+
: (env.value?.substring(0, 30) + (env.value?.length > 30 ? "..." : ""));
|
|
51
|
+
console.log(" " +
|
|
52
|
+
chalk.cyan(env.key.padEnd(maxKeyLen + 2)) +
|
|
53
|
+
chalk.white(maskedValue));
|
|
54
|
+
}
|
|
55
|
+
console.log(chalk.gray(`\n Total: ${data.length} variables\n`));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
spinner.fail(chalk.red(error.message));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* gufi env:set <key> <value> - Set an environment variable
|
|
64
|
+
*/
|
|
65
|
+
export async function envSetCommand(key, value, options) {
|
|
66
|
+
const spinner = ora(`Guardando ${key}...`).start();
|
|
67
|
+
try {
|
|
68
|
+
await apiRequest("/api/cli/env", {
|
|
69
|
+
method: "POST",
|
|
70
|
+
body: JSON.stringify({ key, value }),
|
|
71
|
+
});
|
|
72
|
+
spinner.succeed(chalk.green(`${key} guardada`));
|
|
73
|
+
console.log();
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
spinner.fail(chalk.red(error.message));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* gufi env:delete <key> - Delete an environment variable
|
|
82
|
+
*/
|
|
83
|
+
export async function envDeleteCommand(key, options) {
|
|
84
|
+
const spinner = ora(`Eliminando ${key}...`).start();
|
|
85
|
+
try {
|
|
86
|
+
await apiRequest(`/api/cli/env/${encodeURIComponent(key)}`, {
|
|
87
|
+
method: "DELETE",
|
|
88
|
+
});
|
|
89
|
+
spinner.succeed(chalk.green(`${key} eliminada`));
|
|
90
|
+
console.log();
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
spinner.fail(chalk.red(error.message));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* gufi schema - View company table structure
|
|
99
|
+
*/
|
|
100
|
+
export async function schemaCommand(options) {
|
|
101
|
+
const spinner = ora("Cargando schema...").start();
|
|
102
|
+
try {
|
|
103
|
+
const data = await apiRequest("/api/cli/schema");
|
|
104
|
+
spinner.stop();
|
|
105
|
+
console.log(chalk.magenta("\n 📊 Schema de la Company\n"));
|
|
106
|
+
if (!data.modules || data.modules.length === 0) {
|
|
107
|
+
console.log(chalk.gray(" No hay módulos\n"));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
for (const module of data.modules) {
|
|
111
|
+
// Filter by module if specified
|
|
112
|
+
if (options.module && module.id !== parseInt(options.module)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
console.log(chalk.cyan(` 📦 ${module.name}`) +
|
|
116
|
+
chalk.gray(` (ID: ${module.id})`));
|
|
117
|
+
if (module.entities && module.entities.length > 0) {
|
|
118
|
+
for (const entity of module.entities) {
|
|
119
|
+
const tableName = `m${module.id}_t${entity.id}`;
|
|
120
|
+
console.log(chalk.white(` └─ ${entity.name}`) +
|
|
121
|
+
chalk.gray(` → ${tableName}`));
|
|
122
|
+
// Show fields if available
|
|
123
|
+
if (entity.fields && entity.fields.length > 0) {
|
|
124
|
+
const fieldList = entity.fields
|
|
125
|
+
.slice(0, 5)
|
|
126
|
+
.map((f) => f.name)
|
|
127
|
+
.join(", ");
|
|
128
|
+
const more = entity.fields.length > 5
|
|
129
|
+
? ` +${entity.fields.length - 5} más`
|
|
130
|
+
: "";
|
|
131
|
+
console.log(chalk.gray(` campos: ${fieldList}${more}`));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
console.log();
|
|
136
|
+
}
|
|
137
|
+
// Summary
|
|
138
|
+
const totalTables = data.modules.reduce((acc, m) => acc + (m.entities?.length || 0), 0);
|
|
139
|
+
console.log(chalk.gray(` ${data.modules.length} módulos, ${totalTables} tablas\n`));
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
spinner.fail(chalk.red(error.message));
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
}
|
package/dist/commands/login.js
CHANGED
|
@@ -1,53 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* gufi login - Authenticate with Gufi
|
|
3
3
|
*/
|
|
4
|
-
import
|
|
4
|
+
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
8
|
import { setToken, isLoggedIn, clearToken, loadConfig, setApiUrl } from "../lib/config.js";
|
|
9
|
-
function prompt(question, hidden = false) {
|
|
10
|
-
const rl = readline.createInterface({
|
|
11
|
-
input: process.stdin,
|
|
12
|
-
output: process.stdout,
|
|
13
|
-
});
|
|
14
|
-
return new Promise((resolve) => {
|
|
15
|
-
if (hidden) {
|
|
16
|
-
process.stdout.write(question);
|
|
17
|
-
let input = "";
|
|
18
|
-
process.stdin.setRawMode?.(true);
|
|
19
|
-
process.stdin.resume();
|
|
20
|
-
process.stdin.on("data", (char) => {
|
|
21
|
-
const c = char.toString();
|
|
22
|
-
if (c === "\n" || c === "\r") {
|
|
23
|
-
process.stdin.setRawMode?.(false);
|
|
24
|
-
process.stdout.write("\n");
|
|
25
|
-
rl.close();
|
|
26
|
-
resolve(input);
|
|
27
|
-
}
|
|
28
|
-
else if (c === "\u0003") {
|
|
29
|
-
process.exit();
|
|
30
|
-
}
|
|
31
|
-
else if (c === "\u007F") {
|
|
32
|
-
if (input.length > 0) {
|
|
33
|
-
input = input.slice(0, -1);
|
|
34
|
-
process.stdout.write("\b \b");
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
input += c;
|
|
39
|
-
process.stdout.write("*");
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
rl.question(question, (answer) => {
|
|
45
|
-
rl.close();
|
|
46
|
-
resolve(answer);
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
9
|
export async function loginCommand(options) {
|
|
52
10
|
console.log(chalk.magenta("\n 🟣 Gufi Developer CLI\n"));
|
|
53
11
|
// Set custom API URL if provided
|
|
@@ -62,8 +20,13 @@ export async function loginCommand(options) {
|
|
|
62
20
|
const valid = await validateToken();
|
|
63
21
|
if (valid) {
|
|
64
22
|
spinner.succeed(chalk.green(`Ya estás logueado como ${config.email}`));
|
|
65
|
-
const relogin = await
|
|
66
|
-
|
|
23
|
+
const { relogin } = await prompts({
|
|
24
|
+
type: "confirm",
|
|
25
|
+
name: "relogin",
|
|
26
|
+
message: "¿Quieres iniciar sesión con otra cuenta?",
|
|
27
|
+
initial: false,
|
|
28
|
+
});
|
|
29
|
+
if (!relogin) {
|
|
67
30
|
return;
|
|
68
31
|
}
|
|
69
32
|
clearToken();
|
|
@@ -73,19 +36,34 @@ export async function loginCommand(options) {
|
|
|
73
36
|
clearToken();
|
|
74
37
|
}
|
|
75
38
|
}
|
|
76
|
-
// Get credentials
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
39
|
+
// Get credentials using prompts (more robust than readline)
|
|
40
|
+
const response = await prompts([
|
|
41
|
+
{
|
|
42
|
+
type: "text",
|
|
43
|
+
name: "email",
|
|
44
|
+
message: "Email",
|
|
45
|
+
validate: (v) => (v ? true : "Email es requerido"),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: "password",
|
|
49
|
+
name: "password",
|
|
50
|
+
message: "Password",
|
|
51
|
+
validate: (v) => (v ? true : "Password es requerido"),
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
if (!response.email || !response.password) {
|
|
80
55
|
console.log(chalk.red("\n ✗ Email y password son requeridos\n"));
|
|
81
56
|
process.exit(1);
|
|
82
57
|
}
|
|
83
58
|
const spinner = ora("Iniciando sesión...").start();
|
|
84
59
|
try {
|
|
85
|
-
const { token, refreshToken } = await login(email, password);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
60
|
+
const { token, refreshToken } = await login(response.email, response.password);
|
|
61
|
+
if (!refreshToken) {
|
|
62
|
+
console.log(chalk.yellow("\n ⚠️ No se recibió refresh token - sesión durará 1 hora"));
|
|
63
|
+
}
|
|
64
|
+
setToken(token, response.email, refreshToken);
|
|
65
|
+
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"));
|
|
89
67
|
console.log(chalk.gray(" Ahora puedes usar: gufi pull <vista>\n"));
|
|
90
68
|
}
|
|
91
69
|
catch (error) {
|
package/dist/commands/pull.js
CHANGED
|
@@ -21,7 +21,7 @@ export async function pullCommand(viewIdentifier) {
|
|
|
21
21
|
const spinner = ora("Obteniendo vista...").start();
|
|
22
22
|
try {
|
|
23
23
|
const view = await getView(viewId);
|
|
24
|
-
viewName = view.name
|
|
24
|
+
viewName = view.name || `vista-${viewId}`;
|
|
25
25
|
packageId = view.package_id;
|
|
26
26
|
spinner.succeed(`Vista: ${viewName}`);
|
|
27
27
|
}
|
|
@@ -57,11 +57,12 @@ export async function pullCommand(viewIdentifier) {
|
|
|
57
57
|
}
|
|
58
58
|
console.log(chalk.gray(" Vistas disponibles:\n"));
|
|
59
59
|
views.forEach((view, i) => {
|
|
60
|
-
|
|
60
|
+
const name = view.name || `Vista ${view.pk_id}`;
|
|
61
|
+
console.log(` ${chalk.cyan(i + 1)}. ${name} ${chalk.gray(`(${view.view_type})`)}`);
|
|
61
62
|
});
|
|
62
63
|
// If viewIdentifier matches a name
|
|
63
64
|
let selectedView = viewIdentifier
|
|
64
|
-
? views.find(v => v.name.toLowerCase().includes(viewIdentifier.toLowerCase()))
|
|
65
|
+
? views.find(v => v.name && v.name.toLowerCase().includes(viewIdentifier.toLowerCase()))
|
|
65
66
|
: views[0];
|
|
66
67
|
if (!selectedView) {
|
|
67
68
|
console.log(chalk.yellow(`\n No se encontró vista "${viewIdentifier}"\n`));
|
|
@@ -69,7 +70,7 @@ export async function pullCommand(viewIdentifier) {
|
|
|
69
70
|
process.exit(1);
|
|
70
71
|
}
|
|
71
72
|
viewId = selectedView.pk_id;
|
|
72
|
-
viewName = selectedView.name
|
|
73
|
+
viewName = selectedView.name || `vista-${selectedView.pk_id}`;
|
|
73
74
|
console.log(chalk.gray(`\n Descargando: ${viewName}\n`));
|
|
74
75
|
}
|
|
75
76
|
catch (error) {
|
package/dist/index.js
CHANGED
|
@@ -39,11 +39,12 @@ import { listCommand } from "./commands/list.js";
|
|
|
39
39
|
import { logsCommand } from "./commands/logs.js";
|
|
40
40
|
import { companiesCommand, modulesCommand, moduleCommand, moduleUpdateCommand, companyCreateCommand, automationsCommand, automationCommand, } from "./commands/companies.js";
|
|
41
41
|
import { rowsListCommand, rowGetCommand, rowCreateCommand, rowUpdateCommand, rowDeleteCommand, rowDuplicateCommand, rowsBulkCreateCommand, } from "./commands/rows.js";
|
|
42
|
+
import { envListCommand, envSetCommand, envDeleteCommand, schemaCommand, } from "./commands/env.js";
|
|
42
43
|
const program = new Command();
|
|
43
44
|
program
|
|
44
45
|
.name("gufi")
|
|
45
46
|
.description("🟣 Gufi CLI - Desarrolla módulos, vistas y automations")
|
|
46
|
-
.version("0.1.
|
|
47
|
+
.version("0.1.7");
|
|
47
48
|
// ════════════════════════════════════════════════════════════════════
|
|
48
49
|
// 🔐 Auth
|
|
49
50
|
// ════════════════════════════════════════════════════════════════════
|
|
@@ -75,6 +76,11 @@ program
|
|
|
75
76
|
.command("modules <company_id>")
|
|
76
77
|
.description("Ver módulos de una company")
|
|
77
78
|
.action(modulesCommand);
|
|
79
|
+
program
|
|
80
|
+
.command("schema")
|
|
81
|
+
.description("Ver estructura de tablas de la company")
|
|
82
|
+
.option("-m, --module <id>", "Filtrar por módulo")
|
|
83
|
+
.action(schemaCommand);
|
|
78
84
|
program
|
|
79
85
|
.command("module <module_id>")
|
|
80
86
|
.description("Ver/editar JSON de un módulo")
|
|
@@ -104,6 +110,21 @@ program
|
|
|
104
110
|
.option("-f, --file <path>", "Exportar a archivo")
|
|
105
111
|
.action(automationCommand);
|
|
106
112
|
// ════════════════════════════════════════════════════════════════════
|
|
113
|
+
// 🔐 Environment Variables
|
|
114
|
+
// ════════════════════════════════════════════════════════════════════
|
|
115
|
+
program
|
|
116
|
+
.command("env")
|
|
117
|
+
.description("Ver variables de entorno de la company")
|
|
118
|
+
.action(envListCommand);
|
|
119
|
+
program
|
|
120
|
+
.command("env:set <key> <value>")
|
|
121
|
+
.description("Crear/actualizar variable de entorno")
|
|
122
|
+
.action(envSetCommand);
|
|
123
|
+
program
|
|
124
|
+
.command("env:delete <key>")
|
|
125
|
+
.description("Eliminar variable de entorno")
|
|
126
|
+
.action(envDeleteCommand);
|
|
127
|
+
// ════════════════════════════════════════════════════════════════════
|
|
107
128
|
// 📊 Row CRUD - Datos de tablas
|
|
108
129
|
// ════════════════════════════════════════════════════════════════════
|
|
109
130
|
program
|
package/dist/lib/api.js
CHANGED
|
@@ -14,8 +14,10 @@ class ApiError extends Error {
|
|
|
14
14
|
// Auto-refresh the token if expired
|
|
15
15
|
async function refreshAccessToken() {
|
|
16
16
|
const refreshToken = getRefreshToken();
|
|
17
|
-
if (!refreshToken)
|
|
17
|
+
if (!refreshToken) {
|
|
18
|
+
console.error("[gufi] No refresh token available");
|
|
18
19
|
return null;
|
|
20
|
+
}
|
|
19
21
|
try {
|
|
20
22
|
const url = `${getApiUrl()}/api/auth/refresh`;
|
|
21
23
|
const response = await fetch(url, {
|
|
@@ -26,14 +28,18 @@ async function refreshAccessToken() {
|
|
|
26
28
|
"X-Refresh-Token": refreshToken,
|
|
27
29
|
},
|
|
28
30
|
});
|
|
29
|
-
if (!response.ok)
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
console.error(`[gufi] Refresh failed: ${response.status}`);
|
|
30
33
|
return null;
|
|
34
|
+
}
|
|
31
35
|
const data = await response.json();
|
|
32
36
|
const config = loadConfig();
|
|
33
37
|
setToken(data.accessToken, config.email || "", data.refreshToken);
|
|
38
|
+
console.log("[gufi] Token refreshed successfully");
|
|
34
39
|
return data.accessToken;
|
|
35
40
|
}
|
|
36
|
-
catch {
|
|
41
|
+
catch (err) {
|
|
42
|
+
console.error("[gufi] Refresh error:", err);
|
|
37
43
|
return null;
|
|
38
44
|
}
|
|
39
45
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gufi-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "CLI for developing Gufi Marketplace views locally with Claude Code",
|
|
5
5
|
"bin": {
|
|
6
6
|
"gufi": "./bin/gufi.js"
|
|
@@ -22,12 +22,14 @@
|
|
|
22
22
|
"start": "node bin/gufi.js"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
+
"@types/prompts": "^2.4.9",
|
|
25
26
|
"@types/ws": "^8.18.1",
|
|
26
27
|
"chalk": "^5.3.0",
|
|
27
28
|
"chokidar": "^3.5.3",
|
|
28
29
|
"commander": "^12.0.0",
|
|
29
30
|
"node-fetch": "^3.3.2",
|
|
30
31
|
"ora": "^8.0.1",
|
|
32
|
+
"prompts": "^2.4.2",
|
|
31
33
|
"ws": "^8.18.3"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|