Coverage for little_loops / cli / issues / list_cmd.py: 6%
78 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-04-11 23:20 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-04-11 23:20 -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 from little_loops.cli_args import parse_priorities
41 type_filter = getattr(args, "type", None)
42 priority_filter: set[str] | None = parse_priorities(getattr(args, "priority", None))
44 filtered = [
45 (issue, stat)
46 for issue, stat in raw
47 if (not type_filter or issue.issue_id.split("-", 1)[0] == type_filter)
48 and (not priority_filter or issue.priority in priority_filter)
49 ]
51 # Sort
52 sort_field = getattr(args, "sort", "priority") or "priority"
53 need_content = sort_field in {"created", "completed"}
54 enriched: list[tuple] = []
55 for issue, stat in filtered:
56 disc_date: date | None = None
57 comp_date: date | None = None
58 if need_content:
59 try:
60 content = issue.path.read_text(encoding="utf-8")
61 except Exception:
62 content = ""
63 if sort_field == "created":
64 disc_date = _parse_discovered_date(content)
65 elif sort_field == "completed":
66 from little_loops.issue_history.parsing import _parse_completion_date
68 comp_date = _parse_completion_date(content, issue.path)
69 enriched.append((issue, stat, disc_date, comp_date))
71 if getattr(args, "desc", False):
72 descending = True
73 elif getattr(args, "asc", False):
74 descending = False
75 else:
76 descending = sort_field in {"created", "completed"}
78 enriched = _sort_issues(enriched, sort_field, descending)
80 limit = getattr(args, "limit", None)
81 if limit is not None and limit < 1:
82 import sys
84 print(f"Error: --limit must be a positive integer, got {limit}", file=sys.stderr)
85 return 1
87 if limit is not None:
88 enriched = enriched[:limit]
90 issues_with_status = [(item[0], item[1]) for item in enriched]
92 if not issues_with_status:
93 print("No active issues")
94 return 0
96 if getattr(args, "json", False):
97 print_json(
98 [
99 {
100 "id": issue.issue_id,
101 "priority": issue.priority,
102 "type": issue.issue_id.split("-", 1)[0],
103 "title": issue.title,
104 "path": str(issue.path),
105 "status": stat,
106 "discovered_date": str(disc_date) if disc_date else None,
107 }
108 for issue, stat, disc_date, _comp_date in enriched
109 ]
110 )
111 return 0
113 if getattr(args, "flat", False):
114 for issue, _stat in issues_with_status:
115 print(f"{issue.path.name} {issue.title}")
116 return 0
118 # Group by type prefix
119 buckets: dict[str, list] = {"BUG": [], "FEAT": [], "ENH": []}
120 for issue, stat in issues_with_status:
121 prefix = issue.issue_id.split("-", 1)[0]
122 if prefix in buckets:
123 buckets[prefix].append((issue, stat))
125 type_labels = {"BUG": "Bugs", "FEAT": "Features", "ENH": "Enhancements"}
126 lines: list[str] = []
127 for prefix, label in type_labels.items():
128 group = buckets[prefix]
129 header = colorize(f"{label} ({len(group)})", f"{TYPE_COLOR.get(prefix, '0')};1")
130 lines.append(header)
131 for issue, stat in group:
132 issue_type = issue.issue_id.split("-", 1)[0]
133 colored_id = colorize(issue.issue_id, TYPE_COLOR.get(issue_type, "0"))
134 colored_priority = colorize(issue.priority, PRIORITY_COLOR.get(issue.priority, "0"))
135 status_tag = f" [{stat}]" if stat != "active" else ""
136 lines.append(f" {colored_priority} {colored_id} {issue.title}{status_tag}")
137 lines.append("")
138 lines.append(f"Total: {len(issues_with_status)} active issues")
139 print("\n".join(lines))
140 return 0