Add CLI alias setup for first-time users (#9542)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Xingyao Wang 2025-07-13 11:36:06 -04:00 committed by GitHub
parent bfe0aa08b6
commit 4aaa2ccd39
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 684 additions and 2 deletions

View File

@ -14,8 +14,14 @@ from openhands.cli.commands import (
handle_commands,
)
from openhands.cli.settings import modify_llm_settings_basic
from openhands.cli.shell_config import (
ShellConfigManager,
add_aliases_to_shell_config,
aliases_exist_in_shell_config,
)
from openhands.cli.tui import (
UsageMetrics,
cli_confirm,
display_agent_running_message,
display_banner,
display_event,
@ -361,6 +367,115 @@ async def run_setup_flow(config: OpenHandsConfig, settings_store: FileSettingsSt
await modify_llm_settings_basic(config, settings_store)
def run_alias_setup_flow(config: OpenHandsConfig) -> None:
"""Run the alias setup flow to configure shell aliases.
Prompts the user to set up aliases for 'openhands' and 'oh' commands.
Handles existing aliases by offering to keep or remove them.
"""
print_formatted_text('')
print_formatted_text(HTML('<gold>🚀 Welcome to OpenHands CLI!</gold>'))
print_formatted_text('')
# Check if aliases already exist
if aliases_exist_in_shell_config():
print_formatted_text(
HTML(
'<grey>We detected existing OpenHands aliases in your shell configuration.</grey>'
)
)
print_formatted_text('')
print_formatted_text(
HTML(
'<grey> • <b>openhands</b> → uvx --python 3.12 --from openhands-ai openhands</grey>'
)
)
print_formatted_text(
HTML(
'<grey> • <b>oh</b> → uvx --python 3.12 --from openhands-ai openhands</grey>'
)
)
print_formatted_text('')
print_formatted_text(
HTML('<ansigreen>✅ Aliases are already configured.</ansigreen>')
)
return # Exit early since aliases already exist
else:
# No existing aliases, show the normal setup flow
print_formatted_text(
HTML('<grey>Would you like to set up convenient shell aliases?</grey>')
)
print_formatted_text('')
print_formatted_text(
HTML(
'<grey>This will add the following aliases to your shell profile:</grey>'
)
)
print_formatted_text(
HTML(
'<grey> • <b>openhands</b> → uvx --python 3.12 --from openhands-ai openhands</grey>'
)
)
print_formatted_text(
HTML(
'<grey> • <b>oh</b> → uvx --python 3.12 --from openhands-ai openhands</grey>'
)
)
print_formatted_text('')
print_formatted_text(
HTML(
'<ansiyellow>⚠️ Note: This requires uv to be installed first.</ansiyellow>'
)
)
print_formatted_text(
HTML(
'<ansiyellow> Installation guide: https://docs.astral.sh/uv/getting-started/installation</ansiyellow>'
)
)
print_formatted_text('')
# Use cli_confirm to get user choice
choice = cli_confirm(
config,
'Set up shell aliases?',
['Yes, set up aliases', 'No, skip this step'],
)
if choice == 0: # User chose "Yes"
success = add_aliases_to_shell_config()
if success:
print_formatted_text('')
print_formatted_text(
HTML('<ansigreen>✅ Aliases added successfully!</ansigreen>')
)
# Get the appropriate reload command using the shell config manager
shell_manager = ShellConfigManager()
reload_cmd = shell_manager.get_reload_command()
print_formatted_text(
HTML(
f'<grey>Run <b>{reload_cmd}</b> (or restart your terminal) to use the new aliases.</grey>'
)
)
else:
print_formatted_text('')
print_formatted_text(
HTML(
'<ansired>❌ Failed to add aliases. You can set them up manually later.</ansired>'
)
)
else: # User chose "No"
print_formatted_text('')
print_formatted_text(
HTML(
'<grey>Skipped alias setup. You can run this setup again anytime.</grey>'
)
)
print_formatted_text('')
async def main_with_loop(loop: asyncio.AbstractEventLoop) -> None:
"""Runs the agent in CLI mode."""
args = parse_arguments()
@ -452,6 +567,17 @@ async def main_with_loop(loop: asyncio.AbstractEventLoop) -> None:
# This ensures Jupyter plugin is disabled for CLI runtime
finalize_config(config)
# Check if we should show the alias setup flow
# Only show it if aliases don't exist in the shell configuration
# and we're in an interactive environment (not during tests or CI)
if not aliases_exist_in_shell_config() and sys.stdin.isatty():
# Clear the terminal if we haven't shown a banner yet
if not banner_shown:
clear()
run_alias_setup_flow(config)
banner_shown = True
# TODO: Set working directory from config or use current working directory?
current_dir = config.workspace_base

View File

@ -0,0 +1,279 @@
"""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()

4
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
[[package]]
name = "aioboto3"
@ -11790,4 +11790,4 @@ third-party-runtimes = ["daytona", "e2b", "modal", "runloop-api-client"]
[metadata]
lock-version = "2.1"
python-versions = "^3.12,<3.14"
content-hash = "3457e02f9b9fdb18c342bf0e361bf6ad59955aa402aee19227b0aa34d352bf68"
content-hash = "8af146b6bbc131f9f4d8d6c5098865cc4f4aadaeb0158b97665b86295a246d5c"

View File

@ -72,6 +72,7 @@ anyio = "4.9.0"
pythonnet = "*"
fastmcp = "^2.5.2"
python-frontmatter = "^1.1.0"
shellingham = "^1.5.4"
# TODO: Should these go into the runtime group?
ipywidgets = "^8.1.5"
qtconsole = "^5.6.1"

View File

@ -334,7 +334,9 @@ async def test_run_session_with_initial_action(
@patch('openhands.cli.main.LLMSummarizingCondenserConfig')
@patch('openhands.cli.main.NoOpCondenserConfig')
@patch('openhands.cli.main.finalize_config')
@patch('openhands.cli.main.aliases_exist_in_shell_config')
async def test_main_without_task(
mock_aliases_exist,
mock_finalize_config,
mock_noop_condenser,
mock_llm_condenser,
@ -348,6 +350,9 @@ async def test_main_without_task(
"""Test main function without a task."""
loop = asyncio.get_running_loop()
# Mock alias setup functions to prevent the alias setup flow
mock_aliases_exist.return_value = True
# Mock arguments
mock_args = MagicMock()
mock_args.agent_cls = None
@ -420,7 +425,9 @@ async def test_main_without_task(
@patch('openhands.cli.main.LLMSummarizingCondenserConfig')
@patch('openhands.cli.main.NoOpCondenserConfig')
@patch('openhands.cli.main.finalize_config')
@patch('openhands.cli.main.aliases_exist_in_shell_config')
async def test_main_with_task(
mock_aliases_exist,
mock_finalize_config,
mock_noop_condenser,
mock_llm_condenser,
@ -434,6 +441,9 @@ async def test_main_with_task(
"""Test main function with a task."""
loop = asyncio.get_running_loop()
# Mock alias setup functions to prevent the alias setup flow
mock_aliases_exist.return_value = True
# Mock arguments
mock_args = MagicMock()
mock_args.agent_cls = 'custom-agent'
@ -517,7 +527,9 @@ async def test_main_with_task(
@patch('openhands.cli.main.LLMSummarizingCondenserConfig')
@patch('openhands.cli.main.NoOpCondenserConfig')
@patch('openhands.cli.main.finalize_config')
@patch('openhands.cli.main.aliases_exist_in_shell_config')
async def test_main_with_session_name_passes_name_to_run_session(
mock_aliases_exist,
mock_finalize_config,
mock_noop_condenser,
mock_llm_condenser,
@ -532,6 +544,9 @@ async def test_main_with_session_name_passes_name_to_run_session(
loop = asyncio.get_running_loop()
test_session_name = 'my_named_session'
# Mock alias setup functions to prevent the alias setup flow
mock_aliases_exist.return_value = True
# Mock arguments
mock_args = MagicMock()
mock_args.agent_cls = None
@ -703,7 +718,9 @@ async def test_run_session_with_name_attempts_state_restore(
@patch('openhands.cli.main.LLMSummarizingCondenserConfig')
@patch('openhands.cli.main.NoOpCondenserConfig')
@patch('openhands.cli.main.finalize_config')
@patch('openhands.cli.main.aliases_exist_in_shell_config')
async def test_main_security_check_fails(
mock_aliases_exist,
mock_finalize_config,
mock_noop_condenser,
mock_llm_condenser,
@ -717,6 +734,9 @@ async def test_main_security_check_fails(
"""Test main function when security check fails."""
loop = asyncio.get_running_loop()
# Mock alias setup functions to prevent the alias setup flow
mock_aliases_exist.return_value = True
# Mock arguments
mock_args = MagicMock()
mock_parse_args.return_value = mock_args
@ -764,7 +784,9 @@ async def test_main_security_check_fails(
@patch('openhands.cli.main.LLMSummarizingCondenserConfig')
@patch('openhands.cli.main.NoOpCondenserConfig')
@patch('openhands.cli.main.finalize_config')
@patch('openhands.cli.main.aliases_exist_in_shell_config')
async def test_config_loading_order(
mock_aliases_exist,
mock_finalize_config,
mock_noop_condenser,
mock_llm_condenser,
@ -784,6 +806,9 @@ async def test_config_loading_order(
"""
loop = asyncio.get_running_loop()
# Mock alias setup functions to prevent the alias setup flow
mock_aliases_exist.return_value = True
# Mock arguments with specific agent but no LLM config
mock_args = MagicMock()
mock_args.agent_cls = 'cmd-line-agent' # This should override settings
@ -869,9 +894,11 @@ async def test_config_loading_order(
@patch('openhands.cli.main.LLMSummarizingCondenserConfig')
@patch('openhands.cli.main.NoOpCondenserConfig')
@patch('openhands.cli.main.finalize_config')
@patch('openhands.cli.main.aliases_exist_in_shell_config')
@patch('builtins.open', new_callable=MagicMock)
async def test_main_with_file_option(
mock_open,
mock_aliases_exist,
mock_finalize_config,
mock_noop_condenser,
mock_llm_condenser,
@ -885,6 +912,9 @@ async def test_main_with_file_option(
"""Test main function with a file option."""
loop = asyncio.get_running_loop()
# Mock alias setup functions to prevent the alias setup flow
mock_aliases_exist.return_value = True
# Mock arguments
mock_args = MagicMock()
mock_args.agent_cls = None

View File

@ -0,0 +1,246 @@
"""Unit tests for CLI alias setup functionality."""
import tempfile
from pathlib import Path
from unittest.mock import patch
from openhands.cli.shell_config import (
ShellConfigManager,
add_aliases_to_shell_config,
aliases_exist_in_shell_config,
get_shell_config_path,
)
def test_get_shell_config_path_no_files_fallback():
"""Test shell config path fallback when no shell detection and no config files exist."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Mock shellingham to raise an exception (detection failure)
with patch(
'shellingham.detect_shell',
side_effect=Exception('Shell detection failed'),
):
profile_path = get_shell_config_path()
assert profile_path.name == '.bash_profile'
def test_get_shell_config_path_bash_fallback():
"""Test shell config path fallback to bash when it exists."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Create .bashrc
bashrc = Path(temp_dir) / '.bashrc'
bashrc.touch()
# Mock shellingham to raise an exception (detection failure)
with patch(
'shellingham.detect_shell',
side_effect=Exception('Shell detection failed'),
):
profile_path = get_shell_config_path()
assert profile_path.name == '.bashrc'
def test_get_shell_config_path_with_bash_detection():
"""Test shell config path when bash is detected."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Create .bashrc
bashrc = Path(temp_dir) / '.bashrc'
bashrc.touch()
# Mock shellingham to return bash
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
profile_path = get_shell_config_path()
assert profile_path.name == '.bashrc'
def test_get_shell_config_path_with_zsh_detection():
"""Test shell config path when zsh is detected."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Create .zshrc
zshrc = Path(temp_dir) / '.zshrc'
zshrc.touch()
# Mock shellingham to return zsh
with patch('shellingham.detect_shell', return_value=('zsh', 'zsh')):
profile_path = get_shell_config_path()
assert profile_path.name == '.zshrc'
def test_get_shell_config_path_with_fish_detection():
"""Test shell config path when fish is detected."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Create fish config directory and file
fish_config_dir = Path(temp_dir) / '.config' / 'fish'
fish_config_dir.mkdir(parents=True)
fish_config = fish_config_dir / 'config.fish'
fish_config.touch()
# Mock shellingham to return fish
with patch('shellingham.detect_shell', return_value=('fish', 'fish')):
profile_path = get_shell_config_path()
assert profile_path.name == 'config.fish'
assert 'fish' in str(profile_path)
def test_add_aliases_to_shell_config_bash():
"""Test adding aliases to bash config."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Mock shellingham to return bash
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
# Add aliases
success = add_aliases_to_shell_config()
assert success is True
# Get the actual path that was used
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
profile_path = get_shell_config_path()
# Check that the aliases were added
with open(profile_path, 'r') as f:
content = f.read()
assert 'alias openhands=' in content
assert 'alias oh=' in content
assert 'uvx --python 3.12 --from openhands-ai openhands' in content
def test_add_aliases_to_shell_config_zsh():
"""Test adding aliases to zsh config."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Mock shellingham to return zsh
with patch('shellingham.detect_shell', return_value=('zsh', 'zsh')):
# Add aliases
success = add_aliases_to_shell_config()
assert success is True
# Check that the aliases were added to .zshrc
profile_path = Path(temp_dir) / '.zshrc'
with open(profile_path, 'r') as f:
content = f.read()
assert 'alias openhands=' in content
assert 'alias oh=' in content
assert 'uvx --python 3.12 --from openhands-ai openhands' in content
def test_add_aliases_handles_existing_aliases():
"""Test that adding aliases handles existing aliases correctly."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Mock shellingham to return bash
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
# Add aliases first time
success = add_aliases_to_shell_config()
assert success is True
# Try adding again - should detect existing aliases
success = add_aliases_to_shell_config()
assert success is True
# Get the actual path that was used
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
profile_path = get_shell_config_path()
# Check that aliases weren't duplicated
with open(profile_path, 'r') as f:
content = f.read()
# Count occurrences of the alias
openhands_count = content.count('alias openhands=')
oh_count = content.count('alias oh=')
assert openhands_count == 1
assert oh_count == 1
def test_aliases_exist_in_shell_config_no_file():
"""Test alias detection when no shell config exists."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Mock shellingham to return bash
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
assert aliases_exist_in_shell_config() is False
def test_aliases_exist_in_shell_config_no_aliases():
"""Test alias detection when shell config exists but has no aliases."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Mock shellingham to return bash
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
# Create bash profile with other content
profile_path = get_shell_config_path()
with open(profile_path, 'w') as f:
f.write('export PATH=$PATH:/usr/local/bin\n')
assert aliases_exist_in_shell_config() is False
def test_aliases_exist_in_shell_config_with_aliases():
"""Test alias detection when aliases exist."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Mock shellingham to return bash
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
# Add aliases first
add_aliases_to_shell_config()
assert aliases_exist_in_shell_config() is True
def test_shell_config_manager_basic_functionality():
"""Test basic ShellConfigManager functionality."""
manager = ShellConfigManager()
# Test command customization
custom_manager = ShellConfigManager(command='custom-command')
assert custom_manager.command == 'custom-command'
# Test shell type detection from path
assert manager.get_shell_type_from_path(Path('/home/user/.bashrc')) == 'bash'
assert manager.get_shell_type_from_path(Path('/home/user/.zshrc')) == 'zsh'
assert (
manager.get_shell_type_from_path(Path('/home/user/.config/fish/config.fish'))
== 'fish'
)
def test_shell_config_manager_reload_commands():
"""Test reload command generation."""
manager = ShellConfigManager()
# Test different shell reload commands
assert 'source ~/.zshrc' in manager.get_reload_command(Path('/home/user/.zshrc'))
assert 'source ~/.bashrc' in manager.get_reload_command(Path('/home/user/.bashrc'))
assert 'source ~/.bash_profile' in manager.get_reload_command(
Path('/home/user/.bash_profile')
)
assert 'source ~/.config/fish/config.fish' in manager.get_reload_command(
Path('/home/user/.config/fish/config.fish')
)
def test_shell_config_manager_template_rendering():
"""Test that templates are properly rendered."""
manager = ShellConfigManager(command='test-command')
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Create a bash config file
bashrc = Path(temp_dir) / '.bashrc'
bashrc.touch()
# Mock shell detection
with patch.object(manager, 'detect_shell', return_value='bash'):
success = manager.add_aliases()
assert success is True
# Check that the custom command was used
with open(bashrc, 'r') as f:
content = f.read()
assert 'test-command' in content
assert 'alias openhands="test-command"' in content
assert 'alias oh="test-command"' in content