bit-cli-ai 1.0.6 ā 1.0.7
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.
- package/package.json +1 -1
- package/src/commands/branch.js +104 -52
- package/src/commands/commit.js +72 -132
- package/src/utils/ai.js +44 -32
package/package.json
CHANGED
package/src/commands/branch.js
CHANGED
|
@@ -36,28 +36,50 @@ function createGhostBranch(name) {
|
|
|
36
36
|
const spinner = ora(`Creating ghost branch: ${name}`).start();
|
|
37
37
|
|
|
38
38
|
try {
|
|
39
|
-
|
|
39
|
+
// Get current state
|
|
40
|
+
const currentBranch = getCurrentBranch();
|
|
41
|
+
let currentCommit = '';
|
|
42
|
+
try {
|
|
43
|
+
currentCommit = execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
|
|
44
|
+
} catch {
|
|
45
|
+
// If no commits yet, use empty string
|
|
46
|
+
currentCommit = '';
|
|
47
|
+
}
|
|
40
48
|
|
|
41
|
-
//
|
|
42
|
-
execSync(
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
// Get staged and unstaged files
|
|
50
|
+
const staged = execSync('git diff --cached --name-only', { encoding: 'utf-8' })
|
|
51
|
+
.split('\n')
|
|
52
|
+
.filter(f => f.trim());
|
|
53
|
+
const unstaged = execSync('git diff --name-only', { encoding: 'utf-8' })
|
|
54
|
+
.split('\n')
|
|
55
|
+
.filter(f => f.trim());
|
|
56
|
+
|
|
57
|
+
// DO NOT create git branch - ghost branches are metadata only
|
|
58
|
+
// Record in metadata only
|
|
45
59
|
const metadata = loadMetadata();
|
|
46
60
|
metadata.ghostBranches = metadata.ghostBranches || [];
|
|
47
61
|
|
|
48
|
-
if (!metadata.ghostBranches.find(b => b.name ===
|
|
62
|
+
if (!metadata.ghostBranches.find(b => b.name === name)) {
|
|
49
63
|
metadata.ghostBranches.push({
|
|
50
|
-
name:
|
|
64
|
+
name: name, // NO ghost/ prefix
|
|
65
|
+
baseCommit: currentCommit,
|
|
51
66
|
createdFrom: currentBranch,
|
|
52
67
|
createdAt: new Date().toISOString(),
|
|
53
|
-
isGhost: true
|
|
68
|
+
isGhost: true,
|
|
69
|
+
status: {
|
|
70
|
+
commit: currentCommit,
|
|
71
|
+
staged: staged,
|
|
72
|
+
unstaged: unstaged
|
|
73
|
+
}
|
|
54
74
|
});
|
|
55
75
|
saveMetadata(metadata);
|
|
56
76
|
}
|
|
57
77
|
|
|
58
|
-
spinner.succeed(`Ghost branch created: ${chalk.magenta(
|
|
59
|
-
console.log(chalk.gray(`
|
|
60
|
-
console.log(chalk.gray(`
|
|
78
|
+
spinner.succeed(`Ghost branch created: ${chalk.magenta(name)} š»`);
|
|
79
|
+
console.log(chalk.gray(` Based on: ${currentCommit.substring(0, 8)}...`));
|
|
80
|
+
console.log(chalk.gray(` Created from: ${chalk.cyan(currentBranch)}`));
|
|
81
|
+
console.log(chalk.gray(` To checkout: ${chalk.yellow('bit checkout --ghost ' + name)}`));
|
|
82
|
+
console.log(chalk.gray(` Note: Ghost branches exist only in metadata, not as git branches`));
|
|
61
83
|
|
|
62
84
|
} catch (error) {
|
|
63
85
|
spinner.fail('Failed to create ghost branch');
|
|
@@ -68,24 +90,24 @@ function createGhostBranch(name) {
|
|
|
68
90
|
// List all branches (including ghost)
|
|
69
91
|
function listAllBranches(showOnlyGhost = false) {
|
|
70
92
|
try {
|
|
71
|
-
// Get all git branches
|
|
72
|
-
const branches = execSync('git branch -a', { encoding: 'utf-8' })
|
|
73
|
-
.split('\n')
|
|
74
|
-
.map(b => b.trim())
|
|
75
|
-
.filter(b => b);
|
|
76
|
-
|
|
77
93
|
const currentBranch = getCurrentBranch();
|
|
78
94
|
const metadata = loadMetadata();
|
|
79
95
|
const ghostBranches = metadata.ghostBranches || [];
|
|
80
96
|
|
|
81
97
|
console.log(chalk.cyan('\n--- Branches ---\n'));
|
|
82
98
|
|
|
83
|
-
// Regular branches
|
|
99
|
+
// Regular branches (from git)
|
|
84
100
|
if (!showOnlyGhost) {
|
|
101
|
+
const branches = execSync('git branch', { encoding: 'utf-8' })
|
|
102
|
+
.split('\n')
|
|
103
|
+
.map(b => b.trim())
|
|
104
|
+
.filter(b => b && !b.includes('HEAD'));
|
|
105
|
+
|
|
85
106
|
console.log(chalk.yellow('Regular branches:'));
|
|
86
|
-
branches
|
|
87
|
-
.
|
|
88
|
-
|
|
107
|
+
if (branches.length === 0) {
|
|
108
|
+
console.log(chalk.gray(' No branches yet'));
|
|
109
|
+
} else {
|
|
110
|
+
branches.forEach(branch => {
|
|
89
111
|
const isCurrent = branch.startsWith('*');
|
|
90
112
|
const name = branch.replace('* ', '');
|
|
91
113
|
if (isCurrent) {
|
|
@@ -94,30 +116,23 @@ function listAllBranches(showOnlyGhost = false) {
|
|
|
94
116
|
console.log(chalk.gray(` ${name}`));
|
|
95
117
|
}
|
|
96
118
|
});
|
|
119
|
+
}
|
|
97
120
|
console.log('');
|
|
98
121
|
}
|
|
99
122
|
|
|
100
|
-
// Ghost branches
|
|
101
|
-
console.log(chalk.magenta('Ghost branches:'));
|
|
102
|
-
const gitGhostBranches = branches.filter(b => b.includes(GHOST_PREFIX));
|
|
123
|
+
// Ghost branches (from metadata only)
|
|
124
|
+
console.log(chalk.magenta('Ghost branches: š»'));
|
|
103
125
|
|
|
104
|
-
if (
|
|
126
|
+
if (ghostBranches.length === 0) {
|
|
105
127
|
console.log(chalk.gray(' No ghost branches'));
|
|
128
|
+
console.log(chalk.gray(` Create one with: ${chalk.yellow('bit branch --ghost <name>')}`));
|
|
106
129
|
} else {
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const ghostMeta = ghostBranches.find(g => g.name.includes(name));
|
|
111
|
-
|
|
112
|
-
if (isCurrent) {
|
|
113
|
-
console.log(chalk.green(` * ${name}`));
|
|
114
|
-
} else {
|
|
115
|
-
console.log(chalk.magenta(` ${name}`));
|
|
116
|
-
}
|
|
130
|
+
ghostBranches.forEach((g, i) => {
|
|
131
|
+
const age = Math.round((Date.now() - new Date(g.createdAt).getTime()) / 60000);
|
|
132
|
+
const ageText = age < 60 ? `${age}m ago` : `${Math.round(age / 60)}h ago`;
|
|
117
133
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
134
|
+
console.log(chalk.magenta(` ${g.name}`));
|
|
135
|
+
console.log(chalk.gray(` ${chalk.dim('Based on:')} ${g.baseCommit ? g.baseCommit.substring(0, 8) : 'initial'} ${chalk.dim('ā¢')} Created from: ${g.createdFrom} ${chalk.dim('ā¢')} ${ageText}`));
|
|
121
136
|
});
|
|
122
137
|
}
|
|
123
138
|
|
|
@@ -134,17 +149,26 @@ function deleteGhostBranch(name) {
|
|
|
134
149
|
const spinner = ora(`Deleting ghost branch: ${name}`).start();
|
|
135
150
|
|
|
136
151
|
try {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// Delete git branch
|
|
140
|
-
execSync(`git branch -D ${fullName}`, { stdio: 'pipe' });
|
|
152
|
+
// Find in metadata
|
|
153
|
+
const fullName = name.startsWith(GHOST_PREFIX) ? name.replace(GHOST_PREFIX, '') : name;
|
|
141
154
|
|
|
142
|
-
// Remove from metadata
|
|
155
|
+
// Remove from metadata only (no git branch to delete)
|
|
143
156
|
const metadata = loadMetadata();
|
|
157
|
+
const ghostData = metadata.ghostBranches?.find(g => g.name === fullName);
|
|
158
|
+
|
|
159
|
+
if (!ghostData) {
|
|
160
|
+
spinner.fail(`Ghost branch not found: ${fullName}`);
|
|
161
|
+
console.log(chalk.gray('\nAvailable ghost branches:'));
|
|
162
|
+
metadata.ghostBranches?.forEach(b => {
|
|
163
|
+
console.log(chalk.gray(` ${b.name}`));
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
144
168
|
metadata.ghostBranches = (metadata.ghostBranches || []).filter(b => b.name !== fullName);
|
|
145
169
|
saveMetadata(metadata);
|
|
146
170
|
|
|
147
|
-
spinner.succeed(`Ghost branch deleted: ${chalk.magenta(fullName)}
|
|
171
|
+
spinner.succeed(`Ghost branch deleted: ${chalk.magenta(fullName)} š»`);
|
|
148
172
|
|
|
149
173
|
} catch (error) {
|
|
150
174
|
spinner.fail('Failed to delete ghost branch');
|
|
@@ -174,17 +198,45 @@ export function checkoutGhost(branch, options) {
|
|
|
174
198
|
|
|
175
199
|
try {
|
|
176
200
|
let targetBranch = branch;
|
|
201
|
+
let isGhost = false;
|
|
202
|
+
let ghostData = null;
|
|
177
203
|
|
|
178
204
|
if (options.ghost) {
|
|
179
|
-
// It's a ghost branch
|
|
180
|
-
|
|
205
|
+
// It's a ghost branch - find in metadata
|
|
206
|
+
const metadata = loadMetadata();
|
|
207
|
+
const ghostName = branch.startsWith(GHOST_PREFIX) ? branch.replace(GHOST_PREFIX, '') : branch;
|
|
208
|
+
ghostData = metadata.ghostBranches?.find(g => g.name === ghostName);
|
|
209
|
+
|
|
210
|
+
if (!ghostData) {
|
|
211
|
+
spinner.fail(`Ghost branch not found: ${ghostName}`);
|
|
212
|
+
console.log(chalk.yellow('\nAvailable ghost branches:'));
|
|
213
|
+
metadata.ghostBranches?.forEach(b => {
|
|
214
|
+
console.log(chalk.gray(` ${b.name}`));
|
|
215
|
+
});
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
targetBranch = ghostData.baseCommit || 'HEAD';
|
|
220
|
+
isGhost = true;
|
|
181
221
|
}
|
|
182
222
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
223
|
+
if (isGhost && ghostData) {
|
|
224
|
+
// Checkout in detached HEAD state
|
|
225
|
+
execSync(`git checkout ${targetBranch}`, { stdio: 'pipe' });
|
|
226
|
+
|
|
227
|
+
const age = Math.round((Date.now() - new Date(ghostData.createdAt).getTime()) / 60000);
|
|
228
|
+
const ageText = age < 60 ? `${age}m ago` : `${Math.round(age / 60)}h ago`;
|
|
229
|
+
|
|
230
|
+
spinner.succeed(`Switched to ghost workspace: ${chalk.cyan(ghostData.name)} š»`);
|
|
231
|
+
console.log(chalk.magenta(` š Ghost Branch (Detached HEAD)`));
|
|
232
|
+
console.log(chalk.gray(` Based on: ${targetBranch.substring(0, 8)}...`));
|
|
233
|
+
console.log(chalk.gray(` Created from: ${chalk.cyan(ghostData.createdFrom)}`));
|
|
234
|
+
console.log(chalk.gray(` Created: ${ageText}`));
|
|
235
|
+
console.log(chalk.yellow(` Note: Changes won't be tracked to any branch until you create a real branch or commit them`));
|
|
236
|
+
} else {
|
|
237
|
+
// Regular branch checkout
|
|
238
|
+
execSync(`git checkout ${targetBranch}`, { stdio: 'pipe' });
|
|
239
|
+
spinner.succeed(`Switched to: ${chalk.cyan(targetBranch)}`);
|
|
188
240
|
}
|
|
189
241
|
|
|
190
242
|
} catch (error) {
|
|
@@ -195,7 +247,7 @@ export function checkoutGhost(branch, options) {
|
|
|
195
247
|
console.log(chalk.yellow('\nAvailable ghost branches:'));
|
|
196
248
|
const metadata = loadMetadata();
|
|
197
249
|
(metadata.ghostBranches || []).forEach(b => {
|
|
198
|
-
console.log(chalk.gray(` ${b.name
|
|
250
|
+
console.log(chalk.gray(` ${b.name}`));
|
|
199
251
|
});
|
|
200
252
|
}
|
|
201
253
|
}
|
package/src/commands/commit.js
CHANGED
|
@@ -4,172 +4,109 @@ import path from 'path';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import ora from 'ora';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/* ------------------ FALLBACK (RULE-BASED) ------------------ */
|
|
8
8
|
function generateSimpleMessage(diff) {
|
|
9
9
|
const lines = diff.split('\n');
|
|
10
10
|
const stats = {
|
|
11
11
|
additions: 0,
|
|
12
12
|
deletions: 0,
|
|
13
13
|
files: new Set(),
|
|
14
|
-
types: new Set()
|
|
15
14
|
};
|
|
16
15
|
|
|
17
16
|
for (const line of lines) {
|
|
18
|
-
if (line.startsWith('+') && !line.startsWith('+++'))
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
stats.deletions++;
|
|
22
|
-
} else if (line.startsWith('diff --git')) {
|
|
17
|
+
if (line.startsWith('+') && !line.startsWith('+++')) stats.additions++;
|
|
18
|
+
else if (line.startsWith('-') && !line.startsWith('---')) stats.deletions++;
|
|
19
|
+
else if (line.startsWith('diff --git')) {
|
|
23
20
|
const match = line.match(/b\/(.+)$/);
|
|
24
|
-
if (match)
|
|
25
|
-
stats.files.add(match[1]);
|
|
26
|
-
const ext = path.extname(match[1]);
|
|
27
|
-
if (ext) stats.types.add(ext);
|
|
28
|
-
}
|
|
21
|
+
if (match) stats.files.add(match[1]);
|
|
29
22
|
}
|
|
30
23
|
}
|
|
31
24
|
|
|
32
|
-
|
|
25
|
+
const files = Array.from(stats.files);
|
|
33
26
|
let type = 'chore';
|
|
34
|
-
const fileList = Array.from(stats.files);
|
|
35
|
-
|
|
36
|
-
if (fileList.some(f => f.includes('test') || f.includes('spec'))) {
|
|
37
|
-
type = 'test';
|
|
38
|
-
} else if (fileList.some(f => f.includes('README') || f.includes('docs'))) {
|
|
39
|
-
type = 'docs';
|
|
40
|
-
} else if (fileList.some(f => f.includes('fix') || diff.includes('fix'))) {
|
|
41
|
-
type = 'fix';
|
|
42
|
-
} else if (stats.additions > stats.deletions * 2) {
|
|
43
|
-
type = 'feat';
|
|
44
|
-
} else if (stats.deletions > stats.additions) {
|
|
45
|
-
type = 'refactor';
|
|
46
|
-
}
|
|
47
27
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
} else if (fileList.length > 1) {
|
|
53
|
-
const dirs = fileList.map(f => path.dirname(f)).filter(d => d !== '.');
|
|
54
|
-
if (dirs.length > 0) {
|
|
55
|
-
scope = dirs[0].split('/')[0];
|
|
56
|
-
}
|
|
57
|
-
}
|
|
28
|
+
if (files.some(f => f.includes('test'))) type = 'test';
|
|
29
|
+
else if (files.some(f => f.includes('README') || f.includes('docs'))) type = 'docs';
|
|
30
|
+
else if (stats.additions > stats.deletions * 2) type = 'feat';
|
|
31
|
+
else if (stats.deletions > stats.additions) type = 'refactor';
|
|
58
32
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
33
|
+
const scope =
|
|
34
|
+
files.length === 1
|
|
35
|
+
? path.basename(files[0], path.extname(files[0]))
|
|
36
|
+
: '';
|
|
37
|
+
|
|
38
|
+
const desc =
|
|
39
|
+
stats.additions && !stats.deletions
|
|
40
|
+
? `add ${files.length} file(s)`
|
|
41
|
+
: `update ${files.length} file(s) (+${stats.additions}/-${stats.deletions})`;
|
|
68
42
|
|
|
69
|
-
|
|
70
|
-
return scope ? `${type}(${scope}): ${description}` : `${type}: ${description}`;
|
|
43
|
+
return scope ? `${type}(${scope}): ${desc}` : `${type}: ${desc}`;
|
|
71
44
|
}
|
|
72
45
|
|
|
73
|
-
|
|
46
|
+
/* ------------------ OPENAI GENERATION ------------------ */
|
|
74
47
|
async function generateAIMessage(diff) {
|
|
75
48
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
76
|
-
|
|
77
|
-
if (!apiKey) {
|
|
78
|
-
// Fall back to simple generation
|
|
79
|
-
return generateSimpleMessage(diff);
|
|
80
|
-
}
|
|
49
|
+
if (!apiKey) return generateSimpleMessage(diff);
|
|
81
50
|
|
|
82
51
|
try {
|
|
83
52
|
const OpenAI = (await import('openai')).default;
|
|
84
53
|
const openai = new OpenAI({ apiKey });
|
|
85
54
|
|
|
86
|
-
const
|
|
55
|
+
const res = await openai.responses.create({
|
|
87
56
|
model: 'gpt-4o-mini',
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
role: 'system',
|
|
91
|
-
content: `You are a commit message generator. Generate a concise, meaningful commit message following Conventional Commits format.
|
|
92
|
-
|
|
93
|
-
Format: type(scope): description
|
|
94
|
-
|
|
95
|
-
Types: feat, fix, docs, style, refactor, test, chore
|
|
96
|
-
- Keep description under 50 characters
|
|
97
|
-
- Use imperative mood ("add" not "added")
|
|
98
|
-
- No period at the end
|
|
99
|
-
- Be specific about what changed
|
|
100
|
-
|
|
101
|
-
Respond with ONLY the commit message, nothing else.`
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
role: 'user',
|
|
105
|
-
content: `Generate a commit message for this diff:\n\n${diff.slice(0, 4000)}`
|
|
106
|
-
}
|
|
107
|
-
],
|
|
108
|
-
max_tokens: 100,
|
|
109
|
-
temperature: 0.3
|
|
57
|
+
input: `Generate a concise Conventional Commit message for this git diff. Respond ONLY with the commit message itself, no explanations.\n\n${diff.slice(0, 4000)}`,
|
|
58
|
+
temperature: 0.3,
|
|
110
59
|
});
|
|
111
60
|
|
|
112
|
-
return
|
|
61
|
+
return res.output_text.trim();
|
|
113
62
|
} catch (error) {
|
|
114
|
-
|
|
63
|
+
console.log(chalk.red(`\nā ļø AI Generation Failed: ${error.message}`));
|
|
64
|
+
if (error.status === 429) {
|
|
65
|
+
console.log(chalk.yellow('š” Your OpenAI quota is insufficient. Please check your billing at:'));
|
|
66
|
+
console.log(chalk.yellow(' https://platform.openai.com/account/billing'));
|
|
67
|
+
} else if (error.status === 401) {
|
|
68
|
+
console.log(chalk.yellow('š” Your API key is invalid. Please check your OPENAI_API_KEY environment variable.'));
|
|
69
|
+
}
|
|
70
|
+
console.log(chalk.gray('\nFalling back to rule-based generator...\n'));
|
|
115
71
|
return generateSimpleMessage(diff);
|
|
116
72
|
}
|
|
117
73
|
}
|
|
118
74
|
|
|
119
|
-
|
|
75
|
+
/* ------------------ SYMBOL TRACKING ------------------ */
|
|
120
76
|
function updateSymbolTracking(diff) {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
if (!fs.existsSync(symbolsPath)) return;
|
|
77
|
+
const file = '.bit/symbols.json';
|
|
78
|
+
if (!fs.existsSync(file)) return;
|
|
124
79
|
|
|
125
80
|
try {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
// Extract function names from diff (simple regex)
|
|
129
|
-
const functionMatches = diff.match(/(?:function|const|let|var)\s+(\w+)/g) || [];
|
|
130
|
-
const classMatches = diff.match(/class\s+(\w+)/g) || [];
|
|
131
|
-
|
|
132
|
-
for (const match of functionMatches) {
|
|
133
|
-
const name = match.split(/\s+/)[1];
|
|
134
|
-
if (name) {
|
|
135
|
-
symbols.functions[name] = symbols.functions[name] || { changes: 0, lastModified: null };
|
|
136
|
-
symbols.functions[name].changes++;
|
|
137
|
-
symbols.functions[name].lastModified = new Date().toISOString();
|
|
138
|
-
}
|
|
139
|
-
}
|
|
81
|
+
const db = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
82
|
+
const matches = diff.match(/(?:function|class|const)\s+(\w+)/g) || [];
|
|
140
83
|
|
|
141
|
-
|
|
142
|
-
const name =
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
symbols.classes[name].changes++;
|
|
146
|
-
symbols.classes[name].lastModified = new Date().toISOString();
|
|
147
|
-
}
|
|
148
|
-
}
|
|
84
|
+
matches.forEach(m => {
|
|
85
|
+
const name = m.split(/\s+/)[1];
|
|
86
|
+
db[name] = (db[name] || 0) + 1;
|
|
87
|
+
});
|
|
149
88
|
|
|
150
|
-
|
|
151
|
-
fs.writeFileSync(symbolsPath, JSON.stringify(symbols, null, 2));
|
|
89
|
+
fs.writeFileSync(file, JSON.stringify(db, null, 2));
|
|
152
90
|
} catch (error) {
|
|
153
|
-
//
|
|
91
|
+
// Silent fail - file corruption not critical
|
|
154
92
|
}
|
|
155
93
|
}
|
|
156
94
|
|
|
157
|
-
|
|
95
|
+
/* ------------------ MAIN COMMAND ------------------ */
|
|
96
|
+
export async function aiCommit(options = {}) {
|
|
158
97
|
const spinner = ora('Analyzing changes...').start();
|
|
159
98
|
|
|
160
99
|
try {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
100
|
+
const staged = execSync('git diff --cached --name-only', {
|
|
101
|
+
encoding: 'utf-8',
|
|
102
|
+
}).trim();
|
|
103
|
+
|
|
164
104
|
if (!staged) {
|
|
165
105
|
spinner.fail('No staged changes');
|
|
166
|
-
console.log(chalk.yellow('
|
|
167
|
-
console.log(chalk.gray(' git add <files>'));
|
|
168
|
-
console.log(chalk.gray(' git add .'));
|
|
106
|
+
console.log(chalk.yellow('Run: git add .'));
|
|
169
107
|
return;
|
|
170
108
|
}
|
|
171
109
|
|
|
172
|
-
// If manual message provided, use it
|
|
173
110
|
if (options.message) {
|
|
174
111
|
spinner.text = 'Committing...';
|
|
175
112
|
execSync(`git commit -m "${options.message}"`, { stdio: 'inherit' });
|
|
@@ -177,35 +114,38 @@ export async function aiCommit(options) {
|
|
|
177
114
|
return;
|
|
178
115
|
}
|
|
179
116
|
|
|
180
|
-
// Get the diff
|
|
181
117
|
const diff = execSync('git diff --cached', { encoding: 'utf-8' });
|
|
182
118
|
|
|
183
|
-
|
|
119
|
+
/* -------- AI STATUS (CORRECT PLACE) -------- */
|
|
120
|
+
spinner.stop();
|
|
121
|
+
if (process.env.OPENAI_API_KEY) {
|
|
122
|
+
console.log(chalk.green('š§ AI ENABLED: OpenAI will generate commit message\n'));
|
|
123
|
+
} else {
|
|
124
|
+
console.log(chalk.blue('ā¹ AI DISABLED: Using local rule-based generator'));
|
|
125
|
+
console.log(chalk.gray(' To enable AI commits:'));
|
|
126
|
+
console.log(chalk.gray(' setx OPENAI_API_KEY "your-api-key"'));
|
|
127
|
+
console.log(chalk.gray(' Restart terminal after setting the key.\n'));
|
|
128
|
+
}
|
|
129
|
+
spinner.start();
|
|
130
|
+
/* ------------------------------------------ */
|
|
131
|
+
|
|
184
132
|
spinner.text = 'Generating commit message...';
|
|
185
133
|
const message = await generateAIMessage(diff);
|
|
186
134
|
spinner.succeed(`Generated: ${chalk.cyan(message)}`);
|
|
187
135
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
console.log(chalk.yellow('Staged files:'));
|
|
191
|
-
staged.split('\n').forEach(file => {
|
|
192
|
-
console.log(chalk.gray(` ${file}`));
|
|
193
|
-
});
|
|
194
|
-
console.log(chalk.gray('ā'.repeat(50)));
|
|
136
|
+
console.log('\n' + chalk.gray('Staged files:'));
|
|
137
|
+
staged.split('\n').forEach(f => console.log(chalk.gray(` ${f}`)));
|
|
195
138
|
|
|
196
|
-
// Commit with generated message
|
|
197
139
|
spinner.start('Committing...');
|
|
198
140
|
execSync(`git commit -m "${message}"`, { stdio: 'pipe' });
|
|
199
|
-
|
|
200
|
-
// Update symbol tracking
|
|
141
|
+
|
|
201
142
|
updateSymbolTracking(diff);
|
|
202
|
-
|
|
203
|
-
spinner.succeed(chalk.green('Committed successfully!'));
|
|
204
|
-
console.log(chalk.gray(`\n ${message}\n`));
|
|
205
143
|
|
|
206
|
-
|
|
144
|
+
spinner.succeed(chalk.green('Committed successfully!'));
|
|
145
|
+
console.log(chalk.gray(`\n${message}\n`));
|
|
146
|
+
} catch (err) {
|
|
207
147
|
spinner.fail('Commit failed');
|
|
208
|
-
console.error(chalk.red(
|
|
148
|
+
console.error(chalk.red(err.message));
|
|
209
149
|
process.exit(1);
|
|
210
150
|
}
|
|
211
151
|
}
|
package/src/utils/ai.js
CHANGED
|
@@ -7,7 +7,7 @@ import OpenAI from 'openai';
|
|
|
7
7
|
import { config } from './config.js';
|
|
8
8
|
import { log } from './logger.js';
|
|
9
9
|
import { APIKeyError, RateLimitError, AIError } from './errors.js';
|
|
10
|
-
|
|
10
|
+
import chalk from 'chalk';
|
|
11
11
|
/**
|
|
12
12
|
* Get OpenAI client instance
|
|
13
13
|
*/
|
|
@@ -40,18 +40,20 @@ export async function generateCommitMessage(diff, options = {}) {
|
|
|
40
40
|
log.ai('generate_commit', { model: aiConfig.model, diffLength: diff.length });
|
|
41
41
|
|
|
42
42
|
const systemPrompt = getCommitSystemPrompt(style, maxLength);
|
|
43
|
+
console.log('š§ OPENAI REQUEST SENT');
|
|
43
44
|
|
|
44
45
|
const response = await client.chat.completions.create({
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
model: aiConfig.model,
|
|
47
|
+
messages: [
|
|
48
|
+
{ role: 'system', content: systemPrompt },
|
|
49
|
+
{ role: 'user', content: truncateDiff(diff, 4000) },
|
|
50
|
+
],
|
|
51
|
+
max_tokens: aiConfig.maxTokens,
|
|
52
|
+
temperature: aiConfig.temperature,
|
|
53
|
+
});
|
|
54
|
+
console.log('š§ OPENAI RESPONSE RECEIVED');
|
|
55
|
+
const message = response.choices[0]?.message?.content?.trim() || '';
|
|
56
|
+
|
|
55
57
|
|
|
56
58
|
log.ai('generate_commit_success', { message });
|
|
57
59
|
|
|
@@ -59,16 +61,20 @@ export async function generateCommitMessage(diff, options = {}) {
|
|
|
59
61
|
} catch (error) {
|
|
60
62
|
log.exception(error, { operation: 'generate_commit' });
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
throw new RateLimitError();
|
|
64
|
-
}
|
|
64
|
+
console.log(chalk.red(`\nā ļø AI Generation Failed: ${error.message}`));
|
|
65
65
|
|
|
66
|
-
if (error.status ===
|
|
67
|
-
|
|
66
|
+
if (error.status === 429) {
|
|
67
|
+
log.ai('fallback', { reason: 'insufficient_quota' });
|
|
68
|
+
console.log(chalk.yellow('š” Your OpenAI quota is insufficient. Please check your billing at:'));
|
|
69
|
+
console.log(chalk.yellow(' https://platform.openai.com/account/billing'));
|
|
70
|
+
} else if (error.status === 401) {
|
|
71
|
+
log.ai('fallback', { reason: 'invalid_api_key' });
|
|
72
|
+
console.log(chalk.yellow('š” Your API key is invalid. Please check your OPENAI_API_KEY environment variable.'));
|
|
73
|
+
} else {
|
|
74
|
+
log.ai('fallback', { reason: error.message });
|
|
75
|
+
console.log(chalk.gray(' Falling back to rule-based generator...\n'));
|
|
68
76
|
}
|
|
69
77
|
|
|
70
|
-
// Fallback to rule-based on any error
|
|
71
|
-
log.ai('fallback', { reason: error.message });
|
|
72
78
|
return generateRuleBasedMessage(diff);
|
|
73
79
|
}
|
|
74
80
|
}
|
|
@@ -84,26 +90,27 @@ export async function analyzeCodeChanges(diff, options = {}) {
|
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
const response = await client.chat.completions.create({
|
|
94
|
+
model: config.get('ai.model'),
|
|
95
|
+
messages: [
|
|
96
|
+
{
|
|
97
|
+
role: 'system',
|
|
98
|
+
content: `Analyze the following git diff and provide:
|
|
93
99
|
1. A brief summary of changes (1-2 sentences)
|
|
94
100
|
2. List of modified functions/methods
|
|
95
101
|
3. Potential risks or areas needing review
|
|
96
102
|
4. Suggested reviewers based on code areas
|
|
97
103
|
|
|
98
104
|
Format as JSON with keys: summary, modifiedFunctions, risks, suggestedAreas`,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
},
|
|
106
|
+
{ role: 'user', content: truncateDiff(diff, 4000) },
|
|
107
|
+
],
|
|
108
|
+
max_tokens: 1000,
|
|
109
|
+
temperature: 0.3,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const content = response.choices[0]?.message?.content || '';
|
|
113
|
+
|
|
107
114
|
|
|
108
115
|
try {
|
|
109
116
|
return JSON.parse(content);
|
|
@@ -112,6 +119,11 @@ Format as JSON with keys: summary, modifiedFunctions, risks, suggestedAreas`,
|
|
|
112
119
|
}
|
|
113
120
|
} catch (error) {
|
|
114
121
|
log.exception(error, { operation: 'analyze_code' });
|
|
122
|
+
if (error.status === 429) {
|
|
123
|
+
console.log(chalk.yellow('ā ļø Code analysis skipped: Insufficient OpenAI quota'));
|
|
124
|
+
} else if (error.status === 401) {
|
|
125
|
+
console.log(chalk.yellow('ā ļø Code analysis skipped: Invalid API key'));
|
|
126
|
+
}
|
|
115
127
|
return null;
|
|
116
128
|
}
|
|
117
129
|
}
|