diff --git a/errorfix/cli.py b/errorfix/cli.py new file mode 100644 index 0000000..3430cb2 --- /dev/null +++ b/errorfix/cli.py @@ -0,0 +1,149 @@ +import sys +import os +from typing import Optional, List, Tuple + +import click + +from errorfix.rules import RuleLoader +from errorfix.patterns import PatternMatcher +from errorfix.formatters import TextFormatter, JSONFormatter, StructuredFormatter +from errorfix.plugins import PluginLoader + + +def get_rule_loader() -> RuleLoader: + return RuleLoader() + + +def get_pattern_matcher() -> PatternMatcher: + return PatternMatcher() + + +def get_plugin_loader() -> PluginLoader: + return PluginLoader() + + +@click.group() +@click.option('--rules-path', '-r', multiple=True, help='Path to rule files or directories') +@click.option('--plugin', '-p', multiple=True, help='Plugin modules to load') +@click.option('--verbose', '-v', is_flag=True, help='Enable verbose output') +@click.pass_context +def cli(ctx: click.Context, rules_path: Tuple[str], plugin: Tuple[str], verbose: bool): + ctx.ensure_object(dict) + ctx.obj['rules_path'] = list(rules_path) + ctx.obj['plugins'] = list(plugin) + ctx.obj['verbose'] = verbose + + +@cli.command() +@click.option('--input', '-i', 'error_input', type=click.File('r'), default='-', help='Input file or stdin') +@click.option('--output-format', '-f', type=click.Choice(['text', 'json', 'structured']), default='text', help='Output format') +@click.option('--language', '-l', help='Filter rules by language') +@click.option('--tool', '-t', help='Filter rules by tool') +@click.option('--limit', type=int, default=None, help='Limit number of matches') +@click.option('--no-color', is_flag=True, help='Disable colored output') +@click.option('--rules', '-r', multiple=True, help='Additional rule paths') +@click.pass_context +def fix( + ctx: click.Context, + error_input: click.File, + output_format: str, + language: Optional[str], + tool: Optional[str], + limit: Optional[int], + no_color: bool, + rules: Tuple[str] +): + if error_input == sys.stdin and hasattr(sys.stdin, 'closed') and sys.stdin.closed: + error_text = '' + else: + error_text = error_input.read() if error_input and error_input != '-' else '' + + rule_loader = get_rule_loader() + matcher = get_pattern_matcher() + plugin_loader = get_plugin_loader() + + all_rules_paths = list(ctx.obj.get('rules_path', [])) + list(rules) + + rules_list = [] + for path in all_rules_paths: + try: + if os.path.exists(path): + rules_list.extend(rule_loader.load_multiple([path])) + except Exception as e: + if ctx.obj.get('verbose'): + click.echo(f"Warning: Failed to load rules from {path}: {e}", err=True) + + for plugin_name in ctx.obj.get('plugins', []): + try: + plugin_loader.load_plugin_module(plugin_name) + plugin_rules = plugin_loader.get_registry().get_all_rules() + from errorfix.rules import Rule + rules_list.extend([Rule.from_dict(r) for r in plugin_rules]) + except Exception as e: + click.echo(f"Warning: Failed to load plugin {plugin_name}: {e}", err=True) + + if not rules_list: + default_rules_path = os.environ.get('ERRORFIX_RULES_PATH', 'rules') + if os.path.exists(default_rules_path): + try: + rules_list = rule_loader.load_directory(default_rules_path) + except Exception: + pass + + if language: + rules_list = rule_loader.filter_rules(rules_list, language=language) + if tool: + rules_list = rule_loader.filter_rules(rules_list, tool=tool) + + matches = matcher.match_all(error_text, rules_list, limit=limit) + + if output_format == 'json': + formatter = JSONFormatter(pretty=not no_color) + elif output_format == 'structured': + formatter = StructuredFormatter() + else: + formatter = TextFormatter(use_colors=not no_color) + + output = formatter.format(matches, error_text) + click.echo(output) + + +@cli.command() +@click.option('--dry-run', is_flag=True, help='Show what would be fixed without applying') +@click.pass_context +def interactive(ctx: click.Context, dry_run: bool): + click.echo("Interactive mode not yet implemented. Use --dry-run for preview.") + + +@cli.command() +def plugins(): + plugin_loader = get_plugin_loader() + plugin_list = plugin_loader.discover_and_load() + for plugin in plugin_list: + click.echo(f"{plugin.name} v{plugin.version}: {plugin.description}") + + +@cli.command() +@click.option('--rules', '-r', multiple=True, help='Rule paths to check') +@click.pass_context +def check(ctx: click.Context, rules: Tuple[str]): + rule_loader = get_rule_loader() + all_rules_paths = list(ctx.obj.get('rules_path', [])) + list(rules) + + for path in all_rules_paths: + try: + if os.path.exists(path): + rules_list = rule_loader.load_multiple([path]) + click.echo(f"Loaded {len(rules_list)} rules from {path}") + else: + click.echo(f"Path not found: {path}") + except Exception as e: + click.echo(f"Error loading {path}: {e}", err=True) + + +def main(): + cli() + + +if __name__ == '__main__': + main()