Skip to content

Commit 08dc9a9

Browse files
authored
Merge pull request #10 from dedev-llc/feat/update-notifier
Fix npm version import and bump to v0.1.4
2 parents 7924a4c + de2f613 commit 08dc9a9

5 files changed

Lines changed: 133 additions & 5 deletions

File tree

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ rpr 42 --comment-only # Post as single comment (no inline)
3939
rpr 42 --repo owner/repo # Specify repo explicitly
4040
rpr 42 --model claude-sonnet-4-6 # Override model
4141
rpr 42 -v # Verbose mode (debug)
42+
43+
# Review depth:
44+
rpr 42 --depth quick # Blockers & security only (fast)
45+
rpr 42 --depth thorough # Deep: architecture, edge cases, scale
46+
rpr 42 # Default depth (balanced)
47+
48+
# Self-update:
49+
rpr update # Update to latest version
4250
```
4351

4452
### Recommended Workflow
@@ -55,6 +63,22 @@ rpr 42 -v # Verbose mode (debug)
5563

5664
3. Optionally tweak a word or two in the posted review for your personal touch.
5765

66+
### Review Depth
67+
68+
Control how deep the review goes with `--depth` / `-d`:
69+
70+
| Depth | Flag | What gets flagged |
71+
|---|---|---|
72+
| **quick** | `-d quick` | Production bugs, security vulnerabilities, data loss risks, major architectural violations. Skips nits and style. |
73+
| **default** | *(omit flag)* | Bugs, security, performance, error handling, concurrency, resource leaks. Skips style preferences and obvious suggestions. |
74+
| **thorough** | `-d thorough` | Everything in default, plus architecture fit, edge cases, naming clarity, API design, testability, and performance at scale. |
75+
76+
```bash
77+
rpr 42 -d quick # Quick scan before a fast merge
78+
rpr 42 # Standard review (default)
79+
rpr 42 -d thorough # Critical code path — go deep
80+
```
81+
5882
## Configuration
5983

6084
`rpr` ships with sensible defaults. To override them, drop a config file at one of these paths (first match wins):

npm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@dedev-llc/rpr",
3-
"version": "0.1.3",
3+
"version": "0.1.4",
44
"description": "Stealth PR reviewer — looks like you wrote every word.",
55
"keywords": [
66
"pr",

pyproject.toml

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

77
[project]
88
name = "rpr"
9-
version = "0.1.3"
9+
version = "0.1.4"
1010
description = "Stealth PR reviewer — looks like you wrote every word."
1111
readme = "README.md"
1212
license = { file = "LICENSE" }

src/rpr/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""rpr — stealth PR reviewer."""
22

3-
__version__ = "0.1.3"
3+
__version__ = "0.1.4"

src/rpr/cli.py

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
rpr 42 --dry-run # Print review to terminal, don't post
1212
rpr 42 --approve # Post review + approve the PR
1313
rpr 42 --request-changes # Post review + request changes
14+
rpr update # Self-update to latest version
1415
"""
1516

1617
# Postpones evaluation of annotations so PEP 604 syntax (e.g. `str | None`)
@@ -28,6 +29,22 @@
2829
import urllib.request
2930
from pathlib import Path
3031

32+
33+
def _get_version() -> str:
34+
"""Return the package version, with a fallback for npm-installed layout."""
35+
try:
36+
from rpr import __version__
37+
return __version__
38+
except ImportError:
39+
# npm shim runs cli.py directly; __init__.py sits alongside it in lib/
40+
init_file = Path(__file__).parent / "__init__.py"
41+
if init_file.exists():
42+
for line in init_file.read_text().splitlines():
43+
if line.startswith("__version__"):
44+
return line.split("=", 1)[1].strip().strip('"').strip("'")
45+
return "unknown"
46+
47+
3148
# ---------------------------------------------------------------------------
3249
# Config
3350
# ---------------------------------------------------------------------------
@@ -106,7 +123,7 @@ def check_for_update() -> str | None:
106123
Results are cached for UPDATE_CHECK_INTERVAL seconds so most runs
107124
hit a local file instead of the network.
108125
"""
109-
from rpr import __version__
126+
__version__ = _get_version()
110127

111128
cache_path = _update_cache_path()
112129
latest = None
@@ -152,7 +169,7 @@ def check_for_update() -> str | None:
152169

153170
def _update_msg(current: str, latest: str) -> str:
154171
line1 = f" Update available: {current}{latest} "
155-
line2 = " pip install -U rpr "
172+
line2 = " Run: rpr update "
156173
width = max(len(line1), len(line2))
157174
return (
158175
f"\n{'─' * width}\n"
@@ -162,6 +179,88 @@ def _update_msg(current: str, latest: str) -> str:
162179
)
163180

164181

182+
def _detect_update_command() -> list[str]:
183+
"""Detect how rpr was installed and return the appropriate update command."""
184+
# Check pipx
185+
try:
186+
result = subprocess.run(
187+
["pipx", "list", "--short"],
188+
capture_output=True, text=True,
189+
)
190+
if result.returncode == 0 and "rpr " in result.stdout:
191+
return ["pipx", "upgrade", "rpr"]
192+
except (FileNotFoundError, PermissionError):
193+
pass
194+
195+
# Check brew
196+
try:
197+
result = subprocess.run(
198+
["brew", "list", "rpr"],
199+
capture_output=True, text=True,
200+
)
201+
if result.returncode == 0:
202+
return ["brew", "upgrade", "rpr"]
203+
except (FileNotFoundError, PermissionError):
204+
pass
205+
206+
# Check npm (script lives inside node_modules or npm prefix)
207+
script_path = str(Path(sys.argv[0]).resolve())
208+
if "node_modules" in script_path or "/npm/" in script_path:
209+
return ["npm", "update", "-g", "@dedev-llc/rpr"]
210+
211+
# Default to pip
212+
return [sys.executable, "-m", "pip", "install", "-U", "rpr"]
213+
214+
215+
def _fetch_latest_version() -> str:
216+
"""Fetch latest version string from PyPI. Raises on failure."""
217+
req = urllib.request.Request(
218+
"https://pypi.org/pypi/rpr/json",
219+
headers={"Accept": "application/json"},
220+
)
221+
with urllib.request.urlopen(req, timeout=10) as resp:
222+
data = json.loads(resp.read().decode("utf-8"))
223+
return data["info"]["version"]
224+
225+
226+
def handle_update():
227+
"""Self-update rpr to the latest version."""
228+
__version__ = _get_version()
229+
230+
print(f"rpr v{__version__}", file=sys.stderr)
231+
print("Checking for updates...", file=sys.stderr)
232+
233+
try:
234+
latest = _fetch_latest_version()
235+
except Exception as e:
236+
print(f"❌ Failed to check PyPI: {e}", file=sys.stderr)
237+
sys.exit(1)
238+
239+
if _version_tuple(latest) <= _version_tuple(__version__):
240+
print(f"✅ Already up to date.", file=sys.stderr)
241+
return
242+
243+
print(f"Updating: {__version__}{latest}", file=sys.stderr)
244+
245+
cmd = _detect_update_command()
246+
print(f"Running: {' '.join(cmd)}\n", file=sys.stderr)
247+
248+
result = subprocess.run(cmd)
249+
if result.returncode != 0:
250+
print(f"\n❌ Update failed (exit code {result.returncode})", file=sys.stderr)
251+
sys.exit(1)
252+
253+
print(f"\n✅ Updated to v{latest}", file=sys.stderr)
254+
255+
# Clear the update cache so the notification doesn't linger
256+
try:
257+
cache = _update_cache_path()
258+
if cache.exists():
259+
cache.unlink()
260+
except OSError:
261+
pass
262+
263+
165264
# ---------------------------------------------------------------------------
166265
# Review depth modes
167266
# ---------------------------------------------------------------------------
@@ -851,6 +950,11 @@ def parse_review(raw: str) -> dict:
851950

852951

853952
def main():
953+
# Handle `rpr update` before argparse (which expects an int positional)
954+
if len(sys.argv) >= 2 and sys.argv[1] == "update":
955+
handle_update()
956+
return
957+
854958
parser = argparse.ArgumentParser(
855959
prog="rpr",
856960
description="Stealth PR Reviewer — looks like you wrote every word.",

0 commit comments

Comments
 (0)