diff --git a/cubbi/images/gemini-cli/Dockerfile b/cubbi/images/gemini-cli/Dockerfile new file mode 100644 index 0000000..933f0d0 --- /dev/null +++ b/cubbi/images/gemini-cli/Dockerfile @@ -0,0 +1,68 @@ +FROM node:20-slim + +LABEL maintainer="team@monadical.com" +LABEL description="Google Gemini CLI 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 \ + python3 \ + python3-pip \ + && rm -rf /var/lib/apt/lists/* + +# Install uv (Python package manager) for cubbi_init.py compatibility +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 Gemini CLI globally +RUN npm install -g @google/gemini-cli + +# Verify installation +RUN gemini --version + +# Create app directory +WORKDIR /app + +# Copy initialization system +COPY cubbi_init.py /cubbi/cubbi_init.py +COPY gemini_cli_plugin.py /cubbi/gemini_cli_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 init status check to 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"] \ No newline at end of file diff --git a/cubbi/images/gemini-cli/README.md b/cubbi/images/gemini-cli/README.md new file mode 100644 index 0000000..faeeb84 --- /dev/null +++ b/cubbi/images/gemini-cli/README.md @@ -0,0 +1,339 @@ +# Google Gemini CLI for Cubbi + +This image provides Google Gemini CLI in a Cubbi container environment. + +## Overview + +Google Gemini CLI is an AI-powered development tool that allows you to query and edit large codebases, generate applications from PDFs/sketches, automate operational tasks, and integrate with media generation tools using Google's Gemini models. + +## Features + +- **Advanced AI Models**: Access to Gemini 1.5 Pro, Flash, and other Google AI models +- **Codebase Analysis**: Query and edit large codebases intelligently +- **Multi-modal Support**: Work with text, images, PDFs, and sketches +- **Google Search Grounding**: Ground queries using Google Search for up-to-date information +- **Secure Authentication**: API key management through Cubbi's secure environment system +- **Persistent Configuration**: Settings and history preserved across container restarts +- **Project Integration**: Seamless integration with existing projects + +## Quick Start + +### 1. Set up API Key + +```bash +# For Google AI (recommended) +uv run -m cubbi.cli config set services.google.api_key "your-gemini-api-key" + +# Alternative using GEMINI_API_KEY +uv run -m cubbi.cli config set services.gemini.api_key "your-gemini-api-key" +``` + +Get your API key from [Google AI Studio](https://aistudio.google.com/apikey). + +### 2. Run Gemini CLI Environment + +```bash +# Start Gemini CLI container with your project +uv run -m cubbi.cli session create --image gemini-cli /path/to/your/project + +# Or without a project +uv run -m cubbi.cli session create --image gemini-cli +``` + +### 3. Use Gemini CLI + +```bash +# Basic usage +gemini + +# Interactive mode with specific query +gemini +> Write me a Discord bot that answers questions using a FAQ.md file + +# Analyze existing project +gemini +> Give me a summary of all changes that went in yesterday + +# Generate from sketch/PDF +gemini +> Create a web app based on this wireframe.png +``` + +## Configuration + +### Supported API Keys + +- `GEMINI_API_KEY`: Google AI API key for Gemini models +- `GOOGLE_API_KEY`: Alternative Google API key (compatibility) +- `GOOGLE_APPLICATION_CREDENTIALS`: Path to Google Cloud service account JSON file + +### Model Configuration + +- `GEMINI_MODEL`: Default model (default: "gemini-1.5-pro") + - Available: "gemini-1.5-pro", "gemini-1.5-flash", "gemini-1.0-pro" +- `GEMINI_TEMPERATURE`: Model temperature 0.0-2.0 (default: 0.7) +- `GEMINI_MAX_TOKENS`: Maximum tokens in response + +### Advanced Configuration + +- `GEMINI_SEARCH_ENABLED`: Enable Google Search grounding (true/false, default: false) +- `GEMINI_DEBUG`: Enable debug mode (true/false, default: false) +- `GCLOUD_PROJECT`: Google Cloud project ID + +### Network Configuration + +- `HTTP_PROXY`: HTTP proxy server URL +- `HTTPS_PROXY`: HTTPS proxy server URL + +## Usage Examples + +### Basic AI Development + +```bash +# Start Gemini CLI with your project +uv run -m cubbi.cli session create --image gemini-cli /path/to/project + +# Inside the container: +gemini # Start interactive session +``` + +### Codebase Analysis + +```bash +# Analyze changes +gemini +> What are the main functions in src/main.py? + +# Code generation +gemini +> Add error handling to the authentication module + +# Documentation +gemini +> Generate README documentation for this project +``` + +### Multi-modal Development + +```bash +# Work with images +gemini +> Analyze this architecture diagram and suggest improvements + +# PDF processing +gemini +> Convert this API specification PDF to OpenAPI YAML + +# Sketch to code +gemini +> Create a React component based on this UI mockup +``` + +### Advanced Features + +```bash +# With Google Search grounding +uv run -m cubbi.cli session create --image gemini-cli \ + --env GEMINI_SEARCH_ENABLED="true" \ + /path/to/project + +# With specific model +uv run -m cubbi.cli session create --image gemini-cli \ + --env GEMINI_MODEL="gemini-1.5-flash" \ + --env GEMINI_TEMPERATURE="0.3" \ + /path/to/project + +# Debug mode +uv run -m cubbi.cli session create --image gemini-cli \ + --env GEMINI_DEBUG="true" \ + /path/to/project +``` + +### Enterprise/Proxy Setup + +```bash +# With proxy +uv run -m cubbi.cli session create --image gemini-cli \ + --env HTTPS_PROXY="https://proxy.company.com:8080" \ + /path/to/project + +# With Google Cloud authentication +uv run -m cubbi.cli session create --image gemini-cli \ + --env GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json" \ + --env GCLOUD_PROJECT="your-project-id" \ + /path/to/project +``` + +## Persistent Configuration + +The following directories are automatically persisted: + +- `~/.config/gemini/`: Gemini CLI configuration files +- `~/.cache/gemini/`: Model cache and temporary files + +Configuration files are maintained across container restarts, ensuring your preferences and session history are preserved. + +## Model Recommendations + +### Best Overall Performance +- **Gemini 1.5 Pro**: Excellent reasoning and code understanding +- **Gemini 1.5 Flash**: Faster responses, good for iterative development + +### Cost-Effective Options +- **Gemini 1.5 Flash**: Lower cost, high speed +- **Gemini 1.0 Pro**: Basic model for simple tasks + +### Specialized Use Cases +- **Code Analysis**: Gemini 1.5 Pro +- **Quick Iterations**: Gemini 1.5 Flash +- **Multi-modal Tasks**: Gemini 1.5 Pro (supports images, PDFs) + +## File Structure + +``` +cubbi/images/gemini-cli/ +├── Dockerfile # Container image definition +├── cubbi_image.yaml # Cubbi image configuration +├── gemini_plugin.py # Authentication and setup plugin +└── README.md # This documentation +``` + +## Authentication Flow + +1. **API Key Setup**: API key configured via Cubbi configuration or environment variables +2. **Plugin Initialization**: `gemini_plugin.py` creates configuration files +3. **Environment File**: Creates `~/.config/gemini/.env` with API key +4. **Configuration**: Creates `~/.config/gemini/config.json` with settings +5. **Ready**: Gemini CLI is ready for use with configured authentication + +## Troubleshooting + +### Common Issues + +**No API Key Found** +``` +ℹ️ No API key found - Gemini CLI will require authentication +``` +**Solution**: Set API key in Cubbi configuration: +```bash +uv run -m cubbi.cli config set services.google.api_key "your-key" +``` + +**Authentication Failed** +``` +Error: Invalid API key or authentication failed +``` +**Solution**: Verify your API key at [Google AI Studio](https://aistudio.google.com/apikey): +```bash +# Test your API key +curl -H "Content-Type: application/json" \ + -d '{"contents":[{"parts":[{"text":"Hello"}]}]}' \ + "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=YOUR_API_KEY" +``` + +**Model Not Available** +``` +Error: Model 'xyz' not found +``` +**Solution**: Use supported models: +```bash +# List available models (inside container) +curl -H "Content-Type: application/json" \ + "https://generativelanguage.googleapis.com/v1beta/models?key=YOUR_API_KEY" +``` + +**Rate Limit Exceeded** +``` +Error: Quota exceeded +``` +**Solution**: Google AI provides: +- 60 requests per minute +- 1,000 requests per day +- Consider upgrading to Google Cloud for higher limits + +**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 +# Enable debug output +uv run -m cubbi.cli session create --image gemini-cli \ + --env GEMINI_DEBUG="true" + +# Check configuration +cat ~/.config/gemini/config.json + +# Check environment +cat ~/.config/gemini/.env + +# Test CLI directly +gemini --help +``` + +## Security Considerations + +- **API Keys**: Stored securely with 0o600 permissions +- **Environment**: Isolated container environment +- **Configuration**: Secure file permissions for config files +- **Google Cloud**: Supports service account authentication for enterprise use + +## Advanced Configuration + +### Custom Model Configuration + +```bash +# Use specific model with custom settings +uv run -m cubbi.cli session create --image gemini-cli \ + --env GEMINI_MODEL="gemini-1.5-flash" \ + --env GEMINI_TEMPERATURE="0.2" \ + --env GEMINI_MAX_TOKENS="8192" +``` + +### Google Search Integration + +```bash +# Enable Google Search grounding for up-to-date information +uv run -m cubbi.cli session create --image gemini-cli \ + --env GEMINI_SEARCH_ENABLED="true" +``` + +### Google Cloud Integration + +```bash +# Use with Google Cloud service account +uv run -m cubbi.cli session create --image gemini-cli \ + --env GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json" \ + --env GCLOUD_PROJECT="your-project-id" +``` + +## API Limits and Pricing + +### Free Tier (Google AI) +- 60 requests per minute +- 1,000 requests per day +- Personal Google account required + +### Paid Tier (Google Cloud) +- Higher rate limits +- Enterprise features +- Service account authentication +- Custom quotas available + +## Support + +For issues related to: +- **Cubbi Integration**: Check Cubbi documentation or open an issue +- **Gemini CLI Functionality**: Visit [Gemini CLI documentation](https://github.com/google-gemini/gemini-cli) +- **Google AI Platform**: Visit [Google AI documentation](https://ai.google.dev/) +- **API Keys**: Visit [Google AI Studio](https://aistudio.google.com/) + +## License + +This image configuration is provided under the same license as the Cubbi project. Google Gemini CLI is licensed separately by Google. \ No newline at end of file diff --git a/cubbi/images/gemini-cli/cubbi_image.yaml b/cubbi/images/gemini-cli/cubbi_image.yaml new file mode 100644 index 0000000..aaefff4 --- /dev/null +++ b/cubbi/images/gemini-cli/cubbi_image.yaml @@ -0,0 +1,80 @@ +name: gemini-cli +description: Google Gemini CLI environment for AI-powered development +version: 1.0.0 +maintainer: team@monadical.com +image: monadical/cubbi-gemini-cli:latest + +environment: + # Google AI Configuration + - name: GEMINI_API_KEY + description: Google AI API key for Gemini models + required: false + sensitive: true + + - name: GOOGLE_API_KEY + description: Alternative Google API key (compatibility) + required: false + sensitive: true + + # Google Cloud Configuration + - name: GOOGLE_APPLICATION_CREDENTIALS + description: Path to Google Cloud service account JSON file + required: false + sensitive: true + + - name: GCLOUD_PROJECT + description: Google Cloud project ID + required: false + + # Model Configuration + - name: GEMINI_MODEL + description: Default Gemini model (e.g., gemini-1.5-pro, gemini-1.5-flash) + required: false + default: "gemini-1.5-pro" + + - name: GEMINI_TEMPERATURE + description: Model temperature (0.0-2.0) + required: false + default: "0.7" + + - name: GEMINI_MAX_TOKENS + description: Maximum tokens in response + required: false + + # Search Configuration + - name: GEMINI_SEARCH_ENABLED + description: Enable Google Search grounding (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 + + # Debug Configuration + - name: GEMINI_DEBUG + description: Enable debug mode (true/false) + required: false + default: "false" + +ports: [] + +volumes: + - mountPath: /app + description: Application directory + +persistent_configs: + - source: "/home/cubbi/.config/gemini" + target: "/cubbi-config/gemini-settings" + type: "directory" + description: "Gemini CLI configuration and history" + + - source: "/home/cubbi/.cache/gemini" + target: "/cubbi-config/gemini-cache" + type: "directory" + description: "Gemini CLI cache directory" \ No newline at end of file diff --git a/cubbi/images/gemini-cli/gemini_cli_plugin.py b/cubbi/images/gemini-cli/gemini_cli_plugin.py new file mode 100644 index 0000000..11cf858 --- /dev/null +++ b/cubbi/images/gemini-cli/gemini_cli_plugin.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +""" +Gemini CLI Plugin for Cubbi +Handles authentication setup and configuration for Google Gemini CLI +""" + +import json +import os +import stat +from pathlib import Path +from typing import Any, Dict + +from cubbi_init import ToolPlugin + + +class GeminiCliPlugin(ToolPlugin): + """Plugin for setting up Gemini CLI authentication and configuration""" + + @property + def tool_name(self) -> str: + return "gemini-cli" + + 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_gemini_config_dir(self) -> Path: + """Get the Gemini configuration directory""" + return Path("/home/cubbi/.config/gemini") + + def _get_gemini_cache_dir(self) -> Path: + """Get the Gemini cache directory""" + return Path("/home/cubbi/.cache/gemini") + + def _ensure_gemini_dirs(self) -> tuple[Path, Path]: + """Ensure Gemini directories exist with correct ownership""" + config_dir = self._get_gemini_config_dir() + cache_dir = self._get_gemini_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 Gemini directory {directory}: {e}", "ERROR" + ) + + return config_dir, cache_dir + + def initialize(self) -> bool: + """Initialize Gemini CLI configuration""" + self.status.log("Setting up Gemini CLI configuration...") + + # Ensure Gemini directories exist + config_dir, cache_dir = self._ensure_gemini_dirs() + + # Set up authentication and configuration + auth_configured = self._setup_authentication(config_dir) + config_created = self._create_configuration_file(config_dir) + + if auth_configured or config_created: + self.status.log("✅ Gemini CLI configured successfully") + else: + self.status.log( + "ℹ️ No API key found - Gemini CLI will require authentication", + "INFO", + ) + self.status.log( + " You can configure API keys using environment variables", "INFO" + ) + + # Always return True to allow container to start + return True + + def _setup_authentication(self, config_dir: Path) -> bool: + """Set up Gemini authentication""" + api_key = self._get_api_key() + + if not api_key: + return False + + # Create environment file for API key + env_file = config_dir / ".env" + try: + with open(env_file, "w") as f: + f.write(f"GEMINI_API_KEY={api_key}\n") + + # Set ownership and secure file permissions + self._set_ownership(env_file) + os.chmod(env_file, stat.S_IRUSR | stat.S_IWUSR) + + self.status.log(f"Created Gemini environment file at {env_file}") + self.status.log("Added Gemini API key") + return True + + except Exception as e: + self.status.log(f"Failed to create environment file: {e}", "ERROR") + return False + + def _get_api_key(self) -> str: + """Get the Gemini API key from environment variables""" + # Check multiple possible environment variable names + for key_name in ["GEMINI_API_KEY", "GOOGLE_API_KEY"]: + api_key = os.environ.get(key_name) + if api_key: + return api_key + return "" + + def _create_configuration_file(self, config_dir: Path) -> bool: + """Create Gemini CLI configuration file""" + try: + config = self._build_configuration() + + if not config: + return False + + config_file = config_dir / "config.json" + with open(config_file, "w") as f: + json.dump(config, f, indent=2) + + # Set ownership and permissions + self._set_ownership(config_file) + os.chmod(config_file, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) + + self.status.log(f"Created Gemini configuration at {config_file}") + return True + + except Exception as e: + self.status.log(f"Failed to create configuration file: {e}", "ERROR") + return False + + def _build_configuration(self) -> Dict[str, Any]: + """Build Gemini CLI configuration from environment variables""" + config = {} + + # Model configuration + model = os.environ.get("GEMINI_MODEL", "gemini-1.5-pro") + if model: + config["defaultModel"] = model + self.status.log(f"Set default model to {model}") + + # Temperature setting + temperature = os.environ.get("GEMINI_TEMPERATURE") + if temperature: + try: + temp_value = float(temperature) + if 0.0 <= temp_value <= 2.0: + config["temperature"] = temp_value + self.status.log(f"Set temperature to {temp_value}") + else: + self.status.log( + f"Invalid temperature value {temperature}, using default", + "WARNING", + ) + except ValueError: + self.status.log( + f"Invalid temperature format {temperature}, using default", + "WARNING", + ) + + # Max tokens setting + max_tokens = os.environ.get("GEMINI_MAX_TOKENS") + if max_tokens: + try: + tokens_value = int(max_tokens) + if tokens_value > 0: + config["maxTokens"] = tokens_value + self.status.log(f"Set max tokens to {tokens_value}") + else: + self.status.log( + f"Invalid max tokens value {max_tokens}, using default", + "WARNING", + ) + except ValueError: + self.status.log( + f"Invalid max tokens format {max_tokens}, using default", + "WARNING", + ) + + # Search configuration + search_enabled = os.environ.get("GEMINI_SEARCH_ENABLED", "false") + if search_enabled.lower() in ["true", "false"]: + config["searchEnabled"] = search_enabled.lower() == "true" + if config["searchEnabled"]: + self.status.log("Enabled Google Search grounding") + + # Debug mode + debug_mode = os.environ.get("GEMINI_DEBUG", "false") + if debug_mode.lower() in ["true", "false"]: + config["debug"] = debug_mode.lower() == "true" + if config["debug"]: + self.status.log("Enabled debug mode") + + # Proxy settings + for proxy_var in ["HTTP_PROXY", "HTTPS_PROXY"]: + proxy_value = os.environ.get(proxy_var) + if proxy_value: + config[proxy_var.lower()] = proxy_value + self.status.log(f"Added proxy configuration: {proxy_var}") + + # Google Cloud project + project = os.environ.get("GCLOUD_PROJECT") + if project: + config["project"] = project + self.status.log(f"Set Google Cloud project to {project}") + + return config + + def setup_tool_configuration(self) -> bool: + """Set up Gemini CLI 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 Gemini CLI with available MCP servers if applicable""" + if mcp_config["count"] == 0: + self.status.log("No MCP servers to integrate") + return True + + # Gemini CLI doesn't have native MCP support, + # 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 diff --git a/cubbi/images/gemini-cli/test_gemini.py b/cubbi/images/gemini-cli/test_gemini.py new file mode 100644 index 0000000..70f4c55 --- /dev/null +++ b/cubbi/images/gemini-cli/test_gemini.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +""" +Comprehensive test script for Gemini CLI 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_build(): + """Test Docker image build""" + print("\n" + "=" * 60) + print("🧪 Testing Docker Image Build") + print("=" * 60) + + result = run_command( + "cd /home/bouthilx/projects/cubbi/cubbi/images/gemini-cli && docker build -t monadical/cubbi-gemini-cli:latest .", + "Building Gemini CLI Docker image", + ) + + if result.returncode == 0: + print("✅ Gemini CLI Docker image built successfully") + return True + else: + print("❌ Gemini CLI Docker image build failed") + return False + + +def test_docker_image_exists(): + """Test if the Gemini CLI Docker image exists""" + print("\n" + "=" * 60) + print("🧪 Testing Docker Image Existence") + print("=" * 60) + + result = run_command( + "docker images monadical/cubbi-gemini-cli:latest --format 'table {{.Repository}}\t{{.Tag}}\t{{.Size}}'", + "Checking if Gemini CLI Docker image exists", + ) + + if "monadical/cubbi-gemini-cli" in result.stdout: + print("✅ Gemini CLI Docker image exists") + return True + else: + print("❌ Gemini CLI Docker image not found") + return False + + +def test_gemini_version(): + """Test basic Gemini CLI functionality in container""" + print("\n" + "=" * 60) + print("🧪 Testing Gemini CLI Version") + print("=" * 60) + + result = run_command( + "docker run --rm monadical/cubbi-gemini-cli:latest bash -c 'gemini --version'", + "Testing Gemini CLI version command", + ) + + if result.returncode == 0 and ( + "gemini" in result.stdout.lower() or "version" in result.stdout.lower() + ): + print("✅ Gemini CLI version command works") + return True + else: + print("❌ Gemini CLI version command failed") + return False + + +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 = { + "GEMINI_API_KEY": "test-gemini-key", + "GOOGLE_API_KEY": "test-google-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-gemini-cli:latest bash -c 'cat ~/.config/gemini/.env 2>/dev/null || echo \"No .env file found\"'", + "Testing API key configuration in .env file", + ) + + success = True + if "test-gemini-key" in result.stdout: + print("✅ GEMINI_API_KEY configured correctly") + else: + print("❌ GEMINI_API_KEY not found in configuration") + success = False + + return success + + +def test_configuration_file(): + """Test Gemini CLI configuration file creation""" + print("\n" + "=" * 60) + print("🧪 Testing Configuration File") + print("=" * 60) + + env_vars = "-e GEMINI_API_KEY='test-key' -e GEMINI_MODEL='gemini-1.5-pro' -e GEMINI_TEMPERATURE='0.5'" + + result = run_command( + f"docker run --rm {env_vars} monadical/cubbi-gemini-cli:latest bash -c 'cat ~/.config/gemini/config.json 2>/dev/null || echo \"No config file found\"'", + "Testing configuration file creation", + ) + + success = True + if "gemini-1.5-pro" in result.stdout: + print("✅ Default model configured correctly") + else: + print("❌ Default model not found in configuration") + success = False + + if "0.5" in result.stdout: + print("✅ Temperature configured correctly") + else: + print("❌ Temperature not found in configuration") + success = False + + return success + + +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 gemini-cli", + "Testing Cubbi CLI can see Gemini CLI image", + ) + + if "gemini-cli" in result.stdout: + print("✅ Cubbi CLI can list Gemini CLI image") + else: + print("❌ Cubbi CLI cannot see Gemini CLI image") + return False + + # Test session creation with test command + with tempfile.TemporaryDirectory() as temp_dir: + test_env = { + "GEMINI_API_KEY": "test-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 gemini-cli {temp_dir} --no-shell --run \"gemini --version && echo 'Cubbi CLI test successful'\"", + "Testing Cubbi CLI session creation with Gemini CLI", + ) + + if result.returncode == 0 and "Cubbi CLI test successful" in result.stdout: + print("✅ Cubbi CLI session creation works") + return True + else: + print("❌ Cubbi CLI session creation failed") + return False + + +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 GEMINI_API_KEY='test-key' monadical/cubbi-gemini-cli:latest bash -c 'ls -la /home/cubbi/.config/ && ls -la /home/cubbi/.cache/'", + "Testing persistent configuration directories", + ) + + success = True + + if "gemini" in result.stdout: + print("✅ ~/.config/gemini directory exists") + else: + print("❌ ~/.config/gemini directory not found") + success = False + + if "gemini" in result.stdout: + print("✅ ~/.cache/gemini directory exists") + else: + print("❌ ~/.cache/gemini directory not found") + success = False + + return success + + +def test_plugin_functionality(): + """Test the Gemini CLI 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-gemini-cli:latest bash -c 'echo \"Plugin test without API keys\"'", + "Testing plugin functionality without API keys", + ) + + if "No API key found - Gemini CLI will require authentication" in result.stdout: + print("✅ Plugin handles missing API keys gracefully") + else: + print("ℹ️ Plugin API key handling test - check output above") + + # Test plugin with API keys + result = run_command( + "docker run --rm -e GEMINI_API_KEY='test-plugin-key' monadical/cubbi-gemini-cli:latest bash -c 'echo \"Plugin test with API keys\"'", + "Testing plugin functionality with API keys", + ) + + if "Gemini CLI configured successfully" in result.stdout: + print("✅ Plugin configures environment successfully") + return True + else: + print("❌ Plugin environment configuration failed") + return False + + +def main(): + """Run all tests""" + print("🚀 Starting Gemini CLI Cubbi Image Tests") + print("=" * 60) + + tests = [ + ("Docker Image Build", test_docker_build), + ("Docker Image Exists", test_docker_image_exists), + ("Gemini CLI Version", test_gemini_version), + ("API Key Configuration", test_api_key_configuration), + ("Configuration File", test_configuration_file), + ("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: + results[test_name] = test_func() + 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! Gemini CLI 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())