"""
VelozCLI - Command-line tool for managing Veloz CMS deployments

Usage:
    veloz login               - Authenticate with Google OAuth
    veloz logout              - Sign out and revoke token
    veloz list                - List your CMS deployments  
    veloz connect <cms>       - Connect to a specific CMS
    veloz status              - Show auth and connection status
    veloz logs                - Stream real-time logs from connected CMS
    veloz init <name>         - Initialize a plugin
    veloz push                - Upload plugin to connected CMS
    veloz pull                - Download plugin from CMS
    veloz migrate             - Generate Django migrations locally
"""

import typer
import webbrowser
import time
import os
import sys
import zipfile
import io
import ast
from pathlib import Path
from datetime import datetime
from typing import Optional, Dict, Any
from rich.console import Console
from rich.table import Table
from rich.progress import Progress, SpinnerColumn, TextColumn
from rich import print as rprint

from . import config
from .http_client import APIClient, APIError

# Fix Windows console encoding for emojis
if sys.platform == "win32":
    try:
        # Try to set UTF-8 encoding
        sys.stdout.reconfigure(encoding='utf-8')
        sys.stderr.reconfigure(encoding='utf-8')
    except AttributeError:
        # Python < 3.7 fallback
        import codecs
        sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict')
        sys.stderr = codecs.getwriter('utf-8')(sys.stderr.buffer, 'strict')
    
    # Set console code page to UTF-8
    os.system('chcp 65001 > nul 2>&1')

app = typer.Typer(help="VelozCLI - Manage your Veloz CMS deployments")
console = Console()


# ═══════════════════════════════════════════════════════════════════════════
# HELPER FUNCTIONS
# ═══════════════════════════════════════════════════════════════════════════

def extract_plugin_info(apps_py_path: Path) -> Optional[Dict[str, Any]]:
    """
    Extract plugin metadata from apps.py without executing it.
    
    Handles two patterns:
    1. Veloz SDK: plugin.init(name="demo_plugin", version="1.0.0", ...)
    2. Django AppConfig: class XConfig(AppConfig): name = "fix_my_geo_v0"
    
    Returns:
        Dict with 'name', 'version' (optional), 'display_name' (optional)
        or None if parsing fails
    """
    try:
        with open(apps_py_path, 'r', encoding='utf-8') as f:
            tree = ast.parse(f.read())
    except Exception as e:
        console.print(f"[yellow]⚠️  Could not parse apps.py: {e}[/yellow]")
        return None
    
    info = {}
    
    # Pattern 1: *.init(name="...", version="...", ...)
    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            # Check if it's calling .init()
            if isinstance(node.func, ast.Attribute) and node.func.attr == 'init':
                # Extract all keyword arguments
                for kw in node.keywords:
                    if kw.arg in ('name', 'version', 'display_name', 'description'):
                        try:
                            info[kw.arg] = ast.literal_eval(kw.value)
                        except:
                            pass
                
                if 'name' in info:
                    return info
    
    # Pattern 2: class X(AppConfig): name = "..."
    for node in ast.walk(tree):
        if isinstance(node, ast.ClassDef):
            # Check if it inherits from AppConfig
            inherits_appconfig = False
            for base in node.bases:
                if isinstance(base, ast.Name) and base.id == 'AppConfig':
                    inherits_appconfig = True
                    break
            
            if inherits_appconfig:
                # Find name = "..." assignment in class body
                for item in node.body:
                    if isinstance(item, ast.Assign):
                        for target in item.targets:
                            if isinstance(target, ast.Name) and target.id == 'name':
                                try:
                                    info['name'] = ast.literal_eval(item.value)
                                    return info
                                except:
                                    pass
    
    return None if not info else info


# ═══════════════════════════════════════════════════════════════════════════
# AUTHENTICATION COMMANDS
# ═══════════════════════════════════════════════════════════════════════════

@app.command()
def login(refresh: bool = typer.Option(False, "--refresh", help="Force refresh existing token")):
    """Authenticate with Google OAuth"""
    console.print("\n[bold blue]🔐 VelozCLI Authentication[/bold blue]\n")
    
    client = APIClient()
    
    try:
        # Start device flow
        status, data = client.post('/api/cli/auth/device-code/', authenticated=False)
        
        if status != 200:
            console.print(f"[red]✗ Failed to start authentication: {data.get('error', 'Unknown error')}[/red]")
            raise typer.Exit(1)
        
        device_code = data['device_code']
        verification_url = data['verification_url']
        
        console.print(f"[yellow]Press Enter to open browser for authentication...[/yellow]")
        input()
        
        # Open browser
        webbrowser.open(verification_url)
        console.print(f"[dim]Opened: {verification_url}[/dim]\n")
        
        # Poll for completion
        console.print("[yellow]⏳ Waiting for authentication...[/yellow]")
        
        max_attempts = 120  # 10 minutes
        for attempt in range(max_attempts):
            time.sleep(5)
            
            status, data = client.get(f'/api/cli/auth/poll/?code={device_code}', authenticated=False)
            
            if data.get('status') == 'completed':
                # Save credentials
                config.save_credentials(
                    token=data['token'],
                    email=data['email'],
                    expires=data['expires_at'],
                    server=client.server_url
                )
                
                console.print(f"\n[green]✓ Logged in as {data['email']}[/green]")
                
                synced = data.get('synced_tenants', 0)
                if synced > 0:
                    console.print(f"[green]✓ Token synced to {synced} CMS instance(s)[/green]")
                else:
                    console.print(f"[dim]  (No CMS deployments yet)[/dim]")
                
                console.print(f"\n[dim]Token expires: {data['expires_at'][:10]}[/dim]\n")
                return
            
            elif data.get('status') == 'expired':
                console.print("[red]✗ Authentication timeout. Please try again.[/red]")
                raise typer.Exit(1)
        
        console.print("[red]✗ Authentication timeout[/red]")
        raise typer.Exit(1)
    
    except APIError as e:
        console.print(f"[red]✗ {e}[/red]")
        raise typer.Exit(1)


