diff --git a/confsync/cli/validate.py b/confsync/cli/validate.py new file mode 100644 index 0000000..be64b7f --- /dev/null +++ b/confsync/cli/validate.py @@ -0,0 +1,195 @@ +"""Validate command for configuration validation.""" + +from pathlib import Path +from typing import Optional +import typer +from rich.console import Console +from rich.table import Table +from rich.panel import Panel + +from confsync.detectors.base import DetectorRegistry +from confsync.core.validator import Validator +from confsync.models.config_models import ConfigCategory, ValidationResult + +validate_cmd = typer.Typer( + name="validate", + help="Validate configuration files", + no_args_is_help=True, +) + +console = Console() + + +@validate_cmd.command("all") +def validate_all( + category: Optional[str] = typer.Option( + None, "--category", "-c", + help="Filter by category" + ), + strict: bool = typer.Option(False, "--strict", "-s", help="Treat warnings as errors"), + quiet: bool = typer.Option(False, "--quiet", "-q", help="Only show issues"), +): + """Validate all detected configuration files.""" + categories = None + if category: + try: + categories = [ConfigCategory(category.lower())] + except ValueError: + console.print(f"[red]Error:[/red] Unknown category '{category}'") + return + + configs = DetectorRegistry.detect_all(categories) + + if not configs: + console.print("[yellow]No configuration files detected to validate.[/yellow]") + return + + validator = Validator() + result = validator.validate_manifest(configs) + + _display_validation_result(result, strict, quiet) + + +@validate_cmd.command("file") +def validate_file( + path: str = typer.Argument(..., help="Path to configuration file"), + strict: bool = typer.Option(False, "--strict", "-s", help="Treat warnings as errors"), +): + """Validate a specific configuration file.""" + from confsync.models.config_models import ConfigFile + + if not Path(path).exists(): + console.print(f"[red]Error:[/red] File not found: {path}") + return + + config = ConfigFile( + path=path, + name=Path(path).name, + category=ConfigCategory.MISC, + tool_name="unknown", + ) + + validator = Validator() + result = validator.validate_file(config) + + _display_validation_result(result, strict) + + +@validate_cmd.command("manifest") +def validate_manifest( + path: str = typer.Option( + "confsync_manifest.yaml", + "--path", "-p", + help="Path to manifest file" + ), + strict: bool = typer.Option(False, "--strict", "-s", help="Treat warnings as errors"), +): + """Validate configurations in a manifest file.""" + from confsync.core.manifest import ManifestBuilder + + if not Path(path).exists(): + console.print(f"[red]Error:[/red] Manifest file not found: {path}") + return + + builder = ManifestBuilder() + manifest = builder.load_manifest(path) + + configs = [entry.config_file for entry in manifest.entries.values()] + + validator = Validator() + result = validator.validate_manifest(configs) + + _display_validation_result(result, strict) + + +@validate_cmd.command("report") +def validate_report( + path: str = typer.Option( + "confsync_manifest.yaml", + "--path", "-p", + help="Path to manifest file" + ), + output: Optional[str] = typer.Option( + None, "--output", "-o", + help="Output file for report (default: print to stdout)" + ), +): + """Generate a detailed validation report.""" + from confsync.core.manifest import ManifestBuilder + + if not Path(path).exists(): + console.print(f"[red]Error:[/red] Manifest file not found: {path}") + return + + builder = ManifestBuilder() + manifest = builder.load_manifest(path) + + configs = [entry.config_file for entry in manifest.entries.values()] + + validator = Validator() + result = validator.validate_manifest(configs) + + report = validator.generate_report(result) + + if output: + Path(output).write_text(report) + console.print(f"[green]Report written to {output}[/green]") + else: + console.print(report) + + +def _display_validation_result(result: ValidationResult, strict: bool = False, quiet: bool = False): + """Display validation result.""" + if not quiet: + if result.is_valid: + console.print(Panel.fit( + "[bold green]Validation Passed[/bold green]", + style="green", + subtitle=f"Validated {result.validated_files} files", + )) + else: + console.print(Panel.fit( + "[bold yellow]Validation Issues Found[/bold yellow]", + style="yellow", + subtitle=f"Validated {result.validated_files} files, {len(result.issues)} issues", + )) + + if result.issues: + error_issues = [i for i in result.issues if i.severity.value == "error"] + warning_issues = [i for i in result.issues if i.severity.value == "warning"] + info_issues = [i for i in result.issues if i.severity.value == "info"] + + if error_issues or (strict and warning_issues): + if not quiet: + table = Table(title="Issues Found") + table.add_column("Severity", style="red") + table.add_column("Rule", style="cyan") + table.add_column("Message", style="white") + table.add_column("File", style="green") + + for issue in result.issues: + if strict and issue.severity.value == "warning": + severity_style = "red" + else: + severity_style = issue.severity.value + + table.add_row( + f"[{severity_style}]{issue.severity.value}[/{severity_style}]", + issue.rule, + issue.message[:80] + "..." if len(issue.message) > 80 else issue.message, + issue.file_path or "N/A", + ) + + console.print(table) + + if not quiet: + console.print("\n[bold]Summary:[/bold]") + console.print(f" Errors: {len(error_issues)}") + console.print(f" Warnings: {len(warning_issues)}") + console.print(f" Info: {len(info_issues)}") + else: + if not quiet: + console.print("[green]No issues found![/green]") + + if not result.is_valid and strict: + console.print("\n[yellow]Validation failed due to strict mode.[/yellow]")