@grec0/mcp-svn
Version:
MCP server para integración completa con Subversion (SVN)
672 lines • 26.2 kB
JavaScript
import { SvnError } from '../common/types.js';
import { createSvnConfig, executeSvnCommand, parseInfoOutput, parseStatusOutput, parseLogOutput, validateSvnInstallation, isWorkingCopy, normalizePath, validatePath, validateSvnUrl, cleanOutput, clearSvnCredentials } from '../common/utils.js';
export class SvnService {
config;
constructor(config = {}) {
this.config = createSvnConfig(config);
}
/**
* Función auxiliar para manejar errores comunes de SVN
*/
handleSvnError(error, operation) {
let message = `Failed to ${operation}`;
if (error.message.includes('E155007') || error.message.includes('not a working copy')) {
message = `El directorio '${this.config.workingDirectory}' no es un working copy de SVN. Asegúrate de estar en un directorio que contenga un repositorio SVN o hacer checkout primero.`;
}
else if (error.message.includes('E175002') || error.message.includes('Unable to connect')) {
message = `No se puede conectar al repositorio SVN. Verifica tu conexión a internet y las credenciales.`;
}
else if (error.message.includes('E170001') || error.message.includes('Authentication failed')) {
message = `Error de autenticación. Verifica tu nombre de usuario y contraseña SVN.`;
}
else if (error.message.includes('E155036') || error.message.includes('working copy locked')) {
message = `El working copy está bloqueado. Ejecuta 'svn cleanup' para resolverlo.`;
}
else if (error.message.includes('E200030') || error.message.includes('sqlite')) {
message = `Error en la base de datos del working copy. Ejecuta 'svn cleanup' para repararlo.`;
}
else if (error.stderr && error.stderr.length > 0) {
message = `${message}: ${error.stderr}`;
}
else {
message = `${message}: ${error.message}`;
}
throw new SvnError(message);
}
/**
* Verificar que SVN está disponible y configurado correctamente
*/
async healthCheck() {
try {
// Verificar instalación de SVN
const svnAvailable = await validateSvnInstallation(this.config);
if (!svnAvailable) {
return {
success: false,
error: 'SVN is not available in the system PATH',
command: 'svn --version',
workingDirectory: this.config.workingDirectory
};
}
// Obtener versión de SVN
const versionResponse = await executeSvnCommand(this.config, ['--version', '--quiet']);
const version = versionResponse.data;
// Verificar si estamos en un working copy
const workingCopyValid = await isWorkingCopy(this.config.workingDirectory);
let repositoryAccessible = false;
if (workingCopyValid) {
try {
await this.getInfo();
repositoryAccessible = true;
}
catch (error) {
repositoryAccessible = false;
}
}
return {
success: true,
data: {
svnAvailable,
version: version.trim(),
workingCopyValid,
repositoryAccessible
},
command: 'health-check',
workingDirectory: this.config.workingDirectory
};
}
catch (error) {
return {
success: false,
error: error.message,
command: 'health-check',
workingDirectory: this.config.workingDirectory
};
}
}
/**
* Obtener información del working copy o directorio específico
*/
async getInfo(path) {
try {
const args = ['info'];
if (path) {
// Check if it's a URL or a local path
if (validateSvnUrl(path)) {
// It's a URL, add it directly without normalization
args.push(path);
}
else if (validatePath(path)) {
// It's a local path, normalize it
args.push(normalizePath(path));
}
else {
throw new SvnError(`Invalid path or URL: ${path}`);
}
}
const response = await executeSvnCommand(this.config, args);
const info = parseInfoOutput(cleanOutput(response.data));
return {
success: true,
data: info,
command: response.command,
workingDirectory: response.workingDirectory,
executionTime: response.executionTime
};
}
catch (error) {
this.handleSvnError(error, 'get SVN info');
}
}
/**
* Obtener estado de archivos en el working copy
*/
async getStatus(path, showAll = false) {
try {
const args = ['status'];
if (path) {
if (!validatePath(path)) {
throw new SvnError(`Invalid path: ${path}`);
}
args.push(normalizePath(path));
}
let response;
// Si showAll es true, intentar primero con --show-updates
if (showAll) {
try {
const argsWithUpdates = [...args, '--show-updates'];
response = await executeSvnCommand(this.config, argsWithUpdates);
}
catch (error) {
// Si falla con --show-updates, intentar sin él
console.warn(`Warning: --show-updates failed, falling back to local status only: ${error.message}`);
response = await executeSvnCommand(this.config, args);
}
}
else {
response = await executeSvnCommand(this.config, args);
}
const statusList = parseStatusOutput(cleanOutput(response.data));
return {
success: true,
data: statusList,
command: response.command,
workingDirectory: response.workingDirectory,
executionTime: response.executionTime
};
}
catch (error) {
this.handleSvnError(error, 'get SVN status');
}
}
/**
* Obtener historial de cambios (log)
*/
async getLog(path, limit, revision) {
try {
const args = ['log'];
if (limit && limit > 0) {
args.push('--limit', limit.toString());
}
if (revision) {
args.push('--revision', revision);
}
if (path) {
if (!validatePath(path)) {
throw new SvnError(`Invalid path: ${path}`);
}
args.push(normalizePath(path));
}
let response;
try {
response = await executeSvnCommand(this.config, args);
}
catch (error) {
// Detectar si SVN no está instalado
if ((error.message.includes('spawn') && error.message.includes('ENOENT')) ||
error.code === 127) {
const enhancedError = new SvnError('SVN no está instalado o no se encuentra en el PATH del sistema. Instala Subversion para usar este comando.');
enhancedError.command = error.command;
enhancedError.code = error.code;
throw enhancedError;
}
// Detectar errores de red/conectividad y proporcionar mensajes más útiles
if (error.message.includes('E175002') ||
error.message.includes('Unable to connect') ||
error.message.includes('Connection refused') ||
error.message.includes('Network is unreachable') ||
error.code === 1) {
// Intentar con opciones que funcionen sin conectividad remota si es posible
console.warn(`Log remoto falló, posible problema de conectividad: ${error.message}`);
// Para comandos log, podemos intentar usar --offline si está disponible,
// o proporcionar una respuesta vacía con información útil
const enhancedError = new SvnError(`No se pudo obtener el historial de cambios. Posibles causas:
- Sin conectividad al servidor SVN
- Credenciales requeridas pero no proporcionadas
- Servidor SVN temporalmente inaccesible
- Working copy no sincronizado con el repositorio remoto`);
enhancedError.command = error.command;
enhancedError.stderr = error.stderr;
enhancedError.code = error.code;
throw enhancedError;
}
// Re-lanzar otros errores sin modificar
throw error;
}
const logEntries = parseLogOutput(cleanOutput(response.data));
return {
success: true,
data: logEntries,
command: response.command,
workingDirectory: response.workingDirectory,
executionTime: response.executionTime
};
}
catch (error) {
this.handleSvnError(error, 'get SVN log');
}
}
/**
* Obtener diferencias entre versiones
*/
async getDiff(path, oldRevision, newRevision) {
try {
const args = ['diff'];
if (oldRevision && newRevision) {
args.push('--old', `${path || '.'}@${oldRevision}`);
args.push('--new', `${path || '.'}@${newRevision}`);
}
else if (oldRevision) {
args.push('--revision', oldRevision);
if (path) {
args.push(normalizePath(path));
}
}
else if (path) {
if (!validatePath(path)) {
throw new SvnError(`Invalid path: ${path}`);
}
args.push(normalizePath(path));
}
const response = await executeSvnCommand(this.config, args);
return {
success: true,
data: cleanOutput(response.data),
command: response.command,
workingDirectory: response.workingDirectory,
executionTime: response.executionTime
};
}
catch (error) {
throw new SvnError(`Failed to get SVN diff: ${error.message}`);
}
}
/**
* Checkout de un repositorio
*/
async checkout(url, path, options = {}) {
try {
if (!validateSvnUrl(url)) {
throw new SvnError(`Invalid SVN URL: ${url}`);
}
const args = ['checkout'];
if (options.revision) {
args.push('--revision', options.revision.toString());
}
if (options.depth) {
args.push('--depth', options.depth);
}
if (options.force) {
args.push('--force');
}
if (options.ignoreExternals) {
args.push('--ignore-externals');
}
args.push(url);
if (path) {
if (!validatePath(path)) {
throw new SvnError(`Invalid path: ${path}`);
}
args.push(normalizePath(path));
}
const response = await executeSvnCommand(this.config, args);
return {
success: true,
data: cleanOutput(response.data),
command: response.command,
workingDirectory: response.workingDirectory,
executionTime: response.executionTime
};
}
catch (error) {
throw new SvnError(`Failed to checkout: ${error.message}`);
}
}
/**
* Actualizar working copy
*/
async update(path, options = {}) {
try {
const args = ['update'];
if (options.revision) {
args.push('--revision', options.revision.toString());
}
if (options.force) {
args.push('--force');
}
if (options.ignoreExternals) {
args.push('--ignore-externals');
}
if (options.acceptConflicts) {
args.push('--accept', options.acceptConflicts);
}
if (path) {
if (!validatePath(path)) {
throw new SvnError(`Invalid path: ${path}`);
}
args.push(normalizePath(path));
}
const response = await executeSvnCommand(this.config, args);
return {
success: true,
data: cleanOutput(response.data),
command: response.command,
workingDirectory: response.workingDirectory,
executionTime: response.executionTime
};
}
catch (error) {
throw new SvnError(`Failed to update: ${error.message}`);
}
}
/**
* Añadir archivos al control de versiones
*/
async add(paths, options = {}) {
try {
const pathArray = Array.isArray(paths) ? paths : [paths];
// Validar todas las rutas
for (const path of pathArray) {
if (!validatePath(path)) {
throw new SvnError(`Invalid path: ${path}`);
}
}
const args = ['add'];
if (options.force) {
args.push('--force');
}
if (options.noIgnore) {
args.push('--no-ignore');
}
if (options.autoProps) {
args.push('--auto-props');
}
if (options.noAutoProps) {
args.push('--no-auto-props');
}
if (options.parents) {
args.push('--parents');
}
// Añadir rutas normalizadas
args.push(...pathArray.map(p => normalizePath(p)));
const response = await executeSvnCommand(this.config, args);
return {
success: true,
data: cleanOutput(response.data),
command: response.command,
workingDirectory: response.workingDirectory,
executionTime: response.executionTime
};
}
catch (error) {
throw new SvnError(`Failed to add files: ${error.message}`);
}
}
/**
* Confirmar cambios al repositorio
*/
async commit(options, paths) {
try {
if (!options.message && !options.file) {
throw new SvnError('Commit message is required');
}
const args = ['commit'];
if (options.message) {
args.push('--message', options.message);
}
if (options.file) {
args.push('--file', normalizePath(options.file));
}
if (options.force) {
args.push('--force');
}
if (options.keepLocks) {
args.push('--keep-locks');
}
if (options.noUnlock) {
args.push('--no-unlock');
}
// Añadir rutas específicas si se proporcionan
if (paths && paths.length > 0) {
for (const path of paths) {
if (!validatePath(path)) {
throw new SvnError(`Invalid path: ${path}`);
}
}
args.push(...paths.map(p => normalizePath(p)));
}
else if (options.targets) {
args.push(...options.targets.map(p => normalizePath(p)));
}
const response = await executeSvnCommand(this.config, args);
return {
success: true,
data: cleanOutput(response.data),
command: response.command,
workingDirectory: response.workingDirectory,
executionTime: response.executionTime
};
}
catch (error) {
throw new SvnError(`Failed to commit: ${error.message}`);
}
}
/**
* Eliminar archivos del control de versiones
*/
async delete(paths, options = {}) {
try {
const pathArray = Array.isArray(paths) ? paths : [paths];
// Validar todas las rutas
for (const path of pathArray) {
if (!validatePath(path)) {
throw new SvnError(`Invalid path: ${path}`);
}
}
const args = ['delete'];
if (options.force) {
args.push('--force');
}
if (options.keepLocal) {
args.push('--keep-local');
}
if (options.message) {
args.push('--message', options.message);
}
// Añadir rutas normalizadas
args.push(...pathArray.map(p => normalizePath(p)));
const response = await executeSvnCommand(this.config, args);
return {
success: true,
data: cleanOutput(response.data),
command: response.command,
workingDirectory: response.workingDirectory,
executionTime: response.executionTime
};
}
catch (error) {
throw new SvnError(`Failed to delete files: ${error.message}`);
}
}
/**
* Revertir cambios locales
*/
async revert(paths) {
try {
const pathArray = Array.isArray(paths) ? paths : [paths];
// Validar todas las rutas
for (const path of pathArray) {
if (!validatePath(path)) {
throw new SvnError(`Invalid path: ${path}`);
}
}
const args = ['revert'];
// Añadir rutas normalizadas
args.push(...pathArray.map(p => normalizePath(p)));
const response = await executeSvnCommand(this.config, args);
return {
success: true,
data: cleanOutput(response.data),
command: response.command,
workingDirectory: response.workingDirectory,
executionTime: response.executionTime
};
}
catch (error) {
throw new SvnError(`Failed to revert files: ${error.message}`);
}
}
/**
* Limpiar working copy
*/
async cleanup(path) {
try {
const args = ['cleanup'];
if (path) {
if (!validatePath(path)) {
throw new SvnError(`Invalid path: ${path}`);
}
args.push(normalizePath(path));
}
const response = await executeSvnCommand(this.config, args);
return {
success: true,
data: cleanOutput(response.data),
command: response.command,
workingDirectory: response.workingDirectory,
executionTime: response.executionTime
};
}
catch (error) {
throw new SvnError(`Failed to cleanup: ${error.message}`);
}
}
/**
* Diagnóstico específico para comandos problemáticos
*/
async diagnoseCommands() {
const results = {
statusLocal: false,
statusRemote: false,
logBasic: false,
workingCopyPath: this.config.workingDirectory,
errors: [],
suggestions: []
};
try {
// Probar svn status local
try {
await executeSvnCommand(this.config, ['status']);
results.statusLocal = true;
}
catch (error) {
const errorMsg = this.categorizeError(error, 'status local');
results.errors.push(errorMsg.message);
if (errorMsg.suggestion) {
results.suggestions.push(errorMsg.suggestion);
}
}
// Probar svn status con --show-updates
try {
await executeSvnCommand(this.config, ['status', '--show-updates']);
results.statusRemote = true;
}
catch (error) {
const errorMsg = this.categorizeError(error, 'status remoto');
results.errors.push(errorMsg.message);
if (errorMsg.suggestion) {
results.suggestions.push(errorMsg.suggestion);
}
}
// Probar svn log básico
try {
await executeSvnCommand(this.config, ['log', '--limit', '1']);
results.logBasic = true;
}
catch (error) {
const errorMsg = this.categorizeError(error, 'log básico');
results.errors.push(errorMsg.message);
if (errorMsg.suggestion) {
results.suggestions.push(errorMsg.suggestion);
}
}
// Agregar sugerencias generales basadas en los resultados
if (!results.statusRemote && !results.logBasic && results.statusLocal) {
results.suggestions.push('Los comandos remotos fallan pero el local funciona. Revisa la conectividad de red y credenciales SVN.');
}
return {
success: true,
data: results,
command: 'diagnostic',
workingDirectory: this.config.workingDirectory
};
}
catch (error) {
results.errors.push(`Error general: ${error.message}`);
return {
success: false,
data: results,
error: error.message,
command: 'diagnostic',
workingDirectory: this.config.workingDirectory
};
}
}
/**
* Categorizar errores y proporcionar sugerencias específicas
*/
categorizeError(error, commandType) {
const baseMessage = `${commandType} falló`;
// SVN no encontrado en el sistema
if ((error.message.includes('spawn') && error.message.includes('ENOENT')) ||
error.code === 127) {
return {
message: `${baseMessage}: SVN no está instalado o no se encuentra en el PATH`,
suggestion: 'Instala SVN (subversion) o verifica que esté en el PATH del sistema'
};
}
// Errores de conectividad
if (error.message.includes('E175002') ||
error.message.includes('Unable to connect') ||
error.message.includes('Connection refused') ||
error.message.includes('Network is unreachable')) {
return {
message: `${baseMessage}: Sin conectividad al servidor SVN`,
suggestion: 'Verifica tu conexión a internet y que el servidor SVN esté accesible'
};
}
// Errores de autenticación - demasiados intentos
if (error.message.includes('E215004') ||
error.message.includes('No more credentials') ||
error.message.includes('we tried too many times')) {
return {
message: `${baseMessage}: Demasiados intentos de autenticación fallidos`,
suggestion: 'Las credenciales pueden estar incorrectas o cachadas. Limpia el cache de credenciales SVN y verifica SVN_USERNAME y SVN_PASSWORD'
};
}
// Errores de autenticación generales
if (error.message.includes('E170001') ||
error.message.includes('Authentication failed') ||
error.message.includes('authorization failed')) {
return {
message: `${baseMessage}: Error de autenticación`,
suggestion: 'Verifica tus credenciales SVN (SVN_USERNAME y SVN_PASSWORD)'
};
}
// Working copy no válido
if (error.message.includes('E155007') ||
error.message.includes('not a working copy')) {
return {
message: `${baseMessage}: No es un working copy válido`,
suggestion: 'Asegúrate de estar en un directorio con checkout de SVN o ejecuta svn checkout primero'
};
}
// Working copy bloqueado
if (error.message.includes('E155036') ||
error.message.includes('working copy locked')) {
return {
message: `${baseMessage}: Working copy bloqueado`,
suggestion: 'Ejecuta "svn cleanup" para desbloquear el working copy'
};
}
// Error genérico con código 1 (frecuente en comandos remotos)
if (error.code === 1) {
return {
message: `${baseMessage}: Comando falló con código 1 (posible problema de red/autenticación)`,
suggestion: 'Revisa conectividad de red, credenciales SVN, y que el repositorio sea accesible'
};
}
// Error genérico
return {
message: `${baseMessage}: ${error.message}`,
suggestion: undefined
};
}
/**
* Limpiar cache de credenciales SVN para resolver errores de autenticación
*/
async clearCredentials() {
return await clearSvnCredentials(this.config);
}
}
//# sourceMappingURL=svn-service.js.map