Files
shell-speak/shell_speak/main.py
Auto User 95459fb4c8 fix: resolve CI test failure in output.py
- Fixed undefined 'tool' variable in display_history function
- Changed '[tool]' markup tag usage to proper Rich syntax
- All tests now pass (38/38 unit tests)
- Type checking passes with mypy --strict
2026-01-31 06:22:27 +00:00

216 lines
5.1 KiB
Python

"""Main CLI entry point for shell-speak."""
import sys
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
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,
)
app = typer.Typer(
name="shell-speak",
add_completion=False,
help="Convert natural language to shell commands",
)
def version_callback(value: bool) -> None:
"""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",
),
) -> None:
pass
@app.command()
def convert(
query: str = typer.Argument(..., help="Natural language description of the command"),
tool: str | None = 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",
),
) -> None:
"""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",
),
) -> None:
"""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: str | None = typer.Option(
None,
"--tool",
"-t",
help=f"Filter by tool: {', '.join(DEFAULT_TOOLS)}",
),
search: str | None = typer.Option(
None,
"--search",
"-s",
help="Search history for query",
),
) -> None:
"""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)}",
),
) -> None:
"""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",
),
) -> None:
"""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() -> None:
"""Reload command libraries and corrections."""
ensure_data_dir()
loader = get_loader()
loader.reload()
display_info("Command libraries reloaded")
@app.command()
def tools() -> None:
"""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() -> None:
"""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()