Pattern-027: CLI Integration Pattern

Status

Proven

Context

Command-line interfaces often become disconnected from underlying service architectures, leading to duplicated business logic, inconsistent behavior, and poor user experience. Without proper integration patterns, CLI tools either bypass service layers (creating maintenance nightmares) or provide poor formatting and user interaction. The CLI Integration Pattern addresses:

Pattern Description

The CLI Integration Pattern integrates command-line interfaces with underlying service architectures while providing beautiful output formatting, learning system integration, and consistent user experience. The pattern maintains clear separation between CLI presentation logic and business logic while enabling rich, interactive command-line experiences.

Implementation

Structure

# CLI integration framework
class CLIIntegrationFramework:
    def __init__(self):
        self.service_locator = ServiceLocator()
        self.formatter = OutputFormatter()
        self.learning_system = LearningSystemIntegrator()
        self.error_handler = CLIErrorHandler()

    def create_command(self, command_name: str, service_dependencies: List[str]) -> CLICommand:
        """Create integrated CLI command with service dependencies"""
        pass

    def register_formatter(self, output_type: str, formatter: Callable):
        """Register custom formatter for specific output types"""
        pass

    async def execute_command(self, command: CLICommand, args: Dict[str, Any]) -> CommandResult:
        """Execute command with full service integration"""
        pass

Example (Beautiful CLI Command with Service Integration)

import asyncio
from typing import Optional, List, Dict, Any
from dataclasses import dataclass
from abc import ABC, abstractmethod
import structlog

logger = structlog.get_logger()

class CLICommand(ABC):
    """Base class for CLI commands with service integration"""

    COLORS = {
        "reset": "\033[0m",
        "red": "\033[91m",
        "green": "\033[92m",
        "yellow": "\033[93m",
        "blue": "\033[94m",
        "magenta": "\033[95m",
        "cyan": "\033[96m",
        "white": "\033[97m",
        "gray": "\033[90m",
        "bold": "\033[1m",
        "dim": "\033[2m"
    }

    def __init__(self, service_locator: ServiceLocator):
        self.service_locator = service_locator
        self.services_initialized = False

    async def _initialize_services(self):
        """Initialize required services lazily"""
        if not self.services_initialized:
            await self._setup_services()
            self.services_initialized = True

    @abstractmethod
    async def _setup_services(self):
        """Setup service dependencies - implemented by subclasses"""
        pass

    @abstractmethod
    async def execute(self, **kwargs) -> CommandResult:
        """Execute the command - implemented by subclasses"""
        pass

    def print_header(self, text: str, color: str = "cyan") -> None:
        """Print formatted section header with consistent styling"""
        separator = "=" * max(60, len(text) + 4)
        color_code = self.COLORS.get(color, self.COLORS["cyan"])
        bold = self.COLORS["bold"]
        reset = self.COLORS["reset"]

        print(f"\n{bold}{color_code}{separator}{reset}")
        print(f"{bold}{color_code}  {text}{reset}")
        print(f"{bold}{color_code}{separator}{reset}\n")

    def print_subheader(self, text: str, color: str = "blue") -> None:
        """Print formatted subsection header"""
        color_code = self.COLORS.get(color, self.COLORS["blue"])
        bold = self.COLORS["bold"]
        reset = self.COLORS["reset"]

        print(f"{bold}{color_code}{text}{reset}")

    def print_success(self, message: str) -> None:
        """Print success message with consistent formatting"""
        green = self.COLORS["green"]
        bold = self.COLORS["bold"]
        reset = self.COLORS["reset"]
        print(f"{bold}{green}{message}{reset}")

    def print_warning(self, message: str) -> None:
        """Print warning message with consistent formatting"""
        yellow = self.COLORS["yellow"]
        bold = self.COLORS["bold"]
        reset = self.COLORS["reset"]
        print(f"{bold}{yellow}⚠️  {message}{reset}")

    def print_error(self, message: str) -> None:
        """Print error message with consistent formatting"""
        red = self.COLORS["red"]
        bold = self.COLORS["bold"]
        reset = self.COLORS["reset"]
        print(f"{bold}{red}{message}{reset}")

    def print_info(self, message: str, indent: int = 0) -> None:
        """Print informational message with optional indentation"""
        blue = self.COLORS["blue"]
        reset = self.COLORS["reset"]
        indent_str = "  " * indent
        print(f"{indent_str}{blue}ℹ️  {message}{reset}")

