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
dev_setup/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
dev_setup/__main__.py
ADDED
dev_setup/base.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Tool(ABC):
|
|
10
|
+
key: str = ""
|
|
11
|
+
name: str = ""
|
|
12
|
+
description: str = ""
|
|
13
|
+
category: str = "custom"
|
|
14
|
+
install_type: str = "unknown"
|
|
15
|
+
builtin: bool = True
|
|
16
|
+
help_cmd: str = ""
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def is_installed(self) -> bool: ...
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def install(self) -> Optional[str]:
|
|
23
|
+
"""Install the tool. Return version string if available. Raise on failure."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def remove(self) -> None:
|
|
28
|
+
"""Uninstall the tool. Raise on failure."""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
def get_version(self) -> str:
|
|
32
|
+
return ""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_bash(cmd: str, **kwargs) -> subprocess.CompletedProcess:
|
|
36
|
+
return subprocess.run(["bash", "-c", cmd], **kwargs)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def patch_bashrc(block_name: str, content: str) -> bool:
|
|
40
|
+
"""Idempotently append a named block to ~/.bashrc. Returns True if added."""
|
|
41
|
+
bashrc = Path.home() / ".bashrc"
|
|
42
|
+
marker = f"# {block_name}"
|
|
43
|
+
|
|
44
|
+
if bashrc.exists() and marker in bashrc.read_text():
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
with bashrc.open("a") as f:
|
|
48
|
+
f.write(f"\n{marker}\n{content}\n")
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def remove_bashrc_block(block_name: str) -> bool:
|
|
53
|
+
"""Remove a named block (and the line after the marker) from ~/.bashrc."""
|
|
54
|
+
bashrc = Path.home() / ".bashrc"
|
|
55
|
+
if not bashrc.exists():
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
lines = bashrc.read_text().splitlines(keepends=True)
|
|
59
|
+
marker = f"# {block_name}"
|
|
60
|
+
out = []
|
|
61
|
+
i = 0
|
|
62
|
+
removed = False
|
|
63
|
+
while i < len(lines):
|
|
64
|
+
stripped = lines[i].rstrip()
|
|
65
|
+
if stripped == marker:
|
|
66
|
+
i += 1
|
|
67
|
+
while i < len(lines) and lines[i].strip():
|
|
68
|
+
i += 1
|
|
69
|
+
if i < len(lines) and not lines[i].strip():
|
|
70
|
+
i += 1
|
|
71
|
+
removed = True
|
|
72
|
+
else:
|
|
73
|
+
out.append(lines[i])
|
|
74
|
+
i += 1
|
|
75
|
+
|
|
76
|
+
if removed:
|
|
77
|
+
bashrc.write_text("".join(out))
|
|
78
|
+
return removed
|
dev_setup/cli.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from dev_setup import __version__
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@click.group(
|
|
6
|
+
invoke_without_command=True,
|
|
7
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
8
|
+
)
|
|
9
|
+
@click.pass_context
|
|
10
|
+
def cli(ctx: click.Context) -> None:
|
|
11
|
+
if ctx.invoked_subcommand is None:
|
|
12
|
+
from dev_setup.commands.help_cmd import print_help
|
|
13
|
+
print_help()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@cli.command("version")
|
|
17
|
+
def version_cmd() -> None:
|
|
18
|
+
"""Print version and exit."""
|
|
19
|
+
click.echo(f"dev-setup {__version__}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _register_commands() -> None:
|
|
23
|
+
from dev_setup.commands.list_cmd import list_cmd
|
|
24
|
+
from dev_setup.commands.install_cmd import install_cmd
|
|
25
|
+
from dev_setup.commands.remove_cmd import remove_cmd
|
|
26
|
+
from dev_setup.commands.add_cmd import add_cmd
|
|
27
|
+
from dev_setup.commands.delete_cmd import delete_cmd
|
|
28
|
+
|
|
29
|
+
cli.add_command(list_cmd, "list")
|
|
30
|
+
cli.add_command(install_cmd, "install")
|
|
31
|
+
cli.add_command(remove_cmd, "remove")
|
|
32
|
+
cli.add_command(remove_cmd, "uninstall")
|
|
33
|
+
cli.add_command(add_cmd, "add")
|
|
34
|
+
cli.add_command(delete_cmd, "delete")
|
|
35
|
+
cli.add_command(delete_cmd, "rm")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_register_commands()
|
|
File without changes
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from dev_setup import registry, ui
|
|
10
|
+
from dev_setup.generic import GenericTool
|
|
11
|
+
|
|
12
|
+
_VALID_KEY = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
|
|
13
|
+
|
|
14
|
+
_INSTALL_TEMPLATE = """\
|
|
15
|
+
#!/usr/bin/env bash
|
|
16
|
+
# Install script for {name}
|
|
17
|
+
# Save and close to continue. Delete all content (except this line) to abort.
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
_REMOVE_TEMPLATE = """\
|
|
23
|
+
#!/usr/bin/env bash
|
|
24
|
+
# Remove script for {name}
|
|
25
|
+
# Save and close to continue. Leave only comments/blank lines to skip removal.
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.command("add")
|
|
32
|
+
def add_cmd() -> None:
|
|
33
|
+
"""Add a custom package via guided wizard."""
|
|
34
|
+
ui.section("Add Custom Package")
|
|
35
|
+
|
|
36
|
+
install_type = ui.select(
|
|
37
|
+
"Package type:",
|
|
38
|
+
["npm", "pip", "apt", "git", "script", "bash"],
|
|
39
|
+
)
|
|
40
|
+
if not install_type:
|
|
41
|
+
ui.warn("Aborted.")
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
key = _prompt_key()
|
|
45
|
+
if not key:
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
name = ui.text_input("Display name:", default=key, required=True)
|
|
49
|
+
description = ui.text_input("Short description:", required=False)
|
|
50
|
+
|
|
51
|
+
kwargs: dict = dict(
|
|
52
|
+
key=key,
|
|
53
|
+
name=name,
|
|
54
|
+
description=description,
|
|
55
|
+
category="custom",
|
|
56
|
+
install_type=install_type,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if install_type == "npm":
|
|
60
|
+
kwargs["npm_name"] = ui.text_input("npm package name:", default=key, required=True)
|
|
61
|
+
kwargs["check_cmd"] = ui.text_input("Command to check if installed:", default=key)
|
|
62
|
+
|
|
63
|
+
elif install_type == "pip":
|
|
64
|
+
kwargs["pip_name"] = ui.text_input("PyPI package name:", default=key, required=True)
|
|
65
|
+
kwargs["check_cmd"] = ui.text_input("Command to check if installed:", default=key)
|
|
66
|
+
|
|
67
|
+
elif install_type == "apt":
|
|
68
|
+
kwargs["apt_packages"] = ui.text_input(
|
|
69
|
+
"apt package(s) (space-separated):", default=key, required=True
|
|
70
|
+
)
|
|
71
|
+
kwargs["check_cmd"] = ui.text_input("Command to check if installed:", default=key)
|
|
72
|
+
|
|
73
|
+
elif install_type == "git":
|
|
74
|
+
kwargs["git_url"] = ui.text_input("Git repository URL:", required=True)
|
|
75
|
+
kwargs["git_install_cmd"] = ui.text_input(
|
|
76
|
+
"Post-clone install command (optional, run inside repo):", required=False
|
|
77
|
+
)
|
|
78
|
+
kwargs["git_remove_cmd"] = ui.text_input(
|
|
79
|
+
"Pre-delete remove command (optional):", required=False
|
|
80
|
+
)
|
|
81
|
+
kwargs["check_cmd"] = ui.text_input("Command to check if installed:", required=False)
|
|
82
|
+
|
|
83
|
+
elif install_type == "script":
|
|
84
|
+
kwargs["script_url"] = ui.text_input("Install script URL (curl | sh):", required=True)
|
|
85
|
+
kwargs["check_cmd"] = ui.text_input("Command to check if installed:", required=False)
|
|
86
|
+
|
|
87
|
+
elif install_type == "bash":
|
|
88
|
+
kwargs["check_cmd"] = ui.text_input(
|
|
89
|
+
"Command to verify installation (e.g. bat, aws):", required=False
|
|
90
|
+
)
|
|
91
|
+
install_script = _edit_script("install", name, required=True)
|
|
92
|
+
if install_script is None:
|
|
93
|
+
ui.warn("No install script provided — aborted.")
|
|
94
|
+
return
|
|
95
|
+
kwargs["install_script"] = install_script
|
|
96
|
+
|
|
97
|
+
remove_script = _edit_script("remove", name, required=False)
|
|
98
|
+
if remove_script:
|
|
99
|
+
kwargs["remove_script"] = remove_script
|
|
100
|
+
|
|
101
|
+
kwargs["help_cmd"] = ui.text_input(
|
|
102
|
+
"Help command (optional, e.g. tool --help):", required=False
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
ui.console.print()
|
|
106
|
+
ui.console.print("[bold]Summary[/]")
|
|
107
|
+
for k, v in kwargs.items():
|
|
108
|
+
if v:
|
|
109
|
+
display = _truncate_script(v) if k.endswith("_script") else v
|
|
110
|
+
ui.console.print(f" [dim]{k:<18}[/] {display}")
|
|
111
|
+
ui.console.print()
|
|
112
|
+
|
|
113
|
+
if not ui.confirm("Save this package?"):
|
|
114
|
+
ui.warn("Aborted — package not saved.")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
tool = GenericTool(**kwargs)
|
|
118
|
+
tool.save()
|
|
119
|
+
|
|
120
|
+
from dev_setup import registry as _reg
|
|
121
|
+
_reg._registry[key] = tool
|
|
122
|
+
if key not in _reg._order:
|
|
123
|
+
_reg._order.append(key)
|
|
124
|
+
|
|
125
|
+
ui.success(f"Package '{key}' added. Install with: dev-setup install {key}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _prompt_key() -> str:
|
|
129
|
+
while True:
|
|
130
|
+
key = ui.text_input("Package key (lowercase, hyphens ok):", required=True)
|
|
131
|
+
if not _VALID_KEY.match(key):
|
|
132
|
+
ui.error("Key must be lowercase letters, digits, hyphens, or underscores.")
|
|
133
|
+
continue
|
|
134
|
+
if registry.exists(key):
|
|
135
|
+
ui.error(f"A package with key '{key}' already exists.")
|
|
136
|
+
if not ui.confirm("Use a different key?"):
|
|
137
|
+
return ""
|
|
138
|
+
continue
|
|
139
|
+
return key
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _edit_script(action: str, package_name: str, required: bool = True) -> Optional[str]:
|
|
143
|
+
"""Open $EDITOR with a bash template. Returns stripped script or None if aborted."""
|
|
144
|
+
template = (_INSTALL_TEMPLATE if action == "install" else _REMOVE_TEMPLATE).format(
|
|
145
|
+
name=package_name
|
|
146
|
+
)
|
|
147
|
+
label = "install" if action == "install" else "remove (optional)"
|
|
148
|
+
ui.console.print()
|
|
149
|
+
ui.info(f"Opening $EDITOR for the [bold]{label}[/] script...")
|
|
150
|
+
ui.dim("Write your bash commands, save, and close the editor to continue.")
|
|
151
|
+
ui.console.print()
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
content = click.edit(text=template, extension=".sh", require_save=False)
|
|
155
|
+
except Exception as exc:
|
|
156
|
+
ui.error(f"Could not open editor: {exc}")
|
|
157
|
+
ui.dim("Set the EDITOR environment variable (e.g. export EDITOR=nano) and try again.")
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
if content is None:
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
script = _strip_template_comments(content)
|
|
164
|
+
if not script and required:
|
|
165
|
+
ui.error("Install script cannot be empty.")
|
|
166
|
+
return None
|
|
167
|
+
return script or ""
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _strip_template_comments(content: str) -> str:
|
|
171
|
+
"""Remove comment-only lines and leading/trailing blank lines."""
|
|
172
|
+
lines = [
|
|
173
|
+
line for line in content.splitlines()
|
|
174
|
+
if line.strip() and not line.strip().startswith("#")
|
|
175
|
+
]
|
|
176
|
+
return "\n".join(lines).strip()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _truncate_script(script: str) -> str:
|
|
180
|
+
lines = [l for l in script.splitlines() if l.strip()]
|
|
181
|
+
if not lines:
|
|
182
|
+
return "[dim](empty)[/]"
|
|
183
|
+
first = lines[0][:60]
|
|
184
|
+
suffix = f" [dim]+{len(lines) - 1} more lines[/]" if len(lines) > 1 else ""
|
|
185
|
+
return f"{first}{suffix}"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from dev_setup import registry, ui
|
|
8
|
+
from dev_setup.generic import CUSTOM_DIR
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command("delete")
|
|
12
|
+
@click.argument("key")
|
|
13
|
+
def delete_cmd(key: str) -> None:
|
|
14
|
+
"""Remove a custom package from the registry."""
|
|
15
|
+
if not registry.exists(key):
|
|
16
|
+
ui.error(f"No package with key '{key}' found.")
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
tool = registry.get(key)
|
|
20
|
+
assert tool is not None
|
|
21
|
+
|
|
22
|
+
if tool.builtin:
|
|
23
|
+
ui.error(f"'{key}' is a built-in package and cannot be deleted.")
|
|
24
|
+
sys.exit(1)
|
|
25
|
+
|
|
26
|
+
pkg_file = CUSTOM_DIR / f"{key}.json"
|
|
27
|
+
if not pkg_file.exists():
|
|
28
|
+
ui.error(f"Package file not found: {pkg_file}")
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
ui.console.print(f"\n [bold]{tool.name}[/] — {tool.description}\n")
|
|
32
|
+
|
|
33
|
+
if not ui.confirm(f"Delete '{key}' from the registry?", default=False):
|
|
34
|
+
ui.dim("Aborted.")
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
pkg_file.unlink()
|
|
38
|
+
|
|
39
|
+
if key in registry._registry:
|
|
40
|
+
del registry._registry[key]
|
|
41
|
+
if key in registry._order:
|
|
42
|
+
registry._order.remove(key)
|
|
43
|
+
|
|
44
|
+
ui.success(f"Package '{key}' deleted from registry.")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from dev_setup import ui
|
|
2
|
+
from dev_setup.generic import CUSTOM_DIR
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def print_help() -> None:
|
|
6
|
+
ui.print_banner()
|
|
7
|
+
ui.console.print("[bold]USAGE[/]")
|
|
8
|
+
ui.console.print(" dev-setup [bold cyan]<command>[/] [OPTIONS] [ARGS]\n")
|
|
9
|
+
|
|
10
|
+
ui.console.print("[bold]COMMANDS[/]")
|
|
11
|
+
rows = [
|
|
12
|
+
("list", "[--installed] [--available] [category]", "List packages"),
|
|
13
|
+
("install", "[package ...]", "Install packages (interactive if no args)"),
|
|
14
|
+
("remove", "<package ...>", "Uninstall installed packages"),
|
|
15
|
+
("add", "", "Add a custom package (guided wizard)"),
|
|
16
|
+
("delete", "<key>", "Remove a custom package from the registry"),
|
|
17
|
+
("version", "", "Show version"),
|
|
18
|
+
]
|
|
19
|
+
for cmd, args, desc in rows:
|
|
20
|
+
ui.console.print(
|
|
21
|
+
f" [bold cyan]{cmd:<10}[/] [dim]{args:<40}[/] {desc}"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
ui.console.print()
|
|
25
|
+
ui.console.print("[bold]EXAMPLES[/]")
|
|
26
|
+
examples = [
|
|
27
|
+
"dev-setup list",
|
|
28
|
+
"dev-setup list core",
|
|
29
|
+
"dev-setup list --installed",
|
|
30
|
+
"dev-setup install docker nvm",
|
|
31
|
+
"dev-setup install",
|
|
32
|
+
"dev-setup remove htop",
|
|
33
|
+
"dev-setup add",
|
|
34
|
+
"dev-setup delete my-tool",
|
|
35
|
+
]
|
|
36
|
+
for ex in examples:
|
|
37
|
+
ui.console.print(f" [dim]$[/] [green]{ex}[/]")
|
|
38
|
+
|
|
39
|
+
ui.console.print()
|
|
40
|
+
ui.console.print("[bold]CATEGORIES[/]")
|
|
41
|
+
ui.console.print(" [cyan]core[/] Always-installed tools (Docker, NVM, uv)")
|
|
42
|
+
ui.console.print(" [cyan]tools[/] Optional utilities (PHP, Starship, htop)")
|
|
43
|
+
ui.console.print(" [cyan]custom[/] User-added packages\n")
|
|
44
|
+
|
|
45
|
+
ui.console.print("[bold]CONFIG[/]")
|
|
46
|
+
ui.console.print(f" Custom packages: [dim]{CUSTOM_DIR}[/]\n")
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import questionary
|
|
8
|
+
|
|
9
|
+
from dev_setup import registry, ui
|
|
10
|
+
from dev_setup.base import Tool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command("install")
|
|
14
|
+
@click.argument("packages", nargs=-1)
|
|
15
|
+
def install_cmd(packages: Tuple[str, ...]) -> None:
|
|
16
|
+
"""Install packages. Interactive picker when called with no arguments."""
|
|
17
|
+
if not packages:
|
|
18
|
+
_install_interactive()
|
|
19
|
+
else:
|
|
20
|
+
failed = False
|
|
21
|
+
for key in packages:
|
|
22
|
+
if not registry.exists(key):
|
|
23
|
+
ui.error(f"Unknown package: '{key}'")
|
|
24
|
+
failed = True
|
|
25
|
+
continue
|
|
26
|
+
if not _install_one(registry.get(key)): # type: ignore[arg-type]
|
|
27
|
+
failed = True
|
|
28
|
+
if failed:
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _install_one(tool: Tool) -> bool:
|
|
33
|
+
ui.section(tool.name)
|
|
34
|
+
if tool.is_installed():
|
|
35
|
+
ui.success(f"{tool.name} is already installed: {tool.get_version()}")
|
|
36
|
+
return True
|
|
37
|
+
try:
|
|
38
|
+
version = tool.install()
|
|
39
|
+
msg = f"{tool.name} installed"
|
|
40
|
+
if version:
|
|
41
|
+
msg += f": {version}"
|
|
42
|
+
ui.success(msg)
|
|
43
|
+
return True
|
|
44
|
+
except Exception as exc:
|
|
45
|
+
ui.error(f"Failed to install {tool.name}: {exc}")
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _install_interactive() -> None:
|
|
50
|
+
ui.print_banner()
|
|
51
|
+
tools = registry.all_tools()
|
|
52
|
+
|
|
53
|
+
choices = [
|
|
54
|
+
questionary.Choice(
|
|
55
|
+
title=(
|
|
56
|
+
f"{'[installed] ' if t.is_installed() else ''}"
|
|
57
|
+
f"{t.key:<14} {t.description}"
|
|
58
|
+
),
|
|
59
|
+
value=t.key,
|
|
60
|
+
checked=False,
|
|
61
|
+
)
|
|
62
|
+
for t in tools
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
selected = questionary.checkbox(
|
|
66
|
+
"Select packages to install (Space to toggle, Enter to confirm):",
|
|
67
|
+
choices=choices,
|
|
68
|
+
style=ui._STYLE,
|
|
69
|
+
).ask()
|
|
70
|
+
|
|
71
|
+
if not selected:
|
|
72
|
+
ui.info("No packages selected.")
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
already = [k for k in selected if registry.get(k) and registry.get(k).is_installed()] # type: ignore[union-attr]
|
|
76
|
+
to_install = [k for k in selected if k not in already]
|
|
77
|
+
|
|
78
|
+
if already:
|
|
79
|
+
ui.dim(f"Already installed: {', '.join(already)}")
|
|
80
|
+
|
|
81
|
+
if not to_install:
|
|
82
|
+
ui.info("Nothing new to install.")
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
if not ui.confirm(f"Install {len(to_install)} package(s)?"):
|
|
86
|
+
ui.warn("Aborted.")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
failed = False
|
|
90
|
+
for key in to_install:
|
|
91
|
+
tool = registry.get(key)
|
|
92
|
+
if tool and not _install_one(tool):
|
|
93
|
+
failed = True
|
|
94
|
+
|
|
95
|
+
if failed:
|
|
96
|
+
sys.exit(1)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from dev_setup import registry, ui
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command("list")
|
|
10
|
+
@click.option("--installed", "show_filter", flag_value="installed", help="Show only installed packages")
|
|
11
|
+
@click.option("--available", "show_filter", flag_value="available", help="Show only uninstalled packages")
|
|
12
|
+
@click.argument("category", required=False)
|
|
13
|
+
def list_cmd(show_filter: str, category: str) -> None:
|
|
14
|
+
"""List available packages."""
|
|
15
|
+
ui.print_banner()
|
|
16
|
+
|
|
17
|
+
tools = registry.all_tools()
|
|
18
|
+
by_cat: dict = {}
|
|
19
|
+
|
|
20
|
+
for tool in tools:
|
|
21
|
+
if category and tool.category != category:
|
|
22
|
+
continue
|
|
23
|
+
is_inst = tool.is_installed()
|
|
24
|
+
if show_filter == "installed" and not is_inst:
|
|
25
|
+
continue
|
|
26
|
+
if show_filter == "available" and is_inst:
|
|
27
|
+
continue
|
|
28
|
+
by_cat.setdefault(tool.category, []).append((tool, is_inst))
|
|
29
|
+
|
|
30
|
+
if not by_cat:
|
|
31
|
+
ui.warn("No packages match the given filters.")
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
for cat in ("core", "tools", "custom"):
|
|
35
|
+
entries = by_cat.get(cat, [])
|
|
36
|
+
if not entries:
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
ui.console.print(f" [bold magenta]{cat.upper()}[/]")
|
|
40
|
+
ui.divider()
|
|
41
|
+
|
|
42
|
+
tbl = Table(box=None, padding=(0, 1), show_header=True, header_style="dim")
|
|
43
|
+
tbl.add_column("", width=2)
|
|
44
|
+
tbl.add_column("Package", style="bold", min_width=12)
|
|
45
|
+
tbl.add_column("Description", min_width=36)
|
|
46
|
+
tbl.add_column("Type", style="dim", min_width=8)
|
|
47
|
+
tbl.add_column("Version", style="dim")
|
|
48
|
+
|
|
49
|
+
for tool, is_inst in entries:
|
|
50
|
+
icon = "[green bold]✔[/]" if is_inst else "[red bold]✘[/]"
|
|
51
|
+
version = tool.get_version() if is_inst else ""
|
|
52
|
+
tbl.add_row(icon, tool.key, tool.description, tool.install_type, version)
|
|
53
|
+
if tool.help_cmd:
|
|
54
|
+
tbl.add_row("", "", f"[dim cyan] ? {tool.help_cmd}[/]", "", "")
|
|
55
|
+
|
|
56
|
+
ui.console.print(tbl)
|
|
57
|
+
ui.console.print()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Tuple
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from dev_setup import registry, ui
|
|
9
|
+
from dev_setup.base import Tool
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command("remove")
|
|
13
|
+
@click.argument("packages", nargs=-1)
|
|
14
|
+
def remove_cmd(packages: Tuple[str, ...]) -> None:
|
|
15
|
+
"""Uninstall installed packages."""
|
|
16
|
+
if not packages:
|
|
17
|
+
ui.error("Specify at least one package key. See: dev-setup list --installed")
|
|
18
|
+
sys.exit(1)
|
|
19
|
+
|
|
20
|
+
failed = False
|
|
21
|
+
for key in packages:
|
|
22
|
+
if not registry.exists(key):
|
|
23
|
+
ui.error(f"Unknown package: '{key}'")
|
|
24
|
+
failed = True
|
|
25
|
+
continue
|
|
26
|
+
if not _remove_one(registry.get(key)): # type: ignore[arg-type]
|
|
27
|
+
failed = True
|
|
28
|
+
|
|
29
|
+
if failed:
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _remove_one(tool: Tool) -> bool:
|
|
34
|
+
ui.section(f"Remove {tool.name}")
|
|
35
|
+
|
|
36
|
+
if not tool.is_installed():
|
|
37
|
+
ui.warn(f"{tool.name} is not installed — nothing to remove.")
|
|
38
|
+
return True
|
|
39
|
+
|
|
40
|
+
if not ui.confirm(f"Remove {tool.name}?", default=False):
|
|
41
|
+
ui.dim("Skipped.")
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
tool.remove()
|
|
46
|
+
ui.success(f"{tool.name} removed")
|
|
47
|
+
return True
|
|
48
|
+
except Exception as exc:
|
|
49
|
+
ui.error(f"Failed to remove {tool.name}: {exc}")
|
|
50
|
+
return False
|