Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions adds-on/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# adds-on

A small Rust binary named `uv` that acts as a **plugin dispatcher** for the real `uv`. When installed before the real `uv` in `PATH`, it makes any `uv-<name>` executable callable as `uv <name>` — similar to how `kubectl` handles plugins.

## What it does

```
uv shell → finds uv-shell in PATH → exec uv-shell
uv add requests → no uv-add plugin found → exec real uv add requests
uv <TAB> → real uv completions + discovered plugins (dynamic)
uv shell <TAB> → delegates to uv-shell's own completions
```

Any executable named `uv-<name>` on PATH is automatically discovered — no registration needed.

## Setup

**1. Add to `~/.zshrc`:**
```sh
export PATH="$(brew --prefix uv-shell)/libexec/bin:$PATH"

# Optional: skip PATH scan on every uv call
export UV_REAL_PATH="/opt/homebrew/bin/uv"
```

**2. Install completions once:**
```sh
# zsh — plugins discovered dynamically at tab-press time, no re-eval needed
uv generate-shell-completion zsh > "${fpath[1]}/_uv"

# nushell — add `use ~/.config/nushell/completions/uv.nu *` to config.nu
uv generate-shell-completion nushell | save ~/.config/nushell/completions/uv.nu

# bash
echo 'eval "$(uv generate-shell-completion bash)"' >> ~/.bashrc

# fish
uv generate-shell-completion fish > ~/.config/fish/completions/uv.fish
```

## Usage

```
uv --help show this help
uv <plugin> [args] dispatch to uv-<plugin>
uv generate-shell-completion <shell> completions with plugins injected
uv __complete list plugins (used by shell completions at tab-press)
```

## Environment

| Variable | Description |
|---|---|
| `UV_REAL_PATH` | Path to the real uv binary — skips PATH scan on every call |

## How it finds the real uv

Scans `PATH` for a binary named `uv` that is not itself (using `is_file()` + canonicalize comparison). Set `UV_REAL_PATH` to skip the scan entirely.

## License

[MIT](../LICENSE)
36 changes: 36 additions & 0 deletions adds-on/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,46 @@ use std::env;
use std::path::PathBuf;
use std::process::{Command, exit};

fn print_help() {
println!(
"uv — plugin-aware wrapper for the real uv

USAGE:
uv <command> [args...]

PLUGIN DISPATCH:
Any executable named `uv-<name>` on PATH is treated as a plugin.

uv shell [args] → finds uv-shell in PATH, execs it
uv <name> [args] → finds uv-<name> in PATH, execs it
uv <builtin> [args] → no plugin found, passes through to real uv

SPECIAL COMMANDS:
uv generate-shell-completion <shell>
→ real uv completions + plugins injected
shells: bash zsh fish nushell
uv __complete → list discovered plugins (used by shell completions)
uv --help, -h → show this message

ENVIRONMENT:
UV_REAL_PATH Skip PATH scan — point directly to the real uv binary
e.g. export UV_REAL_PATH=/opt/homebrew/bin/uv

SETUP:
export PATH=\"$(brew --prefix uv-shell)/libexec/bin:$PATH\"
uv generate-shell-completion zsh > \"\${fpath[1]}/_uv\"

Any unrecognised command is forwarded to the real uv unchanged."
);
}

