UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

330 lines 13.2 kB
import { existsSync, readFileSync } from 'fs'; import { basename, join } from 'path'; import { FileScanner } from '../init/file-scanner.js'; import { FrameworkDetector, } from '../init/framework-detector.js'; import { LanguageDetector } from '../init/language-detector.js'; export class ProjectAnalyzer { projectPath; fileScanner; frameworkDetector; constructor(projectPath) { this.projectPath = projectPath; this.fileScanner = new FileScanner(projectPath); this.frameworkDetector = new FrameworkDetector(projectPath); } /** * Perform comprehensive project analysis */ analyze() { // Scan files const scanResult = this.fileScanner.scan(); const keyFiles = this.fileScanner.getProjectFiles(); // Detect languages const codeFiles = scanResult.files.filter(file => this.isCodeFile(file) && !this.isTestFile(file)); const languages = LanguageDetector.detectLanguages(codeFiles); // Detect frameworks and dependencies const dependencies = this.frameworkDetector.detectDependencies(); // Analyze project structure const importantDirectories = this.getImportantDirectories(scanResult.directories); // Get build commands const buildCommands = this.frameworkDetector.getBuildCommands(); // Extract project metadata const { projectName, description, repository } = this.extractProjectMetadata(); // Determine project type const projectType = this.determineProjectType(languages, dependencies); return { projectPath: this.projectPath, projectName, languages, dependencies, projectType, keyFiles, structure: { totalFiles: scanResult.totalFiles, scannedFiles: scanResult.scannedFiles, directories: scanResult.directories, importantDirectories, }, buildCommands, description, repository, }; } /** * Check if a file is a code file */ isCodeFile(file) { const codeExtensions = [ '.js', '.jsx', '.ts', '.tsx', '.py', '.rs', '.go', '.java', '.kt', '.c', '.cpp', '.h', '.hpp', '.cs', '.php', '.rb', '.swift', '.dart', '.vue', '.svelte', ]; const ext = file.substring(file.lastIndexOf('.')); return codeExtensions.includes(ext); } /** * Check if a file is a test file */ isTestFile(file) { const fileName = basename(file).toLowerCase(); return (fileName.includes('test') || fileName.includes('spec') || file.includes('__tests__') || file.includes('/test/') || file.includes('/tests/') || file.includes('/spec/')); } /** * Get important directories based on common patterns */ getImportantDirectories(directories) { const important = new Set(); const sourcePatterns = [ 'src', 'source', 'app', 'lib', 'libs', 'components', 'pages', 'views', 'routes', 'api', 'server', 'backend', 'frontend', 'models', 'controllers', 'services', 'utils', 'config', 'configs', 'settings', 'assets', 'static', 'public', 'docs', 'documentation', ]; const testPatterns = ['test', 'tests', '__tests__', 'spec']; // First pass: Add all source directories for (const dir of directories) { const dirName = basename(dir).toLowerCase(); const dirParts = dir.split('/'); // Check if this is a source directory (not inside test directories) const isInTestDir = dirParts.some(part => testPatterns.includes(part.toLowerCase())); if (!isInTestDir) { // Add if directory name matches source patterns if (sourcePatterns.includes(dirName)) { important.add(dir); } // Add if any part of the path matches source patterns (e.g., "src/components") if (dirParts.some(part => sourcePatterns.includes(part.toLowerCase()))) { important.add(dir); } } } // Second pass: Add test directories only if we don't have many source directories if (important.size < 5) { for (const dir of directories) { const dirName = basename(dir).toLowerCase(); const dirParts = dir.split('/'); // Check if this is a test directory const isTestDir = testPatterns.includes(dirName) || dirParts.some(part => testPatterns.includes(part.toLowerCase())); if (isTestDir) { // Only add test directories that contain meaningful structure if (dirParts.length > 1 || directories.filter(d => d.startsWith(dir)).length > 1) { important.add(dir); } } } } // Sort with source directories first, then by depth (fewer levels first), then alphabetically return Array.from(important).sort((a, b) => { const aIsTest = testPatterns.some(pattern => a.includes(pattern)); const bIsTest = testPatterns.some(pattern => b.includes(pattern)); // Source directories first if (aIsTest !== bIsTest) { return aIsTest ? 1 : -1; } // Then by depth (fewer levels first for better overview) const aDepth = a.split('/').length; const bDepth = b.split('/').length; if (aDepth !== bDepth) { return aDepth - bDepth; } // Finally alphabetically return a.localeCompare(b); }); } /** * Extract project metadata from package.json, README, etc. */ extractProjectMetadata() { const defaultName = basename(this.projectPath); let projectName = defaultName; let description; let repository; // Try package.json first const packageJsonPath = join(this.projectPath, 'package.json'); if (existsSync(packageJsonPath)) { try { const content = readFileSync(packageJsonPath, 'utf-8'); const pkg = JSON.parse(content); if (pkg.name) projectName = pkg.name; if (pkg.description) description = pkg.description; if (pkg.repository) { if (typeof pkg.repository === 'string') { repository = pkg.repository; } else if (pkg.repository.url) { repository = pkg.repository.url; } } } catch { // Ignore parsing errors } } // Try Cargo.toml for Rust projects if (!description) { const cargoPath = join(this.projectPath, 'Cargo.toml'); if (existsSync(cargoPath)) { try { const content = readFileSync(cargoPath, 'utf-8'); const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m); const descMatch = content.match(/^description\s*=\s*"([^"]+)"/m); if (nameMatch) projectName = nameMatch[1]; if (descMatch) description = descMatch[1]; } catch { // Ignore parsing errors } } } // Try to extract description from README if (!description) { const readmeFiles = ['README.md', 'README.rst', 'README.txt', 'README']; for (const readmeFile of readmeFiles) { const readmePath = join(this.projectPath, readmeFile); if (existsSync(readmePath)) { try { const content = readFileSync(readmePath, 'utf-8'); const lines = content.split('\n').filter(line => line.trim()); // Look for the first non-title line as description for (const line of lines.slice(1)) { if (line.trim() && !line.startsWith('#') && !line.startsWith('!')) { description = line.trim(); break; } } break; } catch { // Ignore parsing errors } } } } return { projectName, description, repository }; } /** * Determine the overall project type */ determineProjectType(languages, dependencies) { // Use framework-based detection first const webFrameworks = dependencies.frameworks.filter((f) => f.category === 'web'); const backendFrameworks = dependencies.frameworks.filter((f) => f.category === 'backend'); const mobileFrameworks = dependencies.frameworks.filter((f) => f.category === 'mobile'); const desktopFrameworks = dependencies.frameworks.filter((f) => f.category === 'desktop'); if (mobileFrameworks.length > 0) { return `${mobileFrameworks[0].name} Mobile Application`; } if (desktopFrameworks.length > 0) { return `${desktopFrameworks[0].name} Desktop Application`; } if (webFrameworks.length > 0 && backendFrameworks.length > 0) { return `Full Stack Web Application (${webFrameworks[0].name} + ${backendFrameworks[0].name})`; } if (webFrameworks.length > 0) { return `${webFrameworks[0].name} Web Application`; } if (backendFrameworks.length > 0) { return `${backendFrameworks[0].name} Backend Application`; } // Fall back to language-based detection return LanguageDetector.suggestProjectType(languages); } /** * Get coding conventions based on detected languages and frameworks */ getCodingConventions() { const analysis = this.analyze(); const conventions = []; // Language-specific conventions if (analysis.languages.primary) { const lang = analysis.languages.primary.name; switch (lang) { case 'JavaScript': case 'TypeScript': conventions.push('Use camelCase for variables and functions'); conventions.push('Use PascalCase for classes and components'); conventions.push('Use const/let instead of var'); if (analysis.dependencies.frameworks.some((f) => f.name.includes('React'))) { conventions.push('Use functional components with hooks'); conventions.push('Follow React naming conventions'); } break; case 'Python': conventions.push('Follow PEP 8 style guide'); conventions.push('Use snake_case for variables and functions'); conventions.push('Use PascalCase for classes'); conventions.push('Include type hints where appropriate'); break; case 'Rust': conventions.push('Follow Rust naming conventions (snake_case)'); conventions.push('Use cargo fmt for formatting'); conventions.push('Handle errors explicitly with Result<T, E>'); break; case 'Go': conventions.push('Follow Go naming conventions'); conventions.push('Use gofmt for formatting'); conventions.push('Handle errors explicitly'); conventions.push('Use interfaces for abstraction'); break; } } // Testing conventions if (analysis.dependencies.testingFrameworks.length > 0) { conventions.push(`Write tests using ${analysis.dependencies.testingFrameworks.join(', ')}`); conventions.push('Maintain good test coverage'); } return conventions; } } //# sourceMappingURL=project-analyzer.js.map