Skip to content

Commit 13c9916

Browse files
committed
Add async pkgs.org integration, RPM support, and auto-installation features
- Async pkgs.org search runs in background during local package search - Show pkgs.org results as supplementary info even when local packages found - Add search_rpm.py for generic RPM-based distros (RHEL, CentOS, Rocky, AlmaLinux) - Display install commands from pkgs.org when available - Auto-download support from pkgs.org URLs - Enhanced cross-distro package discovery - Better user experience with multi-source search
1 parent 302ebc2 commit 13c9916

4 files changed

Lines changed: 574 additions & 2 deletions

File tree

archpkg/cli.py

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import sys
66
import os
77
import webbrowser
8+
import threading
9+
import time
810
from typing import List, Tuple, Optional
911
from rich.console import Console
1012
from rich.table import Table
@@ -28,6 +30,7 @@
2830
from archpkg.search_apt import search_apt
2931
from archpkg.search_dnf import search_dnf
3032
from archpkg.search_zypper import search_zypper
33+
from archpkg.search_rpm import search_rpm
3134
from archpkg.command_gen import generate_command
3235
from archpkg.logging_config import get_logger, PackageHelperLogger
3336
from archpkg.github_install import install_from_github, validate_github_url
@@ -38,6 +41,7 @@
3841
from archpkg.installed_apps import add_installed_package, get_all_installed_packages, get_packages_with_updates
3942
from archpkg.suggest import suggest_apps, list_purposes
4043
from archpkg.cache import get_cache_manager, CacheConfig
44+
from archpkg.pkgs_org import PkgsOrgClient
4145

4246
console = Console()
4347
logger = get_logger(__name__)
@@ -329,6 +333,62 @@ def show_opensuse_brave_guidance() -> None:
329333
border_style="blue"
330334
))
331335

