# cenv (Claude Environment Manager) Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Build a CLI tool that manages isolated Claude Code configurations using symlinks, similar to pyenv but for Claude environments.

**Architecture:** Python CLI using Click for command parsing. Symlink-based environment switching with complete isolation. Each environment lives in `~/.claude-envs/<name>/` with `~/.claude` as symlink to active environment.

**Tech Stack:** Python 3.10+, Click (CLI), pathlib (paths), psutil (process detection), requests (GitHub cloning)

---

## Task 1: Project Setup and Core Structure

**Files:**
- Create: `pyproject.toml`
- Create: `src/cenv/__init__.py`
- Create: `src/cenv/cli.py`
- Create: `src/cenv/core.py`
- Create: `tests/test_core.py`
- Create: `Makefile`
- Create: `.python-version`

**Step 1: Write the failing test for environment path resolution**

Create `tests/test_core.py`:

```python
from pathlib import Path
from cenv.core import get_envs_dir, get_env_path, get_claude_dir

def test_get_envs_dir_returns_correct_path():
    """Test that envs directory path is ~/.claude-envs"""
    result = get_envs_dir()
    assert result == Path.home() / ".claude-envs"

def test_get_env_path_returns_correct_path():
    """Test that environment path is ~/.claude-envs/<name>"""
    result = get_env_path("work")
    assert result == Path.home() / ".claude-envs" / "work"

def test_get_claude_dir_returns_correct_path():
    """Test that claude dir is ~/.claude"""
    result = get_claude_dir()
    assert result == Path.home() / ".claude"
```

**Step 2: Run test to verify it fails**

Run: `pytest tests/test_core.py -v`
Expected: FAIL with "ModuleNotFoundError: No module named 'cenv'"

**Step 3: Create project structure**

Create `pyproject.toml`:

```toml
[project]
name = "cenv"
version = "0.1.0"
description = "Claude environment manager - switch between Claude Code configurations"
requires-python = ">=3.10"
dependencies = [
    "click>=8.1.0",
    "psutil>=5.9.0",
    "requests>=2.31.0",
]

[project.scripts]
cenv = "cenv.cli:cli"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/cenv"]

[project.optional-dependencies]
dev = [
    "pytest>=7.4.0",
    "pytest-cov>=4.1.0",
]
```

Create `.python-version`:

```
3.10
```

Create `Makefile`:

```makefile
.PHONY: install test clean

install:
	uv pip install -e ".[dev]"

test:
	pytest tests/ -v

clean:
	rm -rf build/ dist/ *.egg-info
	find . -type d -name __pycache__ -exec rm -rf {} +
```

Create `src/cenv/__init__.py`:

```python
"""cenv - Claude environment manager"""
__version__ = "0.1.0"
```

**Step 4: Write minimal implementation**

Create `src/cenv/core.py`:

```python
"""Core functionality for cenv"""
from pathlib import Path

def get_envs_dir() -> Path:
    """Get the base directory for all environments"""
    return Path.home() / ".claude-envs"

def get_env_path(name: str) -> Path:
    """Get the path for a specific environment"""
    return get_envs_dir() / name

def get_claude_dir() -> Path:
    """Get the ~/.claude directory path"""
    return Path.home() / ".claude"
```

**Step 5: Install and run tests**

Run:
```bash
make install
make test
```
Expected: PASS (3 tests)

**Step 6: Commit**

```bash
git add pyproject.toml src/ tests/ Makefile .python-version
git commit -m "feat: add project structure and core path utilities"
```

---

## Task 2: Environment Detection and Listing

**Files:**
- Modify: `src/cenv/core.py`
- Create: `tests/test_environment_detection.py`

**Step 1: Write the failing test for environment detection**

Create `tests/test_environment_detection.py`:

```python
from pathlib import Path
from cenv.core import list_environments, get_current_environment, environment_exists
import tempfile
import pytest

@pytest.fixture
def mock_envs_dir(monkeypatch, tmp_path):
    """Create a temporary envs directory for testing"""
    envs_dir = tmp_path / ".claude-envs"
    envs_dir.mkdir()
    monkeypatch.setattr("cenv.core.get_envs_dir", lambda: envs_dir)
    return envs_dir

def test_list_environments_returns_empty_when_no_envs(mock_envs_dir):
    """Test listing environments when none exist"""
    result = list_environments()
    assert result == []

def test_list_environments_returns_all_directories(mock_envs_dir):
    """Test listing returns all environment directories"""
    (mock_envs_dir / "work").mkdir()
    (mock_envs_dir / "personal").mkdir()
    (mock_envs_dir / "default").mkdir()

    result = sorted(list_environments())
    assert result == ["default", "personal", "work"]

def test_list_environments_ignores_files(mock_envs_dir):
    """Test that files are ignored, only directories counted"""
    (mock_envs_dir / "work").mkdir()
    (mock_envs_dir / "readme.txt").touch()

    result = list_environments()
    assert result == ["work"]

def test_get_current_environment_when_symlink_exists(monkeypatch, tmp_path):
    """Test detecting current environment from symlink"""
    envs_dir = tmp_path / ".claude-envs"
    envs_dir.mkdir()
    work_dir = envs_dir / "work"
    work_dir.mkdir()

    claude_link = tmp_path / ".claude"
    claude_link.symlink_to(work_dir)

    monkeypatch.setattr("cenv.core.get_envs_dir", lambda: envs_dir)
    monkeypatch.setattr("cenv.core.get_claude_dir", lambda: claude_link)

    result = get_current_environment()
    assert result == "work"

def test_get_current_environment_returns_none_when_no_symlink(monkeypatch, tmp_path):
    """Test returns None when ~/.claude is not a symlink"""
    claude_dir = tmp_path / ".claude"
    claude_dir.mkdir()

    monkeypatch.setattr("cenv.core.get_claude_dir", lambda: claude_dir)

    result = get_current_environment()
    assert result is None

def test_environment_exists_returns_true_when_exists(mock_envs_dir):
    """Test environment existence check"""
    (mock_envs_dir / "work").mkdir()

    assert environment_exists("work") is True

def test_environment_exists_returns_false_when_not_exists(mock_envs_dir):
    """Test environment existence check for missing env"""
    assert environment_exists("nonexistent") is False
```

**Step 2: Run test to verify it fails**

Run: `pytest tests/test_environment_detection.py -v`
Expected: FAIL with "ImportError: cannot import name 'list_environments'"

**Step 3: Write minimal implementation**

Modify `src/cenv/core.py`:

```python
"""Core functionality for cenv"""
from pathlib import Path
from typing import List, Optional

def get_envs_dir() -> Path:
    """Get the base directory for all environments"""
    return Path.home() / ".claude-envs"

def get_env_path(name: str) -> Path:
    """Get the path for a specific environment"""
    return get_envs_dir() / name

def get_claude_dir() -> Path:
    """Get the ~/.claude directory path"""
    return Path.home() / ".claude"

def list_environments() -> List[str]:
    """List all available environments"""
    envs_dir = get_envs_dir()

    if not envs_dir.exists():
        return []

    return [
        item.name
        for item in envs_dir.iterdir()
        if item.is_dir()
    ]

def get_current_environment() -> Optional[str]:
    """Get the currently active environment name"""
    claude_dir = get_claude_dir()

    if not claude_dir.is_symlink():
        return None

    target = claude_dir.resolve()
    envs_dir = get_envs_dir()

    if target.parent == envs_dir:
        return target.name

    return None

def environment_exists(name: str) -> bool:
    """Check if an environment exists"""
    return get_env_path(name).exists()
```

