import path from "node:path"; import { unprocessable } from "../errors.js"; function asString(value: unknown): string | undefined { if (typeof value !== "string") return undefined; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; } export async function fetchText(url: string): Promise { const response = await fetch(url); if (!response.ok) { throw unprocessable(`Failed to fetch ${url}: ${response.status}`); } return response.text(); } export async function fetchJson(url: string): Promise { const response = await fetch(url, { headers: { accept: "application/vnd.github+json", }, }); if (!response.ok) { throw unprocessable(`Failed to fetch ${url}: ${response.status}`); } return response.json() as Promise; } export async function resolveGitHubDefaultBranch(owner: string, repo: string): Promise { const response = await fetchJson<{ default_branch?: string }>( `https://api.github.com/repos/${owner}/${repo}`, ); return asString(response.default_branch) ?? "main"; } export async function resolveGitHubCommitSha(owner: string, repo: string, ref: string): Promise { const response = await fetchJson<{ sha?: string }>( `https://api.github.com/repos/${owner}/${repo}/commits/${encodeURIComponent(ref)}`, ); const sha = asString(response.sha); if (!sha) { throw unprocessable(`Failed to resolve GitHub ref ${ref}`); } return sha; } export function parseGitHubSourceUrl(rawUrl: string): { owner: string; repo: string; ref: string; basePath: string; filePath: string | null; explicitRef: boolean; } { const url = new URL(rawUrl); if (url.hostname !== "github.com") { throw unprocessable("GitHub source must use github.com URL"); } const parts = url.pathname.split("/").filter(Boolean); if (parts.length < 2) { throw unprocessable("Invalid GitHub URL"); } const owner = parts[0]!; const repo = parts[1]!.replace(/\.git$/i, ""); let ref = "main"; let basePath = ""; let filePath: string | null = null; let explicitRef = false; if (parts[2] === "tree") { ref = parts[3] ?? "main"; basePath = parts.slice(4).join("/"); explicitRef = true; } else if (parts[2] === "blob") { ref = parts[3] ?? "main"; filePath = parts.slice(4).join("/"); basePath = filePath ? path.posix.dirname(filePath) : ""; explicitRef = true; } return { owner, repo, ref, basePath, filePath, explicitRef }; } export async function resolveGitHubPinnedRef( parsed: ReturnType, ): Promise<{ pinnedRef: string; trackingRef: string | null }> { if (/^[0-9a-f]{40}$/i.test(parsed.ref.trim())) { return { pinnedRef: parsed.ref, trackingRef: parsed.explicitRef ? parsed.ref : null, }; } const trackingRef = parsed.explicitRef ? parsed.ref : await resolveGitHubDefaultBranch(parsed.owner, parsed.repo); const pinnedRef = await resolveGitHubCommitSha(parsed.owner, parsed.repo, trackingRef); return { pinnedRef, trackingRef }; } export function resolveRawGitHubUrl(owner: string, repo: string, ref: string, filePath: string): string { return `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${filePath.replace(/^\/+/, "")}`; }