Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,4 @@ __marimo__/

# Test Downloas
.downloads/*
forklet/*.py.backup
13 changes: 8 additions & 5 deletions forklet/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,15 @@ async def get_repo_info():
click.echo(f"🎯 Current ref: {git_ref}")

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


@cli.command()
def status():
@click.pass_context
def status(ctx):
"""Show current download status and progress"""

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

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


Expand All @@ -196,5 +201,3 @@ def version():
#### MAIN ENTRYPOINT FOR THE FORKLET CLI
def main():
cli()


144 changes: 73 additions & 71 deletions forklet/interfaces/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,19 @@
from forklet.core import DownloadOrchestrator
from forklet.services import GitHubAPIService, DownloadService
from forklet.infrastructure import (
RateLimiter, RetryManager, DownloadError,
RateLimitError, AuthenticationError, RepositoryNotFoundError
RateLimiter,
RetryManager,
DownloadError,
RateLimitError,
AuthenticationError,
RepositoryNotFoundError,
)
from forklet.infrastructure.logger import logger
from forklet.models import (
DownloadRequest, DownloadStrategy, FilterCriteria,
DownloadResult
DownloadRequest,
DownloadStrategy,
FilterCriteria,
DownloadResult,
)


Expand All @@ -26,17 +32,15 @@
#####
class ForkletCLI:
"""Main CLI application class."""

def __init__(self):
self.rate_limiter = RateLimiter()
self.retry_manager = RetryManager()
self.github_service: Optional[GitHubAPIService] = None
self.download_service: Optional[DownloadService] = None
self.orchestrator: Optional[DownloadOrchestrator] = None

def initialize_services(
self, auth_token: Optional[str] = None
) -> None:

def initialize_services(self, auth_token: Optional[str] = None) -> None:
"""Initialize all services with optional authentication."""

self.github_service = GitHubAPIService(
Expand All @@ -46,34 +50,30 @@ def initialize_services(
self.orchestrator = DownloadOrchestrator(
self.github_service, self.download_service
)

def parse_repository_string(self, repo_str: str) -> Tuple[str, str]:
"""
Parse repository string in format owner/repo.

Args:
repo_str: Repository string

Returns:
Tuple of (owner, repo)

Raises:
click.BadParameter: If format is invalid
"""

if '/' not in repo_str:
raise click.BadParameter(
"Repository must be in format 'owner/repo'"
)

parts = repo_str.split('/')
if "/" not in repo_str:
raise click.BadParameter("Repository must be in format 'owner/repo'")

parts = repo_str.split("/")
if len(parts) != 2:
raise click.BadParameter(
"Repository must be in format 'owner/repo'"
)

raise click.BadParameter("Repository must be in format 'owner/repo'")

return parts[0], parts[1]

def create_filter_criteria(
self,
include: List[str],
Expand All @@ -84,11 +84,11 @@ def create_filter_criteria(
exclude_extensions: List[str],
include_hidden: bool,
include_binary: bool,
target_paths: List[str]
target_paths: List[str],
) -> FilterCriteria:
"""
Create filter criteria from CLI options.

Args:
include: Include patterns
exclude: Exclude patterns
Expand All @@ -99,23 +99,23 @@ def create_filter_criteria(
include_hidden: Include hidden files
include_binary: Include binary files
target_paths: Specific paths to download

Returns:
FilterCriteria object
"""

return FilterCriteria(
include_patterns = include,
exclude_patterns = exclude,
max_file_size = max_size,
min_file_size = min_size,
file_extensions = set(extensions),
excluded_extensions = set(exclude_extensions),
include_hidden = include_hidden,
include_binary = include_binary,
target_paths = target_paths
include_patterns=include,
exclude_patterns=exclude,
max_file_size=max_size,
min_file_size=min_size,
file_extensions=set(extensions),
excluded_extensions=set(exclude_extensions),
include_hidden=include_hidden,
include_binary=include_binary,
target_paths=target_paths,
)

async def execute_download(
self,
repository: str,
Expand All @@ -132,7 +132,7 @@ async def execute_download(
) -> None:
"""
Execute the download operation.

Args:
repository: Repository string (owner/repo)
destination: Destination directory
Expand All @@ -147,66 +147,66 @@ async def execute_download(
try:
# Initialize services
self.initialize_services(token)

# Parse repository
owner, repo_name = self.parse_repository_string(repository)

# Get repository info
click.echo(f"📦 Fetching repository information for {owner}/{repo_name}...")
repo_info = await self.github_service.get_repository_info(owner, repo_name)

# Resolve Git reference
click.echo(f"🔍 Resolving reference '{ref}'...")
git_ref = await self.github_service.resolve_reference(owner, repo_name, ref)

# Create download request
request = DownloadRequest(
repository = repo_info,
git_ref = git_ref,
destination = Path(destination),
strategy = strategy,
filters = filters,
token = token,
max_concurrent_downloads = concurrent,
overwrite_existing = overwrite,
show_progress_bars = progress
,dry_run = dry_run
repository=repo_info,
git_ref=git_ref,
destination=Path(destination),
strategy=strategy,
filters=filters,
token=token,
max_concurrent_downloads=concurrent,
overwrite_existing=overwrite,
show_progress_bars=progress,
dry_run=dry_run,
)

# Execute download
click.echo(
f"🚀 Starting download with {concurrent} concurrent workers..."
)
click.echo(f"🚀 Starting download with {concurrent} concurrent workers...")
result = await self.orchestrator.execute_download(request)

# Display results (pass through verbose flag)
self.display_results(result, verbose=verbose)

except (
RateLimitError, AuthenticationError,
RepositoryNotFoundError, DownloadError
RateLimitError,
AuthenticationError,
RepositoryNotFoundError,
DownloadError,
) as e:
click.echo(f" Error: {e}", err=True)
click.echo(f"💥 Error: {e}", err=True)
sys.exit(1)

except Exception as e:
click.echo(f"💥 Unexpected error: {e}", err=True)
logger.exception("Unexpected error in download operation")
logger.debug("Unexpected error in download operation", exc_info=True)
sys.exit(1)

def display_results(self, result: DownloadResult, verbose: bool = False) -> None:
"""
Display download results in a user-friendly format.

Args:
result: Download result
"""

if hasattr(result, 'is_successful') and result.is_successful:
if hasattr(result, "is_successful") and result.is_successful:
click.echo("✅ Download completed successfully!")
click.echo(f" 📁 Files: {len(result.downloaded_files)} downloaded")
click.echo(f" 💾 Size: {result.progress.downloaded_bytes} bytes")

if result.average_speed is not None:
click.echo(f" ⚡ Speed: {result.average_speed:.2f} bytes/sec")

Expand All @@ -216,7 +216,7 @@ def display_results(self, result: DownloadResult, verbose: bool = False) -> None
# When verbose, display file paths (matched / downloaded / skipped)
if verbose:
# Matched files (available in dry-run and set by orchestrator)
if hasattr(result, 'matched_files') and result.matched_files:
if hasattr(result, "matched_files") and result.matched_files:
click.echo(" 🔎 Matched files:")
for p in result.matched_files:
click.echo(f" {p}")
Expand All @@ -231,17 +231,19 @@ def display_results(self, result: DownloadResult, verbose: bool = False) -> None
click.echo(" ⏭️ Skipped paths:")
for p in result.skipped_files:
click.echo(f" {p}")
elif hasattr(result, 'failed_files') and result.failed_files:

elif hasattr(result, "failed_files") and result.failed_files:
click.echo("⚠️ Download completed with errors:")
click.echo(f" ✅ Successful: {len(result.downloaded_files)}")
click.echo(f" ❌ Failed: {len(result.failed_files)}")

# Show first few errors
for i, (filename, error) in enumerate(list(result.failed_files.items())[:3]):
for i, (filename, error) in enumerate(
list(result.failed_files.items())[:3]
):
click.echo(f" {filename}: {error}")
if len(result.failed_files) > 3:
click.echo(f" ... and {len(result.failed_files) - 3} more errors")

else:
click.echo("❌ Download failed completely")
Loading