**Step 4: Run tests to verify they pass**

Run: `pytest tests/test_environment_detection.py -v`
Expected: PASS (7 tests)

**Step 5: Commit**

```bash
git add src/cenv/core.py tests/test_environment_detection.py
git commit -m "feat: add environment detection and listing"
```

---

## Task 3: Process Detection for Running Claude

**Files:**
- Create: `src/cenv/process.py`
- Create: `tests/test_process.py`

**Step 1: Write the failing test for process detection**

Create `tests/test_process.py`:

```python
import pytest
from unittest.mock import Mock, patch
from cenv.process import is_claude_running, get_claude_processes

def test_is_claude_running_returns_false_when_no_processes():
    """Test returns False when no Claude processes found"""
    with patch("cenv.process.get_claude_processes", return_value=[]):
        result = is_claude_running()
        assert result is False

def test_is_claude_running_returns_true_when_processes_exist():
    """Test returns True when Claude processes found"""
    mock_process = Mock()
    with patch("cenv.process.get_claude_processes", return_value=[mock_process]):
        result = is_claude_running()
        assert result is True

def test_get_claude_processes_finds_claude_node_processes():
    """Test that get_claude_processes finds node processes running claude"""
    mock_proc1 = Mock()
    mock_proc1.info = {"pid": 123, "name": "node", "cmdline": ["/usr/bin/node", "/path/to/claude"]}

    mock_proc2 = Mock()
    mock_proc2.info = {"pid": 456, "name": "python", "cmdline": ["python", "script.py"]}

    mock_proc3 = Mock()
    mock_proc3.info = {"pid": 789, "name": "node", "cmdline": ["/usr/bin/node", "/path/to/other"]}

    with patch("psutil.process_iter", return_value=[mock_proc1, mock_proc2, mock_proc3]):
        result = get_claude_processes()
        assert len(result) == 1
        assert result[0].info["pid"] == 123

def test_get_claude_processes_handles_access_denied():
    """Test that process iteration handles access denied errors"""
    import psutil

    mock_proc = Mock()
    mock_proc.info.side_effect = psutil.AccessDenied()

    with patch("psutil.process_iter", return_value=[mock_proc]):
        result = get_claude_processes()
        assert result == []
```

**Step 2: Run test to verify it fails**

Run: `pytest tests/test_process.py -v`
Expected: FAIL with "ModuleNotFoundError: No module named 'cenv.process'"

**Step 3: Write minimal implementation**

Create `src/cenv/process.py`:

```python
"""Process detection for Claude Code"""
import psutil
from typing import List

def get_claude_processes() -> List:
    """Get all running Claude Code processes"""
    claude_processes = []

    for proc in psutil.process_iter(["pid", "name", "cmdline"]):
        try:
            cmdline = proc.info.get("cmdline", [])
            if cmdline and any("claude" in str(arg).lower() for arg in cmdline):
                # Check if it's actually the claude binary/node process
                if proc.info.get("name") == "node" and any("bin/claude" in str(arg) for arg in cmdline):
                    claude_processes.append(proc)
        except (psutil.AccessDenied, psutil.NoSuchProcess):
            continue

    return claude_processes

def is_claude_running() -> bool:
    """Check if Claude Code is currently running"""
    return len(get_claude_processes()) > 0
```

**Step 4: Run tests to verify they pass**

Run: `pytest tests/test_process.py -v`
Expected: PASS (4 tests)

**Step 5: Commit**

```bash
git add src/cenv/process.py tests/test_process.py
git commit -m "feat: add Claude process detection"
```

---

## Task 4: Environment Initialization (cenv init)

**Files:**
- Modify: `src/cenv/core.py`
- Create: `tests/test_init.py`

**Step 1: Write the failing test for initialization**

Create `tests/test_init.py`:

```python
import pytest
from pathlib import Path
from cenv.core import init_environments
from unittest.mock import patch

@pytest.fixture
def mock_dirs(monkeypatch, tmp_path):
    """Mock home directory structure"""
    claude_dir = tmp_path / ".claude"
    claude_dir.mkdir()
    (claude_dir / "CLAUDE.md").write_text("# Test")
    (claude_dir / "settings.json").write_text("{}")

    envs_dir = tmp_path / ".claude-envs"

    monkeypatch.setattr("cenv.core.get_claude_dir", lambda: claude_dir)
    monkeypatch.setattr("cenv.core.get_envs_dir", lambda: envs_dir)

    return {"claude": claude_dir, "envs": envs_dir}

def test_init_creates_envs_directory(mock_dirs):
    """Test that init creates ~/.claude-envs directory"""
    init_environments()
    assert mock_dirs["envs"].exists()
    assert mock_dirs["envs"].is_dir()

def test_init_moves_claude_to_default(mock_dirs):
    """Test that init moves ~/.claude to ~/.claude-envs/default"""
    init_environments()

    default_env = mock_dirs["envs"] / "default"
    assert default_env.exists()
    assert (default_env / "CLAUDE.md").exists()
    assert (default_env / "settings.json").exists()

def test_init_creates_symlink_to_default(mock_dirs):
    """Test that init creates symlink ~/.claude -> default"""
    init_environments()

    assert mock_dirs["claude"].is_symlink()
    assert mock_dirs["claude"].resolve() == mock_dirs["envs"] / "default"

def test_init_raises_if_already_initialized(mock_dirs):
    """Test that init raises error if already initialized"""
    init_environments()

    with pytest.raises(RuntimeError, match="already initialized"):
        init_environments()

def test_init_raises_if_claude_is_already_symlink(mock_dirs):
    """Test that init raises error if ~/.claude is already a symlink"""
    mock_dirs["claude"].rmdir()
    mock_dirs["claude"].symlink_to(mock_dirs["envs"] / "existing")

    with pytest.raises(RuntimeError, match="already a symlink"):
        init_environments()
```

**Step 2: Run test to verify it fails**

Run: `pytest tests/test_init.py -v`
Expected: FAIL with "ImportError: cannot import name 'init_environments'"

**Step 3: Write minimal implementation**

Modify `src/cenv/core.py`, add to end:

```python
import shutil

def init_environments() -> None:
    """Initialize cenv by migrating ~/.claude to ~/.claude-envs/default/"""
    claude_dir = get_claude_dir()
    envs_dir = get_envs_dir()
    default_env = get_env_path("default")

    # Check if already initialized
    if envs_dir.exists():
        raise RuntimeError("cenv already initialized. ~/.claude-envs exists.")

    # Check if ~/.claude is already a symlink
    if claude_dir.is_symlink():
        raise RuntimeError("~/.claude is already a symlink. Cannot initialize.")

    # Create envs directory
    envs_dir.mkdir(parents=True, exist_ok=True)

    # Move ~/.claude to default environment
    if claude_dir.exists():
        shutil.move(str(claude_dir), str(default_env))
    else:
        # Create empty default environment
        default_env.mkdir(parents=True, exist_ok=True)

    # Create symlink
    claude_dir.symlink_to(default_env)
```

**Step 4: Run tests to verify they pass**

Run: `pytest tests/test_init.py -v`
Expected: PASS (5 tests)

**Step 5: Commit**

```bash
git add src/cenv/core.py tests/test_init.py
git commit -m "feat: add environment initialization (cenv init)"
```

