""" 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 ..base.installer import Installer from ..core.registry import ComponentRegistry from ..managers.config_manager import ConfigManager 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 ) from ..utils.logger import get_logger from .. import DEFAULT_INSTALL_DIR, PROJECT_ROOT 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 = ConfigManager(PROJECT_ROOT / "config") 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: ConfigManager) -> Optional[List[str]]: """Determine which components to install""" logger = get_logger() # Explicit components specified if args.components: if 'all' in args.components: return ["core", "commands", "agents", "modes", "mcp", "mcp_docs"] return args.components # Interactive two-stage selection return interactive_component_selection(registry, config_manager) def select_mcp_servers(registry: ComponentRegistry) -> List[str]: """Stage 1: MCP Server Selection""" 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)}") 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: ConfigManager, 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: ConfigManager) -> 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: ConfigManager = 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())) # Resolve dependencies ordered_components = registry.resolve_dependencies(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() # ✅ Inserted validation code expected_home = Path.home().resolve() actual_dir = args.install_dir.resolve() if not str(actual_dir).startswith(str(expected_home)): print(f"\n[✗] Installation must be inside your user profile directory.") print(f" Expected prefix: {expected_home}") print(f" Provided path: {actual_dir}") 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: display_header( "SuperClaude Installation v3.0", "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 = ConfigManager(PROJECT_ROOT / "config") 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 = get_components_to_install(args, registry, config_manager) if not components: logger.error("No components selected for installation") return 1 # Validate system requirements if not validate_system_requirements(validator, 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(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(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)