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:
2025-09-26 15:08:54 +02:00
parent fd8f792f08
commit 7f5309c4b0
17 changed files with 1274 additions and 713 deletions

View File

@@ -1,802 +1,142 @@
#!/usr/bin/env python3
"""
CLI interface for tddai library.
This module now uses the separated architecture with services and presenters.
Business logic is handled by services, presentation by CLI framework.
"""
import sys
import argparse
from pathlib import Path
# Add current directory to path so we can import tddai
# Add current directory to path so we can import modules
sys.path.insert(0, str(Path(__file__).parent))
from tddai import (
WorkspaceManager, IssueFetcher, TestGenerator, CoverageAnalyzer,
WorkspaceStatus, TddaiError
)
from tddai.issue_creator import IssueCreator
from tddai.project_manager import ProjectManager, ProjectState, Priority
from cli import CLIFramework
# Initialize CLI framework
cli = CLIFramework()
def workspace_status():
"""Show current workspace status."""
try:
manager = WorkspaceManager()
status = manager.get_status()
if status == WorkspaceStatus.CLEAN:
print("📋 No active issue workspace")
print(" Use 'make tdd-start NUM=X' to begin working on an issue")
return
if status == WorkspaceStatus.DIRTY:
print("⚠️ 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
workspace = manager.get_current_workspace()
if not workspace:
print("❌ Failed to load workspace")
return
print("📋 Active Issue Workspace")
print("========================")
print()
print(f"🎯 Issue #{workspace.issue_number}: {workspace.issue_title}")
print(f"📊 Status: {workspace.issue_state}")
print(f"📁 Workspace: {workspace.workspace_dir}/issue_{workspace.issue_number}/")
print()
if workspace.tests_dir.exists():
test_files = list(workspace.tests_dir.glob("*.py"))
print(f"🧪 Generated Tests ({len(test_files)}):")
if test_files:
for test_file in test_files:
print(f" - {test_file.name}")
else:
print(" - No tests generated yet")
print()
print("📋 Workspace Files:")
print(" - requirements.md (review and break down issue)")
print(" - test_plan.md (plan test scenarios)")
print(" - tests/ (generated test files)")
print()
print("💡 Commands:")
print(" - make tdd-add-test (generate another test)")
print(" - make tdd-finish (complete and move tests to main)")
except TddaiError as e:
print(f"❌ Error: {e}")
sys.exit(1)
cli.workspace_status()
def start_issue(issue_number: int):
"""Start working on an issue."""
try:
manager = WorkspaceManager()
fetcher = IssueFetcher()
# Check if workspace already active
status = manager.get_status()
if status == WorkspaceStatus.ACTIVE:
current = manager.get_current_workspace()
print(f"⚠️ Already working on issue #{current.issue_number}")
print(" Run 'make tdd-finish' first or 'make tdd-status' to see details")
sys.exit(1)
print(f"🔍 Starting work on issue #{issue_number}...")
print(f"📋 Fetching issue #{issue_number} details...")
# Fetch issue data
issue_data = fetcher.get_issue_data_dict(issue_number)
# Create workspace
workspace = manager.create_workspace(issue_data)
print(f"✅ Workspace created for issue #{issue_number}")
print(f"📁 Workspace: {workspace.workspace_dir}/issue_{issue_number}/")
print(f"📋 Requirements: {workspace.requirements_file}")
print(f"🧪 Test plan: {workspace.test_plan_file}")
print()
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")
except TddaiError as e:
print(f"❌ Error: {e}")
sys.exit(1)
cli.start_issue(issue_number)
def finish_issue():
"""Finish current issue workspace."""
try:
manager = WorkspaceManager()
workspace = manager.get_current_workspace()
if not workspace:
print("❌ No active issue workspace")
print(" Nothing to finish")
sys.exit(1)
print(f"🏁 Finishing work on issue #{workspace.issue_number}")
print()
# Check for tests
if workspace.tests_dir.exists():
test_files = list(workspace.tests_dir.glob("*.py"))
if test_files:
print(f"📦 Moving {len(test_files)} test(s) to tests/ directory...")
print("✅ Tests moved to main tests/ directory")
else:
print("⚠️ No tests found in workspace")
# Finish workspace (moves tests and cleans up)
manager.finish_workspace()
print("🧹 Cleaning up workspace...")
print(f"✅ Issue #{workspace.issue_number} workspace cleaned up")
print()
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'")
except TddaiError as e:
print(f"❌ Error: {e}")
sys.exit(1)
cli.finish_issue()
def add_test_guidance():
"""Show guidance for adding tests."""
try:
manager = WorkspaceManager()
workspace = manager.get_current_workspace()
if not workspace:
print("❌ No active issue workspace")
print(" Run 'make tdd-start NUM=X' first")
sys.exit(1)
print(f"🧪 Adding test to issue #{workspace.issue_number} workspace")
print()
print(f"📋 Issue: {workspace.issue_title}")
print(f"📁 Workspace: {workspace.workspace_dir}/issue_{workspace.issue_number}/")
print()
print("🤖 Please ask Claude Code to generate a test:")
print()
print(" Command: 'Generate a test for the current workspace issue'")
print()
print("📝 Test Requirements:")
print(f" - Save test in: {workspace.tests_dir}/")
print(f" - Name format: test_issue_{workspace.issue_number}_<scenario>.py")
print(f" - Include docstring referencing issue #{workspace.issue_number}")
print(" - Follow TDD principles (test should fail initially)")
print(" - Review requirements.md and test_plan.md for context")
print()
print("📋 Issue Details:")
print(f" Title: {workspace.issue_title}")
print(f" Description: {workspace.issue_body}")
print()
print("💡 After generation: Use 'make tdd-status' to see all tests")
except TddaiError as e:
print(f"❌ Error: {e}")
sys.exit(1)
cli.add_test_guidance()
def list_issues():
"""List all issues."""
try:
fetcher = IssueFetcher()
print("📋 Project Issues")
print("==================")
print()
issues = fetcher.fetch_issues()
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}")
print()
print("💡 Tip: Use 'make show-issue NUM=X' for full details")
except TddaiError as e:
print(f"❌ Error: {e}")
sys.exit(1)
cli.list_issues()
def list_open_issues():
"""List only open issues."""
try:
fetcher = IssueFetcher()
print("📋 Open Project Issues (Active Backlog)")
print("========================================")
print()
issues = fetcher.fetch_open_issues()
if not issues:
print("No open issues found")
return
for issue in issues:
print(f"[OPEN] #{issue.number}: {issue.title}")
print(f" Created: {issue.created_at.strftime('%Y-%m-%d')} | Updated: {issue.updated_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}")
print()
print("💡 Tip: Use 'make show-issue NUM=X' for full details or 'make list-issues' for all issues")
except TddaiError as e:
print(f"❌ Error: {e}")
sys.exit(1)
def analyze_coverage(issue_number: int):
"""Analyze test coverage for a specific issue."""
try:
analyzer = CoverageAnalyzer()
print(f"🔍 Analyzing test coverage for Issue #{issue_number}")
print("=" * 50)
print()
assessment = analyzer.analyze_issue_coverage(issue_number)
print(f"📋 Issue: #{assessment.issue_number} - {assessment.issue_title}")
print(f"📊 Coverage: {assessment.coverage_percentage:.1f}%")
print()
# Show requirements analysis
print("🎯 Identified Requirements:")
if assessment.requirements:
for req in assessment.requirements:
priority_icon = {"critical": "🚨", "important": "⚠️", "nice-to-have": "💡"}
icon = priority_icon.get(req.priority, "📝")
print(f" {icon} [{req.priority.upper()}] {req.category}: {req.description}")
else:
print(" No specific requirements detected")
print()
# Show existing tests
print("🧪 Existing Test Coverage:")
issue_related_tests = [t for t in assessment.existing_tests if t.related_issue == issue_number]
if issue_related_tests:
for test in issue_related_tests:
test_count = len(test.test_methods)
print(f"{test.file_path.name} ({test_count} test methods)")
if test.test_methods:
for method in test.test_methods[:3]: # Show first 3
print(f" - {method}")
if len(test.test_methods) > 3:
print(f" - ... and {len(test.test_methods) - 3} more")
else:
print(" 📝 No tests specifically for this issue found")
# Show general tests that might be relevant
relevant_tests = [t for t in assessment.existing_tests
if any(keyword in ' '.join(t.coverage_keywords)
for req in assessment.requirements
for keyword in req.keywords)]
if relevant_tests:
print(" 📋 Potentially relevant tests:")
for test in relevant_tests[:3]:
print(f" 📄 {test.file_path.name}")
print()
# Show coverage gaps
if assessment.coverage_gaps:
print("❌ Coverage Gaps Found:")
for gap in assessment.coverage_gaps:
priority_icon = {"critical": "🚨", "important": "⚠️", "nice-to-have": "💡"}
icon = priority_icon.get(gap.requirement.priority, "📝")
print(f" {icon} Missing: {gap.requirement.description}")
print(f" 💡 Suggested test: {gap.suggested_test_name}")
print(f" 📄 Suggested file: {gap.suggested_test_file}")
print()
else:
print("✅ No significant coverage gaps detected!")
print()
# Show recommendations
print("📝 Recommendations:")
for recommendation in assessment.recommendations:
print(f" {recommendation}")
except TddaiError as e:
print(f"❌ Error: {e}")
sys.exit(1)
cli.list_open_issues()
def show_issue(issue_number: int):
"""Show detailed issue information with comprehensive project management details."""
try:
fetcher = IssueFetcher()
project_mgr = ProjectManager()
print(f"🔍 Issue #{issue_number} Details")
print("=======================")
print()
# Get basic issue information
issue = fetcher.fetch_issue(issue_number)
print(f"**Title:** {issue.title}")
print(f"**Status:** {issue.state.upper()}")
print(f"**Number:** #{issue.number}")
print(f"**Created:** {issue.created_at.strftime('%Y-%m-%d %H:%M')}")
print(f"**Updated:** {issue.updated_at.strftime('%Y-%m-%d %H:%M')}")
print(f"**URL:** {issue.html_url}")
if issue.assignee:
print(f"**Assignee:** {issue.assignee}")
# Enhanced project management information
print()
print("**Project Management:**")
# Get detailed issue data via API for milestone and project information
from tddai.config import get_config
config = get_config()
issue_url = f"{config.issues_api_url}/{issue_number}"
detailed_issue = project_mgr._make_api_call('GET', issue_url)
# Milestone information
if detailed_issue.get('milestone'):
milestone = detailed_issue['milestone']
print(f" 📋 Milestone: #{milestone['id']} - {milestone['title']} ({milestone['state']})")
else:
print(f" 📋 Milestone: None")
# Project/Board information (if available through API)
# Note: Gitea project boards may use different API endpoints
print(f" 🎯 Project: Getting Started (assumed - requires board API)")
# Labels and state information
labels = detailed_issue.get('labels', [])
if labels:
state_labels = [l['name'] for l in labels if l['name'].startswith('status:')]
priority_labels = [l['name'] for l in labels if l['name'].startswith('priority:')]
type_labels = [l['name'] for l in labels if l['name'].startswith('type:')]
other_labels = [l['name'] for l in labels if not any(l['name'].startswith(p) for p in ['status:', 'priority:', 'type:'])]
if state_labels:
state_display = state_labels[0].replace('status:', '').title()
print(f" 📊 State: {state_display}")
else:
print(f" 📊 State: No state label")
if priority_labels:
priority_display = priority_labels[0].replace('priority:', '').title()
print(f" 🚨 Priority: {priority_display}")
else:
print(f" 🚨 Priority: No priority set")
if type_labels:
type_display = ', '.join([l.replace('type:', '').title() for l in type_labels])
print(f" 🏷️ Type: {type_display}")
if other_labels:
print(f" 🏷️ Other Labels: {', '.join(other_labels)}")
else:
print(f" 📊 State: No state label")
print(f" 🚨 Priority: No priority set")
print(f" 🏷️ Labels: None")
# Column information (based on state and issue status)
if detailed_issue.get('state') == 'closed':
if any(l['name'] == 'status:done' for l in labels):
column = "Done"
else:
column = "Closed"
else:
state_labels = [l['name'] for l in labels if l['name'].startswith('status:')]
if state_labels:
state = state_labels[0].replace('status:', '')
column_map = {
'todo': 'Todo',
'active': 'Active',
'review': 'Review',
'blocked': 'Blocked'
}
column = column_map.get(state, 'Todo')
else:
column = "Todo"
print(f" 📝 Kanban Column: {column}")
print()
print("**Description:**")
print(issue.body)
print()
print("💡 Tip: Use 'make list-issues' to see all issues")
except TddaiError as e:
print(f"❌ Error: {e}")
sys.exit(1)
"""Show detailed issue information."""
cli.show_issue(issue_number)
def create_issue(title: str, body: str, issue_type: str = "enhancement"):
"""Create a new issue."""
try:
creator = IssueCreator()
print(f"🚀 Creating {issue_type} issue: {title}")
print()
if issue_type == "enhancement":
# For enhancements, assume body contains structured content
result = creator.create_issue(title, body, labels=[issue_type])
elif issue_type == "bug":
result = creator.create_issue(title, body, labels=[issue_type])
else:
result = creator.create_issue(title, body)
print("✅ Issue created successfully!")
print(f" Number: #{result['number']}")
print(f" Title: {result['title']}")
print(f" Status: {result['state']}")
if 'html_url' in result:
print(f" URL: {result['html_url']}")
print()
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")
except TddaiError as e:
print(f"❌ Error creating issue: {e}")
sys.exit(1)
cli.create_issue(title, body, issue_type)
def create_enhancement_issue(title: str, use_case: str, technical_requirements: str = "",
acceptance_criteria: str = "", dependencies: str = "",
priority: str = "Medium"):
"""Create a structured enhancement issue."""
try:
creator = IssueCreator()
print(f"🚀 Creating enhancement issue: {title}")
print()
# Parse acceptance criteria if provided
criteria_list = []
if acceptance_criteria:
criteria_list = [line.strip() for line in acceptance_criteria.split('\n') if line.strip()]
# Parse acceptance criteria if provided
criteria_list = []
if acceptance_criteria:
criteria_list = [line.strip() for line in acceptance_criteria.split('\n') if line.strip()]
# Parse dependencies if provided
deps_list = []
if dependencies:
deps_list = [line.strip() for line in dependencies.split('\n') if line.strip()]
# Parse dependencies if provided
deps_list = []
if dependencies:
deps_list = [line.strip() for line in dependencies.split('\n') if line.strip()]
result = creator.create_enhancement_issue(
title=title,
use_case=use_case,
technical_requirements=technical_requirements,
acceptance_criteria=criteria_list,
dependencies=deps_list,
priority=priority
)
print("✅ Enhancement issue created successfully!")
print(f" Number: #{result['number']}")
print(f" Title: {result['title']}")
print(f" Priority: {priority}")
if 'html_url' in result:
print(f" URL: {result['html_url']}")
print()
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")
except TddaiError as e:
print(f"❌ Error creating enhancement issue: {e}")
sys.exit(1)
cli.create_enhancement_issue(
title=title,
use_case=use_case,
technical_requirements=technical_requirements,
acceptance_criteria=criteria_list,
dependencies=deps_list,
priority=priority
)
def create_from_template(template_file: str, **kwargs):
"""Create issue from template file."""
try:
creator = IssueCreator()
print(f"🚀 Creating issue from template: {template_file}")
print()
cli.create_from_template(template_file, **kwargs)
result = creator.create_from_template(template_file, **kwargs)
print("✅ Issue created from template successfully!")
print(f" Number: #{result['number']}")
print(f" Title: {result['title']}")
if 'html_url' in result:
print(f" URL: {result['html_url']}")
print()
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")
except TddaiError as e:
print(f"❌ Error creating issue from template: {e}")
sys.exit(1)
def analyze_coverage(issue_number: int):
"""Analyze test coverage for a specific issue."""
cli.analyze_coverage(issue_number)
def setup_project_management():
"""Setup project management labels and milestones."""
try:
project_mgr = ProjectManager()
print("🚀 Setting up project management system...")
# Ensure all required labels exist
project_mgr.ensure_project_labels()
print("✅ Project management setup complete!")
print("📋 Available states: todo, active, review, done, blocked")
print("📊 Available priorities: low, medium, high, critical")
except TddaiError as e:
print(f"❌ Error setting up project management: {e}")
sys.exit(1)
cli.setup_project_management()
def move_issue_to_state(issue_number: int, state: str):
"""Move issue to a specific project state."""
try:
project_mgr = ProjectManager()
# Convert string to ProjectState enum
state_map = {
'todo': ProjectState.TODO,
'active': ProjectState.ACTIVE,
'review': ProjectState.REVIEW,
'done': ProjectState.DONE,
'blocked': ProjectState.BLOCKED
}
if state not in state_map:
print(f"❌ Invalid state '{state}'. Valid states: {list(state_map.keys())}")
sys.exit(1)
project_state = state_map[state]
print(f"📋 Moving issue #{issue_number} to {state} state...")
result = project_mgr.set_issue_state(issue_number, project_state)
# If moving to done, also close the issue
if state == 'done':
project_mgr.move_issue_to_done(issue_number)
print(f"✅ Issue #{issue_number} moved to {state} and closed")
else:
print(f"✅ Issue #{issue_number} moved to {state}")
except TddaiError as e:
print(f"❌ Error moving issue to {state}: {e}")
sys.exit(1)
cli.move_issue_to_state(issue_number, state)
def set_issue_priority(issue_number: int, priority: str):
"""Set issue priority."""
try:
project_mgr = ProjectManager()
# Convert string to Priority enum
priority_map = {
'low': Priority.LOW,
'medium': Priority.MEDIUM,
'high': Priority.HIGH,
'critical': Priority.CRITICAL
}
if priority not in priority_map:
print(f"❌ Invalid priority '{priority}'. Valid priorities: {list(priority_map.keys())}")
sys.exit(1)
priority_level = priority_map[priority]
print(f"📊 Setting issue #{issue_number} priority to {priority}...")
result = project_mgr.set_issue_priority(issue_number, priority_level)
print(f"✅ Issue #{issue_number} priority set to {priority}")
except TddaiError as e:
print(f"❌ Error setting issue priority: {e}")
sys.exit(1)
cli.set_issue_priority(issue_number, priority)
def create_milestone(title: str, description: str = ""):
"""Create a new milestone (project)."""
try:
project_mgr = ProjectManager()
print(f"🚀 Creating milestone: {title}")
milestone = project_mgr.create_milestone(title, description)
print(f"✅ Milestone created successfully!")
print(f" ID: {milestone.id}")
print(f" Title: {milestone.title}")
print(f" Description: {milestone.description}")
print(f" State: {milestone.state}")
except TddaiError as e:
print(f"❌ Error creating milestone: {e}")
sys.exit(1)
cli.create_milestone(title, description)
def list_milestones():
"""List all milestones."""
try:
project_mgr = ProjectManager()
print("📋 Project Milestones")
print("====================")
print()
milestones = project_mgr.list_milestones("all")
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}")
print()
except TddaiError as e:
print(f"❌ Error listing milestones: {e}")
sys.exit(1)
cli.list_milestones()
def assign_issue_to_milestone(issue_number: int, milestone_id: int):
"""Assign issue to a milestone."""
try:
from tddai.issue_writer import IssueWriter
writer = IssueWriter()
print(f"📋 Assigning issue #{issue_number} to milestone #{milestone_id}...")
result = writer.assign_to_milestone(issue_number, milestone_id)
print(f"✅ Issue #{issue_number} assigned to milestone #{milestone_id}")
except TddaiError as e:
print(f"❌ Error assigning issue to milestone: {e}")
sys.exit(1)
def issue_index(format_type="tsv", sort_by="number", filter_state=None, filter_priority=None, include_state=False):
"""Output compact index of all issues with ID, title, and priority for efficient parsing.
Args:
format_type: Output format (tsv, csv, json, fields)
sort_by: Sort by field (number, title, priority, state, created, updated)
filter_state: Filter by state (open, closed, all) - None means all
filter_priority: Filter by priority (low, medium, high, critical, none) - None means all
include_state: Include state column in output
"""
try:
fetcher = IssueFetcher()
import json
issues = fetcher.fetch_issues()
if not issues:
return
# Collect full issue data with additional fields
issue_data = []
for issue in issues:
# Get priority and state from labels - now using the rich issue model
priority = issue.priority or "none"
status = issue.status or "none"
issue_info = {
'number': issue.number,
'title': issue.title.replace('\t', ' ').replace('\n', ' '), # Clean for TSV
'priority': priority,
'state': issue.state, # open/closed from basic data
'status': status, # detailed status from labels
'created': issue.created_at.strftime('%Y-%m-%d'),
'updated': issue.updated_at.strftime('%Y-%m-%d')
}
issue_data.append(issue_info)
# Apply filters
if filter_state:
if filter_state == "open":
issue_data = [i for i in issue_data if i['state'] == 'open']
elif filter_state == "closed":
issue_data = [i for i in issue_data if i['state'] == 'closed']
if filter_priority:
issue_data = [i for i in issue_data if i['priority'] == filter_priority]
# Sort issues
sort_key_map = {
'number': lambda x: x['number'],
'title': lambda x: x['title'].lower(),
'priority': lambda x: {'critical': 4, 'high': 3, 'medium': 2, 'low': 1, 'none': 0}[x['priority']],
'state': lambda x: x['state'],
'created': lambda x: x['created'],
'updated': lambda x: x['updated']
}
if sort_by in sort_key_map:
issue_data.sort(key=sort_key_map[sort_by], reverse=(sort_by in ['number', 'priority', 'created', 'updated']))
# Output in requested format
if format_type == "json":
print(json.dumps(issue_data, indent=2))
elif format_type == "csv":
# CSV header
if include_state:
print("number,title,priority,state,created,updated")
for issue in issue_data:
title = issue['title'].replace('"', '""') # Escape quotes
print(f'{issue["number"]},"{title}",{issue["priority"]},{issue["state"]},{issue["created"]},{issue["updated"]}')
else:
print("number,title,priority,created,updated")
for issue in issue_data:
title = issue['title'].replace('"', '""')
print(f'{issue["number"]},"{title}",{issue["priority"]},{issue["created"]},{issue["updated"]}')
elif format_type == "fields":
# Space-separated fields for easy awk processing
if include_state:
print("NUMBER TITLE PRIORITY STATE CREATED UPDATED")
for issue in issue_data:
title = issue['title'].replace(' ', '_')
print(f'{issue["number"]} {title} {issue["priority"]} {issue["state"]} {issue["created"]} {issue["updated"]}')
else:
print("NUMBER TITLE PRIORITY CREATED UPDATED")
for issue in issue_data:
title = issue['title'].replace(' ', '_')
print(f'{issue["number"]} {title} {issue["priority"]} {issue["created"]} {issue["updated"]}')
else: # Default TSV
if include_state:
for issue in issue_data:
print(f'{issue["number"]}\t{issue["title"]}\t{issue["priority"]}\t{issue["state"]}\t{issue["created"]}\t{issue["updated"]}')
else:
for issue in issue_data:
print(f'{issue["number"]}\t{issue["title"]}\t{issue["priority"]}\t{issue["created"]}\t{issue["updated"]}')
except TddaiError as e:
print(f"❌ Error: {e}", file=sys.stderr)
sys.exit(1)
cli.assign_issue_to_milestone(issue_number, milestone_id)
def project_overview():
"""Show project management overview."""
try:
project_mgr = ProjectManager()
print("📊 Project Management Overview")
print("==============================")
print()
cli.project_overview()
overview = project_mgr.get_project_overview()
print(f"📋 Milestones: {overview['milestones']} total")
print(f" Active Projects: {overview['active_projects']}")
print(f" Completed Projects: {overview['completed_projects']}")
print(f"🏷️ Total Labels: {overview['total_labels']}")
print(f"🎯 Project Management Ready: {'✅ Yes' if overview['project_management_ready'] else '❌ No - run setup-project-mgmt'}")
except TddaiError as e:
print(f"❌ Error getting project overview: {e}")
sys.exit(1)
def issue_index(format_type="tsv", sort_by="number", filter_state=None, filter_priority=None, include_state=False):
"""Output compact index of all issues for Unix processing."""
cli.issue_index(
format_type=format_type,
sort_by=sort_by,
filter_state=filter_state,
filter_priority=filter_priority,
include_state=include_state
)
def main():