""" CLI commands for advanced asset management - Issue #144. This module provides command-line interface for advanced asset operations including batch processing, discovery, and analytics. """ from pathlib import Path from typing import List, Optional, Dict, Any from dataclasses import dataclass from markitect.assets import AssetManager from markitect.assets.batch_processor import BatchAssetProcessor, ConflictResolution from markitect.assets.discovery import AssetDiscoveryEngine from markitect.assets.optimizer import AssetOptimizer, OptimizationProfile from markitect.assets.analytics import AssetAnalytics @dataclass class CLIResult: """Result of CLI command execution.""" success: bool message: str data: Optional[Dict[str, Any]] = None @dataclass class BatchImportCLIResult(CLIResult): """Result of batch import CLI command.""" imported_count: int = 0 skipped_count: int = 0 error_count: int = 0 @dataclass class StatisticsCLIResult(CLIResult): """Result of statistics CLI command.""" total_assets: int = 0 total_size: int = 0 optimization_potential: Optional[Dict[str, Any]] = None @dataclass class DiscoveryCLIResult(CLIResult): """Result of discovery CLI command.""" total_references: int = 0 broken_links: int = 0 discovered_assets: int = 0 @dataclass class AssetAddResult(CLIResult): """Result of asset addition.""" asset_hash: Optional[str] = None @dataclass class AssetListResult(CLIResult): """Result of asset listing.""" assets: Optional[List[Dict[str, Any]]] = None @dataclass class AssetInfoResult(CLIResult): """Result of asset info retrieval.""" asset_info: Optional[Dict[str, Any]] = None class AssetCommands: """CLI commands for asset management.""" def __init__(self, asset_manager: AssetManager): """Initialize asset commands.""" self.asset_manager = asset_manager self.batch_processor = BatchAssetProcessor(asset_manager) self.discovery_engine = AssetDiscoveryEngine(asset_manager) self.optimizer = AssetOptimizer() self.analytics = AssetAnalytics(asset_manager) def batch_import(self, source_directory: str, recursive: bool = True, patterns: Optional[List[str]] = None, auto_optimize: bool = False, progress: bool = True) -> BatchImportCLIResult: """Execute batch import command.""" try: source_path = Path(source_directory) if not source_path.exists(): return BatchImportCLIResult( success=False, message=f"Source directory does not exist: {source_directory}" ) # Set up progress reporting if requested progress_reporter = None if progress: progress_reporter = self._create_progress_reporter() # Configure batch processor self.batch_processor.progress_reporter = progress_reporter # Execute batch import result = self.batch_processor.import_directory( source_path=source_path, recursive=recursive, patterns=patterns, conflict_resolution=ConflictResolution.SKIP, auto_optimize=auto_optimize ) return BatchImportCLIResult( success=True, message=f"Batch import completed: {result.successful_imports} assets imported", imported_count=result.successful_imports, skipped_count=result.skipped_files, error_count=result.failed_imports, data={ "processing_time": result.processing_time_seconds, "total_size": result.total_size_bytes } ) except Exception as e: return BatchImportCLIResult( success=False, message=f"Batch import failed: {str(e)}" ) def get_statistics(self, include_usage: bool = False, include_optimization_potential: bool = False) -> StatisticsCLIResult: """Get asset library statistics.""" try: # Get basic statistics all_assets = self.asset_manager.registry.list_assets_as_objects() total_assets = len(all_assets) total_size = sum(asset.size_bytes for asset in all_assets) # Get usage statistics if requested usage_data = None if include_usage: usage_report = self.analytics.generate_usage_report() usage_data = { "utilization_rate": usage_report.utilization_rate, "used_assets": usage_report.used_assets, "unused_assets": usage_report.unused_assets } # Get optimization potential if requested optimization_data = None if include_optimization_potential: project_insights = self.analytics.analyze_project_assets(Path.cwd()) optimization_data = { "potential_savings_bytes": project_insights.optimization_potential_bytes, "duplicate_assets": project_insights.duplicate_assets, "recommendations": project_insights.recommendations } message = f"Total assets: {total_assets}, Total size: {total_size:,} bytes" return StatisticsCLIResult( success=True, message=message, total_assets=total_assets, total_size=total_size, optimization_potential=optimization_data, data={ "usage_statistics": usage_data, "optimization_potential": optimization_data } ) except Exception as e: return StatisticsCLIResult( success=False, message=f"Failed to get statistics: {str(e)}" ) def discover_assets(self, scan_directory: str, auto_register: bool = False, report_broken_links: bool = True) -> DiscoveryCLIResult: """Discover assets in project files.""" try: scan_path = Path(scan_directory) if not scan_path.exists(): return DiscoveryCLIResult( success=False, message=f"Scan directory does not exist: {scan_directory}" ) # Scan for asset references scan_result = self.discovery_engine.scan_directory( scan_path, recursive=True ) discovered_count = 0 # Auto-register if requested if auto_register: registration_result = self.discovery_engine.auto_register_assets( scan_path, register_existing=True, skip_broken=True ) discovered_count = registration_result.registered_count message_parts = [ f"Found {len(scan_result.asset_references)} asset references", f"Broken links: {len(scan_result.broken_links)}" ] if auto_register: message_parts.append(f"Registered: {discovered_count} assets") return DiscoveryCLIResult( success=True, message=", ".join(message_parts), total_references=len(scan_result.asset_references), broken_links=len(scan_result.broken_links), discovered_assets=discovered_count, data={ "scanned_files": len(scan_result.scanned_files), "processing_time": scan_result.processing_time, "broken_links": [ { "file": str(ref.source_file), "asset_path": ref.asset_path, "line": ref.line_number } for ref in scan_result.broken_links ] if report_broken_links else [] } ) except Exception as e: return DiscoveryCLIResult( success=False, message=f"Asset discovery failed: {str(e)}" ) def optimize_assets(self, asset_patterns: Optional[List[str]] = None, profile: str = "balanced", dry_run: bool = False) -> CLIResult: """Optimize assets in the library.""" try: # Configure optimization profile if profile == "conservative": opt_profile = OptimizationProfile.CONSERVATIVE elif profile == "aggressive": opt_profile = OptimizationProfile.AGGRESSIVE else: opt_profile = OptimizationProfile.BALANCED self.optimizer.profile = opt_profile # Get assets to optimize all_assets = self.asset_manager.registry.list_assets_as_objects() # Filter by patterns if provided assets_to_optimize = [] for asset in all_assets: if asset_patterns: # Check if asset matches any pattern if any(pattern in asset.filename for pattern in asset_patterns): assets_to_optimize.append(Path(asset.filename)) else: # Optimize images and documents if Path(asset.filename).suffix.lower() in ['.png', '.jpg', '.jpeg', '.svg', '.pdf']: assets_to_optimize.append(Path(asset.filename)) if dry_run: return CLIResult( success=True, message=f"Dry run: Would optimize {len(assets_to_optimize)} assets", data={"assets_to_optimize": [str(p) for p in assets_to_optimize]} ) # Execute optimization optimization_results = self.optimizer.optimize_batch( assets_to_optimize, max_concurrent=2 ) successful_optimizations = [r for r in optimization_results if r.success] total_savings = sum(r.original_size - r.optimized_size for r in successful_optimizations) return CLIResult( success=True, message=f"Optimized {len(successful_optimizations)} assets, saved {total_savings:,} bytes", data={ "optimized_count": len(successful_optimizations), "failed_count": len(optimization_results) - len(successful_optimizations), "total_savings_bytes": total_savings, "optimization_profile": profile } ) except Exception as e: return CLIResult( success=False, message=f"Asset optimization failed: {str(e)}" ) def cleanup_unused(self, dry_run: bool = True, min_size_bytes: int = 0) -> CLIResult: """Clean up unused assets.""" try: # Generate usage report usage_report = self.analytics.generate_usage_report(include_unused=True) unused_assets = usage_report.unused_assets_list # Filter by minimum size if min_size_bytes > 0: unused_assets = [asset for asset in unused_assets if asset["size_bytes"] >= min_size_bytes] total_size_to_free = sum(asset["size_bytes"] for asset in unused_assets) if dry_run: return CLIResult( success=True, message=f"Dry run: Would remove {len(unused_assets)} unused assets, freeing {total_size_to_free:,} bytes", data={ "unused_assets": unused_assets, "total_size_to_free": total_size_to_free } ) # Actually remove unused assets (simplified implementation) removed_count = 0 for asset in unused_assets: try: # Would remove the actual asset file here removed_count += 1 except Exception: pass return CLIResult( success=True, message=f"Removed {removed_count} unused assets, freed {total_size_to_free:,} bytes", data={ "removed_count": removed_count, "freed_bytes": total_size_to_free } ) except Exception as e: return CLIResult( success=False, message=f"Cleanup failed: {str(e)}" ) def _create_progress_reporter(self): """Create a simple progress reporter for CLI.""" class CLIProgressReporter: def __init__(self): self.total = 0 self.current = 0 def start(self, total_items): self.total = total_items self.current = 0 print(f"Processing {total_items} items...") def update(self, current, item_name=""): self.current = current if self.total > 0: progress = (current / self.total) * 100 print(f"Progress: {progress:.1f}% ({current}/{self.total}) - {item_name}") def finish(self): print("Processing complete!") return CLIProgressReporter() def add_asset(self, file_path: str) -> AssetAddResult: """Add a single asset via CLI.""" try: asset_path = Path(file_path) if not asset_path.exists(): return AssetAddResult( success=False, message=f"File does not exist: {file_path}" ) # Add asset using asset manager result = self.asset_manager.add_asset(asset_path) if result and 'content_hash' in result: return AssetAddResult( success=True, message=f"Asset added successfully: {asset_path.name}", asset_hash=result['content_hash'] ) else: return AssetAddResult( success=False, message=f"Failed to add asset: {file_path}" ) except Exception as e: return AssetAddResult( success=False, message=f"Failed to add asset: {str(e)}" ) def list_assets(self) -> AssetListResult: """List all assets via CLI.""" try: assets = self.asset_manager.registry.list_assets() return AssetListResult( success=True, message=f"Found {len(assets)} assets", assets=assets ) except Exception as e: return AssetListResult( success=False, message=f"Failed to list assets: {str(e)}", assets=[] ) def get_asset_info(self, content_hash: str) -> AssetInfoResult: """Get information about a specific asset.""" try: asset_info = self.asset_manager.registry.get_asset(content_hash) return AssetInfoResult( success=True, message=f"Asset info retrieved for {content_hash[:8]}...", asset_info=asset_info ) except Exception as e: return AssetInfoResult( success=False, message=f"Failed to get asset info: {str(e)}" )