gufi-cli 0.1.7 → 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 +156 -2
- package/dist/commands/login.js +31 -53
- package/dist/commands/pull.js +5 -4
- 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.
|
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/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": {
|