Skip to content

Commit 906fb61

Browse files
authored
Merge pull request #9 from dedev-llc/feat/update-notifier
Add update notification (like Claude Code)
2 parents 2e76dc7 + 069e009 commit 906fb61

1 file changed

Lines changed: 99 additions & 0 deletions

File tree

src/rpr/cli.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import subprocess
2424
import sys
2525
import time
26+
import threading
2627
import urllib.error
2728
import urllib.request
2829
from pathlib import Path
@@ -80,6 +81,87 @@ def load_config() -> dict:
8081
return config
8182

8283

84+
# ---------------------------------------------------------------------------
85+
# Update check
86+
# ---------------------------------------------------------------------------
87+
88+
UPDATE_CHECK_INTERVAL = 3600 # seconds between PyPI checks
89+
90+
91+
def _update_cache_path() -> Path:
92+
cache_dir = Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache")
93+
return cache_dir / "rpr" / "update-check.json"
94+
95+
96+
def _version_tuple(v: str) -> tuple[int, ...]:
97+
try:
98+
return tuple(int(x) for x in v.split("."))
99+
except (ValueError, AttributeError):
100+
return (0,)
101+
102+
103+
def check_for_update() -> str | None:
104+
"""Check PyPI for a newer version. Returns a notification string or None.
105+
106+
Results are cached for UPDATE_CHECK_INTERVAL seconds so most runs
107+
hit a local file instead of the network.
108+
"""
109+
from rpr import __version__
110+
111+
cache_path = _update_cache_path()
112+
latest = None
113+
114+
# Use cached result if fresh enough
115+
try:
116+
if cache_path.exists():
117+
cache = json.loads(cache_path.read_text())
118+
if time.time() - cache.get("checked_at", 0) < UPDATE_CHECK_INTERVAL:
119+
latest = cache.get("latest")
120+
if latest and _version_tuple(latest) > _version_tuple(__version__):
121+
return _update_msg(__version__, latest)
122+
return None
123+
except (json.JSONDecodeError, OSError):
124+
pass
125+
126+
# Fetch from PyPI
127+
try:
128+
req = urllib.request.Request(
129+
"https://pypi.org/pypi/rpr/json",
130+
headers={"Accept": "application/json"},
131+
)
132+
with urllib.request.urlopen(req, timeout=5) as resp:
133+
data = json.loads(resp.read().decode("utf-8"))
134+
latest = data["info"]["version"]
135+
except Exception:
136+
return None
137+
138+
# Cache the result
139+
try:
140+
cache_path.parent.mkdir(parents=True, exist_ok=True)
141+
cache_path.write_text(json.dumps({
142+
"checked_at": time.time(),
143+
"latest": latest,
144+
}))
145+
except OSError:
146+
pass
147+
148+
if latest and _version_tuple(latest) > _version_tuple(__version__):
149+
return _update_msg(__version__, latest)
150+
return None
151+
152+
153+
def _update_msg(current: str, latest: str) -> str:
154+
line1 = f" Update available: {current}{latest} "
155+
line2 = " pip install -U rpr "
156+
width = max(len(line1), len(line2))
157+
return (
158+
f"\n{'─' * width}\n"
159+
f"│{line1:<{width}}\n"
160+
f"│{line2:<{width}}\n"
161+
f"╰{'─' * width}╯"
162+
)
163+
164+
83165
# ---------------------------------------------------------------------------
84166
# Review depth modes
85167
# ---------------------------------------------------------------------------
@@ -791,6 +873,15 @@ def main():
791873
args = parser.parse_args()
792874
config = load_config()
793875

876+
# Start update check in background (non-blocking)
877+
update_result: list[str | None] = [None]
878+
879+
def _bg_update_check():
880+
update_result[0] = check_for_update()
881+
882+
update_thread = threading.Thread(target=_bg_update_check, daemon=True)
883+
update_thread.start()
884+
794885
if args.model:
795886
config["model"] = args.model
796887

@@ -931,6 +1022,9 @@ def main():
9311022
print(f"\n📄 {c['path']}:{c['line']}")
9321023
print(f" {c['body']}")
9331024
print()
1025+
update_thread.join(timeout=2)
1026+
if update_result[0]:
1027+
print(update_result[0], file=sys.stderr)
9341028
return
9351029

9361030
if args.comment_only:
@@ -948,6 +1042,11 @@ def main():
9481042

9491043
print(f"🔗 {pr_info.get('url', '')}", file=sys.stderr)
9501044

1045+
# Show update notification (if check finished in time)
1046+
update_thread.join(timeout=2)
1047+
if update_result[0]:
1048+
print(update_result[0], file=sys.stderr)
1049+
9511050

9521051
if __name__ == "__main__":
9531052
main()

0 commit comments

Comments
 (0)