Files
Aperant/auto-claude-ui/src/main/ipc-handlers/github/issue-handlers.ts
T
Amen-Ra Mendel 873cafa46f Fix GitHub organization repository support
## Summary
- Add support for organization repositories by including org members in repo list API
- Add repository reference normalization to handle full GitHub URLs, SSH URLs, and owner/repo format
- Apply normalization to all GitHub API calls to ensure consistent behavior

## Changes Made

### 1. Enhanced repo listing (repository-handlers.ts)
- Updated `/user/repos` endpoint to include affiliation parameter
- Now fetches: owner, collaborator, and organization_member repos
- Fixes #20: Organization repos now appear in repository list

### 2. Added URL normalization utility (utils.ts)
- New `normalizeRepoReference()` function handles:
  - owner/repo format (already normalized)
  - https://github.com/owner/repo URLs
  - https://github.com/owner/repo.git URLs
  - git@github.com:owner/repo.git SSH URLs
- Prevents 404 errors from malformed repository references

### 3. Applied normalization consistently
- Updated repository-handlers.ts to normalize repo refs before API calls
- Updated issue-handlers.ts to normalize repo refs before API calls
- All GitHub API calls now use normalized repository format

## Testing
- Verified with organization repository: imaginationeverywhere/ppsv-charities
- Tested with various URL formats
- GitHub CLI authentication continues to work seamlessly

## Related Issues
Fixes #20: Auto Claude doesn't work with GitHub Organization repositories
2025-12-19 05:22:41 -05:00

185 lines
5.2 KiB
TypeScript

/**
* GitHub issue-related IPC handlers
*/
import { ipcMain } from 'electron';
import { IPC_CHANNELS } from '../../../shared/constants';
import type { IPCResult, GitHubIssue } from '../../../shared/types';
import { projectStore } from '../../project-store';
import { getGitHubConfig, githubFetch, normalizeRepoReference } from './utils';
import type { GitHubAPIIssue, GitHubAPIComment } from './types';
/**
* Transform GitHub API issue to application format
*/
function transformIssue(issue: GitHubAPIIssue, repoFullName: string): GitHubIssue {
return {
id: issue.id,
number: issue.number,
title: issue.title,
body: issue.body,
state: issue.state,
labels: issue.labels,
assignees: issue.assignees.map(a => ({
login: a.login,
avatarUrl: a.avatar_url
})),
author: {
login: issue.user.login,
avatarUrl: issue.user.avatar_url
},
milestone: issue.milestone,
createdAt: issue.created_at,
updatedAt: issue.updated_at,
closedAt: issue.closed_at,
commentsCount: issue.comments,
url: issue.url,
htmlUrl: issue.html_url,
repoFullName
};
}
/**
* Get list of issues from repository
*/
export function registerGetIssues(): void {
ipcMain.handle(
IPC_CHANNELS.GITHUB_GET_ISSUES,
async (_, projectId: string, state: 'open' | 'closed' | 'all' = 'open'): Promise<IPCResult<GitHubIssue[]>> => {
const project = projectStore.getProject(projectId);
if (!project) {
return { success: false, error: 'Project not found' };
}
const config = getGitHubConfig(project);
if (!config) {
return { success: false, error: 'No GitHub token or repository configured' };
}
try {
const normalizedRepo = normalizeRepoReference(config.repo);
if (!normalizedRepo) {
return {
success: false,
error: 'Invalid repository format. Use owner/repo or GitHub URL.'
};
}
const issues = await githubFetch(
config.token,
`/repos/${normalizedRepo}/issues?state=${state}&per_page=100&sort=updated`
) as GitHubAPIIssue[];
// Filter out pull requests
const issuesOnly = issues.filter(issue => !issue.pull_request);
const result: GitHubIssue[] = issuesOnly.map(issue =>
transformIssue(issue, normalizedRepo)
);
return { success: true, data: result };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch issues'
};
}
}
);
}
/**
* Get a single issue by number
*/
export function registerGetIssue(): void {
ipcMain.handle(
IPC_CHANNELS.GITHUB_GET_ISSUE,
async (_, projectId: string, issueNumber: number): Promise<IPCResult<GitHubIssue>> => {
const project = projectStore.getProject(projectId);
if (!project) {
return { success: false, error: 'Project not found' };
}
const config = getGitHubConfig(project);
if (!config) {
return { success: false, error: 'No GitHub token or repository configured' };
}
try {
const normalizedRepo = normalizeRepoReference(config.repo);
if (!normalizedRepo) {
return {
success: false,
error: 'Invalid repository format. Use owner/repo or GitHub URL.'
};
}
const issue = await githubFetch(
config.token,
`/repos/${normalizedRepo}/issues/${issueNumber}`
) as GitHubAPIIssue;
const result = transformIssue(issue, normalizedRepo);
return { success: true, data: result };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch issue'
};
}
}
);
}
/**
* Get comments for a specific issue
*/
export function registerGetIssueComments(): void {
ipcMain.handle(
IPC_CHANNELS.GITHUB_GET_ISSUE_COMMENTS,
async (_, projectId: string, issueNumber: number): Promise<IPCResult<GitHubAPIComment[]>> => {
const project = projectStore.getProject(projectId);
if (!project) {
return { success: false, error: 'Project not found' };
}
const config = getGitHubConfig(project);
if (!config) {
return { success: false, error: 'No GitHub token or repository configured' };
}
try {
const normalizedRepo = normalizeRepoReference(config.repo);
if (!normalizedRepo) {
return {
success: false,
error: 'Invalid repository format. Use owner/repo or GitHub URL.'
};
}
const comments = await githubFetch(
config.token,
`/repos/${normalizedRepo}/issues/${issueNumber}/comments`
) as GitHubAPIComment[];
return { success: true, data: comments };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch issue comments'
};
}
}
);
}
/**
* Register all issue-related handlers
*/
export function registerIssueHandlers(): void {
registerGetIssues();
registerGetIssue();
registerGetIssueComments();
}