fix(CLI): alias persistence issue (#9828)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Xingyao Wang 2025-07-21 17:45:14 -04:00 committed by GitHub
parent 6d7ab8a022
commit a7245f2de2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 219 additions and 89 deletions

View File

@ -17,7 +17,9 @@ from openhands.cli.settings import modify_llm_settings_basic
from openhands.cli.shell_config import (
ShellConfigManager,
add_aliases_to_shell_config,
alias_setup_declined,
aliases_exist_in_shell_config,
mark_alias_setup_declined,
)
from openhands.cli.tui import (
UsageMetrics,
@ -387,106 +389,86 @@ def run_alias_setup_flow(config: OpenHandsConfig) -> None:
Prompts the user to set up aliases for 'openhands' and 'oh' commands.
Handles existing aliases by offering to keep or remove them.
Args:
config: OpenHands configuration
"""
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>'
)
# 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('')
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(
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('')
print_formatted_text(
HTML('<ansigreen>✅ Aliases are already configured.</ansigreen>')
)
print_formatted_text(
HTML(
'<ansiyellow> Installation guide: https://docs.astral.sh/uv/getting-started/installation</ansiyellow>'
)
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('')
)
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'],
)
# 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>')
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>'
)
# 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"
)
else:
print_formatted_text('')
print_formatted_text(
HTML(
'<grey>Skipped alias setup. You can run this setup again anytime.</grey>'
'<ansired>❌ Failed to add aliases. You can set them up manually later.</ansired>'
)
)
else: # User chose "No"
# Mark that the user has declined alias setup
mark_alias_setup_declined()
print_formatted_text('')
print_formatted_text(
HTML(
'<grey>Skipped alias setup. You can run this setup again anytime.</grey>'
)
)
print_formatted_text('')
@ -583,15 +565,23 @@ async def main_with_loop(loop: asyncio.AbstractEventLoop) -> None:
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
# Only show it if:
# 1. Aliases don't exist in the shell configuration
# 2. User hasn't previously declined alias setup
# 3. We're in an interactive environment (not during tests or CI)
should_show_alias_setup = (
not aliases_exist_in_shell_config()
and not alias_setup_declined()
and sys.stdin.isatty()
)
if should_show_alias_setup:
# Clear the terminal if we haven't shown a banner yet (i.e., setup flow didn't run)
if not banner_shown:
clear()
run_alias_setup_flow(config)
banner_shown = True
# Don't set banner_shown = True here, so the ASCII art banner will still be shown
# TODO: Set working directory from config or use current working directory?
current_dir = config.workspace_base

View File

@ -277,3 +277,21 @@ 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()

View File

@ -4,12 +4,16 @@ import tempfile
from pathlib import Path
from unittest.mock import patch
from openhands.cli.main import alias_setup_declined as main_alias_setup_declined
from openhands.cli.main import aliases_exist_in_shell_config, run_alias_setup_flow
from openhands.cli.shell_config import (
ShellConfigManager,
add_aliases_to_shell_config,
aliases_exist_in_shell_config,
alias_setup_declined,
get_shell_config_path,
mark_alias_setup_declined,
)
from openhands.core.config import OpenHandsConfig
def test_get_shell_config_path_no_files_fallback():
@ -244,3 +248,121 @@ def test_shell_config_manager_template_rendering():
assert 'test-command' in content
assert 'alias openhands="test-command"' in content
assert 'alias oh="test-command"' in content
def test_alias_setup_declined_false():
"""Test alias setup declined check when marker file doesn't exist."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
assert alias_setup_declined() is False
def test_alias_setup_declined_true():
"""Test alias setup declined check when marker file exists."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Create the marker file
mark_alias_setup_declined()
assert alias_setup_declined() is True
def test_mark_alias_setup_declined():
"""Test marking alias setup as declined creates the marker file."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Initially should be False
assert alias_setup_declined() is False
# Mark as declined
mark_alias_setup_declined()
# Should now be True
assert alias_setup_declined() is True
# Verify the file exists
marker_file = Path(temp_dir) / '.openhands' / '.cli_alias_setup_declined'
assert marker_file.exists()
def test_alias_setup_declined_persisted():
"""Test that when user declines alias setup, their choice is persisted."""
config = OpenHandsConfig()
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
with patch(
'openhands.cli.shell_config.aliases_exist_in_shell_config',
return_value=False,
):
with patch(
'openhands.cli.main.cli_confirm', return_value=1
): # User chooses "No"
with patch('prompt_toolkit.print_formatted_text'):
# Initially, user hasn't declined
assert not alias_setup_declined()
# Run the alias setup flow
run_alias_setup_flow(config)
# After declining, the marker should be set
assert alias_setup_declined()
def test_alias_setup_skipped_when_previously_declined():
"""Test that alias setup is skipped when user has previously declined."""
OpenHandsConfig()
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Mark that user has previously declined
mark_alias_setup_declined()
assert alias_setup_declined()
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
with patch(
'openhands.cli.shell_config.aliases_exist_in_shell_config',
return_value=False,
):
with patch('openhands.cli.main.cli_confirm'):
with patch('prompt_toolkit.print_formatted_text'):
# This should not show the setup flow since user previously declined
# We test this by checking the main logic conditions
should_show = (
not aliases_exist_in_shell_config()
and not main_alias_setup_declined()
)
assert not should_show, (
'Alias setup should be skipped when user previously declined'
)
def test_alias_setup_accepted_does_not_set_declined_flag():
"""Test that when user accepts alias setup, no declined marker is created."""
config = OpenHandsConfig()
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
with patch(
'openhands.cli.shell_config.aliases_exist_in_shell_config',
return_value=False,
):
with patch(
'openhands.cli.main.cli_confirm', return_value=0
): # User chooses "Yes"
with patch(
'openhands.cli.shell_config.add_aliases_to_shell_config',
return_value=True,
):
with patch('prompt_toolkit.print_formatted_text'):
# Initially, user hasn't declined
assert not alias_setup_declined()
# Run the alias setup flow
run_alias_setup_flow(config)
# After accepting, the declined marker should still be False
assert not alias_setup_declined()