gims 0.6.6 → 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 +89 -0
- package/README.md +33 -13
- package/bin/gims.js +848 -105
- 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();
|
|
@@ -137,7 +138,7 @@ async function hasChanges() {
|
|
|
137
138
|
program
|
|
138
139
|
.name('gims')
|
|
139
140
|
.alias('g')
|
|
140
|
-
.version(require('../package.json').version)
|
|
141
|
+
.version(require('../package.json').version, '-v, --version', 'Output the version number')
|
|
141
142
|
.option('--provider <name>', 'AI provider: auto|openai|gemini|groq|none')
|
|
142
143
|
.option('--model <name>', 'Model identifier for provider')
|
|
143
144
|
.option('--staged-only', 'Use only staged changes (default for suggest)')
|
|
@@ -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) {
|
|
@@ -321,42 +322,45 @@ program.command('config')
|
|
|
321
322
|
}
|
|
322
323
|
});
|
|
323
324
|
|
|
324
|
-
program.command('help
|
|
325
|
+
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,14 +644,26 @@ 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); }
|
|
649
653
|
});
|
|
650
654
|
|
|
655
|
+
program.command('push')
|
|
656
|
+
.description('Push commits to remote')
|
|
657
|
+
.action(async () => {
|
|
658
|
+
await ensureRepo();
|
|
659
|
+
try {
|
|
660
|
+
Progress.info('Pushing to remote...');
|
|
661
|
+
await git.push();
|
|
662
|
+
Progress.success('Pushed to remote');
|
|
663
|
+
}
|
|
664
|
+
catch (e) { handleError('Push error', e); }
|
|
665
|
+
});
|
|
666
|
+
|
|
651
667
|
program.command('sync')
|
|
652
668
|
.description('Smart sync: pull + rebase/merge')
|
|
653
669
|
.option('--rebase', 'Use rebase instead of merge')
|
|
@@ -655,27 +671,27 @@ program.command('sync')
|
|
|
655
671
|
await ensureRepo();
|
|
656
672
|
try {
|
|
657
673
|
const status = await git.status();
|
|
658
|
-
|
|
674
|
+
|
|
659
675
|
if (status.files.length > 0) {
|
|
660
676
|
Progress.warning('You have uncommitted changes. Commit or stash them first.');
|
|
661
677
|
return;
|
|
662
678
|
}
|
|
663
|
-
|
|
679
|
+
|
|
664
680
|
Progress.info('Fetching latest changes...');
|
|
665
681
|
await git.fetch();
|
|
666
|
-
|
|
682
|
+
|
|
667
683
|
const currentBranch = (await git.raw(['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
|
|
668
684
|
const remoteBranch = `origin/${currentBranch}`;
|
|
669
|
-
|
|
685
|
+
|
|
670
686
|
try {
|
|
671
687
|
const behind = await git.raw(['rev-list', '--count', `${currentBranch}..${remoteBranch}`]);
|
|
672
688
|
const ahead = await git.raw(['rev-list', '--count', `${remoteBranch}..${currentBranch}`]);
|
|
673
|
-
|
|
689
|
+
|
|
674
690
|
if (parseInt(behind.trim()) === 0) {
|
|
675
691
|
Progress.success('Already up to date');
|
|
676
692
|
return;
|
|
677
693
|
}
|
|
678
|
-
|
|
694
|
+
|
|
679
695
|
if (parseInt(ahead.trim()) > 0) {
|
|
680
696
|
Progress.info(`Branch is ${ahead.trim()} commits ahead and ${behind.trim()} commits behind`);
|
|
681
697
|
if (cmdOptions.rebase) {
|
|
@@ -701,8 +717,8 @@ program.command('sync')
|
|
|
701
717
|
throw error;
|
|
702
718
|
}
|
|
703
719
|
}
|
|
704
|
-
} catch (e) {
|
|
705
|
-
handleError('Sync error', e);
|
|
720
|
+
} catch (e) {
|
|
721
|
+
handleError('Sync error', e);
|
|
706
722
|
}
|
|
707
723
|
});
|
|
708
724
|
|
|
@@ -720,7 +736,7 @@ program.command('stash')
|
|
|
720
736
|
Progress.info('No stashes found');
|
|
721
737
|
return;
|
|
722
738
|
}
|
|
723
|
-
|
|
739
|
+
|
|
724
740
|
console.log(color.bold('Stashes:'));
|
|
725
741
|
stashes.all.forEach((stash, i) => {
|
|
726
742
|
console.log(`${color.cyan((i).toString())}. ${stash.message}`);
|
|
@@ -739,15 +755,15 @@ program.command('stash')
|
|
|
739
755
|
Progress.warning('No changes to stash');
|
|
740
756
|
return;
|
|
741
757
|
}
|
|
742
|
-
|
|
758
|
+
|
|
743
759
|
Progress.start('🤖 Generating stash description');
|
|
744
760
|
const diff = await git.diff();
|
|
745
|
-
const description = await aiProvider.generateCommitMessage(diff, {
|
|
746
|
-
conventional: false,
|
|
747
|
-
body: false
|
|
761
|
+
const description = await aiProvider.generateCommitMessage(diff, {
|
|
762
|
+
conventional: false,
|
|
763
|
+
body: false
|
|
748
764
|
});
|
|
749
765
|
Progress.stop('');
|
|
750
|
-
|
|
766
|
+
|
|
751
767
|
await git.stash(['push', '-m', `WIP: ${description}`]);
|
|
752
768
|
Progress.success(`Stashed changes: "${description}"`);
|
|
753
769
|
}
|
|
@@ -783,10 +799,10 @@ program.command('amend').alias('a')
|
|
|
783
799
|
Progress.start('🤖 Generating updated commit message');
|
|
784
800
|
const result = await generateCommitMessage(rawDiff, opts);
|
|
785
801
|
Progress.stop('');
|
|
786
|
-
|
|
802
|
+
|
|
787
803
|
const newMessage = result.message || result; // Handle both old and new format
|
|
788
804
|
const usedLocal = result.usedLocal || false;
|
|
789
|
-
|
|
805
|
+
|
|
790
806
|
// Ask for confirmation if using local heuristics (unless --yes flag is set)
|
|
791
807
|
if (usedLocal && !opts.yes) {
|
|
792
808
|
const confirmed = await confirmCommit(newMessage, true);
|
|
@@ -795,7 +811,7 @@ program.command('amend').alias('a')
|
|
|
795
811
|
return;
|
|
796
812
|
}
|
|
797
813
|
}
|
|
798
|
-
|
|
814
|
+
|
|
799
815
|
await git.raw(['commit', '--amend', '-m', newMessage]);
|
|
800
816
|
Progress.success(`Amended commit: "${newMessage}"`);
|
|
801
817
|
} else {
|
|
@@ -817,21 +833,21 @@ program.command('list').alias('ls')
|
|
|
817
833
|
const limit = parseInt(cmdOptions.limit) || 20;
|
|
818
834
|
const log = await git.log({ maxCount: limit });
|
|
819
835
|
const commits = [...log.all].reverse();
|
|
820
|
-
|
|
836
|
+
|
|
821
837
|
if (commits.length === 0) {
|
|
822
838
|
Progress.info('No commits found');
|
|
823
839
|
return;
|
|
824
840
|
}
|
|
825
|
-
|
|
841
|
+
|
|
826
842
|
commits.forEach((c, i) => {
|
|
827
|
-
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}`);
|
|
828
844
|
});
|
|
829
|
-
|
|
845
|
+
|
|
830
846
|
if (log.all.length >= limit) {
|
|
831
847
|
console.log(color.dim(`\n... showing last ${limit} commits (use --limit to see more)`));
|
|
832
848
|
}
|
|
833
|
-
} catch (e) {
|
|
834
|
-
handleError('List error', e);
|
|
849
|
+
} catch (e) {
|
|
850
|
+
handleError('List error', e);
|
|
835
851
|
}
|
|
836
852
|
});
|
|
837
853
|
|
|
@@ -844,22 +860,22 @@ program.command('largelist').alias('ll')
|
|
|
844
860
|
const limit = parseInt(cmdOptions.limit) || 20;
|
|
845
861
|
const log = await git.log({ maxCount: limit });
|
|
846
862
|
const commits = [...log.all].reverse();
|
|
847
|
-
|
|
863
|
+
|
|
848
864
|
if (commits.length === 0) {
|
|
849
865
|
Progress.info('No commits found');
|
|
850
866
|
return;
|
|
851
867
|
}
|
|
852
|
-
|
|
868
|
+
|
|
853
869
|
commits.forEach((c, i) => {
|
|
854
870
|
const date = new Date(c.date).toLocaleString();
|
|
855
|
-
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}`);
|
|
856
872
|
});
|
|
857
|
-
|
|
873
|
+
|
|
858
874
|
if (log.all.length >= limit) {
|
|
859
875
|
console.log(color.dim(`\n... showing last ${limit} commits (use --limit to see more)`));
|
|
860
876
|
}
|
|
861
|
-
} catch (e) {
|
|
862
|
-
handleError('Largelist error', e);
|
|
877
|
+
} catch (e) {
|
|
878
|
+
handleError('Largelist error', e);
|
|
863
879
|
}
|
|
864
880
|
});
|
|
865
881
|
|
|
@@ -872,21 +888,21 @@ program.command('history').alias('h')
|
|
|
872
888
|
const limit = parseInt(cmdOptions.limit) || 20;
|
|
873
889
|
const log = await git.log({ maxCount: limit });
|
|
874
890
|
const commits = [...log.all].reverse();
|
|
875
|
-
|
|
891
|
+
|
|
876
892
|
if (commits.length === 0) {
|
|
877
893
|
Progress.info('No commits found');
|
|
878
894
|
return;
|
|
879
895
|
}
|
|
880
|
-
|
|
896
|
+
|
|
881
897
|
commits.forEach((c, i) => {
|
|
882
|
-
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}`);
|
|
883
899
|
});
|
|
884
|
-
|
|
900
|
+
|
|
885
901
|
if (log.all.length >= limit) {
|
|
886
902
|
console.log(color.dim(`\n... showing last ${limit} commits (use --limit to see more)`));
|
|
887
903
|
}
|
|
888
|
-
} catch (e) {
|
|
889
|
-
handleError('History error', e);
|
|
904
|
+
} catch (e) {
|
|
905
|
+
handleError('History error', e);
|
|
890
906
|
}
|
|
891
907
|
});
|
|
892
908
|
|
|
@@ -894,18 +910,18 @@ program.command('branch <c> [name]').alias('b')
|
|
|
894
910
|
.description('Branch from commit/index')
|
|
895
911
|
.action(async (c, name) => {
|
|
896
912
|
await ensureRepo();
|
|
897
|
-
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}`); }
|
|
898
914
|
catch (e) { handleError('Branch error', e); }
|
|
899
915
|
});
|
|
900
916
|
|
|
901
|
-
program.command('reset <c>').alias('
|
|
917
|
+
program.command('reset <c>').alias('rs')
|
|
902
918
|
.description('Reset branch to commit/index')
|
|
903
|
-
.option('--hard','hard reset')
|
|
919
|
+
.option('--hard', 'hard reset')
|
|
904
920
|
.action(async (c, optsCmd) => {
|
|
905
921
|
await ensureRepo();
|
|
906
922
|
try {
|
|
907
923
|
const sha = await resolveCommit(c);
|
|
908
|
-
const mode = optsCmd.hard? '--hard':'--soft';
|
|
924
|
+
const mode = optsCmd.hard ? '--hard' : '--soft';
|
|
909
925
|
const opts = getOpts();
|
|
910
926
|
if (!opts.yes) {
|
|
911
927
|
console.log(color.yellow(`About to run: git reset ${mode} ${sha}. Use --yes to confirm.`));
|
|
@@ -945,11 +961,11 @@ program.command('undo').alias('u')
|
|
|
945
961
|
Progress.warning('No commits to undo');
|
|
946
962
|
return;
|
|
947
963
|
}
|
|
948
|
-
|
|
964
|
+
|
|
949
965
|
const lastCommit = all[0];
|
|
950
966
|
const mode = cmd.hard ? '--hard' : '--soft';
|
|
951
967
|
const opts = getOpts();
|
|
952
|
-
|
|
968
|
+
|
|
953
969
|
if (!opts.yes) {
|
|
954
970
|
console.log(color.yellow(`About to undo: "${lastCommit.message}"`));
|
|
955
971
|
console.log(color.yellow(`This will run: git reset ${mode} HEAD~1`));
|
|
@@ -959,16 +975,743 @@ program.command('undo').alias('u')
|
|
|
959
975
|
console.log('Use --yes to confirm.');
|
|
960
976
|
process.exit(1);
|
|
961
977
|
}
|
|
962
|
-
|
|
978
|
+
|
|
963
979
|
await git.raw(['reset', mode, 'HEAD~1']);
|
|
964
980
|
Progress.success(`Undone commit: "${lastCommit.message}" (${mode} reset)`);
|
|
965
|
-
|
|
981
|
+
|
|
966
982
|
if (mode === '--soft') {
|
|
967
983
|
Progress.info('Changes are now staged. Use "g status" to see them.');
|
|
968
984
|
}
|
|
969
|
-
} catch (e) {
|
|
970
|
-
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);
|
|
971
1713
|
}
|
|
972
1714
|
});
|
|
973
1715
|
|
|
974
1716
|
program.parse(process.argv);
|
|
1717
|
+
|