import { getRawCredentialsByProvider } from "./credentials"; export interface RepoResult { provider: "github" | "gitlab" | "gitea"; fullName: string; url: string; description: string; defaultBranch: string; private: boolean; } export async function searchRepos(query: string): Promise { if (!query || query.length < 2) return []; const results = await Promise.allSettled([ searchGitHub(query), searchGitLab(query), searchGitea(query), ]); return results.flatMap(r => r.status === "fulfilled" ? r.value : []); } async function searchGitHub(query: string): Promise { const creds = await getRawCredentialsByProvider("github"); if (creds.length === 0) return []; const results: RepoResult[] = []; for (const cred of creds) { try { const res = await fetch( `https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&per_page=10&sort=updated`, { headers: { Authorization: `Bearer ${cred.token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", }, } ); if (!res.ok) continue; const data = await res.json(); for (const repo of data.items || []) { results.push({ provider: "github", fullName: repo.full_name, url: repo.html_url, description: repo.description || "", defaultBranch: repo.default_branch || "main", private: repo.private, }); } } catch { // skip failed credential } } return results; } async function searchGitLab(query: string): Promise { const creds = await getRawCredentialsByProvider("gitlab"); if (creds.length === 0) return []; const results: RepoResult[] = []; for (const cred of creds) { const baseUrl = cred.baseUrl || "https://gitlab.com"; try { const res = await fetch( `${baseUrl}/api/v4/projects?search=${encodeURIComponent(query)}&per_page=10&order_by=updated_at&membership=true`, { headers: { "PRIVATE-TOKEN": cred.token, }, } ); if (!res.ok) continue; const data = await res.json(); for (const project of data) { results.push({ provider: "gitlab", fullName: project.path_with_namespace, url: project.web_url, description: project.description || "", defaultBranch: project.default_branch || "main", private: project.visibility === "private", }); } } catch { // skip failed credential } } return results; } async function searchGitea(query: string): Promise { const creds = await getRawCredentialsByProvider("gitea"); if (creds.length === 0) return []; const results: RepoResult[] = []; for (const cred of creds) { const baseUrl = cred.baseUrl || "https://gitea.coreworlds.io"; try { const res = await fetch( `${baseUrl}/api/v1/repos/search?q=${encodeURIComponent(query)}&limit=10&sort=updated`, { headers: { Authorization: `token ${cred.token}`, }, } ); if (!res.ok) { console.error(`[repo-search] Gitea search failed: ${res.status} ${res.statusText}`); continue; } const data = await res.json(); // Rewrite internal service URLs to external Gitea URL const externalUrl = process.env.GITEA_EXTERNAL_URL || "https://gitea.coreworlds.io"; for (const repo of data.data || []) { let htmlUrl: string = repo.html_url || ""; if (baseUrl !== externalUrl && htmlUrl.startsWith(baseUrl)) { htmlUrl = externalUrl + htmlUrl.slice(baseUrl.length); } results.push({ provider: "gitea", fullName: repo.full_name, url: htmlUrl, description: repo.description || "", defaultBranch: repo.default_branch || "main", private: repo.private, }); } } catch (err) { console.error(`[repo-search] Gitea search error:`, err); } } return results; }