roma-debug 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.
- roma_debug/__init__.py +3 -0
- roma_debug/config.py +79 -0
- roma_debug/core/__init__.py +5 -0
- roma_debug/core/engine.py +423 -0
- roma_debug/core/models.py +313 -0
- roma_debug/main.py +753 -0
- roma_debug/parsers/__init__.py +21 -0
- roma_debug/parsers/base.py +189 -0
- roma_debug/parsers/python_ast_parser.py +268 -0
- roma_debug/parsers/registry.py +196 -0
- roma_debug/parsers/traceback_patterns.py +314 -0
- roma_debug/parsers/treesitter_parser.py +598 -0
- roma_debug/prompts.py +153 -0
- roma_debug/server.py +247 -0
- roma_debug/tracing/__init__.py +28 -0
- roma_debug/tracing/call_chain.py +278 -0
- roma_debug/tracing/context_builder.py +672 -0
- roma_debug/tracing/dependency_graph.py +298 -0
- roma_debug/tracing/error_analyzer.py +399 -0
- roma_debug/tracing/import_resolver.py +315 -0
- roma_debug/tracing/project_scanner.py +569 -0
- roma_debug/utils/__init__.py +5 -0
- roma_debug/utils/context.py +422 -0
- roma_debug-0.1.0.dist-info/METADATA +34 -0
- roma_debug-0.1.0.dist-info/RECORD +36 -0
- roma_debug-0.1.0.dist-info/WHEEL +5 -0
- roma_debug-0.1.0.dist-info/entry_points.txt +2 -0
- roma_debug-0.1.0.dist-info/licenses/LICENSE +201 -0
- roma_debug-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/test_context.py +208 -0
- tests/test_engine.py +296 -0
- tests/test_parsers.py +534 -0
- tests/test_project_scanner.py +275 -0
- tests/test_traceback_patterns.py +222 -0
- tests/test_tracing.py +296 -0
roma_debug/main.py
ADDED
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
"""Interactive CLI for ROMA Debug.
|
|
2
|
+
|
|
3
|
+
Production-grade CLI with diff display and safe file patching.
|
|
4
|
+
Supports V1 (simple) and V2 (deep debugging) modes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import difflib
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.syntax import Syntax
|
|
17
|
+
from rich.prompt import Prompt, Confirm
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
from rich import print as rprint
|
|
20
|
+
|
|
21
|
+
from roma_debug import __version__
|
|
22
|
+
from roma_debug.config import GEMINI_API_KEY, get_api_key_status
|
|
23
|
+
from roma_debug.utils.context import get_file_context, get_primary_file
|
|
24
|
+
from roma_debug.core.engine import analyze_error, analyze_error_v2, FixResult, FixResultV2
|
|
25
|
+
from roma_debug.core.models import Language
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Language name mappings for CLI
|
|
32
|
+
LANGUAGE_CHOICES = {
|
|
33
|
+
"python": Language.PYTHON,
|
|
34
|
+
"py": Language.PYTHON,
|
|
35
|
+
"javascript": Language.JAVASCRIPT,
|
|
36
|
+
"js": Language.JAVASCRIPT,
|
|
37
|
+
"typescript": Language.TYPESCRIPT,
|
|
38
|
+
"ts": Language.TYPESCRIPT,
|
|
39
|
+
"go": Language.GO,
|
|
40
|
+
"golang": Language.GO,
|
|
41
|
+
"rust": Language.RUST,
|
|
42
|
+
"rs": Language.RUST,
|
|
43
|
+
"java": Language.JAVA,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def print_welcome():
|
|
48
|
+
"""Print welcome banner."""
|
|
49
|
+
console.print()
|
|
50
|
+
console.print(Panel(
|
|
51
|
+
"[bold blue]ROMA Debug[/bold blue] - AI-Powered Code Debugger\n"
|
|
52
|
+
f"[dim]Version {__version__} | Powered by Gemini[/dim]",
|
|
53
|
+
border_style="blue",
|
|
54
|
+
))
|
|
55
|
+
console.print()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_multiline_input() -> str:
|
|
59
|
+
"""Get multi-line input from user (paste-friendly)."""
|
|
60
|
+
console.print("[yellow]Paste your error log below.[/yellow]")
|
|
61
|
+
console.print("[dim]Press Enter twice (empty line) when done:[/dim]")
|
|
62
|
+
console.print()
|
|
63
|
+
|
|
64
|
+
lines = []
|
|
65
|
+
empty_count = 0
|
|
66
|
+
|
|
67
|
+
while True:
|
|
68
|
+
try:
|
|
69
|
+
line = input()
|
|
70
|
+
if line == "":
|
|
71
|
+
empty_count += 1
|
|
72
|
+
if empty_count >= 1 and lines:
|
|
73
|
+
break
|
|
74
|
+
lines.append(line)
|
|
75
|
+
else:
|
|
76
|
+
empty_count = 0
|
|
77
|
+
lines.append(line)
|
|
78
|
+
except EOFError:
|
|
79
|
+
break
|
|
80
|
+
except KeyboardInterrupt:
|
|
81
|
+
console.print("\n[yellow]Cancelled.[/yellow]")
|
|
82
|
+
return ""
|
|
83
|
+
|
|
84
|
+
return "\n".join(lines).strip()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def compute_diff(original: str, fixed: str, filepath: str) -> str:
|
|
88
|
+
"""Compute unified diff between original and fixed code.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
original: Original file content
|
|
92
|
+
fixed: Fixed code from AI
|
|
93
|
+
filepath: Path for diff header
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Unified diff string
|
|
97
|
+
"""
|
|
98
|
+
original_lines = original.splitlines(keepends=True)
|
|
99
|
+
fixed_lines = fixed.splitlines(keepends=True)
|
|
100
|
+
|
|
101
|
+
diff = difflib.unified_diff(
|
|
102
|
+
original_lines,
|
|
103
|
+
fixed_lines,
|
|
104
|
+
fromfile=f"a/{filepath}",
|
|
105
|
+
tofile=f"b/{filepath}",
|
|
106
|
+
lineterm=""
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return "".join(diff)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def display_diff(diff_text: str):
|
|
113
|
+
"""Display diff with rich formatting (green=add, red=delete).
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
diff_text: Unified diff string
|
|
117
|
+
"""
|
|
118
|
+
if not diff_text.strip():
|
|
119
|
+
console.print("[yellow]No differences detected.[/yellow]")
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
console.print()
|
|
123
|
+
console.print("[bold]Proposed Changes:[/bold]")
|
|
124
|
+
console.print()
|
|
125
|
+
|
|
126
|
+
for line in diff_text.splitlines():
|
|
127
|
+
if line.startswith("+++") or line.startswith("---"):
|
|
128
|
+
console.print(f"[bold]{line}[/bold]")
|
|
129
|
+
elif line.startswith("@@"):
|
|
130
|
+
console.print(f"[cyan]{line}[/cyan]")
|
|
131
|
+
elif line.startswith("+"):
|
|
132
|
+
console.print(f"[green]{line}[/green]")
|
|
133
|
+
elif line.startswith("-"):
|
|
134
|
+
console.print(f"[red]{line}[/red]")
|
|
135
|
+
else:
|
|
136
|
+
console.print(line)
|
|
137
|
+
|
|
138
|
+
console.print()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def display_fix_result(result: FixResult, is_general: bool = False):
|
|
142
|
+
"""Display the fix result in a panel.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
result: FixResult from engine
|
|
146
|
+
is_general: If True, display as general advice (no file target)
|
|
147
|
+
"""
|
|
148
|
+
console.print()
|
|
149
|
+
|
|
150
|
+
if is_general or result.filepath is None:
|
|
151
|
+
# General system error - no specific file
|
|
152
|
+
console.print(Panel(
|
|
153
|
+
f"[bold]Type:[/bold] General Advice\n"
|
|
154
|
+
f"[bold]Model:[/bold] {result.model_used}\n\n"
|
|
155
|
+
f"[bold]Explanation:[/bold]\n{result.explanation}",
|
|
156
|
+
title="[bold yellow]General Advice[/bold yellow]",
|
|
157
|
+
border_style="yellow",
|
|
158
|
+
))
|
|
159
|
+
else:
|
|
160
|
+
# Specific file fix
|
|
161
|
+
console.print(Panel(
|
|
162
|
+
f"[bold]File:[/bold] {result.filepath}\n"
|
|
163
|
+
f"[bold]Model:[/bold] {result.model_used}\n\n"
|
|
164
|
+
f"[bold]Explanation:[/bold]\n{result.explanation}",
|
|
165
|
+
title="[bold green]Analysis Result[/bold green]",
|
|
166
|
+
border_style="green",
|
|
167
|
+
))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def display_fix_result_v2(result: FixResultV2):
|
|
171
|
+
"""Display V2 fix result with root cause info.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
result: FixResultV2 from engine
|
|
175
|
+
"""
|
|
176
|
+
console.print()
|
|
177
|
+
|
|
178
|
+
# Main result panel
|
|
179
|
+
if result.filepath is None:
|
|
180
|
+
console.print(Panel(
|
|
181
|
+
f"[bold]Type:[/bold] General Advice\n"
|
|
182
|
+
f"[bold]Model:[/bold] {result.model_used}\n\n"
|
|
183
|
+
f"[bold]Explanation:[/bold]\n{result.explanation}",
|
|
184
|
+
title="[bold yellow]General Advice[/bold yellow]",
|
|
185
|
+
border_style="yellow",
|
|
186
|
+
))
|
|
187
|
+
else:
|
|
188
|
+
panel_content = f"[bold]File:[/bold] {result.filepath}\n"
|
|
189
|
+
panel_content += f"[bold]Model:[/bold] {result.model_used}\n\n"
|
|
190
|
+
panel_content += f"[bold]Explanation:[/bold]\n{result.explanation}"
|
|
191
|
+
|
|
192
|
+
console.print(Panel(
|
|
193
|
+
panel_content,
|
|
194
|
+
title="[bold green]Analysis Result[/bold green]",
|
|
195
|
+
border_style="green",
|
|
196
|
+
))
|
|
197
|
+
|
|
198
|
+
# Root cause panel (if different file)
|
|
199
|
+
if result.has_root_cause:
|
|
200
|
+
console.print()
|
|
201
|
+
console.print(Panel(
|
|
202
|
+
f"[bold]Root Cause File:[/bold] {result.root_cause_file}\n\n"
|
|
203
|
+
f"[bold]Root Cause:[/bold]\n{result.root_cause_explanation}",
|
|
204
|
+
title="[bold magenta]Root Cause Analysis[/bold magenta]",
|
|
205
|
+
border_style="magenta",
|
|
206
|
+
))
|
|
207
|
+
|
|
208
|
+
# Additional fixes
|
|
209
|
+
if result.additional_fixes:
|
|
210
|
+
console.print()
|
|
211
|
+
console.print("[bold cyan]Additional Files to Fix:[/bold cyan]")
|
|
212
|
+
for fix in result.additional_fixes:
|
|
213
|
+
console.print(f" • {fix.filepath}: {fix.explanation}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def display_general_advice(result: FixResult):
|
|
217
|
+
"""Display general advice for system errors (no file patching).
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
result: FixResult from engine
|
|
221
|
+
"""
|
|
222
|
+
display_fix_result(result, is_general=True)
|
|
223
|
+
|
|
224
|
+
if result.full_code_block:
|
|
225
|
+
console.print("\n[bold]Suggested Code / Solution:[/bold]")
|
|
226
|
+
console.print(Panel(
|
|
227
|
+
Syntax(result.full_code_block, "python", theme="monokai", line_numbers=True),
|
|
228
|
+
border_style="yellow",
|
|
229
|
+
))
|
|
230
|
+
|
|
231
|
+
console.print("\n[dim]This is general advice. No file will be modified.[/dim]")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def read_file_content(filepath: str) -> str | None:
|
|
235
|
+
"""Read file content if it exists.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
filepath: Path to file
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
File content or None if not readable
|
|
242
|
+
"""
|
|
243
|
+
# Try absolute path first
|
|
244
|
+
if os.path.isfile(filepath):
|
|
245
|
+
try:
|
|
246
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
247
|
+
return f.read()
|
|
248
|
+
except (IOError, OSError):
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
# Try relative to cwd
|
|
252
|
+
cwd_path = os.path.join(os.getcwd(), filepath)
|
|
253
|
+
if os.path.isfile(cwd_path):
|
|
254
|
+
try:
|
|
255
|
+
with open(cwd_path, 'r', encoding='utf-8') as f:
|
|
256
|
+
return f.read()
|
|
257
|
+
except (IOError, OSError):
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def resolve_filepath(filepath: str) -> str:
|
|
264
|
+
"""Resolve filepath relative to cwd if not absolute.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
filepath: Path from AI response
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Resolved absolute or cwd-relative path
|
|
271
|
+
"""
|
|
272
|
+
if os.path.isabs(filepath):
|
|
273
|
+
return filepath
|
|
274
|
+
|
|
275
|
+
# Check if it exists relative to cwd
|
|
276
|
+
cwd_path = os.path.join(os.getcwd(), filepath)
|
|
277
|
+
if os.path.exists(cwd_path) or os.path.exists(os.path.dirname(cwd_path) or "."):
|
|
278
|
+
return cwd_path
|
|
279
|
+
|
|
280
|
+
return filepath
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def create_backup(filepath: str) -> str | None:
|
|
284
|
+
"""Create a backup of the file.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
filepath: Path to file
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Backup path or None if failed
|
|
291
|
+
"""
|
|
292
|
+
backup_path = f"{filepath}.bak"
|
|
293
|
+
try:
|
|
294
|
+
shutil.copy2(filepath, backup_path)
|
|
295
|
+
return backup_path
|
|
296
|
+
except (IOError, OSError) as e:
|
|
297
|
+
console.print(f"[yellow]Warning: Could not create backup: {e}[/yellow]")
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def apply_fix(filepath: str, new_content: str) -> bool:
|
|
302
|
+
"""Apply the fix to the file.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
filepath: Path to file
|
|
306
|
+
new_content: New file content
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
True if successful
|
|
310
|
+
"""
|
|
311
|
+
try:
|
|
312
|
+
# Ensure parent directory exists
|
|
313
|
+
Path(filepath).parent.mkdir(parents=True, exist_ok=True)
|
|
314
|
+
|
|
315
|
+
with open(filepath, 'w', encoding='utf-8') as f:
|
|
316
|
+
f.write(new_content)
|
|
317
|
+
return True
|
|
318
|
+
except (IOError, OSError) as e:
|
|
319
|
+
console.print(f"[red]Error writing file: {e}[/red]")
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def interactive_fix(result: FixResult):
|
|
324
|
+
"""Interactive workflow to apply a fix.
|
|
325
|
+
|
|
326
|
+
Handles three cases:
|
|
327
|
+
1. filepath is None -> Display as general advice (no file ops)
|
|
328
|
+
2. filepath exists -> Show diff and offer to patch
|
|
329
|
+
3. filepath doesn't exist -> Offer to create new file
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
result: FixResult from engine
|
|
333
|
+
"""
|
|
334
|
+
# Case 1: No filepath - general system error
|
|
335
|
+
if result.filepath is None:
|
|
336
|
+
display_general_advice(result)
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
# Resolve filepath relative to cwd
|
|
340
|
+
filepath = resolve_filepath(result.filepath)
|
|
341
|
+
new_code = result.full_code_block
|
|
342
|
+
|
|
343
|
+
# Display the result
|
|
344
|
+
display_fix_result(result)
|
|
345
|
+
|
|
346
|
+
if not new_code:
|
|
347
|
+
console.print("[yellow]No code fix provided by AI.[/yellow]")
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
# Check if file exists
|
|
351
|
+
original_content = read_file_content(filepath)
|
|
352
|
+
|
|
353
|
+
if original_content is None:
|
|
354
|
+
# Case 3: File doesn't exist - offer to create
|
|
355
|
+
console.print(f"\n[yellow]File '{filepath}' does not exist locally.[/yellow]")
|
|
356
|
+
|
|
357
|
+
# Show the proposed new content
|
|
358
|
+
console.print("\n[bold]Proposed new file content:[/bold]")
|
|
359
|
+
console.print(Panel(
|
|
360
|
+
Syntax(new_code, "python", theme="monokai", line_numbers=True),
|
|
361
|
+
border_style="blue",
|
|
362
|
+
))
|
|
363
|
+
|
|
364
|
+
if Confirm.ask(f"\n[bold]Create new file '{filepath}'?[/bold]", default=False):
|
|
365
|
+
if apply_fix(filepath, new_code):
|
|
366
|
+
console.print(f"[green]Created: {filepath}[/green]")
|
|
367
|
+
else:
|
|
368
|
+
console.print(f"[red]Failed to create file.[/red]")
|
|
369
|
+
else:
|
|
370
|
+
console.print("[dim]Skipped.[/dim]")
|
|
371
|
+
return
|
|
372
|
+
|
|
373
|
+
# Case 2: File exists - show diff and offer to patch
|
|
374
|
+
diff_text = compute_diff(original_content, new_code, filepath)
|
|
375
|
+
|
|
376
|
+
if not diff_text.strip():
|
|
377
|
+
console.print("[yellow]The AI suggested no changes to the file.[/yellow]")
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
# Display diff
|
|
381
|
+
display_diff(diff_text)
|
|
382
|
+
|
|
383
|
+
# Ask to apply
|
|
384
|
+
if Confirm.ask(f"[bold]Apply this fix to '{filepath}'?[/bold]", default=True):
|
|
385
|
+
# Create backup
|
|
386
|
+
backup_path = create_backup(filepath)
|
|
387
|
+
if backup_path:
|
|
388
|
+
console.print(f"[dim]Backup created: {backup_path}[/dim]")
|
|
389
|
+
|
|
390
|
+
# Apply fix
|
|
391
|
+
if apply_fix(filepath, new_code):
|
|
392
|
+
console.print(f"[green]Success! Fixed: {filepath}[/green]")
|
|
393
|
+
else:
|
|
394
|
+
console.print(f"[red]Failed to apply fix.[/red]")
|
|
395
|
+
else:
|
|
396
|
+
console.print("[dim]Skipped.[/dim]")
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def interactive_fix_v2(result: FixResultV2):
|
|
400
|
+
"""Interactive workflow for V2 fixes with multiple files.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
result: FixResultV2 from engine
|
|
404
|
+
"""
|
|
405
|
+
# No filepath - general advice
|
|
406
|
+
if result.filepath is None:
|
|
407
|
+
display_general_advice(result)
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
# Display the full analysis
|
|
411
|
+
display_fix_result_v2(result)
|
|
412
|
+
|
|
413
|
+
# Handle primary fix
|
|
414
|
+
_apply_single_fix(result.filepath, result.full_code_block, "primary fix")
|
|
415
|
+
|
|
416
|
+
# Handle additional fixes
|
|
417
|
+
for fix in result.additional_fixes:
|
|
418
|
+
console.print()
|
|
419
|
+
console.print(f"[bold cyan]Additional fix for {fix.filepath}:[/bold cyan]")
|
|
420
|
+
console.print(f"[dim]{fix.explanation}[/dim]")
|
|
421
|
+
_apply_single_fix(fix.filepath, fix.full_code_block, "this fix")
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _apply_single_fix(filepath: str, new_code: str, fix_name: str = "fix"):
|
|
425
|
+
"""Apply a single fix interactively.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
filepath: Path to the file
|
|
429
|
+
new_code: New code content
|
|
430
|
+
fix_name: Name for the fix in prompts
|
|
431
|
+
"""
|
|
432
|
+
if not new_code:
|
|
433
|
+
console.print(f"[yellow]No code provided for {filepath}[/yellow]")
|
|
434
|
+
return
|
|
435
|
+
|
|
436
|
+
resolved = resolve_filepath(filepath)
|
|
437
|
+
original = read_file_content(resolved)
|
|
438
|
+
|
|
439
|
+
if original is None:
|
|
440
|
+
console.print(f"\n[yellow]File '{resolved}' does not exist.[/yellow]")
|
|
441
|
+
if Confirm.ask(f"Create new file?", default=False):
|
|
442
|
+
if apply_fix(resolved, new_code):
|
|
443
|
+
console.print(f"[green]Created: {resolved}[/green]")
|
|
444
|
+
return
|
|
445
|
+
|
|
446
|
+
diff_text = compute_diff(original, new_code, resolved)
|
|
447
|
+
if not diff_text.strip():
|
|
448
|
+
console.print(f"[yellow]No changes needed for {resolved}[/yellow]")
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
display_diff(diff_text)
|
|
452
|
+
|
|
453
|
+
if Confirm.ask(f"[bold]Apply {fix_name} to '{resolved}'?[/bold]", default=True):
|
|
454
|
+
backup = create_backup(resolved)
|
|
455
|
+
if backup:
|
|
456
|
+
console.print(f"[dim]Backup: {backup}[/dim]")
|
|
457
|
+
if apply_fix(resolved, new_code):
|
|
458
|
+
console.print(f"[green]Fixed: {resolved}[/green]")
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def analyze_and_interact(
|
|
462
|
+
error_log: str,
|
|
463
|
+
deep: bool = False,
|
|
464
|
+
language_hint: str | None = None,
|
|
465
|
+
):
|
|
466
|
+
"""Analyze error and run interactive fix workflow.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
error_log: The error log string
|
|
470
|
+
deep: Whether to use V2 deep debugging
|
|
471
|
+
language_hint: Optional language hint
|
|
472
|
+
"""
|
|
473
|
+
if not error_log:
|
|
474
|
+
console.print("[red]No error provided.[/red]")
|
|
475
|
+
return
|
|
476
|
+
|
|
477
|
+
# Extract file context
|
|
478
|
+
context = ""
|
|
479
|
+
contexts = []
|
|
480
|
+
analysis_ctx = None
|
|
481
|
+
|
|
482
|
+
if deep:
|
|
483
|
+
# V2: Use context builder for deep debugging with project awareness
|
|
484
|
+
with console.status("[bold blue]Scanning project and analyzing error..."):
|
|
485
|
+
try:
|
|
486
|
+
from roma_debug.tracing.context_builder import ContextBuilder
|
|
487
|
+
|
|
488
|
+
language = LANGUAGE_CHOICES.get(language_hint.lower()) if language_hint else None
|
|
489
|
+
builder = ContextBuilder(project_root=os.getcwd(), scan_project=True)
|
|
490
|
+
|
|
491
|
+
# Show project info
|
|
492
|
+
project_info = builder.project_info
|
|
493
|
+
console.print(f"[cyan]Project: {project_info.project_type}[/cyan]")
|
|
494
|
+
if project_info.frameworks_detected:
|
|
495
|
+
console.print(f"[cyan]Frameworks: {', '.join(project_info.frameworks_detected)}[/cyan]")
|
|
496
|
+
|
|
497
|
+
# Build analysis context
|
|
498
|
+
analysis_ctx = builder.build_analysis_context(
|
|
499
|
+
error_log,
|
|
500
|
+
language_hint=language,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# Use deep context for comprehensive analysis
|
|
504
|
+
context = builder.get_deep_context(error_log, language_hint=language)
|
|
505
|
+
contexts = analysis_ctx.traceback_contexts
|
|
506
|
+
|
|
507
|
+
# Report what we found
|
|
508
|
+
if contexts:
|
|
509
|
+
console.print(f"[green]Found context from {len(contexts)} file(s)[/green]")
|
|
510
|
+
if analysis_ctx.upstream_context:
|
|
511
|
+
upstream_count = len(analysis_ctx.upstream_context.file_contexts)
|
|
512
|
+
if upstream_count > 0:
|
|
513
|
+
console.print(f"[green]Analyzed {upstream_count} upstream file(s)[/green]")
|
|
514
|
+
|
|
515
|
+
# Report error analysis findings
|
|
516
|
+
if analysis_ctx.error_analysis:
|
|
517
|
+
ea = analysis_ctx.error_analysis
|
|
518
|
+
console.print(f"[dim]Error type: {ea.error_type} ({ea.error_category})[/dim]")
|
|
519
|
+
if ea.relevant_files:
|
|
520
|
+
console.print(f"[green]Identified {len(ea.relevant_files)} relevant file(s):[/green]")
|
|
521
|
+
for rf in ea.relevant_files[:3]:
|
|
522
|
+
console.print(f" [dim]- {rf.path}[/dim]")
|
|
523
|
+
|
|
524
|
+
# Show entry points if no explicit files found
|
|
525
|
+
if not contexts and project_info.entry_points:
|
|
526
|
+
console.print(f"[yellow]No explicit traceback. Using entry points for context.[/yellow]")
|
|
527
|
+
for ep in project_info.entry_points[:2]:
|
|
528
|
+
console.print(f" [dim]- {ep.path}[/dim]")
|
|
529
|
+
|
|
530
|
+
except Exception as e:
|
|
531
|
+
console.print(f"[yellow]Deep analysis failed, falling back to basic: {e}[/yellow]")
|
|
532
|
+
import traceback
|
|
533
|
+
traceback.print_exc()
|
|
534
|
+
context, contexts = get_file_context(error_log)
|
|
535
|
+
else:
|
|
536
|
+
# V1: Basic context extraction - but upgrade to deep if no traceback found
|
|
537
|
+
with console.status("[bold blue]Reading source files..."):
|
|
538
|
+
context, contexts = get_file_context(error_log)
|
|
539
|
+
if context:
|
|
540
|
+
primary = get_primary_file(contexts)
|
|
541
|
+
if primary:
|
|
542
|
+
console.print(f"[green]Found context from {len(contexts)} file(s)[/green]")
|
|
543
|
+
if primary.function_name:
|
|
544
|
+
console.print(f"[dim]Primary: {primary.filepath} (function: {primary.function_name})[/dim]")
|
|
545
|
+
elif primary.class_name:
|
|
546
|
+
console.print(f"[dim]Primary: {primary.filepath} (class: {primary.class_name})[/dim]")
|
|
547
|
+
else:
|
|
548
|
+
# No traceback found - automatically use deep project awareness
|
|
549
|
+
console.print("[yellow]No traceback found. Activating project awareness...[/yellow]")
|
|
550
|
+
try:
|
|
551
|
+
from roma_debug.tracing.context_builder import ContextBuilder
|
|
552
|
+
|
|
553
|
+
language = LANGUAGE_CHOICES.get(language_hint.lower()) if language_hint else None
|
|
554
|
+
builder = ContextBuilder(project_root=os.getcwd(), scan_project=True)
|
|
555
|
+
|
|
556
|
+
# Show project info
|
|
557
|
+
project_info = builder.project_info
|
|
558
|
+
console.print(f"[cyan]Project: {project_info.project_type}[/cyan]")
|
|
559
|
+
if project_info.frameworks_detected:
|
|
560
|
+
console.print(f"[cyan]Frameworks: {', '.join(project_info.frameworks_detected)}[/cyan]")
|
|
561
|
+
if project_info.entry_points:
|
|
562
|
+
console.print(f"[cyan]Entry points: {', '.join(ep.path for ep in project_info.entry_points[:3])}[/cyan]")
|
|
563
|
+
|
|
564
|
+
# Build analysis context with project awareness
|
|
565
|
+
analysis_ctx = builder.build_analysis_context(
|
|
566
|
+
error_log,
|
|
567
|
+
language_hint=language,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
# Get deep context
|
|
571
|
+
context = builder.get_deep_context(error_log, language_hint=language)
|
|
572
|
+
|
|
573
|
+
# Report error analysis findings
|
|
574
|
+
if analysis_ctx.error_analysis:
|
|
575
|
+
ea = analysis_ctx.error_analysis
|
|
576
|
+
console.print(f"[dim]Error type: {ea.error_type} ({ea.error_category})[/dim]")
|
|
577
|
+
if ea.relevant_files:
|
|
578
|
+
console.print(f"[green]Found {len(ea.relevant_files)} relevant file(s):[/green]")
|
|
579
|
+
for rf in ea.relevant_files[:3]:
|
|
580
|
+
console.print(f" [dim]- {rf.path}[/dim]")
|
|
581
|
+
|
|
582
|
+
# Switch to V2 analysis
|
|
583
|
+
deep = True
|
|
584
|
+
|
|
585
|
+
except Exception as e:
|
|
586
|
+
console.print(f"[yellow]Project scanning failed: {e}[/yellow]")
|
|
587
|
+
|
|
588
|
+
# Analyze with Gemini
|
|
589
|
+
with console.status("[bold green]Analyzing with Gemini..."):
|
|
590
|
+
try:
|
|
591
|
+
if deep:
|
|
592
|
+
result = analyze_error_v2(error_log, context)
|
|
593
|
+
else:
|
|
594
|
+
result = analyze_error(error_log, context)
|
|
595
|
+
except RuntimeError as e:
|
|
596
|
+
console.print(f"\n[red]Configuration Error:[/red] {e}")
|
|
597
|
+
return
|
|
598
|
+
except Exception as e:
|
|
599
|
+
console.print(f"\n[red]Analysis failed:[/red] {e}")
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
# Interactive fix workflow
|
|
603
|
+
if deep and isinstance(result, FixResultV2):
|
|
604
|
+
interactive_fix_v2(result)
|
|
605
|
+
else:
|
|
606
|
+
interactive_fix(result)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def interactive_mode(deep: bool = False, language_hint: str | None = None):
|
|
610
|
+
"""Run interactive mode - paste errors, get fixes.
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
deep: Whether to use V2 deep debugging
|
|
614
|
+
language_hint: Optional language hint
|
|
615
|
+
"""
|
|
616
|
+
print_welcome()
|
|
617
|
+
|
|
618
|
+
if deep:
|
|
619
|
+
console.print("[bold cyan]Deep Debugging Mode Enabled[/bold cyan]")
|
|
620
|
+
console.print("[dim]Analyzing imports and call chains for root cause analysis[/dim]")
|
|
621
|
+
console.print()
|
|
622
|
+
|
|
623
|
+
# Check for API key
|
|
624
|
+
status = get_api_key_status()
|
|
625
|
+
if status != "OK":
|
|
626
|
+
console.print("[red]Error: GEMINI_API_KEY not configured[/red]")
|
|
627
|
+
console.print("[dim]Set it in .env file or: export GEMINI_API_KEY=your-key[/dim]")
|
|
628
|
+
console.print("[dim]Get a key at: https://aistudio.google.com/apikey[/dim]")
|
|
629
|
+
sys.exit(1)
|
|
630
|
+
|
|
631
|
+
console.print()
|
|
632
|
+
|
|
633
|
+
while True:
|
|
634
|
+
# Get error input
|
|
635
|
+
error_log = get_multiline_input()
|
|
636
|
+
|
|
637
|
+
if not error_log:
|
|
638
|
+
break
|
|
639
|
+
|
|
640
|
+
# Analyze and offer to fix
|
|
641
|
+
analyze_and_interact(error_log, deep=deep, language_hint=language_hint)
|
|
642
|
+
|
|
643
|
+
# Ask to continue
|
|
644
|
+
console.print()
|
|
645
|
+
if not Confirm.ask("[bold]Debug another error?[/bold]", default=True):
|
|
646
|
+
break
|
|
647
|
+
console.print()
|
|
648
|
+
|
|
649
|
+
console.print("\n[blue]Goodbye![/blue]")
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
@click.command()
|
|
653
|
+
@click.option("--serve", is_flag=True, help="Start the web API server")
|
|
654
|
+
@click.option("--port", default=8080, help="Port for API server")
|
|
655
|
+
@click.option("--version", "-v", is_flag=True, help="Show version")
|
|
656
|
+
@click.option("--no-apply", is_flag=True, help="Show fixes without applying")
|
|
657
|
+
@click.option("--deep", is_flag=True, help="Enable deep debugging (V2) with root cause analysis")
|
|
658
|
+
@click.option(
|
|
659
|
+
"--language", "-l",
|
|
660
|
+
type=click.Choice(list(LANGUAGE_CHOICES.keys()), case_sensitive=False),
|
|
661
|
+
help="Language hint for the error (python, javascript, typescript, go, rust, java)"
|
|
662
|
+
)
|
|
663
|
+
@click.argument("error_input", required=False)
|
|
664
|
+
def cli(serve, port, version, no_apply, deep, language, error_input):
|
|
665
|
+
"""ROMA Debug - AI-powered code debugger with auto-fix.
|
|
666
|
+
|
|
667
|
+
Just run 'roma' to start interactive mode and paste your errors.
|
|
668
|
+
|
|
669
|
+
Examples:
|
|
670
|
+
|
|
671
|
+
roma # Interactive mode
|
|
672
|
+
|
|
673
|
+
roma error.log # Analyze a file directly
|
|
674
|
+
|
|
675
|
+
roma --deep error.log # Deep debugging with root cause analysis
|
|
676
|
+
|
|
677
|
+
roma --language go error.log # Specify language hint
|
|
678
|
+
|
|
679
|
+
roma --serve # Start web API server
|
|
680
|
+
|
|
681
|
+
roma --no-apply error.log # Show fix without applying
|
|
682
|
+
"""
|
|
683
|
+
if version:
|
|
684
|
+
console.print(f"roma-debug {__version__}")
|
|
685
|
+
return
|
|
686
|
+
|
|
687
|
+
if serve:
|
|
688
|
+
import uvicorn
|
|
689
|
+
console.print(f"[green]Starting ROMA Debug API on http://127.0.0.1:{port}[/green]")
|
|
690
|
+
console.print(f"[dim]API docs at http://127.0.0.1:{port}/docs[/dim]")
|
|
691
|
+
uvicorn.run("roma_debug.server:app", host="127.0.0.1", port=port)
|
|
692
|
+
return
|
|
693
|
+
|
|
694
|
+
if error_input:
|
|
695
|
+
# Direct file/string mode
|
|
696
|
+
if os.path.isfile(error_input):
|
|
697
|
+
with open(error_input, 'r') as f:
|
|
698
|
+
error_log = f.read()
|
|
699
|
+
else:
|
|
700
|
+
error_log = error_input
|
|
701
|
+
|
|
702
|
+
print_welcome()
|
|
703
|
+
|
|
704
|
+
if deep:
|
|
705
|
+
console.print("[bold cyan]Deep Debugging Mode[/bold cyan]")
|
|
706
|
+
console.print()
|
|
707
|
+
|
|
708
|
+
if no_apply:
|
|
709
|
+
# Just show fix without interactive apply
|
|
710
|
+
if deep:
|
|
711
|
+
# V2: Use ContextBuilder for deep analysis
|
|
712
|
+
try:
|
|
713
|
+
from roma_debug.tracing.context_builder import ContextBuilder
|
|
714
|
+
|
|
715
|
+
lang_hint = LANGUAGE_CHOICES.get(language.lower()) if language else None
|
|
716
|
+
builder = ContextBuilder(project_root=os.getcwd())
|
|
717
|
+
analysis_ctx = builder.build_analysis_context(
|
|
718
|
+
error_log,
|
|
719
|
+
language_hint=lang_hint,
|
|
720
|
+
)
|
|
721
|
+
context = builder.get_context_for_prompt(analysis_ctx)
|
|
722
|
+
except Exception as e:
|
|
723
|
+
console.print(f"[yellow]Deep analysis failed, using basic: {e}[/yellow]")
|
|
724
|
+
context, _ = get_file_context(error_log)
|
|
725
|
+
|
|
726
|
+
result = analyze_error_v2(error_log, context)
|
|
727
|
+
if isinstance(result, FixResultV2):
|
|
728
|
+
display_fix_result_v2(result)
|
|
729
|
+
else:
|
|
730
|
+
display_fix_result(result)
|
|
731
|
+
else:
|
|
732
|
+
context, _ = get_file_context(error_log)
|
|
733
|
+
result = analyze_error(error_log, context)
|
|
734
|
+
if result.filepath is None:
|
|
735
|
+
display_general_advice(result)
|
|
736
|
+
else:
|
|
737
|
+
display_fix_result(result)
|
|
738
|
+
|
|
739
|
+
console.print("\n[bold]Suggested Code:[/bold]")
|
|
740
|
+
console.print(Panel(
|
|
741
|
+
Syntax(result.full_code_block, "python", theme="monokai", line_numbers=True),
|
|
742
|
+
border_style="green",
|
|
743
|
+
))
|
|
744
|
+
else:
|
|
745
|
+
analyze_and_interact(error_log, deep=deep, language_hint=language)
|
|
746
|
+
return
|
|
747
|
+
|
|
748
|
+
# Default: interactive mode
|
|
749
|
+
interactive_mode(deep=deep, language_hint=language)
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
if __name__ == "__main__":
|
|
753
|
+
cli()
|