|
17 | 17 | import argparse |
18 | 18 | import json |
19 | 19 | import os |
| 20 | +import re |
20 | 21 | import sys |
| 22 | +from collections import Counter |
21 | 23 | from concurrent.futures import ThreadPoolExecutor, as_completed |
22 | 24 |
|
23 | 25 | # Add current directory to path |
|
43 | 45 | update_local_installation, merge_for_display, build_legacy_snapshot, |
44 | 46 | ) |
45 | 47 |
|
| 48 | +# Strip control characters from externally-sourced strings (e.g. GitHub tags) |
| 49 | +# to prevent terminal escape sequence injection. |
| 50 | +_CONTROL_CHAR_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]") |
| 51 | + |
| 52 | + |
| 53 | +def _sanitize(s: str) -> str: |
| 54 | + """Remove control characters from a string for safe terminal display.""" |
| 55 | + return _CONTROL_CHAR_RE.sub("", s) if s else s |
| 56 | + |
| 57 | + |
46 | 58 | # Configuration from environment |
47 | 59 | OFFLINE_MODE = os.environ.get("CLI_AUDIT_OFFLINE", "0") == "1" |
48 | 60 | MAX_WORKERS = int(os.environ.get("CLI_AUDIT_MAX_WORKERS", "16")) |
@@ -601,8 +613,8 @@ def cmd_update(args: argparse.Namespace) -> int: |
601 | 613 | latest_color = BLUE |
602 | 614 | op = "?" |
603 | 615 |
|
604 | | - inst_display = inst if inst else "n/a" |
605 | | - latest_display = latest if latest else "n/a" |
| 616 | + inst_display = _sanitize(inst) if inst else "n/a" |
| 617 | + latest_display = _sanitize(latest) if latest else "n/a" |
606 | 618 |
|
607 | 619 | # Add pinned/skip markers (reuse catalog from outer scope) |
608 | 620 | markers = [] |
@@ -643,31 +655,29 @@ def cmd_update(args: argparse.Namespace) -> int: |
643 | 655 | executor.shutdown(wait=False, cancel_futures=True) |
644 | 656 | raise |
645 | 657 |
|
646 | | - # Print grouped summary |
| 658 | + # Print grouped summary (single-pass counting) |
| 659 | + status_counts: dict[str, Counter] = {cat: Counter() for cat in sorted_cats} |
| 660 | + for r in results: |
| 661 | + cat = r.get("category", "general") |
| 662 | + if cat in status_counts: |
| 663 | + status_counts[cat][r.get("status")] += 1 |
| 664 | + |
647 | 665 | print(f"\n# Summary by category:", file=sys.stderr) |
648 | 666 | for category in sorted_cats: |
649 | | - cat_tools = categorized[category] |
650 | 667 | icon = CATEGORY_ICON.get(category, "📦") |
651 | 668 | desc = CATEGORY_DESC.get(category, category) |
652 | | - # Count statuses for this category |
653 | | - cat_names = {t.name for t in cat_tools} |
654 | | - cat_results = [r for r in results if r.get("tool") in cat_names] |
655 | | - up_to_date = sum(1 for r in cat_results if r.get("status") == "UP-TO-DATE") |
656 | | - outdated = sum(1 for r in cat_results if r.get("status") == "OUTDATED") |
657 | | - not_installed = sum(1 for r in cat_results if r.get("status") == "NOT INSTALLED") |
658 | | - conflict = sum(1 for r in cat_results if r.get("status") == "CONFLICT") |
659 | | - unknown = sum(1 for r in cat_results if r.get("status") == "UNKNOWN") |
| 669 | + counts = status_counts[category] |
660 | 670 | parts = [] |
661 | | - if up_to_date: |
662 | | - parts.append(f"{GREEN}{up_to_date} current{RESET}") |
663 | | - if outdated: |
664 | | - parts.append(f"{YELLOW}{outdated} outdated{RESET}") |
665 | | - if not_installed: |
666 | | - parts.append(f"{BLUE}{not_installed} missing{RESET}") |
667 | | - if conflict: |
668 | | - parts.append(f"{YELLOW}{conflict} conflict{RESET}") |
669 | | - if unknown: |
670 | | - parts.append(f"{BLUE}{unknown} unknown{RESET}") |
| 671 | + if counts["UP-TO-DATE"]: |
| 672 | + parts.append(f"{GREEN}{counts['UP-TO-DATE']} current{RESET}") |
| 673 | + if counts["OUTDATED"]: |
| 674 | + parts.append(f"{YELLOW}{counts['OUTDATED']} outdated{RESET}") |
| 675 | + if counts["NOT INSTALLED"]: |
| 676 | + parts.append(f"{BLUE}{counts['NOT INSTALLED']} missing{RESET}") |
| 677 | + if counts["CONFLICT"]: |
| 678 | + parts.append(f"{YELLOW}{counts['CONFLICT']} conflict{RESET}") |
| 679 | + if counts["UNKNOWN"]: |
| 680 | + parts.append(f"{BLUE}{counts['UNKNOWN']} unknown{RESET}") |
671 | 681 | summary = ", ".join(parts) if parts else "–" |
672 | 682 | print(f"# {icon} {desc}: {summary}", file=sys.stderr) |
673 | 683 |
|
@@ -700,8 +710,8 @@ def cmd_update(args: argparse.Namespace) -> int: |
700 | 710 | inst_color = BLUE |
701 | 711 | op = "?" |
702 | 712 |
|
703 | | - inst_display = inst if inst else "n/a" |
704 | | - latest_display = latest if latest else "n/a" |
| 713 | + inst_display = _sanitize(inst) if inst else "n/a" |
| 714 | + latest_display = _sanitize(latest) if latest else "n/a" |
705 | 715 | inst_fmt = f"{inst_color}{inst_display}{RESET}" |
706 | 716 | latest_fmt = f"{BOLD_GREEN}{latest_display}{RESET}" |
707 | 717 | print(f"# → {versioned_name}: {inst_fmt} {op} {latest_fmt}", file=sys.stderr, flush=True) |
|
0 commit comments