Add uni.py
This commit is contained in:
@@ -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()
|
||||||
Reference in New Issue
Block a user