gims 0.5.3 → 0.6.1

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/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
- const { OpenAI } = require('openai');
11
- const { GoogleGenAI } = require('@google/genai');
12
- const fs = require('fs');
13
- const path = require('path');
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
- // Utility: ANSI colors without extra deps
19
- const color = {
20
- green: (s) => `\x1b[32m${s}\x1b[0m`,
21
- yellow: (s) => `\x1b[33m${s}\x1b[0m`,
22
- red: (s) => `\x1b[31m${s}\x1b[0m`,
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
- // Merge precedence: CLI > config > env handled in loadConfig
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
- console.error(color.red('Not a git repository (or any of the parent directories).'));
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
- console.error(color.red(`${prefix}: ${msg}`));
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
- const { conventional = false, body = false, provider: prefProvider = 'auto', model = '', verbose = false } = options;
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')} 🚀 Fast & free (recommended)`);
317
+ console.log(` ${color.cyan('g setup --api-key openai')} 💎 High quality`);
318
+ console.log(` ${color.cyan('g setup --api-key groq')} ⚡ 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').alias('i')
329
+ program.command('init')
356
330
  .description('Initialize a new Git repository')
357
331
  .action(async () => {
358
- try { await git.init(); console.log('Initialized repo.'); }
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('s')
349
+ program.command('suggest').alias('sg')
370
350
  .description('Suggest commit message and copy to clipboard')
371
- .action(async () => {
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
- console.log('No changes to suggest.');
366
+ Progress.warning('No changes to suggest');
385
367
  return;
386
368
  }
387
- console.log('No staged changes. Use --all to stage everything or stage files manually.');
369
+ Progress.warning('No staged changes. Use --all to stage everything or stage files manually');
388
370
  return;
389
371
  }
390
372
 
391
- const msg = await generateCommitMessage(rawDiff, opts);
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
- if (opts.json) {
394
- const out = { message: msg };
395
- console.log(JSON.stringify(out));
396
- return;
397
- }
396
+ if (opts.json) {
397
+ const out = { message: msg };
398
+ console.log(JSON.stringify(out));
399
+ return;
400
+ }
398
401
 
399
- if (!opts.noClipboard) {
400
- try { clipboard.writeSync(msg); console.log(`Suggested: "${msg}" ${color.green('(copied to clipboard)')}`); }
401
- catch (_) { console.log(`Suggested: "${msg}" ${color.yellow('(clipboard copy failed)')}`); }
402
- } else {
403
- console.log(`Suggested: "${msg}"`);
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
- console.log('No changes to commit.');
426
+ Progress.warning('No changes to commit');
419
427
  return;
420
428
  }
421
429
 
422
- if (opts.all) await git.add('.');
430
+ if (opts.all) {
431
+ Progress.info('Staging all changes...');
432
+ await git.add('.');
433
+ }
423
434
 
424
- const rawDiff = await git.diff(['--cached', '--no-ext-diff']);
425
- if (!rawDiff.trim()) { console.log('No staged changes to commit.'); return; }
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
- console.log('No changes to commit.');
476
+ Progress.warning('No changes to commit');
455
477
  return;
456
478
  }
457
479
 
458
- if (opts.all) await git.add('.');
480
+ if (opts.all) {
481
+ Progress.info('Staging all changes...');
482
+ await git.add('.');
483
+ }
459
484
 
460
- const rawDiff = await git.diff(['--cached', '--no-ext-diff']);
461
- if (!rawDiff.trim()) { console.log('No staged changes to commit.'); return; }
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
- console.log(`Committed & pushed: "${msg}"`);
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
- console.log(`Committed & pushed (upstream set to origin/${branch}): "${msg}"`);
525
+ Progress.success(`Committed & pushed (upstream set to origin/${branch}): "${msg}"`);
488
526
  } else {
489
- console.log(color.yellow('Current branch has no upstream. Use --set-upstream to set origin/<branch> automatically.'));
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
- const rawDiff = await git.diff(['--cached', '--no-ext-diff']);
518
- if (!rawDiff.trim()) { console.log('No staged changes to commit.'); return; }
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').alias('p')
581
+ program.command('pull')
538
582
  .description('Pull latest changes')
539
583
  .action(async () => {
540
584
  await ensureRepo();
541
- try { await git.pull(); console.log('Pulled latest.'); }
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 (no message edit)')
547
- .action(async () => {
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
- console.log('No commits to amend. Make an initial commit first.');
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
- console.log('No staged changes to amend.');
718
+ Progress.warning('No staged changes to amend');
561
719
  return;
562
720
  }
563
721
 
564
- await git.raw(['commit', '--amend', '--no-edit']);
565
- console.log('Amended last commit with staged changes.');
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('ls')
572
- .description('Short numbered git log (oldest → newest)')
573
- .action(async () => {
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 { all } = await safeLog();
577
- [...all].reverse().forEach((c, i) => console.log(`${i+1}. ${c.hash.slice(0,7)} ${c.message}`));
578
- } catch (e) { handleError('List error', e); }
579
- });
580
-
581
- program.command('largelist').alias('ll')
582
- .description('Full numbered git log (oldest → newest)')
583
- .action(async () => {
584
- await ensureRepo();
585
- try {
586
- const { all } = await safeLog();
587
- [...all].reverse().forEach((c, i) => {
588
- const date = new Date(c.date).toLocaleString();
589
- console.log(`${i+1}. ${c.hash.slice(0,7)} | ${date} | ${c.author_name} → ${c.message}`);
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
- } catch (e) { handleError('Largelist error', e); }
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 run: git reset ${mode} HEAD~1. Use --yes to confirm.`));
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
- console.log(`Reset (${mode}) to HEAD~1`);
652
- } catch (e) { handleError('Undo error', e); }
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);