@locofy/mcp 0.1.0 → 1.0.2
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/build/tools/pullComponents.js +286 -11
- package/package.json +1 -1
|
@@ -7,14 +7,17 @@ import axios from 'axios';
|
|
|
7
7
|
* - Returns a hardcoded representation of a directory structure
|
|
8
8
|
* - Contains nested folders, files, and file contents
|
|
9
9
|
*/
|
|
10
|
-
export const pullComponentsToolName = '
|
|
11
|
-
export const pullComponentsToolDescription =
|
|
10
|
+
export const pullComponentsToolName = 'getLatestComponentAndDependencyCode';
|
|
11
|
+
export const pullComponentsToolDescription = `Cursor IDE leverages this MCP tool to scan directory structures with nested folders, files, and their contents. It then compares this information with the current code in the IDE to intelligently update components and their dependencies. If codeChanged is true, the tool will prompt the user to accept the changes before updating the component and its dependencies. If codeChanged is false, the tool will not update the component and its dependencies. The tool ensures that users are informed of the components retrieved and their relationships, providing a clear understanding of the updates being made
|
|
12
|
+
`;
|
|
12
13
|
export const PullComponentsToolSchema = z.object({
|
|
13
14
|
componentNames: z.array(z.string()).describe('The list of component or screen names to be retrieved.').max(10).min(1),
|
|
14
15
|
workspacePath: z.string().describe('The full path to the workspace.'),
|
|
15
16
|
});
|
|
16
17
|
export async function runPullComponentsTool(args) {
|
|
17
|
-
|
|
18
|
+
let componentNames = args.componentNames;
|
|
19
|
+
// clean the component names, remove extensions
|
|
20
|
+
componentNames = componentNames.map(name => name.replace(/\.[^/.]+$/, ''));
|
|
18
21
|
const workspacePath = decodeURIComponent(args.workspacePath);
|
|
19
22
|
const projectID = process.env.PROJECT_ID;
|
|
20
23
|
const personalAccessToken = process.env.PERSONAL_ACCESS_TOKEN;
|
|
@@ -24,13 +27,24 @@ export async function runPullComponentsTool(args) {
|
|
|
24
27
|
try {
|
|
25
28
|
const result = await fetchDirectoryStructure(componentNames, projectID, personalAccessToken);
|
|
26
29
|
if (result.success) {
|
|
30
|
+
// Prepend workspacePath to all filePath attributes
|
|
31
|
+
prepareFilePaths(result.data, workspacePath);
|
|
27
32
|
// Download and save any files referenced in the response
|
|
28
|
-
await downloadAndSaveAssets(result.data
|
|
33
|
+
await downloadAndSaveAssets(result.data);
|
|
34
|
+
// Check for code changes and mark changed files
|
|
35
|
+
await checkForCodeChanges(result.data);
|
|
36
|
+
// clean the directory structure
|
|
37
|
+
const cleanedResult = cleanDirectoryStructure(result.data);
|
|
38
|
+
const resultJSON = cleanedResult;
|
|
39
|
+
// write resultJSON to a file
|
|
40
|
+
if (process.env.DEBUG_MODE === 'true') {
|
|
41
|
+
fs.writeFileSync('result.json', JSON.stringify(resultJSON.children[0], null, 2));
|
|
42
|
+
}
|
|
29
43
|
return {
|
|
30
44
|
content: [
|
|
31
45
|
{
|
|
32
46
|
type: 'text',
|
|
33
|
-
text: JSON.stringify(
|
|
47
|
+
text: JSON.stringify(resultJSON.children[0], null, 2)
|
|
34
48
|
}
|
|
35
49
|
]
|
|
36
50
|
};
|
|
@@ -56,7 +70,7 @@ export async function runPullComponentsTool(args) {
|
|
|
56
70
|
content: [
|
|
57
71
|
{
|
|
58
72
|
type: 'text',
|
|
59
|
-
text: JSON.stringify({ error: 'Failed to fetch directory structure' }, null, 2)
|
|
73
|
+
text: JSON.stringify({ error: 'Failed to fetch directory structure ' + error }, null, 2)
|
|
60
74
|
}
|
|
61
75
|
]
|
|
62
76
|
};
|
|
@@ -64,7 +78,8 @@ export async function runPullComponentsTool(args) {
|
|
|
64
78
|
}
|
|
65
79
|
async function fetchDirectoryStructure(componentNames, projectID, personalAccessToken) {
|
|
66
80
|
// TODO: change to PROD url
|
|
67
|
-
const
|
|
81
|
+
const encodedNames = componentNames.map(name => encodeURIComponent(name)).join(',');
|
|
82
|
+
const url = 'https://codegen-api.locofy.dev/mcp/generators/' + projectID + '?name=' + encodedNames;
|
|
68
83
|
const headers = {
|
|
69
84
|
'Accept': '*/*',
|
|
70
85
|
'Content-Type': 'application/json',
|
|
@@ -129,13 +144,79 @@ async function fetchDirectoryStructure(componentNames, projectID, personalAccess
|
|
|
129
144
|
};
|
|
130
145
|
}
|
|
131
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Traverses the directory structure and prepends workspacePath to all filePath attributes
|
|
149
|
+
* @param directoryStructure The directory structure from the API response
|
|
150
|
+
* @param workspacePath The workspace path to prepend to file paths
|
|
151
|
+
*/
|
|
152
|
+
function prepareFilePaths(directoryStructure, workspacePath) {
|
|
153
|
+
const processNode = (node) => {
|
|
154
|
+
if (node.filePath) {
|
|
155
|
+
if (!path.isAbsolute(node.filePath)) {
|
|
156
|
+
node.filePath = path.join(workspacePath, node.filePath);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Process all file paths in exportData
|
|
160
|
+
if (node.exportData) {
|
|
161
|
+
// Handle exportData.filePath
|
|
162
|
+
if (node.exportData.filePath) {
|
|
163
|
+
if (!path.isAbsolute(node.exportData.filePath)) {
|
|
164
|
+
node.exportData.filePath = path.join(workspacePath, node.exportData.filePath);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Handle exportData.files array
|
|
168
|
+
if (node.exportData.files && Array.isArray(node.exportData.files)) {
|
|
169
|
+
for (const file of node.exportData.files) {
|
|
170
|
+
if (file.filePath && !path.isAbsolute(file.filePath)) {
|
|
171
|
+
file.filePath = path.join(workspacePath, file.filePath);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Handle dependencies array
|
|
176
|
+
if (node.exportData.dependencies && Array.isArray(node.exportData.dependencies)) {
|
|
177
|
+
for (const dependency of node.exportData.dependencies) {
|
|
178
|
+
// Process dependency filePath
|
|
179
|
+
if (dependency.filePath && !path.isAbsolute(dependency.filePath)) {
|
|
180
|
+
dependency.filePath = path.join(workspacePath, dependency.filePath);
|
|
181
|
+
}
|
|
182
|
+
// Process dependency files array
|
|
183
|
+
if (dependency.files && Array.isArray(dependency.files)) {
|
|
184
|
+
for (const file of dependency.files) {
|
|
185
|
+
if (file.filePath && !path.isAbsolute(file.filePath)) {
|
|
186
|
+
file.filePath = path.join(workspacePath, file.filePath);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Recursively process any nested dependencies if they exist
|
|
191
|
+
if (dependency.dependencies && Array.isArray(dependency.dependencies)) {
|
|
192
|
+
for (const nestedDep of dependency.dependencies) {
|
|
193
|
+
processNode(nestedDep); // Recursively process nested dependencies
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Process content.filePath if it exists
|
|
200
|
+
if (node.content && node.content.filePath) {
|
|
201
|
+
if (!path.isAbsolute(node.content.filePath)) {
|
|
202
|
+
node.content.filePath = path.join(workspacePath, node.content.filePath);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Process children recursively
|
|
206
|
+
if (node.children && Array.isArray(node.children)) {
|
|
207
|
+
for (const child of node.children) {
|
|
208
|
+
processNode(child);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
// Start processing from the root
|
|
213
|
+
processNode(directoryStructure);
|
|
214
|
+
}
|
|
132
215
|
/**
|
|
133
216
|
* Downloads and saves asset files from the directory structure
|
|
134
217
|
* @param directoryStructure The directory structure from the API response
|
|
135
|
-
* @param workspacePath The workspace path to save files to
|
|
136
218
|
*/
|
|
137
|
-
|
|
138
|
-
async function downloadAndSaveAssets(directoryStructure, workspacePath) {
|
|
219
|
+
async function downloadAndSaveAssets(directoryStructure) {
|
|
139
220
|
// Process each file in the directory structure
|
|
140
221
|
const processNode = async (node) => {
|
|
141
222
|
/* eslint-enable */
|
|
@@ -143,7 +224,7 @@ async function downloadAndSaveAssets(directoryStructure, workspacePath) {
|
|
|
143
224
|
// For each file in the files array
|
|
144
225
|
for (const file of node.exportData.files) {
|
|
145
226
|
if (file.url && file.fileName) {
|
|
146
|
-
const filePath =
|
|
227
|
+
const filePath = file.filePath;
|
|
147
228
|
// Create the directory structure if it doesn't exist
|
|
148
229
|
const dirPath = path.dirname(filePath);
|
|
149
230
|
if (!fs.existsSync(dirPath)) {
|
|
@@ -178,3 +259,197 @@ async function downloadAndSaveAssets(directoryStructure, workspacePath) {
|
|
|
178
259
|
// Start processing from the root
|
|
179
260
|
await processNode(directoryStructure);
|
|
180
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Checks if the current code on disk differs from the code received from the API
|
|
264
|
+
* @param directoryStructure The directory structure from the API response
|
|
265
|
+
*/
|
|
266
|
+
async function checkForCodeChanges(directoryStructure) {
|
|
267
|
+
// Process a single file and check if its content has changed
|
|
268
|
+
function checkFile(filePath, content) {
|
|
269
|
+
if (!fs.existsSync(filePath)) {
|
|
270
|
+
console.log(`New file: ${filePath}`);
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
275
|
+
const codeChanged = hasCodeChanged(fileContent, content);
|
|
276
|
+
if (codeChanged) {
|
|
277
|
+
console.log(`File changed: ${filePath}`);
|
|
278
|
+
}
|
|
279
|
+
return codeChanged;
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
console.error(`Error reading file ${filePath}:`, error);
|
|
283
|
+
return true; // Consider file as changed if we can't read it
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Check CSS module file if it exists
|
|
287
|
+
function checkCssFile(basePath, cssFileName, cssContent) {
|
|
288
|
+
if (!cssFileName || !cssContent)
|
|
289
|
+
return false;
|
|
290
|
+
const cssFilePath = path.join(path.dirname(basePath), cssFileName);
|
|
291
|
+
if (fs.existsSync(cssFilePath)) {
|
|
292
|
+
const cssFileContent = fs.readFileSync(cssFilePath, 'utf8');
|
|
293
|
+
const cssChanged = hasCodeChanged(cssFileContent, cssContent);
|
|
294
|
+
if (cssChanged) {
|
|
295
|
+
console.log(`CSS module changed: ${cssFilePath}`);
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return true; // CSS file doesn't exist, consider as new/changed
|
|
303
|
+
}
|
|
304
|
+
// Update node with change status and instructions
|
|
305
|
+
function updateNodeStatus(node, fileName, hasChanged, isCss = false) {
|
|
306
|
+
if (!hasChanged) {
|
|
307
|
+
const message = `This file ${fileName} has no changes, no need to update`;
|
|
308
|
+
if (isCss) {
|
|
309
|
+
node.cssContent = message;
|
|
310
|
+
node.instructionCSSForIDE = message;
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
node.instructionForIDE = message;
|
|
314
|
+
node.content = message;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
else if (!fs.existsSync(node.filePath)) {
|
|
318
|
+
node.instructionForIDE = 'This file does not exist, please create it';
|
|
319
|
+
}
|
|
320
|
+
return hasChanged;
|
|
321
|
+
}
|
|
322
|
+
// The main recursive function to process nodes
|
|
323
|
+
function processNode(node) {
|
|
324
|
+
let hasChanges = false;
|
|
325
|
+
// Remove experimental fields first
|
|
326
|
+
if (node.exportData) {
|
|
327
|
+
// Process main component file
|
|
328
|
+
if (node.filePath) {
|
|
329
|
+
const nodeContent = node.name.includes('.css') ?
|
|
330
|
+
node.exportData.cssContent :
|
|
331
|
+
node.exportData.content;
|
|
332
|
+
const fileChanged = checkFile(node.filePath, nodeContent);
|
|
333
|
+
node.codeChanged = fileChanged;
|
|
334
|
+
hasChanges = hasChanges || fileChanged;
|
|
335
|
+
updateNodeStatus(node, node.name, fileChanged);
|
|
336
|
+
// Check associated CSS module
|
|
337
|
+
if (node.exportData.cssFileName && node.exportData.cssContent) {
|
|
338
|
+
const cssChanged = checkCssFile(node.filePath, node.exportData.cssFileName, node.exportData.cssContent);
|
|
339
|
+
hasChanges = hasChanges || cssChanged;
|
|
340
|
+
updateNodeStatus(node.exportData, node.exportData.cssFileName, cssChanged, true);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Process dependencies
|
|
344
|
+
if (node.exportData.dependencies && Array.isArray(node.exportData.dependencies)) {
|
|
345
|
+
for (const dependency of node.exportData.dependencies) {
|
|
346
|
+
// Process dependency file
|
|
347
|
+
if (dependency.filePath) {
|
|
348
|
+
const isCSS = dependency.name && dependency.name.includes('.css');
|
|
349
|
+
const depContent = isCSS ? dependency.cssContent : dependency.content;
|
|
350
|
+
const fileChanged = checkFile(dependency.filePath, depContent);
|
|
351
|
+
dependency.codeChanged = fileChanged;
|
|
352
|
+
hasChanges = hasChanges || fileChanged;
|
|
353
|
+
updateNodeStatus(dependency, dependency.compName, fileChanged);
|
|
354
|
+
// Check associated CSS module
|
|
355
|
+
if (dependency.cssFileName && dependency.cssContent) {
|
|
356
|
+
const cssChanged = checkCssFile(dependency.filePath, dependency.cssFileName, dependency.cssContent);
|
|
357
|
+
hasChanges = hasChanges || cssChanged;
|
|
358
|
+
updateNodeStatus(dependency, dependency.cssFileName, cssChanged, true);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Process nested dependencies recursively
|
|
362
|
+
if (dependency.dependencies && Array.isArray(dependency.dependencies)) {
|
|
363
|
+
for (const nestedDep of dependency.dependencies) {
|
|
364
|
+
const nestedChanged = processNode(nestedDep);
|
|
365
|
+
hasChanges = hasChanges || nestedChanged;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// Process children recursively
|
|
372
|
+
if (node.children && Array.isArray(node.children)) {
|
|
373
|
+
for (const child of node.children) {
|
|
374
|
+
const childChanged = processNode(child);
|
|
375
|
+
hasChanges = hasChanges || childChanged;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return hasChanges;
|
|
379
|
+
}
|
|
380
|
+
// Start processing from the root
|
|
381
|
+
return processNode(directoryStructure);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Compares two code snippets to determine if they are functionally different
|
|
385
|
+
* @param fileContent The code currently in the file
|
|
386
|
+
* @param apiCode The code provided by the API
|
|
387
|
+
* @returns True if the code has changed, false otherwise
|
|
388
|
+
*/
|
|
389
|
+
function hasCodeChanged(fileContent, apiCode) {
|
|
390
|
+
if (!fileContent || !apiCode) {
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
// Normalize both code snippets
|
|
394
|
+
const normalizedFile = normalizeForComparison(fileContent);
|
|
395
|
+
const normalizedApi = normalizeForComparison(apiCode);
|
|
396
|
+
// Simple equality check on normalized versions
|
|
397
|
+
return normalizedFile !== normalizedApi;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Normalizes code for comparison by removing irrelevant differences
|
|
401
|
+
* @param code The code to normalize
|
|
402
|
+
* @returns Normalized code string
|
|
403
|
+
*/
|
|
404
|
+
function normalizeForComparison(code) {
|
|
405
|
+
return code
|
|
406
|
+
.replace(/\s+/g, '') // Remove all whitespace (spaces, tabs, newlines)
|
|
407
|
+
.replace(/[;,'"]/g, '') // Remove semicolons, commas, and quotes
|
|
408
|
+
.replace(/\/\/.*$/gm, '') // Remove single-line comments
|
|
409
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
|
|
410
|
+
.toLowerCase(); // Convert to lowercase for case-insensitive comparison
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Cleans the directory structure by removing unwanted properties from all nodes
|
|
414
|
+
* @param directoryStructure The directory structure to clean
|
|
415
|
+
* @returns The cleaned directory structure
|
|
416
|
+
*/
|
|
417
|
+
function cleanDirectoryStructure(directoryStructure) {
|
|
418
|
+
const propertiesToRemove = [
|
|
419
|
+
'files',
|
|
420
|
+
'component',
|
|
421
|
+
'jsLC',
|
|
422
|
+
'assetCount',
|
|
423
|
+
'cssLC',
|
|
424
|
+
'componentCount',
|
|
425
|
+
'pageMetaData',
|
|
426
|
+
'isHomePage',
|
|
427
|
+
'storybookFileName',
|
|
428
|
+
'isAutoSyncNode'
|
|
429
|
+
];
|
|
430
|
+
function cleanNode(node) {
|
|
431
|
+
// Remove unwanted properties from the current node
|
|
432
|
+
propertiesToRemove.forEach(prop => {
|
|
433
|
+
if (node[prop] !== undefined)
|
|
434
|
+
delete node[prop];
|
|
435
|
+
});
|
|
436
|
+
// Clean exportData if it exists
|
|
437
|
+
if (node.exportData) {
|
|
438
|
+
propertiesToRemove.forEach(prop => {
|
|
439
|
+
if (node.exportData[prop] !== undefined)
|
|
440
|
+
delete node.exportData[prop];
|
|
441
|
+
});
|
|
442
|
+
// Process dependencies in exportData
|
|
443
|
+
if (node.exportData.dependencies && Array.isArray(node.exportData.dependencies)) {
|
|
444
|
+
node.exportData.dependencies.forEach((dep) => cleanNode(dep));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// Process children recursively
|
|
448
|
+
if (node.children && Array.isArray(node.children)) {
|
|
449
|
+
node.children.forEach((child) => cleanNode(child));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// Start cleaning from the root
|
|
453
|
+
cleanNode(directoryStructure);
|
|
454
|
+
return directoryStructure;
|
|
455
|
+
}
|