Compare commits
56 Commits
db9e09c06f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a54d529f31 | |||
| 807d5bcb20 | |||
| ff4232b212 | |||
| b53f06b799 | |||
| c3e0d4ab03 | |||
| 00217a6ac7 | |||
| bd906885c2 | |||
| 5357cbf683 | |||
| 593421d9a3 | |||
| 56d1631e95 | |||
| 9f7522039e | |||
| 12021869a9 | |||
| 14e5dfa754 | |||
| ffade4e0ab | |||
| bf9b8a010b | |||
| bc10b3a074 | |||
| af80f34310 | |||
| c6fdbac8ce | |||
| 9eaa68165e | |||
| 687dd1920f | |||
| 90559c15a1 | |||
| 5d8a92ceb8 | |||
| e5e4713384 | |||
| 4a30dd018f | |||
| 5846c77f51 | |||
| 260577f1f1 | |||
| f2518b7db2 | |||
| ce005adf22 | |||
| 6832a598cb | |||
| a1b0d80fed | |||
| 7d5e6938b6 | |||
| 25bbb32d79 | |||
| f7c07e1af1 | |||
| c0f4cd97eb | |||
| 8de40357a6 | |||
| b3ec6c74e8 | |||
| 9c2bd60cc5 | |||
| f9d979a54e | |||
| 83ae097494 | |||
| db324a4128 | |||
| 0751450a70 | |||
| d73cf374a7 | |||
| 6211fa1b80 | |||
| 00a1ebf386 | |||
| a60fd01444 | |||
| 71a398820a | |||
| b082eb2b7a | |||
| 6b05a9a2c4 | |||
| 78aea896a2 | |||
| 473b2ba461 | |||
| 566b9ff994 | |||
| cccd5173ea | |||
| 9f801c0299 | |||
| 816029e073 | |||
| 79fd2748c0 | |||
| afccff8637 |
+59
@@ -0,0 +1,59 @@
|
||||
# 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
@@ -0,0 +1,23 @@
|
||||
# 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
@@ -0,0 +1,189 @@
|
||||
# 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
|
||||
@@ -1,18 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 s41r4j
|
||||
Copyright (c) 2025 Sairaj Jawalikar
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights 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
|
||||
following conditions:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
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 following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -1,2 +1,385 @@
|
||||
# gims
|
||||
<div align="center">
|
||||
|
||||
<!--
|
||||
# GIMS - Git Made Simple
|
||||
-->
|
||||
|
||||
<img width="1658" height="609" alt="gims" src="https://github.com/user-attachments/assets/fd4ae515-9674-4cc2-8ea0-39d21e82e274" />
|
||||
|
||||
[](https://npmjs.org/package/gims)
|
||||
[](https://npmjs.org/package/gims)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/s41r4j/gims)
|
||||
[](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>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 6.9 MiB |
Executable
+1954
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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 };
|
||||
@@ -0,0 +1,312 @@
|
||||
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 };
|
||||
@@ -0,0 +1,241 @@
|
||||
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 };
|
||||
@@ -0,0 +1,276 @@
|
||||
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 };
|
||||
@@ -0,0 +1,50 @@
|
||||
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 };
|
||||
@@ -0,0 +1,213 @@
|
||||
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 };
|
||||
@@ -0,0 +1,302 @@
|
||||
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 };
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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 };
|
||||
@@ -0,0 +1,421 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Intelligence utilities for smart git operations
|
||||
*/
|
||||
class Intelligence {
|
||||
constructor(git) {
|
||||
this.git = git;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file type emoji based on file extension
|
||||
*/
|
||||
static getFileEmoji(filename) {
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
const name = path.basename(filename).toLowerCase();
|
||||
|
||||
// Special files
|
||||
if (name === 'package.json' || name === 'package-lock.json') return '📦';
|
||||
if (name === 'readme.md' || name === 'readme') return '📚';
|
||||
if (name === '.gitignore') return '🙈';
|
||||
if (name === '.env' || name.startsWith('.env.')) return '🔐';
|
||||
if (name === 'dockerfile' || name === 'docker-compose.yml') return '🐳';
|
||||
if (name.includes('config') || name.includes('rc')) return '⚙️';
|
||||
if (name === 'license' || name === 'license.md') return '📜';
|
||||
|
||||
// Test files
|
||||
if (filename.includes('.test.') || filename.includes('.spec.') || filename.includes('__tests__')) return '🧪';
|
||||
|
||||
// By extension
|
||||
const emojiMap = {
|
||||
'.js': '📄', '.jsx': '⚛️', '.ts': '📘', '.tsx': '⚛️',
|
||||
'.css': '🎨', '.scss': '🎨', '.sass': '🎨', '.less': '🎨',
|
||||
'.html': '🌐', '.htm': '🌐',
|
||||
'.json': '📋', '.yaml': '📋', '.yml': '📋', '.toml': '📋',
|
||||
'.md': '📝', '.mdx': '📝', '.txt': '📄',
|
||||
'.py': '🐍', '.rb': '💎', '.go': '🔷', '.rs': '🦀', '.java': '☕',
|
||||
'.sh': '🐚', '.bash': '🐚', '.zsh': '🐚',
|
||||
'.sql': '🗃️', '.graphql': '🔗', '.gql': '🔗',
|
||||
'.png': '🖼️', '.jpg': '🖼️', '.jpeg': '🖼️', '.gif': '🖼️', '.svg': '🎯',
|
||||
'.mp3': '🎵', '.wav': '🎵', '.mp4': '🎬', '.mov': '🎬',
|
||||
'.zip': '📦', '.tar': '📦', '.gz': '📦',
|
||||
'.lock': '🔒',
|
||||
};
|
||||
|
||||
return emojiMap[ext] || '📄';
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze user's commit patterns from history
|
||||
*/
|
||||
async analyzeCommitPatterns(limit = 50) {
|
||||
try {
|
||||
const log = await this.git.log({ maxCount: limit });
|
||||
const commits = log.all;
|
||||
|
||||
if (commits.length === 0) {
|
||||
return { hasHistory: false };
|
||||
}
|
||||
|
||||
// Analyze patterns
|
||||
const conventionalPattern = /^(feat|fix|docs|style|refactor|test|chore|perf|build|ci|revert)(\(.+\))?:/i;
|
||||
const conventionalCommits = commits.filter(c => conventionalPattern.test(c.message));
|
||||
|
||||
// Calculate average message length
|
||||
const avgLength = Math.round(commits.reduce((sum, c) => sum + c.message.split('\n')[0].length, 0) / commits.length);
|
||||
|
||||
// Detect common scopes
|
||||
const scopes = {};
|
||||
commits.forEach(c => {
|
||||
const match = c.message.match(/^\w+\(([^)]+)\)/);
|
||||
if (match) {
|
||||
scopes[match[1]] = (scopes[match[1]] || 0) + 1;
|
||||
}
|
||||
});
|
||||
const topScopes = Object.entries(scopes)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 5)
|
||||
.map(([scope]) => scope);
|
||||
|
||||
// Detect if user uses emojis
|
||||
const emojiPattern = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]/u;
|
||||
const usesEmojis = commits.some(c => emojiPattern.test(c.message));
|
||||
|
||||
// Detect commit message style (imperative vs past tense)
|
||||
const imperativeWords = ['add', 'fix', 'update', 'remove', 'change', 'implement', 'refactor'];
|
||||
const pastTenseWords = ['added', 'fixed', 'updated', 'removed', 'changed', 'implemented', 'refactored'];
|
||||
|
||||
let imperativeCount = 0;
|
||||
let pastTenseCount = 0;
|
||||
commits.forEach(c => {
|
||||
const firstWord = c.message.split(/[\s(:]/)[0].toLowerCase();
|
||||
if (imperativeWords.includes(firstWord)) imperativeCount++;
|
||||
if (pastTenseWords.includes(firstWord)) pastTenseCount++;
|
||||
});
|
||||
|
||||
return {
|
||||
hasHistory: true,
|
||||
totalAnalyzed: commits.length,
|
||||
conventionalRatio: (conventionalCommits.length / commits.length * 100).toFixed(0),
|
||||
usesConventional: conventionalCommits.length > commits.length * 0.5,
|
||||
avgMessageLength: avgLength,
|
||||
topScopes,
|
||||
usesEmojis,
|
||||
style: imperativeCount >= pastTenseCount ? 'imperative' : 'past-tense',
|
||||
authors: [...new Set(commits.map(c => c.author_name))],
|
||||
};
|
||||
} catch (error) {
|
||||
return { hasHistory: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect semantic meaning of changes
|
||||
*/
|
||||
async detectSemanticChanges(diff) {
|
||||
const changes = {
|
||||
type: 'misc',
|
||||
labels: [],
|
||||
suggestions: [],
|
||||
breakingChange: false,
|
||||
};
|
||||
|
||||
if (!diff || !diff.trim()) return changes;
|
||||
|
||||
const lines = diff.split('\n');
|
||||
const addedLines = lines.filter(l => l.startsWith('+') && !l.startsWith('+++'));
|
||||
const removedLines = lines.filter(l => l.startsWith('-') && !l.startsWith('---'));
|
||||
|
||||
// Detect breaking changes
|
||||
if (removedLines.some(l => l.includes('export ') && l.includes('function ')) ||
|
||||
removedLines.some(l => l.includes('module.exports'))) {
|
||||
changes.labels.push('⚠️ Potential breaking change');
|
||||
changes.breakingChange = true;
|
||||
}
|
||||
|
||||
// Detect new features
|
||||
if (addedLines.some(l => l.includes('export ') && l.includes('function ')) ||
|
||||
addedLines.some(l => l.includes('class ')) ||
|
||||
addedLines.some(l => l.includes('async function '))) {
|
||||
changes.labels.push('✨ New functionality');
|
||||
changes.type = 'feat';
|
||||
}
|
||||
|
||||
// Detect bug fixes
|
||||
if (addedLines.some(l => l.includes('catch') || l.includes('try {')) ||
|
||||
addedLines.some(l => l.includes('|| null') || l.includes('?? ') || l.includes('?.'))) {
|
||||
changes.labels.push('🐛 Error handling');
|
||||
if (changes.type === 'misc') changes.type = 'fix';
|
||||
}
|
||||
|
||||
// Detect refactoring
|
||||
if (addedLines.length > 20 && removedLines.length > 20 &&
|
||||
Math.abs(addedLines.length - removedLines.length) < 10) {
|
||||
changes.labels.push('♻️ Refactoring');
|
||||
if (changes.type === 'misc') changes.type = 'refactor';
|
||||
}
|
||||
|
||||
// Detect documentation
|
||||
if (addedLines.some(l => l.includes('/**') || l.includes('* @') || l.includes('// '))) {
|
||||
changes.labels.push('📝 Documentation');
|
||||
if (changes.type === 'misc') changes.type = 'docs';
|
||||
}
|
||||
|
||||
// Detect test changes
|
||||
if (diff.includes('.test.') || diff.includes('.spec.') ||
|
||||
addedLines.some(l => l.includes('describe(') || l.includes('it(') || l.includes('test('))) {
|
||||
changes.labels.push('🧪 Tests');
|
||||
if (changes.type === 'misc') changes.type = 'test';
|
||||
}
|
||||
|
||||
// Detect dependency changes
|
||||
if (diff.includes('package.json') && (diff.includes('"dependencies"') || diff.includes('"devDependencies"'))) {
|
||||
changes.labels.push('📦 Dependencies');
|
||||
}
|
||||
|
||||
// Detect style/formatting
|
||||
if (addedLines.every(l => l.trim().length < 3 || l.includes(' ') || l === '+')) {
|
||||
changes.labels.push('🎨 Formatting');
|
||||
if (changes.type === 'misc') changes.type = 'style';
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggest how to split a large changeset
|
||||
*/
|
||||
async suggestCommitSplit(status) {
|
||||
const files = status.files || [];
|
||||
if (files.length < 5) return null;
|
||||
|
||||
const groups = {
|
||||
tests: [],
|
||||
config: [],
|
||||
docs: [],
|
||||
styles: [],
|
||||
core: [],
|
||||
};
|
||||
|
||||
files.forEach(file => {
|
||||
const f = typeof file === 'string' ? file : file.path;
|
||||
if (!f) return;
|
||||
|
||||
if (f.includes('.test.') || f.includes('.spec.') || f.includes('__tests__')) {
|
||||
groups.tests.push(f);
|
||||
} else if (f.includes('config') || f.endsWith('.json') || f.endsWith('.yml') || f.endsWith('.yaml') || f.includes('rc')) {
|
||||
groups.config.push(f);
|
||||
} else if (f.endsWith('.md') || f.includes('docs/') || f.includes('README')) {
|
||||
groups.docs.push(f);
|
||||
} else if (f.endsWith('.css') || f.endsWith('.scss') || f.endsWith('.sass')) {
|
||||
groups.styles.push(f);
|
||||
} else {
|
||||
groups.core.push(f);
|
||||
}
|
||||
});
|
||||
|
||||
const suggestions = [];
|
||||
if (groups.tests.length > 0) suggestions.push({ type: 'test', files: groups.tests, message: 'test: add/update tests' });
|
||||
if (groups.config.length > 0) suggestions.push({ type: 'chore', files: groups.config, message: 'chore: update configuration' });
|
||||
if (groups.docs.length > 0) suggestions.push({ type: 'docs', files: groups.docs, message: 'docs: update documentation' });
|
||||
if (groups.styles.length > 0) suggestions.push({ type: 'style', files: groups.styles, message: 'style: update styles' });
|
||||
if (groups.core.length > 0) suggestions.push({ type: 'feat', files: groups.core, message: 'feat: update core functionality' });
|
||||
|
||||
return suggestions.length > 1 ? suggestions : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session statistics
|
||||
*/
|
||||
async getSessionStats() {
|
||||
try {
|
||||
const log = await this.git.log({ maxCount: 1 });
|
||||
const lastCommit = log.all[0];
|
||||
|
||||
let timeSinceLastCommit = 'No commits yet';
|
||||
let lastCommitMessage = null;
|
||||
|
||||
if (lastCommit) {
|
||||
const lastCommitDate = new Date(lastCommit.date);
|
||||
const now = new Date();
|
||||
const diffMs = now - lastCommitDate;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffDays > 0) {
|
||||
timeSinceLastCommit = `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
|
||||
} else if (diffHours > 0) {
|
||||
timeSinceLastCommit = `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
||||
} else if (diffMins > 0) {
|
||||
timeSinceLastCommit = `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
|
||||
} else {
|
||||
timeSinceLastCommit = 'Just now';
|
||||
}
|
||||
|
||||
lastCommitMessage = lastCommit.message.split('\n')[0];
|
||||
}
|
||||
|
||||
// Get today's commits
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const todayLog = await this.git.log({ '--since': today.toISOString() });
|
||||
const todayCommits = todayLog.all.length;
|
||||
|
||||
return {
|
||||
timeSinceLastCommit,
|
||||
lastCommitMessage,
|
||||
commitsToday: todayCommits,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
timeSinceLastCommit: 'Unknown',
|
||||
lastCommitMessage: null,
|
||||
commitsToday: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect branch context for smarter commit messages
|
||||
*/
|
||||
async detectBranchContext() {
|
||||
try {
|
||||
const branch = await this.git.branch();
|
||||
const currentBranch = branch.current;
|
||||
|
||||
// Common patterns: feat/xyz, fix/xyz, feature/xyz, bugfix/xyz, hotfix/xyz
|
||||
const patterns = [
|
||||
{ regex: /^feat(?:ure)?\/(.+)$/i, type: 'feat', scope: null },
|
||||
{ regex: /^fix\/(.+)$/i, type: 'fix', scope: null },
|
||||
{ regex: /^bugfix\/(.+)$/i, type: 'fix', scope: null },
|
||||
{ regex: /^hotfix\/(.+)$/i, type: 'fix', scope: null },
|
||||
{ regex: /^docs?\/(.+)$/i, type: 'docs', scope: null },
|
||||
{ regex: /^refactor\/(.+)$/i, type: 'refactor', scope: null },
|
||||
{ regex: /^test\/(.+)$/i, type: 'test', scope: null },
|
||||
{ regex: /^chore\/(.+)$/i, type: 'chore', scope: null },
|
||||
{ regex: /^style\/(.+)$/i, type: 'style', scope: null },
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = currentBranch.match(pattern.regex);
|
||||
if (match) {
|
||||
// Extract potential scope and description from branch name
|
||||
const branchPart = match[1].replace(/-/g, ' ').replace(/_/g, ' ');
|
||||
return {
|
||||
branch: currentBranch,
|
||||
type: pattern.type,
|
||||
description: branchPart,
|
||||
detected: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check for issue references like PROJ-123 or #123
|
||||
const issueMatch = currentBranch.match(/([A-Z]+-\d+|#\d+)/i);
|
||||
if (issueMatch) {
|
||||
return {
|
||||
branch: currentBranch,
|
||||
issueRef: issueMatch[1],
|
||||
detected: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
branch: currentBranch,
|
||||
detected: false,
|
||||
};
|
||||
} catch (error) {
|
||||
return { branch: 'unknown', detected: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get commit statistics for the user
|
||||
*/
|
||||
async getCommitStats(days = 30) {
|
||||
try {
|
||||
const since = new Date();
|
||||
since.setDate(since.getDate() - days);
|
||||
|
||||
const log = await this.git.log({ '--since': since.toISOString() });
|
||||
const commits = log.all;
|
||||
|
||||
if (commits.length === 0) {
|
||||
return { totalCommits: 0, hasData: false };
|
||||
}
|
||||
|
||||
// Group by day
|
||||
const byDay = {};
|
||||
commits.forEach(c => {
|
||||
const day = new Date(c.date).toLocaleDateString();
|
||||
byDay[day] = (byDay[day] || 0) + 1;
|
||||
});
|
||||
|
||||
// Calculate streaks
|
||||
const sortedDays = Object.keys(byDay).sort((a, b) => new Date(b) - new Date(a));
|
||||
let currentStreak = 0;
|
||||
let longestStreak = 0;
|
||||
let tempStreak = 0;
|
||||
|
||||
const today = new Date().toLocaleDateString();
|
||||
const yesterday = new Date(Date.now() - 86400000).toLocaleDateString();
|
||||
|
||||
// Check if today or yesterday has commits
|
||||
if (byDay[today] || byDay[yesterday]) {
|
||||
sortedDays.forEach((day, i) => {
|
||||
if (i === 0) {
|
||||
tempStreak = 1;
|
||||
} else {
|
||||
const prevDate = new Date(sortedDays[i - 1]);
|
||||
const currDate = new Date(day);
|
||||
const diffDays = Math.round((prevDate - currDate) / 86400000);
|
||||
if (diffDays === 1) {
|
||||
tempStreak++;
|
||||
} else {
|
||||
if (tempStreak > longestStreak) longestStreak = tempStreak;
|
||||
tempStreak = 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (tempStreak > longestStreak) longestStreak = tempStreak;
|
||||
currentStreak = byDay[today] ? tempStreak : (byDay[yesterday] ? tempStreak : 0);
|
||||
}
|
||||
|
||||
// Commit types breakdown
|
||||
const typeBreakdown = { feat: 0, fix: 0, docs: 0, style: 0, refactor: 0, test: 0, chore: 0, other: 0 };
|
||||
const conventionalPattern = /^(feat|fix|docs|style|refactor|test|chore|perf|build|ci|revert)/i;
|
||||
|
||||
commits.forEach(c => {
|
||||
const match = c.message.match(conventionalPattern);
|
||||
if (match) {
|
||||
const type = match[1].toLowerCase();
|
||||
if (typeBreakdown.hasOwnProperty(type)) {
|
||||
typeBreakdown[type]++;
|
||||
} else {
|
||||
typeBreakdown.other++;
|
||||
}
|
||||
} else {
|
||||
typeBreakdown.other++;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
hasData: true,
|
||||
totalCommits: commits.length,
|
||||
daysActive: Object.keys(byDay).length,
|
||||
avgPerDay: (commits.length / days).toFixed(1),
|
||||
currentStreak,
|
||||
longestStreak,
|
||||
typeBreakdown,
|
||||
topDay: Object.entries(byDay).sort((a, b) => b[1] - a[1])[0],
|
||||
};
|
||||
} catch (error) {
|
||||
return { hasData: false, error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Intelligence };
|
||||
@@ -0,0 +1,118 @@
|
||||
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 };
|
||||
@@ -0,0 +1,221 @@
|
||||
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 };
|
||||
@@ -0,0 +1,75 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/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);
|
||||
});
|
||||
Generated
+1932
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "gims",
|
||||
"version": "0.9.2",
|
||||
"description": "Git Made Simple – AI‑powered 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'\""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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();
|
||||
@@ -0,0 +1,18 @@
|
||||
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}`);
|
||||
Reference in New Issue
Block a user