gufi-cli 0.1.7 → 0.1.9

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,15 +604,16 @@ 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
 
614
614
  ├── core/
615
- └── dataProvider.ts # 💜 dataSources + featureConfig (MUY IMPORTANTE)
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
- └── MiComponente.tsx # Componentes React
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,198 @@ 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
+ ### 💜 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
+
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.
1076
+
1077
+ ---
1078
+
885
1079
  ### 💜 automations en Vistas
886
1080
 
887
1081
  Las vistas pueden incluir automations que se ejecutan al hacer click.
@@ -1475,11 +1669,13 @@ gufi rows:create m360_t16192 --file datos.json
1475
1669
  ### Desarrollo de Views
1476
1670
 
1477
1671
  ```bash
1478
- # Ver tus views del Marketplace
1672
+ # Ver tus views del Marketplace (muestra ID de cada vista)
1479
1673
  gufi views
1674
+ # 📦 Gestión de Tareas (ID: 14)
1675
+ # └─ 13 Tasks Manager (custom)
1480
1676
 
1481
- # Descargar view para editar localmente
1482
- gufi view:pull "Stock Overview"
1677
+ # Descargar view por ID (se guarda en ~/gufi-dev/view_<id>/)
1678
+ gufi view:pull 13
1483
1679
 
1484
1680
  # Auto-sync al guardar archivos
1485
1681
  gufi view:watch
@@ -1516,7 +1712,7 @@ gufi view:status
1516
1712
  | Ver estructura de una company | `gufi modules <company_id>` |
1517
1713
  | Ver/editar JSON de módulo | `gufi module <id> -c <company>` |
1518
1714
  | Ver/editar código de automation | `gufi automation <nombre> -c <company>` |
1519
- | Desarrollar una vista | `gufi pull`, `gufi watch`, `gufi logs` |
1715
+ | Desarrollar una vista | `gufi view:pull <id>`, `gufi view:watch`, `gufi view:logs` |
1520
1716
 
1521
1717
  ### Errores comunes
1522
1718
 
@@ -370,11 +370,12 @@ export async function companyCreateCommand(name) {
370
370
  method: "POST",
371
371
  body: JSON.stringify({ name }),
372
372
  });
373
- const company = result.data || result;
373
+ // API returns { company: {...}, message: "..." }
374
+ const company = result.company || result.data || result;
374
375
  console.log(chalk.green(" ✓ Company creada!\n"));
375
376
  console.log(chalk.white(` ID: ${company.id}`));
376
377
  console.log(chalk.white(` Nombre: ${company.name}`));
377
- console.log(chalk.white(` Schema: company_${company.id}`));
378
+ console.log(chalk.white(` Schema: ${company.schema || 'company_' + company.id}`));
378
379
  console.log(chalk.gray("\n 💡 Usa: gufi modules " + company.id + " para ver módulos\n"));
379
380
  }
380
381
  catch (err) {
@@ -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));
@@ -1,53 +1,11 @@
1
1
  /**
2
2
  * gufi login - Authenticate with Gufi
3
3
  */
4
- import readline from "readline";
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 prompt("\n¿Quieres iniciar sesión con otra cuenta? (s/N): ");
66
- if (relogin.toLowerCase() !== "s") {
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 email = await prompt(" Email: ");
78
- const password = await prompt(" Password: ", true);
79
- if (!email || !password) {
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
- setToken(token, email, refreshToken);
87
- spinner.succeed(chalk.green(`Sesión iniciada como ${email}`));
88
- console.log(chalk.gray("\n Tu sesión se mantendrá activa automáticamente.\n"));
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) {
@@ -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;
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,21 +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
- console.log(` ${chalk.cyan(i + 1)}. ${view.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})`)}`);
61
61
  });
62
- // If viewIdentifier matches a name
63
- let selectedView = viewIdentifier
64
- ? views.find(v => v.name.toLowerCase().includes(viewIdentifier.toLowerCase()))
65
- : views[0];
66
- if (!selectedView) {
67
- console.log(chalk.yellow(`\n No se encontró vista "${viewIdentifier}"\n`));
68
- console.log(chalk.gray(" Uso: gufi pull <nombre-vista> o gufi pull <view-id>\n"));
69
- process.exit(1);
62
+ if (viewIdentifier) {
63
+ console.log(chalk.yellow(`\n "${viewIdentifier}" no es un ID válido.\n`));
70
64
  }
71
- viewId = selectedView.pk_id;
72
- viewName = selectedView.name;
73
- 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);
74
67
  }
75
68
  catch (error) {
76
69
  spinner.fail(chalk.red(error.message));
@@ -80,7 +73,7 @@ export async function pullCommand(viewIdentifier) {
80
73
  // Pull the view
81
74
  const pullSpinner = ora("Descargando archivos...").start();
82
75
  try {
83
- const result = await pullView(viewId, viewName, packageId);
76
+ const result = await pullView(viewId, viewKey, packageId);
84
77
  pullSpinner.succeed(chalk.green(`${result.fileCount} archivos descargados`));
85
78
  console.log(chalk.gray(`\n 📁 ${result.dir}\n`));
86
79
  console.log(chalk.gray(" Comandos útiles:"));
package/dist/index.js CHANGED
@@ -44,7 +44,7 @@ const program = new Command();
44
44
  program
45
45
  .name("gufi")
46
46
  .description("🟣 Gufi CLI - Desarrolla módulos, vistas y automations")
47
- .version("0.1.7");
47
+ .version("0.1.8");
48
48
  // ════════════════════════════════════════════════════════════════════
49
49
  // 🔐 Auth
50
50
  // ════════════════════════════════════════════════════════════════════
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
  }
@@ -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.7",
3
+ "version": "0.1.9",
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": {