UNPKG

mcp-zephyr

Version:

MCP server for analyzing Zephyr/West workspace directories and verifying Kconfig options

974 lines (827 loc) 42.5 kB
const fs = require('fs').promises; const path = require('path'); const yaml = require('yaml'); class WestWorkspaceAnalyzer { constructor(workspacePath, options = {}) { this.workspacePath = workspacePath; // Allow custom west path, default to workspacePath/.west this.westPath = options.westPath || path.join(workspacePath, '.west'); this.analysis = { isValid: false, westConfig: null, manifest: null, zephyrVersion: null, sdkVersion: null, modules: [], boards: [], kconfig: { files: [], menustructure: [] }, cmake: { modules: [], toolchains: [] }, projects: [] }; } async analyzeWorkspace() { try { // Check if it's a valid West workspace const westConfigPath = path.join(this.westPath, 'config'); if (!await this.fileExists(westConfigPath)) { return { error: "Not a valid West workspace (missing .west/config)" }; } this.analysis.isValid = true; // Parse West configuration await this.parseWestConfig(); // Parse West manifest await this.parseManifest(); // Get Zephyr version if present await this.getZephyrVersion(); // Get SDK versions await this.getSDKVersions(); // Discover modules await this.discoverModules(); // Find Kconfig files await this.findKconfigFiles(); // Find CMake configurations await this.findCMakeConfigs(); // Find boards await this.findBoards(); return this.analysis; } catch (error) { return { error: error.message, analysis: this.analysis }; } } async fileExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } async readFile(filePath) { try { return await fs.readFile(filePath, 'utf8'); } catch { return null; } } async parseWestConfig() { const configPath = path.join(this.westPath, 'config'); const content = await this.readFile(configPath); if (!content) return; const config = {}; const lines = content.split('\n'); let currentSection = null; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; if (trimmed.startsWith('[') && trimmed.endsWith(']')) { currentSection = trimmed.slice(1, -1); config[currentSection] = {}; } else if (currentSection && trimmed.includes('=')) { const [key, value] = trimmed.split('=').map(s => s.trim()); config[currentSection][key] = value; } } this.analysis.westConfig = config; } async parseManifest() { if (!this.analysis.westConfig?.manifest) return; const manifestPath = path.join( this.workspacePath, this.analysis.westConfig.manifest.path || '', this.analysis.westConfig.manifest.file || 'west.yml' ); const content = await this.readFile(manifestPath); if (!content) return; try { const manifest = yaml.parse(content); this.analysis.manifest = manifest; // Extract projects from manifest if (manifest?.manifest?.projects) { this.analysis.projects = manifest.manifest.projects.map(project => ({ name: project.name, path: project.path || project.name, revision: project.revision, remote: project.remote || manifest.manifest.defaults?.remote, repoPath: project['repo-path'], modules: project.import ? 'has-imports' : null })); } } catch (error) { console.error('Failed to parse manifest:', error); } } async getZephyrVersion() { const zephyrPath = path.join(this.workspacePath, this.analysis.westConfig?.zephyr?.base || 'zephyr'); const versionFile = path.join(zephyrPath, 'VERSION'); const content = await this.readFile(versionFile); if (!content) return; const version = {}; const lines = content.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (trimmed.includes('=')) { const [key, value] = trimmed.split('=').map(s => s.trim()); version[key] = value; } } if (version.VERSION_MAJOR) { this.analysis.zephyrVersion = { major: version.VERSION_MAJOR, minor: version.VERSION_MINOR, patch: version.PATCHLEVEL, tweak: version.VERSION_TWEAK, extra: version.EXTRAVERSION, full: `${version.VERSION_MAJOR}.${version.VERSION_MINOR}.${version.PATCHLEVEL}${version.EXTRAVERSION || ''}` }; } } async getSDKVersions() { // Check for Nordic SDK const nrfPath = path.join(this.workspacePath, 'nrf'); const nrfVersion = await this.readFile(path.join(nrfPath, 'VERSION')); if (nrfVersion) { this.analysis.sdkVersion = { ncs: nrfVersion.trim(), type: 'Nordic Connect SDK' }; } // Check for other SDK versions const sdkVersionFile = path.join(this.workspacePath, 'modules/hal/cirrus-logic/sdk_version.h'); if (await this.fileExists(sdkVersionFile)) { const content = await this.readFile(sdkVersionFile); if (content && this.analysis.sdkVersion) { this.analysis.sdkVersion.additional = 'Cirrus Logic SDK detected'; } } } async discoverModules() { const modulesPath = path.join(this.workspacePath, 'modules'); try { if (await this.fileExists(modulesPath)) { const categories = await fs.readdir(modulesPath); for (const category of categories) { const categoryPath = path.join(modulesPath, category); const stat = await fs.stat(categoryPath); if (stat.isDirectory()) { const modules = await fs.readdir(categoryPath); for (const module of modules) { const modulePath = path.join(categoryPath, module); const modStat = await fs.stat(modulePath); if (modStat.isDirectory()) { // Check for module metadata const moduleInfo = { name: module, category: category, path: path.relative(this.workspacePath, modulePath) }; // Check for CMakeLists.txt if (await this.fileExists(path.join(modulePath, 'CMakeLists.txt'))) { moduleInfo.hasCMake = true; } // Check for Kconfig if (await this.fileExists(path.join(modulePath, 'Kconfig'))) { moduleInfo.hasKconfig = true; } this.analysis.modules.push(moduleInfo); } } } } } } catch (error) { console.error('Error discovering modules:', error); } // Also check for modules from projects list for (const project of this.analysis.projects) { const projectPath = path.join(this.workspacePath, project.path); if (await this.fileExists(projectPath)) { const existingModule = this.analysis.modules.find(m => m.name === project.name); if (!existingModule) { this.analysis.modules.push({ name: project.name, path: project.path, fromManifest: true, revision: project.revision }); } } } } async findKconfigFiles() { try { // Recursively find all Kconfig files in workspace const zephyrPath = path.join(this.workspacePath, this.analysis.westConfig?.zephyr?.base || 'zephyr'); if (await this.fileExists(zephyrPath)) { await this.findKconfigFilesRecursive(zephyrPath); } // Also search Nordic (NRF) directory const nrfPath = path.join(this.workspacePath, 'nrf'); if (await this.fileExists(nrfPath)) { await this.findKconfigFilesRecursive(nrfPath); } // Search other important directories const otherDirs = ['modules', 'bootloader', 'mbedtls', 'trusted-firmware-m']; for (const dir of otherDirs) { const dirPath = path.join(this.workspacePath, dir); if (await this.fileExists(dirPath)) { await this.findKconfigFilesRecursive(dirPath); } } } catch (error) { console.error('Error finding Kconfig files:', error); } } async findKconfigFilesRecursive(dirPath, maxDepth = 4, currentDepth = 0) { if (currentDepth >= maxDepth) return; try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { // Skip some common directories that don't have relevant Kconfigs if (!entry.name.startsWith('.') && !['build', 'doc', 'scripts', 'tools', 'west', 'cmake'].includes(entry.name)) { await this.findKconfigFilesRecursive(fullPath, maxDepth, currentDepth + 1); } } else if (entry.isFile()) { // Check if it's a Kconfig file if (entry.name === 'Kconfig' || entry.name.startsWith('Kconfig.') || entry.name.endsWith('.kconfig')) { this.analysis.kconfig.files.push(fullPath); } } } } catch (error) { // Ignore permission errors and continue if (error.code !== 'EACCES' && error.code !== 'ENOENT') { console.error(`Error reading directory ${dirPath}:`, error); } } } async findCMakeConfigs() { const zephyrPath = path.join(this.workspacePath, this.analysis.westConfig?.zephyr?.base || 'zephyr'); const cmakePath = path.join(zephyrPath, 'cmake'); try { // Find CMake modules const modulesPath = path.join(cmakePath, 'modules'); if (await this.fileExists(modulesPath)) { const files = await fs.readdir(modulesPath); this.analysis.cmake.modules = files .filter(f => f.endsWith('.cmake')) .map(f => `cmake/modules/${f}`); } // Find toolchain files const toolchainFiles = [ 'toolchain/zephyr/generic.cmake', 'toolchain/gnuarmemb/generic.cmake', 'toolchain/xtools/generic.cmake' ]; for (const file of toolchainFiles) { const fullPath = path.join(cmakePath, file); if (await this.fileExists(fullPath)) { this.analysis.cmake.toolchains.push(`cmake/${file}`); } } // Check for Zephyr package config const packageConfigPath = path.join(zephyrPath, 'share/zephyr-package/cmake/ZephyrConfig.cmake'); if (await this.fileExists(packageConfigPath)) { this.analysis.cmake.packageConfig = 'share/zephyr-package/cmake/ZephyrConfig.cmake'; } } catch (error) { console.error('Error finding CMake configs:', error); } } async findBoards() { const zephyrPath = path.join(this.workspacePath, this.analysis.westConfig?.zephyr?.base || 'zephyr'); const boardsPath = path.join(zephyrPath, 'boards'); try { if (await this.fileExists(boardsPath)) { const archDirs = await fs.readdir(boardsPath); for (const arch of archDirs) { const archPath = path.join(boardsPath, arch); const stat = await fs.stat(archPath); if (stat.isDirectory() && !arch.startsWith('.')) { const boards = await fs.readdir(archPath); for (const board of boards) { const boardPath = path.join(archPath, board); const boardStat = await fs.stat(boardPath); if (boardStat.isDirectory()) { // Check for board definition files const defnFile = path.join(boardPath, `${board}.yaml`); const dtsiFile = path.join(boardPath, `${board}.dtsi`); if (await this.fileExists(defnFile) || await this.fileExists(dtsiFile)) { this.analysis.boards.push({ name: board, arch: arch, path: `boards/${arch}/${board}` }); } } } } } } // Also check NRF boards const nrfBoardsPath = path.join(this.workspacePath, 'nrf/boards'); if (await this.fileExists(nrfBoardsPath)) { const nrfBoards = await this.findBoardsInDir(nrfBoardsPath, 'nrf/boards'); this.analysis.boards.push(...nrfBoards); } } catch (error) { console.error('Error finding boards:', error); } } async findBoardsInDir(dirPath, relPath) { const boards = []; try { const entries = await fs.readdir(dirPath); for (const entry of entries) { const entryPath = path.join(dirPath, entry); const stat = await fs.stat(entryPath); if (stat.isDirectory() && !entry.startsWith('.')) { // Check if this is a board directory const yamlFile = path.join(entryPath, `${entry}.yaml`); const dtsFile = path.join(entryPath, `${entry}.dts`); if (await this.fileExists(yamlFile) || await this.fileExists(dtsFile)) { boards.push({ name: entry, path: `${relPath}/${entry}`, vendor: 'nordic' }); } } } } catch (error) { console.error(`Error scanning ${dirPath}:`, error); } return boards; } async verifyKconfigs(kconfigNames) { const results = []; // Ensure we have parsed the West config if (!this.analysis.isValid) { await this.parseWestConfig(); } // Find all Kconfig files if not already done if (this.analysis.kconfig.files.length === 0) { await this.findKconfigFiles(); } // First verification pass for (const configName of kconfigNames) { const result = await this.verifyKconfig(configName); results.push(result); } return results; } async verifyKconfig(configName) { const result = { name: configName, available: false, source: null, description: null, dependencies: [], alternatives: [], suggestions: [], warning: null }; // Known alternatives and suggestions for common missing configs const knownAlternatives = { 'BT_HRS': { alternatives: [], suggestions: ['Use Nordic samples/bluetooth/peripheral_hrs', 'Implement manually using BT_GATT_SERVICE_DEFINE'], warning: 'Heart Rate Service not available as built-in Kconfig' }, 'BT_CSC': { alternatives: [], suggestions: ['Use Nordic samples/bluetooth/peripheral_csc', 'Implement manually using BT_GATT_SERVICE_DEFINE'], warning: 'Cycling Speed and Cadence Service not available as built-in Kconfig' }, 'BT_RSC': { alternatives: [], suggestions: ['Use Nordic samples/bluetooth/peripheral_rsc', 'Implement manually using BT_GATT_SERVICE_DEFINE'], warning: 'Running Speed and Cadence Service not available as built-in Kconfig' }, 'BT_LLS': { alternatives: [], suggestions: ['Implement manually using BT_GATT_SERVICE_DEFINE'], warning: 'Link Loss Service not available as built-in Kconfig' }, 'BT_HIDS': { alternatives: ['BT_HIDS_MAX_CLIENT_COUNT', 'BT_HIDS_ATTR_MAX'], suggestions: ['Use Nordic HIDS service Kconfigs'], warning: 'Use Nordic-specific HIDS Kconfigs instead' }, 'BT_HOGP': { alternatives: ['BT_HOGP_REPORTS_MAX'], suggestions: ['Use Nordic HOGP Kconfigs'], warning: 'Only Nordic HOGP reports configuration available' }, 'NRF_BT_SCAN': { alternatives: ['BT_SCAN'], suggestions: ['Use BT_SCAN from Matter Bridge', 'Implement scanning manually'], warning: 'Use BT_SCAN instead of NRF_BT_SCAN' } }; try { // Use LLM-based intelligent analysis const analysisResult = await this.analyzeKconfigWithLLM(configName); if (analysisResult.available) { result.available = true; result.source = analysisResult.source; result.description = analysisResult.description; result.dependencies = analysisResult.dependencies || []; } else { // First fallback: regex-based search for verification const searchPattern = new RegExp(`^\\s*config\\s+${configName}(?:\\s|$)`, 'm'); const menuConfigPattern = new RegExp(`^\\s*menuconfig\\s+${configName}(?:\\s|$)`, 'm'); for (const kconfigFile of this.analysis.kconfig.files) { const content = await this.readFile(kconfigFile); if (!content) continue; if (searchPattern.test(content) || menuConfigPattern.test(content)) { result.available = true; result.source = kconfigFile.replace(this.workspacePath, ''); // Extract description if available const descMatch = content.match(new RegExp(`config\\s+${configName}[\\s\\S]*?help\\s*\\n\\s*([^\\n]+)`, 'i')); if (descMatch && descMatch[1]) { result.description = descMatch[1].trim(); } // Extract dependencies (simplified - looks for 'depends on' lines) const dependsMatches = content.match(new RegExp(`config\\s+${configName}[\\s\\S]*?(?=config\\s|menuconfig\\s|$)`, 'i')); if (dependsMatches && dependsMatches[0]) { const configSection = dependsMatches[0]; const dependsLines = configSection.match(/depends\s+on\s+(.+)/gi); if (dependsLines) { for (const line of dependsLines) { const deps = line.replace(/depends\s+on\s+/i, '').split(/\s*&&\s*|\s*\|\|\s*/); result.dependencies.push(...deps.map(dep => dep.trim().replace(/^!/, ''))); } } // Look for 'select' dependencies too const selectLines = configSection.match(/select\s+(.+)/gi); if (selectLines) { for (const line of selectLines) { const dep = line.replace(/select\s+/i, '').trim(); result.dependencies.push(dep); } } } break; } } // Second LLM check for missing kconfigs with deep workspace search if (!result.available) { const secondLLMResult = await this.deepWorkspaceKconfigSearch(configName); if (secondLLMResult.available) { result.available = true; result.source = secondLLMResult.source; result.description = secondLLMResult.description; result.dependencies = secondLLMResult.dependencies || []; } else { // Final regex attempt with broader patterns const broadPatterns = [ new RegExp(`${configName}(?:\\s|$)`, 'mi'), new RegExp(`config.*${configName.split('_').join('.*')}`, 'mi'), new RegExp(`${configName.toLowerCase()}`, 'i') ]; for (const kconfigFile of this.analysis.kconfig.files) { const content = await this.readFile(kconfigFile); if (!content) continue; for (const pattern of broadPatterns) { if (pattern.test(content)) { // Found potential match, extract more details const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { if (pattern.test(lines[i])) { result.available = true; result.source = kconfigFile.replace(this.workspacePath, ''); result.description = `Found potential match: ${lines[i].trim()}`; break; } } if (result.available) break; } } if (result.available) break; } } } } // If not found, check known alternatives if (!result.available && knownAlternatives[configName]) { const known = knownAlternatives[configName]; result.alternatives = known.alternatives; result.suggestions = known.suggestions; result.warning = known.warning; } // Special case handling for common BLE configs if (!result.available) { // Check if it's a BLE config that might need BT subsystem if (configName.startsWith('BT_')) { result.suggestions.push('Ensure CONFIG_BT=y is enabled'); // Check if BT subsystem is available const btAvailable = await this.verifyKconfig('BT'); if (btAvailable.available) { result.suggestions.push('BT subsystem is available, config might be conditional'); } } } } catch (error) { result.warning = `Error verifying config: ${error.message}`; } // Remove duplicates from dependencies result.dependencies = [...new Set(result.dependencies)]; return result; } async analyzeKconfigWithLLM(configName, enhancedSearch = false) { // LLM-based intelligent Kconfig analysis try { // Read and analyze relevant Kconfig files const relevantFiles = []; const btRelated = configName.startsWith('BT_'); const mcumgrRelated = configName.startsWith('MCUMGR'); // Find files that might contain this config for (const kconfigFile of this.analysis.kconfig.files) { const fileName = path.basename(kconfigFile).toLowerCase(); const filePath = kconfigFile.toLowerCase(); if (enhancedSearch) { // Enhanced search: include more files and broader patterns const configNameLower = configName.toLowerCase(); const configParts = configName.split('_'); if (btRelated && (filePath.includes('bluetooth') || filePath.includes('/bt/') || fileName.includes('bluetooth') || filePath.includes('ble'))) { relevantFiles.push(kconfigFile); } else if (mcumgrRelated && (filePath.includes('mcumgr') || fileName.includes('mcumgr') || filePath.includes('mgmt'))) { relevantFiles.push(kconfigFile); } else if (fileName === 'kconfig' || fileName.startsWith('kconfig.') || fileName.endsWith('.kconfig')) { relevantFiles.push(kconfigFile); } else if (configParts.some(part => part.length > 2 && filePath.includes(part.toLowerCase()))) { // Include files that contain any significant part of the config name relevantFiles.push(kconfigFile); } // Higher limit for enhanced search if (relevantFiles.length >= 40) break; } else { // Standard filtering based on config name patterns if (btRelated && (filePath.includes('bluetooth') || filePath.includes('/bt/') || fileName.includes('bluetooth'))) { relevantFiles.push(kconfigFile); } else if (mcumgrRelated && (filePath.includes('mcumgr') || fileName.includes('mcumgr'))) { relevantFiles.push(kconfigFile); } else if (fileName === 'kconfig' || fileName.startsWith('kconfig.')) { // Include main Kconfig files for comprehensive search relevantFiles.push(kconfigFile); } // Limit to first 20 files to avoid excessive processing if (relevantFiles.length >= 20) break; } } // Analyze each relevant file with LLM reasoning for (const file of relevantFiles) { const content = await this.readFile(file); if (!content) continue; // Use intelligent text analysis to find configurations const analysis = this.intelligentKconfigSearch(content, configName, file); if (analysis.found) { return { available: true, source: file.replace(this.workspacePath, ''), description: analysis.description, dependencies: analysis.dependencies, context: analysis.context }; } } return { available: false }; } catch (error) { console.error('LLM analysis error:', error); return { available: false, error: error.message }; } } intelligentKconfigSearch(content, configName, filePath) { // Intelligent pattern matching with context analysis const result = { found: false, description: null, dependencies: [], context: null }; try { // Multi-pattern search for different Kconfig formats const patterns = [ new RegExp(`^\\s*config\\s+${configName}(?:\\s|$)`, 'm'), new RegExp(`^\\s*menuconfig\\s+${configName}(?:\\s|$)`, 'm'), new RegExp(`^\\s*choice\\s+${configName}`, 'm'), // Handle if/endif blocks that might contain the config new RegExp(`config\\s+${configName}(?=\\s)`, 'i') ]; let match = null; let matchedPattern = null; for (const pattern of patterns) { match = content.match(pattern); if (match) { matchedPattern = pattern; break; } } if (!match) return result; result.found = true; // Extract the full configuration section const configStart = match.index; const remainingContent = content.slice(configStart); // Find the end of this config section (next config/menuconfig or end of file) const nextConfigMatch = remainingContent.match(/\n\s*(config|menuconfig|choice|endchoice|menu|endmenu)\s+(?!comment)/); const configEnd = nextConfigMatch ? nextConfigMatch.index : remainingContent.length; const configSection = remainingContent.slice(0, configEnd); // Extract description from help section const helpMatch = configSection.match(/help\s*\n\s*(.+?)(?=\n\s*\w|\n\n|$)/s); if (helpMatch) { result.description = helpMatch[1].replace(/\n\s*/g, ' ').trim(); } // Extract dependencies with smart parsing const dependsLines = configSection.match(/depends\s+on\s+(.+)/gi); if (dependsLines) { for (const line of dependsLines) { const deps = line.replace(/depends\s+on\s+/i, '').split(/\s*&&\s*|\s*\|\|\s*/); result.dependencies.push(...deps.map(dep => dep.trim().replace(/^!/, '').replace(/[()]/g, ''))); } } // Extract 'select' dependencies const selectLines = configSection.match(/select\s+([A-Z_][A-Z0-9_]*)/gi); if (selectLines) { for (const line of selectLines) { const dep = line.replace(/select\s+/i, '').trim(); result.dependencies.push(dep); } } // Add context information result.context = { file: path.basename(filePath), section: configSection.slice(0, 200) + (configSection.length > 200 ? '...' : ''), type: matchedPattern.source.includes('menuconfig') ? 'menuconfig' : 'config' }; // Clean up dependencies result.dependencies = [...new Set(result.dependencies.filter(dep => dep && dep.length > 0))]; } catch (error) { console.error('Intelligent search error:', error); } return result; } async deepWorkspaceKconfigSearch(configName) { // Deep workspace-wide Kconfig search for missing configs try { console.log(`Performing deep workspace search for ${configName}...`); // Find ALL Kconfig files in the entire workspace (not just cached ones) const allKconfigFiles = await this.findAllKconfigFilesRecursive(this.workspacePath); // Broader search criteria const btRelated = configName.startsWith('BT_'); const configParts = configName.split('_').filter(part => part.length > 1); const searchTerms = [configName, ...configParts]; // Search through all files with intelligent prioritization const searchResults = []; for (const kconfigFile of allKconfigFiles) { const content = await this.readFile(kconfigFile); if (!content) continue; // Multiple search strategies const strategies = [ // Exact match { pattern: new RegExp(`^\\s*config\\s+${configName}(?:\\s|$)`, 'm'), weight: 10 }, { pattern: new RegExp(`^\\s*menuconfig\\s+${configName}(?:\\s|$)`, 'm'), weight: 10 }, // Case insensitive exact match { pattern: new RegExp(`^\\s*config\\s+${configName}(?:\\s|$)`, 'mi'), weight: 8 }, { pattern: new RegExp(`^\\s*menuconfig\\s+${configName}(?:\\s|$)`, 'mi'), weight: 8 }, // Partial matches with config context { pattern: new RegExp(`config\\s+\\w*${configName}\\w*`, 'i'), weight: 6 }, { pattern: new RegExp(`config\\s+${configName}\\w+`, 'i'), weight: 5 }, { pattern: new RegExp(`config\\s+\\w+${configName}`, 'i'), weight: 5 }, // Any mention in config context { pattern: new RegExp(`${configName}`, 'i'), weight: 2 } ]; for (const strategy of strategies) { const match = content.match(strategy.pattern); if (match) { const analysis = this.intelligentKconfigSearch(content, configName, kconfigFile); if (analysis.found) { return { available: true, source: kconfigFile.replace(this.workspacePath, ''), description: analysis.description || `Found via deep search: ${match[0]}`, dependencies: analysis.dependencies, context: analysis.context, searchMethod: 'deep-workspace' }; } // Store potential matches for fallback searchResults.push({ file: kconfigFile, match: match[0], weight: strategy.weight, line: this.getLineNumber(content, match.index) }); break; // Don't try other strategies for this file } } } // If no exact matches, try semantic search through project files const semanticResult = await this.semanticKconfigSearch(configName, searchTerms); if (semanticResult.available) { return semanticResult; } // Return best potential match if any if (searchResults.length > 0) { const bestMatch = searchResults.sort((a, b) => b.weight - a.weight)[0]; return { available: true, source: bestMatch.file.replace(this.workspacePath, ''), description: `Potential match found: ${bestMatch.match.trim()} (line ${bestMatch.line})`, dependencies: [], searchMethod: 'deep-workspace-fuzzy', confidence: 'low' }; } return { available: false, searchMethod: 'deep-workspace' }; } catch (error) { console.error('Deep workspace search error:', error); return { available: false, error: error.message }; } } async findAllKconfigFilesRecursive(startPath, maxDepth = 6, currentDepth = 0) { const allFiles = []; if (currentDepth >= maxDepth) return allFiles; try { const entries = await fs.readdir(startPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(startPath, entry.name); if (entry.isDirectory()) { // Skip certain directories for performance if (!entry.name.startsWith('.') && !['node_modules', 'build', 'dist', '__pycache__', 'cmake-build'].includes(entry.name)) { const subFiles = await this.findAllKconfigFilesRecursive(fullPath, maxDepth, currentDepth + 1); allFiles.push(...subFiles); } } else if (entry.isFile()) { // Include all potential Kconfig files if (entry.name === 'Kconfig' || entry.name.startsWith('Kconfig.') || entry.name.endsWith('.kconfig') || entry.name.endsWith('.Kconfig') || (entry.name.toLowerCase().includes('kconfig') && entry.name.endsWith('.txt'))) { allFiles.push(fullPath); } } } } catch (error) { // Skip directories we can't read if (error.code !== 'EACCES' && error.code !== 'ENOENT') { console.error(`Error reading ${startPath}:`, error.message); } } return allFiles; } async semanticKconfigSearch(configName, searchTerms) { // Search through project manifests and documentation for semantic clues try { const semanticFiles = [ path.join(this.workspacePath, 'west.yml'), path.join(this.workspacePath, 'zephyr/doc'), path.join(this.workspacePath, 'nrf/doc') ]; for (const searchPath of semanticFiles) { if (await this.fileExists(searchPath)) { const content = await this.readFile(searchPath); if (content) { // Look for mentions of the config or related terms for (const term of searchTerms) { if (content.toLowerCase().includes(term.toLowerCase())) { return { available: true, source: searchPath.replace(this.workspacePath, ''), description: `Referenced in documentation/manifest: ${term}`, dependencies: [], searchMethod: 'semantic', confidence: 'medium' }; } } } } } return { available: false }; } catch (error) { console.error('Semantic search error:', error); return { available: false, error: error.message }; } } getLineNumber(content, index) { return content.substring(0, index).split('\n').length; } } module.exports = WestWorkspaceAnalyzer;