# VibePod Project Structure

**Repository:** `VibePod/vibepod`  
**Language:** Python 3.10+  

---

## Directory Structure

```
VibePod/vibepod/
├── src/
│   └── vibepod/
│       ├── __init__.py              # Version and package info
│       ├── cli.py                   # Main Typer app
│       ├── commands/
│       │   ├── __init__.py
│       │   ├── run.py               # vp run
│       │   ├── stop.py              # vp stop
│       │   ├── list.py              # vp list
│       │   ├── logs.py              # vp logs
│       │   ├── exec.py              # vp exec, vp shell
│       │   ├── skills.py            # vp skills
│       │   ├── config.py            # vp config
│       │   ├── auth.py              # vp auth
│       │   ├── init.py              # vp init
│       │   └── update.py            # vp update, vp version
│       ├── core/
│       │   ├── __init__.py
│       │   ├── docker.py            # Docker SDK wrapper
│       │   ├── config.py            # Config management
│       │   ├── agents.py            # Agent registry
│       │   ├── skills.py            # AGENTS.lock integration
│       │   └── logging.py           # Datasette/SQLite logging
│       ├── utils/
│       │   ├── __init__.py
│       │   ├── console.py           # Rich console output
│       │   ├── paths.py             # Path utilities
│       │   └── validators.py        # Input validation
│       └── constants.py             # Constants and defaults
├── tests/
│   ├── __init__.py
│   ├── conftest.py                  # Pytest fixtures
│   ├── test_cli.py
│   ├── test_docker.py
│   ├── test_config.py
│   └── test_skills.py
├── completions/
│   ├── vp.bash
│   ├── vp.zsh
│   └── vp.fish
├── scripts/
│   ├── install.sh                   # Unix install script
│   └── install.ps1                  # Windows install script
├── .github/
│   └── workflows/
│       ├── test.yml                 # CI tests
│       ├── release.yml              # PyPI + binaries
│       └── docker.yml               # Datasette image
├── pyproject.toml
├── README.md
├── LICENSE
├── CHANGELOG.md
└── Makefile
```

---

## pyproject.toml

```toml
[project]
name = "vibepod"
version = "1.0.0"
description = "One CLI for all AI coding agents"
readme = "README.md"
license = { text = "MIT" }
authors = [
    { name = "nezhar", email = "your-email@example.com" }
]
keywords = [
    "cli",
    "docker",
    "ai",
    "coding-agents",
    "claude",
    "gemini",
    "opencode",
]
classifiers = [
    "Development Status :: 4 - Beta",
    "Environment :: Console",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Topic :: Software Development :: Build Tools",
    "Topic :: System :: Software Distribution",
]
requires-python = ">=3.10"
dependencies = [
    "typer[all]>=0.12.0",
    "docker>=7.0.0",
    "pyyaml>=6.0",
    "rich>=13.0",
    "platformdirs>=4.0",
    "agentslock>=0.1.0",
    "httpx>=0.27.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "pytest-cov>=4.0",
    "pytest-mock>=3.0",
    "ruff>=0.3.0",
    "mypy>=1.8",
    "pre-commit>=3.6",
]
build = [
    "pyinstaller>=6.0",
    "build>=1.0",
    "twine>=5.0",
]

[project.scripts]
vp = "vibepod.cli:app"
vibepod = "vibepod.cli:app"

[project.urls]
Homepage = "https://vibepod.dev"
Documentation = "https://vibepod.dev/docs"
Repository = "https://github.com/VibePod/vibepod"
Issues = "https://github.com/VibePod/vibepod/issues"
Changelog = "https://github.com/VibePod/vibepod/blob/main/CHANGELOG.md"

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

[tool.hatch.build.targets.sdist]
include = [
    "/src",
    "/completions",
    "/scripts",
]

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

[tool.ruff]
line-length = 88
target-version = "py310"

[tool.ruff.lint]
select = [
    "E",    # pycodestyle errors
    "W",    # pycodestyle warnings
    "F",    # pyflakes
    "I",    # isort
    "B",    # flake8-bugbear
    "C4",   # flake8-comprehensions
    "UP",   # pyupgrade
]
ignore = [
    "E501",  # line too long (handled by formatter)
]

[tool.ruff.lint.isort]
known-first-party = ["vibepod"]

[tool.mypy]
python_version = "3.10"
strict = true
warn_return_any = true
warn_unused_ignores = true

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --cov=vibepod --cov-report=term-missing"

[tool.coverage.run]
source = ["src/vibepod"]
branch = true

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "if TYPE_CHECKING:",
    "raise NotImplementedError",
]
```

---

## Core Modules

### `src/vibepod/__init__.py`

```python
"""VibePod - One CLI for all AI coding agents."""

__version__ = "1.0.0"
__author__ = "nezhar"
```

### `src/vibepod/constants.py`

