Skip to content

Commit 47cb85c

Browse files
tob-scott-aclaude
andcommitted
feat(cli): --version, -V, and version subcommand (closes #26)
There was no way to discover the installed Trailmark version from the CLI. Adds three equivalent paths, all printing `trailmark <version>`: - `trailmark --version` - `trailmark -V` - `trailmark version` The long/short flags use argparse's built-in action="version" which prints and exits with status 0. The subcommand form dispatches through main() and uses the same version string. Also bumps __version__ and pyproject.toml to 0.2.2 and refreshes the lockfile. 5 new tests in test_cli_parser.py cover each form end-to-end. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7520ce8 commit 47cb85c

6 files changed

Lines changed: 81 additions & 6 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,10 @@ Requires Python &ge; 3.12.
243243
## Usage
244244

245245
```bash
246+
# Report the installed version
247+
trailmark --version # or: trailmark -V
248+
trailmark version # subcommand form
249+
246250
# Full JSON graph (Python, the default)
247251
trailmark analyze path/to/project
248252

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "trailmark"
7-
version = "0.2.1"
7+
version = "0.2.2"
88
description = "Parse source code into a queryable graph of functions, classes, calls, and semantic annotations"
99
readme = "README.md"
1010
requires-python = ">=3.12"

src/trailmark/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Trailmark: Parse source code into queryable graphs for security analysis."""
22

3-
__version__ = "0.2.1"
3+
__version__ = "0.2.2"

src/trailmark/cli.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,31 @@
88
from pathlib import Path
99
from typing import Any
1010

11+
from trailmark import __version__
1112
from trailmark.query.api import QueryEngine
1213

14+
_VERSION_STRING = f"trailmark {__version__}"
15+
1316

1417
def build_parser() -> argparse.ArgumentParser:
1518
"""Construct the Trailmark CLI's argparse tree."""
1619
parser = argparse.ArgumentParser(
1720
prog="trailmark",
18-
description="Parse source code into queryable graphs",
21+
description=f"Parse source code into queryable graphs (v{__version__})",
22+
)
23+
parser.add_argument(
24+
"--version",
25+
"-V",
26+
action="version",
27+
version=_VERSION_STRING,
1928
)
2029
subparsers = parser.add_subparsers(dest="command")
2130

31+
subparsers.add_parser(
32+
"version",
33+
help="Print the Trailmark version and exit",
34+
)
35+
2236
analyze = subparsers.add_parser(
2337
"analyze",
2438
help="Analyze a directory and output the code graph",
@@ -133,6 +147,9 @@ def main() -> None:
133147
parser.print_help()
134148
sys.exit(1)
135149

150+
if args.command == "version":
151+
print(_VERSION_STRING)
152+
return
136153
if args.command == "analyze":
137154
_run_analyze(args)
138155
elif args.command == "augment":

tests/test_cli_parser.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,48 @@ def test_command_dest(self, parser: argparse.ArgumentParser) -> None:
5858
def test_all_subcommands_registered(
5959
self, subparsers_map: dict[str, argparse.ArgumentParser]
6060
) -> None:
61-
assert set(subparsers_map) == {"analyze", "augment", "entrypoints", "diff"}
61+
assert set(subparsers_map) == {
62+
"analyze",
63+
"augment",
64+
"entrypoints",
65+
"diff",
66+
"version",
67+
}
68+
69+
70+
class TestVersionFlag:
71+
"""Regression for issue #26 — the CLI must expose its version."""
72+
73+
def test_long_flag_prints_and_exits(
74+
self, parser: argparse.ArgumentParser, capsys: pytest.CaptureFixture[str]
75+
) -> None:
76+
import trailmark
77+
78+
with pytest.raises(SystemExit) as excinfo:
79+
parser.parse_args(["--version"])
80+
assert excinfo.value.code == 0
81+
out = capsys.readouterr().out
82+
assert out.strip() == f"trailmark {trailmark.__version__}"
83+
84+
def test_short_flag_prints_and_exits(
85+
self, parser: argparse.ArgumentParser, capsys: pytest.CaptureFixture[str]
86+
) -> None:
87+
import trailmark
88+
89+
with pytest.raises(SystemExit) as excinfo:
90+
parser.parse_args(["-V"])
91+
assert excinfo.value.code == 0
92+
out = capsys.readouterr().out
93+
assert out.strip() == f"trailmark {trailmark.__version__}"
94+
95+
def test_version_subcommand_registered(
96+
self, subparsers_map: dict[str, argparse.ArgumentParser]
97+
) -> None:
98+
assert "version" in subparsers_map
99+
100+
def test_version_subcommand_parses(self, parser: argparse.ArgumentParser) -> None:
101+
args = parser.parse_args(["version"])
102+
assert args.command == "version"
62103

63104

64105
class TestAnalyzeSubparser:
@@ -206,3 +247,16 @@ def test_entrypoints_json_flag(self, parser: argparse.ArgumentParser) -> None:
206247
def test_augment_repeatable_sarif(self, parser: argparse.ArgumentParser) -> None:
207248
args = parser.parse_args(["augment", "p", "--sarif", "a.sarif", "--sarif", "b.sarif"])
208249
assert args.sarif == ["a.sarif", "b.sarif"]
250+
251+
252+
class TestVersionSubcommandExecution:
253+
def test_main_version_prints_version(
254+
self, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
255+
) -> None:
256+
import trailmark
257+
from trailmark.cli import main
258+
259+
monkeypatch.setattr("sys.argv", ["trailmark", "version"])
260+
main()
261+
out = capsys.readouterr().out
262+
assert out.strip() == f"trailmark {trailmark.__version__}"

uv.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)