OnyxMunk/AudioForge
0
1#!/usr/bin/env python3
2"""
3AudioForge Launch Verification Script
4Systematically verifies all items in LAUNCH_CHECKLIST.md
5
6Usage:
7 python scripts/launch_verification.py [--section SECTION] [--fix]
8
9Options:
10 --section SECTION Run specific section only (backend, frontend, security, etc.)
11 --fix Attempt to auto-fix issues where possible
12 --verbose Show detailed output
13"""
14
15import argparse
16import asyncio
17import json
18import os
19import subprocess
20import sys
21import time
22from dataclasses import dataclass, field
23from enum import Enum
24from pathlib import Path
25from typing import Dict, List, Optional, Tuple
26from urllib.parse import urlparse
27
28try:
29 import httpx
30 import psutil
31 from rich.console import Console
32 from rich.progress import Progress, SpinnerColumn, TextColumn
33 from rich.table import Table
34 from rich.panel import Panel
35except ImportError:
36 print("Installing required packages...")
37 subprocess.run([sys.executable, "-m", "pip", "install", "httpx", "psutil", "rich"], check=True)
38 import httpx
39 import psutil
40 from rich.console import Console
41 from rich.progress import Progress, SpinnerColumn, TextColumn
42 from rich.table import Table
43 from rich.panel import Panel
44
45
46class CheckStatus(Enum):
47 """Status of a verification check."""
48 PASS = "✅"
49 FAIL = "❌"
50 WARN = "⚠️"
51 SKIP = "⏭️"
52 INFO = "ℹ️"
53
54
55@dataclass
56class CheckResult:
57 """Result of a single verification check."""
58 name: str
59 status: CheckStatus
60 message: str
61 details: Optional[str] = None
62 fix_available: bool = False
63 fix_command: Optional[str] = None
64
65
66@dataclass
67class SectionResult:
68 """Result of a verification section."""
69 name: str
70 checks: List[CheckResult] = field(default_factory=list)
71
72 @property
73 def passed(self) -> int:
74 return sum(1 for c in self.checks if c.status == CheckStatus.PASS)
75
76 @property
77 def failed(self) -> int:
78 return sum(1 for c in self.checks if c.status == CheckStatus.FAIL)
79
80 @property
81 def warned(self) -> int:
82 return sum(1 for c in self.checks if c.status == CheckStatus.WARN)
83
84 @property
85 def total(self) -> int:
86 return len(self.checks)
87
88 @property
89 def success_rate(self) -> float:
90 if self.total == 0:
91 return 0.0
92 return (self.passed / self.total) * 100
93
94
95class LaunchVerifier:
96 """Main verification orchestrator."""
97
98 def __init__(self, console: Console, fix_mode: bool = False, verbose: bool = False):
99 self.console = console
100 self.fix_mode = fix_mode
101 self.verbose = verbose
102 self.root_path = Path(__file__).parent.parent
103 self.results: Dict[str, SectionResult] = {}
104
105 async def verify_all(self) -> Dict[str, SectionResult]:
106 """Run all verification checks."""
107 sections = [
108 ("Backend", self.verify_backend),
109 ("Frontend", self.verify_frontend),
110 ("UI/UX", self.verify_ui_ux),
111 ("Integration", self.verify_integration),
112 ("Performance", self.verify_performance),
113 ("Security", self.verify_security),
114 ("Documentation", self.verify_documentation),
115 ]
116
117 for section_name, verify_func in sections:
118 self.console.print(f"\n[bold cyan]═══ {section_name} Verification ═══[/bold cyan]\n")
119 result = await verify_func()
120 self.results[section_name] = result
121 self._print_section_summary(result)
122
123 return self.results
124
125 async def verify_backend(self) -> SectionResult:
126 """Verify backend setup and health."""
127 result = SectionResult(name="Backend")
128
129 # Check Python version
130 python_version = sys.version_info
131 if python_version >= (3, 11):
132 result.checks.append(CheckResult(
133 name="Python Version",
134 status=CheckStatus.PASS,
135 message=f"Python {python_version.major}.{python_version.minor}.{python_version.micro}"
136 ))
137 else:
138 result.checks.append(CheckResult(
139 name="Python Version",
140 status=CheckStatus.FAIL,
141 message=f"Python 3.11+ required, found {python_version.major}.{python_version.minor}",
142 fix_available=False
143 ))
144
145 # Check .env file
146 env_path = self.root_path / "backend" / ".env"
147 env_example = self.root_path / "backend" / ".env.example"
148
149 if env_path.exists():
150 result.checks.append(CheckResult(
151 name="Environment File",
152 status=CheckStatus.PASS,
153 message=".env file exists"
154 ))
155 elif env_example.exists() and self.fix_mode:
156 import shutil
157 shutil.copy(env_example, env_path)
158 result.checks.append(CheckResult(
159 name="Environment File",
160 status=CheckStatus.PASS,
161 message=".env created from .env.example"
162 ))
163 else:
164 result.checks.append(CheckResult(
165 name="Environment File",
166 status=CheckStatus.FAIL,
167 message=".env file missing",
168 fix_available=True,
169 fix_command="cp backend/.env.example backend/.env"
170 ))
171
172 # Check backend dependencies
173 backend_path = self.root_path / "backend"
174 pyproject_path = backend_path / "pyproject.toml"
175
176 if pyproject_path.exists():
177 result.checks.append(CheckResult(
178 name="Backend Package Config",
179 status=CheckStatus.PASS,
180 message="pyproject.toml found"
181 ))
182 else:
183 result.checks.append(CheckResult(
184 name="Backend Package Config",
185 status=CheckStatus.FAIL,
186 message="pyproject.toml missing"
187 ))
188
189 # Check if backend is running
190 try:
191 async with httpx.AsyncClient(timeout=5.0) as client:
192 response = await client.get("http://localhost:8000/health")
193 if response.status_code == 200:
194 result.checks.append(CheckResult(
195 name="Backend Health Check",
196 status=CheckStatus.PASS,
197 message="Backend responding on port 8000",
198 details=f"Response: {response.json()}"
199 ))
200 else:
201 result.checks.append(CheckResult(
202 name="Backend Health Check",
203 status=CheckStatus.WARN,
204 message=f"Backend returned status {response.status_code}"
205 ))
206 except Exception as e:
207 result.checks.append(CheckResult(
208 name="Backend Health Check",
209 status=CheckStatus.WARN,
210 message="Backend not running or not accessible",
211 details=str(e),
212 fix_command="cd backend && uvicorn app.main:app --reload"
213 ))
214
215 # Check API documentation
216 try:
217 async with httpx.AsyncClient(timeout=5.0) as client:
218 response = await client.get("http://localhost:8000/docs")
219 if response.status_code == 200:
220 result.checks.append(CheckResult(
221 name="API Documentation",
222 status=CheckStatus.PASS,
223 message="API docs accessible at /docs"
224 ))
225 else:
226 result.checks.append(CheckResult(
227 name="API Documentation",
228 status=CheckStatus.WARN,
229 message="API docs not accessible"
230 ))
231 except Exception:
232 result.checks.append(CheckResult(
233 name="API Documentation",
234 status=CheckStatus.SKIP,
235 message="Backend not running"
236 ))
237
238 # Check storage directories
239 storage_path = backend_path / "storage" / "audio"
240 required_dirs = ["music", "vocals", "mixed", "mastered"]
241 missing_dirs = [d for d in required_dirs if not (storage_path / d).exists()]
242
243 if not missing_dirs:
244 result.checks.append(CheckResult(
245 name="Storage Directories",
246 status=CheckStatus.PASS,
247 message="All storage directories exist"
248 ))
249 elif self.fix_mode:
250 for dir_name in missing_dirs:
251 (storage_path / dir_name).mkdir(parents=True, exist_ok=True)
252 result.checks.append(CheckResult(
253 name="Storage Directories",
254 status=CheckStatus.PASS,
255 message=f"Created missing directories: {', '.join(missing_dirs)}"
256 ))
257 else:
258 result.checks.append(CheckResult(
259 name="Storage Directories",
260 status=CheckStatus.WARN,
261 message=f"Missing directories: {', '.join(missing_dirs)}",
262 fix_available=True
263 ))
264
265 return result
266
267 async def verify_frontend(self) -> SectionResult:
268 """Verify frontend setup and build."""
269 result = SectionResult(name="Frontend")
270 frontend_path = self.root_path / "frontend"
271
272 # Check package.json
273 package_json = frontend_path / "package.json"
274 if package_json.exists():
275 result.checks.append(CheckResult(
276 name="Package Configuration",
277 status=CheckStatus.PASS,
278 message="package.json found"
279 ))
280
281 # Check if node_modules exists
282 node_modules = frontend_path / "node_modules"
283 if node_modules.exists():
284 result.checks.append(CheckResult(
285 name="Dependencies Installed",
286 status=CheckStatus.PASS,
287 message="node_modules directory exists"
288 ))
289 else:
290 result.checks.append(CheckResult(
291 name="Dependencies Installed",
292 status=CheckStatus.FAIL,
293 message="node_modules missing",
294 fix_available=True,
295 fix_command="cd frontend && pnpm install"
296 ))
297 else:
298 result.checks.append(CheckResult(
299 name="Package Configuration",
300 status=CheckStatus.FAIL,
301 message="package.json missing"
302 ))
303
304 # Check .env.local
305 env_local = frontend_path / ".env.local"
306 if env_local.exists():
307 result.checks.append(CheckResult(
308 name="Environment Configuration",
309 status=CheckStatus.PASS,
310 message=".env.local exists"
311 ))
312 else:
313 result.checks.append(CheckResult(
314 name="Environment Configuration",
315 status=CheckStatus.WARN,
316 message=".env.local missing",
317 fix_available=True,
318 fix_command='echo "NEXT_PUBLIC_API_URL=http://localhost:8000" > frontend/.env.local'
319 ))
320
321 # Check if frontend is running
322 try:
323 async with httpx.AsyncClient(timeout=5.0) as client:
324 response = await client.get("http://localhost:3000")
325 if response.status_code == 200:
326 result.checks.append(CheckResult(
327 name="Frontend Server",
328 status=CheckStatus.PASS,
329 message="Frontend responding on port 3000"
330 ))
331 else:
332 result.checks.append(CheckResult(
333 name="Frontend Server",
334 status=CheckStatus.WARN,
335 message=f"Frontend returned status {response.status_code}"
336 ))
337 except Exception as e:
338 result.checks.append(CheckResult(
339 name="Frontend Server",
340 status=CheckStatus.WARN,
341 message="Frontend not running",
342 details=str(e),
343 fix_command="cd frontend && pnpm dev"
344 ))
345
346 # Check TypeScript configuration
347 tsconfig = frontend_path / "tsconfig.json"
348 if tsconfig.exists():
349 result.checks.append(CheckResult(
350 name="TypeScript Configuration",
351 status=CheckStatus.PASS,
352 message="tsconfig.json found"
353 ))
354 else:
355 result.checks.append(CheckResult(
356 name="TypeScript Configuration",
357 status=CheckStatus.FAIL,
358 message="tsconfig.json missing"
359 ))
360
361 # Check for TypeScript errors (if tsc is available)
362 try:
363 proc = subprocess.run(
364 ["pnpm", "tsc", "--noEmit"],
365 cwd=frontend_path,
366 capture_output=True,
367 text=True,
368 timeout=30
369 )
370 if proc.returncode == 0:
371 result.checks.append(CheckResult(
372 name="TypeScript Compilation",
373 status=CheckStatus.PASS,
374 message="No TypeScript errors"
375 ))
376 else:
377 error_lines = proc.stdout.count('\n')
378 result.checks.append(CheckResult(
379 name="TypeScript Compilation",
380 status=CheckStatus.FAIL,
381 message=f"TypeScript errors found ({error_lines} lines)",
382 details=proc.stdout[:500]
383 ))
384 except FileNotFoundError:
385 result.checks.append(CheckResult(
386 name="TypeScript Compilation",
387 status=CheckStatus.SKIP,
388 message="pnpm not found"
389 ))
390 except Exception as e:
391 result.checks.append(CheckResult(
392 name="TypeScript Compilation",
393 status=CheckStatus.SKIP,
394 message=f"Could not run tsc: {str(e)}"
395 ))
396
397 return result
398
399 async def verify_ui_ux(self) -> SectionResult:
400 """Verify UI/UX enhancements are working."""
401 result = SectionResult(name="UI/UX")
402 frontend_path = self.root_path / "frontend" / "src"
403
404 # Check for new components
405 components_to_check = [
406 "sound-wave-background.tsx",
407 "floating-notes.tsx",
408 "prompt-suggestions.tsx",
409 "mini-visualizer.tsx",
410 "footer-stats.tsx",
411 "keyboard-shortcuts.tsx",
412 "confetti-effect.tsx",
413 ]
414
415 components_path = frontend_path / "components"
416 missing_components = []
417
418 for component in components_to_check:
419 if not (components_path / component).exists():
420 missing_components.append(component)
421
422 if not missing_components:
423 result.checks.append(CheckResult(
424 name="Creative Components",
425 status=CheckStatus.PASS,
426 message=f"All {len(components_to_check)} creative components present"
427 ))
428 else:
429 result.checks.append(CheckResult(
430 name="Creative Components",
431 status=CheckStatus.WARN,
432 message=f"Missing {len(missing_components)} components",
433 details=", ".join(missing_components)
434 ))
435
436 # Check globals.css for animations
437 globals_css = frontend_path / "app" / "globals.css"
438 if globals_css.exists():
439 content = globals_css.read_text()
440 animations = [
441 "fade-in",
442 "slide-in-left",
443 "slide-in-right",
444 "gradient",
445 "pulse-glow",
446 "bounce-subtle",
447 "float-up",
448 "confetti-fall"
449 ]
450 missing_animations = [a for a in animations if f"@keyframes {a}" not in content]
451
452 if not missing_animations:
453 result.checks.append(CheckResult(
454 name="CSS Animations",
455 status=CheckStatus.PASS,
456 message=f"All {len(animations)} animations defined"
457 ))
458 else:
459 result.checks.append(CheckResult(
460 name="CSS Animations",
461 status=CheckStatus.WARN,
462 message=f"Missing {len(missing_animations)} animations",
463 details=", ".join(missing_animations)
464 ))
465 else:
466 result.checks.append(CheckResult(
467 name="CSS Animations",
468 status=CheckStatus.FAIL,
469 message="globals.css not found"
470 ))
471
472 # Check tailwind config for font support
473 tailwind_config = self.root_path / "frontend" / "tailwind.config.ts"
474 if tailwind_config.exists():
475 content = tailwind_config.read_text()
476 if "fontFamily" in content and "display" in content:
477 result.checks.append(CheckResult(
478 name="Typography Configuration",
479 status=CheckStatus.PASS,
480 message="Display font configured in Tailwind"
481 ))
482 else:
483 result.checks.append(CheckResult(
484 name="Typography Configuration",
485 status=CheckStatus.WARN,
486 message="Display font may not be configured"
487 ))
488
489 return result
490
491 async def verify_integration(self) -> SectionResult:
492 """Verify integration between frontend and backend."""
493 result = SectionResult(name="Integration")
494
495 # Check if both services are running
496 backend_running = False
497 frontend_running = False
498
499 try:
500 async with httpx.AsyncClient(timeout=5.0) as client:
501 await client.get("http://localhost:8000/health")
502 backend_running = True
503 except Exception:
504 pass
505
506 try:
507 async with httpx.AsyncClient(timeout=5.0) as client:
508 await client.get("http://localhost:3000")
509 frontend_running = True
510 except Exception:
511 pass
512
513 if backend_running and frontend_running:
514 result.checks.append(CheckResult(
515 name="Services Running",
516 status=CheckStatus.PASS,
517 message="Both frontend and backend are running"
518 ))
519
520 # Test API endpoint from frontend perspective
521 try:
522 async with httpx.AsyncClient(timeout=10.0) as client:
523 # Test generations list endpoint
524 response = await client.get("http://localhost:8000/api/v1/generations")
525 if response.status_code in [200, 404]: # 404 is ok if no generations yet
526 result.checks.append(CheckResult(
527 name="API Endpoints",
528 status=CheckStatus.PASS,
529 message="Generations API endpoint accessible"
530 ))
531 else:
532 result.checks.append(CheckResult(
533 name="API Endpoints",
534 status=CheckStatus.WARN,
535 message=f"Unexpected status code: {response.status_code}"
536 ))
537 except Exception as e:
538 result.checks.append(CheckResult(
539 name="API Endpoints",
540 status=CheckStatus.FAIL,
541 message="Could not access API endpoints",
542 details=str(e)
543 ))
544 else:
545 services_status = []
546 if not backend_running:
547 services_status.append("backend")
548 if not frontend_running:
549 services_status.append("frontend")
550
551 result.checks.append(CheckResult(
552 name="Services Running",
553 status=CheckStatus.FAIL,
554 message=f"Services not running: {', '.join(services_status)}",
555 fix_command="docker-compose up -d"
556 ))
557
558 return result
559
560 async def verify_performance(self) -> SectionResult:
561 """Verify performance metrics."""
562 result = SectionResult(name="Performance")
563
564 # Check backend response time
565 try:
566 async with httpx.AsyncClient(timeout=5.0) as client:
567 start = time.time()
568 response = await client.get("http://localhost:8000/health")
569 duration = (time.time() - start) * 1000 # Convert to ms
570
571 if response.status_code == 200 and duration < 200:
572 result.checks.append(CheckResult(
573 name="Backend Response Time",
574 status=CheckStatus.PASS,
575 message=f"Health check: {duration:.0f}ms (< 200ms target)"
576 ))
577 elif duration < 500:
578 result.checks.append(CheckResult(
579 name="Backend Response Time",
580 status=CheckStatus.WARN,
581 message=f"Health check: {duration:.0f}ms (target: < 200ms)"
582 ))
583 else:
584 result.checks.append(CheckResult(
585 name="Backend Response Time",
586 status=CheckStatus.FAIL,
587 message=f"Health check: {duration:.0f}ms (too slow)"
588 ))
589 except Exception:
590 result.checks.append(CheckResult(
591 name="Backend Response Time",
592 status=CheckStatus.SKIP,
593 message="Backend not running"
594 ))
595
596 # Check system resources
597 cpu_percent = psutil.cpu_percent(interval=1)
598 memory = psutil.virtual_memory()
599
600 if cpu_percent < 80:
601 result.checks.append(CheckResult(
602 name="CPU Usage",
603 status=CheckStatus.PASS,
604 message=f"CPU: {cpu_percent:.1f}% (healthy)"
605 ))
606 else:
607 result.checks.append(CheckResult(
608 name="CPU Usage",
609 status=CheckStatus.WARN,
610 message=f"CPU: {cpu_percent:.1f}% (high)"
611 ))
612
613 if memory.percent < 80:
614 result.checks.append(CheckResult(
615 name="Memory Usage",
616 status=CheckStatus.PASS,
617 message=f"Memory: {memory.percent:.1f}% (healthy)"
618 ))
619 else:
620 result.checks.append(CheckResult(
621 name="Memory Usage",
622 status=CheckStatus.WARN,
623 message=f"Memory: {memory.percent:.1f}% (high)"
624 ))
625
626 return result
627
628 async def verify_security(self) -> SectionResult:
629 """Verify security configurations."""
630 result = SectionResult(name="Security")
631
632 # Check for .env in .gitignore
633 gitignore = self.root_path / ".gitignore"
634 if gitignore.exists():
635 content = gitignore.read_text()
636 if ".env" in content:
637 result.checks.append(CheckResult(
638 name="Environment Files Protected",
639 status=CheckStatus.PASS,
640 message=".env files in .gitignore"
641 ))
642 else:
643 result.checks.append(CheckResult(
644 name="Environment Files Protected",
645 status=CheckStatus.FAIL,
646 message=".env not in .gitignore",
647 fix_available=True
648 ))
649
650 # Check for exposed secrets in frontend
651 frontend_env = self.root_path / "frontend" / ".env.local"
652 if frontend_env.exists():
653 content = frontend_env.read_text()
654 dangerous_keys = ["SECRET", "PRIVATE", "KEY", "PASSWORD"]
655 exposed = [key for key in dangerous_keys if key in content.upper() and "NEXT_PUBLIC" not in content]
656
657 if not exposed:
658 result.checks.append(CheckResult(
659 name="Frontend Secrets",
660 status=CheckStatus.PASS,
661 message="No exposed secrets in .env.local"
662 ))
663 else:
664 result.checks.append(CheckResult(
665 name="Frontend Secrets",
666 status=CheckStatus.WARN,
667 message=f"Potential secrets found: {', '.join(exposed)}"
668 ))
669
670 # Check CORS configuration (if backend is running)
671 try:
672 async with httpx.AsyncClient(timeout=5.0) as client:
673 response = await client.options(
674 "http://localhost:8000/api/v1/generations",
675 headers={"Origin": "http://localhost:3000"}
676 )
677 if "access-control-allow-origin" in response.headers:
678 result.checks.append(CheckResult(
679 name="CORS Configuration",
680 status=CheckStatus.PASS,
681 message="CORS headers present"
682 ))
683 else:
684 result.checks.append(CheckResult(
685 name="CORS Configuration",
686 status=CheckStatus.WARN,
687 message="CORS headers not found"
688 ))
689 except Exception:
690 result.checks.append(CheckResult(
691 name="CORS Configuration",
692 status=CheckStatus.SKIP,
693 message="Backend not running"
694 ))
695
696 return result
697
698 async def verify_documentation(self) -> SectionResult:
699 """Verify documentation completeness."""
700 result = SectionResult(name="Documentation")
701
702 required_docs = {
703 "README.md": "Main documentation",
704 "SETUP.md": "Setup instructions",
705 "ARCHITECTURE.md": "Architecture overview",
706 "CONTRIBUTING.md": "Contribution guidelines",
707 "LAUNCH_CHECKLIST.md": "Launch checklist",
708 }
709
710 missing_docs = []
711 for doc_file, description in required_docs.items():
712 doc_path = self.root_path / doc_file
713 if doc_path.exists():
714 size = doc_path.stat().st_size
715 if size > 100: # At least 100 bytes
716 continue
717 missing_docs.append(f"{doc_file} ({description})")
718
719 if not missing_docs:
720 result.checks.append(CheckResult(
721 name="Required Documentation",
722 status=CheckStatus.PASS,
723 message=f"All {len(required_docs)} documentation files present"
724 ))
725 else:
726 result.checks.append(CheckResult(
727 name="Required Documentation",
728 status=CheckStatus.WARN,
729 message=f"Missing or incomplete: {len(missing_docs)} files",
730 details="\n".join(missing_docs)
731 ))
732
733 # Check for LICENSE
734 license_file = self.root_path / "LICENSE"
735 if license_file.exists():
736 result.checks.append(CheckResult(
737 name="License File",
738 status=CheckStatus.PASS,
739 message="LICENSE file present"
740 ))
741 else:
742 result.checks.append(CheckResult(
743 name="License File",
744 status=CheckStatus.WARN,
745 message="LICENSE file missing"
746 ))
747
748 return result
749
750 def _print_section_summary(self, result: SectionResult):
751 """Print summary for a section."""
752 table = Table(show_header=True, header_style="bold magenta")
753 table.add_column("Check", style="cyan", width=30)
754 table.add_column("Status", justify="center", width=8)
755 table.add_column("Message", width=50)
756
757 for check in result.checks:
758 status_str = check.status.value
759 message = check.message
760 if check.details and self.verbose:
761 message += f"\n[dim]{check.details}[/dim]"
762 if check.fix_available and check.fix_command:
763 message += f"\n[yellow]Fix: {check.fix_command}[/yellow]"
764
765 table.add_row(check.name, status_str, message)
766
767 self.console.print(table)
768
769 # Print summary
770 summary = f"[bold]Summary:[/bold] {result.passed}/{result.total} passed"
771 if result.failed > 0:
772 summary += f", {result.failed} failed"
773 if result.warned > 0:
774 summary += f", {result.warned} warnings"
775
776 success_rate = result.success_rate
777 if success_rate == 100:
778 color = "green"
779 elif success_rate >= 80:
780 color = "yellow"
781 else:
782 color = "red"
783
784 self.console.print(f"\n{summary} ([{color}]{success_rate:.1f}% success rate[/{color}])\n")
785
786 def print_final_report(self):
787 """Print final verification report."""
788 self.console.print("\n[bold cyan]═══ FINAL VERIFICATION REPORT ═══[/bold cyan]\n")
789
790 table = Table(show_header=True, header_style="bold magenta")
791 table.add_column("Section", style="cyan", width=20)
792 table.add_column("Passed", justify="center", width=10)
793 table.add_column("Failed", justify="center", width=10)
794 table.add_column("Warnings", justify="center", width=10)
795 table.add_column("Success Rate", justify="center", width=15)
796
797 total_passed = 0
798 total_failed = 0
799 total_warned = 0
800 total_checks = 0
801
802 for section_name, result in self.results.items():
803 total_passed += result.passed
804 total_failed += result.failed
805 total_warned += result.warned
806 total_checks += result.total
807
808 success_rate = result.success_rate
809 if success_rate == 100:
810 rate_str = f"[green]{success_rate:.1f}%[/green]"
811 elif success_rate >= 80:
812 rate_str = f"[yellow]{success_rate:.1f}%[/yellow]"
813 else:
814 rate_str = f"[red]{success_rate:.1f}%[/red]"
815
816 table.add_row(
817 section_name,
818 str(result.passed),
819 str(result.failed) if result.failed > 0 else "-",
820 str(result.warned) if result.warned > 0 else "-",
821 rate_str
822 )
823
824 self.console.print(table)
825
826 # Overall summary
827 overall_rate = (total_passed / total_checks * 100) if total_checks > 0 else 0
828
829 if overall_rate == 100:
830 status_emoji = "🎉"
831 status_msg = "[bold green]READY TO LAUNCH![/bold green]"
832 elif overall_rate >= 90:
833 status_emoji = "✅"
834 status_msg = "[bold yellow]ALMOST READY - Minor issues to fix[/bold yellow]"
835 elif overall_rate >= 70:
836 status_emoji = "⚠️"
837 status_msg = "[bold yellow]NOT READY - Several issues to address[/bold yellow]"
838 else:
839 status_emoji = "❌"
840 status_msg = "[bold red]NOT READY - Critical issues found[/bold red]"
841
842 panel = Panel(
843 f"{status_emoji} {status_msg}\n\n"
844 f"Total Checks: {total_checks}\n"
845 f"Passed: {total_passed}\n"
846 f"Failed: {total_failed}\n"
847 f"Warnings: {total_warned}\n"
848 f"Overall Success Rate: {overall_rate:.1f}%",
849 title="[bold]Launch Status[/bold]",
850 border_style="cyan"
851 )
852
853 self.console.print("\n", panel, "\n")
854
855 # Print actionable items
856 if total_failed > 0 or total_warned > 0:
857 self.console.print("[bold yellow]Action Items:[/bold yellow]\n")
858 for section_name, result in self.results.items():
859 fixable = [c for c in result.checks if c.fix_available and c.fix_command]
860 if fixable:
861 self.console.print(f"[cyan]{section_name}:[/cyan]")
862 for check in fixable:
863 self.console.print(f" • {check.name}: [yellow]{check.fix_command}[/yellow]")
864 self.console.print()
865
866
867async def main():
868 """Main entry point."""
869 parser = argparse.ArgumentParser(description="AudioForge Launch Verification")
870 parser.add_argument("--section", help="Run specific section only")
871 parser.add_argument("--fix", action="store_true", help="Attempt to auto-fix issues")
872 parser.add_argument("--verbose", action="store_true", help="Show detailed output")
873 parser.add_argument("--json", help="Output results to JSON file")
874
875 args = parser.parse_args()
876
877 console = Console()
878 console.print(Panel.fit(
879 "[bold cyan]AudioForge Launch Verification[/bold cyan]\n"
880 "Systematically verifying all launch checklist items",
881 border_style="cyan"
882 ))
883
884 verifier = LaunchVerifier(console, fix_mode=args.fix, verbose=args.verbose)
885
886 try:
887 results = await verifier.verify_all()
888 verifier.print_final_report()
889
890 # Export to JSON if requested
891 if args.json:
892 output = {
893 section: {
894 "passed": result.passed,
895 "failed": result.failed,
896 "warned": result.warned,
897 "total": result.total,
898 "success_rate": result.success_rate,
899 "checks": [
900 {
901 "name": c.name,
902 "status": c.status.name,
903 "message": c.message,
904 "details": c.details,
905 }
906 for c in result.checks
907 ]
908 }
909 for section, result in results.items()
910 }
911
912 with open(args.json, 'w') as f:
913 json.dump(output, f, indent=2)
914 console.print(f"\n[green]Results exported to {args.json}[/green]")
915
916 # Exit code based on failures
917 total_failed = sum(r.failed for r in results.values())
918 sys.exit(0 if total_failed == 0 else 1)
919
920 except KeyboardInterrupt:
921 console.print("\n[yellow]Verification cancelled by user[/yellow]")
922 sys.exit(130)
923 except Exception as e:
924 console.print(f"\n[red]Error during verification: {e}[/red]")
925 if args.verbose:
926 import traceback
927 console.print(traceback.format_exc())
928 sys.exit(1)
929
930
931if __name__ == "__main__":
932 asyncio.run(main())
933 