boost-skill-cli 1.0.20__tar.gz → 1.0.22__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/PKG-INFO +1 -1
  2. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/_version.py +2 -2
  3. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/commands/discovery.py +16 -9
  4. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/commands/quality.py +7 -0
  5. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/output.py +33 -1
  6. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_skill_cli.egg-info/PKG-INFO +1 -1
  7. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/docs/design-roadmap.html +2 -2
  8. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/functional/test_cli_discovery.py +13 -0
  9. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/functional/test_cli_quality.py +3 -0
  10. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_mutation_hardening.py +2 -1
  11. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_output.py +89 -0
  12. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/.gitignore +0 -0
  13. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/CLAUDE.md +0 -0
  14. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/CONTRIBUTING.md +0 -0
  15. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/LICENSE +0 -0
  16. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/MANIFEST.in +0 -0
  17. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/Makefile +0 -0
  18. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/README.md +0 -0
  19. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/SECURITY.md +0 -0
  20. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost +0 -0
  21. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/__init__.py +0 -0
  22. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/__main__.py +0 -0
  23. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/cli.py +0 -0
  24. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/commands/__init__.py +0 -0
  25. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/commands/bmad.py +0 -0
  26. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/commands/configuration.py +0 -0
  27. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/commands/hooks.py +0 -0
  28. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/commands/info.py +0 -0
  29. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/commands/intelligence.py +0 -0
  30. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/commands/pkg.py +0 -0
  31. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/commands/taps.py +0 -0
  32. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/commands/team.py +0 -0
  33. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/__init__.py +0 -0
  34. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/agents.py +0 -0
  35. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/ai.py +0 -0
  36. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/catalog.py +0 -0
  37. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/claude_settings.py +0 -0
  38. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/config.py +0 -0
  39. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/dense.py +0 -0
  40. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/embed.py +0 -0
  41. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/frontmatter.py +0 -0
  42. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/gitutil.py +0 -0
  43. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/journal.py +0 -0
  44. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/lockfile.py +0 -0
  45. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/logs.py +0 -0
  46. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/paths.py +0 -0
  47. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/policy.py +0 -0
  48. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/rag.py +0 -0
  49. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/registry.py +0 -0
  50. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/store.py +0 -0
  51. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/core/util.py +0 -0
  52. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/data/registries.json +0 -0
  53. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_cli/errors.py +0 -0
  54. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_skill_cli.egg-info/SOURCES.txt +0 -0
  55. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_skill_cli.egg-info/dependency_links.txt +0 -0
  56. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_skill_cli.egg-info/entry_points.txt +0 -0
  57. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_skill_cli.egg-info/requires.txt +0 -0
  58. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/boost_skill_cli.egg-info/top_level.txt +0 -0
  59. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/docs/.claude/settings.local.json +0 -0
  60. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/docs/DEBUGGING.md +0 -0
  61. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/docs/demo.gif +0 -0
  62. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/docs/demo.tape +0 -0
  63. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/docs/index.html +0 -0
  64. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/docs/rag-architecture.md +0 -0
  65. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/docs/roadmap.html +0 -0
  66. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/pyproject.toml +0 -0
  67. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/scripts/build_registries.py +0 -0
  68. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/scripts/mutation_gate.py +0 -0
  69. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/setup.cfg +0 -0
  70. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/style/README.md +0 -0
  71. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/style/boost.css +0 -0
  72. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/style/boost.js +0 -0
  73. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/style/demo.html +0 -0
  74. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/conftest.py +0 -0
  75. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/functional/test_cli_bmad.py +0 -0
  76. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/functional/test_cli_configuration.py +0 -0
  77. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/functional/test_cli_hooks.py +0 -0
  78. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/functional/test_cli_info.py +0 -0
  79. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/functional/test_cli_intelligence.py +0 -0
  80. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/functional/test_cli_pkg.py +0 -0
  81. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/functional/test_cli_taps.py +0 -0
  82. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/functional/test_cli_team.py +0 -0
  83. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/functional/test_everyday_loop.py +0 -0
  84. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/make_fixture.py +0 -0
  85. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/smoke.sh +0 -0
  86. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_agents.py +0 -0
  87. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_ai.py +0 -0
  88. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_catalog.py +0 -0
  89. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_claude_settings.py +0 -0
  90. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_config.py +0 -0
  91. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_dense.py +0 -0
  92. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_embed.py +0 -0
  93. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_errors_and_cli_table.py +0 -0
  94. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_frontmatter.py +0 -0
  95. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_gitutil.py +0 -0
  96. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_journal.py +0 -0
  97. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_lockfile.py +0 -0
  98. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_logs.py +0 -0
  99. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_paths.py +0 -0
  100. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_policy.py +0 -0
  101. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_rag.py +0 -0
  102. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_registry.py +0 -0
  103. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_store.py +0 -0
  104. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_util.py +0 -0
  105. {boost_skill_cli-1.0.20 → boost_skill_cli-1.0.22}/tests/unit/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boost-skill-cli
