Compare commits

..

1 Commits

Author SHA1 Message Date
s41r4j db9e09c06f Initial commit 2026-06-08 20:40:12 +00:00
25 changed files with 14 additions and 7136 deletions
-59
View File
@@ -1,59 +0,0 @@
# Dependencies
node_modules/
# Lock files (keep package-lock.json for consistency)
bun.lock
yarn.lock
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
# Development and build tools
.kiro/
.github/
.npm-cache/
.cache/
dist/
build/
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# Temporary files
*.tmp
*.temp
# Local configuration files
.gimsrc
-23
View File
@@ -1,23 +0,0 @@
# NPM Ignore
.git
.github
.gitignore
.DS_Store
.env*
test/
tests/
examples/
scripts/
docs/
mcp/
AI.md
*.log
coverage/
node_modules/
# Keep bin and library code
!bin/
!package.json
!README.md
!LICENSE
!CHANGELOG.md
-189
View File
@@ -1,189 +0,0 @@
# 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
- **Added `-v` flag for version**: Now `g -v` or `gims -v` shows the version number
- **Renamed command**: Changed `help-quick` to `quick-help` (alias `q` still works)
- **Added `push` command**: Now `g push` or `gims push` works as a standalone command
---
## [0.6.6] - 2025-10-26
### 🔧 Improvements
- **Removed commit message length restriction**: AI can now generate commit messages of any length (no more `...` truncation at 72 chars)
- **Added confirmation prompt for local heuristics**: When no AI provider is configured, the tool now asks for confirmation `[Y/n]` before committing
- Default is `Y` (just press Enter to accept)
- Warning message clearly indicates "local heuristics" instead of AI
- Can be bypassed with `--yes` flag for automation
- **Better user awareness**: Users are now informed when local heuristics are used instead of AI
### 📚 User Experience
- More transparent about when AI is used vs. local pattern matching
- Safer commits when API keys are not configured
---
## [0.6.5] - 2025-10-20
### 🔧 Command Behavior Updated
- **`g a` (amend) default behavior changed**: Now keeps the original commit message by default (`--no-edit`)
- **New `--edit` flag**: Use `g a --edit` to generate a new AI commit message
- **Simpler workflow**: `g a` simply merges current changes into previous commit without editing the message
### 📚 Documentation Updates
- Updated all documentation to reflect the new amend behavior
- Clarified that `g a` merges changes while keeping the original message
---
## [0.6.4] - 2025-10-19
### 🔧 Command Aliases Fixed
- **Fixed `g i` alias**: Now correctly initializes a git repository (was incorrectly mapped to interactive mode)
- **Updated `g interactive` alias**: Changed from `g i` to `g int` to avoid conflicts
- **Verified `g ls` and `g ll`**: Both commands working correctly (short and detailed commit history)
### 📚 Documentation Updates
- Updated `QUICK_REFERENCE.md` to reflect correct command aliases
- Updated help output (`g q`) to show correct mappings
---
## [0.6.2] - 2024-12-19
### 🚀 AI Model Updates
- **Updated to latest AI models**:
- Gemini: `gemini-2.5-flash` (latest and fastest)
- OpenAI: `gpt-5` (latest GPT model)
- Groq: `groq/compound` (latest Groq model)
### 📚 Documentation
- **Simplified README**: Reduced from verbose to concise format
- **Focused on essentials**: Quick start, main commands, examples
- **Better readability**: Easier to scan and understand
### 🧹 Repository Cleanup
- Removed development-specific files (.kiro/, .github/, coverage/)
- Updated .gitignore to exclude dev directories
- Cleaner package for npm distribution
---
## [0.6.1] - 2024-12-19
### ✨ Enhanced User Experience
- **Single-letter aliases** for main workflow commands
- **Quick API setup**: `g setup --api-key <provider>`
- **Better onboarding**: Clear setup instructions with direct links
- **Quick help**: `g q` command for instant reference
### 🔧 Improved Commands
- `g s` - Status (enhanced git status)
- `g i` - Interactive (guided commit wizard)
- `g p` - Preview (see what will be committed)
- `g l` - Local (AI commit locally)
- `g o` - Online (AI commit + push)
- `g h` - History (numbered commit log)
- `g a` - Amend (smart amend with AI)
- `g u` - Undo (undo last commit)
---
## [0.6.0] - 2024-12-19
### 🚀 Major Enhanced Release
- **Interactive commit wizard** with multiple AI suggestions
- **Enhanced git status** with AI insights and project analysis
- **Modular architecture** for better performance and maintainability
- **Smart configuration** with project type detection
- **Progress indicators** and better error handling
- **Caching system** for improved AI response times
---
## [0.5.4] - Previous Version
- Basic AI-powered commit messages
- Simple configuration
- Core git operations
+13 -16
View File
@@ -1,21 +1,18 @@
MIT License MIT License
Copyright (c) 2025 Sairaj Jawalikar Copyright (c) 2026 s41r4j
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
of this software and associated documentation files (the "Software"), to deal associated documentation files (the "Software"), to deal in the Software without restriction, including
in the Software without restriction, including without limitation the rights without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
copies of the Software, and to permit persons to whom the Software is following conditions:
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all The above copyright notice and this permission notice shall be included in all copies or substantial
copies or substantial portions of the Software. portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, USE OR OTHER DEALINGS IN THE SOFTWARE.
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1 -384
View File
@@ -1,385 +1,2 @@
<div align="center"> # gims
<!--
# GIMS - Git Made Simple
-->
<img width="1658" height="609" alt="gims" src="https://github.com/user-attachments/assets/fd4ae515-9674-4cc2-8ea0-39d21e82e274" />
[![npm version](https://img.shields.io/npm/v/gims.svg)](https://npmjs.org/package/gims)
[![npm downloads](https://img.shields.io/npm/dm/gims.svg)](https://npmjs.org/package/gims)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![AI Powered](https://img.shields.io/badge/AI-Powered-blueviolet.svg)](https://github.com/s41r4j/gims)
[![Node.js](https://img.shields.io/badge/Node.js->=18.18.0-green.svg)](https://nodejs.org)
**AI-powered Git CLI that writes your commit messages for you**
*Because life's too short for "fix stuff" commits* 🎯
[Installation](#-installation) • [Quick Start](#-quick-start) • [Commands](#-commands) • [AI Setup](#-ai-providers) • [Configuration](#-configuration)
</div>
---
## ✨ Features
- 🤖 **AI-Generated Commits** — Let AI analyze your changes and write meaningful commit messages
- 📊 **Smart Status** — Enhanced git status with file type detection and insights
- 🔄 **Workflow Shortcuts** — Common multi-step operations condensed to single commands
- 📈 **Commit Analytics** — Track your commit streak, patterns, and statistics
- 🔍 **Code Review** — Get AI-powered code review before committing
- 🛡️ **Safe Operations** — Built-in safeguards for destructive commands
-**Lightning Fast** — Optimized for speed with intelligent caching
---
## 📦 Installation
```bash
npm install -g gims
```
**Requirements:** Node.js >= 18.18.0
---
## ⚡ Quick Start
```bash
# 1. Setup AI provider (choose one)
g setup --api-key gemini # 🆓 Free & fast (recommended)
g setup --api-key openai # 🎯 High quality
g setup --api-key groq # ⚡ Ultra fast
# 2. Start using it!
g s # Check status with AI insights
g o # AI commit + push (one command!)
```
### Your New Workflow
```bash
# Before (traditional git)
git add .
git commit -m "trying to think of message..."
git push
# After (with GIMS)
g o # That's it. AI handles the rest.
```
---
## 📚 Commands
### 🧠 Core Commands
| Command | Alias | Description |
|---------|-------|-------------|
| `g status` | `g s` | Enhanced status with AI insights & file type emojis |
| `g oneshot` | `g o` | Stage all → AI commit → Push (full workflow) |
| `g local` | `g l` | Stage all → AI commit (no push) |
| `g suggest` | `g sg` | Get AI commit message suggestions |
| `g version` | `g v` | Manage project version (S4 system) |
| `g whoami` | — | Show system identity & version |
| `g wip` | — | Quick work-in-progress commit |
### 📊 Analytics & Review
| Command | Alias | Description |
|---------|-------|-------------|
| `g review` | `g r` | AI code review with complexity analysis |
| `g today` | `g t` | Show all commits made today |
| `g stats` | — | Personal statistics: streak, patterns, style |
| `g split` | — | AI suggestions for splitting large changesets |
### ⚡ Workflow Shortcuts
| Command | Alias | What it does |
|---------|-------|--------------|
| `g safe-pull` | `g sp` | Stash → Pull → Pop (safe pull with uncommitted changes) |
| `g fix` | — | Smart fix for diverged/ahead/behind branches |
| `g main` | — | Switch to main/master + pull latest |
| `g stash-save` | `g ss` | Quick stash with auto-generated name |
| `g stash-pop` | `g pop` | Pop the latest stash |
| `g unstage` | `g us` | Unstage all staged files |
| `g discard` | `g x` | Discard all changes (requires `--yes`) |
### 🛠️ Git Helpers
| Command | Alias | Description |
|---------|-------|-------------|
| `g last` | — | Show last commit details with diff |
| `g conflicts` | — | Conflict resolution helper |
| `g cleanup` | `g clean` | Remove local branches deleted from remote |
| `g list` | `g ls` | Compact commit history |
| `g amend` | `g a` | Amend the previous commit |
| `g undo` | `g u` | Undo last commit (keep changes) |
| `g delete-branch` | `g del` | Delete branch locally and remotely |
### 🔢 S4 Versioning (New)
GIMS now uses the **[S4 Versioning System](https://github.com/s41r4j/s4vs)**, a forensic-grade versioning standard stored entirely in **Git Tags**.
`MAJOR.MINOR.PATCH-STAGE.BUILD+DATE.TIME.COMMIT.BRANCH`
*(e.g., `0.8.2-dev.1+20250129.1305.af8d9c.main`)*
| Command | Description |
|---------|-------------|
| `g v` | Smart bump (resets build on new day) + Create Tag |
| `g v -n` | Dry run (preview next version) |
| `g v major` | Force major bump (`1.0.0``2.0.0`) |
| `g v --list` | List stable releases (hides dev tags) |
| `g v --prune` | Delete old `dev` tags (keeps last 10) |
| `g v --undo` | Undo last version bump (delete latest tag) |
| `g v -i` | Version analysis with timestamp |
| `g whoami` | Dynamic identity & version banner |
> **Note:** S4 uses Git Tags as the Single Source of Truth. No `VERSION` file is created. It updates your `.env` version variables if they exist.
### 🔧 Setup & Config
| Command | Description |
|---------|-------------|
| `g setup` | Interactive setup wizard |
| `g setup --api-key <provider>` | Quick API key setup |
| `g config --list` | View all settings |
| `g config --set key=value` | Update configuration |
| `g quick-help` / `g q` | Quick command reference |
---
## 🤖 AI-Native Integration (New)
GIMS is designed to be "AI-Native", allowing AI agents to directly understand and control your git repository.
### 1. Model Context Protocol (MCP) Server
GIMS includes a built-in [MCP](https://modelcontextprotocol.io/) server. This allows AI assistants like **Claude Desktop**, **Cursor**, and **VS Code** to directly "see" and "use" GIMS tools.
#### Available Tools
- `get_status`: Get AI-enhanced git status and insights.
- `analyze_history`: Analyze commit history and patterns.
- `version_info`: Get current S4 version details.
- `generate_commit_message`: Generate a commit message for staged changes.
- `run_git_command`: Safe execution of raw git commands.
#### Usage with Claude Desktop
Add to your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"gims": {
"command": "node",
"args": ["/path/to/gims/mcp/index.js"]
}
}
}
```
### 2. OpenAI / Custom Agent Integration
If you are building your own AI agent (using OpenAI, LangChain, etc.), you can plug GIMS tools directly into your `tools` array using our auto-generated schemas.
**Schema Location:** `bin/tools-schema.json`
```javascript
const tools = require('gims/bin/tools-schema.json');
// Pass 'tools' directly to OpenAI API
```
---
## 🤖 AI Providers
GIMS supports multiple AI providers. Choose based on your needs:
| Provider | Model | Speed | Quality | Cost |
|----------|-------|-------|---------|------|
| **Gemini** | `gemini-3-flash-preview` | ⚡⚡⚡ | ⭐⭐⭐ | 🆓 Free |
| **Groq** | `groq/compound` | ⚡⚡⚡⚡ | ⭐⭐⭐ | 🆓 Free tier |
| **OpenAI** | `gpt-5.2-2025-12-11` | ⚡⚡ | ⭐⭐⭐⭐ | 💰 Paid |
### Setting Up API Keys
#### Option 1: Interactive Setup (Recommended)
```bash
g setup --api-key gemini # Guided setup with instructions
g setup --api-key openai
g setup --api-key groq
```
#### Option 2: Environment Variables (Auto-detected)
GIMS automatically detects API keys from environment variables. Add to your shell profile (`~/.zshrc`, `~/.bashrc`, etc.):
```bash
# Gemini (recommended - free)
export GEMINI_API_KEY="your-api-key-here"
# OpenAI
export OPENAI_API_KEY="your-api-key-here"
# Groq
export GROQ_API_KEY="your-api-key-here"
```
Then reload your shell: `source ~/.zshrc`
> **💡 Tip:** If multiple API keys are set, GIMS auto-selects in order: Gemini → OpenAI → Groq
### Getting API Keys
<details>
<summary><b>🔷 Gemini (Recommended)</b></summary>
1. Visit [Google AI Studio](https://aistudio.google.com/app/apikey)
2. Create a new API key
3. Set it: `export GEMINI_API_KEY="your-key"` or run `g setup --api-key gemini`
</details>
<details>
<summary><b>🟢 Groq</b></summary>
1. Visit [Groq Console](https://console.groq.com/keys)
2. Create a new API key
3. Set it: `export GROQ_API_KEY="your-key"` or run `g setup --api-key groq`
</details>
<details>
<summary><b>🟡 OpenAI</b></summary>
1. Visit [OpenAI Platform](https://platform.openai.com/api-keys)
2. Create a new API key
3. Set it: `export OPENAI_API_KEY="your-key"` or run `g setup --api-key openai`
</details>
---
## 🔧 Configuration
```bash
# View current settings
g config --list
# Enable conventional commits (feat:, fix:, etc.)
g config --set conventional=true
# Auto-stage all changes before commit
g config --set autoStage=true
# Disable clipboard copy
g config --set copy=false
# Show/hide progress indicators
g config --set progressIndicators=true
```
### Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `provider` | string | `auto` | AI provider: `gemini`, `openai`, `groq`, `auto`, `none` |
| `model` | string | — | Override default model for provider |
| `conventional` | boolean | `false` | Use Conventional Commits format |
| `autoStage` | boolean | `false` | Auto-stage all changes |
| `copy` | boolean | `true` | Copy suggestions to clipboard |
| `progressIndicators` | boolean | `true` | Show progress spinners |
---
## 📖 Usage Examples
### Daily Workflow
```bash
g s # Check what's changed
g sp # Safe pull (won't lose uncommitted work)
# ... do your work ...
g o # Commit everything with AI message + push
```
### Code Review Before Commit
```bash
g r # Get AI review of your changes
g o # Commit if review looks good
```
### Handling Branch Issues
```bash
g fix # See sync status and options
g fix --ai # Get AI recommendation
g fix --merge # Merge remote changes
g fix --rebase # Rebase onto remote
```
### Working with Stashes
```bash
g ss # Quick stash
g main # Switch to main and pull
# ... check something ...
g pop # Get your work back
```
### Large Changeset
```bash
g split # Get suggestions for splitting changes
g sg --multiple # Get multiple commit message options
```
---
## 🆚 GIMS vs Plain Git
| Task | Git | GIMS |
|------|-----|------|
| Commit & push | `git add . && git commit -m "msg" && git push` | `g o` |
| Pull with uncommitted changes | `git stash && git pull && git stash pop` | `g sp` |
| Check status | `git status` | `g s` (+ AI insights) |
| Fix diverged branch | Multiple commands + decisions | `g fix --ai` |
| Code review | External tool | `g r` |
---
## 🆘 Getting Help
```bash
g --help # Full command list with descriptions
g q # Quick reference card
g <command> -h # Help for specific command
```
---
## 🤝 Contributing
Contributions are welcome! Feel free to:
- 🐛 Report bugs
- 💡 Suggest features
- 🔧 Submit pull requests
---
## 📄 License
MIT © [S41R4J](https://github.com/s41r4j)
---
<div align="center">
**[⬆ Back to Top](#-gims---git-made-simple)**
Made with ❤️ for developers who'd rather code than write commit messages
</div>
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 MiB

-1954
View File
File diff suppressed because it is too large Load Diff
-132
View File
@@ -1,132 +0,0 @@
/**
* GIMS AI Interface Registry
*
* This module defines the "Tools" that GIMS exposes to AI systems (MCP, OpenAI, etc.).
* It serves as the Single Source of Truth for tool definitions and their schemas.
*/
const simpleGit = require('simple-git');
const { GitAnalyzer } = require('../git/analyzer');
const { S4Versioning } = require('../utils/s4');
const { ConfigManager } = require('../config/manager');
const { AIProviderManager } = require('./providers');
// Initialize shared instances
const git = simpleGit();
const gitAnalyzer = new GitAnalyzer(git);
const configManager = new ConfigManager();
const config = configManager.load();
const aiProvider = new AIProviderManager(config);
const s4 = new S4Versioning(git);
/**
* Tool Definitions
* Format:
* {
* name: "tool_name",
* description: "Description for the AI",
* inputSchema: { ...JSON Schema... },
* handler: async (args) => { ... implementation ... }
* }
*/
const tools = [
{
name: "get_status",
description: "Get enhanced git status with AI insights, file modification stats, and branch context. Use this to understand the current state of the repository.",
inputSchema: {
type: "object",
properties: {},
},
handler: async () => {
return await gitAnalyzer.getEnhancedStatus();
}
},
{
name: "analyze_history",
description: "Analyze recent commit history, including authors, conventions, and activity patterns.",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description: "Number of commits to analyze (default: 10)"
}
}
},
handler: async ({ limit = 10 }) => {
return await gitAnalyzer.analyzeCommitHistory(limit);
}
},
{
name: "version_info",
description: "Get current project version information using S4 versioning (Semantic + Date/Time).",
inputSchema: {
type: "object",
properties: {}
},
handler: async () => {
const currentVersion = await s4.getCurrentVersion();
if (!currentVersion) return { version: "0.0.0", source: "fallback" };
return { version: currentVersion, details: S4Versioning.parse(currentVersion) };
}
},
{
name: "generate_commit_message",
description: "Generate a high-quality commit message based on currently staged changes.",
inputSchema: {
type: "object",
properties: {
context: {
type: "string",
description: "Optional context or user instruction for the message generation"
}
}
},
handler: async ({ context }) => {
const rawDiff = await git.diff(['--cached', '--no-ext-diff']);
if (!rawDiff.trim()) {
return { error: "No staged changes to generate message for." };
}
// Pass pseudo-options resembling CLI opts
const opts = { conventional: config.conventional };
const message = await aiProvider.generateCommitMessage(rawDiff, opts);
// Handle object return from provider (some return { message, usedLocal })
const msgStr = typeof message === 'string' ? message : message.message;
return {
message: msgStr,
original_diff_size: rawDiff.length
};
}
},
{
name: "run_git_command",
description: "Execute a raw git command safely. Use this for standard git operations not covered by other tools.",
inputSchema: {
type: "object",
properties: {
command: {
type: "string",
description: "The full git command to run (e.g., 'log -n 5', 'branch --list')"
}
},
required: ["command"]
},
handler: async ({ command }) => {
// Safety check: block potentially destructive commands if needed,
// but for now we assume the agent is trusted or user-monitored.
// We'll strip 'git ' prefix if present.
const cleanCmd = command.replace(/^git\s+/, '').trim();
const args = cleanCmd.split(' ');
try {
const result = await git.raw(args);
return { output: result ? result.trim() : "Success" };
} catch (e) {
return { error: e.message };
}
}
}
];
module.exports = { tools };
-312
View File
@@ -1,312 +0,0 @@
const fs = require('fs');
const path = require('path');
const { OpenAI } = require('openai');
const { GoogleGenAI } = require('@google/genai');
const { Progress } = require('../utils/progress');
const { color } = require('../utils/colors');
/**
* Enhanced AI provider management with caching and fallbacks
*/
class AIProviderManager {
constructor(config = {}) {
this.config = config;
this.cacheFile = path.join(
process.env.HOME || process.env.USERPROFILE || process.cwd(),
'.gims', 'cache.json'
);
}
getDefaultModel(provider) {
const defaults = {
'gemini': 'gemini-2.0-flash',
'openai': 'gpt-4o-mini',
'groq': 'llama-3.3-70b-versatile'
};
return defaults[provider] || '';
}
getCacheKey(prompt, options) {
const crypto = require('crypto');
const key = JSON.stringify({ prompt: prompt.substring(0, 1000), options });
return crypto.createHash('md5').update(key).digest('hex');
}
getFromCache(cacheKey) {
if (!this.config.cacheEnabled) return null;
try {
if (!fs.existsSync(this.cacheFile)) return null;
const data = JSON.parse(fs.readFileSync(this.cacheFile, 'utf8'));
return data[cacheKey] || null;
} catch {
return null;
}
}
setCache(cacheKey, result, usedLocal = false) {
if (!this.config.cacheEnabled) return;
try {
const dir = path.dirname(this.cacheFile);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
let data = {};
if (fs.existsSync(this.cacheFile)) {
try { data = JSON.parse(fs.readFileSync(this.cacheFile, 'utf8')); } catch {}
}
const keys = Object.keys(data);
if (keys.length >= 100) {
const oldest = keys.sort((a, b) => (data[a].timestamp || 0) - (data[b].timestamp || 0));
oldest.slice(0, keys.length - 99).forEach(k => delete data[k]);
}
data[cacheKey] = { result, usedLocal, timestamp: Date.now() };
fs.writeFileSync(this.cacheFile, JSON.stringify(data));
} catch {}
}
async generateWithProvider(provider, prompt, options = {}) {
const { model = '' } = options;
try {
switch (provider) {
case 'gemini':
return await this.generateWithGemini(prompt, model || this.getDefaultModel('gemini'), options);
case 'openai':
return await this.generateWithOpenAI(prompt, model || this.getDefaultModel('openai'), options);
case 'groq':
return await this.generateWithGroq(prompt, model || this.getDefaultModel('groq'), options);
default:
throw new Error(`Unknown provider: ${provider}`);
}
} catch (error) {
const msg = error.message || '';
if (msg.includes('429') || /rate.?limit/i.test(msg)) {
throw new Error(`${provider} rate limit hit — try again in a moment`);
}
if (msg.includes('401') || /invalid.api.key|api_key/i.test(msg)) {
throw new Error(`${provider} API key invalid — run: g setup --api-key ${provider}`);
}
throw new Error(`${provider} generation failed: ${msg}`);
}
}
async generateWithGemini(prompt, model, options) {
const genai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const actualModel = model || this.getDefaultModel('gemini');
const response = await genai.models.generateContent({
model: actualModel,
contents: prompt,
});
// Handle @google/genai v1.5+ raw response structure
if (response && response.candidates && response.candidates.length > 0) {
const parts = response.candidates[0].content.parts;
return parts.map(p => p.text || '').join('').trim();
}
return '';
}
async generateWithOpenAI(prompt, model, options) {
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const actualModel = model || this.getDefaultModel('openai');
const response = await openai.chat.completions.create({
model: actualModel,
messages: [{ role: 'user', content: prompt }],
temperature: options.temperature || 0.3,
max_tokens: options.maxTokens || 200,
});
return (response.choices[0]?.message?.content || '').trim();
}
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 actualModel = model || this.getDefaultModel('groq');
const response = await groq.chat.completions.create({
model: actualModel,
messages: [{ role: 'user', content: prompt }],
temperature: options.temperature || 0.3,
max_tokens: options.maxTokens || 200,
});
return (response.choices[0]?.message?.content || '').trim();
}
async generateCommitMessage(diff, options = {}) {
const {
provider: preferredProvider = this.config.provider || 'auto',
conventional = this.config.conventional || false,
body = false,
verbose = false
} = options;
// Truncate oversized diffs before sending to AI
const maxDiffSize = this.config.maxDiffSize || 100000;
const truncatedDiff = diff.length > maxDiffSize
? diff.substring(0, maxDiffSize) + '\n... (diff truncated for AI)'
: diff;
// Check cache first
const cacheKey = this.getCacheKey(truncatedDiff, { conventional, body });
const cached = this.getFromCache(cacheKey);
if (cached && Date.now() - cached.timestamp < 3600000) { // 1 hour cache
if (verbose) Progress.info('Using cached result');
return { message: cached.result, usedLocal: cached.usedLocal || false };
}
const providerChain = this.buildProviderChain(preferredProvider);
for (const provider of providerChain) {
try {
if (provider === 'local') {
const result = await this.generateLocalHeuristic(truncatedDiff, options);
this.setCache(cacheKey, result, true);
return { message: result, usedLocal: true };
}
const prompt = this.buildPrompt(truncatedDiff, { 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;
}
}
// Final fallback
const fallback = 'Update project files';
this.setCache(cacheKey, fallback, true);
return { message: fallback, usedLocal: true };
}
buildProviderChain(preferred) {
const available = [];
// First, try the preferred provider if its key is available
if (preferred !== 'auto' && preferred !== 'none') {
if (preferred === 'gemini' && process.env.GEMINI_API_KEY) available.push('gemini');
else if (preferred === 'openai' && process.env.OPENAI_API_KEY) available.push('openai');
else if (preferred === 'groq' && process.env.GROQ_API_KEY) available.push('groq');
}
// Then add all other available providers as fallbacks
if (process.env.GEMINI_API_KEY && !available.includes('gemini')) available.push('gemini');
if (process.env.OPENAI_API_KEY && !available.includes('openai')) available.push('openai');
if (process.env.GROQ_API_KEY && !available.includes('groq')) available.push('groq');
// Local heuristics as final fallback
available.push('local');
return available;
}
buildPrompt(diff, options) {
const { conventional, body } = options;
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
? '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, '')
.replace(/`([^`]+)`/g, '$1')
.replace(/^\s*[-*+]\s*/gm, '')
.replace(/^\s*\d+\.\s*/gm, '')
.replace(/^\s*#+\s*/gm, '')
.replace(/\*\*(.*?)\*\*/g, '$1')
.replace(/\*(.*?)\*/g, '$1')
.replace(/[\u{1F300}-\u{1FAFF}]/gu, '')
.replace(/[\t\r]+/g, ' ')
.trim();
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
if (!options.body) return subject;
const bodyLines = lines.slice(1).filter(l => l.length > 0);
const bodyText = bodyLines.join('\n').trim();
return bodyText ? `${subject}\n\n${bodyText}` : subject;
}
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`;
} else if (deletions > additions * 2) {
type = 'chore';
subject = files === 1 ? 'remove unused code' : `clean up ${files} files`;
} else if (diff.includes('test') || diff.includes('spec')) {
type = 'test';
subject = 'update tests';
} else if (diff.includes('README') || diff.includes('doc')) {
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 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;
}
}
module.exports = { AIProviderManager };
-241
View File
@@ -1,241 +0,0 @@
const readline = require('readline');
const { color } = require('../utils/colors');
const { Progress } = require('../utils/progress');
/**
* Interactive commit wizard and user input utilities
*/
class InteractiveCommands {
constructor(git, aiProvider, analyzer) {
this.git = git;
this.aiProvider = aiProvider;
this.analyzer = analyzer;
}
async promptUser(question, defaultValue = '') {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise(resolve => {
rl.question(question, answer => {
rl.close();
resolve(answer.trim() || defaultValue);
});
});
}
async selectFromList(items, prompt = 'Select an option:', allowCustom = false) {
console.log(`\n${color.bold(prompt)}`);
items.forEach((item, index) => {
console.log(` ${color.cyan((index + 1).toString())}. ${item}`);
});
if (allowCustom) {
console.log(` ${color.cyan('c')}. Custom message`);
}
const maxChoice = items.length;
const validChoices = Array.from({length: maxChoice}, (_, i) => (i + 1).toString());
if (allowCustom) validChoices.push('c');
let choice;
do {
choice = await this.promptUser(`\nChoice (1-${maxChoice}${allowCustom ? ', c' : ''}): `);
} while (!validChoices.includes(choice));
if (choice === 'c') {
return await this.promptUser('Enter custom message: ');
}
return items[parseInt(choice) - 1];
}
async runInteractiveCommit(options = {}) {
try {
console.log(color.bold('\n🎯 Interactive Commit Wizard\n'));
// Step 1: Check for changes
Progress.step(1, 4, 'Analyzing changes...');
const status = await this.git.status();
if (status.files.length === 0) {
Progress.warning('No changes detected');
return;
}
// Step 2: Show status and get staging preference
Progress.step(2, 4, 'Reviewing file changes...');
const enhancedStatus = await this.analyzer.getEnhancedStatus();
console.log(this.analyzer.formatStatusOutput(enhancedStatus));
// Ask about staging
let shouldStage = false;
if (status.staged.length === 0) {
const stageChoice = await this.promptUser(
'No files are staged. Stage all changes? (y/n) [y]: ',
'y'
);
shouldStage = stageChoice.toLowerCase() === 'y';
if (shouldStage) {
await this.git.add('.');
Progress.success('All changes staged');
} else {
console.log('You can manually stage files with: git add <file>');
return;
}
}
// Step 3: Generate commit message suggestions
Progress.step(3, 4, 'Generating AI suggestions...');
Progress.start('🤖 AI is analyzing your changes');
const diff = await this.git.diff(['--cached', '--no-ext-diff']);
if (!diff.trim()) {
Progress.stop(color.yellow('No staged changes to commit'));
return;
}
const suggestions = await this.aiProvider.generateMultipleSuggestions(diff, options, 3);
Progress.stop(color.green('✓ Generated suggestions'));
// Step 4: Let user choose
Progress.step(4, 4, 'Select commit message...');
const selectedMessage = await this.selectFromList(
suggestions,
'Choose a commit message:',
true
);
// Confirm and commit
console.log(`\nSelected message: ${color.green(selectedMessage)}`);
const confirm = await this.promptUser('Proceed with commit? (y/n) [y]: ', 'y');
if (confirm.toLowerCase() === 'y') {
if (options.dryRun) {
console.log(color.yellow('[dry-run] Would commit with message:'));
console.log(selectedMessage);
} else {
await this.git.commit(selectedMessage);
Progress.success(`Committed: "${selectedMessage}"`);
// Ask about pushing
const pushChoice = await this.promptUser('Push to remote? (y/n) [n]: ', 'n');
if (pushChoice.toLowerCase() === 'y') {
try {
await this.git.push();
Progress.success('Pushed to remote');
} catch (error) {
Progress.error(`Push failed: ${error.message}`);
}
}
}
} else {
console.log('Commit cancelled');
}
} catch (error) {
Progress.error(`Interactive commit failed: ${error.message}`);
throw error;
}
}
async runQuickCommit(message, options = {}) {
try {
const status = await this.git.status();
if (status.files.length === 0) {
console.log('No changes to commit');
return;
}
// Auto-stage if nothing is staged
if (status.staged.length === 0) {
console.log(color.yellow('Auto-staging all changes...'));
await this.git.add('.');
}
if (options.dryRun) {
console.log(color.yellow('[dry-run] Would commit with message:'));
console.log(message);
return;
}
await this.git.commit(message);
Progress.success(`Committed: "${message}"`);
if (options.push) {
try {
await this.git.push();
Progress.success('Pushed to remote');
} catch (error) {
Progress.warning(`Push failed: ${error.message}`);
}
}
} catch (error) {
Progress.error(`Quick commit failed: ${error.message}`);
throw error;
}
}
async showCommitPreview(options = {}) {
try {
const status = await this.git.status();
if (status.files.length === 0) {
console.log('No changes to preview');
return;
}
// Show what would be committed
const diff = await this.git.diff(['--cached', '--no-ext-diff']);
if (!diff.trim()) {
console.log(color.yellow('No staged changes. Use --all to stage everything.'));
return;
}
console.log(color.bold('\n📋 Commit Preview\n'));
// Show file summary
const complexity = await this.analyzer.getChangeComplexity(diff);
console.log(`Complexity: ${color.cyan(complexity.complexity)}`);
console.log(`Files: ${complexity.files}, +${complexity.additions} -${complexity.deletions}`);
// Generate and show AI suggestion
Progress.start('🤖 Generating commit message');
const suggestion = await this.aiProvider.generateCommitMessage(diff, options);
Progress.stop('');
console.log(`\nSuggested message: ${color.green(suggestion)}`);
// Show diff summary (first few lines)
console.log(`\n${color.bold('Changes:')}`);
const diffLines = diff.split('\n').slice(0, 20);
diffLines.forEach(line => {
if (line.startsWith('+')) {
console.log(color.green(line));
} else if (line.startsWith('-')) {
console.log(color.red(line));
} else if (line.startsWith('@@')) {
console.log(color.cyan(line));
} else {
console.log(color.dim(line));
}
});
if (diff.split('\n').length > 20) {
console.log(color.dim('... (truncated)'));
}
} catch (error) {
Progress.error(`Preview failed: ${error.message}`);
throw error;
}
}
}
module.exports = { InteractiveCommands };
-276
View File
@@ -1,276 +0,0 @@
const fs = require('fs');
const path = require('path');
const { S4Versioning } = require('../utils/s4');
const { color } = require('../utils/colors');
const { Progress } = require('../utils/progress');
class VersionCommand {
constructor(git, configManager) {
this.git = git;
this.configManager = configManager;
this.s4 = new S4Versioning(git);
}
async run(type = 'auto', options = {}) {
const { info, dryRun, stage, list, prune, all } = options;
try {
// --- Special Commands: List & Prune ---
if (list) {
await this.listTags(all);
return;
}
if (prune) {
await this.pruneTags();
return;
}
if (options.undo) {
await this.undoLastBump();
return;
}
// --- Main Bump Logic ---
// 1. Get current version from Git Tags (SSOT)
let currentVersion = await this.s4.getCurrentVersion();
let source = 'git-tag';
// Fallback: If no tags, check package.json just for migration base
if (!currentVersion) {
currentVersion = this.getPackageJsonVersion() || '0.0.0';
source = 'package.json (fallback)';
}
// 2. Info mode
if (info) {
console.log(`Current Version: ${color.green(currentVersion)} (${source})`);
this.showInfo(currentVersion);
return;
}
// 3. Calculate next version
Progress.start('Calculated next version...');
const nextVersion = await this.s4.bump(currentVersion, type, stage);
Progress.stop('');
// 4. Dry run
if (dryRun) {
console.log(color.bold('\nDry Run Results:'));
console.log(`Current: ${color.dim(currentVersion)}`);
console.log(`Next: ${color.green(nextVersion)}`);
this.showInfo(nextVersion);
return;
}
// 5. Execution (Tag + .env)
console.log(`\nBumping version: ${color.dim(currentVersion)}${color.green(nextVersion)}`);
const updatedFiles = [];
// Update .env (Only if exists)
const envUpdates = this.updateEnvFiles(nextVersion);
updatedFiles.push(...envUpdates);
if (updatedFiles.length > 0) {
console.log(`${color.green('✔')} Updated ${updatedFiles.join(', ')}`);
// Stage .env updates if any (usually gitignored, but if not...)
// We won't auto-commit them to avoid messing with user's git config too much logic
// But usually .env is ignored.
}
// 6. Create Git Tag (The Truth)
await this.createTag(nextVersion);
} catch (error) {
Progress.error(`Version bump failed: ${error.message}`);
}
}
getPackageJsonVersion() {
if (fs.existsSync('package.json')) {
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
return pkg.version;
}
return null;
}
updateEnvFiles(newVersion) {
const updates = [];
const envFiles = ['.env', '.env.local', '.env.development', '.env.production'];
envFiles.forEach(file => {
if (fs.existsSync(file)) {
let content = fs.readFileSync(file, 'utf8');
let changed = false;
// Match common version vars
const regex = /^(VERSION|APP_VERSION|NEXT_PUBLIC_VERSION|REACT_APP_VERSION|S4_VERSION)=(.*)$/gm;
if (regex.test(content)) {
content = content.replace(regex, `$1=${newVersion}`);
changed = true;
}
if (changed) {
fs.writeFileSync(file, content);
updates.push(file);
}
}
});
return updates;
}
async createTag(version) {
try {
// Create lightweight or annotated tag? S4 recommends annotated.
await this.git.addAnnotatedTag(version, `Release ${version}`);
console.log(`${color.green('✔')} Created git tag: ${color.cyan(version)}`);
console.log(`\nRun ${color.cyan('g push --tags')} to publish`);
} catch (e) {
console.log(color.yellow(`⚠ Git tag failed: ${e.message}`));
}
}
async listTags(showAll) {
const tags = await this.git.tags();
if (!tags.all.length) {
console.log('No tags found.');
return;
}
console.log(color.bold('\nRelease History:'));
// Reverse order (newest first)
// Filter out dev tags unless --all
const filtered = tags.all.filter(t => {
if (showAll) return true;
return !t.includes('-dev.');
}).reverse();
if (filtered.length === 0) {
console.log(color.dim('No stable/beta/rc tags found. Use --all to see dev tags.'));
return;
}
filtered.forEach(t => {
console.log(`- ${t}`);
});
}
async pruneTags() {
Progress.start('Pruning old dev tags...');
const tags = await this.git.tags();
const allTags = tags.all;
// Identify dev tags
const devTags = allTags.filter(t => t.includes('-dev.'));
// Sort them ensuring we keep the newest
// (Using simple string sort works roughly if S4 format is strictly kept,
// but better to rely on S4 parse if possible. For simplicity here, we assume standard S4)
// We want to KEEP the last 10 dev tags.
if (devTags.length <= 10) {
Progress.stop('');
console.log('Fewer than 10 dev tags. No pruning needed.');
return;
}
// Identify tags to delete (older ones)
// S4 strings sort chronologically well enough lexicographically mostly,
// but let's trust the array order returned by git tags usually...
// actually git tags return is arbitrary.
// Let's use our S4 sorter from utility if possible, or just simple sort
// Assuming S4Versioning.getCurrentVersion logic uses sort, we can reuse it?
// Or just simple sort for now.
devTags.sort(); // Lexicographical sort (older dates come first: 2025... < 2026...)
const toKeep = 10;
const toDelete = devTags.slice(0, devTags.length - toKeep);
for (const tag of toDelete) {
await this.git.tag(['-d', tag]);
}
Progress.stop('');
console.log(`${color.green('✔')} Deleted ${toDelete.length} old dev tags.`);
console.log(color.dim('Kept last 10 dev tags + all stable tags.'));
}
async undoLastBump() {
Progress.start('Undoing last version bump...');
// 1. Get current version (the one to delete)
const currentVersion = await this.s4.getCurrentVersion();
if (!currentVersion) {
Progress.stop('');
console.log(color.yellow('No S4 version tags found to undo.'));
return;
}
// 2. Delete the tag
try {
await this.git.tag(['-d', currentVersion]);
Progress.stop('');
console.log(`${color.green('✔')} Deleted tag: ${color.cyan(currentVersion)}`);
} catch (e) {
Progress.error(`Failed to delete tag: ${e.message}`);
return;
}
// 3. Revert .env files to *previous* version
// Now that the latest tag is gone, getCurrentVersion will return the previous one
const previousVersion = await this.s4.getCurrentVersion() || '0.0.0';
if (previousVersion) {
console.log(`Reverting environment to previous version: ${color.dim(previousVersion)}`);
const updatedFiles = this.updateEnvFiles(previousVersion);
if (updatedFiles.length > 0) {
console.log(`${color.green('✔')} Updated ${updatedFiles.join(', ')}`);
}
}
console.log(color.dim('\nNote: If you already pushed the tag, you must verify deletion on remote:'));
console.log(` ${color.cyan(`git push --delete origin ${currentVersion}`)}`);
}
showInfo(versionStr) {
const parsed = S4Versioning.parse(versionStr);
if (!parsed) {
console.log(color.red('Invalid S4 version format'));
return;
}
// Format date/time
const y = parsed.date.substring(0, 4);
const m = parsed.date.substring(4, 6);
const d = parsed.date.substring(6, 8);
const H = parsed.time.substring(0, 2);
const M = parsed.time.substring(2, 4);
const dateObj = new Date(`${y}-${m}-${d}T${H}:${M}:00`);
// 12-hour format logic
let hour = parseInt(H);
const ampm = hour >= 12 ? 'PM' : 'AM';
hour = hour % 12;
hour = hour ? hour : 12; // the hour '0' should be '12'
const time12 = `${hour}:${M} ${ampm}`;
console.log(color.bold('\nS4 Version Analysis:'));
console.log(`Major: ${parsed.major}`);
console.log(`Minor: ${parsed.minor}`);
console.log(`Patch: ${parsed.patch}`);
console.log(`Stage: ${color.magenta(parsed.stage)}`);
console.log(`Build: ${parsed.build}`);
console.log(`Date: ${dateObj.toDateString()}`);
console.log(`Time: ${time12} (${H}:${M})`);
console.log(`Git: ${parsed.branch} (${parsed.commit})`);
}
}
module.exports = { VersionCommand };
-50
View File
@@ -1,50 +0,0 @@
const { color } = require('../utils/colors');
const os = require('os');
class WhoAmICommand {
constructor(git) {
this.git = git;
}
async run() {
const pkg = require('../../../package.json');
// Get SSOT version from S4 (Tags)
const { S4Versioning } = require('../utils/s4');
const s4 = new S4Versioning(this.git);
const version = (await s4.getCurrentVersion()) || pkg.version;
const user = os.userInfo().username;
const hostname = os.hostname();
const platform = os.platform();
const arch = os.arch();
let gitUser = 'unknown';
let gitEmail = 'unknown';
try {
gitUser = (await this.git.raw(['config', 'user.name'])).trim();
gitEmail = (await this.git.raw(['config', 'user.email'])).trim();
} catch (e) { }
console.log(color.bold('\n⚡ GIMS SYSTEM STATUS ⚡\n'));
console.log(`${color.green('SYSTEM_IDENTITY')}`);
console.log(`├─ Host: ${color.cyan(hostname)}`);
console.log(`├─ User: ${color.cyan(user)}`);
console.log(`└─ OS: ${platform} (${arch})`);
console.log(`\n${color.green('GIT_OPERATOR')}`);
console.log(`├─ Name: ${color.cyan(gitUser)}`);
console.log(`└─ Email: ${color.cyan(gitEmail)}`);
console.log(`\n${color.green('TOOL_METRICS')}`);
console.log(`├─ Version: ${color.magenta(version)}`);
console.log(`├─ Engine: Node ${process.version}`);
console.log(`└─ Author: ${pkg.author}`);
console.log('\n' + color.dim('----------------------------------------'));
console.log(color.dim(`Initialized at: ${new Date().toISOString()}`));
}
}
module.exports = { WhoAmICommand };
-213
View File
@@ -1,213 +0,0 @@
const fs = require('fs');
const path = require('path');
const { color } = require('../utils/colors');
/**
* Enhanced configuration management with validation and setup wizard
*/
class ConfigManager {
constructor() {
this.configPaths = [
path.join(process.cwd(), '.gimsrc'),
path.join(process.env.HOME || process.cwd(), '.gimsrc'),
];
}
getDefaults() {
return {
provider: process.env.GIMS_PROVIDER || 'auto',
model: process.env.GIMS_MODEL || '',
conventional: !!(process.env.GIMS_CONVENTIONAL === '1'),
copy: process.env.GIMS_COPY !== '0',
autoStage: process.env.GIMS_AUTO_STAGE === '1',
maxDiffSize: parseInt(process.env.GIMS_MAX_DIFF_SIZE) || 100000,
cacheEnabled: process.env.GIMS_CACHE !== '0',
progressIndicators: process.env.GIMS_PROGRESS !== '0'
};
}
load() {
const defaults = this.getDefaults();
for (const configPath of this.configPaths) {
try {
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, 'utf8');
const config = JSON.parse(content);
return { ...defaults, ...config, _source: configPath };
}
} catch (error) {
console.warn(color.yellow(`Warning: Invalid config file ${configPath}: ${error.message}`));
}
}
return { ...defaults, _source: 'defaults' };
}
save(config, global = false) {
const configPath = global
? path.join(process.env.HOME || process.cwd(), '.gimsrc')
: path.join(process.cwd(), '.gimsrc');
// Remove internal properties
const { _source, ...cleanConfig } = config;
try {
fs.writeFileSync(configPath, JSON.stringify(cleanConfig, null, 2));
return configPath;
} catch (error) {
throw new Error(`Failed to save config: ${error.message}`);
}
}
set(key, value, global = false) {
const config = this.load();
// Validate key
const validKeys = Object.keys(this.getDefaults());
if (!validKeys.includes(key)) {
throw new Error(`Invalid config key: ${key}. Valid keys: ${validKeys.join(', ')}`);
}
// Type conversion
if (typeof this.getDefaults()[key] === 'boolean') {
value = value === 'true' || value === '1';
} else if (typeof this.getDefaults()[key] === 'number') {
value = parseInt(value);
if (isNaN(value)) {
throw new Error(`Invalid number value for ${key}`);
}
}
config[key] = value;
const savedPath = this.save(config, global);
return { key, value, savedPath };
}
get(key) {
const config = this.load();
if (key) {
return config[key];
}
return config;
}
detectProjectType() {
const cwd = process.cwd();
if (fs.existsSync(path.join(cwd, 'package.json'))) {
try {
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
if (pkg.dependencies?.react || pkg.devDependencies?.react) return 'react';
if (pkg.dependencies?.vue || pkg.devDependencies?.vue) return 'vue';
if (pkg.dependencies?.angular || pkg.devDependencies?.angular) return 'angular';
if (pkg.dependencies?.express || pkg.devDependencies?.express) return 'express';
return 'node';
} catch (e) {
// ignore
}
}
if (fs.existsSync(path.join(cwd, 'requirements.txt')) ||
fs.existsSync(path.join(cwd, 'pyproject.toml'))) return 'python';
if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) return 'rust';
if (fs.existsSync(path.join(cwd, 'go.mod'))) return 'go';
if (fs.existsSync(path.join(cwd, 'pom.xml'))) return 'java';
return 'generic';
}
getProjectCommitStyle(projectType) {
const styles = {
react: { conventional: true, types: ['feat', 'fix', 'style', 'refactor', 'test'] },
vue: { conventional: true, types: ['feat', 'fix', 'style', 'refactor', 'test'] },
angular: { conventional: true, types: ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore'] },
node: { conventional: true, types: ['feat', 'fix', 'perf', 'refactor', 'test', 'chore'] },
python: { conventional: false, style: 'descriptive' },
generic: { conventional: false, style: 'simple' }
};
return styles[projectType] || styles.generic;
}
async runSetupWizard() {
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (prompt) => new Promise(resolve => {
rl.question(prompt, answer => {
resolve(answer.trim());
});
});
console.log(color.bold('\n🚀 GIMS Setup Wizard\n'));
// Detect project
const projectType = this.detectProjectType();
const projectStyle = this.getProjectCommitStyle(projectType);
console.log(`Detected project type: ${color.cyan(projectType)}`);
// Provider setup
console.log('\n📡 AI Provider Setup:');
const hasOpenAI = !!process.env.OPENAI_API_KEY;
const hasGemini = !!process.env.GEMINI_API_KEY;
const hasGroq = !!process.env.GROQ_API_KEY;
if (!hasOpenAI && !hasGemini && !hasGroq) {
console.log(color.yellow('No AI providers detected. GIMS will use local heuristics.'));
console.log('\nTo enable AI features, run:');
console.log(` ${color.cyan('g setup --api-key gemini')} # Recommended: Fast & free`);
console.log(` ${color.cyan('g setup --api-key openai')} # High quality`);
console.log(` ${color.cyan('g setup --api-key groq')} # Ultra fast`);
console.log('\nOr set environment variables manually:');
console.log(' - OPENAI_API_KEY (gpt-4o-mini)');
console.log(' - GEMINI_API_KEY (gemini-2.0-flash)');
console.log(' - GROQ_API_KEY (llama-3.3-70b-versatile)');
} else {
console.log('Available providers with default models:');
if (hasGemini) console.log(` ${color.green('✓')} Google Gemini (gemini-2.0-flash)`);
if (hasOpenAI) console.log(` ${color.green('✓')} OpenAI (gpt-4o-mini)`);
if (hasGroq) console.log(` ${color.green('✓')} Groq (llama-3.3-70b-versatile)`);
}
const provider = await question(`\nPreferred provider (auto/openai/gemini/groq/none) [auto]: `) || 'auto';
// Commit style
const conventionalDefault = projectStyle.conventional ? 'y' : 'n';
const conventional = await question(`\nUse Conventional Commits? (y/n) [${conventionalDefault}]: `) || conventionalDefault;
// Other preferences
const autoStage = await question('Auto-stage all changes by default? (y/n) [n]: ') || 'n';
const copy = await question('Copy suggestions to clipboard? (y/n) [y]: ') || 'y';
// Global or local config
const scope = await question('\nSave config globally or for this project? (global/local) [local]: ') || 'local';
rl.close();
// Save configuration
const config = {
provider,
conventional: conventional.toLowerCase() === 'y',
autoStage: autoStage.toLowerCase() === 'y',
copy: copy.toLowerCase() === 'y',
projectType
};
const savedPath = this.save(config, scope === 'global');
console.log(`\n${color.green('✓')} Configuration saved to: ${savedPath}`);
console.log('\nYou\'re all set! Try running:');
console.log(` ${color.cyan('g status')} - See enhanced git status`);
console.log(` ${color.cyan('g o')} - AI commit and push`);
console.log(` ${color.cyan('g s')} - Get AI suggestions`);
return config;
}
}
module.exports = { ConfigManager };
-302
View File
@@ -1,302 +0,0 @@
const { color } = require('../utils/colors');
const { Intelligence } = require('../utils/intelligence');
/**
* Enhanced git analysis and insights
*/
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),
sessionStats,
branchContext,
};
} catch (error) {
throw new Error(`Failed to get git status: ${error.message}`);
}
}
generateStatusSummary(status) {
const files = Array.isArray(status.files) ? status.files : [];
const staged = Array.isArray(status.staged) ? status.staged : [];
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 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 using `g split` to break into smaller commits');
}
// Check for test files
const testFiles = files.filter(f => {
const fileName = String(f);
return fileName.includes('.test.') || fileName.includes('.spec.') || fileName.includes('__tests__');
});
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);
return fileName.includes('config') || fileName.includes('.json') || fileName.includes('.yml') || fileName.includes('.yaml');
});
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;
}
async analyzeCommitHistory(limit = 10) {
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))],
averageMessageLength: commits.reduce((sum, c) => sum + c.message.length, 0) / commits.length,
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 };
}
}
analyzeRecentActivity(commits) {
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,
frequency: weeklyCommits.length > 0 ? 'active' : 'quiet'
};
}
async getChangeComplexity(diff) {
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 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,
total: additions + deletions
};
}
formatStatusOutput(enhancedStatus) {
const {
files = [],
staged = [],
modified = [],
created = [],
deleted = [],
not_added = [],
insights = [],
summary = 'Unknown status',
sessionStats = {},
branchContext = {},
} = enhancedStatus;
let output = '';
// 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 => {
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 => {
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 => {
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 => {
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 => {
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('💡 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 };
-31
View File
@@ -1,31 +0,0 @@
/**
* ANSI color utilities without external dependencies
*/
const color = {
green: (s) => `\x1b[32m${s}\x1b[0m`,
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
red: (s) => `\x1b[31m${s}\x1b[0m`,
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'
};
module.exports = { color };
-421
View File
@@ -1,421 +0,0 @@
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 };
-118
View File
@@ -1,118 +0,0 @@
const { color } = require('./colors');
/**
* Progress indicators and user feedback utilities
*/
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;
process.stdout.write(`\r${message} ${this.spinner[this.current]}`);
}, 100);
}
static stop(finalMessage = '') {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
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) {
console.log(`${color.green('✓')} ${message}`);
}
static warning(message) {
console.log(`${color.yellow('⚠')} ${message}`);
}
static error(message) {
console.log(`${color.red('✗')} ${message}`);
}
static info(message) {
console.log(`${color.cyan('')} ${message}`);
}
static step(step, total, message) {
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 };
-221
View File
@@ -1,221 +0,0 @@
const simpleGit = require('simple-git');
const path = require('path');
const fs = require('fs');
class S4Versioning {
constructor(git) {
this.git = git || simpleGit();
}
// Regex for parsing S4 version
static get REGEX() {
return /^([0-9]+)\.([0-9]+)\.([0-9]+)-([a-z]+)\.([0-9]+)\+([0-9]{8})\.([0-9]{4})\.([0-9a-f]+)\.([a-zA-Z0-9._\/-]+)$/;
}
/**
* Parse an S4 version string into components
*/
static parse(versionStr) {
if (!versionStr) return null;
const match = versionStr.match(S4Versioning.REGEX);
if (!match) return null;
return {
major: parseInt(match[1]),
minor: parseInt(match[2]),
patch: parseInt(match[3]),
stage: match[4],
build: parseInt(match[5]),
date: match[6],
time: match[7],
commit: match[8],
branch: match[9],
original: versionStr
};
}
/**
* Generate S4 version string from components
*/
static stringify(components) {
const { major, minor, patch, stage, build, date, time, commit, branch } = components;
return `${major}.${minor}.${patch}-${stage}.${build}+${date}.${time}.${commit}.${branch}`;
}
/**
* Get the latest S4 version from Git Tags
*/
async getCurrentVersion() {
try {
// Get all tags
const tags = await this.git.tags();
if (!tags.all || tags.all.length === 0) {
return null; // No tags yet
}
// Filter and sort S4 tags
// Sorting strategy: We rely on the date+time in the S4 string for chronological order
// but S4 strings format puts SemVer first.
// Let's parse all valid S4 tags and sort them.
const parsedTags = tags.all
.map(t => ({ tag: t, parsed: S4Versioning.parse(t) }))
.filter(item => item.parsed !== null);
if (parsedTags.length === 0) return null;
// Sort logic:
// 1. Major.Minor.Patch
// 2. Date.Time
// 3. Build
parsedTags.sort((a, b) => {
const pA = a.parsed;
const pB = b.parsed;
if (pA.major !== pB.major) return pA.major - pB.major;
if (pA.minor !== pB.minor) return pA.minor - pB.minor;
if (pA.patch !== pB.patch) return pA.patch - pB.patch;
// Compare Date (YYYYMMDD)
const dateA = parseInt(pA.date);
const dateB = parseInt(pB.date);
if (dateA !== dateB) return dateA - dateB;
// Compare Time (HHMM)
const timeA = parseInt(pA.time);
const timeB = parseInt(pB.time);
if (timeA !== timeB) return timeA - timeB;
return pA.build - pB.build;
});
// Return the latest
return parsedTags[parsedTags.length - 1].tag;
} catch (e) {
return null;
}
}
/**
* Migrate a standard SemVer or empty version to S4
*/
async migrate(currentVersion) {
// Basic SemVer parsing (loose)
const semVerMatch = currentVersion.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([a-zA-Z0-9.]+))?/);
let base = {
major: 0, minor: 0, patch: 0, stage: 'dev', build: 1
};
if (semVerMatch) {
base.major = parseInt(semVerMatch[1]);
base.minor = parseInt(semVerMatch[2]);
base.patch = parseInt(semVerMatch[3]);
// Attempt to salvage stage if present, else default to dev
if (semVerMatch[4]) {
const parts = semVerMatch[4].split('.');
const possibleStage = parts.find(p => /^[a-z]+$/.test(p));
if (possibleStage) base.stage = possibleStage;
}
}
const ctx = await this.getContext();
return S4Versioning.stringify({ ...base, ...ctx });
}
/**
* Get current Git and Time context
*/
async getContext() {
const now = new Date();
const pad = (n) => n.toString().padStart(2, '0');
const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
const time = `${pad(now.getHours())}${pad(now.getMinutes())}`;
let commit = '0000000';
let branch = 'unknown';
try {
const isRepo = await this.git.checkIsRepo();
if (isRepo) {
commit = (await this.git.revparse(['--short=7', 'HEAD'])).trim();
branch = (await this.git.revparse(['--abbrev-ref', 'HEAD'])).trim();
}
} catch (e) {
// Ignore git errors, stick to defaults
}
// Sanitize branch name for S4 compliance
branch = branch.replace(/[^a-zA-Z0-9._\/-]/g, '-');
return { date, time, commit, branch };
}
/**
* Calculate next version based on bump type
*/
async bump(currentStr, type = 'auto', stageVal = null) {
let parsed = S4Versioning.parse(currentStr);
// If not S4, migrate first
if (!parsed) {
const migrated = await this.migrate(currentStr);
parsed = S4Versioning.parse(migrated);
}
const ctx = await this.getContext();
const next = { ...parsed, ...ctx }; // Update context (date, time, commit, branch)
// Handle stage change
if (stageVal && stageVal !== parsed.stage) {
next.stage = stageVal;
next.build = 1;
}
// Logic for bumping
switch (type) {
case 'major':
next.major++;
next.minor = 0;
next.patch = 0;
next.build = 1;
break;
case 'minor':
next.minor++;
next.patch = 0;
next.build = 1;
break;
case 'patch':
next.patch++;
next.build = 1;
break;
case 'auto':
// Smart Auto Logic
// 1. If Date Changed, reset build to 1
if (parsed.date !== ctx.date) {
next.build = 1;
}
// 2. If Date is SAME
else {
// If Commit Changed -> new build
if (parsed.commit !== ctx.commit) {
next.build = parsed.build + 1;
}
// If same commit & same date, usually we don't need a new tag
// unless user forces it.
// For safety, increment build to ensure uniqueness if they insisted on running `g v`.
else {
next.build = parsed.build + 1;
}
}
break;
}
return S4Versioning.stringify(next);
}
}
module.exports = { S4Versioning };
-75
View File
@@ -1,75 +0,0 @@
[
{
"type": "function",
"function": {
"name": "get_status",
"description": "Get enhanced git status with AI insights, file modification stats, and branch context. Use this to understand the current state of the repository.",
"parameters": {
"type": "object",
"properties": {}
}
}
},
{
"type": "function",
"function": {
"name": "analyze_history",
"description": "Analyze recent commit history, including authors, conventions, and activity patterns.",
"parameters": {
"type": "object",
"properties": {
"limit": {
"type": "number",
"description": "Number of commits to analyze (default: 10)"
}
}
}
}
},
{
"type": "function",
"function": {
"name": "version_info",
"description": "Get current project version information using S4 versioning (Semantic + Date/Time).",
"parameters": {
"type": "object",
"properties": {}
}
}
},
{
"type": "function",
"function": {
"name": "generate_commit_message",
"description": "Generate a high-quality commit message based on currently staged changes.",
"parameters": {
"type": "object",
"properties": {
"context": {
"type": "string",
"description": "Optional context or user instruction for the message generation"
}
}
}
}
},
{
"type": "function",
"function": {
"name": "run_git_command",
"description": "Execute a raw git command safely. Use this for standard git operations not covered by other tools.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The full git command to run (e.g., 'log -n 5', 'branch --list')"
}
},
"required": [
"command"
]
}
}
}
]
-94
View File
@@ -1,94 +0,0 @@
#!/usr/bin/env node
/**
* GIMS MCP Server
*
* Exposes GIMS capabilities as Model Context Protocol (MCP) tools.
* Usage: node mcp/index.js
*/
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
const {
CallToolRequestSchema,
ListToolsRequestSchema,
} = require("@modelcontextprotocol/sdk/types.js");
const { tools } = require('../bin/lib/ai/interface');
// Create the server
const server = new Server(
{
name: "gims-mcp-server",
version: require('../package.json').version,
},
{
capabilities: {
tools: {},
},
}
);
/**
* Handler for listing available tools
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: tools.map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
};
});
/**
* Handler for calling tools
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const toolName = request.params.name;
const toolArgs = request.params.arguments;
const tool = tools.find((t) => t.name === toolName);
if (!tool) {
throw new Error(`Unknown tool: ${toolName}`);
}
try {
const result = await tool.handler(toolArgs);
// MCP expects content to be an array of text/image objects
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error executing ${toolName}: ${error.message}`,
},
],
isError: true,
};
}
});
/**
* Start the server
*/
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("GIMS MCP Server running on stdio");
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});
-1932
View File
File diff suppressed because it is too large Load Diff
-47
View File
@@ -1,47 +0,0 @@
{
"name": "gims",
"version": "0.9.2",
"description": "Git Made Simple AIpowered git helper with smart insights, stats & code review",
"author": "S41R4J",
"license": "MIT",
"bin": {
"gims": "bin/gims.js",
"g": "bin/gims.js"
},
"keywords": [
"git",
"cli",
"ai",
"commit",
"gpt",
"gemini",
"openai",
"developer-tools",
"version-control"
],
"repository": {
"type": "git",
"url": "git+https://github.com/s41r4j/gims.git"
},
"bugs": {
"url": "https://github.com/s41r4j/gims/issues"
},
"homepage": "https://github.com/s41r4j/gims#readme",
"main": "bin/gims.js",
"engines": {
"node": ">=18.18.0"
},
"dependencies": {
"@google/genai": "1.5.1",
"@modelcontextprotocol/sdk": "^1.25.3",
"clipboardy": "^3.0.0",
"commander": "^11.1.0",
"openai": "^4.0.0",
"simple-git": "^3.19.1"
},
"type": "commonjs",
"scripts": {
"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'\""
}
}
-28
View File
@@ -1,28 +0,0 @@
const { OpenAI } = require('openai');
const process = require('process');
async function testOpenAI() {
console.log('Testing OpenAI connection...');
console.log('API Key present:', !!process.env.OPENAI_API_KEY);
console.log('Model:', 'gpt-5.2-2025-12-11');
try {
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// Log the base URL to see if it's custom
console.log('Base URL:', openai.baseURL);
const response = await openai.chat.completions.create({
model: 'gpt-5.2-2025-12-11',
messages: [{ role: 'user', content: 'Say hello' }],
});
console.log('Success!');
console.log('Content:', response.choices[0].message.content);
} catch (error) {
console.error('ERROR OCCURRED:');
console.error(error);
}
}
testOpenAI();
-18
View File
@@ -1,18 +0,0 @@
const fs = require('fs');
const path = require('path');
const { tools } = require('../bin/lib/ai/interface');
const schema = tools.map(tool => ({
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: tool.inputSchema
}
}));
const outputPath = path.resolve(__dirname, '../bin/tools-schema.json');
fs.writeFileSync(outputPath, JSON.stringify(schema, null, 2));
console.log(`✅ Generated OpenAI-compatible tool schemas at: ${outputPath}`);
console.log(`Total tools: ${tools.length}`);