import { GITEA_URL, USERNAME, type Session } from './auth'; export interface Repo { name: string; url: string; repo: string; } export interface RepoFile { type: 'file' | 'dir'; name: string; message: string | undefined; } export interface RepoView { branch: string; files: RepoFile[]; readme: string; cloneUrl: string; } export interface PR { number: string | undefined; title: string | undefined; url: string | undefined; labels: string[]; } export interface PRDetails { title: string | undefined; state: string | undefined; body: string | undefined; headBranch: string | undefined; baseBranch: string | undefined; comments: Array<{ author: string | undefined; body: string | undefined }>; canMerge: boolean; isClosed: boolean; isMerged: boolean; } // Create a new repository export async function createRepo( { page }: Session, name: string, description = '', isPrivate = false, ): Promise<{ url: string; cloneUrl: string }> { await page.goto(`${GITEA_URL}/repo/create`, { waitUntil: 'load' }); await page.fill('input[name="repo_name"]', name); if (description) await page.fill('textarea[name="description"]', description); if (isPrivate) await page.check('input[name="private"]'); await Promise.all([ page.waitForNavigation({ waitUntil: 'load' }), page.getByRole('button', { name: 'Create Repository' }).click(), ]); const cloneUrl = (await page.locator('#repo-clone-https, input#clone-url').inputValue().catch(() => '')) || `${GITEA_URL}/${USERNAME}/${name}.git`; return { url: page.url(), cloneUrl }; } // List repos visible to the logged-in user export async function listRepos({ page }: Session): Promise { await page.goto(`${GITEA_URL}/${USERNAME}`, { waitUntil: 'load' }); const reposTab = page.locator('a[href*="?tab=repos"]'); if (await reposTab.count() > 0) { await reposTab.click(); await page.waitForLoadState('load'); } return page.evaluate((username: string) => { const seen = new Set(); return [...document.querySelectorAll(`a[href^="/${username}/"]`)] .filter(a => { const parts = (a.getAttribute('href') ?? '').split('/').filter(Boolean); return parts.length === 2; }) .map(a => ({ name: a.textContent?.trim() ?? '', url: a.href, repo: (a.getAttribute('href') ?? '').split('/')[2] ?? '', })) .filter(r => r.name && !seen.has(r.url) && !!seen.add(r.url)); }, USERNAME); } // View a repo's root: file list, default branch, README excerpt export async function viewRepo({ page }: Session, owner: string, repo: string): Promise { await page.goto(`${GITEA_URL}/${owner}/${repo}`, { waitUntil: 'load' }); return page.evaluate((): RepoView => { const files = [...document.querySelectorAll('table.files tr, .repository.file.list table tr')] .slice(1) .map(row => { const icon = row.querySelector('svg, .octicon'); const type: 'file' | 'dir' = icon?.classList.contains('octicon-file-directory') || icon?.getAttribute('aria-label')?.includes('dir') ? 'dir' : 'file'; const nameEl = row.querySelector('td.name a, td a.muted'); const msgEl = row.querySelector('td.message a'); return nameEl ? { type, name: nameEl.textContent?.trim() ?? '', message: msgEl?.textContent?.trim() } : null; }) .filter((f): f is RepoFile => f !== null); const branch = document.querySelector('.branch-dropdown-button, [data-clipboard-text]')?.textContent?.trim() || (document.querySelector('input[name="ref"]') as HTMLInputElement | null)?.value || ''; const readme = document.querySelector('#readme .markdown-body, #readme article, .plain-text pre') ?.textContent?.slice(0, 3000) ?? ''; const cloneUrl = (document.querySelector('#repo-clone-https, input#clone-url') as HTMLInputElement | null)?.value ?? ''; return { branch, files, readme, cloneUrl }; }); } // Get raw file content export async function viewFile( { page }: Session, owner: string, repo: string, branch: string, filepath: string, ): Promise { const url = `${GITEA_URL}/${owner}/${repo}/raw/branch/${encodeURIComponent(branch)}/${filepath}`; const response = await page.goto(url, { waitUntil: 'load' }); if (!response) throw new Error('No response received'); return response.text(); } // List pull requests for a repo export async function listPRs( { page }: Session, owner: string, repo: string, state: 'open' | 'closed' | 'all' = 'open', ): Promise { await page.goto(`${GITEA_URL}/${owner}/${repo}/pulls?state=${state}&type=pullrequests`, { waitUntil: 'load' }); return page.evaluate((): PR[] => { return [...document.querySelectorAll('.issue.list .item')] .map(el => { const titleEl = el.querySelector('.title, a.title'); const numberEl = el.querySelector('.index'); const labels = [...el.querySelectorAll('.label')].map(l => l.textContent?.trim() ?? ''); return { number: numberEl?.textContent?.replace('#', '').trim(), title: titleEl?.textContent?.trim(), url: titleEl?.href ?? el.querySelector('a')?.href, labels, }; }) .filter(pr => pr.title); }); } // Create a new pull request export async function createPR( { page }: Session, owner: string, repo: string, head: string, base: string, title: string, body = '', ): Promise<{ url: string; number: string | undefined }> { await page.goto(`${GITEA_URL}/${owner}/${repo}/compare/${base}...${head}`, { waitUntil: 'load' }); await page.fill('input[name="title"]', title); if (body) { await page.locator('textarea[name="content"], #content').fill(body); } await Promise.all([ page.waitForNavigation({ waitUntil: 'load' }), page.locator('button[type="submit"]:has-text("Create"), input[type="submit"]').first().click(), ]); return { url: page.url(), number: page.url().match(/\/pulls\/(\d+)/)?.[1], }; } // View a specific pull request: details + comments export async function viewPR( { page }: Session, owner: string, repo: string, number: number, ): Promise { await page.goto(`${GITEA_URL}/${owner}/${repo}/pulls/${number}`, { waitUntil: 'load' }); return page.evaluate((): PRDetails => { const comments = [...document.querySelectorAll('.comment:not(.first)')] .map(c => ({ author: c.querySelector('.author')?.textContent?.trim(), body: c.querySelector('.render-content')?.textContent?.trim(), })) .filter(c => c.body); return { title: document.querySelector('.issue-title, h1.issue-title')?.textContent?.trim(), state: document.querySelector('.issue-state-label, .label.green, .label.red, .label.purple')?.textContent?.trim(), body: document.querySelector('.comment.first .render-content, .post-content')?.textContent?.trim(), headBranch: document.querySelector('.head-branch, .compare-branch')?.textContent?.trim(), baseBranch: document.querySelector('.base-branch')?.textContent?.trim(), comments, canMerge: !!document.querySelector('button.merge-button, .merge-section button[type="submit"]'), isClosed: !!document.querySelector('.issue-state-label')?.textContent?.toLowerCase().includes('closed'), isMerged: !!document.querySelector('.merged-section, .label.purple'), }; }); } // Comment on a pull request export async function commentPR( { page }: Session, owner: string, repo: string, number: number, body: string, ): Promise<{ url: string }> { await page.goto(`${GITEA_URL}/${owner}/${repo}/pulls/${number}`, { waitUntil: 'load' }); await page.locator('textarea[name="content"]').last().fill(body); await Promise.all([ page.waitForNavigation({ waitUntil: 'load' }), page.locator('button[type="submit"]:has-text("Comment")').last().click(), ]); return { url: page.url() }; } // Merge a pull request export async function mergePR( { page }: Session, owner: string, repo: string, number: number, ): Promise<{ url: string; merged: true }> { await page.goto(`${GITEA_URL}/${owner}/${repo}/pulls/${number}`, { waitUntil: 'load' }); const mergeBtn = page.locator('button.merge-button, .merge-section button[type="submit"]').first(); await mergeBtn.waitFor({ state: 'visible' }); await mergeBtn.click(); await page.waitForLoadState('load'); return { url: page.url(), merged: true }; } // Close a pull request without merging export async function closePR( { page }: Session, owner: string, repo: string, number: number, ): Promise<{ url: string; closed: true }> { await page.goto(`${GITEA_URL}/${owner}/${repo}/pulls/${number}`, { waitUntil: 'load' }); const closeBtn = page.locator('button:has-text("Close"), a:has-text("Close Pull Request")').first(); await closeBtn.waitFor({ state: 'visible' }); await closeBtn.click(); await page.waitForLoadState('load'); return { url: page.url(), closed: true }; }