Skip to content

Commit 497ac6d

Browse files
committed
fix: Improve error handling in CLI to show clean error messages instead of tracebacks
- Replace raw exception tracebacks with clean, user-friendly error messages - Add verbose mode logging for debugging when needed - Standardize error message formatting with 💥 prefix - Apply improvements to both download and info commands - Add context parameter to status command for verbose logging access - Improve import organization for better readability Changes: - In download command: Show '💥 Error: {message}' instead of full traceback - In info command: Show '❌ Error: {message}' with verbose debug logging - In status command: Show '❌ Error: {message}' with verbose debug logging - When verbose flag is enabled, log full traceback for debugging - Maintain proper exit codes for error conditions
1 parent 6843cd0 commit 497ac6d

3 files changed

Lines changed: 82 additions & 76 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,4 @@ __marimo__/
214214

215215
# Test Downloas
216216
.downloads/*
217+
forklet/*.py.backup

forklet/__main__.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,15 @@ async def get_repo_info():
152152
click.echo(f"🎯 Current ref: {git_ref}")
153153

154154
except Exception as e:
155-
click.echo(f"❌ Error: {e}", err=True)
155+
click.echo(f"💥 Error: {e}", err=True)
156+
if ctx.obj.get("verbose", False):
157+
logger.debug("Error in info command", exc_info=True)
156158
sys.exit(1)
157159

158160

159161
@cli.command()
160-
def status():
162+
@click.pass_context
163+
def status(ctx):
161164
"""Show current download status and progress"""
162165

163166
try:
@@ -182,7 +185,9 @@ def status():
182185
click.echo(f" ⏱️ ETA: {progress.eta_seconds:.0f} seconds")
183186

184187
except Exception as e:
185-
click.echo(f"❌ Error: {e}", err=True)
188+
click.echo(f"💥 Error: {e}", err=True)
189+
if ctx.obj.get("verbose", False):
190+
logger.debug("Error in status command", exc_info=True)
186191
sys.exit(1)
187192

188193

@@ -196,5 +201,3 @@ def version():
196201
#### MAIN ENTRYPOINT FOR THE FORKLET CLI
197202
def main():
198203
cli()
199-
200-

forklet/interfaces/cli.py

Lines changed: 73 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,19 @@
1111
from forklet.core import DownloadOrchestrator
1212
from forklet.services import GitHubAPIService, DownloadService
1313
from forklet.infrastructure import (
14-
RateLimiter, RetryManager, DownloadError,
15-
RateLimitError, AuthenticationError, RepositoryNotFoundError
14+
RateLimiter,
15+
RetryManager,
16+
DownloadError,
17+
RateLimitError,
18+
AuthenticationError,
19+
RepositoryNotFoundError,
1620
)
1721
from forklet.infrastructure.logger import logger
1822
from forklet.models import (
19-
DownloadRequest, DownloadStrategy, FilterCriteria,
20-
DownloadResult
23+
DownloadRequest,
24+
DownloadStrategy,
25+
FilterCriteria,
26+
DownloadResult,
2127
)
2228

2329

@@ -26,17 +32,15 @@
2632
#####
2733
class ForkletCLI:
2834
"""Main CLI application class."""
29-
35+
3036
def __init__(self):
3137
self.rate_limiter = RateLimiter()
3238
self.retry_manager = RetryManager()
3339
self.github_service: Optional[GitHubAPIService] = None
3440
self.download_service: Optional[DownloadService] = None
3541
self.orchestrator: Optional[DownloadOrchestrator] = None
36-
37-
def initialize_services(
38-
self, auth_token: Optional[str] = None
39-
) -> None:
42+
43+
def initialize_services(self, auth_token: Optional[str] = None) -> None:
4044
"""Initialize all services with optional authentication."""
4145

4246
self.github_service = GitHubAPIService(
@@ -46,34 +50,30 @@ def initialize_services(
4650
self.orchestrator = DownloadOrchestrator(
4751
self.github_service, self.download_service
4852
)
49-
53+
5054
def parse_repository_string(self, repo_str: str) -> Tuple[str, str]:
5155
"""
5256
Parse repository string in format owner/repo.
53-
57+
5458
Args:
5559
repo_str: Repository string
56-
60+
5761
Returns:
5862
Tuple of (owner, repo)
59-
63+
6064
Raises:
6165
click.BadParameter: If format is invalid
6266
"""
6367

64-
if '/' not in repo_str:
65-
raise click.BadParameter(
66-
"Repository must be in format 'owner/repo'"
67-
)
68-
69-
parts = repo_str.split('/')
68+
if "/" not in repo_str:
69+
raise click.BadParameter("Repository must be in format 'owner/repo'")
70+
71+
parts = repo_str.split("/")
7072
if len(parts) != 2:
71-
raise click.BadParameter(
72-
"Repository must be in format 'owner/repo'"
73-
)
74-
73+
raise click.BadParameter("Repository must be in format 'owner/repo'")
74+
7575
return parts[0], parts[1]
76-
76+
7777
def create_filter_criteria(
7878
self,
7979
include: List[str],
@@ -84,11 +84,11 @@ def create_filter_criteria(
8484
exclude_extensions: List[str],
8585
include_hidden: bool,
8686
include_binary: bool,
87-
target_paths: List[str]
87+
target_paths: List[str],
8888
) -> FilterCriteria:
8989
"""
9090
Create filter criteria from CLI options.
91-
91+
9292
Args:
9393
include: Include patterns
9494
exclude: Exclude patterns
@@ -99,23 +99,23 @@ def create_filter_criteria(
9999
include_hidden: Include hidden files
100100
include_binary: Include binary files
101101
target_paths: Specific paths to download
102-
102+
103103
Returns:
104104
FilterCriteria object
105105
"""
106106

107107
return FilterCriteria(
108-
include_patterns = include,
109-
exclude_patterns = exclude,
110-
max_file_size = max_size,
111-
min_file_size = min_size,
112-
file_extensions = set(extensions),
113-
excluded_extensions = set(exclude_extensions),
114-
include_hidden = include_hidden,
115-
include_binary = include_binary,
116-
target_paths = target_paths
108+
include_patterns=include,
109+
exclude_patterns=exclude,
110+
max_file_size=max_size,
111+
min_file_size=min_size,
112+
file_extensions=set(extensions),
113+
excluded_extensions=set(exclude_extensions),
114+
include_hidden=include_hidden,
115+
include_binary=include_binary,
116+
target_paths=target_paths,
117117
)
118-
118+
119119
async def execute_download(
120120
self,
121121
repository: str,
@@ -132,7 +132,7 @@ async def execute_download(
132132
) -> None:
133133
"""
134134
Execute the download operation.
135-
135+
136136
Args:
137137
repository: Repository string (owner/repo)
138138
destination: Destination directory
@@ -147,66 +147,66 @@ async def execute_download(
147147
try:
148148
# Initialize services
149149
self.initialize_services(token)
150-
150+
151151
# Parse repository
152152
owner, repo_name = self.parse_repository_string(repository)
153-
153+
154154
# Get repository info
155155
click.echo(f"📦 Fetching repository information for {owner}/{repo_name}...")
156156
repo_info = await self.github_service.get_repository_info(owner, repo_name)
157-
157+
158158
# Resolve Git reference
159159
click.echo(f"🔍 Resolving reference '{ref}'...")
160160
git_ref = await self.github_service.resolve_reference(owner, repo_name, ref)
161-
161+
162162
# Create download request
163163
request = DownloadRequest(
164-
repository = repo_info,
165-
git_ref = git_ref,
166-
destination = Path(destination),
167-
strategy = strategy,
168-
filters = filters,
169-
token = token,
170-
max_concurrent_downloads = concurrent,
171-
overwrite_existing = overwrite,
172-
show_progress_bars = progress
173-
,dry_run = dry_run
164+
repository=repo_info,
165+
git_ref=git_ref,
166+
destination=Path(destination),
167+
strategy=strategy,
168+
filters=filters,
169+
token=token,
170+
max_concurrent_downloads=concurrent,
171+
overwrite_existing=overwrite,
172+
show_progress_bars=progress,
173+
dry_run=dry_run,
174174
)
175-
175+
176176
# Execute download
177-
click.echo(
178-
f"🚀 Starting download with {concurrent} concurrent workers..."
179-
)
177+
click.echo(f"🚀 Starting download with {concurrent} concurrent workers...")
180178
result = await self.orchestrator.execute_download(request)
181179

182180
# Display results (pass through verbose flag)
183181
self.display_results(result, verbose=verbose)
184-
182+
185183
except (
186-
RateLimitError, AuthenticationError,
187-
RepositoryNotFoundError, DownloadError
184+
RateLimitError,
185+
AuthenticationError,
186+
RepositoryNotFoundError,
187+
DownloadError,
188188
) as e:
189-
click.echo(f" Error: {e}", err=True)
189+
click.echo(f"💥 Error: {e}", err=True)
190190
sys.exit(1)
191191

192192
except Exception as e:
193193
click.echo(f"💥 Unexpected error: {e}", err=True)
194-
logger.exception("Unexpected error in download operation")
194+
logger.debug("Unexpected error in download operation", exc_info=True)
195195
sys.exit(1)
196-
196+
197197
def display_results(self, result: DownloadResult, verbose: bool = False) -> None:
198198
"""
199199
Display download results in a user-friendly format.
200-
200+
201201
Args:
202202
result: Download result
203203
"""
204204

205-
if hasattr(result, 'is_successful') and result.is_successful:
205+
if hasattr(result, "is_successful") and result.is_successful:
206206
click.echo("✅ Download completed successfully!")
207207
click.echo(f" 📁 Files: {len(result.downloaded_files)} downloaded")
208208
click.echo(f" 💾 Size: {result.progress.downloaded_bytes} bytes")
209-
209+
210210
if result.average_speed is not None:
211211
click.echo(f" ⚡ Speed: {result.average_speed:.2f} bytes/sec")
212212

@@ -216,7 +216,7 @@ def display_results(self, result: DownloadResult, verbose: bool = False) -> None
216216
# When verbose, display file paths (matched / downloaded / skipped)
217217
if verbose:
218218
# Matched files (available in dry-run and set by orchestrator)
219-
if hasattr(result, 'matched_files') and result.matched_files:
219+
if hasattr(result, "matched_files") and result.matched_files:
220220
click.echo(" 🔎 Matched files:")
221221
for p in result.matched_files:
222222
click.echo(f" {p}")
@@ -231,17 +231,19 @@ def display_results(self, result: DownloadResult, verbose: bool = False) -> None
231231
click.echo(" ⏭️ Skipped paths:")
232232
for p in result.skipped_files:
233233
click.echo(f" {p}")
234-
235-
elif hasattr(result, 'failed_files') and result.failed_files:
234+
235+
elif hasattr(result, "failed_files") and result.failed_files:
236236
click.echo("⚠️ Download completed with errors:")
237237
click.echo(f" ✅ Successful: {len(result.downloaded_files)}")
238238
click.echo(f" ❌ Failed: {len(result.failed_files)}")
239-
239+
240240
# Show first few errors
241-
for i, (filename, error) in enumerate(list(result.failed_files.items())[:3]):
241+
for i, (filename, error) in enumerate(
242+
list(result.failed_files.items())[:3]
243+
):
242244
click.echo(f" {filename}: {error}")
243245
if len(result.failed_files) > 3:
244246
click.echo(f" ... and {len(result.failed_files) - 3} more errors")
245-
247+
246248
else:
247249
click.echo("❌ Download failed completely")

0 commit comments

Comments
 (0)