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.
- one_updater-0.0.8/PKG-INFO +116 -0
- one_updater-0.0.8/README.md +98 -0
- one_updater-0.0.8/one_updater/__init__.py +3 -0
- one_updater-0.0.8/one_updater/cli.py +155 -0
- one_updater-0.0.8/one_updater/package_managers/__init__.py +33 -0
- one_updater-0.0.8/one_updater/package_managers/apt.py +25 -0
- one_updater-0.0.8/one_updater/package_managers/base.py +92 -0
- one_updater-0.0.8/one_updater/package_managers/basher.py +41 -0
- one_updater-0.0.8/one_updater/package_managers/brew.py +22 -0
- one_updater-0.0.8/one_updater/package_managers/cargo.py +65 -0
- one_updater-0.0.8/one_updater/package_managers/gem.py +32 -0
- one_updater-0.0.8/one_updater/package_managers/ghcli.py +26 -0
- one_updater-0.0.8/one_updater/package_managers/go.py +134 -0
- one_updater-0.0.8/one_updater/package_managers/krew.py +26 -0
- one_updater-0.0.8/one_updater/package_managers/micro.py +28 -0
- one_updater-0.0.8/one_updater/package_managers/npm.py +23 -0
- one_updater-0.0.8/one_updater/package_managers/pip.py +114 -0
- one_updater-0.0.8/one_updater/package_managers/pipx.py +22 -0
- one_updater-0.0.8/one_updater/package_managers/pkgx.py +28 -0
- one_updater-0.0.8/one_updater/package_managers/registry.py +49 -0
- one_updater-0.0.8/one_updater/package_managers/snap.py +29 -0
- one_updater-0.0.8/one_updater/package_managers/tldr.py +24 -0
- one_updater-0.0.8/one_updater/package_managers/vagrant.py +21 -0
- one_updater-0.0.8/pyproject.toml +70 -0
|
@@ -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,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
|