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
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:
20
issue_tracker/cli/__init__.py
Normal file
20
issue_tracker/cli/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
CLI presentation layer.
|
||||
|
||||
This package handles all CLI-specific concerns:
|
||||
- Argument parsing and validation
|
||||
- Output formatting and presentation
|
||||
- User interaction and feedback
|
||||
- Error handling and display
|
||||
|
||||
The CLI layer delegates business logic to services and focuses purely on
|
||||
presentation and user interface concerns.
|
||||
"""
|
||||
|
||||
from .core import CLIFramework
|
||||
from .presenters import *
|
||||
from .commands import *
|
||||
|
||||
__all__ = [
|
||||
'CLIFramework'
|
||||
]
|
||||
12
issue_tracker/cli/commands/__init__.py
Normal file
12
issue_tracker/cli/commands/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
CLI command modules.
|
||||
|
||||
Commands handle argument parsing and delegation to services.
|
||||
They contain no business logic, only CLI-specific concerns.
|
||||
"""
|
||||
|
||||
from .config import ConfigCommands
|
||||
|
||||
__all__ = [
|
||||
'ConfigCommands'
|
||||
]
|
||||
325
issue_tracker/cli/commands/config.py
Normal file
325
issue_tracker/cli/commands/config.py
Normal file
@@ -0,0 +1,325 @@
|
||||
"""
|
||||
Configuration management CLI commands.
|
||||
|
||||
Provides commands for configuration validation, display, and troubleshooting.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Tuple, Optional
|
||||
|
||||
from config import (
|
||||
get_unified_config, get_config_status, MarkitectConfig,
|
||||
ConfigurationError, ConfigValidationError, load_env_file
|
||||
)
|
||||
from ..presenters.config import ConfigPresenter
|
||||
|
||||
|
||||
class ConfigCommands:
|
||||
"""Configuration management command handlers."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.presenter = ConfigPresenter()
|
||||
|
||||
def show_config(self, show_sensitive: bool = False) -> None:
|
||||
"""Display current configuration values."""
|
||||
try:
|
||||
config = get_unified_config()
|
||||
status = get_config_status()
|
||||
|
||||
self.presenter.show_configuration(config, status, show_sensitive)
|
||||
|
||||
except ConfigurationError as e:
|
||||
self.presenter.show_error(f"Configuration error: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
self.presenter.show_error(f"Unexpected error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def validate_config(self, verbose: bool = False) -> None:
|
||||
"""Validate current configuration and show any issues."""
|
||||
try:
|
||||
config = get_unified_config()
|
||||
validation_results = self._perform_validation_checks(config)
|
||||
|
||||
self.presenter.show_validation_results(validation_results, verbose)
|
||||
|
||||
# Exit with non-zero code if there are errors
|
||||
if any(result['status'] == 'error' for result in validation_results):
|
||||
sys.exit(1)
|
||||
|
||||
except ConfigurationError as e:
|
||||
self.presenter.show_error(f"Configuration error: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
self.presenter.show_error(f"Unexpected error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def troubleshoot_config(self) -> None:
|
||||
"""Run comprehensive configuration troubleshooting."""
|
||||
try:
|
||||
config = get_unified_config()
|
||||
status = get_config_status()
|
||||
|
||||
# Perform all diagnostic checks
|
||||
diagnostics = self._run_diagnostics(config)
|
||||
|
||||
self.presenter.show_troubleshooting_results(config, status, diagnostics)
|
||||
|
||||
except Exception as e:
|
||||
# Even if config loading fails, we can still provide diagnostics
|
||||
diagnostics = self._run_basic_diagnostics()
|
||||
self.presenter.show_troubleshooting_results(None, None, diagnostics)
|
||||
|
||||
def check_config_files(self) -> None:
|
||||
"""Check for configuration files and their status."""
|
||||
file_checks = self._check_configuration_files()
|
||||
self.presenter.show_config_file_status(file_checks)
|
||||
|
||||
def _perform_validation_checks(self, config: MarkitectConfig) -> List[Dict[str, Any]]:
|
||||
"""Perform comprehensive configuration validation."""
|
||||
results = []
|
||||
|
||||
# Check required fields
|
||||
required_fields = [
|
||||
('gitea_url', 'Gitea/Git platform URL'),
|
||||
('repo_owner', 'Repository owner'),
|
||||
('repo_name', 'Repository name'),
|
||||
]
|
||||
|
||||
for field, description in required_fields:
|
||||
value = getattr(config, field, None)
|
||||
if not value or (isinstance(value, str) and not value.strip()):
|
||||
results.append({
|
||||
'check': f'Required field: {description}',
|
||||
'status': 'error',
|
||||
'message': f'{description} is required but not set',
|
||||
'suggestion': f'Set {field.upper()} in environment or .env.tddai file'
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
'check': f'Required field: {description}',
|
||||
'status': 'success',
|
||||
'message': f'{description} is properly configured'
|
||||
})
|
||||
|
||||
# Check URL format
|
||||
if config.gitea_url:
|
||||
if not (config.gitea_url.startswith('http://') or config.gitea_url.startswith('https://')):
|
||||
results.append({
|
||||
'check': 'URL format validation',
|
||||
'status': 'error',
|
||||
'message': 'Gitea URL must start with http:// or https://',
|
||||
'suggestion': 'Update gitea_url to include protocol (e.g., https://github.com)'
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
'check': 'URL format validation',
|
||||
'status': 'success',
|
||||
'message': 'Gitea URL format is valid'
|
||||
})
|
||||
|
||||
# Check workspace directory
|
||||
workspace_path = Path(config.workspace_dir)
|
||||
if workspace_path.exists() and not workspace_path.is_dir():
|
||||
results.append({
|
||||
'check': 'Workspace directory',
|
||||
'status': 'error',
|
||||
'message': f'Workspace path exists but is not a directory: {workspace_path}',
|
||||
'suggestion': 'Remove the file or choose a different workspace directory'
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
'check': 'Workspace directory',
|
||||
'status': 'success',
|
||||
'message': f'Workspace directory is valid: {workspace_path}'
|
||||
})
|
||||
|
||||
# Check authentication token
|
||||
auth_token = os.getenv('GITEA_API_TOKEN') or os.getenv('GITHUB_TOKEN')
|
||||
if not auth_token:
|
||||
results.append({
|
||||
'check': 'Authentication token',
|
||||
'status': 'warning',
|
||||
'message': 'No authentication token found',
|
||||
'suggestion': 'Set GITEA_API_TOKEN or GITHUB_TOKEN environment variable for API access'
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
'check': 'Authentication token',
|
||||
'status': 'success',
|
||||
'message': 'Authentication token is configured'
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
def _run_diagnostics(self, config: Optional[MarkitectConfig]) -> Dict[str, Any]:
|
||||
"""Run comprehensive diagnostics."""
|
||||
diagnostics = {}
|
||||
|
||||
# Environment diagnostics
|
||||
diagnostics['environment'] = self._check_environment()
|
||||
|
||||
# File system diagnostics
|
||||
diagnostics['filesystem'] = self._check_filesystem()
|
||||
|
||||
# Configuration files diagnostics
|
||||
diagnostics['config_files'] = self._check_configuration_files()
|
||||
|
||||
# Git repository diagnostics
|
||||
diagnostics['git_repository'] = self._check_git_repository()
|
||||
|
||||
# Network diagnostics (if config available)
|
||||
if config:
|
||||
diagnostics['network'] = self._check_network_connectivity(config)
|
||||
|
||||
return diagnostics
|
||||
|
||||
def _run_basic_diagnostics(self) -> Dict[str, Any]:
|
||||
"""Run basic diagnostics when config loading fails."""
|
||||
return {
|
||||
'environment': self._check_environment(),
|
||||
'filesystem': self._check_filesystem(),
|
||||
'config_files': self._check_configuration_files(),
|
||||
'git_repository': self._check_git_repository(),
|
||||
}
|
||||
|
||||
def _check_environment(self) -> Dict[str, Any]:
|
||||
"""Check environment variables and settings."""
|
||||
relevant_vars = [
|
||||
'TDDAI_GITEA_URL', 'TDDAI_REPO_OWNER', 'TDDAI_REPO_NAME',
|
||||
'TDDAI_WORKSPACE_DIR', 'GITEA_API_TOKEN', 'GITHUB_TOKEN',
|
||||
'PYTHONPATH', 'PATH'
|
||||
]
|
||||
|
||||
env_status = {}
|
||||
for var in relevant_vars:
|
||||
value = os.getenv(var)
|
||||
env_status[var] = {
|
||||
'set': value is not None,
|
||||
'value': '***HIDDEN***' if 'TOKEN' in var and value else value,
|
||||
'length': len(value) if value else 0
|
||||
}
|
||||
|
||||
return {
|
||||
'python_version': sys.version,
|
||||
'python_executable': sys.executable,
|
||||
'current_directory': str(Path.cwd()),
|
||||
'environment_variables': env_status
|
||||
}
|
||||
|
||||
def _check_filesystem(self) -> Dict[str, Any]:
|
||||
"""Check file system permissions and paths."""
|
||||
current_dir = Path.cwd()
|
||||
|
||||
return {
|
||||
'current_directory': {
|
||||
'path': str(current_dir),
|
||||
'exists': current_dir.exists(),
|
||||
'readable': os.access(current_dir, os.R_OK),
|
||||
'writable': os.access(current_dir, os.W_OK),
|
||||
},
|
||||
'home_directory': {
|
||||
'path': str(Path.home()),
|
||||
'exists': Path.home().exists(),
|
||||
'readable': os.access(Path.home(), os.R_OK),
|
||||
'writable': os.access(Path.home(), os.W_OK),
|
||||
}
|
||||
}
|
||||
|
||||
def _check_configuration_files(self) -> Dict[str, Any]:
|
||||
"""Check for configuration files and their status."""
|
||||
config_files = {
|
||||
'.env.tddai': Path('.env.tddai'),
|
||||
'.env': Path('.env'),
|
||||
'pyproject.toml': Path('pyproject.toml'),
|
||||
'tddai-setup.sh': Path('tddai-setup.sh'),
|
||||
}
|
||||
|
||||
file_status = {}
|
||||
for name, path in config_files.items():
|
||||
file_status[name] = {
|
||||
'path': str(path),
|
||||
'exists': path.exists(),
|
||||
'readable': path.exists() and os.access(path, os.R_OK),
|
||||
'size': path.stat().st_size if path.exists() else 0,
|
||||
'modified': path.stat().st_mtime if path.exists() else None
|
||||
}
|
||||
|
||||
# Try to parse .env files
|
||||
if name.startswith('.env') and path.exists():
|
||||
try:
|
||||
env_vars = load_env_file(path)
|
||||
file_status[name]['parsed_variables'] = len(env_vars)
|
||||
file_status[name]['parse_error'] = None
|
||||
except Exception as e:
|
||||
file_status[name]['parsed_variables'] = 0
|
||||
file_status[name]['parse_error'] = str(e)
|
||||
|
||||
return file_status
|
||||
|
||||
def _check_git_repository(self) -> Dict[str, Any]:
|
||||
"""Check git repository status."""
|
||||
git_dir = Path('.git')
|
||||
|
||||
status = {
|
||||
'is_git_repository': git_dir.exists(),
|
||||
'git_directory': str(git_dir),
|
||||
}
|
||||
|
||||
if git_dir.exists():
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
# Get remote origin URL
|
||||
result = subprocess.run(
|
||||
['git', 'remote', 'get-url', 'origin'],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
status['remote_origin'] = result.stdout.strip()
|
||||
|
||||
# Get current branch
|
||||
result = subprocess.run(
|
||||
['git', 'branch', '--show-current'],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
status['current_branch'] = result.stdout.strip()
|
||||
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
status['git_command_available'] = False
|
||||
|
||||
return status
|
||||
|
||||
def _check_network_connectivity(self, config: MarkitectConfig) -> Dict[str, Any]:
|
||||
"""Check network connectivity to configured services."""
|
||||
status = {}
|
||||
|
||||
if config.gitea_url:
|
||||
try:
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
parsed_url = urllib.parse.urlparse(config.gitea_url)
|
||||
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
||||
|
||||
req = urllib.request.Request(base_url)
|
||||
req.add_header('User-Agent', 'tddai-config-check/1.0')
|
||||
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
status['gitea_connectivity'] = {
|
||||
'url': base_url,
|
||||
'status_code': response.getcode(),
|
||||
'reachable': True
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
status['gitea_connectivity'] = {
|
||||
'url': config.gitea_url,
|
||||
'reachable': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
return status
|
||||
28
issue_tracker/cli/core.py
Normal file
28
issue_tracker/cli/core.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
CLI framework core.
|
||||
|
||||
Provides the main CLI framework and command delegation.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from .commands import ConfigCommands
|
||||
|
||||
|
||||
class CLIFramework:
|
||||
"""Main CLI framework that delegates to command classes."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.config = ConfigCommands()
|
||||
|
||||
# Configuration operations
|
||||
def show_config(self, show_sensitive: bool = False) -> None:
|
||||
return self.config.show_config(show_sensitive)
|
||||
|
||||
def validate_config(self, verbose: bool = False) -> None:
|
||||
return self.config.validate_config(verbose)
|
||||
|
||||
def troubleshoot_config(self) -> None:
|
||||
return self.config.troubleshoot_config()
|
||||
|
||||
def check_config_files(self) -> None:
|
||||
return self.config.check_config_files()
|
||||
18
issue_tracker/cli/presenters/__init__.py
Normal file
18
issue_tracker/cli/presenters/__init__.py
Normal 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'
|
||||
]
|
||||
347
issue_tracker/cli/presenters/config.py
Normal file
347
issue_tracker/cli/presenters/config.py
Normal 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()
|
||||
85
issue_tracker/cli/presenters/formatters.py
Normal file
85
issue_tracker/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()
|
||||
325
issue_tracker/cli/presenters/views.py
Normal file
325
issue_tracker/cli/presenters/views.py
Normal 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)
|
||||
Reference in New Issue
Block a user