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 ADDED
@@ -0,0 +1,3 @@
1
+ """RepoDoctor: A Copilot-first CLI tool for repository health analysis."""
2
+
3
+ __version__ = "0.1.0"
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"]
@@ -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)
@@ -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)