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