""" CLI utilities for MarkiTect command-line interface. This module provides common utilities and patterns used across CLI commands: - Output formatting (table, JSON) - Error handling decorators - Common Click options - Configuration loading helpers Used by asset management commands and can be extended for other CLI modules. """ import click import json import sys from functools import wraps from pathlib import Path from tabulate import tabulate from typing import Any, Dict, List, Optional, Callable # Import for configuration support try: from .config_manager import ConfigurationManager CONFIG_AVAILABLE = True except ImportError: CONFIG_AVAILABLE = False def format_table_output(data: List[Dict[str, Any]], headers: List[str], tablefmt: str = 'grid') -> str: """Format data as table for console output. Args: data: List of dictionaries containing row data headers: List of column headers tablefmt: Table format style (default: 'grid') Returns: Formatted table string """ if not data: return "No data to display" # Convert dict data to list of lists for tabulate table_data = [] for item in data: row = [item.get(header.lower(), item.get(header, 'N/A')) for header in headers] table_data.append(row) return tabulate(table_data, headers=headers, tablefmt=tablefmt) def format_json_output(data: Any, indent: int = 2) -> str: """Format data as JSON for programmatic consumption. Args: data: Data to format as JSON indent: JSON indentation level Returns: JSON formatted string """ return json.dumps(data, indent=indent, default=str) def handle_asset_errors(func: Callable) -> Callable: """Decorator to handle common asset management errors. Provides consistent error handling for asset-related CLI commands. """ @wraps(func) def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except ImportError as e: if "assets" in str(e).lower(): click.echo("Error: Asset management backend not available", err=True) click.echo("Ensure markitect.assets module is properly installed", err=True) else: click.echo(f"Import error: {e}", err=True) sys.exit(1) except Exception as e: # Import asset exceptions if available try: from .assets import AssetError, PackagingError if isinstance(e, (AssetError, PackagingError)): click.echo(f"Asset error: {e}", err=True) else: click.echo(f"Unexpected error: {e}", err=True) except ImportError: click.echo(f"Unexpected error: {e}", err=True) sys.exit(1) return wrapper def require_workspace(func: Callable) -> Callable: """Decorator to ensure workspace exists before running command. Checks for workspace directory and shows helpful message if not found. """ @wraps(func) def wrapper(*args, **kwargs): workspace_dir = Path.cwd() / "markitect_workspace" if not workspace_dir.exists(): click.echo("No workspace found in current directory", err=True) click.echo("Run 'markitect workspace init' to create one", err=True) sys.exit(1) return func(*args, **kwargs) return wrapper # Common Click options def output_format_option(default: str = 'table'): """Common output format option for list commands.""" return click.option( '--format', 'output_format', type=click.Choice(['table', 'json']), default=default, help=f'Output format (default: {default})' ) def dry_run_option(): """Common dry-run option for potentially destructive commands.""" return click.option( '--dry-run', is_flag=True, help='Show what would be done without making changes' ) def verbose_option(): """Common verbose option for detailed output.""" return click.option( '--verbose', '-v', is_flag=True, help='Enable verbose output' ) class ClickOutputFormatter: """ Helper class for consistent CLI output formatting across MarkiTect commands. Provides standardized methods for displaying success, info, warning, and error messages with consistent formatting including icons and structured details. Usage: ClickOutputFormatter.success("Operation completed", {"Files": 5}) ClickOutputFormatter.error("Failed to process") """ @staticmethod def success(message: str, details: Optional[Dict[str, Any]] = None): """ Display success message with checkmark and optional details. Args: message: Success message to display details: Optional dictionary of key-value details to show """ click.echo(f"✓ {message}") if details: for key, value in details.items(): click.echo(f" {key}: {value}") @staticmethod def info(message: str, details: Optional[Dict[str, Any]] = None): """ Display informational message with optional details. Args: message: Info message to display details: Optional dictionary of key-value details to show """ click.echo(message) if details: for key, value in details.items(): click.echo(f" {key}: {value}") @staticmethod def warning(message: str): """ Display warning message with warning icon. Args: message: Warning message to display """ click.echo(f"⚠ {message}", err=True) @staticmethod def error(message: str, exit_code: int = 1): """ Display error message with error icon and exit. Args: message: Error message to display exit_code: Exit code to use (default: 1) """ click.echo(f"✗ {message}", err=True) sys.exit(exit_code) @staticmethod def table(data: List[Dict[str, Any]], headers: List[str]): """Display data as formatted table.""" if not data: click.echo("No data to display") return table_output = format_table_output(data, headers) click.echo(table_output) @staticmethod def json_output(data: Any): """Display data as JSON.""" json_output = format_json_output(data) click.echo(json_output) def get_configuration() -> Optional[Dict[str, Any]]: """Get current markitect configuration. Returns: Configuration dictionary if available, None otherwise """ if not CONFIG_AVAILABLE: return None try: config_manager = ConfigurationManager() return config_manager.get_config() except Exception: return None def get_asset_config() -> Dict[str, Any]: """Get asset management configuration with defaults. Returns: Asset configuration dictionary with sensible defaults """ config = get_configuration() if config and 'asset_management' in config: asset_config = config['asset_management'] else: asset_config = {} # Apply defaults defaults = { 'enabled': True, 'workspace_path': './markitect_workspace', 'shared_assets_path': './markitect_workspace/shared_assets', 'packages_path': './markitect_workspace/packages', 'auto_dedupe': True, 'symlink_preferred': True, 'fallback_to_copy': True, 'compression_level': 6, 'include_manifest': True, 'validate_on_create': True, 'cache_enabled': True, 'batch_size': 100, 'max_file_size_mb': 50 } # Merge with defaults for key, default_value in defaults.items(): if key not in asset_config: asset_config[key] = default_value return asset_config def validate_file_path(path: str, must_exist: bool = True) -> Path: """Validate and normalize file path. Args: path: File path string must_exist: Whether file must exist Returns: Validated Path object Raises: click.ClickException: If validation fails """ file_path = Path(path).resolve() if must_exist and not file_path.exists(): raise click.ClickException(f"File not found: {file_path}") if must_exist and file_path.is_dir(): raise click.ClickException(f"Expected file, got directory: {file_path}") return file_path def validate_directory_path(path: str, must_exist: bool = True, create_if_missing: bool = False) -> Path: """Validate and normalize directory path. Args: path: Directory path string must_exist: Whether directory must exist create_if_missing: Whether to create directory if missing Returns: Validated Path object Raises: click.ClickException: If validation fails """ dir_path = Path(path).resolve() if not dir_path.exists(): if create_if_missing: dir_path.mkdir(parents=True, exist_ok=True) elif must_exist: raise click.ClickException(f"Directory not found: {dir_path}") elif dir_path.exists() and not dir_path.is_dir(): raise click.ClickException(f"Expected directory, got file: {dir_path}") return dir_path def confirm_destructive_action(message: str, default: bool = False) -> bool: """Prompt user to confirm destructive action. Args: message: Confirmation message default: Default choice if user just presses enter Returns: True if user confirms, False otherwise """ return click.confirm(message, default=default)