#!/usr/bin/env python3 """ CLI interface for tddai library. """ import sys import argparse from pathlib import Path # Add current directory to path so we can import tddai sys.path.insert(0, str(Path(__file__).parent)) from tddai import ( WorkspaceManager, IssueFetcher, TestGenerator, CoverageAnalyzer, WorkspaceStatus, TddaiError ) 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) 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) 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) 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}_.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) def list_issues(): """List all issues.""" try: fetcher = IssueFetcher() print("๐Ÿ“‹ MarkiTect 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) def list_open_issues(): """List only open issues.""" try: fetcher = IssueFetcher() print("๐Ÿ“‹ Open MarkiTect 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) def show_issue(issue_number: int): """Show detailed issue information.""" try: fetcher = IssueFetcher() print(f"๐Ÿ” Issue #{issue_number} Details") print("=======================") print() 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}") if issue.labels: print(f"**Labels:** {', '.join(issue.labels)}") 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) def main(): """Main CLI entry point.""" parser = argparse.ArgumentParser(description="tddai CLI tool") subparsers = parser.add_subparsers(dest='command', help='Available commands') # Workspace commands subparsers.add_parser('workspace-status', help='Show workspace status') start_parser = subparsers.add_parser('start-issue', help='Start working on issue') start_parser.add_argument('issue_number', type=int, help='Issue number') subparsers.add_parser('finish-issue', help='Finish current issue') subparsers.add_parser('add-test', help='Show guidance for adding tests') # Issue commands subparsers.add_parser('list-issues', help='List all issues') subparsers.add_parser('list-open-issues', help='List open issues') show_parser = subparsers.add_parser('show-issue', help='Show issue details') show_parser.add_argument('issue_number', type=int, help='Issue number') coverage_parser = subparsers.add_parser('analyze-coverage', help='Analyze test coverage for issue') coverage_parser.add_argument('issue_number', type=int, help='Issue number') args = parser.parse_args() if not args.command: parser.print_help() return try: if args.command == 'workspace-status': workspace_status() elif args.command == 'start-issue': start_issue(args.issue_number) elif args.command == 'finish-issue': finish_issue() elif args.command == 'add-test': add_test_guidance() elif args.command == 'list-issues': list_issues() elif args.command == 'list-open-issues': list_open_issues() elif args.command == 'show-issue': show_issue(args.issue_number) elif args.command == 'analyze-coverage': analyze_coverage(args.issue_number) except KeyboardInterrupt: print("\nโš ๏ธ Operation cancelled") sys.exit(1) if __name__ == '__main__': main()