commitcraft 0.2.0
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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +232 -0
- data/exe/commitcraft +11 -0
- data/lib/commitcraft/ai_client.rb +124 -0
- data/lib/commitcraft/cli.rb +220 -0
- data/lib/commitcraft/configuration.rb +66 -0
- data/lib/commitcraft/errors.rb +9 -0
- data/lib/commitcraft/git_client.rb +84 -0
- data/lib/commitcraft/message_generator.rb +85 -0
- data/lib/commitcraft/mock_ai_client.rb +100 -0
- data/lib/commitcraft/version.rb +5 -0
- data/lib/commitcraft.rb +32 -0
- metadata +187 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7e514536174168988c5ab69979cd03087275c27a070a08b75c764d04f9ee2d63
|
|
4
|
+
data.tar.gz: 0aee9c93f8a43c25f1fe46ba83daf73f70776f1a6db7258966a0052559d5e56c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0c663dcd43da6f431955d6825393449dc7c6312002cf199c16f41c4a13eb02ff2ef0d14b901a07e9b3ddd4a0f617db6469c06203d21b819e4e3191691469731f
|
|
7
|
+
data.tar.gz: e7eec0574248aebdeee7da1f1a9945ae55e393dce0c6e36b603299aee45c1a089e4e93838cb75f688139242c6ff5af2bb5ce93b4633bbb8ab165c63a71de563b
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CommitCraft
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# CommitCraft
|
|
2
|
+
|
|
3
|
+
**AI-powered git commit message generator using FREE Google Gemini API**
|
|
4
|
+
|
|
5
|
+
CommitCraft uses Google's Gemini AI to analyze your code changes and automatically generate meaningful, conventional commit messages. Completely FREE - no credit card required.
|
|
6
|
+
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 100% FREE - Uses Google's free Gemini API
|
|
12
|
+
- AI-Powered - Uses latest Gemini models to understand your code
|
|
13
|
+
- Multiple Suggestions - Get 3 different commit message options
|
|
14
|
+
- Multiple Styles - Conventional, Semantic, Descriptive, or Custom
|
|
15
|
+
- Jira Integration - Automatic ticket key prepending
|
|
16
|
+
- Interactive CLI - Beautiful terminal interface
|
|
17
|
+
- Context-Aware - Considers branch names, files, and history
|
|
18
|
+
- Fast & Easy - One command to generate and commit
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
gem install commitcraft
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or add to your Gemfile:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
gem 'commitcraft'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Setup
|
|
33
|
+
|
|
34
|
+
### Get Your FREE API Key
|
|
35
|
+
|
|
36
|
+
1. Go to https://aistudio.google.com/app/apikey
|
|
37
|
+
2. Click "Create API Key"
|
|
38
|
+
3. Copy your key
|
|
39
|
+
|
|
40
|
+
### Set Your API Key
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
export GEMINI_API_KEY='your-key-here'
|
|
44
|
+
|
|
45
|
+
# Make it permanent:
|
|
46
|
+
echo 'export GEMINI_API_KEY="your-key"' >> ~/.bashrc
|
|
47
|
+
source ~/.bashrc
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Use It
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cd your-project
|
|
54
|
+
git add .
|
|
55
|
+
commitcraft generate
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
### Basic Usage
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
git add .
|
|
64
|
+
commitcraft generate
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
CommitCraft will:
|
|
68
|
+
- Analyze your staged changes
|
|
69
|
+
- Generate 3 commit message suggestions
|
|
70
|
+
- Let you choose one (or write your own)
|
|
71
|
+
- Commit with your selected message
|
|
72
|
+
|
|
73
|
+
### Command Options
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
commitcraft generate --auto-commit # Auto-commit with first suggestion
|
|
77
|
+
commitcraft generate --style semantic # Use specific commit style
|
|
78
|
+
commitcraft generate --all # Include all changes
|
|
79
|
+
commitcraft generate --amend # Amend previous commit
|
|
80
|
+
commitcraft generate --include-history # Include commit history as context
|
|
81
|
+
commitcraft generate --jira CF-123 # Add Jira ticket key
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Jira Integration
|
|
85
|
+
|
|
86
|
+
Automatically prepend Jira ticket keys to your commit messages:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# One-time Jira key
|
|
90
|
+
commitcraft generate --jira CF-123
|
|
91
|
+
|
|
92
|
+
# Short form
|
|
93
|
+
commitcraft generate -j CF-123
|
|
94
|
+
|
|
95
|
+
# Combined with other options
|
|
96
|
+
commitcraft generate -j CF-123 -s semantic -y
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Output:**
|
|
100
|
+
```
|
|
101
|
+
Choose a commit message:
|
|
102
|
+
[CF-123] feat(auth): add OAuth2 authentication
|
|
103
|
+
[CF-123] feat: implement user login system
|
|
104
|
+
[CF-123] Add authentication with OAuth2
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Set default Jira prefix:**
|
|
108
|
+
```bash
|
|
109
|
+
# Set once for all commits
|
|
110
|
+
commitcraft config --jira-prefix CF-123
|
|
111
|
+
|
|
112
|
+
# Now every commit includes CF-123 automatically
|
|
113
|
+
commitcraft generate
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Supported formats:**
|
|
117
|
+
- `PROJECT-123`
|
|
118
|
+
- `ABC-456`
|
|
119
|
+
- `JIRA-789`
|
|
120
|
+
|
|
121
|
+
### Commit Styles
|
|
122
|
+
|
|
123
|
+
**Conventional (default)**
|
|
124
|
+
```
|
|
125
|
+
feat(auth): add OAuth2 login flow
|
|
126
|
+
fix(api): resolve null pointer error
|
|
127
|
+
docs(readme): update installation steps
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Semantic**
|
|
131
|
+
```
|
|
132
|
+
Add user authentication middleware
|
|
133
|
+
Fix memory leak in background worker
|
|
134
|
+
Update dependencies to latest versions
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Descriptive**
|
|
138
|
+
```
|
|
139
|
+
Implement caching to improve response time by 40%
|
|
140
|
+
Refactor database queries to eliminate N+1 problems
|
|
141
|
+
Add comprehensive error handling for edge cases
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Configuration
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
commitcraft config --show # Show current config
|
|
148
|
+
commitcraft config --style conventional # Set default style
|
|
149
|
+
commitcraft config --model gemini-2.5-flash # Set AI model
|
|
150
|
+
commitcraft config --jira-prefix CF-123 # Set default Jira prefix
|
|
151
|
+
commitcraft status # View git status
|
|
152
|
+
commitcraft version # Check version
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Available Models (All FREE)
|
|
156
|
+
|
|
157
|
+
- `gemini-2.5-flash` (default) - Best balance
|
|
158
|
+
- `gemini-2.5-pro` - Highest quality
|
|
159
|
+
- `gemini-2.5-flash-lite` - Fastest
|
|
160
|
+
- `gemini-2.0-flash` - Fast and versatile
|
|
161
|
+
- `gemini-flash-latest` - Auto-updates to latest
|
|
162
|
+
|
|
163
|
+
### Git Alias
|
|
164
|
+
|
|
165
|
+
Add to `~/.gitconfig`:
|
|
166
|
+
|
|
167
|
+
```ini
|
|
168
|
+
[alias]
|
|
169
|
+
ai = !commitcraft generate
|
|
170
|
+
aic = !commitcraft generate --auto-commit
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Then use:
|
|
174
|
+
```bash
|
|
175
|
+
git ai # Interactive mode
|
|
176
|
+
git aic # Auto-commit
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Set Default for Long-Running Work
|
|
180
|
+
```bash
|
|
181
|
+
# Working on CF-123 all week
|
|
182
|
+
commitcraft config --jira-prefix CF-123
|
|
183
|
+
|
|
184
|
+
# All commits include CF-123
|
|
185
|
+
git add file1.rb && commitcraft generate -y
|
|
186
|
+
git add file2.rb && commitcraft generate -y
|
|
187
|
+
git add file3.rb && commitcraft generate -y
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Git History with Jira
|
|
191
|
+
```bash
|
|
192
|
+
$ git log --oneline
|
|
193
|
+
|
|
194
|
+
a1b2c3d [CF-123] feat(auth): add OAuth2 authentication
|
|
195
|
+
d4e5f6g [CF-123] test(auth): add integration tests
|
|
196
|
+
h7i8j9k [CF-456] fix(api): resolve validation error
|
|
197
|
+
l0m1n2o [CF-456] docs(api): update API documentation
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Filter by ticket:**
|
|
201
|
+
```bash
|
|
202
|
+
git log --grep="CF-123" --oneline
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Rate Limits (Free Tier)
|
|
206
|
+
|
|
207
|
+
- 15 requests/minute
|
|
208
|
+
- 1 million tokens/minute
|
|
209
|
+
- 1,500 requests/day
|
|
210
|
+
|
|
211
|
+
## Contributing
|
|
212
|
+
|
|
213
|
+
Bug reports and pull requests are welcome.
|
|
214
|
+
|
|
215
|
+
1. Fork the repository
|
|
216
|
+
2. Create your feature branch
|
|
217
|
+
3. Commit your changes
|
|
218
|
+
4. Push to the branch
|
|
219
|
+
5. Create a Pull Request
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT License - see LICENSE.txt
|
|
224
|
+
|
|
225
|
+
## Tips
|
|
226
|
+
|
|
227
|
+
1. **Stage related changes together** for better commit messages
|
|
228
|
+
2. **Use --include-history** for context-aware suggestions
|
|
229
|
+
3. **Try different styles** to match your team's conventions
|
|
230
|
+
4. **Set default Jira prefix** for long-running feature work
|
|
231
|
+
5. **Use --auto-commit** for quick, simple changes
|
|
232
|
+
6. **Create git aliases** for faster workflow
|
data/exe/commitcraft
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "gemini-ai"
|
|
4
|
+
|
|
5
|
+
module CommitCraft
|
|
6
|
+
class AIClient
|
|
7
|
+
def initialize(config = CommitCraft.configuration)
|
|
8
|
+
@config = config
|
|
9
|
+
@config.validate!
|
|
10
|
+
@client = Gemini.new(
|
|
11
|
+
credentials: {
|
|
12
|
+
service: "generative-language-api",
|
|
13
|
+
api_key: @config.api_key
|
|
14
|
+
},
|
|
15
|
+
options: {
|
|
16
|
+
model: @config.model,
|
|
17
|
+
server_sent_events: true
|
|
18
|
+
}
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def generate_commit_messages(diff, context = {})
|
|
23
|
+
prompt = build_prompt(diff, context)
|
|
24
|
+
|
|
25
|
+
result = @client.stream_generate_content({
|
|
26
|
+
contents: { role: "user", parts: { text: prompt } }
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
# Collect the full response
|
|
30
|
+
full_response = ""
|
|
31
|
+
result.each do |event|
|
|
32
|
+
next unless event.dig("candidates", 0, "content", "parts")
|
|
33
|
+
|
|
34
|
+
event.dig("candidates", 0, "content", "parts").each do |part|
|
|
35
|
+
full_response += part["text"] if part["text"]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
parse_response(full_response)
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
raise AIError, "Failed to generate commit message: #{e.message}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def build_prompt(diff, context)
|
|
47
|
+
style_instruction = style_instructions[@config.commit_style]
|
|
48
|
+
|
|
49
|
+
prompt = <<~PROMPT
|
|
50
|
+
You are an expert at writing clear, concise git commit messages.
|
|
51
|
+
|
|
52
|
+
#{style_instruction}
|
|
53
|
+
|
|
54
|
+
Please generate 3 different commit message options for the following changes.
|
|
55
|
+
Each message should be on its own line, numbered 1-3.
|
|
56
|
+
|
|
57
|
+
#{context_section(context)}
|
|
58
|
+
|
|
59
|
+
Git diff:
|
|
60
|
+
```
|
|
61
|
+
#{diff}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Generate exactly 3 commit messages, one per line, numbered 1-3.
|
|
65
|
+
PROMPT
|
|
66
|
+
|
|
67
|
+
prompt.strip
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def context_section(context)
|
|
71
|
+
return "" if context.empty?
|
|
72
|
+
|
|
73
|
+
sections = []
|
|
74
|
+
sections << "Current branch: #{context[:branch]}" if context[:branch]
|
|
75
|
+
sections << "Files changed: #{context[:files].join(", ")}" if context[:files]&.any?
|
|
76
|
+
sections << "Recent commits:\n#{context[:recent_commits].join("\n")}" if context[:recent_commits]&.any?
|
|
77
|
+
|
|
78
|
+
return "" if sections.empty?
|
|
79
|
+
|
|
80
|
+
"Context:\n#{sections.join("\n")}\n"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def style_instructions
|
|
84
|
+
{
|
|
85
|
+
"conventional" => <<~STYLE,
|
|
86
|
+
Follow the Conventional Commits specification:
|
|
87
|
+
- Format: <type>(<scope>): <description>
|
|
88
|
+
- Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build
|
|
89
|
+
- Keep the description concise and in present tense
|
|
90
|
+
- Example: "feat(auth): add OAuth2 login flow"
|
|
91
|
+
STYLE
|
|
92
|
+
"semantic" => <<~STYLE,
|
|
93
|
+
Follow semantic commit format:
|
|
94
|
+
- Start with a verb in present tense (Add, Update, Fix, Remove, etc.)
|
|
95
|
+
- Be specific about what changed
|
|
96
|
+
- Example: "Add user authentication middleware"
|
|
97
|
+
STYLE
|
|
98
|
+
"descriptive" => <<~STYLE,
|
|
99
|
+
Write descriptive commit messages:
|
|
100
|
+
- Focus on the "what" and "why"
|
|
101
|
+
- Use complete sentences
|
|
102
|
+
- Example: "Implement caching to improve API response time by 40%"
|
|
103
|
+
STYLE
|
|
104
|
+
"custom" => <<~STYLE
|
|
105
|
+
Write clear, professional commit messages that accurately describe the changes.
|
|
106
|
+
STYLE
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def parse_response(response)
|
|
111
|
+
return [] if response.nil? || response.empty?
|
|
112
|
+
|
|
113
|
+
messages = response.split("\n")
|
|
114
|
+
.map(&:strip)
|
|
115
|
+
.grep(/^\d+[.)]\s+/)
|
|
116
|
+
.map { |line| line.sub(/^\d+[.)]\s+/, "") }
|
|
117
|
+
.reject(&:empty?)
|
|
118
|
+
|
|
119
|
+
raise AIError, "Failed to parse commit messages from AI response" if messages.empty?
|
|
120
|
+
|
|
121
|
+
messages
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require "tty-prompt"
|
|
5
|
+
require "tty-spinner"
|
|
6
|
+
require "pastel"
|
|
7
|
+
|
|
8
|
+
module CommitCraft
|
|
9
|
+
class CLI < Thor
|
|
10
|
+
def self.exit_on_failure?
|
|
11
|
+
true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
desc "generate", "Generate AI-powered commit messages"
|
|
15
|
+
method_option :all, type: :boolean, aliases: "-a", desc: "Include all changes (staged and unstaged)"
|
|
16
|
+
method_option :unstaged, type: :boolean, aliases: "-u", desc: "Use unstaged changes only"
|
|
17
|
+
method_option :amend, type: :boolean, desc: "Amend the previous commit"
|
|
18
|
+
method_option :no_context, type: :boolean, desc: "Don't include git context (branch, files)"
|
|
19
|
+
method_option :include_history, type: :boolean, aliases: "-h", desc: "Include recent commit history as context"
|
|
20
|
+
method_option :auto_commit, type: :boolean, aliases: "-y", desc: "Automatically commit with the first suggestion"
|
|
21
|
+
method_option :style, type: :string, aliases: "-s",
|
|
22
|
+
desc: "Commit message style (conventional, semantic, descriptive, custom)"
|
|
23
|
+
method_option :jira, type: :string, aliases: '-j', desc: 'Jira ticket key (e.g., CF-123)'
|
|
24
|
+
def generate
|
|
25
|
+
setup_style(options[:style]) if options[:style]
|
|
26
|
+
|
|
27
|
+
generator = MessageGenerator.new
|
|
28
|
+
prompt = TTY::Prompt.new
|
|
29
|
+
pastel = Pastel.new
|
|
30
|
+
|
|
31
|
+
if options[:jira]
|
|
32
|
+
validate_jira_key!(options[:jira])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Show test mode indicator
|
|
36
|
+
if CommitCraft.test_mode?
|
|
37
|
+
puts pastel.yellow("⚠️ TEST MODE - Using mock AI (no API calls)")
|
|
38
|
+
puts ""
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Show spinner while generating
|
|
42
|
+
spinner = TTY::Spinner.new("[:spinner] Analyzing changes and generating commit messages...", format: :dots)
|
|
43
|
+
spinner.auto_spin
|
|
44
|
+
|
|
45
|
+
begin
|
|
46
|
+
result = generator.generate(
|
|
47
|
+
all: options[:all],
|
|
48
|
+
unstaged: options[:unstaged],
|
|
49
|
+
no_context: options[:no_context],
|
|
50
|
+
include_history: options[:include_history],
|
|
51
|
+
jira: options[:jira]
|
|
52
|
+
)
|
|
53
|
+
rescue CommitCraft::Error => e
|
|
54
|
+
spinner.error("(#{pastel.red("failed")})")
|
|
55
|
+
puts pastel.red("Error: #{e.message}")
|
|
56
|
+
exit 1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
spinner.success("(#{pastel.green("done")})")
|
|
60
|
+
puts
|
|
61
|
+
|
|
62
|
+
# Show diff summary
|
|
63
|
+
summary = result[:diff_summary]
|
|
64
|
+
puts pastel.dim("Files changed: #{summary[:files_changed]}, ") +
|
|
65
|
+
pastel.green("+#{summary[:additions]}") + pastel.dim(" / ") +
|
|
66
|
+
pastel.red("-#{summary[:deletions]}")
|
|
67
|
+
puts
|
|
68
|
+
|
|
69
|
+
# Show generated messages
|
|
70
|
+
messages = result[:messages]
|
|
71
|
+
|
|
72
|
+
if options[:auto_commit]
|
|
73
|
+
selected_message = messages.first
|
|
74
|
+
puts pastel.cyan("Auto-committing with: ") + pastel.white(selected_message)
|
|
75
|
+
else
|
|
76
|
+
choices = messages.map.with_index do |msg, _idx|
|
|
77
|
+
{ name: msg, value: msg }
|
|
78
|
+
end
|
|
79
|
+
choices << { name: pastel.dim("✎ Write custom message"), value: :custom }
|
|
80
|
+
choices << { name: pastel.dim("✗ Cancel"), value: :cancel }
|
|
81
|
+
|
|
82
|
+
selected_message = prompt.select("Choose a commit message:", choices, per_page: 10)
|
|
83
|
+
|
|
84
|
+
case selected_message
|
|
85
|
+
when :custom
|
|
86
|
+
selected_message = prompt.ask("Enter your commit message:") do |q|
|
|
87
|
+
q.required true
|
|
88
|
+
q.validate(/\S+/)
|
|
89
|
+
end
|
|
90
|
+
when :cancel
|
|
91
|
+
puts pastel.yellow("Commit cancelled.")
|
|
92
|
+
exit 0
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
puts
|
|
97
|
+
|
|
98
|
+
# Confirm and commit
|
|
99
|
+
begin
|
|
100
|
+
generator.commit_with_message(selected_message, amend: options[:amend])
|
|
101
|
+
action = options[:amend] ? "amended" : "created"
|
|
102
|
+
puts pastel.green("✓ Commit #{action} successfully!")
|
|
103
|
+
puts pastel.dim(" Message: #{selected_message}")
|
|
104
|
+
rescue CommitCraft::GitError => e
|
|
105
|
+
puts pastel.red("Failed to commit: #{e.message}")
|
|
106
|
+
exit 1
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
desc "config", "Configure CommitCraft"
|
|
111
|
+
method_option :api_key, type: :string, desc: "Set Anthropic API key"
|
|
112
|
+
method_option :model, type: :string, desc: "Set AI model"
|
|
113
|
+
method_option :style, type: :string, desc: "Set default commit message style"
|
|
114
|
+
method_option :show, type: :boolean, desc: "Show current configuration"
|
|
115
|
+
method_option :jira_prefix, type: :string, desc: 'Set default Jira prefix'
|
|
116
|
+
|
|
117
|
+
def config
|
|
118
|
+
config_file = File.join(Dir.home, ".commitcraft.yml")
|
|
119
|
+
|
|
120
|
+
if options[:show]
|
|
121
|
+
show_config(config_file)
|
|
122
|
+
elsif options[:jira_prefix]
|
|
123
|
+
validate_jira_key!(options[:jira_prefix])
|
|
124
|
+
set_config_value('jira_prefix', options[:jira_prefix])
|
|
125
|
+
say pastel.green("✓ Default Jira prefix set to: #{options[:jira_prefix]}")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
require "yaml"
|
|
129
|
+
|
|
130
|
+
current_config = File.exist?(config_file) ? YAML.load_file(config_file) : {}
|
|
131
|
+
|
|
132
|
+
current_config["api_key"] = options[:api_key] if options[:api_key]
|
|
133
|
+
current_config["model"] = options[:model] if options[:model]
|
|
134
|
+
current_config["style"] = options[:style] if options[:style]
|
|
135
|
+
|
|
136
|
+
File.write(config_file, current_config.to_yaml)
|
|
137
|
+
|
|
138
|
+
pastel = Pastel.new
|
|
139
|
+
puts pastel.green("✓ Configuration saved to #{config_file}")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
desc "status", "Show git status and staged changes"
|
|
143
|
+
def status
|
|
144
|
+
git_client = GitClient.new
|
|
145
|
+
pastel = Pastel.new
|
|
146
|
+
|
|
147
|
+
begin
|
|
148
|
+
puts pastel.bold("Git Status:")
|
|
149
|
+
puts git_client.status
|
|
150
|
+
puts
|
|
151
|
+
|
|
152
|
+
staged_files = git_client.staged_files
|
|
153
|
+
if staged_files.any?
|
|
154
|
+
puts pastel.bold("Staged Files:")
|
|
155
|
+
staged_files.each { |file| puts pastel.green(" + #{file}") }
|
|
156
|
+
puts
|
|
157
|
+
|
|
158
|
+
stats = git_client.file_stats
|
|
159
|
+
total_add = stats.sum { |s| s[:additions] }
|
|
160
|
+
total_del = stats.sum { |s| s[:deletions] }
|
|
161
|
+
puts pastel.dim("Total changes: ") +
|
|
162
|
+
pastel.green("+#{total_add}") + pastel.dim(" / ") +
|
|
163
|
+
pastel.red("-#{total_del}")
|
|
164
|
+
else
|
|
165
|
+
puts pastel.yellow("No staged changes.")
|
|
166
|
+
end
|
|
167
|
+
rescue CommitCraft::GitError => e
|
|
168
|
+
puts pastel.red("Error: #{e.message}")
|
|
169
|
+
exit 1
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
desc "version", "Show CommitCraft version"
|
|
174
|
+
def version
|
|
175
|
+
puts "CommitCraft v#{CommitCraft::VERSION}"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
def validate_jira_key!(key)
|
|
181
|
+
pastel = Pastel.new
|
|
182
|
+
unless key.match?(/^[A-Z]+-\d+$/)
|
|
183
|
+
say pastel.red("✗ Invalid Jira key format: #{key}")
|
|
184
|
+
say pastel.dim("Expected format: PROJECT-123 (e.g., CF-123, JIRA-456)")
|
|
185
|
+
exit 1
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def setup_style(style)
|
|
190
|
+
CommitCraft.configure do |config|
|
|
191
|
+
config.commit_style = style
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def show_config(config_file)
|
|
196
|
+
pastel = Pastel.new
|
|
197
|
+
|
|
198
|
+
if File.exist?(config_file)
|
|
199
|
+
require "yaml"
|
|
200
|
+
config = YAML.load_file(config_file)
|
|
201
|
+
|
|
202
|
+
puts pastel.bold("Current Configuration:")
|
|
203
|
+
puts pastel.dim("Location: #{config_file}")
|
|
204
|
+
puts
|
|
205
|
+
config.each do |key, value|
|
|
206
|
+
display_value = key == "api_key" ? "#{value[0..8]}..." : value
|
|
207
|
+
puts " #{pastel.cyan(key)}: #{display_value}"
|
|
208
|
+
end
|
|
209
|
+
else
|
|
210
|
+
puts pastel.yellow("No configuration file found.")
|
|
211
|
+
puts pastel.dim("Using environment variable: GEMINI_API_KEY")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
puts
|
|
215
|
+
puts pastel.bold("Environment:")
|
|
216
|
+
api_key = ENV.fetch("GEMINI_API_KEY", nil)
|
|
217
|
+
puts " GEMINI_API_KEY: #{api_key ? pastel.green("Set") : pastel.red("Not set")}"
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommitCraft
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :api_key, :model, :max_tokens, :temperature, :commit_style, :test_mode, :jira_prefix
|
|
6
|
+
|
|
7
|
+
SUPPORTED_MODELS = %w[
|
|
8
|
+
gemini-2.5-flash
|
|
9
|
+
gemini-2.5-pro
|
|
10
|
+
gemini-2.0-flash
|
|
11
|
+
gemini-flash-latest
|
|
12
|
+
gemini-pro-latest
|
|
13
|
+
gemini-2.5-flash-lite
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
COMMIT_STYLES = %w[
|
|
17
|
+
conventional
|
|
18
|
+
semantic
|
|
19
|
+
descriptive
|
|
20
|
+
custom
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
load_config_file
|
|
25
|
+
@api_key ||= ENV.fetch("GEMINI_API_KEY", nil)
|
|
26
|
+
@model ||= "gemini-2.5-flash"
|
|
27
|
+
@max_tokens ||= 1000
|
|
28
|
+
@temperature ||= 0.7
|
|
29
|
+
@commit_style ||= "conventional"
|
|
30
|
+
@jira_prefix ||= nil
|
|
31
|
+
@test_mode = ENV["COMMITCRAFT_TEST_MODE"] == "true"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def validate!
|
|
35
|
+
return if @test_mode
|
|
36
|
+
raise ConfigurationError, "API key is required" if api_key.nil? || api_key.empty?
|
|
37
|
+
raise ConfigurationError, "Invalid model: #{model}" unless SUPPORTED_MODELS.include?(model)
|
|
38
|
+
raise ConfigurationError, "Invalid commit style: #{commit_style}" unless COMMIT_STYLES.include?(commit_style)
|
|
39
|
+
validate_jira_prefix! if jira_prefix
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def load_config_file
|
|
45
|
+
config_file = File.join(Dir.home, ".commitcraft.yml")
|
|
46
|
+
return unless File.exist?(config_file)
|
|
47
|
+
|
|
48
|
+
require "yaml"
|
|
49
|
+
config = YAML.load_file(config_file)
|
|
50
|
+
@api_key = config["api_key"]
|
|
51
|
+
@model = config["model"]
|
|
52
|
+
@max_tokens = config["max_tokens"]
|
|
53
|
+
@temperature = config["temperature"]
|
|
54
|
+
@commit_style = config["style"]
|
|
55
|
+
@jira_prefix = config["jira_prefix"]
|
|
56
|
+
rescue StandardError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def validate_jira_prefix!
|
|
61
|
+
unless jira_prefix.match?(/^[A-Z]+-\d+$/)
|
|
62
|
+
raise ConfigurationError, "Invalid Jira key format: #{jira_prefix}. Expected format: PROJECT-123"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
module CommitCraft
|
|
5
|
+
class GitClient
|
|
6
|
+
def initialize(repo_path: Dir.pwd)
|
|
7
|
+
@repo_path = repo_path
|
|
8
|
+
validate_git_repo!
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def staged_diff
|
|
12
|
+
diff = execute_command("git diff --cached")
|
|
13
|
+
raise NoChangesError, "No staged changes found. Use 'git add' to stage your changes." if diff.empty?
|
|
14
|
+
|
|
15
|
+
diff
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def unstaged_diff
|
|
19
|
+
execute_command("git diff")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def all_diff
|
|
23
|
+
execute_command("git diff HEAD")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def status
|
|
27
|
+
execute_command("git status --short")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def current_branch
|
|
31
|
+
execute_command("git branch --show-current").strip
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def recent_commits(count = 5)
|
|
35
|
+
execute_command("git log -#{count} --oneline").split("\n")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def commit(message, amend: false)
|
|
39
|
+
escaped_message = message.gsub("'", "'\\''")
|
|
40
|
+
cmd = amend ? "git commit --amend -m '#{escaped_message}'" : "git commit -m '#{escaped_message}'"
|
|
41
|
+
execute_command(cmd)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def staged_files
|
|
45
|
+
execute_command("git diff --cached --name-only").split("\n")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def file_stats
|
|
49
|
+
stats = execute_command("git diff --cached --numstat")
|
|
50
|
+
parse_file_stats(stats)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def validate_git_repo!
|
|
56
|
+
Dir.chdir(@repo_path) do
|
|
57
|
+
`git rev-parse --git-dir 2>&1`
|
|
58
|
+
raise GitError, "Not a git repository: #{@repo_path}" unless $CHILD_STATUS.success?
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def execute_command(command)
|
|
63
|
+
Dir.chdir(@repo_path) do
|
|
64
|
+
result = `#{command} 2>&1`
|
|
65
|
+
raise GitError, "Git command failed: #{result}" unless $CHILD_STATUS.success?
|
|
66
|
+
|
|
67
|
+
result
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def parse_file_stats(stats)
|
|
72
|
+
return [] if stats.empty?
|
|
73
|
+
|
|
74
|
+
stats.split("\n").map do |line|
|
|
75
|
+
added, deleted, file = line.split("\t")
|
|
76
|
+
{
|
|
77
|
+
file: file,
|
|
78
|
+
additions: added.to_i,
|
|
79
|
+
deletions: deleted.to_i
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommitCraft
|
|
4
|
+
class MessageGenerator
|
|
5
|
+
def initialize(git_client: nil, ai_client: nil)
|
|
6
|
+
@git_client = git_client || GitClient.new
|
|
7
|
+
@config = CommitCraft.configuration
|
|
8
|
+
|
|
9
|
+
@ai_client = if ai_client
|
|
10
|
+
ai_client
|
|
11
|
+
elsif CommitCraft.test_mode?
|
|
12
|
+
MockAIClient.new
|
|
13
|
+
else
|
|
14
|
+
AIClient.new
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def generate(options = {})
|
|
19
|
+
diff = get_diff(options)
|
|
20
|
+
context = build_context(options)
|
|
21
|
+
|
|
22
|
+
jira_key = options[:jira] || @config.jira_prefix
|
|
23
|
+
|
|
24
|
+
messages = @ai_client.generate_commit_messages(diff, context)
|
|
25
|
+
|
|
26
|
+
if jira_key
|
|
27
|
+
messages = messages.map { |msg| format_with_jira(msg, jira_key) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
messages: messages,
|
|
32
|
+
context: context,
|
|
33
|
+
diff_summary: summarize_diff
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def commit_with_message(message, amend: false)
|
|
38
|
+
@git_client.commit(message, amend: amend)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def format_with_jira(message, jira_key)
|
|
44
|
+
return message if message.match?(/^\[[A-Z]+-\d+\]/)
|
|
45
|
+
|
|
46
|
+
"[#{jira_key}] #{message}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def get_diff(options)
|
|
50
|
+
if options[:all]
|
|
51
|
+
@git_client.all_diff
|
|
52
|
+
elsif options[:unstaged]
|
|
53
|
+
@git_client.unstaged_diff
|
|
54
|
+
else
|
|
55
|
+
@git_client.staged_diff
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def build_context(options)
|
|
60
|
+
context = {}
|
|
61
|
+
|
|
62
|
+
unless options[:no_context]
|
|
63
|
+
context[:branch] = @git_client.current_branch
|
|
64
|
+
context[:files] = @git_client.staged_files
|
|
65
|
+
context[:recent_commits] = @git_client.recent_commits(3) if options[:include_history]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
context
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def summarize_diff
|
|
72
|
+
stats = @git_client.file_stats
|
|
73
|
+
|
|
74
|
+
total_additions = stats.sum { |s| s[:additions] }
|
|
75
|
+
total_deletions = stats.sum { |s| s[:deletions] }
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
files_changed: stats.length,
|
|
79
|
+
additions: total_additions,
|
|
80
|
+
deletions: total_deletions,
|
|
81
|
+
file_stats: stats
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CommitCraft
|
|
4
|
+
class MockAIClient
|
|
5
|
+
def initialize(config = CommitCraft.configuration)
|
|
6
|
+
@config = config
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def generate_commit_messages(diff, context = {})
|
|
10
|
+
# Analyze the diff to generate relevant messages
|
|
11
|
+
files_changed = context[:files] || []
|
|
12
|
+
|
|
13
|
+
# Generate messages based on commit style
|
|
14
|
+
case @config.commit_style
|
|
15
|
+
when "conventional"
|
|
16
|
+
generate_conventional_messages(diff, files_changed)
|
|
17
|
+
when "semantic"
|
|
18
|
+
generate_semantic_messages(diff, files_changed)
|
|
19
|
+
when "descriptive"
|
|
20
|
+
generate_descriptive_messages(diff, files_changed)
|
|
21
|
+
else
|
|
22
|
+
generate_default_messages(diff, files_changed)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def generate_conventional_messages(diff, files)
|
|
29
|
+
type = detect_type(diff, files)
|
|
30
|
+
scope = detect_scope(files)
|
|
31
|
+
|
|
32
|
+
[
|
|
33
|
+
"#{type}(#{scope}): implement changes to #{files.first || "codebase"}",
|
|
34
|
+
"#{type}: update #{files.length} file#{"s" if files.length != 1}",
|
|
35
|
+
"#{type}(#{scope}): add new functionality"
|
|
36
|
+
]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def generate_semantic_messages(diff, files)
|
|
40
|
+
[
|
|
41
|
+
"Add #{extract_feature(diff, files)}",
|
|
42
|
+
"Update #{files.first || "code"} with improvements",
|
|
43
|
+
"Implement #{extract_feature(diff, files)}"
|
|
44
|
+
]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def generate_descriptive_messages(diff, files)
|
|
48
|
+
[
|
|
49
|
+
"Implement new features across #{files.length} file#{"s" if files.length != 1}",
|
|
50
|
+
"Refactor code to improve maintainability and readability",
|
|
51
|
+
"Add functionality for #{extract_feature(diff, files)}"
|
|
52
|
+
]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def generate_default_messages(_diff, files)
|
|
56
|
+
[
|
|
57
|
+
"Update #{files.first || "code"}",
|
|
58
|
+
"Implement changes",
|
|
59
|
+
"Add new functionality"
|
|
60
|
+
]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def detect_type(diff, files)
|
|
64
|
+
# Simple heuristic based on files
|
|
65
|
+
return "feat" if diff.include?("class") || diff.include?("def")
|
|
66
|
+
return "fix" if diff.include?("bug") || diff.include?("fix")
|
|
67
|
+
return "docs" if files.any? { |f| f.end_with?(".md", ".txt") }
|
|
68
|
+
return "test" if files.any? { |f| f.include?("spec") || f.include?("test") }
|
|
69
|
+
return "refactor" if diff.include?("refactor")
|
|
70
|
+
|
|
71
|
+
"feat"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def detect_scope(files)
|
|
75
|
+
return "core" if files.empty?
|
|
76
|
+
|
|
77
|
+
# Get first file's directory or name
|
|
78
|
+
first_file = files.first.to_s
|
|
79
|
+
if first_file.include?("/")
|
|
80
|
+
parts = first_file.split("/")
|
|
81
|
+
parts.length > 1 ? parts[0] : "core"
|
|
82
|
+
else
|
|
83
|
+
first_file.split(".").first
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def extract_feature(diff, files)
|
|
88
|
+
# Try to extract a feature name from class/method names
|
|
89
|
+
if diff =~ /class\s+(\w+)/
|
|
90
|
+
::Regexp.last_match(1).downcase
|
|
91
|
+
elsif diff =~ /def\s+(\w+)/
|
|
92
|
+
::Regexp.last_match(1).gsub("_", " ")
|
|
93
|
+
elsif files.any?
|
|
94
|
+
files.first.to_s.split("/").last.split(".").first
|
|
95
|
+
else
|
|
96
|
+
"new feature"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/commitcraft.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "commitcraft/version"
|
|
4
|
+
require_relative "commitcraft/configuration"
|
|
5
|
+
require_relative "commitcraft/git_client"
|
|
6
|
+
require_relative "commitcraft/ai_client"
|
|
7
|
+
require_relative "commitcraft/mock_ai_client"
|
|
8
|
+
require_relative "commitcraft/message_generator"
|
|
9
|
+
require_relative "commitcraft/cli"
|
|
10
|
+
require_relative "commitcraft/errors"
|
|
11
|
+
|
|
12
|
+
module CommitCraft
|
|
13
|
+
class << self
|
|
14
|
+
attr_writer :configuration
|
|
15
|
+
|
|
16
|
+
def configuration
|
|
17
|
+
@configuration ||= Configuration.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def configure
|
|
21
|
+
yield(configuration)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def reset_configuration!
|
|
25
|
+
@configuration = Configuration.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_mode?
|
|
29
|
+
ENV["COMMITCRAFT_TEST_MODE"] == "true" || configuration.test_mode
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: commitcraft
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Khaled Elabady
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-02-15 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: gemini-ai
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '4.2'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '4.2'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: pastel
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.8'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0.8'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: thor
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.3'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.3'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: tty-prompt
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0.23'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0.23'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: tty-spinner
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0.9'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0.9'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: pry
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0.14'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0.14'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rake
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '13.0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '13.0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: rspec
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '3.12'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '3.12'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: rubocop
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '1.50'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '1.50'
|
|
139
|
+
description: CommitCraft uses AI to analyze your code changes and generate meaningful,
|
|
140
|
+
conventional commit messages automatically.
|
|
141
|
+
email:
|
|
142
|
+
- khaledelabadyy@gmail.com
|
|
143
|
+
executables:
|
|
144
|
+
- commitcraft
|
|
145
|
+
extensions: []
|
|
146
|
+
extra_rdoc_files: []
|
|
147
|
+
files:
|
|
148
|
+
- LICENSE.txt
|
|
149
|
+
- README.md
|
|
150
|
+
- exe/commitcraft
|
|
151
|
+
- lib/commitcraft.rb
|
|
152
|
+
- lib/commitcraft/ai_client.rb
|
|
153
|
+
- lib/commitcraft/cli.rb
|
|
154
|
+
- lib/commitcraft/configuration.rb
|
|
155
|
+
- lib/commitcraft/errors.rb
|
|
156
|
+
- lib/commitcraft/git_client.rb
|
|
157
|
+
- lib/commitcraft/message_generator.rb
|
|
158
|
+
- lib/commitcraft/mock_ai_client.rb
|
|
159
|
+
- lib/commitcraft/version.rb
|
|
160
|
+
homepage: https://github.com/Khaledelabady11/commitcraft
|
|
161
|
+
licenses:
|
|
162
|
+
- MIT
|
|
163
|
+
metadata:
|
|
164
|
+
homepage_uri: https://github.com/Khaledelabady11/commitcraft
|
|
165
|
+
source_code_uri: https://github.com/Khaledelabady11/commitcraft
|
|
166
|
+
changelog_uri: https://github.com/Khaledelabady11/commitcraft/blob/main/CHANGELOG.md
|
|
167
|
+
rubygems_mfa_required: 'true'
|
|
168
|
+
post_install_message:
|
|
169
|
+
rdoc_options: []
|
|
170
|
+
require_paths:
|
|
171
|
+
- lib
|
|
172
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
173
|
+
requirements:
|
|
174
|
+
- - ">="
|
|
175
|
+
- !ruby/object:Gem::Version
|
|
176
|
+
version: 2.7.0
|
|
177
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
178
|
+
requirements:
|
|
179
|
+
- - ">="
|
|
180
|
+
- !ruby/object:Gem::Version
|
|
181
|
+
version: '0'
|
|
182
|
+
requirements: []
|
|
183
|
+
rubygems_version: 3.5.22
|
|
184
|
+
signing_key:
|
|
185
|
+
specification_version: 4
|
|
186
|
+
summary: AI-powered git commit message generator
|
|
187
|
+
test_files: []
|