claude-commit 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.
- claude_commit/__init__.py +12 -0
- claude_commit/config.py +149 -0
- claude_commit/main.py +876 -0
- claude_commit-0.1.0.dist-info/METADATA +292 -0
- claude_commit-0.1.0.dist-info/RECORD +9 -0
- claude_commit-0.1.0.dist-info/WHEEL +5 -0
- claude_commit-0.1.0.dist-info/entry_points.txt +2 -0
- claude_commit-0.1.0.dist-info/licenses/LICENSE +22 -0
- claude_commit-0.1.0.dist-info/top_level.txt +1 -0
claude_commit/config.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Configuration management for claude-commit
|
|
4
|
+
Handles user aliases and preferences
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Config:
|
|
13
|
+
"""Manages configuration and aliases for claude-commit"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, config_path: Optional[Path] = None):
|
|
16
|
+
"""Initialize config manager
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
config_path: Path to config file. Defaults to ~/.claude-commit/config.json
|
|
20
|
+
"""
|
|
21
|
+
if config_path is None:
|
|
22
|
+
config_path = Path.home() / ".claude-commit" / "config.json"
|
|
23
|
+
|
|
24
|
+
self.config_path = config_path
|
|
25
|
+
self._config = self._load_config()
|
|
26
|
+
|
|
27
|
+
def _load_config(self) -> dict:
|
|
28
|
+
"""Load configuration from file"""
|
|
29
|
+
if not self.config_path.exists():
|
|
30
|
+
return {"aliases": self._default_aliases()}
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
with open(self.config_path, "r", encoding="utf-8") as f:
|
|
34
|
+
config = json.load(f)
|
|
35
|
+
# Ensure aliases key exists
|
|
36
|
+
if "aliases" not in config:
|
|
37
|
+
config["aliases"] = self._default_aliases()
|
|
38
|
+
return config
|
|
39
|
+
except Exception:
|
|
40
|
+
return {"aliases": self._default_aliases()}
|
|
41
|
+
|
|
42
|
+
def _save_config(self):
|
|
43
|
+
"""Save configuration to file"""
|
|
44
|
+
# Ensure directory exists
|
|
45
|
+
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
|
|
47
|
+
with open(self.config_path, "w", encoding="utf-8") as f:
|
|
48
|
+
json.dump(self._config, f, indent=2, ensure_ascii=False)
|
|
49
|
+
|
|
50
|
+
def _default_aliases(self) -> Dict[str, str]:
|
|
51
|
+
"""Get default command aliases (like git aliases)"""
|
|
52
|
+
return {
|
|
53
|
+
"cc": "", # just claude-commit
|
|
54
|
+
"cca": "--all", # analyze all changes
|
|
55
|
+
"ccv": "--verbose", # verbose mode
|
|
56
|
+
"ccc": "--commit", # auto-commit
|
|
57
|
+
"ccp": "--preview", # preview only
|
|
58
|
+
"ccac": "--all --commit", # all changes + commit
|
|
59
|
+
"ccav": "--all --verbose", # all changes + verbose
|
|
60
|
+
"ccvc": "--verbose --commit", # verbose + commit
|
|
61
|
+
"ccopy": "--copy", # copy to clipboard
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def aliases(self) -> Dict[str, str]:
|
|
66
|
+
"""Get all aliases"""
|
|
67
|
+
return self._config.get("aliases", {})
|
|
68
|
+
|
|
69
|
+
def get_alias(self, alias: str) -> Optional[str]:
|
|
70
|
+
"""Get command for an alias
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
alias: The alias name
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Command string or None if alias doesn't exist
|
|
77
|
+
"""
|
|
78
|
+
return self.aliases.get(alias)
|
|
79
|
+
|
|
80
|
+
def set_alias(self, alias: str, command: str):
|
|
81
|
+
"""Set or update an alias
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
alias: The alias name
|
|
85
|
+
command: The command arguments (without 'claude-commit')
|
|
86
|
+
"""
|
|
87
|
+
self._config["aliases"][alias] = command
|
|
88
|
+
self._save_config()
|
|
89
|
+
|
|
90
|
+
def delete_alias(self, alias: str) -> bool:
|
|
91
|
+
"""Delete an alias
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
alias: The alias name
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True if alias was deleted, False if it didn't exist
|
|
98
|
+
"""
|
|
99
|
+
if alias in self._config["aliases"]:
|
|
100
|
+
del self._config["aliases"][alias]
|
|
101
|
+
self._save_config()
|
|
102
|
+
return True
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
def list_aliases(self) -> Dict[str, str]:
|
|
106
|
+
"""List all aliases"""
|
|
107
|
+
return self.aliases.copy()
|
|
108
|
+
|
|
109
|
+
def is_first_run(self) -> bool:
|
|
110
|
+
"""Check if this is the first run"""
|
|
111
|
+
return not self.config_path.exists()
|
|
112
|
+
|
|
113
|
+
def mark_first_run_complete(self):
|
|
114
|
+
"""Mark that first run is complete"""
|
|
115
|
+
if not self.config_path.exists():
|
|
116
|
+
self._save_config()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def resolve_alias(args: list) -> list:
|
|
120
|
+
"""Resolve alias if first argument is an alias
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
args: Command line arguments
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Resolved arguments (with alias expanded)
|
|
127
|
+
"""
|
|
128
|
+
if not args:
|
|
129
|
+
return args
|
|
130
|
+
|
|
131
|
+
config = Config()
|
|
132
|
+
first_arg = args[0]
|
|
133
|
+
|
|
134
|
+
# Check if first argument is an alias
|
|
135
|
+
alias_cmd = config.get_alias(first_arg)
|
|
136
|
+
if alias_cmd is not None:
|
|
137
|
+
# Replace alias with its command
|
|
138
|
+
if alias_cmd:
|
|
139
|
+
# Parse the alias command into arguments
|
|
140
|
+
import shlex
|
|
141
|
+
expanded_args = shlex.split(alias_cmd)
|
|
142
|
+
# Combine expanded args with remaining original args
|
|
143
|
+
return expanded_args + args[1:]
|
|
144
|
+
else:
|
|
145
|
+
# Empty alias (just the base command)
|
|
146
|
+
return args[1:]
|
|
147
|
+
|
|
148
|
+
return args
|
|
149
|
+
|
claude_commit/main.py
ADDED
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
claude-commit - AI-powered git commit message generator
|
|
4
|
+
|
|
5
|
+
Analyzes your git repository changes and generates a meaningful commit message
|
|
6
|
+
using Claude's AI capabilities.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import asyncio
|
|
11
|
+
import argparse
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
19
|
+
import pyperclip
|
|
20
|
+
|
|
21
|
+
from claude_agent_sdk import (
|
|
22
|
+
query,
|
|
23
|
+
ClaudeAgentOptions,
|
|
24
|
+
AssistantMessage,
|
|
25
|
+
TextBlock,
|
|
26
|
+
ToolUseBlock,
|
|
27
|
+
ToolResultBlock,
|
|
28
|
+
ResultMessage,
|
|
29
|
+
CLINotFoundError,
|
|
30
|
+
ProcessError,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from .config import Config, resolve_alias
|
|
34
|
+
|
|
35
|
+
console = Console()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
SYSTEM_PROMPT = """You are an expert software engineer tasked with analyzing code changes and writing excellent git commit messages.
|
|
39
|
+
|
|
40
|
+
Your goal: Generate a clear, accurate, and meaningful commit message that captures the essence of the changes.
|
|
41
|
+
|
|
42
|
+
Available tools you can use:
|
|
43
|
+
- Bash: Run git commands (git diff, git status, git log, etc.) and other shell commands
|
|
44
|
+
- Read: Read any file in the repository to understand context
|
|
45
|
+
- Grep: Search for patterns across files to understand relationships
|
|
46
|
+
- Glob: Find files matching patterns
|
|
47
|
+
|
|
48
|
+
Analysis approach (you decide what's necessary):
|
|
49
|
+
1. IMPORTANT: First check recent commit history (git log -10 --oneline or git log -10 --pretty=format:"%s") to understand the existing commit message style
|
|
50
|
+
- Check if the project uses gitmoji (emojis like 🎉, ✨, 🐛, etc.)
|
|
51
|
+
- Check if messages are in Chinese, English, or other languages
|
|
52
|
+
- Check if they use conventional commits (feat:, fix:, etc.) or other formats
|
|
53
|
+
- Note any specific patterns or conventions used
|
|
54
|
+
2. Examine what files changed (git status, git diff)
|
|
55
|
+
3. For significant changes, READ the modified files to understand:
|
|
56
|
+
- The purpose and context of changed functions/classes
|
|
57
|
+
- How the changes fit into the larger codebase
|
|
58
|
+
- The intent behind the modifications
|
|
59
|
+
4. Search for related code (grep) to understand dependencies and impacts
|
|
60
|
+
5. Consider the scope: is this a feature, fix, refactor, docs, chore, etc.?
|
|
61
|
+
|
|
62
|
+
Commit message guidelines:
|
|
63
|
+
- **FOLLOW THE EXISTING FORMAT**: Match the style, language, and conventions used in recent commits
|
|
64
|
+
- If no clear pattern exists in history, use conventional commits format (feat:, fix:, docs:, refactor:, test:, chore:, style:, perf:)
|
|
65
|
+
- First line: < 50 chars (or follow existing convention), imperative mood, summarize the main change
|
|
66
|
+
- **IMPORTANT**: Use multi-line format with bullet points for detailed changes:
|
|
67
|
+
```
|
|
68
|
+
type: brief summary (< 50 chars)
|
|
69
|
+
|
|
70
|
+
- First change detail
|
|
71
|
+
- Second change detail
|
|
72
|
+
- Third change detail
|
|
73
|
+
```
|
|
74
|
+
- Be specific and meaningful (avoid vague terms like "update", "change", "modify")
|
|
75
|
+
- Focus on WHAT changed and WHY (the intent), not HOW (implementation details)
|
|
76
|
+
- Base your message on deep understanding, not just diff surface analysis
|
|
77
|
+
|
|
78
|
+
Examples of excellent commit messages (multi-line format):
|
|
79
|
+
|
|
80
|
+
Conventional commits style:
|
|
81
|
+
```
|
|
82
|
+
feat: add user authentication system
|
|
83
|
+
|
|
84
|
+
- Implement JWT-based authentication with refresh tokens
|
|
85
|
+
- Add login and registration endpoints
|
|
86
|
+
- Create user session management
|
|
87
|
+
- Add password hashing with bcrypt
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
fix: prevent memory leak in connection pool
|
|
92
|
+
|
|
93
|
+
- Close idle connections after timeout
|
|
94
|
+
- Add connection limit configuration
|
|
95
|
+
- Improve error handling for failed connections
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
With gitmoji:
|
|
99
|
+
```
|
|
100
|
+
✨ add user authentication system
|
|
101
|
+
|
|
102
|
+
- Implement JWT-based authentication with refresh tokens
|
|
103
|
+
- Add login and registration endpoints
|
|
104
|
+
- Create user session management
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
In Chinese:
|
|
108
|
+
```
|
|
109
|
+
新增:用户认证系统
|
|
110
|
+
|
|
111
|
+
- 实现基于 JWT 的身份验证和刷新令牌
|
|
112
|
+
- 添加登录和注册接口
|
|
113
|
+
- 创建用户会话管理
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
At the end of your analysis, output your final commit message in this format:
|
|
117
|
+
|
|
118
|
+
COMMIT_MESSAGE:
|
|
119
|
+
<your commit message here>
|
|
120
|
+
|
|
121
|
+
Everything between COMMIT_MESSAGE: and the end will be used as the commit message.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def generate_commit_message(
|
|
126
|
+
repo_path: Optional[Path] = None,
|
|
127
|
+
staged_only: bool = True,
|
|
128
|
+
verbose: bool = False,
|
|
129
|
+
max_diff_lines: int = 500,
|
|
130
|
+
) -> Optional[str]:
|
|
131
|
+
"""
|
|
132
|
+
Generate a commit message based on current git changes.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
repo_path: Path to git repository (defaults to current directory)
|
|
136
|
+
staged_only: Only analyze staged changes (git diff --cached)
|
|
137
|
+
verbose: Print detailed information
|
|
138
|
+
max_diff_lines: Maximum number of diff lines to analyze
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Generated commit message or None if failed
|
|
142
|
+
"""
|
|
143
|
+
repo_path = repo_path or Path.cwd()
|
|
144
|
+
|
|
145
|
+
if verbose:
|
|
146
|
+
console.print(f"[blue]🔍 Analyzing repository:[/blue] {repo_path}")
|
|
147
|
+
console.print(f"[blue]📝 Mode:[/blue] {'staged changes only' if staged_only else 'all changes'}")
|
|
148
|
+
|
|
149
|
+
# Build the analysis prompt - give AI freedom to explore
|
|
150
|
+
prompt = f"""Analyze the git repository changes and generate an excellent commit message.
|
|
151
|
+
|
|
152
|
+
Context:
|
|
153
|
+
- Working directory: {repo_path.absolute()}
|
|
154
|
+
- Analysis scope: {"staged changes only (git diff --cached)" if staged_only else "all uncommitted changes (git diff)"}
|
|
155
|
+
- You have access to: Bash, Read, Grep, and Glob tools
|
|
156
|
+
|
|
157
|
+
Your task:
|
|
158
|
+
1. **FIRST**: Check the recent commit history (e.g., `git log -3 --oneline` or `git log -3 --pretty=format:"%s"`) to understand the commit message format/style used in this project
|
|
159
|
+
- Does it use gitmoji? (emojis like ✨, 🐛, ♻️, etc.)
|
|
160
|
+
- What language? (Chinese, English, etc.)
|
|
161
|
+
- What format? (conventional commits, custom format, etc.)
|
|
162
|
+
- **IMPORTANT**: You MUST follow the same style/format/language as the existing commits
|
|
163
|
+
2. Investigate the changes thoroughly. Use whatever tools and commands you need.
|
|
164
|
+
3. Understand the INTENT and IMPACT of the changes, not just the surface-level diff.
|
|
165
|
+
4. Read relevant files to understand context and purpose.
|
|
166
|
+
5. Generate a commit message in **MULTI-LINE FORMAT** with:
|
|
167
|
+
- First line: brief summary (< 50 chars)
|
|
168
|
+
- Empty line
|
|
169
|
+
- Bullet points (starting with "-") for detailed changes
|
|
170
|
+
Example:
|
|
171
|
+
```
|
|
172
|
+
fix: correct formatting issue
|
|
173
|
+
|
|
174
|
+
- Preserve empty lines in commit messages
|
|
175
|
+
- Update prompt to require multi-line format
|
|
176
|
+
- Add examples showing proper structure
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Recommendations (not requirements - use your judgment):
|
|
180
|
+
- Start with `git log -3 --oneline` to check the commit message style
|
|
181
|
+
- Then use `git status` and `git diff {"--cached" if staged_only else ""}` to see what changed
|
|
182
|
+
- For non-trivial changes, READ the modified files to understand their purpose
|
|
183
|
+
- Use grep to find related code or understand how functions are used
|
|
184
|
+
- Consider the broader context of the codebase
|
|
185
|
+
|
|
186
|
+
When you're confident you understand the changes, output your commit message in this exact format:
|
|
187
|
+
|
|
188
|
+
COMMIT_MESSAGE:
|
|
189
|
+
<your commit message>
|
|
190
|
+
|
|
191
|
+
Everything after "COMMIT_MESSAGE:" will be extracted as the final commit message.
|
|
192
|
+
Begin your analysis now.
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
options = ClaudeAgentOptions(
|
|
196
|
+
system_prompt=SYSTEM_PROMPT,
|
|
197
|
+
allowed_tools=["Bash", "Read", "Grep", "Glob"],
|
|
198
|
+
permission_mode="acceptEdits",
|
|
199
|
+
cwd=str(repo_path.absolute()),
|
|
200
|
+
max_turns=10
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if verbose:
|
|
204
|
+
console.print("[cyan]🔍 Claude is analyzing your changes...[/cyan]\n")
|
|
205
|
+
else:
|
|
206
|
+
console.print("[cyan]🔍 Analyzing changes...[/cyan]\n")
|
|
207
|
+
|
|
208
|
+
commit_message = None
|
|
209
|
+
all_text = []
|
|
210
|
+
|
|
211
|
+
# Use rich progress for spinner
|
|
212
|
+
progress = None
|
|
213
|
+
task_id = None
|
|
214
|
+
spinner_started = False
|
|
215
|
+
|
|
216
|
+
async for message in query(prompt=prompt, options=options):
|
|
217
|
+
if isinstance(message, AssistantMessage):
|
|
218
|
+
# Stop spinner when we get content
|
|
219
|
+
if progress is not None and task_id is not None:
|
|
220
|
+
progress.stop()
|
|
221
|
+
progress = None
|
|
222
|
+
task_id = None
|
|
223
|
+
spinner_started = False
|
|
224
|
+
|
|
225
|
+
for block in message.content:
|
|
226
|
+
if isinstance(block, TextBlock):
|
|
227
|
+
text = block.text.strip()
|
|
228
|
+
all_text.append(text)
|
|
229
|
+
if verbose and text:
|
|
230
|
+
console.print(f"[dim]💭 {text}[/dim]")
|
|
231
|
+
|
|
232
|
+
elif isinstance(block, ToolUseBlock):
|
|
233
|
+
# Show what tool Claude is using (simplified output)
|
|
234
|
+
tool_name = block.name
|
|
235
|
+
tool_input = block.input
|
|
236
|
+
|
|
237
|
+
if tool_name == "Bash":
|
|
238
|
+
cmd = tool_input.get("command", "")
|
|
239
|
+
if verbose:
|
|
240
|
+
description = tool_input.get("description", "")
|
|
241
|
+
if description:
|
|
242
|
+
console.print(f" [cyan]🔧 {cmd}[/cyan] [dim]# {description}[/dim]")
|
|
243
|
+
else:
|
|
244
|
+
console.print(f" [cyan]🔧 {cmd}[/cyan]")
|
|
245
|
+
else:
|
|
246
|
+
# Non-verbose: only show git commands and other important ones
|
|
247
|
+
if cmd.startswith("git "):
|
|
248
|
+
console.print(f" [cyan]🔧 {cmd}[/cyan]")
|
|
249
|
+
|
|
250
|
+
elif tool_name == "Read":
|
|
251
|
+
file_path = tool_input.get("file_path", "")
|
|
252
|
+
if file_path:
|
|
253
|
+
import os
|
|
254
|
+
try:
|
|
255
|
+
rel_path = os.path.relpath(file_path, repo_path)
|
|
256
|
+
if verbose:
|
|
257
|
+
console.print(f" [yellow]📖 Reading {rel_path}[/yellow]")
|
|
258
|
+
else:
|
|
259
|
+
# Show just filename for non-verbose
|
|
260
|
+
if len(rel_path) > 45:
|
|
261
|
+
filename = os.path.basename(rel_path)
|
|
262
|
+
console.print(f" [yellow]📖 {filename}[/yellow]")
|
|
263
|
+
else:
|
|
264
|
+
console.print(f" [yellow]📖 {rel_path}[/yellow]")
|
|
265
|
+
except:
|
|
266
|
+
filename = os.path.basename(file_path)
|
|
267
|
+
console.print(f" [yellow]📖 {filename}[/yellow]")
|
|
268
|
+
|
|
269
|
+
elif tool_name == "Grep":
|
|
270
|
+
pattern = tool_input.get("pattern", "")
|
|
271
|
+
path = tool_input.get("path", ".")
|
|
272
|
+
if verbose:
|
|
273
|
+
console.print(f" [magenta]🔍 Searching for '{pattern}' in {path}[/magenta]")
|
|
274
|
+
elif pattern and len(pattern) <= 40:
|
|
275
|
+
console.print(f" [magenta]🔍 {pattern}[/magenta]")
|
|
276
|
+
|
|
277
|
+
elif tool_name == "Glob":
|
|
278
|
+
pattern = tool_input.get("pattern", "")
|
|
279
|
+
if pattern:
|
|
280
|
+
if verbose:
|
|
281
|
+
console.print(f" [blue]📁 Finding files matching {pattern}[/blue]")
|
|
282
|
+
else:
|
|
283
|
+
console.print(f" [blue]📁 {pattern}[/blue]")
|
|
284
|
+
|
|
285
|
+
elif isinstance(block, ToolResultBlock):
|
|
286
|
+
# Optionally show tool results in verbose mode
|
|
287
|
+
if verbose and block.content:
|
|
288
|
+
result = str(block.content)
|
|
289
|
+
if len(result) > 200:
|
|
290
|
+
result = result[:197] + "..."
|
|
291
|
+
console.print(f" [dim]↳ {result}[/dim]")
|
|
292
|
+
|
|
293
|
+
# After processing all blocks, start spinner if no output in non-verbose mode
|
|
294
|
+
# Only start spinner once, not on every message
|
|
295
|
+
if not verbose and not spinner_started:
|
|
296
|
+
if progress is None:
|
|
297
|
+
progress = Progress(
|
|
298
|
+
SpinnerColumn(),
|
|
299
|
+
TextColumn("[progress.description]{task.description}"),
|
|
300
|
+
console=console,
|
|
301
|
+
transient=True,
|
|
302
|
+
)
|
|
303
|
+
progress.start()
|
|
304
|
+
task_id = progress.add_task("⏳ Waiting for response...", total=None)
|
|
305
|
+
spinner_started = True
|
|
306
|
+
|
|
307
|
+
elif isinstance(message, ResultMessage):
|
|
308
|
+
# Stop spinner if it's running
|
|
309
|
+
if progress is not None and task_id is not None:
|
|
310
|
+
progress.stop()
|
|
311
|
+
progress = None
|
|
312
|
+
task_id = None
|
|
313
|
+
spinner_started = False
|
|
314
|
+
console.print("\n[green]✨ Analysis complete![/green]")
|
|
315
|
+
if verbose:
|
|
316
|
+
if message.total_cost_usd:
|
|
317
|
+
console.print(f"[yellow]💰 Cost: ${message.total_cost_usd:.4f}[/yellow]")
|
|
318
|
+
console.print(f"[blue]⏱️ Duration: {message.duration_ms / 1000:.2f}s[/blue]")
|
|
319
|
+
console.print(f"[cyan]🔄 Turns: {message.num_turns}[/cyan]")
|
|
320
|
+
|
|
321
|
+
if not message.is_error:
|
|
322
|
+
# Extract commit message from COMMIT_MESSAGE: marker
|
|
323
|
+
full_response = "\n".join(all_text)
|
|
324
|
+
|
|
325
|
+
# Look for COMMIT_MESSAGE: marker
|
|
326
|
+
if "COMMIT_MESSAGE:" in full_response:
|
|
327
|
+
# Extract everything after COMMIT_MESSAGE:
|
|
328
|
+
parts = full_response.split("COMMIT_MESSAGE:", 1)
|
|
329
|
+
if len(parts) > 1:
|
|
330
|
+
commit_message = parts[1].strip()
|
|
331
|
+
else:
|
|
332
|
+
# Fallback: try to extract the last meaningful text block
|
|
333
|
+
# Skip explanatory text and get the actual commit message
|
|
334
|
+
for text in reversed(all_text):
|
|
335
|
+
text = text.strip()
|
|
336
|
+
if text and not any(
|
|
337
|
+
text.lower().startswith(prefix)
|
|
338
|
+
for prefix in ["let me", "i'll", "i will", "now i", "first", "i can see"]
|
|
339
|
+
):
|
|
340
|
+
commit_message = text
|
|
341
|
+
break
|
|
342
|
+
|
|
343
|
+
# Clean up markdown code blocks if present
|
|
344
|
+
if commit_message:
|
|
345
|
+
lines = commit_message.split("\n")
|
|
346
|
+
cleaned_lines = []
|
|
347
|
+
in_code_block = False
|
|
348
|
+
|
|
349
|
+
for line in lines:
|
|
350
|
+
if line.strip().startswith("```"):
|
|
351
|
+
in_code_block = not in_code_block
|
|
352
|
+
continue
|
|
353
|
+
if not in_code_block:
|
|
354
|
+
cleaned_lines.append(line.rstrip())
|
|
355
|
+
|
|
356
|
+
commit_message = "\n".join(cleaned_lines).strip()
|
|
357
|
+
|
|
358
|
+
# Make sure progress is stopped before returning
|
|
359
|
+
if progress is not None and task_id is not None:
|
|
360
|
+
progress.stop()
|
|
361
|
+
|
|
362
|
+
return commit_message
|
|
363
|
+
|
|
364
|
+
except CLINotFoundError:
|
|
365
|
+
# Stop progress on error
|
|
366
|
+
if 'progress' in locals() and progress is not None:
|
|
367
|
+
progress.stop()
|
|
368
|
+
console.print("[red]❌ Error: Claude Code CLI not found.[/red]", file=sys.stderr)
|
|
369
|
+
console.print("[yellow]📦 Please install it: npm install -g @anthropic-ai/claude-code[/yellow]", file=sys.stderr)
|
|
370
|
+
return None
|
|
371
|
+
except ProcessError as e:
|
|
372
|
+
if 'progress' in locals() and progress is not None:
|
|
373
|
+
progress.stop()
|
|
374
|
+
console.print(f"[red]❌ Process error: {e}[/red]", file=sys.stderr)
|
|
375
|
+
if e.stderr:
|
|
376
|
+
console.print(f" stderr: {e.stderr}", file=sys.stderr)
|
|
377
|
+
return None
|
|
378
|
+
except Exception as e:
|
|
379
|
+
if 'progress' in locals() and progress is not None:
|
|
380
|
+
progress.stop()
|
|
381
|
+
console.print(f"[red]❌ Unexpected error: {e}[/red]", file=sys.stderr)
|
|
382
|
+
if verbose:
|
|
383
|
+
import traceback
|
|
384
|
+
|
|
385
|
+
traceback.print_exc()
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def handle_alias_command(args):
|
|
390
|
+
"""Handle alias management subcommands"""
|
|
391
|
+
if len(args) == 0 or args[0] == "list":
|
|
392
|
+
# List all aliases
|
|
393
|
+
config = Config()
|
|
394
|
+
aliases = config.list_aliases()
|
|
395
|
+
|
|
396
|
+
if not aliases:
|
|
397
|
+
print("📋 No aliases configured")
|
|
398
|
+
return
|
|
399
|
+
|
|
400
|
+
print("📋 Configured aliases:")
|
|
401
|
+
print()
|
|
402
|
+
max_alias_len = max(len(alias) for alias in aliases.keys())
|
|
403
|
+
|
|
404
|
+
for alias, command in sorted(aliases.items()):
|
|
405
|
+
if command:
|
|
406
|
+
print(f" {alias:<{max_alias_len}} → claude-commit {command}")
|
|
407
|
+
else:
|
|
408
|
+
print(f" {alias:<{max_alias_len}} → claude-commit")
|
|
409
|
+
|
|
410
|
+
print()
|
|
411
|
+
print("💡 Usage: claude-commit <alias> [additional args]")
|
|
412
|
+
print(" Example: claude-commit cca (expands to: claude-commit --all)")
|
|
413
|
+
print()
|
|
414
|
+
print("🔧 To use aliases directly in shell (like 'ccc' instead of 'claude-commit ccc'):")
|
|
415
|
+
print(" Run: claude-commit alias install")
|
|
416
|
+
|
|
417
|
+
elif args[0] == "install":
|
|
418
|
+
# Install shell aliases
|
|
419
|
+
config = Config()
|
|
420
|
+
aliases = config.list_aliases()
|
|
421
|
+
|
|
422
|
+
if not aliases:
|
|
423
|
+
print("📋 No aliases configured")
|
|
424
|
+
return
|
|
425
|
+
|
|
426
|
+
import platform
|
|
427
|
+
import os
|
|
428
|
+
|
|
429
|
+
# Detect shell and platform
|
|
430
|
+
shell = os.environ.get("SHELL", "")
|
|
431
|
+
system = platform.system()
|
|
432
|
+
|
|
433
|
+
# Windows detection
|
|
434
|
+
if system == "Windows":
|
|
435
|
+
# Check if running in Git Bash (has SHELL env var on Windows)
|
|
436
|
+
if shell and ("bash" in shell or "sh" in shell):
|
|
437
|
+
# Git Bash on Windows
|
|
438
|
+
rc_file = Path.home() / ".bashrc"
|
|
439
|
+
shell_name = "bash (Git Bash)"
|
|
440
|
+
else:
|
|
441
|
+
# PowerShell (default on Windows)
|
|
442
|
+
# Check for PowerShell profile
|
|
443
|
+
ps_profile = os.environ.get("USERPROFILE", "")
|
|
444
|
+
if ps_profile:
|
|
445
|
+
# PowerShell 7+ or Windows PowerShell
|
|
446
|
+
rc_file = Path(ps_profile) / "Documents" / "WindowsPowerShell" / "Microsoft.PowerShell_profile.ps1"
|
|
447
|
+
# Also check PowerShell 7+
|
|
448
|
+
ps7_profile = Path(ps_profile) / "Documents" / "PowerShell" / "Microsoft.PowerShell_profile.ps1"
|
|
449
|
+
if ps7_profile.parent.exists():
|
|
450
|
+
rc_file = ps7_profile
|
|
451
|
+
shell_name = "powershell"
|
|
452
|
+
else:
|
|
453
|
+
print("⚠️ Could not detect PowerShell profile location")
|
|
454
|
+
print()
|
|
455
|
+
print(" To manually add aliases in PowerShell, add to your $PROFILE:")
|
|
456
|
+
print()
|
|
457
|
+
for alias, command in sorted(aliases.items()):
|
|
458
|
+
if command:
|
|
459
|
+
print(f' Set-Alias -Name {alias} -Value "claude-commit {command}"')
|
|
460
|
+
else:
|
|
461
|
+
print(f' Set-Alias -Name {alias} -Value "claude-commit"')
|
|
462
|
+
return
|
|
463
|
+
# Unix-like systems
|
|
464
|
+
elif "zsh" in shell:
|
|
465
|
+
rc_file = Path.home() / ".zshrc"
|
|
466
|
+
shell_name = "zsh"
|
|
467
|
+
elif "bash" in shell:
|
|
468
|
+
rc_file = Path.home() / ".bashrc"
|
|
469
|
+
# On macOS, also check .bash_profile
|
|
470
|
+
if system == "Darwin":
|
|
471
|
+
bash_profile = Path.home() / ".bash_profile"
|
|
472
|
+
if bash_profile.exists():
|
|
473
|
+
rc_file = bash_profile
|
|
474
|
+
shell_name = "bash"
|
|
475
|
+
elif "fish" in shell:
|
|
476
|
+
# Fish shell uses different config location
|
|
477
|
+
rc_file = Path.home() / ".config" / "fish" / "config.fish"
|
|
478
|
+
shell_name = "fish"
|
|
479
|
+
else:
|
|
480
|
+
print(f"⚠️ Unknown shell: {shell or 'not detected'}")
|
|
481
|
+
print(" Supported shells: bash, zsh, fish, powershell (Windows)")
|
|
482
|
+
print()
|
|
483
|
+
print(" To manually add aliases, add these lines to your shell config:")
|
|
484
|
+
print()
|
|
485
|
+
for alias, command in sorted(aliases.items()):
|
|
486
|
+
if command:
|
|
487
|
+
print(f" alias {alias}='claude-commit {command}'")
|
|
488
|
+
else:
|
|
489
|
+
print(f" alias {alias}='claude-commit'")
|
|
490
|
+
return
|
|
491
|
+
|
|
492
|
+
# Generate alias commands (different syntax for PowerShell)
|
|
493
|
+
if shell_name == "powershell":
|
|
494
|
+
alias_lines = ["", "# claude-commit aliases (auto-generated)"]
|
|
495
|
+
for alias, command in sorted(aliases.items()):
|
|
496
|
+
if command:
|
|
497
|
+
# PowerShell doesn't support Set-Alias with arguments, use function instead
|
|
498
|
+
alias_lines.append(f"function {alias} {{ claude-commit {command} $args }}")
|
|
499
|
+
else:
|
|
500
|
+
alias_lines.append(f"function {alias} {{ claude-commit $args }}")
|
|
501
|
+
alias_lines.append("")
|
|
502
|
+
else:
|
|
503
|
+
# Unix-style shells (bash, zsh, fish)
|
|
504
|
+
alias_lines = ["", "# claude-commit aliases (auto-generated)"]
|
|
505
|
+
for alias, command in sorted(aliases.items()):
|
|
506
|
+
if command:
|
|
507
|
+
alias_lines.append(f"alias {alias}='claude-commit {command}'")
|
|
508
|
+
else:
|
|
509
|
+
alias_lines.append(f"alias {alias}='claude-commit'")
|
|
510
|
+
alias_lines.append("")
|
|
511
|
+
|
|
512
|
+
alias_block = "\n".join(alias_lines)
|
|
513
|
+
|
|
514
|
+
print(f"📝 Generated shell aliases for {shell_name}:")
|
|
515
|
+
print(alias_block)
|
|
516
|
+
print()
|
|
517
|
+
|
|
518
|
+
# Check if aliases already exist
|
|
519
|
+
if rc_file.exists():
|
|
520
|
+
content = rc_file.read_text()
|
|
521
|
+
if "# claude-commit aliases" in content:
|
|
522
|
+
print(f"⚠️ Aliases already exist in {rc_file}")
|
|
523
|
+
response = input(" Replace existing aliases? [Y/n]: ").strip().lower()
|
|
524
|
+
if response == "n" or response == "no":
|
|
525
|
+
print("❌ Installation cancelled")
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
# Remove old aliases
|
|
529
|
+
lines = content.split("\n")
|
|
530
|
+
new_lines = []
|
|
531
|
+
skip = False
|
|
532
|
+
for line in lines:
|
|
533
|
+
if "# claude-commit aliases" in line:
|
|
534
|
+
skip = True
|
|
535
|
+
elif skip and (line.strip() == "" or not line.startswith("alias ")):
|
|
536
|
+
skip = False
|
|
537
|
+
|
|
538
|
+
if not skip:
|
|
539
|
+
new_lines.append(line)
|
|
540
|
+
|
|
541
|
+
content = "\n".join(new_lines)
|
|
542
|
+
else:
|
|
543
|
+
content = ""
|
|
544
|
+
|
|
545
|
+
# Append new aliases
|
|
546
|
+
new_content = content.rstrip() + alias_block + "\n"
|
|
547
|
+
|
|
548
|
+
try:
|
|
549
|
+
# Ensure directory exists (especially for PowerShell profile)
|
|
550
|
+
rc_file.parent.mkdir(parents=True, exist_ok=True)
|
|
551
|
+
|
|
552
|
+
rc_file.write_text(new_content)
|
|
553
|
+
print(f"✅ Aliases installed to {rc_file}")
|
|
554
|
+
print()
|
|
555
|
+
|
|
556
|
+
# Show activation instructions (different for PowerShell)
|
|
557
|
+
if shell_name == "powershell":
|
|
558
|
+
print("📋 To activate aliases in your current PowerShell session, run:")
|
|
559
|
+
print()
|
|
560
|
+
print(f" \033[1;36m. {rc_file}\033[0m")
|
|
561
|
+
print()
|
|
562
|
+
print("Or restart PowerShell.")
|
|
563
|
+
print()
|
|
564
|
+
print("💡 Note: You may need to run this first to allow script execution:")
|
|
565
|
+
print(" Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser")
|
|
566
|
+
else:
|
|
567
|
+
print("📋 To activate aliases in your current shell, run:")
|
|
568
|
+
print()
|
|
569
|
+
print(f" \033[1;36msource {rc_file}\033[0m")
|
|
570
|
+
print()
|
|
571
|
+
print("Or copy and paste this command:")
|
|
572
|
+
print(f" \033[1;32msource {rc_file} && echo '✅ Aliases activated! Try: ccc'\033[0m")
|
|
573
|
+
print()
|
|
574
|
+
print("💡 Aliases will be automatically available in new terminal windows.")
|
|
575
|
+
except Exception as e:
|
|
576
|
+
print(f"❌ Failed to write to {rc_file}: {e}", file=sys.stderr)
|
|
577
|
+
sys.exit(1)
|
|
578
|
+
|
|
579
|
+
elif args[0] == "uninstall":
|
|
580
|
+
# Remove shell aliases
|
|
581
|
+
import platform
|
|
582
|
+
import os
|
|
583
|
+
|
|
584
|
+
shell = os.environ.get("SHELL", "")
|
|
585
|
+
|
|
586
|
+
if "zsh" in shell:
|
|
587
|
+
rc_file = Path.home() / ".zshrc"
|
|
588
|
+
elif "bash" in shell:
|
|
589
|
+
rc_file = Path.home() / ".bashrc"
|
|
590
|
+
if platform.system() == "Darwin":
|
|
591
|
+
bash_profile = Path.home() / ".bash_profile"
|
|
592
|
+
if bash_profile.exists():
|
|
593
|
+
rc_file = bash_profile
|
|
594
|
+
else:
|
|
595
|
+
print(f"⚠️ Unknown shell: {shell}")
|
|
596
|
+
return
|
|
597
|
+
|
|
598
|
+
if not rc_file.exists():
|
|
599
|
+
print(f"❌ {rc_file} not found")
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
content = rc_file.read_text()
|
|
603
|
+
|
|
604
|
+
if "# claude-commit aliases" not in content:
|
|
605
|
+
print(f"📋 No claude-commit aliases found in {rc_file}")
|
|
606
|
+
return
|
|
607
|
+
|
|
608
|
+
# Remove aliases block
|
|
609
|
+
lines = content.split("\n")
|
|
610
|
+
new_lines = []
|
|
611
|
+
skip = False
|
|
612
|
+
removed = False
|
|
613
|
+
|
|
614
|
+
for line in lines:
|
|
615
|
+
if "# claude-commit aliases" in line:
|
|
616
|
+
skip = True
|
|
617
|
+
removed = True
|
|
618
|
+
elif skip and (line.strip() == "" or not line.startswith("alias ")):
|
|
619
|
+
skip = False
|
|
620
|
+
|
|
621
|
+
if not skip:
|
|
622
|
+
new_lines.append(line)
|
|
623
|
+
|
|
624
|
+
if removed:
|
|
625
|
+
rc_file.write_text("\n".join(new_lines))
|
|
626
|
+
print(f"✅ Aliases removed from {rc_file}")
|
|
627
|
+
print()
|
|
628
|
+
print("🔄 To apply changes, run:")
|
|
629
|
+
print(f" source {rc_file}")
|
|
630
|
+
print()
|
|
631
|
+
print(" Or open a new terminal window.")
|
|
632
|
+
|
|
633
|
+
elif args[0] == "set":
|
|
634
|
+
# Set an alias
|
|
635
|
+
if len(args) < 2:
|
|
636
|
+
print("❌ Error: Please provide alias name", file=sys.stderr)
|
|
637
|
+
print(" Usage: claude-commit alias set <name> [command]", file=sys.stderr)
|
|
638
|
+
sys.exit(1)
|
|
639
|
+
|
|
640
|
+
alias_name = args[1]
|
|
641
|
+
command = " ".join(args[2:]) if len(args) > 2 else ""
|
|
642
|
+
|
|
643
|
+
config = Config()
|
|
644
|
+
config.set_alias(alias_name, command)
|
|
645
|
+
|
|
646
|
+
if command:
|
|
647
|
+
print(f"✅ Alias '{alias_name}' set to: claude-commit {command}")
|
|
648
|
+
else:
|
|
649
|
+
print(f"✅ Alias '{alias_name}' set to: claude-commit")
|
|
650
|
+
|
|
651
|
+
elif args[0] == "unset":
|
|
652
|
+
# Delete an alias
|
|
653
|
+
if len(args) < 2:
|
|
654
|
+
print("❌ Error: Please provide alias name", file=sys.stderr)
|
|
655
|
+
print(" Usage: claude-commit alias unset <name>", file=sys.stderr)
|
|
656
|
+
sys.exit(1)
|
|
657
|
+
|
|
658
|
+
alias_name = args[1]
|
|
659
|
+
config = Config()
|
|
660
|
+
|
|
661
|
+
if config.delete_alias(alias_name):
|
|
662
|
+
print(f"✅ Alias '{alias_name}' removed")
|
|
663
|
+
else:
|
|
664
|
+
print(f"❌ Alias '{alias_name}' not found", file=sys.stderr)
|
|
665
|
+
sys.exit(1)
|
|
666
|
+
|
|
667
|
+
else:
|
|
668
|
+
print(f"❌ Unknown alias command: {args[0]}", file=sys.stderr)
|
|
669
|
+
print(" Available commands: list, set, unset, install, uninstall", file=sys.stderr)
|
|
670
|
+
sys.exit(1)
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def show_first_run_tip():
|
|
674
|
+
"""Show helpful tip on first run"""
|
|
675
|
+
welcome_text = """[bold]👋 Welcome to claude-commit![/bold]
|
|
676
|
+
|
|
677
|
+
[yellow]💡 Tip:[/yellow] Install shell aliases for faster usage:
|
|
678
|
+
[cyan]claude-commit alias install[/cyan]
|
|
679
|
+
|
|
680
|
+
After installation, use short commands like:
|
|
681
|
+
• [green]ccc[/green] → auto-commit
|
|
682
|
+
• [green]cca[/green] → analyze all changes
|
|
683
|
+
• [green]ccp[/green] → preview message
|
|
684
|
+
|
|
685
|
+
Run '[cyan]claude-commit alias list[/cyan]' to see all aliases.
|
|
686
|
+
"""
|
|
687
|
+
console.print()
|
|
688
|
+
console.print(Panel(welcome_text, border_style="blue", padding=(1, 2)))
|
|
689
|
+
console.print()
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def main():
|
|
693
|
+
"""Main CLI entry point."""
|
|
694
|
+
# Check if this is the first run
|
|
695
|
+
config = Config()
|
|
696
|
+
if config.is_first_run() and len(sys.argv) > 1 and sys.argv[1] not in ["alias", "-h", "--help"]:
|
|
697
|
+
show_first_run_tip()
|
|
698
|
+
config.mark_first_run_complete()
|
|
699
|
+
|
|
700
|
+
# Check if first argument is 'alias' command
|
|
701
|
+
if len(sys.argv) > 1 and sys.argv[1] == "alias":
|
|
702
|
+
handle_alias_command(sys.argv[2:])
|
|
703
|
+
return
|
|
704
|
+
|
|
705
|
+
# Resolve any aliases in the arguments
|
|
706
|
+
resolved_args = resolve_alias(sys.argv[1:])
|
|
707
|
+
|
|
708
|
+
parser = argparse.ArgumentParser(
|
|
709
|
+
description="Generate AI-powered git commit messages using Claude",
|
|
710
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
711
|
+
epilog="""
|
|
712
|
+
Examples:
|
|
713
|
+
# Generate commit message for staged changes
|
|
714
|
+
claude-commit
|
|
715
|
+
|
|
716
|
+
# Generate message for all changes (staged + unstaged)
|
|
717
|
+
claude-commit --all
|
|
718
|
+
|
|
719
|
+
# Show verbose output with analysis details
|
|
720
|
+
claude-commit --verbose
|
|
721
|
+
|
|
722
|
+
# Generate message and copy to clipboard (requires pbcopy/xclip)
|
|
723
|
+
claude-commit --copy
|
|
724
|
+
|
|
725
|
+
# Automatically commit with generated message
|
|
726
|
+
claude-commit --commit
|
|
727
|
+
|
|
728
|
+
# Preview without committing
|
|
729
|
+
claude-commit --preview
|
|
730
|
+
|
|
731
|
+
Alias Management:
|
|
732
|
+
# List all aliases
|
|
733
|
+
claude-commit alias list
|
|
734
|
+
|
|
735
|
+
# Install shell aliases (so you can use 'ccc' directly)
|
|
736
|
+
claude-commit alias install
|
|
737
|
+
|
|
738
|
+
# Set a custom alias
|
|
739
|
+
claude-commit alias set cca --all
|
|
740
|
+
claude-commit alias set ccv --verbose
|
|
741
|
+
claude-commit alias set ccac --all --commit
|
|
742
|
+
|
|
743
|
+
# Remove an alias
|
|
744
|
+
claude-commit alias unset cca
|
|
745
|
+
|
|
746
|
+
# Uninstall shell aliases
|
|
747
|
+
claude-commit alias uninstall
|
|
748
|
+
|
|
749
|
+
# Use an alias (after install)
|
|
750
|
+
cca (expands to: claude-commit --all)
|
|
751
|
+
ccc (expands to: claude-commit --commit)
|
|
752
|
+
""",
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
parser.add_argument(
|
|
756
|
+
"-a",
|
|
757
|
+
"--all",
|
|
758
|
+
action="store_true",
|
|
759
|
+
help="Analyze all changes, not just staged ones",
|
|
760
|
+
)
|
|
761
|
+
parser.add_argument(
|
|
762
|
+
"-v",
|
|
763
|
+
"--verbose",
|
|
764
|
+
action="store_true",
|
|
765
|
+
help="Show detailed analysis and processing information",
|
|
766
|
+
)
|
|
767
|
+
parser.add_argument(
|
|
768
|
+
"-p",
|
|
769
|
+
"--path",
|
|
770
|
+
type=Path,
|
|
771
|
+
default=None,
|
|
772
|
+
help="Path to git repository (defaults to current directory)",
|
|
773
|
+
)
|
|
774
|
+
parser.add_argument(
|
|
775
|
+
"--max-diff-lines",
|
|
776
|
+
type=int,
|
|
777
|
+
default=500,
|
|
778
|
+
help="Maximum number of diff lines to analyze (default: 500)",
|
|
779
|
+
)
|
|
780
|
+
parser.add_argument(
|
|
781
|
+
"-c",
|
|
782
|
+
"--commit",
|
|
783
|
+
action="store_true",
|
|
784
|
+
help="Automatically commit with the generated message",
|
|
785
|
+
)
|
|
786
|
+
parser.add_argument(
|
|
787
|
+
"--copy",
|
|
788
|
+
action="store_true",
|
|
789
|
+
help="Copy the generated message to clipboard",
|
|
790
|
+
)
|
|
791
|
+
parser.add_argument(
|
|
792
|
+
"--preview",
|
|
793
|
+
action="store_true",
|
|
794
|
+
help="Just preview the message without any action",
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
args = parser.parse_args(resolved_args)
|
|
798
|
+
|
|
799
|
+
# Run async function
|
|
800
|
+
try:
|
|
801
|
+
commit_message = asyncio.run(
|
|
802
|
+
generate_commit_message(
|
|
803
|
+
repo_path=args.path,
|
|
804
|
+
staged_only=not args.all,
|
|
805
|
+
verbose=args.verbose,
|
|
806
|
+
max_diff_lines=args.max_diff_lines,
|
|
807
|
+
)
|
|
808
|
+
)
|
|
809
|
+
except KeyboardInterrupt:
|
|
810
|
+
console.print("\n[yellow]⚠️ Interrupted by user[/yellow]", file=sys.stderr)
|
|
811
|
+
sys.exit(130)
|
|
812
|
+
|
|
813
|
+
if not commit_message:
|
|
814
|
+
console.print("[red]❌ Failed to generate commit message[/red]", file=sys.stderr)
|
|
815
|
+
sys.exit(1)
|
|
816
|
+
|
|
817
|
+
# Display the generated message with rich formatting
|
|
818
|
+
console.print()
|
|
819
|
+
console.print(Panel(
|
|
820
|
+
commit_message,
|
|
821
|
+
title="[bold]📝 Generated Commit Message[/bold]",
|
|
822
|
+
border_style="green",
|
|
823
|
+
padding=(1, 2)
|
|
824
|
+
))
|
|
825
|
+
|
|
826
|
+
# Handle different output modes
|
|
827
|
+
if args.preview:
|
|
828
|
+
console.print("\n[green]✅ Preview complete (no action taken)[/green]")
|
|
829
|
+
return
|
|
830
|
+
|
|
831
|
+
if args.copy:
|
|
832
|
+
try:
|
|
833
|
+
pyperclip.copy(commit_message)
|
|
834
|
+
console.print("\n[green]✅ Commit message copied to clipboard![/green]")
|
|
835
|
+
except Exception as e:
|
|
836
|
+
console.print(f"\n[yellow]⚠️ Failed to copy to clipboard: {e}[/yellow]", file=sys.stderr)
|
|
837
|
+
|
|
838
|
+
if args.commit:
|
|
839
|
+
try:
|
|
840
|
+
import subprocess
|
|
841
|
+
|
|
842
|
+
# Confirm before committing
|
|
843
|
+
response = console.input("\n[yellow]❓ Commit with this message? [Y/n]:[/yellow] ").strip().lower()
|
|
844
|
+
if response == "n" or response == "no":
|
|
845
|
+
console.print("[red]❌ Commit cancelled[/red]")
|
|
846
|
+
return
|
|
847
|
+
|
|
848
|
+
# Execute git commit
|
|
849
|
+
result = subprocess.run(
|
|
850
|
+
["git", "commit", "-m", commit_message],
|
|
851
|
+
capture_output=True,
|
|
852
|
+
text=True,
|
|
853
|
+
check=True,
|
|
854
|
+
)
|
|
855
|
+
console.print("\n[green]✅ Successfully committed![/green]")
|
|
856
|
+
if result.stdout:
|
|
857
|
+
console.print(result.stdout)
|
|
858
|
+
except subprocess.CalledProcessError as e:
|
|
859
|
+
console.print(f"\n[red]❌ Failed to commit: {e}[/red]", file=sys.stderr)
|
|
860
|
+
if e.stderr:
|
|
861
|
+
console.print(e.stderr, file=sys.stderr)
|
|
862
|
+
sys.exit(1)
|
|
863
|
+
except Exception as e:
|
|
864
|
+
console.print(f"\n[red]❌ Unexpected error during commit: {e}[/red]", file=sys.stderr)
|
|
865
|
+
sys.exit(1)
|
|
866
|
+
else:
|
|
867
|
+
# Default: just show the command
|
|
868
|
+
console.print("\n[dim]💡 To commit with this message, run:[/dim]")
|
|
869
|
+
# Escape single quotes in the message for shell
|
|
870
|
+
escaped_message = commit_message.replace("'", "'\\''")
|
|
871
|
+
console.print(f" [cyan]git commit -m '{escaped_message}'[/cyan]")
|
|
872
|
+
console.print("\n[dim]Or use: claude-commit --commit[/dim]")
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
if __name__ == "__main__":
|
|
876
|
+
main()
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: claude-commit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI-powered git commit message generator using Claude Agent SDK
|
|
5
|
+
Author-email: Johannlai <johannli666@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: git,commit,ai,claude,automation
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: claude-agent-sdk>=0.1.0
|
|
20
|
+
Requires-Dist: click>=8.0.0
|
|
21
|
+
Requires-Dist: rich>=13.0.0
|
|
22
|
+
Requires-Dist: pyperclip>=1.8.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
26
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
27
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# claude-commit
|
|
31
|
+
|
|
32
|
+
🤖 AI-powered git commit message generator using Claude Agent SDK and Claude Code CLI
|
|
33
|
+
|
|
34
|
+
## What is this?
|
|
35
|
+
|
|
36
|
+
`claude-commit` uses Claude AI to analyze your code changes and write meaningful commit messages. Claude reads your files, understands the context, and generates commit messages following best practices.
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
**Install:**
|
|
41
|
+
```bash
|
|
42
|
+
pip install claude-commit
|
|
43
|
+
|
|
44
|
+
# Required dependency
|
|
45
|
+
npm install -g @anthropic-ai/claude-code
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Use:**
|
|
49
|
+
```bash
|
|
50
|
+
git add .
|
|
51
|
+
claude-commit --commit
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
That's it! Claude will analyze your changes and create a commit.
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
### Prerequisites
|
|
59
|
+
|
|
60
|
+
- Python 3.10+
|
|
61
|
+
- Node.js
|
|
62
|
+
- Git
|
|
63
|
+
|
|
64
|
+
### Install Steps
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# 1. Install Claude Code CLI (required)
|
|
68
|
+
npm install -g @anthropic-ai/claude-code
|
|
69
|
+
|
|
70
|
+
# 2. Install claude-commit
|
|
71
|
+
pip install claude-commit
|
|
72
|
+
|
|
73
|
+
# Or use pipx for isolation
|
|
74
|
+
pipx install claude-commit
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Authentication
|
|
78
|
+
|
|
79
|
+
`claude-commit` supports two ways to authenticate with Claude:
|
|
80
|
+
|
|
81
|
+
**Option 1: [Official Claude Code Login](https://docs.claude.com/en/docs/claude-code/quickstart#step-2%3A-log-in-to-your-account) (Recommended)**
|
|
82
|
+
|
|
83
|
+
**Option 2: Custom API Endpoint (Environment Variables)**
|
|
84
|
+
|
|
85
|
+
For custom Claude API endpoints or proxies, set these environment variables:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# Required: Set custom endpoint and credentials
|
|
89
|
+
export ANTHROPIC_BASE_URL="https://your-endpoint.com/api/v1"
|
|
90
|
+
export ANTHROPIC_AUTH_TOKEN="your-auth-token"
|
|
91
|
+
|
|
92
|
+
# Optional: Specify custom model name
|
|
93
|
+
export ANTHROPIC_MODEL="your-model-name"
|
|
94
|
+
|
|
95
|
+
# Then use claude-commit normally
|
|
96
|
+
claude-commit --commit
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Add these to your `~/.zshrc` or `~/.bashrc` to persist across sessions.
|
|
100
|
+
|
|
101
|
+
## Usage
|
|
102
|
+
|
|
103
|
+
### Basic Commands
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Generate commit message (default: staged changes only)
|
|
107
|
+
claude-commit
|
|
108
|
+
|
|
109
|
+
# Auto-commit with generated message
|
|
110
|
+
claude-commit --commit
|
|
111
|
+
|
|
112
|
+
# Include all changes (staged + unstaged)
|
|
113
|
+
claude-commit --all
|
|
114
|
+
|
|
115
|
+
# Copy message to clipboard
|
|
116
|
+
claude-commit --copy
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Common Options
|
|
120
|
+
|
|
121
|
+
| Option | Description |
|
|
122
|
+
| -------------------- | ---------------------------------- |
|
|
123
|
+
| `-a, --all` | Include unstaged changes |
|
|
124
|
+
| `-c, --commit` | Auto-commit with generated message |
|
|
125
|
+
| `--copy` | Copy message to clipboard |
|
|
126
|
+
| `--preview` | Preview message only |
|
|
127
|
+
| `-v, --verbose` | Show detailed analysis |
|
|
128
|
+
| `-p, --path PATH` | Specify repository path |
|
|
129
|
+
| `--max-diff-lines N` | Limit diff lines (default: 500) |
|
|
130
|
+
|
|
131
|
+
## Aliases
|
|
132
|
+
|
|
133
|
+
Create shortcuts for common commands:
|
|
134
|
+
|
|
135
|
+
### Install Shell Aliases
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# Install to your shell config
|
|
139
|
+
claude-commit alias install
|
|
140
|
+
|
|
141
|
+
# Activate in current terminal
|
|
142
|
+
source ~/.zshrc # zsh
|
|
143
|
+
source ~/.bashrc # bash
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Default Aliases
|
|
147
|
+
|
|
148
|
+
| Alias | Command | Description |
|
|
149
|
+
| ------- | ------------------------------ | ------------------- |
|
|
150
|
+
| `ccc` | `claude-commit --commit` | Quick commit |
|
|
151
|
+
| `ccp` | `claude-commit --preview` | Preview message |
|
|
152
|
+
| `cca` | `claude-commit --all` | Include all changes |
|
|
153
|
+
| `ccac` | `claude-commit --all --commit` | Commit all changes |
|
|
154
|
+
| `ccopy` | `claude-commit --copy` | Copy to clipboard |
|
|
155
|
+
|
|
156
|
+
After installation, just use:
|
|
157
|
+
```bash
|
|
158
|
+
git add .
|
|
159
|
+
ccc # analyzes and commits
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Custom Aliases
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# Create your own aliases
|
|
166
|
+
claude-commit alias set quick --all --commit
|
|
167
|
+
claude-commit alias list
|
|
168
|
+
claude-commit alias unset quick
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## How It Works
|
|
172
|
+
|
|
173
|
+
Claude autonomously analyzes your changes:
|
|
174
|
+
|
|
175
|
+
1. **Reads** your modified files to understand context
|
|
176
|
+
2. **Searches** the codebase for related code
|
|
177
|
+
3. **Understands** the intent and impact of changes
|
|
178
|
+
4. **Generates** a clear commit message following conventions
|
|
179
|
+
|
|
180
|
+
**Example:**
|
|
181
|
+
```
|
|
182
|
+
feat: add JWT authentication
|
|
183
|
+
|
|
184
|
+
Implement secure authentication system with token refresh.
|
|
185
|
+
Includes login, logout, and session management.
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Examples
|
|
189
|
+
|
|
190
|
+
### Typical Workflow
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Make changes
|
|
194
|
+
git add .
|
|
195
|
+
|
|
196
|
+
# Preview message
|
|
197
|
+
claude-commit --preview
|
|
198
|
+
|
|
199
|
+
# Commit if satisfied
|
|
200
|
+
claude-commit --commit
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### With Aliases
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# Make changes
|
|
207
|
+
git add .
|
|
208
|
+
|
|
209
|
+
# Quick commit
|
|
210
|
+
ccc
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Large Changes
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
# Limit analysis for faster results
|
|
217
|
+
claude-commit --max-diff-lines 200 --commit
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Configuration
|
|
221
|
+
|
|
222
|
+
Configuration files:
|
|
223
|
+
- Aliases: `~/.claude-commit/config.json`
|
|
224
|
+
- Shell integration: `~/.zshrc`, `~/.bashrc`, or `$PROFILE`
|
|
225
|
+
|
|
226
|
+
## Platform Support
|
|
227
|
+
|
|
228
|
+
| Platform | Status | Shells |
|
|
229
|
+
| -------- | ------ | -------------------- |
|
|
230
|
+
| macOS | ✅ | zsh, bash, fish |
|
|
231
|
+
| Linux | ✅ | bash, zsh, fish |
|
|
232
|
+
| Windows | ✅ | PowerShell, Git Bash |
|
|
233
|
+
|
|
234
|
+
**Windows PowerShell** first-time setup:
|
|
235
|
+
```powershell
|
|
236
|
+
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Troubleshooting
|
|
240
|
+
|
|
241
|
+
**Claude Code not found?**
|
|
242
|
+
```bash
|
|
243
|
+
npm install -g @anthropic-ai/claude-code
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**No changes detected?**
|
|
247
|
+
```bash
|
|
248
|
+
git add . # stage changes
|
|
249
|
+
# or
|
|
250
|
+
claude-commit --all # include unstaged
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Analysis too slow?**
|
|
254
|
+
```bash
|
|
255
|
+
claude-commit --max-diff-lines 200
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Development
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
# Clone and setup
|
|
262
|
+
git clone https://github.com/yourusername/claude-commit.git
|
|
263
|
+
cd claude-commit
|
|
264
|
+
python -m venv venv
|
|
265
|
+
source venv/bin/activate
|
|
266
|
+
pip install -e ".[dev]"
|
|
267
|
+
|
|
268
|
+
# Run tests
|
|
269
|
+
pytest tests/
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Contributing
|
|
273
|
+
|
|
274
|
+
Contributions welcome! Please:
|
|
275
|
+
1. Fork the repository
|
|
276
|
+
2. Create a feature branch
|
|
277
|
+
3. Make your changes
|
|
278
|
+
4. Submit a Pull Request
|
|
279
|
+
|
|
280
|
+
## License
|
|
281
|
+
|
|
282
|
+
MIT License - see [LICENSE](LICENSE) file
|
|
283
|
+
|
|
284
|
+
## Links
|
|
285
|
+
|
|
286
|
+
- [Claude Agent SDK](https://docs.anthropic.com/en/docs/claude-code/agent-sdk)
|
|
287
|
+
- [Conventional Commits](https://www.conventionalcommits.org/)
|
|
288
|
+
- [Issue Tracker](https://github.com/yourusername/claude-commit/issues)
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
Made with ❤️ by [Johann Lai](https://x.com/programerjohann)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
claude_commit/__init__.py,sha256=6fjmGt5STBtXIsuMl_tyAuzFVD11gKIzGAi5DKfhfzY,229
|
|
2
|
+
claude_commit/config.py,sha256=FH2cuzliS4MLosoxinlIgVsPNE5Hh-wmQO3oSlvPNNU,4598
|
|
3
|
+
claude_commit/main.py,sha256=6nMc6rM3muYPcLAdrMq-UiHR2zCAk3v6DTYyKKg31HM,34258
|
|
4
|
+
claude_commit-0.1.0.dist-info/licenses/LICENSE,sha256=99lkvv3fc8S64oQW5l4XpX9l-Q_NJaGeI7SV1sWh8f8,1084
|
|
5
|
+
claude_commit-0.1.0.dist-info/METADATA,sha256=8goUwmYztrSOhk5lxcYELEmGXQIvm6j5jeynogy0rhk,6837
|
|
6
|
+
claude_commit-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
claude_commit-0.1.0.dist-info/entry_points.txt,sha256=yHa9XQ5bPe_MxLGKWFG7XvmDfCB6weRZFDcEvdZG74k,58
|
|
8
|
+
claude_commit-0.1.0.dist-info/top_level.txt,sha256=e6emPTq4dNMZ3nUn4PFyxiOVNJuKXE_82ft2BqDjQZ4,14
|
|
9
|
+
claude_commit-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 claude-commit contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
claude_commit
|