version 0.8.1 release, stable release
This commit is contained in:
@@ -1,5 +1,85 @@
|
||||
# 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
|
||||
|
||||
### 🔧 Fixes
|
||||
|
||||
@@ -26,17 +26,36 @@ g o # AI commit + push
|
||||
|
||||
## 🎯 Main Commands
|
||||
|
||||
### 🧠 Smart Commands
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `g s` | Enhanced status with AI insights |
|
||||
| `g i` | Initialize git repository |
|
||||
| `g int` | Interactive commit wizard |
|
||||
| `g o` | AI commit + push |
|
||||
| `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 ll` | Commit history (detailed) |
|
||||
| `g h` | Commit history (alias for ls) |
|
||||
| `g a` | Amend previous commit (keeps message) |
|
||||
| `g a` | Amend previous commit |
|
||||
|
||||
## 🤖 AI Models
|
||||
|
||||
@@ -48,14 +67,15 @@ g o # AI commit + push
|
||||
|
||||
```bash
|
||||
# Daily workflow
|
||||
g s # Check what changed
|
||||
g int # Interactive commit
|
||||
g o # Quick commit + push
|
||||
g s # Check status
|
||||
g sp # Safe pull updates
|
||||
g fix # Fix any sync issues
|
||||
g o # Commit + push
|
||||
|
||||
# Advanced
|
||||
g sg --multiple # Get 3 AI suggestions
|
||||
g ll # Detailed history
|
||||
g sync --rebase # Smart sync
|
||||
# Power functions
|
||||
g r # Review code before commit
|
||||
g stats # Check your streak
|
||||
g split # Split big changesets
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
+834
-103
File diff suppressed because it is too large
Load Diff
+36
-34
@@ -18,7 +18,7 @@ class AIProviderManager {
|
||||
if (preference === 'openai') return process.env.OPENAI_API_KEY ? 'openai' : 'none';
|
||||
if (preference === 'gemini') return process.env.GEMINI_API_KEY ? 'gemini' : 'none';
|
||||
if (preference === 'groq') return process.env.GROQ_API_KEY ? 'groq' : 'none';
|
||||
|
||||
|
||||
// Auto-detection with preference order (Gemini first - fastest and cheapest)
|
||||
if (process.env.GEMINI_API_KEY) return 'gemini';
|
||||
if (process.env.OPENAI_API_KEY) return 'openai';
|
||||
@@ -48,12 +48,12 @@ class AIProviderManager {
|
||||
|
||||
setCache(cacheKey, result, usedLocal = false) {
|
||||
if (!this.config.cacheEnabled) return;
|
||||
|
||||
|
||||
if (this.cache.size >= this.maxCacheSize) {
|
||||
const firstKey = this.cache.keys().next().value;
|
||||
this.cache.delete(firstKey);
|
||||
}
|
||||
|
||||
|
||||
this.cache.set(cacheKey, {
|
||||
result,
|
||||
usedLocal,
|
||||
@@ -103,9 +103,9 @@ class AIProviderManager {
|
||||
}
|
||||
|
||||
async generateWithGroq(prompt, model, options) {
|
||||
const groq = new OpenAI({
|
||||
apiKey: process.env.GROQ_API_KEY,
|
||||
baseURL: process.env.GROQ_BASE_URL || 'https://api.groq.com/openai/v1'
|
||||
const groq = new OpenAI({
|
||||
apiKey: process.env.GROQ_API_KEY,
|
||||
baseURL: process.env.GROQ_BASE_URL || 'https://api.groq.com/openai/v1'
|
||||
});
|
||||
const actualModel = model || this.getDefaultModel('groq');
|
||||
const response = await groq.chat.completions.create({
|
||||
@@ -118,11 +118,11 @@ class AIProviderManager {
|
||||
}
|
||||
|
||||
async generateCommitMessage(diff, options = {}) {
|
||||
const {
|
||||
provider: preferredProvider = 'auto',
|
||||
conventional = false,
|
||||
const {
|
||||
provider: preferredProvider = 'auto',
|
||||
conventional = false,
|
||||
body = false,
|
||||
verbose = false
|
||||
verbose = false
|
||||
} = options;
|
||||
|
||||
// Check cache first
|
||||
@@ -134,11 +134,11 @@ class AIProviderManager {
|
||||
}
|
||||
|
||||
const providerChain = this.buildProviderChain(preferredProvider);
|
||||
|
||||
|
||||
for (const provider of providerChain) {
|
||||
try {
|
||||
if (verbose) Progress.info(`Trying provider: ${provider}`);
|
||||
|
||||
|
||||
if (provider === 'local') {
|
||||
const result = await this.generateLocalHeuristic(diff, options);
|
||||
this.setCache(cacheKey, result, true);
|
||||
@@ -148,10 +148,10 @@ class AIProviderManager {
|
||||
const prompt = this.buildPrompt(diff, { conventional, body });
|
||||
const result = await this.generateWithProvider(provider, prompt, options);
|
||||
const cleaned = this.cleanCommitMessage(result, { body });
|
||||
|
||||
|
||||
this.setCache(cacheKey, cleaned, false);
|
||||
return { message: cleaned, usedLocal: false };
|
||||
|
||||
|
||||
} catch (error) {
|
||||
if (verbose) Progress.warning(`${provider} failed: ${error.message}`);
|
||||
continue;
|
||||
@@ -166,7 +166,7 @@ class AIProviderManager {
|
||||
|
||||
buildProviderChain(preferred) {
|
||||
const available = [];
|
||||
|
||||
|
||||
if (preferred !== 'auto' && preferred !== 'none') {
|
||||
const resolved = this.resolveProvider(preferred);
|
||||
if (resolved !== 'none') available.push(resolved);
|
||||
@@ -175,28 +175,28 @@ class AIProviderManager {
|
||||
if (process.env.OPENAI_API_KEY) available.push('openai');
|
||||
if (process.env.GROQ_API_KEY) available.push('groq');
|
||||
}
|
||||
|
||||
|
||||
available.push('local');
|
||||
return [...new Set(available)];
|
||||
}
|
||||
|
||||
buildPrompt(diff, options) {
|
||||
const { conventional, body } = options;
|
||||
|
||||
const style = conventional
|
||||
|
||||
const style = conventional
|
||||
? 'Use Conventional Commits format (e.g., feat:, fix:, chore:) for the subject.'
|
||||
: 'Subject must be a single short line.';
|
||||
|
||||
const bodyInstr = body
|
||||
|
||||
const bodyInstr = body
|
||||
? 'Provide a short subject line followed by an optional body separated by a blank line.'
|
||||
: 'Return only a short subject line without extra quotes.';
|
||||
|
||||
|
||||
return `Write a concise git commit message for these changes:\n${diff}\n\n${style} ${bodyInstr}`;
|
||||
}
|
||||
|
||||
cleanCommitMessage(message, options = {}) {
|
||||
if (!message) return 'Update project code';
|
||||
|
||||
|
||||
// Remove markdown formatting
|
||||
let cleaned = message
|
||||
.replace(/```[\s\S]*?```/g, '')
|
||||
@@ -212,7 +212,7 @@ class AIProviderManager {
|
||||
|
||||
const lines = cleaned.split('\n').map(l => l.trim()).filter(Boolean);
|
||||
let subject = (lines[0] || '').replace(/\s{2,}/g, ' ').replace(/[\s:,.!;]+$/g, '').trim();
|
||||
|
||||
|
||||
if (subject.length === 0) subject = 'Update project code';
|
||||
// No length restriction - allow AI to generate full commit messages
|
||||
|
||||
@@ -226,16 +226,16 @@ class AIProviderManager {
|
||||
async generateLocalHeuristic(diff, options) {
|
||||
// This would need access to git status - simplified version
|
||||
const { conventional = false } = options;
|
||||
|
||||
|
||||
// Analyze diff for patterns
|
||||
const lines = diff.split('\n');
|
||||
const additions = lines.filter(l => l.startsWith('+')).length;
|
||||
const deletions = lines.filter(l => l.startsWith('-')).length;
|
||||
const files = (diff.match(/diff --git/g) || []).length;
|
||||
|
||||
|
||||
let type = 'chore';
|
||||
let subject = 'update files';
|
||||
|
||||
|
||||
if (additions > deletions * 2) {
|
||||
type = 'feat';
|
||||
subject = files === 1 ? 'add new functionality' : `add features to ${files} files`;
|
||||
@@ -249,37 +249,39 @@ class AIProviderManager {
|
||||
type = 'docs';
|
||||
subject = 'update documentation';
|
||||
}
|
||||
|
||||
|
||||
return conventional ? `${type}: ${subject}` : subject.charAt(0).toUpperCase() + subject.slice(1);
|
||||
}
|
||||
|
||||
async generateMultipleSuggestions(diff, options = {}, count = 3) {
|
||||
const suggestions = [];
|
||||
const baseOptions = { ...options };
|
||||
|
||||
|
||||
// Generate different styles
|
||||
const variants = [
|
||||
{ ...baseOptions, conventional: false },
|
||||
{ ...baseOptions, conventional: true },
|
||||
{ ...baseOptions, conventional: true, body: true }
|
||||
];
|
||||
|
||||
|
||||
for (let i = 0; i < Math.min(count, variants.length); i++) {
|
||||
try {
|
||||
const suggestion = await this.generateCommitMessage(diff, variants[i]);
|
||||
if (!suggestions.includes(suggestion)) {
|
||||
suggestions.push(suggestion);
|
||||
const result = await this.generateCommitMessage(diff, variants[i]);
|
||||
// Extract message string from result object
|
||||
const message = result.message || result;
|
||||
if (message && !suggestions.includes(message)) {
|
||||
suggestions.push(message);
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip failed generations
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Ensure we have at least one suggestion
|
||||
if (suggestions.length === 0) {
|
||||
suggestions.push('Update project files');
|
||||
}
|
||||
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
}
|
||||
|
||||
+118
-47
@@ -1,4 +1,5 @@
|
||||
const { color } = require('../utils/colors');
|
||||
const { Intelligence } = require('../utils/intelligence');
|
||||
|
||||
/**
|
||||
* Enhanced git analysis and insights
|
||||
@@ -6,17 +7,22 @@ const { color } = require('../utils/colors');
|
||||
class GitAnalyzer {
|
||||
constructor(git) {
|
||||
this.git = git;
|
||||
this.intelligence = new Intelligence(git);
|
||||
}
|
||||
|
||||
async getEnhancedStatus() {
|
||||
try {
|
||||
const status = await this.git.status();
|
||||
const insights = await this.generateStatusInsights(status);
|
||||
|
||||
const sessionStats = await this.intelligence.getSessionStats();
|
||||
const branchContext = await this.intelligence.detectBranchContext();
|
||||
|
||||
return {
|
||||
...status,
|
||||
insights,
|
||||
summary: this.generateStatusSummary(status)
|
||||
summary: this.generateStatusSummary(status),
|
||||
sessionStats,
|
||||
branchContext,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get git status: ${error.message}`);
|
||||
@@ -32,47 +38,48 @@ class GitAnalyzer {
|
||||
const untracked = Array.isArray(status.not_added) ? status.not_added : [];
|
||||
|
||||
if (files.length === 0) return 'Working tree clean';
|
||||
|
||||
|
||||
const parts = [];
|
||||
if (staged.length > 0) parts.push(`${staged.length} staged`);
|
||||
if (modified.length > 0) parts.push(`${modified.length} modified`);
|
||||
if (created.length > 0) parts.push(`${created.length} new`);
|
||||
if (deleted.length > 0) parts.push(`${deleted.length} deleted`);
|
||||
if (untracked.length > 0) parts.push(`${untracked.length} untracked`);
|
||||
|
||||
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
async generateStatusInsights(status) {
|
||||
const insights = [];
|
||||
|
||||
|
||||
// Ensure arrays exist and have proper methods
|
||||
const modified = Array.isArray(status.modified) ? status.modified : [];
|
||||
const created = Array.isArray(status.created) ? status.created : [];
|
||||
const deleted = Array.isArray(status.deleted) ? status.deleted : [];
|
||||
const files = Array.isArray(status.files) ? status.files : [];
|
||||
|
||||
const staged = Array.isArray(status.staged) ? status.staged : [];
|
||||
|
||||
// Check for common patterns
|
||||
if (modified.some(f => String(f).includes('package.json'))) {
|
||||
insights.push('📦 Dependencies may have changed - consider updating package-lock.json');
|
||||
}
|
||||
|
||||
|
||||
if (created.some(f => String(f).includes('.env'))) {
|
||||
insights.push('🔐 New environment file detected - ensure it\'s in .gitignore');
|
||||
}
|
||||
|
||||
|
||||
if (modified.some(f => String(f).includes('README'))) {
|
||||
insights.push('📚 Documentation updated - good practice!');
|
||||
}
|
||||
|
||||
|
||||
if (deleted.length > created.length + modified.length) {
|
||||
insights.push('🧹 Cleanup operation detected - removing more than adding');
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
const testFiles = files.filter(f => {
|
||||
const fileName = String(f);
|
||||
@@ -81,7 +88,7 @@ class GitAnalyzer {
|
||||
if (testFiles.length > 0) {
|
||||
insights.push('🧪 Test files modified - great for code quality!');
|
||||
}
|
||||
|
||||
|
||||
// Check for config files
|
||||
const configFiles = files.filter(f => {
|
||||
const fileName = String(f);
|
||||
@@ -90,7 +97,12 @@ class GitAnalyzer {
|
||||
if (configFiles.length > 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -98,7 +110,11 @@ class GitAnalyzer {
|
||||
try {
|
||||
const log = await this.git.log({ maxCount: limit });
|
||||
const commits = log.all;
|
||||
|
||||
|
||||
if (commits.length === 0) {
|
||||
return { totalCommits: 0 };
|
||||
}
|
||||
|
||||
const analysis = {
|
||||
totalCommits: commits.length,
|
||||
authors: [...new Set(commits.map(c => c.author_name))],
|
||||
@@ -106,7 +122,7 @@ class GitAnalyzer {
|
||||
conventionalCommits: commits.filter(c => /^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?:/.test(c.message)).length,
|
||||
recentActivity: this.analyzeRecentActivity(commits)
|
||||
};
|
||||
|
||||
|
||||
return analysis;
|
||||
} catch (error) {
|
||||
return { error: error.message };
|
||||
@@ -117,10 +133,10 @@ class GitAnalyzer {
|
||||
const now = new Date();
|
||||
const oneDayAgo = new Date(now - 24 * 60 * 60 * 1000);
|
||||
const oneWeekAgo = new Date(now - 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
|
||||
const recentCommits = commits.filter(c => new Date(c.date) > oneDayAgo);
|
||||
const weeklyCommits = commits.filter(c => new Date(c.date) > oneWeekAgo);
|
||||
|
||||
|
||||
return {
|
||||
last24h: recentCommits.length,
|
||||
lastWeek: weeklyCommits.length,
|
||||
@@ -133,16 +149,20 @@ class GitAnalyzer {
|
||||
const additions = lines.filter(l => l.startsWith('+')).length;
|
||||
const deletions = lines.filter(l => l.startsWith('-')).length;
|
||||
const files = (diff.match(/diff --git/g) || []).length;
|
||||
|
||||
|
||||
let complexity = 'simple';
|
||||
let emoji = '🟢';
|
||||
if (files > 10 || additions + deletions > 500) {
|
||||
complexity = 'complex';
|
||||
emoji = '🔴';
|
||||
} else if (files > 5 || additions + deletions > 100) {
|
||||
complexity = 'moderate';
|
||||
emoji = '🟡';
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
complexity,
|
||||
emoji,
|
||||
files,
|
||||
additions,
|
||||
deletions,
|
||||
@@ -151,81 +171,132 @@ class GitAnalyzer {
|
||||
}
|
||||
|
||||
formatStatusOutput(enhancedStatus) {
|
||||
const {
|
||||
files = [],
|
||||
staged = [],
|
||||
modified = [],
|
||||
created = [],
|
||||
deleted = [],
|
||||
not_added = [],
|
||||
insights = [],
|
||||
summary = 'Unknown status'
|
||||
const {
|
||||
files = [],
|
||||
staged = [],
|
||||
modified = [],
|
||||
created = [],
|
||||
deleted = [],
|
||||
not_added = [],
|
||||
insights = [],
|
||||
summary = 'Unknown status',
|
||||
sessionStats = {},
|
||||
branchContext = {},
|
||||
} = enhancedStatus;
|
||||
|
||||
|
||||
let output = '';
|
||||
|
||||
// Header
|
||||
output += `${color.bold('Git Status')}\n`;
|
||||
output += `${color.dim(summary)}\n\n`;
|
||||
|
||||
|
||||
// Header with branch info
|
||||
output += `${color.bold('Git Status')}`;
|
||||
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
|
||||
if (staged.length > 0) {
|
||||
output += `${color.green('Staged for commit:')}\n`;
|
||||
staged.forEach(file => {
|
||||
output += ` ${color.green('+')} ${file}\n`;
|
||||
const emoji = Intelligence.getFileEmoji(file);
|
||||
output += ` ${color.green('+')} ${emoji} ${file}\n`;
|
||||
});
|
||||
output += '\n';
|
||||
}
|
||||
|
||||
|
||||
// Modified files
|
||||
if (modified.length > 0) {
|
||||
output += `${color.yellow('Modified (not staged):')}\n`;
|
||||
modified.forEach(file => {
|
||||
output += ` ${color.yellow('M')} ${file}\n`;
|
||||
const emoji = Intelligence.getFileEmoji(file);
|
||||
output += ` ${color.yellow('M')} ${emoji} ${file}\n`;
|
||||
});
|
||||
output += '\n';
|
||||
}
|
||||
|
||||
|
||||
// New files
|
||||
if (created.length > 0) {
|
||||
output += `${color.cyan('New files:')}\n`;
|
||||
created.forEach(file => {
|
||||
output += ` ${color.cyan('N')} ${file}\n`;
|
||||
const emoji = Intelligence.getFileEmoji(file);
|
||||
output += ` ${color.cyan('N')} ${emoji} ${file}\n`;
|
||||
});
|
||||
output += '\n';
|
||||
}
|
||||
|
||||
|
||||
// Deleted files
|
||||
if (deleted.length > 0) {
|
||||
output += `${color.red('Deleted:')}\n`;
|
||||
deleted.forEach(file => {
|
||||
output += ` ${color.red('D')} ${file}\n`;
|
||||
const emoji = Intelligence.getFileEmoji(file);
|
||||
output += ` ${color.red('D')} ${emoji} ${file}\n`;
|
||||
});
|
||||
output += '\n';
|
||||
}
|
||||
|
||||
|
||||
// Untracked files
|
||||
if (not_added.length > 0) {
|
||||
output += `${color.dim('Untracked files:')}\n`;
|
||||
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) {
|
||||
output += ` ${color.dim(`... and ${not_added.length - 10} more`)}\n`;
|
||||
}
|
||||
output += '\n';
|
||||
}
|
||||
|
||||
|
||||
// AI Insights
|
||||
if (insights.length > 0) {
|
||||
output += `${color.cyan('💡 AI Insights:')}\n`;
|
||||
output += `${color.cyan('💡 Insights:')}\n`;
|
||||
insights.forEach(insight => {
|
||||
output += ` ${insight}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
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 };
|
||||
@@ -8,8 +8,23 @@ const color = {
|
||||
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
||||
blue: (s) => `\x1b[34m${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`,
|
||||
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'
|
||||
};
|
||||
|
||||
|
||||
@@ -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 };
|
||||
@@ -7,8 +7,11 @@ class Progress {
|
||||
static spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
||||
static current = 0;
|
||||
static interval = null;
|
||||
static startTime = null;
|
||||
|
||||
static start(message) {
|
||||
this.startTime = Date.now();
|
||||
this.lastMessage = message;
|
||||
process.stdout.write(`${message} ${this.spinner[0]}`);
|
||||
this.interval = setInterval(() => {
|
||||
this.current = (this.current + 1) % this.spinner.length;
|
||||
@@ -21,7 +24,25 @@ class Progress {
|
||||
clearInterval(this.interval);
|
||||
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) {
|
||||
@@ -44,6 +65,54 @@ class Progress {
|
||||
const progress = `[${step}/${total}]`;
|
||||
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 };
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "gims",
|
||||
"version": "0.6.7",
|
||||
"description": "Git Made Simple – AI‑powered git helper using Gemini / OpenAI",
|
||||
"version": "0.8.1",
|
||||
"description": "Git Made Simple – AI‑powered git helper with smart insights, stats & code review",
|
||||
"author": "S41R4J",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -43,4 +43,4 @@
|
||||
"test": "echo \"Enhanced GIMS v$(node -p \"require('./package.json').version\") - All systems operational!\"",
|
||||
"postinstall": "echo \"🚀 GIMS installed! Quick start: 'g setup --api-key gemini' then 'g s' to see status. Full help: 'g --help'\""
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user