---

## Task 5: Environment Creation from Default

**Files:**
- Modify: `src/cenv/core.py`
- Create: `tests/test_create.py`

**Step 1: Write the failing test for environment creation**

Create `tests/test_create.py`:

```python
import pytest
from pathlib import Path
from cenv.core import create_environment
from unittest.mock import patch

@pytest.fixture
def initialized_envs(monkeypatch, tmp_path):
    """Setup initialized environment structure"""
    envs_dir = tmp_path / ".claude-envs"
    envs_dir.mkdir()

    default_env = envs_dir / "default"
    default_env.mkdir()
    (default_env / "CLAUDE.md").write_text("# Default")
    (default_env / "settings.json").write_text('{"test": true}')
    agents_dir = default_env / "agents"
    agents_dir.mkdir()
    (agents_dir / "test.md").write_text("# Agent")

    claude_dir = tmp_path / ".claude"
    claude_dir.symlink_to(default_env)

    monkeypatch.setattr("cenv.core.get_envs_dir", lambda: envs_dir)
    monkeypatch.setattr("cenv.core.get_claude_dir", lambda: claude_dir)

    return {"envs": envs_dir, "claude": claude_dir, "default": default_env}

def test_create_environment_copies_from_default(initialized_envs):
    """Test creating environment copies from default"""
    create_environment("work")

    work_env = initialized_envs["envs"] / "work"
    assert work_env.exists()
    assert (work_env / "CLAUDE.md").read_text() == "# Default"
    assert (work_env / "settings.json").read_text() == '{"test": true}'
    assert (work_env / "agents" / "test.md").exists()

def test_create_environment_raises_if_exists(initialized_envs):
    """Test that creating existing environment raises error"""
    create_environment("work")

    with pytest.raises(RuntimeError, match="already exists"):
        create_environment("work")

def test_create_environment_raises_if_not_initialized(monkeypatch, tmp_path):
    """Test that create raises if not initialized"""
    monkeypatch.setattr("cenv.core.get_envs_dir", lambda: tmp_path / ".claude-envs")

    with pytest.raises(RuntimeError, match="not initialized"):
        create_environment("work")

def test_create_environment_raises_if_default_missing(initialized_envs):
    """Test that create raises if default environment doesn't exist"""
    (initialized_envs["envs"] / "default").rmdir()

    with pytest.raises(RuntimeError, match="default environment not found"):
        create_environment("work")
```

**Step 2: Run test to verify it fails**

Run: `pytest tests/test_create.py -v`
Expected: FAIL with "ImportError: cannot import name 'create_environment'"

**Step 3: Write minimal implementation**

Modify `src/cenv/core.py`, add to end:

```python
def create_environment(name: str, source: str = "default") -> None:
    """Create a new environment by copying from source environment"""
    envs_dir = get_envs_dir()

    # Check if initialized
    if not envs_dir.exists():
        raise RuntimeError("cenv not initialized. Run 'cenv init' first.")

    # Check if environment already exists
    target_env = get_env_path(name)
    if target_env.exists():
        raise RuntimeError(f"Environment '{name}' already exists.")

    # Check if source environment exists
    source_env = get_env_path(source)
    if not source_env.exists():
        raise RuntimeError(f"{source} environment not found.")

    # Copy source to target
    shutil.copytree(source_env, target_env, symlinks=True)
```

**Step 4: Run tests to verify they pass**

Run: `pytest tests/test_create.py -v`
Expected: PASS (4 tests)

**Step 5: Commit**

```bash
git add src/cenv/core.py tests/test_create.py
git commit -m "feat: add environment creation from default"
```

---

## Task 6: Environment Switching with Confirmation

**Files:**
- Modify: `src/cenv/core.py`
- Create: `tests/test_switch.py`

**Step 1: Write the failing test for environment switching**

Create `tests/test_switch.py`:

```python
import pytest
from pathlib import Path
from cenv.core import switch_environment
from unittest.mock import patch

@pytest.fixture
def multi_env_setup(monkeypatch, tmp_path):
    """Setup with multiple environments"""
    envs_dir = tmp_path / ".claude-envs"
    envs_dir.mkdir()

    for env_name in ["default", "work", "personal"]:
        env_dir = envs_dir / env_name
        env_dir.mkdir()
        (env_dir / "CLAUDE.md").write_text(f"# {env_name}")

    claude_dir = tmp_path / ".claude"
    claude_dir.symlink_to(envs_dir / "default")

    monkeypatch.setattr("cenv.core.get_envs_dir", lambda: envs_dir)
    monkeypatch.setattr("cenv.core.get_claude_dir", lambda: claude_dir)

    return {"envs": envs_dir, "claude": claude_dir}

def test_switch_environment_updates_symlink(multi_env_setup):
    """Test that switching updates ~/.claude symlink"""
    with patch("cenv.process.is_claude_running", return_value=False):
        switch_environment("work", force=True)

    assert multi_env_setup["claude"].resolve() == multi_env_setup["envs"] / "work"

def test_switch_environment_raises_if_not_exists(multi_env_setup):
    """Test that switching to non-existent env raises error"""
    with patch("cenv.process.is_claude_running", return_value=False):
        with pytest.raises(RuntimeError, match="does not exist"):
            switch_environment("nonexistent", force=True)

def test_switch_environment_raises_if_claude_running_without_force(multi_env_setup):
    """Test that switching raises if Claude running and force=False"""
    with patch("cenv.process.is_claude_running", return_value=True):
        with pytest.raises(RuntimeError, match="Claude is running"):
            switch_environment("work", force=False)

def test_switch_environment_succeeds_if_claude_running_with_force(multi_env_setup):
    """Test that switching works if Claude running but force=True"""
    with patch("cenv.process.is_claude_running", return_value=True):
        switch_environment("work", force=True)

    assert multi_env_setup["claude"].resolve() == multi_env_setup["envs"] / "work"

def test_switch_environment_removes_existing_symlink(multi_env_setup):
    """Test that switching removes old symlink correctly"""
    with patch("cenv.process.is_claude_running", return_value=False):
        switch_environment("work", force=True)
        assert multi_env_setup["claude"].resolve() == multi_env_setup["envs"] / "work"

        switch_environment("personal", force=True)
        assert multi_env_setup["claude"].resolve() == multi_env_setup["envs"] / "personal"
```

**Step 2: Run test to verify it fails**

Run: `pytest tests/test_switch.py -v`
Expected: FAIL with "ImportError: cannot import name 'switch_environment'"

**Step 3: Write minimal implementation**

Modify `src/cenv/core.py`, add import at top:

```python
from cenv.process import is_claude_running
```

Add to end of file:

```python
def switch_environment(name: str, force: bool = False) -> None:
    """Switch to a different environment"""
    target_env = get_env_path(name)

    # Check if target environment exists
    if not target_env.exists():
        raise RuntimeError(f"Environment '{name}' does not exist.")

    # Check if Claude is running
    if is_claude_running() and not force:
        raise RuntimeError(
            "Claude is running. Please exit Claude first or use force=True."
        )

    claude_dir = get_claude_dir()

    # Remove existing symlink
    if claude_dir.is_symlink():
        claude_dir.unlink()
    elif claude_dir.exists():
        raise RuntimeError("~/.claude exists but is not a symlink. Cannot switch.")

    # Create new symlink
    claude_dir.symlink_to(target_env)
```

