Initial upload: CLI Explain Fix project with CI/CD workflow
This commit is contained in:
271
src/cli_explain_fix/cli.py
Normal file
271
src/cli_explain_fix/cli.py
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
"""Command-line interface for CLI Explain Fix."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from typing import Optional, Any
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.text import Text
|
||||||
|
from rich.syntax import Syntax
|
||||||
|
|
||||||
|
from cli_explain_fix.parser import ErrorParser
|
||||||
|
from cli_explain_fix.knowledge_base import KnowledgeBase
|
||||||
|
from cli_explain_fix.explainer import Explainer
|
||||||
|
from cli_explain_fix.config import Config
|
||||||
|
|
||||||
|
app = typer.Typer(
|
||||||
|
name="cli-explain-fix",
|
||||||
|
help="Parse command output and generate human-readable explanations with actionable fix suggestions",
|
||||||
|
add_completion=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
kb = KnowledgeBase()
|
||||||
|
parser = ErrorParser()
|
||||||
|
explainer = Explainer(kb)
|
||||||
|
|
||||||
|
|
||||||
|
def print_rich_output(explanation: dict, verbose: bool = False, no_color: bool = False) -> None:
|
||||||
|
"""Print explanation using Rich formatting."""
|
||||||
|
console = Console(force_terminal=not no_color)
|
||||||
|
|
||||||
|
summary = explanation.get('summary', 'Error')
|
||||||
|
what = explanation.get('what_happened', '')
|
||||||
|
why = explanation.get('why_happened', '')
|
||||||
|
fixes = explanation.get('how_to_fix', [])
|
||||||
|
|
||||||
|
parts: list[Any] = []
|
||||||
|
|
||||||
|
parts.append(Text(f"Summary: {summary}\n\n", style="bold"))
|
||||||
|
parts.append(Text("What happened:\n", style="bold blue"))
|
||||||
|
parts.append(Text(f"{what}\n\n", style=""))
|
||||||
|
parts.append(Text("Why it happened:\n", style="bold blue"))
|
||||||
|
parts.append(Text(f"{why}\n\n", style=""))
|
||||||
|
|
||||||
|
if fixes:
|
||||||
|
parts.append(Text("How to fix:\n", style="bold green"))
|
||||||
|
for i, fix in enumerate(fixes, 1):
|
||||||
|
parts.append(Text(f" {i}. {fix}\n", style=""))
|
||||||
|
|
||||||
|
if 'location' in explanation:
|
||||||
|
loc = explanation['location']
|
||||||
|
parts.append(Text(f"\nLocation: {loc['file']}:{loc['line']}\n", style="italic"))
|
||||||
|
|
||||||
|
if 'code_examples' in explanation:
|
||||||
|
parts.append(Text("\nCode examples:\n", style="bold blue"))
|
||||||
|
from rich.console import Group
|
||||||
|
for example in explanation['code_examples']:
|
||||||
|
if example['description']:
|
||||||
|
parts.append(Text(f"{example['description']}\n", style="italic"))
|
||||||
|
syntax = Syntax(example['code'], example['language'], theme="monokai")
|
||||||
|
parts.append(Text.assemble(("", "")) if False else syntax)
|
||||||
|
parts.append(Text("\n", style=""))
|
||||||
|
|
||||||
|
if 'documentation' in explanation:
|
||||||
|
parts.append(Text(f"\nDocumentation: {explanation['documentation']}\n", style="link"))
|
||||||
|
|
||||||
|
if verbose and 'raw_error' in explanation:
|
||||||
|
parts.append(Text("\n--- Raw Error ---\n", style="dim"))
|
||||||
|
parts.append(Text(explanation['raw_error'], style="dim"))
|
||||||
|
|
||||||
|
from rich.console import Group
|
||||||
|
content = Group(*parts)
|
||||||
|
|
||||||
|
panel = Panel(
|
||||||
|
content,
|
||||||
|
title="CLI Explain Fix",
|
||||||
|
border_style="blue",
|
||||||
|
expand=True,
|
||||||
|
)
|
||||||
|
console.print(panel)
|
||||||
|
|
||||||
|
|
||||||
|
def print_json_output(explanation: dict) -> None:
|
||||||
|
"""Print explanation as JSON."""
|
||||||
|
import json
|
||||||
|
console.print(json.dumps(explanation, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def print_plain_output(explanation: dict) -> None:
|
||||||
|
"""Print explanation as plain text."""
|
||||||
|
print(f"Error: {explanation.get('error_type', 'Unknown')}")
|
||||||
|
print(f"Language: {explanation.get('language', 'unknown')}")
|
||||||
|
print()
|
||||||
|
print("What happened:")
|
||||||
|
print(f" {explanation.get('what_happened', 'Unknown')}")
|
||||||
|
print()
|
||||||
|
print("Why it happened:")
|
||||||
|
print(f" {explanation.get('why_happened', 'Unknown')}")
|
||||||
|
print()
|
||||||
|
print("How to fix:")
|
||||||
|
for i, fix in enumerate(explanation.get('how_to_fix', []), 1):
|
||||||
|
print(f" {i}. {fix}")
|
||||||
|
|
||||||
|
|
||||||
|
def process_input(
|
||||||
|
input_text: str,
|
||||||
|
language: Optional[str],
|
||||||
|
verbose: bool,
|
||||||
|
output_format: str,
|
||||||
|
no_color: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Process input and display explanation."""
|
||||||
|
if not input_text.strip():
|
||||||
|
console.print(Panel(
|
||||||
|
"[bold yellow]No input provided[/bold yellow]\n\n"
|
||||||
|
"Usage examples:\n"
|
||||||
|
" echo 'ImportError: No module named foo' | cli-explain-fix\n"
|
||||||
|
" cli-explain-fix --file error.txt\n"
|
||||||
|
" cli-explain-fix --lang python 'ValueError: invalid value'",
|
||||||
|
title="CLI Explain Fix",
|
||||||
|
border_style="yellow",
|
||||||
|
))
|
||||||
|
return
|
||||||
|
|
||||||
|
parsed = parser.parse(input_text, language)
|
||||||
|
explanation = explainer.explain(parsed, verbose)
|
||||||
|
|
||||||
|
if output_format == "json":
|
||||||
|
print_json_output(explanation)
|
||||||
|
elif output_format == "plain":
|
||||||
|
print_plain_output(explanation)
|
||||||
|
else:
|
||||||
|
print_rich_output(explanation, verbose, no_color)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def main(
|
||||||
|
input_text: str = typer.Argument(
|
||||||
|
None,
|
||||||
|
help="Error text to explain (reads from stdin if not provided)",
|
||||||
|
),
|
||||||
|
file: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--file",
|
||||||
|
"-f",
|
||||||
|
help="Read error from file",
|
||||||
|
),
|
||||||
|
language: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--lang",
|
||||||
|
"-l",
|
||||||
|
help="Explicitly specify language (python, javascript, go, rust, etc.)",
|
||||||
|
),
|
||||||
|
verbose: bool = typer.Option(
|
||||||
|
False,
|
||||||
|
"--verbose",
|
||||||
|
"-v",
|
||||||
|
help="Show detailed output including raw error",
|
||||||
|
),
|
||||||
|
json_output: bool = typer.Option(
|
||||||
|
False,
|
||||||
|
"--json",
|
||||||
|
"-j",
|
||||||
|
help="Output as JSON",
|
||||||
|
),
|
||||||
|
plain: bool = typer.Option(
|
||||||
|
False,
|
||||||
|
"--plain",
|
||||||
|
"-p",
|
||||||
|
help="Output as plain text",
|
||||||
|
),
|
||||||
|
no_color: bool = typer.Option(
|
||||||
|
False,
|
||||||
|
"--no-color",
|
||||||
|
help="Disable colors in output",
|
||||||
|
),
|
||||||
|
theme: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--theme",
|
||||||
|
help="Color theme for output",
|
||||||
|
),
|
||||||
|
) -> None:
|
||||||
|
"""Explain errors and provide fix suggestions."""
|
||||||
|
config = Config.load()
|
||||||
|
|
||||||
|
if theme:
|
||||||
|
output_format = "rich"
|
||||||
|
elif json_output:
|
||||||
|
output_format = "json"
|
||||||
|
elif plain:
|
||||||
|
output_format = "plain"
|
||||||
|
else:
|
||||||
|
output_format = config.output_format
|
||||||
|
|
||||||
|
input_source = ""
|
||||||
|
|
||||||
|
if file:
|
||||||
|
try:
|
||||||
|
with open(file, 'r', encoding='utf-8') as f:
|
||||||
|
input_source = f.read()
|
||||||
|
except OSError as e:
|
||||||
|
console.print(f"[bold red]Error:[/bold red] Could not read file '{file}': {e}")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
elif input_text:
|
||||||
|
input_source = input_text
|
||||||
|
else:
|
||||||
|
input_source = sys.stdin.read()
|
||||||
|
|
||||||
|
effective_language = language or config.default_language
|
||||||
|
process_input(input_source, effective_language, verbose, output_format, no_color)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def list_languages() -> None:
|
||||||
|
"""List all supported programming languages."""
|
||||||
|
languages = kb.list_languages()
|
||||||
|
console.print(Panel(
|
||||||
|
"\n".join(f" • {lang}" for lang in languages),
|
||||||
|
title="Supported Languages",
|
||||||
|
border_style="green",
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def list_errors(
|
||||||
|
language: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--lang",
|
||||||
|
"-l",
|
||||||
|
help="Filter by language",
|
||||||
|
),
|
||||||
|
) -> None:
|
||||||
|
"""List all known error types."""
|
||||||
|
errors = kb.list_errors(language)
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
console.print("[yellow]No errors found in knowledge base[/yellow]")
|
||||||
|
return
|
||||||
|
|
||||||
|
content = "\n".join(
|
||||||
|
f" • [{e['language']}] {e['error_type']}: {e['description']}"
|
||||||
|
for e in errors
|
||||||
|
)
|
||||||
|
console.print(Panel(
|
||||||
|
content,
|
||||||
|
title="Known Errors",
|
||||||
|
border_style="blue",
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def show_config() -> None:
|
||||||
|
"""Show current configuration."""
|
||||||
|
config = Config.load()
|
||||||
|
|
||||||
|
content = f"""Default Language: {config.default_language}
|
||||||
|
Output Format: {config.output_format}
|
||||||
|
Verbosity: {config.verbosity}
|
||||||
|
Theme: {config.theme}
|
||||||
|
Config File: {config.config_path}"""
|
||||||
|
|
||||||
|
console.print(Panel(
|
||||||
|
content,
|
||||||
|
title="Current Configuration",
|
||||||
|
border_style="blue",
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app()
|
||||||
Reference in New Issue
Block a user