class IssuesCommand(CLICommand):
    """CLI command for GitHub issues with beautiful formatting and learning integration"""

    def __init__(self, service_locator: ServiceLocator):
        super().__init__(service_locator)
        self.github_agent = None
        self.learning_system = None
        self.issue_classifier = None

    async def _setup_services(self):
        """Setup GitHub and learning services"""
        self.github_agent = await self.service_locator.get_service('github_agent')
        self.learning_system = await self.service_locator.get_service('learning_system')
        self.issue_classifier = await self.service_locator.get_service('issue_classifier')

        logger.info("Issues command services initialized")

    async def triage_issues(self, project: Optional[str] = None, limit: int = 10) -> CommandResult:
        """AI-powered issue triage with learning integration and beautiful output"""
        await self._initialize_services()

        try:
            self.print_header(f"🔍 AI-Powered Issue Triage", "cyan")

            if project:
                self.print_info(f"Analyzing issues for project: {project}")
            else:
                self.print_info("Analyzing issues across all accessible repositories")

            # Get and analyze issues through service layer
            self.print_subheader("Fetching Issues", "blue")
            issues = await self.github_agent.get_open_issues(project=project, limit=limit)

            if not issues:
                self.print_warning("No open issues found")
                return CommandResult(success=True, message="No issues to triage")

            self.print_success(f"Retrieved {len(issues)} open issues")

            # Classify and prioritize issues
            self.print_subheader("Analyzing Priority and Classification", "blue")

            high_priority = []
            medium_priority = []
            low_priority = []

            for issue in issues:
                # Use service layer for classification
                classification = await self.issue_classifier.classify_issue(issue)
                priority = self._determine_priority(issue, classification)

                # Learn from triage decisions for future improvement
                await self._learn_triage_pattern(issue, priority, classification)

                # Categorize by priority
                issue_data = {
                    'issue': issue,
                    'classification': classification,
                    'priority': priority
                }

                if priority >= 0.8:
                    high_priority.append(issue_data)
                elif priority >= 0.5:
                    medium_priority.append(issue_data)
                else:
                    low_priority.append(issue_data)

            # Display results with beautiful formatting
            self._display_triage_results(high_priority, medium_priority, low_priority)

            return CommandResult(
                success=True,
                message=f"Triaged {len(issues)} issues",
                data={
                    'high_priority': len(high_priority),
                    'medium_priority': len(medium_priority),
                    'low_priority': len(low_priority)
                }
            )

        except Exception as e:
            error_message = f"Failed to triage issues: {str(e)}"
            self.print_error(error_message)
            logger.error("Issue triage failed", error=str(e), project=project)

            return CommandResult(
                success=False,
                message=error_message,
                error=str(e)
            )

    def _determine_priority(self, issue: Dict[str, Any], classification: Dict[str, Any]) -> float:
        """Determine issue priority based on multiple factors"""
        priority_factors = []

        # Label-based priority
        labels = [label['name'].lower() for label in issue.get('labels', [])]
        if any(label in ['critical', 'urgent', 'blocker'] for label in labels):
            priority_factors.append(0.9)
        elif any(label in ['high', 'important'] for label in labels):
            priority_factors.append(0.7)
        elif any(label in ['medium'] for label in labels):
            priority_factors.append(0.5)
        else:
            priority_factors.append(0.3)

        # Classification-based priority
        if classification.get('category') == 'bug':
            priority_factors.append(0.8)
        elif classification.get('category') == 'feature':
            priority_factors.append(0.5)
        elif classification.get('category') == 'enhancement':
            priority_factors.append(0.4)
        else:
            priority_factors.append(0.3)

        # Age-based priority (older issues get slightly higher priority)
        from datetime import datetime, timezone
        created_at = datetime.fromisoformat(issue['created_at'].replace('Z', '+00:00'))
        age_days = (datetime.now(timezone.utc) - created_at).days
        age_factor = min(age_days / 30.0 * 0.2, 0.2)  # Max 0.2 boost for age
        priority_factors.append(age_factor)

        # Comment activity factor
        comment_count = issue.get('comments', 0)
        activity_factor = min(comment_count / 10.0 * 0.1, 0.1)  # Max 0.1 boost for activity
        priority_factors.append(activity_factor)

        return min(sum(priority_factors) / len(priority_factors), 1.0)

    async def _learn_triage_pattern(self, issue: Dict[str, Any], priority: float, classification: Dict[str, Any]):
        """Learn from triage decisions to improve future classifications"""
        try:
            learning_data = {
                'issue_title': issue['title'],
                'issue_body': issue.get('body', ''),
                'labels': [label['name'] for label in issue.get('labels', [])],
                'priority': priority,
                'classification': classification,
                'timestamp': datetime.now().isoformat()
            }

            await self.learning_system.record_triage_decision(learning_data)

        except Exception as e:
            logger.warning("Failed to record triage learning data", error=str(e))

    def _display_triage_results(self, high_priority: List[Dict], medium_priority: List[Dict], low_priority: List[Dict]):
        """Display triage results with beautiful formatting"""

        # High Priority Issues
        if high_priority:
            self.print_header("🚨 HIGH PRIORITY ISSUES", "red")
            for item in high_priority:
                issue = item['issue']
                classification = item['classification']
                self._display_issue_summary(issue, classification, "red")

        # Medium Priority Issues
        if medium_priority:
            self.print_header("⚠️  MEDIUM PRIORITY ISSUES", "yellow")
            for item in medium_priority:
                issue = item['issue']
                classification = item['classification']
                self._display_issue_summary(issue, classification, "yellow")

        # Low Priority Issues
        if low_priority:
            self.print_header("📝 LOW PRIORITY ISSUES", "green")
            for item in low_priority:
                issue = item['issue']
                classification = item['classification']
                self._display_issue_summary(issue, classification, "green")

        # Summary
        total = len(high_priority) + len(medium_priority) + len(low_priority)
        self.print_header("📊 TRIAGE SUMMARY", "cyan")
        self.print_info(f"High Priority: {len(high_priority)} issues", 1)
        self.print_info(f"Medium Priority: {len(medium_priority)} issues", 1)
        self.print_info(f"Low Priority: {len(low_priority)} issues", 1)
        self.print_info(f"Total Analyzed: {total} issues", 1)

    def _display_issue_summary(self, issue: Dict[str, Any], classification: Dict[str, Any], color: str):
        """Display individual issue summary with consistent formatting"""
        color_code = self.COLORS.get(color, self.COLORS["blue"])
        bold = self.COLORS["bold"]
        reset = self.COLORS["reset"]
        gray = self.COLORS["gray"]

        # Issue title and number
        print(f"{bold}{color_code}#{issue['number']}: {issue['title']}{reset}")

        # Classification and labels
        category = classification.get('category', 'unknown')
        confidence = classification.get('confidence', 0.0)
        print(f"  {gray}Category: {category} (confidence: {confidence:.2f}){reset}")

        if issue.get('labels'):
            labels = ', '.join([label['name'] for label in issue['labels']])
            print(f"  {gray}Labels: {labels}{reset}")

        # Issue URL for reference
        print(f"  {gray}URL: {issue['html_url']}{reset}")
        print()

