import { execFile } from "node:child_process"; import { promisify } from "node:util"; import type { EncryptedSecretStore } from "@rakazo/adapters"; import type { PrismaClient } from "@rakazo/db"; const execFileAsync = promisify(execFile); export interface GithubRepoItem { name: string; fullName: string; url: string; description?: string | null; isPrivate: boolean; updatedAt?: string; } export interface ListGithubReposOutput { connected: boolean; username?: string; repos: GithubRepoItem[]; } export async function listUserGithubRepos( prisma: Pick, input?: { query?: string; limit?: number }, secrets?: Pick, ): Promise { let token: string | null = null; // 1. Check CapabilityInstall in database if secret store is provided if (secrets) { try { const installs = await prisma.capabilityInstall.findMany({ where: { kind: "mcp" }, }); for (const inst of installs) { const cfg = inst.config as Record | null; if (cfg?.preset === "github" && inst.secretId) { const secret = await prisma.secret.findUnique({ where: { id: inst.secretId }, select: { id: true, ciphertext: true }, }); if (secret) { try { const loaded = secrets.load(secret.ciphertext, secret.id); if (loaded?.trim()) { token = loaded.trim(); break; } } catch { // decryption error fallback } } } } } catch { // Database lookup error fallback } } // 2. Check environment if (!token) { token = process.env.GITHUB_TOKEN?.trim() || process.env.GH_TOKEN?.trim() || null; } // 3. Check local gh CLI auth token if (!token) { try { const { stdout } = await execFileAsync("gh", ["auth", "token"], { timeout: 3000 }); if (stdout.trim()) { token = stdout.trim(); } } catch { // gh CLI not available or not logged in } } // If token is available, query GitHub REST API if (token) { try { let username: string | undefined; const userRes = await fetch("https://api.github.com/user", { headers: { Authorization: `Bearer ${token}`, "User-Agent": "evOo-App", Accept: "application/vnd.github+json", }, }); if (userRes.ok) { const userData = (await userRes.json()) as { login?: string }; username = userData.login; } const reposRes = await fetch( "https://api.github.com/user/repos?sort=pushed&per_page=100&type=all", { headers: { Authorization: `Bearer ${token}`, "User-Agent": "evOo-App", Accept: "application/vnd.github+json", }, }, ); if (reposRes.ok) { const rawRepos = (await reposRes.json()) as Array<{ name: string; full_name: string; html_url: string; description?: string | null; private?: boolean; pushed_at?: string; updated_at?: string; }>; let items: GithubRepoItem[] = rawRepos.map((r) => ({ name: r.name, fullName: r.full_name, url: r.html_url, description: r.description ?? null, isPrivate: Boolean(r.private), updatedAt: r.pushed_at || r.updated_at, })); if (input?.query?.trim()) { const q = input.query.trim().toLowerCase(); items = items.filter( (r) => r.name.toLowerCase().includes(q) || r.fullName.toLowerCase().includes(q) || (r.description && r.description.toLowerCase().includes(q)), ); } const limit = input?.limit ?? 50; return { connected: true, username, repos: items.slice(0, limit), }; } } catch { // Fall through to gh repo list fallback } } // Fallback: try gh repo list directly try { const { stdout } = await execFileAsync( "gh", [ "repo", "list", "--limit", "100", "--json", "nameWithOwner,name,url,description,isPrivate,pushedAt", ], { timeout: 5000 }, ); const parsed = JSON.parse(stdout) as Array<{ nameWithOwner: string; name: string; url: string; description?: string; isPrivate?: boolean; pushedAt?: string; }>; let items: GithubRepoItem[] = parsed.map((r) => ({ name: r.name, fullName: r.nameWithOwner, url: r.url, description: r.description || null, isPrivate: Boolean(r.isPrivate), updatedAt: r.pushedAt, })); if (input?.query?.trim()) { const q = input.query.trim().toLowerCase(); items = items.filter( (r) => r.name.toLowerCase().includes(q) || r.fullName.toLowerCase().includes(q) || (r.description && r.description.toLowerCase().includes(q)), ); } const limit = input?.limit ?? 50; return { connected: true, repos: items.slice(0, limit), }; } catch { return { connected: false, repos: [], }; } }