import { useCallback, useState } from 'react'; import { createLogger } from '@automaker/utils/logger'; import { useQueryClient } from '@tanstack/react-query'; import { useAppStore, FileTreeNode, ProjectAnalysis, Feature } from '@/store/app-store'; import { getElectronAPI } from '@/lib/electron'; import { queryKeys } from '@/lib/query-keys'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Folder, FolderOpen, File, ChevronRight, ChevronDown, Search, RefreshCw, BarChart3, FileCode, FileText, CheckCircle, AlertCircle, ListChecks, } from 'lucide-react'; import { Spinner } from '@/components/ui/spinner'; import { cn, generateUUID } from '@/lib/utils'; const logger = createLogger('AnalysisView'); const IGNORE_PATTERNS = [ 'node_modules', '.git', '.next', 'dist', 'build', '.DS_Store', '*.log', '.cache', 'coverage', '__pycache__', '.pytest_cache', '.venv', 'venv', '.env', ]; const shouldIgnore = (name: string) => { return IGNORE_PATTERNS.some((pattern) => { if (pattern.startsWith('*')) { return name.endsWith(pattern.slice(1)); } return name === pattern; }); }; const getExtension = (filename: string): string => { const parts = filename.split('.'); return parts.length > 1 ? parts.pop() || '' : ''; }; export function AnalysisView() { const { currentProject, projectAnalysis, isAnalyzing, setProjectAnalysis, setIsAnalyzing, clearAnalysis, } = useAppStore(); const [expandedFolders, setExpandedFolders] = useState>(new Set()); const [isGeneratingSpec, setIsGeneratingSpec] = useState(false); const [specGenerated, setSpecGenerated] = useState(false); const [specError, setSpecError] = useState(null); const [isGeneratingFeatureList, setIsGeneratingFeatureList] = useState(false); const [featureListGenerated, setFeatureListGenerated] = useState(false); const [featureListError, setFeatureListError] = useState(null); const queryClient = useQueryClient(); // Recursively scan directory const scanDirectory = useCallback( async (path: string, depth: number = 0): Promise => { if (depth > 10) return []; // Prevent infinite recursion const api = getElectronAPI(); try { const result = await api.readdir(path); if (!result.success || !result.entries) return []; const nodes: FileTreeNode[] = []; const entries = result.entries.filter((e) => !shouldIgnore(e.name)); for (const entry of entries) { const fullPath = `${path}/${entry.name}`; const node: FileTreeNode = { name: entry.name, path: fullPath, isDirectory: entry.isDirectory, extension: entry.isFile ? getExtension(entry.name) : undefined, }; if (entry.isDirectory) { // Recursively scan subdirectories node.children = await scanDirectory(fullPath, depth + 1); } nodes.push(node); } // Sort: directories first, then files alphabetically nodes.sort((a, b) => { if (a.isDirectory && !b.isDirectory) return -1; if (!a.isDirectory && b.isDirectory) return 1; return a.name.localeCompare(b.name); }); return nodes; } catch (error) { logger.error('Failed to scan directory:', path, error); return []; } }, [] ); // Count files and directories const countNodes = ( nodes: FileTreeNode[] ): { files: number; dirs: number; byExt: Record } => { let files = 0; let dirs = 0; const byExt: Record = {}; const traverse = (items: FileTreeNode[]) => { for (const item of items) { if (item.isDirectory) { dirs++; if (item.children) traverse(item.children); } else { files++; if (item.extension) { byExt[item.extension] = (byExt[item.extension] || 0) + 1; } else { byExt['(no extension)'] = (byExt['(no extension)'] || 0) + 1; } } } }; traverse(nodes); return { files, dirs, byExt }; }; // Run the analysis const runAnalysis = useCallback(async () => { if (!currentProject) return; setIsAnalyzing(true); clearAnalysis(); try { const fileTree = await scanDirectory(currentProject.path); const counts = countNodes(fileTree); const analysis: ProjectAnalysis = { fileTree, totalFiles: counts.files, totalDirectories: counts.dirs, filesByExtension: counts.byExt, analyzedAt: new Date().toISOString(), }; setProjectAnalysis(analysis); } catch (error) { logger.error('Analysis failed:', error); } finally { setIsAnalyzing(false); } }, [currentProject, setIsAnalyzing, clearAnalysis, scanDirectory, setProjectAnalysis]); // Generate app_spec.txt from analysis const generateSpec = useCallback(async () => { if (!currentProject || !projectAnalysis) return; setIsGeneratingSpec(true); setSpecError(null); setSpecGenerated(false); try { const api = getElectronAPI(); // Read key files to understand the project better const fileContents: Record = {}; const keyFiles = ['package.json', 'README.md', 'tsconfig.json']; // Collect file paths from analysis const collectFilePaths = ( nodes: FileTreeNode[], maxDepth: number = 3, currentDepth: number = 0 ): string[] => { const paths: string[] = []; for (const node of nodes) { if (!node.isDirectory) { paths.push(node.path); } else if (node.children && currentDepth < maxDepth) { paths.push(...collectFilePaths(node.children, maxDepth, currentDepth + 1)); } } return paths; }; collectFilePaths(projectAnalysis.fileTree); // Try to read key configuration files for (const keyFile of keyFiles) { const filePath = `${currentProject.path}/${keyFile}`; const exists = await api.exists(filePath); if (exists) { const result = await api.readFile(filePath); if (result.success && result.content) { fileContents[keyFile] = result.content; } } } // Detect project type and tech stack const detectTechStack = () => { const stack: string[] = []; const extensions = projectAnalysis.filesByExtension; // Check package.json for dependencies if (fileContents['package.json']) { try { const pkg = JSON.parse(fileContents['package.json']); if (pkg.dependencies?.react || pkg.dependencies?.['react-dom']) stack.push('React'); if (pkg.dependencies?.next) stack.push('Next.js'); if (pkg.dependencies?.vue) stack.push('Vue'); if (pkg.dependencies?.angular) stack.push('Angular'); if (pkg.dependencies?.express) stack.push('Express'); if (pkg.dependencies?.electron) stack.push('Electron'); if (pkg.devDependencies?.typescript || pkg.dependencies?.typescript) stack.push('TypeScript'); if (pkg.devDependencies?.tailwindcss || pkg.dependencies?.tailwindcss) stack.push('Tailwind CSS'); if (pkg.devDependencies?.playwright || pkg.dependencies?.playwright) stack.push('Playwright'); if (pkg.devDependencies?.jest || pkg.dependencies?.jest) stack.push('Jest'); } catch { // Ignore JSON parse errors } } // Detect by file extensions if (extensions['ts'] || extensions['tsx']) stack.push('TypeScript'); if (extensions['py']) stack.push('Python'); if (extensions['go']) stack.push('Go'); if (extensions['rs']) stack.push('Rust'); if (extensions['java']) stack.push('Java'); if (extensions['css'] || extensions['scss'] || extensions['sass']) stack.push('CSS/SCSS'); // Remove duplicates return [...new Set(stack)]; }; // Get project name from package.json or folder name const getProjectName = () => { if (fileContents['package.json']) { try { const pkg = JSON.parse(fileContents['package.json']); if (pkg.name) return pkg.name; } catch { // Ignore JSON parse errors } } // Fall back to folder name return currentProject.name; }; // Get project description from package.json or README const getProjectDescription = () => { if (fileContents['package.json']) { try { const pkg = JSON.parse(fileContents['package.json']); if (pkg.description) return pkg.description; } catch { // Ignore JSON parse errors } } if (fileContents['README.md']) { // Extract first paragraph from README const lines = fileContents['README.md'].split('\n'); for (const line of lines) { const trimmed = line.trim(); if ( trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('!') && trimmed.length > 20 ) { return trimmed.substring(0, 200); } } } return 'A software project'; }; // Group files by directory for structure analysis const analyzeStructure = () => { const structure: string[] = []; const topLevelDirs = projectAnalysis.fileTree .filter((n: FileTreeNode) => n.isDirectory) .map((n: FileTreeNode) => n.name); for (const dir of topLevelDirs) { structure.push(` `); } return structure.join('\n'); }; const projectName = getProjectName(); const description = getProjectDescription(); const techStack = detectTechStack(); // Generate the spec content // Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts const specContent = ` ${projectName} ${description} ${Object.entries(projectAnalysis.filesByExtension) .filter(([ext]: [string, number]) => ['ts', 'tsx', 'js', 'jsx', 'py', 'go', 'rs', 'java', 'cpp', 'c'].includes(ext) ) .sort((a: [string, number], b: [string, number]) => b[1] - a[1]) .slice(0, 5) .map(([ext, count]: [string, number]) => ` `) .join('\n')} ${techStack.map((tech) => ` ${tech}`).join('\n')} ${projectAnalysis.totalFiles} ${projectAnalysis.totalDirectories} ${analyzeStructure()} ${Object.entries(projectAnalysis.filesByExtension) .sort((a: [string, number], b: [string, number]) => b[1] - a[1]) .slice(0, 10) .map( ([ext, count]: [string, number]) => ` ` ) .join('\n')} ${projectAnalysis.analyzedAt} `; // Write the spec file const specPath = `${currentProject.path}/.automaker/app_spec.txt`; const writeResult = await api.writeFile(specPath, specContent); if (writeResult.success) { setSpecGenerated(true); } else { setSpecError(writeResult.error || 'Failed to write spec file'); } } catch (error) { logger.error('Failed to generate spec:', error); setSpecError(error instanceof Error ? error.message : 'Failed to generate spec'); } finally { setIsGeneratingSpec(false); } }, [currentProject, projectAnalysis]); // Generate features from analysis and save to .automaker/features folder const generateFeatureList = useCallback(async () => { if (!currentProject || !projectAnalysis) return; setIsGeneratingFeatureList(true); setFeatureListError(null); setFeatureListGenerated(false); try { const api = getElectronAPI(); // Read key files to understand the project const fileContents: Record = {}; const keyFiles = ['package.json', 'README.md']; // Try to read key configuration files for (const keyFile of keyFiles) { const filePath = `${currentProject.path}/${keyFile}`; const exists = await api.exists(filePath); if (exists) { const result = await api.readFile(filePath); if (result.success && result.content) { fileContents[keyFile] = result.content; } } } // Collect file paths from analysis const collectFilePaths = (nodes: FileTreeNode[]): string[] => { const paths: string[] = []; for (const node of nodes) { if (!node.isDirectory) { paths.push(node.path); } else if (node.children) { paths.push(...collectFilePaths(node.children)); } } return paths; }; const allFilePaths = collectFilePaths(projectAnalysis.fileTree); // Analyze directories and files to detect features interface DetectedFeature { category: string; description: string; passes: boolean; } const detectedFeatures: DetectedFeature[] = []; // Detect features based on project structure and files const detectFeatures = () => { const extensions = projectAnalysis.filesByExtension; const topLevelDirs = projectAnalysis.fileTree .filter((n: FileTreeNode) => n.isDirectory) .map((n: FileTreeNode) => n.name.toLowerCase()); const topLevelFiles = projectAnalysis.fileTree .filter((n: FileTreeNode) => !n.isDirectory) .map((n: FileTreeNode) => n.name.toLowerCase()); // Check for test directories and files const hasTests = topLevelDirs.includes('tests') || topLevelDirs.includes('test') || topLevelDirs.includes('__tests__') || allFilePaths.some((p) => p.includes('.spec.') || p.includes('.test.')); if (hasTests) { detectedFeatures.push({ category: 'Testing', description: 'Automated test suite', passes: true, }); } // Check for components directory (UI components) const hasComponents = topLevelDirs.includes('components') || allFilePaths.some((p) => p.toLowerCase().includes('/components/')); if (hasComponents) { detectedFeatures.push({ category: 'UI/Design', description: 'Component-based UI architecture', passes: true, }); } // Check for src directory (organized source code) if (topLevelDirs.includes('src')) { detectedFeatures.push({ category: 'Project Structure', description: 'Organized source code structure', passes: true, }); } // Check package.json for dependencies and detect features if (fileContents['package.json']) { try { const pkg = JSON.parse(fileContents['package.json']); // React/Next.js app detection if (pkg.dependencies?.react || pkg.dependencies?.['react-dom']) { detectedFeatures.push({ category: 'Frontend', description: 'React-based user interface', passes: true, }); } if (pkg.dependencies?.next) { detectedFeatures.push({ category: 'Framework', description: 'Next.js framework integration', passes: true, }); } // TypeScript support if ( pkg.devDependencies?.typescript || pkg.dependencies?.typescript || extensions['ts'] || extensions['tsx'] ) { detectedFeatures.push({ category: 'Developer Experience', description: 'TypeScript type safety', passes: true, }); } // Tailwind CSS if (pkg.devDependencies?.tailwindcss || pkg.dependencies?.tailwindcss) { detectedFeatures.push({ category: 'UI/Design', description: 'Tailwind CSS styling', passes: true, }); } // ESLint/Prettier (code quality) if (pkg.devDependencies?.eslint || pkg.devDependencies?.prettier) { detectedFeatures.push({ category: 'Developer Experience', description: 'Code quality tools', passes: true, }); } // Electron (desktop app) if (pkg.dependencies?.electron || pkg.devDependencies?.electron) { detectedFeatures.push({ category: 'Platform', description: 'Electron desktop application', passes: true, }); } // Playwright testing if (pkg.devDependencies?.playwright || pkg.devDependencies?.['@playwright/test']) { detectedFeatures.push({ category: 'Testing', description: 'Playwright end-to-end testing', passes: true, }); } } catch { // Ignore JSON parse errors } } // Check for documentation if (topLevelFiles.includes('readme.md') || topLevelDirs.includes('docs')) { detectedFeatures.push({ category: 'Documentation', description: 'Project documentation', passes: true, }); } // Check for CI/CD configuration const hasCICD = topLevelDirs.includes('.github') || topLevelFiles.includes('.gitlab-ci.yml') || topLevelFiles.includes('.travis.yml'); if (hasCICD) { detectedFeatures.push({ category: 'DevOps', description: 'CI/CD pipeline configuration', passes: true, }); } // Check for API routes (Next.js API or Express) const hasAPIRoutes = allFilePaths.some( (p) => p.includes('/api/') || p.includes('/routes/') || p.includes('/endpoints/') ); if (hasAPIRoutes) { detectedFeatures.push({ category: 'Backend', description: 'API endpoints', passes: true, }); } // Check for state management const hasStateManagement = allFilePaths.some( (p) => p.includes('/store/') || p.includes('/stores/') || p.includes('/redux/') || p.includes('/context/') ); if (hasStateManagement) { detectedFeatures.push({ category: 'Architecture', description: 'State management system', passes: true, }); } // Check for configuration files if (topLevelFiles.includes('tsconfig.json') || topLevelFiles.includes('package.json')) { detectedFeatures.push({ category: 'Configuration', description: 'Project configuration files', passes: true, }); } }; detectFeatures(); // If no features were detected, add a default feature if (detectedFeatures.length === 0) { detectedFeatures.push({ category: 'Core', description: 'Basic project structure', passes: true, }); } // Create each feature using the features API if (!api.features) { throw new Error('Features API not available'); } for (const detectedFeature of detectedFeatures) { const newFeature: Feature = { id: generateUUID(), category: detectedFeature.category, description: detectedFeature.description, status: 'backlog', steps: [], }; await api.features.create(currentProject.path, newFeature); } // Invalidate React Query cache to sync UI queryClient.invalidateQueries({ queryKey: queryKeys.features.all(currentProject.path), }); setFeatureListGenerated(true); } catch (error) { logger.error('Failed to generate feature list:', error); setFeatureListError( error instanceof Error ? error.message : 'Failed to generate feature list' ); } finally { setIsGeneratingFeatureList(false); } }, [currentProject, projectAnalysis, queryClient]); // Toggle folder expansion const toggleFolder = (path: string) => { const newExpanded = new Set(expandedFolders); if (expandedFolders.has(path)) { newExpanded.delete(path); } else { newExpanded.add(path); } setExpandedFolders(newExpanded); }; // Render file tree node const renderNode = (node: FileTreeNode, depth: number = 0) => { const isExpanded = expandedFolders.has(node.path); return (
{ if (node.isDirectory) { toggleFolder(node.path); } }} > {node.isDirectory ? ( <> {isExpanded ? ( ) : ( )} {isExpanded ? ( ) : ( )} ) : ( <> )} {node.name} {node.extension && ( .{node.extension} )}
{node.isDirectory && isExpanded && node.children && (
{node.children.map((child: FileTreeNode) => renderNode(child, depth + 1))}
)}
); }; if (!currentProject) { return (

No project selected

); } return (
{/* Header */}

Project Analysis

{currentProject.name}

{/* Content */}
{!projectAnalysis && !isAnalyzing ? (

No Analysis Yet

Click "Analyze Project" to scan your codebase and get insights about its structure.

) : isAnalyzing ? (

Scanning project files...

) : projectAnalysis ? (
{/* Stats Panel */}
Statistics Analyzed {new Date(projectAnalysis.analyzedAt).toLocaleString()}
Total Files {projectAnalysis.totalFiles}
Total Directories {projectAnalysis.totalDirectories}
Files by Extension
{Object.entries(projectAnalysis.filesByExtension) .sort((a: [string, number], b: [string, number]) => b[1] - a[1]) .slice(0, 15) .map(([ext, count]: [string, number]) => (
{ext.startsWith('(') ? ext : `.${ext}`} {count}
))}
{/* Generate Spec Card */} Generate Specification Create app_spec.txt from analysis

Generate a project specification file based on the analyzed codebase structure and detected technologies.

{specGenerated && (
app_spec.txt created successfully!
)} {specError && (
{specError}
)}
{/* Generate Feature List Card */} Generate Feature List Create features from analysis

Automatically detect and generate a feature list based on the analyzed codebase structure, dependencies, and project configuration.

{featureListGenerated && (
Features created successfully!
)} {featureListError && (
{featureListError}
)}
{/* File Tree */} File Tree {projectAnalysis.totalFiles} files in {projectAnalysis.totalDirectories}{' '} directories
{projectAnalysis.fileTree.map((node: FileTreeNode) => renderNode(node))}
) : null}
); }