229 lines
7.3 KiB
TypeScript
229 lines
7.3 KiB
TypeScript
import { ipcMain } from 'electron'
|
|
import { resolve } from 'path'
|
|
import type { Repo, GitHubIssueUpdate } from '../../shared/types'
|
|
import type { Store } from '../persistence'
|
|
import type { StatsCollector } from '../stats/collector'
|
|
import {
|
|
getPRForBranch,
|
|
getIssue,
|
|
getRepoSlug,
|
|
listIssues,
|
|
listWorkItems,
|
|
countWorkItems,
|
|
getWorkItem,
|
|
createIssue,
|
|
updateIssue,
|
|
addIssueComment,
|
|
listLabels,
|
|
listAssignableUsers,
|
|
getAuthenticatedViewer,
|
|
getPRChecks,
|
|
getPRComments,
|
|
resolveReviewThread,
|
|
updatePRTitle,
|
|
mergePR,
|
|
checkOrcaStarred,
|
|
starOrca
|
|
} from '../github/client'
|
|
import { getWorkItemDetails, getPRFileContents } from '../github/work-item-details'
|
|
import type { GitHubPRFile } from '../../shared/types'
|
|
|
|
// Why: returns the full Repo object instead of just the path string so that
|
|
// callers have access to repo.id for stat tracking and other context.
|
|
function assertRegisteredRepo(repoPath: string, store: Store): Repo {
|
|
const resolvedRepoPath = resolve(repoPath)
|
|
const repo = store.getRepos().find((r) => resolve(r.path) === resolvedRepoPath)
|
|
if (!repo) {
|
|
throw new Error('Access denied: unknown repository path')
|
|
}
|
|
return repo
|
|
}
|
|
|
|
export function registerGitHubHandlers(store: Store, stats: StatsCollector): void {
|
|
ipcMain.handle('gh:prForBranch', async (_event, args: { repoPath: string; branch: string }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
const pr = await getPRForBranch(repo.path, args.branch)
|
|
// Emit pr_created when a PR is first detected for a branch.
|
|
// Why here: the renderer polls gh:prForBranch to check PR status per worktree.
|
|
// This captures PRs opened from any workflow (Orca UI, gh CLI, github.com).
|
|
if (pr && !stats.hasCountedPR(pr.url)) {
|
|
stats.record({
|
|
type: 'pr_created',
|
|
at: Date.now(),
|
|
repoId: repo.id,
|
|
meta: { prNumber: pr.number, prUrl: pr.url }
|
|
})
|
|
}
|
|
return pr
|
|
})
|
|
|
|
ipcMain.handle('gh:issue', (_event, args: { repoPath: string; number: number }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return getIssue(repo.path, args.number)
|
|
})
|
|
|
|
ipcMain.handle('gh:listIssues', (_event, args: { repoPath: string; limit?: number }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return listIssues(repo.path, args.limit)
|
|
})
|
|
|
|
ipcMain.handle(
|
|
'gh:createIssue',
|
|
(_event, args: { repoPath: string; title: string; body: string }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return createIssue(repo.path, args.title, args.body)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gh:listWorkItems',
|
|
(_event, args: { repoPath: string; limit?: number; query?: string; before?: string }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return listWorkItems(repo.path, args.limit, args.query, args.before)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle('gh:countWorkItems', (_event, args: { repoPath: string; query?: string }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return countWorkItems(repo.path, args.query)
|
|
})
|
|
|
|
ipcMain.handle('gh:workItem', (_event, args: { repoPath: string; number: number }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return getWorkItem(repo.path, args.number)
|
|
})
|
|
|
|
ipcMain.handle('gh:workItemDetails', (_event, args: { repoPath: string; number: number }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return getWorkItemDetails(repo.path, args.number)
|
|
})
|
|
|
|
ipcMain.handle(
|
|
'gh:prFileContents',
|
|
(
|
|
_event,
|
|
args: {
|
|
repoPath: string
|
|
prNumber: number
|
|
path: string
|
|
oldPath?: string
|
|
status: GitHubPRFile['status']
|
|
headSha: string
|
|
baseSha: string
|
|
}
|
|
) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return getPRFileContents({
|
|
repoPath: repo.path,
|
|
prNumber: args.prNumber,
|
|
path: args.path,
|
|
oldPath: args.oldPath,
|
|
status: args.status,
|
|
headSha: args.headSha,
|
|
baseSha: args.baseSha
|
|
})
|
|
}
|
|
)
|
|
|
|
ipcMain.handle('gh:repoSlug', (_event, args: { repoPath: string }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return getRepoSlug(repo.path)
|
|
})
|
|
|
|
ipcMain.handle(
|
|
'gh:prChecks',
|
|
(
|
|
_event,
|
|
args: {
|
|
repoPath: string
|
|
prNumber: number
|
|
headSha?: string
|
|
noCache?: boolean
|
|
}
|
|
) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return getPRChecks(repo.path, args.prNumber, args.headSha, {
|
|
noCache: args.noCache
|
|
})
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gh:prComments',
|
|
(_event, args: { repoPath: string; prNumber: number; noCache?: boolean }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return getPRComments(repo.path, args.prNumber, { noCache: args.noCache })
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gh:resolveReviewThread',
|
|
(_event, args: { repoPath: string; threadId: string; resolve: boolean }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return resolveReviewThread(repo.path, args.threadId, args.resolve)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gh:updatePRTitle',
|
|
(_event, args: { repoPath: string; prNumber: number; title: string }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return updatePRTitle(repo.path, args.prNumber, args.title)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gh:mergePR',
|
|
(
|
|
_event,
|
|
args: { repoPath: string; prNumber: number; method?: 'merge' | 'squash' | 'rebase' }
|
|
) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return mergePR(repo.path, args.prNumber, args.method)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gh:updateIssue',
|
|
(_event, args: { repoPath: string; number: number; updates: GitHubIssueUpdate }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
if (typeof args.number !== 'number' || !Number.isInteger(args.number) || args.number < 1) {
|
|
return { ok: false, error: 'Invalid issue number' }
|
|
}
|
|
if (!args.updates || typeof args.updates !== 'object') {
|
|
return { ok: false, error: 'Updates object is required' }
|
|
}
|
|
return updateIssue(repo.path, args.number, args.updates)
|
|
}
|
|
)
|
|
|
|
ipcMain.handle(
|
|
'gh:addIssueComment',
|
|
(_event, args: { repoPath: string; number: number; body: string }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
if (typeof args.number !== 'number' || !Number.isInteger(args.number) || args.number < 1) {
|
|
return { ok: false, error: 'Invalid issue number' }
|
|
}
|
|
if (!args.body?.trim()) {
|
|
return { ok: false, error: 'Comment body required' }
|
|
}
|
|
return addIssueComment(repo.path, args.number, args.body.trim())
|
|
}
|
|
)
|
|
|
|
ipcMain.handle('gh:listLabels', (_event, args: { repoPath: string }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return listLabels(repo.path)
|
|
})
|
|
|
|
ipcMain.handle('gh:listAssignableUsers', (_event, args: { repoPath: string }) => {
|
|
const repo = assertRegisteredRepo(args.repoPath, store)
|
|
return listAssignableUsers(repo.path)
|
|
})
|
|
|
|
// Star operations target the Orca repo itself — no repoPath validation needed
|
|
ipcMain.handle('gh:viewer', () => getAuthenticatedViewer())
|
|
ipcMain.handle('gh:checkOrcaStarred', () => checkOrcaStarred())
|
|
ipcMain.handle('gh:starOrca', () => starOrca())
|
|
}
|