import type { GitHubAPI, GitHubAuthStatus, GitHubBranchesSearchResult, GitHubIssueCommentsResult, GitHubIssueCommentInput, GitHubIssueCommentResult, GitHubIssueCreateInput, GitHubIssueCreateResult, GitHubIssueGetResult, GitHubIssueUpdateInput, GitHubIssueUpdateResult, GitHubIssuesListResult, GitHubLabelsSearchResult, GitHubMilestonesSearchResult, GitHubPullRequestContextResult, GitHubPullRequestCommitsResult, GitHubPullRequestTimelineResult, GitHubPullRequestsListResult, GitHubPullRequest, GitHubPullRequestCreateInput, GitHubPullRequestMergeInput, GitHubPullRequestMergeResult, GitHubPullRequestReadyInput, GitHubPullRequestReadyResult, GitHubPullRequestUpdateInput, GitHubPullRequestStatus, GitHubPullRequestReviewInput, GitHubPullRequestReviewResult, GitHubRepoUpstreamResult, GitHubReviewCommentInput, GitHubReviewCommentResult, GitHubDeviceFlowComplete, GitHubDeviceFlowStart, GitHubTagsSearchResult, GitHubUserSummary, GitHubUsersSearchResult, } from '@openchamber/ui/lib/api/types'; import { runtimeFetch } from '@openchamber/ui/lib/runtime-fetch'; import type { RuntimeUrlResolver } from '@openchamber/ui/lib/runtime-url'; interface WebGitHubAPIOptions { urls: RuntimeUrlResolver; } const jsonOrNull = async (response: Response): Promise => { return (await response.json().catch(() => null)) as T | null; }; export const createWebGitHubAPI = ({ urls }: WebGitHubAPIOptions): GitHubAPI => ({ async authStatus(): Promise { const response = await runtimeFetch('/api/github/auth/status', { method: 'GET', headers: { Accept: 'application/json' } }); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to load GitHub status'); } return payload; }, async authStart(): Promise { const response = await runtimeFetch('/api/github/auth/start', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({}), }); const payload = await jsonOrNull(response); if (!response.ok || !payload || !('deviceCode' in payload)) { throw new Error((payload as { error?: string } | null)?.error || response.statusText || 'Failed to start GitHub auth'); } return payload; }, async authComplete(deviceCode: string): Promise { const response = await runtimeFetch('/api/github/auth/complete', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ deviceCode }), }); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error((payload as { error?: string } | null)?.error || response.statusText || 'Failed to complete GitHub auth'); } return payload; }, async authDisconnect(): Promise<{ removed: boolean }> { const response = await runtimeFetch('/api/github/auth', { method: 'DELETE', headers: { Accept: 'application/json' } }); const payload = await jsonOrNull<{ removed?: boolean; error?: string }>(response); if (!response.ok) { throw new Error(payload?.error || response.statusText || 'Failed to disconnect GitHub'); } return { removed: Boolean(payload?.removed) }; }, async authActivate(accountId: string): Promise { const response = await runtimeFetch('/api/github/auth/activate', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ accountId }), }); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to activate GitHub account'); } return payload; }, async authSetGhCliDisabled(disabled: boolean): Promise<{ disabled: boolean }> { const response = await runtimeFetch('/api/github/auth/gh-cli', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ disabled }), }); const payload = await jsonOrNull<{ disabled?: boolean; error?: string }>(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to update gh CLI setting'); } return { disabled: Boolean(payload.disabled) }; }, async me(): Promise { const response = await runtimeFetch('/api/github/me', { method: 'GET', headers: { Accept: 'application/json' } }); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to fetch GitHub user'); } return payload; }, async searchUsers(directory, query, options): Promise { const params = new URLSearchParams({ directory, query }); if (options?.sourceRepo?.owner && options.sourceRepo.repo) { params.set('owner', options.sourceRepo.owner); params.set('repo', options.sourceRepo.repo); } const response = await runtimeFetch(urls.api('/api/github/users/search', params), { method: 'GET', headers: { Accept: 'application/json' } }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to search GitHub users'); } return { connected: body.connected, repo: body.repo ?? null, users: body.users ?? [] }; }, async searchLabels(directory, query, options): Promise { const params = new URLSearchParams({ directory, query }); if (options?.sourceRepo?.owner && options.sourceRepo.repo) { params.set('owner', options.sourceRepo.owner); params.set('repo', options.sourceRepo.repo); } const response = await runtimeFetch(urls.api('/api/github/labels/search', params), { method: 'GET', headers: { Accept: 'application/json' } }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to search GitHub labels'); } return { connected: body.connected, repo: body.repo ?? null, labels: body.labels ?? [] }; }, async searchMilestones(directory, query, options): Promise { const params = new URLSearchParams({ directory, query }); if (options?.sourceRepo?.owner && options.sourceRepo.repo) { params.set('owner', options.sourceRepo.owner); params.set('repo', options.sourceRepo.repo); } const response = await runtimeFetch(urls.api('/api/github/milestones/search', params), { method: 'GET', headers: { Accept: 'application/json' } }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to search GitHub milestones'); } return { connected: body.connected, repo: body.repo ?? null, milestones: body.milestones ?? [] }; }, async searchBranches(directory, query, options): Promise { const params = new URLSearchParams({ directory, query }); if (options?.sourceRepo?.owner && options.sourceRepo.repo) { params.set('owner', options.sourceRepo.owner); params.set('repo', options.sourceRepo.repo); } const response = await runtimeFetch(urls.api('/api/github/branches/search', params), { method: 'GET', headers: { Accept: 'application/json' } }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to search GitHub branches'); } return { connected: body.connected, repo: body.repo ?? null, branches: body.branches ?? [] }; }, async searchTags(directory, query, options): Promise { const params = new URLSearchParams({ directory, query }); if (options?.sourceRepo?.owner && options.sourceRepo.repo) { params.set('owner', options.sourceRepo.owner); params.set('repo', options.sourceRepo.repo); } const response = await runtimeFetch(urls.api('/api/github/tags/search', params), { method: 'GET', headers: { Accept: 'application/json' } }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to search GitHub tags'); } return { connected: body.connected, repo: body.repo ?? null, tags: body.tags ?? [] }; }, async prStatus(directory: string, branch: string, remote?: string, options?: { force?: boolean }): Promise { const params = new URLSearchParams({ directory, branch, ...(remote ? { remote } : {}), ...(options?.force ? { force: 'true' } : {}), }); const response = await runtimeFetch( `/api/github/pr/status?${params.toString()}`, { method: 'GET', headers: { Accept: 'application/json' } } ); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to load PR status'); } return payload; }, async prCreate(payload: GitHubPullRequestCreateInput): Promise { const response = await runtimeFetch('/api/github/pr/create', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(payload), }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error((body as { error?: string } | null)?.error || response.statusText || 'Failed to create PR'); } return body; }, async prUpdate(payload: GitHubPullRequestUpdateInput): Promise { const response = await runtimeFetch('/api/github/pr/update', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(payload), }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error((body as { error?: string } | null)?.error || response.statusText || 'Failed to update PR'); } return body; }, async prMerge(payload: GitHubPullRequestMergeInput): Promise { const response = await runtimeFetch('/api/github/pr/merge', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(payload), }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error((body as { error?: string } | null)?.error || response.statusText || 'Failed to merge PR'); } return body; }, async prReady(payload: GitHubPullRequestReadyInput): Promise { const response = await runtimeFetch('/api/github/pr/ready', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(payload), }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error((body as { error?: string } | null)?.error || response.statusText || 'Failed to mark PR ready'); } return body; }, async repoUpstream(directory: string): Promise { const response = await runtimeFetch( `/api/github/repo/upstream?directory=${encodeURIComponent(directory)}`, { method: 'GET', headers: { Accept: 'application/json' } } ); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to detect upstream repo'); } return body; }, async repoBranches(owner: string, repo: string): Promise { const response = await runtimeFetch( `/api/github/repo/branches?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}`, { method: 'GET', headers: { Accept: 'application/json' } } ); const body = await jsonOrNull<{ branches?: string[]; error?: string }>(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to fetch repo branches'); } return body.branches ?? []; }, async prsList(directory: string, options?: { page?: number; query?: string }): Promise { const page = options?.page ?? 1; const params = new URLSearchParams({ directory, page: String(page), }); if (options?.query) { params.set('query', options.query); } const response = await runtimeFetch( `/api/github/pulls/list?${params.toString()}`, { method: 'GET', headers: { Accept: 'application/json' } } ); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to load pull requests'); } return body; }, async prContext( directory: string, number: number, options?: { includeDiff?: boolean; includeCheckDetails?: boolean; sourceRepo?: { owner: string; repo: string } | null } ): Promise { const params = new URLSearchParams({ directory, number: String(number) }); if (options?.includeDiff) { params.set('diff', '1'); } if (options?.includeCheckDetails) { params.set('checkDetails', '1'); } if (options?.sourceRepo?.owner && options.sourceRepo.repo) { params.set('owner', options.sourceRepo.owner); params.set('repo', options.sourceRepo.repo); } const response = await runtimeFetch(urls.api('/api/github/pulls/context', params), { method: 'GET', headers: { Accept: 'application/json' } }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to load pull request context'); } return body; }, async issuesList(directory: string, options?: { page?: number; query?: string }): Promise { const page = options?.page ?? 1; const params = new URLSearchParams({ directory, page: String(page), }); if (options?.query) { params.set('query', options.query); } const response = await runtimeFetch( `/api/github/issues/list?${params.toString()}`, { method: 'GET', headers: { Accept: 'application/json' } } ); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to load issues'); } return payload; }, async issueGet(directory: string, number: number, options?: { sourceRepo?: { owner: string; repo: string } | null }): Promise { const params = new URLSearchParams({ directory, number: String(number) }); if (options?.sourceRepo?.owner && options.sourceRepo.repo) { params.set('owner', options.sourceRepo.owner); params.set('repo', options.sourceRepo.repo); } const response = await runtimeFetch(urls.api('/api/github/issues/get', params), { method: 'GET', headers: { Accept: 'application/json' } }); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to load issue'); } return payload; }, async issueComments(directory: string, number: number, options?: { sourceRepo?: { owner: string; repo: string } | null }): Promise { const params = new URLSearchParams({ directory, number: String(number) }); if (options?.sourceRepo?.owner && options.sourceRepo.repo) { params.set('owner', options.sourceRepo.owner); params.set('repo', options.sourceRepo.repo); } const response = await runtimeFetch(urls.api('/api/github/issues/comments', params), { method: 'GET', headers: { Accept: 'application/json' } }); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to load issue comments'); } return payload; }, async prCommits(directory: string, number: number, options?: { sourceRepo?: { owner: string; repo: string } | null }): Promise { const params = new URLSearchParams({ directory, number: String(number) }); if (options?.sourceRepo?.owner && options.sourceRepo.repo) { params.set('owner', options.sourceRepo.owner); params.set('repo', options.sourceRepo.repo); } const response = await runtimeFetch(urls.api('/api/github/pulls/commits', params), { method: 'GET', headers: { Accept: 'application/json' } }); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to load pull request commits'); } return payload; }, async prTimeline(directory: string, number: number, options?: { sourceRepo?: { owner: string; repo: string } | null }): Promise { const params = new URLSearchParams({ directory, number: String(number) }); if (options?.sourceRepo?.owner && options.sourceRepo.repo) { params.set('owner', options.sourceRepo.owner); params.set('repo', options.sourceRepo.repo); } const response = await runtimeFetch(urls.api('/api/github/pulls/timeline', params), { method: 'GET', headers: { Accept: 'application/json' } }); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to load pull request timeline'); } return payload; }, async issueComment(input: GitHubIssueCommentInput): Promise { const response = await runtimeFetch('/api/github/issues/comment', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to post GitHub comment'); } return body; }, async issueCreate(input: GitHubIssueCreateInput): Promise { const response = await runtimeFetch('/api/github/issues/create', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to create GitHub issue'); } return body; }, async issueUpdate(input: GitHubIssueUpdateInput): Promise { const response = await runtimeFetch('/api/github/issues/update', { method: 'PATCH', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to update GitHub issue'); } return body; }, async prComment(input: GitHubIssueCommentInput): Promise { const response = await runtimeFetch('/api/github/pulls/comment', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to post GitHub PR comment'); } return body; }, async prReviewComment(input: GitHubReviewCommentInput): Promise { const response = await runtimeFetch('/api/github/pulls/review-comment', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to post GitHub review comment'); } return body; }, async prSubmitReview(input: GitHubPullRequestReviewInput): Promise { const response = await runtimeFetch('/api/github/pulls/review', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to submit GitHub review'); } return body; }, });