dev-setup 1.1.0__tar.gz → 1.4.0__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.
Files changed (48) hide show
  1. dev_setup-1.4.0/.claude/settings.local.json +11 -0
  2. dev_setup-1.4.0/.github/workflows/publish.yml +44 -0
  3. dev_setup-1.4.0/CHANGELOG.md +54 -0
  4. {dev_setup-1.1.0 → dev_setup-1.4.0}/PKG-INFO +27 -12
  5. {dev_setup-1.1.0 → dev_setup-1.4.0}/README.md +26 -11
  6. dev_setup-1.4.0/app-design-output/recommendations.md +100 -0
  7. dev_setup-1.4.0/dev/Dockerfile +38 -0
  8. dev_setup-1.4.0/dev/Makefile +19 -0
  9. dev_setup-1.4.0/dev/docker-compose.yml +14 -0
  10. {dev_setup-1.1.0 → dev_setup-1.4.0}/pyproject.toml +12 -1
  11. dev_setup-1.4.0/src/dev_setup/__init__.py +6 -0
  12. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/base.py +19 -0
  13. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/cli.py +2 -0
  14. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/commands/add_cmd.py +5 -4
  15. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/commands/delete_cmd.py +1 -6
  16. dev_setup-1.4.0/src/dev_setup/commands/docs_cmd.py +34 -0
  17. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/commands/help_cmd.py +1 -0
  18. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/commands/install_cmd.py +22 -10
  19. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/commands/list_cmd.py +5 -1
  20. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/commands/remove_cmd.py +5 -1
  21. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/generic.py +42 -28
  22. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/packages/aws_cli.py +3 -10
  23. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/packages/docker.py +3 -10
  24. dev_setup-1.4.0/src/dev_setup/packages/eza.py +59 -0
  25. dev_setup-1.4.0/src/dev_setup/packages/gh.py +53 -0
  26. dev_setup-1.4.0/src/dev_setup/packages/go.py +107 -0
  27. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/packages/htop.py +3 -10
  28. dev_setup-1.4.0/src/dev_setup/packages/java.py +42 -0
  29. dev_setup-1.4.0/src/dev_setup/packages/mkcert.py +71 -0
  30. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/packages/nvm.py +1 -0
  31. dev_setup-1.4.0/src/dev_setup/packages/ollama.py +53 -0
  32. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/packages/php.py +3 -9
  33. dev_setup-1.4.0/src/dev_setup/packages/pi_agent.py +63 -0
  34. dev_setup-1.4.0/src/dev_setup/packages/ruby.py +114 -0
  35. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/packages/saml2aws.py +3 -11
  36. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/packages/starship.py +3 -10
  37. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/packages/uv_tool.py +3 -9
  38. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/registry.py +25 -2
  39. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/ui.py +2 -1
  40. dev_setup-1.4.0/uv.lock +510 -0
  41. dev_setup-1.1.0/src/dev_setup/__init__.py +0 -1
  42. dev_setup-1.1.0/uv.lock +0 -117
  43. {dev_setup-1.1.0 → dev_setup-1.4.0}/.gitignore +0 -0
  44. {dev_setup-1.1.0 → dev_setup-1.4.0}/dev-setup +0 -0
  45. {dev_setup-1.1.0 → dev_setup-1.4.0}/install.sh +0 -0
  46. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/__main__.py +0 -0
  47. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/commands/__init__.py +0 -0
  48. {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/packages/__init__.py +0 -0
@@ -0,0 +1,11 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(uv run *)",
5
+ "Bash(docker build *)",
6
+ "Bash(git -C /home/sawyer/Documents/Projects/ClaudeProjects/dev-setup-py log --format=\"%H %ai %s\")",
7
+ "Bash(git -C /home/sawyer/Documents/Projects/ClaudeProjects/dev-setup-py add src/dev_setup/packages/eza.py)",
8
+ "Bash(git -C /home/sawyer/Documents/Projects/ClaudeProjects/dev-setup-py commit -m ' *)"
9
+ ]
10
+ }
11
+ }
@@ -0,0 +1,44 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build:
10
+ name: Build distribution
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - name: Install uv
16
+ uses: astral-sh/setup-uv@v5
17
+
18
+ - name: Build wheel and source distribution
19
+ run: uv build
20
+
21
+ - name: Upload dist artifacts
22
+ uses: actions/upload-artifact@v4
23
+ with:
24
+ name: dist
25
+ path: dist/
26
+
27
+ publish:
28
+ name: Publish to PyPI
29
+ needs: build
30
+ runs-on: ubuntu-latest
31
+ environment:
32
+ name: pypi
33
+ url: https://pypi.org/p/dev-setup
34
+ permissions:
35
+ id-token: write # required for OIDC trusted publishing
36
+ steps:
37
+ - name: Download dist artifacts
38
+ uses: actions/download-artifact@v4
39
+ with:
40
+ name: dist
41
+ path: dist/
42
+
43
+ - name: Publish to PyPI
44
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,54 @@
1
+ ## v1.4.0 (2026-06-19)
2
+
3
+ ### Feat
4
+
5
+ - add eza builtin tool
6
+
7
+ ## v1.3.0 (2026-06-19)
8
+
9
+ ### Feat
10
+
11
+ - gate tool availability on required dependencies
12
+
13
+ ## v1.2.0 (2026-06-19)
14
+
15
+ ### Feat
16
+
17
+ - add docker-compose.yml to dev/ sandbox
18
+ - add dev/ sandbox with Dockerfile for local install testing
19
+ - add docs command to open tool documentation in the browser
20
+ - add Go, Java, and Ruby builtin tools under languages category
21
+ - add gh, mkcert, ollama, and pi-coding-agent builtin tools
22
+ - add --verbose/-v flag to stream install/remove output
23
+
24
+ ### Docs
25
+
26
+ - add Prerequisites section to README
27
+
28
+ ### Chore
29
+
30
+ - add commitizen dev dep and unify version source
31
+
32
+ ## v1.1.0 (2026-06-18)
33
+
34
+ ### Feat
35
+
36
+ - add aws-cli and saml2aws built-in packages
37
+ - add bash type for multi-step custom installs
38
+ - add help_cmd field to all tools, display in list output
39
+ - enhance project metadata in pyproject.toml
40
+
41
+ ### Refactor
42
+
43
+ - improve maintainability and extensibility for new tool additions
44
+ - switch from uv-run to pip/pipx install pattern
45
+
46
+ ### Docs
47
+
48
+ - add comprehensive README
49
+
50
+ ## v1.0.0 (2026-06-17)
51
+
52
+ ### Feat
53
+
54
+ - initial Python implementation of dev-setup CLI
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dev-setup
3
- Version: 1.1.0
3
+ Version: 1.4.0
4
4
  Summary: Development environment setup CLI for Linux