```python
"""Constants and default values."""

from pathlib import Path
from platformdirs import user_config_dir, user_cache_dir

# Package info
APP_NAME = "vibepod"
VERSION = "1.0.0"

# Paths
CONFIG_DIR = Path(user_config_dir(APP_NAME))
CACHE_DIR = Path(user_cache_dir(APP_NAME))
GLOBAL_CONFIG_FILE = CONFIG_DIR / "config.yaml"
LOGS_DB_FILE = CONFIG_DIR / "logs.db"
GLOBAL_LOCKFILE = CONFIG_DIR / "AGENTS.lock"

# Project paths
PROJECT_CONFIG_DIR = ".vibepod"
PROJECT_CONFIG_FILE = "config.yaml"
PROJECT_LOCKFILE = "AGENTS.lock"

# Docker
DOCKER_NETWORK = "vibepod-network"
CONTAINER_PREFIX = "vibepod"

# Default images
DEFAULT_IMAGES = {
    "claude": "vibepod/claude:latest",
    "gemini": "vibepod/gemini:latest",
    "opencode": "vibepod/opencode:latest",
    "devstral": "vibepod/devstral:latest",
    "auggie": "vibepod/auggie:latest",
    "datasette": "vibepod/datasette:latest",
}

# Agent metadata
AGENTS = {
    "claude": {
        "name": "Claude Code",
        "provider": "Anthropic",
        "image": "vibepod/claude:latest",
        "config_subdir": "claude",
    },
    "gemini": {
        "name": "Gemini CLI",
        "provider": "Google",
        "image": "vibepod/gemini:latest",
        "config_subdir": "gemini",
    },
    "opencode": {
        "name": "OpenCode",
        "provider": "OpenAI",
        "image": "vibepod/opencode:latest",
        "config_subdir": "opencode",
    },
    "devstral": {
        "name": "Devstral",
        "provider": "Mistral",
        "image": "vibepod/devstral:latest",
        "config_subdir": "devstral",
    },
    "auggie": {
        "name": "Auggie",
        "provider": "Augment",
        "image": "vibepod/auggie:latest",
        "config_subdir": "auggie",
    },
}

# Skills shortcuts
SKILL_SHORTCUTS = {
    "vibepod": "https://github.com/VibePod/skills/tree/main",
    "anthropic": "https://github.com/anthropics/skills/tree/main/skills",
}

# Exit codes
EXIT_SUCCESS = 0
EXIT_ERROR = 1
EXIT_INVALID_ARGS = 2
EXIT_DOCKER_NOT_RUNNING = 3
EXIT_IMAGE_NOT_FOUND = 4
EXIT_AUTH_REQUIRED = 5
EXIT_NETWORK_ERROR = 6
EXIT_CONTAINER_ERROR = 7
EXIT_CONFIG_ERROR = 8

# Logging
DEFAULT_DATASETTE_PORT = 8001
DEFAULT_LOG_RETENTION_DAYS = 30

# Aliases
DEFAULT_ALIASES = {
    "c": "run claude",
    "g": "run gemini",
    "o": "run opencode",
    "d": "run devstral",
    "a": "run auggie",
    "ui": "logs ui",
}
```

### `src/vibepod/cli.py`

```python
"""Main CLI application."""

import typer
from rich.console import Console

from vibepod import __version__
from vibepod.commands import run, stop, list_cmd, logs, exec_cmd, skills, config, auth, init, update

# Console for rich output
console = Console()

# Main app
app = typer.Typer(
    name="vp",
    help="VibePod - One CLI for all AI coding agents",
    no_args_is_help=True,
    rich_markup_mode="rich",
    context_settings={"help_option_names": ["-h", "--help"]},
)

# Add command groups
app.add_typer(logs.app, name="logs", help="View logs and analytics")
app.add_typer(skills.app, name="skills", help="Manage skills via AGENTS.lock")
app.add_typer(config.app, name="config", help="Manage configuration")
app.add_typer(auth.app, name="auth", help="Manage authentication")

# Add standalone commands
app.command()(run.run)
app.command()(stop.stop)
app.command(name="list")(list_cmd.list_agents)
app.command()(exec_cmd.exec_command)
app.command()(exec_cmd.shell)
app.command()(init.init)
app.command()(update.update)
app.command()(update.version)


def main():
    """Entry point."""
    app()


if __name__ == "__main__":
    main()
```

---

## Command Module Template

### `src/vibepod/commands/run.py`

