189 lines
6.1 KiB
Python
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.")
|