From f7311bf4808d9cdaeb5704c31bee662c8ac60a12 Mon Sep 17 00:00:00 2001 From: Andrew Low Date: Tue, 22 Jul 2025 18:27:34 +0800 Subject: [PATCH] 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 --- setup/base/component.py | 203 ++++++++++++++++++++++-- setup/components/commands.py | 247 ++++++----------------------- setup/components/core.py | 260 ++++++------------------------- setup/components/hooks.py | 293 ++++++++++++++--------------------- setup/components/mcp.py | 202 +++++++++++------------- 5 files changed, 491 insertions(+), 714 deletions(-) diff --git a/setup/base/component.py b/setup/base/component.py index ca0daa3..c0f2500 100644 --- a/setup/base/component.py +++ b/setup/base/component.py @@ -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']})>" \ No newline at end of file + return f"<{self.__class__.__name__}({self.get_metadata()['name']})>" diff --git a/setup/components/commands.py b/setup/components/commands.py index 59de6e2..c7983f8 100644 --- a/setup/components/commands.py +++ b/setup/components/commands.py @@ -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) diff --git a/setup/components/core.py b/setup/components/core.py index b2b7994..eec433c 100644 --- a/setup/components/core.py +++ b/setup/components/core.py @@ -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() - } \ No newline at end of file + } diff --git a/setup/components/hooks.py b/setup/components/hooks.py index 76deb92..c0e9b08 100644 --- a/setup/components/hooks.py +++ b/setup/components/hooks.py @@ -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() - } \ No newline at end of file + } diff --git a/setup/components/mcp.py b/setup/components/mcp.py index deec4a0..08d61e8 100644 --- a/setup/components/mcp.py +++ b/setup/components/mcp.py @@ -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"] - } \ No newline at end of file + }