**Step 4: Run tests to verify they pass**

Run: `pytest tests/test_switch.py -v`
Expected: PASS (5 tests)

**Step 5: Commit**

```bash
git add src/cenv/core.py tests/test_switch.py
git commit -m "feat: add environment switching with safety checks"
```

---

## Task 7: Environment Deletion

**Files:**
- Modify: `src/cenv/core.py`
- Create: `tests/test_delete.py`

**Step 1: Write the failing test for environment deletion**

Create `tests/test_delete.py`:

```python
import pytest
from cenv.core import delete_environment
from unittest.mock import patch

@pytest.fixture
def multi_env_setup(monkeypatch, tmp_path):
    """Setup with multiple environments"""
    envs_dir = tmp_path / ".claude-envs"
    envs_dir.mkdir()

    for env_name in ["default", "work", "personal"]:
        env_dir = envs_dir / env_name
        env_dir.mkdir()
        (env_dir / "CLAUDE.md").write_text(f"# {env_name}")

    claude_dir = tmp_path / ".claude"
    claude_dir.symlink_to(envs_dir / "default")

    monkeypatch.setattr("cenv.core.get_envs_dir", lambda: envs_dir)
    monkeypatch.setattr("cenv.core.get_claude_dir", lambda: claude_dir)

    return {"envs": envs_dir, "claude": claude_dir}

def test_delete_environment_removes_directory(multi_env_setup):
    """Test that deleting removes the environment directory"""
    delete_environment("work")

    assert not (multi_env_setup["envs"] / "work").exists()
    assert (multi_env_setup["envs"] / "default").exists()
    assert (multi_env_setup["envs"] / "personal").exists()

def test_delete_environment_raises_if_not_exists(multi_env_setup):
    """Test that deleting non-existent env raises error"""
    with pytest.raises(RuntimeError, match="does not exist"):
        delete_environment("nonexistent")

def test_delete_environment_raises_if_currently_active(multi_env_setup):
    """Test that deleting active environment raises error"""
    with pytest.raises(RuntimeError, match="currently active"):
        delete_environment("default")

def test_delete_environment_raises_if_default(multi_env_setup):
    """Test that deleting default environment raises error"""
    with patch("cenv.core.get_current_environment", return_value="work"):
        with pytest.raises(RuntimeError, match="cannot delete default"):
            delete_environment("default")
```

**Step 2: Run test to verify it fails**

Run: `pytest tests/test_delete.py -v`
Expected: FAIL with "ImportError: cannot import name 'delete_environment'"

**Step 3: Write minimal implementation**

Modify `src/cenv/core.py`, add to end:

```python
def delete_environment(name: str) -> None:
    """Delete an environment"""
    target_env = get_env_path(name)

    # Check if environment exists
    if not target_env.exists():
        raise RuntimeError(f"Environment '{name}' does not exist.")

    # Check if it's the default environment
    if name == "default":
        raise RuntimeError("Cannot delete default environment.")

    # Check if it's currently active
    current = get_current_environment()
    if current == name:
        raise RuntimeError(
            f"Environment '{name}' is currently active. "
            "Switch to another environment first."
        )

    # Delete the environment
    shutil.rmtree(target_env)
```

**Step 4: Run tests to verify they pass**

Run: `pytest tests/test_delete.py -v`
Expected: PASS (4 tests)

**Step 5: Commit**

```bash
git add src/cenv/core.py tests/test_delete.py
git commit -m "feat: add environment deletion with safety checks"
```

---

## Task 8: GitHub Repository Cloning

**Files:**
- Create: `src/cenv/github.py`
- Create: `tests/test_github.py`

**Step 1: Write the failing test for GitHub cloning**

Create `tests/test_github.py`:

```python
import pytest
from pathlib import Path
from cenv.github import clone_from_github, is_valid_github_url
from unittest.mock import patch, MagicMock
import tempfile
import shutil

def test_is_valid_github_url_validates_https():
    """Test GitHub URL validation for HTTPS"""
    assert is_valid_github_url("https://github.com/user/repo") is True
    assert is_valid_github_url("https://github.com/user/repo.git") is True

def test_is_valid_github_url_validates_ssh():
    """Test GitHub URL validation for SSH"""
    assert is_valid_github_url("git@github.com:user/repo.git") is True

def test_is_valid_github_url_rejects_invalid():
    """Test GitHub URL validation rejects invalid URLs"""
    assert is_valid_github_url("not-a-url") is False
    assert is_valid_github_url("https://gitlab.com/user/repo") is False

@patch("subprocess.run")
def test_clone_from_github_calls_git_clone(mock_run):
    """Test that clone_from_github calls git clone"""
    mock_run.return_value = MagicMock(returncode=0)

    with tempfile.TemporaryDirectory() as tmpdir:
        target = Path(tmpdir) / "test-env"
        clone_from_github("https://github.com/user/repo", target)

        mock_run.assert_called_once()
        call_args = mock_run.call_args
        assert "git" in call_args[0][0]
        assert "clone" in call_args[0][0]
        assert "https://github.com/user/repo" in call_args[0][0]

@patch("subprocess.run")
def test_clone_from_github_raises_on_git_error(mock_run):
    """Test that clone raises error if git clone fails"""
    mock_run.return_value = MagicMock(returncode=1, stderr="error")

    with tempfile.TemporaryDirectory() as tmpdir:
        target = Path(tmpdir) / "test-env"
        with pytest.raises(RuntimeError, match="Failed to clone"):
            clone_from_github("https://github.com/user/repo", target)

@patch("subprocess.run")
def test_clone_from_github_removes_git_directory(mock_run):
    """Test that clone removes .git directory after cloning"""
    mock_run.return_value = MagicMock(returncode=0)

    with tempfile.TemporaryDirectory() as tmpdir:
        target = Path(tmpdir) / "test-env"
        target.mkdir()
        git_dir = target / ".git"
        git_dir.mkdir()

        with patch("cenv.github.Path.exists", return_value=True):
            with patch("shutil.rmtree") as mock_rmtree:
                clone_from_github("https://github.com/user/repo", target)
                # Check that rmtree was called (for .git directory)
                assert mock_rmtree.called
```

**Step 2: Run test to verify it fails**

Run: `pytest tests/test_github.py -v`
Expected: FAIL with "ModuleNotFoundError: No module named 'cenv.github'"

**Step 3: Write minimal implementation**

Create `src/cenv/github.py`:

```python
"""GitHub repository cloning functionality"""
import subprocess
import shutil
from pathlib import Path
import re

def is_valid_github_url(url: str) -> bool:
    """Validate if URL is a valid GitHub repository URL"""
    patterns = [
        r"^https://github\.com/[\w-]+/[\w.-]+(\.git)?$",
        r"^git@github\.com:[\w-]+/[\w.-]+\.git$",
    ]

    return any(re.match(pattern, url) for pattern in patterns)

def clone_from_github(url: str, target: Path) -> None:
    """Clone a GitHub repository to target directory"""
    if not is_valid_github_url(url):
        raise ValueError(f"Invalid GitHub URL: {url}")

    # Create temporary directory for cloning
    temp_dir = target.parent / f".tmp_{target.name}"

    try:
        # Clone the repository
        result = subprocess.run(
            ["git", "clone", url, str(temp_dir)],
            capture_output=True,
            text=True,
            check=False,
        )

        if result.returncode != 0:
            raise RuntimeError(f"Failed to clone repository: {result.stderr}")

        # Remove .git directory
        git_dir = temp_dir / ".git"
        if git_dir.exists():
            shutil.rmtree(git_dir)

        # Move to final location
        if target.exists():
            shutil.rmtree(target)

        shutil.move(str(temp_dir), str(target))

    except Exception as e:
        # Clean up temp directory if it exists
        if temp_dir.exists():
            shutil.rmtree(temp_dir)
        raise
```

