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 typing import List, Dict, Tuple, Optional, Any
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import json
|
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):
|
class Component(ABC):
|
||||||
"""Base class for all installable components"""
|
"""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
|
Initialize component with installation directory
|
||||||
|
|
||||||
@@ -20,10 +24,11 @@ class Component(ABC):
|
|||||||
"""
|
"""
|
||||||
from .. import DEFAULT_INSTALL_DIR
|
from .. import DEFAULT_INSTALL_DIR
|
||||||
self.install_dir = install_dir or DEFAULT_INSTALL_DIR
|
self.install_dir = install_dir or DEFAULT_INSTALL_DIR
|
||||||
self._metadata = None
|
self.settings_manager = SettingsManager(self.install_dir)
|
||||||
self._dependencies = None
|
self.logger = get_logger()
|
||||||
self._files_to_install = None
|
self.component_files = self._discover_component_files()
|
||||||
self._settings_modifications = None
|
self.file_manager = FileManager()
|
||||||
|
self.install_component_subdir = self.install_dir / component_subdir
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_metadata(self) -> Dict[str, str]:
|
def get_metadata(self) -> Dict[str, str]:
|
||||||
@@ -39,17 +44,58 @@ class Component(ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
def validate_prerequisites(self, installSubPath: Optional[Path] = None) -> Tuple[bool, List[str]]:
|
||||||
def validate_prerequisites(self) -> Tuple[bool, List[str]]:
|
|
||||||
"""
|
"""
|
||||||
Check prerequisites for this component
|
Check prerequisites for this component
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (success: bool, error_messages: List[str])
|
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]]:
|
def get_files_to_install(self) -> List[Tuple[Path, Path]]:
|
||||||
"""
|
"""
|
||||||
Return list of files to install
|
Return list of files to install
|
||||||
@@ -57,20 +103,37 @@ class Component(ABC):
|
|||||||
Returns:
|
Returns:
|
||||||
List of tuples (source_path, target_path)
|
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]:
|
def get_settings_modifications(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Return settings.json modifications to apply
|
Return settings.json modifications to apply
|
||||||
|
(now only Claude Code compatible settings)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict of settings to merge into settings.json
|
Dict of settings to merge into settings.json
|
||||||
"""
|
"""
|
||||||
pass
|
# Return empty dict as we don't modify Claude Code settings
|
||||||
|
return {}
|
||||||
|
|
||||||
|
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
|
@abstractmethod
|
||||||
def install(self, config: Dict[str, Any]) -> bool:
|
def _install(self, config: Dict[str, Any]) -> bool:
|
||||||
"""
|
"""
|
||||||
Perform component-specific installation logic
|
Perform component-specific installation logic
|
||||||
|
|
||||||
@@ -80,8 +143,41 @@ class Component(ABC):
|
|||||||
Returns:
|
Returns:
|
||||||
True if successful, False otherwise
|
True if successful, False otherwise
|
||||||
"""
|
"""
|
||||||
|
# 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
|
pass
|
||||||
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def uninstall(self) -> bool:
|
def uninstall(self) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -102,6 +198,11 @@ class Component(ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _get_source_dir(self) -> Optional[Path]:
|
||||||
|
"""Get source directory for component files"""
|
||||||
|
pass
|
||||||
|
|
||||||
def update(self, config: Dict[str, Any]) -> bool:
|
def update(self, config: Dict[str, Any]) -> bool:
|
||||||
"""
|
"""
|
||||||
Update component (default: uninstall then install)
|
Update component (default: uninstall then install)
|
||||||
@@ -124,8 +225,10 @@ class Component(ABC):
|
|||||||
Returns:
|
Returns:
|
||||||
Version string if installed, None otherwise
|
Version string if installed, None otherwise
|
||||||
"""
|
"""
|
||||||
|
print("GETTING INSTALLED VERSION")
|
||||||
settings_file = self.install_dir / "settings.json"
|
settings_file = self.install_dir / "settings.json"
|
||||||
if settings_file.exists():
|
if settings_file.exists():
|
||||||
|
print("SETTINGS.JSON EXISTS")
|
||||||
try:
|
try:
|
||||||
with open(settings_file, 'r') as f:
|
with open(settings_file, 'r') as f:
|
||||||
settings = json.load(f)
|
settings = json.load(f)
|
||||||
@@ -133,6 +236,7 @@ class Component(ABC):
|
|||||||
return settings.get('components', {}).get(component_name, {}).get('version')
|
return settings.get('components', {}).get(component_name, {}).get('version')
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
print("SETTINGS.JSON DOESNT EXIST RETURNING NONE")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def is_installed(self) -> bool:
|
def is_installed(self) -> bool:
|
||||||
@@ -180,6 +284,73 @@ class Component(ABC):
|
|||||||
total_size += sum(f.stat().st_size for f in source.rglob('*') if f.is_file())
|
total_size += sum(f.stat().st_size for f in source.rglob('*') if f.is_file())
|
||||||
return total_size
|
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:
|
def __str__(self) -> str:
|
||||||
"""String representation of component"""
|
"""String representation of component"""
|
||||||
metadata = self.get_metadata()
|
metadata = self.get_metadata()
|
||||||
|
|||||||
@@ -2,28 +2,17 @@
|
|||||||
Commands component for SuperClaude slash command definitions
|
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 pathlib import Path
|
||||||
|
|
||||||
from ..base.component import Component
|
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):
|
class CommandsComponent(Component):
|
||||||
"""SuperClaude slash commands component"""
|
"""SuperClaude slash commands component"""
|
||||||
|
|
||||||
def __init__(self, install_dir: Path = None):
|
def __init__(self, install_dir: Optional[Path] = None):
|
||||||
"""Initialize commands component"""
|
"""Initialize commands component"""
|
||||||
super().__init__(install_dir)
|
super().__init__(install_dir, Path("commands/sc"))
|
||||||
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()
|
|
||||||
|
|
||||||
def get_metadata(self) -> Dict[str, str]:
|
def get_metadata(self) -> Dict[str, str]:
|
||||||
"""Get component metadata"""
|
"""Get component metadata"""
|
||||||
@@ -34,53 +23,6 @@ class CommandsComponent(Component):
|
|||||||
"category": "commands"
|
"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]:
|
def get_metadata_modifications(self) -> Dict[str, Any]:
|
||||||
"""Get metadata modifications for commands component"""
|
"""Get metadata modifications for commands component"""
|
||||||
return {
|
return {
|
||||||
@@ -88,85 +30,45 @@ class CommandsComponent(Component):
|
|||||||
"commands": {
|
"commands": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"installed": True,
|
"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]:
|
def _install(self, config: Dict[str, Any]) -> bool:
|
||||||
"""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:
|
|
||||||
"""Install commands component"""
|
"""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:
|
try:
|
||||||
self.logger.info("Installing SuperClaude command definitions...")
|
metadata_mods = self.get_metadata_modifications()
|
||||||
|
self.settings_manager.update_metadata(metadata_mods)
|
||||||
# Check for and migrate existing commands from old location
|
self.logger.info("Updated metadata with commands configuration")
|
||||||
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
|
|
||||||
|
|
||||||
|
# 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:
|
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 False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def uninstall(self) -> bool:
|
def uninstall(self) -> bool:
|
||||||
"""Uninstall commands component"""
|
"""Uninstall commands component"""
|
||||||
try:
|
try:
|
||||||
@@ -176,7 +78,7 @@ class CommandsComponent(Component):
|
|||||||
commands_dir = self.install_dir / "commands" / "sc"
|
commands_dir = self.install_dir / "commands" / "sc"
|
||||||
removed_count = 0
|
removed_count = 0
|
||||||
|
|
||||||
for filename in self.command_files:
|
for filename in self.component_files:
|
||||||
file_path = commands_dir / filename
|
file_path = commands_dir / filename
|
||||||
if self.file_manager.remove_file(file_path):
|
if self.file_manager.remove_file(file_path):
|
||||||
removed_count += 1
|
removed_count += 1
|
||||||
@@ -188,7 +90,7 @@ class CommandsComponent(Component):
|
|||||||
old_commands_dir = self.install_dir / "commands"
|
old_commands_dir = self.install_dir / "commands"
|
||||||
old_removed_count = 0
|
old_removed_count = 0
|
||||||
|
|
||||||
for filename in self.command_files:
|
for filename in self.component_files:
|
||||||
old_file_path = old_commands_dir / filename
|
old_file_path = old_commands_dir / filename
|
||||||
if old_file_path.exists() and old_file_path.is_file():
|
if old_file_path.exists() and old_file_path.is_file():
|
||||||
if self.file_manager.remove_file(old_file_path):
|
if self.file_manager.remove_file(old_file_path):
|
||||||
@@ -224,6 +126,11 @@ class CommandsComponent(Component):
|
|||||||
try:
|
try:
|
||||||
if self.settings_manager.is_component_installed("commands"):
|
if self.settings_manager.is_component_installed("commands"):
|
||||||
self.settings_manager.remove_component_registration("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")
|
self.logger.info("Removed commands component from metadata")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Could not update metadata: {e}")
|
self.logger.warning(f"Could not update metadata: {e}")
|
||||||
@@ -259,7 +166,7 @@ class CommandsComponent(Component):
|
|||||||
backup_files = []
|
backup_files = []
|
||||||
|
|
||||||
if commands_dir.exists():
|
if commands_dir.exists():
|
||||||
for filename in self.command_files:
|
for filename in self.component_files:
|
||||||
file_path = commands_dir / filename
|
file_path = commands_dir / filename
|
||||||
if file_path.exists():
|
if file_path.exists():
|
||||||
backup_path = self.file_manager.backup_file(file_path)
|
backup_path = self.file_manager.backup_file(file_path)
|
||||||
@@ -307,7 +214,7 @@ class CommandsComponent(Component):
|
|||||||
return False, errors
|
return False, errors
|
||||||
|
|
||||||
# Check if all command files exist
|
# Check if all command files exist
|
||||||
for filename in self.command_files:
|
for filename in self.component_files:
|
||||||
file_path = commands_dir / filename
|
file_path = commands_dir / filename
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
errors.append(f"Missing command file: {filename}")
|
errors.append(f"Missing command file: {filename}")
|
||||||
@@ -326,68 +233,6 @@ class CommandsComponent(Component):
|
|||||||
|
|
||||||
return len(errors) == 0, errors
|
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:
|
def _get_source_dir(self) -> Path:
|
||||||
"""Get source directory for command files"""
|
"""Get source directory for command files"""
|
||||||
# Assume we're in SuperClaude/setup/components/commands.py
|
# Assume we're in SuperClaude/setup/components/commands.py
|
||||||
@@ -400,7 +245,7 @@ class CommandsComponent(Component):
|
|||||||
total_size = 0
|
total_size = 0
|
||||||
source_dir = self._get_source_dir()
|
source_dir = self._get_source_dir()
|
||||||
|
|
||||||
for filename in self.command_files:
|
for filename in self.component_files:
|
||||||
file_path = source_dir / filename
|
file_path = source_dir / filename
|
||||||
if file_path.exists():
|
if file_path.exists():
|
||||||
total_size += file_path.stat().st_size
|
total_size += file_path.stat().st_size
|
||||||
@@ -415,8 +260,8 @@ class CommandsComponent(Component):
|
|||||||
return {
|
return {
|
||||||
"component": self.get_metadata()["name"],
|
"component": self.get_metadata()["name"],
|
||||||
"version": self.get_metadata()["version"],
|
"version": self.get_metadata()["version"],
|
||||||
"files_installed": len(self.command_files),
|
"files_installed": len(self.component_files),
|
||||||
"command_files": self.command_files,
|
"command_files": self.component_files,
|
||||||
"estimated_size": self.get_size_estimate(),
|
"estimated_size": self.get_size_estimate(),
|
||||||
"install_directory": str(self.install_dir / "commands" / "sc"),
|
"install_directory": str(self.install_dir / "commands" / "sc"),
|
||||||
"dependencies": self.get_dependencies()
|
"dependencies": self.get_dependencies()
|
||||||
@@ -433,7 +278,7 @@ class CommandsComponent(Component):
|
|||||||
commands_to_migrate = []
|
commands_to_migrate = []
|
||||||
|
|
||||||
if old_commands_dir.exists():
|
if old_commands_dir.exists():
|
||||||
for filename in self.command_files:
|
for filename in self.component_files:
|
||||||
old_file_path = old_commands_dir / filename
|
old_file_path = old_commands_dir / filename
|
||||||
if old_file_path.exists() and old_file_path.is_file():
|
if old_file_path.exists() and old_file_path.is_file():
|
||||||
commands_to_migrate.append(filename)
|
commands_to_migrate.append(filename)
|
||||||
|
|||||||
@@ -2,30 +2,18 @@
|
|||||||
Core component for SuperClaude framework files installation
|
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
|
from pathlib import Path
|
||||||
import json
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from ..base.component import Component
|
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):
|
class CoreComponent(Component):
|
||||||
"""Core SuperClaude framework files component"""
|
"""Core SuperClaude framework files component"""
|
||||||
|
|
||||||
def __init__(self, install_dir: Path = None):
|
def __init__(self, install_dir: Optional[Path] = None):
|
||||||
"""Initialize core component"""
|
"""Initialize core component"""
|
||||||
super().__init__(install_dir)
|
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]:
|
def get_metadata(self) -> Dict[str, str]:
|
||||||
"""Get component metadata"""
|
"""Get component metadata"""
|
||||||
@@ -36,52 +24,6 @@ class CoreComponent(Component):
|
|||||||
"category": "core"
|
"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]:
|
def get_metadata_modifications(self) -> Dict[str, Any]:
|
||||||
"""Get metadata modifications for SuperClaude"""
|
"""Get metadata modifications for SuperClaude"""
|
||||||
return {
|
return {
|
||||||
@@ -100,94 +42,45 @@ class CoreComponent(Component):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_settings_modifications(self) -> Dict[str, Any]:
|
def _install(self, config: Dict[str, Any]) -> bool:
|
||||||
"""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:
|
|
||||||
"""Install core component"""
|
"""Install core component"""
|
||||||
|
self.logger.info("Installing SuperClaude core framework files...")
|
||||||
|
|
||||||
|
return super()._install(config);
|
||||||
|
|
||||||
|
def _post_install(self):
|
||||||
|
# Create or update metadata
|
||||||
try:
|
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
|
# Add component registration to metadata
|
||||||
success, errors = self.validate_prerequisites()
|
self.settings_manager.add_component_registration("core", {
|
||||||
if not success:
|
"version": "3.0.0",
|
||||||
for error in errors:
|
"category": "core",
|
||||||
self.logger.error(error)
|
"files_count": len(self.component_files)
|
||||||
return False
|
})
|
||||||
|
|
||||||
# Get files to install
|
self.logger.info("Updated metadata with core component registration")
|
||||||
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
|
|
||||||
|
|
||||||
|
# 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:
|
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
|
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:
|
def uninstall(self) -> bool:
|
||||||
"""Uninstall core component"""
|
"""Uninstall core component"""
|
||||||
try:
|
try:
|
||||||
@@ -195,7 +88,7 @@ class CoreComponent(Component):
|
|||||||
|
|
||||||
# Remove framework files
|
# Remove framework files
|
||||||
removed_count = 0
|
removed_count = 0
|
||||||
for filename in self.framework_files:
|
for filename in self.component_files:
|
||||||
file_path = self.install_dir / filename
|
file_path = self.install_dir / filename
|
||||||
if self.file_manager.remove_file(file_path):
|
if self.file_manager.remove_file(file_path):
|
||||||
removed_count += 1
|
removed_count += 1
|
||||||
@@ -207,6 +100,13 @@ class CoreComponent(Component):
|
|||||||
try:
|
try:
|
||||||
if self.settings_manager.is_component_installed("core"):
|
if self.settings_manager.is_component_installed("core"):
|
||||||
self.settings_manager.remove_component_registration("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")
|
self.logger.info("Removed core component from metadata")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Could not update metadata: {e}")
|
self.logger.warning(f"Could not update metadata: {e}")
|
||||||
@@ -239,7 +139,7 @@ class CoreComponent(Component):
|
|||||||
|
|
||||||
# Create backup of existing files
|
# Create backup of existing files
|
||||||
backup_files = []
|
backup_files = []
|
||||||
for filename in self.framework_files:
|
for filename in self.component_files:
|
||||||
file_path = self.install_dir / filename
|
file_path = self.install_dir / filename
|
||||||
if file_path.exists():
|
if file_path.exists():
|
||||||
backup_path = self.file_manager.backup_file(file_path)
|
backup_path = self.file_manager.backup_file(file_path)
|
||||||
@@ -281,7 +181,7 @@ class CoreComponent(Component):
|
|||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
# Check if all framework files exist
|
# Check if all framework files exist
|
||||||
for filename in self.framework_files:
|
for filename in self.component_files:
|
||||||
file_path = self.install_dir / filename
|
file_path = self.install_dir / filename
|
||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
errors.append(f"Missing framework file: {filename}")
|
errors.append(f"Missing framework file: {filename}")
|
||||||
@@ -313,69 +213,7 @@ class CoreComponent(Component):
|
|||||||
|
|
||||||
return len(errors) == 0, errors
|
return len(errors) == 0, errors
|
||||||
|
|
||||||
def _discover_framework_files(self) -> List[str]:
|
def _get_source_dir(self):
|
||||||
"""
|
|
||||||
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:
|
|
||||||
"""Get source directory for framework files"""
|
"""Get source directory for framework files"""
|
||||||
# Assume we're in SuperClaude/setup/components/core.py
|
# Assume we're in SuperClaude/setup/components/core.py
|
||||||
# and framework files are in SuperClaude/SuperClaude/Core/
|
# and framework files are in SuperClaude/SuperClaude/Core/
|
||||||
@@ -387,7 +225,7 @@ class CoreComponent(Component):
|
|||||||
total_size = 0
|
total_size = 0
|
||||||
source_dir = self._get_source_dir()
|
source_dir = self._get_source_dir()
|
||||||
|
|
||||||
for filename in self.framework_files:
|
for filename in self.component_files:
|
||||||
file_path = source_dir / filename
|
file_path = source_dir / filename
|
||||||
if file_path.exists():
|
if file_path.exists():
|
||||||
total_size += file_path.stat().st_size
|
total_size += file_path.stat().st_size
|
||||||
@@ -402,8 +240,8 @@ class CoreComponent(Component):
|
|||||||
return {
|
return {
|
||||||
"component": self.get_metadata()["name"],
|
"component": self.get_metadata()["name"],
|
||||||
"version": self.get_metadata()["version"],
|
"version": self.get_metadata()["version"],
|
||||||
"files_installed": len(self.framework_files),
|
"files_installed": len(self.component_files),
|
||||||
"framework_files": self.framework_files,
|
"framework_files": self.component_files,
|
||||||
"estimated_size": self.get_size_estimate(),
|
"estimated_size": self.get_size_estimate(),
|
||||||
"install_directory": str(self.install_dir),
|
"install_directory": str(self.install_dir),
|
||||||
"dependencies": self.get_dependencies()
|
"dependencies": self.get_dependencies()
|
||||||
|
|||||||
@@ -2,25 +2,18 @@
|
|||||||
Hooks component for Claude Code hooks integration (future-ready)
|
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 pathlib import Path
|
||||||
|
|
||||||
from ..base.component import Component
|
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):
|
class HooksComponent(Component):
|
||||||
"""Claude Code hooks integration component"""
|
"""Claude Code hooks integration component"""
|
||||||
|
|
||||||
def __init__(self, install_dir: Path = None):
|
def __init__(self, install_dir: Optional[Path] = None):
|
||||||
"""Initialize hooks component"""
|
"""Initialize hooks component"""
|
||||||
super().__init__(install_dir)
|
super().__init__(install_dir, Path("hooks"))
|
||||||
self.logger = get_logger()
|
|
||||||
self.file_manager = FileManager()
|
|
||||||
self.settings_manager = SettingsManager(self.install_dir)
|
|
||||||
|
|
||||||
# Define hook files to install (when hooks are ready)
|
# Define hook files to install (when hooks are ready)
|
||||||
self.hook_files = [
|
self.hook_files = [
|
||||||
@@ -39,59 +32,16 @@ class HooksComponent(Component):
|
|||||||
"description": "Claude Code hooks integration (future-ready)",
|
"description": "Claude Code hooks integration (future-ready)",
|
||||||
"category": "integration"
|
"category": "integration"
|
||||||
}
|
}
|
||||||
|
def get_metadata_modifications(self) -> Dict[str, Any]:
|
||||||
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"
|
|
||||||
|
|
||||||
# Build hooks configuration based on available files
|
# Build hooks configuration based on available files
|
||||||
hook_config = {}
|
hook_config = {}
|
||||||
for filename in self.hook_files:
|
for filename in self.hook_files:
|
||||||
hook_path = hooks_dir / filename
|
hook_path = self.install_component_subdir / filename
|
||||||
if hook_path.exists():
|
if hook_path.exists():
|
||||||
hook_name = filename.replace('.py', '')
|
hook_name = filename.replace('.py', '')
|
||||||
hook_config[hook_name] = [str(hook_path)]
|
hook_config[hook_name] = [str(hook_path)]
|
||||||
|
|
||||||
settings_mods = {
|
metadata_mods = {
|
||||||
"components": {
|
"components": {
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
@@ -103,31 +53,31 @@ class HooksComponent(Component):
|
|||||||
|
|
||||||
# Only add hooks configuration if we have actual hook files
|
# Only add hooks configuration if we have actual hook files
|
||||||
if hook_config:
|
if hook_config:
|
||||||
settings_mods["hooks"] = {
|
metadata_mods["hooks"] = {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
**hook_config
|
**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"""
|
"""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
|
# This component is future-ready - hooks aren't implemented yet
|
||||||
source_dir = self._get_source_dir()
|
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
|
if not source_dir.exists() or (source_dir / "PLACEHOLDER.py").exists :
|
||||||
hooks_dir = self.install_dir / "hooks"
|
self.logger.info("Hooks are not yet implemented - installing placeholder component")
|
||||||
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
|
# Create placeholder hooks directory
|
||||||
placeholder_content = '''"""
|
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
|
SuperClaude Hooks - Future Implementation
|
||||||
|
|
||||||
This directory is reserved for Claude Code hooks integration.
|
This directory is reserved for Claude Code hooks integration.
|
||||||
@@ -145,130 +95,123 @@ For more information, see SuperClaude documentation.
|
|||||||
|
|
||||||
# Placeholder for future hooks implementation
|
# Placeholder for future hooks implementation
|
||||||
def placeholder_hook():
|
def placeholder_hook():
|
||||||
"""Placeholder hook function"""
|
"""Placeholder hook function"""
|
||||||
pass
|
pass
|
||||||
'''
|
'''
|
||||||
|
|
||||||
placeholder_path = hooks_dir / "PLACEHOLDER.py"
|
placeholder_path = self.install_component_subdir / "PLACEHOLDER.py"
|
||||||
try:
|
try:
|
||||||
with open(placeholder_path, 'w') as f:
|
with open(placeholder_path, 'w') as f:
|
||||||
f.write(placeholder_content)
|
f.write(placeholder_content)
|
||||||
self.logger.debug("Created hooks placeholder file")
|
self.logger.debug("Created hooks placeholder file")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Could not create placeholder file: {e}")
|
self.logger.warning(f"Could not create placeholder file: {e}")
|
||||||
|
|
||||||
# Update settings with placeholder registration
|
# Update settings with placeholder registration
|
||||||
try:
|
try:
|
||||||
settings_mods = {
|
metadata_mods = {
|
||||||
"components": {
|
"components": {
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"installed": True,
|
"installed": True,
|
||||||
"status": "placeholder",
|
"status": "placeholder",
|
||||||
"files_count": 0
|
"files_count": 0
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.settings_manager.update_settings(settings_mods)
|
}
|
||||||
self.logger.info("Updated settings.json with hooks component registration")
|
self.settings_manager.update_metadata(metadata_mods)
|
||||||
except Exception as e:
|
self.logger.info("Updated metadata with hooks component registration")
|
||||||
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")
|
|
||||||
except Exception as e:
|
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
|
return False
|
||||||
|
|
||||||
self.logger.success(f"Hooks component installed successfully ({success_count} hook files)")
|
self.logger.success("Hooks component installed successfully (placeholder)")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
# If hooks source directory exists, install actual hooks
|
||||||
self.logger.exception(f"Unexpected error during hooks installation: {e}")
|
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
|
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:
|
def uninstall(self) -> bool:
|
||||||
"""Uninstall hooks component"""
|
"""Uninstall hooks component"""
|
||||||
try:
|
try:
|
||||||
self.logger.info("Uninstalling SuperClaude hooks component...")
|
self.logger.info("Uninstalling SuperClaude hooks component...")
|
||||||
|
|
||||||
# Remove hook files and placeholder
|
# Remove hook files and placeholder
|
||||||
hooks_dir = self.install_dir / "hooks"
|
|
||||||
removed_count = 0
|
removed_count = 0
|
||||||
|
|
||||||
# Remove actual hook files
|
# Remove actual hook files
|
||||||
for filename in self.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):
|
if self.file_manager.remove_file(file_path):
|
||||||
removed_count += 1
|
removed_count += 1
|
||||||
self.logger.debug(f"Removed {filename}")
|
self.logger.debug(f"Removed {filename}")
|
||||||
|
|
||||||
# Remove placeholder file
|
# 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):
|
if self.file_manager.remove_file(placeholder_path):
|
||||||
removed_count += 1
|
removed_count += 1
|
||||||
self.logger.debug("Removed hooks placeholder")
|
self.logger.debug("Removed hooks placeholder")
|
||||||
|
|
||||||
# Remove hooks directory if empty
|
# Remove hooks directory if empty
|
||||||
try:
|
try:
|
||||||
if hooks_dir.exists():
|
if self.install_component_subdir.exists():
|
||||||
remaining_files = list(hooks_dir.iterdir())
|
remaining_files = list(self.install_component_subdir.iterdir())
|
||||||
if not remaining_files:
|
if not remaining_files:
|
||||||
hooks_dir.rmdir()
|
self.install_component_subdir.rmdir()
|
||||||
self.logger.debug("Removed empty hooks directory")
|
self.logger.debug("Removed empty hooks directory")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Could not remove hooks directory: {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}")
|
self.logger.info(f"Updating hooks component from {current_version} to {target_version}")
|
||||||
|
|
||||||
# Create backup of existing hook files
|
# Create backup of existing hook files
|
||||||
hooks_dir = self.install_dir / "hooks"
|
|
||||||
backup_files = []
|
backup_files = []
|
||||||
|
|
||||||
if hooks_dir.exists():
|
if self.install_component_subdir.exists():
|
||||||
for filename in self.hook_files + ["PLACEHOLDER.py"]:
|
for filename in self.hook_files + ["PLACEHOLDER.py"]:
|
||||||
file_path = hooks_dir / filename
|
file_path = self.install_component_subdir / filename
|
||||||
if file_path.exists():
|
if file_path.exists():
|
||||||
backup_path = self.file_manager.backup_file(file_path)
|
backup_path = self.file_manager.backup_file(file_path)
|
||||||
if backup_path:
|
if backup_path:
|
||||||
@@ -361,8 +303,7 @@ def placeholder_hook():
|
|||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
# Check if hooks directory exists
|
# Check if hooks directory exists
|
||||||
hooks_dir = self.install_dir / "hooks"
|
if not self.install_component_subdir.exists():
|
||||||
if not hooks_dir.exists():
|
|
||||||
errors.append("Hooks directory not found")
|
errors.append("Hooks directory not found")
|
||||||
return False, errors
|
return False, errors
|
||||||
|
|
||||||
@@ -377,8 +318,8 @@ def placeholder_hook():
|
|||||||
errors.append(f"Version mismatch: installed {installed_version}, expected {expected_version}")
|
errors.append(f"Version mismatch: installed {installed_version}, expected {expected_version}")
|
||||||
|
|
||||||
# Check if we have either actual hooks or placeholder
|
# Check if we have either actual hooks or placeholder
|
||||||
has_placeholder = (hooks_dir / "PLACEHOLDER.py").exists()
|
has_placeholder = (self.install_component_subdir / "PLACEHOLDER.py").exists()
|
||||||
has_actual_hooks = any((hooks_dir / filename).exists() for filename in self.hook_files)
|
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:
|
if not has_placeholder and not has_actual_hooks:
|
||||||
errors.append("No hook files or placeholder found")
|
errors.append("No hook files or placeholder found")
|
||||||
|
|||||||
@@ -4,24 +4,19 @@ MCP component for MCP server integration
|
|||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import json
|
from typing import Dict, List, Tuple, Optional, Any
|
||||||
from typing import Dict, List, Tuple, Any
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ..base.component import Component
|
from ..base.component import Component
|
||||||
from ..core.settings_manager import SettingsManager
|
from ..utils.ui import display_info, display_warning
|
||||||
from ..utils.logger import get_logger
|
|
||||||
from ..utils.ui import confirm, display_info, display_warning
|
|
||||||
|
|
||||||
|
|
||||||
class MCPComponent(Component):
|
class MCPComponent(Component):
|
||||||
"""MCP servers integration component"""
|
"""MCP servers integration component"""
|
||||||
|
|
||||||
def __init__(self, install_dir: Path = None):
|
def __init__(self, install_dir: Optional[Path] = None):
|
||||||
"""Initialize MCP component"""
|
"""Initialize MCP component"""
|
||||||
super().__init__(install_dir)
|
super().__init__(install_dir)
|
||||||
self.logger = get_logger()
|
|
||||||
self.settings_manager = SettingsManager(self.install_dir)
|
|
||||||
|
|
||||||
# Define MCP servers to install
|
# Define MCP servers to install
|
||||||
self.mcp_servers = {
|
self.mcp_servers = {
|
||||||
@@ -29,21 +24,18 @@ class MCPComponent(Component):
|
|||||||
"name": "sequential-thinking",
|
"name": "sequential-thinking",
|
||||||
"description": "Multi-step problem solving and systematic analysis",
|
"description": "Multi-step problem solving and systematic analysis",
|
||||||
"npm_package": "@modelcontextprotocol/server-sequential-thinking",
|
"npm_package": "@modelcontextprotocol/server-sequential-thinking",
|
||||||
"command": "npx @modelcontextprotocol/server-sequential-thinking",
|
|
||||||
"required": True
|
"required": True
|
||||||
},
|
},
|
||||||
"context7": {
|
"context7": {
|
||||||
"name": "context7",
|
"name": "context7",
|
||||||
"description": "Official library documentation and code examples",
|
"description": "Official library documentation and code examples",
|
||||||
"npm_package": "@context7/mcp",
|
"npm_package": "@upstash/context7-mcp",
|
||||||
"command": "npx @context7/mcp",
|
|
||||||
"required": True
|
"required": True
|
||||||
},
|
},
|
||||||
"magic": {
|
"magic": {
|
||||||
"name": "magic",
|
"name": "magic",
|
||||||
"description": "Modern UI component generation and design systems",
|
"description": "Modern UI component generation and design systems",
|
||||||
"npm_package": "@21st/mcp",
|
"npm_package": "@21st-dev/magic",
|
||||||
"command": "npx @21st/mcp",
|
|
||||||
"required": False,
|
"required": False,
|
||||||
"api_key_env": "TWENTYFIRST_API_KEY",
|
"api_key_env": "TWENTYFIRST_API_KEY",
|
||||||
"api_key_description": "21st.dev API key for UI component generation"
|
"api_key_description": "21st.dev API key for UI component generation"
|
||||||
@@ -51,8 +43,7 @@ class MCPComponent(Component):
|
|||||||
"playwright": {
|
"playwright": {
|
||||||
"name": "playwright",
|
"name": "playwright",
|
||||||
"description": "Cross-browser E2E testing and automation",
|
"description": "Cross-browser E2E testing and automation",
|
||||||
"npm_package": "@modelcontextprotocol/server-playwright",
|
"npm_package": "@playright/mcp@latest",
|
||||||
"command": "npx @modelcontextprotocol/server-playwright",
|
|
||||||
"required": False
|
"required": False
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +57,7 @@ class MCPComponent(Component):
|
|||||||
"category": "integration"
|
"category": "integration"
|
||||||
}
|
}
|
||||||
|
|
||||||
def validate_prerequisites(self) -> Tuple[bool, List[str]]:
|
def validate_prerequisites(self, installSubPath: Optional[Path] = None) -> Tuple[bool, List[str]]:
|
||||||
"""Check prerequisites"""
|
"""Check prerequisites"""
|
||||||
errors = []
|
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:
|
def _check_mcp_server_installed(self, server_name: str) -> bool:
|
||||||
"""Check if MCP server is already installed"""
|
"""Check if MCP server is already installed"""
|
||||||
try:
|
try:
|
||||||
@@ -185,8 +171,7 @@ class MCPComponent(Component):
|
|||||||
server_name = server_info["name"]
|
server_name = server_info["name"]
|
||||||
npm_package = server_info["npm_package"]
|
npm_package = server_info["npm_package"]
|
||||||
|
|
||||||
# Get the command to use - either specified in server_info or default to npx format
|
command = "npx"
|
||||||
command = server_info.get("command", f"npx {npm_package}")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.logger.info(f"Installing MCP server: {server_name}")
|
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")
|
self.logger.warning(f"Proceeding without {api_key_env} - server may not function properly")
|
||||||
|
|
||||||
# Install using Claude CLI
|
# Install using Claude CLI
|
||||||
if config.get("dry_run", False):
|
if config.get("dry_run"):
|
||||||
self.logger.info(f"Would install MCP server (user scope): claude mcp add -s user {server_name} {command}")
|
self.logger.info(f"Would install MCP server (user scope): claude mcp add -s user {server_name} {command} -y {npm_package}")
|
||||||
return True
|
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(
|
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,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=120, # 2 minutes timeout for installation
|
timeout=120, # 2 minutes timeout for installation
|
||||||
@@ -277,91 +262,84 @@ class MCPComponent(Component):
|
|||||||
self.logger.error(f"Error uninstalling MCP server {server_name}: {e}")
|
self.logger.error(f"Error uninstalling MCP server {server_name}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def install(self, config: Dict[str, Any]) -> bool:
|
def _install(self, config: Dict[str, Any]) -> bool:
|
||||||
"""Install MCP component"""
|
"""Install MCP component"""
|
||||||
try:
|
self.logger.info("Installing SuperClaude MCP servers...")
|
||||||
self.logger.info("Installing SuperClaude MCP servers...")
|
|
||||||
|
|
||||||
# Validate prerequisites
|
# Validate prerequisites
|
||||||
success, errors = self.validate_prerequisites()
|
success, errors = self.validate_prerequisites()
|
||||||
if not success:
|
if not success:
|
||||||
for error in errors:
|
for error in errors:
|
||||||
self.logger.error(error)
|
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}")
|
|
||||||
return False
|
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:
|
def uninstall(self) -> bool:
|
||||||
"""Uninstall MCP component"""
|
"""Uninstall MCP component"""
|
||||||
try:
|
try:
|
||||||
@@ -497,6 +475,10 @@ class MCPComponent(Component):
|
|||||||
|
|
||||||
return len(errors) == 0, errors
|
return len(errors) == 0, errors
|
||||||
|
|
||||||
|
def _get_source_dir(self):
|
||||||
|
"""Get source directory for framework files"""
|
||||||
|
return None
|
||||||
|
|
||||||
def get_size_estimate(self) -> int:
|
def get_size_estimate(self) -> int:
|
||||||
"""Get estimated installation size"""
|
"""Get estimated installation size"""
|
||||||
# MCP servers are installed via npm, estimate based on typical sizes
|
# MCP servers are installed via npm, estimate based on typical sizes
|
||||||
|
|||||||
Reference in New Issue
Block a user