repodoc 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.
- repodoc/__init__.py +3 -0
- repodoc/cli.py +98 -0
- repodoc/commands/__init__.py +10 -0
- repodoc/commands/base.py +284 -0
- repodoc/commands/deadcode.py +114 -0
- repodoc/commands/diet.py +126 -0
- repodoc/commands/docker.py +150 -0
- repodoc/commands/report.py +139 -0
- repodoc/commands/scan.py +255 -0
- repodoc/commands/tour.py +98 -0
- repodoc/core/__init__.py +26 -0
- repodoc/core/copilot.py +170 -0
- repodoc/core/exceptions.py +135 -0
- repodoc/core/logger.py +84 -0
- repodoc/core/parser.py +155 -0
- repodoc/prompts/__init__.py +5 -0
- repodoc/prompts/templates.py +134 -0
- repodoc/prompts/v1/deadcode.txt +98 -0
- repodoc/prompts/v1/diet.txt +88 -0
- repodoc/prompts/v1/docker.txt +109 -0
- repodoc/prompts/v1/report.txt +53 -0
- repodoc/prompts/v1/scan.txt +115 -0
- repodoc/prompts/v1/tour.txt +90 -0
- repodoc/renderers/__init__.py +7 -0
- repodoc/renderers/base.py +68 -0
- repodoc/renderers/command_renderers.py +306 -0
- repodoc/renderers/json_renderer.py +50 -0
- repodoc/renderers/terminal_renderer.py +280 -0
- repodoc/schemas/__init__.py +70 -0
- repodoc/schemas/base.py +76 -0
- repodoc/schemas/deadcode.py +47 -0
- repodoc/schemas/diet.py +58 -0
- repodoc/schemas/docker.py +52 -0
- repodoc/schemas/report.py +15 -0
- repodoc/schemas/scan.py +50 -0
- repodoc/schemas/tour.py +56 -0
- repodoc-0.1.0.dist-info/METADATA +509 -0
- repodoc-0.1.0.dist-info/RECORD +41 -0
- repodoc-0.1.0.dist-info/WHEEL +4 -0
- repodoc-0.1.0.dist-info/entry_points.txt +2 -0
- repodoc-0.1.0.dist-info/licenses/LICENSE +21 -0
repodoc/__init__.py
ADDED
repodoc/cli.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""CLI entry point for RepoDoctor."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import locale
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from repodoc import __version__
|
|
11
|
+
from repodoc.commands import deadcode, diet, docker, report, scan, tour
|
|
12
|
+
|
|
13
|
+
# Force UTF-8 encoding for all output streams (Windows compatibility)
|
|
14
|
+
if sys.platform == "win32":
|
|
15
|
+
# Set console code page to UTF-8 on Windows
|
|
16
|
+
with contextlib.suppress(Exception):
|
|
17
|
+
import ctypes
|
|
18
|
+
|
|
19
|
+
kernel32 = ctypes.windll.kernel32
|
|
20
|
+
kernel32.SetConsoleCP(65001)
|
|
21
|
+
kernel32.SetConsoleOutputCP(65001)
|
|
22
|
+
|
|
23
|
+
# Set default encoding for Python's text streams
|
|
24
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
25
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined]
|
|
26
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
27
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined]
|
|
28
|
+
|
|
29
|
+
# Set locale to UTF-8 if possible
|
|
30
|
+
with contextlib.suppress(locale.Error):
|
|
31
|
+
locale.setlocale(locale.LC_ALL, "en_US.UTF-8")
|
|
32
|
+
with contextlib.suppress(locale.Error):
|
|
33
|
+
locale.setlocale(locale.LC_ALL, "C.UTF-8")
|
|
34
|
+
|
|
35
|
+
app = typer.Typer(
|
|
36
|
+
name="repodoc",
|
|
37
|
+
help="""
|
|
38
|
+
🏥 RepoDoctor - AI-Powered Repository Health Analysis
|
|
39
|
+
|
|
40
|
+
RepoDoctor uses GitHub Copilot CLI to analyze your codebase and provide
|
|
41
|
+
actionable insights on repository health, code quality, and best practices.
|
|
42
|
+
|
|
43
|
+
📋 Quick Start:
|
|
44
|
+
|
|
45
|
+
$ repodoc scan # Full health check
|
|
46
|
+
$ repodoc diet # Find bloat and missing files
|
|
47
|
+
$ repodoc tour # Generate TOUR.md for onboarding
|
|
48
|
+
$ repodoc docker # Analyze Dockerfiles
|
|
49
|
+
$ repodoc deadcode # Detect unused code
|
|
50
|
+
|
|
51
|
+
⚙️ Requirements:
|
|
52
|
+
|
|
53
|
+
• GitHub Copilot CLI must be installed and authenticated
|
|
54
|
+
• Run from within a code repository directory
|
|
55
|
+
|
|
56
|
+
📖 For detailed help on each command:
|
|
57
|
+
|
|
58
|
+
$ repodoc <command> --help
|
|
59
|
+
""",
|
|
60
|
+
add_completion=False,
|
|
61
|
+
no_args_is_help=True,
|
|
62
|
+
)
|
|
63
|
+
# Force UTF-8 for Rich console output
|
|
64
|
+
console = Console(force_terminal=None, legacy_windows=False)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def version_callback(value: bool) -> None:
|
|
68
|
+
"""Print version and exit."""
|
|
69
|
+
if value:
|
|
70
|
+
console.print(f"[bold]RepoDoctor[/bold] version {__version__}")
|
|
71
|
+
raise typer.Exit()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.callback()
|
|
75
|
+
def main(
|
|
76
|
+
version: bool = typer.Option(
|
|
77
|
+
False,
|
|
78
|
+
"--version",
|
|
79
|
+
help="Show version and exit.",
|
|
80
|
+
callback=version_callback,
|
|
81
|
+
is_eager=True,
|
|
82
|
+
),
|
|
83
|
+
) -> None:
|
|
84
|
+
"""RepoDoctor - Copilot-powered repository health analysis."""
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Register commands
|
|
89
|
+
app.command(name="diet")(diet)
|
|
90
|
+
app.command(name="tour")(tour)
|
|
91
|
+
app.command(name="docker")(docker)
|
|
92
|
+
app.command(name="deadcode")(deadcode)
|
|
93
|
+
app.command(name="scan")(scan)
|
|
94
|
+
app.command(name="report")(report)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
app()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Command implementations for RepoDoctor CLI."""
|
|
2
|
+
|
|
3
|
+
from repodoc.commands.deadcode import deadcode
|
|
4
|
+
from repodoc.commands.diet import diet
|
|
5
|
+
from repodoc.commands.docker import docker
|
|
6
|
+
from repodoc.commands.report import report
|
|
7
|
+
from repodoc.commands.scan import scan
|
|
8
|
+
from repodoc.commands.tour import tour
|
|
9
|
+
|
|
10
|
+
__all__ = ["diet", "tour", "docker", "deadcode", "scan", "report"]
|
repodoc/commands/base.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Base command infrastructure with common patterns and utilities."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from repodoc.core.exceptions import RepoDocError
|
|
14
|
+
from repodoc.core.logger import get_logger
|
|
15
|
+
|
|
16
|
+
# Force UTF-8 for Rich console output
|
|
17
|
+
console = Console(force_terminal=None, legacy_windows=False)
|
|
18
|
+
logger = get_logger()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_repo_root() -> Path:
|
|
22
|
+
"""
|
|
23
|
+
Get the repository root directory (current working directory).
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Path to repository root
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
EmptyRepositoryError: If repository has no analyzable content
|
|
30
|
+
"""
|
|
31
|
+
from repodoc.core.exceptions import EmptyRepositoryError
|
|
32
|
+
|
|
33
|
+
cwd = Path.cwd().resolve()
|
|
34
|
+
logger.debug(f"Working directory: {cwd}")
|
|
35
|
+
|
|
36
|
+
# Basic validation: check if directory has any code files
|
|
37
|
+
has_content = False
|
|
38
|
+
common_code_extensions = {
|
|
39
|
+
".py",
|
|
40
|
+
".js",
|
|
41
|
+
".ts",
|
|
42
|
+
".java",
|
|
43
|
+
".go",
|
|
44
|
+
".rs",
|
|
45
|
+
".rb",
|
|
46
|
+
".cpp",
|
|
47
|
+
".c",
|
|
48
|
+
".h",
|
|
49
|
+
".css",
|
|
50
|
+
".html",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
# Quick check: look for at least one code file in first few levels
|
|
55
|
+
for item in cwd.iterdir():
|
|
56
|
+
if item.is_file() and item.suffix in common_code_extensions:
|
|
57
|
+
has_content = True
|
|
58
|
+
break
|
|
59
|
+
elif item.is_dir() and not item.name.startswith("."):
|
|
60
|
+
# Check one level deep
|
|
61
|
+
for subitem in item.iterdir():
|
|
62
|
+
if subitem.is_file() and subitem.suffix in common_code_extensions:
|
|
63
|
+
has_content = True
|
|
64
|
+
break
|
|
65
|
+
if has_content:
|
|
66
|
+
break
|
|
67
|
+
except (PermissionError, OSError):
|
|
68
|
+
# If we can't read, assume it's okay
|
|
69
|
+
logger.warning("Could not fully validate repository content")
|
|
70
|
+
return cwd
|
|
71
|
+
|
|
72
|
+
if not has_content:
|
|
73
|
+
logger.warning(f"No code files found in {cwd}")
|
|
74
|
+
raise EmptyRepositoryError()
|
|
75
|
+
|
|
76
|
+
return cwd
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def ensure_repodoc_dir(repo_root: Path) -> Path:
|
|
80
|
+
"""Ensure .repodoc directory exists in repository root."""
|
|
81
|
+
repodoc_dir = repo_root / ".repodoc"
|
|
82
|
+
repodoc_dir.mkdir(exist_ok=True)
|
|
83
|
+
logger.debug(f"Ensured .repodoc directory: {repodoc_dir}")
|
|
84
|
+
return repodoc_dir
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def save_json_output(data: Any, output_path: Path) -> None:
|
|
88
|
+
"""Save structured data as JSON to specified path."""
|
|
89
|
+
try:
|
|
90
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
91
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
92
|
+
console.print(f"[green]✓[/green] JSON output saved to: {output_path}")
|
|
93
|
+
logger.info(f"Saved JSON output to {output_path}")
|
|
94
|
+
except Exception as e:
|
|
95
|
+
error_msg = f"Failed to save JSON output to {output_path}: {e}"
|
|
96
|
+
logger.error(error_msg)
|
|
97
|
+
raise RepoDocError(error_msg) from e
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def save_text_output(
|
|
101
|
+
output_path: Path, render_func: Callable[..., None], *args: Any, **kwargs: Any
|
|
102
|
+
) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Save formatted text output to file by capturing rendered content.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
output_path: Path to save the output
|
|
108
|
+
render_func: Renderer function to call (e.g., renderer.render)
|
|
109
|
+
*args, **kwargs: Arguments to pass to the render function
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
from rich.console import Console
|
|
113
|
+
|
|
114
|
+
# Create a file console that writes to the output file
|
|
115
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
117
|
+
file_console = Console(
|
|
118
|
+
file=f,
|
|
119
|
+
width=120,
|
|
120
|
+
force_terminal=False, # Disable color codes for file
|
|
121
|
+
legacy_windows=False,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Create a new terminal renderer with the file console
|
|
125
|
+
from repodoc.renderers.terminal_renderer import TerminalRenderer
|
|
126
|
+
|
|
127
|
+
file_terminal = TerminalRenderer(
|
|
128
|
+
verbose=kwargs.get("verbose", False), console=file_console
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Replace the console in the renderer temporarily
|
|
132
|
+
original_terminal = kwargs.get("terminal")
|
|
133
|
+
if original_terminal:
|
|
134
|
+
# Create new renderer with file terminal
|
|
135
|
+
# Type ignore needed because __self__ is not part of Callable type
|
|
136
|
+
renderer_class = render_func.__self__.__class__ # type: ignore[attr-defined]
|
|
137
|
+
file_renderer = renderer_class(file_terminal)
|
|
138
|
+
# Call render method with file renderer
|
|
139
|
+
getattr(file_renderer, render_func.__name__)(*args) # type: ignore[attr-defined]
|
|
140
|
+
|
|
141
|
+
console.print(f"[green]✓[/green] Output saved to: {output_path}")
|
|
142
|
+
logger.info(f"Saved text output to {output_path}")
|
|
143
|
+
except Exception as e:
|
|
144
|
+
error_msg = f"Failed to save output to {output_path}: {e}"
|
|
145
|
+
logger.error(error_msg)
|
|
146
|
+
raise RepoDocError(error_msg) from e
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def handle_json_flag(data: dict[str, Any], json_flag: bool, out_path: str | None) -> None:
|
|
150
|
+
"""Handle --json and --out flags for structured output."""
|
|
151
|
+
if json_flag and not out_path:
|
|
152
|
+
# Use print() for pure JSON output without Rich formatting
|
|
153
|
+
print(json.dumps(data, indent=2))
|
|
154
|
+
elif out_path:
|
|
155
|
+
output_file = Path(out_path)
|
|
156
|
+
save_json_output(data, output_file)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def print_success_message(command: str, next_actions: list[str] | None = None) -> None:
|
|
160
|
+
"""
|
|
161
|
+
Print a success message with optional next actions.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
command: Name of the command that succeeded
|
|
165
|
+
next_actions: List of suggested next actions
|
|
166
|
+
"""
|
|
167
|
+
console.print(f"\n[green bold]✓ {command} completed successfully![/green bold]")
|
|
168
|
+
|
|
169
|
+
if next_actions:
|
|
170
|
+
console.print("\n[cyan]💡 Next steps:[/cyan]")
|
|
171
|
+
for action in next_actions:
|
|
172
|
+
console.print(f" • {action}")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def print_progress(message: str, emoji: str = "⏳") -> None:
|
|
176
|
+
"""Print a progress message with emoji."""
|
|
177
|
+
console.print(f"\n{emoji} {message}...")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def render_health_score(score: float, label: str = "Health Score") -> None:
|
|
181
|
+
"""Render a health score with color coding."""
|
|
182
|
+
if score >= 80:
|
|
183
|
+
color = "green"
|
|
184
|
+
emoji = "✓"
|
|
185
|
+
elif score >= 60:
|
|
186
|
+
color = "yellow"
|
|
187
|
+
emoji = "⚠"
|
|
188
|
+
else:
|
|
189
|
+
color = "red"
|
|
190
|
+
emoji = "✗"
|
|
191
|
+
|
|
192
|
+
console.print(
|
|
193
|
+
Panel(f"[{color} bold]{emoji} {label}: {score}/100[/{color} bold]", border_style=color)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def render_issues_table(issues: list[dict[str, Any]], title: str = "Issues") -> None:
|
|
198
|
+
"""Render a table of issues with severity indicators."""
|
|
199
|
+
if not issues:
|
|
200
|
+
console.print(f"[green]✓[/green] No {title.lower()} found")
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
table = Table(title=title, show_header=True, header_style="bold magenta")
|
|
204
|
+
table.add_column("Severity", style="dim", width=10)
|
|
205
|
+
table.add_column("Category", style="cyan", width=15)
|
|
206
|
+
table.add_column("Description")
|
|
207
|
+
|
|
208
|
+
for issue in issues:
|
|
209
|
+
severity = issue.get("severity", "unknown")
|
|
210
|
+
category = issue.get("category", "general")
|
|
211
|
+
description = issue.get("description", "")
|
|
212
|
+
|
|
213
|
+
severity_color = {
|
|
214
|
+
"critical": "red bold",
|
|
215
|
+
"high": "red",
|
|
216
|
+
"medium": "yellow",
|
|
217
|
+
"low": "blue",
|
|
218
|
+
"info": "dim",
|
|
219
|
+
}.get(severity, "white")
|
|
220
|
+
|
|
221
|
+
table.add_row(
|
|
222
|
+
f"[{severity_color}]{severity.upper()}[/{severity_color}]", category, description
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
console.print(table)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def render_recommendations(recommendations: list[dict[str, Any]]) -> None:
|
|
229
|
+
"""Render recommendations as a formatted list."""
|
|
230
|
+
if not recommendations:
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
console.print("\n[bold cyan]Recommendations:[/bold cyan]")
|
|
234
|
+
for i, rec in enumerate(recommendations, 1):
|
|
235
|
+
title = rec.get("title", "Recommendation")
|
|
236
|
+
description = rec.get("description", "")
|
|
237
|
+
console.print(f" {i}. [yellow]{title}[/yellow]")
|
|
238
|
+
console.print(f" {description}\n")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def handle_command_error(error: Exception, verbose: bool = False) -> None:
|
|
242
|
+
"""Handle command errors with appropriate logging and user messages."""
|
|
243
|
+
from repodoc.core.exceptions import (
|
|
244
|
+
CopilotExecutionError,
|
|
245
|
+
CopilotNotFoundError,
|
|
246
|
+
CopilotTimeoutError,
|
|
247
|
+
EmptyRepositoryError,
|
|
248
|
+
InvalidRepositoryError,
|
|
249
|
+
OutputParseError,
|
|
250
|
+
RepoDocError,
|
|
251
|
+
SchemaValidationError,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
error_msg = str(error)
|
|
255
|
+
logger.error(f"Command failed: {error_msg}", exc_info=verbose)
|
|
256
|
+
|
|
257
|
+
# Handle specific error types with tailored messages
|
|
258
|
+
if isinstance(error, CopilotNotFoundError):
|
|
259
|
+
console.print("[red bold]✗ Copilot CLI Not Found[/red bold]")
|
|
260
|
+
console.print(error_msg)
|
|
261
|
+
elif isinstance(error, CopilotTimeoutError):
|
|
262
|
+
console.print("[red bold]✗ Operation Timed Out[/red bold]")
|
|
263
|
+
console.print(error_msg)
|
|
264
|
+
elif isinstance(error, CopilotExecutionError):
|
|
265
|
+
console.print("[red bold]✗ Copilot Execution Failed[/red bold]")
|
|
266
|
+
console.print(error_msg)
|
|
267
|
+
elif isinstance(error, (OutputParseError, SchemaValidationError)):
|
|
268
|
+
console.print("[red bold]✗ Output Parsing Failed[/red bold]")
|
|
269
|
+
console.print(error_msg)
|
|
270
|
+
if verbose:
|
|
271
|
+
console.print("\n[dim]Enable --verbose for more details[/dim]")
|
|
272
|
+
elif isinstance(error, (EmptyRepositoryError, InvalidRepositoryError)):
|
|
273
|
+
console.print("[red bold]✗ Repository Error[/red bold]")
|
|
274
|
+
console.print(error_msg)
|
|
275
|
+
elif isinstance(error, RepoDocError):
|
|
276
|
+
console.print(f"[red]✗ Error:[/red] {error_msg}")
|
|
277
|
+
else:
|
|
278
|
+
console.print(f"[red]✗ Unexpected error:[/red] {error_msg}")
|
|
279
|
+
if verbose:
|
|
280
|
+
console.print_exception()
|
|
281
|
+
else:
|
|
282
|
+
console.print("\n[dim]Run with --verbose for full error details[/dim]")
|
|
283
|
+
|
|
284
|
+
raise typer.Exit(code=1)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Deadcode command: Detect unused code with confidence levels."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
|
|
9
|
+
from repodoc.commands.base import (
|
|
10
|
+
console,
|
|
11
|
+
get_repo_root,
|
|
12
|
+
handle_command_error,
|
|
13
|
+
handle_json_flag,
|
|
14
|
+
save_text_output,
|
|
15
|
+
)
|
|
16
|
+
from repodoc.core.copilot import CopilotInvoker
|
|
17
|
+
from repodoc.core.logger import get_logger
|
|
18
|
+
from repodoc.core.parser import OutputParser
|
|
19
|
+
from repodoc.prompts import get_prompt_loader
|
|
20
|
+
from repodoc.renderers.command_renderers import DeadCodeRenderer
|
|
21
|
+
from repodoc.renderers.terminal_renderer import TerminalRenderer
|
|
22
|
+
from repodoc.schemas.deadcode import DeadCodeOutput
|
|
23
|
+
|
|
24
|
+
logger = get_logger()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def deadcode(
|
|
28
|
+
verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose output")] = False,
|
|
29
|
+
json_output: Annotated[
|
|
30
|
+
bool, typer.Option("--json", help="Output raw JSON instead of formatted text")
|
|
31
|
+
] = False,
|
|
32
|
+
out: Annotated[
|
|
33
|
+
str | None, typer.Option("--out", "-o", help="Save output to specified file")
|
|
34
|
+
] = None,
|
|
35
|
+
min_confidence: Annotated[
|
|
36
|
+
str,
|
|
37
|
+
typer.Option(
|
|
38
|
+
"--min-confidence", help="Minimum confidence level to report (low, medium, high)"
|
|
39
|
+
),
|
|
40
|
+
] = "medium",
|
|
41
|
+
timeout: Annotated[
|
|
42
|
+
int | None, typer.Option("--timeout", help="Timeout in seconds for Copilot CLI")
|
|
43
|
+
] = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""💀 Detect potentially dead code with confidence levels.
|
|
46
|
+
|
|
47
|
+
Identifies unused functions, classes, imports, and files based on
|
|
48
|
+
static analysis. Results include confidence levels for each finding.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
repo_root = get_repo_root()
|
|
52
|
+
|
|
53
|
+
# Validate confidence level
|
|
54
|
+
valid_levels = ["low", "medium", "high"]
|
|
55
|
+
if min_confidence not in valid_levels:
|
|
56
|
+
console.print(
|
|
57
|
+
f"[red]✗ Error:[/red] Invalid confidence level. "
|
|
58
|
+
f"Must be one of: {', '.join(valid_levels)}"
|
|
59
|
+
)
|
|
60
|
+
raise typer.Exit(code=1)
|
|
61
|
+
|
|
62
|
+
if verbose:
|
|
63
|
+
console.print(f"[dim]Detecting dead code in: {repo_root}[/dim]")
|
|
64
|
+
|
|
65
|
+
# Load prompt template
|
|
66
|
+
prompt_loader = get_prompt_loader()
|
|
67
|
+
prompt = prompt_loader.get_prompt("deadcode", repo_path=str(repo_root))
|
|
68
|
+
|
|
69
|
+
# Invoke Copilot CLI
|
|
70
|
+
copilot = CopilotInvoker(timeout=timeout)
|
|
71
|
+
|
|
72
|
+
if not json_output:
|
|
73
|
+
with console.status("[bold blue]Analyzing codebase for dead code...[/bold blue]"):
|
|
74
|
+
raw_output, _ = copilot.invoke_with_retry(prompt, cwd=repo_root)
|
|
75
|
+
else:
|
|
76
|
+
# Silent mode for JSON output
|
|
77
|
+
raw_output, _ = copilot.invoke_with_retry(prompt, cwd=repo_root)
|
|
78
|
+
|
|
79
|
+
# Parse and validate output
|
|
80
|
+
parser = OutputParser()
|
|
81
|
+
result = parser.parse_and_validate(raw_output, DeadCodeOutput)
|
|
82
|
+
|
|
83
|
+
# Handle JSON output (to stdout or file if --out specified)
|
|
84
|
+
if json_output:
|
|
85
|
+
handle_json_flag(result.model_dump(), json_output, out)
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
# Render human-readable output to terminal
|
|
89
|
+
terminal = TerminalRenderer(verbose=verbose, console=console)
|
|
90
|
+
renderer = DeadCodeRenderer(terminal)
|
|
91
|
+
renderer.render(result, min_confidence)
|
|
92
|
+
|
|
93
|
+
# If --out specified (without --json), save formatted text to file
|
|
94
|
+
if out:
|
|
95
|
+
save_text_output(
|
|
96
|
+
Path(out),
|
|
97
|
+
renderer.render,
|
|
98
|
+
result,
|
|
99
|
+
min_confidence,
|
|
100
|
+
terminal=terminal,
|
|
101
|
+
verbose=verbose,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
except ValidationError as e:
|
|
105
|
+
logger.error(f"Failed to validate deadcode output: {e}")
|
|
106
|
+
console.print("[red]✗ Failed to parse Copilot output[/red]")
|
|
107
|
+
if verbose:
|
|
108
|
+
console.print_exception()
|
|
109
|
+
raise typer.Exit(code=1) from None
|
|
110
|
+
except typer.Exit:
|
|
111
|
+
# Let typer.Exit propagate without handling
|
|
112
|
+
raise
|
|
113
|
+
except Exception as e:
|
|
114
|
+
handle_command_error(e, verbose)
|
repodoc/commands/diet.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Diet command: Analyze repository bloat and hygiene."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
|
|
9
|
+
from repodoc.commands.base import (
|
|
10
|
+
console,
|
|
11
|
+
get_repo_root,
|
|
12
|
+
handle_command_error,
|
|
13
|
+
print_success_message,
|
|
14
|
+
)
|
|
15
|
+
from repodoc.core.copilot import CopilotInvoker
|
|
16
|
+
from repodoc.core.logger import get_logger
|
|
17
|
+
from repodoc.core.parser import OutputParser
|
|
18
|
+
from repodoc.prompts import get_prompt_loader
|
|
19
|
+
from repodoc.renderers.command_renderers import DietRenderer
|
|
20
|
+
from repodoc.renderers.terminal_renderer import TerminalRenderer
|
|
21
|
+
from repodoc.schemas.diet import DietOutput
|
|
22
|
+
|
|
23
|
+
logger = get_logger()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def diet(
|
|
27
|
+
verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Enable verbose output")] = False,
|
|
28
|
+
json_output: Annotated[
|
|
29
|
+
bool, typer.Option("--json", help="Output JSON summary instead of generating DIET.md")
|
|
30
|
+
] = False,
|
|
31
|
+
out: Annotated[
|
|
32
|
+
str | None,
|
|
33
|
+
typer.Option("--out", "-o", help="Custom output path for DIET.md (default: DIET.md)"),
|
|
34
|
+
] = None,
|
|
35
|
+
timeout: Annotated[
|
|
36
|
+
int | None, typer.Option("--timeout", help="Timeout in seconds for Copilot CLI")
|
|
37
|
+
] = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""🍔 Analyze repository bloat and hygiene issues.
|
|
40
|
+
|
|
41
|
+
Generates a comprehensive diet analysis documenting repository size,
|
|
42
|
+
largest files/directories, suspected artifacts, and missing hygiene files.
|
|
43
|
+
|
|
44
|
+
By default, generates DIET.md in the repository root.
|
|
45
|
+
|
|
46
|
+
\b
|
|
47
|
+
Examples:
|
|
48
|
+
$ repodoc diet # Generate DIET.md (default)
|
|
49
|
+
$ repodoc diet --out docs/BLOAT.md # Save to custom path
|
|
50
|
+
$ repodoc diet --json # Output JSON summary instead
|
|
51
|
+
|
|
52
|
+
\b
|
|
53
|
+
What it analyzes:
|
|
54
|
+
• Largest files in the repository
|
|
55
|
+
• Potentially unused dependencies
|
|
56
|
+
• Missing hygiene files (.gitignore, LICENSE, etc.)
|
|
57
|
+
• Overall repository size and health
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
repo_root = get_repo_root()
|
|
61
|
+
|
|
62
|
+
if verbose and not json_output:
|
|
63
|
+
console.print(f"[dim]Running diet analysis on: {repo_root}[/dim]")
|
|
64
|
+
|
|
65
|
+
# Load prompt template
|
|
66
|
+
prompt_loader = get_prompt_loader()
|
|
67
|
+
prompt = prompt_loader.get_prompt("diet", repo_path=str(repo_root))
|
|
68
|
+
|
|
69
|
+
# Invoke Copilot CLI
|
|
70
|
+
copilot = CopilotInvoker(timeout=timeout)
|
|
71
|
+
|
|
72
|
+
if not json_output:
|
|
73
|
+
with console.status("[bold blue]🔍 Analyzing repository bloat...[/bold blue]"):
|
|
74
|
+
raw_output, _ = copilot.invoke_with_retry(prompt, cwd=repo_root)
|
|
75
|
+
else:
|
|
76
|
+
# Silent mode for JSON output
|
|
77
|
+
raw_output, _ = copilot.invoke_with_retry(prompt, cwd=repo_root)
|
|
78
|
+
|
|
79
|
+
# Parse and validate output
|
|
80
|
+
parser = OutputParser()
|
|
81
|
+
result = parser.parse_and_validate(raw_output, DietOutput)
|
|
82
|
+
|
|
83
|
+
# Handle JSON output
|
|
84
|
+
if json_output:
|
|
85
|
+
console.print_json(result.analysis.model_dump_json(indent=2))
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
# Generate DIET.md
|
|
89
|
+
diet_path = Path(out) if out else repo_root / "DIET.md"
|
|
90
|
+
|
|
91
|
+
# Write the DIET.md file
|
|
92
|
+
try:
|
|
93
|
+
with open(diet_path, "w", encoding="utf-8") as f:
|
|
94
|
+
f.write(result.diet_markdown)
|
|
95
|
+
if not json_output:
|
|
96
|
+
console.print(f"[green]✓[/green] Generated diet analysis: {diet_path}")
|
|
97
|
+
except Exception as e:
|
|
98
|
+
error_msg = f"Failed to write diet file: {e}"
|
|
99
|
+
logger.error(error_msg)
|
|
100
|
+
console.print(f"[red]✗ {error_msg}[/red]")
|
|
101
|
+
raise typer.Exit(code=1) from e
|
|
102
|
+
|
|
103
|
+
# Show terminal summary
|
|
104
|
+
terminal = TerminalRenderer(verbose=verbose, console=console)
|
|
105
|
+
renderer = DietRenderer(terminal)
|
|
106
|
+
renderer.render(result)
|
|
107
|
+
|
|
108
|
+
# Show success message with next actions
|
|
109
|
+
next_actions = [
|
|
110
|
+
"Review identified bloat and consider cleanup",
|
|
111
|
+
"Add missing hygiene files to improve repo health",
|
|
112
|
+
"Run 'repodoc scan' for a full health check",
|
|
113
|
+
]
|
|
114
|
+
print_success_message("Diet analysis", next_actions)
|
|
115
|
+
|
|
116
|
+
except ValidationError as e:
|
|
117
|
+
logger.error(f"Failed to validate diet output: {e}")
|
|
118
|
+
console.print("[red]✗ Failed to parse Copilot output[/red]")
|
|
119
|
+
if verbose:
|
|
120
|
+
console.print_exception()
|
|
121
|
+
raise typer.Exit(code=1) from None
|
|
122
|
+
except typer.Exit:
|
|
123
|
+
# Let typer.Exit propagate without handling
|
|
124
|
+
raise
|
|
125
|
+
except Exception as e:
|
|
126
|
+
handle_command_error(e, verbose)
|