gims 0.5.4 → 0.6.2
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/CHANGELOG.md +58 -0
- package/QUICK_REFERENCE.md +74 -0
- package/README.md +44 -297
- package/bin/gims.js +520 -322
- package/bin/lib/ai/providers.js +286 -0
- package/bin/lib/commands/interactive.js +241 -0
- package/bin/lib/config/manager.js +213 -0
- package/bin/lib/git/analyzer.js +231 -0
- package/bin/lib/utils/colors.js +16 -0
- package/bin/lib/utils/progress.js +49 -0
- package/package.json +3 -2
- package/.github/workflows/release.yml +0 -25
- package/.npm-cache/_cacache/content-v2/sha512/50/76/b59cd1b7920f67e1f272759509b642dc898dfdd62cd143c2a6dda42c67217b79f4b5232f22a05a756d9b8fd266eadbae5f7df0bc886c63be4ace4bf547c1 +0 -0
- package/.npm-cache/_cacache/content-v2/sha512/cb/27/8dffe19cd1bef0222561d1d858d3968bfc7075a71ad21c55130ed2699a0778b0617302b9193447dad62c881e04104695de314ccec3f27617ba03d8ce01bd +0 -0
- package/.npm-cache/_cacache/content-v2/sha512/e4/ee/b849c92ec29d7d9df6b988c2e4272e363b85375a28c8a75d7ca3140e27eb4be1cdd615cd3e292c46d76fa982a36ca5fee968dd37f3e380fb50efbf6db22b +0 -0
- package/.npm-cache/_cacache/index-v5/54/dc/0588b4b9d94e884840efa9c9a3979d639d3849a2f409cb2845aaaafe0137 +0 -4
- package/.npm-cache/_update-notifier-last-checked +0 -0
package/bin/gims.js
CHANGED
|
@@ -1,64 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
gims (Git Made Simple) CLI
|
|
4
|
+
gims (Git Made Simple) CLI - Enhanced Version
|
|
5
5
|
*/
|
|
6
6
|
const { Command } = require('commander');
|
|
7
7
|
const simpleGit = require('simple-git');
|
|
8
8
|
const clipboard = require('clipboardy');
|
|
9
9
|
const process = require('process');
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
10
|
+
|
|
11
|
+
// Enhanced modular imports
|
|
12
|
+
const { color } = require('./lib/utils/colors');
|
|
13
|
+
const { Progress } = require('./lib/utils/progress');
|
|
14
|
+
const { ConfigManager } = require('./lib/config/manager');
|
|
15
|
+
const { GitAnalyzer } = require('./lib/git/analyzer');
|
|
16
|
+
const { AIProviderManager } = require('./lib/ai/providers');
|
|
17
|
+
const { InteractiveCommands } = require('./lib/commands/interactive');
|
|
14
18
|
|
|
15
19
|
const program = new Command();
|
|
16
20
|
const git = simpleGit();
|
|
17
21
|
|
|
18
|
-
//
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
24
|
-
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// Load simple config from .gimsrc (JSON) in cwd or home and env vars
|
|
28
|
-
function loadConfig() {
|
|
29
|
-
const defaults = {
|
|
30
|
-
provider: process.env.GIMS_PROVIDER || 'auto', // auto | openai | gemini | groq | none
|
|
31
|
-
model: process.env.GIMS_MODEL || '',
|
|
32
|
-
conventional: !!(process.env.GIMS_CONVENTIONAL === '1'),
|
|
33
|
-
copy: process.env.GIMS_COPY !== '0',
|
|
34
|
-
};
|
|
35
|
-
const tryFiles = [
|
|
36
|
-
path.join(process.cwd(), '.gimsrc'),
|
|
37
|
-
path.join(process.env.HOME || process.cwd(), '.gimsrc'),
|
|
38
|
-
];
|
|
39
|
-
for (const fp of tryFiles) {
|
|
40
|
-
try {
|
|
41
|
-
if (fs.existsSync(fp)) {
|
|
42
|
-
const txt = fs.readFileSync(fp, 'utf8');
|
|
43
|
-
const json = JSON.parse(txt);
|
|
44
|
-
return { ...defaults, ...json };
|
|
45
|
-
}
|
|
46
|
-
} catch (_) {
|
|
47
|
-
// ignore malformed config
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return defaults;
|
|
51
|
-
}
|
|
22
|
+
// Initialize enhanced components
|
|
23
|
+
const configManager = new ConfigManager();
|
|
24
|
+
const gitAnalyzer = new GitAnalyzer(git);
|
|
25
|
+
let aiProvider;
|
|
26
|
+
let interactive;
|
|
52
27
|
|
|
53
28
|
function getOpts() {
|
|
54
|
-
|
|
55
|
-
const cfg = loadConfig();
|
|
29
|
+
const cfg = configManager.load();
|
|
56
30
|
const cli = program.opts();
|
|
57
31
|
return {
|
|
58
32
|
provider: cli.provider || cfg.provider,
|
|
59
33
|
model: cli.model || cfg.model,
|
|
60
34
|
stagedOnly: !!cli.stagedOnly,
|
|
61
|
-
all: !!cli.all,
|
|
35
|
+
all: !!cli.all || cfg.autoStage,
|
|
62
36
|
noClipboard: !!cli.noClipboard || cfg.copy === false,
|
|
63
37
|
body: !!cli.body,
|
|
64
38
|
conventional: !!cli.conventional || cfg.conventional,
|
|
@@ -68,20 +42,38 @@ function getOpts() {
|
|
|
68
42
|
yes: !!cli.yes,
|
|
69
43
|
amend: !!cli.amend,
|
|
70
44
|
setUpstream: !!cli.setUpstream,
|
|
45
|
+
progressIndicators: cfg.progressIndicators !== false,
|
|
71
46
|
};
|
|
72
47
|
}
|
|
73
48
|
|
|
49
|
+
function initializeComponents() {
|
|
50
|
+
const config = configManager.load();
|
|
51
|
+
aiProvider = new AIProviderManager(config);
|
|
52
|
+
interactive = new InteractiveCommands(git, aiProvider, gitAnalyzer);
|
|
53
|
+
}
|
|
54
|
+
|
|
74
55
|
async function ensureRepo() {
|
|
75
56
|
const isRepo = await git.checkIsRepo();
|
|
76
57
|
if (!isRepo) {
|
|
77
|
-
|
|
58
|
+
Progress.error('Not a git repository (or any of the parent directories).');
|
|
59
|
+
console.log(`\nTo initialize a new repository, run: ${color.cyan('g init')}`);
|
|
78
60
|
process.exit(1);
|
|
79
61
|
}
|
|
80
62
|
}
|
|
81
63
|
|
|
82
64
|
function handleError(prefix, err) {
|
|
83
65
|
const msg = err && err.message ? err.message : String(err);
|
|
84
|
-
|
|
66
|
+
Progress.error(`${prefix}: ${msg}`);
|
|
67
|
+
|
|
68
|
+
// Provide helpful suggestions based on error type
|
|
69
|
+
if (msg.includes('not found') || msg.includes('does not exist')) {
|
|
70
|
+
console.log(`\nTip: Check if the file/branch exists with: ${color.cyan('g status')}`);
|
|
71
|
+
} else if (msg.includes('permission') || msg.includes('access')) {
|
|
72
|
+
console.log(`\nTip: Check file permissions or authentication`);
|
|
73
|
+
} else if (msg.includes('merge') || msg.includes('conflict')) {
|
|
74
|
+
console.log(`\nTip: Resolve conflicts and try again`);
|
|
75
|
+
}
|
|
76
|
+
|
|
85
77
|
process.exit(1);
|
|
86
78
|
}
|
|
87
79
|
|
|
@@ -95,226 +87,10 @@ async function safeLog() {
|
|
|
95
87
|
}
|
|
96
88
|
}
|
|
97
89
|
|
|
98
|
-
// Clean up AI-generated commit message
|
|
99
|
-
function cleanCommitMessage(message, { body = false } = {}) {
|
|
100
|
-
if (!message) return 'Update project code';
|
|
101
|
-
// Remove markdown code blocks and formatting
|
|
102
|
-
let cleaned = message
|
|
103
|
-
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
|
|
104
|
-
.replace(/`([^`]+)`/g, '$1') // Remove inline code formatting
|
|
105
|
-
.replace(/^\s*[-*+]\s*/gm, '') // Remove bullet points
|
|
106
|
-
.replace(/^\s*\d+\.\s*/gm, '') // Remove numbered lists
|
|
107
|
-
.replace(/^\s*#+\s*/gm, '') // Remove headers
|
|
108
|
-
.replace(/\*\*(.*?)\*\*/g, '$1') // Remove bold formatting
|
|
109
|
-
.replace(/\*(.*?)\*/g, '$1') // Remove italic formatting
|
|
110
|
-
.replace(/[\u{1F300}-\u{1FAFF}]/gu, '') // strip most emojis
|
|
111
|
-
.replace(/[\t\r]+/g, ' ')
|
|
112
|
-
.trim();
|
|
113
|
-
|
|
114
|
-
// If a body is allowed, split subject/body, otherwise keep first line only
|
|
115
|
-
const lines = cleaned.split('\n').map(l => l.trim()).filter(Boolean);
|
|
116
|
-
let subject = (lines[0] || '').replace(/\s{2,}/g, ' ').replace(/[\s:,.!;]+$/g, '').trim();
|
|
117
|
-
if (subject.length === 0) subject = 'Update project code';
|
|
118
|
-
// Enforce concise subject
|
|
119
|
-
if (subject.length > 72) subject = subject.substring(0, 69) + '...';
|
|
120
|
-
|
|
121
|
-
if (!body) return subject;
|
|
122
|
-
|
|
123
|
-
const bodyLines = lines.slice(1).filter(l => l.length > 0);
|
|
124
|
-
const bodyText = bodyLines.join('\n').trim();
|
|
125
|
-
return bodyText ? `${subject}\n\n${bodyText}` : subject;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Estimate tokens (rough approximation: 1 token ≈ 4 characters)
|
|
129
|
-
function estimateTokens(text) {
|
|
130
|
-
return Math.ceil((text || '').length / 4);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function resolveProvider(pref) {
|
|
134
|
-
// pref: auto|openai|gemini|groq|none
|
|
135
|
-
if (pref === 'none') return 'none';
|
|
136
|
-
if (pref === 'openai') return process.env.OPENAI_API_KEY ? 'openai' : 'none';
|
|
137
|
-
if (pref === 'gemini') return process.env.GEMINI_API_KEY ? 'gemini' : 'none';
|
|
138
|
-
if (pref === 'groq') return process.env.GROQ_API_KEY ? 'groq' : 'none';
|
|
139
|
-
// auto
|
|
140
|
-
if (process.env.GEMINI_API_KEY) return 'gemini';
|
|
141
|
-
if (process.env.OPENAI_API_KEY) return 'openai';
|
|
142
|
-
if (process.env.GROQ_API_KEY) return 'groq';
|
|
143
|
-
return 'none';
|
|
144
|
-
}
|
|
145
90
|
|
|
146
|
-
async function getHumanReadableChanges(limitPerList = 10) {
|
|
147
|
-
try {
|
|
148
|
-
const status = await git.status();
|
|
149
|
-
const modified = status.modified.slice(0, limitPerList);
|
|
150
|
-
const created = status.created.slice(0, limitPerList);
|
|
151
|
-
const deleted = status.deleted.slice(0, limitPerList);
|
|
152
|
-
const renamed = status.renamed.map(r => `${r.from}→${r.to}`).slice(0, limitPerList);
|
|
153
|
-
const parts = [];
|
|
154
|
-
if (created.length) parts.push(`Added: ${created.join(', ')}`);
|
|
155
|
-
if (modified.length) parts.push(`Modified: ${modified.join(', ')}`);
|
|
156
|
-
if (deleted.length) parts.push(`Deleted: ${deleted.join(', ')}`);
|
|
157
|
-
if (renamed.length) parts.push(`Renamed: ${renamed.join(', ')}`);
|
|
158
|
-
return parts.join('\n');
|
|
159
|
-
} catch (_) {
|
|
160
|
-
return 'Multiple file changes.';
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
91
|
|
|
164
|
-
function localHeuristicMessage(status, { conventional = false } = {}) {
|
|
165
|
-
const created = status.created.length;
|
|
166
|
-
const modified = status.modified.length;
|
|
167
|
-
const deleted = status.deleted.length;
|
|
168
|
-
const total = created + modified + deleted + status.renamed.length;
|
|
169
|
-
|
|
170
|
-
const listFew = (arr) => arr.slice(0, 3).join(', ') + (arr.length > 3 ? ` and ${arr.length - 3} more` : '');
|
|
171
|
-
|
|
172
|
-
let type = 'chore';
|
|
173
|
-
let subject = 'update files';
|
|
174
|
-
if (created > 0 && modified === 0 && deleted === 0) {
|
|
175
|
-
type = 'feat';
|
|
176
|
-
subject = created <= 3 ? `add ${listFew(status.created)}` : `add ${created} files`;
|
|
177
|
-
} else if (deleted > 0 && created === 0 && modified === 0) {
|
|
178
|
-
type = 'chore';
|
|
179
|
-
subject = deleted <= 3 ? `remove ${listFew(status.deleted)}` : `remove ${deleted} files`;
|
|
180
|
-
} else if (modified > 0 && created === 0 && deleted === 0) {
|
|
181
|
-
type = 'chore';
|
|
182
|
-
subject = modified <= 3 ? `update ${listFew(status.modified)}` : `update ${modified} files`;
|
|
183
|
-
} else if (created > 0 || deleted > 0 || modified > 0) {
|
|
184
|
-
type = 'chore';
|
|
185
|
-
subject = `update ${total} files`;
|
|
186
|
-
}
|
|
187
|
-
const msg = conventional ? `${type}: ${subject}` : subject.charAt(0).toUpperCase() + subject.slice(1);
|
|
188
|
-
return msg;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Generate commit message with multiple fallback strategies
|
|
192
92
|
async function generateCommitMessage(rawDiff, options = {}) {
|
|
193
|
-
|
|
194
|
-
const MAX_TOKENS = 100000; // Conservative limit (well below 128k)
|
|
195
|
-
const MAX_CHARS = MAX_TOKENS * 4;
|
|
196
|
-
|
|
197
|
-
let content = rawDiff;
|
|
198
|
-
let strategy = 'full';
|
|
199
|
-
|
|
200
|
-
const logv = (m) => { if (verbose) console.log(color.cyan(`[gims] ${m}`)); };
|
|
201
|
-
|
|
202
|
-
// Strategy 1: Check if full diff is too large
|
|
203
|
-
if (estimateTokens(rawDiff) > MAX_TOKENS) {
|
|
204
|
-
strategy = 'summary';
|
|
205
|
-
try {
|
|
206
|
-
const summary = await git.diffSummary();
|
|
207
|
-
content = summary.files
|
|
208
|
-
.map(f => `${f.file}: +${f.insertions} -${f.deletions}`)
|
|
209
|
-
.join('\n');
|
|
210
|
-
} catch (e) {
|
|
211
|
-
strategy = 'fallback';
|
|
212
|
-
content = 'Large changes across multiple files';
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Strategy 2: If summary is still too large, use status
|
|
217
|
-
if (strategy === 'summary' && estimateTokens(content) > MAX_TOKENS) {
|
|
218
|
-
strategy = 'status';
|
|
219
|
-
try {
|
|
220
|
-
const status = await git.status();
|
|
221
|
-
const modified = status.modified.slice(0, 10);
|
|
222
|
-
const created = status.created.slice(0, 10);
|
|
223
|
-
const deleted = status.deleted.slice(0, 10);
|
|
224
|
-
const renamed = status.renamed.map(r => `${r.from}→${r.to}`).slice(0, 10);
|
|
225
|
-
|
|
226
|
-
content = [
|
|
227
|
-
modified.length > 0 ? `Modified: ${modified.join(', ')}` : '',
|
|
228
|
-
created.length > 0 ? `Added: ${created.join(', ')}` : '',
|
|
229
|
-
deleted.length > 0 ? `Deleted: ${deleted.join(', ')}` : '',
|
|
230
|
-
renamed.length > 0 ? `Renamed: ${renamed.join(', ')}` : '',
|
|
231
|
-
].filter(Boolean).join('\n');
|
|
232
|
-
|
|
233
|
-
if (status.files.length > 30) {
|
|
234
|
-
content += `\n... and ${status.files.length - 30} more files`;
|
|
235
|
-
}
|
|
236
|
-
} catch (e) {
|
|
237
|
-
strategy = 'fallback';
|
|
238
|
-
content = 'Large changes across multiple files';
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Strategy 3: If still too large, truncate
|
|
243
|
-
if (estimateTokens(content) > MAX_TOKENS) {
|
|
244
|
-
strategy = 'truncated';
|
|
245
|
-
content = content.substring(0, MAX_CHARS - 1000) + '\n... (truncated)';
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const prompts = {
|
|
249
|
-
full: 'Write a concise git commit message for these changes:',
|
|
250
|
-
summary: 'Changes are large; using summary. Write a concise git commit message for these changes:',
|
|
251
|
-
status: 'Many files changed. Write a concise git commit message based on these file changes:',
|
|
252
|
-
truncated: 'Large diff truncated. Write a concise git commit message for these changes:',
|
|
253
|
-
fallback: 'Write a concise git commit message for:',
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const style = conventional ? 'Use Conventional Commits (e.g., feat:, fix:, chore:) for the subject.' : 'Subject must be a single short line.';
|
|
257
|
-
const bodyInstr = body ? 'Provide a short subject line followed by an optional body separated by a blank line.' : 'Return only a short subject line without extra quotes.';
|
|
258
|
-
const prompt = `${prompts[strategy]}\n${content}\n\n${style} ${bodyInstr}`;
|
|
259
|
-
|
|
260
|
-
// Final safety check
|
|
261
|
-
if (estimateTokens(prompt) > MAX_TOKENS) {
|
|
262
|
-
console.warn(color.yellow('Changes too large for AI analysis, using default message'));
|
|
263
|
-
return cleanCommitMessage('Update multiple files', { body });
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
let message = 'Update project code'; // Default fallback
|
|
267
|
-
const provider = resolveProvider(prefProvider);
|
|
268
|
-
logv(`strategy=${strategy}, provider=${provider}${model ? `, model=${model}` : ''}`);
|
|
269
|
-
|
|
270
|
-
try {
|
|
271
|
-
if (provider === 'gemini') {
|
|
272
|
-
const genai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
|
273
|
-
const res = await genai.models.generateContent({
|
|
274
|
-
model: model || 'gemini-2.0-flash',
|
|
275
|
-
contents: prompt,
|
|
276
|
-
});
|
|
277
|
-
message = (await res.response.text()).trim();
|
|
278
|
-
} else if (provider === 'openai') {
|
|
279
|
-
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
280
|
-
const res = await openai.chat.completions.create({
|
|
281
|
-
model: model || 'gpt-4o-mini',
|
|
282
|
-
messages: [{ role: 'user', content: prompt }],
|
|
283
|
-
temperature: 0.3,
|
|
284
|
-
max_tokens: body ? 200 : 80,
|
|
285
|
-
});
|
|
286
|
-
message = (res.choices[0] && res.choices[0].message && res.choices[0].message.content || '').trim();
|
|
287
|
-
} else if (provider === 'groq') {
|
|
288
|
-
// Use OpenAI-compatible API via baseURL
|
|
289
|
-
const groq = new OpenAI({ apiKey: process.env.GROQ_API_KEY, baseURL: process.env.GROQ_BASE_URL || 'https://api.groq.com/openai/v1' });
|
|
290
|
-
const res = await groq.chat.completions.create({
|
|
291
|
-
model: model || 'llama-3.1-8b-instant',
|
|
292
|
-
messages: [{ role: 'user', content: prompt }],
|
|
293
|
-
temperature: 0.3,
|
|
294
|
-
max_tokens: body ? 200 : 80,
|
|
295
|
-
});
|
|
296
|
-
message = (res.choices[0] && res.choices[0].message && res.choices[0].message.content || '').trim();
|
|
297
|
-
} else {
|
|
298
|
-
// Local heuristic fallback
|
|
299
|
-
const status = await git.status();
|
|
300
|
-
message = localHeuristicMessage(status, { conventional });
|
|
301
|
-
const human = await getHumanReadableChanges();
|
|
302
|
-
if (body) message = `${message}\n\n${human}`;
|
|
303
|
-
}
|
|
304
|
-
} catch (error) {
|
|
305
|
-
if (error && error.code === 'context_length_exceeded') {
|
|
306
|
-
console.warn(color.yellow('Content still too large for AI, using default message'));
|
|
307
|
-
return cleanCommitMessage('Update multiple files', { body });
|
|
308
|
-
}
|
|
309
|
-
console.warn(color.yellow(`AI generation failed: ${error && error.message ? error.message : error}`));
|
|
310
|
-
// fallback to local heuristic
|
|
311
|
-
const status = await git.status();
|
|
312
|
-
message = localHeuristicMessage(status, { conventional });
|
|
313
|
-
const human = await getHumanReadableChanges();
|
|
314
|
-
if (body) message = `${message}\n\n${human}`;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
return cleanCommitMessage(message, { body });
|
|
93
|
+
return await aiProvider.generateCommitMessage(rawDiff, options);
|
|
318
94
|
}
|
|
319
95
|
|
|
320
96
|
async function resolveCommit(input) {
|
|
@@ -350,12 +126,216 @@ program
|
|
|
350
126
|
.option('--json', 'JSON output for suggest')
|
|
351
127
|
.option('--yes', 'Assume yes for confirmations')
|
|
352
128
|
.option('--amend', 'Amend the last commit instead of creating a new one')
|
|
353
|
-
.option('--set-upstream', 'Set upstream on push if missing')
|
|
129
|
+
.option('--set-upstream', 'Set upstream on push if missing')
|
|
130
|
+
.hook('preAction', () => {
|
|
131
|
+
initializeComponents();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
program.command('setup')
|
|
135
|
+
.description('Run interactive setup wizard')
|
|
136
|
+
.option('--api-key <provider>', 'Quick API key setup (openai|gemini|groq)')
|
|
137
|
+
.action(async (options) => {
|
|
138
|
+
try {
|
|
139
|
+
if (options.apiKey) {
|
|
140
|
+
await setupApiKey(options.apiKey);
|
|
141
|
+
} else {
|
|
142
|
+
await configManager.runSetupWizard();
|
|
143
|
+
}
|
|
144
|
+
} catch (e) {
|
|
145
|
+
handleError('Setup error', e);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
async function setupApiKey(provider) {
|
|
150
|
+
const readline = require('readline');
|
|
151
|
+
const rl = readline.createInterface({
|
|
152
|
+
input: process.stdin,
|
|
153
|
+
output: process.stdout
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const question = (prompt) => new Promise(resolve => {
|
|
157
|
+
rl.question(prompt, answer => {
|
|
158
|
+
resolve(answer.trim());
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
console.log(color.bold(`\n🔑 ${provider.toUpperCase()} API Key Setup\n`));
|
|
163
|
+
|
|
164
|
+
const envVars = {
|
|
165
|
+
'openai': 'OPENAI_API_KEY',
|
|
166
|
+
'gemini': 'GEMINI_API_KEY',
|
|
167
|
+
'groq': 'GROQ_API_KEY'
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const envVar = envVars[provider.toLowerCase()];
|
|
171
|
+
if (!envVar) {
|
|
172
|
+
console.log(color.red('Invalid provider. Use: openai, gemini, or groq'));
|
|
173
|
+
rl.close();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(`To get your ${provider.toUpperCase()} API key:`);
|
|
178
|
+
if (provider === 'openai') {
|
|
179
|
+
console.log('1. Go to: https://platform.openai.com/api-keys');
|
|
180
|
+
console.log('2. Create a new API key');
|
|
181
|
+
} else if (provider === 'gemini') {
|
|
182
|
+
console.log('1. Go to: https://aistudio.google.com/app/apikey');
|
|
183
|
+
console.log('2. Create a new API key');
|
|
184
|
+
} else if (provider === 'groq') {
|
|
185
|
+
console.log('1. Go to: https://console.groq.com/keys');
|
|
186
|
+
console.log('2. Create a new API key');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const apiKey = await question(`\nEnter your ${provider.toUpperCase()} API key: `);
|
|
190
|
+
|
|
191
|
+
if (!apiKey) {
|
|
192
|
+
console.log(color.yellow('No API key provided. Setup cancelled.'));
|
|
193
|
+
rl.close();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
rl.close();
|
|
198
|
+
|
|
199
|
+
// Show how to set the environment variable
|
|
200
|
+
console.log(`\n${color.green('✓')} API key received!`);
|
|
201
|
+
console.log('\nTo use this API key, set the environment variable:');
|
|
202
|
+
console.log(color.cyan(`export ${envVar}="${apiKey}"`));
|
|
203
|
+
console.log('\nOr add it to your shell profile (~/.bashrc, ~/.zshrc, etc.):');
|
|
204
|
+
console.log(color.cyan(`echo 'export ${envVar}="${apiKey}"' >> ~/.zshrc`));
|
|
205
|
+
|
|
206
|
+
// Set provider in config
|
|
207
|
+
const config = configManager.load();
|
|
208
|
+
config.provider = provider;
|
|
209
|
+
configManager.save(config);
|
|
210
|
+
|
|
211
|
+
console.log(`\n${color.green('✓')} Provider set to ${provider} in local config`);
|
|
212
|
+
console.log('\nRestart your terminal and try:');
|
|
213
|
+
console.log(` ${color.cyan('g sg')} - Get AI suggestions`);
|
|
214
|
+
console.log(` ${color.cyan('g o')} - AI commit and push`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
program.command('status').alias('s')
|
|
218
|
+
.description('Enhanced git status with AI insights')
|
|
219
|
+
.action(async () => {
|
|
220
|
+
await ensureRepo();
|
|
221
|
+
try {
|
|
222
|
+
const enhancedStatus = await gitAnalyzer.getEnhancedStatus();
|
|
223
|
+
console.log(gitAnalyzer.formatStatusOutput(enhancedStatus));
|
|
224
|
+
|
|
225
|
+
// Show commit history summary
|
|
226
|
+
const history = await gitAnalyzer.analyzeCommitHistory(5);
|
|
227
|
+
if (history.totalCommits > 0) {
|
|
228
|
+
console.log(`\n${color.bold('Recent Activity:')}`);
|
|
229
|
+
console.log(`${history.recentActivity.last24h} commits in last 24h, ${history.recentActivity.lastWeek} in last week`);
|
|
230
|
+
if (history.conventionalCommits > 0) {
|
|
231
|
+
const percentage = Math.round((history.conventionalCommits / history.totalCommits) * 100);
|
|
232
|
+
console.log(`${percentage}% of recent commits use Conventional Commits format`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch (e) {
|
|
236
|
+
handleError('Status error', e);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
program.command('interactive').alias('i')
|
|
241
|
+
.description('Interactive commit wizard')
|
|
242
|
+
.action(async () => {
|
|
243
|
+
await ensureRepo();
|
|
244
|
+
const opts = getOpts();
|
|
245
|
+
try {
|
|
246
|
+
await interactive.runInteractiveCommit(opts);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
handleError('Interactive commit error', e);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
program.command('preview').alias('p')
|
|
253
|
+
.description('Preview commit with AI-generated message')
|
|
254
|
+
.action(async () => {
|
|
255
|
+
await ensureRepo();
|
|
256
|
+
const opts = getOpts();
|
|
257
|
+
try {
|
|
258
|
+
await interactive.showCommitPreview(opts);
|
|
259
|
+
} catch (e) {
|
|
260
|
+
handleError('Preview error', e);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
program.command('config')
|
|
265
|
+
.description('Manage GIMS configuration')
|
|
266
|
+
.option('--set <key=value>', 'Set configuration value')
|
|
267
|
+
.option('--get <key>', 'Get configuration value')
|
|
268
|
+
.option('--list', 'List all configuration')
|
|
269
|
+
.option('--global', 'Use global configuration')
|
|
270
|
+
.action(async (options) => {
|
|
271
|
+
try {
|
|
272
|
+
if (options.set) {
|
|
273
|
+
const [key, value] = options.set.split('=');
|
|
274
|
+
if (!key || value === undefined) {
|
|
275
|
+
console.log('Usage: --set key=value');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const result = configManager.set(key, value, options.global);
|
|
279
|
+
Progress.success(`Set ${result.key}=${result.value} in ${result.savedPath}`);
|
|
280
|
+
} else if (options.get) {
|
|
281
|
+
const value = configManager.get(options.get);
|
|
282
|
+
console.log(value !== undefined ? value : 'Not set');
|
|
283
|
+
} else if (options.list) {
|
|
284
|
+
const config = configManager.get();
|
|
285
|
+
console.log(color.bold('Current Configuration:'));
|
|
286
|
+
Object.entries(config).forEach(([key, value]) => {
|
|
287
|
+
if (key !== '_source') {
|
|
288
|
+
console.log(` ${color.cyan(key)}: ${value}`);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
console.log(`\n${color.dim('Source: ' + config._source)}`);
|
|
292
|
+
} else {
|
|
293
|
+
console.log('Use --set, --get, or --list');
|
|
294
|
+
}
|
|
295
|
+
} catch (e) {
|
|
296
|
+
handleError('Config error', e);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
program.command('help-quick').alias('q')
|
|
301
|
+
.description('Show quick reference for main commands')
|
|
302
|
+
.action(() => {
|
|
303
|
+
console.log(color.bold('🚀 GIMS Quick Reference\n'));
|
|
304
|
+
|
|
305
|
+
console.log(color.bold('Single-Letter Workflow:'));
|
|
306
|
+
console.log(` ${color.cyan('g s')} Status - Enhanced git status with AI insights`);
|
|
307
|
+
console.log(` ${color.cyan('g i')} Interactive - Guided commit wizard`);
|
|
308
|
+
console.log(` ${color.cyan('g p')} Preview - See what will be committed`);
|
|
309
|
+
console.log(` ${color.cyan('g l')} Local - AI commit locally`);
|
|
310
|
+
console.log(` ${color.cyan('g o')} Online - AI commit + push`);
|
|
311
|
+
console.log(` ${color.cyan('g h')} History - Numbered commit log`);
|
|
312
|
+
console.log(` ${color.cyan('g a')} Amend - Smart amend with AI`);
|
|
313
|
+
console.log(` ${color.cyan('g u')} Undo - Undo last commit\n`);
|
|
314
|
+
|
|
315
|
+
console.log(color.bold('Quick Setup:'));
|
|
316
|
+
console.log(` ${color.cyan('g setup --api-key gemini')} 🚀 gemini-2.5-flash (recommended)`);
|
|
317
|
+
console.log(` ${color.cyan('g setup --api-key openai')} 💎 gpt-5 (high quality)`);
|
|
318
|
+
console.log(` ${color.cyan('g setup --api-key groq')} ⚡ groq/compound (ultra fast)\n`);
|
|
319
|
+
|
|
320
|
+
console.log(color.bold('Essential Workflow:'));
|
|
321
|
+
console.log(` ${color.cyan('g s')} Check what's changed`);
|
|
322
|
+
console.log(` ${color.cyan('g i')} or ${color.cyan('g o')} Commit with AI`);
|
|
323
|
+
console.log(` ${color.cyan('g h')} View history\n`);
|
|
324
|
+
|
|
325
|
+
console.log(`For full help: ${color.cyan('g --help')}`);
|
|
326
|
+
console.log(`For detailed docs: See README.md`);
|
|
327
|
+
});
|
|
354
328
|
|
|
355
|
-
program.command('init')
|
|
329
|
+
program.command('init')
|
|
356
330
|
.description('Initialize a new Git repository')
|
|
357
331
|
.action(async () => {
|
|
358
|
-
try {
|
|
332
|
+
try {
|
|
333
|
+
await git.init();
|
|
334
|
+
Progress.success('Initialized git repository');
|
|
335
|
+
console.log(`\nNext steps:`);
|
|
336
|
+
console.log(` ${color.cyan('g setup')} - Configure GIMS`);
|
|
337
|
+
console.log(` ${color.cyan('g s')} - Check repository status`);
|
|
338
|
+
}
|
|
359
339
|
catch (e) { handleError('Init error', e); }
|
|
360
340
|
});
|
|
361
341
|
|
|
@@ -366,14 +346,16 @@ program.command('clone <repo>').alias('c')
|
|
|
366
346
|
catch (e) { handleError('Clone error', e); }
|
|
367
347
|
});
|
|
368
348
|
|
|
369
|
-
program.command('suggest').alias('
|
|
349
|
+
program.command('suggest').alias('sg')
|
|
370
350
|
.description('Suggest commit message and copy to clipboard')
|
|
371
|
-
.
|
|
351
|
+
.option('--multiple', 'Generate multiple suggestions')
|
|
352
|
+
.action(async (cmdOptions) => {
|
|
372
353
|
await ensureRepo();
|
|
373
354
|
const opts = getOpts();
|
|
374
355
|
|
|
375
356
|
try {
|
|
376
357
|
if (opts.all) {
|
|
358
|
+
Progress.info('Staging all changes...');
|
|
377
359
|
await git.add('.');
|
|
378
360
|
}
|
|
379
361
|
|
|
@@ -381,26 +363,52 @@ program.command('suggest').alias('s')
|
|
|
381
363
|
const rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
|
382
364
|
if (!rawDiff.trim()) {
|
|
383
365
|
if (opts.all) {
|
|
384
|
-
|
|
366
|
+
Progress.warning('No changes to suggest');
|
|
385
367
|
return;
|
|
386
368
|
}
|
|
387
|
-
|
|
369
|
+
Progress.warning('No staged changes. Use --all to stage everything or stage files manually');
|
|
388
370
|
return;
|
|
389
371
|
}
|
|
390
372
|
|
|
391
|
-
|
|
373
|
+
if (cmdOptions.multiple) {
|
|
374
|
+
if (opts.progressIndicators) Progress.start('🤖 Generating multiple suggestions');
|
|
375
|
+
const suggestions = await aiProvider.generateMultipleSuggestions(rawDiff, opts, 3);
|
|
376
|
+
if (opts.progressIndicators) Progress.stop('');
|
|
377
|
+
|
|
378
|
+
console.log(color.bold('\n📝 Suggested commit messages:\n'));
|
|
379
|
+
suggestions.forEach((msg, i) => {
|
|
380
|
+
console.log(`${color.cyan((i + 1).toString())}. ${msg}`);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
if (!opts.noClipboard && suggestions.length > 0) {
|
|
384
|
+
try {
|
|
385
|
+
clipboard.writeSync(suggestions[0]);
|
|
386
|
+
console.log(`\n${color.green('✓')} First suggestion copied to clipboard`);
|
|
387
|
+
} catch (_) {
|
|
388
|
+
console.log(`\n${color.yellow('⚠')} Clipboard copy failed`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
if (opts.progressIndicators) Progress.start('🤖 Analyzing changes');
|
|
393
|
+
const msg = await generateCommitMessage(rawDiff, opts);
|
|
394
|
+
if (opts.progressIndicators) Progress.stop('');
|
|
392
395
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
396
|
+
if (opts.json) {
|
|
397
|
+
const out = { message: msg };
|
|
398
|
+
console.log(JSON.stringify(out));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
398
401
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
402
|
+
if (!opts.noClipboard) {
|
|
403
|
+
try {
|
|
404
|
+
clipboard.writeSync(msg);
|
|
405
|
+
Progress.success(`"${msg}" (copied to clipboard)`);
|
|
406
|
+
} catch (_) {
|
|
407
|
+
console.log(`Suggested: "${msg}" ${color.yellow('(clipboard copy failed)')}`);
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
console.log(`Suggested: "${msg}"`);
|
|
411
|
+
}
|
|
404
412
|
}
|
|
405
413
|
} catch (e) {
|
|
406
414
|
handleError('Suggest error', e);
|
|
@@ -415,16 +423,29 @@ program.command('local').alias('l')
|
|
|
415
423
|
|
|
416
424
|
try {
|
|
417
425
|
if (!(await hasChanges()) && !opts.all) {
|
|
418
|
-
|
|
426
|
+
Progress.warning('No changes to commit');
|
|
419
427
|
return;
|
|
420
428
|
}
|
|
421
429
|
|
|
422
|
-
if (opts.all)
|
|
430
|
+
if (opts.all) {
|
|
431
|
+
Progress.info('Staging all changes...');
|
|
432
|
+
await git.add('.');
|
|
433
|
+
}
|
|
423
434
|
|
|
424
|
-
|
|
425
|
-
if (!rawDiff.trim()) {
|
|
435
|
+
let rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
|
436
|
+
if (!rawDiff.trim()) {
|
|
437
|
+
Progress.info('No staged changes found; staging all changes...');
|
|
438
|
+
await git.add('.');
|
|
439
|
+
rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
|
440
|
+
if (!rawDiff.trim()) {
|
|
441
|
+
Progress.warning('No changes to commit');
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
426
445
|
|
|
446
|
+
if (opts.progressIndicators) Progress.start('🤖 Generating commit message');
|
|
427
447
|
const msg = await generateCommitMessage(rawDiff, opts);
|
|
448
|
+
if (opts.progressIndicators) Progress.stop('');
|
|
428
449
|
|
|
429
450
|
if (opts.dryRun) {
|
|
430
451
|
console.log(color.yellow('[dry-run] Would commit with message:'));
|
|
@@ -434,10 +455,11 @@ program.command('local').alias('l')
|
|
|
434
455
|
|
|
435
456
|
if (opts.amend) {
|
|
436
457
|
await git.raw(['commit', '--amend', '-m', msg]);
|
|
458
|
+
Progress.success(`Amended commit: "${msg}"`);
|
|
437
459
|
} else {
|
|
438
460
|
await git.commit(msg);
|
|
461
|
+
Progress.success(`Committed locally: "${msg}"`);
|
|
439
462
|
}
|
|
440
|
-
console.log(`Committed locally: "${msg}"`);
|
|
441
463
|
} catch (e) {
|
|
442
464
|
handleError('Local commit error', e);
|
|
443
465
|
}
|
|
@@ -451,16 +473,29 @@ program.command('online').alias('o')
|
|
|
451
473
|
|
|
452
474
|
try {
|
|
453
475
|
if (!(await hasChanges()) && !opts.all) {
|
|
454
|
-
|
|
476
|
+
Progress.warning('No changes to commit');
|
|
455
477
|
return;
|
|
456
478
|
}
|
|
457
479
|
|
|
458
|
-
if (opts.all)
|
|
480
|
+
if (opts.all) {
|
|
481
|
+
Progress.info('Staging all changes...');
|
|
482
|
+
await git.add('.');
|
|
483
|
+
}
|
|
459
484
|
|
|
460
|
-
|
|
461
|
-
if (!rawDiff.trim()) {
|
|
485
|
+
let rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
|
486
|
+
if (!rawDiff.trim()) {
|
|
487
|
+
Progress.info('No staged changes found; staging all changes...');
|
|
488
|
+
await git.add('.');
|
|
489
|
+
rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
|
490
|
+
if (!rawDiff.trim()) {
|
|
491
|
+
Progress.warning('No changes to commit');
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
462
495
|
|
|
496
|
+
if (opts.progressIndicators) Progress.start('🤖 Generating commit message');
|
|
463
497
|
const msg = await generateCommitMessage(rawDiff, opts);
|
|
498
|
+
if (opts.progressIndicators) Progress.stop('');
|
|
464
499
|
|
|
465
500
|
if (opts.dryRun) {
|
|
466
501
|
console.log(color.yellow('[dry-run] Would commit & push with message:'));
|
|
@@ -468,6 +503,7 @@ program.command('online').alias('o')
|
|
|
468
503
|
return;
|
|
469
504
|
}
|
|
470
505
|
|
|
506
|
+
Progress.info('Committing changes...');
|
|
471
507
|
if (opts.amend) {
|
|
472
508
|
await git.raw(['commit', '--amend', '-m', msg]);
|
|
473
509
|
} else {
|
|
@@ -475,18 +511,20 @@ program.command('online').alias('o')
|
|
|
475
511
|
}
|
|
476
512
|
|
|
477
513
|
try {
|
|
514
|
+
Progress.info('Pushing to remote...');
|
|
478
515
|
await git.push();
|
|
479
|
-
|
|
516
|
+
Progress.success(`Committed & pushed: "${msg}"`);
|
|
480
517
|
} catch (pushErr) {
|
|
481
518
|
const msgErr = pushErr && pushErr.message ? pushErr.message : String(pushErr);
|
|
482
519
|
if (/no upstream|set the remote as upstream|have no upstream/.test(msgErr)) {
|
|
483
520
|
// Try to set upstream if requested
|
|
484
521
|
if (opts.setUpstream) {
|
|
522
|
+
Progress.info('Setting upstream branch...');
|
|
485
523
|
const branch = (await git.raw(['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
|
|
486
524
|
await git.push(['--set-upstream', 'origin', branch]);
|
|
487
|
-
|
|
525
|
+
Progress.success(`Committed & pushed (upstream set to origin/${branch}): "${msg}"`);
|
|
488
526
|
} else {
|
|
489
|
-
|
|
527
|
+
Progress.warning('Current branch has no upstream. Use --set-upstream to set origin/<branch> automatically');
|
|
490
528
|
}
|
|
491
529
|
} else {
|
|
492
530
|
throw pushErr;
|
|
@@ -514,8 +552,14 @@ program.command('commit <message...>').alias('m')
|
|
|
514
552
|
|
|
515
553
|
if (opts.all) await git.add('.');
|
|
516
554
|
|
|
517
|
-
|
|
518
|
-
if (!rawDiff.trim()) {
|
|
555
|
+
let rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
|
556
|
+
if (!rawDiff.trim()) {
|
|
557
|
+
// Auto-stage all changes by default when nothing is staged
|
|
558
|
+
console.log(color.yellow('No staged changes found; staging all changes (git add .).'));
|
|
559
|
+
await git.add('.');
|
|
560
|
+
rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
|
561
|
+
if (!rawDiff.trim()) { console.log('No changes to commit.'); return; }
|
|
562
|
+
}
|
|
519
563
|
|
|
520
564
|
if (opts.dryRun) {
|
|
521
565
|
console.log(color.yellow('[dry-run] Would commit with custom message:'));
|
|
@@ -534,61 +578,195 @@ program.command('commit <message...>').alias('m')
|
|
|
534
578
|
}
|
|
535
579
|
});
|
|
536
580
|
|
|
537
|
-
program.command('pull')
|
|
581
|
+
program.command('pull')
|
|
538
582
|
.description('Pull latest changes')
|
|
539
583
|
.action(async () => {
|
|
540
584
|
await ensureRepo();
|
|
541
|
-
try {
|
|
585
|
+
try {
|
|
586
|
+
Progress.info('Pulling latest changes...');
|
|
587
|
+
await git.pull();
|
|
588
|
+
Progress.success('Pulled latest changes');
|
|
589
|
+
}
|
|
542
590
|
catch (e) { handleError('Pull error', e); }
|
|
543
591
|
});
|
|
544
592
|
|
|
593
|
+
program.command('sync')
|
|
594
|
+
.description('Smart sync: pull + rebase/merge')
|
|
595
|
+
.option('--rebase', 'Use rebase instead of merge')
|
|
596
|
+
.action(async (cmdOptions) => {
|
|
597
|
+
await ensureRepo();
|
|
598
|
+
try {
|
|
599
|
+
const status = await git.status();
|
|
600
|
+
|
|
601
|
+
if (status.files.length > 0) {
|
|
602
|
+
Progress.warning('You have uncommitted changes. Commit or stash them first.');
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
Progress.info('Fetching latest changes...');
|
|
607
|
+
await git.fetch();
|
|
608
|
+
|
|
609
|
+
const currentBranch = (await git.raw(['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
|
|
610
|
+
const remoteBranch = `origin/${currentBranch}`;
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
const behind = await git.raw(['rev-list', '--count', `${currentBranch}..${remoteBranch}`]);
|
|
614
|
+
const ahead = await git.raw(['rev-list', '--count', `${remoteBranch}..${currentBranch}`]);
|
|
615
|
+
|
|
616
|
+
if (parseInt(behind.trim()) === 0) {
|
|
617
|
+
Progress.success('Already up to date');
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (parseInt(ahead.trim()) > 0) {
|
|
622
|
+
Progress.info(`Branch is ${ahead.trim()} commits ahead and ${behind.trim()} commits behind`);
|
|
623
|
+
if (cmdOptions.rebase) {
|
|
624
|
+
Progress.info('Rebasing...');
|
|
625
|
+
await git.rebase([remoteBranch]);
|
|
626
|
+
Progress.success('Rebased successfully');
|
|
627
|
+
} else {
|
|
628
|
+
Progress.info('Merging...');
|
|
629
|
+
await git.merge([remoteBranch]);
|
|
630
|
+
Progress.success('Merged successfully');
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
Progress.info('Fast-forwarding...');
|
|
634
|
+
await git.merge([remoteBranch]);
|
|
635
|
+
Progress.success('Fast-forwarded successfully');
|
|
636
|
+
}
|
|
637
|
+
} catch (error) {
|
|
638
|
+
if (error.message.includes('unknown revision')) {
|
|
639
|
+
Progress.info('No remote tracking branch, pulling...');
|
|
640
|
+
await git.pull();
|
|
641
|
+
Progress.success('Pulled latest changes');
|
|
642
|
+
} else {
|
|
643
|
+
throw error;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
} catch (e) {
|
|
647
|
+
handleError('Sync error', e);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
program.command('stash')
|
|
652
|
+
.description('Enhanced stash with AI descriptions')
|
|
653
|
+
.option('--list', 'List stashes')
|
|
654
|
+
.option('--pop', 'Pop latest stash')
|
|
655
|
+
.option('--apply <n>', 'Apply stash by index')
|
|
656
|
+
.action(async (cmdOptions) => {
|
|
657
|
+
await ensureRepo();
|
|
658
|
+
try {
|
|
659
|
+
if (cmdOptions.list) {
|
|
660
|
+
const stashes = await git.stashList();
|
|
661
|
+
if (stashes.all.length === 0) {
|
|
662
|
+
Progress.info('No stashes found');
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
console.log(color.bold('Stashes:'));
|
|
667
|
+
stashes.all.forEach((stash, i) => {
|
|
668
|
+
console.log(`${color.cyan((i).toString())}. ${stash.message}`);
|
|
669
|
+
});
|
|
670
|
+
} else if (cmdOptions.pop) {
|
|
671
|
+
await git.stash(['pop']);
|
|
672
|
+
Progress.success('Popped latest stash');
|
|
673
|
+
} else if (cmdOptions.apply !== undefined) {
|
|
674
|
+
const index = parseInt(cmdOptions.apply);
|
|
675
|
+
await git.stash(['apply', `stash@{${index}}`]);
|
|
676
|
+
Progress.success(`Applied stash ${index}`);
|
|
677
|
+
} else {
|
|
678
|
+
// Create new stash with AI description
|
|
679
|
+
const status = await git.status();
|
|
680
|
+
if (status.files.length === 0) {
|
|
681
|
+
Progress.warning('No changes to stash');
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
Progress.start('🤖 Generating stash description');
|
|
686
|
+
const diff = await git.diff();
|
|
687
|
+
const description = await aiProvider.generateCommitMessage(diff, {
|
|
688
|
+
conventional: false,
|
|
689
|
+
body: false
|
|
690
|
+
});
|
|
691
|
+
Progress.stop('');
|
|
692
|
+
|
|
693
|
+
await git.stash(['push', '-m', `WIP: ${description}`]);
|
|
694
|
+
Progress.success(`Stashed changes: "${description}"`);
|
|
695
|
+
}
|
|
696
|
+
} catch (e) {
|
|
697
|
+
handleError('Stash error', e);
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
|
|
545
701
|
program.command('amend').alias('a')
|
|
546
|
-
.description('Stage all changes and amend last commit
|
|
547
|
-
.
|
|
702
|
+
.description('Stage all changes and amend last commit')
|
|
703
|
+
.option('--no-edit', 'Keep existing commit message')
|
|
704
|
+
.action(async (cmdOptions) => {
|
|
548
705
|
await ensureRepo();
|
|
549
706
|
try {
|
|
550
707
|
const { all } = await safeLog();
|
|
551
708
|
if (!all || all.length === 0) {
|
|
552
|
-
|
|
709
|
+
Progress.warning('No commits to amend. Make an initial commit first');
|
|
553
710
|
return;
|
|
554
711
|
}
|
|
555
712
|
|
|
713
|
+
Progress.info('Staging all changes...');
|
|
556
714
|
await git.add('.');
|
|
557
715
|
|
|
558
716
|
const rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
|
559
717
|
if (!rawDiff.trim()) {
|
|
560
|
-
|
|
718
|
+
Progress.warning('No staged changes to amend');
|
|
561
719
|
return;
|
|
562
720
|
}
|
|
563
721
|
|
|
564
|
-
|
|
565
|
-
|
|
722
|
+
if (cmdOptions.noEdit) {
|
|
723
|
+
await git.raw(['commit', '--amend', '--no-edit']);
|
|
724
|
+
Progress.success('Amended last commit with staged changes');
|
|
725
|
+
} else {
|
|
726
|
+
// Generate new message for amend
|
|
727
|
+
Progress.start('🤖 Generating updated commit message');
|
|
728
|
+
const newMessage = await generateCommitMessage(rawDiff, getOpts());
|
|
729
|
+
Progress.stop('');
|
|
730
|
+
|
|
731
|
+
await git.raw(['commit', '--amend', '-m', newMessage]);
|
|
732
|
+
Progress.success(`Amended commit: "${newMessage}"`);
|
|
733
|
+
}
|
|
566
734
|
} catch (e) {
|
|
567
735
|
handleError('Amend error', e);
|
|
568
736
|
}
|
|
569
737
|
});
|
|
570
738
|
|
|
571
|
-
program.command('list').alias('
|
|
572
|
-
.description('
|
|
573
|
-
.
|
|
739
|
+
program.command('list').alias('h')
|
|
740
|
+
.description('Numbered git log (oldest → newest)')
|
|
741
|
+
.option('--detailed', 'Show detailed information')
|
|
742
|
+
.option('--limit <n>', 'Limit number of commits', '20')
|
|
743
|
+
.action(async (cmdOptions) => {
|
|
574
744
|
await ensureRepo();
|
|
575
745
|
try {
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
746
|
+
const limit = parseInt(cmdOptions.limit) || 20;
|
|
747
|
+
const log = await git.log({ maxCount: limit });
|
|
748
|
+
const commits = [...log.all].reverse();
|
|
749
|
+
|
|
750
|
+
if (commits.length === 0) {
|
|
751
|
+
Progress.info('No commits found');
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
commits.forEach((c, i) => {
|
|
756
|
+
if (cmdOptions.detailed) {
|
|
757
|
+
const date = new Date(c.date).toLocaleString();
|
|
758
|
+
console.log(`${color.cyan((i+1).toString())}. ${color.yellow(c.hash.slice(0,7))} | ${color.dim(date)} | ${color.green(c.author_name)} → ${c.message}`);
|
|
759
|
+
} else {
|
|
760
|
+
console.log(`${color.cyan((i+1).toString())}. ${color.yellow(c.hash.slice(0,7))} ${c.message}`);
|
|
761
|
+
}
|
|
590
762
|
});
|
|
591
|
-
|
|
763
|
+
|
|
764
|
+
if (log.all.length >= limit) {
|
|
765
|
+
console.log(color.dim(`\n... showing last ${limit} commits (use --limit to see more)`));
|
|
766
|
+
}
|
|
767
|
+
} catch (e) {
|
|
768
|
+
handleError('List error', e);
|
|
769
|
+
}
|
|
592
770
|
});
|
|
593
771
|
|
|
594
772
|
program.command('branch <c> [name]').alias('b')
|
|
@@ -641,15 +819,35 @@ program.command('undo').alias('u')
|
|
|
641
819
|
.action(async (cmd) => {
|
|
642
820
|
await ensureRepo();
|
|
643
821
|
try {
|
|
822
|
+
const { all } = await safeLog();
|
|
823
|
+
if (!all || all.length === 0) {
|
|
824
|
+
Progress.warning('No commits to undo');
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const lastCommit = all[0];
|
|
644
829
|
const mode = cmd.hard ? '--hard' : '--soft';
|
|
645
830
|
const opts = getOpts();
|
|
831
|
+
|
|
646
832
|
if (!opts.yes) {
|
|
647
|
-
console.log(color.yellow(`About to
|
|
833
|
+
console.log(color.yellow(`About to undo: "${lastCommit.message}"`));
|
|
834
|
+
console.log(color.yellow(`This will run: git reset ${mode} HEAD~1`));
|
|
835
|
+
if (mode === '--hard') {
|
|
836
|
+
console.log(color.red('WARNING: Hard reset will permanently delete uncommitted changes!'));
|
|
837
|
+
}
|
|
838
|
+
console.log('Use --yes to confirm.');
|
|
648
839
|
process.exit(1);
|
|
649
840
|
}
|
|
841
|
+
|
|
650
842
|
await git.raw(['reset', mode, 'HEAD~1']);
|
|
651
|
-
|
|
652
|
-
|
|
843
|
+
Progress.success(`Undone commit: "${lastCommit.message}" (${mode} reset)`);
|
|
844
|
+
|
|
845
|
+
if (mode === '--soft') {
|
|
846
|
+
Progress.info('Changes are now staged. Use "g status" to see them.');
|
|
847
|
+
}
|
|
848
|
+
} catch (e) {
|
|
849
|
+
handleError('Undo error', e);
|
|
850
|
+
}
|
|
653
851
|
});
|
|
654
852
|
|
|
655
853
|
program.parse(process.argv);
|