import type { GiteaAPI, GiteaAuthStatus, GiteaBranchesResult, GiteaBranchesSearchResult, GiteaIssueCommentInput, GiteaIssueCommentResult, GiteaIssueCommentsResult, GiteaIssueCreateInput, GiteaIssueCreateResult, GiteaIssueGetResult, GiteaIssuesListResult, GiteaIssueUpdateInput, GiteaIssueUpdateResult, GiteaLabelsSearchResult, GiteaMilestonesSearchResult, GiteaPullRequest, GiteaPullRequestCommitsResult, GiteaPullRequestContextResult, GiteaPullRequestCreateInput, GiteaPullRequestMergeInput, GiteaPullRequestMergeResult, GiteaPullRequestReviewsResult, GiteaPullRequestsListResult, GiteaPullRequestStatusesResult, GiteaPullRequestUpdateInput, GiteaPullReviewInput, GiteaPullReviewResult, GiteaRepoLabelsResult, GiteaTagsSearchResult, GiteaUserSummary, GiteaUsersSearchResult, } from '@openchamber/ui/lib/api/types'; import { runtimeFetch } from '@openchamber/ui/lib/runtime-fetch'; import type { RuntimeUrlResolver } from '@openchamber/ui/lib/runtime-url'; interface WebGiteaAPIOptions { urls: RuntimeUrlResolver; } interface GiteaPullRequestWriteResult { connected: boolean; repo?: { owner: string; repo: string; url?: string } | null; pr?: GiteaPullRequest; } const jsonOrNull = async (response: Response): Promise => { return (await response.json().catch(() => null)) as T | null; }; export const createWebGiteaAPI = ({ urls }: WebGiteaAPIOptions): GiteaAPI => ({ async authStatus(): Promise { const response = await runtimeFetch('/api/gitea/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 Gitea status'); } return payload; }, async authConnect(input: { accessToken: string; baseUrl: string }): Promise { const response = await runtimeFetch('/api/gitea/auth/connect', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const payload = await jsonOrNull(response); if (!response.ok || !payload) { throw new Error(payload?.error || response.statusText || 'Failed to connect Gitea'); } return payload; }, async authActivate(accountId: string): Promise { const response = await runtimeFetch('/api/gitea/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 Gitea account'); } return payload; }, async authDisconnect(): Promise<{ removed: boolean }> { const response = await runtimeFetch('/api/gitea/auth', { method: 'DELETE', headers: { 'Content-Type': 'application/json', 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 Gitea'); } return { removed: Boolean(payload?.removed) }; }, async me(): Promise { const response = await runtimeFetch('/api/gitea/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 Gitea user'); } return payload; }, async searchUsers(directory, query, options): Promise { const params = new URLSearchParams({ directory, query }); if (options?.owner) params.set('owner', options.owner); if (options?.repo) params.set('repo', options.repo); const response = await runtimeFetch(urls.api('/api/gitea/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 Gitea 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?.owner) params.set('owner', options.owner); if (options?.repo) params.set('repo', options.repo); const response = await runtimeFetch(urls.api('/api/gitea/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 Gitea 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?.owner) params.set('owner', options.owner); if (options?.repo) params.set('repo', options.repo); const response = await runtimeFetch(urls.api('/api/gitea/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 Gitea 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?.owner) params.set('owner', options.owner); if (options?.repo) params.set('repo', options.repo); const response = await runtimeFetch(urls.api('/api/gitea/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 Gitea 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?.owner) params.set('owner', options.owner); if (options?.repo) params.set('repo', options.repo); const response = await runtimeFetch(urls.api('/api/gitea/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 Gitea tags'); } return { connected: body.connected, repo: body.repo ?? null, tags: body.tags ?? [] }; }, 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/gitea/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 Gitea issues'); } return payload; }, async issueGet(directory: string, number: number, options?: { owner?: string; repo?: string }): Promise { const params = new URLSearchParams({ directory, number: String(number) }); if (options?.owner) { params.set('owner', options.owner); } if (options?.repo) { params.set('repo', options.repo); } const response = await runtimeFetch(urls.api('/api/gitea/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 Gitea issue'); } return payload; }, async issueComments(directory: string, number: number, options?: { owner?: string; repo?: string }): Promise { const params = new URLSearchParams({ directory, number: String(number) }); if (options?.owner) { params.set('owner', options.owner); } if (options?.repo) { params.set('repo', options.repo); } const response = await runtimeFetch(urls.api('/api/gitea/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 Gitea issue comments'); } return payload; }, async prsList(directory: string, options?: { page?: number; query?: string; sourceBranch?: string }): Promise { const page = options?.page ?? 1; const params = new URLSearchParams({ directory, page: String(page), }); if (options?.query) { params.set('query', options.query); } if (options?.sourceBranch) { params.set('sourceBranch', options.sourceBranch); } const response = await runtimeFetch( `/api/gitea/prs/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 Gitea pull requests'); } return payload; }, async prContext( directory: string, number: number, options?: { includeDiff?: boolean; owner?: string; repo?: string } ): Promise { const params = new URLSearchParams({ directory, number: String(number) }); if (options?.includeDiff) { params.set('includeDiff', '1'); } if (options?.owner) { params.set('owner', options.owner); } if (options?.repo) { params.set('repo', options.repo); } const response = await runtimeFetch(urls.api('/api/gitea/pr/context', 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 Gitea pull request context'); } return payload; }, async prCommits(directory: string, number: number, options?: { owner?: string; repo?: string }): Promise { const params = new URLSearchParams({ directory, number: String(number) }); if (options?.owner) { params.set('owner', options.owner); } if (options?.repo) { params.set('repo', options.repo); } const response = await runtimeFetch(urls.api('/api/gitea/prs/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 Gitea pull request commits'); } return payload; }, async prStatuses(directory: string, number: number, options?: { owner?: string; repo?: string }): Promise { const params = new URLSearchParams({ directory, number: String(number) }); if (options?.owner) { params.set('owner', options.owner); } if (options?.repo) { params.set('repo', options.repo); } const response = await runtimeFetch(urls.api('/api/gitea/prs/statuses', 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 Gitea pull request statuses'); } return payload; }, async prReviews(directory: string, number: number, options?: { owner?: string; repo?: string }): Promise { const params = new URLSearchParams({ directory, number: String(number) }); if (options?.owner) { params.set('owner', options.owner); } if (options?.repo) { params.set('repo', options.repo); } const response = await runtimeFetch(urls.api('/api/gitea/prs/reviews', 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 Gitea pull request reviews'); } return payload; }, async prCreate(input: GiteaPullRequestCreateInput): Promise { const response = await runtimeFetch('/api/gitea/pr/create', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const payload = await jsonOrNull(response); if (!response.ok || !payload?.pr) { throw new Error(payload?.error || response.statusText || 'Failed to create Gitea pull request'); } return payload.pr; }, async prUpdate(input: GiteaPullRequestUpdateInput): Promise { const response = await runtimeFetch('/api/gitea/pr/update', { method: 'PATCH', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const payload = await jsonOrNull(response); if (!response.ok || !payload?.pr) { throw new Error(payload?.error || response.statusText || 'Failed to update Gitea pull request'); } return payload.pr; }, async prMerge(input: GiteaPullRequestMergeInput): Promise { const response = await runtimeFetch('/api/gitea/pr/merge', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(input), }); const payload = await jsonOrNull(response); // The server rejects non-mergeable requests with 405/409/422 and a // `{ connected, merged: false, message }` body — parse it and return it // instead of throwing. Only throw when there is no parseable payload // (network failure) or the server surfaced a real `{ error }`. if (!payload) { throw new Error(response.statusText || 'Failed to merge Gitea pull request'); } if (payload.error) { throw new Error(payload.error); } return { connected: Boolean(payload.connected), merged: Boolean(payload.merged), ...(payload.message ? { message: payload.message } : {}), }; }, async issueComment(input: GiteaIssueCommentInput): Promise { const response = await runtimeFetch('/api/gitea/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 Gitea issue comment'); } return body; }, async issueCreate(input: GiteaIssueCreateInput): Promise { const response = await runtimeFetch('/api/gitea/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 Gitea issue'); } return body; }, async issueUpdate(input: GiteaIssueUpdateInput): Promise { const response = await runtimeFetch('/api/gitea/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 Gitea issue'); } return body; }, async prComment(input: GiteaIssueCommentInput): Promise { const response = await runtimeFetch('/api/gitea/prs/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 Gitea pull request comment'); } return body; }, async prSubmitReview(input: GiteaPullReviewInput): Promise { const response = await runtimeFetch('/api/gitea/prs/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 Gitea pull request review'); } return body; }, async repoLabels(directory: string, options?: { owner?: string; repo?: string }): Promise { const params = new URLSearchParams({ directory }); if (options?.owner) { params.set('owner', options.owner); } if (options?.repo) { params.set('repo', options.repo); } const response = await runtimeFetch( `/api/gitea/repo/labels?${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 fetch Gitea repo labels'); } return body; }, async repoBranches(owner: string, repo: string): Promise { const response = await runtimeFetch( `/api/gitea/repo/branches?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}`, { method: 'GET', headers: { Accept: 'application/json' } } ); const body = await jsonOrNull(response); if (!response.ok || !body) { throw new Error(body?.error || response.statusText || 'Failed to fetch Gitea repo branches'); } return { branches: body.branches ?? [], defaultBranch: body.defaultBranch ?? null, }; }, });