Files
shell-speak/shell_speak/main.py
7000pctAUTO b6343f3c56
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
Initial upload: shell-speak CLI tool with natural language to shell command conversion
2026-01-31 05:31:19 +00:00

219 lines
5.1 KiB
Python

"""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()