vulnctl 0.2.1__tar.gz → 0.2.2__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 (130) hide show
  1. {vulnctl-0.2.1 → vulnctl-0.2.2}/CHANGELOG.md +14 -1
  2. {vulnctl-0.2.1 → vulnctl-0.2.2}/PKG-INFO +1 -1
  3. {vulnctl-0.2.1 → vulnctl-0.2.2}/pyproject.toml +1 -1
  4. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/cli_help.py +23 -24
  5. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_cli.py +20 -0
  6. {vulnctl-0.2.1 → vulnctl-0.2.2}/uv.lock +1 -1
  7. {vulnctl-0.2.1 → vulnctl-0.2.2}/.github/workflows/ci.yml +0 -0
  8. {vulnctl-0.2.1 → vulnctl-0.2.2}/.github/workflows/live-smoke.yml +0 -0
  9. {vulnctl-0.2.1 → vulnctl-0.2.2}/.github/workflows/publish-testpypi.yml +0 -0
  10. {vulnctl-0.2.1 → vulnctl-0.2.2}/.github/workflows/release.yml +0 -0
  11. {vulnctl-0.2.1 → vulnctl-0.2.2}/.gitignore +0 -0
  12. {vulnctl-0.2.1 → vulnctl-0.2.2}/.pre-commit-config.yaml +0 -0
  13. {vulnctl-0.2.1 → vulnctl-0.2.2}/.python-version +0 -0
  14. {vulnctl-0.2.1 → vulnctl-0.2.2}/CLAUDE.md +0 -0
  15. {vulnctl-0.2.1 → vulnctl-0.2.2}/FRAMEWORK.md +0 -0
  16. {vulnctl-0.2.1 → vulnctl-0.2.2}/LICENSE +0 -0
  17. {vulnctl-0.2.1 → vulnctl-0.2.2}/README.md +0 -0
  18. {vulnctl-0.2.1 → vulnctl-0.2.2}/ROADMAP.md +0 -0
  19. {vulnctl-0.2.1 → vulnctl-0.2.2}/SPEC.md +0 -0
  20. {vulnctl-0.2.1 → vulnctl-0.2.2}/docs/announcement.md +0 -0
  21. {vulnctl-0.2.1 → vulnctl-0.2.2}/docs/case-study.md +0 -0
  22. {vulnctl-0.2.1 → vulnctl-0.2.2}/docs/cli.md +0 -0
  23. {vulnctl-0.2.1 → vulnctl-0.2.2}/docs/context.md +0 -0
  24. {vulnctl-0.2.1 → vulnctl-0.2.2}/docs/demo.tape +0 -0
  25. {vulnctl-0.2.1 → vulnctl-0.2.2}/docs/exit-codes.md +0 -0
  26. {vulnctl-0.2.1 → vulnctl-0.2.2}/docs/output.md +0 -0
  27. {vulnctl-0.2.1 → vulnctl-0.2.2}/docs/releasing.md +0 -0
  28. {vulnctl-0.2.1 → vulnctl-0.2.2}/docs/schema.json +0 -0
  29. {vulnctl-0.2.1 → vulnctl-0.2.2}/docs/schema.md +0 -0
  30. {vulnctl-0.2.1 → vulnctl-0.2.2}/docs/trees.md +0 -0
  31. {vulnctl-0.2.1 → vulnctl-0.2.2}/examples/app.cdx.json +0 -0
  32. {vulnctl-0.2.1 → vulnctl-0.2.2}/examples/ci/vulnctl-gate.yml +0 -0
  33. {vulnctl-0.2.1 → vulnctl-0.2.2}/examples/context.yaml +0 -0
  34. {vulnctl-0.2.1 → vulnctl-0.2.2}/scripts/build_exploit_index.py +0 -0
  35. {vulnctl-0.2.1 → vulnctl-0.2.2}/scripts/case_study_stats.py +0 -0
  36. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/__init__.py +0 -0
  37. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/adapters/__init__.py +0 -0
  38. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/adapters/base.py +0 -0
  39. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/adapters/epss.py +0 -0
  40. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/adapters/exploits.py +0 -0
  41. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/adapters/ghsa.py +0 -0
  42. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/adapters/kev.py +0 -0
  43. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/adapters/nvd.py +0 -0
  44. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/adapters/osv.py +0 -0
  45. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/cache.py +0 -0
  46. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/cli.py +0 -0
  47. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/commands.py +0 -0
  48. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/context.py +0 -0
  49. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/data/__init__.py +0 -0
  50. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/data/epss_snapshot.csv.gz +0 -0
  51. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/data/exploit_index.json.gz +0 -0
  52. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/data/kev_snapshot.json.gz +0 -0
  53. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/ingest/__init__.py +0 -0
  54. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/ingest/cve_list.py +0 -0
  55. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/ingest/cyclonedx.py +0 -0
  56. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/ingest/grype.py +0 -0
  57. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/models.py +0 -0
  58. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/output/__init__.py +0 -0
  59. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/output/explain.py +0 -0
  60. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/output/json_out.py +0 -0
  61. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/output/markdown.py +0 -0
  62. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/output/progress.py +0 -0
  63. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/output/render.py +0 -0
  64. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/output/sarif.py +0 -0
  65. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/output/table.py +0 -0
  66. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/pipeline.py +0 -0
  67. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/py.typed +0 -0
  68. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/ssvc/__init__.py +0 -0
  69. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/ssvc/engine.py +0 -0
  70. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/ssvc/tree.py +0 -0
  71. {vulnctl-0.2.1 → vulnctl-0.2.2}/src/vulnctl/ssvc/trees/cisa-deployer-v1.yaml +0 -0
  72. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/conftest.py +0 -0
  73. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/epss/batch.json +0 -0
  74. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/epss/malformed.json +0 -0
  75. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/epss/missing.json +0 -0
  76. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/ghsa/advisory-by-cve.json +0 -0
  77. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/ghsa/advisory-by-ghsa-id.json +0 -0
  78. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/ghsa/malformed.json +0 -0
  79. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/ghsa/not-found-empty-list.json +0 -0
  80. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/ghsa/rate-limited.json +0 -0
  81. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/golden/enrich.json +0 -0
  82. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/golden/enrich.md +0 -0
  83. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/grype/duplicate-layers.json +0 -0
  84. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/grype/ghsa-only.json +0 -0
  85. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/grype/npm-app.json +0 -0
  86. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/kev/catalog.json +0 -0
  87. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/nvd/cve-2021-44228.json +0 -0
  88. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/nvd/multiple-cvss.json +0 -0
  89. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/nvd/not-found.json +0 -0
  90. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/nvd/rejected.json +0 -0
  91. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/nvd/v2-only.json +0 -0
  92. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/osv/cve-2021-44228.json +0 -0
  93. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/osv/malformed.json +0 -0
  94. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/osv/not-found.json +0 -0
  95. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/osv/querybatch-npm-app.json +0 -0
  96. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/osv/querybatch.json +0 -0
  97. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/osv/vuln-cve-record.json +0 -0
  98. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/osv/vuln-ghsa-no-cve.json +0 -0
  99. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/osv/vuln-ghsa-with-cve.json +0 -0
  100. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/osv/vuln-go.json +0 -0
  101. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/osv/vuln-pypi.json +0 -0
  102. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/sarif/sarif-schema-2.1.0.json +0 -0
  103. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/sbom/npm-app-1.6.cdx.json +0 -0
  104. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/sbom/npm-app.cdx.json +0 -0
  105. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/sbom/py-app.cdx.json +0 -0
  106. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/fixtures/trees/toy.yaml +0 -0
  107. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/live/test_live_smoke.py +0 -0
  108. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_adapter_epss.py +0 -0
  109. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_adapter_exploits.py +0 -0
  110. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_adapter_ghsa.py +0 -0
  111. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_adapter_kev.py +0 -0
  112. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_adapter_nvd.py +0 -0
  113. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_adapter_osv.py +0 -0
  114. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_adapters_base.py +0 -0
  115. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_cache.py +0 -0
  116. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_context.py +0 -0
  117. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_ingest_cve_list.py +0 -0
  118. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_ingest_cyclonedx.py +0 -0
  119. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_ingest_grype.py +0 -0
  120. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_models.py +0 -0
  121. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_output_explain.py +0 -0
  122. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_output_json.py +0 -0
  123. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_output_markdown.py +0 -0
  124. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_output_progress.py +0 -0
  125. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_output_sarif.py +0 -0
  126. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_output_table.py +0 -0
  127. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_pipeline.py +0 -0
  128. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_ssvc_cisa_tree.py +0 -0
  129. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_ssvc_engine.py +0 -0
  130. {vulnctl-0.2.1 → vulnctl-0.2.2}/tests/test_ssvc_tree.py +0 -0
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.2] - 2026-08-01
11
+
12
+ ### Fixed
13
+
14
+ - **Root help type column now matches Typer's own spelling.** The generated
15
+ "Options by command" panel derived its type column itself (`PATH`, `TEXT`),
16
+ which disagreed with the per-command panels on Typer 0.27+ — what a fresh
17
+ `pipx install vulnctl` resolves — where Typer prints `<path>`, `<str>`. It
18
+ now delegates to Click's metavar, so both spellings track the installed
19
+ version. Arguments drop the column entirely: their metavar already names
20
+ what they take.
21
+
10
22
  ## [0.2.1] - 2026-08-01
