diff --git a/confsync/cli/manifest.py b/confsync/cli/manifest.py new file mode 100644 index 0000000..1fd4c2e --- /dev/null +++ b/confsync/cli/manifest.py @@ -0,0 +1,219 @@ +"""Manifest command for managing configuration manifests.""" + +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 +import yaml + +from confsync.detectors.base import DetectorRegistry +from confsync.core.manifest import ManifestBuilder +from confsync.models.config_models import ConfigCategory + +manifest_cmd = typer.Typer( + name="manifest", + help="Manage configuration manifests", + no_args_is_help=True, +) + +console = Console() + + +@manifest_cmd.command("init") +def manifest_init( + output: str = typer.Option( + "confsync_manifest.yaml", + "--output", "-o", + help="Output file path for the manifest" + ), + category: Optional[str] = typer.Option( + None, "--category", "-c", + help="Filter by category" + ), + tool: Optional[str] = typer.Option( + None, "--tool", "-t", + help="Filter by tool name" + ), + exclude: str = typer.Option( + "", "--exclude", "-e", + help="Comma-separated patterns to exclude" + ), +): + """Initialize a new configuration manifest.""" + categories = None + if category: + try: + categories = [ConfigCategory(category.lower())] + except ValueError: + console.print(f"[red]Error:[/red] Unknown category '{category}'") + return + + exclude_patterns = [p.strip() for p in exclude.split(",") if p.strip()] + + console.print("[bold]Detecting configuration files...[/bold]") + configs = DetectorRegistry.detect_all(categories) + + if tool: + configs = [c for c in configs if c.tool_name.lower() == tool.lower()] + + if exclude_patterns: + exclude_set = set(exclude_patterns) + configs = [c for c in configs if not any(ep in c.path for ep in exclude_set)] + + builder = ManifestBuilder(ignore_patterns=exclude_patterns) + manifest = builder.build_from_detected( + configs, + metadata={"description": f"ConfSync manifest - {len(configs)} files", "categories": [c.value for c in categories] if categories else "all"} + ) + + builder.save_manifest(manifest, output) + + summary = builder.get_summary(manifest) + + console.print(Panel.fit( + "[bold]Manifest created successfully![/bold]", + style="green", + subtitle=f"Output: {output}", + )) + console.print(f"Total files: {summary['total_files']}") + console.print("\n[bold]By Category:[/bold]") + for cat, count in summary['by_category'].items(): + console.print(f" {cat}: {count}") + console.print("\n[bold]By Tool:[/bold]") + for tool_name, count in summary['by_tool'].items(): + console.print(f" {tool_name}: {count}") + + +@manifest_cmd.command("show") +def manifest_show( + path: str = typer.Option( + "confsync_manifest.yaml", + "--path", "-p", + help="Path to manifest file" + ), + format: str = typer.Option( + "table", "--format", "-f", + help="Output format (table, json, yaml)" + ), +): + """Display the contents of a manifest.""" + if not Path(path).exists(): + console.print(f"[red]Error:[/red] Manifest file not found: {path}") + return + + builder = ManifestBuilder() + manifest = builder.load_manifest(path) + + if format == "yaml": + console.print(yaml.dump(manifest.to_dict(), default_flow_style=False)) + elif format == "json": + import json + console.print(json.dumps(manifest.to_dict(), indent=2, default=str)) + else: + table = Table(title="Configuration Manifest") + table.add_column("ID", style="cyan", no_wrap=True) + table.add_column("Tool", style="magenta") + table.add_column("Category", style="green") + table.add_column("File Path", style="yellow") + table.add_column("Merge Strategy", style="blue") + table.add_column("Priority", justify="right", style="red") + + for entry_id, entry in manifest.entries.items(): + table.add_row( + entry_id[:8], + entry.config_file.tool_name, + entry.config_file.category.value, + entry.config_file.path, + entry.merge_strategy, + str(entry.priority), + ) + + console.print(Panel.fit( + f"[bold]Manifest Contents[/bold] - {len(manifest.entries)} files", + style="blue", + )) + console.print(table) + + +@manifest_cmd.command("export") +def manifest_export( + path: str = typer.Option( + "confsync_manifest.yaml", + "--path", "-p", + help="Path to manifest file" + ), + output: str = typer.Option( + "manifest_backup.zip", + "--output", "-o", + help="Output archive path" + ), +): + """Export manifest with configurations to an archive.""" + from zipfile import ZipFile + import json + + if not Path(path).exists(): + console.print(f"[red]Error:[/red] Manifest file not found: {path}") + return + + builder = ManifestBuilder() + manifest = builder.load_manifest(path) + + try: + with ZipFile(output, 'w') as zipf: + zipf.write(path, "manifest.yaml") + + for entry_id, entry in manifest.entries.items(): + config_path = entry.config_file.path + if Path(config_path).exists(): + arcname = f"configs/{Path(config_path).name}" + zipf.write(config_path, arcname) + + manifest_data = json.dumps({ + "manifest_version": manifest.version, + "files_count": len(manifest.entries), + "exported_at": str(manifest.created_at), + }, indent=2) + zipf.writestr("metadata.json", manifest_data) + + console.print(f"[green]Successfully exported manifest to {output}[/green]") + except Exception as e: + console.print(f"[red]Error exporting manifest:[/red] {e}") + + +@manifest_cmd.command("summary") +def manifest_summary( + path: str = typer.Option( + "confsync_manifest.yaml", + "--path", "-p", + help="Path to manifest file" + ), +): + """Show summary statistics of a manifest.""" + if not Path(path).exists(): + console.print(f"[red]Error:[/red] Manifest file not found: {path}") + return + + builder = ManifestBuilder() + manifest = builder.load_manifest(path) + + summary = builder.get_summary(manifest) + + console.print(Panel.fit( + "[bold]Manifest Summary[/bold]", + style="blue", + )) + console.print(f"Version: {manifest.version}") + console.print(f"Total Files: {summary['total_files']}") + console.print(f"Created: {manifest.created_at.isoformat()}") + console.print(f"Updated: {manifest.updated_at.isoformat()}") + + console.print("\n[bold]By Category:[/bold]") + for cat, count in summary['by_category'].items(): + console.print(f" {cat}: {count}") + + console.print("\n[bold]By Tool:[/bold]") + for tool_name, count in summary['by_tool'].items(): + console.print(f" {tool_name}: {count}")