devstuff 2.2.1__py3-none-any.whl → 2.3.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/catalog.py CHANGED
@@ -14,6 +14,9 @@ BUNDLED_CATALOG = "tools.yaml"
14
14
 
15
15
  VERSION = 1
16
16
  VALID_KEY = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
17
+ # Fields that only mean anything to `uv tool install`, and the types that accept them.
18
+ UV_ONLY_FIELDS = ("uv_with", "uv_executables_from", "uv_python")
19
+ UV_TYPES = ("pip", "uvx")
17
20
  SUPPORTED_FIELDS = {
18
21
  "name",
19
22
  "description",
@@ -26,6 +29,9 @@ SUPPORTED_FIELDS = {
26
29
  "requires",
27
30
  "npm_name",
28
31
  "pip_name",
32
+ "uv_with",
33
+ "uv_executables_from",
34
+ "uv_python",
29
35
  "apt_packages",
30
36
  "git_url",
31
37
  "git_install_cmd",
@@ -89,6 +95,22 @@ def validate_catalog(raw: Any, *, source: Path | str = "<catalog>") -> dict[str,
89
95
  item.setdefault("category", "custom")
90
96
  item.setdefault("type", "unknown")
91
97
 
98
+ for field in UV_ONLY_FIELDS:
99
+ if field not in item:
100
+ continue
101
+ if item["type"] not in UV_TYPES:
102
+ types = "/".join(UV_TYPES)
103
+ raise CatalogError(
104
+ f"{source}: tool {key!r} sets {field!r}, which is only valid on "
105
+ f"type {types} (got {item['type']!r})"
106
+ )
107
+ value = item[field]
108
+ if field == "uv_python":
109
+ if not isinstance(value, str):
110
+ raise CatalogError(f"{source}: tool {key!r} {field} must be a string")
111
+ elif not isinstance(value, list) or not all(isinstance(v, str) for v in value):
112
+ raise CatalogError(f"{source}: tool {key!r} {field} must be a list of strings")
113
+
92
114
  requires = item.get("requires")
93
115
  if requires is None:
94
116
  if item["type"] == "npm":
@@ -61,6 +61,20 @@ def add_cmd() -> None:
61
61
  elif install_type == "uvx":
62
62
  kwargs["pip_name"] = ui.text_input("PyPI package name:", default=key, required=True)
63
63
  kwargs["check_cmd"] = ui.text_input("Command to check if installed:", default=key)
64
+ # All optional. Empty answers must not write empty keys into the user catalog.
65
+ exes = ui.text_input(
66
+ "Also expose executables from (space-separated packages, blank for none):"
67
+ )
68
+ if exes:
69
+ kwargs["uv_executables_from"] = exes.split()
70
+ extra = ui.text_input(
71
+ "Extra packages to install alongside it (space-separated, blank for none):"
72
+ )
73
+ if extra:
74
+ kwargs["uv_with"] = extra.split()
75
+ py = ui.text_input("Pin to a Python version (e.g. 3.12, blank for uv's default):")
76
+ if py:
77
+ kwargs["uv_python"] = py
64
78
 
65
79
  elif install_type == "apt":
66
80
  kwargs["apt_packages"] = ui.text_input(
dev_setup/functions.yaml CHANGED
@@ -238,3 +238,101 @@ functions:
238
238
  saml2aws login --force
239
239
  fi
240
240
  docs_url: https://github.com/Versent/saml2aws
241
+
242
+ which-ansible:
243
+ name: Which Ansible
244
+ description: Show which ansible a directory will actually use — the project's uv venv or the global one
245
+ category: validation
246
+ type: script
247
+ params:
248
+ - name: path
249
+ description: Directory to inspect (default the current one)
250
+ required: false
251
+ default: "."
252
+ docs_url: https://docs.astral.sh/uv/guides/projects/
253
+ script: |
254
+ # No `set -e`: reporting "this project has no ansible" is a successful answer,
255
+ # not a failure, and -e would turn a stray non-zero probe into a red banner.
256
+ # Non-zero is reserved for "could not perform the lookup" (see whats-on-port).
257
+ set -uo pipefail
258
+
259
+ if ! command -v uv >/dev/null 2>&1; then
260
+ echo "uv is not installed, so there is nothing to inspect." >&2
261
+ echo "Install it with: devstuff install uv" >&2
262
+ exit 1
263
+ fi
264
+
265
+ start="${path:-.}"
266
+ if [ ! -d "$start" ]; then
267
+ echo "Not a directory: $start" >&2
268
+ exit 1
269
+ fi
270
+ start="$(cd "$start" && pwd)"
271
+
272
+ # Walk up for a uv project root, the same way uv itself does.
273
+ root=""
274
+ dir="$start"
275
+ while [ -n "$dir" ]; do
276
+ if [ -f "$dir/pyproject.toml" ] || [ -f "$dir/uv.lock" ]; then
277
+ root="$dir"
278
+ break
279
+ fi
280
+ [ "$dir" = "/" ] && break
281
+ dir="$(dirname "$dir")"
282
+ done
283
+
284
+ global="$(command -v ansible-playbook 2>/dev/null || true)"
285
+
286
+ if [ -z "$root" ]; then
287
+ echo "No uv project above $start (no pyproject.toml or uv.lock)."
288
+ if [ -n "$global" ]; then
289
+ echo "ansible-playbook here is the global one: $global"
290
+ else
291
+ echo "No ansible-playbook on PATH either — install one with: devstuff install ansible"
292
+ fi
293
+ exit 0
294
+ fi
295
+
296
+ echo "uv project root: $root"
297
+
298
+ declared=""
299
+ for f in "$root/uv.lock" "$root/pyproject.toml"; do
300
+ [ -f "$f" ] || continue
301
+ if grep -qiE '(^|[^-a-z])ansible(-core)?([^-a-z]|$)' "$f" 2>/dev/null; then
302
+ declared="$f"
303
+ break
304
+ fi
305
+ done
306
+
307
+ venv_ansible="$root/.venv/bin/ansible-playbook"
308
+ if [ -x "$venv_ansible" ]; then
309
+ echo "Project ansible: $venv_ansible"
310
+ echo " $("$venv_ansible" --version 2>/dev/null | head -1)"
311
+ echo
312
+ echo "Inside this project, run ansible through uv so it uses that one:"
313
+ echo " uv run ansible-playbook <playbook.yml>"
314
+ # `[ -n "$x" ] && echo ...` as the last statement of a branch would make the
315
+ # whole script exit 1 whenever the test is false — a correct answer reported
316
+ # as a failure. Use a full `if`.
317
+ if [ -n "$global" ]; then
318
+ echo "(A bare 'ansible-playbook' would use $global instead.)"
319
+ fi
320
+ elif [ -n "$declared" ]; then
321
+ echo "Declared in: $declared"
322
+ echo "But .venv has no ansible-playbook yet — create it with:"
323
+ echo " uv sync"
324
+ if [ -n "$global" ]; then
325
+ echo "Until then a bare 'ansible-playbook' uses the global $global."
326
+ fi
327
+ else
328
+ echo "This project does not declare ansible."
329
+ if [ -n "$global" ]; then
330
+ echo "ansible-playbook here is the global one: $global"
331
+ else
332
+ echo "No ansible-playbook on PATH — install one with: devstuff install ansible"
333
+ fi
334
+ fi
335
+
336
+ # Belt and braces for the rule above: every path that got far enough to print
337
+ # an answer is a success, whatever the last command happened to return.
338
+ exit 0
dev_setup/generic.py CHANGED
@@ -95,6 +95,12 @@ class GenericTool(Tool):
95
95
  version_cmd: str = ""
96
96
  npm_name: str = ""
97
97
  pip_name: str = ""
98
+ # uvx/pip only. `uv tool install` exposes console scripts of the *requested*
99
+ # package alone, so a package whose entry points live in a dependency (ansible's
100
+ # live in ansible-core) needs uv_executables_from or it installs nothing usable.
101
+ uv_with: list | None = None
102
+ uv_executables_from: list | None = None
103
+ uv_python: str = ""
98
104
  git_url: str = ""
99
105
  git_install_cmd: str = ""
100
106
  git_remove_cmd: str = ""
@@ -113,6 +119,12 @@ class GenericTool(Tool):
113
119
  self.name = self.key
114
120
  if self.requires is None:
115
121
  self.requires = list(AUTO_REQUIRES.get(self.install_type, []))
122
+ # Normalise to lists so the installer can iterate without None checks. Both
123
+ # stay falsy when unset, so to_dict() still omits them.
124
+ if self.uv_with is None:
125
+ self.uv_with = []
126
+ if self.uv_executables_from is None:
127
+ self.uv_executables_from = []
116
128
 
117
129
  @classmethod
118
130
  def from_dict(cls, data: dict, key: str) -> GenericTool:
@@ -214,6 +226,22 @@ def _install_npm(tool: GenericTool) -> None:
214
226
  _run(["bash", "-lc", f"{_npm_init()} && npm install -g {shlex.quote(tool.npm_name)}"])
215
227
 
216
228
 
229
+ def _uv_install_flags(tool: GenericTool) -> list[str]:
230
+ """Catalog uv_* fields as `uv tool install` flags.
231
+
232
+ uv records all of these in the tool's uv-receipt.toml and re-applies them on
233
+ `uv tool upgrade`, so they only need passing at install time.
234
+ """
235
+ flags: list[str] = []
236
+ if tool.uv_python:
237
+ flags += ["--python", tool.uv_python]
238
+ for pkg in tool.uv_with or []:
239
+ flags += ["--with", pkg]
240
+ for pkg in tool.uv_executables_from or []:
241
+ flags += ["--with-executables-from", pkg]
242
+ return flags
243
+
244
+
217
245
  def _install_uvx(tool: GenericTool) -> None:
218
246
  if not tool.pip_name:
219
247
  raise RuntimeError("pip_name not set")
@@ -224,7 +252,7 @@ def _install_uvx(tool: GenericTool) -> None:
224
252
  "Install it first: devstuff install uv"
225
253
  )
226
254
  with verbose.step(f"Installing {tool.name} via uvx..."):
227
- _run([uv, "tool", "install", tool.pip_name])
255
+ _run([uv, "tool", "install"] + _uv_install_flags(tool) + [tool.pip_name])
228
256
 
229
257
 
230
258
  def _install_git(tool: GenericTool) -> None:
@@ -284,9 +312,30 @@ def _update_uvx(tool: GenericTool, version: str | None) -> None:
284
312
  "uv is required to update uvx packages. "
285
313
  "Install it first: devstuff install uv"
286
314
  )
287
- target = f"{tool.pip_name}=={version}" if version else tool.pip_name
288
- with verbose.step(f"Updating {tool.name} via uv tool upgrade..."):
289
- _run([uv, "tool", "upgrade", target])
315
+ # Both paths go through `uv tool install`, not `uv tool upgrade`.
316
+ #
317
+ # `uv tool upgrade` takes a tool *name*, not a requirement, so the pinned form
318
+ # ("pkg==1.2.3") was read as the whole name and always failed. And it is a no-op
319
+ # on an already-pinned tool ("Nothing to upgrade"), which would leave a user who
320
+ # ever pinned with no way back to latest through devstuff. `install pkg@latest`
321
+ # re-resolves *and* clears the pin in one call — measured against uv 0.11.21.
322
+ #
323
+ # Passing the uv_* flags on every update is deliberate: this writes a fresh
324
+ # receipt, and re-deriving from the catalog means a newly added uv_with or
325
+ # uv_executables_from takes effect on update rather than only on reinstall.
326
+ target = f"{tool.pip_name}=={version}" if version else f"{tool.pip_name}@latest"
327
+ cmd = [uv, "tool", "install", "--force"] + _uv_install_flags(tool) + [target]
328
+
329
+ if version:
330
+ from dev_setup import ui
331
+ ui.warn(
332
+ f"Pinning {tool.name} to {version}. It stays there until the next "
333
+ f"'devstuff update {tool.key}' without --version, which moves it back "
334
+ f"to the latest release."
335
+ )
336
+
337
+ with verbose.step(f"Updating {tool.name} via uv tool install..."):
338
+ _run(cmd)
290
339
 
291
340
 
292
341
  def _update_apt(tool: GenericTool, version: str | None) -> None:
@@ -500,6 +549,12 @@ def _remove_npm(tool: GenericTool) -> None:
500
549
 
501
550
 
502
551
  def _remove_uvx(tool: GenericTool) -> None:
552
+ # An explicit remove_script wins, mirroring _remove_apt. Without this, a tool that
553
+ # shares a pip_name with another entry (ansible-vault shares ansible's) would
554
+ # uninstall the whole shared tool environment out from under it.
555
+ if tool.remove_script:
556
+ _run_bash_script(tool.remove_script)
557
+ return
503
558
  uv = shutil.which("uv")
504
559
  if not uv:
505
560
  raise RuntimeError(
dev_setup/tools.yaml CHANGED
@@ -4,23 +4,29 @@ tools:
4
4
  name: Ansible
5
5
  description: Automation engine for configuration management and app deployment
6
6
  category: tools
7
- type: apt
7
+ type: uvx
8
8
  check_cmd: ansible
9
- apt_packages: ansible
9
+ pip_name: ansible
10
+ # The `ansible` distribution is the collections bundle and declares no console
11
+ # scripts of its own — `ansible`, `ansible-playbook`, `ansible-vault` and the rest
12
+ # are entry points of its ansible-core dependency, and `uv tool install` only
13
+ # exposes the requested package's scripts. Without this, the install "succeeds"
14
+ # and puts a single unusable `ansible-community` on PATH. Do not swap this for
15
+ # pip_name: ansible-core — that drops the bundled collections.
16
+ uv_executables_from:
17
+ - ansible-core
10
18
  help_cmd: ansible --help
11
19
  docs_url: https://docs.ansible.com/
12
- remove_script: |
13
- set -euo pipefail
14
- sudo apt-get remove -y ansible
15
- sudo apt-get autoremove -y
16
20
 
17
21
  ansible-vault:
18
22
  name: Ansible Vault
19
23
  description: Encrypt and manage secrets within Ansible projects (bundled with Ansible)
20
24
  category: tools
21
- type: apt
25
+ type: uvx
22
26
  check_cmd: ansible-vault
23
- apt_packages: ansible
27
+ pip_name: ansible
28
+ uv_executables_from:
29
+ - ansible-core
24
30
  help_cmd: ansible-vault --help
25
31
  docs_url: https://docs.ansible.com/ansible/latest/vault_guide/index.html
26
32
  requires:
@@ -525,6 +531,50 @@ tools:
525
531
  sudo apt-get remove -y 'php8.4*'
526
532
  sudo apt-get autoremove -y
527
533
 
534
+ python:
535
+ name: Python (uv-managed)
536
+ description: CPython provisioned by uv, installed as the default python/python3
537
+ category: languages
538
+ type: bash
539
+ # Every Linux host already has a python3, so "is python3 on PATH" would report
540
+ # this installed before it ever ran. Ask uv where its interpreters live and
541
+ # require the default shim to resolve *inside* that directory. `test -x` rather
542
+ # than `test -e` because a leftover shim pointing at an uninstalled version is a
543
+ # dangling symlink — readlink still prints a matching path, but it is not usable.
544
+ # The `test -n "$d"` guard is load-bearing: with uv absent, "$d" is empty and the
545
+ # case pattern would collapse to "/*", which matches every absolute path.
546
+ check_cmd: |
547
+ bin="${UV_PYTHON_BIN_DIR:-$HOME/.local/bin}"; d="$(uv python dir 2>/dev/null)"; test -n "$d" || exit 1; case "$(readlink -f "$bin/python3" 2>/dev/null)" in "$d"/*) test -x "$bin/python3";; *) exit 1;; esac
548
+ help_cmd: uv python list
549
+ docs_url: https://docs.astral.sh/uv/guides/install-python/
550
+ requires:
551
+ - uv
552
+ install_script: |
553
+ set -euo pipefail
554
+ uv python install --default
555
+ bin="${UV_PYTHON_BIN_DIR:-$HOME/.local/bin}"
556
+ echo
557
+ echo "'python' and 'python3' in $bin now resolve to uv's CPython."
558
+ echo "Since uv puts $bin early on PATH, that is what your shell will pick."
559
+ echo "The system interpreter at /usr/bin/python3 is untouched, and anything"
560
+ echo "with an explicit /usr/bin/python3 shebang keeps using it."
561
+ remove_script: |
562
+ set -euo pipefail
563
+ bin="${UV_PYTHON_BIN_DIR:-$HOME/.local/bin}"
564
+ target="$(readlink -f "$bin/python3" 2>/dev/null || true)"
565
+ if [ -z "$target" ] || [ ! -x "$target" ]; then
566
+ echo "No uv-managed default python3 found in $bin — nothing to remove."
567
+ exit 0
568
+ fi
569
+ # Uninstall only the version the default shim points at. `uv python uninstall
570
+ # --all` would also take out managed interpreters that existing `uv tool`
571
+ # environments were built against, breaking tools this command was not asked
572
+ # to touch.
573
+ version="$("$target" -c 'import sys; print("%d.%d.%d" % sys.version_info[:3])')"
574
+ uv python uninstall "$version"
575
+ echo "Removed uv-managed Python $version and its python/python3 shims."
576
+ echo "Other uv-managed versions and /usr/bin/python3 were left alone."
577
+
528
578
  pre-commit:
529
579
  name: pre-commit
530
580
  description: Git hook manager for automated code quality checks
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: devstuff
3
- Version: 2.2.1
3
+ Version: 2.3.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>
@@ -287,7 +287,7 @@ How "update" is performed depends on the package's install `type`:
287
287
  | Type | Latest | Specific version |
288
288
  |------|--------|-------------------|
289
289
  | `npm` | `npm install -g <pkg>@latest` | `npm install -g <pkg>@<version>` |
290
- | `pip` / `uvx` | `uv tool upgrade <pkg>` | `uv tool upgrade <pkg>==<version>` |
290
+ | `pip` / `uvx` | `uv tool install --force <pkg>@latest` | `uv tool install --force <pkg>==<version>` |
291
291
  | `apt` | `apt-get install --only-upgrade` | `apt-get install <pkg>=<version>` (single package only) |
292
292
  | `git` | `git pull` (+ re-run `git_install_cmd`) | not supported — repos are cloned shallow (`--depth=1`) |
293
293
  | `script` / `bash` | Re-runs the install script | not supported — no version parameter to inject |
@@ -296,6 +296,17 @@ For `script`/`bash` packages, "update" is a full reinstall (the same script that
296
296
  `sudo` runs again), since there's no narrower update mechanism available. `devstuff update` asks
297
297
  for confirmation before doing this.
298
298
 
299
+ `pip`/`uvx` packages go through `uv tool install` rather than `uv tool upgrade` in **both**
300
+ columns. `uv tool upgrade` takes a tool *name*, so it cannot accept `<pkg>==<version>` at all, and
301
+ it is a no-op on a tool that was previously pinned — using it for the "latest" column would mean
302
+ pinning a package once left no way to move it again. `<pkg>@latest` re-resolves and clears any
303
+ existing pin in one step. Pinning warns that the pin holds until the next `devstuff update`
304
+ without `--version`.
305
+
306
+ Any `uv_with` / `uv_executables_from` / `uv_python` fields on the package are re-applied on every
307
+ update, so adding one to your catalog takes effect on the next update rather than needing a
308
+ reinstall.
309
+
299
310
  ---
300
311
 
301
312
  ### `configure`
@@ -1132,6 +1143,7 @@ ones tools already have — for now, custom functions are hand-edited YAML at
1132
1143
  | `whats-on-port` | network | script | Find which process is listening on a port | `port`, `protocol` (optional: `tcp`/`udp`/`all`) |
1133
1144
  | `acc-check` | web-dev | script | Run the pi coding agent's `/dogfood` skill against a web URL | `url`, `instruction` (optional) |
1134
1145
  | `aws-saml-reauth` | web-dev | script | Reauthorize the AWS CLI via `saml2aws login --force` | `profile` (optional) |
1146
+ | `which-ansible` | validation | script | Show which ansible a directory will actually use — the project's uv venv or the global one | `path` (optional) |
1135
1147
 
1136
1148
  #### `whats-on-port`
1137
1149
 
@@ -1163,6 +1175,32 @@ so a missing `ss` names the apt package instead of surfacing "command not found"
1163
1175
 
1164
1176
  ---
1165
1177
 
1178
+ #### `which-ansible`
1179
+
1180
+ ```bash
1181
+ devstuff run which-ansible # the current directory
1182
+ devstuff run which-ansible ~/infra # somewhere else
1183
+ ```
1184
+
1185
+ Answers "if I type `ansible-playbook` here, which one runs?" — the project's own uv venv, or the
1186
+ global tool install. It walks up for a `pyproject.toml`/`uv.lock` the way uv does, then reports
1187
+ whichever applies:
1188
+
1189
+ - a synced project venv, plus the `uv run ansible-playbook` invocation that uses it (and what a
1190
+ bare `ansible-playbook` would pick instead);
1191
+ - ansible declared but not synced yet, pointing at `uv sync`;
1192
+ - no ansible in the project, or no project at all, naming the global one.
1193
+
1194
+ Every one of those is a success and exits 0 — including "this project has no ansible", which is
1195
+ an answer, not a failure. Non-zero is reserved for not being able to look at all (no uv, or a
1196
+ path that isn't a directory).
1197
+
1198
+ It exists because devstuff's ansible and a uv-managed ansible repo are now the *same shape* — both
1199
+ a uv-created venv — so the question stopped being "which tool is broken" and became "which
1200
+ environment am I in". See `docs/specs/uv-backbone/` for why the global one moved off apt.
1201
+
1202
+ ---
1203
+
1166
1204
  ## Built-in packages
1167
1205
 
1168
1206
  ### Core
@@ -1182,13 +1220,18 @@ Optional utilities you may want on some machines.
1182
1220
  | Key | Name | Description | Help |
1183
1221
  |-----|------|-------------|------|
1184
1222
  | `ansible` | Ansible | Automation engine for configuration management and app deployment (`devstuff configure ansible`) | `ansible --help` |
1223
+ | `ansible-vault` | Ansible Vault | Encrypt and manage secrets within Ansible projects (bundled with Ansible) | `ansible-vault --help` |
1185
1224
  | `aws` | AWS CLI | Amazon Web Services CLI v2 | `aws help` |
1186
1225
  | `bat` | bat | cat replacement with syntax highlighting and git integration (`devstuff configure bat`) | `bat --help` |
1187
1226
  | `commitizen` | Commitizen | Conventional-commit prompt, semantic version bumping, and changelog generation (`devstuff configure commitizen`) | `cz --help` |
1188
- | `eza` | eza | Modern ls replacement with git status, icons, and tree view | `eza --help` |
1189
1227
  | `gh` | GitHub CLI | GitHub's official CLI | `gh --help` |
1228
+ | `git-lfs` | Git LFS | Git extension for versioning large files | `git lfs --help` |
1190
1229
  | `htop` | htop | Interactive process and resource monitor | `man htop` |
1230
+ | `homebrew` | Homebrew | Package manager for Linux (and macOS) | `brew --help` |
1231
+ | `ipython` | IPython | Enhanced interactive Python shell with rich tab completion and magic commands | `ipython --help` |
1191
1232
  | `lazygit` | lazygit | TUI git client for fast, keyboard-driven git workflows (`devstuff configure lazygit`) | `lazygit --help` |
1233
+ | `llm-checker` | llm-checker | Check LLM compatibility with your hardware | `llm-checker --help` |
1234
+ | `lmstudio` | LM Studio | Run large language models locally with a GUI | `lms --help` |
1192
1235
  | `mkcert` | mkcert | Zero-config local HTTPS certificates | `mkcert --help` |
1193
1236
  | `nerd-font` | JetBrainsMono Nerd Font | Patched font supplying the icons Starship and other CLI tools draw | `fc-list \| grep -i "nerd font"` |
1194
1237
  | `ollama` | Ollama | Run large language models locally | `ollama --help` |
@@ -1205,6 +1248,7 @@ Optional utilities you may want on some machines.
1205
1248
  |-----|------|-------------|------|
1206
1249
  | `go` | Go | Go programming language toolchain | `go help` |
1207
1250
  | `java` | Java 21 (OpenJDK) | OpenJDK 21 LTS - JDK and JRE | `java --help` |
1251
+ | `python` | Python (uv-managed) | CPython provisioned by uv, installed as the default `python`/`python3` | `uv python list` |
1208
1252
  | `ruby` | Ruby (rbenv) | Ruby via rbenv version manager + ruby-build | `ruby --version` |
1209
1253
 
1210
1254
  ---
@@ -1247,7 +1291,10 @@ tools:
1247
1291
  | `docs_url` | no | URL opened by `devstuff docs <key>` |
1248
1292
  | `requires` | no | List of package keys that must already be installed |
1249
1293
  | `npm_name` | npm | npm package name |
1250
- | `pip_name` | pip | PyPI package name |
1294
+ | `pip_name` | pip | PyPI package name. Extras work as-is: `pip_name: "ansible-lint[lock]"` |
1295
+ | `uv_with` | no (pip/uvx) | Extra packages installed into the tool's environment (`uv tool install --with`) |
1296
+ | `uv_executables_from` | no (pip/uvx) | Also expose console scripts from these packages (`--with-executables-from`). Needed when a package's entry points live in a dependency |
1297
+ | `uv_python` | no (pip/uvx) | Pin the tool environment's Python (`--python`), e.g. `"3.12"` |
1251
1298
  | `apt_packages` | apt | Space-separated list of apt packages |
1252
1299
  | `git_url` | git | Repository URL to clone |
1253
1300
  | `git_install_cmd` | git | Bash command run inside the cloned repo after clone |
@@ -1256,7 +1303,21 @@ tools:
1256
1303
  | `install_script` | bash | Full bash script to run on install |
1257
1304
  | `remove_script` | bash | Full bash script to run on remove |
1258
1305
 
1259
- Unknown fields fail validation. `requires` defaults to `["nvm"]` for `npm` tools and `["uv"]` for `pip`/`uvx` tools unless explicitly set.
1306
+ Unknown fields fail validation — including the three `uv_*` fields on any type other than `pip`/`uvx`. `requires` defaults to `["nvm"]` for `npm` tools and `["uv"]` for `pip`/`uvx` tools unless explicitly set.
1307
+
1308
+ `uv_executables_from` is worth understanding before you skip it: `uv tool install` exposes the console scripts of the *requested* package only. The `ansible` distribution is a collections bundle that declares none of its own — `ansible`, `ansible-playbook` and `ansible-vault` are all entry points of its `ansible-core` dependency — so without this field the install reports success and puts a single unusable `ansible-community` on `PATH`:
1309
+
1310
+ ```yaml
1311
+ version: 1
1312
+ tools:
1313
+ ansible:
1314
+ name: Ansible
1315
+ type: uvx
1316
+ check_cmd: ansible
1317
+ pip_name: ansible
1318
+ uv_executables_from:
1319
+ - ansible-core
1320
+ ```
1260
1321
 
1261
1322
  ### Examples
1262
1323
 
@@ -3,17 +3,17 @@ dev_setup/__main__.py,sha256=w-Vnqe3bgAw_UNlPywPkbOqoFKcEk2H2S79wrQjXvU4,102
3
3
  dev_setup/agent_tools.schema.json,sha256=veXnih00xW4w9yK3ruWz-npUrCnJmcQpQdFvfcu7v_I,4928
4
4
  dev_setup/agent_tools.yaml,sha256=56E70B1W7hBqjGhYhhCAGzFQvi1nUZ04cnNiqZlMjyY,4506
5
5
  dev_setup/base.py,sha256=j2EQ5gzeGgtveWQN_BgbLcZianMENJ7PQy8EQ9J9MPE,1946
6
- dev_setup/catalog.py,sha256=YST_dpTpIh08aLz1FBziywtKX_zFfhlU5fr56hgf0mA,5514
6
+ dev_setup/catalog.py,sha256=PrimTBUYoh8cW9QYhXUxP8qSMeK51kzQjUoPEGD9EBw,6530
7
7
  dev_setup/cli.py,sha256=Y_RbHkexTAIQN4neDQ3buo6vmtTMNdXrsYsmY0X_Om8,2976
8
8
  dev_setup/doctor.py,sha256=JliUAgmiW2HVCWHhT76PIXTBekNXXrc0VfDMaOeLBWY,15918
9
9
  dev_setup/function_runner.py,sha256=08F6jvB9-oOyE5ivYtRjnL16JTGsfRnkpvkjSVrJhsM,6957
10
10
  dev_setup/functions.schema.json,sha256=VVdpH3xWsdbtqw47S1QqpBcs4O9wd3XZ_s0Q3sGwv0Q,6220
11
- dev_setup/functions.yaml,sha256=IZbdIsfRkQEvgYOjEtBMYxD-TRdqKCsoF1siWTHEYtE,9173
11
+ dev_setup/functions.yaml,sha256=MYeBgR6mkpJZDcoSW88lCIfrtayvJViY8BZxjxedM7o,12734
12
12
  dev_setup/functions_catalog.py,sha256=YgR2hY9HM1iol1AZcF-_j02EXCP9U4P3e_5eWes9KH0,7116
13
13
  dev_setup/functions_registry.py,sha256=YSl-VgNXPf0hj4WgReg-pZUj4fAjFlwvC165dgon_BE,3682
14
- dev_setup/generic.py,sha256=S-5kHtvXM2xVmlJ4siZvwbaQFnkodZbuaCcvc4_1yDI,25788
14
+ dev_setup/generic.py,sha256=Tr7YAkS_SXjExJpyNw3palDuaiNn1TcAoeZ0wHdXIeM,28424
15
15
  dev_setup/registry.py,sha256=8lrk6EXCCWk1WNeNnCGLTJ-v8rpCs7-TLYtVXgiuxnA,2243
16
- dev_setup/tools.yaml,sha256=3r8XV3UBJqO6WWrFlVBJ2BQ_c7r6zk0zpkrNyVXJolI,27562
16
+ dev_setup/tools.yaml,sha256=5etktWY2tRAm7l4CG34wspdw9M-zipuhhvmJHg5ufJQ,30431
17
17
  dev_setup/ui.py,sha256=Z5nNZV8MN8tAjo1ZWq7-QPZYkPKQKuM7JbmGtiuKTqM,3806
18
18
  dev_setup/verbose.py,sha256=N3U9JAYOvFwdd8DT6W7Pqq6RCIUfbcQnPsm0O1TnvQY,5003
19
19
  dev_setup/agent/__init__.py,sha256=znrgxUCr_y0Qp-f41S6EaTv-hoTYwoZ9mOMLVge-IJg,226
@@ -32,7 +32,7 @@ dev_setup/agent/session.py,sha256=KxBBbFFvO5bBbDJ6H94O7tgtrQ2i5kGq6EsXCHaDvPo,11
32
32
  dev_setup/agent/transcript.py,sha256=hJQuSLriAe5hvnD3SCMqOWduuYtblyhmqzZQ3jDco1Y,1868
33
33
  dev_setup/agent/wizard.py,sha256=p0vHiJ-palTMoL6p7zQscL01laBFKw9f5HHY1yemZ4w,4056
34
34
  dev_setup/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
- dev_setup/commands/add_cmd.py,sha256=rBdPoY6DiMLz26MewXE-lobepOYhe-55bA8GP4hrx9w,11666
35
+ dev_setup/commands/add_cmd.py,sha256=dqgagwBDkeK47RTnIREf0PlXjfLHQpxjdQ0kHJqLqDE,12290
36
36
  dev_setup/commands/agent_cmd.py,sha256=TtsCzNVXgwxS2ACFLFl6ibnjoohEQWXgM2rfWkBnKk0,5128
37
37
  dev_setup/commands/catalog_cmd.py,sha256=dz_0UfZy3v622sbJ4AWiNftZr6yYtsjaBQw1z7UF4Lk,1002
38
38
  dev_setup/commands/configure_cmd.py,sha256=-6UQhrcdEpf6AecDfGq7otq2G4rqdeRPjJUV1lze2Zg,3738
@@ -90,7 +90,7 @@ dev_setup/configure/starship/model.py,sha256=HNkIj69DqBQFAq0VTHqPLNZT2DuBZSfF-5y
90
90
  dev_setup/configure/starship/preview.py,sha256=ZaNhNA3AeHHtPnMqrJbB9XVVXDFbH1R36jm8xyYdt2U,6145
91
91
  dev_setup/configure/starship/render.py,sha256=gDE0hbGZ9oDCR1VAZQRDm5EFYviMjj0-eSmr1EnZfn8,10262
92
92
  dev_setup/configure/starship/wizard.py,sha256=BmWl-U6ttF79gepDhCv8UxT8ZDIMe1SWnhSkA1iIrGQ,15498
93
- devstuff-2.2.1.dist-info/METADATA,sha256=QLY5Y0oqXNKDKuGz7uCso5sWbXNCyE927_czjOKC_Pg,68408
94
- devstuff-2.2.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
95
- devstuff-2.2.1.dist-info/entry_points.txt,sha256=48ITy9ENdXDFrTHwAyVPUpq1MDmgFqcOisOo5mAR6OQ,53
96
- devstuff-2.2.1.dist-info/RECORD,,
93
+ devstuff-2.3.0.dist-info/METADATA,sha256=GN8skkw5mqOAce6_TDbHMk7SFX5EUiPQAL8w0O7Chek,72225
94
+ devstuff-2.3.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
95
+ devstuff-2.3.0.dist-info/entry_points.txt,sha256=48ITy9ENdXDFrTHwAyVPUpq1MDmgFqcOisOo5mAR6OQ,53
96
+ devstuff-2.3.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.31.0
2
+ Generator: hatchling 1.32.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any