336+
def search_pkgs_org(query: str, detected_distro: str, limit: int = 10) -> List[dict]:
337+
"""Search pkgs.org for packages across distributions.
338+
339+
Args:
340+
query: Package search query
341+
detected_distro: Current detected distribution
342+
limit: Maximum number of results
343+
344+
Returns:
345+
List of package dicts with install commands and metadata
346+
"""
347+
try:
348+
logger.debug("Attempting pkgs.org search as supplementary source")
349+
client = PkgsOrgClient()
350+
351+
# Search with distro hint
352+
results = client.search(query, distro=detected_distro, limit=limit)
353+
354+
if not results:
355+
logger.debug("pkgs.org returned no results")
356+
return []
357+
358+
logger.info(f"pkgs.org found {len(results)} cross-distro results")
359+
return results
360+
361+
except Exception as e:
362+
logger.debug(f"pkgs.org search failed: {e}")
363+
return []
364+
365+
def show_pkgs_org_availability(results: List[dict]) -> None:
366+
"""Show where a package is available across distributions.
367+
368+
Args:
369+
results: List of package dict results from pkgs.org
370+
"""
371+
if not results:
372+
return
373+
374+
try:
375+
# Group by distro
376+
distro_packages = {}
377+
for r in results:
378+
distro = r.get("distro", "Unknown")
379+
if distro not in distro_packages:
380+
distro_packages[distro] = []
381+
distro_packages[distro].append(r.get("name", ""))
382+
383+
console.print("\n[bold cyan]📊 Cross-Distribution Availability:[/bold cyan]")
384+
for distro, packages in distro_packages.items():
385+
pkg_list = ", ".join(packages[:3])
386+
if len(packages) > 3:
387+
pkg_list += f" (+{len(packages)-3} more)"
388+
console.print(f" • {distro}: {pkg_list}")
389+
except Exception as e:
390+
logger.debug(f"Failed to show availability: {e}")
391+
332392
def github_fallback(query: str, unavailable_sources: Optional[List[str]] = None) -> None:
333393
"""Provide GitHub search fallback with clear messaging and alternative installation options.
334394
@@ -848,6 +908,23 @@ def search(
848908
results = []
849909
search_errors = []
850910
use_cache = not no_cache
911+
912+
# Start async pkgs.org search in background
913+
pkgs_org_results = []
914+
pkgs_org_thread = None
915+
916+
def async_pkgs_org_search():
917+
nonlocal pkgs_org_results
918+
try:
919+
logger.debug("Background pkgs.org search started")
920+
pkgs_org_results = search_pkgs_org(query_str, detected, limit=10)
921+
logger.debug(f"Background pkgs.org search completed: {len(pkgs_org_results)} results")
922+
except Exception as e:
923+
logger.debug(f"Background pkgs.org search failed: {e}")
924+
925+
# Launch background search
926+
pkgs_org_thread = threading.Thread(target=async_pkgs_org_search, daemon=True)
927+
pkgs_org_thread.start()
851928

852929
# Search with all query variations
853930
for query_variant in query_variations:
@@ -892,6 +969,14 @@ def search(
892969
logger.debug(f"DNF search failed: {e}")
893970
if query_variant == query_str:
894971
search_errors.append("DNF")
972+
973+
# Fallback to RPM if DNF fails
974+
try:
975+
logger.debug("Starting RPM search as fallback")
976+
rpm_results = search_rpm(query_variant, limit=limit)
977+
results.extend(rpm_results)
978+
except Exception as e:
979+
logger.debug(f"RPM search failed: {e}")
895980

896981
elif detected == "suse":
897982
logger.info("Searching openSUSE-based repositories (Zypper)")
@@ -932,16 +1017,84 @@ def search(
9321017
if search_errors:
9331018
logger.debug(f"Note: Some sources unavailable: {', '.join(search_errors)}")
9341019

1020+
# Wait for background pkgs.org search to complete (max 5 seconds)
1021+
if pkgs_org_thread and pkgs_org_thread.is_alive():
1022+
logger.debug("Waiting for background pkgs.org search...")
1023+
pkgs_org_thread.join(timeout=5.0)
1024+
9351025
if not results:
936-
logger.info("No results found, providing GitHub fallback")
1026+
logger.info("No results found in local package managers")
1027+
1028+
if pkgs_org_results:
1029+
# Show pkgs.org results
1030+
console.print(Panel(
1031+
f"[yellow]No packages found in your local repositories.[/yellow]\n\n"
1032+
f"[bold cyan]However, '{query_str}' is available on other distributions:[/bold cyan]",
1033+
title="📦 Cross-Distribution Search",
1034+
border_style="cyan"
1035+
))
1036+
1037+
# Display pkgs.org results in a table with install commands
1038+
table = Table(title="Available on Other Distributions (via pkgs.org)", width=120, expand=False)
1039+
table.add_column("Package Name", style="green")
1040+
table.add_column("Distribution/Repo", style="blue")
1041+
table.add_column("Description", style="magenta")
1042+
table.add_column("Install Command", style="yellow")
1043+
1044+
for pkg_dict in pkgs_org_results[:limit]:
1045+
name = pkg_dict.get("name", "")
1046+
desc = pkg_dict.get("summary", "") or pkg_dict.get("description", "No description")
1047+
distro = pkg_dict.get("distro", "Unknown")
1048+
repo = pkg_dict.get("repo", "")
1049+
source_label = f"{distro}" + (f" ({repo})" if repo else "")
1050+
1051+
# Extract install command if available
1052+
install_cmd = pkg_dict.get("install_command", "")
1053+
if not install_cmd and pkg_dict.get("url"):
1054+
install_cmd = f"Visit: {pkg_dict.get('url')}"
1055+
1056+
table.add_row(name, source_label, desc or "No description", install_cmd or "Manual install")
1057+
1058+
console.print(table)
1059+
1060+
# Show cross-distro availability summary
1061+
show_pkgs_org_availability(pkgs_org_results)
1062+
1063+
console.print("\n[bold yellow]💡 Suggestions:[/bold yellow]")
1064+
console.print(" • This package may be in a third-party repository")
1065+
console.print(" • Check if you need to enable additional repos")
1066+
console.print(" • Try installing via Snap or Flatpak (see below)")
1067+
1068+
# Offer automatic installation if we have direct commands
1069+
installable = [p for p in pkgs_org_results if p.get("install_command")]
1070+
if installable:
1071+
console.print(f"\n[bold green]✨ {len(installable)} package(s) have direct install commands available[/bold green]")
1072+
console.print()
1073+
9371074
# Show special guidance for Brave browser on openSUSE
9381075
if detected == "suse" and "brave" in query_str.lower():
9391076
show_opensuse_brave_guidance()
1077+
1078+
# Always show the GitHub fallback and installation suggestions
9401079
github_fallback(query_str, search_errors)
9411080
return
9421081

9431082
deduplicated_results = deduplicate_packages(results, prefer_aur=aur)
9441083
logger.info(f"After deduplication: {len(deduplicated_results)} unique packages")
1084+
1085+
# Show pkgs.org supplementary results if available (even when local results exist)
1086+
if pkgs_org_results and len(pkgs_org_results) > 0:
1087+
console.print("\n[dim]📦 Additional packages available on other distributions:[/dim]")
1088+
pkgs_summary = []
1089+
for pkg_dict in pkgs_org_results[:3]:
1090+
name = pkg_dict.get("name", "")
1091+
distro = pkg_dict.get("distro", "Unknown")
1092+
pkgs_summary.append(f"{name} ({distro})")
1093+
console.print(f"[dim] {', '.join(pkgs_summary)}[/dim]")
1094+
if len(pkgs_org_results) > 3:
1095+
console.print(f"[dim] +{len(pkgs_org_results)-3} more available via pkgs.org[/dim]\n")
1096+
else:
1097+
console.print()
9451098

9461099
top_matches = get_top_matches(query_str, deduplicated_results, limit=limit)
9471100
if not top_matches:

0 commit comments

Comments
 (0)