-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathai.js
More file actions
260 lines (215 loc) Β· 8.49 KB
/
Copy pathai.js
File metadata and controls
260 lines (215 loc) Β· 8.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
import OpenAI from 'openai';
import chalk from 'chalk';
// Lazy initialization of OpenAI client
let openai = null;
function getOpenAIClient() {
if (!openai) {
openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
}
return openai;
}
/**
* Generate a commit message using OpenAI
* @param {string} diff - The git diff output
* @returns {Promise<string>} The generated commit message
*/
export async function generateCommitMessage(diff) {
try {
if (!diff || diff.trim() === '') {
throw new Error('No diff provided for commit message generation');
}
const model = process.env.OPENAI_MODEL || 'gpt-4o-mini';
const prompt = createCommitPrompt(diff);
const completion = await getOpenAIClient().chat.completions.create({
model: model,
messages: [
{
role: 'system',
content: 'You are a helpful assistant that writes clear and concise git commit messages following best practices.'
},
{
role: 'user',
content: prompt
}
],
max_tokens: 100,
temperature: 0.7,
});
const message = completion.choices[0]?.message?.content?.trim();
if (!message) {
throw new Error('No commit message generated by AI');
}
return cleanCommitMessage(message);
} catch (error) {
if (error.status === 401) {
throw new Error('Invalid OpenAI API key. Please check your .env file.');
} else if (error.status === 429) {
throw new Error('OpenAI API rate limit exceeded. Please try again later.');
} else if (error.status === 500) {
throw new Error('OpenAI API server error. Please try again later.');
}
throw new Error(`Failed to generate commit message: ${error.message}`);
}
}
/**
* Analyze diff to determine the type of changes and suggest appropriate emoji
* @param {string} diff - The git diff output
* @returns {string} The appropriate emoji for the changes
*/
export function analyzeDiffForEmoji(diff) {
const lowerDiff = diff.toLowerCase();
// Bug fixes (check first to avoid conflicts with "add")
if (lowerDiff.includes('fix') || lowerDiff.includes('bug') || lowerDiff.includes('error') ||
lowerDiff.includes('issue') || lowerDiff.includes('problem') || lowerDiff.includes('exception')) {
return 'π';
}
// Performance improvements (check before feature additions)
if (lowerDiff.includes('performance') || lowerDiff.includes('optimize') || lowerDiff.includes('speed') ||
lowerDiff.includes('fast') || lowerDiff.includes('efficient') || lowerDiff.includes('cache')) {
return 'β‘';
}
// Feature additions
if (lowerDiff.includes('add') || lowerDiff.includes('new') || lowerDiff.includes('create') ||
lowerDiff.includes('implement') || lowerDiff.includes('introduce')) {
return 'β¨';
}
// Performance improvements
if (lowerDiff.includes('performance') || lowerDiff.includes('optimize') || lowerDiff.includes('speed') ||
lowerDiff.includes('fast') || lowerDiff.includes('efficient') || lowerDiff.includes('cache')) {
return 'β‘';
}
// Documentation
if (lowerDiff.includes('readme') || lowerDiff.includes('docs') || lowerDiff.includes('documentation') ||
lowerDiff.includes('comment') || lowerDiff.includes('doc')) {
return 'π';
}
// Refactoring
if (lowerDiff.includes('refactor') || lowerDiff.includes('restructure') || lowerDiff.includes('clean') ||
lowerDiff.includes('improve') || lowerDiff.includes('update')) {
return 'β»οΈ';
}
// Testing
if (lowerDiff.includes('test') || lowerDiff.includes('spec') || lowerDiff.includes('unit') ||
lowerDiff.includes('integration') || lowerDiff.includes('coverage')) {
return 'π§ͺ';
}
// Configuration changes
if (lowerDiff.includes('config') || lowerDiff.includes('setting') || lowerDiff.includes('env') ||
lowerDiff.includes('package.json') || lowerDiff.includes('dependencies')) {
return 'βοΈ';
}
// Security
if (lowerDiff.includes('security') || lowerDiff.includes('vulnerability') || lowerDiff.includes('auth') ||
lowerDiff.includes('password') || lowerDiff.includes('token')) {
return 'π';
}
// UI/UX changes
if (lowerDiff.includes('ui') || lowerDiff.includes('ux') || lowerDiff.includes('style') ||
lowerDiff.includes('css') || lowerDiff.includes('design') || lowerDiff.includes('layout')) {
return 'π¨';
}
// Database changes
if (lowerDiff.includes('database') || lowerDiff.includes('db') || lowerDiff.includes('sql') ||
lowerDiff.includes('migration') || lowerDiff.includes('schema')) {
return 'ποΈ';
}
// API changes
if (lowerDiff.includes('api') || lowerDiff.includes('endpoint') || lowerDiff.includes('route') ||
lowerDiff.includes('controller') || lowerDiff.includes('service')) {
return 'π';
}
// Deployment/CI/CD
if (lowerDiff.includes('deploy') || lowerDiff.includes('ci') || lowerDiff.includes('cd') ||
lowerDiff.includes('pipeline') || lowerDiff.includes('docker') || lowerDiff.includes('build')) {
return 'π';
}
// Dependencies
if (lowerDiff.includes('package.json') || lowerDiff.includes('dependencies') || lowerDiff.includes('npm') ||
lowerDiff.includes('yarn') || lowerDiff.includes('install')) {
return 'π¦';
}
// Default for general changes
return 'π';
}
/**
* Create the prompt for the AI model
* @param {string} diff - The git diff output
* @returns {string} The formatted prompt
*/
function createCommitPrompt(diff) {
// Truncate very long diffs to avoid token limits
const maxDiffLength = 8000;
const truncatedDiff = diff.length > maxDiffLength
? diff.substring(0, maxDiffLength) + '\n\n... (diff truncated for brevity)'
: diff;
// Analyze diff to determine appropriate emoji
const emoji = analyzeDiffForEmoji(diff);
return `Generate a clear and concise git commit message for the following staged changes.
The commit message should:
- Start with the emoji: ${emoji}
- Be written in the imperative mood (e.g., "Add feature" not "Added feature")
- Be concise but descriptive (ideally under 72 characters)
- Focus on what the change does, not how it does it
- Not include unnecessary punctuation or quotes
- Follow conventional commit format if appropriate (feat:, fix:, docs:, etc.)
Here are the staged changes:
${truncatedDiff}
Respond with just the commit message starting with the emoji, nothing else.`;
}
/**
* Clean and validate the AI-generated commit message
* @param {string} message - The raw message from AI
* @returns {string} The cleaned message
*/
function cleanCommitMessage(message) {
// Remove quotes if present
let cleaned = message.replace(/^["']|["']$/g, '');
// Remove any extra whitespace
cleaned = cleaned.trim();
// Remove trailing periods unless it's part of an abbreviation
cleaned = cleaned.replace(/\.$/, '');
// Ensure the message starts with an emoji
const emojiRegex = /^[^\w\s]*[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}][^\w\s]*/u;
if (!emojiRegex.test(cleaned)) {
// If no emoji at the start, add a default one
cleaned = 'π ' + cleaned;
}
// Ensure first letter after emoji is capitalized if not using conventional commit format
const afterEmoji = cleaned.replace(/^[^\w\s]*[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}][^\w\s]*/u, '').trim();
if (!afterEmoji.match(/^(feat|fix|docs|style|refactor|perf|test|chore|build|ci):/i)) {
// Capitalize the first letter after emoji
const capitalizedAfterEmoji = afterEmoji.charAt(0).toUpperCase() + afterEmoji.slice(1);
cleaned = cleaned.replace(afterEmoji, capitalizedAfterEmoji);
}
return cleaned;
}
/**
* Test the OpenAI connection
* @returns {Promise<boolean>} True if connection is successful
*/
export async function testOpenAIConnection() {
try {
await getOpenAIClient().models.list();
return true;
} catch (error) {
console.log(chalk.red(`β OpenAI connection failed: ${error.message}`));
return false;
}
}
/**
* Get available OpenAI models
* @returns {Promise<Array>} List of available models
*/
export async function getAvailableModels() {
try {
const models = await getOpenAIClient().models.list();
return models.data
.filter(model => model.id.includes('gpt'))
.map(model => model.id)
.sort();
} catch (error) {
throw new Error(`Failed to fetch available models: ${error.message}`);
}
}