---
name: python-manager-discovery
description: Environment manager-specific discovery patterns and known issues. Use when working on or reviewing environment discovery code for conda, poetry, pipenv, pyenv, or venv.
argument-hint: 'manager name (e.g., poetry, conda, pyenv)'
user-invocable: false
category: dev
---

# Environment Manager Discovery Patterns

This skill documents manager-specific discovery patterns, environment variable precedence, and known issues.

## Manager Quick Reference

| Manager | Config Files                                   | Cache Location           | Key Env Vars                                        |
| ------- | ---------------------------------------------- | ------------------------ | --------------------------------------------------- |
| Poetry  | `poetry.toml`, `pyproject.toml`, `config.toml` | Platform-specific        | `POETRY_VIRTUALENVS_IN_PROJECT`, `POETRY_CACHE_DIR` |
| Pipenv  | `Pipfile`, `Pipfile.lock`                      | XDG or WORKON_HOME       | `WORKON_HOME`, `XDG_DATA_HOME`                      |
| Pyenv   | `.python-version`, `versions/`                 | `~/.pyenv/` or pyenv-win | `PYENV_ROOT`, `PYENV_VERSION`                       |
| Conda   | `environment.yml`, `conda-meta/`               | Registries + paths       | `CONDA_PREFIX`, `CONDA_DEFAULT_ENV`                 |
| venv    | `pyvenv.cfg`                                   | In-project               | None                                                |

---

## Poetry

### Discovery Locations

**Virtualenvs cache (default):**

- Windows: `%LOCALAPPDATA%\pypoetry\Cache\virtualenvs`
- macOS: `~/Library/Caches/pypoetry/virtualenvs`
- Linux: `~/.cache/pypoetry/virtualenvs`

**In-project (when enabled):**

- `.venv/` in project root

### Config Precedence (highest to lowest)

1. Local config: `poetry.toml` in project root
2. Environment variables: `POETRY_VIRTUALENVS_*`
3. Global config: `~/.config/pypoetry/config.toml`

### Known Issues

| Issue                     | Description                                     | Fix                              |
| ------------------------- | ----------------------------------------------- | -------------------------------- |
| `{cache-dir}` placeholder | Not resolved in paths from config               | Resolve placeholder before use   |
| Wrong default path        | Windows/macOS differ from Linux                 | Use platform-specific defaults   |
| In-project detection      | `POETRY_VIRTUALENVS_IN_PROJECT` must be checked | Check env var first, then config |

### Code Pattern

```typescript
async function getPoetryVirtualenvsPath(): Promise<string> {
    // 1. Check environment variable first
    const envVar = process.env.POETRY_VIRTUALENVS_PATH;
    if (envVar) return envVar;

    // 2. Check local poetry.toml
    const localConfig = await readPoetryToml(projectRoot);
    if (localConfig?.virtualenvs?.path) {
        return resolvePoetryPath(localConfig.virtualenvs.path);
    }

    // 3. Use platform-specific default
    return getDefaultPoetryCache();
}

function resolvePoetryPath(configPath: string): string {
    // Handle {cache-dir} placeholder
    if (configPath.includes('{cache-dir}')) {
        const cacheDir = getDefaultPoetryCache();
        return configPath.replace('{cache-dir}', cacheDir);
    }
    return configPath;
}
```

---

## Pipenv

### Discovery Locations

**Default:**

- Linux: `~/.local/share/virtualenvs/` (XDG_DATA_HOME)
- macOS: `~/.local/share/virtualenvs/`
- Windows: `~\.virtualenvs\`

**When WORKON_HOME is set:**

- Use `$WORKON_HOME/` directly

### Environment Variables

| Var                      | Purpose                      |
| ------------------------ | ---------------------------- |
| `WORKON_HOME`            | Override virtualenv location |
| `XDG_DATA_HOME`          | Base for Linux default       |
| `PIPENV_VENV_IN_PROJECT` | Create `.venv/` in project   |

### Known Issues

| Issue                         | Description         | Fix                          |
| ----------------------------- | ------------------- | ---------------------------- |
| Missing WORKON_HOME support   | Env var not checked | Read env var before defaults |
| Missing XDG_DATA_HOME support | Not used on Linux   | Check XDG spec               |

### Code Pattern

```typescript
function getPipenvVirtualenvsPath(): string {
    // Check WORKON_HOME first
    if (process.env.WORKON_HOME) {
        return process.env.WORKON_HOME;
    }

    // Check XDG_DATA_HOME on Linux
    if (process.platform === 'linux') {
        const xdgData = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
        return path.join(xdgData, 'virtualenvs');
    }

    // Windows/macOS defaults
    return path.join(os.homedir(), '.virtualenvs');
}
```

---

## PyEnv

### Discovery Locations

**Unix:**

- `~/.pyenv/versions/` (default)
- `$PYENV_ROOT/versions/` (if PYENV_ROOT set)

**Windows (pyenv-win):**

- `%USERPROFILE%\.pyenv\pyenv-win\versions\`
- Different directory structure than Unix!

### Key Differences: Unix vs Windows

| Aspect  | Unix              | Windows (pyenv-win)                     |
| ------- | ----------------- | --------------------------------------- |
| Command | `pyenv`           | `pyenv.bat`                             |
| Root    | `~/.pyenv/`       | `%USERPROFILE%\.pyenv\pyenv-win\`       |
| Shims   | `~/.pyenv/shims/` | `%USERPROFILE%\.pyenv\pyenv-win\shims\` |

### Known Issues

| Issue                              | Description                                | Fix                                |
| ---------------------------------- | ------------------------------------------ | ---------------------------------- |
| path.normalize() vs path.resolve() | Windows drive letter missing               | Use `path.resolve()` on both sides |
| Wrong command on Windows           | Looking for `pyenv` instead of `pyenv.bat` | Check for `.bat` extension         |

### Code Pattern

```typescript
function getPyenvRoot(): string {
    if (process.env.PYENV_ROOT) {
        return process.env.PYENV_ROOT;
    }

    if (process.platform === 'win32') {
        // pyenv-win uses different structure
        return path.join(os.homedir(), '.pyenv', 'pyenv-win');
    }

    return path.join(os.homedir(), '.pyenv');
}

