cctally 1.27.0 → 1.27.1
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.
- package/CHANGELOG.md +8 -0
- package/bin/_cctally_setup.py +7 -3
- package/bin/_cctally_statusline.py +8 -0
- package/bin/_cctally_update.py +3 -3
- package/bin/cctally +13 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.27.1] - 2026-06-04
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Three Linux-only bugs, surfaced by running the full test suite on Linux for the first time** (cctally is developed on macOS, whose case-insensitive filesystem and BSD tooling had masked them): (1) `cctally statusline` with `display.tz = utc` no longer prints a spurious `invalid timezone 'utc'; using 'UTC'` warning on Linux — the lowercase `utc` preference is now normalized to the portable IANA key `UTC` before resolution (Linux's case-sensitive zoneinfo rejects `"utc"` where macOS silently accepts it); (2) the local web dashboard's background update-check thread no longer crashes with `TypeError: 'Event' object is not callable` when the dashboard shuts down — its internal stop-event field was shadowing `threading.Thread._stop`, which `Thread.join()` invokes during teardown; (3) `cctally setup --uninstall` now reliably terminates a running legacy usage-poller on Linux even when it was launched from a long filesystem path — the process-identity check passes `ps -ww` (unlimited-width output) so Linux's default ~80-column truncation can't drop the identifying token and mis-read the live daemon as a dead PID. No action needed on upgrade.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- **Internal (no user-facing change): the full test suite (`bin/cctally-test-all`) is now Linux-portable, and a GitHub-hosted Linux matrix (Ubuntu × Python 3.11/3.12/3.13) runs it on every tag, weekly, and on demand** — closing the cross-version verification gap left when the floor was lowered to 3.11 in 1.27.0. The portability work fixed interpreter- and OS-divergences that only ever ran on macOS before: a `pytest-xdist` timezone leak (one test pinned a Pacific `tzset()` whose libc state outlived `monkeypatch`, flipping later tests' date boundaries — now reset suite-wide via an autouse `conftest` fixture), terminal-width-dependent block-gap rendering under `COLUMNS=80`, Python 3.13's `~~~^^^` traceback caret-anchors in a migration golden, the macOS-`osascript`-vs-Linux-`notify-send` notifier-dispatch assumptions, a BSD-vs-util-linux `script` PTY invocation in the update-banner harness (now a portable `pty.spawn`), a host-config leak in the dashboard-envelope golden, and a detached background update-check that raced fixture teardown. (#132)
|
|
15
|
+
|
|
8
16
|
## [1.27.0] - 2026-06-04
|
|
9
17
|
|
|
10
18
|
### Changed
|
package/bin/_cctally_setup.py
CHANGED
|
@@ -998,11 +998,15 @@ def _legacy_stop_active_poller() -> str:
|
|
|
998
998
|
# Ownership probe: the PID file is at a predictable /tmp path that
|
|
999
999
|
# outlives the daemon on uncleanly exit, and macOS PIDs cycle in a
|
|
1000
1000
|
# narrow space — verify the live process is actually our legacy
|
|
1001
|
-
# poller before signaling.
|
|
1002
|
-
#
|
|
1001
|
+
# poller before signaling. `-o command=` emits the cmdline with no
|
|
1002
|
+
# header on both macOS BSD ps and Linux util-linux ps; `-ww` forces
|
|
1003
|
+
# UNLIMITED width so the cmdline is never truncated. Without it,
|
|
1004
|
+
# Linux util-linux ps clamps the column to ~80 chars (macOS BSD ps
|
|
1005
|
+
# does not), so a poller launched from a long path drops the
|
|
1006
|
+
# "usage-poller.py" token off the end → a false "stale-pid".
|
|
1003
1007
|
try:
|
|
1004
1008
|
probe = subprocess.run(
|
|
1005
|
-
["ps", "-p", str(pid), "-o", "command="],
|
|
1009
|
+
["ps", "-ww", "-p", str(pid), "-o", "command="],
|
|
1006
1010
|
capture_output=True, text=True, timeout=2.0,
|
|
1007
1011
|
)
|
|
1008
1012
|
except (OSError, subprocess.TimeoutExpired):
|
|
@@ -91,6 +91,14 @@ def _resolve_statusline_tz(cli_tz, cfg, warn_once):
|
|
|
91
91
|
tz_name = c._local_tz_name() or "UTC"
|
|
92
92
|
except Exception:
|
|
93
93
|
tz_name = "UTC"
|
|
94
|
+
elif tz_name and tz_name.lower() == "utc":
|
|
95
|
+
# Canonical "utc" (the value get_display_tz_pref / normalize_display_tz_value
|
|
96
|
+
# emit) -> the portable IANA key "UTC". macOS's case-insensitive
|
|
97
|
+
# filesystem resolves ZoneInfo("utc") to UTC, but Linux's case-sensitive
|
|
98
|
+
# /usr/share/zoneinfo raises ZoneInfoNotFoundError, which would emit a
|
|
99
|
+
# spurious "invalid timezone 'utc'" warning below. Mirrors
|
|
100
|
+
# resolve_display_tz, which maps canonical "utc" -> ZoneInfo("Etc/UTC").
|
|
101
|
+
tz_name = "UTC"
|
|
94
102
|
try:
|
|
95
103
|
ZoneInfo(tz_name)
|
|
96
104
|
except (ZoneInfoNotFoundError, Exception):
|
package/bin/_cctally_update.py
CHANGED
|
@@ -1890,13 +1890,13 @@ class _DashboardUpdateCheckThread(threading.Thread):
|
|
|
1890
1890
|
snapshot_ref: "_SnapshotRef | None" = None,
|
|
1891
1891
|
) -> None:
|
|
1892
1892
|
super().__init__(name="cctally-update-check")
|
|
1893
|
-
self.
|
|
1893
|
+
self._stop_event = stop_event
|
|
1894
1894
|
self._hub = hub
|
|
1895
1895
|
self._ref = snapshot_ref
|
|
1896
1896
|
|
|
1897
1897
|
def run(self) -> None:
|
|
1898
1898
|
c = _cctally()
|
|
1899
|
-
while not self.
|
|
1899
|
+
while not self._stop_event.is_set():
|
|
1900
1900
|
try:
|
|
1901
1901
|
# Self-heal runs every tick (every 30 min by default),
|
|
1902
1902
|
# NOT gated by `_is_update_check_due`'s 24h TTL. Catches
|
|
@@ -1938,7 +1938,7 @@ class _DashboardUpdateCheckThread(threading.Thread):
|
|
|
1938
1938
|
)
|
|
1939
1939
|
except Exception:
|
|
1940
1940
|
pass
|
|
1941
|
-
self.
|
|
1941
|
+
self._stop_event.wait(c.UPDATE_DASHBOARD_CHECK_POLL_S)
|
|
1942
1942
|
|
|
1943
1943
|
|
|
1944
1944
|
def cmd_update(args) -> int:
|
package/bin/cctally
CHANGED
|
@@ -2720,7 +2720,19 @@ def _post_command_update_hooks(command: str | None, args) -> None:
|
|
|
2720
2720
|
``_spawn_background_update_check`` here would create ``config.json``,
|
|
2721
2721
|
``update-state.json``, and ``update.log`` on a fresh install where
|
|
2722
2722
|
the existing-install gate already makes the symlink work a no-op.
|
|
2723
|
-
Same rationale as doctor.
|
|
2723
|
+
Same rationale as doctor.
|
|
2724
|
+
|
|
2725
|
+
Test/CI seam: ``CCTALLY_DISABLE_UPDATE_CHECK`` short-circuits the whole
|
|
2726
|
+
hook (mirrors ``CCTALLY_DISABLE_DEV_AUTODETECT``). The background
|
|
2727
|
+
``_spawn_background_update_check`` is a DETACHED process that
|
|
2728
|
+
``mkdir``s APP_DIR to write ``update-state.json`` / ``update.log``; a
|
|
2729
|
+
fixture harness that runs a command and then asserts on APP_DIR (e.g.
|
|
2730
|
+
``cctally-setup-test``'s uninstall-purge "data dir is gone" check)
|
|
2731
|
+
otherwise races that detached writer re-creating the dir after the
|
|
2732
|
+
command returns. Setting this env var makes such harnesses
|
|
2733
|
+
deterministic without disabling the feature for real users."""
|
|
2734
|
+
if os.environ.get("CCTALLY_DISABLE_UPDATE_CHECK"):
|
|
2735
|
+
return
|
|
2724
2736
|
if command == "setup" and getattr(args, "uninstall", False):
|
|
2725
2737
|
return
|
|
2726
2738
|
if command == "doctor":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cctally",
|
|
3
|
-
"version": "1.27.
|
|
3
|
+
"version": "1.27.1",
|
|
4
4
|
"description": "Claude Code usage tracker and local dashboard for Pro/Max subscription limits - weekly cost-per-percent trend, quota forecasts, threshold alerts. ccusage-compatible.",
|
|
5
5
|
"homepage": "https://github.com/omrikais/cctally",
|
|
6
6
|
"repository": {
|