From 4aa94ae7cc13eba71f2b55afe7ba259025ad6805 Mon Sep 17 00:00:00 2001 From: teyrebaz33 Date: Wed, 11 Mar 2026 15:52:35 +0300 Subject: [PATCH] fix: detect non-interactive TTY in setup wizard to prevent hang MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hermes setup hung indefinitely on headless SSH sessions, Docker containers, and CI/CD environments because the interactive provider selection menu could not receive input. Two-layer fix: 1. sys.stdin.isatty() check — auto-detects non-interactive environments 2. --non-interactive flag support — already in CLI parser, now honored In both cases the wizard exits immediately with helpful guidance pointing users to 'hermes config set' commands. Closes #905 --- hermes_cli/setup.py | 22 ++++++++++ tests/hermes_cli/test_setup_noninteractive.py | 43 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 tests/hermes_cli/test_setup_noninteractive.py diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index 4f1a1c24..6eb2ce0a 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -2338,6 +2338,28 @@ def run_setup_wizard(args): config = load_config() hermes_home = get_hermes_home() + # Detect non-interactive environments (headless SSH, Docker, CI/CD) + non_interactive = getattr(args, 'non_interactive', False) + if not non_interactive and not sys.stdin.isatty(): + non_interactive = True + + if non_interactive: + print() + print(color("⚕ Hermes Setup — Non-interactive mode", Colors.CYAN, Colors.BOLD)) + print() + print_info("Running in a non-interactive environment (no TTY detected).") + print_info("The interactive wizard cannot be used here.") + print() + print_info("Configure Hermes using environment variables or config commands:") + print_info(" hermes config set model.provider custom") + print_info(" hermes config set model.base_url http://localhost:8080/v1") + print_info(" hermes config set model.default your-model-name") + print() + print_info("Or set OPENROUTER_API_KEY / OPENAI_API_KEY in your environment.") + print_info("Run 'hermes setup' in an interactive terminal to use the full wizard.") + print() + return + # Check if a specific section was requested section = getattr(args, "section", None) if section: diff --git a/tests/hermes_cli/test_setup_noninteractive.py b/tests/hermes_cli/test_setup_noninteractive.py new file mode 100644 index 00000000..724337bf --- /dev/null +++ b/tests/hermes_cli/test_setup_noninteractive.py @@ -0,0 +1,43 @@ +"""Tests for non-interactive setup wizard behavior.""" +import pytest +from unittest.mock import patch, MagicMock + + +def _make_args(**kwargs): + args = MagicMock() + args.non_interactive = kwargs.get("non_interactive", False) + args.section = kwargs.get("section", None) + args.reset = kwargs.get("reset", False) + return args + + +class TestNonInteractiveSetup: + """Verify setup wizard exits cleanly in non-interactive environments.""" + + def test_non_interactive_flag_skips_wizard(self, capsys): + """--non-interactive flag should print help and return without hanging.""" + from hermes_cli.setup import run_setup_wizard + args = _make_args(non_interactive=True) + + with patch("hermes_cli.setup.ensure_hermes_home"), \ + patch("hermes_cli.setup.load_config", return_value={}), \ + patch("hermes_cli.setup.get_hermes_home", return_value="/tmp/.hermes"): + run_setup_wizard(args) + + out = capsys.readouterr().out + assert "hermes config set" in out + + def test_no_tty_skips_wizard(self, capsys): + """When stdin has no TTY, wizard should exit with helpful message.""" + from hermes_cli.setup import run_setup_wizard + args = _make_args(non_interactive=False) + + with patch("hermes_cli.setup.ensure_hermes_home"), \ + patch("hermes_cli.setup.load_config", return_value={}), \ + patch("hermes_cli.setup.get_hermes_home", return_value="/tmp/.hermes"), \ + patch("sys.stdin") as mock_stdin: + mock_stdin.isatty.return_value = False + run_setup_wizard(args) + + out = capsys.readouterr().out + assert "hermes config set" in out