/** * Main-process gitea bridge. All gitea traffic runs here — the token never * reaches the renderer (which is CSP-locked to 'self' anyway). The renderer * calls these over IPC (see preload). Config for the dogfood slice comes from * the environment or the repo's .env.local; Settings/Onboarding wire it up * properly later. */ import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { createGiteaClient, type GiteaConfig, type LifecycleEvent } from '@commitea/core' import { ipcMain } from 'electron' /** Walk up from cwd looking for a .env.local with a GITEA_TOKEN (dev convenience). */ function loadEnvLocalToken(): string | undefined { let dir = process.cwd() for (let i = 0; i < 6; i++) { try { const txt = readFileSync(join(dir, '.env.local'), 'utf8') const m = /^GITEA_TOKEN\s*=\s*(.+?)\s*$/m.exec(txt) if (m) return m[1].trim() } catch { // not in this dir — keep walking up } const parent = dirname(dir) if (parent === dir) break dir = parent } return undefined } function resolveConfig(): GiteaConfig | null { // E2E runs against fixtures — never hit the network from the test harness. if (process.env.COMMITEA_E2E === '1') return null const token = process.env.GITEA_TOKEN ?? loadEnvLocalToken() if (!token) return null return { baseUrl: process.env.GITEA_BASE_URL ?? 'https://gitea.stephenmann.io', token, owner: process.env.GITEA_OWNER ?? 'christian', repo: process.env.GITEA_REPO ?? 'commitea', } } export function registerGiteaIpc(): void { const config = resolveConfig() const client = config ? createGiteaClient(config, fetch) : null const repo = config ? `${config.owner}/${config.repo}` : null ipcMain.handle('gitea:status', () => ({ configured: !!config, repo })) ipcMain.handle('gitea:reconcile', async () => { if (!client) return { configured: false, issues: [], milestones: [], deps: [], timelines: {} } const [issues, milestones] = await Promise.all([client.listIssues(), client.listMilestones()]) // dependency edges among the open scope (the scheduler only plans what's left) const open = issues.filter((i) => i.state === 'open') const perIssue = await Promise.all( open.map(async (i) => ({ issue: i.number, dependsOn: await client.getIssueDependencies(i.number) })), ) const deps = perIssue.flatMap(({ issue, dependsOn }) => dependsOn.map((d) => ({ issue, dependsOn: d }))) // lifecycle timelines for every issue (open → columns/badges, closed → calibration actuals) const timelineEntries = await Promise.all( issues.map(async (i) => [i.number, await client.getIssueTimeline(i.number)] as const), ) const timelines: Record = Object.fromEntries(timelineEntries) return { configured: true, issues, milestones, deps, timelines } }) ipcMain.handle('gitea:getIssue', async (_event, index: number) => { if (!client) return null return client.getIssue(index) }) }