@app.command()
def logout():
    """Sign out and revoke token"""
    console.print("\n[bold blue]🚪 Logging out...[/bold blue]\n")
    
    creds = config.load_credentials()
    if not creds:
        console.print("[yellow]Not logged in[/yellow]")
        return
    
    client = APIClient()
    
    try:
        status, data = client.delete('/api/cli/auth/revoke/')
        
        if status == 200:
            deleted = data.get('deleted_from_tenants', 0)
            console.print(f"[green]✓ Logged out successfully[/green]")
            if deleted > 0:
                console.print(f"[green]✓ Token revoked from {deleted} CMS instance(s)[/green]")
        else:
            console.print(f"[yellow]⚠ Token revocation may have failed[/yellow]")
        
        # Delete local credentials
        config.delete_credentials()
        console.print()
    
    except APIError as e:
        console.print(f"[yellow]⚠ {e}[/yellow]")
        # Delete local credentials anyway
        config.delete_credentials()


@app.command()
def status():
    """Show authentication and connection status"""
    console.print("\n[bold blue]📊 VelozCLI Status[/bold blue]\n")
    
    # Authentication status
    creds = config.load_credentials()
    if creds:
        # Decode expiration
        from datetime import datetime, timezone as tz
        expires_dt = datetime.fromisoformat(creds['expires'].replace('Z', '+00:00'))
        days_left = (expires_dt - datetime.now(tz.utc)).days
        
        console.print(f"[green]✓ Authenticated[/green]")
        console.print(f"  Email: {creds['email']}")
        console.print(f"  Expires: {expires_dt.strftime('%Y-%m-%d')} ({days_left} days remaining)")
        console.print(f"  Server: {creds['server']}\n")
    else:
        console.print("[yellow]✗ Not logged in[/yellow]")
        console.print("[dim]  Run `veloz login` to authenticate[/dim]\n")
        return
    
    # Connection status
    cms = config.get_connected_cms()
    if cms:
        console.print(f"[green]✓ Connected to: {cms}[/green]\n")
    else:
        console.print("[yellow]○ Not connected to any CMS[/yellow]")
        console.print("[dim]  Run `veloz list` and `veloz connect <cms>` [/dim]\n")


# ═══════════════════════════════════════════════════════════════════════════
# DISCOVERY COMMANDS
# ═══════════════════════════════════════════════════════════════════════════

@app.command()
def list():
    """List all CMS deployments"""
    console.print("\n[bold blue]📋 Your CMS Deployments[/bold blue]\n")
    
    client = APIClient()
    
    try:
        status, data = client.get('/api/cli/tenants/')
        
        if status != 200:
            console.print(f"[red]✗ {data.get('error', 'Failed to fetch tenants')}[/red]")
            raise typer.Exit(1)
        
        tenants = data.get('tenants', [])
        
        if not tenants:
            console.print("[yellow]No CMS deployments found[/yellow]")
            console.print("[dim]Deploy your first CMS at https://veloz.site[/dim]\n")
            return
        
        for tenant in tenants:
            status_icon = "✓" if tenant['status'] == 'running' else "○"
            status_color = "green" if tenant['status'] == 'running' else "yellow"
            
            console.print(f"  [{status_color}]{status_icon}[/{status_color}] [bold]{tenant['fly_app_name']}[/bold]")
            console.print(f"     URL: {tenant['fly_url']}")
            console.print(f"     Created: {tenant['created_at'][:10]}")
            console.print()
        
        console.print(f"[dim]Total: {len(tenants)} deployment(s)[/dim]\n")
    
    except APIError as e:
        console.print(f"[red]✗ {e}[/red]")
        raise typer.Exit(1)


@app.command()
def connect(cms_name: str):
    """Connect to a specific CMS"""
    console.print(f"\n[bold blue]🔗 Connecting to {cms_name}...[/bold blue]\n")
    
    client = APIClient()
    
    try:
        # Verify CMS exists
        status, data = client.get('/api/cli/tenants/')
        
        if status != 200:
            console.print(f"[red]✗ Failed to fetch tenants[/red]")
            raise typer.Exit(1)
        
        tenants = data.get('tenants', [])
        tenant_names = [t['fly_app_name'] for t in tenants]
        
        if cms_name not in tenant_names:
            console.print(f"[red]✗ CMS '{cms_name}' not found or no access[/red]")
            console.print(f"[dim]Run `veloz list` to see available CMS[/dim]\n")
            raise typer.Exit(1)
        
        # Find URL
        cms_url = next((t['fly_url'] for t in tenants if t['fly_app_name'] == cms_name), 'N/A')
        
        # Save connection
        config.set_connected_cms(cms_name, cms_url)
        
        console.print(f"[green]✓ Connected to {cms_name}[/green]")
        console.print(f"[dim]  URL: {cms_url}[/dim]\n")
    
    except APIError as e:
        console.print(f"[red]✗ {e}[/red]")
        raise typer.Exit(1)


# ═══════════════════════════════════════════════════════════════════════════
# PLUGIN COMMANDS
# ═══════════════════════════════════════════════════════════════════════════

def sanitize_plugin_name(name: str) -> str:
    """
    Sanitize plugin name to be a valid Python module name.
    Converts hyphens to underscores and removes invalid characters.
    """
    import re
    # Replace hyphens and spaces with underscores
    sanitized = name.replace('-', '_').replace(' ', '_')
    # Remove any characters that aren't alphanumeric or underscore
    sanitized = re.sub(r'[^a-zA-Z0-9_]', '', sanitized)
    # Ensure it doesn't start with a number
    if sanitized and sanitized[0].isdigit():
        sanitized = '_' + sanitized
    # Convert to lowercase for consistency
    sanitized = sanitized.lower()
    return sanitized