function getPyenvVersionsPath(): string {
    const root = getPyenvRoot();
    return path.join(root, 'versions');
}

// Use path.resolve() for comparisons!
function comparePyenvPaths(pathA: string, pathB: string): boolean {
    return path.resolve(pathA) === path.resolve(pathB);
}
```

---

## Conda

### Discovery Locations

**Environment locations:**

- Base install `envs/` directory
- `~/.conda/envs/`
- Paths in `~/.condarc` `envs_dirs`

**Windows Registry:**

- `HKCU\Software\Python\ContinuumAnalytics\`
- `HKLM\SOFTWARE\Python\ContinuumAnalytics\`

### Shell Activation

| Shell      | Activation Command                     |
| ---------- | -------------------------------------- |
| bash, zsh  | `source activate envname`              |
| fish       | `conda activate envname` (NOT source!) |
| PowerShell | `conda activate envname`               |
| cmd        | `activate.bat envname`                 |

### Known Issues

| Issue                 | Description             | Fix                        |
| --------------------- | ----------------------- | -------------------------- |
| Fish shell activation | Uses bash-style command | Use fish-compatible syntax |
| Registry paths        | May be stale/invalid    | Verify paths exist         |
| Base vs named envs    | Different activation    | Check if activating base   |

### Code Pattern

```typescript
function getCondaActivationCommand(shell: ShellType, envName: string): string {
    switch (shell) {
        case 'fish':
            // Fish uses different syntax!
            return `conda activate ${envName}`;
        case 'cmd':
            return `activate.bat ${envName}`;
        case 'powershell':
            return `conda activate ${envName}`;
        default:
            // bash, zsh
            return `source activate ${envName}`;
    }
}
```

---

## venv

### Discovery

**Identification:**

- Look for `pyvenv.cfg` file in directory
- Contains `home` and optionally `version` keys

### Version Extraction Priority

1. `version` field in `pyvenv.cfg`
2. Parse from `home` path (e.g., `Python311`)
3. Spawn Python executable (last resort)

### Code Pattern

```typescript
async function getVenvVersion(venvPath: string): Promise<string | undefined> {
    const cfgPath = path.join(venvPath, 'pyvenv.cfg');

    try {
        const content = await fs.readFile(cfgPath, 'utf-8');
        const lines = content.split('\n');

        for (const line of lines) {
            const [key, value] = line.split('=').map((s) => s.trim());
            if (key === 'version') {
                return value;
            }
        }

        // Fall back to parsing home path
        const homeLine = lines.find((l) => l.startsWith('home'));
        if (homeLine) {
            const home = homeLine.split('=')[1].trim();
            const match = home.match(/(\d+)\.(\d+)/);
            if (match) {
                return `${match[1]}.${match[2]}`;
            }
        }
    } catch {
        // Config file not found or unreadable
    }

    return undefined;
}
```

---

## PET Server (Native Finder)

### JSON-RPC Communication

The PET server is a Rust-based locator that communicates via JSON-RPC over stdio.

### Known Issues

| Issue               | Description                      | Fix                           |
| ------------------- | -------------------------------- | ----------------------------- |
| No timeout          | JSON-RPC can hang forever        | Add Promise.race with timeout |
| Silent spawn errors | Extension continues without envs | Surface spawn errors to user  |
| Resource leaks      | Worker pool not cleaned up       | Dispose on deactivation       |
| Type guard missing  | Response types not validated     | Add runtime type checks       |
| Cache key collision | Paths normalize to same key      | Use consistent normalization  |

### Code Pattern

```typescript
async function fetchFromPET<T>(method: string, params: unknown): Promise<T> {
    const timeout = 30000; // 30 seconds

    const result = await Promise.race([
        this.client.request(method, params),
        new Promise<never>((_, reject) => setTimeout(() => reject(new Error('PET server timeout')), timeout)),
    ]);

    // Validate response type
    if (!isValidResponse<T>(result)) {
        throw new Error(`Invalid response from PET: ${JSON.stringify(result)}`);
    }

    return result;
}
```
