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.
- dev_setup-1.4.0/.claude/settings.local.json +11 -0
- dev_setup-1.4.0/.github/workflows/publish.yml +44 -0
- dev_setup-1.4.0/CHANGELOG.md +54 -0
- {dev_setup-1.1.0 → dev_setup-1.4.0}/PKG-INFO +27 -12
- {dev_setup-1.1.0 → dev_setup-1.4.0}/README.md +26 -11
- dev_setup-1.4.0/app-design-output/recommendations.md +100 -0
- dev_setup-1.4.0/dev/Dockerfile +38 -0
- dev_setup-1.4.0/dev/Makefile +19 -0
- dev_setup-1.4.0/dev/docker-compose.yml +14 -0
- {dev_setup-1.1.0 → dev_setup-1.4.0}/pyproject.toml +12 -1
- dev_setup-1.4.0/src/dev_setup/__init__.py +6 -0
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/base.py +19 -0
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/cli.py +2 -0
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/commands/add_cmd.py +5 -4
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/commands/delete_cmd.py +1 -6
- dev_setup-1.4.0/src/dev_setup/commands/docs_cmd.py +34 -0
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/commands/help_cmd.py +1 -0
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/commands/install_cmd.py +22 -10
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/commands/list_cmd.py +5 -1
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/commands/remove_cmd.py +5 -1
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/generic.py +42 -28
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/packages/aws_cli.py +3 -10
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/packages/docker.py +3 -10
- dev_setup-1.4.0/src/dev_setup/packages/eza.py +59 -0
- dev_setup-1.4.0/src/dev_setup/packages/gh.py +53 -0
- dev_setup-1.4.0/src/dev_setup/packages/go.py +107 -0
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/packages/htop.py +3 -10
- dev_setup-1.4.0/src/dev_setup/packages/java.py +42 -0
- dev_setup-1.4.0/src/dev_setup/packages/mkcert.py +71 -0
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/packages/nvm.py +1 -0
- dev_setup-1.4.0/src/dev_setup/packages/ollama.py +53 -0
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/packages/php.py +3 -9
- dev_setup-1.4.0/src/dev_setup/packages/pi_agent.py +63 -0
- dev_setup-1.4.0/src/dev_setup/packages/ruby.py +114 -0
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/packages/saml2aws.py +3 -11
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/packages/starship.py +3 -10
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/packages/uv_tool.py +3 -9
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/registry.py +25 -2
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/ui.py +2 -1
- dev_setup-1.4.0/uv.lock +510 -0
- dev_setup-1.1.0/src/dev_setup/__init__.py +0 -1
- dev_setup-1.1.0/uv.lock +0 -117
- {dev_setup-1.1.0 → dev_setup-1.4.0}/.gitignore +0 -0
- {dev_setup-1.1.0 → dev_setup-1.4.0}/dev-setup +0 -0
- {dev_setup-1.1.0 → dev_setup-1.4.0}/install.sh +0 -0
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/__main__.py +0 -0
- {dev_setup-1.1.0 → dev_setup-1.4.0}/src/dev_setup/commands/__init__.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
@@ -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
|
|
121
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|