5
5
  Project-URL: Repository, https://github.com/thesawdawg/dev-setup-py
6
6
  Author-email: Sawyer <sawyerksu@gmail.com>
@@ -30,6 +30,32 @@ A Python-based CLI for managing your Linux development environment. Install, rem
30
30
 
31
31
  ---
32
32
 
33
+ ## Prerequisites
34
+
35
+ | Requirement | Notes |
36
+ |-------------|-------|
37
+ | **OS** | Ubuntu 20.04+ or Debian 11+ (amd64) |
38
+ | **Python** | 3.11 or later |
39
+ | **curl** | Used by script-based installers (Docker, NVM, uv, etc.) |
40
+ | **sudo** | Required for tools that write to system paths (`/usr/local/bin`, apt packages) |
41
+ | **ca-certificates** | For HTTPS downloads — present on most systems by default |
42
+
43
+ These are available on any standard Ubuntu/Debian install. On a fresh minimal image, run:
44
+
45
+ ```bash
46
+ sudo apt-get install -y python3 python3-pip curl ca-certificates sudo
47
+ ```
48
+
49
+ **Optional** — only needed when using specific install types:
50
+
51
+ | Requirement | When |
52
+ |-------------|------|
53
+ | `git` | `git`-type custom packages (`dev-setup add` → git) |
54
+ | `node` / `npm` | `npm`-type custom packages |
55
+ | `uv` | Running from source via `./dev-setup` (auto-installed if missing) |
56
+
57
+ ---
58
+
33
59
  ## Installation
34
60
 
35
61
  ### From PyPI (recommended)
@@ -361,17 +387,6 @@ class MyTool(Tool):
361
387
 
