Something went wrong. Try again.
A collaborative coding-agent orchestrator for atproto radl.app
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216import type { FetchLike } from '@radial/atproto'import type { ForgeAdapter, GitHubRepo, PullRequestState } from './forge.js'
export function parseGitHubRepo(gitUrl: string): GitHubRepo { const fail = (): never => { throw new Error(`not a GitHub repository URL: ${gitUrl}`) }
let host: string let path: string if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(gitUrl)) { let url: URL try { url = new URL(gitUrl) } catch { return fail() } host = url.hostname.toLowerCase() path = url.pathname } else { const scp = /^(?:[^@/:]+@)?([^@/:]+):(.+)$/.exec(gitUrl) if (!scp) return fail() host = (scp[1] as string).toLowerCase() path = scp[2] as string }
if (host !== 'github.com' && host !== 'www.github.com') return fail() const [owner, repo] = path.replace(/^\/+/, '').replace(/\.git$/, '').split('/') if (!owner || !repo) return fail() return { owner, repo }}
export function parseGitHubPullUrl(prUrl: string): { repo: GitHubRepo; number: number } { let url: URL try { url = new URL(prUrl) } catch { throw new Error(`invalid pull request URL: ${prUrl}`) } if ( url.protocol !== 'https:' || url.hostname.toLowerCase() !== 'github.com' || url.username || url.password || url.port || url.search || url.hash ) { throw new Error(`not a canonical GitHub pull request URL: ${prUrl}`) } const match = /^\/([^/]+)\/([^/]+)\/pull\/([1-9][0-9]*)$/.exec(url.pathname) if (!match) { throw new Error(`not a canonical github.com/owner/repo/pull/<n> URL: ${prUrl}`) } return { repo: { owner: match[1] as string, repo: match[2] as string }, number: Number(match[3]), }}
export function compareUrl(repo: GitHubRepo, base: string, commit: string): string { return `https://github.com/${repo.owner}/${repo.repo}/compare/${base}...${commit}`}
/** `owner/repo`, lowercased — how GitHub names a repository in both `projectIdentity` and * `PullRequestState.headRepoFullName`, so the dispatcher's reuse guard compares like with like. */const fullName = (repo: GitHubRepo): string => `${repo.owner}/${repo.repo}`.toLowerCase()
/** * The GitHub adapter. Observation-only by design (design §10): a turn pushes its branch and opens * or updates its own pull request with `gh` from inside the container, and this adapter only ever * reads. That is why it implements neither `openPullRequest` nor `turnEnvironment` — `--pr` stays * required on a GitHub implementation submit, exactly as before. * * `token` is optional so the daemon can still reason about GitHub URLs (which pull belongs to which * project, what a compare link looks like) when operator authentication is unavailable. Without one * `canObserve` is false and `getPullRequestState` throws rather than making an unauthenticated call * that would silently 404 on a private repository and read as "no such pull request". */export class GitHubForge implements ForgeAdapter { readonly kind = 'github' as const readonly #fetch: FetchLike readonly #token: string | undefined readonly #apiBase: string
constructor(options: { fetch: FetchLike; token?: string; apiBase?: string }) { this.#fetch = options.fetch this.#token = options.token this.#apiBase = options.apiBase ?? 'https://api.github.com' }
get canObserve(): boolean { return Boolean(this.#token) }
matchesGitUrl(gitUrl: string): boolean { try { parseGitHubRepo(gitUrl) return true } catch { return false } }
ownsPullUrl(prUrl: string): boolean { try { parseGitHubPullUrl(prUrl) return true } catch { return false } }
async pullBelongsToProject(prUrl: string, gitUrl: string): Promise<boolean> { const project = parseGitHubRepo(gitUrl) const parsed = parseGitHubPullUrl(prUrl) return fullName(parsed.repo) === fullName(project) }
async projectIdentity(gitUrl: string): Promise<string> { return fullName(parseGitHubRepo(gitUrl)) }
implementationBlockedReason(): string | undefined { return this.#token ? undefined : 'GitHub authentication is unavailable' }
async getPullRequestState(prUrl: string): Promise<PullRequestState> { if (!this.#token) throw new Error('GitHub authentication is unavailable') const { repo, number } = parseGitHubPullUrl(prUrl) const response = await this.#fetch( `${this.#apiBase}/repos/${repo.owner}/${repo.repo}/pulls/${number}`, { headers: { authorization: `Bearer ${this.#token}`, accept: 'application/vnd.github+json', 'x-github-api-version': '2022-11-28', }, }, ) if (response.status !== 200) { throw new Error(`getPullRequestState #${number} failed (${response.status})`) } const value = JSON.parse(await response.text()) as { state?: string merged?: boolean merged_at?: string | null head?: { ref?: string; repo?: { full_name?: string } | null } base?: { ref?: string } } return { state: value.merged ? 'merged' : value.state === 'open' ? 'open' : 'closed', headRef: value.head?.ref ?? '', headRepoFullName: value.head?.repo?.full_name ?? '', baseRef: value.base?.ref ?? '', ...(value.merged_at ? { mergedAt: value.merged_at } : {}), } }
compareUrl(gitUrl: string, base: string, commit: string): string { return compareUrl(parseGitHubRepo(gitUrl), base, commit) }}
export class FakeForge implements ForgeAdapter { readonly kind = 'github' as const readonly canObserve = true readonly pulls = new Map<string, PullRequestState>()
matchesGitUrl(gitUrl: string): boolean { try { parseGitHubRepo(gitUrl) return true } catch { return false } }
ownsPullUrl(prUrl: string): boolean { try { parseGitHubPullUrl(prUrl) return true } catch { return false } }
async pullBelongsToProject(prUrl: string, gitUrl: string): Promise<boolean> { return fullName(parseGitHubPullUrl(prUrl).repo) === fullName(parseGitHubRepo(gitUrl)) }
async projectIdentity(gitUrl: string): Promise<string> { return fullName(parseGitHubRepo(gitUrl)) }
async getPullRequestState(url: string): Promise<PullRequestState> { parseGitHubPullUrl(url) const pull = this.pulls.get(url) if (!pull) throw new Error(`FakeForge: no pull request at ${url}`) return pull }
compareUrl(gitUrl: string, base: string, commit: string): string { return compareUrl(parseGitHubRepo(gitUrl), base, commit) }}
/** URL reasoning with no credential behind it: which pull belongs to which project, and what a * compare link looks like. `dispatch.ts` and `turn-socket.ts` fall back to this when no adapter is * configured for a project, so their refusals keep the exact shape they had before the registry. */export function urlOnlyForgeAdapters(): ForgeAdapter[] { return [new GitHubForge({ fetch })]}