import type { GitHubUserResponse, GitHubRepoInfo, GitHubBranch, GitHubOrganization, GitHubStats, GitHubLanguageStats, } from '~/types/GitHub'; export interface GitHubApiServiceConfig { token?: string; tokenType?: 'classic' | 'fine-grained'; baseURL?: string; } export interface DetailedRepoInfo extends GitHubRepoInfo { branches_count?: number; contributors_count?: number; issues_count?: number; pull_requests_count?: number; } export interface GitHubApiError { message: string; status: number; code?: string; } export class GitHubApiServiceClass { private _config: GitHubApiServiceConfig; private _baseURL: string; constructor(config: GitHubApiServiceConfig = {}) { this._config = config; this._baseURL = config.baseURL || 'https://api.github.com'; } /** * Configure the service with authentication details */ configure(config: GitHubApiServiceConfig): void { this._config = { ...this._config, ...config }; this._baseURL = config.baseURL || this._baseURL; } private async _makeRequestInternal(endpoint: string, options: RequestInit = {}): Promise { if (!this._config.token) { throw new Error('GitHub token is required. Call configure() first.'); } const response = await fetch(`${this._baseURL}${endpoint}`, { headers: { Accept: 'application/vnd.github.v3+json', Authorization: `${this._config.tokenType === 'classic' ? 'token' : 'Bearer'} ${this._config.token}`, 'User-Agent': 'Bolt.diy', ...options.headers, }, ...options, }); if (!response.ok) { const errorData: any = await response.json().catch(() => ({ message: response.statusText })); const error: GitHubApiError = { message: errorData.message || response.statusText, status: response.status, code: errorData.code, }; throw error; } return response.json(); } /** * Fetch all user repositories with pagination */ async getAuthenticatedUser(): Promise { return this._makeRequestInternal('/user'); } async getAllUserRepositories(): Promise { const allRepos: GitHubRepoInfo[] = []; let page = 1; let hasMore = true; while (hasMore) { const repos = await this._makeRequestInternal( `/user/repos?per_page=100&page=${page}&sort=updated`, ); allRepos.push(...repos); hasMore = repos.length === 100; // If we got 100 repos, there might be more page++; } return allRepos; } /** * Fetch detailed information for a repository including additional metrics */ async getDetailedRepositoryInfo(owner: string, repo: string): Promise { const [repoInfo, branches] = await Promise.all([ this._makeRequestInternal(`/repos/${owner}/${repo}`), this.getRepositoryBranches(owner, repo).catch(() => []), ]); // Try to get additional metrics const [contributors, issues, pullRequests] = await Promise.allSettled([ this._getRepositoryContributorsCount(owner, repo), this._getRepositoryIssuesCount(owner, repo), this._getRepositoryPullRequestsCount(owner, repo), ]); const detailedInfo: DetailedRepoInfo = { ...repoInfo, branches_count: branches.length, contributors_count: contributors.status === 'fulfilled' ? contributors.value : undefined, issues_count: issues.status === 'fulfilled' ? issues.value : undefined, pull_requests_count: pullRequests.status === 'fulfilled' ? pullRequests.value : undefined, }; return detailedInfo; } /** * Get repository branches */ async getRepositoryBranches(owner: string, repo: string): Promise { return this._makeRequestInternal(`/repos/${owner}/${repo}/branches`); } /** * Get contributors count using Link header pagination info */ private async _getRepositoryContributorsCount(owner: string, repo: string): Promise { const response = await fetch(`${this._baseURL}/repos/${owner}/${repo}/contributors?per_page=1`, { headers: { Accept: 'application/vnd.github.v3+json', Authorization: `${this._config.tokenType === 'classic' ? 'token' : 'Bearer'} ${this._config.token}`, 'User-Agent': 'Bolt.diy', }, }); if (!response.ok) { return 0; } const linkHeader = response.headers.get('Link'); if (linkHeader) { const match = linkHeader.match(/page=(\d+)>; rel="last"/); return match ? parseInt(match[1], 10) : 1; } const data = await response.json(); return Array.isArray(data) ? data.length : 0; } /** * Get issues count using Link header pagination info */ private async _getRepositoryIssuesCount(owner: string, repo: string): Promise { const response = await fetch(`${this._baseURL}/repos/${owner}/${repo}/issues?state=all&per_page=1`, { headers: { Accept: 'application/vnd.github.v3+json', Authorization: `${this._config.tokenType === 'classic' ? 'token' : 'Bearer'} ${this._config.token}`, 'User-Agent': 'Bolt.diy', }, }); if (!response.ok) { return 0; } const linkHeader = response.headers.get('Link'); if (linkHeader) { const match = linkHeader.match(/page=(\d+)>; rel="last"/); return match ? parseInt(match[1], 10) : 1; } const data = await response.json(); return Array.isArray(data) ? data.length : 0; } /** * Get pull requests count using Link header pagination info */ private async _getRepositoryPullRequestsCount(owner: string, repo: string): Promise { const response = await fetch(`${this._baseURL}/repos/${owner}/${repo}/pulls?state=all&per_page=1`, { headers: { Accept: 'application/vnd.github.v3+json', Authorization: `${this._config.tokenType === 'classic' ? 'token' : 'Bearer'} ${this._config.token}`, 'User-Agent': 'Bolt.diy', }, }); if (!response.ok) { return 0; } const linkHeader = response.headers.get('Link'); if (linkHeader) { const match = linkHeader.match(/page=(\d+)>; rel="last"/); return match ? parseInt(match[1], 10) : 1; } const data = await response.json(); return Array.isArray(data) ? data.length : 0; } /** * Fetch detailed information for multiple repositories in batches */ async getDetailedRepositoriesInfo( repos: GitHubRepoInfo[], batchSize: number = 5, delayMs: number = 100, ): Promise { const detailedRepos: DetailedRepoInfo[] = []; for (let i = 0; i < repos.length; i += batchSize) { const batch = repos.slice(i, i + batchSize); const batchResults = await Promise.allSettled( batch.map((repo) => { const [owner, repoName] = repo.full_name.split('/'); return this.getDetailedRepositoryInfo(owner, repoName); }), ); // Collect successful results batchResults.forEach((result, index) => { if (result.status === 'fulfilled') { detailedRepos.push(result.value); } else { console.error(`Failed to fetch details for ${batch[index].full_name}:`, result.reason); // Fallback to original repo data detailedRepos.push(batch[index]); } }); // Add delay between batches to be respectful to the API if (i + batchSize < repos.length) { await new Promise((resolve) => setTimeout(resolve, delayMs)); } } return detailedRepos; } /** * Calculate comprehensive statistics from repositories */ calculateRepositoryStats(repos: DetailedRepoInfo[]): { languages: GitHubLanguageStats; mostUsedLanguages: Array<{ language: string; bytes: number; repos: number }>; totalBranches: number; totalContributors: number; totalIssues: number; totalPullRequests: number; repositoryHealth: { healthy: number; active: number; archived: number; forked: number; }; } { const languages: GitHubLanguageStats = {}; const languageBytes: Record = {}; const languageRepos: Record = {}; let totalBranches = 0; let totalContributors = 0; let totalIssues = 0; let totalPullRequests = 0; let healthyRepos = 0; let activeRepos = 0; let archivedRepos = 0; let forkedRepos = 0; repos.forEach((repo) => { // Language statistics if (repo.language) { languages[repo.language] = (languages[repo.language] || 0) + 1; languageBytes[repo.language] = (languageBytes[repo.language] || 0) + (repo.size || 0); languageRepos[repo.language] = (languageRepos[repo.language] || 0) + 1; } // Aggregate metrics totalBranches += repo.branches_count || 0; totalContributors += repo.contributors_count || 0; totalIssues += repo.issues_count || 0; totalPullRequests += repo.pull_requests_count || 0; // Repository health analysis const daysSinceUpdate = Math.floor((Date.now() - new Date(repo.updated_at).getTime()) / (1000 * 60 * 60 * 24)); if (repo.archived) { archivedRepos++; } else if (repo.fork) { forkedRepos++; } else if (daysSinceUpdate < 7) { activeRepos++; } else if (daysSinceUpdate < 30 && repo.stargazers_count > 0) { healthyRepos++; } }); // Create most used languages array sorted by bytes const mostUsedLanguages = Object.entries(languageBytes) .map(([language, bytes]) => ({ language, bytes, repos: languageRepos[language] || 0, })) .sort((a, b) => b.bytes - a.bytes) .slice(0, 20); return { languages, mostUsedLanguages, totalBranches, totalContributors, totalIssues, totalPullRequests, repositoryHealth: { healthy: healthyRepos, active: activeRepos, archived: archivedRepos, forked: forkedRepos, }, }; } /** * Generate comprehensive GitHub stats for a user */ async generateComprehensiveStats(userData: GitHubUserResponse): Promise { try { // Fetch all repositories const allRepos = await this.getAllUserRepositories(); // Get detailed information for repositories (in batches) const detailedRepos = await this.getDetailedRepositoriesInfo(allRepos); // Calculate statistics const stats = this.calculateRepositoryStats(detailedRepos); // Fetch additional data in parallel const [organizations, recentActivity] = await Promise.allSettled([ this._makeRequestInternal('/user/orgs'), this._makeRequestInternal(`/users/${userData.login}/events?per_page=10`), ]); // Calculate aggregated metrics const totalStars = detailedRepos.reduce((sum, repo) => sum + repo.stargazers_count, 0); const totalForks = detailedRepos.reduce((sum, repo) => sum + repo.forks_count, 0); const privateRepos = detailedRepos.filter((repo) => repo.private).length; const githubStats: GitHubStats = { repos: detailedRepos, recentActivity: recentActivity.status === 'fulfilled' ? recentActivity.value.slice(0, 10).map((event: any) => ({ id: event.id, type: event.type, repo: { name: event.repo.name, url: event.repo.url }, created_at: event.created_at, payload: event.payload || {}, })) : [], languages: stats.languages, totalGists: userData.public_gists || 0, publicRepos: userData.public_repos || 0, privateRepos, stars: totalStars, forks: totalForks, followers: userData.followers || 0, publicGists: userData.public_gists || 0, privateGists: 0, // This would need additional API call lastUpdated: new Date().toISOString(), totalStars, totalForks, organizations: organizations.status === 'fulfilled' ? organizations.value : [], totalBranches: stats.totalBranches, totalContributors: stats.totalContributors, totalIssues: stats.totalIssues, totalPullRequests: stats.totalPullRequests, mostUsedLanguages: stats.mostUsedLanguages, }; return githubStats; } catch (error) { console.error('Error generating comprehensive stats:', error); throw error; } } /** * Fetch authenticated user and rate limit info */ async fetchUser( token: string, tokenType: 'classic' | 'fine-grained' = 'classic', ): Promise<{ user: GitHubUserResponse; rateLimit: any }> { this.configure({ token, tokenType }); const [user, rateLimit] = await Promise.all([ this.getAuthenticatedUser(), this._makeRequestInternal('/rate_limit'), ]); return { user, rateLimit }; } /** * Fetch comprehensive GitHub stats for authenticated user */ async fetchStats(token: string, tokenType: 'classic' | 'fine-grained' = 'classic'): Promise { this.configure({ token, tokenType }); const user = await this.getAuthenticatedUser(); return this.generateComprehensiveStats(user); } /** * Clear all cached data */ clearCache(): void { // This is a placeholder - implement caching if needed } /** * Clear user-specific cache */ clearUserCache(_token: string): void { // This is a placeholder - implement user-specific caching if needed } } // Export an instance of the service export const gitHubApiService = new GitHubApiServiceClass();