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:
116
services/workspace_service.py
Normal file
116
services/workspace_service.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Workspace service - business logic for TDD workspace operations.
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
from pathlib import Path
|
||||
|
||||
from tddai import WorkspaceManager, IssueFetcher, WorkspaceStatus, TddaiError
|
||||
|
||||
|
||||
class WorkspaceInfo:
|
||||
"""Value object for workspace information."""
|
||||
|
||||
def __init__(self, status: WorkspaceStatus, workspace=None):
|
||||
self.status = status
|
||||
self.workspace = workspace
|
||||
|
||||
@property
|
||||
def is_clean(self) -> bool:
|
||||
return self.status == WorkspaceStatus.CLEAN
|
||||
|
||||
@property
|
||||
def is_dirty(self) -> bool:
|
||||
return self.status == WorkspaceStatus.DIRTY
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
return self.status == WorkspaceStatus.ACTIVE
|
||||
|
||||
def get_test_files(self) -> list:
|
||||
"""Get list of test files in workspace."""
|
||||
if not self.workspace or not self.workspace.tests_dir.exists():
|
||||
return []
|
||||
return list(self.workspace.tests_dir.glob("*.py"))
|
||||
|
||||
|
||||
class WorkspaceService:
|
||||
"""Service for workspace operations."""
|
||||
|
||||
def __init__(self):
|
||||
self.workspace_manager = WorkspaceManager()
|
||||
self.issue_fetcher = IssueFetcher()
|
||||
|
||||
def get_workspace_info(self) -> WorkspaceInfo:
|
||||
"""Get current workspace information."""
|
||||
status = self.workspace_manager.get_status()
|
||||
workspace = None
|
||||
|
||||
if status == WorkspaceStatus.ACTIVE:
|
||||
workspace = self.workspace_manager.get_current_workspace()
|
||||
|
||||
return WorkspaceInfo(status, workspace)
|
||||
|
||||
def start_issue_workspace(self, issue_number: int) -> WorkspaceInfo:
|
||||
"""Start working on an issue.
|
||||
|
||||
Returns:
|
||||
WorkspaceInfo with the created workspace
|
||||
|
||||
Raises:
|
||||
TddaiError: If workspace already active or issue cannot be fetched
|
||||
"""
|
||||
# Check if workspace already active
|
||||
current_info = self.get_workspace_info()
|
||||
if current_info.is_active:
|
||||
raise TddaiError(f"Already working on issue #{current_info.workspace.issue_number}")
|
||||
|
||||
# Fetch issue data
|
||||
issue_data = self.issue_fetcher.get_issue_data_dict(issue_number)
|
||||
|
||||
# Create workspace
|
||||
workspace = self.workspace_manager.create_workspace(issue_data)
|
||||
return WorkspaceInfo(WorkspaceStatus.ACTIVE, workspace)
|
||||
|
||||
def finish_current_workspace(self) -> Optional[int]:
|
||||
"""Finish current workspace and return the issue number.
|
||||
|
||||
Returns:
|
||||
Issue number that was finished, or None if no active workspace
|
||||
|
||||
Raises:
|
||||
TddaiError: If workspace operations fail
|
||||
"""
|
||||
current_info = self.get_workspace_info()
|
||||
if not current_info.is_active:
|
||||
return None
|
||||
|
||||
issue_number = current_info.workspace.issue_number
|
||||
self.workspace_manager.finish_workspace()
|
||||
return issue_number
|
||||
|
||||
def get_workspace_summary(self) -> Dict[str, Any]:
|
||||
"""Get workspace summary for display purposes."""
|
||||
info = self.get_workspace_info()
|
||||
|
||||
summary = {
|
||||
'status': info.status,
|
||||
'active': info.is_active,
|
||||
'clean': info.is_clean,
|
||||
'dirty': info.is_dirty
|
||||
}
|
||||
|
||||
if info.workspace:
|
||||
test_files = info.get_test_files()
|
||||
summary.update({
|
||||
'issue_number': info.workspace.issue_number,
|
||||
'issue_title': info.workspace.issue_title,
|
||||
'issue_state': info.workspace.issue_state,
|
||||
'workspace_dir': str(info.workspace.workspace_dir),
|
||||
'test_count': len(test_files),
|
||||
'test_files': [f.name for f in test_files],
|
||||
'requirements_file': str(info.workspace.requirements_file),
|
||||
'test_plan_file': str(info.workspace.test_plan_file)
|
||||
})
|
||||
|
||||
return summary
|
||||
Reference in New Issue
Block a user