diff --git a/shell_alias_gen/cli.py b/shell_alias_gen/cli.py new file mode 100644 index 0000000..1501e70 --- /dev/null +++ b/shell_alias_gen/cli.py @@ -0,0 +1,279 @@ +"""CLI interface for Shell History Alias Generator.""" + +import sys +from pathlib import Path +from typing import List, Optional + +import click +from rich.console import Console +from rich.panel import Panel +from rich.text import Text + +from .parsers import HistoryParserFactory, BashHistoryParser, ZshHistoryParser, FishHistoryParser +from .command_analyzer import CommandAnalyzer +from .interactive_ui import InteractiveUI +from .export_manager import ExportManager + + +console = Console() + + +@click.group() +@click.version_option(version="0.1.0") +def main(): + """Shell History Alias Generator - Analyze shell history and create aliases.""" + pass + + +@main.command() +@click.argument( + 'history_file', + type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path), + required=False +) +@click.option( + '--shell', + type=click.Choice(['bash', 'zsh', 'fish', 'auto']), + default='auto', + help='Shell type for history parsing' +) +@click.option( + '--min-length', + type=int, + default=15, + help='Minimum command length to consider' +) +@click.option( + '--min-frequency', + type=int, + default=2, + help='Minimum command frequency to consider' +) +@click.option( + '--export', + type=click.Choice(['bash', 'zsh', 'fish']), + default=None, + help='Export format for generated aliases' +) +@click.option( + '--output', + type=click.Path(path_type=Path), + default=None, + help='Output file path for export' +) +@click.option( + '--non-interactive', + is_flag=True, + default=False, + help='Skip interactive review mode' +) +def analyze( + history_file: Optional[Path], + shell: str, + min_length: int, + min_frequency: int, + export: Optional[str], + output: Optional[Path], + non_interactive: bool +): + """Analyze shell history and generate alias suggestions.""" + console.print(Panel.fit( + Text("Shell History Alias Generator", style="bold magenta"), + subtitle="Analyzing your command history..." + )) + + parser = _get_parser(shell, history_file) + + if parser is None: + console.print("[red]Error: Could not determine shell type.[/red]") + sys.exit(1) + + if history_file: + commands = parser.parse_file(str(history_file)) + else: + commands = parser.parse_all_history_files() if hasattr(parser, 'parse_all_history_files') else [] + if not commands: + default_paths = parser.get_default_history_locations() + if default_paths: + if isinstance(default_paths, list) and default_paths: + commands = parser.parse_file(default_paths[0]) if isinstance(default_paths[0], str) else [] + else: + commands = [] + + if not commands: + console.print("[yellow]No commands found in history.[/yellow]") + sys.exit(0) + + console.print(f"[dim]Parsed {len(commands)} commands[/dim]") + + analyzer = CommandAnalyzer() + suggestions = analyzer.analyze_commands(commands) + + if not suggestions: + console.print("[yellow]No alias suggestions generated.[/yellow]") + sys.exit(0) + + if non_interactive: + selected = suggestions + else: + ui = InteractiveUI() + ui.display_suggestions(suggestions, "Generated Alias Suggestions") + + console.print("\n[bold]Reviewing suggestions...[/bold]") + selected = ui.run_review_session(suggestions) + + if not selected: + console.print("[yellow]No aliases selected.[/yellow]") + sys.exit(0) + + ui.display_summary(selected) + + if not ui.confirm_selection(): + console.print("[yellow]Cancelled.[/yellow]") + sys.exit(0) + + if export: + export_manager = ExportManager() + if output: + content = export_manager.export(selected, export, str(output)) + console.print(f"[green]Exported to {output}[/green]") + else: + content = export_manager.export(selected, export) + console.print("\n[bold]Generated aliases:[/bold]") + console.print(content) + else: + _print_selected_aliases(selected) + + +@main.command() +@click.argument( + 'history_file', + type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path), + required=False +) +@click.option( + '--shell', + type=click.Choice(['bash', 'zsh', 'fish', 'auto']), + default='auto', + help='Shell type for history parsing' +) +@click.option( + '--format', + 'output_format', + type=click.Choice(['bash', 'zsh', 'fish']), + default='bash', + help='Export format' +) +@click.option( + '--append/--replace', + default=False, + help='Append to rcfile or replace aliases section' +) +def install( + history_file: Optional[Path], + shell: str, + output_format: str, + append: bool +): + """Install aliases directly to shell rc file.""" + parser = _get_parser(shell, history_file) + + if parser is None: + console.print("[red]Error: Could not determine shell type.[/red]") + sys.exit(1) + + if history_file: + commands = parser.parse_file(str(history_file)) + else: + commands = parser.parse_all_history_files() if hasattr(parser, 'parse_all_history_files') else [] + + if not commands: + console.print("[yellow]No commands found in history.[/yellow]") + sys.exit(0) + + analyzer = CommandAnalyzer() + suggestions = analyzer.analyze_commands(commands) + + if not suggestions: + console.print("[yellow]No alias suggestions generated.[/yellow]") + sys.exit(0) + + rcfile = _get_rcfile_path(output_format) + + if rcfile is None or not rcfile.exists(): + console.print(f"[red]Could not find {output_format} rc file.[/red]") + sys.exit(1) + + export_manager = ExportManager() + if export_manager.export_to_rcfile(suggestions, output_format, str(rcfile)): + console.print(f"[green]Aliases added to {rcfile}[/green]") + console.print(f"[dim]Restart your shell or run: source {rcfile}[/dim]") + else: + console.print("[red]Failed to write to rc file.[/red]") + sys.exit(1) + + +@main.command() +def shells(): + """List supported shell types.""" + shells_list = HistoryParserFactory.get_supported_shells() + console.print("Supported shell types:") + for shell in shells_list: + console.print(f" - {shell}") + + +@main.command() +def locations(): + """Show default history file locations.""" + console.print("Default history file locations:\n") + + for shell_name in ['bash', 'zsh', 'fish']: + parser = HistoryParserFactory.get_parser(shell_name) + if parser: + locations = parser.get_default_history_locations() + console.print(f"[bold]{shell_name.upper()}[/bold]:") + for loc in locations: + exists = "[green]✓[/green]" if Path(loc).exists() else "[red]✗[/red]" + console.print(f" {exists} {loc}") + console.print() + + +def _get_parser(shell: str, history_file: Optional[Path]): + """Get appropriate history parser.""" + if shell == 'auto' and history_file: + detected = HistoryParserFactory.detect_shell_from_file(str(history_file)) + if detected: + shell = detected + + if shell == 'auto': + shell = 'bash' + + return HistoryParserFactory.get_parser(shell) + + +def _get_rcfile_path(shell: str): + """Get the rc file path for a shell.""" + from os.path import expanduser + + rcfiles = { + 'bash': expanduser('~/.bashrc'), + 'zsh': expanduser('~/.zshrc'), + 'fish': expanduser('~/.config/fish/config.fish'), + } + + path = rcfiles.get(shell) + return Path(path) if path else None + + +def _print_selected_aliases(suggestions: List): + """Print selected aliases in a nice format.""" + console.print("\n[bold green]Generated Aliases:[/bold green]\n") + + for suggestion in suggestions: + console.print(f"[bold green]={suggestion.alias_name}[/bold green]='{suggestion.original_command}'") + + console.print("\n[dim]Add these to your shell rc file or run with --export[/dim]") + + +if __name__ == "__main__": + main()