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.
- package/dist/commands/push.js +176 -0
- package/dist/mcp.js +140 -6
- 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
|
|
@@ -289,9 +391,10 @@ const TOOLS = [
|
|
|
289
391
|
items: {
|
|
290
392
|
type: "object",
|
|
291
393
|
properties: {
|
|
292
|
-
op: { type: "string", description: "Operation:
|
|
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
|
|
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,
|
|
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
|
-
|
|
993
|
-
|
|
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
|
-
|
|
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,
|