**Step 4: Run tests to verify they pass**

Run: `pytest tests/test_github.py -v`
Expected: PASS (5 tests)

**Step 5: Commit**

```bash
git add src/cenv/github.py tests/test_github.py
git commit -m "feat: add GitHub repository cloning"
```

---

## Task 9: Integrate GitHub Cloning into Environment Creation

**Files:**
- Modify: `src/cenv/core.py`
- Modify: `tests/test_create.py`

**Step 1: Write the failing test for GitHub-based creation**

Modify `tests/test_create.py`, add to end:

```python
from unittest.mock import patch

def test_create_environment_from_github_url(initialized_envs):
    """Test creating environment from GitHub repository"""
    with patch("cenv.github.clone_from_github") as mock_clone:
        with patch("cenv.github.is_valid_github_url", return_value=True):
            create_environment("work", source="https://github.com/user/repo")

            work_env = initialized_envs["envs"] / "work"
            mock_clone.assert_called_once_with("https://github.com/user/repo", work_env)

def test_create_environment_validates_github_url(initialized_envs):
    """Test that invalid GitHub URL raises error"""
    with patch("cenv.github.is_valid_github_url", return_value=False):
        with pytest.raises(ValueError, match="Invalid GitHub URL"):
            create_environment("work", source="not-a-url")
```

**Step 2: Run test to verify it fails**

Run: `pytest tests/test_create.py::test_create_environment_from_github_url -v`
Expected: FAIL

**Step 3: Write minimal implementation**

Modify `src/cenv/core.py`, add import at top:

```python
from cenv.github import clone_from_github, is_valid_github_url
```

Modify `create_environment` function:

```python
def create_environment(name: str, source: str = "default") -> None:
    """Create a new environment by copying from source environment or GitHub URL"""
    envs_dir = get_envs_dir()

    # Check if initialized
    if not envs_dir.exists():
        raise RuntimeError("cenv not initialized. Run 'cenv init' first.")

    # Check if environment already exists
    target_env = get_env_path(name)
    if target_env.exists():
        raise RuntimeError(f"Environment '{name}' already exists.")

    # Check if source is a GitHub URL
    if source.startswith("https://") or source.startswith("git@"):
        if not is_valid_github_url(source):
            raise ValueError(f"Invalid GitHub URL: {source}")

        clone_from_github(source, target_env)
    else:
        # Source is an environment name
        source_env = get_env_path(source)
        if not source_env.exists():
            raise RuntimeError(f"{source} environment not found.")

        # Copy source to target
        shutil.copytree(source_env, target_env, symlinks=True)
```

**Step 4: Run tests to verify they pass**

Run: `pytest tests/test_create.py -v`
Expected: PASS (6 tests)

**Step 5: Commit**

```bash
git add src/cenv/core.py tests/test_create.py
git commit -m "feat: support creating environments from GitHub repos"
```

---

## Task 10: CLI Commands - Init and Create

**Files:**
- Modify: `src/cenv/cli.py`
- Create: `tests/test_cli_init.py`
- Create: `tests/test_cli_create.py`

**Step 1: Write the failing test for CLI init command**

Create `tests/test_cli_init.py`:

```python
import pytest
from click.testing import CliRunner
from cenv.cli import cli
from unittest.mock import patch

def test_init_command_calls_init_environments():
    """Test that 'cenv init' calls init_environments"""
    runner = CliRunner()

    with patch("cenv.cli.init_environments") as mock_init:
        result = runner.invoke(cli, ["init"])

        assert result.exit_code == 0
        mock_init.assert_called_once()
        assert "Initialized" in result.output

def test_init_command_shows_error_if_already_initialized():
    """Test that init shows error if already initialized"""
    runner = CliRunner()

    with patch("cenv.cli.init_environments", side_effect=RuntimeError("already initialized")):
        result = runner.invoke(cli, ["init"])

        assert result.exit_code == 1
        assert "Error" in result.output
```

Create `tests/test_cli_create.py`:

```python
import pytest
from click.testing import CliRunner
from cenv.cli import cli
from unittest.mock import patch

def test_create_command_creates_from_default():
    """Test that 'cenv create work' creates from default"""
    runner = CliRunner()

    with patch("cenv.cli.create_environment") as mock_create:
        result = runner.invoke(cli, ["create", "work"])

        assert result.exit_code == 0
        mock_create.assert_called_once_with("work", source="default")
        assert "Created" in result.output

def test_create_command_creates_from_github_url():
    """Test that 'cenv create work --from-repo URL' clones from GitHub"""
    runner = CliRunner()

    with patch("cenv.cli.create_environment") as mock_create:
        result = runner.invoke(cli, ["create", "work", "--from-repo", "https://github.com/user/repo"])

        assert result.exit_code == 0
        mock_create.assert_called_once_with("work", source="https://github.com/user/repo")

def test_create_command_shows_error_if_exists():
    """Test that create shows error if environment exists"""
    runner = CliRunner()

    with patch("cenv.cli.create_environment", side_effect=RuntimeError("already exists")):
        result = runner.invoke(cli, ["create", "work"])

        assert result.exit_code == 1
        assert "Error" in result.output
```

**Step 2: Run test to verify it fails**

Run: `pytest tests/test_cli_init.py tests/test_cli_create.py -v`
Expected: FAIL with import or attribute errors

**Step 3: Write minimal implementation**

Modify `src/cenv/cli.py`:

```python
"""CLI interface for cenv"""
import click
from cenv.core import (
    init_environments,
    create_environment,
    switch_environment,
    delete_environment,
    list_environments,
    get_current_environment,
)
from cenv.process import is_claude_running

@click.group()
@click.version_option()
def cli():
    """cenv - Claude environment manager

    Manage isolated Claude Code configurations like pyenv manages Python versions.
    """
    pass

@cli.command()
def init():
    """Initialize cenv by migrating ~/.claude to ~/.claude-envs/default/"""
    try:
        init_environments()
        click.echo("✓ Initialized cenv successfully!")
        click.echo("  ~/.claude → ~/.claude-envs/default/")
        click.echo("\nUse 'cenv create <name>' to create new environments.")
    except RuntimeError as e:
        click.echo(f"Error: {e}", err=True)
        raise SystemExit(1)

@cli.command()
@click.argument("name")
@click.option(
    "--from-repo",
    help="Clone from GitHub repository URL",
    metavar="URL",
)
def create(name: str, from_repo: str):
    """Create a new environment"""
    try:
        source = from_repo if from_repo else "default"
        create_environment(name, source=source)

        if from_repo:
            click.echo(f"✓ Created environment '{name}' from {from_repo}")
        else:
            click.echo(f"✓ Created environment '{name}' from default")
    except (RuntimeError, ValueError) as e:
        click.echo(f"Error: {e}", err=True)
        raise SystemExit(1)
```

**Step 4: Run tests to verify they pass**

Run: `pytest tests/test_cli_init.py tests/test_cli_create.py -v`
Expected: PASS (5 tests)