11
23
 
12
24
  ### Added
@@ -130,7 +142,8 @@ that produced it.
130
142
  sign with keyless cosign (OIDC), and publish to PyPI via trusted publishing
131
143
  (no stored token).
132
144
 
133
- [Unreleased]: https://github.com/NokiGuard/vulnctl/compare/v0.2.1...HEAD
145
+ [Unreleased]: https://github.com/NokiGuard/vulnctl/compare/v0.2.2...HEAD
146
+ [0.2.2]: https://github.com/NokiGuard/vulnctl/compare/v0.2.1...v0.2.2
134
147
  [0.2.1]: https://github.com/NokiGuard/vulnctl/compare/v0.2.0...v0.2.1
135
148
  [0.2.0]: https://github.com/NokiGuard/vulnctl/compare/v0.1.0...v0.2.0
136
149
  [0.1.0]: https://github.com/NokiGuard/vulnctl/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vulnctl
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: CLI-first vulnerability prioritization: auditable SSVC verdicts from fused threat intelligence
5
5
  Project-URL: Homepage, https://github.com/NokiGuard/vulnctl
6
6
  Author: Brian Truong
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "vulnctl"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "CLI-first vulnerability prioritization: auditable SSVC verdicts from fused threat intelligence"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -57,21 +57,18 @@ _EXAMPLES: tuple[tuple[str, str], ...] = (
57
57
  )
