"""Shell configuration management for OpenHands CLI aliases. This module provides a simplified, more maintainable approach to managing shell aliases across different shell types and platforms. """ import platform import re from pathlib import Path from typing import Optional from jinja2 import Template try: import shellingham except ImportError: shellingham = None class ShellConfigManager: """Manages shell configuration files and aliases across different shells.""" # Shell configuration templates ALIAS_TEMPLATES = { 'bash': Template(""" # OpenHands CLI aliases alias openhands="{{ command }}" alias oh="{{ command }}" """), 'zsh': Template(""" # OpenHands CLI aliases alias openhands="{{ command }}" alias oh="{{ command }}" """), 'fish': Template(""" # OpenHands CLI aliases alias openhands="{{ command }}" alias oh="{{ command }}" """), 'powershell': Template(""" # OpenHands CLI aliases function openhands { {{ command }} $args } function oh { {{ command }} $args } """), } # Shell configuration file patterns SHELL_CONFIG_PATTERNS = { 'bash': ['.bashrc', '.bash_profile'], 'zsh': ['.zshrc'], 'fish': ['.config/fish/config.fish'], 'csh': ['.cshrc'], 'tcsh': ['.tcshrc'], 'ksh': ['.kshrc'], 'powershell': [ 'Documents/PowerShell/Microsoft.PowerShell_profile.ps1', 'Documents/WindowsPowerShell/Microsoft.PowerShell_profile.ps1', '.config/powershell/Microsoft.PowerShell_profile.ps1', ], } # Regex patterns for detecting existing aliases ALIAS_PATTERNS = { 'bash': [ r'^\s*alias\s+openhands\s*=', r'^\s*alias\s+oh\s*=', ], 'zsh': [ r'^\s*alias\s+openhands\s*=', r'^\s*alias\s+oh\s*=', ], 'fish': [ r'^\s*alias\s+openhands\s*=', r'^\s*alias\s+oh\s*=', ], 'powershell': [ r'^\s*function\s+openhands\s*\{', r'^\s*function\s+oh\s*\{', ], } def __init__( self, command: str = 'uvx --python 3.12 --from openhands-ai openhands' ): """Initialize the shell config manager. Args: command: The command that aliases should point to. """ self.command = command self.is_windows = platform.system() == 'Windows' def detect_shell(self) -> Optional[str]: """Detect the current shell using shellingham. Returns: Shell name if detected, None otherwise. """ if not shellingham: return None try: shell_name, _ = shellingham.detect_shell() return shell_name except Exception: return None def get_shell_config_path(self, shell: Optional[str] = None) -> Path: """Get the path to the shell configuration file. Args: shell: Shell name. If None, will attempt to detect. Returns: Path to the shell configuration file. """ if shell is None: shell = self.detect_shell() home = Path.home() # Try to find existing config file for the detected shell if shell and shell in self.SHELL_CONFIG_PATTERNS: for config_file in self.SHELL_CONFIG_PATTERNS[shell]: config_path = home / config_file if config_path.exists(): return config_path # If no existing file found, return the first option return home / self.SHELL_CONFIG_PATTERNS[shell][0] # Fallback logic if self.is_windows: # Windows fallback to PowerShell ps_profile = ( home / 'Documents' / 'PowerShell' / 'Microsoft.PowerShell_profile.ps1' ) return ps_profile else: # Unix fallback to bash bashrc = home / '.bashrc' if bashrc.exists(): return bashrc return home / '.bash_profile' def get_shell_type_from_path(self, config_path: Path) -> str: """Determine shell type from configuration file path. Args: config_path: Path to the shell configuration file. Returns: Shell type name. """ path_str = str(config_path).lower() if 'powershell' in path_str: return 'powershell' elif '.zshrc' in path_str: return 'zsh' elif 'fish' in path_str: return 'fish' elif '.bashrc' in path_str or '.bash_profile' in path_str: return 'bash' else: return 'bash' # Default fallback def aliases_exist(self, config_path: Optional[Path] = None) -> bool: """Check if OpenHands aliases already exist in the shell config. Args: config_path: Path to check. If None, will detect automatically. Returns: True if aliases exist, False otherwise. """ if config_path is None: config_path = self.get_shell_config_path() if not config_path.exists(): return False shell_type = self.get_shell_type_from_path(config_path) patterns = self.ALIAS_PATTERNS.get(shell_type, self.ALIAS_PATTERNS['bash']) try: with open(config_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() for pattern in patterns: if re.search(pattern, content, re.MULTILINE): return True return False except Exception: return False def add_aliases(self, config_path: Optional[Path] = None) -> bool: """Add OpenHands aliases to the shell configuration. Args: config_path: Path to modify. If None, will detect automatically. Returns: True if successful, False otherwise. """ if config_path is None: config_path = self.get_shell_config_path() # Check if aliases already exist if self.aliases_exist(config_path): return True try: # Ensure parent directory exists config_path.parent.mkdir(parents=True, exist_ok=True) # Get the appropriate template shell_type = self.get_shell_type_from_path(config_path) template = self.ALIAS_TEMPLATES.get( shell_type, self.ALIAS_TEMPLATES['bash'] ) # Render the aliases aliases_content = template.render(command=self.command) # Append to the config file with open(config_path, 'a', encoding='utf-8') as f: f.write(aliases_content) return True except Exception as e: print(f'Error adding aliases: {e}') return False def get_reload_command(self, config_path: Optional[Path] = None) -> str: """Get the command to reload the shell configuration. Args: config_path: Path to the config file. If None, will detect automatically. Returns: Command to reload the shell configuration. """ if config_path is None: config_path = self.get_shell_config_path() shell_type = self.get_shell_type_from_path(config_path) if shell_type == 'zsh': return 'source ~/.zshrc' elif shell_type == 'fish': return 'source ~/.config/fish/config.fish' elif shell_type == 'powershell': return '. $PROFILE' else: # bash and others if '.bash_profile' in str(config_path): return 'source ~/.bash_profile' else: return 'source ~/.bashrc' # Convenience functions that use the ShellConfigManager def add_aliases_to_shell_config() -> bool: """Add OpenHands aliases to the shell configuration.""" manager = ShellConfigManager() return manager.add_aliases() def aliases_exist_in_shell_config() -> bool: """Check if OpenHands aliases exist in the shell configuration.""" manager = ShellConfigManager() return manager.aliases_exist() def get_shell_config_path() -> Path: """Get the path to the shell configuration file.""" manager = ShellConfigManager() return manager.get_shell_config_path() def alias_setup_declined() -> bool: """Check if the user has previously declined alias setup. Returns: True if user has declined alias setup, False otherwise. """ marker_file = Path.home() / '.openhands' / '.cli_alias_setup_declined' return marker_file.exists() def mark_alias_setup_declined() -> None: """Mark that the user has declined alias setup.""" openhands_dir = Path.home() / '.openhands' openhands_dir.mkdir(exist_ok=True) marker_file = openhands_dir / '.cli_alias_setup_declined' marker_file.touch()