vmn 0.1.2

Fast local Python virtual environment manager with zsh/fzf integration
Documentation

vmn

vmn is a fast local Python virtual environment manager for developers who work across many projects.

It keeps a SQLite registry of known virtual environments, gives you optional zsh/bash/fzf shell integration for switching between them, and can create, discover, inspect, refresh, remove, and prune environments from one CLI.

With shell integration installed, the daily workflow is:

v

Pick an environment in fzf. vmn changes into that environment's project directory when one is known, then activates the venv in your current shell.

Features

  • Create centrally managed venvs.
  • Create project-local ./.venv environments.
  • Scan project folders for existing .venv directories.
  • Activate environments from anywhere with optional zsh/bash/fzf integration.
  • Inspect Python, pip, package, path, status, and source metadata.
  • Mark missing environments instead of silently deleting registry records.
  • Safely remove registry entries and optionally delete venv directories.
  • Use scriptable output contracts for shell integration.

Requirements

  • Rust/Cargo for installation.
  • Python 3 with the standard venv module.
  • Optional: fzf for the interactive shell picker.
  • Optional: zsh or bash for generated shell integration.

vmn targets Unix-like developer environments: Linux, macOS, and WSL2. The CLI commands work without shell integration or fzf. Shell integration is only needed for the v picker and for vmn activate/vmn deactivate to mutate the current parent shell. Without shell integration, vmn activate <selector> starts a child shell with the venv active.

Install

From crates.io:

cargo install vmn

From a local checkout:

cargo install --path .

Verify:

vmn --version
vmn doctor

Shell Setup

Initialize local state and print shell integration for your shell:

vmn init --shell zsh
vmn init --shell bash

Append the printed snippet to your shell config:

zsh:  ~/.zshrc
bash: ~/.bashrc

Then restart your shell:

exec zsh
# or
exec bash

The snippet defines:

v             open the VMN fzf picker, cd to the project, activate the venv
vd            deactivate the current Python venv
Ctrl-F        open the VMN picker
Ctrl-Shift-F  deactivate when supported by the terminal
Ctrl-X Ctrl-F deactivate fallback

Many terminals cannot distinguish Ctrl-F from Ctrl-Shift-F. When that happens, use vd or Ctrl-X Ctrl-F to deactivate.

If fzf is not installed, the CLI still works. Use:

vmn list
vmn activate <selector>

Quick Start

Create a managed environment:

vmn create api

Create a project-local environment:

cd ~/Projects/api
vmn create api --here

Scan existing project venvs:

vmn scan ~/Projects

Open the picker:

v

Activate directly without the picker or shell integration:

vmn activate api
exit

Deactivate:

vd

or use Python venv's standard function:

deactivate

Managed vs Scanned Environments

vmn supports two common styles.

Managed environments live under the VMN data directory:

~/.local/share/vmn/envs/<name>

Project-local environments live in your project:

~/Projects/my-app/.venv

Use managed envs when you want named environments independent of a project folder. Use --here or scan when each project owns its own .venv.

Commands

vmn pythons

List Python interpreters discovered on PATH.

vmn pythons
vmn pythons --json

Use this before creating a venv when multiple Python versions are installed.

vmn init

Create config/data directories, initialize the SQLite registry, and print shell integration.

vmn init --shell zsh
vmn init --shell bash

vmn list

List registered environments.

vmn list
vmn list --fzf
vmn list --json
vmn list --include-missing
vmn list --all

--fzf prints tab-separated rows for shell integration:

<id> <name> <status> <path> <python_version> <last_used_at>

vmn create

Create and register a new venv.

vmn create api
vmn create api --here
vmn create api --path ~/scratch/api-venv
vmn create api --python 3.12
vmn create api --python python3.12
vmn create api --python /opt/homebrew/bin/python3.12

By default, vmn create <name> creates a managed environment. --here creates ./.venv in the current directory. --python accepts an executable name, a full executable path, or a version selector such as 3.11 or 3.12.

vmn scan

Discover existing venvs under one or more directories.

vmn scan ~/Projects
vmn scan ~/Projects ~/Work --max-depth 5
vmn scan ~/Projects --dry-run
vmn scan ~/Projects --json

Scanning detects venv roots with pyvenv.cfg and an activation script. Permission-denied paths are skipped and counted instead of crashing the scan.

