"""Main CLI entry point for shell-speak.""" import os import sys from typing import Optional import typer from rich.panel import Panel from rich.text import Text from shell_speak import __version__ from shell_speak.config import DEFAULT_TOOLS, ensure_data_dir, get_history_file from shell_speak.history import get_history_manager from shell_speak.interactive import run_interactive_mode from shell_speak.library import get_loader from shell_speak.matcher import get_matcher from shell_speak.output import ( console, display_command, display_error, display_history, display_info, ) from shell_speak.models import CommandMatch app = typer.Typer( name="shell-speak", add_completion=False, help="Convert natural language to shell commands", ) def version_callback(value: bool): """Show version information.""" if value: console.print(f"Shell Speak v{__version__}") raise typer.Exit() @app.callback() def main( version: bool = typer.Option( False, "--version", "-V", callback=version_callback, is_eager=True, help="Show version information", ), ): pass @app.command() def convert( query: str = typer.Argument(..., help="Natural language description of the command"), tool: Optional[str] = typer.Option( None, "--tool", "-t", help=f"Filter by tool: {', '.join(DEFAULT_TOOLS)}", ), explain: bool = typer.Option( False, "--explain", "-e", help="Show detailed explanation of the command", ), dry_run: bool = typer.Option( False, "--dry-run", "-n", help="Preview the command without executing", ), ): """Convert natural language to a shell command.""" ensure_data_dir() matcher = get_matcher() match = matcher.match(query, tool) if match: display_command(match, explain=explain) if dry_run: display_info("Dry run - command not executed") else: display_info("Use --dry-run to preview without execution") else: display_error(f"Could not find a matching command for: '{query}'") display_info("Try using --tool to specify which tool you're using") @app.command() def interactive( interactive_mode: bool = typer.Option( False, "--interactive", "-i", is_eager=True, help="Enter interactive mode", ), ): """Enter interactive mode with history and auto-completion.""" run_interactive_mode() @app.command() def history( limit: int = typer.Option( 20, "--limit", "-l", help="Number of entries to show", ), tool: Optional[str] = typer.Option( None, "--tool", "-t", help=f"Filter by tool: {', '.join(DEFAULT_TOOLS)}", ), search: Optional[str] = typer.Option( None, "--search", "-s", help="Search history for query", ), ): """View command history.""" ensure_data_dir() history_manager = get_history_manager() history_manager.load() if search: entries = history_manager.search(search, tool) else: entries = history_manager.get_recent(limit) if entries: display_history(entries, limit) else: display_info("No history entries found") @app.command() def learn( query: str = typer.Argument(..., help="The natural language query"), command: str = typer.Argument(..., help="The shell command to associate"), tool: str = typer.Option( "custom", "--tool", "-t", help=f"Tool category: {', '.join(DEFAULT_TOOLS)}", ), ): """Learn a new command pattern from your correction.""" ensure_data_dir() loader = get_loader() loader.load_library() loader.add_correction(query, command, tool) display_info(f"Learned: '{query}' -> '{command}'") @app.command() def forget( query: str = typer.Argument(..., help="The query to forget"), tool: str = typer.Option( "custom", "--tool", "-t", help="Tool category", ), ): """Forget a learned pattern.""" ensure_data_dir() loader = get_loader() loader.load_library() if loader.remove_correction(query, tool): display_info(f"Forgot pattern for: '{query}'") else: display_error(f"Pattern not found: '{query}'") @app.command() def reload(): """Reload command libraries and corrections.""" ensure_data_dir() loader = get_loader() loader.reload() display_info("Command libraries reloaded") @app.command() def tools(): """List available tools.""" console.print(Panel( Text("Available Tools", justify="center", style="bold cyan"), expand=False, )) for tool in DEFAULT_TOOLS: console.print(f" [tool]{tool}[/]") def main_entry(): """Entry point for the CLI.""" try: app() except KeyboardInterrupt: console.print("\n[info]Interrupted.[/]") sys.exit(130) except Exception as e: display_error(f"An error occurred: {e}") sys.exit(1) if __name__ == "__main__": main_entry()