mirror of
https://github.com/SuperClaude-Org/SuperClaude_Framework.git
synced 2025-12-18 18:26:46 +00:00
* Fix: Install only selected MCP servers and ensure valid empty backups This commit addresses two separate issues: 1. **MCP Installation:** The `install` command was installing all MCP servers instead of only the ones selected by the user. The `_install` method in `setup/components/mcp.py` was iterating through all available servers, not the user's selection. This has been fixed to respect the `selected_mcp_servers` configuration. A new test has been added to verify this fix. 2. **Backup Creation:** The `create_backup` method in `setup/core/installer.py` created an invalid `.tar.gz` file when the backup source was empty. This has been fixed to ensure that a valid, empty tar archive is always created. A test was added for this as well. Co-authored-by: Mithun Gowda B <mithungowda.b7411@gmail.com> Co-authored-by: Jules <jules-ai-assistant@users.noreply.github.com> * Fix: Correct installer validation for MCP and MCP Docs components This commit fixes a validation issue in the installer where it would incorrectly fail after a partial installation of MCP servers. The `MCPComponent` validation logic was checking for all "required" servers, regardless of whether they were selected by the user. This has been corrected to only validate the servers that were actually installed, by checking against the list of installed servers stored in the metadata. The metadata storage has also been fixed to only record the installed servers. The `MCPDocsComponent` was failing validation because it was not being registered in the metadata if no documentation files were installed. This has been fixed by ensuring the post-installation hook runs even when no files are copied. New tests have been added for both components to verify the corrected logic. Co-authored-by: Mithun Gowda B <mithungowda.b7411@gmail.com> Co-authored-by: Jules <jules-ai-assistant@users.noreply.github.com> * Fix: Allow re-installation of components and correct validation logic This commit fixes a bug that prevented new MCP servers from being installed on subsequent runs of the installer. It also fixes the validation logic that was causing failures after a partial installation. The key changes are: 1. A new `is_reinstallable` method has been added to the base `Component` class. This allows certain components (like the `mcp` component) to be re-run even if they are already marked as installed. 2. The installer logic has been updated to respect this new method. 3. The `MCPComponent` now correctly stores only the installed servers in the metadata. 4. The validation logic for `MCPComponent` and `MCPDocsComponent` has been corrected to prevent incorrect failures. New tests have been added to verify all aspects of the new logic. Co-authored-by: Mithun Gowda B <mithungowda.b7411@gmail.com> Co-authored-by: Jules <jules-ai-assistant@users.noreply.github.com> * feat: Display authors in UI header and update author info This commit implements the user's request to display author names and emails in the UI header of the installer. The key changes are: 1. The `__email__` field in `SuperClaude/__init__.py` has been updated to include both authors' emails. 2. The `display_header` function in `setup/utils/ui.py` has been modified to read the author and email information and display it. 3. A new test has been added to `tests/test_ui.py` to verify the new UI output. Co-authored-by: Mithun Gowda B <mithungowda.b7411@gmail.com> Co-authored-by: Jules <jules-ai-assistant@users.noreply.github.com> * feat: Version bump to 4.1.0 and various fixes This commit prepares the project for the v4.1.0 release. It includes a version bump across all relevant files and incorporates several bug fixes and feature enhancements from recent tasks. Key changes in this release: - **Version Bump**: The project version has been updated from 4.0.9 to 4.1.0 in all configuration files, documentation, and source code. - **Installer Fixes**: - Components can now be marked as `reinstallable`, allowing them to be re-run on subsequent installations. This fixes a bug where new MCP servers could not be added. - The validation logic for `mcp` and `mcp_docs` components has been corrected to avoid incorrect failures. - A bug in the backup creation process that created invalid empty archives has been fixed. - **UI Enhancements**: - Author names and emails are now displayed in the installer UI header. - **Metadata Updates**: - Mithun Gowda B has been added as an author. - **New Tests**: - Comprehensive tests have been added for the installer logic, MCP components, and UI changes to ensure correctness and prevent regressions. Co-authored-by: Mithun Gowda B <mithungowda.b7411@gmail.com> Co-authored-by: Jules <jules-ai-assistant@users.noreply.github.com> * fix: Resolve dependencies for partial installs and other fixes This commit addresses several issues, the main one being a dependency resolution failure during partial installations. Key changes: - **Dependency Resolution**: The installer now correctly resolves the full dependency tree when a user requests to install a subset of components. This fixes the "Unknown component: core" error. - **Component Re-installation**: A new `is_reinstallable` flag allows components like `mcp` to be re-run on subsequent installs, enabling the addition of new servers. - **Validation Logic**: The validation for `mcp` and `mcp_docs` has been corrected to avoid spurious failures. - **UI and Metadata**: Author information has been added to the UI header and source files. - **Version Bump**: The project version has been updated to 4.1.0. - **Tests**: New tests have been added to cover all the above changes. Co-authored-by: Mithun Gowda B <mithungowda.b7411@gmail.com> Co-authored-by: Jules <jules-ai-assistant@users.noreply.github.com> * fix: Installer fixes and version bump to 4.1.0 This commit includes a collection of fixes for the installer logic, UI enhancements, and a version bump to 4.1.0. Key changes: - **Dependency Resolution**: The installer now correctly resolves the full dependency tree for partial installations, fixing the "Unknown component: core" error. - **Component Re-installation**: A new `is_reinstallable` flag allows components like `mcp` to be re-run to add new servers. - **MCP Installation**: The non-interactive installation of the `mcp` component now correctly prompts the user to select servers. - **Validation Logic**: The post-installation validation logic has been corrected to only validate components from the current session and to use the correct list of installed servers. - **UI & Metadata**: Author information has been added to the UI and source files. - **Version Bump**: The project version has been updated from 4.0.9 to 4.1.0 across all files. - **Tests**: New tests have been added to cover all the bug fixes. Co-authored-by: Mithun Gowda B <mithungowda.b7411@gmail.com> Co-authored-by: Jules <jules-ai-assistant@users.noreply.github.com> * feat: Add --authors flag and multiple installer fixes This commit introduces the `--authors` flag to display author information and includes a collection of fixes for the installer logic. Key changes: - **New Feature**: Added an `--authors` flag that displays the names, emails, and GitHub usernames of the project authors. - **Dependency Resolution**: Fixed a critical bug where partial installations would fail due to unresolved dependencies. - **Component Re-installation**: Added a mechanism to allow components to be "reinstallable", fixing an issue that prevented adding new MCP servers on subsequent runs. - **MCP Installation**: The non-interactive installation of the `mcp` component now correctly prompts for server selection. - **Validation Logic**: Corrected the post-installation validation to prevent spurious errors. - **Version Bump**: The project version has been updated to 4.1.0. - **Metadata**: Author and GitHub information has been added to the source files. - **UI**: The installer header now displays author information. - **Tests**: Added new tests for all new features and bug fixes. Co-authored-by: Jules <jules-ai-assistant@users.noreply.github.com> --------- Co-authored-by: Mithun Gowda B <mithungowda.b7411@gmail.com> Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Jules <jules-ai-assistant@users.noreply.github.com>
686 lines
27 KiB
Python
686 lines
27 KiB
Python
"""
|
|
SuperClaude Installation Operation Module
|
|
Refactored from install.py for unified CLI hub
|
|
"""
|
|
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
from typing import List, Optional, Dict, Any
|
|
import argparse
|
|
|
|
from ...core.installer import Installer
|
|
from ...core.registry import ComponentRegistry
|
|
from ...services.config import ConfigService
|
|
from ...core.validator import Validator
|
|
from ...utils.ui import (
|
|
display_header, display_info, display_success, display_error,
|
|
display_warning, Menu, confirm, ProgressBar, Colors, format_size, prompt_api_key
|
|
)
|
|
from ...utils.environment import setup_environment_variables
|
|
from ...utils.logger import get_logger
|
|
from ... import DEFAULT_INSTALL_DIR, PROJECT_ROOT, DATA_DIR
|
|
from . import OperationBase
|
|
|
|
|
|
class InstallOperation(OperationBase):
|
|
"""Installation operation implementation"""
|
|
|
|
def __init__(self):
|
|
super().__init__("install")
|
|
|
|
|
|
def register_parser(subparsers, global_parser=None) -> argparse.ArgumentParser:
|
|
"""Register installation CLI arguments"""
|
|
parents = [global_parser] if global_parser else []
|
|
|
|
parser = subparsers.add_parser(
|
|
"install",
|
|
help="Install SuperClaude framework components",
|
|
description="Install SuperClaude Framework with various options and profiles",
|
|
epilog="""
|
|
Examples:
|
|
SuperClaude install # Interactive installation
|
|
SuperClaude install --dry-run # Dry-run mode
|
|
SuperClaude install --components core mcp # Specific components
|
|
SuperClaude install --verbose --force # Verbose with force mode
|
|
""",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
parents=parents
|
|
)
|
|
|
|
# Installation mode options
|
|
|
|
parser.add_argument(
|
|
"--components",
|
|
type=str,
|
|
nargs="+",
|
|
help="Specific components to install"
|
|
)
|
|
|
|
# Installation options
|
|
parser.add_argument(
|
|
"--no-backup",
|
|
action="store_true",
|
|
help="Skip backup creation"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--list-components",
|
|
action="store_true",
|
|
help="List available components and exit"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--diagnose",
|
|
action="store_true",
|
|
help="Run system diagnostics and show installation help"
|
|
)
|
|
|
|
return parser
|
|
|
|
|
|
def validate_system_requirements(validator: Validator, component_names: List[str]) -> bool:
|
|
"""Validate system requirements"""
|
|
logger = get_logger()
|
|
|
|
logger.info("Validating system requirements...")
|
|
|
|
try:
|
|
# Load requirements configuration
|
|
config_manager = ConfigService(DATA_DIR)
|
|
requirements = config_manager.get_requirements_for_components(component_names)
|
|
|
|
# Validate requirements
|
|
success, errors = validator.validate_component_requirements(component_names, requirements)
|
|
|
|
if success:
|
|
logger.success("All system requirements met")
|
|
return True
|
|
else:
|
|
logger.error("System requirements not met:")
|
|
for error in errors:
|
|
logger.error(f" - {error}")
|
|
|
|
# Provide additional guidance
|
|
print(f"\n{Colors.CYAN}💡 Installation Help:{Colors.RESET}")
|
|
print(" Run 'SuperClaude install --diagnose' for detailed system diagnostics")
|
|
print(" and step-by-step installation instructions.")
|
|
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Could not validate system requirements: {e}")
|
|
return False
|
|
|
|
|
|
def get_components_to_install(args: argparse.Namespace, registry: ComponentRegistry, config_manager: ConfigService) -> Optional[List[str]]:
|
|
"""Determine which components to install"""
|
|
logger = get_logger()
|
|
|
|
# Explicit components specified
|
|
if args.components:
|
|
if 'all' in args.components:
|
|
components = ["core", "commands", "agents", "modes", "mcp", "mcp_docs"]
|
|
else:
|
|
components = args.components
|
|
|
|
# If mcp is specified non-interactively, we should still ask which servers to install.
|
|
if 'mcp' in components:
|
|
selected_servers = select_mcp_servers(registry)
|
|
if not hasattr(config_manager, '_installation_context'):
|
|
config_manager._installation_context = {}
|
|
config_manager._installation_context["selected_mcp_servers"] = selected_servers
|
|
|
|
# If the user selected some servers, but didn't select mcp_docs, add it.
|
|
if selected_servers and 'mcp_docs' not in components:
|
|
components.append('mcp_docs')
|
|
|
|
return components
|
|
|
|
# Interactive two-stage selection
|
|
return interactive_component_selection(registry, config_manager)
|
|
|
|
|
|
def collect_api_keys_for_servers(selected_servers: List[str], mcp_instance) -> Dict[str, str]:
|
|
"""
|
|
Collect API keys for servers that require them
|
|
|
|
Args:
|
|
selected_servers: List of selected server keys
|
|
mcp_instance: MCP component instance
|
|
|
|
Returns:
|
|
Dictionary of environment variable names to API key values
|
|
"""
|
|
# Filter servers needing keys
|
|
servers_needing_keys = [
|
|
(server_key, mcp_instance.mcp_servers[server_key])
|
|
for server_key in selected_servers
|
|
if server_key in mcp_instance.mcp_servers and
|
|
mcp_instance.mcp_servers[server_key].get("requires_api_key", False)
|
|
]
|
|
|
|
if not servers_needing_keys:
|
|
return {}
|
|
|
|
# Display API key configuration header
|
|
print(f"\n{Colors.CYAN}{Colors.BRIGHT}═══ API Key Configuration ═══{Colors.RESET}")
|
|
print(f"{Colors.YELLOW}The following servers require API keys for full functionality:{Colors.RESET}\n")
|
|
|
|
collected_keys = {}
|
|
for server_key, server_info in servers_needing_keys:
|
|
api_key_env = server_info.get("api_key_env")
|
|
service_name = server_info["name"]
|
|
|
|
if api_key_env:
|
|
key = prompt_api_key(service_name, api_key_env)
|
|
if key:
|
|
collected_keys[api_key_env] = key
|
|
|
|
return collected_keys
|
|
|
|
|
|
def select_mcp_servers(registry: ComponentRegistry) -> List[str]:
|
|
"""Stage 1: MCP Server Selection with API Key Collection"""
|
|
logger = get_logger()
|
|
|
|
try:
|
|
# Get MCP component to access server list
|
|
mcp_instance = registry.get_component_instance("mcp", Path.home() / ".claude")
|
|
if not mcp_instance or not hasattr(mcp_instance, 'mcp_servers'):
|
|
logger.error("Could not access MCP server information")
|
|
return []
|
|
|
|
# Create MCP server menu
|
|
mcp_servers = mcp_instance.mcp_servers
|
|
server_options = []
|
|
|
|
for server_key, server_info in mcp_servers.items():
|
|
description = server_info["description"]
|
|
api_key_note = " (requires API key)" if server_info.get("requires_api_key", False) else ""
|
|
server_options.append(f"{server_key} - {description}{api_key_note}")
|
|
|
|
print(f"\n{Colors.CYAN}{Colors.BRIGHT}═══════════════════════════════════════════════════{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{Colors.BRIGHT}Stage 1: MCP Server Selection (Optional){Colors.RESET}")
|
|
print(f"{Colors.CYAN}{Colors.BRIGHT}═══════════════════════════════════════════════════{Colors.RESET}")
|
|
print(f"\n{Colors.BLUE}MCP servers extend Claude Code with specialized capabilities.{Colors.RESET}")
|
|
print(f"{Colors.BLUE}Select servers to configure (you can always add more later):{Colors.RESET}")
|
|
|
|
# Add option to skip MCP
|
|
server_options.append("Skip MCP Server installation")
|
|
|
|
menu = Menu("Select MCP servers to configure:", server_options, multi_select=True)
|
|
selections = menu.display()
|
|
|
|
if not selections:
|
|
logger.info("No MCP servers selected")
|
|
return []
|
|
|
|
# Filter out the "skip" option and return server keys
|
|
server_keys = list(mcp_servers.keys())
|
|
selected_servers = []
|
|
|
|
for i in selections:
|
|
if i < len(server_keys): # Not the "skip" option
|
|
selected_servers.append(server_keys[i])
|
|
|
|
if selected_servers:
|
|
logger.info(f"Selected MCP servers: {', '.join(selected_servers)}")
|
|
|
|
# NEW: Collect API keys for selected servers
|
|
collected_keys = collect_api_keys_for_servers(selected_servers, mcp_instance)
|
|
|
|
# Set up environment variables
|
|
if collected_keys:
|
|
setup_environment_variables(collected_keys)
|
|
|
|
# Store keys for MCP component to use during installation
|
|
mcp_instance.collected_api_keys = collected_keys
|
|
else:
|
|
logger.info("No MCP servers selected")
|
|
|
|
return selected_servers
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in MCP server selection: {e}")
|
|
return []
|
|
|
|
|
|
def select_framework_components(registry: ComponentRegistry, config_manager: ConfigService, selected_mcp_servers: List[str]) -> List[str]:
|
|
"""Stage 2: Framework Component Selection"""
|
|
logger = get_logger()
|
|
|
|
try:
|
|
# Framework components (excluding MCP-related ones)
|
|
framework_components = ["core", "modes", "commands", "agents"]
|
|
|
|
# Create component menu
|
|
component_options = []
|
|
component_info = {}
|
|
|
|
for component_name in framework_components:
|
|
metadata = registry.get_component_metadata(component_name)
|
|
if metadata:
|
|
description = metadata.get("description", "No description")
|
|
component_options.append(f"{component_name} - {description}")
|
|
component_info[component_name] = metadata
|
|
|
|
# Add MCP documentation option
|
|
if selected_mcp_servers:
|
|
mcp_docs_desc = f"MCP documentation for {', '.join(selected_mcp_servers)} (auto-selected)"
|
|
component_options.append(f"mcp_docs - {mcp_docs_desc}")
|
|
auto_selected_mcp_docs = True
|
|
else:
|
|
component_options.append("mcp_docs - MCP server documentation (none selected)")
|
|
auto_selected_mcp_docs = False
|
|
|
|
print(f"\n{Colors.CYAN}{Colors.BRIGHT}═══════════════════════════════════════════════════{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{Colors.BRIGHT}Stage 2: Framework Component Selection{Colors.RESET}")
|
|
print(f"{Colors.CYAN}{Colors.BRIGHT}═══════════════════════════════════════════════════{Colors.RESET}")
|
|
print(f"\n{Colors.BLUE}Select SuperClaude framework components to install:{Colors.RESET}")
|
|
|
|
menu = Menu("Select components (Core is recommended):", component_options, multi_select=True)
|
|
selections = menu.display()
|
|
|
|
if not selections:
|
|
# Default to core if nothing selected
|
|
logger.info("No components selected, defaulting to core")
|
|
selected_components = ["core"]
|
|
else:
|
|
selected_components = []
|
|
all_components = framework_components + ["mcp_docs"]
|
|
|
|
for i in selections:
|
|
if i < len(all_components):
|
|
selected_components.append(all_components[i])
|
|
|
|
# Auto-select MCP docs if not explicitly deselected and we have MCP servers
|
|
if auto_selected_mcp_docs and "mcp_docs" not in selected_components:
|
|
# Check if user explicitly deselected it
|
|
mcp_docs_index = len(framework_components) # Index of mcp_docs in the menu
|
|
if mcp_docs_index not in selections:
|
|
# User didn't select it, but we auto-select it
|
|
selected_components.append("mcp_docs")
|
|
logger.info("Auto-selected MCP documentation for configured servers")
|
|
|
|
# Always include MCP component if servers were selected
|
|
if selected_mcp_servers and "mcp" not in selected_components:
|
|
selected_components.append("mcp")
|
|
|
|
logger.info(f"Selected framework components: {', '.join(selected_components)}")
|
|
return selected_components
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in framework component selection: {e}")
|
|
return ["core"] # Fallback to core
|
|
|
|
|
|
def interactive_component_selection(registry: ComponentRegistry, config_manager: ConfigService) -> Optional[List[str]]:
|
|
"""Two-stage interactive component selection"""
|
|
logger = get_logger()
|
|
|
|
try:
|
|
print(f"\n{Colors.CYAN}SuperClaude Interactive Installation{Colors.RESET}")
|
|
print(f"{Colors.BLUE}Select components to install using the two-stage process:{Colors.RESET}")
|
|
|
|
# Stage 1: MCP Server Selection
|
|
selected_mcp_servers = select_mcp_servers(registry)
|
|
|
|
# Stage 2: Framework Component Selection
|
|
selected_components = select_framework_components(registry, config_manager, selected_mcp_servers)
|
|
|
|
# Store selected MCP servers for components to use
|
|
if not hasattr(config_manager, '_installation_context'):
|
|
config_manager._installation_context = {}
|
|
config_manager._installation_context["selected_mcp_servers"] = selected_mcp_servers
|
|
|
|
return selected_components
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in component selection: {e}")
|
|
return None
|
|
|
|
|
|
def display_installation_plan(components: List[str], registry: ComponentRegistry, install_dir: Path) -> None:
|
|
"""Display installation plan"""
|
|
logger = get_logger()
|
|
|
|
print(f"\n{Colors.CYAN}{Colors.BRIGHT}Installation Plan{Colors.RESET}")
|
|
print("=" * 50)
|
|
|
|
# Resolve dependencies
|
|
try:
|
|
ordered_components = registry.resolve_dependencies(components)
|
|
|
|
print(f"{Colors.BLUE}Installation Directory:{Colors.RESET} {install_dir}")
|
|
print(f"{Colors.BLUE}Components to install:{Colors.RESET}")
|
|
|
|
total_size = 0
|
|
for i, component_name in enumerate(ordered_components, 1):
|
|
metadata = registry.get_component_metadata(component_name)
|
|
if metadata:
|
|
description = metadata.get("description", "No description")
|
|
print(f" {i}. {component_name} - {description}")
|
|
|
|
# Get size estimate if component supports it
|
|
try:
|
|
instance = registry.get_component_instance(component_name, install_dir)
|
|
if instance and hasattr(instance, 'get_size_estimate'):
|
|
size = instance.get_size_estimate()
|
|
total_size += size
|
|
except Exception:
|
|
pass
|
|
else:
|
|
print(f" {i}. {component_name} - Unknown component")
|
|
|
|
if total_size > 0:
|
|
print(f"\n{Colors.BLUE}Estimated size:{Colors.RESET} {format_size(total_size)}")
|
|
|
|
print()
|
|
|
|
except Exception as e:
|
|
logger.error(f"Could not resolve dependencies: {e}")
|
|
raise
|
|
|
|
|
|
def run_system_diagnostics(validator: Validator) -> None:
|
|
"""Run comprehensive system diagnostics"""
|
|
logger = get_logger()
|
|
|
|
print(f"\n{Colors.CYAN}{Colors.BRIGHT}SuperClaude System Diagnostics{Colors.RESET}")
|
|
print("=" * 50)
|
|
|
|
# Run diagnostics
|
|
diagnostics = validator.diagnose_system()
|
|
|
|
# Display platform info
|
|
print(f"{Colors.BLUE}Platform:{Colors.RESET} {diagnostics['platform']}")
|
|
|
|
# Display check results
|
|
print(f"\n{Colors.BLUE}System Checks:{Colors.RESET}")
|
|
all_passed = True
|
|
|
|
for check_name, check_info in diagnostics['checks'].items():
|
|
status = check_info['status']
|
|
message = check_info['message']
|
|
|
|
if status == 'pass':
|
|
print(f" ✅ {check_name}: {message}")
|
|
else:
|
|
print(f" ❌ {check_name}: {message}")
|
|
all_passed = False
|
|
|
|
# Display issues and recommendations
|
|
if diagnostics['issues']:
|
|
print(f"\n{Colors.YELLOW}Issues Found:{Colors.RESET}")
|
|
for issue in diagnostics['issues']:
|
|
print(f" ⚠️ {issue}")
|
|
|
|
print(f"\n{Colors.CYAN}Recommendations:{Colors.RESET}")
|
|
for recommendation in diagnostics['recommendations']:
|
|
print(recommendation)
|
|
|
|
# Summary
|
|
if all_passed:
|
|
print(f"\n{Colors.GREEN}✅ All system checks passed! Your system is ready for SuperClaude.{Colors.RESET}")
|
|
else:
|
|
print(f"\n{Colors.YELLOW}⚠️ Some issues found. Please address the recommendations above.{Colors.RESET}")
|
|
|
|
print(f"\n{Colors.BLUE}Next steps:{Colors.RESET}")
|
|
if all_passed:
|
|
print(" 1. Run 'SuperClaude install' to proceed with installation")
|
|
print(" 2. Choose your preferred installation mode (quick, minimal, or custom)")
|
|
else:
|
|
print(" 1. Install missing dependencies using the commands above")
|
|
print(" 2. Restart your terminal after installing tools")
|
|
print(" 3. Run 'SuperClaude install --diagnose' again to verify")
|
|
|
|
|
|
def perform_installation(components: List[str], args: argparse.Namespace, config_manager: ConfigService = None) -> bool:
|
|
"""Perform the actual installation"""
|
|
logger = get_logger()
|
|
start_time = time.time()
|
|
|
|
try:
|
|
# Create installer
|
|
installer = Installer(args.install_dir, dry_run=args.dry_run)
|
|
|
|
# Create component registry
|
|
registry = ComponentRegistry(PROJECT_ROOT / "setup" / "components")
|
|
registry.discover_components()
|
|
|
|
# Create component instances
|
|
component_instances = registry.create_component_instances(components, args.install_dir)
|
|
|
|
if not component_instances:
|
|
logger.error("No valid component instances created")
|
|
return False
|
|
|
|
# Register components with installer
|
|
installer.register_components(list(component_instances.values()))
|
|
|
|
# The 'components' list is already resolved, so we can use it directly.
|
|
ordered_components = components
|
|
|
|
# Setup progress tracking
|
|
progress = ProgressBar(
|
|
total=len(ordered_components),
|
|
prefix="Installing: ",
|
|
suffix=""
|
|
)
|
|
|
|
# Install components
|
|
logger.info(f"Installing {len(ordered_components)} components...")
|
|
|
|
config = {
|
|
"force": args.force,
|
|
"backup": not args.no_backup,
|
|
"dry_run": args.dry_run,
|
|
"selected_mcp_servers": getattr(config_manager, '_installation_context', {}).get("selected_mcp_servers", [])
|
|
}
|
|
|
|
success = installer.install_components(ordered_components, config)
|
|
|
|
# Update progress
|
|
for i, component_name in enumerate(ordered_components):
|
|
if component_name in installer.installed_components:
|
|
progress.update(i + 1, f"Installed {component_name}")
|
|
else:
|
|
progress.update(i + 1, f"Failed {component_name}")
|
|
time.sleep(0.1) # Brief pause for visual effect
|
|
|
|
progress.finish("Installation complete")
|
|
|
|
# Show results
|
|
duration = time.time() - start_time
|
|
|
|
if success:
|
|
logger.success(f"Installation completed successfully in {duration:.1f} seconds")
|
|
|
|
# Show summary
|
|
summary = installer.get_installation_summary()
|
|
if summary['installed']:
|
|
logger.info(f"Installed components: {', '.join(summary['installed'])}")
|
|
|
|
if summary['backup_path']:
|
|
logger.info(f"Backup created: {summary['backup_path']}")
|
|
|
|
else:
|
|
logger.error(f"Installation completed with errors in {duration:.1f} seconds")
|
|
|
|
summary = installer.get_installation_summary()
|
|
if summary['failed']:
|
|
logger.error(f"Failed components: {', '.join(summary['failed'])}")
|
|
|
|
return success
|
|
|
|
except Exception as e:
|
|
logger.exception(f"Unexpected error during installation: {e}")
|
|
return False
|
|
|
|
|
|
def run(args: argparse.Namespace) -> int:
|
|
"""Execute installation operation with parsed arguments"""
|
|
operation = InstallOperation()
|
|
operation.setup_operation_logging(args)
|
|
logger = get_logger()
|
|
# ✅ Enhanced security validation with symlink protection
|
|
expected_home = Path.home().resolve()
|
|
install_dir_original = args.install_dir
|
|
install_dir_resolved = args.install_dir.resolve()
|
|
|
|
# Check for symlink attacks - compare original vs resolved paths
|
|
try:
|
|
# Verify the resolved path is still within user home
|
|
install_dir_resolved.relative_to(expected_home)
|
|
|
|
# Additional check: if there's a symlink in the path, verify it doesn't escape user home
|
|
if install_dir_original != install_dir_resolved:
|
|
# Path contains symlinks - verify each component stays within user home
|
|
current_path = expected_home
|
|
parts = install_dir_original.parts
|
|
home_parts = expected_home.parts
|
|
|
|
# Skip home directory parts
|
|
if len(parts) >= len(home_parts) and parts[:len(home_parts)] == home_parts:
|
|
relative_parts = parts[len(home_parts):]
|
|
|
|
for part in relative_parts:
|
|
current_path = current_path / part
|
|
if current_path.is_symlink():
|
|
symlink_target = current_path.resolve()
|
|
# Ensure symlink target is also within user home
|
|
symlink_target.relative_to(expected_home)
|
|
except ValueError:
|
|
print(f"\n[✗] Installation must be inside your user profile directory.")
|
|
print(f" Expected prefix: {expected_home}")
|
|
print(f" Provided path: {install_dir_resolved}")
|
|
print(f" Security: Symlinks outside user directory are not allowed.")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
print(f"\n[✗] Security validation failed: {e}")
|
|
print(f" Please use a standard directory path within your user profile.")
|
|
sys.exit(1)
|
|
|
|
try:
|
|
# Validate global arguments
|
|
success, errors = operation.validate_global_args(args)
|
|
if not success:
|
|
for error in errors:
|
|
logger.error(error)
|
|
return 1
|
|
|
|
# Display header
|
|
if not args.quiet:
|
|
from setup.cli.base import __version__
|
|
display_header(
|
|
f"SuperClaude Installation v{__version__}",
|
|
"Installing SuperClaude framework components"
|
|
)
|
|
|
|
# Handle special modes
|
|
if args.list_components:
|
|
registry = ComponentRegistry(PROJECT_ROOT / "setup" / "components")
|
|
registry.discover_components()
|
|
|
|
components = registry.list_components()
|
|
if components:
|
|
print(f"\n{Colors.CYAN}Available Components:{Colors.RESET}")
|
|
for component_name in components:
|
|
metadata = registry.get_component_metadata(component_name)
|
|
if metadata:
|
|
desc = metadata.get("description", "No description")
|
|
category = metadata.get("category", "unknown")
|
|
print(f" {component_name} ({category}) - {desc}")
|
|
else:
|
|
print(f" {component_name} - Unknown component")
|
|
else:
|
|
print("No components found")
|
|
return 0
|
|
|
|
# Handle diagnostic mode
|
|
if args.diagnose:
|
|
validator = Validator()
|
|
run_system_diagnostics(validator)
|
|
return 0
|
|
|
|
# Create component registry and load configuration
|
|
logger.info("Initializing installation system...")
|
|
|
|
registry = ComponentRegistry(PROJECT_ROOT / "setup" / "components")
|
|
registry.discover_components()
|
|
|
|
config_manager = ConfigService(DATA_DIR)
|
|
validator = Validator()
|
|
|
|
# Validate configuration
|
|
config_errors = config_manager.validate_config_files()
|
|
if config_errors:
|
|
logger.error("Configuration validation failed:")
|
|
for error in config_errors:
|
|
logger.error(f" - {error}")
|
|
return 1
|
|
|
|
# Get components to install
|
|
components_to_install = get_components_to_install(args, registry, config_manager)
|
|
if not components_to_install:
|
|
logger.error("No components selected for installation")
|
|
return 1
|
|
|
|
# Resolve dependencies
|
|
try:
|
|
resolved_components = registry.resolve_dependencies(components_to_install)
|
|
except ValueError as e:
|
|
logger.error(f"Dependency resolution error: {e}")
|
|
return 1
|
|
|
|
# Validate system requirements for all components
|
|
if not validate_system_requirements(validator, resolved_components):
|
|
if not args.force:
|
|
logger.error("System requirements not met. Use --force to override.")
|
|
return 1
|
|
else:
|
|
logger.warning("System requirements not met, but continuing due to --force flag")
|
|
|
|
# Check for existing installation
|
|
if args.install_dir.exists() and not args.force:
|
|
if not args.dry_run:
|
|
logger.warning(f"Installation directory already exists: {args.install_dir}")
|
|
if not args.yes and not confirm("Continue and update existing installation?", default=False):
|
|
logger.info("Installation cancelled by user")
|
|
return 0
|
|
|
|
# Display installation plan
|
|
if not args.quiet:
|
|
display_installation_plan(resolved_components, registry, args.install_dir)
|
|
|
|
if not args.dry_run:
|
|
if not args.yes and not confirm("Proceed with installation?", default=True):
|
|
logger.info("Installation cancelled by user")
|
|
return 0
|
|
|
|
# Perform installation
|
|
success = perform_installation(resolved_components, args, config_manager)
|
|
|
|
if success:
|
|
if not args.quiet:
|
|
display_success("SuperClaude installation completed successfully!")
|
|
|
|
if not args.dry_run:
|
|
print(f"\n{Colors.CYAN}Next steps:{Colors.RESET}")
|
|
print(f"1. Restart your Claude Code session")
|
|
print(f"2. Framework files are now available in {args.install_dir}")
|
|
print(f"3. Use SuperClaude commands and features in Claude Code")
|
|
|
|
return 0
|
|
else:
|
|
display_error("Installation failed. Check logs for details.")
|
|
return 1
|
|
|
|
except KeyboardInterrupt:
|
|
print(f"\n{Colors.YELLOW}Installation cancelled by user{Colors.RESET}")
|
|
return 130
|
|
except Exception as e:
|
|
return operation.handle_operation_error("install", e)
|