@app.command()
def init(plugin_name: str):
    """Initialize a plugin (pull existing or create new)"""
    # Sanitize plugin name
    original_name = plugin_name
    plugin_name = sanitize_plugin_name(plugin_name)
    
    if original_name != plugin_name:
        console.print(f"\n[yellow]⚠️  Plugin name '{original_name}' contains invalid characters[/yellow]")
        console.print(f"[yellow]    Auto-converted to: '{plugin_name}'[/yellow]")
        console.print(f"[dim]    Python modules can only contain: letters, numbers, underscores[/dim]\n")
    
    console.print(f"\n[bold blue]🔧 Initializing plugin '{plugin_name}'...[/bold blue]\n")
    
    cms = config.get_connected_cms()
    if not cms:
        console.print("[red]✗ Not connected to any CMS[/red]")
        console.print("[dim]Run `veloz list` and `veloz connect <cms>` first[/dim]\n")
        raise typer.Exit(1)
    
    client = APIClient()
    
    try:
        # Check if plugin exists
        status, data = client.get(f'/api/cli/plugins/{plugin_name}/info/')
        
        if status == 200:
            # Plugin exists - pull it
            console.print(f"[yellow]Plugin '{plugin_name}' exists on {cms}. Pulling...[/yellow]\n")
            pull_plugin(plugin_name)
            return
        
        # Plugin doesn't exist - create new
        console.print(f"[yellow]Plugin '{plugin_name}' doesn't exist on your CMS[/yellow]\n")
        
        console.print("Pick a template to create a new plugin:")
        console.print("  1. Django App Template")
        console.print("  2. Veloz SDK Template")
        
        choice = input("\nEnter 1 or 2: ").strip()
        
        if choice == '1':
            create_django_template(plugin_name)
        elif choice == '2':
            create_sdk_template(plugin_name)
        else:
            console.print("[red]✗ Invalid choice[/red]")
            raise typer.Exit(1)
        
        console.print(f"\n[green]✓ Plugin '{plugin_name}' created[/green]")
        console.print(f"[dim]Edit your plugin, then run `veloz push` to deploy[/dim]\n")
    
    except APIError as e:
        console.print(f"[red]✗ {e}[/red]")
        raise typer.Exit(1)


def snake_to_pascal(name: str) -> str:
    """Convert snake_case to PascalCase for class names"""
    return ''.join(word.capitalize() for word in name.split('_'))


def create_django_template(name: str):
    """Create Django app template"""
    path = Path(name)
    path.mkdir(exist_ok=True)
    
    class_name = snake_to_pascal(name)
    display_name = name.replace('_', ' ').title()
    
    (path / "__init__.py").write_text("", encoding='utf-8')
    
    # Create apps.py with URL hooking
    (path / "apps.py").write_text(f"""from django.apps import AppConfig


class {class_name}Config(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = '{name}'
    verbose_name = '{display_name}'

    def ready(self) -> None:
        \"\"\"Hook this plugin's URLs into the main URL configuration.\"\"\"
        from django.urls import include, path
        import flowpress_dashboard.urls as project_urls

        # Only hook once (prevents duplicate registrations on reload)
        if not getattr(project_urls, "_{name}_hooked", False):
            print("[{display_name}] Registering URLs")
            include_pattern = path("", include("{name}.urls"))
            # Insert at beginning for higher priority
            project_urls.urlpatterns.insert(0, include_pattern)
            project_urls._{name}_hooked = True
""", encoding='utf-8')
    
    # Create models.py
    (path / "models.py").write_text("from django.db import models\n\n# Create your models here.\n", encoding='utf-8')
    
    # Create views.py with example view (using string concatenation to avoid triple-quote PostgreSQL corruption)
    (path / "views.py").write_text(f"""from django.shortcuts import render
from django.http import HttpResponse


def hello_world(request):
    \"\"\"Example view - shows a simple hello world page\"\"\"
    html = "<html>"
    html += "<head><title>{display_name}</title>"
    html += "<style>"
    html += "body {{ font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; background: #f5f5f5; }}"
    html += ".card {{ background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}"
    html += "h1 {{ color: #2563eb; }}"
    html += "code {{ background: #f1f5f9; padding: 2px 6px; border-radius: 3px; font-family: monospace; }}"
    html += "</style></head>"
    html += "<body><div class='card'>"
    html += "<h1>🎉 {display_name}</h1>"
    html += "<p>Congratulations! Your plugin is installed and working.</p>"
    html += "<p><strong>Plugin:</strong> {name}</p>"
    html += "<hr><h3>Next Steps:</h3><ul>"
    html += "<li>Edit <code>views.py</code> to add your logic</li>"
    html += "<li>Update <code>urls.py</code> to add more routes</li>"
    html += "<li>Create models in <code>models.py</code></li>"
    html += "<li>Run <code>veloz push</code> to deploy changes</li>"
    html += "</ul></div></body></html>"
    return HttpResponse(html)
""", encoding='utf-8')
    
    # Create urls.py with example route
    (path / "urls.py").write_text(f"""from django.urls import path
from . import views

urlpatterns = [
    path('{name}/', views.hello_world, name='{name}_hello'),
]
""", encoding='utf-8')
    
    # Create forms.py
    (path / "forms.py").write_text("from django import forms\n\n# Create your forms here.\n", encoding='utf-8')
    
    # Create requirements.txt
    (path / "requirements.txt").write_text("", encoding='utf-8')
    
    # Create README.md with better instructions
    (path / "README.md").write_text(f"""# {display_name}

Django App Plugin for Veloz

## Structure

- `apps.py` - Plugin configuration with URL hooking
- `models.py` - Database models
- `views.py` - View functions
- `urls.py` - URL patterns
- `forms.py` - Django forms
- `templates/{name}/` - HTML templates

## URLs

This plugin registers its URLs automatically via the `ready()` method in `apps.py`.

Main URL: `http://localhost:8000/{name}/`

## Development

1. Edit your code
2. Run: `veloz push` to deploy
3. Restart your CMS server
4. Visit: `http://localhost:8000/{name}/`

## Notes

- The `ready()` method in `apps.py` hooks URLs into the main Django project
- URLs are inserted at the beginning for priority
- Plugin only hooks once (prevents duplicates on reload)
""", encoding='utf-8')
    
    (path / "migrations").mkdir(exist_ok=True)
    (path / "migrations" / "__init__.py").write_text("", encoding='utf-8')
    
    (path / "templates" / name).mkdir(parents=True, exist_ok=True)
    
    console.print(f"[green]✓ Created Django app template in ./{name}/[/green]")


