Initial commit: ArchStore package manager for Arch Linux

This commit is contained in:
2026-05-21 02:42:03 +05:30
commit 027847fbac
51 changed files with 6993 additions and 0 deletions
+228
View File
@@ -0,0 +1,228 @@
"""
Unified package service for ArchStore.
Merges pacman and AUR results, handles deduplication and ranking.
"""
import asyncio
from typing import Optional
from services import pacman_service, aur_service
from database.db import db
from utils.sanitize import sanitize_search_query, sanitize_package_name, validate_source_filter
# Category definitions — maps friendly names to pacman groups
CATEGORIES = {
"Development": {
"icon": "code",
"description": "Programming tools, compilers, and IDEs",
"groups": ["base-devel"],
"keywords": ["gcc", "git", "python", "nodejs", "rust", "go", "vim", "neovim", "code"],
},
"System": {
"icon": "monitor",
"description": "Core system utilities and tools",
"groups": ["base", "sys-utils"],
"keywords": ["systemd", "kernel", "grub", "filesystem", "coreutils"],
},
"Network": {
"icon": "wifi",
"description": "Networking tools, browsers, and servers",
"groups": ["network"],
"keywords": ["firefox", "chromium", "curl", "wget", "nginx", "ssh"],
},
"Multimedia": {
"icon": "music",
"description": "Audio, video, and image tools",
"groups": ["multimedia"],
"keywords": ["vlc", "mpv", "ffmpeg", "gimp", "audacity", "obs"],
},
"Games": {
"icon": "gamepad-2",
"description": "Games and gaming tools",
"groups": ["games"],
"keywords": ["steam", "lutris", "wine", "gamemode"],
},
"Desktop": {
"icon": "layout-dashboard",
"description": "Desktop environments and window managers",
"groups": ["gnome", "kde-applications", "xfce4"],
"keywords": ["gnome", "kde", "xfce", "i3", "sway", "hyprland"],
},
"Fonts": {
"icon": "type",
"description": "Fonts and typography",
"groups": ["fonts"],
"keywords": ["ttf", "otf", "nerd-fonts", "noto"],
},
"Security": {
"icon": "shield",
"description": "Security and privacy tools",
"groups": [],
"keywords": ["firewall", "gpg", "openssl", "wireguard", "tor"],
},
}
async def search_packages(query: str, source: str = "all") -> list[dict]:
"""
Search packages from pacman, AUR, or both.
Results are deduplicated, merged, and ranked.
"""
query = sanitize_search_query(query)
source = validate_source_filter(source)
# Check cache first
cached = await db.get_cached_search(query, source)
if cached:
return cached
results = []
if source in ("all", "pacman"):
pacman_task = pacman_service.search_packages(query)
else:
pacman_task = asyncio.coroutine(lambda: [])()
if source in ("all", "aur"):
aur_task = aur_service.search_packages(query)
else:
aur_task = asyncio.coroutine(lambda: [])()
# Run both searches concurrently
if source == "all":
pacman_results, aur_results = await asyncio.gather(
pacman_service.search_packages(query),
aur_service.search_packages(query),
return_exceptions=True,
)
if isinstance(pacman_results, Exception):
pacman_results = []
if isinstance(aur_results, Exception):
aur_results = []
results = _merge_results(pacman_results, aur_results)
elif source == "pacman":
results = await pacman_service.search_packages(query)
elif source == "aur":
results = await aur_service.search_packages(query)
# Cache results
await db.set_cached_search(query, source, results)
return results
async def get_package_info(name: str) -> Optional[dict]:
"""Get detailed package info, trying pacman first then AUR."""
name = sanitize_package_name(name)
# Check cache
cached = await db.get_cached_package(name)
if cached:
return cached
# Try pacman first
info = await pacman_service.get_package_info(name)
if not info:
# Try AUR
info = await aur_service.get_package_info(name)
if not info:
# Check if installed locally
info = await pacman_service.get_installed_info(name)
if info:
await db.set_cached_package(name, info)
return info
async def install_package(name: str) -> dict:
"""Install a package, auto-detecting source."""
name = sanitize_package_name(name)
# Try pacman first
info = await pacman_service.get_package_info(name)
if info:
return await pacman_service.install_package(name)
# Fall back to AUR via yay
return await aur_service.install_package(name)
async def remove_package(name: str) -> dict:
"""Remove an installed package."""
name = sanitize_package_name(name)
return await pacman_service.remove_package(name)
async def list_installed() -> list[dict]:
"""List all installed packages."""
return await pacman_service.list_installed()
async def check_updates() -> list[dict]:
"""Check for all available updates (pacman + AUR)."""
pacman_updates, aur_updates = await asyncio.gather(
pacman_service.check_updates(),
aur_service.check_updates(),
return_exceptions=True,
)
if isinstance(pacman_updates, Exception):
pacman_updates = []
if isinstance(aur_updates, Exception):
aur_updates = []
return pacman_updates + aur_updates
async def get_categories() -> list[dict]:
"""Get list of package categories."""
return [
{"name": name, **data}
for name, data in CATEGORIES.items()
]
async def get_category_packages(category: str) -> list[dict]:
"""Get packages for a specific category using search."""
cat = CATEGORIES.get(category)
if not cat:
return []
# Search using category keywords
all_results = []
for keyword in cat.get("keywords", [])[:5]:
try:
results = await search_packages(keyword, "all")
all_results.extend(results)
except Exception:
continue
# Deduplicate by name
seen = set()
unique = []
for pkg in all_results:
if pkg["name"] not in seen:
seen.add(pkg["name"])
unique.append(pkg)
return unique[:50]
def _merge_results(pacman: list[dict], aur: list[dict]) -> list[dict]:
"""Merge and deduplicate pacman + AUR results."""
seen = {}
# Pacman results take priority
for pkg in pacman:
seen[pkg["name"]] = pkg
# Add AUR results if not already from pacman
for pkg in aur:
if pkg["name"] not in seen:
seen[pkg["name"]] = pkg
# Sort: installed first, then by name
results = list(seen.values())
results.sort(key=lambda p: (not p.get("installed", False), p.get("name", "")))
return results