gims 0.6.7 → 0.8.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/CHANGELOG.md +80 -0
- package/README.md +33 -13
- package/bin/gims.js +834 -103
- package/bin/lib/ai/providers.js +36 -34
- package/bin/lib/git/analyzer.js +118 -47
- package/bin/lib/utils/colors.js +15 -0
- package/bin/lib/utils/intelligence.js +421 -0
- package/bin/lib/utils/progress.js +70 -1
- package/package.json +3 -3
package/bin/gims.js
CHANGED
|
@@ -15,6 +15,7 @@ const { ConfigManager } = require('./lib/config/manager');
|
|
|
15
15
|
const { GitAnalyzer } = require('./lib/git/analyzer');
|
|
16
16
|
const { AIProviderManager } = require('./lib/ai/providers');
|
|
17
17
|
const { InteractiveCommands } = require('./lib/commands/interactive');
|
|
18
|
+
const { Intelligence } = require('./lib/utils/intelligence');
|
|
18
19
|
|
|
19
20
|
const program = new Command();
|
|
20
21
|
const git = simpleGit();
|
|
@@ -64,7 +65,7 @@ async function ensureRepo() {
|
|
|
64
65
|
function handleError(prefix, err) {
|
|
65
66
|
const msg = err && err.message ? err.message : String(err);
|
|
66
67
|
Progress.error(`${prefix}: ${msg}`);
|
|
67
|
-
|
|
68
|
+
|
|
68
69
|
// Provide helpful suggestions based on error type
|
|
69
70
|
if (msg.includes('not found') || msg.includes('does not exist')) {
|
|
70
71
|
console.log(`\nTip: Check if the file/branch exists with: ${color.cyan('g status')}`);
|
|
@@ -73,7 +74,7 @@ function handleError(prefix, err) {
|
|
|
73
74
|
} else if (msg.includes('merge') || msg.includes('conflict')) {
|
|
74
75
|
console.log(`\nTip: Resolve conflicts and try again`);
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
+
|
|
77
78
|
process.exit(1);
|
|
78
79
|
}
|
|
79
80
|
|
|
@@ -97,7 +98,7 @@ async function generateCommitMessage(rawDiff, options = {}) {
|
|
|
97
98
|
|
|
98
99
|
async function confirmCommit(message, isLocalHeuristic) {
|
|
99
100
|
if (!isLocalHeuristic) return true; // No confirmation needed for AI-generated messages
|
|
100
|
-
|
|
101
|
+
|
|
101
102
|
const readline = require('readline');
|
|
102
103
|
const rl = readline.createInterface({
|
|
103
104
|
input: process.stdin,
|
|
@@ -106,7 +107,7 @@ async function confirmCommit(message, isLocalHeuristic) {
|
|
|
106
107
|
|
|
107
108
|
console.log(color.yellow('\n⚠️ No AI provider configured - using local heuristics'));
|
|
108
109
|
console.log(`Suggested commit: "${message}"`);
|
|
109
|
-
|
|
110
|
+
|
|
110
111
|
return new Promise((resolve) => {
|
|
111
112
|
rl.question('Proceed with this commit? [Y/n]: ', (answer) => {
|
|
112
113
|
rl.close();
|
|
@@ -184,10 +185,10 @@ async function setupApiKey(provider) {
|
|
|
184
185
|
});
|
|
185
186
|
|
|
186
187
|
console.log(color.bold(`\n🔑 ${provider.toUpperCase()} API Key Setup\n`));
|
|
187
|
-
|
|
188
|
+
|
|
188
189
|
const envVars = {
|
|
189
190
|
'openai': 'OPENAI_API_KEY',
|
|
190
|
-
'gemini': 'GEMINI_API_KEY',
|
|
191
|
+
'gemini': 'GEMINI_API_KEY',
|
|
191
192
|
'groq': 'GROQ_API_KEY'
|
|
192
193
|
};
|
|
193
194
|
|
|
@@ -211,7 +212,7 @@ async function setupApiKey(provider) {
|
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
const apiKey = await question(`\nEnter your ${provider.toUpperCase()} API key: `);
|
|
214
|
-
|
|
215
|
+
|
|
215
216
|
if (!apiKey) {
|
|
216
217
|
console.log(color.yellow('No API key provided. Setup cancelled.'));
|
|
217
218
|
rl.close();
|
|
@@ -226,12 +227,12 @@ async function setupApiKey(provider) {
|
|
|
226
227
|
console.log(color.cyan(`export ${envVar}="${apiKey}"`));
|
|
227
228
|
console.log('\nOr add it to your shell profile (~/.bashrc, ~/.zshrc, etc.):');
|
|
228
229
|
console.log(color.cyan(`echo 'export ${envVar}="${apiKey}"' >> ~/.zshrc`));
|
|
229
|
-
|
|
230
|
+
|
|
230
231
|
// Set provider in config
|
|
231
232
|
const config = configManager.load();
|
|
232
233
|
config.provider = provider;
|
|
233
234
|
configManager.save(config);
|
|
234
|
-
|
|
235
|
+
|
|
235
236
|
console.log(`\n${color.green('✓')} Provider set to ${provider} in local config`);
|
|
236
237
|
console.log('\nRestart your terminal and try:');
|
|
237
238
|
console.log(` ${color.cyan('g sg')} - Get AI suggestions`);
|
|
@@ -245,7 +246,7 @@ program.command('status').alias('s')
|
|
|
245
246
|
try {
|
|
246
247
|
const enhancedStatus = await gitAnalyzer.getEnhancedStatus();
|
|
247
248
|
console.log(gitAnalyzer.formatStatusOutput(enhancedStatus));
|
|
248
|
-
|
|
249
|
+
|
|
249
250
|
// Show commit history summary
|
|
250
251
|
const history = await gitAnalyzer.analyzeCommitHistory(5);
|
|
251
252
|
if (history.totalCommits > 0) {
|
|
@@ -325,38 +326,41 @@ program.command('quick-help').alias('q')
|
|
|
325
326
|
.description('Show quick reference for main commands')
|
|
326
327
|
.action(() => {
|
|
327
328
|
console.log(color.bold('🚀 GIMS Quick Reference\n'));
|
|
328
|
-
|
|
329
|
-
console.log(color.bold('
|
|
330
|
-
console.log(` ${color.cyan('g s')}
|
|
331
|
-
console.log(` ${color.cyan('g
|
|
332
|
-
console.log(` ${color.cyan('g
|
|
333
|
-
console.log(` ${color.cyan('g
|
|
334
|
-
|
|
335
|
-
console.log(
|
|
336
|
-
console.log(` ${color.cyan('g
|
|
337
|
-
console.log(` ${color.cyan('g
|
|
338
|
-
console.log(` ${color.cyan('g
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
console.log(color.
|
|
342
|
-
console.log(` ${color.cyan('g
|
|
343
|
-
console.log(` ${color.cyan('g
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
console.log(color.
|
|
347
|
-
console.log(` ${color.cyan('g
|
|
348
|
-
console.log(` ${color.cyan('g
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
console.log(`
|
|
352
|
-
console.log(`
|
|
329
|
+
|
|
330
|
+
console.log(color.bold('Core Workflow:'));
|
|
331
|
+
console.log(` ${color.cyan('g s')} Status with AI insights`);
|
|
332
|
+
console.log(` ${color.cyan('g o')} AI commit + push`);
|
|
333
|
+
console.log(` ${color.cyan('g l')} AI commit locally`);
|
|
334
|
+
console.log(` ${color.cyan('g wip')} Quick WIP commit\n`);
|
|
335
|
+
|
|
336
|
+
console.log(color.bold('Sync & Fix:'));
|
|
337
|
+
console.log(` ${color.cyan('g sp')} Safe pull (stash → pull → pop)`);
|
|
338
|
+
console.log(` ${color.cyan('g fix')} Fix branch sync issues`);
|
|
339
|
+
console.log(` ${color.cyan('g main')} Switch to main + pull\n`);
|
|
340
|
+
|
|
341
|
+
console.log(color.bold('Stash:'));
|
|
342
|
+
console.log(` ${color.cyan('g ss')} Quick stash save`);
|
|
343
|
+
console.log(` ${color.cyan('g pop')} Pop latest stash`);
|
|
344
|
+
console.log(` ${color.cyan('g us')} Unstage all files\n`);
|
|
345
|
+
|
|
346
|
+
console.log(color.bold('Smart Commands:'));
|
|
347
|
+
console.log(` ${color.cyan('g r')} AI code review`);
|
|
348
|
+
console.log(` ${color.cyan('g t')} Today's commits`);
|
|
349
|
+
console.log(` ${color.cyan('g last')} Last commit details\n`);
|
|
350
|
+
|
|
351
|
+
console.log(color.bold('History:'));
|
|
352
|
+
console.log(` ${color.cyan('g ls')} Commit history`);
|
|
353
|
+
console.log(` ${color.cyan('g a')} Amend last commit`);
|
|
354
|
+
console.log(` ${color.cyan('g u')} Undo last commit\n`);
|
|
355
|
+
|
|
356
|
+
console.log(`Full help: ${color.cyan('g --help')}`);
|
|
353
357
|
});
|
|
354
358
|
|
|
355
359
|
program.command('init').alias('i')
|
|
356
360
|
.description('Initialize a new Git repository')
|
|
357
|
-
.action(async () => {
|
|
358
|
-
try {
|
|
359
|
-
await git.init();
|
|
361
|
+
.action(async () => {
|
|
362
|
+
try {
|
|
363
|
+
await git.init();
|
|
360
364
|
Progress.success('Initialized git repository');
|
|
361
365
|
console.log(`\nNext steps:`);
|
|
362
366
|
console.log(` ${color.cyan('g setup')} - Configure GIMS`);
|
|
@@ -400,17 +404,17 @@ program.command('suggest').alias('sg')
|
|
|
400
404
|
if (opts.progressIndicators) Progress.start('🤖 Generating multiple suggestions');
|
|
401
405
|
const suggestions = await aiProvider.generateMultipleSuggestions(rawDiff, opts, 3);
|
|
402
406
|
if (opts.progressIndicators) Progress.stop('');
|
|
403
|
-
|
|
407
|
+
|
|
404
408
|
console.log(color.bold('\n📝 Suggested commit messages:\n'));
|
|
405
409
|
suggestions.forEach((msg, i) => {
|
|
406
410
|
console.log(`${color.cyan((i + 1).toString())}. ${msg}`);
|
|
407
411
|
});
|
|
408
|
-
|
|
412
|
+
|
|
409
413
|
if (!opts.noClipboard && suggestions.length > 0) {
|
|
410
|
-
try {
|
|
411
|
-
clipboard.writeSync(suggestions[0]);
|
|
414
|
+
try {
|
|
415
|
+
clipboard.writeSync(suggestions[0]);
|
|
412
416
|
console.log(`\n${color.green('✓')} First suggestion copied to clipboard`);
|
|
413
|
-
} catch (_) {
|
|
417
|
+
} catch (_) {
|
|
414
418
|
console.log(`\n${color.yellow('⚠')} Clipboard copy failed`);
|
|
415
419
|
}
|
|
416
420
|
}
|
|
@@ -418,10 +422,10 @@ program.command('suggest').alias('sg')
|
|
|
418
422
|
if (opts.progressIndicators) Progress.start('🤖 Analyzing changes');
|
|
419
423
|
const result = await generateCommitMessage(rawDiff, opts);
|
|
420
424
|
if (opts.progressIndicators) Progress.stop('');
|
|
421
|
-
|
|
425
|
+
|
|
422
426
|
const msg = result.message || result; // Handle both old and new format
|
|
423
427
|
const usedLocal = result.usedLocal || false;
|
|
424
|
-
|
|
428
|
+
|
|
425
429
|
// Warn if using local heuristics
|
|
426
430
|
if (usedLocal) {
|
|
427
431
|
console.log(color.yellow('⚠️ No AI provider configured - using local heuristics'));
|
|
@@ -434,10 +438,10 @@ program.command('suggest').alias('sg')
|
|
|
434
438
|
}
|
|
435
439
|
|
|
436
440
|
if (!opts.noClipboard) {
|
|
437
|
-
try {
|
|
438
|
-
clipboard.writeSync(msg);
|
|
441
|
+
try {
|
|
442
|
+
clipboard.writeSync(msg);
|
|
439
443
|
Progress.success(`"${msg}" (copied to clipboard)`);
|
|
440
|
-
} catch (_) {
|
|
444
|
+
} catch (_) {
|
|
441
445
|
console.log(`Suggested: "${msg}" ${color.yellow('(clipboard copy failed)')}`);
|
|
442
446
|
}
|
|
443
447
|
} else {
|
|
@@ -471,16 +475,16 @@ program.command('local').alias('l')
|
|
|
471
475
|
Progress.info('No staged changes found; staging all changes...');
|
|
472
476
|
await git.add('.');
|
|
473
477
|
rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
|
474
|
-
if (!rawDiff.trim()) {
|
|
475
|
-
Progress.warning('No changes to commit');
|
|
476
|
-
return;
|
|
478
|
+
if (!rawDiff.trim()) {
|
|
479
|
+
Progress.warning('No changes to commit');
|
|
480
|
+
return;
|
|
477
481
|
}
|
|
478
482
|
}
|
|
479
483
|
|
|
480
484
|
if (opts.progressIndicators) Progress.start('🤖 Generating commit message');
|
|
481
485
|
const result = await generateCommitMessage(rawDiff, opts);
|
|
482
486
|
if (opts.progressIndicators) Progress.stop('');
|
|
483
|
-
|
|
487
|
+
|
|
484
488
|
const msg = result.message || result; // Handle both old and new format
|
|
485
489
|
const usedLocal = result.usedLocal || false;
|
|
486
490
|
|
|
@@ -533,16 +537,16 @@ program.command('online').alias('o')
|
|
|
533
537
|
Progress.info('No staged changes found; staging all changes...');
|
|
534
538
|
await git.add('.');
|
|
535
539
|
rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
|
536
|
-
if (!rawDiff.trim()) {
|
|
537
|
-
Progress.warning('No changes to commit');
|
|
538
|
-
return;
|
|
540
|
+
if (!rawDiff.trim()) {
|
|
541
|
+
Progress.warning('No changes to commit');
|
|
542
|
+
return;
|
|
539
543
|
}
|
|
540
544
|
}
|
|
541
545
|
|
|
542
546
|
if (opts.progressIndicators) Progress.start('🤖 Generating commit message');
|
|
543
547
|
const result = await generateCommitMessage(rawDiff, opts);
|
|
544
548
|
if (opts.progressIndicators) Progress.stop('');
|
|
545
|
-
|
|
549
|
+
|
|
546
550
|
const msg = result.message || result; // Handle both old and new format
|
|
547
551
|
const usedLocal = result.usedLocal || false;
|
|
548
552
|
|
|
@@ -640,9 +644,9 @@ program.command('pull')
|
|
|
640
644
|
.description('Pull latest changes')
|
|
641
645
|
.action(async () => {
|
|
642
646
|
await ensureRepo();
|
|
643
|
-
try {
|
|
647
|
+
try {
|
|
644
648
|
Progress.info('Pulling latest changes...');
|
|
645
|
-
await git.pull();
|
|
649
|
+
await git.pull();
|
|
646
650
|
Progress.success('Pulled latest changes');
|
|
647
651
|
}
|
|
648
652
|
catch (e) { handleError('Pull error', e); }
|
|
@@ -667,27 +671,27 @@ program.command('sync')
|
|
|
667
671
|
await ensureRepo();
|
|
668
672
|
try {
|
|
669
673
|
const status = await git.status();
|
|
670
|
-
|
|
674
|
+
|
|
671
675
|
if (status.files.length > 0) {
|
|
672
676
|
Progress.warning('You have uncommitted changes. Commit or stash them first.');
|
|
673
677
|
return;
|
|
674
678
|
}
|
|
675
|
-
|
|
679
|
+
|
|
676
680
|
Progress.info('Fetching latest changes...');
|
|
677
681
|
await git.fetch();
|
|
678
|
-
|
|
682
|
+
|
|
679
683
|
const currentBranch = (await git.raw(['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
|
|
680
684
|
const remoteBranch = `origin/${currentBranch}`;
|
|
681
|
-
|
|
685
|
+
|
|
682
686
|
try {
|
|
683
687
|
const behind = await git.raw(['rev-list', '--count', `${currentBranch}..${remoteBranch}`]);
|
|
684
688
|
const ahead = await git.raw(['rev-list', '--count', `${remoteBranch}..${currentBranch}`]);
|
|
685
|
-
|
|
689
|
+
|
|
686
690
|
if (parseInt(behind.trim()) === 0) {
|
|
687
691
|
Progress.success('Already up to date');
|
|
688
692
|
return;
|
|
689
693
|
}
|
|
690
|
-
|
|
694
|
+
|
|
691
695
|
if (parseInt(ahead.trim()) > 0) {
|
|
692
696
|
Progress.info(`Branch is ${ahead.trim()} commits ahead and ${behind.trim()} commits behind`);
|
|
693
697
|
if (cmdOptions.rebase) {
|
|
@@ -713,8 +717,8 @@ program.command('sync')
|
|
|
713
717
|
throw error;
|
|
714
718
|
}
|
|
715
719
|
}
|
|
716
|
-
} catch (e) {
|
|
717
|
-
handleError('Sync error', e);
|
|
720
|
+
} catch (e) {
|
|
721
|
+
handleError('Sync error', e);
|
|
718
722
|
}
|
|
719
723
|
});
|
|
720
724
|
|
|
@@ -732,7 +736,7 @@ program.command('stash')
|
|
|
732
736
|
Progress.info('No stashes found');
|
|
733
737
|
return;
|
|
734
738
|
}
|
|
735
|
-
|
|
739
|
+
|
|
736
740
|
console.log(color.bold('Stashes:'));
|
|
737
741
|
stashes.all.forEach((stash, i) => {
|
|
738
742
|
console.log(`${color.cyan((i).toString())}. ${stash.message}`);
|
|
@@ -751,15 +755,15 @@ program.command('stash')
|
|
|
751
755
|
Progress.warning('No changes to stash');
|
|
752
756
|
return;
|
|
753
757
|
}
|
|
754
|
-
|
|
758
|
+
|
|
755
759
|
Progress.start('🤖 Generating stash description');
|
|
756
760
|
const diff = await git.diff();
|
|
757
|
-
const description = await aiProvider.generateCommitMessage(diff, {
|
|
758
|
-
conventional: false,
|
|
759
|
-
body: false
|
|
761
|
+
const description = await aiProvider.generateCommitMessage(diff, {
|
|
762
|
+
conventional: false,
|
|
763
|
+
body: false
|
|
760
764
|
});
|
|
761
765
|
Progress.stop('');
|
|
762
|
-
|
|
766
|
+
|
|
763
767
|
await git.stash(['push', '-m', `WIP: ${description}`]);
|
|
764
768
|
Progress.success(`Stashed changes: "${description}"`);
|
|
765
769
|
}
|
|
@@ -795,10 +799,10 @@ program.command('amend').alias('a')
|
|
|
795
799
|
Progress.start('🤖 Generating updated commit message');
|
|
796
800
|
const result = await generateCommitMessage(rawDiff, opts);
|
|
797
801
|
Progress.stop('');
|
|
798
|
-
|
|
802
|
+
|
|
799
803
|
const newMessage = result.message || result; // Handle both old and new format
|
|
800
804
|
const usedLocal = result.usedLocal || false;
|
|
801
|
-
|
|
805
|
+
|
|
802
806
|
// Ask for confirmation if using local heuristics (unless --yes flag is set)
|
|
803
807
|
if (usedLocal && !opts.yes) {
|
|
804
808
|
const confirmed = await confirmCommit(newMessage, true);
|
|
@@ -807,7 +811,7 @@ program.command('amend').alias('a')
|
|
|
807
811
|
return;
|
|
808
812
|
}
|
|
809
813
|
}
|
|
810
|
-
|
|
814
|
+
|
|
811
815
|
await git.raw(['commit', '--amend', '-m', newMessage]);
|
|
812
816
|
Progress.success(`Amended commit: "${newMessage}"`);
|
|
813
817
|
} else {
|
|
@@ -829,21 +833,21 @@ program.command('list').alias('ls')
|
|
|
829
833
|
const limit = parseInt(cmdOptions.limit) || 20;
|
|
830
834
|
const log = await git.log({ maxCount: limit });
|
|
831
835
|
const commits = [...log.all].reverse();
|
|
832
|
-
|
|
836
|
+
|
|
833
837
|
if (commits.length === 0) {
|
|
834
838
|
Progress.info('No commits found');
|
|
835
839
|
return;
|
|
836
840
|
}
|
|
837
|
-
|
|
841
|
+
|
|
838
842
|
commits.forEach((c, i) => {
|
|
839
|
-
console.log(`${color.cyan((i+1).toString())}. ${color.yellow(c.hash.slice(0,7))} ${c.message}`);
|
|
843
|
+
console.log(`${color.cyan((i + 1).toString())}. ${color.yellow(c.hash.slice(0, 7))} ${c.message}`);
|
|
840
844
|
});
|
|
841
|
-
|
|
845
|
+
|
|
842
846
|
if (log.all.length >= limit) {
|
|
843
847
|
console.log(color.dim(`\n... showing last ${limit} commits (use --limit to see more)`));
|
|
844
848
|
}
|
|
845
|
-
} catch (e) {
|
|
846
|
-
handleError('List error', e);
|
|
849
|
+
} catch (e) {
|
|
850
|
+
handleError('List error', e);
|
|
847
851
|
}
|
|
848
852
|
});
|
|
849
853
|
|
|
@@ -856,22 +860,22 @@ program.command('largelist').alias('ll')
|
|
|
856
860
|
const limit = parseInt(cmdOptions.limit) || 20;
|
|
857
861
|
const log = await git.log({ maxCount: limit });
|
|
858
862
|
const commits = [...log.all].reverse();
|
|
859
|
-
|
|
863
|
+
|
|
860
864
|
if (commits.length === 0) {
|
|
861
865
|
Progress.info('No commits found');
|
|
862
866
|
return;
|
|
863
867
|
}
|
|
864
|
-
|
|
868
|
+
|
|
865
869
|
commits.forEach((c, i) => {
|
|
866
870
|
const date = new Date(c.date).toLocaleString();
|
|
867
|
-
console.log(`${color.cyan((i+1).toString())}. ${color.yellow(c.hash.slice(0,7))} | ${color.dim(date)} | ${color.green(c.author_name)} → ${c.message}`);
|
|
871
|
+
console.log(`${color.cyan((i + 1).toString())}. ${color.yellow(c.hash.slice(0, 7))} | ${color.dim(date)} | ${color.green(c.author_name)} → ${c.message}`);
|
|
868
872
|
});
|
|
869
|
-
|
|
873
|
+
|
|
870
874
|
if (log.all.length >= limit) {
|
|
871
875
|
console.log(color.dim(`\n... showing last ${limit} commits (use --limit to see more)`));
|
|
872
876
|
}
|
|
873
|
-
} catch (e) {
|
|
874
|
-
handleError('Largelist error', e);
|
|
877
|
+
} catch (e) {
|
|
878
|
+
handleError('Largelist error', e);
|
|
875
879
|
}
|
|
876
880
|
});
|
|
877
881
|
|
|
@@ -884,21 +888,21 @@ program.command('history').alias('h')
|
|
|
884
888
|
const limit = parseInt(cmdOptions.limit) || 20;
|
|
885
889
|
const log = await git.log({ maxCount: limit });
|
|
886
890
|
const commits = [...log.all].reverse();
|
|
887
|
-
|
|
891
|
+
|
|
888
892
|
if (commits.length === 0) {
|
|
889
893
|
Progress.info('No commits found');
|
|
890
894
|
return;
|
|
891
895
|
}
|
|
892
|
-
|
|
896
|
+
|
|
893
897
|
commits.forEach((c, i) => {
|
|
894
|
-
console.log(`${color.cyan((i+1).toString())}. ${color.yellow(c.hash.slice(0,7))} ${c.message}`);
|
|
898
|
+
console.log(`${color.cyan((i + 1).toString())}. ${color.yellow(c.hash.slice(0, 7))} ${c.message}`);
|
|
895
899
|
});
|
|
896
|
-
|
|
900
|
+
|
|
897
901
|
if (log.all.length >= limit) {
|
|
898
902
|
console.log(color.dim(`\n... showing last ${limit} commits (use --limit to see more)`));
|
|
899
903
|
}
|
|
900
|
-
} catch (e) {
|
|
901
|
-
handleError('History error', e);
|
|
904
|
+
} catch (e) {
|
|
905
|
+
handleError('History error', e);
|
|
902
906
|
}
|
|
903
907
|
});
|
|
904
908
|
|
|
@@ -906,18 +910,18 @@ program.command('branch <c> [name]').alias('b')
|
|
|
906
910
|
.description('Branch from commit/index')
|
|
907
911
|
.action(async (c, name) => {
|
|
908
912
|
await ensureRepo();
|
|
909
|
-
try { const sha = await resolveCommit(c); const br = name || `branch-${sha.slice(0,7)}`; await git.checkout(['-b', br, sha]); console.log(`Switched to branch ${br} at ${sha}`); }
|
|
913
|
+
try { const sha = await resolveCommit(c); const br = name || `branch-${sha.slice(0, 7)}`; await git.checkout(['-b', br, sha]); console.log(`Switched to branch ${br} at ${sha}`); }
|
|
910
914
|
catch (e) { handleError('Branch error', e); }
|
|
911
915
|
});
|
|
912
916
|
|
|
913
|
-
program.command('reset <c>').alias('
|
|
917
|
+
program.command('reset <c>').alias('rs')
|
|
914
918
|
.description('Reset branch to commit/index')
|
|
915
|
-
.option('--hard','hard reset')
|
|
919
|
+
.option('--hard', 'hard reset')
|
|
916
920
|
.action(async (c, optsCmd) => {
|
|
917
921
|
await ensureRepo();
|
|
918
922
|
try {
|
|
919
923
|
const sha = await resolveCommit(c);
|
|
920
|
-
const mode = optsCmd.hard? '--hard':'--soft';
|
|
924
|
+
const mode = optsCmd.hard ? '--hard' : '--soft';
|
|
921
925
|
const opts = getOpts();
|
|
922
926
|
if (!opts.yes) {
|
|
923
927
|
console.log(color.yellow(`About to run: git reset ${mode} ${sha}. Use --yes to confirm.`));
|
|
@@ -957,11 +961,11 @@ program.command('undo').alias('u')
|
|
|
957
961
|
Progress.warning('No commits to undo');
|
|
958
962
|
return;
|
|
959
963
|
}
|
|
960
|
-
|
|
964
|
+
|
|
961
965
|
const lastCommit = all[0];
|
|
962
966
|
const mode = cmd.hard ? '--hard' : '--soft';
|
|
963
967
|
const opts = getOpts();
|
|
964
|
-
|
|
968
|
+
|
|
965
969
|
if (!opts.yes) {
|
|
966
970
|
console.log(color.yellow(`About to undo: "${lastCommit.message}"`));
|
|
967
971
|
console.log(color.yellow(`This will run: git reset ${mode} HEAD~1`));
|
|
@@ -971,16 +975,743 @@ program.command('undo').alias('u')
|
|
|
971
975
|
console.log('Use --yes to confirm.');
|
|
972
976
|
process.exit(1);
|
|
973
977
|
}
|
|
974
|
-
|
|
978
|
+
|
|
975
979
|
await git.raw(['reset', mode, 'HEAD~1']);
|
|
976
980
|
Progress.success(`Undone commit: "${lastCommit.message}" (${mode} reset)`);
|
|
977
|
-
|
|
981
|
+
|
|
978
982
|
if (mode === '--soft') {
|
|
979
983
|
Progress.info('Changes are now staged. Use "g status" to see them.');
|
|
980
984
|
}
|
|
981
|
-
} catch (e) {
|
|
982
|
-
handleError('Undo error', e);
|
|
985
|
+
} catch (e) {
|
|
986
|
+
handleError('Undo error', e);
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
// ===== NEW INTELLIGENT COMMANDS =====
|
|
991
|
+
|
|
992
|
+
program.command('wip')
|
|
993
|
+
.description('Quick work-in-progress commit')
|
|
994
|
+
.action(async () => {
|
|
995
|
+
await ensureRepo();
|
|
996
|
+
try {
|
|
997
|
+
const status = await git.status();
|
|
998
|
+
if (status.files.length === 0) {
|
|
999
|
+
Progress.warning('No changes to commit');
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
Progress.info('Staging all changes...');
|
|
1004
|
+
await git.add('.');
|
|
1005
|
+
|
|
1006
|
+
const fileCount = status.files.length;
|
|
1007
|
+
const message = `WIP: ${fileCount} file${fileCount > 1 ? 's' : ''} changed`;
|
|
1008
|
+
|
|
1009
|
+
await git.commit(message);
|
|
1010
|
+
Progress.success(`Committed: "${message}"`);
|
|
1011
|
+
Progress.tip('Use `g a` to amend this commit when ready, or `g undo` to undo');
|
|
1012
|
+
} catch (e) {
|
|
1013
|
+
handleError('WIP error', e);
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
program.command('today').alias('t')
|
|
1018
|
+
.description('Show commits made today')
|
|
1019
|
+
.action(async () => {
|
|
1020
|
+
await ensureRepo();
|
|
1021
|
+
try {
|
|
1022
|
+
const commits = await gitAnalyzer.getTodayCommits();
|
|
1023
|
+
|
|
1024
|
+
if (commits.length === 0) {
|
|
1025
|
+
console.log(color.dim('No commits today yet.'));
|
|
1026
|
+
Progress.tip('Start your day with `g o` to commit and push!');
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
console.log(color.bold(`📅 Today's Commits (${commits.length})\n`));
|
|
1031
|
+
commits.forEach((commit, i) => {
|
|
1032
|
+
console.log(gitAnalyzer.formatCommit(commit, i));
|
|
1033
|
+
});
|
|
1034
|
+
} catch (e) {
|
|
1035
|
+
handleError('Today error', e);
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
program.command('stats')
|
|
1040
|
+
.description('Your personal commit statistics')
|
|
1041
|
+
.option('--days <n>', 'Number of days to analyze', '30')
|
|
1042
|
+
.action(async (cmdOptions) => {
|
|
1043
|
+
await ensureRepo();
|
|
1044
|
+
try {
|
|
1045
|
+
const days = parseInt(cmdOptions.days) || 30;
|
|
1046
|
+
const intelligence = new Intelligence(git);
|
|
1047
|
+
|
|
1048
|
+
Progress.start('📊 Analyzing your commit history');
|
|
1049
|
+
const stats = await intelligence.getCommitStats(days);
|
|
1050
|
+
const patterns = await intelligence.analyzeCommitPatterns();
|
|
1051
|
+
Progress.stop('');
|
|
1052
|
+
|
|
1053
|
+
if (!stats.hasData) {
|
|
1054
|
+
Progress.info('Not enough commit history to analyze');
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
console.log(color.bold(`\n📊 Your Git Stats (last ${days} days)\n`));
|
|
1059
|
+
|
|
1060
|
+
// Overview
|
|
1061
|
+
console.log(color.cyan('Overview:'));
|
|
1062
|
+
console.log(` Total commits: ${color.bold(stats.totalCommits.toString())}`);
|
|
1063
|
+
console.log(` Days active: ${stats.daysActive}`);
|
|
1064
|
+
console.log(` Average: ${stats.avgPerDay} commits/day`);
|
|
1065
|
+
console.log(` Current streak: ${color.green(stats.currentStreak + ' days')}`);
|
|
1066
|
+
if (stats.longestStreak > stats.currentStreak) {
|
|
1067
|
+
console.log(` Longest streak: ${stats.longestStreak} days`);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Commit types breakdown
|
|
1071
|
+
if (patterns.usesConventional) {
|
|
1072
|
+
console.log(`\n${color.cyan('Commit Types:')}`);
|
|
1073
|
+
const types = stats.typeBreakdown;
|
|
1074
|
+
const total = Object.values(types).reduce((a, b) => a + b, 0);
|
|
1075
|
+
Object.entries(types).forEach(([type, count]) => {
|
|
1076
|
+
if (count > 0) {
|
|
1077
|
+
const pct = Math.round(count / total * 100);
|
|
1078
|
+
const bar = '█'.repeat(Math.ceil(pct / 5)) + '░'.repeat(20 - Math.ceil(pct / 5));
|
|
1079
|
+
console.log(` ${type.padEnd(8)} ${bar} ${pct}%`);
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Style insights
|
|
1085
|
+
if (patterns.hasHistory) {
|
|
1086
|
+
console.log(`\n${color.cyan('Your Style:')}`);
|
|
1087
|
+
console.log(` Conventional commits: ${patterns.conventionalRatio}%`);
|
|
1088
|
+
console.log(` Message style: ${patterns.style}`);
|
|
1089
|
+
console.log(` Avg message length: ${patterns.avgMessageLength} chars`);
|
|
1090
|
+
if (patterns.topScopes.length > 0) {
|
|
1091
|
+
console.log(` Common scopes: ${patterns.topScopes.join(', ')}`);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
Progress.showRandomTip();
|
|
1096
|
+
} catch (e) {
|
|
1097
|
+
handleError('Stats error', e);
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
program.command('review').alias('r')
|
|
1102
|
+
.description('AI code review before committing')
|
|
1103
|
+
.action(async () => {
|
|
1104
|
+
await ensureRepo();
|
|
1105
|
+
const opts = getOpts();
|
|
1106
|
+
try {
|
|
1107
|
+
let diff = await git.diff(['--cached', '--no-ext-diff']);
|
|
1108
|
+
|
|
1109
|
+
if (!diff.trim()) {
|
|
1110
|
+
// Try unstaged changes
|
|
1111
|
+
diff = await git.diff(['--no-ext-diff']);
|
|
1112
|
+
if (!diff.trim()) {
|
|
1113
|
+
Progress.warning('No changes to review');
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
console.log(color.dim('(Reviewing unstaged changes)\n'));
|
|
1117
|
+
} else {
|
|
1118
|
+
console.log(color.dim('(Reviewing staged changes)\n'));
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const intelligence = new Intelligence(git);
|
|
1122
|
+
|
|
1123
|
+
// Analyze complexity
|
|
1124
|
+
const complexity = await gitAnalyzer.getChangeComplexity(diff);
|
|
1125
|
+
console.log(color.bold('📋 Change Summary'));
|
|
1126
|
+
console.log(` Complexity: ${complexity.emoji} ${complexity.complexity}`);
|
|
1127
|
+
console.log(` Files: ${complexity.files}`);
|
|
1128
|
+
console.log(` Changes: ${color.green('+' + complexity.additions)} ${color.red('-' + complexity.deletions)}`);
|
|
1129
|
+
|
|
1130
|
+
// Detect semantic changes
|
|
1131
|
+
const semantic = await intelligence.detectSemanticChanges(diff);
|
|
1132
|
+
if (semantic.labels.length > 0) {
|
|
1133
|
+
console.log(`\n${color.bold('🔍 Detected Patterns')}`);
|
|
1134
|
+
semantic.labels.forEach(label => {
|
|
1135
|
+
console.log(` ${label}`);
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Get AI suggestions
|
|
1140
|
+
if (opts.progressIndicators) Progress.start('🤖 Generating AI review');
|
|
1141
|
+
const message = await aiProvider.generateCommitMessage(diff, { ...opts, body: true });
|
|
1142
|
+
if (opts.progressIndicators) Progress.stop('');
|
|
1143
|
+
|
|
1144
|
+
console.log(`\n${color.bold('💬 Suggested Commit Message')}`);
|
|
1145
|
+
console.log(` ${color.green(message.message || message)}`);
|
|
1146
|
+
|
|
1147
|
+
// Actionable next steps
|
|
1148
|
+
console.log(`\n${color.bold('📌 Next Steps')}`);
|
|
1149
|
+
console.log(` ${color.cyan('g o')} Commit and push with AI message`);
|
|
1150
|
+
console.log(` ${color.cyan('g l')} Commit locally with AI message`);
|
|
1151
|
+
console.log(` ${color.cyan('g int')} Interactive commit with options`);
|
|
1152
|
+
|
|
1153
|
+
} catch (e) {
|
|
1154
|
+
handleError('Review error', e);
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
program.command('split')
|
|
1159
|
+
.description('Suggest how to split a large changeset')
|
|
1160
|
+
.action(async () => {
|
|
1161
|
+
await ensureRepo();
|
|
1162
|
+
try {
|
|
1163
|
+
const status = await git.status();
|
|
1164
|
+
|
|
1165
|
+
if (status.files.length === 0) {
|
|
1166
|
+
Progress.info('No changes to split');
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (status.files.length < 5) {
|
|
1171
|
+
Progress.info('Changeset is small enough - no need to split');
|
|
1172
|
+
console.log(`\nYou have ${status.files.length} file${status.files.length > 1 ? 's' : ''} changed.`);
|
|
1173
|
+
console.log(`Use ${color.cyan('g o')} to commit them all at once.`);
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const intelligence = new Intelligence(git);
|
|
1178
|
+
const suggestions = await intelligence.suggestCommitSplit(status);
|
|
1179
|
+
|
|
1180
|
+
if (!suggestions) {
|
|
1181
|
+
Progress.info('All changes look related - commit them together');
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
console.log(color.bold(`\n📦 Suggested Commit Split\n`));
|
|
1186
|
+
console.log(color.dim(`Your ${status.files.length} files could be split into ${suggestions.length} commits:\n`));
|
|
1187
|
+
|
|
1188
|
+
suggestions.forEach((group, i) => {
|
|
1189
|
+
console.log(`${color.cyan((i + 1).toString())}. ${color.bold(group.message)}`);
|
|
1190
|
+
group.files.slice(0, 5).forEach(file => {
|
|
1191
|
+
const emoji = Intelligence.getFileEmoji(file);
|
|
1192
|
+
console.log(` ${emoji} ${file}`);
|
|
1193
|
+
});
|
|
1194
|
+
if (group.files.length > 5) {
|
|
1195
|
+
console.log(color.dim(` ... and ${group.files.length - 5} more`));
|
|
1196
|
+
}
|
|
1197
|
+
console.log();
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
console.log(color.bold('💡 How to split:'));
|
|
1201
|
+
console.log(` 1. Stage specific files: ${color.cyan('git add <file>')}`);
|
|
1202
|
+
console.log(` 2. Commit them: ${color.cyan('g l')} or ${color.cyan('g o')}`);
|
|
1203
|
+
console.log(` 3. Repeat for remaining files`);
|
|
1204
|
+
|
|
1205
|
+
} catch (e) {
|
|
1206
|
+
handleError('Split error', e);
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
// ===== WORKFLOW SHORTHAND COMMANDS =====
|
|
1211
|
+
|
|
1212
|
+
program.command('safe-pull').alias('sp')
|
|
1213
|
+
.description('Safe pull: stash → pull → stash pop')
|
|
1214
|
+
.action(async () => {
|
|
1215
|
+
await ensureRepo();
|
|
1216
|
+
try {
|
|
1217
|
+
const status = await git.status();
|
|
1218
|
+
const hasChanges = status.files.length > 0;
|
|
1219
|
+
|
|
1220
|
+
if (hasChanges) {
|
|
1221
|
+
Progress.info('Stashing changes...');
|
|
1222
|
+
await git.stash(['push', '-m', 'GIMS: auto-stash before pull']);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
Progress.info('Pulling latest changes...');
|
|
1226
|
+
await git.pull();
|
|
1227
|
+
|
|
1228
|
+
if (hasChanges) {
|
|
1229
|
+
Progress.info('Restoring stashed changes...');
|
|
1230
|
+
await git.stash(['pop']);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
Progress.success('Safe pull complete');
|
|
1234
|
+
} catch (e) {
|
|
1235
|
+
handleError('Safe pull error', e);
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
program.command('main')
|
|
1240
|
+
.description('Switch to main/master and pull latest')
|
|
1241
|
+
.action(async () => {
|
|
1242
|
+
await ensureRepo();
|
|
1243
|
+
try {
|
|
1244
|
+
// Detect main branch name
|
|
1245
|
+
let mainBranch = 'main';
|
|
1246
|
+
try {
|
|
1247
|
+
await git.raw(['rev-parse', '--verify', 'main']);
|
|
1248
|
+
} catch {
|
|
1249
|
+
try {
|
|
1250
|
+
await git.raw(['rev-parse', '--verify', 'master']);
|
|
1251
|
+
mainBranch = 'master';
|
|
1252
|
+
} catch {
|
|
1253
|
+
Progress.error('No main or master branch found');
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const status = await git.status();
|
|
1259
|
+
if (status.files.length > 0) {
|
|
1260
|
+
Progress.warning('You have uncommitted changes. Commit or stash them first.');
|
|
1261
|
+
console.log(`Tip: Use ${color.cyan('g sp')} to safe-pull with auto-stash`);
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
Progress.info(`Switching to ${mainBranch}...`);
|
|
1266
|
+
await git.checkout(mainBranch);
|
|
1267
|
+
|
|
1268
|
+
Progress.info('Pulling latest...');
|
|
1269
|
+
await git.pull();
|
|
1270
|
+
|
|
1271
|
+
Progress.success(`On ${mainBranch} with latest changes`);
|
|
1272
|
+
} catch (e) {
|
|
1273
|
+
handleError('Main error', e);
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
program.command('unstage').alias('us')
|
|
1278
|
+
.description('Unstage all staged files')
|
|
1279
|
+
.action(async () => {
|
|
1280
|
+
await ensureRepo();
|
|
1281
|
+
try {
|
|
1282
|
+
const status = await git.status();
|
|
1283
|
+
if (status.staged.length === 0) {
|
|
1284
|
+
Progress.info('Nothing is staged');
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
await git.reset([]);
|
|
1289
|
+
Progress.success(`Unstaged ${status.staged.length} file${status.staged.length > 1 ? 's' : ''}`);
|
|
1290
|
+
} catch (e) {
|
|
1291
|
+
handleError('Unstage error', e);
|
|
1292
|
+
}
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
program.command('discard').alias('x')
|
|
1296
|
+
.description('Discard all changes (with confirmation)')
|
|
1297
|
+
.action(async () => {
|
|
1298
|
+
await ensureRepo();
|
|
1299
|
+
const opts = getOpts();
|
|
1300
|
+
try {
|
|
1301
|
+
const status = await git.status();
|
|
1302
|
+
if (status.files.length === 0) {
|
|
1303
|
+
Progress.info('No changes to discard');
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
if (!opts.yes) {
|
|
1308
|
+
console.log(color.yellow(`⚠️ About to discard ALL changes in ${status.files.length} file(s)`));
|
|
1309
|
+
console.log(color.red('This cannot be undone!'));
|
|
1310
|
+
console.log('Use --yes to confirm.');
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Discard tracked file changes
|
|
1315
|
+
await git.checkout(['--', '.']);
|
|
1316
|
+
|
|
1317
|
+
// Remove untracked files
|
|
1318
|
+
if (status.not_added.length > 0) {
|
|
1319
|
+
await git.clean('fd');
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
Progress.success('All changes discarded');
|
|
1323
|
+
} catch (e) {
|
|
1324
|
+
handleError('Discard error', e);
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
program.command('stash-save').alias('ss')
|
|
1329
|
+
.description('Quick stash: stage all and stash')
|
|
1330
|
+
.action(async () => {
|
|
1331
|
+
await ensureRepo();
|
|
1332
|
+
try {
|
|
1333
|
+
const status = await git.status();
|
|
1334
|
+
if (status.files.length === 0) {
|
|
1335
|
+
Progress.info('No changes to stash');
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
await git.add('.');
|
|
1340
|
+
|
|
1341
|
+
// Generate a descriptive stash name
|
|
1342
|
+
const fileCount = status.files.length;
|
|
1343
|
+
const timestamp = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
|
1344
|
+
const message = `WIP: ${fileCount} file${fileCount > 1 ? 's' : ''} at ${timestamp}`;
|
|
1345
|
+
|
|
1346
|
+
await git.stash(['push', '-m', message]);
|
|
1347
|
+
Progress.success(`Stashed: "${message}"`);
|
|
1348
|
+
} catch (e) {
|
|
1349
|
+
handleError('Stash save error', e);
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
program.command('stash-pop').alias('pop')
|
|
1354
|
+
.description('Pop the latest stash')
|
|
1355
|
+
.action(async () => {
|
|
1356
|
+
await ensureRepo();
|
|
1357
|
+
try {
|
|
1358
|
+
const stashList = await git.stashList();
|
|
1359
|
+
if (stashList.all.length === 0) {
|
|
1360
|
+
Progress.info('No stashes to pop');
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const latestStash = stashList.all[0];
|
|
1365
|
+
await git.stash(['pop']);
|
|
1366
|
+
Progress.success(`Popped: "${latestStash.message}"`);
|
|
1367
|
+
} catch (e) {
|
|
1368
|
+
handleError('Stash pop error', e);
|
|
1369
|
+
}
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
program.command('delete-branch').alias('del')
|
|
1373
|
+
.description('Delete branch locally and remotely')
|
|
1374
|
+
.argument('<branch>', 'Branch name to delete')
|
|
1375
|
+
.action(async (branch) => {
|
|
1376
|
+
await ensureRepo();
|
|
1377
|
+
const opts = getOpts();
|
|
1378
|
+
try {
|
|
1379
|
+
const current = (await git.branch()).current;
|
|
1380
|
+
if (branch === current) {
|
|
1381
|
+
Progress.error(`Cannot delete current branch. Switch to another branch first.`);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
if (branch === 'main' || branch === 'master') {
|
|
1386
|
+
Progress.error('Cannot delete main/master branch');
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
if (!opts.yes) {
|
|
1391
|
+
console.log(color.yellow(`⚠️ About to delete branch: ${branch}`));
|
|
1392
|
+
console.log('This will delete both local and remote copies.');
|
|
1393
|
+
console.log('Use --yes to confirm.');
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// Delete local
|
|
1398
|
+
Progress.info('Deleting local branch...');
|
|
1399
|
+
try {
|
|
1400
|
+
await git.branch(['-D', branch]);
|
|
1401
|
+
} catch (e) {
|
|
1402
|
+
Progress.warning(`Local branch not found or already deleted`);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Delete remote
|
|
1406
|
+
Progress.info('Deleting remote branch...');
|
|
1407
|
+
try {
|
|
1408
|
+
await git.push(['origin', '--delete', branch]);
|
|
1409
|
+
} catch (e) {
|
|
1410
|
+
Progress.warning(`Remote branch not found or already deleted`);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
Progress.success(`Deleted branch: ${branch}`);
|
|
1414
|
+
} catch (e) {
|
|
1415
|
+
handleError('Delete branch error', e);
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
program.command('cleanup').alias('clean')
|
|
1420
|
+
.description('Remove local branches that no longer exist on remote')
|
|
1421
|
+
.action(async () => {
|
|
1422
|
+
await ensureRepo();
|
|
1423
|
+
const opts = getOpts();
|
|
1424
|
+
try {
|
|
1425
|
+
Progress.info('Fetching and pruning...');
|
|
1426
|
+
await git.fetch(['--prune']);
|
|
1427
|
+
|
|
1428
|
+
// Find gone branches
|
|
1429
|
+
const branchOutput = await git.raw(['branch', '-vv']);
|
|
1430
|
+
const goneBranches = branchOutput
|
|
1431
|
+
.split('\n')
|
|
1432
|
+
.filter(line => line.includes(': gone]'))
|
|
1433
|
+
.map(line => line.trim().split(/\s+/)[0].replace('*', '').trim())
|
|
1434
|
+
.filter(b => b && b !== 'main' && b !== 'master');
|
|
1435
|
+
|
|
1436
|
+
if (goneBranches.length === 0) {
|
|
1437
|
+
Progress.success('No dead branches to clean up');
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
console.log(color.bold(`\nFound ${goneBranches.length} dead branch(es):`));
|
|
1442
|
+
goneBranches.forEach(b => console.log(` ${color.dim('•')} ${b}`));
|
|
1443
|
+
|
|
1444
|
+
if (!opts.yes) {
|
|
1445
|
+
console.log(`\nUse ${color.cyan('g clean --yes')} to delete them`);
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
for (const branch of goneBranches) {
|
|
1450
|
+
try {
|
|
1451
|
+
await git.branch(['-D', branch]);
|
|
1452
|
+
Progress.success(`Deleted: ${branch}`);
|
|
1453
|
+
} catch (e) {
|
|
1454
|
+
Progress.warning(`Could not delete: ${branch}`);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
Progress.success(`Cleaned up ${goneBranches.length} branch(es)`);
|
|
1459
|
+
} catch (e) {
|
|
1460
|
+
handleError('Cleanup error', e);
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
program.command('last')
|
|
1465
|
+
.description('Show last commit details and diff')
|
|
1466
|
+
.action(async () => {
|
|
1467
|
+
await ensureRepo();
|
|
1468
|
+
try {
|
|
1469
|
+
const log = await git.log({ maxCount: 1 });
|
|
1470
|
+
if (log.all.length === 0) {
|
|
1471
|
+
Progress.info('No commits yet');
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
const commit = log.all[0];
|
|
1476
|
+
console.log(color.bold('\n📝 Last Commit\n'));
|
|
1477
|
+
console.log(` ${color.yellow(commit.hash.substring(0, 7))} ${commit.message.split('\n')[0]}`);
|
|
1478
|
+
console.log(` ${color.dim(`by ${commit.author_name} • ${new Date(commit.date).toLocaleString()}`)}`);
|
|
1479
|
+
|
|
1480
|
+
// Show diff stats
|
|
1481
|
+
const diff = await git.diff(['HEAD~1', '--stat']);
|
|
1482
|
+
if (diff.trim()) {
|
|
1483
|
+
console.log(`\n${color.bold('Changes:')}`);
|
|
1484
|
+
console.log(color.dim(diff));
|
|
1485
|
+
}
|
|
1486
|
+
} catch (e) {
|
|
1487
|
+
handleError('Last error', e);
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
// ===== SMART SYNC FIX COMMAND =====
|
|
1492
|
+
|
|
1493
|
+
program.command('fix')
|
|
1494
|
+
.description('Smart fix for branch sync issues (diverged, behind, ahead)')
|
|
1495
|
+
.option('--local', 'Keep local changes, force push to remote')
|
|
1496
|
+
.option('--remote', 'Keep remote changes, discard local')
|
|
1497
|
+
.option('--merge', 'Merge remote into local')
|
|
1498
|
+
.option('--rebase', 'Rebase local onto remote')
|
|
1499
|
+
.option('--ai', 'Get AI recommendation for best approach')
|
|
1500
|
+
.action(async (cmdOptions) => {
|
|
1501
|
+
await ensureRepo();
|
|
1502
|
+
const opts = getOpts();
|
|
1503
|
+
|
|
1504
|
+
try {
|
|
1505
|
+
// Fetch latest
|
|
1506
|
+
Progress.info('Fetching remote status...');
|
|
1507
|
+
await git.fetch();
|
|
1508
|
+
|
|
1509
|
+
const branch = (await git.branch()).current;
|
|
1510
|
+
const remoteBranch = `origin/${branch}`;
|
|
1511
|
+
|
|
1512
|
+
// Check if remote exists
|
|
1513
|
+
let remoteExists = true;
|
|
1514
|
+
try {
|
|
1515
|
+
await git.raw(['rev-parse', '--verify', remoteBranch]);
|
|
1516
|
+
} catch {
|
|
1517
|
+
remoteExists = false;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
if (!remoteExists) {
|
|
1521
|
+
console.log(color.bold(`\n📍 Branch Status: ${color.cyan(branch)}\n`));
|
|
1522
|
+
console.log(`Remote branch ${color.yellow(remoteBranch)} doesn't exist yet.`);
|
|
1523
|
+
console.log(`\nOptions:`);
|
|
1524
|
+
console.log(` ${color.cyan('g push --set-upstream')} Push and create remote branch`);
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// Get ahead/behind counts
|
|
1529
|
+
const ahead = parseInt((await git.raw(['rev-list', '--count', `${remoteBranch}..${branch}`])).trim());
|
|
1530
|
+
const behind = parseInt((await git.raw(['rev-list', '--count', `${branch}..${remoteBranch}`])).trim());
|
|
1531
|
+
|
|
1532
|
+
// Get local changes status
|
|
1533
|
+
const status = await git.status();
|
|
1534
|
+
const hasLocalChanges = status.files.length > 0;
|
|
1535
|
+
|
|
1536
|
+
console.log(color.bold(`\n📍 Branch Status: ${color.cyan(branch)}\n`));
|
|
1537
|
+
|
|
1538
|
+
// Determine situation
|
|
1539
|
+
let situation = '';
|
|
1540
|
+
if (ahead === 0 && behind === 0) {
|
|
1541
|
+
Progress.success('Branch is up to date with remote!');
|
|
1542
|
+
if (hasLocalChanges) {
|
|
1543
|
+
console.log(`\nYou have ${status.files.length} uncommitted change(s).`);
|
|
1544
|
+
console.log(`Use ${color.cyan('g o')} to commit and push them.`);
|
|
1545
|
+
}
|
|
1546
|
+
return;
|
|
1547
|
+
} else if (ahead > 0 && behind === 0) {
|
|
1548
|
+
situation = 'ahead';
|
|
1549
|
+
console.log(` ${color.green('↑')} ${ahead} commit(s) ahead of remote`);
|
|
1550
|
+
console.log(` ${color.dim('Your local has commits not on remote')}\n`);
|
|
1551
|
+
} else if (ahead === 0 && behind > 0) {
|
|
1552
|
+
situation = 'behind';
|
|
1553
|
+
console.log(` ${color.yellow('↓')} ${behind} commit(s) behind remote`);
|
|
1554
|
+
console.log(` ${color.dim('Remote has commits you don\'t have')}\n`);
|
|
1555
|
+
} else {
|
|
1556
|
+
situation = 'diverged';
|
|
1557
|
+
console.log(` ${color.red('⚡')} Branch has diverged!`);
|
|
1558
|
+
console.log(` ${color.green('↑')} ${ahead} commit(s) ahead`);
|
|
1559
|
+
console.log(` ${color.yellow('↓')} ${behind} commit(s) behind\n`);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
if (hasLocalChanges) {
|
|
1563
|
+
console.log(color.yellow(`⚠️ You have ${status.files.length} uncommitted file(s)`));
|
|
1564
|
+
console.log(`${color.dim('Commit or stash them before fixing sync issues')}\n`);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// If specific option provided, execute it
|
|
1568
|
+
if (cmdOptions.local) {
|
|
1569
|
+
if (!opts.yes) {
|
|
1570
|
+
console.log(color.red('⚠️ This will FORCE PUSH and overwrite remote!'));
|
|
1571
|
+
console.log(`Use ${color.cyan('g fix --local --yes')} to confirm.`);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
Progress.info('Force pushing local to remote...');
|
|
1575
|
+
await git.push(['--force']);
|
|
1576
|
+
Progress.success('Force pushed! Remote now matches local.');
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
if (cmdOptions.remote) {
|
|
1581
|
+
if (!opts.yes) {
|
|
1582
|
+
console.log(color.red('⚠️ This will DISCARD local commits!'));
|
|
1583
|
+
console.log(`Use ${color.cyan('g fix --remote --yes')} to confirm.`);
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
Progress.info('Resetting to remote...');
|
|
1587
|
+
await git.reset(['--hard', remoteBranch]);
|
|
1588
|
+
Progress.success('Reset! Local now matches remote.');
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
if (cmdOptions.merge) {
|
|
1593
|
+
Progress.info('Merging remote into local...');
|
|
1594
|
+
try {
|
|
1595
|
+
await git.merge([remoteBranch]);
|
|
1596
|
+
Progress.success('Merged successfully!');
|
|
1597
|
+
} catch (e) {
|
|
1598
|
+
Progress.error('Merge conflict! Resolve conflicts then run: g o');
|
|
1599
|
+
}
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
if (cmdOptions.rebase) {
|
|
1604
|
+
Progress.info('Rebasing local onto remote...');
|
|
1605
|
+
try {
|
|
1606
|
+
await git.rebase([remoteBranch]);
|
|
1607
|
+
Progress.success('Rebased successfully!');
|
|
1608
|
+
console.log(`Now run ${color.cyan('g push --force')} to update remote.`);
|
|
1609
|
+
} catch (e) {
|
|
1610
|
+
Progress.error('Rebase conflict! Resolve conflicts then run: git rebase --continue');
|
|
1611
|
+
}
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// AI recommendation
|
|
1616
|
+
if (cmdOptions.ai) {
|
|
1617
|
+
Progress.info('Analyzing best approach...');
|
|
1618
|
+
let recommendation = '';
|
|
1619
|
+
|
|
1620
|
+
if (situation === 'ahead') {
|
|
1621
|
+
recommendation = `Your local is ${ahead} commits ahead. Simply push to sync.`;
|
|
1622
|
+
console.log(`\n${color.bold('🤖 AI Recommendation:')}`);
|
|
1623
|
+
console.log(` ${recommendation}`);
|
|
1624
|
+
console.log(`\n Run: ${color.cyan('g push')}`);
|
|
1625
|
+
} else if (situation === 'behind') {
|
|
1626
|
+
recommendation = `Remote has ${behind} new commits. Pull to get them.`;
|
|
1627
|
+
console.log(`\n${color.bold('🤖 AI Recommendation:')}`);
|
|
1628
|
+
console.log(` ${recommendation}`);
|
|
1629
|
+
console.log(`\n Run: ${color.cyan('g pull')} or ${color.cyan('g sp')} (if you have changes)`);
|
|
1630
|
+
} else {
|
|
1631
|
+
// Diverged - more complex
|
|
1632
|
+
if (ahead <= 2 && behind > ahead) {
|
|
1633
|
+
recommendation = `Small local changes (${ahead}), larger remote (${behind}). Rebase recommended for clean history.`;
|
|
1634
|
+
console.log(`\n${color.bold('🤖 AI Recommendation:')}`);
|
|
1635
|
+
console.log(` ${recommendation}`);
|
|
1636
|
+
console.log(`\n Run: ${color.cyan('g fix --rebase')}`);
|
|
1637
|
+
} else if (behind <= 2 && ahead > behind) {
|
|
1638
|
+
recommendation = `Large local changes (${ahead}), small remote (${behind}). Merge is safe.`;
|
|
1639
|
+
console.log(`\n${color.bold('🤖 AI Recommendation:')}`);
|
|
1640
|
+
console.log(` ${recommendation}`);
|
|
1641
|
+
console.log(`\n Run: ${color.cyan('g fix --merge')}`);
|
|
1642
|
+
} else {
|
|
1643
|
+
recommendation = `Significant divergence. Review changes first, then choose merge or rebase.`;
|
|
1644
|
+
console.log(`\n${color.bold('🤖 AI Recommendation:')}`);
|
|
1645
|
+
console.log(` ${recommendation}`);
|
|
1646
|
+
console.log(`\n View remote changes: ${color.cyan(`git log ${branch}..${remoteBranch} --oneline`)}`);
|
|
1647
|
+
console.log(` View local changes: ${color.cyan(`git log ${remoteBranch}..${branch} --oneline`)}`);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// Show interactive menu
|
|
1654
|
+
console.log(color.bold('🔧 Fix Options:\n'));
|
|
1655
|
+
|
|
1656
|
+
if (situation === 'ahead') {
|
|
1657
|
+
console.log(` ${color.cyan('g push')} Push your commits to remote`);
|
|
1658
|
+
} else if (situation === 'behind') {
|
|
1659
|
+
console.log(` ${color.cyan('g pull')} Get remote commits (fast-forward)`);
|
|
1660
|
+
console.log(` ${color.cyan('g sp')} Safe pull (stash → pull → pop)`);
|
|
1661
|
+
} else {
|
|
1662
|
+
// Diverged
|
|
1663
|
+
console.log(` ${color.cyan('g fix --merge')} Merge remote into local (creates merge commit)`);
|
|
1664
|
+
console.log(` ${color.cyan('g fix --rebase')} Rebase local onto remote (linear history)`);
|
|
1665
|
+
console.log(color.dim(' ─────────────────'));
|
|
1666
|
+
console.log(` ${color.cyan('g fix --local')} ${color.yellow('⚠')} Force push local, overwrite remote`);
|
|
1667
|
+
console.log(` ${color.cyan('g fix --remote')} ${color.yellow('⚠')} Reset to remote, discard local commits`);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
console.log(color.dim(' ─────────────────'));
|
|
1671
|
+
console.log(` ${color.cyan('g fix --ai')} Get AI recommendation`);
|
|
1672
|
+
|
|
1673
|
+
} catch (e) {
|
|
1674
|
+
handleError('Fix error', e);
|
|
1675
|
+
}
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
program.command('conflicts')
|
|
1679
|
+
.description('Show and help resolve merge conflicts')
|
|
1680
|
+
.action(async () => {
|
|
1681
|
+
await ensureRepo();
|
|
1682
|
+
try {
|
|
1683
|
+
const status = await git.status();
|
|
1684
|
+
const conflicts = status.conflicted || [];
|
|
1685
|
+
|
|
1686
|
+
if (conflicts.length === 0) {
|
|
1687
|
+
Progress.success('No merge conflicts!');
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
console.log(color.bold(`\n⚠️ ${conflicts.length} Conflicted File(s):\n`));
|
|
1692
|
+
conflicts.forEach((file, i) => {
|
|
1693
|
+
const emoji = Intelligence.getFileEmoji(file);
|
|
1694
|
+
console.log(` ${i + 1}. ${emoji} ${color.red(file)}`);
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
console.log(color.bold('\n🔧 How to resolve:\n'));
|
|
1698
|
+
console.log(` 1. Edit each file and resolve the conflict markers`);
|
|
1699
|
+
console.log(` ${color.dim('<<<<<<< HEAD')}`);
|
|
1700
|
+
console.log(` ${color.dim('your changes')}`);
|
|
1701
|
+
console.log(` ${color.dim('=======')}`);
|
|
1702
|
+
console.log(` ${color.dim('their changes')}`);
|
|
1703
|
+
console.log(` ${color.dim('>>>>>>> branch')}`);
|
|
1704
|
+
console.log(` 2. Stage resolved files: ${color.cyan('git add <file>')}`);
|
|
1705
|
+
console.log(` 3. Complete the merge: ${color.cyan('g o')} or ${color.cyan('git merge --continue')}`);
|
|
1706
|
+
|
|
1707
|
+
console.log(color.bold('\n⚡ Quick options:\n'));
|
|
1708
|
+
console.log(` ${color.cyan('git checkout --ours <file>')} Keep YOUR version`);
|
|
1709
|
+
console.log(` ${color.cyan('git checkout --theirs <file>')} Keep THEIR version`);
|
|
1710
|
+
|
|
1711
|
+
} catch (e) {
|
|
1712
|
+
handleError('Conflicts error', e);
|
|
983
1713
|
}
|
|
984
1714
|
});
|
|
985
1715
|
|
|
986
1716
|
program.parse(process.argv);
|
|
1717
|
+
|