def create_sdk_template(name: str):
    """Create Veloz SDK template with Hello World example"""
    path = Path(name)
    path.mkdir(exist_ok=True)
    
    # Create __init__.py
    (path / "__init__.py").write_text("", encoding='utf-8')
    
    # Create apps.py with full SDK example
    (path / "apps.py").write_text(f'''"""
{name.replace('_', ' ').title()} - Veloz SDK Hello World Plugin

This is a minimal example showing how to use the Veloz SDK.
See VELOZ_SDK_GUIDE.md for complete documentation.
"""

from django.apps import AppConfig
from django.shortcuts import render
from django.http import JsonResponse

# Import SDK decorators
from dashboard.veloz_sdk import plugin, route, menu

# ============================================================================
# 1. PLUGIN INITIALIZATION
# ============================================================================

plugin.init(
    name="{name}",
    version="1.0.0",
    display_name="{name.replace('_', ' ').title()}",
    description="A Hello World plugin demonstrating Veloz SDK basics"
)


# ============================================================================
# 2. ROUTES & VIEWS
# ============================================================================

@route("/")
@menu("{name.replace('_', ' ').title()}", icon="hand")
def index_view(request):
    """Main plugin page (staff only, with sidebar)"""
    context = {{
        "title": "{name.replace('_', ' ').title()}",
        "message": "Hello World from the Veloz SDK!",
        "tips": [
            "This page is only visible to staff members",
            "Add @route() to create new URLs",
            "Use @menu() to add items to the sidebar",
            "See VELOZ_SDK_GUIDE.md for more features",
        ]
    }}
    return render(request, "{name}/index.html", context)


@route("/public", permission=None)
def public_view(request):
    """Public page (no login required, no sidebar)"""
    context = {{
        "title": "Public Hello World",
        "message": "This page is accessible to everyone!",
    }}
    return render(request, "{name}/public.html", context)


@route("/api/hello")
def api_view(request):
    """Example API endpoint"""
    return JsonResponse({{
        "message": "Hello from the API!",
        "plugin": "{name}",
        "version": "1.0.0"
    }})


# ============================================================================
# 3. APP CONFIG (Required - Must be at bottom)
# ============================================================================

class {name.title().replace('_', '')}Config(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = '{name}'
    verbose_name = '{name.replace('_', ' ').title()}'
    
    def ready(self):
        """Called when Django loads the plugin"""
        plugin.register_urls("{name.replace('_', '-')}")  # URL prefix: /{name.replace('_', '-')}/
        plugin.finalize()  # Register all routes and menus
        
        print(f"[{name}] Loaded successfully!")
''', encoding='utf-8')
    
    # Create models.py
    (path / "models.py").write_text('''"""
Database models for your plugin.

Example:
from django.db import models

class MyModel(models.Model):
    name = models.CharField(max_length=200)
    created_at = models.DateTimeField(auto_now_add=True)
"""

from django.db import models

# Add your models here
''', encoding='utf-8')
    
    # Create forms.py
    (path / "forms.py").write_text('''"""
Django forms for your plugin.

Example:
from django import forms

class MyForm(forms.Form):
    name = forms.CharField(max_length=200)
"""

from django import forms

# Add your forms here
''', encoding='utf-8')
    
    # Create migrations folder with initial empty migration
    (path / "migrations").mkdir(exist_ok=True)
    (path / "migrations" / "__init__.py").write_text("", encoding='utf-8')
    
    # Create initial empty migration (prevents auto-setup restart loop)
    (path / "migrations" / "0001_initial.py").write_text('''# Generated by Veloz CLI

from django.db import migrations


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
    ]
''', encoding='utf-8')
    
    # Create templates directory structure
    templates_dir = path / "templates" / name
    templates_dir.mkdir(parents=True, exist_ok=True)
    
    # Create index.html (staff page with sidebar)
    (templates_dir / "index.html").write_text(f'''{{%extends "dashboard/index.html"%}}

{{%block admin_content%}}
<div class="container mx-auto p-6">
    <div class="bg-base-100 rounded-lg shadow-lg p-8">
        <h1 class="text-3xl font-bold mb-4">{{{{title}}}}</h1>
        
        <div class="alert alert-success mb-6">
            <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
            <span>{{{{message}}}}</span>
        </div>
        
        <div class="space-y-4">
            <h2 class="text-xl font-semibold">Quick Tips:</h2>
            <ul class="list-disc list-inside space-y-2">
                {{%for tip in tips%}}
                <li class="text-base-content/70">{{{{tip}}}}</li>
                {{%endfor%}}
            </ul>
        </div>
        
        <div class="mt-8 space-x-4">
            <a href="/{name.replace('_', '-')}/public/" class="btn btn-primary">View Public Page</a>
            <a href="/{name.replace('_', '-')}/api/hello/" class="btn btn-secondary">Test API</a>
        </div>
    </div>
</div>
{{%endblock%}}
''', encoding='utf-8')
    
    # Create public.html (public page without sidebar)
    (templates_dir / "public.html").write_text(f'''<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{{{title}}}}</title>
    <link href="https://cdn.jsdelivr.net/npm/daisyui@4.4.19/dist/full.min.css" rel="stylesheet">
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
    <div class="min-h-screen bg-gradient-to-br from-primary/10 to-secondary/10 flex items-center justify-center p-4">
        <div class="card w-full max-w-2xl bg-base-100 shadow-2xl">
            <div class="card-body">
                <h1 class="card-title text-4xl justify-center mb-4">{{{{title}}}}</h1>
                
                <div class="alert alert-info">
                    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
                    <span>{{{{message}}}}</span>
                </div>
                
                <div class="divider"></div>
                
                <div class="space-y-4">
                    <p class="text-center text-base-content/70">
                        This is a public page created with the Veloz SDK.<br>
                        No authentication required!
                    </p>
                    
                    <div class="text-sm bg-base-200 p-4 rounded-lg">
                        <p class="font-mono">
                            <strong>Route:</strong> @route("/public", permission=None)
                        </p>
                    </div>
                </div>
                
                <div class="card-actions justify-center mt-6">
                    <a href="/{name.replace('_', '-')}/" class="btn btn-primary">Go to Admin Page</a>
                </div>
            </div>
        </div>
    </div>
</body>
</html>
''', encoding='utf-8')
    
    # Create requirements.txt
    (path / "requirements.txt").write_text("# Add your Python dependencies here\n", encoding='utf-8')
    
    # Create README.md
    (path / "README.md").write_text(f'''# {name.replace('_', ' ').title()}

A Hello World plugin demonstrating Veloz SDK basics.

## Features

- Staff-only admin page with sidebar
- Public page (no authentication)
- API endpoint example
- Menu integration

## URLs

- `/{name.replace('_', '-')}/` - Main page (staff only)
- `/{name.replace('_', '-')}/public/` - Public page
- `/{name.replace('_', '-')}/api/hello/` - API endpoint

## Development

1. Edit `apps.py` to add more routes
2. Run `veloz migrate` to generate migrations (if you add models)
3. Run `veloz push` to deploy

## Documentation

See `VELOZ_SDK_GUIDE.md` for complete SDK documentation.
''', encoding='utf-8')
    
    # Create SDK guide
    (path / "VELOZ_SDK_GUIDE.md").write_text('''# Veloz SDK - Plugin Development Guide

## Overview
The Veloz SDK simplifies plugin development by abstracting Django app registration, AI agent integration, URL routing, and admin menus into simple decorators.

## Plugin Structure
```
PLUGINS/your_plugin/
├── __init__.py
├── apps.py          # Main plugin file (SDK decorators here)
├── models.py        # Standard Django models
├── forms.py         # Standard Django forms (optional)
├── templates/       # Templates (optional)
│   └── your_plugin/
│       ├── base.html         # Extends "dashboard/index.html"
│       └── your_views.html
└── migrations/      # Auto-generated by Django
```

## Core Concepts

### 1. Plugin Initialization
```python
from dashboard.veloz_sdk import plugin, function_tool, agent, route, menu, ai, log

plugin.init(
    name="My Plugin",
    version="1.0.0",
    description="Plugin description"
)
```

### 2. AI Tools (for Manager v3 Agent)
```python
from dashboard.veloz_sdk import function_tool

@function_tool
def my_tool(param: str) -> dict:
    """Tool description - used by AI to understand when to call this"""
    from .models import MyModel
    # Normal sync Django ORM code works!
    obj = MyModel.objects.get(name=param)
    return {"id": obj.id, "name": obj.name}
```

**Key Points:**
- Import `function_tool` from `dashboard.veloz_sdk`, NOT from `agents`
- Write normal synchronous Django code - SDK auto-wraps for async context
- Type hints are required (used for AI schema generation)
- Docstring is important - AI uses it to decide when to call your tool

### 3. Register Agent (Makes Tools Available to Manager v3)
```python
@agent(
    name="MyPluginSpecialist",
    model="gpt-5-mini",
    handoff_description="Delegate for X, Y, Z operations. Can do A, B, C."
)
def my_specialist():
    """You are the MyPluginSpecialist. [Instructions for agent behavior]"""
    return [my_tool, another_tool, third_tool]  # List all @function_tool functions
```

**Key Points:**
- `handoff_description` is crucial - tells Manager v3 WHEN to delegate to this specialist
- Return a list of all `@function_tool` decorated functions
- Agent runs on specified model (default: "gpt-5-mini")

### 4. Routes (URL + View)
```python
@route("/")
def list_view(request):
    from .models import MyModel
    items = MyModel.objects.all()
    return render(request, "my_plugin/list.html", {"items": items})

@route("/create")
def create_view(request):
    # Standard Django view logic
    return render(request, "my_plugin/form.html")

@route("/api/data")
def api_view(request):
    return JsonResponse({"status": "ok"})

# With permissions
@route("/admin-only", permission="staff")
def staff_view(request):
    return render(request, "my_plugin/admin.html")
```

**Key Points:**
- Routes are relative to plugin prefix (set in `register_urls`)
- Auto-adds trailing slashes (e.g., `/create` → `/create/`)
- `permission`: `"staff"` (staff only), `"superuser"` (superuser only), or omit (login required)

### 5. Menus (Admin Sidebar)
```python
@menu("My Plugin", icon="cube")
def list_view(request):
    # Same view function as route
    return render(request, "my_plugin/list.html")

@menu("Settings", parent="My Plugin", icon="cog")
def settings_view(request):
    return render(request, "my_plugin/settings.html")
```

**Key Points:**
- Decorate the same function as `@route`
- SDK auto-resolves URLs by matching functions
- `parent` creates submenu

### 6. Models (Standard Django)
```python
from django.db import models
from dashboard.veloz_sdk import SingletonMixin

class MyModel(models.Model):
    name = models.CharField(max_length=200)
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        ordering = ["-created_at"]

class MySettings(SingletonMixin, models.Model):
    # Singleton pattern - one row per tenant
    enable_feature = models.BooleanField(default=True)
```

**Key Points:**
- Standard Django models - nothing special
- Migrations auto-run when plugin is activated
- `SingletonMixin` is optional for settings-style models

### 7. AppConfig (Required)
```python
from django.apps import AppConfig

class MyPluginConfig(AppConfig):
    default_auto_field = "django.db.models.BigAutoField"
    name = "my_plugin"
    verbose_name = "My Plugin"

    def ready(self):
        """Called when Django loads the plugin"""
        plugin.register_urls("my-plugin")  # URL prefix
        plugin.finalize()  # Register everything
```

**Key Points:**
- Must be at bottom of `apps.py`
- `register_urls("prefix")` sets URL prefix (e.g., `/prefix/`)
- `finalize()` registers all routes, tools, agents, menus

## Templates

### Admin Template (with sidebar)
```html
{% extends "dashboard/index.html" %}

{% block admin_content %}
<div class="container mx-auto p-6">
    <h1>My Plugin</h1>
    <!-- Your content -->
</div>
{% endblock %}
```

### Public Template (no sidebar)
```html
<!DOCTYPE html>
<html>
<head>
    <title>My Public Page</title>
</head>
<body>
    <!-- Your content -->
</body>
</html>
```

## AI Integration

### Call LLM from Plugin
```python
from dashboard.veloz_sdk import ai

response = ai.complete(
    model="gpt-5-mini",
    messages=[{"role": "user", "content": "Analyze this..."}]
)
```

### Logging
```python
from dashboard.veloz_sdk import log

log.info("Something happened", extra={"key": "value"})
log.error("Error occurred", extra={"error": str(e)})
```

## Complete Minimal Example
```python
from django.apps import AppConfig
from django.shortcuts import render
from dashboard.veloz_sdk import plugin, function_tool, agent, route, menu

plugin.init(name="Todo", version="1.0.0", description="Task manager")

@function_tool
def list_todos(status: str = "all") -> list:
    """List all todos, optionally filtered by status"""
    from .models import Todo
    todos = Todo.objects.all()
    if status != "all":
        todos = todos.filter(status=status)
    return [{"id": t.id, "title": t.title} for t in todos]

@agent(
    name="TodoSpecialist",
    handoff_description="Handles todo operations - list, create, update tasks"
)
def todo_specialist():
    """You manage todos. List, create, and update tasks as requested."""
    return [list_todos]

@route("/")
@menu("Todos", icon="check")
def todo_list(request):
    from .models import Todo
    todos = Todo.objects.all()
    return render(request, "todo/list.html", {"todos": todos})

class TodoConfig(AppConfig):
    name = "todo"
    def ready(self):
        plugin.register_urls("todo")
        plugin.finalize()
```

## Key Rules
1. **Import `function_tool` from SDK** - NOT from `agents` package
2. **Type hints required** on all `@function_tool` functions
3. **`handoff_description` is critical** - tells Manager v3 when to delegate
4. **Sync code is fine** - SDK auto-wraps for async context
5. **Templates extend `dashboard/index.html`** for admin pages
6. **Call `plugin.finalize()`** in `ready()` method

## Database & Migrations
- Plugin models auto-migrate when plugin is activated in admin
- Tables are prefixed: `{plugin_name}_{model_name}`
- Tenant-isolated automatically (each CMS machine has separate data)
- Standard Django migration workflow

## Testing Your Plugin
1. Upload plugin ZIP to `/agentic-apps/`
2. System validates and auto-migrates
3. Routes available at `/{prefix}/`
4. Menus appear in admin sidebar
5. AI agent available in chat (ask "what can you do?" to see specialists)

## Common Patterns
- Settings model: Use `SingletonMixin` + `MySettings.get_solo()`
- API endpoints: Use `@route("/api/endpoint")` + `JsonResponse`
- Forms: Standard Django forms in `forms.py`
- Permissions: Add `permission="staff"` to `@route` decorator
- Logging: Use `log.info/error/debug` from SDK
''', encoding='utf-8')
    
    console.print(f"[green]✓ Created Veloz SDK Hello World template in ./{name}/[/green]")
    console.print(f"[dim]  Includes: routes, menus, templates, and SDK guide[/dim]")


