"""Self-update: refresh the installed CLI from the project source tree. The project is distributed as a source directory (no PyPI release yet), so "updating" means: find the source tree, re-install it with the *same* interpreter that is running us, and rehash pyenv shims if pyenv is in use. The source directory is remembered in ``~/.tripo/config.json`` (``install_source``) the first time it is discovered, so later runs only need ``tripocli update``. """ import json import os import shutil import subprocess import sys from pathlib import Path from . import __version__ INSTALL_SOURCE_KEY = "install_source" def _running_package_dir(): """Directory of the ``tripo`` package that is currently imported.""" return Path(__file__).resolve().parent def _is_site_packages(path): parts = {p.lower() for p in Path(path).parts} return bool(parts & {"site-packages", "dist-packages"}) def detect_source(explicit=None, config=None): """Locate the source tree to install from. Order: ``--from`` > remembered ``install_source`` > the tree we were imported from (only when that is not site-packages). """ if explicit: candidate = Path(explicit).expanduser().resolve() if not (candidate / "pyproject.toml").exists(): raise ValueError(f"{candidate} 看起来不是项目目录(缺少 pyproject.toml)") return candidate if config: remembered = config.data.get(INSTALL_SOURCE_KEY) if remembered and (Path(remembered) / "pyproject.toml").exists(): return Path(remembered) package_dir = _running_package_dir() if not _is_site_packages(package_dir): root = package_dir.parent if (root / "pyproject.toml").exists(): return root return None def installed_version(): """Version of the copy in site-packages (what other directories will run).""" try: out = subprocess.run( [sys.executable, "-m", "pip", "show", "tripocli"], capture_output=True, text=True, timeout=60, ) except (OSError, subprocess.SubprocessError): return None for line in out.stdout.splitlines(): if line.lower().startswith("version:"): return line.split(":", 1)[1].strip() return None def _has_pyenv(): return bool(shutil.which("pyenv") or os.environ.get("PYENV_ROOT")) def update(source, rehash=True, dry_run=False): """Re-install ``source`` with the running interpreter. Returns a report dict.""" source = Path(source) steps = [] command = [ sys.executable, "-m", "pip", "install", "--no-build-isolation", "--no-deps", "--upgrade", "--force-reinstall", str(source), ] if dry_run: planned = [" ".join(command)] if rehash and _has_pyenv(): planned.append("pyenv rehash") return {"source": str(source), "command": command, "steps": planned, "rehash": False, "dry_run": True, "version": __version__} proc = subprocess.run(command, capture_output=True, text=True) steps.append(" ".join(command)) tail = "\n".join((proc.stdout or "").strip().splitlines()[-6:]) if proc.returncode != 0: raise RuntimeError( f"pip 安装失败(退出码 {proc.returncode}):\n{(proc.stderr or proc.stdout or '')[-800:]}" ) steps.append(tail) rehashed = False if rehash and _has_pyenv(): subprocess.run(["pyenv", "rehash"], capture_output=True, text=True) steps.append("pyenv rehash") rehashed = True return { "source": str(source), "command": command, "steps": steps, "rehash": rehashed, "version": __version__, "installed_version": installed_version(), } def report_lines(report): prefix = "将执行" if report.get("dry_run") else "已执行" lines = [ f"源码目录 : {report['source']}", f"版本 : {report.get('installed_version') or report['version']}", f"{prefix} : {'; '.join(report['steps'])}", ] if report.get("rehash"): lines.append("已执行 : pyenv rehash") return lines