bugbee 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bugbee/__init__.py +13 -0
- bugbee/cache/__init__.py +76 -0
- bugbee/cli/__init__.py +0 -0
- bugbee/cli/main.py +258 -0
- bugbee/config/settings.py +116 -0
- bugbee/lazy.py +42 -0
- bugbee/legacy.py +31 -0
- bugbee/llm/__init__.py +59 -0
- bugbee/metrics/__init__.py +58 -0
- bugbee/parser/__init__.py +0 -0
- bugbee/parser/error_parser.py +106 -0
- bugbee/patcher/__init__.py +61 -0
- bugbee/retrieval/__init__.py +91 -0
- bugbee/ui.py +239 -0
- bugbee/utils/logging.py +90 -0
- bugbee/validator/__init__.py +30 -0
- bugbee-0.1.0.dist-info/METADATA +292 -0
- bugbee-0.1.0.dist-info/RECORD +22 -0
- bugbee-0.1.0.dist-info/WHEEL +5 -0
- bugbee-0.1.0.dist-info/entry_points.txt +2 -0
- bugbee-0.1.0.dist-info/licenses/LICENSE +209 -0
- bugbee-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Error‑parsing utilities for BugBee.
|
|
2
|
+
|
|
3
|
+
The original script performed a handful of regex‑based operations to:
|
|
4
|
+
|
|
5
|
+
1. Extract the file and line number from a stack‑trace.
|
|
6
|
+
2. Sanitize an error string for stable hashing.
|
|
7
|
+
3. Compute a SHA‑256 hash of the sanitized error.
|
|
8
|
+
|
|
9
|
+
These helpers are now grouped in a dedicated module so they can be reused by the
|
|
10
|
+
CLI and any future programmatic API.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional, Tuple
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"extract_location",
|
|
23
|
+
"sanitize_error",
|
|
24
|
+
"error_hash",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def extract_location(stderr: str, command: list[str]) -> Tuple[Optional[Path], Optional[int]]:
|
|
29
|
+
"""Return the source file path and line number for the *first* stack entry.
|
|
30
|
+
|
|
31
|
+
The function mirrors the logic that existed in ``main.py`` for the supported
|
|
32
|
+
languages (Python, C, C++, Go, JavaScript, generic build tools). It returns
|
|
33
|
+
``(None, None)`` when no location can be determined.
|
|
34
|
+
"""
|
|
35
|
+
file_under_execution = command[-1] if command else ""
|
|
36
|
+
line_number: Optional[int] = None
|
|
37
|
+
path: Optional[Path] = None
|
|
38
|
+
|
|
39
|
+
# Python stack trace
|
|
40
|
+
if file_under_execution.endswith(".py"):
|
|
41
|
+
match = re.search(r'File "([^\"]+)", line (\d+)', stderr)
|
|
42
|
+
if match:
|
|
43
|
+
path = Path(match.group(1))
|
|
44
|
+
line_number = int(match.group(2))
|
|
45
|
+
|
|
46
|
+
# C source (main.c)
|
|
47
|
+
elif file_under_execution.endswith(".c"):
|
|
48
|
+
match = re.search(r"main\.c:(\d+):\d+:", stderr)
|
|
49
|
+
if match:
|
|
50
|
+
path = Path(os.getcwd()) / file_under_execution
|
|
51
|
+
line_number = int(match.group(1))
|
|
52
|
+
|
|
53
|
+
# C++ source (main.cpp)
|
|
54
|
+
elif file_under_execution.endswith(".cpp"):
|
|
55
|
+
match = re.search(r"main\.cpp:(\d+):\d+:", stderr)
|
|
56
|
+
if match:
|
|
57
|
+
path = Path(os.getcwd()) / file_under_execution
|
|
58
|
+
line_number = int(match.group(1))
|
|
59
|
+
|
|
60
|
+
# Go source (main.go)
|
|
61
|
+
elif file_under_execution.endswith(".go"):
|
|
62
|
+
match = re.search(r"main\.go:(\d+):\d+:", stderr)
|
|
63
|
+
if match:
|
|
64
|
+
path = Path(os.getcwd()) / file_under_execution
|
|
65
|
+
line_number = int(match.group(1))
|
|
66
|
+
|
|
67
|
+
# JavaScript (Node) – try to pull the line from a V8 stack frame.
|
|
68
|
+
elif file_under_execution.endswith(".js"):
|
|
69
|
+
cwd = os.getcwd()
|
|
70
|
+
escaped = re.escape(cwd)
|
|
71
|
+
match = re.search(rf"at\s+.*?\({escaped}[\\/](.+?):(\d+):\d+\)", stderr)
|
|
72
|
+
if match:
|
|
73
|
+
path = Path(os.getcwd()) / file_under_execution
|
|
74
|
+
line_number = int(match.group(2))
|
|
75
|
+
|
|
76
|
+
# Generic build tool – attempts to find a "file:line" pattern.
|
|
77
|
+
elif file_under_execution.endswith("build"):
|
|
78
|
+
cwd = os.getcwd()
|
|
79
|
+
escaped = re.escape(cwd)
|
|
80
|
+
match = re.search(rf"(?:\()?({escaped}[\\/](.+?))[^\s]*?(\d+):\d+", stderr)
|
|
81
|
+
if match:
|
|
82
|
+
path = Path(match.group(1))
|
|
83
|
+
line_number = int(match.group(3))
|
|
84
|
+
|
|
85
|
+
return path, line_number
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def sanitize_error(stderr: str) -> str:
|
|
89
|
+
"""Remove environment‑specific noise from *stderr*.
|
|
90
|
+
|
|
91
|
+
The transformations aim to produce a stable string that can be used for
|
|
92
|
+
hashing and cache lookup:
|
|
93
|
+
|
|
94
|
+
* Replace absolute paths with ``<PATH>``.
|
|
95
|
+
* Replace timestamps (HH:MM:SS) with ``<TIME>``.
|
|
96
|
+
* Replace numeric line identifiers with ``<NUM>``.
|
|
97
|
+
"""
|
|
98
|
+
cleaned = re.sub(r"(/[\w\.-]+)+", "<PATH>", stderr)
|
|
99
|
+
cleaned = re.sub(r"\d{2}:\d{2}:\d{2}", "<TIME>", cleaned)
|
|
100
|
+
cleaned = re.sub(r"line \d+", "line <NUM>", cleaned)
|
|
101
|
+
return cleaned.strip()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def error_hash(sanitized_error: str) -> str:
|
|
105
|
+
"""Return the SHA‑256 hash of *sanitized_error* as a hex string."""
|
|
106
|
+
return hashlib.sha256(sanitized_error.encode("utf-8")).hexdigest()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Patch application utilities for BugBee.
|
|
2
|
+
|
|
3
|
+
The historic ``auto_code_fix.auto_fix`` function performed a line‑level replace
|
|
4
|
+
based on a *starting line* and a *search string* for the erroneous line. The new
|
|
5
|
+
implementation adds safety features:
|
|
6
|
+
|
|
7
|
+
* Creates a timestamped backup before modifying the file.
|
|
8
|
+
* Returns the path of the backup for possible rollback.
|
|
9
|
+
* Raises ``FileNotFoundError`` if the target does not exist.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import shutil
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
__all__ = ["apply_patch"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _backup_file(original: Path) -> Path:
|
|
23
|
+
"""Create a backup ``<original>.bak.<timestamp>`` and return its path."""
|
|
24
|
+
timestamp = time.strftime("%Y%m%d%H%M%S")
|
|
25
|
+
backup_path = original.with_name(f"{original.name}.bak.{timestamp}")
|
|
26
|
+
shutil.copy2(original, backup_path)
|
|
27
|
+
return backup_path
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def apply_patch(
|
|
31
|
+
file_path: Path,
|
|
32
|
+
line_number: int,
|
|
33
|
+
error_line: str,
|
|
34
|
+
fixed_line: str,
|
|
35
|
+
*,
|
|
36
|
+
create_backup: bool = True,
|
|
37
|
+
) -> bool:
|
|
38
|
+
"""Replace *error_line* with *fixed_line* around *line_number* in *file_path*.
|
|
39
|
+
|
|
40
|
+
The function reads the file, searches backwards from ``line_number`` for a
|
|
41
|
+
line containing ``error_line`` (mirroring the original behaviour), replaces
|
|
42
|
+
that line, writes the file back, and optionally creates a backup before the
|
|
43
|
+
modification.
|
|
44
|
+
|
|
45
|
+
Returns ``True`` if a replacement was made, ``False`` otherwise.
|
|
46
|
+
"""
|
|
47
|
+
if not file_path.is_file():
|
|
48
|
+
raise FileNotFoundError(str(file_path))
|
|
49
|
+
|
|
50
|
+
if create_backup:
|
|
51
|
+
_backup_file(file_path)
|
|
52
|
+
|
|
53
|
+
lines = file_path.read_text(encoding="utf-8").splitlines(keepends=True)
|
|
54
|
+
start_idx = max(0, line_number - 1) # zero‑based index
|
|
55
|
+
# Search backwards up to 30 lines as in the original script.
|
|
56
|
+
for idx in range(start_idx, max(-1, start_idx - 31), -1):
|
|
57
|
+
if error_line in lines[idx]:
|
|
58
|
+
lines[idx] = fixed_line
|
|
59
|
+
file_path.write_text("".join(lines), encoding="utf-8")
|
|
60
|
+
return True
|
|
61
|
+
return False
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Retrieval layer for BugBee.
|
|
2
|
+
|
|
3
|
+
Wraps the existing ChromaDB vector store used to fetch relevant documentation
|
|
4
|
+
based on an error message. The public function ``get_related_docs`` matches the
|
|
5
|
+
signature of the original ``retrieval.get_related_docs`` but is now a method of
|
|
6
|
+
the ``Retriever`` class for easier testing/mocking.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Tuple
|
|
13
|
+
|
|
14
|
+
from ..lazy import lazy_import
|
|
15
|
+
|
|
16
|
+
# Lazy imports for heavy dependencies
|
|
17
|
+
Chroma = lazy_import('langchain_chroma').Chroma
|
|
18
|
+
Document = lazy_import('langchain_core.documents').Document
|
|
19
|
+
HuggingFaceEmbeddings = lazy_import('langchain_huggingface').HuggingFaceEmbeddings
|
|
20
|
+
|
|
21
|
+
from bugbee.config.settings import settings
|
|
22
|
+
|
|
23
|
+
__all__ = ["Retriever", "get_related_docs"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Lazy singleton for Retriever to avoid repeated heavy initialisation.
|
|
27
|
+
_retriever: Retriever | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_retriever() -> "Retriever":
|
|
31
|
+
"""Return a cached ``Retriever`` instance, creating it on first call.
|
|
32
|
+
|
|
33
|
+
This function is used by ``get_related_docs`` and any external callers to
|
|
34
|
+
ensure that the heavy ``Chroma`` connection and embedding model are only
|
|
35
|
+
instantiated once per process.
|
|
36
|
+
"""
|
|
37
|
+
global _retriever
|
|
38
|
+
if _retriever is None:
|
|
39
|
+
_retriever = Retriever()
|
|
40
|
+
return _retriever
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Retriever:
|
|
44
|
+
"""ChromaDB retriever configured from the global settings.
|
|
45
|
+
|
|
46
|
+
``settings.chromadb_path`` determines where the persistent collection lives.
|
|
47
|
+
``settings.retrieval_k`` defines how many results to return.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self) -> None:
|
|
51
|
+
self.vector_store = Chroma(
|
|
52
|
+
persist_directory=str(settings.chromadb_path),
|
|
53
|
+
embedding_function=HuggingFaceEmbeddings(
|
|
54
|
+
model_name="sentence-transformers/all-MiniLM-L6-v2"
|
|
55
|
+
),
|
|
56
|
+
collection_name="framework_docs",
|
|
57
|
+
)
|
|
58
|
+
self.k = settings.retrieval_k
|
|
59
|
+
|
|
60
|
+
def get_related_docs(self, query: str) -> Tuple[str, float, float]:
|
|
61
|
+
"""Return formatted docs, top score and average score for *query*.
|
|
62
|
+
|
|
63
|
+
The return type mirrors the original implementation: ``(docs, top_score,
|
|
64
|
+
avg_score)`` where *docs* is a formatted string of source and content.
|
|
65
|
+
"""
|
|
66
|
+
results = self.vector_store.similarity_search_with_score(query, k=self.k)
|
|
67
|
+
if not results:
|
|
68
|
+
return "No Relevant documentation found", 0.0, 0.0
|
|
69
|
+
|
|
70
|
+
docs: List[Document] = [doc for doc, _ in results]
|
|
71
|
+
scores = [score for _, score in results]
|
|
72
|
+
# Convert Chroma's distance (0..2) to a cosine‑like similarity.
|
|
73
|
+
cosine_scores = [1.0 - (s / 2.0) for s in scores]
|
|
74
|
+
top_score = cosine_scores[0]
|
|
75
|
+
avg_score = sum(cosine_scores) / len(cosine_scores)
|
|
76
|
+
|
|
77
|
+
formatted = ""
|
|
78
|
+
for doc in docs:
|
|
79
|
+
source = doc.metadata.get("source_file", "Unknown File")
|
|
80
|
+
formatted += f"--- Source: {source} ---\n{doc.page_content}\n\n"
|
|
81
|
+
return formatted, top_score, avg_score
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Convenience function used throughout the code base.
|
|
85
|
+
def get_related_docs(query: str) -> Tuple[str, float, float]:
|
|
86
|
+
"""Convenience wrapper that uses a lazy singleton ``Retriever``.
|
|
87
|
+
|
|
88
|
+
This preserves the original public API while avoiding a new ``Retriever``
|
|
89
|
+
instance on every call.
|
|
90
|
+
"""
|
|
91
|
+
return get_retriever().get_related_docs(query)
|
bugbee/ui.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""bugbee.ui
|
|
2
|
+
~~~~~~~~~~~~
|
|
3
|
+
|
|
4
|
+
Centralised user-facing reporting for the BugBee CLI.
|
|
5
|
+
|
|
6
|
+
Every ``log.info(...)`` / ``log.debug(...)`` / ``typer.echo(...)`` call that
|
|
7
|
+
used to be scattered through ``bugbee.cli.main`` now lives here, behind a
|
|
8
|
+
single ``UI`` class. This keeps two concerns cleanly separated:
|
|
9
|
+
|
|
10
|
+
* ``bugbee.utils.logging`` – plain structured logging (file / stdout,
|
|
11
|
+
DEBUG / INFO / WARNING / ERROR) for diagnostics, CI logs, and bug
|
|
12
|
+
reports.
|
|
13
|
+
* ``bugbee.ui`` (this module) – everything a *human* actually sees:
|
|
14
|
+
spinners while work happens, colored status lines, a syntax-friendly
|
|
15
|
+
panel for the AI's analysis, tables for config/doctor output, and a
|
|
16
|
+
confirmation prompt for applying fixes.
|
|
17
|
+
|
|
18
|
+
``main.py`` only ever talks to the module-level ``ui`` singleton, e.g.::
|
|
19
|
+
|
|
20
|
+
from bugbee.ui import ui
|
|
21
|
+
|
|
22
|
+
ui.command_failed()
|
|
23
|
+
with ui.spinner("Consulting the model…"):
|
|
24
|
+
result = default_provider().generate(prompt)
|
|
25
|
+
ui.show_analysis(result)
|
|
26
|
+
|
|
27
|
+
This makes the command pipeline read like a checklist of steps instead of
|
|
28
|
+
being interleaved with logging boilerplate, and it makes the presentation
|
|
29
|
+
layer trivial to swap out later (e.g. a ``--quiet`` or ``--json`` mode just
|
|
30
|
+
means dropping in a different ``UI`` implementation).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from contextlib import contextmanager
|
|
36
|
+
from typing import Any, Dict, Iterator, List, Optional
|
|
37
|
+
|
|
38
|
+
from rich.console import Console
|
|
39
|
+
from rich.panel import Panel
|
|
40
|
+
from rich.prompt import Confirm
|
|
41
|
+
from rich.rule import Rule
|
|
42
|
+
from rich.status import Status
|
|
43
|
+
from rich.table import Table
|
|
44
|
+
from rich.theme import Theme
|
|
45
|
+
|
|
46
|
+
from bugbee.utils.logging import get_logger
|
|
47
|
+
|
|
48
|
+
_log = get_logger(__name__)
|
|
49
|
+
|
|
50
|
+
_THEME = Theme(
|
|
51
|
+
{
|
|
52
|
+
"bugbee.brand": "bold magenta",
|
|
53
|
+
"bugbee.success": "bold green",
|
|
54
|
+
"bugbee.error": "bold red",
|
|
55
|
+
"bugbee.warning": "bold yellow",
|
|
56
|
+
"bugbee.info": "cyan",
|
|
57
|
+
"bugbee.muted": "dim",
|
|
58
|
+
"bugbee.path": "bold blue",
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class UI:
|
|
64
|
+
"""Single point of contact for everything printed to the terminal.
|
|
65
|
+
|
|
66
|
+
Every method here pairs a structured log call (for diagnostics) with a
|
|
67
|
+
short, friendly line of console output (for humans). Methods are named
|
|
68
|
+
after pipeline *events* rather than log levels, so call sites in
|
|
69
|
+
``main.py`` read declaratively, e.g. ``ui.cache_hit()`` instead of
|
|
70
|
+
``log.info("Cache hit – using previously generated response.")``.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, console: Optional[Console] = None) -> None:
|
|
74
|
+
self.console = console or Console(theme=_THEME, highlight=False)
|
|
75
|
+
self.err_console = Console(theme=_THEME, stderr=True, highlight=False)
|
|
76
|
+
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
# Generic helpers
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
def banner(self, version: str) -> None:
|
|
81
|
+
self.console.print(
|
|
82
|
+
Panel.fit(
|
|
83
|
+
f"[bugbee.brand]\U0001F41D BugBee[/] [bugbee.muted]v{version}[/]\n"
|
|
84
|
+
"[bugbee.muted]AI-powered CLI debugger & auto-repair[/]",
|
|
85
|
+
border_style="magenta",
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@contextmanager
|
|
90
|
+
def spinner(self, message: str) -> Iterator[Status]:
|
|
91
|
+
"""Show a spinner + *message* for the duration of the ``with`` block."""
|
|
92
|
+
_log.debug(message)
|
|
93
|
+
with self.console.status(f"[bugbee.info]{message}[/]", spinner="dots") as status:
|
|
94
|
+
yield status
|
|
95
|
+
|
|
96
|
+
# ------------------------------------------------------------------
|
|
97
|
+
# Command execution
|
|
98
|
+
# ------------------------------------------------------------------
|
|
99
|
+
def running_command(self, command: List[str]) -> None:
|
|
100
|
+
cmd = " ".join(command)
|
|
101
|
+
_log.info(f"Running command: {cmd}")
|
|
102
|
+
self.console.print(Rule(f"[bugbee.muted]$ {cmd}[/]", style="dim"))
|
|
103
|
+
|
|
104
|
+
def command_succeeded(self) -> None:
|
|
105
|
+
_log.info("Command succeeded – no analysis needed.")
|
|
106
|
+
self.console.print("[bugbee.success]\u2713 Command succeeded — nothing to debug.[/]")
|
|
107
|
+
|
|
108
|
+
def command_failed(self) -> None:
|
|
109
|
+
_log.warning("Command failed – analyzing error.")
|
|
110
|
+
self.console.print(
|
|
111
|
+
"[bugbee.error]\u2717 Command failed.[/] [bugbee.muted]Analyzing the error…[/]"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
# Error parsing / caching
|
|
116
|
+
# ------------------------------------------------------------------
|
|
117
|
+
def location_found(self, file_path: str, line_number: int) -> None:
|
|
118
|
+
_log.debug(f"Extracted location: {file_path}:{line_number}")
|
|
119
|
+
self.console.print(
|
|
120
|
+
f"[bugbee.info]\U0001F4CD Located error at[/] [bugbee.path]{file_path}:{line_number}[/]"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def location_not_found(self) -> None:
|
|
124
|
+
_log.debug("Could not determine source location from stderr.")
|
|
125
|
+
self.console.print("[bugbee.warning]\u26A0 Could not determine the source location.[/]")
|
|
126
|
+
|
|
127
|
+
def cache_hit(self) -> None:
|
|
128
|
+
_log.info("Cache hit – using previously generated response.")
|
|
129
|
+
self.console.print(
|
|
130
|
+
"[bugbee.info]\u26A1 Cache hit[/] [bugbee.muted]— reusing a previous analysis.[/]"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def cache_miss(self) -> None:
|
|
134
|
+
_log.info("Cache miss – invoking LLM.")
|
|
135
|
+
self.console.print("[bugbee.muted]No cached fix found — asking the model…[/]")
|
|
136
|
+
|
|
137
|
+
def cache_cleared(self) -> None:
|
|
138
|
+
_log.info("Cache cleared.")
|
|
139
|
+
self.console.print("[bugbee.success]\u2713 Cache cleared.[/]")
|
|
140
|
+
|
|
141
|
+
def retrieval_stats(self, top_score: float, avg_score: float) -> None:
|
|
142
|
+
_log.debug(f"Retrieval scores — top: {top_score}, avg: {avg_score}")
|
|
143
|
+
|
|
144
|
+
# ------------------------------------------------------------------
|
|
145
|
+
# LLM analysis
|
|
146
|
+
# ------------------------------------------------------------------
|
|
147
|
+
def show_analysis(self, content: str) -> None:
|
|
148
|
+
self.console.print(
|
|
149
|
+
Panel(content, title="\U0001F916 AI Analysis", border_style="cyan", expand=True)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# ------------------------------------------------------------------
|
|
153
|
+
# Patch / auto-fix
|
|
154
|
+
# ------------------------------------------------------------------
|
|
155
|
+
def confirm_fix(self, auto_yes: bool) -> bool:
|
|
156
|
+
if auto_yes:
|
|
157
|
+
return True
|
|
158
|
+
return Confirm.ask("[bold]Apply suggested fix?[/]", console=self.console, default=False)
|
|
159
|
+
|
|
160
|
+
def applying_patch(self, file_path: str, line_number: int) -> None:
|
|
161
|
+
_log.info(f"Applying patch to {file_path} at line {line_number}")
|
|
162
|
+
self.console.print(
|
|
163
|
+
f"[bugbee.info]\U0001F527 Applying patch to[/] [bugbee.path]{file_path}:{line_number}[/]…"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def patch_applied(self) -> None:
|
|
167
|
+
_log.info("Patch applied successfully.")
|
|
168
|
+
self.console.print("[bugbee.success]\u2713 Patch applied successfully.[/]")
|
|
169
|
+
|
|
170
|
+
def patch_not_applied(self) -> None:
|
|
171
|
+
_log.warning("Patch could not be applied – error line not found.")
|
|
172
|
+
self.console.print(
|
|
173
|
+
"[bugbee.warning]\u26A0 Patch could not be applied — error line not found.[/]"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def patch_error(self, exc: Exception) -> None:
|
|
177
|
+
_log.error(f"Patch application failed: {exc}")
|
|
178
|
+
self.console.print(f"[bugbee.error]\u2717 Patch application failed:[/] {exc}")
|
|
179
|
+
|
|
180
|
+
def auto_fix_declined(self) -> None:
|
|
181
|
+
_log.info("Auto-fix declined or no source file identified.")
|
|
182
|
+
self.console.print("[bugbee.muted]No changes were made.[/]")
|
|
183
|
+
|
|
184
|
+
# ------------------------------------------------------------------
|
|
185
|
+
# Validation
|
|
186
|
+
# ------------------------------------------------------------------
|
|
187
|
+
def validation_passed(self) -> None:
|
|
188
|
+
_log.info("Validation succeeded after patch.")
|
|
189
|
+
self.console.print("[bugbee.success]\u2713 Validation succeeded after patch.[/]")
|
|
190
|
+
|
|
191
|
+
def validation_failed(self) -> None:
|
|
192
|
+
_log.warning("Validation failed after applying the patch.")
|
|
193
|
+
self.console.print(
|
|
194
|
+
"[bugbee.warning]\u26A0 Validation failed after applying the patch.[/]"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# ------------------------------------------------------------------
|
|
198
|
+
# Misc commands
|
|
199
|
+
# ------------------------------------------------------------------
|
|
200
|
+
def config_table(self, values: Dict[str, Any]) -> None:
|
|
201
|
+
table = Table(title="BugBee Configuration", border_style="magenta")
|
|
202
|
+
table.add_column("Setting", style="bold")
|
|
203
|
+
table.add_column("Value", style="bugbee.muted")
|
|
204
|
+
for name, value in values.items():
|
|
205
|
+
table.add_row(name, str(value))
|
|
206
|
+
self.console.print(table)
|
|
207
|
+
|
|
208
|
+
def doctor_start(self) -> None:
|
|
209
|
+
self.console.print("[bugbee.info]Running BugBee doctor…[/]")
|
|
210
|
+
|
|
211
|
+
def doctor_result(self, missing: List[str]) -> None:
|
|
212
|
+
if missing:
|
|
213
|
+
_log.warning(f"Missing dependencies: {', '.join(missing)}")
|
|
214
|
+
self.console.print(
|
|
215
|
+
f"[bugbee.error]\u2717 Missing dependencies:[/] {', '.join(missing)}"
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
_log.info("All dependencies present.")
|
|
219
|
+
self.console.print("[bugbee.success]\u2713 All good![/]")
|
|
220
|
+
|
|
221
|
+
def version(self, version: str) -> None:
|
|
222
|
+
self.console.print(f"[bugbee.brand]bugbee[/] [bugbee.muted]{version}[/]")
|
|
223
|
+
|
|
224
|
+
# ------------------------------------------------------------------
|
|
225
|
+
# Final summary
|
|
226
|
+
# ------------------------------------------------------------------
|
|
227
|
+
def summary(self, *, fix_applied: bool, returncode: int) -> None:
|
|
228
|
+
if fix_applied:
|
|
229
|
+
self.console.print(Rule(style="green"))
|
|
230
|
+
self.console.print("[bugbee.success]Done — fix applied.[/]")
|
|
231
|
+
else:
|
|
232
|
+
self.console.print(Rule(style="red"))
|
|
233
|
+
self.console.print(
|
|
234
|
+
f"[bugbee.error]Done — no fix applied (exit code {returncode}).[/]"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# Module-level singleton so call sites can simply do ``ui.cache_hit()``.
|
|
239
|
+
ui = UI()
|
bugbee/utils/logging.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Logging utilities for BugBee.
|
|
2
|
+
|
|
3
|
+
The module configures a root logger the first time it is imported. By
|
|
4
|
+
**default BugBee writes structured logs to a file only** — the terminal is
|
|
5
|
+
owned entirely by ``bugbee.ui``, which renders a polished, human-readable
|
|
6
|
+
line for the same events via ``rich``. Previously this handler wrote to
|
|
7
|
+
``sys.stdout``, which meant every event printed *twice*: once as a plain
|
|
8
|
+
``HH:MM:SS | INFO | ... | message`` line from this module, and once as the
|
|
9
|
+
styled line from ``bugbee.ui``. Routing this logger to disk instead removes
|
|
10
|
+
that duplication while keeping the full diagnostic trail available for bug
|
|
11
|
+
reports.
|
|
12
|
+
|
|
13
|
+
Verbosity (the file log level) is controlled via the global
|
|
14
|
+
``bugbee.config.settings.Settings.log_level`` value. If you want the raw,
|
|
15
|
+
timestamped log lines on the console too — e.g. while debugging a flaky LLM
|
|
16
|
+
call — pass ``--verbose`` on the CLI, which calls
|
|
17
|
+
:func:`enable_console_logging`.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from bugbee.config.settings import settings
|
|
26
|
+
|
|
27
|
+
_LOG_FORMAT = "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
|
|
28
|
+
_DATE_FORMAT = "%H:%M:%S"
|
|
29
|
+
|
|
30
|
+
_DEFAULT_LOG_DIR = Path.home() / ".bugbee" / "logs"
|
|
31
|
+
_DEFAULT_LOG_FILE = _DEFAULT_LOG_DIR / "bugbee.log"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _log_file_path() -> Path:
|
|
35
|
+
"""Resolve the log file location, honouring ``settings.log_file`` if set."""
|
|
36
|
+
configured = getattr(settings, "log_file", None)
|
|
37
|
+
path = Path(configured) if configured else _DEFAULT_LOG_FILE
|
|
38
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
return path
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _configure_logging() -> None:
|
|
43
|
+
"""Attach a file handler to the root logger according to current settings.
|
|
44
|
+
|
|
45
|
+
Deliberately does **not** attach a console handler by default: BugBee's
|
|
46
|
+
terminal output is owned by ``bugbee.ui`` so users see one clean, styled
|
|
47
|
+
line per event instead of a duplicate plain-text log line underneath it.
|
|
48
|
+
"""
|
|
49
|
+
root = logging.getLogger()
|
|
50
|
+
if any(isinstance(h, logging.FileHandler) for h in root.handlers):
|
|
51
|
+
return # already configured
|
|
52
|
+
|
|
53
|
+
level = getattr(logging, settings.log_level.upper(), logging.INFO)
|
|
54
|
+
|
|
55
|
+
handler = logging.FileHandler(_log_file_path(), encoding="utf-8")
|
|
56
|
+
handler.setFormatter(logging.Formatter(_LOG_FORMAT, datefmt=_DATE_FORMAT))
|
|
57
|
+
handler.setLevel(level)
|
|
58
|
+
|
|
59
|
+
root.setLevel(level)
|
|
60
|
+
root.addHandler(handler)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def enable_console_logging(level: int = logging.DEBUG) -> None:
|
|
64
|
+
"""Additionally stream raw log lines to the console (``--verbose`` mode).
|
|
65
|
+
|
|
66
|
+
Uses ``rich.logging.RichHandler`` instead of a plain ``StreamHandler`` so
|
|
67
|
+
the extra output stays visually consistent with the rest of the BugBee
|
|
68
|
+
UI instead of looking like a different, bolted-on tool.
|
|
69
|
+
"""
|
|
70
|
+
from rich.logging import RichHandler
|
|
71
|
+
|
|
72
|
+
root = logging.getLogger()
|
|
73
|
+
if any(isinstance(h, RichHandler) for h in root.handlers):
|
|
74
|
+
return # already enabled
|
|
75
|
+
|
|
76
|
+
handler = RichHandler(level=level, show_time=True, show_path=False, markup=True)
|
|
77
|
+
root.addHandler(handler)
|
|
78
|
+
if root.level > level:
|
|
79
|
+
root.setLevel(level)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Configure file logging on import – mirrors the behaviour of many libraries,
|
|
83
|
+
# but writes to disk instead of the terminal so it never collides with the
|
|
84
|
+
# rich-powered UI in ``bugbee.ui``.
|
|
85
|
+
_configure_logging()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_logger(name: str) -> logging.Logger:
|
|
89
|
+
"""Return a logger scoped to *name* (typically ``__name__``)."""
|
|
90
|
+
return logging.getLogger(name)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Validation utilities for BugBee.
|
|
2
|
+
|
|
3
|
+
After applying an automatic patch the original tool re‑ran the failing command to
|
|
4
|
+
ensure the issue is resolved. ``run_validation`` mirrors that behaviour: it
|
|
5
|
+
executes the supplied command and returns ``True`` when the exit code is ``0``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import subprocess
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List
|
|
13
|
+
|
|
14
|
+
__all__ = ["run_validation"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run_validation(command: List[str], cwd: Path | None = None) -> bool:
|
|
18
|
+
"""Execute *command* and return ``True`` if it finishes with exit code ``0``.
|
|
19
|
+
|
|
20
|
+
``cwd`` defaults to the current working directory. Standard output and
|
|
21
|
+
error streams are captured but not displayed; callers can log them if they
|
|
22
|
+
wish.
|
|
23
|
+
"""
|
|
24
|
+
result = subprocess.run(
|
|
25
|
+
command,
|
|
26
|
+
cwd=cwd,
|
|
27
|
+
capture_output=True,
|
|
28
|
+
text=True,
|
|
29
|
+
)
|
|
30
|
+
return result.returncode == 0
|