@locofy/mcp 1.0.11 → 1.1.1

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.
@@ -1,505 +1 @@
1
- import { z } from 'zod';
2
- import * as fs from 'fs';
3
- import * as path from 'path';
4
- import axios from 'axios';
5
- import { getProjectID } from '../helpers/helpers.js';
6
- import { track } from '../utils/analytics.js';
7
- /**
8
- * PullFiles tool
9
- * - Returns a hardcoded representation of a directory structure
10
- * - Contains nested folders, files, and file contents
11
- */
12
- export const pullFilesToolName = 'getLatestFileCode';
13
- export const pullFilesToolDescription = `Cursor IDE uses this tool to retrieve individual file contents without component dependency management. Unlike the component tool that handles components and their dependencies as a unit, this tool focuses on specific files by name, retrieving only their content without dependency resolution. Use this tool when you need the raw content of specific files rather than complete components with their dependency trees. If codeChanged is true, the tool will prompt the user to accept the changes before updating the file. If codeChanged is false, the tool will not update the files.
14
- `;
15
- export const PullFilesToolSchema = z.object({
16
- fileNames: z.array(z.string()).describe('The list of specific file names to be retrieved, without resolving their component dependencies.').max(10).min(1),
17
- workspacePath: z.string().describe('The full path to the workspace.'),
18
- });
19
- export async function runPullFilesTool(args) {
20
- let fileNames = args.fileNames;
21
- // clean the file names, remove extensions
22
- fileNames = fileNames.map(name => name.replace(/\.[^/.]+$/, ''));
23
- const workspacePath = decodeURIComponent(args.workspacePath);
24
- const projectID = await getProjectID(workspacePath);
25
- if (!projectID || projectID.length === 0) {
26
- throw new Error('PROJECT_ID is not set');
27
- }
28
- const personalAccessToken = process.env.PERSONAL_ACCESS_TOKEN;
29
- if (!personalAccessToken) {
30
- throw new Error('PERSONAL_ACCESS_TOKEN is not set');
31
- }
32
- try {
33
- const result = await fetchDirectoryStructure(fileNames, projectID, personalAccessToken);
34
- if (result.success) {
35
- const userID = result.user_id;
36
- // Prepend workspacePath to all filePath attributes
37
- prepareFilePaths(result.data, workspacePath);
38
- // Download and save any files referenced in the response
39
- await downloadAndSaveAssets(result.data);
40
- // Check for code changes and mark changed files
41
- await checkForCodeChanges(result.data);
42
- // clean the directory structure
43
- const cleanedResult = processDirectoryStructure(result.data);
44
- track('mcp_execute_tool_e', {
45
- tool_name: pullFilesToolName,
46
- project_id: projectID,
47
- user_id: userID,
48
- source: 'cursor', // TODO: update it when we release vscode extension
49
- });
50
- return {
51
- content: [
52
- {
53
- type: 'text',
54
- text: JSON.stringify(cleanedResult, null, 2)
55
- }
56
- ]
57
- };
58
- }
59
- else {
60
- // Return the error message from fetchDirectoryStructure
61
- return {
62
- content: [
63
- {
64
- type: 'text',
65
- statusCode: result.statusCode || 'unknown',
66
- text: JSON.stringify({
67
- error: result.error,
68
- }, null, 2)
69
- }
70
- ]
71
- };
72
- }
73
- }
74
- catch (error) {
75
- console.error('Error fetching directory structure:', error);
76
- return {
77
- content: [
78
- {
79
- type: 'text',
80
- text: JSON.stringify({ error: 'Failed to fetch directory structure ' + error }, null, 2)
81
- }
82
- ]
83
- };
84
- }
85
- }
86
- async function fetchDirectoryStructure(fileNames, projectID, personalAccessToken) {
87
- const encodedNames = fileNames.map(name => encodeURIComponent(name)).join(',');
88
- let baseURL = 'https://codegen-api.locofy.ai/mcp/generators/';
89
- if (process.env.IS_DEV === 'true') {
90
- baseURL = 'https://codegen-api.locofy.dev/mcp/generators/';
91
- }
92
- const url = baseURL + projectID + '?name=' + encodedNames;
93
- const headers = {
94
- 'Accept': '*/*',
95
- 'Content-Type': 'application/json',
96
- 'Authorization': 'Bearer ' + personalAccessToken
97
- };
98
- const data = {};
99
- try {
100
- const response = await axios.post(url, data, { headers, });
101
- if (response.status !== 200) {
102
- // Instead of throwing, return an error object
103
- return {
104
- success: false,
105
- error: `API returned status code ${response.status}`,
106
- statusCode: response.status
107
- };
108
- }
109
- if (!response.data || !response.data.directoryTree) {
110
- return {
111
- success: false,
112
- error: 'API response is missing directoryTree property',
113
- statusCode: response.status
114
- };
115
- }
116
- return {
117
- success: true,
118
- data: response.data.directoryTree,
119
- user_id: response.data.user_id
120
- };
121
- }
122
- catch (error) {
123
- if (axios.isAxiosError(error)) {
124
- if (error.response) {
125
- const statusCode = error.response.status;
126
- let errorMessage = '';
127
- if (statusCode === 401) {
128
- errorMessage = 'Authentication failed: Invalid or expired token. Please check your mcp.json configuration in the .cursor folder to ensure your PERSONAL_ACCESS_TOKEN is correct. You can visit the Locofy project settings page to check or regenerate your token if needed.';
129
- }
130
- else if (statusCode === 500) {
131
- errorMessage = 'Server error: The API service is experiencing issues';
132
- }
133
- else {
134
- errorMessage = `API request failed with status ${statusCode}: ${error.message}`;
135
- }
136
- return {
137
- success: false,
138
- error: errorMessage,
139
- statusCode: statusCode
140
- };
141
- }
142
- else if (error.request) {
143
- return {
144
- success: false,
145
- error: 'Network error: No response received from API',
146
- statusCode: 'network_error'
147
- };
148
- }
149
- }
150
- console.error('API request failed:', error);
151
- return {
152
- success: false,
153
- error: `Failed to fetch directory structure: ${error instanceof Error ? error.message : 'Unknown error'}`,
154
- statusCode: 'unknown_error'
155
- };
156
- }
157
- }
158
- /**
159
- * Traverses the directory structure and prepends workspacePath to all filePath attributes
160
- * @param directoryStructure The directory structure from the API response
161
- * @param workspacePath The workspace path to prepend to file paths
162
- */
163
- function prepareFilePaths(directoryStructure, workspacePath) {
164
- const processNode = (node) => {
165
- if (node.filePath) {
166
- if (!path.isAbsolute(node.filePath)) {
167
- node.filePath = path.join(workspacePath, node.filePath);
168
- }
169
- }
170
- // Process all file paths in exportData
171
- if (node.exportData) {
172
- // Handle exportData.filePath
173
- if (node.exportData.filePath) {
174
- if (!path.isAbsolute(node.exportData.filePath)) {
175
- node.exportData.filePath = path.join(workspacePath, node.exportData.filePath);
176
- }
177
- }
178
- // Handle exportData.files array
179
- if (node.exportData.files && Array.isArray(node.exportData.files)) {
180
- for (const file of node.exportData.files) {
181
- if (file.filePath && !path.isAbsolute(file.filePath)) {
182
- file.filePath = path.join(workspacePath, file.filePath);
183
- }
184
- }
185
- }
186
- // Handle dependencies array
187
- if (node.exportData.dependencies && Array.isArray(node.exportData.dependencies)) {
188
- for (const dependency of node.exportData.dependencies) {
189
- // Process dependency filePath
190
- if (dependency.filePath && !path.isAbsolute(dependency.filePath)) {
191
- dependency.filePath = path.join(workspacePath, dependency.filePath);
192
- }
193
- // Process dependency files array
194
- if (dependency.files && Array.isArray(dependency.files)) {
195
- for (const file of dependency.files) {
196
- if (file.filePath && !path.isAbsolute(file.filePath)) {
197
- file.filePath = path.join(workspacePath, file.filePath);
198
- }
199
- }
200
- }
201
- // Recursively process any nested dependencies if they exist
202
- if (dependency.dependencies && Array.isArray(dependency.dependencies)) {
203
- for (const nestedDep of dependency.dependencies) {
204
- processNode(nestedDep); // Recursively process nested dependencies
205
- }
206
- }
207
- }
208
- }
209
- }
210
- // Process content.filePath if it exists
211
- if (node.content && node.content.filePath) {
212
- if (!path.isAbsolute(node.content.filePath)) {
213
- node.content.filePath = path.join(workspacePath, node.content.filePath);
214
- }
215
- }
216
- // Process children recursively
217
- if (node.children && Array.isArray(node.children)) {
218
- for (const child of node.children) {
219
- processNode(child);
220
- }
221
- }
222
- };
223
- // Start processing from the root
224
- processNode(directoryStructure);
225
- }
226
- /**
227
- * Downloads and saves asset files from the directory structure
228
- * @param directoryStructure The directory structure from the API response
229
- */
230
- async function downloadAndSaveAssets(directoryStructure) {
231
- // Process each file in the directory structure
232
- const processNode = async (node) => {
233
- /* eslint-enable */
234
- if (node.type === 'file' && node.exportData && node.exportData.files) {
235
- // For each file in the files array
236
- for (const file of node.exportData.files) {
237
- if (file.url && file.fileName) {
238
- const filePath = file.filePath;
239
- // Create the directory structure if it doesn't exist
240
- const dirPath = path.dirname(filePath);
241
- if (!fs.existsSync(dirPath)) {
242
- fs.mkdirSync(dirPath, { recursive: true });
243
- console.log(`Created directory: ${dirPath}`);
244
- }
245
- // Only download if file doesn't exist
246
- if (!fs.existsSync(filePath)) {
247
- try {
248
- console.log(`Downloading ${file.fileName} from ${file.url}`);
249
- const response = await axios.get(file.url, { responseType: 'arraybuffer' });
250
- fs.writeFileSync(filePath, Buffer.from(response.data));
251
- console.log(`Saved ${file.fileName} to ${filePath}`);
252
- }
253
- catch (error) {
254
- console.error(`Error downloading ${file.fileName}:`, error);
255
- }
256
- }
257
- else {
258
- console.log(`File ${filePath} already exists, skipping download`);
259
- }
260
- }
261
- }
262
- }
263
- // Process children recursively
264
- if (node.children && Array.isArray(node.children)) {
265
- for (const child of node.children) {
266
- await processNode(child);
267
- }
268
- }
269
- };
270
- // Start processing from the root
271
- await processNode(directoryStructure);
272
- }
273
- /**
274
- * Checks if the current code on disk differs from the code received from the API
275
- * @param directoryStructure The directory structure from the API response
276
- */
277
- async function checkForCodeChanges(directoryStructure) {
278
- // Process a single file and check if its content has changed
279
- function checkFile(filePath, content) {
280
- if (!fs.existsSync(filePath)) {
281
- console.log(`New file: ${filePath}`);
282
- return true;
283
- }
284
- try {
285
- const fileContent = fs.readFileSync(filePath, 'utf8');
286
- const codeChanged = hasCodeChanged(fileContent, content);
287
- if (codeChanged) {
288
- console.log(`File changed: ${filePath}`);
289
- }
290
- return codeChanged;
291
- }
292
- catch (error) {
293
- console.error(`Error reading file ${filePath}:`, error);
294
- return true; // Consider file as changed if we can't read it
295
- }
296
- }
297
- // Check CSS module file if it exists
298
- function checkCssFile(basePath, cssFileName, cssContent) {
299
- if (!cssFileName || !cssContent)
300
- return false;
301
- const cssFilePath = path.join(path.dirname(basePath), cssFileName);
302
- if (fs.existsSync(cssFilePath)) {
303
- const cssFileContent = fs.readFileSync(cssFilePath, 'utf8');
304
- const cssChanged = hasCodeChanged(cssFileContent, cssContent);
305
- if (cssChanged) {
306
- console.log(`CSS module changed: ${cssFilePath}`);
307
- return true;
308
- }
309
- else {
310
- return false;
311
- }
312
- }
313
- return true; // CSS file doesn't exist, consider as new/changed
314
- }
315
- // Update node with change status and instructions
316
- function updateNodeStatus(node, fileName, hasChanged, isCss = false) {
317
- if (!hasChanged) {
318
- const message = `This file ${fileName} has no changes, no need to update`;
319
- if (isCss) {
320
- node.cssContent = message;
321
- node.instructionCSSForIDE = message;
322
- }
323
- else {
324
- node.instructionForIDE = message;
325
- node.content = message;
326
- }
327
- }
328
- else if (!fs.existsSync(node.filePath)) {
329
- node.instructionForIDE = 'This file does not exist, please create it';
330
- }
331
- return hasChanged;
332
- }
333
- // The main recursive function to process nodes
334
- function processNode(node) {
335
- let hasChanges = false;
336
- // Remove experimental fields first
337
- if (node.exportData) {
338
- if (node.filePath) {
339
- const nodeContent = node.name.includes('.css') ?
340
- node.exportData.cssContent :
341
- node.exportData.content;
342
- const fileChanged = checkFile(node.filePath, nodeContent);
343
- node.codeChanged = fileChanged;
344
- hasChanges = hasChanges || fileChanged;
345
- updateNodeStatus(node, node.name, fileChanged);
346
- // Check associated CSS module
347
- if (node.exportData.cssFileName && node.exportData.cssContent) {
348
- const cssChanged = checkCssFile(node.filePath, node.exportData.cssFileName, node.exportData.cssContent);
349
- hasChanges = hasChanges || cssChanged;
350
- updateNodeStatus(node.exportData, node.exportData.cssFileName, cssChanged, true);
351
- }
352
- }
353
- // Process dependencies
354
- if (node.exportData.dependencies && Array.isArray(node.exportData.dependencies)) {
355
- for (const dependency of node.exportData.dependencies) {
356
- // Process dependency file
357
- if (dependency.filePath) {
358
- const isCSS = dependency.name && dependency.name.includes('.css');
359
- const depContent = isCSS ? dependency.cssContent : dependency.content;
360
- const fileChanged = checkFile(dependency.filePath, depContent);
361
- dependency.codeChanged = fileChanged;
362
- hasChanges = hasChanges || fileChanged;
363
- updateNodeStatus(dependency, dependency.compName, fileChanged);
364
- // Check associated CSS module
365
- if (dependency.cssFileName && dependency.cssContent) {
366
- const cssChanged = checkCssFile(dependency.filePath, dependency.cssFileName, dependency.cssContent);
367
- hasChanges = hasChanges || cssChanged;
368
- updateNodeStatus(dependency, dependency.cssFileName, cssChanged, true);
369
- }
370
- }
371
- // Process nested dependencies recursively
372
- if (dependency.dependencies && Array.isArray(dependency.dependencies)) {
373
- for (const nestedDep of dependency.dependencies) {
374
- const nestedChanged = processNode(nestedDep);
375
- hasChanges = hasChanges || nestedChanged;
376
- }
377
- }
378
- }
379
- }
380
- }
381
- // Process children recursively
382
- if (node.children && Array.isArray(node.children)) {
383
- for (const child of node.children) {
384
- const childChanged = processNode(child);
385
- hasChanges = hasChanges || childChanged;
386
- }
387
- }
388
- return hasChanges;
389
- }
390
- // Start processing from the root
391
- return processNode(directoryStructure);
392
- }
393
- /**
394
- * Compares two code snippets to determine if they are functionally different
395
- * @param fileContent The code currently in the file
396
- * @param apiCode The code provided by the API
397
- * @returns True if the code has changed, false otherwise
398
- */
399
- function hasCodeChanged(fileContent, apiCode) {
400
- if (!fileContent || !apiCode) {
401
- return true;
402
- }
403
- // Normalize both code snippets
404
- const normalizedFile = normalizeForComparison(fileContent);
405
- const normalizedApi = normalizeForComparison(apiCode);
406
- // Simple equality check on normalized versions
407
- return normalizedFile !== normalizedApi;
408
- }
409
- /**
410
- * Normalizes code for comparison by removing irrelevant differences
411
- * @param code The code to normalize
412
- * @returns Normalized code string
413
- */
414
- function normalizeForComparison(code) {
415
- return code
416
- .replace(/\s+/g, '') // Remove all whitespace (spaces, tabs, newlines)
417
- .replace(/[;,'"]/g, '') // Remove semicolons, commas, and quotes
418
- .replace(/\/\/.*$/gm, '') // Remove single-line comments
419
- .replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
420
- .toLowerCase(); // Convert to lowercase for case-insensitive comparison
421
- }
422
- /**
423
- * Cleans the directory structure by removing unwanted properties from all nodes
424
- * @param directoryStructure The directory structure to clean
425
- * @returns The cleaned directory structure
426
- */
427
- function processDirectoryStructure(directoryStructure) {
428
- const propertiesToRemove = [
429
- 'files',
430
- 'component',
431
- 'jsLC',
432
- 'assetCount',
433
- 'cssLC',
434
- 'componentCount',
435
- 'pageMetaData',
436
- 'isHomePage',
437
- 'storybookFileName',
438
- 'isAutoSyncNode'
439
- ];
440
- const filesToUpdate = [];
441
- const filesUnchanged = [];
442
- function collectChangeStatus(node) {
443
- // Check if this is a component node with a name and change status
444
- if (node.name && node.codeChanged !== undefined) {
445
- if (node.codeChanged) {
446
- filesToUpdate.push(node.name);
447
- }
448
- else {
449
- filesUnchanged.push(node.name);
450
- }
451
- }
452
- // Check dependencies
453
- if (node.exportData && node.exportData.dependencies) {
454
- node.exportData.dependencies.forEach((dep) => {
455
- if (dep.compName && dep.codeChanged !== undefined) {
456
- if (dep.codeChanged) {
457
- filesToUpdate.push(dep.compName);
458
- }
459
- else {
460
- filesUnchanged.push(dep.compName);
461
- }
462
- }
463
- // Check nested dependencies
464
- if (dep.dependencies && Array.isArray(dep.dependencies)) {
465
- dep.dependencies.forEach((nestedDep) => collectChangeStatus(nestedDep));
466
- }
467
- });
468
- }
469
- // Process children recursively
470
- if (node.children && Array.isArray(node.children)) {
471
- node.children.forEach((child) => collectChangeStatus(child));
472
- }
473
- }
474
- function cleanNode(node) {
475
- // Remove unwanted properties from the current node
476
- propertiesToRemove.forEach(prop => {
477
- if (node[prop] !== undefined)
478
- delete node[prop];
479
- });
480
- // Clean exportData if it exists
481
- if (node.exportData) {
482
- propertiesToRemove.forEach(prop => {
483
- if (node.exportData[prop] !== undefined)
484
- delete node.exportData[prop];
485
- });
486
- // Process dependencies in exportData
487
- if (node.exportData.dependencies && Array.isArray(node.exportData.dependencies)) {
488
- node.exportData.dependencies.forEach((dep) => cleanNode(dep));
489
- }
490
- }
491
- // Process children recursively
492
- if (node.children && Array.isArray(node.children)) {
493
- node.children.forEach((child) => cleanNode(child));
494
- }
495
- }
496
- // First collect all change statuses
497
- collectChangeStatus(directoryStructure);
498
- // Then clean the structure
499
- cleanNode(directoryStructure);
500
- return {
501
- directoryStructure: directoryStructure.children,
502
- filesToUpdate: [...new Set(filesToUpdate)],
503
- filesUnchanged: [...new Set(filesUnchanged)]
504
- };
505
- }
1
+ import{z}from"zod";import*as fs from"fs";import*as path from"path";import axios from"axios";import{getProjectID}from"../helpers/helpers.js";import{track}from"../utils/analytics.js";export const pullFilesToolName="getLatestFileCode";export const pullFilesToolDescription="Cursor IDE uses this tool to retrieve individual file contents without component dependency management. Unlike the component tool that handles components and their dependencies as a unit, this tool focuses on specific files by name, retrieving only their content without dependency resolution. Use this tool when you need the raw content of specific files rather than complete components with their dependency trees. If codeChanged is true, the tool will prompt the user to accept the changes before updating the file. If codeChanged is false, the tool will not update the files.\n ";export const PullFilesToolSchema=z.object({fileNames:z.array(z.string()).describe("The list of specific file names to be retrieved, without resolving their component dependencies.").max(10).min(1),workspacePath:z.string().describe("The full path to the workspace.")});export async function runPullFilesTool(args){let fileNames=args.fileNames;fileNames=fileNames.map((name=>name.replace(/\.[^/.]+$/,"")));const workspacePath=decodeURIComponent(args.workspacePath),projectID=await getProjectID(workspacePath);if(!projectID||0===projectID.length)throw new Error("PROJECT_ID is not set");const personalAccessToken=process.env.PERSONAL_ACCESS_TOKEN;if(!personalAccessToken)throw new Error("PERSONAL_ACCESS_TOKEN is not set");try{const result=await fetchDirectoryStructure(fileNames,projectID,personalAccessToken);if(result.success){const userID=result.user_id;prepareFilePaths(result.data,workspacePath),await downloadAndSaveAssets(result.data),await checkForCodeChanges(result.data);const cleanedResult=processDirectoryStructure(result.data);return track("mcp_execute_tool_e",{tool_name:pullFilesToolName,project_id:projectID,user_id:userID,source:"cursor"}),{content:[{type:"text",text:JSON.stringify(cleanedResult,null,2)}]}}return{content:[{type:"text",statusCode:result.statusCode||"unknown",text:JSON.stringify({error:result.error},null,2)}]}}catch(error){return{content:[{type:"text",text:JSON.stringify({error:"Failed to fetch directory structure "+error},null,2)}]}}}async function fetchDirectoryStructure(fileNames,projectID,personalAccessToken){const encodedNames=fileNames.map((name=>encodeURIComponent(name))).join(",");let baseURL="https://codegen-api.locofy.ai/mcp/generators/";"true"===process.env.IS_DEV&&(baseURL="https://codegen-api.locofy.dev/mcp/generators/");const url=baseURL+projectID+"?name="+encodedNames,headers={Accept:"*/*","Content-Type":"application/json",Authorization:"Bearer "+personalAccessToken},data={};try{const response=await axios.post(url,data,{headers:headers});return 200!==response.status?{success:!1,error:`API returned status code ${response.status}`,statusCode:response.status}:response.data&&response.data.directoryTree?{success:!0,data:response.data.directoryTree,user_id:response.data.user_id}:{success:!1,error:"API response is missing directoryTree property",statusCode:response.status}}catch(error){if(axios.isAxiosError(error)){if(error.response){const statusCode=error.response.status;let errorMessage="";return errorMessage=401===statusCode?"Authentication failed: Invalid or expired token. Please check your mcp.json configuration in the .cursor folder to ensure your PERSONAL_ACCESS_TOKEN is correct. You can visit the Locofy project settings page to check or regenerate your token if needed.":500===statusCode?"Server error: The API service is experiencing issues":`API request failed with status ${statusCode}: ${error.message}`,{success:!1,error:errorMessage,statusCode:statusCode}}if(error.request)return{success:!1,error:"Network error: No response received from API",statusCode:"network_error"}}return{success:!1,error:`Failed to fetch directory structure: ${error instanceof Error?error.message:"Unknown error"}`,statusCode:"unknown_error"}}}function prepareFilePaths(directoryStructure,workspacePath){const processNode=node=>{if(node.filePath&&(path.isAbsolute(node.filePath)||(node.filePath=path.join(workspacePath,node.filePath))),node.exportData){if(node.exportData.filePath&&(path.isAbsolute(node.exportData.filePath)||(node.exportData.filePath=path.join(workspacePath,node.exportData.filePath))),node.exportData.files&&Array.isArray(node.exportData.files))for(const file of node.exportData.files)file.filePath&&!path.isAbsolute(file.filePath)&&(file.filePath=path.join(workspacePath,file.filePath));if(node.exportData.dependencies&&Array.isArray(node.exportData.dependencies))for(const dependency of node.exportData.dependencies){if(dependency.filePath&&!path.isAbsolute(dependency.filePath)&&(dependency.filePath=path.join(workspacePath,dependency.filePath)),dependency.files&&Array.isArray(dependency.files))for(const file of dependency.files)file.filePath&&!path.isAbsolute(file.filePath)&&(file.filePath=path.join(workspacePath,file.filePath));if(dependency.dependencies&&Array.isArray(dependency.dependencies))for(const nestedDep of dependency.dependencies)processNode(nestedDep)}}if(node.content&&node.content.filePath&&(path.isAbsolute(node.content.filePath)||(node.content.filePath=path.join(workspacePath,node.content.filePath))),node.children&&Array.isArray(node.children))for(const child of node.children)processNode(child)};processNode(directoryStructure)}async function downloadAndSaveAssets(directoryStructure){const processNode=async node=>{if("file"===node.type&&node.exportData&&node.exportData.files)for(const file of node.exportData.files)if(file.url&&file.fileName){const filePath=file.filePath;if(!filePath)continue;const dirPath=path.dirname(filePath);if(fs.existsSync(dirPath)||fs.mkdirSync(dirPath,{recursive:!0}),!fs.existsSync(filePath))try{const response=await axios.get(file.url,{responseType:"arraybuffer"});fs.writeFileSync(filePath,Buffer.from(response.data))}catch(error){}}if(node.children&&Array.isArray(node.children))for(const child of node.children)await processNode(child)};await processNode(directoryStructure)}async function checkForCodeChanges(directoryStructure){function checkFile(filePath,content){if(!fs.existsSync(filePath))return!0;try{const codeChanged=hasCodeChanged(fs.readFileSync(filePath,"utf8"),content);return codeChanged}catch(error){return!0}}function checkCssFile(basePath,cssFileName,cssContent){if(!cssFileName||!cssContent)return!1;const cssFilePath=path.join(path.dirname(basePath),cssFileName);if(fs.existsSync(cssFilePath)){return!!hasCodeChanged(fs.readFileSync(cssFilePath,"utf8"),cssContent)}return!0}function updateNodeStatus(node,fileName,hasChanged,isCss=!1){if(hasChanged)fs.existsSync(node.filePath)||(node.instructionForIDE="This file does not exist, please create it");else{const message=`This file ${fileName} has no changes, no need to update`;isCss?(node.cssContent=message,node.instructionCSSForIDE=message):(node.instructionForIDE=message,node.content=message)}return hasChanged}return function processNode(node){let hasChanges=!1;if(node.exportData){if(node.filePath){const nodeContent=node.name.includes(".css")?node.exportData.cssContent:node.exportData.content,fileChanged=checkFile(node.filePath,nodeContent);if(node.codeChanged=fileChanged,hasChanges=hasChanges||fileChanged,updateNodeStatus(node,node.name,fileChanged),node.exportData.cssFileName&&node.exportData.cssContent){const cssChanged=checkCssFile(node.filePath,node.exportData.cssFileName,node.exportData.cssContent);hasChanges=hasChanges||cssChanged,updateNodeStatus(node.exportData,node.exportData.cssFileName,cssChanged,!0)}}if(node.exportData.dependencies&&Array.isArray(node.exportData.dependencies))for(const dependency of node.exportData.dependencies){if(dependency.filePath){const depContent=dependency.name&&dependency.name.includes(".css")?dependency.cssContent:dependency.content,fileChanged=checkFile(dependency.filePath,depContent);if(dependency.codeChanged=fileChanged,hasChanges=hasChanges||fileChanged,updateNodeStatus(dependency,dependency.compName,fileChanged),dependency.cssFileName&&dependency.cssContent){const cssChanged=checkCssFile(dependency.filePath,dependency.cssFileName,dependency.cssContent);hasChanges=hasChanges||cssChanged,updateNodeStatus(dependency,dependency.cssFileName,cssChanged,!0)}}if(dependency.dependencies&&Array.isArray(dependency.dependencies))for(const nestedDep of dependency.dependencies){const nestedChanged=processNode(nestedDep);hasChanges=hasChanges||nestedChanged}}}if(node.children&&Array.isArray(node.children))for(const child of node.children){const childChanged=processNode(child);hasChanges=hasChanges||childChanged}return hasChanges}(directoryStructure)}function hasCodeChanged(fileContent,apiCode){if(!fileContent||!apiCode)return!0;return normalizeForComparison(fileContent)!==normalizeForComparison(apiCode)}function normalizeForComparison(code){return code.replace(/\s+/g,"").replace(/[;,'"]/g,"").replace(/\/\/.*$/gm,"").replace(/\/\*[\s\S]*?\*\//g,"").toLowerCase()}function processDirectoryStructure(directoryStructure){const propertiesToRemove=["files","component","jsLC","assetCount","cssLC","componentCount","pageMetaData","isHomePage","storybookFileName","isAutoSyncNode"],filesToUpdate=[],filesUnchanged=[];return function collectChangeStatus(node){node.name&&void 0!==node.codeChanged&&(node.codeChanged?filesToUpdate.push(node.name):filesUnchanged.push(node.name)),node.exportData&&node.exportData.dependencies&&node.exportData.dependencies.forEach((dep=>{dep.compName&&void 0!==dep.codeChanged&&(dep.codeChanged?filesToUpdate.push(dep.compName):filesUnchanged.push(dep.compName)),dep.dependencies&&Array.isArray(dep.dependencies)&&dep.dependencies.forEach((nestedDep=>collectChangeStatus(nestedDep)))})),node.children&&Array.isArray(node.children)&&node.children.forEach((child=>collectChangeStatus(child)))}(directoryStructure),function cleanNode(node){propertiesToRemove.forEach((prop=>{void 0!==node[prop]&&delete node[prop]})),node.exportData&&(propertiesToRemove.forEach((prop=>{void 0!==node.exportData[prop]&&delete node.exportData[prop]})),node.exportData.dependencies&&Array.isArray(node.exportData.dependencies)&&node.exportData.dependencies.forEach((dep=>cleanNode(dep)))),node.children&&Array.isArray(node.children)&&node.children.forEach((child=>cleanNode(child)))}(directoryStructure),{directoryStructure:directoryStructure.children,filesToUpdate:[...new Set(filesToUpdate)],filesUnchanged:[...new Set(filesUnchanged)]}}
@@ -1,47 +1 @@
1
- import { MIXPANEL_PROJECT_TOKEN, MIXPANEL_PROXY_DOMAIN } from '../constants.js';
2
- export function track(event, props = {}) {
3
- if (process.env.NODE_ENV === 'test' || process.env.IS_TEST === 'true') {
4
- console.log('Analytics disabled: NODE_ENV is test or IS_TEST is true');
5
- return;
6
- }
7
- if (!MIXPANEL_PROJECT_TOKEN || !MIXPANEL_PROXY_DOMAIN) {
8
- console.log('Analytics disabled: MIXPANEL_PROJECT_TOKEN or MIXPANEL_PROXY_DOMAIN not set');
9
- return;
10
- }
11
- try {
12
- // Create the Mixpanel-compatible event object
13
- const eventObject = {
14
- event: event,
15
- properties: {
16
- ...props,
17
- token: MIXPANEL_PROJECT_TOKEN,
18
- time: Math.floor(Date.now() / 1000),
19
- distinct_id: props.user_id || 'anonymous',
20
- }
21
- };
22
- const encodedData = Buffer.from(JSON.stringify([eventObject])).toString('base64');
23
- console.log(`Analytics: tracking event '${event}'`);
24
- fetch(MIXPANEL_PROXY_DOMAIN + '/track/?data=' + encodedData, {
25
- method: 'GET',
26
- headers: {
27
- 'Accept': 'text/plain'
28
- }
29
- })
30
- .then(response => {
31
- if (response.ok) { // Status code in the range 200-299
32
- if (process.env.IS_DEV === 'true') {
33
- console.log(`Analytics: successfully sent event '${event}' with status ${response.status}`);
34
- }
35
- }
36
- else {
37
- console.error(`Error sending event '${event}' to Mixpanel:`, response.statusText);
38
- }
39
- })
40
- .catch(error => {
41
- console.error(`Error sending event '${event}' to Mixpanel:`, error);
42
- });
43
- }
44
- catch (error) {
45
- console.error(`Error preparing event '${event}' for tracking:`, error);
46
- }
47
- }
1
+ import{MIXPANEL_PROJECT_TOKEN,MIXPANEL_PROXY_DOMAIN}from"../constants.js";export function track(event,props={}){if("test"!==process.env.NODE_ENV&&"true"!==process.env.IS_TEST&&MIXPANEL_PROJECT_TOKEN&&MIXPANEL_PROXY_DOMAIN)try{const eventObject={event:event,properties:{...props,token:MIXPANEL_PROJECT_TOKEN,time:Math.floor(Date.now()/1e3),distinct_id:props.user_id||"anonymous"}},encodedData=Buffer.from(JSON.stringify([eventObject])).toString("base64");fetch(MIXPANEL_PROXY_DOMAIN+"/track/?data="+encodedData,{method:"GET",headers:{Accept:"text/plain"}}).then((response=>{response.ok&&process.env.IS_DEV})).catch((error=>{}))}catch(error){}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@locofy/mcp",
3
- "version": "1.0.11",
3
+ "version": "1.1.1",
4
4
  "description": "Locofy MCP Server with Cursor",
5
5
  "keywords": [
6
6
  "figma",
@@ -18,10 +18,11 @@
18
18
  "main": "./src/index.ts",
19
19
  "type": "module",
20
20
  "scripts": {
21
- "build": "rm -rf build && tsc && node -e \"const fs=require('fs');const f='build/index.js';fs.writeFileSync(f,'#!/usr/bin/env node\\n'+fs.readFileSync(f));fs.chmodSync(f,'755')\"",
21
+ "build": "rm -rf build && tsc && node -e \"const fs=require('fs');const f='build/index.js';fs.writeFileSync(f,'#!/usr/bin/env node\\n'+fs.readFileSync(f));fs.chmodSync(f,'755')\" && find build -name '*.js' -exec npx terser {} --compress drop_console=true --output {} \\;",
22
22
  "link": "npm run build && npm link",
23
23
  "start": "node build/index.js",
24
- "lint": "eslint . --ext .ts --fix"
24
+ "lint": "eslint . --ext .ts --fix --ignore-pattern \"**/__tests__/**\" --ignore-pattern \"**/*.test.ts\"",
25
+ "test": "NODE_ENV=test jest --coverage --coverageReporters=text-summary --detectOpenHandles --forceExit"
25
26
  },
26
27
  "bin": {
27
28
  "locofy-mcp": "./build/index.js"
@@ -36,12 +37,17 @@
36
37
  "zod": "^3.24.1"
37
38
  },
38
39
  "devDependencies": {
40
+ "@jest/globals": "^29.7.0",
39
41
  "@types/express": "^5.0.1",
42
+ "@types/jest": "^29.5.14",
40
43
  "@types/node": "^22.13.0",
41
44
  "@typescript-eslint/eslint-plugin": "^6.7.0",
42
45
  "@typescript-eslint/parser": "^6.7.0",
43
46
  "eslint": "^8.49.0",
44
47
  "eslint-config-prettier": "^9.0.0",
48
+ "terser": "^5.39.0",
49
+ "jest": "^29.7.0",
50
+ "ts-jest": "^29.3.0",
45
51
  "typescript": "^5.7.3"
46
52
  }
47
53
  }