Add CLI modules: main, detect, manifest
Some checks failed
CI / test (push) Failing after 6s
CI / build (push) Has been skipped

This commit is contained in:
2026-02-04 20:03:53 +00:00
parent c3a7243bb9
commit abe7f17add

219
confsync/cli/manifest.py Normal file
View 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}")