mega-minds
Version:
Enhanced multi-agent workflow system for Claude Code projects with automated handoff management and Claude Code hooks integration
554 lines (462 loc) • 18.9 kB
JavaScript
// lib/core/ProjectManager.js
// Multi-project management for mega-minds with full backward compatibility
// Phase 3.2: Enterprise Features - Multi-project coordination
const fs = require('fs-extra');
const path = require('path');
const os = require('os');
/**
* ProjectManager handles multiple mega-minds projects simultaneously
* Maintains 100% backward compatibility with single-project workflows
* PRD Requirements: Support 10+ concurrent projects with isolation
*/
class ProjectManager {
constructor(options = {}) {
this.options = {
multiProjectMode: false, // Default: single-project mode (backward compatible)
registryPath: options.registryPath || path.join(os.homedir(), '.mega-minds-registry'),
maxProjects: options.maxProjects || 15, // PRD: 10+ projects support
autoSaveInterval: options.autoSaveInterval || 5 * 60 * 1000, // 5 minutes
...options
};
// Current project context (maintains existing behavior)
this.currentProject = null;
this.currentProjectPath = process.cwd();
// Multi-project registry
this.projectRegistry = new Map();
this.registryLoaded = false;
// Project metadata cache
this.projectMetadata = new Map();
// Auto-save timer for registry
this.autoSaveTimer = null;
}
/**
* Initialize project manager
* Maintains single-project mode unless explicitly enabled
*/
async initialize(enableMultiProject = false) {
try {
// Load existing registry if it exists
await this.loadProjectRegistry();
// Enable multi-project mode only if requested
if (enableMultiProject) {
this.options.multiProjectMode = true;
console.log('📁 Multi-project mode enabled');
}
// Initialize current project (backward compatible)
await this.initializeCurrentProject();
// Start auto-save timer if multi-project mode is enabled
if (this.options.multiProjectMode) {
this.startAutoSave();
}
return true;
} catch (error) {
console.warn('⚠️ ProjectManager initialization warning:', error.message);
// Graceful degradation - continue in single-project mode
this.options.multiProjectMode = false;
return false;
}
}
/**
* Initialize current project (maintains existing behavior)
*/
async initializeCurrentProject() {
const projectPath = this.currentProjectPath;
const projectName = path.basename(projectPath);
// Create project info for current directory
this.currentProject = {
id: this.generateProjectId(projectPath),
name: projectName,
path: projectPath,
status: 'active',
created: new Date().toISOString(),
lastAccessed: new Date().toISOString(),
megamindsPath: path.join(projectPath, '.mega-minds'),
isDefault: true
};
// Register project if multi-project mode is enabled
if (this.options.multiProjectMode) {
this.projectRegistry.set(this.currentProject.id, this.currentProject);
await this.saveProjectRegistry();
}
// Ensure project directory structure exists
await this.ensureProjectStructure(this.currentProject);
}
/**
* Get current project info (backward compatible)
*/
getCurrentProject() {
return this.currentProject;
}
/**
* Get current project path (maintains existing behavior)
*/
getCurrentProjectPath() {
return this.currentProjectPath;
}
/**
* Check if multi-project mode is enabled
*/
isMultiProjectMode() {
return this.options.multiProjectMode;
}
/**
* Add a new project to the registry
*/
async addProject(projectPath, options = {}) {
if (!this.options.multiProjectMode) {
throw new Error('Multi-project mode not enabled. Use mega-minds enable-multi-project first.');
}
// Validate project path
if (!await fs.pathExists(projectPath)) {
throw new Error(`Project path does not exist: ${projectPath}`);
}
const absolutePath = path.resolve(projectPath);
const projectId = this.generateProjectId(absolutePath);
// Check if project already exists
if (this.projectRegistry.has(projectId)) {
console.log('📁 Project already registered:', path.basename(absolutePath));
return this.projectRegistry.get(projectId);
}
// Check project limit
if (this.projectRegistry.size >= this.options.maxProjects) {
throw new Error(`Maximum projects limit reached (${this.options.maxProjects}). Remove inactive projects first.`);
}
// Create project entry
const project = {
id: projectId,
name: options.name || path.basename(absolutePath),
path: absolutePath,
status: 'active',
created: new Date().toISOString(),
lastAccessed: new Date().toISOString(),
megamindsPath: path.join(absolutePath, '.mega-minds'),
description: options.description || '',
tags: options.tags || []
};
// Ensure project structure
await this.ensureProjectStructure(project);
// Add to registry
this.projectRegistry.set(projectId, project);
await this.saveProjectRegistry();
console.log(`✅ Project added: ${project.name} (${projectId})`);
return project;
}
/**
* Remove project from registry (does not delete files)
*/
async removeProject(projectId) {
if (!this.options.multiProjectMode) {
throw new Error('Multi-project mode not enabled');
}
const project = this.projectRegistry.get(projectId);
if (!project) {
throw new Error(`Project not found: ${projectId}`);
}
// Prevent removing current project
if (this.currentProject && this.currentProject.id === projectId) {
throw new Error('Cannot remove currently active project. Switch to another project first.');
}
// Remove from registry
this.projectRegistry.delete(projectId);
await this.saveProjectRegistry();
console.log(`✅ Project removed from registry: ${project.name}`);
return project;
}
/**
* Switch to a different project
*/
async switchToProject(projectId) {
if (!this.options.multiProjectMode) {
throw new Error('Multi-project mode not enabled');
}
const project = this.projectRegistry.get(projectId);
if (!project) {
throw new Error(`Project not found: ${projectId}`);
}
// Validate project path still exists
if (!await fs.pathExists(project.path)) {
throw new Error(`Project path no longer exists: ${project.path}`);
}
// Update last accessed time for current project
if (this.currentProject) {
this.currentProject.lastAccessed = new Date().toISOString();
this.projectRegistry.set(this.currentProject.id, this.currentProject);
}
// Switch to new project
this.currentProject = project;
this.currentProjectPath = project.path;
project.lastAccessed = new Date().toISOString();
project.status = 'active';
// Update registry
this.projectRegistry.set(projectId, project);
await this.saveProjectRegistry();
console.log(`🔄 Switched to project: ${project.name}`);
console.log(`📁 Project path: ${project.path}`);
return project;
}
/**
* List all registered projects
*/
listProjects() {
if (!this.options.multiProjectMode) {
return [this.currentProject].filter(Boolean);
}
return Array.from(this.projectRegistry.values()).sort((a, b) => {
// Current project first, then by last accessed
if (this.currentProject && a.id === this.currentProject.id) return -1;
if (this.currentProject && b.id === this.currentProject.id) return 1;
return new Date(b.lastAccessed) - new Date(a.lastAccessed);
});
}
/**
* Get project by ID
*/
getProject(projectId) {
if (!this.options.multiProjectMode) {
return this.currentProject?.id === projectId ? this.currentProject : null;
}
return this.projectRegistry.get(projectId);
}
/**
* Find projects by name or path
*/
findProjects(query) {
const projects = this.listProjects();
const lowerQuery = query.toLowerCase();
return projects.filter(project =>
project.name.toLowerCase().includes(lowerQuery) ||
project.path.toLowerCase().includes(lowerQuery) ||
(project.description && project.description.toLowerCase().includes(lowerQuery)) ||
(project.tags && project.tags.some(tag => tag.toLowerCase().includes(lowerQuery)))
);
}
/**
* Get project health status
*/
async getProjectHealth(projectId) {
const project = this.getProject(projectId);
if (!project) {
throw new Error(`Project not found: ${projectId}`);
}
const health = {
project: project,
status: 'healthy',
issues: [],
metrics: {}
};
try {
// Check if project path exists
if (!await fs.pathExists(project.path)) {
health.status = 'error';
health.issues.push('Project directory not found');
return health;
}
// Check mega-minds directory
if (!await fs.pathExists(project.megamindsPath)) {
health.status = 'warning';
health.issues.push('Mega-minds directory not found');
} else {
// Count state files
const stateDir = path.join(project.megamindsPath, 'state');
if (await fs.pathExists(stateDir)) {
const stateFiles = await fs.readdir(stateDir);
health.metrics.stateFiles = stateFiles.length;
}
// Count session files
const sessionsDir = path.join(project.megamindsPath, 'sessions');
if (await fs.pathExists(sessionsDir)) {
const sessionFiles = await fs.readdir(sessionsDir);
health.metrics.sessionFiles = sessionFiles.length;
}
// Count quality reports
const qualityDir = path.join(project.megamindsPath, 'quality', 'reports');
if (await fs.pathExists(qualityDir)) {
const qualityFiles = await fs.readdir(qualityDir);
health.metrics.qualityReports = qualityFiles.length;
}
}
// Check project age
const created = new Date(project.created);
const ageInDays = Math.floor((Date.now() - created.getTime()) / (1000 * 60 * 60 * 24));
health.metrics.ageInDays = ageInDays;
// Check last access
const lastAccessed = new Date(project.lastAccessed);
const lastAccessDays = Math.floor((Date.now() - lastAccessed.getTime()) / (1000 * 60 * 60 * 24));
health.metrics.lastAccessDays = lastAccessDays;
if (lastAccessDays > 30) {
health.status = health.status === 'healthy' ? 'warning' : health.status;
health.issues.push('Project not accessed in 30+ days');
}
} catch (error) {
health.status = 'error';
health.issues.push(`Health check failed: ${error.message}`);
}
return health;
}
/**
* Archive a project (sets status to archived)
*/
async archiveProject(projectId) {
if (!this.options.multiProjectMode) {
throw new Error('Multi-project mode not enabled');
}
const project = this.projectRegistry.get(projectId);
if (!project) {
throw new Error(`Project not found: ${projectId}`);
}
// Prevent archiving current project
if (this.currentProject && this.currentProject.id === projectId) {
throw new Error('Cannot archive currently active project. Switch to another project first.');
}
project.status = 'archived';
project.archivedAt = new Date().toISOString();
this.projectRegistry.set(projectId, project);
await this.saveProjectRegistry();
console.log(`📦 Project archived: ${project.name}`);
return project;
}
/**
* Restore an archived project
*/
async restoreProject(projectId) {
if (!this.options.multiProjectMode) {
throw new Error('Multi-project mode not enabled');
}
const project = this.projectRegistry.get(projectId);
if (!project) {
throw new Error(`Project not found: ${projectId}`);
}
project.status = 'active';
project.restoredAt = new Date().toISOString();
delete project.archivedAt;
this.projectRegistry.set(projectId, project);
await this.saveProjectRegistry();
console.log(`📁 Project restored: ${project.name}`);
return project;
}
/**
* Generate unique project ID
*/
generateProjectId(projectPath) {
// Use path hash for consistent IDs
const crypto = require('crypto');
return crypto.createHash('md5').update(path.resolve(projectPath)).digest('hex').substring(0, 8);
}
/**
* Ensure project directory structure
*/
async ensureProjectStructure(project) {
const megamindsDir = project.megamindsPath;
// Create necessary directories
await fs.ensureDir(path.join(megamindsDir, 'state'));
await fs.ensureDir(path.join(megamindsDir, 'sessions'));
await fs.ensureDir(path.join(megamindsDir, 'quality', 'reports'));
await fs.ensureDir(path.join(megamindsDir, 'intelligence'));
// Create project info file
const projectInfoFile = path.join(megamindsDir, 'project-info.json');
if (!await fs.pathExists(projectInfoFile)) {
await fs.writeJSON(projectInfoFile, {
id: project.id,
name: project.name,
created: project.created,
version: '2.0'
}, { spaces: 2 });
}
}
/**
* Load project registry from disk
*/
async loadProjectRegistry() {
try {
if (await fs.pathExists(this.options.registryPath)) {
const registryData = await fs.readJSON(this.options.registryPath);
// Load projects into registry
if (registryData.projects) {
for (const project of registryData.projects) {
this.projectRegistry.set(project.id, project);
}
}
// Load options
if (registryData.options) {
Object.assign(this.options, registryData.options);
}
console.log(`📁 Loaded ${this.projectRegistry.size} projects from registry`);
}
this.registryLoaded = true;
} catch (error) {
console.warn('⚠️ Could not load project registry:', error.message);
this.registryLoaded = false;
}
}
/**
* Save project registry to disk
*/
async saveProjectRegistry() {
try {
const registryData = {
version: '2.0',
lastUpdated: new Date().toISOString(),
projects: Array.from(this.projectRegistry.values()),
options: {
multiProjectMode: this.options.multiProjectMode,
maxProjects: this.options.maxProjects
}
};
// Ensure registry directory exists
await fs.ensureDir(path.dirname(this.options.registryPath));
// Save registry
await fs.writeJSON(this.options.registryPath, registryData, { spaces: 2 });
} catch (error) {
console.warn('⚠️ Could not save project registry:', error.message);
}
}
/**
* Start auto-save timer
*/
startAutoSave() {
if (this.autoSaveTimer) {
clearInterval(this.autoSaveTimer);
}
this.autoSaveTimer = setInterval(() => {
this.saveProjectRegistry().catch(error => {
console.warn('⚠️ Auto-save failed:', error.message);
});
}, this.options.autoSaveInterval);
}
/**
* Stop auto-save timer
*/
stopAutoSave() {
if (this.autoSaveTimer) {
clearInterval(this.autoSaveTimer);
this.autoSaveTimer = null;
}
}
/**
* Cleanup and shutdown
*/
async shutdown() {
this.stopAutoSave();
if (this.options.multiProjectMode) {
await this.saveProjectRegistry();
}
console.log('📁 ProjectManager shutdown complete');
}
/**
* Get registry statistics
*/
getStats() {
const projects = this.listProjects();
const activeProjects = projects.filter(p => p.status === 'active');
const archivedProjects = projects.filter(p => p.status === 'archived');
return {
totalProjects: projects.length,
activeProjects: activeProjects.length,
archivedProjects: archivedProjects.length,
currentProject: this.currentProject?.name || 'None',
multiProjectMode: this.options.multiProjectMode,
registryPath: this.options.registryPath
};
}
}
module.exports = ProjectManager;