362
388
  ---
363
389
 
364
- ## Requirements
365
-
366
- - Debian/Ubuntu Linux (apt-based; htop and php fall back to yum/dnf/pacman for detection)
367
- - `curl` (for bootstrapping uv and install scripts)
368
- - `git` (for `git`-type custom packages)
369
- - `sudo` access (Docker, PHP, saml2aws, htop installs write to system paths)
370
-
371
- Python 3.11+ is provisioned automatically by uv if not already present.
372
-
373
- ---
374
-
375
390
  ## Dependencies
376
391
 
377
392
  | Package | Version | Purpose |
@@ -4,6 +4,32 @@ A Python-based CLI for managing your Linux development environment. Install, rem
4
4
 
5
5
  ---
6
6
 
7
+ ## Prerequisites
8
+
9
+ | Requirement | Notes |
10
+ |-------------|-------|
11
+ | **OS** | Ubuntu 20.04+ or Debian 11+ (amd64) |
12
+ | **Python** | 3.11 or later |
13
+ | **curl** | Used by script-based installers (Docker, NVM, uv, etc.) |
14
+ | **sudo** | Required for tools that write to system paths (`/usr/local/bin`, apt packages) |
15
+ | **ca-certificates** | For HTTPS downloads — present on most systems by default |
16
+
17
+ These are available on any standard Ubuntu/Debian install. On a fresh minimal image, run:
18
+
19
+ ```bash
20
+ sudo apt-get install -y python3 python3-pip curl ca-certificates sudo
21
+ ```
22
+
23
+ **Optional** — only needed when using specific install types:
24
+
25
+ | Requirement | When |
26
+ |-------------|------|
27
+ | `git` | `git`-type custom packages (`dev-setup add` → git) |
28
+ | `node` / `npm` | `npm`-type custom packages |
29
+ | `uv` | Running from source via `./dev-setup` (auto-installed if missing) |
30
+
31
+ ---
32
+
7
33
  ## Installation
8
34
 
9
35
  ### From PyPI (recommended)
@@ -335,17 +361,6 @@ class MyTool(Tool):
335
361
 
336
362
  ---
337
363
 
338
- ## Requirements
339
-
340
- - Debian/Ubuntu Linux (apt-based; htop and php fall back to yum/dnf/pacman for detection)
341
- - `curl` (for bootstrapping uv and install scripts)
342
- - `git` (for `git`-type custom packages)
343
- - `sudo` access (Docker, PHP, saml2aws, htop installs write to system paths)
344
-
345
- Python 3.11+ is provisioned automatically by uv if not already present.
346
-
347
- ---
348
-
349
364
  ## Dependencies
350
365
 
351
366
  | Package | Version | Purpose |
