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