feat: complete issue-facade capability enhancement and project cleanup
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled

- Update TODO.md to reflect completed issue-facade capability fixes
- Archive old CLI structure files that were moved to capabilities/issue-facade
- Reorganize remaining CLI components into issue_tracker/ package
- Add test coverage for issue #166 substack theme implementation
- Update document manager and markdown command plugins with latest improvements
- Complete project reorganization following capability-based architecture

This commit finalizes the issue-facade capability enhancement project and
ensures the main repository reflects the current state of all completed work.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-10 10:53:37 +01:00
parent 747b77b854
commit 32d26e7648
12 changed files with 246 additions and 22 deletions

View File

@@ -0,0 +1,18 @@
"""
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
from .config import ConfigPresenter
__all__ = [
'OutputFormatter',
'WorkspaceView',
'IssueView',
'ProjectView',
'ConfigPresenter'
]

View File

@@ -0,0 +1,347 @@
"""
Configuration command presenters.
Handles output formatting and display for configuration management commands.
"""
import json
from datetime import datetime
from pathlib import Path
from typing import Dict, Any, List, Optional
from config import MarkitectConfig
from .formatters import OutputFormatter
class ConfigPresenter:
"""Presenter for configuration management commands."""
def show_error(self, message: str) -> None:
"""Display error message."""
OutputFormatter.error(message)
def show_configuration(self, config: MarkitectConfig, status: Dict[str, Any],
show_sensitive: bool = False) -> None:
"""Display current configuration values."""
OutputFormatter.header("🔧 Configuration Status")
# Basic configuration
OutputFormatter.section("Core Configuration")
self._show_config_table(config, show_sensitive)
# Configuration sources
OutputFormatter.section("Configuration Sources")
self._show_config_sources(status)
# Workspace status
OutputFormatter.section("Workspace Information")
self._show_workspace_info(config)
def show_validation_results(self, results: List[Dict[str, Any]], verbose: bool = False) -> None:
"""Display configuration validation results."""
OutputFormatter.header("✅ Configuration Validation")
# Count results by status
success_count = sum(1 for r in results if r['status'] == 'success')
warning_count = sum(1 for r in results if r['status'] == 'warning')
error_count = sum(1 for r in results if r['status'] == 'error')
# Summary
total = len(results)
print(f"📊 Summary: {success_count}/{total} checks passed")
if warning_count > 0:
print(f"⚠️ {warning_count} warnings")
if error_count > 0:
print(f"{error_count} errors")
print()
# Show results
for result in results:
status_icon = {
'success': '',
'warning': '⚠️',
'error': ''
}[result['status']]
print(f"{status_icon} {result['check']}")
print(f" {result['message']}")
if result['status'] != 'success' and 'suggestion' in result:
print(f" 💡 {result['suggestion']}")
if verbose or result['status'] == 'error':
print()
def show_troubleshooting_results(self, config: Optional[MarkitectConfig],
status: Optional[Dict[str, Any]],
diagnostics: Dict[str, Any]) -> None:
"""Display comprehensive troubleshooting information."""
OutputFormatter.header("🔍 Configuration Troubleshooting")
if config:
print("✅ Configuration loaded successfully")
print()
# Environment diagnostics
if 'environment' in diagnostics:
OutputFormatter.section("Environment Diagnostics")
self._show_environment_diagnostics(diagnostics['environment'])
# File system diagnostics
if 'filesystem' in diagnostics:
OutputFormatter.section("File System Diagnostics")
self._show_filesystem_diagnostics(diagnostics['filesystem'])
# Configuration files diagnostics
if 'config_files' in diagnostics:
OutputFormatter.section("Configuration Files")
self._show_config_files_diagnostics(diagnostics['config_files'])
# Git repository diagnostics
if 'git_repository' in diagnostics:
OutputFormatter.section("Git Repository")
self._show_git_diagnostics(diagnostics['git_repository'])
# Network diagnostics
if 'network' in diagnostics:
OutputFormatter.section("Network Connectivity")
self._show_network_diagnostics(diagnostics['network'])
# Show configuration if available
if config and status:
OutputFormatter.section("Current Configuration")
self._show_config_table(config, show_sensitive=False)
# Recommendations
self._show_troubleshooting_recommendations(diagnostics)
def show_config_file_status(self, file_checks: Dict[str, Any]) -> None:
"""Display configuration file status."""
OutputFormatter.header("📁 Configuration Files Status")
for filename, info in file_checks.items():
status_icon = "" if info['exists'] else ""
print(f"{status_icon} {filename}")
if info['exists']:
print(f" 📍 Path: {info['path']}")
print(f" 📏 Size: {info['size']} bytes")
if info['readable']:
print(" 🔓 Readable: Yes")
else:
print(" 🔒 Readable: No")
# Show parsed variables for .env files
if 'parsed_variables' in info:
if info['parse_error']:
print(f" ❌ Parse error: {info['parse_error']}")
else:
print(f" 🔧 Variables: {info['parsed_variables']}")
if info.get('modified'):
modified_time = datetime.fromtimestamp(info['modified'])
print(f" 🕒 Modified: {modified_time.strftime('%Y-%m-%d %H:%M:%S')}")
else:
print(f" 📍 Expected path: {info['path']}")
print(" ❌ File not found")
print()
def _show_config_table(self, config: MarkitectConfig, show_sensitive: bool = False) -> None:
"""Show configuration in table format."""
config_items = [
("Gitea URL", config.gitea_url),
("Repository Owner", config.repo_owner),
("Repository Name", config.repo_name),
("Workspace Directory", config.workspace_dir),
("Database Path", getattr(config, 'database_path', 'Default')),
]
# Add sensitive information if requested
if show_sensitive:
import os
token = os.getenv('GITEA_API_TOKEN') or os.getenv('GITHUB_TOKEN')
if token:
masked_token = f"{token[:8]}...{token[-4:]}" if len(token) > 12 else "***"
config_items.append(("Auth Token", masked_token))
max_key_length = max(len(key) for key, _ in config_items)
for key, value in config_items:
print(f" {key:<{max_key_length}} : {value or 'Not set'}")
print()
def _show_config_sources(self, status: Dict[str, Any]) -> None:
"""Show configuration sources information."""
if not status:
print(" ❌ Configuration status not available")
return
sources = status.get('sources', {})
for source_name, source_info in sources.items():
if source_info.get('loaded'):
print(f"{source_name}: {source_info.get('path', 'System')}")
else:
print(f" ⏸️ {source_name}: Not loaded")
print()
def _show_workspace_info(self, config: MarkitectConfig) -> None:
"""Show workspace information."""
workspace_path = Path(config.workspace_dir)
print(f" 📁 Workspace: {workspace_path}")
print(f" 📍 Exists: {'Yes' if workspace_path.exists() else 'No'}")
if workspace_path.exists():
try:
items = list(workspace_path.iterdir())
print(f" 📄 Items: {len(items)}")
except PermissionError:
print(" ❌ Permission denied")
print()
def _show_environment_diagnostics(self, env_info: Dict[str, Any]) -> None:
"""Show environment diagnostics."""
print(f" 🐍 Python: {env_info['python_version'].split()[0]}")
print(f" 📍 Executable: {env_info['python_executable']}")
print(f" 📁 Current Dir: {env_info['current_directory']}")
print()
print(" Environment Variables:")
env_vars = env_info['environment_variables']
for var_name, var_info in env_vars.items():
if var_info['set']:
icon = ""
if 'TOKEN' in var_name:
value_display = f"Set ({var_info['length']} chars)"
else:
value_display = var_info['value']
else:
icon = ""
value_display = "Not set"
print(f" {icon} {var_name}: {value_display}")
print()
def _show_filesystem_diagnostics(self, fs_info: Dict[str, Any]) -> None:
"""Show filesystem diagnostics."""
for dir_type, dir_info in fs_info.items():
print(f" 📁 {dir_type.replace('_', ' ').title()}:")
print(f" 📍 Path: {dir_info['path']}")
print(f" ✅ Exists: {dir_info['exists']}")
print(f" 🔓 Readable: {dir_info['readable']}")
print(f" ✏️ Writable: {dir_info['writable']}")
print()
def _show_config_files_diagnostics(self, files_info: Dict[str, Any]) -> None:
"""Show configuration files diagnostics."""
for filename, file_info in files_info.items():
status_icon = "" if file_info['exists'] else ""
print(f" {status_icon} {filename}")
if file_info['exists']:
print(f" 📏 Size: {file_info['size']} bytes")
print(f" 🔓 Readable: {file_info['readable']}")
if 'parsed_variables' in file_info:
if file_info['parse_error']:
print(f" ❌ Parse error: {file_info['parse_error']}")
else:
print(f" 🔧 Variables: {file_info['parsed_variables']}")
print()
def _show_git_diagnostics(self, git_info: Dict[str, Any]) -> None:
"""Show git repository diagnostics."""
if git_info['is_git_repository']:
print(" ✅ Git repository detected")
if 'remote_origin' in git_info:
print(f" 🌐 Remote origin: {git_info['remote_origin']}")
if 'current_branch' in git_info:
print(f" 🌿 Current branch: {git_info['current_branch']}")
if git_info.get('git_command_available', True):
print(" ✅ Git command available")
else:
print(" ❌ Git command not available")
else:
print(" ❌ Not a git repository")
print()
def _show_network_diagnostics(self, network_info: Dict[str, Any]) -> None:
"""Show network connectivity diagnostics."""
if 'gitea_connectivity' in network_info:
conn_info = network_info['gitea_connectivity']
if conn_info['reachable']:
print(f"{conn_info['url']} - Reachable (HTTP {conn_info['status_code']})")
else:
print(f"{conn_info['url']} - Not reachable")
print(f" Error: {conn_info['error']}")
print()
def _show_troubleshooting_recommendations(self, diagnostics: Dict[str, Any]) -> None:
"""Show troubleshooting recommendations based on diagnostics."""
OutputFormatter.section("💡 Recommendations")
recommendations = []
# Check for missing .env.tddai
config_files = diagnostics.get('config_files', {})
if not config_files.get('.env.tddai', {}).get('exists'):
recommendations.append(
"Create .env.tddai file with your configuration:\n"
" TDDAI_GITEA_URL=https://your-git-platform.com\n"
" TDDAI_REPO_OWNER=your-username\n"
" TDDAI_REPO_NAME=your-repo"
)
# Check for missing environment variables
env_vars = diagnostics.get('environment', {}).get('environment_variables', {})
missing_required = []
for var in ['TDDAI_GITEA_URL', 'TDDAI_REPO_OWNER', 'TDDAI_REPO_NAME']:
if not env_vars.get(var, {}).get('set'):
missing_required.append(var)
if missing_required:
recommendations.append(
f"Set missing required environment variables: {', '.join(missing_required)}"
)
# Check for missing auth token
if not env_vars.get('GITEA_API_TOKEN', {}).get('set') and \
not env_vars.get('GITHUB_TOKEN', {}).get('set'):
recommendations.append(
"Set authentication token for API access:\n"
" export GITEA_API_TOKEN=your-token\n"
" or\n"
" export GITHUB_TOKEN=your-token"
)
# Check git repository
git_info = diagnostics.get('git_repository', {})
if not git_info.get('is_git_repository'):
recommendations.append(
"Initialize git repository:\n"
" git init\n"
" git remote add origin <your-repo-url>"
)
# Network connectivity issues
network_info = diagnostics.get('network', {})
gitea_conn = network_info.get('gitea_connectivity', {})
if gitea_conn and not gitea_conn.get('reachable'):
recommendations.append(
"Check network connectivity and firewall settings\n"
"Verify the Gitea URL is correct and accessible"
)
if recommendations:
for i, rec in enumerate(recommendations, 1):
print(f"{i}. {rec}")
print()
else:
print("✅ No issues detected! Configuration looks good.")
print()

View 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()

View File

@@ -0,0 +1,325 @@
"""
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 = getattr(issue, 'body', '')
body_preview = body[:80] + "..." if len(body) > 80 else 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 = getattr(issue, 'body', '')
body_preview = body[:80] + "..." if len(body) > 80 else 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")
@staticmethod
def show_issue_details(issue_data: Dict[str, Any]) -> None:
"""Display comprehensive issue details."""
OutputFormatter.header(f"Issue #{issue_data['number']} Details", "=")
print(f"**Title:** {issue_data['title']}")
print(f"**Status:** {issue_data['state'].upper()}")
print(f"**Number:** #{issue_data['number']}")
print(f"**Created:** {issue_data['created_at'].strftime('%Y-%m-%d %H:%M')}")
print(f"**Updated:** {issue_data['updated_at'].strftime('%Y-%m-%d %H:%M')}")
print(f"**URL:** {issue_data['html_url']}")
if issue_data['assignee']:
print(f"**Assignee:** {issue_data['assignee']}")
OutputFormatter.empty_line()
print("**Project Management:**")
# Milestone information
if issue_data['milestone']:
milestone = issue_data['milestone']
print(f" 📋 Milestone: #{milestone['id']} - {milestone['title']} ({milestone['state']})")
else:
print(" 📋 Milestone: None")
# Project/Board information
print(" 🎯 Project: Getting Started (assumed - requires board API)")
# Labels and state information
print(f" 📊 State: {issue_data['state_label']}")
print(f" 🚨 Priority: {issue_data['priority_label']}")
if issue_data['type_labels']:
type_display = ', '.join(issue_data['type_labels'])
print(f" 🏷️ Type: {type_display}")
if issue_data['other_labels']:
print(f" 🏷️ Other Labels: {', '.join(issue_data['other_labels'])}")
print(f" 📝 Kanban Column: {issue_data['kanban_column']}")
OutputFormatter.empty_line()
print("**Description:**")
print(issue_data['body'])
OutputFormatter.empty_line()
print("💡 Tip: Use 'make list-issues' to see all issues")
@staticmethod
def show_coverage_analysis(coverage_data: Dict[str, Any]) -> None:
"""Display test coverage analysis results."""
OutputFormatter.header(f"Test Coverage Analysis for Issue #{coverage_data['issue_number']}", "=")
print(f"📋 Issue: #{coverage_data['issue_number']} - {coverage_data['issue_title']}")
print(f"📊 Coverage: {coverage_data['coverage_percentage']:.1f}%")
OutputFormatter.empty_line()
# Show requirements analysis
print("🎯 Identified Requirements:")
if coverage_data['requirements']:
for req in coverage_data['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")
OutputFormatter.empty_line()
# Show existing tests
print("🧪 Existing Test Coverage:")
issue_number = coverage_data['issue_number']
issue_related_tests = [t for t in coverage_data['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 coverage_data['existing_tests']
if any(keyword in ' '.join(t.coverage_keywords)
for req in coverage_data['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}")
OutputFormatter.empty_line()
# Show coverage gaps
if coverage_data['coverage_gaps']:
print("❌ Coverage Gaps Found:")
for gap in coverage_data['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}")
OutputFormatter.empty_line()
else:
print("✅ No significant coverage gaps detected!")
OutputFormatter.empty_line()
# Show recommendations
print("📝 Recommendations:")
for recommendation in coverage_data['recommendations']:
print(f" {recommendation}")
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)