**Step 5: Commit**

```bash
git add src/cenv/cli.py tests/test_cli_init.py tests/test_cli_create.py
git commit -m "feat: add init and create CLI commands"
```

---

## Task 11: CLI Commands - Use (Switch) with Confirmation

**Files:**
- Modify: `src/cenv/cli.py`
- Create: `tests/test_cli_use.py`

**Step 1: Write the failing test for CLI use command**

Create `tests/test_cli_use.py`:

```python
import pytest
from click.testing import CliRunner
from cenv.cli import cli
from unittest.mock import patch

def test_use_command_switches_environment():
    """Test that 'cenv use work' switches to work environment"""
    runner = CliRunner()

    with patch("cenv.cli.is_claude_running", return_value=False):
        with patch("cenv.cli.switch_environment") as mock_switch:
            result = runner.invoke(cli, ["use", "work"])

            assert result.exit_code == 0
            mock_switch.assert_called_once_with("work", force=False)
            assert "Switched" in result.output

def test_use_command_prompts_if_claude_running():
    """Test that use prompts for confirmation if Claude is running"""
    runner = CliRunner()

    with patch("cenv.cli.is_claude_running", return_value=True):
        with patch("cenv.cli.switch_environment") as mock_switch:
            # User confirms with 'y'
            result = runner.invoke(cli, ["use", "work"], input="y\n")

            assert result.exit_code == 0
            mock_switch.assert_called_once_with("work", force=True)
            assert "Claude is running" in result.output

def test_use_command_cancels_if_user_declines():
    """Test that use cancels if user declines confirmation"""
    runner = CliRunner()

    with patch("cenv.cli.is_claude_running", return_value=True):
        with patch("cenv.cli.switch_environment") as mock_switch:
            # User declines with 'n'
            result = runner.invoke(cli, ["use", "work"], input="n\n")

            assert result.exit_code == 0
            mock_switch.assert_not_called()
            assert "Cancelled" in result.output

def test_use_command_with_force_flag_skips_prompt():
    """Test that --force flag skips confirmation prompt"""
    runner = CliRunner()

    with patch("cenv.cli.is_claude_running", return_value=True):
        with patch("cenv.cli.switch_environment") as mock_switch:
            result = runner.invoke(cli, ["use", "work", "--force"])

            assert result.exit_code == 0
            mock_switch.assert_called_once_with("work", force=True)
            # Should not prompt
            assert "Claude is running" not in result.output

def test_use_command_shows_error_if_env_not_exists():
    """Test that use shows error if environment doesn't exist"""
    runner = CliRunner()

    with patch("cenv.cli.is_claude_running", return_value=False):
        with patch("cenv.cli.switch_environment", side_effect=RuntimeError("does not exist")):
            result = runner.invoke(cli, ["use", "nonexistent"])

            assert result.exit_code == 1
            assert "Error" in result.output
```

**Step 2: Run test to verify it fails**

Run: `pytest tests/test_cli_use.py -v`
Expected: FAIL with attribute error for 'use' command

**Step 3: Write minimal implementation**

Modify `src/cenv/cli.py`, add to end:

```python
@cli.command()
@click.argument("name")
@click.option("--force", "-f", is_flag=True, help="Skip confirmation prompt")
def use(name: str, force: bool):
    """Switch to a different environment"""
    try:
        # Check if Claude is running
        if is_claude_running() and not force:
            click.echo("⚠️  Claude is running. Switching environments may cause issues.")
            if not click.confirm("Continue anyway?"):
                click.echo("Cancelled.")
                return
            force = True

        switch_environment(name, force=force)
        click.echo(f"✓ Switched to environment '{name}'")
    except RuntimeError as e:
        click.echo(f"Error: {e}", err=True)
        raise SystemExit(1)
```

**Step 4: Run tests to verify they pass**

Run: `pytest tests/test_cli_use.py -v`
Expected: PASS (5 tests)

**Step 5: Commit**

```bash
git add src/cenv/cli.py tests/test_cli_use.py
git commit -m "feat: add use command with confirmation prompt"
```

---

## Task 12: CLI Commands - List, Current, Delete

**Files:**
- Modify: `src/cenv/cli.py`
- Create: `tests/test_cli_list.py`

**Step 1: Write the failing test for CLI list/current/delete commands**

Create `tests/test_cli_list.py`:

```python
import pytest
from click.testing import CliRunner
from cenv.cli import cli
from unittest.mock import patch

def test_list_command_shows_all_environments():
    """Test that 'cenv list' shows all environments"""
    runner = CliRunner()

    with patch("cenv.cli.list_environments", return_value=["default", "work", "personal"]):
        with patch("cenv.cli.get_current_environment", return_value="work"):
            result = runner.invoke(cli, ["list"])

            assert result.exit_code == 0
            assert "default" in result.output
            assert "work" in result.output
            assert "personal" in result.output
            # Current environment should be marked
            assert "*" in result.output or "→" in result.output

def test_list_command_shows_empty_message():
    """Test that list shows message when no environments exist"""
    runner = CliRunner()

    with patch("cenv.cli.list_environments", return_value=[]):
        result = runner.invoke(cli, ["list"])

        assert result.exit_code == 0
        assert "No environments" in result.output or "cenv init" in result.output

def test_current_command_shows_active_environment():
    """Test that 'cenv current' shows active environment"""
    runner = CliRunner()

    with patch("cenv.cli.get_current_environment", return_value="work"):
        result = runner.invoke(cli, ["current"])

        assert result.exit_code == 0
        assert "work" in result.output

def test_current_command_shows_not_initialized():
    """Test that current shows message when not initialized"""
    runner = CliRunner()

    with patch("cenv.cli.get_current_environment", return_value=None):
        result = runner.invoke(cli, ["current"])

        assert result.exit_code == 0
        assert "not initialized" in result.output or "cenv init" in result.output

def test_delete_command_removes_environment():
    """Test that 'cenv delete work' removes environment"""
    runner = CliRunner()

    with patch("cenv.cli.delete_environment") as mock_delete:
        # User confirms with 'y'
        result = runner.invoke(cli, ["delete", "work"], input="y\n")

        assert result.exit_code == 0
        mock_delete.assert_called_once_with("work")
        assert "Deleted" in result.output

def test_delete_command_prompts_for_confirmation():
    """Test that delete prompts for confirmation"""
    runner = CliRunner()

    with patch("cenv.cli.delete_environment") as mock_delete:
        # User declines with 'n'
        result = runner.invoke(cli, ["delete", "work"], input="n\n")

        assert result.exit_code == 0
        mock_delete.assert_not_called()
        assert "Cancelled" in result.output

def test_delete_command_with_force_skips_prompt():
    """Test that --force flag skips confirmation"""
    runner = CliRunner()

    with patch("cenv.cli.delete_environment") as mock_delete:
        result = runner.invoke(cli, ["delete", "work", "--force"])

        assert result.exit_code == 0
        mock_delete.assert_called_once_with("work")

def test_delete_command_shows_error_if_not_exists():
    """Test that delete shows error if environment doesn't exist"""
    runner = CliRunner()

    with patch("cenv.cli.delete_environment", side_effect=RuntimeError("does not exist")):
        result = runner.invoke(cli, ["delete", "nonexistent"], input="y\n")

        assert result.exit_code == 1
        assert "Error" in result.output
```

**Step 2: Run test to verify it fails**

