boost-skill-cli 1.0.28__tar.gz → 1.0.30__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.28 → boost_skill_cli-1.0.30}/PKG-INFO +1 -1
  2. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/_version.py +2 -2
  3. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/commands/info.py +10 -7
  4. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/output.py +28 -0
  5. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_skill_cli.egg-info/PKG-INFO +1 -1
  6. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/docs/design-roadmap.html +4 -4
  7. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_output.py +49 -0
  8. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/.gitignore +0 -0
  9. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/CLAUDE.md +0 -0
  10. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/CONTRIBUTING.md +0 -0
  11. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/LICENSE +0 -0
  12. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/MANIFEST.in +0 -0
  13. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/Makefile +0 -0
  14. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/README.md +0 -0
  15. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/SECURITY.md +0 -0
  16. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost +0 -0
  17. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/__init__.py +0 -0
  18. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/__main__.py +0 -0
  19. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/cli.py +0 -0
  20. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/commands/__init__.py +0 -0
  21. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/commands/bmad.py +0 -0
  22. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/commands/configuration.py +0 -0
  23. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/commands/discovery.py +0 -0
  24. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/commands/hooks.py +0 -0
  25. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/commands/intelligence.py +0 -0
  26. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/commands/pkg.py +0 -0
  27. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/commands/quality.py +0 -0
  28. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/commands/taps.py +0 -0
  29. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/commands/team.py +0 -0
  30. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/__init__.py +0 -0
  31. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/agents.py +0 -0
  32. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/ai.py +0 -0
  33. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/catalog.py +0 -0
  34. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/claude_settings.py +0 -0
  35. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/config.py +0 -0
  36. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/dense.py +0 -0
  37. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/embed.py +0 -0
  38. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/frontmatter.py +0 -0
  39. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/gitutil.py +0 -0
  40. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/journal.py +0 -0
  41. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/lockfile.py +0 -0
  42. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/logs.py +0 -0
  43. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/paths.py +0 -0
  44. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/policy.py +0 -0
  45. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/rag.py +0 -0
  46. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/registry.py +0 -0
  47. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/store.py +0 -0
  48. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/core/util.py +0 -0
  49. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/data/registries.json +0 -0
  50. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_cli/errors.py +0 -0
  51. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_skill_cli.egg-info/SOURCES.txt +0 -0
  52. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_skill_cli.egg-info/dependency_links.txt +0 -0
  53. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_skill_cli.egg-info/entry_points.txt +0 -0
  54. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_skill_cli.egg-info/requires.txt +0 -0
  55. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/boost_skill_cli.egg-info/top_level.txt +0 -0
  56. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/docs/.claude/settings.local.json +0 -0
  57. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/docs/DEBUGGING.md +0 -0
  58. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/docs/demo.gif +0 -0
  59. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/docs/demo.tape +0 -0
  60. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/docs/index.html +0 -0
  61. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/docs/rag-architecture.md +0 -0
  62. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/docs/roadmap.html +0 -0
  63. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/pyproject.toml +0 -0
  64. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/scripts/build_registries.py +0 -0
  65. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/scripts/mutation_gate.py +0 -0
  66. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/setup.cfg +0 -0
  67. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/style/README.md +0 -0
  68. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/style/boost.css +0 -0
  69. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/style/boost.js +0 -0
  70. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/style/demo.html +0 -0
  71. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/conftest.py +0 -0
  72. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/functional/test_cli_bmad.py +0 -0
  73. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/functional/test_cli_configuration.py +0 -0
  74. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/functional/test_cli_discovery.py +0 -0
  75. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/functional/test_cli_hooks.py +0 -0
  76. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/functional/test_cli_info.py +0 -0
  77. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/functional/test_cli_intelligence.py +0 -0
  78. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/functional/test_cli_pkg.py +0 -0
  79. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/functional/test_cli_quality.py +0 -0
  80. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/functional/test_cli_taps.py +0 -0
  81. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/functional/test_cli_team.py +0 -0
  82. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/functional/test_everyday_loop.py +0 -0
  83. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/make_fixture.py +0 -0
  84. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/smoke.sh +0 -0
  85. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_agents.py +0 -0
  86. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_ai.py +0 -0
  87. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_catalog.py +0 -0
  88. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_claude_settings.py +0 -0
  89. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_config.py +0 -0
  90. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_dense.py +0 -0
  91. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_embed.py +0 -0
  92. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_errors_and_cli_table.py +0 -0
  93. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_frontmatter.py +0 -0
  94. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_gitutil.py +0 -0
  95. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_journal.py +0 -0
  96. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_lockfile.py +0 -0
  97. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_logs.py +0 -0
  98. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_mutation_hardening.py +0 -0
  99. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_paths.py +0 -0
  100. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_policy.py +0 -0
  101. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_rag.py +0 -0
  102. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_registry.py +0 -0
  103. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_store.py +0 -0
  104. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/tests/unit/test_util.py +0 -0
  105. {boost_skill_cli-1.0.28 → boost_skill_cli-1.0.30}/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.28
