// Updated routes/github-integration.js const express = require('express'); const router = express.Router(); const GitHubIntegrationService = require('../services/github-integration.service'); const GitHubOAuthService = require('../services/github-oauth'); const FileStorageService = require('../services/file-storage.service'); const database = require('../config/database'); const fs = require('fs'); const path = require('path'); const githubService = new GitHubIntegrationService(); const oauthService = new GitHubOAuthService(); const fileStorageService = new FileStorageService(); // Attach GitHub repository to template router.post('/attach-repository', async (req, res) => { try { const { template_id, repository_url, branch_name } = req.body; // Validate input if (!template_id || !repository_url) { return res.status(400).json({ success: false, message: 'Template ID and repository URL are required' }); } // Check if template exists const templateQuery = 'SELECT * FROM templates WHERE id = $1 AND is_active = true'; const templateResult = await database.query(templateQuery, [template_id]); if (templateResult.rows.length === 0) { return res.status(404).json({ success: false, message: 'Template not found' }); } // Parse GitHub URL const { owner, repo, branch } = githubService.parseGitHubUrl(repository_url); // Check repository access const accessCheck = await githubService.checkRepositoryAccess(owner, repo); if (!accessCheck.hasAccess) { if (accessCheck.requiresAuth) { // Check if we have OAuth token const tokenRecord = await oauthService.getToken(); if (!tokenRecord) { return res.status(401).json({ success: false, message: 'GitHub authentication required for this repository', requires_auth: true, auth_url: `/api/github/auth/github` }); } } return res.status(404).json({ success: false, message: accessCheck.error || 'Repository not accessible' }); } // Get repository information from GitHub const repositoryData = await githubService.fetchRepositoryMetadata(owner, repo); // Analyze the codebase const codebaseAnalysis = await githubService.analyzeCodebase(owner, repo, branch || branch_name); // Store everything in PostgreSQL const insertQuery = ` INSERT INTO github_repositories ( template_id, repository_url, repository_name, owner_name, branch_name, is_public, metadata, codebase_analysis, sync_status, requires_auth ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING * `; const insertValues = [ template_id, repository_url, repo, owner, branch || branch_name || 'main', repositoryData.visibility === 'public', JSON.stringify(repositoryData), JSON.stringify(codebaseAnalysis), 'synced', accessCheck.requiresAuth ]; const insertResult = await database.query(insertQuery, insertValues); const repositoryRecord = insertResult.rows[0]; // Download repository with file storage console.log('Downloading repository with storage...'); const downloadResult = await githubService.downloadRepositoryWithStorage( owner, repo, branch || branch_name || 'main', repositoryRecord.id ); if (!downloadResult.success) { // If download failed, still return the repository record but mark the storage issue console.warn('Repository download failed:', downloadResult.error); } // Create feature-codebase mappings const featureQuery = 'SELECT id FROM template_features WHERE template_id = $1'; const featureResult = await database.query(featureQuery, [template_id]); if (featureResult.rows.length > 0) { const mappingValues = []; const mappingParams = []; let paramIndex = 1; for (const feature of featureResult.rows) { mappingValues.push(`(uuid_generate_v4(), $${paramIndex++}, $${paramIndex++}, $${paramIndex++}, $${paramIndex++})`); mappingParams.push( feature.id, repositoryRecord.id, '[]', // Empty paths for now '{}' // Empty snippets for now ); } const mappingQuery = ` INSERT INTO feature_codebase_mappings (id, feature_id, repository_id, code_paths, code_snippets) VALUES ${mappingValues.join(', ')} `; await database.query(mappingQuery, mappingParams); } // Get storage information const storageInfo = await githubService.getRepositoryStorage(repositoryRecord.id); res.status(201).json({ success: true, message: 'Repository attached successfully', data: { repository_id: repositoryRecord.id, template_id: repositoryRecord.template_id, repository_name: repositoryRecord.repository_name, owner_name: repositoryRecord.owner_name, branch_name: repositoryRecord.branch_name, is_public: repositoryRecord.is_public, requires_auth: repositoryRecord.requires_auth, metadata: repositoryData, codebase_analysis: codebaseAnalysis, storage_info: storageInfo, download_result: downloadResult } }); } catch (error) { console.error('Error attaching repository:', error); res.status(500).json({ success: false, message: error.message || 'Failed to attach repository' }); } }); // Get repository information for a template router.get('/template/:id/repository', async (req, res) => { try { const { id } = req.params; const query = ` SELECT gr.*, rs.local_path, rs.storage_status, rs.total_files_count, rs.total_directories_count, rs.total_size_bytes, rs.download_completed_at FROM github_repositories gr LEFT JOIN repository_storage rs ON gr.id = rs.repository_id WHERE gr.template_id = $1 ORDER BY gr.created_at DESC LIMIT 1 `; const result = await database.query(query, [id]); if (result.rows.length === 0) { return res.status(404).json({ success: false, message: 'No repository found for this template' }); } const repository = result.rows[0]; const parseMaybe = (v) => { if (v == null) return {}; if (typeof v === 'string') { try { return JSON.parse(v); } catch { return {}; } } return v; // already an object from jsonb }; res.json({ success: true, data: { ...repository, metadata: parseMaybe(repository.metadata), codebase_analysis: parseMaybe(repository.codebase_analysis) } }); } catch (error) { console.error('Error fetching repository:', error); res.status(500).json({ success: false, message: error.message || 'Failed to fetch repository' }); } }); // Get repository file structure router.get('/repository/:id/structure', async (req, res) => { try { const { id } = req.params; const { path: directoryPath } = req.query; // Get repository info const repoQuery = 'SELECT * FROM github_repositories WHERE id = $1'; const repoResult = await database.query(repoQuery, [id]); if (repoResult.rows.length === 0) { return res.status(404).json({ success: false, message: 'Repository not found' }); } const structure = await fileStorageService.getRepositoryStructure(id, directoryPath); res.json({ success: true, data: { repository_id: id, directory_path: directoryPath || '', structure: structure } }); } catch (error) { console.error('Error fetching repository structure:', error); res.status(500).json({ success: false, message: error.message || 'Failed to fetch repository structure' }); } }); // Get files in a directory router.get('/repository/:id/files', async (req, res) => { try { const { id } = req.params; const { directory_path = '' } = req.query; // Get repository info const repoQuery = 'SELECT * FROM github_repositories WHERE id = $1'; const repoResult = await database.query(repoQuery, [id]); if (repoResult.rows.length === 0) { return res.status(404).json({ success: false, message: 'Repository not found' }); } const files = await fileStorageService.getDirectoryFiles(id, directory_path); res.json({ success: true, data: { repository_id: id, directory_path: directory_path, files: files } }); } catch (error) { console.error('Error fetching directory files:', error); res.status(500).json({ success: false, message: error.message || 'Failed to fetch directory files' }); } }); // Get file content router.get('/repository/:id/file-content', async (req, res) => { try { const { id } = req.params; const { file_path } = req.query; if (!file_path) { return res.status(400).json({ success: false, message: 'File path is required' }); } const query = ` SELECT rf.*, rfc.content_text, rfc.content_preview, rfc.language_detected, rfc.line_count, rfc.char_count FROM repository_files rf LEFT JOIN repository_file_contents rfc ON rf.id = rfc.file_id WHERE rf.repository_id = $1 AND rf.relative_path = $2 `; const result = await database.query(query, [id, file_path]); if (result.rows.length === 0) { return res.status(404).json({ success: false, message: 'File not found' }); } const file = result.rows[0]; res.json({ success: true, data: { file_info: { id: file.id, filename: file.filename, file_extension: file.file_extension, relative_path: file.relative_path, file_size_bytes: file.file_size_bytes, mime_type: file.mime_type, is_binary: file.is_binary, language_detected: file.language_detected, line_count: file.line_count, char_count: file.char_count }, content: file.is_binary ? null : file.content_text, preview: file.content_preview } }); } catch (error) { console.error('Error fetching file content:', error); res.status(500).json({ success: false, message: error.message || 'Failed to fetch file content' }); } }); // Search repository files router.get('/repository/:id/search', async (req, res) => { try { const { id } = req.params; const { q: query } = req.query; if (!query) { return res.status(400).json({ success: false, message: 'Search query is required' }); } const results = await fileStorageService.searchFileContent(id, query); res.json({ success: true, data: { repository_id: id, search_query: query, results: results, total_results: results.length } }); } catch (error) { console.error('Error searching repository:', error); res.status(500).json({ success: false, message: error.message || 'Failed to search repository' }); } }); // List all repositories for a template router.get('/template/:id/repositories', async (req, res) => { try { const { id } = req.params; const query = ` SELECT gr.*, rs.local_path, rs.storage_status, rs.total_files_count, rs.total_directories_count, rs.total_size_bytes, rs.download_completed_at FROM github_repositories gr LEFT JOIN repository_storage rs ON gr.id = rs.repository_id WHERE gr.template_id = $1 ORDER BY gr.created_at DESC `; const result = await database.query(query, [id]); const repositories = result.rows.map(repo => ({ ...repo, metadata: JSON.parse(repo.metadata || '{}'), codebase_analysis: JSON.parse(repo.codebase_analysis || '{}') })); res.json({ success: true, data: repositories }); } catch (error) { console.error('Error fetching repositories:', error); res.status(500).json({ success: false, message: error.message || 'Failed to fetch repositories' }); } }); // Download repository files (legacy endpoint for backward compatibility) router.post('/download', async (req, res) => { try { const { repository_url, branch_name } = req.body; if (!repository_url) { return res.status(400).json({ success: false, message: 'Repository URL is required' }); } const { owner, repo, branch } = githubService.parseGitHubUrl(repository_url); const targetBranch = branch || branch_name || 'main'; const result = await githubService.downloadRepository(owner, repo, targetBranch); if (result.success) { res.json({ success: true, message: 'Repository downloaded successfully', data: result }); } else { res.status(500).json({ success: false, message: 'Failed to download repository', error: result.error }); } } catch (error) { console.error('Error downloading repository:', error); res.status(500).json({ success: false, message: error.message || 'Failed to download repository' }); } }); // Re-sync repository (re-download and update database) router.post('/repository/:id/sync', async (req, res) => { try { const { id } = req.params; // Get repository info const repoQuery = 'SELECT * FROM github_repositories WHERE id = $1'; const repoResult = await database.query(repoQuery, [id]); if (repoResult.rows.length === 0) { return res.status(404).json({ success: false, message: 'Repository not found' }); } const repository = repoResult.rows[0]; const { owner, repo, branch } = githubService.parseGitHubUrl(repository.repository_url); // Clean up existing storage await githubService.cleanupRepositoryStorage(id); // Re-download with storage const downloadResult = await githubService.downloadRepositoryWithStorage( owner, repo, branch || repository.branch_name, id ); // Update sync status await database.query( 'UPDATE github_repositories SET sync_status = $1, updated_at = NOW() WHERE id = $2', [downloadResult.success ? 'synced' : 'error', id] ); res.json({ success: downloadResult.success, message: downloadResult.success ? 'Repository synced successfully' : 'Failed to sync repository', data: downloadResult }); } catch (error) { console.error('Error syncing repository:', error); res.status(500).json({ success: false, message: error.message || 'Failed to sync repository' }); } }); // Remove repository from template router.delete('/repository/:id', async (req, res) => { try { const { id } = req.params; // Get repository info before deletion const getQuery = 'SELECT * FROM github_repositories WHERE id = $1'; const getResult = await database.query(getQuery, [id]); if (getResult.rows.length === 0) { return res.status(404).json({ success: false, message: 'Repository not found' }); } const repository = getResult.rows[0]; // Clean up file storage await githubService.cleanupRepositoryStorage(id); // Delete feature mappings first await database.query( 'DELETE FROM feature_codebase_mappings WHERE repository_id = $1', [id] ); // Delete repository record await database.query( 'DELETE FROM github_repositories WHERE id = $1', [id] ); res.json({ success: true, message: 'Repository removed successfully', data: { removed_repository: repository.repository_name, template_id: repository.template_id } }); } catch (error) { console.error('Error removing repository:', error); res.status(500).json({ success: false, message: error.message || 'Failed to remove repository' }); } }); module.exports = router;