```python
"""Run command - start agent containers."""

from pathlib import Path
from typing import Optional, List

import typer
from rich.console import Console
from typing_extensions import Annotated

from vibepod.core.docker import DockerClient
from vibepod.core.config import get_config
from vibepod.core.agents import get_agent, validate_agent
from vibepod.core.skills import sync_skills_if_enabled
from vibepod.constants import AGENTS, EXIT_DOCKER_NOT_RUNNING

console = Console()


def run(
    agent: Annotated[
        Optional[str],
        typer.Argument(help="Agent to run (claude, gemini, opencode, devstral, auggie)"),
    ] = None,
    workspace: Annotated[
        Path,
        typer.Option("-w", "--workspace", help="Workspace directory"),
    ] = Path("."),
    proxy: Annotated[
        bool,
        typer.Option("-p", "--proxy", help="Enable API logging proxy"),
    ] = False,
    detach: Annotated[
        bool,
        typer.Option("-d", "--detach", help="Run in background"),
    ] = False,
    env: Annotated[
        Optional[List[str]],
        typer.Option("-e", "--env", help="Environment variables (KEY=VAL)"),
    ] = None,
    prompt: Annotated[
        Optional[str],
        typer.Option("--prompt", help="Initial prompt"),
    ] = None,
    headless: Annotated[
        bool,
        typer.Option("--headless", help="Non-interactive mode"),
    ] = False,
    pull: Annotated[
        bool,
        typer.Option("--pull", help="Pull latest image"),
    ] = False,
):
    """Start an agent container."""
    config = get_config()
    
    # Default agent from config
    if agent is None:
        agent = config.get("default_agent", "claude")
    
    # Validate agent
    if not validate_agent(agent):
        console.print(f"[red]Unknown agent: {agent}[/red]")
        console.print(f"Available: {', '.join(AGENTS.keys())}")
        raise typer.Exit(1)
    
    # Get Docker client
    try:
        docker = DockerClient()
    except Exception as e:
        console.print(f"[red]Docker not available: {e}[/red]")
        raise typer.Exit(EXIT_DOCKER_NOT_RUNNING)
    
    # Auto-sync skills if enabled
    sync_skills_if_enabled()
    
    agent_info = get_agent(agent)
    console.print(f"[green]Starting {agent_info['name']}...[/green]")
    
    # Pull if requested
    if pull or config.get("auto_pull", False):
        console.print(f"Pulling {agent_info['image']}...")
        docker.pull_image(agent_info["image"])
    
    # Start container
    container = docker.run_agent(
        agent=agent,
        workspace=workspace.resolve(),
        proxy=proxy,
        detach=detach,
        env=env or [],
        headless=headless,
    )
    
    if detach:
        console.print(f"[green]✓ {agent_info['name']} started in background[/green]")
        console.print(f"  Container: {container.name}")
        console.print(f"  Stop with: vp stop {agent}")
    else:
        # Attach handled by DockerClient
        pass
```

---

## Makefile

```makefile
.PHONY: install dev test lint format build release clean

# Install for development
install:
	pip install -e ".[dev]"

# Install all dependencies
dev:
	pip install -e ".[dev,build]"
	pre-commit install

# Run tests
test:
	pytest

# Run tests with coverage
coverage:
	pytest --cov=vibepod --cov-report=html
	open htmlcov/index.html

# Lint
lint:
	ruff check src tests
	mypy src

# Format
format:
	ruff format src tests
	ruff check --fix src tests

# Build package
build:
	python -m build

# Build binaries with PyInstaller
binary:
	pyinstaller --onefile --name vp src/vibepod/cli.py

# Build binaries for all platforms (CI)
binaries:
	# Linux
	pyinstaller --onefile --name vp-linux-amd64 src/vibepod/cli.py
	# macOS handled by CI matrix

# Upload to PyPI
release: build
	twine upload dist/*

# Clean
clean:
	rm -rf build dist *.egg-info
	rm -rf .pytest_cache .mypy_cache .ruff_cache
	rm -rf htmlcov .coverage
	find . -type d -name __pycache__ -exec rm -rf {} +

# Generate shell completions
completions:
	vp --install-completion bash > completions/vp.bash
	vp --install-completion zsh > completions/vp.zsh
	vp --install-completion fish > completions/vp.fish
```

---

## Testing Structure

### `tests/conftest.py`

```python
"""Pytest fixtures."""

import pytest
from pathlib import Path
from unittest.mock import MagicMock

@pytest.fixture
def mock_docker():
    """Mock Docker client."""
    mock = MagicMock()
    mock.ping.return_value = True
    return mock

@pytest.fixture
def temp_config(tmp_path):
    """Temporary config directory."""
    config_dir = tmp_path / ".config" / "vibepod"
    config_dir.mkdir(parents=True)
    return config_dir

@pytest.fixture
def temp_workspace(tmp_path):
    """Temporary workspace directory."""
    workspace = tmp_path / "workspace"
    workspace.mkdir()
    return workspace
```

### `tests/test_cli.py`

```python
"""CLI tests."""

from typer.testing import CliRunner
from vibepod.cli import app

runner = CliRunner()

def test_version():
    result = runner.invoke(app, ["version"])
    assert result.exit_code == 0
    assert "VibePod" in result.stdout

def test_help():
    result = runner.invoke(app, ["--help"])
    assert result.exit_code == 0
    assert "VibePod" in result.stdout

def test_list():
    result = runner.invoke(app, ["list", "--available"])
    assert result.exit_code == 0
    assert "claude" in result.stdout
```
