dev-setup 1.0.0__py3-none-any.whl
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.
- dev_setup/__init__.py +1 -0
- dev_setup/__main__.py +9 -0
- dev_setup/base.py +78 -0
- dev_setup/cli.py +38 -0
- dev_setup/commands/__init__.py +0 -0
- dev_setup/commands/add_cmd.py +185 -0
- dev_setup/commands/delete_cmd.py +44 -0
- dev_setup/commands/help_cmd.py +46 -0
- dev_setup/commands/install_cmd.py +96 -0
- dev_setup/commands/list_cmd.py +57 -0
- dev_setup/commands/remove_cmd.py +50 -0
- dev_setup/generic.py +302 -0
- dev_setup/packages/__init__.py +0 -0
- dev_setup/packages/aws_cli.py +73 -0
- dev_setup/packages/docker.py +93 -0
- dev_setup/packages/htop.py +53 -0
- dev_setup/packages/nvm.py +84 -0
- dev_setup/packages/php.py +65 -0
- dev_setup/packages/saml2aws.py +100 -0
- dev_setup/packages/starship.py +65 -0
- dev_setup/packages/uv_tool.py +68 -0
- dev_setup/registry.py +78 -0
- dev_setup/ui.py +97 -0
- dev_setup-1.0.0.dist-info/METADATA +358 -0
- dev_setup-1.0.0.dist-info/RECORD +27 -0
- dev_setup-1.0.0.dist-info/WHEEL +4 -0
- dev_setup-1.0.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import platform
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import tarfile
|
|
8
|
+
import tempfile
|
|
9
|
+
import urllib.request
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from dev_setup.base import Tool
|
|
14
|
+
|
|
15
|
+
_INSTALL_PATH = Path("/usr/local/bin/saml2aws")
|
|
16
|
+
_RELEASES_API = "https://api.github.com/repos/Versent/saml2aws/releases/latest"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Saml2AwsTool(Tool):
|
|
20
|
+
key = "saml2aws"
|
|
21
|
+
name = "saml2aws"
|
|
22
|
+
description = "SAML → AWS STS credentials CLI (Versent)"
|
|
23
|
+
category = "tools"
|
|
24
|
+
install_type = "script"
|
|
25
|
+
help_cmd = "saml2aws --help"
|
|
26
|
+
|
|
27
|
+
def is_installed(self) -> bool:
|
|
28
|
+
return shutil.which("saml2aws") is not None
|
|
29
|
+
|
|
30
|
+
def get_version(self) -> str:
|
|
31
|
+
r = subprocess.run(["saml2aws", "--version"], capture_output=True, text=True)
|
|
32
|
+
out = r.stdout.strip() or r.stderr.strip()
|
|
33
|
+
return out.splitlines()[0] if out else ""
|
|
34
|
+
|
|
35
|
+
def install(self) -> Optional[str]:
|
|
36
|
+
from dev_setup import ui
|
|
37
|
+
|
|
38
|
+
version = self._latest_version()
|
|
39
|
+
arch = "arm64" if platform.machine() == "aarch64" else "amd64"
|
|
40
|
+
filename = f"saml2aws_{version}_linux_{arch}.tar.gz"
|
|
41
|
+
url = (
|
|
42
|
+
f"https://github.com/Versent/saml2aws/releases/download/"
|
|
43
|
+
f"v{version}/{filename}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
47
|
+
archive = Path(tmpdir) / filename
|
|
48
|
+
|
|
49
|
+
with ui.spinner(f"Downloading saml2aws v{version}..."):
|
|
50
|
+
subprocess.run(
|
|
51
|
+
["curl", "-fsSL", "-o", str(archive), url],
|
|
52
|
+
check=True, capture_output=True,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
with ui.spinner("Extracting saml2aws..."):
|
|
56
|
+
with tarfile.open(archive) as tf:
|
|
57
|
+
tf.extractall(tmpdir)
|
|
58
|
+
|
|
59
|
+
binary = Path(tmpdir) / "saml2aws"
|
|
60
|
+
if not binary.exists():
|
|
61
|
+
raise RuntimeError(f"saml2aws binary not found in archive")
|
|
62
|
+
|
|
63
|
+
with ui.spinner("Installing saml2aws to /usr/local/bin..."):
|
|
64
|
+
subprocess.run(
|
|
65
|
+
["sudo", "mv", str(binary), str(_INSTALL_PATH)],
|
|
66
|
+
check=True, capture_output=True,
|
|
67
|
+
)
|
|
68
|
+
subprocess.run(
|
|
69
|
+
["sudo", "chmod", "+x", str(_INSTALL_PATH)],
|
|
70
|
+
check=True, capture_output=True,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if not self.is_installed():
|
|
74
|
+
raise RuntimeError("saml2aws installation failed — binary not found")
|
|
75
|
+
return self.get_version()
|
|
76
|
+
|
|
77
|
+
def remove(self) -> None:
|
|
78
|
+
from dev_setup import ui
|
|
79
|
+
|
|
80
|
+
if not _INSTALL_PATH.exists():
|
|
81
|
+
raise RuntimeError(f"saml2aws not found at {_INSTALL_PATH}")
|
|
82
|
+
|
|
83
|
+
with ui.spinner("Removing saml2aws..."):
|
|
84
|
+
subprocess.run(
|
|
85
|
+
["sudo", "rm", "-f", str(_INSTALL_PATH)],
|
|
86
|
+
check=True, capture_output=True,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _latest_version() -> str:
|
|
91
|
+
try:
|
|
92
|
+
req = urllib.request.Request(
|
|
93
|
+
_RELEASES_API, headers={"User-Agent": "dev-setup"}
|
|
94
|
+
)
|
|
95
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
96
|
+
data = json.loads(resp.read())
|
|
97
|
+
tag = data["tag_name"]
|
|
98
|
+
return tag.lstrip("v")
|
|
99
|
+
except Exception:
|
|
100
|
+
return "2.36.6"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from dev_setup.base import Tool, patch_bashrc, remove_bashrc_block
|
|
9
|
+
|
|
10
|
+
STARSHIP_BLOCK = "Starship prompt"
|
|
11
|
+
STARSHIP_INIT_LINE = 'eval "$(starship init bash)"'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StarshipTool(Tool):
|
|
15
|
+
key = "starship"
|
|
16
|
+
name = "Starship"
|
|
17
|
+
description = "Fast, cross-shell customizable prompt"
|
|
18
|
+
category = "tools"
|
|
19
|
+
install_type = "script"
|
|
20
|
+
help_cmd = "starship --help"
|
|
21
|
+
|
|
22
|
+
def is_installed(self) -> bool:
|
|
23
|
+
return shutil.which("starship") is not None
|
|
24
|
+
|
|
25
|
+
def get_version(self) -> str:
|
|
26
|
+
r = subprocess.run(["starship", "--version"], capture_output=True, text=True)
|
|
27
|
+
return r.stdout.strip().splitlines()[0] if r.returncode == 0 else ""
|
|
28
|
+
|
|
29
|
+
def install(self) -> Optional[str]:
|
|
30
|
+
from dev_setup import ui
|
|
31
|
+
|
|
32
|
+
with ui.spinner("Installing Starship..."):
|
|
33
|
+
subprocess.run(
|
|
34
|
+
["bash", "-c", "curl -fsSL https://starship.rs/install.sh | sh -s -- --yes"],
|
|
35
|
+
check=True, capture_output=True,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if not self.is_installed():
|
|
39
|
+
raise RuntimeError("Starship installation failed")
|
|
40
|
+
|
|
41
|
+
added = patch_bashrc(STARSHIP_BLOCK, STARSHIP_INIT_LINE)
|
|
42
|
+
if added:
|
|
43
|
+
ui.info("Starship init added to ~/.bashrc")
|
|
44
|
+
else:
|
|
45
|
+
ui.dim("Starship init already in ~/.bashrc")
|
|
46
|
+
|
|
47
|
+
return self.get_version()
|
|
48
|
+
|
|
49
|
+
def remove(self) -> None:
|
|
50
|
+
from dev_setup import ui
|
|
51
|
+
|
|
52
|
+
for p in [
|
|
53
|
+
Path("/usr/local/bin/starship"),
|
|
54
|
+
Path.home() / ".cargo" / "bin" / "starship",
|
|
55
|
+
Path.home() / ".local" / "bin" / "starship",
|
|
56
|
+
]:
|
|
57
|
+
if p.exists():
|
|
58
|
+
if str(p).startswith("/usr"):
|
|
59
|
+
subprocess.run(["sudo", "rm", "-f", str(p)], capture_output=True)
|
|
60
|
+
else:
|
|
61
|
+
p.unlink()
|
|
62
|
+
ui.dim(f"Removed: {p}")
|
|
63
|
+
|
|
64
|
+
remove_bashrc_block(STARSHIP_BLOCK)
|
|
65
|
+
ui.info("Starship init removed from ~/.bashrc")
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from dev_setup.base import Tool, patch_bashrc
|
|
9
|
+
|
|
10
|
+
UV_PATH_BLOCK = "uv (and other ~/.local/bin tools)"
|
|
11
|
+
UV_PATH_LINE = 'export PATH="$HOME/.local/bin:$PATH"'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UvTool(Tool):
|
|
15
|
+
key = "uv"
|
|
16
|
+
name = "uv"
|
|
17
|
+
description = "Astral Python package and project manager"
|
|
18
|
+
category = "core"
|
|
19
|
+
install_type = "script"
|
|
20
|
+
help_cmd = "uv --help"
|
|
21
|
+
|
|
22
|
+
def is_installed(self) -> bool:
|
|
23
|
+
return shutil.which("uv") is not None
|
|
24
|
+
|
|
25
|
+
def get_version(self) -> str:
|
|
26
|
+
r = subprocess.run(["uv", "--version"], capture_output=True, text=True)
|
|
27
|
+
return r.stdout.strip() if r.returncode == 0 else ""
|
|
28
|
+
|
|
29
|
+
def install(self) -> Optional[str]:
|
|
30
|
+
from dev_setup import ui
|
|
31
|
+
|
|
32
|
+
with ui.spinner("Installing uv..."):
|
|
33
|
+
subprocess.run(
|
|
34
|
+
["bash", "-c", "curl -LsSf https://astral.sh/uv/install.sh | sh"],
|
|
35
|
+
check=True, capture_output=True,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
import os
|
|
39
|
+
path = os.environ.get("PATH", "")
|
|
40
|
+
local_bin = str(Path.home() / ".local" / "bin")
|
|
41
|
+
if local_bin not in path:
|
|
42
|
+
os.environ["PATH"] = f"{local_bin}:{path}"
|
|
43
|
+
|
|
44
|
+
if not shutil.which("uv"):
|
|
45
|
+
raise RuntimeError("uv binary not found after install — add ~/.local/bin to PATH")
|
|
46
|
+
|
|
47
|
+
patch_bashrc(UV_PATH_BLOCK, UV_PATH_LINE)
|
|
48
|
+
ui.info("~/.local/bin added to PATH in ~/.bashrc")
|
|
49
|
+
|
|
50
|
+
return self.get_version()
|
|
51
|
+
|
|
52
|
+
def remove(self) -> None:
|
|
53
|
+
from dev_setup import ui
|
|
54
|
+
|
|
55
|
+
removed = False
|
|
56
|
+
for p in [
|
|
57
|
+
Path.home() / ".local" / "bin" / "uv",
|
|
58
|
+
Path.home() / ".local" / "bin" / "uvx",
|
|
59
|
+
Path.home() / ".cargo" / "bin" / "uv",
|
|
60
|
+
Path.home() / ".cargo" / "bin" / "uvx",
|
|
61
|
+
]:
|
|
62
|
+
if p.exists():
|
|
63
|
+
p.unlink()
|
|
64
|
+
ui.dim(f"Removed: {p}")
|
|
65
|
+
removed = True
|
|
66
|
+
|
|
67
|
+
if not removed:
|
|
68
|
+
raise RuntimeError("uv binary not found in ~/.local/bin or ~/.cargo/bin")
|
dev_setup/registry.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import json
|
|
5
|
+
import pkgutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from dev_setup.base import Tool
|
|
10
|
+
|
|
11
|
+
_registry: Dict[str, Tool] = {}
|
|
12
|
+
_order: List[str] = []
|
|
13
|
+
_initialized = False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _register(tool: Tool) -> None:
|
|
17
|
+
if tool.key not in _registry:
|
|
18
|
+
_registry[tool.key] = tool
|
|
19
|
+
_order.append(tool.key)
|
|
20
|
+
else:
|
|
21
|
+
_registry[tool.key] = tool
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _load_builtins() -> None:
|
|
25
|
+
from dev_setup import packages as pkg_ns
|
|
26
|
+
|
|
27
|
+
for finder, name, _ in pkgutil.iter_modules(pkg_ns.__path__): # type: ignore[attr-defined]
|
|
28
|
+
module = importlib.import_module(f"dev_setup.packages.{name}")
|
|
29
|
+
for attr in dir(module):
|
|
30
|
+
obj = getattr(module, attr)
|
|
31
|
+
if (
|
|
32
|
+
isinstance(obj, type)
|
|
33
|
+
and issubclass(obj, Tool)
|
|
34
|
+
and obj is not Tool
|
|
35
|
+
and getattr(obj, "key", "")
|
|
36
|
+
):
|
|
37
|
+
instance = obj()
|
|
38
|
+
instance.builtin = True
|
|
39
|
+
_register(instance)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _load_custom(config_dir: Path) -> None:
|
|
43
|
+
if not config_dir.is_dir():
|
|
44
|
+
return
|
|
45
|
+
for f in sorted(config_dir.glob("*.json")):
|
|
46
|
+
try:
|
|
47
|
+
data = json.loads(f.read_text())
|
|
48
|
+
from dev_setup.generic import GenericTool
|
|
49
|
+
tool = GenericTool.from_dict(data, key=f.stem)
|
|
50
|
+
tool.builtin = False
|
|
51
|
+
_register(tool)
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def init() -> None:
|
|
57
|
+
global _initialized
|
|
58
|
+
if _initialized:
|
|
59
|
+
return
|
|
60
|
+
_initialized = True
|
|
61
|
+
_load_builtins()
|
|
62
|
+
from dev_setup.generic import _CUSTOM_DIR
|
|
63
|
+
_load_custom(_CUSTOM_DIR)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get(key: str) -> Optional[Tool]:
|
|
67
|
+
init()
|
|
68
|
+
return _registry.get(key)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def all_tools() -> List[Tool]:
|
|
72
|
+
init()
|
|
73
|
+
return [_registry[k] for k in _order if k in _registry]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def exists(key: str) -> bool:
|
|
77
|
+
init()
|
|
78
|
+
return key in _registry
|
dev_setup/ui.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from typing import Generator, Optional
|
|
5
|
+
|
|
6
|
+
import questionary
|
|
7
|
+
from questionary import Style as QStyle
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.rule import Rule
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
console = Console(highlight=False)
|
|
14
|
+
|
|
15
|
+
_STYLE = QStyle([
|
|
16
|
+
("qmark", "fg:#7C3AED bold"),
|
|
17
|
+
("question", "bold"),
|
|
18
|
+
("answer", "fg:#A78BFA bold"),
|
|
19
|
+
("pointer", "fg:#7C3AED bold"),
|
|
20
|
+
("highlighted", "fg:#A78BFA bold"),
|
|
21
|
+
("selected", "fg:#A78BFA"),
|
|
22
|
+
("separator", "fg:#6B7280"),
|
|
23
|
+
("instruction", "fg:#6B7280 italic"),
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def info(msg: str) -> None:
|
|
28
|
+
console.print(f" [cyan bold]❯[/] {msg}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def success(msg: str) -> None:
|
|
32
|
+
console.print(f" [green bold]✔[/] {msg}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def warn(msg: str) -> None:
|
|
36
|
+
console.print(f" [yellow bold]⚠[/] {msg}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def error(msg: str) -> None:
|
|
40
|
+
console.print(f" [red bold]✖[/] {msg}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def dim(msg: str) -> None:
|
|
44
|
+
console.print(f" [dim]{msg}[/]")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def section(title: str) -> None:
|
|
48
|
+
console.print()
|
|
49
|
+
console.print(
|
|
50
|
+
Panel(f"[bold]{title}[/]", border_style="bright_magenta", expand=False, padding=(0, 1))
|
|
51
|
+
)
|
|
52
|
+
console.print()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def divider() -> None:
|
|
56
|
+
console.print(Rule(style="dim"))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def print_banner() -> None:
|
|
60
|
+
t = Text()
|
|
61
|
+
t.append(" dev", style="bold bright_magenta")
|
|
62
|
+
t.append("-", style="dim")
|
|
63
|
+
t.append("setup", style="bold white")
|
|
64
|
+
t.append(" v1.0.0", style="dim")
|
|
65
|
+
console.print()
|
|
66
|
+
console.print(Panel(t, border_style="bright_magenta", padding=(0, 2), expand=False))
|
|
67
|
+
console.print()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@contextmanager
|
|
71
|
+
def spinner(label: str) -> Generator[None, None, None]:
|
|
72
|
+
with console.status(f" [dim]{label}[/]", spinner="dots"):
|
|
73
|
+
yield
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def confirm(prompt: str, default: bool = False) -> bool:
|
|
77
|
+
result = questionary.confirm(prompt, default=default, style=_STYLE).ask()
|
|
78
|
+
return bool(result)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def text_input(prompt: str, default: str = "", required: bool = False) -> str:
|
|
82
|
+
while True:
|
|
83
|
+
result = questionary.text(prompt, default=default, style=_STYLE).ask()
|
|
84
|
+
val = (result or "").strip()
|
|
85
|
+
if val or not required:
|
|
86
|
+
return val
|
|
87
|
+
error("This field is required.")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def select(prompt: str, choices: list[str]) -> str:
|
|
91
|
+
result = questionary.select(prompt, choices=choices, style=_STYLE).ask()
|
|
92
|
+
return result or ""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def checkbox(prompt: str, choices: list) -> list:
|
|
96
|
+
result = questionary.checkbox(prompt, choices=choices, style=_STYLE).ask()
|
|
97
|
+
return result or []
|