Add CLI modules: main, detect, manifest
This commit is contained in:
219
confsync/cli/manifest.py
Normal file
219
confsync/cli/manifest.py
Normal file
@@ -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}")
|
||||||
Reference in New Issue
Block a user