feat: replace GitHub API with Forgejo REST API in agent routes

This commit is contained in:
WildDragon Deploy 2026-05-27 23:33:42 -04:00
parent 8ce8bf72f7
commit 376fb9ee00

View file

@ -9,7 +9,7 @@ import { queryClaudeSDK } from '../claude-sdk.js';
import { spawnCursor } from '../cursor-cli.js';
import { queryCodex } from '../openai-codex.js';
import { spawnGemini } from '../gemini-cli.js';
import { Octokit } from '@octokit/rest';
const FORGEJO_BASE_URL = process.env.FORGEJO_BASE_URL || 'https://forge.wilddragon.net';
import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
import { IS_PLATFORM } from '../constants/config.js';
import { normalizeProjectPath } from '../shared/utils.js';
@ -99,36 +99,28 @@ async function getGitRemoteUrl(repoPath) {
}
/**
* Normalize GitHub URLs for comparison
* @param {string} url - GitHub URL
* Normalize git URLs for comparison
* @param {string} url - Git URL (any host)
* @returns {string} - Normalized URL
*/
function normalizeGitHubUrl(url) {
// Remove .git suffix
function normalizeGitUrl(url) {
let normalized = url.replace(/\.git$/, '');
// Convert SSH to HTTPS format for comparison
normalized = normalized.replace(/^git@github\.com:/, 'https://github.com/');
// Remove trailing slash
normalized = normalized.replace(/^git@([^:]+):/, 'https://$1/');
normalized = normalized.replace(/\/$/, '');
return normalized.toLowerCase();
}
/**
* Parse GitHub URL to extract owner and repo
* @param {string} url - GitHub URL (HTTPS or SSH)
* Parse git URL to extract owner and repo (works with any host)
* @param {string} url - Git URL (HTTPS or SSH, any host)
* @returns {{owner: string, repo: string}} - Parsed owner and repo
*/
function parseGitHubUrl(url) {
// Handle HTTPS URLs: https://github.com/owner/repo or https://github.com/owner/repo.git
// Handle SSH URLs: git@github.com:owner/repo or git@github.com:owner/repo.git
const match = url.match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/);
if (!match) {
throw new Error('Invalid GitHub URL format');
}
return {
owner: match[1],
repo: match[2].replace(/\.git$/, '')
};
function parseGitUrl(url) {
const httpsMatch = url.match(/https?:\/\/[^/]+\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/.*)?$/);
const sshMatch = url.match(/git@[^:]+:([^/]+)\/([^/]+?)(?:\.git)?$/);
const match = httpsMatch || sshMatch;
if (!match) { throw new Error('Invalid git URL format'); }
return { owner: match[1], repo: match[2] };
}
/**
@ -257,47 +249,11 @@ async function getCommitMessages(projectPath, limit = 5) {
});
}
/**
* Create a new branch on GitHub using the API
* @param {Octokit} octokit - Octokit instance
* @param {string} owner - Repository owner
* @param {string} repo - Repository name
* @param {string} branchName - Name of the new branch
* @param {string} baseBranch - Base branch to branch from (default: 'main')
* @returns {Promise<void>}
*/
async function createGitHubBranch(octokit, owner, repo, branchName, baseBranch = 'main') {
try {
// Get the SHA of the base branch
const { data: ref } = await octokit.git.getRef({
owner,
repo,
ref: `heads/${baseBranch}`
});
const baseSha = ref.object.sha;
// Create the new branch
await octokit.git.createRef({
owner,
repo,
ref: `refs/heads/${branchName}`,
sha: baseSha
});
console.log(`✅ Created branch '${branchName}' on GitHub`);
} catch (error) {
if (error.status === 422 && error.message.includes('Reference already exists')) {
console.log(` Branch '${branchName}' already exists on GitHub`);
} else {
throw error;
}
}
}
/**
* Create a pull request on GitHub
* @param {Octokit} octokit - Octokit instance
* Create a pull request on a Forgejo instance
* @param {string} baseUrl - Forgejo base URL
* @param {string} token - Forgejo access token
* @param {string} owner - Repository owner
* @param {string} repo - Repository name
* @param {string} branchName - Head branch name
@ -306,22 +262,24 @@ async function createGitHubBranch(octokit, owner, repo, branchName, baseBranch =
* @param {string} baseBranch - Base branch (default: 'main')
* @returns {Promise<{number: number, url: string}>} - PR number and URL
*/
async function createGitHubPR(octokit, owner, repo, branchName, title, body, baseBranch = 'main') {
const { data: pr } = await octokit.pulls.create({
owner,
repo,
title,
head: branchName,
base: baseBranch,
body
async function createForgejoPR(baseUrl, token, owner, repo, branchName, title, body, baseBranch = 'main') {
const response = await fetch(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls`, {
method: 'POST',
headers: {
'Authorization': `token ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ title, body, head: branchName, base: baseBranch }),
});
console.log(`✅ Created pull request #${pr.number}: ${pr.html_url}`);
if (!response.ok) {
const err = await response.json().catch(() => ({ message: response.statusText }));
throw new Error(`Failed to create PR: ${err.message || response.statusText}`);
}
return {
number: pr.number,
url: pr.html_url
};
const pr = await response.json();
console.log(`Created pull request #${pr.number}: ${pr.html_url}`);
return { number: pr.number, url: pr.html_url };
}
/**
@ -331,12 +289,12 @@ async function createGitHubPR(octokit, owner, repo, branchName, title, body, bas
* @param {string} projectPath - Path for cloning the repository
* @returns {Promise<string>} - Path to the cloned repository
*/
async function cloneGitHubRepo(githubUrl, githubToken = null, projectPath) {
async function cloneGitRepo(repoUrl, gitToken = null, projectPath) {
return new Promise(async (resolve, reject) => {
try {
// Validate GitHub URL
if (!githubUrl || !githubUrl.includes('github.com')) {
throw new Error('Invalid GitHub URL');
if (!repoUrl || !repoUrl.trim()) {
throw new Error('Repository URL is required');
}
const cloneDir = path.resolve(projectPath);
@ -347,14 +305,14 @@ async function cloneGitHubRepo(githubUrl, githubToken = null, projectPath) {
// Directory exists - check if it's a git repo with the same URL
try {
const existingUrl = await getGitRemoteUrl(cloneDir);
const normalizedExisting = normalizeGitHubUrl(existingUrl);
const normalizedRequested = normalizeGitHubUrl(githubUrl);
const normalizedExisting = normalizeGitUrl(existingUrl);
const normalizedRequested = normalizeGitUrl(repoUrl);
if (normalizedExisting === normalizedRequested) {
console.log('✅ Repository already exists at path with correct URL');
return resolve(cloneDir);
} else {
throw new Error(`Directory ${cloneDir} already exists with a different repository (${existingUrl}). Expected: ${githubUrl}`);
throw new Error(`Directory ${cloneDir} already exists with a different repository (${existingUrl}). Expected: ${repoUrl}`);
}
} catch (gitError) {
throw new Error(`Directory ${cloneDir} already exists but is not a valid git repository or git command failed`);
@ -367,11 +325,16 @@ async function cloneGitHubRepo(githubUrl, githubToken = null, projectPath) {
await fs.mkdir(path.dirname(cloneDir), { recursive: true });
// Prepare the git clone URL with authentication if token is provided
let cloneUrl = githubUrl;
if (githubToken) {
// Convert HTTPS URL to authenticated URL
// Example: https://github.com/user/repo -> https://token@github.com/user/repo
cloneUrl = githubUrl.replace('https://github.com', `https://${githubToken}@github.com`);
let cloneUrl = repoUrl;
if (gitToken) {
try {
const url = new URL(repoUrl);
url.username = gitToken;
url.password = '';
cloneUrl = url.toString();
} catch {
// SSH URLs used as-is
}
}
console.log('🔄 Cloning repository:', githubUrl);
@ -887,7 +850,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
targetPath = path.join(os.homedir(), '.claude', 'external-projects', repoHash);
}
finalProjectPath = await cloneGitHubRepo(githubUrl.trim(), tokenToUse, targetPath);
finalProjectPath = await cloneGitRepo(githubUrl.trim(), tokenToUse, targetPath);
} else {
// Use existing project path
finalProjectPath = normalizeProjectPath(path.resolve(projectPath));
@ -997,17 +960,14 @@ router.post('/', validateExternalApiKey, async (req, res) => {
throw new Error('GitHub token required for branch/PR creation. Please configure a GitHub token in settings.');
}
// Initialize Octokit
const octokit = new Octokit({ auth: tokenToUse });
// Get GitHub URL - either from parameter or from git remote
let repoUrl = githubUrl;
if (!repoUrl) {
console.log('🔍 Getting GitHub URL from git remote...');
try {
repoUrl = await getGitRemoteUrl(finalProjectPath);
if (!repoUrl.includes('github.com')) {
throw new Error('Project does not have a GitHub remote configured');
if (!repoUrl.trim()) {
throw new Error('Project does not have a git remote configured');
}
console.log(`✅ Found GitHub remote: ${repoUrl}`);
} catch (error) {
@ -1016,7 +976,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
}
// Parse GitHub URL to get owner and repo
const { owner, repo } = parseGitHubUrl(repoUrl);
const { owner, repo } = parseGitUrl(repoUrl);
console.log(`📦 Repository: ${owner}/${repo}`);
// Use provided branch name or auto-generate from message
@ -1101,7 +1061,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
branchInfo = {
name: finalBranchName,
url: `https://github.com/${owner}/${repo}/tree/${finalBranchName}`
url: `${FORGEJO_BASE_URL}/${owner}/${repo}/src/branch/${finalBranchName}`
};
}
@ -1126,7 +1086,7 @@ router.post('/', validateExternalApiKey, async (req, res) => {
// Create the pull request
console.log('🔄 Creating pull request...');
prInfo = await createGitHubPR(octokit, owner, repo, finalBranchName, prTitle, prBody, 'main');
prInfo = await createForgejoPR(FORGEJO_BASE_URL, tokenToUse, owner, repo, finalBranchName, prTitle, prBody, 'main');
}
// Send branch/PR info in response