one-updater 0.0.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.1
2
+ Name: one-updater
3
+ Version: 0.0.8
4
+ Summary: One tool many packages
5
+ Author: Tim Bryant
6
+ Author-email: timothybryant3@gmail.com
7
+ Requires-Python: >=3.11,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Requires-Dist: click (>=8.1.7,<9.0.0)
13
+ Requires-Dist: python-semantic-release (>=9.14.0,<10.0.0)
14
+ Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
15
+ Requires-Dist: rich (>=13.9.4,<14.0.0)
16
+ Description-Content-Type: text/markdown
17
+
18
+ # One Updater
19
+
20
+ A flexible package manager updater that helps you keep all your development tools up to date.
21
+
22
+ ## Features
23
+
24
+ - Update multiple package managers with a single command
25
+ - Configure which package managers to update
26
+ - Support for virtual environments and pyenv for Python packages
27
+ - Beautiful command-line interface with rich formatting
28
+ - Extensible architecture for adding new package managers
29
+
30
+ ## Supported Package Managers
31
+
32
+ - Homebrew
33
+ - pip (with virtualenv/pyenv support)
34
+ - npm
35
+ - cargo
36
+ - gem
37
+ - pipx
38
+ - More coming soon!
39
+
40
+ ## Installation
41
+
42
+ 1. Clone the repository:
43
+
44
+ ```bash
45
+ git clone https://github.com/yourusername/one-update.git
46
+ cd one-update
47
+ ```
48
+
49
+ 2. Install dependencies:
50
+
51
+ ```bash
52
+ pip install -r requirements.txt
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ ### Basic Usage
58
+
59
+ Update all enabled package managers:
60
+
61
+ ```bash
62
+ python -m one_update.cli update
63
+ ```
64
+
65
+ Update specific package managers:
66
+
67
+ ```bash
68
+ python -m one_update.cli update -m homebrew -m pip
69
+ ```
70
+
71
+ List configured package managers:
72
+
73
+ ```bash
74
+ python -m one_update.cli list-managers
75
+ ```
76
+
77
+ ### Configuration
78
+
79
+ The tool uses a YAML configuration file (`config.yaml`) to specify package manager settings. You can:
80
+
81
+ 1. Enable/disable specific package managers
82
+ 2. Configure virtualenv/pyenv for Python packages
83
+ 3. Customize update commands
84
+ 4. Configure logging
85
+
86
+ Example configuration:
87
+
88
+ ```yaml
89
+ package_managers:
90
+ homebrew:
91
+ enabled: true
92
+ commands:
93
+ update: ["brew", "update"]
94
+ upgrade: ["brew", "upgrade"]
95
+
96
+ pip:
97
+ enabled: true
98
+ virtualenv: "/path/to/virtualenv" # Optional
99
+ pyenv: "3.11.0" # Optional
100
+ commands:
101
+ update: ["pip", "install", "--upgrade", "pip"]
102
+ ```
103
+
104
+ ## Contributing
105
+
106
+ Contributions are welcome! Feel free to:
107
+
108
+ 1. Add support for new package managers
109
+ 2. Improve error handling and logging
110
+ 3. Add new features
111
+ 4. Fix bugs
112
+
113
+ ## License
114
+
115
+ MIT License
116
+
@@ -0,0 +1,98 @@
1
+ # One Updater
2
+
3
+ A flexible package manager updater that helps you keep all your development tools up to date.
4
+
5
+ ## Features
6
+
7
+ - Update multiple package managers with a single command
8
+ - Configure which package managers to update
9
+ - Support for virtual environments and pyenv for Python packages
10
+ - Beautiful command-line interface with rich formatting
11
+ - Extensible architecture for adding new package managers
12
+
13
+ ## Supported Package Managers
14
+
15
+ - Homebrew
16
+ - pip (with virtualenv/pyenv support)
17
+ - npm
18
+ - cargo
19
+ - gem
20
+ - pipx
21
+ - More coming soon!
22
+
23
+ ## Installation
24
+
25
+ 1. Clone the repository:
26
+
27
+ ```bash
28
+ git clone https://github.com/yourusername/one-update.git
29
+ cd one-update
30
+ ```
31
+
32
+ 2. Install dependencies:
33
+
34
+ ```bash
35
+ pip install -r requirements.txt
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ### Basic Usage
41
+
42
+ Update all enabled package managers:
43
+
44
+ ```bash
45
+ python -m one_update.cli update
46
+ ```
47
+
48
+ Update specific package managers:
49
+
50
+ ```bash
51
+ python -m one_update.cli update -m homebrew -m pip
52
+ ```
53
+
54
+ List configured package managers:
55
+
56
+ ```bash
57
+ python -m one_update.cli list-managers
58
+ ```
59
+
60
+ ### Configuration
61
+
62
+ The tool uses a YAML configuration file (`config.yaml`) to specify package manager settings. You can:
63
+
64
+ 1. Enable/disable specific package managers
65
+ 2. Configure virtualenv/pyenv for Python packages
66
+ 3. Customize update commands
67
+ 4. Configure logging
68
+
69
+ Example configuration:
70
+
71
+ ```yaml
72
+ package_managers:
73
+ homebrew:
74
+ enabled: true
75
+ commands:
76
+ update: ["brew", "update"]
77
+ upgrade: ["brew", "upgrade"]
78
+
79
+ pip:
80
+ enabled: true
81
+ virtualenv: "/path/to/virtualenv" # Optional
82
+ pyenv: "3.11.0" # Optional
83
+ commands:
84
+ update: ["pip", "install", "--upgrade", "pip"]
85
+ ```
86
+
87
+ ## Contributing
88
+
89
+ Contributions are welcome! Feel free to:
90
+
91
+ 1. Add support for new package managers
92
+ 2. Improve error handling and logging
93
+ 3. Add new features
94
+ 4. Fix bugs
95
+
96
+ ## License
97
+
98
+ MIT License
@@ -0,0 +1,3 @@
1
+ """One Update - A flexible package manager updater."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,155 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ import click
5
+ import yaml
6
+ from rich.console import Console
7
+ from rich.logging import RichHandler
8
+
9
+ from one_updater import __version__
10
+ from one_updater.package_managers.base import PackageManager
11
+ from one_updater.package_managers.registry import PackageManagerRegistry
12
+
13
+ console = Console()
14
+
15
+
16
+ def setup_logging(config: dict):
17
+ """Setup logging configuration."""
18
+ logging.basicConfig(
19
+ level=config.get("logging", {}).get("level", "INFO"),
20
+ format=config.get("logging", {}).get("format", "%(message)s"),
21
+ handlers=[RichHandler(console=console)],
22
+ )
23
+
24
+
25
+ def load_config(config_path: str) -> dict:
26
+ """Load configuration from YAML file."""
27
+ try:
28
+ with open(config_path, encoding="utf-8") as f:
29
+ return yaml.safe_load(f)
30
+ except Exception as e:
31
+ console.print(f"[red]Error loading config file: {e}[/red]")
32
+ return {}
33
+
34
+
35
+ def get_package_manager(name: str, config: dict) -> Optional[PackageManager]:
36
+ """Get a package manager instance by name."""
37
+ try:
38
+ return PackageManagerRegistry.get_manager(name, config)
39
+ except ValueError as e:
40
+ logging.warning(str(e))
41
+ return None
42
+
43
+
44
+ def run_package_manager_action(
45
+ name: str, cfg: dict, action_name: str, action_func, verbose: bool
46
+ ) -> None:
47
+ """Run a package manager action (update or upgrade) with proper console output."""
48
+ if not cfg.get("enabled", True):
49
+ return
50
+
51
+ # Check if the command is configured
52
+ commands = cfg.get("commands", {})
53
+
54
+ if action_name not in commands:
55
+ console.print(
56
+ f"[yellow]! {name} does not have a {action_name} command configured[/yellow]"
57
+ )
58
+ return
59
+
60
+ # Set verbose mode for this specific package manager
61
+ cfg["verbose"] = verbose
62
+
63
+ if pm := get_package_manager(name, cfg):
64
+ # strip e at end of action_name if it exists
65
+ action_name_without_e = action_name.title().rstrip("e")
66
+ console.print(f"\n[bold blue]{action_name_without_e}ing {name}...[/bold blue]")
67
+ if success := action_func(pm):
68
+ console.print(f"[green]✓ {name} {action_name}d successfully[/green]")
69
+ else:
70
+ console.print(f"[red]✗ {name} {action_name} failed[/red]")
71
+
72
+
73
+ @click.group()
74
+ @click.option("--config", "-c", default="config.yaml", help="Path to config file")
75
+ @click.version_option(version=__version__, prog_name="one-updater")
76
+ @click.pass_context
77
+ def cli(ctx, config):
78
+ """One Update - Update all your package managers with one command."""
79
+ ctx.ensure_object(dict)
80
+ ctx.obj["config"] = load_config(config)
81
+ setup_logging(ctx.obj["config"])
82
+
83
+
84
+ @cli.command()
85
+ @click.option(
86
+ "--manager", "-m", multiple=True, help="Specific package manager(s) to update"
87
+ )
88
+ @click.option(
89
+ "--verbose", "-v", is_flag=True, help="Show verbose output from package managers"
90
+ )
91
+ @click.pass_context
92
+ def update(ctx, manager, verbose):
93
+ """Update package manager indices/registries."""
94
+ config = ctx.obj["config"]
95
+ package_managers = config.get("package_managers", {})
96
+
97
+ # Filter package managers if specified
98
+ if manager:
99
+ # Check for requested managers that don't exist in config
100
+ for m in manager:
101
+ if m not in package_managers:
102
+ logging.warning(f"Package manager '{m}' is not defined in config")
103
+ package_managers = {k: v for k, v in package_managers.items() if k in manager}
104
+
105
+ with console.status("[bold green]Updating package managers...") as status:
106
+ for name, cfg in package_managers.items():
107
+ run_package_manager_action(
108
+ name, cfg, "update", lambda pm: pm.update(), verbose
109
+ )
110
+
111
+
112
+ @cli.command()
113
+ @click.option(
114
+ "--manager", "-m", multiple=True, help="Specific package manager(s) to upgrade"
115
+ )
116
+ @click.option(
117
+ "--verbose", "-v", is_flag=True, help="Show verbose output from package managers"
118
+ )
119
+ @click.pass_context
120
+ def upgrade(ctx, manager, verbose):
121
+ """Upgrade all packages for specified package managers."""
122
+ config = ctx.obj["config"]
123
+ package_managers = config.get("package_managers", {})
124
+
125
+ # Filter package managers if specified
126
+ if manager:
127
+ # Check for requested managers that don't exist in config
128
+ for m in manager:
129
+ if m not in package_managers:
130
+ logging.warning(f"Package manager '{m}' is not defined in config")
131
+ package_managers = {k: v for k, v in package_managers.items() if k in manager}
132
+
133
+ with console.status("[bold green]Upgrading packages...") as status:
134
+ for name, cfg in package_managers.items():
135
+ run_package_manager_action(
136
+ name, cfg, "upgrade", lambda pm: pm.upgrade(), verbose
137
+ )
138
+
139
+
140
+ @cli.command()
141
+ @click.pass_context
142
+ def list_managers(ctx):
143
+ """List all configured package managers and their status."""
144
+ config = ctx.obj["config"]
145
+ package_managers = config.get("package_managers", {})
146
+
147
+ console.print("\n[bold]Configured Package Managers:[/bold]")
148
+ for name, cfg in package_managers.items():
149
+ enabled = cfg.get("enabled", True)
150
+ status = "[green]enabled[/green]" if enabled else "[red]disabled[/red]"
151
+ console.print(f" • {name}: {status}")
152
+
153
+
154
+ if __name__ == "__main__":
155
+ cli(obj={})
@@ -0,0 +1,33 @@
1
+ from .apt import AptManager
2
+ from .base import PackageManager
3
+ from .basher import BasherManager
4
+ from .brew import HomebrewManager
5
+ from .cargo import CargoManager
6
+ from .gem import GemManager
7
+ from .ghcli import GhCliManager
8
+ from .go import GoManager
9
+ from .krew import KubectlKrewManager
10
+ from .micro import MicroEditorManager
11
+ from .npm import NpmManager
12
+ from .pip import PipManager
13
+ from .pipx import PipxManager
14
+ from .pkgx import PkgxManager
15
+ from .vagrant import VagrantPluginManager
16
+
17
+ __all__ = [
18
+ "PackageManager",
19
+ "AptManager",
20
+ "BasherManager",
21
+ "HomebrewManager",
22
+ "CargoManager",
23
+ "GemManager",
24
+ "GhCliManager",
25
+ "GoManager",
26
+ "KubectlKrewManager",
27
+ "MicroEditorManager",
28
+ "NpmManager",
29
+ "PipManager",
30
+ "PipxManager",
31
+ "PkgxManager",
32
+ "VagrantPluginManager",
33
+ ]
@@ -0,0 +1,25 @@
1
+ """apt package manager implementation."""
2
+
3
+ from .base import PackageManager
4
+
5
+
6
+ class AptManager(PackageManager):
7
+ """Manager for apt packages."""
8
+
9
+ def is_available(self) -> bool:
10
+ """Check if apt is available."""
11
+ return self.run_command(["which", "apt"])
12
+
13
+ def update(self) -> bool:
14
+ """Update apt package lists."""
15
+ if not self._check_available("update"):
16
+ return False
17
+ return self.run_command(self.commands.get("update", ["sudo", "apt", "update"]))
18
+
19
+ def upgrade(self) -> bool:
20
+ """Upgrade apt packages."""
21
+ if not self._check_available("upgrade"):
22
+ return False
23
+ return self.run_command(
24
+ self.commands.get("upgrade", ["sudo", "apt", "upgrade", "-y"])
25
+ )
@@ -0,0 +1,92 @@
1
+ import logging
2
+ import subprocess
3
+ from abc import ABC, abstractmethod
4
+ from typing import Optional
5
+
6
+
7
+ class PackageManager(ABC):
8
+ """Base class for all package managers."""
9
+
10
+ def __init__(self, config: dict):
11
+ """Initialize the package manager with its configuration."""
12
+ self.config = config
13
+ self.enabled = config.get("enabled", True)
14
+ self.commands = config.get("commands", {})
15
+ self.verbose = config.get("verbose", False)
16
+
17
+ def run_command(self, command: list[str]) -> bool:
18
+ """Run a command and return True if it succeeded."""
19
+ if not command:
20
+ return True
21
+
22
+ try:
23
+ if self.verbose:
24
+ logging.info(f"Running command: {' '.join(command)}")
25
+ result = subprocess.run(
26
+ command,
27
+ capture_output=True,
28
+ text=True,
29
+ check=True,
30
+ )
31
+ if result.stdout:
32
+ logging.info(
33
+ f"INFO - stdout from {' '.join(command)}:\n{result.stdout}"
34
+ )
35
+ return True
36
+ except subprocess.CalledProcessError as e:
37
+ if self.verbose:
38
+ logging.error(f"Command failed with exit code {e.returncode}")
39
+ if e.stdout:
40
+ logging.error(f"stdout: {e.stdout}")
41
+ if e.stderr:
42
+ logging.error(f"stderr: {e.stderr}")
43
+ if e.stdout:
44
+ logging.error(f"ERROR - stdout from {' '.join(command)}:\n{e.stdout}")
45
+ if e.stderr:
46
+ logging.error(f"ERROR - stderr from {' '.join(command)}:\n{e.stderr}")
47
+ return False
48
+
49
+ def run_command_with_output(
50
+ self, command: list[str]
51
+ ) -> tuple[bool, Optional[str], Optional[str]]:
52
+ """Run a command and return success status and output."""
53
+ try:
54
+ result = subprocess.run(
55
+ command,
56
+ capture_output=True,
57
+ text=True,
58
+ check=True,
59
+ )
60
+ return True, result.stdout, result.stderr
61
+ except subprocess.CalledProcessError as e:
62
+ return False, e.stdout, e.stderr
63
+
64
+ def _check_available(self, operation: str) -> bool:
65
+ """Check if package manager is available and log if not.
66
+
67
+ Args:
68
+ operation: Name of the operation being attempted (e.g., 'update', 'upgrade')
69
+
70
+ Returns:
71
+ bool: True if available, False if not
72
+ """
73
+ if not self.is_available():
74
+ manager_name = self.__class__.__name__.replace("Manager", "").lower()
75
+ logging.info(f"{manager_name} is not available. Skipping {operation}.")
76
+ return False
77
+ return True
78
+
79
+ @abstractmethod
80
+ def is_available(self) -> bool:
81
+ """Check if this package manager is available on the system."""
82
+ pass
83
+
84
+ @abstractmethod
85
+ def update(self) -> bool:
86
+ """Update package lists/indices."""
87
+ pass
88
+
89
+ @abstractmethod
90
+ def upgrade(self) -> bool:
91
+ """Upgrade installed packages."""
92
+ pass
@@ -0,0 +1,41 @@
1
+ """basher package manager implementation."""
2
+
3
+ import logging
4
+ import subprocess
5
+
6
+ from .base import PackageManager
7
+
8
+
9
+ class BasherManager(PackageManager):
10
+ def is_available(self) -> bool:
11
+ return self.run_command(["which", "basher"])
12
+
13
+ def update(self) -> bool:
14
+ """Update Homebrew package lists."""
15
+ if not self.is_available():
16
+ return False
17
+ return self.run_command(
18
+ self.commands.get("update", ["bash", "-c", "cd ~/.basher && git pull"])
19
+ )
20
+
21
+ def upgrade(self) -> bool:
22
+ if not self.is_available():
23
+ logging.info("basher is not installed. Skipping.")
24
+ return False
25
+
26
+ success = True
27
+ try:
28
+ result = subprocess.run(
29
+ ["basher", "outdated"], capture_output=True, text=True, check=True
30
+ )
31
+ outdated_packages = (
32
+ result.stdout.strip().split("\n") if result.stdout else []
33
+ )
34
+ upgrade_command = self.commands.get("upgrade", ["basher", "upgrade"])
35
+ for package in outdated_packages:
36
+ cmd = upgrade_command + [package]
37
+ success &= self.run_command(cmd)
38
+ except subprocess.CalledProcessError:
39
+ logging.error("Failed to get outdated basher packages")
40
+ success = False
41
+ return success
@@ -0,0 +1,22 @@
1
+ """Homebrew package manager implementation."""
2
+ from .base import PackageManager
3
+
4
+
5
+ class HomebrewManager(PackageManager):
6
+ """Manager for Homebrew packages."""
7
+
8
+ def is_available(self) -> bool:
9
+ """Check if Homebrew is installed."""
10
+ return self.run_command(["which", "brew"])
11
+
12
+ def update(self) -> bool:
13
+ """Update Homebrew package lists."""
14
+ if not self.is_available():
15
+ return False
16
+ return self.run_command(self.commands.get("update", ["brew", "update"]))
17
+
18
+ def upgrade(self) -> bool:
19
+ """Upgrade Homebrew packages."""
20
+ if not self.is_available():
21
+ return False
22
+ return self.run_command(self.commands.get("upgrade", ["brew", "upgrade"]))
@@ -0,0 +1,65 @@
1
+ """cargo package manager implementation."""
2
+
3
+ import logging
4
+ import subprocess
5
+
6
+ from .base import PackageManager
7
+
8
+
9
+ class CargoManager(PackageManager):
10
+ """Manager for cargo packages."""
11
+
12
+ def is_available(self) -> bool:
13
+ """Check if cargo is available."""
14
+ return self.run_command(["which", "cargo"])
15
+
16
+ def update(self) -> bool:
17
+ """Update cargo package lists."""
18
+ if not self.is_available():
19
+ return False
20
+ return self.run_command(self.commands.get("update", ["rustup", "update"]))
21
+
22
+ def upgrade(self) -> bool:
23
+ """Upgrade cargo packages."""
24
+ if not self.is_available():
25
+ logging.info("cargo is not installed. Skipping.")
26
+ return False
27
+
28
+ success = True
29
+ # First update rustup
30
+ success &= self.run_command(self.commands.get("update", ["rustup", "update"]))
31
+
32
+ # Check if upgrade command is configured (even if empty list)
33
+ if "upgrade" not in self.commands:
34
+ logging.info("cargo upgrade command not configured. Skipping.")
35
+ return success
36
+
37
+ # Get list of installed packages
38
+ try:
39
+ result = subprocess.run(
40
+ ["cargo", "install", "--list"],
41
+ capture_output=True,
42
+ text=True,
43
+ check=True,
44
+ )
45
+ # Parse output to get package names
46
+ # Output format is like:
47
+ # package-name v1.2.3:
48
+ # package-name
49
+ packages = []
50
+ for line in result.stdout.split("\n"):
51
+ if ":" in line: # This line contains a package name
52
+ package = line.split(" ")[0].strip()
53
+ packages.append(package)
54
+
55
+ # Update each package
56
+ for package in packages:
57
+ if self.verbose:
58
+ logging.info(f"Updating cargo package: {package}")
59
+ success &= self.run_command(["cargo", "install", package])
60
+
61
+ except subprocess.CalledProcessError:
62
+ logging.error("Failed to list cargo packages")
63
+ success = False
64
+
65
+ return success
@@ -0,0 +1,32 @@
1
+ """gem package manager implementation."""
2
+
3
+ import logging
4
+
5
+ from .base import PackageManager
6
+
7
+
8
+ class GemManager(PackageManager):
9
+ """Manager for gem packages."""
10
+
11
+ def is_available(self) -> bool:
12
+ """Check if gem is available."""
13
+ return self.run_command(["which", "gem"])
14
+
15
+ def update(self) -> bool:
16
+ """Update RubyGems system."""
17
+ if not self._check_available("update"):
18
+ return False
19
+ # First update RubyGems itself
20
+ success = self.run_command(
21
+ self.commands.get("update", ["gem", "update", "--system"])
22
+ )
23
+ if not success:
24
+ logging.error("Failed to update RubyGems system")
25
+ return success
26
+
27
+ def upgrade(self) -> bool:
28
+ """Upgrade installed gems."""
29
+ if not self._check_available("upgrade"):
30
+ return False
31
+ # Update all installed gems
32
+ return self.run_command(self.commands.get("upgrade", ["gem", "update"]))
@@ -0,0 +1,26 @@
1
+ """gh-cli package manager implementation."""
2
+ from .base import PackageManager
3
+
4
+
5
+ class GhCliManager(PackageManager):
6
+ """Manager for GitHub CLI."""
7
+
8
+ def is_available(self) -> bool:
9
+ """Check if gh is available."""
10
+ return self.run_command(["which", "gh"])
11
+
12
+ def update(self) -> bool:
13
+ """Update GitHub CLI."""
14
+ if not self.is_available():
15
+ return False
16
+ return self.run_command(
17
+ self.commands.get("update", ["gh", "extension", "upgrade", "--all"])
18
+ )
19
+
20
+ def upgrade(self) -> bool:
21
+ """Upgrade GitHub CLI extensions."""
22
+ if not self.is_available():
23
+ return False
24
+ return self.run_command(
25
+ self.commands.get("upgrade", ["gh", "extension", "upgrade", "--all"])
26
+ )
@@ -0,0 +1,134 @@
1
+ import logging
2
+ import os
3
+ import subprocess
4
+
5
+ from .base import PackageManager
6
+
7
+
8
+ class GoManager(PackageManager):
9
+ """Manager for Go packages."""
10
+
11
+ # Special case mappings for known tools that need specific module paths
12
+ SPECIAL_CASES: dict[str, str] = {
13
+ "staticcheck": "honnef.co/go/tools/cmd/staticcheck",
14
+ }
15
+
16
+ def is_available(self) -> bool:
17
+ """Check if Go is installed."""
18
+ return self.run_command(["which", "go"])
19
+
20
+ def update(self) -> bool:
21
+ """Go itself doesn't need updating, that's handled by the system package manager."""
22
+ return True
23
+
24
+ def upgrade(self) -> bool:
25
+ """Upgrade all globally installed Go packages."""
26
+ if not self.is_available():
27
+ logging.info("go is not installed. Skipping.")
28
+ return False
29
+
30
+ success = True
31
+ try:
32
+ # First try to get GOPATH
33
+ result = subprocess.run(
34
+ ["go", "env", "GOPATH"],
35
+ capture_output=True,
36
+ text=True,
37
+ check=True,
38
+ )
39
+ gopath = result.stdout.strip() or os.path.expanduser(
40
+ "~/go"
41
+ ) # Default GOPATH
42
+ logging.info(f"Using GOPATH: {gopath}")
43
+
44
+ # Get list of binaries in GOPATH/bin
45
+ bin_dir = os.path.join(gopath, "bin")
46
+ if not os.path.exists(bin_dir):
47
+ logging.info(f"No Go binaries found in {bin_dir}")
48
+ return True
49
+
50
+ binaries = [
51
+ binary
52
+ for binary in os.listdir(bin_dir)
53
+ if not binary.startswith(".")
54
+ and os.path.isfile(os.path.join(bin_dir, binary))
55
+ ]
56
+ if not binaries:
57
+ logging.info("No Go binaries found to update")
58
+ return True
59
+
60
+ logging.info(f"Found Go binaries: {', '.join(binaries)}")
61
+
62
+ # For each binary, try to find its package and update it
63
+ for binary in binaries:
64
+ try:
65
+ # Use go version -m to get module info
66
+ binary_path = os.path.join(bin_dir, binary)
67
+ logging.info(f"Getting module info for: {binary}")
68
+ result = subprocess.run(
69
+ ["go", "version", "-m", binary_path],
70
+ capture_output=True,
71
+ text=True,
72
+ check=True,
73
+ )
74
+ logging.debug(f"Module info output:\n{result.stdout}")
75
+
76
+ # Check special cases first
77
+ if binary in self.SPECIAL_CASES:
78
+ module_path = self.SPECIAL_CASES[binary]
79
+ logging.info(
80
+ f"Using special case path for {binary}: {module_path}"
81
+ )
82
+ else:
83
+ module_path = next(
84
+ (
85
+ line.strip().split("\t")[1]
86
+ for line in result.stdout.split("\n")
87
+ if line.strip().startswith("mod\t")
88
+ ),
89
+ None,
90
+ )
91
+
92
+ if module_path:
93
+ logging.info(f"Found module path: {module_path}")
94
+ # Install latest version using direct subprocess call
95
+ try:
96
+ logging.info(f"Attempting to update {module_path}")
97
+ result = subprocess.run(
98
+ ["go", "install", f"{module_path}@latest"],
99
+ capture_output=True,
100
+ text=True,
101
+ check=True,
102
+ )
103
+ # Check if there was meaningful stderr output
104
+ if result.stderr and not (
105
+ "go: downloading" in result.stderr
106
+ or "go: found" in result.stderr
107
+ ):
108
+ logging.error(
109
+ f"Error updating {module_path}: {result.stderr}"
110
+ )
111
+ success = False
112
+ else:
113
+ if result.stdout:
114
+ logging.info(f"Update output: {result.stdout}")
115
+ if result.stderr:
116
+ logging.info(f"Update info: {result.stderr}")
117
+ except subprocess.CalledProcessError as e:
118
+ logging.error(f"Failed to update {module_path}: {e.stderr}")
119
+ success = False
120
+ else:
121
+ logging.warning(
122
+ f"Could not determine package for binary: {binary}"
123
+ )
124
+ except subprocess.CalledProcessError as e:
125
+ logging.warning(
126
+ f"Failed to get module info for {binary}: {e.stderr}"
127
+ )
128
+ success = False
129
+
130
+ except subprocess.CalledProcessError as e:
131
+ logging.error(f"Error during Go package updates: {e}")
132
+ success = False
133
+
134
+ return success
@@ -0,0 +1,26 @@
1
+ """kubectl-krew package manager implementation."""
2
+ from .base import PackageManager
3
+
4
+
5
+ class KubectlKrewManager(PackageManager):
6
+ """Manager for kubectl-krew packages."""
7
+
8
+ def is_available(self) -> bool:
9
+ """Check if kubectl-krew is available."""
10
+ return self.run_command(["which", "kubectl-krew"])
11
+
12
+ def update(self) -> bool:
13
+ """Update kubectl-krew package lists."""
14
+ if not self.is_available():
15
+ return False
16
+ return self.run_command(
17
+ self.commands.get("update", ["kubectl", "krew", "update"])
18
+ )
19
+
20
+ def upgrade(self) -> bool:
21
+ """Upgrade kubectl-krew packages."""
22
+ if not self.is_available():
23
+ return False
24
+ return self.run_command(
25
+ self.commands.get("upgrade", ["kubectl", "krew", "upgrade"])
26
+ )
@@ -0,0 +1,28 @@
1
+ """micro-editor package manager implementation."""
2
+
3
+ from .base import PackageManager
4
+
5
+
6
+ class MicroEditorManager(PackageManager):
7
+ """Manager for micro-editor packages."""
8
+
9
+ def is_available(self) -> bool:
10
+ """Check if micro is available."""
11
+ return self.run_command(["which", "micro"])
12
+
13
+ def update(self) -> bool:
14
+ """Update micro-editor package lists."""
15
+ if not self.is_available():
16
+ return False
17
+ return self.run_command(
18
+ self.commands.get("update", ["micro", "-plugin", "update"])
19
+ )
20
+
21
+ def upgrade(self) -> bool:
22
+ """Upgrade micro-editor packages."""
23
+ if not self.is_available():
24
+ return False
25
+ # Micro editor's plugin update command handles both update and upgrade
26
+ return self.run_command(
27
+ self.commands.get("upgrade", ["micro", "-plugin", "update"])
28
+ )
@@ -0,0 +1,23 @@
1
+ """npm package manager implementation."""
2
+ from .base import PackageManager
3
+
4
+
5
+ class NpmManager(PackageManager):
6
+ """Manager for npm packages."""
7
+
8
+ def is_available(self) -> bool:
9
+ """Check if npm is available."""
10
+ return self.run_command(["which", "npm"])
11
+
12
+ def update(self) -> bool:
13
+ """Update npm package lists."""
14
+ if not self.is_available():
15
+ return False
16
+ return self.run_command(self.commands.get("update", ["npm", "update", "-g"]))
17
+
18
+ def upgrade(self) -> bool:
19
+ """Upgrade npm packages."""
20
+ if not self.is_available():
21
+ return False
22
+ # npm update -g handles both update and upgrade
23
+ return self.run_command(self.commands.get("upgrade", ["npm", "update", "-g"]))
@@ -0,0 +1,114 @@
1
+ """pip package manager implementation."""
2
+
3
+ import json
4
+ import logging
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ from .base import PackageManager
9
+
10
+
11
+ class PipManager(PackageManager):
12
+ """Manager for pip packages."""
13
+
14
+ def __init__(self, config: dict):
15
+ super().__init__(config)
16
+ self.virtualenv = config.get("virtualenv")
17
+ self.pyenv = config.get("pyenv")
18
+
19
+ def is_available(self) -> bool:
20
+ """Check if pip is available."""
21
+ return self.run_command(["which", "pip"])
22
+
23
+ def _get_pip_command(self) -> list[str]:
24
+ """Get the correct pip command based on virtualenv/pyenv settings."""
25
+ if self.virtualenv:
26
+ return [str(Path(self.virtualenv) / "bin" / "pip")]
27
+ return ["pip"]
28
+
29
+ def _check_available(self, operation: str) -> bool:
30
+ """Check if pip is available for the given operation."""
31
+ if not self.is_available():
32
+ logging.info(f"pip is not installed. Skipping {operation}.")
33
+ return False
34
+ return True
35
+
36
+ def update(self) -> bool:
37
+ """Update pip package lists.
38
+
39
+ pip doesn't have a separate update operation, as it checks PyPI
40
+ directly when installing or upgrading packages.
41
+ """
42
+ if not self._check_available("update"):
43
+ return False
44
+ # pip doesn't need a separate update operation
45
+ return self.run_command(self.commands.get("update", []))
46
+
47
+ def upgrade(self) -> bool:
48
+ """Upgrade pip packages."""
49
+ if not self._check_available("upgrade"):
50
+ return False
51
+
52
+ success = True
53
+ pip_cmd = self._get_pip_command()
54
+
55
+ try:
56
+ if self.verbose:
57
+ logging.info("Checking for outdated packages...")
58
+ # Get list of outdated packages using JSON format
59
+ result = subprocess.run(
60
+ pip_cmd + ["list", "--outdated", "--format=json"],
61
+ capture_output=True,
62
+ text=True,
63
+ check=True,
64
+ )
65
+
66
+ try:
67
+ packages = json.loads(result.stdout)
68
+ if not packages:
69
+ logging.info("No outdated packages found.")
70
+ return True
71
+
72
+ if self.verbose:
73
+ package_names = [pkg["name"] for pkg in packages]
74
+ logging.info(
75
+ f"Found {len(packages)} outdated packages: {', '.join(package_names)}"
76
+ )
77
+
78
+ # Upgrade each package
79
+ for package in packages:
80
+ package_name = package["name"]
81
+ if self.verbose:
82
+ current_version = package.get("version", "unknown")
83
+ latest_version = package.get("latest_version", "unknown")
84
+ logging.info(
85
+ f"Upgrading {package_name} from {current_version} to {latest_version}..."
86
+ )
87
+
88
+ try:
89
+ upgrade_cmd = self.commands.get(
90
+ "upgrade", pip_cmd + ["install", "--upgrade"]
91
+ )
92
+ if not self.run_command(upgrade_cmd + [package_name]):
93
+ logging.error(f"Failed to upgrade {package_name}")
94
+ success = False
95
+ except Exception as e:
96
+ logging.error(f"Error upgrading {package_name}: {e}")
97
+ success = False
98
+
99
+ except json.JSONDecodeError as e:
100
+ logging.error(f"Failed to parse JSON output: {e}")
101
+ if self.verbose:
102
+ logging.error(f"Raw output: {result.stdout}")
103
+ success = False
104
+
105
+ except subprocess.CalledProcessError as e:
106
+ logging.error(f"Error listing outdated packages: {e}")
107
+ if e.stderr:
108
+ logging.error(f"Error output: {e.stderr}")
109
+ success = False
110
+ except Exception as e:
111
+ logging.error(f"Unexpected error during pip upgrade: {e}")
112
+ success = False
113
+
114
+ return success
@@ -0,0 +1,22 @@
1
+ """pipx package manager implementation."""
2
+ from .base import PackageManager
3
+
4
+
5
+ class PipxManager(PackageManager):
6
+ """Manager for pipx packages."""
7
+
8
+ def is_available(self) -> bool:
9
+ """Check if pipx is available."""
10
+ return self.run_command(["which", "pipx"])
11
+
12
+ def update(self) -> bool:
13
+ """Update pipx package lists."""
14
+ if not self.is_available():
15
+ return False
16
+ return self.run_command(self.commands.get("update", ["pipx", "upgrade-all"]))
17
+
18
+ def upgrade(self) -> bool:
19
+ """Upgrade pipx packages."""
20
+ if not self.is_available():
21
+ return False
22
+ return self.run_command(self.commands.get("upgrade", ["pipx", "upgrade-all"]))
@@ -0,0 +1,28 @@
1
+ """pkgx package manager implementation."""
2
+
3
+ from .base import PackageManager
4
+
5
+
6
+ class PkgxManager(PackageManager):
7
+ """Manager for pkgx packages."""
8
+
9
+ def is_available(self) -> bool:
10
+ """Check if pkgx is available."""
11
+ return self.run_command(["which", "pkgx"])
12
+
13
+ def update(self) -> bool:
14
+ """Update npm package lists."""
15
+ if not self.is_available():
16
+ return False
17
+ return self.run_command(
18
+ self.commands.get("update", ["pkgx", "mash", "pkgx/cache", "upgrade"])
19
+ )
20
+
21
+ def upgrade(self) -> bool:
22
+ """Upgrade npm packages."""
23
+ if not self.is_available():
24
+ return False
25
+ # npm update -g handles both update and upgrade
26
+ return self.run_command(
27
+ self.commands.get("upgrade", ["pkgx", "mash", "pkgx/cache", "upgrade"])
28
+ )
@@ -0,0 +1,49 @@
1
+ """Registry for package managers."""
2
+
3
+ from .apt import AptManager
4
+ from .base import PackageManager
5
+ from .basher import BasherManager
6
+ from .brew import HomebrewManager
7
+ from .cargo import CargoManager
8
+ from .gem import GemManager
9
+ from .ghcli import GhCliManager
10
+ from .go import GoManager
11
+ from .krew import KubectlKrewManager
12
+ from .micro import MicroEditorManager
13
+ from .npm import NpmManager
14
+ from .pip import PipManager
15
+ from .pipx import PipxManager
16
+ from .pkgx import PkgxManager
17
+ from .snap import SnapManager
18
+ from .tldr import TldrManager
19
+ from .vagrant import VagrantPluginManager
20
+
21
+
22
+ class PackageManagerRegistry:
23
+ """Registry for package managers."""
24
+
25
+ _managers: dict[str, type[PackageManager]] = {
26
+ "apt": AptManager,
27
+ "basher": BasherManager,
28
+ "brew": HomebrewManager,
29
+ "cargo": CargoManager,
30
+ "gem": GemManager,
31
+ "gh-cli": GhCliManager,
32
+ "go": GoManager,
33
+ "krew": KubectlKrewManager,
34
+ "micro": MicroEditorManager,
35
+ "npm": NpmManager,
36
+ "pip": PipManager,
37
+ "pipx": PipxManager,
38
+ "pkgx": PkgxManager,
39
+ "snap": SnapManager,
40
+ "tldr": TldrManager,
41
+ "vagrant": VagrantPluginManager,
42
+ }
43
+
44
+ @classmethod
45
+ def get_manager(cls, name: str, config: dict) -> PackageManager:
46
+ """Get a package manager instance by name."""
47
+ if name not in cls._managers:
48
+ raise ValueError(f"Unknown package manager: {name}")
49
+ return cls._managers[name](config)
@@ -0,0 +1,29 @@
1
+ """snap package manager implementation."""
2
+
3
+ from .base import PackageManager
4
+
5
+
6
+ class SnapManager(PackageManager):
7
+ """Manager for snap packages."""
8
+
9
+ def is_available(self) -> bool:
10
+ """Check if snap is available."""
11
+ return self.run_command(["which", "snap"])
12
+
13
+ def update(self) -> bool:
14
+ """Update snap package lists."""
15
+ if not self.is_available():
16
+ return False
17
+ # snap refresh handles both update and upgrade
18
+ return self.run_command(
19
+ self.commands.get("update", ["sudo", "snap", "refresh"])
20
+ )
21
+
22
+ def upgrade(self) -> bool:
23
+ """Upgrade snap packages."""
24
+ if not self.is_available():
25
+ return False
26
+ # snap refresh handles both update and upgrade
27
+ return self.run_command(
28
+ self.commands.get("upgrade", ["sudo", "snap", "refresh"])
29
+ )
@@ -0,0 +1,24 @@
1
+ """tldr package manager implementation."""
2
+
3
+ from .base import PackageManager
4
+
5
+
6
+ class TldrManager(PackageManager):
7
+ """Manager for tldr pages."""
8
+
9
+ def is_available(self) -> bool:
10
+ """Check if tldr is available."""
11
+ return self.run_command(["which", "tldr"])
12
+
13
+ def update(self) -> bool:
14
+ """Update tldr pages cache."""
15
+ if not self.is_available():
16
+ return False
17
+ return self.run_command(self.commands.get("update", ["tldr", "--update"]))
18
+
19
+ def upgrade(self) -> bool:
20
+ """Upgrade tldr pages cache."""
21
+ if not self.is_available():
22
+ return False
23
+ # tldr --update handles both update and upgrade
24
+ return self.run_command(self.commands.get("upgrade", ["tldr", "--update"]))
@@ -0,0 +1,21 @@
1
+ """vagrant package manager implementation."""
2
+
3
+ from .base import PackageManager
4
+
5
+
6
+ class VagrantPluginManager(PackageManager):
7
+ """Manager for vagrant packages."""
8
+
9
+ def is_available(self) -> bool:
10
+ """Check if vagrant is available."""
11
+ return self.run_command(["which", "vagrant"])
12
+
13
+ def update(self) -> bool:
14
+ if not self.is_available():
15
+ return False
16
+ return self.run_command(["vagrant", "plugin", "update"])
17
+
18
+ def upgrade(self) -> bool:
19
+ if not self.is_available():
20
+ return False
21
+ return self.run_command(["vagrant", "plugin", "update"])
@@ -0,0 +1,70 @@
1
+ [tool.poetry]
2
+ name = "one-updater"
3
+ version = "0.0.8"
4
+ description = "One tool many packages"
5
+ authors = ["Tim Bryant <timothybryant3@gmail.com>"]
6
+ readme = "README.md"
7
+ packages = [{include = "one_updater", from = "."}]
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.11"
11
+ click = "^8.1.7"
12
+ pyyaml = "^6.0.2"
13
+ rich = "^13.9.4"
14
+ python-semantic-release = "^9.14.0"
15
+
16
+ [tool.poetry.group.dev.dependencies]
17
+ autopep8 = "^2.3.1"
18
+ black = "^24.8.0"
19
+ pytest = "^8.3.3"
20
+ pre-commit = "^3.8.0"
21
+ isort = "^5.13.2"
22
+
23
+ [build-system]
24
+ requires = ["poetry-core"]
25
+ build-backend = "poetry.core.masonry.api"
26
+
27
+ [tool.pytest.ini_options]
28
+ pythonpath = [
29
+ ".", "one_updater"
30
+ ]
31
+ filterwarnings = [
32
+ "error",
33
+ "ignore::RuntimeWarning",
34
+ "ignore::DeprecationWarning",
35
+ ]
36
+
37
+ [tool.poetry.scripts]
38
+ one-updater = "one_updater.cli:cli"
39
+
40
+ [tool.semantic_release.commit_parser_options]
41
+ allowed_tags = [
42
+ "build",
43
+ "chore",
44
+ "refactor",
45
+ "fix",
46
+ "perf",
47
+ "style",
48
+ "docs",
49
+ "ci",
50
+ "test",
51
+ "feat",
52
+ ":boom:",
53
+ "BREAKING_CHANGE",
54
+ ]
55
+ major_tags = [":boom:", "BREAKING_CHANGE"]
56
+ minor_tags = ["feat"]
57
+ patch_tags = ["fix", "perf", "style", "docs", "ci", "test", "refactor", "chore", "build"]
58
+
59
+ [tool.semantic_release]
60
+ version_toml = [
61
+ "pyproject.toml:tool.poetry.version",
62
+ ]
63
+ branch = "main"
64
+ changelog_file = "CHANGELOG.md"
65
+ build_command = "poetry build"
66
+ dist_path = "dist/"
67
+ upload_to_vcs_release = true
68
+ upload_to_pypi = false
69
+ remove_dist = false
70
+ patch_without_tag = true