3
+ Version: 1.0.30
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.28'
22
- __version_tuple__ = version_tuple = (1, 0, 28)
21
+ __version__ = version = '1.0.30'
22
+ __version_tuple__ = version_tuple = (1, 0, 30)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -114,10 +114,12 @@ def cmd_list(argv):
114
114
  print(json.dumps(skills, indent=2, sort_keys=True))
115
115
  return 0
116
116
  if not skills:
117
- out.info("no skills installed" +
118
- (" with tag #%s" % args.tag.lstrip("#") if args.tag else ""))
119
- out.info(out.c("hint: boost tap --defaults && boost search <topic>", out.DIM))
117
+ print(out.empty_state(
118
+ "no skills installed"
119
+ + (" with tag #%s" % args.tag.lstrip("#") if args.tag else ""),
120
+ hint="boost tap --defaults && boost search <topic>"))
120
121
  return 0
122
+ out.heading("installed skills")
121
123
  rows = []
122
124
  for name in sorted(skills):
123
125
  e = skills[name]
@@ -128,7 +130,8 @@ def cmd_list(argv):
128
130
  "·".join(a.split("-")[0] for a in e.get("agents") or []),
129
131
  " ".join(flags)))
130
132
  out.table(rows, headers=("NAME", "VERSION", "TAP", "AGENTS", "FLAGS"))
131
- print(out.c(" %d skill%s" % (len(rows), "" if len(rows) == 1 else "s"), out.DIM))
133
+ print(" " + out.aurora("%d skill%s installed"
134
+ % (len(rows), "" if len(rows) == 1 else "s"), "cyan"))
132
135
  return 0
133
136
 
134
137
 
@@ -344,9 +347,9 @@ def cmd_preview(argv):
344
347
  args = ap.parse_args(argv)
345
348
  path, lock, cat = _resolve_skill_md(args.name)
346
349
  meta, body = frontmatter.parse(_read(path))
347
- print(out.c("%s · v%s · %s" % (meta.get("name") or args.name,
348
- meta.get("version") or "?",
349
- (lock or cat or {}).get("tap", "local")), out.DIM))
350
+ print(out.titlebar("%s · v%s · %s" % (meta.get("name") or args.name,
351
+ meta.get("version") or "?",
352
+ (lock or cat or {}).get("tap", "local"))))
350
353
  print()
351
354
  _render_markdown(body)
352
355
  return 0
@@ -217,6 +217,34 @@ def panel(lines, title: str | None = None, hue: str = "cyan") -> str:
217
217
  return "\n".join(rows)
218
218
 
219
219
 
