gufi-cli 0.1.31 → 0.1.33

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.
@@ -51,6 +51,163 @@ function scanFilesInDirectory(dir, changedFiles) {
51
51
  }));
52
52
  return scanFilesForXSS(filesToScan);
53
53
  }
54
+ /**
55
+ * Validate view structure before push
56
+ * REQUIRED files:
57
+ * - index.tsx (entry point)
58
+ * - core/dataSources.ts (table mappings)
59
+ *
60
+ * RECOMMENDED files:
61
+ * - core/index.tsx (featureConfig)
62
+ * - views/page.tsx (main component)
63
+ */
64
+ function validateViewStructure(dir) {
65
+ const errors = [];
66
+ const allFiles = getAllFilesInDir(dir);
67
+ // 1. REQUIRED: index.tsx (entry point)
68
+ if (!allFiles.includes("index.tsx")) {
69
+ errors.push({
70
+ severity: "error",
71
+ message: "Falta archivo obligatorio: index.tsx",
72
+ file: "index.tsx",
73
+ hint: `Crear con:
74
+ export { featureConfig } from './core';
75
+ import Page from './views/page';
76
+ export default Page;`,
77
+ });
78
+ }
79
+ else {
80
+ // Validate index.tsx exports featureConfig
81
+ const indexContent = fs.readFileSync(path.join(dir, "index.tsx"), "utf-8");
82
+ if (!indexContent.includes("featureConfig")) {
83
+ errors.push({
84
+ severity: "error",
85
+ message: "index.tsx debe exportar featureConfig",
86
+ file: "index.tsx",
87
+ hint: "Añadir: export { featureConfig } from './core';",
88
+ });
89
+ }
90
+ if (!indexContent.includes("export default")) {
91
+ errors.push({
92
+ severity: "error",
93
+ message: "index.tsx debe tener un export default (componente)",
94
+ file: "index.tsx",
95
+ hint: "Añadir: export default Page; (o importar y re-exportar)",
96
+ });
97
+ }
98
+ }
99
+ // 2. REQUIRED: core/dataSources.ts (table mappings)
100
+ const hasDataSources = allFiles.includes("core/dataSources.ts");
101
+ const hasCoreIndex = allFiles.includes("core/index.tsx");
102
+ if (!hasDataSources) {
103
+ // Check if dataSources is in core/index.tsx (also valid)
104
+ let dataSourcesInCoreIndex = false;
105
+ if (hasCoreIndex) {
106
+ const coreIndexContent = fs.readFileSync(path.join(dir, "core/index.tsx"), "utf-8");
107
+ dataSourcesInCoreIndex = coreIndexContent.includes("dataSources");
108
+ }
109
+ if (!dataSourcesInCoreIndex) {
110
+ errors.push({
111
+ severity: "error",
112
+ message: "Falta archivo obligatorio: core/dataSources.ts",
113
+ file: "core/dataSources.ts",
114
+ hint: `Crear con:
115
+ export const dataSources = [
116
+ { key: 'main', table: 'modulo.entidad' },
117
+ ] as const;`,
118
+ });
119
+ }
120
+ }
121
+ else {
122
+ // Validate dataSources.ts has correct structure
123
+ const dsContent = fs.readFileSync(path.join(dir, "core/dataSources.ts"), "utf-8");
124
+ if (!dsContent.includes("dataSources")) {
125
+ errors.push({
126
+ severity: "error",
127
+ message: "core/dataSources.ts debe exportar 'dataSources'",
128
+ file: "core/dataSources.ts",
129
+ hint: "export const dataSources = [{ key: '...', table: '...' }] as const;",
130
+ });
131
+ }
132
+ }
133
+ // 3. RECOMMENDED: core/index.tsx (featureConfig)
134
+ if (!hasCoreIndex) {
135
+ errors.push({
136
+ severity: "warning",
137
+ message: "Recomendado: crear core/index.tsx con featureConfig",
138
+ file: "core/index.tsx",
139
+ hint: `export const featureConfig = {
140
+ dataSources,
141
+ meta: { name: 'Mi Vista', version: '1.0.0' }
142
+ };`,
143
+ });
144
+ }
145
+ else {
146
+ const coreContent = fs.readFileSync(path.join(dir, "core/index.tsx"), "utf-8");
147
+ if (!coreContent.includes("featureConfig")) {
148
+ errors.push({
149
+ severity: "warning",
150
+ message: "core/index.tsx debería exportar featureConfig",
151
+ file: "core/index.tsx",
152
+ });
153
+ }
154
+ }
155
+ // 4. RECOMMENDED: views/page.tsx or similar
156
+ const hasPageComponent = allFiles.some((f) => f.startsWith("views/") && (f.endsWith(".tsx") || f.endsWith(".jsx")));
157
+ if (!hasPageComponent) {
158
+ errors.push({
159
+ severity: "warning",
160
+ message: "Recomendado: crear views/page.tsx (componente principal)",
161
+ file: "views/page.tsx",
162
+ });
163
+ }
164
+ return errors;
165
+ }
166
+ /**
167
+ * Get all files in directory recursively (relative paths)
168
+ */
169
+ function getAllFilesInDir(dir) {
170
+ const files = [];
171
+ function scan(currentDir, relativePath = "") {
172
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
173
+ for (const entry of entries) {
174
+ const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
175
+ if (entry.isDirectory()) {
176
+ // Skip hidden dirs and node_modules
177
+ if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
178
+ scan(path.join(currentDir, entry.name), relPath);
179
+ }
180
+ }
181
+ else if (!entry.name.startsWith(".")) {
182
+ files.push(relPath);
183
+ }
184
+ }
185
+ }
186
+ scan(dir);
187
+ return files;
188
+ }
189
+ function showStructureErrors(errors) {
190
+ const criticalErrors = errors.filter((e) => e.severity === "error");
191
+ const warnings = errors.filter((e) => e.severity === "warning");
192
+ if (criticalErrors.length > 0) {
193
+ console.log(chalk.red("\n ✗ Errores de estructura (BLOQUEANTES):"));
194
+ for (const error of criticalErrors) {
195
+ console.log(chalk.red(` • ${error.message}`));
196
+ if (error.hint) {
197
+ console.log(chalk.gray(` ${error.hint.split("\n").join("\n ")}`));
198
+ }
199
+ }
200
+ }
201
+ if (warnings.length > 0) {
202
+ console.log(chalk.yellow("\n ⚠ Advertencias de estructura:"));
203
+ for (const warning of warnings) {
204
+ console.log(chalk.yellow(` • ${warning.message}`));
205
+ if (warning.hint) {
206
+ console.log(chalk.gray(` ${warning.hint.split("\n").join("\n ")}`));
207
+ }
208
+ }
209
+ }
210
+ }
54
211
  export async function pushCommand(viewDir) {
55
212
  if (!isLoggedIn()) {
56
213
  console.log(chalk.red("\n ✗ No estás logueado. Usa: gufi login\n"));
@@ -87,6 +244,25 @@ export async function pushCommand(viewDir) {
87
244
  console.log(chalk.yellow(` • ${file}`));
88
245
  });
89
246
  console.log();
247
+ // 💜 Structure validation (BEFORE pushing)
248
+ const structureSpinner = ora("Validando estructura...").start();
249
+ const structureErrors = validateViewStructure(dir);
250
+ const criticalErrors = structureErrors.filter((e) => e.severity === "error");
251
+ const structureWarnings = structureErrors.filter((e) => e.severity === "warning");
252
+ if (criticalErrors.length > 0) {
253
+ structureSpinner.fail(chalk.red("Estructura inválida"));
254
+ showStructureErrors(structureErrors);
255
+ console.log(chalk.red("\n ✗ Push bloqueado. Corrige los errores primero.\n"));
256
+ process.exit(1);
257
+ }
258
+ else if (structureWarnings.length > 0) {
259
+ structureSpinner.warn(chalk.yellow("Advertencias de estructura"));
260
+ showStructureErrors(structureErrors);
261
+ console.log();
262
+ }
263
+ else {
264
+ structureSpinner.succeed(chalk.green("Estructura válida"));
265
+ }
90
266
  // Security scan
91
267
  const securitySpinner = ora("Escaneando seguridad...").start();
92
268
  const securityWarnings = scanFilesInDirectory(dir, changedFiles);
package/dist/mcp.js CHANGED
@@ -49,6 +49,35 @@ function setCachedViewContext(viewId, context) {
49
49
  viewContextCache.set(viewId, { data: context, timestamp: Date.now() });
50
50
  }
51
51
  // ════════════════════════════════════════════════════════════════════════════
52
+ // Analytics - Track MCP tool calls
53
+ // ════════════════════════════════════════════════════════════════════════════
54
+ async function trackAnalytics(data) {
55
+ try {
56
+ const token = getToken();
57
+ if (!token) {
58
+ console.error("[MCP Analytics] No token, skipping");
59
+ return;
60
+ }
61
+ const url = `${getApiUrl()}/api/cli/mcp-analytics`;
62
+ console.error(`[MCP Analytics] Tracking ${data.tool_name} to ${url}`);
63
+ const res = await fetch(url, {
64
+ method: "POST",
65
+ headers: {
66
+ "Content-Type": "application/json",
67
+ "X-Client": "mcp",
68
+ Authorization: `Bearer ${token}`,
69
+ },
70
+ body: JSON.stringify(data),
71
+ });
72
+ if (!res.ok) {
73
+ console.error(`[MCP Analytics] Failed: ${res.status}`);
74
+ }
75
+ }
76
+ catch (err) {
77
+ console.error(`[MCP Analytics] Error: ${err.message}`);
78
+ }
79
+ }
80
+ // ════════════════════════════════════════════════════════════════════════════
52
81
  // API Client (reused from CLI)
53
82
  // ════════════════════════════════════════════════════════════════════════════
54
83
  async function autoLogin() {
@@ -235,6 +264,79 @@ function normalizeFileFields(data) {
235
264
  return normalized;
236
265
  }
237
266
  // ════════════════════════════════════════════════════════════════════════════
267
+ // Table Name Resolution (logical → physical)
268
+ // ════════════════════════════════════════════════════════════════════════════
269
+ // Cache for company schemas (to resolve logical table names)
270
+ const schemaCache = new Map();
271
+ const SCHEMA_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
272
+ /**
273
+ * Physical table name regex: m{moduleId}_t{tableId}
274
+ */
275
+ const PHYSICAL_TABLE_RE = /^m\d+_t\d+$/;
276
+ /**
277
+ * Get company schema (cached)
278
+ */
279
+ async function getCompanySchema(companyId) {
280
+ const cached = schemaCache.get(companyId);
281
+ if (cached && Date.now() - cached.timestamp < SCHEMA_CACHE_TTL) {
282
+ return cached.schema;
283
+ }
284
+ try {
285
+ const data = await apiRequest("/api/cli/schema", {}, companyId);
286
+ schemaCache.set(companyId, { schema: data, timestamp: Date.now() });
287
+ return data;
288
+ }
289
+ catch (err) {
290
+ console.error(`[MCP] Failed to get schema for company ${companyId}:`, err.message);
291
+ return null;
292
+ }
293
+ }
294
+ /**
295
+ * Resolve a table name: logical (module.entity) → physical (m123_t456)
296
+ * If already physical, returns as-is
297
+ * @param tableName - logical name like "taller.clientes" or physical like "m366_t12269"
298
+ * @param companyId - company ID for schema lookup
299
+ * @returns physical table name or throws error
300
+ */
301
+ async function resolveTableName(tableName, companyId) {
302
+ // If already physical, return as-is
303
+ if (PHYSICAL_TABLE_RE.test(tableName)) {
304
+ return tableName;
305
+ }
306
+ // Try to resolve logical name (module.entity format)
307
+ const parts = tableName.split(".");
308
+ if (parts.length !== 2) {
309
+ throw new Error(`Invalid table name: "${tableName}". Use physical (m123_t456) or logical (module.entity) format`);
310
+ }
311
+ const [moduleName, entityName] = parts;
312
+ const schema = await getCompanySchema(companyId);
313
+ if (!schema?.modules) {
314
+ throw new Error(`Cannot resolve logical table name "${tableName}": schema not available. Use physical name (m123_t456) instead`);
315
+ }
316
+ // Find module and entity
317
+ for (const mod of schema.modules) {
318
+ const modMatch = mod.name?.toLowerCase() === moduleName.toLowerCase() ||
319
+ mod.label?.toLowerCase() === moduleName.toLowerCase();
320
+ if (!modMatch)
321
+ continue;
322
+ for (const ent of mod.entities || []) {
323
+ const entMatch = ent.name?.toLowerCase() === entityName.toLowerCase() ||
324
+ ent.label?.toLowerCase() === entityName.toLowerCase();
325
+ if (entMatch) {
326
+ // Found! Build physical name from entity ID
327
+ const moduleId = mod.id;
328
+ const tableId = ent.id;
329
+ if (moduleId && tableId) {
330
+ return `m${moduleId}_t${tableId}`;
331
+ }
332
+ }
333
+ }
334
+ }
335
+ throw new Error(`Table "${tableName}" not found in schema. Available modules: ${schema.modules
336
+ .map((m) => m.name)
337
+ .join(", ")}`);
338
+ }
339
+ // ════════════════════════════════════════════════════════════════════════════
238
340
  // Tool Definitions - Descriptions loaded from docs/mcp/tool-descriptions.json
239
341
  // ════════════════════════════════════════════════════════════════════════════
240
342
  // Load tool descriptions from centralized documentation
@@ -891,7 +993,9 @@ const toolHandlers = {
891
993
  // ─────────────────────────────────────────────────────────────────────────
892
994
  async gufi_data(params) {
893
995
  // 💜 Unified data tool - all CRUD operations in one
894
- const { action, table, company_id } = params;
996
+ const { action, company_id } = params;
997
+ // 💜 Resolve logical table names (module.entity) to physical (m123_t456)
998
+ const table = await resolveTableName(params.table, company_id);
895
999
  switch (action) {
896
1000
  case "list": {
897
1001
  const limit = params.limit || 20;
@@ -1623,10 +1727,27 @@ async function handleRequest(request) {
1623
1727
  const handler = toolHandlers[toolName];
1624
1728
  if (!handler) {
1625
1729
  sendError(id, -32601, `Unknown tool: ${toolName}`);
1730
+ // Track unknown tool
1731
+ trackAnalytics({
1732
+ tool_name: toolName || "unknown",
1733
+ success: false,
1734
+ error_message: `Unknown tool: ${toolName}`,
1735
+ duration_ms: 0,
1736
+ input: toolParams,
1737
+ });
1626
1738
  return;
1627
1739
  }
1740
+ const startTime = Date.now();
1628
1741
  try {
1629
1742
  const result = await handler(toolParams);
1743
+ const duration = Date.now() - startTime;
1744
+ // Track success
1745
+ trackAnalytics({
1746
+ tool_name: toolName,
1747
+ success: true,
1748
+ duration_ms: duration,
1749
+ input: toolParams,
1750
+ });
1630
1751
  sendResponse({
1631
1752
  jsonrpc: "2.0",
1632
1753
  id,
@@ -1641,6 +1762,15 @@ async function handleRequest(request) {
1641
1762
  });
1642
1763
  }
1643
1764
  catch (err) {
1765
+ const duration = Date.now() - startTime;
1766
+ // Track error
1767
+ trackAnalytics({
1768
+ tool_name: toolName,
1769
+ success: false,
1770
+ error_message: err.message,
1771
+ duration_ms: duration,
1772
+ input: toolParams,
1773
+ });
1644
1774
  sendResponse({
1645
1775
  jsonrpc: "2.0",
1646
1776
  id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gufi-cli",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "CLI for developing Gufi Marketplace views locally with Claude Code",
5
5
  "bin": {
6
6
  "gufi": "./bin/gufi.js"