fix(gateway): honor --start-now/--start-on-login flags and support non-TTY headless installs

When running `hermes gateway install` on Linux/systemd, the command
unconditionally prompts with two `prompt_yes_no` questions, breaking
headless installs (SSH, CI, provisioning scripts) and ignoring the
existing --start-now / --start-on-login CLI flags that the Windows
branch already respects.

The fix mirrors the Windows path: read CLI flags first, prompt only
when flags are not provided AND stdin is a TTY, and fall back to True
defaults for non-TTY contexts. The argparse help strings are promoted
from SUPPRESS to visible so users can discover the flags.

Fixes #42065
This commit is contained in:
liuhao1024 2026-06-08 20:08:06 +08:00 committed by Teknium
parent 5e7bca95d9
commit 831d443b03
3 changed files with 105 additions and 6 deletions

View file

@ -6268,8 +6268,24 @@ def _gateway_command_inner(args):
" Or use tmux/screen for persistence: tmux new -s hermes 'hermes gateway run'"
)
print()
start_now = prompt_yes_no("Start the gateway now after installing the service?", True)
start_on_login = prompt_yes_no("Start the gateway automatically on login/boot with systemd?", True)
# Honor CLI flags (--start-now / --no-start-now, --start-on-login /
# --no-start-on-login). When not provided, prompt interactively or
# fall back to True for non-TTY / headless contexts (SSH, CI, pipes).
_sn = getattr(args, "start_now", None)
if _sn is not None:
start_now = _sn
elif sys.stdin.isatty():
start_now = prompt_yes_no("Start the gateway now after installing the service?", True)
else:
start_now = True
_sol = getattr(args, "start_on_login", None)
if _sol is not None:
start_on_login = _sol
elif sys.stdin.isatty():
start_on_login = prompt_yes_no("Start the gateway automatically on login/boot with systemd?", True)
else:
start_on_login = True
systemd_install(
force=force,
system=system,

View file

@ -169,26 +169,26 @@ def build_gateway_parser(
dest="start_now",
action="store_true",
default=None,
help=argparse.SUPPRESS,
help="Start the gateway service immediately after installing",
)
gateway_install.add_argument(
"--no-start-now",
dest="start_now",
action="store_false",
help=argparse.SUPPRESS,
help="Do not start the gateway service after installing",
)
gateway_install.add_argument(
"--start-on-login",
dest="start_on_login",
action="store_true",
default=None,
help=argparse.SUPPRESS,
help="Enable the service to start automatically on login/boot",
)
gateway_install.add_argument(
"--no-start-on-login",
dest="start_on_login",
action="store_false",
help=argparse.SUPPRESS,
help="Do not enable the service to start on login/boot",
)
gateway_install.add_argument(
"--elevated-handoff",

View file

@ -444,6 +444,7 @@ def test_gateway_install_in_container_with_operational_systemd_uses_systemd(monk
monkeypatch.setattr(gateway, "is_wsl", lambda: False)
monkeypatch.setattr(gateway, "is_macos", lambda: False)
monkeypatch.setattr(gateway, "is_managed", lambda: False)
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
calls = []
monkeypatch.setattr(gateway, "prompt_yes_no", lambda question, default=True: calls.append(("prompt", question, default)) or True)
@ -796,6 +797,7 @@ def test_gateway_install_can_decline_start_now_and_startup(monkeypatch):
monkeypatch.setattr(gateway, "is_wsl", lambda: False)
monkeypatch.setattr(gateway, "is_macos", lambda: False)
monkeypatch.setattr(gateway, "is_managed", lambda: False)
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
answers = iter([False, False])
calls = []
@ -817,6 +819,87 @@ def test_gateway_install_can_decline_start_now_and_startup(monkeypatch):
]
def test_gateway_install_systemd_honors_start_now_flag(monkeypatch):
"""--start-now / --no-start-now should bypass the interactive prompt."""
monkeypatch.setattr(gateway, "supports_systemd_services", lambda: True)
monkeypatch.setattr(gateway, "is_wsl", lambda: False)
monkeypatch.setattr(gateway, "is_macos", lambda: False)
monkeypatch.setattr(gateway, "is_managed", lambda: False)
calls = []
monkeypatch.setattr(gateway, "prompt_yes_no", lambda question, default=True: calls.append(("prompt", question)))
monkeypatch.setattr(
gateway,
"systemd_install",
lambda force=False, system=False, run_as_user=None, enable_on_startup=True: calls.append(("install", enable_on_startup)),
)
monkeypatch.setattr(gateway, "systemd_start", lambda system=False: calls.append(("start",)))
args = SimpleNamespace(
gateway_command="install", force=False, system=False,
run_as_user=None, start_now=True, start_on_login=False,
)
gateway.gateway_command(args)
assert ("prompt", "Start the gateway now after installing the service?") not in calls
assert ("start",) in calls
assert ("install", False) in calls
def test_gateway_install_systemd_non_tty_uses_defaults(monkeypatch):
"""Non-TTY stdin (headless/CI) should use True defaults without prompting."""
monkeypatch.setattr(gateway, "supports_systemd_services", lambda: True)
monkeypatch.setattr(gateway, "is_wsl", lambda: False)
monkeypatch.setattr(gateway, "is_macos", lambda: False)
monkeypatch.setattr(gateway, "is_managed", lambda: False)
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
calls = []
monkeypatch.setattr(gateway, "prompt_yes_no", lambda question, default=True: calls.append(("prompt", question)))
monkeypatch.setattr(
gateway,
"systemd_install",
lambda force=False, system=False, run_as_user=None, enable_on_startup=True: calls.append(("install", enable_on_startup)),
)
monkeypatch.setattr(gateway, "systemd_start", lambda system=False: calls.append(("start",)))
args = SimpleNamespace(gateway_command="install", force=False, system=False, run_as_user=None)
gateway.gateway_command(args)
# No prompts — defaults used (start_now=True, start_on_login=True)
assert all(c[0] != "prompt" for c in calls)
assert ("install", True) in calls
assert ("start",) in calls
def test_gateway_install_systemd_no_start_now_flag_non_tty(monkeypatch):
"""--no-start-now in non-TTY should skip starting the service."""
monkeypatch.setattr(gateway, "supports_systemd_services", lambda: True)
monkeypatch.setattr(gateway, "is_wsl", lambda: False)
monkeypatch.setattr(gateway, "is_macos", lambda: False)
monkeypatch.setattr(gateway, "is_managed", lambda: False)
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
calls = []
monkeypatch.setattr(gateway, "prompt_yes_no", lambda question, default=True: calls.append(("prompt", question)))
monkeypatch.setattr(
gateway,
"systemd_install",
lambda force=False, system=False, run_as_user=None, enable_on_startup=True: calls.append(("install", enable_on_startup)),
)
monkeypatch.setattr(gateway, "systemd_start", lambda system=False: calls.append(("start",)))
args = SimpleNamespace(
gateway_command="install", force=False, system=False,
run_as_user=None, start_now=False, start_on_login=True,
)
gateway.gateway_command(args)
assert all(c[0] != "prompt" for c in calls)
assert ("install", True) in calls
assert ("start",) not in calls
def test_find_gateway_pids_falls_back_to_pid_file_when_process_scan_fails(monkeypatch):
monkeypatch.setattr(gateway, "_get_service_pids", lambda: set())
monkeypatch.setattr(gateway, "is_windows", lambda: False)