UNPKG

@typecad/kicad-symbols

Version:

Intelligent fuzzy search for KiCad symbols with CLI interface

242 lines 9.08 kB
/** * Scanner for finding KiCad symbol files (.kicad_sym) in the local filesystem * Recursively searches through the KiCad symbols directory to locate all symbol files */ import fs from 'node:fs'; import path from 'node:path'; /** * Default scan options */ const DEFAULT_SCAN_OPTIONS = { recursive: true, maxDepth: 0, // No limit maxFileSize: 0, // No limit followSymlinks: false, excludePatterns: ['.git', '.svn', '.hg', 'node_modules', '__pycache__'] }; /** * Scanner for KiCad symbol files in the local filesystem */ export class KiCadSymbolFileScanner { symbolsRootPath; options; statistics; constructor(symbolsRootPath, options = {}) { this.symbolsRootPath = symbolsRootPath; this.options = { ...DEFAULT_SCAN_OPTIONS, ...options }; this.statistics = { totalFiles: 0, totalSize: 0, directoriesScanned: 0, scanDuration: 0, errors: [] }; } /** * Scan the symbols directory for all .kicad_sym files * @returns Promise resolving to array of symbol file information */ async scanForSymbolFiles() { const startTime = Date.now(); this.resetStatistics(); if (!fs.existsSync(this.symbolsRootPath)) { throw new Error(`Symbols directory does not exist: ${this.symbolsRootPath}`); } const stat = await fs.promises.stat(this.symbolsRootPath); if (!stat.isDirectory()) { throw new Error(`Symbols path is not a directory: ${this.symbolsRootPath}`); } const symbolFiles = []; try { await this.scanDirectory(this.symbolsRootPath, symbolFiles, 0); } catch (error) { const errorMessage = `Error scanning symbols directory: ${error instanceof Error ? error.message : String(error)}`; this.statistics.errors.push(errorMessage); throw new Error(errorMessage); } this.statistics.scanDuration = Date.now() - startTime; return symbolFiles; } /** * Get scanning statistics from the last scan operation * @returns Scan statistics */ getStatistics() { return { ...this.statistics }; } /** * Recursively scan a directory for symbol files * @param dirPath - Directory path to scan * @param symbolFiles - Array to collect found symbol files * @param currentDepth - Current scanning depth * @private */ async scanDirectory(dirPath, symbolFiles, currentDepth) { // Check depth limit if (this.options.maxDepth > 0 && currentDepth >= this.options.maxDepth) { return; } this.statistics.directoriesScanned++; try { const entries = await fs.promises.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); try { if (entry.isDirectory()) { // Skip excluded directories if (this.shouldExcludeDirectory(entry.name)) { continue; } // Recursively scan subdirectory if recursive option is enabled if (this.options.recursive) { await this.scanDirectory(fullPath, symbolFiles, currentDepth + 1); } } else if (entry.isFile() || (entry.isSymbolicLink() && this.options.followSymlinks)) { // Check if this is a .kicad_sym file if (this.isSymbolFile(entry.name)) { const fileInfo = await this.createFileInfo(fullPath); if (fileInfo && this.isValidFileSize(fileInfo.size)) { symbolFiles.push(fileInfo); this.statistics.totalFiles++; this.statistics.totalSize += fileInfo.size; } } } } catch (error) { const errorMessage = `Error processing ${fullPath}: ${error instanceof Error ? error.message : String(error)}`; this.statistics.errors.push(errorMessage); // Continue scanning other files continue; } } } catch (error) { const errorMessage = `Error reading directory ${dirPath}: ${error instanceof Error ? error.message : String(error)}`; this.statistics.errors.push(errorMessage); throw error; } } /** * Create file information object for a symbol file * @param filePath - Full path to the file * @returns File information or null if file cannot be accessed * @private */ async createFileInfo(filePath) { try { const stat = await fs.promises.stat(filePath); // Skip if it's not actually a file (could be a directory with .kicad_sym name) if (!stat.isFile()) { return null; } const fileName = path.basename(filePath, '.kicad_sym'); const fullFileName = path.basename(filePath); const directory = path.dirname(filePath); const relativePath = path.relative(this.symbolsRootPath, filePath); return { filePath, fileName, fullFileName, directory, relativePath, size: stat.size, lastModified: stat.mtime }; } catch (error) { const errorMessage = `Cannot access file ${filePath}: ${error instanceof Error ? error.message : String(error)}`; this.statistics.errors.push(errorMessage); return null; } } /** * Check if a file is a KiCad symbol file * @param fileName - Name of the file * @returns True if it's a .kicad_sym file * @private */ isSymbolFile(fileName) { return fileName.toLowerCase().endsWith('.kicad_sym'); } /** * Check if a directory should be excluded from scanning * @param dirName - Directory name * @returns True if directory should be excluded * @private */ shouldExcludeDirectory(dirName) { return this.options.excludePatterns.some(pattern => { if (pattern.includes('*') || pattern.includes('?')) { // Simple glob pattern matching const regexPattern = pattern .replace(/\*/g, '.*') .replace(/\?/g, '.'); return new RegExp(`^${regexPattern}$`, 'i').test(dirName); } else { // Exact match (case insensitive) return dirName.toLowerCase() === pattern.toLowerCase(); } }); } /** * Check if file size is within acceptable limits * @param fileSize - Size of the file in bytes * @returns True if file size is acceptable * @private */ isValidFileSize(fileSize) { if (this.options.maxFileSize <= 0) { return true; // No size limit } return fileSize <= this.options.maxFileSize; } /** * Reset scanning statistics * @private */ resetStatistics() { this.statistics = { totalFiles: 0, totalSize: 0, directoriesScanned: 0, scanDuration: 0, errors: [] }; } /** * Get a human-readable summary of the scan results * @returns Formatted summary string */ getScanSummary() { const stats = this.statistics; const sizeInMB = (stats.totalSize / (1024 * 1024)).toFixed(2); const durationInSeconds = (stats.scanDuration / 1000).toFixed(2); let summary = `Scan Summary:\n`; summary += ` Files found: ${stats.totalFiles}\n`; summary += ` Total size: ${sizeInMB} MB\n`; summary += ` Directories scanned: ${stats.directoriesScanned}\n`; summary += ` Scan duration: ${durationInSeconds} seconds\n`; if (stats.errors.length > 0) { summary += ` Errors encountered: ${stats.errors.length}\n`; if (stats.errors.length <= 5) { summary += ` Error details:\n`; stats.errors.forEach(error => { summary += ` - ${error}\n`; }); } else { summary += ` First 5 errors:\n`; stats.errors.slice(0, 5).forEach(error => { summary += ` - ${error}\n`; }); summary += ` ... and ${stats.errors.length - 5} more errors\n`; } } return summary; } } //# sourceMappingURL=KiCadSymbolFileScanner.js.map