3
- Version: 1.0.20
3
+ Version: 1.0.22
4
4
  Summary: boost — Homebrew for AI coding skills
5
5
  Author: Jonathan Reyes
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.0.20'
22
- __version_tuple__ = version_tuple = (1, 0, 20)
21
+ __version__ = version = '1.0.22'
22
+ __version_tuple__ = version_tuple = (1, 0, 22)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -209,12 +209,16 @@ def cmd_search(argv):
209
209
  else:
210
210
  out.warn(ai.fallback_note())
211
211
  shown = scored[:args.limit]
212
- width = max(len(e["name"]) for e, _ in shown)
212
+ # Cap the name column so one long name can't pad every row, and clip each
213
+ # description to the remaining terminal width so results stay one line each.
214
+ name_w = min(max(len(e["name"]) for e, _ in shown), 32)
215
+ cols = out.term_width()
213
216
  for e, _s in shown:
214
- line = out.c(e["name"].ljust(width + 3), out.CYAN) + (e["description"] or "")
215
- if e.get("curated"):
216
- line += " " + out.c("★ curated", out.YELLOW)
217
- out.info(line)
217
+ tag = " " + out.c("★ curated", out.YELLOW) if e.get("curated") else ""
218
+ tag_w = len(" ★ curated") if e.get("curated") else 0
219
+ desc_w = max(cols - 2 - name_w - 2 - tag_w, 8)
220
+ name_cell = out.c(out.truncate(e["name"], name_w).ljust(name_w), out.CYAN)
221
+ out.info(name_cell + " " + out.truncate(e["description"] or "", desc_w) + tag)
218
222
  out.info(out.c("%d match%s · ranked by %s"
219
223
  % (len(scored), "" if len(scored) == 1 else "es", ranker),
220
224
  out.DIM))
@@ -450,12 +454,15 @@ def cmd_recommend(argv):
450
454
  out.info("no recommendations for this stack — try `boost search <keyword>`")
451
455
  return 0
452
456
  out.info("no stack-specific matches — curated picks instead:")
453
- width = max(len(r["entry"]["name"]) for r in shown)
457
+ width = min(max(len(r["entry"]["name"]) for r in shown), 32)
458
+ cols = out.term_width()
454
459
  for r in shown:
455
460
  e = r["entry"]
456
- out.info(out.c(e["name"].ljust(width + 3), out.CYAN)
457
- + (e["description"] or "") + " "
458
- + out.c("because: %s" % ", ".join(sorted(r["because"])), out.DIM))
461
+ because = "because: %s" % ", ".join(sorted(r["because"]))
462
+ desc_w = max(cols - 2 - width - 2 - (len(because) + 2), 8)
463
+ name_cell = out.c(out.truncate(e["name"], width).ljust(width), out.CYAN)
464
+ out.info(name_cell + " " + out.truncate(e["description"] or "", desc_w)
465
+ + " " + out.c(because, out.DIM))
459
466
  if ai.available():
460
467
  picks = _ai_picks(stack, [r["entry"] for r in ranked[:20]])
461
468
  if picks:
@@ -239,6 +239,8 @@ def cmd_doctor(argv):
239
239
  ap.parse_args(argv)
240
240
  issues = 0
241
241
 
242
+ out.heading("boost doctor")
243
+
242
244
  def bad(msg):
243
245
  nonlocal issues
244
246
  issues += 1
@@ -341,6 +343,11 @@ def cmd_doctor(argv):
341
343
  out.ok("lock file integrity OK · log rotation healthy")
342
344
  else:
343
345
  out.warn("lock file integrity or log rotation needs attention")
346
+
347
+ out.verdict(issues == 0,
348
+ "healthy" if not issues else
349
+ "%d issue%s need attention — see the suggestions above"
350
+ % (issues, _s(issues)))
344
351
  return 1 if issues else 0
