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:
Andrew Low
2025-07-22 18:27:34 +08:00
parent fff47ec1b7
commit f7311bf480
5 changed files with 491 additions and 714 deletions

View File

@@ -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']})>"

View File

@@ -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)

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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"]
}
}