import sys import os from typing import Optional, Tuple, IO import click from errorfix.rules import RuleLoader from errorfix.patterns import PatternMatcher from errorfix.formatters import TextFormatter, JSONFormatter, StructuredFormatter, Formatter 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: IO[str], 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 = '' if error_input and error_input != '-': try: error_text = error_input.read() except Exception: pass 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: 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()