vault-cli: TUI-style colored --pretty report
This commit is contained in:
Binary file not shown.
+185
-29
@@ -147,66 +147,222 @@ def build_report():
|
||||
}
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# TUI color helpers
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
import shutil
|
||||
|
||||
_TTY = sys.stdout.isatty() and os.environ.get("NO_COLOR") is None
|
||||
|
||||
# ANSI color codes
|
||||
_BOLD = "\033[1m"
|
||||
_DIM = "\033[2m"
|
||||
_RED = "\033[31m"
|
||||
_GREEN = "\033[32m"
|
||||
_YELLOW = "\033[33m"
|
||||
_CYAN = "\033[36m"
|
||||
_RESET = "\033[0m"
|
||||
|
||||
|
||||
def c(text, code):
|
||||
"""Apply color code if TTY; return plain text otherwise."""
|
||||
if not _TTY:
|
||||
return text
|
||||
return f"{code}{text}{_RESET}"
|
||||
|
||||
|
||||
def rule(title):
|
||||
"""Draw a full-width section header with title."""
|
||||
width = shutil.get_terminal_size((100, 20)).columns
|
||||
# Format: "━ TITLE " padded to width with ━
|
||||
prefix = "━ "
|
||||
suffix = " "
|
||||
available = width - len(prefix) - len(suffix) - len(title)
|
||||
if available < 0:
|
||||
available = 0
|
||||
padding = "━" * available
|
||||
return c(f"{prefix}{title}{suffix}{padding}", _CYAN)
|
||||
|
||||
|
||||
def badge(count, good_if_zero=True):
|
||||
"""Return a color-coded count badge."""
|
||||
if count == 0:
|
||||
return c("✓ none", _GREEN) if good_if_zero else c("0", _GREEN)
|
||||
elif count <= 3:
|
||||
return c(str(count), _YELLOW)
|
||||
else:
|
||||
return c(str(count), _RED)
|
||||
|
||||
|
||||
def box_summary(counts):
|
||||
"""Render a boxed summary header with counts."""
|
||||
# Build content lines
|
||||
items = [
|
||||
(c("Systems", _DIM), c(str(counts["systems"]), _CYAN + _BOLD)),
|
||||
(c("Areas", _DIM), c(str(counts["areas"]), _CYAN + _BOLD)),
|
||||
(c("Categories", _DIM), c(str(counts["categories"]), _CYAN + _BOLD)),
|
||||
(c("IDs", _DIM), c(str(counts["ids"]), _CYAN + _BOLD)),
|
||||
]
|
||||
# Calculate width accounting for ANSI codes (rough estimate)
|
||||
box_width = shutil.get_terminal_size((100, 20)).columns
|
||||
|
||||
# Build box
|
||||
if _TTY:
|
||||
top = f"┌─{c('─' * (box_width - 4), _CYAN)}─┐"
|
||||
else:
|
||||
top = f"┌─{'─' * (box_width - 4)}─┐"
|
||||
|
||||
content = " " + " · ".join(f"{label} {value}" for label, value in items) + " "
|
||||
mid = f"│ {content} │"
|
||||
|
||||
if _TTY:
|
||||
bot = f"└─{c('─' * (box_width - 4), _CYAN)}─┘"
|
||||
else:
|
||||
bot = f"└─{'─' * (box_width - 4)}─┘"
|
||||
|
||||
return f"{top}\n{mid}\n{bot}"
|
||||
|
||||
|
||||
def pretty(r):
|
||||
L = []
|
||||
c = r["counts"]
|
||||
L.append(f"# Vault Librarian report")
|
||||
L.append(f"{c['systems']} systems · {c['areas']} areas · {c['categories']} categories · {c['ids']} IDs\n")
|
||||
c_counts = r["counts"]
|
||||
|
||||
# Summary banner
|
||||
L.append(box_summary(c_counts))
|
||||
L.append("")
|
||||
|
||||
# Broken links
|
||||
bl = r["broken_links"]
|
||||
L.append(f"## Broken wikilinks: {bl['count']}")
|
||||
if bl["count"] == 0:
|
||||
L.append(c(rule("Broken wikilinks"), _GREEN))
|
||||
L.append(f" {c('✓ none', _GREEN)}")
|
||||
else:
|
||||
L.append(c(rule(f"Broken wikilinks — {bl['count']} found"), _RED))
|
||||
if bl["guessable"]:
|
||||
L.append(f"**Likely fixable ({len(bl['guessable'])})** — target renamed, single candidate:")
|
||||
guessable_len = len(bl["guessable"])
|
||||
L.append(f" {c(f'Likely fixable ({guessable_len})', _GREEN)} — target renamed, single candidate:")
|
||||
for b in bl["guessable"][:30]:
|
||||
L.append(f" - `{b['file']}`: [[{b['target']}]] → guess [[{b['guess']}]]")
|
||||
target = b['target']
|
||||
guess = b['guess']
|
||||
L.append(f" {c(b['file'], _DIM)}: {c(f'[[{target}]]', _BOLD)} {c('→', _DIM)} guess {c(f'[[{guess}]]', _BOLD)}")
|
||||
if len(bl["guessable"]) > 30:
|
||||
more = len(bl["guessable"]) - 30
|
||||
L.append(f" {c(f'… and {more} more', _DIM)}")
|
||||
if bl["unresolvable"]:
|
||||
L.append(f"**Unresolvable ({len(bl['unresolvable'])})** — target never existed / dropped stub:")
|
||||
unresolvable_len = len(bl["unresolvable"])
|
||||
L.append(f" {c(f'Unresolvable ({unresolvable_len})', _YELLOW)} — target never existed / dropped stub:")
|
||||
for b in bl["unresolvable"][:30]:
|
||||
L.append(f" - `{b['file']}`: [[{b['target']}]]")
|
||||
target = b['target']
|
||||
L.append(f" {c(b['file'], _DIM)}: {c(f'[[{target}]]', _BOLD)}")
|
||||
if len(bl["unresolvable"]) > 30:
|
||||
more = len(bl["unresolvable"]) - 30
|
||||
L.append(f" {c(f'… and {more} more', _DIM)}")
|
||||
L.append("")
|
||||
|
||||
# Topology
|
||||
t = r["topology"]
|
||||
L.append("## Cross-tree topology (/vault vs /data)")
|
||||
has_mismatch = any(
|
||||
t[level]["missing_in_data"] or t[level]["orphan_in_data"]
|
||||
for level in ("systems", "areas", "cats", "ids")
|
||||
)
|
||||
|
||||
if not has_mismatch:
|
||||
L.append(c(rule("Cross-tree topology (/vault vs /data)"), _GREEN))
|
||||
L.append(f" {c('✓ in sync', _GREEN)}")
|
||||
else:
|
||||
L.append(c(rule("Cross-tree topology — mismatches found"), _YELLOW))
|
||||
for level in ("systems", "areas", "cats", "ids"):
|
||||
md = t[level]["missing_in_data"]; od = t[level]["orphan_in_data"]
|
||||
md = t[level]["missing_in_data"]
|
||||
od = t[level]["orphan_in_data"]
|
||||
if md:
|
||||
L.append(f" - **{level} missing /data home ({len(md)}):** {', '.join(md[:20])}{' …' if len(md)>20 else ''}")
|
||||
md_str = ", ".join(md[:20]) + (f" {c('…', _DIM)}" if len(md) > 20 else "")
|
||||
L.append(f" {c(f'{level} missing /data ({len(md)})', _YELLOW)}: {md_str}")
|
||||
if od:
|
||||
L.append(f" - **{level} orphan in /data ({len(od)}):** {', '.join(od[:20])}")
|
||||
od_str = ", ".join(od[:20]) + (f" {c('…', _DIM)}" if len(od) > 20 else "")
|
||||
L.append(f" {c(f'{level} orphan in /data ({len(od)})', _YELLOW)}: {od_str}")
|
||||
L.append("")
|
||||
|
||||
# Structure
|
||||
st = r["structure"]
|
||||
L.append("## JD structural")
|
||||
has_struct_issues = bool(st["extend_notation"] or st["missing_dot00"] or st["missing_folder_note"])
|
||||
|
||||
if has_struct_issues:
|
||||
L.append(c(rule("JD structural issues"), _YELLOW))
|
||||
if st["extend_notation"]:
|
||||
L.append(f" - **extend-notation `NN+` needing real IDs ({len(st['extend_notation'])}):**")
|
||||
for e in st["extend_notation"]:
|
||||
L.append(f" {e}")
|
||||
extend_len = len(st["extend_notation"])
|
||||
L.append(f" {c(f'extend-notation `NN+` needing real IDs ({extend_len})', _YELLOW)}:")
|
||||
for e in st["extend_notation"][:20]:
|
||||
L.append(f" {c(e, _DIM)}")
|
||||
if len(st["extend_notation"]) > 20:
|
||||
more = len(st["extend_notation"]) - 20
|
||||
L.append(f" {c(f'… and {more} more', _DIM)}")
|
||||
if st["missing_dot00"]:
|
||||
L.append(f" - **categories missing a .00 note ({len(st['missing_dot00'])}):** {', '.join(sorted(st['missing_dot00'])[:25])}")
|
||||
codes = ", ".join(sorted(st["missing_dot00"])[:25])
|
||||
if len(st["missing_dot00"]) > 25:
|
||||
codes += f" {c('…', _DIM)}"
|
||||
dot00_len = len(st["missing_dot00"])
|
||||
L.append(f" {c(f'categories missing .00 note ({dot00_len})', _YELLOW)}: {codes}")
|
||||
if st["missing_folder_note"]:
|
||||
L.append(f" - **folders missing a folder-note ({len(st['missing_folder_note'])}):** {len(st['missing_folder_note'])} (Folder Notes plugin)")
|
||||
folder_len = len(st["missing_folder_note"])
|
||||
L.append(f" {c(f'folders missing folder-note ({folder_len})', _YELLOW)}")
|
||||
L.append("")
|
||||
|
||||
# To-dos
|
||||
td = r["todos"]
|
||||
L.append(f"## To-dos: {td['count']} open ({len(td['pressing'])} pressing, {len(td['long_term'])} long-term)")
|
||||
L.append(f"**Pressing ({len(td['pressing'])}):**")
|
||||
total_todos = td['count']
|
||||
L.append(c(rule(f"To-dos: {c(str(total_todos), _BOLD)} open"), _CYAN))
|
||||
pressing_len = len(td["pressing"])
|
||||
longterm_len = len(td["long_term"])
|
||||
L.append(f" {c(f'Pressing: {pressing_len}', _BOLD)} / {c(f'Long-term: {longterm_len}', _DIM)}")
|
||||
|
||||
if td["pressing"]:
|
||||
L.append(f" {c('Pressing:', _BOLD)}")
|
||||
for x in td["pressing"][:40]:
|
||||
L.append(f" - `{x['file']}`:{x['line']} {x['text']}")
|
||||
L.append(f"**Long-term ({len(td['long_term'])})** — standing checklists, not nagged:")
|
||||
file_info = c(f'{x["file"]}:{x["line"]}', _CYAN)
|
||||
L.append(f" {file_info} {x['text']}")
|
||||
if len(td["pressing"]) > 40:
|
||||
more = len(td["pressing"]) - 40
|
||||
L.append(f" {c(f'… and {more} more', _DIM)}")
|
||||
|
||||
if td["long_term"]:
|
||||
L.append(f" {c('Long-term (standing checklists):', _DIM)}")
|
||||
for x in td["long_term"][:15]:
|
||||
L.append(f" - `{x['file']}`:{x['line']} {x['text']}")
|
||||
file_info = c(f'{x["file"]}:{x["line"]}', _DIM)
|
||||
text_info = c(x['text'], _DIM)
|
||||
L.append(f" {file_info} {text_info}")
|
||||
if len(td["long_term"]) > 15:
|
||||
L.append(f" … and {len(td['long_term'])-15} more long-term")
|
||||
more = len(td["long_term"]) - 15
|
||||
L.append(f" {c(f'… and {more} more', _DIM)}")
|
||||
L.append("")
|
||||
|
||||
# Category health
|
||||
ch = r["category_health"]
|
||||
if ch["near_id_limit"]:
|
||||
L.append("## Category health")
|
||||
if ch["near_id_limit"] or ch["duplicate_titles"]:
|
||||
L.append(c(rule("Category health"), _YELLOW))
|
||||
for x in ch["near_id_limit"]:
|
||||
L.append(f" - `{x['category']}` has {x['ids']} IDs (near 10 limit)")
|
||||
category = c(x['category'], _BOLD)
|
||||
ids_count = c(str(x['ids']), _YELLOW)
|
||||
L.append(f" {category} — {ids_count} IDs, near 10 limit")
|
||||
if ch["duplicate_titles"]:
|
||||
L.append(f" - duplicate ID titles across systems ({len(ch['duplicate_titles'])}): "
|
||||
+ "; ".join(f"{t} {v}" for t, v in list(ch["duplicate_titles"].items())[:10]))
|
||||
for t, codes in list(ch["duplicate_titles"].items())[:10]:
|
||||
codes_str = ", ".join(codes)
|
||||
L.append(f" {c('⚠ duplicate title:', _YELLOW)} {t} {c(codes_str, _DIM)}")
|
||||
L.append("")
|
||||
|
||||
# Footer status bar
|
||||
issues_count = sum([
|
||||
bl["count"],
|
||||
sum(1 for level in ("systems", "areas", "cats", "ids")
|
||||
if t[level]["missing_in_data"] or t[level]["orphan_in_data"]),
|
||||
len(st["extend_notation"]) + len(st["missing_dot00"]) + len(st["missing_folder_note"]),
|
||||
len(ch["near_id_limit"]) + len(ch["duplicate_titles"]),
|
||||
])
|
||||
|
||||
if issues_count == 0:
|
||||
L.append(c("● VAULT HEALTHY", _GREEN))
|
||||
else:
|
||||
L.append(c(f"● {issues_count} item(s) need attention", _YELLOW))
|
||||
|
||||
return "\n".join(L)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user