gufi-cli 0.1.31 → 0.1.35

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
@@ -289,9 +391,10 @@ const TOOLS = [
289
391
  items: {
290
392
  type: "object",
291
393
  properties: {
292
- op: { type: "string", description: "Operation: add_field, update_field, remove_field, add_entity, update_entity, remove_entity" },
394
+ op: { type: "string", description: "Operation: add_module, remove_module, add_submodule, remove_submodule, add_entity, update_entity, remove_entity, add_field, update_field, remove_field" },
293
395
  entity: { type: "string", description: "Entity reference: id, name, or 'module.entity'" },
294
- module: { type: "string", description: "Module reference (for add_entity)" },
396
+ module: { type: "string", description: "Module reference or definition { name, label }" },
397
+ submodule: { type: "string", description: "Submodule reference or definition { name, label }" },
295
398
  field: { type: "object", description: "Field definition (for add_field)" },
296
399
  field_name: { type: "string", description: "Field name (for update_field, remove_field)" },
297
400
  changes: { type: "object", description: "Changes to apply (for update_*)" },
@@ -419,6 +522,7 @@ Example: gufi_view_pull({ view_id: 13 })`,
419
522
  type: "object",
420
523
  properties: {
421
524
  view_id: { type: "number", description: "View ID to pull" },
525
+ company_id: { type: "string", description: "Company ID (required for company-specific views)" },
422
526
  },
423
527
  required: ["view_id"],
424
528
  },
@@ -891,7 +995,9 @@ const toolHandlers = {
891
995
  // ─────────────────────────────────────────────────────────────────────────
892
996
  async gufi_data(params) {
893
997
  // 💜 Unified data tool - all CRUD operations in one
894
- const { action, table, company_id } = params;
998
+ const { action, company_id } = params;
999
+ // 💜 Resolve logical table names (module.entity) to physical (m123_t456)
1000
+ const table = await resolveTableName(params.table, company_id);
895
1001
  switch (action) {
896
1002
  case "list": {
897
1003
  const limit = params.limit || 20;
@@ -989,10 +1095,12 @@ const toolHandlers = {
989
1095
  // ─────────────────────────────────────────────────────────────────────────
990
1096
  async gufi_view_pull(params) {
991
1097
  const viewId = params.view_id;
992
- // Get view info for package_id
993
- const viewResponse = await apiRequest(`/api/marketplace/views/${viewId}`);
1098
+ const companyId = params.company_id;
1099
+ // Get view info for package_id (pass company_id for access check)
1100
+ const viewResponse = await apiRequest(`/api/marketplace/views/${viewId}`, {}, companyId);
994
1101
  const view = viewResponse.data || viewResponse;
995
- if (!view || !view.id) {
1102
+ // 💜 Backend returns pk_id, not id
1103
+ if (!view || !view.pk_id) {
996
1104
  return { error: `View ${viewId} not found` };
997
1105
  }
998
1106
  const viewKey = `view_${viewId}`;
@@ -1623,10 +1731,27 @@ async function handleRequest(request) {
1623
1731
  const handler = toolHandlers[toolName];
1624
1732
  if (!handler) {
1625
1733
  sendError(id, -32601, `Unknown tool: ${toolName}`);
1734
+ // Track unknown tool
1735
+ trackAnalytics({
1736
+ tool_name: toolName || "unknown",
1737
+ success: false,
1738
+ error_message: `Unknown tool: ${toolName}`,
1739
+ duration_ms: 0,
1740
+ input: toolParams,
1741
+ });
1626
1742
  return;
1627
1743
  }
1744
+ const startTime = Date.now();
1628
1745
  try {
1629
1746
  const result = await handler(toolParams);
1747
+ const duration = Date.now() - startTime;
1748
+ // Track success
1749
+ trackAnalytics({
1750
+ tool_name: toolName,
1751
+ success: true,
1752
+ duration_ms: duration,
1753
+ input: toolParams,
1754
+ });
1630
1755
  sendResponse({
1631
1756
  jsonrpc: "2.0",
1632
1757
  id,
@@ -1641,6 +1766,15 @@ async function handleRequest(request) {
1641
1766
  });
1642
1767
  }
1643
1768
  catch (err) {
1769
+ const duration = Date.now() - startTime;
1770
+ // Track error
1771
+ trackAnalytics({
1772
+ tool_name: toolName,
1773
+ success: false,
1774
+ error_message: err.message,
1775
+ duration_ms: duration,
1776
+ input: toolParams,
1777
+ });
1644
1778
  sendResponse({
1645
1779
  jsonrpc: "2.0",
1646
1780
  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.35",
4
4
  "description": "CLI for developing Gufi Marketplace views locally with Claude Code",
5
5
  "bin": {
6
6
  "gufi": "./bin/gufi.js"