58
58
 
59
59
 
60
- def _value_hint(param: Any) -> str:
61
- """Render what an option takes: ``PATH``, ``[a|b]``, or "" for a switch."""
62
- if getattr(param, "is_flag", False):
63
- return ""
64
- if param.metavar:
65
- return str(param.metavar)
66
- choices = getattr(param.type, "choices", None)
67
- if choices:
68
- return "[" + "|".join(str(choice) for choice in choices) + "]"
69
- return str(param.type.name).upper()
60
+ def _metavar(param: Any, ctx: Any) -> str:
61
+ """What a parameter takes, spelled the way Typer's own help spells it.
70
62
 
71
-
72
- def _arg_name(param: Any) -> str:
73
- """The metavar an argument is documented under, e.g. ``[VULN_ID...]``."""
74
- return str(param.metavar or str(param.name).upper())
63
+ Delegates to Click rather than deriving it, so the appended panel agrees
64
+ with the per-command panels on whatever version is installed — Typer 0.26
65
+ renders ``PATH``, 0.27 renders ``<path>``. Click gained the ``ctx``
66
+ argument in 8.2; older versions take none.
67
+ """
68
+ try:
69
+ return str(param.make_metavar(ctx=ctx))
70
+ except TypeError:
71
+ return str(param.make_metavar())
75
72
 
76
73
 
77
74
  def _visible_params(cmd: Any, kind: str) -> list[Any]:
@@ -82,28 +79,30 @@ def _visible_params(cmd: Any, kind: str) -> list[Any]:
82
79
  ]
83
80
 
84
81
 
85
- def _usage(path: str, cmd: Any) -> str:
82
+ def _usage(path: str, cmd: Any, ctx: Any) -> str:
86
83
  """The one-line invocation shape, e.g. ``vulnctl enrich [OPTIONS] [VULN_ID...]``."""
87
84
  parts = [path]
88
85
  if _visible_params(cmd, "option"):
89
86
  parts.append("[OPTIONS]")
90
- parts.extend(_arg_name(param) for param in _visible_params(cmd, "argument"))
87
+ parts.extend(_metavar(param, ctx) for param in _visible_params(cmd, "argument"))
91
88
  return " ".join(parts)
92
89
 
93
90
 
94
- def _param_rows(cmd: Any) -> Iterator[tuple[Text, Text, str]]:
91
+ def _param_rows(cmd: Any, ctx: Any) -> Iterator[tuple[Text, Text, str]]:
95
92
  """Yield ``(name, value hint, help)`` for one command's arguments and options."""
96
93
  for param in _visible_params(cmd, "argument"):