345
352
 
346
353
 
@@ -10,6 +10,7 @@ Conventions used across all commands:
10
10
  from __future__ import annotations
11
11
 
12
12
  import os
13
+ import shutil
13
14
  import sys
14
15
 
15
16
  RESET = "\033[0m"
@@ -142,7 +143,38 @@ def dim(msg: str) -> None:
142
143
 
143
144
 
144
145
  def heading(msg: str) -> None:
145
- print(c("==> ", BLUE, BOLD) + c(msg, BOLD))
146
+ # Brand the section marker in Aurora cyan (truecolor, 16-color fallback,
147
+ # plain under NO_COLOR) so every command's headers read as one system.
148
+ print(aurora("==>", "cyan") + " " + c(msg, BOLD))
149
+
150
+
151
+ def verdict(ok: bool, msg: str) -> None:
152
+ """A dashboard verdict line: a green dot when healthy, amber when not."""
153
+ dot = aurora("●", "green" if ok else "yellow")
154
+ print(" " + dot + " " + c(msg, GREEN if ok else YELLOW))
155
+
156
+
157
+ def term_width(default: int = 80) -> int:
158
+ """Best-effort terminal column count; a stable default when detached."""
159
+ return shutil.get_terminal_size((default, 20)).columns
160
+
161
+
162
+ def truncate(text: str, width: int, ellipsis: str = "…") -> str:
163
+ """Collapse whitespace (including literal \\n / \\t escapes) to single
164
+ spaces, then clip to at most `width` columns with a trailing ellipsis.
165
+
166
+ Keeps list output to one tidy line each — a 2,000-char blob becomes a
167
+ scannable snippet instead of blowing up the pane.
168
+ """
169
+ text = text.replace("\\n", " ").replace("\\t", " ").replace("\\r", " ")
170
+ text = " ".join(text.split())
171
+ if width <= 0:
172
+ return ""
173
+ if len(text) <= width:
174
+ return text
175
+ if width <= len(ellipsis):
176
+ return ellipsis[:width]
177
+ return text[:width - len(ellipsis)] + ellipsis
146
178
 
147
179
 
148
180
  def kv(key: str, value: str, width: int = 14) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boost-skill-cli
3
- Version: 1.0.20
3
+ Version: 1.0.22
4
4
  Summary: boost — Homebrew for AI coding skills
5
5
  Author: Jonathan Reyes
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -278,7 +278,7 @@
278
278
  <div class="rcards">
279
279
 
280
280
  <article class="ritem" data-track="layout" data-impact="high">
281
- <div class="ritem-top"><span class="rid">BOOST-D05</span><span class="rstatus next">Next</span></div>
281
+ <div class="ritem-top"><span class="rid">BOOST-D05</span><span class="rstatus done">Shipped</span></div>
282
282
  <h4>Fix <code>search</code>: truncate &amp; width-clamp results</h4>
283
283
  <p>Today <code>boost search</code> is the worst screen in the app: a single result can dump a <b>2,000-character description with literal, unrendered <code>\\n\\n</code> escapes</b>, while names are padded to a fixed 30 columns that waste half the row. Clamp each row to the live terminal width, collapse whitespace/escape artifacts, and ellipsize. This one fix turns a wall of noise into a scannable list.</p>
284
284
  <div class="rmeta"><span class="m-cat">layout</span><span>Complexity M</span><span class="m-imp high">Impact High</span><span class="m-wow">★★★★★</span></div>
@@ -388,7 +388,7 @@
388
388
  </article>
389
389
 
390
390
  <article class="ritem" data-track="commands" data-impact="med">
391
- <div class="ritem-top"><span class="rid">BOOST-D15</span><span class="rstatus proposed">Proposed</span></div>
391
+ <div class="ritem-top"><span class="rid">BOOST-D15</span><span class="rstatus done">Shipped</span></div>
392
392
  <h4><code>doctor</code> as a health dashboard</h4>
393
393
  <p><code>doctor</code> is an undifferentiated wall of green checks — no hierarchy, no headline. Reframe it as a panel with a verdict line (<code>● Healthy</code> in green / <code>● Attention</code> in amber), grouped checks, dim sub-detail, and severity color so a problem actually stands out instead of hiding among the ✓s.</p>
394
394
  <div class="rmeta"><span class="m-cat">commands</span><span>Complexity M</span><span class="m-imp med">Impact Med</span><span class="m-wow">★★★★☆</span></div>
