#!/usr/bin/env python3 import sys import os import json import time import subprocess STATE_FILE = os.path.expanduser("~/.uni/state.json") # ------------------------- # STATE # ------------------------- def load_state(): if not os.path.exists(STATE_FILE): return {"backends": {}, "packages": {}} with open(STATE_FILE, "r") as f: return json.load(f) def save_state(state): os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True) with open(STATE_FILE, "w") as f: json.dump(state, f, indent=2) # ------------------------- # NORMALIZATION (FIX) # ------------------------- def normalize_state(state): fixed = {} for pkg, entries in state.get("packages", {}).items(): # old format: "apt" if isinstance(entries, str): fixed[pkg] = [{"backend": entries}] continue # already correct list if isinstance(entries, list): clean = [] for e in entries: if isinstance(e, dict) and "backend" in e: clean.append(e) elif isinstance(e, str): clean.append({"backend": e}) fixed[pkg] = clean continue fixed[pkg] = [] state["packages"] = fixed return state # ------------------------- # BACKEND DETECTION # ------------------------- def has(cmd): return subprocess.call( ["which", cmd], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) == 0 def detect_backends(state): state["backends"] = { "apt": has("apt"), "dnf": has("dnf"), "pacman": has("pacman"), "flatpak": has("flatpak") } return state["backends"] # ------------------------- # RESOLVER # ------------------------- def resolve_backend(pkg, state): gui_apps = ["firefox", "discord", "spotify", "vscode"] if pkg in gui_apps and state["backends"].get("flatpak"): return "flatpak" if state["backends"].get("apt"): return "apt" if state["backends"].get("dnf"): return "dnf" if state["backends"].get("pacman"): return "pacman" return None # ------------------------- # CONFLICT CHECK # ------------------------- def check_conflict(state, pkg, backend): if pkg not in state["packages"]: return False for e in state["packages"][pkg]: if e.get("backend") != backend: return True return False # ------------------------- # EXEC # ------------------------- def run(cmd): subprocess.run(cmd) def install_pkg(pkg, backend): if backend == "apt": run(["sudo", "apt", "install", "-y", pkg]) elif backend == "dnf": run(["sudo", "dnf", "install", "-y", pkg]) elif backend == "pacman": run(["sudo", "pacman", "-S", "--noconfirm", pkg]) elif backend == "flatpak": run(["flatpak", "install", "-y", pkg]) def remove_pkg(pkg, backend): if backend == "apt": run(["sudo", "apt", "remove", "-y", pkg]) elif backend == "dnf": run(["sudo", "dnf", "remove", "-y", pkg]) elif backend == "pacman": run(["sudo", "pacman", "-R", "--noconfirm", pkg]) elif backend == "flatpak": run(["flatpak", "uninstall", "-y", pkg]) def update_all(state): if state["backends"].get("apt"): run(["sudo", "apt", "update"]) run(["sudo", "apt", "upgrade", "-y"]) if state["backends"].get("dnf"): run(["sudo", "dnf", "upgrade", "-y"]) if state["backends"].get("pacman"): run(["sudo", "pacman", "-Syu", "--noconfirm"]) if state["backends"].get("flatpak"): run(["flatpak", "update", "-y"]) # ------------------------- # TRACKING # ------------------------- def register_install(state, pkg, backend): if pkg not in state["packages"]: state["packages"][pkg] = [] state["packages"][pkg].append({ "backend": backend, "time": int(time.time()) }) def register_remove(state, pkg, backend): if pkg not in state["packages"]: return state["packages"][pkg] = [ e for e in state["packages"][pkg] if e.get("backend") != backend ] if not state["packages"][pkg]: del state["packages"][pkg] # ------------------------- # LIST # ------------------------- def list_installed(): state = load_state() state = normalize_state(state) for pkg, entries in state["packages"].items(): backends = ", ".join([e["backend"] for e in entries]) print(f"{pkg} -> {backends}") # ------------------------- # DOCTOR # ------------------------- def doctor(): state = load_state() state = normalize_state(state) detect_backends(state) save_state(state) print("=== SYSTEM ===") for k, v in state["backends"].items(): print(f"{k}: {'yes' if v else 'no'}") print("\n=== HEALTH ===") broken = [] for pkg, entries in state["packages"].items(): if not isinstance(entries, list): broken.append(pkg) continue for e in entries: if not isinstance(e, dict) or "backend" not in e: broken.append(pkg) print("OK" if not broken else f"broken: {len(broken)}") print("\n=== CONFLICTS ===") conflicts = {} for pkg, entries in state["packages"].items(): backends = list(set([e["backend"] for e in entries])) if len(backends) > 1: conflicts[pkg] = backends if not conflicts: print("none") else: for k, v in conflicts.items(): print(f"{k}: {', '.join(v)}") print("\n=== SUMMARY ===") print(f"packages: {len(state['packages'])}") print(f"conflicts: {len(conflicts)}") print(f"corrupt: {len(broken)}") # ------------------------- # INSTALL / REMOVE # ------------------------- def install(pkg): state = load_state() state = normalize_state(state) detect_backends(state) backend = resolve_backend(pkg, state) if not backend: print("no backend") return if check_conflict(state, pkg, backend): print("WARNING: backend mismatch") install_pkg(pkg, backend) register_install(state, pkg, backend) save_state(state) def remove(pkg): state = load_state() state = normalize_state(state) if pkg not in state["packages"]: print("not tracked") return for e in state["packages"][pkg]: remove_pkg(pkg, e["backend"]) del state["packages"][pkg] save_state(state) def update(): state = load_state() detect_backends(state) update_all(state) # ------------------------- # MAIN # ------------------------- def main(): if len(sys.argv) < 2: print("install|remove|update|list|doctor") return cmd = sys.argv[1] if cmd == "install": install(sys.argv[2]) elif cmd == "remove": remove(sys.argv[2]) elif cmd == "update": update() elif cmd == "list": list_installed() elif cmd == "doctor": doctor() else: print("unknown") if __name__ == "__main__": main()