version 0.8.1 release, stable release

This commit is contained in:
s41r4j
2025-12-30 19:47:40 +05:30
parent 4a30dd018f
commit e5e4713384
9 changed files with 1609 additions and 200 deletions
+80
View File
@@ -1,5 +1,85 @@
# Changelog # Changelog
## [0.8.1] - 2025-12-19
### 🚀 Smart Sync Fix
New `g fix` command to handle branch sync issues (diverged/ahead/behind):
| Option | What it does |
|--------|--------------|
| `g fix` | Show status and available options |
| `g fix --ai` | Get AI recommendation for best approach |
| `g fix --merge` | Merge remote into local |
| `g fix --rebase` | Rebase local onto remote |
| `g fix --local --yes` | Force push local to remote |
| `g fix --remote --yes` | Reset to remote, discard local |
### 🔧 Conflict Helper
New `g conflicts` command shows conflicted files and how to resolve them.
---
## [0.8.0] - 2025-12-19
### 🚀 New Workflow Shorthand Commands
Multi-step git workflows simplified to single commands:
| Command | Alias | What it does |
|---------|-------|--------------|
| `safe-pull` | `sp` | Stash → Pull → Stash pop (safe pull with uncommitted changes) |
| `main` | - | Switch to main/master and pull latest |
| `unstage` | `us` | Unstage all staged files |
| `discard` | `x` | Discard all changes (with --yes confirmation) |
| `stash-save` | `ss` | Quick stash all changes with auto-generated name |
| `stash-pop` | `pop` | Pop the latest stash |
| `delete-branch` | `del` | Delete branch locally and remotely |
| `cleanup` | `clean` | Remove local branches deleted from remote |
| `last` | - | Show last commit details and diff |
---
## [0.7.2] - 2025-12-18
### 🐛 Bug Fix
- **Fixed progress spinner garbage output**: Spinner now properly clears the line when stopping, preventing partial text artifacts like ` (1ms)rating AI review ⠋`
---
## [0.7.1] - 2025-12-18
### 🐛 Bug Fix
- **Fixed AI suggestions in interactive mode**: Multiple suggestions now correctly display message strings instead of `[object Object]`
---
## [0.7.0] - 2025-12-18
### 🚀 New Intelligent Commands
- **`g wip`**: Quick work-in-progress commit - stage all and commit instantly
- **`g today` / `g t`**: Show all commits made today with timestamps
- **`g stats`**: Personal commit statistics with streak tracking, type breakdown, and style analysis
- **`g review` / `g r`**: AI code review before committing - shows complexity, detected patterns, and suggested message
- **`g split`**: Smart suggestions for splitting large changesets into atomic commits
### ✨ Enhanced Status
- **File type emojis**: 📄 JS, 🎨 CSS, 🧪 tests, ⚙️ config, 📦 package.json, etc.
- **Session awareness**: Shows time since last commit and daily commit count
- **Branch context**: Detects branch type from naming patterns (feat/, fix/, etc.)
- **Smarter insights**: Suggests staging, split commits, and more
### 🧠 Intelligence Module
- **Commit pattern analysis**: Learns your style from git history
- **Semantic change detection**: Identifies breaking changes, new features, bug fixes
- **Complexity analysis**: Visual indicators for simple/moderate/complex changes
### 📊 UX Improvements
- **Time elapsed display**: Shows how long operations take
- **Random tips**: Contextual tips to help learn GIMS features
- **Cached response indicators**: Know when AI cache is used
- **Better quick-help**: Reorganized command reference with new commands
---
## [0.6.7] - 2025-10-26 ## [0.6.7] - 2025-10-26
### 🔧 Fixes ### 🔧 Fixes
+32 -12
View File
@@ -26,17 +26,36 @@ g o # AI commit + push
## 🎯 Main Commands ## 🎯 Main Commands
### 🧠 Smart Commands
| Command | Description | | Command | Description |
|---------|-------------| |---------|-------------|
| `g s` | Enhanced status with AI insights | | `g s` | Enhanced status with AI insights |
| `g i` | Initialize git repository |
| `g int` | Interactive commit wizard |
| `g o` | AI commit + push | | `g o` | AI commit + push |
| `g l` | AI commit locally | | `g l` | AI commit locally |
| `g wip` | Quick WIP commit |
| `g r` | AI code review |
| `g t` | Show today's commits |
| `g stats` | Personal commit statistics |
### ⚡ Workflow Shortcuts
| Command | What it does |
|---------|--------------|
| `g sp` | **Safe Pull** (Stash → Pull → Pop) |
| `g fix` | **Smart Fix** for branch sync issues |
| `g main` | Switch to main/master + pull |
| `g ss` | Quick stash save |
| `g pop` | Pop latest stash |
| `g us` | Unstage all files |
| `g x` | Discard all changes (with confirm) |
### 🛠️ Helper Commands
| Command | Description |
|---------|-------------|
| `g last` | Show last commit details |
| `g conflicts` | Conflict resolution helper |
| `g clean` | Remove dead local branches |
| `g ls` | Commit history (short) | | `g ls` | Commit history (short) |
| `g ll` | Commit history (detailed) | | `g a` | Amend previous commit |
| `g h` | Commit history (alias for ls) |
| `g a` | Amend previous commit (keeps message) |
## 🤖 AI Models ## 🤖 AI Models
@@ -48,14 +67,15 @@ g o # AI commit + push
```bash ```bash
# Daily workflow # Daily workflow
g s # Check what changed g s # Check status
g int # Interactive commit g sp # Safe pull updates
g o # Quick commit + push g fix # Fix any sync issues
g o # Commit + push
# Advanced # Power functions
g sg --multiple # Get 3 AI suggestions g r # Review code before commit
g ll # Detailed history g stats # Check your streak
g sync --rebase # Smart sync g split # Split big changesets
``` ```
## 🔧 Configuration ## 🔧 Configuration
+759 -28
View File
@@ -15,6 +15,7 @@ const { ConfigManager } = require('./lib/config/manager');
const { GitAnalyzer } = require('./lib/git/analyzer'); const { GitAnalyzer } = require('./lib/git/analyzer');
const { AIProviderManager } = require('./lib/ai/providers'); const { AIProviderManager } = require('./lib/ai/providers');
const { InteractiveCommands } = require('./lib/commands/interactive'); const { InteractiveCommands } = require('./lib/commands/interactive');
const { Intelligence } = require('./lib/utils/intelligence');
const program = new Command(); const program = new Command();
const git = simpleGit(); const git = simpleGit();
@@ -326,30 +327,33 @@ program.command('quick-help').alias('q')
.action(() => { .action(() => {
console.log(color.bold('🚀 GIMS Quick Reference\n')); console.log(color.bold('🚀 GIMS Quick Reference\n'));
console.log(color.bold('Single-Letter Workflow:')); console.log(color.bold('Core Workflow:'));
console.log(` ${color.cyan('g s')} Status - Enhanced git status with AI insights`); console.log(` ${color.cyan('g s')} Status with AI insights`);
console.log(` ${color.cyan('g i')} Init - Initialize a new Git repository`); console.log(` ${color.cyan('g o')} AI commit + push`);
console.log(` ${color.cyan('g p')} Preview - See what will be committed`); console.log(` ${color.cyan('g l')} AI commit locally`);
console.log(` ${color.cyan('g l')} Local - AI commit locally`); console.log(` ${color.cyan('g wip')} Quick WIP commit\n`);
console.log(` ${color.cyan('g o')} Online - AI commit + push`);
console.log(` ${color.cyan('g ls')} List - Short commit history`);
console.log(` ${color.cyan('g ll')} Large List - Detailed commit history`);
console.log(` ${color.cyan('g h')} History - Alias for list`);
console.log(` ${color.cyan('g a')} Amend - Merge changes to previous commit (keeps message)`);
console.log(` ${color.cyan('g u')} Undo - Undo last commit\n`);
console.log(color.bold('Quick Setup:')); console.log(color.bold('Sync & Fix:'));
console.log(` ${color.cyan('g setup --api-key gemini')} 🚀 gemini-2.5-flash (recommended)`); console.log(` ${color.cyan('g sp')} Safe pull (stash → pull → pop)`);
console.log(` ${color.cyan('g setup --api-key openai')} 💎 gpt-5 (high quality)`); console.log(` ${color.cyan('g fix')} Fix branch sync issues`);
console.log(` ${color.cyan('g setup --api-key groq')} ⚡ groq/compound (ultra fast)\n`); console.log(` ${color.cyan('g main')} Switch to main + pull\n`);
console.log(color.bold('Essential Workflow:')); console.log(color.bold('Stash:'));
console.log(` ${color.cyan('g s')} Check what's changed`); console.log(` ${color.cyan('g ss')} Quick stash save`);
console.log(` ${color.cyan('g int')} or ${color.cyan('g o')} Commit with AI (interactive or online)`); console.log(` ${color.cyan('g pop')} Pop latest stash`);
console.log(` ${color.cyan('g ls')} or ${color.cyan('g h')} View history\n`); console.log(` ${color.cyan('g us')} Unstage all files\n`);
console.log(`For full help: ${color.cyan('g --help')}`); console.log(color.bold('Smart Commands:'));
console.log(`For detailed docs: See README.md`); console.log(` ${color.cyan('g r')} AI code review`);
console.log(` ${color.cyan('g t')} Today's commits`);
console.log(` ${color.cyan('g last')} Last commit details\n`);
console.log(color.bold('History:'));
console.log(` ${color.cyan('g ls')} Commit history`);
console.log(` ${color.cyan('g a')} Amend last commit`);
console.log(` ${color.cyan('g u')} Undo last commit\n`);
console.log(`Full help: ${color.cyan('g --help')}`);
}); });
program.command('init').alias('i') program.command('init').alias('i')
@@ -836,7 +840,7 @@ program.command('list').alias('ls')
} }
commits.forEach((c, i) => { commits.forEach((c, i) => {
console.log(`${color.cyan((i+1).toString())}. ${color.yellow(c.hash.slice(0,7))} ${c.message}`); console.log(`${color.cyan((i + 1).toString())}. ${color.yellow(c.hash.slice(0, 7))} ${c.message}`);
}); });
if (log.all.length >= limit) { if (log.all.length >= limit) {
@@ -864,7 +868,7 @@ program.command('largelist').alias('ll')
commits.forEach((c, i) => { commits.forEach((c, i) => {
const date = new Date(c.date).toLocaleString(); const date = new Date(c.date).toLocaleString();
console.log(`${color.cyan((i+1).toString())}. ${color.yellow(c.hash.slice(0,7))} | ${color.dim(date)} | ${color.green(c.author_name)}${c.message}`); console.log(`${color.cyan((i + 1).toString())}. ${color.yellow(c.hash.slice(0, 7))} | ${color.dim(date)} | ${color.green(c.author_name)}${c.message}`);
}); });
if (log.all.length >= limit) { if (log.all.length >= limit) {
@@ -891,7 +895,7 @@ program.command('history').alias('h')
} }
commits.forEach((c, i) => { commits.forEach((c, i) => {
console.log(`${color.cyan((i+1).toString())}. ${color.yellow(c.hash.slice(0,7))} ${c.message}`); console.log(`${color.cyan((i + 1).toString())}. ${color.yellow(c.hash.slice(0, 7))} ${c.message}`);
}); });
if (log.all.length >= limit) { if (log.all.length >= limit) {
@@ -906,18 +910,18 @@ program.command('branch <c> [name]').alias('b')
.description('Branch from commit/index') .description('Branch from commit/index')
.action(async (c, name) => { .action(async (c, name) => {
await ensureRepo(); await ensureRepo();
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}`); } 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}`); }
catch (e) { handleError('Branch error', e); } catch (e) { handleError('Branch error', e); }
}); });
program.command('reset <c>').alias('r') program.command('reset <c>').alias('rs')
.description('Reset branch to commit/index') .description('Reset branch to commit/index')
.option('--hard','hard reset') .option('--hard', 'hard reset')
.action(async (c, optsCmd) => { .action(async (c, optsCmd) => {
await ensureRepo(); await ensureRepo();
try { try {
const sha = await resolveCommit(c); const sha = await resolveCommit(c);
const mode = optsCmd.hard? '--hard':'--soft'; const mode = optsCmd.hard ? '--hard' : '--soft';
const opts = getOpts(); const opts = getOpts();
if (!opts.yes) { if (!opts.yes) {
console.log(color.yellow(`About to run: git reset ${mode} ${sha}. Use --yes to confirm.`)); console.log(color.yellow(`About to run: git reset ${mode} ${sha}. Use --yes to confirm.`));
@@ -983,4 +987,731 @@ program.command('undo').alias('u')
} }
}); });
// ===== NEW INTELLIGENT COMMANDS =====
program.command('wip')
.description('Quick work-in-progress commit')
.action(async () => {
await ensureRepo();
try {
const status = await git.status();
if (status.files.length === 0) {
Progress.warning('No changes to commit');
return;
}
Progress.info('Staging all changes...');
await git.add('.');
const fileCount = status.files.length;
const message = `WIP: ${fileCount} file${fileCount > 1 ? 's' : ''} changed`;
await git.commit(message);
Progress.success(`Committed: "${message}"`);
Progress.tip('Use `g a` to amend this commit when ready, or `g undo` to undo');
} catch (e) {
handleError('WIP error', e);
}
});
program.command('today').alias('t')
.description('Show commits made today')
.action(async () => {
await ensureRepo();
try {
const commits = await gitAnalyzer.getTodayCommits();
if (commits.length === 0) {
console.log(color.dim('No commits today yet.'));
Progress.tip('Start your day with `g o` to commit and push!');
return;
}
console.log(color.bold(`📅 Today's Commits (${commits.length})\n`));
commits.forEach((commit, i) => {
console.log(gitAnalyzer.formatCommit(commit, i));
});
} catch (e) {
handleError('Today error', e);
}
});
program.command('stats')
.description('Your personal commit statistics')
.option('--days <n>', 'Number of days to analyze', '30')
.action(async (cmdOptions) => {
await ensureRepo();
try {
const days = parseInt(cmdOptions.days) || 30;
const intelligence = new Intelligence(git);
Progress.start('📊 Analyzing your commit history');
const stats = await intelligence.getCommitStats(days);
const patterns = await intelligence.analyzeCommitPatterns();
Progress.stop('');
if (!stats.hasData) {
Progress.info('Not enough commit history to analyze');
return;
}
console.log(color.bold(`\n📊 Your Git Stats (last ${days} days)\n`));
// Overview
console.log(color.cyan('Overview:'));
console.log(` Total commits: ${color.bold(stats.totalCommits.toString())}`);
console.log(` Days active: ${stats.daysActive}`);
console.log(` Average: ${stats.avgPerDay} commits/day`);
console.log(` Current streak: ${color.green(stats.currentStreak + ' days')}`);
if (stats.longestStreak > stats.currentStreak) {
console.log(` Longest streak: ${stats.longestStreak} days`);
}
// Commit types breakdown
if (patterns.usesConventional) {
console.log(`\n${color.cyan('Commit Types:')}`);
const types = stats.typeBreakdown;
const total = Object.values(types).reduce((a, b) => a + b, 0);
Object.entries(types).forEach(([type, count]) => {
if (count > 0) {
const pct = Math.round(count / total * 100);
const bar = '█'.repeat(Math.ceil(pct / 5)) + '░'.repeat(20 - Math.ceil(pct / 5));
console.log(` ${type.padEnd(8)} ${bar} ${pct}%`);
}
});
}
// Style insights
if (patterns.hasHistory) {
console.log(`\n${color.cyan('Your Style:')}`);
console.log(` Conventional commits: ${patterns.conventionalRatio}%`);
console.log(` Message style: ${patterns.style}`);
console.log(` Avg message length: ${patterns.avgMessageLength} chars`);
if (patterns.topScopes.length > 0) {
console.log(` Common scopes: ${patterns.topScopes.join(', ')}`);
}
}
Progress.showRandomTip();
} catch (e) {
handleError('Stats error', e);
}
});
program.command('review').alias('r')
.description('AI code review before committing')
.action(async () => {
await ensureRepo();
const opts = getOpts();
try {
let diff = await git.diff(['--cached', '--no-ext-diff']);
if (!diff.trim()) {
// Try unstaged changes
diff = await git.diff(['--no-ext-diff']);
if (!diff.trim()) {
Progress.warning('No changes to review');
return;
}
console.log(color.dim('(Reviewing unstaged changes)\n'));
} else {
console.log(color.dim('(Reviewing staged changes)\n'));
}
const intelligence = new Intelligence(git);
// Analyze complexity
const complexity = await gitAnalyzer.getChangeComplexity(diff);
console.log(color.bold('📋 Change Summary'));
console.log(` Complexity: ${complexity.emoji} ${complexity.complexity}`);
console.log(` Files: ${complexity.files}`);
console.log(` Changes: ${color.green('+' + complexity.additions)} ${color.red('-' + complexity.deletions)}`);
// Detect semantic changes
const semantic = await intelligence.detectSemanticChanges(diff);
if (semantic.labels.length > 0) {
console.log(`\n${color.bold('🔍 Detected Patterns')}`);
semantic.labels.forEach(label => {
console.log(` ${label}`);
});
}
// Get AI suggestions
if (opts.progressIndicators) Progress.start('🤖 Generating AI review');
const message = await aiProvider.generateCommitMessage(diff, { ...opts, body: true });
if (opts.progressIndicators) Progress.stop('');
console.log(`\n${color.bold('💬 Suggested Commit Message')}`);
console.log(` ${color.green(message.message || message)}`);
// Actionable next steps
console.log(`\n${color.bold('📌 Next Steps')}`);
console.log(` ${color.cyan('g o')} Commit and push with AI message`);
console.log(` ${color.cyan('g l')} Commit locally with AI message`);
console.log(` ${color.cyan('g int')} Interactive commit with options`);
} catch (e) {
handleError('Review error', e);
}
});
program.command('split')
.description('Suggest how to split a large changeset')
.action(async () => {
await ensureRepo();
try {
const status = await git.status();
if (status.files.length === 0) {
Progress.info('No changes to split');
return;
}
if (status.files.length < 5) {
Progress.info('Changeset is small enough - no need to split');
console.log(`\nYou have ${status.files.length} file${status.files.length > 1 ? 's' : ''} changed.`);
console.log(`Use ${color.cyan('g o')} to commit them all at once.`);
return;
}
const intelligence = new Intelligence(git);
const suggestions = await intelligence.suggestCommitSplit(status);
if (!suggestions) {
Progress.info('All changes look related - commit them together');
return;
}
console.log(color.bold(`\n📦 Suggested Commit Split\n`));
console.log(color.dim(`Your ${status.files.length} files could be split into ${suggestions.length} commits:\n`));
suggestions.forEach((group, i) => {
console.log(`${color.cyan((i + 1).toString())}. ${color.bold(group.message)}`);
group.files.slice(0, 5).forEach(file => {
const emoji = Intelligence.getFileEmoji(file);
console.log(` ${emoji} ${file}`);
});
if (group.files.length > 5) {
console.log(color.dim(` ... and ${group.files.length - 5} more`));
}
console.log();
});
console.log(color.bold('💡 How to split:'));
console.log(` 1. Stage specific files: ${color.cyan('git add <file>')}`);
console.log(` 2. Commit them: ${color.cyan('g l')} or ${color.cyan('g o')}`);
console.log(` 3. Repeat for remaining files`);
} catch (e) {
handleError('Split error', e);
}
});
// ===== WORKFLOW SHORTHAND COMMANDS =====
program.command('safe-pull').alias('sp')
.description('Safe pull: stash → pull → stash pop')
.action(async () => {
await ensureRepo();
try {
const status = await git.status();
const hasChanges = status.files.length > 0;
if (hasChanges) {
Progress.info('Stashing changes...');
await git.stash(['push', '-m', 'GIMS: auto-stash before pull']);
}
Progress.info('Pulling latest changes...');
await git.pull();
if (hasChanges) {
Progress.info('Restoring stashed changes...');
await git.stash(['pop']);
}
Progress.success('Safe pull complete');
} catch (e) {
handleError('Safe pull error', e);
}
});
program.command('main')
.description('Switch to main/master and pull latest')
.action(async () => {
await ensureRepo();
try {
// Detect main branch name
let mainBranch = 'main';
try {
await git.raw(['rev-parse', '--verify', 'main']);
} catch {
try {
await git.raw(['rev-parse', '--verify', 'master']);
mainBranch = 'master';
} catch {
Progress.error('No main or master branch found');
return;
}
}
const status = await git.status();
if (status.files.length > 0) {
Progress.warning('You have uncommitted changes. Commit or stash them first.');
console.log(`Tip: Use ${color.cyan('g sp')} to safe-pull with auto-stash`);
return;
}
Progress.info(`Switching to ${mainBranch}...`);
await git.checkout(mainBranch);
Progress.info('Pulling latest...');
await git.pull();
Progress.success(`On ${mainBranch} with latest changes`);
} catch (e) {
handleError('Main error', e);
}
});
program.command('unstage').alias('us')
.description('Unstage all staged files')
.action(async () => {
await ensureRepo();
try {
const status = await git.status();
if (status.staged.length === 0) {
Progress.info('Nothing is staged');
return;
}
await git.reset([]);
Progress.success(`Unstaged ${status.staged.length} file${status.staged.length > 1 ? 's' : ''}`);
} catch (e) {
handleError('Unstage error', e);
}
});
program.command('discard').alias('x')
.description('Discard all changes (with confirmation)')
.action(async () => {
await ensureRepo();
const opts = getOpts();
try {
const status = await git.status();
if (status.files.length === 0) {
Progress.info('No changes to discard');
return;
}
if (!opts.yes) {
console.log(color.yellow(`⚠️ About to discard ALL changes in ${status.files.length} file(s)`));
console.log(color.red('This cannot be undone!'));
console.log('Use --yes to confirm.');
return;
}
// Discard tracked file changes
await git.checkout(['--', '.']);
// Remove untracked files
if (status.not_added.length > 0) {
await git.clean('fd');
}
Progress.success('All changes discarded');
} catch (e) {
handleError('Discard error', e);
}
});
program.command('stash-save').alias('ss')
.description('Quick stash: stage all and stash')
.action(async () => {
await ensureRepo();
try {
const status = await git.status();
if (status.files.length === 0) {
Progress.info('No changes to stash');
return;
}
await git.add('.');
// Generate a descriptive stash name
const fileCount = status.files.length;
const timestamp = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
const message = `WIP: ${fileCount} file${fileCount > 1 ? 's' : ''} at ${timestamp}`;
await git.stash(['push', '-m', message]);
Progress.success(`Stashed: "${message}"`);
} catch (e) {
handleError('Stash save error', e);
}
});
program.command('stash-pop').alias('pop')
.description('Pop the latest stash')
.action(async () => {
await ensureRepo();
try {
const stashList = await git.stashList();
if (stashList.all.length === 0) {
Progress.info('No stashes to pop');
return;
}
const latestStash = stashList.all[0];
await git.stash(['pop']);
Progress.success(`Popped: "${latestStash.message}"`);
} catch (e) {
handleError('Stash pop error', e);
}
});
program.command('delete-branch').alias('del')
.description('Delete branch locally and remotely')
.argument('<branch>', 'Branch name to delete')
.action(async (branch) => {
await ensureRepo();
const opts = getOpts();
try {
const current = (await git.branch()).current;
if (branch === current) {
Progress.error(`Cannot delete current branch. Switch to another branch first.`);
return;
}
if (branch === 'main' || branch === 'master') {
Progress.error('Cannot delete main/master branch');
return;
}
if (!opts.yes) {
console.log(color.yellow(`⚠️ About to delete branch: ${branch}`));
console.log('This will delete both local and remote copies.');
console.log('Use --yes to confirm.');
return;
}
// Delete local
Progress.info('Deleting local branch...');
try {
await git.branch(['-D', branch]);
} catch (e) {
Progress.warning(`Local branch not found or already deleted`);
}
// Delete remote
Progress.info('Deleting remote branch...');
try {
await git.push(['origin', '--delete', branch]);
} catch (e) {
Progress.warning(`Remote branch not found or already deleted`);
}
Progress.success(`Deleted branch: ${branch}`);
} catch (e) {
handleError('Delete branch error', e);
}
});
program.command('cleanup').alias('clean')
.description('Remove local branches that no longer exist on remote')
.action(async () => {
await ensureRepo();
const opts = getOpts();
try {
Progress.info('Fetching and pruning...');
await git.fetch(['--prune']);
// Find gone branches
const branchOutput = await git.raw(['branch', '-vv']);
const goneBranches = branchOutput
.split('\n')
.filter(line => line.includes(': gone]'))
.map(line => line.trim().split(/\s+/)[0].replace('*', '').trim())
.filter(b => b && b !== 'main' && b !== 'master');
if (goneBranches.length === 0) {
Progress.success('No dead branches to clean up');
return;
}
console.log(color.bold(`\nFound ${goneBranches.length} dead branch(es):`));
goneBranches.forEach(b => console.log(` ${color.dim('•')} ${b}`));
if (!opts.yes) {
console.log(`\nUse ${color.cyan('g clean --yes')} to delete them`);
return;
}
for (const branch of goneBranches) {
try {
await git.branch(['-D', branch]);
Progress.success(`Deleted: ${branch}`);
} catch (e) {
Progress.warning(`Could not delete: ${branch}`);
}
}
Progress.success(`Cleaned up ${goneBranches.length} branch(es)`);
} catch (e) {
handleError('Cleanup error', e);
}
});
program.command('last')
.description('Show last commit details and diff')
.action(async () => {
await ensureRepo();
try {
const log = await git.log({ maxCount: 1 });
if (log.all.length === 0) {
Progress.info('No commits yet');
return;
}
const commit = log.all[0];
console.log(color.bold('\n📝 Last Commit\n'));
console.log(` ${color.yellow(commit.hash.substring(0, 7))} ${commit.message.split('\n')[0]}`);
console.log(` ${color.dim(`by ${commit.author_name}${new Date(commit.date).toLocaleString()}`)}`);
// Show diff stats
const diff = await git.diff(['HEAD~1', '--stat']);
if (diff.trim()) {
console.log(`\n${color.bold('Changes:')}`);
console.log(color.dim(diff));
}
} catch (e) {
handleError('Last error', e);
}
});
// ===== SMART SYNC FIX COMMAND =====
program.command('fix')
.description('Smart fix for branch sync issues (diverged, behind, ahead)')
.option('--local', 'Keep local changes, force push to remote')
.option('--remote', 'Keep remote changes, discard local')
.option('--merge', 'Merge remote into local')
.option('--rebase', 'Rebase local onto remote')
.option('--ai', 'Get AI recommendation for best approach')
.action(async (cmdOptions) => {
await ensureRepo();
const opts = getOpts();
try {
// Fetch latest
Progress.info('Fetching remote status...');
await git.fetch();
const branch = (await git.branch()).current;
const remoteBranch = `origin/${branch}`;
// Check if remote exists
let remoteExists = true;
try {
await git.raw(['rev-parse', '--verify', remoteBranch]);
} catch {
remoteExists = false;
}
if (!remoteExists) {
console.log(color.bold(`\n📍 Branch Status: ${color.cyan(branch)}\n`));
console.log(`Remote branch ${color.yellow(remoteBranch)} doesn't exist yet.`);
console.log(`\nOptions:`);
console.log(` ${color.cyan('g push --set-upstream')} Push and create remote branch`);
return;
}
// Get ahead/behind counts
const ahead = parseInt((await git.raw(['rev-list', '--count', `${remoteBranch}..${branch}`])).trim());
const behind = parseInt((await git.raw(['rev-list', '--count', `${branch}..${remoteBranch}`])).trim());
// Get local changes status
const status = await git.status();
const hasLocalChanges = status.files.length > 0;
console.log(color.bold(`\n📍 Branch Status: ${color.cyan(branch)}\n`));
// Determine situation
let situation = '';
if (ahead === 0 && behind === 0) {
Progress.success('Branch is up to date with remote!');
if (hasLocalChanges) {
console.log(`\nYou have ${status.files.length} uncommitted change(s).`);
console.log(`Use ${color.cyan('g o')} to commit and push them.`);
}
return;
} else if (ahead > 0 && behind === 0) {
situation = 'ahead';
console.log(` ${color.green('↑')} ${ahead} commit(s) ahead of remote`);
console.log(` ${color.dim('Your local has commits not on remote')}\n`);
} else if (ahead === 0 && behind > 0) {
situation = 'behind';
console.log(` ${color.yellow('↓')} ${behind} commit(s) behind remote`);
console.log(` ${color.dim('Remote has commits you don\'t have')}\n`);
} else {
situation = 'diverged';
console.log(` ${color.red('⚡')} Branch has diverged!`);
console.log(` ${color.green('↑')} ${ahead} commit(s) ahead`);
console.log(` ${color.yellow('↓')} ${behind} commit(s) behind\n`);
}
if (hasLocalChanges) {
console.log(color.yellow(`⚠️ You have ${status.files.length} uncommitted file(s)`));
console.log(`${color.dim('Commit or stash them before fixing sync issues')}\n`);
}
// If specific option provided, execute it
if (cmdOptions.local) {
if (!opts.yes) {
console.log(color.red('⚠️ This will FORCE PUSH and overwrite remote!'));
console.log(`Use ${color.cyan('g fix --local --yes')} to confirm.`);
return;
}
Progress.info('Force pushing local to remote...');
await git.push(['--force']);
Progress.success('Force pushed! Remote now matches local.');
return;
}
if (cmdOptions.remote) {
if (!opts.yes) {
console.log(color.red('⚠️ This will DISCARD local commits!'));
console.log(`Use ${color.cyan('g fix --remote --yes')} to confirm.`);
return;
}
Progress.info('Resetting to remote...');
await git.reset(['--hard', remoteBranch]);
Progress.success('Reset! Local now matches remote.');
return;
}
if (cmdOptions.merge) {
Progress.info('Merging remote into local...');
try {
await git.merge([remoteBranch]);
Progress.success('Merged successfully!');
} catch (e) {
Progress.error('Merge conflict! Resolve conflicts then run: g o');
}
return;
}
if (cmdOptions.rebase) {
Progress.info('Rebasing local onto remote...');
try {
await git.rebase([remoteBranch]);
Progress.success('Rebased successfully!');
console.log(`Now run ${color.cyan('g push --force')} to update remote.`);
} catch (e) {
Progress.error('Rebase conflict! Resolve conflicts then run: git rebase --continue');
}
return;
}
// AI recommendation
if (cmdOptions.ai) {
Progress.info('Analyzing best approach...');
let recommendation = '';
if (situation === 'ahead') {
recommendation = `Your local is ${ahead} commits ahead. Simply push to sync.`;
console.log(`\n${color.bold('🤖 AI Recommendation:')}`);
console.log(` ${recommendation}`);
console.log(`\n Run: ${color.cyan('g push')}`);
} else if (situation === 'behind') {
recommendation = `Remote has ${behind} new commits. Pull to get them.`;
console.log(`\n${color.bold('🤖 AI Recommendation:')}`);
console.log(` ${recommendation}`);
console.log(`\n Run: ${color.cyan('g pull')} or ${color.cyan('g sp')} (if you have changes)`);
} else {
// Diverged - more complex
if (ahead <= 2 && behind > ahead) {
recommendation = `Small local changes (${ahead}), larger remote (${behind}). Rebase recommended for clean history.`;
console.log(`\n${color.bold('🤖 AI Recommendation:')}`);
console.log(` ${recommendation}`);
console.log(`\n Run: ${color.cyan('g fix --rebase')}`);
} else if (behind <= 2 && ahead > behind) {
recommendation = `Large local changes (${ahead}), small remote (${behind}). Merge is safe.`;
console.log(`\n${color.bold('🤖 AI Recommendation:')}`);
console.log(` ${recommendation}`);
console.log(`\n Run: ${color.cyan('g fix --merge')}`);
} else {
recommendation = `Significant divergence. Review changes first, then choose merge or rebase.`;
console.log(`\n${color.bold('🤖 AI Recommendation:')}`);
console.log(` ${recommendation}`);
console.log(`\n View remote changes: ${color.cyan(`git log ${branch}..${remoteBranch} --oneline`)}`);
console.log(` View local changes: ${color.cyan(`git log ${remoteBranch}..${branch} --oneline`)}`);
}
}
return;
}
// Show interactive menu
console.log(color.bold('🔧 Fix Options:\n'));
if (situation === 'ahead') {
console.log(` ${color.cyan('g push')} Push your commits to remote`);
} else if (situation === 'behind') {
console.log(` ${color.cyan('g pull')} Get remote commits (fast-forward)`);
console.log(` ${color.cyan('g sp')} Safe pull (stash → pull → pop)`);
} else {
// Diverged
console.log(` ${color.cyan('g fix --merge')} Merge remote into local (creates merge commit)`);
console.log(` ${color.cyan('g fix --rebase')} Rebase local onto remote (linear history)`);
console.log(color.dim(' ─────────────────'));
console.log(` ${color.cyan('g fix --local')} ${color.yellow('⚠')} Force push local, overwrite remote`);
console.log(` ${color.cyan('g fix --remote')} ${color.yellow('⚠')} Reset to remote, discard local commits`);
}
console.log(color.dim(' ─────────────────'));
console.log(` ${color.cyan('g fix --ai')} Get AI recommendation`);
} catch (e) {
handleError('Fix error', e);
}
});
program.command('conflicts')
.description('Show and help resolve merge conflicts')
.action(async () => {
await ensureRepo();
try {
const status = await git.status();
const conflicts = status.conflicted || [];
if (conflicts.length === 0) {
Progress.success('No merge conflicts!');
return;
}
console.log(color.bold(`\n⚠️ ${conflicts.length} Conflicted File(s):\n`));
conflicts.forEach((file, i) => {
const emoji = Intelligence.getFileEmoji(file);
console.log(` ${i + 1}. ${emoji} ${color.red(file)}`);
});
console.log(color.bold('\n🔧 How to resolve:\n'));
console.log(` 1. Edit each file and resolve the conflict markers`);
console.log(` ${color.dim('<<<<<<< HEAD')}`);
console.log(` ${color.dim('your changes')}`);
console.log(` ${color.dim('=======')}`);
console.log(` ${color.dim('their changes')}`);
console.log(` ${color.dim('>>>>>>> branch')}`);
console.log(` 2. Stage resolved files: ${color.cyan('git add <file>')}`);
console.log(` 3. Complete the merge: ${color.cyan('g o')} or ${color.cyan('git merge --continue')}`);
console.log(color.bold('\n⚡ Quick options:\n'));
console.log(` ${color.cyan('git checkout --ours <file>')} Keep YOUR version`);
console.log(` ${color.cyan('git checkout --theirs <file>')} Keep THEIR version`);
} catch (e) {
handleError('Conflicts error', e);
}
});
program.parse(process.argv); program.parse(process.argv);
+5 -3
View File
@@ -266,9 +266,11 @@ class AIProviderManager {
for (let i = 0; i < Math.min(count, variants.length); i++) { for (let i = 0; i < Math.min(count, variants.length); i++) {
try { try {
const suggestion = await this.generateCommitMessage(diff, variants[i]); const result = await this.generateCommitMessage(diff, variants[i]);
if (!suggestions.includes(suggestion)) { // Extract message string from result object
suggestions.push(suggestion); const message = result.message || result;
if (message && !suggestions.includes(message)) {
suggestions.push(message);
} }
} catch (error) { } catch (error) {
// Skip failed generations // Skip failed generations
+83 -12
View File
@@ -1,4 +1,5 @@
const { color } = require('../utils/colors'); const { color } = require('../utils/colors');
const { Intelligence } = require('../utils/intelligence');
/** /**
* Enhanced git analysis and insights * Enhanced git analysis and insights
@@ -6,17 +7,22 @@ const { color } = require('../utils/colors');
class GitAnalyzer { class GitAnalyzer {
constructor(git) { constructor(git) {
this.git = git; this.git = git;
this.intelligence = new Intelligence(git);
} }
async getEnhancedStatus() { async getEnhancedStatus() {
try { try {
const status = await this.git.status(); const status = await this.git.status();
const insights = await this.generateStatusInsights(status); const insights = await this.generateStatusInsights(status);
const sessionStats = await this.intelligence.getSessionStats();
const branchContext = await this.intelligence.detectBranchContext();
return { return {
...status, ...status,
insights, insights,
summary: this.generateStatusSummary(status) summary: this.generateStatusSummary(status),
sessionStats,
branchContext,
}; };
} catch (error) { } catch (error) {
throw new Error(`Failed to get git status: ${error.message}`); throw new Error(`Failed to get git status: ${error.message}`);
@@ -51,6 +57,7 @@ class GitAnalyzer {
const created = Array.isArray(status.created) ? status.created : []; const created = Array.isArray(status.created) ? status.created : [];
const deleted = Array.isArray(status.deleted) ? status.deleted : []; const deleted = Array.isArray(status.deleted) ? status.deleted : [];
const files = Array.isArray(status.files) ? status.files : []; const files = Array.isArray(status.files) ? status.files : [];
const staged = Array.isArray(status.staged) ? status.staged : [];
// Check for common patterns // Check for common patterns
if (modified.some(f => String(f).includes('package.json'))) { if (modified.some(f => String(f).includes('package.json'))) {
@@ -70,7 +77,7 @@ class GitAnalyzer {
} }
if (files.length > 20) { if (files.length > 20) {
insights.push('📊 Large changeset - consider breaking into smaller commits'); insights.push('📊 Large changeset - consider using `g split` to break into smaller commits');
} }
// Check for test files // Check for test files
@@ -91,6 +98,11 @@ class GitAnalyzer {
insights.push('⚙️ Configuration changes detected'); insights.push('⚙️ Configuration changes detected');
} }
// Suggest staging if nothing staged
if (staged.length === 0 && files.length > 0) {
insights.push('💡 Nothing staged yet - use `g o --all` to stage and commit everything');
}
return insights; return insights;
} }
@@ -99,6 +111,10 @@ class GitAnalyzer {
const log = await this.git.log({ maxCount: limit }); const log = await this.git.log({ maxCount: limit });
const commits = log.all; const commits = log.all;
if (commits.length === 0) {
return { totalCommits: 0 };
}
const analysis = { const analysis = {
totalCommits: commits.length, totalCommits: commits.length,
authors: [...new Set(commits.map(c => c.author_name))], authors: [...new Set(commits.map(c => c.author_name))],
@@ -135,14 +151,18 @@ class GitAnalyzer {
const files = (diff.match(/diff --git/g) || []).length; const files = (diff.match(/diff --git/g) || []).length;
let complexity = 'simple'; let complexity = 'simple';
let emoji = '🟢';
if (files > 10 || additions + deletions > 500) { if (files > 10 || additions + deletions > 500) {
complexity = 'complex'; complexity = 'complex';
emoji = '🔴';
} else if (files > 5 || additions + deletions > 100) { } else if (files > 5 || additions + deletions > 100) {
complexity = 'moderate'; complexity = 'moderate';
emoji = '🟡';
} }
return { return {
complexity, complexity,
emoji,
files, files,
additions, additions,
deletions, deletions,
@@ -159,20 +179,40 @@ class GitAnalyzer {
deleted = [], deleted = [],
not_added = [], not_added = [],
insights = [], insights = [],
summary = 'Unknown status' summary = 'Unknown status',
sessionStats = {},
branchContext = {},
} = enhancedStatus; } = enhancedStatus;
let output = ''; let output = '';
// Header // Header with branch info
output += `${color.bold('Git Status')}\n`; output += `${color.bold('Git Status')}`;
output += `${color.dim(summary)}\n\n`; if (branchContext.branch) {
output += ` ${color.dim('on')} ${color.cyan(branchContext.branch)}`;
if (branchContext.type) {
output += ` ${color.dim(`(${branchContext.type})`)}`;
}
}
output += '\n';
output += `${color.dim(summary)}\n`;
// Session stats
if (sessionStats.timeSinceLastCommit) {
output += `${color.dim(`Last commit: ${sessionStats.timeSinceLastCommit}`)}`;
if (sessionStats.commitsToday > 0) {
output += `${color.dim(`${sessionStats.commitsToday} commit${sessionStats.commitsToday > 1 ? 's' : ''} today`)}`;
}
output += '\n';
}
output += '\n';
// Staged changes // Staged changes
if (staged.length > 0) { if (staged.length > 0) {
output += `${color.green('Staged for commit:')}\n`; output += `${color.green('Staged for commit:')}\n`;
staged.forEach(file => { staged.forEach(file => {
output += ` ${color.green('+')} ${file}\n`; const emoji = Intelligence.getFileEmoji(file);
output += ` ${color.green('+')} ${emoji} ${file}\n`;
}); });
output += '\n'; output += '\n';
} }
@@ -181,7 +221,8 @@ class GitAnalyzer {
if (modified.length > 0) { if (modified.length > 0) {
output += `${color.yellow('Modified (not staged):')}\n`; output += `${color.yellow('Modified (not staged):')}\n`;
modified.forEach(file => { modified.forEach(file => {
output += ` ${color.yellow('M')} ${file}\n`; const emoji = Intelligence.getFileEmoji(file);
output += ` ${color.yellow('M')} ${emoji} ${file}\n`;
}); });
output += '\n'; output += '\n';
} }
@@ -190,7 +231,8 @@ class GitAnalyzer {
if (created.length > 0) { if (created.length > 0) {
output += `${color.cyan('New files:')}\n`; output += `${color.cyan('New files:')}\n`;
created.forEach(file => { created.forEach(file => {
output += ` ${color.cyan('N')} ${file}\n`; const emoji = Intelligence.getFileEmoji(file);
output += ` ${color.cyan('N')} ${emoji} ${file}\n`;
}); });
output += '\n'; output += '\n';
} }
@@ -199,7 +241,8 @@ class GitAnalyzer {
if (deleted.length > 0) { if (deleted.length > 0) {
output += `${color.red('Deleted:')}\n`; output += `${color.red('Deleted:')}\n`;
deleted.forEach(file => { deleted.forEach(file => {
output += ` ${color.red('D')} ${file}\n`; const emoji = Intelligence.getFileEmoji(file);
output += ` ${color.red('D')} ${emoji} ${file}\n`;
}); });
output += '\n'; output += '\n';
} }
@@ -208,7 +251,8 @@ class GitAnalyzer {
if (not_added.length > 0) { if (not_added.length > 0) {
output += `${color.dim('Untracked files:')}\n`; output += `${color.dim('Untracked files:')}\n`;
not_added.slice(0, 10).forEach(file => { not_added.slice(0, 10).forEach(file => {
output += ` ${color.dim('?')} ${file}\n`; const emoji = Intelligence.getFileEmoji(file);
output += ` ${color.dim('?')} ${emoji} ${file}\n`;
}); });
if (not_added.length > 10) { if (not_added.length > 10) {
output += ` ${color.dim(`... and ${not_added.length - 10} more`)}\n`; output += ` ${color.dim(`... and ${not_added.length - 10} more`)}\n`;
@@ -218,7 +262,7 @@ class GitAnalyzer {
// AI Insights // AI Insights
if (insights.length > 0) { if (insights.length > 0) {
output += `${color.cyan('💡 AI Insights:')}\n`; output += `${color.cyan('💡 Insights:')}\n`;
insights.forEach(insight => { insights.forEach(insight => {
output += ` ${insight}\n`; output += ` ${insight}\n`;
}); });
@@ -226,6 +270,33 @@ class GitAnalyzer {
return output; return output;
} }
/**
* Get today's commits formatted nicely
*/
async getTodayCommits() {
try {
const today = new Date();
today.setHours(0, 0, 0, 0);
const log = await this.git.log({ '--since': today.toISOString() });
return log.all;
} catch (error) {
return [];
}
}
/**
* Format commit for display
*/
formatCommit(commit, index) {
const hash = color.yellow(commit.hash.substring(0, 7));
const message = commit.message.split('\n')[0];
const time = new Date(commit.date).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
return ` ${color.dim(`${index + 1}.`)} ${hash} ${message} ${color.dim(`(${time})`)}`;
}
} }
module.exports = { GitAnalyzer }; module.exports = { GitAnalyzer };
+15
View File
@@ -8,8 +8,23 @@ const color = {
cyan: (s) => `\x1b[36m${s}\x1b[0m`, cyan: (s) => `\x1b[36m${s}\x1b[0m`,
blue: (s) => `\x1b[34m${s}\x1b[0m`, blue: (s) => `\x1b[34m${s}\x1b[0m`,
magenta: (s) => `\x1b[35m${s}\x1b[0m`, magenta: (s) => `\x1b[35m${s}\x1b[0m`,
white: (s) => `\x1b[37m${s}\x1b[0m`,
gray: (s) => `\x1b[90m${s}\x1b[0m`,
bold: (s) => `\x1b[1m${s}\x1b[0m`, bold: (s) => `\x1b[1m${s}\x1b[0m`,
dim: (s) => `\x1b[2m${s}\x1b[0m`, dim: (s) => `\x1b[2m${s}\x1b[0m`,
italic: (s) => `\x1b[3m${s}\x1b[0m`,
underline: (s) => `\x1b[4m${s}\x1b[0m`,
// Background colors
bgGreen: (s) => `\x1b[42m\x1b[30m${s}\x1b[0m`,
bgYellow: (s) => `\x1b[43m\x1b[30m${s}\x1b[0m`,
bgRed: (s) => `\x1b[41m\x1b[37m${s}\x1b[0m`,
bgCyan: (s) => `\x1b[46m\x1b[30m${s}\x1b[0m`,
bgBlue: (s) => `\x1b[44m\x1b[37m${s}\x1b[0m`,
// Compound styles
success: (s) => `\x1b[32m✓\x1b[0m ${s}`,
warning: (s) => `\x1b[33m⚠\x1b[0m ${s}`,
error: (s) => `\x1b[31m✗\x1b[0m ${s}`,
info: (s) => `\x1b[36m\x1b[0m ${s}`,
reset: '\x1b[0m' reset: '\x1b[0m'
}; };
+421
View File
@@ -0,0 +1,421 @@
const fs = require('fs');
const path = require('path');
/**
* Intelligence utilities for smart git operations
*/
class Intelligence {
constructor(git) {
this.git = git;
}
/**
* Get file type emoji based on file extension
*/
static getFileEmoji(filename) {
const ext = path.extname(filename).toLowerCase();
const name = path.basename(filename).toLowerCase();
// Special files
if (name === 'package.json' || name === 'package-lock.json') return '📦';
if (name === 'readme.md' || name === 'readme') return '📚';
if (name === '.gitignore') return '🙈';
if (name === '.env' || name.startsWith('.env.')) return '🔐';
if (name === 'dockerfile' || name === 'docker-compose.yml') return '🐳';
if (name.includes('config') || name.includes('rc')) return '⚙️';
if (name === 'license' || name === 'license.md') return '📜';
// Test files
if (filename.includes('.test.') || filename.includes('.spec.') || filename.includes('__tests__')) return '🧪';
// By extension
const emojiMap = {
'.js': '📄', '.jsx': '⚛️', '.ts': '📘', '.tsx': '⚛️',
'.css': '🎨', '.scss': '🎨', '.sass': '🎨', '.less': '🎨',
'.html': '🌐', '.htm': '🌐',
'.json': '📋', '.yaml': '📋', '.yml': '📋', '.toml': '📋',
'.md': '📝', '.mdx': '📝', '.txt': '📄',
'.py': '🐍', '.rb': '💎', '.go': '🔷', '.rs': '🦀', '.java': '☕',
'.sh': '🐚', '.bash': '🐚', '.zsh': '🐚',
'.sql': '🗃️', '.graphql': '🔗', '.gql': '🔗',
'.png': '🖼️', '.jpg': '🖼️', '.jpeg': '🖼️', '.gif': '🖼️', '.svg': '🎯',
'.mp3': '🎵', '.wav': '🎵', '.mp4': '🎬', '.mov': '🎬',
'.zip': '📦', '.tar': '📦', '.gz': '📦',
'.lock': '🔒',
};
return emojiMap[ext] || '📄';
}
/**
* Analyze user's commit patterns from history
*/
async analyzeCommitPatterns(limit = 50) {
try {
const log = await this.git.log({ maxCount: limit });
const commits = log.all;
if (commits.length === 0) {
return { hasHistory: false };
}
// Analyze patterns
const conventionalPattern = /^(feat|fix|docs|style|refactor|test|chore|perf|build|ci|revert)(\(.+\))?:/i;
const conventionalCommits = commits.filter(c => conventionalPattern.test(c.message));
// Calculate average message length
const avgLength = Math.round(commits.reduce((sum, c) => sum + c.message.split('\n')[0].length, 0) / commits.length);
// Detect common scopes
const scopes = {};
commits.forEach(c => {
const match = c.message.match(/^\w+\(([^)]+)\)/);
if (match) {
scopes[match[1]] = (scopes[match[1]] || 0) + 1;
}
});
const topScopes = Object.entries(scopes)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([scope]) => scope);
// Detect if user uses emojis
const emojiPattern = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]/u;
const usesEmojis = commits.some(c => emojiPattern.test(c.message));
// Detect commit message style (imperative vs past tense)
const imperativeWords = ['add', 'fix', 'update', 'remove', 'change', 'implement', 'refactor'];
const pastTenseWords = ['added', 'fixed', 'updated', 'removed', 'changed', 'implemented', 'refactored'];
let imperativeCount = 0;
let pastTenseCount = 0;
commits.forEach(c => {
const firstWord = c.message.split(/[\s(:]/)[0].toLowerCase();
if (imperativeWords.includes(firstWord)) imperativeCount++;
if (pastTenseWords.includes(firstWord)) pastTenseCount++;
});
return {
hasHistory: true,
totalAnalyzed: commits.length,
conventionalRatio: (conventionalCommits.length / commits.length * 100).toFixed(0),
usesConventional: conventionalCommits.length > commits.length * 0.5,
avgMessageLength: avgLength,
topScopes,
usesEmojis,
style: imperativeCount >= pastTenseCount ? 'imperative' : 'past-tense',
authors: [...new Set(commits.map(c => c.author_name))],
};
} catch (error) {
return { hasHistory: false, error: error.message };
}
}
/**
* Detect semantic meaning of changes
*/
async detectSemanticChanges(diff) {
const changes = {
type: 'misc',
labels: [],
suggestions: [],
breakingChange: false,
};
if (!diff || !diff.trim()) return changes;
const lines = diff.split('\n');
const addedLines = lines.filter(l => l.startsWith('+') && !l.startsWith('+++'));
const removedLines = lines.filter(l => l.startsWith('-') && !l.startsWith('---'));
// Detect breaking changes
if (removedLines.some(l => l.includes('export ') && l.includes('function ')) ||
removedLines.some(l => l.includes('module.exports'))) {
changes.labels.push('⚠️ Potential breaking change');
changes.breakingChange = true;
}
// Detect new features
if (addedLines.some(l => l.includes('export ') && l.includes('function ')) ||
addedLines.some(l => l.includes('class ')) ||
addedLines.some(l => l.includes('async function '))) {
changes.labels.push('✨ New functionality');
changes.type = 'feat';
}
// Detect bug fixes
if (addedLines.some(l => l.includes('catch') || l.includes('try {')) ||
addedLines.some(l => l.includes('|| null') || l.includes('?? ') || l.includes('?.'))) {
changes.labels.push('🐛 Error handling');
if (changes.type === 'misc') changes.type = 'fix';
}
// Detect refactoring
if (addedLines.length > 20 && removedLines.length > 20 &&
Math.abs(addedLines.length - removedLines.length) < 10) {
changes.labels.push('♻️ Refactoring');
if (changes.type === 'misc') changes.type = 'refactor';
}
// Detect documentation
if (addedLines.some(l => l.includes('/**') || l.includes('* @') || l.includes('// '))) {
changes.labels.push('📝 Documentation');
if (changes.type === 'misc') changes.type = 'docs';
}
// Detect test changes
if (diff.includes('.test.') || diff.includes('.spec.') ||
addedLines.some(l => l.includes('describe(') || l.includes('it(') || l.includes('test('))) {
changes.labels.push('🧪 Tests');
if (changes.type === 'misc') changes.type = 'test';
}
// Detect dependency changes
if (diff.includes('package.json') && (diff.includes('"dependencies"') || diff.includes('"devDependencies"'))) {
changes.labels.push('📦 Dependencies');
}
// Detect style/formatting
if (addedLines.every(l => l.trim().length < 3 || l.includes(' ') || l === '+')) {
changes.labels.push('🎨 Formatting');
if (changes.type === 'misc') changes.type = 'style';
}
return changes;
}
/**
* Suggest how to split a large changeset
*/
async suggestCommitSplit(status) {
const files = status.files || [];
if (files.length < 5) return null;
const groups = {
tests: [],
config: [],
docs: [],
styles: [],
core: [],
};
files.forEach(file => {
const f = typeof file === 'string' ? file : file.path;
if (!f) return;
if (f.includes('.test.') || f.includes('.spec.') || f.includes('__tests__')) {
groups.tests.push(f);
} else if (f.includes('config') || f.endsWith('.json') || f.endsWith('.yml') || f.endsWith('.yaml') || f.includes('rc')) {
groups.config.push(f);
} else if (f.endsWith('.md') || f.includes('docs/') || f.includes('README')) {
groups.docs.push(f);
} else if (f.endsWith('.css') || f.endsWith('.scss') || f.endsWith('.sass')) {
groups.styles.push(f);
} else {
groups.core.push(f);
}
});
const suggestions = [];
if (groups.tests.length > 0) suggestions.push({ type: 'test', files: groups.tests, message: 'test: add/update tests' });
if (groups.config.length > 0) suggestions.push({ type: 'chore', files: groups.config, message: 'chore: update configuration' });
if (groups.docs.length > 0) suggestions.push({ type: 'docs', files: groups.docs, message: 'docs: update documentation' });
if (groups.styles.length > 0) suggestions.push({ type: 'style', files: groups.styles, message: 'style: update styles' });
if (groups.core.length > 0) suggestions.push({ type: 'feat', files: groups.core, message: 'feat: update core functionality' });
return suggestions.length > 1 ? suggestions : null;
}
/**
* Get session statistics
*/
async getSessionStats() {
try {
const log = await this.git.log({ maxCount: 1 });
const lastCommit = log.all[0];
let timeSinceLastCommit = 'No commits yet';
let lastCommitMessage = null;
if (lastCommit) {
const lastCommitDate = new Date(lastCommit.date);
const now = new Date();
const diffMs = now - lastCommitDate;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffDays > 0) {
timeSinceLastCommit = `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
} else if (diffHours > 0) {
timeSinceLastCommit = `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
} else if (diffMins > 0) {
timeSinceLastCommit = `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
} else {
timeSinceLastCommit = 'Just now';
}
lastCommitMessage = lastCommit.message.split('\n')[0];
}
// Get today's commits
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayLog = await this.git.log({ '--since': today.toISOString() });
const todayCommits = todayLog.all.length;
return {
timeSinceLastCommit,
lastCommitMessage,
commitsToday: todayCommits,
};
} catch (error) {
return {
timeSinceLastCommit: 'Unknown',
lastCommitMessage: null,
commitsToday: 0,
};
}
}
/**
* Detect branch context for smarter commit messages
*/
async detectBranchContext() {
try {
const branch = await this.git.branch();
const currentBranch = branch.current;
// Common patterns: feat/xyz, fix/xyz, feature/xyz, bugfix/xyz, hotfix/xyz
const patterns = [
{ regex: /^feat(?:ure)?\/(.+)$/i, type: 'feat', scope: null },
{ regex: /^fix\/(.+)$/i, type: 'fix', scope: null },
{ regex: /^bugfix\/(.+)$/i, type: 'fix', scope: null },
{ regex: /^hotfix\/(.+)$/i, type: 'fix', scope: null },
{ regex: /^docs?\/(.+)$/i, type: 'docs', scope: null },
{ regex: /^refactor\/(.+)$/i, type: 'refactor', scope: null },
{ regex: /^test\/(.+)$/i, type: 'test', scope: null },
{ regex: /^chore\/(.+)$/i, type: 'chore', scope: null },
{ regex: /^style\/(.+)$/i, type: 'style', scope: null },
];
for (const pattern of patterns) {
const match = currentBranch.match(pattern.regex);
if (match) {
// Extract potential scope and description from branch name
const branchPart = match[1].replace(/-/g, ' ').replace(/_/g, ' ');
return {
branch: currentBranch,
type: pattern.type,
description: branchPart,
detected: true,
};
}
}
// Check for issue references like PROJ-123 or #123
const issueMatch = currentBranch.match(/([A-Z]+-\d+|#\d+)/i);
if (issueMatch) {
return {
branch: currentBranch,
issueRef: issueMatch[1],
detected: true,
};
}
return {
branch: currentBranch,
detected: false,
};
} catch (error) {
return { branch: 'unknown', detected: false };
}
}
/**
* Get commit statistics for the user
*/
async getCommitStats(days = 30) {
try {
const since = new Date();
since.setDate(since.getDate() - days);
const log = await this.git.log({ '--since': since.toISOString() });
const commits = log.all;
if (commits.length === 0) {
return { totalCommits: 0, hasData: false };
}
// Group by day
const byDay = {};
commits.forEach(c => {
const day = new Date(c.date).toLocaleDateString();
byDay[day] = (byDay[day] || 0) + 1;
});
// Calculate streaks
const sortedDays = Object.keys(byDay).sort((a, b) => new Date(b) - new Date(a));
let currentStreak = 0;
let longestStreak = 0;
let tempStreak = 0;
const today = new Date().toLocaleDateString();
const yesterday = new Date(Date.now() - 86400000).toLocaleDateString();
// Check if today or yesterday has commits
if (byDay[today] || byDay[yesterday]) {
sortedDays.forEach((day, i) => {
if (i === 0) {
tempStreak = 1;
} else {
const prevDate = new Date(sortedDays[i - 1]);
const currDate = new Date(day);
const diffDays = Math.round((prevDate - currDate) / 86400000);
if (diffDays === 1) {
tempStreak++;
} else {
if (tempStreak > longestStreak) longestStreak = tempStreak;
tempStreak = 1;
}
}
});
if (tempStreak > longestStreak) longestStreak = tempStreak;
currentStreak = byDay[today] ? tempStreak : (byDay[yesterday] ? tempStreak : 0);
}
// Commit types breakdown
const typeBreakdown = { feat: 0, fix: 0, docs: 0, style: 0, refactor: 0, test: 0, chore: 0, other: 0 };
const conventionalPattern = /^(feat|fix|docs|style|refactor|test|chore|perf|build|ci|revert)/i;
commits.forEach(c => {
const match = c.message.match(conventionalPattern);
if (match) {
const type = match[1].toLowerCase();
if (typeBreakdown.hasOwnProperty(type)) {
typeBreakdown[type]++;
} else {
typeBreakdown.other++;
}
} else {
typeBreakdown.other++;
}
});
return {
hasData: true,
totalCommits: commits.length,
daysActive: Object.keys(byDay).length,
avgPerDay: (commits.length / days).toFixed(1),
currentStreak,
longestStreak,
typeBreakdown,
topDay: Object.entries(byDay).sort((a, b) => b[1] - a[1])[0],
};
} catch (error) {
return { hasData: false, error: error.message };
}
}
}
module.exports = { Intelligence };
+70 -1
View File
@@ -7,8 +7,11 @@ class Progress {
static spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; static spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
static current = 0; static current = 0;
static interval = null; static interval = null;
static startTime = null;
static start(message) { static start(message) {
this.startTime = Date.now();
this.lastMessage = message;
process.stdout.write(`${message} ${this.spinner[0]}`); process.stdout.write(`${message} ${this.spinner[0]}`);
this.interval = setInterval(() => { this.interval = setInterval(() => {
this.current = (this.current + 1) % this.spinner.length; this.current = (this.current + 1) % this.spinner.length;
@@ -21,7 +24,25 @@ class Progress {
clearInterval(this.interval); clearInterval(this.interval);
this.interval = null; this.interval = null;
} }
process.stdout.write(`\r${finalMessage}\n`); const elapsed = this.getElapsed();
const elapsedStr = elapsed ? ` ${color.dim(`(${elapsed})`)}` : '';
// Clear the entire line before writing final message
const clearLine = '\r\x1b[K'; // Carriage return + clear to end of line
if (finalMessage) {
process.stdout.write(`${clearLine}${finalMessage}${elapsedStr}\n`);
} else {
// Just clear the spinner line completely
process.stdout.write(`${clearLine}`);
}
this.startTime = null;
this.lastMessage = null;
}
static getElapsed() {
if (!this.startTime) return null;
const ms = Date.now() - this.startTime;
if (ms < 1000) return `${ms}ms`;
return `${(ms / 1000).toFixed(1)}s`;
} }
static success(message) { static success(message) {
@@ -44,6 +65,54 @@ class Progress {
const progress = `[${step}/${total}]`; const progress = `[${step}/${total}]`;
console.log(`${color.dim(progress)} ${message}`); console.log(`${color.dim(progress)} ${message}`);
} }
static cached(message) {
console.log(`${color.magenta('⚡')} ${message} ${color.dim('(cached)')}`);
}
static tip(message) {
console.log(`${color.blue('💡')} ${color.dim('Tip:')} ${message}`);
}
static box(title, lines) {
const maxLen = Math.max(title.length, ...lines.map(l => l.replace(/\x1b\[[0-9;]*m/g, '').length));
const top = `${'─'.repeat(maxLen + 2)}`;
const bottom = `${'─'.repeat(maxLen + 2)}`;
const titleLine = `${color.bold(title.padEnd(maxLen))}`;
console.log(top);
console.log(titleLine);
console.log(`${'─'.repeat(maxLen + 2)}`);
lines.forEach(line => {
const plainLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
const padding = ' '.repeat(maxLen - plainLen);
console.log(`${line}${padding}`);
});
console.log(bottom);
}
// Random tips for contextual help
static tips = [
'Use `g int` for interactive commit wizard with multiple AI suggestions',
'Use `g o` to commit and push in one command',
'Use `g sg --multiple` to get 3 different commit message suggestions',
'Configure your preferences with `g config --set key=value`',
'Use `--all` flag to auto-stage all changes',
'Use `g a` to amend your last commit',
'Use `g sync --rebase` for cleaner history',
'Set GEMINI_API_KEY for free AI-powered commits',
'Use `g wip` for quick work-in-progress commits',
'Use `g stats` to see your commit statistics',
];
static showRandomTip() {
const shouldShow = Math.random() < 0.15; // 15% chance
if (shouldShow) {
const tip = this.tips[Math.floor(Math.random() * this.tips.length)];
console.log();
this.tip(tip);
}
}
} }
module.exports = { Progress }; module.exports = { Progress };
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "gims", "name": "gims",
"version": "0.6.7", "version": "0.8.1",
"description": "Git Made Simple AIpowered git helper using Gemini / OpenAI", "description": "Git Made Simple AIpowered git helper with smart insights, stats & code review",
"author": "S41R4J", "author": "S41R4J",
"license": "MIT", "license": "MIT",
"bin": { "bin": {