mirror of
https://github.com/SuperClaude-Org/SuperClaude_Framework.git
synced 2025-12-29 16:16:08 +00:00
refactor: simplify Component architecture and move shared logic to base class
* Component Base Class: * Add constructor parameter for component subdirectory * Move file discovery utilities to base class to avoid repetition in subclasses * Same for validate_prerequisites, get_files_to_install, get_settings_modifications methods * Split install method into _install and _post_install for better separation of concerns * Add error handling wrapper around _install method * All Component Subclasses: * Remove duplicate code now handled by base class * Use shared file discovery and installation logic * Simplify metadata updates using base class methods * Leverage base class file handling and validation * Hooks Component: * Fix the logic for handling both placeholder and actual hooks scenarios * MCP Component: * Fix npm package names and installation commands
This commit is contained in:
@@ -6,12 +6,16 @@ from abc import ABC, abstractmethod
|
||||
from typing import List, Dict, Tuple, Optional, Any
|
||||
from pathlib import Path
|
||||
import json
|
||||
from ..managers.file_manager import FileManager
|
||||
from ..managers.settings_manager import SettingsManager
|
||||
from ..utils.logger import get_logger
|
||||
from ..utils.security import SecurityValidator
|
||||
|
||||
|
||||
class Component(ABC):
|
||||
"""Base class for all installable components"""
|
||||
|
||||
def __init__(self, install_dir: Optional[Path] = None):
|
||||
def __init__(self, install_dir: Optional[Path] = None, component_subdir: Path = Path('')):
|
||||
"""
|
||||
Initialize component with installation directory
|
||||
|
||||
@@ -20,10 +24,11 @@ class Component(ABC):
|
||||
"""
|
||||
from .. import DEFAULT_INSTALL_DIR
|
||||
self.install_dir = install_dir or DEFAULT_INSTALL_DIR
|
||||
self._metadata = None
|
||||
self._dependencies = None
|
||||
self._files_to_install = None
|
||||
self._settings_modifications = None
|
||||
self.settings_manager = SettingsManager(self.install_dir)
|
||||
self.logger = get_logger()
|
||||
self.component_files = self._discover_component_files()
|
||||
self.file_manager = FileManager()
|
||||
self.install_component_subdir = self.install_dir / component_subdir
|
||||
|
||||
@abstractmethod
|
||||
def get_metadata(self) -> Dict[str, str]:
|
||||
@@ -39,17 +44,58 @@ class Component(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def validate_prerequisites(self) -> Tuple[bool, List[str]]:
|
||||
def validate_prerequisites(self, installSubPath: Optional[Path] = None) -> Tuple[bool, List[str]]:
|
||||
"""
|
||||
Check prerequisites for this component
|
||||
|
||||
Returns:
|
||||
Tuple of (success: bool, error_messages: List[str])
|
||||
"""
|
||||
pass
|
||||
errors = []
|
||||
|
||||
# Check if we have read access to source files
|
||||
source_dir = self._get_source_dir()
|
||||
if not source_dir or (source_dir and not source_dir.exists()):
|
||||
errors.append(f"Source directory not found: {source_dir}")
|
||||
return False, errors
|
||||
|
||||
# Check if all required framework files exist
|
||||
missing_files = []
|
||||
for filename in self.component_files:
|
||||
source_file = source_dir / filename
|
||||
if not source_file.exists():
|
||||
missing_files.append(filename)
|
||||
|
||||
if missing_files:
|
||||
errors.append(f"Missing component files: {missing_files}")
|
||||
|
||||
# Check write permissions to install directory
|
||||
has_perms, missing = SecurityValidator.check_permissions(
|
||||
self.install_dir, {'write'}
|
||||
)
|
||||
if not has_perms:
|
||||
errors.append(f"No write permissions to {self.install_dir}: {missing}")
|
||||
|
||||
# Validate installation target
|
||||
is_safe, validation_errors = SecurityValidator.validate_installation_target(self.install_component_subdir)
|
||||
if not is_safe:
|
||||
errors.extend(validation_errors)
|
||||
|
||||
# Get files to install
|
||||
files_to_install = self.get_files_to_install()
|
||||
|
||||
# Validate all files for security
|
||||
is_safe, security_errors = SecurityValidator.validate_component_files(
|
||||
files_to_install, source_dir, self.install_component_subdir
|
||||
)
|
||||
if not is_safe:
|
||||
errors.extend(security_errors)
|
||||
|
||||
if not self.file_manager.ensure_directory(self.install_component_subdir):
|
||||
errors.append(f"Could not create install directory: {self.install_component_subdir}")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
@abstractmethod
|
||||
def get_files_to_install(self) -> List[Tuple[Path, Path]]:
|
||||
"""
|
||||
Return list of files to install
|
||||
@@ -57,20 +103,37 @@ class Component(ABC):
|
||||
Returns:
|
||||
List of tuples (source_path, target_path)
|
||||
"""
|
||||
pass
|
||||
source_dir = self._get_source_dir()
|
||||
files = []
|
||||
|
||||
if source_dir:
|
||||
for filename in self.component_files:
|
||||
source = source_dir / filename
|
||||
target = self.install_component_subdir / filename
|
||||
files.append((source, target))
|
||||
|
||||
return files
|
||||
|
||||
@abstractmethod
|
||||
def get_settings_modifications(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Return settings.json modifications to apply
|
||||
|
||||
(now only Claude Code compatible settings)
|
||||
|
||||
Returns:
|
||||
Dict of settings to merge into settings.json
|
||||
"""
|
||||
pass
|
||||
# Return empty dict as we don't modify Claude Code settings
|
||||
return {}
|
||||
|
||||
@abstractmethod
|
||||
def install(self, config: Dict[str, Any]) -> bool:
|
||||
try:
|
||||
return self._install(config)
|
||||
except Exception as e:
|
||||
self.logger.exception(f"Unexpected error during {repr(self)} installation: {e}")
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
def _install(self, config: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Perform component-specific installation logic
|
||||
|
||||
@@ -80,8 +143,41 @@ class Component(ABC):
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
pass
|
||||
# Validate installation
|
||||
success, errors = self.validate_prerequisites()
|
||||
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()
|
||||
|
||||
# Copy framework 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)} files copied successfully")
|
||||
return False
|
||||
|
||||
self.logger.success(f"{repr(self)} component installed successfully ({success_count} files)")
|
||||
|
||||
return self._post_install()
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def _post_install(self) -> bool:
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def uninstall(self) -> bool:
|
||||
"""
|
||||
@@ -101,6 +197,11 @@ class Component(ABC):
|
||||
List of component names this component depends on
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _get_source_dir(self) -> Optional[Path]:
|
||||
"""Get source directory for component files"""
|
||||
pass
|
||||
|
||||
def update(self, config: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
@@ -124,8 +225,10 @@ class Component(ABC):
|
||||
Returns:
|
||||
Version string if installed, None otherwise
|
||||
"""
|
||||
print("GETTING INSTALLED VERSION")
|
||||
settings_file = self.install_dir / "settings.json"
|
||||
if settings_file.exists():
|
||||
print("SETTINGS.JSON EXISTS")
|
||||
try:
|
||||
with open(settings_file, 'r') as f:
|
||||
settings = json.load(f)
|
||||
@@ -133,6 +236,7 @@ class Component(ABC):
|
||||
return settings.get('components', {}).get(component_name, {}).get('version')
|
||||
except Exception:
|
||||
pass
|
||||
print("SETTINGS.JSON DOESNT EXIST RETURNING NONE")
|
||||
return None
|
||||
|
||||
def is_installed(self) -> bool:
|
||||
@@ -179,6 +283,73 @@ class Component(ABC):
|
||||
elif source.is_dir():
|
||||
total_size += sum(f.stat().st_size for f in source.rglob('*') if f.is_file())
|
||||
return total_size
|
||||
|
||||
def _discover_component_files(self) -> List[str]:
|
||||
"""
|
||||
Dynamically discover framework .md files in the Core directory
|
||||
|
||||
Returns:
|
||||
List of framework filenames (e.g., ['CLAUDE.md', 'COMMANDS.md', ...])
|
||||
"""
|
||||
source_dir = self._get_source_dir()
|
||||
|
||||
if not source_dir:
|
||||
return []
|
||||
|
||||
return self._discover_files_in_directory(
|
||||
source_dir,
|
||||
extension='.md',
|
||||
exclude_patterns=['README.md', 'CHANGELOG.md', 'LICENSE.md']
|
||||
)
|
||||
|
||||
def _discover_files_in_directory(self, directory: Path, extension: str = '.md',
|
||||
exclude_patterns: Optional[List[str]] = None) -> List[str]:
|
||||
"""
|
||||
Shared utility for discovering files in a directory
|
||||
|
||||
Args:
|
||||
directory: Directory to scan
|
||||
extension: File extension to look for (default: '.md')
|
||||
exclude_patterns: List of filename patterns to exclude
|
||||
|
||||
Returns:
|
||||
List of filenames found in the directory
|
||||
"""
|
||||
if exclude_patterns is None:
|
||||
exclude_patterns = []
|
||||
|
||||
try:
|
||||
if not directory.exists():
|
||||
self.logger.warning(f"Source directory not found: {directory}")
|
||||
return []
|
||||
|
||||
if not directory.is_dir():
|
||||
self.logger.warning(f"Source path is not a directory: {directory}")
|
||||
return []
|
||||
|
||||
# Discover files with the specified extension
|
||||
files = []
|
||||
for file_path in directory.iterdir():
|
||||
if (file_path.is_file() and
|
||||
file_path.suffix.lower() == extension.lower() and
|
||||
file_path.name not in exclude_patterns):
|
||||
files.append(file_path.name)
|
||||
|
||||
# Sort for consistent ordering
|
||||
files.sort()
|
||||
|
||||
self.logger.debug(f"Discovered {len(files)} {extension} files in {directory}")
|
||||
if files:
|
||||
self.logger.debug(f"Files found: {files}")
|
||||
|
||||
return files
|
||||
|
||||
except PermissionError:
|
||||
self.logger.error(f"Permission denied accessing directory: {directory}")
|
||||
return []
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error discovering files in {directory}: {e}")
|
||||
return []
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of component"""
|
||||
@@ -187,4 +358,4 @@ class Component(ABC):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Developer representation of component"""
|
||||
return f"<{self.__class__.__name__}({self.get_metadata()['name']})>"
|
||||
return f"<{self.__class__.__name__}({self.get_metadata()['name']})>"
|
||||
|
||||
@@ -2,28 +2,17 @@
|
||||
Commands component for SuperClaude slash command definitions
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Tuple, Any
|
||||
from typing import Dict, List, Tuple, Optional, Any
|
||||
from pathlib import Path
|
||||
|
||||
from ..base.component import Component
|
||||
from ..core.file_manager import FileManager
|
||||
from ..core.settings_manager import SettingsManager
|
||||
from ..utils.security import SecurityValidator
|
||||
from ..utils.logger import get_logger
|
||||
|
||||
|
||||
class CommandsComponent(Component):
|
||||
"""SuperClaude slash commands component"""
|
||||
|
||||
def __init__(self, install_dir: Path = None):
|
||||
def __init__(self, install_dir: Optional[Path] = None):
|
||||
"""Initialize commands component"""
|
||||
super().__init__(install_dir)
|
||||
self.logger = get_logger()
|
||||
self.file_manager = FileManager()
|
||||
self.settings_manager = SettingsManager(self.install_dir)
|
||||
|
||||
# Dynamically discover command files to install
|
||||
self.command_files = self._discover_command_files()
|
||||
super().__init__(install_dir, Path("commands/sc"))
|
||||
|
||||
def get_metadata(self) -> Dict[str, str]:
|
||||
"""Get component metadata"""
|
||||
@@ -34,53 +23,6 @@ class CommandsComponent(Component):
|
||||
"category": "commands"
|
||||
}
|
||||
|
||||
def validate_prerequisites(self) -> Tuple[bool, List[str]]:
|
||||
"""Check prerequisites"""
|
||||
errors = []
|
||||
|
||||
# Check if we have read access to source files
|
||||
source_dir = self._get_source_dir()
|
||||
if not source_dir.exists():
|
||||
errors.append(f"Source directory not found: {source_dir}")
|
||||
return False, errors
|
||||
|
||||
# Check if all required command files exist
|
||||
missing_files = []
|
||||
for filename in self.command_files:
|
||||
source_file = source_dir / filename
|
||||
if not source_file.exists():
|
||||
missing_files.append(filename)
|
||||
|
||||
if missing_files:
|
||||
errors.append(f"Missing command files: {missing_files}")
|
||||
|
||||
# Check write permissions to install directory
|
||||
commands_dir = self.install_dir / "commands" / "sc"
|
||||
has_perms, missing = SecurityValidator.check_permissions(
|
||||
self.install_dir, {'write'}
|
||||
)
|
||||
if not has_perms:
|
||||
errors.append(f"No write permissions to {self.install_dir}: {missing}")
|
||||
|
||||
# Validate installation target
|
||||
is_safe, validation_errors = SecurityValidator.validate_installation_target(commands_dir)
|
||||
if not is_safe:
|
||||
errors.extend(validation_errors)
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
def get_files_to_install(self) -> List[Tuple[Path, Path]]:
|
||||
"""Get files to install"""
|
||||
source_dir = self._get_source_dir()
|
||||
files = []
|
||||
|
||||
for filename in self.command_files:
|
||||
source = source_dir / filename
|
||||
target = self.install_dir / "commands" / "sc" / filename
|
||||
files.append((source, target))
|
||||
|
||||
return files
|
||||
|
||||
def get_metadata_modifications(self) -> Dict[str, Any]:
|
||||
"""Get metadata modifications for commands component"""
|
||||
return {
|
||||
@@ -88,84 +30,44 @@ class CommandsComponent(Component):
|
||||
"commands": {
|
||||
"version": "3.0.0",
|
||||
"installed": True,
|
||||
"files_count": len(self.command_files)
|
||||
"files_count": len(self.component_files)
|
||||
}
|
||||
},
|
||||
"commands": {
|
||||
"enabled": True,
|
||||
"version": "3.0.0",
|
||||
"auto_update": False
|
||||
}
|
||||
}
|
||||
|
||||
def get_settings_modifications(self) -> Dict[str, Any]:
|
||||
"""Get settings.json modifications (now only Claude Code compatible settings)"""
|
||||
# Return empty dict as we don't modify Claude Code settings
|
||||
return {}
|
||||
|
||||
def install(self, config: Dict[str, Any]) -> bool:
|
||||
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:
|
||||
self.logger.info("Installing SuperClaude command definitions...")
|
||||
|
||||
# Check for and migrate existing commands from old location
|
||||
self._migrate_existing_commands()
|
||||
|
||||
# Validate installation
|
||||
success, errors = self.validate_prerequisites()
|
||||
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()
|
||||
|
||||
# Validate all files for security
|
||||
source_dir = self._get_source_dir()
|
||||
commands_dir = self.install_dir / "commands" / "sc"
|
||||
is_safe, security_errors = SecurityValidator.validate_component_files(
|
||||
files_to_install, source_dir, commands_dir
|
||||
)
|
||||
if not is_safe:
|
||||
for error in security_errors:
|
||||
self.logger.error(f"Security validation failed: {error}")
|
||||
return False
|
||||
|
||||
# Ensure commands directory exists
|
||||
if not self.file_manager.ensure_directory(commands_dir):
|
||||
self.logger.error(f"Could not create commands directory: {commands_dir}")
|
||||
return False
|
||||
|
||||
# Copy command 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)} command files copied successfully")
|
||||
return False
|
||||
|
||||
# Update metadata
|
||||
try:
|
||||
# Add component registration to metadata
|
||||
self.settings_manager.add_component_registration("commands", {
|
||||
"version": "3.0.0",
|
||||
"category": "commands",
|
||||
"files_count": len(self.command_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
|
||||
|
||||
self.logger.success(f"Commands component installed successfully ({success_count} command files)")
|
||||
return True
|
||||
|
||||
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.exception(f"Unexpected error during commands installation: {e}")
|
||||
self.logger.error(f"Failed to update metadata: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def uninstall(self) -> bool:
|
||||
"""Uninstall commands component"""
|
||||
@@ -176,7 +78,7 @@ class CommandsComponent(Component):
|
||||
commands_dir = self.install_dir / "commands" / "sc"
|
||||
removed_count = 0
|
||||
|
||||
for filename in self.command_files:
|
||||
for filename in self.component_files:
|
||||
file_path = commands_dir / filename
|
||||
if self.file_manager.remove_file(file_path):
|
||||
removed_count += 1
|
||||
@@ -188,7 +90,7 @@ class CommandsComponent(Component):
|
||||
old_commands_dir = self.install_dir / "commands"
|
||||
old_removed_count = 0
|
||||
|
||||
for filename in self.command_files:
|
||||
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):
|
||||
@@ -224,6 +126,11 @@ class CommandsComponent(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}")
|
||||
@@ -259,7 +166,7 @@ class CommandsComponent(Component):
|
||||
backup_files = []
|
||||
|
||||
if commands_dir.exists():
|
||||
for filename in self.command_files:
|
||||
for filename in self.component_files:
|
||||
file_path = commands_dir / filename
|
||||
if file_path.exists():
|
||||
backup_path = self.file_manager.backup_file(file_path)
|
||||
@@ -307,7 +214,7 @@ class CommandsComponent(Component):
|
||||
return False, errors
|
||||
|
||||
# Check if all command files exist
|
||||
for filename in self.command_files:
|
||||
for filename in self.component_files:
|
||||
file_path = commands_dir / filename
|
||||
if not file_path.exists():
|
||||
errors.append(f"Missing command file: {filename}")
|
||||
@@ -326,68 +233,6 @@ class CommandsComponent(Component):
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
def _discover_command_files(self) -> List[str]:
|
||||
"""
|
||||
Dynamically discover command .md files in the Commands directory
|
||||
|
||||
Returns:
|
||||
List of command filenames (e.g., ['analyze.md', 'build.md', ...])
|
||||
"""
|
||||
return self._discover_files_in_directory(
|
||||
self._get_source_dir(),
|
||||
extension='.md',
|
||||
exclude_patterns=['README.md', 'CHANGELOG.md', 'LICENSE.md']
|
||||
)
|
||||
|
||||
def _discover_files_in_directory(self, directory: Path, extension: str = '.md',
|
||||
exclude_patterns: List[str] = None) -> List[str]:
|
||||
"""
|
||||
Shared utility for discovering files in a directory
|
||||
|
||||
Args:
|
||||
directory: Directory to scan
|
||||
extension: File extension to look for (default: '.md')
|
||||
exclude_patterns: List of filename patterns to exclude
|
||||
|
||||
Returns:
|
||||
List of filenames found in the directory
|
||||
"""
|
||||
if exclude_patterns is None:
|
||||
exclude_patterns = []
|
||||
|
||||
try:
|
||||
if not directory.exists():
|
||||
self.logger.warning(f"Source directory not found: {directory}")
|
||||
return []
|
||||
|
||||
if not directory.is_dir():
|
||||
self.logger.warning(f"Source path is not a directory: {directory}")
|
||||
return []
|
||||
|
||||
# Discover files with the specified extension
|
||||
files = []
|
||||
for file_path in directory.iterdir():
|
||||
if (file_path.is_file() and
|
||||
file_path.suffix.lower() == extension.lower() and
|
||||
file_path.name not in exclude_patterns):
|
||||
files.append(file_path.name)
|
||||
|
||||
# Sort for consistent ordering
|
||||
files.sort()
|
||||
|
||||
self.logger.debug(f"Discovered {len(files)} {extension} files in {directory}")
|
||||
if files:
|
||||
self.logger.debug(f"Files found: {files}")
|
||||
|
||||
return files
|
||||
|
||||
except PermissionError:
|
||||
self.logger.error(f"Permission denied accessing directory: {directory}")
|
||||
return []
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error discovering files in {directory}: {e}")
|
||||
return []
|
||||
|
||||
def _get_source_dir(self) -> Path:
|
||||
"""Get source directory for command files"""
|
||||
# Assume we're in SuperClaude/setup/components/commands.py
|
||||
@@ -400,7 +245,7 @@ class CommandsComponent(Component):
|
||||
total_size = 0
|
||||
source_dir = self._get_source_dir()
|
||||
|
||||
for filename in self.command_files:
|
||||
for filename in self.component_files:
|
||||
file_path = source_dir / filename
|
||||
if file_path.exists():
|
||||
total_size += file_path.stat().st_size
|
||||
@@ -415,8 +260,8 @@ class CommandsComponent(Component):
|
||||
return {
|
||||
"component": self.get_metadata()["name"],
|
||||
"version": self.get_metadata()["version"],
|
||||
"files_installed": len(self.command_files),
|
||||
"command_files": self.command_files,
|
||||
"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()
|
||||
@@ -433,7 +278,7 @@ class CommandsComponent(Component):
|
||||
commands_to_migrate = []
|
||||
|
||||
if old_commands_dir.exists():
|
||||
for filename in self.command_files:
|
||||
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)
|
||||
|
||||
@@ -2,30 +2,18 @@
|
||||
Core component for SuperClaude framework files installation
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Tuple, Any
|
||||
from typing import Dict, List, Tuple, Optional, Any
|
||||
from pathlib import Path
|
||||
import json
|
||||
import shutil
|
||||
|
||||
from ..base.component import Component
|
||||
from ..core.file_manager import FileManager
|
||||
from ..core.settings_manager import SettingsManager
|
||||
from ..utils.security import SecurityValidator
|
||||
from ..utils.logger import get_logger
|
||||
|
||||
|
||||
class CoreComponent(Component):
|
||||
"""Core SuperClaude framework files component"""
|
||||
|
||||
def __init__(self, install_dir: Path = None):
|
||||
def __init__(self, install_dir: Optional[Path] = None):
|
||||
"""Initialize core component"""
|
||||
super().__init__(install_dir)
|
||||
self.logger = get_logger()
|
||||
self.file_manager = FileManager()
|
||||
self.settings_manager = SettingsManager(self.install_dir)
|
||||
|
||||
# Dynamically discover framework files to install
|
||||
self.framework_files = self._discover_framework_files()
|
||||
|
||||
def get_metadata(self) -> Dict[str, str]:
|
||||
"""Get component metadata"""
|
||||
@@ -36,52 +24,6 @@ class CoreComponent(Component):
|
||||
"category": "core"
|
||||
}
|
||||
|
||||
def validate_prerequisites(self) -> Tuple[bool, List[str]]:
|
||||
"""Check prerequisites for core component"""
|
||||
errors = []
|
||||
|
||||
# Check if we have read access to source files
|
||||
source_dir = self._get_source_dir()
|
||||
if not source_dir.exists():
|
||||
errors.append(f"Source directory not found: {source_dir}")
|
||||
return False, errors
|
||||
|
||||
# Check if all required framework files exist
|
||||
missing_files = []
|
||||
for filename in self.framework_files:
|
||||
source_file = source_dir / filename
|
||||
if not source_file.exists():
|
||||
missing_files.append(filename)
|
||||
|
||||
if missing_files:
|
||||
errors.append(f"Missing framework files: {missing_files}")
|
||||
|
||||
# Check write permissions to install directory
|
||||
has_perms, missing = SecurityValidator.check_permissions(
|
||||
self.install_dir, {'write'}
|
||||
)
|
||||
if not has_perms:
|
||||
errors.append(f"No write permissions to {self.install_dir}: {missing}")
|
||||
|
||||
# Validate installation target
|
||||
is_safe, validation_errors = SecurityValidator.validate_installation_target(self.install_dir)
|
||||
if not is_safe:
|
||||
errors.extend(validation_errors)
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
def get_files_to_install(self) -> List[Tuple[Path, Path]]:
|
||||
"""Get list of files to install"""
|
||||
source_dir = self._get_source_dir()
|
||||
files = []
|
||||
|
||||
for filename in self.framework_files:
|
||||
source = source_dir / filename
|
||||
target = self.install_dir / filename
|
||||
files.append((source, target))
|
||||
|
||||
return files
|
||||
|
||||
def get_metadata_modifications(self) -> Dict[str, Any]:
|
||||
"""Get metadata modifications for SuperClaude"""
|
||||
return {
|
||||
@@ -100,93 +42,44 @@ class CoreComponent(Component):
|
||||
}
|
||||
}
|
||||
|
||||
def get_settings_modifications(self) -> Dict[str, Any]:
|
||||
"""Get settings.json modifications (now only Claude Code compatible settings)"""
|
||||
# Return empty dict as we don't modify Claude Code settings
|
||||
return {}
|
||||
|
||||
def install(self, config: Dict[str, Any]) -> bool:
|
||||
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:
|
||||
self.logger.info("Installing SuperClaude core framework files...")
|
||||
metadata_mods = self.get_metadata_modifications()
|
||||
self.settings_manager.update_metadata(metadata_mods)
|
||||
self.logger.info("Updated metadata with framework configuration")
|
||||
|
||||
# Validate installation
|
||||
success, errors = self.validate_prerequisites()
|
||||
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()
|
||||
|
||||
# Validate all files for security
|
||||
source_dir = self._get_source_dir()
|
||||
is_safe, security_errors = SecurityValidator.validate_component_files(
|
||||
files_to_install, source_dir, self.install_dir
|
||||
)
|
||||
if not is_safe:
|
||||
for error in security_errors:
|
||||
self.logger.error(f"Security validation failed: {error}")
|
||||
return False
|
||||
|
||||
# Ensure install directory exists
|
||||
if not self.file_manager.ensure_directory(self.install_dir):
|
||||
self.logger.error(f"Could not create install directory: {self.install_dir}")
|
||||
return False
|
||||
|
||||
# Copy framework 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)} files copied successfully")
|
||||
return False
|
||||
|
||||
# Create or update metadata
|
||||
try:
|
||||
metadata_mods = self.get_metadata_modifications()
|
||||
# Update metadata directly
|
||||
existing_metadata = self.settings_manager.load_metadata()
|
||||
merged_metadata = self.settings_manager._deep_merge(existing_metadata, metadata_mods)
|
||||
self.settings_manager.save_metadata(merged_metadata)
|
||||
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.framework_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}")
|
||||
|
||||
self.logger.success(f"Core component installed successfully ({success_count} files)")
|
||||
return True
|
||||
# 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.exception(f"Unexpected error during core installation: {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"""
|
||||
@@ -195,7 +88,7 @@ class CoreComponent(Component):
|
||||
|
||||
# Remove framework files
|
||||
removed_count = 0
|
||||
for filename in self.framework_files:
|
||||
for filename in self.component_files:
|
||||
file_path = self.install_dir / filename
|
||||
if self.file_manager.remove_file(file_path):
|
||||
removed_count += 1
|
||||
@@ -207,6 +100,13 @@ class CoreComponent(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}")
|
||||
@@ -239,7 +139,7 @@ class CoreComponent(Component):
|
||||
|
||||
# Create backup of existing files
|
||||
backup_files = []
|
||||
for filename in self.framework_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)
|
||||
@@ -281,7 +181,7 @@ class CoreComponent(Component):
|
||||
errors = []
|
||||
|
||||
# Check if all framework files exist
|
||||
for filename in self.framework_files:
|
||||
for filename in self.component_files:
|
||||
file_path = self.install_dir / filename
|
||||
if not file_path.exists():
|
||||
errors.append(f"Missing framework file: {filename}")
|
||||
@@ -313,69 +213,7 @@ class CoreComponent(Component):
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
def _discover_framework_files(self) -> List[str]:
|
||||
"""
|
||||
Dynamically discover framework .md files in the Core directory
|
||||
|
||||
Returns:
|
||||
List of framework filenames (e.g., ['CLAUDE.md', 'COMMANDS.md', ...])
|
||||
"""
|
||||
return self._discover_files_in_directory(
|
||||
self._get_source_dir(),
|
||||
extension='.md',
|
||||
exclude_patterns=['README.md', 'CHANGELOG.md', 'LICENSE.md']
|
||||
)
|
||||
|
||||
def _discover_files_in_directory(self, directory: Path, extension: str = '.md',
|
||||
exclude_patterns: List[str] = None) -> List[str]:
|
||||
"""
|
||||
Shared utility for discovering files in a directory
|
||||
|
||||
Args:
|
||||
directory: Directory to scan
|
||||
extension: File extension to look for (default: '.md')
|
||||
exclude_patterns: List of filename patterns to exclude
|
||||
|
||||
Returns:
|
||||
List of filenames found in the directory
|
||||
"""
|
||||
if exclude_patterns is None:
|
||||
exclude_patterns = []
|
||||
|
||||
try:
|
||||
if not directory.exists():
|
||||
self.logger.warning(f"Source directory not found: {directory}")
|
||||
return []
|
||||
|
||||
if not directory.is_dir():
|
||||
self.logger.warning(f"Source path is not a directory: {directory}")
|
||||
return []
|
||||
|
||||
# Discover files with the specified extension
|
||||
files = []
|
||||
for file_path in directory.iterdir():
|
||||
if (file_path.is_file() and
|
||||
file_path.suffix.lower() == extension.lower() and
|
||||
file_path.name not in exclude_patterns):
|
||||
files.append(file_path.name)
|
||||
|
||||
# Sort for consistent ordering
|
||||
files.sort()
|
||||
|
||||
self.logger.debug(f"Discovered {len(files)} {extension} files in {directory}")
|
||||
if files:
|
||||
self.logger.debug(f"Files found: {files}")
|
||||
|
||||
return files
|
||||
|
||||
except PermissionError:
|
||||
self.logger.error(f"Permission denied accessing directory: {directory}")
|
||||
return []
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error discovering files in {directory}: {e}")
|
||||
return []
|
||||
|
||||
def _get_source_dir(self) -> Path:
|
||||
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/
|
||||
@@ -387,7 +225,7 @@ class CoreComponent(Component):
|
||||
total_size = 0
|
||||
source_dir = self._get_source_dir()
|
||||
|
||||
for filename in self.framework_files:
|
||||
for filename in self.component_files:
|
||||
file_path = source_dir / filename
|
||||
if file_path.exists():
|
||||
total_size += file_path.stat().st_size
|
||||
@@ -402,9 +240,9 @@ class CoreComponent(Component):
|
||||
return {
|
||||
"component": self.get_metadata()["name"],
|
||||
"version": self.get_metadata()["version"],
|
||||
"files_installed": len(self.framework_files),
|
||||
"framework_files": self.framework_files,
|
||||
"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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,25 +2,18 @@
|
||||
Hooks component for Claude Code hooks integration (future-ready)
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Tuple, Any
|
||||
from typing import Dict, List, Tuple, Optional, Any
|
||||
from pathlib import Path
|
||||
|
||||
from ..base.component import Component
|
||||
from ..core.file_manager import FileManager
|
||||
from ..core.settings_manager import SettingsManager
|
||||
from ..utils.security import SecurityValidator
|
||||
from ..utils.logger import get_logger
|
||||
|
||||
|
||||
class HooksComponent(Component):
|
||||
"""Claude Code hooks integration component"""
|
||||
|
||||
def __init__(self, install_dir: Path = None):
|
||||
def __init__(self, install_dir: Optional[Path] = None):
|
||||
"""Initialize hooks component"""
|
||||
super().__init__(install_dir)
|
||||
self.logger = get_logger()
|
||||
self.file_manager = FileManager()
|
||||
self.settings_manager = SettingsManager(self.install_dir)
|
||||
super().__init__(install_dir, Path("hooks"))
|
||||
|
||||
# Define hook files to install (when hooks are ready)
|
||||
self.hook_files = [
|
||||
@@ -39,59 +32,16 @@ class HooksComponent(Component):
|
||||
"description": "Claude Code hooks integration (future-ready)",
|
||||
"category": "integration"
|
||||
}
|
||||
|
||||
def validate_prerequisites(self) -> Tuple[bool, List[str]]:
|
||||
"""Check prerequisites"""
|
||||
errors = []
|
||||
|
||||
# Check if source directory exists (when hooks are implemented)
|
||||
source_dir = self._get_source_dir()
|
||||
if not source_dir.exists():
|
||||
# This is expected for now - hooks are future-ready
|
||||
self.logger.debug(f"Hooks source directory not found: {source_dir} (expected for future implementation)")
|
||||
|
||||
# Check write permissions to install directory
|
||||
hooks_dir = self.install_dir / "hooks"
|
||||
has_perms, missing = SecurityValidator.check_permissions(
|
||||
self.install_dir, {'write'}
|
||||
)
|
||||
if not has_perms:
|
||||
errors.append(f"No write permissions to {self.install_dir}: {missing}")
|
||||
|
||||
# Validate installation target
|
||||
is_safe, validation_errors = SecurityValidator.validate_installation_target(hooks_dir)
|
||||
if not is_safe:
|
||||
errors.extend(validation_errors)
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
def get_files_to_install(self) -> List[Tuple[Path, Path]]:
|
||||
"""Get files to install"""
|
||||
source_dir = self._get_source_dir()
|
||||
files = []
|
||||
|
||||
# Only include files that actually exist
|
||||
for filename in self.hook_files:
|
||||
source = source_dir / filename
|
||||
if source.exists():
|
||||
target = self.install_dir / "hooks" / filename
|
||||
files.append((source, target))
|
||||
|
||||
return files
|
||||
|
||||
def get_settings_modifications(self) -> Dict[str, Any]:
|
||||
"""Get settings modifications"""
|
||||
hooks_dir = self.install_dir / "hooks"
|
||||
|
||||
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 = hooks_dir / filename
|
||||
hook_path = self.install_component_subdir / filename
|
||||
if hook_path.exists():
|
||||
hook_name = filename.replace('.py', '')
|
||||
hook_config[hook_name] = [str(hook_path)]
|
||||
|
||||
settings_mods = {
|
||||
metadata_mods = {
|
||||
"components": {
|
||||
"hooks": {
|
||||
"version": "3.0.0",
|
||||
@@ -103,31 +53,31 @@ class HooksComponent(Component):
|
||||
|
||||
# Only add hooks configuration if we have actual hook files
|
||||
if hook_config:
|
||||
settings_mods["hooks"] = {
|
||||
metadata_mods["hooks"] = {
|
||||
"enabled": True,
|
||||
**hook_config
|
||||
}
|
||||
|
||||
|
||||
return settings_mods
|
||||
|
||||
def install(self, config: Dict[str, Any]) -> bool:
|
||||
return metadata_mods
|
||||
|
||||
def _install(self, config: Dict[str, Any]) -> bool:
|
||||
"""Install hooks component"""
|
||||
try:
|
||||
self.logger.info("Installing SuperClaude 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")
|
||||
|
||||
# This component is future-ready - hooks aren't implemented yet
|
||||
source_dir = self._get_source_dir()
|
||||
if not source_dir.exists():
|
||||
self.logger.info("Hooks are not yet implemented - installing placeholder component")
|
||||
|
||||
# Create placeholder hooks directory
|
||||
hooks_dir = self.install_dir / "hooks"
|
||||
if not self.file_manager.ensure_directory(hooks_dir):
|
||||
self.logger.error(f"Could not create hooks directory: {hooks_dir}")
|
||||
return False
|
||||
|
||||
# Create placeholder file
|
||||
placeholder_content = '''"""
|
||||
# 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.
|
||||
@@ -145,101 +95,95 @@ For more information, see SuperClaude documentation.
|
||||
|
||||
# Placeholder for future hooks implementation
|
||||
def placeholder_hook():
|
||||
"""Placeholder hook function"""
|
||||
pass
|
||||
"""Placeholder hook function"""
|
||||
pass
|
||||
'''
|
||||
|
||||
placeholder_path = hooks_dir / "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:
|
||||
settings_mods = {
|
||||
"components": {
|
||||
"hooks": {
|
||||
"version": "3.0.0",
|
||||
"installed": True,
|
||||
"status": "placeholder",
|
||||
"files_count": 0
|
||||
}
|
||||
|
||||
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_settings(settings_mods)
|
||||
self.logger.info("Updated settings.json with hooks component registration")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to update settings.json: {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()
|
||||
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
|
||||
|
||||
# Validate all files for security
|
||||
hooks_dir = self.install_dir / "hooks"
|
||||
is_safe, security_errors = SecurityValidator.validate_component_files(
|
||||
files_to_install, source_dir, hooks_dir
|
||||
)
|
||||
if not is_safe:
|
||||
for error in security_errors:
|
||||
self.logger.error(f"Security validation failed: {error}")
|
||||
return False
|
||||
|
||||
# Ensure hooks directory exists
|
||||
if not self.file_manager.ensure_directory(hooks_dir):
|
||||
self.logger.error(f"Could not create hooks directory: {hooks_dir}")
|
||||
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
|
||||
|
||||
# Update settings.json
|
||||
try:
|
||||
settings_mods = self.get_settings_modifications()
|
||||
self.settings_manager.update_settings(settings_mods)
|
||||
self.logger.info("Updated settings.json with hooks configuration")
|
||||
}
|
||||
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 settings.json: {e}")
|
||||
self.logger.error(f"Failed to update metadata for hooks component: {e}")
|
||||
return False
|
||||
|
||||
self.logger.success(f"Hooks component installed successfully ({success_count} hook files)")
|
||||
self.logger.success("Hooks component installed successfully (placeholder)")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.exception(f"Unexpected error during hooks installation: {e}")
|
||||
|
||||
# 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"""
|
||||
@@ -247,28 +191,27 @@ def placeholder_hook():
|
||||
self.logger.info("Uninstalling SuperClaude hooks component...")
|
||||
|
||||
# Remove hook files and placeholder
|
||||
hooks_dir = self.install_dir / "hooks"
|
||||
removed_count = 0
|
||||
|
||||
# Remove actual hook files
|
||||
for filename in self.hook_files:
|
||||
file_path = hooks_dir / filename
|
||||
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 = hooks_dir / "PLACEHOLDER.py"
|
||||
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 hooks_dir.exists():
|
||||
remaining_files = list(hooks_dir.iterdir())
|
||||
if self.install_component_subdir.exists():
|
||||
remaining_files = list(self.install_component_subdir.iterdir())
|
||||
if not remaining_files:
|
||||
hooks_dir.rmdir()
|
||||
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}")
|
||||
@@ -315,12 +258,11 @@ def placeholder_hook():
|
||||
self.logger.info(f"Updating hooks component from {current_version} to {target_version}")
|
||||
|
||||
# Create backup of existing hook files
|
||||
hooks_dir = self.install_dir / "hooks"
|
||||
backup_files = []
|
||||
|
||||
if hooks_dir.exists():
|
||||
if self.install_component_subdir.exists():
|
||||
for filename in self.hook_files + ["PLACEHOLDER.py"]:
|
||||
file_path = hooks_dir / filename
|
||||
file_path = self.install_component_subdir / filename
|
||||
if file_path.exists():
|
||||
backup_path = self.file_manager.backup_file(file_path)
|
||||
if backup_path:
|
||||
@@ -361,8 +303,7 @@ def placeholder_hook():
|
||||
errors = []
|
||||
|
||||
# Check if hooks directory exists
|
||||
hooks_dir = self.install_dir / "hooks"
|
||||
if not hooks_dir.exists():
|
||||
if not self.install_component_subdir.exists():
|
||||
errors.append("Hooks directory not found")
|
||||
return False, errors
|
||||
|
||||
@@ -377,8 +318,8 @@ def placeholder_hook():
|
||||
errors.append(f"Version mismatch: installed {installed_version}, expected {expected_version}")
|
||||
|
||||
# Check if we have either actual hooks or placeholder
|
||||
has_placeholder = (hooks_dir / "PLACEHOLDER.py").exists()
|
||||
has_actual_hooks = any((hooks_dir / filename).exists() for filename in self.hook_files)
|
||||
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")
|
||||
@@ -422,4 +363,4 @@ def placeholder_hook():
|
||||
"estimated_size": self.get_size_estimate(),
|
||||
"install_directory": str(self.install_dir / "hooks"),
|
||||
"dependencies": self.get_dependencies()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,24 +4,19 @@ MCP component for MCP server integration
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import json
|
||||
from typing import Dict, List, Tuple, Any
|
||||
from typing import Dict, List, Tuple, Optional, Any
|
||||
from pathlib import Path
|
||||
|
||||
from ..base.component import Component
|
||||
from ..core.settings_manager import SettingsManager
|
||||
from ..utils.logger import get_logger
|
||||
from ..utils.ui import confirm, display_info, display_warning
|
||||
from ..utils.ui import display_info, display_warning
|
||||
|
||||
|
||||
class MCPComponent(Component):
|
||||
"""MCP servers integration component"""
|
||||
|
||||
def __init__(self, install_dir: Path = None):
|
||||
def __init__(self, install_dir: Optional[Path] = None):
|
||||
"""Initialize MCP component"""
|
||||
super().__init__(install_dir)
|
||||
self.logger = get_logger()
|
||||
self.settings_manager = SettingsManager(self.install_dir)
|
||||
|
||||
# Define MCP servers to install
|
||||
self.mcp_servers = {
|
||||
@@ -29,21 +24,18 @@ class MCPComponent(Component):
|
||||
"name": "sequential-thinking",
|
||||
"description": "Multi-step problem solving and systematic analysis",
|
||||
"npm_package": "@modelcontextprotocol/server-sequential-thinking",
|
||||
"command": "npx @modelcontextprotocol/server-sequential-thinking",
|
||||
"required": True
|
||||
},
|
||||
"context7": {
|
||||
"name": "context7",
|
||||
"description": "Official library documentation and code examples",
|
||||
"npm_package": "@context7/mcp",
|
||||
"command": "npx @context7/mcp",
|
||||
"npm_package": "@upstash/context7-mcp",
|
||||
"required": True
|
||||
},
|
||||
"magic": {
|
||||
"name": "magic",
|
||||
"description": "Modern UI component generation and design systems",
|
||||
"npm_package": "@21st/mcp",
|
||||
"command": "npx @21st/mcp",
|
||||
"npm_package": "@21st-dev/magic",
|
||||
"required": False,
|
||||
"api_key_env": "TWENTYFIRST_API_KEY",
|
||||
"api_key_description": "21st.dev API key for UI component generation"
|
||||
@@ -51,8 +43,7 @@ class MCPComponent(Component):
|
||||
"playwright": {
|
||||
"name": "playwright",
|
||||
"description": "Cross-browser E2E testing and automation",
|
||||
"npm_package": "@modelcontextprotocol/server-playwright",
|
||||
"command": "npx @modelcontextprotocol/server-playwright",
|
||||
"npm_package": "@playright/mcp@latest",
|
||||
"required": False
|
||||
}
|
||||
}
|
||||
@@ -66,7 +57,7 @@ class MCPComponent(Component):
|
||||
"category": "integration"
|
||||
}
|
||||
|
||||
def validate_prerequisites(self) -> Tuple[bool, List[str]]:
|
||||
def validate_prerequisites(self, installSubPath: Optional[Path] = None) -> Tuple[bool, List[str]]:
|
||||
"""Check prerequisites"""
|
||||
errors = []
|
||||
|
||||
@@ -152,11 +143,6 @@ class MCPComponent(Component):
|
||||
}
|
||||
}
|
||||
|
||||
def get_settings_modifications(self) -> Dict[str, Any]:
|
||||
"""Get settings.json modifications (now only Claude Code compatible settings)"""
|
||||
# Return empty dict as we don't modify Claude Code settings
|
||||
return {}
|
||||
|
||||
def _check_mcp_server_installed(self, server_name: str) -> bool:
|
||||
"""Check if MCP server is already installed"""
|
||||
try:
|
||||
@@ -185,8 +171,7 @@ class MCPComponent(Component):
|
||||
server_name = server_info["name"]
|
||||
npm_package = server_info["npm_package"]
|
||||
|
||||
# Get the command to use - either specified in server_info or default to npx format
|
||||
command = server_info.get("command", f"npx {npm_package}")
|
||||
command = "npx"
|
||||
|
||||
try:
|
||||
self.logger.info(f"Installing MCP server: {server_name}")
|
||||
@@ -213,14 +198,14 @@ class MCPComponent(Component):
|
||||
self.logger.warning(f"Proceeding without {api_key_env} - server may not function properly")
|
||||
|
||||
# Install using Claude CLI
|
||||
if config.get("dry_run", False):
|
||||
self.logger.info(f"Would install MCP server (user scope): claude mcp add -s user {server_name} {command}")
|
||||
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}")
|
||||
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],
|
||||
["claude", "mcp", "add", "-s", "user", "--", server_name, command, "-y", npm_package],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120, # 2 minutes timeout for installation
|
||||
@@ -277,90 +262,83 @@ class MCPComponent(Component):
|
||||
self.logger.error(f"Error uninstalling MCP server {server_name}: {e}")
|
||||
return False
|
||||
|
||||
def install(self, config: Dict[str, Any]) -> bool:
|
||||
def _install(self, config: Dict[str, Any]) -> bool:
|
||||
"""Install MCP component"""
|
||||
try:
|
||||
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
|
||||
|
||||
# Update metadata
|
||||
try:
|
||||
# Add component registration to metadata
|
||||
self.settings_manager.add_component_registration("mcp", {
|
||||
"version": "3.0.0",
|
||||
"category": "integration",
|
||||
"servers_count": len(self.mcp_servers)
|
||||
})
|
||||
|
||||
# Add MCP configuration to metadata
|
||||
metadata = self.settings_manager.load_metadata()
|
||||
metadata["mcp"] = {
|
||||
"enabled": True,
|
||||
"servers": list(self.mcp_servers.keys()),
|
||||
"auto_update": False
|
||||
}
|
||||
self.settings_manager.save_metadata(metadata)
|
||||
|
||||
self.logger.info("Updated metadata with MCP component registration")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to update metadata: {e}")
|
||||
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 True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.exception(f"Unexpected error during MCP installation: {e}")
|
||||
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"""
|
||||
@@ -497,6 +475,10 @@ class MCPComponent(Component):
|
||||
|
||||
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
|
||||
@@ -513,4 +495,4 @@ class MCPComponent(Component):
|
||||
"estimated_size": self.get_size_estimate(),
|
||||
"dependencies": self.get_dependencies(),
|
||||
"required_tools": ["node", "npm", "claude"]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user