mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
The GitLab, GitHub, Jira and Linear integration modules, their two IPC registrars, and the shared GitHub project types each carried a file-level `eslint-disable max-lines` and ran 351-614 counted lines against a 300-line budget. AGENTS.md calls for splitting rather than suppressing, and config/max-lines-baseline.txt is a shrink-only ratchet, so this removes all eight suppressions and prunes their entries (341 -> 333). Pure move, no behavior change. Each client is cut along the seam it already had: per-operation modules for the issue APIs (create / update / comment / field options), and for Jira the request queue, site credential store, authenticated request, and site identity. The two IPC registrars keep their own handlers and delegate the rest to per-domain sub-registrars, so they remain real entry points rather than re-export shims. The IPC surface is proved intact rather than assumed: comparing (method, channel) multisets between HEAD and the split gives 52 registrations across 52 distinct channels on both sides. Provider-neutrality is preserved -- GitLab and GitHub keep separate, parallel module layouts rather than being merged behind a shared abstraction. Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green (the one remaining failure is a pre-existing load flake in an untouched file, green when re-run serially), no new runtime import cycles among 744 modules, and no lint suppression added anywhere.
170 lines
5.8 KiB
TypeScript
170 lines
5.8 KiB
TypeScript
import type { ClassifiedError } from '../../shared/classified-error'
|
|
import type { IssueInfo } from '../../shared/github/pull-request-types'
|
|
import type { IssueSourcePreference } from '../../shared/repo-types'
|
|
import { mapIssueInfo } from './mappers'
|
|
import type { LocalGitExecOptions } from './gh-utils'
|
|
import {
|
|
getIssueGitHubApiRepository,
|
|
resolveGitHubRepoExecution,
|
|
resolveIssueGitHubApiRepositorySource
|
|
} from './github-api-repository'
|
|
// prettier-ignore
|
|
import { ghExecFileAsync, acquire, release, classifyListIssuesError } from './gh-utils'
|
|
|
|
// Why: distinguishes a successful-empty listing from a failed fetch. The
|
|
// previous `catch { return [] }` conflated a 403 on a private upstream with an
|
|
// empty backlog. Callers decide how to surface `error`.
|
|
//
|
|
// Why no `fellBack` here: the fell-back signal for the renderer toast rides on
|
|
// `ListWorkItemsResult.issueSourceFellBack` (the Tasks list's envelope). The
|
|
// only consumer of `listIssues` — the `gh:listIssues` IPC handler — unwraps
|
|
// to `.items` and has no UI hook to surface a fallback toast. Adding a dead
|
|
// `fellBack` field here invited drift between the JSDoc promise and reality.
|
|
export type IssueListResult = {
|
|
items: IssueInfo[]
|
|
error?: ClassifiedError
|
|
}
|
|
|
|
/**
|
|
* Get a single issue by number.
|
|
* Uses gh api --cache so 304 Not Modified responses don't count against the rate limit.
|
|
*
|
|
* Why this path doesn't take a preference: linked-issue lookups persist a
|
|
* number to a worktree at creation time. Routing detail lookups through the
|
|
* live per-repo preference would silently flip an existing link to a
|
|
* different repo after the user toggled the selector — the opposite of what
|
|
* #1186 / the parent design doc guard against. List and create paths honor
|
|
* preference; number-resolution stays on the heuristic.
|
|
*/
|
|
export async function getIssue(
|
|
repoPath: string,
|
|
issueNumber: number,
|
|
connectionId?: string | null,
|
|
localGitOptions: LocalGitExecOptions = {}
|
|
): Promise<IssueInfo | null> {
|
|
const { ownerRepo, ghOptions } = await resolveGitHubRepoExecution(
|
|
repoPath,
|
|
() => getIssueGitHubApiRepository(repoPath, connectionId, localGitOptions),
|
|
connectionId,
|
|
localGitOptions
|
|
)
|
|
// Why: a connection-backed request has no local cwd, so the non-GitHub
|
|
// fallback below would let gh target its default repository. Refuse instead.
|
|
if (connectionId && !ownerRepo) {
|
|
return null
|
|
}
|
|
await acquire()
|
|
try {
|
|
if (ownerRepo) {
|
|
const { stdout } = await ghExecFileAsync(
|
|
[
|
|
'api',
|
|
'--cache',
|
|
'300s',
|
|
`repos/${ownerRepo.owner}/${ownerRepo.repo}/issues/${issueNumber}`
|
|
],
|
|
ghOptions
|
|
)
|
|
const data = JSON.parse(stdout)
|
|
return mapIssueInfo(data)
|
|
}
|
|
// Fallback for non-GitHub remotes
|
|
const { stdout } = await ghExecFileAsync(
|
|
['issue', 'view', String(issueNumber), '--json', 'number,title,state,url,labels,body'],
|
|
ghOptions
|
|
)
|
|
const data = JSON.parse(stdout)
|
|
return mapIssueInfo(data)
|
|
} catch {
|
|
return null
|
|
} finally {
|
|
release()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List issues for a repo.
|
|
* Uses gh api --cache so 304 Not Modified responses don't count against the rate limit.
|
|
*
|
|
* Why: returns a structured result so a 403 (e.g. fork contributor without
|
|
* read access to a private upstream) surfaces as an error the UI can render
|
|
* instead of collapsing to "No issues". The empty-list-on-error behavior this
|
|
* replaces was explicitly flagged as a merge-blocker in the parent design doc
|
|
* (§3) — silently hiding failures re-creates the same silent-source-switch
|
|
* class of wrongness #1186 warned against, one level deeper.
|
|
*/
|
|
export async function listIssues(
|
|
repoPath: string,
|
|
limit = 20,
|
|
preference?: IssueSourcePreference,
|
|
connectionId?: string | null,
|
|
localGitOptions: LocalGitExecOptions = {}
|
|
): Promise<IssueListResult> {
|
|
const { ownerRepo, ghOptions } = await resolveGitHubRepoExecution(
|
|
repoPath,
|
|
async () =>
|
|
(
|
|
await resolveIssueGitHubApiRepositorySource(
|
|
repoPath,
|
|
preference,
|
|
connectionId,
|
|
localGitOptions
|
|
)
|
|
).source,
|
|
connectionId,
|
|
localGitOptions
|
|
)
|
|
// Why: a connection-backed request has no local cwd, so the non-GitHub
|
|
// fallback below would let gh list its default repository. Refuse instead.
|
|
if (connectionId && !ownerRepo) {
|
|
return {
|
|
items: [],
|
|
error: {
|
|
type: 'not_found',
|
|
message: 'Could not resolve GitHub owner/repo for this repository'
|
|
}
|
|
}
|
|
}
|
|
await acquire()
|
|
try {
|
|
if (ownerRepo) {
|
|
const { stdout } = await ghExecFileAsync(
|
|
[
|
|
'api',
|
|
'--cache',
|
|
'120s',
|
|
`repos/${ownerRepo.owner}/${ownerRepo.repo}/issues?per_page=${limit}&state=open&sort=updated&direction=desc`
|
|
],
|
|
ghOptions
|
|
)
|
|
const data = JSON.parse(stdout) as Record<string, unknown>[]
|
|
// Why: the GitHub REST `/repos/{owner}/{repo}/issues` endpoint returns
|
|
// pull requests alongside issues (PRs carry a `pull_request` key).
|
|
// Strip them here so `listIssues` only returns true issues, matching the
|
|
// filter applied in `listRecentWorkItems` (src/main/github/client.ts).
|
|
return {
|
|
items: data
|
|
.filter((d) => !('pull_request' in d))
|
|
.map((d) => mapIssueInfo(d as Parameters<typeof mapIssueInfo>[0]))
|
|
}
|
|
}
|
|
// Fallback for non-GitHub remotes
|
|
const { stdout } = await ghExecFileAsync(
|
|
['issue', 'list', '--json', 'number,title,state,url,labels', '--limit', String(limit)],
|
|
ghOptions
|
|
)
|
|
const data = JSON.parse(stdout) as unknown[]
|
|
return {
|
|
items: data.map((d) => mapIssueInfo(d as Parameters<typeof mapIssueInfo>[0]))
|
|
}
|
|
} catch (err) {
|
|
const stderr = err instanceof Error ? err.message : String(err)
|
|
return {
|
|
items: [],
|
|
error: classifyListIssuesError(stderr)
|
|
}
|
|
} finally {
|
|
release()
|
|
}
|
|
}
|