Run: `pytest tests/test_cli_list.py -v`
Expected: FAIL with attribute errors for commands

**Step 3: Write minimal implementation**

Modify `src/cenv/cli.py`, add to end:

```python
@cli.command()
def list():
    """List all environments"""
    envs = list_environments()
    current = get_current_environment()

    if not envs:
        click.echo("No environments found.")
        click.echo("Run 'cenv init' to initialize.")
        return

    click.echo("Available environments:")
    for env in sorted(envs):
        marker = " → " if env == current else "   "
        click.echo(f"{marker}{env}")

@cli.command()
def current():
    """Show the currently active environment"""
    current_env = get_current_environment()

    if current_env is None:
        click.echo("No active environment.")
        click.echo("Run 'cenv init' to initialize.")
    else:
        click.echo(current_env)

@cli.command()
@click.argument("name")
@click.option("--force", "-f", is_flag=True, help="Skip confirmation prompt")
def delete(name: str, force: bool):
    """Delete an environment"""
    try:
        if not force:
            if not click.confirm(f"Delete environment '{name}'?"):
                click.echo("Cancelled.")
                return

        delete_environment(name)
        click.echo(f"✓ Deleted environment '{name}'")
    except RuntimeError as e:
        click.echo(f"Error: {e}", err=True)
        raise SystemExit(1)
```

**Step 4: Run tests to verify they pass**

Run: `pytest tests/test_cli_list.py -v`
Expected: PASS (8 tests)

**Step 5: Commit**

```bash
git add src/cenv/cli.py tests/test_cli_list.py
git commit -m "feat: add list, current, and delete commands"
```

---

## Task 13: Integration Testing and Documentation

**Files:**
- Create: `tests/test_integration.py`
- Create: `README.md`
- Create: `docs/usage.md`

**Step 1: Write integration test**

Create `tests/test_integration.py`:

```python
"""Integration tests for full cenv workflow"""
import pytest
from click.testing import CliRunner
from cenv.cli import cli
from pathlib import Path
import tempfile
from unittest.mock import patch

@pytest.fixture
def isolated_home(monkeypatch, tmp_path):
    """Create isolated home directory for testing"""
    home = tmp_path / "home"
    home.mkdir()

    # Create initial ~/.claude
    claude_dir = home / ".claude"
    claude_dir.mkdir()
    (claude_dir / "CLAUDE.md").write_text("# Original")
    (claude_dir / "settings.json").write_text('{"test": true}')

    monkeypatch.setattr("cenv.core.Path.home", lambda: home)

    return home

def test_full_workflow_init_create_use_delete(isolated_home):
    """Test complete workflow: init → create → use → delete"""
    runner = CliRunner()

    with patch("cenv.process.is_claude_running", return_value=False):
        # Step 1: Initialize
        result = runner.invoke(cli, ["init"])
        assert result.exit_code == 0
        assert (isolated_home / ".claude-envs" / "default").exists()
        assert (isolated_home / ".claude").is_symlink()

        # Step 2: Create work environment
        result = runner.invoke(cli, ["create", "work"])
        assert result.exit_code == 0
        assert (isolated_home / ".claude-envs" / "work").exists()

        # Step 3: List environments
        result = runner.invoke(cli, ["list"])
        assert result.exit_code == 0
        assert "default" in result.output
        assert "work" in result.output

        # Step 4: Check current (should be default)
        result = runner.invoke(cli, ["current"])
        assert result.exit_code == 0
        assert "default" in result.output

        # Step 5: Switch to work
        result = runner.invoke(cli, ["use", "work"])
        assert result.exit_code == 0
        assert (isolated_home / ".claude").resolve() == (isolated_home / ".claude-envs" / "work")

        # Step 6: Check current (should be work)
        result = runner.invoke(cli, ["current"])
        assert result.exit_code == 0
        assert "work" in result.output

        # Step 7: Switch back to default
        result = runner.invoke(cli, ["use", "default"])
        assert result.exit_code == 0

        # Step 8: Delete work environment
        result = runner.invoke(cli, ["delete", "work", "--force"])
        assert result.exit_code == 0
        assert not (isolated_home / ".claude-envs" / "work").exists()

def test_error_handling_workflow(isolated_home):
    """Test error handling in various scenarios"""
    runner = CliRunner()

    # Cannot create before init
    result = runner.invoke(cli, ["create", "work"])
    assert result.exit_code == 1
    assert "not initialized" in result.output

    # Initialize
    with patch("cenv.process.is_claude_running", return_value=False):
        result = runner.invoke(cli, ["init"])
        assert result.exit_code == 0

    # Cannot init twice
    result = runner.invoke(cli, ["init"])
    assert result.exit_code == 1
    assert "already initialized" in result.output

    # Cannot use non-existent environment
    with patch("cenv.process.is_claude_running", return_value=False):
        result = runner.invoke(cli, ["use", "nonexistent"])
        assert result.exit_code == 1
        assert "does not exist" in result.output

    # Cannot delete default environment
    result = runner.invoke(cli, ["delete", "default", "--force"])
    assert result.exit_code == 1
    assert "cannot delete default" in result.output
```

**Step 2: Run test to verify current state**

Run: `pytest tests/test_integration.py -v`
Expected: PASS (2 tests)

**Step 3: Create user documentation**

Create `README.md`:

```markdown
# cenv - Claude Environment Manager

Manage isolated Claude Code configurations like pyenv manages Python versions. Switch between different Claude setups (work, personal, experiments) with complete isolation.

## Installation

```bash
# Using uv (recommended)
git clone https://github.com/yourusername/cenv.git
cd cenv
uv pip install -e .

# Or using pip
pip install -e .
```

## Quick Start

```bash
# Initialize cenv (migrates your current ~/.claude to default environment)
cenv init

# Create a new environment for work
cenv create work

# Switch to work environment
cenv use work

# List all environments (* marks active)
cenv list

# Show current environment
cenv current

# Delete an environment
cenv delete work
```

## Features

- **Complete Isolation**: Each environment has its own CLAUDE.md, settings.json, agents, plugins
- **Symlink-based**: Fast switching with no data copying
- **Safety Checks**: Warns when Claude is running
- **GitHub Templates**: Clone environment configs from repositories
- **Shared Credentials**: API keys stored in macOS Keychain work across all environments

## Commands

### `cenv init`

Initialize cenv by migrating your existing `~/.claude` to `~/.claude-envs/default/`.

```bash
cenv init
```

### `cenv create <name>`

Create a new environment. By default, copies from the `default` environment.

```bash
# Create from default
cenv create work

# Create from GitHub repository
cenv create work --from-repo https://github.com/user/claude-work-setup
```

### `cenv use <name>`

Switch to a different environment. Prompts for confirmation if Claude is running.

```bash
cenv use work

# Force switch without confirmation
cenv use work --force
```

### `cenv list`

List all available environments. Active environment is marked with `→`.

```bash
cenv list
```

### `cenv current`

Show the currently active environment.

```bash
cenv current
```

### `cenv delete <name>`

Delete an environment. Cannot delete the `default` environment or currently active environment.

```bash
cenv delete work

# Skip confirmation
cenv delete work --force
```

## How It Works

cenv uses symlinks for fast, efficient environment switching:

```
~/.claude              → symlink to active environment
~/.claude-envs/
  ├── default/         Your original setup
  ├── work/            Work configuration
  └── personal/        Personal configuration
