hermes-agent/hermes_cli/relaunch.py
ethernet 95f2802f84 feat(cli): preserve --tui and other flags across internal relaunches
Extract all os.execvp('hermes', ...) calls into a utility so flags like
--tui, --dev, --profile, --model, --provider, et al. survive session
resume and post-setup relaunch.

- resolve_hermes_bin: prefers sys.argv[0] when callable, then PATH,
  then falls back to '${sys.executable} -m hermes_cli.main' (fixes nix
run relaunches)
- build_relaunch_argv: allowlists critical flags so they carry over
- cmd_sessions browse now calls relaunch(['--resume', <id>])
- _apply_profile_override skips redundant work when HERMES_HOME is
  already set (child inherits parent profile)
- setup.py replaces _resolve_hermes_chat_argv with relaunch_chat()
- added comprehensive tests for flag extraction and binary resolution

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 20:33:29 -07:00

147 lines
4.2 KiB
Python

"""
Unified self-relaunch for Hermes CLI.
Preserves critical flags (--tui, --dev, --profile, --model, etc.) across
process replacement so that ``hermes sessions browse`` or post-setup relaunch
doesn't silently drop the user's UI mode or other preferences.
Also works when ``hermes`` is not on PATH (e.g. ``nix run`` or ``python -m``).
"""
import os
import shutil
import sys
from typing import Optional, Sequence
# (option_string, takes_value) — flags whose presence (and value, where
# applicable) on the original argv must survive a self-relaunch.
_CRITICAL_FLAGS: list[tuple[str, bool]] = [
("--tui", False),
("--dev", False),
("--profile", True),
("-p", True),
("--model", True),
("-m", True),
("--provider", True),
("--yolo", False),
("--ignore-user-config", False),
("--ignore-rules", False),
("--pass-session-id", False),
("--accept-hooks", False),
("--worktree", False),
("-w", False),
("--skills", True),
("-s", True),
("--quiet", False),
("-Q", False),
("--verbose", False),
("-v", False),
("--source", True),
]
def _extract_critical_flags(argv: Sequence[str]) -> list[str]:
"""Pull out flags that affect session behaviour / UI mode."""
flags: list[str] = []
i = 0
while i < len(argv):
arg = argv[i]
if "=" in arg:
key = arg.split("=", 1)[0]
for flag, _ in _CRITICAL_FLAGS:
if key == flag:
flags.append(arg)
break
i += 1
continue
for flag, takes_value in _CRITICAL_FLAGS:
if arg == flag:
flags.append(arg)
if takes_value and i + 1 < len(argv) and not argv[i + 1].startswith("-"):
flags.append(argv[i + 1])
i += 1
break
i += 1
return flags
def resolve_hermes_bin() -> Optional[str]:
"""Find the hermes entry point.
Priority:
1. ``sys.argv[0]`` if it resolves to a real executable.
2. ``shutil.which("hermes")`` on PATH.
3. ``None`` → caller should fall back to ``python -m hermes_cli.main``.
"""
argv0 = sys.argv[0]
# Absolute path to an executable (covers nix store, venv wrappers, etc.)
if os.path.isabs(argv0) and os.path.isfile(argv0) and os.access(argv0, os.X_OK):
return argv0
# Relative path — resolve against CWD
if not argv0.startswith("-") and os.path.isfile(argv0):
abs_path = os.path.abspath(argv0)
if os.access(abs_path, os.X_OK):
return abs_path
# PATH lookup
path_bin = shutil.which("hermes")
if path_bin:
return path_bin
return None
def build_relaunch_argv(
extra_args: Sequence[str],
*,
preserve_critical: bool = True,
original_argv: Optional[Sequence[str]] = None,
) -> list[str]:
"""Construct an argv list for replacing the current process with hermes.
Args:
extra_args: Arguments to append (e.g. ``["--resume", id]``).
preserve_critical: Whether to carry over UI / behaviour flags.
original_argv: The original argv to scan for flags (defaults to
``sys.argv[1:]``).
"""
bin_path = resolve_hermes_bin()
if bin_path:
argv = [bin_path]
else:
argv = [sys.executable, "-m", "hermes_cli.main"]
src = list(original_argv) if original_argv is not None else list(sys.argv[1:])
if preserve_critical:
argv.extend(_extract_critical_flags(src))
argv.extend(extra_args)
return argv
def relaunch(
extra_args: Sequence[str],
*,
preserve_critical: bool = True,
original_argv: Optional[Sequence[str]] = None,
) -> None:
"""Replace the current process with a fresh hermes invocation."""
new_argv = build_relaunch_argv(
extra_args, preserve_critical=preserve_critical, original_argv=original_argv
)
os.execvp(new_argv[0], new_argv)
def relaunch_chat(
*,
preserve_critical: bool = True,
original_argv: Optional[Sequence[str]] = None,
) -> None:
"""Convenience wrapper: relaunch into ``hermes chat``."""
relaunch(["chat"], preserve_critical=preserve_critical, original_argv=original_argv)