UNPKG

@handit.ai/cli

Version:

AI-Powered Agent Instrumentation & Monitoring CLI Tool

1,209 lines (1,080 loc) 37.5 kB
const fs = require('fs-extra'); const path = require('path'); const { parse } = require('@babel/parser'); const traverse = require('@babel/traverse').default; /** * Node in the execution tree */ class ExecutionNode { constructor(name, file, line, type = 'function') { this.id = `${file}:${name}`; this.name = name; this.file = file; this.line = line; this.type = type; this.children = []; this.parent = null; this.metadata = { isAsync: false, isExported: false, parameters: [], returnType: null, }; } addChild(child) { child.parent = this; this.children.push(child); } toJSON() { return { id: this.id, name: this.name, file: this.file, line: this.line, type: this.type, children: this.children.map((child) => child.id), metadata: this.metadata, }; } } /** * Create entry node with correct line number * @param {string} entryFunction - Name of entry function * @param {string} entryFile - Path to entry file * @param {string} projectRoot - Project root directory * @returns {Promise<ExecutionNode>} - Entry node */ async function createEntryNode(entryFunction, entryFile, projectRoot) { try { const filePath = path.join(projectRoot, entryFile); const content = await fs.readFile(filePath, 'utf8'); const functionDef = await findFunctionDefinition( entryFunction, content, entryFile ); if (functionDef) { return new ExecutionNode( entryFunction, entryFile, functionDef.line, 'endpoint' ); } } catch (error) { console.warn( `Warning: Could not find entry function definition: ${error.message}` ); } // Fallback to line 1 if not found return new ExecutionNode(entryFunction, entryFile, 1, 'endpoint'); } /** * Build execution tree from entry function * @param {string} entryFile - Path to entry file * @param {string} entryFunction - Name of entry function * @param {string} projectRoot - Project root directory * @returns {Promise<Object>} - Execution tree */ async function buildExecutionTree(entryFile, entryFunction, projectRoot) { try { const visited = new Set(); const nodeMap = new Map(); // Start with entry function - find the actual line number const entryNode = await createEntryNode( entryFunction, entryFile, projectRoot ); nodeMap.set(entryNode.id, entryNode); // Build tree recursively await buildNodeTree(entryNode, projectRoot, visited, nodeMap); const result = { root: entryNode.id, nodes: Array.from(nodeMap.values()).map((node) => node.toJSON()), edges: buildEdges(nodeMap), }; return result; } catch (error) { console.error(`Error building execution tree: ${error.message}`); console.error(`Error details: ${error.stack}`); throw error; } } /** * Build tree for a specific node * @param {ExecutionNode} node - Current node * @param {string} projectRoot - Project root * @param {Set} visited - Visited nodes * @param {Map} nodeMap - Node map */ async function buildNodeTree(node, projectRoot, visited, nodeMap) { if (visited.has(node.id)) { return; // Prevent cycles } visited.add(node.id); try { const filePath = path.join(projectRoot, node.file); const content = await fs.readFile(filePath, 'utf8'); // Parse file based on extension const fileExt = path.extname(node.file); let functionCalls = []; if ( fileExt === '.js' || fileExt === '.ts' || fileExt === '.jsx' || fileExt === '.tsx' ) { functionCalls = await parseJavaScriptFile(content, node.name); // Add file information to each call functionCalls.forEach((call) => { call.file = node.file; }); // console.log(`Found ${functionCalls.length} function calls in ${node.name}:`, functionCalls.map(c => c.name)); } else if (fileExt === '.py') { functionCalls = await parsePythonFile(content, node.name); // Add file information to each call functionCalls.forEach((call) => { call.file = node.file; }); } // Process each function call for (const call of functionCalls) { const childNode = await resolveFunctionCall(call, projectRoot, nodeMap); if (childNode) { node.addChild(childNode); await buildNodeTree(childNode, projectRoot, visited, nodeMap); } } } catch (error) { console.warn(`Warning: Could not analyze ${node.file}: ${error.message}`); console.warn(`Error details: ${error.stack}`); } } /** * Parse JavaScript/TypeScript file to find function calls * @param {string} content - File content * @param {string} functionName - Function to analyze * @returns {Array} - Array of function calls */ async function parseJavaScriptFile(content, functionName) { const functionCalls = []; try { const ast = parse(content, { sourceType: 'module', plugins: ['jsx', 'typescript'], allowImportExportEverywhere: true, allowReturnOutsideFunction: true, allowAwaitOutsideFunction: true, errorRecovery: true, }); let targetFunction = null; // Check if this is an Express route (contains hyphens or slashes) // Try Express route detection first traverse(ast, { CallExpression(path) { if ( path.node.callee.type === 'MemberExpression' && path.node.callee.object.name === 'app' && (path.node.callee.property.name === 'post' || path.node.callee.property.name === 'get' || path.node.callee.property.name === 'put' || path.node.callee.property.name === 'delete') ) { // Check if this is the route we're looking for const args = path.node.arguments; if (args.length >= 2) { const routePath = args[0]; if ( routePath.type === 'StringLiteral' && (routePath.value === `/${functionName}` || routePath.value === functionName) ) { // Found the route, the handler is the last argument const handler = args[args.length - 1]; if ( handler.type === 'FunctionExpression' || handler.type === 'ArrowFunctionExpression' ) { // For Express routes, we need to find the path to the handler traverse(ast, { FunctionExpression(handlerPath) { if (handlerPath.node === handler) { targetFunction = handlerPath; } }, ArrowFunctionExpression(handlerPath) { if (handlerPath.node === handler) { targetFunction = handlerPath; } }, }); // console.log(`Found Express route handler for ${functionName} at line ${path.node.loc?.start.line}`); } } } } }, }); // If Express route not found, try regular function detection if (!targetFunction) { traverse(ast, { FunctionDeclaration(path) { if (path.node.id && path.node.id.name === functionName) { targetFunction = path; } }, VariableDeclarator(path) { if ( path.node.id && path.node.id.name === functionName && path.node.init && path.node.init.type === 'FunctionExpression' ) { targetFunction = path.node.init; } }, FunctionExpression(path) { // Handle arrow functions and other function expressions if ( path.parent && path.parent.id && path.parent.id.name === functionName ) { targetFunction = path; } }, }); } if (!targetFunction) { console.warn(`Function/Route "${functionName}" not found in file`); return functionCalls; } // console.log(`Target function found, searching for calls within it...`); // Find function calls within the target function // We need to traverse the entire AST and check if we're within the target function traverse(ast, { CallExpression(path) { // Check if this call is within our target function let currentPath = path; let inTargetFunction = false; while (currentPath.parentPath) { if (currentPath.node === targetFunction.node) { inTargetFunction = true; break; } currentPath = currentPath.parentPath; } if (inTargetFunction) { const callee = path.node.callee; if (callee.type === 'Identifier') { functionCalls.push({ name: callee.name, line: path.node.loc?.start.line || 0, type: 'function', }); } else if (callee.type === 'MemberExpression') { // Handle method calls like obj.method() if (callee.property && callee.property.type === 'Identifier') { functionCalls.push({ name: callee.property.name, line: path.node.loc?.start.line || 0, type: 'method', object: callee.object.name, }); } } } }, }); } catch (error) { console.warn(`Warning: Could not parse JavaScript file: ${error.message}`); console.warn(`Error details: ${error.stack}`); } return functionCalls; } /** * Parse Python file to find function calls using AST * @param {string} content - File content * @param {string} functionName - Function to analyze * @returns {Array} - Array of function calls */ async function parsePythonFile(content, functionName) { // Extract just the function name from method calls like "processor.process_document" const targetFunctionName = functionName.includes('.') ? functionName.split('.')[1] : functionName; const functionCalls = []; try { // Use Python's built-in ast module to parse the code const { spawn } = require('child_process'); // Create a Python script to parse the AST const pythonScript = ` import ast import json import sys def find_function_calls_and_definitions(code, target_function): try: tree = ast.parse(code) calls = [] definitions = [] class FunctionVisitor(ast.NodeVisitor): def __init__(self): self.calls = [] self.definitions = [] self.in_target_function = False self.current_function = None def visit_FunctionDef(self, node): # Check if this is our target function if node.name == target_function: self.in_target_function = True self.current_function = node.name # Visit all nodes in this function self.generic_visit(node) # If we were in the target function, we're done if self.current_function == target_function: self.in_target_function = False self.current_function = None def visit_AsyncFunctionDef(self, node): # Same as FunctionDef but for async functions if node.name == target_function: self.in_target_function = True self.current_function = node.name self.generic_visit(node) if self.current_function == target_function: self.in_target_function = False self.current_function = None def visit_Call(self, node): if self.in_target_function: call_info = {} if isinstance(node.func, ast.Name): # Simple function call: function_name() call_info = { 'name': node.func.id, 'line': node.lineno, 'type': 'function' } elif isinstance(node.func, ast.Attribute): # Method call: object.method() call_info = { 'name': f"{self._get_attribute_name(node.func)}.{node.func.attr}", 'line': node.lineno, 'type': 'method' } # Skip built-in functions and common keywords skip_list = [ 'print', 'len', 'str', 'int', 'float', 'list', 'dict', 'set', 'if', 'for', 'while', 'try', 'except', 'finally', 'with', 'as', 'import', 'from', 'return', 'yield', 'await', 'raise', 'HTTPException', 'logger', 'File', 'Form', 'image', 'email', 'startswith', 'file', 'read', 'info', 'error' ] skip_objects = ['file', 'result', 'logger'] skip_methods = ['read', 'startswith', 'split', 'strip', 'join', 'append', 'extend', 'pop', 'get', 'set', 'keys', 'values', 'items', 'update', 'copy', 'clear'] if call_info.get('name'): if 'type' in call_info and call_info['type'] == 'function': if call_info['name'] not in skip_list: self.calls.append(call_info) elif 'type' in call_info and call_info['type'] == 'method': parts = call_info['name'].split('.') if len(parts) == 2: obj_name, method_name = parts if obj_name not in skip_objects and method_name not in skip_methods: self.calls.append(call_info) self.generic_visit(node) def visit_Assign(self, node): # Find variable assignments like "processor = DocumentProcessor()" for target in node.targets: if isinstance(target, ast.Name): if isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name): assignment_info = { 'variable': target.id, 'class': node.value.func.id, 'line': node.lineno, 'type': 'assignment' } self.definitions.append(assignment_info) self.generic_visit(node) def visit_ImportFrom(self, node): # Find imports like "from document_processor import DocumentProcessor" for alias in node.names: import_info = { 'module': node.module, 'name': alias.name, 'asname': alias.asname, 'line': node.lineno, 'type': 'import' } self.definitions.append(import_info) self.generic_visit(node) def _get_attribute_name(self, node): """Get the full attribute name (e.g., 'processor.process_document')""" if isinstance(node.value, ast.Name): return node.value.id elif isinstance(node.value, ast.Attribute): return f"{self._get_attribute_name(node.value)}.{node.value.attr}" else: return "unknown" visitor = FunctionVisitor() visitor.visit(tree) return { 'calls': visitor.calls, 'definitions': visitor.definitions } except Exception as e: return {'calls': [], 'definitions': []} # Read input from stdin code = sys.stdin.read() target_function = sys.argv[1] if len(sys.argv) > 1 else '' result = find_function_calls_and_definitions(code, target_function) print(json.dumps(result)) `; return new Promise((resolve, reject) => { const pythonProcess = spawn('python3', [ '-c', pythonScript, targetFunctionName, ]); let stdout = ''; let stderr = ''; pythonProcess.stdout.on('data', (data) => { stdout += data.toString(); }); pythonProcess.stderr.on('data', (data) => { stderr += data.toString(); }); pythonProcess.on('close', (code) => { if (code === 0) { try { const result = JSON.parse(stdout); // Use the definitions to resolve function calls const resolvedCalls = []; for (const call of result.calls) { if (call.type === 'method') { // For method calls, try to resolve using definitions const parts = call.name.split('.'); const objectName = parts[0]; const methodName = parts[1]; // Find the object definition const objectDef = result.definitions.find( (def) => def.type === 'assignment' && def.variable === objectName ); if (objectDef) { // Find the class import const classImport = result.definitions.find( (def) => def.type === 'import' && def.name === objectDef.class ); if (classImport) { // Add the module information to the call call.module = classImport.module; call.className = objectDef.class; } } } resolvedCalls.push(call); } resolve(resolvedCalls); } catch (error) { console.warn('Error parsing Python AST output:', error); resolve([]); } } else { console.warn('Python AST parsing failed:', stderr); resolve([]); } }); pythonProcess.stdin.write(content); pythonProcess.stdin.end(); }); } catch (error) { console.warn(`Warning: Could not parse Python file: ${error.message}`); return []; } } /** * Resolve a function call to find its definition * @param {Object} call - Function call info * @param {string} projectRoot - Project root * @param {Map} nodeMap - Node map * @returns {Promise<ExecutionNode|null>} - Resolved node or null */ async function resolveFunctionCall(call, projectRoot, nodeMap) { // Handle method calls (e.g., "processor.process_document") let searchName = call.name; let isMethodCall = false; let objectName = null; if (call.name.includes('.')) { const parts = call.name.split('.'); objectName = parts[0]; searchName = parts[1]; // Use the method name for searching isMethodCall = true; } // First, try to find in the same file const sameFileCalls = Array.from(nodeMap.values()).filter( (node) => node.name === call.name && node.file === call.file ); if (sameFileCalls.length > 0) { return sameFileCalls[0]; } // Search in the same file first (for functions defined in the same file) const currentFile = call.file || 'server.py'; try { const filePath = path.join(projectRoot, currentFile); const content = await fs.readFile(filePath, 'utf8'); // For method calls, try to find the object definition first if (isMethodCall && objectName) { // If the call has module information from AST, use it directly if (call.module) { const importedFile = await findImportedFile(call.module, projectRoot); if (importedFile) { const importedContent = await fs.readFile( path.join(projectRoot, importedFile), 'utf8' ); const functionDef = await findFunctionDefinition( searchName, importedContent, importedFile ); if (functionDef) { const nodeId = `${importedFile}:${call.name}`; if (nodeMap.has(nodeId)) { return nodeMap.get(nodeId); } const newNode = new ExecutionNode( call.name, importedFile, functionDef.line, call.type ); newNode.metadata = functionDef.metadata; nodeMap.set(nodeId, newNode); return newNode; } } } // Fallback to the old method if no module info from AST const objectDef = await findObjectDefinition( objectName, content, currentFile ); if (objectDef) { // If it's an import, search in the imported file if (objectDef.type === 'import' && objectDef.module) { const importedFile = await findImportedFile( objectDef.module, projectRoot ); if (importedFile) { const importedContent = await fs.readFile( path.join(projectRoot, importedFile), 'utf8' ); const functionDef = await findFunctionDefinition( searchName, importedContent, importedFile ); if (functionDef) { const nodeId = `${importedFile}:${call.name}`; if (nodeMap.has(nodeId)) { return nodeMap.get(nodeId); } const newNode = new ExecutionNode( call.name, importedFile, functionDef.line, call.type ); newNode.metadata = functionDef.metadata; nodeMap.set(nodeId, newNode); return newNode; } } } // If it's an assignment, search for the class definition else if (objectDef.type === 'assignment' && objectDef.className) { // First, try to find the class import const classImport = await findObjectDefinition( objectDef.className, content, currentFile ); if ( classImport && classImport.type === 'import' && classImport.module ) { const importedFile = await findImportedFile( classImport.module, projectRoot ); if (importedFile) { const importedContent = await fs.readFile( path.join(projectRoot, importedFile), 'utf8' ); const functionDef = await findFunctionDefinition( searchName, importedContent, importedFile ); if (functionDef) { const nodeId = `${importedFile}:${call.name}`; if (nodeMap.has(nodeId)) { return nodeMap.get(nodeId); } const newNode = new ExecutionNode( call.name, importedFile, functionDef.line, call.type ); newNode.metadata = functionDef.metadata; return newNode; } } } } } else { // No object definition found, continue with other search methods } } // For method calls, search for the method name in the file const functionDef = await findFunctionDefinition( searchName, content, currentFile ); if (functionDef) { const nodeId = `${currentFile}:${call.name}`; if (nodeMap.has(nodeId)) { return nodeMap.get(nodeId); } const newNode = new ExecutionNode( call.name, currentFile, functionDef.line, call.type ); newNode.metadata = functionDef.metadata; nodeMap.set(nodeId, newNode); return newNode; } } catch (error) { console.warn(`Error searching in same file: ${error.message}`); // Continue searching other files } // Search in other files const allFiles = await getAllFiles(projectRoot); for (const file of allFiles) { if (file === currentFile) continue; // Already checked try { const filePath = path.join(projectRoot, file); const content = await fs.readFile(filePath, 'utf8'); const functionDef = await findFunctionDefinition( searchName, content, file ); if (functionDef) { const nodeId = `${file}:${call.name}`; if (nodeMap.has(nodeId)) { return nodeMap.get(nodeId); } const newNode = new ExecutionNode( call.name, file, functionDef.line, call.type ); newNode.metadata = functionDef.metadata; nodeMap.set(nodeId, newNode); return newNode; } } catch (error) { // Continue searching other files } } return null; } /** * Find function definition in file content * @param {string} functionName - Function name to find * @param {string} content - File content * @param {string} filePath - File path * @returns {Promise<Object|null>} - Function definition or null */ async function findFunctionDefinition(functionName, content, filePath) { const fileExt = path.extname(filePath); if ( fileExt === '.js' || fileExt === '.ts' || fileExt === '.jsx' || fileExt === '.tsx' ) { return findJavaScriptFunction(functionName, content); } else if (fileExt === '.py') { return findPythonFunction(functionName, content); } return null; } /** * Find object definition (import, class, etc.) * @param {string} objectName - Object name to find * @param {string} content - File content * @param {string} filePath - File path * @returns {Promise<Object|null>} - Object definition or null */ async function findObjectDefinition(objectName, content, filePath) { const fileExt = path.extname(filePath); if (fileExt === '.py') { return findPythonObject(objectName, content); } return null; } /** * Find Python object definition * @param {string} objectName - Object name * @param {string} content - File content * @returns {Object|null} - Object definition */ function findPythonObject(objectName, content) { const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Check for imports const importMatch = line.match(/^from\s+(\S+)\s+import\s+(\w+)/); if (importMatch) { const module = importMatch[1]; const importedName = importMatch[2]; if (importedName === objectName) { return { type: 'import', module: module, line: i + 1, }; } } // Check for direct imports const directImportMatch = line.match(/^import\s+(\w+)/); if (directImportMatch) { const module = directImportMatch[1]; if (module === objectName) { return { type: 'import', module: module, line: i + 1, }; } } // Check for class definitions const classMatch = line.match(/^class\s+(\w+)/); if (classMatch && classMatch[1] === objectName) { return { type: 'class', line: i + 1, }; } // Check for variable assignments like "processor = DocumentProcessor()" const assignmentMatch = line.match(/^(\w+)\s*=\s*(\w+)\s*\(/); if (assignmentMatch) { const varName = assignmentMatch[1]; const className = assignmentMatch[2]; if (varName === objectName) { return { type: 'assignment', className: className, line: i + 1, }; } } } return null; } /** * Find imported file based on module name * @param {string} moduleName - Module name * @param {string} projectRoot - Project root * @returns {Promise<string|null>} - File path or null */ async function findImportedFile(moduleName, projectRoot) { // Common Python file extensions const extensions = ['.py', '.pyx', '.pyi']; for (const ext of extensions) { const possibleFiles = [ `${moduleName}${ext}`, `${moduleName}/__init__${ext}`, `${moduleName.replace(/\./g, '/')}${ext}`, `${moduleName.replace(/\./g, '/')}/__init__${ext}`, ]; for (const file of possibleFiles) { const filePath = path.join(projectRoot, file); try { await fs.access(filePath); return file; } catch (error) { // File doesn't exist, try next } } } return null; } /** * Find JavaScript function definition * @param {string} functionName - Function name * @param {string} content - File content * @returns {Object|null} - Function definition */ function findJavaScriptFunction(functionName, content) { try { const ast = parse(content, { sourceType: 'module', plugins: ['jsx', 'typescript'], allowImportExportEverywhere: true, allowReturnOutsideFunction: true, allowAwaitOutsideFunction: true, errorRecovery: true, }); let result = null; // Check if this is an Express route (contains hyphens or slashes) const isExpressRoute = functionName.includes('-') || functionName.includes('/'); try { // Find Express route handler traverse(ast, { CallExpression(path) { if ( path.node.callee.type === 'MemberExpression' && path.node.callee.object.name === 'app' && (path.node.callee.property.name === 'post' || path.node.callee.property.name === 'get' || path.node.callee.property.name === 'put' || path.node.callee.property.name === 'delete') ) { // Check if this is the route we're looking for const args = path.node.arguments; if (args.length >= 2) { const routePath = args[0]; if ( routePath.type === 'StringLiteral' && (routePath.value === `/${functionName}` || routePath.value === functionName || '/' + functionName === routePath.value) ) { // Found the route, the handler is the last argument const handler = args[args.length - 1]; if ( handler.type === 'FunctionExpression' || handler.type === 'ArrowFunctionExpression' ) { result = { line: path.node.loc?.start.line || 0, metadata: { isAsync: handler.async, isExported: false, parameters: handler.params.map((param) => param.name), returnType: null, }, }; } } } } }, }); } catch (error) { // Ignore parsing errors for arrow functions } if (!result) { try { // Find regular function traverse(ast, { FunctionDeclaration(path) { if (path.node.id && path.node.id.name === functionName) { result = { line: path.node.loc?.start.line || 0, metadata: { isAsync: path.node.async, isExported: path.parent.type === 'ExportNamedDeclaration' || path.parent.type === 'ExportDefaultDeclaration', parameters: path.node.params.map((param) => param.name), returnType: null, }, }; } }, VariableDeclarator(path) { if ( path.node.id && path.node.id.name === functionName && path.node.init && path.node.init.type === 'FunctionExpression' ) { result = { line: path.node.loc?.start.line || 0, metadata: { isAsync: path.node.init.async, isExported: path.parent.parent.type === 'ExportNamedDeclaration', parameters: path.node.init.params.map((param) => param.name), returnType: null, }, }; } }, }); } catch (error) { // Ignore parsing errors for regular functions } } return result; } catch (error) { console.warn( `Warning: Could not find JavaScript function "${functionName}": ${error.message}` ); return null; } } /** * Find Python function definition * @param {string} functionName - Function name * @param {string} content - File content * @returns {Object|null} - Function definition */ function findPythonFunction(functionName, content) { const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Handle FastAPI decorators - look for @app.post, @app.get, etc. // Support both @app.post("/path") and @app.post("/path", response_model=...) const decoratorMatch = line.match( /^@(\w+)\.(post|get|put|delete|patch)\s*\(/ ); if (decoratorMatch) { // Check if the next line contains the function definition if (i + 1 < lines.length) { const nextLine = lines[i + 1]; const defMatch = nextLine.match(/^(?:async\s+)?def\s+(\w+)/); if (defMatch && defMatch[1] === functionName) { // Extract parameters from the function definition const paramMatch = nextLine.match(/\(([^)]*)\)/); const parameters = paramMatch ? paramMatch[1] .split(',') .map((p) => p.trim()) .filter((p) => p) : []; return { line: i + 2, // Function definition is on the next line metadata: { isAsync: nextLine.includes('async def'), isExported: true, // FastAPI endpoints are exported parameters: parameters, returnType: null, isEndpoint: true, }, }; } } } // Handle class method definitions (for method calls like processor.process_document) const classMethodMatch = line.match(/^\s*def\s+(\w+)\s*\(/); if (classMethodMatch && classMethodMatch[1] === functionName) { // Extract parameters const paramMatch = line.match(/\(([^)]*)\)/); const parameters = paramMatch ? paramMatch[1] .split(',') .map((p) => p.trim()) .filter((p) => p) : []; return { line: i + 1, metadata: { isAsync: line.includes('async def'), isExported: false, parameters: parameters, returnType: null, isMethod: true, }, }; } // Handle regular function definitions const defMatch = line.match(/^(?:async\s+)?def\s+(\w+)/); if (defMatch && defMatch[1] === functionName) { // Extract parameters const paramMatch = line.match(/\(([^)]*)\)/); const parameters = paramMatch ? paramMatch[1] .split(',') .map((p) => p.trim()) .filter((p) => p) : []; return { line: i + 1, metadata: { isAsync: line.includes('async def'), isExported: false, // Python doesn't have explicit exports like JS parameters: parameters, returnType: null, }, }; } } return null; } /** * Get all files in project * @param {string} projectRoot - Project root * @returns {Promise<Array>} - Array of file paths */ async function getAllFiles(projectRoot) { const { glob } = require('glob'); const patterns = ['**/*.js', '**/*.ts', '**/*.py', '**/*.jsx', '**/*.tsx']; const files = []; for (const pattern of patterns) { const matches = await glob(pattern, { cwd: projectRoot, ignore: [ 'node_modules/**', '.git/**', 'dist/**', 'build/**', 'coverage/**', '__pycache__/**', '*.pyc', '.env*', 'package-lock.json', 'yarn.lock', ], }); files.push(...matches); } return files.sort(); } /** * Build edges from node map * @param {Map} nodeMap - Node map * @returns {Array} - Array of edges */ function buildEdges(nodeMap) { const edges = []; for (const node of nodeMap.values()) { for (const child of node.children) { edges.push({ from: node.id, to: child.id, }); } } return edges; } module.exports = { buildExecutionTree, ExecutionNode, };