@@ -6,6 +6,7 @@ asserting exact output shapes, exit codes, and on-disk cache effects.
6
6
  from __future__ import annotations
7
7
 
8
8
  import json
9
+ import re
9
10
  import sqlite3
10
11
  import subprocess
11
12
  import types
@@ -104,6 +105,18 @@ class TestSearch:
104
105
  r = boost("search", "brainstorming")
105
106
  assert "★ curated" in r.out
106
107
 
108
+ def test_result_rows_clamp_to_terminal_width(self, boost, tapped,
109
+ monkeypatch):
110
+ # D05: every result stays one line within the terminal width; a long
111
+ # description is clipped instead of blowing up the pane.
112
+ monkeypatch.setenv("COLUMNS", "60")
113
+ r = boost("search", "workflow")
114
+ ansi = re.compile(r"\x1b\[[0-9;]*m")
115
+ rows = [ln for ln in r.out.splitlines() if ln and "ranked by" not in ln]
116
+ assert rows
117
+ for ln in rows:
118
+ assert len(ansi.sub("", ln)) <= 60
119
+
107
120
  def test_smart_without_ai_warns_and_stays_heuristic(self, boost, tapped):
108
121
  r = boost("search", "testing", "--smart")
109
122
  assert "using the heuristic fallback" in r.out
@@ -52,12 +52,14 @@ class TestDoctor:
52
52
  def test_healthy_exact_summary_rc0(self, boost, tapped):
53
53
  boost("install", "brainstorming", "commit-messages")
54
54
  r = boost("doctor")
55
+ assert "boost doctor" in r.out # branded dashboard header
55
56
  assert "git on PATH" in r.out
56
57
  assert "1 tap cloned & cached" in r.out
57
58
  assert "lock file parses (v3)" in r.out
58
59
  assert "2 skills present in store with agent links" in r.out
59
60
  assert "2 skills installed · 1 tap synced · 0 broken links" in r.out
60
61
  assert "lock file integrity OK · log rotation healthy" in r.out
62
+ assert "● healthy" in r.out # dashboard verdict
61
63
 
62
64
  def test_broken_symlink_rc1(self, boost, installed):
63
65
  ghost = paths.home() / ".claude" / "skills" / "ghost"
@@ -65,6 +67,7 @@ class TestDoctor:
65
67
  r = boost("doctor", expect=1)
66
68
  assert "1 broken symlink in agent dirs — run `boost heal`" in r.out
67
69
  assert "1 skill installed · 1 tap synced · 1 broken link" in r.out
70
+ assert "need attention" in r.out # verdict flips on issues
68
71
 
69
72
  def test_missing_store_rc1(self, boost, installed):
70
73
  shutil.rmtree(paths.store_dir() / "brainstorming")
@@ -128,9 +128,10 @@ class TestForcedColorOutput:
128
128
  "\033[2m hint: try x\033[0m\n")
129
129
 
130
130
  def test_heading_exact_ansi(self, capsys):
131
+ # Aurora cyan marker (truecolor under forced color) + bold title.
131
132
  out.heading("Section")
132
133
  assert capsys.readouterr().out == (
133
- "\033[34m\033[1m==> \033[0m\033[1mSection\033[0m\n")
134
+ "\033[38;2;34;211;238m==>\033[0m \033[1mSection\033[0m\n")
134
135
 
135
136
  def test_c_multi_style_order(self):
136
137
  assert out.c("x", out.RED, out.BOLD) == "\033[31m\033[1mx\033[0m"
@@ -1,6 +1,7 @@
1
1
  """Unit tests: boost_cli/core/output.py — colors, symbols, tables, confirm."""
2
2
  from __future__ import annotations
3
3
 
4
+ import os
4
5
  import sys
5
6
 
6
7
  import pytest
@@ -197,6 +198,86 @@ class TestGradient:
197
198
  "\033[0m")
198
199
 
199
200
 
