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
JavaScript
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);
}
}