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.
This commit is contained in:
xxxigm 2026-06-24 19:06:02 +07:00 committed by kshitij
parent 8446c15706
commit 33926eb315

View file

@ -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