refactor: fix installation process (#209)

# summary
* this pr refactors superclaude’s install system to boost
maintainability, cut duplicate code, and most important fixes a bunch of
issues that ppl are having when trying to install v3

Fixes: #172 #182 #189 #193 #201 #202 #206 #207

## key changes:

### architecture improvements

* moved shared logic to a base component class to reduce duplication.
* shifted config, file, and settings managers to a managers/ module for
better organization.
* streamlined installer logic by removing duplicate code patterns.

### component system refactoring

* unified install flow with custom hooks (_install(), _post_install()).
* components now auto-discover files, no hardcoding (sub)path names.
* centralized error handling and logging.
* security validation moved to base class for consistency.

### code organization

* simplified component files by leveraging base class logic.
* eliminated repetitive validation, install, and file management code.
* cleaned up imports after module restructure.

### loc impact

* 554 insertions, 863 deletions.
* net: 309 lines cut, with added functionality.


## next steps
### e2e tests

* test migration from v2: use an invalid `.claude/settings.json` with
superclaude config (i.e fields like `framework`, `components`) and
verify it migrates to the new metadata json.

### cleanup and chores

* there's still bits of dead code from the initial v3 commit that i've
noticed while refactoring this shit
* update documentation
* add guardrails (maybe use github actions?) so we can't push stuff that
breaks users envs onto master
This commit is contained in:
Mithun Gowda B
2025-07-22 19:27:55 +05:30
committed by GitHub
16 changed files with 561 additions and 870 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

@@ -4,13 +4,11 @@ Base installer logic for SuperClaude installation system fixed some issues
from typing import List, Dict, Optional, Set, Tuple, Any
from pathlib import Path
import json
import shutil
import tempfile
from datetime import datetime
from .component import Component
class Installer:
"""Main installer orchestrator"""
@@ -217,8 +215,6 @@ class Installer:
if success:
self.installed_components.add(component_name)
self._update_settings_registry(component)
# Component handles its own metadata registration
else:
self.failed_components.add(component_name)
@@ -270,102 +266,11 @@ class Installer:
all_success = False
# Continue installing other components even if one fails
# Post-installation validation
if all_success and not self.dry_run:
if not self.dry_run:
self._run_post_install_validation()
return all_success
def uninstall_component(self, component_name: str) -> bool:
"""
Uninstall a single component
Args:
component_name: Name of component to uninstall
Returns:
True if successful, False otherwise
"""
if component_name not in self.components:
raise ValueError(f"Unknown component: {component_name}")
component = self.components[component_name]
try:
if self.dry_run:
print(f"[DRY RUN] Would uninstall {component_name}")
return True
else:
success = component.uninstall()
# Component handles its own metadata removal
return success
except Exception as e:
print(f"Error uninstalling {component_name}: {e}")
return False
def _update_settings_registry(self, component: Component) -> None:
"""Update settings.json with component registration"""
if self.dry_run:
return
settings_file = self.install_dir / "settings.json"
settings = {}
if settings_file.exists():
with open(settings_file, 'r') as f:
settings = json.load(f)
# Update components registry
if 'components' not in settings:
settings['components'] = {}
metadata = component.get_metadata()
settings['components'][metadata['name']] = {
'version': metadata['version'],
'installed_at': datetime.now().isoformat(),
'category': metadata.get('category', 'unknown')
}
# Update framework.components array for operation compatibility
if 'framework' not in settings:
settings['framework'] = {}
if 'components' not in settings['framework']:
settings['framework']['components'] = []
# Add component to framework.components if not already present
component_name = metadata['name']
if component_name not in settings['framework']['components']:
settings['framework']['components'].append(component_name)
# Save settings
settings_file.parent.mkdir(parents=True, exist_ok=True)
with open(settings_file, 'w') as f:
json.dump(settings, f, indent=2)
def _remove_from_settings_registry(self, component_name: str) -> None:
"""Remove component from settings.json registry"""
if self.dry_run:
return
settings_file = self.install_dir / "settings.json"
if not settings_file.exists():
return
with open(settings_file, 'r') as f:
settings = json.load(f)
# Remove from components registry
if 'components' in settings and component_name in settings['components']:
del settings['components'][component_name]
# Remove from framework.components array for operation compatibility
if 'framework' in settings and 'components' in settings['framework']:
if component_name in settings['framework']['components']:
settings['framework']['components'].remove(component_name)
with open(settings_file, 'w') as f:
json.dump(settings, f, indent=2)
def _run_post_install_validation(self) -> None:
"""Run post-installation validation for all installed components"""
print("\nRunning post-installation validation...")

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

View File

@@ -1,15 +1,9 @@
"""Core modules for SuperClaude installation system"""
from .config_manager import ConfigManager
from .settings_manager import SettingsManager
from .file_manager import FileManager
from .validator import Validator
from .registry import ComponentRegistry
__all__ = [
'ConfigManager',
'SettingsManager',
'FileManager',
'Validator',
'ComponentRegistry'
]
]

