refactor: Separate CLI presentation from core business logic
Complete architectural separation of concerns implementing clean layered design: • Services Layer: Pure business logic isolated from presentation - WorkspaceService: TDD workspace operations - IssueService: Issue management and creation - ProjectService: Project management and milestones - ExportService: Unix-friendly data export • CLI Layer: Clean presentation with command/presenter separation - Commands delegate to services for all business operations - Presenters handle formatted output and error messaging - Framework provides unified interface • Benefits: - Eliminates mixed concerns in 943-line CLI monolith - Enables easier testing and maintenance - Preserves all existing functionality and Unix pipeline compatibility - Provides foundation for future CLI development Resolves issue #20: CLI separation from core logic 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
16
cli/presenters/__init__.py
Normal file
16
cli/presenters/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
Presenters for CLI output formatting.
|
||||
|
||||
Presenters handle all output formatting and user feedback without
|
||||
containing business logic.
|
||||
"""
|
||||
|
||||
from .formatters import OutputFormatter
|
||||
from .views import WorkspaceView, IssueView, ProjectView
|
||||
|
||||
__all__ = [
|
||||
'OutputFormatter',
|
||||
'WorkspaceView',
|
||||
'IssueView',
|
||||
'ProjectView'
|
||||
]
|
||||
85
cli/presenters/formatters.py
Normal file
85
cli/presenters/formatters.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
Output formatting utilities for CLI presentation.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
class OutputFormatter:
|
||||
"""Handles output formatting and display."""
|
||||
|
||||
@staticmethod
|
||||
def success(message: str) -> None:
|
||||
"""Display success message."""
|
||||
print(f"✅ {message}")
|
||||
|
||||
@staticmethod
|
||||
def info(message: str) -> None:
|
||||
"""Display info message."""
|
||||
print(f"📋 {message}")
|
||||
|
||||
@staticmethod
|
||||
def warning(message: str) -> None:
|
||||
"""Display warning message."""
|
||||
print(f"⚠️ {message}")
|
||||
|
||||
@staticmethod
|
||||
def error(message: str) -> None:
|
||||
"""Display error message."""
|
||||
print(f"❌ {message}")
|
||||
|
||||
@staticmethod
|
||||
def header(title: str, separator: str = "=") -> None:
|
||||
"""Display section header."""
|
||||
print(title)
|
||||
print(separator * len(title))
|
||||
print()
|
||||
|
||||
@staticmethod
|
||||
def section(title: str) -> None:
|
||||
"""Display section title."""
|
||||
print(f"## {title}")
|
||||
print()
|
||||
|
||||
@staticmethod
|
||||
def bullet_point(text: str, indent: int = 0) -> None:
|
||||
"""Display bullet point."""
|
||||
prefix = " " * indent
|
||||
print(f"{prefix}- {text}")
|
||||
|
||||
@staticmethod
|
||||
def key_value(key: str, value: Any, indent: int = 0) -> None:
|
||||
"""Display key-value pair."""
|
||||
prefix = " " * indent
|
||||
print(f"{prefix}{key}: {value}")
|
||||
|
||||
@staticmethod
|
||||
def empty_line() -> None:
|
||||
"""Display empty line."""
|
||||
print()
|
||||
|
||||
@staticmethod
|
||||
def exit_with_error(message: str, exit_code: int = 1) -> None:
|
||||
"""Display error and exit."""
|
||||
OutputFormatter.error(message)
|
||||
sys.exit(exit_code)
|
||||
|
||||
@staticmethod
|
||||
def format_file_list(files: List[str], title: str = "Files") -> None:
|
||||
"""Format and display file list."""
|
||||
print(f"📄 {title} ({len(files)}):")
|
||||
if files:
|
||||
for file in files:
|
||||
print(f" - {file}")
|
||||
else:
|
||||
print(" - No files found")
|
||||
print()
|
||||
|
||||
@staticmethod
|
||||
def format_command_list(commands: List[str], title: str = "Commands") -> None:
|
||||
"""Format and display command list."""
|
||||
print(f"💡 {title}:")
|
||||
for command in commands:
|
||||
print(f" - {command}")
|
||||
print()
|
||||
211
cli/presenters/views.py
Normal file
211
cli/presenters/views.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
View models for displaying complex data structures.
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
from .formatters import OutputFormatter
|
||||
|
||||
|
||||
class WorkspaceView:
|
||||
"""View for workspace information display."""
|
||||
|
||||
@staticmethod
|
||||
def show_status(summary: Dict[str, Any]) -> None:
|
||||
"""Display workspace status."""
|
||||
if summary['clean']:
|
||||
OutputFormatter.info("No active issue workspace")
|
||||
print(" Use 'make tdd-start NUM=X' to begin working on an issue")
|
||||
return
|
||||
|
||||
if summary['dirty']:
|
||||
OutputFormatter.warning("Workspace directory exists but no current issue file")
|
||||
print(" Run 'make tdd-finish' to clean up or 'make tdd-start' to create new workspace")
|
||||
return
|
||||
|
||||
if summary['active']:
|
||||
WorkspaceView._show_active_workspace(summary)
|
||||
else:
|
||||
OutputFormatter.error("Failed to load workspace")
|
||||
|
||||
@staticmethod
|
||||
def _show_active_workspace(summary: Dict[str, Any]) -> None:
|
||||
"""Display active workspace details."""
|
||||
OutputFormatter.header("Active Issue Workspace", "=")
|
||||
|
||||
issue_num = summary['issue_number']
|
||||
issue_title = summary['issue_title']
|
||||
print(f"🎯 Issue #{issue_num}: {issue_title}")
|
||||
OutputFormatter.key_value("Status", summary['issue_state'])
|
||||
OutputFormatter.key_value("Workspace", f"{summary['workspace_dir']}/issue_{issue_num}/")
|
||||
OutputFormatter.empty_line()
|
||||
|
||||
# Test files
|
||||
test_count = summary['test_count']
|
||||
test_files = summary.get('test_files', [])
|
||||
print(f"🧪 Generated Tests ({test_count}):")
|
||||
if test_files:
|
||||
for test_file in test_files:
|
||||
print(f" - {test_file}")
|
||||
else:
|
||||
print(" - No tests generated yet")
|
||||
OutputFormatter.empty_line()
|
||||
|
||||
# Workspace files
|
||||
print("📋 Workspace Files:")
|
||||
print(" - requirements.md (review and break down issue)")
|
||||
print(" - test_plan.md (plan test scenarios)")
|
||||
print(" - tests/ (generated test files)")
|
||||
OutputFormatter.empty_line()
|
||||
|
||||
# Commands
|
||||
commands = [
|
||||
"make tdd-add-test (generate another test)",
|
||||
"make tdd-finish (complete and move tests to main)"
|
||||
]
|
||||
OutputFormatter.format_command_list(commands)
|
||||
|
||||
@staticmethod
|
||||
def show_start_success(summary: Dict[str, Any]) -> None:
|
||||
"""Display successful workspace start."""
|
||||
issue_num = summary['issue_number']
|
||||
OutputFormatter.success(f"Workspace created for issue #{issue_num}")
|
||||
OutputFormatter.key_value("Workspace", f"{summary['workspace_dir']}/issue_{issue_num}/")
|
||||
OutputFormatter.key_value("Requirements", summary['requirements_file'])
|
||||
OutputFormatter.key_value("Test plan", summary['test_plan_file'])
|
||||
OutputFormatter.empty_line()
|
||||
|
||||
print("💡 Next steps:")
|
||||
print(" 1. Review requirements.md and break down the issue")
|
||||
print(" 2. Plan test scenarios in test_plan.md")
|
||||
print(" 3. Use 'make tdd-add-test' to generate tests")
|
||||
print(" 4. Use 'make tdd-finish' when complete")
|
||||
|
||||
@staticmethod
|
||||
def show_finish_success(issue_number: int, test_count: int) -> None:
|
||||
"""Display successful workspace finish."""
|
||||
OutputFormatter.info(f"Finishing work on issue #{issue_number}")
|
||||
OutputFormatter.empty_line()
|
||||
|
||||
if test_count > 0:
|
||||
print(f"📦 Moving {test_count} test(s) to tests/ directory...")
|
||||
OutputFormatter.success("Tests moved to main tests/ directory")
|
||||
else:
|
||||
OutputFormatter.warning("No tests found in workspace")
|
||||
|
||||
print("🧹 Cleaning up workspace...")
|
||||
OutputFormatter.success(f"Issue #{issue_number} workspace cleaned up")
|
||||
OutputFormatter.empty_line()
|
||||
|
||||
print("💡 Next steps:")
|
||||
print(" - Run 'make test' to verify tests fail (red state)")
|
||||
print(" - Implement code to make tests pass (green state)")
|
||||
print(" - Start next issue with 'make tdd-start NUM=X'")
|
||||
|
||||
|
||||
class IssueView:
|
||||
"""View for issue information display."""
|
||||
|
||||
@staticmethod
|
||||
def show_list(issues: List[Any], title: str = "Project Issues") -> None:
|
||||
"""Display issue list."""
|
||||
OutputFormatter.header(title, "=")
|
||||
|
||||
if not issues:
|
||||
print("No issues found")
|
||||
return
|
||||
|
||||
for issue in issues:
|
||||
status_icon = "🟢" if issue.state == "open" else "🔴"
|
||||
print(f"{status_icon} #{issue.number}: {issue.title}")
|
||||
print(f" Status: {issue.state.upper()} | Created: {issue.created_at.strftime('%Y-%m-%d')}")
|
||||
|
||||
# Truncate body for list view
|
||||
body_preview = issue.body[:80] + "..." if len(issue.body) > 80 else issue.body
|
||||
if body_preview:
|
||||
print(f" {body_preview}")
|
||||
OutputFormatter.empty_line()
|
||||
|
||||
print("💡 Tip: Use 'make show-issue NUM=X' for full details")
|
||||
|
||||
@staticmethod
|
||||
def show_open_issues(issues: List[Any]) -> None:
|
||||
"""Display open issues list."""
|
||||
OutputFormatter.header("Open Project Issues (Active Backlog)", "=")
|
||||
|
||||
if not issues:
|
||||
print("No open issues found")
|
||||
return
|
||||
|
||||
for issue in issues:
|
||||
print(f"[OPEN] #{issue.number}: {issue.title}")
|
||||
created = issue.created_at.strftime('%Y-%m-%d')
|
||||
updated = issue.updated_at.strftime('%Y-%m-%d')
|
||||
print(f" Created: {created} | Updated: {updated}")
|
||||
|
||||
# Truncate body for list view
|
||||
body_preview = issue.body[:80] + "..." if len(issue.body) > 80 else issue.body
|
||||
if body_preview:
|
||||
print(f" {body_preview}")
|
||||
OutputFormatter.empty_line()
|
||||
|
||||
print("💡 Tip: Use 'make show-issue NUM=X' for full details or 'make list-issues' for all issues")
|
||||
|
||||
@staticmethod
|
||||
def show_creation_success(result: Dict[str, Any], issue_type: str = "issue") -> None:
|
||||
"""Display successful issue creation."""
|
||||
OutputFormatter.success(f"{issue_type.title()} created successfully!")
|
||||
OutputFormatter.key_value("Number", f"#{result['number']}")
|
||||
OutputFormatter.key_value("Title", result['title'])
|
||||
OutputFormatter.key_value("Status", result['state'])
|
||||
|
||||
if 'html_url' in result:
|
||||
OutputFormatter.key_value("URL", result['html_url'])
|
||||
|
||||
OutputFormatter.empty_line()
|
||||
print("💡 Next steps:")
|
||||
print(f" - Use 'make tdd-start NUM={result['number']}' to begin work")
|
||||
print(f" - Use 'make show-issue NUM={result['number']}' to view details")
|
||||
|
||||
|
||||
class ProjectView:
|
||||
"""View for project management information display."""
|
||||
|
||||
@staticmethod
|
||||
def show_setup_success() -> None:
|
||||
"""Display successful project setup."""
|
||||
OutputFormatter.success("Project management setup complete!")
|
||||
print("📋 Available states: todo, active, review, done, blocked")
|
||||
print("📊 Available priorities: low, medium, high, critical")
|
||||
|
||||
@staticmethod
|
||||
def show_milestone_list(milestones: List[Any]) -> None:
|
||||
"""Display milestone list."""
|
||||
OutputFormatter.header("Project Milestones", "=")
|
||||
|
||||
if not milestones:
|
||||
print("No milestones found")
|
||||
return
|
||||
|
||||
for milestone in milestones:
|
||||
status_icon = "🟢" if milestone.state == "open" else "🔴"
|
||||
print(f"{status_icon} Milestone #{milestone.id}: {milestone.title}")
|
||||
print(f" State: {milestone.state.upper()}")
|
||||
print(f" Issues: {milestone.open_issues} open, {milestone.closed_issues} closed")
|
||||
if milestone.description:
|
||||
print(f" Description: {milestone.description}")
|
||||
if milestone.due_on:
|
||||
print(f" Due: {milestone.due_on}")
|
||||
OutputFormatter.empty_line()
|
||||
|
||||
@staticmethod
|
||||
def show_overview(overview: Dict[str, Any]) -> None:
|
||||
"""Display project overview."""
|
||||
OutputFormatter.header("Project Management Overview", "=")
|
||||
|
||||
OutputFormatter.key_value("Milestones", f"{overview['milestones']} total")
|
||||
OutputFormatter.key_value("Active Projects", overview['active_projects'], 1)
|
||||
OutputFormatter.key_value("Completed Projects", overview['completed_projects'], 1)
|
||||
OutputFormatter.key_value("Total Labels", overview['total_labels'])
|
||||
|
||||
ready_status = "✅ Yes" if overview['project_management_ready'] else "❌ No - run setup-project-mgmt"
|
||||
OutputFormatter.key_value("Project Management Ready", ready_status)
|
||||
Reference in New Issue
Block a user