UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

304 lines (303 loc) 12.3 kB
import * as path from 'path'; import { FileUtils } from './file-utils.js'; import logger from '../../../logger.js'; export class StorageInitializer { static async initialize(config) { const result = { success: false, metadata: { storageType: config.storageType, dataDirectory: config.dataDirectory, directoriesCreated: [], indexFilesCreated: [], operation: 'initialize', timestamp: new Date() } }; try { logger.debug({ storageType: config.storageType, dataDirectory: config.dataDirectory }, 'Starting storage initialization'); for (const directory of config.directories) { const fullPath = path.isAbsolute(directory) ? directory : path.join(config.dataDirectory, directory); const dirResult = await FileUtils.ensureDirectory(fullPath); if (!dirResult.success) { result.error = `Failed to create directory ${fullPath}: ${dirResult.error}`; return result; } result.metadata.directoriesCreated.push(fullPath); logger.debug({ directory: fullPath }, 'Directory created successfully'); } for (const indexFile of config.indexFiles) { const fullPath = path.isAbsolute(indexFile.path) ? indexFile.path : path.join(config.dataDirectory, indexFile.path); if (await FileUtils.fileExists(fullPath)) { logger.debug({ indexFile: fullPath }, 'Index file already exists, skipping creation'); continue; } const fileResult = await FileUtils.writeJsonFile(fullPath, indexFile.defaultData); if (!fileResult.success) { result.error = `Failed to create index file ${fullPath}: ${fileResult.error}`; return result; } result.metadata.indexFilesCreated.push(fullPath); logger.debug({ indexFile: fullPath }, 'Index file created successfully'); } if (config.validatePaths) { const validationResult = await this.validateInitialization(config); if (!validationResult.success) { result.error = `Initialization validation failed: ${validationResult.error}`; return result; } } result.success = true; logger.info({ storageType: config.storageType, directoriesCreated: result.metadata.directoriesCreated.length, indexFilesCreated: result.metadata.indexFilesCreated.length }, 'Storage initialization completed successfully'); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); result.error = errorMessage; logger.error({ err: error, storageType: config.storageType, dataDirectory: config.dataDirectory }, 'Storage initialization failed'); return result; } } static async validateInitialization(config) { try { for (const directory of config.directories) { const fullPath = path.isAbsolute(directory) ? directory : path.join(config.dataDirectory, directory); if (!await FileUtils.fileExists(fullPath)) { return { success: false, error: `Directory ${fullPath} was not created successfully` }; } } for (const indexFile of config.indexFiles) { const fullPath = path.isAbsolute(indexFile.path) ? indexFile.path : path.join(config.dataDirectory, indexFile.path); if (!await FileUtils.fileExists(fullPath)) { return { success: false, error: `Index file ${fullPath} was not created successfully` }; } const readResult = await FileUtils.readJsonFile(fullPath); if (!readResult.success) { return { success: false, error: `Index file ${fullPath} is not readable or contains invalid JSON: ${readResult.error}` }; } } return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) }; } } static createIndexData(entityType, version = '1.0.0') { return { [entityType]: [], lastUpdated: new Date().toISOString(), version }; } static getStandardConfig(storageType, dataDirectory) { const configs = { project: { storageType: 'ProjectStorage', directories: ['projects'], indexFiles: [ { path: 'projects-index.json', defaultData: this.createIndexData('projects') } ] }, task: { storageType: 'TaskStorage', directories: ['tasks', 'epics'], indexFiles: [ { path: 'tasks-index.json', defaultData: this.createIndexData('tasks') }, { path: 'epics-index.json', defaultData: this.createIndexData('epics') } ] }, dependency: { storageType: 'DependencyStorage', directories: ['dependencies', 'graphs'], indexFiles: [ { path: 'dependencies-index.json', defaultData: this.createIndexData('dependencies') } ] } }; const config = configs[storageType]; if (!config) { throw new Error(`Unknown storage type: ${storageType}`); } return { ...config, dataDirectory, validatePaths: true }; } static async initializeWithRecovery(config, maxRetries = 3) { let lastError; let lastResult; const normalizedConfig = this.normalizeStorageConfig(config); for (let attempt = 1; attempt <= maxRetries; attempt++) { logger.debug({ storageType: normalizedConfig.storageType, attempt, maxRetries, dataDirectory: normalizedConfig.dataDirectory }, 'Attempting storage initialization'); const result = await this.initialize(normalizedConfig); lastResult = result; if (result.success) { if (attempt > 1) { logger.info({ storageType: normalizedConfig.storageType, attempt }, 'Storage initialization succeeded after retry'); } return result; } lastError = result.error; if (attempt < maxRetries) { const isRetryable = this.isRetryableError(result.error || ''); if (!isRetryable) { logger.error({ storageType: normalizedConfig.storageType, error: result.error }, 'Non-retryable error encountered, stopping retries'); break; } const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); logger.warn({ storageType: normalizedConfig.storageType, attempt, error: result.error, nextRetryIn: delay, isRetryable }, 'Storage initialization failed, retrying'); await this.cleanupPartialInitialization(normalizedConfig, result); await new Promise(resolve => setTimeout(resolve, delay)); } } logger.error({ storageType: normalizedConfig.storageType, maxRetries, finalError: lastError }, 'Storage initialization failed after all retries'); return { success: false, error: `Failed after ${maxRetries} attempts. Last error: ${lastError}`, metadata: { storageType: normalizedConfig.storageType, dataDirectory: normalizedConfig.dataDirectory, directoriesCreated: lastResult?.metadata.directoriesCreated || [], indexFilesCreated: lastResult?.metadata.indexFilesCreated || [], operation: 'initialize', timestamp: new Date() } }; } static normalizeStorageConfig(config) { return { ...config, dataDirectory: path.resolve(config.dataDirectory), directories: config.directories.map(dir => path.isAbsolute(dir) ? path.resolve(dir) : dir), indexFiles: config.indexFiles.map(indexFile => ({ ...indexFile, path: path.isAbsolute(indexFile.path) ? path.resolve(indexFile.path) : indexFile.path })) }; } static isRetryableError(error) { const retryablePatterns = [ /EBUSY/i, /EMFILE/i, /ENFILE/i, /EAGAIN/i, /ENOTREADY/i, /temporarily/i, /timeout/i, /network/i ]; const nonRetryablePatterns = [ /EACCES/i, /EPERM/i, /ENOENT/i, /ENOSPC/i, /EROFS/i, /Invalid file path/i, /Path contains dangerous characters/i, /Path is outside allowed directories/i ]; if (nonRetryablePatterns.some(pattern => pattern.test(error))) { return false; } if (retryablePatterns.some(pattern => pattern.test(error))) { return true; } return true; } static async cleanupPartialInitialization(config, failedResult) { try { if (failedResult.metadata.directoriesCreated.length === 0 && failedResult.metadata.indexFilesCreated.length === 0) { return; } logger.debug({ storageType: config.storageType, directoriesCreated: failedResult.metadata.directoriesCreated.length, indexFilesCreated: failedResult.metadata.indexFilesCreated.length }, 'Cleaning up partial initialization before retry'); for (const indexFile of failedResult.metadata.indexFilesCreated) { try { if (await FileUtils.fileExists(indexFile)) { await FileUtils.deleteFile(indexFile); logger.debug({ indexFile }, 'Cleaned up partial index file'); } } catch (error) { logger.warn({ err: error, indexFile }, 'Failed to clean up index file'); } } } catch (error) { logger.warn({ err: error, storageType: config.storageType }, 'Failed to clean up partial initialization'); } } } export async function initializeStorage(storageType, dataDirectory, withRecovery = true) { const config = StorageInitializer.getStandardConfig(storageType, dataDirectory); if (withRecovery) { return StorageInitializer.initializeWithRecovery(config); } else { return StorageInitializer.initialize(config); } }