-
-
Notifications
You must be signed in to change notification settings - Fork 50
Feat: unified package manager #632
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d44b60e
5b6376b
7a7143a
198af46
fb68e92
b4b6d4c
aabe5ce
2c12a05
05ad4a0
50f8706
d7d6624
828a8e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,22 @@ | ||
| from .cli import main | ||
| from .env_loader import load_env | ||
| from .packages import PackageManager, PackageManagerType | ||
| from .unified_package_manager import ( | ||
| PackageFormat, | ||
| PackageInfo, | ||
| StorageAnalysis, | ||
| UnifiedPackageManager, | ||
| ) | ||
|
|
||
| __version__ = "0.1.0" | ||
|
|
||
| __all__ = ["main", "load_env", "PackageManager", "PackageManagerType"] | ||
| __all__ = [ | ||
| "main", | ||
| "load_env", | ||
| "PackageManager", | ||
| "PackageManagerType", | ||
| "UnifiedPackageManager", | ||
| "PackageFormat", | ||
| "PackageInfo", | ||
| "StorageAnalysis", | ||
| ] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -44,6 +44,7 @@ | |||||||||||||||||
|
|
||||||||||||||||||
| if TYPE_CHECKING: | ||||||||||||||||||
| from cortex.shell_env_analyzer import ShellEnvironmentAnalyzer | ||||||||||||||||||
| from cortex.unified_package_manager import UnifiedPackageManager | ||||||||||||||||||
|
|
||||||||||||||||||
| # Suppress noisy log messages in normal operation | ||||||||||||||||||
| logging.getLogger("httpx").setLevel(logging.WARNING) | ||||||||||||||||||
|
|
@@ -2825,6 +2826,256 @@ def progress_callback(current: int, total: int, step: InstallationStep) -> None: | |||||||||||||||||
| console.print(f"Error: {result.error_message}", style="red") | ||||||||||||||||||
| return 1 | ||||||||||||||||||
|
|
||||||||||||||||||
| # --- Unified Package Manager Commands (Issue #450) --- | ||||||||||||||||||
| def pkg(self, args: argparse.Namespace) -> int: | ||||||||||||||||||
| """Handle unified package manager commands for snap/flatpak/deb.""" | ||||||||||||||||||
| from cortex.unified_package_manager import ( | ||||||||||||||||||
| PackageFormat, | ||||||||||||||||||
| UnifiedPackageManager, | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| upm = UnifiedPackageManager() | ||||||||||||||||||
| action = getattr(args, "pkg_action", None) | ||||||||||||||||||
|
|
||||||||||||||||||
| if not action: | ||||||||||||||||||
| cx_print("\n📦 Unified Package Manager - Snap/Flatpak/Deb\n", "info") | ||||||||||||||||||
| console.print("Usage: cortex pkg <command> [options]") | ||||||||||||||||||
| console.print("\nCommands:") | ||||||||||||||||||
| console.print(" sources <package> Show available sources for a package") | ||||||||||||||||||
| console.print(" compare <package> Compare package across formats") | ||||||||||||||||||
| console.print(" list [--format FORMAT] List installed packages") | ||||||||||||||||||
| console.print(" permissions <package> Show package permissions") | ||||||||||||||||||
| console.print(" storage Analyze storage by format") | ||||||||||||||||||
| console.print(" snap-redirects [--disable] Check/disable snap redirects") | ||||||||||||||||||
| return 0 | ||||||||||||||||||
|
|
||||||||||||||||||
| if action == "sources": | ||||||||||||||||||
| return self._pkg_sources(upm, args) | ||||||||||||||||||
| elif action == "compare": | ||||||||||||||||||
| return self._pkg_compare(upm, args) | ||||||||||||||||||
| elif action == "list": | ||||||||||||||||||
| return self._pkg_list(upm, args) | ||||||||||||||||||
| elif action == "permissions": | ||||||||||||||||||
| return self._pkg_permissions(upm, args) | ||||||||||||||||||
| elif action == "storage": | ||||||||||||||||||
| return self._pkg_storage(upm) | ||||||||||||||||||
| elif action == "snap-redirects": | ||||||||||||||||||
| return self._pkg_snap_redirects(upm, args) | ||||||||||||||||||
| else: | ||||||||||||||||||
| self._print_error(f"Unknown pkg action: {action}") | ||||||||||||||||||
| return 1 | ||||||||||||||||||
|
|
||||||||||||||||||
| def _pkg_sources(self, upm: "UnifiedPackageManager", args: argparse.Namespace) -> int: | ||||||||||||||||||
| """Show available sources for a package.""" | ||||||||||||||||||
| package = args.package | ||||||||||||||||||
| cx_print(f"\n🔍 Checking sources for '{package}'...\n", "info") | ||||||||||||||||||
|
|
||||||||||||||||||
| sources = upm.detect_package_sources(package) | ||||||||||||||||||
|
|
||||||||||||||||||
| found_any = False | ||||||||||||||||||
| for format_name, info in sources.items(): | ||||||||||||||||||
| if info is not None: | ||||||||||||||||||
| found_any = True | ||||||||||||||||||
| status = "[green]✓ Installed[/green]" if info.installed else "[dim]Available[/dim]" | ||||||||||||||||||
| console.print(f" [{format_name.upper()}] {status}") | ||||||||||||||||||
| console.print(f" Version: {info.version or 'N/A'}") | ||||||||||||||||||
| if info.description: | ||||||||||||||||||
| console.print(f" {info.description[:60]}...") | ||||||||||||||||||
|
|
||||||||||||||||||
| if not found_any: | ||||||||||||||||||
| cx_print(f"No sources found for '{package}'", "warning") | ||||||||||||||||||
|
|
||||||||||||||||||
| return 0 | ||||||||||||||||||
|
|
||||||||||||||||||
| def _pkg_compare(self, upm, args: argparse.Namespace) -> int: | ||||||||||||||||||
| """Compare package across formats.""" | ||||||||||||||||||
| package = args.package | ||||||||||||||||||
| cx_print(f"\n📊 Comparing '{package}' across formats...\n", "info") | ||||||||||||||||||
|
|
||||||||||||||||||
| comparison = upm.compare_package_options(package) | ||||||||||||||||||
|
|
||||||||||||||||||
| if not comparison["available_formats"]: | ||||||||||||||||||
| cx_print(f"Package '{package}' not found in any format", "warning") | ||||||||||||||||||
| return 1 | ||||||||||||||||||
|
|
||||||||||||||||||
| console.print(f"[bold]Package:[/bold] {comparison['package_name']}") | ||||||||||||||||||
| if comparison["installed_as"]: | ||||||||||||||||||
| console.print(f"[bold]Installed as:[/bold] {comparison['installed_as']}") | ||||||||||||||||||
| console.print(f"[bold]Available in:[/bold] {', '.join(comparison['available_formats'])}") | ||||||||||||||||||
| console.print() | ||||||||||||||||||
|
|
||||||||||||||||||
| # Show comparison table | ||||||||||||||||||
| for fmt, data in comparison["comparison"].items(): | ||||||||||||||||||
| status = "[green]Installed[/green]" if data["installed"] else "[dim]Not installed[/dim]" | ||||||||||||||||||
| console.print(f"[cyan]{fmt.upper()}[/cyan]: {status}") | ||||||||||||||||||
| console.print(f" Version: {data['version'] or 'N/A'}") | ||||||||||||||||||
| if data["size"]: | ||||||||||||||||||
| size_mb = data["size"] / (1024 * 1024) | ||||||||||||||||||
| console.print(f" Size: {size_mb:.1f} MB") | ||||||||||||||||||
|
|
||||||||||||||||||
| return 0 | ||||||||||||||||||
|
|
||||||||||||||||||
| def _pkg_list(self, upm, args: argparse.Namespace) -> int: | ||||||||||||||||||
| """List installed packages by format.""" | ||||||||||||||||||
| from cortex.unified_package_manager import PackageFormat | ||||||||||||||||||
|
|
||||||||||||||||||
| format_filter = None | ||||||||||||||||||
| if hasattr(args, "format") and args.format: | ||||||||||||||||||
| format_map = { | ||||||||||||||||||
| "deb": PackageFormat.DEB, | ||||||||||||||||||
| "snap": PackageFormat.SNAP, | ||||||||||||||||||
| "flatpak": PackageFormat.FLATPAK, | ||||||||||||||||||
| } | ||||||||||||||||||
| format_filter = format_map.get(args.format.lower()) | ||||||||||||||||||
|
|
||||||||||||||||||
| cx_print("\n📦 Installed Packages\n", "info") | ||||||||||||||||||
|
|
||||||||||||||||||
| packages = upm.list_installed_packages(format_filter) | ||||||||||||||||||
|
|
||||||||||||||||||
| for fmt, pkgs in packages.items(): | ||||||||||||||||||
| if pkgs: | ||||||||||||||||||
| console.print(f"\n[cyan][{fmt.upper()}][/cyan] ({len(pkgs)} packages)") | ||||||||||||||||||
| for pkg in pkgs[:10]: # Show top 10 | ||||||||||||||||||
| console.print(f" • {pkg.name} ({pkg.version})") | ||||||||||||||||||
| if len(pkgs) > 10: | ||||||||||||||||||
| console.print(f" ... and {len(pkgs) - 10} more") | ||||||||||||||||||
|
|
||||||||||||||||||
| return 0 | ||||||||||||||||||
|
|
||||||||||||||||||
| def _pkg_permissions(self, upm, args: argparse.Namespace) -> int: | ||||||||||||||||||
| """Show package permissions.""" | ||||||||||||||||||
| package = args.package | ||||||||||||||||||
| fmt = getattr(args, "format", None) | ||||||||||||||||||
|
|
||||||||||||||||||
| cx_print(f"\n🔐 Permissions for '{package}'\n", "info") | ||||||||||||||||||
|
|
||||||||||||||||||
| # Determine format: explicit flag > detect from installed packages | ||||||||||||||||||
| if fmt is None: | ||||||||||||||||||
| # Check where package is actually installed | ||||||||||||||||||
| sources = upm.detect_package_sources(package) | ||||||||||||||||||
| if sources.get("flatpak") and sources["flatpak"].installed: | ||||||||||||||||||
| fmt = "flatpak" | ||||||||||||||||||
| elif sources.get("snap") and sources["snap"].installed: | ||||||||||||||||||
| fmt = "snap" | ||||||||||||||||||
| else: | ||||||||||||||||||
| fmt = "flatpak" if "." in package and package.count(".") >= 2 else "snap" | ||||||||||||||||||
|
||||||||||||||||||
| fmt = "flatpak" if "." in package and package.count(".") >= 2 else "snap" | |
| # Ambiguous or unknown format: avoid unreliable heuristics and require explicit format | |
| self._print_error( | |
| "Unable to determine package format automatically for " | |
| f"'{package}'. Please re-run with '--format flatpak' or " | |
| "'--format snap'." | |
| ) | |
| return 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pkg snap-redirects --disable command requires interactive confirmation, which prevents its use in scripts. For consistency with other destructive commands like cortex remove, a --yes/-y flag should be added to bypass the prompt.
This would involve two changes:
- In
main(), add the--yesargument to thepkg_redirects_parser:pkg_redirects_parser.add_argument( "--yes", "-y", action="store_true", help="Skip confirmation prompt", )
- In
_pkg_snap_redirects, check for this flag to skip theinput()call:def _pkg_snap_redirects(self, upm, args: argparse.Namespace) -> int: disable = getattr(args, "disable", False) skip_confirm = getattr(args, "yes", False) # Add this if disable: # ... print warnings ... if not skip_confirm: # Add this check try: confirm = input(...) # ... except (EOFError, KeyboardInterrupt): # ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The description truncation on line 2883 uses a magic number 60 without explanation. This should be either defined as a named constant (e.g., MAX_DESCRIPTION_LENGTH) or documented with a comment explaining why this specific length was chosen.