View File

@@ -533,7 +533,7 @@ class Validator:
Installation commands dict
"""
try:
from .config_manager import ConfigManager
from ..managers.config_manager import ConfigManager
from .. import PROJECT_ROOT
config_manager = ConfigManager(PROJECT_ROOT / "config")
@@ -682,4 +682,4 @@ class Validator:
def clear_cache(self) -> None:
"""Clear validation cache"""
self.validation_cache.clear()
self.validation_cache.clear()

View File

@@ -0,0 +1,9 @@
from .config_manager import ConfigManager
from .settings_manager import SettingsManager
from .file_manager import FileManager
__all__ = [
'ConfigManager',
'SettingsManager',
'FileManager'
]

View File

@@ -1,6 +1,7 @@
"""
Settings management for SuperClaude installation system
Handles settings.json manipulation with deep merge and backup
Handles settings.json migration to the new SuperClaude metadata json file
Allows for manipulation of these json files with deep merge and backup
"""
import json
@@ -96,7 +97,31 @@ class SettingsManager:
json.dump(metadata, f, indent=2, ensure_ascii=False, sort_keys=True)
except IOError as e:
raise ValueError(f"Could not save metadata to {self.metadata_file}: {e}")
def merge_metadata(self, modifications: Dict[str, Any]) -> Dict[str, Any]:
"""
Deep merge modifications into existing settings
Args:
modifications: Settings modifications to merge
Returns:
Merged settings dict
"""
existing = self.load_metadata()
return self._deep_merge(existing, modifications)
def update_metadata(self, modifications: Dict[str, Any]) -> None:
"""
Update settings with modifications
Args:
modifications: Settings modifications to apply
create_backup: Whether to create backup before updating
"""
merged = self.merge_metadata(modifications)
self.save_metadata(merged)
def migrate_superclaude_data(self) -> bool:
"""
Migrate SuperClaude-specific data from settings.json to metadata file
@@ -322,16 +347,23 @@ class SettingsManager:
self.save_metadata(metadata)
def get_framework_version(self) -> Optional[str]:
def check_installation_exists(self) -> bool:
"""
Get SuperClaude framework version from metadata
Returns:
Version string or None if not set
"""
metadata = self.load_metadata()
framework = metadata.get("framework", {})
return framework.get("version")
return self.metadata_file.exists()
def check_v2_installation_exists(self) -> bool:
"""
Get SuperClaude framework version from metadata
Returns:
Version string or None if not set
"""
return self.settings_file.exists()
def get_metadata_setting(self, key_path: str, default: Any = None) -> Any:
"""
@@ -480,4 +512,4 @@ class SettingsManager:
return True
except (json.JSONDecodeError, IOError):
return False
return False

View File

@@ -12,8 +12,7 @@ from datetime import datetime
from typing import List, Optional, Dict, Any, Tuple
import argparse
from ..core.settings_manager import SettingsManager
from ..core.file_manager import FileManager
from ..managers.settings_manager import SettingsManager
from ..utils.ui import (
display_header, display_info, display_success, display_error,
display_warning, Menu, confirm, ProgressBar, Colors, format_size
@@ -138,8 +137,10 @@ def get_backup_directory(args: argparse.Namespace) -> Path:
def check_installation_exists(install_dir: Path) -> bool:
"""Check if SuperClaude installation exists"""
return install_dir.exists() and (install_dir / "settings.json").exists()
"""Check if SuperClaude installation (v2 included) exists"""
settings_manager = SettingsManager(install_dir)
return settings_manager.check_installation_exists() or settings_manager.check_v2_installation_exists()
def get_backup_info(backup_path: Path) -> Dict[str, Any]:

View File

@@ -11,7 +11,7 @@ import argparse
from ..base.installer import Installer
from ..core.registry import ComponentRegistry
from ..core.config_manager import ConfigManager
from ..managers.config_manager import ConfigManager
from ..core.validator import Validator
from ..utils.ui import (
display_header, display_info, display_success, display_error,
@@ -137,6 +137,8 @@ def get_components_to_install(args: argparse.Namespace, registry: ComponentRegis
# Explicit components specified
if args.components:
if 'all' in args.components:
return ["core", "commands", "hooks", "mcp"]
return args.components
# Profile-based selection

View File

@@ -10,8 +10,8 @@ from typing import List, Optional, Dict, Any
import argparse
from ..core.registry import ComponentRegistry
from ..core.settings_manager import SettingsManager
from ..core.file_manager import FileManager
from ..managers.settings_manager import SettingsManager
from ..managers.file_manager import FileManager
from ..utils.ui import (
display_header, display_info, display_success, display_error,
display_warning, Menu, confirm, ProgressBar, Colors
@@ -89,28 +89,11 @@ Examples:
return parser
def check_installation_exists(install_dir: Path) -> bool:
"""Check if SuperClaude is installed"""
settings_file = install_dir / "settings.json"
return settings_file.exists() and install_dir.exists()
def get_installed_components(install_dir: Path) -> Dict[str, str]:
def get_installed_components(install_dir: Path) -> Dict[str, Dict[str, Any]]:
"""Get currently installed components and their versions"""
try:
settings_manager = SettingsManager(install_dir)
components = {}
# Check for framework configuration in metadata
framework_config = settings_manager.get_metadata_setting("framework")
if framework_config and "components" in framework_config:
for component_name in framework_config["components"]:
version = settings_manager.get_component_version(component_name)
if version:
components[component_name] = version
return components
return settings_manager.get_installed_components()
except Exception:
return {}

View File

@@ -11,8 +11,7 @@ import argparse
from ..base.installer import Installer
from ..core.registry import ComponentRegistry
from ..core.config_manager import ConfigManager
from ..core.settings_manager import SettingsManager
from ..managers.settings_manager import SettingsManager
from ..core.validator import Validator
from ..utils.ui import (
display_header, display_info, display_success, display_error,
@@ -85,28 +84,17 @@ Examples:
return parser
def check_installation_exists(install_dir: Path) -> bool:
"""Check if SuperClaude is installed"""
settings_file = install_dir / "settings.json"
return settings_file.exists()
"""Check if SuperClaude installation exists"""
settings_manager = SettingsManager(install_dir)
return settings_manager.check_installation_exists()
def get_installed_components(install_dir: Path) -> Dict[str, str]:
def get_installed_components(install_dir: Path) -> Dict[str, Dict[str, Any]]:
"""Get currently installed components and their versions"""
try:
settings_manager = SettingsManager(install_dir)
components = {}
# Check for framework configuration in metadata
framework_config = settings_manager.get_metadata_setting("framework")
if framework_config and "components" in framework_config:
for component_name in framework_config["components"]:
version = settings_manager.get_component_version(component_name)
if version:
components[component_name] = version
return components
return settings_manager.get_installed_components()
except Exception:
return {}