Coverage for little_loops / cli / issues / list_cmd.py: 0%
77 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:18 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:18 -0500
1"""ll-issues list: List active issues with optional type/priority/status filters."""
3from __future__ import annotations
5import argparse
6from typing import TYPE_CHECKING
8from little_loops.cli.output import PRIORITY_COLOR, TYPE_COLOR, colorize, print_json
10if TYPE_CHECKING:
11 from little_loops.config import BRConfig
14def cmd_list(config: BRConfig, args: argparse.Namespace) -> int:
15 """List issues with optional filters.
17 Args:
18 config: Project configuration
19 args: Parsed arguments with optional .type, .priority, .status, .flat, and .json attributes
21 Returns:
22 Exit code (0 = success)
23 """
24 from datetime import date
26 from little_loops.cli.issues.search import (
27 _load_issues_with_status,
28 _parse_discovered_date,
29 _sort_issues,
30 )
32 status = getattr(args, "status", "active") or "active"
33 include_active = status in ("active", "all")
34 include_completed = status in ("completed", "all")
35 include_deferred = status in ("deferred", "all")
37 raw = _load_issues_with_status(config, include_active, include_completed, include_deferred)
39 type_filter = getattr(args, "type", None)
40 priority_filter = getattr(args, "priority", None)
42 filtered = [
43 (issue, stat)
44 for issue, stat in raw
45 if (not type_filter or issue.issue_id.split("-", 1)[0] == type_filter)
46 and (not priority_filter or issue.priority == priority_filter)
47 ]
49 # Sort
50 sort_field = getattr(args, "sort", "priority") or "priority"
51 need_content = sort_field in {"created", "completed"}
52 enriched: list[tuple] = []
53 for issue, stat in filtered:
54 disc_date: date | None = None
55 comp_date: date | None = None
56 if need_content:
57 try:
58 content = issue.path.read_text(encoding="utf-8")
59 except Exception:
60 content = ""
61 if sort_field == "created":
62 disc_date = _parse_discovered_date(content)
63 elif sort_field == "completed":
64 from little_loops.issue_history.parsing import _parse_completion_date
66 comp_date = _parse_completion_date(content, issue.path)
67 enriched.append((issue, stat, disc_date, comp_date))
69 if getattr(args, "desc", False):
70 descending = True
71 elif getattr(args, "asc", False):
72 descending = False
73 else:
74 descending = sort_field in {"created", "completed"}
76 enriched = _sort_issues(enriched, sort_field, descending)
78 limit = getattr(args, "limit", None)
79 if limit is not None and limit < 1:
80 import sys
82 print(f"Error: --limit must be a positive integer, got {limit}", file=sys.stderr)
83 return 1
85 if limit is not None:
86 enriched = enriched[:limit]
88 issues_with_status = [(item[0], item[1]) for item in enriched]
90 if not issues_with_status:
91 print("No active issues")
92 return 0
94 if getattr(args, "json", False):
95 print_json(
96 [
97 {
98 "id": issue.issue_id,
99 "priority": issue.priority,
100 "type": issue.issue_id.split("-", 1)[0],
101 "title": issue.title,
102 "path": str(issue.path),
103 "status": stat,
104 "discovered_date": str(disc_date) if disc_date else None,
105 }
106 for issue, stat, disc_date, _comp_date in enriched
107 ]
108 )
109 return 0
111 if getattr(args, "flat", False):
112 for issue, _stat in issues_with_status:
113 print(f"{issue.path.name} {issue.title}")
114 return 0
116 # Group by type prefix
117 buckets: dict[str, list] = {"BUG": [], "FEAT": [], "ENH": []}
118 for issue, stat in issues_with_status:
119 prefix = issue.issue_id.split("-", 1)[0]
120 if prefix in buckets:
121 buckets[prefix].append((issue, stat))
123 type_labels = {"BUG": "Bugs", "FEAT": "Features", "ENH": "Enhancements"}
124 lines: list[str] = []
125 for prefix, label in type_labels.items():
126 group = buckets[prefix]
127 header = colorize(f"{label} ({len(group)})", f"{TYPE_COLOR.get(prefix, '0')};1")
128 lines.append(header)
129 for issue, stat in group:
130 issue_type = issue.issue_id.split("-", 1)[0]
131 colored_id = colorize(issue.issue_id, TYPE_COLOR.get(issue_type, "0"))
132 colored_priority = colorize(issue.priority, PRIORITY_COLOR.get(issue.priority, "0"))
133 status_tag = f" [{stat}]" if stat != "active" else ""
134 lines.append(f" {colored_priority} {colored_id} {issue.title}{status_tag}")
135 lines.append("")
136 lines.append(f"Total: {len(issues_with_status)} active issues")
137 print("\n".join(lines))
138 return 0