220 lines
6.6 KiB
Python
220 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."""
|
|
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()
|