```

Each environment contains:
- `CLAUDE.md` - Global instructions
- `settings.json` - Settings and preferences
- `agents/` - Custom agents
- `plugins/` - Installed plugins
- `history.jsonl`, `sessions/`, etc.

## Use Cases

**Work vs Personal**: Separate configurations for professional and personal use
```bash
cenv create work
cenv create personal
```

**Experimentation**: Test new plugins or settings without affecting your main setup
```bash
cenv create experiment
# Try new things...
cenv delete experiment
```

**Team Templates**: Share environment configs via GitHub
```bash
cenv create work --from-repo https://github.com/company/claude-work-template
```

## Development

```bash
# Install with dev dependencies
make install

# Run tests
make test

# Clean build artifacts
make clean
```

## License

MIT
```

Create `docs/usage.md`:

```markdown
# cenv Usage Guide

## Complete Workflow Example

### Initial Setup

```bash
# 1. Initialize cenv
$ cenv init
✓ Initialized cenv successfully!
  ~/.claude → ~/.claude-envs/default/

Use 'cenv create <name>' to create new environments.

# 2. Check what you have
$ cenv list
Available environments:
 → default

$ cenv current
default
```

### Creating Environments

```bash
# Create work environment (copies from default)
$ cenv create work
✓ Created environment 'work' from default

# Create from GitHub template
$ cenv create client-project --from-repo https://github.com/company/claude-client-setup
✓ Created environment 'client-project' from https://github.com/company/claude-client-setup

# List all environments
$ cenv list
Available environments:
   client-project
 → default
   work
```

### Switching Environments

```bash
# Switch to work environment
$ cenv use work
✓ Switched to environment 'work'

# Verify current environment
$ cenv current
work

# If Claude is running, you'll be prompted
$ cenv use personal
⚠️  Claude is running. Switching environments may cause issues.
Continue anyway? [y/N]: y
✓ Switched to environment 'personal'

# Skip the prompt with --force
$ cenv use work --force
✓ Switched to environment 'work'
```

### Managing Environments

```bash
# Delete an environment
$ cenv delete experiment
Delete environment 'experiment'? [y/N]: y
✓ Deleted environment 'experiment'

# Force delete without confirmation
$ cenv delete experiment --force
✓ Deleted environment 'experiment'

# Cannot delete default or active environment
$ cenv delete default
Error: Cannot delete default environment.

$ cenv current
work
$ cenv delete work
Error: Environment 'work' is currently active. Switch to another environment first.
```

## Advanced Usage

### GitHub Repository Templates

Create a repository with your Claude configuration:

```
my-claude-setup/
  ├── CLAUDE.md
  ├── settings.json
  ├── agents/
  │   └── custom-agent.md
  └── plugins/
      └── config.json
```

Share with your team:

```bash
cenv create team-setup --from-repo https://github.com/yourteam/claude-team-setup
```

### Environment Organization

**Strategy 1: By Project**
```bash
cenv create client-a
cenv create client-b
cenv create internal
```

**Strategy 2: By Role**
```bash
cenv create development  # Coding-focused plugins
cenv create writing      # Writing-focused agents
cenv create research     # Research tools
```

**Strategy 3: By Experiment**
```bash
cenv create stable       # Known good configuration
cenv create testing      # Try new things
```

## Troubleshooting

### "Claude is running" Warning

If you see this warning, either:
1. Exit Claude completely, then switch
2. Use `--force` flag (may cause issues with running session)

### Cannot Switch Environments

Check that `~/.claude` is a symlink:
```bash
ls -la ~/.claude
```

Should show: `~/.claude -> /Users/you/.claude-envs/default`

### Environment Missing After Switch

Verify environments exist:
```bash
ls ~/.claude-envs/
```

Check current symlink target:
```bash
readlink ~/.claude
```

### Reset to Default

If something goes wrong:
```bash
# Switch back to default
cenv use default --force

# Or manually:
rm ~/.claude
ln -s ~/.claude-envs/default ~/.claude
```
```

**Step 4: Run all tests**

Run: `pytest -v`
Expected: All tests pass

**Step 5: Commit**

```bash
git add tests/test_integration.py README.md docs/usage.md
git commit -m "docs: add comprehensive documentation and integration tests"
```

---

## Task 14: Final Validation and Release Preparation

**Files:**
- Create: `CHANGELOG.md`
- Create: `.gitignore`
- Modify: `README.md` (add badges)

**Step 1: Create CHANGELOG**

Create `CHANGELOG.md`:

```markdown
# Changelog

All notable changes to cenv will be documented in this file.

## [0.1.0] - 2025-11-01

### Added
- Initial release of cenv
- Environment initialization (`cenv init`)
- Environment creation from default (`cenv create`)
- Environment creation from GitHub repos (`cenv create --from-repo`)
- Environment switching with safety checks (`cenv use`)
- Environment listing (`cenv list`)
- Current environment display (`cenv current`)
- Environment deletion with protection (`cenv delete`)
- Process detection for running Claude instances
- Automatic confirmation prompts when Claude is running
- Complete test coverage (unit + integration)
- Comprehensive documentation

### Features
- Symlink-based environment switching for speed
- Complete isolation between environments
- Shared credentials via macOS Keychain
- GitHub template support for team sharing
- Safety checks to prevent accidental data loss
```

**Step 2: Create .gitignore**

Create `.gitignore`:

```
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Virtual environments
venv/
ENV/
env/
.venv

# IDEs
.vscode/
.idea/
*.swp
*.swo
*~

# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/

# OS
.DS_Store
Thumbs.db
```

**Step 3: Run all tests one final time**

Run:
```bash
pytest -v --cov=src/cenv --cov-report=html
```
Expected: All tests pass with good coverage

**Step 4: Manual smoke test**

Manual test in a safe environment:
```bash
# Build and install
make install

# Test basic workflow
cenv --help
cenv --version
```

**Step 5: Final commit**

```bash
git add CHANGELOG.md .gitignore
git commit -m "chore: add changelog and gitignore for v0.1.0 release"
```

---

## Completion Summary

**Files Created:**
- `pyproject.toml` - Project configuration
- `src/cenv/__init__.py` - Package initialization
- `src/cenv/core.py` - Core functionality (paths, init, create, switch, delete, list)
- `src/cenv/process.py` - Process detection
- `src/cenv/github.py` - GitHub cloning
- `src/cenv/cli.py` - CLI interface
- `tests/test_*.py` - Comprehensive test suite (13 test files)
- `README.md` - User documentation
- `docs/usage.md` - Detailed usage guide
- `docs/plans/2025-11-01-cenv.md` - This implementation plan
- `Makefile` - Build automation
- `.python-version` - Python version specification
- `CHANGELOG.md` - Release notes
- `.gitignore` - Git exclusions

**Commands Implemented:**
- `cenv init` - Initialize environment system
- `cenv create <name> [--from-repo URL]` - Create environments
- `cenv use <name> [--force]` - Switch environments
- `cenv list` - List all environments
- `cenv current` - Show active environment
- `cenv delete <name> [--force]` - Delete environments

**Test Coverage:**
- Unit tests for all core functions
- CLI command tests
- Integration tests for full workflows
- Error handling tests
- Process detection tests
- GitHub cloning tests

**Next Steps:**
1. Use superpowers:executing-plans or superpowers:subagent-driven-development to implement
2. After implementation, test thoroughly
3. Tag v0.1.0 release
4. Publish to PyPI (optional)
5. Share with users!
