Spaces:
Build error
Build error
| #!/usr/bin/env python3 | |
| """ | |
| AudioForge Launch Verification Script | |
| Systematically verifies all items in LAUNCH_CHECKLIST.md | |
| Usage: | |
| python scripts/launch_verification.py [--section SECTION] [--fix] | |
| Options: | |
| --section SECTION Run specific section only (backend, frontend, security, etc.) | |
| --fix Attempt to auto-fix issues where possible | |
| --verbose Show detailed output | |
| """ | |
| import argparse | |
| import asyncio | |
| import json | |
| import os | |
| import subprocess | |
| import sys | |
| import time | |
| from dataclasses import dataclass, field | |
| from enum import Enum | |
| from pathlib import Path | |
| from typing import Dict, List, Optional, Tuple | |
| from urllib.parse import urlparse | |
| try: | |
| import httpx | |
| import psutil | |
| from rich.console import Console | |
| from rich.progress import Progress, SpinnerColumn, TextColumn | |
| from rich.table import Table | |
| from rich.panel import Panel | |
| except ImportError: | |
| print("Installing required packages...") | |
| subprocess.run([sys.executable, "-m", "pip", "install", "httpx", "psutil", "rich"], check=True) | |
| import httpx | |
| import psutil | |
| from rich.console import Console | |
| from rich.progress import Progress, SpinnerColumn, TextColumn | |
| from rich.table import Table | |
| from rich.panel import Panel | |
| class CheckStatus(Enum): | |
| """Status of a verification check.""" | |
| PASS = "✅" | |
| FAIL = "❌" | |
| WARN = "⚠️" | |
| SKIP = "⏭️" | |
| INFO = "ℹ️" | |
| class CheckResult: | |
| """Result of a single verification check.""" | |
| name: str | |
| status: CheckStatus | |
| message: str | |
| details: Optional[str] = None | |
| fix_available: bool = False | |
| fix_command: Optional[str] = None | |
| class SectionResult: | |
| """Result of a verification section.""" | |
| name: str | |
| checks: List[CheckResult] = field(default_factory=list) | |
| def passed(self) -> int: | |
| return sum(1 for c in self.checks if c.status == CheckStatus.PASS) | |
| def failed(self) -> int: | |
| return sum(1 for c in self.checks if c.status == CheckStatus.FAIL) | |
| def warned(self) -> int: | |
| return sum(1 for c in self.checks if c.status == CheckStatus.WARN) | |
| def total(self) -> int: | |
| return len(self.checks) | |
| def success_rate(self) -> float: | |
| if self.total == 0: | |
| return 0.0 | |
| return (self.passed / self.total) * 100 | |
| class LaunchVerifier: | |
| """Main verification orchestrator.""" | |
| def __init__(self, console: Console, fix_mode: bool = False, verbose: bool = False): | |
| self.console = console | |
| self.fix_mode = fix_mode | |
| self.verbose = verbose | |
| self.root_path = Path(__file__).parent.parent | |
| self.results: Dict[str, SectionResult] = {} | |
| async def verify_all(self) -> Dict[str, SectionResult]: | |
| """Run all verification checks.""" | |
| sections = [ | |
| ("Backend", self.verify_backend), | |
| ("Frontend", self.verify_frontend), | |
| ("UI/UX", self.verify_ui_ux), | |
| ("Integration", self.verify_integration), | |
| ("Performance", self.verify_performance), | |
| ("Security", self.verify_security), | |
| ("Documentation", self.verify_documentation), | |
| ] | |
| for section_name, verify_func in sections: | |
| self.console.print(f"\n[bold cyan]═══ {section_name} Verification ═══[/bold cyan]\n") | |
| result = await verify_func() | |
| self.results[section_name] = result | |
| self._print_section_summary(result) | |
| return self.results | |
| async def verify_backend(self) -> SectionResult: | |
| """Verify backend setup and health.""" | |
| result = SectionResult(name="Backend") | |
| # Check Python version | |
| python_version = sys.version_info | |
| if python_version >= (3, 11): | |
| result.checks.append(CheckResult( | |
| name="Python Version", | |
| status=CheckStatus.PASS, | |
| message=f"Python {python_version.major}.{python_version.minor}.{python_version.micro}" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="Python Version", | |
| status=CheckStatus.FAIL, | |
| message=f"Python 3.11+ required, found {python_version.major}.{python_version.minor}", | |
| fix_available=False | |
| )) | |
| # Check .env file | |
| env_path = self.root_path / "backend" / ".env" | |
| env_example = self.root_path / "backend" / ".env.example" | |
| if env_path.exists(): | |
| result.checks.append(CheckResult( | |
| name="Environment File", | |
| status=CheckStatus.PASS, | |
| message=".env file exists" | |
| )) | |
| elif env_example.exists() and self.fix_mode: | |
| import shutil | |
| shutil.copy(env_example, env_path) | |
| result.checks.append(CheckResult( | |
| name="Environment File", | |
| status=CheckStatus.PASS, | |
| message=".env created from .env.example" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="Environment File", | |
| status=CheckStatus.FAIL, | |
| message=".env file missing", | |
| fix_available=True, | |
| fix_command="cp backend/.env.example backend/.env" | |
| )) | |
| # Check backend dependencies | |
| backend_path = self.root_path / "backend" | |
| pyproject_path = backend_path / "pyproject.toml" | |
| if pyproject_path.exists(): | |
| result.checks.append(CheckResult( | |
| name="Backend Package Config", | |
| status=CheckStatus.PASS, | |
| message="pyproject.toml found" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="Backend Package Config", | |
| status=CheckStatus.FAIL, | |
| message="pyproject.toml missing" | |
| )) | |
| # Check if backend is running | |
| try: | |
| async with httpx.AsyncClient(timeout=5.0) as client: | |
| response = await client.get("http://localhost:8000/health") | |
| if response.status_code == 200: | |
| result.checks.append(CheckResult( | |
| name="Backend Health Check", | |
| status=CheckStatus.PASS, | |
| message="Backend responding on port 8000", | |
| details=f"Response: {response.json()}" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="Backend Health Check", | |
| status=CheckStatus.WARN, | |
| message=f"Backend returned status {response.status_code}" | |
| )) | |
| except Exception as e: | |
| result.checks.append(CheckResult( | |
| name="Backend Health Check", | |
| status=CheckStatus.WARN, | |
| message="Backend not running or not accessible", | |
| details=str(e), | |
| fix_command="cd backend && uvicorn app.main:app --reload" | |
| )) | |
| # Check API documentation | |
| try: | |
| async with httpx.AsyncClient(timeout=5.0) as client: | |
| response = await client.get("http://localhost:8000/docs") | |
| if response.status_code == 200: | |
| result.checks.append(CheckResult( | |
| name="API Documentation", | |
| status=CheckStatus.PASS, | |
| message="API docs accessible at /docs" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="API Documentation", | |
| status=CheckStatus.WARN, | |
| message="API docs not accessible" | |
| )) | |
| except Exception: | |
| result.checks.append(CheckResult( | |
| name="API Documentation", | |
| status=CheckStatus.SKIP, | |
| message="Backend not running" | |
| )) | |
| # Check storage directories | |
| storage_path = backend_path / "storage" / "audio" | |
| required_dirs = ["music", "vocals", "mixed", "mastered"] | |
| missing_dirs = [d for d in required_dirs if not (storage_path / d).exists()] | |
| if not missing_dirs: | |
| result.checks.append(CheckResult( | |
| name="Storage Directories", | |
| status=CheckStatus.PASS, | |
| message="All storage directories exist" | |
| )) | |
| elif self.fix_mode: | |
| for dir_name in missing_dirs: | |
| (storage_path / dir_name).mkdir(parents=True, exist_ok=True) | |
| result.checks.append(CheckResult( | |
| name="Storage Directories", | |
| status=CheckStatus.PASS, | |
| message=f"Created missing directories: {', '.join(missing_dirs)}" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="Storage Directories", | |
| status=CheckStatus.WARN, | |
| message=f"Missing directories: {', '.join(missing_dirs)}", | |
| fix_available=True | |
| )) | |
| return result | |
| async def verify_frontend(self) -> SectionResult: | |
| """Verify frontend setup and build.""" | |
| result = SectionResult(name="Frontend") | |
| frontend_path = self.root_path / "frontend" | |
| # Check package.json | |
| package_json = frontend_path / "package.json" | |
| if package_json.exists(): | |
| result.checks.append(CheckResult( | |
| name="Package Configuration", | |
| status=CheckStatus.PASS, | |
| message="package.json found" | |
| )) | |
| # Check if node_modules exists | |
| node_modules = frontend_path / "node_modules" | |
| if node_modules.exists(): | |
| result.checks.append(CheckResult( | |
| name="Dependencies Installed", | |
| status=CheckStatus.PASS, | |
| message="node_modules directory exists" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="Dependencies Installed", | |
| status=CheckStatus.FAIL, | |
| message="node_modules missing", | |
| fix_available=True, | |
| fix_command="cd frontend && pnpm install" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="Package Configuration", | |
| status=CheckStatus.FAIL, | |
| message="package.json missing" | |
| )) | |
| # Check .env.local | |
| env_local = frontend_path / ".env.local" | |
| if env_local.exists(): | |
| result.checks.append(CheckResult( | |
| name="Environment Configuration", | |
| status=CheckStatus.PASS, | |
| message=".env.local exists" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="Environment Configuration", | |
| status=CheckStatus.WARN, | |
| message=".env.local missing", | |
| fix_available=True, | |
| fix_command='echo "NEXT_PUBLIC_API_URL=http://localhost:8000" > frontend/.env.local' | |
| )) | |
| # Check if frontend is running | |
| try: | |
| async with httpx.AsyncClient(timeout=5.0) as client: | |
| response = await client.get("http://localhost:3000") | |
| if response.status_code == 200: | |
| result.checks.append(CheckResult( | |
| name="Frontend Server", | |
| status=CheckStatus.PASS, | |
| message="Frontend responding on port 3000" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="Frontend Server", | |
| status=CheckStatus.WARN, | |
| message=f"Frontend returned status {response.status_code}" | |
| )) | |
| except Exception as e: | |
| result.checks.append(CheckResult( | |
| name="Frontend Server", | |
| status=CheckStatus.WARN, | |
| message="Frontend not running", | |
| details=str(e), | |
| fix_command="cd frontend && pnpm dev" | |
| )) | |
| # Check TypeScript configuration | |
| tsconfig = frontend_path / "tsconfig.json" | |
| if tsconfig.exists(): | |
| result.checks.append(CheckResult( | |
| name="TypeScript Configuration", | |
| status=CheckStatus.PASS, | |
| message="tsconfig.json found" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="TypeScript Configuration", | |
| status=CheckStatus.FAIL, | |
| message="tsconfig.json missing" | |
| )) | |
| # Check for TypeScript errors (if tsc is available) | |
| try: | |
| proc = subprocess.run( | |
| ["pnpm", "tsc", "--noEmit"], | |
| cwd=frontend_path, | |
| capture_output=True, | |
| text=True, | |
| timeout=30 | |
| ) | |
| if proc.returncode == 0: | |
| result.checks.append(CheckResult( | |
| name="TypeScript Compilation", | |
| status=CheckStatus.PASS, | |
| message="No TypeScript errors" | |
| )) | |
| else: | |
| error_lines = proc.stdout.count('\n') | |
| result.checks.append(CheckResult( | |
| name="TypeScript Compilation", | |
| status=CheckStatus.FAIL, | |
| message=f"TypeScript errors found ({error_lines} lines)", | |
| details=proc.stdout[:500] | |
| )) | |
| except FileNotFoundError: | |
| result.checks.append(CheckResult( | |
| name="TypeScript Compilation", | |
| status=CheckStatus.SKIP, | |
| message="pnpm not found" | |
| )) | |
| except Exception as e: | |
| result.checks.append(CheckResult( | |
| name="TypeScript Compilation", | |
| status=CheckStatus.SKIP, | |
| message=f"Could not run tsc: {str(e)}" | |
| )) | |
| return result | |
| async def verify_ui_ux(self) -> SectionResult: | |
| """Verify UI/UX enhancements are working.""" | |
| result = SectionResult(name="UI/UX") | |
| frontend_path = self.root_path / "frontend" / "src" | |
| # Check for new components | |
| components_to_check = [ | |
| "sound-wave-background.tsx", | |
| "floating-notes.tsx", | |
| "prompt-suggestions.tsx", | |
| "mini-visualizer.tsx", | |
| "footer-stats.tsx", | |
| "keyboard-shortcuts.tsx", | |
| "confetti-effect.tsx", | |
| ] | |
| components_path = frontend_path / "components" | |
| missing_components = [] | |
| for component in components_to_check: | |
| if not (components_path / component).exists(): | |
| missing_components.append(component) | |
| if not missing_components: | |
| result.checks.append(CheckResult( | |
| name="Creative Components", | |
| status=CheckStatus.PASS, | |
| message=f"All {len(components_to_check)} creative components present" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="Creative Components", | |
| status=CheckStatus.WARN, | |
| message=f"Missing {len(missing_components)} components", | |
| details=", ".join(missing_components) | |
| )) | |
| # Check globals.css for animations | |
| globals_css = frontend_path / "app" / "globals.css" | |
| if globals_css.exists(): | |
| content = globals_css.read_text() | |
| animations = [ | |
| "fade-in", | |
| "slide-in-left", | |
| "slide-in-right", | |
| "gradient", | |
| "pulse-glow", | |
| "bounce-subtle", | |
| "float-up", | |
| "confetti-fall" | |
| ] | |
| missing_animations = [a for a in animations if f"@keyframes {a}" not in content] | |
| if not missing_animations: | |
| result.checks.append(CheckResult( | |
| name="CSS Animations", | |
| status=CheckStatus.PASS, | |
| message=f"All {len(animations)} animations defined" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="CSS Animations", | |
| status=CheckStatus.WARN, | |
| message=f"Missing {len(missing_animations)} animations", | |
| details=", ".join(missing_animations) | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="CSS Animations", | |
| status=CheckStatus.FAIL, | |
| message="globals.css not found" | |
| )) | |
| # Check tailwind config for font support | |
| tailwind_config = self.root_path / "frontend" / "tailwind.config.ts" | |
| if tailwind_config.exists(): | |
| content = tailwind_config.read_text() | |
| if "fontFamily" in content and "display" in content: | |
| result.checks.append(CheckResult( | |
| name="Typography Configuration", | |
| status=CheckStatus.PASS, | |
| message="Display font configured in Tailwind" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="Typography Configuration", | |
| status=CheckStatus.WARN, | |
| message="Display font may not be configured" | |
| )) | |
| return result | |
| async def verify_integration(self) -> SectionResult: | |
| """Verify integration between frontend and backend.""" | |
| result = SectionResult(name="Integration") | |
| # Check if both services are running | |
| backend_running = False | |
| frontend_running = False | |
| try: | |
| async with httpx.AsyncClient(timeout=5.0) as client: | |
| await client.get("http://localhost:8000/health") | |
| backend_running = True | |
| except Exception: | |
| pass | |
| try: | |
| async with httpx.AsyncClient(timeout=5.0) as client: | |
| await client.get("http://localhost:3000") | |
| frontend_running = True | |
| except Exception: | |
| pass | |
| if backend_running and frontend_running: | |
| result.checks.append(CheckResult( | |
| name="Services Running", | |
| status=CheckStatus.PASS, | |
| message="Both frontend and backend are running" | |
| )) | |
| # Test API endpoint from frontend perspective | |
| try: | |
| async with httpx.AsyncClient(timeout=10.0) as client: | |
| # Test generations list endpoint | |
| response = await client.get("http://localhost:8000/api/v1/generations") | |
| if response.status_code in [200, 404]: # 404 is ok if no generations yet | |
| result.checks.append(CheckResult( | |
| name="API Endpoints", | |
| status=CheckStatus.PASS, | |
| message="Generations API endpoint accessible" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="API Endpoints", | |
| status=CheckStatus.WARN, | |
| message=f"Unexpected status code: {response.status_code}" | |
| )) | |
| except Exception as e: | |
| result.checks.append(CheckResult( | |
| name="API Endpoints", | |
| status=CheckStatus.FAIL, | |
| message="Could not access API endpoints", | |
| details=str(e) | |
| )) | |
| else: | |
| services_status = [] | |
| if not backend_running: | |
| services_status.append("backend") | |
| if not frontend_running: | |
| services_status.append("frontend") | |
| result.checks.append(CheckResult( | |
| name="Services Running", | |
| status=CheckStatus.FAIL, | |
| message=f"Services not running: {', '.join(services_status)}", | |
| fix_command="docker-compose up -d" | |
| )) | |
| return result | |
| async def verify_performance(self) -> SectionResult: | |
| """Verify performance metrics.""" | |
| result = SectionResult(name="Performance") | |
| # Check backend response time | |
| try: | |
| async with httpx.AsyncClient(timeout=5.0) as client: | |
| start = time.time() | |
| response = await client.get("http://localhost:8000/health") | |
| duration = (time.time() - start) * 1000 # Convert to ms | |
| if response.status_code == 200 and duration < 200: | |
| result.checks.append(CheckResult( | |
| name="Backend Response Time", | |
| status=CheckStatus.PASS, | |
| message=f"Health check: {duration:.0f}ms (< 200ms target)" | |
| )) | |
| elif duration < 500: | |
| result.checks.append(CheckResult( | |
| name="Backend Response Time", | |
| status=CheckStatus.WARN, | |
| message=f"Health check: {duration:.0f}ms (target: < 200ms)" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="Backend Response Time", | |
| status=CheckStatus.FAIL, | |
| message=f"Health check: {duration:.0f}ms (too slow)" | |
| )) | |
| except Exception: | |
| result.checks.append(CheckResult( | |
| name="Backend Response Time", | |
| status=CheckStatus.SKIP, | |
| message="Backend not running" | |
| )) | |
| # Check system resources | |
| cpu_percent = psutil.cpu_percent(interval=1) | |
| memory = psutil.virtual_memory() | |
| if cpu_percent < 80: | |
| result.checks.append(CheckResult( | |
| name="CPU Usage", | |
| status=CheckStatus.PASS, | |
| message=f"CPU: {cpu_percent:.1f}% (healthy)" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="CPU Usage", | |
| status=CheckStatus.WARN, | |
| message=f"CPU: {cpu_percent:.1f}% (high)" | |
| )) | |
| if memory.percent < 80: | |
| result.checks.append(CheckResult( | |
| name="Memory Usage", | |
| status=CheckStatus.PASS, | |
| message=f"Memory: {memory.percent:.1f}% (healthy)" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="Memory Usage", | |
| status=CheckStatus.WARN, | |
| message=f"Memory: {memory.percent:.1f}% (high)" | |
| )) | |
| return result | |
| async def verify_security(self) -> SectionResult: | |
| """Verify security configurations.""" | |
| result = SectionResult(name="Security") | |
| # Check for .env in .gitignore | |
| gitignore = self.root_path / ".gitignore" | |
| if gitignore.exists(): | |
| content = gitignore.read_text() | |
| if ".env" in content: | |
| result.checks.append(CheckResult( | |
| name="Environment Files Protected", | |
| status=CheckStatus.PASS, | |
| message=".env files in .gitignore" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="Environment Files Protected", | |
| status=CheckStatus.FAIL, | |
| message=".env not in .gitignore", | |
| fix_available=True | |
| )) | |
| # Check for exposed secrets in frontend | |
| frontend_env = self.root_path / "frontend" / ".env.local" | |
| if frontend_env.exists(): | |
| content = frontend_env.read_text() | |
| dangerous_keys = ["SECRET", "PRIVATE", "KEY", "PASSWORD"] | |
| exposed = [key for key in dangerous_keys if key in content.upper() and "NEXT_PUBLIC" not in content] | |
| if not exposed: | |
| result.checks.append(CheckResult( | |
| name="Frontend Secrets", | |
| status=CheckStatus.PASS, | |
| message="No exposed secrets in .env.local" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="Frontend Secrets", | |
| status=CheckStatus.WARN, | |
| message=f"Potential secrets found: {', '.join(exposed)}" | |
| )) | |
| # Check CORS configuration (if backend is running) | |
| try: | |
| async with httpx.AsyncClient(timeout=5.0) as client: | |
| response = await client.options( | |
| "http://localhost:8000/api/v1/generations", | |
| headers={"Origin": "http://localhost:3000"} | |
| ) | |
| if "access-control-allow-origin" in response.headers: | |
| result.checks.append(CheckResult( | |
| name="CORS Configuration", | |
| status=CheckStatus.PASS, | |
| message="CORS headers present" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="CORS Configuration", | |
| status=CheckStatus.WARN, | |
| message="CORS headers not found" | |
| )) | |
| except Exception: | |
| result.checks.append(CheckResult( | |
| name="CORS Configuration", | |
| status=CheckStatus.SKIP, | |
| message="Backend not running" | |
| )) | |
| return result | |
| async def verify_documentation(self) -> SectionResult: | |
| """Verify documentation completeness.""" | |
| result = SectionResult(name="Documentation") | |
| required_docs = { | |
| "README.md": "Main documentation", | |
| "SETUP.md": "Setup instructions", | |
| "ARCHITECTURE.md": "Architecture overview", | |
| "CONTRIBUTING.md": "Contribution guidelines", | |
| "LAUNCH_CHECKLIST.md": "Launch checklist", | |
| } | |
| missing_docs = [] | |
| for doc_file, description in required_docs.items(): | |
| doc_path = self.root_path / doc_file | |
| if doc_path.exists(): | |
| size = doc_path.stat().st_size | |
| if size > 100: # At least 100 bytes | |
| continue | |
| missing_docs.append(f"{doc_file} ({description})") | |
| if not missing_docs: | |
| result.checks.append(CheckResult( | |
| name="Required Documentation", | |
| status=CheckStatus.PASS, | |
| message=f"All {len(required_docs)} documentation files present" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="Required Documentation", | |
| status=CheckStatus.WARN, | |
| message=f"Missing or incomplete: {len(missing_docs)} files", | |
| details="\n".join(missing_docs) | |
| )) | |
| # Check for LICENSE | |
| license_file = self.root_path / "LICENSE" | |
| if license_file.exists(): | |
| result.checks.append(CheckResult( | |
| name="License File", | |
| status=CheckStatus.PASS, | |
| message="LICENSE file present" | |
| )) | |
| else: | |
| result.checks.append(CheckResult( | |
| name="License File", | |
| status=CheckStatus.WARN, | |
| message="LICENSE file missing" | |
| )) | |
| return result | |
| def _print_section_summary(self, result: SectionResult): | |
| """Print summary for a section.""" | |
| table = Table(show_header=True, header_style="bold magenta") | |
| table.add_column("Check", style="cyan", width=30) | |
| table.add_column("Status", justify="center", width=8) | |
| table.add_column("Message", width=50) | |
| for check in result.checks: | |
| status_str = check.status.value | |
| message = check.message | |
| if check.details and self.verbose: | |
| message += f"\n[dim]{check.details}[/dim]" | |
| if check.fix_available and check.fix_command: | |
| message += f"\n[yellow]Fix: {check.fix_command}[/yellow]" | |
| table.add_row(check.name, status_str, message) | |
| self.console.print(table) | |
| # Print summary | |
| summary = f"[bold]Summary:[/bold] {result.passed}/{result.total} passed" | |
| if result.failed > 0: | |
| summary += f", {result.failed} failed" | |
| if result.warned > 0: | |
| summary += f", {result.warned} warnings" | |
| success_rate = result.success_rate | |
| if success_rate == 100: | |
| color = "green" | |
| elif success_rate >= 80: | |
| color = "yellow" | |
| else: | |
| color = "red" | |
| self.console.print(f"\n{summary} ([{color}]{success_rate:.1f}% success rate[/{color}])\n") | |
| def print_final_report(self): | |
| """Print final verification report.""" | |
| self.console.print("\n[bold cyan]═══ FINAL VERIFICATION REPORT ═══[/bold cyan]\n") | |
| table = Table(show_header=True, header_style="bold magenta") | |
| table.add_column("Section", style="cyan", width=20) | |
| table.add_column("Passed", justify="center", width=10) | |
| table.add_column("Failed", justify="center", width=10) | |
| table.add_column("Warnings", justify="center", width=10) | |
| table.add_column("Success Rate", justify="center", width=15) | |
| total_passed = 0 | |
| total_failed = 0 | |
| total_warned = 0 | |
| total_checks = 0 | |
| for section_name, result in self.results.items(): | |
| total_passed += result.passed | |
| total_failed += result.failed | |
| total_warned += result.warned | |
| total_checks += result.total | |
| success_rate = result.success_rate | |
| if success_rate == 100: | |
| rate_str = f"[green]{success_rate:.1f}%[/green]" | |
| elif success_rate >= 80: | |
| rate_str = f"[yellow]{success_rate:.1f}%[/yellow]" | |
| else: | |
| rate_str = f"[red]{success_rate:.1f}%[/red]" | |
| table.add_row( | |
| section_name, | |
| str(result.passed), | |
| str(result.failed) if result.failed > 0 else "-", | |
| str(result.warned) if result.warned > 0 else "-", | |
| rate_str | |
| ) | |
| self.console.print(table) | |
| # Overall summary | |
| overall_rate = (total_passed / total_checks * 100) if total_checks > 0 else 0 | |
| if overall_rate == 100: | |
| status_emoji = "🎉" | |
| status_msg = "[bold green]READY TO LAUNCH![/bold green]" | |
| elif overall_rate >= 90: | |
| status_emoji = "✅" | |
| status_msg = "[bold yellow]ALMOST READY - Minor issues to fix[/bold yellow]" | |
| elif overall_rate >= 70: | |
| status_emoji = "⚠️" | |
| status_msg = "[bold yellow]NOT READY - Several issues to address[/bold yellow]" | |
| else: | |
| status_emoji = "❌" | |
| status_msg = "[bold red]NOT READY - Critical issues found[/bold red]" | |
| panel = Panel( | |
| f"{status_emoji} {status_msg}\n\n" | |
| f"Total Checks: {total_checks}\n" | |
| f"Passed: {total_passed}\n" | |
| f"Failed: {total_failed}\n" | |
| f"Warnings: {total_warned}\n" | |
| f"Overall Success Rate: {overall_rate:.1f}%", | |
| title="[bold]Launch Status[/bold]", | |
| border_style="cyan" | |
| ) | |
| self.console.print("\n", panel, "\n") | |
| # Print actionable items | |
| if total_failed > 0 or total_warned > 0: | |
| self.console.print("[bold yellow]Action Items:[/bold yellow]\n") | |
| for section_name, result in self.results.items(): | |
| fixable = [c for c in result.checks if c.fix_available and c.fix_command] | |
| if fixable: | |
| self.console.print(f"[cyan]{section_name}:[/cyan]") | |
| for check in fixable: | |
| self.console.print(f" • {check.name}: [yellow]{check.fix_command}[/yellow]") | |
| self.console.print() | |
| async def main(): | |
| """Main entry point.""" | |
| parser = argparse.ArgumentParser(description="AudioForge Launch Verification") | |
| parser.add_argument("--section", help="Run specific section only") | |
| parser.add_argument("--fix", action="store_true", help="Attempt to auto-fix issues") | |
| parser.add_argument("--verbose", action="store_true", help="Show detailed output") | |
| parser.add_argument("--json", help="Output results to JSON file") | |
| args = parser.parse_args() | |
| console = Console() | |
| console.print(Panel.fit( | |
| "[bold cyan]AudioForge Launch Verification[/bold cyan]\n" | |
| "Systematically verifying all launch checklist items", | |
| border_style="cyan" | |
| )) | |
| verifier = LaunchVerifier(console, fix_mode=args.fix, verbose=args.verbose) | |
| try: | |
| results = await verifier.verify_all() | |
| verifier.print_final_report() | |
| # Export to JSON if requested | |
| if args.json: | |
| output = { | |
| section: { | |
| "passed": result.passed, | |
| "failed": result.failed, | |
| "warned": result.warned, | |
| "total": result.total, | |
| "success_rate": result.success_rate, | |
| "checks": [ | |
| { | |
| "name": c.name, | |
| "status": c.status.name, | |
| "message": c.message, | |
| "details": c.details, | |
| } | |
| for c in result.checks | |
| ] | |
| } | |
| for section, result in results.items() | |
| } | |
| with open(args.json, 'w') as f: | |
| json.dump(output, f, indent=2) | |
| console.print(f"\n[green]Results exported to {args.json}[/green]") | |
| # Exit code based on failures | |
| total_failed = sum(r.failed for r in results.values()) | |
| sys.exit(0 if total_failed == 0 else 1) | |
| except KeyboardInterrupt: | |
| console.print("\n[yellow]Verification cancelled by user[/yellow]") | |
| sys.exit(130) | |
| except Exception as e: | |
| console.print(f"\n[red]Error during verification: {e}[/red]") | |
| if args.verbose: | |
| import traceback | |
| console.print(traceback.format_exc()) | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| asyncio.run(main()) | |