@app.command()
def push():
    """Upload plugin to connected CMS"""
    console.print("\n[bold blue]⬆️  Pushing plugin...[/bold blue]\n")
    
    cms = config.get_connected_cms()
    if not cms:
        console.print("[red]✗ Not connected to any CMS[/red]")
        console.print("[dim]Run `veloz connect <cms>` first[/dim]\n")
        raise typer.Exit(1)
    
    # Check for apps.py
    cwd = Path.cwd()
    apps_py = cwd / "apps.py"
    
    if not apps_py.exists():
        console.print("[red]✗ No plugin detected in current directory[/red]")
        console.print("[dim]Run `veloz init <name>` to create a plugin[/dim]\n")
        raise typer.Exit(1)
    
    # Extract plugin info from apps.py
    plugin_info = extract_plugin_info(apps_py)
    
    if plugin_info and 'name' in plugin_info:
        plugin_name = plugin_info['name']
        version = plugin_info.get('version', 'unknown')
        display_name = plugin_info.get('display_name', plugin_name)
        console.print(f"→ Plugin: [cyan]{display_name}[/cyan] ({plugin_name})")
        if version != 'unknown':
            console.print(f"→ Version: [cyan]{version}[/cyan]")
    else:
        # Fall back to folder name
        plugin_name = cwd.name
        console.print(f"[yellow]⚠️  Could not parse apps.py, using folder name: {plugin_name}[/yellow]")
    
    # Get tenant URL
    tenant_url = config.get_connected_cms_url()
    if not tenant_url:
        console.print("[red]✗ No URL found for connected CMS[/red]")
        console.print("[dim]Run `veloz connect <cms>` again[/dim]\n")
        raise typer.Exit(1)
    
    console.print(f"→ Uploading to {cms}...")
    console.print(f"[dim]  Target: {tenant_url}[/dim]")
    
    # Create ZIP (include root folder)
    zip_buffer = io.BytesIO()
    with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
        for file_path in cwd.rglob('*'):
            if file_path.is_file():
                # Skip unwanted files
                if any(x in str(file_path) for x in ['__pycache__', '.pyc', '.git', 'venv']):
                    continue
                
                # Include plugin name as root folder in ZIP
                rel_path = file_path.relative_to(cwd)
                arcname = f"{plugin_name}/{rel_path}"
                zf.write(file_path, arcname)
    
    zip_buffer.seek(0)
    
    # Upload to TENANT (not central server!)
    client = APIClient(server_url=tenant_url)
    
    try:
        status, data = client.post(
            f'/api/cli/plugins/{plugin_name}/upload/',
            files={'plugin_zip': (f'{plugin_name}.zip', zip_buffer, 'application/zip')}
        )
        
        if status != 200:
            console.print(f"[red]✗ Upload failed: {data.get('error', 'Unknown error')}[/red]")
            raise typer.Exit(1)
        
        console.print(f"[green]✓ Upload complete[/green]\n")
        
        action = data.get('action', 'unknown')
        if action == 'upgraded':
            old_v = data.get('old_version', 'unknown')
            new_v = data.get('new_version', 'unknown')
            console.print(f"[green]✓ Upgraded from v{old_v} → v{new_v}[/green]")
        else:
            console.print(f"[green]✓ Installed successfully[/green]")
        
        if data.get('restart_required'):
            console.print("\n[yellow]→ Triggering server restart...[/yellow]")
            
            # Call the restart endpoint
            try:
                restart_status, restart_data = client.post('/api/cli/plugins/restart/')
                if restart_status != 200 or not restart_data.get('success'):
                    console.print(f"[yellow]⚠ Restart trigger failed: {restart_data.get('message', 'Unknown error')}[/yellow]")
            except Exception as e:
                console.print(f"[yellow]⚠ Could not trigger restart: {e}[/yellow]")
            
            # Poll for server restart completion
            console.print("[yellow]→ Waiting for server to restart...[/yellow]")
            max_attempts = 30  # 30 seconds max
            attempt = 0
            server_ready = False
            
            while attempt < max_attempts:
                try:
                    response = client.session.head(f"{client.server_url}/health/", timeout=2)
                    if response.status_code == 200:
                        server_ready = True
                        break
                except:
                    pass
                time.sleep(1)
                attempt += 1
            
            if not server_ready:
                console.print("[yellow]⚠ Server restart timeout, but plugin may still activate[/yellow]")
            else:
                console.print("[green]✓ Server restarted[/green]")
                
                # Poll for migrations to complete
                console.print("[yellow]→ Waiting for migrations to complete...[/yellow]")
                migrations_done = False
                max_migration_attempts = 20  # 20 seconds max
                migration_attempt = 0
                
                while migration_attempt < max_migration_attempts:
                    try:
                        # Check plugin status
                        info_status, info_data = client.get(f'/api/cli/plugins/{plugin_name}/info/')
                        if info_status == 200:
                            pipeline = info_data.get('processing_pipeline', {})
                            pending = pipeline.get('pending_migrations', False)
                            is_active = info_data.get('is_active', False)
                            
                            if not pending and is_active:
                                migrations_done = True
                                break
                    except:
                        pass
                    
                    time.sleep(1)
                    migration_attempt += 1
                
                if migrations_done:
                    console.print("[green]✓ Migrations complete[/green]")
                else:
                    console.print("[yellow]⚠ Migration timeout, but plugin may still be activating[/yellow]")
        
        console.print(f"\n[green]✓ Plugin is now live![/green]\n")
    
    except APIError as e:
        console.print(f"[red]✗ {e}[/red]")
        raise typer.Exit(1)