94
+ # No type column: an argument's metavar already names what it takes.
97
95
  yield (
98
- Text(f" {_arg_name(param)}", style=_STYLE_OPTION),
99
- Text(str(param.type.name).upper(), style=_STYLE_TYPES),
96
+ Text(f" {_metavar(param, ctx)}", style=_STYLE_OPTION),
97
+ Text(""),
100
98
  str(getattr(param, "help", "") or ""),
101
99
  )
102
100
  for param in _visible_params(cmd, "option"):
103
101
  switch = bool(getattr(param, "is_flag", False))
102
+ hint = "" if switch else _metavar(param, ctx)
104
103
  yield (
105
104
  Text(" " + " ".join(param.opts), style=_STYLE_SWITCH if switch else _STYLE_OPTION),
106
- Text(_value_hint(param), style=_STYLE_TYPES),
105
+ Text(hint, style=_STYLE_TYPES),
107
106
  str(getattr(param, "help", "") or ""),
108
107
  )
109
108
 
@@ -127,7 +126,7 @@ def _leaf_commands(group: Any, prefix: str) -> Iterator[tuple[str, str, Any]]:
127
126
  yield label, name, cmd
128
127
 
129
128
 
130
- def _flags_panel(group: Any, prog: str) -> Panel:
129
+ def _flags_panel(group: Any, prog: str, ctx: Any) -> Panel:
131
130
  """Every command's parameters, generated from the registered Click objects."""
132
131
  table = Table(box=None, show_header=False, pad_edge=False, padding=(0, 1))
133
132
  table.add_column(no_wrap=True) # command / flag
@@ -140,9 +139,9 @@ def _flags_panel(group: Any, prog: str) -> Panel:
140
139
  table.add_row(
141
140
  Text(label, style="bold"),
142
141
  "",
143
- Text(_usage(f"{prog} {label}", cmd), style="dim"),
142
+ Text(_usage(f"{prog} {label}", cmd, ctx), style="dim"),
144
143
  )
145
- for name, hint, help_text in _param_rows(cmd):
144
+ for name, hint, help_text in _param_rows(cmd, ctx):
146
145
  table.add_row(name, hint, help_text)
147
146
 
148
147
  return Panel(
@@ -178,7 +177,7 @@ class HelpfulGroup(TyperGroup):
178
177
  super().format_help(ctx, formatter)
179
178
  prog = str(ctx.find_root().info_name or "vulnctl")
180
179
  console = Console()
181
- console.print(_flags_panel(self, prog))
180
+ console.print(_flags_panel(self, prog, ctx))
182
181
  console.print(_examples_panel())
183
182
  console.print(
184
183
  Padding(
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import re
6
7
  from importlib.metadata import version as pkg_version
7
8
  from pathlib import Path
8
9
 
@@ -77,6 +78,25 @@ def test_root_help_shows_usage_shapes_and_examples() -> None:
77
78
  assert "vulnctl COMMAND --help" in result.output # pointer to per-command detail
78
79
 
79
80
 
81
+ def test_root_help_type_column_matches_per_command_help() -> None:
82
+ # The type column is Click's own metavar, not a hand-derived one, so it
83
+ # tracks whatever spelling the installed Typer uses (0.26 prints `PATH`,
84
+ # 0.27 prints `<path>`) instead of disagreeing with the panels above it.
85
+ def plain(text: str) -> str:
86
+ return re.sub(r"\x1b\[[0-9;]*m", "", text) # CI forces colour on Typer's console
87
+
88
+ def option_row(output: str) -> str:
89
+ # The flag also occurs inside other params' help text; take the row it
90
+ # heads, panel border and indent stripped.
91
+ rows = [ln.lstrip("│ ") for ln in plain(output).splitlines()]
92
+ (row,) = [ln for ln in rows if ln.startswith("--sbom")]
93
+ return row
94
+
95
+ metavar = option_row(runner.invoke(app, ["enrich", "--help"]).output).split()[1]
96
+ assert metavar.strip("<>").lower() == "path"
97
+ assert metavar in option_row(runner.invoke(app, ["--help"]).output)
98
+
99
+
80
100
  def test_help_examples_use_real_flags() -> None:
81
101
  """Worked examples rot silently; assert every flag in them still exists."""
82
102
  known = _subcommand_flags()
@@ -1066,7 +1066,7 @@ wheels = [
1066
1066
 
1067
1067
  [[package]]
1068
1068
  name = "vulnctl"
1069
- version = "0.2.1"
1069
+ version = "0.2.2"
1070
1070
  source = { editable = "." }
1071
1071
  dependencies = [
1072
1072
  { name = "httpx" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes