#!/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 ) from tddai.issue_creator import IssueCreator from tddai.project_manager import ProjectManager, ProjectState, Priority 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("๐Ÿ“‹ 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) 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) 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 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) 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 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) 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() 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 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) 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) 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) 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) 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) 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 project_overview(): """Show project management overview.""" try: project_mgr = ProjectManager() print("๐Ÿ“Š Project Management Overview") print("==============================") print() 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 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') # Issue creation commands create_parser = subparsers.add_parser('create-issue', help='Create a new issue') create_parser.add_argument('title', help='Issue title') create_parser.add_argument('body', help='Issue body/description') create_parser.add_argument('--type', choices=['enhancement', 'bug'], default='enhancement', help='Issue type') create_enh_parser = subparsers.add_parser('create-enhancement', help='Create a structured enhancement issue') create_enh_parser.add_argument('title', help='Issue title') create_enh_parser.add_argument('use_case', help='UseCase description') create_enh_parser.add_argument('--technical', help='Technical requirements', default='') create_enh_parser.add_argument('--criteria', help='Acceptance criteria (newline separated)', default='') create_enh_parser.add_argument('--dependencies', help='Dependencies (newline separated)', default='') create_enh_parser.add_argument('--priority', choices=['High', 'Medium', 'Low'], default='Medium', help='Priority level') template_parser = subparsers.add_parser('create-from-template', help='Create issue from template') template_parser.add_argument('template_file', help='Template file path') template_parser.add_argument('--vars', help='Template variables in key=value format', nargs='*', default=[]) # Project management commands subparsers.add_parser('setup-project-mgmt', help='Setup project management labels and milestones') subparsers.add_parser('project-overview', help='Show project management overview') state_parser = subparsers.add_parser('set-issue-state', help='Set issue project state') state_parser.add_argument('issue_number', type=int, help='Issue number') state_parser.add_argument('state', choices=['todo', 'active', 'review', 'done', 'blocked'], help='Project state') priority_parser = subparsers.add_parser('set-issue-priority', help='Set issue priority') priority_parser.add_argument('issue_number', type=int, help='Issue number') priority_parser.add_argument('priority', choices=['low', 'medium', 'high', 'critical'], help='Priority level') milestone_parser = subparsers.add_parser('create-milestone', help='Create a new milestone (project)') milestone_parser.add_argument('title', help='Milestone title') milestone_parser.add_argument('--description', help='Milestone description', default='') subparsers.add_parser('list-milestones', help='List all milestones') assign_parser = subparsers.add_parser('assign-to-milestone', help='Assign issue to milestone') assign_parser.add_argument('issue_number', type=int, help='Issue number') assign_parser.add_argument('milestone_id', type=int, help='Milestone ID') 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) elif args.command == 'create-issue': create_issue(args.title, args.body, args.type) elif args.command == 'create-enhancement': create_enhancement_issue( args.title, args.use_case, args.technical, args.criteria, args.dependencies, args.priority ) elif args.command == 'create-from-template': # Parse template variables template_vars = {} for var in args.vars: if '=' in var: key, value = var.split('=', 1) template_vars[key] = value create_from_template(args.template_file, **template_vars) elif args.command == 'setup-project-mgmt': setup_project_management() elif args.command == 'project-overview': project_overview() elif args.command == 'set-issue-state': move_issue_to_state(args.issue_number, args.state) elif args.command == 'set-issue-priority': set_issue_priority(args.issue_number, args.priority) elif args.command == 'create-milestone': create_milestone(args.title, args.description) elif args.command == 'list-milestones': list_milestones() elif args.command == 'assign-to-milestone': assign_issue_to_milestone(args.issue_number, args.milestone_id) except KeyboardInterrupt: print("\nโš ๏ธ Operation cancelled") sys.exit(1) if __name__ == '__main__': main()