Initial upload: CLI Explain Fix project with CI/CD workflow
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled

This commit is contained in:
2026-01-30 23:35:10 +00:00
parent 3b87785539
commit 321103bf8c

271
src/cli_explain_fix/cli.py Normal file
View 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()