feat: Add Unix-friendly issue index with multiple output formats
- Add issue-index command with TSV, CSV, JSON, and fields output formats - Support sorting by number, title, priority, state, created, updated - Add filtering by state (open/closed) and priority level - Include proper data cleaning for Unix pipeline processing - Add make targets: issues-get, issues-csv, issues-json, issues-high - Optimize for awk, cut, grep, and other Unix text processing tools 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
137
tddai_cli.py
137
tddai_cli.py
@@ -679,6 +679,123 @@ def assign_issue_to_milestone(issue_number: int, milestone_id: int):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def issue_index(format_type="tsv", sort_by="number", filter_state=None, filter_priority=None, include_state=False):
|
||||
"""Output compact index of all issues with ID, title, and priority for efficient parsing.
|
||||
|
||||
Args:
|
||||
format_type: Output format (tsv, csv, json, fields)
|
||||
sort_by: Sort by field (number, title, priority, state, created, updated)
|
||||
filter_state: Filter by state (open, closed, all) - None means all
|
||||
filter_priority: Filter by priority (low, medium, high, critical, none) - None means all
|
||||
include_state: Include state column in output
|
||||
"""
|
||||
try:
|
||||
fetcher = IssueFetcher()
|
||||
project_mgr = ProjectManager()
|
||||
from tddai.config import get_config
|
||||
import json
|
||||
|
||||
config = get_config()
|
||||
|
||||
issues = fetcher.fetch_issues()
|
||||
if not issues:
|
||||
return
|
||||
|
||||
# Collect full issue data with additional fields
|
||||
issue_data = []
|
||||
for issue in issues:
|
||||
# Get priority and state from labels
|
||||
priority = "none"
|
||||
state = "none"
|
||||
try:
|
||||
# Use ProjectManager's API call method
|
||||
issue_url = f"{config.issues_api_url}/{issue.number}"
|
||||
detailed_issue = project_mgr._make_api_call('GET', issue_url)
|
||||
labels = detailed_issue.get('labels', [])
|
||||
priority_labels = [l['name'] for l in labels if l['name'].startswith('priority:')]
|
||||
state_labels = [l['name'] for l in labels if l['name'].startswith('status:')]
|
||||
|
||||
if priority_labels:
|
||||
priority = priority_labels[0].replace('priority:', '')
|
||||
if state_labels:
|
||||
state = state_labels[0].replace('status:', '')
|
||||
except:
|
||||
pass # Keep defaults if API call fails
|
||||
|
||||
issue_info = {
|
||||
'number': issue.number,
|
||||
'title': issue.title.replace('\t', ' ').replace('\n', ' '), # Clean for TSV
|
||||
'priority': priority,
|
||||
'state': issue.state, # open/closed from basic data
|
||||
'status': state, # detailed status from labels
|
||||
'created': issue.created_at.strftime('%Y-%m-%d'),
|
||||
'updated': issue.updated_at.strftime('%Y-%m-%d')
|
||||
}
|
||||
issue_data.append(issue_info)
|
||||
|
||||
# Apply filters
|
||||
if filter_state:
|
||||
if filter_state == "open":
|
||||
issue_data = [i for i in issue_data if i['state'] == 'open']
|
||||
elif filter_state == "closed":
|
||||
issue_data = [i for i in issue_data if i['state'] == 'closed']
|
||||
|
||||
if filter_priority:
|
||||
issue_data = [i for i in issue_data if i['priority'] == filter_priority]
|
||||
|
||||
# Sort issues
|
||||
sort_key_map = {
|
||||
'number': lambda x: x['number'],
|
||||
'title': lambda x: x['title'].lower(),
|
||||
'priority': lambda x: {'critical': 4, 'high': 3, 'medium': 2, 'low': 1, 'none': 0}[x['priority']],
|
||||
'state': lambda x: x['state'],
|
||||
'created': lambda x: x['created'],
|
||||
'updated': lambda x: x['updated']
|
||||
}
|
||||
|
||||
if sort_by in sort_key_map:
|
||||
issue_data.sort(key=sort_key_map[sort_by], reverse=(sort_by in ['number', 'priority', 'created', 'updated']))
|
||||
|
||||
# Output in requested format
|
||||
if format_type == "json":
|
||||
print(json.dumps(issue_data, indent=2))
|
||||
elif format_type == "csv":
|
||||
# CSV header
|
||||
if include_state:
|
||||
print("number,title,priority,state,created,updated")
|
||||
for issue in issue_data:
|
||||
title = issue['title'].replace('"', '""') # Escape quotes
|
||||
print(f'{issue["number"]},"{title}",{issue["priority"]},{issue["state"]},{issue["created"]},{issue["updated"]}')
|
||||
else:
|
||||
print("number,title,priority,created,updated")
|
||||
for issue in issue_data:
|
||||
title = issue['title'].replace('"', '""')
|
||||
print(f'{issue["number"]},"{title}",{issue["priority"]},{issue["created"]},{issue["updated"]}')
|
||||
elif format_type == "fields":
|
||||
# Space-separated fields for easy awk processing
|
||||
if include_state:
|
||||
print("NUMBER TITLE PRIORITY STATE CREATED UPDATED")
|
||||
for issue in issue_data:
|
||||
title = issue['title'].replace(' ', '_')
|
||||
print(f'{issue["number"]} {title} {issue["priority"]} {issue["state"]} {issue["created"]} {issue["updated"]}')
|
||||
else:
|
||||
print("NUMBER TITLE PRIORITY CREATED UPDATED")
|
||||
for issue in issue_data:
|
||||
title = issue['title'].replace(' ', '_')
|
||||
print(f'{issue["number"]} {title} {issue["priority"]} {issue["created"]} {issue["updated"]}')
|
||||
else: # Default TSV
|
||||
if include_state:
|
||||
for issue in issue_data:
|
||||
print(f'{issue["number"]}\t{issue["title"]}\t{issue["priority"]}\t{issue["state"]}\t{issue["created"]}\t{issue["updated"]}')
|
||||
else:
|
||||
for issue in issue_data:
|
||||
print(f'{issue["number"]}\t{issue["title"]}\t{issue["priority"]}\t{issue["created"]}\t{issue["updated"]}')
|
||||
|
||||
except TddaiError as e:
|
||||
print(f"❌ Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def project_overview():
|
||||
"""Show project management overview."""
|
||||
try:
|
||||
@@ -718,6 +835,18 @@ def main():
|
||||
subparsers.add_parser('list-issues', help='List all issues')
|
||||
subparsers.add_parser('list-open-issues', help='List open issues')
|
||||
|
||||
index_parser = subparsers.add_parser('issue-index', help='Output compact issue index for Unix processing')
|
||||
index_parser.add_argument('--format', choices=['tsv', 'csv', 'json', 'fields'], default='tsv',
|
||||
help='Output format (default: tsv)')
|
||||
index_parser.add_argument('--sort', choices=['number', 'title', 'priority', 'state', 'created', 'updated'],
|
||||
default='number', help='Sort by field (default: number)')
|
||||
index_parser.add_argument('--filter-state', choices=['open', 'closed'],
|
||||
help='Filter by issue state')
|
||||
index_parser.add_argument('--filter-priority', choices=['low', 'medium', 'high', 'critical', 'none'],
|
||||
help='Filter by priority level')
|
||||
index_parser.add_argument('--include-state', action='store_true',
|
||||
help='Include state column in output')
|
||||
|
||||
show_parser = subparsers.add_parser('show-issue', help='Show issue details')
|
||||
show_parser.add_argument('issue_number', type=int, help='Issue number')
|
||||
|
||||
@@ -783,6 +912,14 @@ def main():
|
||||
list_issues()
|
||||
elif args.command == 'list-open-issues':
|
||||
list_open_issues()
|
||||
elif args.command == 'issue-index':
|
||||
issue_index(
|
||||
format_type=args.format,
|
||||
sort_by=args.sort,
|
||||
filter_state=args.filter_state,
|
||||
filter_priority=args.filter_priority,
|
||||
include_state=args.include_state
|
||||
)
|
||||
elif args.command == 'show-issue':
|
||||
show_issue(args.issue_number)
|
||||
elif args.command == 'analyze-coverage':
|
||||
|
||||
Reference in New Issue
Block a user