import sys from pathlib import Path from typing import Optional import click from config_auditor.discovery import ConfigDiscovery from config_auditor.parsers import ParserFactory from config_auditor.rules import RuleRegistry, Issue from config_auditor.report import ReportGenerator @click.group() @click.option("--path", "-p", default=".", help="Path to scan") @click.option("--format", "-f", type=click.Choice(["json", "yaml", "text"]), default="text", help="Output format") @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output") @click.pass_context def cli(ctx: click.Context, path: str, format: str, verbose: bool): ctx.ensure_object(dict) ctx.obj["path"] = Path(path) ctx.obj["format"] = format ctx.obj["verbose"] = verbose @cli.command() @click.pass_context def scan(ctx: click.Context): """Scan a directory for configuration files.""" path = ctx.obj["path"] verbose = ctx.obj.get("verbose", False) if not path.exists(): click.echo(f"Error: Path does not exist: {path}", err=True) sys.exit(2) if not path.is_dir(): click.echo(f"Error: Path is not a directory: {path}", err=True) sys.exit(2) discovery = ConfigDiscovery(max_depth=3) config_files = discovery.discover(path) if verbose: for cf in config_files: click.echo(f"Found: {cf.path} ({cf.format})") click.echo(f"Found {len(config_files)} configuration files") if ctx.obj["format"] == "json": import json result = [{"path": str(cf.path), "format": cf.format} for cf in config_files] click.echo(json.dumps(result, indent=2)) elif ctx.obj["format"] == "yaml": import yaml result = [{"path": str(cf.path), "format": cf.format} for cf in config_files] click.echo(yaml.dump(result)) else: for cf in config_files: click.echo(f" {cf.path} ({cf.format})") return 0 @cli.command() @click.option("--format", "-f", type=click.Choice(["json", "yaml", "text"]), default="text", help="Output format") @click.pass_context def audit(ctx: click.Context, format: str): """Audit configuration files for issues.""" path = ctx.obj["path"] format_output = format verbose = ctx.obj.get("verbose", False) if not path.exists(): click.echo(f"Error: Path does not exist: {path}", err=True) sys.exit(2) discovery = ConfigDiscovery(max_depth=3) config_files = discovery.discover(path) if not config_files: click.echo("No configuration files found", err=True) sys.exit(3) parser_factory = ParserFactory() rule_registry = RuleRegistry() issues: list[Issue] = [] for cf in config_files: try: content = cf.path.read_text() data = parser_factory.parse(cf.format, content) if data is None: continue file_issues = rule_registry.evaluate(cf.format, data, cf.path) issues.extend(file_issues) except Exception as e: if verbose: click.echo(f"Warning: Failed to parse {cf.path}: {e}") report_gen = ReportGenerator() if format_output == "json": output = report_gen.to_json(issues) click.echo(output) elif format_output == "yaml": output = report_gen.to_yaml(issues) click.echo(output) else: report_gen.to_text(issues, click.echo) critical_count = sum(1 for i in issues if i.severity == "critical") if critical_count > 0: sys.exit(4) elif issues: sys.exit(4) return 0 @cli.command() @click.option("--dry-run", is_flag=True, default=False, help="Preview changes without applying") @click.option("--force", is_flag=True, default=False, help="Skip confirmation") @click.pass_context def fix(ctx: click.Context, dry_run: bool, force: bool): """Automatically fix detected issues.""" path = ctx.obj["path"] verbose = ctx.obj.get("verbose", False) if not path.exists(): click.echo(f"Error: Path does not exist: {path}", err=True) sys.exit(2) discovery = ConfigDiscovery(max_depth=3) config_files = discovery.discover(path) if not config_files: click.echo("No configuration files found", err=True) sys.exit(3) from config_auditor.fixes import Fixer fixer = Fixer(dry_run=dry_run, force=force) fixed_count = 0 for cf in config_files: try: content = cf.path.read_text() fixes_applied = fixer.fix_config(cf.path, cf.format, content) if fixes_applied: fixed_count += fixes_applied if verbose: click.echo(f"Applied {fixes_applied} fixes to {cf.path}") except Exception as e: if verbose: click.echo(f"Warning: Failed to fix {cf.path}: {e}") if fixed_count > 0: click.echo(f"Applied {fixed_count} fixes") return 0 else: click.echo("No fixes applied") return 0 @cli.command() @click.option("--template", "-t", type=click.Choice(["node", "python", "typescript"]), help="Template type") @click.pass_context def generate(ctx: click.Context, template: Optional[str]): """Generate optimal configurations.""" path = ctx.obj["path"] if not path.exists(): click.echo(f"Error: Path does not exist: {path}", err=True) sys.exit(2) from config_auditor.generate import ConfigGenerator generator = ConfigGenerator() if template: config = generator.generate_from_template(template, path) output = ctx.obj["format"] if output == "json": import json click.echo(json.dumps(config, indent=2)) elif output == "yaml": import yaml click.echo(yaml.dump(config)) else: import json click.echo(json.dumps(config, indent=2)) else: project_type = generator.detect_project_type(path) if project_type: config = generator.generate_from_template(project_type, path) import json click.echo(json.dumps(config, indent=2)) else: click.echo("Could not detect project type. Use --template to specify.", err=True) sys.exit(2) return 0 @cli.command() @click.pass_context def config(ctx: click Context): """Show current configuration.""" import yaml config_path = Path("config.yaml") if config_path.exists(): content = config_path.read_text() click.echo(content) else: click.echo("No config.yaml found in current directory") def main(): cli(obj={}) if __name__ == "__main__": main()