mirror of
https://github.com/SuperClaude-Org/SuperClaude_Framework.git
synced 2025-12-29 16:16:08 +00:00
SuperClaude V4 Beta: Major framework restructuring
- Restructured core framework components - Added new Agents, MCP servers, and Modes documentation - Introduced SuperClaude-Lite minimal implementation - Enhanced Commands with session management capabilities - Added comprehensive Hooks system with Python integration - Removed legacy setup and profile components - Updated .gitignore to exclude Tests/, ClaudeDocs/, and .serena/ - Consolidated configuration into SuperClaude/Config/ - Added Templates for consistent component creation This is the initial commit for the V4 Beta branch containing all recent framework improvements and architectural changes. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
"""Component implementations for SuperClaude installation system"""
|
||||
|
||||
from .core import CoreComponent
|
||||
from .commands import CommandsComponent
|
||||
from .mcp import MCPComponent
|
||||
from .hooks import HooksComponent
|
||||
|
||||
__all__ = [
|
||||
'CoreComponent',
|
||||
'CommandsComponent',
|
||||
'MCPComponent',
|
||||
'HooksComponent'
|
||||
]
|
||||
@@ -1,329 +0,0 @@
|
||||
"""
|
||||
Commands component for SuperClaude slash command definitions
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Tuple, Optional, Any
|
||||
from pathlib import Path
|
||||
|
||||
from ..base.component import Component
|
||||
|
||||
class CommandsComponent(Component):
|
||||
"""SuperClaude slash commands component"""
|
||||
|
||||
def __init__(self, install_dir: Optional[Path] = None):
|
||||
"""Initialize commands component"""
|
||||
super().__init__(install_dir, Path("commands/sc"))
|
||||
|
||||
def get_metadata(self) -> Dict[str, str]:
|
||||
"""Get component metadata"""
|
||||
return {
|
||||
"name": "commands",
|
||||
"version": "3.0.0",
|
||||
"description": "SuperClaude slash command definitions",
|
||||
"category": "commands"
|
||||
}
|
||||
|
||||
def get_metadata_modifications(self) -> Dict[str, Any]:
|
||||
"""Get metadata modifications for commands component"""
|
||||
return {
|
||||
"components": {
|
||||
"commands": {
|
||||
"version": "3.0.0",
|
||||
"installed": True,
|
||||
"files_count": len(self.component_files)
|
||||
}
|
||||
},
|
||||
"commands": {
|
||||
"enabled": True,
|
||||
"version": "3.0.0",
|
||||
"auto_update": False
|
||||
}
|
||||
}
|
||||
|
||||
def _install(self, config: Dict[str, Any]) -> bool:
|
||||
"""Install commands component"""
|
||||
self.logger.info("Installing SuperClaude command definitions...")
|
||||
|
||||
# Check for and migrate existing commands from old location
|
||||
self._migrate_existing_commands()
|
||||
|
||||
return super()._install(config);
|
||||
|
||||
def _post_install(self):
|
||||
# Update metadata
|
||||
try:
|
||||
metadata_mods = self.get_metadata_modifications()
|
||||
self.settings_manager.update_metadata(metadata_mods)
|
||||
self.logger.info("Updated metadata with commands configuration")
|
||||
|
||||
# Add component registration to metadata
|
||||
self.settings_manager.add_component_registration("commands", {
|
||||
"version": "3.0.0",
|
||||
"category": "commands",
|
||||
"files_count": len(self.component_files)
|
||||
})
|
||||
self.logger.info("Updated metadata with commands component registration")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to update metadata: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def uninstall(self) -> bool:
|
||||
"""Uninstall commands component"""
|
||||
try:
|
||||
self.logger.info("Uninstalling SuperClaude commands component...")
|
||||
|
||||
# Remove command files from sc subdirectory
|
||||
commands_dir = self.install_dir / "commands" / "sc"
|
||||
removed_count = 0
|
||||
|
||||
for filename in self.component_files:
|
||||
file_path = commands_dir / filename
|
||||
if self.file_manager.remove_file(file_path):
|
||||
removed_count += 1
|
||||
self.logger.debug(f"Removed {filename}")
|
||||
else:
|
||||
self.logger.warning(f"Could not remove {filename}")
|
||||
|
||||
# Also check and remove any old commands in root commands directory
|
||||
old_commands_dir = self.install_dir / "commands"
|
||||
old_removed_count = 0
|
||||
|
||||
for filename in self.component_files:
|
||||
old_file_path = old_commands_dir / filename
|
||||
if old_file_path.exists() and old_file_path.is_file():
|
||||
if self.file_manager.remove_file(old_file_path):
|
||||
old_removed_count += 1
|
||||
self.logger.debug(f"Removed old {filename}")
|
||||
else:
|
||||
self.logger.warning(f"Could not remove old {filename}")
|
||||
|
||||
if old_removed_count > 0:
|
||||
self.logger.info(f"Also removed {old_removed_count} commands from old location")
|
||||
|
||||
removed_count += old_removed_count
|
||||
|
||||
# Remove sc subdirectory if empty
|
||||
try:
|
||||
if commands_dir.exists():
|
||||
remaining_files = list(commands_dir.iterdir())
|
||||
if not remaining_files:
|
||||
commands_dir.rmdir()
|
||||
self.logger.debug("Removed empty sc commands directory")
|
||||
|
||||
# Also remove parent commands directory if empty
|
||||
parent_commands_dir = self.install_dir / "commands"
|
||||
if parent_commands_dir.exists():
|
||||
remaining_files = list(parent_commands_dir.iterdir())
|
||||
if not remaining_files:
|
||||
parent_commands_dir.rmdir()
|
||||
self.logger.debug("Removed empty parent commands directory")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not remove commands directory: {e}")
|
||||
|
||||
# Update metadata to remove commands component
|
||||
try:
|
||||
if self.settings_manager.is_component_installed("commands"):
|
||||
self.settings_manager.remove_component_registration("commands")
|
||||
# Also remove commands configuration from metadata
|
||||
metadata = self.settings_manager.load_metadata()
|
||||
if "commands" in metadata:
|
||||
del metadata["commands"]
|
||||
self.settings_manager.save_metadata(metadata)
|
||||
self.logger.info("Removed commands component from metadata")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not update metadata: {e}")
|
||||
|
||||
self.logger.success(f"Commands component uninstalled ({removed_count} files removed)")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.exception(f"Unexpected error during commands uninstallation: {e}")
|
||||
return False
|
||||
|
||||
def get_dependencies(self) -> List[str]:
|
||||
"""Get dependencies"""
|
||||
return ["core"]
|
||||
|
||||
def update(self, config: Dict[str, Any]) -> bool:
|
||||
"""Update commands component"""
|
||||
try:
|
||||
self.logger.info("Updating SuperClaude commands component...")
|
||||
|
||||
# Check current version
|
||||
current_version = self.settings_manager.get_component_version("commands")
|
||||
target_version = self.get_metadata()["version"]
|
||||
|
||||
if current_version == target_version:
|
||||
self.logger.info(f"Commands component already at version {target_version}")
|
||||
return True
|
||||
|
||||
self.logger.info(f"Updating commands component from {current_version} to {target_version}")
|
||||
|
||||
# Create backup of existing command files
|
||||
commands_dir = self.install_dir / "commands" / "sc"
|
||||
backup_files = []
|
||||
|
||||
if commands_dir.exists():
|
||||
for filename in self.component_files:
|
||||
file_path = commands_dir / filename
|
||||
if file_path.exists():
|
||||
backup_path = self.file_manager.backup_file(file_path)
|
||||
if backup_path:
|
||||
backup_files.append(backup_path)
|
||||
self.logger.debug(f"Backed up {filename}")
|
||||
|
||||
# Perform installation (overwrites existing files)
|
||||
success = self.install(config)
|
||||
|
||||
if success:
|
||||
# Remove backup files on successful update
|
||||
for backup_path in backup_files:
|
||||
try:
|
||||
backup_path.unlink()
|
||||
except Exception:
|
||||
pass # Ignore cleanup errors
|
||||
|
||||
self.logger.success(f"Commands component updated to version {target_version}")
|
||||
else:
|
||||
# Restore from backup on failure
|
||||
self.logger.warning("Update failed, restoring from backup...")
|
||||
for backup_path in backup_files:
|
||||
try:
|
||||
original_path = backup_path.with_suffix('')
|
||||
backup_path.rename(original_path)
|
||||
self.logger.debug(f"Restored {original_path.name}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Could not restore {backup_path}: {e}")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
self.logger.exception(f"Unexpected error during commands update: {e}")
|
||||
return False
|
||||
|
||||
def validate_installation(self) -> Tuple[bool, List[str]]:
|
||||
"""Validate commands component installation"""
|
||||
errors = []
|
||||
|
||||
# Check if sc commands directory exists
|
||||
commands_dir = self.install_dir / "commands" / "sc"
|
||||
if not commands_dir.exists():
|
||||
errors.append("SC commands directory not found")
|
||||
return False, errors
|
||||
|
||||
# Check if all command files exist
|
||||
for filename in self.component_files:
|
||||
file_path = commands_dir / filename
|
||||
if not file_path.exists():
|
||||
errors.append(f"Missing command file: {filename}")
|
||||
elif not file_path.is_file():
|
||||
errors.append(f"Command file is not a regular file: {filename}")
|
||||
|
||||
# Check metadata registration
|
||||
if not self.settings_manager.is_component_installed("commands"):
|
||||
errors.append("Commands component not registered in metadata")
|
||||
else:
|
||||
# Check version matches
|
||||
installed_version = self.settings_manager.get_component_version("commands")
|
||||
expected_version = self.get_metadata()["version"]
|
||||
if installed_version != expected_version:
|
||||
errors.append(f"Version mismatch: installed {installed_version}, expected {expected_version}")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
def _get_source_dir(self) -> Path:
|
||||
"""Get source directory for command files"""
|
||||
# Assume we're in SuperClaude/setup/components/commands.py
|
||||
# and command files are in SuperClaude/SuperClaude/Commands/
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
return project_root / "SuperClaude" / "Commands"
|
||||
|
||||
def get_size_estimate(self) -> int:
|
||||
"""Get estimated installation size"""
|
||||
total_size = 0
|
||||
source_dir = self._get_source_dir()
|
||||
|
||||
for filename in self.component_files:
|
||||
file_path = source_dir / filename
|
||||
if file_path.exists():
|
||||
total_size += file_path.stat().st_size
|
||||
|
||||
# Add overhead for directory and settings
|
||||
total_size += 5120 # ~5KB overhead
|
||||
|
||||
return total_size
|
||||
|
||||
def get_installation_summary(self) -> Dict[str, Any]:
|
||||
"""Get installation summary"""
|
||||
return {
|
||||
"component": self.get_metadata()["name"],
|
||||
"version": self.get_metadata()["version"],
|
||||
"files_installed": len(self.component_files),
|
||||
"command_files": self.component_files,
|
||||
"estimated_size": self.get_size_estimate(),
|
||||
"install_directory": str(self.install_dir / "commands" / "sc"),
|
||||
"dependencies": self.get_dependencies()
|
||||
}
|
||||
|
||||
def _migrate_existing_commands(self) -> None:
|
||||
"""Migrate existing commands from old location to new sc subdirectory"""
|
||||
try:
|
||||
old_commands_dir = self.install_dir / "commands"
|
||||
new_commands_dir = self.install_dir / "commands" / "sc"
|
||||
|
||||
# Check if old commands exist in root commands directory
|
||||
migrated_count = 0
|
||||
commands_to_migrate = []
|
||||
|
||||
if old_commands_dir.exists():
|
||||
for filename in self.component_files:
|
||||
old_file_path = old_commands_dir / filename
|
||||
if old_file_path.exists() and old_file_path.is_file():
|
||||
commands_to_migrate.append(filename)
|
||||
|
||||
if commands_to_migrate:
|
||||
self.logger.info(f"Found {len(commands_to_migrate)} existing commands to migrate to sc/ subdirectory")
|
||||
|
||||
# Ensure new directory exists
|
||||
if not self.file_manager.ensure_directory(new_commands_dir):
|
||||
self.logger.error(f"Could not create sc commands directory: {new_commands_dir}")
|
||||
return
|
||||
|
||||
# Move files from old to new location
|
||||
for filename in commands_to_migrate:
|
||||
old_file_path = old_commands_dir / filename
|
||||
new_file_path = new_commands_dir / filename
|
||||
|
||||
try:
|
||||
# Copy file to new location
|
||||
if self.file_manager.copy_file(old_file_path, new_file_path):
|
||||
# Remove old file
|
||||
if self.file_manager.remove_file(old_file_path):
|
||||
migrated_count += 1
|
||||
self.logger.debug(f"Migrated {filename} to sc/ subdirectory")
|
||||
else:
|
||||
self.logger.warning(f"Could not remove old {filename}")
|
||||
else:
|
||||
self.logger.warning(f"Could not copy {filename} to sc/ subdirectory")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error migrating {filename}: {e}")
|
||||
|
||||
if migrated_count > 0:
|
||||
self.logger.success(f"Successfully migrated {migrated_count} commands to /sc: namespace")
|
||||
self.logger.info("Commands are now available as /sc:analyze, /sc:build, etc.")
|
||||
|
||||
# Try to remove old commands directory if empty
|
||||
try:
|
||||
if old_commands_dir.exists():
|
||||
remaining_files = [f for f in old_commands_dir.iterdir() if f.is_file()]
|
||||
if not remaining_files:
|
||||
# Only remove if no user files remain
|
||||
old_commands_dir.rmdir()
|
||||
self.logger.debug("Removed empty old commands directory")
|
||||
except Exception as e:
|
||||
self.logger.debug(f"Could not remove old commands directory: {e}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error during command migration: {e}")
|
||||
@@ -1,248 +0,0 @@
|
||||
"""
|
||||
Core component for SuperClaude framework files installation
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Tuple, Optional, Any
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
from ..base.component import Component
|
||||
|
||||
class CoreComponent(Component):
|
||||
"""Core SuperClaude framework files component"""
|
||||
|
||||
def __init__(self, install_dir: Optional[Path] = None):
|
||||
"""Initialize core component"""
|
||||
super().__init__(install_dir)
|
||||
|
||||
def get_metadata(self) -> Dict[str, str]:
|
||||
"""Get component metadata"""
|
||||
return {
|
||||
"name": "core",
|
||||
"version": "3.0.0",
|
||||
"description": "SuperClaude framework documentation and core files",
|
||||
"category": "core"
|
||||
}
|
||||
|
||||
def get_metadata_modifications(self) -> Dict[str, Any]:
|
||||
"""Get metadata modifications for SuperClaude"""
|
||||
return {
|
||||
"framework": {
|
||||
"version": "3.0.0",
|
||||
"name": "SuperClaude",
|
||||
"description": "AI-enhanced development framework for Claude Code",
|
||||
"installation_type": "global",
|
||||
"components": ["core"]
|
||||
},
|
||||
"superclaude": {
|
||||
"enabled": True,
|
||||
"version": "3.0.0",
|
||||
"profile": "default",
|
||||
"auto_update": False
|
||||
}
|
||||
}
|
||||
|
||||
def _install(self, config: Dict[str, Any]) -> bool:
|
||||
"""Install core component"""
|
||||
self.logger.info("Installing SuperClaude core framework files...")
|
||||
|
||||
return super()._install(config);
|
||||
|
||||
def _post_install(self):
|
||||
# Create or update metadata
|
||||
try:
|
||||
metadata_mods = self.get_metadata_modifications()
|
||||
self.settings_manager.update_metadata(metadata_mods)
|
||||
self.logger.info("Updated metadata with framework configuration")
|
||||
|
||||
# Add component registration to metadata
|
||||
self.settings_manager.add_component_registration("core", {
|
||||
"version": "3.0.0",
|
||||
"category": "core",
|
||||
"files_count": len(self.component_files)
|
||||
})
|
||||
|
||||
self.logger.info("Updated metadata with core component registration")
|
||||
|
||||
# Migrate any existing SuperClaude data from settings.json
|
||||
if self.settings_manager.migrate_superclaude_data():
|
||||
self.logger.info("Migrated existing SuperClaude data from settings.json")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to update metadata: {e}")
|
||||
return False
|
||||
|
||||
# Create additional directories for other components
|
||||
additional_dirs = ["commands", "hooks", "backups", "logs"]
|
||||
for dirname in additional_dirs:
|
||||
dir_path = self.install_dir / dirname
|
||||
if not self.file_manager.ensure_directory(dir_path):
|
||||
self.logger.warning(f"Could not create directory: {dir_path}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def uninstall(self) -> bool:
|
||||
"""Uninstall core component"""
|
||||
try:
|
||||
self.logger.info("Uninstalling SuperClaude core component...")
|
||||
|
||||
# Remove framework files
|
||||
removed_count = 0
|
||||
for filename in self.component_files:
|
||||
file_path = self.install_dir / filename
|
||||
if self.file_manager.remove_file(file_path):
|
||||
removed_count += 1
|
||||
self.logger.debug(f"Removed {filename}")
|
||||
else:
|
||||
self.logger.warning(f"Could not remove {filename}")
|
||||
|
||||
# Update metadata to remove core component
|
||||
try:
|
||||
if self.settings_manager.is_component_installed("core"):
|
||||
self.settings_manager.remove_component_registration("core")
|
||||
metadata_mods = self.get_metadata_modifications()
|
||||
metadata = self.settings_manager.load_metadata()
|
||||
for key in metadata_mods.keys():
|
||||
if key in metadata:
|
||||
del metadata[key]
|
||||
|
||||
self.settings_manager.save_metadata(metadata)
|
||||
self.logger.info("Removed core component from metadata")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not update metadata: {e}")
|
||||
|
||||
self.logger.success(f"Core component uninstalled ({removed_count} files removed)")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.exception(f"Unexpected error during core uninstallation: {e}")
|
||||
return False
|
||||
|
||||
def get_dependencies(self) -> List[str]:
|
||||
"""Get component dependencies (core has none)"""
|
||||
return []
|
||||
|
||||
def update(self, config: Dict[str, Any]) -> bool:
|
||||
"""Update core component"""
|
||||
try:
|
||||
self.logger.info("Updating SuperClaude core component...")
|
||||
|
||||
# Check current version
|
||||
current_version = self.settings_manager.get_component_version("core")
|
||||
target_version = self.get_metadata()["version"]
|
||||
|
||||
if current_version == target_version:
|
||||
self.logger.info(f"Core component already at version {target_version}")
|
||||
return True
|
||||
|
||||
self.logger.info(f"Updating core component from {current_version} to {target_version}")
|
||||
|
||||
# Create backup of existing files
|
||||
backup_files = []
|
||||
for filename in self.component_files:
|
||||
file_path = self.install_dir / filename
|
||||
if file_path.exists():
|
||||
backup_path = self.file_manager.backup_file(file_path)
|
||||
if backup_path:
|
||||
backup_files.append(backup_path)
|
||||
self.logger.debug(f"Backed up {filename}")
|
||||
|
||||
# Perform installation (overwrites existing files)
|
||||
success = self.install(config)
|
||||
|
||||
if success:
|
||||
# Remove backup files on successful update
|
||||
for backup_path in backup_files:
|
||||
try:
|
||||
backup_path.unlink()
|
||||
except Exception:
|
||||
pass # Ignore cleanup errors
|
||||
|
||||
self.logger.success(f"Core component updated to version {target_version}")
|
||||
else:
|
||||
# Restore from backup on failure
|
||||
self.logger.warning("Update failed, restoring from backup...")
|
||||
for backup_path in backup_files:
|
||||
try:
|
||||
original_path = backup_path.with_suffix('')
|
||||
shutil.move(str(backup_path), str(original_path))
|
||||
self.logger.debug(f"Restored {original_path.name}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Could not restore {backup_path}: {e}")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
self.logger.exception(f"Unexpected error during core update: {e}")
|
||||
return False
|
||||
|
||||
def validate_installation(self) -> Tuple[bool, List[str]]:
|
||||
"""Validate core component installation"""
|
||||
errors = []
|
||||
|
||||
# Check if all framework files exist
|
||||
for filename in self.component_files:
|
||||
file_path = self.install_dir / filename
|
||||
if not file_path.exists():
|
||||
errors.append(f"Missing framework file: {filename}")
|
||||
elif not file_path.is_file():
|
||||
errors.append(f"Framework file is not a regular file: {filename}")
|
||||
|
||||
# Check metadata registration
|
||||
if not self.settings_manager.is_component_installed("core"):
|
||||
errors.append("Core component not registered in metadata")
|
||||
else:
|
||||
# Check version matches
|
||||
installed_version = self.settings_manager.get_component_version("core")
|
||||
expected_version = self.get_metadata()["version"]
|
||||
if installed_version != expected_version:
|
||||
errors.append(f"Version mismatch: installed {installed_version}, expected {expected_version}")
|
||||
|
||||
# Check metadata structure
|
||||
try:
|
||||
framework_config = self.settings_manager.get_metadata_setting("framework")
|
||||
if not framework_config:
|
||||
errors.append("Missing framework configuration in metadata")
|
||||
else:
|
||||
required_keys = ["version", "name", "description"]
|
||||
for key in required_keys:
|
||||
if key not in framework_config:
|
||||
errors.append(f"Missing framework.{key} in metadata")
|
||||
except Exception as e:
|
||||
errors.append(f"Could not validate metadata: {e}")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
def _get_source_dir(self):
|
||||
"""Get source directory for framework files"""
|
||||
# Assume we're in SuperClaude/setup/components/core.py
|
||||
# and framework files are in SuperClaude/SuperClaude/Core/
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
return project_root / "SuperClaude" / "Core"
|
||||
|
||||
def get_size_estimate(self) -> int:
|
||||
"""Get estimated installation size"""
|
||||
total_size = 0
|
||||
source_dir = self._get_source_dir()
|
||||
|
||||
for filename in self.component_files:
|
||||
file_path = source_dir / filename
|
||||
if file_path.exists():
|
||||
total_size += file_path.stat().st_size
|
||||
|
||||
# Add overhead for settings.json and directories
|
||||
total_size += 10240 # ~10KB overhead
|
||||
|
||||
return total_size
|
||||
|
||||
def get_installation_summary(self) -> Dict[str, Any]:
|
||||
"""Get installation summary"""
|
||||
return {
|
||||
"component": self.get_metadata()["name"],
|
||||
"version": self.get_metadata()["version"],
|
||||
"files_installed": len(self.component_files),
|
||||
"framework_files": self.component_files,
|
||||
"estimated_size": self.get_size_estimate(),
|
||||
"install_directory": str(self.install_dir),
|
||||
"dependencies": self.get_dependencies()
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
"""
|
||||
Hooks component for Claude Code hooks integration (future-ready)
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Tuple, Optional, Any
|
||||
from pathlib import Path
|
||||
|
||||
from ..base.component import Component
|
||||
|
||||
|
||||
class HooksComponent(Component):
|
||||
"""Claude Code hooks integration component"""
|
||||
|
||||
def __init__(self, install_dir: Optional[Path] = None):
|
||||
"""Initialize hooks component"""
|
||||
super().__init__(install_dir, Path("hooks"))
|
||||
|
||||
# Define hook files to install (when hooks are ready)
|
||||
self.hook_files = [
|
||||
"pre_tool_use.py",
|
||||
"post_tool_use.py",
|
||||
"error_handler.py",
|
||||
"context_accumulator.py",
|
||||
"performance_monitor.py"
|
||||
]
|
||||
|
||||
def get_metadata(self) -> Dict[str, str]:
|
||||
"""Get component metadata"""
|
||||
return {
|
||||
"name": "hooks",
|
||||
"version": "3.0.0",
|
||||
"description": "Claude Code hooks integration (future-ready)",
|
||||
"category": "integration"
|
||||
}
|
||||
def get_metadata_modifications(self) -> Dict[str, Any]:
|
||||
# Build hooks configuration based on available files
|
||||
hook_config = {}
|
||||
for filename in self.hook_files:
|
||||
hook_path = self.install_component_subdir / filename
|
||||
if hook_path.exists():
|
||||
hook_name = filename.replace('.py', '')
|
||||
hook_config[hook_name] = [str(hook_path)]
|
||||
|
||||
metadata_mods = {
|
||||
"components": {
|
||||
"hooks": {
|
||||
"version": "3.0.0",
|
||||
"installed": True,
|
||||
"files_count": len(hook_config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Only add hooks configuration if we have actual hook files
|
||||
if hook_config:
|
||||
metadata_mods["hooks"] = {
|
||||
"enabled": True,
|
||||
**hook_config
|
||||
}
|
||||
|
||||
|
||||
return metadata_mods
|
||||
|
||||
def _install(self, config: Dict[str, Any]) -> bool:
|
||||
"""Install hooks component"""
|
||||
self.logger.info("Installing SuperClaude hooks component...")
|
||||
|
||||
# This component is future-ready - hooks aren't implemented yet
|
||||
source_dir = self._get_source_dir()
|
||||
|
||||
if not source_dir.exists() or (source_dir / "PLACEHOLDER.py").exists :
|
||||
self.logger.info("Hooks are not yet implemented - installing placeholder component")
|
||||
|
||||
# Create placeholder hooks directory
|
||||
if not self.file_manager.ensure_directory(self.install_component_subdir):
|
||||
self.logger.error(f"Could not create hooks directory: {self.install_component_subdir}")
|
||||
return False
|
||||
|
||||
# Create placeholder file
|
||||
placeholder_content = '''"""
|
||||
SuperClaude Hooks - Future Implementation
|
||||
|
||||
This directory is reserved for Claude Code hooks integration.
|
||||
Hooks will provide lifecycle management and automation capabilities.
|
||||
|
||||
Planned hooks:
|
||||
- pre_tool_use: Execute before tool usage
|
||||
- post_tool_use: Execute after tool completion
|
||||
- error_handler: Handle tool errors and recovery
|
||||
- context_accumulator: Manage context across operations
|
||||
- performance_monitor: Track and optimize performance
|
||||
|
||||
For more information, see SuperClaude documentation.
|
||||
"""
|
||||
|
||||
# Placeholder for future hooks implementation
|
||||
def placeholder_hook():
|
||||
"""Placeholder hook function"""
|
||||
pass
|
||||
'''
|
||||
|
||||
placeholder_path = self.install_component_subdir / "PLACEHOLDER.py"
|
||||
try:
|
||||
with open(placeholder_path, 'w') as f:
|
||||
f.write(placeholder_content)
|
||||
self.logger.debug("Created hooks placeholder file")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not create placeholder file: {e}")
|
||||
|
||||
# Update settings with placeholder registration
|
||||
try:
|
||||
metadata_mods = {
|
||||
"components": {
|
||||
"hooks": {
|
||||
"version": "3.0.0",
|
||||
"installed": True,
|
||||
"status": "placeholder",
|
||||
"files_count": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
self.settings_manager.update_metadata(metadata_mods)
|
||||
self.logger.info("Updated metadata with hooks component registration")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to update metadata for hooks component: {e}")
|
||||
return False
|
||||
|
||||
self.logger.success("Hooks component installed successfully (placeholder)")
|
||||
return True
|
||||
|
||||
# If hooks source directory exists, install actual hooks
|
||||
self.logger.info("Installing actual hook files...")
|
||||
|
||||
# Validate installation
|
||||
success, errors = self.validate_prerequisites(Path("hooks"))
|
||||
if not success:
|
||||
for error in errors:
|
||||
self.logger.error(error)
|
||||
return False
|
||||
|
||||
# Get files to install
|
||||
files_to_install = self.get_files_to_install()
|
||||
|
||||
if not files_to_install:
|
||||
self.logger.warning("No hook files found to install")
|
||||
return False
|
||||
|
||||
# Copy hook files
|
||||
success_count = 0
|
||||
for source, target in files_to_install:
|
||||
self.logger.debug(f"Copying {source.name} to {target}")
|
||||
|
||||
if self.file_manager.copy_file(source, target):
|
||||
success_count += 1
|
||||
self.logger.debug(f"Successfully copied {source.name}")
|
||||
else:
|
||||
self.logger.error(f"Failed to copy {source.name}")
|
||||
|
||||
if success_count != len(files_to_install):
|
||||
self.logger.error(f"Only {success_count}/{len(files_to_install)} hook files copied successfully")
|
||||
return False
|
||||
|
||||
self.logger.success(f"Hooks component installed successfully ({success_count} hook files)")
|
||||
|
||||
return self._post_install()
|
||||
|
||||
def _post_install(self):
|
||||
# Update metadata
|
||||
try:
|
||||
metadata_mods = self.get_metadata_modifications()
|
||||
self.settings_manager.update_metadata(metadata_mods)
|
||||
self.logger.info("Updated metadata with hooks configuration")
|
||||
|
||||
# Add hook registration to metadata
|
||||
self.settings_manager.add_component_registration("hooks", {
|
||||
"version": "3.0.0",
|
||||
"category": "commands",
|
||||
"files_count": len(self.hook_files)
|
||||
})
|
||||
|
||||
self.logger.info("Updated metadata with commands component registration")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to update metadata: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def uninstall(self) -> bool:
|
||||
"""Uninstall hooks component"""
|
||||
try:
|
||||
self.logger.info("Uninstalling SuperClaude hooks component...")
|
||||
|
||||
# Remove hook files and placeholder
|
||||
removed_count = 0
|
||||
|
||||
# Remove actual hook files
|
||||
for filename in self.hook_files:
|
||||
file_path = self.install_component_subdir / filename
|
||||
if self.file_manager.remove_file(file_path):
|
||||
removed_count += 1
|
||||
self.logger.debug(f"Removed {filename}")
|
||||
|
||||
# Remove placeholder file
|
||||
placeholder_path = self.install_component_subdir / "PLACEHOLDER.py"
|
||||
if self.file_manager.remove_file(placeholder_path):
|
||||
removed_count += 1
|
||||
self.logger.debug("Removed hooks placeholder")
|
||||
|
||||
# Remove hooks directory if empty
|
||||
try:
|
||||
if self.install_component_subdir.exists():
|
||||
remaining_files = list(self.install_component_subdir.iterdir())
|
||||
if not remaining_files:
|
||||
self.install_component_subdir.rmdir()
|
||||
self.logger.debug("Removed empty hooks directory")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not remove hooks directory: {e}")
|
||||
|
||||
# Update settings.json to remove hooks component and configuration
|
||||
try:
|
||||
if self.settings_manager.is_component_installed("hooks"):
|
||||
self.settings_manager.remove_component_registration("hooks")
|
||||
|
||||
# Also remove hooks configuration section if it exists
|
||||
settings = self.settings_manager.load_settings()
|
||||
if "hooks" in settings:
|
||||
del settings["hooks"]
|
||||
self.settings_manager.save_settings(settings)
|
||||
|
||||
self.logger.info("Removed hooks component and configuration from settings.json")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not update settings.json: {e}")
|
||||
|
||||
self.logger.success(f"Hooks component uninstalled ({removed_count} files removed)")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.exception(f"Unexpected error during hooks uninstallation: {e}")
|
||||
return False
|
||||
|
||||
def get_dependencies(self) -> List[str]:
|
||||
"""Get dependencies"""
|
||||
return ["core"]
|
||||
|
||||
def update(self, config: Dict[str, Any]) -> bool:
|
||||
"""Update hooks component"""
|
||||
try:
|
||||
self.logger.info("Updating SuperClaude hooks component...")
|
||||
|
||||
# Check current version
|
||||
current_version = self.settings_manager.get_component_version("hooks")
|
||||
target_version = self.get_metadata()["version"]
|
||||
|
||||
if current_version == target_version:
|
||||
self.logger.info(f"Hooks component already at version {target_version}")
|
||||
return True
|
||||
|
||||
self.logger.info(f"Updating hooks component from {current_version} to {target_version}")
|
||||
|
||||
# Create backup of existing hook files
|
||||
backup_files = []
|
||||
|
||||
if self.install_component_subdir.exists():
|
||||
for filename in self.hook_files + ["PLACEHOLDER.py"]:
|
||||
file_path = self.install_component_subdir / filename
|
||||
if file_path.exists():
|
||||
backup_path = self.file_manager.backup_file(file_path)
|
||||
if backup_path:
|
||||
backup_files.append(backup_path)
|
||||
self.logger.debug(f"Backed up {filename}")
|
||||
|
||||
# Perform installation (overwrites existing files)
|
||||
success = self.install(config)
|
||||
|
||||
if success:
|
||||
# Remove backup files on successful update
|
||||
for backup_path in backup_files:
|
||||
try:
|
||||
backup_path.unlink()
|
||||
except Exception:
|
||||
pass # Ignore cleanup errors
|
||||
|
||||
self.logger.success(f"Hooks component updated to version {target_version}")
|
||||
else:
|
||||
# Restore from backup on failure
|
||||
self.logger.warning("Update failed, restoring from backup...")
|
||||
for backup_path in backup_files:
|
||||
try:
|
||||
original_path = backup_path.with_suffix('')
|
||||
backup_path.rename(original_path)
|
||||
self.logger.debug(f"Restored {original_path.name}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Could not restore {backup_path}: {e}")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
self.logger.exception(f"Unexpected error during hooks update: {e}")
|
||||
return False
|
||||
|
||||
def validate_installation(self) -> Tuple[bool, List[str]]:
|
||||
"""Validate hooks component installation"""
|
||||
errors = []
|
||||
|
||||
# Check if hooks directory exists
|
||||
if not self.install_component_subdir.exists():
|
||||
errors.append("Hooks directory not found")
|
||||
return False, errors
|
||||
|
||||
# Check settings.json registration
|
||||
if not self.settings_manager.is_component_installed("hooks"):
|
||||
errors.append("Hooks component not registered in settings.json")
|
||||
else:
|
||||
# Check version matches
|
||||
installed_version = self.settings_manager.get_component_version("hooks")
|
||||
expected_version = self.get_metadata()["version"]
|
||||
if installed_version != expected_version:
|
||||
errors.append(f"Version mismatch: installed {installed_version}, expected {expected_version}")
|
||||
|
||||
# Check if we have either actual hooks or placeholder
|
||||
has_placeholder = (self.install_component_subdir / "PLACEHOLDER.py").exists()
|
||||
has_actual_hooks = any((self.install_component_subdir / filename).exists() for filename in self.hook_files)
|
||||
|
||||
if not has_placeholder and not has_actual_hooks:
|
||||
errors.append("No hook files or placeholder found")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
def _get_source_dir(self) -> Path:
|
||||
"""Get source directory for hook files"""
|
||||
# Assume we're in SuperClaude/setup/components/hooks.py
|
||||
# and hook files are in SuperClaude/SuperClaude/Hooks/
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
return project_root / "SuperClaude" / "Hooks"
|
||||
|
||||
def get_size_estimate(self) -> int:
|
||||
"""Get estimated installation size"""
|
||||
# Estimate based on placeholder or actual files
|
||||
source_dir = self._get_source_dir()
|
||||
total_size = 0
|
||||
|
||||
if source_dir.exists():
|
||||
for filename in self.hook_files:
|
||||
file_path = source_dir / filename
|
||||
if file_path.exists():
|
||||
total_size += file_path.stat().st_size
|
||||
|
||||
# Add placeholder overhead or minimum size
|
||||
total_size = max(total_size, 10240) # At least 10KB
|
||||
|
||||
return total_size
|
||||
|
||||
def get_installation_summary(self) -> Dict[str, Any]:
|
||||
"""Get installation summary"""
|
||||
source_dir = self._get_source_dir()
|
||||
status = "placeholder" if not source_dir.exists() else "implemented"
|
||||
|
||||
return {
|
||||
"component": self.get_metadata()["name"],
|
||||
"version": self.get_metadata()["version"],
|
||||
"status": status,
|
||||
"hook_files": self.hook_files if source_dir.exists() else ["PLACEHOLDER.py"],
|
||||
"estimated_size": self.get_size_estimate(),
|
||||
"install_directory": str(self.install_dir / "hooks"),
|
||||
"dependencies": self.get_dependencies()
|
||||
}
|
||||
@@ -1,498 +0,0 @@
|
||||
"""
|
||||
MCP component for MCP server integration
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Dict, List, Tuple, Optional, Any
|
||||
from pathlib import Path
|
||||
|
||||
from ..base.component import Component
|
||||
from ..utils.ui import display_info, display_warning
|
||||
|
||||
|
||||
class MCPComponent(Component):
|
||||
"""MCP servers integration component"""
|
||||
|
||||
def __init__(self, install_dir: Optional[Path] = None):
|
||||
"""Initialize MCP component"""
|
||||
super().__init__(install_dir)
|
||||
|
||||
# Define MCP servers to install
|
||||
self.mcp_servers = {
|
||||
"sequential-thinking": {
|
||||
"name": "sequential-thinking",
|
||||
"description": "Multi-step problem solving and systematic analysis",
|
||||
"npm_package": "@modelcontextprotocol/server-sequential-thinking",
|
||||
"required": True
|
||||
},
|
||||
"context7": {
|
||||
"name": "context7",
|
||||
"description": "Official library documentation and code examples",
|
||||
"npm_package": "@upstash/context7-mcp",
|
||||
"required": True
|
||||
},
|
||||
"magic": {
|
||||
"name": "magic",
|
||||
"description": "Modern UI component generation and design systems",
|
||||
"npm_package": "@21st-dev/magic",
|
||||
"required": False,
|
||||
"api_key_env": "TWENTYFIRST_API_KEY",
|
||||
"api_key_description": "21st.dev API key for UI component generation"
|
||||
},
|
||||
"playwright": {
|
||||
"name": "playwright",
|
||||
"description": "Cross-browser E2E testing and automation",
|
||||
"npm_package": "@playwright/mcp@latest",
|
||||
"required": False
|
||||
}
|
||||
}
|
||||
|
||||
def get_metadata(self) -> Dict[str, str]:
|
||||
"""Get component metadata"""
|
||||
return {
|
||||
"name": "mcp",
|
||||
"version": "3.0.0",
|
||||
"description": "MCP server integration (Context7, Sequential, Magic, Playwright)",
|
||||
"category": "integration"
|
||||
}
|
||||
|
||||
def validate_prerequisites(self, installSubPath: Optional[Path] = None) -> Tuple[bool, List[str]]:
|
||||
"""Check prerequisites"""
|
||||
errors = []
|
||||
|
||||
# Check if Node.js is available
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["node", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
shell=(sys.platform == "win32")
|
||||
)
|
||||
if result.returncode != 0:
|
||||
errors.append("Node.js not found - required for MCP servers")
|
||||
else:
|
||||
version = result.stdout.strip()
|
||||
self.logger.debug(f"Found Node.js {version}")
|
||||
|
||||
# Check version (require 18+)
|
||||
try:
|
||||
version_num = int(version.lstrip('v').split('.')[0])
|
||||
if version_num < 18:
|
||||
errors.append(f"Node.js version {version} found, but version 18+ required")
|
||||
except:
|
||||
self.logger.warning(f"Could not parse Node.js version: {version}")
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
errors.append("Node.js not found - required for MCP servers")
|
||||
|
||||
# Check if Claude CLI is available
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["claude", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
shell=(sys.platform == "win32")
|
||||
)
|
||||
if result.returncode != 0:
|
||||
errors.append("Claude CLI not found - required for MCP server management")
|
||||
else:
|
||||
version = result.stdout.strip()
|
||||
self.logger.debug(f"Found Claude CLI {version}")
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
errors.append("Claude CLI not found - required for MCP server management")
|
||||
|
||||
# Check if npm is available
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["npm", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
shell=(sys.platform == "win32")
|
||||
)
|
||||
if result.returncode != 0:
|
||||
errors.append("npm not found - required for MCP server installation")
|
||||
else:
|
||||
version = result.stdout.strip()
|
||||
self.logger.debug(f"Found npm {version}")
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
errors.append("npm not found - required for MCP server installation")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
def get_files_to_install(self) -> List[Tuple[Path, Path]]:
|
||||
"""Get files to install (none for MCP component)"""
|
||||
return []
|
||||
|
||||
def get_metadata_modifications(self) -> Dict[str, Any]:
|
||||
"""Get metadata modifications for MCP component"""
|
||||
return {
|
||||
"components": {
|
||||
"mcp": {
|
||||
"version": "3.0.0",
|
||||
"installed": True,
|
||||
"servers_count": len(self.mcp_servers)
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"enabled": True,
|
||||
"servers": list(self.mcp_servers.keys()),
|
||||
"auto_update": False
|
||||
}
|
||||
}
|
||||
|
||||
def _check_mcp_server_installed(self, server_name: str) -> bool:
|
||||
"""Check if MCP server is already installed"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["claude", "mcp", "list"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
shell=(sys.platform == "win32")
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
self.logger.warning(f"Could not list MCP servers: {result.stderr}")
|
||||
return False
|
||||
|
||||
# Parse output to check if server is installed
|
||||
output = result.stdout.lower()
|
||||
return server_name.lower() in output
|
||||
|
||||
except (subprocess.TimeoutExpired, subprocess.SubprocessError) as e:
|
||||
self.logger.warning(f"Error checking MCP server status: {e}")
|
||||
return False
|
||||
|
||||
def _install_mcp_server(self, server_info: Dict[str, Any], config: Dict[str, Any]) -> bool:
|
||||
"""Install a single MCP server"""
|
||||
server_name = server_info["name"]
|
||||
npm_package = server_info["npm_package"]
|
||||
|
||||
command = "npx"
|
||||
|
||||
try:
|
||||
self.logger.info(f"Installing MCP server: {server_name}")
|
||||
|
||||
# Check if already installed
|
||||
if self._check_mcp_server_installed(server_name):
|
||||
self.logger.info(f"MCP server {server_name} already installed")
|
||||
return True
|
||||
|
||||
# Handle API key requirements
|
||||
if "api_key_env" in server_info:
|
||||
api_key_env = server_info["api_key_env"]
|
||||
api_key_desc = server_info.get("api_key_description", f"API key for {server_name}")
|
||||
|
||||
if not config.get("dry_run", False):
|
||||
display_info(f"MCP server '{server_name}' requires an API key")
|
||||
display_info(f"Environment variable: {api_key_env}")
|
||||
display_info(f"Description: {api_key_desc}")
|
||||
|
||||
# Check if API key is already set
|
||||
import os
|
||||
if not os.getenv(api_key_env):
|
||||
display_warning(f"API key {api_key_env} not found in environment")
|
||||
self.logger.warning(f"Proceeding without {api_key_env} - server may not function properly")
|
||||
|
||||
# Install using Claude CLI
|
||||
if config.get("dry_run"):
|
||||
self.logger.info(f"Would install MCP server (user scope): claude mcp add -s user {server_name} {command} -y {npm_package}")
|
||||
return True
|
||||
|
||||
self.logger.debug(f"Running: claude mcp add -s user {server_name} {command} -y {npm_package}")
|
||||
|
||||
result = subprocess.run(
|
||||
["claude", "mcp", "add", "-s", "user", "--", server_name, command, "-y", npm_package],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120, # 2 minutes timeout for installation
|
||||
shell=(sys.platform == "win32")
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
self.logger.success(f"Successfully installed MCP server (user scope): {server_name}")
|
||||
return True
|
||||
else:
|
||||
error_msg = result.stderr.strip() if result.stderr else "Unknown error"
|
||||
self.logger.error(f"Failed to install MCP server {server_name}: {error_msg}")
|
||||
return False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
self.logger.error(f"Timeout installing MCP server {server_name}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error installing MCP server {server_name}: {e}")
|
||||
return False
|
||||
|
||||
def _uninstall_mcp_server(self, server_name: str) -> bool:
|
||||
"""Uninstall a single MCP server"""
|
||||
try:
|
||||
self.logger.info(f"Uninstalling MCP server: {server_name}")
|
||||
|
||||
# Check if installed
|
||||
if not self._check_mcp_server_installed(server_name):
|
||||
self.logger.info(f"MCP server {server_name} not installed")
|
||||
return True
|
||||
|
||||
self.logger.debug(f"Running: claude mcp remove {server_name} (auto-detect scope)")
|
||||
|
||||
result = subprocess.run(
|
||||
["claude", "mcp", "remove", server_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
shell=(sys.platform == "win32")
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
self.logger.success(f"Successfully uninstalled MCP server: {server_name}")
|
||||
return True
|
||||
else:
|
||||
error_msg = result.stderr.strip() if result.stderr else "Unknown error"
|
||||
self.logger.error(f"Failed to uninstall MCP server {server_name}: {error_msg}")
|
||||
return False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
self.logger.error(f"Timeout uninstalling MCP server {server_name}")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error uninstalling MCP server {server_name}: {e}")
|
||||
return False
|
||||
|
||||
def _install(self, config: Dict[str, Any]) -> bool:
|
||||
"""Install MCP component"""
|
||||
self.logger.info("Installing SuperClaude MCP servers...")
|
||||
|
||||
# Validate prerequisites
|
||||
success, errors = self.validate_prerequisites()
|
||||
if not success:
|
||||
for error in errors:
|
||||
self.logger.error(error)
|
||||
return False
|
||||
|
||||
# Install each MCP server
|
||||
installed_count = 0
|
||||
failed_servers = []
|
||||
|
||||
for server_name, server_info in self.mcp_servers.items():
|
||||
if self._install_mcp_server(server_info, config):
|
||||
installed_count += 1
|
||||
else:
|
||||
failed_servers.append(server_name)
|
||||
|
||||
# Check if this is a required server
|
||||
if server_info.get("required", False):
|
||||
self.logger.error(f"Required MCP server {server_name} failed to install")
|
||||
return False
|
||||
|
||||
# Verify installation
|
||||
if not config.get("dry_run", False):
|
||||
self.logger.info("Verifying MCP server installation...")
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["claude", "mcp", "list"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
shell=(sys.platform == "win32")
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
self.logger.debug("MCP servers list:")
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line.strip():
|
||||
self.logger.debug(f" {line.strip()}")
|
||||
else:
|
||||
self.logger.warning("Could not verify MCP server installation")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not verify MCP installation: {e}")
|
||||
|
||||
if failed_servers:
|
||||
self.logger.warning(f"Some MCP servers failed to install: {failed_servers}")
|
||||
self.logger.success(f"MCP component partially installed ({installed_count} servers)")
|
||||
else:
|
||||
self.logger.success(f"MCP component installed successfully ({installed_count} servers)")
|
||||
|
||||
return self._post_install()
|
||||
|
||||
def _post_install(self) -> bool:
|
||||
# Update metadata
|
||||
try:
|
||||
metadata_mods = self.get_metadata_modifications()
|
||||
self.settings_manager.update_metadata(metadata_mods)
|
||||
|
||||
# Add component registration to metadata
|
||||
self.settings_manager.add_component_registration("mcp", {
|
||||
"version": "3.0.0",
|
||||
"category": "integration",
|
||||
"servers_count": len(self.mcp_servers)
|
||||
})
|
||||
|
||||
self.logger.info("Updated metadata with MCP component registration")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to update metadata: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def uninstall(self) -> bool:
|
||||
"""Uninstall MCP component"""
|
||||
try:
|
||||
self.logger.info("Uninstalling SuperClaude MCP servers...")
|
||||
|
||||
# Uninstall each MCP server
|
||||
uninstalled_count = 0
|
||||
|
||||
for server_name in self.mcp_servers.keys():
|
||||
if self._uninstall_mcp_server(server_name):
|
||||
uninstalled_count += 1
|
||||
|
||||
# Update metadata to remove MCP component
|
||||
try:
|
||||
if self.settings_manager.is_component_installed("mcp"):
|
||||
self.settings_manager.remove_component_registration("mcp")
|
||||
# Also remove MCP configuration from metadata
|
||||
metadata = self.settings_manager.load_metadata()
|
||||
if "mcp" in metadata:
|
||||
del metadata["mcp"]
|
||||
self.settings_manager.save_metadata(metadata)
|
||||
self.logger.info("Removed MCP component from metadata")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not update metadata: {e}")
|
||||
|
||||
self.logger.success(f"MCP component uninstalled ({uninstalled_count} servers removed)")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.exception(f"Unexpected error during MCP uninstallation: {e}")
|
||||
return False
|
||||
|
||||
def get_dependencies(self) -> List[str]:
|
||||
"""Get dependencies"""
|
||||
return ["core"]
|
||||
|
||||
def update(self, config: Dict[str, Any]) -> bool:
|
||||
"""Update MCP component"""
|
||||
try:
|
||||
self.logger.info("Updating SuperClaude MCP servers...")
|
||||
|
||||
# Check current version
|
||||
current_version = self.settings_manager.get_component_version("mcp")
|
||||
target_version = self.get_metadata()["version"]
|
||||
|
||||
if current_version == target_version:
|
||||
self.logger.info(f"MCP component already at version {target_version}")
|
||||
return True
|
||||
|
||||
self.logger.info(f"Updating MCP component from {current_version} to {target_version}")
|
||||
|
||||
# For MCP servers, update means reinstall to get latest versions
|
||||
updated_count = 0
|
||||
failed_servers = []
|
||||
|
||||
for server_name, server_info in self.mcp_servers.items():
|
||||
try:
|
||||
# Uninstall old version
|
||||
if self._check_mcp_server_installed(server_name):
|
||||
self._uninstall_mcp_server(server_name)
|
||||
|
||||
# Install new version
|
||||
if self._install_mcp_server(server_info, config):
|
||||
updated_count += 1
|
||||
else:
|
||||
failed_servers.append(server_name)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error updating MCP server {server_name}: {e}")
|
||||
failed_servers.append(server_name)
|
||||
|
||||
# Update metadata
|
||||
try:
|
||||
# Update component version in metadata
|
||||
metadata = self.settings_manager.load_metadata()
|
||||
if "components" in metadata and "mcp" in metadata["components"]:
|
||||
metadata["components"]["mcp"]["version"] = target_version
|
||||
metadata["components"]["mcp"]["servers_count"] = len(self.mcp_servers)
|
||||
if "mcp" in metadata:
|
||||
metadata["mcp"]["servers"] = list(self.mcp_servers.keys())
|
||||
self.settings_manager.save_metadata(metadata)
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not update metadata: {e}")
|
||||
|
||||
if failed_servers:
|
||||
self.logger.warning(f"Some MCP servers failed to update: {failed_servers}")
|
||||
return False
|
||||
else:
|
||||
self.logger.success(f"MCP component updated to version {target_version}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.exception(f"Unexpected error during MCP update: {e}")
|
||||
return False
|
||||
|
||||
def validate_installation(self) -> Tuple[bool, List[str]]:
|
||||
"""Validate MCP component installation"""
|
||||
errors = []
|
||||
|
||||
# Check metadata registration
|
||||
if not self.settings_manager.is_component_installed("mcp"):
|
||||
errors.append("MCP component not registered in metadata")
|
||||
return False, errors
|
||||
|
||||
# Check version matches
|
||||
installed_version = self.settings_manager.get_component_version("mcp")
|
||||
expected_version = self.get_metadata()["version"]
|
||||
if installed_version != expected_version:
|
||||
errors.append(f"Version mismatch: installed {installed_version}, expected {expected_version}")
|
||||
|
||||
# Check if Claude CLI is available
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["claude", "mcp", "list"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
shell=(sys.platform == "win32")
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
errors.append("Could not communicate with Claude CLI for MCP server verification")
|
||||
else:
|
||||
# Check if required servers are installed
|
||||
output = result.stdout.lower()
|
||||
for server_name, server_info in self.mcp_servers.items():
|
||||
if server_info.get("required", False):
|
||||
if server_name.lower() not in output:
|
||||
errors.append(f"Required MCP server not found: {server_name}")
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"Could not verify MCP server installation: {e}")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
def _get_source_dir(self):
|
||||
"""Get source directory for framework files"""
|
||||
return None
|
||||
|
||||
def get_size_estimate(self) -> int:
|
||||
"""Get estimated installation size"""
|
||||
# MCP servers are installed via npm, estimate based on typical sizes
|
||||
base_size = 50 * 1024 * 1024 # ~50MB for all servers combined
|
||||
return base_size
|
||||
|
||||
def get_installation_summary(self) -> Dict[str, Any]:
|
||||
"""Get installation summary"""
|
||||
return {
|
||||
"component": self.get_metadata()["name"],
|
||||
"version": self.get_metadata()["version"],
|
||||
"servers_count": len(self.mcp_servers),
|
||||
"mcp_servers": list(self.mcp_servers.keys()),
|
||||
"estimated_size": self.get_size_estimate(),
|
||||
"dependencies": self.get_dependencies(),
|
||||
"required_tools": ["node", "npm", "claude"]
|
||||
}
|
||||
Reference in New Issue
Block a user