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

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