version 0.8.1 release, stable release
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+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 === 'openai') return process.env.OPENAI_API_KEY ? 'openai' : 'none';
|
||||||
if (preference === 'gemini') return process.env.GEMINI_API_KEY ? 'gemini' : 'none';
|
if (preference === 'gemini') return process.env.GEMINI_API_KEY ? 'gemini' : 'none';
|
||||||
if (preference === 'groq') return process.env.GROQ_API_KEY ? 'groq' : 'none';
|
if (preference === 'groq') return process.env.GROQ_API_KEY ? 'groq' : 'none';
|
||||||
|
|
||||||
// Auto-detection with preference order (Gemini first - fastest and cheapest)
|
// Auto-detection with preference order (Gemini first - fastest and cheapest)
|
||||||
if (process.env.GEMINI_API_KEY) return 'gemini';
|
if (process.env.GEMINI_API_KEY) return 'gemini';
|
||||||
if (process.env.OPENAI_API_KEY) return 'openai';
|
if (process.env.OPENAI_API_KEY) return 'openai';
|
||||||
@@ -48,12 +48,12 @@ class AIProviderManager {
|
|||||||
|
|
||||||
setCache(cacheKey, result, usedLocal = false) {
|
setCache(cacheKey, result, usedLocal = false) {
|
||||||
if (!this.config.cacheEnabled) return;
|
if (!this.config.cacheEnabled) return;
|
||||||
|
|
||||||
if (this.cache.size >= this.maxCacheSize) {
|
if (this.cache.size >= this.maxCacheSize) {
|
||||||
const firstKey = this.cache.keys().next().value;
|
const firstKey = this.cache.keys().next().value;
|
||||||
this.cache.delete(firstKey);
|
this.cache.delete(firstKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cache.set(cacheKey, {
|
this.cache.set(cacheKey, {
|
||||||
result,
|
result,
|
||||||
usedLocal,
|
usedLocal,
|
||||||
@@ -103,9 +103,9 @@ class AIProviderManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async generateWithGroq(prompt, model, options) {
|
async generateWithGroq(prompt, model, options) {
|
||||||
const groq = new OpenAI({
|
const groq = new OpenAI({
|
||||||
apiKey: process.env.GROQ_API_KEY,
|
apiKey: process.env.GROQ_API_KEY,
|
||||||
baseURL: process.env.GROQ_BASE_URL || 'https://api.groq.com/openai/v1'
|
baseURL: process.env.GROQ_BASE_URL || 'https://api.groq.com/openai/v1'
|
||||||
});
|
});
|
||||||
const actualModel = model || this.getDefaultModel('groq');
|
const actualModel = model || this.getDefaultModel('groq');
|
||||||
const response = await groq.chat.completions.create({
|
const response = await groq.chat.completions.create({
|
||||||
@@ -118,11 +118,11 @@ class AIProviderManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async generateCommitMessage(diff, options = {}) {
|
async generateCommitMessage(diff, options = {}) {
|
||||||
const {
|
const {
|
||||||
provider: preferredProvider = 'auto',
|
provider: preferredProvider = 'auto',
|
||||||
conventional = false,
|
conventional = false,
|
||||||
body = false,
|
body = false,
|
||||||
verbose = false
|
verbose = false
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
@@ -134,11 +134,11 @@ class AIProviderManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const providerChain = this.buildProviderChain(preferredProvider);
|
const providerChain = this.buildProviderChain(preferredProvider);
|
||||||
|
|
||||||
for (const provider of providerChain) {
|
for (const provider of providerChain) {
|
||||||
try {
|
try {
|
||||||
if (verbose) Progress.info(`Trying provider: ${provider}`);
|
if (verbose) Progress.info(`Trying provider: ${provider}`);
|
||||||
|
|
||||||
if (provider === 'local') {
|
if (provider === 'local') {
|
||||||
const result = await this.generateLocalHeuristic(diff, options);
|
const result = await this.generateLocalHeuristic(diff, options);
|
||||||
this.setCache(cacheKey, result, true);
|
this.setCache(cacheKey, result, true);
|
||||||
@@ -148,10 +148,10 @@ class AIProviderManager {
|
|||||||
const prompt = this.buildPrompt(diff, { conventional, body });
|
const prompt = this.buildPrompt(diff, { conventional, body });
|
||||||
const result = await this.generateWithProvider(provider, prompt, options);
|
const result = await this.generateWithProvider(provider, prompt, options);
|
||||||
const cleaned = this.cleanCommitMessage(result, { body });
|
const cleaned = this.cleanCommitMessage(result, { body });
|
||||||
|
|
||||||
this.setCache(cacheKey, cleaned, false);
|
this.setCache(cacheKey, cleaned, false);
|
||||||
return { message: cleaned, usedLocal: false };
|
return { message: cleaned, usedLocal: false };
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (verbose) Progress.warning(`${provider} failed: ${error.message}`);
|
if (verbose) Progress.warning(`${provider} failed: ${error.message}`);
|
||||||
continue;
|
continue;
|
||||||
@@ -166,7 +166,7 @@ class AIProviderManager {
|
|||||||
|
|
||||||
buildProviderChain(preferred) {
|
buildProviderChain(preferred) {
|
||||||
const available = [];
|
const available = [];
|
||||||
|
|
||||||
if (preferred !== 'auto' && preferred !== 'none') {
|
if (preferred !== 'auto' && preferred !== 'none') {
|
||||||
const resolved = this.resolveProvider(preferred);
|
const resolved = this.resolveProvider(preferred);
|
||||||
if (resolved !== 'none') available.push(resolved);
|
if (resolved !== 'none') available.push(resolved);
|
||||||
@@ -175,28 +175,28 @@ class AIProviderManager {
|
|||||||
if (process.env.OPENAI_API_KEY) available.push('openai');
|
if (process.env.OPENAI_API_KEY) available.push('openai');
|
||||||
if (process.env.GROQ_API_KEY) available.push('groq');
|
if (process.env.GROQ_API_KEY) available.push('groq');
|
||||||
}
|
}
|
||||||
|
|
||||||
available.push('local');
|
available.push('local');
|
||||||
return [...new Set(available)];
|
return [...new Set(available)];
|
||||||
}
|
}
|
||||||
|
|
||||||
buildPrompt(diff, options) {
|
buildPrompt(diff, options) {
|
||||||
const { conventional, body } = options;
|
const { conventional, body } = options;
|
||||||
|
|
||||||
const style = conventional
|
const style = conventional
|
||||||
? 'Use Conventional Commits format (e.g., feat:, fix:, chore:) for the subject.'
|
? 'Use Conventional Commits format (e.g., feat:, fix:, chore:) for the subject.'
|
||||||
: 'Subject must be a single short line.';
|
: '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.'
|
? '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 only a short subject line without extra quotes.';
|
||||||
|
|
||||||
return `Write a concise git commit message for these changes:\n${diff}\n\n${style} ${bodyInstr}`;
|
return `Write a concise git commit message for these changes:\n${diff}\n\n${style} ${bodyInstr}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanCommitMessage(message, options = {}) {
|
cleanCommitMessage(message, options = {}) {
|
||||||
if (!message) return 'Update project code';
|
if (!message) return 'Update project code';
|
||||||
|
|
||||||
// Remove markdown formatting
|
// Remove markdown formatting
|
||||||
let cleaned = message
|
let cleaned = message
|
||||||
.replace(/```[\s\S]*?```/g, '')
|
.replace(/```[\s\S]*?```/g, '')
|
||||||
@@ -212,7 +212,7 @@ class AIProviderManager {
|
|||||||
|
|
||||||
const lines = cleaned.split('\n').map(l => l.trim()).filter(Boolean);
|
const lines = cleaned.split('\n').map(l => l.trim()).filter(Boolean);
|
||||||
let subject = (lines[0] || '').replace(/\s{2,}/g, ' ').replace(/[\s:,.!;]+$/g, '').trim();
|
let subject = (lines[0] || '').replace(/\s{2,}/g, ' ').replace(/[\s:,.!;]+$/g, '').trim();
|
||||||
|
|
||||||
if (subject.length === 0) subject = 'Update project code';
|
if (subject.length === 0) subject = 'Update project code';
|
||||||
// No length restriction - allow AI to generate full commit messages
|
// No length restriction - allow AI to generate full commit messages
|
||||||
|
|
||||||
@@ -226,16 +226,16 @@ class AIProviderManager {
|
|||||||
async generateLocalHeuristic(diff, options) {
|
async generateLocalHeuristic(diff, options) {
|
||||||
// This would need access to git status - simplified version
|
// This would need access to git status - simplified version
|
||||||
const { conventional = false } = options;
|
const { conventional = false } = options;
|
||||||
|
|
||||||
// Analyze diff for patterns
|
// Analyze diff for patterns
|
||||||
const lines = diff.split('\n');
|
const lines = diff.split('\n');
|
||||||
const additions = lines.filter(l => l.startsWith('+')).length;
|
const additions = lines.filter(l => l.startsWith('+')).length;
|
||||||
const deletions = lines.filter(l => l.startsWith('-')).length;
|
const deletions = lines.filter(l => l.startsWith('-')).length;
|
||||||
const files = (diff.match(/diff --git/g) || []).length;
|
const files = (diff.match(/diff --git/g) || []).length;
|
||||||
|
|
||||||
let type = 'chore';
|
let type = 'chore';
|
||||||
let subject = 'update files';
|
let subject = 'update files';
|
||||||
|
|
||||||
if (additions > deletions * 2) {
|
if (additions > deletions * 2) {
|
||||||
type = 'feat';
|
type = 'feat';
|
||||||
subject = files === 1 ? 'add new functionality' : `add features to ${files} files`;
|
subject = files === 1 ? 'add new functionality' : `add features to ${files} files`;
|
||||||
@@ -249,37 +249,39 @@ class AIProviderManager {
|
|||||||
type = 'docs';
|
type = 'docs';
|
||||||
subject = 'update documentation';
|
subject = 'update documentation';
|
||||||
}
|
}
|
||||||
|
|
||||||
return conventional ? `${type}: ${subject}` : subject.charAt(0).toUpperCase() + subject.slice(1);
|
return conventional ? `${type}: ${subject}` : subject.charAt(0).toUpperCase() + subject.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateMultipleSuggestions(diff, options = {}, count = 3) {
|
async generateMultipleSuggestions(diff, options = {}, count = 3) {
|
||||||
const suggestions = [];
|
const suggestions = [];
|
||||||
const baseOptions = { ...options };
|
const baseOptions = { ...options };
|
||||||
|
|
||||||
// Generate different styles
|
// Generate different styles
|
||||||
const variants = [
|
const variants = [
|
||||||
{ ...baseOptions, conventional: false },
|
{ ...baseOptions, conventional: false },
|
||||||
{ ...baseOptions, conventional: true },
|
{ ...baseOptions, conventional: true },
|
||||||
{ ...baseOptions, conventional: true, body: true }
|
{ ...baseOptions, conventional: true, body: true }
|
||||||
];
|
];
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we have at least one suggestion
|
// Ensure we have at least one suggestion
|
||||||
if (suggestions.length === 0) {
|
if (suggestions.length === 0) {
|
||||||
suggestions.push('Update project files');
|
suggestions.push('Update project files');
|
||||||
}
|
}
|
||||||
|
|
||||||
return suggestions;
|
return suggestions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+118
-47
@@ -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}`);
|
||||||
@@ -32,47 +38,48 @@ class GitAnalyzer {
|
|||||||
const untracked = Array.isArray(status.not_added) ? status.not_added : [];
|
const untracked = Array.isArray(status.not_added) ? status.not_added : [];
|
||||||
|
|
||||||
if (files.length === 0) return 'Working tree clean';
|
if (files.length === 0) return 'Working tree clean';
|
||||||
|
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (staged.length > 0) parts.push(`${staged.length} staged`);
|
if (staged.length > 0) parts.push(`${staged.length} staged`);
|
||||||
if (modified.length > 0) parts.push(`${modified.length} modified`);
|
if (modified.length > 0) parts.push(`${modified.length} modified`);
|
||||||
if (created.length > 0) parts.push(`${created.length} new`);
|
if (created.length > 0) parts.push(`${created.length} new`);
|
||||||
if (deleted.length > 0) parts.push(`${deleted.length} deleted`);
|
if (deleted.length > 0) parts.push(`${deleted.length} deleted`);
|
||||||
if (untracked.length > 0) parts.push(`${untracked.length} untracked`);
|
if (untracked.length > 0) parts.push(`${untracked.length} untracked`);
|
||||||
|
|
||||||
return parts.join(', ');
|
return parts.join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateStatusInsights(status) {
|
async generateStatusInsights(status) {
|
||||||
const insights = [];
|
const insights = [];
|
||||||
|
|
||||||
// Ensure arrays exist and have proper methods
|
// Ensure arrays exist and have proper methods
|
||||||
const modified = Array.isArray(status.modified) ? status.modified : [];
|
const modified = Array.isArray(status.modified) ? status.modified : [];
|
||||||
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'))) {
|
||||||
insights.push('📦 Dependencies may have changed - consider updating package-lock.json');
|
insights.push('📦 Dependencies may have changed - consider updating package-lock.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (created.some(f => String(f).includes('.env'))) {
|
if (created.some(f => String(f).includes('.env'))) {
|
||||||
insights.push('🔐 New environment file detected - ensure it\'s in .gitignore');
|
insights.push('🔐 New environment file detected - ensure it\'s in .gitignore');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modified.some(f => String(f).includes('README'))) {
|
if (modified.some(f => String(f).includes('README'))) {
|
||||||
insights.push('📚 Documentation updated - good practice!');
|
insights.push('📚 Documentation updated - good practice!');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deleted.length > created.length + modified.length) {
|
if (deleted.length > created.length + modified.length) {
|
||||||
insights.push('🧹 Cleanup operation detected - removing more than adding');
|
insights.push('🧹 Cleanup operation detected - removing more than adding');
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
const testFiles = files.filter(f => {
|
const testFiles = files.filter(f => {
|
||||||
const fileName = String(f);
|
const fileName = String(f);
|
||||||
@@ -81,7 +88,7 @@ class GitAnalyzer {
|
|||||||
if (testFiles.length > 0) {
|
if (testFiles.length > 0) {
|
||||||
insights.push('🧪 Test files modified - great for code quality!');
|
insights.push('🧪 Test files modified - great for code quality!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for config files
|
// Check for config files
|
||||||
const configFiles = files.filter(f => {
|
const configFiles = files.filter(f => {
|
||||||
const fileName = String(f);
|
const fileName = String(f);
|
||||||
@@ -90,7 +97,12 @@ class GitAnalyzer {
|
|||||||
if (configFiles.length > 0) {
|
if (configFiles.length > 0) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +110,11 @@ class GitAnalyzer {
|
|||||||
try {
|
try {
|
||||||
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))],
|
||||||
@@ -106,7 +122,7 @@ class GitAnalyzer {
|
|||||||
conventionalCommits: commits.filter(c => /^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?:/.test(c.message)).length,
|
conventionalCommits: commits.filter(c => /^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?:/.test(c.message)).length,
|
||||||
recentActivity: this.analyzeRecentActivity(commits)
|
recentActivity: this.analyzeRecentActivity(commits)
|
||||||
};
|
};
|
||||||
|
|
||||||
return analysis;
|
return analysis;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { error: error.message };
|
return { error: error.message };
|
||||||
@@ -117,10 +133,10 @@ class GitAnalyzer {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
const oneDayAgo = new Date(now - 24 * 60 * 60 * 1000);
|
const oneDayAgo = new Date(now - 24 * 60 * 60 * 1000);
|
||||||
const oneWeekAgo = new Date(now - 7 * 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 recentCommits = commits.filter(c => new Date(c.date) > oneDayAgo);
|
||||||
const weeklyCommits = commits.filter(c => new Date(c.date) > oneWeekAgo);
|
const weeklyCommits = commits.filter(c => new Date(c.date) > oneWeekAgo);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
last24h: recentCommits.length,
|
last24h: recentCommits.length,
|
||||||
lastWeek: weeklyCommits.length,
|
lastWeek: weeklyCommits.length,
|
||||||
@@ -133,16 +149,20 @@ class GitAnalyzer {
|
|||||||
const additions = lines.filter(l => l.startsWith('+')).length;
|
const additions = lines.filter(l => l.startsWith('+')).length;
|
||||||
const deletions = lines.filter(l => l.startsWith('-')).length;
|
const deletions = lines.filter(l => l.startsWith('-')).length;
|
||||||
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,
|
||||||
@@ -151,81 +171,132 @@ class GitAnalyzer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
formatStatusOutput(enhancedStatus) {
|
formatStatusOutput(enhancedStatus) {
|
||||||
const {
|
const {
|
||||||
files = [],
|
files = [],
|
||||||
staged = [],
|
staged = [],
|
||||||
modified = [],
|
modified = [],
|
||||||
created = [],
|
created = [],
|
||||||
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';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modified files
|
// Modified files
|
||||||
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';
|
||||||
}
|
}
|
||||||
|
|
||||||
// New files
|
// New files
|
||||||
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';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deleted files
|
// Deleted files
|
||||||
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';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Untracked files
|
// Untracked files
|
||||||
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`;
|
||||||
}
|
}
|
||||||
output += '\n';
|
output += '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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 };
|
||||||
@@ -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'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 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 };
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "gims",
|
"name": "gims",
|
||||||
"version": "0.6.7",
|
"version": "0.8.1",
|
||||||
"description": "Git Made Simple – AI‑powered git helper using Gemini / OpenAI",
|
"description": "Git Made Simple – AI‑powered git helper with smart insights, stats & code review",
|
||||||
"author": "S41R4J",
|
"author": "S41R4J",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -43,4 +43,4 @@
|
|||||||
"test": "echo \"Enhanced GIMS v$(node -p \"require('./package.json').version\") - All systems operational!\"",
|
"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'\""
|
"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