import { simpleGit, SimpleGit, CleanOptions } from 'simple-git';
import { AuthManager } from './auth-manager.js';
import fs from 'fs-extra';
import path from 'path';
export class GitManager {
private authManager: AuthManager;
constructor() {
this.authManager = new AuthManager();
}
private getGitUrl(projectId: string, email?: string, token?: string): string {
// If credentials are provided directly, use them
if (email && token) {
// Encode email to handle special characters like '@'
const encodedEmail = encodeURIComponent(email);
return `https://${encodedEmail}:${token}@git.overleaf.com/${projectId}`;
}
return `https://git.overleaf.com/${projectId}`;
}
async cloneProject(projectId: string, localPath: string, email?: string, token?: string) {
// Ensure parent directory exists
await fs.ensureDir(path.dirname(localPath));
let gitUrl = '';
// Try to get credentials if not provided
if (!email || !token) {
const creds = await this.authManager.getCredentials();
if (creds) {
gitUrl = this.getGitUrl(projectId, creds.email, creds.token);
} else {
// Fallback to no-auth URL (will likely fail or prompt if interactive, but MCP is non-interactive)
// Ideally we should error here if no auth is available for a private repo,
// but let's try to construct it.
gitUrl = this.getGitUrl(projectId);
}
} else {
gitUrl = this.getGitUrl(projectId, email, token);
}
const git: SimpleGit = simpleGit();
try {
await git.clone(gitUrl, localPath);
console.error(`Successfully cloned project ${projectId} to ${localPath}`);
return { success: true, message: `Cloned to ${localPath}` };
} catch (error: any) {
console.error('Clone failed:', error);
throw new Error(`Failed to clone project: ${error.message}`);
}
}
async pullChanges(localPath: string) {
if (!await fs.pathExists(localPath)) {
throw new Error(`Directory ${localPath} does not exist`);
}
const git: SimpleGit = simpleGit(localPath);
try {
await git.pull();
return { success: true, message: 'Pulled latest changes' };
} catch (error: any) {
throw new Error(`Failed to pull changes: ${error.message}`);
}
}
async pushChanges(localPath: string, message: string) {
if (!await fs.pathExists(localPath)) {
throw new Error(`Directory ${localPath} does not exist`);
}
const git: SimpleGit = simpleGit(localPath);
try {
await git.add('.');
const status = await git.status();
if (status.staged.length === 0 && status.created.length === 0 && status.modified.length === 0 && status.deleted.length === 0 && status.renamed.length === 0) {
return { success: true, message: 'No changes to commit' };
}
await git.commit(message);
await git.push();
return { success: true, message: 'Pushed changes to Overleaf' };
} catch (error: any) {
throw new Error(`Failed to push changes: ${error.message}`);
}
}
async getStatus(localPath: string) {
if (!await fs.pathExists(localPath)) {
throw new Error(`Directory ${localPath} does not exist`);
}
const git: SimpleGit = simpleGit(localPath);
try {
const status = await git.status();
const diff = await git.diff(['--stat']);
return {
success: true,
status: status,
diff: diff
};
} catch (error: any) {
throw new Error(`Failed to get status: ${error.message}`);
}
}
}