2026-02-02 22:18:18 -08:00
|
|
|
"""
|
|
|
|
|
Hermes Agent Uninstaller.
|
|
|
|
|
|
|
|
|
|
Provides options for:
|
|
|
|
|
- Full uninstall: Remove everything including configs and data
|
|
|
|
|
- Keep data: Remove code but keep ~/.hermes/ (configs, sessions, logs)
|
|
|
|
|
"""
|
|
|
|
|
|
2026-04-10 17:41:33 +08:00
|
|
|
import os
|
2026-02-02 22:18:18 -08:00
|
|
|
import shutil
|
|
|
|
|
import subprocess
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
refactor: consolidate get_hermes_home() and parse_reasoning_effort() (#3062)
Centralizes two widely-duplicated patterns into hermes_constants.py:
1. get_hermes_home() — Path resolution for ~/.hermes (HERMES_HOME env var)
- Was copy-pasted inline across 30+ files as:
Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
- Now defined once in hermes_constants.py (zero-dependency module)
- hermes_cli/config.py re-exports it for backward compatibility
- Removed local wrapper functions in honcho_integration/client.py,
tools/website_policy.py, tools/tirith_security.py, hermes_cli/uninstall.py
2. parse_reasoning_effort() — Reasoning effort string validation
- Was copy-pasted in cli.py, gateway/run.py, cron/scheduler.py
- Same validation logic: check against (xhigh, high, medium, low, minimal, none)
- Now defined once in hermes_constants.py, called from all 3 locations
- Warning log for unknown values kept at call sites (context-specific)
31 files changed, net +31 lines (125 insertions, 94 deletions)
Full test suite: 6179 passed, 0 failed
2026-03-25 15:54:28 -07:00
|
|
|
from hermes_constants import get_hermes_home
|
|
|
|
|
|
2026-02-20 23:23:32 -08:00
|
|
|
from hermes_cli.colors import Colors, color
|
2026-02-02 22:18:18 -08:00
|
|
|
|
|
|
|
|
def log_info(msg: str):
|
|
|
|
|
print(f"{color('→', Colors.CYAN)} {msg}")
|
|
|
|
|
|
|
|
|
|
def log_success(msg: str):
|
|
|
|
|
print(f"{color('✓', Colors.GREEN)} {msg}")
|
|
|
|
|
|
|
|
|
|
def log_warn(msg: str):
|
|
|
|
|
print(f"{color('⚠', Colors.YELLOW)} {msg}")
|
|
|
|
|
|
|
|
|
|
def get_project_root() -> Path:
|
|
|
|
|
"""Get the project installation directory."""
|
|
|
|
|
return Path(__file__).parent.parent.resolve()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def find_shell_configs() -> list:
|
|
|
|
|
"""Find shell configuration files that might have PATH entries."""
|
|
|
|
|
home = Path.home()
|
|
|
|
|
configs = []
|
|
|
|
|
|
|
|
|
|
candidates = [
|
|
|
|
|
home / ".bashrc",
|
|
|
|
|
home / ".bash_profile",
|
|
|
|
|
home / ".profile",
|
|
|
|
|
home / ".zshrc",
|
|
|
|
|
home / ".zprofile",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for config in candidates:
|
|
|
|
|
if config.exists():
|
|
|
|
|
configs.append(config)
|
|
|
|
|
|
|
|
|
|
return configs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def remove_path_from_shell_configs():
|
|
|
|
|
"""Remove Hermes PATH entries from shell configuration files."""
|
|
|
|
|
configs = find_shell_configs()
|
|
|
|
|
removed_from = []
|
|
|
|
|
|
|
|
|
|
for config_path in configs:
|
|
|
|
|
try:
|
|
|
|
|
content = config_path.read_text()
|
|
|
|
|
original_content = content
|
|
|
|
|
|
|
|
|
|
# Remove lines containing hermes-agent or hermes PATH entries
|
|
|
|
|
new_lines = []
|
|
|
|
|
skip_next = False
|
|
|
|
|
|
|
|
|
|
for line in content.split('\n'):
|
|
|
|
|
# Skip the "# Hermes Agent" comment and following line
|
|
|
|
|
if '# Hermes Agent' in line or '# hermes-agent' in line:
|
|
|
|
|
skip_next = True
|
|
|
|
|
continue
|
|
|
|
|
if skip_next and ('hermes' in line.lower() and 'PATH' in line):
|
|
|
|
|
skip_next = False
|
|
|
|
|
continue
|
|
|
|
|
skip_next = False
|
|
|
|
|
|
|
|
|
|
# Remove any PATH line containing hermes
|
|
|
|
|
if 'hermes' in line.lower() and ('PATH=' in line or 'path=' in line.lower()):
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
new_lines.append(line)
|
|
|
|
|
|
|
|
|
|
new_content = '\n'.join(new_lines)
|
|
|
|
|
|
|
|
|
|
# Clean up multiple blank lines
|
|
|
|
|
while '\n\n\n' in new_content:
|
|
|
|
|
new_content = new_content.replace('\n\n\n', '\n\n')
|
|
|
|
|
|
|
|
|
|
if new_content != original_content:
|
|
|
|
|
config_path.write_text(new_content)
|
|
|
|
|
removed_from.append(config_path)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log_warn(f"Could not update {config_path}: {e}")
|
|
|
|
|
|
|
|
|
|
return removed_from
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def remove_wrapper_script():
|
|
|
|
|
"""Remove the hermes wrapper script if it exists."""
|
|
|
|
|
wrapper_paths = [
|
|
|
|
|
Path.home() / ".local" / "bin" / "hermes",
|
|
|
|
|
Path("/usr/local/bin/hermes"),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
removed = []
|
|
|
|
|
for wrapper in wrapper_paths:
|
|
|
|
|
if wrapper.exists():
|
|
|
|
|
try:
|
|
|
|
|
# Check if it's our wrapper (contains hermes_cli reference)
|
|
|
|
|
content = wrapper.read_text()
|
|
|
|
|
if 'hermes_cli' in content or 'hermes-agent' in content:
|
|
|
|
|
wrapper.unlink()
|
|
|
|
|
removed.append(wrapper)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log_warn(f"Could not remove {wrapper}: {e}")
|
|
|
|
|
|
|
|
|
|
return removed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def uninstall_gateway_service():
|
2026-03-22 18:20:25 -07:00
|
|
|
"""Stop and uninstall the gateway service (systemd, launchd) and kill any
|
|
|
|
|
standalone gateway processes.
|
2026-04-09 10:29:32 +02:00
|
|
|
|
2026-03-22 18:20:25 -07:00
|
|
|
Delegates to the gateway module which handles:
|
|
|
|
|
- Linux: user + system systemd services (with proper DBUS env setup)
|
|
|
|
|
- macOS: launchd plists
|
|
|
|
|
- All platforms: standalone ``hermes gateway run`` processes
|
|
|
|
|
- Termux/Android: skips systemd (no systemd on Android), still kills standalone processes
|
|
|
|
|
"""
|
|
|
|
|
import platform
|
|
|
|
|
stopped_something = False
|
2026-03-16 04:42:46 -07:00
|
|
|
|
2026-03-22 18:20:25 -07:00
|
|
|
# 1. Kill any standalone gateway processes (all platforms, including Termux)
|
2026-02-02 22:18:18 -08:00
|
|
|
try:
|
2026-03-22 18:20:25 -07:00
|
|
|
from hermes_cli.gateway import kill_gateway_processes, find_gateway_pids
|
|
|
|
|
pids = find_gateway_pids()
|
|
|
|
|
if pids:
|
|
|
|
|
killed = kill_gateway_processes()
|
|
|
|
|
if killed:
|
|
|
|
|
log_success(f"Killed {killed} running gateway process(es)")
|
|
|
|
|
stopped_something = True
|
2026-02-02 22:18:18 -08:00
|
|
|
except Exception as e:
|
2026-03-22 18:20:25 -07:00
|
|
|
log_warn(f"Could not check for gateway processes: {e}")
|
|
|
|
|
|
|
|
|
|
system = platform.system()
|
|
|
|
|
|
|
|
|
|
# Termux/Android has no systemd and no launchd — nothing left to do.
|
|
|
|
|
prefix = os.getenv("PREFIX", "")
|
|
|
|
|
is_termux = bool(os.getenv("TERMUX_VERSION") or "com.termux/files/usr" in prefix)
|
|
|
|
|
if is_termux:
|
|
|
|
|
return stopped_something
|
|
|
|
|
|
|
|
|
|
# 2. Linux: uninstall systemd services (both user and system scopes)
|
|
|
|
|
if system == "Linux":
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.gateway import (
|
|
|
|
|
get_systemd_unit_path,
|
|
|
|
|
get_service_name,
|
|
|
|
|
_systemctl_cmd,
|
|
|
|
|
)
|
|
|
|
|
svc_name = get_service_name()
|
|
|
|
|
|
|
|
|
|
for is_system in (False, True):
|
|
|
|
|
unit_path = get_systemd_unit_path(system=is_system)
|
|
|
|
|
if not unit_path.exists():
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
scope = "system" if is_system else "user"
|
|
|
|
|
try:
|
|
|
|
|
if is_system and os.geteuid() != 0:
|
|
|
|
|
log_warn(f"System gateway service exists at {unit_path} "
|
|
|
|
|
f"but needs sudo to remove")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
cmd = _systemctl_cmd(is_system)
|
|
|
|
|
subprocess.run(cmd + ["stop", svc_name],
|
|
|
|
|
capture_output=True, check=False)
|
|
|
|
|
subprocess.run(cmd + ["disable", svc_name],
|
|
|
|
|
capture_output=True, check=False)
|
|
|
|
|
unit_path.unlink()
|
|
|
|
|
subprocess.run(cmd + ["daemon-reload"],
|
|
|
|
|
capture_output=True, check=False)
|
|
|
|
|
log_success(f"Removed {scope} gateway service ({unit_path})")
|
|
|
|
|
stopped_something = True
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log_warn(f"Could not remove {scope} gateway service: {e}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log_warn(f"Could not check systemd gateway services: {e}")
|
|
|
|
|
|
|
|
|
|
# 3. macOS: uninstall launchd plist
|
|
|
|
|
elif system == "Darwin":
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.gateway import get_launchd_plist_path
|
|
|
|
|
plist_path = get_launchd_plist_path()
|
|
|
|
|
if plist_path.exists():
|
|
|
|
|
subprocess.run(["launchctl", "unload", str(plist_path)],
|
|
|
|
|
capture_output=True, check=False)
|
|
|
|
|
plist_path.unlink()
|
|
|
|
|
log_success(f"Removed macOS gateway service ({plist_path})")
|
|
|
|
|
stopped_something = True
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log_warn(f"Could not remove launchd gateway service: {e}")
|
|
|
|
|
|
|
|
|
|
return stopped_something
|
2026-02-02 22:18:18 -08:00
|
|
|
|
|
|
|
|
|
2026-04-18 19:14:55 -07:00
|
|
|
def _is_default_hermes_home(hermes_home: Path) -> bool:
|
|
|
|
|
"""Return True when ``hermes_home`` points at the default (non-profile) root."""
|
|
|
|
|
try:
|
|
|
|
|
from hermes_constants import get_default_hermes_root
|
|
|
|
|
return hermes_home.resolve() == get_default_hermes_root().resolve()
|
|
|
|
|
except Exception:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _discover_named_profiles():
|
|
|
|
|
"""Return a list of ``ProfileInfo`` for every non-default profile, or ``[]``
|
|
|
|
|
if profile support is unavailable or nothing is installed beyond the
|
|
|
|
|
default root."""
|
|
|
|
|
try:
|
|
|
|
|
from hermes_cli.profiles import list_profiles
|
|
|
|
|
except Exception:
|
|
|
|
|
return []
|
|
|
|
|
try:
|
|
|
|
|
return [p for p in list_profiles() if not getattr(p, "is_default", False)]
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log_warn(f"Could not enumerate profiles: {e}")
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _uninstall_profile(profile) -> None:
|
|
|
|
|
"""Fully uninstall a single named profile: stop its gateway service,
|
|
|
|
|
remove its alias wrapper, and wipe its HERMES_HOME directory.
|
|
|
|
|
|
|
|
|
|
We shell out to ``hermes -p <name> gateway stop|uninstall`` because
|
|
|
|
|
service names, unit paths, and plist paths are all derived from the
|
|
|
|
|
current HERMES_HOME and can't be easily switched in-process.
|
|
|
|
|
"""
|
|
|
|
|
import sys as _sys
|
|
|
|
|
name = profile.name
|
|
|
|
|
profile_home = profile.path
|
|
|
|
|
|
|
|
|
|
log_info(f"Uninstalling profile '{name}'...")
|
|
|
|
|
|
|
|
|
|
# 1. Stop and remove this profile's gateway service.
|
|
|
|
|
# Use `python -m hermes_cli.main` so we don't depend on a `hermes`
|
|
|
|
|
# wrapper that may be half-removed mid-uninstall.
|
|
|
|
|
hermes_invocation = [_sys.executable, "-m", "hermes_cli.main", "--profile", name]
|
|
|
|
|
for subcmd in ("stop", "uninstall"):
|
|
|
|
|
try:
|
|
|
|
|
subprocess.run(
|
|
|
|
|
hermes_invocation + ["gateway", subcmd],
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
timeout=60,
|
|
|
|
|
check=False,
|
|
|
|
|
)
|
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
|
log_warn(f" Gateway {subcmd} timed out for '{name}'")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log_warn(f" Could not run gateway {subcmd} for '{name}': {e}")
|
|
|
|
|
|
|
|
|
|
# 2. Remove the wrapper alias script at ~/.local/bin/<name> (if any).
|
|
|
|
|
alias_path = getattr(profile, "alias_path", None)
|
|
|
|
|
if alias_path and alias_path.exists():
|
|
|
|
|
try:
|
|
|
|
|
alias_path.unlink()
|
|
|
|
|
log_success(f" Removed alias {alias_path}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log_warn(f" Could not remove alias {alias_path}: {e}")
|
|
|
|
|
|
|
|
|
|
# 3. Wipe the profile's HERMES_HOME directory.
|
|
|
|
|
try:
|
|
|
|
|
if profile_home.exists():
|
|
|
|
|
shutil.rmtree(profile_home)
|
|
|
|
|
log_success(f" Removed {profile_home}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log_warn(f" Could not remove {profile_home}: {e}")
|
|
|
|
|
|
|
|
|
|
|
2026-02-02 22:18:18 -08:00
|
|
|
def run_uninstall(args):
|
|
|
|
|
"""
|
|
|
|
|
Run the uninstall process.
|
|
|
|
|
|
|
|
|
|
Options:
|
|
|
|
|
- Full uninstall: removes code + ~/.hermes/ (configs, data, logs)
|
|
|
|
|
- Keep data: removes code but keeps ~/.hermes/ for future reinstall
|
|
|
|
|
"""
|
|
|
|
|
project_root = get_project_root()
|
|
|
|
|
hermes_home = get_hermes_home()
|
2026-04-18 19:14:55 -07:00
|
|
|
|
|
|
|
|
# Detect named profiles when uninstalling from the default root —
|
|
|
|
|
# offer to clean them up too instead of leaving zombie HERMES_HOMEs
|
|
|
|
|
# and systemd units behind.
|
|
|
|
|
is_default_profile = _is_default_hermes_home(hermes_home)
|
|
|
|
|
named_profiles = _discover_named_profiles() if is_default_profile else []
|
|
|
|
|
|
2026-02-02 22:18:18 -08:00
|
|
|
print()
|
|
|
|
|
print(color("┌─────────────────────────────────────────────────────────┐", Colors.MAGENTA, Colors.BOLD))
|
2026-02-20 21:25:04 -08:00
|
|
|
print(color("│ ⚕ Hermes Agent Uninstaller │", Colors.MAGENTA, Colors.BOLD))
|
2026-02-02 22:18:18 -08:00
|
|
|
print(color("└─────────────────────────────────────────────────────────┘", Colors.MAGENTA, Colors.BOLD))
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
# Show what will be affected
|
|
|
|
|
print(color("Current Installation:", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
print(f" Code: {project_root}")
|
|
|
|
|
print(f" Config: {hermes_home / 'config.yaml'}")
|
|
|
|
|
print(f" Secrets: {hermes_home / '.env'}")
|
|
|
|
|
print(f" Data: {hermes_home / 'cron/'}, {hermes_home / 'sessions/'}, {hermes_home / 'logs/'}")
|
|
|
|
|
print()
|
2026-04-18 19:14:55 -07:00
|
|
|
|
|
|
|
|
if named_profiles:
|
|
|
|
|
print(color("Other profiles detected:", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
for p in named_profiles:
|
|
|
|
|
running = " (gateway running)" if getattr(p, "gateway_running", False) else ""
|
|
|
|
|
print(f" • {p.name}{running}: {p.path}")
|
|
|
|
|
print()
|
2026-02-02 22:18:18 -08:00
|
|
|
|
|
|
|
|
# Ask for confirmation
|
|
|
|
|
print(color("Uninstall Options:", Colors.YELLOW, Colors.BOLD))
|
|
|
|
|
print()
|
|
|
|
|
print(" 1) " + color("Keep data", Colors.GREEN) + " - Remove code only, keep configs/sessions/logs")
|
|
|
|
|
print(" (Recommended - you can reinstall later with your settings intact)")
|
|
|
|
|
print()
|
|
|
|
|
print(" 2) " + color("Full uninstall", Colors.RED) + " - Remove everything including all data")
|
|
|
|
|
print(" (Warning: This deletes all configs, sessions, and logs permanently)")
|
|
|
|
|
print()
|
|
|
|
|
print(" 3) " + color("Cancel", Colors.CYAN) + " - Don't uninstall")
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
choice = input(color("Select option [1/2/3]: ", Colors.BOLD)).strip()
|
|
|
|
|
except (KeyboardInterrupt, EOFError):
|
|
|
|
|
print()
|
|
|
|
|
print("Cancelled.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if choice == "3" or choice.lower() in ("c", "cancel", "q", "quit", "n", "no"):
|
|
|
|
|
print()
|
|
|
|
|
print("Uninstall cancelled.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
full_uninstall = (choice == "2")
|
2026-04-18 19:14:55 -07:00
|
|
|
|
|
|
|
|
# When doing a full uninstall from the default profile, also offer to
|
|
|
|
|
# remove any named profiles — stopping their gateway services, unlinking
|
|
|
|
|
# their alias wrappers, and wiping their HERMES_HOME dirs. Otherwise
|
|
|
|
|
# those leave zombie services and data behind.
|
|
|
|
|
remove_profiles = False
|
|
|
|
|
if full_uninstall and named_profiles:
|
|
|
|
|
print()
|
|
|
|
|
print(color("Other profiles will NOT be removed by default.", Colors.YELLOW))
|
|
|
|
|
print(f"Found {len(named_profiles)} named profile(s): " +
|
|
|
|
|
", ".join(p.name for p in named_profiles))
|
|
|
|
|
print()
|
|
|
|
|
try:
|
|
|
|
|
resp = input(color(
|
|
|
|
|
f"Also stop and remove these {len(named_profiles)} profile(s)? [y/N]: ",
|
|
|
|
|
Colors.BOLD
|
|
|
|
|
)).strip().lower()
|
|
|
|
|
except (KeyboardInterrupt, EOFError):
|
|
|
|
|
print()
|
|
|
|
|
print("Cancelled.")
|
|
|
|
|
return
|
|
|
|
|
remove_profiles = resp in ("y", "yes")
|
|
|
|
|
|
2026-02-02 22:18:18 -08:00
|
|
|
# Final confirmation
|
|
|
|
|
print()
|
|
|
|
|
if full_uninstall:
|
|
|
|
|
print(color("⚠️ WARNING: This will permanently delete ALL Hermes data!", Colors.RED, Colors.BOLD))
|
|
|
|
|
print(color(" Including: configs, API keys, sessions, scheduled jobs, logs", Colors.RED))
|
2026-04-18 19:14:55 -07:00
|
|
|
if remove_profiles:
|
|
|
|
|
print(color(
|
|
|
|
|
f" Plus {len(named_profiles)} profile(s): " +
|
|
|
|
|
", ".join(p.name for p in named_profiles),
|
|
|
|
|
Colors.RED
|
|
|
|
|
))
|
2026-02-02 22:18:18 -08:00
|
|
|
else:
|
|
|
|
|
print("This will remove the Hermes code but keep your configuration and data.")
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
try:
|
|
|
|
|
confirm = input(f"Type '{color('yes', Colors.YELLOW)}' to confirm: ").strip().lower()
|
|
|
|
|
except (KeyboardInterrupt, EOFError):
|
|
|
|
|
print()
|
|
|
|
|
print("Cancelled.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if confirm != "yes":
|
|
|
|
|
print()
|
|
|
|
|
print("Uninstall cancelled.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
print(color("Uninstalling...", Colors.CYAN, Colors.BOLD))
|
|
|
|
|
print()
|
|
|
|
|
|
2026-03-22 18:20:25 -07:00
|
|
|
# 1. Stop and uninstall gateway service + kill standalone processes
|
|
|
|
|
log_info("Checking for running gateway...")
|
|
|
|
|
if not uninstall_gateway_service():
|
|
|
|
|
log_info("No gateway service or processes found")
|
2026-02-02 22:18:18 -08:00
|
|
|
|
|
|
|
|
# 2. Remove PATH entries from shell configs
|
|
|
|
|
log_info("Removing PATH entries from shell configs...")
|
|
|
|
|
removed_configs = remove_path_from_shell_configs()
|
|
|
|
|
if removed_configs:
|
|
|
|
|
for config in removed_configs:
|
|
|
|
|
log_success(f"Updated {config}")
|
|
|
|
|
else:
|
|
|
|
|
log_info("No PATH entries found to remove")
|
|
|
|
|
|
|
|
|
|
# 3. Remove wrapper script
|
|
|
|
|
log_info("Removing hermes command...")
|
|
|
|
|
removed_wrappers = remove_wrapper_script()
|
|
|
|
|
if removed_wrappers:
|
|
|
|
|
for wrapper in removed_wrappers:
|
|
|
|
|
log_success(f"Removed {wrapper}")
|
|
|
|
|
else:
|
|
|
|
|
log_info("No wrapper script found")
|
|
|
|
|
|
|
|
|
|
# 4. Remove installation directory (code)
|
chore: fix 154 f-strings, simplify getattr/URL patterns, remove dead code (#3119)
Three categories of cleanup, all zero-behavioral-change:
1. F-strings without placeholders (154 fixes across 29 files)
- Converted f'...' to '...' where no {expression} was present
- Heaviest files: run_agent.py (24), cli.py (20), honcho_integration/cli.py (34)
2. Simplify defensive patterns in run_agent.py
- Added explicit self._is_anthropic_oauth = False in __init__ (before
the api_mode branch that conditionally sets it)
- Replaced 7x getattr(self, '_is_anthropic_oauth', False) with direct
self._is_anthropic_oauth (attribute always initialized now)
- Added _is_openrouter_url() and _is_anthropic_url() helper methods
- Replaced 3 inline 'openrouter' in self._base_url_lower checks
3. Remove dead code in small files
- hermes_cli/claw.py: removed unused 'total' computation
- tools/fuzzy_match.py: removed unused strip_indent() function and
pattern_stripped variable
Full test suite: 6184 passed, 0 failures
E2E PTY: banner clean, tool calls work, zero garbled ANSI
2026-03-25 19:47:58 -07:00
|
|
|
log_info("Removing installation directory...")
|
2026-02-02 22:18:18 -08:00
|
|
|
|
|
|
|
|
# Check if we're running from within the install dir
|
|
|
|
|
# We need to be careful here
|
|
|
|
|
try:
|
|
|
|
|
if project_root.exists():
|
|
|
|
|
# If the install is inside ~/.hermes/, just remove the hermes-agent subdir
|
|
|
|
|
if hermes_home in project_root.parents or project_root.parent == hermes_home:
|
|
|
|
|
shutil.rmtree(project_root)
|
|
|
|
|
log_success(f"Removed {project_root}")
|
|
|
|
|
else:
|
|
|
|
|
# Installation is somewhere else entirely
|
|
|
|
|
shutil.rmtree(project_root)
|
|
|
|
|
log_success(f"Removed {project_root}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log_warn(f"Could not fully remove {project_root}: {e}")
|
|
|
|
|
log_info("You may need to manually remove it")
|
|
|
|
|
|
2026-04-18 19:14:55 -07:00
|
|
|
# 5. Optionally remove ~/.hermes/ data directory (and named profiles)
|
2026-02-02 22:18:18 -08:00
|
|
|
if full_uninstall:
|
2026-04-18 19:14:55 -07:00
|
|
|
# 5a. Stop and remove each named profile's gateway service and
|
|
|
|
|
# alias wrapper. The profile HERMES_HOME dirs live under
|
|
|
|
|
# ``<default>/profiles/<name>/`` and will be swept away by the
|
|
|
|
|
# rmtree below, but services + alias scripts live OUTSIDE the
|
|
|
|
|
# default root and have to be cleaned up explicitly.
|
|
|
|
|
if remove_profiles and named_profiles:
|
|
|
|
|
for prof in named_profiles:
|
|
|
|
|
_uninstall_profile(prof)
|
|
|
|
|
|
2026-02-02 22:18:18 -08:00
|
|
|
log_info("Removing configuration and data...")
|
|
|
|
|
try:
|
|
|
|
|
if hermes_home.exists():
|
|
|
|
|
shutil.rmtree(hermes_home)
|
|
|
|
|
log_success(f"Removed {hermes_home}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log_warn(f"Could not fully remove {hermes_home}: {e}")
|
|
|
|
|
log_info("You may need to manually remove it")
|
|
|
|
|
else:
|
|
|
|
|
log_info(f"Keeping configuration and data in {hermes_home}")
|
|
|
|
|
|
|
|
|
|
# Done
|
|
|
|
|
print()
|
|
|
|
|
print(color("┌─────────────────────────────────────────────────────────┐", Colors.GREEN, Colors.BOLD))
|
|
|
|
|
print(color("│ ✓ Uninstall Complete! │", Colors.GREEN, Colors.BOLD))
|
|
|
|
|
print(color("└─────────────────────────────────────────────────────────┘", Colors.GREEN, Colors.BOLD))
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
if not full_uninstall:
|
|
|
|
|
print(color("Your configuration and data have been preserved:", Colors.CYAN))
|
|
|
|
|
print(f" {hermes_home}/")
|
|
|
|
|
print()
|
|
|
|
|
print("To reinstall later with your existing settings:")
|
|
|
|
|
print(color(" curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash", Colors.DIM))
|
|
|
|
|
print()
|
|
|
|
|
|
|
|
|
|
print(color("Reload your shell to complete the process:", Colors.YELLOW))
|
|
|
|
|
print(" source ~/.bashrc # or ~/.zshrc")
|
|
|
|
|
print()
|
2026-02-20 21:25:04 -08:00
|
|
|
print("Thank you for using Hermes Agent! ⚕")
|
2026-02-02 22:18:18 -08:00
|
|
|
print()
|