Files
myagents/myagents/commands/uninstall.py
T
Zhengshou Lai 3d40e27a93 Remove MetaBot coupling from uninstall and workspace init.
Keep silent cleanup of legacy metabot stubs; drop METABOT_HOME symlink.
2026-08-11 17:23:23 +08:00

189 lines
6.1 KiB
Python

"""``uninstall`` — remove CLI entry points; keep config/workspace by default.
Also removes leftover legacy stubs (metabot / mb / mm / doubao-tts) if present.
``--all`` deletes ~/.xiaohe, ~/.mytoolkit and leftover ~/.metabot;
workspace is never touched.
"""
import os
import shutil
import subprocess
import sys
from pathlib import Path
import click
from rich.console import Console
stderr_console = Console(stderr=True)
console = Console()
BIN_NAMES = (
"xiaohe",
"myagents",
"myclaude",
"mykimi",
"mycodex",
"myhermes",
"mycursor",
"mytoolkit",
)
LEGACY_BIN_NAMES = (
"metabot",
"mb",
"mm",
"doubao-tts",
)
LEGACY_COMPLETIONS = (
"mb",
"mm",
"metabot",
"doubao-tts",
"_mb",
"_mm",
"_metabot",
"_doubao-tts",
)
PY_PACKAGES = ("mytoolkit", "myagents")
def _launcher_paths(home: Path) -> list[Path]:
apps = home / ".local" / "share" / "applications"
icon = (
home / ".local" / "share" / "icons" / "hicolor" / "512x512"
/ "apps" / "xiaohe.png"
)
return [
home / "Desktop" / "Xiaohe Agent.app",
home / "Desktop" / "XiaoheAgent.app",
home / "Desktop" / "XiaoheAgent.command",
apps / "Xiaohe Agent.desktop",
apps / "XiaoheAgent.desktop",
icon,
home / ".local" / "share" / "icons" / "hicolor" / "512x512"
/ "apps" / "xiaohe.png",
]
def _remove_path(path: Path, removed: list[Path]) -> None:
if path.is_symlink() or path.is_file():
path.unlink()
removed.append(path)
elif path.is_dir():
shutil.rmtree(path)
removed.append(path)
def _pip_uninstall() -> list[str]:
"""Uninstall the pip packages (removes their pip-owned entry points)."""
import sysconfig
if os.environ.get("VIRTUAL_ENV") and shutil.which("uv"):
base = ["uv", "pip", "uninstall"]
else:
base = [sys.executable, "-m", "pip", "uninstall"]
stdlib = Path(sysconfig.get_path("stdlib"))
if (stdlib / "EXTERNALLY-MANAGED").exists():
base.append("--break-system-packages")
base.append("-y")
warnings: list[str] = []
for pkg in PY_PACKAGES:
result = subprocess.run([*base, pkg], capture_output=True, text=True)
if result.returncode != 0:
tail = (result.stderr or "").strip()[:120]
warnings.append(f"pip uninstall {pkg} failed: {tail}")
return warnings
@click.command("uninstall")
@click.option("--all", "remove_all", is_flag=True,
help="Also remove ~/.xiaohe (settings, API keys)")
@click.option("--yes", is_flag=True,
help="Skip the first confirmation (--all still requires the full chain)")
def uninstall_cmd(remove_all: bool, yes: bool) -> None:
"""Remove the xiaohe CLI layer; keeps ~/.xiaohe and the workspace."""
home = Path.home()
from myagents.commands.completion_install import uninstall_completions
from myagents.entrypoints import _progs
from myagents.project_root import get_workspace_root
console.print("[bold]Will remove:[/bold]")
console.print(
" - CLI entry points: xiaohe, myclaude, mykimi, mycodex, myhermes,"
)
console.print(" mycursor, myagents, mytoolkit")
console.print(
" - legacy leftovers if present: metabot, mb, mm, doubao-tts"
)
console.print(" - shell completions for the above")
console.print(" - desktop launcher (Xiaohe Agent.app / Xiaohe Agent.desktop)")
console.print(" - pip packages: mytoolkit, myagents")
if remove_all:
console.print(" - [red]~/.xiaohe (settings, secrets, keys)[/red]")
console.print(" - [red]~/.mytoolkit (mytoolkit config, incl. keys)[/red]")
console.print(
" - [red]~/.metabot (legacy leftover, if present)[/red]"
)
console.print("[bold]Will keep:[/bold]")
if not remove_all:
console.print(" - ~/.xiaohe (settings, keys)")
console.print(" - ~/.mytoolkit (tool configs)")
console.print(f" - workspace: {get_workspace_root(create=False)}")
console.print(" - rc-file edits (PATH line, ANTHROPIC_* exports) and the claude CLI")
if not yes and not click.confirm("Proceed with uninstall?", default=False, err=True):
console.print("Cancelled.")
return
if remove_all:
# Deleting these dirs destroys settings and every saved credential —
# always require the full confirmation chain, even with --yes.
if not click.confirm(
"This also deletes ~/.xiaohe, ~/.mytoolkit and any leftover "
"~/.metabot — config and ALL saved API keys. Continue?",
default=False,
err=True,
):
console.print("Cancelled.")
return
phrase = click.prompt(
'Final confirmation — type "delete-all" to proceed', err=True
)
if phrase.strip() != "delete-all":
console.print("Cancelled (phrase did not match).")
return
removed: list[Path] = []
# Shell completions first — needs myagents still importable.
removed += uninstall_completions(_progs())
# Entry-point scripts in ~/.local/bin (defensive: pip removes its own).
local_bin = home / ".local" / "bin"
for name in (*BIN_NAMES, *LEGACY_BIN_NAMES):
_remove_path(local_bin / name, removed)
for name in LEGACY_COMPLETIONS:
_remove_path(local_bin / "completions" / name, removed)
comp_dir = local_bin / "completions"
if comp_dir.is_dir() and not any(comp_dir.iterdir()):
comp_dir.rmdir()
for path in _launcher_paths(home):
_remove_path(path, removed)
warnings = _pip_uninstall()
if remove_all:
_remove_path(home / ".xiaohe", removed)
_remove_path(home / ".mytoolkit", removed)
_remove_path(home / ".metabot", removed)
for warning in warnings:
stderr_console.print(f"[yellow]warning: {warning}[/yellow]")
console.print(
f"[green]Removed {len(removed)} items.[/green] "
"Open a new terminal to pick up the change."
)
if remove_all:
console.print("Kept: your workspace and rc-file edits.")
else:
console.print("Kept: ~/.xiaohe, your workspace, rc-file edits.")