@app.command()
def pull(plugin_name: Optional[str] = typer.Argument(None)):
    """Download plugin from connected CMS"""
    cms = config.get_connected_cms()
    if not cms:
        console.print("[red]✗ Not connected to any CMS[/red]")
        raise typer.Exit(1)
    
    if not plugin_name:
        plugin_name = Path.cwd().name
    
    pull_plugin(plugin_name)


def pull_plugin(plugin_name: str):
    """Helper to pull plugin files"""
    console.print(f"\n[bold blue]⬇️  Pulling '{plugin_name}'...[/bold blue]\n")
    
    # Get tenant URL
    tenant_url = config.get_connected_cms_url()
    if not tenant_url:
        console.print("[red]✗ No URL found for connected CMS[/red]")
        console.print("[dim]Run `veloz connect <cms>` again[/dim]\n")
        raise typer.Exit(1)
    
    client = APIClient(server_url=tenant_url)
    
    try:
        # Download ZIP
        url = f"{client.server_url}/api/cli/plugins/{plugin_name}/files/"
        headers = client._get_headers(authenticated=True)
        
        response = client.session.get(url, headers=headers, timeout=60)
        
        if response.status_code != 200:
            console.print(f"[red]✗ Download failed[/red]")
            raise typer.Exit(1)
        
        # Extract ZIP
        path = Path(plugin_name)
        path.mkdir(exist_ok=True)
        
        with zipfile.ZipFile(io.BytesIO(response.content)) as zf:
            # Check if all files have a common root folder (and strip it)
            all_files = zf.namelist()
            
            # Find common prefix
            common_prefix = None
            for name in all_files:
                parts = name.split('/')
                if len(parts) > 1:
                    root = parts[0] + '/'
                    if common_prefix is None:
                        common_prefix = root
                    elif common_prefix != root:
                        common_prefix = None
                        break
            
            # Extract, stripping common prefix if found
            file_count = 0
            for member in zf.namelist():
                # Skip if it's just the root folder itself
                if common_prefix and member == common_prefix.rstrip('/'):
                    continue
                
                # Determine target path
                if common_prefix and member.startswith(common_prefix):
                    target_path = path / member[len(common_prefix):]
                else:
                    target_path = path / member
                
                # Skip if it's a directory
                if member.endswith('/'):
                    target_path.mkdir(parents=True, exist_ok=True)
                    continue
                
                # Extract file
                target_path.parent.mkdir(parents=True, exist_ok=True)
                with zf.open(member) as source, open(target_path, 'wb') as target:
                    target.write(source.read())
                file_count += 1
        
        console.print(f"[green]✓ Plugin files downloaded to ./{plugin_name}/[/green]")
        console.print(f"[green]✓ {file_count} files extracted[/green]\n")
    
    except Exception as e:
        console.print(f"[red]✗ {e}[/red]")
        raise typer.Exit(1)