201
+ class TestHeadingAndVerdictColor:
202
+ def _force_truecolor(self, monkeypatch):
203
+ monkeypatch.setenv("COLORTERM", "truecolor")
204
+ monkeypatch.setenv("CLICOLOR_FORCE", "1")
205
+
206
+ def test_heading_marker_is_cyan_truecolor(self, monkeypatch, capsys):
207
+ self._force_truecolor(monkeypatch)
208
+ output.heading("Section")
209
+ out = capsys.readouterr().out
210
+ assert out.startswith("\033[38;2;34;211;238m==>\033[0m ")
211
+ assert "Section" in out
212
+
213
+ def test_verdict_ok_green_dot_and_green_text(self, monkeypatch, capsys):
214
+ self._force_truecolor(monkeypatch)
215
+ output.verdict(True, "healthy")
216
+ # aurora green dot (#4ade80) + green message text
217
+ assert capsys.readouterr().out == (
218
+ " \033[38;2;74;222;128m●\033[0m \033[32mhealthy\033[0m\n")
219
+
220
+ def test_verdict_bad_yellow_dot_and_yellow_text(self, monkeypatch, capsys):
221
+ self._force_truecolor(monkeypatch)
222
+ output.verdict(False, "1 issue")
223
+ # aurora yellow dot (#facc15) + yellow message text
224
+ assert capsys.readouterr().out == (
225
+ " \033[38;2;250;204;21m●\033[0m \033[33m1 issue\033[0m\n")
226
+
227
+
228
+ class TestTermWidth:
229
+ def test_honors_columns_env(self, monkeypatch):
230
+ monkeypatch.setenv("COLUMNS", "123")
231
+ assert output.term_width() == 123
232
+
233
+ def test_detected_columns(self, monkeypatch):
234
+ monkeypatch.setattr(output.shutil, "get_terminal_size",
235
+ lambda fb: os.terminal_size((93, 24)))
236
+ assert output.term_width() == 93
237
+
238
+ def test_default_80_when_undetectable(self, monkeypatch):
239
+ monkeypatch.delenv("COLUMNS", raising=False)
240
+ monkeypatch.setattr(output.shutil, "get_terminal_size",
241
+ lambda fb: os.terminal_size(fb))
242
+ assert output.term_width() == 80
243
+
244
+ def test_custom_default_when_undetectable(self, monkeypatch):
245
+ monkeypatch.delenv("COLUMNS", raising=False)
246
+ monkeypatch.setattr(output.shutil, "get_terminal_size",
247
+ lambda fb: os.terminal_size(fb))
248
+ assert output.term_width(77) == 77
249
+
250
+
251
+ class TestTruncate:
252
+ def test_short_text_unchanged(self):
253
+ assert output.truncate("hello", 80) == "hello"
254
+
255
+ def test_exact_width_not_clipped(self):
256
+ assert output.truncate("abcd", 4) == "abcd"
257
+
258
+ def test_clips_with_ellipsis(self):
259
+ assert output.truncate("abcdef", 4) == "abc…"
260
+
261
+ def test_collapses_real_whitespace(self):
262
+ assert output.truncate("a\n\n b\tc", 80) == "a b c"
263
+
264
+ def test_collapses_literal_escape_sequences(self):
265
+ # descriptions in the wild carry literal backslash-n blobs
266
+ assert output.truncate("a\\n\\nb", 80) == "a b"
267
+
268
+ def test_collapses_literal_tab_escape(self):
269
+ assert output.truncate("a\\tb", 80) == "a b"
270
+
271
+ def test_collapses_literal_cr_escape(self):
272
+ assert output.truncate("a\\rb", 80) == "a b"
273
+
274
+ def test_zero_width_is_empty(self):
275
+ assert output.truncate("abc", 0) == ""
276
+
277
+ def test_width_at_or_below_ellipsis_returns_ellipsis(self):
278
+ assert output.truncate("abcdef", 1) == "…"
279
+
280
+
200
281
  class TestHelpers:
201
282
  @pytest.fixture(autouse=True)
202
283
  def plain(self, monkeypatch):
@@ -236,6 +317,14 @@ class TestHelpers:
236
317
  output.heading("Section")
237
318
  assert capsys.readouterr().out == "==> Section\n"
238
319
 
320
+ def test_verdict_healthy_plain(self, capsys):
321
+ output.verdict(True, "healthy")
322
+ assert capsys.readouterr().out == " ● healthy\n"
323
+
324
+ def test_verdict_attention_plain(self, capsys):
325
+ output.verdict(False, "2 issues need attention")
326
+ assert capsys.readouterr().out == " ● 2 issues need attention\n"
327
+
239
328
  def test_kv_default_width(self, capsys):
240
329
  output.kv("key", "value")
241
330
  assert capsys.readouterr().out == " key value\n"
File without changes