@@ -0,0 +1,100 @@
1
+ # dev-setup — Maintainability Review
2
+
3
+ _Generated 2026-06-18_
4
+
5
+ ## Goal
6
+ Lower the cost of adding new tools to the core PyPI package build.
7
+
8
+ ---
9
+
10
+ ## Findings
11
+
12
+ ### 1. Builtin boilerplate: every package file duplicates the same `is_installed` / `get_version` logic [HIGH]
13
+
14
+ Seven of eight builtin package files implement the same two-liner:
15
+ ```python
16
+ def is_installed(self) -> bool:
17
+ return shutil.which(self.key) is not None
18
+
19
+ def get_version(self) -> str:
20
+ r = subprocess.run([self.key, "--version"], capture_output=True, text=True)
21
+ return r.stdout.strip().splitlines()[0] if r.returncode == 0 else ""
22
+ ```
23
+ Adding a new tool means copy-pasting this pattern. Errors diverge silently (`aws_cli` and `saml2aws` check `stderr` fallback; `docker` skips `splitlines()`).
24
+
25
+ **Fix:** Add `WhichTool(Tool)` to `base.py` — a shared mixin that provides these two methods. Each builtin that passes `shutil.which` inherits from `WhichTool` instead of `Tool` and only overrides what's truly unique (install/remove logic). `NvmTool` keeps its own `is_installed()` since it checks a file path, not `which`.
26
+
27
+ **Effort:** Low. Touches 7 files, each loses 8–12 lines.
28
+
29
+ ---
30
+
31
+ ### 2. Hardcoded category list in `list_cmd.py` silently drops new tools [HIGH]
32
+
33
+ `list_cmd.py:34` iterates `("core", "tools", "custom")`. A builtin with any other `category` value is collected into `by_cat` but never rendered. Verified: a tool with `category = "cloud"` is invisible to `dev-setup list`.
34
+
35
+ **Fix:** Derive iteration order dynamically using a priority dict — `core=0`, `custom=999`, all others sorted by name in between.
36
+
37
+ **Effort:** 4-line change.
38
+
39
+ ---
40
+
41
+ ### 3. `add_cmd` and `delete_cmd` mutate private registry state directly [MEDIUM]
42
+
43
+ `add_cmd.py:120-123` and `delete_cmd.py:39-42` both reach into `registry._registry` and `registry._order` directly. Any internal registry refactor (e.g., ordering strategy change) silently breaks these commands.
44
+
45
+ **Fix:** Add public `register(tool)` and `deregister(key)` functions to `registry.py`. Commands use those; internals stay internal.
46
+
47
+ **Effort:** Low.
48
+
49
+ ---
50
+
51
+ ### 4. Version is defined in two places and is out of sync [MEDIUM]
52
+
53
+ `src/dev_setup/__init__.py` says `1.0.0`; `pyproject.toml` says `1.1.0`. Additionally, `ui.py:64` hardcodes the string `"v1.0.0"` in `print_banner()`.
54
+
55
+ **Fix:** Single source of truth: update `__init__.py` to `1.1.0`, and have `print_banner()` read from `from dev_setup import __version__`.
56
+
57
+ _Note: for a fully automated solution, `pyproject.toml` can be set to `dynamic = ["version"]` with `[tool.hatch.version] path = "src/dev_setup/__init__.py"`, eliminating the need to update two files on release. That's a separate change._
58
+
59
+ **Effort:** 2 lines.
60
+
61
+ ---
62
+
63
+ ### 5. `CUSTOM_DIR` / `_CUSTOM_DIR` duplicated in `generic.py` [LOW]
64
+
65
+ `generic.py` exports both `CUSTOM_DIR` and `_CUSTOM_DIR` (identical). `registry.py` imports the underscore form. This is confusing — two names for the same path.
66
+
67
+ **Fix:** Remove `_CUSTOM_DIR`, update `registry.py` to import `CUSTOM_DIR`.
68
+
69
+ ---
70
+
71
+ ### 6. `import shutil as _shutil` inside `GenericTool.remove()` shadows a top-level import [LOW]
72
+
73
+ `generic.py` imports `shutil` at the top of the file, then re-imports it as `_shutil` inside the `git` branch of `remove()`. The local shadowing is unnecessary.
74
+
75
+ **Fix:** Use the module-level `shutil` directly.
76
+
77
+ ---
78
+
79
+ ## What "adding a new builtin" looks like after fixes
80
+
81
+ Before (current): ~50 lines per file including copy-pasted `is_installed`, `get_version`, and `shutil`/`subprocess` imports.
82
+
83
+ After: ~30 lines. A simple tool like `htop` becomes:
84
+ ```python
85
+ class HtopTool(WhichTool):
86
+ key = "htop"
87
+ name = "htop"
88
+ description = "Interactive process and resource monitor"
89
+ category = "tools"
90
+ install_type = "apt"
91
+ help_cmd = "man htop"
92
+
93
+ def install(self) -> Optional[str]:
94
+ ... # only the unique install logic
95
+
96
+ def remove(self) -> None:
97
+ ... # only the unique remove logic
98
+ ```
99
+
100
+ New tools with unusual categories won't vanish from `list`. New categories appear automatically without touching `list_cmd.py`.
@@ -0,0 +1,38 @@
1
+ FROM ubuntu:24.04
2
+
3
+ ENV DEBIAN_FRONTEND=noninteractive \
4
+ TZ=UTC \
5
+ LANG=C.UTF-8
6
+
7
+ # System packages needed to bootstrap dev-setup and run its installers
8
+ RUN apt-get update && apt-get install -y --no-install-recommends \
9
+ python3.12 \
10
+ python3.12-venv \
11
+ python3-pip \
12
+ curl \
13
+ git \
14
+ sudo \
15
+ ca-certificates \
16
+ gnupg \
17
+ lsb-release \
18
+ && rm -rf /var/lib/apt/lists/*
19
+
20
+ # Non-root user with passwordless sudo — mirrors a real developer workstation
21
+ RUN useradd -m -s /bin/bash developer \
22
+ && echo "developer ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
23
+
24
+ USER developer
25
+ WORKDIR /home/developer
26
+
27
+ # Install the pre-built wheel (built by 'make build' before docker build runs)
28
+ COPY --chown=developer:developer dist/dev_setup-*.whl /tmp/
29
+
30
+ RUN pip install --user --break-system-packages /tmp/dev_setup-*.whl \
31
+ && rm /tmp/dev_setup-*.whl
32
+
33
+ ENV PATH="/home/developer/.local/bin:$PATH"
34
+
35
+ # Pre-create config dir so the tool has somewhere to write on first run
36
+ RUN mkdir -p /home/developer/.config/dev-setup/packages
37
+
38
+ CMD ["/bin/bash", "--login"]
@@ -0,0 +1,19 @@
1
+ ROOT := ..
2
+
3
+ .PHONY: build run shell clean
4
+
5
+ # Build a fresh wheel from source, then build the Docker image.
6
+ # Clean dist first so the glob in the Dockerfile matches exactly one file.
7
+ build:
8
+ cd $(ROOT) && rm -f dist/*.whl dist/*.tar.gz && uv build
9
+ docker compose build
10
+
11
+ # Drop into an interactive shell as the 'developer' user
12
+ run: build
13
+ docker compose run --rm sandbox
14
+
15
+ shell: run
16
+
17
+ # Remove the built image and named volume
18
+ clean:
19
+ docker compose down --rmi local --volumes 2>/dev/null || true
@@ -0,0 +1,14 @@
1
+ services:
2
+ sandbox:
3
+ build:
4
+ context: ..
5
+ dockerfile: dev/Dockerfile
6
+ image: dev-setup-sandbox
7
+ stdin_open: true
8
+ tty: true
9
+ volumes:
10
+ # Persist custom packages added via 'dev-setup add' across container runs
11
+ - dev-setup-config:/home/developer/.config/dev-setup
12
+
13
+ volumes:
14
+ dev-setup-config:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dev-setup"
3
- version = "1.1.0"
3
+ version = "1.4.0"
4
4
  description = "Development environment setup CLI for Linux"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -41,3 +41,14 @@ build-backend = "hatchling.build"
41
41
 
42
42
  [tool.hatch.build.targets.wheel]
43
43
  packages = ["src/dev_setup"]
44
+
45
+ [dependency-groups]
46
+ dev = [
47
+ "commitizen>=3.0",
48
+ ]
49
+
50
+ [tool.commitizen]
51
+ name = "cz_conventional_commits"
52
+ version_provider = "pep621"
53
+ tag_format = "v$version"
54
+ update_changelog_on_bump = true
@@ -0,0 +1,6 @@
1
+ from importlib.metadata import version, PackageNotFoundError
2
+
3
+ try:
4
+ __version__ = version("dev-setup")
5
+ except PackageNotFoundError:
6
+ __version__ = "dev"
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import shutil
3
4
  import subprocess
4
5
  from abc import ABC, abstractmethod
5
6
  from pathlib import Path
@@ -14,6 +15,8 @@ class Tool(ABC):
14
15
  install_type: str = "unknown"
15
16
  builtin: bool = True
16
17
  help_cmd: str = ""
18
+ docs_url: str = ""
19
+ requires: list = [] # keys of tools that must be installed before this one
17
20
 
18
21
  @abstractmethod
19
22
  def is_installed(self) -> bool: ...
@@ -32,6 +35,22 @@ class Tool(ABC):
32
35
  return ""
33
36
 
34
37
 
38
+ class WhichTool(Tool):
39
+ """Base for tools whose presence is detected via PATH lookup (shutil.which)."""
40
+
41
+ def is_installed(self) -> bool:
42
+ return shutil.which(self.key) is not None
43
+
44
+ def get_version(self) -> str:
45
+ if not shutil.which(self.key):
46
+ return ""
47
+ r = subprocess.run([self.key, "--version"], capture_output=True, text=True)
48
+ if r.returncode != 0:
49
+ return ""
50
+ out = r.stdout.strip() or r.stderr.strip()
51
+ return out.splitlines()[0] if out else ""
52
+
53
+
35
54
  def run_bash(cmd: str, **kwargs) -> subprocess.CompletedProcess:
36
55
  return subprocess.run(["bash", "-c", cmd], **kwargs)
37
56
 
@@ -25,6 +25,7 @@ def _register_commands() -> None:
25
25
  from dev_setup.commands.remove_cmd import remove_cmd
26
26
  from dev_setup.commands.add_cmd import add_cmd
27
27
  from dev_setup.commands.delete_cmd import delete_cmd
28
+ from dev_setup.commands.docs_cmd import docs_cmd
28
29
 
29
30
  cli.add_command(list_cmd, "list")
30
31
  cli.add_command(install_cmd, "install")
@@ -33,6 +34,7 @@ def _register_commands() -> None:
33
34
  cli.add_command(add_cmd, "add")
34
35
  cli.add_command(delete_cmd, "delete")
35
36
  cli.add_command(delete_cmd, "rm")
37
+ cli.add_command(docs_cmd, "docs")
36
38
 
37
39
 
38
40
  _register_commands()
@@ -101,6 +101,9 @@ def add_cmd() -> None:
101
101
  kwargs["help_cmd"] = ui.text_input(
102
102
  "Help command (optional, e.g. tool --help):", required=False
103
103
  )
104
+ kwargs["docs_url"] = ui.text_input(
105
+ "Documentation URL (optional):", required=False
106
+ )
104
107
 
105
108
  ui.console.print()
106
109
  ui.console.print("[bold]Summary[/]")
@@ -117,10 +120,8 @@ def add_cmd() -> None:
117
120
  tool = GenericTool(**kwargs)
118
121
  tool.save()
119
122
 
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)
123
+ from dev_setup import registry
124
+ registry.register(tool)
124
125
 
125
126
  ui.success(f"Package '{key}' added. Install with: dev-setup install {key}")
126
127
 
@@ -35,10 +35,5 @@ def delete_cmd(key: str) -> None:
35
35
  return
36
36
 
37
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
-
38
+ registry.deregister(key)
44
39
  ui.success(f"Package '{key}' deleted from registry.")
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import webbrowser
5
+
6
+ import click
7
+
8
+ from dev_setup import registry, ui
9
+
10
+
11
+ @click.command("docs")
12
+ @click.argument("package")
13
+ def docs_cmd(package: str) -> None:
14
+ """Open the documentation site for a package in your browser."""
15
+ if not registry.exists(package):
16
+ ui.error(f"Unknown package: '{package}'")
17
+ sys.exit(1)
18
+
19
+ tool = registry.get(package)
20
+ assert tool is not None
21
+
22
+ if not tool.docs_url:
23
+ ui.warn(f"No documentation URL is configured for '{package}'.")
24
+ ui.dim("For custom packages, re-add with a docs URL via: dev-setup add")
25
+ sys.exit(1)
26
+
27
+ ui.console.print(f"\n [bold]{tool.name}[/] docs")
28
+ ui.console.print(f" [cyan]{tool.docs_url}[/]\n")
29
+
30
+ opened = webbrowser.open(tool.docs_url)
31
+ if opened:
32
+ ui.success("Opened in browser.")
33
+ else:
34
+ ui.warn("Could not open a browser — copy the URL above to open manually.")
@@ -14,6 +14,7 @@ def print_help() -> None:
14
14
  ("remove", "<package ...>", "Uninstall installed packages"),
15
15
  ("add", "", "Add a custom package (guided wizard)"),
16
16
  ("delete", "<key>", "Remove a custom package from the registry"),
17
+ ("docs", "<package>", "Open documentation in browser"),
17
18
  ("version", "", "Show version"),
18
19
  ]
19
20
  for cmd, args, desc in rows:
@@ -11,9 +11,13 @@ from dev_setup.base import Tool
11
11
 
12
12
 
13
13
  @click.command("install")
14
+ @click.option("--verbose", "-v", is_flag=True, help="Stream install output to the terminal.")
14
15
  @click.argument("packages", nargs=-1)
15
- def install_cmd(packages: Tuple[str, ...]) -> None:
16
+ def install_cmd(packages: Tuple[str, ...], verbose: bool) -> None:
16
17
  """Install packages. Interactive picker when called with no arguments."""
18
+ from dev_setup import generic
19
+ generic._verbose = verbose
20
+
17
21
  if not packages:
18
22
  _install_interactive()
19
23
  else:
@@ -34,6 +38,11 @@ def _install_one(tool: Tool) -> bool:
34
38
  if tool.is_installed():
35
39
  ui.success(f"{tool.name} is already installed: {tool.get_version()}")
36
40
  return True
41
+ missing = registry.missing_requires(tool)
42
+ if missing:
43
+ ui.error(f"Cannot install {tool.name} — missing required tools: {', '.join(missing)}")
44
+ ui.dim(f"Install first: dev-setup install {' '.join(missing)}")
45
+ return False
37
46
  try:
38
47
  version = tool.install()
39
48
  msg = f"{tool.name} installed"
@@ -50,17 +59,20 @@ def _install_interactive() -> None:
50
59
  ui.print_banner()
51
60
  tools = registry.all_tools()
52
61
 
53
- choices = [
54
- questionary.Choice(
55
- title=(
56
- f"{'[installed] ' if t.is_installed() else ''}"
57
- f"{t.key:<14} {t.description}"
58
- ),
62
+ choices = []
63
+ for t in tools:
64
+ is_inst = t.is_installed()
65
+ missing = registry.missing_requires(t) if not is_inst else []
66
+ label = (
67
+ f"{'[installed] ' if is_inst else ''}"
68
+ f"{t.key:<14} {t.description}"
69
+ )
70
+ choices.append(questionary.Choice(
71
+ title=label,
59
72
  value=t.key,
60
73
  checked=False,
61
- )
62
- for t in tools
63
- ]
74
+ disabled=f"requires {', '.join(missing)}" if missing else False,
75
+ ))
64
76
 
65
77
  selected = questionary.checkbox(
66
78
  "Select packages to install (Space to toggle, Enter to confirm):",
@@ -31,7 +31,8 @@ def list_cmd(show_filter: str, category: str) -> None:
31
31
  ui.warn("No packages match the given filters.")
32
32
  return
33
33
 
34
- for cat in ("core", "tools", "custom"):
34
+ _ORDER = {"core": 0, "tools": 1, "custom": 999}
35
+ for cat in sorted(by_cat, key=lambda c: (_ORDER.get(c, 500), c)):
35
36
  entries = by_cat.get(cat, [])
36
37
  if not entries:
37
38
  continue
@@ -47,11 +48,14 @@ def list_cmd(show_filter: str, category: str) -> None:
47
48
  tbl.add_column("Version", style="dim")
48
49
 
49
50
  for tool, is_inst in entries:
51
+ missing = registry.missing_requires(tool) if not is_inst else []
50
52
  icon = "[green bold]✔[/]" if is_inst else "[red bold]✘[/]"
51
53
  version = tool.get_version() if is_inst else ""
52
54
  tbl.add_row(icon, tool.key, tool.description, tool.install_type, version)
53
55
  if tool.help_cmd:
54
56
  tbl.add_row("", "", f"[dim cyan] ? {tool.help_cmd}[/]", "", "")
57
+ if missing:
58
+ tbl.add_row("", "", f"[yellow] ⚠ requires: {', '.join(missing)}[/]", "", "")
55
59
 
56
60
  ui.console.print(tbl)
57
61
  ui.console.print()
@@ -10,9 +10,13 @@ from dev_setup.base import Tool
10
10
 
11
11
 
12
12
  @click.command("remove")
13
+ @click.option("--verbose", "-v", is_flag=True, help="Stream removal output to the terminal.")
13
14
  @click.argument("packages", nargs=-1)
14
- def remove_cmd(packages: Tuple[str, ...]) -> None:
15
+ def remove_cmd(packages: Tuple[str, ...], verbose: bool) -> None:
15
16
  """Uninstall installed packages."""
17
+ from dev_setup import generic
18
+ generic._verbose = verbose
19
+
16
20
  if not packages:
17
21
  ui.error("Specify at least one package key. See: dev-setup list --installed")
18
22
  sys.exit(1)