export type Platform = "github" | "gitea" | "gitlab"; export async function getDefaultBranch(): Promise { try { const result = await Bun.$`git symbolic-ref refs/remotes/origin/HEAD`.quiet().text(); return result.trim().replace("refs/remotes/origin/", ""); } catch { try { const branches = await Bun.$`git branch -r`.quiet().text(); for (const line of branches.split("\n")) { const trimmed = line.trim(); if (trimmed === "origin/main" || trimmed === "origin/master") { return trimmed.replace("origin/", ""); } } } catch {} return "main"; } } export async function getBranchName(): Promise { const result = await Bun.$`git rev-parse --abbrev-ref HEAD`.quiet().text(); return result.trim(); } export async function getBranchPushStatus(): Promise<{ pushed: boolean; upstream: string | null; }> { try { const upstream = (await Bun.$`git rev-parse --abbrev-ref --symbolic-full-name @{u}`.quiet().text()).trim(); const localHead = (await Bun.$`git rev-parse HEAD`.quiet().text()).trim(); const remoteHead = (await Bun.$`git rev-parse @{u}`.quiet().text()).trim(); return { pushed: localHead === remoteHead, upstream }; } catch { return { pushed: false, upstream: null }; } } export async function pushCurrentBranch(branch: string, upstream: string | null): Promise { const args = upstream ? ["push"] : ["push", "-u", "origin", branch]; const proc = Bun.spawn(["git", ...args], { stdout: "pipe", stderr: "pipe", }); const exitCode = await proc.exited; const stdout = await new Response(proc.stdout).text(); const stderr = await new Response(proc.stderr).text(); if (exitCode !== 0) { throw new Error(stderr.trim() || stdout.trim() || `git push failed (exit code ${exitCode})`); } } export async function getBranchCommits(base: string): Promise { try { const result = await Bun.$`git log --oneline origin/${base}..HEAD`.quiet().text(); return result.trim().split("\n").filter(Boolean); } catch { try { const result = await Bun.$`git log --oneline ${base}..HEAD`.quiet().text(); return result.trim().split("\n").filter(Boolean); } catch { return []; } } } export async function getBranchDiff(base: string): Promise { try { const result = await Bun.$`git diff ${base}...HEAD`.quiet().text(); return result.trim(); } catch { return ""; } } function parseRemoteHostname(url: string): string | null { const hostname = url .trim() .toLowerCase() .replace(/^(https?:\/\/|ssh:\/\/|git:\/\/)/, "") .replace(/^[^@]+@/, "") .split(/[:/]/)[0]; return hostname || null; } export function detectPlatformFromHostname(hostname: string | null): Platform | null { if (!hostname) return null; if (hostname === "github.com") return "github"; if (hostname === "gitlab.com" || hostname.includes("gitlab")) { return "gitlab"; } if (hostname.includes("gitea")) return "gitea"; return null; } export async function detectPlatform(): Promise { try { const url = await Bun.$`git remote get-url origin`.quiet().text(); const hostname = parseRemoteHostname(url); return detectPlatformFromHostname(hostname); } catch { return null; } } export async function getRemoteHostname(): Promise { try { const url = await Bun.$`git remote get-url origin`.quiet().text(); return parseRemoteHostname(url); } catch { return null; } } export async function createPR( platform: Platform, title: string, body: string, base: string, draft: boolean, ): Promise { if (platform === "github") { const args = [ "pr", "create", "--title", title, "--body", body, "--base", base, ]; if (draft) args.push("--draft"); const proc = Bun.spawn(["gh", ...args], { stdout: "pipe", stderr: "pipe", }); const exitCode = await proc.exited; const stdout = await new Response(proc.stdout).text(); const stderr = await new Response(proc.stderr).text(); if (exitCode !== 0) { throw new Error( stderr.trim() || `gh pr create failed (exit code ${exitCode})`, ); } const match = stdout.match(/(https?:\/\/[^\s]+)/); return match?.[1] ?? stdout.trim(); } if (platform === "gitlab") { const args = [ "mr", "create", "--title", title, "--description", body, "--target-branch", base, ]; if (draft) args.push("--draft"); const proc = Bun.spawn(["glab", ...args], { stdout: "pipe", stderr: "pipe", }); const exitCode = await proc.exited; const stdout = await new Response(proc.stdout).text(); const stderr = await new Response(proc.stderr).text(); if (exitCode !== 0) { throw new Error( stderr.trim() || `glab mr create failed (exit code ${exitCode})`, ); } const match = stdout.match(/(https?:\/\/[^\s]+)/); return match?.[1] ?? stdout.trim(); } const args = [ "pulls", "create", "--title", title, "--description", body, "--base", base, ]; const proc = Bun.spawn(["tea", ...args], { stdout: "pipe", stderr: "pipe", }); const exitCode = await proc.exited; const stdout = await new Response(proc.stdout).text(); const stderr = await new Response(proc.stderr).text(); if (exitCode !== 0) { throw new Error( stderr.trim() || `tea pulls create failed (exit code ${exitCode})`, ); } const match = stdout.match(/(https?:\/\/[^\s]+)/); return match?.[1] ?? stdout.trim(); }