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.
- package/dist/commands/push.js +176 -0
- package/dist/mcp.js +131 -1
- package/package.json +1 -1
package/dist/commands/push.js
CHANGED
|
@@ -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,
|
|
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,
|