Spaces:
Sleeping
Sleeping
| """Implementation of `uofa setup uninstall` (REQ-DIST-007). | |
| Removes the UofA-managed Ollama runtime + cached model store + config | |
| file. Leaves bring-your-own-Ollama installs (REQ-DIST-005) untouched — | |
| we own only the trees inside ``~/.uofa/runtime/`` and | |
| ``~/.uofa/cache/ollama_models/`` that we created at setup time. | |
| A disk-space estimate is computed and reported up front; the caller is | |
| expected to prompt for confirmation before invoking ``uninstall()``. | |
| """ | |
| from __future__ import annotations | |
| import shutil | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from uofa_cli import setup_state | |
| class UninstallPlan: | |
| """What `uninstall(plan)` will remove if it runs.""" | |
| targets: list[Path] | |
| bytes_to_free: int | |
| def mb_to_free(self) -> float: | |
| return self.bytes_to_free / (1024 * 1024) | |
| class UninstallResult: | |
| removed: list[Path] | |
| skipped: list[Path] | |
| bytes_freed: int | |
| def plan_uninstall(cfg: setup_state.SetupConfig | None = None) -> UninstallPlan: | |
| """Return what `uninstall()` would remove without touching disk.""" | |
| cfg = cfg if cfg is not None else setup_state.load_config() | |
| targets: list[Path] = [] | |
| # Always remove the runtime/cache trees we own. | |
| runtime_root = setup_state.runtime_dir() | |
| if runtime_root.exists(): | |
| targets.append(runtime_root) | |
| models_dir = setup_state.models_cache_dir() | |
| if models_dir.exists(): | |
| targets.append(models_dir) | |
| config_path = setup_state.config_path() | |
| if config_path.exists(): | |
| targets.append(config_path) | |
| # Stale download cache from setup_install. | |
| downloads_dir = setup_state.uofa_data_dir() / "downloads" | |
| if downloads_dir.exists(): | |
| targets.append(downloads_dir) | |
| bytes_to_free = sum(_size_on_disk(t) for t in targets) | |
| return UninstallPlan(targets=targets, bytes_to_free=bytes_to_free) | |
| def uninstall( | |
| cfg: setup_state.SetupConfig | None = None, | |
| *, | |
| on_status=None, | |
| ) -> UninstallResult: | |
| """Remove the managed install. Does not touch BYO Ollama installs.""" | |
| say = on_status or (lambda _: None) | |
| plan = plan_uninstall(cfg) | |
| cfg = cfg if cfg is not None else setup_state.load_config() | |
| # If the recorded binary is a BYO install (lives outside our managed | |
| # runtime tree), we must NOT touch it. plan_uninstall already excludes | |
| # everything outside ~/.uofa, but be defensive. | |
| skipped: list[Path] = [] | |
| if cfg is not None and cfg.mode == "byo": | |
| managed_root = setup_state.uofa_data_dir().resolve() | |
| try: | |
| cfg.ollama_binary.resolve().relative_to(managed_root) | |
| except ValueError: | |
| skipped.append(cfg.ollama_binary) | |
| say(f"Skipping BYO Ollama at {cfg.ollama_binary} (not managed by UofA).") | |
| removed: list[Path] = [] | |
| for target in plan.targets: | |
| if target.is_dir(): | |
| shutil.rmtree(target) | |
| else: | |
| target.unlink() | |
| removed.append(target) | |
| say(f"removed {target}") | |
| return UninstallResult( | |
| removed=removed, | |
| skipped=skipped, | |
| bytes_freed=plan.bytes_to_free, | |
| ) | |
| def _size_on_disk(path: Path) -> int: | |
| if path.is_file(): | |
| return path.stat().st_size | |
| total = 0 | |
| for p in path.rglob("*"): | |
| if p.is_file(): | |
| total += p.stat().st_size | |
| return total | |