@dataclass
class CommandResult:
    """Result of CLI command execution"""
    success: bool
    message: str
    data: Optional[Dict[str, Any]] = None
    error: Optional[str] = None

class ServiceLocator:
    """Service locator for CLI command dependencies"""

    def __init__(self):
        self._services: Dict[str, Any] = {}
        self._initializers: Dict[str, Callable] = {}

    def register_service(self, name: str, initializer: Callable):
        """Register service initializer"""
        self._initializers[name] = initializer

    async def get_service(self, name: str) -> Any:
        """Get service instance, initializing if needed"""
        if name not in self._services:
            if name not in self._initializers:
                raise ValueError(f"Service '{name}' not registered")

            self._services[name] = await self._initializers[name]()

        return self._services[name]

Example (CLI Error Handling and User Guidance)

class CLIErrorHandler:
    """Centralized error handling for CLI commands"""

    def __init__(self):
        self.colors = CLICommand.COLORS

    def handle_service_error(self, error: Exception, service_name: str) -> str:
        """Handle service-layer errors with user-friendly messages"""

        error_mappings = {
            'GitHubAuthError': f"GitHub authentication failed. Please check your GITHUB_TOKEN environment variable.",
            'GitHubRateLimitError': f"GitHub API rate limit exceeded. Please wait before retrying.",
            'NetworkError': f"Network connection failed. Please check your internet connection.",
            'ConfigurationError': f"Configuration error in {service_name}. Please check your settings."
        }

        error_type = type(error).__name__
        user_message = error_mappings.get(error_type, f"An error occurred in {service_name}: {str(error)}")

        return user_message

    def print_actionable_error(self, error: Exception, service_name: str, suggested_actions: List[str]):
        """Print error with actionable guidance"""
        red = self.colors["red"]
        yellow = self.colors["yellow"]
        bold = self.colors["bold"]
        reset = self.colors["reset"]

        # Error message
        user_message = self.handle_service_error(error, service_name)
        print(f"{bold}{red}{user_message}{reset}")

        # Suggested actions
        if suggested_actions:
            print(f"\n{bold}{yellow}💡 Suggested Actions:{reset}")
            for i, action in enumerate(suggested_actions, 1):
                print(f"  {yellow}{i}.{reset} {action}")

        print()  # Add spacing

