|
14 | 14 | import logging |
15 | 15 | import os |
16 | 16 | import sys |
| 17 | +from datetime import datetime, timezone |
17 | 18 |
|
18 | 19 | import click |
19 | 20 |
|
20 | 21 | from . import __version__ |
21 | 22 |
|
22 | 23 |
|
| 24 | +def _relative_time(iso_str: str | None) -> str: |
| 25 | + """Format an ISO 8601 timestamp as a human-readable relative time string.""" |
| 26 | + if not iso_str: |
| 27 | + return "" |
| 28 | + try: |
| 29 | + dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00")) |
| 30 | + delta = datetime.now(timezone.utc) - dt |
| 31 | + seconds = int(delta.total_seconds()) |
| 32 | + if seconds < 0: |
| 33 | + return "just now" |
| 34 | + if seconds < 60: |
| 35 | + return f"{seconds}s ago" |
| 36 | + minutes = seconds // 60 |
| 37 | + if minutes < 60: |
| 38 | + return f"{minutes}m ago" |
| 39 | + hours = minutes // 60 |
| 40 | + if hours < 24: |
| 41 | + return f"{hours}h ago" |
| 42 | + days = hours // 24 |
| 43 | + if days < 30: |
| 44 | + return f"{days}d ago" |
| 45 | + months = days // 30 |
| 46 | + if months < 12: |
| 47 | + return f"{months}mo ago" |
| 48 | + years = days // 365 |
| 49 | + return f"{years}y ago" |
| 50 | + except (ValueError, TypeError): |
| 51 | + return "" |
| 52 | + |
| 53 | + |
23 | 54 | @click.group() |
24 | 55 | @click.version_option(version=__version__, prog_name="agentevals") |
25 | 56 | @click.option( |
@@ -314,21 +345,32 @@ def evaluator_list(source: str, refresh: bool) -> None: |
314 | 345 | max_name = max(len(g.name) for g in all_evaluators) |
315 | 346 | max_src = max(len(g.source) for g in all_evaluators) |
316 | 347 |
|
| 348 | + has_updated = any(g.last_updated for g in all_evaluators) |
| 349 | + updated_col_width = 10 if has_updated else 0 |
| 350 | + |
317 | 351 | try: |
318 | 352 | term_width = os.get_terminal_size().columns |
319 | 353 | except OSError: |
320 | 354 | term_width = 120 |
321 | | - desc_width = max(20, term_width - max_name - max_src - 8) |
322 | 355 |
|
323 | | - click.echo(f" {'NAME':<{max_name}} {'SOURCE':<{max_src}} DESCRIPTION") |
324 | | - click.echo(f" {'-' * max_name} {'-' * max_src} {'-' * min(40, desc_width)}") |
| 356 | + overhead = max_name + max_src + 8 |
| 357 | + if has_updated: |
| 358 | + overhead += updated_col_width + 2 |
| 359 | + desc_width = max(20, term_width - overhead) |
| 360 | + |
| 361 | + hdr_updated = f" {'UPDATED':<{updated_col_width}}" if has_updated else "" |
| 362 | + sep_updated = f" {'-' * updated_col_width}" if has_updated else "" |
| 363 | + |
| 364 | + click.echo(f" {'NAME':<{max_name}} {'SOURCE':<{max_src}}{hdr_updated} DESCRIPTION") |
| 365 | + click.echo(f" {'-' * max_name} {'-' * max_src}{sep_updated} {'-' * min(40, desc_width)}") |
325 | 366 |
|
326 | 367 | for g in sorted(all_evaluators, key=lambda x: (x.source, x.name)): |
327 | 368 | lang = f" [{g.language}]" if g.language else "" |
328 | 369 | desc = g.description + lang |
329 | 370 | if len(desc) > desc_width: |
330 | 371 | desc = desc[: desc_width - 3] + "..." |
331 | | - click.echo(f" {g.name:<{max_name}} {g.source:<{max_src}} {desc}") |
| 372 | + col_updated = f" {_relative_time(g.last_updated):<{updated_col_width}}" if has_updated else "" |
| 373 | + click.echo(f" {g.name:<{max_name}} {g.source:<{max_src}}{col_updated} {desc}") |
332 | 374 |
|
333 | 375 | click.echo(f"\n {len(all_evaluators)} evaluator(s) found.") |
334 | 376 |
|
|
0 commit comments