vmn info

Show cached metadata for one environment.

vmn info api
vmn info api --packages
vmn info api --json
vmn info api --live

Default info reads SQLite only. Use --live to probe Python/pip before printing.

vmn refresh

Refresh Python, pip, and package metadata.

vmn refresh api
vmn refresh --all

Package capture can be slower than registry reads, so vmn does it explicitly through refresh or info --live.

vmn path

Print only the venv root path.

vmn path api

vmn project-dir

Print the associated project directory when known. Managed envs without a project print nothing.

vmn project-dir api

vmn activate

Activate an environment by selector.

vmn activate api

With shell integration installed, this changes the current shell, changes into the associated project directory when one is known, and sources the venv activation script.

Without shell integration, vmn cannot modify its parent shell, so it starts a child shell with VIRTUAL_ENV and PATH configured for the selected venv. Type exit to return to the previous shell.

vmn deactivate

Deactivate the current environment when shell integration is installed.

vmn deactivate

Without shell integration, leave a standalone activated subshell with exit.

vmn activate-path

Print only the activation script path and update last_used_at.

source "$(vmn activate-path api)"

The shell integration uses this internally. Prefer vmn activate <selector> for normal interactive use.

vmn doctor

Check local health.

vmn doctor

Checks include writable config/data directories, database availability, Python, optional fzf availability, missing active paths, activation scripts, and duplicate names.

vmn remove

Remove an environment from active use.

vmn remove api
vmn remove api --delete-files
vmn remove api --delete-files --yes

Without --delete-files, vmn marks the environment deleted in the registry and leaves files alone.

With --delete-files, vmn requires the target directory to look like a venv before deleting it. It refuses to delete paths without pyvenv.cfg and an activation script.

vmn prune

Delete stale registry records.

vmn prune --dry-run
vmn prune --missing
vmn prune --deleted
vmn prune --deleted --yes

Selectors

Most commands accept:

  • a full environment id
  • a unique id prefix
  • a unique name

If a name or prefix is ambiguous, vmn fails and lists matching ids and paths.

Data Locations

On Linux and WSL2/XDG-style systems:

~/.config/vmn/config.toml
~/.local/share/vmn/vmn.db
~/.local/share/vmn/envs/

On macOS, vmn uses the platform-standard application support directory through Rust's directories crate, typically under:

~/Library/Application Support/vmn/

For tests or isolated usage, override locations:

export VMN_CONFIG_DIR=/tmp/vmn-config
export VMN_DATA_DIR=/tmp/vmn-data

Reset all local VMN state:

rm -rf ~/.config/vmn ~/.local/share/vmn

This deletes VMN-managed environments under ~/.local/share/vmn/envs/. It does not delete project-local .venv directories elsewhere.

Troubleshooting

fzf is missing

fzf is optional for the CLI and required for the v picker. Install fzf if you want interactive selection, then rerun:

vmn doctor

Without fzf, use direct activation:

vmn list
vmn activate <selector>

v is not found

Restart your shell:

exec zsh
# or
exec bash

Then check:

type v

The picker opens but activation does not stick

Activation that persists in the current shell must happen through shell integration from vmn init --shell zsh or vmn init --shell bash. Without that integration, vmn activate <selector> intentionally opens an activated child shell instead.

pip reports externally-managed-environment

That message means system Python/pip is being used instead of the venv. Activate with:

vmn activate <selector>

If shell integration is not installed, this opens a child shell. Install packages inside that shell, then type exit when done.

I selected an env but it did not cd where I expected

Check the project directory:

vmn info <name-or-id>
vmn project-dir <name-or-id>

Managed envs usually have no project directory. Project-local envs created with --here or discovered by scan should have one.

A path is missing

If a venv is deleted manually, vmn marks it missing instead of silently deleting it.

vmn doctor
vmn prune --missing --dry-run
vmn prune --missing --yes

A selector is ambiguous

Use the id prefix shown by:

vmn list --all

Package metadata looks stale

Refresh it:

vmn refresh <name-or-id>

Development

Run checks:

cargo fmt
cargo test --all-targets --all-features
cargo clippy --all-targets --all-features -- -D warnings

Install locally:

cargo install --path . --force

Package check:

cargo package --list
cargo publish --dry-run

License

MIT. See LICENSE.