From 33926eb31554e8c1917a828d9336f9eb344faa6a Mon Sep 17 00:00:00 2001 From: xxxigm Date: Wed, 24 Jun 2026 19:06:02 +0700 Subject: [PATCH] fix(cli): honor non-interactive context in prompt_yes_no The dashboard/desktop spawn gateway actions with stdin=DEVNULL and HERMES_NONINTERACTIVE=1 (hermes_cli/web_server.py), but prompt_yes_no ignored that contract and called sys.exit(1) on the resulting EOFError. On Windows, `gateway start` asks "Install it now so the gateway starts on login? [Y/n]" when the scheduled task / startup entry is not yet installed. Spawned from the desktop app there is no stdin to answer it, so every desktop-triggered gateway restart aborted at that prompt and the gateway never started ("Gateway service is not installed"). Fall back to the prompt's default when HERMES_NONINTERACTIVE is set, and treat a bare EOFError as "accept default" rather than exiting. This lets the Windows start path proceed unattended (Startup-folder fallback + direct spawn) while interactive TTY usage is unchanged. Ctrl+C still exits. --- hermes_cli/setup.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index 6f7514f74c8..e1e787c31db 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -272,8 +272,35 @@ def prompt_choice(question: str, choices: list, default: int = 0, description: s sys.exit(1) +def is_noninteractive() -> bool: + """True when no human is available to answer a prompt. + + The dashboard/desktop spawn CLI actions with ``stdin=DEVNULL`` and + ``HERMES_NONINTERACTIVE=1`` (see ``hermes_cli/web_server.py``). In that + context an ``input()`` raises ``EOFError`` immediately, so a prompt that + aborts on EOF kills the spawned action — this is what made the desktop + "restart gateway" fail when the Windows gateway service was not yet + installed (the start path asks "Install it now?" with no one to answer). + Honour the explicit env flag here so callers fall back to their default. + """ + return os.environ.get("HERMES_NONINTERACTIVE", "").strip().lower() in { + "1", + "true", + "yes", + "on", + } + + def prompt_yes_no(question: str, default: bool = True) -> bool: - """Prompt for yes/no. Ctrl+C exits, empty input returns default.""" + """Prompt for yes/no. Ctrl+C exits, empty input returns default. + + Non-interactive callers (``HERMES_NONINTERACTIVE=1`` or a closed/redirected + stdin) have no one to answer, so fall back to ``default`` instead of + aborting the whole process. + """ + if is_noninteractive(): + return default + default_str = "Y/n" if default else "y/N" while True: @@ -283,9 +310,15 @@ def prompt_yes_no(question: str, default: bool = True) -> bool: .strip() .lower() ) - except (KeyboardInterrupt, EOFError): + except KeyboardInterrupt: print() sys.exit(1) + except EOFError: + # No stdin to read (closed/redirected, e.g. a spawned action with + # stdin=DEVNULL). Accept the default rather than exit so the caller + # can proceed unattended instead of failing the whole command. + print() + return default if not value: return default