diff --git a/package-lock.json b/package-lock.json index ddc6691..2fd9129 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3708,7 +3708,7 @@ "version": "19.1.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz", "integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -3718,7 +3718,7 @@ "version": "19.1.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -4896,7 +4896,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/d3-array": { @@ -8393,7 +8393,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/react-redux": { diff --git a/src/app/vcs/repos/page.tsx b/src/app/vcs/repos/page.tsx new file mode 100644 index 0000000..45f2a1d --- /dev/null +++ b/src/app/vcs/repos/page.tsx @@ -0,0 +1,422 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { + Github, + Gitlab, + FolderOpen, + Search, + RefreshCw, + ExternalLink, + GitBranch, + Star, + Eye, + Code, + Calendar, + GitCompare, + Brain, + GitCommit, + Server +} from 'lucide-react'; +import { authApiClient } from '@/components/apis/authApiClients'; +import Link from 'next/link'; + +interface VcsRepoSummary { + id: string; + name: string; + full_name: string; + description?: string; + language?: string; + visibility: 'public' | 'private'; + provider: 'github' | 'gitlab' | 'bitbucket' | 'gitea'; + html_url: string; + clone_url: string; + default_branch: string; + stargazers_count?: number; + watchers_count?: number; + forks_count?: number; + updated_at: string; + created_at: string; +} + +const VcsReposPage: React.FC = () => { + const [repositories, setRepositories] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(''); + const [filter, setFilter] = useState<'all' | 'public' | 'private'>('all'); + const [providerFilter, setProviderFilter] = useState<'all' | 'github' | 'gitlab' | 'bitbucket' | 'gitea'>('all'); + const [aiAnalysisLoading, setAiAnalysisLoading] = useState(null); + const [aiAnalysisError, setAiAnalysisError] = useState(null); + + // Handle OAuth callback for all providers + useEffect(() => { + const handleVcsCallback = async () => { + const urlParams = new URLSearchParams(window.location.search); + const code = urlParams.get('code'); + const state = urlParams.get('state'); + const error = urlParams.get('error'); + + if (error) { + console.error('VCS OAuth error:', error); + alert(`OAuth error: ${error}`); + return; + } + + if (code && state) { + console.log('🔐 [VCS OAuth] Callback received, processing...'); + + try { + // Check if we have a pending repository attachment + const pendingAttach = sessionStorage.getItem('pending_git_attach'); + if (pendingAttach) { + const { repository_url, branch_name, provider } = JSON.parse(pendingAttach); + console.log(`🔐 [VCS OAuth] Resuming repository attachment:`, { repository_url, branch_name, provider }); + + // Clear the pending attach + sessionStorage.removeItem('pending_git_attach'); + + // Now attach the repository + try { + const response = await authApiClient.post(`/api/vcs/${provider}/attach-repository`, { + repository_url, + branch_name: branch_name || undefined, + }); + + alert(`${provider?.toUpperCase()} account connected and repository attached successfully!`); + + // Clean up URL parameters + const newUrl = window.location.pathname; + window.history.replaceState({}, document.title, newUrl); + + // Refresh the page to show the attached repository + window.location.reload(); + } catch (attachError) { + console.error('Failed to attach repository after OAuth:', attachError); + alert(`${provider?.toUpperCase()} account connected, but failed to attach repository. Please try again.`); + } + } else { + console.log('🔐 [VCS OAuth] No pending repository attachment, just connecting account'); + alert('Account connected successfully!'); + + // Clean up URL parameters + const newUrl = window.location.pathname; + window.history.replaceState({}, document.title, newUrl); + + // Refresh repositories + loadRepositories(); + } + } catch (error) { + console.error('Error handling VCS OAuth callback:', error); + alert('Error processing OAuth callback'); + } + } + }; + + handleVcsCallback(); + }, []); + + // Load repositories from all providers + const loadRepositories = async () => { + try { + setIsLoading(true); + setError(null); + + const allRepos: VcsRepoSummary[] = []; + + // Try to load from each provider + const providers = ['github', 'gitlab', 'bitbucket', 'gitea']; + + for (const provider of providers) { + try { + const response = await authApiClient.get(`/api/vcs/${provider}/repositories`); + const repos = response.data?.data || []; + + // Add provider info to each repo + const reposWithProvider = repos.map((repo: any) => ({ + ...repo, + provider: provider + })); + + allRepos.push(...reposWithProvider); + } catch (err) { + console.log(`No ${provider} repositories or not connected:`, err); + } + } + + setRepositories(allRepos); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load repositories'); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + loadRepositories(); + }, []); + + const handleRefresh = async () => { + await loadRepositories(); + }; + + const handleAiAnalysis = async (repositoryId: string) => { + try { + setAiAnalysisLoading(repositoryId); + setAiAnalysisError(null); + + const response = await authApiClient.get(`/api/ai-stream/repository/${repositoryId}/ai-stream`); + const data = response.data; + + console.log('AI Analysis Result:', data); + + alert('AI Analysis completed successfully!'); + + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'AI Analysis failed'; + setAiAnalysisError(errorMessage); + console.error('AI Analysis Error:', err); + } finally { + setAiAnalysisLoading(null); + } + }; + + const getProviderIcon = (provider: string) => { + switch (provider) { + case 'github': return ; + case 'gitlab': return ; + case 'bitbucket': return ; + case 'gitea': return ; + default: return ; + } + }; + + const getProviderColor = (provider: string) => { + switch (provider) { + case 'github': return 'bg-gray-100 text-gray-800'; + case 'gitlab': return 'bg-orange-100 text-orange-800'; + case 'bitbucket': return 'bg-blue-100 text-blue-800'; + case 'gitea': return 'bg-green-100 text-green-800'; + default: return 'bg-gray-100 text-gray-800'; + } + }; + + const filteredRepositories = repositories.filter(repo => { + const matchesSearch = repo.full_name?.toLowerCase().includes(searchQuery.toLowerCase()) || + repo.description?.toLowerCase().includes(searchQuery.toLowerCase()) || + repo.language?.toLowerCase().includes(searchQuery.toLowerCase()); + + const matchesFilter = filter === 'all' || + (filter === 'public' && repo.visibility === 'public') || + (filter === 'private' && repo.visibility === 'private'); + + const matchesProvider = providerFilter === 'all' || repo.provider === providerFilter; + + return matchesSearch && matchesFilter && matchesProvider; + }); + + return ( +
+
+

My Repositories

+

Manage and analyze your repositories from all connected providers

+
+ + {/* Search and Filters */} +
+
+
+
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+
+ +
+ +
+
+ + + +
+ +
+ + + + + +
+
+
+ + {/* Error State */} + {error && ( +
+

{error}

+
+ )} + + {/* Loading State */} + {isLoading && ( +
+ + Loading repositories... +
+ )} + + {/* Repositories Grid */} + {!isLoading && ( +
+ {filteredRepositories.map((repo) => ( + + +
+
+ {getProviderIcon(repo.provider)} + {repo.name} +
+ + {repo.visibility} + +
+
+ + {repo.provider.toUpperCase()} + +
+ {repo.description && ( +

{repo.description}

+ )} +
+ + +
+ {repo.language && ( +
+ + {repo.language} +
+ )} +
+ + {repo.default_branch} +
+
+ +
+
+ + +
+
+ + {aiAnalysisError && ( +

{aiAnalysisError}

+ )} +
+
+ ))} +
+ )} + + {/* Empty State */} + {!isLoading && filteredRepositories.length === 0 && ( +
+ +

No repositories found

+

+ {searchQuery || filter !== 'all' || providerFilter !== 'all' + ? 'Try adjusting your search or filters' + : 'Connect your accounts to see your repositories' + } +

+ + + +
+ )} +
+ ); +}; + +export default VcsReposPage; diff --git a/src/components/main-dashboard.tsx b/src/components/main-dashboard.tsx index 28fc369..985d060 100644 --- a/src/components/main-dashboard.tsx +++ b/src/components/main-dashboard.tsx @@ -8,7 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Badge } from "@/components/ui/badge" import { Checkbox } from "@/components/ui/checkbox" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog" -import { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers, AlertCircle, Edit, Trash2, User, Palette, GitBranch } from "lucide-react" +import { ArrowRight, Plus, Globe, BarChart3, Zap, Code, Search, Star, Clock, Users, Layers, AlertCircle, Edit, Trash2, User, Palette, GitBranch, FolderGit2 } from "lucide-react" import { useTemplates } from "@/hooks/useTemplates" import { CustomTemplateForm } from "@/components/custom-template-form" import { EditTemplateForm } from "@/components/edit-template-form" @@ -23,7 +23,8 @@ import PromptSidePanel from "@/components/prompt-side-panel" import { DualCanvasEditor } from "@/components/dual-canvas-editor" import { getAccessToken } from "@/components/apis/authApiClients" import TechStackSummary from "@/components/tech-stack-summary" -import { attachRepository, getGitHubAuthStatus, AttachRepositoryResponse, connectGitHubWithRepo, initiateGitHubOAuth } from "@/lib/api/github" +import { attachRepository, detectProvider, connectProvider, AttachRepositoryResponse } from "@/lib/api/vcs" +import { getGitHubAuthStatus } from "@/lib/api/github" import ViewUserReposButton from "@/components/github/ViewUserReposButton" import { ErrorBanner } from "@/components/ui/error-banner" @@ -126,7 +127,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi throw new Error('Repository attachment failed') } - const isPrivate = result?.data?.is_public === false || result?.data?.requires_auth === true + const isPrivate = result?.data?.is_public === false console.log('✅ Repository attached successfully:', result) const repoType = isPrivate ? 'private' : 'public' @@ -155,15 +156,18 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi }) if (status === 401 && (data?.requires_auth || data?.auth_url || data?.service_auth_url)) { - console.log('🔐 Private repository detected - initiating GitHub OAuth with repository context') + console.log('🔐 Private repository detected - initiating OAuth with repository context') // Reset loading state before redirect setIsGeneratingAuth(false) - // Use the new OAuth helper that will auto-attach the repo after authentication + // Detect provider from URL + const detectedProvider = detectProvider(gitUrl.trim()); + + // Use the universal OAuth helper that will auto-attach the repo after authentication setTimeout(() => { - connectGitHubWithRepo(gitUrl.trim(), gitBranch?.trim() || 'main').catch((oauthError) => { + connectProvider(detectedProvider, gitUrl.trim(), gitBranch?.trim() || 'main').catch((oauthError) => { console.error('OAuth initiation failed:', oauthError) - alert('Failed to initiate GitHub authentication. Please try again.') + alert(`Failed to initiate ${detectedProvider.toUpperCase()} authentication. Please try again.`) }) }, 100) return @@ -173,15 +177,18 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi // Reset loading state before showing dialog setIsGeneratingAuth(false) - // Repository not accessible with current GitHub account - prompt to re-authenticate + // Repository not accessible with current account - prompt to re-authenticate setTimeout(() => { - const confirmReauth = confirm('Repository not accessible with your current GitHub account.\n\nThis could mean:\n- The repository belongs to a different GitHub account\n- Your token expired or lacks permissions\n\nWould you like to re-authenticate with GitHub?') + // Detect provider from URL + const detectedProvider = detectProvider(gitUrl.trim()); + + const confirmReauth = confirm(`Repository not accessible with your current ${detectedProvider.toUpperCase()} account.\n\nThis could mean:\n- The repository belongs to a different ${detectedProvider.toUpperCase()} account\n- Your token expired or lacks permissions\n\nWould you like to re-authenticate with ${detectedProvider.toUpperCase()}?`) if (confirmReauth) { - console.log('🔐 Re-authenticating with GitHub for private repository access') - connectGitHubWithRepo(gitUrl.trim(), gitBranch?.trim() || 'main').catch((oauthError) => { + console.log(`🔐 Re-authenticating with ${detectedProvider.toUpperCase()} for private repository access`) + connectProvider(detectedProvider, gitUrl.trim(), gitBranch?.trim() || 'main').catch((oauthError) => { console.error('OAuth initiation failed:', oauthError) - alert('Failed to initiate GitHub authentication. Please try again.') + alert(`Failed to initiate ${detectedProvider.toUpperCase()} authentication. Please try again.`) }) } }, 100) @@ -205,21 +212,10 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi const handleOAuthAuth = async (provider: string) => { setAuthLoading(true) try { - const providerConfig = gitProviders[provider as keyof typeof gitProviders] - if (!providerConfig.oauthEndpoint) { - throw new Error('OAuth not supported for this provider') - } + console.log(`🔐 [handleOAuthAuth] Starting OAuth for provider: ${provider}`) - // For GitHub, use the new OAuth helper - if (provider === 'github') { - console.log('Initiating GitHub OAuth flow...') - initiateGitHubOAuth() - return - } - - // For other providers, use the old method - window.open(providerConfig.oauthEndpoint, '_blank', 'width=600,height=700') - alert(`Redirecting to ${providerConfig.name} OAuth...`) + // Use the new VCS API for all providers + await connectProvider(provider) } catch (error) { console.error('OAuth error:', error) alert('OAuth authentication failed') @@ -294,7 +290,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi icon: 'M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z', placeholder: 'https://github.com/org/repo.git', authMethods: ['token', 'ssh', 'oauth'], - oauthEndpoint: '/api/auth/github', + oauthEndpoint: '/api/vcs/github/auth/start', apiEndpoint: 'https://api.github.com' }, bitbucket: { @@ -302,7 +298,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi icon: 'M.778 1.213a.768.768 0 00-.768.892l3.263 19.81c.084.5.515.868 1.022.873H19.95a.772.772 0 00.77-.646l3.27-20.03a.768.768 0 00-.768-.891L.778 1.213zM14.518 18.197H9.482l-1.4-8.893h7.436l-1.4 8.893z', placeholder: 'https://bitbucket.org/org/repo.git', authMethods: ['username_password', 'app_password', 'oauth'], - oauthEndpoint: '/api/auth/bitbucket', + oauthEndpoint: '/api/vcs/bitbucket/auth/start', apiEndpoint: 'https://api.bitbucket.org/2.0' }, gitlab: { @@ -310,7 +306,7 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi icon: 'M23.6004 9.5927l-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.874.874 0 0 0-.9997.0539.874.874 0 0 0-.29.4399l-2.5465 7.7838H7.2162l-2.5465-7.7838a.857.857 0 0 0-.29-.4412.874.874 0 0 0-.9997-.0537.858.858 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.065 6.065 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.008 1.008 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7466.0125-.01a6.068 6.068 0 0 0 2.0094-7.003z', placeholder: 'https://gitlab.com/org/repo.git', authMethods: ['token', 'oauth', 'ssh'], - oauthEndpoint: '/api/auth/gitlab', + oauthEndpoint: '/api/vcs/gitlab/auth/start', apiEndpoint: 'https://gitlab.com/api/v4' }, other: { @@ -550,6 +546,12 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi alert('Please enter a Git repository URL'); return; } + + // Detect provider from URL + const detectedProvider = detectProvider(gitUrl.trim()); + console.log('🔍 [handleCreateFromGit] Detected provider:', detectedProvider); + console.log('🔍 [handleCreateFromGit] Git URL:', gitUrl.trim()); + // Attach the repository via backend (skip template creation) try { await attachRepository({ @@ -568,15 +570,15 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi // Return a mock template object to proceed to next step return { id: 'git-imported', - title: `Imported from ${gitProvider === 'other' ? 'Git' : gitProvider.charAt(0).toUpperCase() + gitProvider.slice(1)}`, - description: `Template imported from ${gitProvider === 'other' ? 'Git' : gitProvider.charAt(0).toUpperCase() + gitProvider.slice(1)}: ${gitUrl}`, + title: `Imported from ${detectedProvider.charAt(0).toUpperCase() + detectedProvider.slice(1)}`, + description: `Template imported from ${detectedProvider.charAt(0).toUpperCase() + detectedProvider.slice(1)}: ${gitUrl}`, type: 'custom', category: 'imported', is_custom: true, source: 'git', git_url: gitUrl.trim(), git_branch: gitBranch?.trim() || 'main', - git_provider: gitProvider + git_provider: detectedProvider } } catch (attachErr) { console.error('[TemplateSelectionStep] attachRepository failed:', attachErr) @@ -587,10 +589,10 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi console.log('🔍 HandleCreateFromGit error response:', { status, data }) - // If backend signals GitHub auth required, open the OAuth URL for this user - if (status === 401 && (data?.requires_auth || data?.message?.includes('authentication'))) { - const url: string = data?.auth_url - if (!url) { + // If backend signals auth required, redirect to provider OAuth + if ((status === 401 || status === 200) && (data?.requires_auth || data?.message?.includes('authentication'))) { + const authUrl: string = data?.auth_url + if (!authUrl) { alert('Authentication URL is missing.'); return } @@ -599,13 +601,15 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi try { sessionStorage.setItem('pending_git_attach', JSON.stringify({ repository_url: gitUrl.trim(), - branch_name: (gitBranch?.trim() || undefined) + branch_name: (gitBranch?.trim() || undefined), + provider: detectedProvider })) } catch {} - console.log('🔐 Redirecting to GitHub OAuth for repository attachment:', url) - // Force same-tab redirect directly to GitHub consent screen - window.location.replace(url) + console.log(`🔐 Redirecting to ${detectedProvider.toUpperCase()} OAuth for repository attachment:`, authUrl) + console.log(`🔐 Provider being passed to connectProvider:`, detectedProvider) + // Use connectProvider function to handle OAuth flow properly + await connectProvider(detectedProvider, gitUrl.trim(), gitBranch?.trim()) return } @@ -863,6 +867,12 @@ function TemplateSelectionStep({ onNext }: { onNext: (template: Template) => voi {/* Right-aligned quick navigation to user repos */}
+ + +
diff --git a/src/components/vcs/VcsConnectionButton.tsx b/src/components/vcs/VcsConnectionButton.tsx new file mode 100644 index 0000000..bc45153 --- /dev/null +++ b/src/components/vcs/VcsConnectionButton.tsx @@ -0,0 +1,63 @@ +"use client" + +import { Button } from "@/components/ui/button" +import { Github, Gitlab, GitCommit, Server } from "lucide-react" +import { connectProvider } from "@/lib/api/vcs" + +interface Props { + provider: 'github' | 'gitlab' | 'bitbucket' | 'gitea' + className?: string + size?: "default" | "sm" | "lg" | "icon" + label?: string + repoUrl?: string + branch?: string +} + +export default function VcsConnectionButton({ + provider, + className, + size = "default", + label, + repoUrl, + branch +}: Props) { + const getProviderIcon = () => { + switch (provider) { + case 'github': return ; + case 'gitlab': return ; + case 'bitbucket': return ; + case 'gitea': return ; + default: return ; + } + }; + + const getProviderColor = () => { + switch (provider) { + case 'github': return 'bg-gray-600 hover:bg-gray-700'; + case 'gitlab': return 'bg-orange-600 hover:bg-orange-700'; + case 'bitbucket': return 'bg-blue-600 hover:bg-blue-700'; + case 'gitea': return 'bg-green-600 hover:bg-green-700'; + default: return 'bg-gray-600 hover:bg-gray-700'; + } + }; + + const handleConnect = async () => { + try { + await connectProvider(provider, repoUrl, branch); + } catch (error) { + console.error(`Failed to connect ${provider}:`, error); + alert(`Failed to connect ${provider.toUpperCase()} account. Please try again.`); + } + }; + + return ( + + ) +} diff --git a/src/lib/api/vcs.ts b/src/lib/api/vcs.ts new file mode 100644 index 0000000..091d07c --- /dev/null +++ b/src/lib/api/vcs.ts @@ -0,0 +1,239 @@ +import { authApiClient } from '@/components/apis/authApiClients' + +export interface AttachRepositoryPayload { + repository_url: string + branch_name?: string + user_id?: string + template_id?: string +} + +export interface AttachRepositoryResponse { + success: boolean + message: string + data?: { + repository_id: string + repository_name: string + owner_name: string + branch_name: string + is_public: boolean + sync_status: string + webhook_result?: { + created: boolean + hook_id: string | number + } + storage_info?: { + total_files: number + total_directories: number + total_size_bytes: number + } + } + requires_auth?: boolean + auth_url?: string + auth_error?: boolean +} + +// Provider detection function +export function detectProvider(repoUrl: string): string { + if (repoUrl.includes('github.com')) return 'github'; + if (repoUrl.includes('gitlab.com') || repoUrl.includes('gitlab')) return 'gitlab'; + if (repoUrl.includes('bitbucket.org')) return 'bitbucket'; + if (repoUrl.includes('gitea')) return 'gitea'; + throw new Error('Unsupported repository provider'); +} + +// Universal repository attachment function +export async function attachRepository(payload: AttachRepositoryPayload, retries = 3): Promise { + const rawUser = (typeof window !== 'undefined') ? localStorage.getItem('codenuk_user') : null + const userId = rawUser ? (JSON.parse(rawUser)?.id || null) : null + + // Detect provider from URL + const provider = detectProvider(payload.repository_url); + console.log('🔍 [attachRepository] Detected provider:', provider); + + const url = userId + ? `/api/vcs/${provider}/attach-repository?user_id=${encodeURIComponent(userId)}` + : `/api/vcs/${provider}/attach-repository`; + + // Retry logic for connection issues + for (let i = 0; i < retries; i++) { + try { + const response = await authApiClient.post(url, { + ...payload, + user_id: userId + }, { + headers: { 'Content-Type': 'application/json' }, + timeout: 60000 // 60 seconds for repository operations + }); + + console.log(`📡 [attachRepository] ${provider.toUpperCase()} response:`, response.data); + + // Normalize response + let parsed: any = response.data; + if (typeof parsed === 'string') { + try { + parsed = JSON.parse(parsed); + } catch (e) { + console.warn('📡 [attachRepository] Failed to parse string response'); + } + } + + const normalized: AttachRepositoryResponse = { + ...(parsed || {}), + success: (parsed?.success === true || parsed?.success === 'true') + }; + + return normalized; + } catch (error: any) { + // If it's the last retry or not a connection error, throw immediately + if (i === retries - 1 || (error.code !== 'ECONNREFUSED' && error.code !== 'ECONNRESET')) { + throw error; + } + + // Wait before retrying (exponential backoff) + const waitTime = Math.min(1000 * Math.pow(2, i), 5000); + console.log(`📡 [attachRepository] Retry ${i + 1}/${retries} in ${waitTime}ms...`); + await new Promise(resolve => setTimeout(resolve, waitTime)); + } + } + + throw new Error('Max retries exceeded'); +} + +// Universal OAuth connection function +export async function connectProvider(provider: string, repoUrl?: string, branch?: string): Promise { + const rawUser = (typeof window !== 'undefined') ? localStorage.getItem('codenuk_user') : null; + const userId = rawUser ? (JSON.parse(rawUser)?.id || null) : null; + + if (!userId) { + alert('Please sign in first to connect your account'); + return; + } + + // Build state with repository context if provided + const stateBase = Math.random().toString(36).substring(7); + let state = stateBase; + + if (repoUrl) { + const encodedRepoUrl = encodeURIComponent(repoUrl); + const encodedBranch = encodeURIComponent(branch || 'main'); + state = `${stateBase}|uid=${userId}|repo=${encodedRepoUrl}|branch=${encodedBranch}`; + + // Store in sessionStorage for recovery + try { + sessionStorage.setItem('pending_git_attach', JSON.stringify({ + repository_url: repoUrl, + branch_name: branch || 'main', + provider: provider + })); + } catch (e) { + console.warn('Failed to store pending attach in sessionStorage:', e); + } + } + + console.log(`🔗 [connectProvider] Connecting ${provider.toUpperCase()} account for user:`, userId); + console.log(`🔗 [connectProvider] API URL: /api/vcs/${provider}/auth/start?user_id=${encodeURIComponent(userId)}&state=${encodeURIComponent(state)}`); + + try { + const response = await authApiClient.get( + `/api/vcs/${provider}/auth/start?user_id=${encodeURIComponent(userId)}&state=${encodeURIComponent(state)}` + ); + + console.log(`🔗 [connectProvider] Backend response:`, response.data); + + const authUrl = response.data?.auth_url; + if (authUrl) { + console.log(`🔗 [connectProvider] Redirecting to ${provider.toUpperCase()} OAuth:`, authUrl); + console.log(`🔗 [connectProvider] Auth URL domain:`, new URL(authUrl).hostname); + window.location.replace(authUrl); + } else { + console.error('No auth URL in response:', response.data); + throw new Error('No auth URL received from backend'); + } + } catch (error) { + console.error(`Failed to get ${provider.toUpperCase()} OAuth URL:`, error); + throw error; + } +} + +// Provider-specific OAuth functions (for backward compatibility) +export async function connectGitHub(repoUrl?: string, branch?: string): Promise { + return connectProvider('github', repoUrl, branch); +} + +export async function connectGitLab(repoUrl?: string, branch?: string): Promise { + return connectProvider('gitlab', repoUrl, branch); +} + +export async function connectBitbucket(repoUrl?: string, branch?: string): Promise { + return connectProvider('bitbucket', repoUrl, branch); +} + +export async function connectGitea(repoUrl?: string, branch?: string): Promise { + return connectProvider('gitea', repoUrl, branch); +} + +// Repository structure and file content (works for all providers) +export interface RepoStructureEntry { + name: string + type: 'file' | 'directory' + size?: number + path: string +} + +export interface RepoStructureResponse { + repository_id: string + directory_path: string + structure: RepoStructureEntry[] +} + +export async function getRepositoryStructure(repositoryId: string, path: string = ''): Promise { + // Try to detect provider from repository ID or use a generic endpoint + const url = `/api/github/repository/${encodeURIComponent(repositoryId)}/structure${path ? `?path=${encodeURIComponent(path)}` : ''}`; + const res = await authApiClient.get(url); + return res.data?.data as RepoStructureResponse; +} + +export interface FileContentResponse { + file_info: { + id: string + filename: string + file_extension?: string + relative_path: string + file_size_bytes?: number + mime_type?: string + is_binary?: boolean + language_detected?: string + line_count?: number + char_count?: number + } + content: string | null + preview?: string | null +} + +export async function getRepositoryFileContent(repositoryId: string, filePath: string): Promise { + const url = `/api/github/repository/${encodeURIComponent(repositoryId)}/file-content?file_path=${encodeURIComponent(filePath)}`; + const res = await authApiClient.get(url); + return res.data?.data as FileContentResponse; +} + +// AI Streaming (works for all providers) +export async function startAIStreaming(repositoryId: string, options: { + fileTypes?: string[] + maxSize?: number + includeBinary?: boolean + chunkSize?: number + directoryFilter?: string + excludePatterns?: string[] +} = {}): Promise { + const params = new URLSearchParams(); + + if (options.fileTypes) params.append('file_types', options.fileTypes.join(',')); + if (options.maxSize) params.append('max_size', options.maxSize.toString()); + if (options.includeBinary) params.append('include_binary', 'true'); + if (options.chunkSize) params.append('chunk_size', options.chunkSize.toString()); + if (options.directoryFilter) params.append('directory_filter', options.directoryFilter); + if (options.excludePatterns) params.append('exclude_patterns', options.excludePatterns.join(',')); + + const url = `/api/ai-stream/repository/${encodeURIComponent(repositoryId)}/ai-stream?${params.toString()}`; + return new EventSource(url); +}