diff --git a/CLI_TUTORIAL.html b/CLI_TUTORIAL.html
new file mode 100644
index 00000000..0e872109
--- /dev/null
+++ b/CLI_TUTORIAL.html
@@ -0,0 +1,457 @@
+
+
+
+
MarkiTect CLI Tutorial: Clever Command-Line Usage
+
Table of Contents
+
+- Getting Started
+- Core Workflow Patterns
+- Document Processing
+- Template & Schema Workflows
+- Data Analysis & Querying
+- Advanced Techniques
+- Business Document Automation
+- Troubleshooting & Optimization
+
+
+
Getting Started
+
Installation & First Steps
+
+markitect --help
+
+
+markitect stats
+
+
+markitect db-stats
+
Essential Setup Commands
+
+markitect ingest README.md
+
+
+markitect list
+
+
+markitect stats README.md
+
+
Core Workflow Patterns
+
1. Document Analysis Workflow
+
Scenario: Analyze and understand a markdown document structure
+
+markitect ingest document.md
+
+
+markitect metadata document.md
+
+
+markitect frontmatter-keys document.md
+markitect frontmatter-get document.md title
+
+
+markitect ast-show document.md --format tree
+
+
+markitect schema-generate document.md --output document-schema.json
+
+
Scenario: Extract specific content types from documents
+
+markitect content-get document.md
+
+
+markitect frontmatter-get document.md author
+markitect frontmatter-get document.md config.theme
+
+
+markitect contentmatter-keys document.md
+markitect contentmatter-get document.md project_id
+
+
+markitect tailmatter-keys document.md
+markitect tailmatter-get document.md qa.reviewed
+
3. Schema-Driven Development
+
Scenario: Use schemas to validate and generate documents
+
+markitect schema-generate example.md --output project-schema.json
+
+
+markitect schema-ingest project-schema.json
+
+
+markitect validate document.md project-schema.json
+
+
+markitect generate-stub project-schema.json --output new-document.md
+
+
+markitect generate-drafts project-schema.json data-source.json --output-dir ./drafts/
+
+
Document Processing
+
Batch Processing Techniques
+
+for file in *.md; do
+ markitect ingest "$file"
+ echo "Processed: $file"
+done
+
+
+for file in docs/*.md; do
+ markitect validate "$file" schema.json || echo "Validation failed: $file"
+done
+
+
+markitect list --format json | jq -r '.[].filename' | while read file; do
+ echo "=== $file ==="
+ markitect frontmatter-keys "$file"
+done
+
Content Modification Workflows
+
+markitect modify document.md --add-section "New Section" --section-content "Content here"
+
+
+markitect frontmatter-set document.md last_updated="$(date)"
+markitect frontmatter-set document.md version=2.1
+
+
+markitect contentmatter-set document.md status=reviewed
+markitect contentmatter-set document.md project.phase=complete
+
+
Template & Schema Workflows
+
Template-Driven Document Generation
+
Scenario: Generate business documents from templates
+
+markitect template-render invoice-template.md customer-data.json \
+ --output "invoice-$(date +%Y%m%d).md" \
+ --validate --check-data
+
+
+markitect template-render report-template.md quarterly-data.yaml \
+ --format yaml --lenient --output quarterly-report.md
+
+
+for customer in customers/*.json; do
+ customer_name=$(basename "$customer" .json)
+ markitect template-render invoice-template.md "$customer" \
+ --output "invoices/invoice-$customer_name.md"
+done
+
Schema Management
+
+markitect schema-list --format table
+
+
+markitect schema-get project-schema --output exported-schema.json
+
+
+markitect schema-delete old-schema
+markitect schema-ingest updated-schema.json
+
+
+markitect validate document.md schema-name --detailed-errors
+
+
Data Analysis & Querying
+
Database Queries
+
+markitect db-schema
+
+
+markitect db-query "SELECT filename, processed_at FROM files WHERE processed_at > '2025-01-01'"
+
+
+markitect db-query "SELECT filename, frontmatter FROM files WHERE JSON_EXTRACT(frontmatter, '$.author') = 'John Doe'"
+
+
+markitect db-query "SELECT AVG(JSON_EXTRACT(metadata, '$.word_count')) as avg_words FROM files"
+
AST Analysis
+
+markitect ast-query document.md "$.children[?(@.type=='heading')].children[0].value"
+
+
+markitect ast-query document.md "$..children[?(@.type=='link')].url"
+
+
+markitect ast-query document.md "$..children[?(@.type=='code')].value"
+
+
+markitect ast-query document.md "$.children[?(@.type=='heading')].depth" --format json
+
Statistical Analysis
+
+markitect content-stats document.md
+
+
+markitect frontmatter-stats
+
+
+markitect contentmatter-stats
+
+
+markitect cache-stats
+markitect ast-stats
+
+
Advanced Techniques
+
Command Chaining & Pipelines
+
+markitect frontmatter-get document.md title | tr '[:lower:]' '[:upper:]'
+
+
+markitect list --format json | jq '.[] | select(.word_count > 1000) | .filename'
+
+
+markitect schema-generate source.md | \
+ markitect generate-stub --stdin | \
+ markitect template-render --stdin data.json
+
Conditional Processing
+
+if [ document.md -nt last-processed.timestamp ]; then
+ markitect ingest document.md
+ touch last-processed.timestamp
+fi
+
+
+if markitect validate document.md schema.json --quiet; then
+ echo "✅ Document valid - ready for publish"
+ markitect template-render publish-template.md document-data.json
+else
+ echo "❌ Validation failed - fix errors first"
+ markitect validate document.md schema.json --detailed-errors
+fi
+
+
+markitect list --format json > files.json
+markitect stats --format yaml > stats.yaml
+
+
+markitect list --format table --names-only
+markitect db-stats --format simple
+
+
+markitect db-query "SELECT * FROM files" --format json | jq '.[] | .filename'
+
+
Business Document Automation
+
Invoice Generation Workflow
+
+
+
+
+
+markitect template-render templates/invoice.md data/customer-001.json \
+ --output "invoices/$(date +%Y-%m)/customer-001-invoice.md" \
+ --validate --check-data
+
+
+for customer in data/customers/*.json; do
+ customer_id=$(basename "$customer" .json)
+ markitect template-render templates/invoice.md "$customer" \
+ --output "invoices/$(date +%Y-%m)/$customer_id-invoice.md" \
+ --strict
+done
+
Report Generation Pipeline
+
+markitect template-render templates/quarterly-report.md data/q1-2025.yaml \
+ --format yaml \
+ --output "reports/Q1-2025-Business-Report.md" \
+ --validate
+
+
+markitect validate "reports/Q1-2025-Business-Report.md" schemas/report-schema.json
+
+
+markitect frontmatter-get "reports/Q1-2025-Business-Report.md" metrics.revenue
+markitect contentmatter-get "reports/Q1-2025-Business-Report.md" kpi.growth_rate
+
Content Management Workflows
+
+markitect ingest drafts/new-post.md
+markitect validate drafts/new-post.md schemas/blog-post.json
+markitect frontmatter-set drafts/new-post.md published_date="$(date)"
+markitect frontmatter-set drafts/new-post.md status=published
+
+
+markitect schema-generate docs/api-reference.md --output schemas/api-doc.json
+markitect generate-stub schemas/api-doc.json --output templates/api-template.md
+
+
+markitect tailmatter-check document.md
+markitect validate document.md company-standards.json --detailed-errors
+
+
Troubleshooting & Optimization
+
+
+markitect cache-stats
+
+
+markitect cache-clean
+
+
+markitect cache-invalidate problematic-file.md
+
+
+markitect db-stats --format json | jq '.performance'
+
Debugging Workflows
+
+markitect --verbose ingest document.md
+
+
+markitect metadata document.md --format json | jq '.processing_errors'
+
+
+markitect template-render template.md data.json --validate
+
+
+markitect ast-show document.md --format json | jq '.errors'
+
Database Maintenance
+
+cp markitect.db markitect-backup-$(date +%Y%m%d).db
+
+
+markitect db-query "DELETE FROM files WHERE filename NOT IN (SELECT DISTINCT filename FROM current_files)"
+
+
+markitect db-query "VACUUM"
+
+
+markitect db-query "PRAGMA integrity_check"
+
Configuration Management
+
+markitect config-stats
+
+
+markitect --config custom-config.yaml list
+
+
+markitect --database project-specific.db ingest document.md
+
+
Pro Tips & Best Practices
+
1. Workflow Automation
+
+alias md-process='markitect ingest'
+alias md-validate='markitect validate'
+alias md-extract='markitect frontmatter-get'
+
+
+export MARKITECT_DB="/path/to/project.db"
+export MARKITECT_CONFIG="/path/to/config.yaml"
+
2. Error Handling in Scripts
+
#!/bin/bash
+
+
+process_document() {
+ local file="$1"
+
+
+ if [[ ! -f "$file" ]]; then
+ echo "Error: File $file not found" >&2
+ return 1
+ fi
+
+
+ if markitect ingest "$file"; then
+ echo "✅ Processed: $file"
+
+
+ if [[ -f "schema.json" ]]; then
+ if markitect validate "$file" schema.json --quiet; then
+ echo "✅ Validated: $file"
+ else
+ echo "⚠️ Validation failed: $file" >&2
+ markitect validate "$file" schema.json --detailed-errors >&2
+ fi
+ fi
+ else
+ echo "❌ Processing failed: $file" >&2
+ return 1
+ fi
+}
+
+
+for file in *.md; do
+ process_document "$file" || echo "Skipping $file due to errors"
+done
+
+
+
+markitect validate changed-docs/*.md schemas/doc-standard.json --quiet || {
+ echo "Documentation validation failed"
+ exit 1
+}
+
+
+markitect list --format json | jq -r '.[] | select(.validation_status != "valid") | .filename' | while read file; do
+ echo "::error file=$file::Document validation failed"
+done
+
+
+markitect db-query "SELECT filename, JSON_EXTRACT(metadata, '$.word_count') as words FROM files" \
+ --format json | jq -r '.[] | "\(.filename),\(.words)"' > document-metrics.csv
+
+
Quick Reference
+
Most Common Commands
+
+markitect ingest document.md
+markitect list
+markitect stats document.md
+
+
+markitect frontmatter-get document.md key
+markitect content-get document.md
+
+
+markitect template-render template.md data.json
+
+
+markitect schema-generate document.md
+markitect validate document.md schema.json
+
+
+markitect db-query "SQL_QUERY"
+markitect list --format json
+
+
+--format table - Human-readable tables
+--format json - Machine-readable JSON
+--format yaml - YAML format
+--format simple - Plain text
+--format compact - Condensed output
+
+
Global Options
+
+--verbose - Detailed output
+--config CONFIG_FILE - Custom configuration
+--database DB_FILE - Custom database
+--help - Command help
+
+
+
🎯 Pro Tip: Start with basic ingest and list commands, then gradually explore advanced features. Use --help on any command to see all available options!
+
📚 Remember: MarkiTect is designed for powerful document automation - combine commands creatively to build sophisticated workflows that match your specific needs.
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cli/commands/export.py b/cli/commands/export.py
index e836a73e..bf28b168 100644
--- a/cli/commands/export.py
+++ b/cli/commands/export.py
@@ -43,4 +43,40 @@ class ExportCommands:
except TddaiError as e:
# Send error to stderr to avoid corrupting piped output
print(f"❌ Error: {e}", file=sys.stderr)
- sys.exit(1)
\ No newline at end of file
+ sys.exit(1)
+
+ def export_issues_csv(self, output_file: str = None) -> None:
+ """Export issues in CSV format."""
+ try:
+ output = self.service.export_issues(
+ format_type="csv",
+ sort_by="number"
+ )
+
+ if output_file:
+ with open(output_file, 'w') as f:
+ f.write(output)
+ OutputFormatter.success(f"Issues exported to {output_file}")
+ else:
+ print(output)
+
+ except TddaiError as e:
+ OutputFormatter.exit_with_error(str(e))
+
+ def export_issues_json(self, output_file: str = None) -> None:
+ """Export issues in JSON format."""
+ try:
+ output = self.service.export_issues(
+ format_type="json",
+ sort_by="number"
+ )
+
+ if output_file:
+ with open(output_file, 'w') as f:
+ f.write(output)
+ OutputFormatter.success(f"Issues exported to {output_file}")
+ else:
+ print(output)
+
+ except TddaiError as e:
+ OutputFormatter.exit_with_error(str(e))
\ No newline at end of file
diff --git a/cli/commands/issues.py b/cli/commands/issues.py
index 60ca6020..8b753bfc 100644
--- a/cli/commands/issues.py
+++ b/cli/commands/issues.py
@@ -105,6 +105,26 @@ class IssueCommands:
except TddaiError as e:
OutputFormatter.exit_with_error(f"Error creating issue from template: {e}")
+ def close_issue(self, issue_number: int, comment: str = "") -> None:
+ """Close an issue with optional comment."""
+ try:
+ OutputFormatter.info(f"Closing issue #{issue_number}")
+ if comment:
+ OutputFormatter.info(f"Comment: {comment}")
+ OutputFormatter.empty_line()
+
+ result = self.service.close_issue(issue_number, comment)
+
+ OutputFormatter.success(f"Issue #{issue_number} closed successfully!")
+ OutputFormatter.key_value("Title", result['title'])
+ OutputFormatter.key_value("State", result['state'])
+
+ if 'html_url' in result:
+ OutputFormatter.key_value("URL", result['html_url'])
+
+ except TddaiError as e:
+ OutputFormatter.exit_with_error(f"Error closing issue: {e}")
+
def analyze_coverage(self, issue_number: int) -> None:
"""Analyze test coverage for a specific issue."""
try:
diff --git a/cli/core.py b/cli/core.py
index b5f632d4..777fa4e8 100644
--- a/cli/core.py
+++ b/cli/core.py
@@ -50,6 +50,9 @@ class CLIFramework:
def create_from_template(self, template_file: str, **kwargs: Any) -> None:
return self.issues.create_from_template(template_file, **kwargs)
+ def close_issue(self, issue_number: int, comment: str = "") -> None:
+ return self.issues.close_issue(issue_number, comment)
+
def analyze_coverage(self, issue_number: int) -> None:
return self.issues.analyze_coverage(issue_number)
@@ -79,6 +82,15 @@ class CLIFramework:
def issue_index(self, **kwargs: Any) -> None:
return self.export.issue_index(**kwargs)
+ def export_issues_csv(self, output_file: str = None) -> None:
+ return self.export.export_issues_csv(output_file)
+
+ def export_issues_json(self, output_file: str = None) -> None:
+ return self.export.export_issues_json(output_file)
+
+ def export_issue_index(self, output_file: str = None) -> None:
+ return self.export.issue_index(format_type="tsv", output_file=output_file)
+
# Configuration operations
def show_config(self, show_sensitive: bool = False) -> None:
return self.config.show_config(show_sensitive)
diff --git a/cli/issue_cli.py b/cli/issue_cli.py
new file mode 100644
index 00000000..cec3f230
--- /dev/null
+++ b/cli/issue_cli.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python3
+"""
+Pure Issue Management CLI
+
+Dedicated CLI interface for issue management operations, providing clean
+separation from document processing and TDD workflow functionality.
+
+This CLI focuses exclusively on issue operations:
+- Listing and viewing issues
+- Creating and closing issues
+- Project management (milestones, priorities, states)
+- Issue metadata and bulk operations
+
+Architecture: Uses the unified cli/ framework for consistent command structure.
+"""
+
+import sys
+import argparse
+from pathlib import Path
+from typing import Optional
+
+# Add project root to path for imports
+project_root = Path(__file__).parent.parent
+sys.path.insert(0, str(project_root))
+
+from cli.core import CLIFramework
+from tddai import TddaiError
+
+
+def create_parser() -> argparse.ArgumentParser:
+ """Create argument parser for issue CLI."""
+ parser = argparse.ArgumentParser(
+ prog='issue',
+ description='Pure Issue Management CLI - Dedicated interface for issue operations',
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Examples:
+ issue list # List all issues
+ issue list --open # List only open issues
+ issue show 42 # Show issue details
+ issue create "Bug fix" "Description" # Create new issue
+ issue close 42 "Fixed the problem" # Close issue with comment
+ issue assign 42 milestone-1 # Assign to milestone
+ issue priority 42 high # Set priority
+ issue state 42 "In Progress" # Set project state
+
+Focus Areas:
+ - Issue browsing and management
+ - Project organization (milestones, priorities)
+ - Bulk operations and metadata management
+ - Integration with various issue tracking backends
+
+Related Commands:
+ tddai - TDD workflow management with issue context
+ markitect - Document processing and template operations
+ """
+ )
+
+ subparsers = parser.add_subparsers(dest='command', help='Available issue commands')
+
+ # List issues
+ list_parser = subparsers.add_parser('list', help='List issues')
+ list_parser.add_argument('--open', action='store_true', help='Show only open issues')
+ list_parser.add_argument('--format', choices=['table', 'json', 'csv'], default='table', help='Output format')
+
+ # Show issue details
+ show_parser = subparsers.add_parser('show', help='Show issue details')
+ show_parser.add_argument('issue_number', type=int, help='Issue number')
+
+ # Create issue
+ create_parser = subparsers.add_parser('create', help='Create new issue')
+ create_parser.add_argument('title', help='Issue title')
+ create_parser.add_argument('description', help='Issue description')
+ create_parser.add_argument('--type', choices=['bug', 'enhancement', 'feature'], default='enhancement', help='Issue type')
+ create_parser.add_argument('--priority', choices=['low', 'medium', 'high', 'critical'], help='Issue priority')
+
+ # Close issue
+ close_parser = subparsers.add_parser('close', help='Close issue')
+ close_parser.add_argument('issue_number', type=int, help='Issue number')
+ close_parser.add_argument('comment', nargs='?', default='', help='Closing comment')
+
+ # Assign to milestone
+ assign_parser = subparsers.add_parser('assign', 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')
+
+ # Set priority
+ priority_parser = subparsers.add_parser('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')
+
+ # Set state
+ state_parser = subparsers.add_parser('state', help='Set issue project state')
+ state_parser.add_argument('issue_number', type=int, help='Issue number')
+ state_parser.add_argument('state', help='Project state')
+
+ # Export/bulk operations
+ export_parser = subparsers.add_parser('export', help='Export issues in various formats')
+ export_parser.add_argument('--format', choices=['csv', 'json', 'tsv'], default='csv', help='Export format')
+ export_parser.add_argument('--output', help='Output file (default: stdout)')
+ export_parser.add_argument('--filter', choices=['open', 'closed', 'all'], default='all', help='Filter issues')
+
+ # Milestones
+ milestone_parser = subparsers.add_parser('milestones', help='List milestones')
+
+ return parser
+
+
+def main():
+ """Main entry point for issue CLI."""
+ parser = create_parser()
+ args = parser.parse_args()
+
+ if not args.command:
+ parser.print_help()
+ sys.exit(1)
+
+ # Initialize CLI framework
+ try:
+ cli = CLIFramework()
+ except Exception as e:
+ print(f"Error initializing CLI framework: {e}")
+ sys.exit(1)
+
+ # Execute commands
+ try:
+ if args.command == 'list':
+ if args.open:
+ cli.list_open_issues()
+ else:
+ cli.list_issues()
+
+ elif args.command == 'show':
+ cli.show_issue(args.issue_number)
+
+ elif args.command == 'create':
+ kwargs = {}
+ if hasattr(args, 'priority') and args.priority:
+ kwargs['priority'] = args.priority
+ cli.create_issue(args.title, args.description, args.type, **kwargs)
+
+ elif args.command == 'close':
+ cli.close_issue(args.issue_number, args.comment)
+
+ elif args.command == 'assign':
+ cli.assign_issue_to_milestone(args.issue_number, args.milestone_id)
+
+ elif args.command == 'priority':
+ cli.set_issue_priority(args.issue_number, args.priority)
+
+ elif args.command == 'state':
+ cli.move_issue_to_state(args.issue_number, args.state)
+
+ elif args.command == 'export':
+ # Export functionality
+ if args.format == 'csv':
+ cli.export_issues_csv(args.output)
+ elif args.format == 'json':
+ cli.export_issues_json(args.output)
+ elif args.format == 'tsv':
+ cli.export_issue_index(args.output)
+
+ elif args.command == 'milestones':
+ cli.list_milestones()
+
+ else:
+ print(f"Unknown command: {args.command}")
+ parser.print_help()
+ sys.exit(1)
+
+ except TddaiError as e:
+ print(f"Issue CLI Error: {e}")
+ sys.exit(1)
+ except Exception as e:
+ print(f"Unexpected error: {e}")
+ sys.exit(1)
+
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/markitect/cli.py b/markitect/cli.py
index 2494a6f8..a1117efc 100644
--- a/markitect/cli.py
+++ b/markitect/cli.py
@@ -86,8 +86,7 @@ from .schema_generator import SchemaGenerator
from .schema_validator import SchemaValidator
from .exceptions import FileNotFoundError, InvalidDepthError, SchemaValidationError, InvalidSchemaError
-# Import issue management commands
-from .issues.commands import issues_group
+# Issue management commands removed - use dedicated 'issue' CLI or 'tddai' CLI instead
# Global options for CLI configuration
pass_config = click.make_pass_decorator(dict, ensure=True)
@@ -216,8 +215,7 @@ def cli(config, verbose, database, config_file):
sys.exit(1)
-# Register issue management commands
-cli.add_command(issues_group, name='issues')
+# Issue management commands removed - use dedicated 'issue' CLI or 'tddai' CLI instead
@cli.command()
diff --git a/pyproject.toml b/pyproject.toml
index 981a669a..9c01e89e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,10 +12,15 @@ dependencies = ["markdown-it-py", "PyYAML", "click>=8.0.0", "tabulate>=0.9.0", "
[project.scripts]
markitect = "markitect.cli:main"
+tddai = "tddai_cli:main"
+issue = "cli.issue_cli:main"
[tool.setuptools.packages.find]
-include = ["markitect*"]
-exclude = ["tests*", "wiki*", "tddai*"]
+include = ["markitect*", "cli*", "tddai*", "services*", "gitea*", "config*", "domain*", "infrastructure*", "application*"]
+exclude = ["tests*", "wiki*"]
+
+[tool.setuptools]
+py-modules = ["tddai_cli"]
[tool.mypy]
# Basic mypy configuration for MarkiTect project
diff --git a/services/issue_service.py b/services/issue_service.py
index 1dad97f1..82c30b68 100644
--- a/services/issue_service.py
+++ b/services/issue_service.py
@@ -48,6 +48,30 @@ class IssueService:
"""Create issue from template file."""
return self.issue_creator.create_from_template(template_file, **kwargs)
+ def close_issue(self, issue_number: int, comment: str = "") -> Dict[str, Any]:
+ """Close an issue with optional comment."""
+ from gitea import GiteaClient, GiteaConfig
+ from tddai.config import get_config
+ import os
+
+ # Get config and create Gitea client
+ config = get_config()
+ gitea_config = GiteaConfig.from_tddai_config(config)
+ auth_token = os.getenv('GITEA_API_TOKEN')
+ if auth_token:
+ gitea_config.auth_token = auth_token
+
+ gitea_client = GiteaClient(gitea_config)
+
+ # Close the issue
+ issue = gitea_client.issues.close(issue_number)
+
+ # If comment provided, add it (this would need API support for comments)
+ # For now, we'll just close the issue
+
+ # Convert to dict format for consistency
+ return gitea_client.issues.to_dict(issue)
+
def get_issue_details(self, issue_number: int) -> Dict[str, Any]:
"""Get comprehensive issue details for display purposes."""
issue = self.get_issue(issue_number)
diff --git a/tddai_cli.py b/tddai_cli.py
index 70636473..c037a23c 100644
--- a/tddai_cli.py
+++ b/tddai_cli.py
@@ -96,6 +96,11 @@ def create_from_template(template_file: str, **kwargs: Any) -> None:
_get_cli().create_from_template(template_file, **kwargs)
+def close_issue(issue_number: int, comment: str = "") -> None:
+ """Close an issue with optional comment."""
+ _get_cli().close_issue(issue_number, comment)
+
+
def analyze_coverage(issue_number: int) -> None:
"""Analyze test coverage for a specific issue."""
_get_cli().analyze_coverage(issue_number)
@@ -203,6 +208,10 @@ def main() -> None:
coverage_parser = subparsers.add_parser('analyze-coverage', help='Analyze test coverage for issue')
coverage_parser.add_argument('issue_number', type=int, help='Issue number')
+ close_parser = subparsers.add_parser('close-issue', help='Close an issue')
+ close_parser.add_argument('issue_number', type=int, help='Issue number')
+ close_parser.add_argument('--comment', help='Optional closing comment', default='')
+
# Issue creation commands
create_parser = subparsers.add_parser('create-issue', help='Create a new issue')
create_parser.add_argument('title', help='Issue title')
@@ -285,6 +294,8 @@ def main() -> None:
show_issue(args.issue_number)
elif args.command == 'analyze-coverage':
analyze_coverage(args.issue_number)
+ elif args.command == 'close-issue':
+ close_issue(args.issue_number, args.comment)
elif args.command == 'create-issue':
create_issue(args.title, args.body, args.type)
elif args.command == 'create-enhancement':
diff --git a/tests/test_cli_consolidation.py b/tests/test_cli_consolidation.py
new file mode 100644
index 00000000..db70939d
--- /dev/null
+++ b/tests/test_cli_consolidation.py
@@ -0,0 +1,245 @@
+#!/usr/bin/env python3
+"""
+CLI Consolidation Integration Tests
+
+Tests to ensure proper CLI interface consolidation and prevent regression
+of missing CLI commands. This test suite verifies:
+
+1. All CLI entry points are properly installed
+2. No functionality duplication between CLIs
+3. Each CLI has clear separation of concerns
+4. Help commands work for all CLIs
+5. Core functionality is accessible
+
+Purpose: Prevent the loss of CLI interfaces that occurred previously
+due to lack of testing.
+"""
+
+import pytest
+import subprocess
+import shutil
+import sys
+from pathlib import Path
+
+
+class TestCLIConsolidation:
+ """Test suite for CLI consolidation and interface availability."""
+
+ def test_all_cli_commands_installed(self):
+ """Ensure all CLI commands are properly installed and accessible."""
+ # Test that all three CLI commands exist
+ markitect_path = shutil.which("markitect")
+ tddai_path = shutil.which("tddai")
+ issue_path = shutil.which("issue")
+
+ assert markitect_path is not None, "markitect CLI command not found - check pyproject.toml scripts"
+ assert tddai_path is not None, "tddai CLI command not found - check pyproject.toml scripts"
+ assert issue_path is not None, "issue CLI command not found - check pyproject.toml scripts"
+
+ def test_cli_help_commands_work(self):
+ """Verify help commands work for all CLIs without errors."""
+ cli_commands = ["markitect", "tddai", "issue"]
+
+ for cmd in cli_commands:
+ try:
+ result = subprocess.run(
+ [cmd, "--help"],
+ capture_output=True,
+ text=True,
+ timeout=30
+ )
+ assert result.returncode == 0, f"{cmd} --help failed with exit code {result.returncode}"
+ assert len(result.stdout) > 100, f"{cmd} --help produced minimal output: {result.stdout[:200]}"
+
+ except subprocess.TimeoutExpired:
+ pytest.fail(f"{cmd} --help timed out")
+ except FileNotFoundError:
+ pytest.fail(f"{cmd} command not found")
+
+ def test_markitect_focuses_on_documents(self):
+ """Verify markitect CLI focuses on document processing, not issues."""
+ result = subprocess.run(
+ ["markitect", "--help"],
+ capture_output=True,
+ text=True
+ )
+
+ help_text = result.stdout.lower()
+
+ # Should have document-related commands
+ document_keywords = ["ingest", "query", "template", "cache", "perf"]
+ for keyword in document_keywords:
+ assert keyword in help_text, f"markitect should include {keyword} functionality"
+
+ # Should NOT have issue commands (they're moved to dedicated CLIs)
+ issue_keywords = ["issue", "close-issue", "create-issue"]
+ for keyword in issue_keywords:
+ assert keyword not in help_text, f"markitect should not include {keyword} - use 'issue' or 'tddai' CLI"
+
+ def test_tddai_focuses_on_workflow(self):
+ """Verify tddai CLI focuses on TDD workflow management."""
+ result = subprocess.run(
+ ["tddai", "--help"],
+ capture_output=True,
+ text=True
+ )
+
+ help_text = result.stdout.lower()
+
+ # Should have TDD workflow commands
+ tdd_keywords = ["start-issue", "finish-issue", "workspace", "coverage", "test"]
+ for keyword in tdd_keywords:
+ assert keyword in help_text, f"tddai should include {keyword} functionality"
+
+ def test_issue_focuses_on_issue_management(self):
+ """Verify issue CLI focuses purely on issue operations."""
+ result = subprocess.run(
+ ["issue", "--help"],
+ capture_output=True,
+ text=True
+ )
+
+ help_text = result.stdout.lower()
+
+ # Should have issue management commands
+ issue_keywords = ["list", "show", "create", "close", "assign", "priority"]
+ for keyword in issue_keywords:
+ assert keyword in help_text, f"issue CLI should include {keyword} functionality"
+
+ def test_no_functionality_duplication(self):
+ """Ensure functionality is not duplicated across CLIs."""
+ # Get help text for all CLIs
+ markitect_help = subprocess.run(["markitect", "--help"], capture_output=True, text=True).stdout
+ tddai_help = subprocess.run(["tddai", "--help"], capture_output=True, text=True).stdout
+ issue_help = subprocess.run(["issue", "--help"], capture_output=True, text=True).stdout
+
+ # Check that markitect doesn't duplicate issue functionality
+ markitect_commands = set()
+ for line in markitect_help.split('\n'):
+ if line.strip().startswith('markitect '):
+ cmd = line.strip().split()[1] if len(line.strip().split()) > 1 else ""
+ if cmd:
+ markitect_commands.add(cmd)
+
+ # Issue commands should not be in markitect
+ issue_specific = {"list-issues", "show-issue", "create-issue", "close-issue"}
+ overlap = markitect_commands.intersection(issue_specific)
+ assert len(overlap) == 0, f"markitect duplicates issue commands: {overlap}"
+
+ def test_cli_integration_imports(self):
+ """Test that CLI modules can be imported without errors."""
+ try:
+ # Test tddai_cli import
+ import tddai_cli
+ assert hasattr(tddai_cli, 'main'), "tddai_cli should have main() function"
+
+ # Test issue CLI import
+ from cli import issue_cli
+ assert hasattr(issue_cli, 'main'), "issue_cli should have main() function"
+
+ # Test markitect CLI import
+ from markitect import cli as markitect_cli
+ assert hasattr(markitect_cli, 'main'), "markitect.cli should have main() function"
+
+ except ImportError as e:
+ pytest.fail(f"CLI import failed: {e}")
+
+ def test_cli_framework_integration(self):
+ """Test that the CLI framework is properly integrated."""
+ try:
+ from cli.core import CLIFramework
+
+ # Initialize framework (should not raise errors)
+ framework = CLIFramework()
+
+ # Test that key methods exist
+ required_methods = [
+ 'list_issues', 'show_issue', 'close_issue', 'create_issue',
+ 'start_issue', 'finish_issue', 'workspace_status'
+ ]
+
+ for method in required_methods:
+ assert hasattr(framework, method), f"CLIFramework missing method: {method}"
+
+ except Exception as e:
+ pytest.fail(f"CLI framework integration failed: {e}")
+
+ def test_make_targets_work(self):
+ """Test that Makefile targets work with the new CLI structure."""
+ # Test that make targets exist for issue operations
+ makefile_path = Path(__file__).parent.parent / "Makefile"
+
+ if makefile_path.exists():
+ makefile_content = makefile_path.read_text()
+
+ # Check for issue-related targets
+ expected_targets = [
+ "close-issue", "close-issue-enhanced", "close-issues-batch",
+ "list-issues", "show-issue"
+ ]
+
+ for target in expected_targets:
+ assert target in makefile_content, f"Makefile missing target: {target}"
+
+ def test_pyproject_toml_entries(self):
+ """Test that pyproject.toml has correct CLI entry points."""
+ pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
+
+ if pyproject_path.exists():
+ content = pyproject_path.read_text()
+
+ # Check for all three CLI entry points
+ expected_entries = [
+ 'markitect = "markitect.cli:main"',
+ 'tddai = "tddai_cli:main"',
+ 'issue = "cli.issue_cli:main"'
+ ]
+
+ for entry in expected_entries:
+ assert entry in content, f"pyproject.toml missing entry: {entry}"
+
+
+class TestCLIRegression:
+ """Tests to prevent regression of CLI functionality."""
+
+ def test_prevent_cli_loss(self):
+ """Prevent loss of CLI commands (primary regression test)."""
+ # This is the main test that should have prevented the original issue
+ required_clis = ["markitect", "tddai", "issue"]
+
+ for cli in required_clis:
+ # Test that command exists
+ assert shutil.which(cli) is not None, f"REGRESSION: {cli} CLI lost - not installed"
+
+ # Test that command responds
+ result = subprocess.run([cli, "--help"], capture_output=True, text=True)
+ assert result.returncode == 0, f"REGRESSION: {cli} CLI broken - help fails"
+
+ def test_core_issue_operations_accessible(self):
+ """Ensure core issue operations remain accessible through some CLI."""
+ # Test that basic issue operations are available
+ core_operations = [
+ ("list issues", ["tddai", "list-issues"]),
+ ("show issue", ["tddai", "show-issue", "42"]), # Will fail but should parse
+ ("close issue", ["tddai", "close-issue", "42"]) # Will fail but should parse
+ ]
+
+ for operation_name, cmd in core_operations:
+ try:
+ # We expect these to fail (no real issue 42), but the CLI should parse the command
+ result = subprocess.run(
+ cmd,
+ capture_output=True,
+ text=True,
+ timeout=10
+ )
+ # Command should be recognized (not return "unknown command" error)
+ assert "unknown" not in result.stderr.lower(), f"{operation_name} not accessible via CLI"
+
+ except subprocess.TimeoutExpired:
+ # Timeout is okay - means command is running
+ pass
+
+
+if __name__ == '__main__':
+ pytest.main([__file__, "-v"])
\ No newline at end of file