import { resolveRepoFromDirectory } from './githubPr'; const API_BASE = 'https://api.github.com'; type JsonRecord = Record; type GitHubRepoRef = { owner: string; repo: string; url: string }; type GitHubIssuesListResult = { connected: boolean; repo?: GitHubRepoRef | null; issues?: Array<{ number: number; title: string; url: string; state: 'open' | 'closed'; author?: { login: string; id?: number; avatarUrl?: string; name?: string; email?: string } | null; labels?: Array<{ name: string; color?: string }>; }>; page?: number; hasMore?: boolean; }; type GitHubIssueGetResult = { connected: boolean; repo?: GitHubRepoRef | null; issue?: { number: number; title: string; url: string; state: 'open' | 'closed'; author?: { login: string; id?: number; avatarUrl?: string; name?: string; email?: string } | null; labels?: Array<{ name: string; color?: string }>; body?: string; assignees?: Array<{ login: string; id?: number; avatarUrl?: string; name?: string; email?: string }>; createdAt?: string; updatedAt?: string; } | null; }; type GitHubIssueCommentsResult = { connected: boolean; repo?: GitHubRepoRef | null; comments?: Array<{ id: number; url: string; body: string; author?: { login: string; id?: number; avatarUrl?: string; name?: string; email?: string } | null; createdAt?: string; updatedAt?: string; }>; }; const githubFetch = async ( url: string, accessToken: string, init?: RequestInit, ): Promise => { return fetch(url, { ...init, headers: { Accept: 'application/vnd.github+json', Authorization: `Bearer ${accessToken}`, 'User-Agent': 'OpenChamber', ...(init?.headers || {}), }, }); }; const jsonOrNull = async (response: Response): Promise => { return (await response.json().catch(() => null)) as T | null; }; const readString = (value: unknown): string => (typeof value === 'string' ? value : ''); const mapUser = (raw: unknown) => { const rec = raw && typeof raw === 'object' ? (raw as JsonRecord) : null; const login = readString(rec?.login); if (!login) return null; return { login, id: typeof rec?.id === 'number' ? rec.id : undefined, avatarUrl: readString(rec?.avatar_url) || undefined, name: undefined, email: undefined, }; }; const mapLabels = (raw: unknown): Array<{ name: string; color?: string }> => { const list = Array.isArray(raw) ? raw : []; return list .map((item) => { const rec = item && typeof item === 'object' ? (item as JsonRecord) : null; const name = readString(rec?.name); if (!name) return null; return { name, color: readString(rec?.color) || undefined, }; }) .filter(Boolean) as Array<{ name: string; color?: string }>; }; export const listIssues = async ( accessToken: string, directory: string, page: number = 1, ): Promise => { const repo = await resolveRepoFromDirectory(directory); if (!repo) { return { connected: true, repo: null, issues: [] }; } const url = new URL(`${API_BASE}/repos/${repo.owner}/${repo.repo}/issues`); url.searchParams.set('state', 'open'); url.searchParams.set('per_page', '50'); url.searchParams.set('page', String(page)); const resp = await githubFetch(url.toString(), accessToken); if (resp.status === 401) { return { connected: false }; } const link = resp.headers.get('link') || ''; const hasMore = /rel="next"/.test(link); const json = await jsonOrNull(resp); if (!resp.ok || !Array.isArray(json)) { throw new Error('Failed to load issues'); } const issues = json .map((entry) => { const rec = entry && typeof entry === 'object' ? (entry as JsonRecord) : null; if (!rec || rec.pull_request) return null; const number = typeof rec.number === 'number' ? rec.number : 0; if (!number) return null; const state = readString(rec.state) === 'closed' ? 'closed' : 'open'; return { number, title: readString(rec.title) || '', url: readString(rec.html_url) || '', state, author: mapUser(rec.user), labels: mapLabels(rec.labels), }; }) .filter(Boolean) as GitHubIssuesListResult['issues']; return { connected: true, repo, issues: issues || [], page, hasMore }; }; export const getIssue = async ( accessToken: string, directory: string, number: number, ): Promise => { const repo = await resolveRepoFromDirectory(directory); if (!repo) { return { connected: true, repo: null, issue: null }; } const resp = await githubFetch(`${API_BASE}/repos/${repo.owner}/${repo.repo}/issues/${number}`, accessToken); if (resp.status === 401) { return { connected: false }; } const json = await jsonOrNull(resp); if (!resp.ok || !json) { throw new Error('Failed to load issue'); } if (json.pull_request) { throw new Error('Not a GitHub issue'); } const state = readString(json.state) === 'closed' ? 'closed' : 'open'; const assigneesRaw = Array.isArray(json.assignees) ? json.assignees : []; const assignees = assigneesRaw.map(mapUser).filter(Boolean) as Array>>; return { connected: true, repo, issue: { number: typeof json.number === 'number' ? json.number : number, title: readString(json.title) || '', url: readString(json.html_url) || '', state, author: mapUser(json.user), labels: mapLabels(json.labels), body: readString(json.body) || '', assignees, createdAt: readString(json.created_at) || undefined, updatedAt: readString(json.updated_at) || undefined, }, }; }; export const listIssueComments = async ( accessToken: string, directory: string, number: number, ): Promise => { const repo = await resolveRepoFromDirectory(directory); if (!repo) { return { connected: true, repo: null, comments: [] }; } const url = new URL(`${API_BASE}/repos/${repo.owner}/${repo.repo}/issues/${number}/comments`); url.searchParams.set('per_page', '100'); const resp = await githubFetch(url.toString(), accessToken); if (resp.status === 401) { return { connected: false }; } const json = await jsonOrNull(resp); if (!resp.ok || !Array.isArray(json)) { throw new Error('Failed to load issue comments'); } const comments = json .map((entry) => { const rec = entry && typeof entry === 'object' ? (entry as JsonRecord) : null; if (!rec) return null; const id = typeof rec.id === 'number' ? rec.id : 0; if (!id) return null; return { id, url: readString(rec.html_url) || '', body: readString(rec.body) || '', author: mapUser(rec.user), createdAt: readString(rec.created_at) || undefined, updatedAt: readString(rec.updated_at) || undefined, }; }) .filter(Boolean) as GitHubIssueCommentsResult['comments']; return { connected: true, repo, comments: comments || [] }; };