220
+ # macOS-style traffic-light dots (close/minimise/zoom), matching the web
221
+ # .window .bar dots — exact truecolor with a 16-color fallback each.
222
+ _TRAFFIC = ((0xff, 0x5f, 0x57, RED), (0xfe, 0xbc, 0x2e, YELLOW),
223
+ (0x28, 0xc8, 0x40, GREEN))
224
+
225
+
226
+ def empty_state(message: str, hint: str | None = None) -> str:
227
+ """A standardized empty-state: a muted ○ bullet + message, with an optional
228
+ dim → hint on the next line. One affordance so 'nothing here' always reads
229
+ the same across commands."""
230
+ out_lines = [" " + c("○ " + message, DIM)]
231
+ if hint:
232
+ out_lines.append(" " + c("→ " + hint, DIM))
233
+ return "\n".join(out_lines)
234
+
235
+
236
+ def titlebar(title: str) -> str:
237
+ """A terminal window title bar: traffic-light dots + a bold title, the
238
+ terminal echo of the web .window .bar. Plain dots under NO_COLOR."""
239
+ level = color_level()
240
+ if level == 0:
241
+ dots = "● ● ●"
242
+ else:
243
+ dots = " ".join((rgb(r, g, bl) if level == 2 else fb) + "●" + RESET
244
+ for r, g, bl, fb in _TRAFFIC)
245
+ return " " + dots + " " + c(title, BOLD)
246
+
247
+
220
248
  def meter(fraction: float, width: int = 4) -> str:
221
249
  """A tiny proportional bar for a 0..1 fraction — filled ▰ vs empty ▱.
222
250
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boost-skill-cli
3
- Version: 1.0.28
3
+ Version: 1.0.30
4
4
  Summary: boost — Homebrew for AI coding skills
5
5
  Author: Jonathan Reyes
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -310,7 +310,7 @@
310
310
  </article>
311
311
 
312
312
  <article class="ritem" data-track="layout" data-impact="med">
313
- <div class="ritem-top"><span class="rid">BOOST-D09</span><span class="rstatus proposed">Proposed</span></div>
313
+ <div class="ritem-top"><span class="rid">BOOST-D09</span><span class="rstatus done">Shipped</span></div>
314
314
  <h4>Terminal-window chrome for <code>preview</code> / <code>cat</code></h4>
315
315
  <p>The web guide renders skills inside a titled window with traffic-light dots (<code>.window .dots</code>). Give <code>preview</code>/<code>cat</code> the same treatment: a titled frame with the skill name in the bar, so a rendered <code>SKILL.md</code> in the terminal matches the screenshots on the site.</p>
316
316
  <div class="rmeta"><span class="m-cat">layout</span><span>Complexity M</span><span class="m-imp med">Impact Med</span><span class="m-wow">★★★★☆</span></div>
@@ -412,7 +412,7 @@
412
412
  </article>
413
413
 
414
414
  <article class="ritem" data-track="commands" data-impact="med">
415
- <div class="ritem-top"><span class="rid">BOOST-D16</span><span class="rstatus proposed">Proposed</span></div>
415
+ <div class="ritem-top"><span class="rid">BOOST-D16</span><span class="rstatus done">Shipped</span></div>
416
416
  <h4><code>info</code> skill identity card</h4>
417
417
  <p><code>info</code> lists <code>kv()</code> pairs as flat dim labels. Promote it to a framed identity card: gradient skill name, status badges (<b>installed</b> / <b>pinned</b> / tap origin) styled like the web <code>.badge</code> pills, then the metadata — a real "detail view," not a key-value dump.</p>
418
418
  <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>
@@ -428,7 +428,7 @@
428
428
  </article>
429
429
 
430
430
  <article class="ritem" data-track="commands" data-impact="low">
431
- <div class="ritem-top"><span class="rid">BOOST-D18</span><span class="rstatus proposed">Proposed</span></div>
431
+ <div class="ritem-top"><span class="rid">BOOST-D18</span><span class="rstatus done">Shipped</span></div>
432
432
  <h4>Consistent empty-states &amp; hint styling</h4>
433
433
  <p>Empty and hint lines are ad-hoc — <code>info</code>'s "no skills installed" hint is a hand-rolled <code>DIM</code> string. Standardize a single empty-state affordance (muted icon + one-line guidance + suggested command) and a single hint style, applied everywhere, so guidance always looks the same.</p>
434
434
  <div class="rmeta"><span class="m-cat">ux</span><span>Complexity S</span><span class="m-imp low">Impact Low</span><span class="m-wow">★★☆☆☆</span></div>
@@ -459,7 +459,7 @@
459
459
  <div class="rcards">
460
460
 
461
461
  <article class="ritem" data-track="system" data-impact="med">
462
- <div class="ritem-top"><span class="rid">BOOST-D19</span><span class="rstatus next">Next</span></div>
462
+ <div class="ritem-top"><span class="rid">BOOST-D19</span><span class="rstatus done">Shipped</span></div>
463
463
  <h4>Resolve theme drift: <code>style/</code> vs <code>docs/style/</code></h4>
464
464
  <p>Two copies of <code>boost.css</code> have <b>diverged</b>: the root <code>style/</code> uses a cyan→violet→pink gradient (<code>--violet #a855f7</code>), while <code>docs/style/</code> uses an orange→pink→violet one (<code>--violet #a78bfa</code>). Same brand, two looks. Pick <code>style/</code> as canonical and make <code>docs/style/</code> a generated copy or symlink so the portfolio, roadmaps and CLI can't drift again.</p>
465
465
  <div class="rmeta"><span class="m-cat">system</span><span>Complexity S</span><span class="m-imp med">Impact Med</span><span class="m-wow">★★★☆☆</span></div>
@@ -343,6 +343,55 @@ class TestPanel:
343
343
  assert "\033[1minventory\033[0m" in output.panel("x", title="inventory")
344
344
 
345
345
 
346
+ class TestEmptyState:
347
+ def test_message_only_plain(self, monkeypatch):
348
+ monkeypatch.setenv("NO_COLOR", "1")
349
+ assert output.empty_state("nothing here") == " ○ nothing here"
350
+
351
+ def test_message_and_hint_plain(self, monkeypatch):
352
+ monkeypatch.setenv("NO_COLOR", "1")
353
+ assert output.empty_state("empty", hint="try x") == (
354
+ " ○ empty\n → try x")
355
+
356
+ def test_no_hint_is_single_line(self, monkeypatch):
357
+ monkeypatch.setenv("NO_COLOR", "1")
358
+ assert "\n" not in output.empty_state("solo")
359
+
360
+ def test_message_is_dim_when_forced(self, monkeypatch):
361
+ monkeypatch.setenv("CLICOLOR_FORCE", "1")
362
+ assert output.empty_state("x") == " \033[2m○ x\033[0m"
363
+
364
+ def test_both_lines_dim_when_forced(self, monkeypatch):
365
+ monkeypatch.setenv("CLICOLOR_FORCE", "1")
366
+ assert output.empty_state("x", hint="y") == (
367
+ " \033[2m○ x\033[0m\n \033[2m→ y\033[0m")
368
+
369
+
370
+ class TestTitlebar:
371
+ def test_plain_dots_and_title(self, monkeypatch):
372
+ monkeypatch.setenv("NO_COLOR", "1")
373
+ assert output.titlebar("skill") == " ● ● ● skill"
374
+
375
+ def test_truecolor_dots_exact(self, monkeypatch):
376
+ monkeypatch.setenv("COLORTERM", "truecolor")
377
+ monkeypatch.setenv("CLICOLOR_FORCE", "1")
378
+ # exact bar pins the three traffic-light hexes, single-space separators
379
+ # between dots, and the bold title
380
+ assert output.titlebar("x") == (
381
+ " \033[38;2;255;95;87m●\033[0m" # #ff5f57 close
382
+ " \033[38;2;254;188;46m●\033[0m" # #febc2e minimise
383
+ " \033[38;2;40;200;64m●\033[0m" # #28c840 zoom
384
+ " \033[1mx\033[0m")
385
+
386
+ def test_basic_dots_use_16color_fallback(self, monkeypatch):
387
+ # Force the 16-color tier: dots fall back to basic RED/YELLOW/GREEN.
388
+ monkeypatch.setattr(output, "color_level", lambda: 1)
389
+ bar = output.titlebar("x")
390
+ assert output.RED + "●" + output.RESET in bar
391
+ assert output.YELLOW + "●" + output.RESET in bar
392
+ assert output.GREEN + "●" + output.RESET in bar
393
+
394
+
346
395
  class TestMeter:
347
396
  def test_full(self):
348
397
  assert output.meter(1.0, 4) == "▰▰▰▰"
File without changes