def _poll_logs(tenant_url: str, cms_name: str):
    """
    Poll logs endpoint - simple, reliable, no streaming complexity.
    
    Polls every 500ms for new logs. Much more reliable than SSE/WebSocket.
    """
    import requests
    import time
    import signal
    import sys
    
    # Polling endpoint URL
    poll_url = f"{tenant_url}/api/cli/logs/poll/"
    
    # Get auth token
    creds = config.load_credentials()
    if not creds:
        console.print("[red]✗ Not logged in. Run `veloz login` first[/red]")
        raise typer.Exit(1)
    
    token = creds["token"]
    
    # Headers with auth token
    headers = {
        'Authorization': f'Bearer {token}',
    }
    
    # Set up Ctrl+C handler for immediate exit
    def signal_handler(sig, frame):
        console.print("\n[green]✓ Stopped[/green]\n")
        sys.exit(0)
    
    signal.signal(signal.SIGINT, signal_handler)
    
    console.print("[green]✓ Connected - polling for logs...[/green]\n")
    
    # Track last seen log ID
    last_id = 0
    consecutive_errors = 0
    max_errors = 10
    
    try:
        while True:
            try:
                # Poll for new logs
                response = requests.get(
                    poll_url,
                    headers=headers,
                    params={'since': last_id},
                    timeout=10
                )
                
                if response.status_code == 200:
                    try:
                        data = response.json()
                        logs = data.get('logs', [])
                        
                        # Print each new log
                        for log in logs:
                            # Validate log structure
                            if 'id' in log and 'line' in log:
                                print(log['line'])
                                sys.stdout.flush()
                                last_id = log['id']  # Update last seen ID
                            else:
                                # Malformed log entry, skip it
                                pass
                        
                        # Reset error counter on success
                        consecutive_errors = 0
                        
                    except (ValueError, KeyError, TypeError) as e:
                        # JSON parsing or structure error
                        console.print(f"[yellow]⚠ Malformed response: {e}[/yellow]")
                        consecutive_errors += 1
                    
                elif response.status_code == 401:
                    console.print("[red]✗ Authentication failed. Please run `veloz login` again[/red]")
                    raise typer.Exit(1)
                    
                else:
                    console.print(f"[yellow]⚠ Server returned {response.status_code}[/yellow]")
                    consecutive_errors += 1
                
                # Poll every 500ms for good balance between responsiveness and server load
                time.sleep(0.5)
                
            except KeyboardInterrupt:
                # Already handled by signal handler, but just in case
                raise
                
            except requests.exceptions.RequestException as e:
                consecutive_errors += 1
                if consecutive_errors >= max_errors:
                    console.print(f"\n[red]✗ Connection failed after {max_errors} errors[/red]")
                    console.print(f"[dim]Error: {e}[/dim]\n")
                    raise typer.Exit(1)
                
                # Brief pause before retry
                time.sleep(1)
    
    except KeyboardInterrupt:
        console.print("\n[green]✓ Stopped[/green]\n")
        sys.exit(0)