class CLIIntegrationManager:
    """Main manager for CLI integration with service architecture"""

    def __init__(self):
        self.service_locator = ServiceLocator()
        self.error_handler = CLIErrorHandler()
        self.commands: Dict[str, CLICommand] = {}
        self._setup_services()

    def _setup_services(self):
        """Setup service initializers"""
        # Register service initializers
        self.service_locator.register_service('github_agent', self._init_github_agent)
        self.service_locator.register_service('learning_system', self._init_learning_system)
        self.service_locator.register_service('issue_classifier', self._init_issue_classifier)

    async def _init_github_agent(self):
        """Initialize GitHub agent service"""
        from services.integrations.github.github_agent import GitHubAgent
        return GitHubAgent()

    async def _init_learning_system(self):
        """Initialize learning system service"""
        from services.learning.learning_system import LearningSystem
        return LearningSystem()

    async def _init_issue_classifier(self):
        """Initialize issue classifier service"""
        from services.classification.issue_classifier import IssueClassifier
        return IssueClassifier()

    def register_command(self, name: str, command_class: type):
        """Register CLI command"""
        self.commands[name] = command_class(self.service_locator)

    async def execute_command(self, command_name: str, **kwargs) -> CommandResult:
        """Execute registered command with error handling"""
        if command_name not in self.commands:
            return CommandResult(
                success=False,
                message=f"Unknown command: {command_name}",
                error=f"Command '{command_name}' not found"
            )

        try:
            command = self.commands[command_name]
            return await command.execute(**kwargs)

        except Exception as e:
            # Handle error with user-friendly messages and guidance
            suggested_actions = self._get_suggested_actions(e, command_name)
            self.error_handler.print_actionable_error(e, command_name, suggested_actions)

            return CommandResult(
                success=False,
                message=f"Command '{command_name}' failed",
                error=str(e)
            )

    def _get_suggested_actions(self, error: Exception, command_name: str) -> List[str]:
        """Get suggested actions based on error type and command"""
        error_type = type(error).__name__

        common_actions = {
            'GitHubAuthError': [
                "Set your GITHUB_TOKEN environment variable",
                "Verify your GitHub token has the required permissions",
                "Check if your token has expired"
            ],
            'NetworkError': [
                "Check your internet connection",
                "Verify GitHub.com is accessible",
                "Try again in a few moments"
            ],
            'ConfigurationError': [
                "Review your configuration settings",
                "Check environment variables are set correctly",
                "Consult the documentation for setup instructions"
            ]
        }

        return common_actions.get(error_type, [
            f"Check the logs for more detailed error information",
            f"Verify the '{command_name}' command is properly configured",
            "Contact support if the issue persists"
        ])

# Usage example
async def main():
    """Example CLI application with service integration"""
    cli_manager = CLIIntegrationManager()

    # Register commands
    cli_manager.register_command('issues', IssuesCommand)

    # Execute command
    result = await cli_manager.execute_command('issues', project='piper-morgan', limit=15)

    if result.success:
        print(f"Command completed successfully: {result.message}")
    else:
        print(f"Command failed: {result.message}")

if __name__ == "__main__":
    asyncio.run(main())

Usage Guidelines

CLI Design Best Practices

Service Integration Best Practices

Learning System Integration Best Practices

Output and Interaction Best Practices

Anti-Patterns to Avoid

Benefits

Trade-offs

Migration Notes (for consolidation from legacy systems)

Quality Assurance Checklist

Agent Coordination Notes

References

Last updated: September 15, 2025