Add uni.py

This commit is contained in:
2026-04-22 16:42:03 +02:00
parent 03de04d4a6
commit 27b026cc96
+316
View File
@@ -0,0 +1,316 @@
#!/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()