roastbuddy 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,64 @@
1
+ """Status display for RoastBuddy."""
2
+
3
+ from rich.console import Console
4
+ from rich.panel import Panel
5
+ from rich.table import Table
6
+ from roastbuddy.utils.git_utils import (
7
+ find_git_repo,
8
+ get_last_commit,
9
+ get_recent_commits,
10
+ analyze_commit_patterns
11
+ )
12
+ from roastbuddy.utils.copilot_utils import is_copilot_available
13
+
14
+ console = Console()
15
+
16
+
17
+ def show_status():
18
+ """Show current RoastBuddy status."""
19
+ repo = find_git_repo()
20
+
21
+ if not repo:
22
+ console.print("[red]❌ Not in a git repository![/red]")
23
+ return
24
+
25
+ # Get commit data
26
+ last_commit = get_last_commit(repo)
27
+ recent_commits = get_recent_commits(repo, count=10)
28
+ patterns = analyze_commit_patterns(repo)
29
+
30
+ # Check Copilot availability
31
+ copilot = "✅ Available" if is_copilot_available() else "❌ Not available"
32
+
33
+ # Last commit info
34
+ if last_commit:
35
+ last_commit_text = (
36
+ f"[bold]{last_commit['short_hash']}[/bold] - {last_commit['message'][:40]}...\n"
37
+ f"Files: {last_commit['files_changed']} | "
38
+ f"Lines: +{last_commit['lines_added']} -{last_commit['lines_deleted']}"
39
+ )
40
+ else:
41
+ last_commit_text = "[dim]No commits yet[/dim]"
42
+
43
+ # Statistics table
44
+ stats_table = Table(show_header=False, box=None)
45
+ stats_table.add_column("Metric", style="cyan")
46
+ stats_table.add_column("Value", style="green")
47
+
48
+ stats_table.add_row("Total Commits", str(patterns["total_commits"]))
49
+ stats_table.add_row("Clean Commits", f"{patterns['clean_commits']} ({patterns.get('clean_ratio', 0):.0%})")
50
+ stats_table.add_row("Avg Files/Commit", f"{patterns['avg_files_per_commit']:.1f}")
51
+ stats_table.add_row("Avg Lines/Commit", f"{patterns['avg_lines_per_commit']:.0f}")
52
+ stats_table.add_row("GitHub Copilot", copilot)
53
+
54
+ # Display everything
55
+ console.print(Panel(
56
+ f"[bold cyan]📊 Repository Status[/bold cyan]\n\n"
57
+ f"[bold]Last Commit:[/bold]\n{last_commit_text}\n\n"
58
+ f"[bold]Statistics (last {patterns['total_commits']} commits):[/bold]",
59
+ border_style="cyan"
60
+ ))
61
+
62
+ console.print(stats_table)
63
+
64
+ console.print("\n[dim]💡 Try: [bold]roastbuddy roast[/bold] or [bold]roastbuddy praise[/bold][/dim]")
@@ -0,0 +1,22 @@
1
+ """Streak tracking functionality."""
2
+
3
+ from rich.console import Console
4
+ from rich.panel import Panel
5
+
6
+ console = Console()
7
+
8
+
9
+ def show_streaks(streak_type: str = "all"):
10
+ """Show coding streaks."""
11
+ # TODO: Implement streak tracking
12
+ console.print(Panel.fit(
13
+ "[bold magenta]🔥 Your Streaks[/bold magenta]\n\n"
14
+ "[yellow]Streak tracking coming soon![/yellow]\n"
15
+ "This will track:\n"
16
+ "• Clean commit streaks\n"
17
+ "• Daily coding streaks\n"
18
+ "• Test passing streaks\n"
19
+ "• Personal records",
20
+ title=f"Streaks ({streak_type})",
21
+ border_style="magenta"
22
+ ))
@@ -0,0 +1 @@
1
+ """Templates __init__ file for roastbuddy.templates package."""
@@ -0,0 +1,113 @@
1
+ """Praise templates for fallback mode."""
2
+
3
+ PRAISE_TEMPLATES = {
4
+ "clean_commit": [
5
+ "✨ Beautiful commit! Clear message, focused changes, and just the right size. You're a git wizard! 🧙‍♂️",
6
+ "🎯 PERFECTION! This is exactly how commits should look. Keep this standard up! 👑",
7
+ "💎 A gem of a commit! Clean, descriptive, and well-scoped. This is chef's kiss! 👨‍🍳💋",
8
+ "🌟 Outstanding work! Your commit message tells a story and your changes are surgical. Bravo! 👏",
9
+ ],
10
+
11
+ "good_message": [
12
+ "📝 That commit message is *chef's kiss*! Clear, concise, and informative. Nice work! 🎉",
13
+ "✅ Love the detailed commit message! Future you will thank present you. Great practice! 🙌",
14
+ "💬 Your commit message game is STRONG! This is how you document changes properly! 📚",
15
+ ],
16
+
17
+ "test_included": [
18
+ "🧪 Tests included? You absolute legend! That's how we write reliable code! 💪",
19
+ "✅ Testing your code like a pro! Your future self will thank you when bugs don't appear! 🛡️",
20
+ "🎯 Tests AND features? Now that's what I call comprehensive! Keep it up! 🚀",
21
+ ],
22
+
23
+ "refactor": [
24
+ "♻️ Refactoring done right! Cleaning up the codebase like a true craftsperson! 🔧",
25
+ "✨ That refactor is *smooth*! Code quality just went up a notch! 📈",
26
+ "🧹 Love a good cleanup! Your codebase is looking fresher than ever! 🌟",
27
+ ],
28
+
29
+ "bug_fix": [
30
+ "🐛 Bug squashed! Your debugging skills are on point! 💥",
31
+ "🔨 Fixed it! Every bug you crush makes the codebase stronger! 💪",
32
+ "🎯 Bug fix delivered! You're keeping the app running smooth! 🚀",
33
+ ],
34
+
35
+ "feature": [
36
+ "🚀 New feature deployed! Innovation in action! Keep building! 🏗️",
37
+ "✨ Feature added! Your users are going to love this! 💝",
38
+ "🎉 Shipping features like a boss! This is what progress looks like! 📦",
39
+ ],
40
+
41
+ "focused": [
42
+ "🎯 Laser-focused commit! One thing at a time, done right. This is the way! ⭐",
43
+ "👌 Perfect scope! You're not trying to boil the ocean in one commit. Smart! 🧠",
44
+ "💡 Focused and purposeful! This is textbook commit discipline! 📖",
45
+ ],
46
+
47
+ "documentation": [
48
+ "📚 Documentation update! The unsung hero of codebases! Thank you! 🙏",
49
+ "✍️ Docs updated! Helping future developers understand the code. True hero! 🦸",
50
+ "📖 Documentation love! This makes everyone's life easier! 💚",
51
+ ],
52
+
53
+ "small_improvement": [
54
+ "💎 Small but mighty! Every little improvement counts! Keep polishing! ✨",
55
+ "🎨 Nice touch! These small improvements add up to greatness! 🌟",
56
+ "⚡ Quick win! Small optimizations lead to big improvements over time! 📈",
57
+ ],
58
+
59
+ "consistent_work": [
60
+ "🔥 Consistent commits! You're building momentum and it shows! 💪",
61
+ "📈 Steady progress! This is how successful projects are built! 🏗️",
62
+ "⚡ You're on a roll! Keep that coding energy flowing! 🌊",
63
+ ],
64
+
65
+ "default": [
66
+ "👏 Good work! Every commit moves the project forward! 🚀",
67
+ "✅ Committed and deployed! Your contribution matters! 🌟",
68
+ "💪 Keep it up! Building great things one commit at a time! 🏗️",
69
+ "🎯 Solid work! You're making progress and that's what counts! 📈",
70
+ ]
71
+ }
72
+
73
+
74
+ def get_praise_category(commit_data: dict) -> str:
75
+ """Determine the praise category based on commit data."""
76
+ message = commit_data.get("message", "").lower()
77
+ lines_changed = commit_data.get("lines_added", 0) + commit_data.get("lines_deleted", 0)
78
+ files_changed = commit_data.get("files_changed", 0)
79
+ is_clean = commit_data.get("is_clean", False)
80
+
81
+ # Check for specific keywords in message
82
+ if any(word in message for word in ["test", "spec", "unittest"]):
83
+ return "test_included"
84
+
85
+ if any(word in message for word in ["refactor", "cleanup", "clean up"]):
86
+ return "refactor"
87
+
88
+ if any(word in message for word in ["fix", "bug", "issue", "resolve"]):
89
+ return "bug_fix"
90
+
91
+ if any(word in message for word in ["feat", "feature", "add", "implement"]):
92
+ return "feature"
93
+
94
+ if any(word in message for word in ["doc", "readme", "comment"]):
95
+ return "documentation"
96
+
97
+ # Check for clean commit characteristics
98
+ if is_clean and lines_changed < 200 and files_changed <= 5:
99
+ return "clean_commit"
100
+
101
+ # Good message but maybe larger scope
102
+ if is_clean:
103
+ return "good_message"
104
+
105
+ # Small improvements
106
+ if lines_changed < 50:
107
+ return "small_improvement"
108
+
109
+ # Focused work
110
+ if files_changed <= 3:
111
+ return "focused"
112
+
113
+ return "default"
@@ -0,0 +1,87 @@
1
+ """Roast templates for fallback mode."""
2
+
3
+ ROAST_TEMPLATES = {
4
+ "tiny_commit": [
5
+ "🔥 A one-liner? What is this, a commit for ants? At least it's not breaking anything... probably. 😅",
6
+ "😏 One line changed. Did you forget to save the rest of your work, or is this your idea of 'moving fast'?",
7
+ "🤏 Congratulations on the microscopic commit! Every journey begins with a single step... but maybe take a few more? 💨",
8
+ ],
9
+
10
+ "poor_message": [
11
+ "🙄 'fixed stuff' - ah yes, the ancient art of commit message poetry. Shakespeare would be proud... NOT! Try being specific next time. 📝",
12
+ "😬 That commit message though... Did you fall asleep on the keyboard? Give future you some context! 🤔",
13
+ "🎭 'WIP' - Working In Progress? More like 'Why Is Perfection' so hard to achieve! Clean up that message! ✨",
14
+ "💀 Your commit message has less detail than a tweet. And that's saying something. Be descriptive! 📖",
15
+ ],
16
+
17
+ "massive_commit": [
18
+ "🚨 WOAH THERE! 500+ lines changed? That's not a commit, that's a novel! Break it up next time, champ. 📚",
19
+ "😱 This commit is MASSIVE! Did you just push your entire life's work? Smaller commits = easier debugging! 🔍",
20
+ "🎪 Welcome to the three-ring circus of commits! This one's doing WAY too much. Simplify, friend! 🎯",
21
+ "🌋 Your commit is erupting with changes! Cool feature, but try atomic commits next time. ⚛️",
22
+ ],
23
+
24
+ "many_files": [
25
+ "🤯 You touched how many files?! Someone's been busy... or refactoring everything. Hope you tested it all! ✅",
26
+ "📂 Filing cabinet explosion detected! That's a lot of files for one commit. Everything still work? 🤞",
27
+ "🌪️ Tornado commit alert! You swept through the codebase like nobody's business. Bold move! 💪",
28
+ ],
29
+
30
+ "no_tests": [
31
+ "🧪 No test files in sight... Living dangerously, I see! Your code might work... until it doesn't. 🎲",
32
+ "⚠️ Tests? Where we're going, we don't need tests! (Narrator: They definitely needed tests.) 🚀",
33
+ "🎰 Pushing without tests is like gambling at a casino. Feeling lucky? Add some tests! 🍀",
34
+ ],
35
+
36
+ "good_commit": [
37
+ "✅ Now THIS is a commit! Clean message, reasonable size, proper focus. You're getting the hang of it! 🎯",
38
+ "👏 Not bad! This commit actually makes sense. Keep up this energy! 💯",
39
+ "😌 A solid commit! Clear, focused, and well-documented. This is the way! 🌟",
40
+ ],
41
+
42
+ "average_commit": [
43
+ "🤷 Eh, it's... a commit. Not great, not terrible. Room for improvement but you're getting there! 📈",
44
+ "😐 Middle-of-the-road commit vibes. Does the job, could use some polish. Keep pushing! 💪",
45
+ "📊 This commit is like a 'C+' - passing, but you can do better! Level up that commit game! 🎮",
46
+ ],
47
+
48
+ "default": [
49
+ "🔥 Another day, another commit! Keep the code flowing and the bugs fleeing! 🐛",
50
+ "💻 Commit received! Your code contribution has been noted in the annals of history. Make it count! 📜",
51
+ "🚀 Code committed! Now the real test: does it work in production? 🤔",
52
+ "⚡ Commit logged! Remember: with great commits comes great responsibility! 🦸",
53
+ ]
54
+ }
55
+
56
+
57
+ def get_roast_category(commit_data: dict) -> str:
58
+ """Determine the roast category based on commit data."""
59
+ lines_changed = commit_data.get("lines_added", 0) + commit_data.get("lines_deleted", 0)
60
+ files_changed = commit_data.get("files_changed", 0)
61
+ is_clean = commit_data.get("is_clean", False)
62
+
63
+ # Check for poor commit message first
64
+ if not is_clean:
65
+ return "poor_message"
66
+
67
+ # Check for tiny commits
68
+ if lines_changed < 5:
69
+ return "tiny_commit"
70
+
71
+ # Check for massive commits
72
+ if lines_changed > 500:
73
+ return "massive_commit"
74
+
75
+ # Check for many files changed
76
+ if files_changed > 15:
77
+ return "many_files"
78
+
79
+ # Check commit quality
80
+ if is_clean and lines_changed < 200 and files_changed <= 5:
81
+ return "good_commit"
82
+
83
+ # Average commit
84
+ if lines_changed < 200:
85
+ return "average_commit"
86
+
87
+ return "default"
@@ -0,0 +1 @@
1
+ """Utils __init__ file for roastbuddy.utils package."""
@@ -0,0 +1,164 @@
1
+ """Code quality analysis utilities."""
2
+
3
+ import os
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Dict, Any, List
7
+ from git import Repo
8
+
9
+
10
+ def count_todos_fixmes(repo_path: str) -> Dict[str, int]:
11
+ """Count TODO and FIXME comments in the codebase."""
12
+ todo_pattern = re.compile(r'#\s*(TODO|FIXME|XXX|HACK|NOTE):', re.IGNORECASE)
13
+
14
+ counts = {"TODO": 0, "FIXME": 0, "XXX": 0, "HACK": 0, "NOTE": 0}
15
+
16
+ # Common code extensions
17
+ code_extensions = {
18
+ ".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".go", ".rs",
19
+ ".c", ".cpp", ".h", ".hpp", ".rb", ".php", ".swift", ".kt"
20
+ }
21
+
22
+ try:
23
+ repo_path_obj = Path(repo_path)
24
+ for file_path in repo_path_obj.rglob("*"):
25
+ if file_path.is_file() and file_path.suffix in code_extensions:
26
+ # Skip common directories
27
+ if any(part in file_path.parts for part in ["node_modules", "venv", ".git", "dist", "build"]):
28
+ continue
29
+
30
+ try:
31
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
32
+ for line in f:
33
+ match = todo_pattern.search(line)
34
+ if match:
35
+ keyword = match.group(1).upper()
36
+ if keyword in counts:
37
+ counts[keyword] += 1
38
+ except Exception:
39
+ continue
40
+ except Exception as e:
41
+ print(f"Error counting TODOs: {e}")
42
+
43
+ return counts
44
+
45
+
46
+ def detect_test_files(changed_files: List[str]) -> bool:
47
+ """Detect if any test files were changed."""
48
+ test_indicators = [
49
+ "test_", "_test.", "test.", ".test.", "spec.",
50
+ "/tests/", "/test/", "__tests__", ".spec."
51
+ ]
52
+
53
+ for file_path in changed_files:
54
+ file_lower = file_path.lower()
55
+ if any(indicator in file_lower for indicator in test_indicators):
56
+ return True
57
+
58
+ return False
59
+
60
+
61
+ def analyze_file_sizes(repo_path: str) -> Dict[str, Any]:
62
+ """Analyze file sizes in the repository."""
63
+ code_extensions = {
64
+ ".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".go", ".rs",
65
+ ".c", ".cpp", ".h", ".hpp", ".rb", ".php", ".swift", ".kt"
66
+ }
67
+
68
+ file_sizes = []
69
+ total_lines = 0
70
+
71
+ try:
72
+ repo_path_obj = Path(repo_path)
73
+ for file_path in repo_path_obj.rglob("*"):
74
+ if file_path.is_file() and file_path.suffix in code_extensions:
75
+ if any(part in file_path.parts for part in ["node_modules", "venv", ".git", "dist", "build"]):
76
+ continue
77
+
78
+ try:
79
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
80
+ lines = len(f.readlines())
81
+ file_sizes.append(lines)
82
+ total_lines += lines
83
+ except Exception:
84
+ continue
85
+ except Exception as e:
86
+ print(f"Error analyzing file sizes: {e}")
87
+
88
+ if not file_sizes:
89
+ return {
90
+ "total_files": 0,
91
+ "total_lines": 0,
92
+ "avg_file_size": 0,
93
+ "largest_file": 0,
94
+ }
95
+
96
+ return {
97
+ "total_files": len(file_sizes),
98
+ "total_lines": total_lines,
99
+ "avg_file_size": sum(file_sizes) / len(file_sizes),
100
+ "largest_file": max(file_sizes),
101
+ }
102
+
103
+
104
+ def categorize_commit_size(lines_changed: int) -> str:
105
+ """Categorize commit by size."""
106
+ if lines_changed < 10:
107
+ return "tiny"
108
+ elif lines_changed < 50:
109
+ return "small"
110
+ elif lines_changed < 200:
111
+ return "medium"
112
+ elif lines_changed < 500:
113
+ return "large"
114
+ else:
115
+ return "massive"
116
+
117
+
118
+ def analyze_commit_quality(commit_data: Dict[str, Any]) -> Dict[str, Any]:
119
+ """Analyze the quality of a commit."""
120
+ message = commit_data.get("message", "")
121
+ lines_changed = commit_data.get("lines_added", 0) + commit_data.get("lines_deleted", 0)
122
+ files_changed = commit_data.get("files_changed", 0)
123
+
124
+ quality_score = 0
125
+ feedback = []
126
+
127
+ # Message quality (40 points)
128
+ if commit_data.get("is_clean", False):
129
+ quality_score += 40
130
+ feedback.append("Good commit message")
131
+ else:
132
+ feedback.append("Improve commit message clarity")
133
+
134
+ # Size appropriateness (30 points)
135
+ size_category = categorize_commit_size(lines_changed)
136
+ if size_category in ["small", "medium"]:
137
+ quality_score += 30
138
+ feedback.append("Appropriate commit size")
139
+ elif size_category == "tiny":
140
+ quality_score += 20
141
+ feedback.append("Very small commit")
142
+ elif size_category == "large":
143
+ quality_score += 15
144
+ feedback.append("Large commit - consider breaking it up")
145
+ else: # massive
146
+ quality_score += 5
147
+ feedback.append("Massive commit - definitely break it up!")
148
+
149
+ # Files changed (30 points)
150
+ if files_changed <= 5:
151
+ quality_score += 30
152
+ feedback.append("Focused file changes")
153
+ elif files_changed <= 15:
154
+ quality_score += 20
155
+ feedback.append("Moderate file changes")
156
+ else:
157
+ quality_score += 5
158
+ feedback.append("Many files changed - be careful!")
159
+
160
+ return {
161
+ "quality_score": quality_score,
162
+ "size_category": size_category,
163
+ "feedback": feedback,
164
+ }
@@ -0,0 +1,106 @@
1
+ """Copilot CLI detection and integration."""
2
+
3
+ import subprocess
4
+ from typing import Optional, Dict, Any
5
+ from rich.console import Console
6
+
7
+ console = Console()
8
+
9
+
10
+ def is_copilot_available() -> bool:
11
+ """Check if GitHub Copilot CLI is available."""
12
+ try:
13
+ result = subprocess.run(
14
+ ["gh", "copilot", "--version"],
15
+ capture_output=True,
16
+ text=True,
17
+ timeout=5
18
+ )
19
+ return result.returncode == 0
20
+ except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
21
+ return False
22
+
23
+
24
+ def get_copilot_suggestion(prompt: str, system_prompt: Optional[str] = None) -> Optional[str]:
25
+ """Get a suggestion from GitHub Copilot CLI."""
26
+ if not is_copilot_available():
27
+ return None
28
+
29
+ try:
30
+ # Build the command
31
+ cmd = ["gh", "copilot", "suggest", "-t", "shell"]
32
+
33
+ full_prompt = prompt
34
+ if system_prompt:
35
+ full_prompt = f"{system_prompt}\n\n{prompt}"
36
+
37
+ # Run the command
38
+ result = subprocess.run(
39
+ cmd + [full_prompt],
40
+ capture_output=True,
41
+ text=True,
42
+ timeout=30
43
+ )
44
+
45
+ if result.returncode == 0:
46
+ return result.stdout.strip()
47
+ else:
48
+ return None
49
+
50
+ except Exception as e:
51
+ console.print(f"[yellow]Copilot CLI error: {e}[/yellow]")
52
+ return None
53
+
54
+
55
+ def generate_roast_with_copilot(commit_data: Dict[str, Any]) -> Optional[str]:
56
+ """Generate a roast using Copilot CLI."""
57
+ if not is_copilot_available():
58
+ return None
59
+
60
+ system_prompt = """You are RoastBuddy, a witty but supportive coding companion.
61
+ Your job is to roast code commits in a humorous way that's funny but never mean-spirited.
62
+ Keep roasts short (2-3 sentences), use emojis, and end with a constructive tip.
63
+ Be playful, sarcastic, but ultimately encouraging."""
64
+
65
+ commit_info = (
66
+ f"Commit: {commit_data.get('message', 'No message')}\n"
67
+ f"Files changed: {commit_data.get('files_changed', 0)}\n"
68
+ f"Lines: +{commit_data.get('lines_added', 0)} -{commit_data.get('lines_deleted', 0)}\n"
69
+ f"Clean message: {commit_data.get('is_clean', False)}"
70
+ )
71
+
72
+ prompt = f"Generate a witty roast for this commit:\n{commit_info}"
73
+
74
+ return get_copilot_suggestion(prompt, system_prompt)
75
+
76
+
77
+ def generate_praise_with_copilot(commit_data: Dict[str, Any]) -> Optional[str]:
78
+ """Generate praise using Copilot CLI."""
79
+ if not is_copilot_available():
80
+ return None
81
+
82
+ system_prompt = """You are RoastBuddy, a supportive coding companion.
83
+ Your job is to genuinely praise good work and celebrate achievements.
84
+ Keep praise short (2-3 sentences), use emojis, and be enthusiastic and specific.
85
+ Be genuine and encouraging."""
86
+
87
+ commit_info = (
88
+ f"Commit: {commit_data.get('message', 'No message')}\n"
89
+ f"Files changed: {commit_data.get('files_changed', 0)}\n"
90
+ f"Lines: +{commit_data.get('lines_added', 0)} -{commit_data.get('lines_deleted', 0)}\n"
91
+ f"Clean message: {commit_data.get('is_clean', False)}"
92
+ )
93
+
94
+ prompt = f"Generate enthusiastic praise for this commit:\n{commit_info}"
95
+
96
+ return get_copilot_suggestion(prompt, system_prompt)
97
+
98
+
99
+ def suggest_copilot_install():
100
+ """Suggest installing GitHub Copilot CLI."""
101
+ console.print("\n[bold cyan]💡 GitHub Copilot CLI not found[/bold cyan]")
102
+ console.print("\n[yellow]RoastBuddy works best with GitHub Copilot CLI for dynamic roasts![/yellow]")
103
+ console.print("\n[dim]To install:[/dim]")
104
+ console.print(" 1. Install GitHub CLI: [bold]https://cli.github.com/[/bold]")
105
+ console.print(" 2. Install Copilot extension: [bold]gh extension install github/gh-copilot[/bold]")
106
+ console.print("\n[dim]Don't worry! RoastBuddy will use built-in templates for now.[/dim]\n")