Skip to main content
Glama
installer.py37.3 kB
""" Node.js Package Installer Handles automatic installation of the Node.js MCP server package with comprehensive uv/uvx compatibility. Supports multiple package managers with intelligent fallback, auto-detection, and cross-platform optimization. """ import os import sys import subprocess import shutil import platform import json import tempfile import time from pathlib import Path from typing import Optional, List, Dict, Any, Tuple, Union from dataclasses import dataclass, field from concurrent.futures import ThreadPoolExecutor, as_completed from rich.console import Console from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn from rich.panel import Panel from rich.table import Table from rich.text import Text from rich.tree import Tree from packaging import version from .launcher import NodeJSLauncher, NodeJSNotFoundError class InstallationError(Exception): """Base exception for installation errors""" pass @dataclass class PackageManager: """Information about a detected package manager""" name: str path: str version: str install_cmd: List[str] global_flag: str = "-g" priority: int = 0 supports_uvx: bool = False performance_score: float = 1.0 reliability_score: float = 1.0 features: List[str] = field(default_factory=list) @dataclass class PythonManager: """Information about detected Python package managers (uv/uvx)""" name: str path: str version: str supports_nodejs: bool install_cmd: List[str] run_cmd: List[str] priority: int features: List[str] = field(default_factory=list) @dataclass class InstallationPlan: """Comprehensive installation plan with fallback strategies""" primary_manager: Union[PackageManager, PythonManager] fallback_managers: List[Union[PackageManager, PythonManager]] installation_type: str # 'global', 'local', 'uvx', 'ephemeral' estimated_time: int # seconds disk_usage: int # bytes success_probability: float # 0.0 to 1.0 class NodeJSInstaller: """ Installs Node.js packages with automatic package manager detection. Provides intelligent package manager selection, version validation, and cross-platform installation with fallback strategies. """ def __init__(self, nodejs_package: str = "@castplan/automation-mcp", console: Optional[Console] = None): self.nodejs_package = nodejs_package self.console = console or Console() self.detected_managers: List[PackageManager] = [] self.python_managers: List[PythonManager] = [] self.installation_history: List[Dict[str, Any]] = [] self.preferred_managers: List[str] = ["uvx", "uv", "pnpm", "yarn", "npm"] self.platform = platform.system().lower() self.temp_dir = None def detect_python_managers(self) -> List[PythonManager]: """ Detect available Python package managers (uv/uvx) with Node.js support. Returns: List[PythonManager]: List of detected Python managers in priority order """ python_managers_config = [ { "name": "uvx", "executables": ["uvx", "uvx.exe"] if platform.system() == "Windows" else ["uvx"], "supports_nodejs": True, "install_cmd": ["--from", "git+https://github.com/Ghostseller/CastPlan_mcp.git", "castplan-mcp"], "run_cmd": ["castplan-mcp"], "priority": 100, "features": ["ephemeral", "isolated", "fast", "no-install"] }, { "name": "uv", "executables": ["uv", "uv.exe"] if platform.system() == "Windows" else ["uv"], "supports_nodejs": True, "install_cmd": ["add", "castplan-automation"], "run_cmd": ["run", "castplan-mcp"], "priority": 90, "features": ["fast", "reliable", "modern", "lockfile"] }, { "name": "pip", "executables": ["pip", "pip3"] + (["pip.exe"] if platform.system() == "Windows" else []), "supports_nodejs": True, "install_cmd": ["install", "castplan-automation"], "run_cmd": ["castplan-mcp"], "priority": 70, "features": ["universal", "stable", "fallback"] } ] detected = [] for manager_config in python_managers_config: for executable in manager_config["executables"]: path = shutil.which(executable) if path: try: # Get version result = subprocess.run( [path, "--version"], capture_output=True, text=True, timeout=10 ) if result.returncode == 0: version_str = result.stdout.strip() manager = PythonManager( name=manager_config["name"], path=path, version=version_str, supports_nodejs=manager_config["supports_nodejs"], install_cmd=manager_config["install_cmd"], run_cmd=manager_config["run_cmd"], priority=manager_config["priority"], features=manager_config["features"] ) detected.append(manager) break # Found this manager, move to next except (subprocess.TimeoutExpired, Exception): continue # Try next executable # Sort by priority (highest first) return sorted(detected, key=lambda m: m.priority, reverse=True) def detect_package_managers(self) -> List[PackageManager]: """ Detect available Node.js package managers with enhanced scoring. Returns: List[PackageManager]: List of detected package managers in priority order """ managers_config = [ { "name": "pnpm", "executables": ["pnpm", "pnpm.exe"] if platform.system() == "Windows" else ["pnpm"], "install_cmd": ["add"], "global_flag": "-g", "priority": 95, "performance_score": 1.0, "reliability_score": 0.95, "features": ["fast", "efficient", "workspace", "lockfile"] }, { "name": "yarn", "executables": ["yarn", "yarn.exe"] if platform.system() == "Windows" else ["yarn"], "install_cmd": ["global", "add"], "global_flag": "", # yarn uses 'global add' instead of -g "priority": 85, "performance_score": 0.9, "reliability_score": 0.9, "features": ["workspace", "offline", "deterministic"] }, { "name": "npm", "executables": ["npm", "npm.exe"] if platform.system() == "Windows" else ["npm"], "install_cmd": ["install"], "global_flag": "-g", "priority": 80, "performance_score": 0.8, "reliability_score": 1.0, "features": ["universal", "stable", "registry"] }, { "name": "bun", "executables": ["bun", "bun.exe"] if platform.system() == "Windows" else ["bun"], "install_cmd": ["add"], "global_flag": "-g", "priority": 90, "performance_score": 1.0, "reliability_score": 0.85, "features": ["ultra-fast", "all-in-one", "modern"] } ] detected = [] for manager_config in managers_config: for executable in manager_config["executables"]: path = shutil.which(executable) if path: try: # Get version result = subprocess.run( [path, "--version"], capture_output=True, text=True, timeout=10 ) if result.returncode == 0: version_str = result.stdout.strip() manager = PackageManager( name=manager_config["name"], path=path, version=version_str, install_cmd=manager_config["install_cmd"], global_flag=manager_config["global_flag"], priority=manager_config["priority"], performance_score=manager_config["performance_score"], reliability_score=manager_config["reliability_score"], features=manager_config["features"] ) detected.append(manager) break # Found this manager, move to next except (subprocess.TimeoutExpired, Exception): continue # Try next executable # Sort by priority and performance score detected = sorted(detected, key=lambda m: (m.priority, m.performance_score), reverse=True) self.detected_managers = detected return detected def check_nodejs_installation(self) -> bool: """ Check if Node.js and the target package are already installed. Returns: bool: True if everything is properly installed """ try: launcher = NodeJSLauncher(self.nodejs_package) launcher.detect_nodejs() # Check if package is installed package_path = launcher.find_package_installation() if package_path: self.console.print(f"✅ Package {self.nodejs_package} is already installed") return True except NodeJSNotFoundError as e: self.console.print(f"❌ Node.js check failed: {e}") return False def install_nodejs_package(self, prefer_manager: Optional[str] = None, global_install: bool = True, force_reinstall: bool = False) -> bool: """ Install the Node.js package using the best available package manager. Args: prefer_manager: Preferred package manager name global_install: Install globally vs locally force_reinstall: Force reinstallation even if already installed Returns: bool: True if installation succeeded Raises: InstallationError: If installation fails """ # Check if already installed (unless force reinstall) if not force_reinstall and self.check_nodejs_installation(): return True # Detect available package managers if not self.detected_managers: self.detect_package_managers() # Also detect Python managers for alternative installation paths if not self.python_managers: self.python_managers = self.detect_python_managers() if not self.detected_managers: raise InstallationError("No Node.js package managers found") # Select manager selected_manager = None if prefer_manager: for manager in self.detected_managers: if manager.name == prefer_manager: selected_manager = manager break if not selected_manager: # Use first available (highest priority) selected_manager = self.detected_managers[0] self.console.print(f"🔧 Installing {self.nodejs_package} using {selected_manager.name}") # Build install command cmd = [selected_manager.path] + selected_manager.install_cmd if global_install and selected_manager.global_flag: if selected_manager.name == "yarn": # yarn already has 'global add' in install_cmd pass else: cmd.append(selected_manager.global_flag) cmd.append(self.nodejs_package) # Execute installation with progress try: with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=self.console ) as progress: task = progress.add_task(f"Installing via {selected_manager.name}...", total=None) result = subprocess.run( cmd, capture_output=True, text=True, timeout=300 # 5 minute timeout ) progress.update(task, completed=True) if result.returncode == 0: self.console.print(f"✅ Successfully installed {self.nodejs_package}") return True else: error_msg = result.stderr or result.stdout raise InstallationError(f"Installation failed: {error_msg}") except subprocess.TimeoutExpired: raise InstallationError("Installation timed out") except Exception as e: raise InstallationError(f"Installation error: {e}") def install_with_fallback(self, managers_priority: Optional[List[str]] = None, global_install: bool = True) -> bool: """ Install with automatic fallback to other package managers. Args: managers_priority: List of manager names in priority order global_install: Install globally vs locally Returns: bool: True if any installation succeeded """ if not managers_priority: managers_priority = ["npm", "yarn", "pnpm"] # Detect managers if not self.detected_managers: self.detect_package_managers() # Order managers by priority ordered_managers = [] for manager_name in managers_priority: for manager in self.detected_managers: if manager.name == manager_name: ordered_managers.append(manager) break # Add any remaining detected managers for manager in self.detected_managers: if manager not in ordered_managers: ordered_managers.append(manager) if not ordered_managers: raise InstallationError("No package managers available") # Try each manager in order last_error = None for manager in ordered_managers: try: self.console.print(f"🔄 Trying installation with {manager.name}") success = self.install_nodejs_package( prefer_manager=manager.name, global_install=global_install ) if success: return True except InstallationError as e: last_error = e self.console.print(f"❌ {manager.name} installation failed: {e}") continue # All managers failed raise InstallationError(f"All installation attempts failed. Last error: {last_error}") def verify_installation(self) -> Dict[str, Any]: """ Verify the installation and return status information. Returns: Dict[str, Any]: Installation verification results """ results = { "nodejs_installed": False, "nodejs_version": None, "package_installed": False, "package_path": None, "package_version": None, "managers_available": len(self.detected_managers), "errors": [] } try: launcher = NodeJSLauncher(self.nodejs_package) node_info = launcher.detect_nodejs() results["nodejs_installed"] = True results["nodejs_version"] = node_info.version # Check package installation package_path = launcher.find_package_installation() if package_path: results["package_installed"] = True results["package_path"] = package_path # Try to get package version try: package_json_path = Path(package_path) / "package.json" if package_json_path.exists(): with open(package_json_path, 'r') as f: package_data = json.load(f) results["package_version"] = package_data.get("version") except Exception as e: results["errors"].append(f"Could not read package version: {e}") except NodeJSNotFoundError as e: results["errors"].append(f"Node.js not found: {e}") except Exception as e: results["errors"].append(f"Verification error: {e}") return results def get_installation_info(self) -> Dict[str, Any]: """ Get comprehensive installation information. Returns: Dict[str, Any]: Installation environment information """ if not self.detected_managers: self.detect_package_managers() return self.get_comprehensive_info() def create_installation_plan(self, prefer_manager: Optional[str] = None, installation_type: str = "auto") -> InstallationPlan: """ Create an intelligent installation plan with fallback strategies. Args: prefer_manager: Preferred package manager name installation_type: Installation type (auto, global, local, uvx, ephemeral) Returns: InstallationPlan: Comprehensive installation plan """ # Detect managers if not already done if not self.detected_managers: self.detect_package_managers() if not self.python_managers: self.python_managers = self.detect_python_managers() all_managers = [] # Add Python managers (uv/uvx) with higher priority for modern workflows for manager in self.python_managers: all_managers.append(manager) # Add Node.js managers for manager in self.detected_managers: all_managers.append(manager) if not all_managers: raise InstallationError("No package managers found") # Select primary manager primary_manager = None # Handle specific installation types if installation_type == "uvx" or installation_type == "ephemeral": # Prefer uvx for ephemeral installations for manager in self.python_managers: if manager.name == "uvx": primary_manager = manager break elif installation_type == "uv": # Prefer uv for project installations for manager in self.python_managers: if manager.name == "uv": primary_manager = manager break elif prefer_manager: # Look for preferred manager for manager in all_managers: if manager.name == prefer_manager: primary_manager = manager break if not primary_manager: # Auto-select best available manager if installation_type == "auto": # Prefer modern Python managers for auto installation primary_manager = all_managers[0] if all_managers else None else: # Fallback to first available primary_manager = all_managers[0] if all_managers else None if not primary_manager: raise InstallationError("No suitable package manager found") # Create fallback list (excluding primary) fallback_managers = [m for m in all_managers if m != primary_manager] # Determine actual installation type if installation_type == "auto": if isinstance(primary_manager, PythonManager) and primary_manager.name == "uvx": actual_type = "ephemeral" elif isinstance(primary_manager, PythonManager) and primary_manager.name == "uv": actual_type = "local" else: actual_type = "global" else: actual_type = installation_type # Estimate installation metrics estimated_time = self._estimate_installation_time(primary_manager) disk_usage = self._estimate_disk_usage(actual_type) success_probability = self._calculate_success_probability(primary_manager, fallback_managers) return InstallationPlan( primary_manager=primary_manager, fallback_managers=fallback_managers, installation_type=actual_type, estimated_time=estimated_time, disk_usage=disk_usage, success_probability=success_probability ) def _estimate_installation_time(self, manager: Union[PackageManager, PythonManager]) -> int: """Estimate installation time in seconds""" base_time = 30 # Base installation time if isinstance(manager, PythonManager): if manager.name == "uvx": return 5 # Very fast ephemeral installation elif manager.name == "uv": return 15 # Fast modern installer if isinstance(manager, PackageManager): if manager.name == "pnpm": return 20 # Fast with good caching elif manager.name == "yarn": return 25 # Moderately fast elif manager.name == "npm": return 35 # Slower but reliable return base_time def _estimate_disk_usage(self, installation_type: str) -> int: """Estimate disk usage in bytes""" if installation_type == "ephemeral": return 0 # No permanent disk usage elif installation_type == "local": return 50 * 1024 * 1024 # ~50MB for local installation else: return 100 * 1024 * 1024 # ~100MB for global installation def _calculate_success_probability(self, primary: Union[PackageManager, PythonManager], fallbacks: List[Union[PackageManager, PythonManager]]) -> float: """Calculate success probability based on manager reliability""" if isinstance(primary, PythonManager): base_probability = 0.95 if primary.name in ["uvx", "uv"] else 0.9 else: base_probability = primary.reliability_score # Increase probability based on number of fallbacks fallback_boost = min(len(fallbacks) * 0.02, 0.08) return min(base_probability + fallback_boost, 0.99) def install_with_uvx(self, args: Optional[List[str]] = None, ephemeral: bool = True) -> bool: """ Install using uvx for ephemeral execution. Args: args: Additional arguments to pass ephemeral: Use ephemeral installation (no persistent install) Returns: bool: True if installation/execution succeeded """ uvx_manager = None for manager in self.python_managers: if manager.name == "uvx": uvx_manager = manager break if not uvx_manager: raise InstallationError("uvx not found") self.console.print(f"🚀 Running with uvx (ephemeral mode)") # Build uvx command cmd = [uvx_manager.path] if ephemeral: cmd.extend(["--from", "git+https://github.com/Ghostseller/CastPlan_mcp.git", "castplan-mcp"]) else: cmd.extend(["castplan-automation"]) if args: cmd.extend(args) try: with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=self.console ) as progress: task = progress.add_task("Executing with uvx...", total=None) result = subprocess.run( cmd, capture_output=False, # Allow direct output text=True, timeout=300 ) progress.update(task, completed=True) if result.returncode == 0: self.console.print("✅ uvx execution completed successfully") return True else: raise InstallationError(f"uvx execution failed with code {result.returncode}") except subprocess.TimeoutExpired: raise InstallationError("uvx execution timed out") except Exception as e: raise InstallationError(f"uvx execution error: {e}") def install_with_uv(self, project_mode: bool = True, args: Optional[List[str]] = None) -> bool: """ Install using uv for modern Python dependency management. Args: project_mode: Install in project mode vs global args: Additional arguments Returns: bool: True if installation succeeded """ uv_manager = None for manager in self.python_managers: if manager.name == "uv": uv_manager = manager break if not uv_manager: raise InstallationError("uv not found") self.console.print(f"📦 Installing with uv {'(project mode)' if project_mode else '(global)'}") # Build uv command cmd = [uv_manager.path] if project_mode: cmd.extend(["add", "castplan-automation"]) else: cmd.extend(["tool", "install", "castplan-automation"]) try: with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), BarColumn(), TaskProgressColumn(), console=self.console ) as progress: task = progress.add_task("Installing with uv...", total=100) # Start installation process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) # Monitor progress while process.poll() is None: progress.advance(task, 1) time.sleep(0.1) progress.update(task, completed=100) stdout, stderr = process.communicate() if process.returncode == 0: self.console.print("✅ uv installation completed successfully") return True else: error_msg = stderr or stdout raise InstallationError(f"uv installation failed: {error_msg}") except Exception as e: raise InstallationError(f"uv installation error: {e}") def smart_install(self, installation_type: str = "auto", prefer_manager: Optional[str] = None, interactive: bool = True) -> bool: """ Intelligent installation with automatic tool selection and fallback. Args: installation_type: Installation type preference prefer_manager: Preferred package manager interactive: Allow interactive prompts Returns: bool: True if installation succeeded """ # Create installation plan try: plan = self.create_installation_plan(prefer_manager, installation_type) except InstallationError as e: self.console.print(f"❌ Could not create installation plan: {e}") return False # Display installation plan if interactive if interactive: self._display_installation_plan(plan) # Execute installation plan return self._execute_installation_plan(plan) def _display_installation_plan(self, plan: InstallationPlan) -> None: """Display the installation plan to user""" panel_content = [] # Primary manager info if isinstance(plan.primary_manager, PythonManager): manager_type = "Python Manager" features = ", ".join(plan.primary_manager.features) else: manager_type = "Node.js Manager" features = ", ".join(plan.primary_manager.features) panel_content.extend([ f"[bold cyan]Primary Manager:[/bold cyan] {plan.primary_manager.name} ({manager_type})", f"[dim]Version:[/dim] {plan.primary_manager.version}", f"[dim]Features:[/dim] {features}", f"[dim]Installation Type:[/dim] {plan.installation_type}", f"[dim]Estimated Time:[/dim] {plan.estimated_time}s", f"[dim]Success Probability:[/dim] {plan.success_probability:.1%}", ]) if plan.fallback_managers: panel_content.append(f"[dim]Fallbacks:[/dim] {len(plan.fallback_managers)} available") self.console.print(Panel( "\n".join(panel_content), title="[bold blue]Installation Plan[/bold blue]", border_style="blue" )) def _execute_installation_plan(self, plan: InstallationPlan) -> bool: """Execute the installation plan with fallback handling""" primary = plan.primary_manager # Try primary manager try: if isinstance(primary, PythonManager): if primary.name == "uvx": return self.install_with_uvx() elif primary.name == "uv": return self.install_with_uv() else: # Use pip or other Python manager return self._install_with_python_manager(primary) else: # Use Node.js package manager return self.install_nodejs_package( prefer_manager=primary.name, global_install=(plan.installation_type == "global") ) except InstallationError as e: self.console.print(f"⚠️ Primary installation failed: {e}") # Try fallback managers for fallback in plan.fallback_managers: try: self.console.print(f"🔄 Trying fallback: {fallback.name}") if isinstance(fallback, PythonManager): if fallback.name == "uvx": return self.install_with_uvx() elif fallback.name == "uv": return self.install_with_uv() else: return self._install_with_python_manager(fallback) else: return self.install_nodejs_package( prefer_manager=fallback.name, global_install=(plan.installation_type == "global") ) except InstallationError as fallback_error: self.console.print(f"❌ Fallback {fallback.name} failed: {fallback_error}") continue # All methods failed raise InstallationError("All installation methods failed") return False def _install_with_python_manager(self, manager: PythonManager) -> bool: """Install using a generic Python package manager""" cmd = [manager.path] + manager.install_cmd try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=300 ) if result.returncode == 0: self.console.print(f"✅ Installation with {manager.name} succeeded") return True else: error_msg = result.stderr or result.stdout raise InstallationError(f"Installation failed: {error_msg}") except subprocess.TimeoutExpired: raise InstallationError(f"Installation with {manager.name} timed out") except Exception as e: raise InstallationError(f"Installation with {manager.name} error: {e}") def get_comprehensive_info(self) -> Dict[str, Any]: """Get comprehensive installation environment information""" if not self.detected_managers: self.detect_package_managers() if not self.python_managers: self.python_managers = self.detect_python_managers() return { "platform": { "system": platform.system(), "version": platform.version(), "architecture": platform.architecture()[0], "python_version": platform.python_version() }, "python_managers": [ { "name": manager.name, "path": manager.path, "version": manager.version, "priority": manager.priority, "features": manager.features, "supports_nodejs": manager.supports_nodejs } for manager in self.python_managers ], "nodejs_managers": [ { "name": manager.name, "path": manager.path, "version": manager.version, "priority": manager.priority, "performance_score": manager.performance_score, "reliability_score": manager.reliability_score, "features": manager.features } for manager in self.detected_managers ], "environment": { "PATH": os.environ.get("PATH", ""), "NODE_PATH": os.environ.get("NODE_PATH", ""), "NPM_CONFIG_PREFIX": os.environ.get("NPM_CONFIG_PREFIX", ""), "UV_CACHE_DIR": os.environ.get("UV_CACHE_DIR", ""), "UV_CONFIG_FILE": os.environ.get("UV_CONFIG_FILE", "") }, "capabilities": { "uvx_available": any(m.name == "uvx" for m in self.python_managers), "uv_available": any(m.name == "uv" for m in self.python_managers), "modern_nodejs_managers": len([m for m in self.detected_managers if m.name in ["pnpm", "yarn", "bun"]]), "total_managers": len(self.python_managers) + len(self.detected_managers) } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Ghostseller/CastPlan_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server