@app.command()
def logs(
    follow: bool = typer.Option(True, "--follow/--no-follow", "-f", help="Follow log output (default: true)"),
):
    """Stream real-time logs from connected CMS via polling (simple & reliable)"""
    console.print("\n[bold blue]📋 Streaming Logs...[/bold blue]\n")
    
    cms = config.get_connected_cms()
    if not cms:
        console.print("[red]✗ Not connected to any CMS[/red]")
        console.print("[dim]Run `veloz list` and `veloz connect <cms>` first[/dim]\n")
        raise typer.Exit(1)
    
    # Get tenant URL
    tenant_url = config.get_connected_cms_url()
    if not tenant_url:
        console.print("[red]✗ No URL found for connected CMS[/red]")
        console.print("[dim]Run `veloz connect <cms>` again[/dim]\n")
        raise typer.Exit(1)
    
    console.print(f"[cyan]→ Polling logs from {cms}...[/cyan]")
    console.print(f"[dim]  URL: {tenant_url}[/dim]")
    console.print(f"[dim]  Press Ctrl+C to stop[/dim]\n")
    
    # Simple polling - reliable, no streaming/buffering issues
    try:
        _poll_logs(tenant_url, cms)
    except KeyboardInterrupt:
        console.print("\n[green]✓ Stopped[/green]\n")


@app.command()
def migrate(app_name: Optional[str] = typer.Option(None, help="Override app name for migrations")):
    """Generate Django migrations locally"""
    console.print("\n[bold blue]🔧 Generating Migrations...[/bold blue]\n")
    
    # Check for apps.py
    cwd = Path.cwd()
    apps_py = cwd / "apps.py"
    
    if not apps_py.exists():
        console.print("[red]✗ No plugin detected in current directory[/red]")
        console.print("[dim]Run this command from inside your plugin folder[/dim]\n")
        raise typer.Exit(1)
    
    # Extract plugin info
    plugin_info = extract_plugin_info(apps_py)
    
    if not plugin_info or 'name' not in plugin_info:
        if not app_name:
            console.print("[red]✗ Could not detect plugin name from apps.py[/red]")
            console.print("[dim]Try: veloz migrate --app-name your_plugin_name[/dim]\n")
            raise typer.Exit(1)
        plugin_name = app_name
    else:
        plugin_name = plugin_info['name']
    
    console.print(f"→ Plugin: [cyan]{plugin_name}[/cyan]")
    
    # Setup minimal Django environment
    try:
        # Import mock dashboard before Django setup (so plugins can import from dashboard)
        cli_dir = Path(__file__).parent
        if str(cli_dir) not in sys.path:
            sys.path.insert(0, str(cli_dir))
        
        import mock_dashboard  # noqa - This sets up sys.modules mocks
        
        import django
        from django.conf import settings
        from django.core.management import call_command
        
        if not settings.configured:
            settings.configure(
                DEBUG=True,
                SECRET_KEY='veloz-cli-migrations-key',
                INSTALLED_APPS=[
                    'django.contrib.contenttypes',
                    'django.contrib.auth',
                    plugin_name,
                ],
                DATABASES={
                    'default': {
                        'ENGINE': 'django.db.backends.sqlite3',
                        'NAME': ':memory:',  # In-memory DB (migrations don't need real DB)
                    }
                },
                USE_TZ=True,
            )
            
            # Add current directory to Python path so Django can import the plugin
            if str(cwd) not in sys.path:
                sys.path.insert(0, str(cwd))
            
            django.setup()
        
        console.print("→ Running makemigrations...\n")
        
        # Run makemigrations
        from io import StringIO
        output = StringIO()
        
        try:
            call_command(
                'makemigrations',
                plugin_name,
                verbosity=2,
                interactive=False,
                stdout=output,
                stderr=output
            )
            
            result = output.getvalue()
            
            # Check if migrations were created
            if "No changes detected" in result:
                console.print("[yellow]→ No changes detected in models[/yellow]")
            elif "Migrations for" in result:
                console.print("[green]✓ Migrations generated successfully![/green]\n")
                console.print("[dim]" + result + "[/dim]")
            else:
                console.print(result)
            
            console.print("\n[green]✓ Done! Check the migrations/ folder[/green]\n")
            
        except Exception as e:
            console.print(f"[red]✗ Migration generation failed: {e}[/red]")
            raise typer.Exit(1)
            
    except ImportError as e:
        console.print(f"[red]✗ Django not installed: {e}[/red]")
        console.print("[dim]Run: pip install -r requirements.txt[/dim]\n")
        raise typer.Exit(1)
    except Exception as e:
        console.print(f"[red]✗ Error: {e}[/red]")
        import traceback
        console.print(f"[dim]{traceback.format_exc()}[/dim]")
        raise typer.Exit(1)


if __name__ == "__main__":
    app()