fn main() {
let args: Vec<String> = env::args().collect();

match args.get(1).map(|s| s.as_str()) {
Some("-h") | Some("--help") => {
print_help();
}
// Dynamic plugin discovery — called by shell completion at tab-press time
Some("__complete") => {
for plugin in discover_plugins() {
Expand Down
4 changes: 2 additions & 2 deletions src/completions/uv-shell.bash
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ _uv_shell() {
local commands="anchor completions"

# All options
local opts="-p --python --seed -c --clear --allow-existing --prompt
local opts="-p --python --seed -c --clear --allow-existing --prefix --prompt
--system-site-packages --relocatable --no-project --no-config
--index-strategy --keyring-provider --exclude-newer --link-mode
--index --default-index -i --index-url --extra-index-url
Expand All @@ -17,7 +17,7 @@ _uv_shell() {

# Handle completions for options that take a value
case "$prev" in
-p|--python|--prompt|--index-strategy|--keyring-provider| \
-p|--python|--prefix|--prompt|--index-strategy|--keyring-provider| \
--link-mode|--exclude-newer|--exclude-newer-package| \
--index|--default-index|-i|--index-url|--extra-index-url| \
-f|--find-links|--cache-dir|--color|--directory|--project| \
Expand Down
3 changes: 2 additions & 1 deletion src/completions/uv-shell.fish
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ complete -c uv-shell -n '__fish_use_subcommand' -s p -l python -d 'Python interp
complete -c uv-shell -n '__fish_use_subcommand' -l seed -d 'Install seed packages (pip, setuptools, wheel)'
complete -c uv-shell -n '__fish_use_subcommand' -s c -l clear -d 'Re-create the venv even if .venv already exists'
complete -c uv-shell -n '__fish_use_subcommand' -l allow-existing -d 'Preserve existing files at the target path'
complete -c uv-shell -n '__fish_use_subcommand' -l prompt -d 'Custom prompt prefix (skips auto-prompt)' -r
complete -c uv-shell -n '__fish_use_subcommand' -l prefix -d 'Override project name in prompt (keeps -pyX.XX suffix)' -r
complete -c uv-shell -n '__fish_use_subcommand' -l prompt -d 'Full custom prompt (replaces auto-generated name entirely)' -r
complete -c uv-shell -n '__fish_use_subcommand' -l system-site-packages -d 'Access system site packages'
complete -c uv-shell -n '__fish_use_subcommand' -l relocatable -d 'Make the venv relocatable'
complete -c uv-shell -n '__fish_use_subcommand' -l no-project -d 'Avoid discovering a project or workspace'
Expand Down
3 changes: 2 additions & 1 deletion src/completions/uv-shell.nu
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ module uv-shell-completions {
--seed # Install seed packages (pip, setuptools, wheel)
--clear(-c) # Re-create the venv even if .venv already exists
--allow-existing # Preserve existing files at the target path
--prompt: string # Custom prompt prefix (skips auto-prompt)
--prefix: string # Override project name in prompt (keeps -pyX.XX suffix)
--prompt: string # Full custom prompt (replaces auto-generated name entirely)
--system-site-packages # Access system site packages
--relocatable # Make the venv relocatable
--no-project # Avoid discovering a project or workspace
Expand Down
3 changes: 2 additions & 1 deletion src/completions/uv-shell.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ _uv-shell() {
'--seed[Install seed packages (pip, setuptools, wheel)]'
'(-c --clear)'{-c,--clear}'[Re-create the venv even if .venv already exists]'
'--allow-existing[Preserve existing files at the target path]'
'--prompt[Custom prompt prefix (skips auto-prompt)]:prompt:'
'--prefix[Override project name in prompt (keeps -pyX.XX suffix)]:prefix:'
'--prompt[Full custom prompt (replaces auto-generated name entirely)]:prompt:'
'--system-site-packages[Give the venv access to system site packages]'
'--relocatable[Make the venv relocatable]'
'--no-project[Avoid discovering a project or workspace]'
Expand Down
7 changes: 6 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,12 @@ Options (forwarded to `uv venv`):
-v, --verbose Verbose output
-h, --help Show this help message

All other `uv venv` options are also forwarded. See `uv venv --help` for the full list."
All other `uv venv` options are also forwarded. See `uv venv --help` for the full list.

uv plugin wrapper (adds-on):
Enables `uv shell` as a native uv subcommand with plugin-aware tab completions.
Any `uv-<name>` binary on PATH becomes callable as `uv <name>`.
See the README for setup instructions."
);
}

Expand Down
Loading