UNPKG

@grec0/mcp-svn

Version:

MCP server para integración completa con Subversion (SVN)

672 lines 26.2 kB
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