Files
config-auditor-cli/config_auditor/cli.py
7000pctAUTO ad34da3bb7
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
Add config_auditor module files: cli, discovery, parsers
2026-01-30 18:02:16 +00:00

222 lines
6.6 KiB
Python

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()