mirror of
https://github.com/Monadical-SAS/cubbi.git
synced 2025-12-20 12:19:07 +00:00
feat: implement Aider AI pair programming support (#17)
* feat: implement Aider AI pair programming support - Add comprehensive Aider Docker image with Python 3.12 and system pip installation - Implement aider_plugin.py for secure API key management and environment configuration - Support multiple LLM providers: OpenAI, Anthropic, DeepSeek, Gemini, OpenRouter - Add persistent configuration for ~/.aider/ and ~/.cache/aider/ directories - Create comprehensive documentation with usage examples and troubleshooting - Include automated test suite with 6 test categories covering all functionality - Update container.py to support DEEPSEEK_API_KEY and GEMINI_API_KEY - Integrate with Cubbi CLI for seamless session management 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix pytest for aider * Fix pre-commit --------- Co-authored-by: Your Name <you@example.com>
This commit is contained in:
committed by
GitHub
parent
b28c2bd63e
commit
fc0d6b51af
3
.github/workflows/pytests.yml
vendored
3
.github/workflows/pytests.yml
vendored
@@ -30,10 +30,11 @@ jobs:
|
||||
- name: Install all dependencies
|
||||
run: uv sync --frozen --all-extras --all-groups
|
||||
|
||||
- name: Build goose image
|
||||
- name: Build required images
|
||||
run: |
|
||||
uv tool install --with-editable . .
|
||||
cubbi image build goose
|
||||
cubbi image build aider
|
||||
|
||||
- name: Tests
|
||||
run: |
|
||||
|
||||
67
cubbi/images/aider/Dockerfile
Normal file
67
cubbi/images/aider/Dockerfile
Normal file
@@ -0,0 +1,67 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
LABEL maintainer="team@monadical.com"
|
||||
LABEL description="Aider AI pair programming for Cubbi"
|
||||
|
||||
# Install system dependencies including gosu for user switching
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gosu \
|
||||
passwd \
|
||||
bash \
|
||||
curl \
|
||||
bzip2 \
|
||||
iputils-ping \
|
||||
iproute2 \
|
||||
libxcb1 \
|
||||
libdbus-1-3 \
|
||||
nano \
|
||||
tmux \
|
||||
git-core \
|
||||
ripgrep \
|
||||
openssh-client \
|
||||
vim \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install uv (Python package manager)
|
||||
WORKDIR /tmp
|
||||
RUN curl -fsSL https://astral.sh/uv/install.sh -o install.sh && \
|
||||
sh install.sh && \
|
||||
mv /root/.local/bin/uv /usr/local/bin/uv && \
|
||||
mv /root/.local/bin/uvx /usr/local/bin/uvx && \
|
||||
rm install.sh
|
||||
|
||||
# Install Aider using pip in system Python (more compatible with user switching)
|
||||
RUN python -m pip install aider-chat
|
||||
|
||||
# Make sure aider is in PATH
|
||||
ENV PATH="/root/.local/bin:$PATH"
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy initialization system
|
||||
COPY cubbi_init.py /cubbi/cubbi_init.py
|
||||
COPY aider_plugin.py /cubbi/aider_plugin.py
|
||||
COPY cubbi_image.yaml /cubbi/cubbi_image.yaml
|
||||
COPY init-status.sh /cubbi/init-status.sh
|
||||
|
||||
# Make scripts executable
|
||||
RUN chmod +x /cubbi/cubbi_init.py /cubbi/init-status.sh
|
||||
|
||||
# Add aider to PATH in bashrc and init status check
|
||||
RUN echo 'PATH="/root/.local/bin:$PATH"' >> /etc/bash.bashrc
|
||||
RUN echo '[ -x /cubbi/init-status.sh ] && /cubbi/init-status.sh' >> /etc/bash.bashrc
|
||||
|
||||
# Set up environment
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV UV_LINK_MODE=copy
|
||||
|
||||
# Pre-install the cubbi_init
|
||||
RUN /cubbi/cubbi_init.py --help
|
||||
|
||||
# Set WORKDIR to /app
|
||||
WORKDIR /app
|
||||
|
||||
ENTRYPOINT ["/cubbi/cubbi_init.py"]
|
||||
CMD ["tail", "-f", "/dev/null"]
|
||||
277
cubbi/images/aider/README.md
Normal file
277
cubbi/images/aider/README.md
Normal file
@@ -0,0 +1,277 @@
|
||||
# Aider for Cubbi
|
||||
|
||||
This image provides Aider (AI pair programming) in a Cubbi container environment.
|
||||
|
||||
## Overview
|
||||
|
||||
Aider is an AI pair programming tool that works in your terminal. This Cubbi image integrates Aider with secure API key management, persistent configuration, and support for multiple LLM providers.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multiple LLM Support**: Works with OpenAI, Anthropic, DeepSeek, Gemini, OpenRouter, and more
|
||||
- **Secure Authentication**: API key management through Cubbi's secure environment system
|
||||
- **Persistent Configuration**: Settings and history preserved across container restarts
|
||||
- **Git Integration**: Automatic commits and git awareness
|
||||
- **Multi-Language Support**: Works with 100+ programming languages
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Set up API Key
|
||||
|
||||
```bash
|
||||
# For OpenAI (GPT models)
|
||||
uv run -m cubbi.cli config set services.openai.api_key "your-openai-key"
|
||||
|
||||
# For Anthropic (Claude models)
|
||||
uv run -m cubbi.cli config set services.anthropic.api_key "your-anthropic-key"
|
||||
|
||||
# For DeepSeek (recommended for cost-effectiveness)
|
||||
uv run -m cubbi.cli config set services.deepseek.api_key "your-deepseek-key"
|
||||
```
|
||||
|
||||
### 2. Run Aider Environment
|
||||
|
||||
```bash
|
||||
# Start Aider container with your project
|
||||
uv run -m cubbi.cli session create --image aider /path/to/your/project
|
||||
|
||||
# Or without a project
|
||||
uv run -m cubbi.cli session create --image aider
|
||||
```
|
||||
|
||||
### 3. Use Aider
|
||||
|
||||
```bash
|
||||
# Basic usage
|
||||
aider
|
||||
|
||||
# With specific model
|
||||
aider --model sonnet
|
||||
|
||||
# With specific files
|
||||
aider main.py utils.py
|
||||
|
||||
# One-shot request
|
||||
aider --message "Add error handling to the login function"
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Supported API Keys
|
||||
|
||||
- `OPENAI_API_KEY`: OpenAI GPT models (GPT-4, GPT-4o, etc.)
|
||||
- `ANTHROPIC_API_KEY`: Anthropic Claude models (Sonnet, Haiku, etc.)
|
||||
- `DEEPSEEK_API_KEY`: DeepSeek models (cost-effective option)
|
||||
- `GEMINI_API_KEY`: Google Gemini models
|
||||
- `OPENROUTER_API_KEY`: OpenRouter (access to many models)
|
||||
|
||||
### Additional Configuration
|
||||
|
||||
- `AIDER_MODEL`: Default model to use (e.g., "sonnet", "o3-mini", "deepseek")
|
||||
- `AIDER_AUTO_COMMITS`: Enable automatic git commits (default: true)
|
||||
- `AIDER_DARK_MODE`: Enable dark mode interface (default: false)
|
||||
- `AIDER_API_KEYS`: Additional API keys in format "provider1=key1,provider2=key2"
|
||||
|
||||
### Network Configuration
|
||||
|
||||
- `HTTP_PROXY`: HTTP proxy server URL
|
||||
- `HTTPS_PROXY`: HTTPS proxy server URL
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic AI Pair Programming
|
||||
|
||||
```bash
|
||||
# Start Aider with your project
|
||||
uv run -m cubbi.cli session create --image aider /path/to/project
|
||||
|
||||
# Inside the container:
|
||||
aider # Start interactive session
|
||||
aider main.py # Work on specific file
|
||||
aider --message "Add tests" # One-shot request
|
||||
```
|
||||
|
||||
### Model Selection
|
||||
|
||||
```bash
|
||||
# Use Claude Sonnet
|
||||
aider --model sonnet
|
||||
|
||||
# Use GPT-4o
|
||||
aider --model gpt-4o
|
||||
|
||||
# Use DeepSeek (cost-effective)
|
||||
aider --model deepseek
|
||||
|
||||
# Use OpenRouter
|
||||
aider --model openrouter/anthropic/claude-3.5-sonnet
|
||||
```
|
||||
|
||||
### Advanced Features
|
||||
|
||||
```bash
|
||||
# Work with multiple files
|
||||
aider src/main.py tests/test_main.py
|
||||
|
||||
# Auto-commit changes
|
||||
aider --auto-commits
|
||||
|
||||
# Read-only mode (won't edit files)
|
||||
aider --read
|
||||
|
||||
# Apply a specific change
|
||||
aider --message "Refactor the database connection code to use connection pooling"
|
||||
```
|
||||
|
||||
### Enterprise/Proxy Setup
|
||||
|
||||
```bash
|
||||
# With proxy
|
||||
uv run -m cubbi.cli session create --image aider \
|
||||
--env HTTPS_PROXY="https://proxy.company.com:8080" \
|
||||
/path/to/project
|
||||
|
||||
# With custom model
|
||||
uv run -m cubbi.cli session create --image aider \
|
||||
--env AIDER_MODEL="sonnet" \
|
||||
/path/to/project
|
||||
```
|
||||
|
||||
## Persistent Configuration
|
||||
|
||||
The following directories are automatically persisted:
|
||||
|
||||
- `~/.aider/`: Aider configuration and chat history
|
||||
- `~/.cache/aider/`: Model cache and temporary files
|
||||
|
||||
Configuration files are maintained across container restarts, ensuring your preferences and chat history are preserved.
|
||||
|
||||
## Model Recommendations
|
||||
|
||||
### Best Overall Performance
|
||||
- **Claude 3.5 Sonnet**: Excellent code understanding and generation
|
||||
- **OpenAI GPT-4o**: Strong performance across languages
|
||||
- **Gemini 2.5 Pro**: Good balance of quality and speed
|
||||
|
||||
### Cost-Effective Options
|
||||
- **DeepSeek V3**: Very cost-effective, good quality
|
||||
- **OpenRouter**: Access to multiple models with competitive pricing
|
||||
|
||||
### Free Options
|
||||
- **Gemini 2.5 Pro Exp**: Free tier available
|
||||
- **OpenRouter**: Some free models available
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
cubbi/images/aider/
|
||||
├── Dockerfile # Container image definition
|
||||
├── cubbi_image.yaml # Cubbi image configuration
|
||||
├── aider_plugin.py # Authentication and setup plugin
|
||||
└── README.md # This documentation
|
||||
```
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
1. **Environment Variables**: API keys passed from Cubbi configuration
|
||||
2. **Plugin Setup**: `aider_plugin.py` creates environment configuration
|
||||
3. **Environment File**: Creates `~/.aider/.env` with API keys
|
||||
4. **Ready**: Aider is ready for use with configured authentication
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**No API Key Found**
|
||||
```
|
||||
ℹ️ No API keys found - Aider will run without pre-configuration
|
||||
```
|
||||
**Solution**: Set API key in Cubbi configuration:
|
||||
```bash
|
||||
uv run -m cubbi.cli config set services.openai.api_key "your-key"
|
||||
```
|
||||
|
||||
**Model Not Available**
|
||||
```
|
||||
Error: Model 'xyz' not found
|
||||
```
|
||||
**Solution**: Check available models for your provider:
|
||||
```bash
|
||||
aider --models # List available models
|
||||
```
|
||||
|
||||
**Git Issues**
|
||||
```
|
||||
Git repository not found
|
||||
```
|
||||
**Solution**: Initialize git in your project or mount a git repository:
|
||||
```bash
|
||||
git init
|
||||
# or
|
||||
uv run -m cubbi.cli session create --image aider /path/to/git/project
|
||||
```
|
||||
|
||||
**Network/Proxy Issues**
|
||||
```
|
||||
Connection timeout or proxy errors
|
||||
```
|
||||
**Solution**: Configure proxy settings:
|
||||
```bash
|
||||
uv run -m cubbi.cli config set network.https_proxy "your-proxy-url"
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
|
||||
```bash
|
||||
# Check Aider version
|
||||
aider --version
|
||||
|
||||
# List available models
|
||||
aider --models
|
||||
|
||||
# Check configuration
|
||||
cat ~/.aider/.env
|
||||
|
||||
# Verbose output
|
||||
aider --verbose
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **API Keys**: Stored securely with 0o600 permissions
|
||||
- **Environment**: Isolated container environment
|
||||
- **Git Integration**: Respects .gitignore and git configurations
|
||||
- **Code Safety**: Always review changes before accepting
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Custom Model Configuration
|
||||
|
||||
```bash
|
||||
# Use with custom API endpoint
|
||||
uv run -m cubbi.cli session create --image aider \
|
||||
--env OPENAI_API_BASE="https://api.custom-provider.com/v1" \
|
||||
--env OPENAI_API_KEY="your-key"
|
||||
```
|
||||
|
||||
### Multiple API Keys
|
||||
|
||||
```bash
|
||||
# Configure multiple providers
|
||||
uv run -m cubbi.cli session create --image aider \
|
||||
--env OPENAI_API_KEY="openai-key" \
|
||||
--env ANTHROPIC_API_KEY="anthropic-key" \
|
||||
--env AIDER_API_KEYS="provider1=key1,provider2=key2"
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For issues related to:
|
||||
- **Cubbi Integration**: Check Cubbi documentation or open an issue
|
||||
- **Aider Functionality**: Visit [Aider documentation](https://aider.chat/)
|
||||
- **Model Configuration**: Check [LLM documentation](https://aider.chat/docs/llms.html)
|
||||
- **API Keys**: Visit provider documentation (OpenAI, Anthropic, etc.)
|
||||
|
||||
## License
|
||||
|
||||
This image configuration is provided under the same license as the Cubbi project. Aider is licensed separately under Apache 2.0.
|
||||
186
cubbi/images/aider/aider_plugin.py
Executable file
186
cubbi/images/aider/aider_plugin.py
Executable file
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Aider Plugin for Cubbi
|
||||
Handles authentication setup and configuration for Aider AI pair programming
|
||||
"""
|
||||
|
||||
import os
|
||||
import stat
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from cubbi_init import ToolPlugin
|
||||
|
||||
|
||||
class AiderPlugin(ToolPlugin):
|
||||
"""Plugin for setting up Aider authentication and configuration"""
|
||||
|
||||
@property
|
||||
def tool_name(self) -> str:
|
||||
return "aider"
|
||||
|
||||
def _get_user_ids(self) -> tuple[int, int]:
|
||||
"""Get the cubbi user and group IDs from environment"""
|
||||
user_id = int(os.environ.get("CUBBI_USER_ID", "1000"))
|
||||
group_id = int(os.environ.get("CUBBI_GROUP_ID", "1000"))
|
||||
return user_id, group_id
|
||||
|
||||
def _set_ownership(self, path: Path) -> None:
|
||||
"""Set ownership of a path to the cubbi user"""
|
||||
user_id, group_id = self._get_user_ids()
|
||||
try:
|
||||
os.chown(path, user_id, group_id)
|
||||
except OSError as e:
|
||||
self.status.log(f"Failed to set ownership for {path}: {e}", "WARNING")
|
||||
|
||||
def _get_aider_config_dir(self) -> Path:
|
||||
"""Get the Aider configuration directory"""
|
||||
return Path("/home/cubbi/.aider")
|
||||
|
||||
def _get_aider_cache_dir(self) -> Path:
|
||||
"""Get the Aider cache directory"""
|
||||
return Path("/home/cubbi/.cache/aider")
|
||||
|
||||
def _ensure_aider_dirs(self) -> tuple[Path, Path]:
|
||||
"""Ensure Aider directories exist with correct ownership"""
|
||||
config_dir = self._get_aider_config_dir()
|
||||
cache_dir = self._get_aider_cache_dir()
|
||||
|
||||
# Create directories
|
||||
for directory in [config_dir, cache_dir]:
|
||||
try:
|
||||
directory.mkdir(mode=0o755, parents=True, exist_ok=True)
|
||||
self._set_ownership(directory)
|
||||
except OSError as e:
|
||||
self.status.log(
|
||||
f"Failed to create Aider directory {directory}: {e}", "ERROR"
|
||||
)
|
||||
|
||||
return config_dir, cache_dir
|
||||
|
||||
def initialize(self) -> bool:
|
||||
"""Initialize Aider configuration"""
|
||||
self.status.log("Setting up Aider configuration...")
|
||||
|
||||
# Ensure Aider directories exist
|
||||
config_dir, cache_dir = self._ensure_aider_dirs()
|
||||
|
||||
# Set up environment variables for the session
|
||||
env_vars = self._create_environment_config()
|
||||
|
||||
# Create .env file if we have API keys
|
||||
if env_vars:
|
||||
env_file = config_dir / ".env"
|
||||
success = self._write_env_file(env_file, env_vars)
|
||||
if success:
|
||||
self.status.log("✅ Aider environment configured successfully")
|
||||
else:
|
||||
self.status.log("⚠️ Failed to write Aider environment file", "WARNING")
|
||||
else:
|
||||
self.status.log(
|
||||
"ℹ️ No API keys found - Aider will run without pre-configuration", "INFO"
|
||||
)
|
||||
self.status.log(
|
||||
" You can configure API keys later using environment variables",
|
||||
"INFO",
|
||||
)
|
||||
|
||||
# Always return True to allow container to start
|
||||
return True
|
||||
|
||||
def _create_environment_config(self) -> Dict[str, str]:
|
||||
"""Create environment variable configuration for Aider"""
|
||||
env_vars = {}
|
||||
|
||||
# Map environment variables to Aider configuration
|
||||
api_key_mappings = {
|
||||
"OPENAI_API_KEY": "OPENAI_API_KEY",
|
||||
"ANTHROPIC_API_KEY": "ANTHROPIC_API_KEY",
|
||||
"DEEPSEEK_API_KEY": "DEEPSEEK_API_KEY",
|
||||
"GEMINI_API_KEY": "GEMINI_API_KEY",
|
||||
"OPENROUTER_API_KEY": "OPENROUTER_API_KEY",
|
||||
}
|
||||
|
||||
# Check for standard API keys
|
||||
for env_var, aider_var in api_key_mappings.items():
|
||||
value = os.environ.get(env_var)
|
||||
if value:
|
||||
env_vars[aider_var] = value
|
||||
provider = env_var.replace("_API_KEY", "").lower()
|
||||
self.status.log(f"Added {provider} API key")
|
||||
|
||||
# Handle additional API keys from AIDER_API_KEYS
|
||||
additional_keys = os.environ.get("AIDER_API_KEYS")
|
||||
if additional_keys:
|
||||
try:
|
||||
# Parse format: "provider1=key1,provider2=key2"
|
||||
for pair in additional_keys.split(","):
|
||||
if "=" in pair:
|
||||
provider, key = pair.strip().split("=", 1)
|
||||
env_var_name = f"{provider.upper()}_API_KEY"
|
||||
env_vars[env_var_name] = key
|
||||
self.status.log(f"Added {provider} API key from AIDER_API_KEYS")
|
||||
except Exception as e:
|
||||
self.status.log(f"Failed to parse AIDER_API_KEYS: {e}", "WARNING")
|
||||
|
||||
# Add model configuration
|
||||
model = os.environ.get("AIDER_MODEL")
|
||||
if model:
|
||||
env_vars["AIDER_MODEL"] = model
|
||||
self.status.log(f"Set default model to {model}")
|
||||
|
||||
# Add git configuration
|
||||
auto_commits = os.environ.get("AIDER_AUTO_COMMITS", "true")
|
||||
if auto_commits.lower() in ["true", "false"]:
|
||||
env_vars["AIDER_AUTO_COMMITS"] = auto_commits
|
||||
|
||||
# Add dark mode setting
|
||||
dark_mode = os.environ.get("AIDER_DARK_MODE", "false")
|
||||
if dark_mode.lower() in ["true", "false"]:
|
||||
env_vars["AIDER_DARK_MODE"] = dark_mode
|
||||
|
||||
# Add proxy settings
|
||||
for proxy_var in ["HTTP_PROXY", "HTTPS_PROXY"]:
|
||||
value = os.environ.get(proxy_var)
|
||||
if value:
|
||||
env_vars[proxy_var] = value
|
||||
self.status.log(f"Added proxy configuration: {proxy_var}")
|
||||
|
||||
return env_vars
|
||||
|
||||
def _write_env_file(self, env_file: Path, env_vars: Dict[str, str]) -> bool:
|
||||
"""Write environment variables to .env file"""
|
||||
try:
|
||||
content = "\n".join(f"{key}={value}" for key, value in env_vars.items())
|
||||
|
||||
with open(env_file, "w") as f:
|
||||
f.write(content)
|
||||
f.write("\n")
|
||||
|
||||
# Set ownership and secure file permissions (read/write for owner only)
|
||||
self._set_ownership(env_file)
|
||||
os.chmod(env_file, stat.S_IRUSR | stat.S_IWUSR)
|
||||
|
||||
self.status.log(f"Created Aider environment file at {env_file}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.status.log(f"Failed to write Aider environment file: {e}", "ERROR")
|
||||
return False
|
||||
|
||||
def setup_tool_configuration(self) -> bool:
|
||||
"""Set up Aider configuration - called by base class"""
|
||||
# Additional tool configuration can be added here if needed
|
||||
return True
|
||||
|
||||
def integrate_mcp_servers(self, mcp_config: Dict[str, Any]) -> bool:
|
||||
"""Integrate Aider with available MCP servers if applicable"""
|
||||
if mcp_config["count"] == 0:
|
||||
self.status.log("No MCP servers to integrate")
|
||||
return True
|
||||
|
||||
# Aider doesn't have native MCP support like Claude Code,
|
||||
# but we could potentially add custom integrations here
|
||||
self.status.log(
|
||||
f"Found {mcp_config['count']} MCP server(s) - no direct integration available"
|
||||
)
|
||||
return True
|
||||
88
cubbi/images/aider/cubbi_image.yaml
Normal file
88
cubbi/images/aider/cubbi_image.yaml
Normal file
@@ -0,0 +1,88 @@
|
||||
name: aider
|
||||
description: Aider AI pair programming environment
|
||||
version: 1.0.0
|
||||
maintainer: team@monadical.com
|
||||
image: monadical/cubbi-aider:latest
|
||||
|
||||
init:
|
||||
pre_command: /cubbi-init.sh
|
||||
command: /entrypoint.sh
|
||||
|
||||
environment:
|
||||
# OpenAI Configuration
|
||||
- name: OPENAI_API_KEY
|
||||
description: OpenAI API key for GPT models
|
||||
required: false
|
||||
sensitive: true
|
||||
|
||||
# Anthropic Configuration
|
||||
- name: ANTHROPIC_API_KEY
|
||||
description: Anthropic API key for Claude models
|
||||
required: false
|
||||
sensitive: true
|
||||
|
||||
# DeepSeek Configuration
|
||||
- name: DEEPSEEK_API_KEY
|
||||
description: DeepSeek API key for DeepSeek models
|
||||
required: false
|
||||
sensitive: true
|
||||
|
||||
# Gemini Configuration
|
||||
- name: GEMINI_API_KEY
|
||||
description: Google Gemini API key
|
||||
required: false
|
||||
sensitive: true
|
||||
|
||||
# OpenRouter Configuration
|
||||
- name: OPENROUTER_API_KEY
|
||||
description: OpenRouter API key for various models
|
||||
required: false
|
||||
sensitive: true
|
||||
|
||||
# Generic provider API keys
|
||||
- name: AIDER_API_KEYS
|
||||
description: Additional API keys in format "provider1=key1,provider2=key2"
|
||||
required: false
|
||||
sensitive: true
|
||||
|
||||
# Model Configuration
|
||||
- name: AIDER_MODEL
|
||||
description: Default model to use (e.g., sonnet, o3-mini, deepseek)
|
||||
required: false
|
||||
|
||||
# Git Configuration
|
||||
- name: AIDER_AUTO_COMMITS
|
||||
description: Enable automatic commits (true/false)
|
||||
required: false
|
||||
default: "true"
|
||||
|
||||
- name: AIDER_DARK_MODE
|
||||
description: Enable dark mode (true/false)
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
# Proxy Configuration
|
||||
- name: HTTP_PROXY
|
||||
description: HTTP proxy server URL
|
||||
required: false
|
||||
|
||||
- name: HTTPS_PROXY
|
||||
description: HTTPS proxy server URL
|
||||
required: false
|
||||
|
||||
ports: []
|
||||
|
||||
volumes:
|
||||
- mountPath: /app
|
||||
description: Application directory
|
||||
|
||||
persistent_configs:
|
||||
- source: "/home/cubbi/.aider"
|
||||
target: "/cubbi-config/aider-settings"
|
||||
type: "directory"
|
||||
description: "Aider configuration and history"
|
||||
|
||||
- source: "/home/cubbi/.cache/aider"
|
||||
target: "/cubbi-config/aider-cache"
|
||||
type: "directory"
|
||||
description: "Aider cache directory"
|
||||
273
cubbi/images/aider/test_aider.py
Executable file
273
cubbi/images/aider/test_aider.py
Executable file
@@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive test script for Aider Cubbi image
|
||||
Tests Docker image build, API key configuration, and Cubbi CLI integration
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def run_command(cmd, description="", check=True):
|
||||
"""Run a shell command and return result"""
|
||||
print(f"\n🔍 {description}")
|
||||
print(f"Running: {cmd}")
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, shell=True, capture_output=True, text=True, check=check
|
||||
)
|
||||
|
||||
if result.stdout:
|
||||
print("STDOUT:")
|
||||
print(result.stdout)
|
||||
|
||||
if result.stderr:
|
||||
print("STDERR:")
|
||||
print(result.stderr)
|
||||
|
||||
return result
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ Command failed with exit code {e.returncode}")
|
||||
if e.stdout:
|
||||
print("STDOUT:")
|
||||
print(e.stdout)
|
||||
if e.stderr:
|
||||
print("STDERR:")
|
||||
print(e.stderr)
|
||||
if check:
|
||||
raise
|
||||
return e
|
||||
|
||||
|
||||
def test_docker_image_exists():
|
||||
"""Test if the Aider Docker image exists"""
|
||||
print("\n" + "=" * 60)
|
||||
print("🧪 Testing Docker Image Existence")
|
||||
print("=" * 60)
|
||||
|
||||
result = run_command(
|
||||
"docker images monadical/cubbi-aider:latest --format 'table {{.Repository}}\t{{.Tag}}\t{{.Size}}'",
|
||||
"Checking if Aider Docker image exists",
|
||||
)
|
||||
|
||||
if "monadical/cubbi-aider" in result.stdout:
|
||||
print("✅ Aider Docker image exists")
|
||||
else:
|
||||
print("❌ Aider Docker image not found")
|
||||
assert False, "Aider Docker image not found"
|
||||
|
||||
|
||||
def test_aider_version():
|
||||
"""Test basic Aider functionality in container"""
|
||||
print("\n" + "=" * 60)
|
||||
print("🧪 Testing Aider Version")
|
||||
print("=" * 60)
|
||||
|
||||
result = run_command(
|
||||
"docker run --rm monadical/cubbi-aider:latest bash -c 'aider --version'",
|
||||
"Testing Aider version command",
|
||||
)
|
||||
|
||||
assert (
|
||||
"aider" in result.stdout and result.returncode == 0
|
||||
), "Aider version command failed"
|
||||
print("✅ Aider version command works")
|
||||
|
||||
|
||||
def test_api_key_configuration():
|
||||
"""Test API key configuration and environment setup"""
|
||||
print("\n" + "=" * 60)
|
||||
print("🧪 Testing API Key Configuration")
|
||||
print("=" * 60)
|
||||
|
||||
# Test with multiple API keys
|
||||
test_keys = {
|
||||
"OPENAI_API_KEY": "test-openai-key",
|
||||
"ANTHROPIC_API_KEY": "test-anthropic-key",
|
||||
"DEEPSEEK_API_KEY": "test-deepseek-key",
|
||||
"GEMINI_API_KEY": "test-gemini-key",
|
||||
"OPENROUTER_API_KEY": "test-openrouter-key",
|
||||
}
|
||||
|
||||
env_flags = " ".join([f'-e {key}="{value}"' for key, value in test_keys.items()])
|
||||
|
||||
result = run_command(
|
||||
f"docker run --rm {env_flags} monadical/cubbi-aider:latest bash -c 'cat ~/.aider/.env'",
|
||||
"Testing API key configuration in .env file",
|
||||
)
|
||||
|
||||
success = True
|
||||
for key, value in test_keys.items():
|
||||
if f"{key}={value}" not in result.stdout:
|
||||
print(f"❌ {key} not found in .env file")
|
||||
success = False
|
||||
else:
|
||||
print(f"✅ {key} configured correctly")
|
||||
|
||||
# Test default configuration values
|
||||
if "AIDER_AUTO_COMMITS=true" in result.stdout:
|
||||
print("✅ Default AIDER_AUTO_COMMITS configured")
|
||||
else:
|
||||
print("❌ Default AIDER_AUTO_COMMITS not found")
|
||||
success = False
|
||||
|
||||
if "AIDER_DARK_MODE=false" in result.stdout:
|
||||
print("✅ Default AIDER_DARK_MODE configured")
|
||||
else:
|
||||
print("❌ Default AIDER_DARK_MODE not found")
|
||||
success = False
|
||||
|
||||
assert success, "API key configuration test failed"
|
||||
|
||||
|
||||
def test_cubbi_cli_integration():
|
||||
"""Test Cubbi CLI integration"""
|
||||
print("\n" + "=" * 60)
|
||||
print("🧪 Testing Cubbi CLI Integration")
|
||||
print("=" * 60)
|
||||
|
||||
# Test image listing
|
||||
result = run_command(
|
||||
"uv run -m cubbi.cli image list | grep aider",
|
||||
"Testing Cubbi CLI can see Aider image",
|
||||
)
|
||||
|
||||
if "aider" in result.stdout and "Aider AI pair" in result.stdout:
|
||||
print("✅ Cubbi CLI can list Aider image")
|
||||
else:
|
||||
print("❌ Cubbi CLI cannot see Aider image")
|
||||
return False
|
||||
|
||||
# Test session creation with test command
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
test_env = {
|
||||
"OPENAI_API_KEY": "test-session-key",
|
||||
"ANTHROPIC_API_KEY": "test-anthropic-session-key",
|
||||
}
|
||||
|
||||
env_vars = " ".join([f"{k}={v}" for k, v in test_env.items()])
|
||||
|
||||
result = run_command(
|
||||
f"{env_vars} uv run -m cubbi.cli session create --image aider {temp_dir} --no-shell --run \"aider --version && echo 'Cubbi CLI test successful'\"",
|
||||
"Testing Cubbi CLI session creation with Aider",
|
||||
)
|
||||
|
||||
assert (
|
||||
result.returncode == 0
|
||||
and "aider 0.84.0" in result.stdout
|
||||
and "Cubbi CLI test successful" in result.stdout
|
||||
), "Cubbi CLI session creation failed"
|
||||
print("✅ Cubbi CLI session creation works")
|
||||
|
||||
|
||||
def test_persistent_configuration():
|
||||
"""Test persistent configuration directories"""
|
||||
print("\n" + "=" * 60)
|
||||
print("🧪 Testing Persistent Configuration")
|
||||
print("=" * 60)
|
||||
|
||||
# Test that persistent directories are created
|
||||
result = run_command(
|
||||
"docker run --rm -e OPENAI_API_KEY='test-key' monadical/cubbi-aider:latest bash -c 'ls -la /home/cubbi/.aider/ && ls -la /home/cubbi/.cache/'",
|
||||
"Testing persistent configuration directories",
|
||||
)
|
||||
|
||||
success = True
|
||||
|
||||
if ".env" in result.stdout:
|
||||
print("✅ .env file created in ~/.aider/")
|
||||
else:
|
||||
print("❌ .env file not found in ~/.aider/")
|
||||
success = False
|
||||
|
||||
if "aider" in result.stdout:
|
||||
print("✅ ~/.cache/aider directory exists")
|
||||
else:
|
||||
print("❌ ~/.cache/aider directory not found")
|
||||
success = False
|
||||
|
||||
assert success, "API key configuration test failed"
|
||||
|
||||
|
||||
def test_plugin_functionality():
|
||||
"""Test the Aider plugin functionality"""
|
||||
print("\n" + "=" * 60)
|
||||
print("🧪 Testing Plugin Functionality")
|
||||
print("=" * 60)
|
||||
|
||||
# Test plugin without API keys (should still work)
|
||||
result = run_command(
|
||||
"docker run --rm monadical/cubbi-aider:latest bash -c 'echo \"Plugin test without API keys\"'",
|
||||
"Testing plugin functionality without API keys",
|
||||
)
|
||||
|
||||
if "No API keys found - Aider will run without pre-configuration" in result.stdout:
|
||||
print("✅ Plugin handles missing API keys gracefully")
|
||||
else:
|
||||
# This might be in stderr or initialization might have changed
|
||||
print("ℹ️ Plugin API key handling test - check output above")
|
||||
|
||||
# Test plugin with API keys
|
||||
result = run_command(
|
||||
"docker run --rm -e OPENAI_API_KEY='test-plugin-key' monadical/cubbi-aider:latest bash -c 'echo \"Plugin test with API keys\"'",
|
||||
"Testing plugin functionality with API keys",
|
||||
)
|
||||
|
||||
if "Aider environment configured successfully" in result.stdout:
|
||||
print("✅ Plugin configures environment successfully")
|
||||
else:
|
||||
print("❌ Plugin environment configuration failed")
|
||||
assert False, "Plugin environment configuration failed"
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all tests"""
|
||||
print("🚀 Starting Aider Cubbi Image Tests")
|
||||
print("=" * 60)
|
||||
|
||||
tests = [
|
||||
("Docker Image Exists", test_docker_image_exists),
|
||||
("Aider Version", test_aider_version),
|
||||
("API Key Configuration", test_api_key_configuration),
|
||||
("Persistent Configuration", test_persistent_configuration),
|
||||
("Plugin Functionality", test_plugin_functionality),
|
||||
("Cubbi CLI Integration", test_cubbi_cli_integration),
|
||||
]
|
||||
|
||||
results = {}
|
||||
|
||||
for test_name, test_func in tests:
|
||||
try:
|
||||
test_func()
|
||||
results[test_name] = True
|
||||
except Exception as e:
|
||||
print(f"❌ Test '{test_name}' failed with exception: {e}")
|
||||
results[test_name] = False
|
||||
|
||||
# Print summary
|
||||
print("\n" + "=" * 60)
|
||||
print("📊 TEST SUMMARY")
|
||||
print("=" * 60)
|
||||
|
||||
total_tests = len(tests)
|
||||
passed_tests = sum(1 for result in results.values() if result)
|
||||
failed_tests = total_tests - passed_tests
|
||||
|
||||
for test_name, result in results.items():
|
||||
status = "✅ PASS" if result else "❌ FAIL"
|
||||
print(f"{status} {test_name}")
|
||||
|
||||
print(f"\nTotal: {total_tests} | Passed: {passed_tests} | Failed: {failed_tests}")
|
||||
|
||||
if failed_tests == 0:
|
||||
print("\n🎉 All tests passed! Aider image is ready for use.")
|
||||
return 0
|
||||
else:
|
||||
print(f"\n⚠️ {failed_tests} test(s) failed. Please check the output above.")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user