# xnv — Python Virtual Environment Doctor

Diagnose and fix broken Python virtual environments, stale poetry locks, and pip issues.

**Zero dependencies. Single file. Works when pip is broken.**

```bash
python3 xnv.py fix   # repairs venv without needing pip
```

## Table of Contents

- [Why?](#why)
- [Quick Start](#quick-start-no-install-needed)
- [Installation](#installation-optional)
- [Commands](#commands)
- [Error Codes](#error-codes)
- [Troubleshooting](#troubleshooting)
- [Architecture](#architecture)
- [Python API](#python-api)
- [License](#license)

## Why?

Common Python environment failures:

| Problem | Cause | xnv solution |
|---|---|---|
| `pip: cannot execute: required file not found` | venv symlinks point to deleted/upgraded Python | Detect broken symlinks → recreate venv |
| `poetry.lock out of sync` | pyproject.toml edited without `poetry lock` | Run `poetry lock` automatically |
| Conda + venv conflicts | PATH confusion between conda base and project venv | Detect CONDA_ACTIVE vs VIRTUAL_ENV mismatch |
| `pyvenv.cfg` stale home | System Python relocated after venv creation | Check pyvenv.cfg home dir exists |
| Broken pip shim | pip binary broken but `python -m pip` works | Reinstall pip via ensurepip |

When pip is broken, you can't `pip install` a fix tool. **xnv solves this by running as a single file with zero dependencies.**

## Quick Start (no install needed)

```bash
# 1. Download/copy xnv.py to your project
wget https://raw.githubusercontent.com/youruser/xnv/main/xnv.py
# or simply copy the file manually

# 2. Diagnose
python3 xnv.py                          # current directory
python3 xnv.py doctor /path/to/project  # specific project

# 3. Fix auto-magically
python3 xnv.py fix                      # diagnose + repair + install deps
python3 xnv.py fix --force              # force recreate venv
python3 xnv.py fix --no-install         # repair only, skip pip install

# 4. Manual operations
python3 xnv.py nuke                     # remove venv completely
python3 xnv.py create                   # create fresh venv
python3 xnv.py lock                     # regenerate poetry.lock
```

## Installation (optional)

If you want the `xnv` shortcut command:

```bash
# Development install (editable)
pip install -e .

# Or use pipx for isolated install
pipx install .

# Then use `xnv` instead of `python3 xnv.py`
xnv doctor
xnv fix
```

## Commands

### `doctor [path]` — Diagnose environment

```bash
$ python3 xnv.py doctor

xnv doctor — /home/user/myproject

  ✓ [VENV_PYTHON_OK] venv Python: Python 3.12.4
  ✓ [VENV_PIP_OK] venv pip: pip 24.0
  ⚠ [NO_PYPROJECT] No pyproject.toml found
  ✗ [POETRY_LOCK_STALE] poetry.lock out of sync with pyproject.toml
      hint: poetry lock

  1 errors / 1 warnings / 2 ok

✗ Issues found — run 'python3 xnv.py fix' to repair
```

### `fix [options] [path]` — Auto-repair

Options:
- `--force, -f` — Force recreate venv even if it appears healthy
- `--no-install` — Skip dependency installation (repair only)
- `--dev` — Install dev dependencies too (with Poetry)

```bash
# Standard repair
python3 xnv.py fix

# Force clean slate
python3 xnv.py fix --force

# Repair without installing packages
python3 xnv.py fix --no-install
```

What `fix` does:
1. Runs full diagnosis
2. If poetry.lock stale → `poetry lock`
3. If venv broken → `rm -rf venv && python3 -m venv venv`
4. Upgrades pip in new venv
5. Installs dependencies (Poetry preferred, pip fallback)
6. Verifies the fix

### `nuke [path]` — Remove venv

```bash
python3 xnv.py nuke              # remove ./venv
python3 xnv.py nuke --name .venv # remove ./.venv
```

### `create [options] [path]` — Create venv

```bash
python3 xnv.py create                    # create ./venv
python3 xnv.py create --name .venv       # create ./.venv
python3 xnv.py create --force            # recreate if exists
```

### `lock [path]` — Fix poetry.lock

```bash
python3 xnv.py lock   # runs 'poetry lock'
```

## Error Codes

| Code | Severity | Description | Auto-fixed by `fix`? |
|---|---|---|---|
| `VENV_BROKEN_PYTHON` | error | Python symlink points to missing target | ✅ Yes |
| `VENV_NO_PYTHON` | error | Python binary missing from venv | ✅ Yes |
| `VENV_PYTHON_FAIL` | error | Python binary not executable | ✅ Yes |
| `VENV_BROKEN_PIP` | error | pip symlink broken | ✅ Yes |
| `VENV_NO_PIP` | error | pip binary missing | ✅ Yes (via ensurepip) |
| `VENV_PIP_FAIL` | error | pip not functional | ✅ Yes |
| `VENV_PIP_SHIM_BROKEN` | error | pip shim broken but `python -m pip` works | ✅ Yes (reinstall pip) |
| `VENV_STALE_HOME` | error | pyvenv.cfg home dir missing | ✅ Yes |
| `POETRY_LOCK_STALE` | error | poetry.lock out of sync | ✅ Yes |
| `VENV_MISMATCH` | warn | Active VIRTUAL_ENV ≠ project venv | ❌ Manual fix |
| `NO_VENV` | warn | No venv found | ✅ Yes (creates one) |
| `NO_PYPROJECT` | warn | No pyproject.toml | ❌ N/A |
| `POETRY_NO_LOCK` | warn | poetry.lock missing | ✅ Yes |
| `CONDA_ACTIVE` | info | Conda environment detected | — |
| `VENV_EXTERNAL` | info | Using external venv | — |
| `VENV_PYTHON_OK` | info | Python works | — |
| `VENV_PIP_OK` | info | pip works | — |
| `POETRY_LOCK_OK` | info | poetry.lock in sync | — |

## Troubleshooting

### "pip: cannot execute: required file not found"

**Symptom**: `/venv/bin/pip: cannot execute: required file not found`

**Cause**: venv symlinks point to old Python location (e.g., after conda update)

**Fix**:
```bash
python3 xnv.py fix --force
```

### poetry.lock keeps being stale

**Symptom**: `POETRY_LOCK_STALE` keeps appearing

**Cause**: pyproject.toml dependencies changed without updating lock

**Fix**:
```bash
python3 xnv.py lock      # regenerate lock
python3 xnv.py fix       # reinstall with new lock
```

### Conda base env active

**Symptom**: `CONDA_ACTIVE` shows `(base)` even though you want project venv

**Fix**:
```bash
conda deactivate
python3 xnv.py fix
source venv/bin/activate
```

### Venv exists but xnv says "NO_VENV"

**Symptom**: `venv/` exists but xnv doesn't detect it

**Check**: Does `venv/pyvenv.cfg` exist? If not, it's not a proper venv.

**Fix**:
```bash
python3 xnv.py fix --force
```

## Architecture

### Two modes of operation

```
┌─────────────────┐     ┌─────────────────┐
│  Standalone     │     │  Package        │
│  xnv.py         │     │  pip install -e │
│                 │     │                 │
│  • Zero deps    │     │  • CLI shortcut │
│  • Copy anywhere│     │  • Importable   │
│  • Works when   │     │    API          │
│    pip broken   │     │                 │
└─────────────────┘     └─────────────────┘
         │                       │
         └───────────┬───────────┘
                     │
              ┌──────▼──────┐
              │  Core logic  │
              │  (diagnose, │
              │   fix)       │
              └─────────────┘
```

### Detection logic

```python
def diagnose(project_dir) -> Diagnosis:
    1. Find venv (venv, .venv, env, .env)
    2. Check Python binary exists & is executable
    3. Check pip binary exists & works
    4. Check pyvenv.cfg home dir valid
    5. Check poetry.lock sync (if poetry available)
    6. Check conda vs venv conflicts
    7. Return list of Issues
```

## Makefile Integration

Add to your project's Makefile:

```makefile
# Use standalone xnv.py (no install needed)
fix-env:
	@python3 $(XNV_PATH)/xnv.py fix .

doctor-env:
	@python3 $(XNV_PATH)/xnv.py doctor .

# Or if xnv installed
fix-env:
	@xnv fix .
```

## Python API

For programmatic use after `pip install -e .`:

```python
from xnv.diagnose import diagnose, Issue
from xnv.fix import fix

# Diagnose
 diag = diagnose("/path/to/project")
print(f"Healthy: {diag.healthy}")
print(f"Venv: {diag.venv_path}")

for issue in diag.issues:
    print(f"[{issue.severity}] {issue.code}: {issue.message}")
    if issue.fix_hint:
        print(f"  Hint: {issue.fix_hint}")

# Fix
ok = fix("/path/to/project", force_recreate=False, install=True, dev=False)
if ok:
    print("Environment fixed!")
else:
    print("Some issues remain")
```

### Data classes

```python
@dataclass
class Issue:
    code: str           # e.g., "VENV_BROKEN_PYTHON"
    severity: str       # "error", "warn", "info"
    message: str        # Human-readable description
    fix_hint: str       # Suggested fix command

@dataclass
class Diagnosis:
    project_dir: Path
    venv_path: Path | None
    python_path: Path | None
    pip_path: Path | None
    poetry_available: bool
    poetry_lock_stale: bool
    conda_active: bool
    issues: list[Issue]
    
    @property
    def healthy(self) -> bool: ...
    @property
    def errors(self) -> list[Issue]: ...
    @property
    def warnings(self) -> list[Issue]: ...
```

## How it works (single file)

`xnv.py` is a self-contained Python script that:

1. Uses **only stdlib** (`argparse`, `dataclasses`, `pathlib`, `subprocess`, etc.)
2. Implements all diagnosis/fix logic inline
3. Has zero external dependencies
4. Can be copied to any project and run immediately

This is the **bootstrap problem solution**: when your package manager (pip/poetry) is broken, you can't use it to install a fix tool. xnv bypasses this by not needing installation.

## Contributing

1. Fork and clone
2. `pip install -e ".[dev]"`
3. `python3 -m pytest tests/ -v`
4. Make changes, add tests
5. Submit PR

## License

Apache License 2.0 - see [LICENSE](LICENSE) for details.

## Author

Created by **Tom Sapletta** - [tom@sapletta.com](mailto:tom@sapletta.com)

