@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,507 +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
- * PullComponents tool
9
- * - Returns a hardcoded representation of a directory structure
10
- * - Contains nested folders, files, and file contents
11
- */
12
- export const pullComponentsToolName = 'getLatestComponentAndDependencyCode';
13
- 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
14
- `;
15
- export const PullComponentsToolSchema = z.object({
16
- componentNames: z.array(z.string()).describe('The list of component or screen names to be retrieved.').max(10).min(1),
17
- workspacePath: z.string().describe('The full path to the workspace.'),
18
- });
19
- export async function runPullComponentsTool(args) {
20
- let componentNames = args.componentNames;
21
- // clean the component names, remove extensions
22
- componentNames = componentNames.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(componentNames, 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: pullComponentsToolName,
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(componentNames, projectID, personalAccessToken) {
87
- const encodedNames = componentNames.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
- // Process main component file
339
- if (node.filePath) {
340
- const nodeContent = node.name.includes('.css') ?
341
- node.exportData.cssContent :
342
- node.exportData.content;
343
- const fileChanged = checkFile(node.filePath, nodeContent);
344
- node.codeChanged = fileChanged;
345
- hasChanges = hasChanges || fileChanged;
346
- updateNodeStatus(node, node.name, fileChanged);
347
- // Check associated CSS module
348
- if (node.exportData.cssFileName && node.exportData.cssContent) {
349
- const cssChanged = checkCssFile(node.filePath, node.exportData.cssFileName, node.exportData.cssContent);
350
- hasChanges = hasChanges || cssChanged;
351
- updateNodeStatus(node.exportData, node.exportData.cssFileName, cssChanged, true);
352
- }
353
- }
354
- // Process dependencies
355
- if (node.exportData.dependencies && Array.isArray(node.exportData.dependencies)) {
356
- for (const dependency of node.exportData.dependencies) {
357
- // Process dependency file
358
- if (dependency.filePath) {
359
- const isCSS = dependency.name && dependency.name.includes('.css');
360
- const depContent = isCSS ? dependency.cssContent : dependency.content;
361
- const fileChanged = checkFile(dependency.filePath, depContent);
362
- dependency.codeChanged = fileChanged;
363
- hasChanges = hasChanges || fileChanged;
364
- updateNodeStatus(dependency, dependency.compName, fileChanged);
365
- // Check associated CSS module
366
- if (dependency.cssFileName && dependency.cssContent) {
367
- const cssChanged = checkCssFile(dependency.filePath, dependency.cssFileName, dependency.cssContent);
368
- hasChanges = hasChanges || cssChanged;
369
- updateNodeStatus(dependency, dependency.cssFileName, cssChanged, true);
370
- }
371
- }
372
- // Process nested dependencies recursively
373
- if (dependency.dependencies && Array.isArray(dependency.dependencies)) {
374
- for (const nestedDep of dependency.dependencies) {
375
- const nestedChanged = processNode(nestedDep);
376
- hasChanges = hasChanges || nestedChanged;
377
- }
378
- }
379
- }
380
- }
381
- }
382
- // Process children recursively
383
- if (node.children && Array.isArray(node.children)) {
384
- for (const child of node.children) {
385
- const childChanged = processNode(child);
386
- hasChanges = hasChanges || childChanged;
387
- }
388
- }
389
- return hasChanges;
390
- }
391
- // Start processing from the root
392
- return processNode(directoryStructure);
393
- }
394
- /**
395
- * Compares two code snippets to determine if they are functionally different
396
- * @param fileContent The code currently in the file
397
- * @param apiCode The code provided by the API
398
- * @returns True if the code has changed, false otherwise
399
- */
400
- function hasCodeChanged(fileContent, apiCode) {
401
- if (!fileContent || !apiCode) {
402
- return true;
403
- }
404
- // Normalize both code snippets
405
- const normalizedFile = normalizeForComparison(fileContent);
406
- const normalizedApi = normalizeForComparison(apiCode);
407
- // Simple equality check on normalized versions
408
- return normalizedFile !== normalizedApi;
409
- }
410
- /**
411
- * Normalizes code for comparison by removing irrelevant differences
412
- * @param code The code to normalize
413
- * @returns Normalized code string
414
- */
415
- function normalizeForComparison(code) {
416
- return code
417
- .replace(/\s+/g, '') // Remove all whitespace (spaces, tabs, newlines)
418
- .replace(/[;,'"]/g, '') // Remove semicolons, commas, and quotes
419
- .replace(/\/\/.*$/gm, '') // Remove single-line comments
420
- .replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
421
- .toLowerCase(); // Convert to lowercase for case-insensitive comparison
422
- }
423
- /**
424
- * Cleans the directory structure by removing unwanted properties from all nodes
425
- * @param directoryStructure The directory structure to clean
426
- * @returns The cleaned directory structure
427
- */
428
- function processDirectoryStructure(directoryStructure) {
429
- const propertiesToRemove = [
430
- 'files',
431
- 'component',
432
- 'jsLC',
433
- 'assetCount',
434
- 'cssLC',
435
- 'componentCount',
436
- 'pageMetaData',
437
- 'isHomePage',
438
- 'storybookFileName',
439
- 'isAutoSyncNode'
440
- ];
441
- const componentsToUpdate = [];
442
- const componentsUnchanged = [];
443
- function collectChangeStatus(node) {
444
- // Check if this is a component node with a name and change status
445
- if (node.name && node.codeChanged !== undefined) {
446
- if (node.codeChanged) {
447
- componentsToUpdate.push(node.name);
448
- }
449
- else {
450
- componentsUnchanged.push(node.name);
451
- }
452
- }
453
- // Check dependencies
454
- if (node.exportData && node.exportData.dependencies) {
455
- node.exportData.dependencies.forEach((dep) => {
456
- if (dep.compName && dep.codeChanged !== undefined) {
457
- if (dep.codeChanged) {
458
- componentsToUpdate.push(dep.compName);
459
- }
460
- else {
461
- componentsUnchanged.push(dep.compName);
462
- }
463
- }
464
- // Check nested dependencies
465
- if (dep.dependencies && Array.isArray(dep.dependencies)) {
466
- dep.dependencies.forEach((nestedDep) => collectChangeStatus(nestedDep));
467
- }
468
- });
469
- }
470
- // Process children recursively
471
- if (node.children && Array.isArray(node.children)) {
472
- node.children.forEach((child) => collectChangeStatus(child));
473
- }
474
- }
475
- function cleanNode(node) {
476
- // Remove unwanted properties from the current node
477
- propertiesToRemove.forEach(prop => {
478
- if (node[prop] !== undefined)
479
- delete node[prop];
480
- });
481
- // Clean exportData if it exists
482
- if (node.exportData) {
483
- propertiesToRemove.forEach(prop => {
484
- if (node.exportData[prop] !== undefined)
485
- delete node.exportData[prop];
486
- });
487
- // Process dependencies in exportData
488
- if (node.exportData.dependencies && Array.isArray(node.exportData.dependencies)) {
489
- node.exportData.dependencies.forEach((dep) => cleanNode(dep));
490
- }
491
- }
492
- // Process children recursively
493
- if (node.children && Array.isArray(node.children)) {
494
- node.children.forEach((child) => cleanNode(child));
495
- }
496
- }
497
- // First collect all change statuses
498
- collectChangeStatus(directoryStructure);
499
- // Then clean the structure
500
- cleanNode(directoryStructure);
501
- // Add the component lists to the component data
502
- return {
503
- directoryStructure: directoryStructure.children,
504
- componentsToUpdate: [...new Set(componentsToUpdate)],
505
- componentsUnchanged: [...new Set(componentsUnchanged)]
506
- };
507
- }
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 pullComponentsToolName="getLatestComponentAndDependencyCode";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\n ";export const PullComponentsToolSchema=z.object({componentNames:z.array(z.string()).describe("The list of component or screen names to be retrieved.").max(10).min(1),workspacePath:z.string().describe("The full path to the workspace.")});export async function runPullComponentsTool(args){let componentNames=args.componentNames;componentNames=componentNames.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(componentNames,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:pullComponentsToolName,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(componentNames,projectID,personalAccessToken){const encodedNames=componentNames.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"],componentsToUpdate=[],componentsUnchanged=[];return function collectChangeStatus(node){node.name&&void 0!==node.codeChanged&&(node.codeChanged?componentsToUpdate.push(node.name):componentsUnchanged.push(node.name)),node.exportData&&node.exportData.dependencies&&node.exportData.dependencies.forEach((dep=>{dep.compName&&void 0!==dep.codeChanged&&(dep.codeChanged?componentsToUpdate.push(dep.compName):componentsUnchanged.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,componentsToUpdate:[...new Set(componentsToUpdate)],componentsUnchanged:[...new Set(componentsUnchanged)]}}