Skip to content

Commit 0b8c7e7

Browse files
phurynclaude
andcommitted
release: v1.5.3 — pyproject.toml for uv tool / pipx install (#144)
Merge DEV: adds pyproject.toml so `uv tool install git+…` / pipx install the `claude-usage` command without a clone (#144, thanks @jamesbraza). Packaging only — stdlib-only at runtime, version read dynamically from scanner.VERSION, clone + `python cli.py` path unchanged. Also carries the bump-formula.sh release helper added earlier on DEV. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 parents 0757d5f + 039e037 commit 0b8c7e7

9 files changed

Lines changed: 142 additions & 5 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,10 @@ __pycache__/
22
server.log
33
logs/
44

5+
# Python packaging build artifacts (uv build / pip / setuptools) — see pyproject.toml
6+
dist/
7+
build/
8+
*.egg-info/
9+
510
.pr-candidates.md
611
/tmp/

AGENTS.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ This applies to all agents working on this repo, not just Claude Code.
109109

110110
The release flow:
111111
1. While work accumulates on `DEV`, the `## vX.Y.Z — TBD` heading at the top of `CHANGELOG.md` collects bullets. (For automated triage runs, see the routine note below.)
112-
2. When the maintainer is ready to release, they finalize the heading (`TBD` → today's date), **bump both `scanner.VERSION` and `vscode-extension/package.json`'s `version` to match the CHANGELOG version** (all three ship in lockstep — the extension bundles the Python sources, and `scanner.VERSION` is the runtime version reported by `cli.py --version` and the dashboard footer since the CHANGELOG isn't bundled into the `.vsix`), merge `DEV → main` with `merge --no-ff` (so the release boundary is visible in `git log main`), and push `main`. A parity test (`tests/test_version.py`) fails the suite if the three drift, so in practice they're bumped together on `DEV` when the version heading is written.
112+
2. When the maintainer is ready to release, they finalize the heading (`TBD` → today's date), **bump both `scanner.VERSION` and `vscode-extension/package.json`'s `version` to match the CHANGELOG version** (all three ship in lockstep — the extension bundles the Python sources, and `scanner.VERSION` is the runtime version reported by `cli.py --version` and the dashboard footer since the CHANGELOG isn't bundled into the `.vsix`), **run [`scripts/bump-formula.sh`](scripts/bump-formula.sh) on `DEV` to repoint the Homebrew formula at the previous release's tag tarball** (see "Homebrew formula and self-referential SHA" below — this is a plain `DEV` commit that reaches brew users via this same merge, so it never touches `main` directly), merge `DEV → main` with `merge --no-ff` (so the release boundary is visible in `git log main`), and push `main`. A parity test (`tests/test_version.py`) fails the suite if the three drift, so in practice they're bumped together on `DEV` when the version heading is written.
113113
3. [`.github/workflows/tag-on-merge.yml`](.github/workflows/tag-on-merge.yml) fires on the push, sees the new `## vX.Y.Z` heading in the CHANGELOG diff, and:
114114
- creates a lightweight tag at the merge commit (**no `git tag` step for the maintainer**), then
115115
- builds the VS Code extension `.vsix` and publishes a **GitHub Release** for that tag — the matching CHANGELOG section as the release notes, the built `.vsix` attached as a release asset.
@@ -152,7 +152,11 @@ Patch (`Z` increments) is the default for any release. Bump minor (`Y`) when a n
152152

153153
The Homebrew formula at `Formula/claude-usage.rb` lives inside this same repo. Be careful when bumping it: if the formula's `url` points at a tarball that **contains the formula itself with that sha256**, the sha256 is self-referential and uncomputable. Practical rule: a release's formula must point at the **previous** release's tarball, never its own. In v1.1.1 the formula points at v1.1.0's commit-SHA tarball, so brew users installing v1.1.1's formula receive v1.1.0 code — that's the trade-off of keeping the formula in-tree.
154154

155-
Now that the auto-tag workflow exists, future formula bumps can use the tag-tarball URL (`archive/refs/tags/vX.Y.Z.tar.gz`) instead of commit SHAs — stabler and shorter — as long as the tag-tarball pointed at is from the *previous* release.
155+
Now that the auto-tag workflow exists, formula bumps use the tag-tarball URL (`archive/refs/tags/vX.Y.Z.tar.gz`) instead of commit SHAs — stabler and shorter — as long as the tag-tarball pointed at is from the *previous* release.
156+
157+
**Automate the bump; never hand-edit the three pinned lines.** [`scripts/bump-formula.sh`](scripts/bump-formula.sh) fetches a released tag's tarball, computes its `sha256`, and rewrites the `url` / `version` / `sha256` lines (leaving `head`, `homepage`, and comments alone). With no argument it targets the latest `v*` tag on origin — run during release prep, before the new tag exists, that's the previous release, exactly what the self-referential rule requires.
158+
159+
**Why a `DEV`-only commit is enough — and why it's one release behind.** Brew reads the formula from the tap's default branch (`main`) HEAD, never from `DEV` or a tag. So the bump is a normal `DEV` commit that becomes visible to brew users only when `DEV → main` merges — which is precisely at the next release. That timing is the point: it lets us pin at the just-frozen *previous* tag and ship it with the release, so **brew always tracks one release behind, advancing automatically each release**, with no direct push to `main` (dodging `main`'s branch protection) and no hand-editing to forget. The manual, forget-prone bump is what let the pin silently rot at v1.1.0 from v1.1.1 through v1.5.0; v1.5.2 caught it up to v1.5.1 and wired in this routine. The only thing a `DEV`-only bump can't do is move brew *between* releases — you'd need a release (a `DEV → main` merge) for that, which is fine because the pin only ever changes at release boundaries anyway.
156160

157161
## Weekly triage routine
158162

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## v1.5.3 — 2026-07-01
4+
5+
### Packaging
6+
7+
- Added a `pyproject.toml` so the tool installs with `uv tool install git+https://github.com/phuryn/claude-usage` (or `pipx install …`) — no clone needed (#144, thanks @jamesbraza). Packaging only: `[project.dependencies]` is empty so the tool stays stdlib-only at runtime, the version is read dynamically from `scanner.VERSION` (still the single source of truth), and the clone + `python cli.py` path is unchanged. The `cli.py` `__main__` block was lifted into a `main()` function to serve as the `claude-usage` console entry point.
8+
39
## v1.5.2 — 2026-07-01
410

511
### Packaging

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ claude-usage dashboard
5252
5353
After install, the `claude-usage` command is on your `PATH` and accepts the same subcommands as `python cli.py` (`scan`, `today`, `stats`, `dashboard`).
5454

55+
### Any OS (uv tool / pipx)
56+
```
57+
uv tool install git+https://github.com/phuryn/claude-usage
58+
claude-usage dashboard
59+
```
60+
61+
Installs the `claude-usage` command without a clone (works with [`pipx`](https://pipx.pypa.io/) too: `pipx install git+https://github.com/phuryn/claude-usage`). The tool stays dependency-free — this only adds packaging metadata, no third-party runtime deps (#144).
62+
5563
### macOS / Linux (clone)
5664
```
5765
git clone https://github.com/phuryn/claude-usage

cli.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,8 @@ def parse_named_arg(args, flag):
459459
return args[i + 1]
460460
return None
461461

462-
if __name__ == "__main__":
462+
def main():
463+
"""Console entry point (``claude-usage``) and ``python cli.py`` dispatch."""
463464
if len(sys.argv) >= 2 and sys.argv[1] in ("--version", "-V", "version"):
464465
print(VERSION)
465466
sys.exit(0)
@@ -484,3 +485,7 @@ def parse_named_arg(args, flag):
484485
cmd_scan(projects_dir=projects_dir)
485486
else:
486487
COMMANDS[command]()
488+
489+
490+
if __name__ == "__main__":
491+
main()

pyproject.toml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Packaging metadata so the tool can be installed with `uv tool install` / `pipx`
2+
# without a clone (#144). This is packaging only — the tool stays stdlib-only at
3+
# runtime ([project.dependencies] is empty) and the clone + `python cli.py` path
4+
# is unaffected.
5+
#
6+
# The version is read dynamically from scanner.VERSION so it stays a single
7+
# source of truth (kept in lockstep with the CHANGELOG heading and the VS Code
8+
# extension's package.json by tests/test_version.py) — do NOT hardcode it here.
9+
[build-system]
10+
requires = ["setuptools>=61.0"]
11+
build-backend = "setuptools.build_meta"
12+
13+
[project]
14+
name = "claude-usage"
15+
dynamic = ["version"]
16+
description = "Token, cost, and session dashboard for Claude Code usage"
17+
readme = "README.md"
18+
requires-python = ">=3.8"
19+
license = { text = "MIT" }
20+
authors = [{ name = "Paweł Huryn" }]
21+
keywords = ["claude", "claude-code", "usage", "tokens", "cost", "dashboard"]
22+
dependencies = []
23+
24+
[project.urls]
25+
Homepage = "https://github.com/phuryn/claude-usage"
26+
Issues = "https://github.com/phuryn/claude-usage/issues"
27+
28+
[project.scripts]
29+
claude-usage = "cli:main"
30+
31+
# Three top-level modules, no package directory — mirror the flat repo layout.
32+
[tool.setuptools]
33+
py-modules = ["cli", "scanner", "dashboard"]
34+
35+
[tool.setuptools.dynamic]
36+
version = { attr = "scanner.VERSION" }

scanner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# runtime version has to live here as a constant. Keep this in lockstep with the
1616
# top CHANGELOG heading and vscode-extension/package.json (a parity test guards
1717
# all three; see tests/test_version.py).
18-
VERSION = "1.5.2"
18+
VERSION = "1.5.3"
1919

2020
PROJECTS_DIR = Path.home() / ".claude" / "projects"
2121
XCODE_PROJECTS_DIR = Path.home() / "Library" / "Developer" / "Xcode" / "CodingAssistant" / "ClaudeAgentConfig" / "projects"

scripts/bump-formula.sh

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Repoint the in-tree Homebrew formula at a released tag's source tarball.
5+
#
6+
# WHY THIS EXISTS
7+
# Formula/claude-usage.rb ships *inside* the repo it installs, so it can never
8+
# hash its own release tarball — the sha256 would have to be the hash of a file
9+
# that contains that very hash (self-referential, uncomputable). So the formula
10+
# must point at an ALREADY-FROZEN tag: the previous release.
11+
#
12+
# Brew reads the formula from the tap's default branch (main), never from DEV
13+
# or a tag. Run this on DEV while prepping a release; the change reaches brew
14+
# users through the normal DEV -> main release merge. It never touches main
15+
# directly, so it dodges main's branch protection. Net effect: brew tracks one
16+
# release behind, and it advances automatically every release instead of being
17+
# hand-edited (which is how the pin silently rotted at v1.1.0 for months).
18+
#
19+
# USAGE
20+
# scripts/bump-formula.sh [TAG]
21+
# TAG tag to pin at (e.g. v1.5.2). Defaults to the latest v* tag on origin,
22+
# which — run during release prep, before the new tag exists — is the
23+
# previous release. Review the diff, then commit on DEV.
24+
#
25+
# See AGENTS.md "Homebrew formula and self-referential SHA".
26+
27+
REPO_SLUG="phuryn/claude-usage"
28+
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
29+
FORMULA="$REPO_DIR/Formula/claude-usage.rb"
30+
31+
TAG="${1:-}"
32+
if [ -z "$TAG" ]; then
33+
echo "🔎 No tag given — finding the latest v* tag on origin..."
34+
git -C "$REPO_DIR" fetch --tags --quiet origin || true
35+
TAG="$(git -C "$REPO_DIR" ls-remote --tags --refs origin 'v*' \
36+
| awk -F/ '{print $NF}' | sort -V | tail -n1)"
37+
fi
38+
[ -n "$TAG" ] || { echo "❌ Could not determine a tag to pin at." >&2; exit 1; }
39+
40+
VERSION="${TAG#v}"
41+
URL="https://github.com/${REPO_SLUG}/archive/refs/tags/${TAG}.tar.gz"
42+
43+
echo "📌 Pinning formula at ${TAG}"
44+
echo " ${URL}"
45+
46+
TARBALL="$(mktemp)"
47+
trap 'rm -f "$TARBALL"' EXIT
48+
curl -fsSL "$URL" -o "$TARBALL"
49+
50+
if command -v sha256sum >/dev/null 2>&1; then
51+
SHA="$(sha256sum "$TARBALL" | awk '{print $1}')"
52+
else
53+
SHA="$(shasum -a 256 "$TARBALL" | awk '{print $1}')"
54+
fi
55+
[ -n "$SHA" ] || { echo "❌ Could not compute sha256." >&2; exit 1; }
56+
echo "🔑 sha256 ${SHA}"
57+
58+
# Rewrite exactly the three pinned lines. Anchored to `^<ws>url `/`version `/
59+
# `sha256 ` so the `head`, `homepage`, and comment lines are left untouched
60+
# (there is exactly one of each pinned line in the stanza).
61+
tmp="$(mktemp)"
62+
sed -E \
63+
-e "s|^([[:space:]]*)url .*|\1url \"${URL}\"|" \
64+
-e "s|^([[:space:]]*)version .*|\1version \"${VERSION}\"|" \
65+
-e "s|^([[:space:]]*)sha256 .*|\1sha256 \"${SHA}\"|" \
66+
"$FORMULA" > "$tmp"
67+
mv "$tmp" "$FORMULA"
68+
69+
echo
70+
echo "✅ Updated $(git -C "$REPO_DIR" rev-parse --show-prefix 2>/dev/null)Formula/claude-usage.rb:"
71+
grep -E '^[[:space:]]*(url|version|sha256) ' "$FORMULA" | sed 's/^/ /'
72+
echo
73+
echo "➡ Review, then commit on DEV. It ships to brew users at the next DEV -> main release merge."

vscode-extension/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "claude-usage-phuryn",
33
"displayName": "Claude Code Usage by Paweł Huryn",
44
"description": "Embed your Claude Code usage dashboard (token counts, costs, sessions, projects) directly inside VS Code. Reads local JSONL transcripts, no API calls.",
5-
"version": "1.5.2",
5+
"version": "1.5.3",
66
"publisher": "PawelHuryn",
77
"author": {
88
"name": "Paweł Huryn",

0 commit comments

Comments
 (0)