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.
@@ -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 []