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.
@@ -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()
@@ -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