Files
mp-silma-ai-aide/src/api.ts
T
jacob-mathison f1f4c0df3a
Build / build (push) Successful in 11s
Show selected issue details
2026-07-17 17:26:34 -05:00

429 lines
11 KiB
TypeScript

import axios from "axios";
const rawGiteaBaseUrl = import.meta.env.VITE_GITEA_BASE_URL?.trim() ?? "";
const giteaToken = import.meta.env.VITE_GITEA_TOKEN?.trim() ?? "";
const rawGiteaRepoOwner = import.meta.env.VITE_GITEA_REPO_OWNER?.trim() ?? "";
const rawGiteaRepoName = import.meta.env.VITE_GITEA_REPO_NAME?.trim() ?? "";
export interface GiteaUser {
id: number;
login: string;
full_name?: string;
email?: string;
}
interface GiteaRepoOwner {
login: string;
}
interface GiteaRepoResponse {
name: string;
full_name?: string;
description?: string;
html_url?: string;
clone_url?: string;
ssh_url?: string;
updated_at?: string;
default_branch?: string;
private?: boolean;
owner?: GiteaRepoOwner;
}
interface GiteaBranchResponse {
name: string;
commit?: {
id?: string;
message?: string;
timestamp?: string;
url?: string;
};
}
interface GiteaIssueResponse {
number?: number;
title?: string;
body?: string;
state?: string;
html_url?: string;
created_at?: string;
updated_at?: string;
user?: {
login?: string;
};
comments?: number;
}
interface GiteaIssueCommentResponse {
id?: number;
body?: string;
html_url?: string;
created_at?: string;
updated_at?: string;
user?: {
login?: string;
};
}
interface GiteaLabelResponse {
id: number;
name: string;
}
interface GiteaAttachmentResponse {
id?: number;
name?: string;
browser_download_url?: string;
download_url?: string;
size?: number;
created_at?: string;
}
export interface CreatedGiteaIssue {
number: number;
webUrl: string;
}
export interface GiteaIssueSummary {
number: number;
title: string;
body: string;
state: string;
webUrl: string;
createdAt: string;
updatedAt: string;
author: string;
comments: number;
}
export interface GiteaIssueAttachment {
id: number;
name: string;
downloadUrl: string;
size: number;
createdAt: string;
}
export interface GiteaIssueComment {
id: number;
body: string;
webUrl: string;
createdAt: string;
updatedAt: string;
author: string;
}
export interface GiteaIssueDetail {
issue: GiteaIssueSummary;
comments: GiteaIssueComment[];
attachments: GiteaIssueAttachment[];
}
export interface GiteaRepoSummary {
name: string;
fullName: string;
description: string;
webUrl: string;
cloneUrl: string;
defaultBranch: string;
isPrivate: boolean;
lastCommitAt: string;
lastCommitSha: string;
lastCommitMessage: string;
}
export const giteaConfig = {
baseUrl: rawGiteaBaseUrl,
hasToken: giteaToken.length > 0,
repoOwner: rawGiteaRepoOwner,
repoName: rawGiteaRepoName,
targetRepo:
rawGiteaRepoOwner.length > 0 && rawGiteaRepoName.length > 0
? `${rawGiteaRepoOwner}/${rawGiteaRepoName}`
: "",
isConfigured: rawGiteaBaseUrl.length > 0 && giteaToken.length > 0
};
export function getApiBaseUrl(baseUrl: string): string {
const normalized = baseUrl.replace(/\/+$/, "");
return normalized.endsWith("/api/v1") ? normalized : `${normalized}/api/v1`;
}
function assertGiteaConfigured(): void {
if (!giteaConfig.isConfigured) {
throw new Error("Set VITE_GITEA_BASE_URL and VITE_GITEA_TOKEN before testing the connection.");
}
}
function createGiteaClient() {
assertGiteaConfigured();
return axios.create({
baseURL: getApiBaseUrl(giteaConfig.baseUrl),
headers: {
Authorization: `token ${giteaToken}`
}
});
}
export async function getCurrentGiteaUser(): Promise<GiteaUser> {
const client = createGiteaClient();
const response = await client.get<GiteaUser>("/user");
return response.data;
}
export async function createGiteaIssue(
owner: string,
repo: string,
title: string,
body: string,
labelIds: number[] = []
): Promise<CreatedGiteaIssue> {
const client = createGiteaClient();
const response = await client.post<GiteaIssueResponse>(`/repos/${owner}/${repo}/issues`, {
title,
body,
labels: labelIds
});
const number = response.data.number;
if (!number) {
throw new Error("Gitea did not return an issue number.");
}
return {
number,
webUrl: response.data.html_url || `${giteaConfig.baseUrl.replace(/\/+$/, "")}/${owner}/${repo}/issues/${number}`
};
}
export async function getGiteaIssues(owner: string, repo: string): Promise<GiteaIssueSummary[]> {
const client = createGiteaClient();
const response = await client.get<GiteaIssueResponse[]>(`/repos/${owner}/${repo}/issues`, {
params: {
limit: 25,
page: 1,
state: "all",
type: "issues"
}
});
return response.data
.filter((issue) => issue.number !== undefined)
.map((issue) => ({
number: issue.number ?? 0,
title: issue.title || "Untitled issue",
body: issue.body || "",
state: issue.state || "unknown",
webUrl: issue.html_url || `${giteaConfig.baseUrl.replace(/\/+$/, "")}/${owner}/${repo}/issues/${issue.number}`,
createdAt: issue.created_at || "",
updatedAt: issue.updated_at || "",
author: issue.user?.login || "unknown",
comments: issue.comments ?? 0
}));
}
export async function getGiteaIssueDetail(owner: string, repo: string, issueNumber: number): Promise<GiteaIssueDetail> {
const client = createGiteaClient();
const [issueResponse, commentsResponse, attachmentsResponse] = await Promise.all([
client.get<GiteaIssueResponse>(`/repos/${owner}/${repo}/issues/${issueNumber}`),
client.get<GiteaIssueCommentResponse[]>(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
params: {
limit: 100,
page: 1
}
}),
client.get<GiteaAttachmentResponse[]>(`/repos/${owner}/${repo}/issues/${issueNumber}/assets`, {
params: {
limit: 100,
page: 1
}
})
]);
const issue = issueResponse.data;
return {
issue: {
number: issue.number ?? issueNumber,
title: issue.title || "Untitled issue",
body: issue.body || "",
state: issue.state || "unknown",
webUrl: issue.html_url || `${giteaConfig.baseUrl.replace(/\/+$/, "")}/${owner}/${repo}/issues/${issueNumber}`,
createdAt: issue.created_at || "",
updatedAt: issue.updated_at || "",
author: issue.user?.login || "unknown",
comments: issue.comments ?? commentsResponse.data.length
},
comments: commentsResponse.data.map((comment) => ({
id: comment.id ?? 0,
body: comment.body || "",
webUrl: comment.html_url || "",
createdAt: comment.created_at || "",
updatedAt: comment.updated_at || "",
author: comment.user?.login || "unknown"
})),
attachments: attachmentsResponse.data.map((attachment) => ({
id: attachment.id ?? 0,
name: attachment.name || "attachment",
downloadUrl: attachment.browser_download_url || attachment.download_url || "",
size: attachment.size ?? 0,
createdAt: attachment.created_at || ""
}))
};
}
export async function uploadGiteaIssueAttachment(
owner: string,
repo: string,
issueNumber: number,
file: Blob,
fileName: string
): Promise<GiteaIssueAttachment> {
const client = createGiteaClient();
const formData = new FormData();
formData.append("attachment", file, fileName);
const response = await client.post<GiteaAttachmentResponse>(
`/repos/${owner}/${repo}/issues/${issueNumber}/assets`,
formData
);
return {
id: response.data.id ?? 0,
name: response.data.name || fileName,
downloadUrl: response.data.browser_download_url || response.data.download_url || "",
size: response.data.size ?? file.size,
createdAt: response.data.created_at || ""
};
}
export async function createGiteaIssueComment(
owner: string,
repo: string,
issueNumber: number,
body: string
): Promise<void> {
const client = createGiteaClient();
await client.post(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
body
});
}
export async function deleteGiteaIssue(owner: string, repo: string, issueNumber: number): Promise<void> {
const client = createGiteaClient();
await client.delete(`/repos/${owner}/${repo}/issues/${issueNumber}`);
}
async function getGiteaLabels(owner: string, repo: string): Promise<GiteaLabelResponse[]> {
const client = createGiteaClient();
const response = await client.get<GiteaLabelResponse[]>(`/repos/${owner}/${repo}/labels`, {
params: {
limit: 100
}
});
return response.data;
}
export async function ensureGiteaLabel(owner: string, repo: string, name: string, color = "0e8a16"): Promise<GiteaLabelResponse> {
const existingLabels = await getGiteaLabels(owner, repo);
const existing = existingLabels.find((label) => label.name.toLowerCase() === name.toLowerCase());
if (existing) {
return existing;
}
const client = createGiteaClient();
try {
const response = await client.post<GiteaLabelResponse>(`/repos/${owner}/${repo}/labels`, {
name,
color
});
return response.data;
} catch {
const refreshedLabels = await getGiteaLabels(owner, repo);
const refreshed = refreshedLabels.find((label) => label.name.toLowerCase() === name.toLowerCase());
if (refreshed) {
return refreshed;
}
throw new Error(`Could not create or find the ${name} label.`);
}
}
export async function addGiteaIssueLabels(
owner: string,
repo: string,
issueNumber: number,
labelIds: number[]
): Promise<void> {
const client = createGiteaClient();
await client.post(`/repos/${owner}/${repo}/issues/${issueNumber}/labels`, {
labels: labelIds
});
}
export async function getReposOrderedByLastCommit(): Promise<GiteaRepoSummary[]> {
const client = axios.create({
baseURL: getApiBaseUrl(giteaConfig.baseUrl),
headers: {
Authorization: `token ${giteaToken}`
}
});
assertGiteaConfigured();
const repos: GiteaRepoResponse[] = [];
let page = 1;
while (page <= 10) {
const response = await client.get<GiteaRepoResponse[]>("/user/repos", {
params: {
limit: 50,
page
}
});
repos.push(...response.data);
if (response.data.length < 50) {
break;
}
page += 1;
}
const repoDetails = await Promise.all(
repos.map(async (repo): Promise<GiteaRepoSummary> => {
const owner = repo.owner?.login || giteaConfig.repoOwner;
const defaultBranch = repo.default_branch || "main";
const fullName = repo.full_name || `${owner}/${repo.name}`;
let branch: GiteaBranchResponse | null = null;
try {
const branchResponse = await client.get<GiteaBranchResponse>(
`/repos/${owner}/${repo.name}/branches/${defaultBranch}`
);
branch = branchResponse.data;
} catch {
branch = null;
}
return {
name: repo.name,
fullName,
description: repo.description || "",
webUrl: repo.html_url || `${giteaConfig.baseUrl.replace(/\/+$/, "")}/${fullName}`,
cloneUrl: repo.clone_url || "",
defaultBranch,
isPrivate: Boolean(repo.private),
lastCommitAt: branch?.commit?.timestamp || repo.updated_at || "",
lastCommitSha: branch?.commit?.id || "",
lastCommitMessage: branch?.commit?.message?.split("\n")[0] || "No commit message available"
};
})
);
return repoDetails.sort((a, b) => {
const aTime = a.lastCommitAt ? Date.parse(a.lastCommitAt) : 0;
const bTime = b.lastCommitAt ? Date.parse(b.lastCommitAt) : 0;
return bTime - aTime || a.fullName.localeCompare(b.fullName);
});
}