VIBN Frontend for Coolify deployment

This commit is contained in:
2026-02-15 19:25:52 -08:00
commit 40bf8428cd
398 changed files with 76513 additions and 0 deletions

View File

@@ -0,0 +1,298 @@
/**
* 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<string, unknown> | 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<RepositoryAnalysis | null> {
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<string>();
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<string, unknown> | 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<string | null> {
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<string, unknown> | 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<string, unknown> || {}),
...(packageJson.devDependencies as Record<string, unknown> || {})
};
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<string, unknown> | 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<string, string> || {};
const devDeps = pkg.devDependencies as Record<string, string> || {};
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');
}