arcadiasuite/server/integrations/github/service.ts

569 lines
13 KiB
TypeScript

/**
* Arcadia Suite - GitHub Integration Service (Expandido)
*
* Este serviço fornece funcionalidades para:
* 1. Fazer commits automáticos no repositório do Arcadia Suite
* 2. LER repositórios externos (como n8n, OpenManus, etc.) para análise e implementação
*
* @author Arcadia Development Team
* @version 2.0.0
*/
import { Octokit } from "@octokit/rest";
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
const ARCADIA_OWNER = process.env.GITHUB_OWNER || "JonasRodriguesPachceo";
const ARCADIA_REPO = process.env.GITHUB_REPO || "ArcadiaSuite-";
const DEFAULT_BRANCH = process.env.GITHUB_DEFAULT_BRANCH || "main";
interface FileContent {
path: string;
content: string;
size: number;
type: "file" | "dir";
}
interface RepositoryStructure {
owner: string;
repo: string;
branch: string;
tree: TreeItem[];
totalFiles: number;
totalDirs: number;
}
interface TreeItem {
path: string;
type: "blob" | "tree";
size?: number;
}
interface AnalysisResult {
success: boolean;
repository: string;
structure?: RepositoryStructure;
files?: FileContent[];
summary?: string;
error?: string;
}
interface FileToCommit {
path: string;
content: string;
}
interface CommitResult {
success: boolean;
commitSha?: string;
commitUrl?: string;
message: string;
}
interface BranchResult {
success: boolean;
branchName?: string;
message: string;
}
interface PullRequestResult {
success: boolean;
prNumber?: number;
prUrl?: string;
message: string;
}
export async function getRepositoryStructure(
owner: string,
repo: string,
branch?: string
): Promise<RepositoryStructure> {
if (!branch) {
const { data: repoData } = await octokit.repos.get({ owner, repo });
branch = repoData.default_branch;
}
const { data: refData } = await octokit.git.getRef({
owner,
repo,
ref: `heads/${branch}`,
});
const { data: commitData } = await octokit.git.getCommit({
owner,
repo,
commit_sha: refData.object.sha,
});
const { data: treeData } = await octokit.git.getTree({
owner,
repo,
tree_sha: commitData.tree.sha,
recursive: "true",
});
const tree: TreeItem[] = treeData.tree.map((item) => ({
path: item.path || "",
type: item.type as "blob" | "tree",
size: item.size,
}));
return {
owner,
repo,
branch,
tree,
totalFiles: tree.filter((t) => t.type === "blob").length,
totalDirs: tree.filter((t) => t.type === "tree").length,
};
}
export async function readExternalFile(
owner: string,
repo: string,
path: string,
branch?: string
): Promise<string | null> {
try {
const { data } = await octokit.repos.getContent({
owner,
repo,
path,
ref: branch,
});
if ("content" in data && data.content) {
return Buffer.from(data.content, "base64").toString("utf-8");
}
return null;
} catch {
return null;
}
}
export async function readMultipleFiles(
owner: string,
repo: string,
paths: string[],
branch?: string
): Promise<FileContent[]> {
const results: FileContent[] = [];
for (const path of paths) {
const content = await readExternalFile(owner, repo, path, branch);
if (content !== null) {
results.push({
path,
content,
size: content.length,
type: "file",
});
}
}
return results;
}
export async function listDirectory(
owner: string,
repo: string,
dirPath: string,
branch?: string
): Promise<{ name: string; path: string; type: "file" | "dir" }[]> {
try {
const { data } = await octokit.repos.getContent({
owner,
repo,
path: dirPath,
ref: branch,
});
if (Array.isArray(data)) {
return data.map((item) => ({
name: item.name,
path: item.path,
type: item.type === "dir" ? "dir" : "file",
}));
}
return [];
} catch {
return [];
}
}
export async function searchFiles(
owner: string,
repo: string,
pattern: string,
branch?: string
): Promise<string[]> {
const structure = await getRepositoryStructure(owner, repo, branch);
return structure.tree
.filter((item) => item.type === "blob" && item.path.includes(pattern))
.map((item) => item.path);
}
export async function analyzeRepository(
owner: string,
repo: string,
focusPaths?: string[]
): Promise<AnalysisResult> {
try {
const structure = await getRepositoryStructure(owner, repo);
let files: FileContent[] = [];
if (focusPaths && focusPaths.length > 0) {
for (const focusPath of focusPaths) {
const matchingFiles = structure.tree
.filter((item) => item.type === "blob" && item.path.startsWith(focusPath))
.slice(0, 20);
for (const file of matchingFiles) {
const content = await readExternalFile(owner, repo, file.path, structure.branch);
if (content) {
files.push({
path: file.path,
content,
size: content.length,
type: "file",
});
}
}
}
}
const summary = generateRepositorySummary(structure, files);
return {
success: true,
repository: `${owner}/${repo}`,
structure,
files,
summary,
};
} catch (error: any) {
return {
success: false,
repository: `${owner}/${repo}`,
error: error.message,
};
}
}
function generateRepositorySummary(structure: RepositoryStructure, files: FileContent[]): string {
const lines: string[] = [];
lines.push(`## Análise do Repositório: ${structure.owner}/${structure.repo}`);
lines.push(`**Branch:** ${structure.branch}`);
lines.push(`**Total de Arquivos:** ${structure.totalFiles}`);
lines.push(`**Total de Diretórios:** ${structure.totalDirs}`);
lines.push("");
const hasPackageJson = structure.tree.some((t) => t.path === "package.json");
const hasRequirements = structure.tree.some((t) => t.path === "requirements.txt");
const hasTsConfig = structure.tree.some((t) => t.path.includes("tsconfig.json"));
lines.push("### Tecnologias Detectadas:");
if (hasPackageJson) lines.push("- Node.js / JavaScript");
if (hasTsConfig) lines.push("- TypeScript");
if (hasRequirements) lines.push("- Python");
lines.push("");
const topLevelDirs = Array.from(new Set(structure.tree.map((t) => t.path.split("/")[0]))).slice(0, 15);
lines.push("### Estrutura Principal:");
topLevelDirs.forEach((dir) => lines.push(`- ${dir}/`));
lines.push("");
if (files.length > 0) {
lines.push(`### Arquivos Analisados (${files.length}):`);
files.forEach((f) => lines.push(`- ${f.path} (${f.size} bytes)`));
}
return lines.join("\n");
}
async function getCurrentCommit(owner: string, repo: string, branch: string) {
const { data: refData } = await octokit.git.getRef({
owner,
repo,
ref: `heads/${branch}`,
});
const commitSha = refData.object.sha;
const { data: commitData } = await octokit.git.getCommit({
owner,
repo,
commit_sha: commitSha,
});
return { commitSha, treeSha: commitData.tree.sha };
}
export async function commitFiles(
files: FileToCommit[],
message: string,
branch: string = DEFAULT_BRANCH
): Promise<CommitResult> {
try {
const { commitSha, treeSha } = await getCurrentCommit(ARCADIA_OWNER, ARCADIA_REPO, branch);
const blobs: { sha: string; path: string }[] = [];
for (const file of files) {
const { data } = await octokit.git.createBlob({
owner: ARCADIA_OWNER,
repo: ARCADIA_REPO,
content: file.content,
encoding: "utf-8",
});
blobs.push({ sha: data.sha, path: file.path });
}
const tree = blobs.map(({ sha, path }) => ({
path,
mode: "100644" as const,
type: "blob" as const,
sha,
}));
const { data: newTree } = await octokit.git.createTree({
owner: ARCADIA_OWNER,
repo: ARCADIA_REPO,
tree,
base_tree: treeSha,
});
const { data: newCommit } = await octokit.git.createCommit({
owner: ARCADIA_OWNER,
repo: ARCADIA_REPO,
message,
tree: newTree.sha,
parents: [commitSha],
});
await octokit.git.updateRef({
owner: ARCADIA_OWNER,
repo: ARCADIA_REPO,
ref: `heads/${branch}`,
sha: newCommit.sha,
});
return {
success: true,
commitSha: newCommit.sha,
commitUrl: `https://github.com/${ARCADIA_OWNER}/${ARCADIA_REPO}/commit/${newCommit.sha}`,
message: `Commit realizado: ${message}`,
};
} catch (error: any) {
return {
success: false,
message: `Erro ao fazer commit: ${error.message}`,
};
}
}
export async function createBranch(
newBranchName: string,
sourceBranch: string = DEFAULT_BRANCH
): Promise<BranchResult> {
try {
const { data: refData } = await octokit.git.getRef({
owner: ARCADIA_OWNER,
repo: ARCADIA_REPO,
ref: `heads/${sourceBranch}`,
});
await octokit.git.createRef({
owner: ARCADIA_OWNER,
repo: ARCADIA_REPO,
ref: `refs/heads/${newBranchName}`,
sha: refData.object.sha,
});
return {
success: true,
branchName: newBranchName,
message: `Branch '${newBranchName}' criada com sucesso a partir de '${sourceBranch}'`,
};
} catch (error: any) {
return {
success: false,
message: `Erro ao criar branch: ${error.message}`,
};
}
}
export async function createPullRequest(
title: string,
body: string,
headBranch: string,
baseBranch: string = DEFAULT_BRANCH
): Promise<PullRequestResult> {
try {
const { data } = await octokit.pulls.create({
owner: ARCADIA_OWNER,
repo: ARCADIA_REPO,
title,
body,
head: headBranch,
base: baseBranch,
});
return {
success: true,
prNumber: data.number,
prUrl: data.html_url,
message: `Pull Request #${data.number} criado com sucesso`,
};
} catch (error: any) {
return {
success: false,
message: `Erro ao criar Pull Request: ${error.message}`,
};
}
}
export async function getRepositoryInfo(): Promise<{
name: string;
fullName: string;
defaultBranch: string;
url: string;
}> {
const { data } = await octokit.repos.get({
owner: ARCADIA_OWNER,
repo: ARCADIA_REPO,
});
return {
name: data.name,
fullName: data.full_name,
defaultBranch: data.default_branch,
url: data.html_url,
};
}
export async function listBranches(): Promise<string[]> {
const { data } = await octokit.repos.listBranches({
owner: ARCADIA_OWNER,
repo: ARCADIA_REPO,
});
return data.map((branch) => branch.name);
}
export async function getFileContent(
filePath: string,
branch: string = DEFAULT_BRANCH
): Promise<string | null> {
try {
const { data } = await octokit.repos.getContent({
owner: ARCADIA_OWNER,
repo: ARCADIA_REPO,
path: filePath,
ref: branch,
});
if ("content" in data && data.content) {
return Buffer.from(data.content, "base64").toString("utf-8");
}
return null;
} catch {
return null;
}
}
export async function toolGitHubCommit(
message: string,
files: { path: string; content: string }[]
): Promise<{ success: boolean; result: string }> {
const result = await commitFiles(files, message);
return {
success: result.success,
result: result.success
? `✅ Commit realizado: ${result.commitUrl}`
: `❌ Erro: ${result.message}`,
};
}
export async function toolAnalyzeExternalRepo(
repoUrl: string,
focusPaths?: string[]
): Promise<{ success: boolean; result: string; data?: AnalysisResult }> {
const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/);
if (!match) {
return {
success: false,
result: "URL inválida. Use o formato: https://github.com/owner/repo",
};
}
const [, owner, repo] = match;
const cleanRepo = repo.replace(/\.git$/, "");
const analysis = await analyzeRepository(owner, cleanRepo, focusPaths);
return {
success: analysis.success,
result: analysis.success
? `✅ Repositório analisado: ${analysis.repository}\n\n${analysis.summary}`
: `❌ Erro: ${analysis.error}`,
data: analysis,
};
}
export async function toolReadExternalFile(
repoUrl: string,
filePath: string
): Promise<{ success: boolean; result: string; content?: string }> {
const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/);
if (!match) {
return {
success: false,
result: "URL inválida. Use o formato: https://github.com/owner/repo",
};
}
const [, owner, repo] = match;
const cleanRepo = repo.replace(/\.git$/, "");
const content = await readExternalFile(owner, cleanRepo, filePath);
if (content) {
return {
success: true,
result: `✅ Arquivo lido: ${filePath} (${content.length} bytes)`,
content,
};
} else {
return {
success: false,
result: `❌ Arquivo não encontrado: ${filePath}`,
};
}
}
export default {
getRepositoryStructure,
readExternalFile,
readMultipleFiles,
listDirectory,
searchFiles,
analyzeRepository,
commitFiles,
createBranch,
createPullRequest,
getRepositoryInfo,
listBranches,
getFileContent,
toolGitHubCommit,
toolAnalyzeExternalRepo,
toolReadExternalFile,
};