/** * GitHub Repository Analyzer * Fetches and analyzes repository structure and key files for AI context */ import { getAdminDb } from '@/lib/firebase/admin'; interface RepositoryAnalysis { repoFullName: string; totalFiles: number; fileStructure: { directories: string[]; keyFiles: string[]; }; readme: string | null; packageJson: Record | null; techStack: string[]; summary: string; } /** * Analyze a GitHub repository to extract key information for AI context */ export async function analyzeGitHubRepository( userId: string, repoFullName: string, branch = 'main' ): Promise { try { const adminDb = getAdminDb(); // Get GitHub access token const connectionDoc = await adminDb .collection('githubConnections') .doc(userId) .get(); if (!connectionDoc.exists) { console.log('[GitHub Analyzer] No GitHub connection found'); return null; } const connection = connectionDoc.data()!; const accessToken = connection.accessToken; const [owner, repo] = repoFullName.split('/'); // Fetch repository tree const treeResponse = await fetch( `https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`, { headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/vnd.github.v3+json', }, } ); if (!treeResponse.ok) { console.error('[GitHub Analyzer] Failed to fetch tree:', treeResponse.statusText); return null; } const treeData = await treeResponse.json(); // Extract directories and key files const directories = new Set(); const keyFiles: string[] = []; let totalFiles = 0; treeData.tree?.forEach((item: { path: string; type: string }) => { if (item.type === 'blob') { totalFiles++; // Track key files const fileName = item.path.toLowerCase(); if ( fileName === 'readme.md' || fileName === 'package.json' || fileName === 'requirements.txt' || fileName === 'cargo.toml' || fileName === 'go.mod' || fileName === 'pom.xml' || fileName.startsWith('dockerfile') ) { keyFiles.push(item.path); } } // Track top-level directories const parts = item.path.split('/'); if (parts.length > 1) { directories.add(parts[0]); } }); // Fetch README content (truncate to first 3000 chars to avoid bloating prompts) let readme: string | null = null; const readmePath = keyFiles.find(f => f.toLowerCase().endsWith('readme.md')); if (readmePath) { const fullReadme = await fetchFileContent(accessToken, owner, repo, readmePath, branch); if (fullReadme) { // Truncate to first 3000 characters (roughly 750 tokens) readme = fullReadme.length > 3000 ? fullReadme.substring(0, 3000) + '\n\n[... README truncated for brevity ...]' : fullReadme; } } // Fetch package.json content let packageJson: Record | null = null; const packageJsonPath = keyFiles.find(f => f.toLowerCase().endsWith('package.json')); if (packageJsonPath) { const content = await fetchFileContent(accessToken, owner, repo, packageJsonPath, branch); if (content) { try { packageJson = JSON.parse(content); } catch (e) { console.error('[GitHub Analyzer] Failed to parse package.json'); } } } // Detect tech stack const techStack = detectTechStack(keyFiles, Array.from(directories), packageJson); // Generate summary const summary = generateRepositorySummary({ repoFullName, totalFiles, directories: Array.from(directories), keyFiles, techStack, readme, packageJson, }); return { repoFullName, totalFiles, fileStructure: { directories: Array.from(directories).slice(0, 20), // Limit to top 20 keyFiles, }, readme: readme ? readme.substring(0, 2000) : null, // First 2000 chars packageJson, techStack, summary, }; } catch (error) { console.error('[GitHub Analyzer] Error analyzing repository:', error); return null; } } /** * Fetch file content from GitHub */ async function fetchFileContent( accessToken: string, owner: string, repo: string, path: string, branch: string ): Promise { try { const response = await fetch( `https://api.github.com/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}?ref=${branch}`, { headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/vnd.github.v3+json', }, } ); if (!response.ok) return null; const data = await response.json(); return Buffer.from(data.content, 'base64').toString('utf-8'); } catch (error) { console.error(`[GitHub Analyzer] Failed to fetch ${path}:`, error); return null; } } /** * Detect tech stack from repository structure */ function detectTechStack( keyFiles: string[], directories: string[], packageJson: Record | null ): string[] { const stack: string[] = []; // From key files if (keyFiles.some(f => f.toLowerCase().includes('package.json'))) { stack.push('Node.js/JavaScript'); if (packageJson) { const deps = { ...(packageJson.dependencies as Record || {}), ...(packageJson.devDependencies as Record || {}) }; if (deps.next) stack.push('Next.js'); if (deps.react) stack.push('React'); if (deps.vue) stack.push('Vue'); if (deps.express) stack.push('Express'); if (deps.typescript) stack.push('TypeScript'); } } if (keyFiles.some(f => f.toLowerCase().includes('requirements.txt') || f.toLowerCase().includes('pyproject.toml'))) { stack.push('Python'); } if (keyFiles.some(f => f.toLowerCase().includes('cargo.toml'))) { stack.push('Rust'); } if (keyFiles.some(f => f.toLowerCase().includes('go.mod'))) { stack.push('Go'); } if (keyFiles.some(f => f.toLowerCase().includes('pom.xml') || f.toLowerCase().includes('build.gradle'))) { stack.push('Java'); } if (keyFiles.some(f => f.toLowerCase().startsWith('dockerfile'))) { stack.push('Docker'); } // From directories if (directories.includes('.github')) stack.push('GitHub Actions'); if (directories.includes('terraform') || directories.includes('infrastructure')) { stack.push('Infrastructure as Code'); } return stack; } /** * Generate a human-readable summary */ function generateRepositorySummary(analysis: { repoFullName: string; totalFiles: number; directories: string[]; keyFiles: string[]; techStack: string[]; readme: string | null; packageJson: Record | null; }): string { const parts: string[] = []; parts.push(`## Repository Analysis: ${analysis.repoFullName}`); parts.push(`\n**Structure:**`); parts.push(`- Total files: ${analysis.totalFiles}`); if (analysis.directories.length > 0) { parts.push(`- Main directories: ${analysis.directories.slice(0, 15).join(', ')}`); } if (analysis.techStack.length > 0) { parts.push(`\n**Tech Stack:** ${analysis.techStack.join(', ')}`); } if (analysis.packageJson) { const pkg = analysis.packageJson; parts.push(`\n**Package Info:**`); if (pkg.name) parts.push(`- Name: ${pkg.name}`); if (pkg.description) parts.push(`- Description: ${pkg.description}`); if (pkg.version) parts.push(`- Version: ${pkg.version}`); // Show key dependencies const deps = pkg.dependencies as Record || {}; const devDeps = pkg.devDependencies as Record || {}; const allDeps = { ...deps, ...devDeps }; const keyDeps = Object.keys(allDeps).slice(0, 10); if (keyDeps.length > 0) { parts.push(`- Key dependencies: ${keyDeps.join(', ')}`); } } if (analysis.readme) { parts.push(`\n**README Content:**`); // Get first few paragraphs or up to 1000 chars const readmeExcerpt = analysis.readme.substring(0, 1000); parts.push(readmeExcerpt); if (analysis.readme.length > 1000) { parts.push('...(truncated)'); } } return parts.join('\n'); }