vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
674 lines (673 loc) • 27.8 kB
JavaScript
import path from 'path';
import { FileUtils } from '../../utils/file-utils.js';
import { getVibeTaskManagerOutputDir } from '../../utils/config-loader.js';
import logger from '../../../../logger.js';
function isDependencyIndex(data) {
if (!data || typeof data !== 'object')
return false;
const index = data;
return Array.isArray(index.dependencies) &&
typeof index.lastUpdated === 'string' &&
typeof index.version === 'string';
}
export class DependencyStorage {
dataDirectory;
dependenciesDirectory;
graphsDirectory;
dependencyIndexFile;
constructor(dataDirectory) {
this.dataDirectory = dataDirectory || getVibeTaskManagerOutputDir();
this.dependenciesDirectory = path.join(this.dataDirectory, 'dependencies');
this.graphsDirectory = path.join(this.dataDirectory, 'dependency-graphs');
this.dependencyIndexFile = path.join(this.dataDirectory, 'dependencies-index.json');
}
async initialize() {
try {
const depDirResult = await FileUtils.ensureDirectory(this.dependenciesDirectory);
if (!depDirResult.success) {
return depDirResult;
}
const graphDirResult = await FileUtils.ensureDirectory(this.graphsDirectory);
if (!graphDirResult.success) {
return graphDirResult;
}
if (!await FileUtils.fileExists(this.dependencyIndexFile)) {
const indexData = {
dependencies: [],
lastUpdated: new Date().toISOString(),
version: '1.0.0'
};
const indexResult = await FileUtils.writeJsonFile(this.dependencyIndexFile, indexData);
if (!indexResult.success) {
return indexResult;
}
}
logger.debug({ dataDirectory: this.dataDirectory }, 'Dependency storage initialized');
return {
success: true,
metadata: {
filePath: this.dataDirectory,
operation: 'initialize',
timestamp: new Date()
}
};
}
catch (error) {
logger.error({ err: error, dataDirectory: this.dataDirectory }, 'Failed to initialize dependency storage');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: this.dataDirectory,
operation: 'initialize',
timestamp: new Date()
}
};
}
}
async createDependency(dependency) {
try {
logger.info({ dependencyId: dependency.id, from: dependency.fromTaskId, to: dependency.toTaskId }, 'Creating dependency');
const validationResult = this.validateDependency(dependency);
if (!validationResult.valid) {
return {
success: false,
error: `Dependency validation failed: ${validationResult.errors.join(', ')}`,
metadata: {
filePath: this.getDependencyFilePath(dependency.id),
operation: 'create_dependency',
timestamp: new Date()
}
};
}
if (await this.dependencyExists(dependency.id)) {
return {
success: false,
error: `Dependency with ID ${dependency.id} already exists`,
metadata: {
filePath: this.getDependencyFilePath(dependency.id),
operation: 'create_dependency',
timestamp: new Date()
}
};
}
const initResult = await this.initialize();
if (!initResult.success) {
return {
success: false,
error: `Failed to initialize storage: ${initResult.error}`,
metadata: initResult.metadata
};
}
const dependencyToSave = {
...dependency,
metadata: {
...dependency.metadata,
createdAt: new Date()
}
};
const dependencyFilePath = this.getDependencyFilePath(dependency.id);
const saveResult = await FileUtils.writeYamlFile(dependencyFilePath, dependencyToSave);
if (!saveResult.success) {
return {
success: false,
error: `Failed to save dependency: ${saveResult.error}`,
metadata: saveResult.metadata
};
}
const indexUpdateResult = await this.updateIndex('add', dependency.id, {
id: dependency.id,
fromTaskId: dependency.fromTaskId,
toTaskId: dependency.toTaskId,
type: dependency.type,
critical: dependency.critical,
createdAt: dependencyToSave.metadata.createdAt
});
if (!indexUpdateResult.success) {
await FileUtils.deleteFile(dependencyFilePath);
return {
success: false,
error: `Failed to update index: ${indexUpdateResult.error}`,
metadata: indexUpdateResult.metadata
};
}
logger.info({ dependencyId: dependency.id }, 'Dependency created successfully');
return {
success: true,
data: dependencyToSave,
metadata: {
filePath: dependencyFilePath,
operation: 'create_dependency',
timestamp: new Date()
}
};
}
catch (error) {
logger.error({ err: error, dependencyId: dependency.id }, 'Failed to create dependency');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: this.getDependencyFilePath(dependency.id),
operation: 'create_dependency',
timestamp: new Date()
}
};
}
}
async getDependency(dependencyId) {
try {
logger.debug({ dependencyId }, 'Getting dependency');
const dependencyFilePath = this.getDependencyFilePath(dependencyId);
if (!await FileUtils.fileExists(dependencyFilePath)) {
return {
success: false,
error: `Dependency ${dependencyId} not found`,
metadata: {
filePath: dependencyFilePath,
operation: 'get_dependency',
timestamp: new Date()
}
};
}
const loadResult = await FileUtils.readYamlFile(dependencyFilePath);
if (!loadResult.success) {
return {
success: false,
error: `Failed to load dependency: ${loadResult.error}`,
metadata: loadResult.metadata
};
}
return {
success: true,
data: loadResult.data,
metadata: {
filePath: dependencyFilePath,
operation: 'get_dependency',
timestamp: new Date()
}
};
}
catch (error) {
logger.error({ err: error, dependencyId }, 'Failed to get dependency');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: this.getDependencyFilePath(dependencyId),
operation: 'get_dependency',
timestamp: new Date()
}
};
}
}
async updateDependency(dependencyId, updates) {
try {
logger.info({ dependencyId, updates: Object.keys(updates) }, 'Updating dependency');
const getResult = await this.getDependency(dependencyId);
if (!getResult.success) {
return getResult;
}
const existingDependency = getResult.data;
const updatedDependency = {
...existingDependency,
...updates,
id: dependencyId,
metadata: {
...existingDependency.metadata,
...updates.metadata,
createdAt: existingDependency.metadata.createdAt
}
};
const validationResult = this.validateDependency(updatedDependency);
if (!validationResult.valid) {
return {
success: false,
error: `Dependency validation failed: ${validationResult.errors.join(', ')}`,
metadata: {
filePath: this.getDependencyFilePath(dependencyId),
operation: 'update_dependency',
timestamp: new Date()
}
};
}
const dependencyFilePath = this.getDependencyFilePath(dependencyId);
const saveResult = await FileUtils.writeYamlFile(dependencyFilePath, updatedDependency);
if (!saveResult.success) {
return {
success: false,
error: `Failed to save updated dependency: ${saveResult.error}`,
metadata: saveResult.metadata
};
}
const indexUpdateResult = await this.updateIndex('update', dependencyId, {
id: updatedDependency.id,
fromTaskId: updatedDependency.fromTaskId,
toTaskId: updatedDependency.toTaskId,
type: updatedDependency.type,
critical: updatedDependency.critical,
createdAt: updatedDependency.metadata.createdAt
});
if (!indexUpdateResult.success) {
logger.warn({ dependencyId, error: indexUpdateResult.error }, 'Failed to update index, but dependency was saved');
}
logger.info({ dependencyId }, 'Dependency updated successfully');
return {
success: true,
data: updatedDependency,
metadata: {
filePath: dependencyFilePath,
operation: 'update_dependency',
timestamp: new Date()
}
};
}
catch (error) {
logger.error({ err: error, dependencyId }, 'Failed to update dependency');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: this.getDependencyFilePath(dependencyId),
operation: 'update_dependency',
timestamp: new Date()
}
};
}
}
async deleteDependency(dependencyId) {
try {
logger.info({ dependencyId }, 'Deleting dependency');
if (!await this.dependencyExists(dependencyId)) {
return {
success: false,
error: `Dependency ${dependencyId} not found`,
metadata: {
filePath: this.getDependencyFilePath(dependencyId),
operation: 'delete_dependency',
timestamp: new Date()
}
};
}
const dependencyFilePath = this.getDependencyFilePath(dependencyId);
const deleteResult = await FileUtils.deleteFile(dependencyFilePath);
if (!deleteResult.success) {
return {
success: false,
error: `Failed to delete dependency file: ${deleteResult.error}`,
metadata: deleteResult.metadata
};
}
const indexUpdateResult = await this.updateIndex('remove', dependencyId);
if (!indexUpdateResult.success) {
logger.warn({ dependencyId, error: indexUpdateResult.error }, 'Failed to update index, but dependency file was deleted');
}
logger.info({ dependencyId }, 'Dependency deleted successfully');
return {
success: true,
metadata: {
filePath: dependencyFilePath,
operation: 'delete_dependency',
timestamp: new Date()
}
};
}
catch (error) {
logger.error({ err: error, dependencyId }, 'Failed to delete dependency');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: this.getDependencyFilePath(dependencyId),
operation: 'delete_dependency',
timestamp: new Date()
}
};
}
}
async listDependencies(projectId) {
try {
logger.debug({ projectId }, 'Listing dependencies');
const indexResult = await this.loadIndex();
if (!indexResult.success) {
return {
success: false,
error: `Failed to load dependency index: ${indexResult.error}`,
metadata: indexResult.metadata
};
}
const index = indexResult.data;
const dependencies = [];
for (const dependencyInfo of index.dependencies) {
const dependencyResult = await this.getDependency(dependencyInfo.id);
if (dependencyResult.success) {
const dependency = dependencyResult.data;
if (!projectId || this.isDependencyInProject(dependency, projectId)) {
dependencies.push(dependency);
}
}
else {
logger.warn({ dependencyId: dependencyInfo.id, error: dependencyResult.error }, 'Failed to load dependency from index');
}
}
return {
success: true,
data: dependencies,
metadata: {
filePath: this.dependencyIndexFile,
operation: 'list_dependencies',
timestamp: new Date()
}
};
}
catch (error) {
logger.error({ err: error, projectId }, 'Failed to list dependencies');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: this.dependencyIndexFile,
operation: 'list_dependencies',
timestamp: new Date()
}
};
}
}
async getDependenciesForTask(taskId) {
const listResult = await this.listDependencies();
if (!listResult.success) {
return listResult;
}
const dependencies = listResult.data.filter(dep => dep.fromTaskId === taskId);
return {
success: true,
data: dependencies,
metadata: {
filePath: this.dependencyIndexFile,
operation: 'get_dependencies_for_task',
timestamp: new Date()
}
};
}
async getDependentsForTask(taskId) {
const listResult = await this.listDependencies();
if (!listResult.success) {
return listResult;
}
const dependents = listResult.data.filter(dep => dep.toTaskId === taskId);
return {
success: true,
data: dependents,
metadata: {
filePath: this.dependencyIndexFile,
operation: 'get_dependents_for_task',
timestamp: new Date()
}
};
}
async dependencyExists(dependencyId) {
const dependencyFilePath = this.getDependencyFilePath(dependencyId);
return await FileUtils.fileExists(dependencyFilePath);
}
async saveDependencyGraph(projectId, graph) {
try {
logger.info({ projectId }, 'Saving dependency graph');
const graphFilePath = this.getGraphFilePath(projectId);
const initResult = await this.initialize();
if (!initResult.success) {
return initResult;
}
const graphToSave = {
...graph,
nodes: Object.fromEntries(graph.nodes),
metadata: {
...graph.metadata,
generatedAt: new Date()
}
};
const saveResult = await FileUtils.writeJsonFile(graphFilePath, graphToSave);
if (!saveResult.success) {
return saveResult;
}
logger.info({ projectId }, 'Dependency graph saved successfully');
return {
success: true,
metadata: {
filePath: graphFilePath,
operation: 'save_dependency_graph',
timestamp: new Date()
}
};
}
catch (error) {
logger.error({ err: error, projectId }, 'Failed to save dependency graph');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: this.getGraphFilePath(projectId),
operation: 'save_dependency_graph',
timestamp: new Date()
}
};
}
}
async loadDependencyGraph(projectId) {
try {
logger.debug({ projectId }, 'Loading dependency graph');
const graphFilePath = this.getGraphFilePath(projectId);
if (!await FileUtils.fileExists(graphFilePath)) {
return {
success: false,
error: `Dependency graph for project ${projectId} not found`,
metadata: {
filePath: graphFilePath,
operation: 'load_dependency_graph',
timestamp: new Date()
}
};
}
const loadResult = await FileUtils.readJsonFile(graphFilePath);
if (!loadResult.success) {
return {
success: false,
error: `Failed to load dependency graph: ${loadResult.error}`,
metadata: loadResult.metadata
};
}
const graphData = loadResult.data;
const graphData_ = graphData;
const edges = Array.isArray(graphData_.edges) ? graphData_.edges : [];
const executionOrder = Array.isArray(graphData_.executionOrder) ? graphData_.executionOrder : [];
const criticalPath = Array.isArray(graphData_.criticalPath) ? graphData_.criticalPath : [];
const statisticsData = graphData_.statistics && typeof graphData_.statistics === 'object' ? graphData_.statistics : {};
const statistics = {
totalTasks: typeof statisticsData.totalTasks === 'number' ? statisticsData.totalTasks : 0,
totalDependencies: typeof statisticsData.totalDependencies === 'number' ? statisticsData.totalDependencies : 0,
maxDepth: typeof statisticsData.maxDepth === 'number' ? statisticsData.maxDepth : 0,
cyclicDependencies: Array.isArray(statisticsData.cyclicDependencies) ? statisticsData.cyclicDependencies : [],
orphanedTasks: Array.isArray(statisticsData.orphanedTasks) ? statisticsData.orphanedTasks : []
};
const metadataData = graphData_.metadata && typeof graphData_.metadata === 'object' ? graphData_.metadata : {};
const metadata = {
generatedAt: typeof metadataData.generatedAt === 'string' || typeof metadataData.generatedAt === 'number'
? new Date(metadataData.generatedAt)
: new Date(),
version: typeof metadataData.version === 'string' ? metadataData.version : '1.0.0',
isValid: typeof metadataData.isValid === 'boolean' ? metadataData.isValid : true,
validationErrors: Array.isArray(metadataData.validationErrors) ? metadataData.validationErrors : []
};
const graph = {
projectId: graphData_.projectId || '',
nodes: new Map(Object.entries(graphData_.nodes || {})),
edges,
executionOrder,
criticalPath,
statistics,
metadata
};
return {
success: true,
data: graph,
metadata: {
filePath: graphFilePath,
operation: 'load_dependency_graph',
timestamp: new Date()
}
};
}
catch (error) {
logger.error({ err: error, projectId }, 'Failed to load dependency graph');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: this.getGraphFilePath(projectId),
operation: 'load_dependency_graph',
timestamp: new Date()
}
};
}
}
async deleteDependencyGraph(projectId) {
try {
logger.info({ projectId }, 'Deleting dependency graph');
const graphFilePath = this.getGraphFilePath(projectId);
if (!await FileUtils.fileExists(graphFilePath)) {
return {
success: true,
metadata: {
filePath: graphFilePath,
operation: 'delete_dependency_graph',
timestamp: new Date()
}
};
}
const deleteResult = await FileUtils.deleteFile(graphFilePath);
if (!deleteResult.success) {
return deleteResult;
}
logger.info({ projectId }, 'Dependency graph deleted successfully');
return {
success: true,
metadata: {
filePath: graphFilePath,
operation: 'delete_dependency_graph',
timestamp: new Date()
}
};
}
catch (error) {
logger.error({ err: error, projectId }, 'Failed to delete dependency graph');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: this.getGraphFilePath(projectId),
operation: 'delete_dependency_graph',
timestamp: new Date()
}
};
}
}
getDependencyFilePath(dependencyId) {
return path.join(this.dependenciesDirectory, `${dependencyId}.yaml`);
}
getGraphFilePath(projectId) {
return path.join(this.graphsDirectory, `${projectId}-graph.json`);
}
validateDependency(dependency) {
const errors = [];
if (!dependency.id || typeof dependency.id !== 'string') {
errors.push('Dependency ID is required and must be a string');
}
if (!dependency.fromTaskId || typeof dependency.fromTaskId !== 'string') {
errors.push('From task ID is required and must be a string');
}
if (!dependency.toTaskId || typeof dependency.toTaskId !== 'string') {
errors.push('To task ID is required and must be a string');
}
if (dependency.fromTaskId === dependency.toTaskId) {
errors.push('A task cannot depend on itself');
}
if (!['blocks', 'enables', 'requires', 'suggests'].includes(dependency.type)) {
errors.push('Dependency type must be one of: blocks, enables, requires, suggests');
}
if (!dependency.description || typeof dependency.description !== 'string') {
errors.push('Dependency description is required and must be a string');
}
if (typeof dependency.critical !== 'boolean') {
errors.push('Critical flag must be a boolean');
}
return {
valid: errors.length === 0,
errors
};
}
isDependencyInProject(_dependency, _projectId) {
return true;
}
async loadIndex() {
if (!await FileUtils.fileExists(this.dependencyIndexFile)) {
const initResult = await this.initialize();
if (!initResult.success) {
return initResult;
}
}
const result = await FileUtils.readJsonFile(this.dependencyIndexFile);
if (!result.success) {
return result;
}
if (!isDependencyIndex(result.data)) {
return {
success: false,
error: 'Invalid dependency index format',
metadata: result.metadata
};
}
return {
success: true,
data: result.data,
metadata: result.metadata
};
}
async updateIndex(operation, dependencyId, dependencyInfo) {
try {
const indexResult = await this.loadIndex();
if (!indexResult.success) {
return indexResult;
}
const index = indexResult.data;
switch (operation) {
case 'add':
if (!index.dependencies.find(d => d.id === dependencyId) && dependencyInfo) {
index.dependencies.push(dependencyInfo);
}
break;
case 'update': {
const updateIndex = index.dependencies.findIndex(d => d.id === dependencyId);
if (updateIndex !== -1 && dependencyInfo) {
index.dependencies[updateIndex] = dependencyInfo;
}
break;
}
case 'remove':
index.dependencies = index.dependencies.filter(d => d.id !== dependencyId);
break;
}
index.lastUpdated = new Date().toISOString();
return await FileUtils.writeJsonFile(this.dependencyIndexFile, index);
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: this.dependencyIndexFile,
operation: 'update_index',
timestamp: new Date()
}
};
}
}
}