2026-06-01 00:59:22 +00:00
|
|
|
import axios from 'axios';
|
2026-06-02 23:42:07 +00:00
|
|
|
import { Config, Issue, Comment, RepoItem, Label } from './types.js';
|
2026-06-01 00:59:22 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Normalizes the Gitea/Forgejo URL by stripping trailing slashes
|
|
|
|
|
* and ensuring it has a protocol (defaults to https).
|
|
|
|
|
*/
|
|
|
|
|
export function normalizeUrl(url: string): string {
|
|
|
|
|
let cleaned = url.trim();
|
|
|
|
|
if (!cleaned) return '';
|
|
|
|
|
if (!/^https?:\/\//i.test(cleaned)) {
|
|
|
|
|
cleaned = 'https://' + cleaned;
|
|
|
|
|
}
|
|
|
|
|
return cleaned.replace(/\/+$/, '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates an Axios instance with standard Gitea/Forgejo headers.
|
|
|
|
|
*/
|
|
|
|
|
function createAxiosInstance(config: Config) {
|
|
|
|
|
const headers: Record<string, string> = {
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (config.token) {
|
|
|
|
|
headers['Authorization'] = `token ${config.token.trim()}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return axios.create({
|
|
|
|
|
baseURL: `${normalizeUrl(config.url)}/api/v1`,
|
|
|
|
|
headers,
|
|
|
|
|
timeout: 10000,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests the connection to the Gitea/Forgejo instance and validates that the repository exists.
|
|
|
|
|
* Returns the repository full name on success.
|
|
|
|
|
*/
|
|
|
|
|
export async function validateConnection(config: Config): Promise<string> {
|
|
|
|
|
const client = createAxiosInstance(config);
|
|
|
|
|
try {
|
|
|
|
|
const response = await client.get(`/repos/${config.owner}/${config.repo}`);
|
|
|
|
|
return response.data.full_name || `${config.owner}/${config.repo}`;
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.response) {
|
|
|
|
|
if (error.response.status === 401) {
|
|
|
|
|
throw new Error('Unauthorized: Invalid API Token or authentication failed.');
|
|
|
|
|
}
|
|
|
|
|
if (error.response.status === 404) {
|
|
|
|
|
throw new Error(`Repository "${config.owner}/${config.repo}" not found or is private.`);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`API Error: ${error.response.data?.message || error.message}`);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Failed to connect to ${config.url}: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fetches a page of issues for a specific repository.
|
|
|
|
|
*/
|
|
|
|
|
export async function fetchIssues(
|
|
|
|
|
config: Config,
|
|
|
|
|
options: {
|
|
|
|
|
page: number;
|
|
|
|
|
limit: number;
|
|
|
|
|
state: 'open' | 'closed' | 'all';
|
|
|
|
|
type: 'issues' | 'pulls' | 'all';
|
|
|
|
|
q: string;
|
2026-06-02 18:41:33 +00:00
|
|
|
sortField: 'created' | 'updated' | 'comments' | 'assignees';
|
2026-06-01 00:59:22 +00:00
|
|
|
sortOrder: 'asc' | 'desc';
|
|
|
|
|
}
|
|
|
|
|
): Promise<{ issues: Issue[]; totalCount: number }> {
|
|
|
|
|
const client = createAxiosInstance(config);
|
|
|
|
|
|
|
|
|
|
// Map our internal sort fields to Gitea API sort parameters
|
|
|
|
|
let sort = 'newest';
|
|
|
|
|
if (options.sortField === 'created') {
|
|
|
|
|
sort = options.sortOrder === 'asc' ? 'oldest' : 'newest';
|
|
|
|
|
} else if (options.sortField === 'updated') {
|
|
|
|
|
sort = options.sortOrder === 'asc' ? 'leastupdate' : 'recentupdate';
|
|
|
|
|
} else if (options.sortField === 'comments') {
|
|
|
|
|
sort = options.sortOrder === 'asc' ? 'leastcomment' : 'mostcomment';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const params: Record<string, any> = {
|
|
|
|
|
page: options.page,
|
|
|
|
|
limit: options.limit,
|
|
|
|
|
state: options.state,
|
|
|
|
|
sort: sort,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Filter by type: pulls or issues
|
|
|
|
|
if (options.type !== 'all') {
|
|
|
|
|
params.type = options.type;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filter by search query if present
|
|
|
|
|
if (options.q.trim()) {
|
|
|
|
|
params.q = options.q.trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await client.get(`/repos/${config.owner}/${config.repo}/issues`, { params });
|
|
|
|
|
|
|
|
|
|
// Parse x-total-count header
|
|
|
|
|
const totalCountHeader = response.headers['x-total-count'];
|
|
|
|
|
const totalCount = totalCountHeader ? parseInt(totalCountHeader, 10) : response.data.length;
|
|
|
|
|
|
|
|
|
|
const issues: Issue[] = response.data.map((item: any) => ({
|
|
|
|
|
id: item.id,
|
|
|
|
|
number: item.number,
|
|
|
|
|
title: item.title,
|
|
|
|
|
state: item.state,
|
|
|
|
|
body: item.body || '',
|
|
|
|
|
user: {
|
|
|
|
|
id: item.user?.id || 0,
|
|
|
|
|
login: item.user?.login || 'unknown',
|
|
|
|
|
full_name: item.user?.full_name || '',
|
|
|
|
|
},
|
|
|
|
|
created_at: item.created_at,
|
|
|
|
|
updated_at: item.updated_at,
|
|
|
|
|
comments: item.comments_count || item.comments || 0,
|
|
|
|
|
labels: (item.labels || []).map((l: any) => ({
|
|
|
|
|
id: l.id,
|
|
|
|
|
name: l.name,
|
|
|
|
|
color: l.color,
|
|
|
|
|
})),
|
|
|
|
|
pull_request: item.pull_request,
|
2026-06-01 23:34:20 +00:00
|
|
|
total_tracked_time: 0,
|
2026-06-02 18:41:33 +00:00
|
|
|
assignees: (item.assignees || []).map((u: any) => ({
|
|
|
|
|
id: u.id,
|
|
|
|
|
login: u.login,
|
|
|
|
|
full_name: u.full_name || '',
|
|
|
|
|
})),
|
2026-06-01 00:59:22 +00:00
|
|
|
}));
|
|
|
|
|
|
2026-06-01 23:34:20 +00:00
|
|
|
// Fetch tracked times concurrently for all issues
|
|
|
|
|
await Promise.all(
|
|
|
|
|
issues.map(async (issue) => {
|
|
|
|
|
issue.total_tracked_time = await fetchIssueTrackedTimeTotal(config, issue.number);
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
2026-06-02 00:02:10 +00:00
|
|
|
// Apply local sorting as the Forgejo API does not inherently sort via endpoint parameters
|
|
|
|
|
issues.sort((a, b) => {
|
|
|
|
|
let diff = 0;
|
|
|
|
|
if (options.sortField === 'created') {
|
|
|
|
|
diff = new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
|
|
|
|
|
} else if (options.sortField === 'updated') {
|
|
|
|
|
diff = new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
|
|
|
|
|
} else if (options.sortField === 'comments') {
|
|
|
|
|
diff = b.comments - a.comments;
|
2026-06-02 18:41:33 +00:00
|
|
|
} else if (options.sortField === 'assignees') {
|
|
|
|
|
const aAssignees = (a.assignees || []).map(u => u.login).join(',');
|
|
|
|
|
const bAssignees = (b.assignees || []).map(u => u.login).join(',');
|
|
|
|
|
diff = aAssignees.localeCompare(bAssignees);
|
2026-06-02 00:02:10 +00:00
|
|
|
}
|
|
|
|
|
return options.sortOrder === 'desc' ? diff : -diff;
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-01 00:59:22 +00:00
|
|
|
return { issues, totalCount };
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.response) {
|
|
|
|
|
throw new Error(`API Error: ${error.response.data?.message || error.message}`);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Network Error: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 23:20:58 +00:00
|
|
|
/**
|
|
|
|
|
* Fetches a single issue by number.
|
|
|
|
|
*/
|
|
|
|
|
export async function fetchIssue(
|
|
|
|
|
config: Config,
|
|
|
|
|
issueNumber: number
|
|
|
|
|
): Promise<Issue> {
|
|
|
|
|
const client = createAxiosInstance(config);
|
|
|
|
|
try {
|
|
|
|
|
const response = await client.get(`/repos/${config.owner}/${config.repo}/issues/${issueNumber}`);
|
|
|
|
|
const item = response.data;
|
2026-06-01 23:34:20 +00:00
|
|
|
const trackedTime = await fetchIssueTrackedTimeTotal(config, issueNumber);
|
2026-06-01 23:20:58 +00:00
|
|
|
return {
|
|
|
|
|
id: item.id,
|
|
|
|
|
number: item.number,
|
|
|
|
|
title: item.title,
|
|
|
|
|
state: item.state,
|
|
|
|
|
body: item.body || '',
|
|
|
|
|
user: {
|
|
|
|
|
id: item.user?.id || 0,
|
|
|
|
|
login: item.user?.login || 'unknown',
|
|
|
|
|
full_name: item.user?.full_name || '',
|
|
|
|
|
},
|
|
|
|
|
created_at: item.created_at,
|
|
|
|
|
updated_at: item.updated_at,
|
|
|
|
|
comments: item.comments_count || item.comments || 0,
|
|
|
|
|
labels: (item.labels || []).map((l: any) => ({
|
|
|
|
|
id: l.id,
|
|
|
|
|
name: l.name,
|
|
|
|
|
color: l.color,
|
|
|
|
|
})),
|
|
|
|
|
pull_request: item.pull_request,
|
2026-06-01 23:34:20 +00:00
|
|
|
total_tracked_time: trackedTime,
|
2026-06-02 18:41:33 +00:00
|
|
|
assignees: (item.assignees || []).map((u: any) => ({
|
|
|
|
|
id: u.id,
|
|
|
|
|
login: u.login,
|
|
|
|
|
full_name: u.full_name || '',
|
|
|
|
|
})),
|
2026-06-01 23:20:58 +00:00
|
|
|
};
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.response) {
|
|
|
|
|
if (error.response.status === 401) {
|
|
|
|
|
throw new Error('Unauthorized: Please check your token.');
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Failed to fetch issue: ${error.response.data?.message || error.message}`);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Network Error: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 00:59:22 +00:00
|
|
|
/**
|
|
|
|
|
* Fetches comments for a specific issue.
|
|
|
|
|
*/
|
|
|
|
|
export async function fetchIssueComments(
|
|
|
|
|
config: Config,
|
|
|
|
|
issueNumber: number
|
|
|
|
|
): Promise<Comment[]> {
|
|
|
|
|
const client = createAxiosInstance(config);
|
|
|
|
|
try {
|
|
|
|
|
const response = await client.get(`/repos/${config.owner}/${config.repo}/issues/${issueNumber}/comments`);
|
|
|
|
|
|
|
|
|
|
const comments: Comment[] = response.data.map((item: any) => ({
|
|
|
|
|
id: item.id,
|
|
|
|
|
user: {
|
|
|
|
|
id: item.user?.id || 0,
|
|
|
|
|
login: item.user?.login || 'unknown',
|
|
|
|
|
full_name: item.user?.full_name || '',
|
|
|
|
|
},
|
|
|
|
|
body: item.body || '',
|
|
|
|
|
created_at: item.created_at,
|
|
|
|
|
updated_at: item.updated_at,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return comments;
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.response) {
|
|
|
|
|
throw new Error(`Failed to fetch comments: ${error.response.data?.message || error.message}`);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Network Error: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-01 02:02:23 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validates connection and fetches all repositories accessible by the user.
|
|
|
|
|
*/
|
|
|
|
|
export async function authenticateAndFetchRepos(
|
|
|
|
|
url: string,
|
|
|
|
|
userid: string,
|
|
|
|
|
token: string
|
|
|
|
|
): Promise<RepoItem[]> {
|
|
|
|
|
const normalized = normalizeUrl(url);
|
|
|
|
|
const headers: Record<string, string> = {
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (token.trim()) {
|
|
|
|
|
headers['Authorization'] = `token ${token.trim()}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const client = axios.create({
|
|
|
|
|
baseURL: `${normalized}/api/v1`,
|
|
|
|
|
headers,
|
|
|
|
|
timeout: 10000,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 1. Verify credentials / user existence
|
|
|
|
|
try {
|
|
|
|
|
if (token.trim()) {
|
|
|
|
|
await client.get('/user');
|
|
|
|
|
} else if (userid.trim()) {
|
|
|
|
|
await client.get(`/users/${userid.trim()}`);
|
|
|
|
|
}
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.response) {
|
|
|
|
|
if (error.response.status === 401) {
|
|
|
|
|
throw new Error('Unauthorized: Invalid API Token.');
|
|
|
|
|
}
|
|
|
|
|
if (error.response.status === 404) {
|
|
|
|
|
throw new Error(`User "${userid}" not found.`);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Authentication failed: ${error.response.data?.message || error.message}`);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Failed to connect to ${normalized}: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Fetch repositories
|
|
|
|
|
try {
|
|
|
|
|
let repos: any[] = [];
|
|
|
|
|
|
|
|
|
|
if (token.trim()) {
|
|
|
|
|
// Fetch authenticated user's repos (lists all they have access to)
|
|
|
|
|
const response = await client.get('/user/repos', { params: { limit: 100 } });
|
|
|
|
|
repos = response.data;
|
|
|
|
|
} else if (userid.trim()) {
|
|
|
|
|
// Fetch public repos of the target username
|
|
|
|
|
const response = await client.get(`/users/${userid.trim()}/repos`, { params: { limit: 100 } });
|
|
|
|
|
repos = response.data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!Array.isArray(repos)) {
|
|
|
|
|
throw new Error('Invalid response from server (expected list of repositories).');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return repos.map((r: any) => ({
|
|
|
|
|
id: r.id,
|
|
|
|
|
name: r.name,
|
|
|
|
|
full_name: r.full_name || `${r.owner?.login}/${r.name}`,
|
|
|
|
|
private: !!r.private,
|
|
|
|
|
description: r.description || '',
|
|
|
|
|
}));
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.response) {
|
|
|
|
|
throw new Error(`Failed to fetch repositories: ${error.response.data?.message || error.message}`);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Failed to fetch repositories: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 03:06:53 +00:00
|
|
|
/**
|
|
|
|
|
* Creates a new issue in the specified repository.
|
|
|
|
|
*/
|
|
|
|
|
export async function createIssue(
|
|
|
|
|
config: Config,
|
|
|
|
|
title: string,
|
|
|
|
|
body: string
|
|
|
|
|
): Promise<Issue> {
|
|
|
|
|
const client = createAxiosInstance(config);
|
|
|
|
|
try {
|
|
|
|
|
const response = await client.post(`/repos/${config.owner}/${config.repo}/issues`, {
|
|
|
|
|
title,
|
|
|
|
|
body,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const item = response.data;
|
|
|
|
|
return {
|
|
|
|
|
id: item.id,
|
|
|
|
|
number: item.number,
|
|
|
|
|
title: item.title,
|
|
|
|
|
state: item.state,
|
|
|
|
|
body: item.body || '',
|
|
|
|
|
user: {
|
|
|
|
|
id: item.user?.id || 0,
|
|
|
|
|
login: item.user?.login || 'unknown',
|
|
|
|
|
full_name: item.user?.full_name || '',
|
|
|
|
|
},
|
|
|
|
|
created_at: item.created_at,
|
|
|
|
|
updated_at: item.updated_at,
|
|
|
|
|
comments: item.comments_count || item.comments || 0,
|
|
|
|
|
labels: (item.labels || []).map((l: any) => ({
|
|
|
|
|
id: l.id,
|
|
|
|
|
name: l.name,
|
|
|
|
|
color: l.color,
|
|
|
|
|
})),
|
|
|
|
|
pull_request: item.pull_request,
|
2026-06-02 18:41:33 +00:00
|
|
|
assignees: (item.assignees || []).map((u: any) => ({
|
|
|
|
|
id: u.id,
|
|
|
|
|
login: u.login,
|
|
|
|
|
full_name: u.full_name || '',
|
|
|
|
|
})),
|
2026-06-01 03:06:53 +00:00
|
|
|
};
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.response) {
|
|
|
|
|
if (error.response.status === 401) {
|
|
|
|
|
throw new Error('Unauthorized: You must be logged in with a token to create an issue.');
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Failed to create issue: ${error.response.data?.message || error.message}`);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Network Error: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-01 03:18:12 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a comment on a specific issue.
|
|
|
|
|
*/
|
|
|
|
|
export async function createIssueComment(
|
|
|
|
|
config: Config,
|
|
|
|
|
issueNumber: number,
|
|
|
|
|
body: string
|
|
|
|
|
): Promise<Comment> {
|
|
|
|
|
const client = createAxiosInstance(config);
|
|
|
|
|
try {
|
|
|
|
|
const response = await client.post(`/repos/${config.owner}/${config.repo}/issues/${issueNumber}/comments`, {
|
|
|
|
|
body,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const item = response.data;
|
|
|
|
|
return {
|
|
|
|
|
id: item.id,
|
|
|
|
|
user: {
|
|
|
|
|
id: item.user?.id || 0,
|
|
|
|
|
login: item.user?.login || 'unknown',
|
|
|
|
|
full_name: item.user?.full_name || '',
|
|
|
|
|
},
|
|
|
|
|
body: item.body,
|
|
|
|
|
created_at: item.created_at,
|
|
|
|
|
updated_at: item.updated_at,
|
|
|
|
|
};
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.response) {
|
|
|
|
|
if (error.response.status === 401) {
|
|
|
|
|
throw new Error('Unauthorized: You must be logged in with a token to comment.');
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Failed to create comment: ${error.response.data?.message || error.message}`);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Network Error: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-01 23:15:01 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Edits an existing issue.
|
|
|
|
|
*/
|
|
|
|
|
export async function editIssue(
|
|
|
|
|
config: Config,
|
|
|
|
|
issueNumber: number,
|
|
|
|
|
title: string,
|
|
|
|
|
body: string
|
|
|
|
|
): Promise<Issue> {
|
|
|
|
|
const client = createAxiosInstance(config);
|
|
|
|
|
try {
|
|
|
|
|
const response = await client.patch(`/repos/${config.owner}/${config.repo}/issues/${issueNumber}`, {
|
|
|
|
|
title,
|
|
|
|
|
body,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const item = response.data;
|
|
|
|
|
return {
|
|
|
|
|
id: item.id,
|
|
|
|
|
number: item.number,
|
|
|
|
|
title: item.title,
|
|
|
|
|
state: item.state,
|
|
|
|
|
body: item.body || '',
|
|
|
|
|
user: {
|
|
|
|
|
id: item.user?.id || 0,
|
|
|
|
|
login: item.user?.login || 'unknown',
|
|
|
|
|
full_name: item.user?.full_name || '',
|
|
|
|
|
},
|
|
|
|
|
created_at: item.created_at,
|
|
|
|
|
updated_at: item.updated_at,
|
|
|
|
|
comments: item.comments_count || item.comments || 0,
|
|
|
|
|
labels: (item.labels || []).map((l: any) => ({
|
|
|
|
|
id: l.id,
|
|
|
|
|
name: l.name,
|
|
|
|
|
color: l.color,
|
|
|
|
|
})),
|
|
|
|
|
pull_request: item.pull_request,
|
2026-06-02 18:41:33 +00:00
|
|
|
assignees: (item.assignees || []).map((u: any) => ({
|
|
|
|
|
id: u.id,
|
|
|
|
|
login: u.login,
|
|
|
|
|
full_name: u.full_name || '',
|
|
|
|
|
})),
|
2026-06-01 23:15:01 +00:00
|
|
|
};
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.response) {
|
|
|
|
|
if (error.response.status === 401) {
|
|
|
|
|
throw new Error('Unauthorized: You must be logged in with a token to edit an issue.');
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Failed to edit issue: ${error.response.data?.message || error.message}`);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Network Error: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-01 23:34:20 +00:00
|
|
|
|
2026-06-02 00:15:30 +00:00
|
|
|
/**
|
|
|
|
|
* Changes the state of an issue (open/closed).
|
|
|
|
|
*/
|
|
|
|
|
export async function changeIssueState(
|
|
|
|
|
config: Config,
|
|
|
|
|
issueNumber: number,
|
|
|
|
|
state: 'open' | 'closed'
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const client = createAxiosInstance(config);
|
|
|
|
|
try {
|
|
|
|
|
await client.patch(`/repos/${config.owner}/${config.repo}/issues/${issueNumber}`, {
|
|
|
|
|
state,
|
|
|
|
|
});
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.response) {
|
|
|
|
|
if (error.response.status === 401) {
|
|
|
|
|
throw new Error('Unauthorized: You must be logged in with a token to change issue state.');
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Failed to change issue state: ${error.response.data?.message || error.message}`);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Network Error: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 23:34:20 +00:00
|
|
|
/**
|
|
|
|
|
* Adds time to an issue.
|
|
|
|
|
* @param time Time in seconds
|
|
|
|
|
*/
|
|
|
|
|
export async function addIssueTime(
|
|
|
|
|
config: Config,
|
|
|
|
|
issueNumber: number,
|
|
|
|
|
time: number
|
|
|
|
|
): Promise<any> {
|
|
|
|
|
const client = createAxiosInstance(config);
|
|
|
|
|
try {
|
|
|
|
|
const response = await client.post(`/repos/${config.owner}/${config.repo}/issues/${issueNumber}/times`, {
|
|
|
|
|
time,
|
|
|
|
|
});
|
|
|
|
|
return response.data;
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.response) {
|
|
|
|
|
if (error.response.status === 401) {
|
|
|
|
|
throw new Error('Unauthorized: You must be logged in with a token to add time.');
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Failed to add time: ${error.response.data?.message || error.message}`);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Network Error: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fetches the total tracked time for a specific issue.
|
|
|
|
|
* @returns Total time in seconds
|
|
|
|
|
*/
|
|
|
|
|
export async function fetchIssueTrackedTimeTotal(
|
|
|
|
|
config: Config,
|
|
|
|
|
issueNumber: number
|
|
|
|
|
): Promise<number> {
|
|
|
|
|
const client = createAxiosInstance(config);
|
|
|
|
|
try {
|
|
|
|
|
const response = await client.get(`/repos/${config.owner}/${config.repo}/issues/${issueNumber}/times`);
|
|
|
|
|
const times: any[] = response.data;
|
|
|
|
|
let total = 0;
|
|
|
|
|
for (const t of times) {
|
|
|
|
|
if (t && typeof t.time === 'number') {
|
|
|
|
|
total += t.time;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return total;
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
// If times are not supported or missing, just return 0
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-02 18:41:33 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Updates the assignees for an issue.
|
|
|
|
|
*/
|
|
|
|
|
export async function setIssueAssignees(
|
|
|
|
|
config: Config,
|
|
|
|
|
issueNumber: number,
|
|
|
|
|
assignees: string[]
|
|
|
|
|
): Promise<Issue> {
|
|
|
|
|
const client = createAxiosInstance(config);
|
|
|
|
|
try {
|
|
|
|
|
const response = await client.patch(`/repos/${config.owner}/${config.repo}/issues/${issueNumber}`, {
|
|
|
|
|
assignees,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const item = response.data;
|
|
|
|
|
return {
|
|
|
|
|
id: item.id,
|
|
|
|
|
number: item.number,
|
|
|
|
|
title: item.title,
|
|
|
|
|
state: item.state,
|
|
|
|
|
body: item.body || '',
|
|
|
|
|
user: {
|
|
|
|
|
id: item.user?.id || 0,
|
|
|
|
|
login: item.user?.login || 'unknown',
|
|
|
|
|
full_name: item.user?.full_name || '',
|
|
|
|
|
},
|
|
|
|
|
created_at: item.created_at,
|
|
|
|
|
updated_at: item.updated_at,
|
|
|
|
|
comments: item.comments_count || item.comments || 0,
|
|
|
|
|
labels: (item.labels || []).map((l: any) => ({
|
|
|
|
|
id: l.id,
|
|
|
|
|
name: l.name,
|
|
|
|
|
color: l.color,
|
|
|
|
|
})),
|
|
|
|
|
pull_request: item.pull_request,
|
|
|
|
|
assignees: (item.assignees || []).map((u: any) => ({
|
|
|
|
|
id: u.id,
|
|
|
|
|
login: u.login,
|
|
|
|
|
full_name: u.full_name || '',
|
|
|
|
|
})),
|
|
|
|
|
};
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.response) {
|
|
|
|
|
if (error.response.status === 401) {
|
|
|
|
|
throw new Error('Unauthorized: Please check your token.');
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Failed to update assignees: ${error.response.data?.message || error.message}`);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Network Error: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-02 23:42:07 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fetches all labels for a specific repository.
|
|
|
|
|
*/
|
|
|
|
|
export async function fetchLabels(config: Config): Promise<Label[]> {
|
|
|
|
|
const client = createAxiosInstance(config);
|
|
|
|
|
try {
|
|
|
|
|
const response = await client.get(`/repos/${config.owner}/${config.repo}/labels`);
|
|
|
|
|
return response.data.map((item: any) => ({
|
|
|
|
|
id: item.id,
|
|
|
|
|
name: item.name,
|
|
|
|
|
color: item.color,
|
|
|
|
|
description: item.description,
|
|
|
|
|
exclusive: item.exclusive,
|
|
|
|
|
}));
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.response) {
|
|
|
|
|
throw new Error(`Failed to fetch labels: ${error.response.data?.message || error.message}`);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Network Error: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates a new label in the specified repository.
|
|
|
|
|
*/
|
|
|
|
|
export async function createLabel(config: Config, label: Partial<Label>): Promise<Label> {
|
|
|
|
|
const client = createAxiosInstance(config);
|
|
|
|
|
try {
|
|
|
|
|
const response = await client.post(`/repos/${config.owner}/${config.repo}/labels`, {
|
|
|
|
|
name: label.name,
|
|
|
|
|
color: label.color, // expects hex without #
|
|
|
|
|
description: label.description,
|
|
|
|
|
exclusive: label.exclusive,
|
|
|
|
|
});
|
|
|
|
|
const item = response.data;
|
|
|
|
|
return {
|
|
|
|
|
id: item.id,
|
|
|
|
|
name: item.name,
|
|
|
|
|
color: item.color,
|
|
|
|
|
description: item.description,
|
|
|
|
|
exclusive: item.exclusive,
|
|
|
|
|
};
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.response) {
|
|
|
|
|
if (error.response.status === 401) {
|
|
|
|
|
throw new Error('Unauthorized: You must be logged in with a token to create a label.');
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Failed to create label: ${error.response.data?.message || error.message}`);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Network Error: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Updates an existing label.
|
|
|
|
|
*/
|
|
|
|
|
export async function updateLabel(config: Config, id: number, label: Partial<Label>): Promise<Label> {
|
|
|
|
|
const client = createAxiosInstance(config);
|
|
|
|
|
try {
|
|
|
|
|
const response = await client.patch(`/repos/${config.owner}/${config.repo}/labels/${id}`, {
|
|
|
|
|
name: label.name,
|
|
|
|
|
color: label.color,
|
|
|
|
|
description: label.description,
|
|
|
|
|
exclusive: label.exclusive,
|
|
|
|
|
});
|
|
|
|
|
const item = response.data;
|
|
|
|
|
return {
|
|
|
|
|
id: item.id,
|
|
|
|
|
name: item.name,
|
|
|
|
|
color: item.color,
|
|
|
|
|
description: item.description,
|
|
|
|
|
exclusive: item.exclusive,
|
|
|
|
|
};
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.response) {
|
|
|
|
|
if (error.response.status === 401) {
|
|
|
|
|
throw new Error('Unauthorized: You must be logged in with a token to update a label.');
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Failed to update label: ${error.response.data?.message || error.message}`);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Network Error: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Deletes a label.
|
|
|
|
|
*/
|
|
|
|
|
export async function deleteLabel(config: Config, id: number): Promise<void> {
|
|
|
|
|
const client = createAxiosInstance(config);
|
|
|
|
|
try {
|
|
|
|
|
await client.delete(`/repos/${config.owner}/${config.repo}/labels/${id}`);
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.response) {
|
|
|
|
|
if (error.response.status === 401) {
|
|
|
|
|
throw new Error('Unauthorized: You must be logged in with a token to delete a label.');
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Failed to delete label: ${error.response.data?.message || error.message}`);
|
|
|
|
|
}
|
|
|
|
|
throw new Error(`Network Error: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|